From a47443ed1ceff9aab6d8183f81b7b7179cd5ae73 Mon Sep 17 00:00:00 2001 From: Astra Date: Tue, 26 Nov 2024 19:26:58 +0900 Subject: [PATCH 001/268] Snooze, timeouts, and dismissing from the locked state --- applications/services/desktop/desktop.c | 2 +- .../desktop/scenes/desktop_scene_locked.c | 2 +- applications/services/gui/gui.c | 28 ++++++++++++--- applications/services/gui/gui.h | 18 ++++++++-- applications/services/gui/gui_i.h | 2 +- .../clock_settings/clock_settings_alarm.c | 34 +++++++++++++++++++ targets/f18/api_symbols.csv | 6 ++-- targets/f7/api_symbols.csv | 6 ++-- 8 files changed, 84 insertions(+), 14 deletions(-) diff --git a/applications/services/desktop/desktop.c b/applications/services/desktop/desktop.c index 1132760d5..9b7310370 100644 --- a/applications/services/desktop/desktop.c +++ b/applications/services/desktop/desktop.c @@ -417,7 +417,7 @@ void desktop_unlock(Desktop* desktop) { view_port_enabled_set(desktop->lock_icon_viewport, false); Gui* gui = furi_record_open(RECORD_GUI); - gui_set_lockdown(gui, false); + gui_remove_lockdown(gui); furi_record_close(RECORD_GUI); desktop_view_locked_unlock(desktop->locked_view); scene_manager_search_and_switch_to_previous_scene(desktop->scene_manager, DesktopSceneMain); diff --git a/applications/services/desktop/scenes/desktop_scene_locked.c b/applications/services/desktop/scenes/desktop_scene_locked.c index e7eeebca6..8e0c5bfc1 100644 --- a/applications/services/desktop/scenes/desktop_scene_locked.c +++ b/applications/services/desktop/scenes/desktop_scene_locked.c @@ -45,7 +45,7 @@ void desktop_scene_locked_on_enter(void* context) { if(state == DesktopSceneLockedStateFirstEnter) { view_port_enabled_set(desktop->lock_icon_viewport, true); Gui* gui = furi_record_open(RECORD_GUI); - gui_set_lockdown(gui, true); + gui_set_lockdown(gui); furi_record_close(RECORD_GUI); if(desktop_pin_code_is_set()) { diff --git a/applications/services/gui/gui.c b/applications/services/gui/gui.c index 24ea57bc0..085507d1d 100644 --- a/applications/services/gui/gui.c +++ b/applications/services/gui/gui.c @@ -249,7 +249,7 @@ static void gui_redraw(Gui* gui) { canvas_reset(gui->canvas); - if(gui->lockdown) { + if(gui_is_lockdown(gui)) { gui_redraw_desktop(gui); bool need_attention = (gui_view_port_find_enabled(gui->layers[GuiLayerWindow]) != 0 || @@ -299,7 +299,7 @@ static void gui_input(Gui* gui, InputEvent* input_event) { ViewPort* view_port = NULL; - if(gui->lockdown) { + if(gui_is_lockdown(gui)) { view_port = gui_view_port_find_enabled(gui->layers[GuiLayerDesktop]); } else { view_port = gui_view_port_find_enabled(gui->layers[GuiLayerFullscreen]); @@ -484,17 +484,35 @@ size_t gui_get_framebuffer_size(const Gui* gui) { return canvas_get_buffer_size(gui->canvas); } -void gui_set_lockdown(Gui* gui, bool lockdown) { +void gui_set_lockdown(Gui* gui) { furi_check(gui); gui_lock(gui); - gui->lockdown = lockdown; + FURI_LOG_D(TAG, "Releasing lockdown semaphore"); + furi_semaphore_release(gui->unlock); gui_unlock(gui); // Request redraw gui_update(gui); } +void gui_remove_lockdown(Gui* gui) { + furi_check(gui); + FURI_LOG_D(TAG, "Acquiring lockdown semaphore"); + + gui_lock(gui); + if(furi_semaphore_acquire(gui->unlock, 1000) != FuriStatusOk) { + furi_crash("Could not acquire lockdown semaphore"); + } + gui_unlock(gui); +} + +bool gui_is_lockdown(const Gui* gui) { + furi_check(gui); + + return furi_semaphore_get_count(gui->unlock) == 0; +} + Canvas* gui_direct_draw_acquire(Gui* gui) { furi_check(gui); @@ -529,6 +547,8 @@ Gui* gui_alloc(void) { gui->thread_id = furi_thread_get_current_id(); // Allocate mutex gui->mutex = furi_mutex_alloc(FuriMutexTypeNormal); + // Semaphore for the lockdown state + gui->unlock = furi_semaphore_alloc(2, 1); // Layers for(size_t i = 0; i < GuiLayerMAX; i++) { diff --git a/applications/services/gui/gui.h b/applications/services/gui/gui.h index 1b5987eda..1712d5eca 100644 --- a/applications/services/gui/gui.h +++ b/applications/services/gui/gui.h @@ -100,15 +100,27 @@ void gui_remove_framebuffer_callback(Gui* gui, GuiCanvasCommitCallback callback, */ size_t gui_get_framebuffer_size(const Gui* gui); -/** Set lockdown mode +/** Enable lockdown mode * * When lockdown mode is enabled, only GuiLayerDesktop is shown. * This feature prevents services from showing sensitive information when flipper is locked. * * @param gui Gui instance - * @param lockdown bool, true if enabled */ -void gui_set_lockdown(Gui* gui, bool lockdown); +void gui_set_lockdown(Gui* gui); + +/** Disable lockdown mode + * + * @param gui Gui instance + */ +void gui_remove_lockdown(Gui* gui); + +/** Check if Gui is in lockdown mode + * + * @param gui Gui instance + * @return bool true if Gui is in lockdown mode + */ +bool gui_is_lockdown(const Gui* gui); /** Acquire Direct Draw lock and get Canvas instance * diff --git a/applications/services/gui/gui_i.h b/applications/services/gui/gui_i.h index 8bd3215f9..5d5f11f93 100644 --- a/applications/services/gui/gui_i.h +++ b/applications/services/gui/gui_i.h @@ -48,9 +48,9 @@ struct Gui { // Thread and lock FuriThreadId thread_id; FuriMutex* mutex; + FuriSemaphore* unlock; // Layers and Canvas - bool lockdown; bool direct_draw; ViewPortArray_t layers[GuiLayerMAX]; Canvas* canvas; diff --git a/applications/settings/clock_settings/clock_settings_alarm.c b/applications/settings/clock_settings/clock_settings_alarm.c index 7b096ef70..acbeb0736 100644 --- a/applications/settings/clock_settings/clock_settings_alarm.c +++ b/applications/settings/clock_settings/clock_settings_alarm.c @@ -11,8 +11,12 @@ #define TAG "ClockSettingsAlarm" +#define SNOOZE_MINUTES 9 +#define TIMEOUT_MINUTES 10 + typedef struct { DateTime now; + DateTime alarm_start; IconAnimation* icon; } ClockSettingsAlramModel; @@ -47,12 +51,15 @@ static void clock_settings_alarm_draw_callback(Canvas* canvas, void* ctx) { ClockSettingsAlramModel* model = ctx; char buffer[64] = {}; + // Clock icon canvas_draw_icon_animation(canvas, 5, 6, model->icon); + // Time canvas_set_font(canvas, FontBigNumbers); snprintf(buffer, sizeof(buffer), "%02u:%02u", model->now.hour, model->now.minute); canvas_draw_str(canvas, 58, 32, buffer); + // Date canvas_set_font(canvas, FontPrimary); snprintf( buffer, @@ -62,6 +69,11 @@ static void clock_settings_alarm_draw_callback(Canvas* canvas, void* ctx) { model->now.month, model->now.year); canvas_draw_str(canvas, 60, 44, buffer); + + // Press Back to snooze + canvas_set_font(canvas, FontPrimary); + canvas_draw_icon_ex(canvas, 5, 50, &I_back_btn_10x8, 0); + canvas_draw_str_aligned(canvas, 20, 50, AlignLeft, AlignTop, "Snooze"); } static void clock_settings_alarm_input_callback(InputEvent* input_event, void* ctx) { @@ -95,6 +107,7 @@ int32_t clock_settings_alarm(void* p) { // Register view port in GUI Gui* gui = furi_record_open(RECORD_GUI); + gui_remove_lockdown(gui); gui_add_view_port(gui, view_port, GuiLayerFullscreen); NotificationApp* notification = furi_record_open(RECORD_NOTIFICATION); @@ -110,12 +123,32 @@ int32_t clock_settings_alarm(void* p) { while(running) { if(furi_message_queue_get(event_queue, &event, 2000) == FuriStatusOk) { if(event.type == InputTypePress) { + // Snooze + if(event.key == InputKeyBack) { + furi_hal_rtc_get_datetime(&model.now); + model.now.minute += SNOOZE_MINUTES; + model.now.hour += model.now.minute / 60; + model.now.minute %= 60; + model.now.hour %= 24; + + furi_hal_rtc_set_alarm(NULL, false); + furi_hal_rtc_set_alarm(&model.now, true); + } running = false; } } else { notification_message(notification, &sequence_alarm); furi_hal_rtc_get_datetime(&model.now); view_port_update(view_port); + + // Stop the alarm if it has been ringing for more than TIMEOUT_MINUTES + furi_hal_rtc_get_alarm(&model.alarm_start); + if((model.now.hour == model.alarm_start.hour && + model.now.minute >= model.alarm_start.minute + TIMEOUT_MINUTES) || + (model.now.hour == (model.alarm_start.hour + 1) % 24 && + model.now.minute < (model.alarm_start.minute + TIMEOUT_MINUTES) % 60)) { + running = false; + } } } @@ -125,6 +158,7 @@ int32_t clock_settings_alarm(void* p) { furi_record_close(RECORD_NOTIFICATION); view_port_enabled_set(view_port, false); + gui_set_lockdown(gui); gui_remove_view_port(gui, view_port); view_port_free(view_port); furi_message_queue_free(event_queue); diff --git a/targets/f18/api_symbols.csv b/targets/f18/api_symbols.csv index b5d51a0dd..6b8ad287e 100644 --- a/targets/f18/api_symbols.csv +++ b/targets/f18/api_symbols.csv @@ -1,5 +1,5 @@ entry,status,name,type,params -Version,+,78.1,, +Version,v,79.0,, Header,+,applications/services/bt/bt_service/bt.h,, Header,+,applications/services/bt/bt_service/bt_keys_storage.h,, Header,+,applications/services/cli/cli.h,, @@ -1724,9 +1724,11 @@ Function,+,gui_add_view_port,void,"Gui*, ViewPort*, GuiLayer" Function,+,gui_direct_draw_acquire,Canvas*,Gui* Function,+,gui_direct_draw_release,void,Gui* Function,+,gui_get_framebuffer_size,size_t,const Gui* +Function,+,gui_is_lockdown,_Bool,const Gui* Function,+,gui_remove_framebuffer_callback,void,"Gui*, GuiCanvasCommitCallback, void*" +Function,+,gui_remove_lockdown,void,Gui* Function,+,gui_remove_view_port,void,"Gui*, ViewPort*" -Function,+,gui_set_lockdown,void,"Gui*, _Bool" +Function,+,gui_set_lockdown,void,Gui* Function,-,gui_view_port_send_to_back,void,"Gui*, ViewPort*" Function,+,gui_view_port_send_to_front,void,"Gui*, ViewPort*" Function,-,hci_send_req,int,"hci_request*, uint8_t" diff --git a/targets/f7/api_symbols.csv b/targets/f7/api_symbols.csv index ee81f76a9..05e09bef6 100644 --- a/targets/f7/api_symbols.csv +++ b/targets/f7/api_symbols.csv @@ -1,5 +1,5 @@ entry,status,name,type,params -Version,+,78.1,, +Version,+,79.0,, Header,+,applications/drivers/subghz/cc1101_ext/cc1101_ext_interconnect.h,, Header,+,applications/services/bt/bt_service/bt.h,, Header,+,applications/services/bt/bt_service/bt_keys_storage.h,, @@ -1943,9 +1943,11 @@ Function,+,gui_add_view_port,void,"Gui*, ViewPort*, GuiLayer" Function,+,gui_direct_draw_acquire,Canvas*,Gui* Function,+,gui_direct_draw_release,void,Gui* Function,+,gui_get_framebuffer_size,size_t,const Gui* +Function,+,gui_is_lockdown,_Bool,const Gui* Function,+,gui_remove_framebuffer_callback,void,"Gui*, GuiCanvasCommitCallback, void*" +Function,+,gui_remove_lockdown,void,Gui* Function,+,gui_remove_view_port,void,"Gui*, ViewPort*" -Function,+,gui_set_lockdown,void,"Gui*, _Bool" +Function,+,gui_set_lockdown,void,Gui* Function,-,gui_view_port_send_to_back,void,"Gui*, ViewPort*" Function,+,gui_view_port_send_to_front,void,"Gui*, ViewPort*" Function,-,hci_send_req,int,"hci_request*, uint8_t" From 905f9498aae9d18bab1c35d8b0a29850d89e82c2 Mon Sep 17 00:00:00 2001 From: Astra Date: Tue, 26 Nov 2024 19:31:37 +0900 Subject: [PATCH 002/268] F18 api --- targets/f18/api_symbols.csv | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/targets/f18/api_symbols.csv b/targets/f18/api_symbols.csv index 6b8ad287e..36ce8eadf 100644 --- a/targets/f18/api_symbols.csv +++ b/targets/f18/api_symbols.csv @@ -1,5 +1,5 @@ entry,status,name,type,params -Version,v,79.0,, +Version,+,79.0,, Header,+,applications/services/bt/bt_service/bt.h,, Header,+,applications/services/bt/bt_service/bt_keys_storage.h,, Header,+,applications/services/cli/cli.h,, From 93d2120e5519442d2ca167ce5f7422db1b9c0a57 Mon Sep 17 00:00:00 2001 From: Astra Date: Tue, 24 Dec 2024 19:26:31 +0900 Subject: [PATCH 003/268] Fix snooze and next day alarm behavior --- .../clock_settings/clock_settings_alarm.c | 34 ++++++++++++++----- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/applications/settings/clock_settings/clock_settings_alarm.c b/applications/settings/clock_settings/clock_settings_alarm.c index acbeb0736..1327fb4f7 100644 --- a/applications/settings/clock_settings/clock_settings_alarm.c +++ b/applications/settings/clock_settings/clock_settings_alarm.c @@ -16,8 +16,11 @@ typedef struct { DateTime now; + DateTime snooze_until; DateTime alarm_start; IconAnimation* icon; + + bool is_snooze; } ClockSettingsAlramModel; const NotificationSequence sequence_alarm = { @@ -93,8 +96,10 @@ int32_t clock_settings_alarm(void* p) { // View Model ClockSettingsAlramModel model; + model.is_snooze = false; furi_hal_rtc_get_datetime(&model.now); + furi_hal_rtc_get_alarm(&model.alarm_start); model.icon = icon_animation_alloc(&A_Alarm_47x39); // Alloc message queue @@ -125,16 +130,28 @@ int32_t clock_settings_alarm(void* p) { if(event.type == InputTypePress) { // Snooze if(event.key == InputKeyBack) { - furi_hal_rtc_get_datetime(&model.now); - model.now.minute += SNOOZE_MINUTES; - model.now.hour += model.now.minute / 60; - model.now.minute %= 60; - model.now.hour %= 24; + furi_hal_rtc_get_datetime(&model.snooze_until); + model.snooze_until.minute += SNOOZE_MINUTES; + model.snooze_until.hour += model.snooze_until.minute / 60; + model.snooze_until.minute %= 60; + model.snooze_until.hour %= 24; - furi_hal_rtc_set_alarm(NULL, false); - furi_hal_rtc_set_alarm(&model.now, true); + model.is_snooze = true; + model.alarm_start = model.snooze_until; // For correct timeout behavior + view_port_enabled_set(view_port, false); + gui_set_lockdown(gui); + } else { + running = false; } - running = false; + } + } else if(model.is_snooze) { + furi_hal_rtc_get_datetime(&model.now); + if(datetime_datetime_to_timestamp(&model.now) >= + datetime_datetime_to_timestamp(&model.snooze_until)) { + view_port_enabled_set(view_port, true); + gui_remove_lockdown(gui); + + model.is_snooze = false; } } else { notification_message(notification, &sequence_alarm); @@ -142,7 +159,6 @@ int32_t clock_settings_alarm(void* p) { view_port_update(view_port); // Stop the alarm if it has been ringing for more than TIMEOUT_MINUTES - furi_hal_rtc_get_alarm(&model.alarm_start); if((model.now.hour == model.alarm_start.hour && model.now.minute >= model.alarm_start.minute + TIMEOUT_MINUTES) || (model.now.hour == (model.alarm_start.hour + 1) % 24 && From 31365cb170f327e7f26c6b51c8f3052828d701ca Mon Sep 17 00:00:00 2001 From: Astra Date: Wed, 25 Dec 2024 18:38:32 +0900 Subject: [PATCH 004/268] Don't use semaphores for lock state tracking --- applications/services/desktop/desktop.c | 2 +- .../desktop/scenes/desktop_scene_locked.c | 2 +- applications/services/gui/gui.c | 19 ++++++++----------- applications/services/gui/gui.h | 13 +++++++++---- applications/services/gui/gui_i.h | 3 ++- .../clock_settings/clock_settings_alarm.c | 8 ++++---- targets/f7/api_symbols.csv | 8 ++++---- 7 files changed, 29 insertions(+), 26 deletions(-) diff --git a/applications/services/desktop/desktop.c b/applications/services/desktop/desktop.c index 9b7310370..1132760d5 100644 --- a/applications/services/desktop/desktop.c +++ b/applications/services/desktop/desktop.c @@ -417,7 +417,7 @@ void desktop_unlock(Desktop* desktop) { view_port_enabled_set(desktop->lock_icon_viewport, false); Gui* gui = furi_record_open(RECORD_GUI); - gui_remove_lockdown(gui); + gui_set_lockdown(gui, false); furi_record_close(RECORD_GUI); desktop_view_locked_unlock(desktop->locked_view); scene_manager_search_and_switch_to_previous_scene(desktop->scene_manager, DesktopSceneMain); diff --git a/applications/services/desktop/scenes/desktop_scene_locked.c b/applications/services/desktop/scenes/desktop_scene_locked.c index 8e0c5bfc1..e7eeebca6 100644 --- a/applications/services/desktop/scenes/desktop_scene_locked.c +++ b/applications/services/desktop/scenes/desktop_scene_locked.c @@ -45,7 +45,7 @@ void desktop_scene_locked_on_enter(void* context) { if(state == DesktopSceneLockedStateFirstEnter) { view_port_enabled_set(desktop->lock_icon_viewport, true); Gui* gui = furi_record_open(RECORD_GUI); - gui_set_lockdown(gui); + gui_set_lockdown(gui, true); furi_record_close(RECORD_GUI); if(desktop_pin_code_is_set()) { diff --git a/applications/services/gui/gui.c b/applications/services/gui/gui.c index 085507d1d..68a287310 100644 --- a/applications/services/gui/gui.c +++ b/applications/services/gui/gui.c @@ -484,33 +484,32 @@ size_t gui_get_framebuffer_size(const Gui* gui) { return canvas_get_buffer_size(gui->canvas); } -void gui_set_lockdown(Gui* gui) { +void gui_set_lockdown(Gui* gui, bool lockdown) { furi_check(gui); gui_lock(gui); - FURI_LOG_D(TAG, "Releasing lockdown semaphore"); - furi_semaphore_release(gui->unlock); + gui->lockdown = lockdown; gui_unlock(gui); // Request redraw gui_update(gui); } -void gui_remove_lockdown(Gui* gui) { +void gui_set_lockdown_inhibit(Gui* gui, bool inhibit) { furi_check(gui); - FURI_LOG_D(TAG, "Acquiring lockdown semaphore"); gui_lock(gui); - if(furi_semaphore_acquire(gui->unlock, 1000) != FuriStatusOk) { - furi_crash("Could not acquire lockdown semaphore"); - } + gui->lockdown_inhibit = inhibit; gui_unlock(gui); + + // Request redraw + gui_update(gui); } bool gui_is_lockdown(const Gui* gui) { furi_check(gui); - return furi_semaphore_get_count(gui->unlock) == 0; + return gui->lockdown && !gui->lockdown_inhibit; } Canvas* gui_direct_draw_acquire(Gui* gui) { @@ -547,8 +546,6 @@ Gui* gui_alloc(void) { gui->thread_id = furi_thread_get_current_id(); // Allocate mutex gui->mutex = furi_mutex_alloc(FuriMutexTypeNormal); - // Semaphore for the lockdown state - gui->unlock = furi_semaphore_alloc(2, 1); // Layers for(size_t i = 0; i < GuiLayerMAX; i++) { diff --git a/applications/services/gui/gui.h b/applications/services/gui/gui.h index 1712d5eca..db30e2529 100644 --- a/applications/services/gui/gui.h +++ b/applications/services/gui/gui.h @@ -106,14 +106,19 @@ size_t gui_get_framebuffer_size(const Gui* gui); * This feature prevents services from showing sensitive information when flipper is locked. * * @param gui Gui instance + * @param lockdown true to enable lockdown mode */ -void gui_set_lockdown(Gui* gui); +void gui_set_lockdown(Gui* gui, bool lockdown); -/** Disable lockdown mode - * +/** Inhibit lockdown mode + * + * Lockdown mode can be inhibited by calling this function with inhibit set to true. + * This is used to show information even when flipper is locked. + * * @param gui Gui instance + * @param inhibit true to inhibit lockdown mode */ -void gui_remove_lockdown(Gui* gui); +void gui_set_lockdown_inhibit(Gui* gui, bool inhibit); /** Check if Gui is in lockdown mode * diff --git a/applications/services/gui/gui_i.h b/applications/services/gui/gui_i.h index 5d5f11f93..f146ad1fc 100644 --- a/applications/services/gui/gui_i.h +++ b/applications/services/gui/gui_i.h @@ -48,9 +48,10 @@ struct Gui { // Thread and lock FuriThreadId thread_id; FuriMutex* mutex; - FuriSemaphore* unlock; // Layers and Canvas + bool lockdown; + bool lockdown_inhibit; bool direct_draw; ViewPortArray_t layers[GuiLayerMAX]; Canvas* canvas; diff --git a/applications/settings/clock_settings/clock_settings_alarm.c b/applications/settings/clock_settings/clock_settings_alarm.c index 1327fb4f7..ef348c0b1 100644 --- a/applications/settings/clock_settings/clock_settings_alarm.c +++ b/applications/settings/clock_settings/clock_settings_alarm.c @@ -112,7 +112,7 @@ int32_t clock_settings_alarm(void* p) { // Register view port in GUI Gui* gui = furi_record_open(RECORD_GUI); - gui_remove_lockdown(gui); + gui_set_lockdown_inhibit(gui, true); gui_add_view_port(gui, view_port, GuiLayerFullscreen); NotificationApp* notification = furi_record_open(RECORD_NOTIFICATION); @@ -139,7 +139,7 @@ int32_t clock_settings_alarm(void* p) { model.is_snooze = true; model.alarm_start = model.snooze_until; // For correct timeout behavior view_port_enabled_set(view_port, false); - gui_set_lockdown(gui); + gui_set_lockdown_inhibit(gui, false); } else { running = false; } @@ -149,7 +149,7 @@ int32_t clock_settings_alarm(void* p) { if(datetime_datetime_to_timestamp(&model.now) >= datetime_datetime_to_timestamp(&model.snooze_until)) { view_port_enabled_set(view_port, true); - gui_remove_lockdown(gui); + gui_set_lockdown_inhibit(gui, true); model.is_snooze = false; } @@ -174,7 +174,7 @@ int32_t clock_settings_alarm(void* p) { furi_record_close(RECORD_NOTIFICATION); view_port_enabled_set(view_port, false); - gui_set_lockdown(gui); + gui_set_lockdown_inhibit(gui, false); gui_remove_view_port(gui, view_port); view_port_free(view_port); furi_message_queue_free(event_queue); diff --git a/targets/f7/api_symbols.csv b/targets/f7/api_symbols.csv index 1f108f77f..ccdf3711d 100644 --- a/targets/f7/api_symbols.csv +++ b/targets/f7/api_symbols.csv @@ -1,5 +1,5 @@ entry,status,name,type,params -Version,+,79.2,, +Version,+,80.0,, Header,+,applications/drivers/subghz/cc1101_ext/cc1101_ext_interconnect.h,, Header,+,applications/services/bt/bt_service/bt.h,, Header,+,applications/services/bt/bt_service/bt_keys_storage.h,, @@ -1957,9 +1957,9 @@ Function,+,gui_direct_draw_release,void,Gui* Function,+,gui_get_framebuffer_size,size_t,const Gui* Function,+,gui_is_lockdown,_Bool,const Gui* Function,+,gui_remove_framebuffer_callback,void,"Gui*, GuiCanvasCommitCallback, void*" -Function,+,gui_remove_lockdown,void,Gui* Function,+,gui_remove_view_port,void,"Gui*, ViewPort*" -Function,+,gui_set_lockdown,void,Gui* +Function,+,gui_set_lockdown,void,"Gui*, _Bool" +Function,+,gui_set_lockdown_inhibit,void,"Gui*, _Bool" Function,-,gui_view_port_send_to_back,void,"Gui*, ViewPort*" Function,+,gui_view_port_send_to_front,void,"Gui*, ViewPort*" Function,-,hci_send_req,int,"hci_request*, uint8_t" @@ -2940,9 +2940,9 @@ Function,+,pipe_install_as_stdio,void,PipeSide* Function,+,pipe_receive,size_t,"PipeSide*, void*, size_t, FuriWait" Function,+,pipe_role,PipeRole,PipeSide* Function,+,pipe_send,size_t,"PipeSide*, const void*, size_t, FuriWait" +Function,+,pipe_set_broken_callback,void,"PipeSide*, PipeSideBrokenCallback, FuriEventLoopEvent" Function,+,pipe_set_callback_context,void,"PipeSide*, void*" Function,+,pipe_set_data_arrived_callback,void,"PipeSide*, PipeSideDataArrivedCallback, FuriEventLoopEvent" -Function,+,pipe_set_broken_callback,void,"PipeSide*, PipeSideBrokenCallback, FuriEventLoopEvent" Function,+,pipe_set_space_freed_callback,void,"PipeSide*, PipeSideSpaceFreedCallback, FuriEventLoopEvent" Function,+,pipe_spaces_available,size_t,PipeSide* Function,+,pipe_state,PipeState,PipeSide* From 701969e3341a9c6d3965e8aa935dd8d55ee5cef0 Mon Sep 17 00:00:00 2001 From: Astra Date: Wed, 25 Dec 2024 18:39:57 +0900 Subject: [PATCH 005/268] Fix comments --- applications/services/gui/gui.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/applications/services/gui/gui.h b/applications/services/gui/gui.h index db30e2529..7eb19b91d 100644 --- a/applications/services/gui/gui.h +++ b/applications/services/gui/gui.h @@ -100,13 +100,13 @@ void gui_remove_framebuffer_callback(Gui* gui, GuiCanvasCommitCallback callback, */ size_t gui_get_framebuffer_size(const Gui* gui); -/** Enable lockdown mode +/** Set lockdown mode * * When lockdown mode is enabled, only GuiLayerDesktop is shown. * This feature prevents services from showing sensitive information when flipper is locked. * * @param gui Gui instance - * @param lockdown true to enable lockdown mode + * @param lockdown bool, true if enabled */ void gui_set_lockdown(Gui* gui, bool lockdown); From c1c2bca8500d5525d89116bcc16860ac76f8f50a Mon Sep 17 00:00:00 2001 From: Astra Date: Wed, 25 Dec 2024 18:41:50 +0900 Subject: [PATCH 006/268] F18 API --- targets/f18/api_symbols.csv | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/targets/f18/api_symbols.csv b/targets/f18/api_symbols.csv index 381c6cb8d..13c1afb87 100644 --- a/targets/f18/api_symbols.csv +++ b/targets/f18/api_symbols.csv @@ -1,5 +1,5 @@ entry,status,name,type,params -Version,+,79.2,, +Version,+,80.0,, Header,+,applications/services/bt/bt_service/bt.h,, Header,+,applications/services/bt/bt_service/bt_keys_storage.h,, Header,+,applications/services/cli/cli.h,, @@ -1738,9 +1738,9 @@ Function,+,gui_direct_draw_release,void,Gui* Function,+,gui_get_framebuffer_size,size_t,const Gui* Function,+,gui_is_lockdown,_Bool,const Gui* Function,+,gui_remove_framebuffer_callback,void,"Gui*, GuiCanvasCommitCallback, void*" -Function,+,gui_remove_lockdown,void,Gui* Function,+,gui_remove_view_port,void,"Gui*, ViewPort*" -Function,+,gui_set_lockdown,void,Gui* +Function,+,gui_set_lockdown,void,"Gui*, _Bool" +Function,+,gui_set_lockdown_inhibit,void,"Gui*, _Bool" Function,-,gui_view_port_send_to_back,void,"Gui*, ViewPort*" Function,+,gui_view_port_send_to_front,void,"Gui*, ViewPort*" Function,-,hci_send_req,int,"hci_request*, uint8_t" @@ -2304,9 +2304,9 @@ Function,+,pipe_install_as_stdio,void,PipeSide* Function,+,pipe_receive,size_t,"PipeSide*, void*, size_t, FuriWait" Function,+,pipe_role,PipeRole,PipeSide* Function,+,pipe_send,size_t,"PipeSide*, const void*, size_t, FuriWait" +Function,+,pipe_set_broken_callback,void,"PipeSide*, PipeSideBrokenCallback, FuriEventLoopEvent" Function,+,pipe_set_callback_context,void,"PipeSide*, void*" Function,+,pipe_set_data_arrived_callback,void,"PipeSide*, PipeSideDataArrivedCallback, FuriEventLoopEvent" -Function,+,pipe_set_broken_callback,void,"PipeSide*, PipeSideBrokenCallback, FuriEventLoopEvent" Function,+,pipe_set_space_freed_callback,void,"PipeSide*, PipeSideSpaceFreedCallback, FuriEventLoopEvent" Function,+,pipe_spaces_available,size_t,PipeSide* Function,+,pipe_state,PipeState,PipeSide* From a5f62f756ae10029ead39db32d0d9bebdada9291 Mon Sep 17 00:00:00 2001 From: Dmitry422 Date: Fri, 31 Jan 2025 19:25:52 +0700 Subject: [PATCH 007/268] Begin work with Input_service settings (edit,save) for vibro_touch option --- applications/services/input/input.c | 22 ++++++ applications/services/input/input.h | 8 ++ applications/services/input/input_settings.c | 77 +++++++++++++++++++ applications/services/input/input_settings.h | 18 +++++ .../services/input/input_settings_filename.h | 3 + .../power/power_service/power_settings.h | 2 + .../settings/input_settings/application.fam | 9 +++ .../input_settings/input_settings_app.c | 67 ++++++++++++++++ .../input_settings/input_settings_app.h | 23 ++++++ targets/f7/api_symbols.csv | 4 +- 10 files changed, 232 insertions(+), 1 deletion(-) create mode 100644 applications/services/input/input_settings.c create mode 100644 applications/services/input/input_settings.h create mode 100644 applications/services/input/input_settings_filename.h create mode 100644 applications/settings/input_settings/application.fam create mode 100644 applications/settings/input_settings/input_settings_app.c create mode 100644 applications/settings/input_settings/input_settings_app.h diff --git a/applications/services/input/input.c b/applications/services/input/input.c index 6cbafb795..4ba77cc34 100644 --- a/applications/services/input/input.c +++ b/applications/services/input/input.c @@ -6,6 +6,7 @@ #include #include #include +#include #define INPUT_DEBOUNCE_TICKS_HALF (INPUT_DEBOUNCE_TICKS / 2) #define INPUT_PRESS_TICKS 150 @@ -79,9 +80,23 @@ const char* input_get_type_name(InputType type) { } } +// allocate memory for input_settings structure +static InputSettings* input_settings_alloc (void) { + InputSettings* input_settings = malloc(sizeof(InputSettings)); + return input_settings; +} + +//free memory from input_settings structure +void input_settings_free (InputSettings* input_settings) { + free (input_settings); +} + int32_t input_srv(void* p) { UNUSED(p); + //define object input_settings and take memory for him + InputSettings* input_settings = input_settings_alloc(); + const FuriThreadId thread_id = furi_thread_get_current_id(); FuriPubSub* event_pubsub = furi_pubsub_alloc(); uint32_t counter = 1; @@ -149,6 +164,11 @@ int32_t input_srv(void* p) { // Send Press/Release event event.type = pin_states[i].state ? InputTypePress : InputTypeRelease; furi_pubsub_publish(event_pubsub, &event); + if (input_settings->vibro_touch_level) { + furi_hal_vibro_on(true); + furi_delay_tick (30); + furi_hal_vibro_on(false); + }; } } @@ -165,5 +185,7 @@ int32_t input_srv(void* p) { } } + // if we come here and ready exit from service (whats going on ??!!) then best practice is free memory + input_settings_free(input_settings); return 0; } diff --git a/applications/services/input/input.h b/applications/services/input/input.h index 5233b4a01..3d8d6c9ec 100644 --- a/applications/services/input/input.h +++ b/applications/services/input/input.h @@ -6,6 +6,7 @@ #pragma once #include +#include "input_settings.h" #ifdef __cplusplus extern "C" { @@ -40,6 +41,13 @@ typedef struct { InputType type; } InputEvent; +//for next step input structure globalization; +//typedef struct Input { + //InputSettings* settings; + //InputType* type; + //InputEvent* event; +//} Input; + /** Get human readable input key name * @param key - InputKey * @return string diff --git a/applications/services/input/input_settings.c b/applications/services/input/input_settings.c new file mode 100644 index 000000000..fc738c004 --- /dev/null +++ b/applications/services/input/input_settings.c @@ -0,0 +1,77 @@ +#include "input_settings.h" +#include "input_settings_filename.h" + +#include +#include + +#define TAG "InputSettings" + +#define INPUT_SETTINGS_VER_0 (0) // OLD version number +#define INPUT_SETTINGS_VER (1) // NEW actual version nnumber + +#define INPUT_SETTINGS_PATH INT_PATH(INPUT_SETTINGS_FILE_NAME) +#define INPUT_SETTINGS_MAGIC (0x19) + +typedef struct { + //inital set - empty +} InputSettingsV0; + +void input_settings_load(InputSettings* settings) { + furi_assert(settings); + + bool success = false; + + do { + uint8_t version; + if(!saved_struct_get_metadata(INPUT_SETTINGS_PATH, NULL, &version, NULL)) break; + + if(version == INPUT_SETTINGS_VER) { // if config actual version - load it directly + success = saved_struct_load( + INPUT_SETTINGS_PATH, + settings, + sizeof(InputSettings), + INPUT_SETTINGS_MAGIC, + INPUT_SETTINGS_VER); + + } else if( + version == + INPUT_SETTINGS_VER_0) { // if config previous version - load it and manual set new settings to inital value + InputSettingsV0* settings_v0 = malloc(sizeof(InputSettingsV0)); + + success = saved_struct_load( + INPUT_SETTINGS_PATH, + settings_v0, + sizeof(InputSettingsV0), + INPUT_SETTINGS_MAGIC, + INPUT_SETTINGS_VER_0); + + if(success) { + settings->vibro_touch_level = 0; + } + + free(settings_v0); + } + + } while(false); + + if(!success) { + FURI_LOG_W(TAG, "Failed to load file, using defaults"); + memset(settings, 0, sizeof(InputSettings)); + input_settings_save(settings); + } +} + +void input_settings_save(const InputSettings* settings) { + furi_assert(settings); + + const bool success = saved_struct_save( + INPUT_SETTINGS_PATH, + settings, + sizeof(InputSettings), + INPUT_SETTINGS_MAGIC, + INPUT_SETTINGS_VER); + + if(!success) { + FURI_LOG_E(TAG, "Failed to save file"); + } +} diff --git a/applications/services/input/input_settings.h b/applications/services/input/input_settings.h new file mode 100644 index 000000000..376e8e226 --- /dev/null +++ b/applications/services/input/input_settings.h @@ -0,0 +1,18 @@ +#pragma once + +#include + +typedef struct { + uint8_t vibro_touch_level; +} InputSettings; + +#ifdef __cplusplus +extern "C" { +#endif + +void input_settings_load(InputSettings* settings); +void input_settings_save(const InputSettings* settings); + +#ifdef __cplusplus +} +#endif diff --git a/applications/services/input/input_settings_filename.h b/applications/services/input/input_settings_filename.h new file mode 100644 index 000000000..025ed6ad5 --- /dev/null +++ b/applications/services/input/input_settings_filename.h @@ -0,0 +1,3 @@ +#pragma once + +#define INPUT_SETTINGS_FILE_NAME ".input.settings" diff --git a/applications/services/power/power_service/power_settings.h b/applications/services/power/power_service/power_settings.h index 65d8e079e..b1cc71001 100644 --- a/applications/services/power/power_service/power_settings.h +++ b/applications/services/power/power_service/power_settings.h @@ -9,8 +9,10 @@ typedef struct { #ifdef __cplusplus extern "C" { #endif + void power_settings_load(PowerSettings* settings); void power_settings_save(const PowerSettings* settings); + #ifdef __cplusplus } #endif diff --git a/applications/settings/input_settings/application.fam b/applications/settings/input_settings/application.fam new file mode 100644 index 000000000..f137c2512 --- /dev/null +++ b/applications/settings/input_settings/application.fam @@ -0,0 +1,9 @@ +App( + appid="input_settings", + name="Input", + apptype=FlipperAppType,SETTINGS, + entry_point="input_settings_app", + requires=["gui"], + stack_size= 1 * 1024, + order=100, +) diff --git a/applications/settings/input_settings/input_settings_app.c b/applications/settings/input_settings/input_settings_app.c new file mode 100644 index 000000000..943bbc02f --- /dev/null +++ b/applications/settings/input_settings/input_settings_app.c @@ -0,0 +1,67 @@ +#include +#include "input_settings_app.h" + +#define VIBRO_TOUCH_LEVEL_COUNT 10 +// vibro touch human readable levels +const char* const vibro_touch_level_text[VIBRO_TOUCH_LEVEL_COUNT] = { + "OFF", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", +}; +// vibro touch levels tick valies delay +const uint32_t vibro_touch_level_value[VIBRO_TOUCH_LEVEL_COUNT] = + {0,13,16,19,21,24,27,30,33,36}; + +input_settings_start_vibro_touch_level_changed (){ + +} + +InputSettingsApp* input_settings_alloc (void) { + InputSettingsApp* app = malloc(sizeof(InputSettingsApp)); + + app->gui = furi_record_open (RECORD_GUI); + app->view_dispatcher = view_dispatcher_alloc (); + + VariableItem* item; + app->var_item_list = variable_item_list_alloc(); + + item = variable_item_list_add( + variable_item_list, + "Vibro touch level", + VIBRO_TOUCH_LEVEL_COUNT, + input_settings_start_vibro_touch_level_changed, + app); + + value_index = value_index_uint32( + app->settings.vibro_touch_level, vibro_touch_level_value, VIBRO_TOUCH_LEVEL_COUNT); + variable_item_set_current_value_index(item, value_index); + variable_item_set_current_value_text(item, vibro_touch_level_text[value_index]); + + +return app; +} + + +// Enter point +int32_t input_settings_app (void* p) { + UNUSED (p); + + //take memory + InputSettingsApp* app = input_settings_alloc (); + + //start view_dispatcher + view_dispatcher_run(app->view_dispatcher); + + //free memory + Input_settings_free (app); + + //exit with 0 code + return 0; +}; \ No newline at end of file diff --git a/applications/settings/input_settings/input_settings_app.h b/applications/settings/input_settings/input_settings_app.h new file mode 100644 index 000000000..7ba29b511 --- /dev/null +++ b/applications/settings/input_settings/input_settings_app.h @@ -0,0 +1,23 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include "input/input_settings.h" + +// app stucture +typedef struct { +InputSettings* settings; +Input* input; +Gui* gui; +ViewDispatcher* view_dispatcher; +VariableItemList* variable_item_list; +} InputSettingsApp; + +// list of menu views for view dispatcher +typedef enum { + InputSettingsViewVariableItemList, +} InputSettingsView; \ No newline at end of file diff --git a/targets/f7/api_symbols.csv b/targets/f7/api_symbols.csv index b649f1f99..420d7e678 100644 --- a/targets/f7/api_symbols.csv +++ b/targets/f7/api_symbols.csv @@ -1,5 +1,5 @@ entry,status,name,type,params -Version,+,79.3,, +Version,+,79.4,, Header,+,applications/drivers/subghz/cc1101_ext/cc1101_ext_interconnect.h,, Header,+,applications/services/bt/bt_service/bt.h,, Header,+,applications/services/bt/bt_service/bt_keys_storage.h,, @@ -2112,6 +2112,8 @@ Function,+,infrared_worker_tx_stop,void,InfraredWorker* Function,-,initstate,char*,"unsigned, char*, size_t" Function,+,input_get_key_name,const char*,InputKey Function,+,input_get_type_name,const char*,InputType +Function,+,input_settings_load,void,InputSettings* +Function,+,input_settings_save,void,const InputSettings* Function,-,iprintf,int,"const char*, ..." Function,-,isalnum,int,int Function,-,isalnum_l,int,"int, locale_t" From d0a33b22f46161644bacd9c526a19181000e698d Mon Sep 17 00:00:00 2001 From: mi-lrn <179659299+mi-lrn@users.noreply.github.com> Date: Fri, 31 Jan 2025 18:46:31 -0700 Subject: [PATCH 008/268] Update FAQ.md Hi! Great project. Made some minor grammar and clarity changes to FAQ.md to make it easier to read. --- documentation/FAQ.md | 151 +++++++++++++++++++++---------------------- 1 file changed, 75 insertions(+), 76 deletions(-) diff --git a/documentation/FAQ.md b/documentation/FAQ.md index 67581999e..a180c827d 100644 --- a/documentation/FAQ.md +++ b/documentation/FAQ.md @@ -1,57 +1,56 @@ # FAQ -## I bought Flipper Zero and I don't know what I can do with it, pls help +## I bought Flipper Zero, and I don't know what I can do with it, please help! - Start with reading [official main page](https://flipperzero.one/) - Then check out official docs where you can find answers to [most questions](https://docs.flipper.net/) -## How do I install Unleashed firmware? +## How do I install Unleashed Firmware? See [this](https://github.com/DarkFlippers/unleashed-firmware/blob/dev/documentation/HowToInstall.md) -## What version I should install? What do letters `e`, `r`, `c`... mean? +## What version should I install? What do the letters `e`, `r`, `c`... mean? Follow this link for [details](https://github.com/DarkFlippers/unleashed-firmware/blob/dev/CHANGELOG.md#recommended-update-option---web-updater). -## INSTALLED UNLEASHED AND NOW BACKLIGHT DOESNT WORK? +## I installed Unleashed and now the backlight doesn't work You’ve installed a version made for custom RGB modded flippers. The version ending in `r` is specifically for `RGB` modded flippers.
Please, do not use that version if your flipper isn’t modded! -## What apps (plugins) are included with Unleashed FW? +## What apps (plugins) are included with Unleashed Firmware? See default pack and extra pack (for `e` build) list [here](https://github.com/xMasterX/all-the-plugins/tree/dev). -## Where I can find differences between original (official) firmware and Unleashed firmware? +## Where can I find differences between the original (official) firmware and Unleashed Firmware? [Right here](https://github.com/DarkFlippers/unleashed-firmware#whats-changed) -## How to use SubGHz Remote app? +## How to use the SubGHz Remote app? -1. Open app, press `Back` button, select `New map file` -2. Configure signal files and their names for every button (also you can add only one signal and make other buttons empty - just don't select any files for them in config) -3. Save new map file -4. Open map file and select your previously created file -5. Use buttons to send subghz signal files that you selected in map config at step 2 +1. Open the app, press the `Back` button, and select `New map file` +2. Configure signal files and their names for every button (also you can only add one signal and make other buttons empty - just don't select any files for them in config) +3. Save the new map file +4. Open the map file and select your previously created file +5. Use buttons to send the subghz signal files that you selected in map config at step 2 -## How to build (compile) firmware? +## How to build (compile) the firmware? Follow this [link](https://github.com/DarkFlippers/unleashed-firmware/blob/dev/documentation/HowToBuild.md#how-to-build-by-yourself). -## I installed Unleashed firmware and now my mobile app doesn't connect to flipper ( OR I changed flipper device name and my mobile app now doesn't connect to flipper ) +## I installed Unleashed firmware, and now my mobile app doesn't connect to flipper ( OR I changed flipper device name, and my mobile app now doesn't connect to flipper ) - 1. Click Forget flipper in mobile app - 2. Open your `phone settings - bluetooth`, find flipper - if it present here - open its options and click forget device - 3. On flipper itself open `Settings -> Bluetooth -> Forget all devices` and confirm - 4. Make sure your flipper has bluetooth `ON` and open Mobile app and pair it to flipper - 5. Done + 1. Click Forget flipper in the mobile app + 2. Open `Phone Settings - Bluetooth`, find the flipper - if it present here - open its options and click forget device + 3. On the flipper itself open `Settings -> Bluetooth -> Forget all devices` and confirm + 4. Make sure your flipper has bluetooth `ON` then open the mobile app and pair it to the flipper -## My desktop (pin, favourites, etc..) (or other) settings was reset to default after update, what to do? +## My desktop (pin, favourites, etc..) (or other) settings were reset to default after an update, what do I do? -Just configure that settings again, all is fine, and make sure you seen changelogs for the releases that came out after your previous version, when settings struct is changed, settings file are reset after update, this happens only when struct changes is required, so don't assume that settings will be reset in every release, this will happen only in specific ones +Just configure those settings again, and make sure you view the changelogs for the releases that came out after your previous version, when settings struct is changed, the settings file is reset after an update, this happens only when struct changes are required, so don't assume that settings will be reset in every release, this will only happen in specific ones -## Why is flipper not connecting to Chrome? +## Why is my flipper not connecting to Chrome? The most common cause of the flipper not connecting to google chrome is having qFlipper open while trying to connect your flipper.
@@ -59,7 +58,7 @@ Or having second flipper lab page open at same time.
You must close qFlipper (or other flipper lab web pages) before attempting to connect your flipper to Chrome. -## Flipper doesn't work! How to restore firmware??? +## Flipper doesn't work! How to restore firmware? Follow this [guide](https://docs.flipper.net/basics/firmware-update/firmware-recovery) @@ -74,20 +73,20 @@ Flipper Awesome - place where you can find almost all links that you might need: * UL Dev Builds in [Telegram](https://t.me/kotnehleb) * Our [Discord](https://discord.unleashedflip.com) -## How to change flipper name? +## How do I change my flipper's name? -All is simple: +It's easy: 1. Open `Settings -> Desktop -> Change Flipper Name` 2. Enter new name and click `Save` -3. Exit from settings - Flipper will automatically reboot -4. Done, you have custom name which will stay until you reset it to default or replace with new one +3. Exit from settings - flipper will automatically reboot +4. Done, you now have a custom name which will stay until you reset it or replace it with a new one -## How to reset name to default? +## How to reset the name to default? 1. Open `Settings -> Desktop -> Change Flipper Name` -2. Do not enter anything, just click Save +2. Do not enter anything, just click `Save` 3. Exit from settings - Flipper will automatically reboot -4. Done, name is reset to original one. +4. Done, name is reset to its original form. ## How do I copy files from GitHub to my Flipper Zero? @@ -95,41 +94,41 @@ Follow this detailed [guide](https://github.com/wrenchathome/flipperfiles/blob/m ## Where can I find “This file” or “That file” for my flipper? -These 2 repos will cover most (99.9%) of your needs:
+These 2 repos will cover (99.9%) of your needs:
* https://github.com/UberGuidoZ/Flipper/tree/main * https://github.com/UberGuidoZ/Flipper-IRDB/tree/main -## How can I support Unleashed firmware project? +## How can I support the Unleashed firmware project? Please follow this [link](https://github.com/DarkFlippers/unleashed-firmware#please-support-development-of-the-project). -## What are the dev builds? Where I can get latest build for dev branch? +## What are the dev builds? Where I can get the latest build for dev branch? -This is an automatic assembly of the latest commits from the repository that have not yet been released, the previous build is deleted when a new one is uploaded and old remains only as file in the telegram channel +This is an automatic assembly of the latest commits from this repository that have not yet been released. The previous build is deleted when a new one is uploaded, and the old one remains only as a file in the telegram channel > [!CAUTION] > -> Be aware that this is not release ready builds! +> Be wary - these are not release ready builds! > -> They may have bugs and issues, -> if you are using dev build and found issue, -> report it! In [GitHub issues](https://github.com/DarkFlippers/unleashed-firmware/issues) +> They may have bugs and issues! +> If you are using the dev build and find issues: +> Report it! [GitHub issues](https://github.com/DarkFlippers/unleashed-firmware/issues) -Dev builds is available in Discord, Win channel - `unleashed-development`
-Builds also can be found [here](https://t.me/kotnehleb).
+Dev builds are available in Discord, in channel - `unleashed-development`
+Builds can also be found [here](https://t.me/kotnehleb).
And [here](https://dev.unleashedflip.com/)
## What is the update server? We have our own update server https://up.unleashedflip.com/directory.json

-It is identical to the official one, it is impossible to change it in applications without rebuilding the application, it is hardcoded there

+It is identical to the official one; it is impossible to change it in applications without rebuilding the application

If you want to use it, you need to patch or build your own build of the application you are interested in
-Also you can use it with uFBT to build apps for UL SDK, uFBT will accept that link as one of args
+Also you can use it with uFBT to build apps for UL SDK - uFBT will accept that link as one of args
The server will remain active and will be automatically updated -## External Radio: How to connect CC1101 module +## External Radio: How to connect the CC1101 module [Guide](https://github.com/quen0n/flipperzero-ext-cc1101) @@ -145,59 +144,59 @@ The server will remain active and will be automatically updated > [!TIP] > -> If you are using Unleashed firmware - **all region locks are removed by default**! +> If you are using Unleashed Firmware - **all region locks are removed by default**! Also, there is a way to go outside of frequencies stated in `CC1101 datasheet`, but transmission on those frequencies may cause chip damage, make sure you know what you are doing! -Do not edit this settings to bypass region lock since there is no region locks in unleashed, all chip supported frequencies will work without any extra steps.

+Do not edit these settings to bypass region lock since there is no region locks in unleashed, all chip supported frequencies will work without any extra steps.

-But, if you know that you need to bypass subghz chip safety restriction you can unlock the safety restriction which will allow you to go outside the chips supported frequency.

+But, if you know that you need to bypass subghz chip safety restrictions - you can unlock the safety restriction which will allow you to go outside the chips supported frequency.

This covers how to do it and information regarding the risks of damage to the flipper by doing so. Please read [this](https://github.com/DarkFlippers/unleashed-firmware/blob/dev/documentation/DangerousSettings.md) before. -## Can I clone a car key fob for my own car to use flipper as a key? +## Can I clone a car key fob for my own car to use my flipper as a key? -No, and trying to do so with Read RAW will lead to key desync or unpair with blacklist which means re-pair is very hard and requires service tools +No, and trying to do so with Read RAW will lead to key desync or unpair with blacklist which means re-pair is very hard to do and requires service tools -## Will Unleashed FW support car keyfobs decoding, cloning, emulating? +## Will Unleashed Firmware support car keyfobs decoding, cloning, emulating? -No, never +No, never! ## Where can I find jamming files? -Nowhere, this is illegal in almost every country in the world +Nowhere, this is illegal in almost every country in the world! -## I saw something on TikTok and want to ask how to do it, I just wanna be like real hacker +## I saw something on TikTok and want to ask how to do it. I just wanna be like real hacker! -And you might be banned for that in our communities, since 99% of that content is fake, or showing illegal actions, and we don't like TikTok related questions. +You might be banned for that in our communities since 99% of that content is fake or illegal. We do NOT like TikTok questions. -## I was banned in Unleashed Discord / Telegram / etc.. How to remove ban? I created GitHub issue and it was removed too! +## I was banned from the Unleashed Discord / Telegram / etc.. How do I get unbanned? I created a GitHub issue, and it was removed too! -Not possible, rules is rules, read them before sending messages in our communities +Not possible, rules are rules, read them before sending messages in our communities ## How to clean .DS_Store and other dot files left from macOS -`sudo dot_clean -mn /Volumes/Flipper\ SD` -> `Flipper\ SD` may be named differently for you, replace with your microSD card name +`sudo dot_clean -mn /Volumes/Flipper\ SD` -> `Flipper\ SD` may be named differently for you, replace it with your microSD card name ## How to sort files on flipper microSD on macOS / Linux? -Will make sorting faster, and will work for OFW +Will make sorting faster, and will work for OFW: 1. `brew install fatsort` -> Install fatsort using `brew.sh` (only on macOS) 2. `diskutil list` -> Find your disk name for flipper microSD 3. `diskutil unmount /Volumes/Flipper\ SD` 4. `sudo fatsort -n /dev/disk4s1` -> Replace `disk4s1` with your microSD id found on step 2 -## Your Flipper feels slow and unresponsive? +## My flipper feels slow and unresponsive? -1. Make sure you using good microSD card from known brand, flipper works with microSD via SPI that means not any microSD will work good even if it works ok with other devices. +1. Make sure you are using a good microSD card from a known brand. Flipper works with microSD via SPI meaning not all microSDs will work well even if they are compatible with other devices. 2. Go into `Settings -> System` and make sure that you have ```text Log Level = None Debug = OFF Heap Trace = None ``` -3. If some of that settings is set to something different - change it to `None` / `OFF` +3. If some settings are set to something different - change them to `None` / `OFF` 4. Make sure your battery is charged, that can affect performance too ## Flipper crashed, stuck, frozen? @@ -206,31 +205,31 @@ Reboot it by holding `Left` + `Back` buttons ![how to reboot flipper gif, shows how to hold left and back button](https://media.tenor.com/eUbBDDEzmwMAAAAC/flipper-zero-flipper-zero-reboot.gif) -## How to reset forgotten Flipper pin code? +## How to reset a forgotten Flipper pin code? **Disconnect USB Cable if it was connected** 1. Turn off the device - hold back button -> `Turn Off` -**If you can't turn it off, try next step but hold buttons for 30-40 seconds)** -2. Hold Up + Back for `~5 sec` -> You will see reset screen -> Hold Right to reset (and Down arrow to exit if you don't want to reset pin code) -3. Done, internal memory (dolphin level, settings, pin code, is erased to default settings) +**If you can't turn it off, try the next step but hold the buttons for 30-40 seconds)** +2. Hold Up + Back for `~5 sec` -> You will see a reset screen -> Hold Right to reset (and Down arrow to exit if you don't want to reset your pin code) +3. Done, internal memory (dolphin level, settings, pin code) is erased to default settings -## What are the differences between x, y and z firmware? +## What are the differences between x, y, and z firmware? If you just got your flipper and not sure what will work better for you, start with original official firmware, if you think you need more features or want to remove subghz region locks then:
* Try installing **Unleashed firmware**, which is fork of official firmware with many new features and preinstalled plugins (check out `e` build).
-* In other case, If you want to experiment more with UI and other things look for existing forks of Unleashed firmware.
+* In other case, If you want to experiment more with UI and other things look for existing forks of Unleashed Firmware.
* Or, create your own fork with your own customisations
-* Also, before reporting any found issue make sure you are in correct repo, if you are using not **Unleashed**, but different fork or original firmware, do not report issue in **Unleashed firmware** repo or UL communities (Telegram, Discord, etc..) +* Also, before reporting any found issues, make sure you are in correct repo, if you are not using **Unleashed**, but a different fork or the original firmware, do not report issue in **Unleashed firmware** repo or UL communities (Telegram, Discord, etc..) -## Is there a correct way to capturing Infrared signals? +## Is there a correct way to capture Infrared signals? -There is indeed especially with AC units, a new documentation has been released with some notes and steps on capturing infrared signals correctly along with some example data so you are able to understand the difference visually between the two. +There is indeed, especially with AC units - a new documentation has been released with some notes and steps on capturing infrared signals correctly along with some example data so you visually able to understand the difference between the two. [More info](https://github.com/DarkFlippers/unleashed-firmware/blob/dev/documentation/InfraredCaptures.md) ## NFC / RFID FAQ -From our good friend `@Equip` and `@np0`
+From our good friends `@Equip` and `@np0`
### MIFARE Ultralight @@ -249,20 +248,20 @@ The Flipper Zero has no available attacks for this card currently. ### Amiibos - `NTAG215`. That's it. It's not going on a MIFARE Classic. -- Currently, you cannot write Amiibos to new physical tags. yet. +- Currently, you cannot write Amiibos to new physical tags. ### HID / iClass - `Picopass` iClass can be read using the `Picopass` reader plugin - 26bit Picopass can be downgraded to H10301 RFID credentials (note, it is not guaranteed to work if the reader is not configured to read low frequency) -- Readers will need to be configured and have an LF RFID antenna in order to be read. Certain iClass readers are HF only, and do not have the ability to have LF configured -- **Emulation for Picopass** was added on July 26th, and the updated version can be found in latest releases of **Unleashed** firmware with apps preinstalled, or in official Apps Hub via Flipper Mobile app -- Write support for personalization mode cards is doable with app -- The Seader app and a [SAM expansion board](https://www.redteamtools.com/nard-sam-expansion-board-for-flipper-zero-with-hid-seos-iclass-sam/) will allow reading more secure HID cards, which may be helpful in downgrade attacks +- Readers will need to be configured and have an LF RFID antenna in order to be read. Certain iClass readers are HF only, and do not have the ability to have LF configured. +- **Emulation for Picopass** was added on July 26th, and the updated version can be found in latest releases of **Unleashed** Firmware with apps preinstalled, or in official Apps Hub via Flipper Mobile app +- Write support for personalization mode cards is doable with the app +- The Seader app and a [SAM expansion board](https://www.redteamtools.com/nard-sam-expansion-board-for-flipper-zero-with-hid-seos-iclass-sam/) will allow you to read more secure HID cards, which may be helpful in downgrade attacks ### LF-RFID -If you're wanting to make clones of low frequency RFID chips you need to write to T5577's. `Blanks` do not exist. All of the chips the Flipper Zero can interact with are read-only and cannot be overwritten or purchased blank. +If you want to make clones of low frequency RFID chips you need to write to T5577's. `Blanks` do not exist. All of the chips the Flipper Zero can interact with are read-only and cannot be overwritten or purchased blank. T5577s are multi-emulator chips that the Flipper Zero can program to be other tags From eef2441af9d06bd093ec36a824f39efbaec73957 Mon Sep 17 00:00:00 2001 From: Dmitry422 Date: Sun, 2 Feb 2025 14:57:41 +0700 Subject: [PATCH 009/268] InputSettings complete. Added VibroTouchLevel --- applications/ReadMe.md | 1 + applications/services/input/application.fam | 1 + applications/services/input/input.c | 75 +++++++++++-- applications/services/input/input.h | 10 +- applications/services/input/input_settings.c | 18 ++- .../services/power/power_service/power.c | 2 +- .../input_settings/input_settings_app.c | 67 ------------ .../application.fam | 6 +- .../input_settings_app/input_settings_app.c | 103 ++++++++++++++++++ .../input_settings_app.h | 12 +- 10 files changed, 197 insertions(+), 98 deletions(-) delete mode 100644 applications/settings/input_settings/input_settings_app.c rename applications/settings/{input_settings => input_settings_app}/application.fam (55%) create mode 100644 applications/settings/input_settings_app/input_settings_app.c rename applications/settings/{input_settings => input_settings_app}/input_settings_app.h (65%) diff --git a/applications/ReadMe.md b/applications/ReadMe.md index a3567df71..02469f252 100644 --- a/applications/ReadMe.md +++ b/applications/ReadMe.md @@ -67,6 +67,7 @@ Small applications providing configuration for basic firmware and its services. - `power_settings_app` - Basic power options - `storage_settings` - Storage settings app - `system` - System settings +- `input_settings_app` - Basic input options ## system diff --git a/applications/services/input/application.fam b/applications/services/input/application.fam index d344fc350..af6a91339 100644 --- a/applications/services/input/application.fam +++ b/applications/services/input/application.fam @@ -7,4 +7,5 @@ App( stack_size=1 * 1024, order=80, sdk_headers=["input.h"], + provides=["input_settings"], ) diff --git a/applications/services/input/input.c b/applications/services/input/input.c index 4ba77cc34..a5635912f 100644 --- a/applications/services/input/input.c +++ b/applications/services/input/input.c @@ -13,6 +13,8 @@ #define INPUT_LONG_PRESS_COUNTS 2 #define INPUT_THREAD_FLAG_ISR 0x00000001 +#define TAG "Input" + /** Input pin state */ typedef struct { const InputPin* pin; @@ -80,27 +82,76 @@ const char* input_get_type_name(InputType type) { } } + +// static void input_storage_callback(const void* message, void* context) { +// furi_assert(context); +// InputSettings* settings = context; +// const StorageEvent* event = message; +// UNUSED (settings); +// if(event->type == StorageEventTypeCardMount) { +// // furi_hal_vibro_on(true); +// // furi_delay_tick (100); +// // furi_hal_vibro_on(false); +// // furi_delay_tick (100); +// // furi_hal_vibro_on(true); +// // furi_delay_tick (100); +// // furi_hal_vibro_on(false); +// // furi_delay_tick (100); +// // furi_hal_vibro_on(true); +// // furi_delay_tick (100); +// // furi_hal_vibro_on(false); +// //input_settings_load(settings); +// } +// } + +// // load inital settings from file for power service +// static void input_init_settings(InputSettings* settings) { +// Storage* storage = furi_record_open(RECORD_STORAGE); +// furi_pubsub_subscribe(storage_get_pubsub(storage), input_storage_callback, settings); + +// if(storage_sd_status(storage) != FSE_OK) { +// FURI_LOG_D(TAG, "SD Card not ready, skipping settings"); +// //set default value +// settings->vibro_touch_level=0; +// //furi_record_close(RECORD_STORAGE); +// return; +// } + + // furi_hal_vibro_on(true); + // furi_delay_tick (100); + // furi_hal_vibro_on(false); + // furi_delay_tick (100); + // furi_hal_vibro_on(true); + // furi_delay_tick (100); + // furi_hal_vibro_on(false); + +// input_settings_load(settings); +// furi_record_close(RECORD_STORAGE); +// } + // allocate memory for input_settings structure static InputSettings* input_settings_alloc (void) { - InputSettings* input_settings = malloc(sizeof(InputSettings)); - return input_settings; + InputSettings* settings = malloc(sizeof(InputSettings)); + return settings; } //free memory from input_settings structure -void input_settings_free (InputSettings* input_settings) { - free (input_settings); +void input_settings_free (InputSettings* settings) { + free (settings); } int32_t input_srv(void* p) { UNUSED(p); - //define object input_settings and take memory for him - InputSettings* input_settings = input_settings_alloc(); - const FuriThreadId thread_id = furi_thread_get_current_id(); FuriPubSub* event_pubsub = furi_pubsub_alloc(); - uint32_t counter = 1; + uint32_t counter = 1; furi_record_create(RECORD_INPUT_EVENTS, event_pubsub); + + //define object input_settings, take memory load (or init) settings and create record for access to settings structure outside + InputSettings* settings = input_settings_alloc(); + furi_record_create(RECORD_INPUT_SETTINGS, settings); + input_settings_load(settings); #ifdef INPUT_DEBUG furi_hal_gpio_init_simple(&gpio_ext_pa4, GpioModeOutputPushPull); @@ -164,9 +215,10 @@ int32_t input_srv(void* p) { // Send Press/Release event event.type = pin_states[i].state ? InputTypePress : InputTypeRelease; furi_pubsub_publish(event_pubsub, &event); - if (input_settings->vibro_touch_level) { + // do vibro if user setup vibro touch level in Settings-Input. + if (settings->vibro_touch_level) { furi_hal_vibro_on(true); - furi_delay_tick (30); + furi_delay_tick (settings->vibro_touch_level); furi_hal_vibro_on(false); }; } @@ -185,7 +237,6 @@ int32_t input_srv(void* p) { } } - // if we come here and ready exit from service (whats going on ??!!) then best practice is free memory - input_settings_free(input_settings); + return 0; } diff --git a/applications/services/input/input.h b/applications/services/input/input.h index 3d8d6c9ec..650a95b51 100644 --- a/applications/services/input/input.h +++ b/applications/services/input/input.h @@ -7,12 +7,15 @@ #include #include "input_settings.h" +#include + #ifdef __cplusplus extern "C" { #endif #define RECORD_INPUT_EVENTS "input_events" +#define RECORD_INPUT_SETTINGS "input_settings" #define INPUT_SEQUENCE_SOURCE_HARDWARE (0u) #define INPUT_SEQUENCE_SOURCE_SOFTWARE (1u) @@ -41,13 +44,6 @@ typedef struct { InputType type; } InputEvent; -//for next step input structure globalization; -//typedef struct Input { - //InputSettings* settings; - //InputType* type; - //InputEvent* event; -//} Input; - /** Get human readable input key name * @param key - InputKey * @return string diff --git a/applications/services/input/input_settings.c b/applications/services/input/input_settings.c index fc738c004..34c01077d 100644 --- a/applications/services/input/input_settings.c +++ b/applications/services/input/input_settings.c @@ -21,18 +21,21 @@ void input_settings_load(InputSettings* settings) { bool success = false; + //a useless cycle do-while, may will be used in future with anoter condition do { + // take version from settings file metadata, if cant then break and fill settings with 0 and save to settings file; uint8_t version; if(!saved_struct_get_metadata(INPUT_SETTINGS_PATH, NULL, &version, NULL)) break; - - if(version == INPUT_SETTINGS_VER) { // if config actual version - load it directly + + // if config actual version - load it directly + if(version == INPUT_SETTINGS_VER) { success = saved_struct_load( INPUT_SETTINGS_PATH, settings, sizeof(InputSettings), INPUT_SETTINGS_MAGIC, INPUT_SETTINGS_VER); - + // if config previous version - load it and inicialize new settings } else if( version == INPUT_SETTINGS_VER_0) { // if config previous version - load it and manual set new settings to inital value @@ -51,9 +54,10 @@ void input_settings_load(InputSettings* settings) { free(settings_v0); } - + // in case of another config version we exit from useless cycle to next step } while(false); + // fill settings with 0 and save to settings file; if(!success) { FURI_LOG_W(TAG, "Failed to load file, using defaults"); memset(settings, 0, sizeof(InputSettings)); @@ -71,6 +75,12 @@ void input_settings_save(const InputSettings* settings) { INPUT_SETTINGS_MAGIC, INPUT_SETTINGS_VER); + // debug log + // FURI_LOG_D(TAG,"SAVE"); + // char buffer[12] = {}; + // snprintf(buffer, sizeof(buffer), "%d",settings->vibro_touch_level); + // FURI_LOG_D(TAG,buffer); + if(!success) { FURI_LOG_E(TAG, "Failed to save file"); } diff --git a/applications/services/power/power_service/power.c b/applications/services/power/power_service/power.c index 511c64c78..252be5027 100644 --- a/applications/services/power/power_service/power.c +++ b/applications/services/power/power_service/power.c @@ -615,7 +615,7 @@ static Power* power_alloc(void) { free(settings); // auto_poweroff - //---define subscription to loader events message (info about started apps) and defina callback for this + //---define subscription to loader events message (info about started apps) and define callback for this Loader* loader = furi_record_open(RECORD_LOADER); furi_pubsub_subscribe(loader_get_pubsub(loader), power_loader_callback, power); power->input_events_pubsub = furi_record_open(RECORD_INPUT_EVENTS); diff --git a/applications/settings/input_settings/input_settings_app.c b/applications/settings/input_settings/input_settings_app.c deleted file mode 100644 index 943bbc02f..000000000 --- a/applications/settings/input_settings/input_settings_app.c +++ /dev/null @@ -1,67 +0,0 @@ -#include -#include "input_settings_app.h" - -#define VIBRO_TOUCH_LEVEL_COUNT 10 -// vibro touch human readable levels -const char* const vibro_touch_level_text[VIBRO_TOUCH_LEVEL_COUNT] = { - "OFF", - "1", - "2", - "3", - "4", - "5", - "6", - "7", - "8", - "9", -}; -// vibro touch levels tick valies delay -const uint32_t vibro_touch_level_value[VIBRO_TOUCH_LEVEL_COUNT] = - {0,13,16,19,21,24,27,30,33,36}; - -input_settings_start_vibro_touch_level_changed (){ - -} - -InputSettingsApp* input_settings_alloc (void) { - InputSettingsApp* app = malloc(sizeof(InputSettingsApp)); - - app->gui = furi_record_open (RECORD_GUI); - app->view_dispatcher = view_dispatcher_alloc (); - - VariableItem* item; - app->var_item_list = variable_item_list_alloc(); - - item = variable_item_list_add( - variable_item_list, - "Vibro touch level", - VIBRO_TOUCH_LEVEL_COUNT, - input_settings_start_vibro_touch_level_changed, - app); - - value_index = value_index_uint32( - app->settings.vibro_touch_level, vibro_touch_level_value, VIBRO_TOUCH_LEVEL_COUNT); - variable_item_set_current_value_index(item, value_index); - variable_item_set_current_value_text(item, vibro_touch_level_text[value_index]); - - -return app; -} - - -// Enter point -int32_t input_settings_app (void* p) { - UNUSED (p); - - //take memory - InputSettingsApp* app = input_settings_alloc (); - - //start view_dispatcher - view_dispatcher_run(app->view_dispatcher); - - //free memory - Input_settings_free (app); - - //exit with 0 code - return 0; -}; \ No newline at end of file diff --git a/applications/settings/input_settings/application.fam b/applications/settings/input_settings_app/application.fam similarity index 55% rename from applications/settings/input_settings/application.fam rename to applications/settings/input_settings_app/application.fam index f137c2512..14be52fc4 100644 --- a/applications/settings/input_settings/application.fam +++ b/applications/settings/input_settings_app/application.fam @@ -1,9 +1,9 @@ App( appid="input_settings", name="Input", - apptype=FlipperAppType,SETTINGS, + apptype=FlipperAppType.SETTINGS, entry_point="input_settings_app", - requires=["gui"], - stack_size= 1 * 1024, + requires=["input"], + stack_size=1 * 1024, order=100, ) diff --git a/applications/settings/input_settings_app/input_settings_app.c b/applications/settings/input_settings_app/input_settings_app.c new file mode 100644 index 000000000..452f7ac52 --- /dev/null +++ b/applications/settings/input_settings_app/input_settings_app.c @@ -0,0 +1,103 @@ +#include +#include "input_settings_app.h" + +#define TAG "InputSettingsApp" + +#define VIBRO_TOUCH_LEVEL_COUNT 10 +// vibro touch human readable levels +const char* const vibro_touch_level_text[VIBRO_TOUCH_LEVEL_COUNT] = { + "OFF","1","2","3","4","5","6","7","8","9",}; +// vibro touch levels tick valies delay +const uint32_t vibro_touch_level_value[VIBRO_TOUCH_LEVEL_COUNT] = + {0,13,16,19,21,24,27,30,33,36}; + +static void input_settings_vibro_touch_level_changed (VariableItem* item) { + uint8_t index = variable_item_get_current_value_index(item); + variable_item_set_current_value_text(item, vibro_touch_level_text[index]); + + //change settings to selected + InputSettingsApp* app = variable_item_get_context(item); + app->settings->vibro_touch_level = vibro_touch_level_value[index]; + + // use RECORD for acces to input service instance and set settings + InputSettings* service_settings = furi_record_open (RECORD_INPUT_SETTINGS); + service_settings->vibro_touch_level = vibro_touch_level_value[index]; + furi_record_close (RECORD_INPUT_SETTINGS); + +} + +static uint32_t input_settings_app_exit(void* context) { + UNUSED(context); + return VIEW_NONE; +} + +InputSettingsApp* input_settings_app_alloc (void) { + InputSettingsApp* app = malloc(sizeof(InputSettingsApp)); + //app->inputservice = furi_record_open(RECORD_INPUT_EVENTS); + + app->gui = furi_record_open (RECORD_GUI); + + app->settings = malloc(sizeof(InputSettings)); + input_settings_load (app->settings); + + app->variable_item_list = variable_item_list_alloc(); + View* view = variable_item_list_get_view(app->variable_item_list); + view_set_previous_callback(view, input_settings_app_exit); + + VariableItem* item; + uint8_t value_index; + + item = variable_item_list_add( + app->variable_item_list,"VibroTouchLevel",VIBRO_TOUCH_LEVEL_COUNT,input_settings_vibro_touch_level_changed,app); + + value_index = value_index_uint32( + app->settings->vibro_touch_level, vibro_touch_level_value, VIBRO_TOUCH_LEVEL_COUNT); + variable_item_set_current_value_index(item, value_index); + variable_item_set_current_value_text(item, vibro_touch_level_text[value_index]); + + // create and setup view and view dispatcher + app->view_dispatcher = view_dispatcher_alloc (); + view_dispatcher_set_event_callback_context(app->view_dispatcher, app); + view_dispatcher_attach_to_gui(app->view_dispatcher, app->gui, ViewDispatcherTypeFullscreen); + view_dispatcher_add_view(app->view_dispatcher,InputSettingsViewVariableItemList,view); + view_dispatcher_switch_to_view(app->view_dispatcher, InputSettingsViewVariableItemList); + +return app; +} + +void input_settings_app_free (InputSettingsApp* app){ + furi_assert(app); + + // Variable item list + view_dispatcher_remove_view(app->view_dispatcher, InputSettingsViewVariableItemList); + variable_item_list_free(app->variable_item_list); + + // View dispatcher + view_dispatcher_free(app->view_dispatcher); + + // Records + furi_record_close(RECORD_GUI); + //furi_record_close(RECORD_INPUT_EVENTS); + free (app->settings); + free(app); +} + +// Enter point +int32_t input_settings_app (void* p) { + UNUSED (p); + InputSettingsApp* app = input_settings_app_alloc (); + + view_dispatcher_run(app->view_dispatcher); + + // // debug code + // FURI_LOG_D(TAG,"Vibro Touch level before save"); + // char buffer[12] = {}; + // snprintf(buffer, sizeof(buffer), "%d",app->settings->vibro_touch_level); + // FURI_LOG_D(TAG,buffer); + + //save current settings; + input_settings_save(app->settings); + + input_settings_app_free (app); + return 0; +}; \ No newline at end of file diff --git a/applications/settings/input_settings/input_settings_app.h b/applications/settings/input_settings_app/input_settings_app.h similarity index 65% rename from applications/settings/input_settings/input_settings_app.h rename to applications/settings/input_settings_app/input_settings_app.h index 7ba29b511..c65597ea6 100644 --- a/applications/settings/input_settings/input_settings_app.h +++ b/applications/settings/input_settings_app/input_settings_app.h @@ -6,15 +6,19 @@ #include #include #include -#include "input/input_settings.h" +#include +#include +#include +#include -// app stucture + +// input_settings_app stucture typedef struct { -InputSettings* settings; -Input* input; +//InputService* inputservice; //link to input_sevice with they setings and events Gui* gui; ViewDispatcher* view_dispatcher; VariableItemList* variable_item_list; +InputSettings* settings; } InputSettingsApp; // list of menu views for view dispatcher From a5c241139d61bc49db4ef75b2116accc9cd53ae2 Mon Sep 17 00:00:00 2001 From: Dmitry422 Date: Sun, 2 Feb 2025 15:13:55 +0700 Subject: [PATCH 010/268] Source code C\C++ formating --- applications/services/input/input.c | 32 +++++----- applications/services/input/input.h | 1 - applications/services/input/input_settings.c | 16 ++--- .../input_settings_app/input_settings_app.c | 63 +++++++++++-------- .../input_settings_app/input_settings_app.h | 13 ++-- 5 files changed, 67 insertions(+), 58 deletions(-) diff --git a/applications/services/input/input.c b/applications/services/input/input.c index a5635912f..5e2fdb9d5 100644 --- a/applications/services/input/input.c +++ b/applications/services/input/input.c @@ -82,7 +82,6 @@ const char* input_get_type_name(InputType type) { } } - // static void input_storage_callback(const void* message, void* context) { // furi_assert(context); // InputSettings* settings = context; @@ -116,28 +115,28 @@ const char* input_get_type_name(InputType type) { // //furi_record_close(RECORD_STORAGE); // return; // } - - // furi_hal_vibro_on(true); - // furi_delay_tick (100); - // furi_hal_vibro_on(false); - // furi_delay_tick (100); - // furi_hal_vibro_on(true); - // furi_delay_tick (100); - // furi_hal_vibro_on(false); + +// furi_hal_vibro_on(true); +// furi_delay_tick (100); +// furi_hal_vibro_on(false); +// furi_delay_tick (100); +// furi_hal_vibro_on(true); +// furi_delay_tick (100); +// furi_hal_vibro_on(false); // input_settings_load(settings); // furi_record_close(RECORD_STORAGE); // } // allocate memory for input_settings structure -static InputSettings* input_settings_alloc (void) { +static InputSettings* input_settings_alloc(void) { InputSettings* settings = malloc(sizeof(InputSettings)); return settings; } //free memory from input_settings structure -void input_settings_free (InputSettings* settings) { - free (settings); +void input_settings_free(InputSettings* settings) { + free(settings); } int32_t input_srv(void* p) { @@ -145,9 +144,9 @@ int32_t input_srv(void* p) { const FuriThreadId thread_id = furi_thread_get_current_id(); FuriPubSub* event_pubsub = furi_pubsub_alloc(); - uint32_t counter = 1; + uint32_t counter = 1; furi_record_create(RECORD_INPUT_EVENTS, event_pubsub); - + //define object input_settings, take memory load (or init) settings and create record for access to settings structure outside InputSettings* settings = input_settings_alloc(); furi_record_create(RECORD_INPUT_SETTINGS, settings); @@ -216,9 +215,9 @@ int32_t input_srv(void* p) { event.type = pin_states[i].state ? InputTypePress : InputTypeRelease; furi_pubsub_publish(event_pubsub, &event); // do vibro if user setup vibro touch level in Settings-Input. - if (settings->vibro_touch_level) { + if(settings->vibro_touch_level) { furi_hal_vibro_on(true); - furi_delay_tick (settings->vibro_touch_level); + furi_delay_tick(settings->vibro_touch_level); furi_hal_vibro_on(false); }; } @@ -237,6 +236,5 @@ int32_t input_srv(void* p) { } } - return 0; } diff --git a/applications/services/input/input.h b/applications/services/input/input.h index 650a95b51..92dbfeb68 100644 --- a/applications/services/input/input.h +++ b/applications/services/input/input.h @@ -9,7 +9,6 @@ #include "input_settings.h" #include - #ifdef __cplusplus extern "C" { #endif diff --git a/applications/services/input/input_settings.c b/applications/services/input/input_settings.c index 34c01077d..2868e5974 100644 --- a/applications/services/input/input_settings.c +++ b/applications/services/input/input_settings.c @@ -26,16 +26,16 @@ void input_settings_load(InputSettings* settings) { // take version from settings file metadata, if cant then break and fill settings with 0 and save to settings file; uint8_t version; if(!saved_struct_get_metadata(INPUT_SETTINGS_PATH, NULL, &version, NULL)) break; - + // if config actual version - load it directly - if(version == INPUT_SETTINGS_VER) { + if(version == INPUT_SETTINGS_VER) { success = saved_struct_load( INPUT_SETTINGS_PATH, settings, sizeof(InputSettings), INPUT_SETTINGS_MAGIC, INPUT_SETTINGS_VER); - // if config previous version - load it and inicialize new settings + // if config previous version - load it and inicialize new settings } else if( version == INPUT_SETTINGS_VER_0) { // if config previous version - load it and manual set new settings to inital value @@ -75,11 +75,11 @@ void input_settings_save(const InputSettings* settings) { INPUT_SETTINGS_MAGIC, INPUT_SETTINGS_VER); - // debug log - // FURI_LOG_D(TAG,"SAVE"); - // char buffer[12] = {}; - // snprintf(buffer, sizeof(buffer), "%d",settings->vibro_touch_level); - // FURI_LOG_D(TAG,buffer); + // debug log + // FURI_LOG_D(TAG,"SAVE"); + // char buffer[12] = {}; + // snprintf(buffer, sizeof(buffer), "%d",settings->vibro_touch_level); + // FURI_LOG_D(TAG,buffer); if(!success) { FURI_LOG_E(TAG, "Failed to save file"); diff --git a/applications/settings/input_settings_app/input_settings_app.c b/applications/settings/input_settings_app/input_settings_app.c index 452f7ac52..0f25900c1 100644 --- a/applications/settings/input_settings_app/input_settings_app.c +++ b/applications/settings/input_settings_app/input_settings_app.c @@ -6,24 +6,33 @@ #define VIBRO_TOUCH_LEVEL_COUNT 10 // vibro touch human readable levels const char* const vibro_touch_level_text[VIBRO_TOUCH_LEVEL_COUNT] = { - "OFF","1","2","3","4","5","6","7","8","9",}; + "OFF", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", +}; // vibro touch levels tick valies delay const uint32_t vibro_touch_level_value[VIBRO_TOUCH_LEVEL_COUNT] = - {0,13,16,19,21,24,27,30,33,36}; + {0, 13, 16, 19, 21, 24, 27, 30, 33, 36}; -static void input_settings_vibro_touch_level_changed (VariableItem* item) { +static void input_settings_vibro_touch_level_changed(VariableItem* item) { uint8_t index = variable_item_get_current_value_index(item); variable_item_set_current_value_text(item, vibro_touch_level_text[index]); - + //change settings to selected InputSettingsApp* app = variable_item_get_context(item); app->settings->vibro_touch_level = vibro_touch_level_value[index]; // use RECORD for acces to input service instance and set settings - InputSettings* service_settings = furi_record_open (RECORD_INPUT_SETTINGS); + InputSettings* service_settings = furi_record_open(RECORD_INPUT_SETTINGS); service_settings->vibro_touch_level = vibro_touch_level_value[index]; - furi_record_close (RECORD_INPUT_SETTINGS); - + furi_record_close(RECORD_INPUT_SETTINGS); } static uint32_t input_settings_app_exit(void* context) { @@ -31,15 +40,15 @@ static uint32_t input_settings_app_exit(void* context) { return VIEW_NONE; } -InputSettingsApp* input_settings_app_alloc (void) { +InputSettingsApp* input_settings_app_alloc(void) { InputSettingsApp* app = malloc(sizeof(InputSettingsApp)); //app->inputservice = furi_record_open(RECORD_INPUT_EVENTS); - - app->gui = furi_record_open (RECORD_GUI); + + app->gui = furi_record_open(RECORD_GUI); app->settings = malloc(sizeof(InputSettings)); - input_settings_load (app->settings); - + input_settings_load(app->settings); + app->variable_item_list = variable_item_list_alloc(); View* view = variable_item_list_get_view(app->variable_item_list); view_set_previous_callback(view, input_settings_app_exit); @@ -48,7 +57,11 @@ InputSettingsApp* input_settings_app_alloc (void) { uint8_t value_index; item = variable_item_list_add( - app->variable_item_list,"VibroTouchLevel",VIBRO_TOUCH_LEVEL_COUNT,input_settings_vibro_touch_level_changed,app); + app->variable_item_list, + "VibroTouchLevel", + VIBRO_TOUCH_LEVEL_COUNT, + input_settings_vibro_touch_level_changed, + app); value_index = value_index_uint32( app->settings->vibro_touch_level, vibro_touch_level_value, VIBRO_TOUCH_LEVEL_COUNT); @@ -56,16 +69,16 @@ InputSettingsApp* input_settings_app_alloc (void) { variable_item_set_current_value_text(item, vibro_touch_level_text[value_index]); // create and setup view and view dispatcher - app->view_dispatcher = view_dispatcher_alloc (); + app->view_dispatcher = view_dispatcher_alloc(); view_dispatcher_set_event_callback_context(app->view_dispatcher, app); view_dispatcher_attach_to_gui(app->view_dispatcher, app->gui, ViewDispatcherTypeFullscreen); - view_dispatcher_add_view(app->view_dispatcher,InputSettingsViewVariableItemList,view); + view_dispatcher_add_view(app->view_dispatcher, InputSettingsViewVariableItemList, view); view_dispatcher_switch_to_view(app->view_dispatcher, InputSettingsViewVariableItemList); -return app; + return app; } -void input_settings_app_free (InputSettingsApp* app){ +void input_settings_app_free(InputSettingsApp* app) { furi_assert(app); // Variable item list @@ -74,18 +87,18 @@ void input_settings_app_free (InputSettingsApp* app){ // View dispatcher view_dispatcher_free(app->view_dispatcher); - + // Records furi_record_close(RECORD_GUI); //furi_record_close(RECORD_INPUT_EVENTS); - free (app->settings); + free(app->settings); free(app); } -// Enter point -int32_t input_settings_app (void* p) { - UNUSED (p); - InputSettingsApp* app = input_settings_app_alloc (); +// Enter point +int32_t input_settings_app(void* p) { + UNUSED(p); + InputSettingsApp* app = input_settings_app_alloc(); view_dispatcher_run(app->view_dispatcher); @@ -98,6 +111,6 @@ int32_t input_settings_app (void* p) { //save current settings; input_settings_save(app->settings); - input_settings_app_free (app); + input_settings_app_free(app); return 0; -}; \ No newline at end of file +} diff --git a/applications/settings/input_settings_app/input_settings_app.h b/applications/settings/input_settings_app/input_settings_app.h index c65597ea6..e7accc772 100644 --- a/applications/settings/input_settings_app/input_settings_app.h +++ b/applications/settings/input_settings_app/input_settings_app.h @@ -11,17 +11,16 @@ #include #include - // input_settings_app stucture typedef struct { -//InputService* inputservice; //link to input_sevice with they setings and events -Gui* gui; -ViewDispatcher* view_dispatcher; -VariableItemList* variable_item_list; -InputSettings* settings; + //InputService* inputservice; //link to input_sevice with they setings and events + Gui* gui; + ViewDispatcher* view_dispatcher; + VariableItemList* variable_item_list; + InputSettings* settings; } InputSettingsApp; // list of menu views for view dispatcher typedef enum { InputSettingsViewVariableItemList, -} InputSettingsView; \ No newline at end of file +} InputSettingsView; From 8c31d8a682b4fc1652a63464371eac6bbbc1d92f Mon Sep 17 00:00:00 2001 From: Dmitry422 Date: Sun, 2 Feb 2025 21:41:30 +0700 Subject: [PATCH 011/268] Source code cosmetic changes --- applications/services/input/input.c | 48 +------------------ .../input_settings_app/input_settings_app.c | 1 - .../input_settings_app/input_settings_app.h | 1 - 3 files changed, 1 insertion(+), 49 deletions(-) diff --git a/applications/services/input/input.c b/applications/services/input/input.c index 5e2fdb9d5..2b697d6ce 100644 --- a/applications/services/input/input.c +++ b/applications/services/input/input.c @@ -82,52 +82,6 @@ const char* input_get_type_name(InputType type) { } } -// static void input_storage_callback(const void* message, void* context) { -// furi_assert(context); -// InputSettings* settings = context; -// const StorageEvent* event = message; -// UNUSED (settings); -// if(event->type == StorageEventTypeCardMount) { -// // furi_hal_vibro_on(true); -// // furi_delay_tick (100); -// // furi_hal_vibro_on(false); -// // furi_delay_tick (100); -// // furi_hal_vibro_on(true); -// // furi_delay_tick (100); -// // furi_hal_vibro_on(false); -// // furi_delay_tick (100); -// // furi_hal_vibro_on(true); -// // furi_delay_tick (100); -// // furi_hal_vibro_on(false); -// //input_settings_load(settings); -// } -// } - -// // load inital settings from file for power service -// static void input_init_settings(InputSettings* settings) { -// Storage* storage = furi_record_open(RECORD_STORAGE); -// furi_pubsub_subscribe(storage_get_pubsub(storage), input_storage_callback, settings); - -// if(storage_sd_status(storage) != FSE_OK) { -// FURI_LOG_D(TAG, "SD Card not ready, skipping settings"); -// //set default value -// settings->vibro_touch_level=0; -// //furi_record_close(RECORD_STORAGE); -// return; -// } - -// furi_hal_vibro_on(true); -// furi_delay_tick (100); -// furi_hal_vibro_on(false); -// furi_delay_tick (100); -// furi_hal_vibro_on(true); -// furi_delay_tick (100); -// furi_hal_vibro_on(false); - -// input_settings_load(settings); -// furi_record_close(RECORD_STORAGE); -// } - // allocate memory for input_settings structure static InputSettings* input_settings_alloc(void) { InputSettings* settings = malloc(sizeof(InputSettings)); @@ -147,7 +101,7 @@ int32_t input_srv(void* p) { uint32_t counter = 1; furi_record_create(RECORD_INPUT_EVENTS, event_pubsub); - //define object input_settings, take memory load (or init) settings and create record for access to settings structure outside + //define object input_settings, take memory load (or init) settings and create record for access to settings structure from outside InputSettings* settings = input_settings_alloc(); furi_record_create(RECORD_INPUT_SETTINGS, settings); input_settings_load(settings); diff --git a/applications/settings/input_settings_app/input_settings_app.c b/applications/settings/input_settings_app/input_settings_app.c index 0f25900c1..2902625ae 100644 --- a/applications/settings/input_settings_app/input_settings_app.c +++ b/applications/settings/input_settings_app/input_settings_app.c @@ -90,7 +90,6 @@ void input_settings_app_free(InputSettingsApp* app) { // Records furi_record_close(RECORD_GUI); - //furi_record_close(RECORD_INPUT_EVENTS); free(app->settings); free(app); } diff --git a/applications/settings/input_settings_app/input_settings_app.h b/applications/settings/input_settings_app/input_settings_app.h index e7accc772..c96ca49e9 100644 --- a/applications/settings/input_settings_app/input_settings_app.h +++ b/applications/settings/input_settings_app/input_settings_app.h @@ -13,7 +13,6 @@ // input_settings_app stucture typedef struct { - //InputService* inputservice; //link to input_sevice with they setings and events Gui* gui; ViewDispatcher* view_dispatcher; VariableItemList* variable_item_list; From cce8a7c4130f9c39a352cb346c1153c82df6e73e Mon Sep 17 00:00:00 2001 From: Dmitry422 Date: Mon, 3 Feb 2025 19:18:53 +0700 Subject: [PATCH 012/268] Begin work with SafeCharging (charging supress) --- .../power/power_service/power_settings.c | 31 ++++++------- .../power/power_service/power_settings.h | 3 ++ .../scenes/power_settings_scene_start.c | 45 ++++++++++++++----- 3 files changed, 54 insertions(+), 25 deletions(-) diff --git a/applications/services/power/power_service/power_settings.c b/applications/services/power/power_service/power_settings.c index 139323a7a..93657823d 100644 --- a/applications/services/power/power_service/power_settings.c +++ b/applications/services/power/power_service/power_settings.c @@ -6,15 +6,15 @@ #define TAG "PowerSettings" -#define POWER_SETTINGS_VER_0 (0) // OLD version number -#define POWER_SETTINGS_VER (1) // NEW actual version nnumber +#define POWER_SETTINGS_VER_1 (1) // Previous version number +#define POWER_SETTINGS_VER (2) // New version number #define POWER_SETTINGS_PATH INT_PATH(POWER_SETTINGS_FILE_NAME) #define POWER_SETTINGS_MAGIC (0x18) typedef struct { - //inital set - empty -} PowerSettingsV0; + uint32_t auto_poweroff_delay_ms; +} PowerSettingsPrevious; void power_settings_load(PowerSettings* settings) { furi_assert(settings); @@ -25,7 +25,8 @@ void power_settings_load(PowerSettings* settings) { uint8_t version; if(!saved_struct_get_metadata(POWER_SETTINGS_PATH, NULL, &version, NULL)) break; - if(version == POWER_SETTINGS_VER) { // if config actual version - load it directly + // if config actual version - load it directly + if(version == POWER_SETTINGS_VER) { success = saved_struct_load( POWER_SETTINGS_PATH, settings, @@ -33,23 +34,23 @@ void power_settings_load(PowerSettings* settings) { POWER_SETTINGS_MAGIC, POWER_SETTINGS_VER); - } else if( - version == - POWER_SETTINGS_VER_0) { // if config previous version - load it and manual set new settings to inital value - PowerSettingsV0* settings_v0 = malloc(sizeof(PowerSettingsV0)); + // if config previous version - load it and manual set new settings to inital value + } else if(version == POWER_SETTINGS_VER_1) { + PowerSettingsPrevious* settings_previous = malloc(sizeof(PowerSettingsPrevious)); success = saved_struct_load( POWER_SETTINGS_PATH, - settings_v0, - sizeof(PowerSettingsV0), + settings_previous, + sizeof(PowerSettingsPrevious), POWER_SETTINGS_MAGIC, - POWER_SETTINGS_VER_0); - + POWER_SETTINGS_VER_1); + // new settings initialization if(success) { - settings->auto_poweroff_delay_ms = 0; + settings->charge_supress_percent = 0; + settings->charge_is_supressed = false; } - free(settings_v0); + free(settings_previous); } } while(false); diff --git a/applications/services/power/power_service/power_settings.h b/applications/services/power/power_service/power_settings.h index b1cc71001..c07199cf8 100644 --- a/applications/services/power/power_service/power_settings.h +++ b/applications/services/power/power_service/power_settings.h @@ -1,9 +1,12 @@ #pragma once #include +#include typedef struct { uint32_t auto_poweroff_delay_ms; + uint8_t charge_supress_percent; + bool charge_is_supressed; } PowerSettings; #ifdef __cplusplus diff --git a/applications/settings/power_settings_app/scenes/power_settings_scene_start.c b/applications/settings/power_settings_app/scenes/power_settings_scene_start.c index 3fc3a8a30..0d23d3915 100644 --- a/applications/settings/power_settings_app/scenes/power_settings_scene_start.c +++ b/applications/settings/power_settings_app/scenes/power_settings_scene_start.c @@ -15,6 +15,20 @@ const char* const auto_poweroff_delay_text[AUTO_POWEROFF_DELAY_COUNT] = const uint32_t auto_poweroff_delay_value[AUTO_POWEROFF_DELAY_COUNT] = {0, 300000, 600000, 900000, 1800000, 2700000, 3600000, 5400000}; +#define CHARGE_SUPRESS_PERCENT_COUNT 2 +const char* const charge_supress_percent_text[CHARGE_SUPRESS_PERCENT_COUNT] = {"OFF", "80%"}; + +const uint32_t charge_supress_percent_value[CHARGE_SUPRESS_PERCENT_COUNT] = {0, 80}; + +// change variable_item_list visible text and charge_supress_percent_settings when user change item in variable_item_list +static void power_settings_scene_start_charge_supress_percent_changed(VariableItem* item) { + PowerSettingsApp* app = variable_item_get_context(item); + uint8_t index = variable_item_get_current_value_index(item); + + variable_item_set_current_value_text(item, charge_supress_percent_text[index]); + app->settings.charge_supress_percent = charge_supress_percent_value[index]; +} + // change variable_item_list visible text and app_poweroff_delay_time_settings when user change item in variable_item_list static void power_settings_scene_start_auto_poweroff_delay_changed(VariableItem* item) { PowerSettingsApp* app = variable_item_get_context(item); @@ -24,9 +38,8 @@ static void power_settings_scene_start_auto_poweroff_delay_changed(VariableItem* app->settings.auto_poweroff_delay_ms = auto_poweroff_delay_value[index]; } -static void power_settings_scene_start_submenu_callback( - void* context, - uint32_t index) { //show selected menu screen +static void power_settings_scene_start_submenu_callback(void* context, uint32_t index) { + //show selected menu screen by index furi_assert(context); PowerSettingsApp* app = context; view_dispatcher_send_custom_event(app->view_dispatcher, index); @@ -42,11 +55,12 @@ void power_settings_scene_start_on_enter(void* context) { VariableItem* item; uint8_t value_index; + item = variable_item_list_add( variable_item_list, "Auto PowerOff", AUTO_POWEROFF_DELAY_COUNT, - power_settings_scene_start_auto_poweroff_delay_changed, //function for change visible item list value and app settings + power_settings_scene_start_auto_poweroff_delay_changed, app); value_index = value_index_uint32( @@ -56,14 +70,25 @@ void power_settings_scene_start_on_enter(void* context) { variable_item_set_current_value_index(item, value_index); variable_item_set_current_value_text(item, auto_poweroff_delay_text[value_index]); + item = variable_item_list_add( + variable_item_list, + "Safe charging", + CHARGE_SUPRESS_PERCENT_COUNT, + power_settings_scene_start_charge_supress_percent_changed, + app); + + value_index = value_index_uint32( + app->settings.charge_supress_percent, + charge_supress_percent_value, + CHARGE_SUPRESS_PERCENT_COUNT); + variable_item_set_current_value_index(item, value_index); + variable_item_set_current_value_text(item, charge_supress_percent_text[value_index]); + variable_item_list_set_selected_item( variable_item_list, scene_manager_get_scene_state(app->scene_manager, PowerSettingsAppSceneStart)); - variable_item_list_set_enter_callback( //callback to show next mennu screen - variable_item_list, - power_settings_scene_start_submenu_callback, - app); - + variable_item_list_set_enter_callback( + variable_item_list, power_settings_scene_start_submenu_callback, app); view_dispatcher_switch_to_view(app->view_dispatcher, PowerSettingsAppViewVariableItemList); } @@ -88,5 +113,5 @@ bool power_settings_scene_start_on_event(void* context, SceneManagerEvent event) void power_settings_scene_start_on_exit(void* context) { PowerSettingsApp* app = context; variable_item_list_reset(app->variable_item_list); - power_settings_save(&app->settings); //actual need save every time when use ? + power_settings_save(&app->settings); } From 724c4ca445860bb3b000fd8a55439e2c67ed6e9a Mon Sep 17 00:00:00 2001 From: Dmitry422 Date: Tue, 4 Feb 2025 00:42:52 +0700 Subject: [PATCH 013/268] SafeCharging (charging supress) finished. --- .../services/power/power_service/power.c | 27 ++++++++++++++++--- .../services/power/power_service/power_i.h | 1 + .../power/power_service/power_settings.h | 1 - .../scenes/power_settings_scene_start.c | 9 ++++--- 4 files changed, 30 insertions(+), 8 deletions(-) diff --git a/applications/services/power/power_service/power.c b/applications/services/power/power_service/power.c index 252be5027..c6e1147d7 100644 --- a/applications/services/power/power_service/power.c +++ b/applications/services/power/power_service/power.c @@ -442,7 +442,7 @@ static void power_auto_poweroff_timer_callback(void* context) { Power* power = context; //Dont poweroff device if charger connected - if (furi_hal_power_is_charging()) { + if(furi_hal_power_is_charging()) { FURI_LOG_D(TAG, "We dont auto_power_off until battery is charging"); power_start_auto_poweroff_timer(power); } else { @@ -548,6 +548,24 @@ static void power_message_callback(FuriEventLoopObject* object, void* context) { } } +static void power_charge_supress(Power* power) { + // if charge_supress_percent selected (not OFF) and current charge level equal or higher than selected level + // then we start supression if we not supress it before. + if(power->settings.charge_supress_percent && + power->info.charge >= power->settings.charge_supress_percent) { + if(!power->charge_is_supressed) { + power->charge_is_supressed = true; + furi_hal_power_suppress_charge_enter(); + } + // disable supression if charge_supress_percent OFF but charge still supressed + } else { + if(power->charge_is_supressed) { + power->charge_is_supressed = false; + furi_hal_power_suppress_charge_exit(); + } + } +} + static void power_tick_callback(void* context) { furi_assert(context); Power* power = context; @@ -560,6 +578,8 @@ static void power_tick_callback(void* context) { power_check_charging_state(power); // Check and notify about battery level change power_check_battery_level_change(power); + // charge supress arm/disarm + power_charge_supress(power); // Update battery view port if(need_refresh) { view_port_update(power->battery_view_port); @@ -585,7 +605,7 @@ static void power_storage_callback(const void* message, void* context) { } } -// load inital settings from file for power service +// loading and initializing power service settings static void power_init_settings(Power* power) { Storage* storage = furi_record_open(RECORD_STORAGE); furi_pubsub_subscribe(storage_get_pubsub(storage), power_storage_callback, power); @@ -598,6 +618,7 @@ static void power_init_settings(Power* power) { power_settings_load(&power->settings); power_settings_apply(power); furi_record_close(RECORD_STORAGE); + power->charge_is_supressed = false; } static Power* power_alloc(void) { @@ -661,7 +682,7 @@ int32_t power_srv(void* p) { Power* power = power_alloc(); - // load inital settings for power service + // power service settings initialization power_init_settings(power); power_update_info(power); diff --git a/applications/services/power/power_service/power_i.h b/applications/services/power/power_service/power_i.h index 40b4a1ef4..09f8e7816 100644 --- a/applications/services/power/power_service/power_i.h +++ b/applications/services/power/power_service/power_i.h @@ -41,6 +41,7 @@ struct Power { bool app_running; FuriPubSub* input_events_pubsub; FuriPubSubSubscription* input_events_subscription; + bool charge_is_supressed; }; typedef enum { diff --git a/applications/services/power/power_service/power_settings.h b/applications/services/power/power_service/power_settings.h index c07199cf8..63f24e097 100644 --- a/applications/services/power/power_service/power_settings.h +++ b/applications/services/power/power_service/power_settings.h @@ -6,7 +6,6 @@ typedef struct { uint32_t auto_poweroff_delay_ms; uint8_t charge_supress_percent; - bool charge_is_supressed; } PowerSettings; #ifdef __cplusplus diff --git a/applications/settings/power_settings_app/scenes/power_settings_scene_start.c b/applications/settings/power_settings_app/scenes/power_settings_scene_start.c index 0d23d3915..2b8bd773f 100644 --- a/applications/settings/power_settings_app/scenes/power_settings_scene_start.c +++ b/applications/settings/power_settings_app/scenes/power_settings_scene_start.c @@ -15,10 +15,11 @@ const char* const auto_poweroff_delay_text[AUTO_POWEROFF_DELAY_COUNT] = const uint32_t auto_poweroff_delay_value[AUTO_POWEROFF_DELAY_COUNT] = {0, 300000, 600000, 900000, 1800000, 2700000, 3600000, 5400000}; -#define CHARGE_SUPRESS_PERCENT_COUNT 2 -const char* const charge_supress_percent_text[CHARGE_SUPRESS_PERCENT_COUNT] = {"OFF", "80%"}; +#define CHARGE_SUPRESS_PERCENT_COUNT 6 +const char* const charge_supress_percent_text[CHARGE_SUPRESS_PERCENT_COUNT] = + {"OFF", "90%", "85%", "80%", "75%", "70%"}; -const uint32_t charge_supress_percent_value[CHARGE_SUPRESS_PERCENT_COUNT] = {0, 80}; +const uint32_t charge_supress_percent_value[CHARGE_SUPRESS_PERCENT_COUNT] = {0, 90, 85, 80, 75, 70}; // change variable_item_list visible text and charge_supress_percent_settings when user change item in variable_item_list static void power_settings_scene_start_charge_supress_percent_changed(VariableItem* item) { @@ -72,7 +73,7 @@ void power_settings_scene_start_on_enter(void* context) { item = variable_item_list_add( variable_item_list, - "Safe charging", + "Safe Charging", CHARGE_SUPRESS_PERCENT_COUNT, power_settings_scene_start_charge_supress_percent_changed, app); From 3029f0d6d6bf69f61640e4fee70c7a38de8cf1b5 Mon Sep 17 00:00:00 2001 From: Dmitry422 Date: Tue, 4 Feb 2025 00:53:28 +0700 Subject: [PATCH 014/268] Litle bit mistakes cleanup. --- applications/services/power/power_service/power_settings.c | 1 - 1 file changed, 1 deletion(-) diff --git a/applications/services/power/power_service/power_settings.c b/applications/services/power/power_service/power_settings.c index 93657823d..d12754784 100644 --- a/applications/services/power/power_service/power_settings.c +++ b/applications/services/power/power_service/power_settings.c @@ -47,7 +47,6 @@ void power_settings_load(PowerSettings* settings) { // new settings initialization if(success) { settings->charge_supress_percent = 0; - settings->charge_is_supressed = false; } free(settings_previous); From 92b58bc99f2ddd2036547310e4d81b3c7eb78129 Mon Sep 17 00:00:00 2001 From: Dmitry422 Date: Wed, 5 Feb 2025 16:31:26 +0700 Subject: [PATCH 015/268] Litle bit cahnges in RGB led driver for compatibility with VibroTouch. Shell files for buil firmware. --- .ci_files/rgb.patch | 4 +++- extra.sh | 9 +++++++++ rgb.sh | 5 +++++ 3 files changed, 17 insertions(+), 1 deletion(-) create mode 100755 extra.sh create mode 100755 rgb.sh diff --git a/.ci_files/rgb.patch b/.ci_files/rgb.patch index 51a305aec..81783325f 100644 --- a/.ci_files/rgb.patch +++ b/.ci_files/rgb.patch @@ -466,7 +466,7 @@ new file mode 100644 index 0000000..b89f82a --- /dev/null +++ b/lib/drivers/SK6805.c -@@ -0,0 +1,101 @@ +@@ -0,0 +1,103 @@ +/* + SK6805 FlipperZero driver + Copyright (C) 2022-2023 Victor Nikitchuk (https://github.com/quen0n) @@ -527,6 +527,7 @@ index 0000000..b89f82a +void SK6805_update(void) { + SK6805_init(); + FURI_CRITICAL_ENTER(); ++ furi_delay_us(150); + uint32_t end; + /* Последовательная отправка цветов светодиодов */ + for(uint8_t lednumber = 0; lednumber < SK6805_LED_COUNT; lednumber++) { @@ -566,6 +567,7 @@ index 0000000..b89f82a + } + } + } ++ furi_delay_us(150); + FURI_CRITICAL_EXIT(); +} diff --git a/lib/drivers/SK6805.h b/lib/drivers/SK6805.h diff --git a/extra.sh b/extra.sh new file mode 100755 index 000000000..923c2bc47 --- /dev/null +++ b/extra.sh @@ -0,0 +1,9 @@ +wget https://github.com/xMasterX/all-the-plugins/releases/latest/download/all-the-apps-extra.tgz +tar zxf all-the-apps-extra.tgz +mkdir -p applications/main/clock_app/resources/apps +cp -R extra_pack_build/artifacts-extra/* applications/main/clock_app/resources/apps/ +rm -rf extra_pack_build +rm -f build/f7-firmware-C/toolbox/version.* +./fbt COMPACT=1 DEBUG=0 updater_package +mkdir artifacts-extra-apps +mv dist/f7-C/* artifacts-extra-apps/ diff --git a/rgb.sh b/rgb.sh new file mode 100755 index 000000000..31f9144a4 --- /dev/null +++ b/rgb.sh @@ -0,0 +1,5 @@ +git apply .ci_files/rgb.patch +rm -f build/f7-firmware-C/toolbox/version.* +./fbt COMPACT=1 DEBUG=0 updater_package +mkdir artifacts-rgb-patch +mv dist/f7-C/* artifacts-rgb-patch/ From 5bd35f435be38b73b3713ae10ccde08a8fc9081d Mon Sep 17 00:00:00 2001 From: Astra <93453568+Astrrra@users.noreply.github.com> Date: Mon, 10 Feb 2025 18:35:39 +0900 Subject: [PATCH 016/268] [FL-3951] Add the Showtime animation (#4100) * Add the Showtime animation * Format images --- .../external/L1_Showtime_128x64/frame_0.png | Bin 0 -> 542 bytes .../external/L1_Showtime_128x64/frame_1.png | Bin 0 -> 696 bytes .../external/L1_Showtime_128x64/frame_10.png | Bin 0 -> 779 bytes .../external/L1_Showtime_128x64/frame_11.png | Bin 0 -> 753 bytes .../external/L1_Showtime_128x64/frame_12.png | Bin 0 -> 749 bytes .../external/L1_Showtime_128x64/frame_13.png | Bin 0 -> 646 bytes .../external/L1_Showtime_128x64/frame_14.png | Bin 0 -> 708 bytes .../external/L1_Showtime_128x64/frame_15.png | Bin 0 -> 707 bytes .../external/L1_Showtime_128x64/frame_16.png | Bin 0 -> 515 bytes .../external/L1_Showtime_128x64/frame_17.png | Bin 0 -> 525 bytes .../external/L1_Showtime_128x64/frame_18.png | Bin 0 -> 581 bytes .../external/L1_Showtime_128x64/frame_19.png | Bin 0 -> 553 bytes .../external/L1_Showtime_128x64/frame_2.png | Bin 0 -> 744 bytes .../external/L1_Showtime_128x64/frame_20.png | Bin 0 -> 626 bytes .../external/L1_Showtime_128x64/frame_21.png | Bin 0 -> 676 bytes .../external/L1_Showtime_128x64/frame_22.png | Bin 0 -> 676 bytes .../external/L1_Showtime_128x64/frame_23.png | Bin 0 -> 597 bytes .../external/L1_Showtime_128x64/frame_24.png | Bin 0 -> 550 bytes .../external/L1_Showtime_128x64/frame_25.png | Bin 0 -> 525 bytes .../external/L1_Showtime_128x64/frame_26.png | Bin 0 -> 639 bytes .../external/L1_Showtime_128x64/frame_27.png | Bin 0 -> 606 bytes .../external/L1_Showtime_128x64/frame_28.png | Bin 0 -> 716 bytes .../external/L1_Showtime_128x64/frame_29.png | Bin 0 -> 614 bytes .../external/L1_Showtime_128x64/frame_3.png | Bin 0 -> 696 bytes .../external/L1_Showtime_128x64/frame_30.png | Bin 0 -> 750 bytes .../external/L1_Showtime_128x64/frame_31.png | Bin 0 -> 782 bytes .../external/L1_Showtime_128x64/frame_32.png | Bin 0 -> 474 bytes .../external/L1_Showtime_128x64/frame_33.png | Bin 0 -> 523 bytes .../external/L1_Showtime_128x64/frame_34.png | Bin 0 -> 577 bytes .../external/L1_Showtime_128x64/frame_35.png | Bin 0 -> 579 bytes .../external/L1_Showtime_128x64/frame_36.png | Bin 0 -> 321 bytes .../external/L1_Showtime_128x64/frame_37.png | Bin 0 -> 570 bytes .../external/L1_Showtime_128x64/frame_38.png | Bin 0 -> 775 bytes .../external/L1_Showtime_128x64/frame_39.png | Bin 0 -> 795 bytes .../external/L1_Showtime_128x64/frame_4.png | Bin 0 -> 713 bytes .../external/L1_Showtime_128x64/frame_40.png | Bin 0 -> 703 bytes .../external/L1_Showtime_128x64/frame_41.png | Bin 0 -> 681 bytes .../external/L1_Showtime_128x64/frame_42.png | Bin 0 -> 665 bytes .../external/L1_Showtime_128x64/frame_43.png | Bin 0 -> 794 bytes .../external/L1_Showtime_128x64/frame_44.png | Bin 0 -> 888 bytes .../external/L1_Showtime_128x64/frame_45.png | Bin 0 -> 914 bytes .../external/L1_Showtime_128x64/frame_46.png | Bin 0 -> 913 bytes .../external/L1_Showtime_128x64/frame_47.png | Bin 0 -> 929 bytes .../external/L1_Showtime_128x64/frame_48.png | Bin 0 -> 925 bytes .../external/L1_Showtime_128x64/frame_49.png | Bin 0 -> 925 bytes .../external/L1_Showtime_128x64/frame_5.png | Bin 0 -> 735 bytes .../external/L1_Showtime_128x64/frame_6.png | Bin 0 -> 762 bytes .../external/L1_Showtime_128x64/frame_7.png | Bin 0 -> 764 bytes .../external/L1_Showtime_128x64/frame_8.png | Bin 0 -> 741 bytes .../external/L1_Showtime_128x64/frame_9.png | Bin 0 -> 721 bytes .../external/L1_Showtime_128x64/meta.txt | 23 ++++++++++++++++++ assets/dolphin/external/manifest.txt | 11 +++++++-- 52 files changed, 32 insertions(+), 2 deletions(-) create mode 100755 assets/dolphin/external/L1_Showtime_128x64/frame_0.png create mode 100755 assets/dolphin/external/L1_Showtime_128x64/frame_1.png create mode 100755 assets/dolphin/external/L1_Showtime_128x64/frame_10.png create mode 100755 assets/dolphin/external/L1_Showtime_128x64/frame_11.png create mode 100755 assets/dolphin/external/L1_Showtime_128x64/frame_12.png create mode 100755 assets/dolphin/external/L1_Showtime_128x64/frame_13.png create mode 100755 assets/dolphin/external/L1_Showtime_128x64/frame_14.png create mode 100755 assets/dolphin/external/L1_Showtime_128x64/frame_15.png create mode 100755 assets/dolphin/external/L1_Showtime_128x64/frame_16.png create mode 100755 assets/dolphin/external/L1_Showtime_128x64/frame_17.png create mode 100755 assets/dolphin/external/L1_Showtime_128x64/frame_18.png create mode 100755 assets/dolphin/external/L1_Showtime_128x64/frame_19.png create mode 100755 assets/dolphin/external/L1_Showtime_128x64/frame_2.png create mode 100755 assets/dolphin/external/L1_Showtime_128x64/frame_20.png create mode 100755 assets/dolphin/external/L1_Showtime_128x64/frame_21.png create mode 100755 assets/dolphin/external/L1_Showtime_128x64/frame_22.png create mode 100755 assets/dolphin/external/L1_Showtime_128x64/frame_23.png create mode 100755 assets/dolphin/external/L1_Showtime_128x64/frame_24.png create mode 100755 assets/dolphin/external/L1_Showtime_128x64/frame_25.png create mode 100755 assets/dolphin/external/L1_Showtime_128x64/frame_26.png create mode 100755 assets/dolphin/external/L1_Showtime_128x64/frame_27.png create mode 100755 assets/dolphin/external/L1_Showtime_128x64/frame_28.png create mode 100755 assets/dolphin/external/L1_Showtime_128x64/frame_29.png create mode 100755 assets/dolphin/external/L1_Showtime_128x64/frame_3.png create mode 100755 assets/dolphin/external/L1_Showtime_128x64/frame_30.png create mode 100755 assets/dolphin/external/L1_Showtime_128x64/frame_31.png create mode 100755 assets/dolphin/external/L1_Showtime_128x64/frame_32.png create mode 100755 assets/dolphin/external/L1_Showtime_128x64/frame_33.png create mode 100755 assets/dolphin/external/L1_Showtime_128x64/frame_34.png create mode 100755 assets/dolphin/external/L1_Showtime_128x64/frame_35.png create mode 100755 assets/dolphin/external/L1_Showtime_128x64/frame_36.png create mode 100755 assets/dolphin/external/L1_Showtime_128x64/frame_37.png create mode 100755 assets/dolphin/external/L1_Showtime_128x64/frame_38.png create mode 100755 assets/dolphin/external/L1_Showtime_128x64/frame_39.png create mode 100755 assets/dolphin/external/L1_Showtime_128x64/frame_4.png create mode 100755 assets/dolphin/external/L1_Showtime_128x64/frame_40.png create mode 100755 assets/dolphin/external/L1_Showtime_128x64/frame_41.png create mode 100755 assets/dolphin/external/L1_Showtime_128x64/frame_42.png create mode 100755 assets/dolphin/external/L1_Showtime_128x64/frame_43.png create mode 100755 assets/dolphin/external/L1_Showtime_128x64/frame_44.png create mode 100755 assets/dolphin/external/L1_Showtime_128x64/frame_45.png create mode 100755 assets/dolphin/external/L1_Showtime_128x64/frame_46.png create mode 100755 assets/dolphin/external/L1_Showtime_128x64/frame_47.png create mode 100755 assets/dolphin/external/L1_Showtime_128x64/frame_48.png create mode 100755 assets/dolphin/external/L1_Showtime_128x64/frame_49.png create mode 100755 assets/dolphin/external/L1_Showtime_128x64/frame_5.png create mode 100755 assets/dolphin/external/L1_Showtime_128x64/frame_6.png create mode 100755 assets/dolphin/external/L1_Showtime_128x64/frame_7.png create mode 100755 assets/dolphin/external/L1_Showtime_128x64/frame_8.png create mode 100755 assets/dolphin/external/L1_Showtime_128x64/frame_9.png create mode 100755 assets/dolphin/external/L1_Showtime_128x64/meta.txt diff --git a/assets/dolphin/external/L1_Showtime_128x64/frame_0.png b/assets/dolphin/external/L1_Showtime_128x64/frame_0.png new file mode 100755 index 0000000000000000000000000000000000000000..7eed9a024d332e5e7ec021b2b3cd6f476975fb16 GIT binary patch literal 542 zcmV+(0^$9MP)oo$o%Efhiw;W8T4m9CRZ$kDScqeqUj5j+{f6lNhaLuWAR=@)&3hVyb+T)K2A(FeNJCEZPz zs^H+jaVqFxqMh1F$OS!&k+|Pm{dKx5hSG<2 zinLkR!1?Co0WbHx8;L8 z)G^@P-_R$Pie1}<^@;zQ?d+~Z&5i84)d=}k_xH7EXmd&Xexu~j=a~|<6kWr()WRF( gvin-}hxI?<3w|ZWq$Zfwg#Z8m07*qoM6N<$g0Rj9MgRZ+ literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_Showtime_128x64/frame_1.png b/assets/dolphin/external/L1_Showtime_128x64/frame_1.png new file mode 100755 index 0000000000000000000000000000000000000000..827730087e4827b0164437625a8395f7a8a310f4 GIT binary patch literal 696 zcmV;p0!RIcP)`=p0c0>DKMXbmtF04TWF8lX`Gzz5c?0T?m> zCgXlHP;m+Xba2Cf3jIz=U_c6cIUEU`fEF5yy1c1vQAksH*w`jIpak zBbYix{cgsqR&$I0W9gah>3v3}MoUoSTzx+?O7t%xP33KMCo`jz-L!^2eNpe9&WxQD z4_(64uj=E|iIG9Y(ZThu-dAQ!MoO6py6Tk~6&jT$UV+zkpwd;=Ccu$bwqWum@XIMi zpSQp$DH%8nUg}S>rG;+DjI?k&E*SArZPFt2(niHH%`IH?0pL1V{4}o&o9%;u1wU<8 z@ag{mpEeOVdkN^$5aDtKyB-q#!_Xxs6~;!AU>&?9Wy^&H;8!w0wq%*@3;hEyTbjzf z6@XU2cNUDow$=vt(gF<0*KLQ(JNsBu0yN69yT)2Vd&r-%7(f8LnRxow0*t9U3vRt< z5i&~v7D2Z^v7pMongIKC{pc+V7`-rEhRt0)|9+dYU|!-LelR{OTTlQFUi~cJnijmA ecJXbzll}v1k#w&P1Wh6U0000FlED}_kVEmN0WXRBp)`bQf^pyVd<}4P0E`ov)1=DnDJmsYwl0p+g z$Ph{g4INt5q#iRA`V`!dAx3IK{y<-l;uK8LAk|51**v||L2_dEx1Epg9`3#87(V>v zuLg4fM5m8bEHk9uqZcW+#F)`Xf4avW^o^l|){FzGUE|59$Q01@0ocq3ZLvk-q+9@o z%OFFwy#~mEA7P&{IsHH5lWXkkGcJt*&1C={=HE=pp$w3k1>jg2Po@gQ=bOt^1RmzE z#%*!wT}Ykk8(SaPd){+sF3kbtv2{KQTes)4k=`|`sv+_*?0nz6;Q^6}e_D|5V7G@L zKP6=>1e#0BFJLp%HFuc+_e8dldlxu-gimi&d;{952fW`aO^_9bGy$)&B8`7D?8$=FgJEM2xGZba(0L7Vb}|GX z)!x?CS7e-hS&#<6_;F*5F~)LP0Qju^sz(nntE!tdU`5bD@Y2B0+ZwVUL>oZz^*!ju za7+C%Bq!&0ZZkO32bL9_$scl^#otv`RkwpPWG`eV&Q6@`ftNufuIv8nuk9X@!*q^1 z>%Wcn1bt9HPdh$R4Ho<0pK6y2aZDuKDs75CHXhc=Js@jbl z=@{b;BmqMSOp%IyoxNFC)ksxCeJZ+mt6pIPj0Z7yRQR(p)lE?w_RT+*Fz;vEW5 zg8SYPLt(b?AB&`tt>{|CVCMZJTd>W%;a002ov JPDHLkV1mV`Z4>|i literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_Showtime_128x64/frame_11.png b/assets/dolphin/external/L1_Showtime_128x64/frame_11.png new file mode 100755 index 0000000000000000000000000000000000000000..0535101be90b52ecc2b3a0ec5b02c5dfe0ff8c95 GIT binary patch literal 753 zcmV4`mx9M^9!}L;|fO3dVpVyB13#bJ&Td$!0q{uZP{uAD`Rz@OvJ_W^Z}VR@v2AU2oU-JtmK18>=7I?E&!cH zkdE2S1jw$tu!Yz^@DXw1H|kr6UweUY5`ek3Ao8dhVNQ`r)AiqGnLy$kx z(i;Qe#N;xp^f%13L^e2?Q)!?Rn3mfy5Z(`fJjO@-7|6IbP@E-)cs~K*WVq8`$pf)4 z0`-i$4LXa5!IESi_^YxCgARbm@4z<9vg-+ykktYX|C=maOP-LmWqYAXfG5D3yt)c3 zq2SRUW67TL0Qwrv#nABN>|W~4L3#j}WViz0uff&EEF6=0NJ9$L1sK5HUM>5i@JN9P z6tr57NHuFrkRz+ztU|oOQWv{1+s)>((yqSdYj)Q#-Ua?d)n9bDejHobH8x+mW|3k-QI<7wvZB z``ukE)Xeze`r~RUiw?e3Rn>;7#?D@;8RJC}V7Lutgp&UJ!1brY$Z27rmEvpI-^TUVJONzP*v5kswQK0dI27Owf+HF j`(#gwV+s(Zfj1ujtfoifv3sDi00000NkvXXu0mjfVGCrJ literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_Showtime_128x64/frame_12.png b/assets/dolphin/external/L1_Showtime_128x64/frame_12.png new file mode 100755 index 0000000000000000000000000000000000000000..1284019a8a145db5eca8347029b35636d2c09125 GIT binary patch literal 749 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 FV1k{y@PGgS literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_Showtime_128x64/frame_17.png b/assets/dolphin/external/L1_Showtime_128x64/frame_17.png new file mode 100755 index 0000000000000000000000000000000000000000..4d0b7227ff8721640c6b1a2861e877c93c0c7eb0 GIT binary patch literal 525 zcmV+o0`mQdP)J!Rp^B4qlH+1BcP8D{Z*}kC`_4V*d>2k!Mkk;7-wLB+Z%6G#U@3gN2%Oir zlb=0+v-pn03g}^!TkwoArU%<*UH~SP-a8IS(+$(y)Vw@njJ+|~_45%=(j;c>lIs8r z@S&_D1(wS~S8lWRvn*_MUHg@Cw&$eTnKFlk-H!|Apc#~b09y7~N0C?t=vP@rb)fXo z)Lo^dp(LCZZoOv_9;tJSNx<;kg^E9GXc9&($8j_am4w=J92bTFTyY%4U~jVIgav^2 zgi1+ijB(!ZpgDwL324<5$by}BoSs0BA5KSKgc@met^;)y*EHjAv8ZVMH%3XpQ8!Ff7!#_hy{P><=kQ~`)GxP}k2)D(4_^?BeA{?Kwq3ja63d-i zTi-TJHWI)eZNDnawE{lvKe+eY0dbwr-Gi&-Dcg>vZ%$n_?oaKcvqXBhHSXnJD&Aw* z-10kax9MH!;Hes!7Ykkl)fx9Q9fVR%8n3E9Pwbu4kkUa8c-H`^xP1Nrxx4ZPM1)3a P00000NkvXXu0mjfqB!(u literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_Showtime_128x64/frame_18.png b/assets/dolphin/external/L1_Showtime_128x64/frame_18.png new file mode 100755 index 0000000000000000000000000000000000000000..d53d074e644a1e4812176442657fddb37fb7eb47 GIT binary patch literal 581 zcmV-L0=oT)P)`nhruzl;)**?|u%+i%A`-XZ&%_ckXw-3n!J)i7o$Me~H@T9Vdf-EirlvCw(6iJrC0Z;0d4&#W9z+jk0KTU_ZWQaqQyO%%DjTY6TUkERX*47?!)zEs5CBs(6XoF71T{({LAjJN zE*|F6sdd#<(r|e$36D8Lugr=u#sW;ep%i=?(fAlg?A;rHZa|x*ef$>)z^;14>(#A@ zJ9#eG+{3W>tdvOo^dc=gHJ>LXsV7O))y(vBE1i*=l4P&)(lK$AmSG>jJ~8nR+-$cB z3&t43jI^e#RS%WM0QM$|F~Ke9^uJ7^1IeX+sO^DUd`U}BmCg6H7{q_?9k`2u>_IS4plmQPbV5wjn3!9rEG_Mz^iMDruvcj8_!lTGHCSk3bgeA4 zq%htYO|&rHA_|AM>{#r+y~RqtYIia--^}~q)MIpd&Hqj-{byJA!b!jjKF243xy`J5 z0$7%o%1;ZxRk&LMBLe8cwX%aX#u(d$bEcR=^1^4z`AL$zd%?bnvBntV?T&u&O9_&6 z{A%#)rel=KfnU#&ulih}wsu0lL))%-97_B5OAi2Mqz^ZeTDKXg_ckiK0L;Sd&)Sw2 z=zF8aXwRb`=Z6fl8IgCW0Z{^Vn|hcpnma)mB*Phf+%xWKFw<+UEnH3|1^x9ZX%3LGRGBXOF~Ic-SbqpWIE-#Wh#7Z)Ny1wYRl$_AG!0D} z!@E2)ut4f0dpc^wOhk_z(D`#Q3NdY0PpzHaR_~qEDLuR-^ZWiJtdw2G)H!@})DQaZ zP%~-`l3aM@|H!T)F0J4_chT04HTb&qG1zvDD7SFOYdQa$5u%o rN%k0k$-FI^0PIPfdFiD#)RUyYy7Bcj>;v9u00000NkvXXu0mjf@T~=B literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_Showtime_128x64/frame_2.png b/assets/dolphin/external/L1_Showtime_128x64/frame_2.png new file mode 100755 index 0000000000000000000000000000000000000000..a52e051b9f04fcfc1940ae657851fc9f9996c096 GIT binary patch literal 744 zcmVP)e2c*0Z8Q~Nwtex2>|n0N>ZP+3?S{TB&nKR0Vb4^)SCanh#}%hDgg4D zvUXDR%%}k;tXoOiA>}&Y6i_SaCoro3Q}UE_0sqt(IRH-KeQ>uioJSV$9ee<+Dr&5E z(v1093d}M`)BcL=)71vR5iXF{d4KEa#iUdKkWYHrZ|`3oSVe%mO%-1G^#0{z$_PNY zl9Erj^<>))Anl~fRG&TBVHe2ZdafwN*Y>6vPy}o!*OCq&>;nep$+HZ&;kV8`6zp2H zH`M2vKkFlmtMpJhRJ7V(_tqQsx{nGRm){dMXd>gJ9--g$@<$zYJ=UOk6E>}nk7$+9 z7+9X%X=d;jy#X@d=AW^kh` z1Pt6gneuTO4S@5EVZxr&JX0;l4r6e2BK1y7ZDS1(oQI?aTx<*h*yU=h&v3zAZHC3> zzlL+SoKi4oG1vny_k!ji6D|xcW4s?8x>RtWJ^m68(aSbEm8!{YH5|)Cb3dDi>(xGvJfzb z2%?P^LiP&1z!euUx$I?sycRQ)giN#SyU(x>?>9q7l2mu_OLi5pdpB=woJ6OI%oBEV zk<7RtkbRa_^AyoIO@zi1DZn5JPW~lssZi{R1gYCeh>nR$V-?Cg(VMB6=p=X%rQttm zsZf6Gu?L+5FQV?1AJY3m^${ThgM{Fic(ZgfTb~6O5L_cAiFH3o%<^lTF)|4bDxCXw7B-RwRGim-3lV zQVL)uFRsI63SLCRX-P>aX&1mS1o#Qiv;hFW@FIp@4qQ6^sEtrk6-tszp917ZB3jH= zJdpw{Y~(E;WB{8R^`ul3aw6sn!ACS`dJ(e&x1#zwM}UqV^p5^8SCrV%3=ZWMpQ@Ih|#)*t<6woFfGpIdq&t^EH+CU;vlr?gAhv}rN=NA3 z3Y82f#9gx}#X_m$s)dU8hKhI6nLCe*`;m~dc{vZi=Wsq=3Q1B|un5hfiSyiln#sfb zG*K;xnYkHebHOxw+i7}=NKV)2fK|YY65z$qfVWQ)im)GnyZ0pNZlMwzR40iF-{?DD z{FQ-fq#a9 zt#3kvCzKxQKZ{a(cOV}y0YVv6h4`y9Dk0{s*R)b918f^2{yyEAPYmGTAVhG%q|_KF zNg}xY$F1Cw9Io%~M@Sxk?h7FMYUKk+jGI>O*^VH>juPu*? zRiZj4TJe08cLh`XF8<9{h`34wKPO6nMP_aVft{+GV?G=afj2$`9lxn6Oy+8UyYFE?T>0palOOZ7_c3gJXW$=TVB5Qpu$zPA zq5f-F>cRI=;Guv}22~;bELB9_#&t9fBj=Qu3(B7}LW(1xfNII&h@LiUXZQ15B z36e^sYmi@6kpW*?mXQet)$2))iIsX0)>*IqX> zvkb69z+<+Q=pPDv_pnQ+t{Eh5$F9qy`Qj^`;9vf4vVsfvYrtU9w(aA123CdyFk6t7 zeh3#~56pkP%US6qY{Di)X2;x1F-7_iAuPDg6CiRRH9I; z*(aL*=I`u(2V_A9n%q8J49nZ_0)8zE>f&NsyYJ*8e{qmi93P8-E&JZxIHsEhBUiawJ&=xrrg|)$e(o?Up@54R;Qs*+?JD6;NPL6<0000< KMNUMnLSTX!jXvH0 literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_Showtime_128x64/frame_23.png b/assets/dolphin/external/L1_Showtime_128x64/frame_23.png new file mode 100755 index 0000000000000000000000000000000000000000..6507bdc6ba9c18d34baf7bf98cbe351f002fa0fd GIT binary patch literal 597 zcmV-b0;>IqP)4f26otQgXX4HpkwGjh)E!AG6cr)3M+dSTR=W@?G_b`jF;BtW0=xei86|iDk)(MB4`B(^8P*M6N0QnUS}!;(2R40q$7*=wOe85dx(UDr zU}_}kBj99xH!uN+Mv`hGNty$kIuDFOOT7UeC(AK$3T%Ap8MUdKfv@F23+SK#@U0`t zaPYGL-4QGS=!0gD!MB01Mh_1~3~;?si}Vf%xtKJP)Ll6I#ZLhCRSdvBe`5H3s{%$L zg?p{^bdlkh*_MqHfSuhMxPn0v?X_zE{V4E0769_^_i*yCH;dV2`~)I!NTS-b*F1ZB z*{#X^4&t9(^zq(u!>WT){0@?kEP)cgA6o$WfGY|(>VhdWdj&gx;f=s~y(xeYZprpbr5)xMk9g>mo0g!M>M@xrkP$Y`1 z1Y$R_{U3-GIPyQ`?0hz6i;h`U5L5kHoox?vJg>(%XTdTsA|^mW4|qA!Q){HE796N0yO zLl&`99Jtj9BoXu5?{%xs@5v(OaHsJFC6uY52YT3Q30cIRGTwXU0E$e&rqvp@Ze93%aQsh!Bn?;k6lGsRDaU2l-qP!3r)Q?<*zPIoI$9a0W{jQ~{~QYe3p^ z1c76vJfwjZ0(;x3G5UGK{Mt*>0?Yrlx&nBUqOBT?$H{6ApnfmukFCOedqS`jSCQAIS=Wt3ya)ccs$FoOvLXC oJQ8_yLe`K|z-{NG>%+ z4hguE?DJ1CYoB#f*z)+>oo{D%sU%5##Srpn%k*uxp00~SVl@yxxUJ_Y5#;Y~mmd(> zArWmL8h~GQIIN;_AZmcBrZ^;41MxTqRW*Z%nyA%m(`q~QXsen*L_bfpiknvbNC-Yx zHA%!S2oM(M;35{c4^^Y@HzW}Ygp1+>OIUhB&-A>}9FmB=qCa{Q0P;k@rqLRbh=qS# z7a=Ra8)A2$Zvv8t1-`s5LI+92zQV_)_pwOCNV`Z4Rzj4t2C#F+$eBcx)Pdte*!V~h zJh6}@@2Jz4;~Z)LOV#%xah5hJiDbv~01&$*V%bC|1GxN=Cfx@0fStZgzqKm^U{`0W z{g@K$Me%Z~9E1(k313 zfIUc?FqWm{o%95GOxL8bGgwbnvt%aSPl&a3a6ch(+rghnB7=pB$&mg(XTb}RDgd20 P00000NkvXXu0mjf-qPtm literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_Showtime_128x64/frame_26.png b/assets/dolphin/external/L1_Showtime_128x64/frame_26.png new file mode 100755 index 0000000000000000000000000000000000000000..93f02f48eda60abc18e49f3040e8314bbf0d9d54 GIT binary patch literal 639 zcmV-_0)YLAP)1wz*A_yIoQ6b_P=QOk4o~ zM=uvrq6m3azWS%JcNja;H4g-3tp{;sVYNUeBm$p4=vwO}n_jx;nt6A(7h9YQW z!X@>*c+z9s++n zckSleUSe1F-fpQ!s41z|mV?wnNGssuSVp|jFCEz)Vw2xEJV*(fXikZ>8R2*zaFh$W zL`xgrrMSCA*;uMe$Fivf^3bH&_9!`)j`wun?KL`x1V}Lqj4CZG&Wu^4xX573)=?ukY}6LKAT?eL0dRW#UXX6- z$WAe3^t>6gyk+Rk<(2^R4VYvC3V4%%=|7=5&v@wFMoT~*z^4L*%B9Jx%o%>M6%s)& zGrZ>Ip$ZhDH!Fa_z}UaY*ZvGJ0Cgxo&RM`9kTS(j^AfC9%egVgAo zL!dweK**7b0gjPgsR*W&(fv}#ITQgUM}Y!7>$WJOq+kFVN+`=&zNYQ4=1z{a7B>`S z=X`0*wA~DUva?RTf7U21By5N-h9;o=HoQna-!SS_~No1 zBvaB%!m^DVLp#zSs)67$Zpo@HX>)8~s2;%h%^(9zgz#y@&szFHPWsw+48LEY?L`A4 zg66Qbnc+R8R$h@89vHR}>QAGuI0GNl4b_v)z_wnVn|ljV0CW~#|JF0b(nwPgkc?dK s+EI077iVMv-G75Kpxbyt79)lw52^Q`kL}WJ5?ZZr;AHhndYf$IpCz?|u3H9)V(XsJ;7D$pgmT z-_eAbf9Wo-z;z(dk4rl6pRT@=d>Kk=cjJlSrG?ciZ2$-qqyEX?-s~J1Q?!R)PN!#{ zjEp68@OUohcr zn>T4~}NiP5?2CDxk9ZD%6^?~Y6Lx)lyY?I>vw8)?WbhMj5wcAx!a$qm; z_blLkW~IpY(Ts=ce=~ERNuCpy+bxqouo+wf!+Fa^n1#Rgo&u0qKCTf`8&0!=VFAnF z8KiJt&3Dwxpuw*Vxf^llloGyj$VD9uyD@tn0y*T@GaY8d5|vqe64nWeQ1pf}w1L6$ce_D8D>>zn{8A$lO=3e*O$G~vBE3zCh1Nk(QF_o0js@{nJ1DL$f-b!)D5AJHy7&hdNgNJA z5SLu(;-EBwmM+prHx*)SxwJj+UWX)2F7Ng|-}C#u--k9SX?;^wRb5rh)E@sj5cv!nEjPX8%{+iZe@pugZm%N~5Y+$@#{m#CHZ)p6Rkg01`fxF;hflSjqP75Z;V#9;4dR_%q@}J- zW+W+k0J|^91Fye}XFPyn6M)J%ZS6%Ob@kuUf1h6^Exbq&wEzGB07*qoM6N<$g20*% AcK`qY literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_Showtime_128x64/frame_3.png b/assets/dolphin/external/L1_Showtime_128x64/frame_3.png new file mode 100755 index 0000000000000000000000000000000000000000..e7bf65287dd05555c329f6c3639a316bdcda6d97 GIT binary patch literal 696 zcmV;p0!RIcP)~f&blyO4er%G`N5~6klg2{y zLl2{r)e({ZKtH#%yGUzSXUOqzB zGtABL_`QPJO{$l%hD9Bb6AY%%Y>J%*Rg2~N?hY79pTTTX?5#5z%J|(vha%WF)BD=lv4)))*q{(U}wx*?Py>9Cok1TVSv08Df+ojUwzt6hn+O zo9JfY^@z?8783jg zo|hco`(M?Q?CrEDgD9@Tt(;PHk{7*Miz=tWA@16I3fVpEB0k&!kJ0N+RFAWm$~;_J zKxEHuElk}1$hQh(ws6Bbx(_`02Px|h%VawKd|pgpVt7Y;A)lR~-31#>7sqGv&T4Nz z^)ZV(U2y%Uh~BL%s%syV#hu1#E>vc)8O et<|2s8vg;5T0WPiw&R!p0000P_^WAeeXw0DYs-~*4 z=1DvN?J5BOCcYtA2jEtZ*!mC0{}SO^o8+@M0IH1r{Y!`1p(F4lYXDg_cLZCLgSz|= zqT)$DJFb3%-zV1cZEbrnDwp2Vm2IjW{FCthk6<_154RfG$Is3-B0qij~m(A0#F zSPCa@7yTE@Q!nTRRMZEFuPx-nMiJS!6y~~hTmBW{`mPc6B#q8};#8{6=ifuAdhZqZU96*1cpjVMQ9`P~ncG_f+d?)54STeYX0NavAWFGMC?mm#4 z=TXZ=co;+gZNONBG=HHV2o`e!sB;(K=&{2fuhdTGLDUO$;b9B!0qD6%01hB_F`&!W za-z&;a~NRe83A}6-aHcmoZJ-Et%Lbuu;4`%t|RbOFhZ%PZa6_>URju=WgIZ&z^Ep( znmwlQ{$>3|Fd!d548TY}n2EvRfZv_y9vJ6jl?IDp8rnQuqv;u-5l zp#=(+ad8Z;^ppmEaN^>(pCtr?SJ*(@?ezf2VlbEq!pURt6YlZw_=}DNQyG`$EPr}n zr{W5jkDZL;Zq(~mqcXqo$6LrvPF#z+gr$H->0Ea5G-gFDvb4<_$DfMibN0*G*_K)S zn%9~5Fb-c`$EScPCF|zWmHSeCjp9xVnCYwX^rU0 zMld#E1#qeB7R9s2z@?_>-a-8*8rD!Q5nS0L$xbn%8tCZOV07*qoM6N<$f<-1~ng9R* literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_Showtime_128x64/frame_31.png b/assets/dolphin/external/L1_Showtime_128x64/frame_31.png new file mode 100755 index 0000000000000000000000000000000000000000..274a1607434e8871acf8242c4de39128ae34f102 GIT binary patch literal 782 zcmV+p1M&QcP)8gi(V>7j=mfedMkR6Lf!dCV&b8Q zc(Nc`^v^B9L zTS&+f0M@4f_;+!UtO3B4t)lfG#{U-K!aCXcrvRoH{rgHv{HGBlWD5Yr)TR-%YD1d) zM=_a@y?=_YS>G(oMU*0-*mmw5fGs9uiIN9Dm~wraMw6kRZcl@j=#RaVDIrVLxcBZU z3Y5F>wusWf&auyf(b@q`BU2J&i9SB*@1$U&o# z=q3bf81QSDSApCUys!a)0Tw#wy~K#4{Jta(e9qf=M6x z?e6H6k)kbd@a=h&t2ue~!mOmCer9FWJPapI%>fQ~KVgmTb|vFAAj3_L5$8SKQkqFp z{he=CZ49b!pdRoGH=f77qjxQ(oC0@{p7FL9H|^mpu)xE6sytJ4Bo%uk<@NTdQ6{9gY>Q``lLm!(B9^4UZvKM&-ae;rk1Pz9_BbQw5(nfyje z(#=Pt>xwlTSTW!MMQ14k^FNw*PXlZ9CIF*%aZ}5&w3JJkV)g2O07u4)RT4f26otR{X3Q)h$^;U2jlyhUVVA~A13T%g{0qBar(!FDvSYWMt(93wW2dEskVSt$ zKqG7+?ucMxkXa<`gqfXd@n(`Y!9v5{YWO(ky!+1Z0Qw>lQI6)!{5z;+jLw%)1Mvbf zlD{bAKQqz=?2(@>lmKKEXY%p-7@%_XnB7KqccT_*cyBS)FJrVgDSBl8+ z)iV|vz74)9xLtm$JrO^UARCto4buJcaDZDXoIsMFJ#J-(C2;q}Nnm~c3vdDTarG)N zrBPW>lETt_R)VFKKypw|v#Jj#MuUc^wrR1gtxb=sAFhESg)u0i@KiAcQ*~AHz7#T~ zT~;*{0VkStdPbpEpd-g4@QtFGiGVZg25$`Tb-(E6H`53PR@5=S&hgtPJraPNkylUeb5aPK< zF&h*_Rr8O%oaF;_2!NP5x)K*7yRi$v&<6UR|ID{7^NUI6t*uax8u2y$0OZP>S0P*Z Qp8x;=07*qoM6N<$f&(erhX4Qo literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_Showtime_128x64/frame_33.png b/assets/dolphin/external/L1_Showtime_128x64/frame_33.png new file mode 100755 index 0000000000000000000000000000000000000000..3cfbe5a982a6701e4a40313d4e4c6a678e0838b5 GIT binary patch literal 523 zcmV+m0`&cfP)F$SZSI-8X@2h5W58-nTWmq1d9~03l?_4 zTi6A$4F+WvQ5#7%h%hEQ_qCX>oxzX1<-cCI8DWenoWu6)en+^)WOY56|evnW1_l6L^l@;{=I1g&l`#~YJe)` z9E{3)ET9L8Qri` zX>J1w=nUU>ZT9t-o*m|SbX~aoGE@}(uni>UMG%aq?uoCWaWGf~zYGWn*diQLBz=OS zd$x#($kNo|UKXK&PuZg>%QX$)Cr%0aAS=7&|A~mm#~JtAL7_O=27}kO3$XtVv-=%X zzQ@Chv$J!W#8mw~zZ}{N6Guf&UhlER%+-lus`Kw`Y1E>&5|aTX$#vQ{=3B{Hj03o; z^XhQpnLCe)umF+4&i&>7mB&xDpVy&t_VW*Y?g?Zex8c#fyQqlR<2Mg(nro?1r;P)4e-9L0b0zr`FOmJ>VZ3`i|*(;;@W!GPE$EM!2$ zU4Q}DR5>Z_ zNd_Om4RFt)I{>7fq)?YaRVeA*0vPiMNSA_Tk~CU!dw!&K z*@5P{ud(q`TW}i^Nm5>q%;Z`HKoNLH*|H74@V5pK_Mmegn5e6XPjVE1dsokOZ^z#i5hPvY`h47Srm za2pBO1b_nI;ss#0nwTyI>`UAL09#8IAovH^(l1uy2mTNz(b#a1DFcQ&A}$oM7cSSbuR1y)F5zyP?c0VHin8qc(-2NX5I ziOD=rWY-!KfYLeVobw=kk)+hp5z?~af|B%FsOm^k6@ecz&bt730PbQ80)UGeEsSku z!n{q|F%32JW-TV$&N;W>0;C-dBx#~8Yt2f`(YISE9|Aa!I9k zeXD(F-o|Eokjl3T$s|6U}J~coA7~_`!x*q$x$rG@PzX9li08|Z7DgLwuD2{y&6wr;HPFBJt@wB6- z=Xrg_z3Ay^IIMuY@$_lnJ|HQ1y6pMv<9RHt2e5~<+`DK>8^9j(i08S(&gi2B)Nw<=^!onN$3-|(DI`cK~&Tv?5P0sL;IbaP=k~Wg`AxY^Mtxth= TXkbF400000NkvXXu0mjfHou8K literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_Showtime_128x64/frame_37.png b/assets/dolphin/external/L1_Showtime_128x64/frame_37.png new file mode 100755 index 0000000000000000000000000000000000000000..0384fbdae685dac5f198fa3a53d059ee8f01943e GIT binary patch literal 570 zcmV-A0>%A_P)Gsn6vTh;-S+mxC3jUN2)dybQCW9ahXYez5aEuJ;{K((1Sas zLrLg>5-ev(ElKmyz7>uN<^YgBr#}V236i9$9O3}N02p*Y9(NKR;y})!t^DU?8fREj`-0R93(M<0M67u2*~`ze01jXH*1;EN&T9dd@V1w{uO zxT?fez_?q< zNXAu2vK5u&!eF|R7QobK{F&baAeDYbAebE3St`imM#H$Q!9krSmfzMmRg=4I{Zw^) zGqZ68@YS3oHaxEYHe*3R6Q_v#l5~A3NfSv8BZ20=MU7DRVbNo$A>azY&CJXONe-XQ z%-VA6O*Ow&ol4SQ^)x?}W+J?dKyi2^>61JZk-24aAjzpVBjZw%R8Cq^Rn6C`(+JEY z#m-!kX8sYN98g8_uAcz0xsU_*lQV#|DOY>n0fq@cy|&NQ-tX4{$r#|~)#JBC02p`% z(0{5o&-UZ}DZu*J*4M|;SI(_5=f5LKXA?;Z8)}~+#4KZgfA!cdAJJEN;{X5v07*qo IM6N<$f|n!x761SM literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_Showtime_128x64/frame_38.png b/assets/dolphin/external/L1_Showtime_128x64/frame_38.png new file mode 100755 index 0000000000000000000000000000000000000000..2632807f9d08b1feab9b2817721a9cfb2251b819 GIT binary patch literal 775 zcmV+i1Ni)jP)3+Gul?^S>L zPW6_mD&>rFMs2DQ8>*^eTkY#D((X0rvnhz|A}_G8b^)4nbK~3AQm&Jyy2tfWpU^1f z45PO8w|-sPur7lV=F`8fL7%%7M0VSCc)xoAnjAj#ecM_Jz>gk0dD5p+o{8)dLYZ68 z1U#{=9Y7dCA3$W+#6bHrG~o&|tJ8Kw+=p*}6)N%~fFsWBi@AsoW~az#3Kbpzo+Cg= zd=rsJBSP0juVgoNQ2s}hc#}W!5nP5J5GtO2V?GW~1Mps77(TJ%UD{&=z{QsZ_`a9} zU=?T_S!*M}*n@`p5G-OkdjRM?1n>gV$W?R$0r-V|r&+%zcoEN-q%g`|;pup`rg34s zwl1`}f0Mn!@V3Nka2U~Lj))vnZ*jfQ|9bxM?*s{J+X@!O5z|QRSv|O;s=3_{S0w=* zxC;QW3K8}rdQO`V8tAO1&&7bT`@uR5^yiv-17|c;LIYf$kI~L0f~`;C0mMtlR^)Xk~aXV z^Ddj>@c|KbGGCcg!?y__E4t)NuO>P!D zaVwTMednt;Y(bddOTkXgtjtT2x6VkE#MOG}^gte8{0~7`Ud)~mK=S|q002ovPDHLk FV1m^RbFKgY literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_Showtime_128x64/frame_39.png b/assets/dolphin/external/L1_Showtime_128x64/frame_39.png new file mode 100755 index 0000000000000000000000000000000000000000..f257489a12a2e6df4f2d12aeda9800ac9d79ac2f GIT binary patch literal 795 zcmV+$1LXXPP)P;L>}7+|MffAz2=9TF{ys^E1yaKn)Px-_rT;sKxkoqk^Yc1=B!k2_Yb+{AY+J=Y$x5 z2o3_eA`1XyPsmCZTA5f2FbXt1>hRkt0QcE|!f!ETRsh~+5wwL(al>9H&l1_ zVJ-~%I~wW%U1{m;=BgsjnA+^8L9aC!`1WA`c5d06+`1>LhOp#k#Q2kU*i?1?)QlKT Z-2c>fc#PygC%6Cr002ovPDHLkV1lRQehmNs literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_Showtime_128x64/frame_4.png b/assets/dolphin/external/L1_Showtime_128x64/frame_4.png new file mode 100755 index 0000000000000000000000000000000000000000..8a0c1734eedb7e15bc915316aa9562fcdde96109 GIT binary patch literal 713 zcmV;)0yh1LP)1wo-P_e9Xeb6nf|-6+En*oUsC5W(KOt=)5DX2^!F(n% z<5IH0NghrBEx@=8hCuMR0Aw*f;)g)ewSoKu-H3Mr2!?|-{(Ke)g?^|c-Br+%-wu|< zAAsk@1?X`AL~a7BFmtXaFoG=PaQJIda5;WS%9f^F)c{X`MfrRQ_=$o?Cd88V(*XJf zPK6M8axzV&`jH;M1?ejS_=|8Unt(3(06;US3($$%G&`1~8INW#1r@(sYbXP_k|GT( zQ5cot&kPM2Ahy)6A59^a>CQ%h9xT-}f~>0Q`~)zH#!Zc- z$I)=H5yV1dmFf*4J#wxxmZ$@pQMr@(c&xz~p!;HXs^0OZ5ZkU>KDaz08%=rJ;&@vP zOt##mhdZMb; v$B)n2i5YmhtA3M|eW10OE(IthfN%c*{1i`Qd%d5600000NkvXXu0mjfguX}c literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_Showtime_128x64/frame_40.png b/assets/dolphin/external/L1_Showtime_128x64/frame_40.png new file mode 100755 index 0000000000000000000000000000000000000000..cbcff2c8937fa4d41c17b3a70792f924d957c98f GIT binary patch literal 703 zcmV;w0zmzVP)1v7Ztf&nN`loA5J^_0t5T#wKxl63raKWtTv$vuMY^(uQs^cploAod?ka8^ zkU|*-bRptKa{XE8LauZng+hD#1G>wkb~-aA=e%9qGPsnoHx*bn3&u%_xu;27DX2xXiH0`khiq|hr3fHvT5A=nBJd4ypx7Vf(Jkn93{XIYM|!QU!kO5@;%{{!vEZr70p z-1>Z11})tisH@^g-DjND{i-6RCHPHwYeqL;0GmVaAD%8VMw-Y)d?iU5`;3ux0@XBH z01MG8VvdyeaM-tbBk9x|bC6(idijYj{+cU&DK**|PG-x-IxECieu~P+h3Yn@mEcO? zfU6bGO8NZxKn)rTqw+4$fLd8lvoIn>Qx)CBLOG(%5JAe6hyN z=FQvpfRC8($icp|NJS0WV5<&d2Nr7NU{_YjO=FTkC{i@kb8Xb0c)gtJxX;i;FC}ey zJ@-YYuMq<)h98S9&bBKFz@eK)IWW>NRHdzeLO=7Ft#QV@f}a^;ra9QdyiUJO&uqi7 lRj{$ECA~j(Qh+Bt_zya`Pt|GWY{9iXaGrzBV1Ym?n|ZlqUCcxVxl%Q>(w<`A}~!u)y$#`(;&s z-;;6>$#c2+?Ug@j>5Y;E;7vTK!fB|`UKW7-k9ipKXz4EjfExc*h0~B@?ZKXU0Y|ZM z!0JlZk3N&Q%+w7Abode%v<4T`6((OW1g~><; z^i!0#xSR;eX`-+;YA$ln>ly=$1ZmU4ub5ELajtX0#{|nRH?j1u8HCWo0KPr7J@rxiX%nPQqtrg z*li<-q&aR*EiOfvR1yf}=C~p@vb(wH?K%6+FU8!<4*8l7emw8}9FU&PGMB%v z5dbh>CnD}=^IlF`5MR98luym^^_n!f_jpQN{{_7EnBQnDZ4tccgZ^W&&bkj236 zPm)gO;mz`n&2>1uXkztY6acBD^gxl911*=>EztQUq58$iHUu+9n5##@C&j;B5dZ;9 z37=PNV{o=2Hli~S!gS=3W%_Ir{!yW+h>ySgUl9Bn)kfOz>{iV|M_a4vsu*dxgd2L% zkf*o`c9eD|bkj1h!1BNRK6UJ9q{!oTHL|J0oJQ z=b|gUNTMPm+Ov{vP&i4k72=LE^z?UUo`q+}o%3|@tgA1)`SAYo<@<@m&mDC4($J1i z_CE0X1OYl1iho_;JnJBg-RiE;^FQcZ@Ue4rn@R+PvDE?K`+|T4|5OcC$|d6^0b{=K z=hi#LAuo;O(&Y}fPsM5XLYya@5vDZhL`z;Yk)DJlhn0!~)3pL*-NtpfV!wuwh0 zl`Npj4G+^PySEWQ=USzqMDqUMZ6Gf8eD>HG%xufAy}DZ7v^lv|;>Y!C=PH-^x#;p} zNU@L|>D@#}mefv%G430yo%Iz1!9%0rDmR^+Z60Pt^2q5>o3z$kD#SOaM4lXt48C|| zadTJn!k#CddF`I)Qb-pPLldGywPG!ijjC&}uMzDWuYNF>+7_N?ZjtC+oz+5H@~@fl zhCPV(jc4`8r_)G%Evh4KjA!Mg>8fyhM{5?%-{@ynm_L^l5bw9z&1K=+sTi6c4OYt3 zl5os0c;mx`DGIe`$?9lMx1 zgrHRh?P|T35SFt8c_h7}-iw(}BZ#xFpBA+e;iomw%w{I?vH(x@bBPs#6IxCku7iE!uvSg0WxbIZsHFb;oj*2H~xB>z#M<8Cs1J$gU6OV9VqI z2+oe=U|@U}HYYddb5Mv97|?(^b literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_Showtime_128x64/frame_44.png b/assets/dolphin/external/L1_Showtime_128x64/frame_44.png new file mode 100755 index 0000000000000000000000000000000000000000..6d9362c51df3966a23f10ba58693f7626ad0bb7d GIT binary patch literal 888 zcmV-;1Bd*HP)u&;=vX{SmUJ>(f@XZ z2#U?2f{3IeB0>aF2!*JaNeM|#O&}!=NV<~}l2x&pO{$qqcjxb6y?%Ijp3|dDZQyL{ z$?XSsH;+#)kAeW{5??U`Wu`R(NWOX)0B*SmP~t&IUpdH8^bjD=1b^*&l$+uFAOcXa z*&Vn1a>;5W7=w3h|nxka~nh!Gc%{6cvdp34zjkx`&zE<4Q-8D05X z-h?OfJCpuRSP`OcWz}Vqv4#5EkVeRUsPZ9qm54~cop&20{Mwq9QhR?*h? z_-!fErNix;+No^mBjYNvQwY#t%cdQ#7fqS@sJv=GbD0b((4h^glw~65q3%zU(On z;O=79kES4#=^aR!t=SOK%pU~DS$Hvn$2{U$JPoc}Ed(ZD88vdDcI|pCvYJ%99pC-M z$T^eE+e+cXDi~i5FdwvwguR$|U09TlYWTVIdXQqr_uwJF28vqOjZe(MTA#P-OP2mMm@MRrs1Mp3 zkU8->g;FO-1b{MYPuN%M=G4UAnL`9P39uzS9Iqlk$9@ECT9O>sF>owGNMUvM)*WAB z%?R+}+8^oAgr$+f67<=KV-2R4SW5jYkHd@&7uO3tU8b$-s}-Of`3Zi^)qDPQWgej& zQr!h*Lh}VG2RKO8Bgv9kK>`#Wa8&o3mg+3xLs0`sWooZoJcwwxr}+=717ZR%KkaG& O0000Z}n6FyRCJuoX11BtRP^sD}UtR{$UG@GP{WEChg9b*n-yW)eLB zGxNryV;&Zbh5*FY%3ghg;u=X20LdFi-p8R<1#Vd$(obQd68~vn*(RYbkOQNe_cA7_NI(S zKfdx6Y~^`jQtW5Kc`13#f5TCc<9)&{?NC%^KjnnAu{$Xk=CHL%^Czh-FR*kU`mWKf zf6`w1jIF&CkBbv}9$(yI@x=tWUiFZ8;Y2)KoANMpIG|jE*@6)dr(Z2%=GzoZXDRn zl1Z~98@N$FS*_{0aoiJD??E9Vm3BBv3P{=zoCdKE-X}z6pae7+SW4?G-=-$tzn>$Iv4le zm!LQ910YQ%-qm(?m`VY_NbBvmeMNty%#HnIeCF(~;puCH0a}5)W1p^$sLFUmR`xdr z%R>zDE5@+D4>2cN=WvdLgCZtT#%q}vVG1%J%n@yUs2)QaApt_gBmM*@RTw}Bg4x@6 zC0$Fp0<2^Z#cAGjOF1_LI#NgyT1FcU1LQt8%4f_bf%X2*O5KN zMZi8fI-q_1F-8Gs1oF3SJsV~*R}LVZ?$!D36sJ)O<{D31fy#Pr~m)}07*qoM6N<$f>f%j+yDRo literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_Showtime_128x64/frame_46.png b/assets/dolphin/external/L1_Showtime_128x64/frame_46.png new file mode 100755 index 0000000000000000000000000000000000000000..4c799effbdbc1ad6bff86b20237957487332494b GIT binary patch literal 913 zcmV;C18)3@P)$G9LDkQ|CiXrF10}?#yRMV-E<(93fsuo*1lyzEA%kL4g?QjsDt4l=w<5Y-ZbhE zh4r8)6G{pbq(#t!W$h+?OX?aG+rp%l!P<9bYYQ9M+aQ% zyIuR4$bI^I>rhnyHlu8hh3pL{?Sutj1fDw#v6Yg80Gsw z5;Vi#OrZpDVT~7{qKG2^#OfP0auAQ04G5*)*m~JPWzHJ{5L>A_%`Mgwxp>J9o7X|o zg}qBp(UI!AhztX(`kc`W9tS?_m(#E(U7lnDmSNYL+%{Qfq~M>07|1;-U!v|mJQ-Sp z1V5FO=y%Q3`uhi(zFKr{>np5cw?=fEHMnQOGBzLhm8*C}yt>jK9?ZIz{?a@5q!$*} z<&(@1G1oN1-!tblb+yf&ul8rP*RfC^z^f~_ty=uJmeo$F{Af%f?ZG0o>&I`wi|X^6 zqsW|PYs3th^&VZmo7-h@9+_~Y<_R@G5l5-UI$5Xqnc|;ExVp0HL;b4N?liTC6 zSjS^@mTJb)&J{gC-cdSOnY@qEn-9j!j3l{U6duQIOfsE#f_8hm^$=o-Vfbef?ba4^%zYp+$u;@223?4B# zRaAhe-aaD=KU(V!0O6j=XM^47A6^Um1{8<}KDFWLMt;a7QN;QaHMr4$WG{Y(w?I6! z1rn(_ckxp(f@Mg7kUweYU#!Ow7ZM=Q$KVnttN;jTv|dU2hAo}23}OMUUCG%&99$3s zRFRvJDe=Gr&a7utn{KVz>K2SRkhpV)3-#_Ip+8_Q4iV#s(ysm%RfA(uz*Z5R57LA@ zi+mJF(yd#nH&EsO7fnjnyUsjA=q?aPtet?$%ptQWs5Jh&M07rQ<)274hhQK9(7*FK n1U3O6KU?^nr7uAo6gmC_0sn?dbhNl_00000NkvXXu0mjf&o#8% literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_Showtime_128x64/frame_47.png b/assets/dolphin/external/L1_Showtime_128x64/frame_47.png new file mode 100755 index 0000000000000000000000000000000000000000..017549d298e6b3a72760490b0901d6e61ca952f9 GIT binary patch literal 929 zcmV;S177@zP)tv*eFK2XvYk_EKq5&#Hk&+!(G;z=X%w9yq1tXpU>y%3+6Gc&t152 zL&a~O*7?u^$VSQkP~+@$v9tgT!S3f_-6aeJ$nFL65@c*w7X%pcfRo@8^Z`;lQ^TC!{{xw6`4&JCqW{wK#^_D=1)#s^e+ zw!;Oc-SSq7>8|zx?LRwxq(ctWZI2n7Kfm}rWSrP$heVzTQ?l;XFTg(HpN*o^A?AXf zY#Q(zR`X4l1jFpJ($t@8&bUlIJ!%m!;r{|XyRP|gFfIyQ&i}^jbG0O`{HW0|(OA9e z-rg5CLVad+PZI-aqb3}GDTkr6nPnc9*A3<02~Og`QdvxGNS*Bj<9MnCeWuN_^D+S8 zp6eeIA?KVw)vP5xiA?cxYio*pdr*g0k0t4=+h zi5*slr~Ld9+L1pE6@Ra6Qq3TX*KFqK7-{RD&^lM4!z00zviRoBHtlNdrCNiW-=dIj zO^5;AbNMg*8ITi&BvGZ9$?o%?_o<|`+MvgoGehvDc2gUn1x5> literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_Showtime_128x64/frame_48.png b/assets/dolphin/external/L1_Showtime_128x64/frame_48.png new file mode 100755 index 0000000000000000000000000000000000000000..28497ac23a33044b9ebde7d5b1300e139065752b GIT binary patch literal 925 zcmV;O17iG%P)woq;znCFStr5>jX0vYI|iD^P-CpXa^v#6tLCR_dDW0Vsu5{XWAwwsKzU$`heYi9J z?YzH4%sPdaPUv=Xa_dFotgeLGMTJDAZt01Zp5W|Uqyw=InA;{_bnp8m6}W-fAYb|V z$=->oeusU+A-qbZZUx(`%b(SH)L+rzK}qb0=BbVC`vabVCx(P6bt(Bf4pGg(I|qf( z#wnWcT(;n&&kn@^Sbl0#Zu4kG&2C3r8vy27GJL_v#j`=&AU94Q zh&5ukw%QcrYrw}&H9fvp1FO5Lix^~!Np9ja=wA=R2SDtNwj_?j8kfcv76EDMz)8Z7 z=ANJT0cro#cQ>Oy5V64kv3a|5I;7S+E@2HohB<(kt5_!=#RQNS3}H8Kg5+zK0omwr zhcK5SiYYk|%i!pUc^m;Z1428Tyh*!>Ncs^7wH&3^(D8&yVG+dWVFIso-iy#$0OBxX zEwhlgEDhOl4>;mm)^<%-J!?VzK7(8U&4tfF7z)MN& zMI#5jXhPTcQ3LUVgV;g>Loh&MQb_^$zVP{?sT%SA!p<~zhddCxt zX2-wCWRD6!Axvc`WX0?zH6;Lpu=fQh->e4#3at<})@Jqy!+cTD2Q@s$$kqaRHFbeC`(9OhZ&G z;Eui+Z-g+U!jgeKnbP&>C>(&eDXXx~;9T=LC<7v;NosyR)o~AU5EW>B_fWzqa!q7KfyMe=sLyyZJUeMPhLny%rFMyE&fws}7>0f`P zDHuCe!qA0$z%DuOW!<~AE-Y99^-v zscQ~ct5BXpPGOnZ0icp0?cG8~Ane3zzMdWMyNe}dc6IED=A!MYea5Y&K+AZPEgntH;|%=cBzn72g_uIPVOaii7fvE98yg^ME1*1eBvlvqruINGI?ST} zQW=*vF#rjG&aF2gSVqS|MihSA#-|`AMhyN3pxK?ciPk&C00000NkvXXu0mjf`x~>E literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_Showtime_128x64/frame_5.png b/assets/dolphin/external/L1_Showtime_128x64/frame_5.png new file mode 100755 index 0000000000000000000000000000000000000000..04ad4360ca043b6e1b0f03ccbf4486d2a1c3c5f9 GIT binary patch literal 735 zcmV<50wDc~P)fW?(2=ZBj53F;)+D>@%)A~pyRN?5`~E)f^ZW6BhQs$>lo$aZ zIo3O4Ge~+r^^)SM7}NFaZ+G{WFHXv0IcVOve;udq+I|yiy$5I ztpv!n-{31^XWuu(k?YjHBCd1;;WPjX`CTd5kpt300PHT}$-V;4XgJM`z(W3-V~iq}ddx@W-)!EVhL?0Vf)d=|+YB$3v4@ivjOF=F}!y(AslypZx zI63_Z)^o9$ld=s?dN>7i0&_AE0pa}s$YXrOkASpm1I2mz5$^;LPKV$5>v(dFyH`)+yL4zpIlF12H7a!@Y!VGQu2h1En5n80z3h#^710^0R@j- zge5yp1L!L_6+!ID$t<<{M|uF~WwZp~SK(qj4+mrsfOb$9pbvM;>TH2_JleqoDt@J5 zr~tT79m>wukIj+yf0cQ?Y`r?$2u5BftW?acFAJ)u zymMIARaK2uHFC22p^~gDscNFPHP#De=HR)QLKf6OY%PnPdFWI62*v};?Gk>U0Hy$! z*t_aTwi4m`1(<+;yk0|LsGNDAs`ce3XYJGqJp8fwl8imr)y@G0D5roo{{t&9O!vUn RNK*g+002ovPDHLkV1lW6Sj7MU literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_Showtime_128x64/frame_6.png b/assets/dolphin/external/L1_Showtime_128x64/frame_6.png new file mode 100755 index 0000000000000000000000000000000000000000..86bcbf48c229b985944a3e258d847f239de59c2b GIT binary patch literal 762 zcmVATB?c5)6=T#5*`|e5=CGTzCX;pMeLQS-+WI>_e4gL?^70y<|K_hY z(*U?vjxG6|qVhETNV6lxjQ##|c>K2mG4hCSjslf|SRWHP0VFX1yNy;?{8<7jApoOI zkdf>63y_Duf(OLolm8PheMavA@$op2ECR6Fc%m#vCV91j;X?;iUxuT;04uJ> zzO0`Iwo$NH@Z`ul0D1%7?nUI5H)@?2;^EkYW_!Km$xIs{-iPxt3@2p`fD&k!1CxZq zo#luUmXayuEtNR!e+5gbT?nO04M+!zSEao@!64&p=5U_IgTbTgEhn3k1z@Mm6rSpa z9^}sD*F68|J-|r))?rldb(m`{C)}SlQ+?4;Ro%UAIu|^nIPH}0= z3AxuTI)<2%_j}9b;*|HeDiD@Rx7);-EY^CNx(P2`R+X0)}QPo^kv!KQgDsF2-Rh?ecT+f-M zXFkY@YM=&kZ&UQ$D>3aIFc!RUO!#&lxCDgc_LD$rX+{`FFb}`lV}|H-xB9)R_8;G! s^~+mu<=NsURsYmjiIWP@Ed#gy14n;Zjt)Wng8%>k07*qoM6N<$f*&knYXATM literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_Showtime_128x64/frame_7.png b/assets/dolphin/external/L1_Showtime_128x64/frame_7.png new file mode 100755 index 0000000000000000000000000000000000000000..3e2a2c739f21d4a4ab3020e0509a1acee8c3c548 GIT binary patch literal 764 zcmVA$loyp|S#YG(wN^!TV6R$1=tv8#pdjL* zNFht9AThK#_9U~V)PRM?e=*n%nWjRNJ?u2L$?Q5auZP{8w*HPkp6_`--tRNK_TAl( z833}k_t$Ms;J!*Xvd|J^Mt}dw8~?3G3_oX^y@1;%)<;DS0?hz`ok}ATdkzQJ17KtU zWO(IC0_52%@D%al>>jm$_WyzA;tW6qdxO`L)~(rcrnil%YJwbt&ezSWK9CvbovM5WeTN|5 z7Gx{|nu|*hVW*gyWr=KX+>-@hFbr9iS^_k$H-NX=Zh!;{;CeQ2WzZ{w&b0z(yf$*X z{%Bg)H4)~Z<9Y)yGG8cQNwx<381BNxd;qk9(sPF)REmP4*!}WdDYt6`ugwJ8)KlxMv_v?t&*pN)=IWfd3%IC|)zC#~1GTZawH*><4eI`E8ULNZg zt0R|*oM*Q4BY)qCxW8%LJJT;_?q_Vp=2Gr3vC3Z#o7z&rCU1F5ws`q7Pma2t#dmNzO~s{vO5kJSIdky4!C1y!iQFaCZ*bvi13ud3bU uXJ_of7Th?J-=t`t7#-q}0z?Ji&;I~kFkMXW!-@a^0000A$lopG`W1sBVbAeP;#Cl9jFc#H1D{_`NjOBSJ*^%8Pupa;=<%18wj6hu4( z5e%h=vJE!J9!lAS5Cd&#tQbQ(k~IoZ=a7k}$!0qYy6KCGg4j4(@YE_B$ig1z;ybFEcdkpG^8I(bxE5sz9su; z%NQf5Zg=pJpGdnvNPhzqRaH0US!V`KI<`w={~I*gY6poBTVbmsqF*l7XVWcUGb4BN z|9stMj8M2-$hJEE^Ap>3YbUoy!_-t>I4tX`s>Z4sIXUjDBr8j*n&@4P^|F~d_FYUV2WlX;mPId2SE#-Q!*{W$kr!@dWfDjWeII=X4V;t=Vb zG$O@uF{b7HYiH}bIb!)5%WMGBG4Z)YWC!4T08Ho0Rk6CTNjm^E7eSVn784+=@4*sc zbL)S^!5h?<5Led$em?*s`7J3~p99i|0oYo^l^q4_PQRZ4fsy=m+v@7u4(X0LVrH-P z#+?Me?+8F1Yn1)L_)uqCq%VxBYJePo`Yr#V3q-~~R*)+YI|R9(l63*#_w`S~bS^Z* zQnkQNccg%3U|4zsz<*o@@)!^C0wC>JK(Ry{;+_Qjet(5God*J;3+icS88jESLqeht z!1KxswA%n8H-TlCDaREUKxPWq{52W49K9rCNmHRgfGfbPeC`1zP;kiwBxL;=0R0N5 z0|;F?m8IVJNEhIubXEYoS?CE%uuDDwkOXxAT5+1jhYBQdNrEY>c(vG218^im7MN>c zP%0BS7Nwggq2Bz_lq31px)`9HgzBiq3CjuDumqqR)&Cqz0oofyUR8Cv1Pr24(_-lf zG?@L8StueQO#CgyhJbcCUmyCFj0=lFxs&_Y!vfJw+oiV5Ozikmjx5Kib=2ZJmTWY& zZL`Ba;th;6-=%7=nw%fx=c}qrmZyGIV(Ia Date: Mon, 10 Feb 2025 10:44:44 +0000 Subject: [PATCH 017/268] ufbt: bumped action version in example github workflow for project template (#4097) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: あく --- .../project_template/app_template/.github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/ufbt/project_template/app_template/.github/workflows/build.yml b/scripts/ufbt/project_template/app_template/.github/workflows/build.yml index 143847c4a..613590d47 100644 --- a/scripts/ufbt/project_template/app_template/.github/workflows/build.yml +++ b/scripts/ufbt/project_template/app_template/.github/workflows/build.yml @@ -34,7 +34,7 @@ jobs: with: sdk-channel: ${{ matrix.sdk-channel }} - name: Upload app artifacts - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: # See ufbt action docs for other output variables name: ${{ github.event.repository.name }}-${{ steps.build-app.outputs.suffix }} From 7a92fd359c760ebd2ca07791881fc861805b3366 Mon Sep 17 00:00:00 2001 From: Anna Antonenko Date: Mon, 10 Feb 2025 15:57:16 +0400 Subject: [PATCH 018/268] [FL-3950] Update mbedtls & expose AES (#4092) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * update mbedtls * expose mbedtls/aes.h * update f18 Co-authored-by: あく --- lib/mbedtls | 2 +- lib/mbedtls.scons | 2 ++ targets/f18/api_symbols.csv | 26 +++++++++++++++++++++++++- targets/f7/api_symbols.csv | 26 +++++++++++++++++++++++++- 4 files changed, 53 insertions(+), 3 deletions(-) diff --git a/lib/mbedtls b/lib/mbedtls index edb8fec98..107ea89da 160000 --- a/lib/mbedtls +++ b/lib/mbedtls @@ -1 +1 @@ -Subproject commit edb8fec9882084344a314368ac7fd957a187519c +Subproject commit 107ea89daaefb9867ea9121002fbbdf926780e98 diff --git a/lib/mbedtls.scons b/lib/mbedtls.scons index 77add7696..759f263af 100644 --- a/lib/mbedtls.scons +++ b/lib/mbedtls.scons @@ -6,6 +6,7 @@ env.Append( "#/lib/mbedtls/include", ], SDK_HEADERS=[ + File("mbedtls/include/mbedtls/aes.h"), File("mbedtls/include/mbedtls/des.h"), File("mbedtls/include/mbedtls/sha1.h"), File("mbedtls/include/mbedtls/sha256.h"), @@ -37,6 +38,7 @@ libenv.AppendUnique( # sources = libenv.GlobRecursive("*.c*", "mbedtls/library") # Otherwise, we can just use the files we need: sources = [ + File("mbedtls/library/aes.c"), File("mbedtls/library/bignum.c"), File("mbedtls/library/bignum_core.c"), File("mbedtls/library/ecdsa.c"), diff --git a/targets/f18/api_symbols.csv b/targets/f18/api_symbols.csv index 0a4b7dde6..29ed764e8 100644 --- a/targets/f18/api_symbols.csv +++ b/targets/f18/api_symbols.csv @@ -84,6 +84,7 @@ Header,+,lib/libusb_stm32/inc/usb_hid.h,, Header,+,lib/libusb_stm32/inc/usb_std.h,, Header,+,lib/libusb_stm32/inc/usb_tmc.h,, Header,+,lib/libusb_stm32/inc/usbd_core.h,, +Header,+,lib/mbedtls/include/mbedtls/aes.h,, Header,+,lib/mbedtls/include/mbedtls/des.h,, Header,+,lib/mbedtls/include/mbedtls/ecdh.h,, Header,+,lib/mbedtls/include/mbedtls/ecdsa.h,, @@ -1891,6 +1892,21 @@ Function,+,manchester_encoder_advance,_Bool,"ManchesterEncoderState*, const _Boo Function,+,manchester_encoder_finish,ManchesterEncoderResult,ManchesterEncoderState* Function,+,manchester_encoder_reset,void,ManchesterEncoderState* Function,+,maxim_crc8,uint8_t,"const uint8_t*, const uint8_t, const uint8_t" +Function,-,mbedtls_aes_crypt_cbc,int,"mbedtls_aes_context*, int, size_t, unsigned char[16], const unsigned char*, unsigned char*" +Function,-,mbedtls_aes_crypt_cfb128,int,"mbedtls_aes_context*, int, size_t, size_t*, unsigned char[16], const unsigned char*, unsigned char*" +Function,-,mbedtls_aes_crypt_cfb8,int,"mbedtls_aes_context*, int, size_t, unsigned char[16], const unsigned char*, unsigned char*" +Function,-,mbedtls_aes_crypt_ctr,int,"mbedtls_aes_context*, size_t, size_t*, unsigned char[16], unsigned char[16], const unsigned char*, unsigned char*" +Function,-,mbedtls_aes_crypt_ecb,int,"mbedtls_aes_context*, int, const unsigned char[16], unsigned char[16]" +Function,-,mbedtls_aes_crypt_ofb,int,"mbedtls_aes_context*, size_t, size_t*, unsigned char[16], const unsigned char*, unsigned char*" +Function,-,mbedtls_aes_crypt_xts,int,"mbedtls_aes_xts_context*, int, size_t, const unsigned char[16], const unsigned char*, unsigned char*" +Function,-,mbedtls_aes_free,void,mbedtls_aes_context* +Function,-,mbedtls_aes_init,void,mbedtls_aes_context* +Function,-,mbedtls_aes_setkey_dec,int,"mbedtls_aes_context*, const unsigned char*, unsigned int" +Function,-,mbedtls_aes_setkey_enc,int,"mbedtls_aes_context*, const unsigned char*, unsigned int" +Function,-,mbedtls_aes_xts_free,void,mbedtls_aes_xts_context* +Function,-,mbedtls_aes_xts_init,void,mbedtls_aes_xts_context* +Function,-,mbedtls_aes_xts_setkey_dec,int,"mbedtls_aes_xts_context*, const unsigned char*, unsigned int" +Function,-,mbedtls_aes_xts_setkey_enc,int,"mbedtls_aes_xts_context*, const unsigned char*, unsigned int" Function,-,mbedtls_des3_crypt_cbc,int,"mbedtls_des3_context*, int, size_t, unsigned char[8], const unsigned char*, unsigned char*" Function,-,mbedtls_des3_crypt_ecb,int,"mbedtls_des3_context*, const unsigned char[8], unsigned char[8]" Function,-,mbedtls_des3_free,void,mbedtls_des3_context* @@ -1914,6 +1930,7 @@ Function,-,mbedtls_ecdh_can_do,int,mbedtls_ecp_group_id Function,-,mbedtls_ecdh_compute_shared,int,"mbedtls_ecp_group*, mbedtls_mpi*, const mbedtls_ecp_point*, const mbedtls_mpi*, int (*)(void*, unsigned char*, size_t), void*" Function,-,mbedtls_ecdh_free,void,mbedtls_ecdh_context* Function,-,mbedtls_ecdh_gen_public,int,"mbedtls_ecp_group*, mbedtls_mpi*, mbedtls_ecp_point*, int (*)(void*, unsigned char*, size_t), void*" +Function,-,mbedtls_ecdh_get_grp_id,mbedtls_ecp_group_id,mbedtls_ecdh_context* Function,-,mbedtls_ecdh_get_params,int,"mbedtls_ecdh_context*, const mbedtls_ecp_keypair*, mbedtls_ecdh_side" Function,-,mbedtls_ecdh_init,void,mbedtls_ecdh_context* Function,-,mbedtls_ecdh_make_params,int,"mbedtls_ecdh_context*, size_t*, unsigned char*, size_t, int (*)(void*, unsigned char*, size_t), void*" @@ -1954,7 +1971,9 @@ Function,-,mbedtls_ecp_group_init,void,mbedtls_ecp_group* Function,-,mbedtls_ecp_group_load,int,"mbedtls_ecp_group*, mbedtls_ecp_group_id" Function,-,mbedtls_ecp_grp_id_list,const mbedtls_ecp_group_id*, Function,-,mbedtls_ecp_is_zero,int,mbedtls_ecp_point* +Function,-,mbedtls_ecp_keypair_calc_public,int,"mbedtls_ecp_keypair*, int (*)(void*, unsigned char*, size_t), void*" Function,-,mbedtls_ecp_keypair_free,void,mbedtls_ecp_keypair* +Function,-,mbedtls_ecp_keypair_get_group_id,mbedtls_ecp_group_id,const mbedtls_ecp_keypair* Function,-,mbedtls_ecp_keypair_init,void,mbedtls_ecp_keypair* Function,-,mbedtls_ecp_mul,int,"mbedtls_ecp_group*, mbedtls_ecp_point*, const mbedtls_mpi*, const mbedtls_ecp_point*, int (*)(void*, unsigned char*, size_t), void*" Function,-,mbedtls_ecp_mul_restartable,int,"mbedtls_ecp_group*, mbedtls_ecp_point*, const mbedtls_mpi*, const mbedtls_ecp_point*, int (*)(void*, unsigned char*, size_t), void*, mbedtls_ecp_restart_ctx*" @@ -1967,6 +1986,7 @@ Function,-,mbedtls_ecp_point_read_binary,int,"const mbedtls_ecp_group*, mbedtls_ Function,-,mbedtls_ecp_point_read_string,int,"mbedtls_ecp_point*, int, const char*, const char*" Function,-,mbedtls_ecp_point_write_binary,int,"const mbedtls_ecp_group*, const mbedtls_ecp_point*, int, size_t*, unsigned char*, size_t" Function,-,mbedtls_ecp_read_key,int,"mbedtls_ecp_group_id, mbedtls_ecp_keypair*, const unsigned char*, size_t" +Function,-,mbedtls_ecp_set_public_key,int,"mbedtls_ecp_group_id, mbedtls_ecp_keypair*, const mbedtls_ecp_point*" Function,-,mbedtls_ecp_set_zero,int,mbedtls_ecp_point* Function,-,mbedtls_ecp_tls_read_group,int,"mbedtls_ecp_group*, const unsigned char**, size_t" Function,-,mbedtls_ecp_tls_read_group_id,int,"mbedtls_ecp_group_id*, const unsigned char**, size_t" @@ -1974,6 +1994,10 @@ Function,-,mbedtls_ecp_tls_read_point,int,"const mbedtls_ecp_group*, mbedtls_ecp Function,-,mbedtls_ecp_tls_write_group,int,"const mbedtls_ecp_group*, size_t*, unsigned char*, size_t" Function,-,mbedtls_ecp_tls_write_point,int,"const mbedtls_ecp_group*, const mbedtls_ecp_point*, int, size_t*, unsigned char*, size_t" Function,-,mbedtls_ecp_write_key,int,"mbedtls_ecp_keypair*, unsigned char*, size_t" +Function,-,mbedtls_ecp_write_key_ext,int,"const mbedtls_ecp_keypair*, size_t*, unsigned char*, size_t" +Function,-,mbedtls_ecp_write_public_key,int,"const mbedtls_ecp_keypair*, int, size_t*, unsigned char*, size_t" +Function,-,mbedtls_internal_aes_decrypt,int,"mbedtls_aes_context*, const unsigned char[16], unsigned char[16]" +Function,-,mbedtls_internal_aes_encrypt,int,"mbedtls_aes_context*, const unsigned char[16], unsigned char[16]" Function,-,mbedtls_internal_md5_process,int,"mbedtls_md5_context*, const unsigned char[64]" Function,-,mbedtls_internal_sha1_process,int,"mbedtls_sha1_context*, const unsigned char[64]" Function,-,mbedtls_internal_sha256_process,int,"mbedtls_sha256_context*, const unsigned char[64]" @@ -2302,9 +2326,9 @@ Function,+,pipe_install_as_stdio,void,PipeSide* Function,+,pipe_receive,size_t,"PipeSide*, void*, size_t, FuriWait" Function,+,pipe_role,PipeRole,PipeSide* Function,+,pipe_send,size_t,"PipeSide*, const void*, size_t, FuriWait" +Function,+,pipe_set_broken_callback,void,"PipeSide*, PipeSideBrokenCallback, FuriEventLoopEvent" Function,+,pipe_set_callback_context,void,"PipeSide*, void*" Function,+,pipe_set_data_arrived_callback,void,"PipeSide*, PipeSideDataArrivedCallback, FuriEventLoopEvent" -Function,+,pipe_set_broken_callback,void,"PipeSide*, PipeSideBrokenCallback, FuriEventLoopEvent" Function,+,pipe_set_space_freed_callback,void,"PipeSide*, PipeSideSpaceFreedCallback, FuriEventLoopEvent" Function,+,pipe_spaces_available,size_t,PipeSide* Function,+,pipe_state,PipeState,PipeSide* diff --git a/targets/f7/api_symbols.csv b/targets/f7/api_symbols.csv index 15f4d70d7..4a385d538 100644 --- a/targets/f7/api_symbols.csv +++ b/targets/f7/api_symbols.csv @@ -96,6 +96,7 @@ Header,+,lib/libusb_stm32/inc/usb_hid.h,, Header,+,lib/libusb_stm32/inc/usb_std.h,, Header,+,lib/libusb_stm32/inc/usb_tmc.h,, Header,+,lib/libusb_stm32/inc/usbd_core.h,, +Header,+,lib/mbedtls/include/mbedtls/aes.h,, Header,+,lib/mbedtls/include/mbedtls/des.h,, Header,+,lib/mbedtls/include/mbedtls/ecdh.h,, Header,+,lib/mbedtls/include/mbedtls/ecdsa.h,, @@ -2317,6 +2318,21 @@ Function,+,manchester_encoder_advance,_Bool,"ManchesterEncoderState*, const _Boo Function,+,manchester_encoder_finish,ManchesterEncoderResult,ManchesterEncoderState* Function,+,manchester_encoder_reset,void,ManchesterEncoderState* Function,+,maxim_crc8,uint8_t,"const uint8_t*, const uint8_t, const uint8_t" +Function,-,mbedtls_aes_crypt_cbc,int,"mbedtls_aes_context*, int, size_t, unsigned char[16], const unsigned char*, unsigned char*" +Function,-,mbedtls_aes_crypt_cfb128,int,"mbedtls_aes_context*, int, size_t, size_t*, unsigned char[16], const unsigned char*, unsigned char*" +Function,-,mbedtls_aes_crypt_cfb8,int,"mbedtls_aes_context*, int, size_t, unsigned char[16], const unsigned char*, unsigned char*" +Function,-,mbedtls_aes_crypt_ctr,int,"mbedtls_aes_context*, size_t, size_t*, unsigned char[16], unsigned char[16], const unsigned char*, unsigned char*" +Function,-,mbedtls_aes_crypt_ecb,int,"mbedtls_aes_context*, int, const unsigned char[16], unsigned char[16]" +Function,-,mbedtls_aes_crypt_ofb,int,"mbedtls_aes_context*, size_t, size_t*, unsigned char[16], const unsigned char*, unsigned char*" +Function,-,mbedtls_aes_crypt_xts,int,"mbedtls_aes_xts_context*, int, size_t, const unsigned char[16], const unsigned char*, unsigned char*" +Function,-,mbedtls_aes_free,void,mbedtls_aes_context* +Function,-,mbedtls_aes_init,void,mbedtls_aes_context* +Function,-,mbedtls_aes_setkey_dec,int,"mbedtls_aes_context*, const unsigned char*, unsigned int" +Function,-,mbedtls_aes_setkey_enc,int,"mbedtls_aes_context*, const unsigned char*, unsigned int" +Function,-,mbedtls_aes_xts_free,void,mbedtls_aes_xts_context* +Function,-,mbedtls_aes_xts_init,void,mbedtls_aes_xts_context* +Function,-,mbedtls_aes_xts_setkey_dec,int,"mbedtls_aes_xts_context*, const unsigned char*, unsigned int" +Function,-,mbedtls_aes_xts_setkey_enc,int,"mbedtls_aes_xts_context*, const unsigned char*, unsigned int" Function,-,mbedtls_des3_crypt_cbc,int,"mbedtls_des3_context*, int, size_t, unsigned char[8], const unsigned char*, unsigned char*" Function,-,mbedtls_des3_crypt_ecb,int,"mbedtls_des3_context*, const unsigned char[8], unsigned char[8]" Function,-,mbedtls_des3_free,void,mbedtls_des3_context* @@ -2340,6 +2356,7 @@ Function,-,mbedtls_ecdh_can_do,int,mbedtls_ecp_group_id Function,-,mbedtls_ecdh_compute_shared,int,"mbedtls_ecp_group*, mbedtls_mpi*, const mbedtls_ecp_point*, const mbedtls_mpi*, int (*)(void*, unsigned char*, size_t), void*" Function,-,mbedtls_ecdh_free,void,mbedtls_ecdh_context* Function,-,mbedtls_ecdh_gen_public,int,"mbedtls_ecp_group*, mbedtls_mpi*, mbedtls_ecp_point*, int (*)(void*, unsigned char*, size_t), void*" +Function,-,mbedtls_ecdh_get_grp_id,mbedtls_ecp_group_id,mbedtls_ecdh_context* Function,-,mbedtls_ecdh_get_params,int,"mbedtls_ecdh_context*, const mbedtls_ecp_keypair*, mbedtls_ecdh_side" Function,-,mbedtls_ecdh_init,void,mbedtls_ecdh_context* Function,-,mbedtls_ecdh_make_params,int,"mbedtls_ecdh_context*, size_t*, unsigned char*, size_t, int (*)(void*, unsigned char*, size_t), void*" @@ -2380,7 +2397,9 @@ Function,-,mbedtls_ecp_group_init,void,mbedtls_ecp_group* Function,-,mbedtls_ecp_group_load,int,"mbedtls_ecp_group*, mbedtls_ecp_group_id" Function,-,mbedtls_ecp_grp_id_list,const mbedtls_ecp_group_id*, Function,-,mbedtls_ecp_is_zero,int,mbedtls_ecp_point* +Function,-,mbedtls_ecp_keypair_calc_public,int,"mbedtls_ecp_keypair*, int (*)(void*, unsigned char*, size_t), void*" Function,-,mbedtls_ecp_keypair_free,void,mbedtls_ecp_keypair* +Function,-,mbedtls_ecp_keypair_get_group_id,mbedtls_ecp_group_id,const mbedtls_ecp_keypair* Function,-,mbedtls_ecp_keypair_init,void,mbedtls_ecp_keypair* Function,-,mbedtls_ecp_mul,int,"mbedtls_ecp_group*, mbedtls_ecp_point*, const mbedtls_mpi*, const mbedtls_ecp_point*, int (*)(void*, unsigned char*, size_t), void*" Function,-,mbedtls_ecp_mul_restartable,int,"mbedtls_ecp_group*, mbedtls_ecp_point*, const mbedtls_mpi*, const mbedtls_ecp_point*, int (*)(void*, unsigned char*, size_t), void*, mbedtls_ecp_restart_ctx*" @@ -2393,6 +2412,7 @@ Function,-,mbedtls_ecp_point_read_binary,int,"const mbedtls_ecp_group*, mbedtls_ Function,-,mbedtls_ecp_point_read_string,int,"mbedtls_ecp_point*, int, const char*, const char*" Function,-,mbedtls_ecp_point_write_binary,int,"const mbedtls_ecp_group*, const mbedtls_ecp_point*, int, size_t*, unsigned char*, size_t" Function,-,mbedtls_ecp_read_key,int,"mbedtls_ecp_group_id, mbedtls_ecp_keypair*, const unsigned char*, size_t" +Function,-,mbedtls_ecp_set_public_key,int,"mbedtls_ecp_group_id, mbedtls_ecp_keypair*, const mbedtls_ecp_point*" Function,-,mbedtls_ecp_set_zero,int,mbedtls_ecp_point* Function,-,mbedtls_ecp_tls_read_group,int,"mbedtls_ecp_group*, const unsigned char**, size_t" Function,-,mbedtls_ecp_tls_read_group_id,int,"mbedtls_ecp_group_id*, const unsigned char**, size_t" @@ -2400,6 +2420,10 @@ Function,-,mbedtls_ecp_tls_read_point,int,"const mbedtls_ecp_group*, mbedtls_ecp Function,-,mbedtls_ecp_tls_write_group,int,"const mbedtls_ecp_group*, size_t*, unsigned char*, size_t" Function,-,mbedtls_ecp_tls_write_point,int,"const mbedtls_ecp_group*, const mbedtls_ecp_point*, int, size_t*, unsigned char*, size_t" Function,-,mbedtls_ecp_write_key,int,"mbedtls_ecp_keypair*, unsigned char*, size_t" +Function,-,mbedtls_ecp_write_key_ext,int,"const mbedtls_ecp_keypair*, size_t*, unsigned char*, size_t" +Function,-,mbedtls_ecp_write_public_key,int,"const mbedtls_ecp_keypair*, int, size_t*, unsigned char*, size_t" +Function,-,mbedtls_internal_aes_decrypt,int,"mbedtls_aes_context*, const unsigned char[16], unsigned char[16]" +Function,-,mbedtls_internal_aes_encrypt,int,"mbedtls_aes_context*, const unsigned char[16], unsigned char[16]" Function,-,mbedtls_internal_md5_process,int,"mbedtls_md5_context*, const unsigned char[64]" Function,-,mbedtls_internal_sha1_process,int,"mbedtls_sha1_context*, const unsigned char[64]" Function,-,mbedtls_internal_sha256_process,int,"mbedtls_sha256_context*, const unsigned char[64]" @@ -2938,9 +2962,9 @@ Function,+,pipe_install_as_stdio,void,PipeSide* Function,+,pipe_receive,size_t,"PipeSide*, void*, size_t, FuriWait" Function,+,pipe_role,PipeRole,PipeSide* Function,+,pipe_send,size_t,"PipeSide*, const void*, size_t, FuriWait" +Function,+,pipe_set_broken_callback,void,"PipeSide*, PipeSideBrokenCallback, FuriEventLoopEvent" Function,+,pipe_set_callback_context,void,"PipeSide*, void*" Function,+,pipe_set_data_arrived_callback,void,"PipeSide*, PipeSideDataArrivedCallback, FuriEventLoopEvent" -Function,+,pipe_set_broken_callback,void,"PipeSide*, PipeSideBrokenCallback, FuriEventLoopEvent" Function,+,pipe_set_space_freed_callback,void,"PipeSide*, PipeSideSpaceFreedCallback, FuriEventLoopEvent" Function,+,pipe_spaces_available,size_t,PipeSide* Function,+,pipe_state,PipeState,PipeSide* From f05302813af8768cefa686d02090b307e8d28ea8 Mon Sep 17 00:00:00 2001 From: A0i Date: Tue, 11 Feb 2025 23:58:19 +1300 Subject: [PATCH 019/268] Add new AC for Fujitsu ASTG12LVCC (#4095) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add new AC for Fujitsu ASTG12LVCC * Updated timeout settings in GitHub Actions workflow * Revert CI changes Co-authored-by: Banana Blue Co-authored-by: あく --- .../infrared/resources/infrared/assets/ac.ir | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/applications/main/infrared/resources/infrared/assets/ac.ir b/applications/main/infrared/resources/infrared/assets/ac.ir index f57fe3aa1..96b4eb1c8 100644 --- a/applications/main/infrared/resources/infrared/assets/ac.ir +++ b/applications/main/infrared/resources/infrared/assets/ac.ir @@ -1896,3 +1896,42 @@ type: raw frequency: 38000 duty_cycle: 0.330000 data: 9024 4481 655 551 655 1653 654 550 656 1652 655 1652 656 550 656 550 656 550 656 550 656 551 655 1652 655 551 655 1652 655 550 656 551 655 1654 654 550 656 550 656 551 655 1652 655 550 656 551 654 550 656 1652 655 1651 657 550 655 551 655 550 656 1654 654 550 656 1653 654 551 654 551 655 1651 656 551 655 19984 655 550 656 551 655 550 656 551 655 550 655 551 655 551 655 550 656 1654 653 1653 655 1653 655 550 655 551 655 550 656 551 655 551 655 550 656 551 655 551 655 551 655 551 655 551 655 550 656 550 656 550 656 551 655 551 655 551 655 1652 656 550 656 551 655 550 656 39996 8999 4479 656 551 655 1652 656 550 656 1653 655 1653 655 550 656 551 655 550 656 551 655 551 655 1652 655 551 655 1652 655 550 656 551 655 1653 655 551 655 550 656 550 656 1652 655 551 654 551 655 551 655 1652 655 1652 656 551 655 551 655 552 654 551 655 1653 655 1653 655 551 655 549 656 1653 655 552 654 19984 655 1652 655 551 655 550 656 1652 656 551 655 551 655 551 655 1652 655 1652 655 551 656 1652 656 1653 655 1653 655 551 655 1652 655 551 655 551 655 551 654 551 654 551 655 551 655 1653 655 550 656 551 655 1652 656 1653 654 551 655 551 655 551 655 550 655 550 656 551 655 +# +# Model: Fujitsu ASTG12LVCC +# +name: Off +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 3258 1573 427 404 426 404 425 1180 428 403 427 1183 425 402 427 402 428 402 427 1180 428 1181 427 404 426 403 427 402 428 1181 427 1181 427 402 427 405 425 402 427 402 427 403 427 402 428 402 428 403 426 401 429 403 427 402 428 403 427 403 427 1180 428 401 428 404 425 401 428 402 427 402 427 402 427 402 428 1180 427 401 428 403 427 402 427 401 428 1180 427 401 428 402 427 402 428 402 427 401 428 403 427 1180 427 402 427 1180 427 1180 427 1177 429 1179 427 1179 427 1178 428 +# +name: Dh +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 39677 99167 3233 1570 425 405 425 404 425 1184 424 405 425 1182 426 405 424 404 426 404 425 1181 427 1182 426 403 427 403 426 404 426 1183 425 1183 425 403 426 404 426 406 424 404 425 405 425 402 427 405 425 404 425 403 426 404 426 404 425 402 427 405 424 1182 425 402 427 404 426 403 426 404 425 404 426 404 425 404 425 1183 424 406 423 404 426 403 426 404 425 1181 427 1182 426 1181 426 1181 426 1181 425 1182 425 1181 426 1182 426 403 426 404 425 1182 426 404 425 404 425 405 425 404 426 403 426 403 427 404 426 404 426 1182 426 1182 426 403 426 404 426 1182 426 405 424 404 426 403 426 1182 426 405 425 403 426 1182 426 404 426 1183 425 403 426 403 426 404 425 403 426 405 425 403 426 1182 425 1182 425 403 427 404 425 1181 426 403 427 403 426 404 425 406 424 404 426 404 425 404 426 404 425 404 426 404 426 404 426 404 426 404 425 404 426 404 426 403 426 403 427 404 425 402 427 405 425 403 426 404 425 404 425 404 425 405 425 404 425 404 425 404 426 403 426 402 427 403 427 403 426 1182 425 404 426 404 425 403 426 1182 425 403 426 1181 426 403 427 403 426 404 425 405 425 +# +name: Cool_hi +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 39674 99137 3228 1573 422 407 421 408 422 1185 423 408 422 1187 421 410 419 409 421 408 421 1186 422 1187 421 408 421 408 422 409 421 1187 420 1186 422 408 421 410 419 407 424 408 421 407 421 410 420 409 421 408 422 408 421 408 422 407 422 410 419 408 422 1187 420 408 421 408 422 408 421 408 421 408 421 408 420 409 421 1187 444 382 422 408 422 408 421 408 445 1162 419 1187 420 1184 423 1185 421 1184 423 1186 421 1186 421 1187 422 409 419 409 420 1186 422 407 420 409 422 409 420 407 422 407 422 411 419 406 421 409 422 1185 446 1162 420 409 421 409 421 1189 418 407 421 408 422 407 422 409 420 409 421 408 420 412 417 1187 421 407 422 408 420 410 421 408 421 409 421 409 445 384 420 410 421 407 421 407 422 409 421 1187 420 409 419 409 421 408 422 408 421 410 419 409 420 410 419 410 420 407 422 409 420 408 421 407 422 408 421 408 421 410 419 409 420 407 423 407 422 409 421 410 419 411 418 408 421 408 422 410 420 407 421 409 419 409 421 409 419 408 422 407 422 407 422 409 420 1188 419 409 421 409 420 409 419 1189 419 1186 421 1188 419 1187 420 408 421 407 422 1188 419 +# +name: Cool_lo +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 39689 99188 3229 1576 421 407 422 409 420 1188 419 409 421 1187 422 408 422 409 419 410 419 1187 422 1186 423 410 419 409 421 409 420 1187 420 1188 420 410 447 382 420 409 422 410 446 383 422 409 420 407 422 410 395 435 420 408 422 407 422 410 420 409 445 1162 420 410 420 409 420 410 420 409 421 410 419 409 421 409 419 1189 420 407 422 409 395 437 419 410 418 1186 422 1186 423 1187 420 1185 422 1188 420 1184 421 1188 419 1188 419 408 420 410 419 1186 421 408 420 410 419 410 419 409 419 411 418 409 421 410 419 409 420 1187 445 1163 419 412 417 409 420 1188 419 409 419 410 420 409 444 1164 418 1187 419 1189 419 409 419 1187 420 408 422 409 419 410 420 409 419 410 419 434 393 411 420 409 421 408 421 409 419 409 421 1188 418 410 419 410 420 410 418 412 417 409 445 385 419 409 420 410 420 408 419 409 421 410 419 411 419 408 446 382 421 409 420 409 420 410 418 409 420 409 419 410 419 412 442 384 419 411 416 412 419 409 420 410 419 410 419 410 418 410 420 409 420 409 420 434 394 1187 419 412 417 410 418 410 420 1188 419 1187 420 1188 419 410 419 1188 419 411 418 434 396 +# +name: Heat_hi +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 39692 99118 3226 1571 423 406 423 405 423 1185 421 405 424 1183 423 406 422 408 422 405 423 1184 446 1160 422 409 420 406 424 405 424 1185 422 1183 424 404 425 406 422 408 421 406 423 407 422 406 423 407 421 406 424 407 422 407 422 404 425 407 421 409 420 1185 422 406 423 408 421 404 424 405 424 407 447 383 421 406 424 1184 447 380 424 406 422 409 421 407 423 1183 447 1159 424 1185 422 1185 421 1184 422 1185 422 1185 421 1185 423 407 423 406 423 1185 423 406 424 406 423 408 446 381 424 406 423 408 421 406 424 406 423 1185 423 1184 422 407 423 407 422 1187 421 408 421 407 423 407 423 405 424 1185 422 1186 421 1184 423 407 422 407 422 1186 422 407 422 406 423 408 422 405 423 408 447 383 420 409 421 406 423 407 423 1184 423 407 423 407 422 408 421 408 423 406 424 406 422 409 422 406 423 408 421 408 421 406 422 406 424 407 422 406 423 409 421 407 422 408 423 406 423 406 423 409 446 382 447 384 420 407 423 405 424 406 423 406 423 407 423 407 422 406 423 405 422 407 424 406 422 1185 422 406 423 407 422 1183 423 1184 422 407 422 1185 423 1186 421 1184 424 407 422 1185 422 +# +name: Heat_lo +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 39670 99106 3227 1570 424 406 422 407 422 1183 424 405 424 1184 448 380 423 407 421 406 424 1183 424 1185 421 406 423 404 424 405 424 1184 423 1182 425 407 448 380 423 407 422 405 423 406 422 406 424 405 423 407 421 406 423 407 422 405 424 405 423 406 423 1184 422 408 421 408 422 405 424 406 421 407 422 406 423 405 423 1183 424 406 423 405 423 405 423 405 423 1186 421 1184 422 1184 422 1185 422 1184 447 1159 423 1184 422 1184 422 408 421 407 423 1184 421 407 448 381 422 405 423 409 421 406 422 406 422 407 422 406 423 1183 423 1185 422 406 423 405 424 1184 423 408 421 405 424 405 424 1184 422 1185 422 1184 422 407 423 408 420 409 420 1185 447 382 423 405 423 408 421 406 423 407 422 406 423 406 423 408 421 406 423 1183 424 407 422 406 424 405 424 406 423 407 423 406 423 408 422 407 422 405 424 408 421 407 422 407 422 406 423 406 423 407 422 406 423 406 422 408 421 407 422 408 421 407 422 406 423 408 422 406 423 405 423 409 422 406 422 406 423 406 423 407 422 407 423 405 424 1184 423 407 421 406 424 1184 423 1184 422 407 423 1183 423 405 424 1184 423 409 420 407 422 +# From a2b81e4e37e1e0c216aee8cace01ecafa0ffb12c Mon Sep 17 00:00:00 2001 From: jay candel Date: Tue, 11 Feb 2025 22:54:11 +0800 Subject: [PATCH 020/268] Infrared: Update TV universal (#4080) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update tv.ir * Infrared: cleanup tv.ir merge artifacts Co-authored-by: あく --- .../infrared/resources/infrared/assets/tv.ir | 1176 +++++++++++++++++ 1 file changed, 1176 insertions(+) diff --git a/applications/main/infrared/resources/infrared/assets/tv.ir b/applications/main/infrared/resources/infrared/assets/tv.ir index ae3c4d4b4..6311f500c 100644 --- a/applications/main/infrared/resources/infrared/assets/tv.ir +++ b/applications/main/infrared/resources/infrared/assets/tv.ir @@ -3657,3 +3657,1179 @@ type: parsed protocol: NECext address: AD ED 00 00 command: C5 3A 00 00 +# +# Model: AKAI ATE_22Y604W +# +name: Power +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 9292 4518 635 522 635 522 635 523 634 523 662 494 664 494 663 1625 661 519 662 1602 660 1626 660 1650 636 1650 636 1651 635 1651 635 522 634 1653 633 524 633 1655 631 526 631 527 630 1657 630 527 630 527 630 527 630 1658 630 527 630 1658 630 1658 630 527 631 1658 629 1658 629 1658 629 40773 9295 2239 630 98164 9297 2241 630 +# +name: Vol_up +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 9319 4500 657 499 658 498 660 498 659 500 658 498 660 499 688 1600 685 472 634 1654 634 1680 607 1681 607 1681 607 1681 607 1680 608 549 609 1680 608 550 608 1681 607 550 607 1682 631 1657 631 527 631 527 631 527 631 1658 631 527 631 1659 630 527 631 528 630 1659 630 1659 630 1658 631 40798 9302 2242 630 98247 9298 2243 631 98266 9301 2243 630 +# +name: Vol_dn +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 9335 4533 632 501 659 499 660 499 660 500 660 501 658 500 689 1604 685 474 635 1683 608 1659 632 1683 608 1684 607 1684 608 1684 608 551 609 1683 609 551 609 1684 608 1684 608 1685 607 1685 632 528 632 528 632 528 632 1661 631 529 631 529 631 529 631 529 631 1662 631 1662 631 1662 631 40877 9313 2247 631 98424 9314 2248 631 98437 9314 2247 632 +# +name: Mute +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 9305 4503 660 498 690 470 688 470 687 473 635 524 635 524 635 1657 634 525 663 1629 662 1630 659 1655 609 1682 609 1682 609 1682 633 525 635 1657 634 525 634 525 634 526 633 527 632 1660 631 528 632 528 631 528 631 1661 631 1661 631 1661 631 1660 631 528 631 1661 631 1661 630 1661 630 40849 9309 2243 631 98361 9313 2244 631 +# +name: Ch_next +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 9338 4507 658 500 689 470 688 471 636 525 635 525 635 524 636 1658 635 525 663 1655 637 1630 661 1655 635 1657 635 1657 636 1657 635 524 635 1658 635 1658 634 1658 634 526 634 1660 632 1661 632 528 632 528 632 528 632 528 632 528 632 1662 631 528 632 528 632 1662 631 1662 631 1662 631 40877 9317 2245 631 98426 9319 2246 631 +# +# Model: Brandt B3230HD_TV +# +name: Ch_prev +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 9344 4504 663 494 666 494 666 495 665 496 663 521 638 522 637 1634 660 522 638 1633 660 1655 638 1656 636 1656 636 1657 636 1658 635 526 633 1661 632 1662 631 1662 631 1663 631 1663 631 1662 632 530 631 530 631 530 630 530 631 530 631 530 631 530 631 530 631 1663 631 1664 630 1663 631 40893 9321 2247 631 98484 9323 2247 632 +# +# Model: BUSH TV_VL32HDLED +# +name: Power +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 9028 4480 593 1667 589 541 597 532 596 534 594 562 566 564 564 539 589 567 571 558 570 1663 594 1666 591 1696 571 1662 595 1666 591 1669 598 1662 595 562 566 563 565 539 589 567 571 1661 596 561 567 563 565 564 564 1669 598 1662 595 1691 566 1668 599 558 570 1663 594 1692 565 575 1669 +# +name: Ch_next +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 9200 4445 656 510 628 511 626 1616 626 514 623 542 597 542 597 542 597 542 597 1648 596 1648 596 543 596 1648 596 1648 596 1648 596 1648 596 1648 596 543 596 543 595 1649 595 1648 596 543 595 544 596 543 594 545 596 1648 595 1649 593 545 595 543 595 1650 595 1648 595 1649 595 1649 595 39850 9193 2218 595 95963 9218 2218 596 95989 9193 2219 595 +# +name: Ch_prev +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 9226 4417 657 508 628 511 625 1617 624 515 622 517 622 517 621 517 622 517 622 1622 621 1623 620 518 621 1622 622 1623 620 1623 621 1623 621 1623 620 518 621 518 620 518 621 1623 621 1623 619 519 620 518 620 518 621 1624 620 1623 595 1649 619 519 620 519 619 1625 619 1648 596 1625 594 39874 9194 2192 620 95968 9197 2242 571 +# +name: Vol_up +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 9205 4446 630 536 628 511 627 1616 626 539 598 542 597 542 597 542 597 542 597 1647 597 1647 597 542 597 1647 597 1648 596 1648 596 1648 596 1648 596 1648 596 1648 596 542 597 1648 596 542 597 542 597 543 597 542 597 542 597 543 597 1648 596 543 596 1648 596 1648 596 1648 596 1648 596 39850 9202 2218 596 95967 9230 2217 597 +# +# Model: ContinentalEdison +# +name: Vol_dn +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 9209 4446 655 512 628 510 628 1616 626 515 622 542 597 542 597 542 597 542 597 1648 596 1648 597 542 597 1647 597 1647 597 1647 597 1647 597 1647 597 1648 596 542 597 1648 596 542 597 1648 597 542 597 542 597 542 597 542 597 1648 596 543 596 1648 597 543 596 1648 596 1648 597 1648 596 39847 9203 2218 596 95986 9203 2218 596 95964 9230 2218 596 95965 9232 2218 596 +# +name: Vol_dn +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 9225 4476 586 1675 613 527 612 529 610 530 609 532 607 558 581 558 582 559 581 559 581 1681 581 1681 581 1681 581 1681 581 1681 581 1681 581 1681 581 1683 581 1681 581 1681 581 1681 581 559 581 559 581 558 582 559 580 559 581 559 581 559 581 559 581 1681 581 1681 581 1682 580 1682 581 +# +name: Power +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 337 1695 339 664 340 664 339 665 338 664 339 693 310 1695 339 1696 338 693 310 1697 337 666 337 693 310 693 310 1723 311 692 311 44457 309 1723 310 692 311 692 311 692 311 693 310 1723 310 694 334 669 334 1700 333 672 331 1703 330 1727 306 1704 330 697 306 1727 306 42378 309 1724 333 670 333 670 333 672 331 673 330 673 330 1704 329 1704 329 674 329 1727 306 698 305 698 305 698 305 1728 305 698 305 44436 309 1724 334 670 333 671 332 672 331 697 306 1728 306 674 329 697 306 1728 306 698 306 1728 306 1727 306 1728 305 698 305 1727 306 42378 309 1724 334 669 334 671 332 672 330 673 330 697 306 1703 330 1727 306 697 306 1727 306 697 306 698 305 697 306 1728 305 697 306 44431 309 1724 334 670 333 670 333 672 331 696 306 1727 306 697 306 697 306 1727 306 697 306 1727 306 1727 306 1727 306 697 306 1728 305 42373 309 1724 334 670 333 670 333 671 332 697 306 697 306 1727 305 1727 306 697 306 1728 305 697 306 697 306 697 306 1727 306 697 306 44427 309 1724 334 669 334 670 333 672 331 697 306 1728 306 697 306 697 306 1727 306 697 306 1727 306 1727 306 1727 306 697 306 1728 305 +# +name: Mute +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 336 1697 336 667 336 668 335 668 336 667 337 1696 338 1696 337 1696 337 667 336 1697 337 668 335 668 335 669 334 1699 334 693 309 43422 335 1723 310 694 309 694 309 695 333 671 332 672 331 673 331 673 331 1704 330 673 330 1703 331 1704 330 1703 331 673 331 1704 330 43424 309 1725 308 695 308 696 332 672 332 673 331 1703 331 1703 330 1703 331 673 331 1703 331 673 331 673 330 673 331 1703 331 673 331 43418 308 1725 308 695 333 671 332 672 331 672 331 673 331 673 330 673 330 1703 331 673 331 1703 331 1704 330 1703 331 673 330 1703 331 43418 309 1725 308 695 308 696 332 672 331 673 330 1703 331 1703 331 1703 331 673 331 1703 330 673 330 673 330 673 331 1703 331 673 331 43419 308 1725 308 695 308 696 307 697 331 673 331 672 331 672 331 673 330 1703 331 673 330 1703 331 1703 331 1703 331 673 331 1703 331 43419 309 1725 308 695 308 696 332 672 331 673 330 1703 331 1703 331 1703 331 673 330 1704 330 673 330 673 330 673 330 1703 331 673 330 43424 309 1725 309 695 333 671 332 672 331 672 331 673 331 673 330 673 331 1703 331 673 331 1703 331 1704 330 1703 331 672 332 1703 331 +# +name: Vol_up +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 310 1696 337 664 339 693 310 693 310 665 338 666 337 665 338 1696 338 665 338 1723 310 666 337 693 310 693 310 1723 310 693 310 45493 309 1724 309 693 310 693 310 692 311 692 311 1722 311 1723 310 693 310 1724 334 669 334 1700 331 1701 308 1727 331 673 330 1703 330 41342 333 1699 334 670 333 670 333 671 332 672 331 671 332 672 331 1702 331 671 332 1702 331 672 331 672 331 672 331 1702 331 672 331 45459 333 1699 334 670 333 671 332 696 306 673 331 1701 332 1702 331 672 331 1702 331 672 331 1702 331 1702 331 1701 332 672 331 1702 331 +# +# Model: Grandin +# +name: Vol_dn +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 337 1698 336 667 336 665 338 666 337 665 392 1644 337 666 338 1696 338 666 338 1723 310 667 337 667 336 693 310 1723 311 692 311 44474 310 1723 310 692 311 692 311 693 310 693 310 694 309 1725 334 670 333 1701 332 672 332 1702 331 1702 332 1702 331 672 332 1702 331 +# +name: Power +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 970 719 972 718 1822 718 944 747 920 771 921 799 893 802 893 799 893 1644 893 799 1767 770 918 86138 893 798 894 798 1768 772 915 778 913 779 913 804 888 807 888 804 888 1627 911 805 1737 801 888 86143 893 798 918 774 1766 775 913 780 912 780 912 779 913 783 912 780 912 1625 913 780 1763 777 912 86141 892 798 918 774 1766 776 912 780 911 780 912 780 912 783 912 780 912 1626 912 780 1762 801 888 86137 892 799 917 774 1766 775 913 780 912 780 912 779 913 783 912 781 911 1650 888 805 1738 801 887 86148 892 799 918 775 1765 800 888 804 888 804 888 804 888 808 888 804 888 1651 888 805 1738 801 888 86133 944 772 919 773 1767 774 914 778 914 778 914 778 914 781 915 777 915 1624 914 778 1766 774 914 +# +name: Vol_up +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 972 719 1823 716 973 719 972 720 945 747 920 772 920 802 893 1645 1743 796 918 773 918 774 916 86148 917 774 1767 772 916 777 914 778 913 779 913 779 913 783 913 1625 1763 776 913 779 913 779 913 86148 918 773 1767 773 915 778 914 779 913 779 913 780 912 783 913 1625 1764 777 912 780 912 780 912 +# +name: Vol_dn +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 946 745 947 745 1850 688 945 746 944 748 893 798 893 802 893 1644 1743 794 894 797 894 1643 918 85284 892 798 894 798 1769 769 918 775 915 778 914 778 914 782 913 1649 1739 775 913 778 914 1625 913 85274 943 771 919 773 1766 773 915 778 914 777 915 777 915 781 914 1623 1765 774 914 778 914 1623 914 +# +name: Ch_next +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 972 719 1822 715 972 717 973 719 970 721 919 772 919 1647 1743 794 894 797 894 797 918 773 918 86132 892 798 1768 770 918 774 916 778 913 803 889 804 888 1653 1739 800 889 804 888 804 888 804 888 86144 918 773 1769 771 915 777 915 777 915 778 914 778 914 1627 1765 774 914 777 915 777 915 778 914 +# +name: Ch_prev +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 975 746 946 717 1879 660 974 719 972 721 920 798 894 1648 1744 794 894 798 894 797 894 1644 918 85281 920 773 945 749 1793 769 917 775 916 777 915 777 915 1628 1765 774 915 778 914 778 914 1624 914 85282 943 772 919 773 1767 774 914 778 914 778 914 778 914 1627 1766 774 914 778 914 778 914 1624 914 85311 919 773 919 774 1766 773 915 778 914 778 914 778 914 1627 1766 774 915 778 914 778 914 1624 914 +# +# Model: Grundig AndroidTV +# +name: Mute +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 920 772 920 772 1771 768 920 771 920 772 920 771 948 748 948 743 948 1590 946 746 1795 1613 919 85291 892 799 893 799 1767 772 916 777 914 778 914 779 913 783 913 779 913 1625 913 780 1762 1623 912 +# +name: Power +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 872 800 873 797 1723 808 875 798 875 827 873 772 900 799 873 799 873 1614 873 800 1718 816 867 86386 844 828 844 828 1744 786 845 828 845 828 845 828 845 828 844 828 845 1643 844 828 1693 838 844 +# +name: Ch_next +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 896 775 845 828 1746 785 845 828 846 827 848 826 845 1641 1693 838 844 828 845 828 845 828 845 +# +name: Ch_prev +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 957 716 1725 805 878 826 847 826 846 827 875 797 847 1641 1695 834 874 799 873 800 872 1617 870 85382 847 824 1695 835 873 800 871 803 869 804 868 806 867 1645 1691 841 842 831 842 831 842 1646 842 +# +name: Vol_up +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 868 804 868 805 1717 812 870 804 896 777 869 803 870 804 869 1618 1744 790 867 805 866 807 866 86344 867 807 866 807 1716 816 868 806 867 805 867 806 868 805 869 1622 1713 841 843 804 867 806 869 +# +name: Vol_dn +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 925 747 1743 787 926 747 925 747 926 746 953 719 872 801 927 1562 1772 785 897 776 894 1594 867 85501 926 747 1771 785 897 776 894 779 867 806 867 807 866 807 866 1622 1713 818 866 807 866 1622 866 85508 926 748 1770 786 869 804 868 806 867 807 866 807 866 807 866 1623 1713 819 865 807 866 1623 866 85517 925 748 1770 786 896 777 868 806 867 807 866 807 866 807 866 1622 1714 818 866 807 866 1623 865 +# +# Model: GRUNDIG UNKNOWN +# +name: Mute +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 846 826 847 797 1723 837 846 826 847 826 847 826 846 826 846 826 846 1641 847 827 1717 1631 867 85430 845 827 845 827 1718 813 869 805 867 806 867 808 865 809 864 809 864 1624 864 832 1689 1635 863 +# +name: Power +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 926 751 1760 756 922 756 921 782 897 782 897 783 897 784 897 784 897 1624 897 784 1736 787 895 85521 921 780 1734 784 896 783 894 785 895 784 895 784 895 785 895 784 896 1625 895 785 1735 787 896 85546 894 783 1732 763 917 783 896 783 896 784 895 784 895 785 894 785 897 1624 897 784 1734 786 897 +# +name: Vol_up +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 953 724 927 750 1764 755 924 755 924 758 922 782 898 782 899 1624 1737 786 898 784 897 784 898 85552 923 756 921 757 1759 783 898 782 898 782 898 783 897 783 897 1623 1737 785 897 784 897 784 897 85557 927 750 926 753 1762 782 898 782 898 759 921 782 898 783 898 1623 1737 785 898 784 898 783 898 +# +name: Vol_dn +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 952 725 1763 752 927 753 925 756 922 780 899 781 899 782 899 1623 1737 786 897 784 897 1626 895 84746 892 785 1731 787 894 785 894 785 895 786 895 789 891 786 895 1627 1732 789 894 788 893 1628 894 84705 929 748 1766 751 930 750 929 750 929 750 929 752 928 753 927 1593 1766 756 926 756 925 1597 924 +# +name: Mute +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 896 782 895 784 1733 785 896 785 868 812 894 784 920 762 871 812 918 1605 893 787 1735 1629 892 84752 893 784 895 781 1735 762 919 783 895 786 897 783 896 785 896 786 895 1625 897 784 1738 1625 896 84727 916 784 896 781 1736 785 897 783 896 784 896 785 897 784 895 785 897 1624 897 783 1737 1627 897 +# +name: Ch_next +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 921 753 1759 785 896 782 897 782 897 783 895 784 896 1623 1733 786 895 784 895 784 893 785 895 85568 893 783 1734 786 893 785 895 783 895 785 894 785 895 1622 1733 786 894 784 896 784 895 784 895 +# +# Model: GuestTek Marriot_Hotel +# +name: Ch_prev +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 5349 102096 3672 530 1194 102545 5171 102367 969 658 1613 481 1499 103984 503 388 210 337 161 +# +name: Vol_up +type: parsed +protocol: NECext +address: 00 7F 00 00 +command: 48 B7 00 00 +# +name: Vol_dn +type: parsed +protocol: NECext +address: 00 7F 00 00 +command: 44 BB 00 00 +# +name: Ch_next +type: parsed +protocol: NECext +address: 00 7F 00 00 +command: 0A F5 00 00 +# +name: Ch_prev +type: parsed +protocol: NECext +address: 00 7F 00 00 +command: 06 F9 00 00 +# +# Model: Haier L42C1180 +# +name: Mute +type: parsed +protocol: NECext +address: 00 7F 00 00 +command: 5A A5 00 00 +# +name: Power +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 9021 4377 655 452 654 452 654 1567 654 453 653 453 653 453 652 454 651 455 651 1568 654 1569 653 455 651 1570 652 1570 652 1570 652 1571 651 1572 649 480 626 481 624 482 623 1599 623 483 623 484 622 484 622 484 622 1601 621 1601 621 1601 621 484 622 1601 621 1601 621 1601 621 1601 621 39912 8910 2137 622 95435 8933 2137 622 95434 8934 2137 622 95434 8934 2137 622 95434 8934 2137 622 95434 8933 2137 622 95434 8933 2138 621 95436 8932 2138 621 95435 8933 2138 621 95435 8933 2137 622 +# +name: Vol_up +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 8990 4407 626 479 627 479 627 1596 626 479 627 480 681 425 681 425 681 425 681 1541 680 1542 679 428 626 1596 626 1596 626 1596 626 1596 626 1596 626 480 626 1597 625 481 625 482 624 506 600 506 600 506 600 507 599 1624 622 483 623 1599 623 1599 623 1599 623 1600 622 1600 622 1600 622 39912 8909 2138 623 95460 8906 2140 623 +# +name: Vol_dn +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 8990 4406 626 481 625 481 625 1595 627 481 625 481 653 453 654 452 654 452 654 1567 654 1568 654 453 653 1568 654 1569 653 1569 653 1569 653 1570 652 1594 627 1595 626 479 626 480 625 481 624 482 624 483 623 483 623 483 623 484 622 1600 622 1600 622 1600 622 1600 622 1600 622 1600 622 39913 8911 2137 622 95438 8934 2136 623 +# +# Model: Hisense ER22601A +# +name: Mute +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 9020 4375 657 450 656 451 654 1566 656 451 655 451 653 453 626 480 653 452 655 1567 654 1568 654 454 651 1568 654 1568 654 1569 653 1570 651 1572 650 1595 626 479 626 480 625 1597 624 482 624 482 624 483 623 483 623 483 623 1599 623 1599 623 483 623 1599 623 1599 623 1599 623 1599 623 39900 8912 2136 623 +# +name: Power +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 976 723 1776 796 925 804 897 804 897 803 897 803 897 803 897 803 922 1610 919 783 1767 807 914 86103 921 778 1769 805 916 786 914 788 913 787 914 787 914 788 913 811 890 1617 914 787 1764 832 889 86082 920 778 1768 804 916 786 914 786 914 786 914 786 914 786 915 786 915 1616 915 786 1763 809 913 +# +name: Ch_next +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 950 750 980 720 1800 770 926 774 926 774 926 775 925 1632 1775 796 922 779 920 782 919 782 919 86232 897 802 923 777 1772 801 918 782 919 783 917 783 918 1613 1769 802 918 783 917 783 918 783 917 +# +name: Ch_prev +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 979 721 1776 794 926 775 925 775 925 775 925 775 925 1606 1800 796 922 779 919 781 919 1612 919 85400 924 776 1772 800 919 781 919 782 918 781 919 782 919 1612 1770 801 919 782 918 782 919 1612 919 85390 923 776 1772 799 919 781 919 781 919 781 919 781 919 1612 1770 801 919 782 918 782 918 1613 918 +# +name: Vol_up +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 979 720 977 724 1776 795 925 775 924 775 951 750 950 750 950 1582 1796 799 919 781 919 782 919 86138 949 752 946 777 1771 801 919 782 919 782 918 782 918 782 918 1612 1769 802 918 782 918 782 918 +# +# Model: Hisense K321UW +# +name: Vol_dn +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 951 749 1832 741 978 721 927 774 926 776 924 776 924 801 899 1632 1773 798 920 782 918 1613 917 85257 898 802 1773 800 919 782 918 782 919 782 919 782 918 782 918 1613 1769 802 918 783 918 1614 917 85260 898 802 1772 800 919 782 919 782 919 782 919 782 918 782 919 1613 1769 802 918 782 918 1613 918 +# +name: Power +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 8510 4237 528 1592 528 1592 528 526 529 526 529 526 529 526 529 526 529 527 528 1591 529 1591 529 1592 528 527 528 1590 530 526 529 526 529 525 528 22533 529 1590 529 1592 528 526 529 526 529 526 529 526 529 526 529 526 529 1592 528 1591 555 1564 556 500 555 1565 554 500 529 526 529 524 529 +# +name: Vol_up +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 8507 4232 528 1591 528 1590 529 527 528 527 527 526 528 527 528 526 529 525 530 526 529 1591 528 1591 528 1590 529 1591 528 528 527 527 527 525 528 22529 525 1592 527 1592 526 528 527 527 528 529 525 527 528 527 527 527 528 527 527 1592 527 1593 526 1592 527 1592 527 527 528 554 500 527 526 +# +name: Vol_dn +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 8505 4231 528 1593 526 1595 524 525 529 526 529 526 528 525 529 527 527 526 528 1590 529 1590 529 1590 529 1590 529 1591 528 527 527 527 527 525 528 21460 528 1590 529 1590 529 525 529 526 528 526 528 526 528 526 528 527 527 1592 527 1592 527 1591 528 1591 528 1591 528 526 528 526 528 525 528 +# +name: Mute +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 8506 4232 528 1591 528 1591 528 526 528 526 528 528 526 526 528 527 527 526 528 526 528 526 528 1591 528 1592 527 1591 528 525 529 526 528 526 527 23593 528 1591 528 1592 527 526 528 526 528 526 528 526 529 527 527 528 526 525 529 527 527 1591 528 1591 528 1591 528 526 528 526 528 526 527 +# +name: Ch_next +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 8505 4231 528 1591 528 1591 527 527 527 526 528 526 552 503 527 528 526 526 528 1591 528 527 527 526 529 1592 527 1592 527 525 529 527 527 526 527 23590 527 1590 528 1590 528 525 529 526 528 526 528 526 528 526 528 526 528 1590 529 526 528 526 528 1590 529 1591 528 526 528 526 528 523 530 +# +# Model: Kendo CP20M36VT +# +name: Ch_prev +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 8505 4233 526 1591 528 1591 528 526 528 526 528 527 527 527 527 526 528 528 526 528 526 526 528 525 529 1591 528 1591 528 527 527 526 528 524 529 24651 528 1595 524 1591 527 526 528 528 526 526 528 526 528 528 526 527 527 526 528 526 528 527 527 1592 527 1591 527 527 527 528 526 525 552 +# +name: Vol_dn +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 9121 4377 685 475 658 476 656 1610 656 479 654 482 651 483 651 483 651 483 651 1618 650 1618 650 506 627 1640 628 1641 627 1641 627 1640 628 1641 627 1641 627 1641 627 506 627 506 628 507 627 506 627 507 627 507 626 507 627 507 627 1641 626 1641 627 1641 627 1641 627 1641 627 1641 627 39937 9096 2169 651 +# +# Model: LG OLED48C37LA (LG_OLED C3 models) +# +name: Mute +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 9097 4402 685 475 659 475 658 1609 657 477 656 480 653 480 654 480 653 481 653 1615 653 1614 654 481 653 1615 653 1615 653 1615 653 1615 653 1615 653 1615 653 481 652 481 653 1616 652 482 652 482 652 482 652 482 652 482 652 1616 652 1616 651 482 652 1617 651 1617 651 1640 628 1640 628 39937 9097 2167 652 +# +name: Mute +type: parsed +protocol: NECext +address: 83 7A 00 00 +command: 09 00 00 00 +# +name: Vol_up +type: parsed +protocol: NECext +address: 83 7A 00 00 +command: 02 40 00 00 +# +name: Vol_dn +type: parsed +protocol: NECext +address: 83 7A 00 00 +command: 03 40 00 00 +# +name: Ch_prev +type: parsed +protocol: NECext +address: 83 7A 00 00 +command: 01 00 00 00 +# +# Model: Manta +# +name: Ch_next +type: parsed +protocol: NECext +address: 83 7A 00 00 +command: 00 00 00 00 +# +name: Power +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 8073 3997 524 502 495 505 492 1508 498 503 494 1505 501 1500 495 1504 491 1510 496 3988 522 502 495 1505 501 501 496 504 493 1507 499 502 495 1505 501 501 496 18806 8072 3997 524 502 495 505 492 1507 499 502 495 1505 490 1509 497 1504 491 1510 496 3988 522 502 495 1505 501 500 497 503 494 1506 500 501 496 1504 491 510 498 18806 8072 3998 523 503 494 506 491 1509 497 504 493 1506 499 1501 494 1506 500 1502 493 3989 522 504 493 1507 499 502 495 505 492 1508 498 503 494 1506 499 502 495 18807 8072 3998 523 503 494 506 491 1509 497 504 493 1506 500 1501 494 1506 500 1502 493 3989 521 503 494 1506 500 502 495 505 492 1508 498 503 494 1506 500 502 495 18807 8072 3998 523 502 495 505 492 1508 498 503 494 1505 501 1500 495 1504 491 1510 496 3988 523 502 495 1505 501 501 496 503 494 1506 500 501 496 1504 491 510 498 +# +name: Vol_up +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 8069 3998 522 503 494 506 491 1509 496 505 492 1507 498 1501 494 1506 499 1502 493 3989 521 1503 492 1508 497 1503 492 508 500 1501 494 506 491 510 498 504 493 17810 8067 4003 517 508 500 501 496 1504 491 510 498 1502 493 1507 498 1501 494 1508 497 3984 526 1474 521 1504 501 1500 495 505 492 1509 496 505 492 508 500 502 495 17809 8069 4000 520 506 491 509 499 1501 494 507 501 1499 496 1503 492 1508 497 1504 491 3991 519 1480 525 1500 495 1505 500 500 497 1503 492 509 499 503 494 507 490 17809 8069 3999 521 505 492 508 500 1500 495 506 491 1508 497 1502 493 1507 498 1503 492 3990 520 1504 491 1509 496 1504 491 509 499 1501 494 507 501 501 496 505 492 17808 8070 3998 523 503 494 506 491 1509 496 504 493 1507 498 1501 494 1506 499 1502 493 3988 522 1502 493 1507 498 1502 493 507 501 1500 495 505 492 509 499 502 495 +# +name: Vol_dn +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 8066 4002 519 507 501 500 497 1503 492 508 500 1500 495 1480 525 1475 520 1506 499 3983 517 508 500 1500 495 1481 524 501 496 1504 491 510 498 503 494 507 501 18803 8073 3997 524 503 494 506 491 1483 522 504 493 1506 499 1476 519 1482 523 1478 517 3989 521 504 493 1482 523 1478 517 508 500 1476 519 507 501 500 497 505 492 18809 8066 4003 517 508 500 501 496 1503 492 509 499 1501 494 1480 525 1475 520 1481 524 3983 516 509 499 1501 494 1482 523 502 495 1505 500 500 497 504 493 508 500 +# +name: Mute +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 8069 4000 520 480 517 508 500 1500 495 505 492 1508 497 1502 493 1506 499 1502 493 3989 521 1503 492 1509 496 503 494 1506 499 1501 494 506 491 510 498 504 493 17807 8072 3997 524 501 496 505 492 1508 497 502 495 1505 500 1499 496 1504 491 1510 495 3986 524 1500 495 1506 499 500 497 1503 492 1508 497 503 494 507 501 501 496 17804 8064 4004 517 509 499 501 496 1504 491 509 499 1500 495 1505 500 1499 496 1505 500 3980 520 1505 500 1500 495 505 492 1508 497 1502 493 508 500 501 496 505 492 17807 8072 3995 526 500 497 503 494 1506 499 501 496 1504 491 1508 497 1503 492 1509 496 3985 515 1509 496 1503 492 508 500 1500 495 1506 499 501 496 505 492 509 499 17803 8065 4003 518 508 500 501 496 1504 491 509 499 1502 493 1507 498 1502 493 1508 497 3985 525 1500 495 1505 500 500 497 1503 492 1508 497 504 493 508 500 501 496 +# +name: Ch_next +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 8064 4006 525 501 496 504 493 1507 498 502 495 1505 500 1499 496 1504 501 1500 495 3987 523 1501 494 1507 498 502 495 505 492 1508 497 1503 492 509 499 503 494 17808 8069 4000 520 505 492 508 500 1500 495 506 491 1508 497 1503 492 1508 497 1504 491 3991 519 1505 500 1501 494 507 501 500 497 1502 493 1508 497 503 494 508 500 17803 8064 4006 525 501 496 504 493 1507 498 503 494 1505 500 1500 495 1505 500 1500 495 3988 522 1502 493 1507 498 503 494 506 491 1508 497 1503 492 509 499 503 494 +# +# Model: NEC E425 +# +name: Ch_prev +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 8068 3999 521 504 493 507 501 1498 497 504 493 1505 500 1475 520 1479 526 1500 495 3987 523 502 495 1505 500 501 496 504 493 1506 499 1501 494 507 501 500 497 18802 8072 3996 524 502 495 505 492 1507 498 503 494 1505 500 1475 520 1480 525 1476 519 3987 523 501 496 1504 491 510 498 502 495 1504 501 1475 520 505 492 509 498 18798 8065 4001 519 507 501 499 498 1502 493 507 501 1499 496 1504 501 1473 522 1504 501 3981 518 506 491 1509 496 505 492 508 499 1500 495 1505 500 500 497 505 492 18808 8065 4001 519 507 501 500 497 1503 492 508 499 1500 495 1505 500 1499 496 1506 499 3983 516 508 499 1501 494 507 501 500 497 1502 493 1508 497 504 493 508 500 +# +name: Mute +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 8953 4403 601 417 701 516 602 515 602 411 706 515 603 515 575 1635 603 514 628 1553 628 1609 600 1583 627 1606 598 1610 601 1608 600 495 624 1607 599 516 603 515 601 517 572 547 599 1608 599 518 601 516 598 545 573 1610 573 1637 572 1636 597 1612 599 518 600 1610 598 1610 599 1611 598 39250 8972 2171 599 94711 8976 2167 602 94731 8955 2168 602 94737 8927 2198 598 +# +name: Mute +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 3554 1678 495 403 468 1248 495 403 497 374 497 375 497 374 497 374 497 375 496 375 496 375 495 377 493 378 492 380 490 1254 489 383 488 383 489 383 488 383 488 383 488 383 489 383 488 383 489 383 488 1255 488 383 488 384 488 383 488 384 487 384 487 384 487 384 488 384 488 384 487 1256 487 384 488 384 487 1256 488 1256 488 384 487 384 487 384 488 1256 487 384 488 384 487 1257 487 1257 487 385 486 1257 487 +# +# Model: Panasonic N2QAYA_152 +# +name: Ch_next +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 3526 1678 495 403 468 1248 496 403 496 375 496 375 497 375 496 375 496 376 495 376 495 376 495 377 493 378 492 380 490 1254 488 384 488 383 488 384 488 384 487 384 487 384 487 384 488 384 487 384 488 1257 486 385 486 385 486 385 487 385 486 386 486 409 462 409 462 410 461 410 462 410 461 1282 462 409 462 1282 462 1282 461 410 462 410 461 410 461 410 462 1282 461 410 462 1282 461 1282 462 410 461 1282 462 +# +name: Ch_prev +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 3519 1775 426 448 422 1332 418 455 425 449 421 459 421 452 418 456 424 450 420 459 421 452 418 456 424 450 420 460 420 1339 422 458 422 451 419 455 425 449 421 459 421 452 418 455 425 449 421 459 421 1339 422 457 423 451 419 454 426 448 422 458 422 451 419 454 426 448 422 1331 419 454 426 1327 423 450 420 1307 454 1299 451 449 421 453 427 1326 424 449 421 1333 417 456 424 1329 421 1304 446 454 426 1327 423 +# +name: Vol_dn +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 3516 1747 444 456 424 1329 421 453 427 446 424 451 419 460 420 454 426 447 423 451 419 461 419 454 426 447 423 451 419 1334 427 448 422 458 422 451 419 455 425 450 420 459 421 453 417 456 424 450 420 1333 417 457 423 456 424 449 421 453 427 447 423 457 423 450 420 454 426 1328 422 451 419 456 424 455 425 448 422 1331 419 455 425 448 422 1332 418 455 425 449 421 459 421 452 418 1336 425 449 421 1332 418 +# +name: Vol_up +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 3482 1730 448 425 450 1296 444 429 446 427 448 424 451 422 453 420 445 428 447 425 450 423 452 421 444 429 446 426 449 1297 454 419 446 427 448 425 450 423 452 420 445 428 447 426 449 424 451 421 444 1303 448 425 450 422 453 420 445 428 447 425 450 423 452 421 444 429 446 427 448 424 451 422 453 419 446 428 447 1298 453 420 445 428 447 426 449 424 451 421 444 429 446 427 448 1298 453 420 445 1301 450 +# +name: Vol_dn +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 3512 1700 478 395 480 1266 474 399 476 396 479 394 481 392 473 400 475 397 478 395 480 393 482 390 475 398 477 396 479 1267 473 399 476 397 478 395 480 392 473 400 475 398 477 396 479 394 481 391 474 1272 479 394 481 392 473 400 475 398 477 395 480 393 482 391 474 399 476 1269 482 391 474 399 476 397 478 395 480 1266 474 398 477 396 479 1267 473 399 476 397 478 394 481 392 473 1273 478 395 480 1266 474 +# +name: Vol_up +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 3596 1604 513 388 429 1282 459 442 428 442 428 442 428 442 428 442 428 441 429 441 429 442 428 442 428 443 426 445 449 1289 452 446 423 447 423 448 422 448 422 448 422 448 422 448 422 448 422 448 422 1319 422 448 422 448 422 448 422 448 422 448 422 448 422 448 423 448 422 448 422 448 422 448 422 448 422 448 422 1319 422 448 422 449 422 448 422 448 422 448 422 448 422 448 422 1319 422 448 422 1320 421 +# +name: Vol_dn +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 3480 1715 457 441 429 1284 457 442 428 442 428 442 428 442 428 442 428 441 429 442 428 442 453 417 453 417 453 418 451 1289 451 421 449 447 423 447 423 447 423 447 423 448 422 448 422 448 423 447 423 1318 423 447 423 448 422 448 423 447 423 448 423 448 422 448 422 448 422 1319 422 448 422 448 422 448 423 448 422 1319 422 448 423 448 422 1319 422 448 422 448 422 448 422 448 422 1319 422 448 423 1319 422 +# +name: Mute +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 3537 1660 458 441 429 1311 430 442 428 442 428 442 428 442 428 442 428 442 428 442 428 442 428 442 453 417 453 417 453 1287 453 420 449 422 447 447 423 448 422 448 422 448 422 448 422 448 422 448 422 1319 422 448 422 447 423 448 422 448 422 448 422 448 422 448 422 448 423 448 422 1319 422 448 422 448 423 1319 422 1319 423 448 422 448 422 448 422 1319 422 448 422 448 422 1319 422 1319 422 448 422 1319 422 +# +name: Ch_next +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 3481 1715 457 441 429 1311 430 442 429 442 428 442 428 442 428 442 428 441 429 442 428 442 453 417 453 417 452 419 450 1289 451 422 447 447 423 447 423 447 423 448 422 448 422 448 422 448 422 448 422 1319 422 448 422 448 422 448 422 448 422 448 423 448 422 448 422 448 422 448 422 448 422 1319 422 448 422 1319 422 1319 422 448 422 448 422 448 422 448 422 1319 422 448 422 1319 422 1319 422 448 422 1319 422 +# +# Model: Panasonic TC-P50S2 +# +name: Ch_prev +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 3505 1690 483 416 454 1258 483 416 454 416 454 417 428 442 428 442 428 441 455 416 454 416 454 416 454 417 452 419 450 1289 451 421 448 423 447 448 422 448 422 448 422 448 422 448 422 448 422 448 422 1319 422 448 422 448 422 448 422 448 423 448 422 448 423 448 422 448 422 1319 422 448 422 1319 422 448 422 1319 422 1319 422 448 422 448 423 1319 422 448 422 1319 422 448 422 1319 422 1320 421 449 421 1319 422 +# +name: Vol_up +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 228 144285 3545 1690 497 411 495 1256 496 382 465 410 466 410 493 383 493 386 489 411 464 413 462 415 460 417 459 417 459 418 459 1295 458 418 459 418 459 418 458 418 458 418 459 418 458 418 459 418 459 418 459 1295 459 418 459 418 458 418 459 418 458 418 458 418 458 419 458 418 458 419 458 419 458 418 458 419 458 419 458 1296 458 418 458 418 458 419 457 419 458 419 457 419 458 419 458 1296 457 419 457 1296 458 +# +# Model: Panasonic Unknown_Full +# +name: Ch_prev +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 3404 1652 462 422 432 1230 457 427 437 420 434 424 440 417 436 420 434 424 440 417 436 421 433 424 440 417 436 421 432 1228 459 425 439 419 434 422 432 426 438 419 434 422 431 426 438 419 435 422 431 1230 457 426 438 420 433 423 431 426 438 420 433 423 430 427 437 420 434 1228 459 424 440 1221 466 418 435 1225 462 1226 461 422 432 426 438 1224 463 420 433 1228 459 399 465 1222 465 1223 464 420 433 1225 462 +# +name: Vol_dn +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 2737 825 494 832 494 390 492 392 1352 1328 461 450 405 450 435 477 297 587 296 644 295 92395 2734 829 491 836 490 394 491 419 1286 1395 296 587 378 1474 239 +# +name: Power +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 182 7827 172 2332 177 2328 181 2323 176 2330 179 1309 175 1331 174 2331 178 1328 177 2328 181 1307 177 2327 182 1325 180 1326 179 1309 176 1331 174 1332 173 2333 176 2310 178 1328 177 2328 181 1325 180 2306 182 1325 180 2325 174 8340 183 7825 175 2330 179 2326 173 2333 176 2310 178 1328 177 1329 176 2329 180 1308 176 2329 180 1326 179 2326 173 1334 182 1306 179 1328 177 1329 176 1312 183 2322 177 2329 180 1326 179 2325 174 1315 180 2326 173 1333 183 2323 176 8339 183 7824 175 2330 179 2307 181 2324 175 2331 178 1328 177 1330 175 2311 177 1329 176 2329 180 1326 179 2327 182 1305 179 1328 177 1328 177 1311 173 1334 182 2323 176 2330 179 1326 571 1916 180 1327 178 2325 587 920 575 1930 579 +# +name: Vol_up +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 179 7828 182 2323 176 2328 181 2323 176 2329 180 1308 176 1330 175 2329 180 1326 179 2307 181 2323 176 2329 180 2325 174 1332 173 1315 180 1327 179 1328 177 2327 182 2322 177 1311 173 2332 177 1329 176 1330 175 1312 183 1324 181 8332 180 7826 174 2331 178 2326 173 2313 176 2330 179 1327 178 1328 177 2327 182 1306 179 2326 183 2322 177 2328 181 2323 176 1312 183 1324 181 1325 180 1325 180 2307 181 2323 176 1331 174 2330 179 1327 178 1310 175 1331 174 1332 173 8323 179 7845 176 2311 177 2327 182 2322 177 2328 181 1325 180 1308 177 2328 181 1325 180 2325 174 2330 179 2326 173 2314 174 1332 173 1332 173 1333 183 1306 178 2326 183 2322 177 1329 176 2328 181 1307 177 1329 176 1330 175 1313 182 +# +name: Vol_dn +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 183 7824 176 2329 180 2324 175 2329 180 2324 175 1314 181 1325 180 2324 175 1331 174 2331 178 2307 181 2324 175 1331 174 1331 174 1314 181 1326 179 1326 179 2325 174 2332 177 1310 174 2330 179 1327 178 1328 177 1310 174 2330 179 8334 178 7827 173 2332 177 2327 182 2323 176 2310 178 1328 177 1328 177 2327 182 1306 179 2326 183 2322 177 2327 182 1324 181 1307 177 1329 176 1330 176 1331 174 2311 177 2328 181 1325 180 2324 175 1332 173 1313 182 1325 180 2324 175 8339 173 1383 2522 3925 179 2325 576 1909 590 1915 594 1910 589 918 588 919 576 1911 175 1332 173 2330 592 1912 587 1918 581 907 588 918 598 908 587 920 575 913 592 1914 182 2320 592 914 581 1927 180 1307 178 1327 592 915 580 1925 574 +# +name: Ch_next +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 176 7830 180 2324 175 2329 180 2324 175 2329 180 1308 176 1329 176 2328 181 1325 180 2305 183 2321 178 1329 177 2327 182 1324 181 1306 178 1328 177 1329 176 2327 182 2304 174 1332 173 2332 177 1328 177 1310 174 2330 179 1327 178 8333 179 7826 174 2330 179 2325 174 2313 175 2329 180 1326 179 1326 179 2325 174 1315 180 2324 175 2329 180 1326 179 2325 174 1314 181 1325 180 1326 179 1308 177 2328 181 2323 176 1330 175 2329 180 1308 176 1330 175 2328 181 1325 180 8314 177 7845 176 2310 178 2327 182 2322 177 2327 182 1323 182 1306 179 2326 173 1333 183 2322 177 2327 182 1305 179 2326 173 1333 183 1323 182 1305 179 1327 178 2326 173 2331 178 1327 178 2308 180 1326 179 1327 178 2325 174 1315 180 +# +name: Ch_prev +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 182 7824 176 2328 181 2323 176 2328 181 2324 175 1312 183 1323 182 2322 177 1330 175 2309 179 2325 174 1332 173 1333 183 1305 179 1327 178 1328 177 1328 177 2308 180 262 177 1885 175 295 175 861 174 2330 179 99 313 911 573 915 590 1916 180 64 349 1910 176 265 174 7896 177 3081 835 3912 182 2322 177 207 179 1942 180 2304 174 268 181 1881 179 263 176 889 177 264 175 886 583 1925 182 271 178 855 180 240 178 1907 174 2331 178 1327 178 1309 176 1329 590 916 589 919 173 306 175 833 181 317 174 1832 175 2329 180 1325 180 309 182 1833 174 1313 182 1325 180 289 181 1853 175 2329 180 8313 178 7845 176 2309 179 2325 174 2331 178 2326 173 1333 183 1304 180 2324 175 1332 173 2330 179 2325 174 1314 181 1325 180 1326 179 1327 178 1308 177 1330 175 2329 180 2324 175 1312 183 2322 177 1329 176 1329 176 2329 180 2304 174 8339 173 351 3575 3902 594 1911 588 1916 593 1911 588 1916 583 904 591 916 589 1915 594 911 584 1920 579 1907 592 915 590 915 591 916 579 908 597 909 596 909 586 1919 580 1907 179 1326 592 1912 597 908 587 901 594 1911 588 1916 593 +# +name: Power +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 9047 4385 682 474 682 1578 708 476 679 1581 706 477 679 1582 705 1582 705 1582 678 1583 679 1607 679 1582 678 478 679 478 678 477 679 1582 705 1582 679 1583 679 1608 704 1582 705 478 678 1582 705 478 678 478 678 478 679 477 679 478 679 478 678 1582 705 478 678 1583 704 1582 705 1582 679 39574 9073 4387 679 478 678 1583 679 503 677 1584 705 478 678 1582 705 1582 705 1582 705 1582 705 1582 705 1582 678 479 677 479 678 479 677 1583 679 1608 704 1582 705 1582 705 1582 705 478 678 1583 704 478 678 478 678 1582 680 478 703 453 704 453 703 1557 703 480 676 1584 704 1583 704 478 678 +# +name: Ch_next +type: parsed +protocol: Samsung32 +address: 05 00 00 00 +command: 12 00 00 00 +# +# Model: Samsung BN59-01081A +# +name: Ch_prev +type: parsed +protocol: Samsung32 +address: 05 00 00 00 +command: 10 00 00 00 +# +name: Power +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 4499 4472 566 1662 565 1664 563 1665 562 565 538 564 539 562 541 560 543 558 545 1683 544 1658 569 1686 541 559 544 557 536 565 538 563 540 561 542 559 544 1684 543 558 545 556 537 564 539 562 541 560 543 558 545 1684 543 558 545 1683 544 1684 543 1660 567 1688 539 1689 538 1664 563 565 538 563 540 561 542 559 544 42973 4495 4472 566 1662 565 1663 564 1664 563 564 539 562 541 560 543 558 545 556 537 1691 536 1691 536 1666 572 555 538 564 539 561 542 559 544 557 536 565 538 1689 538 563 540 560 543 558 545 555 538 563 540 561 542 1686 541 559 544 1684 543 1684 543 1658 569 1685 542 1686 541 1660 567 559 544 557 536 565 538 563 540 42959 4499 4466 562 1666 572 1656 571 1656 571 556 537 564 539 562 541 559 544 556 537 1691 536 1690 537 1664 563 564 539 562 541 560 543 557 536 565 538 563 540 1688 539 561 542 559 544 556 537 564 539 562 541 560 543 1684 543 558 545 1682 545 1683 544 1657 570 1684 543 1659 568 1659 568 559 544 557 536 565 538 563 540 42955 4503 4463 565 1663 564 1663 564 1664 563 563 540 561 542 558 545 556 537 564 539 1688 539 1688 539 1662 565 562 541 559 544 557 536 565 538 563 540 560 543 1685 542 559 544 556 537 564 539 562 541 559 544 557 536 1692 535 565 538 1690 537 1690 537 1664 563 1691 536 1666 572 1656 571 556 537 564 539 562 541 559 544 +# +name: Vol_up +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 4507 4464 564 1690 537 1691 536 1666 572 555 538 538 565 562 541 559 544 557 536 1693 545 1656 571 1657 570 557 536 565 538 563 540 561 542 558 545 1683 544 1657 570 1684 543 558 545 555 538 563 540 561 542 559 544 557 536 565 538 563 540 1688 539 1662 565 1663 564 1663 564 1664 563 564 539 536 567 560 543 558 545 42962 4496 4470 568 1686 541 1660 567 1661 566 560 543 558 545 556 537 564 539 561 542 1686 541 1660 567 1687 540 534 569 558 545 555 538 563 540 561 542 1686 541 1686 541 1686 541 533 570 557 536 565 538 563 540 560 543 558 545 555 538 563 540 1661 566 1687 540 1661 566 1661 566 1662 565 561 542 533 570 557 536 564 539 +# +name: Vol_dn +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 4506 4465 563 1692 546 1658 569 1659 568 559 544 557 536 565 538 563 540 561 542 1687 540 1662 565 1689 538 563 540 561 542 560 543 558 545 556 537 1665 562 1693 545 530 563 1692 546 555 538 564 539 562 541 560 543 558 545 556 537 1665 562 539 564 1690 537 1691 536 1692 535 1693 545 556 537 539 564 563 540 561 542 42974 4505 4464 564 1690 537 1665 562 1667 571 530 563 565 538 563 540 561 542 559 544 1684 543 1659 568 1661 566 561 542 559 544 557 536 565 538 563 540 1663 564 1690 537 563 540 1662 565 559 558 545 556 537 564 539 562 541 1688 539 561 542 1687 540 1688 539 1688 539 1663 564 563 540 561 542 559 544 557 536 +# +name: Ch_next +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 4498 4471 567 1661 566 1662 565 1664 563 564 539 563 540 561 542 559 544 557 536 1693 545 1684 543 1659 568 559 544 557 536 566 537 564 539 562 541 560 543 1686 541 560 543 558 545 1684 543 558 545 556 537 564 539 1690 537 564 539 1689 538 1690 537 564 539 1689 538 1665 562 1666 572 556 537 564 539 563 540 560 543 42964 4504 4464 564 1690 537 1665 562 1666 572 555 538 564 539 562 541 560 543 558 545 1683 544 1684 543 1659 568 558 545 556 537 565 538 563 540 560 543 558 545 1683 544 556 537 565 538 1690 537 564 539 561 542 560 543 1685 542 558 545 1683 544 1657 570 557 536 1692 546 1683 544 1684 543 557 536 566 537 563 540 561 542 +# +name: Ch_prev +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 4506 4464 564 1691 536 1692 546 1656 571 556 537 564 539 562 541 560 543 558 545 1683 544 1684 543 1685 542 558 545 556 537 565 538 563 540 561 542 558 545 556 537 564 539 562 541 1687 540 561 542 558 545 556 537 1691 536 1665 562 1692 546 1656 571 556 537 1691 536 1692 546 1683 544 557 536 565 538 563 540 560 543 42966 4502 4466 562 1693 545 1683 544 1684 543 558 545 556 537 564 539 561 542 559 544 1684 543 1684 543 1658 569 558 545 530 563 564 539 562 541 560 543 558 535 566 537 564 539 562 541 1687 540 560 543 558 545 555 538 1691 536 1665 562 1693 545 1657 570 557 536 1666 572 1683 544 1658 569 557 546 529 564 563 540 561 542 +# +# Model: Samsung LE37S71B +# +name: Mute +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 4497 4474 564 1690 537 1691 536 1666 572 556 537 564 539 562 541 560 543 558 545 1683 544 1683 544 1684 543 532 571 556 537 564 539 562 541 560 543 1659 568 1686 541 1661 566 1662 565 562 541 560 543 558 535 565 538 563 540 561 542 558 545 556 537 1665 562 1666 572 1656 571 1684 543 557 546 555 538 564 539 562 541 42966 4502 4466 562 1692 535 1693 545 1657 570 556 537 565 538 562 541 560 543 543 1656 571 1656 571 1657 570 557 536 565 538 563 540 560 543 558 545 1683 544 1683 544 1657 570 1684 543 531 562 565 538 563 540 560 543 558 545 556 537 564 539 561 542 1660 567 1686 541 1687 540 1662 565 562 541 533 570 557 536 565 538 +# +name: Power +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 4567 4475 732 1555 703 1608 705 1608 705 478 676 460 669 507 651 506 649 508 621 1640 672 1616 673 1639 674 508 648 508 648 508 647 485 644 511 647 508 648 1639 674 508 649 508 647 485 645 511 647 508 648 509 647 1639 674 509 647 1613 673 1641 673 1640 673 1640 672 1617 671 1640 673 48544 4566 4505 648 1639 674 1639 674 1639 647 510 648 508 648 509 648 508 648 508 648 1639 647 1642 673 1639 674 508 648 509 648 508 647 485 645 511 647 509 647 1640 673 509 647 509 646 486 643 511 647 509 647 509 647 1640 673 509 647 1615 672 1640 673 1640 673 +# +name: Vol_up +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 4583 4485 687 1600 690 1623 716 1596 690 493 660 470 634 521 662 495 659 497 658 1629 684 1630 631 1657 682 500 656 500 656 500 656 500 656 475 653 1633 682 1631 682 1631 682 500 629 502 630 526 656 500 656 500 656 500 656 500 657 500 630 1632 682 1631 682 1631 682 1631 654 1633 682 48536 4553 4518 656 1632 682 1631 682 1631 682 500 630 502 629 526 656 500 656 501 656 1631 682 1631 631 1657 656 526 655 501 655 501 655 501 655 501 629 1634 680 1632 681 1632 681 501 629 502 629 527 654 501 655 +# +name: Vol_dn +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 4551 4491 683 1630 658 1628 685 1628 685 500 654 499 631 525 657 499 656 500 656 1632 681 1632 652 1635 681 501 655 501 655 501 655 501 655 501 628 1634 680 1632 681 501 655 1632 680 479 626 528 628 528 654 502 654 502 654 502 655 1632 654 504 627 1659 680 1632 681 1633 654 1634 679 48515 4596 4498 654 1634 653 1633 654 1633 628 529 653 502 654 502 654 502 654 502 654 1633 653 1635 679 1633 680 503 653 503 653 479 651 504 627 528 654 1633 680 1633 680 503 627 1634 679 503 653 503 653 503 653 503 653 503 627 504 627 1660 679 503 653 1634 679 1634 652 1636 678 1634 679 +# +name: Ch_next +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 1221 1189 435 588 436 890 433 2388 435 590 434 1489 434 1789 434 1190 434 1188 436 2689 435 1488 435 1190 434 86920 327 929 326 377 327 652 328 +# +# Model: Samsung Royal_Caribbean +# +name: Ch_prev +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 1195 1216 407 616 408 917 406 2414 410 617 406 1518 405 1815 409 1215 434 590 408 2717 406 1516 408 2417 407 86346 375 881 375 326 378 602 377 +# +name: Ch_next +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 1253 1157 464 560 465 858 460 2364 460 563 462 1462 463 1761 467 1156 465 1159 462 2661 467 1456 469 1155 466 86886 331 925 330 373 328 652 331 +# +# Model: Samsung TV_1 +# +name: Ch_prev +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 1224 1186 467 556 437 887 441 2382 463 560 433 1490 466 1757 461 1163 458 566 438 2686 463 1460 465 2359 434 86319 301 953 302 402 330 649 334 +# +name: Vol_dn +type: parsed +protocol: Samsung32 +address: 07 00 00 00 +command: 0b 00 00 00 +# +name: Mute +type: parsed +protocol: Samsung32 +address: 07 00 00 00 +command: 0f 00 00 00 +# +# Model: Sencor 25801 +# +name: Vol_dn +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 9040 4407 659 463 659 1579 661 461 685 437 658 463 658 463 658 464 657 470 656 1585 654 491 629 1611 628 1613 626 1614 625 1615 625 1615 625 502 625 1615 625 497 625 496 625 1614 625 1615 625 497 625 497 624 503 624 496 625 1615 624 1615 624 497 625 496 625 1615 624 1615 624 1614 624 41051 9036 2175 625 +# +name: Power +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 264 1848 264 792 264 792 264 792 264 792 264 792 264 1848 264 1848 264 792 264 1848 264 792 264 792 264 792 264 1848 264 792 264 43560 264 1848 264 792 264 792 264 792 264 792 264 1848 264 792 264 792 264 1848 264 792 264 1848 264 1848 264 1848 264 792 264 1848 264 43560 +# +name: Ch_next +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 264 1848 264 792 264 792 264 792 264 792 264 1848 264 792 264 792 264 792 264 1848 264 792 264 792 264 792 264 1848 264 792 264 43560 264 1848 264 792 264 792 264 792 264 792 264 792 264 1848 264 1848 264 1848 264 792 264 1848 264 1848 264 1848 264 792 264 1848 264 43560 +# +name: Ch_prev +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 264 1848 264 792 264 792 264 792 264 792 264 792 264 1848 264 792 264 792 264 1848 264 792 264 792 264 792 264 1848 264 792 264 43560 264 1848 264 792 264 792 264 792 264 792 264 1848 264 792 264 1848 264 1848 264 792 264 1848 264 1848 264 1848 264 792 264 1848 264 43560 +# +name: Vol_up +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 264 1848 264 792 264 792 264 792 264 792 264 792 264 792 264 1848 264 792 264 1848 264 792 264 792 264 792 264 1848 264 792 264 43560 264 1848 264 792 264 792 264 792 264 792 264 1848 264 1848 264 792 264 1848 264 792 264 1848 264 1848 264 1848 264 792 264 1848 264 43560 +# +name: Vol_dn +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 264 1848 264 792 264 792 264 792 264 792 264 1848 264 792 264 1848 264 792 264 1848 264 792 264 792 264 792 264 1848 264 792 264 43560 264 1848 264 792 264 792 264 792 264 792 264 792 264 1848 264 792 264 1848 264 792 264 1848 264 1848 264 1848 264 792 264 1848 264 43560 +# +# Model: Sharp Aquos_32BG3E +# +name: Mute +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 264 1848 264 792 264 792 264 792 264 792 264 1848 264 1848 264 1848 264 792 264 1848 264 792 264 792 264 792 264 1848 264 792 264 43560 264 1848 264 792 264 792 264 792 264 792 264 792 264 792 264 792 264 1848 264 792 264 1848 264 1848 264 1848 264 792 264 1848 264 43560 +# +name: Power +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 3381 1656 439 401 438 1239 439 401 438 1240 439 401 438 1239 440 401 438 1239 440 400 439 1238 440 401 438 1239 440 1240 438 426 438 1240 439 400 439 1240 439 1240 438 1240 438 1240 438 401 438 401 438 402 437 1242 436 402 437 1242 437 402 437 403 436 1242 437 402 437 403 436 403 436 403 436 1242 437 1242 437 403 436 1242 437 403 436 403 436 403 436 1242 437 403 436 403 436 403 436 1243 436 403 436 1242 437 1242 437 +# +name: Mute +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 3378 1656 440 400 439 1239 440 400 439 1239 439 400 439 1239 440 399 440 1238 440 400 439 1239 521 371 412 1215 464 1215 464 399 440 1239 439 399 440 1239 439 1240 438 1240 438 1241 438 401 438 401 438 402 437 1242 437 402 437 1242 437 402 437 402 437 1242 437 402 437 402 437 402 437 1242 437 1242 437 1242 437 402 437 1242 437 402 437 402 437 402 437 1242 437 402 437 402 437 402 437 402 437 402 437 1242 437 1242 437 +# +name: Vol_up +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 3374 1661 436 403 436 1243 436 404 435 1243 436 404 435 1243 436 403 435 1242 464 376 463 1215 464 378 461 1216 463 1219 460 402 437 1242 436 403 436 1243 435 1244 435 1244 434 1245 433 405 434 406 433 406 433 1246 433 406 433 1246 433 406 433 406 433 1246 433 406 433 406 433 406 433 406 433 406 433 1246 433 406 433 1246 433 406 433 406 433 406 433 1246 433 406 433 406 433 406 433 1246 433 1246 433 1246 433 1246 433 +# +name: Vol_dn +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 3516 1521 438 401 438 1240 439 401 438 1240 439 401 438 1239 440 400 439 1238 466 375 465 1213 465 375 522 1156 522 1158 520 372 466 1184 494 372 412 1240 439 1240 438 1241 438 1241 437 402 437 402 437 402 437 1242 437 402 437 1242 437 403 436 402 437 1243 436 403 436 403 436 403 436 1243 436 403 436 1243 436 403 436 1243 436 403 436 403 436 403 436 1243 436 403 436 403 436 403 436 403 436 1243 436 1243 436 1243 436 +# +name: Ch_next +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 3382 1654 467 374 440 1238 465 375 440 1238 441 398 441 1238 466 374 466 1211 442 424 415 1238 441 424 415 1240 465 1238 441 398 441 1238 441 398 441 1238 440 1238 440 1239 439 1240 439 400 439 400 439 401 438 1241 438 401 438 1241 438 401 438 401 438 1241 438 401 438 401 438 401 438 1241 438 401 439 401 438 401 438 1241 438 401 438 401 438 401 438 1241 438 401 438 401 438 401 438 401 438 1241 438 401 438 1241 438 +# +# Model: Sharp g0684cesa_NES_TV +# +name: Ch_prev +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 3361 1649 443 423 416 1237 442 398 441 1235 444 398 441 1237 442 398 441 1237 442 397 442 1236 443 397 442 1236 443 1237 442 399 440 1236 443 398 441 1263 416 1263 472 1207 471 1207 470 371 414 423 416 423 416 1263 416 423 416 1263 416 423 416 423 416 1262 417 422 417 422 417 422 417 422 417 1262 416 422 417 422 417 1262 416 423 441 398 441 398 441 1238 441 398 441 398 441 399 440 1239 440 399 440 399 440 1238 441 +# +name: Power +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 278 1811 277 788 246 794 250 764 280 786 248 792 252 1813 275 1815 273 791 253 1812 276 789 255 785 249 791 253 1812 276 789 255 45322 280 1809 279 786 248 766 278 788 246 794 250 1815 273 792 252 788 246 1819 280 785 249 1817 271 1819 280 1810 278 787 247 1818 281 43217 274 1818 270 794 250 764 280 786 248 792 252 788 256 1809 279 1811 277 788 246 1819 280 785 249 766 278 762 272 1819 280 785 248 +# +name: Mute +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 278 1812 276 762 282 758 276 765 279 761 273 1818 281 1809 279 1811 277 762 282 1809 279 760 274 766 278 762 282 1809 279 760 274 44279 276 1813 275 763 281 759 275 766 278 762 272 768 276 764 280 760 274 1817 271 768 276 1815 273 1817 271 1819 280 759 275 1816 272 44276 279 1812 276 763 281 758 276 765 279 761 273 1818 281 1810 278 1811 277 762 272 1819 279 760 274 766 278 762 282 1809 279 760 274 +# +name: Vol_up +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 272 1817 271 794 250 790 254 786 248 792 252 762 272 794 250 1815 273 792 252 1813 275 790 254 785 249 766 278 1813 275 789 255 46372 273 1817 271 794 250 763 281 785 248 792 252 1813 275 1814 274 791 253 1812 276 789 255 1810 278 1812 276 1813 275 790 254 1811 277 42170 277 1814 274 791 253 787 247 793 251 763 281 759 275 791 253 1812 276 789 255 1810 278 787 247 793 251 789 255 1810 278 787 247 +# +name: Vol_dn +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 275 1814 274 791 253 787 247 793 251 789 255 1810 278 787 247 1818 281 785 249 1816 272 793 251 789 255 785 249 1816 272 766 278 45325 274 1815 273 792 252 762 272 794 250 790 254 786 247 1818 270 794 250 1815 273 792 252 1813 275 1815 273 1816 272 793 251 1814 274 43224 277 1814 274 764 280 786 248 792 252 788 246 1820 279 786 247 1817 271 768 276 1815 273 792 252 761 273 794 250 1815 273 791 253 +# +name: Ch_next +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 272 1817 271 794 250 790 254 786 248 792 252 1813 275 790 254 759 275 792 252 1813 275 789 255 785 248 792 252 1813 275 789 255 46372 273 1817 271 793 251 763 281 759 275 791 253 787 247 1818 281 1810 278 1812 276 789 255 1809 279 1811 277 1813 275 790 254 1811 277 42169 277 1815 273 792 252 787 247 794 250 789 255 1810 278 787 247 794 250 789 255 1810 278 761 273 793 251 789 255 1810 278 787 247 +# +# Model: Sharp LC-RC1-16 +# +name: Ch_prev +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 273 1816 272 767 277 789 255 785 249 791 253 787 246 1818 281 785 248 765 279 1812 276 789 255 759 275 791 253 1812 276 789 255 46372 281 1808 280 785 249 791 253 787 247 793 251 1814 274 791 253 1812 276 1814 274 791 253 1812 276 1814 274 1815 273 792 252 1813 275 42172 272 1819 280 785 249 765 279 761 273 768 276 764 280 1811 277 788 246 768 276 1815 273 792 252 788 246 794 250 1815 273 791 253 +# +name: Power +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 352 1747 353 694 354 694 353 694 354 694 354 693 354 1747 354 1745 355 693 355 1746 354 693 355 691 357 691 356 1745 355 689 356 46388 358 1740 359 689 358 688 360 689 358 689 359 1741 358 688 359 689 359 1741 358 691 356 1743 356 1743 356 1742 357 690 357 1741 356 44286 261 1839 260 786 262 786 262 785 263 785 263 786 262 1838 262 1838 262 786 262 1839 261 786 262 784 264 787 261 1837 263 783 262 46491 261 1839 261 786 262 786 262 786 261 786 262 1839 261 786 262 786 262 1839 261 786 262 1838 262 1838 262 1838 262 786 262 1835 262 +# +name: Vol_up +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 308 1788 312 735 313 734 313 735 313 736 312 735 313 736 312 1788 312 735 313 1786 314 735 313 735 313 734 314 1787 313 731 314 47491 314 1786 314 734 314 734 314 734 314 732 316 1786 315 1784 316 733 315 1786 315 732 316 1785 315 1787 314 1785 315 733 315 1781 317 43284 314 1784 316 731 317 730 318 732 316 732 316 732 316 731 317 1783 317 732 316 1784 316 733 315 731 317 731 317 1784 317 730 315 47500 313 1785 315 733 315 733 315 733 315 734 314 1787 313 1786 315 733 315 1787 313 733 315 1787 313 1786 314 1787 313 734 314 1783 314 +# +name: Vol_dn +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 357 1739 361 688 360 690 357 688 360 691 356 1741 359 689 359 1741 358 690 358 1742 357 691 357 690 357 693 355 1744 356 689 356 46399 355 1743 357 691 357 691 357 691 357 692 355 692 356 1744 356 692 356 1744 356 692 356 1744 355 1745 355 1745 263 785 263 1835 263 44390 261 1839 262 786 262 787 261 787 261 786 262 1839 261 787 261 1840 261 786 262 1839 262 786 262 785 263 785 263 1839 261 784 261 46497 262 1839 261 786 262 786 262 787 261 786 262 786 262 1838 262 786 262 1840 260 787 261 1839 261 1840 260 1840 260 787 261 1835 263 +# +name: Mute +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 260 1838 262 787 261 786 262 787 261 786 261 1838 262 1838 262 1838 262 786 262 1839 261 786 262 786 262 786 262 1839 261 784 261 45433 261 1839 261 785 263 785 263 785 263 785 263 786 262 786 262 785 263 1838 262 786 262 1839 261 1837 263 1838 262 786 262 1835 263 45436 356 1744 356 691 356 691 356 692 356 691 356 1743 357 1744 356 1745 355 691 356 1745 354 692 356 693 354 691 356 1745 355 691 353 45343 359 1742 358 688 360 688 359 689 359 687 361 688 360 689 359 688 360 1741 359 688 360 1742 358 1739 361 1741 359 689 359 1739 359 45337 286 1813 287 761 287 761 287 761 287 760 288 1813 287 1813 287 1813 287 760 288 1813 287 760 288 760 288 760 288 1812 288 757 288 45413 287 1814 286 761 287 761 287 759 289 760 288 760 288 761 287 760 288 1813 287 762 286 1813 287 1813 287 1813 361 685 288 1810 288 +# +name: Ch_next +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 356 1743 358 689 359 690 358 689 359 689 359 1741 359 688 360 688 360 688 360 1739 362 687 361 687 361 686 362 1739 361 684 361 47444 287 1812 288 760 288 759 289 760 288 759 288 760 288 1813 287 1812 288 1812 312 736 288 1812 288 1812 288 1813 287 760 288 1810 287 43309 286 1812 288 761 310 736 288 759 289 762 286 1812 312 737 311 736 312 736 312 1790 311 737 311 736 312 736 312 1789 311 734 312 47501 313 1786 314 733 315 734 314 733 315 734 314 733 315 1785 315 1785 315 1786 314 733 315 1785 315 1786 314 1786 314 731 317 1782 316 43279 339 1760 317 731 339 709 316 730 318 733 315 1783 317 732 316 731 317 731 317 1784 316 732 316 731 317 731 317 1785 315 729 316 +# +# Model: Sharp Roku_TV +# +name: Ch_prev +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 287 1812 288 760 288 761 287 761 287 761 287 760 288 1814 286 760 288 761 287 1813 287 760 288 760 288 760 288 1813 287 757 312 47498 340 1759 341 707 341 709 315 731 340 708 340 1760 340 707 341 1759 318 1783 317 730 318 1782 318 1783 317 1784 316 731 317 1782 316 43284 314 1787 314 736 312 734 314 734 314 734 314 735 313 1787 314 735 313 734 314 1787 313 735 313 734 314 733 315 1787 314 732 313 47500 285 1813 361 685 363 687 361 687 361 686 362 1739 360 687 361 1740 359 1740 360 689 358 1741 359 1743 356 1746 353 690 357 1741 356 +# +name: Power +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 195 1833 300 766 280 760 275 790 276 737 309 731 304 1801 301 1804 309 731 304 1801 270 795 282 758 277 762 273 1832 270 769 246 45851 326 1780 302 739 307 785 282 732 303 736 310 1795 307 732 303 763 303 1775 307 733 334 1798 273 1832 270 1810 251 814 273 1780 281 43762 302 1804 309 758 277 737 330 762 284 730 305 734 301 1803 310 1796 306 733 302 1829 273 767 279 734 301 791 275 1804 278 762 253 45870 307 1798 304 763 272 767 279 787 279 760 275 1829 284 730 305 734 301 1804 309 757 278 1827 275 1804 278 1828 274 765 270 1835 278 43740 303 1776 306 787 279 760 275 765 281 759 307 758 277 1775 307 1799 303 736 299 1832 281 759 276 763 304 736 299 1832 281 733 302 45820 306 1800 302 764 282 758 277 788 278 762 284 1821 281 732 303 736 310 1796 307 733 302 1829 273 1806 276 1830 272 767 268 1837 245 43772 302 1778 304 789 277 762 284 756 279 786 249 765 301 1777 336 1770 301 764 282 1824 278 761 274 765 301 738 308 1824 278 761 274 +# +# Model: Sharp TV2 +# +name: Mute +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 254 1721 360 681 354 738 308 706 329 711 355 1774 307 1772 361 1744 327 687 359 1772 299 742 335 705 330 736 279 1825 298 742 283 44773 384 1721 360 707 308 707 359 733 302 711 335 705 361 704 331 708 338 1766 336 704 331 1773 329 1776 306 1773 360 681 323 1782 331 44726 411 1722 328 686 360 733 302 711 335 705 361 1742 329 1803 330 1749 332 708 327 1777 335 705 330 710 325 741 274 1830 303 737 278 44778 359 1747 355 712 303 711 355 711 335 705 330 709 337 703 363 703 332 1770 332 709 337 1767 335 1771 300 1752 360 733 302 1776 326 44731 355 1751 330 711 355 737 309 705 330 710 336 1793 309 1771 331 1774 307 706 360 1771 300 740 326 714 332 735 280 1798 325 741 274 +# +name: Power +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 277 1806 274 775 281 776 279 770 275 774 281 768 277 1814 277 1806 274 775 280 1803 277 780 276 773 282 766 279 1831 249 781 274 45962 281 1803 277 771 274 783 273 802 253 770 275 1834 257 801 255 768 277 1807 273 775 280 1811 280 1804 276 1806 274 774 282 1811 280 43887 275 1809 282 767 278 779 276 799 256 766 279 771 274 1843 248 1810 281 767 278 1806 274 782 274 776 279 796 249 1807 273 784 282 45962 279 1804 276 772 273 784 282 768 277 798 247 1836 255 802 253 796 249 1808 283 766 279 1813 278 1805 275 1808 272 776 279 1813 278 43890 282 1801 279 769 276 781 274 775 280 769 276 773 283 1834 257 1801 279 769 276 1808 272 784 282 767 278 772 273 1810 281 776 279 +# +name: Ch_next +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 282 1801 280 769 276 781 275 774 282 768 277 1805 276 781 275 775 281 769 276 1832 249 809 247 776 280 770 275 1834 247 784 282 47004 273 1811 280 768 277 780 276 773 283 767 278 771 274 1816 275 1809 272 1811 280 768 277 1815 276 1807 274 1809 282 767 278 1813 278 42841 284 1799 282 768 277 780 276 774 282 767 278 1805 276 781 275 774 282 768 277 1806 275 782 274 775 281 769 276 1807 274 783 283 +# +name: Ch_prev +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 273 1810 281 768 277 780 276 799 257 767 278 797 248 1816 275 774 282 768 277 1806 275 808 248 801 255 795 250 1807 274 809 247 46989 278 1805 276 799 246 784 282 767 278 798 247 1835 256 775 281 1803 278 1806 275 773 283 1809 272 1811 280 1803 278 771 274 1817 274 42868 278 1806 275 799 257 775 281 768 277 798 247 776 280 1811 280 770 275 773 283 1801 280 777 279 771 274 801 255 1802 279 778 278 +# +name: Vol_up +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 282 1801 280 769 276 781 275 775 281 768 277 772 273 784 282 1801 280 770 275 1807 274 784 282 767 278 771 274 1809 282 775 281 47005 282 1801 280 770 275 782 274 776 280 769 276 1807 274 1817 274 775 280 1803 278 771 274 1817 274 1809 282 1801 280 770 275 1815 276 42841 273 1811 280 769 276 781 275 774 282 768 277 772 273 783 273 1811 280 794 251 1806 275 782 274 776 280 769 276 1808 273 783 283 +# +name: Vol_dn +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 277 1805 276 799 246 785 281 768 277 798 247 1810 281 775 281 1804 277 771 274 1810 281 801 255 795 250 772 273 1811 280 776 280 45957 274 1809 272 777 279 778 278 772 273 776 280 769 276 1815 276 799 246 1811 280 769 276 1815 276 1807 274 1809 282 767 278 1813 278 43890 280 1803 278 797 248 783 273 776 280 796 249 1834 247 784 282 1802 279 796 249 1808 273 783 283 793 252 770 275 1808 273 784 282 +# +# Model: Silver LE410004 +# +name: Mute +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 281 1803 278 771 274 782 274 775 281 769 276 1807 274 1817 274 1835 256 766 280 1804 277 780 276 773 283 767 278 1831 250 780 276 44910 276 1835 256 766 280 777 279 771 275 774 282 767 278 779 277 773 283 1800 281 768 277 1814 277 1806 275 1808 283 766 279 1811 280 44937 280 1803 278 771 274 783 283 766 279 770 275 1808 273 1819 272 1811 280 768 277 1807 274 782 274 776 280 769 276 1833 248 784 282 +# +name: Ch_next +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 8958 4449 510 4475 515 4444 515 2213 508 4477 513 2215 516 2212 509 2219 512 2217 514 2214 517 2211 540 2214 517 2211 510 4449 510 2218 513 4472 507 2220 511 30572 8960 2218 513 87698 8966 2211 510 87701 8963 2214 568 +# +name: Ch_prev +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 8956 4451 508 2220 511 2217 514 4470 510 4449 510 2218 544 2211 510 2218 513 2215 516 2212 509 2220 511 2217 514 2214 517 2211 510 2245 517 4441 508 2220 511 35049 8961 2215 516 87696 8959 2217 514 87698 8956 2220 511 87701 8964 2213 508 +# +name: Ch_prev +type: parsed +protocol: SIRC +address: 01 00 00 00 +command: 10 00 00 00 +# +name: Ch_next +type: parsed +protocol: SIRC +address: 01 00 00 00 +command: 11 00 00 00 +# +name: Power +type: parsed +protocol: NEC +address: 01 00 00 00 +command: 1C 00 00 00 +# +name: Vol_up +type: parsed +protocol: NEC +address: 01 00 00 00 +command: 4B 00 00 00 +# +name: Vol_dn +type: parsed +protocol: NEC +address: 01 00 00 00 +command: 4F 00 00 00 +# +name: Ch_next +type: parsed +protocol: NEC +address: 01 00 00 00 +command: 09 00 00 00 +# +name: Ch_prev +type: parsed +protocol: NEC +address: 01 00 00 00 +command: 05 00 00 00 +# +# Model: Strong TVD221_B1825 +# +name: Mute +type: parsed +protocol: NEC +address: 01 00 00 00 +command: 08 00 00 00 +# +name: Power +type: raw +frequency: 36700 +duty_cycle: 0.330000 +data: 3488 3488 872 2616 872 872 872 872 872 2616 872 872 872 2616 872 872 872 872 872 872 872 872 872 872 872 2616 872 872 872 2616 872 2616 872 872 872 2616 872 872 872 2616 872 2616 872 2616 872 2616 872 2616 872 872 872 34008 +# +name: Vol_up +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 3488 3488 872 2616 872 872 872 872 872 2616 872 872 872 2616 872 872 872 872 872 872 872 872 872 2616 872 2616 872 872 872 2616 872 2616 872 872 872 2616 872 872 872 2616 872 2616 872 2616 872 2616 872 872 872 872 872 34008 +# +name: Vol_dn +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 3488 3488 872 2616 872 872 872 872 872 2616 872 872 872 2616 872 2616 872 872 872 872 872 872 872 2616 872 2616 872 872 872 2616 872 2616 872 872 872 2616 872 872 872 872 872 2616 872 2616 872 2616 872 872 872 872 872 34008 +# +name: Ch_next +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 3488 3488 872 2616 872 872 872 872 872 2616 872 872 872 2616 872 872 872 872 872 872 872 872 872 2616 872 872 872 872 872 2616 872 2616 872 872 872 2616 872 872 872 2616 872 2616 872 2616 872 2616 872 872 872 2616 872 34008 +# +name: Ch_prev +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 3488 3488 872 2616 872 872 872 872 872 2616 872 872 872 2616 872 872 872 872 872 872 872 2616 872 872 872 872 872 872 872 2616 872 2616 872 872 872 2616 872 872 872 2616 872 2616 872 2616 872 872 872 2616 872 2616 872 34008 +# +# Model: TCL 32S327 +# +name: Mute +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 3488 3488 872 2616 872 872 872 872 872 2616 872 872 872 2616 872 2616 872 2616 872 872 872 872 872 2616 872 2616 872 872 872 2616 872 2616 872 872 872 2616 872 872 872 872 872 872 872 2616 872 2616 872 872 872 872 872 34008 +# +name: Vol_dn +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 4012 3982 514 1984 519 1979 514 1984 519 1979 514 983 519 980 512 1988 516 982 520 1979 514 1984 519 1978 515 983 519 980 511 987 515 983 519 980 511 1988 516 1982 511 987 515 1983 521 978 514 985 517 981 521 1978 515 +# +name: Ch_next +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 171 91301 168 17617 175 4907 167 15077 177 55723 170 4910 174 7448 174 15071 173 4908 176 17609 172 7450 172 7451 171 68432 169 7454 168 7454 168 4913 171 7451 171 4911 173 4908 176 7446 176 7446 176 50643 176 4905 169 4912 172 7450 172 7450 172 7451 171 4911 173 7448 174 4908 176 4905 169 7452 170 7453 169 50649 169 4912 173 4909 175 7447 175 7448 174 7448 174 4908 176 7446 176 4906 168 4913 171 7450 172 7451 171 50647 171 4911 173 4908 176 7445 177 7445 177 7446 176 4906 168 7454 168 4913 171 4910 174 7448 174 7449 173 50645 173 9990 169 7453 169 7454 168 7455 177 4905 169 7452 170 4912 172 4909 175 7447 175 7447 175 50644 174 4908 176 4905 169 7453 169 7454 168 7454 178 4904 170 7452 170 4912 172 4909 175 7447 175 7448 174 55726 177 4905 169 7453 169 7452 170 7453 169 4914 170 7451 171 4910 174 4907 177 7446 176 7446 176 50642 176 4906 168 4913 171 7452 170 7452 170 7453 169 4912 172 7450 172 4910 174 4907 177 7445 177 7446 176 50642 176 9987 172 7450 171 15074 170 4911 173 7450 172 4909 175 4906 168 7455 177 7446 176 50641 176 9988 171 7451 171 7451 171 7452 170 4912 172 7450 172 4909 175 4906 168 7454 178 7444 177 50641 167 4914 170 4911 174 7449 173 7449 173 7449 173 4909 175 7447 175 4907 177 4904 170 15075 169 50650 168 4913 171 4910 174 7448 174 7448 174 7448 174 4908 176 7447 175 4907 177 4903 171 7452 170 7452 170 50648 170 4912 172 4909 175 7446 176 7447 175 7448 174 4907 177 7446 176 4906 168 4912 172 7450 172 7451 171 50648 170 4912 172 4908 176 7446 176 7446 176 7447 175 4906 168 7454 178 4904 170 4911 173 15072 172 50646 173 4908 176 4906 168 7454 168 7455 177 7444 168 4915 169 7453 169 4913 171 4910 174 7448 174 7448 174 50644 174 4908 176 4904 170 7453 169 7453 169 7454 178 4904 170 40 198 7213 170 4910 174 4907 177 7446 176 7446 176 50643 175 4905 169 4913 171 7451 171 7452 170 7452 170 4911 173 7449 173 4909 175 4905 169 7454 168 7454 168 50650 178 4904 170 4911 173 7449 173 7450 172 7451 171 4910 174 7448 174 4907 177 4905 169 7453 169 7453 169 50649 169 4913 172 4910 174 15071 173 7449 173 4908 176 7447 175 4906 168 4913 171 7451 171 7451 171 50648 170 4911 173 4909 175 7446 176 7447 175 7448 174 4908 176 7446 176 4905 169 4913 171 15074 170 50648 170 4912 172 20154 175 7448 174 4907 177 7445 177 4904 170 4912 172 7450 172 7450 172 50647 170 4911 173 4908 176 7447 174 7447 174 7448 174 4908 176 7446 175 4906 168 4913 171 7451 171 7452 170 50649 168 4912 172 4910 174 7447 175 7448 174 7449 172 4909 175 7447 174 4907 177 4904 170 7452 170 7453 169 50649 169 4913 171 4910 174 7449 173 7449 173 7450 172 4909 175 7447 175 4906 168 4914 170 7452 170 7453 169 +# +name: Ch_prev +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 170 86221 170 7452 169 7453 168 12536 170 7452 169 43026 168 15077 177 7445 176 20150 168 15076 178 4904 170 7452 170 73516 168 7454 168 12536 171 7452 170 12534 172 7450 171 7451 171 43025 170 7452 170 15075 169 15077 177 4904 170 7452 170 7452 170 12535 172 7450 172 81138 178 12526 170 27779 172 +# +name: Vol_up +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 169 12535 174 88758 177 7445 168 4913 172 7450 173 4908 177 7445 168 48108 171 4910 175 4907 168 7454 169 7454 169 7453 170 7454 169 7452 171 4911 174 7448 175 4907 168 7454 169 +# +# Model: Telefunken d32f660x5cwi +# +name: Vol_dn +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 173 7448 175 7445 178 7443 170 7451 172 7449 174 7447 176 7445 168 4912 173 7447 176 7446 177 4903 172 43015 177 7443 170 7451 172 7448 175 7445 178 7444 169 7452 171 7449 174 4907 168 7452 171 7450 173 4907 168 43018 175 7447 176 7444 169 7453 170 7450 173 7448 175 7446 177 399 174 6870 169 4911 174 7447 176 7445 168 4912 173 43013 169 7451 172 7451 172 7448 175 7445 168 7453 170 7451 172 7449 174 4906 169 7452 171 7450 173 4907 168 43019 174 7447 176 7445 178 7443 170 7450 173 7448 175 7446 177 7444 169 4910 175 7447 176 7444 169 4913 172 43012 170 7452 171 7450 173 7448 175 7445 168 7453 170 7450 173 7449 174 4907 168 7452 171 7450 173 4908 177 43010 171 7448 175 7446 177 7444 169 7451 172 7448 175 +# +name: Power +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 328 605 321 283 643 290 313 589 316 287 639 596 642 589 316 285 318 285 641 591 637 294 309 587 328 +# +name: Vol_up +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 331 601 314 289 647 285 308 595 320 282 644 591 647 584 321 282 644 587 318 285 641 290 313 583 332 +# +name: Vol_dn +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 330 632 294 281 645 288 315 585 320 284 642 593 645 585 320 284 642 589 649 583 644 586 329 +# +name: Mute +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 333 600 315 288 638 294 309 594 321 281 645 591 636 595 643 589 638 592 313 290 646 585 330 +# +name: Ch_next +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 334 628 287 289 647 285 308 593 322 307 619 589 649 582 323 280 646 586 641 589 316 287 639 89967 331 602 313 290 646 286 307 595 320 283 643 591 647 584 321 282 644 588 639 591 314 288 638 +# +# Model: Thomson 40FS3003 +# +name: Ch_prev +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 332 601 314 290 646 286 307 594 321 282 644 590 648 582 323 281 645 586 641 290 313 583 644 +# +name: Power +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 9219 4484 662 469 661 469 661 1627 660 471 658 474 656 499 631 499 631 499 631 1657 630 1657 631 500 630 1657 631 1657 631 1657 630 1657 631 1657 631 500 630 500 630 500 630 1657 630 500 631 500 630 500 631 500 630 1657 630 1658 630 1657 631 500 630 1657 631 1658 630 1658 630 1658 630 40107 9106 2202 631 +# +name: Vol_up +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 9218 4484 636 495 660 469 661 1627 660 471 658 472 658 474 656 475 655 474 656 1632 655 1632 656 474 657 1632 656 1631 657 1632 656 1631 656 1632 656 474 656 1632 655 475 656 474 657 474 656 474 656 474 656 474 656 1632 655 474 656 1632 656 1632 656 1632 656 1632 656 1632 656 1632 656 40103 9107 2177 655 +# +name: Vol_dn +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 9245 4429 689 467 662 468 661 1626 660 471 658 473 657 474 656 474 656 474 656 1631 657 1631 657 474 656 1631 656 1632 656 1631 657 1631 657 1631 656 1632 656 1631 657 474 656 474 656 474 657 474 656 474 656 474 657 474 656 474 656 1631 656 1632 656 1632 656 1632 656 1632 656 1631 656 40082 9109 2175 656 +# +# Model: Vizio D43-C1 +# +name: Mute +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 9219 4485 636 495 660 469 661 1626 661 471 658 473 657 474 656 499 631 500 630 1657 630 1657 631 500 630 1657 630 1657 631 1657 631 1657 630 1657 631 1657 631 500 630 500 630 1657 631 500 630 500 630 500 630 500 631 500 630 1657 631 1657 631 500 630 1657 631 1658 630 1657 631 1658 630 39868 9106 2178 655 +# +name: Power +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 9016 4408 603 512 602 512 629 1599 686 428 630 483 630 484 629 485 628 492 627 1604 625 1605 624 491 623 1630 599 1631 598 1631 598 1631 598 1636 599 515 599 515 599 515 599 1631 599 515 599 515 599 515 599 521 599 1631 599 1631 599 1631 598 515 599 1631 598 1631 598 1631 598 1632 598 39999 9016 2166 626 95735 9012 2168 625 95735 9013 2167 626 +# +name: Vol_up +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 988 605 587 1176 588 589 586 882 587 1178 585 588 588 589 587 1470 587 1175 589 588 587 590 586 13118 986 608 587 1177 587 588 587 882 587 1178 586 588 587 589 587 1470 587 1177 587 590 585 588 588 13117 987 609 586 1177 587 590 585 882 587 1177 587 589 586 588 588 1472 585 1176 588 588 587 588 587 13118 986 608 587 1177 586 589 587 881 588 1177 587 588 587 589 587 1470 587 1177 587 589 586 588 588 13116 988 609 586 1178 586 589 586 883 586 1177 587 589 586 590 586 1471 586 1178 585 588 587 588 588 13117 987 609 586 1177 587 589 587 883 586 1177 586 589 587 589 586 1472 585 1176 588 589 587 589 587 13116 988 610 585 1178 586 588 588 882 587 1176 588 590 586 588 587 1471 586 1177 586 589 587 589 586 +# +# Model: Zenith SC3492Z +# +name: Vol_dn +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 990 603 589 1175 589 587 588 882 587 1175 588 588 588 589 586 1470 587 1471 587 588 588 881 588 12528 988 609 586 1177 587 589 586 882 587 1177 586 588 587 589 587 1470 587 1471 587 588 588 881 588 12527 989 607 588 1175 589 588 588 881 588 1175 588 588 588 587 589 1470 587 1470 588 589 587 881 588 12528 988 607 588 1175 589 588 587 882 587 1178 586 587 588 588 588 1472 586 1470 588 588 587 881 588 12529 987 608 587 1175 589 587 589 882 587 1175 589 589 587 587 589 1469 588 1470 588 587 589 881 588 12528 988 607 588 1175 589 590 585 883 586 1176 587 587 589 588 588 1471 586 1470 588 587 589 883 586 12527 989 607 588 1176 588 588 588 880 589 1176 588 588 588 589 586 1469 588 1469 589 588 587 881 588 +# +name: Power +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 611 386 612 3984 612 4984 612 387 611 3985 612 387 611 3986 611 4985 611 387 611 3986 611 4985 611 387 611 3986 610 4987 609 4985 611 389 609 +# +name: Mute +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 595 403 595 4002 594 5001 595 404 594 4002 594 405 593 4003 594 5001 595 5001 595 405 593 4003 594 405 593 4004 592 5004 592 406 592 4004 593 +# +name: Vol_up +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 596 401 597 4000 596 5000 596 402 596 4001 596 402 596 4002 595 5000 596 5000 596 403 595 4002 595 402 596 4003 594 5002 594 5000 596 404 594 +# +name: Vol_dn +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 484 514 485 4112 485 5111 485 514 485 4112 484 5110 486 513 486 4111 485 5110 486 514 485 4112 484 513 486 4112 484 5112 484 5113 483 513 486 +# +name: Ch_next +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 591 405 593 4005 591 5005 591 407 591 4006 591 408 590 4006 591 5006 590 5004 592 406 591 4005 592 5005 591 407 591 4006 591 407 591 4007 590 +# +# Model: Zenith tv +# +name: Ch_prev +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 598 400 598 3999 597 4999 596 401 597 4000 597 5000 596 402 596 3999 598 4999 597 401 597 4000 597 4998 598 402 596 4000 597 402 596 4000 597 +# +name: Power +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 561 442 562 4055 591 5032 592 413 591 4026 593 411 593 4026 593 5030 618 387 617 4001 592 5032 591 440 564 4028 591 5033 590 5034 589 440 563 +# +name: Mute +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 591 411 593 4025 593 5030 593 411 593 4025 593 411 593 4025 618 5005 619 5003 593 439 564 4027 592 439 564 4029 590 5033 590 439 564 4055 563 123130 617 387 618 3999 593 5031 592 439 564 4027 592 439 564 4029 590 5034 589 5059 563 440 563 4056 563 442 561 4058 561 5063 560 443 560 4059 560 +# +name: Ch_next +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 591 412 592 4026 593 5030 593 411 593 4025 594 411 617 4001 618 5005 619 5004 592 439 565 4028 590 5032 591 439 564 4055 564 440 563 4056 563 +# +name: Ch_prev +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 591 412 592 4026 592 5029 594 411 593 4025 593 5030 593 411 617 4001 619 5003 617 415 565 4027 592 5032 591 439 564 4030 589 439 564 4055 564 +# +name: Vol_up +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 591 413 591 4027 592 5030 593 411 593 4026 593 412 592 4025 618 5005 620 5004 592 439 564 4028 591 439 564 4029 590 5033 590 5035 588 440 563 122125 617 387 619 4000 592 5032 591 439 564 4028 591 439 565 4030 589 5034 589 5059 564 441 562 4057 562 442 561 4058 561 5063 560 5063 560 444 560 +# +name: Vol_dn +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 590 412 592 4026 593 5030 593 412 592 4025 594 5029 594 412 616 4002 618 5003 593 439 564 4027 592 439 564 4029 590 5033 590 5035 588 440 563 From 8059959624ee366ab6d4fdf6aa246809ea373d3e Mon Sep 17 00:00:00 2001 From: David Coles Date: Tue, 11 Feb 2025 08:01:12 -0800 Subject: [PATCH 021/268] Ensure that `furi_record_create` is passed a non-NULL data pointer (#4078) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Ensure that `furi_record_create` is passed a non-NULL data pointer It's currently possible to use `furi_record_create` to create and initialize a `FuriRecordData` pointing to NULL. This means its potentially possible for `furi_record_open` to return a NULL pointer which besides not being particularly useful means the Rust wrapper for `Record` can't assume that the returned record is always a non-NULL value. If by chance this is the intended behaviour, then we can just have the Rust wrapper do a `furi_check` itself, but it seems like it would be better to eliminate this potential corner-case at the source. * Furi: update furi_record_create documentation Co-authored-by: あく --- furi/core/record.c | 1 + furi/core/record.h | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/furi/core/record.c b/furi/core/record.c index fa384369a..17c95aa9b 100644 --- a/furi/core/record.c +++ b/furi/core/record.c @@ -80,6 +80,7 @@ bool furi_record_exists(const char* name) { void furi_record_create(const char* name, void* data) { furi_check(furi_record); furi_check(name); + furi_check(data); furi_record_lock(); diff --git a/furi/core/record.h b/furi/core/record.h index a269484f0..1fb20ed6f 100644 --- a/furi/core/record.h +++ b/furi/core/record.h @@ -27,7 +27,7 @@ bool furi_record_exists(const char* name); /** Create record * * @param name record name - * @param data data pointer + * @param data data pointer (not NULL) * @note Thread safe. Create and destroy must be executed from the same * thread. */ From 00f287e297e8c782942b0d69bf477adddfc0a8da Mon Sep 17 00:00:00 2001 From: Astra <93453568+Astrrra@users.noreply.github.com> Date: Thu, 13 Feb 2025 03:04:24 +0900 Subject: [PATCH 022/268] [FL-2754, FL-3945] EM4305 support (#4069) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Initial EM4305 write support * Support for writing EM4100 data to EM4305 blank tags * F18 API version bump * Satisfy pvs * Lib: cleanup em4305 code * Mask size fix * Electra * Fix leftovers from a previous implementation * Viking * Gallagher * LFRFID: cleanup em4305 Co-authored-by: あく --- lib/lfrfid/lfrfid_worker_modes.c | 112 +++++++++------- lib/lfrfid/protocols/lfrfid_protocols.h | 6 + lib/lfrfid/protocols/protocol_electra.c | 18 +++ lib/lfrfid/protocols/protocol_em4100.c | 26 ++++ lib/lfrfid/protocols/protocol_gallagher.c | 14 ++ lib/lfrfid/protocols/protocol_viking.c | 13 ++ lib/lfrfid/tools/em4305.c | 152 ++++++++++++++++++++++ lib/lfrfid/tools/em4305.h | 61 +++++++++ targets/f18/api_symbols.csv | 2 +- targets/f7/api_symbols.csv | 3 +- 10 files changed, 360 insertions(+), 47 deletions(-) create mode 100644 lib/lfrfid/tools/em4305.c create mode 100644 lib/lfrfid/tools/em4305.h diff --git a/lib/lfrfid/lfrfid_worker_modes.c b/lib/lfrfid/lfrfid_worker_modes.c index aec19e374..d3bcda042 100644 --- a/lib/lfrfid/lfrfid_worker_modes.c +++ b/lib/lfrfid/lfrfid_worker_modes.c @@ -499,9 +499,6 @@ static void lfrfid_worker_mode_emulate_process(LFRFIDWorker* worker) { static void lfrfid_worker_mode_write_process(LFRFIDWorker* worker) { LFRFIDProtocol protocol = worker->protocol; LFRFIDWriteRequest* request = malloc(sizeof(LFRFIDWriteRequest)); - request->write_type = LFRFIDWriteTypeT5577; - - bool can_be_written = protocol_dict_get_write_data(worker->protocols, protocol, request); uint32_t write_start_time = furi_get_tick(); bool too_long = false; @@ -510,63 +507,88 @@ static void lfrfid_worker_mode_write_process(LFRFIDWorker* worker) { size_t data_size = protocol_dict_get_data_size(worker->protocols, protocol); uint8_t* verify_data = malloc(data_size); uint8_t* read_data = malloc(data_size); + protocol_dict_get_data(worker->protocols, protocol, verify_data, data_size); - if(can_be_written) { - while(!lfrfid_worker_check_for_stop(worker)) { - FURI_LOG_D(TAG, "Data write"); - t5577_write(&request->t5577); + while(!lfrfid_worker_check_for_stop(worker)) { + FURI_LOG_D(TAG, "Data write"); + uint16_t skips = 0; + for(size_t i = 0; i < LFRFIDWriteTypeMax; i++) { + memset(request, 0, sizeof(LFRFIDWriteRequest)); + LFRFIDWriteType write_type = i; + request->write_type = write_type; - ProtocolId read_result = PROTOCOL_NO; - LFRFIDWorkerReadState state = lfrfid_worker_read_internal( - worker, - protocol_dict_get_features(worker->protocols, protocol), - LFRFID_WORKER_WRITE_VERIFY_TIME_MS, - &read_result); + protocol_dict_set_data(worker->protocols, protocol, verify_data, data_size); - if(state == LFRFIDWorkerReadOK) { - bool read_success = false; + bool can_be_written = + protocol_dict_get_write_data(worker->protocols, protocol, request); - if(read_result == protocol) { - protocol_dict_get_data(worker->protocols, protocol, read_data, data_size); - - if(memcmp(read_data, verify_data, data_size) == 0) { - read_success = true; - } - } - - if(read_success) { + if(!can_be_written) { + skips++; + if(skips == LFRFIDWriteTypeMax) { if(worker->write_cb) { - worker->write_cb(LFRFIDWorkerWriteOK, worker->cb_ctx); + worker->write_cb(LFRFIDWorkerWriteProtocolCannotBeWritten, worker->cb_ctx); } break; - } else { - unsuccessful_reads++; + } + continue; + } - if(unsuccessful_reads == LFRFID_WORKER_WRITE_MAX_UNSUCCESSFUL_READS) { - if(worker->write_cb) { - worker->write_cb(LFRFIDWorkerWriteFobCannotBeWritten, worker->cb_ctx); - } + memset(read_data, 0, data_size); + + if(request->write_type == LFRFIDWriteTypeT5577) { + t5577_write(&request->t5577); + } else if(request->write_type == LFRFIDWriteTypeEM4305) { + em4305_write(&request->em4305); + } else { + furi_crash("Unknown write type"); + } + } + ProtocolId read_result = PROTOCOL_NO; + LFRFIDWorkerReadState state = lfrfid_worker_read_internal( + worker, + protocol_dict_get_features(worker->protocols, protocol), + LFRFID_WORKER_WRITE_VERIFY_TIME_MS, + &read_result); + + if(state == LFRFIDWorkerReadOK) { + bool read_success = false; + + if(read_result == protocol) { + protocol_dict_get_data(worker->protocols, protocol, read_data, data_size); + + if(memcmp(read_data, verify_data, data_size) == 0) { + read_success = true; + } + } + + if(read_success) { + if(worker->write_cb) { + worker->write_cb(LFRFIDWorkerWriteOK, worker->cb_ctx); + } + break; + } else { + unsuccessful_reads++; + + if(unsuccessful_reads == LFRFID_WORKER_WRITE_MAX_UNSUCCESSFUL_READS) { + if(worker->write_cb) { + worker->write_cb(LFRFIDWorkerWriteFobCannotBeWritten, worker->cb_ctx); } } - } else if(state == LFRFIDWorkerReadExit) { - break; } + } else if(state == LFRFIDWorkerReadExit) { + break; + } - if(!too_long && - (furi_get_tick() - write_start_time) > LFRFID_WORKER_WRITE_TOO_LONG_TIME_MS) { - too_long = true; - if(worker->write_cb) { - worker->write_cb(LFRFIDWorkerWriteTooLongToWrite, worker->cb_ctx); - } + if(!too_long && + (furi_get_tick() - write_start_time) > LFRFID_WORKER_WRITE_TOO_LONG_TIME_MS) { + too_long = true; + if(worker->write_cb) { + worker->write_cb(LFRFIDWorkerWriteTooLongToWrite, worker->cb_ctx); } + } - lfrfid_worker_delay(worker, LFRFID_WORKER_WRITE_DROP_TIME_MS); - } - } else { - if(worker->write_cb) { - worker->write_cb(LFRFIDWorkerWriteProtocolCannotBeWritten, worker->cb_ctx); - } + lfrfid_worker_delay(worker, LFRFID_WORKER_WRITE_DROP_TIME_MS); } free(request); diff --git a/lib/lfrfid/protocols/lfrfid_protocols.h b/lib/lfrfid/protocols/lfrfid_protocols.h index e9c61616e..37b7f06cd 100644 --- a/lib/lfrfid/protocols/lfrfid_protocols.h +++ b/lib/lfrfid/protocols/lfrfid_protocols.h @@ -1,6 +1,7 @@ #pragma once #include #include "../tools/t5577.h" +#include "../tools/em4305.h" typedef enum { LFRFIDFeatureASK = 1 << 0, /** ASK Demodulation */ @@ -31,6 +32,7 @@ typedef enum { LFRFIDProtocolNexwatch, LFRFIDProtocolSecurakey, LFRFIDProtocolGProxII, + LFRFIDProtocolMax, } LFRFIDProtocol; @@ -38,11 +40,15 @@ extern const ProtocolBase* lfrfid_protocols[]; typedef enum { LFRFIDWriteTypeT5577, + LFRFIDWriteTypeEM4305, + + LFRFIDWriteTypeMax, } LFRFIDWriteType; typedef struct { LFRFIDWriteType write_type; union { LFRFIDT5577 t5577; + LFRFIDEM4305 em4305; }; } LFRFIDWriteRequest; diff --git a/lib/lfrfid/protocols/protocol_electra.c b/lib/lfrfid/protocols/protocol_electra.c index 014c83d1f..b4e17763f 100644 --- a/lib/lfrfid/protocols/protocol_electra.c +++ b/lib/lfrfid/protocols/protocol_electra.c @@ -407,6 +407,24 @@ bool protocol_electra_write_data(ProtocolElectra* protocol, void* data) { request->t5577.blocks_to_write = 5; result = true; } + if(request->write_type == LFRFIDWriteTypeEM4305) { + request->em4305.word[4] = + (EM4x05_MODULATION_MANCHESTER | EM4x05_SET_BITRATE(64) | (8 << EM4x05_MAXBLOCK_SHIFT)); + uint64_t encoded_data_reversed = 0; + uint64_t encoded_epilogue_reversed = 0; + for(uint8_t i = 0; i < 64; i++) { + encoded_data_reversed = (encoded_data_reversed << 1) | + ((protocol->encoded_base_data >> i) & 1); + encoded_epilogue_reversed = (encoded_epilogue_reversed << 1) | + ((protocol->encoded_epilogue >> i) & 1); + } + request->em4305.word[5] = encoded_data_reversed & 0xFFFFFFFF; + request->em4305.word[6] = encoded_data_reversed >> 32; + request->em4305.word[7] = encoded_epilogue_reversed & 0xFFFFFFFF; + request->em4305.word[8] = encoded_epilogue_reversed >> 32; + request->em4305.mask = 0x01F0; + result = true; + } return result; } diff --git a/lib/lfrfid/protocols/protocol_em4100.c b/lib/lfrfid/protocols/protocol_em4100.c index 8851a5369..83340895c 100644 --- a/lib/lfrfid/protocols/protocol_em4100.c +++ b/lib/lfrfid/protocols/protocol_em4100.c @@ -69,6 +69,19 @@ uint32_t protocol_em4100_get_t5577_bitrate(ProtocolEM4100* proto) { } } +uint32_t protocol_em4100_get_em4305_bitrate(ProtocolEM4100* proto) { + switch(proto->clock_per_bit) { + case 64: + return EM4x05_SET_BITRATE(64); + case 32: + return EM4x05_SET_BITRATE(32); + case 16: + return EM4x05_SET_BITRATE(16); + default: + return EM4x05_SET_BITRATE(64); + } +} + uint16_t protocol_em4100_get_short_time_low(ProtocolEM4100* proto) { return EM_READ_SHORT_TIME_BASE / protocol_em4100_get_time_divisor(proto) - EM_READ_JITTER_TIME_BASE / protocol_em4100_get_time_divisor(proto); @@ -339,6 +352,19 @@ bool protocol_em4100_write_data(ProtocolEM4100* protocol, void* data) { request->t5577.block[2] = protocol->encoded_data; request->t5577.blocks_to_write = 3; result = true; + } else if(request->write_type == LFRFIDWriteTypeEM4305) { + request->em4305.word[4] = + (EM4x05_MODULATION_MANCHESTER | protocol_em4100_get_em4305_bitrate(protocol) | + (6 << EM4x05_MAXBLOCK_SHIFT)); + uint64_t encoded_data_reversed = 0; + for(uint8_t i = 0; i < 64; i++) { + encoded_data_reversed = (encoded_data_reversed << 1) | + ((protocol->encoded_data >> i) & 1); + } + request->em4305.word[5] = encoded_data_reversed; + request->em4305.word[6] = encoded_data_reversed >> 32; + request->em4305.mask = 0x70; + result = true; } return result; } diff --git a/lib/lfrfid/protocols/protocol_gallagher.c b/lib/lfrfid/protocols/protocol_gallagher.c index 9ae0cf80a..bbed99706 100644 --- a/lib/lfrfid/protocols/protocol_gallagher.c +++ b/lib/lfrfid/protocols/protocol_gallagher.c @@ -264,6 +264,20 @@ bool protocol_gallagher_write_data(ProtocolGallagher* protocol, void* data) { request->t5577.block[3] = bit_lib_get_bits_32(protocol->encoded_data, 64, 32); request->t5577.blocks_to_write = 4; result = true; + } else if(request->write_type == LFRFIDWriteTypeEM4305) { + request->em4305.word[4] = + (EM4x05_MODULATION_MANCHESTER | EM4x05_SET_BITRATE(32) | (7 << EM4x05_MAXBLOCK_SHIFT)); + uint32_t encoded_data_reversed[3] = {0}; + for(uint8_t i = 0; i < (32 * 3); i++) { + encoded_data_reversed[i / 32] = + (encoded_data_reversed[i / 32] << 1) | + (bit_lib_get_bit(protocol->encoded_data, ((32 * 3) - i)) & 1); + } + request->em4305.word[5] = encoded_data_reversed[2]; + request->em4305.word[6] = encoded_data_reversed[1]; + request->em4305.word[7] = encoded_data_reversed[0]; + request->em4305.mask = 0xF0; + result = true; } return result; } diff --git a/lib/lfrfid/protocols/protocol_viking.c b/lib/lfrfid/protocols/protocol_viking.c index 78499a415..7e9009fde 100644 --- a/lib/lfrfid/protocols/protocol_viking.c +++ b/lib/lfrfid/protocols/protocol_viking.c @@ -171,6 +171,19 @@ bool protocol_viking_write_data(ProtocolViking* protocol, void* data) { request->t5577.block[2] = bit_lib_get_bits_32(protocol->encoded_data, 32, 32); request->t5577.blocks_to_write = 3; result = true; + } else if(request->write_type == LFRFIDWriteTypeEM4305) { + request->em4305.word[4] = + (EM4x05_MODULATION_MANCHESTER | EM4x05_SET_BITRATE(32) | (6 << EM4x05_MAXBLOCK_SHIFT)); + uint32_t encoded_data_reversed[2] = {0}; + for(uint8_t i = 0; i < 64; i++) { + encoded_data_reversed[i / 32] = + (encoded_data_reversed[i / 32] << 1) | + (bit_lib_get_bit(protocol->encoded_data, (63 - i)) & 1); + } + request->em4305.word[5] = encoded_data_reversed[1]; + request->em4305.word[6] = encoded_data_reversed[0]; + request->em4305.mask = 0x70; + result = true; } return result; } diff --git a/lib/lfrfid/tools/em4305.c b/lib/lfrfid/tools/em4305.c new file mode 100644 index 000000000..3b072d38d --- /dev/null +++ b/lib/lfrfid/tools/em4305.c @@ -0,0 +1,152 @@ +#include "em4305.h" +#include +#include + +#define TAG "EM4305" + +#define EM4305_TIMING_1 (32) +#define EM4305_TIMING_0_OFF (23) +#define EM4305_TIMING_0_ON (18) + +#define EM4305_FIELD_STOP_OFF_CYCLES (55) +#define EM4305_FIELD_STOP_ON_CYCLES (18) + +#define EM4305_TIMING_POWER_CHECK (1480) +#define EM4305_TIMING_EEPROM_WRITE (9340) + +static bool em4305_line_parity(uint8_t data) { + uint8_t parity = 0; + for(uint8_t i = 0; i < 8; i++) { + parity ^= (data >> i) & 1; + } + return parity; +} + +static uint64_t em4305_prepare_data(uint32_t data) { + uint8_t i, j; + uint64_t data_with_parity = 0; + + // 4 lines of 8 bits of data + // line even parity at bits 8 17 26 35 + // column even parity at bits 36-43 + // bit 44 is always 0 + // final table is 5 lines of 9 bits + + // line parity + for(i = 0; i < 4; i++) { + for(j = 0; j < 8; j++) { + data_with_parity = (data_with_parity << 1) | ((data >> (i * 8 + j)) & 1); + } + data_with_parity = (data_with_parity << 1) | (uint64_t)em4305_line_parity(data >> (i * 8)); + } + + // column parity + for(i = 0; i < 8; i++) { + uint8_t column_parity = 0; + for(j = 0; j < 4; j++) { + column_parity ^= (data >> (j * 8 + i)) & 1; + } + data_with_parity = (data_with_parity << 1) | column_parity; + } + + // bit 44 + data_with_parity = (data_with_parity << 1) | 0; + + return data_with_parity; +} + +static void em4305_start(void) { + furi_hal_rfid_tim_read_start(125000, 0.5); + + // do not ground the antenna + furi_hal_rfid_pin_pull_release(); +} + +static void em4305_stop(void) { + furi_hal_rfid_tim_read_stop(); + furi_hal_rfid_pins_reset(); +} + +static void em4305_write_bit(bool value) { + if(value) { + furi_delay_us(EM4305_TIMING_1 * 8); + } else { + furi_hal_rfid_tim_read_pause(); + furi_delay_us(EM4305_TIMING_0_OFF * 8); + furi_hal_rfid_tim_read_continue(); + furi_delay_us(EM4305_TIMING_0_ON * 8); + } +} + +static void em4305_write_opcode(uint8_t value) { + // 3 bit opcode + for(uint8_t i = 0; i < 3; i++) { + em4305_write_bit((value >> i) & 1); + } + + // parity + bool parity = 0; + for(uint8_t i = 0; i < 3; i++) { + parity ^= (value >> i) & 1; + } + em4305_write_bit(parity); +} + +static void em4305_field_stop() { + furi_hal_rfid_tim_read_pause(); + furi_delay_us(EM4305_FIELD_STOP_OFF_CYCLES * 8); + furi_hal_rfid_tim_read_continue(); + furi_delay_us(EM4305_FIELD_STOP_ON_CYCLES * 8); +} + +static void em4305_write_word(uint8_t address, uint32_t data) { + // parity + uint64_t data_with_parity = em4305_prepare_data(data); + + // power up the tag + furi_delay_us(8000); + + // field stop + em4305_field_stop(); + + // start bit + em4305_write_bit(0); + + // opcode + em4305_write_opcode(EM4x05_OPCODE_WRITE); + + // address + bool address_parity = 0; + for(uint8_t i = 0; i < 4; i++) { + em4305_write_bit((address >> (i)) & 1); + address_parity ^= (address >> (i)) & 1; + } + em4305_write_bit(0); + em4305_write_bit(0); + em4305_write_bit(address_parity); + + // data + for(uint8_t i = 0; i < 45; i++) { + em4305_write_bit((data_with_parity >> (44 - i)) & 1); + } + + // wait for power check and eeprom write + furi_delay_us(EM4305_TIMING_POWER_CHECK); + furi_delay_us(EM4305_TIMING_EEPROM_WRITE); +} + +void em4305_write(LFRFIDEM4305* data) { + furi_check(data); + + em4305_start(); + FURI_CRITICAL_ENTER(); + + for(uint8_t i = 0; i < EM4x05_WORD_COUNT; i++) { + if(data->mask & (1 << i)) { + em4305_write_word(i, data->word[i]); + } + } + + FURI_CRITICAL_EXIT(); + em4305_stop(); +} diff --git a/lib/lfrfid/tools/em4305.h b/lib/lfrfid/tools/em4305.h new file mode 100644 index 000000000..0cec00254 --- /dev/null +++ b/lib/lfrfid/tools/em4305.h @@ -0,0 +1,61 @@ +#pragma once +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +// EM4305/4205 chip config definitions, thanks proxmark3! +#define EM4x05_GET_BITRATE(x) ((((x) & 0x3F) * 2) + 2) +// Note: only data rates 8, 16, 32, 40(*) and 64 are supported. (*) only with EM4305 330pF +#define EM4x05_SET_BITRATE(x) (((x) - 2) / 2) +#define EM4x05_MODULATION_NRZ (0x00000000) +#define EM4x05_MODULATION_MANCHESTER (0x00000040) +#define EM4x05_MODULATION_BIPHASE (0x00000080) +#define EM4x05_MODULATION_MILLER (0x000000C0) // not supported by all 4x05/4x69 chips +#define EM4x05_MODULATION_PSK1 (0x00000100) // not supported by all 4x05/4x69 chips +#define EM4x05_MODULATION_PSK2 (0x00000140) // not supported by all 4x05/4x69 chips +#define EM4x05_MODULATION_PSK3 (0x00000180) // not supported by all 4x05/4x69 chips +#define EM4x05_MODULATION_FSK1 (0x00000200) // not supported by all 4x05/4x69 chips +#define EM4x05_MODULATION_FSK2 (0x00000240) // not supported by all 4x05/4x69 chips +#define EM4x05_PSK_RF_2 (0) +#define EM4x05_PSK_RF_4 (0x00000400) +#define EM4x05_PSK_RF_8 (0x00000800) +#define EM4x05_MAXBLOCK_SHIFT (14) +#define EM4x05_FIRST_USER_BLOCK (5) +#define EM4x05_SET_NUM_BLOCKS(x) \ + (((x) + 4) << 14) // number of blocks sent during default read mode +#define EM4x05_GET_NUM_BLOCKS(x) ((((x) >> 14) & 0xF) - 4) +#define EM4x05_READ_LOGIN_REQ (1 << 18) +#define EM4x05_READ_HK_LOGIN_REQ (1 << 19) +#define EM4x05_WRITE_LOGIN_REQ (1 << 20) +#define EM4x05_WRITE_HK_LOGIN_REQ (1 << 21) +#define EM4x05_READ_AFTER_WRITE (1 << 22) +#define EM4x05_DISABLE_ALLOWED (1 << 23) +#define EM4x05_READER_TALK_FIRST (1 << 24) +#define EM4x05_INVERT (1 << 25) +#define EM4x05_PIGEON (1 << 26) + +#define EM4x05_WORD_COUNT (16) + +#define EM4x05_OPCODE_LOGIN (0b001) +#define EM4x05_OPCODE_WRITE (0b010) +#define EM4x05_OPCODE_READ (0b100) +#define EM4x05_OPCODE_PROTECT (0b110) +#define EM4x05_OPCODE_DISABLE (0b101) + +typedef struct { + uint32_t word[EM4x05_WORD_COUNT]; /**< Word data to write */ + uint16_t mask; /**< Word mask */ +} LFRFIDEM4305; + +/** Write EM4305 tag data to tag + * + * @param data The data to write (mask is taken from that data) + */ +void em4305_write(LFRFIDEM4305* data); + +#ifdef __cplusplus +} +#endif diff --git a/targets/f18/api_symbols.csv b/targets/f18/api_symbols.csv index 29ed764e8..56bfbc2e6 100644 --- a/targets/f18/api_symbols.csv +++ b/targets/f18/api_symbols.csv @@ -1,5 +1,5 @@ entry,status,name,type,params -Version,+,79.2,, +Version,+,79.3,, Header,+,applications/services/bt/bt_service/bt.h,, Header,+,applications/services/bt/bt_service/bt_keys_storage.h,, Header,+,applications/services/cli/cli.h,, diff --git a/targets/f7/api_symbols.csv b/targets/f7/api_symbols.csv index 4a385d538..707acacff 100644 --- a/targets/f7/api_symbols.csv +++ b/targets/f7/api_symbols.csv @@ -1,5 +1,5 @@ entry,status,name,type,params -Version,+,79.2,, +Version,+,79.3,, Header,+,applications/drivers/subghz/cc1101_ext/cc1101_ext_interconnect.h,, Header,+,applications/services/bt/bt_service/bt.h,, Header,+,applications/services/bt/bt_service/bt_keys_storage.h,, @@ -1000,6 +1000,7 @@ Function,+,elements_string_fit_width,void,"Canvas*, FuriString*, size_t" Function,+,elements_text_box,void,"Canvas*, int32_t, int32_t, size_t, size_t, Align, Align, const char*, _Bool" Function,+,elf_resolve_from_hashtable,_Bool,"const ElfApiInterface*, uint32_t, Elf32_Addr*" Function,+,elf_symbolname_hash,uint32_t,const char* +Function,+,em4305_write,void,LFRFIDEM4305* Function,+,empty_screen_alloc,EmptyScreen*, Function,+,empty_screen_free,void,EmptyScreen* Function,+,empty_screen_get_view,View*,EmptyScreen* From ac1b723436c927a2b34f2a83a48e63990fa9efb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=82=E3=81=8F?= Date: Thu, 13 Feb 2025 03:53:14 +0900 Subject: [PATCH 023/268] Infrared: increase max carrier limit (#4070) Co-authored-by: Georgii Surkov <37121527+gsurkov@users.noreply.github.com> --- targets/furi_hal_include/furi_hal_infrared.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/targets/furi_hal_include/furi_hal_infrared.h b/targets/furi_hal_include/furi_hal_infrared.h index 29f7101c1..36eaf122d 100644 --- a/targets/furi_hal_include/furi_hal_infrared.h +++ b/targets/furi_hal_include/furi_hal_infrared.h @@ -13,7 +13,7 @@ extern "C" { #endif -#define INFRARED_MAX_FREQUENCY 56000 +#define INFRARED_MAX_FREQUENCY 1000000 #define INFRARED_MIN_FREQUENCY 10000 typedef enum { From 18e2d8d2f41d1d9dbc357829bf86b20a24962bcb Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Thu, 13 Feb 2025 03:33:49 +0300 Subject: [PATCH 024/268] upd changelog --- CHANGELOG.md | 32 +++++--------------------------- 1 file changed, 5 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 30c0c1f7d..3b4d73eb9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,34 +1,12 @@ ## Main changes - Current API: 79.3 -* SubGHz: Jolly Motors support (with add manually) (Thanks @pkooiman !) -* Power: Auto Power Off Timer (by @Dmitry422 with some fixes by @xMasterX) -* OFW: **Fix lost BadBLE keystrokes** -* OFW: **Add the ability to send a signal once via RPC** -* OFW PR 4070: Infrared: increase max carrier limit (by @skotopes) -* OFW PR 4025: Increase system stack's reserved memory size (Fix USB UART Bridge Crash) (by @Astrrra) -* OFW: merged gsurkov/vcp_break_support branch for usb uart bridge (WIP!!!) +* OFW: LFRFID - **EM4305 support** * Apps: **Check out more Apps updates and fixes by following** [this link](https://github.com/xMasterX/all-the-plugins/commits/dev) ## Other changes -* Power & Desktop: Add input events sub check & use event system for auto power off -* OFW: Rename FuriHalDebuging.md to FuriHalDebugging.md -* OFW: nfc: Fix MIFARE Plus detection -* OFW: u2f: Fix leaking message digest contexts -* OFW: nfc: Fix MFUL PWD_AUTH command creation -* OFW: Bump cross-spawn in /applications/system/js_app/packages/create-fz-app -* OFW: **Pipe** (new api funcs) -* OFW: Fix invalid path errors while deploying SDK by enforcing toolchain to use UTF-8 on initial SDK Extraction -* OFW: **Added flipper_format_write_empty_line(...)** -* OFW: Fix skylander ID reading -* OFW: Work around incorrect serial port handling by the OS -* OFW: Add winter animations -* OFW: FBT: Don't lint JS packages -* OFW: **Loader: Fix BusFault in handling of OOM** (was already included in previous UL release) -* OFW: **NFC Fix ISO15693 stucking in wrong mode.** -* OFW: Update `infrared_test.c` reference -* OFW: **FuriThread stdin** -* OFW: NFC: Plantain parser Last payment amount fix -* OFW: NFC clipper: BART station ids for San Lorenzo, Bay Fair -* OFW: Fix typo for mf_classic_key_cahce_get_next_key() function +* OFW: Infrared: increase max carrier limit +* OFW: Ensure that `furi_record_create` is passed a non-NULL data pointer +* OFW: Update mbedtls & expose AES +* OFW: Add the Showtime animation

#### Known NFC post-refactor regressions list: - Mifare Mini clones reading is broken (original mini working fine) (OFW) From e22669da96ffed7dd35911ce2a2a24d37225d917 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Thu, 13 Feb 2025 03:56:04 +0300 Subject: [PATCH 025/268] fixes and corrections --- CHANGELOG.md | 2 +- applications/services/input/input.c | 13 +------- applications/services/input/input_settings.c | 32 ++----------------- .../input_settings_app/input_settings_app.c | 10 +----- .../scenes/power_settings_scene_start.c | 2 +- extra.sh | 9 ------ rgb.sh | 5 --- 7 files changed, 6 insertions(+), 67 deletions(-) delete mode 100755 extra.sh delete mode 100755 rgb.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b4d73eb9..81bdd909e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,5 @@ ## Main changes -- Current API: 79.3 +- Current API: 79.4 * OFW: LFRFID - **EM4305 support** * Apps: **Check out more Apps updates and fixes by following** [this link](https://github.com/xMasterX/all-the-plugins/commits/dev) ## Other changes diff --git a/applications/services/input/input.c b/applications/services/input/input.c index 2b697d6ce..41759a1dd 100644 --- a/applications/services/input/input.c +++ b/applications/services/input/input.c @@ -82,17 +82,6 @@ const char* input_get_type_name(InputType type) { } } -// allocate memory for input_settings structure -static InputSettings* input_settings_alloc(void) { - InputSettings* settings = malloc(sizeof(InputSettings)); - return settings; -} - -//free memory from input_settings structure -void input_settings_free(InputSettings* settings) { - free(settings); -} - int32_t input_srv(void* p) { UNUSED(p); @@ -102,7 +91,7 @@ int32_t input_srv(void* p) { furi_record_create(RECORD_INPUT_EVENTS, event_pubsub); //define object input_settings, take memory load (or init) settings and create record for access to settings structure from outside - InputSettings* settings = input_settings_alloc(); + InputSettings* settings = malloc(sizeof(InputSettings)); furi_record_create(RECORD_INPUT_SETTINGS, settings); input_settings_load(settings); diff --git a/applications/services/input/input_settings.c b/applications/services/input/input_settings.c index 2868e5974..cd3de6d50 100644 --- a/applications/services/input/input_settings.c +++ b/applications/services/input/input_settings.c @@ -6,15 +6,10 @@ #define TAG "InputSettings" -#define INPUT_SETTINGS_VER_0 (0) // OLD version number -#define INPUT_SETTINGS_VER (1) // NEW actual version nnumber +#define INPUT_SETTINGS_VER (1) // version nnumber #define INPUT_SETTINGS_PATH INT_PATH(INPUT_SETTINGS_FILE_NAME) -#define INPUT_SETTINGS_MAGIC (0x19) - -typedef struct { - //inital set - empty -} InputSettingsV0; +#define INPUT_SETTINGS_MAGIC (0x29) void input_settings_load(InputSettings* settings) { furi_assert(settings); @@ -36,23 +31,6 @@ void input_settings_load(InputSettings* settings) { INPUT_SETTINGS_MAGIC, INPUT_SETTINGS_VER); // if config previous version - load it and inicialize new settings - } else if( - version == - INPUT_SETTINGS_VER_0) { // if config previous version - load it and manual set new settings to inital value - InputSettingsV0* settings_v0 = malloc(sizeof(InputSettingsV0)); - - success = saved_struct_load( - INPUT_SETTINGS_PATH, - settings_v0, - sizeof(InputSettingsV0), - INPUT_SETTINGS_MAGIC, - INPUT_SETTINGS_VER_0); - - if(success) { - settings->vibro_touch_level = 0; - } - - free(settings_v0); } // in case of another config version we exit from useless cycle to next step } while(false); @@ -75,12 +53,6 @@ void input_settings_save(const InputSettings* settings) { INPUT_SETTINGS_MAGIC, INPUT_SETTINGS_VER); - // debug log - // FURI_LOG_D(TAG,"SAVE"); - // char buffer[12] = {}; - // snprintf(buffer, sizeof(buffer), "%d",settings->vibro_touch_level); - // FURI_LOG_D(TAG,buffer); - if(!success) { FURI_LOG_E(TAG, "Failed to save file"); } diff --git a/applications/settings/input_settings_app/input_settings_app.c b/applications/settings/input_settings_app/input_settings_app.c index 2902625ae..4f3e101da 100644 --- a/applications/settings/input_settings_app/input_settings_app.c +++ b/applications/settings/input_settings_app/input_settings_app.c @@ -42,8 +42,6 @@ static uint32_t input_settings_app_exit(void* context) { InputSettingsApp* input_settings_app_alloc(void) { InputSettingsApp* app = malloc(sizeof(InputSettingsApp)); - //app->inputservice = furi_record_open(RECORD_INPUT_EVENTS); - app->gui = furi_record_open(RECORD_GUI); app->settings = malloc(sizeof(InputSettings)); @@ -58,7 +56,7 @@ InputSettingsApp* input_settings_app_alloc(void) { item = variable_item_list_add( app->variable_item_list, - "VibroTouchLevel", + "Buttons Vibro", VIBRO_TOUCH_LEVEL_COUNT, input_settings_vibro_touch_level_changed, app); @@ -101,12 +99,6 @@ int32_t input_settings_app(void* p) { view_dispatcher_run(app->view_dispatcher); - // // debug code - // FURI_LOG_D(TAG,"Vibro Touch level before save"); - // char buffer[12] = {}; - // snprintf(buffer, sizeof(buffer), "%d",app->settings->vibro_touch_level); - // FURI_LOG_D(TAG,buffer); - //save current settings; input_settings_save(app->settings); diff --git a/applications/settings/power_settings_app/scenes/power_settings_scene_start.c b/applications/settings/power_settings_app/scenes/power_settings_scene_start.c index 2b8bd773f..42c53ed02 100644 --- a/applications/settings/power_settings_app/scenes/power_settings_scene_start.c +++ b/applications/settings/power_settings_app/scenes/power_settings_scene_start.c @@ -73,7 +73,7 @@ void power_settings_scene_start_on_enter(void* context) { item = variable_item_list_add( variable_item_list, - "Safe Charging", + "Limit Charge", CHARGE_SUPRESS_PERCENT_COUNT, power_settings_scene_start_charge_supress_percent_changed, app); diff --git a/extra.sh b/extra.sh deleted file mode 100755 index 923c2bc47..000000000 --- a/extra.sh +++ /dev/null @@ -1,9 +0,0 @@ -wget https://github.com/xMasterX/all-the-plugins/releases/latest/download/all-the-apps-extra.tgz -tar zxf all-the-apps-extra.tgz -mkdir -p applications/main/clock_app/resources/apps -cp -R extra_pack_build/artifacts-extra/* applications/main/clock_app/resources/apps/ -rm -rf extra_pack_build -rm -f build/f7-firmware-C/toolbox/version.* -./fbt COMPACT=1 DEBUG=0 updater_package -mkdir artifacts-extra-apps -mv dist/f7-C/* artifacts-extra-apps/ diff --git a/rgb.sh b/rgb.sh deleted file mode 100755 index 31f9144a4..000000000 --- a/rgb.sh +++ /dev/null @@ -1,5 +0,0 @@ -git apply .ci_files/rgb.patch -rm -f build/f7-firmware-C/toolbox/version.* -./fbt COMPACT=1 DEBUG=0 updater_package -mkdir artifacts-rgb-patch -mv dist/f7-C/* artifacts-rgb-patch/ From d57e2c9ef7ca81d2087dc906bcac483f71c4823a Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Thu, 13 Feb 2025 03:56:22 +0300 Subject: [PATCH 026/268] fmt --- applications/services/desktop/desktop.c | 2 +- .../desktop_settings/scenes/desktop_settings_scene_start.c | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/applications/services/desktop/desktop.c b/applications/services/desktop/desktop.c index 843dbebb0..3eda85539 100644 --- a/applications/services/desktop/desktop.c +++ b/applications/services/desktop/desktop.c @@ -149,7 +149,7 @@ static bool desktop_custom_event_callback(void* context, uint32_t event) { if((desktop->settings.usb_inhibit_auto_lock) && (furi_hal_usb_is_locked())) { return (0); } - + desktop_lock(desktop); } } else if(event == DesktopGlobalSaveSettings) { diff --git a/applications/settings/desktop_settings/scenes/desktop_settings_scene_start.c b/applications/settings/desktop_settings/scenes/desktop_settings_scene_start.c index 9b992c80c..dfcac3eed 100644 --- a/applications/settings/desktop_settings/scenes/desktop_settings_scene_start.c +++ b/applications/settings/desktop_settings/scenes/desktop_settings_scene_start.c @@ -52,7 +52,7 @@ const char* const usb_inhibit_auto_lock_delay_text[USB_INHIBIT_AUTO_LOCK_DELAY_C "ON", }; -const uint32_t usb_inhibit_auto_lock_delay_value[USB_INHIBIT_AUTO_LOCK_DELAY_COUNT] = {0,1}; +const uint32_t usb_inhibit_auto_lock_delay_value[USB_INHIBIT_AUTO_LOCK_DELAY_COUNT] = {0, 1}; #define CLOCK_ENABLE_COUNT 2 const char* const clock_enable_text[CLOCK_ENABLE_COUNT] = { From f5c59e0f912f1340add7caeb0b789b2dde47aa5a Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Thu, 13 Feb 2025 03:59:41 +0300 Subject: [PATCH 027/268] upd changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 81bdd909e..c4bdf3760 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ ## Main changes - Current API: 79.4 * OFW: LFRFID - **EM4305 support** +* Input: Vibro on Button press option (PR #867 | by @Dmitry422) +* Power: Option to limit battery charging (suppress charging on selected charge level) (PR #867 | by @Dmitry422) * Apps: **Check out more Apps updates and fixes by following** [this link](https://github.com/xMasterX/all-the-plugins/commits/dev) ## Other changes * OFW: Infrared: increase max carrier limit From 36327877ba4df7aaf76c69abe543202a4917e1f3 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Thu, 13 Feb 2025 04:04:36 +0300 Subject: [PATCH 028/268] move seasonal anims to temp folder todo: make automatic selection??? --- .../L1_Halloween_128x64/frame_0.png | Bin .../L1_Halloween_128x64/frame_1.png | Bin .../L1_Halloween_128x64/frame_2.png | Bin .../L1_Halloween_128x64/frame_3.png | Bin .../L1_Halloween_128x64/meta.txt | 0 .../L1_Happy_holidays_128x64/frame_0.png | Bin .../L1_Happy_holidays_128x64/frame_1.png | Bin .../L1_Happy_holidays_128x64/frame_10.png | Bin .../L1_Happy_holidays_128x64/frame_11.png | Bin .../L1_Happy_holidays_128x64/frame_12.png | Bin .../L1_Happy_holidays_128x64/frame_2.png | Bin .../L1_Happy_holidays_128x64/frame_3.png | Bin .../L1_Happy_holidays_128x64/frame_4.png | Bin .../L1_Happy_holidays_128x64/frame_5.png | Bin .../L1_Happy_holidays_128x64/frame_6.png | Bin .../L1_Happy_holidays_128x64/frame_7.png | Bin .../L1_Happy_holidays_128x64/frame_8.png | Bin .../L1_Happy_holidays_128x64/frame_9.png | Bin .../L1_Happy_holidays_128x64/meta.txt | 0 .../L1_New_year_128x64/frame_0.png | Bin .../L1_New_year_128x64/frame_1.png | Bin .../L1_New_year_128x64/frame_2.png | Bin .../L1_New_year_128x64/frame_3.png | Bin .../season_anims}/L1_New_year_128x64/meta.txt | 0 .../L1_Sleigh_ride_128x64/frame_0.png | Bin .../L1_Sleigh_ride_128x64/frame_1.png | Bin .../L1_Sleigh_ride_128x64/frame_10.png | Bin .../L1_Sleigh_ride_128x64/frame_11.png | Bin .../L1_Sleigh_ride_128x64/frame_12.png | Bin .../L1_Sleigh_ride_128x64/frame_13.png | Bin .../L1_Sleigh_ride_128x64/frame_14.png | Bin .../L1_Sleigh_ride_128x64/frame_15.png | Bin .../L1_Sleigh_ride_128x64/frame_16.png | Bin .../L1_Sleigh_ride_128x64/frame_17.png | Bin .../L1_Sleigh_ride_128x64/frame_18.png | Bin .../L1_Sleigh_ride_128x64/frame_19.png | Bin .../L1_Sleigh_ride_128x64/frame_2.png | Bin .../L1_Sleigh_ride_128x64/frame_20.png | Bin .../L1_Sleigh_ride_128x64/frame_21.png | Bin .../L1_Sleigh_ride_128x64/frame_22.png | Bin .../L1_Sleigh_ride_128x64/frame_23.png | Bin .../L1_Sleigh_ride_128x64/frame_24.png | Bin .../L1_Sleigh_ride_128x64/frame_25.png | Bin .../L1_Sleigh_ride_128x64/frame_26.png | Bin .../L1_Sleigh_ride_128x64/frame_27.png | Bin .../L1_Sleigh_ride_128x64/frame_28.png | Bin .../L1_Sleigh_ride_128x64/frame_29.png | Bin .../L1_Sleigh_ride_128x64/frame_3.png | Bin .../L1_Sleigh_ride_128x64/frame_30.png | Bin .../L1_Sleigh_ride_128x64/frame_31.png | Bin .../L1_Sleigh_ride_128x64/frame_32.png | Bin .../L1_Sleigh_ride_128x64/frame_33.png | Bin .../L1_Sleigh_ride_128x64/frame_34.png | Bin .../L1_Sleigh_ride_128x64/frame_35.png | Bin .../L1_Sleigh_ride_128x64/frame_36.png | Bin .../L1_Sleigh_ride_128x64/frame_4.png | Bin .../L1_Sleigh_ride_128x64/frame_5.png | Bin .../L1_Sleigh_ride_128x64/frame_6.png | Bin .../L1_Sleigh_ride_128x64/frame_7.png | Bin .../L1_Sleigh_ride_128x64/frame_8.png | Bin .../L1_Sleigh_ride_128x64/frame_9.png | Bin .../L1_Sleigh_ride_128x64/meta.txt | 0 .ci_files/season_anims/manifest.txt | 30 ++++++++++++++++++ assets/dolphin/external/manifest.txt | 22 ------------- 64 files changed, 30 insertions(+), 22 deletions(-) rename {assets/dolphin/external => .ci_files/season_anims}/L1_Halloween_128x64/frame_0.png (100%) rename {assets/dolphin/external => .ci_files/season_anims}/L1_Halloween_128x64/frame_1.png (100%) rename {assets/dolphin/external => .ci_files/season_anims}/L1_Halloween_128x64/frame_2.png (100%) rename {assets/dolphin/external => .ci_files/season_anims}/L1_Halloween_128x64/frame_3.png (100%) rename {assets/dolphin/external => .ci_files/season_anims}/L1_Halloween_128x64/meta.txt (100%) rename {assets/dolphin/external => .ci_files/season_anims}/L1_Happy_holidays_128x64/frame_0.png (100%) rename {assets/dolphin/external => .ci_files/season_anims}/L1_Happy_holidays_128x64/frame_1.png (100%) rename {assets/dolphin/external => .ci_files/season_anims}/L1_Happy_holidays_128x64/frame_10.png (100%) rename {assets/dolphin/external => .ci_files/season_anims}/L1_Happy_holidays_128x64/frame_11.png (100%) rename {assets/dolphin/external => .ci_files/season_anims}/L1_Happy_holidays_128x64/frame_12.png (100%) rename {assets/dolphin/external => .ci_files/season_anims}/L1_Happy_holidays_128x64/frame_2.png (100%) rename {assets/dolphin/external => .ci_files/season_anims}/L1_Happy_holidays_128x64/frame_3.png (100%) rename {assets/dolphin/external => .ci_files/season_anims}/L1_Happy_holidays_128x64/frame_4.png (100%) rename {assets/dolphin/external => .ci_files/season_anims}/L1_Happy_holidays_128x64/frame_5.png (100%) rename {assets/dolphin/external => .ci_files/season_anims}/L1_Happy_holidays_128x64/frame_6.png (100%) rename {assets/dolphin/external => .ci_files/season_anims}/L1_Happy_holidays_128x64/frame_7.png (100%) rename {assets/dolphin/external => .ci_files/season_anims}/L1_Happy_holidays_128x64/frame_8.png (100%) rename {assets/dolphin/external => .ci_files/season_anims}/L1_Happy_holidays_128x64/frame_9.png (100%) rename {assets/dolphin/external => .ci_files/season_anims}/L1_Happy_holidays_128x64/meta.txt (100%) rename {assets/dolphin/external => .ci_files/season_anims}/L1_New_year_128x64/frame_0.png (100%) rename {assets/dolphin/external => .ci_files/season_anims}/L1_New_year_128x64/frame_1.png (100%) rename {assets/dolphin/external => .ci_files/season_anims}/L1_New_year_128x64/frame_2.png (100%) rename {assets/dolphin/external => .ci_files/season_anims}/L1_New_year_128x64/frame_3.png (100%) rename {assets/dolphin/external => .ci_files/season_anims}/L1_New_year_128x64/meta.txt (100%) rename {assets/dolphin/external => .ci_files/season_anims}/L1_Sleigh_ride_128x64/frame_0.png (100%) rename {assets/dolphin/external => .ci_files/season_anims}/L1_Sleigh_ride_128x64/frame_1.png (100%) rename {assets/dolphin/external => .ci_files/season_anims}/L1_Sleigh_ride_128x64/frame_10.png (100%) rename {assets/dolphin/external => .ci_files/season_anims}/L1_Sleigh_ride_128x64/frame_11.png (100%) rename {assets/dolphin/external => .ci_files/season_anims}/L1_Sleigh_ride_128x64/frame_12.png (100%) rename {assets/dolphin/external => .ci_files/season_anims}/L1_Sleigh_ride_128x64/frame_13.png (100%) rename {assets/dolphin/external => .ci_files/season_anims}/L1_Sleigh_ride_128x64/frame_14.png (100%) rename {assets/dolphin/external => .ci_files/season_anims}/L1_Sleigh_ride_128x64/frame_15.png (100%) rename {assets/dolphin/external => .ci_files/season_anims}/L1_Sleigh_ride_128x64/frame_16.png (100%) rename {assets/dolphin/external => .ci_files/season_anims}/L1_Sleigh_ride_128x64/frame_17.png (100%) rename {assets/dolphin/external => .ci_files/season_anims}/L1_Sleigh_ride_128x64/frame_18.png (100%) rename {assets/dolphin/external => .ci_files/season_anims}/L1_Sleigh_ride_128x64/frame_19.png (100%) rename {assets/dolphin/external => .ci_files/season_anims}/L1_Sleigh_ride_128x64/frame_2.png (100%) rename {assets/dolphin/external => .ci_files/season_anims}/L1_Sleigh_ride_128x64/frame_20.png (100%) rename {assets/dolphin/external => .ci_files/season_anims}/L1_Sleigh_ride_128x64/frame_21.png (100%) rename {assets/dolphin/external => .ci_files/season_anims}/L1_Sleigh_ride_128x64/frame_22.png (100%) rename {assets/dolphin/external => .ci_files/season_anims}/L1_Sleigh_ride_128x64/frame_23.png (100%) rename {assets/dolphin/external => .ci_files/season_anims}/L1_Sleigh_ride_128x64/frame_24.png (100%) rename {assets/dolphin/external => .ci_files/season_anims}/L1_Sleigh_ride_128x64/frame_25.png (100%) rename {assets/dolphin/external => .ci_files/season_anims}/L1_Sleigh_ride_128x64/frame_26.png (100%) rename {assets/dolphin/external => .ci_files/season_anims}/L1_Sleigh_ride_128x64/frame_27.png (100%) rename {assets/dolphin/external => .ci_files/season_anims}/L1_Sleigh_ride_128x64/frame_28.png (100%) rename {assets/dolphin/external => .ci_files/season_anims}/L1_Sleigh_ride_128x64/frame_29.png (100%) rename {assets/dolphin/external => .ci_files/season_anims}/L1_Sleigh_ride_128x64/frame_3.png (100%) rename {assets/dolphin/external => .ci_files/season_anims}/L1_Sleigh_ride_128x64/frame_30.png (100%) rename {assets/dolphin/external => .ci_files/season_anims}/L1_Sleigh_ride_128x64/frame_31.png (100%) rename {assets/dolphin/external => .ci_files/season_anims}/L1_Sleigh_ride_128x64/frame_32.png (100%) rename {assets/dolphin/external => .ci_files/season_anims}/L1_Sleigh_ride_128x64/frame_33.png (100%) rename {assets/dolphin/external => .ci_files/season_anims}/L1_Sleigh_ride_128x64/frame_34.png (100%) rename {assets/dolphin/external => .ci_files/season_anims}/L1_Sleigh_ride_128x64/frame_35.png (100%) rename {assets/dolphin/external => .ci_files/season_anims}/L1_Sleigh_ride_128x64/frame_36.png (100%) rename {assets/dolphin/external => .ci_files/season_anims}/L1_Sleigh_ride_128x64/frame_4.png (100%) rename {assets/dolphin/external => .ci_files/season_anims}/L1_Sleigh_ride_128x64/frame_5.png (100%) rename {assets/dolphin/external => .ci_files/season_anims}/L1_Sleigh_ride_128x64/frame_6.png (100%) rename {assets/dolphin/external => .ci_files/season_anims}/L1_Sleigh_ride_128x64/frame_7.png (100%) rename {assets/dolphin/external => .ci_files/season_anims}/L1_Sleigh_ride_128x64/frame_8.png (100%) rename {assets/dolphin/external => .ci_files/season_anims}/L1_Sleigh_ride_128x64/frame_9.png (100%) rename {assets/dolphin/external => .ci_files/season_anims}/L1_Sleigh_ride_128x64/meta.txt (100%) create mode 100644 .ci_files/season_anims/manifest.txt diff --git a/assets/dolphin/external/L1_Halloween_128x64/frame_0.png b/.ci_files/season_anims/L1_Halloween_128x64/frame_0.png similarity index 100% rename from assets/dolphin/external/L1_Halloween_128x64/frame_0.png rename to .ci_files/season_anims/L1_Halloween_128x64/frame_0.png diff --git a/assets/dolphin/external/L1_Halloween_128x64/frame_1.png b/.ci_files/season_anims/L1_Halloween_128x64/frame_1.png similarity index 100% rename from assets/dolphin/external/L1_Halloween_128x64/frame_1.png rename to .ci_files/season_anims/L1_Halloween_128x64/frame_1.png diff --git a/assets/dolphin/external/L1_Halloween_128x64/frame_2.png b/.ci_files/season_anims/L1_Halloween_128x64/frame_2.png similarity index 100% rename from assets/dolphin/external/L1_Halloween_128x64/frame_2.png rename to .ci_files/season_anims/L1_Halloween_128x64/frame_2.png diff --git a/assets/dolphin/external/L1_Halloween_128x64/frame_3.png b/.ci_files/season_anims/L1_Halloween_128x64/frame_3.png similarity index 100% rename from assets/dolphin/external/L1_Halloween_128x64/frame_3.png rename to .ci_files/season_anims/L1_Halloween_128x64/frame_3.png diff --git a/assets/dolphin/external/L1_Halloween_128x64/meta.txt b/.ci_files/season_anims/L1_Halloween_128x64/meta.txt similarity index 100% rename from assets/dolphin/external/L1_Halloween_128x64/meta.txt rename to .ci_files/season_anims/L1_Halloween_128x64/meta.txt diff --git a/assets/dolphin/external/L1_Happy_holidays_128x64/frame_0.png b/.ci_files/season_anims/L1_Happy_holidays_128x64/frame_0.png similarity index 100% rename from assets/dolphin/external/L1_Happy_holidays_128x64/frame_0.png rename to .ci_files/season_anims/L1_Happy_holidays_128x64/frame_0.png diff --git a/assets/dolphin/external/L1_Happy_holidays_128x64/frame_1.png b/.ci_files/season_anims/L1_Happy_holidays_128x64/frame_1.png similarity index 100% rename from assets/dolphin/external/L1_Happy_holidays_128x64/frame_1.png rename to .ci_files/season_anims/L1_Happy_holidays_128x64/frame_1.png diff --git a/assets/dolphin/external/L1_Happy_holidays_128x64/frame_10.png b/.ci_files/season_anims/L1_Happy_holidays_128x64/frame_10.png similarity index 100% rename from assets/dolphin/external/L1_Happy_holidays_128x64/frame_10.png rename to .ci_files/season_anims/L1_Happy_holidays_128x64/frame_10.png diff --git a/assets/dolphin/external/L1_Happy_holidays_128x64/frame_11.png b/.ci_files/season_anims/L1_Happy_holidays_128x64/frame_11.png similarity index 100% rename from assets/dolphin/external/L1_Happy_holidays_128x64/frame_11.png rename to .ci_files/season_anims/L1_Happy_holidays_128x64/frame_11.png diff --git a/assets/dolphin/external/L1_Happy_holidays_128x64/frame_12.png b/.ci_files/season_anims/L1_Happy_holidays_128x64/frame_12.png similarity index 100% rename from assets/dolphin/external/L1_Happy_holidays_128x64/frame_12.png rename to .ci_files/season_anims/L1_Happy_holidays_128x64/frame_12.png diff --git a/assets/dolphin/external/L1_Happy_holidays_128x64/frame_2.png b/.ci_files/season_anims/L1_Happy_holidays_128x64/frame_2.png similarity index 100% rename from assets/dolphin/external/L1_Happy_holidays_128x64/frame_2.png rename to .ci_files/season_anims/L1_Happy_holidays_128x64/frame_2.png diff --git a/assets/dolphin/external/L1_Happy_holidays_128x64/frame_3.png b/.ci_files/season_anims/L1_Happy_holidays_128x64/frame_3.png similarity index 100% rename from assets/dolphin/external/L1_Happy_holidays_128x64/frame_3.png rename to .ci_files/season_anims/L1_Happy_holidays_128x64/frame_3.png diff --git a/assets/dolphin/external/L1_Happy_holidays_128x64/frame_4.png b/.ci_files/season_anims/L1_Happy_holidays_128x64/frame_4.png similarity index 100% rename from assets/dolphin/external/L1_Happy_holidays_128x64/frame_4.png rename to .ci_files/season_anims/L1_Happy_holidays_128x64/frame_4.png diff --git a/assets/dolphin/external/L1_Happy_holidays_128x64/frame_5.png b/.ci_files/season_anims/L1_Happy_holidays_128x64/frame_5.png similarity index 100% rename from assets/dolphin/external/L1_Happy_holidays_128x64/frame_5.png rename to .ci_files/season_anims/L1_Happy_holidays_128x64/frame_5.png diff --git a/assets/dolphin/external/L1_Happy_holidays_128x64/frame_6.png b/.ci_files/season_anims/L1_Happy_holidays_128x64/frame_6.png similarity index 100% rename from assets/dolphin/external/L1_Happy_holidays_128x64/frame_6.png rename to .ci_files/season_anims/L1_Happy_holidays_128x64/frame_6.png diff --git a/assets/dolphin/external/L1_Happy_holidays_128x64/frame_7.png b/.ci_files/season_anims/L1_Happy_holidays_128x64/frame_7.png similarity index 100% rename from assets/dolphin/external/L1_Happy_holidays_128x64/frame_7.png rename to .ci_files/season_anims/L1_Happy_holidays_128x64/frame_7.png diff --git a/assets/dolphin/external/L1_Happy_holidays_128x64/frame_8.png b/.ci_files/season_anims/L1_Happy_holidays_128x64/frame_8.png similarity index 100% rename from assets/dolphin/external/L1_Happy_holidays_128x64/frame_8.png rename to .ci_files/season_anims/L1_Happy_holidays_128x64/frame_8.png diff --git a/assets/dolphin/external/L1_Happy_holidays_128x64/frame_9.png b/.ci_files/season_anims/L1_Happy_holidays_128x64/frame_9.png similarity index 100% rename from assets/dolphin/external/L1_Happy_holidays_128x64/frame_9.png rename to .ci_files/season_anims/L1_Happy_holidays_128x64/frame_9.png diff --git a/assets/dolphin/external/L1_Happy_holidays_128x64/meta.txt b/.ci_files/season_anims/L1_Happy_holidays_128x64/meta.txt similarity index 100% rename from assets/dolphin/external/L1_Happy_holidays_128x64/meta.txt rename to .ci_files/season_anims/L1_Happy_holidays_128x64/meta.txt diff --git a/assets/dolphin/external/L1_New_year_128x64/frame_0.png b/.ci_files/season_anims/L1_New_year_128x64/frame_0.png similarity index 100% rename from assets/dolphin/external/L1_New_year_128x64/frame_0.png rename to .ci_files/season_anims/L1_New_year_128x64/frame_0.png diff --git a/assets/dolphin/external/L1_New_year_128x64/frame_1.png b/.ci_files/season_anims/L1_New_year_128x64/frame_1.png similarity index 100% rename from assets/dolphin/external/L1_New_year_128x64/frame_1.png rename to .ci_files/season_anims/L1_New_year_128x64/frame_1.png diff --git a/assets/dolphin/external/L1_New_year_128x64/frame_2.png b/.ci_files/season_anims/L1_New_year_128x64/frame_2.png similarity index 100% rename from assets/dolphin/external/L1_New_year_128x64/frame_2.png rename to .ci_files/season_anims/L1_New_year_128x64/frame_2.png diff --git a/assets/dolphin/external/L1_New_year_128x64/frame_3.png b/.ci_files/season_anims/L1_New_year_128x64/frame_3.png similarity index 100% rename from assets/dolphin/external/L1_New_year_128x64/frame_3.png rename to .ci_files/season_anims/L1_New_year_128x64/frame_3.png diff --git a/assets/dolphin/external/L1_New_year_128x64/meta.txt b/.ci_files/season_anims/L1_New_year_128x64/meta.txt similarity index 100% rename from assets/dolphin/external/L1_New_year_128x64/meta.txt rename to .ci_files/season_anims/L1_New_year_128x64/meta.txt diff --git a/assets/dolphin/external/L1_Sleigh_ride_128x64/frame_0.png b/.ci_files/season_anims/L1_Sleigh_ride_128x64/frame_0.png similarity index 100% rename from assets/dolphin/external/L1_Sleigh_ride_128x64/frame_0.png rename to .ci_files/season_anims/L1_Sleigh_ride_128x64/frame_0.png diff --git a/assets/dolphin/external/L1_Sleigh_ride_128x64/frame_1.png b/.ci_files/season_anims/L1_Sleigh_ride_128x64/frame_1.png similarity index 100% rename from assets/dolphin/external/L1_Sleigh_ride_128x64/frame_1.png rename to .ci_files/season_anims/L1_Sleigh_ride_128x64/frame_1.png diff --git a/assets/dolphin/external/L1_Sleigh_ride_128x64/frame_10.png b/.ci_files/season_anims/L1_Sleigh_ride_128x64/frame_10.png similarity index 100% rename from assets/dolphin/external/L1_Sleigh_ride_128x64/frame_10.png rename to .ci_files/season_anims/L1_Sleigh_ride_128x64/frame_10.png diff --git a/assets/dolphin/external/L1_Sleigh_ride_128x64/frame_11.png b/.ci_files/season_anims/L1_Sleigh_ride_128x64/frame_11.png similarity index 100% rename from assets/dolphin/external/L1_Sleigh_ride_128x64/frame_11.png rename to .ci_files/season_anims/L1_Sleigh_ride_128x64/frame_11.png diff --git a/assets/dolphin/external/L1_Sleigh_ride_128x64/frame_12.png b/.ci_files/season_anims/L1_Sleigh_ride_128x64/frame_12.png similarity index 100% rename from assets/dolphin/external/L1_Sleigh_ride_128x64/frame_12.png rename to .ci_files/season_anims/L1_Sleigh_ride_128x64/frame_12.png diff --git a/assets/dolphin/external/L1_Sleigh_ride_128x64/frame_13.png b/.ci_files/season_anims/L1_Sleigh_ride_128x64/frame_13.png similarity index 100% rename from assets/dolphin/external/L1_Sleigh_ride_128x64/frame_13.png rename to .ci_files/season_anims/L1_Sleigh_ride_128x64/frame_13.png diff --git a/assets/dolphin/external/L1_Sleigh_ride_128x64/frame_14.png b/.ci_files/season_anims/L1_Sleigh_ride_128x64/frame_14.png similarity index 100% rename from assets/dolphin/external/L1_Sleigh_ride_128x64/frame_14.png rename to .ci_files/season_anims/L1_Sleigh_ride_128x64/frame_14.png diff --git a/assets/dolphin/external/L1_Sleigh_ride_128x64/frame_15.png b/.ci_files/season_anims/L1_Sleigh_ride_128x64/frame_15.png similarity index 100% rename from assets/dolphin/external/L1_Sleigh_ride_128x64/frame_15.png rename to .ci_files/season_anims/L1_Sleigh_ride_128x64/frame_15.png diff --git a/assets/dolphin/external/L1_Sleigh_ride_128x64/frame_16.png b/.ci_files/season_anims/L1_Sleigh_ride_128x64/frame_16.png similarity index 100% rename from assets/dolphin/external/L1_Sleigh_ride_128x64/frame_16.png rename to .ci_files/season_anims/L1_Sleigh_ride_128x64/frame_16.png diff --git a/assets/dolphin/external/L1_Sleigh_ride_128x64/frame_17.png b/.ci_files/season_anims/L1_Sleigh_ride_128x64/frame_17.png similarity index 100% rename from assets/dolphin/external/L1_Sleigh_ride_128x64/frame_17.png rename to .ci_files/season_anims/L1_Sleigh_ride_128x64/frame_17.png diff --git a/assets/dolphin/external/L1_Sleigh_ride_128x64/frame_18.png b/.ci_files/season_anims/L1_Sleigh_ride_128x64/frame_18.png similarity index 100% rename from assets/dolphin/external/L1_Sleigh_ride_128x64/frame_18.png rename to .ci_files/season_anims/L1_Sleigh_ride_128x64/frame_18.png diff --git a/assets/dolphin/external/L1_Sleigh_ride_128x64/frame_19.png b/.ci_files/season_anims/L1_Sleigh_ride_128x64/frame_19.png similarity index 100% rename from assets/dolphin/external/L1_Sleigh_ride_128x64/frame_19.png rename to .ci_files/season_anims/L1_Sleigh_ride_128x64/frame_19.png diff --git a/assets/dolphin/external/L1_Sleigh_ride_128x64/frame_2.png b/.ci_files/season_anims/L1_Sleigh_ride_128x64/frame_2.png similarity index 100% rename from assets/dolphin/external/L1_Sleigh_ride_128x64/frame_2.png rename to .ci_files/season_anims/L1_Sleigh_ride_128x64/frame_2.png diff --git a/assets/dolphin/external/L1_Sleigh_ride_128x64/frame_20.png b/.ci_files/season_anims/L1_Sleigh_ride_128x64/frame_20.png similarity index 100% rename from assets/dolphin/external/L1_Sleigh_ride_128x64/frame_20.png rename to .ci_files/season_anims/L1_Sleigh_ride_128x64/frame_20.png diff --git a/assets/dolphin/external/L1_Sleigh_ride_128x64/frame_21.png b/.ci_files/season_anims/L1_Sleigh_ride_128x64/frame_21.png similarity index 100% rename from assets/dolphin/external/L1_Sleigh_ride_128x64/frame_21.png rename to .ci_files/season_anims/L1_Sleigh_ride_128x64/frame_21.png diff --git a/assets/dolphin/external/L1_Sleigh_ride_128x64/frame_22.png b/.ci_files/season_anims/L1_Sleigh_ride_128x64/frame_22.png similarity index 100% rename from assets/dolphin/external/L1_Sleigh_ride_128x64/frame_22.png rename to .ci_files/season_anims/L1_Sleigh_ride_128x64/frame_22.png diff --git a/assets/dolphin/external/L1_Sleigh_ride_128x64/frame_23.png b/.ci_files/season_anims/L1_Sleigh_ride_128x64/frame_23.png similarity index 100% rename from assets/dolphin/external/L1_Sleigh_ride_128x64/frame_23.png rename to .ci_files/season_anims/L1_Sleigh_ride_128x64/frame_23.png diff --git a/assets/dolphin/external/L1_Sleigh_ride_128x64/frame_24.png b/.ci_files/season_anims/L1_Sleigh_ride_128x64/frame_24.png similarity index 100% rename from assets/dolphin/external/L1_Sleigh_ride_128x64/frame_24.png rename to .ci_files/season_anims/L1_Sleigh_ride_128x64/frame_24.png diff --git a/assets/dolphin/external/L1_Sleigh_ride_128x64/frame_25.png b/.ci_files/season_anims/L1_Sleigh_ride_128x64/frame_25.png similarity index 100% rename from assets/dolphin/external/L1_Sleigh_ride_128x64/frame_25.png rename to .ci_files/season_anims/L1_Sleigh_ride_128x64/frame_25.png diff --git a/assets/dolphin/external/L1_Sleigh_ride_128x64/frame_26.png b/.ci_files/season_anims/L1_Sleigh_ride_128x64/frame_26.png similarity index 100% rename from assets/dolphin/external/L1_Sleigh_ride_128x64/frame_26.png rename to .ci_files/season_anims/L1_Sleigh_ride_128x64/frame_26.png diff --git a/assets/dolphin/external/L1_Sleigh_ride_128x64/frame_27.png b/.ci_files/season_anims/L1_Sleigh_ride_128x64/frame_27.png similarity index 100% rename from assets/dolphin/external/L1_Sleigh_ride_128x64/frame_27.png rename to .ci_files/season_anims/L1_Sleigh_ride_128x64/frame_27.png diff --git a/assets/dolphin/external/L1_Sleigh_ride_128x64/frame_28.png b/.ci_files/season_anims/L1_Sleigh_ride_128x64/frame_28.png similarity index 100% rename from assets/dolphin/external/L1_Sleigh_ride_128x64/frame_28.png rename to .ci_files/season_anims/L1_Sleigh_ride_128x64/frame_28.png diff --git a/assets/dolphin/external/L1_Sleigh_ride_128x64/frame_29.png b/.ci_files/season_anims/L1_Sleigh_ride_128x64/frame_29.png similarity index 100% rename from assets/dolphin/external/L1_Sleigh_ride_128x64/frame_29.png rename to .ci_files/season_anims/L1_Sleigh_ride_128x64/frame_29.png diff --git a/assets/dolphin/external/L1_Sleigh_ride_128x64/frame_3.png b/.ci_files/season_anims/L1_Sleigh_ride_128x64/frame_3.png similarity index 100% rename from assets/dolphin/external/L1_Sleigh_ride_128x64/frame_3.png rename to .ci_files/season_anims/L1_Sleigh_ride_128x64/frame_3.png diff --git a/assets/dolphin/external/L1_Sleigh_ride_128x64/frame_30.png b/.ci_files/season_anims/L1_Sleigh_ride_128x64/frame_30.png similarity index 100% rename from assets/dolphin/external/L1_Sleigh_ride_128x64/frame_30.png rename to .ci_files/season_anims/L1_Sleigh_ride_128x64/frame_30.png diff --git a/assets/dolphin/external/L1_Sleigh_ride_128x64/frame_31.png b/.ci_files/season_anims/L1_Sleigh_ride_128x64/frame_31.png similarity index 100% rename from assets/dolphin/external/L1_Sleigh_ride_128x64/frame_31.png rename to .ci_files/season_anims/L1_Sleigh_ride_128x64/frame_31.png diff --git a/assets/dolphin/external/L1_Sleigh_ride_128x64/frame_32.png b/.ci_files/season_anims/L1_Sleigh_ride_128x64/frame_32.png similarity index 100% rename from assets/dolphin/external/L1_Sleigh_ride_128x64/frame_32.png rename to .ci_files/season_anims/L1_Sleigh_ride_128x64/frame_32.png diff --git a/assets/dolphin/external/L1_Sleigh_ride_128x64/frame_33.png b/.ci_files/season_anims/L1_Sleigh_ride_128x64/frame_33.png similarity index 100% rename from assets/dolphin/external/L1_Sleigh_ride_128x64/frame_33.png rename to .ci_files/season_anims/L1_Sleigh_ride_128x64/frame_33.png diff --git a/assets/dolphin/external/L1_Sleigh_ride_128x64/frame_34.png b/.ci_files/season_anims/L1_Sleigh_ride_128x64/frame_34.png similarity index 100% rename from assets/dolphin/external/L1_Sleigh_ride_128x64/frame_34.png rename to .ci_files/season_anims/L1_Sleigh_ride_128x64/frame_34.png diff --git a/assets/dolphin/external/L1_Sleigh_ride_128x64/frame_35.png b/.ci_files/season_anims/L1_Sleigh_ride_128x64/frame_35.png similarity index 100% rename from assets/dolphin/external/L1_Sleigh_ride_128x64/frame_35.png rename to .ci_files/season_anims/L1_Sleigh_ride_128x64/frame_35.png diff --git a/assets/dolphin/external/L1_Sleigh_ride_128x64/frame_36.png b/.ci_files/season_anims/L1_Sleigh_ride_128x64/frame_36.png similarity index 100% rename from assets/dolphin/external/L1_Sleigh_ride_128x64/frame_36.png rename to .ci_files/season_anims/L1_Sleigh_ride_128x64/frame_36.png diff --git a/assets/dolphin/external/L1_Sleigh_ride_128x64/frame_4.png b/.ci_files/season_anims/L1_Sleigh_ride_128x64/frame_4.png similarity index 100% rename from assets/dolphin/external/L1_Sleigh_ride_128x64/frame_4.png rename to .ci_files/season_anims/L1_Sleigh_ride_128x64/frame_4.png diff --git a/assets/dolphin/external/L1_Sleigh_ride_128x64/frame_5.png b/.ci_files/season_anims/L1_Sleigh_ride_128x64/frame_5.png similarity index 100% rename from assets/dolphin/external/L1_Sleigh_ride_128x64/frame_5.png rename to .ci_files/season_anims/L1_Sleigh_ride_128x64/frame_5.png diff --git a/assets/dolphin/external/L1_Sleigh_ride_128x64/frame_6.png b/.ci_files/season_anims/L1_Sleigh_ride_128x64/frame_6.png similarity index 100% rename from assets/dolphin/external/L1_Sleigh_ride_128x64/frame_6.png rename to .ci_files/season_anims/L1_Sleigh_ride_128x64/frame_6.png diff --git a/assets/dolphin/external/L1_Sleigh_ride_128x64/frame_7.png b/.ci_files/season_anims/L1_Sleigh_ride_128x64/frame_7.png similarity index 100% rename from assets/dolphin/external/L1_Sleigh_ride_128x64/frame_7.png rename to .ci_files/season_anims/L1_Sleigh_ride_128x64/frame_7.png diff --git a/assets/dolphin/external/L1_Sleigh_ride_128x64/frame_8.png b/.ci_files/season_anims/L1_Sleigh_ride_128x64/frame_8.png similarity index 100% rename from assets/dolphin/external/L1_Sleigh_ride_128x64/frame_8.png rename to .ci_files/season_anims/L1_Sleigh_ride_128x64/frame_8.png diff --git a/assets/dolphin/external/L1_Sleigh_ride_128x64/frame_9.png b/.ci_files/season_anims/L1_Sleigh_ride_128x64/frame_9.png similarity index 100% rename from assets/dolphin/external/L1_Sleigh_ride_128x64/frame_9.png rename to .ci_files/season_anims/L1_Sleigh_ride_128x64/frame_9.png diff --git a/assets/dolphin/external/L1_Sleigh_ride_128x64/meta.txt b/.ci_files/season_anims/L1_Sleigh_ride_128x64/meta.txt similarity index 100% rename from assets/dolphin/external/L1_Sleigh_ride_128x64/meta.txt rename to .ci_files/season_anims/L1_Sleigh_ride_128x64/meta.txt diff --git a/.ci_files/season_anims/manifest.txt b/.ci_files/season_anims/manifest.txt new file mode 100644 index 000000000..e3c01eb0f --- /dev/null +++ b/.ci_files/season_anims/manifest.txt @@ -0,0 +1,30 @@ +Filetype: Flipper Animation Manifest +Version: 1 + +Name: L1_Happy_holidays_128x64 +Min butthurt: 0 +Max butthurt: 14 +Min level: 1 +Max level: 3 +Weight: 3 + +Name: L1_Sleigh_ride_128x64 +Min butthurt: 0 +Max butthurt: 14 +Min level: 1 +Max level: 3 +Weight: 4 + +Name: L1_New_year_128x64 +Min butthurt: 0 +Max butthurt: 14 +Min level: 1 +Max level: 3 +Weight: 4 + +Name: L1_Halloween_128x64 +Min butthurt: 0 +Max butthurt: 14 +Min level: 1 +Max level: 3 +Weight: 4 diff --git a/assets/dolphin/external/manifest.txt b/assets/dolphin/external/manifest.txt index 0ea791ebb..6abab8e5c 100644 --- a/assets/dolphin/external/manifest.txt +++ b/assets/dolphin/external/manifest.txt @@ -211,20 +211,6 @@ Min level: 1 Max level: 3 Weight: 3 -Name: L1_Happy_holidays_128x64 -Min butthurt: 0 -Max butthurt: 14 -Min level: 1 -Max level: 3 -Weight: 3 - -Name: L1_Sleigh_ride_128x64 -Min butthurt: 0 -Max butthurt: 14 -Min level: 1 -Max level: 3 -Weight: 4 - Name: L1_Showtime_128x64 Min butthurt: 0 Max butthurt: 10 @@ -232,14 +218,6 @@ Min level: 1 Max level: 3 Weight: 4 - -Name: L1_New_year_128x64 -Min butthurt: 0 -Max butthurt: 14 -Min level: 1 -Max level: 3 -Weight: 4 - Name: L3_Fireplace_128x64 Min butthurt: 0 Max butthurt: 13 From 90422948377cfc2e7f203864cb19126108821784 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Thu, 13 Feb 2025 04:06:39 +0300 Subject: [PATCH 029/268] upd changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c4bdf3760..025ff31f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ * Power: Option to limit battery charging (suppress charging on selected charge level) (PR #867 | by @Dmitry422) * Apps: **Check out more Apps updates and fixes by following** [this link](https://github.com/xMasterX/all-the-plugins/commits/dev) ## Other changes +* Anims: Disable winter anims * OFW: Infrared: increase max carrier limit * OFW: Ensure that `furi_record_create` is passed a non-NULL data pointer * OFW: Update mbedtls & expose AES From 45529e76e9e76815172939a52359106b1a0d0589 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Thu, 13 Feb 2025 04:13:00 +0300 Subject: [PATCH 030/268] fix --- .../services/power/power_service/power_settings.c | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/applications/services/power/power_service/power_settings.c b/applications/services/power/power_service/power_settings.c index d12754784..3e101d25a 100644 --- a/applications/services/power/power_service/power_settings.c +++ b/applications/services/power/power_service/power_settings.c @@ -9,8 +9,9 @@ #define POWER_SETTINGS_VER_1 (1) // Previous version number #define POWER_SETTINGS_VER (2) // New version number -#define POWER_SETTINGS_PATH INT_PATH(POWER_SETTINGS_FILE_NAME) -#define POWER_SETTINGS_MAGIC (0x18) +#define POWER_SETTINGS_PATH INT_PATH(POWER_SETTINGS_FILE_NAME) +#define POWER_SETTINGS_MAGIC_V1 (0x19) +#define POWER_SETTINGS_MAGIC (0x21) typedef struct { uint32_t auto_poweroff_delay_ms; @@ -42,10 +43,11 @@ void power_settings_load(PowerSettings* settings) { POWER_SETTINGS_PATH, settings_previous, sizeof(PowerSettingsPrevious), - POWER_SETTINGS_MAGIC, + POWER_SETTINGS_MAGIC_V1, POWER_SETTINGS_VER_1); // new settings initialization if(success) { + settings->auto_poweroff_delay_ms = settings_previous->auto_poweroff_delay_ms; settings->charge_supress_percent = 0; } From e27f82f041d264341fe098e356f050eb0ef2aa61 Mon Sep 17 00:00:00 2001 From: Anna Antonenko Date: Thu, 13 Feb 2025 12:50:38 +0400 Subject: [PATCH 031/268] [FL-3925, FL-3942, FL-3944] JS features & bugfixes (SDK 0.2) (#4075) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: JS GPIO PWM, JS GUI Widget view; fix: JS EvtLoop stop on request, JS EvtLoop stop on error * fix: f18 build * docs: widget * fix: js unit test * change feature naming Co-authored-by: あく --- .../resources/unit_tests/js/basic.js | 2 +- applications/system/js_app/application.fam | 17 ++ .../js_app/examples/apps/Scripts/gpio.js | 8 + .../js_app/examples/apps/Scripts/gui.js | 44 +++ applications/system/js_app/js_modules.c | 2 + applications/system/js_app/js_modules.h | 14 +- applications/system/js_app/js_thread.c | 7 +- .../modules/js_event_loop/js_event_loop.c | 55 ++-- applications/system/js_app/modules/js_gpio.c | 103 ++++++- .../system/js_app/modules/js_gui/icon.c | 61 ++++ .../system/js_app/modules/js_gui/js_gui.c | 77 ++++- .../system/js_app/modules/js_gui/js_gui.h | 16 +- .../system/js_app/modules/js_gui/widget.c | 281 ++++++++++++++++++ .../js_app/packages/fz-sdk/gpio/index.d.ts | 28 ++ .../packages/fz-sdk/gui/byte_input.d.ts | 5 +- .../js_app/packages/fz-sdk/gui/dialog.d.ts | 5 +- .../packages/fz-sdk/gui/empty_screen.d.ts | 5 +- .../js_app/packages/fz-sdk/gui/icon.d.ts | 11 + .../js_app/packages/fz-sdk/gui/index.d.ts | 64 ++-- .../js_app/packages/fz-sdk/gui/loading.d.ts | 5 +- .../js_app/packages/fz-sdk/gui/submenu.d.ts | 5 +- .../js_app/packages/fz-sdk/gui/text_box.d.ts | 5 +- .../packages/fz-sdk/gui/text_input.d.ts | 5 +- .../js_app/packages/fz-sdk/gui/widget.d.ts | 66 ++++ .../js_app/packages/fz-sdk/package.json | 2 +- documentation/images/widget.png | Bin 0 -> 2390 bytes documentation/js/js_gui.md | 34 +-- documentation/js/js_gui__widget.md | 25 ++ lib/mjs/mjs_exec.c | 3 +- targets/f18/furi_hal/furi_hal_resources.h | 2 + targets/f7/furi_hal/furi_hal_pwm.h | 1 + targets/f7/furi_hal/furi_hal_resources.c | 2 + targets/f7/furi_hal/furi_hal_resources.h | 2 + 33 files changed, 858 insertions(+), 104 deletions(-) create mode 100644 applications/system/js_app/modules/js_gui/icon.c create mode 100644 applications/system/js_app/modules/js_gui/widget.c create mode 100644 applications/system/js_app/packages/fz-sdk/gui/icon.d.ts create mode 100644 applications/system/js_app/packages/fz-sdk/gui/widget.d.ts create mode 100644 documentation/images/widget.png create mode 100644 documentation/js/js_gui__widget.md diff --git a/applications/debug/unit_tests/resources/unit_tests/js/basic.js b/applications/debug/unit_tests/resources/unit_tests/js/basic.js index a08041e9f..26f3f68f5 100644 --- a/applications/debug/unit_tests/resources/unit_tests/js/basic.js +++ b/applications/debug/unit_tests/resources/unit_tests/js/basic.js @@ -12,4 +12,4 @@ tests.assert_eq(false, doesSdkSupport(["abobus", "other-nonexistent-feature"])); tests.assert_eq("flipperdevices", flipper.firmwareVendor); tests.assert_eq(0, flipper.jsSdkVersion[0]); -tests.assert_eq(1, flipper.jsSdkVersion[1]); +tests.assert_eq(2, flipper.jsSdkVersion[1]); diff --git a/applications/system/js_app/application.fam b/applications/system/js_app/application.fam index 73bdde21e..db1521b9d 100644 --- a/applications/system/js_app/application.fam +++ b/applications/system/js_app/application.fam @@ -110,6 +110,23 @@ App( fap_libs=["assets"], ) +App( + appid="js_gui__widget", + apptype=FlipperAppType.PLUGIN, + entry_point="js_view_widget_ep", + requires=["js_app"], + sources=["modules/js_gui/widget.c"], +) + +App( + appid="js_gui__icon", + apptype=FlipperAppType.PLUGIN, + entry_point="js_gui_icon_ep", + requires=["js_app"], + sources=["modules/js_gui/icon.c"], + fap_libs=["assets"], +) + App( appid="js_notification", apptype=FlipperAppType.PLUGIN, diff --git a/applications/system/js_app/examples/apps/Scripts/gpio.js b/applications/system/js_app/examples/apps/Scripts/gpio.js index 24d0f0286..6ea0a948f 100644 --- a/applications/system/js_app/examples/apps/Scripts/gpio.js +++ b/applications/system/js_app/examples/apps/Scripts/gpio.js @@ -3,6 +3,7 @@ let gpio = require("gpio"); // initialize pins let led = gpio.get("pc3"); // same as `gpio.get(7)` +let led2 = gpio.get("pa7"); // same as `gpio.get(2)` let pot = gpio.get("pc0"); // same as `gpio.get(16)` let button = gpio.get("pc1"); // same as `gpio.get(15)` led.init({ direction: "out", outMode: "push_pull" }); @@ -16,6 +17,13 @@ eventLoop.subscribe(eventLoop.timer("periodic", 1000), function (_, _item, led, return [led, !state]; }, led, true); +// cycle led pwm +print("Commencing PWM (PA7)"); +eventLoop.subscribe(eventLoop.timer("periodic", 10), function (_, _item, led2, state) { + led2.pwmWrite(10000, state); + return [led2, (state + 1) % 101]; +}, led2, 0); + // read potentiometer when button is pressed print("Press the button (PC1)"); eventLoop.subscribe(button.interrupt(), function (_, _item, pot) { diff --git a/applications/system/js_app/examples/apps/Scripts/gui.js b/applications/system/js_app/examples/apps/Scripts/gui.js index a1e023853..a1c104cf1 100644 --- a/applications/system/js_app/examples/apps/Scripts/gui.js +++ b/applications/system/js_app/examples/apps/Scripts/gui.js @@ -9,8 +9,23 @@ let byteInputView = require("gui/byte_input"); let textBoxView = require("gui/text_box"); let dialogView = require("gui/dialog"); let filePicker = require("gui/file_picker"); +let widget = require("gui/widget"); +let icon = require("gui/icon"); let flipper = require("flipper"); +// declare clock widget children +let cuteDolphinWithWatch = icon.getBuiltin("DolphinWait_59x54"); +let jsLogo = icon.getBuiltin("js_script_10px"); +let stopwatchWidgetElements = [ + { element: "string", x: 67, y: 44, align: "bl", font: "big_numbers", text: "00 00" }, + { element: "string", x: 77, y: 22, align: "bl", font: "primary", text: "Stopwatch" }, + { element: "frame", x: 64, y: 27, w: 28, h: 20, radius: 3 }, + { element: "frame", x: 100, y: 27, w: 28, h: 20, radius: 3 }, + { element: "icon", x: 0, y: 5, iconData: cuteDolphinWithWatch }, + { element: "icon", x: 64, y: 13, iconData: jsLogo }, + { element: "button", button: "right", text: "Back" }, +]; + // declare view instances let views = { loading: loadingView.make(), @@ -31,6 +46,7 @@ let views = { longText: textBoxView.makeWith({ text: "This is a very long string that demonstrates the TextBox view. Use the D-Pad to scroll backwards and forwards.\nLorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse rhoncus est malesuada quam egestas ultrices. Maecenas non eros a nulla eleifend vulputate et ut risus. Quisque in mauris mattis, venenatis risus eget, aliquam diam. Fusce pretium feugiat mauris, ut faucibus ex volutpat in. Phasellus volutpat ex sed gravida consectetur. Aliquam sed lectus feugiat, tristique lectus et, bibendum lacus. Ut sit amet augue eu sapien elementum aliquam quis vitae tortor. Vestibulum quis commodo odio. In elementum fermentum massa, eu pellentesque nibh cursus at. Integer eleifend lacus nec purus elementum sodales. Nulla elementum neque urna, non vulputate massa semper sed. Fusce ut nisi vitae dui blandit congue pretium vitae turpis.", }), + stopwatchWidget: widget.makeWith({}, stopwatchWidgetElements), demos: submenuView.makeWith({ header: "Choose a demo", items: [ @@ -40,6 +56,7 @@ let views = { "Byte input", "Text box", "File picker", + "Widget", "Exit app", ], }), @@ -72,6 +89,8 @@ eventLoop.subscribe(views.demos.chosen, function (_sub, index, gui, eventLoop, v views.helloDialog.set("center", "Nice!"); gui.viewDispatcher.switchTo(views.helloDialog); } else if (index === 6) { + gui.viewDispatcher.switchTo(views.stopwatchWidget); + } else if (index === 7) { eventLoop.stop(); } }, gui, eventLoop, views); @@ -111,6 +130,31 @@ eventLoop.subscribe(gui.viewDispatcher.navigation, function (_sub, _, gui, views gui.viewDispatcher.switchTo(views.demos); }, gui, views, eventLoop); +// go to the demo chooser screen when the right key is pressed on the widget screen +eventLoop.subscribe(views.stopwatchWidget.button, function (_sub, buttonId, gui, views) { + if (buttonId === "right") + gui.viewDispatcher.switchTo(views.demos); +}, gui, views); + +// count time +eventLoop.subscribe(eventLoop.timer("periodic", 500), function (_sub, _item, views, stopwatchWidgetElements, halfSeconds) { + let text = (halfSeconds / 2 / 60).toString(); + if (halfSeconds < 10 * 60 * 2) + text = "0" + text; + + text += (halfSeconds % 2 === 0) ? ":" : " "; + + if (((halfSeconds / 2) % 60) < 10) + text += "0"; + text += ((halfSeconds / 2) % 60).toString(); + + stopwatchWidgetElements[0].text = text; + views.stopwatchWidget.setChildren(stopwatchWidgetElements); + + halfSeconds++; + return [views, stopwatchWidgetElements, halfSeconds]; +}, views, stopwatchWidgetElements, 0); + // run UI gui.viewDispatcher.switchTo(views.demos); eventLoop.run(); diff --git a/applications/system/js_app/js_modules.c b/applications/system/js_app/js_modules.c index bffa553a8..47bdd516c 100644 --- a/applications/system/js_app/js_modules.c +++ b/applications/system/js_app/js_modules.c @@ -267,6 +267,8 @@ void js_check_sdk_compatibility(struct mjs* mjs) { static const char* extra_features[] = { "baseline", // dummy "feature" + "gpio-pwm", + "gui-widget", }; /** diff --git a/applications/system/js_app/js_modules.h b/applications/system/js_app/js_modules.h index 1dfd59521..29de72642 100644 --- a/applications/system/js_app/js_modules.h +++ b/applications/system/js_app/js_modules.h @@ -11,7 +11,7 @@ #define JS_SDK_VENDOR "flipperdevices" #define JS_SDK_MAJOR 0 -#define JS_SDK_MINOR 1 +#define JS_SDK_MINOR 2 /** * @brief Returns the foreign pointer in `obj["_"]` @@ -254,6 +254,18 @@ static inline void return; \ } while(0) +/** + * @brief Prepends an error, sets the JS return value to `undefined` and returns + * a value C function + * @warning This macro executes `return;` by design + */ +#define JS_ERROR_AND_RETURN_VAL(mjs, error_code, ret_val, ...) \ + do { \ + mjs_prepend_errorf(mjs, error_code, __VA_ARGS__); \ + mjs_return(mjs, MJS_UNDEFINED); \ + return ret_val; \ + } while(0) + typedef struct JsModules JsModules; typedef void* (*JsModuleConstructor)(struct mjs* mjs, mjs_val_t* object, JsModules* modules); diff --git a/applications/system/js_app/js_thread.c b/applications/system/js_app/js_thread.c index 600c2676e..4a6d23011 100644 --- a/applications/system/js_app/js_thread.c +++ b/applications/system/js_app/js_thread.c @@ -92,7 +92,7 @@ static void js_console_debug(struct mjs* mjs) { } static void js_exit_flag_poll(struct mjs* mjs) { - uint32_t flags = furi_thread_flags_wait(ThreadEventStop, FuriFlagWaitAny, 0); + uint32_t flags = furi_thread_flags_wait(ThreadEventStop, FuriFlagWaitAny | FuriFlagNoClear, 0); if(flags & FuriFlagError) { return; } @@ -102,7 +102,8 @@ static void js_exit_flag_poll(struct mjs* mjs) { } bool js_delay_with_flags(struct mjs* mjs, uint32_t time) { - uint32_t flags = furi_thread_flags_wait(ThreadEventStop, FuriFlagWaitAny, time); + uint32_t flags = + furi_thread_flags_wait(ThreadEventStop, FuriFlagWaitAny | FuriFlagNoClear, time); if(flags & FuriFlagError) { return false; } @@ -124,7 +125,7 @@ uint32_t js_flags_wait(struct mjs* mjs, uint32_t flags_mask, uint32_t timeout) { uint32_t flags = furi_thread_flags_get(); furi_check((flags & FuriFlagError) == 0); if(flags == 0) { - flags = furi_thread_flags_wait(flags_mask, FuriFlagWaitAny, timeout); + flags = furi_thread_flags_wait(flags_mask, FuriFlagWaitAny | FuriFlagNoClear, timeout); } else { uint32_t state = furi_thread_flags_clear(flags & flags_mask); furi_check((state & FuriFlagError) == 0); diff --git a/applications/system/js_app/modules/js_event_loop/js_event_loop.c b/applications/system/js_app/modules/js_event_loop/js_event_loop.c index 7f45c1a0f..625301ad1 100644 --- a/applications/system/js_app/modules/js_event_loop/js_event_loop.c +++ b/applications/system/js_app/modules/js_event_loop/js_event_loop.c @@ -12,6 +12,7 @@ * @brief Context passed to the generic event callback */ typedef struct { + FuriEventLoop* event_loop; JsEventLoopObjectType object_type; struct mjs* mjs; @@ -36,11 +37,6 @@ typedef struct { void* subscriptions; // SubscriptionArray_t, which we can't reference in this definition } JsEventLoopSubscription; -typedef struct { - FuriEventLoop* loop; - struct mjs* mjs; -} JsEventLoopTickContext; - ARRAY_DEF(SubscriptionArray, JsEventLoopSubscription*, M_PTR_OPLIST); //-V575 ARRAY_DEF(ContractArray, JsEventLoopContract*, M_PTR_OPLIST); //-V575 @@ -51,7 +47,6 @@ struct JsEventLoop { FuriEventLoop* loop; SubscriptionArray_t subscriptions; ContractArray_t owned_contracts; //mjs, &result, context->callback, @@ -68,6 +63,12 @@ static void js_event_loop_callback_generic(void* param) { context->arity, context->arguments); + bool is_error = strcmp(mjs_strerror(context->mjs, error), "NO_ERROR") != 0; + bool asked_to_stop = js_flags_wait(context->mjs, ThreadEventStop, 0) & ThreadEventStop; + if(is_error || asked_to_stop) { + furi_event_loop_stop(context->event_loop); + } + // save returned args for next call if(mjs_array_length(context->mjs, result) != context->arity - SYSTEM_ARGS) return; for(size_t i = 0; i < context->arity - SYSTEM_ARGS; i++) { @@ -111,11 +112,14 @@ static void js_event_loop_subscription_cancel(struct mjs* mjs) { JsEventLoopSubscription* subscription = JS_GET_CONTEXT(mjs); if(subscription->object_type == JsEventLoopObjectTypeTimer) { + // timer operations are deferred, which creates lifetime issues + // just stop the timer and let the cleanup routine free everything when the script is done furi_event_loop_timer_stop(subscription->object); - } else { - furi_event_loop_unsubscribe(subscription->loop, subscription->object); + return; } + furi_event_loop_unsubscribe(subscription->loop, subscription->object); + free(subscription->context->arguments); free(subscription->context); @@ -158,6 +162,7 @@ static void js_event_loop_subscribe(struct mjs* mjs) { mjs_set(mjs, subscription_obj, "cancel", ~0, MJS_MK_FN(js_event_loop_subscription_cancel)); // create callback context + context->event_loop = module->loop; context->object_type = contract->object_type; context->arity = mjs_nargs(mjs) - SYSTEM_ARGS + 2; context->arguments = calloc(context->arity, sizeof(mjs_val_t)); @@ -333,37 +338,22 @@ static void js_event_loop_queue(struct mjs* mjs) { mjs_return(mjs, queue); } -static void js_event_loop_tick(void* param) { - JsEventLoopTickContext* context = param; - uint32_t flags = furi_thread_flags_wait(ThreadEventStop, FuriFlagWaitAny | FuriFlagNoClear, 0); - if(flags & FuriFlagError) { - return; - } - if(flags & ThreadEventStop) { - furi_event_loop_stop(context->loop); - mjs_exit(context->mjs); - } -} - static void* js_event_loop_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) { UNUSED(modules); mjs_val_t event_loop_obj = mjs_mk_object(mjs); JsEventLoop* module = malloc(sizeof(JsEventLoop)); - JsEventLoopTickContext* tick_ctx = malloc(sizeof(JsEventLoopTickContext)); module->loop = furi_event_loop_alloc(); - tick_ctx->loop = module->loop; - tick_ctx->mjs = mjs; - module->tick_context = tick_ctx; - furi_event_loop_tick_set(module->loop, 10, js_event_loop_tick, tick_ctx); SubscriptionArray_init(module->subscriptions); ContractArray_init(module->owned_contracts); - mjs_set(mjs, event_loop_obj, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, module)); - mjs_set(mjs, event_loop_obj, "subscribe", ~0, MJS_MK_FN(js_event_loop_subscribe)); - mjs_set(mjs, event_loop_obj, "run", ~0, MJS_MK_FN(js_event_loop_run)); - mjs_set(mjs, event_loop_obj, "stop", ~0, MJS_MK_FN(js_event_loop_stop)); - mjs_set(mjs, event_loop_obj, "timer", ~0, MJS_MK_FN(js_event_loop_timer)); - mjs_set(mjs, event_loop_obj, "queue", ~0, MJS_MK_FN(js_event_loop_queue)); + JS_ASSIGN_MULTI(mjs, event_loop_obj) { + JS_FIELD(INST_PROP_NAME, mjs_mk_foreign(mjs, module)); + JS_FIELD("subscribe", MJS_MK_FN(js_event_loop_subscribe)); + JS_FIELD("run", MJS_MK_FN(js_event_loop_run)); + JS_FIELD("stop", MJS_MK_FN(js_event_loop_stop)); + JS_FIELD("timer", MJS_MK_FN(js_event_loop_timer)); + JS_FIELD("queue", MJS_MK_FN(js_event_loop_queue)); + } *object = event_loop_obj; return module; @@ -418,7 +408,6 @@ static void js_event_loop_destroy(void* inst) { ContractArray_clear(module->owned_contracts); furi_event_loop_free(module->loop); - free(module->tick_context); free(module); } } diff --git a/applications/system/js_app/modules/js_gpio.c b/applications/system/js_app/modules/js_gpio.c index ae3fefd71..23884a6d4 100644 --- a/applications/system/js_app/modules/js_gpio.c +++ b/applications/system/js_app/modules/js_gpio.c @@ -1,6 +1,7 @@ #include "../js_modules.h" // IWYU pragma: keep #include "./js_event_loop/js_event_loop.h" #include +#include #include #include #include @@ -17,6 +18,7 @@ typedef struct { FuriSemaphore* interrupt_semaphore; JsEventLoopContract* interrupt_contract; FuriHalAdcChannel adc_channel; + FuriHalPwmOutputId pwm_output; FuriHalAdcHandle* adc_handle; } JsGpioPinInst; @@ -231,6 +233,88 @@ static void js_gpio_read_analog(struct mjs* mjs) { mjs_return(mjs, mjs_mk_number(mjs, (double)millivolts)); } +/** + * @brief Determines whether this pin supports PWM + * + * Example usage: + * + * ```js + * let gpio = require("gpio"); + * assert_eq(true, gpio.get("pa4").isPwmSupported()); + * assert_eq(false, gpio.get("pa5").isPwmSupported()); + * ``` + */ +static void js_gpio_is_pwm_supported(struct mjs* mjs) { + JsGpioPinInst* manager_data = JS_GET_CONTEXT(mjs); + mjs_return(mjs, mjs_mk_boolean(mjs, manager_data->pwm_output != FuriHalPwmOutputIdNone)); +} + +/** + * @brief Sets PWM parameters and starts the PWM + * + * Example usage: + * + * ```js + * let gpio = require("gpio"); + * let pa4 = gpio.get("pa4"); + * pa4.pwmWrite(10000, 50); + * ``` + */ +static void js_gpio_pwm_write(struct mjs* mjs) { + JsGpioPinInst* manager_data = JS_GET_CONTEXT(mjs); + int32_t frequency, duty; + JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_INT32(&frequency), JS_ARG_INT32(&duty)); + if(manager_data->pwm_output == FuriHalPwmOutputIdNone) { + JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "PWM is not supported on this pin"); + } + + if(furi_hal_pwm_is_running(manager_data->pwm_output)) { + furi_hal_pwm_set_params(manager_data->pwm_output, frequency, duty); + } else { + furi_hal_pwm_start(manager_data->pwm_output, frequency, duty); + } +} + +/** + * @brief Determines whether PWM is running + * + * Example usage: + * + * ```js + * let gpio = require("gpio"); + * assert_eq(false, gpio.get("pa4").isPwmRunning()); + * ``` + */ +static void js_gpio_is_pwm_running(struct mjs* mjs) { + JsGpioPinInst* manager_data = JS_GET_CONTEXT(mjs); + if(manager_data->pwm_output == FuriHalPwmOutputIdNone) { + JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "PWM is not supported on this pin"); + } + + mjs_return(mjs, mjs_mk_boolean(mjs, furi_hal_pwm_is_running(manager_data->pwm_output))); +} + +/** + * @brief Stops PWM + * + * Example usage: + * + * ```js + * let gpio = require("gpio"); + * let pa4 = gpio.get("pa4"); + * pa4.pwmWrite(10000, 50); + * pa4.pwmStop(); + * ``` + */ +static void js_gpio_pwm_stop(struct mjs* mjs) { + JsGpioPinInst* manager_data = JS_GET_CONTEXT(mjs); + if(manager_data->pwm_output != FuriHalPwmOutputIdNone) { + JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "PWM is not supported on this pin"); + } + + furi_hal_pwm_stop(manager_data->pwm_output); +} + /** * @brief Returns an object that manages a specified pin. * @@ -269,12 +353,19 @@ static void js_gpio_get(struct mjs* mjs) { manager_data->interrupt_semaphore = furi_semaphore_alloc(UINT32_MAX, 0); manager_data->adc_handle = module->adc_handle; manager_data->adc_channel = pin_record->channel; - mjs_set(mjs, manager, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, manager_data)); - mjs_set(mjs, manager, "init", ~0, MJS_MK_FN(js_gpio_init)); - mjs_set(mjs, manager, "write", ~0, MJS_MK_FN(js_gpio_write)); - mjs_set(mjs, manager, "read", ~0, MJS_MK_FN(js_gpio_read)); - mjs_set(mjs, manager, "readAnalog", ~0, MJS_MK_FN(js_gpio_read_analog)); - mjs_set(mjs, manager, "interrupt", ~0, MJS_MK_FN(js_gpio_interrupt)); + manager_data->pwm_output = pin_record->pwm_output; + JS_ASSIGN_MULTI(mjs, manager) { + JS_FIELD(INST_PROP_NAME, mjs_mk_foreign(mjs, manager_data)); + JS_FIELD("init", MJS_MK_FN(js_gpio_init)); + JS_FIELD("write", MJS_MK_FN(js_gpio_write)); + JS_FIELD("read", MJS_MK_FN(js_gpio_read)); + JS_FIELD("readAnalog", MJS_MK_FN(js_gpio_read_analog)); + JS_FIELD("interrupt", MJS_MK_FN(js_gpio_interrupt)); + JS_FIELD("isPwmSupported", MJS_MK_FN(js_gpio_is_pwm_supported)); + JS_FIELD("pwmWrite", MJS_MK_FN(js_gpio_pwm_write)); + JS_FIELD("isPwmRunning", MJS_MK_FN(js_gpio_is_pwm_running)); + JS_FIELD("pwmStop", MJS_MK_FN(js_gpio_pwm_stop)); + } mjs_return(mjs, manager); // remember pin diff --git a/applications/system/js_app/modules/js_gui/icon.c b/applications/system/js_app/modules/js_gui/icon.c new file mode 100644 index 000000000..4b7926ba1 --- /dev/null +++ b/applications/system/js_app/modules/js_gui/icon.c @@ -0,0 +1,61 @@ +#include "../../js_modules.h" +#include + +typedef struct { + const char* name; + const Icon* data; +} IconDefinition; + +#define ICON_DEF(icon) \ + (IconDefinition) { \ + .name = #icon, .data = &I_##icon \ + } + +static const IconDefinition builtin_icons[] = { + ICON_DEF(DolphinWait_59x54), + ICON_DEF(js_script_10px), +}; + +static void js_gui_icon_get_builtin(struct mjs* mjs) { + const char* icon_name; + JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&icon_name)); + + for(size_t i = 0; i < COUNT_OF(builtin_icons); i++) { + if(strcmp(icon_name, builtin_icons[i].name) == 0) { + mjs_return(mjs, mjs_mk_foreign(mjs, (void*)builtin_icons[i].data)); + return; + } + } + + JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "no such built-in icon"); +} + +static void* js_gui_icon_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) { + UNUSED(modules); + *object = mjs_mk_object(mjs); + JS_ASSIGN_MULTI(mjs, *object) { + JS_FIELD("getBuiltin", MJS_MK_FN(js_gui_icon_get_builtin)); + } + return NULL; +} + +static void js_gui_icon_destroy(void* inst) { + UNUSED(inst); +} + +static const JsModuleDescriptor js_gui_icon_desc = { + "gui__icon", + js_gui_icon_create, + js_gui_icon_destroy, + NULL, +}; + +static const FlipperAppPluginDescriptor plugin_descriptor = { + .appid = PLUGIN_APP_ID, + .ep_api_version = PLUGIN_API_VERSION, + .entry_point = &js_gui_icon_desc, +}; + +const FlipperAppPluginDescriptor* js_gui_icon_ep(void) { + return &plugin_descriptor; +} diff --git a/applications/system/js_app/modules/js_gui/js_gui.c b/applications/system/js_app/modules/js_gui/js_gui.c index 22d04855d..e505681df 100644 --- a/applications/system/js_app/modules/js_gui/js_gui.c +++ b/applications/system/js_app/modules/js_gui/js_gui.c @@ -247,6 +247,22 @@ static bool return false; } +/** + * @brief Sets the list of children. Not available from JS. + */ +static bool + js_gui_view_internal_set_children(struct mjs* mjs, mjs_val_t children, JsGuiViewData* data) { + data->descriptor->reset_children(data->specific_view, data->custom_data); + + for(size_t i = 0; i < mjs_array_length(mjs, children); i++) { + mjs_val_t child = mjs_array_get(mjs, children, i); + if(!data->descriptor->add_child(mjs, data->specific_view, data->custom_data, child)) + return false; + } + + return true; +} + /** * @brief `View.set` */ @@ -260,6 +276,46 @@ static void js_gui_view_set(struct mjs* mjs) { mjs_return(mjs, MJS_UNDEFINED); } +/** + * @brief `View.addChild` + */ +static void js_gui_view_add_child(struct mjs* mjs) { + JsGuiViewData* data = JS_GET_CONTEXT(mjs); + if(!data->descriptor->add_child || !data->descriptor->reset_children) + JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "this View can't have children"); + + mjs_val_t child; + JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_ANY(&child)); + bool success = data->descriptor->add_child(mjs, data->specific_view, data->custom_data, child); + UNUSED(success); + mjs_return(mjs, MJS_UNDEFINED); +} + +/** + * @brief `View.resetChildren` + */ +static void js_gui_view_reset_children(struct mjs* mjs) { + JsGuiViewData* data = JS_GET_CONTEXT(mjs); + if(!data->descriptor->add_child || !data->descriptor->reset_children) + JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "this View can't have children"); + + data->descriptor->reset_children(data->specific_view, data->custom_data); + mjs_return(mjs, MJS_UNDEFINED); +} + +/** + * @brief `View.setChildren` + */ +static void js_gui_view_set_children(struct mjs* mjs) { + JsGuiViewData* data = JS_GET_CONTEXT(mjs); + if(!data->descriptor->add_child || !data->descriptor->reset_children) + JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "this View can't have children"); + + mjs_val_t children; + JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_ARR(&children)); + js_gui_view_internal_set_children(mjs, children, data); +} + /** * @brief `View` destructor */ @@ -283,7 +339,12 @@ static mjs_val_t js_gui_make_view(struct mjs* mjs, const JsViewDescriptor* descr // generic view API mjs_val_t view_obj = mjs_mk_object(mjs); - mjs_set(mjs, view_obj, "set", ~0, MJS_MK_FN(js_gui_view_set)); + JS_ASSIGN_MULTI(mjs, view_obj) { + JS_FIELD("set", MJS_MK_FN(js_gui_view_set)); + JS_FIELD("addChild", MJS_MK_FN(js_gui_view_add_child)); + JS_FIELD("resetChildren", MJS_MK_FN(js_gui_view_reset_children)); + JS_FIELD("setChildren", MJS_MK_FN(js_gui_view_set_children)); + } // object data JsGuiViewData* data = malloc(sizeof(JsGuiViewData)); @@ -314,7 +375,7 @@ static void js_gui_vf_make(struct mjs* mjs) { */ static void js_gui_vf_make_with(struct mjs* mjs) { mjs_val_t props; - JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_OBJ(&props)); + JS_FETCH_ARGS_OR_RETURN(mjs, JS_AT_LEAST, JS_ARG_OBJ(&props)); const JsViewDescriptor* descriptor = JS_GET_CONTEXT(mjs); // make the object like normal @@ -334,6 +395,18 @@ static void js_gui_vf_make_with(struct mjs* mjs) { } } + // assign children + if(mjs_nargs(mjs) >= 2) { + if(!data->descriptor->add_child || !data->descriptor->reset_children) + JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "this View can't have children"); + + mjs_val_t children = mjs_arg(mjs, 1); + if(!mjs_is_array(children)) + JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "argument 1: expected array"); + + if(!js_gui_view_internal_set_children(mjs, children, data)) return; + } + mjs_return(mjs, view_obj); } diff --git a/applications/system/js_app/modules/js_gui/js_gui.h b/applications/system/js_app/modules/js_gui/js_gui.h index d400d0a33..d9d98df39 100644 --- a/applications/system/js_app/modules/js_gui/js_gui.h +++ b/applications/system/js_app/modules/js_gui/js_gui.h @@ -50,6 +50,11 @@ typedef void (*JsViewFree)(void* specific_view); typedef void* (*JsViewCustomMake)(struct mjs* mjs, void* specific_view, mjs_val_t view_obj); /** @brief Context destruction for glue code */ typedef void (*JsViewCustomDestroy)(void* specific_view, void* custom_state, FuriEventLoop* loop); +/** @brief `addChild` callback for glue code */ +typedef bool ( + *JsViewAddChild)(struct mjs* mjs, void* specific_view, void* custom_state, mjs_val_t child_obj); +/** @brief `resetChildren` callback for glue code */ +typedef void (*JsViewResetChildren)(void* specific_view, void* custom_state); /** * @brief Descriptor for a JS view @@ -66,15 +71,22 @@ typedef struct { JsViewAlloc alloc; JsViewGetView get_view; JsViewFree free; + JsViewCustomMake custom_make; // get_view -> [custom_make (if set)] -> props[i].assign -> [custom_destroy (if_set)] -> free -// \_______________ creation ________________/ \___ usage ___/ \_________ destruction _________/ +// +-> add_child -+ +// +-> reset_children -+ +// alloc -> get_view -> custom_make -+-> props[i].assign -+> custom_destroy -> free +// \__________ creation __________/ \____ use ____/ \___ destruction ____/ /** * @brief Creates a JS `ViewFactory` object diff --git a/applications/system/js_app/modules/js_gui/widget.c b/applications/system/js_app/modules/js_gui/widget.c new file mode 100644 index 000000000..5f9a222cc --- /dev/null +++ b/applications/system/js_app/modules/js_gui/widget.c @@ -0,0 +1,281 @@ +#include "../../js_modules.h" // IWYU pragma: keep +#include "js_gui.h" +#include "../js_event_loop/js_event_loop.h" +#include + +typedef struct { + FuriMessageQueue* queue; + JsEventLoopContract contract; +} JsWidgetCtx; + +#define QUEUE_LEN 2 + +/** + * @brief Parses position (X and Y) from an element declaration object + */ +static bool element_get_position(struct mjs* mjs, mjs_val_t element, int32_t* x, int32_t* y) { + mjs_val_t x_in = mjs_get(mjs, element, "x", ~0); + mjs_val_t y_in = mjs_get(mjs, element, "y", ~0); + if(!mjs_is_number(x_in) || !mjs_is_number(y_in)) return false; + *x = mjs_get_int32(mjs, x_in); + *y = mjs_get_int32(mjs, y_in); + return true; +} + +/** + * @brief Parses size (W and h) from an element declaration object + */ +static bool element_get_size(struct mjs* mjs, mjs_val_t element, int32_t* w, int32_t* h) { + mjs_val_t w_in = mjs_get(mjs, element, "w", ~0); + mjs_val_t h_in = mjs_get(mjs, element, "h", ~0); + if(!mjs_is_number(w_in) || !mjs_is_number(h_in)) return false; + *w = mjs_get_int32(mjs, w_in); + *h = mjs_get_int32(mjs, h_in); + return true; +} + +/** + * @brief Parses alignment (V and H) from an element declaration object + */ +static bool + element_get_alignment(struct mjs* mjs, mjs_val_t element, Align* align_v, Align* align_h) { + mjs_val_t align_in = mjs_get(mjs, element, "align", ~0); + const char* align = mjs_get_string(mjs, &align_in, NULL); + if(!align) return false; + if(strlen(align) != 2) return false; + + if(align[0] == 't') { + *align_v = AlignTop; + } else if(align[0] == 'c') { + *align_v = AlignCenter; + } else if(align[0] == 'b') { + *align_v = AlignBottom; + } else { + return false; + } + + if(align[1] == 'l') { + *align_h = AlignLeft; + } else if(align[1] == 'm') { // m = middle + *align_h = AlignCenter; + } else if(align[1] == 'r') { + *align_h = AlignRight; + } else { + return false; + } + + return true; +} + +/** + * @brief Parses font from an element declaration object + */ +static bool element_get_font(struct mjs* mjs, mjs_val_t element, Font* font) { + mjs_val_t font_in = mjs_get(mjs, element, "font", ~0); + const char* font_str = mjs_get_string(mjs, &font_in, NULL); + if(!font_str) return false; + + if(strcmp(font_str, "primary") == 0) { + *font = FontPrimary; + } else if(strcmp(font_str, "secondary") == 0) { + *font = FontSecondary; + } else if(strcmp(font_str, "keyboard") == 0) { + *font = FontKeyboard; + } else if(strcmp(font_str, "big_numbers") == 0) { + *font = FontBigNumbers; + } else { + return false; + } + return true; +} + +/** + * @brief Parses text from an element declaration object + */ +static bool element_get_text(struct mjs* mjs, mjs_val_t element, mjs_val_t* text) { + *text = mjs_get(mjs, element, "text", ~0); + return mjs_is_string(*text); +} + +/** + * @brief Widget button element callback + */ +static void js_widget_button_callback(GuiButtonType result, InputType type, JsWidgetCtx* context) { + UNUSED(type); + furi_check(furi_message_queue_put(context->queue, &result, 0) == FuriStatusOk); +} + +#define DESTRUCTURE_OR_RETURN(mjs, child_obj, part, ...) \ + if(!element_get_##part(mjs, child_obj, __VA_ARGS__)) \ + JS_ERROR_AND_RETURN_VAL(mjs, MJS_BAD_ARGS_ERROR, false, "failed to fetch element " #part); + +static bool js_widget_add_child( + struct mjs* mjs, + Widget* widget, + JsWidgetCtx* context, + mjs_val_t child_obj) { + UNUSED(context); + if(!mjs_is_object(child_obj)) + JS_ERROR_AND_RETURN_VAL(mjs, MJS_BAD_ARGS_ERROR, false, "child must be an object"); + + mjs_val_t element_type_term = mjs_get(mjs, child_obj, "element", ~0); + const char* element_type = mjs_get_string(mjs, &element_type_term, NULL); + if(!element_type) + JS_ERROR_AND_RETURN_VAL( + mjs, MJS_BAD_ARGS_ERROR, false, "child object must have `element` property"); + + if((strcmp(element_type, "string") == 0) || (strcmp(element_type, "string_multiline") == 0)) { + int32_t x, y; + Align align_v, align_h; + Font font; + mjs_val_t text; + DESTRUCTURE_OR_RETURN(mjs, child_obj, position, &x, &y); + DESTRUCTURE_OR_RETURN(mjs, child_obj, alignment, &align_v, &align_h); + DESTRUCTURE_OR_RETURN(mjs, child_obj, font, &font); + DESTRUCTURE_OR_RETURN(mjs, child_obj, text, &text); + if(strcmp(element_type, "string") == 0) { + widget_add_string_element( + widget, x, y, align_h, align_v, font, mjs_get_string(mjs, &text, NULL)); + } else { + widget_add_string_multiline_element( + widget, x, y, align_h, align_v, font, mjs_get_string(mjs, &text, NULL)); + } + + } else if(strcmp(element_type, "text_box") == 0) { + int32_t x, y, w, h; + Align align_v, align_h; + Font font; + mjs_val_t text; + DESTRUCTURE_OR_RETURN(mjs, child_obj, position, &x, &y); + DESTRUCTURE_OR_RETURN(mjs, child_obj, size, &w, &h); + DESTRUCTURE_OR_RETURN(mjs, child_obj, alignment, &align_v, &align_h); + DESTRUCTURE_OR_RETURN(mjs, child_obj, font, &font); + DESTRUCTURE_OR_RETURN(mjs, child_obj, text, &text); + mjs_val_t strip_to_dots_in = mjs_get(mjs, child_obj, "stripToDots", ~0); + if(!mjs_is_boolean(strip_to_dots_in)) + JS_ERROR_AND_RETURN_VAL( + mjs, MJS_BAD_ARGS_ERROR, false, "failed to fetch element stripToDots"); + bool strip_to_dots = mjs_get_bool(mjs, strip_to_dots_in); + widget_add_text_box_element( + widget, x, y, w, h, align_h, align_v, mjs_get_string(mjs, &text, NULL), strip_to_dots); + + } else if(strcmp(element_type, "text_scroll") == 0) { + int32_t x, y, w, h; + mjs_val_t text; + DESTRUCTURE_OR_RETURN(mjs, child_obj, position, &x, &y); + DESTRUCTURE_OR_RETURN(mjs, child_obj, size, &w, &h); + DESTRUCTURE_OR_RETURN(mjs, child_obj, text, &text); + widget_add_text_scroll_element(widget, x, y, w, h, mjs_get_string(mjs, &text, NULL)); + + } else if(strcmp(element_type, "button") == 0) { + mjs_val_t btn_in = mjs_get(mjs, child_obj, "button", ~0); + const char* btn_name = mjs_get_string(mjs, &btn_in, NULL); + if(!btn_name) + JS_ERROR_AND_RETURN_VAL( + mjs, MJS_BAD_ARGS_ERROR, false, "failed to fetch element button"); + GuiButtonType btn_type; + if(strcmp(btn_name, "left") == 0) { + btn_type = GuiButtonTypeLeft; + } else if(strcmp(btn_name, "center") == 0) { + btn_type = GuiButtonTypeCenter; + } else if(strcmp(btn_name, "right") == 0) { + btn_type = GuiButtonTypeRight; + } else { + JS_ERROR_AND_RETURN_VAL(mjs, MJS_BAD_ARGS_ERROR, false, "incorrect button type"); + } + mjs_val_t text; + DESTRUCTURE_OR_RETURN(mjs, child_obj, text, &text); + widget_add_button_element( + widget, + btn_type, + mjs_get_string(mjs, &text, NULL), + (ButtonCallback)js_widget_button_callback, + context); + + } else if(strcmp(element_type, "icon") == 0) { + int32_t x, y; + DESTRUCTURE_OR_RETURN(mjs, child_obj, position, &x, &y); + mjs_val_t icon_data_in = mjs_get(mjs, child_obj, "iconData", ~0); + if(!mjs_is_foreign(icon_data_in)) + JS_ERROR_AND_RETURN_VAL( + mjs, MJS_BAD_ARGS_ERROR, false, "failed to fetch element iconData"); + const Icon* icon = mjs_get_ptr(mjs, icon_data_in); + widget_add_icon_element(widget, x, y, icon); + + } else if(strcmp(element_type, "frame") == 0) { + int32_t x, y, w, h; + DESTRUCTURE_OR_RETURN(mjs, child_obj, position, &x, &y); + DESTRUCTURE_OR_RETURN(mjs, child_obj, size, &w, &h); + mjs_val_t radius_in = mjs_get(mjs, child_obj, "radius", ~0); + if(!mjs_is_number(radius_in)) + JS_ERROR_AND_RETURN_VAL( + mjs, MJS_BAD_ARGS_ERROR, false, "failed to fetch element radius"); + int32_t radius = mjs_get_int32(mjs, radius_in); + widget_add_frame_element(widget, x, y, w, h, radius); + } + + return true; +} + +static void js_widget_reset_children(Widget* widget, void* state) { + UNUSED(state); + widget_reset(widget); +} + +static mjs_val_t js_widget_button_event_transformer( + struct mjs* mjs, + FuriMessageQueue* queue, + JsWidgetCtx* context) { + UNUSED(context); + GuiButtonType btn_type; + furi_check(furi_message_queue_get(queue, &btn_type, 0) == FuriStatusOk); + const char* btn_name; + if(btn_type == GuiButtonTypeLeft) { + btn_name = "left"; + } else if(btn_type == GuiButtonTypeCenter) { + btn_name = "center"; + } else if(btn_type == GuiButtonTypeRight) { + btn_name = "right"; + } else { + furi_crash(); + } + return mjs_mk_string(mjs, btn_name, ~0, false); +} + +static void* js_widget_custom_make(struct mjs* mjs, Widget* widget, mjs_val_t view_obj) { + UNUSED(widget); + JsWidgetCtx* context = malloc(sizeof(JsWidgetCtx)); + context->queue = furi_message_queue_alloc(QUEUE_LEN, sizeof(GuiButtonType)); + context->contract = (JsEventLoopContract){ + .magic = JsForeignMagic_JsEventLoopContract, + .object_type = JsEventLoopObjectTypeQueue, + .object = context->queue, + .non_timer = + { + .event = FuriEventLoopEventIn, + .transformer = (JsEventLoopTransformer)js_widget_button_event_transformer, + }, + }; + mjs_set(mjs, view_obj, "button", ~0, mjs_mk_foreign(mjs, &context->contract)); + return context; +} + +static void js_widget_custom_destroy(Widget* widget, JsWidgetCtx* context, FuriEventLoop* loop) { + UNUSED(widget); + furi_event_loop_maybe_unsubscribe(loop, context->queue); + furi_message_queue_free(context->queue); + free(context); +} + +static const JsViewDescriptor view_descriptor = { + .alloc = (JsViewAlloc)widget_alloc, + .free = (JsViewFree)widget_free, + .get_view = (JsViewGetView)widget_get_view, + .custom_make = (JsViewCustomMake)js_widget_custom_make, + .custom_destroy = (JsViewCustomDestroy)js_widget_custom_destroy, + .add_child = (JsViewAddChild)js_widget_add_child, + .reset_children = (JsViewResetChildren)js_widget_reset_children, + .prop_cnt = 0, + .props = {}, +}; +JS_GUI_VIEW_DEF(widget, &view_descriptor); diff --git a/applications/system/js_app/packages/fz-sdk/gpio/index.d.ts b/applications/system/js_app/packages/fz-sdk/gpio/index.d.ts index b484ebbf6..cd5ce2b60 100644 --- a/applications/system/js_app/packages/fz-sdk/gpio/index.d.ts +++ b/applications/system/js_app/packages/fz-sdk/gpio/index.d.ts @@ -75,6 +75,34 @@ export interface Pin { * @version Added in JS SDK 0.1 */ interrupt(): Contract; + /** + * Determines whether this pin supports PWM. If `false`, all other + * PWM-related methods on this pin will throw an error when called. + * @note On Flipper Zero only pins PA4 and PA7 support PWM + * @version Added in JS SDK 0.2, extra feature `"gpio-pwm"` + */ + isPwmSupported(): boolean; + /** + * Sets PWM parameters and starts the PWM. Configures the pin with + * `{ direction: "out", outMode: "push_pull" }`. Throws an error if PWM is + * not supported on this pin. + * @param freq Frequency in Hz + * @param duty Duty cycle in % + * @version Added in JS SDK 0.2, extra feature `"gpio-pwm"` + */ + pwmWrite(freq: number, duty: number): void; + /** + * Determines whether PWM is running. Throws an error if PWM is not + * supported on this pin. + * @version Added in JS SDK 0.2, extra feature `"gpio-pwm"` + */ + isPwmRunning(): boolean; + /** + * Stops PWM. Does not restore previous pin configuration. Throws an error + * if PWM is not supported on this pin. + * @version Added in JS SDK 0.2, extra feature `"gpio-pwm"` + */ + pwmStop(): void; } /** diff --git a/applications/system/js_app/packages/fz-sdk/gui/byte_input.d.ts b/applications/system/js_app/packages/fz-sdk/gui/byte_input.d.ts index 5556e7fbb..7080ad3ae 100644 --- a/applications/system/js_app/packages/fz-sdk/gui/byte_input.d.ts +++ b/applications/system/js_app/packages/fz-sdk/gui/byte_input.d.ts @@ -33,9 +33,10 @@ type Props = { length: number, defaultData: Uint8Array | ArrayBuffer, } -declare class ByteInput extends View { +type Child = never; +declare class ByteInput extends View { input: Contract; } -declare class ByteInputFactory extends ViewFactory { } +declare class ByteInputFactory extends ViewFactory { } declare const factory: ByteInputFactory; export = factory; diff --git a/applications/system/js_app/packages/fz-sdk/gui/dialog.d.ts b/applications/system/js_app/packages/fz-sdk/gui/dialog.d.ts index 9bd0c3966..2fffcb873 100644 --- a/applications/system/js_app/packages/fz-sdk/gui/dialog.d.ts +++ b/applications/system/js_app/packages/fz-sdk/gui/dialog.d.ts @@ -37,9 +37,10 @@ type Props = { center: string, right: string, } -declare class Dialog extends View { +type Child = never; +declare class Dialog extends View { input: Contract<"left" | "center" | "right">; } -declare class DialogFactory extends ViewFactory { } +declare class DialogFactory extends ViewFactory { } declare const factory: DialogFactory; export = factory; diff --git a/applications/system/js_app/packages/fz-sdk/gui/empty_screen.d.ts b/applications/system/js_app/packages/fz-sdk/gui/empty_screen.d.ts index 49e591426..6a848bd03 100644 --- a/applications/system/js_app/packages/fz-sdk/gui/empty_screen.d.ts +++ b/applications/system/js_app/packages/fz-sdk/gui/empty_screen.d.ts @@ -26,7 +26,8 @@ import type { View, ViewFactory } from "."; type Props = {}; -declare class EmptyScreen extends View { } -declare class EmptyScreenFactory extends ViewFactory { } +type Child = never; +declare class EmptyScreen extends View { } +declare class EmptyScreenFactory extends ViewFactory { } declare const factory: EmptyScreenFactory; export = factory; diff --git a/applications/system/js_app/packages/fz-sdk/gui/icon.d.ts b/applications/system/js_app/packages/fz-sdk/gui/icon.d.ts new file mode 100644 index 000000000..577e1e6a9 --- /dev/null +++ b/applications/system/js_app/packages/fz-sdk/gui/icon.d.ts @@ -0,0 +1,11 @@ +export type BuiltinIcon = "DolphinWait_59x54" | "js_script_10px"; + +export type IconData = symbol & { "__tag__": "icon" }; +// introducing a nominal type in a hacky way; the `__tag__` property doesn't really exist. + +/** + * Gets a built-in firmware icon for use in GUI + * @param icon Name of the icon + * @version Added in JS SDK 0.2, extra feature `"gui-widget"` + */ +export declare function getBuiltin(icon: BuiltinIcon): IconData; diff --git a/applications/system/js_app/packages/fz-sdk/gui/index.d.ts b/applications/system/js_app/packages/fz-sdk/gui/index.d.ts index 93a6846c2..969b6934e 100644 --- a/applications/system/js_app/packages/fz-sdk/gui/index.d.ts +++ b/applications/system/js_app/packages/fz-sdk/gui/index.d.ts @@ -26,23 +26,23 @@ * assumes control over the entire viewport and all input events. Different * types of views are available (not all of which are unfortunately currently * implemented in JS): - * | View | Has JS adapter? | - * |----------------------|------------------| - * | `button_menu` | ❌ | - * | `button_panel` | ❌ | - * | `byte_input` | ✅ | - * | `dialog_ex` | ✅ (as `dialog`) | - * | `empty_screen` | ✅ | - * | `file_browser` | ❌ | - * | `loading` | ✅ | - * | `menu` | ❌ | - * | `number_input` | ❌ | - * | `popup` | ❌ | - * | `submenu` | ✅ | - * | `text_box` | ✅ | - * | `text_input` | ✅ | - * | `variable_item_list` | ❌ | - * | `widget` | ❌ | + * | View | Has JS adapter? | + * |----------------------|-----------------------| + * | `button_menu` | ❌ | + * | `button_panel` | ❌ | + * | `byte_input` | ✅ | + * | `dialog_ex` | ✅ (as `dialog`) | + * | `empty_screen` | ✅ | + * | `file_browser` | ✅ (as `file_picker`) | + * | `loading` | ✅ | + * | `menu` | ❌ | + * | `number_input` | ❌ | + * | `popup` | ❌ | + * | `submenu` | ✅ | + * | `text_box` | ✅ | + * | `text_input` | ✅ | + * | `variable_item_list` | ❌ | + * | `widget` | ✅ | * * In JS, each view has its own set of properties (or just "props"). The * programmer can manipulate these properties in two ways: @@ -121,7 +121,7 @@ import type { Contract } from "../event_loop"; type Properties = { [K: string]: any }; -export declare class View { +export declare class View { /** * Assign value to property by name * @param property Name of the property @@ -129,9 +129,26 @@ export declare class View { * @version Added in JS SDK 0.1 */ set

(property: P, value: Props[P]): void; + /** + * Adds a child to the View + * @param child Child to add + * @version Added in JS SDK 0.2, extra feature `"gui-widget"` + */ + addChild(child: C): void; + /** + * Removes all children from the View + * @version Added in JS SDK 0.2, extra feature `"gui-widget"` + */ + resetChildren(): void; + /** + * Removes all previous children from the View and assigns new children + * @param children The list of children to assign + * @version Added in JS SDK 0.2, extra feature `"gui-widget"` + */ + setChildren(children: Child[]): void; } -export declare class ViewFactory> { +export declare class ViewFactory> { /** * Create view instance with default values, can be changed later with set() * @version Added in JS SDK 0.1 @@ -140,9 +157,10 @@ export declare class ViewFactory /** * Create view instance with custom values, can be changed later with set() * @param initial Dictionary of property names to values - * @version Added in JS SDK 0.1 + * @param children Optional list of children to add to the view + * @version Added in JS SDK 0.1; amended in JS SDK 0.2, extra feature `"gui-widget"` */ - makeWith(initial: Partial): V; + makeWith(initial: Partial, children?: Child[]): V; } /** @@ -163,7 +181,7 @@ declare class ViewDispatcher { * View object currently shown * @version Added in JS SDK 0.1 */ - currentView: View; + currentView: View; /** * Sends a number to the custom event handler * @param event number to send @@ -175,7 +193,7 @@ declare class ViewDispatcher { * @param assoc View-ViewDispatcher association as returned by `add` * @version Added in JS SDK 0.1 */ - switchTo(assoc: View): void; + switchTo(assoc: View): void; /** * Sends this ViewDispatcher to the front or back, above or below all other * GUI viewports diff --git a/applications/system/js_app/packages/fz-sdk/gui/loading.d.ts b/applications/system/js_app/packages/fz-sdk/gui/loading.d.ts index b8b10c43a..d636f21ca 100644 --- a/applications/system/js_app/packages/fz-sdk/gui/loading.d.ts +++ b/applications/system/js_app/packages/fz-sdk/gui/loading.d.ts @@ -27,7 +27,8 @@ import type { View, ViewFactory } from "."; type Props = {}; -declare class Loading extends View { } -declare class LoadingFactory extends ViewFactory { } +type Child = never; +declare class Loading extends View { } +declare class LoadingFactory extends ViewFactory { } declare const factory: LoadingFactory; export = factory; diff --git a/applications/system/js_app/packages/fz-sdk/gui/submenu.d.ts b/applications/system/js_app/packages/fz-sdk/gui/submenu.d.ts index 31e08aab8..e73856bee 100644 --- a/applications/system/js_app/packages/fz-sdk/gui/submenu.d.ts +++ b/applications/system/js_app/packages/fz-sdk/gui/submenu.d.ts @@ -31,9 +31,10 @@ type Props = { header: string, items: string[], }; -declare class Submenu extends View { +type Child = never; +declare class Submenu extends View { chosen: Contract; } -declare class SubmenuFactory extends ViewFactory { } +declare class SubmenuFactory extends ViewFactory { } declare const factory: SubmenuFactory; export = factory; diff --git a/applications/system/js_app/packages/fz-sdk/gui/text_box.d.ts b/applications/system/js_app/packages/fz-sdk/gui/text_box.d.ts index a46ec73fa..32003bd95 100644 --- a/applications/system/js_app/packages/fz-sdk/gui/text_box.d.ts +++ b/applications/system/js_app/packages/fz-sdk/gui/text_box.d.ts @@ -33,9 +33,10 @@ type Props = { font: "text" | "hex", focus: "start" | "end", } -declare class TextBox extends View { +type Child = never; +declare class TextBox extends View { chosen: Contract; } -declare class TextBoxFactory extends ViewFactory { } +declare class TextBoxFactory extends ViewFactory { } declare const factory: TextBoxFactory; export = factory; diff --git a/applications/system/js_app/packages/fz-sdk/gui/text_input.d.ts b/applications/system/js_app/packages/fz-sdk/gui/text_input.d.ts index 5d64b038b..9d0d180ba 100644 --- a/applications/system/js_app/packages/fz-sdk/gui/text_input.d.ts +++ b/applications/system/js_app/packages/fz-sdk/gui/text_input.d.ts @@ -37,9 +37,10 @@ type Props = { defaultText: string, defaultTextClear: boolean, } -declare class TextInput extends View { +type Child = never; +declare class TextInput extends View { input: Contract; } -declare class TextInputFactory extends ViewFactory { } +declare class TextInputFactory extends ViewFactory { } declare const factory: TextInputFactory; export = factory; diff --git a/applications/system/js_app/packages/fz-sdk/gui/widget.d.ts b/applications/system/js_app/packages/fz-sdk/gui/widget.d.ts new file mode 100644 index 000000000..b37af8e3c --- /dev/null +++ b/applications/system/js_app/packages/fz-sdk/gui/widget.d.ts @@ -0,0 +1,66 @@ +/** + * Displays a combination of custom elements on one screen. + * + * Sample screenshot of the view + * + * ```js + * let eventLoop = require("event_loop"); + * let gui = require("gui"); + * let emptyView = require("gui/widget"); + * ``` + * + * This module depends on the `gui` module, which in turn depends on the + * `event_loop` module, so they _must_ be imported in this order. It is also + * recommended to conceptualize these modules first before using this one. + * + * # Example + * For an example refer to the GUI example. + * + * # View props + * This view does not have any props. + * + * # Children + * This view has the elements as its children. + * + * @version Added in JS SDK 0.2, extra feature `"gui-widget"` + * @module + */ + +import type { View, ViewFactory } from "."; +import type { IconData } from "./icon"; +import type { Contract } from "../event_loop"; + +type Position = { x: number, y: number }; +type Size = { w: number, h: number }; +type Alignment = { align: `${"t" | "c" | "b"}${"l" | "m" | "r"}` }; +type Font = { font: "primary" | "secondary" | "keyboard" | "big_numbers" }; +type Text = { text: string }; + +type StringMultilineElement = { element: "string_multiline" } & Position & Alignment & Font & Text; +type StringElement = { element: "string" } & Position & Alignment & Font & Text; +type TextBoxElement = { element: "text_box", stripToDots: boolean } & Position & Size & Alignment & Text; +type TextScrollElement = { element: "text_scroll" } & Position & Size & Text; +type ButtonElement = { element: "button", button: "left" | "center" | "right" } & Text; +type IconElement = { element: "icon", iconData: IconData } & Position; +type FrameElement = { element: "frame", radius: number } & Position & Size; + +type Element = StringMultilineElement + | StringElement + | TextBoxElement + | TextScrollElement + | ButtonElement + | IconElement + | FrameElement; + +type Props = {}; +type Child = Element; +declare class Widget extends View { + /** + * Event source for buttons. Only gets fired if there's a corresponding + * button element. + */ + button: Contract<"left" | "center" | "right">; +} +declare class WidgetFactory extends ViewFactory { } +declare const factory: WidgetFactory; +export = factory; diff --git a/applications/system/js_app/packages/fz-sdk/package.json b/applications/system/js_app/packages/fz-sdk/package.json index f500fae2b..523845738 100644 --- a/applications/system/js_app/packages/fz-sdk/package.json +++ b/applications/system/js_app/packages/fz-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@flipperdevices/fz-sdk", - "version": "0.1.3", + "version": "0.2.0", "description": "Type declarations and documentation for native JS modules available on Flipper Zero", "keywords": [ "flipper", diff --git a/documentation/images/widget.png b/documentation/images/widget.png new file mode 100644 index 0000000000000000000000000000000000000000..f4dd1ed5beec5c28242adedc09808bc9c650e29a GIT binary patch literal 2390 zcmbVOeN+=y7Jo5TgssNYU8_h*-If*?&MF#kfe>*0LckTU#7Gd(r4kLLSlRHA6cYEW zJ?mwjE%aFxO9 z{oHG7zhVhT5IN>Gkc_Obc z`!yg~NaSrnrJotLR$x9IQFuMiP#TtHDW=h$Va&PODhhz6%n{~uM9i7cp9p)_b*>*; zr9QdlHRX5|Xs=nwQ(K_<1Dj8ybG^k_Ln;`z^e8UuH2iHknpdLNaTek5G*z2xj>M1B z6h`;I=_cu>^MRXlr3H86pJ-&g^Zs#Kj?5NgqYw0Ax81yGdzE@)zmDPJlocc46y+n> zFP(D4VrFoUY0S6pMQbRkgHeYIJ(#^Ni$u09Jba{AartEhd?>A%K z18EyxI#}3_f`gR2oIG8k4*p(zqJ+4l-f5(^q-xF~@(Vs16D=EKp7;!By`_k~EI%Ie zC4I;BTjVe!bs4C4n}{L!myTG0u5yQF((tWLA8J+vqLsN*9a45F3h(61zqJ`59){4f z(_M^nX2$dh3mNNg@yOhZ#8SPGxJR=y5yp)ctL3xB)#-VXr7=dr3)#c3G2y>vz(2e> zZQKP{ll2sxs$(DFyALtpW?AWbNVPv~I@2$rOIQ`^C)e~JV|duXUMgq13~KmlWaqss z%*M}bKpT=8AGgv)ItS;eS2qa*Fy#bcJcX=x1ScFvr^=XUr3H~c$@q{^j_1ASNglUW zyLDz2!{!%d@-2Xz#a;OW(w+`c$QS89s4Fq}BSl^z@fX=YPeV>|$!}Pb@@Bg9F5(E2 zSWaW$G}u9qc8blsTCXS6BooH2DM;E<(edt`U=d>rrrE8stnC8S7B6n2LajIP@3fOu6+vH|@8~OT4~gBPk;mIvgG|WjPbJfIeGiUK7!q3CAC7(?@aaCklJ)L{2y6CA zk~H)+`XI-msf&Ka_TsrR&9$*MnZx;`omJ#>1q)&f#-)hQDiXw>*)l9Ktot=g?)$nv z(xWS^is@~C;Qs=^dBS&%`Yul0&GQsCN_WZrV_qop(G)7hA$-0`^RQmVh-#(!X~d0R?K0J~Ie+ z^ZARXQlw|*6{~3FnYmG!WefdE?{57o? z^6GR6+44?TV{+K=ux4va#1~XHJ3TQ&$~1MbYniq+=@eS4$~B2qg;rI^Dh<+F3#2<} z;=ozC@8Z5IbhM$TDCiwQDaJmma}ac}Gcc7E&ES5TxUO?#*OOojI`t z=Bjc3Yo5YfHee12=wS|D7I*WV5-PS`v9Ig&i6ys7w2QddRA zm276SL{M|Hv8O`S*pSR}CFguB*;TLFZK}rLjVGMENrn1{5tMJrEGfKjn6?<_jouU5 zu&uyd%IfF4B*z$|Gc^uK + +```js +let eventLoop = require("event_loop"); +let gui = require("gui"); +let widgetView = require("gui/widget"); +``` + +This module depends on the `gui` module, which in turn depends on the +`event_loop` module, so they _must_ be imported in this order. It is also +recommended to conceptualize these modules first before using this one. + +# Example +For an example refer to the `gui.js` example script. + +# View props +This view does not have any props. + +# Children +This view has the elements as its children. diff --git a/lib/mjs/mjs_exec.c b/lib/mjs/mjs_exec.c index 8fdb2d7e5..273c38a34 100644 --- a/lib/mjs/mjs_exec.c +++ b/lib/mjs/mjs_exec.c @@ -584,7 +584,8 @@ static void mjs_apply_(struct mjs* mjs) { if(mjs_is_array(v)) { nargs = mjs_array_length(mjs, v); args = calloc(nargs, sizeof(args[0])); - for(i = 0; i < nargs; i++) args[i] = mjs_array_get(mjs, v, i); + for(i = 0; i < nargs; i++) + args[i] = mjs_array_get(mjs, v, i); } mjs_apply(mjs, &res, func, mjs_arg(mjs, 0), nargs, args); free(args); diff --git a/targets/f18/furi_hal/furi_hal_resources.h b/targets/f18/furi_hal/furi_hal_resources.h index 9a0d04cb6..23a88c215 100644 --- a/targets/f18/furi_hal/furi_hal_resources.h +++ b/targets/f18/furi_hal/furi_hal_resources.h @@ -2,6 +2,7 @@ #include #include +#include #ifdef __cplusplus extern "C" { @@ -40,6 +41,7 @@ typedef struct { const GpioPin* pin; const char* name; const FuriHalAdcChannel channel; + const FuriHalPwmOutputId pwm_output; const uint8_t number; const bool debug; } GpioPinRecord; diff --git a/targets/f7/furi_hal/furi_hal_pwm.h b/targets/f7/furi_hal/furi_hal_pwm.h index 16acca05e..30a5467d2 100644 --- a/targets/f7/furi_hal/furi_hal_pwm.h +++ b/targets/f7/furi_hal/furi_hal_pwm.h @@ -12,6 +12,7 @@ extern "C" { #include typedef enum { + FuriHalPwmOutputIdNone, FuriHalPwmOutputIdTim1PA7, FuriHalPwmOutputIdLptim2PA4, } FuriHalPwmOutputId; diff --git a/targets/f7/furi_hal/furi_hal_resources.c b/targets/f7/furi_hal/furi_hal_resources.c index 123ebc420..a6ba0b083 100644 --- a/targets/f7/furi_hal/furi_hal_resources.c +++ b/targets/f7/furi_hal/furi_hal_resources.c @@ -73,6 +73,7 @@ const GpioPinRecord gpio_pins[] = { {.pin = &gpio_ext_pa7, .name = "PA7", .channel = FuriHalAdcChannel12, + .pwm_output = FuriHalPwmOutputIdTim1PA7, .number = 2, .debug = false}, {.pin = &gpio_ext_pa6, @@ -83,6 +84,7 @@ const GpioPinRecord gpio_pins[] = { {.pin = &gpio_ext_pa4, .name = "PA4", .channel = FuriHalAdcChannel9, + .pwm_output = FuriHalPwmOutputIdLptim2PA4, .number = 4, .debug = false}, {.pin = &gpio_ext_pb3, diff --git a/targets/f7/furi_hal/furi_hal_resources.h b/targets/f7/furi_hal/furi_hal_resources.h index ec8794cc1..3e8501b86 100644 --- a/targets/f7/furi_hal/furi_hal_resources.h +++ b/targets/f7/furi_hal/furi_hal_resources.h @@ -2,6 +2,7 @@ #include #include +#include #ifdef __cplusplus extern "C" { @@ -40,6 +41,7 @@ typedef struct { const GpioPin* pin; const char* name; const FuriHalAdcChannel channel; + const FuriHalPwmOutputId pwm_output; const uint8_t number; const bool debug; } GpioPinRecord; From de85cc7a8e47b4ecb8e6f8754d891b1f07cddb78 Mon Sep 17 00:00:00 2001 From: RebornedBrain <138568282+RebornedBrain@users.noreply.github.com> Date: Thu, 13 Feb 2025 13:07:03 +0300 Subject: [PATCH 032/268] ST25TB poller mode check (#4084) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: あく --- lib/nfc/protocols/st25tb/st25tb_poller.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/nfc/protocols/st25tb/st25tb_poller.c b/lib/nfc/protocols/st25tb/st25tb_poller.c index fd6dc4f09..674a439cd 100644 --- a/lib/nfc/protocols/st25tb/st25tb_poller.c +++ b/lib/nfc/protocols/st25tb/st25tb_poller.c @@ -88,7 +88,7 @@ static NfcCommand st25tb_poller_request_mode_handler(St25tbPoller* instance) { St25tbPollerEventDataModeRequest* mode_request_data = &instance->st25tb_event_data.mode_request; - furi_assert(mode_request_data->mode < St25tbPollerModeNum); + furi_check(mode_request_data->mode < St25tbPollerModeNum); if(mode_request_data->mode == St25tbPollerModeRead) { instance->state = St25tbPollerStateRead; From 59fe896ce8bcec4e5255f242cb6e468c926462b5 Mon Sep 17 00:00:00 2001 From: Yukai Li Date: Thu, 13 Feb 2025 03:31:56 -0700 Subject: [PATCH 033/268] nfc: Enable MFUL sync poller to be provided with passwords (#4050) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * nfc: Enable MFUL sync poller to be provided with passwords * Sync targret api versions Co-authored-by: あく --- .../debug/unit_tests/tests/nfc/nfc_test.c | 8 +++---- .../mf_ultralight/mf_ultralight_poller_sync.c | 24 +++++++++++++------ .../mf_ultralight/mf_ultralight_poller_sync.h | 6 ++++- targets/f18/api_symbols.csv | 2 +- targets/f7/api_symbols.csv | 4 ++-- 5 files changed, 29 insertions(+), 15 deletions(-) diff --git a/applications/debug/unit_tests/tests/nfc/nfc_test.c b/applications/debug/unit_tests/tests/nfc/nfc_test.c index 4ba934b6d..e028b8041 100644 --- a/applications/debug/unit_tests/tests/nfc/nfc_test.c +++ b/applications/debug/unit_tests/tests/nfc/nfc_test.c @@ -262,7 +262,7 @@ static void mf_ultralight_reader_test(const char* path) { nfc_listener_start(mfu_listener, NULL, NULL); MfUltralightData* mfu_data = mf_ultralight_alloc(); - MfUltralightError error = mf_ultralight_poller_sync_read_card(poller, mfu_data); + MfUltralightError error = mf_ultralight_poller_sync_read_card(poller, mfu_data, NULL); mu_assert(error == MfUltralightErrorNone, "mf_ultralight_poller_sync_read_card() failed"); nfc_listener_stop(mfu_listener); @@ -315,7 +315,7 @@ MU_TEST(ntag_213_locked_reader) { nfc_listener_start(mfu_listener, NULL, NULL); MfUltralightData* mfu_data = mf_ultralight_alloc(); - MfUltralightError error = mf_ultralight_poller_sync_read_card(poller, mfu_data); + MfUltralightError error = mf_ultralight_poller_sync_read_card(poller, mfu_data, NULL); mu_assert(error == MfUltralightErrorNone, "mf_ultralight_poller_sync_read_card() failed"); nfc_listener_stop(mfu_listener); @@ -353,7 +353,7 @@ static void mf_ultralight_write(void) { MfUltralightData* mfu_data = mf_ultralight_alloc(); // Initial read - MfUltralightError error = mf_ultralight_poller_sync_read_card(poller, mfu_data); + MfUltralightError error = mf_ultralight_poller_sync_read_card(poller, mfu_data, NULL); mu_assert(error == MfUltralightErrorNone, "mf_ultralight_poller_sync_read_card() failed"); mu_assert( @@ -371,7 +371,7 @@ static void mf_ultralight_write(void) { } // Verification read - error = mf_ultralight_poller_sync_read_card(poller, mfu_data); + error = mf_ultralight_poller_sync_read_card(poller, mfu_data, NULL); mu_assert(error == MfUltralightErrorNone, "mf_ultralight_poller_sync_read_card() failed"); nfc_listener_stop(mfu_listener); diff --git a/lib/nfc/protocols/mf_ultralight/mf_ultralight_poller_sync.c b/lib/nfc/protocols/mf_ultralight/mf_ultralight_poller_sync.c index 9958dc50d..252c46399 100644 --- a/lib/nfc/protocols/mf_ultralight/mf_ultralight_poller_sync.c +++ b/lib/nfc/protocols/mf_ultralight/mf_ultralight_poller_sync.c @@ -23,6 +23,7 @@ typedef struct { FuriThreadId thread_id; MfUltralightError error; MfUltralightPollerContextData data; + const MfUltralightPollerAuthContext* auth_context; } MfUltralightPollerContext; typedef MfUltralightError (*MfUltralightPollerCmdHandler)( @@ -250,12 +251,17 @@ static NfcCommand mf_ultralight_poller_read_callback(NfcGenericEvent event, void poller_context->error = mfu_event->data->error; command = NfcCommandStop; } else if(mfu_event->type == MfUltralightPollerEventTypeAuthRequest) { - mfu_event->data->auth_context.skip_auth = true; - if(mf_ultralight_support_feature( - mfu_poller->feature_set, MfUltralightFeatureSupportAuthenticate)) { - mfu_event->data->auth_context.skip_auth = false; - memset( - mfu_poller->auth_context.tdes_key.data, 0x00, MF_ULTRALIGHT_C_AUTH_DES_KEY_SIZE); + if(poller_context->auth_context != NULL) { + mfu_event->data->auth_context = *poller_context->auth_context; + } else { + mfu_event->data->auth_context.skip_auth = true; + if(mfu_poller->data->type == MfUltralightTypeMfulC) { + mfu_event->data->auth_context.skip_auth = false; + memset( + mfu_poller->auth_context.tdes_key.data, + 0x00, + MF_ULTRALIGHT_C_AUTH_DES_KEY_SIZE); + } } } @@ -266,13 +272,17 @@ static NfcCommand mf_ultralight_poller_read_callback(NfcGenericEvent event, void return command; } -MfUltralightError mf_ultralight_poller_sync_read_card(Nfc* nfc, MfUltralightData* data) { +MfUltralightError mf_ultralight_poller_sync_read_card( + Nfc* nfc, + MfUltralightData* data, + const MfUltralightPollerAuthContext* auth_context) { furi_check(nfc); furi_check(data); MfUltralightPollerContext poller_context = {}; poller_context.thread_id = furi_thread_get_current_id(); poller_context.data.data = mf_ultralight_alloc(); + poller_context.auth_context = auth_context; NfcPoller* poller = nfc_poller_alloc(nfc, NfcProtocolMfUltralight); nfc_poller_start(poller, mf_ultralight_poller_read_callback, &poller_context); diff --git a/lib/nfc/protocols/mf_ultralight/mf_ultralight_poller_sync.h b/lib/nfc/protocols/mf_ultralight/mf_ultralight_poller_sync.h index ac585aad7..3f63203a7 100644 --- a/lib/nfc/protocols/mf_ultralight/mf_ultralight_poller_sync.h +++ b/lib/nfc/protocols/mf_ultralight/mf_ultralight_poller_sync.h @@ -1,6 +1,7 @@ #pragma once #include "mf_ultralight.h" +#include "mf_ultralight_poller.h" #include #ifdef __cplusplus @@ -27,7 +28,10 @@ MfUltralightError mf_ultralight_poller_sync_read_tearing_flag( uint8_t flag_num, MfUltralightTearingFlag* data); -MfUltralightError mf_ultralight_poller_sync_read_card(Nfc* nfc, MfUltralightData* data); +MfUltralightError mf_ultralight_poller_sync_read_card( + Nfc* nfc, + MfUltralightData* data, + const MfUltralightPollerAuthContext* auth_context); #ifdef __cplusplus } diff --git a/targets/f18/api_symbols.csv b/targets/f18/api_symbols.csv index 56bfbc2e6..d158b4f68 100644 --- a/targets/f18/api_symbols.csv +++ b/targets/f18/api_symbols.csv @@ -1,5 +1,5 @@ entry,status,name,type,params -Version,+,79.3,, +Version,+,80.1,, Header,+,applications/services/bt/bt_service/bt.h,, Header,+,applications/services/bt/bt_service/bt_keys_storage.h,, Header,+,applications/services/cli/cli.h,, diff --git a/targets/f7/api_symbols.csv b/targets/f7/api_symbols.csv index 707acacff..2c3b696cb 100644 --- a/targets/f7/api_symbols.csv +++ b/targets/f7/api_symbols.csv @@ -1,5 +1,5 @@ entry,status,name,type,params -Version,+,79.3,, +Version,+,80.1,, Header,+,applications/drivers/subghz/cc1101_ext/cc1101_ext_interconnect.h,, Header,+,applications/services/bt/bt_service/bt.h,, Header,+,applications/services/bt/bt_service/bt_keys_storage.h,, @@ -2681,7 +2681,7 @@ Function,+,mf_ultralight_poller_read_page_from_sector,MfUltralightError,"MfUltra Function,+,mf_ultralight_poller_read_signature,MfUltralightError,"MfUltralightPoller*, MfUltralightSignature*" Function,+,mf_ultralight_poller_read_tearing_flag,MfUltralightError,"MfUltralightPoller*, uint8_t, MfUltralightTearingFlag*" Function,+,mf_ultralight_poller_read_version,MfUltralightError,"MfUltralightPoller*, MfUltralightVersion*" -Function,+,mf_ultralight_poller_sync_read_card,MfUltralightError,"Nfc*, MfUltralightData*" +Function,+,mf_ultralight_poller_sync_read_card,MfUltralightError,"Nfc*, MfUltralightData*, const MfUltralightPollerAuthContext*" Function,+,mf_ultralight_poller_sync_read_counter,MfUltralightError,"Nfc*, uint8_t, MfUltralightCounter*" Function,+,mf_ultralight_poller_sync_read_page,MfUltralightError,"Nfc*, uint16_t, MfUltralightPage*" Function,+,mf_ultralight_poller_sync_read_signature,MfUltralightError,"Nfc*, MfUltralightSignature*" From 429c0dd387e82fa43a9c094e158ca3a3bb8d7aff Mon Sep 17 00:00:00 2001 From: Demae <56660883+Demae@users.noreply.github.com> Date: Thu, 13 Feb 2025 21:19:53 +1030 Subject: [PATCH 034/268] Added naming for DESFire cards + fix MF3ICD40 cards unable to be read (#4058) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fixed MF3ICD40 DESFire cards soft-locking NFC application due to read free memory being an unsupported function, added naming for DESFire cards * NFC: slightly more granular desfire card type resolution Co-authored-by: あく --- lib/nfc/protocols/mf_desfire/mf_desfire.c | 122 +++++++++++++++++- lib/nfc/protocols/mf_desfire/mf_desfire.h | 24 ++++ .../protocols/mf_desfire/mf_desfire_poller.c | 5 +- .../mf_desfire/mf_desfire_poller_i.c | 2 + 4 files changed, 148 insertions(+), 5 deletions(-) diff --git a/lib/nfc/protocols/mf_desfire/mf_desfire.c b/lib/nfc/protocols/mf_desfire/mf_desfire.c index 4d54a2c0e..42ad4634b 100644 --- a/lib/nfc/protocols/mf_desfire/mf_desfire.c +++ b/lib/nfc/protocols/mf_desfire/mf_desfire.c @@ -4,6 +4,46 @@ #define MF_DESFIRE_PROTOCOL_NAME "Mifare DESFire" +#define MF_DESFIRE_HW_MINOR_TYPE (0x00) +#define MF_DESFIRE_HW_MINOR_TYPE_MF3ICD40 (0x02) + +#define MF_DESFIRE_HW_MAJOR_TYPE_EV1 (0x01) +#define MF_DESFIRE_HW_MAJOR_TYPE_EV2 (0x12) +#define MF_DESFIRE_HW_MAJOR_TYPE_EV2_XL (0x22) +#define MF_DESFIRE_HW_MAJOR_TYPE_EV3 (0x33) +#define MF_DESFIRE_HW_MAJOR_TYPE_MF3ICD40 (0x00) + +#define MF_DESFIRE_STORAGE_SIZE_2K (0x16) +#define MF_DESFIRE_STORAGE_SIZE_4K (0x18) +#define MF_DESFIRE_STORAGE_SIZE_8K (0x1A) +#define MF_DESFIRE_STORAGE_SIZE_16K (0x1C) +#define MF_DESFIRE_STORAGE_SIZE_32K (0x1E) +#define MF_DESFIRE_STORAGE_SIZE_MF3ICD40 (0xFF) +#define MF_DESFIRE_STORAGE_SIZE_UNKNOWN (0xFF) + +#define MF_DESFIRE_TEST_TYPE_MF3ICD40(major, minor, storage) \ + (((major) == MF_DESFIRE_HW_MAJOR_TYPE_MF3ICD40) && \ + ((minor) == MF_DESFIRE_HW_MINOR_TYPE_MF3ICD40) && \ + ((storage) == MF_DESFIRE_STORAGE_SIZE_MF3ICD40)) + +static const char* mf_desfire_type_strings[] = { + [MfDesfireTypeMF3ICD40] = "(MF3ICD40)", + [MfDesfireTypeEV1] = "EV1", + [MfDesfireTypeEV2] = "EV2", + [MfDesfireTypeEV2XL] = "EV2 XL", + [MfDesfireTypeEV3] = "EV3", + [MfDesfireTypeUnknown] = "UNK", +}; + +static const char* mf_desfire_size_strings[] = { + [MfDesfireSize2k] = "2K", + [MfDesfireSize4k] = "4K", + [MfDesfireSize8k] = "8K", + [MfDesfireSize16k] = "16K", + [MfDesfireSize32k] = "32K", + [MfDesfireSizeUnknown] = "", +}; + const NfcDeviceBase nfc_device_mf_desfire = { .protocol_name = MF_DESFIRE_PROTOCOL_NAME, .alloc = (NfcDeviceAlloc)mf_desfire_alloc, @@ -26,7 +66,7 @@ MfDesfireData* mf_desfire_alloc(void) { data->master_key_versions = simple_array_alloc(&mf_desfire_key_version_array_config); data->application_ids = simple_array_alloc(&mf_desfire_app_id_array_config); data->applications = simple_array_alloc(&mf_desfire_application_array_config); - + data->device_name = furi_string_alloc(); return data; } @@ -38,6 +78,7 @@ void mf_desfire_free(MfDesfireData* data) { simple_array_free(data->application_ids); simple_array_free(data->master_key_versions); iso14443_4a_free(data->iso14443_4a_data); + furi_string_free(data->device_name); free(data); } @@ -228,10 +269,83 @@ bool mf_desfire_is_equal(const MfDesfireData* data, const MfDesfireData* other) simple_array_is_equal(data->applications, other->applications); } +static MfDesfireType mf_desfire_get_type_from_version(const MfDesfireVersion* const version) { + MfDesfireType type = MfDesfireTypeUnknown; + + switch(version->hw_major) { + case MF_DESFIRE_HW_MAJOR_TYPE_EV1: + type = MfDesfireTypeEV1; + break; + case MF_DESFIRE_HW_MAJOR_TYPE_EV2: + type = MfDesfireTypeEV2; + break; + case MF_DESFIRE_HW_MAJOR_TYPE_EV2_XL: + type = MfDesfireTypeEV2XL; + break; + case MF_DESFIRE_HW_MAJOR_TYPE_EV3: + type = MfDesfireTypeEV3; + break; + default: + if(MF_DESFIRE_TEST_TYPE_MF3ICD40(version->hw_major, version->hw_minor, version->hw_storage)) + type = MfDesfireTypeMF3ICD40; + break; + } + + return type; +} + +static MfDesfireSize mf_desfire_get_size_from_version(const MfDesfireVersion* const version) { + MfDesfireSize size = MfDesfireSizeUnknown; + + switch(version->hw_storage) { + case MF_DESFIRE_STORAGE_SIZE_2K: + size = MfDesfireSize2k; + break; + case MF_DESFIRE_STORAGE_SIZE_4K: + size = MfDesfireSize4k; + break; + case MF_DESFIRE_STORAGE_SIZE_8K: + size = MfDesfireSize8k; + break; + case MF_DESFIRE_STORAGE_SIZE_16K: + size = MfDesfireSize16k; + break; + case MF_DESFIRE_STORAGE_SIZE_32K: + size = MfDesfireSize32k; + break; + default: + if(MF_DESFIRE_TEST_TYPE_MF3ICD40(version->hw_major, version->hw_minor, version->hw_storage)) + size = MfDesfireSize4k; + break; + } + + return size; +} + const char* mf_desfire_get_device_name(const MfDesfireData* data, NfcDeviceNameType name_type) { - UNUSED(data); - UNUSED(name_type); - return MF_DESFIRE_PROTOCOL_NAME; + furi_check(data); + + const MfDesfireType type = mf_desfire_get_type_from_version(&data->version); + const MfDesfireSize size = mf_desfire_get_size_from_version(&data->version); + + if(type == MfDesfireTypeUnknown) { + furi_string_printf(data->device_name, "Unknown %s", MF_DESFIRE_PROTOCOL_NAME); + } else if(name_type == NfcDeviceNameTypeFull) { + furi_string_printf( + data->device_name, + "%s %s %s", + MF_DESFIRE_PROTOCOL_NAME, + mf_desfire_type_strings[type], + mf_desfire_size_strings[size]); + } else { + furi_string_printf( + data->device_name, + "%s %s", + mf_desfire_type_strings[type], + mf_desfire_size_strings[size]); + } + + return furi_string_get_cstr(data->device_name); } const uint8_t* mf_desfire_get_uid(const MfDesfireData* data, size_t* uid_len) { diff --git a/lib/nfc/protocols/mf_desfire/mf_desfire.h b/lib/nfc/protocols/mf_desfire/mf_desfire.h index dd2009276..fb50008db 100644 --- a/lib/nfc/protocols/mf_desfire/mf_desfire.h +++ b/lib/nfc/protocols/mf_desfire/mf_desfire.h @@ -29,6 +29,28 @@ extern "C" { #define MF_DESFIRE_APP_ID_SIZE (3) #define MF_DESFIRE_VALUE_SIZE (4) +typedef enum { + MfDesfireTypeMF3ICD40, + MfDesfireTypeEV1, + MfDesfireTypeEV2, + MfDesfireTypeEV2XL, + MfDesfireTypeEV3, + + MfDesfireTypeUnknown, + MfDesfireTypeNum, +} MfDesfireType; + +typedef enum { + MfDesfireSize2k, + MfDesfireSize4k, + MfDesfireSize8k, + MfDesfireSize16k, + MfDesfireSize32k, + + MfDesfireSizeUnknown, + MfDesfireSizeNum, +} MfDesfireSize; + typedef struct { uint8_t hw_vendor; uint8_t hw_type; @@ -131,6 +153,7 @@ typedef enum { MfDesfireErrorProtocol, MfDesfireErrorTimeout, MfDesfireErrorAuthentication, + MfDesfireErrorCommandNotSupported, } MfDesfireError; typedef struct { @@ -141,6 +164,7 @@ typedef struct { SimpleArray* master_key_versions; SimpleArray* application_ids; SimpleArray* applications; + FuriString* device_name; } MfDesfireData; extern const NfcDeviceBase nfc_device_mf_desfire; diff --git a/lib/nfc/protocols/mf_desfire/mf_desfire_poller.c b/lib/nfc/protocols/mf_desfire/mf_desfire_poller.c index bd8ecfaee..45e5a27f9 100644 --- a/lib/nfc/protocols/mf_desfire/mf_desfire_poller.c +++ b/lib/nfc/protocols/mf_desfire/mf_desfire_poller.c @@ -82,9 +82,12 @@ static NfcCommand mf_desfire_poller_handler_read_free_memory(MfDesfirePoller* in FURI_LOG_D(TAG, "Read free memory success"); instance->state = MfDesfirePollerStateReadMasterKeySettings; } else if(instance->error == MfDesfireErrorNotPresent) { - FURI_LOG_D(TAG, "Read free memoty is unsupported"); + FURI_LOG_D(TAG, "Read free memory is not present"); instance->state = MfDesfirePollerStateReadMasterKeySettings; command = NfcCommandReset; + } else if(instance->error == MfDesfireErrorCommandNotSupported) { + FURI_LOG_D(TAG, "Read free memory is unsupported"); + instance->state = MfDesfirePollerStateReadMasterKeySettings; } else { FURI_LOG_E(TAG, "Failed to read free memory"); iso14443_4a_poller_halt(instance->iso14443_4a_poller); diff --git a/lib/nfc/protocols/mf_desfire/mf_desfire_poller_i.c b/lib/nfc/protocols/mf_desfire/mf_desfire_poller_i.c index 1dd6c50e1..6d8dfda16 100644 --- a/lib/nfc/protocols/mf_desfire/mf_desfire_poller_i.c +++ b/lib/nfc/protocols/mf_desfire/mf_desfire_poller_i.c @@ -25,6 +25,8 @@ MfDesfireError mf_desfire_process_status_code(uint8_t status_code) { return MfDesfireErrorNone; case MF_DESFIRE_STATUS_AUTHENTICATION_ERROR: return MfDesfireErrorAuthentication; + case MF_DESFIRE_STATUS_ILLEGAL_COMMAND_CODE: + return MfDesfireErrorCommandNotSupported; default: return MfDesfireErrorProtocol; } From 95483fb56fe2178abce48a5d65df16d4bb8b65c3 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Thu, 13 Feb 2025 20:12:18 +0300 Subject: [PATCH 035/268] add findmy to system apps [ci skip] autoload by Willy-JL --- applications/system/application.fam | 1 + applications/system/find_my_flipper/README.md | 113 +++++++++ .../system/find_my_flipper/application.fam | 24 ++ applications/system/find_my_flipper/findmy.c | 141 +++++++++++ applications/system/find_my_flipper/findmy.h | 3 + .../system/find_my_flipper/findmy_i.h | 54 ++++ .../system/find_my_flipper/findmy_startup.c | 51 ++++ .../system/find_my_flipper/findmy_state.c | 192 +++++++++++++++ .../system/find_my_flipper/findmy_state.h | 37 +++ .../system/find_my_flipper/helpers/base64.c | 142 +++++++++++ .../system/find_my_flipper/helpers/base64.h | 21 ++ .../icons/DolphinDone_80x58.png | Bin 0 -> 1664 bytes .../system/find_my_flipper/icons/Lock_7x8.png | Bin 0 -> 3597 bytes .../find_my_flipper/icons/Ok_btn_9x9.png | Bin 0 -> 3605 bytes .../icons/WarningDolphinFlip_45x42.png | Bin 0 -> 1437 bytes .../find_my_flipper/icons/text_10px.png | Bin 0 -> 95 bytes .../system/find_my_flipper/location_icon.png | Bin 0 -> 96 bytes .../find_my_flipper/scenes/findmy_scene.c | 31 +++ .../find_my_flipper/scenes/findmy_scene.h | 30 +++ .../scenes/findmy_scene_config.c | 117 +++++++++ .../scenes/findmy_scene_config_import.c | 231 ++++++++++++++++++ .../findmy_scene_config_import_result.c | 60 +++++ .../scenes/findmy_scene_config_mac.c | 60 +++++ .../scenes/findmy_scene_config_packet.c | 58 +++++ .../scenes/findmy_scene_config_tagtype.c | 70 ++++++ .../scenes/findmy_scene_main.c | 56 +++++ .../find_my_flipper/scenes/findmy_scenes.h | 7 + .../system/find_my_flipper/screenshots/1.png | Bin 0 -> 2253 bytes .../system/find_my_flipper/screenshots/2.png | Bin 0 -> 2273 bytes .../system/find_my_flipper/screenshots/3.png | Bin 0 -> 2048 bytes .../find_my_flipper/views/findmy_main.c | 193 +++++++++++++++ .../find_my_flipper/views/findmy_main.h | 32 +++ applications/system/js_app/js_modules.h | 2 +- 33 files changed, 1725 insertions(+), 1 deletion(-) create mode 100644 applications/system/find_my_flipper/README.md create mode 100644 applications/system/find_my_flipper/application.fam create mode 100644 applications/system/find_my_flipper/findmy.c create mode 100644 applications/system/find_my_flipper/findmy.h create mode 100644 applications/system/find_my_flipper/findmy_i.h create mode 100644 applications/system/find_my_flipper/findmy_startup.c create mode 100644 applications/system/find_my_flipper/findmy_state.c create mode 100644 applications/system/find_my_flipper/findmy_state.h create mode 100644 applications/system/find_my_flipper/helpers/base64.c create mode 100644 applications/system/find_my_flipper/helpers/base64.h create mode 100644 applications/system/find_my_flipper/icons/DolphinDone_80x58.png create mode 100644 applications/system/find_my_flipper/icons/Lock_7x8.png create mode 100644 applications/system/find_my_flipper/icons/Ok_btn_9x9.png create mode 100644 applications/system/find_my_flipper/icons/WarningDolphinFlip_45x42.png create mode 100644 applications/system/find_my_flipper/icons/text_10px.png create mode 100644 applications/system/find_my_flipper/location_icon.png create mode 100644 applications/system/find_my_flipper/scenes/findmy_scene.c create mode 100644 applications/system/find_my_flipper/scenes/findmy_scene.h create mode 100644 applications/system/find_my_flipper/scenes/findmy_scene_config.c create mode 100644 applications/system/find_my_flipper/scenes/findmy_scene_config_import.c create mode 100644 applications/system/find_my_flipper/scenes/findmy_scene_config_import_result.c create mode 100644 applications/system/find_my_flipper/scenes/findmy_scene_config_mac.c create mode 100644 applications/system/find_my_flipper/scenes/findmy_scene_config_packet.c create mode 100644 applications/system/find_my_flipper/scenes/findmy_scene_config_tagtype.c create mode 100644 applications/system/find_my_flipper/scenes/findmy_scene_main.c create mode 100644 applications/system/find_my_flipper/scenes/findmy_scenes.h create mode 100644 applications/system/find_my_flipper/screenshots/1.png create mode 100644 applications/system/find_my_flipper/screenshots/2.png create mode 100644 applications/system/find_my_flipper/screenshots/3.png create mode 100644 applications/system/find_my_flipper/views/findmy_main.c create mode 100644 applications/system/find_my_flipper/views/findmy_main.h diff --git a/applications/system/application.fam b/applications/system/application.fam index 9a7ae40b1..e17bf3b7d 100644 --- a/applications/system/application.fam +++ b/applications/system/application.fam @@ -5,6 +5,7 @@ App( provides=[ "updater_app", "js_app", + "findmy_startup", # "archive", ], ) diff --git a/applications/system/find_my_flipper/README.md b/applications/system/find_my_flipper/README.md new file mode 100644 index 000000000..5035d3179 --- /dev/null +++ b/applications/system/find_my_flipper/README.md @@ -0,0 +1,113 @@ +# FindMy Flipper - FindMy SmartTag Emulator + +This app extends the functionality of the FlipperZero's bluetooth capabilities, enabling it to act as an Apple AirTag or Samsung SmartTag, or even both simultaneously. It utilizes the FlipperZero's BLE beacon to broadcast a SmartTag signal to be picked up by the FindMy Network. I made this to serve as a versatile tool for tracking purposes, offering the ability to clone existing tags, generate OpenHaystack key pairs for integration with Apple's FindMy network, and tune the device's beacon broadcast settings. + +## Features + +1. Tag Emulation: Clone your existing Apple AirTag or Samsung SmartTag to the FlipperZero, or generate a key pair for use with the FindMy network without owning an actual AirTag. +2. Customization: Users can adjust the interval between beacon broadcasts and modify the transmit power to suit their needs, optimizing for both visibility and battery life. +3. Efficient Background Operation: The app is optimized to run in the background, ensuring that your FlipperZero can still be tracked with minimal battery usage and without stopping normal use. + +## Usage Guide + +### Step 1: Installation +- **Option A:** Use the released/precompiled firmware appropriate (FAP) for your device. +- **Option B:** Build the firmware yourself using `fbt/ufbt`. +- Both Installation options require you to be running a dev build of firmware. When release gets access to the extra BLE beacon this will change, thank you! +- All firmware should now work with main branch, including icons + +### Step 2: Obtaining SmartTag Data + +#### Option A: Cloning Existing Tag (Preferred and allows you to track without additional setup) +1. **Pair a Tag:** First, pair an AirTag or Samsung SmartTag with your device. +2. **Enter 'Lost' Mode:** Keep the tag away from the device it's registered to for approximately 15 minutes. +3. **Download nrfConnect:** Install nrfConnect from the Google Play Store. (Apple version doesn't reveal the needed Raw data, looking for a workaround) +4. **Filter and Scan:** + - Open the app, click on filters, and exclude all except for the brand of your tag (Apple/Samsung). + - Adjust the RSSI to the lowest setting (-40 dBm). + - Initiate a scan. Wait for your SmartTag to appear as a "FindMy" device. +5. **Capture Data:** Click **Raw** or **View Raw** to capture your **payload** and note your tag's **MAC Address**. Immediately remove the tag's battery to prevent key/MAC rotation. +6. **Enter Data in FlipperZero App:** Input the captured **payload** and **MAC Address** into the FlipperZero app. + +#### Option B: Open Haystack Method +1. **Generate a Tag:** Download the `generate_keys.py` file and execute it in your terminal. (You will need cryptography ```python3 -m pip install cryptography```) +2. **Follow Prompts:** During execution, you'll be prompted for inputs. By the end, you'll obtain a **Private Key**, **Public Key**, **Payload**, and **MAC Address**. + - **Private Key** is necessary to receive location reports from Apple. + - **MAC Address** should be registered in the FlipperZero app: + 1. Open the app and navigate to the config menu. + 2. Choose "register tag" and enter the MAC Address when prompted. + 3. A payload dialog will appear next. Enter your **Payload** here. + 4. Click save. +3. **Configuration Completion:** With this setup, your device is ready for Open Haystack. Proceed with the specific steps for Open Haystack or MaclessHaystack based on your setup. + - Don't Own a Mac: https://github.com/dchristl/macless-haystack or https://github.com/Chapoly1305/FindMy + - Own a Mac: https://github.com/seemoo-lab/openhaystack + +To use OpenHayStack for tracking, you must use MacOS lower than version 14 (Mail Plug-in Incompetiablity of MacOS 14+ seemoo-lab/openhaystack#224). If you do own a device, I believe a convertor script can be provided without much of effort. If you do not own a Mac device or the system has been upgraded to 14 and beyond. The alternative solution includes, + + https://github.com/dchristl/macless-haystack (recommended in README) + https://github.com/Chapoly1305/FindMy (a project uses python and docker to provide location lookup as a backend service) + +## Setting Up on Mac with OpenHayStack (OHS) App -- If you own a Mac instructions + +Follow these steps to get everything working on a Mac using the latest version of the OpenHayStack app. +Thanks to Wr3nch for the help + +### Step 1: Create a New Device +- Start by creating a new device in the OpenHayStack app, but **do not deploy** it immediately after creation. + +### Step 2: Export Configuration +- Choose to **EXPORT** the configuration by selecting "all accessories as file." To simplify, ensure you only have one entry in the list before exporting. +- It is crucial that the export format is in JSON. + +### Step 3: Modify the JSON File +Open the exported JSON file in a text editor and make the following changes: +- **Left OHS, Right keys from my ```generate_keys.py``` script:** + - `symmetricKey` should be set to the `Hashed adv key`. + - `privateKey` should be replaced with your `Private Key`. + - `oldestRelevantSymmetricKey` should also use the `Hashed adv key`. +- Additionally, update the following attributes to `true`: + - `"isDeployed": true` + - `"isActive": true` + +### Step 4: Re-import the Configuration +- After saving your changes to the JSON file, re-import it back into OpenHayStack. + +### Step 5: Adjust Settings in OHS App +- In the OpenHayStack Mac App, navigate to the top bar and change the time setting from `1 Day` to `30min`. +- Give it some time to process and apply the new settings. + +By following these steps, you should have your device set up and ready to go with OpenHayStack on a Mac. +**** + +### Step 3: Configuration on the FlipperZero +- Upon launching the app, open the config menu and either click ```Import Tag From File``` or ```Register Tag Manually```. Put your generated .keys file onto the FlipperZero SD card inside the AppsData/FindMyFlipper folder to import from file. Or you can manually enter the tag information. When using the cloning method, you can export a .txt file from nrfConnect (click save button) amd place that in the same folder in order to import. + +### Step 4: Tracking +- Once the app is configured, your FlipperZero can be tracked using the relevant platform's tracking service (FindMy app for Apple devices, SmartThings for Samsung devices, and respective web browsers). If using generated keys and OpenHaystack then you can track on the OHS app or via the Macless Haystack setup. Links to both are above + + +Customization + +- Beacon Interval: Adjust how frequently your FlipperZero broadcasts its presence. +- Transmit Power: Increase or decrease the signal strength to balance between tracking range and battery life. + +Background Use + +The app is designed to have a negligible impact on battery life, even when running in the background. This allows for continuous tracking without the need for frequent recharging. + +Compatibility + +- Apple devices for AirTag tracking via the FindMy network. +- Any device that supports Samsung SmartTag tracking, including web browsers (previously FindMyMobile). + +Thanks + +- Huge thanks to all the people that contributed to the OpenHaystack project, supporting projects, and guides on the subject. This wouldn't be a thing without any of you! Special thanks to WillyJL for helping get the app input working and overall overhaul of the apps functions! + +Legal and Privacy + +This app is intended for personal and educational use. Users are responsible for complying with local privacy laws and regulations regarding tracking devices. The cloning and emulation of tracking tags should be done responsibly and with respect to the ownership of the original devices. + +Disclaimer + +This project is not affiliated with Apple Inc. or Samsung. All product names, logos, and brands are property of their respective owners. Use this app responsibly and ethically. diff --git a/applications/system/find_my_flipper/application.fam b/applications/system/find_my_flipper/application.fam new file mode 100644 index 000000000..380e2a941 --- /dev/null +++ b/applications/system/find_my_flipper/application.fam @@ -0,0 +1,24 @@ +App( + appid="findmy", + name="FindMy Flipper", + apptype=FlipperAppType.EXTERNAL, + entry_point="findmy_main", + requires=["gui"], + stack_size=2 * 1024, + fap_icon="location_icon.png", + fap_icon_assets="icons", + fap_category="Bluetooth", + fap_author="@MatthewKuKanich", + fap_weburl="https://github.com/MatthewKuKanich/FindMyFlipper", + fap_version="3.5", + fap_description="BLE FindMy Location Beacon", +) + +App( + appid="findmy_startup", + targets=["f7"], + apptype=FlipperAppType.STARTUP, + entry_point="findmy_startup", + sources=["findmy_startup.c", "findmy_state.c"], + order=1000, +) diff --git a/applications/system/find_my_flipper/findmy.c b/applications/system/find_my_flipper/findmy.c new file mode 100644 index 000000000..5afca1a19 --- /dev/null +++ b/applications/system/find_my_flipper/findmy.c @@ -0,0 +1,141 @@ +#include "findmy_i.h" + +static bool findmy_custom_event_callback(void* context, uint32_t event) { + furi_assert(context); + FindMy* app = context; + return scene_manager_handle_custom_event(app->scene_manager, event); +} + +static bool findmy_back_event_callback(void* context) { + furi_assert(context); + FindMy* app = context; + return scene_manager_handle_back_event(app->scene_manager); +} + +static FindMy* findmy_app_alloc() { + FindMy* app = malloc(sizeof(FindMy)); + + app->gui = furi_record_open(RECORD_GUI); + app->storage = furi_record_open(RECORD_STORAGE); + app->dialogs = furi_record_open(RECORD_DIALOGS); + + app->view_dispatcher = view_dispatcher_alloc(); + + app->scene_manager = scene_manager_alloc(&findmy_scene_handlers, app); + + view_dispatcher_set_event_callback_context(app->view_dispatcher, app); + view_dispatcher_set_custom_event_callback(app->view_dispatcher, findmy_custom_event_callback); + view_dispatcher_set_navigation_event_callback( + app->view_dispatcher, findmy_back_event_callback); + + app->findmy_main = findmy_main_alloc(app); + view_dispatcher_add_view( + app->view_dispatcher, FindMyViewMain, findmy_main_get_view(app->findmy_main)); + + app->byte_input = byte_input_alloc(); + view_dispatcher_add_view( + app->view_dispatcher, FindMyViewByteInput, byte_input_get_view(app->byte_input)); + + app->var_item_list = variable_item_list_alloc(); + view_dispatcher_add_view( + app->view_dispatcher, + FindMyViewVarItemList, + variable_item_list_get_view(app->var_item_list)); + + app->popup = popup_alloc(); + view_dispatcher_add_view(app->view_dispatcher, FindMyViewPopup, popup_get_view(app->popup)); + + view_dispatcher_attach_to_gui(app->view_dispatcher, app->gui, ViewDispatcherTypeFullscreen); + + findmy_state_load(&app->state); + findmy_state_apply(&app->state); + + findmy_main_update_active(app->findmy_main, app->state.beacon_active); + findmy_main_update_interval(app->findmy_main, app->state.broadcast_interval); + findmy_main_toggle_mac(app->findmy_main, app->state.show_mac); + findmy_main_update_mac(app->findmy_main, app->state.mac); + findmy_main_update_type(app->findmy_main, app->state.tag_type); + + return app; +} + +static void findmy_app_free(FindMy* app) { + furi_assert(app); + + view_dispatcher_remove_view(app->view_dispatcher, FindMyViewPopup); + popup_free(app->popup); + + view_dispatcher_remove_view(app->view_dispatcher, FindMyViewVarItemList); + variable_item_list_free(app->var_item_list); + + view_dispatcher_remove_view(app->view_dispatcher, FindMyViewByteInput); + byte_input_free(app->byte_input); + + view_dispatcher_remove_view(app->view_dispatcher, FindMyViewMain); + findmy_main_free(app->findmy_main); + + view_dispatcher_free(app->view_dispatcher); + scene_manager_free(app->scene_manager); + + furi_record_close(RECORD_DIALOGS); + furi_record_close(RECORD_STORAGE); + furi_record_close(RECORD_GUI); + + free(app); +} + +int32_t findmy_main(void* p) { + UNUSED(p); + FindMy* app = findmy_app_alloc(); + + scene_manager_next_scene(app->scene_manager, FindMySceneMain); + + view_dispatcher_run(app->view_dispatcher); + + findmy_app_free(app); + return 0; +} + +void findmy_change_broadcast_interval(FindMy* app, uint8_t value) { + if(value > 10 || value < 1) { + return; + } + app->state.broadcast_interval = value; + findmy_main_update_interval(app->findmy_main, app->state.broadcast_interval); + findmy_state_save_and_apply(&app->state); +} + +void findmy_change_transmit_power(FindMy* app, uint8_t value) { + if(value > 6) { + return; + } + app->state.transmit_power = value; + findmy_state_save_and_apply(&app->state); +} + +void findmy_toggle_show_mac(FindMy* app, bool show_mac) { + app->state.show_mac = show_mac; + findmy_main_toggle_mac(app->findmy_main, app->state.show_mac); + findmy_state_save_and_apply(&app->state); +} + +void findmy_toggle_beacon(FindMy* app) { + app->state.beacon_active = !app->state.beacon_active; + findmy_state_save_and_apply(&app->state); + findmy_main_update_active(app->findmy_main, app->state.beacon_active); +} + +void findmy_set_tag_type(FindMy* app, FindMyType type) { + app->state.tag_type = type; + findmy_state_save_and_apply(&app->state); + findmy_main_update_type(app->findmy_main, type); +} + +void findmy_reverse_mac_addr(uint8_t mac_addr[GAP_MAC_ADDR_SIZE]) { + uint8_t tmp; + for(size_t i = 0; i < GAP_MAC_ADDR_SIZE / 2; i++) { + tmp = mac_addr[i]; + mac_addr[i] = mac_addr[GAP_MAC_ADDR_SIZE - 1 - i]; + mac_addr[GAP_MAC_ADDR_SIZE - 1 - i] = tmp; + } +} diff --git a/applications/system/find_my_flipper/findmy.h b/applications/system/find_my_flipper/findmy.h new file mode 100644 index 000000000..344882ef1 --- /dev/null +++ b/applications/system/find_my_flipper/findmy.h @@ -0,0 +1,3 @@ +#pragma once + +typedef struct FindMy FindMy; diff --git a/applications/system/find_my_flipper/findmy_i.h b/applications/system/find_my_flipper/findmy_i.h new file mode 100644 index 000000000..fee227924 --- /dev/null +++ b/applications/system/find_my_flipper/findmy_i.h @@ -0,0 +1,54 @@ +#pragma once + +#include "findmy.h" +#include "findmy_state.h" +#include +#include +#include "findmy_icons.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include "views/findmy_main.h" +#include +#include +#include +#include "scenes/findmy_scene.h" +#include "helpers/base64.h" + +void findmy_reverse_mac_addr(uint8_t mac_addr[GAP_MAC_ADDR_SIZE]); + +struct FindMy { + Gui* gui; + Storage* storage; + DialogsApp* dialogs; + SceneManager* scene_manager; + ViewDispatcher* view_dispatcher; + + FindMyMain* findmy_main; + ByteInput* byte_input; + VariableItemList* var_item_list; + Popup* popup; + + uint8_t mac_buf[EXTRA_BEACON_MAC_ADDR_SIZE]; + uint8_t packet_buf[EXTRA_BEACON_MAX_DATA_SIZE]; + + FindMyState state; +}; + +typedef enum { + FindMyViewMain, + FindMyViewByteInput, + FindMyViewVarItemList, + FindMyViewPopup, +} FindMyView; + +void findmy_change_broadcast_interval(FindMy* app, uint8_t value); +void findmy_change_transmit_power(FindMy* app, uint8_t value); +void findmy_toggle_show_mac(FindMy* app, bool show_mac); +void findmy_set_tag_type(FindMy* app, FindMyType type); +void findmy_toggle_beacon(FindMy* app); diff --git a/applications/system/find_my_flipper/findmy_startup.c b/applications/system/find_my_flipper/findmy_startup.c new file mode 100644 index 000000000..b7af97e23 --- /dev/null +++ b/applications/system/find_my_flipper/findmy_startup.c @@ -0,0 +1,51 @@ +#include "findmy_state.h" +#include +#include +#include +#include + +#define TAG "FindMyStartup" + +static int32_t findmy_startup_apply(void* context) { + UNUSED(context); + FURI_LOG_D(TAG, "Loading state"); + + // Wait for BT init and check core2 + furi_record_open(RECORD_BT); + furi_record_close(RECORD_BT); + if(!furi_hal_bt_is_gatt_gap_supported()) return 0; + + FindMyState state; + if(findmy_state_load(&state)) { + FURI_LOG_D(TAG, "Activating beacon"); + findmy_state_apply(&state); + } else { + FURI_LOG_D(TAG, "Beacon not active, bailing"); + } + + return 0; +} + +static void findmy_startup_mount_callback(const void* message, void* context) { + UNUSED(context); + const StorageEvent* event = message; + + if(event->type == StorageEventTypeCardMount) { + run_parallel(findmy_startup_apply, NULL, 2048); + } +} + +void findmy_startup() { + if(furi_hal_rtc_get_boot_mode() != FuriHalRtcBootModeNormal) return; + + Storage* storage = furi_record_open(RECORD_STORAGE); + furi_pubsub_subscribe(storage_get_pubsub(storage), findmy_startup_mount_callback, NULL); + + if(storage_sd_status(storage) != FSE_OK) { + FURI_LOG_D(TAG, "SD Card not ready, skipping startup hook"); + } else { + findmy_startup_apply(NULL); + } + + furi_record_close(RECORD_STORAGE); +} diff --git a/applications/system/find_my_flipper/findmy_state.c b/applications/system/find_my_flipper/findmy_state.c new file mode 100644 index 000000000..047eb0b83 --- /dev/null +++ b/applications/system/find_my_flipper/findmy_state.c @@ -0,0 +1,192 @@ +#include "findmy_state.h" + +#include +#include +#include +#include +#include + +bool findmy_state_load(FindMyState* out_state) { + FindMyState state; + + // Try to load from file + bool loaded_from_file = false; + Storage* storage = furi_record_open(RECORD_STORAGE); + if(storage_file_exists(storage, FINDMY_STATE_PATH)) { + FlipperFormat* file = flipper_format_file_alloc(storage); + FuriString* str = furi_string_alloc(); + uint32_t tmp; + do { + if(!flipper_format_file_open_existing(file, FINDMY_STATE_PATH)) break; + if(!flipper_format_read_header(file, str, &tmp)) break; + if(furi_string_cmp_str(str, FINDMY_STATE_HEADER)) break; + if(tmp != FINDMY_STATE_VER) break; + + if(!flipper_format_read_bool(file, "beacon_active", &state.beacon_active, 1)) break; + + if(!flipper_format_read_uint32(file, "broadcast_interval", &tmp, 1)) break; + state.broadcast_interval = tmp; + + if(!flipper_format_read_uint32(file, "transmit_power", &tmp, 1)) break; + state.transmit_power = tmp; + + if(!flipper_format_read_uint32(file, "tag_type", &tmp, 1)) { + tmp = FindMyTypeApple; + flipper_format_rewind(file); + } + state.tag_type = tmp; + + if(!flipper_format_read_bool(file, "show_mac", &state.show_mac, 1)) { + // Support migrating from old config + state.show_mac = false; + flipper_format_rewind(file); + } + + if(!flipper_format_read_hex(file, "mac", state.mac, sizeof(state.mac))) break; + + if(!flipper_format_read_hex( + file, "data", state.data, findmy_state_data_size(state.tag_type))) + break; + + loaded_from_file = true; + } while(0); + furi_string_free(str); + flipper_format_free(file); + } + furi_record_close(RECORD_STORAGE); + + // Otherwise set default values + if(!loaded_from_file) { + state.beacon_active = false; + state.broadcast_interval = 5; + state.transmit_power = 6; + state.show_mac = false; + state.tag_type = FindMyTypeApple; + + // Set default mac + uint8_t default_mac[EXTRA_BEACON_MAC_ADDR_SIZE] = {0x66, 0x55, 0x44, 0x33, 0x22, 0x11}; + memcpy(state.mac, default_mac, sizeof(state.mac)); + + // Set default empty AirTag data + uint8_t* data = state.data; + *data++ = 0x1E; // Length + *data++ = 0xFF; // Manufacturer Specific Data + *data++ = 0x4C; // Company ID (Apple, Inc.) + *data++ = 0x00; // ... + *data++ = 0x12; // Type (FindMy) + *data++ = 0x19; // Length + *data++ = 0x00; // Battery Status set to Full + // Placeholder Empty Public Key without the MAC address + for(size_t i = 0; i < 22; ++i) { + *data++ = 0x00; + } + *data++ = 0x00; // First 2 bits are the version + *data++ = 0x00; // Hint (0x00) + } + + // Copy to caller state before popping stack + memcpy(out_state, &state, sizeof(state)); + + // Return if active, can be used to start after loading in an if statement + return state.beacon_active; +} + +static void findmy_state_update_payload_battery(FindMyState* state) { + // Update the battery level in the payload + if(state->tag_type == FindMyTypeApple) { + uint32_t battery_capacity = furi_hal_power_get_battery_full_capacity(); + uint32_t battery_remaining = furi_hal_power_get_battery_remaining_capacity(); + uint8_t battery_percent = (battery_remaining * 100) / battery_capacity; + uint8_t battery_level; + + if(battery_percent > 80) { + battery_level = BATTERY_FULL; + } else if(battery_percent > 50) { + battery_level = BATTERY_MEDIUM; + } else if(battery_percent > 20) { + battery_level = BATTERY_LOW; + } else { + battery_level = BATTERY_CRITICAL; + } + state->data[6] = battery_level; + } +} + +void findmy_state_apply(FindMyState* state) { + // This function applies configured state to the beacon (loaded values) + + // Stop beacon before configuring + if(furi_hal_bt_extra_beacon_is_active()) { + furi_check(furi_hal_bt_extra_beacon_stop()); + } + + // Make config struct from configured parameters and set it + GapExtraBeaconConfig config = { + .min_adv_interval_ms = state->broadcast_interval * 1000, // Converting s to ms + .max_adv_interval_ms = (state->broadcast_interval * 1000) + 150, + .adv_channel_map = GapAdvChannelMapAll, + .adv_power_level = GapAdvPowerLevel_0dBm + state->transmit_power, + .address_type = GapAddressTypePublic, + }; + memcpy(config.address, state->mac, sizeof(config.address)); + furi_check(furi_hal_bt_extra_beacon_set_config(&config)); + + // Update data payload with battery level and set it + findmy_state_update_payload_battery(state); + furi_check( + furi_hal_bt_extra_beacon_set_data(state->data, findmy_state_data_size(state->tag_type))); + + // Start beacon if configured + if(state->beacon_active) { + furi_check(furi_hal_bt_extra_beacon_start()); + } +} + +static void findmy_state_save(FindMyState* state) { + Storage* storage = furi_record_open(RECORD_STORAGE); + storage_simply_mkdir(storage, FINDMY_STATE_DIR); + FlipperFormat* file = flipper_format_file_alloc(storage); + + do { + uint32_t tmp; + if(!flipper_format_file_open_always(file, FINDMY_STATE_PATH)) break; + if(!flipper_format_write_header_cstr(file, FINDMY_STATE_HEADER, FINDMY_STATE_VER)) break; + + if(!flipper_format_write_bool(file, "beacon_active", &state->beacon_active, 1)) break; + + tmp = state->broadcast_interval; + if(!flipper_format_write_uint32(file, "broadcast_interval", &tmp, 1)) break; + + tmp = state->transmit_power; + if(!flipper_format_write_uint32(file, "transmit_power", &tmp, 1)) break; + + tmp = state->tag_type; + if(!flipper_format_write_uint32(file, "tag_type", &tmp, 1)) break; + + if(!flipper_format_write_bool(file, "show_mac", &state->show_mac, 1)) break; + + if(!flipper_format_write_hex(file, "mac", state->mac, sizeof(state->mac))) break; + if(!flipper_format_write_hex( + file, "data", state->data, findmy_state_data_size(state->tag_type))) + break; + } while(0); + flipper_format_free(file); + furi_record_close(RECORD_STORAGE); +} + +void findmy_state_save_and_apply(FindMyState* state) { + findmy_state_apply(state); + findmy_state_save(state); +} + +uint8_t findmy_state_data_size(FindMyType type) { + switch(type) { + case FindMyTypeApple: + case FindMyTypeSamsung: + return 31; + case FindMyTypeTile: + return 21; + default: + return 0; + } +} diff --git a/applications/system/find_my_flipper/findmy_state.h b/applications/system/find_my_flipper/findmy_state.h new file mode 100644 index 000000000..f0be35430 --- /dev/null +++ b/applications/system/find_my_flipper/findmy_state.h @@ -0,0 +1,37 @@ +#pragma once + +#include + +#define FINDMY_STATE_HEADER "FindMy Flipper State" +#define FINDMY_STATE_VER 1 +#define FINDMY_STATE_DIR EXT_PATH("apps_data/findmy") +#define FINDMY_STATE_PATH FINDMY_STATE_DIR "/findmy_state.txt" + +#define BATTERY_FULL 0x00 +#define BATTERY_MEDIUM 0x50 +#define BATTERY_LOW 0xA0 +#define BATTERY_CRITICAL 0xF0 + +typedef enum { + FindMyTypeApple, + FindMyTypeSamsung, + FindMyTypeTile, +} FindMyType; + +typedef struct { + bool beacon_active; + uint8_t broadcast_interval; + uint8_t transmit_power; + bool show_mac; + uint8_t mac[EXTRA_BEACON_MAC_ADDR_SIZE]; + uint8_t data[EXTRA_BEACON_MAX_DATA_SIZE]; + FindMyType tag_type; +} FindMyState; + +bool findmy_state_load(FindMyState* out_state); + +void findmy_state_apply(FindMyState* state); + +void findmy_state_save_and_apply(FindMyState* state); + +uint8_t findmy_state_data_size(FindMyType type); diff --git a/applications/system/find_my_flipper/helpers/base64.c b/applications/system/find_my_flipper/helpers/base64.c new file mode 100644 index 000000000..9c6a32bfa --- /dev/null +++ b/applications/system/find_my_flipper/helpers/base64.c @@ -0,0 +1,142 @@ +/* + * Base64 encoding/decoding (RFC1341) + * Copyright (c) 2005-2011, Jouni Malinen + * + * This software may be distributed under the terms of the BSD license. + * See README for more details. + */ +// https://web.mit.edu/freebsd/head/contrib/wpa/src/utils/base64.c + +#include "base64.h" + +#define os_malloc malloc +#define os_free free +#define os_memset memset + +static const unsigned char base64_table[65] = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + +/** + * base64_encode - Base64 encode + * @src: Data to be encoded + * @len: Length of the data to be encoded + * @out_len: Pointer to output length variable, or %NULL if not used + * Returns: Allocated buffer of out_len bytes of encoded data, + * or %NULL on failure + * + * Caller is responsible for freeing the returned buffer. Returned buffer is + * nul terminated to make it easier to use as a C string. The nul terminator is + * not included in out_len. + */ +unsigned char* base64_encode(const unsigned char* src, size_t len, size_t* out_len) { + unsigned char *out, *pos; + const unsigned char *end, *in; + size_t olen; + int line_len; + + olen = len * 4 / 3 + 4; /* 3-byte blocks to 4-byte */ + olen += olen / 72; /* line feeds */ + olen++; /* nul termination */ + if(olen < len) return NULL; /* integer overflow */ + out = os_malloc(olen); + if(out == NULL) return NULL; + + end = src + len; + in = src; + pos = out; + line_len = 0; + while(end - in >= 3) { + *pos++ = base64_table[in[0] >> 2]; + *pos++ = base64_table[((in[0] & 0x03) << 4) | (in[1] >> 4)]; + *pos++ = base64_table[((in[1] & 0x0f) << 2) | (in[2] >> 6)]; + *pos++ = base64_table[in[2] & 0x3f]; + in += 3; + line_len += 4; + if(line_len >= 72) { + *pos++ = '\n'; + line_len = 0; + } + } + + if(end - in) { + *pos++ = base64_table[in[0] >> 2]; + if(end - in == 1) { + *pos++ = base64_table[(in[0] & 0x03) << 4]; + *pos++ = '='; + } else { + *pos++ = base64_table[((in[0] & 0x03) << 4) | (in[1] >> 4)]; + *pos++ = base64_table[(in[1] & 0x0f) << 2]; + } + *pos++ = '='; + line_len += 4; + } + + if(line_len) *pos++ = '\n'; + + *pos = '\0'; + if(out_len) *out_len = pos - out; + return out; +} + +/** + * base64_decode - Base64 decode + * @src: Data to be decoded + * @len: Length of the data to be decoded + * @out_len: Pointer to output length variable + * Returns: Allocated buffer of out_len bytes of decoded data, + * or %NULL on failure + * + * Caller is responsible for freeing the returned buffer. + */ +unsigned char* base64_decode(const unsigned char* src, size_t len, size_t* out_len) { + unsigned char dtable[256], *out, *pos, block[4], tmp; + size_t i, count, olen; + int pad = 0; + + os_memset(dtable, 0x80, 256); + for(i = 0; i < sizeof(base64_table) - 1; i++) + dtable[base64_table[i]] = (unsigned char)i; + dtable['='] = 0; + + count = 0; + for(i = 0; i < len; i++) { + if(dtable[src[i]] != 0x80) count++; + } + + if(count == 0 || count % 4) return NULL; + + olen = count / 4 * 3; + pos = out = os_malloc(olen); + if(out == NULL) return NULL; + + count = 0; + for(i = 0; i < len; i++) { + tmp = dtable[src[i]]; + if(tmp == 0x80) continue; + + if(src[i] == '=') pad++; + block[count] = tmp; + count++; + if(count == 4) { + *pos++ = (block[0] << 2) | (block[1] >> 4); + *pos++ = (block[1] << 4) | (block[2] >> 2); + *pos++ = (block[2] << 6) | block[3]; + count = 0; + if(pad) { + if(pad == 1) + pos--; + else if(pad == 2) + pos -= 2; + else { + /* Invalid padding */ + os_free(out); + return NULL; + } + break; + } + } + } + + *out_len = pos - out; + return out; +} diff --git a/applications/system/find_my_flipper/helpers/base64.h b/applications/system/find_my_flipper/helpers/base64.h new file mode 100644 index 000000000..d20660c5b --- /dev/null +++ b/applications/system/find_my_flipper/helpers/base64.h @@ -0,0 +1,21 @@ +/* + * Base64 encoding/decoding (RFC1341) + * Copyright (c) 2005, Jouni Malinen + * + * This software may be distributed under the terms of the BSD license. + * See README for more details. + */ +// https://web.mit.edu/freebsd/head/contrib/wpa/src/utils/base64.h + +#ifndef BASE64_H +#define BASE64_H + +#include +#include +#include +#include + +unsigned char* base64_encode(const unsigned char* src, size_t len, size_t* out_len); +unsigned char* base64_decode(const unsigned char* src, size_t len, size_t* out_len); + +#endif /* BASE64_H */ diff --git a/applications/system/find_my_flipper/icons/DolphinDone_80x58.png b/applications/system/find_my_flipper/icons/DolphinDone_80x58.png new file mode 100644 index 0000000000000000000000000000000000000000..594d62d5294997399bc3b256fbe7685ce10b146d GIT binary patch literal 1664 zcmbVNc~BE)6wheY2!uKFLY*phgJAoI;~(9b-S2(h_kQpF-Zi@^ zF+PgtHqDL0;qcVaN)5Xvvag&wj{R2ntG{IzUnWw^ETRmI4WkK8n4Z!RfZBv*5gG#1 zJ63#0gm5_H9b}T0(Z(&5iMAyfDpT!HDDqb46vJwW~kNcFY38LI^aOT(OO4TNw@UFO4^9 zTaz3X0@M&zDwoFDnivAcz-<2B?#QLcvXLjyBwHBFsHE^*6Jci5N(G<25$Z|3oFF79 zt{0&K1d|yAVyOrc=nxShkm0BVg>_OwC(@1Cc@tix3X)1BkVqB;1;KD+Br265;Soxe zN-Rdg!lmdKR&BO2m>DO=e3Pv2Q7rOStUQ7yFovR&D9Sk235nShLs_#a3xG(35HLFq z!%4I2WR9y!uYy(*G?_=}RWxM+M$#-N-#`HpAu80Tmd;G97`! zdI?Mr{87CA|E3RQNrA3j`A_eR9kC7R5?@aPyLmlNgqa;8nw^%VblFu7XWV|ZGAzm7 z-ns8C-3V|CWLx`RUVV5?e=~JLXGNd*gA2VSy}M^liFW=t^rC%5`mI^MKJ^z*yC+9$ zK8y8opgw0i#OZbK-@bIOr+sL3X*0ny?F_5$?^u;L5LsJ%K{s=fe|&Jv>M52PH5+{E zciwC+?Q1wXec-Dn?aE=B+ip?UBV`Bm*T(s!wWZ~plEuod;*9$(j?$d6^(EcKbW!X< z&nIP6%$5Z)#Sc{zYqC;eHfJr#r`xJt{34$f<9D)}sVN9J6SzfMQhTpr?kB2_2ge#4 zPMiznH%5_{H{8XZyCE8Z^qt3(nNamZCKd_dTt1 zybD4vFW|RcM-rOAUY>VP`H4R@hUX>mS_}#FURP5QO}9;kzD&7Mf0X!18#*Cy#s=lb zoGCF8teZgFA$ z+RoI3!Q}^!2oJhss<8vOJLqi_j*V;@D?$qOzS!qAdI~fh>f!$Rlk&;`Vf`4<% z;twaDuf~1bzjhOXb@_XjZ49oi7zcZBs XY(V!OcIS4x{t4>Hc;)f%%(edjvT}hx literal 0 HcmV?d00001 diff --git a/applications/system/find_my_flipper/icons/Lock_7x8.png b/applications/system/find_my_flipper/icons/Lock_7x8.png new file mode 100644 index 0000000000000000000000000000000000000000..f7c9ca2c702f1b93d7d06bd12ae708655c79d7c8 GIT binary patch literal 3597 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 a9|QyVQ~@!E@+ey_4zMz}H7hmoyzn0`d`!~- literal 0 HcmV?d00001 diff --git a/applications/system/find_my_flipper/icons/Ok_btn_9x9.png b/applications/system/find_my_flipper/icons/Ok_btn_9x9.png new file mode 100644 index 0000000000000000000000000000000000000000..9a1539da2049f12f7b25f96b11a9c40cd8227302 GIT binary patch literal 3605 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 Uzg^}hzX270p00i_>zopr0LnWHWB>pF literal 0 HcmV?d00001 diff --git a/applications/system/find_my_flipper/icons/text_10px.png b/applications/system/find_my_flipper/icons/text_10px.png new file mode 100644 index 0000000000000000000000000000000000000000..cd38770f411405a544840c16960e389c64c8bf3a GIT binary patch literal 95 zcmeAS@N?(olHy`uVBq!ia0vp^AT}2xkYHHq`AGmssd>6MhE&W+POv!;a`M27zf(P@ qH@>uDp3KO6S&>c6noW%j2-q274O#x#pIx>eq|?*Y&t;ucLK6TN9vKV( literal 0 HcmV?d00001 diff --git a/applications/system/find_my_flipper/location_icon.png b/applications/system/find_my_flipper/location_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..49e687c231dbc9342f06a03b9d9775c03c47a5e0 GIT binary patch literal 96 zcmeAS@N?(olHy`uVBq!ia0vp^AT}2xkYHHq`AGmsse8IOhE&W+uBn*+(vJCbBeRbU sj}H%zPrQT#zkz|Kk%6Is0jD7YLvtpZ`j0&e=YzC*y85}Sb4q9e0H~K1MgRZ+ literal 0 HcmV?d00001 diff --git a/applications/system/find_my_flipper/scenes/findmy_scene.c b/applications/system/find_my_flipper/scenes/findmy_scene.c new file mode 100644 index 000000000..f854566e5 --- /dev/null +++ b/applications/system/find_my_flipper/scenes/findmy_scene.c @@ -0,0 +1,31 @@ +#include "findmy_scene.h" +#include "findmy.h" + +// Generate scene on_enter handlers array +#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_enter, +void (*const findmy_on_enter_handlers[])(void*) = { +#include "findmy_scenes.h" +}; +#undef ADD_SCENE + +// Generate scene on_event handlers array +#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_event, +bool (*const findmy_on_event_handlers[])(void* context, SceneManagerEvent event) = { +#include "findmy_scenes.h" +}; +#undef ADD_SCENE + +// Generate scene on_exit handlers array +#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_exit, +void (*const findmy_on_exit_handlers[])(void* context) = { +#include "findmy_scenes.h" +}; +#undef ADD_SCENE + +// Initialize scene handlers configuration structure +const SceneManagerHandlers findmy_scene_handlers = { + .on_enter_handlers = findmy_on_enter_handlers, + .on_event_handlers = findmy_on_event_handlers, + .on_exit_handlers = findmy_on_exit_handlers, + .scene_num = FindMySceneNum, +}; diff --git a/applications/system/find_my_flipper/scenes/findmy_scene.h b/applications/system/find_my_flipper/scenes/findmy_scene.h new file mode 100644 index 000000000..14ab9d6bf --- /dev/null +++ b/applications/system/find_my_flipper/scenes/findmy_scene.h @@ -0,0 +1,30 @@ +#pragma once + +#include +#include "findmy.h" + +// Generate scene id and total number +#define ADD_SCENE(prefix, name, id) FindMyScene##id, +typedef enum { +#include "findmy_scenes.h" + FindMySceneNum, +} FindMyScene; +#undef ADD_SCENE + +extern const SceneManagerHandlers findmy_scene_handlers; + +// Generate scene on_enter handlers declaration +#define ADD_SCENE(prefix, name, id) void prefix##_scene_##name##_on_enter(void*); +#include "findmy_scenes.h" +#undef ADD_SCENE + +// Generate scene on_event handlers declaration +#define ADD_SCENE(prefix, name, id) \ + bool prefix##_scene_##name##_on_event(void* context, SceneManagerEvent event); +#include "findmy_scenes.h" +#undef ADD_SCENE + +// Generate scene on_exit handlers declaration +#define ADD_SCENE(prefix, name, id) void prefix##_scene_##name##_on_exit(void* context); +#include "findmy_scenes.h" +#undef ADD_SCENE diff --git a/applications/system/find_my_flipper/scenes/findmy_scene_config.c b/applications/system/find_my_flipper/scenes/findmy_scene_config.c new file mode 100644 index 000000000..caad8f13e --- /dev/null +++ b/applications/system/find_my_flipper/scenes/findmy_scene_config.c @@ -0,0 +1,117 @@ +#include "../findmy_i.h" + +enum VarItemListIndex { + VarItemListIndexBroadcastInterval, + VarItemListIndexTransmitPower, + VarItemListIndexRegisterTag, + VarItemListIndexShowMac, + VarItemListIndexAbout, +}; + +void findmy_scene_config_broadcast_interval_changed(VariableItem* item) { + FindMy* app = variable_item_get_context(item); + uint8_t index = variable_item_get_current_value_index(item); + findmy_change_broadcast_interval(app, index + 1); + char str[5]; + snprintf(str, sizeof(str), "%ds", app->state.broadcast_interval); + variable_item_set_current_value_text(item, str); + variable_item_set_current_value_index(item, app->state.broadcast_interval - 1); +} + +void findmy_scene_config_transmit_power_changed(VariableItem* item) { + FindMy* app = variable_item_get_context(item); + uint8_t index = variable_item_get_current_value_index(item); + findmy_change_transmit_power(app, index); + char str[7]; + snprintf(str, sizeof(str), "%ddBm", app->state.transmit_power); + variable_item_set_current_value_text(item, str); + variable_item_set_current_value_index(item, app->state.transmit_power); +} + +void findmy_scene_config_show_mac(VariableItem* item) { + FindMy* app = variable_item_get_context(item); + uint8_t index = variable_item_get_current_value_index(item); + findmy_toggle_show_mac(app, index); + variable_item_set_current_value_text(item, app->state.show_mac ? "Yes" : "No"); + variable_item_set_current_value_index(item, app->state.show_mac); +} + +void findmy_scene_config_callback(void* context, uint32_t index) { + furi_assert(context); + FindMy* app = context; + view_dispatcher_send_custom_event(app->view_dispatcher, index); +} + +void findmy_scene_config_on_enter(void* context) { + FindMy* app = context; + VariableItemList* var_item_list = app->var_item_list; + VariableItem* item; + + item = variable_item_list_add( + var_item_list, + "Broadcast Interval", + 10, + findmy_scene_config_broadcast_interval_changed, + app); + // Broadcast Interval is 1-10, so use 0-9 and offset indexes by 1 + variable_item_set_current_value_index(item, app->state.broadcast_interval - 1); + char interval_str[5]; + snprintf(interval_str, sizeof(interval_str), "%ds", app->state.broadcast_interval); + variable_item_set_current_value_text(item, interval_str); + + item = variable_item_list_add( + var_item_list, "Transmit Power", 7, findmy_scene_config_transmit_power_changed, app); + variable_item_set_current_value_index(item, app->state.transmit_power); + char power_str[7]; + snprintf(power_str, sizeof(power_str), "%ddBm", app->state.transmit_power); + variable_item_set_current_value_text(item, power_str); + + item = variable_item_list_add(var_item_list, "Register Tag", 0, NULL, NULL); + + item = variable_item_list_add(var_item_list, "Show MAC", 2, findmy_scene_config_show_mac, app); + variable_item_set_current_value_index(item, app->state.show_mac); + variable_item_set_current_value_text(item, app->state.show_mac ? "Yes" : "No"); + + item = variable_item_list_add( + var_item_list, + "Matthew KuKanich, Thanks to Chapoly1305, WillyJL, OpenHaystack, Testers", + 1, + NULL, + NULL); + variable_item_set_current_value_text(item, "Credits"); + + variable_item_list_set_enter_callback(var_item_list, findmy_scene_config_callback, app); + + variable_item_list_set_selected_item( + var_item_list, scene_manager_get_scene_state(app->scene_manager, FindMySceneConfig)); + + view_dispatcher_switch_to_view(app->view_dispatcher, FindMyViewVarItemList); +} + +bool findmy_scene_config_on_event(void* context, SceneManagerEvent event) { + FindMy* app = context; + bool consumed = false; + + if(event.type == SceneManagerEventTypeCustom) { + scene_manager_set_scene_state(app->scene_manager, FindMySceneConfig, event.event); + consumed = true; + switch(event.event) { + case VarItemListIndexRegisterTag: + scene_manager_next_scene(app->scene_manager, FindMySceneConfigTagtype); + break; + case VarItemListIndexAbout: + break; + default: + break; + } + } + + return consumed; +} + +void findmy_scene_config_on_exit(void* context) { + FindMy* app = context; + VariableItemList* var_item_list = app->var_item_list; + + variable_item_list_reset(var_item_list); +} diff --git a/applications/system/find_my_flipper/scenes/findmy_scene_config_import.c b/applications/system/find_my_flipper/scenes/findmy_scene_config_import.c new file mode 100644 index 000000000..231741320 --- /dev/null +++ b/applications/system/find_my_flipper/scenes/findmy_scene_config_import.c @@ -0,0 +1,231 @@ +#include "../findmy_i.h" + +enum VarItemListIndex { + VarItemListIndexNrfConnect, + VarItemListIndexOpenHaystack, + VarItemListIndexRegisterTagManually, +}; + +static const char* parse_nrf_connect(FindMy* app, const char* path) { + const char* error = NULL; + + Stream* stream = file_stream_alloc(app->storage); + FuriString* line = furi_string_alloc(); + do { + // XX-XX-XX-XX-XX-XX_YYYY-MM-DD HH_MM_SS.txt + error = "Filename must\nhave MAC\naddress"; + uint8_t mac[EXTRA_BEACON_MAC_ADDR_SIZE]; + path_extract_filename_no_ext(path, line); + if(furi_string_size(line) < sizeof(mac) * 3 - 1) break; + error = NULL; + for(size_t i = 0; i < sizeof(mac); i++) { + char a = furi_string_get_char(line, i * 3); + char b = furi_string_get_char(line, i * 3 + 1); + if((a < 'A' && a > 'F') || (a < '0' && a > '9') || (b < 'A' && b > 'F') || + (b < '0' && b > '9') || !hex_char_to_uint8(a, b, &mac[i])) { + error = "Filename must\nhave MAC\naddress"; + break; + } + } + if(error) break; + findmy_reverse_mac_addr(mac); + + error = "Can't open file"; + if(!file_stream_open(stream, path, FSAM_READ, FSOM_OPEN_EXISTING)) break; + + // YYYY-MM-DD HH:MM:SS.ms, XX dBm, 0xXXXXX + error = "Wrong file format"; + if(!stream_read_line(stream, line)) break; + const char* marker = " dBm, 0x"; + size_t pos = furi_string_search(line, marker); + if(pos == FURI_STRING_FAILURE) break; + furi_string_right(line, pos + strlen(marker)); + furi_string_trim(line); + + error = "Wrong payload size"; + size_t line_size = furi_string_size(line); + uint8_t data_size = findmy_state_data_size(app->state.tag_type); + FURI_LOG_I("ImportPayload", "Line Size: %d", line_size); + FURI_LOG_I("ImportPayload", "Data Size: %d", data_size * 2); + if(line_size != data_size * 2) break; + // Initialize full data to 0's, then fill only first data_size bytes + uint8_t data[EXTRA_BEACON_MAX_DATA_SIZE] = {0}; + error = NULL; + for(size_t i = 0; i < data_size; i++) { + char a = furi_string_get_char(line, i * 2); + char b = furi_string_get_char(line, i * 2 + 1); + if((a < 'A' && a > 'F') || (a < '0' && a > '9') || (b < 'A' && b > 'F') || + (b < '0' && b > '9') || !hex_char_to_uint8(a, b, &data[i])) { + error = "Invalid payload"; + break; + } + } + if(error) break; + + memcpy(app->state.mac, mac, sizeof(app->state.mac)); + memcpy(app->state.data, data, sizeof(app->state.data)); + findmy_state_save_and_apply(&app->state); + + error = NULL; + + } while(false); + furi_string_free(line); + file_stream_close(stream); + stream_free(stream); + + return error; +} + +static const char* parse_open_haystack(FindMy* app, const char* path) { + const char* error = NULL; + + Stream* stream = file_stream_alloc(app->storage); + FuriString* line = furi_string_alloc(); + do { + error = "Can't open file"; + if(!file_stream_open(stream, path, FSAM_READ, FSOM_OPEN_EXISTING)) break; + + error = "Wrong file format"; + while(stream_read_line(stream, line)) { + if(furi_string_start_with(line, "Public key: ") || + furi_string_start_with(line, "Advertisement key: ")) { + error = NULL; + break; + } + } + if(error) break; + + furi_string_right(line, furi_string_search_char(line, ':') + 2); + furi_string_trim(line); + + error = "Base64 failed"; + size_t decoded_len; + uint8_t* public_key = base64_decode( + (uint8_t*)furi_string_get_cstr(line), furi_string_size(line), &decoded_len); + if(decoded_len != 28) { + free(public_key); + break; + } + + memcpy(app->state.mac, public_key, sizeof(app->state.mac)); + app->state.mac[0] |= 0b11000000; + findmy_reverse_mac_addr(app->state.mac); + + uint8_t advertisement_template[EXTRA_BEACON_MAX_DATA_SIZE] = { + 0x1e, // length (30) + 0xff, // manufacturer specific data + 0x4c, 0x00, // company ID (Apple) + 0x12, 0x19, // offline finding type and length + 0x00, //state + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, // first two bits of key[0] + 0x00, // hint + }; + memcpy(app->state.data, advertisement_template, sizeof(app->state.data)); + memcpy(&app->state.data[7], &public_key[6], decoded_len - 6); + app->state.data[29] = public_key[0] >> 6; + findmy_state_save_and_apply(&app->state); + + free(public_key); + error = NULL; + + } while(false); + furi_string_free(line); + file_stream_close(stream); + stream_free(stream); + + return error; +} + +void findmy_scene_config_import_callback(void* context, uint32_t index) { + furi_assert(context); + FindMy* app = context; + view_dispatcher_send_custom_event(app->view_dispatcher, index); +} + +void findmy_scene_config_import_on_enter(void* context) { + FindMy* app = context; + VariableItemList* var_item_list = app->var_item_list; + VariableItem* item; + + //variable_item_list_set_header(var_item_list, "Choose file type"); + + item = variable_item_list_add(var_item_list, "nRF Connect (.txt)", 0, NULL, NULL); + + item = variable_item_list_add(var_item_list, "OpenHaystack (.keys)", 0, NULL, NULL); + + item = variable_item_list_add(var_item_list, "Register Tag Manually", 0, NULL, NULL); + + // This scene acts more like a submenu than a var item list tbh + UNUSED(item); + + variable_item_list_set_enter_callback(var_item_list, findmy_scene_config_import_callback, app); + + variable_item_list_set_selected_item( + var_item_list, scene_manager_get_scene_state(app->scene_manager, FindMySceneConfigImport)); + + view_dispatcher_switch_to_view(app->view_dispatcher, FindMyViewVarItemList); +} + +bool findmy_scene_config_import_on_event(void* context, SceneManagerEvent event) { + FindMy* app = context; + bool consumed = false; + + if(event.type == SceneManagerEventTypeCustom) { + scene_manager_set_scene_state(app->scene_manager, FindMySceneConfigImport, event.event); + consumed = true; + + const char* extension = NULL; + switch(event.event) { + case VarItemListIndexNrfConnect: + extension = ".txt"; + break; + case VarItemListIndexOpenHaystack: + extension = ".keys"; + break; + case VarItemListIndexRegisterTagManually: + scene_manager_next_scene(app->scene_manager, FindMySceneConfigMac); + break; + default: + break; + } + if(!extension) { + return consumed; + } + + const DialogsFileBrowserOptions browser_options = { + .extension = extension, + .icon = &I_text_10px, + .base_path = FINDMY_STATE_DIR, + }; + storage_simply_mkdir(app->storage, browser_options.base_path); + FuriString* path = furi_string_alloc_set_str(browser_options.base_path); + if(dialog_file_browser_show(app->dialogs, path, path, &browser_options)) { + // The parse functions return the error text, or NULL for success + // Used in result to show success or error message + const char* error = NULL; + switch(event.event) { + case VarItemListIndexNrfConnect: + error = parse_nrf_connect(app, furi_string_get_cstr(path)); + break; + case VarItemListIndexOpenHaystack: + error = parse_open_haystack(app, furi_string_get_cstr(path)); + break; + } + scene_manager_set_scene_state( + app->scene_manager, FindMySceneConfigImportResult, (uint32_t)error); + scene_manager_next_scene(app->scene_manager, FindMySceneConfigImportResult); + } + furi_string_free(path); + } + + return consumed; +} + +void findmy_scene_config_import_on_exit(void* context) { + FindMy* app = context; + VariableItemList* var_item_list = app->var_item_list; + + variable_item_list_reset(var_item_list); +} diff --git a/applications/system/find_my_flipper/scenes/findmy_scene_config_import_result.c b/applications/system/find_my_flipper/scenes/findmy_scene_config_import_result.c new file mode 100644 index 000000000..933b6e611 --- /dev/null +++ b/applications/system/find_my_flipper/scenes/findmy_scene_config_import_result.c @@ -0,0 +1,60 @@ +#include "../findmy_i.h" + +enum PopupEvent { + PopupEventExit, +}; + +static void findmy_scene_config_import_result_callback(void* context) { + FindMy* app = context; + + view_dispatcher_send_custom_event(app->view_dispatcher, PopupEventExit); +} + +void findmy_scene_config_import_result_on_enter(void* context) { + FindMy* app = context; + Popup* popup = app->popup; + + const char* error = (const char*)scene_manager_get_scene_state( + app->scene_manager, FindMySceneConfigImportResult); + if(error) { + popup_set_icon(popup, 83, 22, &I_WarningDolphinFlip_45x42); + popup_set_header(popup, "Error!", 13, 22, AlignLeft, AlignBottom); + popup_set_text(popup, error, 6, 26, AlignLeft, AlignTop); + popup_disable_timeout(popup); + } else { + popup_set_icon(popup, 36, 5, &I_DolphinDone_80x58); + popup_set_header(popup, "Imported!", 7, 14, AlignLeft, AlignBottom); + popup_enable_timeout(popup); + } + popup_set_timeout(popup, 1500); + popup_set_context(popup, app); + popup_set_callback(popup, findmy_scene_config_import_result_callback); + findmy_main_update_active(app->findmy_main, app->state.beacon_active); + findmy_main_update_mac(app->findmy_main, app->state.mac); + findmy_main_update_type(app->findmy_main, app->state.tag_type); + view_dispatcher_switch_to_view(app->view_dispatcher, FindMyViewPopup); +} + +bool findmy_scene_config_import_result_on_event(void* context, SceneManagerEvent event) { + FindMy* app = context; + bool consumed = false; + + if(event.type == SceneManagerEventTypeCustom) { + consumed = true; + switch(event.event) { + case PopupEventExit: + scene_manager_search_and_switch_to_previous_scene( + app->scene_manager, FindMySceneConfig); + break; + default: + break; + } + } + + return consumed; +} + +void findmy_scene_config_import_result_on_exit(void* context) { + FindMy* app = context; + popup_reset(app->popup); +} diff --git a/applications/system/find_my_flipper/scenes/findmy_scene_config_mac.c b/applications/system/find_my_flipper/scenes/findmy_scene_config_mac.c new file mode 100644 index 000000000..7e5a1d0e3 --- /dev/null +++ b/applications/system/find_my_flipper/scenes/findmy_scene_config_mac.c @@ -0,0 +1,60 @@ +#include "../findmy_i.h" + +enum ByteInputResult { + ByteInputResultOk, +}; + +static void findmy_scene_config_mac_callback(void* context) { + FindMy* app = context; + + view_dispatcher_send_custom_event(app->view_dispatcher, ByteInputResultOk); +} + +void findmy_scene_config_mac_on_enter(void* context) { + FindMy* app = context; + ByteInput* byte_input = app->byte_input; + + byte_input_set_header_text(byte_input, "Enter Bluetooth MAC:"); + + memcpy(app->mac_buf, app->state.mac, sizeof(app->mac_buf)); + findmy_reverse_mac_addr(app->mac_buf); + + byte_input_set_result_callback( + byte_input, + findmy_scene_config_mac_callback, + NULL, + app, + app->mac_buf, + sizeof(app->mac_buf)); + + view_dispatcher_switch_to_view(app->view_dispatcher, FindMyViewByteInput); +} + +bool findmy_scene_config_mac_on_event(void* context, SceneManagerEvent event) { + FindMy* app = context; + bool consumed = false; + + if(event.type == SceneManagerEventTypeCustom) { + consumed = true; + switch(event.event) { + case ByteInputResultOk: + findmy_reverse_mac_addr(app->mac_buf); + memcpy(&app->state.mac, app->mac_buf, sizeof(app->state.mac)); + findmy_state_save_and_apply(&app->state); + findmy_main_update_mac(app->findmy_main, app->state.mac); + scene_manager_next_scene(app->scene_manager, FindMySceneConfigPacket); + break; + default: + break; + } + } + + return consumed; +} + +void findmy_scene_config_mac_on_exit(void* context) { + FindMy* app = context; + + byte_input_set_result_callback(app->byte_input, NULL, NULL, NULL, NULL, 0); + byte_input_set_header_text(app->byte_input, ""); +} diff --git a/applications/system/find_my_flipper/scenes/findmy_scene_config_packet.c b/applications/system/find_my_flipper/scenes/findmy_scene_config_packet.c new file mode 100644 index 000000000..06e30856e --- /dev/null +++ b/applications/system/find_my_flipper/scenes/findmy_scene_config_packet.c @@ -0,0 +1,58 @@ +#include "../findmy_i.h" + +enum ByteInputResult { + ByteInputResultOk, +}; + +static void findmy_scene_config_packet_callback(void* context) { + FindMy* app = context; + + view_dispatcher_send_custom_event(app->view_dispatcher, ByteInputResultOk); +} + +void findmy_scene_config_packet_on_enter(void* context) { + FindMy* app = context; + ByteInput* byte_input = app->byte_input; + + byte_input_set_header_text(byte_input, "Enter Bluetooth Payload:"); + + memcpy(app->packet_buf, app->state.data, findmy_state_data_size(app->state.tag_type)); + + byte_input_set_result_callback( + byte_input, + findmy_scene_config_packet_callback, + NULL, + app, + app->packet_buf, + findmy_state_data_size(app->state.tag_type)); + + view_dispatcher_switch_to_view(app->view_dispatcher, FindMyViewByteInput); +} + +bool findmy_scene_config_packet_on_event(void* context, SceneManagerEvent event) { + FindMy* app = context; + bool consumed = false; + + if(event.type == SceneManagerEventTypeCustom) { + consumed = true; + switch(event.event) { + case ByteInputResultOk: + scene_manager_search_and_switch_to_previous_scene( + app->scene_manager, FindMySceneConfig); + memcpy(app->state.data, app->packet_buf, findmy_state_data_size(app->state.tag_type)); + findmy_state_save_and_apply(&app->state); + break; + default: + break; + } + } + + return consumed; +} + +void findmy_scene_config_packet_on_exit(void* context) { + FindMy* app = context; + + byte_input_set_result_callback(app->byte_input, NULL, NULL, NULL, NULL, 0); + byte_input_set_header_text(app->byte_input, ""); +} diff --git a/applications/system/find_my_flipper/scenes/findmy_scene_config_tagtype.c b/applications/system/find_my_flipper/scenes/findmy_scene_config_tagtype.c new file mode 100644 index 000000000..51bc7f61c --- /dev/null +++ b/applications/system/find_my_flipper/scenes/findmy_scene_config_tagtype.c @@ -0,0 +1,70 @@ +#include "../findmy_i.h" + +enum VarItemListIndex { + VarItemListIndexApple, + VarItemListIndexSamsung, + VarItemListIndexTile, +}; + +void findmy_scene_config_tagtype_callback(void* context, uint32_t index) { + furi_assert(context); + FindMy* app = context; + view_dispatcher_send_custom_event(app->view_dispatcher, index); +} + +void findmy_scene_config_tagtype_on_enter(void* context) { + FindMy* app = context; + VariableItemList* var_item_list = app->var_item_list; + VariableItem* item; + + //variable_item_list_set_header(var_item_list, "Choose tag type"); + + item = variable_item_list_add(var_item_list, "Apple AirTag", 0, NULL, NULL); + + item = variable_item_list_add(var_item_list, "Samsung SmartTag", 0, NULL, NULL); + + item = variable_item_list_add(var_item_list, "Tile SmartTag", 0, NULL, NULL); + + UNUSED(item); + + variable_item_list_set_enter_callback( + var_item_list, findmy_scene_config_tagtype_callback, app); + + variable_item_list_set_selected_item( + var_item_list, scene_manager_get_scene_state(app->scene_manager, FindMySceneConfigImport)); + view_dispatcher_switch_to_view(app->view_dispatcher, FindMyViewVarItemList); +} + +bool findmy_scene_config_tagtype_on_event(void* context, SceneManagerEvent event) { + FindMy* app = context; + bool consumed = false; + + if(event.type == SceneManagerEventTypeCustom) { + scene_manager_set_scene_state(app->scene_manager, FindMySceneConfigTagtype, event.event); + consumed = true; + + switch(event.event) { + case VarItemListIndexApple: + findmy_set_tag_type(app, FindMyTypeApple); + break; + case VarItemListIndexSamsung: + findmy_set_tag_type(app, FindMyTypeSamsung); + break; + case VarItemListIndexTile: + findmy_set_tag_type(app, FindMyTypeTile); + break; + default: + break; + } + scene_manager_next_scene(app->scene_manager, FindMySceneConfigImport); + } + + return consumed; +} + +void findmy_scene_config_tagtype_on_exit(void* context) { + FindMy* app = context; + VariableItemList* var_item_list = app->var_item_list; + + variable_item_list_reset(var_item_list); +} diff --git a/applications/system/find_my_flipper/scenes/findmy_scene_main.c b/applications/system/find_my_flipper/scenes/findmy_scene_main.c new file mode 100644 index 000000000..a4e72bdf7 --- /dev/null +++ b/applications/system/find_my_flipper/scenes/findmy_scene_main.c @@ -0,0 +1,56 @@ +#include "../findmy_i.h" + +void findmy_scene_main_callback(FindMyMainEvent event, void* context) { + furi_assert(context); + FindMy* app = context; + view_dispatcher_send_custom_event(app->view_dispatcher, event); +} + +void findmy_scene_main_on_enter(void* context) { + FindMy* app = context; + + findmy_main_set_callback(app->findmy_main, findmy_scene_main_callback, app); + view_dispatcher_switch_to_view(app->view_dispatcher, FindMyViewMain); +} + +bool findmy_scene_main_on_event(void* context, SceneManagerEvent event) { + FindMy* app = context; + bool consumed = false; + + if(event.type == SceneManagerEventTypeCustom) { + consumed = true; + switch(event.event) { + case FindMyMainEventToggle: + findmy_toggle_beacon(app); + break; + case FindMyMainEventBackground: + app->state.beacon_active = true; + findmy_state_save_and_apply(&app->state); + view_dispatcher_stop(app->view_dispatcher); + break; + case FindMyMainEventConfig: + scene_manager_next_scene(app->scene_manager, FindMySceneConfig); + break; + case FindMyMainEventIntervalUp: + findmy_change_broadcast_interval(app, app->state.broadcast_interval + 1); + break; + case FindMyMainEventIntervalDown: + findmy_change_broadcast_interval(app, app->state.broadcast_interval - 1); + break; + case FindMyMainEventQuit: + app->state.beacon_active = false; + findmy_state_save_and_apply(&app->state); + break; + default: + consumed = false; + break; + } + } + + return consumed; +} + +void findmy_scene_main_on_exit(void* context) { + FindMy* app = context; + UNUSED(app); +} diff --git a/applications/system/find_my_flipper/scenes/findmy_scenes.h b/applications/system/find_my_flipper/scenes/findmy_scenes.h new file mode 100644 index 000000000..0680e2c5f --- /dev/null +++ b/applications/system/find_my_flipper/scenes/findmy_scenes.h @@ -0,0 +1,7 @@ +ADD_SCENE(findmy, main, Main) +ADD_SCENE(findmy, config, Config) +ADD_SCENE(findmy, config_import, ConfigImport) +ADD_SCENE(findmy, config_tagtype, ConfigTagtype) +ADD_SCENE(findmy, config_import_result, ConfigImportResult) +ADD_SCENE(findmy, config_mac, ConfigMac) +ADD_SCENE(findmy, config_packet, ConfigPacket) diff --git a/applications/system/find_my_flipper/screenshots/1.png b/applications/system/find_my_flipper/screenshots/1.png new file mode 100644 index 0000000000000000000000000000000000000000..e3511183fe106062306757beba87b4fdf665ca7a GIT binary patch literal 2253 zcmcImYfzI{8a|O)MC#H?$Axf_9S3lRrbW|4K?0OTC?W|rX@QD52!W&!TQ*JPnr_vZ zT~Tzbp@0N!mVyzI3JFBH1V@F1K*HrqQV19whD3u2Es&&12yB=-!ydj?LxRa%QM=aXa^EW(feh|F-#|p5^al z0a$&>MQvC$N98<<)r2 zqz8sqL7%z*SM4;bHeQ(Vr8lZK1zK3JrKPg1>QXdzy&nk zHTgszsT^_4A5v;Gr6k7)^|VSP>>woPz4gbH-2mLVG=>`GpX@@8m+5(2?c2N}@l!X^ zD-QKE@(oXwDk6~77rd*tvPZf^tY`nqT9kZM9AI7R@}m2jup!8jn zH}{jGL}kTip*W+cca$A$M{7}i?%^Av3h_lK)qnor!c5yWXV0zjp}$rb(gn>IN*?vW zQESl)eY^Ft9*X1Iz>Y0Sk4Cuu8bp~M*dO;t^iAv|&KJA`*``~=SbDTg>m~bGU-7gs z!>qoddpU@gB#1VyY~e~*Wko09f^d&E#)B zdv8eMhp|#jZ7Fh3Au^W{*)!g$;ZdS2t7H=vW2h^Nvwvo!4&tIH!~*k@kw)sxiv+~3}dHo{@FMSLpoSk2@*0Jm7r^q}@TFnem(O{JD{S&3SGMTiRIzQaH$h$}m z`sFEP=I8K@PVA#2y)kma1Xdfv%c3k`;{2qAbi?$@35>d}vim6Aj!_@5Tzd?g5nRvp zaLOK&R({H@yVFw4*UCPG=;KAWCZAktUcl1Yzcq&QQ>aKpQOPBB6ZYi+dw1x3>2p6w zf5^`|z*`DPT2a=UT6}8qYHu#a25uOC$3hCDk7Y%7sz`f9Y-t*Sf%Nlu7FJC-`zFq~ z=Maa>?nl#WB-V!GPzw4*;f!WjRLMGY0Al&S+RG5Z6%3#utIfXfrcn34X%qjSqrS}K zuBlv4v#mM1VLIchMtS4|2Q%I;QA$79BgneCU@rO0L8yHA8`E0~@qSYWB1Oo%G{OBz zekq?M#uxVnkw>RyfALtwLNHI3079wJv&vR1GDc7&7$S8=r)!EViMb-4p(%3juJ`3N zU<`N7WO+ZwoK47iHU!0RDyceaDGwpcbM+4hc7xS8g0HGiareiaN;PzQ>Ssb!0X%bO zR#X%8h~JA?_=bt#VB}FeK{MXz@LcuXH<}N%x32c>PiURg)!8}ox#NyH2#Q#FY5qnU#nd}hLW%6xPZe+ z(v^f@d0@oY`L#I!I%!(#DrcB`Cq^EGK)M$dVQ#bOp`2C3#8cPZceE^- zk@Yym&U-~aeUmBbrbPg+z&vTK7XE5q8!|atTo&E-dvQ*(aP>r5;%TR`tnQ`0=vdTg3RlN|- z{FXf?*2Y)#lL>r|oVgc(BJRXHJ>C7?0RW!+_a&qN;NpBlfekNiNzcjKowtpp`^Z!PHlKQNq2`XF zD*)KgxIf`NLd6xs?0XkxqI`ZeUfb~G>6z%WGQ{LZu9%2_^^^J6-bir_&9`QJ{rd8m zo;MF><0KB#?*Ltlodtli1%TZfLDdH?;MFE)wm1I!Yh{|`nL&fDyd{v)KIQkB{~KaW{0v!LqKf2{QAJOHmSYoH!6KH6X5e+ z`Y~iP09UV%p$J5yZwt-w&ua5Z|6FP%1zpj(ZFVZSeTU0Bl?g2%sovgOaGgD<x|;P}3R0%u=Pd2vPsjdi?yT;XM{-*R3N8h;o{`kEHZ5%#RuC$oArNj~guR7bd zSX2HWj<{DG^JPO{7i*_c5ydU~*0Ok4oH!%u=GsX8tMZbfYgmQtknoW-cjoE=Qp?ET zjI^l>lm`AZ!h$i>XeVWi!O6L{ScV8Z*ftVTMn%q$a1jqU3eru7l-adl5@^qo9IWm* z>b!LM)C@hj+YjP9Ux?}&r-u&j3+jrf++HZ@iO^RKeFF7?aXh^B0v@~6v;|m1D7*)bAcZU4n+sTY?S&e{l7ED_Kzo1mX$V50AZdC1P25%sgnTHWfE2Edg z*tW&%jJm`)99Kf)tJso@n>d$t!NPF*rdIBbp^cJdzP0k56s9(nh6$UYXNHY7=YB=2 zE8`Y+FPvuU9ilDy8|1e3ZRQ=F zW$lr!Oc+V{TOXs6-sd{m0?Fh*?eafX)c;>#zvR?hZuZdFZ3iuDGNXe~e&ULZdeX8a zb-IexYsst&y!Uq%-*S5G)Z?Wc@G5Xl@r-}R7S5sE{7iGYaeOEKAB|z!WB6F_AK(Tk z1LOB1aZ6kyNiP*FV~+GkRkG~52#owZ7wKaBULmPJPIY?JG&QACu(%`ltUnuI;CDRp zp`)`Aaa@jiss#DAyl-`)CjA+2^>ZCveU;Bd{1X%=o>CSH?V0@W)=#^3g<|Bx6s%@^ z%I+02vVjXz8d17DzNm<_1ZiDKyXHSED-M7$!<8=n@AR^43UQ+N-*+<&@ILpQEod^3-3m)cdG!k-xU~*nyvU%mL?p#P&=jPm6 zUv|61X%4xGzmJ8y+Ju0r@OG<41{u>6>jru*Hz18=oc9NsO#{@oZ$qa}+ji<*vJ^dd z5mHXW9EkSjj>UM-HCT%ktX{RT@EW$fZvu)Ja$FbW56jv|mWnNY3dg|^O_ptK_-^bl zFADC7vt#~bwjDm zDp}F&`e7EOTPY>U?1K@9%UBhO?x`p1&($5L^y@RQ%bVbl5UC)nAP+MjIOG&ojY&iH z_|qLsKpEE~G~+%(VTuL8X-LGJ8~D!Wr!RpeiF<6YK4AH2e{``j@C(S83H_8Vt~rl5+Vo^mJmoSYNNPd8t`zf{GXcsBMBxf+@QDC<8IQ4c}wuh7#kg~R<31(!H znUsyy;bFzNBC$0P)D{Au_9+0T$Q=*}p5Fm|K0idq}4rIe=SmoLy^)1IfE>hg}i6i}<6J$*`6kew3rmy*p+W0r?f|I7uR(_jq zl-U`MN6pok()IPc{aDr} zTb3rsEijm6j|>>Ct8zj~G#;9xqKE5KE*y@1=0~lVx4@XwxUH+@P5&VBlyu{v3_V{? zaxgBpFRmGzvh?&;VP&#h9W8E{fAmd@`;#d$>7|vu^vw4WhyuKzpCW5eVi1W;VGR`7 z!W3+hvx$&{0<+PGCE0dx?PxHS>RUD3jC1+#!QXvtm`4-kVwg`j9uXlm>!Rh0717@UKpgg7j8s8Wz2{*7(FbFxjP18zgV(3g1nE zNi36+ljn{D(^E=Kayy}<0okKjM#5Ep!li0jJUe-4K)_%xq%Bz2cdGj^=$-_jXdtcb zH(ZcjU-3MKxQ`6LF{MIE7Y$B^n1oBPSyAaG!Wd>0&-3hcdXwJX^hJ=MvSp0TYK{ZT z68C`+CSTBnaoCAOIVJ|7g2CZuHe>c-A>~VbJle(vPMQ~~*Ox3JB@)YbKD9wA@?P(* zSvv<#XVeoIgR>HY@-W-Rd#^A#Nx^H2bXJLC;y5Cy`j!#j`dMlSfIZX7$m}Rxf*9Wx zhtl@{2?>6huSpmU4tjRjyQ_z5Gm^t>_oGTuapKZABYFMWp>LO)TP}gw14_%w#uQdX zH~mj9osJ!#Qx&dgF*l*>?}D+qATuxs1Ai;lOdgcpaIe1%E35Wh*`V=PrwIW?L{9YX zxB>;v&7ggPbhjyXxQBYdzo=L$W5*kS4-P72oVx@)XlS;p?3@b2&eYVLX0a6!86;6v zRgmO1Mr^#wo)_L3Di(9CZ}D53tdVA5QB0ktM74rOz8uJsYwjy^nfH zM%Hl*4^5X=2;nx(LmNcqX~dXBO@iBpej-agHL*eoZ`Ex08ToT!2FCW>srNf0 zU+pl#2^pH>Vs=3kU*>DAFkuY+7?-i~xp*Z_uvm`Vk>@elUjIg`TO2(tY_({-hDa#u zx2DiJ9)%q&l9AxyXGmkS;O@NUU<6;|$}8?D3g~bVSDyT%j$rNDo4tQrfvtype) { + case FindMyTypeApple: + network_text = "Apple Network"; + break; + case FindMyTypeSamsung: + network_text = "Samsung Network"; + break; + case FindMyTypeTile: + network_text = "Tile Network"; + break; + default: + break; + } + + if(model->show_mac == false) { + canvas_set_font(canvas, FontPrimary); + canvas_draw_str(canvas, 4, 31, network_text); + canvas_draw_icon(canvas, 6 + canvas_string_width(canvas, network_text), 24, &I_Lock_7x8); + } else if(model->show_mac == true) { + canvas_set_font(canvas, FontSecondary); + char mac_str[23]; + snprintf( + mac_str, + sizeof(mac_str), + "MAC: %02X:%02X:%02X:%02X:%02X:%02X", + model->mac[0], + model->mac[1], + model->mac[2], + model->mac[3], + model->mac[4], + model->mac[5]); + canvas_draw_str(canvas, 4, 40, mac_str); + canvas_draw_str(canvas, 4, 30, network_text); + canvas_draw_icon(canvas, 6 + canvas_string_width(canvas, network_text), 23, &I_Lock_7x8); + } + canvas_set_font(canvas, FontSecondary); + if(model->active) { + canvas_draw_str(canvas, 4, 49, "Broadcast Active"); + canvas_draw_icon(canvas, 78, 41, &I_Ok_btn_9x9); + } else { + canvas_draw_str(canvas, 4, 49, "Broadcast Inactive"); + } + canvas_set_font(canvas, FontSecondary); + canvas_draw_str(canvas, 4, 21, "Press <- to run in background"); + canvas_set_font(canvas, FontSecondary); + char interval_str[20]; + snprintf(interval_str, sizeof(interval_str), "Ping Interval: %ds", model->interval); + canvas_draw_str(canvas, 4, 62, interval_str); + + canvas_set_font(canvas, FontSecondary); + canvas_draw_str(canvas, 100, 61, "Config"); + canvas_draw_line(canvas, 100, 51, 127, 51); + canvas_draw_line(canvas, 97, 53, 97, 63); + canvas_draw_line(canvas, 97, 53, 99, 51); + canvas_draw_line(canvas, 3, 52, 87, 52); +} + +static bool findmy_main_input_callback(InputEvent* event, void* context) { + furi_assert(context); + FindMyMain* findmy_main = context; + bool consumed = false; + + if(event->type == InputTypePress) { + consumed = true; + FindMyMainEvent cb_event; + + switch(event->key) { + case InputKeyBack: + cb_event = FindMyMainEventQuit; + break; + case InputKeyOk: + cb_event = FindMyMainEventToggle; + break; + case InputKeyLeft: + cb_event = FindMyMainEventBackground; + break; + case InputKeyRight: + cb_event = FindMyMainEventConfig; + break; + case InputKeyUp: + cb_event = FindMyMainEventIntervalUp; + break; + case InputKeyDown: + cb_event = FindMyMainEventIntervalDown; + break; + default: + return consumed; + } + + findmy_main->callback(cb_event, findmy_main->context); + } + + return consumed; +} + +FindMyMain* findmy_main_alloc(FindMy* app) { + FindMyMain* findmy_main = malloc(sizeof(FindMyMain)); + + findmy_main->view = view_alloc(); + view_allocate_model(findmy_main->view, ViewModelTypeLocking, sizeof(FindMyMainModel)); + with_view_model( + findmy_main->view, + FindMyMainModel * model, + { + model->active = app->state.beacon_active; + model->interval = app->state.broadcast_interval; + model->show_mac = app->state.show_mac; + memcpy(model->mac, app->state.mac, sizeof(model->mac)); + model->type = app->state.tag_type; + }, + false); + view_set_context(findmy_main->view, findmy_main); + view_set_draw_callback(findmy_main->view, findmy_main_draw_callback); + view_set_input_callback(findmy_main->view, findmy_main_input_callback); + + return findmy_main; +} + +void findmy_main_free(FindMyMain* findmy_main) { + furi_assert(findmy_main); + view_free(findmy_main->view); + free(findmy_main); +} + +View* findmy_main_get_view(FindMyMain* findmy_main) { + furi_assert(findmy_main); + return findmy_main->view; +} + +void findmy_main_set_callback(FindMyMain* findmy_main, FindMyMainCallback callback, void* context) { + furi_assert(findmy_main); + furi_assert(callback); + findmy_main->callback = callback; + findmy_main->context = context; +} + +void findmy_main_update_active(FindMyMain* findmy_main, bool active) { + furi_assert(findmy_main); + with_view_model(findmy_main->view, FindMyMainModel * model, { model->active = active; }, true); +} + +void findmy_main_toggle_mac(FindMyMain* findmy_main, bool show_mac) { + furi_assert(findmy_main); + with_view_model( + findmy_main->view, FindMyMainModel * model, { model->show_mac = show_mac; }, true); +} + +void findmy_main_update_mac(FindMyMain* findmy_main, uint8_t* mac) { + with_view_model( + findmy_main->view, + FindMyMainModel * model, + { + memcpy(model->mac, mac, sizeof(model->mac)); + findmy_reverse_mac_addr(model->mac); + }, + true); +} +void findmy_main_update_interval(FindMyMain* findmy_main, uint8_t interval) { + furi_assert(findmy_main); + with_view_model( + findmy_main->view, FindMyMainModel * model, { model->interval = interval; }, true); +} + +void findmy_main_update_type(FindMyMain* findmy_main, FindMyType type) { + furi_assert(findmy_main); + with_view_model(findmy_main->view, FindMyMainModel * model, { model->type = type; }, true); +} diff --git a/applications/system/find_my_flipper/views/findmy_main.h b/applications/system/find_my_flipper/views/findmy_main.h new file mode 100644 index 000000000..5680fa641 --- /dev/null +++ b/applications/system/find_my_flipper/views/findmy_main.h @@ -0,0 +1,32 @@ +#pragma once + +#include "../findmy.h" +#include "../findmy_state.h" +#include + +typedef enum { + FindMyMainEventToggle, + FindMyMainEventBackground, + FindMyMainEventConfig, + FindMyMainEventIntervalUp, + FindMyMainEventIntervalDown, + FindMyMainEventQuit, +} FindMyMainEvent; + +typedef struct FindMyMain FindMyMain; +typedef void (*FindMyMainCallback)(FindMyMainEvent event, void* context); + +// Main functionality +FindMyMain* findmy_main_alloc(FindMy* app); +void findmy_main_free(FindMyMain* findmy_main); +View* findmy_main_get_view(FindMyMain* findmy_main); + +// To communicate with scene +void findmy_main_set_callback(FindMyMain* findmy_main, FindMyMainCallback callback, void* context); + +// To redraw when info changes +void findmy_main_update_active(FindMyMain* findmy_main, bool active); +void findmy_main_update_interval(FindMyMain* findmy_main, uint8_t interval); +void findmy_main_toggle_mac(FindMyMain* findmy_main, bool show_mac); +void findmy_main_update_mac(FindMyMain* findmy_main, uint8_t* mac); +void findmy_main_update_type(FindMyMain* findmy_main, FindMyType type); diff --git a/applications/system/js_app/js_modules.h b/applications/system/js_app/js_modules.h index 7aadd8c6b..607564ccd 100644 --- a/applications/system/js_app/js_modules.h +++ b/applications/system/js_app/js_modules.h @@ -12,7 +12,7 @@ #define JS_SDK_VENDOR_FIRMWARE "unleashed" #define JS_SDK_VENDOR "flipperdevices" #define JS_SDK_MAJOR 0 -#define JS_SDK_MINOR 2 +#define JS_SDK_MINOR 2 /** * @brief Returns the foreign pointer in `obj["_"]` From d4830270a484c9caa9260be9a043b22449df0e11 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Thu, 13 Feb 2025 20:14:59 +0300 Subject: [PATCH 036/268] fix --- applications/system/js_app/js_modules.c | 1 - 1 file changed, 1 deletion(-) diff --git a/applications/system/js_app/js_modules.c b/applications/system/js_app/js_modules.c index 2672d9977..bcf309f54 100644 --- a/applications/system/js_app/js_modules.c +++ b/applications/system/js_app/js_modules.c @@ -277,7 +277,6 @@ static const char* extra_features[] = { "subghz", "usbdisk", "vgm", - "widget", }; /** From dea16b7055c0bfc61c6ca243201782ad706225d6 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Thu, 13 Feb 2025 20:20:25 +0300 Subject: [PATCH 037/268] add missing api [ci skip] --- lib/toolbox/SConscript | 1 + lib/toolbox/run_parallel.c | 17 +++++++++++++++++ lib/toolbox/run_parallel.h | 12 ++++++++++++ targets/f7/api_symbols.csv | 2 ++ 4 files changed, 32 insertions(+) create mode 100644 lib/toolbox/run_parallel.c create mode 100644 lib/toolbox/run_parallel.h diff --git a/lib/toolbox/SConscript b/lib/toolbox/SConscript index 0381ce5b9..42ccc9207 100644 --- a/lib/toolbox/SConscript +++ b/lib/toolbox/SConscript @@ -40,6 +40,7 @@ env.Append( File("pulse_protocols/pulse_glue.h"), File("md5_calc.h"), File("varint.h"), + File("run_parallel.h"), ], ) diff --git a/lib/toolbox/run_parallel.c b/lib/toolbox/run_parallel.c new file mode 100644 index 000000000..439949cf4 --- /dev/null +++ b/lib/toolbox/run_parallel.c @@ -0,0 +1,17 @@ +#include "run_parallel.h" + +#include + +static void run_parallel_thread_state(FuriThread* thread, FuriThreadState state, void* context) { + UNUSED(context); + + if(state == FuriThreadStateStopped) { + furi_thread_free(thread); + } +} + +void run_parallel(FuriThreadCallback callback, void* context, uint32_t stack_size) { + FuriThread* thread = furi_thread_alloc_ex(NULL, stack_size, callback, context); + furi_thread_set_state_callback(thread, run_parallel_thread_state); + furi_thread_start(thread); +} diff --git a/lib/toolbox/run_parallel.h b/lib/toolbox/run_parallel.h new file mode 100644 index 000000000..2722c54eb --- /dev/null +++ b/lib/toolbox/run_parallel.h @@ -0,0 +1,12 @@ +#pragma once + +#include + +/** + * @brief Run function in thread, then automatically clean up thread. + * + * @param[in] callback pointer to a function to be executed in parallel + * @param[in] context pointer to a user-specified object (will be passed to the callback) + * @param[in] stack_size stack size in bytes + */ +void run_parallel(FuriThreadCallback callback, void* context, uint32_t stack_size); diff --git a/targets/f7/api_symbols.csv b/targets/f7/api_symbols.csv index 65f8d74e7..9175f8ade 100644 --- a/targets/f7/api_symbols.csv +++ b/targets/f7/api_symbols.csv @@ -241,6 +241,7 @@ Header,+,lib/toolbox/pipe.h,, Header,+,lib/toolbox/pretty_format.h,, Header,+,lib/toolbox/protocols/protocol_dict.h,, Header,+,lib/toolbox/pulse_protocols/pulse_glue.h,, +Header,+,lib/toolbox/run_parallel.h,, Header,+,lib/toolbox/saved_struct.h,, Header,+,lib/toolbox/simple_array.h,, Header,+,lib/toolbox/stream/buffered_file_stream.h,, @@ -3155,6 +3156,7 @@ Function,+,rpc_system_app_set_callback,void,"RpcAppSystem*, RpcAppSystemCallback Function,+,rpc_system_app_set_error_code,void,"RpcAppSystem*, uint32_t" Function,+,rpc_system_app_set_error_text,void,"RpcAppSystem*, const char*" Function,-,rpmatch,int,const char* +Function,+,run_parallel,void,"FuriThreadCallback, void*, uint32_t" Function,+,saved_struct_get_metadata,_Bool,"const char*, uint8_t*, uint8_t*, size_t*" Function,+,saved_struct_load,_Bool,"const char*, void*, size_t, uint8_t, uint8_t" Function,+,saved_struct_save,_Bool,"const char*, const void*, size_t, uint8_t, uint8_t" From 7e1dae67a3e8f2f8a04e5dd83ecb8bb83059341b Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Thu, 13 Feb 2025 20:24:24 +0300 Subject: [PATCH 038/268] remove from api [ci skip] --- lib/toolbox/SConscript | 1 - targets/f7/api_symbols.csv | 2 -- 2 files changed, 3 deletions(-) diff --git a/lib/toolbox/SConscript b/lib/toolbox/SConscript index 42ccc9207..0381ce5b9 100644 --- a/lib/toolbox/SConscript +++ b/lib/toolbox/SConscript @@ -40,7 +40,6 @@ env.Append( File("pulse_protocols/pulse_glue.h"), File("md5_calc.h"), File("varint.h"), - File("run_parallel.h"), ], ) diff --git a/targets/f7/api_symbols.csv b/targets/f7/api_symbols.csv index 9175f8ade..65f8d74e7 100644 --- a/targets/f7/api_symbols.csv +++ b/targets/f7/api_symbols.csv @@ -241,7 +241,6 @@ Header,+,lib/toolbox/pipe.h,, Header,+,lib/toolbox/pretty_format.h,, Header,+,lib/toolbox/protocols/protocol_dict.h,, Header,+,lib/toolbox/pulse_protocols/pulse_glue.h,, -Header,+,lib/toolbox/run_parallel.h,, Header,+,lib/toolbox/saved_struct.h,, Header,+,lib/toolbox/simple_array.h,, Header,+,lib/toolbox/stream/buffered_file_stream.h,, @@ -3156,7 +3155,6 @@ Function,+,rpc_system_app_set_callback,void,"RpcAppSystem*, RpcAppSystemCallback Function,+,rpc_system_app_set_error_code,void,"RpcAppSystem*, uint32_t" Function,+,rpc_system_app_set_error_text,void,"RpcAppSystem*, const char*" Function,-,rpmatch,int,const char* -Function,+,run_parallel,void,"FuriThreadCallback, void*, uint32_t" Function,+,saved_struct_get_metadata,_Bool,"const char*, uint8_t*, uint8_t*, size_t*" Function,+,saved_struct_load,_Bool,"const char*, void*, size_t, uint8_t, uint8_t" Function,+,saved_struct_save,_Bool,"const char*, const void*, size_t, uint8_t, uint8_t" From 96dc1af1e81529d0eccb4b8dc204c00d60d2c385 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Thu, 13 Feb 2025 20:27:31 +0300 Subject: [PATCH 039/268] upd changelog --- CHANGELOG.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 025ff31f0..2fd1541e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,16 @@ ## Main changes -- Current API: 79.4 +- Current API: 80.1 * OFW: LFRFID - **EM4305 support** +* OFW: NFC - Added naming for DESFire cards + fix MF3ICD40 cards unable to be read +* Apps: Add FindMyFlipper to system apps and allow autostart on system boot [app by @MatthewKuKanich](https://github.com/MatthewKuKanich/FindMyFlipper) and autoloader by @Willy-JL - to use app please check how to add keys in [app repo](https://github.com/MatthewKuKanich/FindMyFlipper) * Input: Vibro on Button press option (PR #867 | by @Dmitry422) * Power: Option to limit battery charging (suppress charging on selected charge level) (PR #867 | by @Dmitry422) * Apps: **Check out more Apps updates and fixes by following** [this link](https://github.com/xMasterX/all-the-plugins/commits/dev) ## Other changes * Anims: Disable winter anims +* OFW: nfc: Enable MFUL sync poller to be provided with passwords +* OFW: ST25TB poller mode check +* OFW: JS features & bugfixes (SDK 0.2) **Existing Widget JS module was removed and replaced with new ofw gui/widget module, old apps using widget may be incompatible now!** * OFW: Infrared: increase max carrier limit * OFW: Ensure that `furi_record_create` is passed a non-NULL data pointer * OFW: Update mbedtls & expose AES From 9f1eca22609cf453462814b7a6b5bd764c18f44e Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Sun, 16 Feb 2025 20:49:54 +0300 Subject: [PATCH 040/268] upd readme [ci skip] badge counters are broken --- ReadMe.md | 9 --------- 1 file changed, 9 deletions(-) diff --git a/ReadMe.md b/ReadMe.md index f01dd4898..5227f9805 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -7,15 +7,6 @@ Discord server - - EN TG channel - - - RU TG channel - - - UA TG channel - ### Welcome to the Flipper Zero Unleashed Firmware repo! From 4895ae5d0d390b5a5cc0ff6e707e313d903b7d3a Mon Sep 17 00:00:00 2001 From: Anna Antonenko Date: Mon, 17 Feb 2025 01:57:23 +0400 Subject: [PATCH 041/268] [FL-3957] EventLoop unsubscribe fix (#4109) * Fix memory leak during event loop unsubscription * Event better memory leak fix * unit test for the fix Co-authored-by: Georgii Surkov --- .../tests/furi/furi_event_loop_test.c | 49 +++++++++++++++++++ .../debug/unit_tests/tests/furi/furi_test.c | 6 +++ furi/core/event_loop.c | 29 +++++------ furi/core/event_loop_i.h | 4 +- 4 files changed, 72 insertions(+), 16 deletions(-) diff --git a/applications/debug/unit_tests/tests/furi/furi_event_loop_test.c b/applications/debug/unit_tests/tests/furi/furi_event_loop_test.c index 73f38ab77..08e0e8ad2 100644 --- a/applications/debug/unit_tests/tests/furi/furi_event_loop_test.c +++ b/applications/debug/unit_tests/tests/furi/furi_event_loop_test.c @@ -446,6 +446,55 @@ static int32_t test_furi_event_loop_consumer(void* p) { return 0; } +typedef struct { + FuriEventLoop* event_loop; + FuriSemaphore* semaphore; + size_t counter; +} SelfUnsubTestTimerContext; + +static void test_self_unsub_semaphore_callback(FuriEventLoopObject* object, void* context) { + furi_event_loop_unsubscribe(context, object); // shouldn't crash here +} + +static void test_self_unsub_timer_callback(void* arg) { + SelfUnsubTestTimerContext* context = arg; + + if(context->counter == 0) { + furi_semaphore_release(context->semaphore); + } else if(context->counter == 1) { + furi_event_loop_stop(context->event_loop); + } + + context->counter++; +} + +void test_furi_event_loop_self_unsubscribe(void) { + FuriEventLoop* event_loop = furi_event_loop_alloc(); + + FuriSemaphore* semaphore = furi_semaphore_alloc(1, 0); + furi_event_loop_subscribe_semaphore( + event_loop, + semaphore, + FuriEventLoopEventIn, + test_self_unsub_semaphore_callback, + event_loop); + + SelfUnsubTestTimerContext timer_context = { + .event_loop = event_loop, + .semaphore = semaphore, + .counter = 0, + }; + FuriEventLoopTimer* timer = furi_event_loop_timer_alloc( + event_loop, test_self_unsub_timer_callback, FuriEventLoopTimerTypePeriodic, &timer_context); + furi_event_loop_timer_start(timer, furi_ms_to_ticks(20)); + + furi_event_loop_run(event_loop); + + furi_event_loop_timer_free(timer); + furi_semaphore_free(semaphore); + furi_event_loop_free(event_loop); +} + void test_furi_event_loop(void) { TestFuriEventLoopData data = {}; diff --git a/applications/debug/unit_tests/tests/furi/furi_test.c b/applications/debug/unit_tests/tests/furi/furi_test.c index f23be37a9..03d49aa7e 100644 --- a/applications/debug/unit_tests/tests/furi/furi_test.c +++ b/applications/debug/unit_tests/tests/furi/furi_test.c @@ -8,6 +8,7 @@ void test_furi_concurrent_access(void); void test_furi_pubsub(void); void test_furi_memmgr(void); void test_furi_event_loop(void); +void test_furi_event_loop_self_unsubscribe(void); void test_errno_saving(void); void test_furi_primitives(void); void test_stdin(void); @@ -46,6 +47,10 @@ MU_TEST(mu_test_furi_event_loop) { test_furi_event_loop(); } +MU_TEST(mu_test_furi_event_loop_self_unsubscribe) { + test_furi_event_loop_self_unsubscribe(); +} + MU_TEST(mu_test_errno_saving) { test_errno_saving(); } @@ -68,6 +73,7 @@ MU_TEST_SUITE(test_suite) { MU_RUN_TEST(mu_test_furi_pubsub); MU_RUN_TEST(mu_test_furi_memmgr); MU_RUN_TEST(mu_test_furi_event_loop); + MU_RUN_TEST(mu_test_furi_event_loop_self_unsubscribe); MU_RUN_TEST(mu_test_stdio); MU_RUN_TEST(mu_test_errno_saving); MU_RUN_TEST(mu_test_furi_primitives); diff --git a/furi/core/event_loop.c b/furi/core/event_loop.c index c0998ea90..e09be0ca4 100644 --- a/furi/core/event_loop.c +++ b/furi/core/event_loop.c @@ -32,14 +32,6 @@ static void furi_event_loop_item_notify(FuriEventLoopItem* instance); static bool furi_event_loop_item_is_waiting(FuriEventLoopItem* instance); -static void furi_event_loop_process_pending_callbacks(FuriEventLoop* instance) { - for(; !PendingQueue_empty_p(instance->pending_queue); - PendingQueue_pop_back(NULL, instance->pending_queue)) { - const FuriEventLoopPendingQueueItem* item = PendingQueue_back(instance->pending_queue); - item->callback(item->context); - } -} - static bool furi_event_loop_signal_callback(uint32_t signal, void* arg, void* context) { furi_assert(context); FuriEventLoop* instance = context; @@ -130,12 +122,16 @@ static inline FuriEventLoopProcessStatus furi_event_loop_unsubscribe(instance, item->object); } + instance->current_item = item; + if(item->event & FuriEventLoopEventFlagEdge) { status = furi_event_loop_process_edge_event(item); } else { status = furi_event_loop_process_level_event(item); } + instance->current_item = NULL; + if(item->owner == NULL) { status = FuriEventLoopProcessStatusFreeLater; } @@ -193,6 +189,14 @@ static void furi_event_loop_process_waiting_list(FuriEventLoop* instance) { furi_event_loop_sync_flags(instance); } +static void furi_event_loop_process_pending_callbacks(FuriEventLoop* instance) { + for(; !PendingQueue_empty_p(instance->pending_queue); + PendingQueue_pop_back(NULL, instance->pending_queue)) { + const FuriEventLoopPendingQueueItem* item = PendingQueue_back(instance->pending_queue); + item->callback(item->context); + } +} + static void furi_event_loop_restore_flags(FuriEventLoop* instance, uint32_t flags) { if(flags) { xTaskNotifyIndexed( @@ -203,7 +207,6 @@ static void furi_event_loop_restore_flags(FuriEventLoop* instance, uint32_t flag void furi_event_loop_run(FuriEventLoop* instance) { furi_check(instance); furi_check(instance->thread_id == furi_thread_get_current_id()); - FuriThread* thread = furi_thread_get_current(); // Set the default signal callback if none was previously set @@ -213,9 +216,9 @@ void furi_event_loop_run(FuriEventLoop* instance) { furi_event_loop_init_tick(instance); - while(true) { - instance->state = FuriEventLoopStateIdle; + instance->state = FuriEventLoopStateRunning; + while(true) { const TickType_t ticks_to_sleep = MIN(furi_event_loop_get_timer_wait_time(instance), furi_event_loop_get_tick_wait_time(instance)); @@ -224,8 +227,6 @@ void furi_event_loop_run(FuriEventLoop* instance) { BaseType_t ret = xTaskNotifyWaitIndexed( FURI_EVENT_LOOP_FLAG_NOTIFY_INDEX, 0, FuriEventLoopFlagAll, &flags, ticks_to_sleep); - instance->state = FuriEventLoopStateProcessing; - if(ret == pdTRUE) { if(flags & FuriEventLoopFlagStop) { instance->state = FuriEventLoopStateStopped; @@ -448,7 +449,7 @@ void furi_event_loop_unsubscribe(FuriEventLoop* instance, FuriEventLoopObject* o WaitingList_unlink(item); } - if(instance->state == FuriEventLoopStateProcessing) { + if(instance->current_item == item) { furi_event_loop_item_free_later(item); } else { furi_event_loop_item_free(item); diff --git a/furi/core/event_loop_i.h b/furi/core/event_loop_i.h index 7016e1e1b..ef2774b97 100644 --- a/furi/core/event_loop_i.h +++ b/furi/core/event_loop_i.h @@ -64,8 +64,7 @@ typedef enum { typedef enum { FuriEventLoopStateStopped, - FuriEventLoopStateIdle, - FuriEventLoopStateProcessing, + FuriEventLoopStateRunning, } FuriEventLoopState; typedef struct { @@ -81,6 +80,7 @@ struct FuriEventLoop { // Poller state volatile FuriEventLoopState state; + volatile FuriEventLoopItem* current_item; // Event handling FuriEventLoopTree_t tree; From 17417a487ad851f372b84ff2ffaf904f7947c31b Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Mon, 17 Feb 2025 20:16:26 +0300 Subject: [PATCH 042/268] upd changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2fd1541e4..ea88a52f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ * Apps: **Check out more Apps updates and fixes by following** [this link](https://github.com/xMasterX/all-the-plugins/commits/dev) ## Other changes * Anims: Disable winter anims +* OFW: EventLoop unsubscribe fix * OFW: nfc: Enable MFUL sync poller to be provided with passwords * OFW: ST25TB poller mode check * OFW: JS features & bugfixes (SDK 0.2) **Existing Widget JS module was removed and replaced with new ofw gui/widget module, old apps using widget may be incompatible now!** From 3a42bf812d391999d045d998eef036f1093690f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=82=E3=81=8F?= Date: Tue, 18 Feb 2025 03:16:14 +0900 Subject: [PATCH 043/268] Furi, USB, BLE, Debug: various bug fixes and improvements (#4114) * Furi, USB, BLE: extra stack space for some threads, small code cleanup. * Furi: thread watermark check on exit, explicitly crash if built with LIB_DEBUG=1 * Debug: color logging in apps/furi gdb helper, check and show crash message in gdb console. --- applications/main/gpio/usb_uart_bridge.c | 7 ++- furi/core/thread.c | 14 +++++ scripts/debug/flipperapps.py | 72 +++++++++++++++++++----- targets/f7/ble_glue/ble_event_thread.c | 2 +- targets/f7/furi_hal/furi_hal_usb_cdc.c | 15 +++-- 5 files changed, 87 insertions(+), 23 deletions(-) diff --git a/applications/main/gpio/usb_uart_bridge.c b/applications/main/gpio/usb_uart_bridge.c index f6e68b109..e6b71cb34 100644 --- a/applications/main/gpio/usb_uart_bridge.c +++ b/applications/main/gpio/usb_uart_bridge.c @@ -183,7 +183,7 @@ static int32_t usb_uart_worker(void* context) { usb_uart->usb_mutex = furi_mutex_alloc(FuriMutexTypeNormal); usb_uart->tx_thread = - furi_thread_alloc_ex("UsbUartTxWorker", 512, usb_uart_tx_thread, usb_uart); + furi_thread_alloc_ex("UsbUartTxWorker", 768, usb_uart_tx_thread, usb_uart); usb_uart_vcp_init(usb_uart, usb_uart->cfg.vcp_ch); usb_uart_serial_init(usb_uart, usb_uart->cfg.uart_ch); @@ -288,8 +288,6 @@ static int32_t usb_uart_worker(void* context) { usb_uart_update_ctrl_lines(usb_uart); } } - usb_uart_vcp_deinit(usb_uart, usb_uart->cfg.vcp_ch); - usb_uart_serial_deinit(usb_uart); furi_hal_gpio_init(USB_USART_DE_RE_PIN, GpioModeAnalog, GpioPullNo, GpioSpeedLow); @@ -302,6 +300,9 @@ static int32_t usb_uart_worker(void* context) { furi_thread_join(usb_uart->tx_thread); furi_thread_free(usb_uart->tx_thread); + usb_uart_vcp_deinit(usb_uart, usb_uart->cfg.vcp_ch); + usb_uart_serial_deinit(usb_uart); + furi_stream_buffer_free(usb_uart->rx_stream); furi_mutex_free(usb_uart->usb_mutex); furi_semaphore_free(usb_uart->tx_sem); diff --git a/furi/core/thread.c b/furi/core/thread.c index 6e5157957..f6cfa1d36 100644 --- a/furi/core/thread.c +++ b/furi/core/thread.c @@ -23,6 +23,8 @@ #define THREAD_MAX_STACK_SIZE (UINT16_MAX * sizeof(StackType_t)) +#define THREAD_STACK_WATERMARK_MIN (256u) + typedef struct { FuriThreadStdoutWriteCallback write_callback; FuriString* buffer; @@ -115,6 +117,18 @@ static void furi_thread_body(void* context) { furi_check(!thread->is_service, "Service threads MUST NOT return"); + size_t stack_watermark = furi_thread_get_stack_space(thread); + if(stack_watermark < THREAD_STACK_WATERMARK_MIN) { +#ifdef FURI_DEBUG + furi_crash("Stack watermark is dangerously low"); +#endif + FURI_LOG_E( //-V779 + thread->name ? thread->name : "Thread", + "Stack watermark is too low %zu < " STRINGIFY( + THREAD_STACK_WATERMARK_MIN) ". Increase stack size.", + stack_watermark); + } + if(thread->heap_trace_enabled == true) { furi_delay_ms(33); thread->heap_size = memmgr_heap_get_thread_memory((FuriThreadId)thread); diff --git a/scripts/debug/flipperapps.py b/scripts/debug/flipperapps.py index 81aa43c34..6d2d4c0a9 100644 --- a/scripts/debug/flipperapps.py +++ b/scripts/debug/flipperapps.py @@ -7,6 +7,33 @@ import zlib import gdb +class bcolors: + HEADER = "\033[95m" + OKBLUE = "\033[94m" + OKCYAN = "\033[96m" + OKGREEN = "\033[92m" + WARNING = "\033[93m" + FAIL = "\033[91m" + ENDC = "\033[0m" + BOLD = "\033[1m" + UNDERLINE = "\033[4m" + + +LOG_PREFIX = "[FURI]" + + +def error(line): + print(f"{bcolors.FAIL}{LOG_PREFIX} {line}{bcolors.ENDC}") + + +def warning(line): + print(f"{bcolors.WARNING}{LOG_PREFIX} {line}{bcolors.ENDC}") + + +def info(line): + print(f"{bcolors.OKGREEN}{LOG_PREFIX} {line}{bcolors.ENDC}") + + def get_file_crc32(filename): with open(filename, "rb") as f: return zlib.crc32(f.read()) @@ -39,12 +66,12 @@ class AppState: def is_debug_available(self) -> bool: have_debug_info = bool(self.debug_link_elf and self.debug_link_crc) if not have_debug_info: - print("No debug info available for this app") + warning("No debug info available for this app") return False debug_elf_path = self.get_original_elf_path() debug_elf_crc32 = get_file_crc32(debug_elf_path) if self.debug_link_crc != debug_elf_crc32: - print( + warning( f"Debug info ({debug_elf_path}) CRC mismatch: {self.debug_link_crc:08x} != {debug_elf_crc32:08x}, rebuild app" ) return False @@ -52,7 +79,7 @@ class AppState: def get_gdb_load_command(self) -> str: load_path = self.get_original_elf_path() - print(f"Loading debug information from {load_path}") + info(f"Loading debug information from {load_path}") load_command = ( f"add-symbol-file -readnow {load_path} 0x{self.text_address:08x} " ) @@ -121,12 +148,12 @@ class SetFapDebugElfRoot(gdb.Command): AppState.DEBUG_ELF_ROOT = arg try: global helper - print(f"Set '{arg}' as debug info lookup path for Flipper external apps") + info(f"Set '{arg}' as debug info lookup path for Flipper external apps") helper.attach_to_fw() gdb.events.stop.connect(helper.handle_stop) gdb.events.gdb_exiting.connect(helper.handle_exit) except gdb.error as e: - print(f"Support for Flipper external apps debug is not available: {e}") + error(f"Support for Flipper external apps debug is not available: {e}") class FlipperAppStateHelper: @@ -148,13 +175,29 @@ class FlipperAppStateHelper: gdb.execute(command) return True except gdb.error as e: - print(f"Failed to execute GDB command '{command}': {e}") + error(f"Failed to execute GDB command '{command}': {e}") return False + def _get_crash_message(self): + message = self.app_check_message.value() + if message == 1: + return "furi_assert failed" + elif message == 2: + return "furi_check failed" + else: + return message + def _sync_apps(self) -> None: + crash_message = self._get_crash_message() + if crash_message: + crash_message = f"! System crashed: {crash_message} !" + error("!" * len(crash_message)) + error(crash_message) + error("!" * len(crash_message)) + self.set_debug_mode(True) if not (app_list := self.app_list_ptr.value()): - print("Reset app loader state") + info("Reset app loader state") for app in self._current_apps: self._exec_gdb_command(app.get_gdb_unload_command()) self._current_apps = [] @@ -167,22 +210,23 @@ class FlipperAppStateHelper: for app in self._current_apps.copy(): if app.entry_address not in loaded_apps: - print(f"Application {app.name} is no longer loaded") + warning(f"Application {app.name} is no longer loaded") if not self._exec_gdb_command(app.get_gdb_unload_command()): - print(f"Failed to unload debug info for {app.name}") + error(f"Failed to unload debug info for {app.name}") self._current_apps.remove(app) for entry_point, app in loaded_apps.items(): if entry_point not in set(app.entry_address for app in self._current_apps): new_app_state = AppState.from_gdb(app) - print(f"New application loaded. Adding debug info") + warning(f"New application loaded. Adding debug info") if self._exec_gdb_command(new_app_state.get_gdb_load_command()): self._current_apps.append(new_app_state) else: - print(f"Failed to load debug info for {new_app_state}") + error(f"Failed to load debug info for {new_app_state}") def attach_to_fw(self) -> None: - print("Attaching to Flipper firmware") + info("Attaching to Flipper firmware") + self.app_check_message = gdb.lookup_global_symbol("__furi_check_message") self.app_list_ptr = gdb.lookup_global_symbol( "flipper_application_loaded_app_list" ) @@ -200,10 +244,10 @@ class FlipperAppStateHelper: try: gdb.execute(f"set variable furi_hal_debug_gdb_session_active = {int(mode)}") except gdb.error as e: - print(f"Failed to set debug mode: {e}") + error(f"Failed to set debug mode: {e}") # Init additional 'fap-set-debug-elf-root' command and set up hooks SetFapDebugElfRoot() helper = FlipperAppStateHelper() -print("Support for Flipper external apps debug is loaded") +info("Support for Flipper external apps debug is loaded") diff --git a/targets/f7/ble_glue/ble_event_thread.c b/targets/f7/ble_glue/ble_event_thread.c index c6bc56846..3d1fdd196 100644 --- a/targets/f7/ble_glue/ble_event_thread.c +++ b/targets/f7/ble_glue/ble_event_thread.c @@ -90,7 +90,7 @@ void ble_event_thread_stop(void) { void ble_event_thread_start(void) { furi_check(event_thread == NULL); - event_thread = furi_thread_alloc_ex("BleEventWorker", 1024, ble_event_thread, NULL); + event_thread = furi_thread_alloc_ex("BleEventWorker", 1280, ble_event_thread, NULL); furi_thread_set_priority(event_thread, FuriThreadPriorityHigh); furi_thread_start(event_thread); } diff --git a/targets/f7/furi_hal/furi_hal_usb_cdc.c b/targets/f7/furi_hal/furi_hal_usb_cdc.c index cfedb5e76..f9c1d3a42 100644 --- a/targets/f7/furi_hal/furi_hal_usb_cdc.c +++ b/targets/f7/furi_hal/furi_hal_usb_cdc.c @@ -392,10 +392,11 @@ static void cdc_on_suspend(usbd_device* dev); static usbd_respond cdc_ep_config(usbd_device* dev, uint8_t cfg); static usbd_respond cdc_control(usbd_device* dev, usbd_ctlreq* req, usbd_rqc_callback* callback); + static usbd_device* usb_dev; -static FuriHalUsbInterface* cdc_if_cur = NULL; -static bool connected = false; -static CdcCallbacks* callbacks[IF_NUM_MAX] = {NULL}; +static volatile FuriHalUsbInterface* cdc_if_cur = NULL; +static volatile bool connected = false; +static volatile CdcCallbacks* callbacks[IF_NUM_MAX] = {NULL}; static void* cb_ctx[IF_NUM_MAX]; FuriHalUsbInterface usb_cdc_single = { @@ -506,8 +507,10 @@ uint8_t furi_hal_cdc_get_ctrl_line_state(uint8_t if_num) { void furi_hal_cdc_send(uint8_t if_num, uint8_t* buf, uint16_t len) { if(if_num == 0) { usbd_ep_write(usb_dev, CDC0_TXD_EP, buf, len); - } else { + } else if(if_num == 1) { usbd_ep_write(usb_dev, CDC1_TXD_EP, buf, len); + } else { + furi_crash(); } } @@ -515,8 +518,10 @@ int32_t furi_hal_cdc_receive(uint8_t if_num, uint8_t* buf, uint16_t max_len) { int32_t len = 0; if(if_num == 0) { len = usbd_ep_read(usb_dev, CDC0_RXD_EP, buf, max_len); - } else { + } else if(if_num == 1) { len = usbd_ep_read(usb_dev, CDC1_RXD_EP, buf, max_len); + } else { + furi_crash(); } return (len < 0) ? 0 : len; } From ca527a0183a57bf10293c1e3bdf8ad266184cc1a Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Tue, 18 Feb 2025 00:51:19 +0300 Subject: [PATCH 044/268] upd changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ea88a52f8..c7badafc4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ * Apps: **Check out more Apps updates and fixes by following** [this link](https://github.com/xMasterX/all-the-plugins/commits/dev) ## Other changes * Anims: Disable winter anims +* OFW: Furi, USB, BLE, Debug: various bug fixes and improvements * OFW: EventLoop unsubscribe fix * OFW: nfc: Enable MFUL sync poller to be provided with passwords * OFW: ST25TB poller mode check From 1541c36b1458cbc0d8c2ebe75fba3775c13e840b Mon Sep 17 00:00:00 2001 From: Ryan Peel Date: Wed, 19 Feb 2025 17:24:34 -0600 Subject: [PATCH 045/268] BadUSB: Mouse control (#4004) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add usb hid mouse functions, add mouse functions to BadUsbHidApi * add ble mouse functionality * add hid_usb_mouse_release_all * ducky mouse command skeleton * implement mouse click functions * corrected missing semicolon * added mouse functionality * corrected mouse scroll functionality * mouse key functionality, removed mouse commands, supporting get_mouse_keycode function, added mouse buttons as Keys for HOLD function * add mouse commands * removed mouse middle click * Format sources and fix bunch of mistakes in nfc and subghz * added HID_MOUSE_NONE: added to help with better readability * added script for mouse movement test * Fix: hold and release, imrpove readability * simplified the mouse demo/test * Format sources Co-authored-by: あく --- .../main/bad_usb/helpers/bad_usb_hid.c | 56 ++++++++++ .../main/bad_usb/helpers/bad_usb_hid.h | 4 + .../main/bad_usb/helpers/ducky_script.c | 10 +- .../bad_usb/helpers/ducky_script_commands.c | 102 ++++++++++++++---- .../main/bad_usb/helpers/ducky_script_i.h | 5 + .../bad_usb/helpers/ducky_script_keycodes.c | 23 ++++ .../bad_usb/resources/badusb/test_mouse.txt | 46 ++++++++ .../file_formats/BadUsbScriptFormat.md | 15 +++ lib/subghz/protocols/bin_raw.c | 1 + 9 files changed, 243 insertions(+), 19 deletions(-) create mode 100644 applications/main/bad_usb/resources/badusb/test_mouse.txt diff --git a/applications/main/bad_usb/helpers/bad_usb_hid.c b/applications/main/bad_usb/helpers/bad_usb_hid.c index 5d7076314..c6226cf37 100644 --- a/applications/main/bad_usb/helpers/bad_usb_hid.c +++ b/applications/main/bad_usb/helpers/bad_usb_hid.c @@ -37,6 +37,31 @@ bool hid_usb_kb_release(void* inst, uint16_t button) { return furi_hal_hid_kb_release(button); } +bool hid_usb_mouse_press(void* inst, uint8_t button) { + UNUSED(inst); + return furi_hal_hid_mouse_press(button); +} + +bool hid_usb_mouse_release(void* inst, uint8_t button) { + UNUSED(inst); + return furi_hal_hid_mouse_release(button); +} + +bool hid_usb_mouse_scroll(void* inst, int8_t delta) { + UNUSED(inst); + return furi_hal_hid_mouse_scroll(delta); +} + +bool hid_usb_mouse_move(void* inst, int8_t dx, int8_t dy) { + UNUSED(inst); + return furi_hal_hid_mouse_move(dx, dy); +} + +bool hid_usb_mouse_release_all(void* inst) { + UNUSED(inst); + return furi_hal_hid_mouse_release(0); +} + bool hid_usb_consumer_press(void* inst, uint16_t button) { UNUSED(inst); return furi_hal_hid_consumer_key_press(button); @@ -51,6 +76,7 @@ bool hid_usb_release_all(void* inst) { UNUSED(inst); bool state = furi_hal_hid_kb_release_all(); state &= furi_hal_hid_consumer_key_release_all(); + state &= hid_usb_mouse_release_all(inst); return state; } @@ -67,6 +93,10 @@ static const BadUsbHidApi hid_api_usb = { .kb_press = hid_usb_kb_press, .kb_release = hid_usb_kb_release, + .mouse_press = hid_usb_mouse_press, + .mouse_release = hid_usb_mouse_release, + .mouse_scroll = hid_usb_mouse_scroll, + .mouse_move = hid_usb_mouse_move, .consumer_press = hid_usb_consumer_press, .consumer_release = hid_usb_consumer_release, .release_all = hid_usb_release_all, @@ -157,6 +187,27 @@ bool hid_ble_kb_release(void* inst, uint16_t button) { return ble_profile_hid_kb_release(ble_hid->profile, button); } +bool hid_ble_mouse_press(void* inst, uint8_t button) { + BleHidInstance* ble_hid = inst; + furi_assert(ble_hid); + return ble_profile_hid_mouse_press(ble_hid->profile, button); +} +bool hid_ble_mouse_release(void* inst, uint8_t button) { + BleHidInstance* ble_hid = inst; + furi_assert(ble_hid); + return ble_profile_hid_mouse_release(ble_hid->profile, button); +} +bool hid_ble_mouse_scroll(void* inst, int8_t delta) { + BleHidInstance* ble_hid = inst; + furi_assert(ble_hid); + return ble_profile_hid_mouse_scroll(ble_hid->profile, delta); +} +bool hid_ble_mouse_move(void* inst, int8_t dx, int8_t dy) { + BleHidInstance* ble_hid = inst; + furi_assert(ble_hid); + return ble_profile_hid_mouse_move(ble_hid->profile, dx, dy); +} + bool hid_ble_consumer_press(void* inst, uint16_t button) { BleHidInstance* ble_hid = inst; furi_assert(ble_hid); @@ -174,6 +225,7 @@ bool hid_ble_release_all(void* inst) { furi_assert(ble_hid); bool state = ble_profile_hid_kb_release_all(ble_hid->profile); state &= ble_profile_hid_consumer_key_release_all(ble_hid->profile); + state &= ble_profile_hid_mouse_release_all(ble_hid->profile); return state; } @@ -191,6 +243,10 @@ static const BadUsbHidApi hid_api_ble = { .kb_press = hid_ble_kb_press, .kb_release = hid_ble_kb_release, + .mouse_press = hid_ble_mouse_press, + .mouse_release = hid_ble_mouse_release, + .mouse_scroll = hid_ble_mouse_scroll, + .mouse_move = hid_ble_mouse_move, .consumer_press = hid_ble_consumer_press, .consumer_release = hid_ble_consumer_release, .release_all = hid_ble_release_all, diff --git a/applications/main/bad_usb/helpers/bad_usb_hid.h b/applications/main/bad_usb/helpers/bad_usb_hid.h index 71d3a58e7..e4758ab68 100644 --- a/applications/main/bad_usb/helpers/bad_usb_hid.h +++ b/applications/main/bad_usb/helpers/bad_usb_hid.h @@ -20,6 +20,10 @@ typedef struct { bool (*kb_press)(void* inst, uint16_t button); bool (*kb_release)(void* inst, uint16_t button); + bool (*mouse_press)(void* inst, uint8_t button); + bool (*mouse_release)(void* inst, uint8_t button); + bool (*mouse_scroll)(void* inst, int8_t delta); + bool (*mouse_move)(void* inst, int8_t dx, int8_t dy); bool (*consumer_press)(void* inst, uint16_t button); bool (*consumer_release)(void* inst, uint16_t button); bool (*release_all)(void* inst); diff --git a/applications/main/bad_usb/helpers/ducky_script.c b/applications/main/bad_usb/helpers/ducky_script.c index ccc3caa81..e71c03c48 100644 --- a/applications/main/bad_usb/helpers/ducky_script.c +++ b/applications/main/bad_usb/helpers/ducky_script.c @@ -193,8 +193,16 @@ static int32_t ducky_parse_line(BadUsbScript* bad_usb, FuriString* line) { return cmd_result; } + // Mouse Keys + uint16_t key = ducky_get_mouse_keycode_by_name(line_tmp); + if(key != HID_MOUSE_INVALID) { + bad_usb->hid->mouse_press(bad_usb->hid_inst, key); + bad_usb->hid->mouse_release(bad_usb->hid_inst, key); + return 0; + } + // Special keys + modifiers - uint16_t key = ducky_get_keycode(bad_usb, line_tmp, false); + key = ducky_get_keycode(bad_usb, line_tmp, false); if(key == HID_KEYBOARD_NONE) { return ducky_error(bad_usb, "No keycode defined for %s", line_tmp); } diff --git a/applications/main/bad_usb/helpers/ducky_script_commands.c b/applications/main/bad_usb/helpers/ducky_script_commands.c index 79dcdd531..1b4ff55cb 100644 --- a/applications/main/bad_usb/helpers/ducky_script_commands.c +++ b/applications/main/bad_usb/helpers/ducky_script_commands.c @@ -1,4 +1,5 @@ #include +#include #include "ducky_script.h" #include "ducky_script_i.h" @@ -124,34 +125,58 @@ static int32_t ducky_fnc_altstring(BadUsbScript* bad_usb, const char* line, int3 static int32_t ducky_fnc_hold(BadUsbScript* bad_usb, const char* line, int32_t param) { UNUSED(param); - line = &line[ducky_get_command_len(line) + 1]; - uint16_t key = ducky_get_keycode(bad_usb, line, true); - if(key == HID_KEYBOARD_NONE) { - return ducky_error(bad_usb, "No keycode defined for %s", line); - } - bad_usb->key_hold_nb++; + if(bad_usb->key_hold_nb > (HID_KB_MAX_KEYS - 1)) { - return ducky_error(bad_usb, "Too many keys are hold"); + return ducky_error(bad_usb, "Too many keys are held"); } - bad_usb->hid->kb_press(bad_usb->hid_inst, key); - return 0; + + // Handle Mouse Keys here + uint16_t key = ducky_get_mouse_keycode_by_name(line); + if(key != HID_MOUSE_NONE) { + bad_usb->key_hold_nb++; + bad_usb->hid->mouse_press(bad_usb->hid_inst, key); + return 0; + } + + // Handle Keyboard keys here + key = ducky_get_keycode(bad_usb, line, true); + if(key != HID_KEYBOARD_NONE) { + bad_usb->key_hold_nb++; + bad_usb->hid->kb_press(bad_usb->hid_inst, key); + return 0; + } + + // keyboard and mouse were none + return ducky_error(bad_usb, "Unknown keycode for %s", line); } static int32_t ducky_fnc_release(BadUsbScript* bad_usb, const char* line, int32_t param) { UNUSED(param); - line = &line[ducky_get_command_len(line) + 1]; - uint16_t key = ducky_get_keycode(bad_usb, line, true); - if(key == HID_KEYBOARD_NONE) { - return ducky_error(bad_usb, "No keycode defined for %s", line); - } + if(bad_usb->key_hold_nb == 0) { - return ducky_error(bad_usb, "No keys are hold"); + return ducky_error(bad_usb, "No keys are held"); } - bad_usb->key_hold_nb--; - bad_usb->hid->kb_release(bad_usb->hid_inst, key); - return 0; + + // Handle Mouse Keys here + uint16_t key = ducky_get_mouse_keycode_by_name(line); + if(key != HID_MOUSE_NONE) { + bad_usb->key_hold_nb--; + bad_usb->hid->mouse_release(bad_usb->hid_inst, key); + return 0; + } + + //Handle Keyboard Keys here + key = ducky_get_keycode(bad_usb, line, true); + if(key != HID_KEYBOARD_NONE) { + bad_usb->key_hold_nb--; + bad_usb->hid->kb_release(bad_usb->hid_inst, key); + return 0; + } + + // keyboard and mouse were none + return ducky_error(bad_usb, "No keycode defined for %s", line); } static int32_t ducky_fnc_media(BadUsbScript* bad_usb, const char* line, int32_t param) { @@ -191,6 +216,43 @@ static int32_t ducky_fnc_waitforbutton(BadUsbScript* bad_usb, const char* line, return SCRIPT_STATE_WAIT_FOR_BTN; } +static int32_t ducky_fnc_mouse_scroll(BadUsbScript* bad_usb, const char* line, int32_t param) { + UNUSED(param); + + line = &line[strcspn(line, " ") + 1]; + int32_t mouse_scroll_dist = 0; + + if(strint_to_int32(line, NULL, &mouse_scroll_dist, 10) != StrintParseNoError) { + return ducky_error(bad_usb, "Invalid Number %s", line); + } + + bad_usb->hid->mouse_scroll(bad_usb->hid_inst, mouse_scroll_dist); + + return 0; +} + +static int32_t ducky_fnc_mouse_move(BadUsbScript* bad_usb, const char* line, int32_t param) { + UNUSED(param); + + line = &line[strcspn(line, " ") + 1]; + int32_t mouse_move_x = 0; + int32_t mouse_move_y = 0; + + if(strint_to_int32(line, NULL, &mouse_move_x, 10) != StrintParseNoError) { + return ducky_error(bad_usb, "Invalid Number %s", line); + } + + line = &line[strcspn(line, " ") + 1]; + + if(strint_to_int32(line, NULL, &mouse_move_y, 10) != StrintParseNoError) { + return ducky_error(bad_usb, "Invalid Number %s", line); + } + + bad_usb->hid->mouse_move(bad_usb->hid_inst, mouse_move_x, mouse_move_y); + + return 0; +} + static const DuckyCmd ducky_commands[] = { {"REM", NULL, -1}, {"ID", NULL, -1}, @@ -213,6 +275,10 @@ static const DuckyCmd ducky_commands[] = { {"WAIT_FOR_BUTTON_PRESS", ducky_fnc_waitforbutton, -1}, {"MEDIA", ducky_fnc_media, -1}, {"GLOBE", ducky_fnc_globe, -1}, + {"MOUSEMOVE", ducky_fnc_mouse_move, -1}, + {"MOUSE_MOVE", ducky_fnc_mouse_move, -1}, + {"MOUSESCROLL", ducky_fnc_mouse_scroll, -1}, + {"MOUSE_SCROLL", ducky_fnc_mouse_scroll, -1}, }; #define TAG "BadUsb" diff --git a/applications/main/bad_usb/helpers/ducky_script_i.h b/applications/main/bad_usb/helpers/ducky_script_i.h index 464c8a72b..fd95ecf58 100644 --- a/applications/main/bad_usb/helpers/ducky_script_i.h +++ b/applications/main/bad_usb/helpers/ducky_script_i.h @@ -18,6 +18,9 @@ extern "C" { #define FILE_BUFFER_LEN 16 +#define HID_MOUSE_INVALID 0 +#define HID_MOUSE_NONE 0 + struct BadUsbScript { FuriHalUsbHidConfig hid_cfg; const BadUsbHidApi* hid; @@ -55,6 +58,8 @@ uint16_t ducky_get_keycode_by_name(const char* param); uint16_t ducky_get_media_keycode_by_name(const char* param); +uint8_t ducky_get_mouse_keycode_by_name(const char* param); + bool ducky_get_number(const char* param, uint32_t* val); void ducky_numlock_on(BadUsbScript* bad_usb); diff --git a/applications/main/bad_usb/helpers/ducky_script_keycodes.c b/applications/main/bad_usb/helpers/ducky_script_keycodes.c index 290618c13..7dd2e4d16 100644 --- a/applications/main/bad_usb/helpers/ducky_script_keycodes.c +++ b/applications/main/bad_usb/helpers/ducky_script_keycodes.c @@ -108,6 +108,17 @@ static const DuckyKey ducky_media_keys[] = { {"BRIGHT_DOWN", HID_CONSUMER_BRIGHTNESS_DECREMENT}, }; +static const DuckyKey ducky_mouse_keys[] = { + {"LEFTCLICK", HID_MOUSE_BTN_LEFT}, + {"LEFT_CLICK", HID_MOUSE_BTN_LEFT}, + {"RIGHTCLICK", HID_MOUSE_BTN_RIGHT}, + {"RIGHT_CLICK", HID_MOUSE_BTN_RIGHT}, + {"MIDDLECLICK", HID_MOUSE_BTN_WHEEL}, + {"MIDDLE_CLICK", HID_MOUSE_BTN_WHEEL}, + {"WHEELCLICK", HID_MOUSE_BTN_WHEEL}, + {"WHEEL_CLICK", HID_MOUSE_BTN_WHEEL}, +}; + uint16_t ducky_get_keycode_by_name(const char* param) { for(size_t i = 0; i < COUNT_OF(ducky_keys); i++) { size_t key_cmd_len = strlen(ducky_keys[i].name); @@ -131,3 +142,15 @@ uint16_t ducky_get_media_keycode_by_name(const char* param) { return HID_CONSUMER_UNASSIGNED; } + +uint8_t ducky_get_mouse_keycode_by_name(const char* param) { + for(size_t i = 0; i < COUNT_OF(ducky_mouse_keys); i++) { + size_t key_cmd_len = strlen(ducky_mouse_keys[i].name); + if((strncmp(param, ducky_mouse_keys[i].name, key_cmd_len) == 0) && + (ducky_is_line_end(param[key_cmd_len]))) { + return ducky_mouse_keys[i].keycode; + } + } + + return HID_MOUSE_INVALID; +} diff --git a/applications/main/bad_usb/resources/badusb/test_mouse.txt b/applications/main/bad_usb/resources/badusb/test_mouse.txt new file mode 100644 index 000000000..97391cf17 --- /dev/null +++ b/applications/main/bad_usb/resources/badusb/test_mouse.txt @@ -0,0 +1,46 @@ +ID 1234:abcd Generic:USB Keyboard +REM Declare ourselves as a generic usb keyboard +REM You can override this to use something else +REM Check the `lsusb` command to know your own devices IDs + +DEFAULT_DELAY 200 +DEFAULT_STRING_DELAY 100 + +DELAY 1000 + +REM Test all mouse functions +LEFTCLICK +RIGHTCLICK +MIDDLECLICK + +DELAY 1000 + +MOUSEMOVE -10 0 +REPEAT 20 +MOUSEMOVE 0 10 +REPEAT 20 +MOUSEMOVE 10 0 +REPEAT 20 +MOUSEMOVE 0 -10 +REPEAT 20 + +DELAY 1000 + +MOUSESCROLL -50 +MOUSESCROLL 50 + +DELAY 1000 + +REM Verify Mouse hold working +HOLD LEFTCLICK +DELAY 2000 +RELEASE LEFTCLICK + +DELAY 1000 + +REM Verify KB hold working +HOLD M +DELAY 2000 +RELEASE M + +ENTER \ No newline at end of file diff --git a/documentation/file_formats/BadUsbScriptFormat.md b/documentation/file_formats/BadUsbScriptFormat.md index 1bac3c4aa..11977c9cb 100644 --- a/documentation/file_formats/BadUsbScriptFormat.md +++ b/documentation/file_formats/BadUsbScriptFormat.md @@ -177,3 +177,18 @@ Example: `ID 1234:abcd Flipper Devices:Flipper Zero` VID and PID are hex codes and are mandatory. Manufacturer and Product are text strings and are optional. + +## Mouse Commands + +Mouse movement and click commands. Mouse click commands support HOLD functionality. + +| Command | Parameters | Notes | +| ------------- | -------------------------------| -------------------------------- | +| LEFTCLICK | None | | +| LEFT_CLICK | None | functionally same as LEFTCLICK | +| RIGHTCLICK | None | | +| RIGHT_CLICK | None | functionally same as RIGHTCLICK | +| MOUSEMOVE | x y: int move mount/direction | | +| MOUSE_MOVE | x y: int move mount/direction | functionally same as MOUSEMOVE | +| MOUSESCROLL | delta: int scroll distance | | +| MOUSE_SCROLL | delta: int scroll distance | functionally same as MOUSESCROLL | diff --git a/lib/subghz/protocols/bin_raw.c b/lib/subghz/protocols/bin_raw.c index c2aebb6ab..e90f1508e 100644 --- a/lib/subghz/protocols/bin_raw.c +++ b/lib/subghz/protocols/bin_raw.c @@ -317,6 +317,7 @@ SubGhzProtocolStatus res = SubGhzProtocolStatusErrorEncoderGetUpload; break; } + instance->encoder.is_running = true; res = SubGhzProtocolStatusOk; From 93b02779380ca8fa0f902dd90931d8b8e610f22a Mon Sep 17 00:00:00 2001 From: hedger Date: Wed, 19 Feb 2025 23:32:20 +0000 Subject: [PATCH 046/268] vscode: disabled auto-update for clangd since correct version is in the toolchain (#4122) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: あく --- .vscode/example/settings.json.tmpl | 1 + scripts/ufbt/project_template/.vscode/settings.json | 1 + 2 files changed, 2 insertions(+) diff --git a/.vscode/example/settings.json.tmpl b/.vscode/example/settings.json.tmpl index 5e5b5dcf4..b8e9f81cd 100644 --- a/.vscode/example/settings.json.tmpl +++ b/.vscode/example/settings.json.tmpl @@ -12,6 +12,7 @@ "SConstruct": "python", "*.fam": "python" }, + "clangd.checkUpdates": false, "clangd.path": "${workspaceFolder}/toolchain/current/bin/clangd@FBT_PLATFORM_EXECUTABLE_EXT@", "clangd.arguments": [ "--query-driver=**/arm-none-eabi-*", diff --git a/scripts/ufbt/project_template/.vscode/settings.json b/scripts/ufbt/project_template/.vscode/settings.json index d304752a9..b2ee0ca3d 100644 --- a/scripts/ufbt/project_template/.vscode/settings.json +++ b/scripts/ufbt/project_template/.vscode/settings.json @@ -19,6 +19,7 @@ "[python]": { "editor.defaultFormatter": "ms-python.black-formatter" }, + "clangd.checkUpdates": false, "clangd.path": "@UFBT_TOOLCHAIN_CLANGD@", "clangd.arguments": [ "--query-driver=**/arm-none-eabi-*", From 0f240c4dbc8d2ceb93ad025d893bb5337505fecf Mon Sep 17 00:00:00 2001 From: Anna Antonenko Date: Thu, 20 Feb 2025 03:58:55 +0400 Subject: [PATCH 047/268] [FL-3949] Universal IR signal selection (#4085) * feat: universal ir signal selection * fix: f18, format specifiers * update labels with suggestions from the ui team Co-authored-by: Aleksandr Kutuzov --- .../main/infrared/infrared_brute_force.c | 93 ++++++++++--- .../main/infrared/infrared_brute_force.h | 20 ++- applications/main/infrared/infrared_cli.c | 13 +- .../main/infrared/infrared_custom_event.h | 6 +- .../common/infrared_scene_universal_common.c | 128 +++++++++++++++--- .../scenes/infrared_scene_universal_ac.c | 2 +- .../scenes/infrared_scene_universal_audio.c | 2 +- .../infrared_scene_universal_projector.c | 2 +- .../scenes/infrared_scene_universal_tv.c | 2 +- .../infrared/views/infrared_progress_view.c | 116 +++++++++++----- .../infrared/views/infrared_progress_view.h | 37 +++-- applications/services/gui/scene_manager.c | 5 + applications/services/gui/scene_manager.h | 8 ++ lib/flipper_format/flipper_format.c | 16 +++ lib/flipper_format/flipper_format.h | 24 ++++ targets/f18/api_symbols.csv | 5 +- targets/f7/api_symbols.csv | 5 +- 17 files changed, 376 insertions(+), 108 deletions(-) diff --git a/applications/main/infrared/infrared_brute_force.c b/applications/main/infrared/infrared_brute_force.c index 8c7422d5e..636635894 100644 --- a/applications/main/infrared/infrared_brute_force.c +++ b/applications/main/infrared/infrared_brute_force.c @@ -2,26 +2,61 @@ #include #include +#include #include #include "infrared_signal.h" +ARRAY_DEF(SignalPositionArray, size_t, M_DEFAULT_OPLIST); + typedef struct { - uint32_t index; - uint32_t count; + size_t index; + SignalPositionArray_t signals; } InfraredBruteForceRecord; +static inline void ir_bf_record_init(InfraredBruteForceRecord* record) { + record->index = 0; + SignalPositionArray_init(record->signals); +} +#define IR_BF_RECORD_INIT(r) (ir_bf_record_init(&(r))) + +static inline void + ir_bf_record_init_set(InfraredBruteForceRecord* dest, const InfraredBruteForceRecord* src) { + dest->index = src->index; + SignalPositionArray_init_set(dest->signals, src->signals); +} +#define IR_BF_RECORD_INIT_SET(d, s) (ir_bf_record_init_set(&(d), &(s))) + +static inline void + ir_bf_record_set(InfraredBruteForceRecord* dest, const InfraredBruteForceRecord* src) { + dest->index = src->index; + SignalPositionArray_set(dest->signals, src->signals); +} +#define IR_BF_RECORD_SET(d, s) (ir_bf_record_set(&(d), &(s))) + +static inline void ir_bf_record_clear(InfraredBruteForceRecord* record) { + SignalPositionArray_clear(record->signals); +} +#define IR_BF_RECORD_CLEAR(r) (ir_bf_record_clear(&(r))) + +#define IR_BF_RECORD_OPLIST \ + (INIT(IR_BF_RECORD_INIT), \ + INIT_SET(IR_BF_RECORD_INIT_SET), \ + SET(IR_BF_RECORD_SET), \ + CLEAR(IR_BF_RECORD_CLEAR)) + DICT_DEF2( InfraredBruteForceRecordDict, FuriString*, FURI_STRING_OPLIST, InfraredBruteForceRecord, - M_POD_OPLIST); + IR_BF_RECORD_OPLIST); struct InfraredBruteForce { FlipperFormat* ff; const char* db_filename; FuriString* current_record_name; + InfraredBruteForceRecord current_record; InfraredSignal* current_signal; InfraredBruteForceRecordDict_t records; bool is_started; @@ -39,6 +74,7 @@ InfraredBruteForce* infrared_brute_force_alloc(void) { } void infrared_brute_force_free(InfraredBruteForce* brute_force) { + furi_check(brute_force); furi_assert(!brute_force->is_started); InfraredBruteForceRecordDict_clear(brute_force->records); furi_string_free(brute_force->current_record_name); @@ -46,11 +82,13 @@ void infrared_brute_force_free(InfraredBruteForce* brute_force) { } void infrared_brute_force_set_db_filename(InfraredBruteForce* brute_force, const char* db_filename) { + furi_check(brute_force); furi_assert(!brute_force->is_started); brute_force->db_filename = db_filename; } InfraredErrorCode infrared_brute_force_calculate_messages(InfraredBruteForce* brute_force) { + furi_check(brute_force); furi_assert(!brute_force->is_started); furi_assert(brute_force->db_filename); InfraredErrorCode error = InfraredErrorCodeNone; @@ -66,19 +104,19 @@ InfraredErrorCode infrared_brute_force_calculate_messages(InfraredBruteForce* br break; } - bool signals_valid = false; + size_t signal_start = flipper_format_tell(ff); + bool signal_valid = false; while(infrared_signal_read_name(ff, signal_name) == InfraredErrorCodeNone) { error = infrared_signal_read_body(signal, ff); - signals_valid = (!INFRARED_ERROR_PRESENT(error)) && infrared_signal_is_valid(signal); - if(!signals_valid) break; + signal_valid = (!INFRARED_ERROR_PRESENT(error)) && infrared_signal_is_valid(signal); + if(!signal_valid) break; InfraredBruteForceRecord* record = InfraredBruteForceRecordDict_get(brute_force->records, signal_name); - if(record) { //-V547 - ++(record->count); - } + furi_assert(record); + SignalPositionArray_push_back(record->signals, signal_start); } - if(!signals_valid) break; + if(!signal_valid) break; } while(false); infrared_signal_free(signal); @@ -93,6 +131,7 @@ bool infrared_brute_force_start( InfraredBruteForce* brute_force, uint32_t index, uint32_t* record_count) { + furi_check(brute_force); furi_assert(!brute_force->is_started); bool success = false; *record_count = 0; @@ -103,9 +142,10 @@ bool infrared_brute_force_start( InfraredBruteForceRecordDict_next(it)) { const InfraredBruteForceRecordDict_itref_t* record = InfraredBruteForceRecordDict_cref(it); if(record->value.index == index) { - *record_count = record->value.count; + *record_count = SignalPositionArray_size(record->value.signals); if(*record_count) { furi_string_set(brute_force->current_record_name, record->key); + brute_force->current_record = record->value; } break; } @@ -124,10 +164,12 @@ bool infrared_brute_force_start( } bool infrared_brute_force_is_started(const InfraredBruteForce* brute_force) { + furi_check(brute_force); return brute_force->is_started; } void infrared_brute_force_stop(InfraredBruteForce* brute_force) { + furi_check(brute_force); furi_assert(brute_force->is_started); furi_string_reset(brute_force->current_record_name); infrared_signal_free(brute_force->current_signal); @@ -138,25 +180,32 @@ void infrared_brute_force_stop(InfraredBruteForce* brute_force) { furi_record_close(RECORD_STORAGE); } -bool infrared_brute_force_send_next(InfraredBruteForce* brute_force) { +bool infrared_brute_force_send(InfraredBruteForce* brute_force, uint32_t signal_index) { + furi_check(brute_force); furi_assert(brute_force->is_started); - const bool success = infrared_signal_search_by_name_and_read( - brute_force->current_signal, - brute_force->ff, - furi_string_get_cstr(brute_force->current_record_name)) == - InfraredErrorCodeNone; - if(success) { - infrared_signal_transmit(brute_force->current_signal); - } - return success; + if(signal_index >= SignalPositionArray_size(brute_force->current_record.signals)) return false; + + size_t signal_start = + *SignalPositionArray_cget(brute_force->current_record.signals, signal_index); + if(!flipper_format_seek(brute_force->ff, signal_start, FlipperFormatOffsetFromStart)) + return false; + + if(INFRARED_ERROR_PRESENT( + infrared_signal_read_body(brute_force->current_signal, brute_force->ff))) + return false; + + infrared_signal_transmit(brute_force->current_signal); + return true; } void infrared_brute_force_add_record( InfraredBruteForce* brute_force, uint32_t index, const char* name) { - InfraredBruteForceRecord value = {.index = index, .count = 0}; + InfraredBruteForceRecord value; + ir_bf_record_init(&value); + value.index = index; FuriString* key; key = furi_string_alloc_set(name); InfraredBruteForceRecordDict_set_at(brute_force->records, key, value); diff --git a/applications/main/infrared/infrared_brute_force.h b/applications/main/infrared/infrared_brute_force.h index 879642257..2c75d37f2 100644 --- a/applications/main/infrared/infrared_brute_force.h +++ b/applications/main/infrared/infrared_brute_force.h @@ -78,18 +78,16 @@ bool infrared_brute_force_is_started(const InfraredBruteForce* brute_force); void infrared_brute_force_stop(InfraredBruteForce* brute_force); /** - * @brief Send the next signal from the chosen category. - * - * This function is called repeatedly until no more signals are left - * in the chosen signal category. - * - * @warning Transmission must be started first by calling infrared_brute_force_start() - * before calling this function. - * - * @param[in,out] brute_force pointer to the instance to be used. - * @returns true if the next signal existed and could be transmitted, false otherwise. + * @brief Send an arbitrary signal from the chosen category. + * + * @param[in] brute_force pointer to the instance + * @param signal_index the index of the signal within the category, must be + * between 0 and `record_count` as told by + * `infrared_brute_force_start` + * + * @returns true on success, false otherwise */ -bool infrared_brute_force_send_next(InfraredBruteForce* brute_force); +bool infrared_brute_force_send(InfraredBruteForce* brute_force, uint32_t signal_index); /** * @brief Add a signal category to an InfraredBruteForce instance's dictionary. diff --git a/applications/main/infrared/infrared_cli.c b/applications/main/infrared/infrared_cli.c index b700cf121..85ae95658 100644 --- a/applications/main/infrared/infrared_cli.c +++ b/applications/main/infrared/infrared_cli.c @@ -475,25 +475,24 @@ static void break; } - uint32_t record_count; + uint32_t signal_count, current_signal = 0; bool running = infrared_brute_force_start( - brute_force, INFRARED_BRUTE_FORCE_DUMMY_INDEX, &record_count); + brute_force, INFRARED_BRUTE_FORCE_DUMMY_INDEX, &signal_count); - if(record_count <= 0) { + if(signal_count <= 0) { printf("Invalid signal name.\r\n"); break; } - printf("Sending %lu signal(s)...\r\n", record_count); + printf("Sending %lu signal(s)...\r\n", signal_count); printf("Press Ctrl-C to stop.\r\n"); - int records_sent = 0; while(running) { - running = infrared_brute_force_send_next(brute_force); + running = infrared_brute_force_send(brute_force, current_signal); if(cli_cmd_interrupt_received(cli)) break; - printf("\r%d%% complete.", (int)((float)records_sent++ / (float)record_count * 100)); + printf("\r%d%% complete.", (int)((float)current_signal++ / (float)signal_count * 100)); fflush(stdout); } diff --git a/applications/main/infrared/infrared_custom_event.h b/applications/main/infrared/infrared_custom_event.h index 2efc99f4b..7109a48b7 100644 --- a/applications/main/infrared/infrared_custom_event.h +++ b/applications/main/infrared/infrared_custom_event.h @@ -3,7 +3,7 @@ #include #include -enum InfraredCustomEventType { +typedef enum { // Reserve first 100 events for button types and indexes, starting from 0 InfraredCustomEventTypeReserved = 100, InfraredCustomEventTypeMenuSelected, @@ -13,7 +13,7 @@ enum InfraredCustomEventType { InfraredCustomEventTypeTextEditDone, InfraredCustomEventTypePopupClosed, InfraredCustomEventTypeButtonSelected, - InfraredCustomEventTypeBackPressed, + InfraredCustomEventTypePopupInput, InfraredCustomEventTypeTaskFinished, InfraredCustomEventTypeRpcLoadFile, @@ -27,7 +27,7 @@ enum InfraredCustomEventType { InfraredCustomEventTypeGpioTxPinChanged, InfraredCustomEventTypeGpioOtgChanged, -}; +} InfraredCustomEventType; #pragma pack(push, 1) typedef union { diff --git a/applications/main/infrared/scenes/common/infrared_scene_universal_common.c b/applications/main/infrared/scenes/common/infrared_scene_universal_common.c index a52f141c4..62b7350e7 100644 --- a/applications/main/infrared/scenes/common/infrared_scene_universal_common.c +++ b/applications/main/infrared/scenes/common/infrared_scene_universal_common.c @@ -2,15 +2,28 @@ #include +#pragma pack(push, 1) +typedef union { + uint32_t packed_value; + struct { + bool is_paused; + uint8_t padding; + uint16_t signal_index; + }; +} InfraredSceneState; +#pragma pack(pop) + void infrared_scene_universal_common_item_callback(void* context, uint32_t index) { InfraredApp* infrared = context; uint32_t event = infrared_custom_event_pack(InfraredCustomEventTypeButtonSelected, index); view_dispatcher_send_custom_event(infrared->view_dispatcher, event); } -static void infrared_scene_universal_common_progress_back_callback(void* context) { +static void infrared_scene_universal_common_progress_input_callback( + void* context, + InfraredProgressViewInput input) { InfraredApp* infrared = context; - uint32_t event = infrared_custom_event_pack(InfraredCustomEventTypeBackPressed, -1); + uint32_t event = infrared_custom_event_pack(InfraredCustomEventTypePopupInput, input); view_dispatcher_send_custom_event(infrared->view_dispatcher, event); } @@ -19,8 +32,8 @@ static void ViewStack* view_stack = infrared->view_stack; InfraredProgressView* progress = infrared->progress; infrared_progress_view_set_progress_total(progress, record_count); - infrared_progress_view_set_back_callback( - progress, infrared_scene_universal_common_progress_back_callback, infrared); + infrared_progress_view_set_input_callback( + progress, infrared_scene_universal_common_progress_input_callback, infrared); view_stack_add_view(view_stack, infrared_progress_view_get_view(progress)); infrared_play_notification_message(infrared, InfraredNotificationMessageBlinkStartSend); } @@ -51,29 +64,111 @@ void infrared_scene_universal_common_on_enter(void* context) { infrared_blocking_task_start(infrared, infrared_scene_universal_common_task_callback); } +static void infrared_scene_universal_common_handle_popup_input( + InfraredApp* infrared, + InfraredProgressViewInput input) { + InfraredBruteForce* brute_force = infrared->brute_force; + SceneManager* scene_manager = infrared->scene_manager; + uint32_t scene_id = scene_manager_get_current_scene(infrared->scene_manager); + switch(input) { + case InfraredProgressViewInputStop: { + infrared_brute_force_stop(brute_force); + infrared_scene_universal_common_hide_popup(infrared); + break; + } + + case InfraredProgressViewInputPause: { + infrared_play_notification_message(infrared, InfraredNotificationMessageBlinkStop); + infrared_progress_view_set_paused(infrared->progress, true); + InfraredSceneState scene_state = { + .packed_value = scene_manager_get_scene_state(scene_manager, scene_id)}; + scene_state.is_paused = true; + if(scene_state.signal_index) + scene_state.signal_index--; // when running, the state stores the next index + scene_manager_set_scene_state(scene_manager, scene_id, scene_state.packed_value); + break; + } + + case InfraredProgressViewInputResume: { + infrared_play_notification_message(infrared, InfraredNotificationMessageBlinkStartSend); + infrared_progress_view_set_paused(infrared->progress, false); + InfraredSceneState scene_state = { + .packed_value = scene_manager_get_scene_state(scene_manager, scene_id)}; + scene_state.is_paused = false; + scene_manager_set_scene_state(scene_manager, scene_id, scene_state.packed_value); + break; + } + + case InfraredProgressViewInputNextSignal: { + InfraredSceneState scene_state = { + .packed_value = scene_manager_get_scene_state(scene_manager, scene_id)}; + scene_state.signal_index++; + if(infrared_progress_view_set_progress(infrared->progress, scene_state.signal_index + 1)) + scene_manager_set_scene_state(scene_manager, scene_id, scene_state.packed_value); + break; + } + + case InfraredProgressViewInputPreviousSignal: { + InfraredSceneState scene_state = { + .packed_value = scene_manager_get_scene_state(scene_manager, scene_id)}; + if(scene_state.signal_index) { + scene_state.signal_index--; + if(infrared_progress_view_set_progress( + infrared->progress, scene_state.signal_index + 1)) + scene_manager_set_scene_state(scene_manager, scene_id, scene_state.packed_value); + } + break; + } + + case InfraredProgressViewInputSendSingle: { + InfraredSceneState scene_state = { + .packed_value = scene_manager_get_scene_state(scene_manager, scene_id)}; + infrared_play_notification_message(infrared, InfraredNotificationMessageBlinkStartSend); + infrared_brute_force_send(infrared->brute_force, scene_state.signal_index); + infrared_play_notification_message(infrared, InfraredNotificationMessageBlinkStop); + break; + } + + default: + furi_crash(); + } +} + bool infrared_scene_universal_common_on_event(void* context, SceneManagerEvent event) { InfraredApp* infrared = context; SceneManager* scene_manager = infrared->scene_manager; InfraredBruteForce* brute_force = infrared->brute_force; + uint32_t scene_id = scene_manager_get_current_scene(infrared->scene_manager); bool consumed = false; if(infrared_brute_force_is_started(brute_force)) { if(event.type == SceneManagerEventTypeTick) { - bool success = infrared_brute_force_send_next(brute_force); - if(success) { - success = infrared_progress_view_increase_progress(infrared->progress); + InfraredSceneState scene_state = { + .packed_value = scene_manager_get_scene_state(scene_manager, scene_id)}; + + if(!scene_state.is_paused) { + bool success = infrared_brute_force_send(brute_force, scene_state.signal_index); + if(success) { + success = infrared_progress_view_set_progress( + infrared->progress, scene_state.signal_index + 1); + scene_state.signal_index++; + scene_manager_set_scene_state( + scene_manager, scene_id, scene_state.packed_value); + } + if(!success) { + infrared_brute_force_stop(brute_force); + infrared_scene_universal_common_hide_popup(infrared); + } + consumed = true; } - if(!success) { - infrared_brute_force_stop(brute_force); - infrared_scene_universal_common_hide_popup(infrared); - } - consumed = true; } else if(event.type == SceneManagerEventTypeCustom) { - if(infrared_custom_event_get_type(event.event) == InfraredCustomEventTypeBackPressed) { - infrared_brute_force_stop(brute_force); - infrared_scene_universal_common_hide_popup(infrared); + uint16_t event_type; + int16_t event_value; + infrared_custom_event_unpack(event.event, &event_type, &event_value); + if(event_type == InfraredCustomEventTypePopupInput) { + infrared_scene_universal_common_handle_popup_input(infrared, event_value); + consumed = true; } - consumed = true; } } else { if(event.type == SceneManagerEventTypeBack) { @@ -87,6 +182,7 @@ bool infrared_scene_universal_common_on_event(void* context, SceneManagerEvent e if(event_type == InfraredCustomEventTypeButtonSelected) { uint32_t record_count; if(infrared_brute_force_start(brute_force, event_value, &record_count)) { + scene_manager_set_scene_state(infrared->scene_manager, scene_id, 0); dolphin_deed(DolphinDeedIrSend); infrared_scene_universal_common_show_popup(infrared, record_count); } else { diff --git a/applications/main/infrared/scenes/infrared_scene_universal_ac.c b/applications/main/infrared/scenes/infrared_scene_universal_ac.c index 9288a4a4d..15c4b8db2 100644 --- a/applications/main/infrared/scenes/infrared_scene_universal_ac.c +++ b/applications/main/infrared/scenes/infrared_scene_universal_ac.c @@ -118,7 +118,7 @@ void infrared_scene_universal_ac_on_enter(void* context) { button_panel_add_icon(button_panel, 0, 60, &I_cool_30x51); button_panel_add_icon(button_panel, 34, 60, &I_heat_30x51); - button_panel_add_label(button_panel, 4, 10, FontPrimary, "AC remote"); + button_panel_add_label(button_panel, 24, 10, FontPrimary, "AC"); infrared_scene_universal_common_on_enter(context); } diff --git a/applications/main/infrared/scenes/infrared_scene_universal_audio.c b/applications/main/infrared/scenes/infrared_scene_universal_audio.c index a15b2ce99..223251f10 100644 --- a/applications/main/infrared/scenes/infrared_scene_universal_audio.c +++ b/applications/main/infrared/scenes/infrared_scene_universal_audio.c @@ -114,7 +114,7 @@ void infrared_scene_universal_audio_on_enter(void* context) { context); infrared_brute_force_add_record(brute_force, i++, "Vol_up"); - button_panel_add_label(button_panel, 1, 10, FontPrimary, "Mus. remote"); + button_panel_add_label(button_panel, 1, 10, FontPrimary, "Audio player"); button_panel_add_icon(button_panel, 34, 56, &I_vol_ac_text_30x30); infrared_scene_universal_common_on_enter(context); diff --git a/applications/main/infrared/scenes/infrared_scene_universal_projector.c b/applications/main/infrared/scenes/infrared_scene_universal_projector.c index c665444fb..c939a0f1b 100644 --- a/applications/main/infrared/scenes/infrared_scene_universal_projector.c +++ b/applications/main/infrared/scenes/infrared_scene_universal_projector.c @@ -63,7 +63,7 @@ void infrared_scene_universal_projector_on_enter(void* context) { context); infrared_brute_force_add_record(brute_force, i++, "Vol_dn"); - button_panel_add_label(button_panel, 3, 11, FontPrimary, "Proj. remote"); + button_panel_add_label(button_panel, 10, 11, FontPrimary, "Projector"); button_panel_add_icon(button_panel, 17, 72, &I_vol_ac_text_30x30); infrared_scene_universal_common_on_enter(context); diff --git a/applications/main/infrared/scenes/infrared_scene_universal_tv.c b/applications/main/infrared/scenes/infrared_scene_universal_tv.c index 16633e29c..6130861ee 100644 --- a/applications/main/infrared/scenes/infrared_scene_universal_tv.c +++ b/applications/main/infrared/scenes/infrared_scene_universal_tv.c @@ -91,7 +91,7 @@ void infrared_scene_universal_tv_on_enter(void* context) { context); infrared_brute_force_add_record(brute_force, i++, "Ch_prev"); - button_panel_add_label(button_panel, 5, 10, FontPrimary, "TV remote"); + button_panel_add_label(button_panel, 25, 10, FontPrimary, "TV"); infrared_scene_universal_common_on_enter(context); } diff --git a/applications/main/infrared/views/infrared_progress_view.c b/applications/main/infrared/views/infrared_progress_view.c index 432da7ff1..716027297 100644 --- a/applications/main/infrared/views/infrared_progress_view.c +++ b/applications/main/infrared/views/infrared_progress_view.c @@ -14,54 +14,80 @@ struct InfraredProgressView { View* view; - InfraredProgressViewBackCallback back_callback; + InfraredProgressViewInputCallback input_callback; void* context; }; typedef struct { size_t progress; size_t progress_total; + bool is_paused; } InfraredProgressViewModel; -bool infrared_progress_view_increase_progress(InfraredProgressView* progress) { - furi_assert(progress); - bool result = false; - - InfraredProgressViewModel* model = view_get_model(progress->view); - if(model->progress < model->progress_total) { - ++model->progress; - result = model->progress < model->progress_total; - } - view_commit_model(progress->view, true); - - return result; -} - static void infrared_progress_view_draw_callback(Canvas* canvas, void* _model) { InfraredProgressViewModel* model = (InfraredProgressViewModel*)_model; uint8_t x = 0; - uint8_t y = 36; + uint8_t y = 25; uint8_t width = 63; - uint8_t height = 59; + uint8_t height = 81; elements_bold_rounded_frame(canvas, x, y, width, height); canvas_set_font(canvas, FontSecondary); elements_multiline_text_aligned( - canvas, x + 34, y + 9, AlignCenter, AlignCenter, "Sending ..."); + canvas, + x + 32, + y + 9, + AlignCenter, + AlignCenter, + model->is_paused ? "Paused" : "Sending..."); float progress_value = (float)model->progress / model->progress_total; elements_progress_bar(canvas, x + 4, y + 19, width - 7, progress_value); - uint8_t percent_value = 100 * model->progress / model->progress_total; - char percents_string[10] = {0}; - snprintf(percents_string, sizeof(percents_string), "%d%%", percent_value); + char progress_string[16] = {0}; + if(model->is_paused) { + snprintf( + progress_string, + sizeof(progress_string), + "%zu/%zu", + model->progress, + model->progress_total); + } else { + uint8_t percent_value = 100 * model->progress / model->progress_total; + snprintf(progress_string, sizeof(progress_string), "%d%%", percent_value); + } elements_multiline_text_aligned( - canvas, x + 33, y + 37, AlignCenter, AlignCenter, percents_string); + canvas, x + 33, y + 37, AlignCenter, AlignCenter, progress_string); - canvas_draw_icon(canvas, x + 14, y + height - 14, &I_Pin_back_arrow_10x8); - canvas_draw_str(canvas, x + 30, y + height - 6, "= stop"); + uint8_t buttons_x = x + (model->is_paused ? 10 : 14); + uint8_t buttons_y = y + (model->is_paused ? 46 : 50); + + canvas_draw_icon(canvas, buttons_x + 0, buttons_y + 0, &I_Pin_back_arrow_10x8); + canvas_draw_str(canvas, buttons_x + 14, buttons_y + 8, model->is_paused ? "resume" : "stop"); + + canvas_draw_icon(canvas, buttons_x + 1, buttons_y + 10, &I_Ok_btn_9x9); + canvas_draw_str(canvas, buttons_x + 14, buttons_y + 17, model->is_paused ? "send 1" : "pause"); + + if(model->is_paused) { + canvas_draw_icon(canvas, buttons_x + 2, buttons_y + 21, &I_ButtonLeftSmall_3x5); + canvas_draw_icon(canvas, buttons_x + 7, buttons_y + 21, &I_ButtonRightSmall_3x5); + canvas_draw_str(canvas, buttons_x + 14, buttons_y + 26, "select"); + } +} + +bool infrared_progress_view_set_progress(InfraredProgressView* instance, uint16_t progress) { + bool result; + with_view_model( + instance->view, + InfraredProgressViewModel * model, + { + result = progress <= model->progress_total; + if(result) model->progress = progress; + }, + true); + return result; } void infrared_progress_view_set_progress_total( @@ -74,14 +100,40 @@ void infrared_progress_view_set_progress_total( view_commit_model(progress->view, false); } +void infrared_progress_view_set_paused(InfraredProgressView* instance, bool is_paused) { + with_view_model( + instance->view, InfraredProgressViewModel * model, { model->is_paused = is_paused; }, true); +} + bool infrared_progress_view_input_callback(InputEvent* event, void* context) { InfraredProgressView* instance = context; + if(event->type != InputTypeShort && event->type != InputTypeRepeat) return false; + if(!instance->input_callback) return false; - if((event->type == InputTypeShort) && (event->key == InputKeyBack)) { - if(instance->back_callback) { - instance->back_callback(instance->context); - } - } + with_view_model( + instance->view, + InfraredProgressViewModel * model, + { + if(model->is_paused) { + if(event->key == InputKeyLeft) + instance->input_callback( + instance->context, InfraredProgressViewInputPreviousSignal); + else if(event->key == InputKeyRight) + instance->input_callback( + instance->context, InfraredProgressViewInputNextSignal); + else if(event->key == InputKeyOk) + instance->input_callback( + instance->context, InfraredProgressViewInputSendSingle); + else if(event->key == InputKeyBack) + instance->input_callback(instance->context, InfraredProgressViewInputResume); + } else { + if(event->key == InputKeyOk) + instance->input_callback(instance->context, InfraredProgressViewInputPause); + else if(event->key == InputKeyBack) + instance->input_callback(instance->context, InfraredProgressViewInputStop); + } + }, + false); return true; } @@ -106,12 +158,12 @@ void infrared_progress_view_free(InfraredProgressView* progress) { free(progress); } -void infrared_progress_view_set_back_callback( +void infrared_progress_view_set_input_callback( InfraredProgressView* instance, - InfraredProgressViewBackCallback callback, + InfraredProgressViewInputCallback callback, void* context) { furi_assert(instance); - instance->back_callback = callback; + instance->input_callback = callback; instance->context = context; } diff --git a/applications/main/infrared/views/infrared_progress_view.h b/applications/main/infrared/views/infrared_progress_view.h index c44f1a482..c33f1e553 100644 --- a/applications/main/infrared/views/infrared_progress_view.h +++ b/applications/main/infrared/views/infrared_progress_view.h @@ -10,11 +10,20 @@ extern "C" { #endif -/** Anonumous instance */ +/** Anonymous instance */ typedef struct InfraredProgressView InfraredProgressView; -/** Callback for back button handling */ -typedef void (*InfraredProgressViewBackCallback)(void*); +typedef enum { + InfraredProgressViewInputStop, + InfraredProgressViewInputPause, + InfraredProgressViewInputResume, + InfraredProgressViewInputPreviousSignal, + InfraredProgressViewInputNextSignal, + InfraredProgressViewInputSendSingle, +} InfraredProgressViewInput; + +/** Callback for input handling */ +typedef void (*InfraredProgressViewInputCallback)(void* context, InfraredProgressViewInput event); /** Allocate and initialize Infrared view * @@ -35,13 +44,12 @@ void infrared_progress_view_free(InfraredProgressView* instance); */ View* infrared_progress_view_get_view(InfraredProgressView* instance); -/** Increase progress on progress view module +/** Set progress of progress view module * * @param instance view module - * @retval true - value is incremented and maximum is reached, - * false - value is incremented and maximum is not reached + * @param progress progress value */ -bool infrared_progress_view_increase_progress(InfraredProgressView* instance); +bool infrared_progress_view_set_progress(InfraredProgressView* instance, uint16_t progress); /** Set maximum progress value * @@ -52,15 +60,22 @@ void infrared_progress_view_set_progress_total( InfraredProgressView* instance, uint16_t progress_max); -/** Set back button callback +/** Selects the variant of the View + * + * @param instance view instance + * @param is_paused the "paused" variant is displayed if true; the "sending" one if false + */ +void infrared_progress_view_set_paused(InfraredProgressView* instance, bool is_paused); + +/** Set input callback * * @param instance - view module - * @param callback - callback to call for back button + * @param callback - callback to call for input * @param context - context to pass to callback */ -void infrared_progress_view_set_back_callback( +void infrared_progress_view_set_input_callback( InfraredProgressView* instance, - InfraredProgressViewBackCallback callback, + InfraredProgressViewInputCallback callback, void* context); #ifdef __cplusplus diff --git a/applications/services/gui/scene_manager.c b/applications/services/gui/scene_manager.c index 11acc0796..485e31d11 100644 --- a/applications/services/gui/scene_manager.c +++ b/applications/services/gui/scene_manager.c @@ -230,6 +230,11 @@ bool scene_manager_search_and_switch_to_another_scene( } } +uint32_t scene_manager_get_current_scene(SceneManager* scene_manager) { + furi_check(scene_manager); + return *SceneManagerIdStack_back(scene_manager->scene_id_stack); +} + void scene_manager_stop(SceneManager* scene_manager) { furi_check(scene_manager); diff --git a/applications/services/gui/scene_manager.h b/applications/services/gui/scene_manager.h index 54dfa9cd4..8dad92aac 100644 --- a/applications/services/gui/scene_manager.h +++ b/applications/services/gui/scene_manager.h @@ -170,6 +170,14 @@ bool scene_manager_search_and_switch_to_another_scene( SceneManager* scene_manager, uint32_t scene_id); +/** Get id of current scene + * + * @param scene_manager SceneManager instance + * + * @return Scene ID + */ +uint32_t scene_manager_get_current_scene(SceneManager* scene_manager); + /** Exit from current scene * * @param scene_manager SceneManager instance diff --git a/lib/flipper_format/flipper_format.c b/lib/flipper_format/flipper_format.c index d07022e12..8aebf853f 100644 --- a/lib/flipper_format/flipper_format.c +++ b/lib/flipper_format/flipper_format.c @@ -8,6 +8,11 @@ #include "flipper_format_stream.h" #include "flipper_format_stream_i.h" +// permits direct casting between `FlipperFormatOffset` and `StreamOffset` +static_assert((size_t)FlipperFormatOffsetFromCurrent == (size_t)StreamOffsetFromCurrent); +static_assert((size_t)FlipperFormatOffsetFromStart == (size_t)StreamOffsetFromStart); +static_assert((size_t)FlipperFormatOffsetFromEnd == (size_t)StreamOffsetFromEnd); + /********************************** Private **********************************/ struct FlipperFormat { Stream* stream; @@ -127,6 +132,17 @@ bool flipper_format_rewind(FlipperFormat* flipper_format) { return stream_rewind(flipper_format->stream); } +size_t flipper_format_tell(FlipperFormat* flipper_format) { + furi_check(flipper_format); + return stream_tell(flipper_format->stream); +} + +bool flipper_format_seek(FlipperFormat* flipper_format, int32_t offset, FlipperFormatOffset anchor) { + furi_check(flipper_format); + // direct usage of `anchor` made valid by `static_assert`s at the top of this file + return stream_seek(flipper_format->stream, offset, (StreamOffset)anchor); +} + bool flipper_format_seek_to_end(FlipperFormat* flipper_format) { furi_check(flipper_format); return stream_seek(flipper_format->stream, 0, StreamOffsetFromEnd); diff --git a/lib/flipper_format/flipper_format.h b/lib/flipper_format/flipper_format.h index 4a1bb767b..5b13496e1 100644 --- a/lib/flipper_format/flipper_format.h +++ b/lib/flipper_format/flipper_format.h @@ -94,6 +94,12 @@ extern "C" { typedef struct FlipperFormat FlipperFormat; +typedef enum { + FlipperFormatOffsetFromCurrent, + FlipperFormatOffsetFromStart, + FlipperFormatOffsetFromEnd, +} FlipperFormatOffset; + /** Allocate FlipperFormat as string. * * @return FlipperFormat* pointer to a FlipperFormat instance @@ -216,6 +222,24 @@ void flipper_format_set_strict_mode(FlipperFormat* flipper_format, bool strict_m */ bool flipper_format_rewind(FlipperFormat* flipper_format); +/** Get the RW pointer position + * + * @param flipper_format Pointer to a FlipperFormat instance + * + * @return RW pointer position + */ +size_t flipper_format_tell(FlipperFormat* flipper_format); + +/** Set the RW pointer position to an arbitrary value + * + * @param flipper_format Pointer to a FlipperFormat instance + * @param offset Offset relative to the anchor point + * @param anchor Anchor point (e.g. start of file) + * + * @return True on success + */ +bool flipper_format_seek(FlipperFormat* flipper_format, int32_t offset, FlipperFormatOffset anchor); + /** Move the RW pointer at the end. Can be useful if you want to add some data * after reading. * diff --git a/targets/f18/api_symbols.csv b/targets/f18/api_symbols.csv index d158b4f68..57eaf6ab3 100644 --- a/targets/f18/api_symbols.csv +++ b/targets/f18/api_symbols.csv @@ -1,5 +1,5 @@ entry,status,name,type,params -Version,+,80.1,, +Version,+,80.2,, Header,+,applications/services/bt/bt_service/bt.h,, Header,+,applications/services/bt/bt_service/bt_keys_storage.h,, Header,+,applications/services/cli/cli.h,, @@ -1038,6 +1038,7 @@ Function,+,flipper_format_read_int32,_Bool,"FlipperFormat*, const char*, int32_t Function,+,flipper_format_read_string,_Bool,"FlipperFormat*, const char*, FuriString*" Function,+,flipper_format_read_uint32,_Bool,"FlipperFormat*, const char*, uint32_t*, const uint16_t" Function,+,flipper_format_rewind,_Bool,FlipperFormat* +Function,+,flipper_format_seek,_Bool,"FlipperFormat*, int32_t, FlipperFormatOffset" Function,+,flipper_format_seek_to_end,_Bool,FlipperFormat* Function,+,flipper_format_set_strict_mode,void,"FlipperFormat*, _Bool" Function,+,flipper_format_stream_delete_key_and_write,_Bool,"Stream*, FlipperStreamWriteData*, _Bool" @@ -1046,6 +1047,7 @@ Function,+,flipper_format_stream_read_value_line,_Bool,"Stream*, const char*, Fl Function,+,flipper_format_stream_write_comment_cstr,_Bool,"Stream*, const char*" Function,+,flipper_format_stream_write_value_line,_Bool,"Stream*, FlipperStreamWriteData*" Function,+,flipper_format_string_alloc,FlipperFormat*, +Function,+,flipper_format_tell,size_t,FlipperFormat* Function,+,flipper_format_update_bool,_Bool,"FlipperFormat*, const char*, const _Bool*, const uint16_t" Function,+,flipper_format_update_float,_Bool,"FlipperFormat*, const char*, const float*, const uint16_t" Function,+,flipper_format_update_hex,_Bool,"FlipperFormat*, const char*, const uint8_t*, const uint16_t" @@ -2468,6 +2470,7 @@ Function,-,scalbnl,long double,"long double, int" Function,-,scanf,int,"const char*, ..." Function,+,scene_manager_alloc,SceneManager*,"const SceneManagerHandlers*, void*" Function,+,scene_manager_free,void,SceneManager* +Function,+,scene_manager_get_current_scene,uint32_t,SceneManager* Function,+,scene_manager_get_scene_state,uint32_t,"const SceneManager*, uint32_t" Function,+,scene_manager_handle_back_event,_Bool,SceneManager* Function,+,scene_manager_handle_custom_event,_Bool,"SceneManager*, uint32_t" diff --git a/targets/f7/api_symbols.csv b/targets/f7/api_symbols.csv index 2c3b696cb..900539019 100644 --- a/targets/f7/api_symbols.csv +++ b/targets/f7/api_symbols.csv @@ -1,5 +1,5 @@ entry,status,name,type,params -Version,+,80.1,, +Version,+,80.2,, Header,+,applications/drivers/subghz/cc1101_ext/cc1101_ext_interconnect.h,, Header,+,applications/services/bt/bt_service/bt.h,, Header,+,applications/services/bt/bt_service/bt_keys_storage.h,, @@ -1149,6 +1149,7 @@ Function,+,flipper_format_read_int32,_Bool,"FlipperFormat*, const char*, int32_t Function,+,flipper_format_read_string,_Bool,"FlipperFormat*, const char*, FuriString*" Function,+,flipper_format_read_uint32,_Bool,"FlipperFormat*, const char*, uint32_t*, const uint16_t" Function,+,flipper_format_rewind,_Bool,FlipperFormat* +Function,+,flipper_format_seek,_Bool,"FlipperFormat*, int32_t, FlipperFormatOffset" Function,+,flipper_format_seek_to_end,_Bool,FlipperFormat* Function,+,flipper_format_set_strict_mode,void,"FlipperFormat*, _Bool" Function,+,flipper_format_stream_delete_key_and_write,_Bool,"Stream*, FlipperStreamWriteData*, _Bool" @@ -1157,6 +1158,7 @@ Function,+,flipper_format_stream_read_value_line,_Bool,"Stream*, const char*, Fl Function,+,flipper_format_stream_write_comment_cstr,_Bool,"Stream*, const char*" Function,+,flipper_format_stream_write_value_line,_Bool,"Stream*, FlipperStreamWriteData*" Function,+,flipper_format_string_alloc,FlipperFormat*, +Function,+,flipper_format_tell,size_t,FlipperFormat* Function,+,flipper_format_update_bool,_Bool,"FlipperFormat*, const char*, const _Bool*, const uint16_t" Function,+,flipper_format_update_float,_Bool,"FlipperFormat*, const char*, const float*, const uint16_t" Function,+,flipper_format_update_hex,_Bool,"FlipperFormat*, const char*, const uint8_t*, const uint16_t" @@ -3105,6 +3107,7 @@ Function,-,scalbnl,long double,"long double, int" Function,-,scanf,int,"const char*, ..." Function,+,scene_manager_alloc,SceneManager*,"const SceneManagerHandlers*, void*" Function,+,scene_manager_free,void,SceneManager* +Function,+,scene_manager_get_current_scene,uint32_t,SceneManager* Function,+,scene_manager_get_scene_state,uint32_t,"const SceneManager*, uint32_t" Function,+,scene_manager_handle_back_event,_Bool,SceneManager* Function,+,scene_manager_handle_custom_event,_Bool,"SceneManager*, uint32_t" From 77445fd2f56da00593ece165dc28a4d14ffc44e9 Mon Sep 17 00:00:00 2001 From: Eric Betts Date: Wed, 19 Feb 2025 16:22:01 -0800 Subject: [PATCH 048/268] Faster di card reading (#4087) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: あく --- applications/main/nfc/application.fam | 10 ++ .../plugins/supported_cards/disney_infinity.c | 121 ++++++++++++++++++ 2 files changed, 131 insertions(+) create mode 100644 applications/main/nfc/plugins/supported_cards/disney_infinity.c diff --git a/applications/main/nfc/application.fam b/applications/main/nfc/application.fam index 6dbde7c37..29bdf390a 100644 --- a/applications/main/nfc/application.fam +++ b/applications/main/nfc/application.fam @@ -247,6 +247,16 @@ App( sources=["plugins/supported_cards/ndef.c"], ) +App( + appid="disney_infinity_parser", + apptype=FlipperAppType.PLUGIN, + entry_point="disney_infinity_plugin_ep", + targets=["f7"], + requires=["nfc"], + fap_libs=["mbedtls"], + sources=["plugins/supported_cards/disney_infinity.c"], +) + App( appid="nfc_start", targets=["f7"], diff --git a/applications/main/nfc/plugins/supported_cards/disney_infinity.c b/applications/main/nfc/plugins/supported_cards/disney_infinity.c new file mode 100644 index 000000000..a98d39ec2 --- /dev/null +++ b/applications/main/nfc/plugins/supported_cards/disney_infinity.c @@ -0,0 +1,121 @@ +#include +#include "nfc_supported_card_plugin.h" + +#include + +#include +#include +#include +#include + +#define TAG "DisneyInfinity" +#define UID_LEN 7 + +// Derived from https://nfc.toys/#new-interoperability-for-infinity +static uint8_t seed[38] = {0x0A, 0x14, 0xFD, 0x05, 0x07, 0xFF, 0x4B, 0xCD, 0x02, 0x6B, + 0xA8, 0x3F, 0x0A, 0x3B, 0x89, 0xA9, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x28, 0x63, 0x29, 0x20, 0x44, 0x69, 0x73, + 0x6E, 0x65, 0x79, 0x20, 0x32, 0x30, 0x31, 0x33}; + +void di_key(const uint8_t* uid, MfClassicKey* key) { + uint8_t hash[20]; + memcpy(seed + 16, uid, UID_LEN); + mbedtls_sha1(seed, sizeof(seed), hash); + key->data[0] = hash[3]; + key->data[1] = hash[2]; + key->data[2] = hash[1]; + key->data[3] = hash[0]; + key->data[4] = hash[7]; + key->data[5] = hash[6]; +} + +static bool disney_infinity_read(Nfc* nfc, NfcDevice* device) { + furi_assert(nfc); + furi_assert(device); + size_t* uid_len = 0; + bool is_read = false; + MfClassicData* data = mf_classic_alloc(); + + nfc_device_copy_data(device, NfcProtocolMfClassic, data); + const uint8_t* uid_bytes = mf_classic_get_uid(data, uid_len); + MfClassicDeviceKeys keys = {}; + + do { + MfClassicType type = MfClassicTypeMini; + MfClassicError error = mf_classic_poller_sync_detect_type(nfc, &type); + if(error != MfClassicErrorNone) break; + + data->type = type; + for(size_t i = 0; i < mf_classic_get_total_sectors_num(data->type); i++) { + di_key(uid_bytes, &keys.key_a[i]); + di_key(uid_bytes, &keys.key_b[i]); + FURI_BIT_SET(keys.key_a_mask, i); + FURI_BIT_SET(keys.key_b_mask, i); + } + + error = mf_classic_poller_sync_read(nfc, &keys, data); + if(error != MfClassicErrorNone) { + FURI_LOG_W(TAG, "Failed to read data: %d", error); + break; + } + + nfc_device_set_data(device, NfcProtocolMfClassic, data); + + is_read = mf_classic_is_card_read(data); + } while(false); + + mf_classic_free(data); + + return is_read; +} + +static bool disney_infinity_parse(const NfcDevice* device, FuriString* parsed_data) { + furi_assert(device); + size_t* uid_len = 0; + bool parsed = false; + FuriString* name = furi_string_alloc(); + const uint8_t verify_sector = 0; + MfClassicKey key = {}; + + const MfClassicData* data = nfc_device_get_data(device, NfcProtocolMfClassic); + const uint8_t* uid_bytes = mf_classic_get_uid(data, uid_len); + + do { + // verify key + MfClassicSectorTrailer* sec_tr = + mf_classic_get_sector_trailer_by_sector(data, verify_sector); + + di_key(uid_bytes, &key); + if(memcmp(key.data, sec_tr->key_a.data, 6) != 0) break; + + // At some point I'd like to add name lookup like Skylanders + furi_string_printf(parsed_data, "\e#Disney Infinity\n"); + + parsed = true; + + } while(false); + + furi_string_free(name); + + return parsed; +} + +/* Actual implementation of app<>plugin interface */ +static const NfcSupportedCardsPlugin disney_infinity_plugin = { + .protocol = NfcProtocolMfClassic, + .verify = NULL, // Need UID to verify key(s) + .read = disney_infinity_read, + .parse = disney_infinity_parse, +}; + +/* Plugin descriptor to comply with basic plugin specification */ +static const FlipperAppPluginDescriptor disney_infinity_plugin_descriptor = { + .appid = NFC_SUPPORTED_CARD_PLUGIN_APP_ID, + .ep_api_version = NFC_SUPPORTED_CARD_PLUGIN_API_VERSION, + .entry_point = &disney_infinity_plugin, +}; + +/* Plugin entry point - must return a pointer to const descriptor */ +const FlipperAppPluginDescriptor* disney_infinity_plugin_ep(void) { + return &disney_infinity_plugin_descriptor; +} From 04fa7a9a7fa337dd1344f845b41db4bc925bcdf2 Mon Sep 17 00:00:00 2001 From: Zinong Li <131403964+zinongli@users.noreply.github.com> Date: Wed, 19 Feb 2025 19:36:39 -0500 Subject: [PATCH 049/268] LFRFID: Noralsy Format/Brand (#4090) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * beta version * Working. No parsing yet. No checksum yet. * T5 config caveat * parsings Co-authored-by: あく --- lib/lfrfid/protocols/lfrfid_protocols.c | 2 + lib/lfrfid/protocols/lfrfid_protocols.h | 1 + lib/lfrfid/protocols/protocol_noralsy.c | 233 ++++++++++++++++++++++++ lib/lfrfid/protocols/protocol_noralsy.h | 4 + 4 files changed, 240 insertions(+) create mode 100644 lib/lfrfid/protocols/protocol_noralsy.c create mode 100644 lib/lfrfid/protocols/protocol_noralsy.h diff --git a/lib/lfrfid/protocols/lfrfid_protocols.c b/lib/lfrfid/protocols/lfrfid_protocols.c index 8ea1f2b49..238f8e0cd 100644 --- a/lib/lfrfid/protocols/lfrfid_protocols.c +++ b/lib/lfrfid/protocols/lfrfid_protocols.c @@ -20,6 +20,7 @@ #include "protocol_nexwatch.h" #include "protocol_securakey.h" #include "protocol_gproxii.h" +#include "protocol_noralsy.h" const ProtocolBase* lfrfid_protocols[] = { [LFRFIDProtocolEM4100] = &protocol_em4100, @@ -45,4 +46,5 @@ const ProtocolBase* lfrfid_protocols[] = { [LFRFIDProtocolNexwatch] = &protocol_nexwatch, [LFRFIDProtocolSecurakey] = &protocol_securakey, [LFRFIDProtocolGProxII] = &protocol_gproxii, + [LFRFIDProtocolNoralsy] = &protocol_noralsy, }; diff --git a/lib/lfrfid/protocols/lfrfid_protocols.h b/lib/lfrfid/protocols/lfrfid_protocols.h index 37b7f06cd..86c31c60b 100644 --- a/lib/lfrfid/protocols/lfrfid_protocols.h +++ b/lib/lfrfid/protocols/lfrfid_protocols.h @@ -32,6 +32,7 @@ typedef enum { LFRFIDProtocolNexwatch, LFRFIDProtocolSecurakey, LFRFIDProtocolGProxII, + LFRFIDProtocolNoralsy, LFRFIDProtocolMax, } LFRFIDProtocol; diff --git a/lib/lfrfid/protocols/protocol_noralsy.c b/lib/lfrfid/protocols/protocol_noralsy.c new file mode 100644 index 000000000..8ee9ab30a --- /dev/null +++ b/lib/lfrfid/protocols/protocol_noralsy.c @@ -0,0 +1,233 @@ +#include +#include +#include +#include +#include "lfrfid_protocols.h" + +#define NORALSY_CLOCK_PER_BIT (32) + +#define NORALSY_ENCODED_BIT_SIZE (96) +#define NORALSY_ENCODED_BYTE_SIZE ((NORALSY_ENCODED_BIT_SIZE) / 8) +#define NORALSY_PREAMBLE_BIT_SIZE (32) +#define NORALSY_PREAMBLE_BYTE_SIZE ((NORALSY_PREAMBLE_BIT_SIZE) / 8) +#define NORALSY_ENCODED_BYTE_FULL_SIZE ((NORALSY_ENCODED_BIT_SIZE) / 8) +#define NORALSY_DECODED_DATA_SIZE ((NORALSY_ENCODED_BIT_SIZE) / 8) + +#define NORALSY_READ_SHORT_TIME (128) +#define NORALSY_READ_LONG_TIME (256) +#define NORALSY_READ_JITTER_TIME (60) + +#define NORALSY_READ_SHORT_TIME_LOW (NORALSY_READ_SHORT_TIME - NORALSY_READ_JITTER_TIME) +#define NORALSY_READ_SHORT_TIME_HIGH (NORALSY_READ_SHORT_TIME + NORALSY_READ_JITTER_TIME) +#define NORALSY_READ_LONG_TIME_LOW (NORALSY_READ_LONG_TIME - NORALSY_READ_JITTER_TIME) +#define NORALSY_READ_LONG_TIME_HIGH (NORALSY_READ_LONG_TIME + NORALSY_READ_JITTER_TIME) + +#define TAG "NORALSY" + +typedef struct { + uint8_t data[NORALSY_ENCODED_BYTE_SIZE]; + uint8_t encoded_data[NORALSY_ENCODED_BYTE_SIZE]; + + uint8_t encoded_data_index; + bool encoded_polarity; + + ManchesterState decoder_manchester_state; +} ProtocolNoralsy; + +ProtocolNoralsy* protocol_noralsy_alloc(void) { + ProtocolNoralsy* protocol = malloc(sizeof(ProtocolNoralsy)); + return (void*)protocol; +} + +void protocol_noralsy_free(ProtocolNoralsy* protocol) { + free(protocol); +} + +static uint8_t noralsy_chksum(uint8_t* bits, uint8_t len) { + uint8_t sum = 0; + for(uint8_t i = 0; i < len; i += 4) + sum ^= bit_lib_get_bits(bits, i, 4); + return sum & 0x0F; +} + +uint8_t* protocol_noralsy_get_data(ProtocolNoralsy* protocol) { + return protocol->data; +} + +static void protocol_noralsy_decode(ProtocolNoralsy* protocol) { + bit_lib_copy_bits(protocol->data, 0, NORALSY_ENCODED_BIT_SIZE, protocol->encoded_data, 0); +} + +static bool protocol_noralsy_can_be_decoded(ProtocolNoralsy* protocol) { + // check 12 bits preamble + // If necessary, use 0xBB0214FF for 32 bit preamble check + // However, it is not confirmed the 13-16 bit are static. + if(bit_lib_get_bits_16(protocol->encoded_data, 0, 12) != 0b101110110000) return false; + uint8_t calc1 = noralsy_chksum(&protocol->encoded_data[4], 40); + uint8_t calc2 = noralsy_chksum(&protocol->encoded_data[0], 76); + uint8_t chk1 = bit_lib_get_bits(protocol->encoded_data, 72, 4); + uint8_t chk2 = bit_lib_get_bits(protocol->encoded_data, 76, 4); + if(calc1 != chk1 || calc2 != chk2) return false; + + return true; +} + +void protocol_noralsy_decoder_start(ProtocolNoralsy* protocol) { + memset(protocol->encoded_data, 0, NORALSY_ENCODED_BYTE_FULL_SIZE); + manchester_advance( + protocol->decoder_manchester_state, + ManchesterEventReset, + &protocol->decoder_manchester_state, + NULL); +} + +bool protocol_noralsy_decoder_feed(ProtocolNoralsy* protocol, bool level, uint32_t duration) { + bool result = false; + + ManchesterEvent event = ManchesterEventReset; + + if(duration > NORALSY_READ_SHORT_TIME_LOW && duration < NORALSY_READ_SHORT_TIME_HIGH) { + if(!level) { + event = ManchesterEventShortHigh; + } else { + event = ManchesterEventShortLow; + } + } else if(duration > NORALSY_READ_LONG_TIME_LOW && duration < NORALSY_READ_LONG_TIME_HIGH) { + if(!level) { + event = ManchesterEventLongHigh; + } else { + event = ManchesterEventLongLow; + } + } + + if(event != ManchesterEventReset) { + bool data; + bool data_ok = manchester_advance( + protocol->decoder_manchester_state, event, &protocol->decoder_manchester_state, &data); + + if(data_ok) { + bit_lib_push_bit(protocol->encoded_data, NORALSY_ENCODED_BYTE_FULL_SIZE, data); + + if(protocol_noralsy_can_be_decoded(protocol)) { + protocol_noralsy_decode(protocol); + result = true; + } + } + } + + return result; +} + +bool protocol_noralsy_encoder_start(ProtocolNoralsy* protocol) { + bit_lib_copy_bits(protocol->encoded_data, 0, NORALSY_ENCODED_BIT_SIZE, protocol->data, 0); + + return true; +} + +LevelDuration protocol_noralsy_encoder_yield(ProtocolNoralsy* protocol) { + bool level = bit_lib_get_bit(protocol->encoded_data, protocol->encoded_data_index); + uint32_t duration = NORALSY_CLOCK_PER_BIT / 2; + + if(protocol->encoded_polarity) { + protocol->encoded_polarity = false; + } else { + level = !level; + + protocol->encoded_polarity = true; + bit_lib_increment_index(protocol->encoded_data_index, NORALSY_ENCODED_BIT_SIZE); + } + + return level_duration_make(level, duration); +} + +bool protocol_noralsy_write_data(ProtocolNoralsy* protocol, void* data) { + LFRFIDWriteRequest* request = (LFRFIDWriteRequest*)data; + bool result = false; + + // Correct protocol data by redecoding + protocol_noralsy_encoder_start(protocol); + protocol_noralsy_decode(protocol); + + protocol_noralsy_encoder_start(protocol); + + if(request->write_type == LFRFIDWriteTypeT5577) { + request->t5577.block[0] = + (LFRFID_T5577_MODULATION_MANCHESTER | LFRFID_T5577_BITRATE_RF_32 | + (3 << LFRFID_T5577_MAXBLOCK_SHIFT) | LFRFID_T5577_ST_TERMINATOR); + // In fact, base on the current two dump samples from Iceman server, + // Noralsy are usually T5577s with config = 0x00088C6A + // But the `C` and `A` are not explainable by the ATA5577C datasheet + // and they don't affect reading whatsoever. + // So we are mimicing Proxmark's solution here. Leave those nibbles as zero. + request->t5577.block[1] = bit_lib_get_bits_32(protocol->encoded_data, 0, 32); + request->t5577.block[2] = bit_lib_get_bits_32(protocol->encoded_data, 32, 32); + request->t5577.block[3] = bit_lib_get_bits_32(protocol->encoded_data, 64, 32); + request->t5577.blocks_to_write = 4; + result = true; + } + return result; +} + +static void protocol_noralsy_render_data_internal( + ProtocolNoralsy* protocol, + FuriString* result, + bool brief) { + UNUSED(protocol); + uint32_t raw2 = bit_lib_get_bits_32(protocol->data, 32, 32); + uint32_t raw3 = bit_lib_get_bits_32(protocol->data, 64, 32); + uint32_t cardid = ((raw2 & 0xFFF00000) >> 20) << 16; + cardid |= (raw2 & 0xFF) << 8; + cardid |= ((raw3 & 0xFF000000) >> 24); + + uint8_t year = (raw2 & 0x000ff000) >> 12; + bool tag_is_gen_z = (year > 0x60); + if(brief) { + furi_string_printf( + result, + "Card ID: %07lx\n" + "Year: %s%02x", + cardid, + tag_is_gen_z ? "19" : "20", + year); + } else { + furi_string_printf( + result, + "Card ID: %07lx\n" + "Year: %s%02x", + cardid, + tag_is_gen_z ? "19" : "20", + year); + } +} + +void protocol_noralsy_render_data(ProtocolNoralsy* protocol, FuriString* result) { + protocol_noralsy_render_data_internal(protocol, result, false); +} + +void protocol_noralsy_render_brief_data(ProtocolNoralsy* protocol, FuriString* result) { + protocol_noralsy_render_data_internal(protocol, result, true); +} + +const ProtocolBase protocol_noralsy = { + .name = "Noralsy", + .manufacturer = "Noralsy", + .data_size = NORALSY_DECODED_DATA_SIZE, + .features = LFRFIDFeatureASK, + .validate_count = 3, + .alloc = (ProtocolAlloc)protocol_noralsy_alloc, + .free = (ProtocolFree)protocol_noralsy_free, + .get_data = (ProtocolGetData)protocol_noralsy_get_data, + .decoder = + { + .start = (ProtocolDecoderStart)protocol_noralsy_decoder_start, + .feed = (ProtocolDecoderFeed)protocol_noralsy_decoder_feed, + }, + .encoder = + { + .start = (ProtocolEncoderStart)protocol_noralsy_encoder_start, + .yield = (ProtocolEncoderYield)protocol_noralsy_encoder_yield, + }, + .render_data = (ProtocolRenderData)protocol_noralsy_render_data, + .render_brief_data = (ProtocolRenderData)protocol_noralsy_render_brief_data, + .write_data = (ProtocolWriteData)protocol_noralsy_write_data, +}; diff --git a/lib/lfrfid/protocols/protocol_noralsy.h b/lib/lfrfid/protocols/protocol_noralsy.h new file mode 100644 index 000000000..b1ee51a2e --- /dev/null +++ b/lib/lfrfid/protocols/protocol_noralsy.h @@ -0,0 +1,4 @@ +#pragma once +#include + +extern const ProtocolBase protocol_noralsy; From 3698fc8d02c01a966d3300fb211f7fb515cc1ef5 Mon Sep 17 00:00:00 2001 From: Justin Nesselrotte Date: Wed, 19 Feb 2025 18:10:41 -0700 Subject: [PATCH 050/268] Fixed repeat in subghz tx_from_file command (#4099) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fixed repeat in subghz tx_from_file command * Fix PVS warnings Co-authored-by: あく --- applications/main/subghz/subghz_cli.c | 2 +- lib/lfrfid/protocols/protocol_noralsy.c | 33 ++++++++----------------- 2 files changed, 11 insertions(+), 24 deletions(-) diff --git a/applications/main/subghz/subghz_cli.c b/applications/main/subghz/subghz_cli.c index 6375f2eee..88f9bfa38 100644 --- a/applications/main/subghz/subghz_cli.c +++ b/applications/main/subghz/subghz_cli.c @@ -615,7 +615,7 @@ void subghz_cli_command_tx_from_file(Cli* cli, FuriString* args, void* context) if(furi_string_size(args)) { char* args_cstr = (char*)furi_string_get_cstr(args); StrintParseError parse_err = StrintParseNoError; - parse_err |= strint_to_uint32(args_cstr, &args_cstr, &frequency, 10); + parse_err |= strint_to_uint32(args_cstr, &args_cstr, &repeat, 10); parse_err |= strint_to_uint32(args_cstr, &args_cstr, &device_ind, 10); if(parse_err) { cli_print_usage( diff --git a/lib/lfrfid/protocols/protocol_noralsy.c b/lib/lfrfid/protocols/protocol_noralsy.c index 8ee9ab30a..27cf8cb6b 100644 --- a/lib/lfrfid/protocols/protocol_noralsy.c +++ b/lib/lfrfid/protocols/protocol_noralsy.c @@ -168,10 +168,7 @@ bool protocol_noralsy_write_data(ProtocolNoralsy* protocol, void* data) { return result; } -static void protocol_noralsy_render_data_internal( - ProtocolNoralsy* protocol, - FuriString* result, - bool brief) { +static void protocol_noralsy_render_data_internal(ProtocolNoralsy* protocol, FuriString* result) { UNUSED(protocol); uint32_t raw2 = bit_lib_get_bits_32(protocol->data, 32, 32); uint32_t raw3 = bit_lib_get_bits_32(protocol->data, 64, 32); @@ -181,31 +178,21 @@ static void protocol_noralsy_render_data_internal( uint8_t year = (raw2 & 0x000ff000) >> 12; bool tag_is_gen_z = (year > 0x60); - if(brief) { - furi_string_printf( - result, - "Card ID: %07lx\n" - "Year: %s%02x", - cardid, - tag_is_gen_z ? "19" : "20", - year); - } else { - furi_string_printf( - result, - "Card ID: %07lx\n" - "Year: %s%02x", - cardid, - tag_is_gen_z ? "19" : "20", - year); - } + furi_string_printf( + result, + "Card ID: %07lx\n" + "Year: %s%02x", + cardid, + tag_is_gen_z ? "19" : "20", + year); } void protocol_noralsy_render_data(ProtocolNoralsy* protocol, FuriString* result) { - protocol_noralsy_render_data_internal(protocol, result, false); + protocol_noralsy_render_data_internal(protocol, result); } void protocol_noralsy_render_brief_data(ProtocolNoralsy* protocol, FuriString* result) { - protocol_noralsy_render_data_internal(protocol, result, true); + protocol_noralsy_render_data_internal(protocol, result); } const ProtocolBase protocol_noralsy = { From cd94db3b91957c6997f60cd84a6036e1e05a0a2a Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Thu, 20 Feb 2025 04:39:28 +0300 Subject: [PATCH 051/268] FIX GANG QI --- .../helpers/subghz_txrx_create_protocol_key.c | 35 +++------- lib/subghz/protocols/gangqi.c | 68 +++++++------------ 2 files changed, 31 insertions(+), 72 deletions(-) diff --git a/applications/main/subghz/helpers/subghz_txrx_create_protocol_key.c b/applications/main/subghz/helpers/subghz_txrx_create_protocol_key.c index a79f5dbea..417f3d558 100644 --- a/applications/main/subghz/helpers/subghz_txrx_create_protocol_key.c +++ b/applications/main/subghz/helpers/subghz_txrx_create_protocol_key.c @@ -384,33 +384,14 @@ bool subghz_txrx_gen_secplus_v1_protocol( } void subghz_txrx_gen_serial_gangqi(uint64_t* result_key) { - uint64_t randkey; - uint64_t only_required_bytes; - uint16_t sum_of_3bytes; - uint8_t xorbytes; - - do { - randkey = (uint64_t)rand(); - only_required_bytes = (randkey & 0x0FFFF0000) | 0x200000000; - sum_of_3bytes = ((only_required_bytes >> 32) & 0xFF) + - ((only_required_bytes >> 24) & 0xFF) + - ((only_required_bytes >> 16) & 0xFF); - xorbytes = ((only_required_bytes >> 32) & 0xFF) ^ ((only_required_bytes >> 24) & 0xFF) ^ - ((only_required_bytes >> 16) & 0xFF); - } while( - !((((!(sum_of_3bytes & 0x3)) && ((0xB < sum_of_3bytes) && (sum_of_3bytes < 0x141))) && - ((((only_required_bytes >> 32) & 0xFF) == 0x2) || - (((only_required_bytes >> 32) & 0xFF) == 0x3))) && - ((((xorbytes == 0xBA) || (xorbytes == 0xE2)) || - ((xorbytes == 0x3A) || (xorbytes == 0xF2))) || - (xorbytes == 0xB2)))); - - // Serial 01 button 01 - uint64_t new_key = only_required_bytes | (0b01 << 14) | (0xD << 10) | (0b01 << 8); - - uint8_t crc = -0xD7 - ((new_key >> 32) & 0xFF) - ((new_key >> 24) & 0xFF) - - ((new_key >> 16) & 0xFF) - ((new_key >> 8) & 0xFF); + uint64_t randkey = (uint64_t)rand(); + uint16_t serial = (uint16_t)((randkey) & 0xFFFF); + uint8_t const_and_button = (uint8_t)(0xD0 | 0xD); + uint8_t serial_high = (uint8_t)(serial >> 8); + uint8_t serial_low = (uint8_t)(serial & 0xFF); + uint8_t crc = (uint8_t)(0xC8 - serial_high - serial_low - const_and_button); // Add crc sum to the end - *result_key = (new_key | crc); + // serial | const_and_button + *result_key = (serial << 18) | (const_and_button << 10) | (crc << 2); } diff --git a/lib/subghz/protocols/gangqi.c b/lib/subghz/protocols/gangqi.c index 0cb76393d..5d350e6a0 100644 --- a/lib/subghz/protocols/gangqi.c +++ b/lib/subghz/protocols/gangqi.c @@ -168,13 +168,14 @@ static void subghz_protocol_encoder_gangqi_get_upload(SubGhzProtocolEncoderGangQ // Generate new key using custom or default button instance->generic.btn = subghz_protocol_gangqi_get_btn_code(); - uint64_t new_key = (instance->generic.data >> 14) << 14 | (instance->generic.btn << 10) | - (0b01 << 8); + uint16_t serial = (uint16_t)((instance->generic.data >> 18) & 0xFFFF); + uint8_t const_and_button = (uint8_t)(0xD0 | instance->generic.btn); + uint8_t serial_high = (uint8_t)(serial >> 8); + uint8_t serial_low = (uint8_t)(serial & 0xFF); + uint8_t bytesum = (uint8_t)(0xC8 - serial_high - serial_low - const_and_button); - uint8_t crc = -0xD7 - ((new_key >> 32) & 0xFF) - ((new_key >> 24) & 0xFF) - - ((new_key >> 16) & 0xFF) - ((new_key >> 8) & 0xFF); - - instance->generic.data = (new_key | crc); + instance->generic.data = (instance->generic.data >> 14) << 14 | (instance->generic.btn << 10) | + (bytesum << 2); size_t index = 0; @@ -230,41 +231,12 @@ static void subghz_protocol_gangqi_remote_controller(SubGhzBlockGeneric* instanc subghz_custom_btn_set_max(3); // GangQi Decoder - // 09.2024 - @xMasterX (MMX) + // 09.2024 - @xMasterX (MMX) (last update - bytesum calculation at 02.2025) // Thanks @Skorpionm for support! + // Thanks @Drone1950 and @mishamyte (who spent 2 weeks on this) for making this work properly - //// 4D=F8=171=229 byte sum should be always the same - // Button - // Serial || BBBB || CRC (byte sum) with overflow and starting point 0xD7 - //034AAB75BC = 00110100101010101011 01 1101 01 101111 00 // A (0xD) - //034AAB79B8 = 00110100101010101011 01 1110 01 101110 00 // B (0xE) - //034AAB6DC4 = 00110100101010101011 01 1011 01 110001 00 // C (0xB) - //034AAB5DD4 = 00110100101010101011 01 0111 01 110101 00 // D (0x7) - //034AAB55DC = 00110100101010101011 01 0101 01 110111 00 // Settings (0x5) - //034AAB51E0 = 00110100101010101011 01 0100 01 111000 00 // A (0x4) - //034AAB49E8 = 00110100101010101011 01 0010 01 111010 00 // C (0x2) - //034AAB59D8 = 00110100101010101011 01 0110 01 110110 00 // D (0x6) - //034AAB45EC = 00110100101010101011 01 0001 01 111011 00 // Settings exit (0x1) - // - // Serial 3 bytes should meet requirements see validation example at subghz_protocol_decoder_gangqi_get_string - // - // Code for finding start byte for crc sum - // - //uint64_t test = 0x034AAB79B8; //B8 - //for(size_t byte = 0; byte < 0xFF; ++byte) { - // uint8_t crc_res = -byte - ((test >> 32) & 0xFF) - ((test >> 24) & 0xFF) - - // ((test >> 16) & 0xFF) - ((test >> 8) & 0xFF); - // if(crc_res == 0xB8) { - // uint64_t test2 = 0x034AAB6DC4; //C4 - // uint8_t crc_res2 = -byte - ((test2 >> 32) & 0xFF) - ((test2 >> 24) & 0xFF) - - // ((test2 >> 16) & 0xFF) - ((test2 >> 8) & 0xFF); - // if(crc_res2 == 0xC4) { - // printf("Start byte for CRC = %02lX / CRC = %02X \n", byte, crc_res); - // - // printf("Testing second parcel CRC = %02X", crc_res2); - // } - // } - // } + // Example of correct bytesum calculation + // 0xC8 - serial_high - serial_low - constant_and_button } SubGhzProtocolStatus @@ -483,23 +455,29 @@ void subghz_protocol_decoder_gangqi_get_string(void* context, FuriString* output // Parse serial subghz_protocol_gangqi_remote_controller(&instance->generic); - // Get CRC - uint8_t crc = -0xD7 - ((instance->generic.data >> 32) & 0xFF) - - ((instance->generic.data >> 24) & 0xFF) - - ((instance->generic.data >> 16) & 0xFF) - ((instance->generic.data >> 8) & 0xFF); + // Get byte sum + uint16_t serial = (uint16_t)((instance->generic.data >> 18) & 0xFFFF); + uint8_t const_and_button = (uint8_t)(0xD0 | instance->generic.btn); + uint8_t serial_high = (uint8_t)(serial >> 8); + uint8_t serial_low = (uint8_t)(serial & 0xFF); + // Type 1 is what original remotes use, type 2 is "backdoor" sum that receiver accepts too + uint8_t sum_type1 = (uint8_t)(0xC8 - serial_high - serial_low - const_and_button); + uint8_t sum_type2 = (uint8_t)(0x02 + serial_high + serial_low + const_and_button); furi_string_cat_printf( output, "%s %db\r\n" "Key: 0x%X%08lX\r\n" - "Serial: 0x%05lX CRC: 0x%02X\r\n" + "Serial: 0x%05lX\r\n" + "Sum: 0x%02X Sum2: 0x%02X\r\n" "Btn: 0x%01X - %s\r\n", instance->generic.protocol_name, instance->generic.data_count_bit, (uint8_t)(instance->generic.data >> 32), (uint32_t)(instance->generic.data & 0xFFFFFFFF), instance->generic.serial, - crc, + sum_type1, + sum_type2, instance->generic.btn, subghz_protocol_gangqi_get_button_name(instance->generic.btn)); } From fa2af5a826ab52f1321e09411ccc1655fa11bc6a Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Thu, 20 Feb 2025 04:41:02 +0300 Subject: [PATCH 052/268] fix naming --- .../helpers/subghz_txrx_create_protocol_key.c | 6 +++--- lib/subghz/protocols/hollarm.c | 20 +++++++++---------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/applications/main/subghz/helpers/subghz_txrx_create_protocol_key.c b/applications/main/subghz/helpers/subghz_txrx_create_protocol_key.c index 417f3d558..63b892401 100644 --- a/applications/main/subghz/helpers/subghz_txrx_create_protocol_key.c +++ b/applications/main/subghz/helpers/subghz_txrx_create_protocol_key.c @@ -389,9 +389,9 @@ void subghz_txrx_gen_serial_gangqi(uint64_t* result_key) { uint8_t const_and_button = (uint8_t)(0xD0 | 0xD); uint8_t serial_high = (uint8_t)(serial >> 8); uint8_t serial_low = (uint8_t)(serial & 0xFF); - uint8_t crc = (uint8_t)(0xC8 - serial_high - serial_low - const_and_button); + uint8_t bytesum = (uint8_t)(0xC8 - serial_high - serial_low - const_and_button); - // Add crc sum to the end + // Add bytesum to the end // serial | const_and_button - *result_key = (serial << 18) | (const_and_button << 10) | (crc << 2); + *result_key = (serial << 18) | (const_and_button << 10) | (bytesum << 2); } diff --git a/lib/subghz/protocols/hollarm.c b/lib/subghz/protocols/hollarm.c index ed94cb7a9..ab2ce342f 100644 --- a/lib/subghz/protocols/hollarm.c +++ b/lib/subghz/protocols/hollarm.c @@ -169,10 +169,10 @@ static void subghz_protocol_encoder_hollarm_get_upload(SubGhzProtocolEncoderHoll uint64_t new_key = (instance->generic.data >> 12) << 12 | (instance->generic.btn << 8); - uint8_t crc = ((new_key >> 32) & 0xFF) + ((new_key >> 24) & 0xFF) + ((new_key >> 16) & 0xFF) + - ((new_key >> 8) & 0xFF); + uint8_t bytesum = ((new_key >> 32) & 0xFF) + ((new_key >> 24) & 0xFF) + + ((new_key >> 16) & 0xFF) + ((new_key >> 8) & 0xFF); - instance->generic.data = (new_key | crc); + instance->generic.data = (new_key | bytesum); size_t index = 0; @@ -233,7 +233,7 @@ static void subghz_protocol_hollarm_remote_controller(SubGhzBlockGeneric* instan // F0B9342401 = 01 8bit Sum // F0B9342805 = 05 8bit Sum - // Serial (moved 2bit to right) | Btn | 8b CRC (previous 4 bytes sum) + // Serial (moved 2bit to right) | Btn | 8b previous 4 bytes sum // 00001111000010111001001101000010 0010 11111111 btn = (0x2) // 00001111000010111001001101000010 0001 11111110 btn = (0x1) // 00001111000010111001001101000010 0100 00000001 btn = (0x4) @@ -447,23 +447,23 @@ void subghz_protocol_decoder_hollarm_get_string(void* context, FuriString* outpu // Parse serial subghz_protocol_hollarm_remote_controller(&instance->generic); - // Get CRC - uint8_t crc = ((instance->generic.data >> 32) & 0xFF) + - ((instance->generic.data >> 24) & 0xFF) + - ((instance->generic.data >> 16) & 0xFF) + ((instance->generic.data >> 8) & 0xFF); + // Get byte sum + uint8_t bytesum = + ((instance->generic.data >> 32) & 0xFF) + ((instance->generic.data >> 24) & 0xFF) + + ((instance->generic.data >> 16) & 0xFF) + ((instance->generic.data >> 8) & 0xFF); furi_string_cat_printf( output, "%s %db\r\n" "Key: 0x%02lX%08lX\r\n" - "Serial: 0x%06lX CRC: %02X\r\n" + "Serial: 0x%06lX Sum: %02X\r\n" "Btn: 0x%01X - %s\r\n", instance->generic.protocol_name, instance->generic.data_count_bit, (uint32_t)(instance->generic.data >> 32), (uint32_t)instance->generic.data, instance->generic.serial, - crc, + bytesum, instance->generic.btn, subghz_protocol_hollarm_get_button_name(instance->generic.btn)); } From 9ff141acbb31708b7d490662bf86a1c466933105 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Thu, 20 Feb 2025 05:28:44 +0300 Subject: [PATCH 053/268] fix infrared --- applications/main/infrared/resources/infrared/assets/audio.ir | 2 +- .../main/infrared/resources/infrared/assets/projectors.ir | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/applications/main/infrared/resources/infrared/assets/audio.ir b/applications/main/infrared/resources/infrared/assets/audio.ir index a42dfd9c1..5585a5f8f 100644 --- a/applications/main/infrared/resources/infrared/assets/audio.ir +++ b/applications/main/infrared/resources/infrared/assets/audio.ir @@ -3771,7 +3771,7 @@ protocol: NECext address: D9 14 00 00 command: 4F B0 00 00 # -name: Vol_down +name: Vol_dn type: parsed protocol: NECext address: D9 14 00 00 diff --git a/applications/main/infrared/resources/infrared/assets/projectors.ir b/applications/main/infrared/resources/infrared/assets/projectors.ir index 0279f73e9..00fb5aa9f 100644 --- a/applications/main/infrared/resources/infrared/assets/projectors.ir +++ b/applications/main/infrared/resources/infrared/assets/projectors.ir @@ -1812,7 +1812,7 @@ protocol: NECext address: 86 6B 00 00 command: 09 F6 00 00 # -name: Vol_down +name: Vol_dn type: parsed protocol: NECext address: 4F 50 00 00 From 9378a6f5c4bb9cb4cba03472cfe24a19fca53562 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Thu, 20 Feb 2025 05:29:38 +0300 Subject: [PATCH 054/268] fix text pos --- .../main/infrared/scenes/infrared_scene_universal_projector.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/applications/main/infrared/scenes/infrared_scene_universal_projector.c b/applications/main/infrared/scenes/infrared_scene_universal_projector.c index a624a5c44..ac3f0c3fe 100644 --- a/applications/main/infrared/scenes/infrared_scene_universal_projector.c +++ b/applications/main/infrared/scenes/infrared_scene_universal_projector.c @@ -90,7 +90,7 @@ void infrared_scene_universal_projector_on_enter(void* context) { button_panel_add_icon(button_panel, 4, 109, &I_pause_text_23x5); button_panel_add_label(button_panel, 10, 11, FontPrimary, "Projector"); - button_panel_add_icon(button_panel, 17, 72, &I_vol_ac_text_30x30); + button_panel_add_icon(button_panel, 34, 68, &I_vol_ac_text_30x30); infrared_scene_universal_common_on_enter(context); } From f37044de1892828aec6bf6c6c2954c4022ab965f Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Thu, 20 Feb 2025 05:33:44 +0300 Subject: [PATCH 055/268] upd changelog --- CHANGELOG.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c7badafc4..ea1ab2e28 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,9 @@ ## Main changes -- Current API: 80.1 +- Current API: 80.2 +* SubGHz: Fix GangQi protocol (by @DoberBit and @mishamyte (who spent 2 weeks on this)) * OFW: LFRFID - **EM4305 support** +* OFW: Universal IR signal selection +* OFW: BadUSB: Mouse control * OFW: NFC - Added naming for DESFire cards + fix MF3ICD40 cards unable to be read * Apps: Add FindMyFlipper to system apps and allow autostart on system boot [app by @MatthewKuKanich](https://github.com/MatthewKuKanich/FindMyFlipper) and autoloader by @Willy-JL - to use app please check how to add keys in [app repo](https://github.com/MatthewKuKanich/FindMyFlipper) * Input: Vibro on Button press option (PR #867 | by @Dmitry422) @@ -8,6 +11,10 @@ * Apps: **Check out more Apps updates and fixes by following** [this link](https://github.com/xMasterX/all-the-plugins/commits/dev) ## Other changes * Anims: Disable winter anims +* OFW: Fixed repeat in subghz tx_from_file command +* OFW: LFRFID: Noralsy Format/Brand +* OFW: Faster di card reading +* OFW: vscode: disabled auto-update for clangd since correct version is in the toolchain * OFW: Furi, USB, BLE, Debug: various bug fixes and improvements * OFW: EventLoop unsubscribe fix * OFW: nfc: Enable MFUL sync poller to be provided with passwords From 536ebe3efc93780b156fab6179aead277f978dda Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Thu, 20 Feb 2025 06:03:30 +0300 Subject: [PATCH 056/268] came atomo button hold simulation half cycle --- lib/subghz/protocols/came_atomo.c | 42 ++++++++++++++++++++++++++++--- lib/subghz/protocols/gangqi.c | 2 +- 2 files changed, 39 insertions(+), 5 deletions(-) diff --git a/lib/subghz/protocols/came_atomo.c b/lib/subghz/protocols/came_atomo.c index d21440490..a89a05d8d 100644 --- a/lib/subghz/protocols/came_atomo.c +++ b/lib/subghz/protocols/came_atomo.c @@ -218,8 +218,10 @@ static void subghz_protocol_encoder_came_atomo_get_upload( instance->encoder.upload[index++] = level_duration_make(false, (uint32_t)subghz_protocol_came_atomo_const.te_long * 60); + // Btn counter 0x0 - 0x7F + pack[0] = 0; for(uint8_t i = 0; i < 8; i++) { - pack[0] = (instance->generic.data_2 >> 56); + //pack[0] = (instance->generic.data_2 >> 56); pack[1] = (instance->generic.cnt >> 8); pack[2] = (instance->generic.cnt & 0xFF); pack[3] = ((instance->generic.data_2 >> 32) & 0xFF); @@ -228,11 +230,42 @@ static void subghz_protocol_encoder_came_atomo_get_upload( pack[6] = ((instance->generic.data_2 >> 8) & 0xFF); pack[7] = (btn << 4); - if(pack[0] == 0x7F) { + /* if(pack[0] == 0x7F) { pack[0] = 0; } else { pack[0] += (i + 1); } + */ + switch(i) { + case 0: + pack[0] = 10; // 0A + break; + case 1: + pack[0] = 30; + break; + case 2: + pack[0] = 125; // 7D + break; + case 3: + pack[0] = 126; // 7E + break; + case 4: + pack[0] = 127; // 7F + break; + case 5: + pack[0] = 0; // 00 + break; + case 6: + pack[0] = 1; // 01 + break; + case 7: + pack[0] = 3; + break; + + default: + break; + } + // 10 50 125 126 127 0 1 2 atomo_encrypt(pack); uint32_t hi = pack[0] << 24 | pack[1] << 16 | pack[2] << 8 | pack[3]; @@ -521,7 +554,8 @@ static void subghz_protocol_came_atomo_remote_controller(SubGhzBlockGeneric* ins * 0x931dfb16c0b1 ^ 0xXXXXXXXXXXXXXXXX = 0xEF3ED0F7D9EF * 0xEF3 ED0F7D9E F => 0xEF3 - CNT, 0xED0F7D9E - SN, 0xF - key * - * ***Eng1n33r ver. (actual)*** + * ***Actual*** + * Button hold-cycle counter (8-bit, from 0 to 0x7F) should DO full cycle or half cycle keeping values like zero * 0x1FF08D9924984115 - received data * 0x00F7266DB67BEEA0 - inverted data * 0x0501FD0000A08300 - decrypted data, @@ -720,7 +754,7 @@ void subghz_protocol_decoder_came_atomo_get_string(void* context, FuriString* ou "%s %db\r\n" "Key:%08lX%08lX\r\n" "Sn:0x%08lX Btn:%01X\r\n" - "Pcl_Cnt:0x%04lX\r\n" + "Cnt:0x%04lX\r\n" "Btn_Cnt:0x%02X", instance->generic.protocol_name, diff --git a/lib/subghz/protocols/gangqi.c b/lib/subghz/protocols/gangqi.c index 5d350e6a0..720a4d54a 100644 --- a/lib/subghz/protocols/gangqi.c +++ b/lib/subghz/protocols/gangqi.c @@ -469,7 +469,7 @@ void subghz_protocol_decoder_gangqi_get_string(void* context, FuriString* output "%s %db\r\n" "Key: 0x%X%08lX\r\n" "Serial: 0x%05lX\r\n" - "Sum: 0x%02X Sum2: 0x%02X\r\n" + "Sum: 0x%02X Sum2: 0x%02X\r\n" "Btn: 0x%01X - %s\r\n", instance->generic.protocol_name, instance->generic.data_count_bit, From d7df3f38b7bc2ad6e64fdc0fe619c538524e7651 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Thu, 20 Feb 2025 06:04:35 +0300 Subject: [PATCH 057/268] upd changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ea1ab2e28..1ff3efb28 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## Main changes - Current API: 80.2 * SubGHz: Fix GangQi protocol (by @DoberBit and @mishamyte (who spent 2 weeks on this)) +* SubGHz: Came Atomo button hold simulation with full cycle simulation (to allow proper pairing with receiver) * OFW: LFRFID - **EM4305 support** * OFW: Universal IR signal selection * OFW: BadUSB: Mouse control From 2817666eb94187adf64146636a99922dd330c549 Mon Sep 17 00:00:00 2001 From: Astra <93453568+Astrrra@users.noreply.github.com> Date: Thu, 20 Feb 2025 12:37:52 +0900 Subject: [PATCH 058/268] [FL-3774] Fix 5V on GPIO (#4103) * Move OTG controls to the power service * Accessor: add missing power service import * Power: add is_otg_enabled to info and properly handle OTG enable with VBUS voltage present * Power: method naming * Power: add backward compatibility with old-style use of furi_hal_power * Scripts: lower MIN_GAP_PAGES to 1 * SubGhz: fix incorrect logging tag * SubGhz: delegate OTG management to power service * Power: fix condition race, various improvements Co-authored-by: Aleksandr Kutuzov --- applications/debug/accessor/accessor_app.cpp | 7 +++- applications/debug/accessor/accessor_app.h | 2 + .../examples/example_thermo/example_thermo.c | 10 +++-- applications/main/gpio/gpio_app.c | 3 ++ applications/main/gpio/gpio_app_i.h | 2 + .../main/gpio/scenes/gpio_scene_start.c | 6 +-- applications/main/infrared/infrared_app.c | 12 +++--- applications/main/onewire/onewire_cli.c | 9 ++++- .../main/subghz/helpers/subghz_txrx.c | 21 ++++------ applications/main/subghz/subghz_cli.c | 19 +++------ applications/main/subghz/subghz_i.h | 2 + .../services/expansion/expansion_worker.c | 10 ++++- applications/services/power/power_cli.c | 7 +++- .../services/power/power_service/power.c | 40 +++++++++++++++++-- .../services/power/power_service/power.h | 14 +++++++ .../services/power/power_service/power_api.c | 19 +++++++++ .../services/power/power_service/power_i.h | 2 + applications/services/rpc/rpc_gpio.c | 9 ++++- lib/ibutton/ibutton_worker_modes.c | 19 ++++++--- lib/subghz/protocols/secplus_v1.c | 2 +- scripts/update.py | 2 +- targets/f18/api_symbols.csv | 4 +- targets/f7/api_symbols.csv | 4 +- targets/furi_hal_include/furi_hal_power.h | 4 ++ 24 files changed, 169 insertions(+), 60 deletions(-) diff --git a/applications/debug/accessor/accessor_app.cpp b/applications/debug/accessor/accessor_app.cpp index 8d43acc13..59f5d6cc7 100644 --- a/applications/debug/accessor/accessor_app.cpp +++ b/applications/debug/accessor/accessor_app.cpp @@ -2,6 +2,7 @@ #include #include #include +#include void AccessorApp::run(void) { AccessorEvent event; @@ -35,16 +36,18 @@ AccessorApp::AccessorApp() : text_store{0} { notification = static_cast(furi_record_open(RECORD_NOTIFICATION)); expansion = static_cast(furi_record_open(RECORD_EXPANSION)); + power = static_cast(furi_record_open(RECORD_POWER)); onewire_host = onewire_host_alloc(&gpio_ibutton); expansion_disable(expansion); - furi_hal_power_enable_otg(); + power_enable_otg(power, true); } AccessorApp::~AccessorApp() { - furi_hal_power_disable_otg(); + power_enable_otg(power, false); expansion_enable(expansion); furi_record_close(RECORD_EXPANSION); furi_record_close(RECORD_NOTIFICATION); + furi_record_close(RECORD_POWER); onewire_host_free(onewire_host); } diff --git a/applications/debug/accessor/accessor_app.h b/applications/debug/accessor/accessor_app.h index 890552f5f..1961f9cbf 100644 --- a/applications/debug/accessor/accessor_app.h +++ b/applications/debug/accessor/accessor_app.h @@ -7,6 +7,7 @@ #include #include #include +#include class AccessorApp { public: @@ -53,4 +54,5 @@ private: NotificationApp* notification; Expansion* expansion; + Power* power; }; diff --git a/applications/examples/example_thermo/example_thermo.c b/applications/examples/example_thermo/example_thermo.c index e5af819e9..4b225b70c 100644 --- a/applications/examples/example_thermo/example_thermo.c +++ b/applications/examples/example_thermo/example_thermo.c @@ -22,7 +22,7 @@ #include #include -#include +#include #define UPDATE_PERIOD_MS 1000UL #define TEXT_STORE_SIZE 64U @@ -76,6 +76,7 @@ typedef struct { FuriThread* reader_thread; FuriMessageQueue* event_queue; OneWireHost* onewire; + Power* power; float temp_celsius; bool has_device; } ExampleThermoContext; @@ -273,7 +274,7 @@ static void example_thermo_input_callback(InputEvent* event, void* ctx) { /* Starts the reader thread and handles the input */ static void example_thermo_run(ExampleThermoContext* context) { /* Enable power on external pins */ - furi_hal_power_enable_otg(); + power_enable_otg(context->power, true); /* Configure the hardware in host mode */ onewire_host_start(context->onewire); @@ -309,7 +310,7 @@ static void example_thermo_run(ExampleThermoContext* context) { onewire_host_stop(context->onewire); /* Disable power on external pins */ - furi_hal_power_disable_otg(); + power_enable_otg(context->power, false); } /******************** Initialisation & startup *****************************/ @@ -334,6 +335,8 @@ static ExampleThermoContext* example_thermo_context_alloc(void) { context->onewire = onewire_host_alloc(&THERMO_GPIO_PIN); + context->power = furi_record_open(RECORD_POWER); + return context; } @@ -348,6 +351,7 @@ static void example_thermo_context_free(ExampleThermoContext* context) { view_port_free(context->view_port); furi_record_close(RECORD_GUI); + furi_record_close(RECORD_POWER); } /* The application's entry point. Execution starts from here. */ diff --git a/applications/main/gpio/gpio_app.c b/applications/main/gpio/gpio_app.c index 234cc793a..a21813955 100644 --- a/applications/main/gpio/gpio_app.c +++ b/applications/main/gpio/gpio_app.c @@ -30,6 +30,8 @@ GpioApp* gpio_app_alloc(void) { app->gui = furi_record_open(RECORD_GUI); app->gpio_items = gpio_items_alloc(); + app->power = furi_record_open(RECORD_POWER); + app->view_dispatcher = view_dispatcher_alloc(); app->scene_manager = scene_manager_alloc(&gpio_scene_handlers, app); view_dispatcher_set_event_callback_context(app->view_dispatcher, app); @@ -100,6 +102,7 @@ void gpio_app_free(GpioApp* app) { // Close records furi_record_close(RECORD_GUI); furi_record_close(RECORD_NOTIFICATION); + furi_record_close(RECORD_POWER); expansion_enable(app->expansion); furi_record_close(RECORD_EXPANSION); diff --git a/applications/main/gpio/gpio_app_i.h b/applications/main/gpio/gpio_app_i.h index ce4cb6f55..4fbe25ad8 100644 --- a/applications/main/gpio/gpio_app_i.h +++ b/applications/main/gpio/gpio_app_i.h @@ -5,6 +5,7 @@ #include "scenes/gpio_scene.h" #include "gpio_custom_event.h" #include "usb_uart_bridge.h" +#include #include #include @@ -27,6 +28,7 @@ struct GpioApp { SceneManager* scene_manager; Widget* widget; DialogEx* dialog; + Power* power; VariableItemList* var_item_list; VariableItem* var_item_flow; diff --git a/applications/main/gpio/scenes/gpio_scene_start.c b/applications/main/gpio/scenes/gpio_scene_start.c index 421936488..0f37d77d8 100644 --- a/applications/main/gpio/scenes/gpio_scene_start.c +++ b/applications/main/gpio/scenes/gpio_scene_start.c @@ -60,7 +60,7 @@ void gpio_scene_start_on_enter(void* context) { GpioOtgSettingsNum, gpio_scene_start_var_list_change_callback, app); - if(furi_hal_power_is_otg_enabled()) { + if(power_is_otg_enabled(app->power)) { variable_item_set_current_value_index(item, GpioOtgOn); variable_item_set_current_value_text(item, gpio_otg_text[GpioOtgOn]); } else { @@ -80,9 +80,9 @@ bool gpio_scene_start_on_event(void* context, SceneManagerEvent event) { if(event.type == SceneManagerEventTypeCustom) { if(event.event == GpioStartEventOtgOn) { - furi_hal_power_enable_otg(); + power_enable_otg(app->power, true); } else if(event.event == GpioStartEventOtgOff) { - furi_hal_power_disable_otg(); + power_enable_otg(app->power, false); } else if(event.event == GpioStartEventManualControl) { scene_manager_set_scene_state(app->scene_manager, GpioSceneStart, GpioItemTest); scene_manager_next_scene(app->scene_manager, GpioSceneTest); diff --git a/applications/main/infrared/infrared_app.c b/applications/main/infrared/infrared_app.c index c50039760..4a03a8220 100644 --- a/applications/main/infrared/infrared_app.c +++ b/applications/main/infrared/infrared_app.c @@ -1,6 +1,6 @@ #include "infrared_app_i.h" -#include +#include #include #include @@ -501,12 +501,12 @@ void infrared_set_tx_pin(InfraredApp* infrared, FuriHalInfraredTxPin tx_pin) { } void infrared_enable_otg(InfraredApp* infrared, bool enable) { - if(enable) { - furi_hal_power_enable_otg(); - } else { - furi_hal_power_disable_otg(); - } + Power* power = furi_record_open(RECORD_POWER); + + power_enable_otg(power, enable); infrared->app_state.is_otg_enabled = enable; + + furi_record_close(RECORD_POWER); } static void infrared_load_settings(InfraredApp* infrared) { diff --git a/applications/main/onewire/onewire_cli.c b/applications/main/onewire/onewire_cli.c index af3d4e803..74ca6bc1f 100644 --- a/applications/main/onewire/onewire_cli.c +++ b/applications/main/onewire/onewire_cli.c @@ -1,6 +1,8 @@ #include #include +#include + #include #include @@ -26,13 +28,14 @@ static void onewire_cli_print_usage(void) { static void onewire_cli_search(Cli* cli) { UNUSED(cli); OneWireHost* onewire = onewire_host_alloc(&gpio_ibutton); + Power* power = furi_record_open(RECORD_POWER); uint8_t address[8]; bool done = false; printf("Search started\r\n"); onewire_host_start(onewire); - furi_hal_power_enable_otg(); + power_enable_otg(power, true); while(!done) { if(onewire_host_search(onewire, address, OneWireHostSearchModeNormal) != 1) { @@ -49,8 +52,10 @@ static void onewire_cli_search(Cli* cli) { furi_delay_ms(100); } - furi_hal_power_disable_otg(); + power_enable_otg(power, false); + onewire_host_free(onewire); + furi_record_close(RECORD_POWER); } void onewire_cli(Cli* cli, FuriString* args, void* context) { diff --git a/applications/main/subghz/helpers/subghz_txrx.c b/applications/main/subghz/helpers/subghz_txrx.c index e3a0c6057..eaa56c549 100644 --- a/applications/main/subghz/helpers/subghz_txrx.c +++ b/applications/main/subghz/helpers/subghz_txrx.c @@ -4,27 +4,22 @@ #include #include +#include + #define TAG "SubGhz" static void subghz_txrx_radio_device_power_on(SubGhzTxRx* instance) { UNUSED(instance); - uint8_t attempts = 5; - while(--attempts > 0) { - if(furi_hal_power_enable_otg()) break; - } - if(attempts == 0) { - if(furi_hal_power_get_usb_voltage() < 4.5f) { - FURI_LOG_E( - TAG, - "Error power otg enable. BQ2589 check otg fault = %d", - furi_hal_power_check_otg_fault() ? 1 : 0); - } - } + Power* power = furi_record_open(RECORD_POWER); + power_enable_otg(power, true); + furi_record_close(RECORD_POWER); } static void subghz_txrx_radio_device_power_off(SubGhzTxRx* instance) { UNUSED(instance); - if(furi_hal_power_is_otg_enabled()) furi_hal_power_disable_otg(); + Power* power = furi_record_open(RECORD_POWER); + power_enable_otg(power, false); + furi_record_close(RECORD_POWER); } SubGhzTxRx* subghz_txrx_alloc(void) { diff --git a/applications/main/subghz/subghz_cli.c b/applications/main/subghz/subghz_cli.c index 88f9bfa38..54e02196d 100644 --- a/applications/main/subghz/subghz_cli.c +++ b/applications/main/subghz/subghz_cli.c @@ -28,22 +28,15 @@ #define TAG "SubGhzCli" static void subghz_cli_radio_device_power_on(void) { - uint8_t attempts = 5; - while(--attempts > 0) { - if(furi_hal_power_enable_otg()) break; - } - if(attempts == 0) { - if(furi_hal_power_get_usb_voltage() < 4.5f) { - FURI_LOG_E( - "TAG", - "Error power otg enable. BQ2589 check otg fault = %d", - furi_hal_power_check_otg_fault() ? 1 : 0); - } - } + Power* power = furi_record_open(RECORD_POWER); + power_enable_otg(power, true); + furi_record_close(RECORD_POWER); } static void subghz_cli_radio_device_power_off(void) { - if(furi_hal_power_is_otg_enabled()) furi_hal_power_disable_otg(); + Power* power = furi_record_open(RECORD_POWER); + power_enable_otg(power, false); + furi_record_close(RECORD_POWER); } static SubGhzEnvironment* subghz_cli_environment_init(void) { diff --git a/applications/main/subghz/subghz_i.h b/applications/main/subghz/subghz_i.h index 08687a4f7..b210dd22b 100644 --- a/applications/main/subghz/subghz_i.h +++ b/applications/main/subghz/subghz_i.h @@ -28,6 +28,8 @@ #include "rpc/rpc_app.h" +#include + #include "helpers/subghz_threshold_rssi.h" #include "helpers/subghz_txrx.h" diff --git a/applications/services/expansion/expansion_worker.c b/applications/services/expansion/expansion_worker.c index c05b9cc85..ac2a5935b 100644 --- a/applications/services/expansion/expansion_worker.c +++ b/applications/services/expansion/expansion_worker.c @@ -1,6 +1,8 @@ #include "expansion_worker.h" +#include #include + #include #include @@ -250,9 +252,13 @@ static bool expansion_worker_handle_state_connected( if(!expansion_worker_rpc_session_open(instance)) break; instance->state = ExpansionWorkerStateRpcActive; } else if(command == ExpansionFrameControlCommandEnableOtg) { - furi_hal_power_enable_otg(); + Power* power = furi_record_open(RECORD_POWER); + power_enable_otg(power, true); + furi_record_close(RECORD_POWER); } else if(command == ExpansionFrameControlCommandDisableOtg) { - furi_hal_power_disable_otg(); + Power* power = furi_record_open(RECORD_POWER); + power_enable_otg(power, false); + furi_record_close(RECORD_POWER); } else { break; } diff --git a/applications/services/power/power_cli.c b/applications/services/power/power_cli.c index 93d0f232a..121552768 100644 --- a/applications/services/power/power_cli.c +++ b/applications/services/power/power_cli.c @@ -30,13 +30,16 @@ void power_cli_reboot2dfu(Cli* cli, FuriString* args) { void power_cli_5v(Cli* cli, FuriString* args) { UNUSED(cli); + Power* power = furi_record_open(RECORD_POWER); if(!furi_string_cmp(args, "0")) { - furi_hal_power_disable_otg(); + power_enable_otg(power, false); } else if(!furi_string_cmp(args, "1")) { - furi_hal_power_enable_otg(); + power_enable_otg(power, true); } else { cli_print_usage("power_otg", "<1|0>", furi_string_get_cstr(args)); } + + furi_record_close(RECORD_POWER); } void power_cli_3v3(Cli* cli, FuriString* args) { diff --git a/applications/services/power/power_service/power.c b/applications/services/power/power_service/power.c index b73c4a1dd..fa86328df 100644 --- a/applications/services/power/power_service/power.c +++ b/applications/services/power/power_service/power.c @@ -64,6 +64,7 @@ static bool power_update_info(Power* power) { .is_charging = furi_hal_power_is_charging(), .gauge_is_ok = furi_hal_power_gauge_is_ok(), .is_shutdown_requested = furi_hal_power_is_shutdown_requested(), + .is_otg_enabled = furi_hal_power_is_otg_enabled(), .charge = furi_hal_power_get_pct(), .health = furi_hal_power_get_bat_health_pct(), .capacity_remaining = furi_hal_power_get_battery_remaining_capacity(), @@ -216,6 +217,30 @@ static void power_message_callback(FuriEventLoopObject* object, void* context) { case PowerMessageTypeShowBatteryLowWarning: power->show_battery_low_warning = *msg.bool_param; break; + case PowerMessageTypeSwitchOTG: + power->is_otg_requested = *msg.bool_param; + if(power->is_otg_requested) { + // Only try to enable if VBUS voltage is low, otherwise charger will refuse + if(power->info.voltage_vbus < 4.5f) { + size_t retries = 5; + while(retries-- > 0) { + if(furi_hal_power_enable_otg()) { + break; + } + } + if(!retries) { + FURI_LOG_W(TAG, "Failed to enable OTG, will try later"); + } + } else { + FURI_LOG_W( + TAG, + "Postponing OTG enable: VBUS(%0.1f) >= 4.5v", + (double)power->info.voltage_vbus); + } + } else { + furi_hal_power_disable_otg(); + } + break; default: furi_crash(); } @@ -241,9 +266,18 @@ static void power_tick_callback(void* context) { if(need_refresh) { view_port_update(power->battery_view_port); } - // Check OTG status and disable it in case of fault - if(furi_hal_power_is_otg_enabled()) { - furi_hal_power_check_otg_status(); + // Check OTG status, disable in case of a fault + if(furi_hal_power_check_otg_fault()) { + FURI_LOG_E(TAG, "OTG fault detected, disabling OTG"); + furi_hal_power_disable_otg(); + power->is_otg_requested = false; + } + + // Change OTG state if needed (i.e. after disconnecting USB power) + if(power->is_otg_requested && + (!power->info.is_otg_enabled && power->info.voltage_vbus < 4.5f)) { + FURI_LOG_D(TAG, "OTG requested but not enabled, enabling OTG"); + furi_hal_power_enable_otg(); } } diff --git a/applications/services/power/power_service/power.h b/applications/services/power/power_service/power.h index 0168a8656..0e1de6449 100644 --- a/applications/services/power/power_service/power.h +++ b/applications/services/power/power_service/power.h @@ -39,6 +39,7 @@ typedef struct { bool gauge_is_ok; bool is_charging; bool is_shutdown_requested; + bool is_otg_enabled; float current_charger; float current_gauge; @@ -96,6 +97,19 @@ bool power_is_battery_healthy(Power* power); */ void power_enable_low_battery_level_notification(Power* power, bool enable); +/** Enable or disable OTG + * + * @param power Power instance + * @param enable true - enable, false - disable + */ +void power_enable_otg(Power* power, bool enable); + +/** Check OTG status + * + * @return true if OTG is requested + */ +bool power_is_otg_enabled(Power* power); + #ifdef __cplusplus } #endif diff --git a/applications/services/power/power_service/power_api.c b/applications/services/power/power_service/power_api.c index 6f7515f5e..f634f15e3 100644 --- a/applications/services/power/power_service/power_api.c +++ b/applications/services/power/power_service/power_api.c @@ -70,3 +70,22 @@ void power_enable_low_battery_level_notification(Power* power, bool enable) { furi_check( furi_message_queue_put(power->message_queue, &msg, FuriWaitForever) == FuriStatusOk); } + +void power_enable_otg(Power* power, bool enable) { + furi_check(power); + + PowerMessage msg = { + .type = PowerMessageTypeSwitchOTG, + .bool_param = &enable, + .lock = api_lock_alloc_locked(), + }; + + furi_check( + furi_message_queue_put(power->message_queue, &msg, FuriWaitForever) == FuriStatusOk); + api_lock_wait_unlock_and_free(msg.lock); +} + +bool power_is_otg_enabled(Power* power) { + furi_check(power); + return power->is_otg_requested; +} diff --git a/applications/services/power/power_service/power_i.h b/applications/services/power/power_service/power_i.h index a0c02623a..f08fd8f88 100644 --- a/applications/services/power/power_service/power_i.h +++ b/applications/services/power/power_service/power_i.h @@ -33,6 +33,7 @@ struct Power { bool battery_low; bool show_battery_low_warning; + bool is_otg_requested; uint8_t battery_level; uint8_t power_off_timeout; }; @@ -48,6 +49,7 @@ typedef enum { PowerMessageTypeGetInfo, PowerMessageTypeIsBatteryHealthy, PowerMessageTypeShowBatteryLowWarning, + PowerMessageTypeSwitchOTG, } PowerMessageType; typedef struct { diff --git a/applications/services/rpc/rpc_gpio.c b/applications/services/rpc/rpc_gpio.c index 40fc898a0..d05783afc 100644 --- a/applications/services/rpc/rpc_gpio.c +++ b/applications/services/rpc/rpc_gpio.c @@ -4,6 +4,7 @@ #include #include #include +#include static const GpioPin* rpc_pin_to_hal_pin(PB_Gpio_GpioPin rpc_pin) { switch(rpc_pin) { @@ -218,12 +219,16 @@ void rpc_system_gpio_set_otg_mode(const PB_Main* request, void* context) { const PB_Gpio_GpioOtgMode mode = request->content.gpio_set_otg_mode.mode; + Power* power = furi_record_open(RECORD_POWER); + if(mode == PB_Gpio_GpioOtgMode_OFF) { - furi_hal_power_disable_otg(); + power_enable_otg(power, false); } else { - furi_hal_power_enable_otg(); + power_enable_otg(power, true); } + furi_record_close(RECORD_POWER); + rpc_send_and_release_empty(session, request->command_id, PB_CommandStatus_OK); } diff --git a/lib/ibutton/ibutton_worker_modes.c b/lib/ibutton/ibutton_worker_modes.c index 5900b10a2..ff76e784d 100644 --- a/lib/ibutton/ibutton_worker_modes.c +++ b/lib/ibutton/ibutton_worker_modes.c @@ -1,9 +1,10 @@ #include "ibutton_worker_i.h" #include +#include #include -#include +#include #include "ibutton_protocols.h" @@ -75,7 +76,9 @@ void ibutton_worker_mode_idle_stop(iButtonWorker* worker) { void ibutton_worker_mode_read_start(iButtonWorker* worker) { UNUSED(worker); - furi_hal_power_enable_otg(); + Power* power = furi_record_open(RECORD_POWER); + power_enable_otg(power, true); + furi_record_close(RECORD_POWER); } void ibutton_worker_mode_read_tick(iButtonWorker* worker) { @@ -90,7 +93,9 @@ void ibutton_worker_mode_read_tick(iButtonWorker* worker) { void ibutton_worker_mode_read_stop(iButtonWorker* worker) { UNUSED(worker); - furi_hal_power_disable_otg(); + Power* power = furi_record_open(RECORD_POWER); + power_enable_otg(power, false); + furi_record_close(RECORD_POWER); } /*********************** EMULATE ***********************/ @@ -120,7 +125,9 @@ void ibutton_worker_mode_emulate_stop(iButtonWorker* worker) { void ibutton_worker_mode_write_common_start(iButtonWorker* worker) { //-V524 UNUSED(worker); - furi_hal_power_enable_otg(); + Power* power = furi_record_open(RECORD_POWER); + power_enable_otg(power, true); + furi_record_close(RECORD_POWER); } void ibutton_worker_mode_write_id_tick(iButtonWorker* worker) { @@ -149,5 +156,7 @@ void ibutton_worker_mode_write_copy_tick(iButtonWorker* worker) { void ibutton_worker_mode_write_common_stop(iButtonWorker* worker) { //-V524 UNUSED(worker); - furi_hal_power_disable_otg(); + Power* power = furi_record_open(RECORD_POWER); + power_enable_otg(power, false); + furi_record_close(RECORD_POWER); } diff --git a/lib/subghz/protocols/secplus_v1.c b/lib/subghz/protocols/secplus_v1.c index f878ecb01..47cc100b3 100644 --- a/lib/subghz/protocols/secplus_v1.c +++ b/lib/subghz/protocols/secplus_v1.c @@ -228,7 +228,7 @@ static bool subghz_protocol_secplus_v1_encode(SubGhzProtocolEncoderSecPlus_v1* i rolling = 0xE6000000; } if(fixed > 0xCFD41B90) { - FURI_LOG_E("TAG", "Encode wrong fixed data"); + FURI_LOG_E(TAG, "Encode wrong fixed data"); return false; } diff --git a/scripts/update.py b/scripts/update.py index 2b6620744..992f75603 100755 --- a/scripts/update.py +++ b/scripts/update.py @@ -34,7 +34,7 @@ class Main(App): FLASH_BASE = 0x8000000 FLASH_PAGE_SIZE = 4 * 1024 - MIN_GAP_PAGES = 2 + MIN_GAP_PAGES = 1 # Update stage file larger than that is not loadable without fix # https://github.com/flipperdevices/flipperzero-firmware/pull/3676 diff --git a/targets/f18/api_symbols.csv b/targets/f18/api_symbols.csv index 57eaf6ab3..5a750f24c 100644 --- a/targets/f18/api_symbols.csv +++ b/targets/f18/api_symbols.csv @@ -1,5 +1,5 @@ entry,status,name,type,params -Version,+,80.2,, +Version,+,80.3,, Header,+,applications/services/bt/bt_service/bt.h,, Header,+,applications/services/bt/bt_service/bt_keys_storage.h,, Header,+,applications/services/cli/cli.h,, @@ -2362,8 +2362,10 @@ Function,+,power_enable_low_battery_level_notification,void,"Power*, _Bool" Function,+,power_get_info,void,"Power*, PowerInfo*" Function,+,power_get_pubsub,FuriPubSub*,Power* Function,+,power_is_battery_healthy,_Bool,Power* +Function,+,power_is_otg_enabled,_Bool,Power* Function,+,power_off,void,Power* Function,+,power_reboot,void,"Power*, PowerBootMode" +Function,+,power_enable_otg,void,"Power*, _Bool" Function,+,powf,float,"float, float" Function,-,powl,long double,"long double, long double" Function,+,pretty_format_bytes_hex_canonical,void,"FuriString*, size_t, const char*, const uint8_t*, size_t" diff --git a/targets/f7/api_symbols.csv b/targets/f7/api_symbols.csv index 900539019..217d90a2b 100644 --- a/targets/f7/api_symbols.csv +++ b/targets/f7/api_symbols.csv @@ -1,5 +1,5 @@ entry,status,name,type,params -Version,+,80.2,, +Version,+,80.3,, Header,+,applications/drivers/subghz/cc1101_ext/cc1101_ext_interconnect.h,, Header,+,applications/services/bt/bt_service/bt.h,, Header,+,applications/services/bt/bt_service/bt_keys_storage.h,, @@ -2999,8 +2999,10 @@ Function,+,power_enable_low_battery_level_notification,void,"Power*, _Bool" Function,+,power_get_info,void,"Power*, PowerInfo*" Function,+,power_get_pubsub,FuriPubSub*,Power* Function,+,power_is_battery_healthy,_Bool,Power* +Function,+,power_is_otg_enabled,_Bool,Power* Function,+,power_off,void,Power* Function,+,power_reboot,void,"Power*, PowerBootMode" +Function,+,power_enable_otg,void,"Power*, _Bool" Function,+,powf,float,"float, float" Function,-,powl,long double,"long double, long double" Function,+,pretty_format_bytes_hex_canonical,void,"FuriString*, size_t, const char*, const uint8_t*, size_t" diff --git a/targets/furi_hal_include/furi_hal_power.h b/targets/furi_hal_include/furi_hal_power.h index 06598df86..f5b6ac71b 100644 --- a/targets/furi_hal_include/furi_hal_power.h +++ b/targets/furi_hal_include/furi_hal_power.h @@ -105,10 +105,14 @@ void furi_hal_power_off(void); FURI_NORETURN void furi_hal_power_reset(void); /** OTG enable + * + * @warning this is low level control, use power service instead */ bool furi_hal_power_enable_otg(void); /** OTG disable + * + * @warning this is low level control, use power service instead */ void furi_hal_power_disable_otg(void); From 56853161e20e3e06f7c48f0cf5cdd260ec936444 Mon Sep 17 00:00:00 2001 From: m-xim <170838360+m-xim@users.noreply.github.com> Date: Thu, 20 Feb 2025 17:46:32 +0300 Subject: [PATCH 059/268] docs: update README with modernized design and enhanced navigation - Updated header image - Added emojis to headings for visual variety - Introduced collapsible sections (details) for a more compact layout - Included links to community channels (Telegram) - Restructured document for improved readability --- ReadMe.md | 402 +++++++++++++++++++++++++++++++----------------------- 1 file changed, 231 insertions(+), 171 deletions(-) diff --git a/ReadMe.md b/ReadMe.md index 5227f9805..9f9b11fe5 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -1,133 +1,209 @@

- -Unleashed Firmware Logo - + + Unleashed Firmware Logo +

+ -### Welcome to the Flipper Zero Unleashed Firmware repo! +# Flipper Zero Unleashed Firmware +This firmware is a fork of the original (OFW) version of [flipperdevices/flipperzero-firmware](https://github.com/flipperdevices/flipperzero-firmware) and represents the **most stable** custom build, incorporating **new features** and **improvements** to the original components while remaining **fully compatible** with the API and applications of the original firmware. -#### **This firmware is a fork from original (OFW) firmware** [flipperdevices/flipperzero-firmware](https://github.com/flipperdevices/flipperzero-firmware) +> [!WARNING] +> This software is intended solely for experimental purposes and is not meant for any illegal activities. +> We do not condone unlawful behavior and strongly encourage you to use it only within the bounds of the law. +> +> This project is developed independently and is not affiliated with Flipper Devices. -
+
-### Most stable custom firmware focused on new features and improvements of original firmware components, keeping compatibility with original firmware API and Apps +## 🚀 Usage -
+Before getting started: +- **Review the Official Documentation:** [docs.flipper.net](https://docs.flipper.net) -##### This software is for experimental purposes only and is not meant for any illegal activity/purposes.
We do not condone illegal activity and strongly encourage keeping transmissions to legal/valid uses allowed by law.
Also, this software is made without any support from Flipper Devices and is in no way related to the official team. +- **Installation Guide & Version Info:** + How to install the firmware by following the [Installation Guide](/documentation/HowToInstall.md) and check the [version information](/CHANGELOG.md#recommended-update-option---web-updater) (`r`, `e`, ` `, `c`) +- **FAQ:** + Find answers to common questions in the [FAQ](/documentation/FAQ.md) -
+
-## FAQ (frequently asked questions) -[Follow this link to find answers to most asked questions](https://github.com/DarkFlippers/unleashed-firmware/blob/dev/documentation/FAQ.md) +## 📦 Releases -## Our official domains -- https://flipperunleashed.com/ -> our main web page -- https://unleashedflip.com/ -> update server, direct .tgz update links for web updater or direct download +### Release builds (stable) +- Telegram Telegram: t.me/unleashed_fw +- GitHub GitHub Releases -## Dev builds (unstable) (built automatically from dev branch) -- https://dev.unleashedflip.com/ -- https://t.me/kotnehleb +### Dev builds (unstable) +> [!NOTE] +> Built automatically from dev branch -## Releases in Telegram -- https://t.me/unleashed_fw +- Web site: https://dev.unleashedflip.com +- Telegram Telegram: t.me/kotnehleb -# What's changed -- **Sub-GHz** *lib & hal* - - Regional TX restrictions removed - - Extra Sub-GHz frequencies added - - Frequency range can be extended in settings file (Warning: It can damage Flipper's hardware) - - Many rolling code [protocols](https://github.com/DarkFlippers/unleashed-firmware#current-modified-and-new-sub-ghz-protocols-list) now have the ability to save & send captured signals - - FAAC SLH (Spa) & BFT Mitto (keeloq secure with seed) manual creation - - External CC1101 module support [(by quen0n)](https://github.com/DarkFlippers/unleashed-firmware/pull/307) -- **Sub-GHz** *Main App* - - Save last used settings [(by derskythe)](https://github.com/DarkFlippers/unleashed-firmware/pull/77) - - New frequency analyzer [(by ClusterM)](https://github.com/DarkFlippers/unleashed-firmware/pull/43) - - Press OK in frequency analyzer to use detected frequency in Read modes [(by derskythe)](https://github.com/DarkFlippers/unleashed-firmware/pull/77) - - Long press OK button in Sub-GHz Frequency analyzer to switch to Read menu [(by derskythe)](https://github.com/DarkFlippers/unleashed-firmware/pull/79) - - New option to use timestamps + protocol name when you saving file, instead of random name or timestamp only - Enable in `Radio Settings -> Protocol Names = ON` - - Read mode UI improvements (shows time when signal was received) (by @wosk) - - External CC1101 module support (Hardware SPI used) - - External CC1101 module amplifier control (or LED control) support (enabled by default) - - **Hold right in received signal list to delete selected signal** - - **Custom buttons for Keeloq / Alutech AT4N / Nice Flor S / Somfy Telis / Security+ 2.0 / CAME Atomo** - now you can use arrow buttons to send signal with different button code - - `Add manually` menu extended with new protocols - - FAAC SLH, BFT Mitto / Somfy Telis / Nice Flor S / CAME Atomo, etc.. manual creation with programming new remote into receiver (use button 0xF for BFT Mitto, 0x8 (Prog) on Somfy Telis, (right arrow button for other protocols)) - - Debug mode counter increase settings (+1 -> +5, +10, default: +1) - - Debug PIN output settings for protocol development - -- **Sub-GHz apps** *by unleashed team* - - Sub-GHz Bruteforce - static code brute-force plugin | - - Time delay (between signals) setting (hold Up in main screen(says Up to Save)) + configure repeats in protocols list by pressing right button on selected protocol - - Load your own file and select bytes you want to bruteforce or use preconfigured options in protocols list - - Sub-GHz Remote - remote control for 5 sub-ghz files | bind one file for each button - - use the built-in constructor or make config file by following this [instruction](https://github.com/DarkFlippers/unleashed-firmware/blob/dev/documentation/SubGHzRemotePlugin.md) -- **Infrared** - - Recompiled IR TV Universal Remote for ALL buttons - - Universal remotes for Projectors, Fans, A/Cs and Audio(soundbars, etc.) -> Also always updated and verified by our team - - Infrared -> `RCA` Protocol - - Infrared -> External IR modules support (with autodetect by OFW) -- **NFC/RFID/iButton** - * LFRFID and iButton Fuzzer plugins - * Add DEZ 8 display form for EM4100 (by @korden32) - * Extra Mifare Classic keys in system dict - * EMV Protocol + Public data parser (by @Leptopt1los and @wosk) - * NFC `Add manually` -> Mifare Classic with custom UID - * NFC parsers: Umarsh, Zolotaya Korona, Kazan, Metromoney, Moscow Social Card, Troika (reworked) and [many others](https://github.com/DarkFlippers/unleashed-firmware/tree/dev/applications/main/nfc/plugins/supported_cards) (by @Leptopt1los and @assasinfil) -- **Quality of life & other features** - - Customizable Flipper name **Update! Now can be changed in Settings->Desktop** (by @xMasterX and @Willy-JL) - - Text Input UI element -> Cursor feature (by @Willy-JL) - - Byte Input Mini editor -> **Press UP** multiple times until the nibble editor appears (by @gid9798) - - Clock on Desktop -> `Settings -> Desktop -> Show Clock` (by @gid9798) - - Battery percentage display with different styles `Settings -> Desktop -> Battery View` - - More games in Dummy Mode -> click or hold any of arrow buttons - - Lock device with pin(or regular lock if pin not set) by holding UP button on main screen [(by an4tur0r)](https://github.com/DarkFlippers/unleashed-firmware/pull/107) - - **BadKB** plugin [(by Willy-JL, ClaraCrazy, XFW contributors)](https://github.com/Flipper-XFW/Xtreme-Firmware/tree/dev/applications/main/bad_kb) - (See in Applications->Tools) - (aka BadUSB via Bluetooth) - - BadUSB -> Keyboard layouts [(by rien > dummy-decoy)](https://github.com/dummy-decoy/flipperzero-firmware/tree/dummy_decoy/bad_usb_keyboard_layout) - - Custom community plugins and games added + all known working apps can be downloaded in extra pack in every release - - Other small fixes and changes throughout - - See other changes in readme below +
+ +## 🆕 What's New + +>
+> Sub‑GHz Library & HAL +>
+> +> - Regional TX restrictions removed +> - Extra Sub-GHz frequencies added +> - Frequency range can be extended in settings file (Warning: It can damage Flipper's hardware) +> - Many rolling code [protocols](#current-modified-and-new-sub-ghz-protocols-list) now have the ability to save & send captured signals +> - FAAC SLH (Spa) & BFT Mitto (keeloq secure with seed) manual creation +> - External CC1101 module support [(by quen0n)](https://github.com/DarkFlippers/unleashed-firmware/pull/307) +>
+ +>
+> Sub‑GHz Main App +>
+> +> - Save last used settings [(by derskythe)](https://github.com/DarkFlippers/unleashed-firmware/pull/77) +> - New frequency analyzer [(by ClusterM)](https://github.com/DarkFlippers/unleashed-firmware/pull/43) +> - Press OK in frequency analyzer to use detected frequency in Read modes [(by derskythe)](https://github.com/DarkFlippers/unleashed-firmware/pull/77) +> - Long press OK button in Sub-GHz Frequency analyzer to switch to Read menu [(by derskythe)](https://github.com/DarkFlippers/unleashed-firmware/pull/79) +> - New option to use timestamps + protocol name when you saving file, instead of random name or timestamp only - Enable in `Radio Settings -> Protocol Names = ON` +> - Read mode UI improvements (shows time when signal was received) (by @wosk) +> - External CC1101 module support (Hardware SPI used) +> - External CC1101 module amplifier control (or LED control) support (enabled by default) +> - **Hold right in received signal list to delete selected signal** +>- **Custom buttons for Keeloq / Alutech AT4N / Nice Flor S / Somfy Telis / Security+ 2.0 / CAME Atomo** - now you can use arrow buttons to send signal with different button code +> - `Add manually` menu extended with new protocols +> - FAAC SLH, BFT Mitto / Somfy Telis / Nice Flor S / CAME Atomo, etc. manual creation with programming new remote into receiver (use button 0xF for BFT Mitto, 0x8 (Prog) on Somfy Telis, (right arrow button for other protocols)) +> - Debug mode counter increase settings (+1 -> +5, +10, default: +1) +> - Debug PIN output settings for protocol development +>
+ +>
+> Sub‑GHz Apps (by Unleashed Team) +>
+> +> - Sub-GHz Bruteforce - static code brute-force plugin +> - Time delay (between signals) setting (hold Up in main screen (says Up to Save)) + configure repeats in protocols list by pressing right button on selected protocol +> - Load your own file and select bytes you want to bruteforce or use preconfigured options in protocols list +> - Sub-GHz Remote - remote control for 5 sub-ghz files | bind one file for each button +> - use the built-in constructor or make config file by following this [instruction](/documentation/SubGHzRemotePlugin.md) +>
+ +>
+> Infrared (IR) +>
+> +> - Recompiled IR TV Universal Remote for ALL buttons +> - Universal remotes for Projectors, Fans, A/Cs and Audio(soundbars, etc.) -> Also always updated and verified by our team +> - Infrared -> `RCA` Protocol +> - Infrared -> External IR modules support (with autodetect by OFW) +>
+ +>
+> NFC/RFID/iButton +>
+> +> - LFRFID and iButton Fuzzer plugins +> - Add DEZ 8 display form for EM4100 (by @korden32) +> - Extra Mifare Classic keys in system dict +> - EMV Protocol + Public data parser (by @Leptopt1los and @wosk) +> - NFC `Add manually` -> Mifare Classic with custom UID +> - NFC parsers: Umarsh, Zolotaya Korona, Kazan, Metromoney, Moscow Social Card, Troika (reworked) and [many others](https://github.com/DarkFlippers/unleashed-firmware/tree/dev/applications/main/nfc/plugins/supported_cards) (by @Leptopt1los and @assasinfil) +>
+ +>
+> Quality of Life & Other Features +>
+> +> - Customizable Flipper name **Update! Now can be changed in Settings->Desktop** (by @xMasterX and @Willy-JL) +> - Text Input UI element -> Cursor feature (by @Willy-JL) +> - Byte Input Mini editor -> **Press UP** multiple times until the nibble editor appears (by @gid9798) +> - Clock on Desktop -> `Settings -> Desktop -> Show Clock` (by @gid9798) +> - Battery percentage display with different styles `Settings -> Desktop -> Battery View` +> - More games in Dummy Mode -> click or hold any of arrow buttons +> - Lock device with pin (or regular lock if pin not set) by holding UP button on main screen [(by an4tur0r)](https://github.com/DarkFlippers/unleashed-firmware/pull/107) +> - **BadKB** plugin [(by Willy-JL, ClaraCrazy, XFW contributors)](https://github.com/Flipper-XFW/Xtreme-Firmware/tree/dev/applications/main/bad_kb) - (See in Applications->Tools) - (aka BadUSB via Bluetooth) +> - BadUSB -> Keyboard layouts [(by rien > dummy-decoy)](https://github.com/dummy-decoy/flipperzero-firmware/tree/dummy_decoy/bad_usb_keyboard_layout) +> - Custom community plugins and games added + all known working apps can be downloaded in extra pack in every release +> - Other small fixes and changes throughout +> - See other changes in readme below +>
Also check the [changelog in releases](https://github.com/DarkFlippers/unleashed-firmware/releases) for latest updates! ### Current modified and new Sub-GHz protocols list: -Thanks to Official team (to their SubGHz Developer, Skorp) for implementing support (decoder + encoder / or decode only) for these protocols in OFW. +Thanks to Official team (to their SubGHz Developer, Skorp) for implementing support (decoder + encoder / or decode only) for these protocols in OFW. -Keeloq [Not ALL systems supported for decode or emulation!] - [Supported manufacturers list](https://pastes.io/raw/unuj9bhe4m) +> [!NOTE] +> Not ALL Keeloq systems are supported for decoding or emulation! +>
+> Supported manufacturers include +>
+> +> ``` +> DoorHan, Alligator, Stilmatic, Mongoose, SL_A6-A9/Tomahawk_9010, Pantera, SL_A2-A4, Cenmax_St-5, SL_B6,B9_dop, Harpoon, Tomahawk_TZ-9030, Tomahawk_Z,X_3-5, Cenmax_St-7, Sheriff, Pantera_CLK, Cenmax, Alligator_S-275, Guard_RF-311A, Partisan_RX, APS-1100_APS-2550, Pantera_XS/Jaguar, NICE_Smilo, NICE_MHOUSE, Dea_Mio, Genius_Bravo, FAAC_RC,XT, FAAC_SLH, BFT, Came_Space, DTM_Neo, GSN, Beninca, Elmes_Poland, IronLogic, Comunello, Sommer(fsk476), Normstahl, KEY, JCM_Tech, EcoStar, Gibidi, Aprimatic, Kingates_Stylo4k, Centurion, KGB/Subaru, Magic_1, Magic_2, Magic_3, Magic_4, Teco, Mutanco_Mutancode, Leopard, Faraon, Reff, ZX-730-750-1055 +> ``` +
+ +
+ +
+Decoders/Encoders or emulation (+ programming mode) support made by @xMasterX +
-Decoders/Encoders or emulation (+ programming mode) support made by @xMasterX: - Marantec24 (static 24 bit) with add manually support -- GangQi (static 34 bit) with button parsing and add manually support (thanks to @mishamyte for captures and testing, thanks @Skorpionm for help) -- Hollarm (static 42 bit) with button parsing and add manually support (thanks to @mishamyte for captures, thanks @Skorpionm for help) +- GangQi (static 34 bit) with button parsing and add manually support (thanks to @mishamyte for captures and testing, thanks @Skorpionm for help) +- Hollarm (static 42 bit) with button parsing and add manually support (thanks to @mishamyte for captures, thanks @Skorpionm for help) - Hay21 (dynamic 21 bit) with button parsing - Nero Radio 57bit (+ 56bit support) - CAME 12bit/24bit encoder fixes (Fixes are now merged in OFW) - Keeloq: Dea Mio, Genius Bravo, GSN, HCS101, AN-Motors, JCM Tech, MHouse, Nice Smilo, DTM Neo, FAAC RC,XT, Mutancode, Normstahl, Beninca + Allmatic, Stilmatic, CAME Space, Aprimatic (model TR and similar), Centurion Nova (thanks Carlos !), Hormann EcoStar, Novoferm, Sommer, Monarch (thanks @ashphx !) +
-Protocols support made by Skorp (original implementation) and @xMasterX (current version): -- CAME Atomo -> Update! check out new [instructions](https://github.com/DarkFlippers/unleashed-firmware/blob/dev/documentation/SubGHzRemoteProg.md) -- Nice Flor S -> How to create new remote - [instructions](https://github.com/DarkFlippers/unleashed-firmware/blob/dev/documentation/SubGHzRemoteProg.md) -- FAAC SLH (Spa) -> Update!!! (Programming mode!) Check out new [instructions](https://github.com/DarkFlippers/unleashed-firmware/blob/dev/documentation/SubGHzRemoteProg.md) -- Keeloq: BFT Mitto -> Update! Check out new [instructions](https://github.com/DarkFlippers/unleashed-firmware/blob/dev/documentation/SubGHzRemoteProg.md) +
+Protocols support made by Skorp (original implementation) and @xMasterX (current version) +
+ +- CAME Atomo -> Update! check out new [instructions](https://github.com/DarkFlippers/unleashed-firmware/blob/dev/documentation/SubGHzRemoteProg.md) +- Nice Flor S -> How to create new remote - [instructions](https://github.com/DarkFlippers/unleashed-firmware/blob/dev/documentation/SubGHzRemoteProg.md) +- FAAC SLH (Spa) -> Update!!! (Programming mode!) Check out new [instructions](https://github.com/DarkFlippers/unleashed-firmware/blob/dev/documentation/SubGHzRemoteProg.md) +- Keeloq: BFT Mitto -> Update! Check out new [instructions](https://github.com/DarkFlippers/unleashed-firmware/blob/dev/documentation/SubGHzRemoteProg.md) - Star Line - Security+ v1 & v2 +
-Encoders made by @assasinfil and @xMasterX: -- Somfy Telis -> How to create new remote - [instructions](https://github.com/DarkFlippers/unleashed-firmware/blob/dev/documentation/SubGHzRemoteProg.md) +
+Encoders made by @assasinfil and @xMasterX +
+ +- Somfy Telis -> How to create new remote - [instructions](https://github.com/DarkFlippers/unleashed-firmware/blob/dev/documentation/SubGHzRemoteProg.md) - Somfy Keytis - KingGates Stylo 4k -- Alutech AT-4N -> How to create new remote - [instructions](https://github.com/DarkFlippers/unleashed-firmware/blob/dev/documentation/SubGHzRemoteProg.md) +- Alutech AT-4N -> How to create new remote - [instructions](https://github.com/DarkFlippers/unleashed-firmware/blob/dev/documentation/SubGHzRemoteProg.md) - Nice ON2E (Nice One) -> How to create new remote - [instructions](https://github.com/DarkFlippers/unleashed-firmware/blob/dev/documentation/SubGHzRemoteProg.md) +
+ +
+ +## ❤️ Please support development of the project -## Please support development of the project The majority of this project is developed and maintained by me, @xMasterX. -Our team is small and the guys are working on this project as much as they can solely based on the enthusiasm they have for this project and the community. +Our team is small and the guys are working on this project as much as they can solely based on the enthusiasm they have for this project and the community. - @Leptopt1los - NFC, RFID, Plugins, and many other things - @gid9798 - SubGHz, Plugins, many other things - currently offline :( - @assasinfil - SubGHz protocols, NFC parsers @@ -137,7 +213,7 @@ Our team is small and the guys are working on this project as much as they can s - And of course our GitHub community. Your PRs are a very important part of this firmware and open-source development. The amount of work done on this project is huge and we need your support, no matter how large or small. Even if you just say, "Thank you Unleashed firmware developers!" somewhere. Doing so will help us continue our work and will help drive us to make the firmware better every time. -Also, regarding our releases, every build has and always will be free and open-source. There will be no paywall releases or closed-source apps within the firmware. As long as I am working on this project it will never happen. +Also, regarding our releases, every build has and always will be free and open-source. There will be no paywall releases or closed-source apps within the firmware. As long as I am working on this project it will never happen. You can support us by using links or addresses below: |Service|Remark|QR Code|Link/Wallet| |-|-|-|-| @@ -155,103 +231,77 @@ You can support us by using links or addresses below: |XMR|(Monero)|
QR image
|`41xUz92suUu1u5Mu4qkrcs52gtfpu9rnZRdBpCJ244KRHf6xXSvVFevdf2cnjS7RAeYr5hn9MsEfxKoFDRSctFjG5fv1Mhn`| |TON||
QR image
|`UQCOqcnYkvzOZUV_9bPE_8oTbOrOF03MnF-VcJyjisTZmsxa`| -## Community apps included +
-### [🎲 Download Extra plugins for Unleashed](https://github.com/xMasterX/all-the-plugins/releases/latest) -### [List of Extra pack](https://github.com/xMasterX/all-the-plugins/tree/dev#extra-pack) | [List of Base *(Default)* pack](https://github.com/xMasterX/all-the-plugins/tree/dev#default-pack) +## 📱 Community Apps -See full list and sources here: [xMasterX/all-the-plugins](https://github.com/xMasterX/all-the-plugins/tree/dev) +Enhance your Flipper Zero with apps and plugins created by the community: -### Official Flipper Zero Apps Catalog [web version](https://lab.flipper.net/apps) or mobile app +- **Extra Plugins & Packs:** + Check out the latest extra plugins and plugin packs (Extra Pack and Base Pack) on [GitHub](https://github.com/xMasterX/all-the-plugins/releases/latest). -# Instructions -## First look at official docs [docs.flipper.net](https://docs.flipper.net/) -## [How to install](/documentation/HowToInstall.md) - [versions info](/CHANGELOG.md#recommended-update-option---web-updater): `r`,` `,`e`... -## Firmware & Development +- **Source Code & Full List:** + Find the complete list and source code at [xMasterX/all-the-plugins](https://github.com/xMasterX/all-the-plugins/tree/dev). -### - **Developer Documentation** - [developer.flipper.net](https://developer.flipper.net/flipperzero/doxygen) +- **Official Apps Catalog:** + Browse the official Flipper Zero Apps Catalog on the [web](https://lab.flipper.net/apps) or via the [mobile app](https://flipperzero.one/downloads). -### - **[How to build](/documentation/HowToBuild.md#how-to-build-by-yourself) | [Project-structure](#project-structure)** +
-### - **CLion IDE** - How to setup workspace for flipper firmware development [by Savely Krasovsky](https://krasovs.ky/2022/11/01/flipper-zero-clion.html) +## 📁 Where I can find IR, Sub-GHz, ... files, DBs, and other stuff? +- [UberGuidoZ Playground - Large collection of files - Github](https://github.com/UberGuidoZ/Flipper) +- [Awesome Flipper Zero - Github](https://github.com/djsime1/awesome-flipperzero) -### - **"Hello world!"** - plugin tutorial [English by DroomOne ](https://github.com/DroomOne/Flipper-Plugin-Tutorial) | [Russian by Pavel Yakovlev](https://yakovlev.me/hello-flipper-zero/) +
-### - [How to write your own app](https://flipper.atmanos.com/docs/overview/intro). Docs by atmanos **⚠️outdated API** +## 📘 Instructions -## Firmware & main Apps feature +### Firmware & main Apps feature -### - System: [How to change Flipper name](https://github.com/DarkFlippers/unleashed-firmware/blob/dev/documentation/CustomFlipperName.md) +- System: [How to change Flipper name](/documentation/CustomFlipperName.md) +- BadUSB: [How to add new keyboard layouts](https://github.com/dummy-decoy/flipperzero_badusb_kl) +- Infrared: [How to make captures to add them into Universal IR remotes](https://github.com/DarkFlippers/unleashed-firmware/blob/dev/documentation/InfraredCaptures.md) -### - BadUSB: [How to add new keyboard layouts](https://github.com/dummy-decoy/flipperzero_badusb_kl) +### Sub-GHz -### - Infrared: [How to make captures to add them into Universal IR remotes](https://github.com/DarkFlippers/unleashed-firmware/blob/dev/documentation/InfraredCaptures.md) +- [How to use Flipper as new remote (Nice FlorS, BFT Mitto, Somfy Telis, Aprimatic, AN-Motors, etc..)](https://github.com/DarkFlippers/unleashed-firmware/blob/dev/documentation/SubGHzRemoteProg.md) +- External Radio: [How to connect CC1101 module](https://github.com/quen0n/flipperzero-ext-cc1101) +- Transmission is blocked? [How to extend Sub-GHz frequency range](https://github.com/DarkFlippers/unleashed-firmware/blob/dev/documentation/DangerousSettings.md) +- [How to add extra Sub-GHz frequencies](https://github.com/DarkFlippers/unleashed-firmware/blob/dev/documentation/SubGHzSettings.md) +- [~~Configure Sub-GHz Remote App~~](https://github.com/DarkFlippers/unleashed-firmware/blob/dev/documentation/SubGHzRemotePlugin.md) ⚠️ Not recommended, please use embedded configurator -## **Sub-GHz** +### Plugins -### - [How to use Flipper as new remote (Nice FlorS, BFT Mitto, Somfy Telis, Aprimatic, AN-Motors, etc..)](https://github.com/DarkFlippers/unleashed-firmware/blob/dev/documentation/SubGHzRemoteProg.md) +- TOTP (Authenticator): [config description](https://github.com/akopachov/flipper-zero_authenticator/blob/master/docs/conf-file_description.md) +- Barcode Generator: [How to use](https://github.com/DarkFlippers/unleashed-firmware/blob/dev/documentation/BarcodeGenerator.md) +- Multi Converter: [How to use](https://github.com/DarkFlippers/unleashed-firmware/blob/dev/documentation/MultiConverter.md) +- WAV Player: [sample files & how to convert](https://github.com/UberGuidoZ/Flipper/tree/main/Wav_Player#readme) +- Sub-GHz playlist: [generator script](https://github.com/darmiel/flipper-scripts/blob/main/playlist/playlist_creator_by_chunk.py) -### - External Radio: [How to connect CC1101 module](https://github.com/quen0n/flipperzero-ext-cc1101) +### **Plugins that works with external hardware** [GPIO] -### - Transmission is blocked? [How to extend Sub-GHz frequency range](https://github.com/DarkFlippers/unleashed-firmware/blob/dev/documentation/DangerousSettings.md) +- Unitemp - Temperature sensors reader: [How to use & supported sensors](https://github.com/quen0n/unitemp-flipperzero#readme) +- [NMEA] GPS: [How to use](https://github.com/xMasterX/all-the-plugins/blob/dev/base_pack/gps_nmea_uart/README.md) +- i2c Tools [How to use](https://github.com/xMasterX/all-the-plugins/blob/dev/base_pack/flipper_i2ctools/README.md) +- [NRF24] plugins: [How to use](https://github.com/DarkFlippers/unleashed-firmware/blob/dev/documentation/NRF24.md) +- [WiFi] Scanner: [How to use](https://github.com/SequoiaSan/FlipperZero-WiFi-Scanner_Module#readme) | [Web Flasher](https://sequoiasan.github.io/FlipperZero-WiFi-Scanner_Module/) +- [ESP8266] Deauther: [How to use](https://github.com/SequoiaSan/FlipperZero-Wifi-ESP8266-Deauther-Module#readme) | [Web Flasher](https://sequoiasan.github.io/FlipperZero-Wifi-ESP8266-Deauther-Module/) +- [ESP32] WiFi Marauder: [How to use](https://github.com/UberGuidoZ/Flipper/tree/main/Wifi_DevBoard) docs by UberGuidoZ | [Marauder repo](https://github.com/justcallmekoko/ESP32Marauder) +- [ESP32-CAM] Camera Suite: [How to use](https://github.com/CodyTolene/Flipper-Zero-Camera-Suite) +- How to Upload `.bin` to ESP32/ESP8266: [Windows](https://github.com/SequoiaSan/Guide-How-To-Upload-bin-to-ESP8266-ESP32) | [FAP "ESP flasher"](https://github.com/0xchocolate/flipperzero-esp-flasher) +- [GPIO] SentrySafe plugin: [How to use](https://github.com/DarkFlippers/unleashed-firmware/blob/dev/documentation/SentrySafe.md) -### - [How to add extra Sub-GHz frequencies](https://github.com/DarkFlippers/unleashed-firmware/blob/dev/documentation/SubGHzSettings.md) +
-### - [~~Configure Sub-GHz Remote App~~](https://github.com/DarkFlippers/unleashed-firmware/blob/dev/documentation/SubGHzRemotePlugin.md) Not recommended, please use embedded configurator +## 👨‍💻 Firmware & Development -## **Plugins** +- **Developer Documentation** - [developer.flipper.net](https://developer.flipper.net/flipperzero/doxygen) +- **[How to build](/documentation/HowToBuild.md#how-to-build-by-yourself) | [Project-structure](#project-structure)** +- **CLion IDE** - How to setup workspace for flipper firmware development [by Savely Krasovsky](https://krasovs.ky/2022/11/01/flipper-zero-clion.html) +- **"Hello world!"** - plugin tutorial [English by DroomOne ](https://github.com/DroomOne/Flipper-Plugin-Tutorial) | [Russian by Pavel Yakovlev](https://yakovlev.me/hello-flipper-zero) +- [How to write your own app](https://flipper.atmanos.com/docs/overview/intro). -### - TOTP (Authenticator): [config description](https://github.com/akopachov/flipper-zero_authenticator/blob/master/docs/conf-file_description.md) - -### - Barcode Generator: [How to use](https://github.com/DarkFlippers/unleashed-firmware/blob/dev/documentation/BarcodeGenerator.md) - -### - Multi Converter: [How to use](https://github.com/DarkFlippers/unleashed-firmware/blob/dev/documentation/MultiConverter.md) - -### - WAV Player: [sample files & how to convert](https://github.com/UberGuidoZ/Flipper/tree/main/Wav_Player#readme) - -### - Sub-GHz playlist: [generator script](https://github.com/darmiel/flipper-scripts/blob/main/playlist/playlist_creator_by_chunk.py) - -## **Plugins that works with external hardware** [GPIO] - -### - Unitemp - Temperature sensors reader: [How to use & supported sensors](https://github.com/quen0n/unitemp-flipperzero#readme) - -### - [NMEA] GPS: [How to use](https://github.com/xMasterX/all-the-plugins/blob/dev/base_pack/gps_nmea_uart/README.md) - -### - i2c Tools [How to use](https://github.com/xMasterX/all-the-plugins/blob/dev/base_pack/flipper_i2ctools/README.md) - -### - [NRF24] plugins: [How to use](https://github.com/DarkFlippers/unleashed-firmware/blob/dev/documentation/NRF24.md) - - -### - [WiFi] Scanner: [How to use](https://github.com/SequoiaSan/FlipperZero-WiFi-Scanner_Module#readme) | [Web Flasher](https://sequoiasan.github.io/FlipperZero-WiFi-Scanner_Module/) - -### - [ESP8266] Deauther: [How to use](https://github.com/SequoiaSan/FlipperZero-Wifi-ESP8266-Deauther-Module#readme) | [Web Flasher](https://sequoiasan.github.io/FlipperZero-Wifi-ESP8266-Deauther-Module/) - -### - [ESP32] WiFi Marauder: [How to use](https://github.com/UberGuidoZ/Flipper/tree/main/Wifi_DevBoard) docs by UberGuidoZ | [Marauder repo](https://github.com/justcallmekoko/ESP32Marauder) - -### - [ESP32-CAM] Camera Suite: [How to use](https://github.com/CodyTolene/Flipper-Zero-Camera-Suite) - -### - How to Upload `.bin` to ESP32/ESP8266: [Windows](https://github.com/SequoiaSan/Guide-How-To-Upload-bin-to-ESP8266-ESP32) | [FAP "ESP flasher"](https://github.com/0xchocolate/flipperzero-esp-flasher) - -### - [GPIO] SentrySafe plugin: [How to use](https://github.com/DarkFlippers/unleashed-firmware/blob/dev/documentation/SentrySafe.md) - -
-
- -# Where I can find IR, Sub-GHz, ... files, DBs, and other stuff? -## [UberGuidoZ Playground - Large collection of files - Github](https://github.com/UberGuidoZ/Flipper) -## [Awesome Flipper Zero - Github](https://github.com/djsime1/awesome-flipperzero) - -
-
- -# Links - -* Official Docs: [docs.flipper.net](https://docs.flipper.net/) -* Official Forum: [forum.flipperzero.one](https://forum.flipperzero.one/) - -* Update! Developer Documentation [developer.flipper.net](https://developer.flipper.net/flipperzero/doxygen) - -# Project structure +### Project structure - `applications` - Applications and services used in firmware - `assets` - Assets used by applications and services @@ -263,4 +313,14 @@ See full list and sources here: [xMasterX/all-the-plugins](https://github.com/xM - `site_scons` - Build helpers - `scripts` - Supplementary scripts and python libraries home -Also, pay attention to the `ReadMe.md` files inside those directories. +Also, pay attention to the `ReadMe.md` files inside those directories.** + +
+ +## 🔗 Links +- **Unleashed web page:** [flipperunleashed.com](https://flipperunleashed.com) +- **Unleashed update server, direct .tgz update links for web updater or direct download:** [unleashedflip.com](https://unleashedflip.com) + +- Official Docs: [docs.flipper.net](https://docs.flipper.net) +- Official Forum: [forum.flipperzero.one](https://forum.flipperzero.one) +- Update! Developer Documentation [developer.flipper.net](https://developer.flipper.net/flipperzero/doxygen) From f0b20efbc9e18fdd30f0194e7fabf06acf6bf661 Mon Sep 17 00:00:00 2001 From: m-xim <170838360+m-xim@users.noreply.github.com> Date: Thu, 20 Feb 2025 18:02:49 +0300 Subject: [PATCH 060/268] docs: remove redundant
tags --- ReadMe.md | 8 -------- 1 file changed, 8 deletions(-) diff --git a/ReadMe.md b/ReadMe.md index 9f9b11fe5..2d3122677 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -42,7 +42,6 @@ Before getting started: - **FAQ:** Find answers to common questions in the [FAQ](/documentation/FAQ.md) -
## 📦 Releases @@ -57,7 +56,6 @@ Before getting started: - Web site: https://dev.unleashedflip.com - Telegram Telegram: t.me/kotnehleb -
## 🆕 What's New @@ -198,7 +196,6 @@ Thanks to Official team (to their SubGHz Developer, Skorp) for implementing supp - Nice ON2E (Nice One) -> How to create new remote - [instructions](https://github.com/DarkFlippers/unleashed-firmware/blob/dev/documentation/SubGHzRemoteProg.md) -
## ❤️ Please support development of the project @@ -231,7 +228,6 @@ You can support us by using links or addresses below: |XMR|(Monero)|
QR image
|`41xUz92suUu1u5Mu4qkrcs52gtfpu9rnZRdBpCJ244KRHf6xXSvVFevdf2cnjS7RAeYr5hn9MsEfxKoFDRSctFjG5fv1Mhn`| |TON||
QR image
|`UQCOqcnYkvzOZUV_9bPE_8oTbOrOF03MnF-VcJyjisTZmsxa`| -
## 📱 Community Apps @@ -246,13 +242,11 @@ Enhance your Flipper Zero with apps and plugins created by the community: - **Official Apps Catalog:** Browse the official Flipper Zero Apps Catalog on the [web](https://lab.flipper.net/apps) or via the [mobile app](https://flipperzero.one/downloads). -
## 📁 Where I can find IR, Sub-GHz, ... files, DBs, and other stuff? - [UberGuidoZ Playground - Large collection of files - Github](https://github.com/UberGuidoZ/Flipper) - [Awesome Flipper Zero - Github](https://github.com/djsime1/awesome-flipperzero) -
## 📘 Instructions @@ -291,7 +285,6 @@ Enhance your Flipper Zero with apps and plugins created by the community: - How to Upload `.bin` to ESP32/ESP8266: [Windows](https://github.com/SequoiaSan/Guide-How-To-Upload-bin-to-ESP8266-ESP32) | [FAP "ESP flasher"](https://github.com/0xchocolate/flipperzero-esp-flasher) - [GPIO] SentrySafe plugin: [How to use](https://github.com/DarkFlippers/unleashed-firmware/blob/dev/documentation/SentrySafe.md) -
## 👨‍💻 Firmware & Development @@ -315,7 +308,6 @@ Enhance your Flipper Zero with apps and plugins created by the community: Also, pay attention to the `ReadMe.md` files inside those directories.** -
## 🔗 Links - **Unleashed web page:** [flipperunleashed.com](https://flipperunleashed.com) From 290a6dc1eb787cea472c742f3294c46f3ce1e090 Mon Sep 17 00:00:00 2001 From: Nathan Perry Date: Thu, 20 Feb 2025 13:42:31 -0500 Subject: [PATCH 061/268] gpio: clear irq status before calling user handler (#4118) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * gpio: clear irq status before calling user handler * Format sources Co-authored-by: あく --- targets/f7/furi_hal/furi_hal_gpio.c | 32 ++++++++++++++--------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/targets/f7/furi_hal/furi_hal_gpio.c b/targets/f7/furi_hal/furi_hal_gpio.c index a020e9d7c..0104dd898 100644 --- a/targets/f7/furi_hal/furi_hal_gpio.c +++ b/targets/f7/furi_hal/furi_hal_gpio.c @@ -258,85 +258,85 @@ FURI_ALWAYS_INLINE static void furi_hal_gpio_int_call(uint16_t pin_num) { /* Interrupt handlers */ void EXTI0_IRQHandler(void) { if(LL_EXTI_IsActiveFlag_0_31(LL_EXTI_LINE_0)) { - furi_hal_gpio_int_call(0); LL_EXTI_ClearFlag_0_31(LL_EXTI_LINE_0); + furi_hal_gpio_int_call(0); } } void EXTI1_IRQHandler(void) { if(LL_EXTI_IsActiveFlag_0_31(LL_EXTI_LINE_1)) { - furi_hal_gpio_int_call(1); LL_EXTI_ClearFlag_0_31(LL_EXTI_LINE_1); + furi_hal_gpio_int_call(1); } } void EXTI2_IRQHandler(void) { if(LL_EXTI_IsActiveFlag_0_31(LL_EXTI_LINE_2)) { - furi_hal_gpio_int_call(2); LL_EXTI_ClearFlag_0_31(LL_EXTI_LINE_2); + furi_hal_gpio_int_call(2); } } void EXTI3_IRQHandler(void) { if(LL_EXTI_IsActiveFlag_0_31(LL_EXTI_LINE_3)) { - furi_hal_gpio_int_call(3); LL_EXTI_ClearFlag_0_31(LL_EXTI_LINE_3); + furi_hal_gpio_int_call(3); } } void EXTI4_IRQHandler(void) { if(LL_EXTI_IsActiveFlag_0_31(LL_EXTI_LINE_4)) { - furi_hal_gpio_int_call(4); LL_EXTI_ClearFlag_0_31(LL_EXTI_LINE_4); + furi_hal_gpio_int_call(4); } } void EXTI9_5_IRQHandler(void) { if(LL_EXTI_IsActiveFlag_0_31(LL_EXTI_LINE_5)) { - furi_hal_gpio_int_call(5); LL_EXTI_ClearFlag_0_31(LL_EXTI_LINE_5); + furi_hal_gpio_int_call(5); } if(LL_EXTI_IsActiveFlag_0_31(LL_EXTI_LINE_6)) { - furi_hal_gpio_int_call(6); LL_EXTI_ClearFlag_0_31(LL_EXTI_LINE_6); + furi_hal_gpio_int_call(6); } if(LL_EXTI_IsActiveFlag_0_31(LL_EXTI_LINE_7)) { - furi_hal_gpio_int_call(7); LL_EXTI_ClearFlag_0_31(LL_EXTI_LINE_7); + furi_hal_gpio_int_call(7); } if(LL_EXTI_IsActiveFlag_0_31(LL_EXTI_LINE_8)) { - furi_hal_gpio_int_call(8); LL_EXTI_ClearFlag_0_31(LL_EXTI_LINE_8); + furi_hal_gpio_int_call(8); } if(LL_EXTI_IsActiveFlag_0_31(LL_EXTI_LINE_9)) { - furi_hal_gpio_int_call(9); LL_EXTI_ClearFlag_0_31(LL_EXTI_LINE_9); + furi_hal_gpio_int_call(9); } } void EXTI15_10_IRQHandler(void) { if(LL_EXTI_IsActiveFlag_0_31(LL_EXTI_LINE_10)) { - furi_hal_gpio_int_call(10); LL_EXTI_ClearFlag_0_31(LL_EXTI_LINE_10); + furi_hal_gpio_int_call(10); } if(LL_EXTI_IsActiveFlag_0_31(LL_EXTI_LINE_11)) { - furi_hal_gpio_int_call(11); LL_EXTI_ClearFlag_0_31(LL_EXTI_LINE_11); + furi_hal_gpio_int_call(11); } if(LL_EXTI_IsActiveFlag_0_31(LL_EXTI_LINE_12)) { - furi_hal_gpio_int_call(12); LL_EXTI_ClearFlag_0_31(LL_EXTI_LINE_12); + furi_hal_gpio_int_call(12); } if(LL_EXTI_IsActiveFlag_0_31(LL_EXTI_LINE_13)) { - furi_hal_gpio_int_call(13); LL_EXTI_ClearFlag_0_31(LL_EXTI_LINE_13); + furi_hal_gpio_int_call(13); } if(LL_EXTI_IsActiveFlag_0_31(LL_EXTI_LINE_14)) { - furi_hal_gpio_int_call(14); LL_EXTI_ClearFlag_0_31(LL_EXTI_LINE_14); + furi_hal_gpio_int_call(14); } if(LL_EXTI_IsActiveFlag_0_31(LL_EXTI_LINE_15)) { - furi_hal_gpio_int_call(15); LL_EXTI_ClearFlag_0_31(LL_EXTI_LINE_15); + furi_hal_gpio_int_call(15); } } From 7c5c5d4749c7aabe407e61d9b854ba0f49890245 Mon Sep 17 00:00:00 2001 From: Anna Antonenko Date: Thu, 20 Feb 2025 23:54:38 +0400 Subject: [PATCH 062/268] [FL-3734] UART framing mode selection (#4121) * HAL: feat: uart framing * JS: feat: uart framing * fix formatting * fix pvs warning * HAL: flash impact reduction attempt 1 * HAL: flash impact reduction attempt 2 * fix compile error * HAL: finalize flash impact reduction * HAL: remove user-facing magic numbers Co-authored-by: Aleksandr Kutuzov --- applications/debug/uart_echo/uart_echo.c | 98 +++++++++++++-- .../resources/unit_tests/js/basic.js | 2 +- .../examples/apps/Scripts/uart_echo_8e1.js | 15 +++ applications/system/js_app/js_modules.c | 1 + applications/system/js_app/js_modules.h | 112 ++++++++++++------ .../system/js_app/modules/js_serial.c | 97 +++++++-------- .../create-fz-app/template/package.json | 2 +- .../system/js_app/packages/fz-sdk/global.d.ts | 6 +- .../js_app/packages/fz-sdk/package.json | 2 +- .../js_app/packages/fz-sdk/serial/index.d.ts | 23 +++- targets/f18/api_symbols.csv | 3 +- targets/f7/api_symbols.csv | 3 +- targets/f7/furi_hal/furi_hal_serial.c | 90 +++++++++++++- targets/f7/furi_hal/furi_hal_serial.h | 23 +++- targets/f7/furi_hal/furi_hal_serial_types.h | 35 ++++++ 15 files changed, 404 insertions(+), 108 deletions(-) create mode 100644 applications/system/js_app/examples/apps/Scripts/uart_echo_8e1.js diff --git a/applications/debug/uart_echo/uart_echo.c b/applications/debug/uart_echo/uart_echo.c index 4298dc33d..7150b830b 100644 --- a/applications/debug/uart_echo/uart_echo.c +++ b/applications/debug/uart_echo/uart_echo.c @@ -16,6 +16,9 @@ #define LINES_ON_SCREEN 6 #define COLUMNS_ON_SCREEN 21 #define DEFAULT_BAUD_RATE 230400 +#define DEFAULT_DATA_BITS FuriHalSerialDataBits8 +#define DEFAULT_PARITY FuriHalSerialParityNone +#define DEFAULT_STOP_BITS FuriHalSerialStopBits1 typedef struct UartDumpModel UartDumpModel; @@ -49,11 +52,12 @@ typedef enum { WorkerEventRxOverrunError = (1 << 4), WorkerEventRxFramingError = (1 << 5), WorkerEventRxNoiseError = (1 << 6), + WorkerEventRxParityError = (1 << 7), } WorkerEventFlags; #define WORKER_EVENTS_MASK \ (WorkerEventStop | WorkerEventRxData | WorkerEventRxIdle | WorkerEventRxOverrunError | \ - WorkerEventRxFramingError | WorkerEventRxNoiseError) + WorkerEventRxFramingError | WorkerEventRxNoiseError | WorkerEventRxParityError) const NotificationSequence sequence_notification = { &message_display_backlight_on, @@ -62,6 +66,13 @@ const NotificationSequence sequence_notification = { NULL, }; +const NotificationSequence sequence_error = { + &message_display_backlight_on, + &message_red_255, + &message_delay_10, + NULL, +}; + static void uart_echo_view_draw_callback(Canvas* canvas, void* _model) { UartDumpModel* model = _model; @@ -133,6 +144,9 @@ static void if(event & FuriHalSerialRxEventOverrunError) { flag |= WorkerEventRxOverrunError; } + if(event & FuriHalSerialRxEventParityError) { + flag |= WorkerEventRxParityError; + } furi_thread_flags_set(furi_thread_get_id(app->worker_thread), flag); } @@ -227,13 +241,21 @@ static int32_t uart_echo_worker(void* context) { if(events & WorkerEventRxNoiseError) { furi_hal_serial_tx(app->serial_handle, (uint8_t*)"\r\nDetect NE\r\n", 13); } + if(events & WorkerEventRxParityError) { + furi_hal_serial_tx(app->serial_handle, (uint8_t*)"\r\nDetect PE\r\n", 13); + } + notification_message(app->notification, &sequence_error); } } return 0; } -static UartEchoApp* uart_echo_app_alloc(uint32_t baudrate) { +static UartEchoApp* uart_echo_app_alloc( + uint32_t baudrate, + FuriHalSerialDataBits data_bits, + FuriHalSerialParity parity, + FuriHalSerialStopBits stop_bits) { UartEchoApp* app = malloc(sizeof(UartEchoApp)); app->rx_stream = furi_stream_buffer_alloc(2048, 1); @@ -275,6 +297,7 @@ static UartEchoApp* uart_echo_app_alloc(uint32_t baudrate) { app->serial_handle = furi_hal_serial_control_acquire(FuriHalSerialIdUsart); furi_check(app->serial_handle); furi_hal_serial_init(app->serial_handle, baudrate); + furi_hal_serial_configure_framing(app->serial_handle, data_bits, parity, stop_bits); furi_hal_serial_async_rx_start(app->serial_handle, uart_echo_on_irq_cb, app, true); @@ -318,19 +341,76 @@ static void uart_echo_app_free(UartEchoApp* app) { free(app); } +// silences "same-assignment" false positives in the arg parser below +// -V::1048 + int32_t uart_echo_app(void* p) { uint32_t baudrate = DEFAULT_BAUD_RATE; + FuriHalSerialDataBits data_bits = DEFAULT_DATA_BITS; + FuriHalSerialParity parity = DEFAULT_PARITY; + FuriHalSerialStopBits stop_bits = DEFAULT_STOP_BITS; + if(p) { - const char* baudrate_str = p; - if(strint_to_uint32(baudrate_str, NULL, &baudrate, 10) != StrintParseNoError) { - FURI_LOG_E(TAG, "Invalid baudrate: %s", baudrate_str); - baudrate = DEFAULT_BAUD_RATE; + // parse argument + char* parse_ptr = p; + bool parse_success = false; + + do { + if(strint_to_uint32(parse_ptr, &parse_ptr, &baudrate, 10) != StrintParseNoError) break; + + if(*(parse_ptr++) != '_') break; + + uint16_t data_bits_int; + if(strint_to_uint16(parse_ptr, &parse_ptr, &data_bits_int, 10) != StrintParseNoError) + break; + if(data_bits_int == 6) + data_bits = FuriHalSerialDataBits6; + else if(data_bits_int == 7) + data_bits = FuriHalSerialDataBits7; + else if(data_bits_int == 8) + data_bits = FuriHalSerialDataBits8; + else if(data_bits_int == 9) + data_bits = FuriHalSerialDataBits9; + else + break; + + char parity_char = *(parse_ptr++); + if(parity_char == 'N') + parity = FuriHalSerialParityNone; + else if(parity_char == 'E') + parity = FuriHalSerialParityEven; + else if(parity_char == 'O') + parity = FuriHalSerialParityOdd; + else + break; + + uint16_t stop_bits_int; + if(strint_to_uint16(parse_ptr, &parse_ptr, &stop_bits_int, 10) != StrintParseNoError) + break; + if(stop_bits_int == 5) + stop_bits = FuriHalSerialStopBits0_5; + else if(stop_bits_int == 1) + stop_bits = FuriHalSerialStopBits1; + else if(stop_bits_int == 15) + stop_bits = FuriHalSerialStopBits1_5; + else if(stop_bits_int == 2) + stop_bits = FuriHalSerialStopBits2; + else + break; + + parse_success = true; + } while(0); + + if(!parse_success) { + FURI_LOG_I( + TAG, + "Couldn't parse baud rate and framing (%s). Applying defaults (%d_8N1)", + (const char*)p, + DEFAULT_BAUD_RATE); } } - FURI_LOG_I(TAG, "Using baudrate: %lu", baudrate); - - UartEchoApp* app = uart_echo_app_alloc(baudrate); + UartEchoApp* app = uart_echo_app_alloc(baudrate, data_bits, parity, stop_bits); view_dispatcher_run(app->view_dispatcher); uart_echo_app_free(app); return 0; diff --git a/applications/debug/unit_tests/resources/unit_tests/js/basic.js b/applications/debug/unit_tests/resources/unit_tests/js/basic.js index 26f3f68f5..0ef904ecb 100644 --- a/applications/debug/unit_tests/resources/unit_tests/js/basic.js +++ b/applications/debug/unit_tests/resources/unit_tests/js/basic.js @@ -12,4 +12,4 @@ tests.assert_eq(false, doesSdkSupport(["abobus", "other-nonexistent-feature"])); tests.assert_eq("flipperdevices", flipper.firmwareVendor); tests.assert_eq(0, flipper.jsSdkVersion[0]); -tests.assert_eq(2, flipper.jsSdkVersion[1]); +tests.assert_eq(3, flipper.jsSdkVersion[1]); diff --git a/applications/system/js_app/examples/apps/Scripts/uart_echo_8e1.js b/applications/system/js_app/examples/apps/Scripts/uart_echo_8e1.js new file mode 100644 index 000000000..171bb4637 --- /dev/null +++ b/applications/system/js_app/examples/apps/Scripts/uart_echo_8e1.js @@ -0,0 +1,15 @@ +// This script is like uart_echo, except it uses 8E1 framing (8 data bits, even +// parity, 1 stop bit) as opposed to the default 8N1 (8 data bits, no parity, +// 1 stop bit) + +let serial = require("serial"); +serial.setup("usart", 230400, { dataBits: "8", parity: "even", stopBits: "1" }); + +while (1) { + let rx_data = serial.readBytes(1, 1000); + if (rx_data !== undefined) { + serial.write(rx_data); + let data_view = Uint8Array(rx_data); + print("0x" + data_view[0].toString(16)); + } +} diff --git a/applications/system/js_app/js_modules.c b/applications/system/js_app/js_modules.c index 47bdd516c..57997b09f 100644 --- a/applications/system/js_app/js_modules.c +++ b/applications/system/js_app/js_modules.c @@ -269,6 +269,7 @@ static const char* extra_features[] = { "baseline", // dummy "feature" "gpio-pwm", "gui-widget", + "serial-framing", }; /** diff --git a/applications/system/js_app/js_modules.h b/applications/system/js_app/js_modules.h index 29de72642..2babe231e 100644 --- a/applications/system/js_app/js_modules.h +++ b/applications/system/js_app/js_modules.h @@ -11,7 +11,7 @@ #define JS_SDK_VENDOR "flipperdevices" #define JS_SDK_MAJOR 0 -#define JS_SDK_MINOR 2 +#define JS_SDK_MINOR 3 /** * @brief Returns the foreign pointer in `obj["_"]` @@ -81,6 +81,11 @@ typedef enum { */ #define JS_AT_LEAST >= +typedef struct { + const char* name; + size_t value; +} JsEnumMapping; + #define JS_ENUM_MAP(var_name, ...) \ static const JsEnumMapping var_name##_mapping[] = { \ {NULL, sizeof(var_name)}, \ @@ -90,8 +95,14 @@ typedef enum { typedef struct { const char* name; - size_t value; -} JsEnumMapping; + size_t offset; +} JsObjectMapping; + +#define JS_OBJ_MAP(var_name, ...) \ + static const JsObjectMapping var_name##_mapping[] = { \ + __VA_ARGS__, \ + {NULL, 0}, \ + }; typedef struct { void* out; @@ -199,6 +210,54 @@ static inline void _js_validate_enum, \ var_name##_mapping}) +static inline bool _js_validate_object(struct mjs* mjs, mjs_val_t val, const void* extra) { + for(const JsObjectMapping* mapping = (JsObjectMapping*)extra; mapping->name; mapping++) + if(mjs_get(mjs, val, mapping->name, ~0) == MJS_UNDEFINED) return false; + return true; +} +static inline void + _js_convert_object(struct mjs* mjs, mjs_val_t* val, void* out, const void* extra) { + const JsObjectMapping* mapping = (JsObjectMapping*)extra; + for(; mapping->name; mapping++) { + mjs_val_t field_val = mjs_get(mjs, *val, mapping->name, ~0); + *(mjs_val_t*)((uint8_t*)out + mapping->offset) = field_val; + } +} +#define JS_ARG_OBJECT(var_name, name) \ + ((_js_arg_decl){ \ + &var_name, \ + mjs_is_object, \ + _js_convert_object, \ + name " object", \ + _js_validate_object, \ + var_name##_mapping}) + +/** + * @brief Validates and converts a JS value with a declarative interface + * + * Example: `int32_t my_value; JS_CONVERT_OR_RETURN(mjs, &mjs_val, JS_ARG_INT32(&my_value), "value source");` + * + * @warning This macro executes `return;` by design in case of a validation failure + */ +#define JS_CONVERT_OR_RETURN(mjs, value, decl, source, ...) \ + if(decl.validator) \ + if(!decl.validator(*value)) \ + JS_ERROR_AND_RETURN( \ + mjs, \ + MJS_BAD_ARGS_ERROR, \ + source ": expected %s", \ + ##__VA_ARGS__, \ + decl.expected_type); \ + if(decl.extended_validator) \ + if(!decl.extended_validator(mjs, *value, decl.extra_data)) \ + JS_ERROR_AND_RETURN( \ + mjs, \ + MJS_BAD_ARGS_ERROR, \ + source ": expected %s", \ + ##__VA_ARGS__, \ + decl.expected_type); \ + decl.converter(mjs, value, decl.out, decl.extra_data); + //-V:JS_FETCH_ARGS_OR_RETURN:1008 /** * @brief Fetches and validates the arguments passed to a JS function @@ -208,38 +267,21 @@ static inline void * @warning This macro executes `return;` by design in case of an argument count * mismatch or a validation failure */ -#define JS_FETCH_ARGS_OR_RETURN(mjs, arg_operator, ...) \ - _js_arg_decl _js_args[] = {__VA_ARGS__}; \ - int _js_arg_cnt = COUNT_OF(_js_args); \ - mjs_val_t _js_arg_vals[_js_arg_cnt]; \ - if(!(mjs_nargs(mjs) arg_operator _js_arg_cnt)) \ - JS_ERROR_AND_RETURN( \ - mjs, \ - MJS_BAD_ARGS_ERROR, \ - "expected %s%d arguments, got %d", \ - #arg_operator, \ - _js_arg_cnt, \ - mjs_nargs(mjs)); \ - for(int _i = 0; _i < _js_arg_cnt; _i++) { \ - _js_arg_vals[_i] = mjs_arg(mjs, _i); \ - if(_js_args[_i].validator) \ - if(!_js_args[_i].validator(_js_arg_vals[_i])) \ - JS_ERROR_AND_RETURN( \ - mjs, \ - MJS_BAD_ARGS_ERROR, \ - "argument %d: expected %s", \ - _i, \ - _js_args[_i].expected_type); \ - if(_js_args[_i].extended_validator) \ - if(!_js_args[_i].extended_validator(mjs, _js_arg_vals[_i], _js_args[_i].extra_data)) \ - JS_ERROR_AND_RETURN( \ - mjs, \ - MJS_BAD_ARGS_ERROR, \ - "argument %d: expected %s", \ - _i, \ - _js_args[_i].expected_type); \ - _js_args[_i].converter( \ - mjs, &_js_arg_vals[_i], _js_args[_i].out, _js_args[_i].extra_data); \ +#define JS_FETCH_ARGS_OR_RETURN(mjs, arg_operator, ...) \ + _js_arg_decl _js_args[] = {__VA_ARGS__}; \ + int _js_arg_cnt = COUNT_OF(_js_args); \ + mjs_val_t _js_arg_vals[_js_arg_cnt]; \ + if(!(mjs_nargs(mjs) arg_operator _js_arg_cnt)) \ + JS_ERROR_AND_RETURN( \ + mjs, \ + MJS_BAD_ARGS_ERROR, \ + "expected %s%d arguments, got %d", \ + #arg_operator, \ + _js_arg_cnt, \ + mjs_nargs(mjs)); \ + for(int _i = 0; _i < _js_arg_cnt; _i++) { \ + _js_arg_vals[_i] = mjs_arg(mjs, _i); \ + JS_CONVERT_OR_RETURN(mjs, &_js_arg_vals[_i], _js_args[_i], "argument %d", _i); \ } /** diff --git a/applications/system/js_app/modules/js_serial.c b/applications/system/js_app/modules/js_serial.c index b1e578fbc..20b18a4f1 100644 --- a/applications/system/js_app/modules/js_serial.c +++ b/applications/system/js_app/modules/js_serial.c @@ -20,14 +20,6 @@ typedef struct { char* data; } PatternArrayItem; -static const struct { - const char* name; - const FuriHalSerialId value; -} serial_channels[] = { - {"usart", FuriHalSerialIdUsart}, - {"lpuart", FuriHalSerialIdLpuart}, -}; - ARRAY_DEF(PatternArray, PatternArrayItem, M_POD_OPLIST); static void @@ -43,9 +35,54 @@ static void } static void js_serial_setup(struct mjs* mjs) { - mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0); - JsSerialInst* serial = mjs_get_ptr(mjs, obj_inst); - furi_assert(serial); + FuriHalSerialId serial_id; + int32_t baudrate; + JS_ENUM_MAP(serial_id, {"lpuart", FuriHalSerialIdLpuart}, {"usart", FuriHalSerialIdUsart}); + JS_FETCH_ARGS_OR_RETURN( + mjs, JS_AT_LEAST, JS_ARG_ENUM(serial_id, "SerialId"), JS_ARG_INT32(&baudrate)); + + FuriHalSerialDataBits data_bits = FuriHalSerialDataBits8; + FuriHalSerialParity parity = FuriHalSerialParityNone; + FuriHalSerialStopBits stop_bits = FuriHalSerialStopBits1; + if(mjs_nargs(mjs) > 2) { + struct framing { + mjs_val_t data_bits; + mjs_val_t parity; + mjs_val_t stop_bits; + } framing; + JS_OBJ_MAP( + framing, + {"dataBits", offsetof(struct framing, data_bits)}, + {"parity", offsetof(struct framing, parity)}, + {"stopBits", offsetof(struct framing, stop_bits)}); + JS_ENUM_MAP( + data_bits, + {"6", FuriHalSerialDataBits6}, + {"7", FuriHalSerialDataBits7}, + {"8", FuriHalSerialDataBits8}, + {"9", FuriHalSerialDataBits9}); + JS_ENUM_MAP( + parity, + {"none", FuriHalSerialParityNone}, + {"even", FuriHalSerialParityEven}, + {"odd", FuriHalSerialParityOdd}); + JS_ENUM_MAP( + stop_bits, + {"0.5", FuriHalSerialStopBits0_5}, + {"1", FuriHalSerialStopBits1}, + {"1.5", FuriHalSerialStopBits1_5}, + {"2", FuriHalSerialStopBits2}); + mjs_val_t framing_obj = mjs_arg(mjs, 2); + JS_CONVERT_OR_RETURN(mjs, &framing_obj, JS_ARG_OBJECT(framing, "Framing"), "argument 2"); + JS_CONVERT_OR_RETURN( + mjs, &framing.data_bits, JS_ARG_ENUM(data_bits, "DataBits"), "argument 2: dataBits"); + JS_CONVERT_OR_RETURN( + mjs, &framing.parity, JS_ARG_ENUM(parity, "Parity"), "argument 2: parity"); + JS_CONVERT_OR_RETURN( + mjs, &framing.stop_bits, JS_ARG_ENUM(stop_bits, "StopBits"), "argument 2: stopBits"); + } + + JsSerialInst* serial = JS_GET_CONTEXT(mjs); if(serial->setup_done) { mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "Serial is already configured"); @@ -53,43 +90,6 @@ static void js_serial_setup(struct mjs* mjs) { return; } - bool args_correct = false; - FuriHalSerialId serial_id = FuriHalSerialIdMax; - uint32_t baudrate = 0; - - do { - if(mjs_nargs(mjs) != 2) break; - - mjs_val_t arg = mjs_arg(mjs, 0); - if(!mjs_is_string(arg)) break; - - size_t str_len = 0; - const char* arg_str = mjs_get_string(mjs, &arg, &str_len); - for(size_t i = 0; i < COUNT_OF(serial_channels); i++) { - size_t name_len = strlen(serial_channels[i].name); - if(str_len != name_len) continue; - if(strncmp(arg_str, serial_channels[i].name, str_len) == 0) { - serial_id = serial_channels[i].value; - break; - } - } - if(serial_id == FuriHalSerialIdMax) { - break; - } - - arg = mjs_arg(mjs, 1); - if(!mjs_is_number(arg)) break; - baudrate = mjs_get_int32(mjs, arg); - - args_correct = true; - } while(0); - - if(!args_correct) { - mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, ""); - mjs_return(mjs, MJS_UNDEFINED); - return; - } - expansion_disable(furi_record_open(RECORD_EXPANSION)); furi_record_close(RECORD_EXPANSION); @@ -97,6 +97,7 @@ static void js_serial_setup(struct mjs* mjs) { if(serial->serial_handle) { serial->rx_stream = furi_stream_buffer_alloc(RX_BUF_LEN, 1); furi_hal_serial_init(serial->serial_handle, baudrate); + furi_hal_serial_configure_framing(serial->serial_handle, data_bits, parity, stop_bits); furi_hal_serial_async_rx_start( serial->serial_handle, js_serial_on_async_rx, serial, false); serial->setup_done = true; diff --git a/applications/system/js_app/packages/create-fz-app/template/package.json b/applications/system/js_app/packages/create-fz-app/template/package.json index 7acdeccaa..322a8b58b 100644 --- a/applications/system/js_app/packages/create-fz-app/template/package.json +++ b/applications/system/js_app/packages/create-fz-app/template/package.json @@ -6,7 +6,7 @@ "start": "npm run build && node node_modules/@flipperdevices/fz-sdk/sdk.js upload" }, "devDependencies": { - "@flipperdevices/fz-sdk": "^0.1", + "@flipperdevices/fz-sdk": "^0.3", "typescript": "^5.6.3" } } \ No newline at end of file diff --git a/applications/system/js_app/packages/fz-sdk/global.d.ts b/applications/system/js_app/packages/fz-sdk/global.d.ts index ba6996f27..4c7f217d0 100644 --- a/applications/system/js_app/packages/fz-sdk/global.d.ts +++ b/applications/system/js_app/packages/fz-sdk/global.d.ts @@ -72,7 +72,7 @@ * @brief Checks compatibility between the script and the JS SDK that the * firmware provides * - * @note You're looking at JS SDK v0.1 + * @note You're looking at JS SDK v0.3 * * @param expectedMajor JS SDK major version expected by the script * @param expectedMinor JS SDK minor version expected by the script @@ -92,7 +92,7 @@ declare function sdkCompatibilityStatus(expectedMajor: number, expectedMinor: nu * @brief Checks compatibility between the script and the JS SDK that the * firmware provides in a boolean fashion * - * @note You're looking at JS SDK v0.1 + * @note You're looking at JS SDK v0.3 * * @param expectedMajor JS SDK major version expected by the script * @param expectedMinor JS SDK minor version expected by the script @@ -105,7 +105,7 @@ declare function isSdkCompatible(expectedMajor: number, expectedMinor: number): * @brief Asks the user whether to continue executing the script if the versions * are not compatible. Does nothing if they are. * - * @note You're looking at JS SDK v0.1 + * @note You're looking at JS SDK v0.3 * * @param expectedMajor JS SDK major version expected by the script * @param expectedMinor JS SDK minor version expected by the script diff --git a/applications/system/js_app/packages/fz-sdk/package.json b/applications/system/js_app/packages/fz-sdk/package.json index 523845738..3ab108e48 100644 --- a/applications/system/js_app/packages/fz-sdk/package.json +++ b/applications/system/js_app/packages/fz-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@flipperdevices/fz-sdk", - "version": "0.2.0", + "version": "0.3.0", "description": "Type declarations and documentation for native JS modules available on Flipper Zero", "keywords": [ "flipper", diff --git a/applications/system/js_app/packages/fz-sdk/serial/index.d.ts b/applications/system/js_app/packages/fz-sdk/serial/index.d.ts index 3c249352e..5064c4213 100644 --- a/applications/system/js_app/packages/fz-sdk/serial/index.d.ts +++ b/applications/system/js_app/packages/fz-sdk/serial/index.d.ts @@ -4,16 +4,33 @@ * @module */ +export interface Framing { + /** + * @note 6 data bits can only be selected when parity is enabled (even or + * odd) + * @note 9 data bits can only be selected when parity is disabled (none) + */ + dataBits: "6" | "7" | "8" | "9"; + parity: "none" | "even" | "odd"; + /** + * @note LPUART only supports whole stop bit lengths (i.e. 1 and 2 but not + * 0.5 and 1.5) + */ + stopBits: "0.5" | "1" | "1.5" | "2"; +} + /** * @brief Initializes the serial port * * Automatically disables Expansion module service to prevent interference. * - * @param port The port to initialize (`"lpuart"` or `"start"`) - * @param baudRate + * @param port The port to initialize (`"lpuart"` or `"usart"`) + * @param baudRate Baud rate + * @param framing See `Framing` type * @version Added in JS SDK 0.1 + * @version Added `framing` parameter in JS SDK 0.3, extra feature `"serial-framing"` */ -export declare function setup(port: "lpuart" | "usart", baudRate: number): void; +export declare function setup(port: "lpuart" | "usart", baudRate: number, framing?: Framing): void; /** * @brief Writes data to the serial port diff --git a/targets/f18/api_symbols.csv b/targets/f18/api_symbols.csv index 5a750f24c..b93d6eb52 100644 --- a/targets/f18/api_symbols.csv +++ b/targets/f18/api_symbols.csv @@ -1,5 +1,5 @@ entry,status,name,type,params -Version,+,80.3,, +Version,+,80.4,, Header,+,applications/services/bt/bt_service/bt.h,, Header,+,applications/services/bt/bt_service/bt_keys_storage.h,, Header,+,applications/services/cli/cli.h,, @@ -1437,6 +1437,7 @@ Function,+,furi_hal_serial_async_rx,uint8_t,FuriHalSerialHandle* Function,+,furi_hal_serial_async_rx_available,_Bool,FuriHalSerialHandle* Function,+,furi_hal_serial_async_rx_start,void,"FuriHalSerialHandle*, FuriHalSerialAsyncRxCallback, void*, _Bool" Function,+,furi_hal_serial_async_rx_stop,void,FuriHalSerialHandle* +Function,+,furi_hal_serial_configure_framing,void,"FuriHalSerialHandle*, FuriHalSerialDataBits, FuriHalSerialParity, FuriHalSerialStopBits" Function,+,furi_hal_serial_control_acquire,FuriHalSerialHandle*,FuriHalSerialId Function,+,furi_hal_serial_control_deinit,void, Function,+,furi_hal_serial_control_init,void, diff --git a/targets/f7/api_symbols.csv b/targets/f7/api_symbols.csv index 217d90a2b..0fef0cb36 100644 --- a/targets/f7/api_symbols.csv +++ b/targets/f7/api_symbols.csv @@ -1,5 +1,5 @@ entry,status,name,type,params -Version,+,80.3,, +Version,+,80.4,, Header,+,applications/drivers/subghz/cc1101_ext/cc1101_ext_interconnect.h,, Header,+,applications/services/bt/bt_service/bt.h,, Header,+,applications/services/bt/bt_service/bt_keys_storage.h,, @@ -1627,6 +1627,7 @@ Function,+,furi_hal_serial_async_rx,uint8_t,FuriHalSerialHandle* Function,+,furi_hal_serial_async_rx_available,_Bool,FuriHalSerialHandle* Function,+,furi_hal_serial_async_rx_start,void,"FuriHalSerialHandle*, FuriHalSerialAsyncRxCallback, void*, _Bool" Function,+,furi_hal_serial_async_rx_stop,void,FuriHalSerialHandle* +Function,+,furi_hal_serial_configure_framing,void,"FuriHalSerialHandle*, FuriHalSerialDataBits, FuriHalSerialParity, FuriHalSerialStopBits" Function,+,furi_hal_serial_control_acquire,FuriHalSerialHandle*,FuriHalSerialId Function,+,furi_hal_serial_control_deinit,void, Function,+,furi_hal_serial_control_init,void, diff --git a/targets/f7/furi_hal/furi_hal_serial.c b/targets/f7/furi_hal/furi_hal_serial.c index 5ddb0785f..8ad9794a8 100644 --- a/targets/f7/furi_hal/furi_hal_serial.c +++ b/targets/f7/furi_hal/furi_hal_serial.c @@ -120,7 +120,7 @@ static void furi_hal_serial_usart_irq_callback(void* context) { } if(USART1->ISR & USART_ISR_PE) { USART1->ICR = USART_ICR_PECF; - event |= FuriHalSerialRxEventFrameError; + event |= FuriHalSerialRxEventParityError; } if(furi_hal_serial[FuriHalSerialIdUsart].buffer_rx_ptr == NULL) { @@ -321,7 +321,7 @@ static void furi_hal_serial_lpuart_irq_callback(void* context) { } if(LPUART1->ISR & USART_ISR_PE) { LPUART1->ICR = USART_ICR_PECF; - event |= FuriHalSerialRxEventFrameError; + event |= FuriHalSerialRxEventParityError; } if(furi_hal_serial[FuriHalSerialIdLpuart].buffer_rx_ptr == NULL) { @@ -605,6 +605,92 @@ void furi_hal_serial_set_br(FuriHalSerialHandle* handle, uint32_t baud) { } } +// Avoid duplicating look-up tables between USART and LPUART +static_assert(LL_LPUART_DATAWIDTH_7B == LL_USART_DATAWIDTH_7B); +static_assert(LL_LPUART_DATAWIDTH_8B == LL_USART_DATAWIDTH_8B); +static_assert(LL_LPUART_DATAWIDTH_9B == LL_USART_DATAWIDTH_9B); +static_assert(LL_LPUART_PARITY_NONE == LL_USART_PARITY_NONE); +static_assert(LL_LPUART_PARITY_EVEN == LL_USART_PARITY_EVEN); +static_assert(LL_LPUART_PARITY_ODD == LL_USART_PARITY_ODD); +static_assert(LL_LPUART_STOPBITS_1 == LL_USART_STOPBITS_1); +static_assert(LL_LPUART_STOPBITS_2 == LL_USART_STOPBITS_2); + +static const uint32_t serial_data_bits_lut[] = { + [FuriHalSerialDataBits7] = LL_USART_DATAWIDTH_7B, + [FuriHalSerialDataBits8] = LL_USART_DATAWIDTH_8B, + [FuriHalSerialDataBits9] = LL_USART_DATAWIDTH_9B, +}; + +static const uint32_t serial_parity_lut[] = { + [FuriHalSerialParityNone] = LL_USART_PARITY_NONE, + [FuriHalSerialParityEven] = LL_USART_PARITY_EVEN, + [FuriHalSerialParityOdd] = LL_USART_PARITY_ODD, +}; + +static const uint32_t serial_stop_bits_lut[] = { + [FuriHalSerialStopBits0_5] = LL_USART_STOPBITS_0_5, + [FuriHalSerialStopBits1] = LL_USART_STOPBITS_1, + [FuriHalSerialStopBits1_5] = LL_USART_STOPBITS_1_5, + [FuriHalSerialStopBits2] = LL_USART_STOPBITS_2, +}; + +static void furi_hal_serial_usart_configure_framing( + FuriHalSerialDataBits data_bits, + FuriHalSerialParity parity, + FuriHalSerialStopBits stop_bits) { + LL_USART_SetDataWidth(USART1, serial_data_bits_lut[data_bits]); + LL_USART_SetParity(USART1, serial_parity_lut[parity]); + LL_USART_SetStopBitsLength(USART1, serial_stop_bits_lut[stop_bits]); +} + +static void furi_hal_serial_lpuart_configure_framing( + FuriHalSerialDataBits data_bits, + FuriHalSerialParity parity, + FuriHalSerialStopBits stop_bits) { + LL_LPUART_SetDataWidth(LPUART1, serial_data_bits_lut[data_bits]); + LL_LPUART_SetParity(LPUART1, serial_parity_lut[parity]); + // Unsupported non-whole stop bit numbers have been furi_check'ed away + LL_LPUART_SetStopBitsLength(LPUART1, serial_stop_bits_lut[stop_bits]); +} + +void furi_hal_serial_configure_framing( + FuriHalSerialHandle* handle, + FuriHalSerialDataBits data_bits, + FuriHalSerialParity parity, + FuriHalSerialStopBits stop_bits) { + furi_check(handle); + + // Unsupported combinations + if(data_bits == FuriHalSerialDataBits9) furi_check(parity == FuriHalSerialParityNone); + if(data_bits == FuriHalSerialDataBits6) furi_check(parity != FuriHalSerialParityNone); + + // Extend data word to account for parity bit + if(parity != FuriHalSerialParityNone) data_bits++; + + if(handle->id == FuriHalSerialIdUsart) { + if(LL_USART_IsEnabled(USART1)) { + // Wait for transfer complete flag + while(!LL_USART_IsActiveFlag_TC(USART1)) + ; + LL_USART_Disable(USART1); + furi_hal_serial_usart_configure_framing(data_bits, parity, stop_bits); + LL_USART_Enable(USART1); + } + } else if(handle->id == FuriHalSerialIdLpuart) { + // Unsupported configurations + furi_check(stop_bits == FuriHalSerialStopBits1 || stop_bits == FuriHalSerialStopBits2); + + if(LL_LPUART_IsEnabled(LPUART1)) { + // Wait for transfer complete flag + while(!LL_LPUART_IsActiveFlag_TC(LPUART1)) + ; + LL_LPUART_Disable(LPUART1); + furi_hal_serial_lpuart_configure_framing(data_bits, parity, stop_bits); + LL_LPUART_Enable(LPUART1); + } + } +} + void furi_hal_serial_deinit(FuriHalSerialHandle* handle) { furi_check(handle); furi_hal_serial_async_rx_configure(handle, NULL, NULL); diff --git a/targets/f7/furi_hal/furi_hal_serial.h b/targets/f7/furi_hal/furi_hal_serial.h index 00010d83c..ca8860a60 100644 --- a/targets/f7/furi_hal/furi_hal_serial.h +++ b/targets/f7/furi_hal/furi_hal_serial.h @@ -16,7 +16,9 @@ extern "C" { /** Initialize Serial * - * Configures GPIO, configures and enables transceiver. + * Configures GPIO, configures and enables transceiver. Default framing settings + * are used: 8 data bits, no parity, 1 stop bit. Override them with + * `furi_hal_serial_configure_framing`. * * @param handle Serial handle * @param baud baud rate @@ -64,6 +66,20 @@ bool furi_hal_serial_is_baud_rate_supported(FuriHalSerialHandle* handle, uint32_ */ void furi_hal_serial_set_br(FuriHalSerialHandle* handle, uint32_t baud); +/** + * @brief Configures framing of a serial interface + * + * @param handle Serial handle + * @param data_bits Data bits + * @param parity Parity + * @param stop_bits Stop bits + */ +void furi_hal_serial_configure_framing( + FuriHalSerialHandle* handle, + FuriHalSerialDataBits data_bits, + FuriHalSerialParity parity, + FuriHalSerialStopBits stop_bits); + /** Transmits data in semi-blocking mode * * Fills transmission pipe with data, returns as soon as all bytes from buffer @@ -93,6 +109,7 @@ typedef enum { FuriHalSerialRxEventFrameError = (1 << 2), /**< Framing Error: incorrect frame detected */ FuriHalSerialRxEventNoiseError = (1 << 3), /**< Noise Error: noise on the line detected */ FuriHalSerialRxEventOverrunError = (1 << 4), /**< Overrun Error: no space for received data */ + FuriHalSerialRxEventParityError = (1 << 5), /**< Parity Error: incorrect parity bit received */ } FuriHalSerialRxEvent; /** Receive callback @@ -172,7 +189,7 @@ typedef void (*FuriHalSerialDmaRxCallback)( void* context); /** - * @brief Enable an input/output directon + * @brief Enable an input/output direction * * Takes over the respective pin by reconfiguring it to * the appropriate alternative function. @@ -185,7 +202,7 @@ void furi_hal_serial_enable_direction( FuriHalSerialDirection direction); /** - * @brief Disable an input/output directon + * @brief Disable an input/output direction * * Releases the respective pin by reconfiguring it to * initial state, making possible its use for other purposes. diff --git a/targets/f7/furi_hal/furi_hal_serial_types.h b/targets/f7/furi_hal/furi_hal_serial_types.h index 9f10102e1..a427765dd 100644 --- a/targets/f7/furi_hal/furi_hal_serial_types.h +++ b/targets/f7/furi_hal/furi_hal_serial_types.h @@ -19,4 +19,39 @@ typedef enum { FuriHalSerialDirectionMax, } FuriHalSerialDirection; +/** + * @brief Actual data bits, i.e. not including start/stop and parity bits + * @note 6 data bits are only permitted when parity is enabled + * @note 9 data bits are only permitted when parity is disabled + */ +typedef enum { + FuriHalSerialDataBits6, + FuriHalSerialDataBits7, + FuriHalSerialDataBits8, + FuriHalSerialDataBits9, + + FuriHalSerialDataBitsMax, +} FuriHalSerialDataBits; + +typedef enum { + FuriHalSerialParityNone, + FuriHalSerialParityEven, + FuriHalSerialParityOdd, + + FuriHalSerialParityMax, +} FuriHalSerialParity; + +/** + * @brief Stop bit length + * @note LPUART only supports whole stop bit lengths (i.e. 1 and 2, but not 0.5 and 1.5) + */ +typedef enum { + FuriHalSerialStopBits0_5, + FuriHalSerialStopBits1, + FuriHalSerialStopBits1_5, + FuriHalSerialStopBits2, + + FuriHalSerialStopBits2Max, +} FuriHalSerialStopBits; + typedef struct FuriHalSerialHandle FuriHalSerialHandle; From 4e9aa3883b4ac9ecee688cbcd70079a5d1a94f67 Mon Sep 17 00:00:00 2001 From: Akiva-Cohen <150308530+Akiva-Cohen@users.noreply.github.com> Date: Thu, 20 Feb 2025 16:57:28 -0500 Subject: [PATCH 063/268] Updated Button Panel (#4119) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * updated button panel * fixed mismateched .c and .h files * Gui: extra events for ok button handling in button_panel * Gui: extra events for other buttons handling in button_panel Co-authored-by: あく --- .../common/infrared_scene_universal_common.c | 8 ++++--- .../common/infrared_scene_universal_common.h | 2 +- .../services/gui/modules/button_panel.c | 23 +++++++++++-------- .../services/gui/modules/button_panel.h | 2 +- 4 files changed, 21 insertions(+), 14 deletions(-) diff --git a/applications/main/infrared/scenes/common/infrared_scene_universal_common.c b/applications/main/infrared/scenes/common/infrared_scene_universal_common.c index 62b7350e7..4e5a965a7 100644 --- a/applications/main/infrared/scenes/common/infrared_scene_universal_common.c +++ b/applications/main/infrared/scenes/common/infrared_scene_universal_common.c @@ -13,10 +13,12 @@ typedef union { } InfraredSceneState; #pragma pack(pop) -void infrared_scene_universal_common_item_callback(void* context, uint32_t index) { +void infrared_scene_universal_common_item_callback(void* context, uint32_t index, InputType type) { InfraredApp* infrared = context; - uint32_t event = infrared_custom_event_pack(InfraredCustomEventTypeButtonSelected, index); - view_dispatcher_send_custom_event(infrared->view_dispatcher, event); + if(type == InputTypeShort) { + uint32_t event = infrared_custom_event_pack(InfraredCustomEventTypeButtonSelected, index); + view_dispatcher_send_custom_event(infrared->view_dispatcher, event); + } } static void infrared_scene_universal_common_progress_input_callback( diff --git a/applications/main/infrared/scenes/common/infrared_scene_universal_common.h b/applications/main/infrared/scenes/common/infrared_scene_universal_common.h index a6c697d77..277598e42 100644 --- a/applications/main/infrared/scenes/common/infrared_scene_universal_common.h +++ b/applications/main/infrared/scenes/common/infrared_scene_universal_common.h @@ -5,4 +5,4 @@ void infrared_scene_universal_common_on_enter(void* context); bool infrared_scene_universal_common_on_event(void* context, SceneManagerEvent event); void infrared_scene_universal_common_on_exit(void* context); -void infrared_scene_universal_common_item_callback(void* context, uint32_t index); +void infrared_scene_universal_common_item_callback(void* context, uint32_t index, InputType type); diff --git a/applications/services/gui/modules/button_panel.c b/applications/services/gui/modules/button_panel.c index 9301870ef..efc512bc5 100644 --- a/applications/services/gui/modules/button_panel.c +++ b/applications/services/gui/modules/button_panel.c @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -46,6 +47,7 @@ ARRAY_DEF(ButtonMatrix, ButtonArray_t); struct ButtonPanel { View* view; + bool freeze_input; }; typedef struct { @@ -63,7 +65,7 @@ static void button_panel_process_up(ButtonPanel* button_panel); static void button_panel_process_down(ButtonPanel* button_panel); static void button_panel_process_left(ButtonPanel* button_panel); static void button_panel_process_right(ButtonPanel* button_panel); -static void button_panel_process_ok(ButtonPanel* button_panel); +static void button_panel_process_ok(ButtonPanel* button_panel, InputType input); static void button_panel_view_draw_callback(Canvas* canvas, void* _model); static bool button_panel_view_input_callback(InputEvent* event, void* context); @@ -347,7 +349,7 @@ static void button_panel_process_right(ButtonPanel* button_panel) { true); } -void button_panel_process_ok(ButtonPanel* button_panel) { +void button_panel_process_ok(ButtonPanel* button_panel, InputType type) { ButtonItem* button_item = NULL; with_view_model( @@ -360,7 +362,7 @@ void button_panel_process_ok(ButtonPanel* button_panel) { true); if(button_item && button_item->callback) { - button_item->callback(button_item->callback_context, button_item->index); + button_item->callback(button_item->callback_context, button_item->index, type); } } @@ -368,8 +370,15 @@ static bool button_panel_view_input_callback(InputEvent* event, void* context) { ButtonPanel* button_panel = context; furi_assert(button_panel); bool consumed = false; - - if(event->type == InputTypeShort) { + if(event->key == InputKeyOk) { + if((event->type == InputTypePress) || (event->type == InputTypeRelease)) { + button_panel->freeze_input = (event->type == InputTypePress); + } + consumed = true; + button_panel_process_ok(button_panel, event->type); + } + if(!button_panel->freeze_input && + (!(event->type == InputTypePress) && !(event->type == InputTypeRelease))) { switch(event->key) { case InputKeyUp: consumed = true; @@ -387,10 +396,6 @@ static bool button_panel_view_input_callback(InputEvent* event, void* context) { consumed = true; button_panel_process_right(button_panel); break; - case InputKeyOk: - consumed = true; - button_panel_process_ok(button_panel); - break; default: break; } diff --git a/applications/services/gui/modules/button_panel.h b/applications/services/gui/modules/button_panel.h index 1218222b8..159e14336 100644 --- a/applications/services/gui/modules/button_panel.h +++ b/applications/services/gui/modules/button_panel.h @@ -15,7 +15,7 @@ extern "C" { typedef struct ButtonPanel ButtonPanel; /** Callback type to call for handling selecting button_panel items */ -typedef void (*ButtonItemCallback)(void* context, uint32_t index); +typedef void (*ButtonItemCallback)(void* context, uint32_t index, InputType type); /** Allocate new button_panel module. * From 16d18a79a958fd78ca41b61778136b6957efac77 Mon Sep 17 00:00:00 2001 From: Anna Antonenko Date: Fri, 21 Feb 2025 05:04:02 +0400 Subject: [PATCH 064/268] [FL-3900] Update heap implementation (#4123) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * furi: update heap4 to latest * debug: heap under/overflow testing app * fix formatting * silence pvs warnings * Linker: symbols without type * Infrared: fix crash in universal remotes on long back press * Infrared: properly fix incorrect input handling behavior and crash in universal remote. Fix same issue in hid_app. * FreeRTOSConfig: explicit cast to uint in configTOTAL_HEAP_SIZE * Format sources * C and C++ compatible version of stm32wb55_linker.h Co-authored-by: あく --- applications/debug/crash_test/crash_test.c | 59 ++ .../infrared/views/infrared_progress_view.c | 6 +- .../system/hid_app/views/hid_mouse_clicker.c | 2 +- furi/core/check.c | 6 +- furi/core/memmgr.c | 3 +- furi/core/memmgr_heap.c | 593 ++++++++++-------- furi/flipper.c | 6 + targets/f7/inc/FreeRTOSConfig.h | 9 +- targets/f7/inc/stm32wb55_linker.h | 27 +- 9 files changed, 432 insertions(+), 279 deletions(-) diff --git a/applications/debug/crash_test/crash_test.c b/applications/debug/crash_test/crash_test.c index 2b2be13d6..4c5a53ceb 100644 --- a/applications/debug/crash_test/crash_test.c +++ b/applications/debug/crash_test/crash_test.c @@ -24,8 +24,49 @@ typedef enum { CrashTestSubmenuAssertMessage, CrashTestSubmenuCrash, CrashTestSubmenuHalt, + CrashTestSubmenuHeapUnderflow, + CrashTestSubmenuHeapOverflow, } CrashTestSubmenu; +static void crash_test_corrupt_heap_underflow(void) { + const size_t block_size = 1000; + const size_t underflow_size = 123; + uint8_t* block = malloc(block_size); + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wstringop-overflow" // that's what we want! + memset(block - underflow_size, 0xDD, underflow_size); // -V769 +#pragma GCC diagnostic pop + + free(block); // should crash here (if compiled with DEBUG=1) + + // If we got here, the heap wasn't able to detect our corruption and crash + furi_crash("Test failed, should've crashed with \"FreeRTOS Assert\" error"); +} + +static void crash_test_corrupt_heap_overflow(void) { + const size_t block_size = 1000; + const size_t overflow_size = 123; + uint8_t* block1 = malloc(block_size); + uint8_t* block2 = malloc(block_size); + memset(block2, 12, 34); // simulate use to avoid optimization // -V597 // -V1086 + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wstringop-overflow" // that's what we want! + memset(block1 + block_size, 0xDD, overflow_size); // -V769 // -V512 +#pragma GCC diagnostic pop + + uint8_t* block3 = malloc(block_size); + memset(block3, 12, 34); // simulate use to avoid optimization // -V597 // -V1086 + + free(block3); // should crash here (if compiled with DEBUG=1) + free(block2); + free(block1); + + // If we got here, the heap wasn't able to detect our corruption and crash + furi_crash("Test failed, should've crashed with \"FreeRTOS Assert\" error"); +} + static void crash_test_submenu_callback(void* context, uint32_t index) { CrashTest* instance = (CrashTest*)context; UNUSED(instance); @@ -49,6 +90,12 @@ static void crash_test_submenu_callback(void* context, uint32_t index) { case CrashTestSubmenuHalt: furi_halt("Crash test: furi_halt"); break; + case CrashTestSubmenuHeapUnderflow: + crash_test_corrupt_heap_underflow(); + break; + case CrashTestSubmenuHeapOverflow: + crash_test_corrupt_heap_overflow(); + break; default: furi_crash(); } @@ -94,6 +141,18 @@ CrashTest* crash_test_alloc(void) { instance->submenu, "Crash", CrashTestSubmenuCrash, crash_test_submenu_callback, instance); submenu_add_item( instance->submenu, "Halt", CrashTestSubmenuHalt, crash_test_submenu_callback, instance); + submenu_add_item( + instance->submenu, + "Heap underflow", + CrashTestSubmenuHeapUnderflow, + crash_test_submenu_callback, + instance); + submenu_add_item( + instance->submenu, + "Heap overflow", + CrashTestSubmenuHeapOverflow, + crash_test_submenu_callback, + instance); return instance; } diff --git a/applications/main/infrared/views/infrared_progress_view.c b/applications/main/infrared/views/infrared_progress_view.c index 716027297..76454b5bd 100644 --- a/applications/main/infrared/views/infrared_progress_view.c +++ b/applications/main/infrared/views/infrared_progress_view.c @@ -107,7 +107,11 @@ void infrared_progress_view_set_paused(InfraredProgressView* instance, bool is_p bool infrared_progress_view_input_callback(InputEvent* event, void* context) { InfraredProgressView* instance = context; - if(event->type != InputTypeShort && event->type != InputTypeRepeat) return false; + + if(event->type == InputTypePress || event->type == InputTypeRelease) { + return false; + } + if(!instance->input_callback) return false; with_view_model( diff --git a/applications/system/hid_app/views/hid_mouse_clicker.c b/applications/system/hid_app/views/hid_mouse_clicker.c index e289b7179..491f9962b 100644 --- a/applications/system/hid_app/views/hid_mouse_clicker.c +++ b/applications/system/hid_app/views/hid_mouse_clicker.c @@ -124,7 +124,7 @@ static bool hid_mouse_clicker_input_callback(InputEvent* event, void* context) { bool consumed = false; bool rate_changed = false; - if(event->type != InputTypeShort && event->type != InputTypeRepeat) { + if(event->type == InputTypePress || event->type == InputTypeRelease) { return false; } diff --git a/furi/core/check.c b/furi/core/check.c index ba05a675f..ee8ee4af0 100644 --- a/furi/core/check.c +++ b/furi/core/check.c @@ -102,11 +102,13 @@ static void __furi_print_bt_stack_info(void) { static void __furi_print_heap_info(void) { furi_log_puts("\r\n\t heap total: "); - __furi_put_uint32_as_text(xPortGetTotalHeapSize()); + __furi_put_uint32_as_text(configTOTAL_HEAP_SIZE); furi_log_puts("\r\n\t heap free: "); __furi_put_uint32_as_text(xPortGetFreeHeapSize()); + HeapStats_t heap_stats; + vPortGetHeapStats(&heap_stats); furi_log_puts("\r\n\t heap watermark: "); - __furi_put_uint32_as_text(xPortGetMinimumEverFreeHeapSize()); + __furi_put_uint32_as_text(heap_stats.xMinimumEverFreeBytesRemaining); } static void __furi_print_name(bool isr) { diff --git a/furi/core/memmgr.c b/furi/core/memmgr.c index 8ee0d1723..a3bbf4556 100644 --- a/furi/core/memmgr.c +++ b/furi/core/memmgr.c @@ -1,6 +1,7 @@ #include "memmgr.h" #include #include +#include extern void* pvPortMalloc(size_t xSize); extern void vPortFree(void* pv); @@ -51,7 +52,7 @@ size_t memmgr_get_free_heap(void) { } size_t memmgr_get_total_heap(void) { - return xPortGetTotalHeapSize(); + return configTOTAL_HEAP_SIZE; } size_t memmgr_get_minimum_free_heap(void) { diff --git a/furi/core/memmgr_heap.c b/furi/core/memmgr_heap.c index 359d0e3db..c8a72bc8c 100644 --- a/furi/core/memmgr_heap.c +++ b/furi/core/memmgr_heap.c @@ -1,6 +1,8 @@ /* - * FreeRTOS Kernel V10.2.1 - * Copyright (C) 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * FreeRTOS Kernel V11.1.0 + * Copyright (C) 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * SPDX-License-Identifier: MIT * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in @@ -19,10 +21,9 @@ * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * - * http://www.FreeRTOS.org - * http://aws.amazon.com/freertos + * https://www.FreeRTOS.org + * https://github.com/FreeRTOS * - * 1 tab == 4 spaces! */ /* @@ -31,21 +32,25 @@ * limits memory fragmentation. * * See heap_1.c, heap_2.c and heap_3.c for alternative implementations, and the - * memory management pages of http://www.FreeRTOS.org for more information. + * memory management pages of https://www.FreeRTOS.org for more information. */ #include "memmgr_heap.h" #include "check.h" #include +#include #include #include #include #include #include +// -V::562 +// -V::650 + /* Defining MPU_WRAPPERS_INCLUDED_FROM_API_FILE prevents task.h from redefining -all the API functions to use the MPU wrappers. That should only be done when -task.h is included from an application file. */ + * all the API functions to use the MPU wrappers. That should only be done when + * task.h is included from an application file. */ #define MPU_WRAPPERS_INCLUDED_FROM_API_FILE #include @@ -53,8 +58,12 @@ task.h is included from an application file. */ #undef MPU_WRAPPERS_INCLUDED_FROM_API_FILE -#ifdef HEAP_PRINT_DEBUG -#error This feature is broken, logging transport must be replaced with RTT +#if(configSUPPORT_DYNAMIC_ALLOCATION == 0) +#error This file must not be used if configSUPPORT_DYNAMIC_ALLOCATION is 0 +#endif + +#ifndef configHEAP_CLEAR_MEMORY_ON_FREE +#define configHEAP_CLEAR_MEMORY_ON_FREE 0 #endif /* Block sizes must not get too small. */ @@ -63,16 +72,75 @@ task.h is included from an application file. */ /* Assumes 8bit bytes! */ #define heapBITS_PER_BYTE ((size_t)8) +/* Max value that fits in a size_t type. */ +#define heapSIZE_MAX (~((size_t)0)) + +/* Check if multiplying a and b will result in overflow. */ +#define heapMULTIPLY_WILL_OVERFLOW(a, b) (((a) > 0) && ((b) > (heapSIZE_MAX / (a)))) + +/* Check if adding a and b will result in overflow. */ +#define heapADD_WILL_OVERFLOW(a, b) ((a) > (heapSIZE_MAX - (b))) + +/* Check if the subtraction operation ( a - b ) will result in underflow. */ +#define heapSUBTRACT_WILL_UNDERFLOW(a, b) ((a) < (b)) + +/* MSB of the xBlockSize member of an BlockLink_t structure is used to track + * the allocation status of a block. When MSB of the xBlockSize member of + * an BlockLink_t structure is set then the block belongs to the application. + * When the bit is free the block is still part of the free heap space. */ +#define heapBLOCK_ALLOCATED_BITMASK (((size_t)1) << ((sizeof(size_t) * heapBITS_PER_BYTE) - 1)) +#define heapBLOCK_SIZE_IS_VALID(xBlockSize) (((xBlockSize) & heapBLOCK_ALLOCATED_BITMASK) == 0) +#define heapBLOCK_IS_ALLOCATED(pxBlock) \ + (((pxBlock->xBlockSize) & heapBLOCK_ALLOCATED_BITMASK) != 0) +#define heapALLOCATE_BLOCK(pxBlock) ((pxBlock->xBlockSize) |= heapBLOCK_ALLOCATED_BITMASK) +#define heapFREE_BLOCK(pxBlock) ((pxBlock->xBlockSize) &= ~heapBLOCK_ALLOCATED_BITMASK) + +/*-----------------------------------------------------------*/ + /* Heap start end symbols provided by linker */ uint8_t* ucHeap = (uint8_t*)&__heap_start__; /* Define the linked list structure. This is used to link free blocks in order -of their memory address. */ + * of their memory address. */ typedef struct A_BLOCK_LINK { - struct A_BLOCK_LINK* pxNextFreeBlock; /*<< The next free block in the list. */ - size_t xBlockSize; /*<< The size of the free block. */ + struct A_BLOCK_LINK* pxNextFreeBlock; /**< The next free block in the list. */ + size_t xBlockSize; /**< The size of the free block. */ } BlockLink_t; +/* Setting configENABLE_HEAP_PROTECTOR to 1 enables heap block pointers + * protection using an application supplied canary value to catch heap + * corruption should a heap buffer overflow occur. + */ +#if(configENABLE_HEAP_PROTECTOR == 1) + +/** + * @brief Application provided function to get a random value to be used as canary. + * + * @param pxHeapCanary [out] Output parameter to return the canary value. + */ +extern void vApplicationGetRandomHeapCanary(portPOINTER_SIZE_TYPE* pxHeapCanary); + +/* Canary value for protecting internal heap pointers. */ +PRIVILEGED_DATA static portPOINTER_SIZE_TYPE xHeapCanary; + +/* Macro to load/store BlockLink_t pointers to memory. By XORing the + * pointers with a random canary value, heap overflows will result + * in randomly unpredictable pointer values which will be caught by + * heapVALIDATE_BLOCK_POINTER assert. */ +#define heapPROTECT_BLOCK_POINTER(pxBlock) \ + ((BlockLink_t*)(((portPOINTER_SIZE_TYPE)(pxBlock)) ^ xHeapCanary)) +#else + +#define heapPROTECT_BLOCK_POINTER(pxBlock) (pxBlock) + +#endif /* configENABLE_HEAP_PROTECTOR */ + +/* Assert that a heap block pointer is within the heap bounds. */ +#define heapVALIDATE_BLOCK_POINTER(pxBlock) \ + configASSERT( \ + ((uint8_t*)(pxBlock) >= &(ucHeap[0])) && \ + ((uint8_t*)(pxBlock) <= &(ucHeap[configTOTAL_HEAP_SIZE - 1]))) + /*-----------------------------------------------------------*/ /* @@ -81,34 +149,31 @@ typedef struct A_BLOCK_LINK { * the block in front it and/or the block behind it if the memory blocks are * adjacent to each other. */ -static void prvInsertBlockIntoFreeList(BlockLink_t* pxBlockToInsert); +static void prvInsertBlockIntoFreeList(BlockLink_t* pxBlockToInsert) PRIVILEGED_FUNCTION; /* * Called automatically to setup the required heap structures the first time * pvPortMalloc() is called. */ -static void prvHeapInit(void); +static void prvHeapInit(void) PRIVILEGED_FUNCTION; /*-----------------------------------------------------------*/ /* The size of the structure placed at the beginning of each allocated memory -block must by correctly byte aligned. */ + * block must by correctly byte aligned. */ static const size_t xHeapStructSize = (sizeof(BlockLink_t) + ((size_t)(portBYTE_ALIGNMENT - 1))) & ~((size_t)portBYTE_ALIGNMENT_MASK); /* Create a couple of list links to mark the start and end of the list. */ -static BlockLink_t xStart, *pxEnd = NULL; +PRIVILEGED_DATA static BlockLink_t xStart; +PRIVILEGED_DATA static BlockLink_t* pxEnd = NULL; -/* Keeps track of the number of free bytes remaining, but says nothing about -fragmentation. */ -static size_t xFreeBytesRemaining = 0U; -static size_t xMinimumEverFreeBytesRemaining = 0U; - -/* Gets set to the top bit of an size_t type. When this bit in the xBlockSize -member of an BlockLink_t structure is set then the block belongs to the -application. When the bit is free the block is still part of the free heap -space. */ -static size_t xBlockAllocatedBit = 0; +/* Keeps track of the number of calls to allocate and free memory as well as the + * number of free bytes remaining, but says nothing about fragmentation. */ +PRIVILEGED_DATA static size_t xFreeBytesRemaining = (size_t)0U; +PRIVILEGED_DATA static size_t xMinimumEverFreeBytesRemaining = (size_t)0U; +PRIVILEGED_DATA static size_t xNumberOfSuccessfulAllocations = (size_t)0U; +PRIVILEGED_DATA static size_t xNumberOfSuccessfulFrees = (size_t)0U; /* Furi heap extension */ #include @@ -175,7 +240,7 @@ size_t memmgr_heap_get_thread_memory(FuriThreadId thread_id) { puc -= xHeapStructSize; BlockLink_t* pxLink = (void*)puc; - if((pxLink->xBlockSize & xBlockAllocatedBit) != 0 && + if((pxLink->xBlockSize & heapBLOCK_ALLOCATED_BITMASK) && pxLink->pxNextFreeBlock == NULL) { leftovers += data->value; } @@ -220,20 +285,9 @@ static inline void traceFREE(void* pointer, size_t size) { } size_t memmgr_heap_get_max_free_block(void) { - size_t max_free_size = 0; - BlockLink_t* pxBlock; - vTaskSuspendAll(); - - pxBlock = xStart.pxNextFreeBlock; - while(pxBlock->pxNextFreeBlock != NULL) { - if(pxBlock->xBlockSize > max_free_size) { - max_free_size = pxBlock->xBlockSize; - } - pxBlock = pxBlock->pxNextFreeBlock; - } - - xTaskResumeAll(); - return max_free_size; + HeapStats_t heap_stats; + vPortGetHeapStats(&heap_stats); + return heap_stats.xSizeOfLargestFreeBlockInBytes; } void memmgr_heap_printf_free_blocks(void) { @@ -250,186 +304,115 @@ void memmgr_heap_printf_free_blocks(void) { //xTaskResumeAll(); } -#ifdef HEAP_PRINT_DEBUG -char* ultoa(unsigned long num, char* str, int radix) { - char temp[33]; // at radix 2 the string is at most 32 + 1 null long. - int temp_loc = 0; - int digit; - int str_loc = 0; - - //construct a backward string of the number. - do { - digit = (unsigned long)num % ((unsigned long)radix); - if(digit < 10) - temp[temp_loc++] = digit + '0'; - else - temp[temp_loc++] = digit - 10 + 'A'; - num = ((unsigned long)num) / ((unsigned long)radix); - } while((unsigned long)num > 0); - - temp_loc--; - - //now reverse the string. - while(temp_loc >= 0) { // while there are still chars - str[str_loc++] = temp[temp_loc--]; - } - str[str_loc] = 0; // add null termination. - - return str; -} - -static void print_heap_init(void) { - char tmp_str[33]; - size_t heap_start = (size_t)&__heap_start__; - size_t heap_end = (size_t)&__heap_end__; - - // {PHStart|heap_start|heap_end} - FURI_CRITICAL_ENTER(); - furi_log_puts("{PHStart|"); - ultoa(heap_start, tmp_str, 16); - furi_log_puts(tmp_str); - furi_log_puts("|"); - ultoa(heap_end, tmp_str, 16); - furi_log_puts(tmp_str); - furi_log_puts("}\r\n"); - FURI_CRITICAL_EXIT(); -} - -static void print_heap_malloc(void* ptr, size_t size) { - char tmp_str[33]; - const char* name = furi_thread_get_name(furi_thread_get_current_id()); - if(!name) { - name = ""; - } - - // {thread name|m|address|size} - FURI_CRITICAL_ENTER(); - furi_log_puts("{"); - furi_log_puts(name); - furi_log_puts("|m|0x"); - ultoa((unsigned long)ptr, tmp_str, 16); - furi_log_puts(tmp_str); - furi_log_puts("|"); - utoa(size, tmp_str, 10); - furi_log_puts(tmp_str); - furi_log_puts("}\r\n"); - FURI_CRITICAL_EXIT(); -} - -static void print_heap_free(void* ptr) { - char tmp_str[33]; - const char* name = furi_thread_get_name(furi_thread_get_current_id()); - if(!name) { - name = ""; - } - - // {thread name|f|address} - FURI_CRITICAL_ENTER(); - furi_log_puts("{"); - furi_log_puts(name); - furi_log_puts("|f|0x"); - ultoa((unsigned long)ptr, tmp_str, 16); - furi_log_puts(tmp_str); - furi_log_puts("}\r\n"); - FURI_CRITICAL_EXIT(); -} -#endif /*-----------------------------------------------------------*/ void* pvPortMalloc(size_t xWantedSize) { - BlockLink_t *pxBlock, *pxPreviousBlock, *pxNewBlockLink; + BlockLink_t* pxBlock; + BlockLink_t* pxPreviousBlock; + BlockLink_t* pxNewBlockLink; void* pvReturn = NULL; - size_t to_wipe = xWantedSize; + size_t xToWipe = xWantedSize; + size_t xAdditionalRequiredSize; + size_t xAllocatedBlockSize = 0; if(FURI_IS_IRQ_MODE()) { furi_crash("memmgt in ISR"); } -#ifdef HEAP_PRINT_DEBUG - BlockLink_t* print_heap_block = NULL; -#endif + if(xWantedSize > 0) { + /* The wanted size must be increased so it can contain a BlockLink_t + * structure in addition to the requested amount of bytes. */ + if(heapADD_WILL_OVERFLOW(xWantedSize, xHeapStructSize) == 0) { + xWantedSize += xHeapStructSize; - /* If this is the first call to malloc then the heap will require - initialisation to setup the list of free blocks. */ - if(pxEnd == NULL) { -#ifdef HEAP_PRINT_DEBUG - print_heap_init(); -#endif + /* Ensure that blocks are always aligned to the required number + * of bytes. */ + if((xWantedSize & portBYTE_ALIGNMENT_MASK) != 0x00) { + /* Byte alignment required. */ + xAdditionalRequiredSize = + portBYTE_ALIGNMENT - (xWantedSize & portBYTE_ALIGNMENT_MASK); - vTaskSuspendAll(); - { - prvHeapInit(); - memmgr_heap_init(); + if(heapADD_WILL_OVERFLOW(xWantedSize, xAdditionalRequiredSize) == 0) { + xWantedSize += xAdditionalRequiredSize; + } else { + xWantedSize = 0; + } + } else { + mtCOVERAGE_TEST_MARKER(); + } + } else { + xWantedSize = 0; } - (void)xTaskResumeAll(); } else { mtCOVERAGE_TEST_MARKER(); } vTaskSuspendAll(); { - /* Check the requested block size is not so large that the top bit is - set. The top bit of the block size member of the BlockLink_t structure - is used to determine who owns the block - the application or the - kernel, so it must be free. */ - if((xWantedSize & xBlockAllocatedBit) == 0) { - /* The wanted size is increased so it can contain a BlockLink_t - structure in addition to the requested amount of bytes. */ - if(xWantedSize > 0) { - xWantedSize += xHeapStructSize; - - /* Ensure that blocks are always aligned to the required number - of bytes. */ - if((xWantedSize & portBYTE_ALIGNMENT_MASK) != 0x00) { - /* Byte alignment required. */ - xWantedSize += (portBYTE_ALIGNMENT - (xWantedSize & portBYTE_ALIGNMENT_MASK)); - configASSERT((xWantedSize & portBYTE_ALIGNMENT_MASK) == 0); - } else { - mtCOVERAGE_TEST_MARKER(); - } - } else { - mtCOVERAGE_TEST_MARKER(); - } + /* If this is the first call to malloc then the heap will require + * initialisation to setup the list of free blocks. */ + if(pxEnd == NULL) { + prvHeapInit(); + memmgr_heap_init(); + } else { + mtCOVERAGE_TEST_MARKER(); + } + /* Check the block size we are trying to allocate is not so large that the + * top bit is set. The top bit of the block size member of the BlockLink_t + * structure is used to determine who owns the block - the application or + * the kernel, so it must be free. */ + if(heapBLOCK_SIZE_IS_VALID(xWantedSize) != 0) { if((xWantedSize > 0) && (xWantedSize <= xFreeBytesRemaining)) { /* Traverse the list from the start (lowest address) block until - one of adequate size is found. */ + * one of adequate size is found. */ pxPreviousBlock = &xStart; - pxBlock = xStart.pxNextFreeBlock; - while((pxBlock->xBlockSize < xWantedSize) && (pxBlock->pxNextFreeBlock != NULL)) { + pxBlock = heapPROTECT_BLOCK_POINTER(xStart.pxNextFreeBlock); + heapVALIDATE_BLOCK_POINTER(pxBlock); + + while((pxBlock->xBlockSize < xWantedSize) && + (pxBlock->pxNextFreeBlock != heapPROTECT_BLOCK_POINTER(NULL))) { pxPreviousBlock = pxBlock; - pxBlock = pxBlock->pxNextFreeBlock; + pxBlock = heapPROTECT_BLOCK_POINTER(pxBlock->pxNextFreeBlock); + heapVALIDATE_BLOCK_POINTER(pxBlock); } /* If the end marker was reached then a block of adequate size - was not found. */ + * was not found. */ if(pxBlock != pxEnd) { /* Return the memory space pointed to - jumping over the - BlockLink_t structure at its start. */ - pvReturn = - (void*)(((uint8_t*)pxPreviousBlock->pxNextFreeBlock) + xHeapStructSize); + * BlockLink_t structure at its start. */ + pvReturn = (void*)(((uint8_t*)heapPROTECT_BLOCK_POINTER( + pxPreviousBlock->pxNextFreeBlock)) + + xHeapStructSize); + heapVALIDATE_BLOCK_POINTER(pvReturn); /* This block is being returned for use so must be taken out - of the list of free blocks. */ + * of the list of free blocks. */ pxPreviousBlock->pxNextFreeBlock = pxBlock->pxNextFreeBlock; /* If the block is larger than required it can be split into - two. */ + * two. */ + configASSERT( + heapSUBTRACT_WILL_UNDERFLOW(pxBlock->xBlockSize, xWantedSize) == 0); + if((pxBlock->xBlockSize - xWantedSize) > heapMINIMUM_BLOCK_SIZE) { /* This block is to be split into two. Create a new - block following the number of bytes requested. The void - cast is used to prevent byte alignment warnings from the - compiler. */ + * block following the number of bytes requested. The void + * cast is used to prevent byte alignment warnings from the + * compiler. */ pxNewBlockLink = (void*)(((uint8_t*)pxBlock) + xWantedSize); configASSERT((((size_t)pxNewBlockLink) & portBYTE_ALIGNMENT_MASK) == 0); /* Calculate the sizes of two blocks split from the - single block. */ + * single block. */ pxNewBlockLink->xBlockSize = pxBlock->xBlockSize - xWantedSize; pxBlock->xBlockSize = xWantedSize; /* Insert the new block into the list of free blocks. */ - prvInsertBlockIntoFreeList(pxNewBlockLink); + pxNewBlockLink->pxNextFreeBlock = pxPreviousBlock->pxNextFreeBlock; + pxPreviousBlock->pxNextFreeBlock = + heapPROTECT_BLOCK_POINTER(pxNewBlockLink); } else { mtCOVERAGE_TEST_MARKER(); } @@ -442,14 +425,13 @@ void* pvPortMalloc(size_t xWantedSize) { mtCOVERAGE_TEST_MARKER(); } - /* The block is being returned - it is allocated and owned - by the application and has no "next" block. */ - pxBlock->xBlockSize |= xBlockAllocatedBit; - pxBlock->pxNextFreeBlock = NULL; + xAllocatedBlockSize = pxBlock->xBlockSize; -#ifdef HEAP_PRINT_DEBUG - print_heap_block = pxBlock; -#endif + /* The block is being returned - it is allocated and owned + * by the application and has no "next" block. */ + heapALLOCATE_BLOCK(pxBlock); + pxBlock->pxNextFreeBlock = heapPROTECT_BLOCK_POINTER(NULL); + xNumberOfSuccessfulAllocations++; } else { mtCOVERAGE_TEST_MARKER(); } @@ -460,29 +442,27 @@ void* pvPortMalloc(size_t xWantedSize) { mtCOVERAGE_TEST_MARKER(); } - traceMALLOC(pvReturn, xWantedSize); + traceMALLOC(pvReturn, xAllocatedBlockSize); + + /* Prevent compiler warnings when trace macros are not used. */ + (void)xAllocatedBlockSize; } (void)xTaskResumeAll(); -#ifdef HEAP_PRINT_DEBUG - print_heap_malloc(print_heap_block, print_heap_block->xBlockSize & ~xBlockAllocatedBit); -#endif - #if(configUSE_MALLOC_FAILED_HOOK == 1) { if(pvReturn == NULL) { - extern void vApplicationMallocFailedHook(void); vApplicationMallocFailedHook(); } else { mtCOVERAGE_TEST_MARKER(); } } -#endif +#endif /* if ( configUSE_MALLOC_FAILED_HOOK == 1 ) */ configASSERT((((size_t)pvReturn) & (size_t)portBYTE_ALIGNMENT_MASK) == 0); furi_check(pvReturn, xWantedSize ? "out of memory" : "malloc(0)"); - pvReturn = memset(pvReturn, 0, to_wipe); + pvReturn = memset(pvReturn, 0, xToWipe); return pvReturn; } /*-----------------------------------------------------------*/ @@ -497,24 +477,30 @@ void vPortFree(void* pv) { if(pv != NULL) { /* The memory being freed will have an BlockLink_t structure immediately - before it. */ + * before it. */ puc -= xHeapStructSize; /* This casting is to keep the compiler from issuing warnings. */ pxLink = (void*)puc; - /* Check the block is actually allocated. */ - configASSERT((pxLink->xBlockSize & xBlockAllocatedBit) != 0); - configASSERT(pxLink->pxNextFreeBlock == NULL); + heapVALIDATE_BLOCK_POINTER(pxLink); + configASSERT(heapBLOCK_IS_ALLOCATED(pxLink) != 0); + configASSERT(pxLink->pxNextFreeBlock == heapPROTECT_BLOCK_POINTER(NULL)); - if((pxLink->xBlockSize & xBlockAllocatedBit) != 0) { - if(pxLink->pxNextFreeBlock == NULL) { + if(heapBLOCK_IS_ALLOCATED(pxLink) != 0) { + if(pxLink->pxNextFreeBlock == heapPROTECT_BLOCK_POINTER(NULL)) { /* The block is being returned to the heap - it is no longer - allocated. */ - pxLink->xBlockSize &= ~xBlockAllocatedBit; - -#ifdef HEAP_PRINT_DEBUG - print_heap_free(pxLink); + * allocated. */ + heapFREE_BLOCK(pxLink); +#if(configHEAP_CLEAR_MEMORY_ON_FREE == 1) + { + /* Check for underflow as this can occur if xBlockSize is + * overwritten in a heap block. */ + if(heapSUBTRACT_WILL_UNDERFLOW(pxLink->xBlockSize, xHeapStructSize) == 0) { + (void)memset( + puc + xHeapStructSize, 0, pxLink->xBlockSize - xHeapStructSize); + } + } #endif vTaskSuspendAll(); @@ -527,8 +513,8 @@ void vPortFree(void* pv) { /* Add this block to the list of free blocks. */ xFreeBytesRemaining += pxLink->xBlockSize; traceFREE(pv, pxLink->xBlockSize); - memset(pv, 0, pxLink->xBlockSize - xHeapStructSize); - prvInsertBlockIntoFreeList((BlockLink_t*)pxLink); + prvInsertBlockIntoFreeList(((BlockLink_t*)pxLink)); + xNumberOfSuccessfulFrees++; } (void)xTaskResumeAll(); } else { @@ -537,19 +523,10 @@ void vPortFree(void* pv) { } else { mtCOVERAGE_TEST_MARKER(); } - } else { -#ifdef HEAP_PRINT_DEBUG - print_heap_free(pv); -#endif } } /*-----------------------------------------------------------*/ -size_t xPortGetTotalHeapSize(void) { - return (size_t)&__heap_end__ - (size_t)&__heap_start__; -} -/*-----------------------------------------------------------*/ - size_t xPortGetFreeHeapSize(void) { return xFreeBytesRemaining; } @@ -560,71 +537,98 @@ size_t xPortGetMinimumEverFreeHeapSize(void) { } /*-----------------------------------------------------------*/ +void xPortResetHeapMinimumEverFreeHeapSize(void) { + xMinimumEverFreeBytesRemaining = xFreeBytesRemaining; +} +/*-----------------------------------------------------------*/ + void vPortInitialiseBlocks(void) { /* This just exists to keep the linker quiet. */ } /*-----------------------------------------------------------*/ -static void prvHeapInit(void) { - BlockLink_t* pxFirstFreeBlock; - uint8_t* pucAlignedHeap; - size_t uxAddress; - size_t xTotalHeapSize = (size_t)&__heap_end__ - (size_t)&__heap_start__; +void* pvPortCalloc(size_t xNum, size_t xSize) { + void* pv = NULL; - /* Ensure the heap starts on a correctly aligned boundary. */ - uxAddress = (size_t)ucHeap; + if(heapMULTIPLY_WILL_OVERFLOW(xNum, xSize) == 0) { + pv = pvPortMalloc(xNum * xSize); - if((uxAddress & portBYTE_ALIGNMENT_MASK) != 0) { - uxAddress += (portBYTE_ALIGNMENT - 1); - uxAddress &= ~((size_t)portBYTE_ALIGNMENT_MASK); - xTotalHeapSize -= uxAddress - (size_t)ucHeap; + if(pv != NULL) { + (void)memset(pv, 0, xNum * xSize); + } } - pucAlignedHeap = (uint8_t*)uxAddress; + return pv; +} +/*-----------------------------------------------------------*/ + +static void prvHeapInit(void) /* PRIVILEGED_FUNCTION */ +{ + BlockLink_t* pxFirstFreeBlock; + portPOINTER_SIZE_TYPE uxStartAddress, uxEndAddress; + size_t xTotalHeapSize = configTOTAL_HEAP_SIZE; + + /* Ensure the heap starts on a correctly aligned boundary. */ + uxStartAddress = (portPOINTER_SIZE_TYPE)ucHeap; + + if((uxStartAddress & portBYTE_ALIGNMENT_MASK) != 0) { + uxStartAddress += (portBYTE_ALIGNMENT - 1); + uxStartAddress &= ~((portPOINTER_SIZE_TYPE)portBYTE_ALIGNMENT_MASK); + xTotalHeapSize -= (size_t)(uxStartAddress - (portPOINTER_SIZE_TYPE)ucHeap); + } + +#if(configENABLE_HEAP_PROTECTOR == 1) + { vApplicationGetRandomHeapCanary(&(xHeapCanary)); } +#endif /* xStart is used to hold a pointer to the first item in the list of free - blocks. The void cast is used to prevent compiler warnings. */ - xStart.pxNextFreeBlock = (void*)pucAlignedHeap; + * blocks. The void cast is used to prevent compiler warnings. */ + xStart.pxNextFreeBlock = (void*)heapPROTECT_BLOCK_POINTER(uxStartAddress); xStart.xBlockSize = (size_t)0; /* pxEnd is used to mark the end of the list of free blocks and is inserted - at the end of the heap space. */ - uxAddress = ((size_t)pucAlignedHeap) + xTotalHeapSize; - uxAddress -= xHeapStructSize; - uxAddress &= ~((size_t)portBYTE_ALIGNMENT_MASK); - pxEnd = (void*)uxAddress; + * at the end of the heap space. */ + uxEndAddress = uxStartAddress + (portPOINTER_SIZE_TYPE)xTotalHeapSize; + uxEndAddress -= (portPOINTER_SIZE_TYPE)xHeapStructSize; + uxEndAddress &= ~((portPOINTER_SIZE_TYPE)portBYTE_ALIGNMENT_MASK); + pxEnd = (BlockLink_t*)uxEndAddress; pxEnd->xBlockSize = 0; - pxEnd->pxNextFreeBlock = NULL; + pxEnd->pxNextFreeBlock = heapPROTECT_BLOCK_POINTER(NULL); /* To start with there is a single free block that is sized to take up the - entire heap space, minus the space taken by pxEnd. */ - pxFirstFreeBlock = (void*)pucAlignedHeap; - pxFirstFreeBlock->xBlockSize = uxAddress - (size_t)pxFirstFreeBlock; - pxFirstFreeBlock->pxNextFreeBlock = pxEnd; + * entire heap space, minus the space taken by pxEnd. */ + pxFirstFreeBlock = (BlockLink_t*)uxStartAddress; + pxFirstFreeBlock->xBlockSize = + (size_t)(uxEndAddress - (portPOINTER_SIZE_TYPE)pxFirstFreeBlock); + pxFirstFreeBlock->pxNextFreeBlock = heapPROTECT_BLOCK_POINTER(pxEnd); /* Only one block exists - and it covers the entire usable heap space. */ xMinimumEverFreeBytesRemaining = pxFirstFreeBlock->xBlockSize; xFreeBytesRemaining = pxFirstFreeBlock->xBlockSize; - - /* Work out the position of the top bit in a size_t variable. */ - xBlockAllocatedBit = ((size_t)1) << ((sizeof(size_t) * heapBITS_PER_BYTE) - 1); } /*-----------------------------------------------------------*/ -static void prvInsertBlockIntoFreeList(BlockLink_t* pxBlockToInsert) { +static void prvInsertBlockIntoFreeList(BlockLink_t* pxBlockToInsert) /* PRIVILEGED_FUNCTION */ +{ BlockLink_t* pxIterator; uint8_t* puc; /* Iterate through the list until a block is found that has a higher address - than the block being inserted. */ - for(pxIterator = &xStart; pxIterator->pxNextFreeBlock < pxBlockToInsert; - pxIterator = pxIterator->pxNextFreeBlock) { + * than the block being inserted. */ + for(pxIterator = &xStart; + heapPROTECT_BLOCK_POINTER(pxIterator->pxNextFreeBlock) < pxBlockToInsert; + pxIterator = heapPROTECT_BLOCK_POINTER(pxIterator->pxNextFreeBlock)) { /* Nothing to do here, just iterate to the right position. */ } + if(pxIterator != &xStart) { + heapVALIDATE_BLOCK_POINTER(pxIterator); + } + /* Do the block being inserted, and the block it is being inserted after - make a contiguous block of memory? */ + * make a contiguous block of memory? */ puc = (uint8_t*)pxIterator; + if((puc + pxIterator->xBlockSize) == (uint8_t*)pxBlockToInsert) { pxIterator->xBlockSize += pxBlockToInsert->xBlockSize; pxBlockToInsert = pxIterator; @@ -633,27 +637,98 @@ static void prvInsertBlockIntoFreeList(BlockLink_t* pxBlockToInsert) { } /* Do the block being inserted, and the block it is being inserted before - make a contiguous block of memory? */ + * make a contiguous block of memory? */ puc = (uint8_t*)pxBlockToInsert; - if((puc + pxBlockToInsert->xBlockSize) == (uint8_t*)pxIterator->pxNextFreeBlock) { - if(pxIterator->pxNextFreeBlock != pxEnd) { + + if((puc + pxBlockToInsert->xBlockSize) == + (uint8_t*)heapPROTECT_BLOCK_POINTER(pxIterator->pxNextFreeBlock)) { + if(heapPROTECT_BLOCK_POINTER(pxIterator->pxNextFreeBlock) != pxEnd) { /* Form one big block from the two blocks. */ - pxBlockToInsert->xBlockSize += pxIterator->pxNextFreeBlock->xBlockSize; - pxBlockToInsert->pxNextFreeBlock = pxIterator->pxNextFreeBlock->pxNextFreeBlock; + pxBlockToInsert->xBlockSize += + heapPROTECT_BLOCK_POINTER(pxIterator->pxNextFreeBlock)->xBlockSize; + pxBlockToInsert->pxNextFreeBlock = + heapPROTECT_BLOCK_POINTER(pxIterator->pxNextFreeBlock)->pxNextFreeBlock; } else { - pxBlockToInsert->pxNextFreeBlock = pxEnd; + pxBlockToInsert->pxNextFreeBlock = heapPROTECT_BLOCK_POINTER(pxEnd); } } else { pxBlockToInsert->pxNextFreeBlock = pxIterator->pxNextFreeBlock; } - /* If the block being inserted plugged a gab, so was merged with the block - before and the block after, then it's pxNextFreeBlock pointer will have - already been set, and should not be set here as that would make it point - to itself. */ + /* If the block being inserted plugged a gap, so was merged with the block + * before and the block after, then it's pxNextFreeBlock pointer will have + * already been set, and should not be set here as that would make it point + * to itself. */ if(pxIterator != pxBlockToInsert) { - pxIterator->pxNextFreeBlock = pxBlockToInsert; + pxIterator->pxNextFreeBlock = heapPROTECT_BLOCK_POINTER(pxBlockToInsert); } else { mtCOVERAGE_TEST_MARKER(); } } +/*-----------------------------------------------------------*/ + +void vPortGetHeapStats(HeapStats_t* pxHeapStats) { + BlockLink_t* pxBlock; + size_t + xBlocks = 0, + xMaxSize = 0, + xMinSize = + portMAX_DELAY; /* portMAX_DELAY used as a portable way of getting the maximum value. */ + + vTaskSuspendAll(); + { + pxBlock = heapPROTECT_BLOCK_POINTER(xStart.pxNextFreeBlock); + + /* pxBlock will be NULL if the heap has not been initialised. The heap + * is initialised automatically when the first allocation is made. */ + if(pxBlock != NULL) { + while(pxBlock != pxEnd) { + /* Increment the number of blocks and record the largest block seen + * so far. */ + xBlocks++; + + if(pxBlock->xBlockSize > xMaxSize) { + xMaxSize = pxBlock->xBlockSize; + } + + if(pxBlock->xBlockSize < xMinSize) { + xMinSize = pxBlock->xBlockSize; + } + + /* Move to the next block in the chain until the last block is + * reached. */ + pxBlock = heapPROTECT_BLOCK_POINTER(pxBlock->pxNextFreeBlock); + } + } + } + (void)xTaskResumeAll(); + + pxHeapStats->xSizeOfLargestFreeBlockInBytes = xMaxSize; + pxHeapStats->xSizeOfSmallestFreeBlockInBytes = xMinSize; + pxHeapStats->xNumberOfFreeBlocks = xBlocks; + + taskENTER_CRITICAL(); + { + pxHeapStats->xAvailableHeapSpaceInBytes = xFreeBytesRemaining; + pxHeapStats->xNumberOfSuccessfulAllocations = xNumberOfSuccessfulAllocations; + pxHeapStats->xNumberOfSuccessfulFrees = xNumberOfSuccessfulFrees; + pxHeapStats->xMinimumEverFreeBytesRemaining = xMinimumEverFreeBytesRemaining; + } + taskEXIT_CRITICAL(); +} +/*-----------------------------------------------------------*/ + +/* + * Reset the state in this file. This state is normally initialized at start up. + * This function must be called by the application before restarting the + * scheduler. + */ +void vPortHeapResetState(void) { + pxEnd = NULL; + + xFreeBytesRemaining = (size_t)0U; + xMinimumEverFreeBytesRemaining = (size_t)0U; + xNumberOfSuccessfulAllocations = (size_t)0U; + xNumberOfSuccessfulFrees = (size_t)0U; +} +/*-----------------------------------------------------------*/ diff --git a/furi/flipper.c b/furi/flipper.c index 6d6215a9d..bafe2ed0f 100644 --- a/furi/flipper.c +++ b/furi/flipper.c @@ -9,6 +9,8 @@ #define TAG "Flipper" +#define HEAP_CANARY_VALUE 0x8BADF00D + static void flipper_print_version(const char* target, const Version* version) { if(version) { FURI_LOG_I( @@ -67,3 +69,7 @@ void vApplicationGetTimerTaskMemory( *stack_ptr = memmgr_alloc_from_pool(sizeof(StackType_t) * configTIMER_TASK_STACK_DEPTH); *stack_size = configTIMER_TASK_STACK_DEPTH; } + +void vApplicationGetRandomHeapCanary(portPOINTER_SIZE_TYPE* pxHeapCanary) { + *pxHeapCanary = HEAP_CANARY_VALUE; +} diff --git a/targets/f7/inc/FreeRTOSConfig.h b/targets/f7/inc/FreeRTOSConfig.h index 357019ea2..8d34925ec 100644 --- a/targets/f7/inc/FreeRTOSConfig.h +++ b/targets/f7/inc/FreeRTOSConfig.h @@ -11,13 +11,16 @@ #endif /* CMSIS_device_header */ #include CMSIS_device_header +#include #define configENABLE_FPU 1 #define configENABLE_MPU 0 #define configUSE_PREEMPTION 1 #define configSUPPORT_STATIC_ALLOCATION 1 -#define configSUPPORT_DYNAMIC_ALLOCATION 0 +#define configSUPPORT_DYNAMIC_ALLOCATION 1 +#define configENABLE_HEAP_PROTECTOR 1 +#define configHEAP_CLEAR_MEMORY_ON_FREE 1 #define configUSE_MALLOC_FAILED_HOOK 0 #define configUSE_IDLE_HOOK 0 #define configUSE_TICK_HOOK 0 @@ -30,7 +33,7 @@ #define configUSE_POSIX_ERRNO 1 /* Heap size determined automatically by linker */ -// #define configTOTAL_HEAP_SIZE ((size_t)0) +#define configTOTAL_HEAP_SIZE ((uint32_t) & __heap_end__ - (uint32_t) & __heap_start__) #define configMAX_TASK_NAME_LEN (32) #define configGENERATE_RUN_TIME_STATS 1 @@ -146,7 +149,7 @@ standard names. */ /* Normal assert() semantics without relying on the provision of an assert.h header file. */ -#ifdef DEBUG +#ifdef FURI_DEBUG #define configASSERT(x) \ if((x) == 0) { \ furi_crash("FreeRTOS Assert"); \ diff --git a/targets/f7/inc/stm32wb55_linker.h b/targets/f7/inc/stm32wb55_linker.h index 4b56a11be..e978dff35 100644 --- a/targets/f7/inc/stm32wb55_linker.h +++ b/targets/f7/inc/stm32wb55_linker.h @@ -9,25 +9,28 @@ #ifdef __cplusplus extern "C" { +typedef const char linker_symbol_t; +#else +typedef const void linker_symbol_t; #endif -extern const void _stack_end; /**< end of stack */ -extern const void _stack_size; /**< stack size */ +extern linker_symbol_t _stack_end; /**< end of stack */ +extern linker_symbol_t _stack_size; /**< stack size */ -extern const void _sidata; /**< data initial value start */ -extern const void _sdata; /**< data start */ -extern const void _edata; /**< data end */ +extern linker_symbol_t _sidata; /**< data initial value start */ +extern linker_symbol_t _sdata; /**< data start */ +extern linker_symbol_t _edata; /**< data end */ -extern const void _sbss; /**< bss start */ -extern const void _ebss; /**< bss end */ +extern linker_symbol_t _sbss; /**< bss start */ +extern linker_symbol_t _ebss; /**< bss end */ -extern const void _sMB_MEM2; /**< RAM2a start */ -extern const void _eMB_MEM2; /**< RAM2a end */ +extern linker_symbol_t _sMB_MEM2; /**< RAM2a start */ +extern linker_symbol_t _eMB_MEM2; /**< RAM2a end */ -extern const void __heap_start__; /**< RAM1 Heap start */ -extern const void __heap_end__; /**< RAM1 Heap end */ +extern linker_symbol_t __heap_start__; /**< RAM1 Heap start */ +extern linker_symbol_t __heap_end__; /**< RAM1 Heap end */ -extern const void __free_flash_start__; /**< Free Flash space start */ +extern linker_symbol_t __free_flash_start__; /**< Free Flash space start */ #ifdef __cplusplus } From f94800b22027e27588307216e8fe69e5cd8ab8e6 Mon Sep 17 00:00:00 2001 From: m-xim <170838360+m-xim@users.noreply.github.com> Date: Fri, 21 Feb 2025 04:39:30 +0300 Subject: [PATCH 065/268] docs: add icons --- ReadMe.md | 141 +++++++++++++++++++++++++++++++----------------------- 1 file changed, 82 insertions(+), 59 deletions(-) diff --git a/ReadMe.md b/ReadMe.md index 2d3122677..a1c46d6a7 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -54,7 +54,7 @@ Before getting started: > Built automatically from dev branch - Web site: https://dev.unleashedflip.com -- Telegram Telegram: t.me/kotnehleb +- Telegram Telegram: t.me/kotnehleb ## 🆕 What's New @@ -87,7 +87,7 @@ Before getting started: >- **Custom buttons for Keeloq / Alutech AT4N / Nice Flor S / Somfy Telis / Security+ 2.0 / CAME Atomo** - now you can use arrow buttons to send signal with different button code > - `Add manually` menu extended with new protocols > - FAAC SLH, BFT Mitto / Somfy Telis / Nice Flor S / CAME Atomo, etc. manual creation with programming new remote into receiver (use button 0xF for BFT Mitto, 0x8 (Prog) on Somfy Telis, (right arrow button for other protocols)) -> - Debug mode counter increase settings (+1 -> +5, +10, default: +1) +> - Debug mode counter increase settings (+1 → +5, +10, default: +1) > - Debug PIN output settings for protocol development > @@ -107,9 +107,9 @@ Before getting started: >
> > - Recompiled IR TV Universal Remote for ALL buttons -> - Universal remotes for Projectors, Fans, A/Cs and Audio(soundbars, etc.) -> Also always updated and verified by our team -> - Infrared -> `RCA` Protocol -> - Infrared -> External IR modules support (with autodetect by OFW) +> - Universal remotes for Projectors, Fans, A/Cs and Audio(soundbars, etc.) → Also always updated and verified by our team +> - Infrared → `RCA` Protocol +> - Infrared → External IR modules support (with autodetect by OFW) > >
@@ -120,7 +120,7 @@ Before getting started: > - Add DEZ 8 display form for EM4100 (by @korden32) > - Extra Mifare Classic keys in system dict > - EMV Protocol + Public data parser (by @Leptopt1los and @wosk) -> - NFC `Add manually` -> Mifare Classic with custom UID +> - NFC `Add manually` → Mifare Classic with custom UID > - NFC parsers: Umarsh, Zolotaya Korona, Kazan, Metromoney, Moscow Social Card, Troika (reworked) and [many others](https://github.com/DarkFlippers/unleashed-firmware/tree/dev/applications/main/nfc/plugins/supported_cards) (by @Leptopt1los and @assasinfil) >
@@ -128,16 +128,16 @@ Before getting started: > Quality of Life & Other Features >
> -> - Customizable Flipper name **Update! Now can be changed in Settings->Desktop** (by @xMasterX and @Willy-JL) -> - Text Input UI element -> Cursor feature (by @Willy-JL) -> - Byte Input Mini editor -> **Press UP** multiple times until the nibble editor appears (by @gid9798) -> - Clock on Desktop -> `Settings -> Desktop -> Show Clock` (by @gid9798) +> - Customizable Flipper name **Update! Now can be changed in Settings → Desktop** (by @xMasterX and @Willy-JL) +> - Text Input UI element → Cursor feature (by @Willy-JL) +> - Byte Input Mini editor → **Press UP** multiple times until the nibble editor appears (by @gid9798) +> - Clock on Desktop `Settings -> Desktop -> Show Clock` (by @gid9798) > - Battery percentage display with different styles `Settings -> Desktop -> Battery View` -> - More games in Dummy Mode -> click or hold any of arrow buttons -> - Lock device with pin (or regular lock if pin not set) by holding UP button on main screen [(by an4tur0r)](https://github.com/DarkFlippers/unleashed-firmware/pull/107) -> - **BadKB** plugin [(by Willy-JL, ClaraCrazy, XFW contributors)](https://github.com/Flipper-XFW/Xtreme-Firmware/tree/dev/applications/main/bad_kb) - (See in Applications->Tools) - (aka BadUSB via Bluetooth) -> - BadUSB -> Keyboard layouts [(by rien > dummy-decoy)](https://github.com/dummy-decoy/flipperzero-firmware/tree/dummy_decoy/bad_usb_keyboard_layout) -> - Custom community plugins and games added + all known working apps can be downloaded in extra pack in every release +> - More games in Dummy Mode → click or hold any of arrow buttons +> - Lock device with pin (or regular lock if pin not set) by holding UP button on main screen [(by an4tur0r)](https://github.com/DarkFlippers/unleashed-firmware/pull/107) +> - **BadKB** plugin [(by Willy-JL, ClaraCrazy, XFW contributors)](https://github.com/Flipper-XFW/Xtreme-Firmware/tree/dev/applications/main/bad_kb) - (See in Applications → Tools) - (aka BadUSB via Bluetooth) +> - BadUSB → Keyboard layouts [(by rien > dummy-decoy)](https://github.com/dummy-decoy/flipperzero-firmware/tree/dummy_decoy/bad_usb_keyboard_layout) +> - Custom community plugins and games added + all known working apps can be downloaded in extra pack in every release > - Other small fixes and changes throughout > - See other changes in readme below > @@ -165,8 +165,8 @@ Thanks to Official team (to their SubGHz Developer, Skorp) for implementing supp
- Marantec24 (static 24 bit) with add manually support -- GangQi (static 34 bit) with button parsing and add manually support (thanks to @mishamyte for captures and testing, thanks @Skorpionm for help) -- Hollarm (static 42 bit) with button parsing and add manually support (thanks to @mishamyte for captures, thanks @Skorpionm for help) +- GangQi (static 34 bit) with button parsing and add manually support (thanks to @mishamyte for captures and testing, thanks @Skorpionm for help) +- Hollarm (static 42 bit) with button parsing and add manually support (thanks to @mishamyte for captures, thanks @Skorpionm for help) - Hay21 (dynamic 21 bit) with button parsing - Nero Radio 57bit (+ 56bit support) - CAME 12bit/24bit encoder fixes (Fixes are now merged in OFW) @@ -177,10 +177,10 @@ Thanks to Official team (to their SubGHz Developer, Skorp) for implementing supp Protocols support made by Skorp (original implementation) and @xMasterX (current version)
-- CAME Atomo -> Update! check out new [instructions](https://github.com/DarkFlippers/unleashed-firmware/blob/dev/documentation/SubGHzRemoteProg.md) -- Nice Flor S -> How to create new remote - [instructions](https://github.com/DarkFlippers/unleashed-firmware/blob/dev/documentation/SubGHzRemoteProg.md) -- FAAC SLH (Spa) -> Update!!! (Programming mode!) Check out new [instructions](https://github.com/DarkFlippers/unleashed-firmware/blob/dev/documentation/SubGHzRemoteProg.md) -- Keeloq: BFT Mitto -> Update! Check out new [instructions](https://github.com/DarkFlippers/unleashed-firmware/blob/dev/documentation/SubGHzRemoteProg.md) +- CAME Atomo → Update! check out new [instructions](/documentation/SubGHzRemoteProg.md) +- Nice Flor S → How to create new remote - [instructions](/documentation/SubGHzRemoteProg.md) +- FAAC SLH (Spa) → Update!!! (Programming mode!) Check out new [instructions](/documentation/SubGHzRemoteProg.md) +- Keeloq: BFT Mitto → Update! Check out new [instructions](/documentation/SubGHzRemoteProg.md) - Star Line - Security+ v1 & v2 @@ -189,11 +189,11 @@ Thanks to Official team (to their SubGHz Developer, Skorp) for implementing supp Encoders made by @assasinfil and @xMasterX
-- Somfy Telis -> How to create new remote - [instructions](https://github.com/DarkFlippers/unleashed-firmware/blob/dev/documentation/SubGHzRemoteProg.md) +- Somfy Telis → How to create new remote - [instructions](/documentation/SubGHzRemoteProg.md) - Somfy Keytis - KingGates Stylo 4k -- Alutech AT-4N -> How to create new remote - [instructions](https://github.com/DarkFlippers/unleashed-firmware/blob/dev/documentation/SubGHzRemoteProg.md) -- Nice ON2E (Nice One) -> How to create new remote - [instructions](https://github.com/DarkFlippers/unleashed-firmware/blob/dev/documentation/SubGHzRemoteProg.md) +- Alutech AT-4N → How to create new remote - [instructions](/documentation/SubGHzRemoteProg.md) +- Nice ON2E (Nice One) → How to create new remote - [instructions](/documentation/SubGHzRemoteProg.md) @@ -201,32 +201,33 @@ Thanks to Official team (to their SubGHz Developer, Skorp) for implementing supp The majority of this project is developed and maintained by me, @xMasterX. Our team is small and the guys are working on this project as much as they can solely based on the enthusiasm they have for this project and the community. -- @Leptopt1los - NFC, RFID, Plugins, and many other things -- @gid9798 - SubGHz, Plugins, many other things - currently offline :( -- @assasinfil - SubGHz protocols, NFC parsers -- @Svaarich - UI design and animations -- @amec0e - Infrared assets +- `@Leptopt1los` - NFC, RFID, Plugins, and many other things +- `@gid9798` - SubGHz, Plugins, many other things - currently offline :( +- `@assasinfil` - SubGHz protocols, NFC parsers +- `@Svaarich` - UI design and animations +- `@amec0e` - Infrared assets - Community moderators in Telegram, Discord, and Reddit - And of course our GitHub community. Your PRs are a very important part of this firmware and open-source development. The amount of work done on this project is huge and we need your support, no matter how large or small. Even if you just say, "Thank you Unleashed firmware developers!" somewhere. Doing so will help us continue our work and will help drive us to make the firmware better every time. Also, regarding our releases, every build has and always will be free and open-source. There will be no paywall releases or closed-source apps within the firmware. As long as I am working on this project it will never happen. You can support us by using links or addresses below: -|Service|Remark|QR Code|Link/Wallet| -|-|-|-|-| -|**Patreon**||
QR image
|https://patreon.com/mmxdev| -|**Boosty**|patreon alternative|
QR image
|https://boosty.to/mmxdev| -|cloudtips|only RU payments accepted|
QR image
|https://pay.cloudtips.ru/p/7b3e9d65| -|YooMoney|only RU payments accepted|
QR image
|https://yoomoney.ru/fundraise/XA49mgQLPA0.221209| -|USDT|(TRC20)|
QR image
|`TSXcitMSnWXUFqiUfEXrTVpVewXy2cYhrs`| -|ETH|(BSC/ERC20-Tokens)|
QR image
|`0xFebF1bBc8229418FF2408C07AF6Afa49152fEc6a`| -|BTC||
QR image
|`bc1q0np836jk9jwr4dd7p6qv66d04vamtqkxrecck9`| -|SOL|(Solana/Tokens)|
QR image
|`DSgwouAEgu8iP5yr7EHHDqMNYWZxAqXWsTEeqCAXGLj8`| -|DOGE||
QR image
|`D6R6gYgBn5LwTNmPyvAQR6bZ9EtGgFCpvv`| -|LTC||
QR image
|`ltc1q3ex4ejkl0xpx3znwrmth4lyuadr5qgv8tmq8z9`| -|BCH||
QR image
|`qquxfyzntuqufy2dx0hrfr4sndp0tucvky4sw8qyu3`| -|XMR|(Monero)|
QR image
|`41xUz92suUu1u5Mu4qkrcs52gtfpu9rnZRdBpCJ244KRHf6xXSvVFevdf2cnjS7RAeYr5hn9MsEfxKoFDRSctFjG5fv1Mhn`| -|TON||
QR image
|`UQCOqcnYkvzOZUV_9bPE_8oTbOrOF03MnF-VcJyjisTZmsxa`| + +| Service | Remark | QR Code | Link/Wallet | +|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------| +| Patreon **Patreon** | |
QR image
| [patreon.com/mmxdev](https://patreon.com/mmxdev) | +| Boosty **Boosty** | patreon alternative |
QR image
| [boosty.to/mmxdev](https://boosty.to/mmxdev) | +| Cloudtips CloudTips | only RU payments accepted |
QR image
| [pay.cloudtips.ru/p/7b3e9d65](https://pay.cloudtips.ru/p/7b3e9d65) | +| YooMoney YooMoney | only RU payments accepted |
QR image
| [yoomoney.ru/fundraise/XA49mgQLPA0.221209](https://yoomoney.ru/fundraise/XA49mgQLPA0.221209) | +| USDT USDT | TRC20 |
QR image
| `TSXcitMSnWXUFqiUfEXrTVpVewXy2cYhrs` | +| ETH ETH | BSC/ERC20-Tokens |
QR image
| `0xFebF1bBc8229418FF2408C07AF6Afa49152fEc6a` | +| BTC BTC | |
QR image
| `bc1q0np836jk9jwr4dd7p6qv66d04vamtqkxrecck9` | +| SOL SOL | Solana/Tokens |
QR image
| `DSgwouAEgu8iP5yr7EHHDqMNYWZxAqXWsTEeqCAXGLj8` | +| DOGE DOGE | |
QR image
| `D6R6gYgBn5LwTNmPyvAQR6bZ9EtGgFCpvv` | +| LTC LTC | |
QR image
| `ltc1q3ex4ejkl0xpx3znwrmth4lyuadr5qgv8tmq8z9` | +| BCH BCH | |
QR image
| `qquxfyzntuqufy2dx0hrfr4sndp0tucvky4sw8qyu3` | +| XMR XMR | Monero |
QR image
| `41xUz92suUu1u5Mu4qkrcs52gtfpu9rnZRdBpCJ244KRHf6xXSvVFevdf2cnjS7RAeYr5hn9MsEfxKoFDRSctFjG5fv1Mhn` | +| TON TON | |
QR image
| `UQCOqcnYkvzOZUV_9bPE_8oTbOrOF03MnF-VcJyjisTZmsxa` | ## 📱 Community Apps @@ -244,46 +245,46 @@ Enhance your Flipper Zero with apps and plugins created by the community: ## 📁 Where I can find IR, Sub-GHz, ... files, DBs, and other stuff? -- [UberGuidoZ Playground - Large collection of files - Github](https://github.com/UberGuidoZ/Flipper) +- [UberGuidoZ Playground - Large collection of files - Github](https://github.com/UberGuidoZ/Flipper) - [Awesome Flipper Zero - Github](https://github.com/djsime1/awesome-flipperzero) ## 📘 Instructions -### Firmware & main Apps feature +### ![Tools Icon Badge] Firmware & main Apps feature - System: [How to change Flipper name](/documentation/CustomFlipperName.md) - BadUSB: [How to add new keyboard layouts](https://github.com/dummy-decoy/flipperzero_badusb_kl) -- Infrared: [How to make captures to add them into Universal IR remotes](https://github.com/DarkFlippers/unleashed-firmware/blob/dev/documentation/InfraredCaptures.md) +- Infrared: [How to make captures to add them into Universal IR remotes](/documentation/InfraredCaptures.md) -### Sub-GHz +### ![SubGhz Icon Badge] Sub-GHz -- [How to use Flipper as new remote (Nice FlorS, BFT Mitto, Somfy Telis, Aprimatic, AN-Motors, etc..)](https://github.com/DarkFlippers/unleashed-firmware/blob/dev/documentation/SubGHzRemoteProg.md) +- [How to use Flipper as new remote (Nice FlorS, BFT Mitto, Somfy Telis, Aprimatic, AN-Motors, etc..)](/documentation/SubGHzRemoteProg.md) - External Radio: [How to connect CC1101 module](https://github.com/quen0n/flipperzero-ext-cc1101) -- Transmission is blocked? [How to extend Sub-GHz frequency range](https://github.com/DarkFlippers/unleashed-firmware/blob/dev/documentation/DangerousSettings.md) -- [How to add extra Sub-GHz frequencies](https://github.com/DarkFlippers/unleashed-firmware/blob/dev/documentation/SubGHzSettings.md) -- [~~Configure Sub-GHz Remote App~~](https://github.com/DarkFlippers/unleashed-firmware/blob/dev/documentation/SubGHzRemotePlugin.md) ⚠️ Not recommended, please use embedded configurator +- Transmission is blocked? [How to extend Sub-GHz frequency range](/documentation/DangerousSettings.md) +- [How to add extra Sub-GHz frequencies](/documentation/SubGHzSettings.md) +- [~~Configure Sub-GHz Remote App~~](/documentation/SubGHzRemotePlugin.md) ⚠️ Not recommended, please use embedded configurator -### Plugins +### ![Plugins Icon Badge] Plugins - TOTP (Authenticator): [config description](https://github.com/akopachov/flipper-zero_authenticator/blob/master/docs/conf-file_description.md) -- Barcode Generator: [How to use](https://github.com/DarkFlippers/unleashed-firmware/blob/dev/documentation/BarcodeGenerator.md) -- Multi Converter: [How to use](https://github.com/DarkFlippers/unleashed-firmware/blob/dev/documentation/MultiConverter.md) +- Barcode Generator: [How to use](/documentation/BarcodeGenerator.md) +- Multi Converter: [How to use](/documentation/MultiConverter.md) - WAV Player: [sample files & how to convert](https://github.com/UberGuidoZ/Flipper/tree/main/Wav_Player#readme) - Sub-GHz playlist: [generator script](https://github.com/darmiel/flipper-scripts/blob/main/playlist/playlist_creator_by_chunk.py) -### **Plugins that works with external hardware** [GPIO] +### ![GPIO Icon Badge] GPIO - Plugins that works with external hardware - Unitemp - Temperature sensors reader: [How to use & supported sensors](https://github.com/quen0n/unitemp-flipperzero#readme) - [NMEA] GPS: [How to use](https://github.com/xMasterX/all-the-plugins/blob/dev/base_pack/gps_nmea_uart/README.md) - i2c Tools [How to use](https://github.com/xMasterX/all-the-plugins/blob/dev/base_pack/flipper_i2ctools/README.md) -- [NRF24] plugins: [How to use](https://github.com/DarkFlippers/unleashed-firmware/blob/dev/documentation/NRF24.md) +- [NRF24] plugins: [How to use](/documentation/NRF24.md) - [WiFi] Scanner: [How to use](https://github.com/SequoiaSan/FlipperZero-WiFi-Scanner_Module#readme) | [Web Flasher](https://sequoiasan.github.io/FlipperZero-WiFi-Scanner_Module/) - [ESP8266] Deauther: [How to use](https://github.com/SequoiaSan/FlipperZero-Wifi-ESP8266-Deauther-Module#readme) | [Web Flasher](https://sequoiasan.github.io/FlipperZero-Wifi-ESP8266-Deauther-Module/) - [ESP32] WiFi Marauder: [How to use](https://github.com/UberGuidoZ/Flipper/tree/main/Wifi_DevBoard) docs by UberGuidoZ | [Marauder repo](https://github.com/justcallmekoko/ESP32Marauder) - [ESP32-CAM] Camera Suite: [How to use](https://github.com/CodyTolene/Flipper-Zero-Camera-Suite) - How to Upload `.bin` to ESP32/ESP8266: [Windows](https://github.com/SequoiaSan/Guide-How-To-Upload-bin-to-ESP8266-ESP32) | [FAP "ESP flasher"](https://github.com/0xchocolate/flipperzero-esp-flasher) -- [GPIO] SentrySafe plugin: [How to use](https://github.com/DarkFlippers/unleashed-firmware/blob/dev/documentation/SentrySafe.md) +- [GPIO] SentrySafe plugin: [How to use](/documentation/SentrySafe.md) ## 👨‍💻 Firmware & Development @@ -306,7 +307,7 @@ Enhance your Flipper Zero with apps and plugins created by the community: - `site_scons` - Build helpers - `scripts` - Supplementary scripts and python libraries home -Also, pay attention to the `ReadMe.md` files inside those directories.** +Also, pay attention to the `ReadMe.md` files inside those directories. ## 🔗 Links @@ -316,3 +317,25 @@ Also, pay attention to the `ReadMe.md` files inside those directories.** - Official Docs: [docs.flipper.net](https://docs.flipper.net) - Official Forum: [forum.flipperzero.one](https://forum.flipperzero.one) - Update! Developer Documentation [developer.flipper.net](https://developer.flipper.net/flipperzero/doxygen) + + +[RFID Icon Badge]: https://custom-icon-badges.demolab.com/badge/-rgb(255,244,147)?style=flat&logo=fz-rfid&logoColor=black +[iButton Icon Badge]: https://custom-icon-badges.demolab.com/badge/-rgb(225,187,166)?style=flat&logo=fz-ibutton&logoColor=black +[SubGhz Icon Badge]: https://custom-icon-badges.demolab.com/badge/-rgb(165,244,190)?style=flat&logo=fz-subghz&logoColor=black + +[GPIO Badge]: https://custom-icon-badges.demolab.com/badge/-GPIO-rgb(167,242,234)?style=flat&logo=fz-gpio&logoColor=black +[GPIO Icon Badge]: https://custom-icon-badges.demolab.com/badge/-rgb(167,242,234)?style=flat&logo=fz-gpio&logoColor=black + +[Tools Icon Badge]: https://custom-icon-badges.demolab.com/badge/-rgb(223,241,89)?style=flat&logo=fz-tools&logoColor=black +[Media Icon Badge]: https://custom-icon-badges.demolab.com/badge/-rgb(223,181,255)?style=flat&logo=fz-media&logoColor=black +[BT Icon Badge]: https://custom-icon-badges.demolab.com/badge/-rgb(139,172,255)?style=flat&logo=fz-bluetooth&logoColor=black +[NFC Icon Badge]: https://custom-icon-badges.demolab.com/badge/-rgb(152,206,254)?style=flat&logo=fz-nfc&logoColor=black +[USB Icon Badge]: https://custom-icon-badges.demolab.com/badge/-rgb(255,190,233)?style=flat&logo=fz-badusb&logoColor=black +[IR Icon Badge]: https://custom-icon-badges.demolab.com/badge/-rgb(254,147,140)?style=flat&logo=fz-infrared&logoColor=black +[Games Icon Badge]: https://custom-icon-badges.demolab.com/badge/-rgb(255,196,134)?style=flat&logo=fz-games&logoColor=black +[Plugins Icon Badge]: https://custom-icon-badges.demolab.com/badge/-rgb(226,78,178)?style=flat&logo=fz-plugins&logoColor=black + +[UFW Icon Badge]: https://img.shields.io/badge/by_UFW-%2314D411?style=flat-square +[Official Icon Badge]: https://img.shields.io/badge/by_OFW-%23FF8200?style=flat-square +[Author Icon Badge]: https://img.shields.io/badge/by_author-%23FFFF00?style=flat-square +[None Icon Badge]: https://img.shields.io/badge/None-%23FF0000?style=flat-square From 404764b660160faef0e9aa21f21c806820628fa2 Mon Sep 17 00:00:00 2001 From: WillyJL <49810075+Willy-JL@users.noreply.github.com> Date: Fri, 21 Feb 2025 01:47:56 +0000 Subject: [PATCH 066/268] GUI: Widget view extra options for JS (#4120) * Fill option for widget frame * Add widget circle element * Add widget line element * Fix missing include for InputType * Fix missing comment * Update api symbols * Load .fxbm from file * Fix copy pasta * Add fill param to example * Fix some comments * Bump JS SDK 0.3 * Fix free * Rename widget frame to rect * Gui: add widget_add_frame_element backward compatibility macros Co-authored-by: Aleksandr Kutuzov --- applications/services/gui/modules/widget.c | 21 ++++- applications/services/gui/modules/widget.h | 46 ++++++++-- .../modules/widget_elements/widget_element.h | 2 + .../widget_elements/widget_element_circle.c | 45 ++++++++++ .../widget_elements/widget_element_frame.c | 48 ---------- .../widget_elements/widget_element_i.h | 14 ++- .../widget_elements/widget_element_line.c | 41 +++++++++ .../widget_elements/widget_element_rect.c | 55 ++++++++++++ .../js_app/examples/apps/Scripts/gui.js | 4 +- applications/system/js_app/js_modules.c | 1 + .../system/js_app/modules/js_gui/icon.c | 88 ++++++++++++++++++- .../system/js_app/modules/js_gui/widget.c | 40 ++++++++- .../js_app/packages/fz-sdk/gui/icon.d.ts | 7 ++ .../js_app/packages/fz-sdk/gui/widget.d.ts | 8 +- targets/f18/api_symbols.csv | 6 +- targets/f7/api_symbols.csv | 6 +- 16 files changed, 360 insertions(+), 72 deletions(-) create mode 100644 applications/services/gui/modules/widget_elements/widget_element_circle.c delete mode 100644 applications/services/gui/modules/widget_elements/widget_element_frame.c create mode 100644 applications/services/gui/modules/widget_elements/widget_element_line.c create mode 100644 applications/services/gui/modules/widget_elements/widget_element_rect.c diff --git a/applications/services/gui/modules/widget.c b/applications/services/gui/modules/widget.c index ea3315d8d..f8d2fc346 100644 --- a/applications/services/gui/modules/widget.c +++ b/applications/services/gui/modules/widget.c @@ -195,14 +195,27 @@ void widget_add_icon_element(Widget* widget, uint8_t x, uint8_t y, const Icon* i widget_add_element(widget, icon_element); } -void widget_add_frame_element( +void widget_add_rect_element( Widget* widget, uint8_t x, uint8_t y, uint8_t width, uint8_t height, - uint8_t radius) { + uint8_t radius, + bool fill) { furi_check(widget); - WidgetElement* frame_element = widget_element_frame_create(x, y, width, height, radius); - widget_add_element(widget, frame_element); + WidgetElement* rect_element = widget_element_rect_create(x, y, width, height, radius, fill); + widget_add_element(widget, rect_element); +} + +void widget_add_circle_element(Widget* widget, uint8_t x, uint8_t y, uint8_t radius, bool fill) { + furi_check(widget); + WidgetElement* circle_element = widget_element_circle_create(x, y, radius, fill); + widget_add_element(widget, circle_element); +} + +void widget_add_line_element(Widget* widget, uint8_t x1, uint8_t y1, uint8_t x2, uint8_t y2) { + furi_check(widget); + WidgetElement* line_element = widget_element_line_create(x1, y1, x2, y2); + widget_add_element(widget, line_element); } diff --git a/applications/services/gui/modules/widget.h b/applications/services/gui/modules/widget.h index b31370296..bda444963 100644 --- a/applications/services/gui/modules/widget.h +++ b/applications/services/gui/modules/widget.h @@ -152,21 +152,57 @@ void widget_add_button_element( void widget_add_icon_element(Widget* widget, uint8_t x, uint8_t y, const Icon* icon); /** Add Frame Element + * + * @param widget Widget instance + * @param x top left x coordinate + * @param y top left y coordinate + * @param width frame width + * @param height frame height + * @param radius frame radius + * + * @warning deprecated, use widget_add_rect_element instead + */ +#define widget_add_frame_element(widget, x, y, width, height, radius) \ + widget_add_rect_element((widget), (x), (y), (width), (height), (radius), false) + +/** Add Rect Element * * @param widget Widget instance * @param x top left x coordinate * @param y top left y coordinate - * @param width frame width - * @param height frame height - * @param radius frame radius + * @param width rect width + * @param height rect height + * @param radius corner radius + * @param fill whether to fill the box or not */ -void widget_add_frame_element( +void widget_add_rect_element( Widget* widget, uint8_t x, uint8_t y, uint8_t width, uint8_t height, - uint8_t radius); + uint8_t radius, + bool fill); + +/** Add Circle Element + * + * @param widget Widget instance + * @param x center x coordinate + * @param y center y coordinate + * @param radius circle radius + * @param fill whether to fill the circle or not + */ +void widget_add_circle_element(Widget* widget, uint8_t x, uint8_t y, uint8_t radius, bool fill); + +/** Add Line Element + * + * @param widget Widget instance + * @param x1 first x coordinate + * @param y1 first y coordinate + * @param x2 second x coordinate + * @param y2 second y coordinate + */ +void widget_add_line_element(Widget* widget, uint8_t x1, uint8_t y1, uint8_t x2, uint8_t y2); #ifdef __cplusplus } diff --git a/applications/services/gui/modules/widget_elements/widget_element.h b/applications/services/gui/modules/widget_elements/widget_element.h index 473fabd04..8f14053f4 100644 --- a/applications/services/gui/modules/widget_elements/widget_element.h +++ b/applications/services/gui/modules/widget_elements/widget_element.h @@ -5,6 +5,8 @@ #pragma once +#include + #ifdef __cplusplus extern "C" { #endif diff --git a/applications/services/gui/modules/widget_elements/widget_element_circle.c b/applications/services/gui/modules/widget_elements/widget_element_circle.c new file mode 100644 index 000000000..47a952429 --- /dev/null +++ b/applications/services/gui/modules/widget_elements/widget_element_circle.c @@ -0,0 +1,45 @@ +#include "widget_element_i.h" + +typedef struct { + uint8_t x; + uint8_t y; + uint8_t radius; + bool fill; +} GuiCircleModel; + +static void gui_circle_draw(Canvas* canvas, WidgetElement* element) { + furi_assert(canvas); + furi_assert(element); + GuiCircleModel* model = element->model; + if(model->fill) { + canvas_draw_disc(canvas, model->x, model->y, model->radius); + } else { + canvas_draw_circle(canvas, model->x, model->y, model->radius); + } +} + +static void gui_circle_free(WidgetElement* gui_circle) { + furi_assert(gui_circle); + + free(gui_circle->model); + free(gui_circle); +} + +WidgetElement* widget_element_circle_create(uint8_t x, uint8_t y, uint8_t radius, bool fill) { + // Allocate and init model + GuiCircleModel* model = malloc(sizeof(GuiCircleModel)); + model->x = x; + model->y = y; + model->radius = radius; + model->fill = fill; + + // Allocate and init Element + WidgetElement* gui_circle = malloc(sizeof(WidgetElement)); + gui_circle->parent = NULL; + gui_circle->input = NULL; + gui_circle->draw = gui_circle_draw; + gui_circle->free = gui_circle_free; + gui_circle->model = model; + + return gui_circle; +} diff --git a/applications/services/gui/modules/widget_elements/widget_element_frame.c b/applications/services/gui/modules/widget_elements/widget_element_frame.c deleted file mode 100644 index a7265348e..000000000 --- a/applications/services/gui/modules/widget_elements/widget_element_frame.c +++ /dev/null @@ -1,48 +0,0 @@ -#include "widget_element_i.h" - -typedef struct { - uint8_t x; - uint8_t y; - uint8_t width; - uint8_t height; - uint8_t radius; -} GuiFrameModel; - -static void gui_frame_draw(Canvas* canvas, WidgetElement* element) { - furi_assert(canvas); - furi_assert(element); - GuiFrameModel* model = element->model; - canvas_draw_rframe(canvas, model->x, model->y, model->width, model->height, model->radius); -} - -static void gui_frame_free(WidgetElement* gui_frame) { - furi_assert(gui_frame); - - free(gui_frame->model); - free(gui_frame); -} - -WidgetElement* widget_element_frame_create( - uint8_t x, - uint8_t y, - uint8_t width, - uint8_t height, - uint8_t radius) { - // Allocate and init model - GuiFrameModel* model = malloc(sizeof(GuiFrameModel)); - model->x = x; - model->y = y; - model->width = width; - model->height = height; - model->radius = radius; - - // Allocate and init Element - WidgetElement* gui_frame = malloc(sizeof(WidgetElement)); - gui_frame->parent = NULL; - gui_frame->input = NULL; - gui_frame->draw = gui_frame_draw; - gui_frame->free = gui_frame_free; - gui_frame->model = model; - - return gui_frame; -} diff --git a/applications/services/gui/modules/widget_elements/widget_element_i.h b/applications/services/gui/modules/widget_elements/widget_element_i.h index 2bced5576..adaf09168 100644 --- a/applications/services/gui/modules/widget_elements/widget_element_i.h +++ b/applications/services/gui/modules/widget_elements/widget_element_i.h @@ -73,14 +73,16 @@ WidgetElement* widget_element_button_create( /** Create icon element */ WidgetElement* widget_element_icon_create(uint8_t x, uint8_t y, const Icon* icon); -/** Create frame element */ -WidgetElement* widget_element_frame_create( +/** Create rect element */ +WidgetElement* widget_element_rect_create( uint8_t x, uint8_t y, uint8_t width, uint8_t height, - uint8_t radius); + uint8_t radius, + bool fill); +/** Create text scroll element */ WidgetElement* widget_element_text_scroll_create( uint8_t x, uint8_t y, @@ -88,6 +90,12 @@ WidgetElement* widget_element_text_scroll_create( uint8_t height, const char* text); +/** Create circle element */ +WidgetElement* widget_element_circle_create(uint8_t x, uint8_t y, uint8_t radius, bool fill); + +/** Create line element */ +WidgetElement* widget_element_line_create(uint8_t x1, uint8_t y1, uint8_t x2, uint8_t y2); + #ifdef __cplusplus } #endif diff --git a/applications/services/gui/modules/widget_elements/widget_element_line.c b/applications/services/gui/modules/widget_elements/widget_element_line.c new file mode 100644 index 000000000..4cccdc976 --- /dev/null +++ b/applications/services/gui/modules/widget_elements/widget_element_line.c @@ -0,0 +1,41 @@ +#include "widget_element_i.h" + +typedef struct { + uint8_t x1; + uint8_t y1; + uint8_t x2; + uint8_t y2; +} GuiLineModel; + +static void gui_line_draw(Canvas* canvas, WidgetElement* element) { + furi_assert(canvas); + furi_assert(element); + GuiLineModel* model = element->model; + canvas_draw_line(canvas, model->x1, model->y1, model->x2, model->y2); +} + +static void gui_line_free(WidgetElement* gui_line) { + furi_assert(gui_line); + + free(gui_line->model); + free(gui_line); +} + +WidgetElement* widget_element_line_create(uint8_t x1, uint8_t y1, uint8_t x2, uint8_t y2) { + // Allocate and init model + GuiLineModel* model = malloc(sizeof(GuiLineModel)); + model->x1 = x1; + model->y1 = y1; + model->x2 = x2; + model->y2 = y2; + + // Allocate and init Element + WidgetElement* gui_line = malloc(sizeof(WidgetElement)); + gui_line->parent = NULL; + gui_line->input = NULL; + gui_line->draw = gui_line_draw; + gui_line->free = gui_line_free; + gui_line->model = model; + + return gui_line; +} diff --git a/applications/services/gui/modules/widget_elements/widget_element_rect.c b/applications/services/gui/modules/widget_elements/widget_element_rect.c new file mode 100644 index 000000000..6539f09a8 --- /dev/null +++ b/applications/services/gui/modules/widget_elements/widget_element_rect.c @@ -0,0 +1,55 @@ +#include "widget_element_i.h" + +typedef struct { + uint8_t x; + uint8_t y; + uint8_t width; + uint8_t height; + uint8_t radius; + bool fill; +} GuiRectModel; + +static void gui_rect_draw(Canvas* canvas, WidgetElement* element) { + furi_assert(canvas); + furi_assert(element); + GuiRectModel* model = element->model; + if(model->fill) { + canvas_draw_rbox(canvas, model->x, model->y, model->width, model->height, model->radius); + } else { + canvas_draw_rframe(canvas, model->x, model->y, model->width, model->height, model->radius); + } +} + +static void gui_rect_free(WidgetElement* gui_rect) { + furi_assert(gui_rect); + + free(gui_rect->model); + free(gui_rect); +} + +WidgetElement* widget_element_rect_create( + uint8_t x, + uint8_t y, + uint8_t width, + uint8_t height, + uint8_t radius, + bool fill) { + // Allocate and init model + GuiRectModel* model = malloc(sizeof(GuiRectModel)); + model->x = x; + model->y = y; + model->width = width; + model->height = height; + model->radius = radius; + model->fill = fill; + + // Allocate and init Element + WidgetElement* gui_rect = malloc(sizeof(WidgetElement)); + gui_rect->parent = NULL; + gui_rect->input = NULL; + gui_rect->draw = gui_rect_draw; + gui_rect->free = gui_rect_free; + gui_rect->model = model; + + return gui_rect; +} diff --git a/applications/system/js_app/examples/apps/Scripts/gui.js b/applications/system/js_app/examples/apps/Scripts/gui.js index a1c104cf1..16673524a 100644 --- a/applications/system/js_app/examples/apps/Scripts/gui.js +++ b/applications/system/js_app/examples/apps/Scripts/gui.js @@ -19,8 +19,8 @@ let jsLogo = icon.getBuiltin("js_script_10px"); let stopwatchWidgetElements = [ { element: "string", x: 67, y: 44, align: "bl", font: "big_numbers", text: "00 00" }, { element: "string", x: 77, y: 22, align: "bl", font: "primary", text: "Stopwatch" }, - { element: "frame", x: 64, y: 27, w: 28, h: 20, radius: 3 }, - { element: "frame", x: 100, y: 27, w: 28, h: 20, radius: 3 }, + { element: "frame", x: 64, y: 27, w: 28, h: 20, radius: 3, fill: false }, + { element: "frame", x: 100, y: 27, w: 28, h: 20, radius: 3, fill: false }, { element: "icon", x: 0, y: 5, iconData: cuteDolphinWithWatch }, { element: "icon", x: 64, y: 13, iconData: jsLogo }, { element: "button", button: "right", text: "Back" }, diff --git a/applications/system/js_app/js_modules.c b/applications/system/js_app/js_modules.c index 57997b09f..a8480e6a2 100644 --- a/applications/system/js_app/js_modules.c +++ b/applications/system/js_app/js_modules.c @@ -270,6 +270,7 @@ static const char* extra_features[] = { "gpio-pwm", "gui-widget", "serial-framing", + "gui-widget-extras", }; /** diff --git a/applications/system/js_app/modules/js_gui/icon.c b/applications/system/js_app/modules/js_gui/icon.c index 4b7926ba1..3d8a67a8b 100644 --- a/applications/system/js_app/modules/js_gui/icon.c +++ b/applications/system/js_app/modules/js_gui/icon.c @@ -1,5 +1,8 @@ #include "../../js_modules.h" #include +#include +#include +#include typedef struct { const char* name; @@ -16,6 +19,26 @@ static const IconDefinition builtin_icons[] = { ICON_DEF(js_script_10px), }; +// Firmware's Icon struct needs a frames array, and uses a small CompressHeader +// Here we use a variable size allocation to add the uncompressed data in same allocation +// Also use a one-long array pointing to later in the same struct as the frames array +// CompressHeader includes a first is_compressed byte so we don't need to compress (.fxbm is uncompressed) +typedef struct FURI_PACKED { + Icon icon; + uint8_t* frames[1]; + struct { + uint8_t is_compressed; + uint8_t uncompressed_data[]; + } frame; +} FxbmIconWrapper; + +LIST_DEF(FxbmIconWrapperList, FxbmIconWrapper*, M_PTR_OPLIST); // NOLINT +#define M_OPL_FxbmIconWrapperList_t() LIST_OPLIST(FxbmIconWrapperList) + +typedef struct { + FxbmIconWrapperList_t fxbm_list; +} JsGuiIconInst; + static void js_gui_icon_get_builtin(struct mjs* mjs) { const char* icon_name; JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&icon_name)); @@ -30,17 +53,78 @@ static void js_gui_icon_get_builtin(struct mjs* mjs) { JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "no such built-in icon"); } +static void js_gui_icon_load_fxbm(struct mjs* mjs) { + const char* fxbm_path; + JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&fxbm_path)); + + Storage* storage = furi_record_open(RECORD_STORAGE); + File* file = storage_file_alloc(storage); + FxbmIconWrapper* fxbm = NULL; + + do { + if(!storage_file_open(file, fxbm_path, FSAM_READ, FSOM_OPEN_EXISTING)) { + break; + } + + struct { + uint32_t size; // Total following size including width and height values + uint32_t width; + uint32_t height; + } fxbm_header; + if(storage_file_read(file, &fxbm_header, sizeof(fxbm_header)) != sizeof(fxbm_header)) { + break; + } + + size_t frame_size = fxbm_header.size - sizeof(uint32_t) * 2; + fxbm = malloc(sizeof(FxbmIconWrapper) + frame_size); + if(storage_file_read(file, fxbm->frame.uncompressed_data, frame_size) != frame_size) { + free(fxbm); + fxbm = NULL; + break; + } + + FURI_CONST_ASSIGN(fxbm->icon.width, fxbm_header.width); + FURI_CONST_ASSIGN(fxbm->icon.height, fxbm_header.height); + FURI_CONST_ASSIGN(fxbm->icon.frame_count, 1); + FURI_CONST_ASSIGN(fxbm->icon.frame_rate, 1); + FURI_CONST_ASSIGN_PTR(fxbm->icon.frames, fxbm->frames); + fxbm->frames[0] = (void*)&fxbm->frame; + fxbm->frame.is_compressed = false; + } while(false); + + storage_file_free(file); + furi_record_close(RECORD_STORAGE); + + if(!fxbm) { + JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "could not load .fxbm icon"); + } + + JsGuiIconInst* js_icon = JS_GET_CONTEXT(mjs); + FxbmIconWrapperList_push_back(js_icon->fxbm_list, fxbm); + mjs_return(mjs, mjs_mk_foreign(mjs, (void*)&fxbm->icon)); +} + static void* js_gui_icon_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) { UNUSED(modules); + JsGuiIconInst* js_icon = malloc(sizeof(JsGuiIconInst)); + FxbmIconWrapperList_init(js_icon->fxbm_list); *object = mjs_mk_object(mjs); JS_ASSIGN_MULTI(mjs, *object) { + JS_FIELD(INST_PROP_NAME, mjs_mk_foreign(mjs, js_icon)); JS_FIELD("getBuiltin", MJS_MK_FN(js_gui_icon_get_builtin)); + JS_FIELD("loadFxbm", MJS_MK_FN(js_gui_icon_load_fxbm)); } - return NULL; + return js_icon; } static void js_gui_icon_destroy(void* inst) { - UNUSED(inst); + JsGuiIconInst* js_icon = inst; + for + M_EACH(fxbm, js_icon->fxbm_list, FxbmIconWrapperList_t) { + free(*fxbm); + } + FxbmIconWrapperList_clear(js_icon->fxbm_list); + free(js_icon); } static const JsModuleDescriptor js_gui_icon_desc = { diff --git a/applications/system/js_app/modules/js_gui/widget.c b/applications/system/js_app/modules/js_gui/widget.c index 5f9a222cc..bb2898030 100644 --- a/applications/system/js_app/modules/js_gui/widget.c +++ b/applications/system/js_app/modules/js_gui/widget.c @@ -202,7 +202,7 @@ static bool js_widget_add_child( const Icon* icon = mjs_get_ptr(mjs, icon_data_in); widget_add_icon_element(widget, x, y, icon); - } else if(strcmp(element_type, "frame") == 0) { + } else if(strcmp(element_type, "rect") == 0) { int32_t x, y, w, h; DESTRUCTURE_OR_RETURN(mjs, child_obj, position, &x, &y); DESTRUCTURE_OR_RETURN(mjs, child_obj, size, &w, &h); @@ -211,7 +211,43 @@ static bool js_widget_add_child( JS_ERROR_AND_RETURN_VAL( mjs, MJS_BAD_ARGS_ERROR, false, "failed to fetch element radius"); int32_t radius = mjs_get_int32(mjs, radius_in); - widget_add_frame_element(widget, x, y, w, h, radius); + mjs_val_t fill_in = mjs_get(mjs, child_obj, "fill", ~0); + if(!mjs_is_boolean(fill_in)) + JS_ERROR_AND_RETURN_VAL( + mjs, MJS_BAD_ARGS_ERROR, false, "failed to fetch element fill"); + int32_t fill = mjs_get_bool(mjs, fill_in); + widget_add_rect_element(widget, x, y, w, h, radius, fill); + + } else if(strcmp(element_type, "circle") == 0) { + int32_t x, y; + DESTRUCTURE_OR_RETURN(mjs, child_obj, position, &x, &y); + mjs_val_t radius_in = mjs_get(mjs, child_obj, "radius", ~0); + if(!mjs_is_number(radius_in)) + JS_ERROR_AND_RETURN_VAL( + mjs, MJS_BAD_ARGS_ERROR, false, "failed to fetch element radius"); + int32_t radius = mjs_get_int32(mjs, radius_in); + mjs_val_t fill_in = mjs_get(mjs, child_obj, "fill", ~0); + if(!mjs_is_boolean(fill_in)) + JS_ERROR_AND_RETURN_VAL( + mjs, MJS_BAD_ARGS_ERROR, false, "failed to fetch element fill"); + int32_t fill = mjs_get_bool(mjs, fill_in); + widget_add_circle_element(widget, x, y, radius, fill); + + } else if(strcmp(element_type, "line") == 0) { + int32_t x1, y1, x2, y2; + mjs_val_t x1_in = mjs_get(mjs, child_obj, "x1", ~0); + mjs_val_t y1_in = mjs_get(mjs, child_obj, "y1", ~0); + mjs_val_t x2_in = mjs_get(mjs, child_obj, "x2", ~0); + mjs_val_t y2_in = mjs_get(mjs, child_obj, "y2", ~0); + if(!mjs_is_number(x1_in) || !mjs_is_number(y1_in) || !mjs_is_number(x2_in) || + !mjs_is_number(y2_in)) + JS_ERROR_AND_RETURN_VAL( + mjs, MJS_BAD_ARGS_ERROR, false, "failed to fetch element positions"); + x1 = mjs_get_int32(mjs, x1_in); + y1 = mjs_get_int32(mjs, y1_in); + x2 = mjs_get_int32(mjs, x2_in); + y2 = mjs_get_int32(mjs, y2_in); + widget_add_line_element(widget, x1, y1, x2, y2); } return true; diff --git a/applications/system/js_app/packages/fz-sdk/gui/icon.d.ts b/applications/system/js_app/packages/fz-sdk/gui/icon.d.ts index 577e1e6a9..8d2b68bce 100644 --- a/applications/system/js_app/packages/fz-sdk/gui/icon.d.ts +++ b/applications/system/js_app/packages/fz-sdk/gui/icon.d.ts @@ -9,3 +9,10 @@ export type IconData = symbol & { "__tag__": "icon" }; * @version Added in JS SDK 0.2, extra feature `"gui-widget"` */ export declare function getBuiltin(icon: BuiltinIcon): IconData; + +/** + * Loads a .fxbm icon (XBM Flipper sprite, from flipperzero-game-engine) for use in GUI + * @param path Path to the .fxbm file + * @version Added in JS SDK 0.3, extra feature `"gui-widget-extras"` + */ +export declare function loadFxbm(path: string): IconData; diff --git a/applications/system/js_app/packages/fz-sdk/gui/widget.d.ts b/applications/system/js_app/packages/fz-sdk/gui/widget.d.ts index b37af8e3c..bf4aab22b 100644 --- a/applications/system/js_app/packages/fz-sdk/gui/widget.d.ts +++ b/applications/system/js_app/packages/fz-sdk/gui/widget.d.ts @@ -42,7 +42,9 @@ type TextBoxElement = { element: "text_box", stripToDots: boolean } & Position & type TextScrollElement = { element: "text_scroll" } & Position & Size & Text; type ButtonElement = { element: "button", button: "left" | "center" | "right" } & Text; type IconElement = { element: "icon", iconData: IconData } & Position; -type FrameElement = { element: "frame", radius: number } & Position & Size; +type RectElement = { element: "rect", radius: number, fill: boolean } & Position & Size; /** @version Amended in JS SDK 0.3, extra feature `"gui-widget-extras"` */ +type CircleElement = { element: "circle", radius: number, fill: boolean } & Position; /** @version Added in JS SDK 0.3, extra feature `"gui-widget-extras"` */ +type LineElement = { element: "line", x1: number, y1: number, x2: number, y2: number }; /** @version Added in JS SDK 0.3, extra feature `"gui-widget-extras"` */ type Element = StringMultilineElement | StringElement @@ -50,7 +52,9 @@ type Element = StringMultilineElement | TextScrollElement | ButtonElement | IconElement - | FrameElement; + | RectElement + | CircleElement + | LineElement; type Props = {}; type Child = Element; diff --git a/targets/f18/api_symbols.csv b/targets/f18/api_symbols.csv index b93d6eb52..4aeef3c9d 100644 --- a/targets/f18/api_symbols.csv +++ b/targets/f18/api_symbols.csv @@ -1,5 +1,5 @@ entry,status,name,type,params -Version,+,80.4,, +Version,+,81.1,, Header,+,applications/services/bt/bt_service/bt.h,, Header,+,applications/services/bt/bt_service/bt_keys_storage.h,, Header,+,applications/services/cli/cli.h,, @@ -2899,8 +2899,10 @@ Function,-,vsscanf,int,"const char*, const char*, __gnuc_va_list" Function,-,wcstombs,size_t,"char*, const wchar_t*, size_t" Function,-,wctomb,int,"char*, wchar_t" Function,+,widget_add_button_element,void,"Widget*, GuiButtonType, const char*, ButtonCallback, void*" -Function,+,widget_add_frame_element,void,"Widget*, uint8_t, uint8_t, uint8_t, uint8_t, uint8_t" +Function,+,widget_add_circle_element,void,"Widget*, uint8_t, uint8_t, uint8_t, _Bool" Function,+,widget_add_icon_element,void,"Widget*, uint8_t, uint8_t, const Icon*" +Function,+,widget_add_line_element,void,"Widget*, uint8_t, uint8_t, uint8_t, uint8_t" +Function,+,widget_add_rect_element,void,"Widget*, uint8_t, uint8_t, uint8_t, uint8_t, uint8_t, _Bool" Function,+,widget_add_string_element,void,"Widget*, uint8_t, uint8_t, Align, Align, Font, const char*" Function,+,widget_add_string_multiline_element,void,"Widget*, uint8_t, uint8_t, Align, Align, Font, const char*" Function,+,widget_add_text_box_element,void,"Widget*, uint8_t, uint8_t, uint8_t, uint8_t, Align, Align, const char*, _Bool" diff --git a/targets/f7/api_symbols.csv b/targets/f7/api_symbols.csv index 0fef0cb36..14eaa3142 100644 --- a/targets/f7/api_symbols.csv +++ b/targets/f7/api_symbols.csv @@ -1,5 +1,5 @@ entry,status,name,type,params -Version,+,80.4,, +Version,+,81.1,, Header,+,applications/drivers/subghz/cc1101_ext/cc1101_ext_interconnect.h,, Header,+,applications/services/bt/bt_service/bt.h,, Header,+,applications/services/bt/bt_service/bt_keys_storage.h,, @@ -3750,8 +3750,10 @@ Function,-,vsscanf,int,"const char*, const char*, __gnuc_va_list" Function,-,wcstombs,size_t,"char*, const wchar_t*, size_t" Function,-,wctomb,int,"char*, wchar_t" Function,+,widget_add_button_element,void,"Widget*, GuiButtonType, const char*, ButtonCallback, void*" -Function,+,widget_add_frame_element,void,"Widget*, uint8_t, uint8_t, uint8_t, uint8_t, uint8_t" +Function,+,widget_add_circle_element,void,"Widget*, uint8_t, uint8_t, uint8_t, _Bool" Function,+,widget_add_icon_element,void,"Widget*, uint8_t, uint8_t, const Icon*" +Function,+,widget_add_line_element,void,"Widget*, uint8_t, uint8_t, uint8_t, uint8_t" +Function,+,widget_add_rect_element,void,"Widget*, uint8_t, uint8_t, uint8_t, uint8_t, uint8_t, _Bool" Function,+,widget_add_string_element,void,"Widget*, uint8_t, uint8_t, Align, Align, Font, const char*" Function,+,widget_add_string_multiline_element,void,"Widget*, uint8_t, uint8_t, Align, Align, Font, const char*" Function,+,widget_add_text_box_element,void,"Widget*, uint8_t, uint8_t, uint8_t, uint8_t, Align, Align, const char*, _Bool" From 0214e176c2eb8ad4723a75a5e5e10feeb12125a3 Mon Sep 17 00:00:00 2001 From: m-xim <170838360+m-xim@users.noreply.github.com> Date: Fri, 21 Feb 2025 05:22:25 +0300 Subject: [PATCH 067/268] docs: rearrange badges layout and remove link underline from badges --- ReadMe.md | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/ReadMe.md b/ReadMe.md index a1c46d6a7..c2ca75c01 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -4,21 +4,20 @@ - + + English Telegram Chat + + + Russian Telegram Chat + + + Ukraine Telegram Chat + + + + Discord server + + # Flipper Zero Unleashed Firmware This firmware is a fork of the original (OFW) version of [flipperdevices/flipperzero-firmware](https://github.com/flipperdevices/flipperzero-firmware) and represents the **most stable** custom build, incorporating **new features** and **improvements** to the original components while remaining **fully compatible** with the API and applications of the original firmware. @@ -65,7 +64,7 @@ Before getting started: > > - Regional TX restrictions removed > - Extra Sub-GHz frequencies added -> - Frequency range can be extended in settings file (Warning: It can damage Flipper's hardware) +> - Frequency range can be extended in settings file _(warning: It can damage Flipper's hardware)_ > - Many rolling code [protocols](#current-modified-and-new-sub-ghz-protocols-list) now have the ability to save & send captured signals > - FAAC SLH (Spa) & BFT Mitto (keeloq secure with seed) manual creation > - External CC1101 module support [(by quen0n)](https://github.com/DarkFlippers/unleashed-firmware/pull/307) From 476e8f22d002103942c96135d044e8229ad82e98 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Fri, 21 Feb 2025 05:25:44 +0300 Subject: [PATCH 068/268] upd changelog --- CHANGELOG.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ff3efb28..e8cf72e75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,5 @@ ## Main changes -- Current API: 80.2 +- Current API: 81.1 * SubGHz: Fix GangQi protocol (by @DoberBit and @mishamyte (who spent 2 weeks on this)) * SubGHz: Came Atomo button hold simulation with full cycle simulation (to allow proper pairing with receiver) * OFW: LFRFID - **EM4305 support** @@ -12,6 +12,12 @@ * Apps: **Check out more Apps updates and fixes by following** [this link](https://github.com/xMasterX/all-the-plugins/commits/dev) ## Other changes * Anims: Disable winter anims +* OFW: GUI: Widget view extra options for JS +* OFW: Update heap implementation +* OFW: Updated Button Panel +* OFW: UART framing mode selection +* OFW: gpio: clear irq status before calling user handler +* OFW: Fix 5V on GPIO * OFW: Fixed repeat in subghz tx_from_file command * OFW: LFRFID: Noralsy Format/Brand * OFW: Faster di card reading From cf7348131018d80587b174239564d46cb8f77afc Mon Sep 17 00:00:00 2001 From: m-xim <170838360+m-xim@users.noreply.github.com> Date: Fri, 21 Feb 2025 05:52:17 +0300 Subject: [PATCH 069/268] docs: sort manufacturers list alphabetically and distribute in table columns --- ReadMe.md | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/ReadMe.md b/ReadMe.md index c2ca75c01..2277e45ca 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -152,10 +152,20 @@ Thanks to Official team (to their SubGHz Developer, Skorp) for implementing supp > Supported manufacturers include >
> -> ``` -> DoorHan, Alligator, Stilmatic, Mongoose, SL_A6-A9/Tomahawk_9010, Pantera, SL_A2-A4, Cenmax_St-5, SL_B6,B9_dop, Harpoon, Tomahawk_TZ-9030, Tomahawk_Z,X_3-5, Cenmax_St-7, Sheriff, Pantera_CLK, Cenmax, Alligator_S-275, Guard_RF-311A, Partisan_RX, APS-1100_APS-2550, Pantera_XS/Jaguar, NICE_Smilo, NICE_MHOUSE, Dea_Mio, Genius_Bravo, FAAC_RC,XT, FAAC_SLH, BFT, Came_Space, DTM_Neo, GSN, Beninca, Elmes_Poland, IronLogic, Comunello, Sommer(fsk476), Normstahl, KEY, JCM_Tech, EcoStar, Gibidi, Aprimatic, Kingates_Stylo4k, Centurion, KGB/Subaru, Magic_1, Magic_2, Magic_3, Magic_4, Teco, Mutanco_Mutancode, Leopard, Faraon, Reff, ZX-730-750-1055 -> ``` - +> | Column 1 | Column 2 | Column 3 | Column 4 | Column 5 | +> |-------------------|--------------|------------------|-------------------|------------------------| +> | Alligator | Comunello | GSN | Magic_3 | Reff | +> | Alligator_S-275 | Dea_Mio | Guard_RF-311A | Magic_4 | Sheriff | +> | APS-1100_APS-2550 | DTM_Neo | Harpoon | Mongoose | SL_A2-A4 | +> | Aprimatic | DoorHan | IronLogic | Mutanco_Mutancode | SL_A6-A9/Tomahawk_9010 | +> | Beninca | EcoStar | JCM_Tech | NICE_MHOUSE | SL_B6,B9_dop | +> | BFT | Elmes_Poland | KEY | NICE_Smilo | Sommer(fsk476) | +> | Came_Space | FAAC_RC,XT | Kingates_Stylo4k | Normstahl | Stilmatic | +> | Cenmax | FAAC_SLH | KGB/Subaru | Pantera | Teco | +> | Cenmax_St-5 | Faraon | Leopard | Pantera_CLK | Tomahawk_TZ-9030 | +> | Cenmax_St-7 | Genius_Bravo | Magic_1 | Pantera_XS/Jaguar | Tomahawk_Z,X_3-5 | +> | Centurion | Gibidi | Magic_2 | Partisan_RX | ZX-730-750-1055 | +>
From 12927425387eae5951a3df0ecd409ff0f63d52c3 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Fri, 21 Feb 2025 06:41:25 +0300 Subject: [PATCH 070/268] upd missing parts [ci skip] --- CHANGELOG.md | 34 +++++++++++++++++++--------------- ReadMe.md | 7 +++++-- 2 files changed, 24 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e8cf72e75..7f2628ea9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ * OFW: BadUSB: Mouse control * OFW: NFC - Added naming for DESFire cards + fix MF3ICD40 cards unable to be read * Apps: Add FindMyFlipper to system apps and allow autostart on system boot [app by @MatthewKuKanich](https://github.com/MatthewKuKanich/FindMyFlipper) and autoloader by @Willy-JL - to use app please check how to add keys in [app repo](https://github.com/MatthewKuKanich/FindMyFlipper) +* README Update: Enhanced Visuals & Navigation (PR #871 | by @m-xim) +* Docs: Update FAQ.md (PR #865 | by @mi-lrn) * Input: Vibro on Button press option (PR #867 | by @Dmitry422) * Power: Option to limit battery charging (suppress charging on selected charge level) (PR #867 | by @Dmitry422) * Apps: **Check out more Apps updates and fixes by following** [this link](https://github.com/xMasterX/all-the-plugins/commits/dev) @@ -43,21 +45,23 @@ [-> Download qFlipper (official link)](https://flipperzero.one/update) ## Please support development of the project -|Service|Remark|QR Code|Link/Wallet| -|-|-|-|-| -|**Patreon**||
QR image
|https://patreon.com/mmxdev| -|**Boosty**|patreon alternative|
QR image
|https://boosty.to/mmxdev| -|cloudtips|only RU payments accepted|
QR image
|https://pay.cloudtips.ru/p/7b3e9d65| -|YooMoney|only RU payments accepted|
QR image
|https://yoomoney.ru/fundraise/XA49mgQLPA0.221209| -|USDT|(TRC20)|
QR image
|`TSXcitMSnWXUFqiUfEXrTVpVewXy2cYhrs`| -|ETH|(BSC/ERC20-Tokens)|
QR image
|`0xFebF1bBc8229418FF2408C07AF6Afa49152fEc6a`| -|BTC||
QR image
|`bc1q0np836jk9jwr4dd7p6qv66d04vamtqkxrecck9`| -|SOL|(Solana/Tokens)|
QR image
|`DSgwouAEgu8iP5yr7EHHDqMNYWZxAqXWsTEeqCAXGLj8`| -|DOGE||
QR image
|`D6R6gYgBn5LwTNmPyvAQR6bZ9EtGgFCpvv`| -|LTC||
QR image
|`ltc1q3ex4ejkl0xpx3znwrmth4lyuadr5qgv8tmq8z9`| -|BCH||
QR image
|`qquxfyzntuqufy2dx0hrfr4sndp0tucvky4sw8qyu3`| -|XMR|(Monero)|
QR image
|`41xUz92suUu1u5Mu4qkrcs52gtfpu9rnZRdBpCJ244KRHf6xXSvVFevdf2cnjS7RAeYr5hn9MsEfxKoFDRSctFjG5fv1Mhn`| -|TON||
QR image
|`UQCOqcnYkvzOZUV_9bPE_8oTbOrOF03MnF-VcJyjisTZmsxa`| + +| Service | Remark | QR Code | Link/Wallet | +|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------| +| Patreon **Patreon** | |
QR image
| [patreon.com/mmxdev](https://patreon.com/mmxdev) | +| Boosty **Boosty** | patreon alternative |
QR image
| [boosty.to/mmxdev](https://boosty.to/mmxdev) | +| Cloudtips CloudTips | only RU payments accepted |
QR image
| [pay.cloudtips.ru/p/7b3e9d65](https://pay.cloudtips.ru/p/7b3e9d65) | +| YooMoney YooMoney | only RU payments accepted |
QR image
| [yoomoney.ru/fundraise/XA49mgQLPA0.221209](https://yoomoney.ru/fundraise/XA49mgQLPA0.221209) | +| USDT USDT | TRC20 |
QR image
| `TSXcitMSnWXUFqiUfEXrTVpVewXy2cYhrs` | +| ETH ETH | BSC/ERC20-Tokens |
QR image
| `0xFebF1bBc8229418FF2408C07AF6Afa49152fEc6a` | +| BTC BTC | |
QR image
| `bc1q0np836jk9jwr4dd7p6qv66d04vamtqkxrecck9` | +| SOL SOL | Solana/Tokens |
QR image
| `DSgwouAEgu8iP5yr7EHHDqMNYWZxAqXWsTEeqCAXGLj8` | +| DOGE DOGE | |
QR image
| `D6R6gYgBn5LwTNmPyvAQR6bZ9EtGgFCpvv` | +| LTC LTC | |
QR image
| `ltc1q3ex4ejkl0xpx3znwrmth4lyuadr5qgv8tmq8z9` | +| BCH BCH | |
QR image
| `qquxfyzntuqufy2dx0hrfr4sndp0tucvky4sw8qyu3` | +| XMR XMR | Monero |
QR image
| `41xUz92suUu1u5Mu4qkrcs52gtfpu9rnZRdBpCJ244KRHf6xXSvVFevdf2cnjS7RAeYr5hn9MsEfxKoFDRSctFjG5fv1Mhn` | +| TON TON | |
QR image
| `UQCOqcnYkvzOZUV_9bPE_8oTbOrOF03MnF-VcJyjisTZmsxa` | + #### Thanks to our sponsors who supported project in the past and special thanks to sponsors who supports us on regular basis: @mishamyte, ClaraCrazy, Pathfinder [Count Zero cDc], callmezimbra, Quen0n, MERRON, grvpvl (lvpvrg), art_col, ThurstonWaffles, Moneron, UterGrooll, LUCFER, Northpirate, zloepuzo, T.Rat, Alexey B., ionelife, ... diff --git a/ReadMe.md b/ReadMe.md index 2277e45ca..48361af6d 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -27,6 +27,8 @@ This firmware is a fork of the original (OFW) version of [flipperdevices/flipper > We do not condone unlawful behavior and strongly encourage you to use it only within the bounds of the law. > > This project is developed independently and is not affiliated with Flipper Devices. +> +> Also be aware, DarkFlippers/unleashed-firmware is the only official page of the project, there is no paid, premium or closed source versions and if someone contacts you and say they are from our team and try to offer something like that - they are scammers, block that users ASAP
@@ -149,7 +151,7 @@ Thanks to Official team (to their SubGHz Developer, Skorp) for implementing supp > [!NOTE] > Not ALL Keeloq systems are supported for decoding or emulation! >
-> Supported manufacturers include +> Supported keeloq manufacturers include >
> > | Column 1 | Column 2 | Column 3 | Column 4 | Column 5 | @@ -165,6 +167,7 @@ Thanks to Official team (to their SubGHz Developer, Skorp) for implementing supp > | Cenmax_St-5 | Faraon | Leopard | Pantera_CLK | Tomahawk_TZ-9030 | > | Cenmax_St-7 | Genius_Bravo | Magic_1 | Pantera_XS/Jaguar | Tomahawk_Z,X_3-5 | > | Centurion | Gibidi | Magic_2 | Partisan_RX | ZX-730-750-1055 | +> | Monarch | Centurion | Jolly Motors | | | >

@@ -179,7 +182,7 @@ Thanks to Official team (to their SubGHz Developer, Skorp) for implementing supp - Hay21 (dynamic 21 bit) with button parsing - Nero Radio 57bit (+ 56bit support) - CAME 12bit/24bit encoder fixes (Fixes are now merged in OFW) -- Keeloq: Dea Mio, Genius Bravo, GSN, HCS101, AN-Motors, JCM Tech, MHouse, Nice Smilo, DTM Neo, FAAC RC,XT, Mutancode, Normstahl, Beninca + Allmatic, Stilmatic, CAME Space, Aprimatic (model TR and similar), Centurion Nova (thanks Carlos !), Hormann EcoStar, Novoferm, Sommer, Monarch (thanks @ashphx !) +- Keeloq: Dea Mio, Genius Bravo, GSN, HCS101, AN-Motors, JCM Tech, MHouse, Nice Smilo, DTM Neo, FAAC RC,XT, Mutancode, Normstahl, Beninca + Allmatic, Stilmatic, CAME Space, Aprimatic (model TR and similar), Centurion Nova (thanks Carlos !), Hormann EcoStar, Novoferm, Sommer, Monarch (thanks @ashphx !), Jolly Motors (thanks @pkooiman !)
From 92818a07a25dc0d27cdd3cde534b8b9ba00f282d Mon Sep 17 00:00:00 2001 From: m-xim <170838360+m-xim@users.noreply.github.com> Date: Fri, 21 Feb 2025 13:06:21 +0300 Subject: [PATCH 071/268] docs: remove link underline from badges --- ReadMe.md | 22 ++++++---------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/ReadMe.md b/ReadMe.md index 48361af6d..bb7b58a1f 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -4,20 +4,10 @@ - - English Telegram Chat - - - Russian Telegram Chat - - - Ukraine Telegram Chat - - - - Discord server - - +[![English Telegram Chat](https://img.shields.io/endpoint?color=neon&style=flat&url=https%3A%2F%2Ftg.sumanjay.workers.dev%2Fflipperzero_unofficial)](https://t.me/flipperzero_unofficial) +[![Russian Telegram Chat](https://img.shields.io/endpoint?color=neon&style=flat&url=https%3A%2F%2Ftg.sumanjay.workers.dev%2Fflipperzero_unofficial_ru)](https://t.me/flipperzero_unofficial_ru) +[![Ukraine Telegram Chat](https://img.shields.io/endpoint?color=neon&style=flat&url=https%3A%2F%2Ftg.sumanjay.workers.dev%2Fflipperzero_unofficial_ua)](https://t.me/flipperzero_unofficial_ua) +[![Discord Server](https://img.shields.io/discord/937479784148115456?style=flat&logo=discord&label=Discord&color=%237289DA&link=https%3A%2F%2Fdiscord.unleashedflip.com%2F)](https://discord.unleashedflip.com) # Flipper Zero Unleashed Firmware This firmware is a fork of the original (OFW) version of [flipperdevices/flipperzero-firmware](https://github.com/flipperdevices/flipperzero-firmware) and represents the **most stable** custom build, incorporating **new features** and **improvements** to the original components while remaining **fully compatible** with the API and applications of the original firmware. @@ -54,7 +44,7 @@ Before getting started: > [!NOTE] > Built automatically from dev branch -- Web site: https://dev.unleashedflip.com +- Web site: [dev.unleashedflip.com](https://dev.unleashedflip.com) - Telegram Telegram: t.me/kotnehleb @@ -167,7 +157,7 @@ Thanks to Official team (to their SubGHz Developer, Skorp) for implementing supp > | Cenmax_St-5 | Faraon | Leopard | Pantera_CLK | Tomahawk_TZ-9030 | > | Cenmax_St-7 | Genius_Bravo | Magic_1 | Pantera_XS/Jaguar | Tomahawk_Z,X_3-5 | > | Centurion | Gibidi | Magic_2 | Partisan_RX | ZX-730-750-1055 | -> | Monarch | Centurion | Jolly Motors | | | +> | Monarch | Centurion | Jolly Motors | | | >

From d8b32d74ab4fe75acff97f16dc3503c9f3d37830 Mon Sep 17 00:00:00 2001 From: m-xim <170838360+m-xim@users.noreply.github.com> Date: Fri, 21 Feb 2025 16:07:16 +0300 Subject: [PATCH 072/268] docs: delete duplicate Keeloq manufacturers --- ReadMe.md | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/ReadMe.md b/ReadMe.md index bb7b58a1f..945e46167 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -139,27 +139,26 @@ Also check the [changelog in releases](https://github.com/DarkFlippers/unleashed Thanks to Official team (to their SubGHz Developer, Skorp) for implementing support (decoder + encoder / or decode only) for these protocols in OFW. > [!NOTE] -> Not ALL Keeloq systems are supported for decoding or emulation! +> Not all Keeloq systems are supported for decoding or emulation! >
-> Supported keeloq manufacturers include +> Supported Keeloq manufacturers include >
> > | Column 1 | Column 2 | Column 3 | Column 4 | Column 5 | > |-------------------|--------------|------------------|-------------------|------------------------| -> | Alligator | Comunello | GSN | Magic_3 | Reff | -> | Alligator_S-275 | Dea_Mio | Guard_RF-311A | Magic_4 | Sheriff | -> | APS-1100_APS-2550 | DTM_Neo | Harpoon | Mongoose | SL_A2-A4 | -> | Aprimatic | DoorHan | IronLogic | Mutanco_Mutancode | SL_A6-A9/Tomahawk_9010 | -> | Beninca | EcoStar | JCM_Tech | NICE_MHOUSE | SL_B6,B9_dop | -> | BFT | Elmes_Poland | KEY | NICE_Smilo | Sommer(fsk476) | -> | Came_Space | FAAC_RC,XT | Kingates_Stylo4k | Normstahl | Stilmatic | -> | Cenmax | FAAC_SLH | KGB/Subaru | Pantera | Teco | -> | Cenmax_St-5 | Faraon | Leopard | Pantera_CLK | Tomahawk_TZ-9030 | -> | Cenmax_St-7 | Genius_Bravo | Magic_1 | Pantera_XS/Jaguar | Tomahawk_Z,X_3-5 | -> | Centurion | Gibidi | Magic_2 | Partisan_RX | ZX-730-750-1055 | -> | Monarch | Centurion | Jolly Motors | | | +> | Alligator | Comunello | GSN | Magic_4 | SL_A2-A4 | +> | Alligator_S-275 | Dea_Mio | Guard_RF-311A | Mongoose | SL_A6-A9/Tomahawk_9010 | +> | APS-1100_APS-2550 | DTM_Neo | Harpoon | Mutanco_Mutancode | SL_B6,B9_dop | +> | Aprimatic | DoorHan | IronLogic | NICE_MHOUSE | Sommer(fsk476) | +> | Beninca | EcoStar | JCM_Tech | NICE_Smilo | Stilmatic | +> | BFT | Elmes_Poland | KEY | Normstahl | Teco | +> | Came_Space | FAAC_RC,XT | Kingates_Stylo4k | Pantera | Tomahawk_TZ-9030 | +> | Cenmax | FAAC_SLH | KGB/Subaru | Pantera_CLK | Tomahawk_Z,X_3-5 | +> | Cenmax_St-5 | Faraon | Leopard | Pantera_XS/Jaguar | ZX-730-750-1055 | +> | Cenmax_St-7 | Genius_Bravo | Magic_1 | Partisan_RX | | +> | Centurion | Gibidi | Magic_2 | Reff | | +> | Monarch | Jolly Motors | Magic_3 | Sheriff | | >
-
From 4c47b9baed32ee298553f7d1f3ccac4925eb7ea7 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Fri, 21 Feb 2025 20:57:55 +0300 Subject: [PATCH 073/268] upd changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f2628ea9..5fbadce8f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ * OFW: BadUSB: Mouse control * OFW: NFC - Added naming for DESFire cards + fix MF3ICD40 cards unable to be read * Apps: Add FindMyFlipper to system apps and allow autostart on system boot [app by @MatthewKuKanich](https://github.com/MatthewKuKanich/FindMyFlipper) and autoloader by @Willy-JL - to use app please check how to add keys in [app repo](https://github.com/MatthewKuKanich/FindMyFlipper) -* README Update: Enhanced Visuals & Navigation (PR #871 | by @m-xim) +* README Update: Enhanced Visuals & Navigation (PR #871 #872 | by @m-xim) * Docs: Update FAQ.md (PR #865 | by @mi-lrn) * Input: Vibro on Button press option (PR #867 | by @Dmitry422) * Power: Option to limit battery charging (suppress charging on selected charge level) (PR #867 | by @Dmitry422) From ef024e8086baed68112bab4cbd84d55d1e090b77 Mon Sep 17 00:00:00 2001 From: Anna Antonenko Date: Sat, 22 Feb 2025 03:11:27 +0400 Subject: [PATCH 074/268] [FL-3958] Stdio API improvements (#4110) * improve thread stdio callback signatures * pipe stdout timeout * update api symbols Co-authored-by: Aleksandr Kutuzov --- .../debug/unit_tests/tests/furi/furi_stdio_test.c | 15 +++++++++------ furi/core/thread.c | 14 ++++++++++---- furi/core/thread.h | 10 ++++++---- lib/toolbox/pipe.c | 11 ++++++++++- lib/toolbox/pipe.h | 10 ++++++++++ targets/f18/api_symbols.csv | 7 ++++--- targets/f7/api_symbols.csv | 7 ++++--- 7 files changed, 53 insertions(+), 21 deletions(-) diff --git a/applications/debug/unit_tests/tests/furi/furi_stdio_test.c b/applications/debug/unit_tests/tests/furi/furi_stdio_test.c index 94e2f613b..d80bd7bd5 100644 --- a/applications/debug/unit_tests/tests/furi/furi_stdio_test.c +++ b/applications/debug/unit_tests/tests/furi/furi_stdio_test.c @@ -30,7 +30,9 @@ static size_t mock_in_cb(char* buffer, size_t size, FuriWait wait, void* context } void test_stdin(void) { - FuriThreadStdinReadCallback in_cb = furi_thread_get_stdin_callback(); + FuriThreadStdinReadCallback in_cb; + void* in_ctx; + furi_thread_get_stdin_callback(&in_cb, &in_ctx); furi_thread_set_stdin_callback(mock_in_cb, CONTEXT_MAGIC); char buf[256]; @@ -63,13 +65,14 @@ void test_stdin(void) { fgets(buf, sizeof(buf), stdin); mu_assert_string_eq(" World!\n", buf); - furi_thread_set_stdin_callback(in_cb, CONTEXT_MAGIC); + furi_thread_set_stdin_callback(in_cb, in_ctx); } // stdout static FuriString* mock_out; -FuriThreadStdoutWriteCallback original_out_cb; +static FuriThreadStdoutWriteCallback original_out_cb; +static void* original_out_ctx; static void mock_out_cb(const char* data, size_t size, void* context) { furi_check(context == CONTEXT_MAGIC); @@ -83,7 +86,7 @@ static void assert_and_clear_mock_out(const char* expected) { // return the original stdout callback for the duration of the check // if the check fails, we don't want the error to end up in our buffer, // we want to be able to see it! - furi_thread_set_stdout_callback(original_out_cb, CONTEXT_MAGIC); + furi_thread_set_stdout_callback(original_out_cb, original_out_ctx); mu_assert_string_eq(expected, furi_string_get_cstr(mock_out)); furi_thread_set_stdout_callback(mock_out_cb, CONTEXT_MAGIC); @@ -91,7 +94,7 @@ static void assert_and_clear_mock_out(const char* expected) { } void test_stdout(void) { - original_out_cb = furi_thread_get_stdout_callback(); + furi_thread_get_stdout_callback(&original_out_cb, &original_out_ctx); furi_thread_set_stdout_callback(mock_out_cb, CONTEXT_MAGIC); mock_out = furi_string_alloc(); @@ -104,5 +107,5 @@ void test_stdout(void) { assert_and_clear_mock_out("Hello!"); furi_string_free(mock_out); - furi_thread_set_stdout_callback(original_out_cb, CONTEXT_MAGIC); + furi_thread_set_stdout_callback(original_out_cb, original_out_ctx); } diff --git a/furi/core/thread.c b/furi/core/thread.c index f6cfa1d36..d6132cdc2 100644 --- a/furi/core/thread.c +++ b/furi/core/thread.c @@ -758,16 +758,22 @@ static int32_t __furi_thread_stdout_flush(FuriThread* thread) { return 0; } -FuriThreadStdoutWriteCallback furi_thread_get_stdout_callback(void) { +void furi_thread_get_stdout_callback(FuriThreadStdoutWriteCallback* callback, void** context) { FuriThread* thread = furi_thread_get_current(); furi_check(thread); - return thread->output.write_callback; + furi_check(callback); + furi_check(context); + *callback = thread->output.write_callback; + *context = thread->output.context; } -FuriThreadStdinReadCallback furi_thread_get_stdin_callback(void) { +void furi_thread_get_stdin_callback(FuriThreadStdinReadCallback* callback, void** context) { FuriThread* thread = furi_thread_get_current(); furi_check(thread); - return thread->input.read_callback; + furi_check(callback); + furi_check(context); + *callback = thread->input.read_callback; + *context = thread->input.context; } void furi_thread_set_stdout_callback(FuriThreadStdoutWriteCallback callback, void* context) { diff --git a/furi/core/thread.h b/furi/core/thread.h index 9abfde5cd..c1f615d16 100644 --- a/furi/core/thread.h +++ b/furi/core/thread.h @@ -479,16 +479,18 @@ uint32_t furi_thread_get_stack_space(FuriThreadId thread_id); /** * @brief Get the standard output callback for the current thead. * - * @return pointer to the standard out callback function + * @param[out] callback where to store the stdout callback + * @param[out] context where to store the context */ -FuriThreadStdoutWriteCallback furi_thread_get_stdout_callback(void); +void furi_thread_get_stdout_callback(FuriThreadStdoutWriteCallback* callback, void** context); /** * @brief Get the standard input callback for the current thead. * - * @return pointer to the standard in callback function + * @param[out] callback where to store the stdin callback + * @param[out] context where to store the context */ -FuriThreadStdinReadCallback furi_thread_get_stdin_callback(void); +void furi_thread_get_stdin_callback(FuriThreadStdinReadCallback* callback, void** context); /** Set standard output callback for the current thread. * diff --git a/lib/toolbox/pipe.c b/lib/toolbox/pipe.c index 9dc1d368e..c6a45abf1 100644 --- a/lib/toolbox/pipe.c +++ b/lib/toolbox/pipe.c @@ -23,6 +23,7 @@ struct PipeSide { PipeSideDataArrivedCallback on_data_arrived; PipeSideSpaceFreedCallback on_space_freed; PipeSideBrokenCallback on_pipe_broken; + FuriWait stdout_timeout; }; PipeSideBundle pipe_alloc(size_t capacity, size_t trigger_level) { @@ -52,12 +53,14 @@ PipeSideBundle pipe_alloc_ex(PipeSideReceiveSettings alice, PipeSideReceiveSetti .shared = shared, .sending = alice_to_bob, .receiving = bob_to_alice, + .stdout_timeout = FuriWaitForever, }; *bobs_side = (PipeSide){ .role = PipeRoleBob, .shared = shared, .sending = bob_to_alice, .receiving = alice_to_bob, + .stdout_timeout = FuriWaitForever, }; return (PipeSideBundle){.alices_side = alices_side, .bobs_side = bobs_side}; @@ -100,7 +103,8 @@ static void _pipe_stdout_cb(const char* data, size_t size, void* context) { furi_assert(context); PipeSide* pipe = context; while(size) { - size_t sent = pipe_send(pipe, data, size, FuriWaitForever); + size_t sent = pipe_send(pipe, data, size, pipe->stdout_timeout); + if(!sent) break; data += sent; size -= sent; } @@ -118,6 +122,11 @@ void pipe_install_as_stdio(PipeSide* pipe) { furi_thread_set_stdin_callback(_pipe_stdin_cb, pipe); } +void pipe_set_stdout_timeout(PipeSide* pipe, FuriWait timeout) { + furi_check(pipe); + pipe->stdout_timeout = timeout; +} + size_t pipe_receive(PipeSide* pipe, void* data, size_t length, FuriWait timeout) { furi_check(pipe); return furi_stream_buffer_receive(pipe->receiving, data, length, timeout); diff --git a/lib/toolbox/pipe.h b/lib/toolbox/pipe.h index df75f4c48..c05e60d0c 100644 --- a/lib/toolbox/pipe.h +++ b/lib/toolbox/pipe.h @@ -147,6 +147,16 @@ void pipe_free(PipeSide* pipe); */ void pipe_install_as_stdio(PipeSide* pipe); +/** + * @brief Sets the timeout for `stdout` write operations + * + * @note This value is set to `FuriWaitForever` when the pipe is created + * + * @param [in] pipe Pipe side to set the timeout of + * @param [in] timeout Timeout value in ticks + */ +void pipe_set_stdout_timeout(PipeSide* pipe, FuriWait timeout); + /** * @brief Receives data from the pipe. * diff --git a/targets/f18/api_symbols.csv b/targets/f18/api_symbols.csv index 4aeef3c9d..3da638149 100644 --- a/targets/f18/api_symbols.csv +++ b/targets/f18/api_symbols.csv @@ -1,5 +1,5 @@ entry,status,name,type,params -Version,+,81.1,, +Version,+,82.0,, Header,+,applications/services/bt/bt_service/bt.h,, Header,+,applications/services/bt/bt_service/bt_keys_storage.h,, Header,+,applications/services/cli/cli.h,, @@ -1666,8 +1666,8 @@ Function,+,furi_thread_get_return_code,int32_t,FuriThread* Function,+,furi_thread_get_signal_callback,FuriThreadSignalCallback,const FuriThread* Function,+,furi_thread_get_stack_space,uint32_t,FuriThreadId Function,+,furi_thread_get_state,FuriThreadState,FuriThread* -Function,+,furi_thread_get_stdin_callback,FuriThreadStdinReadCallback, -Function,+,furi_thread_get_stdout_callback,FuriThreadStdoutWriteCallback, +Function,+,furi_thread_get_stdin_callback,void,"FuriThreadStdinReadCallback*, void**" +Function,+,furi_thread_get_stdout_callback,void,"FuriThreadStdoutWriteCallback*, void**" Function,+,furi_thread_is_suspended,_Bool,FuriThreadId Function,+,furi_thread_join,_Bool,FuriThread* Function,+,furi_thread_list_alloc,FuriThreadList*, @@ -2333,6 +2333,7 @@ Function,+,pipe_set_broken_callback,void,"PipeSide*, PipeSideBrokenCallback, Fur Function,+,pipe_set_callback_context,void,"PipeSide*, void*" Function,+,pipe_set_data_arrived_callback,void,"PipeSide*, PipeSideDataArrivedCallback, FuriEventLoopEvent" Function,+,pipe_set_space_freed_callback,void,"PipeSide*, PipeSideSpaceFreedCallback, FuriEventLoopEvent" +Function,+,pipe_set_stdout_timeout,void,"PipeSide*, FuriWait" Function,+,pipe_spaces_available,size_t,PipeSide* Function,+,pipe_state,PipeState,PipeSide* Function,+,plugin_manager_alloc,PluginManager*,"const char*, uint32_t, const ElfApiInterface*" diff --git a/targets/f7/api_symbols.csv b/targets/f7/api_symbols.csv index 14eaa3142..9a87f08d4 100644 --- a/targets/f7/api_symbols.csv +++ b/targets/f7/api_symbols.csv @@ -1,5 +1,5 @@ entry,status,name,type,params -Version,+,81.1,, +Version,+,82.0,, Header,+,applications/drivers/subghz/cc1101_ext/cc1101_ext_interconnect.h,, Header,+,applications/services/bt/bt_service/bt.h,, Header,+,applications/services/bt/bt_service/bt_keys_storage.h,, @@ -1886,8 +1886,8 @@ Function,+,furi_thread_get_return_code,int32_t,FuriThread* Function,+,furi_thread_get_signal_callback,FuriThreadSignalCallback,const FuriThread* Function,+,furi_thread_get_stack_space,uint32_t,FuriThreadId Function,+,furi_thread_get_state,FuriThreadState,FuriThread* -Function,+,furi_thread_get_stdin_callback,FuriThreadStdinReadCallback, -Function,+,furi_thread_get_stdout_callback,FuriThreadStdoutWriteCallback, +Function,+,furi_thread_get_stdin_callback,void,"FuriThreadStdinReadCallback*, void**" +Function,+,furi_thread_get_stdout_callback,void,"FuriThreadStdoutWriteCallback*, void**" Function,+,furi_thread_is_suspended,_Bool,FuriThreadId Function,+,furi_thread_join,_Bool,FuriThread* Function,+,furi_thread_list_alloc,FuriThreadList*, @@ -2970,6 +2970,7 @@ Function,+,pipe_set_broken_callback,void,"PipeSide*, PipeSideBrokenCallback, Fur Function,+,pipe_set_callback_context,void,"PipeSide*, void*" Function,+,pipe_set_data_arrived_callback,void,"PipeSide*, PipeSideDataArrivedCallback, FuriEventLoopEvent" Function,+,pipe_set_space_freed_callback,void,"PipeSide*, PipeSideSpaceFreedCallback, FuriEventLoopEvent" +Function,+,pipe_set_stdout_timeout,void,"PipeSide*, FuriWait" Function,+,pipe_spaces_available,size_t,PipeSide* Function,+,pipe_state,PipeState,PipeSide* Function,+,plugin_manager_alloc,PluginManager*,"const char*, uint32_t, const ElfApiInterface*" From b0835220ac173fc45d5ddde399d910a424f53206 Mon Sep 17 00:00:00 2001 From: hedger Date: Sat, 22 Feb 2025 16:05:56 +0000 Subject: [PATCH 075/268] libs: stricter constness for saving RAM with .rodata section; fbt: sdk: fixed signature generation for nested const params --- applications/services/cli/cli.c | 2 +- applications/services/cli/cli.h | 2 +- applications/services/cli/cli_i.h | 2 +- applications/services/cli/cli_vcp.c | 2 +- applications/services/cli/cli_vcp.h | 2 +- .../dallas/protocol_group_dallas_defs.c | 2 +- .../dallas/protocol_group_dallas_defs.h | 2 +- .../protocols/misc/protocol_group_misc_defs.c | 2 +- .../protocols/misc/protocol_group_misc_defs.h | 2 +- lib/ibutton/protocols/protocol_group_defs.c | 2 +- lib/ibutton/protocols/protocol_group_defs.h | 2 +- lib/lfrfid/protocols/lfrfid_protocols.c | 2 +- lib/lfrfid/protocols/lfrfid_protocols.h | 2 +- lib/lfrfid/protocols/protocol_nexwatch.c | 2 +- .../mf_classic/mf_classic_listener.c | 18 +++++++-------- lib/nfc/protocols/nfc_device_defs.c | 2 +- lib/nfc/protocols/nfc_device_defs.h | 2 +- lib/nfc/protocols/nfc_listener_defs.c | 2 +- lib/nfc/protocols/nfc_listener_defs.h | 2 +- lib/nfc/protocols/nfc_poller_defs.c | 2 +- lib/nfc/protocols/nfc_poller_defs.h | 2 +- lib/subghz/protocols/protocol_items.c | 2 +- lib/subghz/registry.h | 2 +- lib/toolbox/protocols/protocol_dict.c | 4 ++-- lib/toolbox/protocols/protocol_dict.h | 2 +- scripts/fbt/sdk/collector.py | 2 +- targets/f18/api_symbols.csv | 18 +++++++-------- targets/f7/api_symbols.csv | 22 +++++++++---------- targets/f7/furi_hal/furi_hal_usb_u2f.c | 2 +- targets/furi_hal_include/furi_hal_usb.h | 2 +- 30 files changed, 57 insertions(+), 57 deletions(-) diff --git a/applications/services/cli/cli.c b/applications/services/cli/cli.c index 28ba417c2..c2a0b9cb1 100644 --- a/applications/services/cli/cli.c +++ b/applications/services/cli/cli.c @@ -424,7 +424,7 @@ void cli_delete_command(Cli* cli, const char* name) { furi_string_free(name_str); } -void cli_session_open(Cli* cli, void* session) { +void cli_session_open(Cli* cli, const void* session) { furi_check(cli); furi_check(furi_mutex_acquire(cli->mutex, FuriWaitForever) == FuriStatusOk); diff --git a/applications/services/cli/cli.h b/applications/services/cli/cli.h index bb84670a7..fe8f09032 100644 --- a/applications/services/cli/cli.h +++ b/applications/services/cli/cli.h @@ -123,7 +123,7 @@ char cli_getc(Cli* cli); */ void cli_nl(Cli* cli); -void cli_session_open(Cli* cli, void* session); +void cli_session_open(Cli* cli, const void* session); void cli_session_close(Cli* cli); diff --git a/applications/services/cli/cli_i.h b/applications/services/cli/cli_i.h index d7351b9ff..ca126dacd 100644 --- a/applications/services/cli/cli_i.h +++ b/applications/services/cli/cli_i.h @@ -50,7 +50,7 @@ struct Cli { FuriSemaphore* idle_sem; FuriString* last_line; FuriString* line; - CliSession* session; + const CliSession* session; size_t cursor_position; }; diff --git a/applications/services/cli/cli_vcp.c b/applications/services/cli/cli_vcp.c index aa399e78a..39802bd79 100644 --- a/applications/services/cli/cli_vcp.c +++ b/applications/services/cli/cli_vcp.c @@ -312,7 +312,7 @@ static bool cli_vcp_is_connected(void) { return vcp->connected; } -CliSession cli_vcp = { +const CliSession cli_vcp = { cli_vcp_init, cli_vcp_deinit, cli_vcp_rx, diff --git a/applications/services/cli/cli_vcp.h b/applications/services/cli/cli_vcp.h index 3aef2ef70..f51625342 100644 --- a/applications/services/cli/cli_vcp.h +++ b/applications/services/cli/cli_vcp.h @@ -11,7 +11,7 @@ extern "C" { typedef struct CliSession CliSession; -extern CliSession cli_vcp; +extern const CliSession cli_vcp; #ifdef __cplusplus } diff --git a/lib/ibutton/protocols/dallas/protocol_group_dallas_defs.c b/lib/ibutton/protocols/dallas/protocol_group_dallas_defs.c index b4dd51ce7..df758283b 100644 --- a/lib/ibutton/protocols/dallas/protocol_group_dallas_defs.c +++ b/lib/ibutton/protocols/dallas/protocol_group_dallas_defs.c @@ -6,7 +6,7 @@ #include "protocol_ds1971.h" #include "protocol_ds_generic.h" -const iButtonProtocolDallasBase* ibutton_protocols_dallas[] = { +const iButtonProtocolDallasBase* const ibutton_protocols_dallas[] = { [iButtonProtocolDS1990] = &ibutton_protocol_ds1990, [iButtonProtocolDS1992] = &ibutton_protocol_ds1992, [iButtonProtocolDS1996] = &ibutton_protocol_ds1996, diff --git a/lib/ibutton/protocols/dallas/protocol_group_dallas_defs.h b/lib/ibutton/protocols/dallas/protocol_group_dallas_defs.h index 2ba1dd39a..71571d91b 100644 --- a/lib/ibutton/protocols/dallas/protocol_group_dallas_defs.h +++ b/lib/ibutton/protocols/dallas/protocol_group_dallas_defs.h @@ -14,4 +14,4 @@ typedef enum { iButtonProtocolDSMax, } iButtonProtocolDallas; -extern const iButtonProtocolDallasBase* ibutton_protocols_dallas[]; +extern const iButtonProtocolDallasBase* const ibutton_protocols_dallas[]; diff --git a/lib/ibutton/protocols/misc/protocol_group_misc_defs.c b/lib/ibutton/protocols/misc/protocol_group_misc_defs.c index 09ae0bdc7..f8cae0463 100644 --- a/lib/ibutton/protocols/misc/protocol_group_misc_defs.c +++ b/lib/ibutton/protocols/misc/protocol_group_misc_defs.c @@ -3,7 +3,7 @@ #include "protocol_cyfral.h" #include "protocol_metakom.h" -const ProtocolBase* ibutton_protocols_misc[] = { +const ProtocolBase* const ibutton_protocols_misc[] = { [iButtonProtocolMiscCyfral] = &ibutton_protocol_misc_cyfral, [iButtonProtocolMiscMetakom] = &ibutton_protocol_misc_metakom, /* Add new misc protocols here */ diff --git a/lib/ibutton/protocols/misc/protocol_group_misc_defs.h b/lib/ibutton/protocols/misc/protocol_group_misc_defs.h index 0a7f92847..cde6b0aa9 100644 --- a/lib/ibutton/protocols/misc/protocol_group_misc_defs.h +++ b/lib/ibutton/protocols/misc/protocol_group_misc_defs.h @@ -8,4 +8,4 @@ typedef enum { iButtonProtocolMiscMax, } iButtonProtocolMisc; -extern const ProtocolBase* ibutton_protocols_misc[]; +extern const ProtocolBase* const ibutton_protocols_misc[]; diff --git a/lib/ibutton/protocols/protocol_group_defs.c b/lib/ibutton/protocols/protocol_group_defs.c index 40a360f0e..ac428f410 100644 --- a/lib/ibutton/protocols/protocol_group_defs.c +++ b/lib/ibutton/protocols/protocol_group_defs.c @@ -3,7 +3,7 @@ #include "dallas/protocol_group_dallas.h" #include "misc/protocol_group_misc.h" -const iButtonProtocolGroupBase* ibutton_protocol_groups[] = { +const iButtonProtocolGroupBase* const ibutton_protocol_groups[] = { [iButtonProtocolGroupDallas] = &ibutton_protocol_group_dallas, [iButtonProtocolGroupMisc] = &ibutton_protocol_group_misc, }; diff --git a/lib/ibutton/protocols/protocol_group_defs.h b/lib/ibutton/protocols/protocol_group_defs.h index 2d41e3cb8..2c00dfab4 100644 --- a/lib/ibutton/protocols/protocol_group_defs.h +++ b/lib/ibutton/protocols/protocol_group_defs.h @@ -8,4 +8,4 @@ typedef enum { iButtonProtocolGroupMax } iButtonProtocolGroup; -extern const iButtonProtocolGroupBase* ibutton_protocol_groups[]; +extern const iButtonProtocolGroupBase* const ibutton_protocol_groups[]; diff --git a/lib/lfrfid/protocols/lfrfid_protocols.c b/lib/lfrfid/protocols/lfrfid_protocols.c index 238f8e0cd..c6cc57a68 100644 --- a/lib/lfrfid/protocols/lfrfid_protocols.c +++ b/lib/lfrfid/protocols/lfrfid_protocols.c @@ -22,7 +22,7 @@ #include "protocol_gproxii.h" #include "protocol_noralsy.h" -const ProtocolBase* lfrfid_protocols[] = { +const ProtocolBase* const lfrfid_protocols[] = { [LFRFIDProtocolEM4100] = &protocol_em4100, [LFRFIDProtocolEM410032] = &protocol_em4100_32, [LFRFIDProtocolEM410016] = &protocol_em4100_16, diff --git a/lib/lfrfid/protocols/lfrfid_protocols.h b/lib/lfrfid/protocols/lfrfid_protocols.h index 86c31c60b..a33e87990 100644 --- a/lib/lfrfid/protocols/lfrfid_protocols.h +++ b/lib/lfrfid/protocols/lfrfid_protocols.h @@ -37,7 +37,7 @@ typedef enum { LFRFIDProtocolMax, } LFRFIDProtocol; -extern const ProtocolBase* lfrfid_protocols[]; +extern const ProtocolBase* const lfrfid_protocols[]; typedef enum { LFRFIDWriteTypeT5577, diff --git a/lib/lfrfid/protocols/protocol_nexwatch.c b/lib/lfrfid/protocols/protocol_nexwatch.c index a794a4a8e..08324803f 100644 --- a/lib/lfrfid/protocols/protocol_nexwatch.c +++ b/lib/lfrfid/protocols/protocol_nexwatch.c @@ -21,7 +21,7 @@ typedef struct { uint8_t chk; } ProtocolNexwatchMagic; -ProtocolNexwatchMagic magic_items[] = { +static ProtocolNexwatchMagic magic_items[] = { {0xBE, "Quadrakey", 0}, {0x88, "Nexkey", 0}, {0x86, "Honeywell", 0}}; diff --git a/lib/nfc/protocols/mf_classic/mf_classic_listener.c b/lib/nfc/protocols/mf_classic/mf_classic_listener.c index ef571117a..1f5eea271 100644 --- a/lib/nfc/protocols/mf_classic/mf_classic_listener.c +++ b/lib/nfc/protocols/mf_classic/mf_classic_listener.c @@ -19,7 +19,7 @@ typedef struct { uint8_t cmd_start_byte; size_t cmd_len_bits; size_t command_num; - MfClassicListenerCommandHandler* handler; + const MfClassicListenerCommandHandler* handler; } MfClassicListenerCmd; static void mf_classic_listener_prepare_emulation(MfClassicListener* instance) { @@ -441,42 +441,42 @@ static MfClassicListenerCommand return command; } -static MfClassicListenerCommandHandler mf_classic_listener_halt_handlers[] = { +static const MfClassicListenerCommandHandler mf_classic_listener_halt_handlers[] = { mf_classic_listener_halt_handler, }; -static MfClassicListenerCommandHandler mf_classic_listener_auth_key_a_handlers[] = { +static const MfClassicListenerCommandHandler mf_classic_listener_auth_key_a_handlers[] = { mf_classic_listener_auth_key_a_handler, mf_classic_listener_auth_second_part_handler, }; -static MfClassicListenerCommandHandler mf_classic_listener_auth_key_b_handlers[] = { +static const MfClassicListenerCommandHandler mf_classic_listener_auth_key_b_handlers[] = { mf_classic_listener_auth_key_b_handler, mf_classic_listener_auth_second_part_handler, }; -static MfClassicListenerCommandHandler mf_classic_listener_read_block_handlers[] = { +static const MfClassicListenerCommandHandler mf_classic_listener_read_block_handlers[] = { mf_classic_listener_read_block_handler, }; -static MfClassicListenerCommandHandler mf_classic_listener_write_block_handlers[] = { +static const MfClassicListenerCommandHandler mf_classic_listener_write_block_handlers[] = { mf_classic_listener_write_block_first_part_handler, mf_classic_listener_write_block_second_part_handler, }; -static MfClassicListenerCommandHandler mf_classic_listener_value_dec_handlers[] = { +static const MfClassicListenerCommandHandler mf_classic_listener_value_dec_handlers[] = { mf_classic_listener_value_dec_handler, mf_classic_listener_value_data_receive_handler, mf_classic_listener_value_transfer_handler, }; -static MfClassicListenerCommandHandler mf_classic_listener_value_inc_handlers[] = { +static const MfClassicListenerCommandHandler mf_classic_listener_value_inc_handlers[] = { mf_classic_listener_value_inc_handler, mf_classic_listener_value_data_receive_handler, mf_classic_listener_value_transfer_handler, }; -static MfClassicListenerCommandHandler mf_classic_listener_value_restore_handlers[] = { +static const MfClassicListenerCommandHandler mf_classic_listener_value_restore_handlers[] = { mf_classic_listener_value_restore_handler, mf_classic_listener_value_data_receive_handler, mf_classic_listener_value_transfer_handler, diff --git a/lib/nfc/protocols/nfc_device_defs.c b/lib/nfc/protocols/nfc_device_defs.c index 6a145445c..3f508a1fb 100644 --- a/lib/nfc/protocols/nfc_device_defs.c +++ b/lib/nfc/protocols/nfc_device_defs.c @@ -31,7 +31,7 @@ * When implementing a new protocol, add its implementation * here under its own index defined in nfc_protocol.h. */ -const NfcDeviceBase* nfc_devices[NfcProtocolNum] = { +const NfcDeviceBase* const nfc_devices[NfcProtocolNum] = { [NfcProtocolIso14443_3a] = &nfc_device_iso14443_3a, [NfcProtocolIso14443_3b] = &nfc_device_iso14443_3b, [NfcProtocolIso14443_4a] = &nfc_device_iso14443_4a, diff --git a/lib/nfc/protocols/nfc_device_defs.h b/lib/nfc/protocols/nfc_device_defs.h index f5ba2563f..e5d2707fd 100644 --- a/lib/nfc/protocols/nfc_device_defs.h +++ b/lib/nfc/protocols/nfc_device_defs.h @@ -6,7 +6,7 @@ extern "C" { #endif -extern const NfcDeviceBase* nfc_devices[]; +extern const NfcDeviceBase* const nfc_devices[]; #ifdef __cplusplus } diff --git a/lib/nfc/protocols/nfc_listener_defs.c b/lib/nfc/protocols/nfc_listener_defs.c index 2a6167e9c..c27079f5a 100644 --- a/lib/nfc/protocols/nfc_listener_defs.c +++ b/lib/nfc/protocols/nfc_listener_defs.c @@ -8,7 +8,7 @@ #include #include -const NfcListenerBase* nfc_listeners_api[NfcProtocolNum] = { +const NfcListenerBase* const nfc_listeners_api[NfcProtocolNum] = { [NfcProtocolIso14443_3a] = &nfc_listener_iso14443_3a, [NfcProtocolIso14443_3b] = NULL, [NfcProtocolIso14443_4a] = &nfc_listener_iso14443_4a, diff --git a/lib/nfc/protocols/nfc_listener_defs.h b/lib/nfc/protocols/nfc_listener_defs.h index 4d88cc098..7bd4b49b0 100644 --- a/lib/nfc/protocols/nfc_listener_defs.h +++ b/lib/nfc/protocols/nfc_listener_defs.h @@ -7,7 +7,7 @@ extern "C" { #endif -extern const NfcListenerBase* nfc_listeners_api[NfcProtocolNum]; +extern const NfcListenerBase* const nfc_listeners_api[NfcProtocolNum]; #ifdef __cplusplus } diff --git a/lib/nfc/protocols/nfc_poller_defs.c b/lib/nfc/protocols/nfc_poller_defs.c index c007740b7..f3ad15e7d 100644 --- a/lib/nfc/protocols/nfc_poller_defs.c +++ b/lib/nfc/protocols/nfc_poller_defs.c @@ -13,7 +13,7 @@ #include #include -const NfcPollerBase* nfc_pollers_api[NfcProtocolNum] = { +const NfcPollerBase* const nfc_pollers_api[NfcProtocolNum] = { [NfcProtocolIso14443_3a] = &nfc_poller_iso14443_3a, [NfcProtocolIso14443_3b] = &nfc_poller_iso14443_3b, [NfcProtocolIso14443_4a] = &nfc_poller_iso14443_4a, diff --git a/lib/nfc/protocols/nfc_poller_defs.h b/lib/nfc/protocols/nfc_poller_defs.h index a406a5f08..0156cc8b2 100644 --- a/lib/nfc/protocols/nfc_poller_defs.h +++ b/lib/nfc/protocols/nfc_poller_defs.h @@ -7,7 +7,7 @@ extern "C" { #endif -extern const NfcPollerBase* nfc_pollers_api[NfcProtocolNum]; +extern const NfcPollerBase* const nfc_pollers_api[NfcProtocolNum]; #ifdef __cplusplus } diff --git a/lib/subghz/protocols/protocol_items.c b/lib/subghz/protocols/protocol_items.c index 0d9c2a088..d7a14cd1c 100644 --- a/lib/subghz/protocols/protocol_items.c +++ b/lib/subghz/protocols/protocol_items.c @@ -1,6 +1,6 @@ #include "protocol_items.h" // IWYU pragma: keep -const SubGhzProtocol* subghz_protocol_registry_items[] = { +const SubGhzProtocol* const subghz_protocol_registry_items[] = { &subghz_protocol_gate_tx, &subghz_protocol_keeloq, &subghz_protocol_star_line, diff --git a/lib/subghz/registry.h b/lib/subghz/registry.h index 8529c1097..8de376b16 100644 --- a/lib/subghz/registry.h +++ b/lib/subghz/registry.h @@ -12,7 +12,7 @@ typedef struct SubGhzProtocolRegistry SubGhzProtocolRegistry; typedef struct SubGhzProtocol SubGhzProtocol; struct SubGhzProtocolRegistry { - const SubGhzProtocol** items; + const SubGhzProtocol* const* items; const size_t size; }; diff --git a/lib/toolbox/protocols/protocol_dict.c b/lib/toolbox/protocols/protocol_dict.c index 5680be18d..5cc46d6eb 100644 --- a/lib/toolbox/protocols/protocol_dict.c +++ b/lib/toolbox/protocols/protocol_dict.c @@ -2,12 +2,12 @@ #include "protocol_dict.h" struct ProtocolDict { - const ProtocolBase** base; + const ProtocolBase* const* base; size_t count; void* data[]; }; -ProtocolDict* protocol_dict_alloc(const ProtocolBase** protocols, size_t count) { +ProtocolDict* protocol_dict_alloc(const ProtocolBase* const* protocols, size_t count) { furi_check(protocols); ProtocolDict* dict = malloc(sizeof(ProtocolDict) + (sizeof(void*) * count)); diff --git a/lib/toolbox/protocols/protocol_dict.h b/lib/toolbox/protocols/protocol_dict.h index 543b3ead2..a7e02a988 100644 --- a/lib/toolbox/protocols/protocol_dict.h +++ b/lib/toolbox/protocols/protocol_dict.h @@ -12,7 +12,7 @@ typedef int32_t ProtocolId; #define PROTOCOL_NO (-1) #define PROTOCOL_ALL_FEATURES (0xFFFFFFFF) -ProtocolDict* protocol_dict_alloc(const ProtocolBase** protocols, size_t protocol_count); +ProtocolDict* protocol_dict_alloc(const ProtocolBase* const* protocols, size_t protocol_count); void protocol_dict_free(ProtocolDict* dict); diff --git a/scripts/fbt/sdk/collector.py b/scripts/fbt/sdk/collector.py index 5615f105e..14653eb44 100644 --- a/scripts/fbt/sdk/collector.py +++ b/scripts/fbt/sdk/collector.py @@ -113,7 +113,7 @@ def stringify_descr(type_descr): # Hack if isinstance(type_descr.ptr_to, FunctionType): return stringify_descr(type_descr.ptr_to) - return f"{stringify_descr(type_descr.ptr_to)}*" + return f"{stringify_descr(type_descr.ptr_to)}*{' const' if type_descr.const else ''}" elif isinstance(type_descr, Type): return ( f"{'const ' if type_descr.const else ''}" diff --git a/targets/f18/api_symbols.csv b/targets/f18/api_symbols.csv index 3da638149..a95ba0f5e 100644 --- a/targets/f18/api_symbols.csv +++ b/targets/f18/api_symbols.csv @@ -1,5 +1,5 @@ entry,status,name,type,params -Version,+,82.0,, +Version,+,83.0,, Header,+,applications/services/bt/bt_service/bt.h,, Header,+,applications/services/bt/bt_service/bt_keys_storage.h,, Header,+,applications/services/cli/cli.h,, @@ -785,7 +785,7 @@ Function,+,cli_print_usage,void,"const char*, const char*, const char*" Function,+,cli_read,size_t,"Cli*, uint8_t*, size_t" Function,+,cli_read_timeout,size_t,"Cli*, uint8_t*, size_t, uint32_t" Function,+,cli_session_close,void,Cli* -Function,+,cli_session_open,void,"Cli*, void*" +Function,+,cli_session_open,void,"Cli*, const void*" Function,+,cli_write,void,"Cli*, const uint8_t*, size_t" Function,+,composite_api_resolver_add,void,"CompositeApiResolver*, const ElfApiInterface*" Function,+,composite_api_resolver_alloc,CompositeApiResolver*, @@ -1733,7 +1733,7 @@ Function,-,getchar,int, Function,-,getchar_unlocked,int, Function,-,getenv,char*,const char* Function,-,gets,char*,char* -Function,-,getsubopt,int,"char**, char**, char**" +Function,-,getsubopt,int,"char**, char* const*, char**" Function,-,getw,int,FILE* Function,+,gui_add_framebuffer_callback,void,"Gui*, GuiCanvasCommitCallback, void*" Function,+,gui_add_view_port,void,"Gui*, ViewPort*, GuiLayer" @@ -2361,19 +2361,19 @@ Function,-,pow,double,"double, double" Function,-,pow10,double,double Function,-,pow10f,float,float Function,+,power_enable_low_battery_level_notification,void,"Power*, _Bool" +Function,+,power_enable_otg,void,"Power*, _Bool" Function,+,power_get_info,void,"Power*, PowerInfo*" Function,+,power_get_pubsub,FuriPubSub*,Power* Function,+,power_is_battery_healthy,_Bool,Power* Function,+,power_is_otg_enabled,_Bool,Power* Function,+,power_off,void,Power* Function,+,power_reboot,void,"Power*, PowerBootMode" -Function,+,power_enable_otg,void,"Power*, _Bool" Function,+,powf,float,"float, float" Function,-,powl,long double,"long double, long double" Function,+,pretty_format_bytes_hex_canonical,void,"FuriString*, size_t, const char*, const uint8_t*, size_t" Function,-,printf,int,"const char*, ..." Function,+,property_value_out,void,"PropertyValueContext*, const char*, unsigned int, ..." -Function,+,protocol_dict_alloc,ProtocolDict*,"const ProtocolBase**, size_t" +Function,+,protocol_dict_alloc,ProtocolDict*,"const ProtocolBase* const*, size_t" Function,+,protocol_dict_decoders_feed,ProtocolId,"ProtocolDict*, _Bool, uint32_t" Function,+,protocol_dict_decoders_feed_by_feature,ProtocolId,"ProtocolDict*, uint32_t, _Bool, uint32_t" Function,+,protocol_dict_decoders_feed_by_id,ProtocolId,"ProtocolDict*, size_t, _Bool, uint32_t" @@ -2932,13 +2932,13 @@ Variable,-,__stdio_exit_handler,void (*)(), Variable,+,_ctype_,const char[], Variable,+,_impure_data,_reent, Variable,+,_impure_ptr,_reent*, -Variable,-,_sys_errlist,const char*[], +Variable,-,_sys_errlist,const char* const[], Variable,-,_sys_nerr,int, Variable,-,ble_profile_hid,const FuriHalBleProfileTemplate*, Variable,-,ble_profile_serial,const FuriHalBleProfileTemplate*, -Variable,+,cli_vcp,CliSession, +Variable,+,cli_vcp,const CliSession, Variable,+,compress_config_heatshrink_default,const CompressConfigHeatshrink, -Variable,+,firmware_api_interface,const ElfApiInterface*, +Variable,+,firmware_api_interface,const ElfApiInterface* const, Variable,+,furi_hal_i2c_bus_external,FuriHalI2cBus, Variable,+,furi_hal_i2c_bus_power,FuriHalI2cBus, Variable,+,furi_hal_i2c_handle_external,FuriHalI2cBusHandle, @@ -3208,5 +3208,5 @@ Variable,+,usb_ccid,FuriHalUsbInterface, Variable,+,usb_cdc_dual,FuriHalUsbInterface, Variable,+,usb_cdc_single,FuriHalUsbInterface, Variable,+,usb_hid,FuriHalUsbInterface, -Variable,+,usb_hid_u2f,FuriHalUsbInterface, +Variable,+,usb_hid_u2f,const FuriHalUsbInterface, Variable,+,usbd_devfs,const usbd_driver, diff --git a/targets/f7/api_symbols.csv b/targets/f7/api_symbols.csv index 9a87f08d4..b09803e40 100644 --- a/targets/f7/api_symbols.csv +++ b/targets/f7/api_symbols.csv @@ -1,5 +1,5 @@ entry,status,name,type,params -Version,+,82.0,, +Version,+,83.0,, Header,+,applications/drivers/subghz/cc1101_ext/cc1101_ext_interconnect.h,, Header,+,applications/services/bt/bt_service/bt.h,, Header,+,applications/services/bt/bt_service/bt_keys_storage.h,, @@ -862,7 +862,7 @@ Function,+,cli_print_usage,void,"const char*, const char*, const char*" Function,+,cli_read,size_t,"Cli*, uint8_t*, size_t" Function,+,cli_read_timeout,size_t,"Cli*, uint8_t*, size_t, uint32_t" Function,+,cli_session_close,void,Cli* -Function,+,cli_session_open,void,"Cli*, void*" +Function,+,cli_session_open,void,"Cli*, const void*" Function,+,cli_write,void,"Cli*, const uint8_t*, size_t" Function,+,composite_api_resolver_add,void,"CompositeApiResolver*, const ElfApiInterface*" Function,+,composite_api_resolver_alloc,CompositeApiResolver*, @@ -1953,7 +1953,7 @@ Function,-,getchar,int, Function,-,getchar_unlocked,int, Function,-,getenv,char*,const char* Function,-,gets,char*,char* -Function,-,getsubopt,int,"char**, char**, char**" +Function,-,getsubopt,int,"char**, char* const*, char**" Function,-,getw,int,FILE* Function,+,gui_add_framebuffer_callback,void,"Gui*, GuiCanvasCommitCallback, void*" Function,+,gui_add_view_port,void,"Gui*, ViewPort*, GuiLayer" @@ -2655,7 +2655,7 @@ Function,+,mf_ultralight_3des_decrypt,void,"mbedtls_des3_context*, const uint8_t Function,+,mf_ultralight_3des_encrypt,void,"mbedtls_des3_context*, const uint8_t*, const uint8_t*, const uint8_t*, const uint8_t, uint8_t*" Function,+,mf_ultralight_3des_get_key,const uint8_t*,const MfUltralightData* Function,+,mf_ultralight_3des_key_valid,_Bool,const MfUltralightData* -Function,+,mf_ultralight_3des_shift_data,void,uint8_t* +Function,+,mf_ultralight_3des_shift_data,void,uint8_t* const Function,+,mf_ultralight_alloc,MfUltralightData*, Function,+,mf_ultralight_copy,void,"MfUltralightData*, const MfUltralightData*" Function,+,mf_ultralight_detect_protocol,_Bool,const Iso14443_3aData* @@ -2998,19 +2998,19 @@ Function,-,pow,double,"double, double" Function,-,pow10,double,double Function,-,pow10f,float,float Function,+,power_enable_low_battery_level_notification,void,"Power*, _Bool" +Function,+,power_enable_otg,void,"Power*, _Bool" Function,+,power_get_info,void,"Power*, PowerInfo*" Function,+,power_get_pubsub,FuriPubSub*,Power* Function,+,power_is_battery_healthy,_Bool,Power* Function,+,power_is_otg_enabled,_Bool,Power* Function,+,power_off,void,Power* Function,+,power_reboot,void,"Power*, PowerBootMode" -Function,+,power_enable_otg,void,"Power*, _Bool" Function,+,powf,float,"float, float" Function,-,powl,long double,"long double, long double" Function,+,pretty_format_bytes_hex_canonical,void,"FuriString*, size_t, const char*, const uint8_t*, size_t" Function,-,printf,int,"const char*, ..." Function,+,property_value_out,void,"PropertyValueContext*, const char*, unsigned int, ..." -Function,+,protocol_dict_alloc,ProtocolDict*,"const ProtocolBase**, size_t" +Function,+,protocol_dict_alloc,ProtocolDict*,"const ProtocolBase* const*, size_t" Function,+,protocol_dict_decoders_feed,ProtocolId,"ProtocolDict*, _Bool, uint32_t" Function,+,protocol_dict_decoders_feed_by_feature,ProtocolId,"ProtocolDict*, uint32_t, _Bool, uint32_t" Function,+,protocol_dict_decoders_feed_by_id,ProtocolId,"ProtocolDict*, size_t, _Bool, uint32_t" @@ -3783,13 +3783,13 @@ Variable,-,__stdio_exit_handler,void (*)(), Variable,+,_ctype_,const char[], Variable,+,_impure_data,_reent, Variable,+,_impure_ptr,_reent*, -Variable,-,_sys_errlist,const char*[], +Variable,-,_sys_errlist,const char* const[], Variable,-,_sys_nerr,int, Variable,-,ble_profile_hid,const FuriHalBleProfileTemplate*, Variable,-,ble_profile_serial,const FuriHalBleProfileTemplate*, -Variable,+,cli_vcp,CliSession, +Variable,+,cli_vcp,const CliSession, Variable,+,compress_config_heatshrink_default,const CompressConfigHeatshrink, -Variable,+,firmware_api_interface,const ElfApiInterface*, +Variable,+,firmware_api_interface,const ElfApiInterface* const, Variable,+,furi_hal_i2c_bus_external,FuriHalI2cBus, Variable,+,furi_hal_i2c_bus_power,FuriHalI2cBus, Variable,+,furi_hal_i2c_handle_external,FuriHalI2cBusHandle, @@ -3858,7 +3858,7 @@ Variable,+,gpio_usb_dp,const GpioPin, Variable,+,gpio_vibro,const GpioPin, Variable,+,input_pins,const InputPin[], Variable,+,input_pins_count,const size_t, -Variable,+,lfrfid_protocols,const ProtocolBase*[], +Variable,+,lfrfid_protocols,const ProtocolBase* const[], Variable,+,message_blink_set_color_blue,const NotificationMessage, Variable,+,message_blink_set_color_cyan,const NotificationMessage, Variable,+,message_blink_set_color_green,const NotificationMessage, @@ -4079,5 +4079,5 @@ Variable,+,usb_ccid,FuriHalUsbInterface, Variable,+,usb_cdc_dual,FuriHalUsbInterface, Variable,+,usb_cdc_single,FuriHalUsbInterface, Variable,+,usb_hid,FuriHalUsbInterface, -Variable,+,usb_hid_u2f,FuriHalUsbInterface, +Variable,+,usb_hid_u2f,const FuriHalUsbInterface, Variable,+,usbd_devfs,const usbd_driver, diff --git a/targets/f7/furi_hal/furi_hal_usb_u2f.c b/targets/f7/furi_hal/furi_hal_usb_u2f.c index 05f498376..82aeaf6d7 100644 --- a/targets/f7/furi_hal/furi_hal_usb_u2f.c +++ b/targets/f7/furi_hal/furi_hal_usb_u2f.c @@ -174,7 +174,7 @@ void furi_hal_hid_u2f_set_callback(HidU2fCallback cb, void* ctx) { } } -FuriHalUsbInterface usb_hid_u2f = { +const FuriHalUsbInterface usb_hid_u2f = { .init = hid_u2f_init, .deinit = hid_u2f_deinit, .wakeup = hid_u2f_on_wakeup, diff --git a/targets/furi_hal_include/furi_hal_usb.h b/targets/furi_hal_include/furi_hal_usb.h index 213e8d56f..dc4e3eaad 100644 --- a/targets/furi_hal_include/furi_hal_usb.h +++ b/targets/furi_hal_include/furi_hal_usb.h @@ -27,7 +27,7 @@ struct FuriHalUsbInterface { extern FuriHalUsbInterface usb_cdc_single; extern FuriHalUsbInterface usb_cdc_dual; extern FuriHalUsbInterface usb_hid; -extern FuriHalUsbInterface usb_hid_u2f; +extern const FuriHalUsbInterface usb_hid_u2f; extern FuriHalUsbInterface usb_ccid; typedef enum { From b99f65dd9a6a51acb234a790ec8533ad64a8af9b Mon Sep 17 00:00:00 2001 From: hedger Date: Sat, 22 Feb 2025 16:27:04 +0000 Subject: [PATCH 076/268] hal: additional fixes for constness in USB subsystem --- applications/main/bad_usb/bad_usb_app_i.h | 2 +- applications/main/u2f/u2f_hid.c | 2 +- applications/services/cli/cli_vcp.c | 2 +- applications/system/hid_app/hid.c | 2 +- applications/system/js_app/modules/js_badusb.c | 2 +- targets/f18/api_symbols.csv | 4 ++-- targets/f7/api_symbols.csv | 4 ++-- targets/f7/furi_hal/furi_hal_usb.c | 16 ++++++++-------- targets/f7/furi_hal/furi_hal_usb_ccid.c | 4 ++-- targets/f7/furi_hal/furi_hal_usb_cdc.c | 8 +++++--- targets/f7/furi_hal/furi_hal_usb_hid.c | 4 ++-- targets/f7/furi_hal/furi_hal_usb_u2f.c | 4 ++-- targets/furi_hal_include/furi_hal_usb.h | 6 +++--- 13 files changed, 31 insertions(+), 29 deletions(-) diff --git a/applications/main/bad_usb/bad_usb_app_i.h b/applications/main/bad_usb/bad_usb_app_i.h index b34bd5de6..93c24ea8c 100644 --- a/applications/main/bad_usb/bad_usb_app_i.h +++ b/applications/main/bad_usb/bad_usb_app_i.h @@ -44,7 +44,7 @@ struct BadUsbApp { BadUsbScript* bad_usb_script; BadUsbHidInterface interface; - FuriHalUsbInterface* usb_if_prev; + const FuriHalUsbInterface* usb_if_prev; }; typedef enum { diff --git a/applications/main/u2f/u2f_hid.c b/applications/main/u2f/u2f_hid.c index 76d3d7cec..a1b614b19 100644 --- a/applications/main/u2f/u2f_hid.c +++ b/applications/main/u2f/u2f_hid.c @@ -189,7 +189,7 @@ static int32_t u2f_hid_worker(void* context) { FURI_LOG_D(WORKER_TAG, "Init"); - FuriHalUsbInterface* usb_mode_prev = furi_hal_usb_get_config(); + const FuriHalUsbInterface* usb_mode_prev = furi_hal_usb_get_config(); furi_check(furi_hal_usb_set_config(&usb_hid_u2f, NULL) == true); u2f_hid->lock_timer = diff --git a/applications/services/cli/cli_vcp.c b/applications/services/cli/cli_vcp.c index 39802bd79..bc4ea592a 100644 --- a/applications/services/cli/cli_vcp.c +++ b/applications/services/cli/cli_vcp.c @@ -40,7 +40,7 @@ typedef struct { volatile bool connected; volatile bool running; - FuriHalUsbInterface* usb_if_prev; + const FuriHalUsbInterface* usb_if_prev; uint8_t data_buffer[USB_CDC_PKT_LEN]; } CliVcp; diff --git a/applications/system/hid_app/hid.c b/applications/system/hid_app/hid.c index 15c49e3b0..9f3a246ef 100644 --- a/applications/system/hid_app/hid.c +++ b/applications/system/hid_app/hid.c @@ -174,7 +174,7 @@ int32_t hid_usb_app(void* p) { FURI_LOG_D("HID", "Starting as USB app"); - FuriHalUsbInterface* usb_mode_prev = furi_hal_usb_get_config(); + const FuriHalUsbInterface* usb_mode_prev = furi_hal_usb_get_config(); furi_hal_usb_unlock(); furi_check(furi_hal_usb_set_config(&usb_hid, NULL) == true); diff --git a/applications/system/js_app/modules/js_badusb.c b/applications/system/js_app/modules/js_badusb.c index 27f38cbda..880bbdc48 100644 --- a/applications/system/js_app/modules/js_badusb.c +++ b/applications/system/js_app/modules/js_badusb.c @@ -7,7 +7,7 @@ typedef struct { FuriHalUsbHidConfig* hid_cfg; uint16_t layout[128]; - FuriHalUsbInterface* usb_if_prev; + const FuriHalUsbInterface* usb_if_prev; uint8_t key_hold_cnt; } JsBadusbInst; diff --git a/targets/f18/api_symbols.csv b/targets/f18/api_symbols.csv index a95ba0f5e..27d0fe38c 100644 --- a/targets/f18/api_symbols.csv +++ b/targets/f18/api_symbols.csv @@ -1489,12 +1489,12 @@ Function,+,furi_hal_usb_ccid_remove_smartcard,void, Function,+,furi_hal_usb_ccid_set_callbacks,void,"CcidCallbacks*, void*" Function,+,furi_hal_usb_disable,void, Function,+,furi_hal_usb_enable,void, -Function,+,furi_hal_usb_get_config,FuriHalUsbInterface*, +Function,+,furi_hal_usb_get_config,const FuriHalUsbInterface*, Function,-,furi_hal_usb_init,void, Function,+,furi_hal_usb_is_locked,_Bool, Function,+,furi_hal_usb_lock,void, Function,+,furi_hal_usb_reinit,void, -Function,+,furi_hal_usb_set_config,_Bool,"FuriHalUsbInterface*, void*" +Function,+,furi_hal_usb_set_config,_Bool,"const FuriHalUsbInterface*, void*" Function,-,furi_hal_usb_set_state_callback,void,"FuriHalUsbStateCallback, void*" Function,+,furi_hal_usb_unlock,void, Function,+,furi_hal_version_do_i_belong_here,_Bool, diff --git a/targets/f7/api_symbols.csv b/targets/f7/api_symbols.csv index b09803e40..b9e555339 100644 --- a/targets/f7/api_symbols.csv +++ b/targets/f7/api_symbols.csv @@ -1709,12 +1709,12 @@ Function,+,furi_hal_usb_ccid_remove_smartcard,void, Function,+,furi_hal_usb_ccid_set_callbacks,void,"CcidCallbacks*, void*" Function,+,furi_hal_usb_disable,void, Function,+,furi_hal_usb_enable,void, -Function,+,furi_hal_usb_get_config,FuriHalUsbInterface*, +Function,+,furi_hal_usb_get_config,const FuriHalUsbInterface*, Function,-,furi_hal_usb_init,void, Function,+,furi_hal_usb_is_locked,_Bool, Function,+,furi_hal_usb_lock,void, Function,+,furi_hal_usb_reinit,void, -Function,+,furi_hal_usb_set_config,_Bool,"FuriHalUsbInterface*, void*" +Function,+,furi_hal_usb_set_config,_Bool,"const FuriHalUsbInterface*, void*" Function,-,furi_hal_usb_set_state_callback,void,"FuriHalUsbStateCallback, void*" Function,+,furi_hal_usb_unlock,void, Function,+,furi_hal_version_do_i_belong_here,_Bool, diff --git a/targets/f7/furi_hal/furi_hal_usb.c b/targets/f7/furi_hal/furi_hal_usb.c index 22d1523b6..03e92cc16 100644 --- a/targets/f7/furi_hal/furi_hal_usb.c +++ b/targets/f7/furi_hal/furi_hal_usb.c @@ -32,7 +32,7 @@ typedef struct { } UsbApiEventDataStateCallback; typedef struct { - FuriHalUsbInterface* interface; + const FuriHalUsbInterface* interface; void* context; } UsbApiEventDataInterface; @@ -43,7 +43,7 @@ typedef union { typedef union { bool bool_value; - void* void_value; + const void* void_value; } UsbApiEventReturnData; typedef struct { @@ -60,7 +60,7 @@ typedef struct { bool connected; bool mode_lock; bool request_pending; - FuriHalUsbInterface* interface; + const FuriHalUsbInterface* interface; void* interface_context; FuriHalUsbStateCallback callback; void* callback_context; @@ -132,7 +132,7 @@ static void furi_hal_usb_send_message(UsbApiEventMessage* message) { api_lock_wait_unlock_and_free(message->lock); } -bool furi_hal_usb_set_config(FuriHalUsbInterface* new_if, void* ctx) { +bool furi_hal_usb_set_config(const FuriHalUsbInterface* new_if, void* ctx) { UsbApiEventReturnData return_data = { .bool_value = false, }; @@ -152,7 +152,7 @@ bool furi_hal_usb_set_config(FuriHalUsbInterface* new_if, void* ctx) { return return_data.bool_value; } -FuriHalUsbInterface* furi_hal_usb_get_config(void) { +const FuriHalUsbInterface* furi_hal_usb_get_config(void) { UsbApiEventReturnData return_data = { .void_value = NULL, }; @@ -326,7 +326,7 @@ static void wkup_evt(usbd_device* dev, uint8_t event, uint8_t ep) { } } -static void usb_process_mode_start(FuriHalUsbInterface* interface, void* context) { +static void usb_process_mode_start(const FuriHalUsbInterface* interface, void* context) { if(usb.interface != NULL) { usb.interface->deinit(&udev); } @@ -344,7 +344,7 @@ static void usb_process_mode_start(FuriHalUsbInterface* interface, void* context } } -static void usb_process_mode_change(FuriHalUsbInterface* interface, void* context) { +static void usb_process_mode_change(const FuriHalUsbInterface* interface, void* context) { if((interface != usb.interface) || (context != usb.interface_context)) { if(usb.enabled) { // Disable current interface @@ -374,7 +374,7 @@ static void usb_process_mode_reinit(void) { usb_process_mode_start(usb.interface, usb.interface_context); } -static bool usb_process_set_config(FuriHalUsbInterface* interface, void* context) { +static bool usb_process_set_config(const FuriHalUsbInterface* interface, void* context) { if(usb.mode_lock) { return false; } else { diff --git a/targets/f7/furi_hal/furi_hal_usb_ccid.c b/targets/f7/furi_hal/furi_hal_usb_ccid.c index a2c1a0583..5725c70cf 100644 --- a/targets/f7/furi_hal/furi_hal_usb_ccid.c +++ b/targets/f7/furi_hal/furi_hal_usb_ccid.c @@ -170,7 +170,7 @@ static const struct CcidConfigDescriptor ccid_cfg_desc = { }, }; -static void ccid_init(usbd_device* dev, FuriHalUsbInterface* intf, void* ctx); +static void ccid_init(usbd_device* dev, const FuriHalUsbInterface* intf, void* ctx); static void ccid_deinit(usbd_device* dev); static void ccid_on_wakeup(usbd_device* dev); static void ccid_on_suspend(usbd_device* dev); @@ -223,7 +223,7 @@ static void* ccid_set_string_descr(char* str) { return dev_str_desc; } -static void ccid_init(usbd_device* dev, FuriHalUsbInterface* intf, void* ctx) { +static void ccid_init(usbd_device* dev, const FuriHalUsbInterface* intf, void* ctx) { UNUSED(intf); FuriHalUsbCcidConfig* cfg = (FuriHalUsbCcidConfig*)ctx; diff --git a/targets/f7/furi_hal/furi_hal_usb_cdc.c b/targets/f7/furi_hal/furi_hal_usb_cdc.c index f9c1d3a42..62c43ea64 100644 --- a/targets/f7/furi_hal/furi_hal_usb_cdc.c +++ b/targets/f7/furi_hal/furi_hal_usb_cdc.c @@ -385,7 +385,7 @@ static const struct CdcConfigDescriptorDual static struct usb_cdc_line_coding cdc_config[IF_NUM_MAX] = {}; static uint8_t cdc_ctrl_line_state[IF_NUM_MAX]; -static void cdc_init(usbd_device* dev, FuriHalUsbInterface* intf, void* ctx); +static void cdc_init(usbd_device* dev, const FuriHalUsbInterface* intf, void* ctx); static void cdc_deinit(usbd_device* dev); static void cdc_on_wakeup(usbd_device* dev); static void cdc_on_suspend(usbd_device* dev); @@ -429,10 +429,11 @@ FuriHalUsbInterface usb_cdc_dual = { .cfg_descr = (void*)&cdc_cfg_desc_dual, }; -static void cdc_init(usbd_device* dev, FuriHalUsbInterface* intf, void* ctx) { +static void cdc_init(usbd_device* dev, const FuriHalUsbInterface* intf, void* ctx) { UNUSED(ctx); usb_dev = dev; - cdc_if_cur = intf; + cdc_if_cur = malloc(sizeof(FuriHalUsbInterface)); + memcpy((void*)cdc_if_cur, intf, sizeof(FuriHalUsbInterface)); char* name = (char*)furi_hal_version_get_device_name_ptr(); uint8_t len = (name == NULL) ? (0) : (strlen(name)); @@ -469,6 +470,7 @@ static void cdc_deinit(usbd_device* dev) { free(cdc_if_cur->str_prod_descr); free(cdc_if_cur->str_serial_descr); + free((void*)cdc_if_cur); cdc_if_cur = NULL; } diff --git a/targets/f7/furi_hal/furi_hal_usb_hid.c b/targets/f7/furi_hal/furi_hal_usb_hid.c index c83261226..cad8f040b 100644 --- a/targets/f7/furi_hal/furi_hal_usb_hid.c +++ b/targets/f7/furi_hal/furi_hal_usb_hid.c @@ -226,7 +226,7 @@ static struct HidReport { struct HidReportConsumer consumer; } FURI_PACKED hid_report; -static void hid_init(usbd_device* dev, FuriHalUsbInterface* intf, void* ctx); +static void hid_init(usbd_device* dev, const FuriHalUsbInterface* intf, void* ctx); static void hid_deinit(usbd_device* dev); static void hid_on_wakeup(usbd_device* dev); static void hid_on_suspend(usbd_device* dev); @@ -374,7 +374,7 @@ static void* hid_set_string_descr(char* str) { return dev_str_desc; } -static void hid_init(usbd_device* dev, FuriHalUsbInterface* intf, void* ctx) { +static void hid_init(usbd_device* dev, const FuriHalUsbInterface* intf, void* ctx) { UNUSED(intf); FuriHalUsbHidConfig* cfg = (FuriHalUsbHidConfig*)ctx; if(hid_semaphore == NULL) hid_semaphore = furi_semaphore_alloc(1, 1); diff --git a/targets/f7/furi_hal/furi_hal_usb_u2f.c b/targets/f7/furi_hal/furi_hal_usb_u2f.c index 82aeaf6d7..2c0ad7694 100644 --- a/targets/f7/furi_hal/furi_hal_usb_u2f.c +++ b/targets/f7/furi_hal/furi_hal_usb_u2f.c @@ -137,7 +137,7 @@ static const struct HidConfigDescriptor hid_u2f_cfg_desc = { }, }; -static void hid_u2f_init(usbd_device* dev, FuriHalUsbInterface* intf, void* ctx); +static void hid_u2f_init(usbd_device* dev, const FuriHalUsbInterface* intf, void* ctx); static void hid_u2f_deinit(usbd_device* dev); static void hid_u2f_on_wakeup(usbd_device* dev); static void hid_u2f_on_suspend(usbd_device* dev); @@ -189,7 +189,7 @@ const FuriHalUsbInterface usb_hid_u2f = { .cfg_descr = (void*)&hid_u2f_cfg_desc, }; -static void hid_u2f_init(usbd_device* dev, FuriHalUsbInterface* intf, void* ctx) { +static void hid_u2f_init(usbd_device* dev, const FuriHalUsbInterface* intf, void* ctx) { UNUSED(intf); UNUSED(ctx); if(hid_u2f_semaphore == NULL) { diff --git a/targets/furi_hal_include/furi_hal_usb.h b/targets/furi_hal_include/furi_hal_usb.h index dc4e3eaad..03c5247c5 100644 --- a/targets/furi_hal_include/furi_hal_usb.h +++ b/targets/furi_hal_include/furi_hal_usb.h @@ -9,7 +9,7 @@ extern "C" { typedef struct FuriHalUsbInterface FuriHalUsbInterface; struct FuriHalUsbInterface { - void (*init)(usbd_device* dev, FuriHalUsbInterface* intf, void* ctx); + void (*init)(usbd_device* dev, const FuriHalUsbInterface* intf, void* ctx); void (*deinit)(usbd_device* dev); void (*wakeup)(usbd_device* dev); void (*suspend)(usbd_device* dev); @@ -49,13 +49,13 @@ void furi_hal_usb_init(void); * @param ctx context passed to device mode init function * @return true - mode switch started, false - mode switch is locked */ -bool furi_hal_usb_set_config(FuriHalUsbInterface* new_if, void* ctx); +bool furi_hal_usb_set_config(const FuriHalUsbInterface* new_if, void* ctx); /** Get USB device configuration * * @return current USB device mode */ -FuriHalUsbInterface* furi_hal_usb_get_config(void); +const FuriHalUsbInterface* furi_hal_usb_get_config(void); /** Lock USB device mode switch */ From 5d7b63b162b47bd7ebb92d8aebcb1cde414440a9 Mon Sep 17 00:00:00 2001 From: hedger Date: Sat, 22 Feb 2025 16:35:22 +0000 Subject: [PATCH 077/268] debug apps: additional usb-related fixes --- applications/debug/ccid_test/ccid_test_app.c | 2 +- applications/debug/usb_mouse/usb_mouse.c | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/applications/debug/ccid_test/ccid_test_app.c b/applications/debug/ccid_test/ccid_test_app.c index 4158c1a60..a02fd1596 100644 --- a/applications/debug/ccid_test/ccid_test_app.c +++ b/applications/debug/ccid_test/ccid_test_app.c @@ -117,7 +117,7 @@ int32_t ccid_test_app(void* p) { //setup view CcidTestApp* app = ccid_test_app_alloc(); - FuriHalUsbInterface* usb_mode_prev = furi_hal_usb_get_config(); + const FuriHalUsbInterface* usb_mode_prev = furi_hal_usb_get_config(); furi_hal_usb_unlock(); furi_check(furi_hal_usb_set_config(&usb_ccid, &app->ccid_cfg) == true); diff --git a/applications/debug/usb_mouse/usb_mouse.c b/applications/debug/usb_mouse/usb_mouse.c index e322a58ae..7d5ee27e8 100644 --- a/applications/debug/usb_mouse/usb_mouse.c +++ b/applications/debug/usb_mouse/usb_mouse.c @@ -42,7 +42,7 @@ int32_t usb_mouse_app(void* p) { FuriMessageQueue* event_queue = furi_message_queue_alloc(8, sizeof(UsbMouseEvent)); ViewPort* view_port = view_port_alloc(); - FuriHalUsbInterface* usb_mode_prev = furi_hal_usb_get_config(); + const FuriHalUsbInterface* usb_mode_prev = furi_hal_usb_get_config(); furi_hal_usb_unlock(); furi_check(furi_hal_usb_set_config(&usb_hid, NULL) == true); From 7d000abfc4c5008864022ea1810a719cf2c30b57 Mon Sep 17 00:00:00 2001 From: hedger Date: Sat, 22 Feb 2025 18:15:37 +0000 Subject: [PATCH 078/268] mjs: more consts for token parser --- lib/mjs/mjs_parser.c | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/mjs/mjs_parser.c b/lib/mjs/mjs_parser.c index 212804a86..98f60459d 100644 --- a/lib/mjs/mjs_parser.c +++ b/lib/mjs/mjs_parser.c @@ -47,7 +47,7 @@ static int ptest(struct pstate* p) { return tok; } -static int s_unary_ops[] = { +static const int s_unary_ops[] = { TOK_NOT, TOK_TILDA, TOK_PLUS_PLUS, @@ -56,10 +56,10 @@ static int s_unary_ops[] = { TOK_MINUS, TOK_PLUS, TOK_EOF}; -static int s_comparison_ops[] = {TOK_LT, TOK_LE, TOK_GT, TOK_GE, TOK_EOF}; -static int s_postfix_ops[] = {TOK_PLUS_PLUS, TOK_MINUS_MINUS, TOK_EOF}; -static int s_equality_ops[] = {TOK_EQ, TOK_NE, TOK_EQ_EQ, TOK_NE_NE, TOK_EOF}; -static int s_assign_ops[] = { +static const int s_comparison_ops[] = {TOK_LT, TOK_LE, TOK_GT, TOK_GE, TOK_EOF}; +static const int s_postfix_ops[] = {TOK_PLUS_PLUS, TOK_MINUS_MINUS, TOK_EOF}; +static const int s_equality_ops[] = {TOK_EQ, TOK_NE, TOK_EQ_EQ, TOK_NE_NE, TOK_EOF}; +static const int s_assign_ops[] = { TOK_ASSIGN, TOK_PLUS_ASSIGN, TOK_MINUS_ASSIGN, @@ -74,7 +74,7 @@ static int s_assign_ops[] = { TOK_OR_ASSIGN, TOK_EOF}; -static int findtok(int* toks, int tok) { +static int findtok(int const* toks, int tok) { int i = 0; while(tok != toks[i] && toks[i] != TOK_EOF) i++; From bf84daf0d9076debfe34086728533bc019d86784 Mon Sep 17 00:00:00 2001 From: hedger Date: Sat, 22 Feb 2025 18:28:37 +0000 Subject: [PATCH 079/268] fatfs: const driver struct --- targets/f7/fatfs/user_diskio.c | 2 +- targets/f7/fatfs/user_diskio.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/targets/f7/fatfs/user_diskio.c b/targets/f7/fatfs/user_diskio.c index 85e5cad4f..963cb595f 100644 --- a/targets/f7/fatfs/user_diskio.c +++ b/targets/f7/fatfs/user_diskio.c @@ -9,7 +9,7 @@ static DRESULT driver_read(BYTE pdrv, BYTE* buff, DWORD sector, UINT count); static DRESULT driver_write(BYTE pdrv, const BYTE* buff, DWORD sector, UINT count); static DRESULT driver_ioctl(BYTE pdrv, BYTE cmd, void* buff); -Diskio_drvTypeDef sd_fatfs_driver = { +const Diskio_drvTypeDef sd_fatfs_driver = { driver_initialize, driver_status, driver_read, diff --git a/targets/f7/fatfs/user_diskio.h b/targets/f7/fatfs/user_diskio.h index 009a17d4b..2505de704 100644 --- a/targets/f7/fatfs/user_diskio.h +++ b/targets/f7/fatfs/user_diskio.h @@ -6,7 +6,7 @@ extern "C" { #include "fatfs/ff_gen_drv.h" -extern Diskio_drvTypeDef sd_fatfs_driver; +extern const Diskio_drvTypeDef sd_fatfs_driver; #ifdef __cplusplus } From d7221f1b0dc4fec7fa6fe4a8d9959a34a55dbb7b Mon Sep 17 00:00:00 2001 From: hedger Date: Sat, 22 Feb 2025 18:39:52 +0000 Subject: [PATCH 080/268] hal: more consts for ble & nfc vars --- targets/f18/api_symbols.csv | 2 +- targets/f7/api_symbols.csv | 2 +- targets/f7/ble_glue/profiles/serial_profile.c | 4 ++-- targets/f7/ble_glue/profiles/serial_profile.h | 2 +- targets/f7/furi_hal/furi_hal_nfc.c | 2 +- targets/f7/furi_hal/furi_hal_nfc_tech_i.h | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/targets/f18/api_symbols.csv b/targets/f18/api_symbols.csv index 27d0fe38c..2d6cbf2f0 100644 --- a/targets/f18/api_symbols.csv +++ b/targets/f18/api_symbols.csv @@ -2935,7 +2935,7 @@ Variable,+,_impure_ptr,_reent*, Variable,-,_sys_errlist,const char* const[], Variable,-,_sys_nerr,int, Variable,-,ble_profile_hid,const FuriHalBleProfileTemplate*, -Variable,-,ble_profile_serial,const FuriHalBleProfileTemplate*, +Variable,+,ble_profile_serial,const FuriHalBleProfileTemplate* const, Variable,+,cli_vcp,const CliSession, Variable,+,compress_config_heatshrink_default,const CompressConfigHeatshrink, Variable,+,firmware_api_interface,const ElfApiInterface* const, diff --git a/targets/f7/api_symbols.csv b/targets/f7/api_symbols.csv index b9e555339..4346547f3 100644 --- a/targets/f7/api_symbols.csv +++ b/targets/f7/api_symbols.csv @@ -3786,7 +3786,7 @@ Variable,+,_impure_ptr,_reent*, Variable,-,_sys_errlist,const char* const[], Variable,-,_sys_nerr,int, Variable,-,ble_profile_hid,const FuriHalBleProfileTemplate*, -Variable,-,ble_profile_serial,const FuriHalBleProfileTemplate*, +Variable,+,ble_profile_serial,const FuriHalBleProfileTemplate* const, Variable,+,cli_vcp,const CliSession, Variable,+,compress_config_heatshrink_default,const CompressConfigHeatshrink, Variable,+,firmware_api_interface,const ElfApiInterface* const, diff --git a/targets/f7/ble_glue/profiles/serial_profile.c b/targets/f7/ble_glue/profiles/serial_profile.c index 1d414889f..427539427 100644 --- a/targets/f7/ble_glue/profiles/serial_profile.c +++ b/targets/f7/ble_glue/profiles/serial_profile.c @@ -46,7 +46,7 @@ static void ble_profile_serial_stop(FuriHalBleProfileBase* profile) { // Up to 45 ms #define CONNECTION_INTERVAL_MAX (0x24) -static GapConfig serial_template_config = { +static const GapConfig serial_template_config = { .adv_service_uuid = 0x3080, .appearance_char = 0x8600, .bonding_mode = true, @@ -80,7 +80,7 @@ static const FuriHalBleProfileTemplate profile_callbacks = { .get_gap_config = ble_profile_serial_get_config, }; -const FuriHalBleProfileTemplate* ble_profile_serial = &profile_callbacks; +const FuriHalBleProfileTemplate* const ble_profile_serial = &profile_callbacks; void ble_profile_serial_set_event_callback( FuriHalBleProfileBase* profile, diff --git a/targets/f7/ble_glue/profiles/serial_profile.h b/targets/f7/ble_glue/profiles/serial_profile.h index e07eaef03..7938bfc33 100644 --- a/targets/f7/ble_glue/profiles/serial_profile.h +++ b/targets/f7/ble_glue/profiles/serial_profile.h @@ -19,7 +19,7 @@ typedef enum { typedef SerialServiceEventCallback FuriHalBtSerialCallback; /** Serial profile descriptor */ -extern const FuriHalBleProfileTemplate* ble_profile_serial; +extern const FuriHalBleProfileTemplate* const ble_profile_serial; /** Send data through BLE * diff --git a/targets/f7/furi_hal/furi_hal_nfc.c b/targets/f7/furi_hal/furi_hal_nfc.c index ee7a04e45..9d336c508 100644 --- a/targets/f7/furi_hal/furi_hal_nfc.c +++ b/targets/f7/furi_hal/furi_hal_nfc.c @@ -8,7 +8,7 @@ #define TAG "FuriHalNfc" -const FuriHalNfcTechBase* furi_hal_nfc_tech[FuriHalNfcTechNum] = { +const FuriHalNfcTechBase* const furi_hal_nfc_tech[FuriHalNfcTechNum] = { [FuriHalNfcTechIso14443a] = &furi_hal_nfc_iso14443a, [FuriHalNfcTechIso14443b] = &furi_hal_nfc_iso14443b, [FuriHalNfcTechIso15693] = &furi_hal_nfc_iso15693, diff --git a/targets/f7/furi_hal/furi_hal_nfc_tech_i.h b/targets/f7/furi_hal/furi_hal_nfc_tech_i.h index e36dc852e..a2a75aa66 100644 --- a/targets/f7/furi_hal/furi_hal_nfc_tech_i.h +++ b/targets/f7/furi_hal/furi_hal_nfc_tech_i.h @@ -160,7 +160,7 @@ extern const FuriHalNfcTechBase furi_hal_nfc_felica; * This variable is defined in furi_hal_nfc.c. It will need to be modified * in case when a new technology is to be added. */ -extern const FuriHalNfcTechBase* furi_hal_nfc_tech[]; +extern const FuriHalNfcTechBase* const furi_hal_nfc_tech[]; #ifdef __cplusplus } From 251565e9d3f7135943a9be198f579972aeac6212 Mon Sep 17 00:00:00 2001 From: hedger Date: Sat, 22 Feb 2025 19:12:12 +0000 Subject: [PATCH 081/268] hal: made FuriHalSpiBusHandle static --- .../drivers/subghz/cc1101_ext/cc1101_ext.c | 2 +- lib/drivers/cc1101.c | 48 ++++++------ lib/drivers/cc1101.h | 45 +++++------ lib/drivers/st25r3916.c | 8 +- lib/drivers/st25r3916.h | 8 +- lib/drivers/st25r3916_reg.c | 49 +++++++----- lib/drivers/st25r3916_reg.h | 47 +++++++----- targets/f18/api_symbols.csv | 70 +++++++++--------- targets/f18/furi_hal/furi_hal_spi_config.c | 22 +++--- targets/f18/furi_hal/furi_hal_spi_config.h | 8 +- targets/f7/api_symbols.csv | 74 +++++++++---------- targets/f7/furi_hal/furi_hal_nfc.c | 48 ++++++------ targets/f7/furi_hal/furi_hal_nfc_event.c | 4 +- targets/f7/furi_hal/furi_hal_nfc_felica.c | 16 ++-- targets/f7/furi_hal/furi_hal_nfc_i.h | 12 +-- targets/f7/furi_hal/furi_hal_nfc_irq.c | 2 +- targets/f7/furi_hal/furi_hal_nfc_iso14443a.c | 26 +++---- targets/f7/furi_hal/furi_hal_nfc_iso14443b.c | 6 +- targets/f7/furi_hal/furi_hal_nfc_iso15693.c | 26 ++++--- targets/f7/furi_hal/furi_hal_nfc_tech_i.h | 10 +-- targets/f7/furi_hal/furi_hal_sd.c | 2 +- targets/f7/furi_hal/furi_hal_spi.c | 18 ++--- targets/f7/furi_hal/furi_hal_spi_config.c | 32 ++++---- targets/f7/furi_hal/furi_hal_spi_config.h | 12 +-- targets/f7/furi_hal/furi_hal_spi_types.h | 4 +- targets/furi_hal_include/furi_hal_spi.h | 16 ++-- 26 files changed, 321 insertions(+), 294 deletions(-) diff --git a/applications/drivers/subghz/cc1101_ext/cc1101_ext.c b/applications/drivers/subghz/cc1101_ext/cc1101_ext.c index ae3556396..357214505 100644 --- a/applications/drivers/subghz/cc1101_ext/cc1101_ext.c +++ b/applications/drivers/subghz/cc1101_ext/cc1101_ext.c @@ -85,7 +85,7 @@ typedef struct { volatile SubGhzDeviceCC1101ExtState state; volatile SubGhzDeviceCC1101ExtRegulation regulation; const GpioPin* async_mirror_pin; - FuriHalSpiBusHandle* spi_bus_handle; + const FuriHalSpiBusHandle* spi_bus_handle; const GpioPin* g0_pin; SubGhzDeviceCC1101ExtAsyncTx async_tx; SubGhzDeviceCC1101ExtAsyncRx async_rx; diff --git a/lib/drivers/cc1101.c b/lib/drivers/cc1101.c index 40b286a9b..ff2f0d610 100644 --- a/lib/drivers/cc1101.c +++ b/lib/drivers/cc1101.c @@ -3,7 +3,8 @@ #include #include -static bool cc1101_spi_trx(FuriHalSpiBusHandle* handle, uint8_t* tx, uint8_t* rx, uint8_t size) { +static bool + cc1101_spi_trx(const FuriHalSpiBusHandle* handle, uint8_t* tx, uint8_t* rx, uint8_t size) { FuriHalCortexTimer timer = furi_hal_cortex_timer_get(CC1101_TIMEOUT * 1000); while(furi_hal_gpio_read(handle->miso)) { @@ -16,7 +17,7 @@ static bool cc1101_spi_trx(FuriHalSpiBusHandle* handle, uint8_t* tx, uint8_t* rx return true; } -CC1101Status cc1101_strobe(FuriHalSpiBusHandle* handle, uint8_t strobe) { +CC1101Status cc1101_strobe(const FuriHalSpiBusHandle* handle, uint8_t strobe) { uint8_t tx[1] = {strobe}; CC1101Status rx[1] = {0}; rx[0].CHIP_RDYn = 1; @@ -27,7 +28,7 @@ CC1101Status cc1101_strobe(FuriHalSpiBusHandle* handle, uint8_t strobe) { return rx[0]; } -CC1101Status cc1101_write_reg(FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t data) { +CC1101Status cc1101_write_reg(const FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t data) { uint8_t tx[2] = {reg, data}; CC1101Status rx[2] = {0}; rx[0].CHIP_RDYn = 1; @@ -39,7 +40,7 @@ CC1101Status cc1101_write_reg(FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t return rx[1]; } -CC1101Status cc1101_read_reg(FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t* data) { +CC1101Status cc1101_read_reg(const FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t* data) { assert(sizeof(CC1101Status) == 1); uint8_t tx[2] = {reg | CC1101_READ, 0}; CC1101Status rx[2] = {0}; @@ -52,33 +53,36 @@ CC1101Status cc1101_read_reg(FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t* return rx[0]; } -uint8_t cc1101_get_partnumber(FuriHalSpiBusHandle* handle) { +uint8_t cc1101_get_partnumber(const FuriHalSpiBusHandle* handle) { uint8_t partnumber = 0; cc1101_read_reg(handle, CC1101_STATUS_PARTNUM | CC1101_BURST, &partnumber); return partnumber; } -uint8_t cc1101_get_version(FuriHalSpiBusHandle* handle) { +uint8_t cc1101_get_version(const FuriHalSpiBusHandle* handle) { uint8_t version = 0; cc1101_read_reg(handle, CC1101_STATUS_VERSION | CC1101_BURST, &version); return version; } -uint8_t cc1101_get_rssi(FuriHalSpiBusHandle* handle) { +uint8_t cc1101_get_rssi(const FuriHalSpiBusHandle* handle) { uint8_t rssi = 0; cc1101_read_reg(handle, CC1101_STATUS_RSSI | CC1101_BURST, &rssi); return rssi; } -CC1101Status cc1101_reset(FuriHalSpiBusHandle* handle) { +CC1101Status cc1101_reset(const FuriHalSpiBusHandle* handle) { return cc1101_strobe(handle, CC1101_STROBE_SRES); } -CC1101Status cc1101_get_status(FuriHalSpiBusHandle* handle) { +CC1101Status cc1101_get_status(const FuriHalSpiBusHandle* handle) { return cc1101_strobe(handle, CC1101_STROBE_SNOP); } -bool cc1101_wait_status_state(FuriHalSpiBusHandle* handle, CC1101State state, uint32_t timeout_us) { +bool cc1101_wait_status_state( + const FuriHalSpiBusHandle* handle, + CC1101State state, + uint32_t timeout_us) { bool result = false; CC1101Status status = {0}; FuriHalCortexTimer timer = furi_hal_cortex_timer_get(timeout_us); @@ -92,35 +96,35 @@ bool cc1101_wait_status_state(FuriHalSpiBusHandle* handle, CC1101State state, ui return result; } -CC1101Status cc1101_shutdown(FuriHalSpiBusHandle* handle) { +CC1101Status cc1101_shutdown(const FuriHalSpiBusHandle* handle) { return cc1101_strobe(handle, CC1101_STROBE_SPWD); } -CC1101Status cc1101_calibrate(FuriHalSpiBusHandle* handle) { +CC1101Status cc1101_calibrate(const FuriHalSpiBusHandle* handle) { return cc1101_strobe(handle, CC1101_STROBE_SCAL); } -CC1101Status cc1101_switch_to_idle(FuriHalSpiBusHandle* handle) { +CC1101Status cc1101_switch_to_idle(const FuriHalSpiBusHandle* handle) { return cc1101_strobe(handle, CC1101_STROBE_SIDLE); } -CC1101Status cc1101_switch_to_rx(FuriHalSpiBusHandle* handle) { +CC1101Status cc1101_switch_to_rx(const FuriHalSpiBusHandle* handle) { return cc1101_strobe(handle, CC1101_STROBE_SRX); } -CC1101Status cc1101_switch_to_tx(FuriHalSpiBusHandle* handle) { +CC1101Status cc1101_switch_to_tx(const FuriHalSpiBusHandle* handle) { return cc1101_strobe(handle, CC1101_STROBE_STX); } -CC1101Status cc1101_flush_rx(FuriHalSpiBusHandle* handle) { +CC1101Status cc1101_flush_rx(const FuriHalSpiBusHandle* handle) { return cc1101_strobe(handle, CC1101_STROBE_SFRX); } -CC1101Status cc1101_flush_tx(FuriHalSpiBusHandle* handle) { +CC1101Status cc1101_flush_tx(const FuriHalSpiBusHandle* handle) { return cc1101_strobe(handle, CC1101_STROBE_SFTX); } -uint32_t cc1101_set_frequency(FuriHalSpiBusHandle* handle, uint32_t value) { +uint32_t cc1101_set_frequency(const FuriHalSpiBusHandle* handle, uint32_t value) { uint64_t real_value = (uint64_t)value * CC1101_FDIV / CC1101_QUARTZ; // Sanity check @@ -135,7 +139,7 @@ uint32_t cc1101_set_frequency(FuriHalSpiBusHandle* handle, uint32_t value) { return (uint32_t)real_frequency; } -uint32_t cc1101_set_intermediate_frequency(FuriHalSpiBusHandle* handle, uint32_t value) { +uint32_t cc1101_set_intermediate_frequency(const FuriHalSpiBusHandle* handle, uint32_t value) { uint64_t real_value = value * CC1101_IFDIV / CC1101_QUARTZ; assert((real_value & 0xFF) == real_value); @@ -146,7 +150,7 @@ uint32_t cc1101_set_intermediate_frequency(FuriHalSpiBusHandle* handle, uint32_t return (uint32_t)real_frequency; } -void cc1101_set_pa_table(FuriHalSpiBusHandle* handle, const uint8_t value[8]) { +void cc1101_set_pa_table(const FuriHalSpiBusHandle* handle, const uint8_t value[8]) { uint8_t tx[9] = {CC1101_PATABLE | CC1101_BURST}; //-V1009 CC1101Status rx[9] = {0}; rx[0].CHIP_RDYn = 1; @@ -159,7 +163,7 @@ void cc1101_set_pa_table(FuriHalSpiBusHandle* handle, const uint8_t value[8]) { assert((rx[0].CHIP_RDYn | rx[8].CHIP_RDYn) == 0); } -uint8_t cc1101_write_fifo(FuriHalSpiBusHandle* handle, const uint8_t* data, uint8_t size) { +uint8_t cc1101_write_fifo(const FuriHalSpiBusHandle* handle, const uint8_t* data, uint8_t size) { uint8_t buff_tx[64]; uint8_t buff_rx[64]; buff_tx[0] = CC1101_FIFO | CC1101_BURST; @@ -170,7 +174,7 @@ uint8_t cc1101_write_fifo(FuriHalSpiBusHandle* handle, const uint8_t* data, uint return size; } -uint8_t cc1101_read_fifo(FuriHalSpiBusHandle* handle, uint8_t* data, uint8_t* size) { +uint8_t cc1101_read_fifo(const FuriHalSpiBusHandle* handle, uint8_t* data, uint8_t* size) { uint8_t buff_trx[2]; buff_trx[0] = CC1101_FIFO | CC1101_READ | CC1101_BURST; diff --git a/lib/drivers/cc1101.h b/lib/drivers/cc1101.h index c8c552bec..2828f1cdf 100644 --- a/lib/drivers/cc1101.h +++ b/lib/drivers/cc1101.h @@ -19,7 +19,7 @@ extern "C" { * * @return device status */ -CC1101Status cc1101_strobe(FuriHalSpiBusHandle* handle, uint8_t strobe); +CC1101Status cc1101_strobe(const FuriHalSpiBusHandle* handle, uint8_t strobe); /** Write device register * @@ -29,7 +29,7 @@ CC1101Status cc1101_strobe(FuriHalSpiBusHandle* handle, uint8_t strobe); * * @return device status */ -CC1101Status cc1101_write_reg(FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t data); +CC1101Status cc1101_write_reg(const FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t data); /** Read device register * @@ -39,7 +39,7 @@ CC1101Status cc1101_write_reg(FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t * * @return device status */ -CC1101Status cc1101_read_reg(FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t* data); +CC1101Status cc1101_read_reg(const FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t* data); /* High level API */ @@ -49,7 +49,7 @@ CC1101Status cc1101_read_reg(FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t* * * @return CC1101Status structure */ -CC1101Status cc1101_reset(FuriHalSpiBusHandle* handle); +CC1101Status cc1101_reset(const FuriHalSpiBusHandle* handle); /** Get status * @@ -57,7 +57,7 @@ CC1101Status cc1101_reset(FuriHalSpiBusHandle* handle); * * @return CC1101Status structure */ -CC1101Status cc1101_get_status(FuriHalSpiBusHandle* handle); +CC1101Status cc1101_get_status(const FuriHalSpiBusHandle* handle); /** Wait specific chip state * @@ -67,7 +67,10 @@ CC1101Status cc1101_get_status(FuriHalSpiBusHandle* handle); * * @return true on success, false otherwise */ -bool cc1101_wait_status_state(FuriHalSpiBusHandle* handle, CC1101State state, uint32_t timeout_us); +bool cc1101_wait_status_state( + const FuriHalSpiBusHandle* handle, + CC1101State state, + uint32_t timeout_us); /** Enable shutdown mode * @@ -75,7 +78,7 @@ bool cc1101_wait_status_state(FuriHalSpiBusHandle* handle, CC1101State state, ui * * @return CC1101Status structure */ -CC1101Status cc1101_shutdown(FuriHalSpiBusHandle* handle); +CC1101Status cc1101_shutdown(const FuriHalSpiBusHandle* handle); /** Get Partnumber * @@ -83,7 +86,7 @@ CC1101Status cc1101_shutdown(FuriHalSpiBusHandle* handle); * * @return part number id */ -uint8_t cc1101_get_partnumber(FuriHalSpiBusHandle* handle); +uint8_t cc1101_get_partnumber(const FuriHalSpiBusHandle* handle); /** Get Version * @@ -91,7 +94,7 @@ uint8_t cc1101_get_partnumber(FuriHalSpiBusHandle* handle); * * @return version */ -uint8_t cc1101_get_version(FuriHalSpiBusHandle* handle); +uint8_t cc1101_get_version(const FuriHalSpiBusHandle* handle); /** Get raw RSSI value * @@ -99,7 +102,7 @@ uint8_t cc1101_get_version(FuriHalSpiBusHandle* handle); * * @return rssi value */ -uint8_t cc1101_get_rssi(FuriHalSpiBusHandle* handle); +uint8_t cc1101_get_rssi(const FuriHalSpiBusHandle* handle); /** Calibrate oscillator * @@ -107,13 +110,13 @@ uint8_t cc1101_get_rssi(FuriHalSpiBusHandle* handle); * * @return CC1101Status structure */ -CC1101Status cc1101_calibrate(FuriHalSpiBusHandle* handle); +CC1101Status cc1101_calibrate(const FuriHalSpiBusHandle* handle); /** Switch to idle * * @param handle - pointer to FuriHalSpiHandle */ -CC1101Status cc1101_switch_to_idle(FuriHalSpiBusHandle* handle); +CC1101Status cc1101_switch_to_idle(const FuriHalSpiBusHandle* handle); /** Switch to RX * @@ -121,7 +124,7 @@ CC1101Status cc1101_switch_to_idle(FuriHalSpiBusHandle* handle); * * @return CC1101Status structure */ -CC1101Status cc1101_switch_to_rx(FuriHalSpiBusHandle* handle); +CC1101Status cc1101_switch_to_rx(const FuriHalSpiBusHandle* handle); /** Switch to TX * @@ -129,7 +132,7 @@ CC1101Status cc1101_switch_to_rx(FuriHalSpiBusHandle* handle); * * @return CC1101Status structure */ -CC1101Status cc1101_switch_to_tx(FuriHalSpiBusHandle* handle); +CC1101Status cc1101_switch_to_tx(const FuriHalSpiBusHandle* handle); /** Flush RX FIFO * @@ -137,13 +140,13 @@ CC1101Status cc1101_switch_to_tx(FuriHalSpiBusHandle* handle); * * @return CC1101Status structure */ -CC1101Status cc1101_flush_rx(FuriHalSpiBusHandle* handle); +CC1101Status cc1101_flush_rx(const FuriHalSpiBusHandle* handle); /** Flush TX FIFO * * @param handle - pointer to FuriHalSpiHandle */ -CC1101Status cc1101_flush_tx(FuriHalSpiBusHandle* handle); +CC1101Status cc1101_flush_tx(const FuriHalSpiBusHandle* handle); /** Set Frequency * @@ -152,7 +155,7 @@ CC1101Status cc1101_flush_tx(FuriHalSpiBusHandle* handle); * * @return real frequency that were synthesized */ -uint32_t cc1101_set_frequency(FuriHalSpiBusHandle* handle, uint32_t value); +uint32_t cc1101_set_frequency(const FuriHalSpiBusHandle* handle, uint32_t value); /** Set Intermediate Frequency * @@ -161,14 +164,14 @@ uint32_t cc1101_set_frequency(FuriHalSpiBusHandle* handle, uint32_t value); * * @return real inermediate frequency that were synthesized */ -uint32_t cc1101_set_intermediate_frequency(FuriHalSpiBusHandle* handle, uint32_t value); +uint32_t cc1101_set_intermediate_frequency(const FuriHalSpiBusHandle* handle, uint32_t value); /** Set Power Amplifier level table, ramp * * @param handle - pointer to FuriHalSpiHandle * @param value - array of power level values */ -void cc1101_set_pa_table(FuriHalSpiBusHandle* handle, const uint8_t value[8]); +void cc1101_set_pa_table(const FuriHalSpiBusHandle* handle, const uint8_t value[8]); /** Write FIFO * @@ -178,7 +181,7 @@ void cc1101_set_pa_table(FuriHalSpiBusHandle* handle, const uint8_t value[8]); * * @return size, written bytes count */ -uint8_t cc1101_write_fifo(FuriHalSpiBusHandle* handle, const uint8_t* data, uint8_t size); +uint8_t cc1101_write_fifo(const FuriHalSpiBusHandle* handle, const uint8_t* data, uint8_t size); /** Read FIFO * @@ -188,7 +191,7 @@ uint8_t cc1101_write_fifo(FuriHalSpiBusHandle* handle, const uint8_t* data, uint * * @return size, read bytes count */ -uint8_t cc1101_read_fifo(FuriHalSpiBusHandle* handle, uint8_t* data, uint8_t* size); +uint8_t cc1101_read_fifo(const FuriHalSpiBusHandle* handle, uint8_t* data, uint8_t* size); #ifdef __cplusplus } diff --git a/lib/drivers/st25r3916.c b/lib/drivers/st25r3916.c index f8dc9a5eb..0721a52c7 100644 --- a/lib/drivers/st25r3916.c +++ b/lib/drivers/st25r3916.c @@ -2,7 +2,7 @@ #include -void st25r3916_mask_irq(FuriHalSpiBusHandle* handle, uint32_t mask) { +void st25r3916_mask_irq(const FuriHalSpiBusHandle* handle, uint32_t mask) { furi_assert(handle); uint8_t irq_mask_regs[4] = { @@ -14,7 +14,7 @@ void st25r3916_mask_irq(FuriHalSpiBusHandle* handle, uint32_t mask) { st25r3916_write_burst_regs(handle, ST25R3916_REG_IRQ_MASK_MAIN, irq_mask_regs, 4); } -uint32_t st25r3916_get_irq(FuriHalSpiBusHandle* handle) { +uint32_t st25r3916_get_irq(const FuriHalSpiBusHandle* handle) { furi_assert(handle); uint8_t irq_regs[4] = {}; @@ -32,7 +32,7 @@ uint32_t st25r3916_get_irq(FuriHalSpiBusHandle* handle) { return irq; } -void st25r3916_write_fifo(FuriHalSpiBusHandle* handle, const uint8_t* buff, size_t bits) { +void st25r3916_write_fifo(const FuriHalSpiBusHandle* handle, const uint8_t* buff, size_t bits) { furi_assert(handle); furi_assert(buff); @@ -45,7 +45,7 @@ void st25r3916_write_fifo(FuriHalSpiBusHandle* handle, const uint8_t* buff, size } bool st25r3916_read_fifo( - FuriHalSpiBusHandle* handle, + const FuriHalSpiBusHandle* handle, uint8_t* buff, size_t buff_size, size_t* buff_bits) { diff --git a/lib/drivers/st25r3916.h b/lib/drivers/st25r3916.h index 0e77b6317..3eddaa430 100644 --- a/lib/drivers/st25r3916.h +++ b/lib/drivers/st25r3916.h @@ -75,7 +75,7 @@ extern "C" { * @param handle - pointer to FuriHalSpiBusHandle instance * @param mask - mask of interrupts to be disabled */ -void st25r3916_mask_irq(FuriHalSpiBusHandle* handle, uint32_t mask); +void st25r3916_mask_irq(const FuriHalSpiBusHandle* handle, uint32_t mask); /** Get st25r3916 interrupts * @@ -83,7 +83,7 @@ void st25r3916_mask_irq(FuriHalSpiBusHandle* handle, uint32_t mask); * * @return received interrupts */ -uint32_t st25r3916_get_irq(FuriHalSpiBusHandle* handle); +uint32_t st25r3916_get_irq(const FuriHalSpiBusHandle* handle); /** Write FIFO * @@ -91,7 +91,7 @@ uint32_t st25r3916_get_irq(FuriHalSpiBusHandle* handle); * @param buff - buffer to write to FIFO * @param bits - number of bits to write */ -void st25r3916_write_fifo(FuriHalSpiBusHandle* handle, const uint8_t* buff, size_t bits); +void st25r3916_write_fifo(const FuriHalSpiBusHandle* handle, const uint8_t* buff, size_t bits); /** Read FIFO * @@ -103,7 +103,7 @@ void st25r3916_write_fifo(FuriHalSpiBusHandle* handle, const uint8_t* buff, size * @return true if read success, false otherwise */ bool st25r3916_read_fifo( - FuriHalSpiBusHandle* handle, + const FuriHalSpiBusHandle* handle, uint8_t* buff, size_t buff_size, size_t* buff_bits); diff --git a/lib/drivers/st25r3916_reg.c b/lib/drivers/st25r3916_reg.c index f7a47d463..cdbf0fd3d 100644 --- a/lib/drivers/st25r3916_reg.c +++ b/lib/drivers/st25r3916_reg.c @@ -28,18 +28,18 @@ (ST25R3916_CMD_LEN + \ ST25R3916_FIFO_DEPTH) /*!< ST25R3916 communication buffer: CMD + FIFO length */ -static void st25r3916_reg_tx_byte(FuriHalSpiBusHandle* handle, uint8_t byte) { +static void st25r3916_reg_tx_byte(const FuriHalSpiBusHandle* handle, uint8_t byte) { uint8_t val = byte; furi_hal_spi_bus_tx(handle, &val, 1, 5); } -void st25r3916_read_reg(FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t* val) { +void st25r3916_read_reg(const FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t* val) { furi_check(handle); st25r3916_read_burst_regs(handle, reg, val, 1); } void st25r3916_read_burst_regs( - FuriHalSpiBusHandle* handle, + const FuriHalSpiBusHandle* handle, uint8_t reg_start, uint8_t* values, uint8_t length) { @@ -59,14 +59,14 @@ void st25r3916_read_burst_regs( furi_hal_gpio_write(handle->cs, true); } -void st25r3916_write_reg(FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t val) { +void st25r3916_write_reg(const FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t val) { furi_check(handle); uint8_t reg_val = val; st25r3916_write_burst_regs(handle, reg, ®_val, 1); } void st25r3916_write_burst_regs( - FuriHalSpiBusHandle* handle, + const FuriHalSpiBusHandle* handle, uint8_t reg_start, const uint8_t* values, uint8_t length) { @@ -86,7 +86,10 @@ void st25r3916_write_burst_regs( furi_hal_gpio_write(handle->cs, true); } -void st25r3916_reg_write_fifo(FuriHalSpiBusHandle* handle, const uint8_t* buff, size_t length) { +void st25r3916_reg_write_fifo( + const FuriHalSpiBusHandle* handle, + const uint8_t* buff, + size_t length) { furi_check(handle); furi_check(buff); furi_check(length); @@ -98,7 +101,7 @@ void st25r3916_reg_write_fifo(FuriHalSpiBusHandle* handle, const uint8_t* buff, furi_hal_gpio_write(handle->cs, true); } -void st25r3916_reg_read_fifo(FuriHalSpiBusHandle* handle, uint8_t* buff, size_t length) { +void st25r3916_reg_read_fifo(const FuriHalSpiBusHandle* handle, uint8_t* buff, size_t length) { furi_check(handle); furi_check(buff); furi_check(length); @@ -110,7 +113,10 @@ void st25r3916_reg_read_fifo(FuriHalSpiBusHandle* handle, uint8_t* buff, size_t furi_hal_gpio_write(handle->cs, true); } -void st25r3916_write_pta_mem(FuriHalSpiBusHandle* handle, const uint8_t* values, size_t length) { +void st25r3916_write_pta_mem( + const FuriHalSpiBusHandle* handle, + const uint8_t* values, + size_t length) { furi_check(handle); furi_check(values); furi_check(length); @@ -122,7 +128,7 @@ void st25r3916_write_pta_mem(FuriHalSpiBusHandle* handle, const uint8_t* values, furi_hal_gpio_write(handle->cs, true); } -void st25r3916_read_pta_mem(FuriHalSpiBusHandle* handle, uint8_t* buff, size_t length) { +void st25r3916_read_pta_mem(const FuriHalSpiBusHandle* handle, uint8_t* buff, size_t length) { furi_check(handle); furi_check(buff); furi_check(length); @@ -136,7 +142,10 @@ void st25r3916_read_pta_mem(FuriHalSpiBusHandle* handle, uint8_t* buff, size_t l memcpy(buff, tmp_buff + 1, length); } -void st25r3916_write_ptf_mem(FuriHalSpiBusHandle* handle, const uint8_t* values, size_t length) { +void st25r3916_write_ptf_mem( + const FuriHalSpiBusHandle* handle, + const uint8_t* values, + size_t length) { furi_check(handle); furi_check(values); @@ -146,7 +155,7 @@ void st25r3916_write_ptf_mem(FuriHalSpiBusHandle* handle, const uint8_t* values, furi_hal_gpio_write(handle->cs, true); } -void st25r3916_write_pttsn_mem(FuriHalSpiBusHandle* handle, uint8_t* buff, size_t length) { +void st25r3916_write_pttsn_mem(const FuriHalSpiBusHandle* handle, uint8_t* buff, size_t length) { furi_check(handle); furi_check(buff); @@ -156,7 +165,7 @@ void st25r3916_write_pttsn_mem(FuriHalSpiBusHandle* handle, uint8_t* buff, size_ furi_hal_gpio_write(handle->cs, true); } -void st25r3916_direct_cmd(FuriHalSpiBusHandle* handle, uint8_t cmd) { +void st25r3916_direct_cmd(const FuriHalSpiBusHandle* handle, uint8_t cmd) { furi_check(handle); furi_hal_gpio_write(handle->cs, false); @@ -164,7 +173,7 @@ void st25r3916_direct_cmd(FuriHalSpiBusHandle* handle, uint8_t cmd) { furi_hal_gpio_write(handle->cs, true); } -void st25r3916_read_test_reg(FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t* val) { +void st25r3916_read_test_reg(const FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t* val) { furi_check(handle); furi_hal_gpio_write(handle->cs, false); @@ -174,7 +183,7 @@ void st25r3916_read_test_reg(FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t* furi_hal_gpio_write(handle->cs, true); } -void st25r3916_write_test_reg(FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t val) { +void st25r3916_write_test_reg(const FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t val) { furi_check(handle); furi_hal_gpio_write(handle->cs, false); @@ -184,7 +193,7 @@ void st25r3916_write_test_reg(FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t furi_hal_gpio_write(handle->cs, true); } -void st25r3916_clear_reg_bits(FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t clr_mask) { +void st25r3916_clear_reg_bits(const FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t clr_mask) { furi_check(handle); uint8_t reg_val = 0; @@ -195,7 +204,7 @@ void st25r3916_clear_reg_bits(FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t } } -void st25r3916_set_reg_bits(FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t set_mask) { +void st25r3916_set_reg_bits(const FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t set_mask) { furi_check(handle); uint8_t reg_val = 0; @@ -207,7 +216,7 @@ void st25r3916_set_reg_bits(FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t se } void st25r3916_change_reg_bits( - FuriHalSpiBusHandle* handle, + const FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t mask, uint8_t value) { @@ -217,7 +226,7 @@ void st25r3916_change_reg_bits( } void st25r3916_modify_reg( - FuriHalSpiBusHandle* handle, + const FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t clr_mask, uint8_t set_mask) { @@ -233,7 +242,7 @@ void st25r3916_modify_reg( } void st25r3916_change_test_reg_bits( - FuriHalSpiBusHandle* handle, + const FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t mask, uint8_t value) { @@ -248,7 +257,7 @@ void st25r3916_change_test_reg_bits( } } -bool st25r3916_check_reg(FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t mask, uint8_t val) { +bool st25r3916_check_reg(const FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t mask, uint8_t val) { furi_check(handle); uint8_t reg_val = 0; diff --git a/lib/drivers/st25r3916_reg.h b/lib/drivers/st25r3916_reg.h index 5163c4423..524f93cc7 100644 --- a/lib/drivers/st25r3916_reg.h +++ b/lib/drivers/st25r3916_reg.h @@ -967,7 +967,7 @@ extern "C" { * @param reg - register address * @param val - pointer to the variable to store the read value */ -void st25r3916_read_reg(FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t* val); +void st25r3916_read_reg(const FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t* val); /** Read multiple registers * @@ -977,7 +977,7 @@ void st25r3916_read_reg(FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t* val); * @param length - number of registers to read */ void st25r3916_read_burst_regs( - FuriHalSpiBusHandle* handle, + const FuriHalSpiBusHandle* handle, uint8_t reg_start, uint8_t* values, uint8_t length); @@ -988,7 +988,7 @@ void st25r3916_read_burst_regs( * @param reg - register address * @param val - value to write */ -void st25r3916_write_reg(FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t val); +void st25r3916_write_reg(const FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t val); /** Write multiple registers * @@ -998,7 +998,7 @@ void st25r3916_write_reg(FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t val); * @param length - number of registers to write */ void st25r3916_write_burst_regs( - FuriHalSpiBusHandle* handle, + const FuriHalSpiBusHandle* handle, uint8_t reg_start, const uint8_t* values, uint8_t length); @@ -1009,7 +1009,10 @@ void st25r3916_write_burst_regs( * @param buff - buffer to write to FIFO * @param length - number of bytes to write */ -void st25r3916_reg_write_fifo(FuriHalSpiBusHandle* handle, const uint8_t* buff, size_t length); +void st25r3916_reg_write_fifo( + const FuriHalSpiBusHandle* handle, + const uint8_t* buff, + size_t length); /** Read fifo register * @@ -1017,7 +1020,7 @@ void st25r3916_reg_write_fifo(FuriHalSpiBusHandle* handle, const uint8_t* buff, * @param buff - buffer to store the read values * @param length - number of bytes to read */ -void st25r3916_reg_read_fifo(FuriHalSpiBusHandle* handle, uint8_t* buff, size_t length); +void st25r3916_reg_read_fifo(const FuriHalSpiBusHandle* handle, uint8_t* buff, size_t length); /** Write PTA memory register * @@ -1025,7 +1028,10 @@ void st25r3916_reg_read_fifo(FuriHalSpiBusHandle* handle, uint8_t* buff, size_t * @param values - pointer to buffer to write * @param length - number of bytes to write */ -void st25r3916_write_pta_mem(FuriHalSpiBusHandle* handle, const uint8_t* values, size_t length); +void st25r3916_write_pta_mem( + const FuriHalSpiBusHandle* handle, + const uint8_t* values, + size_t length); /** Read PTA memory register * @@ -1033,7 +1039,7 @@ void st25r3916_write_pta_mem(FuriHalSpiBusHandle* handle, const uint8_t* values, * @param values - buffer to store the read values * @param length - number of bytes to read */ -void st25r3916_read_pta_mem(FuriHalSpiBusHandle* handle, uint8_t* values, size_t length); +void st25r3916_read_pta_mem(const FuriHalSpiBusHandle* handle, uint8_t* values, size_t length); /** Write PTF memory register * @@ -1041,7 +1047,10 @@ void st25r3916_read_pta_mem(FuriHalSpiBusHandle* handle, uint8_t* values, size_t * @param values - pointer to buffer to write * @param length - number of bytes to write */ -void st25r3916_write_ptf_mem(FuriHalSpiBusHandle* handle, const uint8_t* values, size_t length); +void st25r3916_write_ptf_mem( + const FuriHalSpiBusHandle* handle, + const uint8_t* values, + size_t length); /** Read PTTSN memory register * @@ -1049,21 +1058,21 @@ void st25r3916_write_ptf_mem(FuriHalSpiBusHandle* handle, const uint8_t* values, * @param values - pointer to buffer to write * @param length - number of bytes to write */ -void st25r3916_write_pttsn_mem(FuriHalSpiBusHandle* handle, uint8_t* values, size_t length); +void st25r3916_write_pttsn_mem(const FuriHalSpiBusHandle* handle, uint8_t* values, size_t length); /** Send Direct command * * @param handle - pointer to FuriHalSpiBusHandle instance * @param cmd - direct command */ -void st25r3916_direct_cmd(FuriHalSpiBusHandle* handle, uint8_t cmd); +void st25r3916_direct_cmd(const FuriHalSpiBusHandle* handle, uint8_t cmd); /** Read test register * @param handle - pointer to FuriHalSpiBusHandle instance * @param reg - register address * @param val - pointer to the variable to store the read value */ -void st25r3916_read_test_reg(FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t* val); +void st25r3916_read_test_reg(const FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t* val); /** Write test register * @@ -1071,7 +1080,7 @@ void st25r3916_read_test_reg(FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t* * @param reg - register address * @param val - value to write */ -void st25r3916_write_test_reg(FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t val); +void st25r3916_write_test_reg(const FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t val); /** Clear register bits * @@ -1079,7 +1088,7 @@ void st25r3916_write_test_reg(FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t * @param reg - register address * @param clr_mask - bit mask to clear */ -void st25r3916_clear_reg_bits(FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t clr_mask); +void st25r3916_clear_reg_bits(const FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t clr_mask); /** Set register bits * @@ -1087,7 +1096,7 @@ void st25r3916_clear_reg_bits(FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t * @param reg - register address * @param set_mask - bit mask to set */ -void st25r3916_set_reg_bits(FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t set_mask); +void st25r3916_set_reg_bits(const FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t set_mask); /** Change register bits * @@ -1097,7 +1106,7 @@ void st25r3916_set_reg_bits(FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t se * @param value - new register value to write */ void st25r3916_change_reg_bits( - FuriHalSpiBusHandle* handle, + const FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t mask, uint8_t value); @@ -1110,7 +1119,7 @@ void st25r3916_change_reg_bits( * @param set_mask - bit mask to set */ void st25r3916_modify_reg( - FuriHalSpiBusHandle* handle, + const FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t clr_mask, uint8_t set_mask); @@ -1123,7 +1132,7 @@ void st25r3916_modify_reg( * @param value - new register value to write */ void st25r3916_change_test_reg_bits( - FuriHalSpiBusHandle* handle, + const FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t mask, uint8_t value); @@ -1137,7 +1146,7 @@ void st25r3916_change_test_reg_bits( * * @return true if register value matches the expected value, false otherwise */ -bool st25r3916_check_reg(FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t mask, uint8_t val); +bool st25r3916_check_reg(const FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t mask, uint8_t val); #ifdef __cplusplus } diff --git a/targets/f18/api_symbols.csv b/targets/f18/api_symbols.csv index 2d6cbf2f0..e2572d267 100644 --- a/targets/f18/api_symbols.csv +++ b/targets/f18/api_symbols.csv @@ -1469,20 +1469,20 @@ Function,+,furi_hal_speaker_release,void, Function,+,furi_hal_speaker_set_volume,void,float Function,+,furi_hal_speaker_start,void,"float, float" Function,+,furi_hal_speaker_stop,void, -Function,+,furi_hal_spi_acquire,void,FuriHalSpiBusHandle* +Function,+,furi_hal_spi_acquire,void,const FuriHalSpiBusHandle* Function,+,furi_hal_spi_bus_deinit,void,FuriHalSpiBus* -Function,+,furi_hal_spi_bus_handle_deinit,void,FuriHalSpiBusHandle* -Function,+,furi_hal_spi_bus_handle_init,void,FuriHalSpiBusHandle* +Function,+,furi_hal_spi_bus_handle_deinit,void,const FuriHalSpiBusHandle* +Function,+,furi_hal_spi_bus_handle_init,void,const FuriHalSpiBusHandle* Function,+,furi_hal_spi_bus_init,void,FuriHalSpiBus* -Function,+,furi_hal_spi_bus_rx,_Bool,"FuriHalSpiBusHandle*, uint8_t*, size_t, uint32_t" -Function,+,furi_hal_spi_bus_trx,_Bool,"FuriHalSpiBusHandle*, const uint8_t*, uint8_t*, size_t, uint32_t" -Function,+,furi_hal_spi_bus_trx_dma,_Bool,"FuriHalSpiBusHandle*, uint8_t*, uint8_t*, size_t, uint32_t" -Function,+,furi_hal_spi_bus_tx,_Bool,"FuriHalSpiBusHandle*, const uint8_t*, size_t, uint32_t" +Function,+,furi_hal_spi_bus_rx,_Bool,"const FuriHalSpiBusHandle*, uint8_t*, size_t, uint32_t" +Function,+,furi_hal_spi_bus_trx,_Bool,"const FuriHalSpiBusHandle*, const uint8_t*, uint8_t*, size_t, uint32_t" +Function,+,furi_hal_spi_bus_trx_dma,_Bool,"const FuriHalSpiBusHandle*, uint8_t*, uint8_t*, size_t, uint32_t" +Function,+,furi_hal_spi_bus_tx,_Bool,"const FuriHalSpiBusHandle*, const uint8_t*, size_t, uint32_t" Function,-,furi_hal_spi_config_deinit_early,void, Function,-,furi_hal_spi_config_init,void, Function,-,furi_hal_spi_config_init_early,void, Function,-,furi_hal_spi_dma_init,void, -Function,+,furi_hal_spi_release,void,FuriHalSpiBusHandle* +Function,+,furi_hal_spi_release,void,const FuriHalSpiBusHandle* Function,+,furi_hal_switch,void,void* Function,+,furi_hal_usb_ccid_insert_smartcard,void, Function,+,furi_hal_usb_ccid_remove_smartcard,void, @@ -2536,29 +2536,29 @@ Function,+,srand,void,unsigned Function,-,srand48,void,long Function,-,srandom,void,unsigned Function,+,sscanf,int,"const char*, const char*, ..." -Function,+,st25r3916_change_reg_bits,void,"FuriHalSpiBusHandle*, uint8_t, uint8_t, uint8_t" -Function,+,st25r3916_change_test_reg_bits,void,"FuriHalSpiBusHandle*, uint8_t, uint8_t, uint8_t" -Function,+,st25r3916_check_reg,_Bool,"FuriHalSpiBusHandle*, uint8_t, uint8_t, uint8_t" -Function,+,st25r3916_clear_reg_bits,void,"FuriHalSpiBusHandle*, uint8_t, uint8_t" -Function,+,st25r3916_direct_cmd,void,"FuriHalSpiBusHandle*, uint8_t" -Function,+,st25r3916_get_irq,uint32_t,FuriHalSpiBusHandle* -Function,+,st25r3916_mask_irq,void,"FuriHalSpiBusHandle*, uint32_t" -Function,+,st25r3916_modify_reg,void,"FuriHalSpiBusHandle*, uint8_t, uint8_t, uint8_t" -Function,+,st25r3916_read_burst_regs,void,"FuriHalSpiBusHandle*, uint8_t, uint8_t*, uint8_t" -Function,+,st25r3916_read_fifo,_Bool,"FuriHalSpiBusHandle*, uint8_t*, size_t, size_t*" -Function,+,st25r3916_read_pta_mem,void,"FuriHalSpiBusHandle*, uint8_t*, size_t" -Function,+,st25r3916_read_reg,void,"FuriHalSpiBusHandle*, uint8_t, uint8_t*" -Function,+,st25r3916_read_test_reg,void,"FuriHalSpiBusHandle*, uint8_t, uint8_t*" -Function,+,st25r3916_reg_read_fifo,void,"FuriHalSpiBusHandle*, uint8_t*, size_t" -Function,+,st25r3916_reg_write_fifo,void,"FuriHalSpiBusHandle*, const uint8_t*, size_t" -Function,+,st25r3916_set_reg_bits,void,"FuriHalSpiBusHandle*, uint8_t, uint8_t" -Function,+,st25r3916_write_burst_regs,void,"FuriHalSpiBusHandle*, uint8_t, const uint8_t*, uint8_t" -Function,+,st25r3916_write_fifo,void,"FuriHalSpiBusHandle*, const uint8_t*, size_t" -Function,+,st25r3916_write_pta_mem,void,"FuriHalSpiBusHandle*, const uint8_t*, size_t" -Function,+,st25r3916_write_ptf_mem,void,"FuriHalSpiBusHandle*, const uint8_t*, size_t" -Function,+,st25r3916_write_pttsn_mem,void,"FuriHalSpiBusHandle*, uint8_t*, size_t" -Function,+,st25r3916_write_reg,void,"FuriHalSpiBusHandle*, uint8_t, uint8_t" -Function,+,st25r3916_write_test_reg,void,"FuriHalSpiBusHandle*, uint8_t, uint8_t" +Function,+,st25r3916_change_reg_bits,void,"const FuriHalSpiBusHandle*, uint8_t, uint8_t, uint8_t" +Function,+,st25r3916_change_test_reg_bits,void,"const FuriHalSpiBusHandle*, uint8_t, uint8_t, uint8_t" +Function,+,st25r3916_check_reg,_Bool,"const FuriHalSpiBusHandle*, uint8_t, uint8_t, uint8_t" +Function,+,st25r3916_clear_reg_bits,void,"const FuriHalSpiBusHandle*, uint8_t, uint8_t" +Function,+,st25r3916_direct_cmd,void,"const FuriHalSpiBusHandle*, uint8_t" +Function,+,st25r3916_get_irq,uint32_t,const FuriHalSpiBusHandle* +Function,+,st25r3916_mask_irq,void,"const FuriHalSpiBusHandle*, uint32_t" +Function,+,st25r3916_modify_reg,void,"const FuriHalSpiBusHandle*, uint8_t, uint8_t, uint8_t" +Function,+,st25r3916_read_burst_regs,void,"const FuriHalSpiBusHandle*, uint8_t, uint8_t*, uint8_t" +Function,+,st25r3916_read_fifo,_Bool,"const FuriHalSpiBusHandle*, uint8_t*, size_t, size_t*" +Function,+,st25r3916_read_pta_mem,void,"const FuriHalSpiBusHandle*, uint8_t*, size_t" +Function,+,st25r3916_read_reg,void,"const FuriHalSpiBusHandle*, uint8_t, uint8_t*" +Function,+,st25r3916_read_test_reg,void,"const FuriHalSpiBusHandle*, uint8_t, uint8_t*" +Function,+,st25r3916_reg_read_fifo,void,"const FuriHalSpiBusHandle*, uint8_t*, size_t" +Function,+,st25r3916_reg_write_fifo,void,"const FuriHalSpiBusHandle*, const uint8_t*, size_t" +Function,+,st25r3916_set_reg_bits,void,"const FuriHalSpiBusHandle*, uint8_t, uint8_t" +Function,+,st25r3916_write_burst_regs,void,"const FuriHalSpiBusHandle*, uint8_t, const uint8_t*, uint8_t" +Function,+,st25r3916_write_fifo,void,"const FuriHalSpiBusHandle*, const uint8_t*, size_t" +Function,+,st25r3916_write_pta_mem,void,"const FuriHalSpiBusHandle*, const uint8_t*, size_t" +Function,+,st25r3916_write_ptf_mem,void,"const FuriHalSpiBusHandle*, const uint8_t*, size_t" +Function,+,st25r3916_write_pttsn_mem,void,"const FuriHalSpiBusHandle*, uint8_t*, size_t" +Function,+,st25r3916_write_reg,void,"const FuriHalSpiBusHandle*, uint8_t, uint8_t" +Function,+,st25r3916_write_test_reg,void,"const FuriHalSpiBusHandle*, uint8_t, uint8_t" Function,+,storage_common_copy,FS_Error,"Storage*, const char*, const char*" Function,+,storage_common_equivalent_path,_Bool,"Storage*, const char*, const char*" Function,+,storage_common_exists,_Bool,"Storage*, const char*" @@ -2944,10 +2944,10 @@ Variable,+,furi_hal_i2c_bus_power,FuriHalI2cBus, Variable,+,furi_hal_i2c_handle_external,FuriHalI2cBusHandle, Variable,+,furi_hal_i2c_handle_power,FuriHalI2cBusHandle, Variable,+,furi_hal_spi_bus_d,FuriHalSpiBus, -Variable,+,furi_hal_spi_bus_handle_display,FuriHalSpiBusHandle, -Variable,+,furi_hal_spi_bus_handle_external,FuriHalSpiBusHandle, -Variable,+,furi_hal_spi_bus_handle_sd_fast,FuriHalSpiBusHandle, -Variable,+,furi_hal_spi_bus_handle_sd_slow,FuriHalSpiBusHandle, +Variable,+,furi_hal_spi_bus_handle_display,const FuriHalSpiBusHandle, +Variable,+,furi_hal_spi_bus_handle_external,const FuriHalSpiBusHandle, +Variable,+,furi_hal_spi_bus_handle_sd_fast,const FuriHalSpiBusHandle, +Variable,+,furi_hal_spi_bus_handle_sd_slow,const FuriHalSpiBusHandle, Variable,+,furi_hal_spi_bus_r,FuriHalSpiBus, Variable,+,furi_hal_spi_preset_1edge_low_16m,const LL_SPI_InitTypeDef, Variable,+,furi_hal_spi_preset_1edge_low_2m,const LL_SPI_InitTypeDef, diff --git a/targets/f18/furi_hal/furi_hal_spi_config.c b/targets/f18/furi_hal/furi_hal_spi_config.c index 8957bfe3a..a7393d3f0 100644 --- a/targets/f18/furi_hal/furi_hal_spi_config.c +++ b/targets/f18/furi_hal/furi_hal_spi_config.c @@ -143,7 +143,7 @@ FuriHalSpiBus furi_hal_spi_bus_d = { /* SPI Bus Handles */ inline static void furi_hal_spi_bus_r_handle_event_callback( - FuriHalSpiBusHandle* handle, + const FuriHalSpiBusHandle* handle, FuriHalSpiBusHandleEvent event, const LL_SPI_InitTypeDef* preset) { if(event == FuriHalSpiBusHandleEventInit) { @@ -189,7 +189,7 @@ inline static void furi_hal_spi_bus_r_handle_event_callback( } inline static void furi_hal_spi_bus_nfc_handle_event_callback( - FuriHalSpiBusHandle* handle, + const FuriHalSpiBusHandle* handle, FuriHalSpiBusHandleEvent event, const LL_SPI_InitTypeDef* preset) { if(event == FuriHalSpiBusHandleEventInit) { @@ -255,12 +255,12 @@ inline static void furi_hal_spi_bus_nfc_handle_event_callback( } static void furi_hal_spi_bus_handle_external_event_callback( - FuriHalSpiBusHandle* handle, + const FuriHalSpiBusHandle* handle, FuriHalSpiBusHandleEvent event) { furi_hal_spi_bus_r_handle_event_callback(handle, event, &furi_hal_spi_preset_1edge_low_2m); } -FuriHalSpiBusHandle furi_hal_spi_bus_handle_external = { +const FuriHalSpiBusHandle furi_hal_spi_bus_handle_external = { .bus = &furi_hal_spi_bus_r, .callback = furi_hal_spi_bus_handle_external_event_callback, .miso = &gpio_ext_pa6, @@ -270,7 +270,7 @@ FuriHalSpiBusHandle furi_hal_spi_bus_handle_external = { }; inline static void furi_hal_spi_bus_d_handle_event_callback( - FuriHalSpiBusHandle* handle, + const FuriHalSpiBusHandle* handle, FuriHalSpiBusHandleEvent event, const LL_SPI_InitTypeDef* preset) { if(event == FuriHalSpiBusHandleEventInit) { @@ -311,12 +311,12 @@ inline static void furi_hal_spi_bus_d_handle_event_callback( } static void furi_hal_spi_bus_handle_display_event_callback( - FuriHalSpiBusHandle* handle, + const FuriHalSpiBusHandle* handle, FuriHalSpiBusHandleEvent event) { furi_hal_spi_bus_d_handle_event_callback(handle, event, &furi_hal_spi_preset_1edge_low_4m); } -FuriHalSpiBusHandle furi_hal_spi_bus_handle_display = { +const FuriHalSpiBusHandle furi_hal_spi_bus_handle_display = { .bus = &furi_hal_spi_bus_d, .callback = furi_hal_spi_bus_handle_display_event_callback, .miso = &gpio_spi_d_miso, @@ -326,12 +326,12 @@ FuriHalSpiBusHandle furi_hal_spi_bus_handle_display = { }; static void furi_hal_spi_bus_handle_sd_fast_event_callback( - FuriHalSpiBusHandle* handle, + const FuriHalSpiBusHandle* handle, FuriHalSpiBusHandleEvent event) { furi_hal_spi_bus_d_handle_event_callback(handle, event, &furi_hal_spi_preset_1edge_low_16m); } -FuriHalSpiBusHandle furi_hal_spi_bus_handle_sd_fast = { +const FuriHalSpiBusHandle furi_hal_spi_bus_handle_sd_fast = { .bus = &furi_hal_spi_bus_d, .callback = furi_hal_spi_bus_handle_sd_fast_event_callback, .miso = &gpio_spi_d_miso, @@ -341,12 +341,12 @@ FuriHalSpiBusHandle furi_hal_spi_bus_handle_sd_fast = { }; static void furi_hal_spi_bus_handle_sd_slow_event_callback( - FuriHalSpiBusHandle* handle, + const FuriHalSpiBusHandle* handle, FuriHalSpiBusHandleEvent event) { furi_hal_spi_bus_d_handle_event_callback(handle, event, &furi_hal_spi_preset_1edge_low_2m); } -FuriHalSpiBusHandle furi_hal_spi_bus_handle_sd_slow = { +const FuriHalSpiBusHandle furi_hal_spi_bus_handle_sd_slow = { .bus = &furi_hal_spi_bus_d, .callback = furi_hal_spi_bus_handle_sd_slow_event_callback, .miso = &gpio_spi_d_miso, diff --git a/targets/f18/furi_hal/furi_hal_spi_config.h b/targets/f18/furi_hal/furi_hal_spi_config.h index da39fbfa6..2ff8b41b1 100644 --- a/targets/f18/furi_hal/furi_hal_spi_config.h +++ b/targets/f18/furi_hal/furi_hal_spi_config.h @@ -39,16 +39,16 @@ extern FuriHalSpiBus furi_hal_spi_bus_d; * Bus pins are floating on inactive state, CS high after initialization * */ -extern FuriHalSpiBusHandle furi_hal_spi_bus_handle_external; +extern const FuriHalSpiBusHandle furi_hal_spi_bus_handle_external; /** ST7567(Display) on `furi_hal_spi_bus_d` */ -extern FuriHalSpiBusHandle furi_hal_spi_bus_handle_display; +extern const FuriHalSpiBusHandle furi_hal_spi_bus_handle_display; /** SdCard in fast mode on `furi_hal_spi_bus_d` */ -extern FuriHalSpiBusHandle furi_hal_spi_bus_handle_sd_fast; +extern const FuriHalSpiBusHandle furi_hal_spi_bus_handle_sd_fast; /** SdCard in slow mode on `furi_hal_spi_bus_d` */ -extern FuriHalSpiBusHandle furi_hal_spi_bus_handle_sd_slow; +extern const FuriHalSpiBusHandle furi_hal_spi_bus_handle_sd_slow; #ifdef __cplusplus } diff --git a/targets/f7/api_symbols.csv b/targets/f7/api_symbols.csv index 4346547f3..db7e72459 100644 --- a/targets/f7/api_symbols.csv +++ b/targets/f7/api_symbols.csv @@ -1659,20 +1659,20 @@ Function,+,furi_hal_speaker_release,void, Function,+,furi_hal_speaker_set_volume,void,float Function,+,furi_hal_speaker_start,void,"float, float" Function,+,furi_hal_speaker_stop,void, -Function,+,furi_hal_spi_acquire,void,FuriHalSpiBusHandle* +Function,+,furi_hal_spi_acquire,void,const FuriHalSpiBusHandle* Function,+,furi_hal_spi_bus_deinit,void,FuriHalSpiBus* -Function,+,furi_hal_spi_bus_handle_deinit,void,FuriHalSpiBusHandle* -Function,+,furi_hal_spi_bus_handle_init,void,FuriHalSpiBusHandle* +Function,+,furi_hal_spi_bus_handle_deinit,void,const FuriHalSpiBusHandle* +Function,+,furi_hal_spi_bus_handle_init,void,const FuriHalSpiBusHandle* Function,+,furi_hal_spi_bus_init,void,FuriHalSpiBus* -Function,+,furi_hal_spi_bus_rx,_Bool,"FuriHalSpiBusHandle*, uint8_t*, size_t, uint32_t" -Function,+,furi_hal_spi_bus_trx,_Bool,"FuriHalSpiBusHandle*, const uint8_t*, uint8_t*, size_t, uint32_t" -Function,+,furi_hal_spi_bus_trx_dma,_Bool,"FuriHalSpiBusHandle*, uint8_t*, uint8_t*, size_t, uint32_t" -Function,+,furi_hal_spi_bus_tx,_Bool,"FuriHalSpiBusHandle*, const uint8_t*, size_t, uint32_t" +Function,+,furi_hal_spi_bus_rx,_Bool,"const FuriHalSpiBusHandle*, uint8_t*, size_t, uint32_t" +Function,+,furi_hal_spi_bus_trx,_Bool,"const FuriHalSpiBusHandle*, const uint8_t*, uint8_t*, size_t, uint32_t" +Function,+,furi_hal_spi_bus_trx_dma,_Bool,"const FuriHalSpiBusHandle*, uint8_t*, uint8_t*, size_t, uint32_t" +Function,+,furi_hal_spi_bus_tx,_Bool,"const FuriHalSpiBusHandle*, const uint8_t*, size_t, uint32_t" Function,-,furi_hal_spi_config_deinit_early,void, Function,-,furi_hal_spi_config_init,void, Function,-,furi_hal_spi_config_init_early,void, Function,-,furi_hal_spi_dma_init,void, -Function,+,furi_hal_spi_release,void,FuriHalSpiBusHandle* +Function,+,furi_hal_spi_release,void,const FuriHalSpiBusHandle* Function,-,furi_hal_subghz_dump_state,void, Function,+,furi_hal_subghz_flush_rx,void, Function,+,furi_hal_subghz_flush_tx,void, @@ -3198,29 +3198,29 @@ Function,+,srand,void,unsigned Function,-,srand48,void,long Function,-,srandom,void,unsigned Function,+,sscanf,int,"const char*, const char*, ..." -Function,+,st25r3916_change_reg_bits,void,"FuriHalSpiBusHandle*, uint8_t, uint8_t, uint8_t" -Function,+,st25r3916_change_test_reg_bits,void,"FuriHalSpiBusHandle*, uint8_t, uint8_t, uint8_t" -Function,+,st25r3916_check_reg,_Bool,"FuriHalSpiBusHandle*, uint8_t, uint8_t, uint8_t" -Function,+,st25r3916_clear_reg_bits,void,"FuriHalSpiBusHandle*, uint8_t, uint8_t" -Function,+,st25r3916_direct_cmd,void,"FuriHalSpiBusHandle*, uint8_t" -Function,+,st25r3916_get_irq,uint32_t,FuriHalSpiBusHandle* -Function,+,st25r3916_mask_irq,void,"FuriHalSpiBusHandle*, uint32_t" -Function,+,st25r3916_modify_reg,void,"FuriHalSpiBusHandle*, uint8_t, uint8_t, uint8_t" -Function,+,st25r3916_read_burst_regs,void,"FuriHalSpiBusHandle*, uint8_t, uint8_t*, uint8_t" -Function,+,st25r3916_read_fifo,_Bool,"FuriHalSpiBusHandle*, uint8_t*, size_t, size_t*" -Function,+,st25r3916_read_pta_mem,void,"FuriHalSpiBusHandle*, uint8_t*, size_t" -Function,+,st25r3916_read_reg,void,"FuriHalSpiBusHandle*, uint8_t, uint8_t*" -Function,+,st25r3916_read_test_reg,void,"FuriHalSpiBusHandle*, uint8_t, uint8_t*" -Function,+,st25r3916_reg_read_fifo,void,"FuriHalSpiBusHandle*, uint8_t*, size_t" -Function,+,st25r3916_reg_write_fifo,void,"FuriHalSpiBusHandle*, const uint8_t*, size_t" -Function,+,st25r3916_set_reg_bits,void,"FuriHalSpiBusHandle*, uint8_t, uint8_t" -Function,+,st25r3916_write_burst_regs,void,"FuriHalSpiBusHandle*, uint8_t, const uint8_t*, uint8_t" -Function,+,st25r3916_write_fifo,void,"FuriHalSpiBusHandle*, const uint8_t*, size_t" -Function,+,st25r3916_write_pta_mem,void,"FuriHalSpiBusHandle*, const uint8_t*, size_t" -Function,+,st25r3916_write_ptf_mem,void,"FuriHalSpiBusHandle*, const uint8_t*, size_t" -Function,+,st25r3916_write_pttsn_mem,void,"FuriHalSpiBusHandle*, uint8_t*, size_t" -Function,+,st25r3916_write_reg,void,"FuriHalSpiBusHandle*, uint8_t, uint8_t" -Function,+,st25r3916_write_test_reg,void,"FuriHalSpiBusHandle*, uint8_t, uint8_t" +Function,+,st25r3916_change_reg_bits,void,"const FuriHalSpiBusHandle*, uint8_t, uint8_t, uint8_t" +Function,+,st25r3916_change_test_reg_bits,void,"const FuriHalSpiBusHandle*, uint8_t, uint8_t, uint8_t" +Function,+,st25r3916_check_reg,_Bool,"const FuriHalSpiBusHandle*, uint8_t, uint8_t, uint8_t" +Function,+,st25r3916_clear_reg_bits,void,"const FuriHalSpiBusHandle*, uint8_t, uint8_t" +Function,+,st25r3916_direct_cmd,void,"const FuriHalSpiBusHandle*, uint8_t" +Function,+,st25r3916_get_irq,uint32_t,const FuriHalSpiBusHandle* +Function,+,st25r3916_mask_irq,void,"const FuriHalSpiBusHandle*, uint32_t" +Function,+,st25r3916_modify_reg,void,"const FuriHalSpiBusHandle*, uint8_t, uint8_t, uint8_t" +Function,+,st25r3916_read_burst_regs,void,"const FuriHalSpiBusHandle*, uint8_t, uint8_t*, uint8_t" +Function,+,st25r3916_read_fifo,_Bool,"const FuriHalSpiBusHandle*, uint8_t*, size_t, size_t*" +Function,+,st25r3916_read_pta_mem,void,"const FuriHalSpiBusHandle*, uint8_t*, size_t" +Function,+,st25r3916_read_reg,void,"const FuriHalSpiBusHandle*, uint8_t, uint8_t*" +Function,+,st25r3916_read_test_reg,void,"const FuriHalSpiBusHandle*, uint8_t, uint8_t*" +Function,+,st25r3916_reg_read_fifo,void,"const FuriHalSpiBusHandle*, uint8_t*, size_t" +Function,+,st25r3916_reg_write_fifo,void,"const FuriHalSpiBusHandle*, const uint8_t*, size_t" +Function,+,st25r3916_set_reg_bits,void,"const FuriHalSpiBusHandle*, uint8_t, uint8_t" +Function,+,st25r3916_write_burst_regs,void,"const FuriHalSpiBusHandle*, uint8_t, const uint8_t*, uint8_t" +Function,+,st25r3916_write_fifo,void,"const FuriHalSpiBusHandle*, const uint8_t*, size_t" +Function,+,st25r3916_write_pta_mem,void,"const FuriHalSpiBusHandle*, const uint8_t*, size_t" +Function,+,st25r3916_write_ptf_mem,void,"const FuriHalSpiBusHandle*, const uint8_t*, size_t" +Function,+,st25r3916_write_pttsn_mem,void,"const FuriHalSpiBusHandle*, uint8_t*, size_t" +Function,+,st25r3916_write_reg,void,"const FuriHalSpiBusHandle*, uint8_t, uint8_t" +Function,+,st25r3916_write_test_reg,void,"const FuriHalSpiBusHandle*, uint8_t, uint8_t" Function,+,st25tb_alloc,St25tbData*, Function,+,st25tb_copy,void,"St25tbData*, const St25tbData*" Function,+,st25tb_free,void,St25tbData* @@ -3795,12 +3795,12 @@ Variable,+,furi_hal_i2c_bus_power,FuriHalI2cBus, Variable,+,furi_hal_i2c_handle_external,FuriHalI2cBusHandle, Variable,+,furi_hal_i2c_handle_power,FuriHalI2cBusHandle, Variable,+,furi_hal_spi_bus_d,FuriHalSpiBus, -Variable,+,furi_hal_spi_bus_handle_display,FuriHalSpiBusHandle, -Variable,+,furi_hal_spi_bus_handle_external,FuriHalSpiBusHandle, -Variable,+,furi_hal_spi_bus_handle_nfc,FuriHalSpiBusHandle, -Variable,+,furi_hal_spi_bus_handle_sd_fast,FuriHalSpiBusHandle, -Variable,+,furi_hal_spi_bus_handle_sd_slow,FuriHalSpiBusHandle, -Variable,+,furi_hal_spi_bus_handle_subghz,FuriHalSpiBusHandle, +Variable,+,furi_hal_spi_bus_handle_display,const FuriHalSpiBusHandle, +Variable,+,furi_hal_spi_bus_handle_external,const FuriHalSpiBusHandle, +Variable,+,furi_hal_spi_bus_handle_nfc,const FuriHalSpiBusHandle, +Variable,+,furi_hal_spi_bus_handle_sd_fast,const FuriHalSpiBusHandle, +Variable,+,furi_hal_spi_bus_handle_sd_slow,const FuriHalSpiBusHandle, +Variable,+,furi_hal_spi_bus_handle_subghz,const FuriHalSpiBusHandle, Variable,+,furi_hal_spi_bus_r,FuriHalSpiBus, Variable,+,furi_hal_spi_preset_1edge_low_16m,const LL_SPI_InitTypeDef, Variable,+,furi_hal_spi_preset_1edge_low_2m,const LL_SPI_InitTypeDef, diff --git a/targets/f7/furi_hal/furi_hal_nfc.c b/targets/f7/furi_hal/furi_hal_nfc.c index 9d336c508..d8dc0c618 100644 --- a/targets/f7/furi_hal/furi_hal_nfc.c +++ b/targets/f7/furi_hal/furi_hal_nfc.c @@ -18,7 +18,7 @@ const FuriHalNfcTechBase* const furi_hal_nfc_tech[FuriHalNfcTechNum] = { FuriHalNfc furi_hal_nfc; -static FuriHalNfcError furi_hal_nfc_turn_on_osc(FuriHalSpiBusHandle* handle) { +static FuriHalNfcError furi_hal_nfc_turn_on_osc(const FuriHalSpiBusHandle* handle) { FuriHalNfcError error = FuriHalNfcErrorNone; furi_hal_nfc_event_start(); @@ -53,7 +53,7 @@ FuriHalNfcError furi_hal_nfc_is_hal_ready(void) { error = furi_hal_nfc_acquire(); if(error != FuriHalNfcErrorNone) break; - FuriHalSpiBusHandle* handle = &furi_hal_spi_bus_handle_nfc; + const FuriHalSpiBusHandle* handle = &furi_hal_spi_bus_handle_nfc; uint8_t chip_id = 0; st25r3916_read_reg(handle, ST25R3916_REG_IC_IDENTITY, &chip_id); if((chip_id & ST25R3916_REG_IC_IDENTITY_ic_type_mask) != @@ -83,7 +83,7 @@ FuriHalNfcError furi_hal_nfc_init(void) { furi_hal_nfc_low_power_mode_start(); } - FuriHalSpiBusHandle* handle = &furi_hal_spi_bus_handle_nfc; + const FuriHalSpiBusHandle* handle = &furi_hal_spi_bus_handle_nfc; // Set default state st25r3916_direct_cmd(handle, ST25R3916_CMD_SET_DEFAULT); // Increase IO driver strength of MISO and IRQ @@ -282,7 +282,7 @@ FuriHalNfcError furi_hal_nfc_release(void) { FuriHalNfcError furi_hal_nfc_low_power_mode_start(void) { FuriHalNfcError error = FuriHalNfcErrorNone; - FuriHalSpiBusHandle* handle = &furi_hal_spi_bus_handle_nfc; + const FuriHalSpiBusHandle* handle = &furi_hal_spi_bus_handle_nfc; st25r3916_direct_cmd(handle, ST25R3916_CMD_STOP); st25r3916_clear_reg_bits( @@ -300,7 +300,7 @@ FuriHalNfcError furi_hal_nfc_low_power_mode_start(void) { FuriHalNfcError furi_hal_nfc_low_power_mode_stop(void) { FuriHalNfcError error = FuriHalNfcErrorNone; - FuriHalSpiBusHandle* handle = &furi_hal_spi_bus_handle_nfc; + const FuriHalSpiBusHandle* handle = &furi_hal_spi_bus_handle_nfc; do { furi_hal_nfc_init_gpio_isr(); @@ -318,7 +318,7 @@ FuriHalNfcError furi_hal_nfc_low_power_mode_stop(void) { return error; } -static FuriHalNfcError furi_hal_nfc_poller_init_common(FuriHalSpiBusHandle* handle) { +static FuriHalNfcError furi_hal_nfc_poller_init_common(const FuriHalSpiBusHandle* handle) { // Disable wake up st25r3916_clear_reg_bits(handle, ST25R3916_REG_OP_CONTROL, ST25R3916_REG_OP_CONTROL_wu); // Enable correlator @@ -339,7 +339,7 @@ static FuriHalNfcError furi_hal_nfc_poller_init_common(FuriHalSpiBusHandle* hand return FuriHalNfcErrorNone; } -static FuriHalNfcError furi_hal_nfc_listener_init_common(FuriHalSpiBusHandle* handle) { +static FuriHalNfcError furi_hal_nfc_listener_init_common(const FuriHalSpiBusHandle* handle) { UNUSED(handle); // No common listener configuration return FuriHalNfcErrorNone; @@ -349,7 +349,7 @@ FuriHalNfcError furi_hal_nfc_set_mode(FuriHalNfcMode mode, FuriHalNfcTech tech) furi_check(mode < FuriHalNfcModeNum); furi_check(tech < FuriHalNfcTechNum); - FuriHalSpiBusHandle* handle = &furi_hal_spi_bus_handle_nfc; + const FuriHalSpiBusHandle* handle = &furi_hal_spi_bus_handle_nfc; FuriHalNfcError error = FuriHalNfcErrorNone; @@ -375,7 +375,7 @@ FuriHalNfcError furi_hal_nfc_set_mode(FuriHalNfcMode mode, FuriHalNfcTech tech) FuriHalNfcError furi_hal_nfc_reset_mode(void) { FuriHalNfcError error = FuriHalNfcErrorNone; - FuriHalSpiBusHandle* handle = &furi_hal_spi_bus_handle_nfc; + const FuriHalSpiBusHandle* handle = &furi_hal_spi_bus_handle_nfc; st25r3916_direct_cmd(handle, ST25R3916_CMD_STOP); @@ -415,7 +415,7 @@ FuriHalNfcError furi_hal_nfc_reset_mode(void) { FuriHalNfcError furi_hal_nfc_field_detect_start(void) { FuriHalNfcError error = FuriHalNfcErrorNone; - FuriHalSpiBusHandle* handle = &furi_hal_spi_bus_handle_nfc; + const FuriHalSpiBusHandle* handle = &furi_hal_spi_bus_handle_nfc; st25r3916_write_reg( handle, @@ -429,7 +429,7 @@ FuriHalNfcError furi_hal_nfc_field_detect_start(void) { FuriHalNfcError furi_hal_nfc_field_detect_stop(void) { FuriHalNfcError error = FuriHalNfcErrorNone; - FuriHalSpiBusHandle* handle = &furi_hal_spi_bus_handle_nfc; + const FuriHalSpiBusHandle* handle = &furi_hal_spi_bus_handle_nfc; st25r3916_clear_reg_bits( handle, @@ -441,7 +441,7 @@ FuriHalNfcError furi_hal_nfc_field_detect_stop(void) { bool furi_hal_nfc_field_is_present(void) { bool is_present = false; - FuriHalSpiBusHandle* handle = &furi_hal_spi_bus_handle_nfc; + const FuriHalSpiBusHandle* handle = &furi_hal_spi_bus_handle_nfc; if(st25r3916_check_reg( handle, @@ -456,7 +456,7 @@ bool furi_hal_nfc_field_is_present(void) { FuriHalNfcError furi_hal_nfc_poller_field_on(void) { FuriHalNfcError error = FuriHalNfcErrorNone; - FuriHalSpiBusHandle* handle = &furi_hal_spi_bus_handle_nfc; + const FuriHalSpiBusHandle* handle = &furi_hal_spi_bus_handle_nfc; if(!st25r3916_check_reg( handle, @@ -476,7 +476,7 @@ FuriHalNfcError furi_hal_nfc_poller_field_on(void) { } FuriHalNfcError furi_hal_nfc_poller_tx_common( - FuriHalSpiBusHandle* handle, + const FuriHalSpiBusHandle* handle, const uint8_t* tx_data, size_t tx_bits) { furi_check(tx_data); @@ -508,7 +508,7 @@ FuriHalNfcError furi_hal_nfc_poller_tx_common( } FuriHalNfcError furi_hal_nfc_common_fifo_tx( - FuriHalSpiBusHandle* handle, + const FuriHalSpiBusHandle* handle, const uint8_t* tx_data, size_t tx_bits) { FuriHalNfcError err = FuriHalNfcErrorNone; @@ -523,7 +523,7 @@ FuriHalNfcError furi_hal_nfc_common_fifo_tx( FuriHalNfcError furi_hal_nfc_poller_tx(const uint8_t* tx_data, size_t tx_bits) { furi_check(furi_hal_nfc.mode == FuriHalNfcModePoller); furi_check(furi_hal_nfc.tech < FuriHalNfcTechNum); - FuriHalSpiBusHandle* handle = &furi_hal_spi_bus_handle_nfc; + const FuriHalSpiBusHandle* handle = &furi_hal_spi_bus_handle_nfc; return furi_hal_nfc_tech[furi_hal_nfc.tech]->poller.tx(handle, tx_data, tx_bits); } @@ -531,7 +531,7 @@ FuriHalNfcError furi_hal_nfc_poller_tx(const uint8_t* tx_data, size_t tx_bits) { FuriHalNfcError furi_hal_nfc_poller_rx(uint8_t* rx_data, size_t rx_data_size, size_t* rx_bits) { furi_check(furi_hal_nfc.mode == FuriHalNfcModePoller); furi_check(furi_hal_nfc.tech < FuriHalNfcTechNum); - FuriHalSpiBusHandle* handle = &furi_hal_spi_bus_handle_nfc; + const FuriHalSpiBusHandle* handle = &furi_hal_spi_bus_handle_nfc; return furi_hal_nfc_tech[furi_hal_nfc.tech]->poller.rx(handle, rx_data, rx_data_size, rx_bits); } @@ -556,12 +556,12 @@ FuriHalNfcError furi_hal_nfc_listener_tx(const uint8_t* tx_data, size_t tx_bits) furi_check(furi_hal_nfc.mode == FuriHalNfcModeListener); furi_check(furi_hal_nfc.tech < FuriHalNfcTechNum); - FuriHalSpiBusHandle* handle = &furi_hal_spi_bus_handle_nfc; + const FuriHalSpiBusHandle* handle = &furi_hal_spi_bus_handle_nfc; return furi_hal_nfc_tech[furi_hal_nfc.tech]->listener.tx(handle, tx_data, tx_bits); } FuriHalNfcError furi_hal_nfc_common_fifo_rx( - FuriHalSpiBusHandle* handle, + const FuriHalSpiBusHandle* handle, uint8_t* rx_data, size_t rx_data_size, size_t* rx_bits) { @@ -581,13 +581,13 @@ FuriHalNfcError furi_hal_nfc_listener_rx(uint8_t* rx_data, size_t rx_data_size, furi_check(furi_hal_nfc.mode == FuriHalNfcModeListener); furi_check(furi_hal_nfc.tech < FuriHalNfcTechNum); - FuriHalSpiBusHandle* handle = &furi_hal_spi_bus_handle_nfc; + const FuriHalSpiBusHandle* handle = &furi_hal_spi_bus_handle_nfc; return furi_hal_nfc_tech[furi_hal_nfc.tech]->listener.rx( handle, rx_data, rx_data_size, rx_bits); } FuriHalNfcError furi_hal_nfc_trx_reset(void) { - FuriHalSpiBusHandle* handle = &furi_hal_spi_bus_handle_nfc; + const FuriHalSpiBusHandle* handle = &furi_hal_spi_bus_handle_nfc; st25r3916_direct_cmd(handle, ST25R3916_CMD_STOP); @@ -598,7 +598,7 @@ FuriHalNfcError furi_hal_nfc_listener_sleep(void) { furi_check(furi_hal_nfc.mode == FuriHalNfcModeListener); furi_check(furi_hal_nfc.tech < FuriHalNfcTechNum); - FuriHalSpiBusHandle* handle = &furi_hal_spi_bus_handle_nfc; + const FuriHalSpiBusHandle* handle = &furi_hal_spi_bus_handle_nfc; return furi_hal_nfc_tech[furi_hal_nfc.tech]->listener.sleep(handle); } @@ -607,13 +607,13 @@ FuriHalNfcError furi_hal_nfc_listener_idle(void) { furi_check(furi_hal_nfc.mode == FuriHalNfcModeListener); furi_check(furi_hal_nfc.tech < FuriHalNfcTechNum); - FuriHalSpiBusHandle* handle = &furi_hal_spi_bus_handle_nfc; + const FuriHalSpiBusHandle* handle = &furi_hal_spi_bus_handle_nfc; return furi_hal_nfc_tech[furi_hal_nfc.tech]->listener.idle(handle); } FuriHalNfcError furi_hal_nfc_listener_enable_rx(void) { - FuriHalSpiBusHandle* handle = &furi_hal_spi_bus_handle_nfc; + const FuriHalSpiBusHandle* handle = &furi_hal_spi_bus_handle_nfc; st25r3916_direct_cmd(handle, ST25R3916_CMD_UNMASK_RECEIVE_DATA); diff --git a/targets/f7/furi_hal/furi_hal_nfc_event.c b/targets/f7/furi_hal/furi_hal_nfc_event.c index 9bcd2f1fe..56681b4fe 100644 --- a/targets/f7/furi_hal/furi_hal_nfc_event.c +++ b/targets/f7/furi_hal/furi_hal_nfc_event.c @@ -48,7 +48,7 @@ FuriHalNfcEvent furi_hal_nfc_wait_event_common(uint32_t timeout_ms) { if(event_flag != (unsigned)FuriFlagErrorTimeout) { if(event_flag & FuriHalNfcEventInternalTypeIrq) { furi_thread_flags_clear(FuriHalNfcEventInternalTypeIrq); - FuriHalSpiBusHandle* handle = &furi_hal_spi_bus_handle_nfc; + const FuriHalSpiBusHandle* handle = &furi_hal_spi_bus_handle_nfc; uint32_t irq = furi_hal_nfc_get_irq(handle); if(irq & ST25R3916_IRQ_MASK_OSC) { event |= FuriHalNfcEventOscOn; @@ -101,7 +101,7 @@ FuriHalNfcEvent furi_hal_nfc_wait_event_common(uint32_t timeout_ms) { } bool furi_hal_nfc_event_wait_for_specific_irq( - FuriHalSpiBusHandle* handle, + const FuriHalSpiBusHandle* handle, uint32_t mask, uint32_t timeout_ms) { furi_check(furi_hal_nfc_event); diff --git a/targets/f7/furi_hal/furi_hal_nfc_felica.c b/targets/f7/furi_hal/furi_hal_nfc_felica.c index 6c3f55525..1267b7b13 100644 --- a/targets/f7/furi_hal/furi_hal_nfc_felica.c +++ b/targets/f7/furi_hal/furi_hal_nfc_felica.c @@ -18,7 +18,7 @@ typedef struct { } FuriHalFelicaPtMemory; #pragma pack(pop) -static FuriHalNfcError furi_hal_nfc_felica_poller_init(FuriHalSpiBusHandle* handle) { +static FuriHalNfcError furi_hal_nfc_felica_poller_init(const FuriHalSpiBusHandle* handle) { // Enable Felica mode, AM modulation st25r3916_change_reg_bits( handle, @@ -61,13 +61,13 @@ static FuriHalNfcError furi_hal_nfc_felica_poller_init(FuriHalSpiBusHandle* hand return FuriHalNfcErrorNone; } -static FuriHalNfcError furi_hal_nfc_felica_poller_deinit(FuriHalSpiBusHandle* handle) { +static FuriHalNfcError furi_hal_nfc_felica_poller_deinit(const FuriHalSpiBusHandle* handle) { UNUSED(handle); return FuriHalNfcErrorNone; } -static FuriHalNfcError furi_hal_nfc_felica_listener_init(FuriHalSpiBusHandle* handle) { +static FuriHalNfcError furi_hal_nfc_felica_listener_init(const FuriHalSpiBusHandle* handle) { furi_assert(handle); st25r3916_direct_cmd(handle, ST25R3916_CMD_SET_DEFAULT); st25r3916_write_reg( @@ -141,7 +141,7 @@ static FuriHalNfcError furi_hal_nfc_felica_listener_init(FuriHalSpiBusHandle* ha return FuriHalNfcErrorNone; } -static FuriHalNfcError furi_hal_nfc_felica_listener_deinit(FuriHalSpiBusHandle* handle) { +static FuriHalNfcError furi_hal_nfc_felica_listener_deinit(const FuriHalSpiBusHandle* handle) { UNUSED(handle); return FuriHalNfcErrorNone; } @@ -154,19 +154,19 @@ static FuriHalNfcEvent furi_hal_nfc_felica_listener_wait_event(uint32_t timeout_ } FuriHalNfcError furi_hal_nfc_felica_listener_tx( - FuriHalSpiBusHandle* handle, + const FuriHalSpiBusHandle* handle, const uint8_t* tx_data, size_t tx_bits) { furi_hal_nfc_common_fifo_tx(handle, tx_data, tx_bits); return FuriHalNfcErrorNone; } -FuriHalNfcError furi_hal_nfc_felica_listener_sleep(FuriHalSpiBusHandle* handle) { +FuriHalNfcError furi_hal_nfc_felica_listener_sleep(const FuriHalSpiBusHandle* handle) { UNUSED(handle); return FuriHalNfcErrorNone; } -FuriHalNfcError furi_hal_nfc_felica_listener_idle(FuriHalSpiBusHandle* handle) { +FuriHalNfcError furi_hal_nfc_felica_listener_idle(const FuriHalSpiBusHandle* handle) { UNUSED(handle); return FuriHalNfcErrorNone; } @@ -182,7 +182,7 @@ FuriHalNfcError furi_hal_nfc_felica_listener_set_sensf_res_data( furi_check(idm_len == FURI_HAL_FELICA_IDM_PMM_LENGTH); furi_check(pmm_len == FURI_HAL_FELICA_IDM_PMM_LENGTH); - FuriHalSpiBusHandle* handle = &furi_hal_spi_bus_handle_nfc; + const FuriHalSpiBusHandle* handle = &furi_hal_spi_bus_handle_nfc; // Write PT Memory FuriHalFelicaPtMemory pt; pt.system_code = sys_code; diff --git a/targets/f7/furi_hal/furi_hal_nfc_i.h b/targets/f7/furi_hal/furi_hal_nfc_i.h index 084196451..5b0f8e68d 100644 --- a/targets/f7/furi_hal/furi_hal_nfc_i.h +++ b/targets/f7/furi_hal/furi_hal_nfc_i.h @@ -104,7 +104,7 @@ void furi_hal_nfc_timers_deinit(void); * @param[in,out] handle pointer to the SPI handle associated with the NFC chip. * @returns bitmask of zero or more occurred interrupts. */ -uint32_t furi_hal_nfc_get_irq(FuriHalSpiBusHandle* handle); +uint32_t furi_hal_nfc_get_irq(const FuriHalSpiBusHandle* handle); /** * @brief Wait until a specified type of interrupt occurs. @@ -115,7 +115,7 @@ uint32_t furi_hal_nfc_get_irq(FuriHalSpiBusHandle* handle); * @returns true if specified interrupt(s) have occured within timeout, false otherwise. */ bool furi_hal_nfc_event_wait_for_specific_irq( - FuriHalSpiBusHandle* handle, + const FuriHalSpiBusHandle* handle, uint32_t mask, uint32_t timeout_ms); @@ -137,7 +137,7 @@ FuriHalNfcEvent furi_hal_nfc_wait_event_common(uint32_t timeout_ms); * @param[in,out] handle pointer to the SPI handle associated with the NFC chip. * @returns FuriHalNfcErrorNone on success, any other error code on failure. */ -FuriHalNfcError furi_hal_nfc_common_listener_rx_start(FuriHalSpiBusHandle* handle); +FuriHalNfcError furi_hal_nfc_common_listener_rx_start(const FuriHalSpiBusHandle* handle); /** * @brief Transmit data using on-chip FIFO. @@ -150,7 +150,7 @@ FuriHalNfcError furi_hal_nfc_common_listener_rx_start(FuriHalSpiBusHandle* handl * @returns FuriHalNfcErrorNone on success, any other error code on failure. */ FuriHalNfcError furi_hal_nfc_common_fifo_tx( - FuriHalSpiBusHandle* handle, + const FuriHalSpiBusHandle* handle, const uint8_t* tx_data, size_t tx_bits); @@ -166,7 +166,7 @@ FuriHalNfcError furi_hal_nfc_common_fifo_tx( * @returns FuriHalNfcErrorNone on success, any other error code on failure. */ FuriHalNfcError furi_hal_nfc_common_fifo_rx( - FuriHalSpiBusHandle* handle, + const FuriHalSpiBusHandle* handle, uint8_t* rx_data, size_t rx_data_size, size_t* rx_bits); @@ -182,7 +182,7 @@ FuriHalNfcError furi_hal_nfc_common_fifo_rx( * @returns FuriHalNfcErrorNone on success, any other error code on failure. */ FuriHalNfcError furi_hal_nfc_poller_tx_common( - FuriHalSpiBusHandle* handle, + const FuriHalSpiBusHandle* handle, const uint8_t* tx_data, size_t tx_bits); diff --git a/targets/f7/furi_hal/furi_hal_nfc_irq.c b/targets/f7/furi_hal/furi_hal_nfc_irq.c index 90373955f..50a0139fd 100644 --- a/targets/f7/furi_hal/furi_hal_nfc_irq.c +++ b/targets/f7/furi_hal/furi_hal_nfc_irq.c @@ -8,7 +8,7 @@ static void furi_hal_nfc_int_callback(void* context) { furi_hal_nfc_event_set(FuriHalNfcEventInternalTypeIrq); } -uint32_t furi_hal_nfc_get_irq(FuriHalSpiBusHandle* handle) { +uint32_t furi_hal_nfc_get_irq(const FuriHalSpiBusHandle* handle) { uint32_t irq = 0; while(furi_hal_gpio_read_port_pin(gpio_nfc_irq_rfid_pull.port, gpio_nfc_irq_rfid_pull.pin)) { irq |= st25r3916_get_irq(handle); diff --git a/targets/f7/furi_hal/furi_hal_nfc_iso14443a.c b/targets/f7/furi_hal/furi_hal_nfc_iso14443a.c index 1ef23dfa4..be7a17264 100644 --- a/targets/f7/furi_hal/furi_hal_nfc_iso14443a.c +++ b/targets/f7/furi_hal/furi_hal_nfc_iso14443a.c @@ -13,7 +13,7 @@ static Iso14443_3aSignal* iso14443_3a_signal = NULL; -static FuriHalNfcError furi_hal_nfc_iso14443a_common_init(FuriHalSpiBusHandle* handle) { +static FuriHalNfcError furi_hal_nfc_iso14443a_common_init(const FuriHalSpiBusHandle* handle) { // Common NFC-A settings, 106 kbps // 1st stage zero = 600kHz, 3rd stage zero = 200 kHz @@ -40,7 +40,7 @@ static FuriHalNfcError furi_hal_nfc_iso14443a_common_init(FuriHalSpiBusHandle* h return FuriHalNfcErrorNone; } -static FuriHalNfcError furi_hal_nfc_iso14443a_poller_init(FuriHalSpiBusHandle* handle) { +static FuriHalNfcError furi_hal_nfc_iso14443a_poller_init(const FuriHalSpiBusHandle* handle) { // Enable ISO14443A mode, OOK modulation st25r3916_change_reg_bits( handle, @@ -57,7 +57,7 @@ static FuriHalNfcError furi_hal_nfc_iso14443a_poller_init(FuriHalSpiBusHandle* h return furi_hal_nfc_iso14443a_common_init(handle); } -static FuriHalNfcError furi_hal_nfc_iso14443a_poller_deinit(FuriHalSpiBusHandle* handle) { +static FuriHalNfcError furi_hal_nfc_iso14443a_poller_deinit(const FuriHalSpiBusHandle* handle) { st25r3916_change_reg_bits( handle, ST25R3916_REG_ISO14443A_NFC, @@ -67,7 +67,7 @@ static FuriHalNfcError furi_hal_nfc_iso14443a_poller_deinit(FuriHalSpiBusHandle* return FuriHalNfcErrorNone; } -static FuriHalNfcError furi_hal_nfc_iso14443a_listener_init(FuriHalSpiBusHandle* handle) { +static FuriHalNfcError furi_hal_nfc_iso14443a_listener_init(const FuriHalSpiBusHandle* handle) { furi_check(iso14443_3a_signal == NULL); iso14443_3a_signal = iso14443_3a_signal_alloc(&gpio_spi_r_mosi); @@ -105,7 +105,7 @@ static FuriHalNfcError furi_hal_nfc_iso14443a_listener_init(FuriHalSpiBusHandle* return furi_hal_nfc_iso14443a_common_init(handle); } -static FuriHalNfcError furi_hal_nfc_iso14443a_listener_deinit(FuriHalSpiBusHandle* handle) { +static FuriHalNfcError furi_hal_nfc_iso14443a_listener_deinit(const FuriHalSpiBusHandle* handle) { UNUSED(handle); if(iso14443_3a_signal) { @@ -118,7 +118,7 @@ static FuriHalNfcError furi_hal_nfc_iso14443a_listener_deinit(FuriHalSpiBusHandl static FuriHalNfcEvent furi_hal_nfc_iso14443_3a_listener_wait_event(uint32_t timeout_ms) { FuriHalNfcEvent event = furi_hal_nfc_wait_event_common(timeout_ms); - FuriHalSpiBusHandle* handle = &furi_hal_spi_bus_handle_nfc; + const FuriHalSpiBusHandle* handle = &furi_hal_spi_bus_handle_nfc; if(event & FuriHalNfcEventListenerActive) { st25r3916_set_reg_bits( @@ -131,7 +131,7 @@ static FuriHalNfcEvent furi_hal_nfc_iso14443_3a_listener_wait_event(uint32_t tim FuriHalNfcError furi_hal_nfc_iso14443a_poller_trx_short_frame(FuriHalNfcaShortFrame frame) { FuriHalNfcError error = FuriHalNfcErrorNone; - FuriHalSpiBusHandle* handle = &furi_hal_spi_bus_handle_nfc; + const FuriHalSpiBusHandle* handle = &furi_hal_spi_bus_handle_nfc; // Disable crc check st25r3916_set_reg_bits(handle, ST25R3916_REG_AUX, ST25R3916_REG_AUX_no_crc_rx); @@ -185,7 +185,7 @@ FuriHalNfcError furi_check(tx_data); FuriHalNfcError err = FuriHalNfcErrorNone; - FuriHalSpiBusHandle* handle = &furi_hal_spi_bus_handle_nfc; + const FuriHalSpiBusHandle* handle = &furi_hal_spi_bus_handle_nfc; // Prepare tx st25r3916_direct_cmd(handle, ST25R3916_CMD_CLEAR_FIFO); @@ -220,7 +220,7 @@ FuriHalNfcError furi_hal_nfc_iso14443a_listener_set_col_res_data( FuriHalNfcError error = FuriHalNfcErrorNone; - FuriHalSpiBusHandle* handle = &furi_hal_spi_bus_handle_nfc; + const FuriHalSpiBusHandle* handle = &furi_hal_spi_bus_handle_nfc; // Set 4 or 7 bytes UID if(uid_len == 4) { @@ -255,7 +255,7 @@ FuriHalNfcError furi_hal_nfc_iso14443a_listener_set_col_res_data( } FuriHalNfcError furi_hal_nfc_iso4443a_listener_tx( - FuriHalSpiBusHandle* handle, + const FuriHalSpiBusHandle* handle, const uint8_t* tx_data, size_t tx_bits) { FuriHalNfcError error = FuriHalNfcErrorNone; @@ -284,7 +284,7 @@ FuriHalNfcError furi_hal_nfc_iso14443a_listener_tx_custom_parity( furi_check(iso14443_3a_signal); - FuriHalSpiBusHandle* handle = &furi_hal_spi_bus_handle_nfc; + const FuriHalSpiBusHandle* handle = &furi_hal_spi_bus_handle_nfc; st25r3916_direct_cmd(handle, ST25R3916_CMD_TRANSPARENT_MODE); // Reconfigure gpio for Transparent mode @@ -303,7 +303,7 @@ FuriHalNfcError furi_hal_nfc_iso14443a_listener_tx_custom_parity( return FuriHalNfcErrorNone; } -FuriHalNfcError furi_hal_nfc_iso14443_3a_listener_sleep(FuriHalSpiBusHandle* handle) { +FuriHalNfcError furi_hal_nfc_iso14443_3a_listener_sleep(const FuriHalSpiBusHandle* handle) { // Enable auto collision resolution st25r3916_clear_reg_bits( handle, ST25R3916_REG_PASSIVE_TARGET, ST25R3916_REG_PASSIVE_TARGET_d_106_ac_a); @@ -313,7 +313,7 @@ FuriHalNfcError furi_hal_nfc_iso14443_3a_listener_sleep(FuriHalSpiBusHandle* han return FuriHalNfcErrorNone; } -FuriHalNfcError furi_hal_nfc_iso14443_3a_listener_idle(FuriHalSpiBusHandle* handle) { +FuriHalNfcError furi_hal_nfc_iso14443_3a_listener_idle(const FuriHalSpiBusHandle* handle) { // Enable auto collision resolution st25r3916_clear_reg_bits( handle, ST25R3916_REG_PASSIVE_TARGET, ST25R3916_REG_PASSIVE_TARGET_d_106_ac_a); diff --git a/targets/f7/furi_hal/furi_hal_nfc_iso14443b.c b/targets/f7/furi_hal/furi_hal_nfc_iso14443b.c index bb1a63515..315223e2f 100644 --- a/targets/f7/furi_hal/furi_hal_nfc_iso14443b.c +++ b/targets/f7/furi_hal/furi_hal_nfc_iso14443b.c @@ -1,7 +1,7 @@ #include "furi_hal_nfc_i.h" #include "furi_hal_nfc_tech_i.h" -static FuriHalNfcError furi_hal_nfc_iso14443b_common_init(FuriHalSpiBusHandle* handle) { +static FuriHalNfcError furi_hal_nfc_iso14443b_common_init(const FuriHalSpiBusHandle* handle) { // Common NFC-B settings, 106kbps // 1st stage zero = 60kHz, 3rd stage zero = 200 kHz @@ -40,7 +40,7 @@ static FuriHalNfcError furi_hal_nfc_iso14443b_common_init(FuriHalSpiBusHandle* h return FuriHalNfcErrorNone; } -static FuriHalNfcError furi_hal_nfc_iso14443b_poller_init(FuriHalSpiBusHandle* handle) { +static FuriHalNfcError furi_hal_nfc_iso14443b_poller_init(const FuriHalSpiBusHandle* handle) { // Enable ISO14443B mode, AM modulation st25r3916_change_reg_bits( handle, @@ -84,7 +84,7 @@ static FuriHalNfcError furi_hal_nfc_iso14443b_poller_init(FuriHalSpiBusHandle* h return furi_hal_nfc_iso14443b_common_init(handle); } -static FuriHalNfcError furi_hal_nfc_iso14443b_poller_deinit(FuriHalSpiBusHandle* handle) { +static FuriHalNfcError furi_hal_nfc_iso14443b_poller_deinit(const FuriHalSpiBusHandle* handle) { UNUSED(handle); return FuriHalNfcErrorNone; } diff --git a/targets/f7/furi_hal/furi_hal_nfc_iso15693.c b/targets/f7/furi_hal/furi_hal_nfc_iso15693.c index 3245b67cc..d35b160f4 100644 --- a/targets/f7/furi_hal/furi_hal_nfc_iso15693.c +++ b/targets/f7/furi_hal/furi_hal_nfc_iso15693.c @@ -74,7 +74,7 @@ static void furi_hal_nfc_iso15693_poller_free(FuriHalNfcIso15693Poller* instance free(instance); } -static FuriHalNfcError furi_hal_nfc_iso15693_common_init(FuriHalSpiBusHandle* handle) { +static FuriHalNfcError furi_hal_nfc_iso15693_common_init(const FuriHalSpiBusHandle* handle) { // Common NFC-V settings, 26.48 kbps // 1st stage zero = 12 kHz, 3rd stage zero = 80 kHz, low-pass = 600 kHz @@ -112,7 +112,7 @@ static FuriHalNfcError furi_hal_nfc_iso15693_common_init(FuriHalSpiBusHandle* ha return FuriHalNfcErrorNone; } -static FuriHalNfcError furi_hal_nfc_iso15693_poller_init(FuriHalSpiBusHandle* handle) { +static FuriHalNfcError furi_hal_nfc_iso15693_poller_init(const FuriHalSpiBusHandle* handle) { furi_check(furi_hal_nfc_iso15693_poller == NULL); furi_hal_nfc_iso15693_poller = furi_hal_nfc_iso15693_poller_alloc(); @@ -141,7 +141,7 @@ static FuriHalNfcError furi_hal_nfc_iso15693_poller_init(FuriHalSpiBusHandle* ha return furi_hal_nfc_iso15693_common_init(handle); } -static FuriHalNfcError furi_hal_nfc_iso15693_poller_deinit(FuriHalSpiBusHandle* handle) { +static FuriHalNfcError furi_hal_nfc_iso15693_poller_deinit(const FuriHalSpiBusHandle* handle) { UNUSED(handle); furi_check(furi_hal_nfc_iso15693_poller); @@ -238,7 +238,7 @@ static FuriHalNfcError iso15693_3_poller_decode_frame( } static FuriHalNfcError furi_hal_nfc_iso15693_poller_tx( - FuriHalSpiBusHandle* handle, + const FuriHalSpiBusHandle* handle, const uint8_t* tx_data, size_t tx_bits) { FuriHalNfcIso15693Poller* instance = furi_hal_nfc_iso15693_poller; @@ -252,7 +252,7 @@ static FuriHalNfcError furi_hal_nfc_iso15693_poller_tx( } static FuriHalNfcError furi_hal_nfc_iso15693_poller_rx( - FuriHalSpiBusHandle* handle, + const FuriHalSpiBusHandle* handle, uint8_t* rx_data, size_t rx_data_size, size_t* rx_bits) { @@ -284,14 +284,16 @@ static FuriHalNfcError furi_hal_nfc_iso15693_poller_rx( return error; } -static void furi_hal_nfc_iso15693_listener_transparent_mode_enter(FuriHalSpiBusHandle* handle) { +static void + furi_hal_nfc_iso15693_listener_transparent_mode_enter(const FuriHalSpiBusHandle* handle) { st25r3916_direct_cmd(handle, ST25R3916_CMD_TRANSPARENT_MODE); furi_hal_spi_bus_handle_deinit(handle); furi_hal_nfc_deinit_gpio_isr(); } -static void furi_hal_nfc_iso15693_listener_transparent_mode_exit(FuriHalSpiBusHandle* handle) { +static void + furi_hal_nfc_iso15693_listener_transparent_mode_exit(const FuriHalSpiBusHandle* handle) { // Configure gpio back to SPI and exit transparent mode furi_hal_nfc_init_gpio_isr(); furi_hal_spi_bus_handle_init(handle); @@ -299,7 +301,7 @@ static void furi_hal_nfc_iso15693_listener_transparent_mode_exit(FuriHalSpiBusHa st25r3916_direct_cmd(handle, ST25R3916_CMD_UNMASK_RECEIVE_DATA); } -static FuriHalNfcError furi_hal_nfc_iso15693_listener_init(FuriHalSpiBusHandle* handle) { +static FuriHalNfcError furi_hal_nfc_iso15693_listener_init(const FuriHalSpiBusHandle* handle) { furi_check(furi_hal_nfc_iso15693_listener == NULL); furi_hal_nfc_iso15693_listener = furi_hal_nfc_iso15693_listener_alloc(); @@ -328,7 +330,7 @@ static FuriHalNfcError furi_hal_nfc_iso15693_listener_init(FuriHalSpiBusHandle* return error; } -static FuriHalNfcError furi_hal_nfc_iso15693_listener_deinit(FuriHalSpiBusHandle* handle) { +static FuriHalNfcError furi_hal_nfc_iso15693_listener_deinit(const FuriHalSpiBusHandle* handle) { furi_check(furi_hal_nfc_iso15693_listener); furi_hal_nfc_iso15693_listener_transparent_mode_exit(handle); @@ -387,7 +389,7 @@ static FuriHalNfcEvent furi_hal_nfc_iso15693_wait_event(uint32_t timeout_ms) { } static FuriHalNfcError furi_hal_nfc_iso15693_listener_tx( - FuriHalSpiBusHandle* handle, + const FuriHalSpiBusHandle* handle, const uint8_t* tx_data, size_t tx_bits) { UNUSED(handle); @@ -407,7 +409,7 @@ FuriHalNfcError furi_hal_nfc_iso15693_listener_tx_sof(void) { } static FuriHalNfcError furi_hal_nfc_iso15693_listener_rx( - FuriHalSpiBusHandle* handle, + const FuriHalSpiBusHandle* handle, uint8_t* rx_data, size_t rx_data_size, size_t* rx_bits) { @@ -425,7 +427,7 @@ static FuriHalNfcError furi_hal_nfc_iso15693_listener_rx( return FuriHalNfcErrorNone; } -FuriHalNfcError furi_hal_nfc_iso15693_listener_sleep(FuriHalSpiBusHandle* handle) { +FuriHalNfcError furi_hal_nfc_iso15693_listener_sleep(const FuriHalSpiBusHandle* handle) { UNUSED(handle); return FuriHalNfcErrorNone; diff --git a/targets/f7/furi_hal/furi_hal_nfc_tech_i.h b/targets/f7/furi_hal/furi_hal_nfc_tech_i.h index a2a75aa66..4a62f67c9 100644 --- a/targets/f7/furi_hal/furi_hal_nfc_tech_i.h +++ b/targets/f7/furi_hal/furi_hal_nfc_tech_i.h @@ -25,7 +25,7 @@ extern "C" { * @param[in,out] handle pointer to the NFC chip SPI handle. * @returns FuriHalNfcErrorNone on success, any other error code on failure. */ -typedef FuriHalNfcError (*FuriHalNfcChipConfig)(FuriHalSpiBusHandle* handle); +typedef FuriHalNfcError (*FuriHalNfcChipConfig)(const FuriHalSpiBusHandle* handle); /** * @brief Transmit data using technology-specific framing and timings. @@ -36,7 +36,7 @@ typedef FuriHalNfcError (*FuriHalNfcChipConfig)(FuriHalSpiBusHandle* handle); * @returns FuriHalNfcErrorNone on success, any other error code on failure. */ typedef FuriHalNfcError ( - *FuriHalNfcTx)(FuriHalSpiBusHandle* handle, const uint8_t* tx_data, size_t tx_bits); + *FuriHalNfcTx)(const FuriHalSpiBusHandle* handle, const uint8_t* tx_data, size_t tx_bits); /** * @brief Receive data using technology-specific framing and timings. @@ -48,7 +48,7 @@ typedef FuriHalNfcError ( * @returns FuriHalNfcErrorNone on success, any other error code on failure. */ typedef FuriHalNfcError (*FuriHalNfcRx)( - FuriHalSpiBusHandle* handle, + const FuriHalSpiBusHandle* handle, uint8_t* rx_data, size_t rx_data_size, size_t* rx_bits); @@ -69,7 +69,7 @@ typedef FuriHalNfcEvent (*FuriHalNfcWaitEvent)(uint32_t timeout_ms); * @param[in,out] handle pointer to the NFC chip SPI handle. * @returns FuriHalNfcErrorNone on success, any other error code on failure. */ -typedef FuriHalNfcError (*FuriHalNfcSleep)(FuriHalSpiBusHandle* handle); +typedef FuriHalNfcError (*FuriHalNfcSleep)(const FuriHalSpiBusHandle* handle); /** * @brief Go to idle in listener mode. @@ -79,7 +79,7 @@ typedef FuriHalNfcError (*FuriHalNfcSleep)(FuriHalSpiBusHandle* handle); * @param[in,out] handle pointer to the NFC chip SPI handle. * @returns FuriHalNfcErrorNone on success, any other error code on failure. */ -typedef FuriHalNfcError (*FuriHalNfcIdle)(FuriHalSpiBusHandle* handle); +typedef FuriHalNfcError (*FuriHalNfcIdle)(const FuriHalSpiBusHandle* handle); /** * @brief Technology-specific compenstaion values for pollers. diff --git a/targets/f7/furi_hal/furi_hal_sd.c b/targets/f7/furi_hal/furi_hal_sd.c index eca5b6da9..152192736 100644 --- a/targets/f7/furi_hal/furi_hal_sd.c +++ b/targets/f7/furi_hal/furi_hal_sd.c @@ -204,7 +204,7 @@ typedef struct { } SD_CardInfo; /** Pointer to currently used SPI Handle */ -FuriHalSpiBusHandle* furi_hal_sd_spi_handle = NULL; +const FuriHalSpiBusHandle* furi_hal_sd_spi_handle = NULL; static inline void sd_spi_select_card(void) { furi_hal_gpio_write(furi_hal_sd_spi_handle->cs, false); diff --git a/targets/f7/furi_hal/furi_hal_spi.c b/targets/f7/furi_hal/furi_hal_spi.c index 2a7cb7c25..9997d278d 100644 --- a/targets/f7/furi_hal/furi_hal_spi.c +++ b/targets/f7/furi_hal/furi_hal_spi.c @@ -38,17 +38,17 @@ void furi_hal_spi_bus_deinit(FuriHalSpiBus* bus) { bus->callback(bus, FuriHalSpiBusEventDeinit); } -void furi_hal_spi_bus_handle_init(FuriHalSpiBusHandle* handle) { +void furi_hal_spi_bus_handle_init(const FuriHalSpiBusHandle* handle) { furi_check(handle); handle->callback(handle, FuriHalSpiBusHandleEventInit); } -void furi_hal_spi_bus_handle_deinit(FuriHalSpiBusHandle* handle) { +void furi_hal_spi_bus_handle_deinit(const FuriHalSpiBusHandle* handle) { furi_check(handle); handle->callback(handle, FuriHalSpiBusHandleEventDeinit); } -void furi_hal_spi_acquire(FuriHalSpiBusHandle* handle) { +void furi_hal_spi_acquire(const FuriHalSpiBusHandle* handle) { furi_check(handle); furi_hal_power_insomnia_enter(); @@ -62,7 +62,7 @@ void furi_hal_spi_acquire(FuriHalSpiBusHandle* handle) { handle->callback(handle, FuriHalSpiBusHandleEventActivate); } -void furi_hal_spi_release(FuriHalSpiBusHandle* handle) { +void furi_hal_spi_release(const FuriHalSpiBusHandle* handle) { furi_check(handle); furi_check(handle->bus->current_handle == handle); @@ -77,7 +77,7 @@ void furi_hal_spi_release(FuriHalSpiBusHandle* handle) { furi_hal_power_insomnia_exit(); } -static void furi_hal_spi_bus_end_txrx(FuriHalSpiBusHandle* handle, uint32_t timeout) { +static void furi_hal_spi_bus_end_txrx(const FuriHalSpiBusHandle* handle, uint32_t timeout) { UNUSED(timeout); // FIXME while(LL_SPI_GetTxFIFOLevel(handle->bus->spi) != LL_SPI_TX_FIFO_EMPTY) ; @@ -89,7 +89,7 @@ static void furi_hal_spi_bus_end_txrx(FuriHalSpiBusHandle* handle, uint32_t time } bool furi_hal_spi_bus_rx( - FuriHalSpiBusHandle* handle, + const FuriHalSpiBusHandle* handle, uint8_t* buffer, size_t size, uint32_t timeout) { @@ -102,7 +102,7 @@ bool furi_hal_spi_bus_rx( } bool furi_hal_spi_bus_tx( - FuriHalSpiBusHandle* handle, + const FuriHalSpiBusHandle* handle, const uint8_t* buffer, size_t size, uint32_t timeout) { @@ -128,7 +128,7 @@ bool furi_hal_spi_bus_tx( } bool furi_hal_spi_bus_trx( - FuriHalSpiBusHandle* handle, + const FuriHalSpiBusHandle* handle, const uint8_t* tx_buffer, uint8_t* rx_buffer, size_t size, @@ -192,7 +192,7 @@ static void spi_dma_isr(void* context) { } bool furi_hal_spi_bus_trx_dma( - FuriHalSpiBusHandle* handle, + const FuriHalSpiBusHandle* handle, uint8_t* tx_buffer, uint8_t* rx_buffer, size_t size, diff --git a/targets/f7/furi_hal/furi_hal_spi_config.c b/targets/f7/furi_hal/furi_hal_spi_config.c index 8a694961a..ece0c05f7 100644 --- a/targets/f7/furi_hal/furi_hal_spi_config.c +++ b/targets/f7/furi_hal/furi_hal_spi_config.c @@ -147,7 +147,7 @@ FuriHalSpiBus furi_hal_spi_bus_d = { /* SPI Bus Handles */ inline static void furi_hal_spi_bus_r_handle_event_callback( - FuriHalSpiBusHandle* handle, + const FuriHalSpiBusHandle* handle, FuriHalSpiBusHandleEvent event, const LL_SPI_InitTypeDef* preset) { if(event == FuriHalSpiBusHandleEventInit) { @@ -193,7 +193,7 @@ inline static void furi_hal_spi_bus_r_handle_event_callback( } inline static void furi_hal_spi_bus_external_handle_event_callback( - FuriHalSpiBusHandle* handle, + const FuriHalSpiBusHandle* handle, FuriHalSpiBusHandleEvent event, const LL_SPI_InitTypeDef* preset) { if(event == FuriHalSpiBusHandleEventInit) { @@ -239,7 +239,7 @@ inline static void furi_hal_spi_bus_external_handle_event_callback( } inline static void furi_hal_spi_bus_nfc_handle_event_callback( - FuriHalSpiBusHandle* handle, + const FuriHalSpiBusHandle* handle, FuriHalSpiBusHandleEvent event, const LL_SPI_InitTypeDef* preset) { if(event == FuriHalSpiBusHandleEventInit) { @@ -305,12 +305,12 @@ inline static void furi_hal_spi_bus_nfc_handle_event_callback( } static void furi_hal_spi_bus_handle_subghz_event_callback( - FuriHalSpiBusHandle* handle, + const FuriHalSpiBusHandle* handle, FuriHalSpiBusHandleEvent event) { furi_hal_spi_bus_r_handle_event_callback(handle, event, &furi_hal_spi_preset_1edge_low_8m); } -FuriHalSpiBusHandle furi_hal_spi_bus_handle_subghz = { +const FuriHalSpiBusHandle furi_hal_spi_bus_handle_subghz = { .bus = &furi_hal_spi_bus_r, .callback = furi_hal_spi_bus_handle_subghz_event_callback, .miso = &gpio_spi_r_miso, @@ -320,12 +320,12 @@ FuriHalSpiBusHandle furi_hal_spi_bus_handle_subghz = { }; static void furi_hal_spi_bus_handle_nfc_event_callback( - FuriHalSpiBusHandle* handle, + const FuriHalSpiBusHandle* handle, FuriHalSpiBusHandleEvent event) { furi_hal_spi_bus_nfc_handle_event_callback(handle, event, &furi_hal_spi_preset_2edge_low_8m); } -FuriHalSpiBusHandle furi_hal_spi_bus_handle_nfc = { +const FuriHalSpiBusHandle furi_hal_spi_bus_handle_nfc = { .bus = &furi_hal_spi_bus_r, .callback = furi_hal_spi_bus_handle_nfc_event_callback, .miso = &gpio_spi_r_miso, @@ -335,13 +335,13 @@ FuriHalSpiBusHandle furi_hal_spi_bus_handle_nfc = { }; static void furi_hal_spi_bus_handle_external_event_callback( - FuriHalSpiBusHandle* handle, + const FuriHalSpiBusHandle* handle, FuriHalSpiBusHandleEvent event) { furi_hal_spi_bus_external_handle_event_callback( handle, event, &furi_hal_spi_preset_1edge_low_2m); } -FuriHalSpiBusHandle furi_hal_spi_bus_handle_external = { +const FuriHalSpiBusHandle furi_hal_spi_bus_handle_external = { .bus = &furi_hal_spi_bus_r, .callback = furi_hal_spi_bus_handle_external_event_callback, .miso = &gpio_ext_pa6, @@ -351,7 +351,7 @@ FuriHalSpiBusHandle furi_hal_spi_bus_handle_external = { }; inline static void furi_hal_spi_bus_d_handle_event_callback( - FuriHalSpiBusHandle* handle, + const FuriHalSpiBusHandle* handle, FuriHalSpiBusHandleEvent event, const LL_SPI_InitTypeDef* preset) { if(event == FuriHalSpiBusHandleEventInit) { @@ -392,12 +392,12 @@ inline static void furi_hal_spi_bus_d_handle_event_callback( } static void furi_hal_spi_bus_handle_display_event_callback( - FuriHalSpiBusHandle* handle, + const FuriHalSpiBusHandle* handle, FuriHalSpiBusHandleEvent event) { furi_hal_spi_bus_d_handle_event_callback(handle, event, &furi_hal_spi_preset_1edge_low_4m); } -FuriHalSpiBusHandle furi_hal_spi_bus_handle_display = { +const FuriHalSpiBusHandle furi_hal_spi_bus_handle_display = { .bus = &furi_hal_spi_bus_d, .callback = furi_hal_spi_bus_handle_display_event_callback, .miso = &gpio_spi_d_miso, @@ -407,12 +407,12 @@ FuriHalSpiBusHandle furi_hal_spi_bus_handle_display = { }; static void furi_hal_spi_bus_handle_sd_fast_event_callback( - FuriHalSpiBusHandle* handle, + const FuriHalSpiBusHandle* handle, FuriHalSpiBusHandleEvent event) { furi_hal_spi_bus_d_handle_event_callback(handle, event, &furi_hal_spi_preset_1edge_low_16m); } -FuriHalSpiBusHandle furi_hal_spi_bus_handle_sd_fast = { +const FuriHalSpiBusHandle furi_hal_spi_bus_handle_sd_fast = { .bus = &furi_hal_spi_bus_d, .callback = furi_hal_spi_bus_handle_sd_fast_event_callback, .miso = &gpio_spi_d_miso, @@ -422,12 +422,12 @@ FuriHalSpiBusHandle furi_hal_spi_bus_handle_sd_fast = { }; static void furi_hal_spi_bus_handle_sd_slow_event_callback( - FuriHalSpiBusHandle* handle, + const FuriHalSpiBusHandle* handle, FuriHalSpiBusHandleEvent event) { furi_hal_spi_bus_d_handle_event_callback(handle, event, &furi_hal_spi_preset_1edge_low_2m); } -FuriHalSpiBusHandle furi_hal_spi_bus_handle_sd_slow = { +const FuriHalSpiBusHandle furi_hal_spi_bus_handle_sd_slow = { .bus = &furi_hal_spi_bus_d, .callback = furi_hal_spi_bus_handle_sd_slow_event_callback, .miso = &gpio_spi_d_miso, diff --git a/targets/f7/furi_hal/furi_hal_spi_config.h b/targets/f7/furi_hal/furi_hal_spi_config.h index eab633a19..e90cd7061 100644 --- a/targets/f7/furi_hal/furi_hal_spi_config.h +++ b/targets/f7/furi_hal/furi_hal_spi_config.h @@ -28,10 +28,10 @@ extern FuriHalSpiBus furi_hal_spi_bus_r; extern FuriHalSpiBus furi_hal_spi_bus_d; /** CC1101 on `furi_hal_spi_bus_r` */ -extern FuriHalSpiBusHandle furi_hal_spi_bus_handle_subghz; +extern const FuriHalSpiBusHandle furi_hal_spi_bus_handle_subghz; /** ST25R3916 on `furi_hal_spi_bus_r` */ -extern FuriHalSpiBusHandle furi_hal_spi_bus_handle_nfc; +extern const FuriHalSpiBusHandle furi_hal_spi_bus_handle_nfc; /** External on `furi_hal_spi_bus_r` * Preset: `furi_hal_spi_preset_1edge_low_2m` @@ -45,16 +45,16 @@ extern FuriHalSpiBusHandle furi_hal_spi_bus_handle_nfc; * Bus pins are floating on inactive state, CS high after initialization * */ -extern FuriHalSpiBusHandle furi_hal_spi_bus_handle_external; +extern const FuriHalSpiBusHandle furi_hal_spi_bus_handle_external; /** ST7567(Display) on `furi_hal_spi_bus_d` */ -extern FuriHalSpiBusHandle furi_hal_spi_bus_handle_display; +extern const FuriHalSpiBusHandle furi_hal_spi_bus_handle_display; /** SdCard in fast mode on `furi_hal_spi_bus_d` */ -extern FuriHalSpiBusHandle furi_hal_spi_bus_handle_sd_fast; +extern const FuriHalSpiBusHandle furi_hal_spi_bus_handle_sd_fast; /** SdCard in slow mode on `furi_hal_spi_bus_d` */ -extern FuriHalSpiBusHandle furi_hal_spi_bus_handle_sd_slow; +extern const FuriHalSpiBusHandle furi_hal_spi_bus_handle_sd_slow; #ifdef __cplusplus } diff --git a/targets/f7/furi_hal/furi_hal_spi_types.h b/targets/f7/furi_hal/furi_hal_spi_types.h index ecc18d50d..9bf138ac0 100644 --- a/targets/f7/furi_hal/furi_hal_spi_types.h +++ b/targets/f7/furi_hal/furi_hal_spi_types.h @@ -31,7 +31,7 @@ typedef void (*FuriHalSpiBusEventCallback)(FuriHalSpiBus* bus, FuriHalSpiBusEven struct FuriHalSpiBus { SPI_TypeDef* spi; FuriHalSpiBusEventCallback callback; - FuriHalSpiBusHandle* current_handle; + const FuriHalSpiBusHandle* current_handle; }; /** FuriHal spi handle states */ @@ -44,7 +44,7 @@ typedef enum { /** FuriHal spi handle event callback */ typedef void (*FuriHalSpiBusHandleEventCallback)( - FuriHalSpiBusHandle* handle, + const FuriHalSpiBusHandle* handle, FuriHalSpiBusHandleEvent event); /** FuriHal spi handle */ diff --git a/targets/furi_hal_include/furi_hal_spi.h b/targets/furi_hal_include/furi_hal_spi.h index d497dff5c..41f3abdaa 100644 --- a/targets/furi_hal_include/furi_hal_spi.h +++ b/targets/furi_hal_include/furi_hal_spi.h @@ -35,13 +35,13 @@ void furi_hal_spi_bus_deinit(FuriHalSpiBus* bus); * * @param handle pointer to FuriHalSpiBusHandle instance */ -void furi_hal_spi_bus_handle_init(FuriHalSpiBusHandle* handle); +void furi_hal_spi_bus_handle_init(const FuriHalSpiBusHandle* handle); /** Deinitialize SPI Bus Handle * * @param handle pointer to FuriHalSpiBusHandle instance */ -void furi_hal_spi_bus_handle_deinit(FuriHalSpiBusHandle* handle); +void furi_hal_spi_bus_handle_deinit(const FuriHalSpiBusHandle* handle); /** Acquire SPI bus * @@ -49,7 +49,7 @@ void furi_hal_spi_bus_handle_deinit(FuriHalSpiBusHandle* handle); * * @param handle pointer to FuriHalSpiBusHandle instance */ -void furi_hal_spi_acquire(FuriHalSpiBusHandle* handle); +void furi_hal_spi_acquire(const FuriHalSpiBusHandle* handle); /** Release SPI bus * @@ -57,7 +57,7 @@ void furi_hal_spi_acquire(FuriHalSpiBusHandle* handle); * * @param handle pointer to FuriHalSpiBusHandle instance */ -void furi_hal_spi_release(FuriHalSpiBusHandle* handle); +void furi_hal_spi_release(const FuriHalSpiBusHandle* handle); /** SPI Receive * @@ -69,7 +69,7 @@ void furi_hal_spi_release(FuriHalSpiBusHandle* handle); * @return true on sucess */ bool furi_hal_spi_bus_rx( - FuriHalSpiBusHandle* handle, + const FuriHalSpiBusHandle* handle, uint8_t* buffer, size_t size, uint32_t timeout); @@ -84,7 +84,7 @@ bool furi_hal_spi_bus_rx( * @return true on success */ bool furi_hal_spi_bus_tx( - FuriHalSpiBusHandle* handle, + const FuriHalSpiBusHandle* handle, const uint8_t* buffer, size_t size, uint32_t timeout); @@ -100,7 +100,7 @@ bool furi_hal_spi_bus_tx( * @return true on success */ bool furi_hal_spi_bus_trx( - FuriHalSpiBusHandle* handle, + const FuriHalSpiBusHandle* handle, const uint8_t* tx_buffer, uint8_t* rx_buffer, size_t size, @@ -117,7 +117,7 @@ bool furi_hal_spi_bus_trx( * @return true on success */ bool furi_hal_spi_bus_trx_dma( - FuriHalSpiBusHandle* handle, + const FuriHalSpiBusHandle* handle, uint8_t* tx_buffer, uint8_t* rx_buffer, size_t size, From 6c17b785ecbcb5cd3b835b162a12888cc5de03c9 Mon Sep 17 00:00:00 2001 From: hedger Date: Sat, 22 Feb 2025 19:23:30 +0000 Subject: [PATCH 082/268] hal: made FuriHalI2cBusHandle static --- lib/drivers/bq25896.c | 38 ++++++++--------- lib/drivers/bq25896.h | 38 ++++++++--------- lib/drivers/bq27220.c | 52 +++++++++++++---------- lib/drivers/bq27220.h | 40 +++++++++-------- lib/drivers/lp5562.c | 31 +++++++++----- lib/drivers/lp5562.h | 31 +++++++++----- targets/f18/api_symbols.csv | 32 +++++++------- targets/f7/api_symbols.csv | 34 ++++++++------- targets/f7/furi_hal/furi_hal_i2c.c | 31 ++++++++------ targets/f7/furi_hal/furi_hal_i2c_config.c | 8 ++-- targets/f7/furi_hal/furi_hal_i2c_config.h | 4 +- targets/f7/furi_hal/furi_hal_i2c_types.h | 4 +- targets/furi_hal_include/furi_hal_i2c.h | 31 ++++++++------ 13 files changed, 206 insertions(+), 168 deletions(-) diff --git a/lib/drivers/bq25896.c b/lib/drivers/bq25896.c index 76aae5e82..a44ff8c39 100644 --- a/lib/drivers/bq25896.c +++ b/lib/drivers/bq25896.c @@ -35,7 +35,7 @@ typedef struct { static bq25896_regs_t bq25896_regs; -bool bq25896_init(FuriHalI2cBusHandle* handle) { +bool bq25896_init(const FuriHalI2cBusHandle* handle) { bool result = true; bq25896_regs.r14.REG_RST = 1; @@ -78,19 +78,19 @@ bool bq25896_init(FuriHalI2cBusHandle* handle) { return result; } -void bq25896_set_boost_lim(FuriHalI2cBusHandle* handle, BoostLim boost_lim) { +void bq25896_set_boost_lim(const FuriHalI2cBusHandle* handle, BoostLim boost_lim) { bq25896_regs.r0A.BOOST_LIM = boost_lim; furi_hal_i2c_write_reg_8( handle, BQ25896_ADDRESS, 0x0A, *(uint8_t*)&bq25896_regs.r0A, BQ25896_I2C_TIMEOUT); } -void bq25896_poweroff(FuriHalI2cBusHandle* handle) { +void bq25896_poweroff(const FuriHalI2cBusHandle* handle) { bq25896_regs.r09.BATFET_DIS = 1; furi_hal_i2c_write_reg_8( handle, BQ25896_ADDRESS, 0x09, *(uint8_t*)&bq25896_regs.r09, BQ25896_I2C_TIMEOUT); } -ChrgStat bq25896_get_charge_status(FuriHalI2cBusHandle* handle) { +ChrgStat bq25896_get_charge_status(const FuriHalI2cBusHandle* handle) { furi_hal_i2c_read_mem( handle, BQ25896_ADDRESS, @@ -103,52 +103,52 @@ ChrgStat bq25896_get_charge_status(FuriHalI2cBusHandle* handle) { return bq25896_regs.r0B.CHRG_STAT; } -bool bq25896_is_charging(FuriHalI2cBusHandle* handle) { +bool bq25896_is_charging(const FuriHalI2cBusHandle* handle) { // Include precharge, fast charging, and charging termination done as "charging" return bq25896_get_charge_status(handle) != ChrgStatNo; } -bool bq25896_is_charging_done(FuriHalI2cBusHandle* handle) { +bool bq25896_is_charging_done(const FuriHalI2cBusHandle* handle) { return bq25896_get_charge_status(handle) == ChrgStatDone; } -void bq25896_enable_charging(FuriHalI2cBusHandle* handle) { +void bq25896_enable_charging(const FuriHalI2cBusHandle* handle) { bq25896_regs.r03.CHG_CONFIG = 1; furi_hal_i2c_write_reg_8( handle, BQ25896_ADDRESS, 0x03, *(uint8_t*)&bq25896_regs.r03, BQ25896_I2C_TIMEOUT); } -void bq25896_disable_charging(FuriHalI2cBusHandle* handle) { +void bq25896_disable_charging(const FuriHalI2cBusHandle* handle) { bq25896_regs.r03.CHG_CONFIG = 0; furi_hal_i2c_write_reg_8( handle, BQ25896_ADDRESS, 0x03, *(uint8_t*)&bq25896_regs.r03, BQ25896_I2C_TIMEOUT); } -void bq25896_enable_otg(FuriHalI2cBusHandle* handle) { +void bq25896_enable_otg(const FuriHalI2cBusHandle* handle) { bq25896_regs.r03.OTG_CONFIG = 1; furi_hal_i2c_write_reg_8( handle, BQ25896_ADDRESS, 0x03, *(uint8_t*)&bq25896_regs.r03, BQ25896_I2C_TIMEOUT); } -void bq25896_disable_otg(FuriHalI2cBusHandle* handle) { +void bq25896_disable_otg(const FuriHalI2cBusHandle* handle) { bq25896_regs.r03.OTG_CONFIG = 0; furi_hal_i2c_write_reg_8( handle, BQ25896_ADDRESS, 0x03, *(uint8_t*)&bq25896_regs.r03, BQ25896_I2C_TIMEOUT); } -bool bq25896_is_otg_enabled(FuriHalI2cBusHandle* handle) { +bool bq25896_is_otg_enabled(const FuriHalI2cBusHandle* handle) { furi_hal_i2c_read_reg_8( handle, BQ25896_ADDRESS, 0x03, (uint8_t*)&bq25896_regs.r03, BQ25896_I2C_TIMEOUT); return bq25896_regs.r03.OTG_CONFIG; } -uint16_t bq25896_get_vreg_voltage(FuriHalI2cBusHandle* handle) { +uint16_t bq25896_get_vreg_voltage(const FuriHalI2cBusHandle* handle) { furi_hal_i2c_read_reg_8( handle, BQ25896_ADDRESS, 0x06, (uint8_t*)&bq25896_regs.r06, BQ25896_I2C_TIMEOUT); return (uint16_t)bq25896_regs.r06.VREG * 16 + 3840; } -void bq25896_set_vreg_voltage(FuriHalI2cBusHandle* handle, uint16_t vreg_voltage) { +void bq25896_set_vreg_voltage(const FuriHalI2cBusHandle* handle, uint16_t vreg_voltage) { if(vreg_voltage < 3840) { // Minimum valid value is 3840 mV vreg_voltage = 3840; @@ -166,13 +166,13 @@ void bq25896_set_vreg_voltage(FuriHalI2cBusHandle* handle, uint16_t vreg_voltage handle, BQ25896_ADDRESS, 0x06, *(uint8_t*)&bq25896_regs.r06, BQ25896_I2C_TIMEOUT); } -bool bq25896_check_otg_fault(FuriHalI2cBusHandle* handle) { +bool bq25896_check_otg_fault(const FuriHalI2cBusHandle* handle) { furi_hal_i2c_read_reg_8( handle, BQ25896_ADDRESS, 0x0C, (uint8_t*)&bq25896_regs.r0C, BQ25896_I2C_TIMEOUT); return bq25896_regs.r0C.BOOST_FAULT; } -uint16_t bq25896_get_vbus_voltage(FuriHalI2cBusHandle* handle) { +uint16_t bq25896_get_vbus_voltage(const FuriHalI2cBusHandle* handle) { furi_hal_i2c_read_reg_8( handle, BQ25896_ADDRESS, 0x11, (uint8_t*)&bq25896_regs.r11, BQ25896_I2C_TIMEOUT); if(bq25896_regs.r11.VBUS_GD) { @@ -182,25 +182,25 @@ uint16_t bq25896_get_vbus_voltage(FuriHalI2cBusHandle* handle) { } } -uint16_t bq25896_get_vsys_voltage(FuriHalI2cBusHandle* handle) { +uint16_t bq25896_get_vsys_voltage(const FuriHalI2cBusHandle* handle) { furi_hal_i2c_read_reg_8( handle, BQ25896_ADDRESS, 0x0F, (uint8_t*)&bq25896_regs.r0F, BQ25896_I2C_TIMEOUT); return (uint16_t)bq25896_regs.r0F.SYSV * 20 + 2304; } -uint16_t bq25896_get_vbat_voltage(FuriHalI2cBusHandle* handle) { +uint16_t bq25896_get_vbat_voltage(const FuriHalI2cBusHandle* handle) { furi_hal_i2c_read_reg_8( handle, BQ25896_ADDRESS, 0x0E, (uint8_t*)&bq25896_regs.r0E, BQ25896_I2C_TIMEOUT); return (uint16_t)bq25896_regs.r0E.BATV * 20 + 2304; } -uint16_t bq25896_get_vbat_current(FuriHalI2cBusHandle* handle) { +uint16_t bq25896_get_vbat_current(const FuriHalI2cBusHandle* handle) { furi_hal_i2c_read_reg_8( handle, BQ25896_ADDRESS, 0x12, (uint8_t*)&bq25896_regs.r12, BQ25896_I2C_TIMEOUT); return (uint16_t)bq25896_regs.r12.ICHGR * 50; } -uint32_t bq25896_get_ntc_mpct(FuriHalI2cBusHandle* handle) { +uint32_t bq25896_get_ntc_mpct(const FuriHalI2cBusHandle* handle) { furi_hal_i2c_read_reg_8( handle, BQ25896_ADDRESS, 0x10, (uint8_t*)&bq25896_regs.r10, BQ25896_I2C_TIMEOUT); return (uint32_t)bq25896_regs.r10.TSPCT * 465 + 21000; diff --git a/lib/drivers/bq25896.h b/lib/drivers/bq25896.h index d35625ab3..69c19868c 100644 --- a/lib/drivers/bq25896.h +++ b/lib/drivers/bq25896.h @@ -7,61 +7,61 @@ #include /** Initialize Driver */ -bool bq25896_init(FuriHalI2cBusHandle* handle); +bool bq25896_init(const FuriHalI2cBusHandle* handle); /** Set boost lim*/ -void bq25896_set_boost_lim(FuriHalI2cBusHandle* handle, BoostLim boost_lim); +void bq25896_set_boost_lim(const FuriHalI2cBusHandle* handle, BoostLim boost_lim); /** Send device into shipping mode */ -void bq25896_poweroff(FuriHalI2cBusHandle* handle); +void bq25896_poweroff(const FuriHalI2cBusHandle* handle); /** Get charging status */ -ChrgStat bq25896_get_charge_status(FuriHalI2cBusHandle* handle); +ChrgStat bq25896_get_charge_status(const FuriHalI2cBusHandle* handle); /** Is currently charging */ -bool bq25896_is_charging(FuriHalI2cBusHandle* handle); +bool bq25896_is_charging(const FuriHalI2cBusHandle* handle); /** Is charging completed while connected to charger */ -bool bq25896_is_charging_done(FuriHalI2cBusHandle* handle); +bool bq25896_is_charging_done(const FuriHalI2cBusHandle* handle); /** Enable charging */ -void bq25896_enable_charging(FuriHalI2cBusHandle* handle); +void bq25896_enable_charging(const FuriHalI2cBusHandle* handle); /** Disable charging */ -void bq25896_disable_charging(FuriHalI2cBusHandle* handle); +void bq25896_disable_charging(const FuriHalI2cBusHandle* handle); /** Enable otg */ -void bq25896_enable_otg(FuriHalI2cBusHandle* handle); +void bq25896_enable_otg(const FuriHalI2cBusHandle* handle); /** Disable otg */ -void bq25896_disable_otg(FuriHalI2cBusHandle* handle); +void bq25896_disable_otg(const FuriHalI2cBusHandle* handle); /** Is otg enabled */ -bool bq25896_is_otg_enabled(FuriHalI2cBusHandle* handle); +bool bq25896_is_otg_enabled(const FuriHalI2cBusHandle* handle); /** Get VREG (charging limit) voltage in mV */ -uint16_t bq25896_get_vreg_voltage(FuriHalI2cBusHandle* handle); +uint16_t bq25896_get_vreg_voltage(const FuriHalI2cBusHandle* handle); /** Set VREG (charging limit) voltage in mV * * Valid range: 3840mV - 4208mV, in steps of 16mV */ -void bq25896_set_vreg_voltage(FuriHalI2cBusHandle* handle, uint16_t vreg_voltage); +void bq25896_set_vreg_voltage(const FuriHalI2cBusHandle* handle, uint16_t vreg_voltage); /** Check OTG BOOST Fault status */ -bool bq25896_check_otg_fault(FuriHalI2cBusHandle* handle); +bool bq25896_check_otg_fault(const FuriHalI2cBusHandle* handle); /** Get VBUS Voltage in mV */ -uint16_t bq25896_get_vbus_voltage(FuriHalI2cBusHandle* handle); +uint16_t bq25896_get_vbus_voltage(const FuriHalI2cBusHandle* handle); /** Get VSYS Voltage in mV */ -uint16_t bq25896_get_vsys_voltage(FuriHalI2cBusHandle* handle); +uint16_t bq25896_get_vsys_voltage(const FuriHalI2cBusHandle* handle); /** Get VBAT Voltage in mV */ -uint16_t bq25896_get_vbat_voltage(FuriHalI2cBusHandle* handle); +uint16_t bq25896_get_vbat_voltage(const FuriHalI2cBusHandle* handle); /** Get VBAT current in mA */ -uint16_t bq25896_get_vbat_current(FuriHalI2cBusHandle* handle); +uint16_t bq25896_get_vbat_current(const FuriHalI2cBusHandle* handle); /** Get NTC voltage in mpct of REGN */ -uint32_t bq25896_get_ntc_mpct(FuriHalI2cBusHandle* handle); +uint32_t bq25896_get_ntc_mpct(const FuriHalI2cBusHandle* handle); diff --git a/lib/drivers/bq27220.c b/lib/drivers/bq27220.c index d60e287da..127f7c6b9 100644 --- a/lib/drivers/bq27220.c +++ b/lib/drivers/bq27220.c @@ -43,7 +43,7 @@ #endif static inline bool bq27220_read_reg( - FuriHalI2cBusHandle* handle, + const FuriHalI2cBusHandle* handle, uint8_t address, uint8_t* buffer, size_t buffer_size) { @@ -52,7 +52,7 @@ static inline bool bq27220_read_reg( } static inline bool bq27220_write( - FuriHalI2cBusHandle* handle, + const FuriHalI2cBusHandle* handle, uint8_t address, const uint8_t* buffer, size_t buffer_size) { @@ -60,11 +60,11 @@ static inline bool bq27220_write( handle, BQ27220_ADDRESS, address, buffer, buffer_size, BQ27220_I2C_TIMEOUT); } -static inline bool bq27220_control(FuriHalI2cBusHandle* handle, uint16_t control) { +static inline bool bq27220_control(const FuriHalI2cBusHandle* handle, uint16_t control) { return bq27220_write(handle, CommandControl, (uint8_t*)&control, 2); } -static uint16_t bq27220_read_word(FuriHalI2cBusHandle* handle, uint8_t address) { +static uint16_t bq27220_read_word(const FuriHalI2cBusHandle* handle, uint8_t address) { uint16_t buf = BQ27220_ERROR; if(!bq27220_read_reg(handle, address, (uint8_t*)&buf, 2)) { @@ -83,7 +83,7 @@ static uint8_t bq27220_get_checksum(uint8_t* data, uint16_t len) { } static bool bq27220_parameter_check( - FuriHalI2cBusHandle* handle, + const FuriHalI2cBusHandle* handle, uint16_t address, uint32_t value, size_t size, @@ -163,7 +163,7 @@ static bool bq27220_parameter_check( } static bool bq27220_data_memory_check( - FuriHalI2cBusHandle* handle, + const FuriHalI2cBusHandle* handle, const BQ27220DMData* data_memory, bool update) { if(update) { @@ -268,7 +268,7 @@ static bool bq27220_data_memory_check( return result; } -bool bq27220_init(FuriHalI2cBusHandle* handle, const BQ27220DMData* data_memory) { +bool bq27220_init(const FuriHalI2cBusHandle* handle, const BQ27220DMData* data_memory) { bool result = false; bool reset_and_provisioning_required = false; @@ -365,7 +365,7 @@ bool bq27220_init(FuriHalI2cBusHandle* handle, const BQ27220DMData* data_memory) return result; } -bool bq27220_reset(FuriHalI2cBusHandle* handle) { +bool bq27220_reset(const FuriHalI2cBusHandle* handle) { bool result = false; do { if(!bq27220_control(handle, Control_RESET)) { @@ -396,7 +396,7 @@ bool bq27220_reset(FuriHalI2cBusHandle* handle) { return result; } -bool bq27220_seal(FuriHalI2cBusHandle* handle) { +bool bq27220_seal(const FuriHalI2cBusHandle* handle) { Bq27220OperationStatus operation_status = {0}; bool result = false; do { @@ -431,7 +431,7 @@ bool bq27220_seal(FuriHalI2cBusHandle* handle) { return result; } -bool bq27220_unseal(FuriHalI2cBusHandle* handle) { +bool bq27220_unseal(const FuriHalI2cBusHandle* handle) { Bq27220OperationStatus operation_status = {0}; bool result = false; do { @@ -465,7 +465,7 @@ bool bq27220_unseal(FuriHalI2cBusHandle* handle) { return result; } -bool bq27220_full_access(FuriHalI2cBusHandle* handle) { +bool bq27220_full_access(const FuriHalI2cBusHandle* handle) { bool result = false; do { @@ -518,29 +518,35 @@ bool bq27220_full_access(FuriHalI2cBusHandle* handle) { return result; } -uint16_t bq27220_get_voltage(FuriHalI2cBusHandle* handle) { +uint16_t bq27220_get_voltage(const FuriHalI2cBusHandle* handle) { return bq27220_read_word(handle, CommandVoltage); } -int16_t bq27220_get_current(FuriHalI2cBusHandle* handle) { +int16_t bq27220_get_current(const FuriHalI2cBusHandle* handle) { return bq27220_read_word(handle, CommandCurrent); } -bool bq27220_get_control_status(FuriHalI2cBusHandle* handle, Bq27220ControlStatus* control_status) { +bool bq27220_get_control_status( + const FuriHalI2cBusHandle* handle, + Bq27220ControlStatus* control_status) { return bq27220_read_reg(handle, CommandControl, (uint8_t*)control_status, 2); } -bool bq27220_get_battery_status(FuriHalI2cBusHandle* handle, Bq27220BatteryStatus* battery_status) { +bool bq27220_get_battery_status( + const FuriHalI2cBusHandle* handle, + Bq27220BatteryStatus* battery_status) { return bq27220_read_reg(handle, CommandBatteryStatus, (uint8_t*)battery_status, 2); } bool bq27220_get_operation_status( - FuriHalI2cBusHandle* handle, + const FuriHalI2cBusHandle* handle, Bq27220OperationStatus* operation_status) { return bq27220_read_reg(handle, CommandOperationStatus, (uint8_t*)operation_status, 2); } -bool bq27220_get_gauging_status(FuriHalI2cBusHandle* handle, Bq27220GaugingStatus* gauging_status) { +bool bq27220_get_gauging_status( + const FuriHalI2cBusHandle* handle, + Bq27220GaugingStatus* gauging_status) { // Request gauging data to be loaded to MAC if(!bq27220_control(handle, Control_GAUGING_STATUS)) { FURI_LOG_E(TAG, "DM SelectSubclass for read failed"); @@ -552,26 +558,26 @@ bool bq27220_get_gauging_status(FuriHalI2cBusHandle* handle, Bq27220GaugingStatu return bq27220_read_reg(handle, CommandMACData, (uint8_t*)gauging_status, 2); } -uint16_t bq27220_get_temperature(FuriHalI2cBusHandle* handle) { +uint16_t bq27220_get_temperature(const FuriHalI2cBusHandle* handle) { return bq27220_read_word(handle, CommandTemperature); } -uint16_t bq27220_get_full_charge_capacity(FuriHalI2cBusHandle* handle) { +uint16_t bq27220_get_full_charge_capacity(const FuriHalI2cBusHandle* handle) { return bq27220_read_word(handle, CommandFullChargeCapacity); } -uint16_t bq27220_get_design_capacity(FuriHalI2cBusHandle* handle) { +uint16_t bq27220_get_design_capacity(const FuriHalI2cBusHandle* handle) { return bq27220_read_word(handle, CommandDesignCapacity); } -uint16_t bq27220_get_remaining_capacity(FuriHalI2cBusHandle* handle) { +uint16_t bq27220_get_remaining_capacity(const FuriHalI2cBusHandle* handle) { return bq27220_read_word(handle, CommandRemainingCapacity); } -uint16_t bq27220_get_state_of_charge(FuriHalI2cBusHandle* handle) { +uint16_t bq27220_get_state_of_charge(const FuriHalI2cBusHandle* handle) { return bq27220_read_word(handle, CommandStateOfCharge); } -uint16_t bq27220_get_state_of_health(FuriHalI2cBusHandle* handle) { +uint16_t bq27220_get_state_of_health(const FuriHalI2cBusHandle* handle) { return bq27220_read_word(handle, CommandStateOfHealth); } diff --git a/lib/drivers/bq27220.h b/lib/drivers/bq27220.h index cdfcb20b1..addbf08f5 100644 --- a/lib/drivers/bq27220.h +++ b/lib/drivers/bq27220.h @@ -136,7 +136,7 @@ typedef struct BQ27220DMData BQ27220DMData; * * @return true on success, false otherwise */ -bool bq27220_init(FuriHalI2cBusHandle* handle, const BQ27220DMData* data_memory); +bool bq27220_init(const FuriHalI2cBusHandle* handle, const BQ27220DMData* data_memory); /** Reset gauge * @@ -144,7 +144,7 @@ bool bq27220_init(FuriHalI2cBusHandle* handle, const BQ27220DMData* data_memory) * * @return true on success, false otherwise */ -bool bq27220_reset(FuriHalI2cBusHandle* handle); +bool bq27220_reset(const FuriHalI2cBusHandle* handle); /** Seal gauge access * @@ -152,7 +152,7 @@ bool bq27220_reset(FuriHalI2cBusHandle* handle); * * @return true on success, false otherwise */ -bool bq27220_seal(FuriHalI2cBusHandle* handle); +bool bq27220_seal(const FuriHalI2cBusHandle* handle); /** Unseal gauge access * @@ -160,7 +160,7 @@ bool bq27220_seal(FuriHalI2cBusHandle* handle); * * @return true on success, false otherwise */ -bool bq27220_unseal(FuriHalI2cBusHandle* handle); +bool bq27220_unseal(const FuriHalI2cBusHandle* handle); /** Get full access * @@ -170,7 +170,7 @@ bool bq27220_unseal(FuriHalI2cBusHandle* handle); * * @return true on success, false otherwise */ -bool bq27220_full_access(FuriHalI2cBusHandle* handle); +bool bq27220_full_access(const FuriHalI2cBusHandle* handle); /** Get battery voltage * @@ -178,7 +178,7 @@ bool bq27220_full_access(FuriHalI2cBusHandle* handle); * * @return voltage in mV or BQ27220_ERROR */ -uint16_t bq27220_get_voltage(FuriHalI2cBusHandle* handle); +uint16_t bq27220_get_voltage(const FuriHalI2cBusHandle* handle); /** Get current * @@ -186,7 +186,7 @@ uint16_t bq27220_get_voltage(FuriHalI2cBusHandle* handle); * * @return current in mA or BQ27220_ERROR */ -int16_t bq27220_get_current(FuriHalI2cBusHandle* handle); +int16_t bq27220_get_current(const FuriHalI2cBusHandle* handle); /** Get control status * @@ -195,7 +195,9 @@ int16_t bq27220_get_current(FuriHalI2cBusHandle* handle); * * @return true on success, false otherwise */ -bool bq27220_get_control_status(FuriHalI2cBusHandle* handle, Bq27220ControlStatus* control_status); +bool bq27220_get_control_status( + const FuriHalI2cBusHandle* handle, + Bq27220ControlStatus* control_status); /** Get battery status * @@ -204,7 +206,9 @@ bool bq27220_get_control_status(FuriHalI2cBusHandle* handle, Bq27220ControlStatu * * @return true on success, false otherwise */ -bool bq27220_get_battery_status(FuriHalI2cBusHandle* handle, Bq27220BatteryStatus* battery_status); +bool bq27220_get_battery_status( + const FuriHalI2cBusHandle* handle, + Bq27220BatteryStatus* battery_status); /** Get operation status * @@ -214,7 +218,7 @@ bool bq27220_get_battery_status(FuriHalI2cBusHandle* handle, Bq27220BatteryStatu * @return true on success, false otherwise */ bool bq27220_get_operation_status( - FuriHalI2cBusHandle* handle, + const FuriHalI2cBusHandle* handle, Bq27220OperationStatus* operation_status); /** Get gauging status @@ -224,7 +228,9 @@ bool bq27220_get_operation_status( * * @return true on success, false otherwise */ -bool bq27220_get_gauging_status(FuriHalI2cBusHandle* handle, Bq27220GaugingStatus* gauging_status); +bool bq27220_get_gauging_status( + const FuriHalI2cBusHandle* handle, + Bq27220GaugingStatus* gauging_status); /** Get temperature * @@ -232,7 +238,7 @@ bool bq27220_get_gauging_status(FuriHalI2cBusHandle* handle, Bq27220GaugingStatu * * @return temperature in units of 0.1°K */ -uint16_t bq27220_get_temperature(FuriHalI2cBusHandle* handle); +uint16_t bq27220_get_temperature(const FuriHalI2cBusHandle* handle); /** Get compensated full charge capacity * @@ -240,7 +246,7 @@ uint16_t bq27220_get_temperature(FuriHalI2cBusHandle* handle); * * @return full charge capacity in mAh or BQ27220_ERROR */ -uint16_t bq27220_get_full_charge_capacity(FuriHalI2cBusHandle* handle); +uint16_t bq27220_get_full_charge_capacity(const FuriHalI2cBusHandle* handle); /** Get design capacity * @@ -248,7 +254,7 @@ uint16_t bq27220_get_full_charge_capacity(FuriHalI2cBusHandle* handle); * * @return design capacity in mAh or BQ27220_ERROR */ -uint16_t bq27220_get_design_capacity(FuriHalI2cBusHandle* handle); +uint16_t bq27220_get_design_capacity(const FuriHalI2cBusHandle* handle); /** Get remaining capacity * @@ -256,7 +262,7 @@ uint16_t bq27220_get_design_capacity(FuriHalI2cBusHandle* handle); * * @return remaining capacity in mAh or BQ27220_ERROR */ -uint16_t bq27220_get_remaining_capacity(FuriHalI2cBusHandle* handle); +uint16_t bq27220_get_remaining_capacity(const FuriHalI2cBusHandle* handle); /** Get predicted remaining battery capacity * @@ -264,7 +270,7 @@ uint16_t bq27220_get_remaining_capacity(FuriHalI2cBusHandle* handle); * * @return state of charge in percents or BQ27220_ERROR */ -uint16_t bq27220_get_state_of_charge(FuriHalI2cBusHandle* handle); +uint16_t bq27220_get_state_of_charge(const FuriHalI2cBusHandle* handle); /** Get ratio of full charge capacity over design capacity * @@ -272,4 +278,4 @@ uint16_t bq27220_get_state_of_charge(FuriHalI2cBusHandle* handle); * * @return state of health in percents or BQ27220_ERROR */ -uint16_t bq27220_get_state_of_health(FuriHalI2cBusHandle* handle); +uint16_t bq27220_get_state_of_health(const FuriHalI2cBusHandle* handle); diff --git a/lib/drivers/lp5562.c b/lib/drivers/lp5562.c index 30a5b559a..7db9bbce4 100644 --- a/lib/drivers/lp5562.c +++ b/lib/drivers/lp5562.c @@ -3,12 +3,12 @@ #include "lp5562_reg.h" #include -void lp5562_reset(FuriHalI2cBusHandle* handle) { +void lp5562_reset(const FuriHalI2cBusHandle* handle) { Reg0D_Reset reg = {.value = 0xFF}; furi_hal_i2c_write_reg_8(handle, LP5562_ADDRESS, 0x0D, *(uint8_t*)®, LP5562_I2C_TIMEOUT); } -void lp5562_configure(FuriHalI2cBusHandle* handle) { +void lp5562_configure(const FuriHalI2cBusHandle* handle) { Reg08_Config config = {.INT_CLK_EN = true, .PS_EN = true, .PWM_HF = true}; furi_hal_i2c_write_reg_8(handle, LP5562_ADDRESS, 0x08, *(uint8_t*)&config, LP5562_I2C_TIMEOUT); @@ -21,14 +21,17 @@ void lp5562_configure(FuriHalI2cBusHandle* handle) { furi_hal_i2c_write_reg_8(handle, LP5562_ADDRESS, 0x70, *(uint8_t*)&map, LP5562_I2C_TIMEOUT); } -void lp5562_enable(FuriHalI2cBusHandle* handle) { +void lp5562_enable(const FuriHalI2cBusHandle* handle) { Reg00_Enable reg = {.CHIP_EN = true, .LOG_EN = true}; furi_hal_i2c_write_reg_8(handle, LP5562_ADDRESS, 0x00, *(uint8_t*)®, LP5562_I2C_TIMEOUT); //>488μs delay is required after writing to 0x00 register, otherwise program engine will not work furi_delay_us(500); } -void lp5562_set_channel_current(FuriHalI2cBusHandle* handle, LP5562Channel channel, uint8_t value) { +void lp5562_set_channel_current( + const FuriHalI2cBusHandle* handle, + LP5562Channel channel, + uint8_t value) { uint8_t reg_no; if(channel == LP5562ChannelRed) { reg_no = LP5562_CHANNEL_RED_CURRENT_REGISTER; @@ -44,7 +47,10 @@ void lp5562_set_channel_current(FuriHalI2cBusHandle* handle, LP5562Channel chann furi_hal_i2c_write_reg_8(handle, LP5562_ADDRESS, reg_no, value, LP5562_I2C_TIMEOUT); } -void lp5562_set_channel_value(FuriHalI2cBusHandle* handle, LP5562Channel channel, uint8_t value) { +void lp5562_set_channel_value( + const FuriHalI2cBusHandle* handle, + LP5562Channel channel, + uint8_t value) { uint8_t reg_no; if(channel == LP5562ChannelRed) { reg_no = LP5562_CHANNEL_RED_VALUE_REGISTER; @@ -60,7 +66,7 @@ void lp5562_set_channel_value(FuriHalI2cBusHandle* handle, LP5562Channel channel furi_hal_i2c_write_reg_8(handle, LP5562_ADDRESS, reg_no, value, LP5562_I2C_TIMEOUT); } -uint8_t lp5562_get_channel_value(FuriHalI2cBusHandle* handle, LP5562Channel channel) { +uint8_t lp5562_get_channel_value(const FuriHalI2cBusHandle* handle, LP5562Channel channel) { uint8_t reg_no; uint8_t value; if(channel == LP5562ChannelRed) { @@ -78,7 +84,10 @@ uint8_t lp5562_get_channel_value(FuriHalI2cBusHandle* handle, LP5562Channel chan return value; } -void lp5562_set_channel_src(FuriHalI2cBusHandle* handle, LP5562Channel channel, LP5562Engine src) { +void lp5562_set_channel_src( + const FuriHalI2cBusHandle* handle, + LP5562Channel channel, + LP5562Engine src) { uint8_t reg_val = 0; uint8_t bit_offset = 0; @@ -107,7 +116,7 @@ void lp5562_set_channel_src(FuriHalI2cBusHandle* handle, LP5562Channel channel, } void lp5562_execute_program( - FuriHalI2cBusHandle* handle, + const FuriHalI2cBusHandle* handle, LP5562Engine eng, LP5562Channel ch, uint16_t* program) { @@ -155,7 +164,7 @@ void lp5562_execute_program( furi_hal_i2c_write_reg_8(handle, LP5562_ADDRESS, 0x00, enable_reg, LP5562_I2C_TIMEOUT); } -void lp5562_stop_program(FuriHalI2cBusHandle* handle, LP5562Engine eng) { +void lp5562_stop_program(const FuriHalI2cBusHandle* handle, LP5562Engine eng) { if((eng < LP5562Engine1) || (eng > LP5562Engine3)) return; uint8_t reg_val = 0; uint8_t bit_offset = 0; @@ -169,7 +178,7 @@ void lp5562_stop_program(FuriHalI2cBusHandle* handle, LP5562Engine eng) { } void lp5562_execute_ramp( - FuriHalI2cBusHandle* handle, + const FuriHalI2cBusHandle* handle, LP5562Engine eng, LP5562Channel ch, uint8_t val_start, @@ -213,7 +222,7 @@ void lp5562_execute_ramp( } void lp5562_execute_blink( - FuriHalI2cBusHandle* handle, + const FuriHalI2cBusHandle* handle, LP5562Engine eng, LP5562Channel ch, uint16_t on_time, diff --git a/lib/drivers/lp5562.h b/lib/drivers/lp5562.h index f5ebeeae2..2e54e1ce3 100644 --- a/lib/drivers/lp5562.h +++ b/lib/drivers/lp5562.h @@ -20,39 +20,48 @@ typedef enum { } LP5562Engine; /** Initialize Driver */ -void lp5562_reset(FuriHalI2cBusHandle* handle); +void lp5562_reset(const FuriHalI2cBusHandle* handle); /** Configure Driver */ -void lp5562_configure(FuriHalI2cBusHandle* handle); +void lp5562_configure(const FuriHalI2cBusHandle* handle); /** Enable Driver */ -void lp5562_enable(FuriHalI2cBusHandle* handle); +void lp5562_enable(const FuriHalI2cBusHandle* handle); /** Set channel current */ -void lp5562_set_channel_current(FuriHalI2cBusHandle* handle, LP5562Channel channel, uint8_t value); +void lp5562_set_channel_current( + const FuriHalI2cBusHandle* handle, + LP5562Channel channel, + uint8_t value); /** Set channel PWM value */ -void lp5562_set_channel_value(FuriHalI2cBusHandle* handle, LP5562Channel channel, uint8_t value); +void lp5562_set_channel_value( + const FuriHalI2cBusHandle* handle, + LP5562Channel channel, + uint8_t value); /** Get channel PWM value */ -uint8_t lp5562_get_channel_value(FuriHalI2cBusHandle* handle, LP5562Channel channel); +uint8_t lp5562_get_channel_value(const FuriHalI2cBusHandle* handle, LP5562Channel channel); /** Set channel source */ -void lp5562_set_channel_src(FuriHalI2cBusHandle* handle, LP5562Channel channel, LP5562Engine src); +void lp5562_set_channel_src( + const FuriHalI2cBusHandle* handle, + LP5562Channel channel, + LP5562Engine src); /** Execute program sequence */ void lp5562_execute_program( - FuriHalI2cBusHandle* handle, + const FuriHalI2cBusHandle* handle, LP5562Engine eng, LP5562Channel ch, uint16_t* program); /** Stop program sequence */ -void lp5562_stop_program(FuriHalI2cBusHandle* handle, LP5562Engine eng); +void lp5562_stop_program(const FuriHalI2cBusHandle* handle, LP5562Engine eng); /** Execute ramp program sequence */ void lp5562_execute_ramp( - FuriHalI2cBusHandle* handle, + const FuriHalI2cBusHandle* handle, LP5562Engine eng, LP5562Channel ch, uint8_t val_start, @@ -61,7 +70,7 @@ void lp5562_execute_ramp( /** Start blink program sequence */ void lp5562_execute_blink( - FuriHalI2cBusHandle* handle, + const FuriHalI2cBusHandle* handle, LP5562Engine eng, LP5562Channel ch, uint16_t on_time, diff --git a/targets/f18/api_symbols.csv b/targets/f18/api_symbols.csv index e2572d267..9ada44f75 100644 --- a/targets/f18/api_symbols.csv +++ b/targets/f18/api_symbols.csv @@ -1287,23 +1287,23 @@ Function,+,furi_hal_hid_u2f_get_request,uint32_t,uint8_t* Function,+,furi_hal_hid_u2f_is_connected,_Bool, Function,+,furi_hal_hid_u2f_send_response,void,"uint8_t*, uint8_t" Function,+,furi_hal_hid_u2f_set_callback,void,"HidU2fCallback, void*" -Function,+,furi_hal_i2c_acquire,void,FuriHalI2cBusHandle* +Function,+,furi_hal_i2c_acquire,void,const FuriHalI2cBusHandle* Function,-,furi_hal_i2c_deinit_early,void, Function,-,furi_hal_i2c_init,void, Function,-,furi_hal_i2c_init_early,void, -Function,+,furi_hal_i2c_is_device_ready,_Bool,"FuriHalI2cBusHandle*, uint8_t, uint32_t" -Function,+,furi_hal_i2c_read_mem,_Bool,"FuriHalI2cBusHandle*, uint8_t, uint8_t, uint8_t*, size_t, uint32_t" -Function,+,furi_hal_i2c_read_reg_16,_Bool,"FuriHalI2cBusHandle*, uint8_t, uint8_t, uint16_t*, uint32_t" -Function,+,furi_hal_i2c_read_reg_8,_Bool,"FuriHalI2cBusHandle*, uint8_t, uint8_t, uint8_t*, uint32_t" -Function,+,furi_hal_i2c_release,void,FuriHalI2cBusHandle* -Function,+,furi_hal_i2c_rx,_Bool,"FuriHalI2cBusHandle*, uint8_t, uint8_t*, size_t, uint32_t" -Function,+,furi_hal_i2c_rx_ext,_Bool,"FuriHalI2cBusHandle*, uint16_t, _Bool, uint8_t*, size_t, FuriHalI2cBegin, FuriHalI2cEnd, uint32_t" -Function,+,furi_hal_i2c_trx,_Bool,"FuriHalI2cBusHandle*, uint8_t, const uint8_t*, size_t, uint8_t*, size_t, uint32_t" -Function,+,furi_hal_i2c_tx,_Bool,"FuriHalI2cBusHandle*, uint8_t, const uint8_t*, size_t, uint32_t" -Function,+,furi_hal_i2c_tx_ext,_Bool,"FuriHalI2cBusHandle*, uint16_t, _Bool, const uint8_t*, size_t, FuriHalI2cBegin, FuriHalI2cEnd, uint32_t" -Function,+,furi_hal_i2c_write_mem,_Bool,"FuriHalI2cBusHandle*, uint8_t, uint8_t, const uint8_t*, size_t, uint32_t" -Function,+,furi_hal_i2c_write_reg_16,_Bool,"FuriHalI2cBusHandle*, uint8_t, uint8_t, uint16_t, uint32_t" -Function,+,furi_hal_i2c_write_reg_8,_Bool,"FuriHalI2cBusHandle*, uint8_t, uint8_t, uint8_t, uint32_t" +Function,+,furi_hal_i2c_is_device_ready,_Bool,"const FuriHalI2cBusHandle*, uint8_t, uint32_t" +Function,+,furi_hal_i2c_read_mem,_Bool,"const FuriHalI2cBusHandle*, uint8_t, uint8_t, uint8_t*, size_t, uint32_t" +Function,+,furi_hal_i2c_read_reg_16,_Bool,"const FuriHalI2cBusHandle*, uint8_t, uint8_t, uint16_t*, uint32_t" +Function,+,furi_hal_i2c_read_reg_8,_Bool,"const FuriHalI2cBusHandle*, uint8_t, uint8_t, uint8_t*, uint32_t" +Function,+,furi_hal_i2c_release,void,const FuriHalI2cBusHandle* +Function,+,furi_hal_i2c_rx,_Bool,"const FuriHalI2cBusHandle*, uint8_t, uint8_t*, size_t, uint32_t" +Function,+,furi_hal_i2c_rx_ext,_Bool,"const FuriHalI2cBusHandle*, uint16_t, _Bool, uint8_t*, size_t, FuriHalI2cBegin, FuriHalI2cEnd, uint32_t" +Function,+,furi_hal_i2c_trx,_Bool,"const FuriHalI2cBusHandle*, uint8_t, const uint8_t*, size_t, uint8_t*, size_t, uint32_t" +Function,+,furi_hal_i2c_tx,_Bool,"const FuriHalI2cBusHandle*, uint8_t, const uint8_t*, size_t, uint32_t" +Function,+,furi_hal_i2c_tx_ext,_Bool,"const FuriHalI2cBusHandle*, uint16_t, _Bool, const uint8_t*, size_t, FuriHalI2cBegin, FuriHalI2cEnd, uint32_t" +Function,+,furi_hal_i2c_write_mem,_Bool,"const FuriHalI2cBusHandle*, uint8_t, uint8_t, const uint8_t*, size_t, uint32_t" +Function,+,furi_hal_i2c_write_reg_16,_Bool,"const FuriHalI2cBusHandle*, uint8_t, uint8_t, uint16_t, uint32_t" +Function,+,furi_hal_i2c_write_reg_8,_Bool,"const FuriHalI2cBusHandle*, uint8_t, uint8_t, uint8_t, uint32_t" Function,+,furi_hal_info_get,void,"PropertyValueCallback, char, void*" Function,+,furi_hal_info_get_api_version,void,"uint16_t*, uint16_t*" Function,-,furi_hal_init,void, @@ -2941,8 +2941,8 @@ Variable,+,compress_config_heatshrink_default,const CompressConfigHeatshrink, Variable,+,firmware_api_interface,const ElfApiInterface* const, Variable,+,furi_hal_i2c_bus_external,FuriHalI2cBus, Variable,+,furi_hal_i2c_bus_power,FuriHalI2cBus, -Variable,+,furi_hal_i2c_handle_external,FuriHalI2cBusHandle, -Variable,+,furi_hal_i2c_handle_power,FuriHalI2cBusHandle, +Variable,+,furi_hal_i2c_handle_external,const FuriHalI2cBusHandle, +Variable,+,furi_hal_i2c_handle_power,const FuriHalI2cBusHandle, Variable,+,furi_hal_spi_bus_d,FuriHalSpiBus, Variable,+,furi_hal_spi_bus_handle_display,const FuriHalSpiBusHandle, Variable,+,furi_hal_spi_bus_handle_external,const FuriHalSpiBusHandle, diff --git a/targets/f7/api_symbols.csv b/targets/f7/api_symbols.csv index db7e72459..187440104 100644 --- a/targets/f7/api_symbols.csv +++ b/targets/f7/api_symbols.csv @@ -1398,23 +1398,23 @@ Function,+,furi_hal_hid_u2f_get_request,uint32_t,uint8_t* Function,+,furi_hal_hid_u2f_is_connected,_Bool, Function,+,furi_hal_hid_u2f_send_response,void,"uint8_t*, uint8_t" Function,+,furi_hal_hid_u2f_set_callback,void,"HidU2fCallback, void*" -Function,+,furi_hal_i2c_acquire,void,FuriHalI2cBusHandle* +Function,+,furi_hal_i2c_acquire,void,const FuriHalI2cBusHandle* Function,-,furi_hal_i2c_deinit_early,void, Function,-,furi_hal_i2c_init,void, Function,-,furi_hal_i2c_init_early,void, -Function,+,furi_hal_i2c_is_device_ready,_Bool,"FuriHalI2cBusHandle*, uint8_t, uint32_t" -Function,+,furi_hal_i2c_read_mem,_Bool,"FuriHalI2cBusHandle*, uint8_t, uint8_t, uint8_t*, size_t, uint32_t" -Function,+,furi_hal_i2c_read_reg_16,_Bool,"FuriHalI2cBusHandle*, uint8_t, uint8_t, uint16_t*, uint32_t" -Function,+,furi_hal_i2c_read_reg_8,_Bool,"FuriHalI2cBusHandle*, uint8_t, uint8_t, uint8_t*, uint32_t" -Function,+,furi_hal_i2c_release,void,FuriHalI2cBusHandle* -Function,+,furi_hal_i2c_rx,_Bool,"FuriHalI2cBusHandle*, uint8_t, uint8_t*, size_t, uint32_t" -Function,+,furi_hal_i2c_rx_ext,_Bool,"FuriHalI2cBusHandle*, uint16_t, _Bool, uint8_t*, size_t, FuriHalI2cBegin, FuriHalI2cEnd, uint32_t" -Function,+,furi_hal_i2c_trx,_Bool,"FuriHalI2cBusHandle*, uint8_t, const uint8_t*, size_t, uint8_t*, size_t, uint32_t" -Function,+,furi_hal_i2c_tx,_Bool,"FuriHalI2cBusHandle*, uint8_t, const uint8_t*, size_t, uint32_t" -Function,+,furi_hal_i2c_tx_ext,_Bool,"FuriHalI2cBusHandle*, uint16_t, _Bool, const uint8_t*, size_t, FuriHalI2cBegin, FuriHalI2cEnd, uint32_t" -Function,+,furi_hal_i2c_write_mem,_Bool,"FuriHalI2cBusHandle*, uint8_t, uint8_t, const uint8_t*, size_t, uint32_t" -Function,+,furi_hal_i2c_write_reg_16,_Bool,"FuriHalI2cBusHandle*, uint8_t, uint8_t, uint16_t, uint32_t" -Function,+,furi_hal_i2c_write_reg_8,_Bool,"FuriHalI2cBusHandle*, uint8_t, uint8_t, uint8_t, uint32_t" +Function,+,furi_hal_i2c_is_device_ready,_Bool,"const FuriHalI2cBusHandle*, uint8_t, uint32_t" +Function,+,furi_hal_i2c_read_mem,_Bool,"const FuriHalI2cBusHandle*, uint8_t, uint8_t, uint8_t*, size_t, uint32_t" +Function,+,furi_hal_i2c_read_reg_16,_Bool,"const FuriHalI2cBusHandle*, uint8_t, uint8_t, uint16_t*, uint32_t" +Function,+,furi_hal_i2c_read_reg_8,_Bool,"const FuriHalI2cBusHandle*, uint8_t, uint8_t, uint8_t*, uint32_t" +Function,+,furi_hal_i2c_release,void,const FuriHalI2cBusHandle* +Function,+,furi_hal_i2c_rx,_Bool,"const FuriHalI2cBusHandle*, uint8_t, uint8_t*, size_t, uint32_t" +Function,+,furi_hal_i2c_rx_ext,_Bool,"const FuriHalI2cBusHandle*, uint16_t, _Bool, uint8_t*, size_t, FuriHalI2cBegin, FuriHalI2cEnd, uint32_t" +Function,+,furi_hal_i2c_trx,_Bool,"const FuriHalI2cBusHandle*, uint8_t, const uint8_t*, size_t, uint8_t*, size_t, uint32_t" +Function,+,furi_hal_i2c_tx,_Bool,"const FuriHalI2cBusHandle*, uint8_t, const uint8_t*, size_t, uint32_t" +Function,+,furi_hal_i2c_tx_ext,_Bool,"const FuriHalI2cBusHandle*, uint16_t, _Bool, const uint8_t*, size_t, FuriHalI2cBegin, FuriHalI2cEnd, uint32_t" +Function,+,furi_hal_i2c_write_mem,_Bool,"const FuriHalI2cBusHandle*, uint8_t, uint8_t, const uint8_t*, size_t, uint32_t" +Function,+,furi_hal_i2c_write_reg_16,_Bool,"const FuriHalI2cBusHandle*, uint8_t, uint8_t, uint16_t, uint32_t" +Function,+,furi_hal_i2c_write_reg_8,_Bool,"const FuriHalI2cBusHandle*, uint8_t, uint8_t, uint8_t, uint32_t" Function,+,furi_hal_ibutton_emulate_set_next,void,uint32_t Function,+,furi_hal_ibutton_emulate_start,void,"uint32_t, FuriHalIbuttonEmulateCallback, void*" Function,+,furi_hal_ibutton_emulate_stop,void, @@ -3792,8 +3792,10 @@ Variable,+,compress_config_heatshrink_default,const CompressConfigHeatshrink, Variable,+,firmware_api_interface,const ElfApiInterface* const, Variable,+,furi_hal_i2c_bus_external,FuriHalI2cBus, Variable,+,furi_hal_i2c_bus_power,FuriHalI2cBus, -Variable,+,furi_hal_i2c_handle_external,FuriHalI2cBusHandle, -Variable,+,furi_hal_i2c_handle_power,FuriHalI2cBusHandle, +Variable,+,furi_hal_i2c_handle_external,const FuriHalI2cBusHandle, +Variable,+,furi_hal_i2c_handle_external,const FuriHalI2cBusHandle, +Variable,+,furi_hal_i2c_handle_power,const FuriHalI2cBusHandle, +Variable,+,furi_hal_i2c_handle_power,const FuriHalI2cBusHandle, Variable,+,furi_hal_spi_bus_d,FuriHalSpiBus, Variable,+,furi_hal_spi_bus_handle_display,const FuriHalSpiBusHandle, Variable,+,furi_hal_spi_bus_handle_external,const FuriHalSpiBusHandle, diff --git a/targets/f7/furi_hal/furi_hal_i2c.c b/targets/f7/furi_hal/furi_hal_i2c.c index 71e1a5814..7eb9b4928 100644 --- a/targets/f7/furi_hal/furi_hal_i2c.c +++ b/targets/f7/furi_hal/furi_hal_i2c.c @@ -22,7 +22,7 @@ void furi_hal_i2c_init(void) { FURI_LOG_I(TAG, "Init OK"); } -void furi_hal_i2c_acquire(FuriHalI2cBusHandle* handle) { +void furi_hal_i2c_acquire(const FuriHalI2cBusHandle* handle) { furi_hal_power_insomnia_enter(); // Lock bus access handle->bus->callback(handle->bus, FuriHalI2cBusEventLock); @@ -36,7 +36,7 @@ void furi_hal_i2c_acquire(FuriHalI2cBusHandle* handle) { handle->callback(handle, FuriHalI2cBusHandleEventActivate); } -void furi_hal_i2c_release(FuriHalI2cBusHandle* handle) { +void furi_hal_i2c_release(const FuriHalI2cBusHandle* handle) { // Ensure that current handle is our handle furi_check(handle->bus->current_handle == handle); // Deactivate handle @@ -196,7 +196,7 @@ static bool furi_hal_i2c_transaction( } bool furi_hal_i2c_rx_ext( - FuriHalI2cBusHandle* handle, + const FuriHalI2cBusHandle* handle, uint16_t address, bool ten_bit, uint8_t* data, @@ -213,7 +213,7 @@ bool furi_hal_i2c_rx_ext( } bool furi_hal_i2c_tx_ext( - FuriHalI2cBusHandle* handle, + const FuriHalI2cBusHandle* handle, uint16_t address, bool ten_bit, const uint8_t* data, @@ -230,7 +230,7 @@ bool furi_hal_i2c_tx_ext( } bool furi_hal_i2c_tx( - FuriHalI2cBusHandle* handle, + const FuriHalI2cBusHandle* handle, uint8_t address, const uint8_t* data, size_t size, @@ -242,7 +242,7 @@ bool furi_hal_i2c_tx( } bool furi_hal_i2c_rx( - FuriHalI2cBusHandle* handle, + const FuriHalI2cBusHandle* handle, uint8_t address, uint8_t* data, size_t size, @@ -254,7 +254,7 @@ bool furi_hal_i2c_rx( } bool furi_hal_i2c_trx( - FuriHalI2cBusHandle* handle, + const FuriHalI2cBusHandle* handle, uint8_t address, const uint8_t* tx_data, size_t tx_size, @@ -281,7 +281,10 @@ bool furi_hal_i2c_trx( timeout); } -bool furi_hal_i2c_is_device_ready(FuriHalI2cBusHandle* handle, uint8_t i2c_addr, uint32_t timeout) { +bool furi_hal_i2c_is_device_ready( + const FuriHalI2cBusHandle* handle, + uint8_t i2c_addr, + uint32_t timeout) { furi_check(handle); furi_check(handle->bus->current_handle == handle); furi_check(timeout > 0); @@ -314,7 +317,7 @@ bool furi_hal_i2c_is_device_ready(FuriHalI2cBusHandle* handle, uint8_t i2c_addr, } bool furi_hal_i2c_read_reg_8( - FuriHalI2cBusHandle* handle, + const FuriHalI2cBusHandle* handle, uint8_t i2c_addr, uint8_t reg_addr, uint8_t* data, @@ -325,7 +328,7 @@ bool furi_hal_i2c_read_reg_8( } bool furi_hal_i2c_read_reg_16( - FuriHalI2cBusHandle* handle, + const FuriHalI2cBusHandle* handle, uint8_t i2c_addr, uint8_t reg_addr, uint16_t* data, @@ -340,7 +343,7 @@ bool furi_hal_i2c_read_reg_16( } bool furi_hal_i2c_read_mem( - FuriHalI2cBusHandle* handle, + const FuriHalI2cBusHandle* handle, uint8_t i2c_addr, uint8_t mem_addr, uint8_t* data, @@ -352,7 +355,7 @@ bool furi_hal_i2c_read_mem( } bool furi_hal_i2c_write_reg_8( - FuriHalI2cBusHandle* handle, + const FuriHalI2cBusHandle* handle, uint8_t i2c_addr, uint8_t reg_addr, uint8_t data, @@ -368,7 +371,7 @@ bool furi_hal_i2c_write_reg_8( } bool furi_hal_i2c_write_reg_16( - FuriHalI2cBusHandle* handle, + const FuriHalI2cBusHandle* handle, uint8_t i2c_addr, uint8_t reg_addr, uint16_t data, @@ -385,7 +388,7 @@ bool furi_hal_i2c_write_reg_16( } bool furi_hal_i2c_write_mem( - FuriHalI2cBusHandle* handle, + const FuriHalI2cBusHandle* handle, uint8_t i2c_addr, uint8_t mem_addr, const uint8_t* data, diff --git a/targets/f7/furi_hal/furi_hal_i2c_config.c b/targets/f7/furi_hal/furi_hal_i2c_config.c index f9d88abb3..b10c53d32 100644 --- a/targets/f7/furi_hal/furi_hal_i2c_config.c +++ b/targets/f7/furi_hal/furi_hal_i2c_config.c @@ -74,7 +74,7 @@ FuriHalI2cBus furi_hal_i2c_bus_external = { }; void furi_hal_i2c_bus_handle_power_event( - FuriHalI2cBusHandle* handle, + const FuriHalI2cBusHandle* handle, FuriHalI2cBusHandleEvent event) { if(event == FuriHalI2cBusHandleEventActivate) { furi_hal_gpio_init_ex( @@ -120,13 +120,13 @@ void furi_hal_i2c_bus_handle_power_event( } } -FuriHalI2cBusHandle furi_hal_i2c_handle_power = { +const FuriHalI2cBusHandle furi_hal_i2c_handle_power = { .bus = &furi_hal_i2c_bus_power, .callback = furi_hal_i2c_bus_handle_power_event, }; void furi_hal_i2c_bus_handle_external_event( - FuriHalI2cBusHandle* handle, + const FuriHalI2cBusHandle* handle, FuriHalI2cBusHandleEvent event) { if(event == FuriHalI2cBusHandleEventActivate) { furi_hal_gpio_init_ex( @@ -160,7 +160,7 @@ void furi_hal_i2c_bus_handle_external_event( } } -FuriHalI2cBusHandle furi_hal_i2c_handle_external = { +const FuriHalI2cBusHandle furi_hal_i2c_handle_external = { .bus = &furi_hal_i2c_bus_external, .callback = furi_hal_i2c_bus_handle_external_event, }; diff --git a/targets/f7/furi_hal/furi_hal_i2c_config.h b/targets/f7/furi_hal/furi_hal_i2c_config.h index a8fb91835..28bd09a0a 100644 --- a/targets/f7/furi_hal/furi_hal_i2c_config.h +++ b/targets/f7/furi_hal/furi_hal_i2c_config.h @@ -17,14 +17,14 @@ extern FuriHalI2cBus furi_hal_i2c_bus_external; * Pins: PA9(SCL) / PA10(SDA), float on release * Params: 400khz */ -extern FuriHalI2cBusHandle furi_hal_i2c_handle_power; +extern const FuriHalI2cBusHandle furi_hal_i2c_handle_power; /** Handle for external i2c bus * Bus: furi_hal_i2c_bus_external * Pins: PC0(SCL) / PC1(SDA), float on release * Params: 100khz */ -extern FuriHalI2cBusHandle furi_hal_i2c_handle_external; +extern const FuriHalI2cBusHandle furi_hal_i2c_handle_external; #ifdef __cplusplus } diff --git a/targets/f7/furi_hal/furi_hal_i2c_types.h b/targets/f7/furi_hal/furi_hal_i2c_types.h index 13f361054..0a35137b0 100644 --- a/targets/f7/furi_hal/furi_hal_i2c_types.h +++ b/targets/f7/furi_hal/furi_hal_i2c_types.h @@ -25,7 +25,7 @@ typedef void (*FuriHalI2cBusEventCallback)(FuriHalI2cBus* bus, FuriHalI2cBusEven /** FuriHal i2c bus */ struct FuriHalI2cBus { I2C_TypeDef* i2c; - FuriHalI2cBusHandle* current_handle; + const FuriHalI2cBusHandle* current_handle; FuriHalI2cBusEventCallback callback; }; @@ -37,7 +37,7 @@ typedef enum { /** FuriHal i2c handle event callback */ typedef void (*FuriHalI2cBusHandleEventCallback)( - FuriHalI2cBusHandle* handle, + const FuriHalI2cBusHandle* handle, FuriHalI2cBusHandleEvent event); /** FuriHal i2c handle */ diff --git a/targets/furi_hal_include/furi_hal_i2c.h b/targets/furi_hal_include/furi_hal_i2c.h index 7d69cd74d..fe9f0949c 100644 --- a/targets/furi_hal_include/furi_hal_i2c.h +++ b/targets/furi_hal_include/furi_hal_i2c.h @@ -55,14 +55,14 @@ void furi_hal_i2c_init(void); * * @param handle Pointer to FuriHalI2cBusHandle instance */ -void furi_hal_i2c_acquire(FuriHalI2cBusHandle* handle); +void furi_hal_i2c_acquire(const FuriHalI2cBusHandle* handle); /** Release I2C bus handle * * @param handle Pointer to FuriHalI2cBusHandle instance acquired in * `furi_hal_i2c_acquire` */ -void furi_hal_i2c_release(FuriHalI2cBusHandle* handle); +void furi_hal_i2c_release(const FuriHalI2cBusHandle* handle); /** Perform I2C TX transfer * @@ -75,7 +75,7 @@ void furi_hal_i2c_release(FuriHalI2cBusHandle* handle); * @return true on successful transfer, false otherwise */ bool furi_hal_i2c_tx( - FuriHalI2cBusHandle* handle, + const FuriHalI2cBusHandle* handle, uint8_t address, const uint8_t* data, size_t size, @@ -96,7 +96,7 @@ bool furi_hal_i2c_tx( * @return true on successful transfer, false otherwise */ bool furi_hal_i2c_tx_ext( - FuriHalI2cBusHandle* handle, + const FuriHalI2cBusHandle* handle, uint16_t address, bool ten_bit, const uint8_t* data, @@ -116,7 +116,7 @@ bool furi_hal_i2c_tx_ext( * @return true on successful transfer, false otherwise */ bool furi_hal_i2c_rx( - FuriHalI2cBusHandle* handle, + const FuriHalI2cBusHandle* handle, uint8_t address, uint8_t* data, size_t size, @@ -136,7 +136,7 @@ bool furi_hal_i2c_rx( * @return true on successful transfer, false otherwise */ bool furi_hal_i2c_rx_ext( - FuriHalI2cBusHandle* handle, + const FuriHalI2cBusHandle* handle, uint16_t address, bool ten_bit, uint8_t* data, @@ -158,7 +158,7 @@ bool furi_hal_i2c_rx_ext( * @return true on successful transfer, false otherwise */ bool furi_hal_i2c_trx( - FuriHalI2cBusHandle* handle, + const FuriHalI2cBusHandle* handle, uint8_t address, const uint8_t* tx_data, size_t tx_size, @@ -174,7 +174,10 @@ bool furi_hal_i2c_trx( * * @return true if device present and is ready, false otherwise */ -bool furi_hal_i2c_is_device_ready(FuriHalI2cBusHandle* handle, uint8_t i2c_addr, uint32_t timeout); +bool furi_hal_i2c_is_device_ready( + const FuriHalI2cBusHandle* handle, + uint8_t i2c_addr, + uint32_t timeout); /** Perform I2C device register read (8-bit) * @@ -187,7 +190,7 @@ bool furi_hal_i2c_is_device_ready(FuriHalI2cBusHandle* handle, uint8_t i2c_addr, * @return true on successful transfer, false otherwise */ bool furi_hal_i2c_read_reg_8( - FuriHalI2cBusHandle* handle, + const FuriHalI2cBusHandle* handle, uint8_t i2c_addr, uint8_t reg_addr, uint8_t* data, @@ -204,7 +207,7 @@ bool furi_hal_i2c_read_reg_8( * @return true on successful transfer, false otherwise */ bool furi_hal_i2c_read_reg_16( - FuriHalI2cBusHandle* handle, + const FuriHalI2cBusHandle* handle, uint8_t i2c_addr, uint8_t reg_addr, uint16_t* data, @@ -222,7 +225,7 @@ bool furi_hal_i2c_read_reg_16( * @return true on successful transfer, false otherwise */ bool furi_hal_i2c_read_mem( - FuriHalI2cBusHandle* handle, + const FuriHalI2cBusHandle* handle, uint8_t i2c_addr, uint8_t mem_addr, uint8_t* data, @@ -240,7 +243,7 @@ bool furi_hal_i2c_read_mem( * @return true on successful transfer, false otherwise */ bool furi_hal_i2c_write_reg_8( - FuriHalI2cBusHandle* handle, + const FuriHalI2cBusHandle* handle, uint8_t i2c_addr, uint8_t reg_addr, uint8_t data, @@ -257,7 +260,7 @@ bool furi_hal_i2c_write_reg_8( * @return true on successful transfer, false otherwise */ bool furi_hal_i2c_write_reg_16( - FuriHalI2cBusHandle* handle, + const FuriHalI2cBusHandle* handle, uint8_t i2c_addr, uint8_t reg_addr, uint16_t data, @@ -275,7 +278,7 @@ bool furi_hal_i2c_write_reg_16( * @return true on successful transfer, false otherwise */ bool furi_hal_i2c_write_mem( - FuriHalI2cBusHandle* handle, + const FuriHalI2cBusHandle* handle, uint8_t i2c_addr, uint8_t mem_addr, const uint8_t* data, From 15a5e652e06f0aa9a03dc898d670753342350d4d Mon Sep 17 00:00:00 2001 From: hedger Date: Sat, 22 Feb 2025 20:40:52 +0000 Subject: [PATCH 083/268] usb: restored previous api --- applications/debug/ccid_test/ccid_test_app.c | 2 +- applications/debug/usb_mouse/usb_mouse.c | 2 +- applications/main/bad_usb/bad_usb_app_i.h | 2 +- applications/main/u2f/u2f_hid.c | 2 +- applications/services/cli/cli_vcp.c | 2 +- applications/system/hid_app/hid.c | 2 +- applications/system/js_app/modules/js_badusb.c | 2 +- targets/f18/api_symbols.csv | 8 ++++---- targets/f7/api_symbols.csv | 10 ++++------ targets/f7/fatfs/user_diskio.h | 2 +- targets/f7/furi_hal/furi_hal_usb.c | 16 ++++++++-------- targets/f7/furi_hal/furi_hal_usb_ccid.c | 4 ++-- targets/f7/furi_hal/furi_hal_usb_cdc.c | 9 ++++----- targets/f7/furi_hal/furi_hal_usb_hid.c | 4 ++-- targets/f7/furi_hal/furi_hal_usb_u2f.c | 6 +++--- targets/furi_hal_include/furi_hal_usb.h | 8 ++++---- 16 files changed, 39 insertions(+), 42 deletions(-) diff --git a/applications/debug/ccid_test/ccid_test_app.c b/applications/debug/ccid_test/ccid_test_app.c index a02fd1596..4158c1a60 100644 --- a/applications/debug/ccid_test/ccid_test_app.c +++ b/applications/debug/ccid_test/ccid_test_app.c @@ -117,7 +117,7 @@ int32_t ccid_test_app(void* p) { //setup view CcidTestApp* app = ccid_test_app_alloc(); - const FuriHalUsbInterface* usb_mode_prev = furi_hal_usb_get_config(); + FuriHalUsbInterface* usb_mode_prev = furi_hal_usb_get_config(); furi_hal_usb_unlock(); furi_check(furi_hal_usb_set_config(&usb_ccid, &app->ccid_cfg) == true); diff --git a/applications/debug/usb_mouse/usb_mouse.c b/applications/debug/usb_mouse/usb_mouse.c index 7d5ee27e8..e322a58ae 100644 --- a/applications/debug/usb_mouse/usb_mouse.c +++ b/applications/debug/usb_mouse/usb_mouse.c @@ -42,7 +42,7 @@ int32_t usb_mouse_app(void* p) { FuriMessageQueue* event_queue = furi_message_queue_alloc(8, sizeof(UsbMouseEvent)); ViewPort* view_port = view_port_alloc(); - const FuriHalUsbInterface* usb_mode_prev = furi_hal_usb_get_config(); + FuriHalUsbInterface* usb_mode_prev = furi_hal_usb_get_config(); furi_hal_usb_unlock(); furi_check(furi_hal_usb_set_config(&usb_hid, NULL) == true); diff --git a/applications/main/bad_usb/bad_usb_app_i.h b/applications/main/bad_usb/bad_usb_app_i.h index 93c24ea8c..b34bd5de6 100644 --- a/applications/main/bad_usb/bad_usb_app_i.h +++ b/applications/main/bad_usb/bad_usb_app_i.h @@ -44,7 +44,7 @@ struct BadUsbApp { BadUsbScript* bad_usb_script; BadUsbHidInterface interface; - const FuriHalUsbInterface* usb_if_prev; + FuriHalUsbInterface* usb_if_prev; }; typedef enum { diff --git a/applications/main/u2f/u2f_hid.c b/applications/main/u2f/u2f_hid.c index a1b614b19..76d3d7cec 100644 --- a/applications/main/u2f/u2f_hid.c +++ b/applications/main/u2f/u2f_hid.c @@ -189,7 +189,7 @@ static int32_t u2f_hid_worker(void* context) { FURI_LOG_D(WORKER_TAG, "Init"); - const FuriHalUsbInterface* usb_mode_prev = furi_hal_usb_get_config(); + FuriHalUsbInterface* usb_mode_prev = furi_hal_usb_get_config(); furi_check(furi_hal_usb_set_config(&usb_hid_u2f, NULL) == true); u2f_hid->lock_timer = diff --git a/applications/services/cli/cli_vcp.c b/applications/services/cli/cli_vcp.c index bc4ea592a..39802bd79 100644 --- a/applications/services/cli/cli_vcp.c +++ b/applications/services/cli/cli_vcp.c @@ -40,7 +40,7 @@ typedef struct { volatile bool connected; volatile bool running; - const FuriHalUsbInterface* usb_if_prev; + FuriHalUsbInterface* usb_if_prev; uint8_t data_buffer[USB_CDC_PKT_LEN]; } CliVcp; diff --git a/applications/system/hid_app/hid.c b/applications/system/hid_app/hid.c index 9f3a246ef..15c49e3b0 100644 --- a/applications/system/hid_app/hid.c +++ b/applications/system/hid_app/hid.c @@ -174,7 +174,7 @@ int32_t hid_usb_app(void* p) { FURI_LOG_D("HID", "Starting as USB app"); - const FuriHalUsbInterface* usb_mode_prev = furi_hal_usb_get_config(); + FuriHalUsbInterface* usb_mode_prev = furi_hal_usb_get_config(); furi_hal_usb_unlock(); furi_check(furi_hal_usb_set_config(&usb_hid, NULL) == true); diff --git a/applications/system/js_app/modules/js_badusb.c b/applications/system/js_app/modules/js_badusb.c index 880bbdc48..27f38cbda 100644 --- a/applications/system/js_app/modules/js_badusb.c +++ b/applications/system/js_app/modules/js_badusb.c @@ -7,7 +7,7 @@ typedef struct { FuriHalUsbHidConfig* hid_cfg; uint16_t layout[128]; - const FuriHalUsbInterface* usb_if_prev; + FuriHalUsbInterface* usb_if_prev; uint8_t key_hold_cnt; } JsBadusbInst; diff --git a/targets/f18/api_symbols.csv b/targets/f18/api_symbols.csv index 9ada44f75..6dbf4a035 100644 --- a/targets/f18/api_symbols.csv +++ b/targets/f18/api_symbols.csv @@ -1489,13 +1489,13 @@ Function,+,furi_hal_usb_ccid_remove_smartcard,void, Function,+,furi_hal_usb_ccid_set_callbacks,void,"CcidCallbacks*, void*" Function,+,furi_hal_usb_disable,void, Function,+,furi_hal_usb_enable,void, -Function,+,furi_hal_usb_get_config,const FuriHalUsbInterface*, +Function,+,furi_hal_usb_get_config,FuriHalUsbInterface*, Function,-,furi_hal_usb_init,void, Function,+,furi_hal_usb_is_locked,_Bool, Function,+,furi_hal_usb_lock,void, Function,+,furi_hal_usb_reinit,void, -Function,+,furi_hal_usb_set_config,_Bool,"const FuriHalUsbInterface*, void*" -Function,-,furi_hal_usb_set_state_callback,void,"FuriHalUsbStateCallback, void*" +Function,+,furi_hal_usb_set_config,_Bool,"FuriHalUsbInterface*, void*" +Function,+,furi_hal_usb_set_state_callback,void,"FuriHalUsbStateCallback, void*" Function,+,furi_hal_usb_unlock,void, Function,+,furi_hal_version_do_i_belong_here,_Bool, Function,+,furi_hal_version_get_ble_local_device_name_ptr,const char*, @@ -3208,5 +3208,5 @@ Variable,+,usb_ccid,FuriHalUsbInterface, Variable,+,usb_cdc_dual,FuriHalUsbInterface, Variable,+,usb_cdc_single,FuriHalUsbInterface, Variable,+,usb_hid,FuriHalUsbInterface, -Variable,+,usb_hid_u2f,const FuriHalUsbInterface, +Variable,+,usb_hid_u2f,FuriHalUsbInterface, Variable,+,usbd_devfs,const usbd_driver, diff --git a/targets/f7/api_symbols.csv b/targets/f7/api_symbols.csv index 187440104..01a83d853 100644 --- a/targets/f7/api_symbols.csv +++ b/targets/f7/api_symbols.csv @@ -1709,13 +1709,13 @@ Function,+,furi_hal_usb_ccid_remove_smartcard,void, Function,+,furi_hal_usb_ccid_set_callbacks,void,"CcidCallbacks*, void*" Function,+,furi_hal_usb_disable,void, Function,+,furi_hal_usb_enable,void, -Function,+,furi_hal_usb_get_config,const FuriHalUsbInterface*, +Function,+,furi_hal_usb_get_config,FuriHalUsbInterface*, Function,-,furi_hal_usb_init,void, Function,+,furi_hal_usb_is_locked,_Bool, Function,+,furi_hal_usb_lock,void, Function,+,furi_hal_usb_reinit,void, -Function,+,furi_hal_usb_set_config,_Bool,"const FuriHalUsbInterface*, void*" -Function,-,furi_hal_usb_set_state_callback,void,"FuriHalUsbStateCallback, void*" +Function,+,furi_hal_usb_set_config,_Bool,"FuriHalUsbInterface*, void*" +Function,+,furi_hal_usb_set_state_callback,void,"FuriHalUsbStateCallback, void*" Function,+,furi_hal_usb_unlock,void, Function,+,furi_hal_version_do_i_belong_here,_Bool, Function,+,furi_hal_version_get_ble_local_device_name_ptr,const char*, @@ -3793,8 +3793,6 @@ Variable,+,firmware_api_interface,const ElfApiInterface* const, Variable,+,furi_hal_i2c_bus_external,FuriHalI2cBus, Variable,+,furi_hal_i2c_bus_power,FuriHalI2cBus, Variable,+,furi_hal_i2c_handle_external,const FuriHalI2cBusHandle, -Variable,+,furi_hal_i2c_handle_external,const FuriHalI2cBusHandle, -Variable,+,furi_hal_i2c_handle_power,const FuriHalI2cBusHandle, Variable,+,furi_hal_i2c_handle_power,const FuriHalI2cBusHandle, Variable,+,furi_hal_spi_bus_d,FuriHalSpiBus, Variable,+,furi_hal_spi_bus_handle_display,const FuriHalSpiBusHandle, @@ -4081,5 +4079,5 @@ Variable,+,usb_ccid,FuriHalUsbInterface, Variable,+,usb_cdc_dual,FuriHalUsbInterface, Variable,+,usb_cdc_single,FuriHalUsbInterface, Variable,+,usb_hid,FuriHalUsbInterface, -Variable,+,usb_hid_u2f,const FuriHalUsbInterface, +Variable,+,usb_hid_u2f,FuriHalUsbInterface, Variable,+,usbd_devfs,const usbd_driver, diff --git a/targets/f7/fatfs/user_diskio.h b/targets/f7/fatfs/user_diskio.h index 2505de704..a0fcb9d57 100644 --- a/targets/f7/fatfs/user_diskio.h +++ b/targets/f7/fatfs/user_diskio.h @@ -10,4 +10,4 @@ extern const Diskio_drvTypeDef sd_fatfs_driver; #ifdef __cplusplus } -#endif +#endif \ No newline at end of file diff --git a/targets/f7/furi_hal/furi_hal_usb.c b/targets/f7/furi_hal/furi_hal_usb.c index 03e92cc16..22d1523b6 100644 --- a/targets/f7/furi_hal/furi_hal_usb.c +++ b/targets/f7/furi_hal/furi_hal_usb.c @@ -32,7 +32,7 @@ typedef struct { } UsbApiEventDataStateCallback; typedef struct { - const FuriHalUsbInterface* interface; + FuriHalUsbInterface* interface; void* context; } UsbApiEventDataInterface; @@ -43,7 +43,7 @@ typedef union { typedef union { bool bool_value; - const void* void_value; + void* void_value; } UsbApiEventReturnData; typedef struct { @@ -60,7 +60,7 @@ typedef struct { bool connected; bool mode_lock; bool request_pending; - const FuriHalUsbInterface* interface; + FuriHalUsbInterface* interface; void* interface_context; FuriHalUsbStateCallback callback; void* callback_context; @@ -132,7 +132,7 @@ static void furi_hal_usb_send_message(UsbApiEventMessage* message) { api_lock_wait_unlock_and_free(message->lock); } -bool furi_hal_usb_set_config(const FuriHalUsbInterface* new_if, void* ctx) { +bool furi_hal_usb_set_config(FuriHalUsbInterface* new_if, void* ctx) { UsbApiEventReturnData return_data = { .bool_value = false, }; @@ -152,7 +152,7 @@ bool furi_hal_usb_set_config(const FuriHalUsbInterface* new_if, void* ctx) { return return_data.bool_value; } -const FuriHalUsbInterface* furi_hal_usb_get_config(void) { +FuriHalUsbInterface* furi_hal_usb_get_config(void) { UsbApiEventReturnData return_data = { .void_value = NULL, }; @@ -326,7 +326,7 @@ static void wkup_evt(usbd_device* dev, uint8_t event, uint8_t ep) { } } -static void usb_process_mode_start(const FuriHalUsbInterface* interface, void* context) { +static void usb_process_mode_start(FuriHalUsbInterface* interface, void* context) { if(usb.interface != NULL) { usb.interface->deinit(&udev); } @@ -344,7 +344,7 @@ static void usb_process_mode_start(const FuriHalUsbInterface* interface, void* c } } -static void usb_process_mode_change(const FuriHalUsbInterface* interface, void* context) { +static void usb_process_mode_change(FuriHalUsbInterface* interface, void* context) { if((interface != usb.interface) || (context != usb.interface_context)) { if(usb.enabled) { // Disable current interface @@ -374,7 +374,7 @@ static void usb_process_mode_reinit(void) { usb_process_mode_start(usb.interface, usb.interface_context); } -static bool usb_process_set_config(const FuriHalUsbInterface* interface, void* context) { +static bool usb_process_set_config(FuriHalUsbInterface* interface, void* context) { if(usb.mode_lock) { return false; } else { diff --git a/targets/f7/furi_hal/furi_hal_usb_ccid.c b/targets/f7/furi_hal/furi_hal_usb_ccid.c index 5725c70cf..a2c1a0583 100644 --- a/targets/f7/furi_hal/furi_hal_usb_ccid.c +++ b/targets/f7/furi_hal/furi_hal_usb_ccid.c @@ -170,7 +170,7 @@ static const struct CcidConfigDescriptor ccid_cfg_desc = { }, }; -static void ccid_init(usbd_device* dev, const FuriHalUsbInterface* intf, void* ctx); +static void ccid_init(usbd_device* dev, FuriHalUsbInterface* intf, void* ctx); static void ccid_deinit(usbd_device* dev); static void ccid_on_wakeup(usbd_device* dev); static void ccid_on_suspend(usbd_device* dev); @@ -223,7 +223,7 @@ static void* ccid_set_string_descr(char* str) { return dev_str_desc; } -static void ccid_init(usbd_device* dev, const FuriHalUsbInterface* intf, void* ctx) { +static void ccid_init(usbd_device* dev, FuriHalUsbInterface* intf, void* ctx) { UNUSED(intf); FuriHalUsbCcidConfig* cfg = (FuriHalUsbCcidConfig*)ctx; diff --git a/targets/f7/furi_hal/furi_hal_usb_cdc.c b/targets/f7/furi_hal/furi_hal_usb_cdc.c index 62c43ea64..f734ef1a4 100644 --- a/targets/f7/furi_hal/furi_hal_usb_cdc.c +++ b/targets/f7/furi_hal/furi_hal_usb_cdc.c @@ -385,7 +385,7 @@ static const struct CdcConfigDescriptorDual static struct usb_cdc_line_coding cdc_config[IF_NUM_MAX] = {}; static uint8_t cdc_ctrl_line_state[IF_NUM_MAX]; -static void cdc_init(usbd_device* dev, const FuriHalUsbInterface* intf, void* ctx); +static void cdc_init(usbd_device* dev, FuriHalUsbInterface* intf, void* ctx); static void cdc_deinit(usbd_device* dev); static void cdc_on_wakeup(usbd_device* dev); static void cdc_on_suspend(usbd_device* dev); @@ -429,11 +429,11 @@ FuriHalUsbInterface usb_cdc_dual = { .cfg_descr = (void*)&cdc_cfg_desc_dual, }; -static void cdc_init(usbd_device* dev, const FuriHalUsbInterface* intf, void* ctx) { +static void cdc_init(usbd_device* dev, FuriHalUsbInterface* intf, void* ctx) { UNUSED(ctx); usb_dev = dev; - cdc_if_cur = malloc(sizeof(FuriHalUsbInterface)); - memcpy((void*)cdc_if_cur, intf, sizeof(FuriHalUsbInterface)); + + cdc_if_cur = intf; char* name = (char*)furi_hal_version_get_device_name_ptr(); uint8_t len = (name == NULL) ? (0) : (strlen(name)); @@ -470,7 +470,6 @@ static void cdc_deinit(usbd_device* dev) { free(cdc_if_cur->str_prod_descr); free(cdc_if_cur->str_serial_descr); - free((void*)cdc_if_cur); cdc_if_cur = NULL; } diff --git a/targets/f7/furi_hal/furi_hal_usb_hid.c b/targets/f7/furi_hal/furi_hal_usb_hid.c index cad8f040b..c83261226 100644 --- a/targets/f7/furi_hal/furi_hal_usb_hid.c +++ b/targets/f7/furi_hal/furi_hal_usb_hid.c @@ -226,7 +226,7 @@ static struct HidReport { struct HidReportConsumer consumer; } FURI_PACKED hid_report; -static void hid_init(usbd_device* dev, const FuriHalUsbInterface* intf, void* ctx); +static void hid_init(usbd_device* dev, FuriHalUsbInterface* intf, void* ctx); static void hid_deinit(usbd_device* dev); static void hid_on_wakeup(usbd_device* dev); static void hid_on_suspend(usbd_device* dev); @@ -374,7 +374,7 @@ static void* hid_set_string_descr(char* str) { return dev_str_desc; } -static void hid_init(usbd_device* dev, const FuriHalUsbInterface* intf, void* ctx) { +static void hid_init(usbd_device* dev, FuriHalUsbInterface* intf, void* ctx) { UNUSED(intf); FuriHalUsbHidConfig* cfg = (FuriHalUsbHidConfig*)ctx; if(hid_semaphore == NULL) hid_semaphore = furi_semaphore_alloc(1, 1); diff --git a/targets/f7/furi_hal/furi_hal_usb_u2f.c b/targets/f7/furi_hal/furi_hal_usb_u2f.c index 2c0ad7694..05f498376 100644 --- a/targets/f7/furi_hal/furi_hal_usb_u2f.c +++ b/targets/f7/furi_hal/furi_hal_usb_u2f.c @@ -137,7 +137,7 @@ static const struct HidConfigDescriptor hid_u2f_cfg_desc = { }, }; -static void hid_u2f_init(usbd_device* dev, const FuriHalUsbInterface* intf, void* ctx); +static void hid_u2f_init(usbd_device* dev, FuriHalUsbInterface* intf, void* ctx); static void hid_u2f_deinit(usbd_device* dev); static void hid_u2f_on_wakeup(usbd_device* dev); static void hid_u2f_on_suspend(usbd_device* dev); @@ -174,7 +174,7 @@ void furi_hal_hid_u2f_set_callback(HidU2fCallback cb, void* ctx) { } } -const FuriHalUsbInterface usb_hid_u2f = { +FuriHalUsbInterface usb_hid_u2f = { .init = hid_u2f_init, .deinit = hid_u2f_deinit, .wakeup = hid_u2f_on_wakeup, @@ -189,7 +189,7 @@ const FuriHalUsbInterface usb_hid_u2f = { .cfg_descr = (void*)&hid_u2f_cfg_desc, }; -static void hid_u2f_init(usbd_device* dev, const FuriHalUsbInterface* intf, void* ctx) { +static void hid_u2f_init(usbd_device* dev, FuriHalUsbInterface* intf, void* ctx) { UNUSED(intf); UNUSED(ctx); if(hid_u2f_semaphore == NULL) { diff --git a/targets/furi_hal_include/furi_hal_usb.h b/targets/furi_hal_include/furi_hal_usb.h index 03c5247c5..213e8d56f 100644 --- a/targets/furi_hal_include/furi_hal_usb.h +++ b/targets/furi_hal_include/furi_hal_usb.h @@ -9,7 +9,7 @@ extern "C" { typedef struct FuriHalUsbInterface FuriHalUsbInterface; struct FuriHalUsbInterface { - void (*init)(usbd_device* dev, const FuriHalUsbInterface* intf, void* ctx); + void (*init)(usbd_device* dev, FuriHalUsbInterface* intf, void* ctx); void (*deinit)(usbd_device* dev); void (*wakeup)(usbd_device* dev); void (*suspend)(usbd_device* dev); @@ -27,7 +27,7 @@ struct FuriHalUsbInterface { extern FuriHalUsbInterface usb_cdc_single; extern FuriHalUsbInterface usb_cdc_dual; extern FuriHalUsbInterface usb_hid; -extern const FuriHalUsbInterface usb_hid_u2f; +extern FuriHalUsbInterface usb_hid_u2f; extern FuriHalUsbInterface usb_ccid; typedef enum { @@ -49,13 +49,13 @@ void furi_hal_usb_init(void); * @param ctx context passed to device mode init function * @return true - mode switch started, false - mode switch is locked */ -bool furi_hal_usb_set_config(const FuriHalUsbInterface* new_if, void* ctx); +bool furi_hal_usb_set_config(FuriHalUsbInterface* new_if, void* ctx); /** Get USB device configuration * * @return current USB device mode */ -const FuriHalUsbInterface* furi_hal_usb_get_config(void); +FuriHalUsbInterface* furi_hal_usb_get_config(void); /** Lock USB device mode switch */ From acc90d0ac3cf317435c14f994289b223624534c8 Mon Sep 17 00:00:00 2001 From: hedger Date: Sat, 22 Feb 2025 20:43:11 +0000 Subject: [PATCH 084/268] linter fixes --- targets/f7/fatfs/user_diskio.h | 2 +- targets/f7/furi_hal/furi_hal_usb_cdc.c | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/targets/f7/fatfs/user_diskio.h b/targets/f7/fatfs/user_diskio.h index a0fcb9d57..2505de704 100644 --- a/targets/f7/fatfs/user_diskio.h +++ b/targets/f7/fatfs/user_diskio.h @@ -10,4 +10,4 @@ extern const Diskio_drvTypeDef sd_fatfs_driver; #ifdef __cplusplus } -#endif \ No newline at end of file +#endif diff --git a/targets/f7/furi_hal/furi_hal_usb_cdc.c b/targets/f7/furi_hal/furi_hal_usb_cdc.c index f734ef1a4..f9c1d3a42 100644 --- a/targets/f7/furi_hal/furi_hal_usb_cdc.c +++ b/targets/f7/furi_hal/furi_hal_usb_cdc.c @@ -432,7 +432,6 @@ FuriHalUsbInterface usb_cdc_dual = { static void cdc_init(usbd_device* dev, FuriHalUsbInterface* intf, void* ctx) { UNUSED(ctx); usb_dev = dev; - cdc_if_cur = intf; char* name = (char*)furi_hal_version_get_device_name_ptr(); From c71ea3aee496382e0122d34a252e30a1ca272699 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Sun, 23 Feb 2025 19:14:13 +0300 Subject: [PATCH 085/268] upd changelog --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5fbadce8f..37e8a3c6c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,5 @@ ## Main changes -- Current API: 81.1 +- Current API: 82.0 * SubGHz: Fix GangQi protocol (by @DoberBit and @mishamyte (who spent 2 weeks on this)) * SubGHz: Came Atomo button hold simulation with full cycle simulation (to allow proper pairing with receiver) * OFW: LFRFID - **EM4305 support** @@ -14,6 +14,7 @@ * Apps: **Check out more Apps updates and fixes by following** [this link](https://github.com/xMasterX/all-the-plugins/commits/dev) ## Other changes * Anims: Disable winter anims +* OFW: Stdio API improvements * OFW: GUI: Widget view extra options for JS * OFW: Update heap implementation * OFW: Updated Button Panel From 0e8a0228bebfde6f41a6b1d5b076a4839d1b793b Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Mon, 24 Feb 2025 10:56:57 +0300 Subject: [PATCH 086/268] add revers rb2 subghz protocol --- lib/subghz/protocols/marantec24.c | 3 + lib/subghz/protocols/protocol_items.c | 1 + lib/subghz/protocols/protocol_items.h | 1 + lib/subghz/protocols/revers_rb2.c | 408 ++++++++++++++++++++++++++ lib/subghz/protocols/revers_rb2.h | 109 +++++++ 5 files changed, 522 insertions(+) create mode 100644 lib/subghz/protocols/revers_rb2.c create mode 100644 lib/subghz/protocols/revers_rb2.h diff --git a/lib/subghz/protocols/marantec24.c b/lib/subghz/protocols/marantec24.c index eee009f2d..588aa1e5a 100644 --- a/lib/subghz/protocols/marantec24.c +++ b/lib/subghz/protocols/marantec24.c @@ -214,6 +214,9 @@ void subghz_protocol_decoder_marantec24_feed(void* context, bool level, volatile furi_assert(context); SubGhzProtocolDecoderMarantec24* instance = context; + // Marantec24 Decoder + // 2024 - @xMasterX (MMX) + // Key samples // 101011000000010111001000 = AC05C8 // 101011000000010111000100 = AC05C4 diff --git a/lib/subghz/protocols/protocol_items.c b/lib/subghz/protocols/protocol_items.c index ff7d29650..069a14a66 100644 --- a/lib/subghz/protocols/protocol_items.c +++ b/lib/subghz/protocols/protocol_items.c @@ -51,6 +51,7 @@ const SubGhzProtocol* subghz_protocol_registry_items[] = { &subghz_protocol_marantec24, &subghz_protocol_hollarm, &subghz_protocol_hay21, + &subghz_protocol_revers_rb2, }; const SubGhzProtocolRegistry subghz_protocol_registry = { diff --git a/lib/subghz/protocols/protocol_items.h b/lib/subghz/protocols/protocol_items.h index 71265e88c..d06882466 100644 --- a/lib/subghz/protocols/protocol_items.h +++ b/lib/subghz/protocols/protocol_items.h @@ -52,3 +52,4 @@ #include "marantec24.h" #include "hollarm.h" #include "hay21.h" +#include "revers_rb2.h" diff --git a/lib/subghz/protocols/revers_rb2.c b/lib/subghz/protocols/revers_rb2.c new file mode 100644 index 000000000..510e2698a --- /dev/null +++ b/lib/subghz/protocols/revers_rb2.c @@ -0,0 +1,408 @@ +#include "revers_rb2.h" +#include +#include +#include "../blocks/const.h" +#include "../blocks/decoder.h" +#include "../blocks/encoder.h" +#include "../blocks/generic.h" +#include "../blocks/math.h" + +#define TAG "SubGhzProtocolRevers_RB2" + +static const SubGhzBlockConst subghz_protocol_revers_rb2_const = { + .te_short = 250, + .te_long = 500, + .te_delta = 160, + .min_count_bit_for_found = 64, +}; + +struct SubGhzProtocolDecoderRevers_RB2 { + SubGhzProtocolDecoderBase base; + + SubGhzBlockDecoder decoder; + SubGhzBlockGeneric generic; + ManchesterState manchester_saved_state; + uint16_t header_count; +}; + +struct SubGhzProtocolEncoderRevers_RB2 { + SubGhzProtocolEncoderBase base; + + SubGhzProtocolBlockEncoder encoder; + SubGhzBlockGeneric generic; +}; + +typedef enum { + Revers_RB2DecoderStepReset = 0, + Revers_RB2DecoderStepHeader, + Revers_RB2DecoderStepDecoderData, +} Revers_RB2DecoderStep; + +const SubGhzProtocolDecoder subghz_protocol_revers_rb2_decoder = { + .alloc = subghz_protocol_decoder_revers_rb2_alloc, + .free = subghz_protocol_decoder_revers_rb2_free, + + .feed = subghz_protocol_decoder_revers_rb2_feed, + .reset = subghz_protocol_decoder_revers_rb2_reset, + + .get_hash_data = subghz_protocol_decoder_revers_rb2_get_hash_data, + .serialize = subghz_protocol_decoder_revers_rb2_serialize, + .deserialize = subghz_protocol_decoder_revers_rb2_deserialize, + .get_string = subghz_protocol_decoder_revers_rb2_get_string, +}; + +const SubGhzProtocolEncoder subghz_protocol_revers_rb2_encoder = { + .alloc = subghz_protocol_encoder_revers_rb2_alloc, + .free = subghz_protocol_encoder_revers_rb2_free, + + .deserialize = subghz_protocol_encoder_revers_rb2_deserialize, + .stop = subghz_protocol_encoder_revers_rb2_stop, + .yield = subghz_protocol_encoder_revers_rb2_yield, +}; + +const SubGhzProtocol subghz_protocol_revers_rb2 = { + .name = SUBGHZ_PROTOCOL_REVERSRB2_NAME, + .type = SubGhzProtocolTypeStatic, + .flag = SubGhzProtocolFlag_433 | SubGhzProtocolFlag_AM | SubGhzProtocolFlag_Decodable | + SubGhzProtocolFlag_Load | SubGhzProtocolFlag_Save | SubGhzProtocolFlag_Send, + + .decoder = &subghz_protocol_revers_rb2_decoder, + .encoder = &subghz_protocol_revers_rb2_encoder, +}; + +void* subghz_protocol_encoder_revers_rb2_alloc(SubGhzEnvironment* environment) { + UNUSED(environment); + SubGhzProtocolEncoderRevers_RB2* instance = malloc(sizeof(SubGhzProtocolEncoderRevers_RB2)); + + instance->base.protocol = &subghz_protocol_revers_rb2; + instance->generic.protocol_name = instance->base.protocol->name; + + instance->encoder.repeat = 10; + instance->encoder.size_upload = 256; + instance->encoder.upload = malloc(instance->encoder.size_upload * sizeof(LevelDuration)); + instance->encoder.is_running = false; + return instance; +} + +void subghz_protocol_encoder_revers_rb2_free(void* context) { + furi_assert(context); + SubGhzProtocolEncoderRevers_RB2* instance = context; + free(instance->encoder.upload); + free(instance); +} + +static LevelDuration + subghz_protocol_encoder_revers_rb2_add_duration_to_upload(ManchesterEncoderResult result) { + LevelDuration data = {.duration = 0, .level = 0}; + switch(result) { + case ManchesterEncoderResultShortLow: + data.duration = subghz_protocol_revers_rb2_const.te_short; + data.level = false; + break; + case ManchesterEncoderResultLongLow: + data.duration = subghz_protocol_revers_rb2_const.te_long; + data.level = false; + break; + case ManchesterEncoderResultLongHigh: + data.duration = subghz_protocol_revers_rb2_const.te_long; + data.level = true; + break; + case ManchesterEncoderResultShortHigh: + data.duration = subghz_protocol_revers_rb2_const.te_short; + data.level = true; + break; + + default: + furi_crash("SubGhz: ManchesterEncoderResult is incorrect."); + break; + } + return level_duration_make(data.level, data.duration); +} + +/** + * Generating an upload from data. + * @param instance Pointer to a SubGhzProtocolEncoderRevers_RB2 instance + */ +static void + subghz_protocol_encoder_revers_rb2_get_upload(SubGhzProtocolEncoderRevers_RB2* instance) { + furi_assert(instance); + size_t index = 0; + + ManchesterEncoderState enc_state; + manchester_encoder_reset(&enc_state); + ManchesterEncoderResult result; + + for(uint8_t i = instance->generic.data_count_bit; i > 0; i--) { + if(!manchester_encoder_advance( + &enc_state, bit_read(instance->generic.data, i - 1), &result)) { + instance->encoder.upload[index++] = + subghz_protocol_encoder_revers_rb2_add_duration_to_upload(result); + manchester_encoder_advance( + &enc_state, bit_read(instance->generic.data, i - 1), &result); + } + instance->encoder.upload[index++] = + subghz_protocol_encoder_revers_rb2_add_duration_to_upload(result); + } + instance->encoder.upload[index] = subghz_protocol_encoder_revers_rb2_add_duration_to_upload( + manchester_encoder_finish(&enc_state)); + if(level_duration_get_level(instance->encoder.upload[index])) { + index++; + } + instance->encoder.upload[index++] = level_duration_make(false, (uint32_t)320); + instance->encoder.size_upload = index; +} + +/** + * Analysis of received data + * @param instance Pointer to a SubGhzBlockGeneric* instance + */ +static void subghz_protocol_revers_rb2_remote_controller(SubGhzBlockGeneric* instance) { + // Revers RB2 / RB2M Decoder + // 02.2025 - @xMasterX (MMX) + instance->serial = (((instance->data << 16) >> 16) >> 10); +} + +SubGhzProtocolStatus + subghz_protocol_encoder_revers_rb2_deserialize(void* context, FlipperFormat* flipper_format) { + furi_assert(context); + SubGhzProtocolEncoderRevers_RB2* instance = context; + SubGhzProtocolStatus ret = SubGhzProtocolStatusError; + do { + ret = subghz_block_generic_deserialize_check_count_bit( + &instance->generic, + flipper_format, + subghz_protocol_revers_rb2_const.min_count_bit_for_found); + if(ret != SubGhzProtocolStatusOk) { + break; + } + //optional parameter parameter + flipper_format_read_uint32( + flipper_format, "Repeat", (uint32_t*)&instance->encoder.repeat, 1); + + subghz_protocol_revers_rb2_remote_controller(&instance->generic); + subghz_protocol_encoder_revers_rb2_get_upload(instance); + instance->encoder.is_running = true; + } while(false); + + return ret; +} + +void subghz_protocol_encoder_revers_rb2_stop(void* context) { + SubGhzProtocolEncoderRevers_RB2* instance = context; + instance->encoder.is_running = false; +} + +LevelDuration subghz_protocol_encoder_revers_rb2_yield(void* context) { + SubGhzProtocolEncoderRevers_RB2* instance = context; + + if(instance->encoder.repeat == 0 || !instance->encoder.is_running) { + instance->encoder.is_running = false; + return level_duration_reset(); + } + + LevelDuration ret = instance->encoder.upload[instance->encoder.front]; + + if(++instance->encoder.front == instance->encoder.size_upload) { + instance->encoder.repeat--; + instance->encoder.front = 0; + } + + return ret; +} + +void* subghz_protocol_decoder_revers_rb2_alloc(SubGhzEnvironment* environment) { + UNUSED(environment); + SubGhzProtocolDecoderRevers_RB2* instance = malloc(sizeof(SubGhzProtocolDecoderRevers_RB2)); + instance->base.protocol = &subghz_protocol_revers_rb2; + instance->generic.protocol_name = instance->base.protocol->name; + return instance; +} + +void subghz_protocol_decoder_revers_rb2_free(void* context) { + furi_assert(context); + SubGhzProtocolDecoderRevers_RB2* instance = context; + free(instance); +} + +void subghz_protocol_decoder_revers_rb2_reset(void* context) { + furi_assert(context); + SubGhzProtocolDecoderRevers_RB2* instance = context; + manchester_advance( + instance->manchester_saved_state, + ManchesterEventReset, + &instance->manchester_saved_state, + NULL); +} + +void subghz_protocol_decoder_revers_rb2_addbit(void* context, bool data) { + SubGhzProtocolDecoderRevers_RB2* instance = context; + instance->decoder.decode_data = (instance->decoder.decode_data << 1) | data; + instance->decoder.decode_count_bit++; + + if(instance->decoder.decode_count_bit >= 65) { + instance->decoder.decode_data = 0; + instance->decoder.decode_count_bit = 0; + return; + } + + if(instance->decoder.decode_count_bit < + subghz_protocol_revers_rb2_const.min_count_bit_for_found) { + return; + } + + // Revers RB2 / RB2M Decoder + // 02.2025 - @xMasterX (MMX) + + uint16_t preamble = (instance->decoder.decode_data >> 48) & 0xFF; + uint16_t stop_code = (instance->decoder.decode_data & 0x3FF); + + if(preamble == 0xFF && stop_code == 0x200) { + //Found header and stop code + instance->generic.data = instance->decoder.decode_data; + instance->generic.data_count_bit = instance->decoder.decode_count_bit; + + if(instance->base.callback) + instance->base.callback(&instance->base, instance->base.context); + + instance->decoder.decode_data = 0; + instance->decoder.decode_count_bit = 0; + manchester_advance( + instance->manchester_saved_state, + ManchesterEventReset, + &instance->manchester_saved_state, + NULL); + } +} + +void subghz_protocol_decoder_revers_rb2_feed(void* context, bool level, volatile uint32_t duration) { + furi_assert(context); + SubGhzProtocolDecoderRevers_RB2* instance = context; + ManchesterEvent event = ManchesterEventReset; + + switch(instance->decoder.parser_step) { + case Revers_RB2DecoderStepReset: + if((!level) && + (DURATION_DIFF(duration, 600) < subghz_protocol_revers_rb2_const.te_delta)) { + instance->decoder.parser_step = Revers_RB2DecoderStepHeader; + instance->decoder.decode_data = 0; + instance->decoder.decode_count_bit = 0; + manchester_advance( + instance->manchester_saved_state, + ManchesterEventReset, + &instance->manchester_saved_state, + NULL); + } + break; + case Revers_RB2DecoderStepHeader: + if(!level) { + if(DURATION_DIFF(duration, subghz_protocol_revers_rb2_const.te_short) < + subghz_protocol_revers_rb2_const.te_delta) { + if(instance->decoder.te_last == 1) { + instance->header_count++; + } + instance->decoder.te_last = level; + } else { + instance->header_count = 0; + instance->decoder.te_last = 0; + instance->decoder.parser_step = Revers_RB2DecoderStepReset; + } + } else { + if(DURATION_DIFF(duration, subghz_protocol_revers_rb2_const.te_short) < + subghz_protocol_revers_rb2_const.te_delta) { + if(instance->decoder.te_last == 0) { + instance->header_count++; + } + instance->decoder.te_last = level; + } else { + instance->header_count = 0; + instance->decoder.te_last = 0; + instance->decoder.parser_step = Revers_RB2DecoderStepReset; + } + } + + if(instance->header_count == 4) { + instance->header_count = 0; + instance->decoder.decode_data = 0xF; + instance->decoder.decode_count_bit = 4; + instance->decoder.parser_step = Revers_RB2DecoderStepDecoderData; + } + break; + case Revers_RB2DecoderStepDecoderData: + if(!level) { + if(DURATION_DIFF(duration, subghz_protocol_revers_rb2_const.te_short) < + subghz_protocol_revers_rb2_const.te_delta) { + event = ManchesterEventShortLow; + } else if( + DURATION_DIFF(duration, subghz_protocol_revers_rb2_const.te_long) < + subghz_protocol_revers_rb2_const.te_delta) { + event = ManchesterEventLongLow; + } else { + instance->decoder.parser_step = Revers_RB2DecoderStepReset; + } + } else { + if(DURATION_DIFF(duration, subghz_protocol_revers_rb2_const.te_short) < + subghz_protocol_revers_rb2_const.te_delta) { + event = ManchesterEventShortHigh; + } else if( + DURATION_DIFF(duration, subghz_protocol_revers_rb2_const.te_long) < + subghz_protocol_revers_rb2_const.te_delta) { + event = ManchesterEventLongHigh; + } else { + instance->decoder.parser_step = Revers_RB2DecoderStepReset; + } + } + if(event != ManchesterEventReset) { + bool data; + bool data_ok = manchester_advance( + instance->manchester_saved_state, event, &instance->manchester_saved_state, &data); + + if(data_ok) { + subghz_protocol_decoder_revers_rb2_addbit(instance, data); + } + } + break; + } +} + +uint8_t subghz_protocol_decoder_revers_rb2_get_hash_data(void* context) { + furi_assert(context); + SubGhzProtocolDecoderRevers_RB2* instance = context; + return subghz_protocol_blocks_get_hash_data( + &instance->decoder, (instance->decoder.decode_count_bit / 8) + 1); +} + +SubGhzProtocolStatus subghz_protocol_decoder_revers_rb2_serialize( + void* context, + FlipperFormat* flipper_format, + SubGhzRadioPreset* preset) { + furi_assert(context); + SubGhzProtocolDecoderRevers_RB2* instance = context; + return subghz_block_generic_serialize(&instance->generic, flipper_format, preset); +} + +SubGhzProtocolStatus + subghz_protocol_decoder_revers_rb2_deserialize(void* context, FlipperFormat* flipper_format) { + furi_assert(context); + SubGhzProtocolDecoderRevers_RB2* instance = context; + return subghz_block_generic_deserialize_check_count_bit( + &instance->generic, + flipper_format, + subghz_protocol_revers_rb2_const.min_count_bit_for_found); +} + +void subghz_protocol_decoder_revers_rb2_get_string(void* context, FuriString* output) { + furi_assert(context); + SubGhzProtocolDecoderRevers_RB2* instance = context; + subghz_protocol_revers_rb2_remote_controller(&instance->generic); + + furi_string_cat_printf( + output, + "%s %db\r\n" + "Key:%lX%08lX\r\n" + "Sn:0x%08lX \r\n", + instance->generic.protocol_name, + instance->generic.data_count_bit, + (uint32_t)(instance->generic.data >> 32), + (uint32_t)(instance->generic.data & 0xFFFFFFFF), + instance->generic.serial); +} diff --git a/lib/subghz/protocols/revers_rb2.h b/lib/subghz/protocols/revers_rb2.h new file mode 100644 index 000000000..7ccd2a9a2 --- /dev/null +++ b/lib/subghz/protocols/revers_rb2.h @@ -0,0 +1,109 @@ +#pragma once + +#include "base.h" + +#define SUBGHZ_PROTOCOL_REVERSRB2_NAME "Revers_RB2" + +typedef struct SubGhzProtocolDecoderRevers_RB2 SubGhzProtocolDecoderRevers_RB2; +typedef struct SubGhzProtocolEncoderRevers_RB2 SubGhzProtocolEncoderRevers_RB2; + +extern const SubGhzProtocolDecoder subghz_protocol_revers_rb2_decoder; +extern const SubGhzProtocolEncoder subghz_protocol_revers_rb2_encoder; +extern const SubGhzProtocol subghz_protocol_revers_rb2; + +/** + * Allocate SubGhzProtocolEncoderRevers_RB2. + * @param environment Pointer to a SubGhzEnvironment instance + * @return SubGhzProtocolEncoderRevers_RB2* pointer to a SubGhzProtocolEncoderRevers_RB2 instance + */ +void* subghz_protocol_encoder_revers_rb2_alloc(SubGhzEnvironment* environment); + +/** + * Free SubGhzProtocolEncoderRevers_RB2. + * @param context Pointer to a SubGhzProtocolEncoderRevers_RB2 instance + */ +void subghz_protocol_encoder_revers_rb2_free(void* context); + +/** + * Deserialize and generating an upload to send. + * @param context Pointer to a SubGhzProtocolEncoderRevers_RB2 instance + * @param flipper_format Pointer to a FlipperFormat instance + * @return status + */ +SubGhzProtocolStatus + subghz_protocol_encoder_revers_rb2_deserialize(void* context, FlipperFormat* flipper_format); + +/** + * Forced transmission stop. + * @param context Pointer to a SubGhzProtocolEncoderRevers_RB2 instance + */ +void subghz_protocol_encoder_revers_rb2_stop(void* context); + +/** + * Getting the level and duration of the upload to be loaded into DMA. + * @param context Pointer to a SubGhzProtocolEncoderRevers_RB2 instance + * @return LevelDuration + */ +LevelDuration subghz_protocol_encoder_revers_rb2_yield(void* context); + +/** + * Allocate SubGhzProtocolDecoderRevers_RB2. + * @param environment Pointer to a SubGhzEnvironment instance + * @return SubGhzProtocolDecoderRevers_RB2* pointer to a SubGhzProtocolDecoderRevers_RB2 instance + */ +void* subghz_protocol_decoder_revers_rb2_alloc(SubGhzEnvironment* environment); + +/** + * Free SubGhzProtocolDecoderRevers_RB2. + * @param context Pointer to a SubGhzProtocolDecoderRevers_RB2 instance + */ +void subghz_protocol_decoder_revers_rb2_free(void* context); + +/** + * Reset decoder SubGhzProtocolDecoderRevers_RB2. + * @param context Pointer to a SubGhzProtocolDecoderRevers_RB2 instance + */ +void subghz_protocol_decoder_revers_rb2_reset(void* context); + +/** + * Parse a raw sequence of levels and durations received from the air. + * @param context Pointer to a SubGhzProtocolDecoderRevers_RB2 instance + * @param level Signal level true-high false-low + * @param duration Duration of this level in, us + */ +void subghz_protocol_decoder_revers_rb2_feed(void* context, bool level, uint32_t duration); + +/** + * Getting the hash sum of the last randomly received parcel. + * @param context Pointer to a SubGhzProtocolDecoderRevers_RB2 instance + * @return hash Hash sum + */ +uint8_t subghz_protocol_decoder_revers_rb2_get_hash_data(void* context); + +/** + * Serialize data SubGhzProtocolDecoderRevers_RB2. + * @param context Pointer to a SubGhzProtocolDecoderRevers_RB2 instance + * @param flipper_format Pointer to a FlipperFormat instance + * @param preset The modulation on which the signal was received, SubGhzRadioPreset + * @return status + */ +SubGhzProtocolStatus subghz_protocol_decoder_revers_rb2_serialize( + void* context, + FlipperFormat* flipper_format, + SubGhzRadioPreset* preset); + +/** + * Deserialize data SubGhzProtocolDecoderRevers_RB2. + * @param context Pointer to a SubGhzProtocolDecoderRevers_RB2 instance + * @param flipper_format Pointer to a FlipperFormat instance + * @return status + */ +SubGhzProtocolStatus + subghz_protocol_decoder_revers_rb2_deserialize(void* context, FlipperFormat* flipper_format); + +/** + * Getting a textual representation of the received data. + * @param context Pointer to a SubGhzProtocolDecoderRevers_RB2 instance + * @param output Resulting text + */ +void subghz_protocol_decoder_revers_rb2_get_string(void* context, FuriString* output); From dd2388e40d48ee228dcf64c7254bfca7abb6bcd0 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Mon, 24 Feb 2025 10:57:27 +0300 Subject: [PATCH 087/268] discard wrong hollarms if bytesum is invalid --- lib/subghz/protocols/hollarm.c | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/lib/subghz/protocols/hollarm.c b/lib/subghz/protocols/hollarm.c index ab2ce342f..fc76affa0 100644 --- a/lib/subghz/protocols/hollarm.c +++ b/lib/subghz/protocols/hollarm.c @@ -376,6 +376,21 @@ void subghz_protocol_decoder_hollarm_feed(void* context, bool level, volatile ui // Saving with 2bit to the right offset for proper parsing instance->generic.data = (instance->decoder.decode_data >> 2); instance->generic.data_count_bit = instance->decoder.decode_count_bit; + + uint8_t bytesum = ((instance->generic.data >> 32) & 0xFF) + + ((instance->generic.data >> 24) & 0xFF) + + ((instance->generic.data >> 16) & 0xFF) + + ((instance->generic.data >> 8) & 0xFF); + + if(bytesum != (instance->generic.data & 0xFF)) { + // Check if the key is valid by verifying the sum + instance->generic.data = 0; + instance->generic.data_count_bit = 0; + instance->decoder.decode_data = 0; + instance->decoder.decode_count_bit = 0; + instance->decoder.parser_step = HollarmDecoderStepReset; + break; + } if(instance->base.callback) instance->base.callback(&instance->base, instance->base.context); } From 326eff734d9245f0fec5df863484040401b0401c Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Mon, 24 Feb 2025 10:59:03 +0300 Subject: [PATCH 088/268] fmt --- applications/system/js_app/js_modules.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/applications/system/js_app/js_modules.h b/applications/system/js_app/js_modules.h index 1166a035e..c35f80d16 100644 --- a/applications/system/js_app/js_modules.h +++ b/applications/system/js_app/js_modules.h @@ -12,7 +12,7 @@ #define JS_SDK_VENDOR_FIRMWARE "unleashed" #define JS_SDK_VENDOR "flipperdevices" #define JS_SDK_MAJOR 0 -#define JS_SDK_MINOR 3 +#define JS_SDK_MINOR 3 /** * @brief Returns the foreign pointer in `obj["_"]` From 6579b4195e5aa96ed36a3891a28c05dfcd56d742 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Mon, 24 Feb 2025 11:12:45 +0300 Subject: [PATCH 089/268] add manually --- .../main/subghz/helpers/subghz_custom_event.h | 1 + .../subghz/scenes/subghz_scene_set_type.c | 853 +++++++++--------- 2 files changed, 405 insertions(+), 449 deletions(-) diff --git a/applications/main/subghz/helpers/subghz_custom_event.h b/applications/main/subghz/helpers/subghz_custom_event.h index e84c4788d..bd8dee161 100644 --- a/applications/main/subghz/helpers/subghz_custom_event.h +++ b/applications/main/subghz/helpers/subghz_custom_event.h @@ -122,6 +122,7 @@ typedef enum { SetTypeBETT_433, SetTypeGangQi_433, SetTypeHollarm_433, + SetTypeReversRB2_433, SetTypeMarantec24_868, SetTypeLinear_300_00, // SetTypeNeroSketch, //Deleted in OFW diff --git a/applications/main/subghz/scenes/subghz_scene_set_type.c b/applications/main/subghz/scenes/subghz_scene_set_type.c index 6c3e44894..20cef68df 100644 --- a/applications/main/subghz/scenes/subghz_scene_set_type.c +++ b/applications/main/subghz/scenes/subghz_scene_set_type.c @@ -67,6 +67,7 @@ static const char* submenu_names[SetTypeMAX] = { [SetTypePricenton433] = "Princeton 433MHz", [SetTypeGangQi_433] = "GangQi 433MHz", [SetTypeHollarm_433] = "Hollarm 433MHz", + [SetTypeReversRB2_433] = "Revers RB2 433MHz", [SetTypeMarantec24_868] = "Marantec24 868MHz", [SetTypeBETT_433] = "BETT 433MHz", [SetTypeLinear_300_00] = "Linear 300MHz", @@ -192,114 +193,104 @@ bool subghz_scene_set_type_on_event(void* context, SceneManagerEvent event) { GenInfo gen_info = {0}; switch(event.event) { case SetTypePricenton433: - gen_info = (GenInfo){ - .type = GenData, - .mod = "AM650", - .freq = 433920000, - .data.name = SUBGHZ_PROTOCOL_PRINCETON_NAME, - .data.key = (key & 0x00FFFFF0) | 0x4, // btn 0x1, 0x2, 0x4, 0x8 - .data.bits = 24, - .data.te = 400}; + gen_info = (GenInfo){.type = GenData, + .mod = "AM650", + .freq = 433920000, + .data.name = SUBGHZ_PROTOCOL_PRINCETON_NAME, + .data.key = (key & 0x00FFFFF0) | 0x4, // btn 0x1, 0x2, 0x4, 0x8 + .data.bits = 24, + .data.te = 400}; break; case SetTypePricenton315: - gen_info = (GenInfo){ - .type = GenData, - .mod = "AM650", - .freq = 315000000, - .data.name = SUBGHZ_PROTOCOL_PRINCETON_NAME, - .data.key = (key & 0x00FFFFF0) | 0x4, // btn 0x1, 0x2, 0x4, 0x8 - .data.bits = 24, - .data.te = 400}; + gen_info = (GenInfo){.type = GenData, + .mod = "AM650", + .freq = 315000000, + .data.name = SUBGHZ_PROTOCOL_PRINCETON_NAME, + .data.key = (key & 0x00FFFFF0) | 0x4, // btn 0x1, 0x2, 0x4, 0x8 + .data.bits = 24, + .data.te = 400}; break; case SetTypeNiceFlo12bit: - gen_info = (GenInfo){ - .type = GenData, - .mod = "AM650", - .freq = 433920000, - .data.name = SUBGHZ_PROTOCOL_NICE_FLO_NAME, - .data.key = (key & 0x00000FF0) | 0x1, // btn 0x1, 0x2, 0x4 - .data.bits = 12, - .data.te = 0}; + gen_info = (GenInfo){.type = GenData, + .mod = "AM650", + .freq = 433920000, + .data.name = SUBGHZ_PROTOCOL_NICE_FLO_NAME, + .data.key = (key & 0x00000FF0) | 0x1, // btn 0x1, 0x2, 0x4 + .data.bits = 12, + .data.te = 0}; break; case SetTypeNiceFlo24bit: - gen_info = (GenInfo){ - .type = GenData, - .mod = "AM650", - .freq = 433920000, - .data.name = SUBGHZ_PROTOCOL_NICE_FLO_NAME, - .data.key = (key & 0x00FFFFF0) | 0x4, // btn 0x1, 0x2, 0x4, 0x8 - .data.bits = 24, - .data.te = 0}; + gen_info = (GenInfo){.type = GenData, + .mod = "AM650", + .freq = 433920000, + .data.name = SUBGHZ_PROTOCOL_NICE_FLO_NAME, + .data.key = (key & 0x00FFFFF0) | 0x4, // btn 0x1, 0x2, 0x4, 0x8 + .data.bits = 24, + .data.te = 0}; break; case SetTypeCAME12bit: - gen_info = (GenInfo){ - .type = GenData, - .mod = "AM650", - .freq = 433920000, - .data.name = SUBGHZ_PROTOCOL_CAME_NAME, - .data.key = (key & 0x00000FF0) | 0x1, // btn 0x1, 0x2, 0x4 - .data.bits = 12, - .data.te = 0}; + gen_info = (GenInfo){.type = GenData, + .mod = "AM650", + .freq = 433920000, + .data.name = SUBGHZ_PROTOCOL_CAME_NAME, + .data.key = (key & 0x00000FF0) | 0x1, // btn 0x1, 0x2, 0x4 + .data.bits = 12, + .data.te = 0}; break; case SetTypeCAME24bit: - gen_info = (GenInfo){ - .type = GenData, - .mod = "AM650", - .freq = 433920000, - .data.name = SUBGHZ_PROTOCOL_CAME_NAME, - .data.key = (key & 0x00FFFFF0) | 0x4, // btn 0x1, 0x2, 0x4, 0x8 - .data.bits = 24, - .data.te = 0}; + gen_info = (GenInfo){.type = GenData, + .mod = "AM650", + .freq = 433920000, + .data.name = SUBGHZ_PROTOCOL_CAME_NAME, + .data.key = (key & 0x00FFFFF0) | 0x4, // btn 0x1, 0x2, 0x4, 0x8 + .data.bits = 24, + .data.te = 0}; break; case SetTypeCAME12bit868: - gen_info = (GenInfo){ - .type = GenData, - .mod = "AM650", - .freq = 868350000, - .data.name = SUBGHZ_PROTOCOL_CAME_NAME, - .data.key = (key & 0x00000FF0) | 0x1, // btn 0x1, 0x2, 0x4 - .data.bits = 12, - .data.te = 0}; + gen_info = (GenInfo){.type = GenData, + .mod = "AM650", + .freq = 868350000, + .data.name = SUBGHZ_PROTOCOL_CAME_NAME, + .data.key = (key & 0x00000FF0) | 0x1, // btn 0x1, 0x2, 0x4 + .data.bits = 12, + .data.te = 0}; break; case SetTypeCAME24bit868: - gen_info = (GenInfo){ - .type = GenData, - .mod = "AM650", - .freq = 868350000, - .data.name = SUBGHZ_PROTOCOL_CAME_NAME, - .data.key = (key & 0x00FFFFF0) | 0x4, // btn 0x1, 0x2, 0x4, 0x8 - .data.bits = 24, - .data.te = 0}; + gen_info = (GenInfo){.type = GenData, + .mod = "AM650", + .freq = 868350000, + .data.name = SUBGHZ_PROTOCOL_CAME_NAME, + .data.key = (key & 0x00FFFFF0) | 0x4, // btn 0x1, 0x2, 0x4, 0x8 + .data.bits = 24, + .data.te = 0}; break; case SetTypeLinear_300_00: - gen_info = (GenInfo){ - .type = GenData, - .mod = "AM650", - .freq = 300000000, - .data.name = SUBGHZ_PROTOCOL_LINEAR_NAME, - .data.key = (key & 0x3FF), - .data.bits = 10, - .data.te = 0}; + gen_info = (GenInfo){.type = GenData, + .mod = "AM650", + .freq = 300000000, + .data.name = SUBGHZ_PROTOCOL_LINEAR_NAME, + .data.key = (key & 0x3FF), + .data.bits = 10, + .data.te = 0}; break; case SetTypeBETT_433: - gen_info = (GenInfo){ - .type = GenData, - .mod = "AM650", - .freq = 433920000, - .data.name = SUBGHZ_PROTOCOL_BETT_NAME, - .data.key = (key & 0x0000FFF0), - .data.bits = 18, - .data.te = 0}; + gen_info = (GenInfo){.type = GenData, + .mod = "AM650", + .freq = 433920000, + .data.name = SUBGHZ_PROTOCOL_BETT_NAME, + .data.key = (key & 0x0000FFF0), + .data.bits = 18, + .data.te = 0}; break; case SetTypeCAMETwee: - gen_info = (GenInfo){ - .type = GenData, - .mod = "AM650", - .freq = 433920000, - .data.name = SUBGHZ_PROTOCOL_CAME_TWEE_NAME, - .data.key = 0x003FFF7200000000 | ((key & 0x0FFFFFF0) ^ 0xE0E0E0EE), // ???? - .data.bits = 54, - .data.te = 0}; + gen_info = (GenInfo){.type = GenData, + .mod = "AM650", + .freq = 433920000, + .data.name = SUBGHZ_PROTOCOL_CAME_TWEE_NAME, + .data.key = 0x003FFF7200000000 | + ((key & 0x0FFFFFF0) ^ 0xE0E0E0EE), // ???? + .data.bits = 54, + .data.te = 0}; break; case SetTypeGateTX: gen_info = (GenInfo){ @@ -337,6 +328,16 @@ bool subghz_scene_set_type_on_event(void* context, SceneManagerEvent event) { .data.bits = 42, .data.te = 0}; break; + case SetTypeReversRB2_433: + gen_info = (GenInfo){.type = GenData, + .mod = "AM650", + .freq = 433920000, + .data.name = SUBGHZ_PROTOCOL_REVERSRB2_NAME, // 64bits no buttons + .data.key = (key & 0x00000FFFFFFFF000) | 0xFFFFF00000000000 | + 0x0000000000000A00, + .data.bits = 64, + .data.te = 0}; + break; case SetTypeMarantec24_868: gen_info = (GenInfo){ .type = GenData, @@ -348,421 +349,379 @@ bool subghz_scene_set_type_on_event(void* context, SceneManagerEvent event) { .data.te = 0}; break; case SetTypeFaacSLH_433: - gen_info = (GenInfo){ - .type = GenFaacSLH, - .mod = "AM650", - .freq = 433920000, - .faac_slh.serial = ((key & 0x00FFFFF0) | 0xA0000006) >> 4, - .faac_slh.btn = 0x06, - .faac_slh.cnt = 0x02, - .faac_slh.seed = key, - .faac_slh.manuf = "FAAC_SLH"}; + gen_info = (GenInfo){.type = GenFaacSLH, + .mod = "AM650", + .freq = 433920000, + .faac_slh.serial = ((key & 0x00FFFFF0) | 0xA0000006) >> 4, + .faac_slh.btn = 0x06, + .faac_slh.cnt = 0x02, + .faac_slh.seed = key, + .faac_slh.manuf = "FAAC_SLH"}; break; case SetTypeFaacSLH_868: - gen_info = (GenInfo){ - .type = GenFaacSLH, - .mod = "AM650", - .freq = 868350000, - .faac_slh.serial = ((key & 0x00FFFFF0) | 0xA0000006) >> 4, - .faac_slh.btn = 0x06, - .faac_slh.cnt = 0x02, - .faac_slh.seed = (key & 0x0FFFFFFF), - .faac_slh.manuf = "FAAC_SLH"}; + gen_info = (GenInfo){.type = GenFaacSLH, + .mod = "AM650", + .freq = 868350000, + .faac_slh.serial = ((key & 0x00FFFFF0) | 0xA0000006) >> 4, + .faac_slh.btn = 0x06, + .faac_slh.cnt = 0x02, + .faac_slh.seed = (key & 0x0FFFFFFF), + .faac_slh.manuf = "FAAC_SLH"}; break; case SetTypeBeninca433: - gen_info = (GenInfo){ - .type = GenKeeloq, - .mod = "AM650", - .freq = 433920000, - .keeloq.serial = (key & 0x000FFF00) | 0x00800080, - .keeloq.btn = 0x01, - .keeloq.cnt = 0x05, - .keeloq.manuf = "Beninca"}; + gen_info = (GenInfo){.type = GenKeeloq, + .mod = "AM650", + .freq = 433920000, + .keeloq.serial = (key & 0x000FFF00) | 0x00800080, + .keeloq.btn = 0x01, + .keeloq.cnt = 0x05, + .keeloq.manuf = "Beninca"}; break; case SetTypeBeninca868: - gen_info = (GenInfo){ - .type = GenKeeloq, - .mod = "AM650", - .freq = 868350000, - .keeloq.serial = (key & 0x000FFF00) | 0x00800080, - .keeloq.btn = 0x01, - .keeloq.cnt = 0x05, - .keeloq.manuf = "Beninca"}; + gen_info = (GenInfo){.type = GenKeeloq, + .mod = "AM650", + .freq = 868350000, + .keeloq.serial = (key & 0x000FFF00) | 0x00800080, + .keeloq.btn = 0x01, + .keeloq.cnt = 0x05, + .keeloq.manuf = "Beninca"}; break; case SetTypeAllmatic433: - gen_info = (GenInfo){ - .type = GenKeeloq, - .mod = "AM650", - .freq = 433920000, - .keeloq.serial = (key & 0x00FFFF00) | 0x01000011, - .keeloq.btn = 0x0C, - .keeloq.cnt = 0x05, - .keeloq.manuf = "Beninca"}; + gen_info = (GenInfo){.type = GenKeeloq, + .mod = "AM650", + .freq = 433920000, + .keeloq.serial = (key & 0x00FFFF00) | 0x01000011, + .keeloq.btn = 0x0C, + .keeloq.cnt = 0x05, + .keeloq.manuf = "Beninca"}; break; case SetTypeAllmatic868: - gen_info = (GenInfo){ - .type = GenKeeloq, - .mod = "AM650", - .freq = 868350000, - .keeloq.serial = (key & 0x00FFFF00) | 0x01000011, - .keeloq.btn = 0x0C, - .keeloq.cnt = 0x05, - .keeloq.manuf = "Beninca"}; + gen_info = (GenInfo){.type = GenKeeloq, + .mod = "AM650", + .freq = 868350000, + .keeloq.serial = (key & 0x00FFFF00) | 0x01000011, + .keeloq.btn = 0x0C, + .keeloq.cnt = 0x05, + .keeloq.manuf = "Beninca"}; break; case SetTypeCenturion433: - gen_info = (GenInfo){ - .type = GenKeeloq, - .mod = "AM650", - .freq = 433920000, - .keeloq.serial = (key & 0x0000FFFF), - .keeloq.btn = 0x02, - .keeloq.cnt = 0x03, - .keeloq.manuf = "Centurion"}; + gen_info = (GenInfo){.type = GenKeeloq, + .mod = "AM650", + .freq = 433920000, + .keeloq.serial = (key & 0x0000FFFF), + .keeloq.btn = 0x02, + .keeloq.cnt = 0x03, + .keeloq.manuf = "Centurion"}; break; case SetTypeMonarch433: - gen_info = (GenInfo){ - .type = GenKeeloq, - .mod = "AM650", - .freq = 433920000, - .keeloq.serial = (key & 0x0000FFFF), - .keeloq.btn = 0x0A, - .keeloq.cnt = 0x03, - .keeloq.manuf = "Monarch"}; + gen_info = (GenInfo){.type = GenKeeloq, + .mod = "AM650", + .freq = 433920000, + .keeloq.serial = (key & 0x0000FFFF), + .keeloq.btn = 0x0A, + .keeloq.cnt = 0x03, + .keeloq.manuf = "Monarch"}; break; case SetTypeJollyMotors433: - gen_info = (GenInfo){ - .type = GenKeeloq, - .mod = "AM650", - .freq = 433920000, - .keeloq.serial = (key & 0x000FFFFF), - .keeloq.btn = 0x02, - .keeloq.cnt = 0x03, - .keeloq.manuf = "Jolly_Motors"}; + gen_info = (GenInfo){.type = GenKeeloq, + .mod = "AM650", + .freq = 433920000, + .keeloq.serial = (key & 0x000FFFFF), + .keeloq.btn = 0x02, + .keeloq.cnt = 0x03, + .keeloq.manuf = "Jolly_Motors"}; break; case SetTypeElmesElectronic: - gen_info = (GenInfo){ - .type = GenKeeloq, - .mod = "AM650", - .freq = 433920000, - .keeloq.serial = (key & 0x00FFFFFF) | 0x02000000, - .keeloq.btn = 0x02, - .keeloq.cnt = 0x03, - .keeloq.manuf = "Elmes_Poland"}; + gen_info = (GenInfo){.type = GenKeeloq, + .mod = "AM650", + .freq = 433920000, + .keeloq.serial = (key & 0x00FFFFFF) | 0x02000000, + .keeloq.btn = 0x02, + .keeloq.cnt = 0x03, + .keeloq.manuf = "Elmes_Poland"}; break; case SetTypeANMotorsAT4: - gen_info = (GenInfo){ - .type = GenKeeloq, - .mod = "AM650", - .freq = 433920000, - .keeloq.serial = (key & 0x000FFFFF) | 0x04700000, - .keeloq.btn = 0x02, - .keeloq.cnt = 0x21, - .keeloq.manuf = "AN-Motors"}; + gen_info = (GenInfo){.type = GenKeeloq, + .mod = "AM650", + .freq = 433920000, + .keeloq.serial = (key & 0x000FFFFF) | 0x04700000, + .keeloq.btn = 0x02, + .keeloq.cnt = 0x21, + .keeloq.manuf = "AN-Motors"}; break; case SetTypeAprimatic: - gen_info = (GenInfo){ - .type = GenKeeloq, - .mod = "AM650", - .freq = 433920000, - .keeloq.serial = (key & 0x000FFFFF) | 0x00600000, - .keeloq.btn = 0x08, - .keeloq.cnt = 0x03, - .keeloq.manuf = "Aprimatic"}; + gen_info = (GenInfo){.type = GenKeeloq, + .mod = "AM650", + .freq = 433920000, + .keeloq.serial = (key & 0x000FFFFF) | 0x00600000, + .keeloq.btn = 0x08, + .keeloq.cnt = 0x03, + .keeloq.manuf = "Aprimatic"}; break; case SetTypeGibidi433: - gen_info = (GenInfo){ - .type = GenKeeloq, - .mod = "AM650", - .freq = 433920000, - .keeloq.serial = key & 0x00FFFFFF, - .keeloq.btn = 0x02, - .keeloq.cnt = 0x03, - .keeloq.manuf = "Gibidi"}; + gen_info = (GenInfo){.type = GenKeeloq, + .mod = "AM650", + .freq = 433920000, + .keeloq.serial = key & 0x00FFFFFF, + .keeloq.btn = 0x02, + .keeloq.cnt = 0x03, + .keeloq.manuf = "Gibidi"}; break; case SetTypeGSN: - gen_info = (GenInfo){ - .type = GenKeeloq, - .mod = "AM650", - .freq = 433920000, - .keeloq.serial = key & 0x0FFFFFFF, - .keeloq.btn = 0x02, - .keeloq.cnt = 0x03, - .keeloq.manuf = "GSN"}; + gen_info = (GenInfo){.type = GenKeeloq, + .mod = "AM650", + .freq = 433920000, + .keeloq.serial = key & 0x0FFFFFFF, + .keeloq.btn = 0x02, + .keeloq.cnt = 0x03, + .keeloq.manuf = "GSN"}; break; case SetTypeIronLogic: - gen_info = (GenInfo){ - .type = GenKeeloq, - .mod = "AM650", - .freq = 433920000, - .keeloq.serial = key & 0x00FFFFF0, - .keeloq.btn = 0x04, - .keeloq.cnt = 0x05, - .keeloq.manuf = "IronLogic"}; + gen_info = (GenInfo){.type = GenKeeloq, + .mod = "AM650", + .freq = 433920000, + .keeloq.serial = key & 0x00FFFFF0, + .keeloq.btn = 0x04, + .keeloq.cnt = 0x05, + .keeloq.manuf = "IronLogic"}; break; case SetTypeStilmatic: - gen_info = (GenInfo){ - .type = GenKeeloq, - .mod = "AM650", - .freq = 433920000, - .keeloq.serial = key & 0x0FFFFFFF, - .keeloq.btn = 0x01, - .keeloq.cnt = 0x03, - .keeloq.manuf = "Stilmatic"}; + gen_info = (GenInfo){.type = GenKeeloq, + .mod = "AM650", + .freq = 433920000, + .keeloq.serial = key & 0x0FFFFFFF, + .keeloq.btn = 0x01, + .keeloq.cnt = 0x03, + .keeloq.manuf = "Stilmatic"}; break; case SetTypeSommer_FM_434: - gen_info = (GenInfo){ - .type = GenKeeloq, - .mod = "FM476", - .freq = 434420000, - .keeloq.serial = (key & 0x0000FFFF) | 0x01700000, - .keeloq.btn = 0x02, - .keeloq.cnt = 0x03, - .keeloq.manuf = "Sommer(fsk476)"}; + gen_info = (GenInfo){.type = GenKeeloq, + .mod = "FM476", + .freq = 434420000, + .keeloq.serial = (key & 0x0000FFFF) | 0x01700000, + .keeloq.btn = 0x02, + .keeloq.cnt = 0x03, + .keeloq.manuf = "Sommer(fsk476)"}; break; case SetTypeSommer_FM_868: - gen_info = (GenInfo){ - .type = GenKeeloq, - .mod = "FM476", - .freq = 868800000, - .keeloq.serial = (key & 0x0000FFFF) | 0x01700000, - .keeloq.btn = 0x02, - .keeloq.cnt = 0x03, - .keeloq.manuf = "Sommer(fsk476)"}; + gen_info = (GenInfo){.type = GenKeeloq, + .mod = "FM476", + .freq = 868800000, + .keeloq.serial = (key & 0x0000FFFF) | 0x01700000, + .keeloq.btn = 0x02, + .keeloq.cnt = 0x03, + .keeloq.manuf = "Sommer(fsk476)"}; break; case SetTypeSommer_FM238_434: - gen_info = (GenInfo){ - .type = GenKeeloq, - .mod = "FM238", - .freq = 434420000, - .keeloq.serial = key & 0x0000FFFF, - .keeloq.btn = 0x02, - .keeloq.cnt = 0x03, - .keeloq.manuf = "Sommer(fsk476)"}; + gen_info = (GenInfo){.type = GenKeeloq, + .mod = "FM238", + .freq = 434420000, + .keeloq.serial = key & 0x0000FFFF, + .keeloq.btn = 0x02, + .keeloq.cnt = 0x03, + .keeloq.manuf = "Sommer(fsk476)"}; break; case SetTypeSommer_FM238_868: - gen_info = (GenInfo){ - .type = GenKeeloq, - .mod = "FM238", - .freq = 868800000, - .keeloq.serial = key & 0x0000FFFF, - .keeloq.btn = 0x02, - .keeloq.cnt = 0x03, - .keeloq.manuf = "Sommer(fsk476)"}; + gen_info = (GenInfo){.type = GenKeeloq, + .mod = "FM238", + .freq = 868800000, + .keeloq.serial = key & 0x0000FFFF, + .keeloq.btn = 0x02, + .keeloq.cnt = 0x03, + .keeloq.manuf = "Sommer(fsk476)"}; break; case SetTypeDTMNeo433: - gen_info = (GenInfo){ - .type = GenKeeloq, - .mod = "AM650", - .freq = 433920000, - .keeloq.serial = key & 0x000FFFFF, - .keeloq.btn = 0x02, - .keeloq.cnt = 0x05, - .keeloq.manuf = "DTM_Neo"}; + gen_info = (GenInfo){.type = GenKeeloq, + .mod = "AM650", + .freq = 433920000, + .keeloq.serial = key & 0x000FFFFF, + .keeloq.btn = 0x02, + .keeloq.cnt = 0x05, + .keeloq.manuf = "DTM_Neo"}; break; case SetTypeCAMESpace: - gen_info = (GenInfo){ - .type = GenKeeloq, - .mod = "AM650", - .freq = 433920000, - .keeloq.serial = key & 0x00FFFFFF, - .keeloq.btn = 0x04, - .keeloq.cnt = 0x03, - .keeloq.manuf = "Came_Space"}; + gen_info = (GenInfo){.type = GenKeeloq, + .mod = "AM650", + .freq = 433920000, + .keeloq.serial = key & 0x00FFFFFF, + .keeloq.btn = 0x04, + .keeloq.cnt = 0x03, + .keeloq.manuf = "Came_Space"}; break; case SetTypeCameAtomo433: - gen_info = (GenInfo){ - .type = GenCameAtomo, - .mod = "AM650", - .freq = 433920000, - .keeloq.serial = (key & 0x0FFFFFFF) | 0x10000000, - .keeloq.cnt = 0x03}; + gen_info = (GenInfo){.type = GenCameAtomo, + .mod = "AM650", + .freq = 433920000, + .keeloq.serial = (key & 0x0FFFFFFF) | 0x10000000, + .keeloq.cnt = 0x03}; break; case SetTypeCameAtomo868: - gen_info = (GenInfo){ - .type = GenCameAtomo, - .mod = "AM650", - .freq = 868350000, - .keeloq.serial = (key & 0x0FFFFFFF) | 0x10000000, - .keeloq.cnt = 0x03}; + gen_info = (GenInfo){.type = GenCameAtomo, + .mod = "AM650", + .freq = 868350000, + .keeloq.serial = (key & 0x0FFFFFFF) | 0x10000000, + .keeloq.cnt = 0x03}; break; case SetTypeBFTMitto: - gen_info = (GenInfo){ - .type = GenKeeloqBFT, - .mod = "AM650", - .freq = 433920000, - .keeloq_bft.serial = key & 0x000FFFFF, - .keeloq_bft.btn = 0x02, - .keeloq_bft.cnt = 0x02, - .keeloq_bft.seed = key & 0x000FFFFF, - .keeloq_bft.manuf = "BFT"}; + gen_info = (GenInfo){.type = GenKeeloqBFT, + .mod = "AM650", + .freq = 433920000, + .keeloq_bft.serial = key & 0x000FFFFF, + .keeloq_bft.btn = 0x02, + .keeloq_bft.cnt = 0x02, + .keeloq_bft.seed = key & 0x000FFFFF, + .keeloq_bft.manuf = "BFT"}; break; case SetTypeAlutechAT4N: - gen_info = (GenInfo){ - .type = GenAlutechAt4n, - .mod = "AM650", - .freq = 433920000, - .alutech_at_4n.serial = (key & 0x000FFFFF) | 0x00100000, - .alutech_at_4n.btn = 0x44, - .alutech_at_4n.cnt = 0x03}; + gen_info = (GenInfo){.type = GenAlutechAt4n, + .mod = "AM650", + .freq = 433920000, + .alutech_at_4n.serial = (key & 0x000FFFFF) | 0x00100000, + .alutech_at_4n.btn = 0x44, + .alutech_at_4n.cnt = 0x03}; break; case SetTypeSomfyTelis: - gen_info = (GenInfo){ - .type = GenSomfyTelis, - .mod = "AM650", - .freq = 433420000, - .somfy_telis.serial = key & 0x00FFFFFF, - .somfy_telis.btn = 0x02, - .somfy_telis.cnt = 0x03}; + gen_info = (GenInfo){.type = GenSomfyTelis, + .mod = "AM650", + .freq = 433420000, + .somfy_telis.serial = key & 0x00FFFFFF, + .somfy_telis.btn = 0x02, + .somfy_telis.cnt = 0x03}; break; case SetTypeDoorHan_433_92: - gen_info = (GenInfo){ - .type = GenKeeloq, - .mod = "AM650", - .freq = 433920000, - .keeloq.serial = key & 0x0FFFFFFF, - .keeloq.btn = 0x02, - .keeloq.cnt = 0x03, - .keeloq.manuf = "DoorHan"}; + gen_info = (GenInfo){.type = GenKeeloq, + .mod = "AM650", + .freq = 433920000, + .keeloq.serial = key & 0x0FFFFFFF, + .keeloq.btn = 0x02, + .keeloq.cnt = 0x03, + .keeloq.manuf = "DoorHan"}; break; case SetTypeDoorHan_315_00: - gen_info = (GenInfo){ - .type = GenKeeloq, - .mod = "AM650", - .freq = 315000000, - .keeloq.serial = key & 0x0FFFFFFF, - .keeloq.btn = 0x02, - .keeloq.cnt = 0x03, - .keeloq.manuf = "DoorHan"}; + gen_info = (GenInfo){.type = GenKeeloq, + .mod = "AM650", + .freq = 315000000, + .keeloq.serial = key & 0x0FFFFFFF, + .keeloq.btn = 0x02, + .keeloq.cnt = 0x03, + .keeloq.manuf = "DoorHan"}; break; case SetTypeNiceFlorS_433_92: - gen_info = (GenInfo){ - .type = GenNiceFlorS, - .mod = "AM650", - .freq = 433920000, - .nice_flor_s.serial = key & 0x0FFFFFFF, - .nice_flor_s.btn = 0x01, - .nice_flor_s.cnt = 0x03, - .nice_flor_s.nice_one = false}; + gen_info = (GenInfo){.type = GenNiceFlorS, + .mod = "AM650", + .freq = 433920000, + .nice_flor_s.serial = key & 0x0FFFFFFF, + .nice_flor_s.btn = 0x01, + .nice_flor_s.cnt = 0x03, + .nice_flor_s.nice_one = false}; break; case SetTypeNiceOne_433_92: - gen_info = (GenInfo){ - .type = GenNiceFlorS, - .mod = "AM650", - .freq = 433920000, - .nice_flor_s.serial = key & 0x0FFFFFFF, - .nice_flor_s.btn = 0x01, - .nice_flor_s.cnt = 0x03, - .nice_flor_s.nice_one = true}; + gen_info = (GenInfo){.type = GenNiceFlorS, + .mod = "AM650", + .freq = 433920000, + .nice_flor_s.serial = key & 0x0FFFFFFF, + .nice_flor_s.btn = 0x01, + .nice_flor_s.cnt = 0x03, + .nice_flor_s.nice_one = true}; break; case SetTypeNiceSmilo_433_92: - gen_info = (GenInfo){ - .type = GenKeeloq, - .mod = "AM650", - .freq = 433920000, - .keeloq.serial = key & 0x00FFFFFF, - .keeloq.btn = 0x02, - .keeloq.cnt = 0x03, - .keeloq.manuf = "NICE_Smilo"}; + gen_info = (GenInfo){.type = GenKeeloq, + .mod = "AM650", + .freq = 433920000, + .keeloq.serial = key & 0x00FFFFFF, + .keeloq.btn = 0x02, + .keeloq.cnt = 0x03, + .keeloq.manuf = "NICE_Smilo"}; break; case SetTypeNiceMHouse_433_92: - gen_info = (GenInfo){ - .type = GenKeeloq, - .mod = "AM650", - .freq = 433920000, - .keeloq.serial = key & 0x00FFFFFF, - .keeloq.btn = 0x09, - .keeloq.cnt = 0x03, - .keeloq.manuf = "NICE_MHOUSE"}; + gen_info = (GenInfo){.type = GenKeeloq, + .mod = "AM650", + .freq = 433920000, + .keeloq.serial = key & 0x00FFFFFF, + .keeloq.btn = 0x09, + .keeloq.cnt = 0x03, + .keeloq.manuf = "NICE_MHOUSE"}; break; case SetTypeDeaMio433: - gen_info = (GenInfo){ - .type = GenKeeloq, - .mod = "AM650", - .freq = 433920000, - .keeloq.serial = (key & 0x0FFFF000) | 0x00000869, - .keeloq.btn = 0x02, - .keeloq.cnt = 0x03, - .keeloq.manuf = "Dea_Mio"}; + gen_info = (GenInfo){.type = GenKeeloq, + .mod = "AM650", + .freq = 433920000, + .keeloq.serial = (key & 0x0FFFF000) | 0x00000869, + .keeloq.btn = 0x02, + .keeloq.cnt = 0x03, + .keeloq.manuf = "Dea_Mio"}; break; case SetTypeGeniusBravo433: - gen_info = (GenInfo){ - .type = GenKeeloq, - .mod = "AM650", - .freq = 433920000, - .keeloq.serial = key & 0x00FFFFFF, - .keeloq.btn = 0x06, - .keeloq.cnt = 0x03, - .keeloq.manuf = "Genius_Bravo"}; + gen_info = (GenInfo){.type = GenKeeloq, + .mod = "AM650", + .freq = 433920000, + .keeloq.serial = key & 0x00FFFFFF, + .keeloq.btn = 0x06, + .keeloq.cnt = 0x03, + .keeloq.manuf = "Genius_Bravo"}; break; case SetTypeJCM_433_92: - gen_info = (GenInfo){ - .type = GenKeeloq, - .mod = "AM650", - .freq = 433920000, - .keeloq.serial = key & 0x00FFFFFF, - .keeloq.btn = 0x02, - .keeloq.cnt = 0x03, - .keeloq.manuf = "JCM_Tech"}; + gen_info = (GenInfo){.type = GenKeeloq, + .mod = "AM650", + .freq = 433920000, + .keeloq.serial = key & 0x00FFFFFF, + .keeloq.btn = 0x02, + .keeloq.cnt = 0x03, + .keeloq.manuf = "JCM_Tech"}; break; case SetTypeNovoferm_433_92: - gen_info = (GenInfo){ - .type = GenKeeloq, - .mod = "AM650", - .freq = 433920000, - .keeloq.serial = (key & 0x0000FFFF) | 0x018F0000, - .keeloq.btn = 0x01, - .keeloq.cnt = 0x03, - .keeloq.manuf = "Novoferm"}; + gen_info = (GenInfo){.type = GenKeeloq, + .mod = "AM650", + .freq = 433920000, + .keeloq.serial = (key & 0x0000FFFF) | 0x018F0000, + .keeloq.btn = 0x01, + .keeloq.cnt = 0x03, + .keeloq.manuf = "Novoferm"}; break; case SetTypeHormannEcoStar_433_92: - gen_info = (GenInfo){ - .type = GenKeeloq, - .mod = "AM650", - .freq = 433920000, - .keeloq.serial = (key & 0x000FFFFF) | 0x02200000, - .keeloq.btn = 0x04, - .keeloq.cnt = 0x03, - .keeloq.manuf = "EcoStar"}; + gen_info = (GenInfo){.type = GenKeeloq, + .mod = "AM650", + .freq = 433920000, + .keeloq.serial = (key & 0x000FFFFF) | 0x02200000, + .keeloq.btn = 0x04, + .keeloq.cnt = 0x03, + .keeloq.manuf = "EcoStar"}; break; case SetTypeFAACRCXT_433_92: - gen_info = (GenInfo){ - .type = GenKeeloq, - .mod = "AM650", - .freq = 433920000, - .keeloq.serial = (key & 0x0000FFFF) | 0x00100000, - .keeloq.btn = 0x02, - .keeloq.cnt = 0x03, - .keeloq.manuf = "FAAC_RC,XT"}; + gen_info = (GenInfo){.type = GenKeeloq, + .mod = "AM650", + .freq = 433920000, + .keeloq.serial = (key & 0x0000FFFF) | 0x00100000, + .keeloq.btn = 0x02, + .keeloq.cnt = 0x03, + .keeloq.manuf = "FAAC_RC,XT"}; break; case SetTypeFAACRCXT_868: - gen_info = (GenInfo){ - .type = GenKeeloq, - .mod = "AM650", - .freq = 868350000, - .keeloq.serial = (key & 0x0000FFFF) | 0x00100000, - .keeloq.btn = 0x02, - .keeloq.cnt = 0x03, - .keeloq.manuf = "FAAC_RC,XT"}; + gen_info = (GenInfo){.type = GenKeeloq, + .mod = "AM650", + .freq = 868350000, + .keeloq.serial = (key & 0x0000FFFF) | 0x00100000, + .keeloq.btn = 0x02, + .keeloq.cnt = 0x03, + .keeloq.manuf = "FAAC_RC,XT"}; break; case SetTypeNormstahl_433_92: - gen_info = (GenInfo){ - .type = GenKeeloq, - .mod = "AM650", - .freq = 433920000, - .keeloq.serial = key & 0x0000FFFF, - .keeloq.btn = 0x04, - .keeloq.cnt = 0x03, - .keeloq.manuf = "Normstahl"}; + gen_info = (GenInfo){.type = GenKeeloq, + .mod = "AM650", + .freq = 433920000, + .keeloq.serial = key & 0x0000FFFF, + .keeloq.btn = 0x04, + .keeloq.cnt = 0x03, + .keeloq.manuf = "Normstahl"}; break; case SetTypeHCS101_433_92: - gen_info = (GenInfo){ - .type = GenKeeloq, - .mod = "AM650", - .freq = 433920000, - .keeloq.serial = key & 0x000FFFFF, - .keeloq.btn = 0x02, - .keeloq.cnt = 0x03, - .keeloq.manuf = "HCS101"}; + gen_info = (GenInfo){.type = GenKeeloq, + .mod = "AM650", + .freq = 433920000, + .keeloq.serial = key & 0x000FFFFF, + .keeloq.btn = 0x02, + .keeloq.cnt = 0x03, + .keeloq.manuf = "HCS101"}; break; case SetTypeSecPlus_v1_315_00: gen_info = (GenInfo){.type = GenSecPlus1, .mod = "AM650", .freq = 315000000}; @@ -774,40 +733,36 @@ bool subghz_scene_set_type_on_event(void* context, SceneManagerEvent event) { gen_info = (GenInfo){.type = GenSecPlus1, .mod = "AM650", .freq = 433920000}; break; case SetTypeSecPlus_v2_310_00: - gen_info = (GenInfo){ - .type = GenSecPlus2, - .mod = "AM650", - .freq = 310000000, - .sec_plus_2.serial = (key & 0x7FFFF3FC), // 850LM pairing - .sec_plus_2.btn = 0x68, - .sec_plus_2.cnt = 0xE500000}; + gen_info = (GenInfo){.type = GenSecPlus2, + .mod = "AM650", + .freq = 310000000, + .sec_plus_2.serial = (key & 0x7FFFF3FC), // 850LM pairing + .sec_plus_2.btn = 0x68, + .sec_plus_2.cnt = 0xE500000}; break; case SetTypeSecPlus_v2_315_00: - gen_info = (GenInfo){ - .type = GenSecPlus2, - .mod = "AM650", - .freq = 315000000, - .sec_plus_2.serial = (key & 0x7FFFF3FC), // 850LM pairing - .sec_plus_2.btn = 0x68, - .sec_plus_2.cnt = 0xE500000}; + gen_info = (GenInfo){.type = GenSecPlus2, + .mod = "AM650", + .freq = 315000000, + .sec_plus_2.serial = (key & 0x7FFFF3FC), // 850LM pairing + .sec_plus_2.btn = 0x68, + .sec_plus_2.cnt = 0xE500000}; break; case SetTypeSecPlus_v2_390_00: - gen_info = (GenInfo){ - .type = GenSecPlus2, - .mod = "AM650", - .freq = 390000000, - .sec_plus_2.serial = (key & 0x7FFFF3FC), // 850LM pairing - .sec_plus_2.btn = 0x68, - .sec_plus_2.cnt = 0xE500000}; + gen_info = (GenInfo){.type = GenSecPlus2, + .mod = "AM650", + .freq = 390000000, + .sec_plus_2.serial = (key & 0x7FFFF3FC), // 850LM pairing + .sec_plus_2.btn = 0x68, + .sec_plus_2.cnt = 0xE500000}; break; case SetTypeSecPlus_v2_433_00: - gen_info = (GenInfo){ - .type = GenSecPlus2, - .mod = "AM650", - .freq = 433920000, - .sec_plus_2.serial = (key & 0x7FFFF3FC), // 850LM pairing - .sec_plus_2.btn = 0x68, - .sec_plus_2.cnt = 0xE500000}; + gen_info = (GenInfo){.type = GenSecPlus2, + .mod = "AM650", + .freq = 433920000, + .sec_plus_2.serial = (key & 0x7FFFF3FC), // 850LM pairing + .sec_plus_2.btn = 0x68, + .sec_plus_2.cnt = 0xE500000}; break; default: furi_crash("Not implemented"); From 9fe149960e58845d00aee67356945b6319e0f067 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Mon, 24 Feb 2025 11:14:43 +0300 Subject: [PATCH 090/268] upd changelog --- CHANGELOG.md | 2 ++ ReadMe.md | 1 + 2 files changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 37e8a3c6c..4d6de6f7c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ ## Main changes - Current API: 82.0 +* SubGHz: Add ReversRB2 / RB2M Protocol (static 64 bit) full support with add manually (by @xMasterX) +* SubGHz: Fix Hollarm protocol with more verification * SubGHz: Fix GangQi protocol (by @DoberBit and @mishamyte (who spent 2 weeks on this)) * SubGHz: Came Atomo button hold simulation with full cycle simulation (to allow proper pairing with receiver) * OFW: LFRFID - **EM4305 support** diff --git a/ReadMe.md b/ReadMe.md index 945e46167..1ead8d451 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -165,6 +165,7 @@ Thanks to Official team (to their SubGHz Developer, Skorp) for implementing supp Decoders/Encoders or emulation (+ programming mode) support made by @xMasterX
+- ReversRB2 / RB2M (static 64 bit) with add manually support - Marantec24 (static 24 bit) with add manually support - GangQi (static 34 bit) with button parsing and add manually support (thanks to @mishamyte for captures and testing, thanks @Skorpionm for help) - Hollarm (static 42 bit) with button parsing and add manually support (thanks to @mishamyte for captures, thanks @Skorpionm for help) From 248341ac0a202a62e6312daba348659585265918 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Mon, 24 Feb 2025 20:57:43 +0300 Subject: [PATCH 091/268] merge ofw pr 4125 by zinongli --- lib/lfrfid/protocols/protocol_securakey.c | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/lib/lfrfid/protocols/protocol_securakey.c b/lib/lfrfid/protocols/protocol_securakey.c index a4bfeca60..947b68e72 100644 --- a/lib/lfrfid/protocols/protocol_securakey.c +++ b/lib/lfrfid/protocols/protocol_securakey.c @@ -61,17 +61,22 @@ uint8_t* protocol_securakey_get_data(ProtocolSecurakey* protocol) { static bool protocol_securakey_can_be_decoded(ProtocolSecurakey* protocol) { // check 19 bits preamble + format flag if(bit_lib_get_bits_32(protocol->RKKT_encoded_data, 0, 19) == 0b0111111111000000000) { - protocol->bit_format = 0; - return true; + if(bit_lib_test_parity(protocol->RKKT_encoded_data, 2, 54, BitLibParityAlways0, 9)) { + protocol->bit_format = 0; + return true; + } } else if(bit_lib_get_bits_32(protocol->RKKT_encoded_data, 0, 19) == 0b0111111111001011010) { - protocol->bit_format = 26; - return true; + if(bit_lib_test_parity(protocol->RKKT_encoded_data, 2, 90, BitLibParityAlways0, 9)) { + protocol->bit_format = 26; + return true; + } } else if(bit_lib_get_bits_32(protocol->RKKT_encoded_data, 0, 19) == 0b0111111111001100000) { - protocol->bit_format = 32; - return true; - } else { - return false; + if(bit_lib_test_parity(protocol->RKKT_encoded_data, 2, 90, BitLibParityAlways0, 9)) { + protocol->bit_format = 32; + return true; + } } + return false; } static void protocol_securakey_decode(ProtocolSecurakey* protocol) { From fc96bf2a2e28f5749bf5310a428ce52b85ef9761 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Mon, 24 Feb 2025 21:21:19 +0300 Subject: [PATCH 092/268] upd changelog --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d6de6f7c..f2bc5b0c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,5 @@ ## Main changes -- Current API: 82.0 +- Current API: 83.0 * SubGHz: Add ReversRB2 / RB2M Protocol (static 64 bit) full support with add manually (by @xMasterX) * SubGHz: Fix Hollarm protocol with more verification * SubGHz: Fix GangQi protocol (by @DoberBit and @mishamyte (who spent 2 weeks on this)) @@ -16,6 +16,8 @@ * Apps: **Check out more Apps updates and fixes by following** [this link](https://github.com/xMasterX/all-the-plugins/commits/dev) ## Other changes * Anims: Disable winter anims +* OFW PR 4126: Stricter constness for const data (by @hedger) +* OFW PR 4125: LFRFID: Fix Detection Conflict Between Securakey and Noralsy Format (by @zinongli) * OFW: Stdio API improvements * OFW: GUI: Widget view extra options for JS * OFW: Update heap implementation From 5b2582930f78b288591b741a50c244956c13b60c Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Mon, 24 Feb 2025 21:40:21 +0300 Subject: [PATCH 093/268] classic poller fix early key reuse in dictionary attack state machine by noproto --- lib/nfc/protocols/mf_classic/mf_classic_poller.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/nfc/protocols/mf_classic/mf_classic_poller.c b/lib/nfc/protocols/mf_classic/mf_classic_poller.c index ec37c8015..b2d9b114a 100644 --- a/lib/nfc/protocols/mf_classic/mf_classic_poller.c +++ b/lib/nfc/protocols/mf_classic/mf_classic_poller.c @@ -1921,7 +1921,8 @@ NfcCommand mf_classic_poller_handler_nested_controller(MfClassicPoller* instance sizeof(MfClassicKey)) : NULL; } - if((is_weak || is_last_iter_for_hard_key) && dict_attack_ctx->nested_nonce.count > 0) { + if((is_weak && (dict_attack_ctx->nested_nonce.count == 1)) || + (is_last_iter_for_hard_key && (dict_attack_ctx->nested_nonce.count == 8))) { // Key verify and reuse dict_attack_ctx->nested_phase = MfClassicNestedPhaseDictAttackVerify; dict_attack_ctx->auth_passed = false; From 8505aab0c35fadfa57424648288bc61da2764666 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Mon, 24 Feb 2025 21:41:04 +0300 Subject: [PATCH 094/268] upd changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f2bc5b0c0..7fc65e191 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ * Apps: **Check out more Apps updates and fixes by following** [this link](https://github.com/xMasterX/all-the-plugins/commits/dev) ## Other changes * Anims: Disable winter anims +* NFC: mfclassic poller fix early key reuse in dictionary attack state machine (by @noproto) * OFW PR 4126: Stricter constness for const data (by @hedger) * OFW PR 4125: LFRFID: Fix Detection Conflict Between Securakey and Noralsy Format (by @zinongli) * OFW: Stdio API improvements From 9f5e93bed80366d2c769dda966a2255613639add Mon Sep 17 00:00:00 2001 From: Zinong Li <131403964+zinongli@users.noreply.github.com> Date: Mon, 24 Feb 2025 14:59:05 -0500 Subject: [PATCH 095/268] LFRFID: Fix Detection Conflict Between Securakey and Noralsy Format (#4125) * Securakey added parity check * Format Sources Co-authored-by: Aleksandr Kutuzov --- lib/lfrfid/protocols/protocol_securakey.c | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/lib/lfrfid/protocols/protocol_securakey.c b/lib/lfrfid/protocols/protocol_securakey.c index a4bfeca60..947b68e72 100644 --- a/lib/lfrfid/protocols/protocol_securakey.c +++ b/lib/lfrfid/protocols/protocol_securakey.c @@ -61,17 +61,22 @@ uint8_t* protocol_securakey_get_data(ProtocolSecurakey* protocol) { static bool protocol_securakey_can_be_decoded(ProtocolSecurakey* protocol) { // check 19 bits preamble + format flag if(bit_lib_get_bits_32(protocol->RKKT_encoded_data, 0, 19) == 0b0111111111000000000) { - protocol->bit_format = 0; - return true; + if(bit_lib_test_parity(protocol->RKKT_encoded_data, 2, 54, BitLibParityAlways0, 9)) { + protocol->bit_format = 0; + return true; + } } else if(bit_lib_get_bits_32(protocol->RKKT_encoded_data, 0, 19) == 0b0111111111001011010) { - protocol->bit_format = 26; - return true; + if(bit_lib_test_parity(protocol->RKKT_encoded_data, 2, 90, BitLibParityAlways0, 9)) { + protocol->bit_format = 26; + return true; + } } else if(bit_lib_get_bits_32(protocol->RKKT_encoded_data, 0, 19) == 0b0111111111001100000) { - protocol->bit_format = 32; - return true; - } else { - return false; + if(bit_lib_test_parity(protocol->RKKT_encoded_data, 2, 90, BitLibParityAlways0, 9)) { + protocol->bit_format = 32; + return true; + } } + return false; } static void protocol_securakey_decode(ProtocolSecurakey* protocol) { From d783204d20f0b1a805c62df9d4330884d3acfdd8 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Tue, 25 Feb 2025 00:02:23 +0300 Subject: [PATCH 096/268] upd changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7fc65e191..59ecdab2b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,7 +18,7 @@ * Anims: Disable winter anims * NFC: mfclassic poller fix early key reuse in dictionary attack state machine (by @noproto) * OFW PR 4126: Stricter constness for const data (by @hedger) -* OFW PR 4125: LFRFID: Fix Detection Conflict Between Securakey and Noralsy Format (by @zinongli) +* OFW: LFRFID: Fix Detection Conflict Between Securakey and Noralsy Format (by @zinongli) * OFW: Stdio API improvements * OFW: GUI: Widget view extra options for JS * OFW: Update heap implementation From 145184f0f20f8bea0bdd57339205c8701442d262 Mon Sep 17 00:00:00 2001 From: Zinong Li <131403964+zinongli@users.noreply.github.com> Date: Mon, 24 Feb 2025 16:07:45 -0500 Subject: [PATCH 097/268] NFC: FeliCa Protocol Expose Read Block API and Allow Specifying Service (#4074) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add one parameter to the rdbl and expose * Bump api version and format sources Co-authored-by: あく --- lib/nfc/protocols/felica/felica_poller.c | 8 +++++--- lib/nfc/protocols/felica/felica_poller.h | 17 +++++++++++++++++ lib/nfc/protocols/felica/felica_poller_i.c | 3 ++- lib/nfc/protocols/felica/felica_poller_i.h | 15 --------------- targets/f18/api_symbols.csv | 2 +- targets/f7/api_symbols.csv | 3 ++- 6 files changed, 27 insertions(+), 21 deletions(-) diff --git a/lib/nfc/protocols/felica/felica_poller.c b/lib/nfc/protocols/felica/felica_poller.c index 720daf238..cf00cbc09 100644 --- a/lib/nfc/protocols/felica/felica_poller.c +++ b/lib/nfc/protocols/felica/felica_poller.c @@ -118,7 +118,8 @@ NfcCommand felica_poller_state_handler_auth_internal(FelicaPoller* instance) { blocks[1] = FELICA_BLOCK_INDEX_WCNT; blocks[2] = FELICA_BLOCK_INDEX_MAC_A; FelicaPollerReadCommandResponse* rx_resp; - error = felica_poller_read_blocks(instance, sizeof(blocks), blocks, &rx_resp); + error = felica_poller_read_blocks( + instance, sizeof(blocks), blocks, FELICA_SERVICE_RO_ACCESS, &rx_resp); if(error != FelicaErrorNone || rx_resp->SF1 != 0 || rx_resp->SF2 != 0) break; if(felica_check_mac( @@ -174,7 +175,7 @@ NfcCommand felica_poller_state_handler_auth_external(FelicaPoller* instance) { if(error != FelicaErrorNone || tx_resp->SF1 != 0 || tx_resp->SF2 != 0) break; FelicaPollerReadCommandResponse* rx_resp; - error = felica_poller_read_blocks(instance, 1, blocks, &rx_resp); + error = felica_poller_read_blocks(instance, 1, blocks, FELICA_SERVICE_RO_ACCESS, &rx_resp); if(error != FelicaErrorNone || rx_resp->SF1 != 0 || rx_resp->SF2 != 0) break; instance->data->data.fs.state.SF1 = 0; @@ -203,7 +204,8 @@ NfcCommand felica_poller_state_handler_read_blocks(FelicaPoller* instance) { } FelicaPollerReadCommandResponse* response; - FelicaError error = felica_poller_read_blocks(instance, block_count, block_list, &response); + FelicaError error = felica_poller_read_blocks( + instance, block_count, block_list, FELICA_SERVICE_RO_ACCESS, &response); if(error == FelicaErrorNone) { block_count = (response->SF1 == 0) ? response->block_count : block_count; uint8_t* data_ptr = diff --git a/lib/nfc/protocols/felica/felica_poller.h b/lib/nfc/protocols/felica/felica_poller.h index b386f4b4b..541442df2 100644 --- a/lib/nfc/protocols/felica/felica_poller.h +++ b/lib/nfc/protocols/felica/felica_poller.h @@ -56,6 +56,23 @@ typedef struct { */ FelicaError felica_poller_activate(FelicaPoller* instance, FelicaData* data); +/** + * @brief Performs felica read operation for blocks provided as parameters + * + * @param[in, out] instance pointer to the instance to be used in the transaction. + * @param[in] block_count Amount of blocks involved in reading procedure + * @param[in] block_numbers Array with block indexes according to felica docs + * @param[in] service_code Service code for the read operation + * @param[out] response_ptr Pointer to the response structure + * @return FelicaErrorNone on success, an error code on failure. +*/ +FelicaError felica_poller_read_blocks( + FelicaPoller* instance, + const uint8_t block_count, + const uint8_t* const block_numbers, + uint16_t service_code, + FelicaPollerReadCommandResponse** const response_ptr); + #ifdef __cplusplus } #endif diff --git a/lib/nfc/protocols/felica/felica_poller_i.c b/lib/nfc/protocols/felica/felica_poller_i.c index 8ec4b2889..49112debd 100644 --- a/lib/nfc/protocols/felica/felica_poller_i.c +++ b/lib/nfc/protocols/felica/felica_poller_i.c @@ -134,6 +134,7 @@ FelicaError felica_poller_read_blocks( FelicaPoller* instance, const uint8_t block_count, const uint8_t* const block_numbers, + uint16_t service_code, FelicaPollerReadCommandResponse** const response_ptr) { furi_assert(instance); furi_assert(block_count <= 4); @@ -143,7 +144,7 @@ FelicaError felica_poller_read_blocks( felica_poller_prepare_tx_buffer( instance, FELICA_CMD_READ_WITHOUT_ENCRYPTION, - FELICA_SERVICE_RO_ACCESS, + service_code, block_count, block_numbers, 0, diff --git a/lib/nfc/protocols/felica/felica_poller_i.h b/lib/nfc/protocols/felica/felica_poller_i.h index df205ba67..df4990a4e 100644 --- a/lib/nfc/protocols/felica/felica_poller_i.h +++ b/lib/nfc/protocols/felica/felica_poller_i.h @@ -70,21 +70,6 @@ FelicaError felica_poller_polling( const FelicaPollerPollingCommand* cmd, FelicaPollerPollingResponse* resp); -/** - * @brief Performs felica read operation for blocks provided as parameters - * - * @param[in, out] instance pointer to the instance to be used in the transaction. - * @param[in] block_count Amount of blocks involved in reading procedure - * @param[in] block_numbers Array with block indexes according to felica docs - * @param[out] response_ptr Pointer to the response structure - * @return FelicaErrorNone on success, an error code on failure. -*/ -FelicaError felica_poller_read_blocks( - FelicaPoller* instance, - const uint8_t block_count, - const uint8_t* const block_numbers, - FelicaPollerReadCommandResponse** const response_ptr); - /** * @brief Performs felica write operation with data provided as parameters * diff --git a/targets/f18/api_symbols.csv b/targets/f18/api_symbols.csv index 3da638149..2074fa131 100644 --- a/targets/f18/api_symbols.csv +++ b/targets/f18/api_symbols.csv @@ -1,5 +1,5 @@ entry,status,name,type,params -Version,+,82.0,, +Version,+,82.1,, Header,+,applications/services/bt/bt_service/bt.h,, Header,+,applications/services/bt/bt_service/bt_keys_storage.h,, Header,+,applications/services/cli/cli.h,, diff --git a/targets/f7/api_symbols.csv b/targets/f7/api_symbols.csv index 9a87f08d4..1168a6eea 100644 --- a/targets/f7/api_symbols.csv +++ b/targets/f7/api_symbols.csv @@ -1,5 +1,5 @@ entry,status,name,type,params -Version,+,82.0,, +Version,+,82.1,, Header,+,applications/drivers/subghz/cc1101_ext/cc1101_ext_interconnect.h,, Header,+,applications/services/bt/bt_service/bt.h,, Header,+,applications/services/bt/bt_service/bt_keys_storage.h,, @@ -1049,6 +1049,7 @@ Function,+,felica_get_uid,const uint8_t*,"const FelicaData*, size_t*" Function,+,felica_is_equal,_Bool,"const FelicaData*, const FelicaData*" Function,+,felica_load,_Bool,"FelicaData*, FlipperFormat*, uint32_t" Function,+,felica_poller_activate,FelicaError,"FelicaPoller*, FelicaData*" +Function,+,felica_poller_read_blocks,FelicaError,"FelicaPoller*, const uint8_t, const uint8_t*, uint16_t, FelicaPollerReadCommandResponse**" Function,+,felica_poller_sync_read,FelicaError,"Nfc*, FelicaData*, const FelicaCardKey*" Function,+,felica_reset,void,FelicaData* Function,+,felica_save,_Bool,"const FelicaData*, FlipperFormat*" From 0d53cb2c48380dff521e9fcbe157524111bbac57 Mon Sep 17 00:00:00 2001 From: Evgeny E <10674163+ssecsd@users.noreply.github.com> Date: Mon, 24 Feb 2025 22:00:51 +0000 Subject: [PATCH 098/268] test: fix timeout for flashing step (#4127) --- .github/workflows/unit_tests.yml | 2 +- .github/workflows/updater_test.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index 309dd7ebd..37df8e099 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -21,7 +21,7 @@ jobs: - name: 'Flash unit tests firmware' id: flashing if: success() - timeout-minutes: 20 + timeout-minutes: 10 run: | source scripts/toolchain/fbtenv.sh ./fbt resources firmware_latest flash LIB_DEBUG=1 FIRMWARE_APP_SET=unit_tests FORCE=1 diff --git a/.github/workflows/updater_test.yml b/.github/workflows/updater_test.yml index 59cc6e716..df62daf58 100644 --- a/.github/workflows/updater_test.yml +++ b/.github/workflows/updater_test.yml @@ -20,7 +20,7 @@ jobs: - name: 'Flashing target firmware' id: first_full_flash - timeout-minutes: 20 + timeout-minutes: 10 run: | source scripts/toolchain/fbtenv.sh python3 scripts/testops.py -t=180 await_flipper From 248666be2bc4f2a88ec0e5be31702bb974c0f0e7 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Tue, 25 Feb 2025 02:13:44 +0300 Subject: [PATCH 099/268] fix --- applications/settings/clock_settings/clock_settings_alarm.c | 2 +- targets/f7/api_symbols.csv | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/applications/settings/clock_settings/clock_settings_alarm.c b/applications/settings/clock_settings/clock_settings_alarm.c index ef348c0b1..aea285f98 100644 --- a/applications/settings/clock_settings/clock_settings_alarm.c +++ b/applications/settings/clock_settings/clock_settings_alarm.c @@ -75,7 +75,7 @@ static void clock_settings_alarm_draw_callback(Canvas* canvas, void* ctx) { // Press Back to snooze canvas_set_font(canvas, FontPrimary); - canvas_draw_icon_ex(canvas, 5, 50, &I_back_btn_10x8, 0); + canvas_draw_icon_ex(canvas, 5, 50, &I_Pin_back_arrow_10x8, 0); canvas_draw_str_aligned(canvas, 20, 50, AlignLeft, AlignTop, "Snooze"); } diff --git a/targets/f7/api_symbols.csv b/targets/f7/api_symbols.csv index 5569761c1..7ad21efb5 100644 --- a/targets/f7/api_symbols.csv +++ b/targets/f7/api_symbols.csv @@ -1079,7 +1079,7 @@ Function,+,felica_get_uid,const uint8_t*,"const FelicaData*, size_t*" Function,+,felica_is_equal,_Bool,"const FelicaData*, const FelicaData*" Function,+,felica_load,_Bool,"FelicaData*, FlipperFormat*, uint32_t" Function,+,felica_poller_activate,FelicaError,"FelicaPoller*, FelicaData*" -Function,+,felica_poller_read_blocks,FelicaError,"FelicaPoller*, const uint8_t, const uint8_t*, uint16_t, FelicaPollerReadCommandResponse**" +Function,+,felica_poller_read_blocks,FelicaError,"FelicaPoller*, const uint8_t, const uint8_t* const, uint16_t, FelicaPollerReadCommandResponse** const" Function,+,felica_poller_sync_read,FelicaError,"Nfc*, FelicaData*, const FelicaCardKey*" Function,+,felica_reset,void,FelicaData* Function,+,felica_save,_Bool,"const FelicaData*, FlipperFormat*" @@ -3070,7 +3070,6 @@ Function,+,power_reboot,void,"Power*, PowerBootMode" Function,+,power_settings_load,void,PowerSettings* Function,+,power_settings_save,void,const PowerSettings* Function,-,power_trigger_ui_update,void,Power* -Function,+,power_enable_otg,void,"Power*, _Bool" Function,+,powf,float,"float, float" Function,-,powl,long double,"long double, long double" Function,+,pretty_format_bytes_hex_canonical,void,"FuriString*, size_t, const char*, const uint8_t*, size_t" From edd42d34c61d7e7d80e1f17218675751eda55477 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Tue, 25 Feb 2025 02:58:14 +0300 Subject: [PATCH 100/268] upd changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 59ecdab2b..7c95c0d38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,8 @@ * Anims: Disable winter anims * NFC: mfclassic poller fix early key reuse in dictionary attack state machine (by @noproto) * OFW PR 4126: Stricter constness for const data (by @hedger) +* OFW PR 4017: Alarm improvements: Snooze, timeouts, and dismissing from the locked state (by @Astrrra) +* OFW: NFC: FeliCa Protocol Expose Read Block API and Allow Specifying Service * OFW: LFRFID: Fix Detection Conflict Between Securakey and Noralsy Format (by @zinongli) * OFW: Stdio API improvements * OFW: GUI: Widget view extra options for JS From 0d99e54a17860469f98a36f749362cb9470648d1 Mon Sep 17 00:00:00 2001 From: Tyler Crumpton Date: Wed, 26 Feb 2025 10:34:54 -0500 Subject: [PATCH 101/268] Fix PWM-supported logic on pwmStop() (#4129) --- applications/system/js_app/modules/js_gpio.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/applications/system/js_app/modules/js_gpio.c b/applications/system/js_app/modules/js_gpio.c index 23884a6d4..2a559570f 100644 --- a/applications/system/js_app/modules/js_gpio.c +++ b/applications/system/js_app/modules/js_gpio.c @@ -308,7 +308,7 @@ static void js_gpio_is_pwm_running(struct mjs* mjs) { */ static void js_gpio_pwm_stop(struct mjs* mjs) { JsGpioPinInst* manager_data = JS_GET_CONTEXT(mjs); - if(manager_data->pwm_output != FuriHalPwmOutputIdNone) { + if(manager_data->pwm_output == FuriHalPwmOutputIdNone) { JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "PWM is not supported on this pin"); } From 3c7d9e63fb30d1e7ab3d4469faafd9239470eef0 Mon Sep 17 00:00:00 2001 From: Dmitry422 Date: Fri, 28 Feb 2025 23:14:18 +0700 Subject: [PATCH 102/268] Start working on combining rgb_backlight and original_backlight. --- .../services/notification/notification_app.c | 2 + .../notification_settings_app.c | 90 +++++++- .../notification_settings/rgb_backlight.c | 217 ++++++++++++++++++ .../notification_settings/rgb_backlight.h | 91 ++++++++ lib/drivers/SK6805.c | 103 +++++++++ lib/drivers/SK6805.h | 51 ++++ targets/f7/furi_hal/furi_hal_light.c | 34 +-- 7 files changed, 571 insertions(+), 17 deletions(-) create mode 100644 applications/settings/notification_settings/rgb_backlight.c create mode 100644 applications/settings/notification_settings/rgb_backlight.h create mode 100644 lib/drivers/SK6805.c create mode 100644 lib/drivers/SK6805.h diff --git a/applications/services/notification/notification_app.c b/applications/services/notification/notification_app.c index 35d2fe675..1af97e2f4 100644 --- a/applications/services/notification/notification_app.c +++ b/applications/services/notification/notification_app.c @@ -9,6 +9,7 @@ #include "notification.h" #include "notification_messages.h" #include "notification_app.h" +#include "applications/settings/notification_settings/rgb_backlight.h" #define TAG "NotificationSrv" @@ -616,6 +617,7 @@ int32_t notification_srv(void* p) { break; case SaveSettingsMessage: notification_save_settings(app); + rgb_backlight_save_settings(); break; case LoadSettingsMessage: notification_load_settings(app); diff --git a/applications/settings/notification_settings/notification_settings_app.c b/applications/settings/notification_settings/notification_settings_app.c index 2462b32bd..8e045cebf 100644 --- a/applications/settings/notification_settings/notification_settings_app.c +++ b/applications/settings/notification_settings/notification_settings_app.c @@ -3,6 +3,7 @@ #include #include #include +#include #define MAX_NOTIFICATION_SETTINGS 4 @@ -13,6 +14,8 @@ typedef struct { VariableItemList* variable_item_list; } NotificationAppSettings; +static VariableItem* temp_item; + static const NotificationSequence sequence_note_c = { &message_note_c5, &message_delay_100, @@ -168,6 +171,59 @@ static void vibro_changed(VariableItem* item) { notification_message(app->notification, &sequence_single_vibro); } +// Set RGB backlight color +static void color_changed(VariableItem* item) { + NotificationAppSettings* app = variable_item_get_context(item); + uint8_t index = variable_item_get_current_value_index(item); + rgb_backlight_set_color(index); + variable_item_set_current_value_text(item, rgb_backlight_get_color_text(index)); + notification_message(app->notification, &sequence_display_backlight_on); +} + +// TODO: refactor and fix this +static void color_set_custom_red(VariableItem* item) { + NotificationAppSettings* app = variable_item_get_context(item); + uint8_t index = variable_item_get_current_value_index(item); + rgb_backlight_set_custom_color(index, 0); + char valtext[4] = {}; + snprintf(valtext, sizeof(valtext), "%d", index); + variable_item_set_current_value_text(item, valtext); + rgb_backlight_set_color(13); + rgb_backlight_update(app->notification->settings.display_brightness * 0xFF, true); + // Set to custom color explicitly + variable_item_set_current_value_index(temp_item, 13); + variable_item_set_current_value_text(temp_item, rgb_backlight_get_color_text(13)); + notification_message(app->notification, &sequence_display_backlight_on); +} +static void color_set_custom_green(VariableItem* item) { + NotificationAppSettings* app = variable_item_get_context(item); + uint8_t index = variable_item_get_current_value_index(item); + rgb_backlight_set_custom_color(index, 1); + char valtext[4] = {}; + snprintf(valtext, sizeof(valtext), "%d", index); + variable_item_set_current_value_text(item, valtext); + rgb_backlight_set_color(13); + rgb_backlight_update(app->notification->settings.display_brightness * 0xFF, true); + // Set to custom color explicitly + variable_item_set_current_value_index(temp_item, 13); + variable_item_set_current_value_text(temp_item, rgb_backlight_get_color_text(13)); + notification_message(app->notification, &sequence_display_backlight_on); +} +static void color_set_custom_blue(VariableItem* item) { + NotificationAppSettings* app = variable_item_get_context(item); + uint8_t index = variable_item_get_current_value_index(item); + rgb_backlight_set_custom_color(index, 2); + char valtext[4] = {}; + snprintf(valtext, sizeof(valtext), "%d", index); + variable_item_set_current_value_text(item, valtext); + rgb_backlight_set_color(13); + rgb_backlight_update(app->notification->settings.display_brightness * 0xFF, true); + // Set to custom color explicitly + variable_item_set_current_value_index(temp_item, 13); + variable_item_set_current_value_text(temp_item, rgb_backlight_get_color_text(13)); + notification_message(app->notification, &sequence_display_backlight_on); +} + static uint32_t notification_app_settings_exit(void* context) { UNUSED(context); return VIEW_NONE; @@ -192,8 +248,40 @@ static NotificationAppSettings* alloc_settings(void) { variable_item_set_current_value_index(item, value_index); variable_item_set_current_value_text(item, contrast_text[value_index]); + // RGB Colors item = variable_item_list_add( - app->variable_item_list, "LCD Backlight", BACKLIGHT_COUNT, backlight_changed, app); + app->variable_item_list, "LCD Color", rgb_backlight_get_color_count(), color_changed, app); + value_index = rgb_backlight_get_settings()->display_color_index; + variable_item_set_current_value_index(item, value_index); + variable_item_set_current_value_text(item, rgb_backlight_get_color_text(value_index)); + temp_item = item; + + // Custom Color - REFACTOR THIS + item = variable_item_list_add( + app->variable_item_list, "Custom Red", 255, color_set_custom_red, app); + value_index = rgb_backlight_get_settings()->custom_r; + variable_item_set_current_value_index(item, value_index); + char valtext[4] = {}; + snprintf(valtext, sizeof(valtext), "%d", value_index); + variable_item_set_current_value_text(item, valtext); + + item = variable_item_list_add( + app->variable_item_list, "Custom Green", 255, color_set_custom_green, app); + value_index = rgb_backlight_get_settings()->custom_g; + variable_item_set_current_value_index(item, value_index); + snprintf(valtext, sizeof(valtext), "%d", value_index); + variable_item_set_current_value_text(item, valtext); + + item = variable_item_list_add( + app->variable_item_list, "Custom Blue", 255, color_set_custom_blue, app); + value_index = rgb_backlight_get_settings()->custom_b; + variable_item_set_current_value_index(item, value_index); + snprintf(valtext, sizeof(valtext), "%d", value_index); + variable_item_set_current_value_text(item, valtext); + // End of RGB + + item = variable_item_list_add( + app->variable_item_list, "LCD Brightness", BACKLIGHT_COUNT, backlight_changed, app); value_index = value_index_float( app->notification->settings.display_brightness, backlight_value, BACKLIGHT_COUNT); variable_item_set_current_value_index(item, value_index); diff --git a/applications/settings/notification_settings/rgb_backlight.c b/applications/settings/notification_settings/rgb_backlight.c new file mode 100644 index 000000000..4edd77568 --- /dev/null +++ b/applications/settings/notification_settings/rgb_backlight.c @@ -0,0 +1,217 @@ +/* + RGB backlight FlipperZero driver + Copyright (C) 2022-2023 Victor Nikitchuk (https://github.com/quen0n) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#include "rgb_backlight.h" +#include +#include + +#define RGB_BACKLIGHT_SETTINGS_VERSION 6 +#define RGB_BACKLIGHT_SETTINGS_FILE_NAME ".rgb_backlight.settings" +#define RGB_BACKLIGHT_SETTINGS_PATH INT_PATH(RGB_BACKLIGHT_SETTINGS_FILE_NAME) + +#define COLOR_COUNT (sizeof(colors) / sizeof(RGBBacklightColor)) + +#define TAG "RGB Backlight" + +static RGBBacklightSettings rgb_settings = { + .version = RGB_BACKLIGHT_SETTINGS_VERSION, + .display_color_index = 0, + .custom_r = 254, + .custom_g = 254, + .custom_b = 254, + .settings_is_loaded = false}; + +static const RGBBacklightColor colors[] = { + {"Orange", 255, 60, 0}, + {"Yellow", 255, 144, 0}, + {"Spring", 167, 255, 0}, + {"Lime", 0, 255, 0}, + {"Aqua", 0, 255, 127}, + {"Cyan", 0, 210, 210}, + {"Azure", 0, 127, 255}, + {"Blue", 0, 0, 255}, + {"Purple", 127, 0, 255}, + {"Magenta", 210, 0, 210}, + {"Pink", 255, 0, 127}, + {"Red", 255, 0, 0}, + {"White", 254, 210, 200}, + {"Custom", 0, 0, 0}, +}; + +uint8_t rgb_backlight_get_color_count(void) { + return COLOR_COUNT; +} + +const char* rgb_backlight_get_color_text(uint8_t index) { + return colors[index].name; +} + +void rgb_backlight_load_settings(void) { + // Do not load settings if we are in other boot modes than normal + if(furi_hal_rtc_get_boot_mode() != FuriHalRtcBootModeNormal) { + rgb_settings.settings_is_loaded = true; + return; + } + + // Wait for all required services to start and create their records + uint8_t timeout = 0; + while(!furi_record_exists(RECORD_STORAGE)) { + timeout++; + if(timeout > 150) { + rgb_settings.settings_is_loaded = true; + return; + } + furi_delay_ms(5); + } + + RGBBacklightSettings settings; + File* file = storage_file_alloc(furi_record_open(RECORD_STORAGE)); + const size_t settings_size = sizeof(RGBBacklightSettings); + + FURI_LOG_D(TAG, "loading settings from \"%s\"", RGB_BACKLIGHT_SETTINGS_PATH); + bool fs_result = + storage_file_open(file, RGB_BACKLIGHT_SETTINGS_PATH, FSAM_READ, FSOM_OPEN_EXISTING); + + if(fs_result) { + uint16_t bytes_count = storage_file_read(file, &settings, settings_size); + + if(bytes_count != settings_size) { + fs_result = false; + } + } + + if(fs_result) { + FURI_LOG_D(TAG, "load success"); + if(settings.version != RGB_BACKLIGHT_SETTINGS_VERSION) { + FURI_LOG_E( + TAG, + "version(%d != %d) mismatch", + settings.version, + RGB_BACKLIGHT_SETTINGS_VERSION); + } else { + memcpy(&rgb_settings, &settings, settings_size); + } + } else { + FURI_LOG_E(TAG, "load failed, %s", storage_file_get_error_desc(file)); + } + + storage_file_close(file); + storage_file_free(file); + furi_record_close(RECORD_STORAGE); + rgb_settings.settings_is_loaded = true; +} + +void rgb_backlight_save_settings(void) { + RGBBacklightSettings settings; + File* file = storage_file_alloc(furi_record_open(RECORD_STORAGE)); + const size_t settings_size = sizeof(RGBBacklightSettings); + + FURI_LOG_D(TAG, "saving settings to \"%s\"", RGB_BACKLIGHT_SETTINGS_PATH); + + memcpy(&settings, &rgb_settings, settings_size); + + bool fs_result = + storage_file_open(file, RGB_BACKLIGHT_SETTINGS_PATH, FSAM_WRITE, FSOM_CREATE_ALWAYS); + + if(fs_result) { + uint16_t bytes_count = storage_file_write(file, &settings, settings_size); + + if(bytes_count != settings_size) { + fs_result = false; + } + } + + if(fs_result) { + FURI_LOG_D(TAG, "save success"); + } else { + FURI_LOG_E(TAG, "save failed, %s", storage_file_get_error_desc(file)); + } + + storage_file_close(file); + storage_file_free(file); + furi_record_close(RECORD_STORAGE); +} + +RGBBacklightSettings* rgb_backlight_get_settings(void) { + if(!rgb_settings.settings_is_loaded) { + rgb_backlight_load_settings(); + } + return &rgb_settings; +} + +void rgb_backlight_set_color(uint8_t color_index) { + if(color_index > (rgb_backlight_get_color_count() - 1)) color_index = 0; + rgb_settings.display_color_index = color_index; +} + +void rgb_backlight_set_custom_color(uint8_t color, uint8_t index) { + if(index > 2) return; + if(index == 0) { + rgb_settings.custom_r = color; + } else if(index == 1) { + rgb_settings.custom_g = color; + } else if(index == 2) { + rgb_settings.custom_b = color; + } +} + +void rgb_backlight_update(uint8_t brightness, bool bypass) { + if(!rgb_settings.settings_is_loaded) { + rgb_backlight_load_settings(); + } + + if(!bypass) { + static uint8_t last_color_index = 255; + static uint8_t last_brightness = 123; + + if(last_brightness == brightness && last_color_index == rgb_settings.display_color_index) { + return; + } + + last_brightness = brightness; + last_color_index = rgb_settings.display_color_index; + } + + for(uint8_t i = 0; i < SK6805_get_led_count(); i++) { + if(rgb_settings.display_color_index == 13) { + uint8_t r = rgb_settings.custom_r * (brightness / 255.0f); + uint8_t g = rgb_settings.custom_g * (brightness / 255.0f); + uint8_t b = rgb_settings.custom_b * (brightness / 255.0f); + + SK6805_set_led_color(i, r, g, b); + } else { + if((colors[rgb_settings.display_color_index].red == 0) && + (colors[rgb_settings.display_color_index].green == 0) && + (colors[rgb_settings.display_color_index].blue == 0)) { + uint8_t r = colors[0].red * (brightness / 255.0f); + uint8_t g = colors[0].green * (brightness / 255.0f); + uint8_t b = colors[0].blue * (brightness / 255.0f); + + SK6805_set_led_color(i, r, g, b); + } else { + uint8_t r = colors[rgb_settings.display_color_index].red * (brightness / 255.0f); + uint8_t g = colors[rgb_settings.display_color_index].green * (brightness / 255.0f); + uint8_t b = colors[rgb_settings.display_color_index].blue * (brightness / 255.0f); + + SK6805_set_led_color(i, r, g, b); + } + } + } + + SK6805_update(); +} diff --git a/applications/settings/notification_settings/rgb_backlight.h b/applications/settings/notification_settings/rgb_backlight.h new file mode 100644 index 000000000..f215ed312 --- /dev/null +++ b/applications/settings/notification_settings/rgb_backlight.h @@ -0,0 +1,91 @@ +/* + RGB backlight FlipperZero driver + Copyright (C) 2022-2023 Victor Nikitchuk (https://github.com/quen0n) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#include +#include "SK6805.h" + +typedef struct { + char* name; + uint8_t red; + uint8_t green; + uint8_t blue; +} RGBBacklightColor; + +typedef struct { + uint8_t version; + uint8_t display_color_index; + uint8_t custom_r; + uint8_t custom_g; + uint8_t custom_b; + bool settings_is_loaded; +} RGBBacklightSettings; + +/** + * @brief Получить текущие настройки RGB-подсветки + * + * @return Указатель на структуру настроек + */ +RGBBacklightSettings* rgb_backlight_get_settings(void); + +/** + * @brief Загрузить настройки подсветки с SD-карты + */ +void rgb_backlight_load_settings(void); + +/** + * @brief Сохранить текущие настройки RGB-подсветки + */ +void rgb_backlight_save_settings(void); + +/** + * @brief Применить текущие настройки RGB-подсветки + * + * @param brightness Яркость свечения (0-255) + * @param bypass Применить настройки принудительно + */ +void rgb_backlight_update(uint8_t brightness, bool bypass); + +/** + * @brief Установить цвет RGB-подсветки + * + * @param color_index Индекс цвета (0 - rgb_backlight_get_color_count()) + */ +void rgb_backlight_set_color(uint8_t color_index); + +/** + * @brief Set custom color values by index - 0=R 1=G 2=B + * + * @param color - color value (0-255) + * @param index - color index (0-2) 0=R 1=G 2=B + */ +void rgb_backlight_set_custom_color(uint8_t color, uint8_t index); + +/** + * @brief Получить количество доступных цветов + * + * @return Число доступных вариантов цвета + */ +uint8_t rgb_backlight_get_color_count(void); + +/** + * @brief Получить текстовое название цвета + * + * @param index Индекс из доступных вариантов цвета + * @return Указатель на строку с названием цвета + */ +const char* rgb_backlight_get_color_text(uint8_t index); diff --git a/lib/drivers/SK6805.c b/lib/drivers/SK6805.c new file mode 100644 index 000000000..8158c55a4 --- /dev/null +++ b/lib/drivers/SK6805.c @@ -0,0 +1,103 @@ +/* + SK6805 FlipperZero driver + Copyright (C) 2022-2023 Victor Nikitchuk (https://github.com/quen0n) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#include "SK6805.h" +#include + +/* Настройки */ +#define SK6805_LED_COUNT 3 //Количество светодиодов на плате подсветки +#define SK6805_LED_PIN &led_pin //Порт подключения светодиодов + +#ifdef FURI_DEBUG +#define DEBUG_PIN &gpio_ext_pa7 +#define DEBUG_INIT() \ + furi_hal_gpio_init(DEBUG_PIN, GpioModeOutputPushPull, GpioPullNo, GpioSpeedVeryHigh) +#define DEBUG_SET_HIGH() furi_hal_gpio_write(DEBUG_PIN, true) +#define DEBUG_SET_LOW() furi_hal_gpio_write(DEBUG_PIN, false) +#else +#define DEBUG_INIT() +#define DEBUG_SET_HIGH() +#define DEBUG_SET_LOW() +#endif + +static const GpioPin led_pin = {.port = GPIOA, .pin = LL_GPIO_PIN_8}; +static uint8_t led_buffer[SK6805_LED_COUNT][3]; + +void SK6805_init(void) { + DEBUG_INIT(); + furi_hal_gpio_write(SK6805_LED_PIN, false); + furi_hal_gpio_init(SK6805_LED_PIN, GpioModeOutputPushPull, GpioPullNo, GpioSpeedVeryHigh); +} + +uint8_t SK6805_get_led_count(void) { + return (const uint8_t)SK6805_LED_COUNT; +} +void SK6805_set_led_color(uint8_t led_index, uint8_t r, uint8_t g, uint8_t b) { + furi_check(led_index < SK6805_LED_COUNT); + + led_buffer[led_index][0] = g; + led_buffer[led_index][1] = r; + led_buffer[led_index][2] = b; +} + +void SK6805_update(void) { + SK6805_init(); + FURI_CRITICAL_ENTER(); + furi_delay_us(150); + uint32_t end; + /* Последовательная отправка цветов светодиодов */ + for(uint8_t lednumber = 0; lednumber < SK6805_LED_COUNT; lednumber++) { + //Последовательная отправка цветов светодиода + for(uint8_t color = 0; color < 3; color++) { + //Последовательная отправка битов цвета + uint8_t i = 0b10000000; + while(i != 0) { + if(led_buffer[lednumber][color] & (i)) { + furi_hal_gpio_write(SK6805_LED_PIN, true); + DEBUG_SET_HIGH(); + end = DWT->CYCCNT + 30; + //T1H 600 us (615 us) + while(DWT->CYCCNT < end) { + } + furi_hal_gpio_write(SK6805_LED_PIN, false); + DEBUG_SET_LOW(); + end = DWT->CYCCNT + 26; + //T1L 600 us (587 us) + while(DWT->CYCCNT < end) { + } + } else { + furi_hal_gpio_write(SK6805_LED_PIN, true); + DEBUG_SET_HIGH(); + end = DWT->CYCCNT + 11; + //T0H 300 ns (312 ns) + while(DWT->CYCCNT < end) { + } + furi_hal_gpio_write(SK6805_LED_PIN, false); + DEBUG_SET_LOW(); + end = DWT->CYCCNT + 43; + //T0L 900 ns (890 ns) + while(DWT->CYCCNT < end) { + } + } + i >>= 1; + } + } + } + furi_delay_us(150); + FURI_CRITICAL_EXIT(); +} diff --git a/lib/drivers/SK6805.h b/lib/drivers/SK6805.h new file mode 100644 index 000000000..c97054f6d --- /dev/null +++ b/lib/drivers/SK6805.h @@ -0,0 +1,51 @@ +/* + SK6805 FlipperZero driver + Copyright (C) 2022-2023 Victor Nikitchuk (https://github.com/quen0n) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#ifndef SK6805_H_ +#define SK6805_H_ + +#include + +/** + * @brief Инициализация линии управления подсветкой + */ +void SK6805_init(void); + +/** + * @brief Получить количество светодиодов в подсветке + * + * @return Количество светодиодов + */ +uint8_t SK6805_get_led_count(void); + +/** + * @brief Установить цвет свечения светодиода + * + * @param led_index номер светодиода (от 0 до SK6805_get_led_count()) + * @param r значение красного (0-255) + * @param g значение зелёного (0-255) + * @param b значение синего (0-255) + */ +void SK6805_set_led_color(uint8_t led_index, uint8_t r, uint8_t g, uint8_t b); + +/** + * @brief Обновление состояния подсветки дисплея + */ +void SK6805_update(void); + +#endif /* SK6805_H_ */ diff --git a/targets/f7/furi_hal/furi_hal_light.c b/targets/f7/furi_hal/furi_hal_light.c index 621478d14..9ee542034 100644 --- a/targets/f7/furi_hal/furi_hal_light.c +++ b/targets/f7/furi_hal/furi_hal_light.c @@ -3,6 +3,7 @@ #include #include #include +#include #define LED_CURRENT_RED (50u) #define LED_CURRENT_GREEN (50u) @@ -31,22 +32,23 @@ void furi_hal_light_init(void) { } void furi_hal_light_set(Light light, uint8_t value) { - furi_hal_i2c_acquire(&furi_hal_i2c_handle_power); - if(light & LightRed) { - lp5562_set_channel_value(&furi_hal_i2c_handle_power, LP5562ChannelRed, value); - } - if(light & LightGreen) { - lp5562_set_channel_value(&furi_hal_i2c_handle_power, LP5562ChannelGreen, value); - } - if(light & LightBlue) { - lp5562_set_channel_value(&furi_hal_i2c_handle_power, LP5562ChannelBlue, value); - } - if(light & LightBacklight) { - uint8_t prev = lp5562_get_channel_value(&furi_hal_i2c_handle_power, LP5562ChannelWhite); - lp5562_execute_ramp( - &furi_hal_i2c_handle_power, LP5562Engine1, LP5562ChannelWhite, prev, value, 100); - } - furi_hal_i2c_release(&furi_hal_i2c_handle_power); + furi_hal_i2c_acquire(&furi_hal_i2c_handle_power); + if(light & LightRed) { + lp5562_set_channel_value(&furi_hal_i2c_handle_power, LP5562ChannelRed, value); + } + if(light & LightGreen) { + lp5562_set_channel_value(&furi_hal_i2c_handle_power, LP5562ChannelGreen, value); + } + if(light & LightBlue) { + lp5562_set_channel_value(&furi_hal_i2c_handle_power, LP5562ChannelBlue, value); + } + if(light & LightBacklight) { + uint8_t prev = lp5562_get_channel_value(&furi_hal_i2c_handle_power, LP5562ChannelWhite); + lp5562_execute_ramp( + &furi_hal_i2c_handle_power, LP5562Engine1, LP5562ChannelWhite, prev, value, 100); + rgb_backlight_update(value, false); + } + furi_hal_i2c_release(&furi_hal_i2c_handle_power); } void furi_hal_light_blink_start(Light light, uint8_t brightness, uint16_t on_time, uint16_t period) { From bb170140e2e9dcd9e3aa7e681280adb620daaf3b Mon Sep 17 00:00:00 2001 From: Dmitry422 Date: Sat, 1 Mar 2025 22:55:25 +0700 Subject: [PATCH 103/268] =?UTF-8?q?=D0=A1ombining=20rgb=5Fbacklight=20and?= =?UTF-8?q?=20original=5Fbacklight=20finished.=20-=20RGB=20Colors=20settin?= =?UTF-8?q?gs=20available=20only=20when=20Settings-Notification-RGB=5FMOD?= =?UTF-8?q?=5FInstalled=20swithed=20ON=20-=20RGB=5FMOD=5FInstalled=20switc?= =?UTF-8?q?h=20(ON|OFF)=20available=20in=20Notification=20only=20with=20De?= =?UTF-8?q?bug=20mode=20swithed=20ON.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../services/notification/notification_app.c | 1 + .../services/notification/notification_app.h | 3 +- .../notification_settings_app.c | 92 +++++++++++++------ 3 files changed, 67 insertions(+), 29 deletions(-) diff --git a/applications/services/notification/notification_app.c b/applications/services/notification/notification_app.c index 1af97e2f4..e02a16a1d 100644 --- a/applications/services/notification/notification_app.c +++ b/applications/services/notification/notification_app.c @@ -523,6 +523,7 @@ static NotificationApp* notification_app_alloc(void) { app->settings.led_brightness = 1.0f; app->settings.display_off_delay_ms = 30000; app->settings.vibro_on = true; + app->settings.rgb_mod_installed = false; app->display.value[LayerInternal] = 0x00; app->display.value[LayerNotification] = 0x00; diff --git a/applications/services/notification/notification_app.h b/applications/services/notification/notification_app.h index e19546574..056a762df 100644 --- a/applications/services/notification/notification_app.h +++ b/applications/services/notification/notification_app.h @@ -33,7 +33,7 @@ typedef struct { Light light; } NotificationLedLayer; -#define NOTIFICATION_SETTINGS_VERSION 0x02 +#define NOTIFICATION_SETTINGS_VERSION 0x03 #define NOTIFICATION_SETTINGS_PATH INT_PATH(NOTIFICATION_SETTINGS_FILE_NAME) typedef struct { @@ -44,6 +44,7 @@ typedef struct { uint32_t display_off_delay_ms; int8_t contrast; bool vibro_on; + bool rgb_mod_installed; } NotificationSettings; struct NotificationApp { diff --git a/applications/settings/notification_settings/notification_settings_app.c b/applications/settings/notification_settings/notification_settings_app.c index 8e045cebf..527e0ba22 100644 --- a/applications/settings/notification_settings/notification_settings_app.c +++ b/applications/settings/notification_settings/notification_settings_app.c @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -107,6 +108,13 @@ const char* const vibro_text[VIBRO_COUNT] = { }; const bool vibro_value[VIBRO_COUNT] = {false, true}; +#define RGB_MOD_COUNT 2 +const char* const rgb_mod_text[RGB_MOD_COUNT] = { + "OFF", + "ON", +}; +const bool rgb_mod_value[RGB_MOD_COUNT] = {false, true}; + static void contrast_changed(VariableItem* item) { NotificationAppSettings* app = variable_item_get_context(item); uint8_t index = variable_item_get_current_value_index(item); @@ -171,6 +179,13 @@ static void vibro_changed(VariableItem* item) { notification_message(app->notification, &sequence_single_vibro); } +static void rgb_mod_installed_changed(VariableItem* item) { + NotificationAppSettings* app = variable_item_get_context(item); + uint8_t index = variable_item_get_current_value_index(item); + variable_item_set_current_value_text(item, rgb_mod_text[index]); + app->notification->settings.rgb_mod_installed = rgb_mod_value[index]; +} + // Set RGB backlight color static void color_changed(VariableItem* item) { NotificationAppSettings* app = variable_item_get_context(item); @@ -248,38 +263,59 @@ static NotificationAppSettings* alloc_settings(void) { variable_item_set_current_value_index(item, value_index); variable_item_set_current_value_text(item, contrast_text[value_index]); - // RGB Colors - item = variable_item_list_add( - app->variable_item_list, "LCD Color", rgb_backlight_get_color_count(), color_changed, app); - value_index = rgb_backlight_get_settings()->display_color_index; - variable_item_set_current_value_index(item, value_index); - variable_item_set_current_value_text(item, rgb_backlight_get_color_text(value_index)); - temp_item = item; + // Show RGB_MOD_Installed_Swith only in Debug mode + if(furi_hal_rtc_is_flag_set(FuriHalRtcFlagDebug)) { + item = variable_item_list_add( + app->variable_item_list, + "RGB MOD Installed", + RGB_MOD_COUNT, + rgb_mod_installed_changed, + app); + value_index = value_index_bool( + app->notification->settings.rgb_mod_installed, rgb_mod_value, RGB_MOD_COUNT); + variable_item_set_current_value_index(item, value_index); + variable_item_set_current_value_text(item, rgb_mod_text[value_index]); + } - // Custom Color - REFACTOR THIS - item = variable_item_list_add( - app->variable_item_list, "Custom Red", 255, color_set_custom_red, app); - value_index = rgb_backlight_get_settings()->custom_r; - variable_item_set_current_value_index(item, value_index); - char valtext[4] = {}; - snprintf(valtext, sizeof(valtext), "%d", value_index); - variable_item_set_current_value_text(item, valtext); + //Show RGB settings only when debug mode enabled or rgb_mod_installed is true + if((furi_hal_rtc_is_flag_set(FuriHalRtcFlagDebug)) || + (app->notification->settings.rgb_mod_installed)) { + // RGB Colors + item = variable_item_list_add( + app->variable_item_list, + "LCD Color", + rgb_backlight_get_color_count(), + color_changed, + app); + value_index = rgb_backlight_get_settings()->display_color_index; + variable_item_set_current_value_index(item, value_index); + variable_item_set_current_value_text(item, rgb_backlight_get_color_text(value_index)); + temp_item = item; - item = variable_item_list_add( - app->variable_item_list, "Custom Green", 255, color_set_custom_green, app); - value_index = rgb_backlight_get_settings()->custom_g; - variable_item_set_current_value_index(item, value_index); - snprintf(valtext, sizeof(valtext), "%d", value_index); - variable_item_set_current_value_text(item, valtext); + // Custom Color - REFACTOR THIS + item = variable_item_list_add( + app->variable_item_list, "Custom Red", 255, color_set_custom_red, app); + value_index = rgb_backlight_get_settings()->custom_r; + variable_item_set_current_value_index(item, value_index); + char valtext[4] = {}; + snprintf(valtext, sizeof(valtext), "%d", value_index); + variable_item_set_current_value_text(item, valtext); - item = variable_item_list_add( - app->variable_item_list, "Custom Blue", 255, color_set_custom_blue, app); - value_index = rgb_backlight_get_settings()->custom_b; - variable_item_set_current_value_index(item, value_index); - snprintf(valtext, sizeof(valtext), "%d", value_index); - variable_item_set_current_value_text(item, valtext); - // End of RGB + item = variable_item_list_add( + app->variable_item_list, "Custom Green", 255, color_set_custom_green, app); + value_index = rgb_backlight_get_settings()->custom_g; + variable_item_set_current_value_index(item, value_index); + snprintf(valtext, sizeof(valtext), "%d", value_index); + variable_item_set_current_value_text(item, valtext); + item = variable_item_list_add( + app->variable_item_list, "Custom Blue", 255, color_set_custom_blue, app); + value_index = rgb_backlight_get_settings()->custom_b; + variable_item_set_current_value_index(item, value_index); + snprintf(valtext, sizeof(valtext), "%d", value_index); + variable_item_set_current_value_text(item, valtext); + // End of RGB + } item = variable_item_list_add( app->variable_item_list, "LCD Brightness", BACKLIGHT_COUNT, backlight_changed, app); value_index = value_index_float( From 08304ccff5a881f05578d8904dfc192bb5b5511e Mon Sep 17 00:00:00 2001 From: Dmitry422 Date: Sat, 1 Mar 2025 23:58:07 +0700 Subject: [PATCH 104/268] Small usability changes --- .../notification_settings/notification_settings_app.c | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/applications/settings/notification_settings/notification_settings_app.c b/applications/settings/notification_settings/notification_settings_app.c index 527e0ba22..a34ac5b4a 100644 --- a/applications/settings/notification_settings/notification_settings_app.c +++ b/applications/settings/notification_settings/notification_settings_app.c @@ -277,9 +277,8 @@ static NotificationAppSettings* alloc_settings(void) { variable_item_set_current_value_text(item, rgb_mod_text[value_index]); } - //Show RGB settings only when debug mode enabled or rgb_mod_installed is true - if((furi_hal_rtc_is_flag_set(FuriHalRtcFlagDebug)) || - (app->notification->settings.rgb_mod_installed)) { + //Show RGB settings only when rgb_mod_installed is true + if(app->notification->settings.rgb_mod_installed) { // RGB Colors item = variable_item_list_add( app->variable_item_list, From 7ac1452618e2c0774cdb03776dea28b21ad6ff42 Mon Sep 17 00:00:00 2001 From: Dmitry422 Date: Tue, 4 Mar 2025 22:13:59 +0700 Subject: [PATCH 105/268] Start working on RGB mod Rainbow effect (inspired by Willy-JL idea). --- .../services/notification/notification_app.c | 57 +++- .../services/notification/notification_app.h | 12 + .../notification_settings_app.c | 267 ++++++++++++++---- 3 files changed, 277 insertions(+), 59 deletions(-) diff --git a/applications/services/notification/notification_app.c b/applications/services/notification/notification_app.c index e02a16a1d..229f7c5ea 100644 --- a/applications/services/notification/notification_app.c +++ b/applications/services/notification/notification_app.c @@ -512,18 +512,49 @@ static void input_event_callback(const void* value, void* context) { notification_message(app, &sequence_display_backlight_on); } +// RGB MOD RAINBOW SECTION + +//start furi timer for rgb_mod_rainbow +static void rgb_mod_rainbow_timer_start(NotificationApp* app) { + furi_timer_start( + app->rgb_mod_rainbow_timer, furi_ms_to_ticks(app->settings.rgb_mod_rainbow_speed_ms)); +} + +//stop furi timer for rgb_mod_rainbow +static void rgb_mod_rainbow_timer_stop(NotificationApp* app) { + furi_timer_stop(app->rgb_mod_rainbow_timer); +} + +// start/stop rgb_mod_rainbow_timer only if rgb_mod_installed +static void rgb_mod_rainbow_timer_control(NotificationApp* app) { + if(app->settings.rgb_mod_installed) { + if(app->settings.rgb_mod_rainbow) { + rgb_mod_rainbow_timer_start(app); + } else { + rgb_mod_rainbow_timer_stop(app); + } + } +} + +// callback for rgb_mod_rainbow_timer (what we do when timer end) +static void rgb_mod_rainbow_timer_callback(void* context) { + furi_assert(context); + FURI_LOG_I("RAINBOW", "Rainbow timer callback - change color"); +} + +// END OF RGB MOD RAINBOW SECTION + // App alloc static NotificationApp* notification_app_alloc(void) { NotificationApp* app = malloc(sizeof(NotificationApp)); app->queue = furi_message_queue_alloc(8, sizeof(NotificationAppMessage)); - app->display_timer = furi_timer_alloc(notification_display_timer, FuriTimerTypeOnce, app); + app->display_timer = furi_timer_alloc(notification_display_timer, FuriTimerTypePeriodic, app); app->settings.speaker_volume = 1.0f; app->settings.display_brightness = 1.0f; app->settings.led_brightness = 1.0f; app->settings.display_off_delay_ms = 30000; app->settings.vibro_on = true; - app->settings.rgb_mod_installed = false; app->display.value[LayerInternal] = 0x00; app->display.value[LayerNotification] = 0x00; @@ -552,7 +583,25 @@ static NotificationApp* notification_app_alloc(void) { furi_pubsub_subscribe(app->event_record, input_event_callback, app); notification_message(app, &sequence_display_backlight_on); + //RGB MOD SECTION + + //rgb mod + app->settings.rgb_mod_installed = false; + + //rgb mod rainbow init settings + app->settings.rgb_mod_rainbow = false; + app->settings.rgb_mod_rainbow_speed_ms = 1000; + app->settings.rgb_mod_rainbow_step = 1; + app->rgb_mod_rainbow_color1 = 1; + app->rgb_mod_rainbow_color2 = 1; + app->rgb_mod_rainbow_color3 = 1; + + //define rgb_mod_rainbow_timer and they callback + app->rgb_mod_rainbow_timer = + furi_timer_alloc(rgb_mod_rainbow_timer_callback, FuriTimerTypePeriodic, app); return app; + + // END OF RGB SECTION } static void notification_storage_callback(const void* message, void* context) { @@ -575,6 +624,8 @@ static void notification_apply_settings(NotificationApp* app) { } notification_apply_lcd_contrast(app); + // on system init start rgb_mod_rainbow_timer if they ON in config + rgb_mod_rainbow_timer_control(app); } static void notification_init_settings(NotificationApp* app) { @@ -619,6 +670,8 @@ int32_t notification_srv(void* p) { case SaveSettingsMessage: notification_save_settings(app); rgb_backlight_save_settings(); + //call rgb_mod_timer_control when we save settings + rgb_mod_rainbow_timer_control(app); break; case LoadSettingsMessage: notification_load_settings(app); diff --git a/applications/services/notification/notification_app.h b/applications/services/notification/notification_app.h index 056a762df..098ef5ee7 100644 --- a/applications/services/notification/notification_app.h +++ b/applications/services/notification/notification_app.h @@ -44,7 +44,12 @@ typedef struct { uint32_t display_off_delay_ms; int8_t contrast; bool vibro_on; + bool rgb_mod_installed; + bool rgb_mod_rainbow; + uint32_t rgb_mod_rainbow_speed_ms; + uint32_t rgb_mod_rainbow_step; + } NotificationSettings; struct NotificationApp { @@ -56,6 +61,13 @@ struct NotificationApp { NotificationLedLayer led[NOTIFICATION_LED_COUNT]; uint8_t display_led_lock; + // rainbow mode section + FuriTimer* rgb_mod_rainbow_timer; + int8_t rgb_mod_rainbow_color1; + int8_t rgb_mod_rainbow_color2; + int8_t rgb_mod_rainbow_color3; + + NotificationSettings settings; }; diff --git a/applications/settings/notification_settings/notification_settings_app.c b/applications/settings/notification_settings/notification_settings_app.c index a34ac5b4a..941284c89 100644 --- a/applications/settings/notification_settings/notification_settings_app.c +++ b/applications/settings/notification_settings/notification_settings_app.c @@ -13,6 +13,7 @@ typedef struct { Gui* gui; ViewDispatcher* view_dispatcher; VariableItemList* variable_item_list; + VariableItemList* variable_item_list_rgb; } NotificationAppSettings; static VariableItem* temp_item; @@ -115,6 +116,50 @@ const char* const rgb_mod_text[RGB_MOD_COUNT] = { }; const bool rgb_mod_value[RGB_MOD_COUNT] = {false, true}; +#define RGB_MOD_RAINBOW_COUNT 2 +const char* const rgb_mod_rainbow_text[RGB_MOD_RAINBOW_COUNT] = { + "OFF", + "ON", +}; +const bool rgb_mod_rainbow_value[RGB_MOD_RAINBOW_COUNT] = {false, true}; + +#define RGB_MOD_RAINBOW_SPEED_COUNT 14 +const char* const rgb_mod_rainbow_speed_text[RGB_MOD_RAINBOW_SPEED_COUNT] = { + "0.1s", + "0.2s", + "0.3s", + "0.4s", + "0.6s", + "0.8s", + "1s", + "1.2s", + "1.4s", + "1.6s", + "1.8s", + "2s", + "2.5s", + "3s"}; +const uint32_t rgb_mod_rainbow_speed_value[RGB_MOD_RAINBOW_SPEED_COUNT] = + {100, 200, 400, 600, 800, 1000, 1200, 1400, 1600, 1800, 2000, 2500, 3000}; + +#define RGB_MOD_RAINBOW_STEP_COUNT 7 +const char* const rgb_mod_rainbow_step_text[RGB_MOD_RAINBOW_SPEED_COUNT] = { + "1", + "2", + "5", + "7", + "10", + "15", + "20", + }; +const uint32_t rgb_mod_rainbow_step_value[RGB_MOD_RAINBOW_STEP_COUNT] = + {1, 2, 5, 7, 10, 15, 20}; + +typedef enum { + MainViewId, + RGBViewId, +} ViewId; + static void contrast_changed(VariableItem* item) { NotificationAppSettings* app = variable_item_get_context(item); uint8_t index = variable_item_get_current_value_index(item); @@ -186,6 +231,27 @@ static void rgb_mod_installed_changed(VariableItem* item) { app->notification->settings.rgb_mod_installed = rgb_mod_value[index]; } +static void rgb_mod_rainbow_changed(VariableItem* item) { + NotificationAppSettings* app = variable_item_get_context(item); + uint8_t index = variable_item_get_current_value_index(item); + variable_item_set_current_value_text(item, rgb_mod_rainbow_text[index]); + app->notification->settings.rgb_mod_rainbow = rgb_mod_rainbow_value[index]; +} + +static void rgb_mod_rainbow_speed_changed(VariableItem* item) { + NotificationAppSettings* app = variable_item_get_context(item); + uint8_t index = variable_item_get_current_value_index(item); + variable_item_set_current_value_text(item, rgb_mod_rainbow_speed_text[index]); + app->notification->settings.rgb_mod_rainbow_speed_ms = rgb_mod_rainbow_speed_value[index]; +} + +static void rgb_mod_rainbow_step_changed(VariableItem* item) { + NotificationAppSettings* app = variable_item_get_context(item); + uint8_t index = variable_item_get_current_value_index(item); + variable_item_set_current_value_text(item, rgb_mod_rainbow_step_text[index]); + app->notification->settings.rgb_mod_rainbow_step = rgb_mod_rainbow_step_value[index]; +} + // Set RGB backlight color static void color_changed(VariableItem* item) { NotificationAppSettings* app = variable_item_get_context(item); @@ -244,6 +310,23 @@ static uint32_t notification_app_settings_exit(void* context) { return VIEW_NONE; } +// open rgb_settings_view if user press OK on first (index=0) menu string and (debug mode or rgb_mod_install is true) +void variable_item_list_enter_callback(void* context, uint32_t index) { + UNUSED(context); + NotificationAppSettings* app = context; + + if(((app->notification->settings.rgb_mod_installed) || + (furi_hal_rtc_is_flag_set(FuriHalRtcFlagDebug))) && (index == 0)) { + view_dispatcher_switch_to_view(app->view_dispatcher, RGBViewId); + } +} + +// switch to main view on exit from rgb_settings_view +static uint32_t notification_app_rgb_settings_exit(void* context) { + UNUSED(context); + return MainViewId; +} + static NotificationAppSettings* alloc_settings(void) { NotificationAppSettings* app = malloc(sizeof(NotificationAppSettings)); app->notification = furi_record_open(RECORD_NOTIFICATION); @@ -251,11 +334,21 @@ static NotificationAppSettings* alloc_settings(void) { app->variable_item_list = variable_item_list_alloc(); View* view = variable_item_list_get_view(app->variable_item_list); + //set callback for exit from view view_set_previous_callback(view, notification_app_settings_exit); + // set callback for OK pressed in menu + variable_item_list_set_enter_callback( + app->variable_item_list, variable_item_list_enter_callback, app); VariableItem* item; uint8_t value_index; + //Show RGB settings only when debug_mode or rgb_mod_installed is active + if((app->notification->settings.rgb_mod_installed) || + (furi_hal_rtc_is_flag_set(FuriHalRtcFlagDebug))) { + item = variable_item_list_add(app->variable_item_list, "RGB mod settings", 0, NULL, app); + } + item = variable_item_list_add( app->variable_item_list, "LCD Contrast", CONTRAST_COUNT, contrast_changed, app); value_index = @@ -263,58 +356,6 @@ static NotificationAppSettings* alloc_settings(void) { variable_item_set_current_value_index(item, value_index); variable_item_set_current_value_text(item, contrast_text[value_index]); - // Show RGB_MOD_Installed_Swith only in Debug mode - if(furi_hal_rtc_is_flag_set(FuriHalRtcFlagDebug)) { - item = variable_item_list_add( - app->variable_item_list, - "RGB MOD Installed", - RGB_MOD_COUNT, - rgb_mod_installed_changed, - app); - value_index = value_index_bool( - app->notification->settings.rgb_mod_installed, rgb_mod_value, RGB_MOD_COUNT); - variable_item_set_current_value_index(item, value_index); - variable_item_set_current_value_text(item, rgb_mod_text[value_index]); - } - - //Show RGB settings only when rgb_mod_installed is true - if(app->notification->settings.rgb_mod_installed) { - // RGB Colors - item = variable_item_list_add( - app->variable_item_list, - "LCD Color", - rgb_backlight_get_color_count(), - color_changed, - app); - value_index = rgb_backlight_get_settings()->display_color_index; - variable_item_set_current_value_index(item, value_index); - variable_item_set_current_value_text(item, rgb_backlight_get_color_text(value_index)); - temp_item = item; - - // Custom Color - REFACTOR THIS - item = variable_item_list_add( - app->variable_item_list, "Custom Red", 255, color_set_custom_red, app); - value_index = rgb_backlight_get_settings()->custom_r; - variable_item_set_current_value_index(item, value_index); - char valtext[4] = {}; - snprintf(valtext, sizeof(valtext), "%d", value_index); - variable_item_set_current_value_text(item, valtext); - - item = variable_item_list_add( - app->variable_item_list, "Custom Green", 255, color_set_custom_green, app); - value_index = rgb_backlight_get_settings()->custom_g; - variable_item_set_current_value_index(item, value_index); - snprintf(valtext, sizeof(valtext), "%d", value_index); - variable_item_set_current_value_text(item, valtext); - - item = variable_item_list_add( - app->variable_item_list, "Custom Blue", 255, color_set_custom_blue, app); - value_index = rgb_backlight_get_settings()->custom_b; - variable_item_set_current_value_index(item, value_index); - snprintf(valtext, sizeof(valtext), "%d", value_index); - variable_item_set_current_value_text(item, valtext); - // End of RGB - } item = variable_item_list_add( app->variable_item_list, "LCD Brightness", BACKLIGHT_COUNT, backlight_changed, app); value_index = value_index_float( @@ -364,17 +405,123 @@ static NotificationAppSettings* alloc_settings(void) { variable_item_set_current_value_text(item, vibro_text[value_index]); } + // RGB settings view + app->variable_item_list_rgb = variable_item_list_alloc(); + View* view_rgb = variable_item_list_get_view(app->variable_item_list_rgb); + // set callback for OK pressed in rgb_settings_menu + view_set_previous_callback(view_rgb, notification_app_rgb_settings_exit); + + // Show RGB_MOD_Installed_Swith only in Debug mode + if(furi_hal_rtc_is_flag_set(FuriHalRtcFlagDebug)) { + item = variable_item_list_add( + app->variable_item_list_rgb, + "RGB MOD Installed", + RGB_MOD_COUNT, + rgb_mod_installed_changed, + app); + value_index = value_index_bool( + app->notification->settings.rgb_mod_installed, rgb_mod_value, RGB_MOD_COUNT); + variable_item_set_current_value_index(item, value_index); + variable_item_set_current_value_text(item, rgb_mod_text[value_index]); + } + + // RGB Colors settings + item = variable_item_list_add( + app->variable_item_list_rgb, + "LCD Color", + rgb_backlight_get_color_count(), + color_changed, + app); + value_index = rgb_backlight_get_settings()->display_color_index; + variable_item_set_current_value_index(item, value_index); + variable_item_set_current_value_text(item, rgb_backlight_get_color_text(value_index)); + variable_item_set_locked( + item, app->notification->settings.rgb_mod_rainbow, "Rainbow mode\nenabled!"); + temp_item = item; + + // Custom Color - REFACTOR THIS + item = variable_item_list_add( + app->variable_item_list_rgb, "Custom Red", 255, color_set_custom_red, app); + value_index = rgb_backlight_get_settings()->custom_r; + variable_item_set_current_value_index(item, value_index); + char valtext[4] = {}; + snprintf(valtext, sizeof(valtext), "%d", value_index); + variable_item_set_current_value_text(item, valtext); + variable_item_set_locked( + item, app->notification->settings.rgb_mod_rainbow, "Rainbow mode\nenabled!"); + + item = variable_item_list_add( + app->variable_item_list_rgb, "Custom Green", 255, color_set_custom_green, app); + value_index = rgb_backlight_get_settings()->custom_g; + variable_item_set_current_value_index(item, value_index); + snprintf(valtext, sizeof(valtext), "%d", value_index); + variable_item_set_current_value_text(item, valtext); + variable_item_set_locked( + item, app->notification->settings.rgb_mod_rainbow, "Rainbow mode\nenabled!"); + + item = variable_item_list_add( + app->variable_item_list_rgb, "Custom Blue", 255, color_set_custom_blue, app); + value_index = rgb_backlight_get_settings()->custom_b; + variable_item_set_current_value_index(item, value_index); + snprintf(valtext, sizeof(valtext), "%d", value_index); + variable_item_set_current_value_text(item, valtext); + variable_item_set_locked( + item, app->notification->settings.rgb_mod_rainbow, "Rainbow mode\nenabled!"); + // End of RGB + + // Rainbow (based on Willy-JL idea) settings + item = variable_item_list_add( + app->variable_item_list_rgb, + "Rainbow mode", + RGB_MOD_RAINBOW_COUNT, + rgb_mod_rainbow_changed, + app); + value_index = value_index_bool( + app->notification->settings.rgb_mod_rainbow, rgb_mod_rainbow_value, RGB_MOD_RAINBOW_COUNT); + variable_item_set_current_value_index(item, value_index); + variable_item_set_current_value_text(item, rgb_mod_rainbow_text[value_index]); + + item = variable_item_list_add( + app->variable_item_list_rgb, + "Rainbow speed", + RGB_MOD_RAINBOW_SPEED_COUNT, + rgb_mod_rainbow_speed_changed, + app); + value_index = value_index_uint32( + app->notification->settings.rgb_mod_rainbow_speed_ms, + rgb_mod_rainbow_speed_value, + RGB_MOD_RAINBOW_SPEED_COUNT); + variable_item_set_current_value_index(item, value_index); + variable_item_set_current_value_text(item, rgb_mod_rainbow_speed_text[value_index]); + + item = variable_item_list_add( + app->variable_item_list_rgb, + "Rainbow step", + RGB_MOD_RAINBOW_STEP_COUNT, + rgb_mod_rainbow_step_changed, + app); + value_index = value_index_uint32( + app->notification->settings.rgb_mod_rainbow_step, + rgb_mod_rainbow_step_value, + RGB_MOD_RAINBOW_SPEED_COUNT); + variable_item_set_current_value_index(item, value_index); + variable_item_set_current_value_text(item, rgb_mod_rainbow_step_text[value_index]); + + // End of Rainbow settings + app->view_dispatcher = view_dispatcher_alloc(); view_dispatcher_attach_to_gui(app->view_dispatcher, app->gui, ViewDispatcherTypeFullscreen); - view_dispatcher_add_view(app->view_dispatcher, 0, view); - view_dispatcher_switch_to_view(app->view_dispatcher, 0); - + view_dispatcher_add_view(app->view_dispatcher, MainViewId, view); + view_dispatcher_add_view(app->view_dispatcher, RGBViewId, view_rgb); + view_dispatcher_switch_to_view(app->view_dispatcher, MainViewId); return app; } static void free_settings(NotificationAppSettings* app) { - view_dispatcher_remove_view(app->view_dispatcher, 0); + view_dispatcher_remove_view(app->view_dispatcher, MainViewId); + view_dispatcher_remove_view(app->view_dispatcher, RGBViewId); variable_item_list_free(app->variable_item_list); + variable_item_list_free(app->variable_item_list_rgb); view_dispatcher_free(app->view_dispatcher); furi_record_close(RECORD_GUI); @@ -387,6 +534,12 @@ int32_t notification_settings_app(void* p) { NotificationAppSettings* app = alloc_settings(); view_dispatcher_run(app->view_dispatcher); notification_message_save_settings(app->notification); + + // Automaticaly switch_off debug_mode when user exit from settings with enabled rgb_mod_installed + // if(app->notification->settings.rgb_mod_installed) { + // furi_hal_rtc_reset_flag(FuriHalRtcFlagDebug); + // } + free_settings(app); return 0; -} + } From 82fd6b213093226e232d0534396c62f74b3c29fe Mon Sep 17 00:00:00 2001 From: Dmitry422 Date: Tue, 4 Mar 2025 23:34:11 +0700 Subject: [PATCH 106/268] RGB Rainbow still in development. --- .../services/notification/notification_app.c | 93 ++++++++++++------- .../services/notification/notification_app.h | 6 +- 2 files changed, 64 insertions(+), 35 deletions(-) diff --git a/applications/services/notification/notification_app.c b/applications/services/notification/notification_app.c index 229f7c5ea..3811c2fde 100644 --- a/applications/services/notification/notification_app.c +++ b/applications/services/notification/notification_app.c @@ -189,6 +189,61 @@ static void notification_display_timer(void* ctx) { notification_message(app, &sequence_display_backlight_off); } +// RGB MOD RAINBOW SECTION + +//start furi timer for rgb_mod_rainbow +static void rgb_mod_rainbow_timer_start(NotificationApp* app) { + furi_timer_start( + app->rgb_mod_rainbow_timer, furi_ms_to_ticks(app->settings.rgb_mod_rainbow_speed_ms)); +} + +//stop furi timer for rgb_mod_rainbow +static void rgb_mod_rainbow_timer_stop(NotificationApp* app) { + furi_timer_stop(app->rgb_mod_rainbow_timer); +} + +// start/stop rgb_mod_rainbow_timer only if rgb_mod_installed +static void rgb_mod_rainbow_timer_control(NotificationApp* app) { + if(app->settings.rgb_mod_installed) { + if(app->settings.rgb_mod_rainbow) { + rgb_mod_rainbow_timer_start(app); + } else { + if(furi_timer_is_running(app->rgb_mod_rainbow_timer)) { + rgb_mod_rainbow_timer_stop(app); + } + } + } +} + +// callback for rgb_mod_rainbow_timer (what we do when timer end) +static void rgb_mod_rainbow_timer_callback(void* context) { + furi_assert(context); + NotificationApp* app = context; + +// УЧЕСТЬ СТЕП и его вероятность превысить 255 +// При выключении радуги активировать настроенный в меню цвет + + app->rgb_mod_rainbow_color3++; + + if(app->rgb_mod_rainbow_color3 == 255) { + app->rgb_mod_rainbow_color2++; + app->rgb_mod_rainbow_color3 = 1; + } + + if(app->rgb_mod_rainbow_color2 == 255) { + app->rgb_mod_rainbow_color1++; + app->rgb_mod_rainbow_color2 = 1; + } + if(app->rgb_mod_rainbow_color1 == 255) { + app->rgb_mod_rainbow_color1 = 1; + } + FURI_LOG_I("RAINBOW", "Color3 %u", app->rgb_mod_rainbow_color3); + FURI_LOG_I("RAINBOW", "Color2 %u", app->rgb_mod_rainbow_color2); + FURI_LOG_I("RAINBOW", "Color1 %u", app->rgb_mod_rainbow_color1); +} + +// END OF RGB MOD RAINBOW SECTION + // message processing static void notification_process_notification_message( NotificationApp* app, @@ -219,12 +274,18 @@ static void notification_process_notification_message( &app->display, notification_message->data.led.value * display_brightness_setting); reset_mask |= reset_display_mask; + //start rgb_mod_rainbow_timer when display backlight is ON + rgb_mod_rainbow_timer_control(app); } else { reset_mask &= ~reset_display_mask; notification_reset_notification_led_layer(&app->display); if(furi_timer_is_running(app->display_timer)) { furi_timer_stop(app->display_timer); } + //stop rgb_mod_rainbow_timer when display backlight is OFF + if(furi_timer_is_running(app->rgb_mod_rainbow_timer)) { + rgb_mod_rainbow_timer_stop(app); + } } break; case NotificationMessageTypeLedDisplayBacklightEnforceOn: @@ -512,38 +573,6 @@ static void input_event_callback(const void* value, void* context) { notification_message(app, &sequence_display_backlight_on); } -// RGB MOD RAINBOW SECTION - -//start furi timer for rgb_mod_rainbow -static void rgb_mod_rainbow_timer_start(NotificationApp* app) { - furi_timer_start( - app->rgb_mod_rainbow_timer, furi_ms_to_ticks(app->settings.rgb_mod_rainbow_speed_ms)); -} - -//stop furi timer for rgb_mod_rainbow -static void rgb_mod_rainbow_timer_stop(NotificationApp* app) { - furi_timer_stop(app->rgb_mod_rainbow_timer); -} - -// start/stop rgb_mod_rainbow_timer only if rgb_mod_installed -static void rgb_mod_rainbow_timer_control(NotificationApp* app) { - if(app->settings.rgb_mod_installed) { - if(app->settings.rgb_mod_rainbow) { - rgb_mod_rainbow_timer_start(app); - } else { - rgb_mod_rainbow_timer_stop(app); - } - } -} - -// callback for rgb_mod_rainbow_timer (what we do when timer end) -static void rgb_mod_rainbow_timer_callback(void* context) { - furi_assert(context); - FURI_LOG_I("RAINBOW", "Rainbow timer callback - change color"); -} - -// END OF RGB MOD RAINBOW SECTION - // App alloc static NotificationApp* notification_app_alloc(void) { NotificationApp* app = malloc(sizeof(NotificationApp)); diff --git a/applications/services/notification/notification_app.h b/applications/services/notification/notification_app.h index 098ef5ee7..13cfb7aac 100644 --- a/applications/services/notification/notification_app.h +++ b/applications/services/notification/notification_app.h @@ -63,9 +63,9 @@ struct NotificationApp { // rainbow mode section FuriTimer* rgb_mod_rainbow_timer; - int8_t rgb_mod_rainbow_color1; - int8_t rgb_mod_rainbow_color2; - int8_t rgb_mod_rainbow_color3; + uint8_t rgb_mod_rainbow_color1; + uint8_t rgb_mod_rainbow_color2; + uint8_t rgb_mod_rainbow_color3; NotificationSettings settings; From cef20b3a5ef4596b010776a19df273f92eb8504c Mon Sep 17 00:00:00 2001 From: WillyJL <49810075+Willy-JL@users.noreply.github.com> Date: Wed, 5 Mar 2025 14:11:51 +0000 Subject: [PATCH 107/268] JS: Fix gui.js stopwatch example borders (#4131) --- applications/system/js_app/examples/apps/Scripts/gui.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/applications/system/js_app/examples/apps/Scripts/gui.js b/applications/system/js_app/examples/apps/Scripts/gui.js index 16673524a..bc63a7ef6 100644 --- a/applications/system/js_app/examples/apps/Scripts/gui.js +++ b/applications/system/js_app/examples/apps/Scripts/gui.js @@ -19,8 +19,8 @@ let jsLogo = icon.getBuiltin("js_script_10px"); let stopwatchWidgetElements = [ { element: "string", x: 67, y: 44, align: "bl", font: "big_numbers", text: "00 00" }, { element: "string", x: 77, y: 22, align: "bl", font: "primary", text: "Stopwatch" }, - { element: "frame", x: 64, y: 27, w: 28, h: 20, radius: 3, fill: false }, - { element: "frame", x: 100, y: 27, w: 28, h: 20, radius: 3, fill: false }, + { element: "rect", x: 64, y: 27, w: 28, h: 20, radius: 3, fill: false }, + { element: "rect", x: 100, y: 27, w: 28, h: 20, radius: 3, fill: false }, { element: "icon", x: 0, y: 5, iconData: cuteDolphinWithWatch }, { element: "icon", x: 64, y: 13, iconData: jsLogo }, { element: "button", button: "right", text: "Back" }, From bf50f0cd2099f6023e38e0f2865610a80ffea2be Mon Sep 17 00:00:00 2001 From: Dmitry422 Date: Wed, 5 Mar 2025 22:47:28 +0700 Subject: [PATCH 108/268] Cosmetic changes. --- .../services/notification/notification_app.c | 43 +++++++++++-------- .../services/notification/notification_app.h | 9 ++-- 2 files changed, 28 insertions(+), 24 deletions(-) diff --git a/applications/services/notification/notification_app.c b/applications/services/notification/notification_app.c index 3811c2fde..c9c1faa3b 100644 --- a/applications/services/notification/notification_app.c +++ b/applications/services/notification/notification_app.c @@ -220,26 +220,31 @@ static void rgb_mod_rainbow_timer_callback(void* context) { furi_assert(context); NotificationApp* app = context; -// УЧЕСТЬ СТЕП и его вероятность превысить 255 // При выключении радуги активировать настроенный в меню цвет - app->rgb_mod_rainbow_color3++; - - if(app->rgb_mod_rainbow_color3 == 255) { - app->rgb_mod_rainbow_color2++; - app->rgb_mod_rainbow_color3 = 1; + if(app->rgb_mod_rainbow_red >= 255) { + app->rgb_mod_rainbow_red = 1; + app->rgb_mod_rainbow_green += app->settings.rgb_mod_rainbow_step; } - if(app->rgb_mod_rainbow_color2 == 255) { - app->rgb_mod_rainbow_color1++; - app->rgb_mod_rainbow_color2 = 1; + if(app->rgb_mod_rainbow_green >= 255) { + app->rgb_mod_rainbow_green = 1; + app->rgb_mod_rainbow_blue += app->settings.rgb_mod_rainbow_step; } - if(app->rgb_mod_rainbow_color1 == 255) { - app->rgb_mod_rainbow_color1 = 1; + + if(app->rgb_mod_rainbow_blue >= 255) { + app->rgb_mod_rainbow_blue = 1; } - FURI_LOG_I("RAINBOW", "Color3 %u", app->rgb_mod_rainbow_color3); - FURI_LOG_I("RAINBOW", "Color2 %u", app->rgb_mod_rainbow_color2); - FURI_LOG_I("RAINBOW", "Color1 %u", app->rgb_mod_rainbow_color1); + + rgb_backlight_set_custom_color(app->rgb_mod_rainbow_red, 0); + rgb_backlight_set_custom_color(app->rgb_mod_rainbow_green, 1); + rgb_backlight_set_custom_color(app->rgb_mod_rainbow_blue, 2); + + FURI_LOG_I("RAINBOW", "RED %u", app->rgb_mod_rainbow_red); + FURI_LOG_I("RAINBOW", "GREEN %u", app->rgb_mod_rainbow_green); + FURI_LOG_I("RAINBOW", "BLUE %u", app->rgb_mod_rainbow_blue); + + app->rgb_mod_rainbow_red += app->settings.rgb_mod_rainbow_step; } // END OF RGB MOD RAINBOW SECTION @@ -274,7 +279,7 @@ static void notification_process_notification_message( &app->display, notification_message->data.led.value * display_brightness_setting); reset_mask |= reset_display_mask; - //start rgb_mod_rainbow_timer when display backlight is ON + //start rgb_mod_rainbow_timer when display backlight is ON and all corresponding settings is ON too rgb_mod_rainbow_timer_control(app); } else { reset_mask &= ~reset_display_mask; @@ -621,9 +626,9 @@ static NotificationApp* notification_app_alloc(void) { app->settings.rgb_mod_rainbow = false; app->settings.rgb_mod_rainbow_speed_ms = 1000; app->settings.rgb_mod_rainbow_step = 1; - app->rgb_mod_rainbow_color1 = 1; - app->rgb_mod_rainbow_color2 = 1; - app->rgb_mod_rainbow_color3 = 1; + app->rgb_mod_rainbow_red = 1; + app->rgb_mod_rainbow_green = 1; + app->rgb_mod_rainbow_blue = 1; //define rgb_mod_rainbow_timer and they callback app->rgb_mod_rainbow_timer = @@ -699,7 +704,7 @@ int32_t notification_srv(void* p) { case SaveSettingsMessage: notification_save_settings(app); rgb_backlight_save_settings(); - //call rgb_mod_timer_control when we save settings + //call rgb_mod_timer_control (start or stop) when we save settings rgb_mod_rainbow_timer_control(app); break; case LoadSettingsMessage: diff --git a/applications/services/notification/notification_app.h b/applications/services/notification/notification_app.h index 13cfb7aac..5220abcd9 100644 --- a/applications/services/notification/notification_app.h +++ b/applications/services/notification/notification_app.h @@ -48,7 +48,7 @@ typedef struct { bool rgb_mod_installed; bool rgb_mod_rainbow; uint32_t rgb_mod_rainbow_speed_ms; - uint32_t rgb_mod_rainbow_step; + uint16_t rgb_mod_rainbow_step; } NotificationSettings; @@ -63,11 +63,10 @@ struct NotificationApp { // rainbow mode section FuriTimer* rgb_mod_rainbow_timer; - uint8_t rgb_mod_rainbow_color1; - uint8_t rgb_mod_rainbow_color2; - uint8_t rgb_mod_rainbow_color3; + uint16_t rgb_mod_rainbow_red; + uint16_t rgb_mod_rainbow_green; + uint16_t rgb_mod_rainbow_blue; - NotificationSettings settings; }; From 5e6c2383895d40244a746c51689daa6ff0299431 Mon Sep 17 00:00:00 2001 From: Dmitry422 Date: Mon, 10 Mar 2025 20:51:09 +0700 Subject: [PATCH 109/268] RGB mod rainbow effect done. --- .../services/notification/notification_app.c | 133 ++++++++++++------ .../services/notification/notification_app.h | 19 +-- .../notification_settings_app.c | 95 +++++++------ .../notification_settings/rgb_backlight.c | 12 ++ .../notification_settings/rgb_backlight.h | 3 + 5 files changed, 169 insertions(+), 93 deletions(-) diff --git a/applications/services/notification/notification_app.c b/applications/services/notification/notification_app.c index c9c1faa3b..8096c2cc3 100644 --- a/applications/services/notification/notification_app.c +++ b/applications/services/notification/notification_app.c @@ -189,7 +189,7 @@ static void notification_display_timer(void* ctx) { notification_message(app, &sequence_display_backlight_off); } -// RGB MOD RAINBOW SECTION +// --- RGB MOD RAINBOW SECTION --- //start furi timer for rgb_mod_rainbow static void rgb_mod_rainbow_timer_start(NotificationApp* app) { @@ -202,10 +202,15 @@ static void rgb_mod_rainbow_timer_stop(NotificationApp* app) { furi_timer_stop(app->rgb_mod_rainbow_timer); } -// start/stop rgb_mod_rainbow_timer only if rgb_mod_installed -static void rgb_mod_rainbow_timer_control(NotificationApp* app) { +// start/restart/stop rgb_mod_rainbow_timer only if rgb_mod_installed and apply rainbow colors to backlight +static void rgb_mod_rainbow_timer_starter(NotificationApp* app) { if(app->settings.rgb_mod_installed) { - if(app->settings.rgb_mod_rainbow) { + if(app->settings.rgb_mod_rainbow_mode > 0) { + rgb_mod_rainbow_update( + app->rgb_mod_rainbow_red, + app->rgb_mod_rainbow_green, + app->rgb_mod_rainbow_blue, + app->settings.display_brightness); rgb_mod_rainbow_timer_start(app); } else { if(furi_timer_is_running(app->rgb_mod_rainbow_timer)) { @@ -220,34 +225,74 @@ static void rgb_mod_rainbow_timer_callback(void* context) { furi_assert(context); NotificationApp* app = context; -// При выключении радуги активировать настроенный в меню цвет + // if rgb_mode_rainbow_mode is rainbow do rainbow effect + if(app->settings.rgb_mod_rainbow_mode == 1) { + switch(app->rgb_mod_rainbow_stage) { + // from red to yellow + case 1: + app->rgb_mod_rainbow_green += app->settings.rgb_mod_rainbow_step; + if(app->rgb_mod_rainbow_green >= 255) { + app->rgb_mod_rainbow_green = 255; + app->rgb_mod_rainbow_stage++; + } + break; + // yellow red to green + case 2: + app->rgb_mod_rainbow_red -= app->settings.rgb_mod_rainbow_step; + if(app->rgb_mod_rainbow_red <= 0) { + app->rgb_mod_rainbow_red = 0; + app->rgb_mod_rainbow_stage++; + } + break; + // from green to light blue + case 3: + app->rgb_mod_rainbow_blue += app->settings.rgb_mod_rainbow_step; + if(app->rgb_mod_rainbow_blue >= 255) { + app->rgb_mod_rainbow_blue = 255; + app->rgb_mod_rainbow_stage++; + } + break; + //from light blue to blue + case 4: + app->rgb_mod_rainbow_green -= app->settings.rgb_mod_rainbow_step; + if(app->rgb_mod_rainbow_green <= 0) { + app->rgb_mod_rainbow_green = 0; + app->rgb_mod_rainbow_stage++; + } + break; + //from blue to violet + case 5: + app->rgb_mod_rainbow_red += app->settings.rgb_mod_rainbow_step; + if(app->rgb_mod_rainbow_red >= 255) { + app->rgb_mod_rainbow_red = 255; + app->rgb_mod_rainbow_stage++; + } + break; + //from violet to red + case 6: + app->rgb_mod_rainbow_blue -= app->settings.rgb_mod_rainbow_step; + if(app->rgb_mod_rainbow_blue <= 0) { + app->rgb_mod_rainbow_blue = 0; + app->rgb_mod_rainbow_stage = 1; + } + break; + default: + break; + } - if(app->rgb_mod_rainbow_red >= 255) { - app->rgb_mod_rainbow_red = 1; - app->rgb_mod_rainbow_green += app->settings.rgb_mod_rainbow_step; + rgb_mod_rainbow_update( + app->rgb_mod_rainbow_red, + app->rgb_mod_rainbow_green, + app->rgb_mod_rainbow_blue, + app->settings.display_brightness); } - if(app->rgb_mod_rainbow_green >= 255) { - app->rgb_mod_rainbow_green = 1; - app->rgb_mod_rainbow_blue += app->settings.rgb_mod_rainbow_step; - } - - if(app->rgb_mod_rainbow_blue >= 255) { - app->rgb_mod_rainbow_blue = 1; - } - - rgb_backlight_set_custom_color(app->rgb_mod_rainbow_red, 0); - rgb_backlight_set_custom_color(app->rgb_mod_rainbow_green, 1); - rgb_backlight_set_custom_color(app->rgb_mod_rainbow_blue, 2); - - FURI_LOG_I("RAINBOW", "RED %u", app->rgb_mod_rainbow_red); - FURI_LOG_I("RAINBOW", "GREEN %u", app->rgb_mod_rainbow_green); - FURI_LOG_I("RAINBOW", "BLUE %u", app->rgb_mod_rainbow_blue); - - app->rgb_mod_rainbow_red += app->settings.rgb_mod_rainbow_step; + // if rgb_mode_rainbow_mode is ..... do another effect + // if(app->settings.rgb_mod_rainbow_mode == 2) { + // } } -// END OF RGB MOD RAINBOW SECTION +// --- END OF RGB MOD RAINBOW SECTION --- // message processing static void notification_process_notification_message( @@ -279,8 +324,10 @@ static void notification_process_notification_message( &app->display, notification_message->data.led.value * display_brightness_setting); reset_mask |= reset_display_mask; + //start rgb_mod_rainbow_timer when display backlight is ON and all corresponding settings is ON too - rgb_mod_rainbow_timer_control(app); + rgb_mod_rainbow_timer_starter(app); + } else { reset_mask &= ~reset_display_mask; notification_reset_notification_led_layer(&app->display); @@ -617,25 +664,23 @@ static NotificationApp* notification_app_alloc(void) { furi_pubsub_subscribe(app->event_record, input_event_callback, app); notification_message(app, &sequence_display_backlight_on); - //RGB MOD SECTION + // --- RGB MOD INIT SETTINGS SECTION --- - //rgb mod app->settings.rgb_mod_installed = false; - - //rgb mod rainbow init settings - app->settings.rgb_mod_rainbow = false; - app->settings.rgb_mod_rainbow_speed_ms = 1000; - app->settings.rgb_mod_rainbow_step = 1; - app->rgb_mod_rainbow_red = 1; - app->rgb_mod_rainbow_green = 1; - app->rgb_mod_rainbow_blue = 1; + app->settings.rgb_mod_rainbow_mode = 0; + app->settings.rgb_mod_rainbow_speed_ms = 100; + app->settings.rgb_mod_rainbow_step = 5; + app->rgb_mod_rainbow_red = 255; + app->rgb_mod_rainbow_green = 0; + app->rgb_mod_rainbow_blue = 0; + app->rgb_mod_rainbow_stage = 1; //define rgb_mod_rainbow_timer and they callback app->rgb_mod_rainbow_timer = furi_timer_alloc(rgb_mod_rainbow_timer_callback, FuriTimerTypePeriodic, app); return app; - // END OF RGB SECTION + // --- END OF RGB MOD INIT SETTINGS SECTION --- } static void notification_storage_callback(const void* message, void* context) { @@ -658,8 +703,8 @@ static void notification_apply_settings(NotificationApp* app) { } notification_apply_lcd_contrast(app); - // on system init start rgb_mod_rainbow_timer if they ON in config - rgb_mod_rainbow_timer_control(app); + //start rgb_mod_rainbow_timer on system init if they ON in config + rgb_mod_rainbow_timer_starter(app); } static void notification_init_settings(NotificationApp* app) { @@ -702,10 +747,10 @@ int32_t notification_srv(void* p) { notification_process_internal_message(app, &message); break; case SaveSettingsMessage: - notification_save_settings(app); - rgb_backlight_save_settings(); //call rgb_mod_timer_control (start or stop) when we save settings - rgb_mod_rainbow_timer_control(app); + rgb_mod_rainbow_timer_starter(app); + rgb_backlight_save_settings(); + notification_save_settings(app); break; case LoadSettingsMessage: notification_load_settings(app); diff --git a/applications/services/notification/notification_app.h b/applications/services/notification/notification_app.h index 5220abcd9..54b3ab7a9 100644 --- a/applications/services/notification/notification_app.h +++ b/applications/services/notification/notification_app.h @@ -44,11 +44,12 @@ typedef struct { uint32_t display_off_delay_ms; int8_t contrast; bool vibro_on; - + /// --- RGB MOD SETTINGS SECTION --- bool rgb_mod_installed; - bool rgb_mod_rainbow; + uint32_t rgb_mod_rainbow_mode; uint32_t rgb_mod_rainbow_speed_ms; uint16_t rgb_mod_rainbow_step; + /// --- END OF RGB MOD SETTINGS SECTION --- } NotificationSettings; @@ -61,13 +62,15 @@ struct NotificationApp { NotificationLedLayer led[NOTIFICATION_LED_COUNT]; uint8_t display_led_lock; - // rainbow mode section + // --- RGB RAINBOW MODE VARIABLES SECTION --- FuriTimer* rgb_mod_rainbow_timer; - uint16_t rgb_mod_rainbow_red; - uint16_t rgb_mod_rainbow_green; - uint16_t rgb_mod_rainbow_blue; - + int16_t rgb_mod_rainbow_red; + int16_t rgb_mod_rainbow_green; + int16_t rgb_mod_rainbow_blue; + uint8_t rgb_mod_rainbow_stage; + // --- ENd OF RGB RAINBOW MODE VARIABLES SECTION --- + NotificationSettings settings; }; -void notification_message_save_settings(NotificationApp* app); +void notification_message_save_settings(NotificationApp* app); \ No newline at end of file diff --git a/applications/settings/notification_settings/notification_settings_app.c b/applications/settings/notification_settings/notification_settings_app.c index 941284c89..0def6769b 100644 --- a/applications/settings/notification_settings/notification_settings_app.c +++ b/applications/settings/notification_settings/notification_settings_app.c @@ -116,44 +116,36 @@ const char* const rgb_mod_text[RGB_MOD_COUNT] = { }; const bool rgb_mod_value[RGB_MOD_COUNT] = {false, true}; -#define RGB_MOD_RAINBOW_COUNT 2 -const char* const rgb_mod_rainbow_text[RGB_MOD_RAINBOW_COUNT] = { +#define RGB_MOD_RAINBOW_MODE_COUNT 2 +const char* const rgb_mod_rainbow_mode_text[RGB_MOD_RAINBOW_MODE_COUNT] = { "OFF", - "ON", + "Rainbow", }; -const bool rgb_mod_rainbow_value[RGB_MOD_RAINBOW_COUNT] = {false, true}; +const uint32_t rgb_mod_rainbow_mode_value[RGB_MOD_RAINBOW_MODE_COUNT] = {0, 1}; -#define RGB_MOD_RAINBOW_SPEED_COUNT 14 +#define RGB_MOD_RAINBOW_SPEED_COUNT 20 const char* const rgb_mod_rainbow_speed_text[RGB_MOD_RAINBOW_SPEED_COUNT] = { - "0.1s", - "0.2s", - "0.3s", - "0.4s", - "0.6s", - "0.8s", - "1s", - "1.2s", - "1.4s", - "1.6s", - "1.8s", - "2s", - "2.5s", - "3s"}; -const uint32_t rgb_mod_rainbow_speed_value[RGB_MOD_RAINBOW_SPEED_COUNT] = - {100, 200, 400, 600, 800, 1000, 1200, 1400, 1600, 1800, 2000, 2500, 3000}; + "0.1s", "0.2s", "0.3s", "0.4s", "0.5s", "0.6s", "0.7", "0.8", "0.9", "1s", + "1.1s", "1.2s", "1.3s", "1.4s", "1.5s", "1.6s", "1.7s", "1.8s", "1.9s", "2s"}; +const uint32_t rgb_mod_rainbow_speed_value[RGB_MOD_RAINBOW_SPEED_COUNT] = { + 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000, + 1100, 1200, 1300, 1400, 1500, 1600, 1700, 1800, 1900, 2000}; -#define RGB_MOD_RAINBOW_STEP_COUNT 7 +#define RGB_MOD_RAINBOW_STEP_COUNT 10 const char* const rgb_mod_rainbow_step_text[RGB_MOD_RAINBOW_SPEED_COUNT] = { "1", "2", + "3", + "4", "5", + "6", "7", + "8", + "9", "10", - "15", - "20", - }; +}; const uint32_t rgb_mod_rainbow_step_value[RGB_MOD_RAINBOW_STEP_COUNT] = - {1, 2, 5, 7, 10, 15, 20}; + {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; typedef enum { MainViewId, @@ -234,8 +226,25 @@ static void rgb_mod_installed_changed(VariableItem* item) { static void rgb_mod_rainbow_changed(VariableItem* item) { NotificationAppSettings* app = variable_item_get_context(item); uint8_t index = variable_item_get_current_value_index(item); - variable_item_set_current_value_text(item, rgb_mod_rainbow_text[index]); - app->notification->settings.rgb_mod_rainbow = rgb_mod_rainbow_value[index]; + variable_item_set_current_value_text(item, rgb_mod_rainbow_mode_text[index]); + app->notification->settings.rgb_mod_rainbow_mode = rgb_mod_rainbow_mode_value[index]; + + // Lock/Unlock color settings if rainbow mode Enabled/Disabled + for(int i = 0; i < 4; i++) { + VariableItem* t_item = variable_item_list_get(app->variable_item_list_rgb, i); + if(app->notification->settings.rgb_mod_rainbow_mode > 0) { + variable_item_set_locked(t_item, true, "Rainbow mode\nenabled!"); + } else { + variable_item_set_locked(t_item, false, "Rainbow mode\nenabled!"); + } + } + //save settings and start/stop rgb_mod_rainbow_timer + notification_message_save_settings(app->notification); + + // restore saved rgb backlight settings if we switch_off rainbow mode + if(app->notification->settings.rgb_mod_rainbow_mode == 0) { + rgb_backlight_update(app->notification->settings.display_brightness * 255, true); + } } static void rgb_mod_rainbow_speed_changed(VariableItem* item) { @@ -243,6 +252,8 @@ static void rgb_mod_rainbow_speed_changed(VariableItem* item) { uint8_t index = variable_item_get_current_value_index(item); variable_item_set_current_value_text(item, rgb_mod_rainbow_speed_text[index]); app->notification->settings.rgb_mod_rainbow_speed_ms = rgb_mod_rainbow_speed_value[index]; + //use message for restart rgb_mod_rainbow_timer with new delay + notification_message_save_settings(app->notification); } static void rgb_mod_rainbow_step_changed(VariableItem* item) { @@ -316,7 +327,8 @@ void variable_item_list_enter_callback(void* context, uint32_t index) { NotificationAppSettings* app = context; if(((app->notification->settings.rgb_mod_installed) || - (furi_hal_rtc_is_flag_set(FuriHalRtcFlagDebug))) && (index == 0)) { + (furi_hal_rtc_is_flag_set(FuriHalRtcFlagDebug))) && + (index == 0)) { view_dispatcher_switch_to_view(app->view_dispatcher, RGBViewId); } } @@ -405,7 +417,7 @@ static NotificationAppSettings* alloc_settings(void) { variable_item_set_current_value_text(item, vibro_text[value_index]); } - // RGB settings view + // --- RGB SETTINGS VIEW --- app->variable_item_list_rgb = variable_item_list_alloc(); View* view_rgb = variable_item_list_get_view(app->variable_item_list_rgb); // set callback for OK pressed in rgb_settings_menu @@ -436,7 +448,7 @@ static NotificationAppSettings* alloc_settings(void) { variable_item_set_current_value_index(item, value_index); variable_item_set_current_value_text(item, rgb_backlight_get_color_text(value_index)); variable_item_set_locked( - item, app->notification->settings.rgb_mod_rainbow, "Rainbow mode\nenabled!"); + item, (app->notification->settings.rgb_mod_rainbow_mode > 0), "Rainbow mode\nenabled!"); temp_item = item; // Custom Color - REFACTOR THIS @@ -448,7 +460,7 @@ static NotificationAppSettings* alloc_settings(void) { snprintf(valtext, sizeof(valtext), "%d", value_index); variable_item_set_current_value_text(item, valtext); variable_item_set_locked( - item, app->notification->settings.rgb_mod_rainbow, "Rainbow mode\nenabled!"); + item, (app->notification->settings.rgb_mod_rainbow_mode > 0), "Rainbow mode\nenabled!"); item = variable_item_list_add( app->variable_item_list_rgb, "Custom Green", 255, color_set_custom_green, app); @@ -457,7 +469,7 @@ static NotificationAppSettings* alloc_settings(void) { snprintf(valtext, sizeof(valtext), "%d", value_index); variable_item_set_current_value_text(item, valtext); variable_item_set_locked( - item, app->notification->settings.rgb_mod_rainbow, "Rainbow mode\nenabled!"); + item, (app->notification->settings.rgb_mod_rainbow_mode > 0), "Rainbow mode\nenabled!"); item = variable_item_list_add( app->variable_item_list_rgb, "Custom Blue", 255, color_set_custom_blue, app); @@ -466,20 +478,21 @@ static NotificationAppSettings* alloc_settings(void) { snprintf(valtext, sizeof(valtext), "%d", value_index); variable_item_set_current_value_text(item, valtext); variable_item_set_locked( - item, app->notification->settings.rgb_mod_rainbow, "Rainbow mode\nenabled!"); - // End of RGB + item, (app->notification->settings.rgb_mod_rainbow_mode > 0), "Rainbow mode\nenabled!"); // Rainbow (based on Willy-JL idea) settings item = variable_item_list_add( app->variable_item_list_rgb, "Rainbow mode", - RGB_MOD_RAINBOW_COUNT, + RGB_MOD_RAINBOW_MODE_COUNT, rgb_mod_rainbow_changed, app); - value_index = value_index_bool( - app->notification->settings.rgb_mod_rainbow, rgb_mod_rainbow_value, RGB_MOD_RAINBOW_COUNT); + value_index = value_index_uint32( + app->notification->settings.rgb_mod_rainbow_mode, + rgb_mod_rainbow_mode_value, + RGB_MOD_RAINBOW_MODE_COUNT); variable_item_set_current_value_index(item, value_index); - variable_item_set_current_value_text(item, rgb_mod_rainbow_text[value_index]); + variable_item_set_current_value_text(item, rgb_mod_rainbow_mode_text[value_index]); item = variable_item_list_add( app->variable_item_list_rgb, @@ -507,7 +520,7 @@ static NotificationAppSettings* alloc_settings(void) { variable_item_set_current_value_index(item, value_index); variable_item_set_current_value_text(item, rgb_mod_rainbow_step_text[value_index]); - // End of Rainbow settings + // --- End of RGB SETTING VIEW --- app->view_dispatcher = view_dispatcher_alloc(); view_dispatcher_attach_to_gui(app->view_dispatcher, app->gui, ViewDispatcherTypeFullscreen); @@ -542,4 +555,4 @@ int32_t notification_settings_app(void* p) { free_settings(app); return 0; - } +} diff --git a/applications/settings/notification_settings/rgb_backlight.c b/applications/settings/notification_settings/rgb_backlight.c index 4edd77568..4fcb898f7 100644 --- a/applications/settings/notification_settings/rgb_backlight.c +++ b/applications/settings/notification_settings/rgb_backlight.c @@ -215,3 +215,15 @@ void rgb_backlight_update(uint8_t brightness, bool bypass) { SK6805_update(); } + +// --- RGB MOD RAINBOW --- +void rgb_mod_rainbow_update(uint8_t red, uint8_t green, uint8_t blue, float brightness) { + for(uint8_t i = 0; i < SK6805_get_led_count(); i++) { + uint8_t r = red * (brightness); + uint8_t g = green * (brightness); + uint8_t b = blue * (brightness); + SK6805_set_led_color(i, r, g, b); + } + SK6805_update(); +} +// --- END OF RGB MOD RAINBOW --- diff --git a/applications/settings/notification_settings/rgb_backlight.h b/applications/settings/notification_settings/rgb_backlight.h index f215ed312..66e0e26a1 100644 --- a/applications/settings/notification_settings/rgb_backlight.h +++ b/applications/settings/notification_settings/rgb_backlight.h @@ -89,3 +89,6 @@ uint8_t rgb_backlight_get_color_count(void); * @return Указатель на строку с названием цвета */ const char* rgb_backlight_get_color_text(uint8_t index); + +// set custom color to display; +void rgb_mod_rainbow_update(uint8_t red, uint8_t green, uint8_t blue, float brightness); From 82d3f452cd8093fff0d30f6dc4b94bb46c625b68 Mon Sep 17 00:00:00 2001 From: Dmitry422 Date: Mon, 10 Mar 2025 21:02:37 +0700 Subject: [PATCH 110/268] Code cosmetic changes. --- .../notification_settings_app.c | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/applications/settings/notification_settings/notification_settings_app.c b/applications/settings/notification_settings/notification_settings_app.c index 0def6769b..da54ea8f2 100644 --- a/applications/settings/notification_settings/notification_settings_app.c +++ b/applications/settings/notification_settings/notification_settings_app.c @@ -109,6 +109,7 @@ const char* const vibro_text[VIBRO_COUNT] = { }; const bool vibro_value[VIBRO_COUNT] = {false, true}; +// --- RGB MOD RAINBOW --- #define RGB_MOD_COUNT 2 const char* const rgb_mod_text[RGB_MOD_COUNT] = { "OFF", @@ -152,6 +153,8 @@ typedef enum { RGBViewId, } ViewId; +// --- END OF RGB MOD RAINBOW --- + static void contrast_changed(VariableItem* item) { NotificationAppSettings* app = variable_item_get_context(item); uint8_t index = variable_item_get_current_value_index(item); @@ -216,6 +219,8 @@ static void vibro_changed(VariableItem* item) { notification_message(app->notification, &sequence_single_vibro); } +// --- RGB MOD AND RAINBOW --- + static void rgb_mod_installed_changed(VariableItem* item) { NotificationAppSettings* app = variable_item_get_context(item); uint8_t index = variable_item_get_current_value_index(item); @@ -316,11 +321,6 @@ static void color_set_custom_blue(VariableItem* item) { notification_message(app->notification, &sequence_display_backlight_on); } -static uint32_t notification_app_settings_exit(void* context) { - UNUSED(context); - return VIEW_NONE; -} - // open rgb_settings_view if user press OK on first (index=0) menu string and (debug mode or rgb_mod_install is true) void variable_item_list_enter_callback(void* context, uint32_t index) { UNUSED(context); @@ -338,6 +338,12 @@ static uint32_t notification_app_rgb_settings_exit(void* context) { UNUSED(context); return MainViewId; } +// --- END OF RGB MOD AND RAINBOW --- + +static uint32_t notification_app_settings_exit(void* context) { + UNUSED(context); + return VIEW_NONE; +} static NotificationAppSettings* alloc_settings(void) { NotificationAppSettings* app = malloc(sizeof(NotificationAppSettings)); @@ -346,8 +352,10 @@ static NotificationAppSettings* alloc_settings(void) { app->variable_item_list = variable_item_list_alloc(); View* view = variable_item_list_get_view(app->variable_item_list); + //set callback for exit from view view_set_previous_callback(view, notification_app_settings_exit); + // set callback for OK pressed in menu variable_item_list_set_enter_callback( app->variable_item_list, variable_item_list_enter_callback, app); From 687a6fd630c2f401897f7484c2796a12088d8375 Mon Sep 17 00:00:00 2001 From: Dmitry422 Date: Tue, 11 Mar 2025 15:11:01 +0700 Subject: [PATCH 111/268] User Interface bug removed. --- .../notification_settings/notification_settings_app.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/applications/settings/notification_settings/notification_settings_app.c b/applications/settings/notification_settings/notification_settings_app.c index da54ea8f2..8c6871acd 100644 --- a/applications/settings/notification_settings/notification_settings_app.c +++ b/applications/settings/notification_settings/notification_settings_app.c @@ -234,8 +234,10 @@ static void rgb_mod_rainbow_changed(VariableItem* item) { variable_item_set_current_value_text(item, rgb_mod_rainbow_mode_text[index]); app->notification->settings.rgb_mod_rainbow_mode = rgb_mod_rainbow_mode_value[index]; - // Lock/Unlock color settings if rainbow mode Enabled/Disabled - for(int i = 0; i < 4; i++) { + // Lock/Unlock color settings if rainbow mode Enabled/Disabled (0-3 index if debug off and 1-4 index if debug on) + int slide = 0; + if (furi_hal_rtc_is_flag_set(FuriHalRtcFlagDebug)) {slide = 1;} + for(int i = slide; i < (slide+4); i++) { VariableItem* t_item = variable_item_list_get(app->variable_item_list_rgb, i); if(app->notification->settings.rgb_mod_rainbow_mode > 0) { variable_item_set_locked(t_item, true, "Rainbow mode\nenabled!"); From 9e6593c09e7c57745b1546892fb5f090df78b06a Mon Sep 17 00:00:00 2001 From: Dmitry422 Date: Tue, 11 Mar 2025 18:54:12 +0700 Subject: [PATCH 112/268] Start moving RGB MOD from Notification to RGB MOD service. --- applications/services/application.fam | 1 + .../services/rgb_backlight/application.fam | 11 +++++++++++ .../services/rgb_backlight/rgb_backlight.c | 15 +++++++++++++++ .../services/rgb_backlight/rgb_backlight.h | 0 applications/settings/application.fam | 2 ++ .../rgb_backlight_settings/application.fam | 9 +++++++++ .../rgb_backlight_settings.c | 13 +++++++++++++ .../rgb_backlight_settings.h | 0 lib/drivers/SK6805.c | 4 ++-- targets/f7/api_symbols.csv | 1 + 10 files changed, 54 insertions(+), 2 deletions(-) create mode 100644 applications/services/rgb_backlight/application.fam create mode 100644 applications/services/rgb_backlight/rgb_backlight.c create mode 100644 applications/services/rgb_backlight/rgb_backlight.h create mode 100644 applications/settings/rgb_backlight_settings/application.fam create mode 100644 applications/settings/rgb_backlight_settings/rgb_backlight_settings.c create mode 100644 applications/settings/rgb_backlight_settings/rgb_backlight_settings.h diff --git a/applications/services/application.fam b/applications/services/application.fam index 90631408a..8cfb22cdb 100644 --- a/applications/services/application.fam +++ b/applications/services/application.fam @@ -11,5 +11,6 @@ App( "loader", "power", "namechanger_srv", + "rgb_backlight", ], ) diff --git a/applications/services/rgb_backlight/application.fam b/applications/services/rgb_backlight/application.fam new file mode 100644 index 000000000..168a7bbd0 --- /dev/null +++ b/applications/services/rgb_backlight/application.fam @@ -0,0 +1,11 @@ +App( + appid="rgb_backlight", + name="RgbBackLightSrv", + apptype=FlipperAppType.SERVICE, + entry_point="rgb_backlight_srv", + cdefines=["SRV_RGB_BACKLIGHT"], + stack_size=1 * 1024, + order=99, + sdk_headers=["rgb_backlight.h"], + provides=["rgb_backlight_settings"], +) \ No newline at end of file diff --git a/applications/services/rgb_backlight/rgb_backlight.c b/applications/services/rgb_backlight/rgb_backlight.c new file mode 100644 index 000000000..046cd4771 --- /dev/null +++ b/applications/services/rgb_backlight/rgb_backlight.c @@ -0,0 +1,15 @@ +#include +#include +#include +#include "rgb_backlight.h" + +#define TAG "RGB_BACKLIGHT_SRV" + +int32_t rgb_backlight_srv (void* p){ +UNUSED (p); +while (1){ + FURI_LOG_I (TAG,"working"); + furi_delay_ms (2000); +} +return 0; +} \ No newline at end of file diff --git a/applications/services/rgb_backlight/rgb_backlight.h b/applications/services/rgb_backlight/rgb_backlight.h new file mode 100644 index 000000000..e69de29bb diff --git a/applications/settings/application.fam b/applications/settings/application.fam index 1d6db35a7..8e184a706 100644 --- a/applications/settings/application.fam +++ b/applications/settings/application.fam @@ -6,6 +6,8 @@ App( "passport", "system_settings", "clock_settings", + "input_settings", + "rgb_backlight_settings", "about", ], ) diff --git a/applications/settings/rgb_backlight_settings/application.fam b/applications/settings/rgb_backlight_settings/application.fam new file mode 100644 index 000000000..50621228a --- /dev/null +++ b/applications/settings/rgb_backlight_settings/application.fam @@ -0,0 +1,9 @@ +App( + appid="rgb_backlight_settings", + name="RGB backlight", + apptype=FlipperAppType.SETTINGS, + entry_point="rgb_backlight_settings", + requires=["rgb_backlight"], + stack_size=1 * 1024, + order=110, +) \ No newline at end of file diff --git a/applications/settings/rgb_backlight_settings/rgb_backlight_settings.c b/applications/settings/rgb_backlight_settings/rgb_backlight_settings.c new file mode 100644 index 000000000..096852063 --- /dev/null +++ b/applications/settings/rgb_backlight_settings/rgb_backlight_settings.c @@ -0,0 +1,13 @@ +#include +#include +#include +#include "rgb_backlight_settings.h" + +#define TAG "RGB_BACKLIGHT_SETTINGS" + +int32_t rgb_backlight_settings (void* p){ +UNUSED (p); +FURI_LOG_I (TAG,"Settings"); +furi_delay_ms (2000); +return 0; +} \ No newline at end of file diff --git a/applications/settings/rgb_backlight_settings/rgb_backlight_settings.h b/applications/settings/rgb_backlight_settings/rgb_backlight_settings.h new file mode 100644 index 000000000..e69de29bb diff --git a/lib/drivers/SK6805.c b/lib/drivers/SK6805.c index 8158c55a4..b6f525eb8 100644 --- a/lib/drivers/SK6805.c +++ b/lib/drivers/SK6805.c @@ -58,7 +58,7 @@ void SK6805_set_led_color(uint8_t led_index, uint8_t r, uint8_t g, uint8_t b) { void SK6805_update(void) { SK6805_init(); FURI_CRITICAL_ENTER(); - furi_delay_us(150); + furi_delay_us(100); uint32_t end; /* Последовательная отправка цветов светодиодов */ for(uint8_t lednumber = 0; lednumber < SK6805_LED_COUNT; lednumber++) { @@ -98,6 +98,6 @@ void SK6805_update(void) { } } } - furi_delay_us(150); + furi_delay_us(100); FURI_CRITICAL_EXIT(); } diff --git a/targets/f7/api_symbols.csv b/targets/f7/api_symbols.csv index 7ad21efb5..f78775215 100644 --- a/targets/f7/api_symbols.csv +++ b/targets/f7/api_symbols.csv @@ -39,6 +39,7 @@ Header,+,applications/services/locale/locale.h,, Header,+,applications/services/notification/notification.h,, Header,+,applications/services/notification/notification_messages.h,, Header,+,applications/services/power/power_service/power.h,, +Header,+,applications/services/rgb_backlight/rgb_backlight.h,, Header,+,applications/services/rpc/rpc_app.h,, Header,+,applications/services/storage/storage.h,, Header,+,lib/bit_lib/bit_lib.h,, From 4045628ac6552d4631376d90d417c295fda70d71 Mon Sep 17 00:00:00 2001 From: Dmitry422 Date: Wed, 12 Mar 2025 02:19:17 +0700 Subject: [PATCH 113/268] Working on moving RGB routines from Notification to RGB service --- applications/services/input/input_settings.c | 1 - .../services/rgb_backlight/application.fam | 1 - .../services/rgb_backlight/rgb_backlight.c | 153 +++++++++++++++++- .../services/rgb_backlight/rgb_backlight.h | 44 +++++ .../rgb_backlight/rgb_backlight_settings.c | 95 +++++++++++ .../rgb_backlight/rgb_backlight_settings.h | 31 ++++ applications/settings/application.fam | 1 - .../rgb_backlight_settings/application.fam | 9 -- .../rgb_backlight_settings.c | 13 -- .../rgb_backlight_settings.h | 0 targets/f7/api_symbols.csv | 8 +- 11 files changed, 324 insertions(+), 32 deletions(-) create mode 100644 applications/services/rgb_backlight/rgb_backlight_settings.c create mode 100644 applications/services/rgb_backlight/rgb_backlight_settings.h delete mode 100644 applications/settings/rgb_backlight_settings/application.fam delete mode 100644 applications/settings/rgb_backlight_settings/rgb_backlight_settings.c delete mode 100644 applications/settings/rgb_backlight_settings/rgb_backlight_settings.h diff --git a/applications/services/input/input_settings.c b/applications/services/input/input_settings.c index cd3de6d50..f1f18ba3d 100644 --- a/applications/services/input/input_settings.c +++ b/applications/services/input/input_settings.c @@ -30,7 +30,6 @@ void input_settings_load(InputSettings* settings) { sizeof(InputSettings), INPUT_SETTINGS_MAGIC, INPUT_SETTINGS_VER); - // if config previous version - load it and inicialize new settings } // in case of another config version we exit from useless cycle to next step } while(false); diff --git a/applications/services/rgb_backlight/application.fam b/applications/services/rgb_backlight/application.fam index 168a7bbd0..bb42d7b83 100644 --- a/applications/services/rgb_backlight/application.fam +++ b/applications/services/rgb_backlight/application.fam @@ -7,5 +7,4 @@ App( stack_size=1 * 1024, order=99, sdk_headers=["rgb_backlight.h"], - provides=["rgb_backlight_settings"], ) \ No newline at end of file diff --git a/applications/services/rgb_backlight/rgb_backlight.c b/applications/services/rgb_backlight/rgb_backlight.c index 046cd4771..f198ae13f 100644 --- a/applications/services/rgb_backlight/rgb_backlight.c +++ b/applications/services/rgb_backlight/rgb_backlight.c @@ -1,15 +1,156 @@ +/* + RGB BackLight Service based on + RGB backlight FlipperZero driver + Copyright (C) 2022-2023 Victor Nikitchuk (https://github.com/quen0n) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + #include #include #include +#include +#include #include "rgb_backlight.h" + +#define STATIC_COLOR_COUNT (sizeof(colors) / sizeof(RGBBacklightStaticColor)) + #define TAG "RGB_BACKLIGHT_SRV" -int32_t rgb_backlight_srv (void* p){ -UNUSED (p); -while (1){ - FURI_LOG_I (TAG,"working"); - furi_delay_ms (2000); +static const RGBBacklightStaticColor colors[] = { + {"Orange", 255, 60, 0}, + {"Yellow", 255, 144, 0}, + {"Spring", 167, 255, 0}, + {"Lime", 0, 255, 0}, + {"Aqua", 0, 255, 127}, + {"Cyan", 0, 210, 210}, + {"Azure", 0, 127, 255}, + {"Blue", 0, 0, 255}, + {"Purple", 127, 0, 255}, + {"Magenta", 210, 0, 210}, + {"Pink", 255, 0, 127}, + {"Red", 255, 0, 0}, + {"White", 254, 210, 200}, + {"Custom", 0, 0, 0}, +}; + +void rgb_backlight_set_custom_color(uint8_t red, uint8_t green, uint8_t blue, float brightness) { + for(uint8_t i = 0; i < SK6805_get_led_count(); i++) { + uint8_t r = red * (brightness); + uint8_t g = green * (brightness); + uint8_t b = blue * (brightness); + SK6805_set_led_color(i, r, g, b); + } + SK6805_update(); } -return 0; + +static void rainbow_timer_callback(void* context) { + furi_assert(context); + RGBBacklightApp* app = context; + + // if rgb_mode_rainbow_mode is rainbow do rainbow effect + if(app->settings->rainbow_mode == 1) { + switch(app->rainbow_stage) { + // from red to yellow + case 1: + app->rainbow_green += app->settings->rainbow_step; + if(app->rainbow_green >= 255) { + app->rainbow_green = 255; + app->rainbow_stage++; + } + break; + // yellow red to green + case 2: + app->rainbow_red -= app->settings->rainbow_step; + if(app->rainbow_red <= 0) { + app->rainbow_red = 0; + app->rainbow_stage++; + } + break; + // from green to light blue + case 3: + app->rainbow_blue += app->settings->rainbow_step; + if(app->rainbow_blue >= 255) { + app->rainbow_blue = 255; + app->rainbow_stage++; + } + break; + //from light blue to blue + case 4: + app->rainbow_green -= app->settings->rainbow_step; + if(app->rainbow_green <= 0) { + app->rainbow_green = 0; + app->rainbow_stage++; + } + break; + //from blue to violet + case 5: + app->rainbow_red += app->settings->rainbow_step; + if(app->rainbow_red >= 255) { + app->rainbow_red = 255; + app->rainbow_stage++; + } + break; + //from violet to red + case 6: + app->rainbow_blue -= app->settings->rainbow_step; + if(app->rainbow_blue <= 0) { + app->rainbow_blue = 0; + app->rainbow_stage = 1; + } + break; + default: + break; + } + + rgb_backlight_set_custom_color( + app->rainbow_red, + app->rainbow_green, + app->rainbow_blue, + app->settings->brightness); + } + + // if rgb_mode_rainbow_mode is ..... do another effect + // if(app->settings.rainbow_mode == 2) { + // } +} + +void rgb_backlight_settings_apply(RGBBacklightSettings* settings) { + UNUSED (settings); + //запуск таймера если все включено + // применить сохраненые настройки цвета к дисплею статику или кастом если индекс=13 +} + +int32_t rgb_backlight_srv (void* p){ + // Define object app (full app with settings and running variables), + // allocate memory and create record for access to app structure from outside + RGBBacklightApp* app = malloc(sizeof(RGBBacklightApp)); + furi_record_create(RECORD_RGB_BACKLIGHT, app); + + //define rainbow_timer and they callback + app->rainbow_timer = + furi_timer_alloc(rainbow_timer_callback, FuriTimerTypePeriodic, app); + + // load or init new settings and apply it + rgb_backlight_settings_load (app->settings); + rgb_backlight_settings_apply (app->settings); + + UNUSED(p); + while(1) { + FURI_LOG_I(TAG, "working"); + furi_delay_ms(2000); + } + return 0; } \ No newline at end of file diff --git a/applications/services/rgb_backlight/rgb_backlight.h b/applications/services/rgb_backlight/rgb_backlight.h index e69de29bb..88f3270c1 100644 --- a/applications/services/rgb_backlight/rgb_backlight.h +++ b/applications/services/rgb_backlight/rgb_backlight.h @@ -0,0 +1,44 @@ +/* + RGB BackLight Service based on + RGB backlight FlipperZero driver + Copyright (C) 2022-2023 Victor Nikitchuk (https://github.com/quen0n) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#include +#include "rgb_backlight_settings.h" +#include "SK6805.h" + +typedef struct { + char* name; + uint8_t red; + uint8_t green; + uint8_t blue; +} RGBBacklightStaticColor; + +typedef struct { + FuriTimer* rainbow_timer; + + int16_t rainbow_red; + int16_t rainbow_green; + int16_t rainbow_blue; + uint8_t rainbow_stage; + + RGBBacklightSettings* settings; + +} RGBBacklightApp; + +#define RECORD_RGB_BACKLIGHT "rgb_backlight" + diff --git a/applications/services/rgb_backlight/rgb_backlight_settings.c b/applications/services/rgb_backlight/rgb_backlight_settings.c new file mode 100644 index 000000000..62cc47994 --- /dev/null +++ b/applications/services/rgb_backlight/rgb_backlight_settings.c @@ -0,0 +1,95 @@ +#include "rgb_backlight_settings.h" + +#include +#include + +#define TAG "RGBBackligthSettings" + +#define RGB_BACKLIGHT_SETTINGS_FILE_NAME ".rgb_backlight.settings" +#define RGB_BACKLIGHT_SETTINGS_PATH INT_PATH(RGB_BACKLIGHT_SETTINGS_FILE_NAME) + +#define RGB_BACKLIGHT_SETTINGS_MAGIC (0x30) +#define RGB_BACKLIGHT_SETTINGS_VER_PREV (0) // Previous version number +#define RGB_BACKLIGHT_SETTINGS_VER (1) // New version number + +//pervious settings must be copyed from previous rgb_backlight_settings.h file +typedef struct { + uint8_t version; + bool rgb_mod_installed; + + uint8_t display_static_color_index; + uint8_t custom_r; + uint8_t custom_g; + uint8_t custom_b; + + uint32_t rainbow_mode; + uint32_t rainbow_speed_ms; + uint16_t rainbow_step; +} RGBBacklightSettingsPrevious; + +void rgb_backlight_settings_load(RGBBacklightSettings* settings) { + furi_assert(settings); + + bool success = false; + + //a useless cycle do-while, may will be used in future with anoter condition + do { + // take version from settings file metadata, if cant then break and fill settings with 0 and save to settings file; + uint8_t version; + if(!saved_struct_get_metadata(RGB_BACKLIGHT_SETTINGS_PATH, NULL, &version, NULL)) break; + + // if config actual version - load it directly + if(version == RGB_BACKLIGHT_SETTINGS_VER) { + success = saved_struct_load( + RGB_BACKLIGHT_SETTINGS_PATH, + settings, + sizeof(RGBBacklightSettings), + RGB_BACKLIGHT_SETTINGS_MAGIC, + RGB_BACKLIGHT_SETTINGS_VER); + // if config previous version - load it and inicialize new settings + } else if(version == RGB_BACKLIGHT_SETTINGS_VER_PREV) { + RGBBacklightSettingsPrevious* settings_previous = malloc(sizeof(RGBBacklightSettingsPrevious)); + + success = saved_struct_load( + RGB_BACKLIGHT_SETTINGS_PATH, + settings_previous, + sizeof(RGBBacklightSettingsPrevious), + RGB_BACKLIGHT_SETTINGS_MAGIC, + RGB_BACKLIGHT_SETTINGS_VER_PREV); + // new settings initialization + if(success) { + // copy loaded old settings as part of new + uint32_t size = sizeof(settings); + memcpy(settings, settings_previous, size); + // set new options to initial value + // settings.something = something; + } + + free(settings_previous); + } + // in case of another config version we exit from useless cycle to next step + } while(false); + + // fill settings with 0 and save to settings file; + // Orange color (index=0) will be default + if(!success) { + FURI_LOG_W(TAG, "Failed to load file, using defaults 0"); + memset(settings, 0, sizeof(RGBBacklightSettings)); + rgb_backlight_settings_save(settings); + } +} + +void rgb_backlight_settings_save(const RGBBacklightSettings* settings) { + furi_assert(settings); + + const bool success = saved_struct_save( + RGB_BACKLIGHT_SETTINGS_PATH, + settings, + sizeof(RGBBacklightSettings), + RGB_BACKLIGHT_SETTINGS_MAGIC, + RGB_BACKLIGHT_SETTINGS_VER); + + if(!success) { + FURI_LOG_E(TAG, "Failed to save rgb_backlight_settings file"); + } +} diff --git a/applications/services/rgb_backlight/rgb_backlight_settings.h b/applications/services/rgb_backlight/rgb_backlight_settings.h new file mode 100644 index 000000000..f7a4af177 --- /dev/null +++ b/applications/services/rgb_backlight/rgb_backlight_settings.h @@ -0,0 +1,31 @@ +#pragma once + +#include +#include + +typedef struct { + uint8_t version; + bool rgb_mod_installed; + + uint8_t display_static_color_index; + uint8_t custom_r; + uint8_t custom_g; + uint8_t custom_b; + float brightness; + + uint32_t rainbow_mode; + uint32_t rainbow_speed_ms; + uint16_t rainbow_step; + +} RGBBacklightSettings; + +#ifdef __cplusplus +extern "C" { +#endif + +void rgb_backlight_settings_load(RGBBacklightSettings* settings); +void rgb_backlight_settings_save(const RGBBacklightSettings* settings); + +#ifdef __cplusplus +} +#endif diff --git a/applications/settings/application.fam b/applications/settings/application.fam index 8e184a706..2ad4aa030 100644 --- a/applications/settings/application.fam +++ b/applications/settings/application.fam @@ -7,7 +7,6 @@ App( "system_settings", "clock_settings", "input_settings", - "rgb_backlight_settings", "about", ], ) diff --git a/applications/settings/rgb_backlight_settings/application.fam b/applications/settings/rgb_backlight_settings/application.fam deleted file mode 100644 index 50621228a..000000000 --- a/applications/settings/rgb_backlight_settings/application.fam +++ /dev/null @@ -1,9 +0,0 @@ -App( - appid="rgb_backlight_settings", - name="RGB backlight", - apptype=FlipperAppType.SETTINGS, - entry_point="rgb_backlight_settings", - requires=["rgb_backlight"], - stack_size=1 * 1024, - order=110, -) \ No newline at end of file diff --git a/applications/settings/rgb_backlight_settings/rgb_backlight_settings.c b/applications/settings/rgb_backlight_settings/rgb_backlight_settings.c deleted file mode 100644 index 096852063..000000000 --- a/applications/settings/rgb_backlight_settings/rgb_backlight_settings.c +++ /dev/null @@ -1,13 +0,0 @@ -#include -#include -#include -#include "rgb_backlight_settings.h" - -#define TAG "RGB_BACKLIGHT_SETTINGS" - -int32_t rgb_backlight_settings (void* p){ -UNUSED (p); -FURI_LOG_I (TAG,"Settings"); -furi_delay_ms (2000); -return 0; -} \ No newline at end of file diff --git a/applications/settings/rgb_backlight_settings/rgb_backlight_settings.h b/applications/settings/rgb_backlight_settings/rgb_backlight_settings.h deleted file mode 100644 index e69de29bb..000000000 diff --git a/targets/f7/api_symbols.csv b/targets/f7/api_symbols.csv index f78775215..310a669ef 100644 --- a/targets/f7/api_symbols.csv +++ b/targets/f7/api_symbols.csv @@ -1,5 +1,5 @@ entry,status,name,type,params -Version,+,83.0,, +Version,+,83.1,, Header,+,applications/drivers/subghz/cc1101_ext/cc1101_ext_interconnect.h,, Header,+,applications/services/bt/bt_service/bt.h,, Header,+,applications/services/bt/bt_service/bt_keys_storage.h,, @@ -411,6 +411,10 @@ Function,-,LL_mDelay,void,uint32_t Function,-,Osal_MemCmp,int,"const void*, const void*, unsigned int" Function,-,Osal_MemCpy,void*,"void*, const void*, unsigned int" Function,-,Osal_MemSet,void*,"void*, int, unsigned int" +Function,+,SK6805_get_led_count,uint8_t, +Function,+,SK6805_init,void, +Function,+,SK6805_set_led_color,void,"uint8_t, uint8_t, uint8_t, uint8_t" +Function,+,SK6805_update,void, Function,-,SystemCoreClockUpdate,void, Function,-,SystemInit,void, Function,-,_Exit,void,int @@ -3139,6 +3143,8 @@ Function,-,remquol,long double,"long double, long double, int*" Function,-,rename,int,"const char*, const char*" Function,-,renameat,int,"int, const char*, int, const char*" Function,-,rewind,void,FILE* +Function,+,rgb_backlight_settings_load,void,RGBBacklightSettings* +Function,+,rgb_backlight_settings_save,void,const RGBBacklightSettings* Function,-,rindex,char*,"const char*, int" Function,-,rint,double,double Function,-,rintf,float,float From 7a19c9e5494839e2aa496aebd9932e5b47c0ffa1 Mon Sep 17 00:00:00 2001 From: Dmitry422 Date: Wed, 12 Mar 2025 18:15:38 +0700 Subject: [PATCH 114/268] Still in progress --- .../services/notification/application.fam | 2 +- .../services/notification/notification_app.c | 239 +++++++++--------- .../services/notification/notification_app.h | 20 +- .../services/rgb_backlight/application.fam | 2 +- .../services/rgb_backlight/rgb_backlight.c | 176 +++++++++---- .../services/rgb_backlight/rgb_backlight.h | 24 +- .../rgb_backlight/rgb_backlight_settings.c | 2 +- .../rgb_backlight/rgb_backlight_settings.h | 8 +- .../notification_settings_app.c | 88 ++++--- .../notification_settings/rgb_backlight.c | 229 ----------------- .../notification_settings/rgb_backlight.h | 94 ------- lib/drivers/SK6805.h | 7 + targets/f7/api_symbols.csv | 9 +- targets/f7/furi_hal/furi_hal_light.c | 4 +- 14 files changed, 361 insertions(+), 543 deletions(-) delete mode 100644 applications/settings/notification_settings/rgb_backlight.c delete mode 100644 applications/settings/notification_settings/rgb_backlight.h diff --git a/applications/services/notification/application.fam b/applications/services/notification/application.fam index 82f94085a..fbfdca848 100644 --- a/applications/services/notification/application.fam +++ b/applications/services/notification/application.fam @@ -4,7 +4,7 @@ App( apptype=FlipperAppType.SERVICE, entry_point="notification_srv", cdefines=["SRV_NOTIFICATION"], - requires=["input"], + requires=["input","rgb_backlight"], provides=["notification_settings"], stack_size=int(1.5 * 1024), order=100, diff --git a/applications/services/notification/notification_app.c b/applications/services/notification/notification_app.c index 8096c2cc3..afb900706 100644 --- a/applications/services/notification/notification_app.c +++ b/applications/services/notification/notification_app.c @@ -9,7 +9,7 @@ #include "notification.h" #include "notification_messages.h" #include "notification_app.h" -#include "applications/settings/notification_settings/rgb_backlight.h" +#include "applications/services/rgb_backlight/rgb_backlight.h" #define TAG "NotificationSrv" @@ -189,110 +189,110 @@ static void notification_display_timer(void* ctx) { notification_message(app, &sequence_display_backlight_off); } -// --- RGB MOD RAINBOW SECTION --- +// // --- RGB MOD RAINBOW SECTION --- -//start furi timer for rgb_mod_rainbow -static void rgb_mod_rainbow_timer_start(NotificationApp* app) { - furi_timer_start( - app->rgb_mod_rainbow_timer, furi_ms_to_ticks(app->settings.rgb_mod_rainbow_speed_ms)); -} +// //start furi timer for rgb_mod_rainbow +// static void rgb_mod_rainbow_timer_start(NotificationApp* app) { +// furi_timer_start( +// app->rgb_mod_rainbow_timer, furi_ms_to_ticks(app->settings.rgb_mod_rainbow_speed_ms)); +// } -//stop furi timer for rgb_mod_rainbow -static void rgb_mod_rainbow_timer_stop(NotificationApp* app) { - furi_timer_stop(app->rgb_mod_rainbow_timer); -} +// //stop furi timer for rgb_mod_rainbow +// static void rgb_mod_rainbow_timer_stop(NotificationApp* app) { +// furi_timer_stop(app->rgb_mod_rainbow_timer); +// } -// start/restart/stop rgb_mod_rainbow_timer only if rgb_mod_installed and apply rainbow colors to backlight -static void rgb_mod_rainbow_timer_starter(NotificationApp* app) { - if(app->settings.rgb_mod_installed) { - if(app->settings.rgb_mod_rainbow_mode > 0) { - rgb_mod_rainbow_update( - app->rgb_mod_rainbow_red, - app->rgb_mod_rainbow_green, - app->rgb_mod_rainbow_blue, - app->settings.display_brightness); - rgb_mod_rainbow_timer_start(app); - } else { - if(furi_timer_is_running(app->rgb_mod_rainbow_timer)) { - rgb_mod_rainbow_timer_stop(app); - } - } - } -} +// // start/restart/stop rgb_mod_rainbow_timer only if rgb_mod_installed and apply rainbow colors to backlight +// static void rgb_mod_rainbow_timer_starter(NotificationApp* app) { +// if(app->settings.rgb_mod_installed) { +// if(app->settings.rgb_mod_rainbow_mode > 0) { +// rgb_mod_rainbow_update( +// app->rgb_mod_rainbow_red, +// app->rgb_mod_rainbow_green, +// app->rgb_mod_rainbow_blue, +// app->settings.display_brightness); +// rgb_mod_rainbow_timer_start(app); +// } else { +// if(furi_timer_is_running(app->rgb_mod_rainbow_timer)) { +// rgb_mod_rainbow_timer_stop(app); +// } +// } +// } +// } -// callback for rgb_mod_rainbow_timer (what we do when timer end) -static void rgb_mod_rainbow_timer_callback(void* context) { - furi_assert(context); - NotificationApp* app = context; +// // callback for rgb_mod_rainbow_timer (what we do when timer end) +// static void rgb_mod_rainbow_timer_callback(void* context) { +// furi_assert(context); +// NotificationApp* app = context; - // if rgb_mode_rainbow_mode is rainbow do rainbow effect - if(app->settings.rgb_mod_rainbow_mode == 1) { - switch(app->rgb_mod_rainbow_stage) { - // from red to yellow - case 1: - app->rgb_mod_rainbow_green += app->settings.rgb_mod_rainbow_step; - if(app->rgb_mod_rainbow_green >= 255) { - app->rgb_mod_rainbow_green = 255; - app->rgb_mod_rainbow_stage++; - } - break; - // yellow red to green - case 2: - app->rgb_mod_rainbow_red -= app->settings.rgb_mod_rainbow_step; - if(app->rgb_mod_rainbow_red <= 0) { - app->rgb_mod_rainbow_red = 0; - app->rgb_mod_rainbow_stage++; - } - break; - // from green to light blue - case 3: - app->rgb_mod_rainbow_blue += app->settings.rgb_mod_rainbow_step; - if(app->rgb_mod_rainbow_blue >= 255) { - app->rgb_mod_rainbow_blue = 255; - app->rgb_mod_rainbow_stage++; - } - break; - //from light blue to blue - case 4: - app->rgb_mod_rainbow_green -= app->settings.rgb_mod_rainbow_step; - if(app->rgb_mod_rainbow_green <= 0) { - app->rgb_mod_rainbow_green = 0; - app->rgb_mod_rainbow_stage++; - } - break; - //from blue to violet - case 5: - app->rgb_mod_rainbow_red += app->settings.rgb_mod_rainbow_step; - if(app->rgb_mod_rainbow_red >= 255) { - app->rgb_mod_rainbow_red = 255; - app->rgb_mod_rainbow_stage++; - } - break; - //from violet to red - case 6: - app->rgb_mod_rainbow_blue -= app->settings.rgb_mod_rainbow_step; - if(app->rgb_mod_rainbow_blue <= 0) { - app->rgb_mod_rainbow_blue = 0; - app->rgb_mod_rainbow_stage = 1; - } - break; - default: - break; - } +// // if rgb_mode_rainbow_mode is rainbow do rainbow effect +// if(app->settings.rgb_mod_rainbow_mode == 1) { +// switch(app->rgb_mod_rainbow_stage) { +// // from red to yellow +// case 1: +// app->rgb_mod_rainbow_green += app->settings.rgb_mod_rainbow_step; +// if(app->rgb_mod_rainbow_green >= 255) { +// app->rgb_mod_rainbow_green = 255; +// app->rgb_mod_rainbow_stage++; +// } +// break; +// // yellow red to green +// case 2: +// app->rgb_mod_rainbow_red -= app->settings.rgb_mod_rainbow_step; +// if(app->rgb_mod_rainbow_red <= 0) { +// app->rgb_mod_rainbow_red = 0; +// app->rgb_mod_rainbow_stage++; +// } +// break; +// // from green to light blue +// case 3: +// app->rgb_mod_rainbow_blue += app->settings.rgb_mod_rainbow_step; +// if(app->rgb_mod_rainbow_blue >= 255) { +// app->rgb_mod_rainbow_blue = 255; +// app->rgb_mod_rainbow_stage++; +// } +// break; +// //from light blue to blue +// case 4: +// app->rgb_mod_rainbow_green -= app->settings.rgb_mod_rainbow_step; +// if(app->rgb_mod_rainbow_green <= 0) { +// app->rgb_mod_rainbow_green = 0; +// app->rgb_mod_rainbow_stage++; +// } +// break; +// //from blue to violet +// case 5: +// app->rgb_mod_rainbow_red += app->settings.rgb_mod_rainbow_step; +// if(app->rgb_mod_rainbow_red >= 255) { +// app->rgb_mod_rainbow_red = 255; +// app->rgb_mod_rainbow_stage++; +// } +// break; +// //from violet to red +// case 6: +// app->rgb_mod_rainbow_blue -= app->settings.rgb_mod_rainbow_step; +// if(app->rgb_mod_rainbow_blue <= 0) { +// app->rgb_mod_rainbow_blue = 0; +// app->rgb_mod_rainbow_stage = 1; +// } +// break; +// default: +// break; +// } - rgb_mod_rainbow_update( - app->rgb_mod_rainbow_red, - app->rgb_mod_rainbow_green, - app->rgb_mod_rainbow_blue, - app->settings.display_brightness); - } +// rgb_mod_rainbow_update( +// app->rgb_mod_rainbow_red, +// app->rgb_mod_rainbow_green, +// app->rgb_mod_rainbow_blue, +// app->settings.display_brightness); +// } - // if rgb_mode_rainbow_mode is ..... do another effect - // if(app->settings.rgb_mod_rainbow_mode == 2) { - // } -} +// // if rgb_mode_rainbow_mode is ..... do another effect +// // if(app->settings.rgb_mod_rainbow_mode == 2) { +// // } +// } -// --- END OF RGB MOD RAINBOW SECTION --- +// // --- END OF RGB MOD RAINBOW SECTION --- // message processing static void notification_process_notification_message( @@ -326,7 +326,7 @@ static void notification_process_notification_message( reset_mask |= reset_display_mask; //start rgb_mod_rainbow_timer when display backlight is ON and all corresponding settings is ON too - rgb_mod_rainbow_timer_starter(app); + rainbow_timer_starter(app->rgb_srv); } else { reset_mask &= ~reset_display_mask; @@ -335,8 +335,8 @@ static void notification_process_notification_message( furi_timer_stop(app->display_timer); } //stop rgb_mod_rainbow_timer when display backlight is OFF - if(furi_timer_is_running(app->rgb_mod_rainbow_timer)) { - rgb_mod_rainbow_timer_stop(app); + if(furi_timer_is_running(app->rgb_srv->rainbow_timer)) { + rainbow_timer_stop(app->rgb_srv); } } break; @@ -664,25 +664,26 @@ static NotificationApp* notification_app_alloc(void) { furi_pubsub_subscribe(app->event_record, input_event_callback, app); notification_message(app, &sequence_display_backlight_on); - // --- RGB MOD INIT SETTINGS SECTION --- + // // --- RGB MOD INIT SETTINGS SECTION --- - app->settings.rgb_mod_installed = false; - app->settings.rgb_mod_rainbow_mode = 0; - app->settings.rgb_mod_rainbow_speed_ms = 100; - app->settings.rgb_mod_rainbow_step = 5; - app->rgb_mod_rainbow_red = 255; - app->rgb_mod_rainbow_green = 0; - app->rgb_mod_rainbow_blue = 0; - app->rgb_mod_rainbow_stage = 1; + // app->settings.rgb_mod_installed = false; + // app->settings.rgb_mod_rainbow_mode = 0; + // app->settings.rgb_mod_rainbow_speed_ms = 100; + // app->settings.rgb_mod_rainbow_step = 5; + // app->rgb_mod_rainbow_red = 255; + // app->rgb_mod_rainbow_green = 0; + // app->rgb_mod_rainbow_blue = 0; + // app->rgb_mod_rainbow_stage = 1; + + // //define rgb_mod_rainbow_timer and they callback + // app->rgb_mod_rainbow_timer = + // furi_timer_alloc(rgb_mod_rainbow_timer_callback, FuriTimerTypePeriodic, app); + // // --- END OF RGB MOD INIT SETTINGS SECTION --- - //define rgb_mod_rainbow_timer and they callback - app->rgb_mod_rainbow_timer = - furi_timer_alloc(rgb_mod_rainbow_timer_callback, FuriTimerTypePeriodic, app); return app; - - // --- END OF RGB MOD INIT SETTINGS SECTION --- } + static void notification_storage_callback(const void* message, void* context) { furi_assert(context); NotificationApp* app = context; @@ -703,8 +704,8 @@ static void notification_apply_settings(NotificationApp* app) { } notification_apply_lcd_contrast(app); - //start rgb_mod_rainbow_timer on system init if they ON in config - rgb_mod_rainbow_timer_starter(app); + // //start rgb_mod_rainbow_timer on system init if they ON in config + // rgb_mod_rainbow_timer_starter(app); } static void notification_init_settings(NotificationApp* app) { @@ -723,7 +724,7 @@ static void notification_init_settings(NotificationApp* app) { int32_t notification_srv(void* p) { UNUSED(p); NotificationApp* app = notification_app_alloc(); - + app->rgb_srv = furi_record_open (RECORD_RGB_BACKLIGHT); notification_init_settings(app); notification_vibro_off(); @@ -748,8 +749,8 @@ int32_t notification_srv(void* p) { break; case SaveSettingsMessage: //call rgb_mod_timer_control (start or stop) when we save settings - rgb_mod_rainbow_timer_starter(app); - rgb_backlight_save_settings(); + rainbow_timer_starter(app->rgb_srv); + rgb_backlight_settings_save(app->rgb_srv->settings); notification_save_settings(app); break; case LoadSettingsMessage: diff --git a/applications/services/notification/notification_app.h b/applications/services/notification/notification_app.h index 54b3ab7a9..4383ca6bc 100644 --- a/applications/services/notification/notification_app.h +++ b/applications/services/notification/notification_app.h @@ -3,6 +3,7 @@ #include "notification.h" #include "notification_messages.h" #include "notification_settings_filename.h" +#include "applications/services/rgb_backlight/rgb_backlight.h" #define NOTIFICATION_LED_COUNT 3 #define NOTIFICATION_EVENT_COMPLETE 0x00000001U @@ -45,10 +46,10 @@ typedef struct { int8_t contrast; bool vibro_on; /// --- RGB MOD SETTINGS SECTION --- - bool rgb_mod_installed; - uint32_t rgb_mod_rainbow_mode; - uint32_t rgb_mod_rainbow_speed_ms; - uint16_t rgb_mod_rainbow_step; + // bool rgb_mod_installed; + // uint32_t rgb_mod_rainbow_mode; + // uint32_t rgb_mod_rainbow_speed_ms; + // uint16_t rgb_mod_rainbow_step; /// --- END OF RGB MOD SETTINGS SECTION --- } NotificationSettings; @@ -63,14 +64,15 @@ struct NotificationApp { uint8_t display_led_lock; // --- RGB RAINBOW MODE VARIABLES SECTION --- - FuriTimer* rgb_mod_rainbow_timer; - int16_t rgb_mod_rainbow_red; - int16_t rgb_mod_rainbow_green; - int16_t rgb_mod_rainbow_blue; - uint8_t rgb_mod_rainbow_stage; + // FuriTimer* rgb_mod_rainbow_timer; + // int16_t rgb_mod_rainbow_red; + // int16_t rgb_mod_rainbow_green; + // int16_t rgb_mod_rainbow_blue; + // uint8_t rgb_mod_rainbow_stage; // --- ENd OF RGB RAINBOW MODE VARIABLES SECTION --- NotificationSettings settings; + RGBBacklightApp* rgb_srv; }; void notification_message_save_settings(NotificationApp* app); \ No newline at end of file diff --git a/applications/services/rgb_backlight/application.fam b/applications/services/rgb_backlight/application.fam index bb42d7b83..5e05233db 100644 --- a/applications/services/rgb_backlight/application.fam +++ b/applications/services/rgb_backlight/application.fam @@ -5,6 +5,6 @@ App( entry_point="rgb_backlight_srv", cdefines=["SRV_RGB_BACKLIGHT"], stack_size=1 * 1024, - order=99, + order=95, sdk_headers=["rgb_backlight.h"], ) \ No newline at end of file diff --git a/applications/services/rgb_backlight/rgb_backlight.c b/applications/services/rgb_backlight/rgb_backlight.c index f198ae13f..64c04b9cb 100644 --- a/applications/services/rgb_backlight/rgb_backlight.c +++ b/applications/services/rgb_backlight/rgb_backlight.c @@ -25,11 +25,11 @@ #include "rgb_backlight.h" -#define STATIC_COLOR_COUNT (sizeof(colors) / sizeof(RGBBacklightStaticColor)) +#define PREDEFINED_COLOR_COUNT (sizeof(colors) / sizeof(RGBBacklightPredefinedColor)) #define TAG "RGB_BACKLIGHT_SRV" -static const RGBBacklightStaticColor colors[] = { +static const RGBBacklightPredefinedColor colors[] = { {"Orange", 255, 60, 0}, {"Yellow", 255, 144, 0}, {"Spring", 167, 255, 0}, @@ -46,16 +46,89 @@ static const RGBBacklightStaticColor colors[] = { {"Custom", 0, 0, 0}, }; -void rgb_backlight_set_custom_color(uint8_t red, uint8_t green, uint8_t blue, float brightness) { +uint8_t rgb_backlight_get_color_count(void) { + return PREDEFINED_COLOR_COUNT; + } + +const char* rgb_backlight_get_color_text(uint8_t index) { + return colors[index].name; + } + + +void rgb_backlight_set_static_color(uint8_t index, float brightness) { + // use RECORD for acces to rgb service instance and use current_* colors and static colors + RGBBacklightApp* app = furi_record_open(RECORD_RGB_BACKLIGHT); + for(uint8_t i = 0; i < SK6805_get_led_count(); i++) { - uint8_t r = red * (brightness); - uint8_t g = green * (brightness); - uint8_t b = blue * (brightness); + app->current_red = colors[index].red; + app->current_green = colors[index].green; + app->current_blue = colors[index].blue; + + uint8_t r = app->current_red * brightness; + uint8_t g = app->current_green * brightness; + uint8_t b = app->current_blue * brightness; + SK6805_set_led_color(i, r, g, b); } + + furi_record_close(RECORD_RGB_BACKLIGHT); SK6805_update(); } +// void rgb_backlight_set_custom_color(uint8_t red, uint8_t green, uint8_t blue, float brightness) { +// for(uint8_t i = 0; i < SK6805_get_led_count(); i++) { +// uint8_t r = red * (brightness); +// uint8_t g = green * (brightness); +// uint8_t b = blue * (brightness); +// SK6805_set_led_color(i, r, g, b); +// } +// SK6805_update(); +// } + +// apply new brightness to current display color set +void rgb_backlight_update (float brightness) { + // use RECORD for acces to rgb service instance and use current_* colors + RGBBacklightApp* app = furi_record_open(RECORD_RGB_BACKLIGHT); + + for(uint8_t i = 0; i < SK6805_get_led_count(); i++) { + uint8_t r = app->current_red * brightness; + uint8_t g = app->current_green * brightness; + uint8_t b = app->current_blue * brightness; + SK6805_set_led_color(i, r, g, b); + } + + furi_record_close(RECORD_RGB_BACKLIGHT); + SK6805_update(); +} +//start furi timer for rainbow +void rainbow_timer_start(RGBBacklightApp* app) { + furi_timer_start( + app->rainbow_timer, furi_ms_to_ticks(app->settings->rainbow_speed_ms)); +} + +//stop furi timer for rainbow +void rainbow_timer_stop(RGBBacklightApp* app) { + furi_timer_stop(app->rainbow_timer); +} + +// if rgb_mod_installed then apply rainbow colors to backlight and start/restart/stop rainbow_timer +void rainbow_timer_starter(RGBBacklightApp* app) { + if(app->settings->rgb_mod_installed) { + if(app->settings->rainbow_mode > 0) { + // rgb_backlight_set_custom_color( + // app->rainbow_red, + // app->rainbow_green, + // app->rainbow_blue, + // app->settings->brightness); + rainbow_timer_start(app); + } else { + if(furi_timer_is_running(app->rainbow_timer)) { + rainbow_timer_stop(app); + } + } + } +} + static void rainbow_timer_callback(void* context) { furi_assert(context); RGBBacklightApp* app = context; @@ -63,51 +136,51 @@ static void rainbow_timer_callback(void* context) { // if rgb_mode_rainbow_mode is rainbow do rainbow effect if(app->settings->rainbow_mode == 1) { switch(app->rainbow_stage) { - // from red to yellow + // from red to yellow (255,0,0) - (255,255,0) case 1: - app->rainbow_green += app->settings->rainbow_step; - if(app->rainbow_green >= 255) { - app->rainbow_green = 255; + app->current_green += app->settings->rainbow_step; + if(app->current_green >= 255) { + app->current_green = 255; app->rainbow_stage++; } break; - // yellow red to green + // yellow to green (255,255,0) - (0,255,0) case 2: - app->rainbow_red -= app->settings->rainbow_step; - if(app->rainbow_red <= 0) { - app->rainbow_red = 0; + app->current_red -= app->settings->rainbow_step; + if(app->current_red <= 0) { + app->current_red = 0; app->rainbow_stage++; } break; - // from green to light blue + // from green to light blue (0,255,0) - (0,255,255) case 3: - app->rainbow_blue += app->settings->rainbow_step; - if(app->rainbow_blue >= 255) { - app->rainbow_blue = 255; + app->current_blue += app->settings->rainbow_step; + if(app->current_blue >= 255) { + app->current_blue = 255; app->rainbow_stage++; } break; - //from light blue to blue + //from light blue to blue (0,255,255) - (0,0,255) case 4: - app->rainbow_green -= app->settings->rainbow_step; - if(app->rainbow_green <= 0) { - app->rainbow_green = 0; + app->current_green -= app->settings->rainbow_step; + if(app->current_green <= 0) { + app->current_green = 0; app->rainbow_stage++; } break; - //from blue to violet + //from blue to violet (0,0,255) - (255,0,255) case 5: - app->rainbow_red += app->settings->rainbow_step; - if(app->rainbow_red >= 255) { - app->rainbow_red = 255; + app->current_red += app->settings->rainbow_step; + if(app->current_red >= 255) { + app->current_red = 255; app->rainbow_stage++; } break; - //from violet to red + //from violet to red (255,0,255) - (255,0,0) case 6: - app->rainbow_blue -= app->settings->rainbow_step; - if(app->rainbow_blue <= 0) { - app->rainbow_blue = 0; + app->current_blue -= app->settings->rainbow_step; + if(app->current_blue <= 0) { + app->current_blue = 0; app->rainbow_stage = 1; } break; @@ -115,25 +188,23 @@ static void rainbow_timer_callback(void* context) { break; } - rgb_backlight_set_custom_color( - app->rainbow_red, - app->rainbow_green, - app->rainbow_blue, - app->settings->brightness); + // rgb_backlight_set_custom_color( + // app->current_red, + // app->current_green, + // app->current_blue, + // app->settings->brightness); } + + rgb_backlight_update (app->settings->brightness); // if rgb_mode_rainbow_mode is ..... do another effect // if(app->settings.rainbow_mode == 2) { // } } -void rgb_backlight_settings_apply(RGBBacklightSettings* settings) { - UNUSED (settings); - //запуск таймера если все включено - // применить сохраненые настройки цвета к дисплею статику или кастом если индекс=13 -} +int32_t rgb_backlight_srv (void* p) { + UNUSED(p); -int32_t rgb_backlight_srv (void* p){ // Define object app (full app with settings and running variables), // allocate memory and create record for access to app structure from outside RGBBacklightApp* app = malloc(sizeof(RGBBacklightApp)); @@ -143,11 +214,28 @@ int32_t rgb_backlight_srv (void* p){ app->rainbow_timer = furi_timer_alloc(rainbow_timer_callback, FuriTimerTypePeriodic, app); - // load or init new settings and apply it + // settings load or create default rgb_backlight_settings_load (app->settings); - rgb_backlight_settings_apply (app->settings); - UNUSED(p); + // Init app variables + app->current_red = 255; + app->current_green = 60; + app->current_green = 0; + app->rainbow_stage = 1; + + // а нужно ли это все при старте сервиса, если мы получим сигнал на подсветку от Нотификейшена. Может вынести в ИНИТ и при старте нотиф дернуть его 1 раз + // if rgb_mod_installed start rainbow or set static color from settings (default index = 0) + if(app->settings->rgb_mod_installed) { + if(app->settings->rainbow_mode > 0 ) { + rainbow_timer_starter(app); + } else { + rgb_backlight_set_static_color(app->settings->static_color_index, app->settings->brightness); + } + // if not rgb_mod_installed set default static orange color (index=0) + } else { + rgb_backlight_set_static_color(0, app->settings->brightness); + } + while(1) { FURI_LOG_I(TAG, "working"); furi_delay_ms(2000); diff --git a/applications/services/rgb_backlight/rgb_backlight.h b/applications/services/rgb_backlight/rgb_backlight.h index 88f3270c1..531d3a9e7 100644 --- a/applications/services/rgb_backlight/rgb_backlight.h +++ b/applications/services/rgb_backlight/rgb_backlight.h @@ -17,23 +17,28 @@ along with this program. If not, see . */ +#pragma once #include #include "rgb_backlight_settings.h" #include "SK6805.h" +#ifdef __cplusplus +extern "C" { +#endif + typedef struct { char* name; uint8_t red; uint8_t green; uint8_t blue; -} RGBBacklightStaticColor; +} RGBBacklightPredefinedColor; typedef struct { FuriTimer* rainbow_timer; - int16_t rainbow_red; - int16_t rainbow_green; - int16_t rainbow_blue; + int16_t current_red; + int16_t current_green; + int16_t current_blue; uint8_t rainbow_stage; RGBBacklightSettings* settings; @@ -42,3 +47,14 @@ typedef struct { #define RECORD_RGB_BACKLIGHT "rgb_backlight" +void rgb_backlight_update (float brightness); +void rgb_backlight_set_static_color(uint8_t index, float brightness); +void rainbow_timer_stop(RGBBacklightApp* app); +void rainbow_timer_start(RGBBacklightApp* app); +void rainbow_timer_starter(RGBBacklightApp* app); +const char* rgb_backlight_get_color_text(uint8_t index); +uint8_t rgb_backlight_get_color_count(void); + +#ifdef __cplusplus +} +#endif diff --git a/applications/services/rgb_backlight/rgb_backlight_settings.c b/applications/services/rgb_backlight/rgb_backlight_settings.c index 62cc47994..9cf1c1440 100644 --- a/applications/services/rgb_backlight/rgb_backlight_settings.c +++ b/applications/services/rgb_backlight/rgb_backlight_settings.c @@ -17,7 +17,7 @@ typedef struct { uint8_t version; bool rgb_mod_installed; - uint8_t display_static_color_index; + uint8_t static_color_index; uint8_t custom_r; uint8_t custom_g; uint8_t custom_b; diff --git a/applications/services/rgb_backlight/rgb_backlight_settings.h b/applications/services/rgb_backlight/rgb_backlight_settings.h index f7a4af177..48b0c43b9 100644 --- a/applications/services/rgb_backlight/rgb_backlight_settings.h +++ b/applications/services/rgb_backlight/rgb_backlight_settings.h @@ -7,10 +7,10 @@ typedef struct { uint8_t version; bool rgb_mod_installed; - uint8_t display_static_color_index; - uint8_t custom_r; - uint8_t custom_g; - uint8_t custom_b; + uint8_t static_color_index; + uint8_t custom_red; + uint8_t custom_green; + uint8_t custom_blue; float brightness; uint32_t rainbow_mode; diff --git a/applications/settings/notification_settings/notification_settings_app.c b/applications/settings/notification_settings/notification_settings_app.c index 8c6871acd..c2262d327 100644 --- a/applications/settings/notification_settings/notification_settings_app.c +++ b/applications/settings/notification_settings/notification_settings_app.c @@ -4,7 +4,6 @@ #include #include #include -#include #define MAX_NOTIFICATION_SETTINGS 4 @@ -170,6 +169,8 @@ static void backlight_changed(VariableItem* item) { variable_item_set_current_value_text(item, backlight_text[index]); app->notification->settings.display_brightness = backlight_value[index]; + //save brightness to rgb backlight settings too + app->notification->rgb_srv->settings->brightness = backlight_value[index]; notification_message(app->notification, &sequence_display_backlight_on); } @@ -225,21 +226,21 @@ static void rgb_mod_installed_changed(VariableItem* item) { NotificationAppSettings* app = variable_item_get_context(item); uint8_t index = variable_item_get_current_value_index(item); variable_item_set_current_value_text(item, rgb_mod_text[index]); - app->notification->settings.rgb_mod_installed = rgb_mod_value[index]; + app->notification->rgb_srv->settings->rgb_mod_installed = rgb_mod_value[index]; } static void rgb_mod_rainbow_changed(VariableItem* item) { NotificationAppSettings* app = variable_item_get_context(item); uint8_t index = variable_item_get_current_value_index(item); variable_item_set_current_value_text(item, rgb_mod_rainbow_mode_text[index]); - app->notification->settings.rgb_mod_rainbow_mode = rgb_mod_rainbow_mode_value[index]; + app->notification->rgb_srv->settings->rainbow_mode = rgb_mod_rainbow_mode_value[index]; // Lock/Unlock color settings if rainbow mode Enabled/Disabled (0-3 index if debug off and 1-4 index if debug on) int slide = 0; if (furi_hal_rtc_is_flag_set(FuriHalRtcFlagDebug)) {slide = 1;} for(int i = slide; i < (slide+4); i++) { VariableItem* t_item = variable_item_list_get(app->variable_item_list_rgb, i); - if(app->notification->settings.rgb_mod_rainbow_mode > 0) { + if(app->notification->rgb_srv->settings->rainbow_mode > 0) { variable_item_set_locked(t_item, true, "Rainbow mode\nenabled!"); } else { variable_item_set_locked(t_item, false, "Rainbow mode\nenabled!"); @@ -249,8 +250,8 @@ static void rgb_mod_rainbow_changed(VariableItem* item) { notification_message_save_settings(app->notification); // restore saved rgb backlight settings if we switch_off rainbow mode - if(app->notification->settings.rgb_mod_rainbow_mode == 0) { - rgb_backlight_update(app->notification->settings.display_brightness * 255, true); + if(app->notification->rgb_srv->settings->rainbow_mode == 0) { + rgb_backlight_update(app->notification->settings.display_brightness); } } @@ -258,7 +259,7 @@ static void rgb_mod_rainbow_speed_changed(VariableItem* item) { NotificationAppSettings* app = variable_item_get_context(item); uint8_t index = variable_item_get_current_value_index(item); variable_item_set_current_value_text(item, rgb_mod_rainbow_speed_text[index]); - app->notification->settings.rgb_mod_rainbow_speed_ms = rgb_mod_rainbow_speed_value[index]; + app->notification->rgb_srv->settings->rainbow_speed_ms = rgb_mod_rainbow_speed_value[index]; //use message for restart rgb_mod_rainbow_timer with new delay notification_message_save_settings(app->notification); } @@ -267,14 +268,14 @@ static void rgb_mod_rainbow_step_changed(VariableItem* item) { NotificationAppSettings* app = variable_item_get_context(item); uint8_t index = variable_item_get_current_value_index(item); variable_item_set_current_value_text(item, rgb_mod_rainbow_step_text[index]); - app->notification->settings.rgb_mod_rainbow_step = rgb_mod_rainbow_step_value[index]; + app->notification->rgb_srv->settings->rainbow_step = rgb_mod_rainbow_step_value[index]; } -// Set RGB backlight color +// --- Set RGB backlight colors --- static void color_changed(VariableItem* item) { NotificationAppSettings* app = variable_item_get_context(item); uint8_t index = variable_item_get_current_value_index(item); - rgb_backlight_set_color(index); + rgb_backlight_set_static_color(index,app->notification->rgb_srv->settings->brightness); variable_item_set_current_value_text(item, rgb_backlight_get_color_text(index)); notification_message(app->notification, &sequence_display_backlight_on); } @@ -283,12 +284,18 @@ static void color_changed(VariableItem* item) { static void color_set_custom_red(VariableItem* item) { NotificationAppSettings* app = variable_item_get_context(item); uint8_t index = variable_item_get_current_value_index(item); - rgb_backlight_set_custom_color(index, 0); + + //Set custom red to settings and current color + app->notification->rgb_srv->settings->custom_red = index; + app->notification->rgb_srv->current_red = index; + app->notification->rgb_srv->settings->static_color_index=13; + rgb_backlight_update(app->notification->rgb_srv->settings->brightness); + char valtext[4] = {}; snprintf(valtext, sizeof(valtext), "%d", index); variable_item_set_current_value_text(item, valtext); - rgb_backlight_set_color(13); - rgb_backlight_update(app->notification->settings.display_brightness * 0xFF, true); + + // Set to custom color explicitly variable_item_set_current_value_index(temp_item, 13); variable_item_set_current_value_text(temp_item, rgb_backlight_get_color_text(13)); @@ -297,12 +304,19 @@ static void color_set_custom_red(VariableItem* item) { static void color_set_custom_green(VariableItem* item) { NotificationAppSettings* app = variable_item_get_context(item); uint8_t index = variable_item_get_current_value_index(item); - rgb_backlight_set_custom_color(index, 1); - char valtext[4] = {}; + + //Set custom green to settings and current color + app->notification->rgb_srv->settings->custom_green = index; + app->notification->rgb_srv->current_green = index; + app->notification->rgb_srv->settings->static_color_index=13; + rgb_backlight_update(app->notification->rgb_srv->settings->brightness); + + char valtext[4] = {}; snprintf(valtext, sizeof(valtext), "%d", index); variable_item_set_current_value_text(item, valtext); - rgb_backlight_set_color(13); - rgb_backlight_update(app->notification->settings.display_brightness * 0xFF, true); + // rgb_backlight_set_color(13); + // rgb_backlight_update(app->rgb_srv->settings->brightness); + // Set to custom color explicitly variable_item_set_current_value_index(temp_item, 13); variable_item_set_current_value_text(temp_item, rgb_backlight_get_color_text(13)); @@ -311,12 +325,18 @@ static void color_set_custom_green(VariableItem* item) { static void color_set_custom_blue(VariableItem* item) { NotificationAppSettings* app = variable_item_get_context(item); uint8_t index = variable_item_get_current_value_index(item); - rgb_backlight_set_custom_color(index, 2); + //Set custom blue to settings and current color + app->notification->rgb_srv->settings->custom_blue = index; + app->notification->rgb_srv->current_blue = index; + app->notification->rgb_srv->settings->static_color_index=13; + rgb_backlight_update(app->notification->rgb_srv->settings->brightness); + char valtext[4] = {}; snprintf(valtext, sizeof(valtext), "%d", index); variable_item_set_current_value_text(item, valtext); - rgb_backlight_set_color(13); - rgb_backlight_update(app->notification->settings.display_brightness * 0xFF, true); + // rgb_backlight_set_color(13); + // rgb_backlight_update(app->rgb_srv->settings->brightness); + // Set to custom color explicitly variable_item_set_current_value_index(temp_item, 13); variable_item_set_current_value_text(temp_item, rgb_backlight_get_color_text(13)); @@ -328,7 +348,7 @@ void variable_item_list_enter_callback(void* context, uint32_t index) { UNUSED(context); NotificationAppSettings* app = context; - if(((app->notification->settings.rgb_mod_installed) || + if(((app->notification->rgb_srv->settings->rgb_mod_installed) || (furi_hal_rtc_is_flag_set(FuriHalRtcFlagDebug))) && (index == 0)) { view_dispatcher_switch_to_view(app->view_dispatcher, RGBViewId); @@ -366,7 +386,7 @@ static NotificationAppSettings* alloc_settings(void) { uint8_t value_index; //Show RGB settings only when debug_mode or rgb_mod_installed is active - if((app->notification->settings.rgb_mod_installed) || + if((app->notification->rgb_srv->settings->rgb_mod_installed) || (furi_hal_rtc_is_flag_set(FuriHalRtcFlagDebug))) { item = variable_item_list_add(app->variable_item_list, "RGB mod settings", 0, NULL, app); } @@ -442,7 +462,7 @@ static NotificationAppSettings* alloc_settings(void) { rgb_mod_installed_changed, app); value_index = value_index_bool( - app->notification->settings.rgb_mod_installed, rgb_mod_value, RGB_MOD_COUNT); + app->notification->rgb_srv->settings->rgb_mod_installed, rgb_mod_value, RGB_MOD_COUNT); variable_item_set_current_value_index(item, value_index); variable_item_set_current_value_text(item, rgb_mod_text[value_index]); } @@ -454,41 +474,41 @@ static NotificationAppSettings* alloc_settings(void) { rgb_backlight_get_color_count(), color_changed, app); - value_index = rgb_backlight_get_settings()->display_color_index; + value_index = app->notification->rgb_srv->settings->static_color_index; variable_item_set_current_value_index(item, value_index); variable_item_set_current_value_text(item, rgb_backlight_get_color_text(value_index)); variable_item_set_locked( - item, (app->notification->settings.rgb_mod_rainbow_mode > 0), "Rainbow mode\nenabled!"); + item, (app->notification->rgb_srv->settings->rainbow_mode > 0), "Rainbow mode\nenabled!"); temp_item = item; // Custom Color - REFACTOR THIS item = variable_item_list_add( app->variable_item_list_rgb, "Custom Red", 255, color_set_custom_red, app); - value_index = rgb_backlight_get_settings()->custom_r; + value_index = app->notification->rgb_srv->settings->custom_red; variable_item_set_current_value_index(item, value_index); char valtext[4] = {}; snprintf(valtext, sizeof(valtext), "%d", value_index); variable_item_set_current_value_text(item, valtext); variable_item_set_locked( - item, (app->notification->settings.rgb_mod_rainbow_mode > 0), "Rainbow mode\nenabled!"); + item, (app->notification->rgb_srv->settings->rainbow_mode > 0), "Rainbow mode\nenabled!"); item = variable_item_list_add( app->variable_item_list_rgb, "Custom Green", 255, color_set_custom_green, app); - value_index = rgb_backlight_get_settings()->custom_g; + value_index = app->notification->rgb_srv->settings->custom_green; variable_item_set_current_value_index(item, value_index); snprintf(valtext, sizeof(valtext), "%d", value_index); variable_item_set_current_value_text(item, valtext); variable_item_set_locked( - item, (app->notification->settings.rgb_mod_rainbow_mode > 0), "Rainbow mode\nenabled!"); + item, (app->notification->rgb_srv->settings->rainbow_mode > 0), "Rainbow mode\nenabled!"); item = variable_item_list_add( app->variable_item_list_rgb, "Custom Blue", 255, color_set_custom_blue, app); - value_index = rgb_backlight_get_settings()->custom_b; + value_index = app->notification->rgb_srv->settings->custom_blue; variable_item_set_current_value_index(item, value_index); snprintf(valtext, sizeof(valtext), "%d", value_index); variable_item_set_current_value_text(item, valtext); variable_item_set_locked( - item, (app->notification->settings.rgb_mod_rainbow_mode > 0), "Rainbow mode\nenabled!"); + item, (app->notification->rgb_srv->settings->rainbow_mode > 0), "Rainbow mode\nenabled!"); // Rainbow (based on Willy-JL idea) settings item = variable_item_list_add( @@ -498,7 +518,7 @@ static NotificationAppSettings* alloc_settings(void) { rgb_mod_rainbow_changed, app); value_index = value_index_uint32( - app->notification->settings.rgb_mod_rainbow_mode, + app->notification->rgb_srv->settings->rainbow_mode, rgb_mod_rainbow_mode_value, RGB_MOD_RAINBOW_MODE_COUNT); variable_item_set_current_value_index(item, value_index); @@ -511,7 +531,7 @@ static NotificationAppSettings* alloc_settings(void) { rgb_mod_rainbow_speed_changed, app); value_index = value_index_uint32( - app->notification->settings.rgb_mod_rainbow_speed_ms, + app->notification->rgb_srv->settings->rainbow_speed_ms, rgb_mod_rainbow_speed_value, RGB_MOD_RAINBOW_SPEED_COUNT); variable_item_set_current_value_index(item, value_index); @@ -524,7 +544,7 @@ static NotificationAppSettings* alloc_settings(void) { rgb_mod_rainbow_step_changed, app); value_index = value_index_uint32( - app->notification->settings.rgb_mod_rainbow_step, + app->notification->rgb_srv->settings->rainbow_step, rgb_mod_rainbow_step_value, RGB_MOD_RAINBOW_SPEED_COUNT); variable_item_set_current_value_index(item, value_index); diff --git a/applications/settings/notification_settings/rgb_backlight.c b/applications/settings/notification_settings/rgb_backlight.c deleted file mode 100644 index 4fcb898f7..000000000 --- a/applications/settings/notification_settings/rgb_backlight.c +++ /dev/null @@ -1,229 +0,0 @@ -/* - RGB backlight FlipperZero driver - Copyright (C) 2022-2023 Victor Nikitchuk (https://github.com/quen0n) - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . -*/ - -#include "rgb_backlight.h" -#include -#include - -#define RGB_BACKLIGHT_SETTINGS_VERSION 6 -#define RGB_BACKLIGHT_SETTINGS_FILE_NAME ".rgb_backlight.settings" -#define RGB_BACKLIGHT_SETTINGS_PATH INT_PATH(RGB_BACKLIGHT_SETTINGS_FILE_NAME) - -#define COLOR_COUNT (sizeof(colors) / sizeof(RGBBacklightColor)) - -#define TAG "RGB Backlight" - -static RGBBacklightSettings rgb_settings = { - .version = RGB_BACKLIGHT_SETTINGS_VERSION, - .display_color_index = 0, - .custom_r = 254, - .custom_g = 254, - .custom_b = 254, - .settings_is_loaded = false}; - -static const RGBBacklightColor colors[] = { - {"Orange", 255, 60, 0}, - {"Yellow", 255, 144, 0}, - {"Spring", 167, 255, 0}, - {"Lime", 0, 255, 0}, - {"Aqua", 0, 255, 127}, - {"Cyan", 0, 210, 210}, - {"Azure", 0, 127, 255}, - {"Blue", 0, 0, 255}, - {"Purple", 127, 0, 255}, - {"Magenta", 210, 0, 210}, - {"Pink", 255, 0, 127}, - {"Red", 255, 0, 0}, - {"White", 254, 210, 200}, - {"Custom", 0, 0, 0}, -}; - -uint8_t rgb_backlight_get_color_count(void) { - return COLOR_COUNT; -} - -const char* rgb_backlight_get_color_text(uint8_t index) { - return colors[index].name; -} - -void rgb_backlight_load_settings(void) { - // Do not load settings if we are in other boot modes than normal - if(furi_hal_rtc_get_boot_mode() != FuriHalRtcBootModeNormal) { - rgb_settings.settings_is_loaded = true; - return; - } - - // Wait for all required services to start and create their records - uint8_t timeout = 0; - while(!furi_record_exists(RECORD_STORAGE)) { - timeout++; - if(timeout > 150) { - rgb_settings.settings_is_loaded = true; - return; - } - furi_delay_ms(5); - } - - RGBBacklightSettings settings; - File* file = storage_file_alloc(furi_record_open(RECORD_STORAGE)); - const size_t settings_size = sizeof(RGBBacklightSettings); - - FURI_LOG_D(TAG, "loading settings from \"%s\"", RGB_BACKLIGHT_SETTINGS_PATH); - bool fs_result = - storage_file_open(file, RGB_BACKLIGHT_SETTINGS_PATH, FSAM_READ, FSOM_OPEN_EXISTING); - - if(fs_result) { - uint16_t bytes_count = storage_file_read(file, &settings, settings_size); - - if(bytes_count != settings_size) { - fs_result = false; - } - } - - if(fs_result) { - FURI_LOG_D(TAG, "load success"); - if(settings.version != RGB_BACKLIGHT_SETTINGS_VERSION) { - FURI_LOG_E( - TAG, - "version(%d != %d) mismatch", - settings.version, - RGB_BACKLIGHT_SETTINGS_VERSION); - } else { - memcpy(&rgb_settings, &settings, settings_size); - } - } else { - FURI_LOG_E(TAG, "load failed, %s", storage_file_get_error_desc(file)); - } - - storage_file_close(file); - storage_file_free(file); - furi_record_close(RECORD_STORAGE); - rgb_settings.settings_is_loaded = true; -} - -void rgb_backlight_save_settings(void) { - RGBBacklightSettings settings; - File* file = storage_file_alloc(furi_record_open(RECORD_STORAGE)); - const size_t settings_size = sizeof(RGBBacklightSettings); - - FURI_LOG_D(TAG, "saving settings to \"%s\"", RGB_BACKLIGHT_SETTINGS_PATH); - - memcpy(&settings, &rgb_settings, settings_size); - - bool fs_result = - storage_file_open(file, RGB_BACKLIGHT_SETTINGS_PATH, FSAM_WRITE, FSOM_CREATE_ALWAYS); - - if(fs_result) { - uint16_t bytes_count = storage_file_write(file, &settings, settings_size); - - if(bytes_count != settings_size) { - fs_result = false; - } - } - - if(fs_result) { - FURI_LOG_D(TAG, "save success"); - } else { - FURI_LOG_E(TAG, "save failed, %s", storage_file_get_error_desc(file)); - } - - storage_file_close(file); - storage_file_free(file); - furi_record_close(RECORD_STORAGE); -} - -RGBBacklightSettings* rgb_backlight_get_settings(void) { - if(!rgb_settings.settings_is_loaded) { - rgb_backlight_load_settings(); - } - return &rgb_settings; -} - -void rgb_backlight_set_color(uint8_t color_index) { - if(color_index > (rgb_backlight_get_color_count() - 1)) color_index = 0; - rgb_settings.display_color_index = color_index; -} - -void rgb_backlight_set_custom_color(uint8_t color, uint8_t index) { - if(index > 2) return; - if(index == 0) { - rgb_settings.custom_r = color; - } else if(index == 1) { - rgb_settings.custom_g = color; - } else if(index == 2) { - rgb_settings.custom_b = color; - } -} - -void rgb_backlight_update(uint8_t brightness, bool bypass) { - if(!rgb_settings.settings_is_loaded) { - rgb_backlight_load_settings(); - } - - if(!bypass) { - static uint8_t last_color_index = 255; - static uint8_t last_brightness = 123; - - if(last_brightness == brightness && last_color_index == rgb_settings.display_color_index) { - return; - } - - last_brightness = brightness; - last_color_index = rgb_settings.display_color_index; - } - - for(uint8_t i = 0; i < SK6805_get_led_count(); i++) { - if(rgb_settings.display_color_index == 13) { - uint8_t r = rgb_settings.custom_r * (brightness / 255.0f); - uint8_t g = rgb_settings.custom_g * (brightness / 255.0f); - uint8_t b = rgb_settings.custom_b * (brightness / 255.0f); - - SK6805_set_led_color(i, r, g, b); - } else { - if((colors[rgb_settings.display_color_index].red == 0) && - (colors[rgb_settings.display_color_index].green == 0) && - (colors[rgb_settings.display_color_index].blue == 0)) { - uint8_t r = colors[0].red * (brightness / 255.0f); - uint8_t g = colors[0].green * (brightness / 255.0f); - uint8_t b = colors[0].blue * (brightness / 255.0f); - - SK6805_set_led_color(i, r, g, b); - } else { - uint8_t r = colors[rgb_settings.display_color_index].red * (brightness / 255.0f); - uint8_t g = colors[rgb_settings.display_color_index].green * (brightness / 255.0f); - uint8_t b = colors[rgb_settings.display_color_index].blue * (brightness / 255.0f); - - SK6805_set_led_color(i, r, g, b); - } - } - } - - SK6805_update(); -} - -// --- RGB MOD RAINBOW --- -void rgb_mod_rainbow_update(uint8_t red, uint8_t green, uint8_t blue, float brightness) { - for(uint8_t i = 0; i < SK6805_get_led_count(); i++) { - uint8_t r = red * (brightness); - uint8_t g = green * (brightness); - uint8_t b = blue * (brightness); - SK6805_set_led_color(i, r, g, b); - } - SK6805_update(); -} -// --- END OF RGB MOD RAINBOW --- diff --git a/applications/settings/notification_settings/rgb_backlight.h b/applications/settings/notification_settings/rgb_backlight.h deleted file mode 100644 index 66e0e26a1..000000000 --- a/applications/settings/notification_settings/rgb_backlight.h +++ /dev/null @@ -1,94 +0,0 @@ -/* - RGB backlight FlipperZero driver - Copyright (C) 2022-2023 Victor Nikitchuk (https://github.com/quen0n) - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . -*/ - -#include -#include "SK6805.h" - -typedef struct { - char* name; - uint8_t red; - uint8_t green; - uint8_t blue; -} RGBBacklightColor; - -typedef struct { - uint8_t version; - uint8_t display_color_index; - uint8_t custom_r; - uint8_t custom_g; - uint8_t custom_b; - bool settings_is_loaded; -} RGBBacklightSettings; - -/** - * @brief Получить текущие настройки RGB-подсветки - * - * @return Указатель на структуру настроек - */ -RGBBacklightSettings* rgb_backlight_get_settings(void); - -/** - * @brief Загрузить настройки подсветки с SD-карты - */ -void rgb_backlight_load_settings(void); - -/** - * @brief Сохранить текущие настройки RGB-подсветки - */ -void rgb_backlight_save_settings(void); - -/** - * @brief Применить текущие настройки RGB-подсветки - * - * @param brightness Яркость свечения (0-255) - * @param bypass Применить настройки принудительно - */ -void rgb_backlight_update(uint8_t brightness, bool bypass); - -/** - * @brief Установить цвет RGB-подсветки - * - * @param color_index Индекс цвета (0 - rgb_backlight_get_color_count()) - */ -void rgb_backlight_set_color(uint8_t color_index); - -/** - * @brief Set custom color values by index - 0=R 1=G 2=B - * - * @param color - color value (0-255) - * @param index - color index (0-2) 0=R 1=G 2=B - */ -void rgb_backlight_set_custom_color(uint8_t color, uint8_t index); - -/** - * @brief Получить количество доступных цветов - * - * @return Число доступных вариантов цвета - */ -uint8_t rgb_backlight_get_color_count(void); - -/** - * @brief Получить текстовое название цвета - * - * @param index Индекс из доступных вариантов цвета - * @return Указатель на строку с названием цвета - */ -const char* rgb_backlight_get_color_text(uint8_t index); - -// set custom color to display; -void rgb_mod_rainbow_update(uint8_t red, uint8_t green, uint8_t blue, float brightness); diff --git a/lib/drivers/SK6805.h b/lib/drivers/SK6805.h index c97054f6d..733f394ad 100644 --- a/lib/drivers/SK6805.h +++ b/lib/drivers/SK6805.h @@ -15,6 +15,9 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . */ +#ifdef __cplusplus +extern "C" { +#endif #ifndef SK6805_H_ #define SK6805_H_ @@ -49,3 +52,7 @@ void SK6805_set_led_color(uint8_t led_index, uint8_t r, uint8_t g, uint8_t b); void SK6805_update(void); #endif /* SK6805_H_ */ + +#ifdef __cplusplus +} +#endif diff --git a/targets/f7/api_symbols.csv b/targets/f7/api_symbols.csv index 310a669ef..f25b9c273 100644 --- a/targets/f7/api_symbols.csv +++ b/targets/f7/api_symbols.csv @@ -1,5 +1,5 @@ entry,status,name,type,params -Version,+,83.1,, +Version,+,84.1,, Header,+,applications/drivers/subghz/cc1101_ext/cc1101_ext_interconnect.h,, Header,+,applications/services/bt/bt_service/bt.h,, Header,+,applications/services/bt/bt_service/bt_keys_storage.h,, @@ -3125,6 +3125,9 @@ Function,-,putw,int,"int, FILE*" Function,-,qsort,void,"void*, size_t, size_t, __compar_fn_t" Function,-,qsort_r,void,"void*, size_t, size_t, int (*)(const void*, const void*, void*), void*" Function,-,quick_exit,void,int +Function,+,rainbow_timer_start,void,RGBBacklightApp* +Function,+,rainbow_timer_starter,void,RGBBacklightApp* +Function,+,rainbow_timer_stop,void,RGBBacklightApp* Function,+,rand,int, Function,-,rand_r,int,unsigned* Function,+,random,long, @@ -3143,8 +3146,12 @@ Function,-,remquol,long double,"long double, long double, int*" Function,-,rename,int,"const char*, const char*" Function,-,renameat,int,"int, const char*, int, const char*" Function,-,rewind,void,FILE* +Function,+,rgb_backlight_get_color_count,uint8_t, +Function,+,rgb_backlight_get_color_text,const char*,uint8_t +Function,+,rgb_backlight_set_static_color,void,"uint8_t, float" Function,+,rgb_backlight_settings_load,void,RGBBacklightSettings* Function,+,rgb_backlight_settings_save,void,const RGBBacklightSettings* +Function,+,rgb_backlight_update,void,float Function,-,rindex,char*,"const char*, int" Function,-,rint,double,double Function,-,rintf,float,float diff --git a/targets/f7/furi_hal/furi_hal_light.c b/targets/f7/furi_hal/furi_hal_light.c index 9ee542034..6cc60da11 100644 --- a/targets/f7/furi_hal/furi_hal_light.c +++ b/targets/f7/furi_hal/furi_hal_light.c @@ -3,7 +3,7 @@ #include #include #include -#include +#include "applications/services/rgb_backlight/rgb_backlight.h" #define LED_CURRENT_RED (50u) #define LED_CURRENT_GREEN (50u) @@ -46,7 +46,7 @@ void furi_hal_light_set(Light light, uint8_t value) { uint8_t prev = lp5562_get_channel_value(&furi_hal_i2c_handle_power, LP5562ChannelWhite); lp5562_execute_ramp( &furi_hal_i2c_handle_power, LP5562Engine1, LP5562ChannelWhite, prev, value, 100); - rgb_backlight_update(value, false); + rgb_backlight_update(value); } furi_hal_i2c_release(&furi_hal_i2c_handle_power); } From 9a8dcc340f8ef1f5865edec6f43079271e3261af Mon Sep 17 00:00:00 2001 From: Evgeny E <10674163+ssecsd@users.noreply.github.com> Date: Wed, 12 Mar 2025 13:25:37 +0000 Subject: [PATCH 115/268] fix: flipper detected before it was rebooted (#4146) --- scripts/testops.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/scripts/testops.py b/scripts/testops.py index 3100a9b7f..3dce51c22 100644 --- a/scripts/testops.py +++ b/scripts/testops.py @@ -37,15 +37,13 @@ class Main(App): self.logger.info(f"Attempting to find flipper with {retry_count} attempts.") for i in range(retry_count): + time.sleep(1) self.logger.info(f"Attempt to find flipper #{i}.") if port := resolve_port(self.logger, self.args.port): self.logger.info(f"Found flipper at {port}") - time.sleep(1) break - time.sleep(1) - if not port: self.logger.info(f"Failed to find flipper {port}") return None From e589cf7246d9aab24cfa7d9b5c2980ea4910147a Mon Sep 17 00:00:00 2001 From: Dmitry422 Date: Thu, 13 Mar 2025 00:42:03 +0700 Subject: [PATCH 116/268] Still working --- applications/services/rgb_backlight/rgb_backlight.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/applications/services/rgb_backlight/rgb_backlight.c b/applications/services/rgb_backlight/rgb_backlight.c index 64c04b9cb..bb1480793 100644 --- a/applications/services/rgb_backlight/rgb_backlight.c +++ b/applications/services/rgb_backlight/rgb_backlight.c @@ -71,8 +71,8 @@ void rgb_backlight_set_static_color(uint8_t index, float brightness) { SK6805_set_led_color(i, r, g, b); } - furi_record_close(RECORD_RGB_BACKLIGHT); SK6805_update(); + furi_record_close(RECORD_RGB_BACKLIGHT); } // void rgb_backlight_set_custom_color(uint8_t red, uint8_t green, uint8_t blue, float brightness) { @@ -208,6 +208,7 @@ int32_t rgb_backlight_srv (void* p) { // Define object app (full app with settings and running variables), // allocate memory and create record for access to app structure from outside RGBBacklightApp* app = malloc(sizeof(RGBBacklightApp)); + furi_record_create(RECORD_RGB_BACKLIGHT, app); //define rainbow_timer and they callback @@ -215,12 +216,13 @@ int32_t rgb_backlight_srv (void* p) { furi_timer_alloc(rainbow_timer_callback, FuriTimerTypePeriodic, app); // settings load or create default + app->settings = malloc(sizeof(RGBBacklightSettings)); rgb_backlight_settings_load (app->settings); // Init app variables app->current_red = 255; app->current_green = 60; - app->current_green = 0; + app->current_blue = 0; app->rainbow_stage = 1; // а нужно ли это все при старте сервиса, если мы получим сигнал на подсветку от Нотификейшена. Может вынести в ИНИТ и при старте нотиф дернуть его 1 раз From c9313c6f52c3d530e63e7f0ebc4138a5fdc905e3 Mon Sep 17 00:00:00 2001 From: Dmitry422 Date: Thu, 13 Mar 2025 18:06:39 +0700 Subject: [PATCH 117/268] still fucking )) --- .../services/notification/notification_app.c | 6 +- .../services/rgb_backlight/rgb_backlight.c | 88 ++++++++----------- .../services/rgb_backlight/rgb_backlight.h | 4 +- .../rgb_backlight/rgb_backlight_settings.c | 2 + .../notification_settings_app.c | 19 ++-- targets/f7/api_symbols.csv | 4 +- 6 files changed, 56 insertions(+), 67 deletions(-) diff --git a/applications/services/notification/notification_app.c b/applications/services/notification/notification_app.c index afb900706..df3bf213e 100644 --- a/applications/services/notification/notification_app.c +++ b/applications/services/notification/notification_app.c @@ -748,9 +748,9 @@ int32_t notification_srv(void* p) { notification_process_internal_message(app, &message); break; case SaveSettingsMessage: - //call rgb_mod_timer_control (start or stop) when we save settings - rainbow_timer_starter(app->rgb_srv); - rgb_backlight_settings_save(app->rgb_srv->settings); + // //call rgb_mod_timer_control (start or stop) when we save settings + // rainbow_timer_starter(app->rgb_srv); + // rgb_backlight_settings_save(app->rgb_srv->settings); notification_save_settings(app); break; case LoadSettingsMessage: diff --git a/applications/services/rgb_backlight/rgb_backlight.c b/applications/services/rgb_backlight/rgb_backlight.c index bb1480793..f449edbee 100644 --- a/applications/services/rgb_backlight/rgb_backlight.c +++ b/applications/services/rgb_backlight/rgb_backlight.c @@ -25,11 +25,11 @@ #include "rgb_backlight.h" -#define PREDEFINED_COLOR_COUNT (sizeof(colors) / sizeof(RGBBacklightPredefinedColor)) +#define COLOR_COUNT (sizeof(colors) / sizeof(RGBBacklightColor)) #define TAG "RGB_BACKLIGHT_SRV" -static const RGBBacklightPredefinedColor colors[] = { +static const RGBBacklightColor colors[] = { {"Orange", 255, 60, 0}, {"Yellow", 255, 144, 0}, {"Spring", 167, 255, 0}, @@ -47,43 +47,32 @@ static const RGBBacklightPredefinedColor colors[] = { }; uint8_t rgb_backlight_get_color_count(void) { - return PREDEFINED_COLOR_COUNT; + return COLOR_COUNT; } const char* rgb_backlight_get_color_text(uint8_t index) { return colors[index].name; } - -void rgb_backlight_set_static_color(uint8_t index, float brightness) { - // use RECORD for acces to rgb service instance and use current_* colors and static colors +// use RECORD for acces to rgb service instance and update current colors by static +void rgb_backlight_set_static_color(uint8_t index) { RGBBacklightApp* app = furi_record_open(RECORD_RGB_BACKLIGHT); - - for(uint8_t i = 0; i < SK6805_get_led_count(); i++) { - app->current_red = colors[index].red; - app->current_green = colors[index].green; - app->current_blue = colors[index].blue; - - uint8_t r = app->current_red * brightness; - uint8_t g = app->current_green * brightness; - uint8_t b = app->current_blue * brightness; - SK6805_set_led_color(i, r, g, b); - } - - SK6805_update(); + app->current_red = colors[index].red; + app->current_green = colors[index].green; + app->current_blue = colors[index].blue; + furi_record_close(RECORD_RGB_BACKLIGHT); } -// void rgb_backlight_set_custom_color(uint8_t red, uint8_t green, uint8_t blue, float brightness) { -// for(uint8_t i = 0; i < SK6805_get_led_count(); i++) { -// uint8_t r = red * (brightness); -// uint8_t g = green * (brightness); -// uint8_t b = blue * (brightness); -// SK6805_set_led_color(i, r, g, b); -// } -// SK6805_update(); -// } +// use RECORD for acces to rgb service instance and update current colors by custom +void rgb_backlight_set_custom_color(uint8_t red, uint8_t green, uint8_t blue) { + RGBBacklightApp* app = furi_record_open(RECORD_RGB_BACKLIGHT); + app->current_red = red; + app->current_green = green; + app->current_blue = blue; + furi_record_close(RECORD_RGB_BACKLIGHT); +} // apply new brightness to current display color set void rgb_backlight_update (float brightness) { @@ -96,10 +85,10 @@ void rgb_backlight_update (float brightness) { uint8_t b = app->current_blue * brightness; SK6805_set_led_color(i, r, g, b); } - - furi_record_close(RECORD_RGB_BACKLIGHT); SK6805_update(); + furi_record_close(RECORD_RGB_BACKLIGHT); } + //start furi timer for rainbow void rainbow_timer_start(RGBBacklightApp* app) { furi_timer_start( @@ -115,11 +104,6 @@ void rainbow_timer_stop(RGBBacklightApp* app) { void rainbow_timer_starter(RGBBacklightApp* app) { if(app->settings->rgb_mod_installed) { if(app->settings->rainbow_mode > 0) { - // rgb_backlight_set_custom_color( - // app->rainbow_red, - // app->rainbow_green, - // app->rainbow_blue, - // app->settings->brightness); rainbow_timer_start(app); } else { if(furi_timer_is_running(app->rainbow_timer)) { @@ -133,7 +117,7 @@ static void rainbow_timer_callback(void* context) { furi_assert(context); RGBBacklightApp* app = context; - // if rgb_mode_rainbow_mode is rainbow do rainbow effect + // if rainbow_mode is rainbow do rainbow effect if(app->settings->rainbow_mode == 1) { switch(app->rainbow_stage) { // from red to yellow (255,0,0) - (255,255,0) @@ -187,18 +171,12 @@ static void rainbow_timer_callback(void* context) { default: break; } - - // rgb_backlight_set_custom_color( - // app->current_red, - // app->current_green, - // app->current_blue, - // app->settings->brightness); - } - + } + //rgb_backlight_set_custom_color(app->current_red,app->current_green,app->current_blue); rgb_backlight_update (app->settings->brightness); - // if rgb_mode_rainbow_mode is ..... do another effect - // if(app->settings.rainbow_mode == 2) { + // if rainbow_mode is ..... do another effect + // if(app->settings.rainbow_mode == ...) { // } } @@ -206,7 +184,7 @@ int32_t rgb_backlight_srv (void* p) { UNUSED(p); // Define object app (full app with settings and running variables), - // allocate memory and create record for access to app structure from outside + // allocate memory and create RECORD for access to app structure from outside RGBBacklightApp* app = malloc(sizeof(RGBBacklightApp)); furi_record_create(RECORD_RGB_BACKLIGHT, app); @@ -220,22 +198,28 @@ int32_t rgb_backlight_srv (void* p) { rgb_backlight_settings_load (app->settings); // Init app variables - app->current_red = 255; - app->current_green = 60; - app->current_blue = 0; app->rainbow_stage = 1; + // app->current_red = 255; + // app->current_green = 60; + // app->current_blue = 0; + // а нужно ли это все при старте сервиса, если мы получим сигнал на подсветку от Нотификейшена. Может вынести в ИНИТ и при старте нотиф дернуть его 1 раз // if rgb_mod_installed start rainbow or set static color from settings (default index = 0) if(app->settings->rgb_mod_installed) { if(app->settings->rainbow_mode > 0 ) { + // app->current_red = 255; + // app->current_green = 0; + // app->current_blue = 0; rainbow_timer_starter(app); } else { - rgb_backlight_set_static_color(app->settings->static_color_index, app->settings->brightness); + rgb_backlight_set_static_color(app->settings->static_color_index); + rgb_backlight_update (app->settings->brightness); } // if not rgb_mod_installed set default static orange color (index=0) } else { - rgb_backlight_set_static_color(0, app->settings->brightness); + rgb_backlight_set_static_color(0); + rgb_backlight_update (app->settings->brightness); } while(1) { diff --git a/applications/services/rgb_backlight/rgb_backlight.h b/applications/services/rgb_backlight/rgb_backlight.h index 531d3a9e7..b0f5530ee 100644 --- a/applications/services/rgb_backlight/rgb_backlight.h +++ b/applications/services/rgb_backlight/rgb_backlight.h @@ -31,7 +31,7 @@ typedef struct { uint8_t red; uint8_t green; uint8_t blue; -} RGBBacklightPredefinedColor; +} RGBBacklightColor; typedef struct { FuriTimer* rainbow_timer; @@ -48,7 +48,7 @@ typedef struct { #define RECORD_RGB_BACKLIGHT "rgb_backlight" void rgb_backlight_update (float brightness); -void rgb_backlight_set_static_color(uint8_t index, float brightness); +void rgb_backlight_set_static_color(uint8_t index); void rainbow_timer_stop(RGBBacklightApp* app); void rainbow_timer_start(RGBBacklightApp* app); void rainbow_timer_starter(RGBBacklightApp* app); diff --git a/applications/services/rgb_backlight/rgb_backlight_settings.c b/applications/services/rgb_backlight/rgb_backlight_settings.c index 9cf1c1440..45a7ae65e 100644 --- a/applications/services/rgb_backlight/rgb_backlight_settings.c +++ b/applications/services/rgb_backlight/rgb_backlight_settings.c @@ -91,5 +91,7 @@ void rgb_backlight_settings_save(const RGBBacklightSettings* settings) { if(!success) { FURI_LOG_E(TAG, "Failed to save rgb_backlight_settings file"); + } else { + FURI_LOG_I(TAG, "Settings saved"); } } diff --git a/applications/settings/notification_settings/notification_settings_app.c b/applications/settings/notification_settings/notification_settings_app.c index c2262d327..c62c78dd8 100644 --- a/applications/settings/notification_settings/notification_settings_app.c +++ b/applications/settings/notification_settings/notification_settings_app.c @@ -246,12 +246,13 @@ static void rgb_mod_rainbow_changed(VariableItem* item) { variable_item_set_locked(t_item, false, "Rainbow mode\nenabled!"); } } - //save settings and start/stop rgb_mod_rainbow_timer - notification_message_save_settings(app->notification); + + rainbow_timer_starter(app->notification->rgb_srv); + rgb_backlight_settings_save(app->notification->rgb_srv->settings); // restore saved rgb backlight settings if we switch_off rainbow mode if(app->notification->rgb_srv->settings->rainbow_mode == 0) { - rgb_backlight_update(app->notification->settings.display_brightness); + rgb_backlight_update(app->notification->rgb_srv->settings->brightness); } } @@ -271,16 +272,17 @@ static void rgb_mod_rainbow_step_changed(VariableItem* item) { app->notification->rgb_srv->settings->rainbow_step = rgb_mod_rainbow_step_value[index]; } -// --- Set RGB backlight colors --- +// Set rgb_backlight colors static and custom static void color_changed(VariableItem* item) { NotificationAppSettings* app = variable_item_get_context(item); uint8_t index = variable_item_get_current_value_index(item); - rgb_backlight_set_static_color(index,app->notification->rgb_srv->settings->brightness); + rgb_backlight_set_static_color(index); variable_item_set_current_value_text(item, rgb_backlight_get_color_text(index)); + + rgb_backlight_update(app->notification->rgb_srv->settings->brightness); notification_message(app->notification, &sequence_display_backlight_on); } -// TODO: refactor and fix this static void color_set_custom_red(VariableItem* item) { NotificationAppSettings* app = variable_item_get_context(item); uint8_t index = variable_item_get_current_value_index(item); @@ -289,8 +291,7 @@ static void color_set_custom_red(VariableItem* item) { app->notification->rgb_srv->settings->custom_red = index; app->notification->rgb_srv->current_red = index; app->notification->rgb_srv->settings->static_color_index=13; - rgb_backlight_update(app->notification->rgb_srv->settings->brightness); - + char valtext[4] = {}; snprintf(valtext, sizeof(valtext), "%d", index); variable_item_set_current_value_text(item, valtext); @@ -299,6 +300,8 @@ static void color_set_custom_red(VariableItem* item) { // Set to custom color explicitly variable_item_set_current_value_index(temp_item, 13); variable_item_set_current_value_text(temp_item, rgb_backlight_get_color_text(13)); + + rgb_backlight_update(app->notification->rgb_srv->settings->brightness); notification_message(app->notification, &sequence_display_backlight_on); } static void color_set_custom_green(VariableItem* item) { diff --git a/targets/f7/api_symbols.csv b/targets/f7/api_symbols.csv index f25b9c273..ecb5047e8 100644 --- a/targets/f7/api_symbols.csv +++ b/targets/f7/api_symbols.csv @@ -1,5 +1,5 @@ entry,status,name,type,params -Version,+,84.1,, +Version,+,86.0,, Header,+,applications/drivers/subghz/cc1101_ext/cc1101_ext_interconnect.h,, Header,+,applications/services/bt/bt_service/bt.h,, Header,+,applications/services/bt/bt_service/bt_keys_storage.h,, @@ -3148,7 +3148,7 @@ Function,-,renameat,int,"int, const char*, int, const char*" Function,-,rewind,void,FILE* Function,+,rgb_backlight_get_color_count,uint8_t, Function,+,rgb_backlight_get_color_text,const char*,uint8_t -Function,+,rgb_backlight_set_static_color,void,"uint8_t, float" +Function,+,rgb_backlight_set_static_color,void,uint8_t Function,+,rgb_backlight_settings_load,void,RGBBacklightSettings* Function,+,rgb_backlight_settings_save,void,const RGBBacklightSettings* Function,+,rgb_backlight_update,void,float From 9b3d737693c072a91ed7bf47548358b168fa3ff0 Mon Sep 17 00:00:00 2001 From: Dmitry422 Date: Fri, 14 Mar 2025 02:18:14 +0700 Subject: [PATCH 118/268] Moving RGB Backlight setings and effect to rgb service finished. --- .../services/notification/notification_app.c | 3 - .../services/rgb_backlight/rgb_backlight.c | 95 ++++----- .../services/rgb_backlight/rgb_backlight.h | 3 +- .../rgb_backlight/rgb_backlight_settings.c | 8 +- .../notification_settings_app.c | 193 +++++++++++------- targets/f7/api_symbols.csv | 3 +- targets/f7/furi_hal/furi_hal_light.c | 36 ++-- 7 files changed, 194 insertions(+), 147 deletions(-) mode change 100644 => 100755 targets/f7/api_symbols.csv diff --git a/applications/services/notification/notification_app.c b/applications/services/notification/notification_app.c index df3bf213e..4bd93cfbc 100644 --- a/applications/services/notification/notification_app.c +++ b/applications/services/notification/notification_app.c @@ -748,9 +748,6 @@ int32_t notification_srv(void* p) { notification_process_internal_message(app, &message); break; case SaveSettingsMessage: - // //call rgb_mod_timer_control (start or stop) when we save settings - // rainbow_timer_starter(app->rgb_srv); - // rgb_backlight_settings_save(app->rgb_srv->settings); notification_save_settings(app); break; case LoadSettingsMessage: diff --git a/applications/services/rgb_backlight/rgb_backlight.c b/applications/services/rgb_backlight/rgb_backlight.c index f449edbee..5510ea916 100644 --- a/applications/services/rgb_backlight/rgb_backlight.c +++ b/applications/services/rgb_backlight/rgb_backlight.c @@ -24,7 +24,6 @@ #include #include "rgb_backlight.h" - #define COLOR_COUNT (sizeof(colors) / sizeof(RGBBacklightColor)) #define TAG "RGB_BACKLIGHT_SRV" @@ -47,21 +46,27 @@ static const RGBBacklightColor colors[] = { }; uint8_t rgb_backlight_get_color_count(void) { - return COLOR_COUNT; - } + return COLOR_COUNT; +} const char* rgb_backlight_get_color_text(uint8_t index) { - return colors[index].name; - } + return colors[index].name; +} // use RECORD for acces to rgb service instance and update current colors by static void rgb_backlight_set_static_color(uint8_t index) { RGBBacklightApp* app = furi_record_open(RECORD_RGB_BACKLIGHT); - app->current_red = colors[index].red; - app->current_green = colors[index].green; - app->current_blue = colors[index].blue; - + //if user select "custom" value then set current colors by custom values + if(index == 13) { + app->current_red = app->settings->custom_red; + app->current_green = app->settings->custom_green; + app->current_blue = app->settings->custom_blue; + } else { + app->current_red = colors[index].red; + app->current_green = colors[index].green; + app->current_blue = colors[index].blue; + } furi_record_close(RECORD_RGB_BACKLIGHT); } @@ -74,15 +79,14 @@ void rgb_backlight_set_custom_color(uint8_t red, uint8_t green, uint8_t blue) { furi_record_close(RECORD_RGB_BACKLIGHT); } -// apply new brightness to current display color set -void rgb_backlight_update (float brightness) { - // use RECORD for acces to rgb service instance and use current_* colors +// use RECORD for acces to rgb service instance, use current_* colors and update backlight +void rgb_backlight_update(float brightness) { RGBBacklightApp* app = furi_record_open(RECORD_RGB_BACKLIGHT); - + for(uint8_t i = 0; i < SK6805_get_led_count(); i++) { - uint8_t r = app->current_red * brightness; - uint8_t g = app->current_green * brightness; - uint8_t b = app->current_blue * brightness; + uint8_t r = app->current_red * (brightness / 1.0f); + uint8_t g = app->current_green * (brightness / 1.0f); + uint8_t b = app->current_blue * (brightness / 1.0f); SK6805_set_led_color(i, r, g, b); } SK6805_update(); @@ -91,8 +95,7 @@ void rgb_backlight_update (float brightness) { //start furi timer for rainbow void rainbow_timer_start(RGBBacklightApp* app) { - furi_timer_start( - app->rainbow_timer, furi_ms_to_ticks(app->settings->rainbow_speed_ms)); + furi_timer_start(app->rainbow_timer, furi_ms_to_ticks(app->settings->rainbow_speed_ms)); } //stop furi timer for rainbow @@ -102,17 +105,15 @@ void rainbow_timer_stop(RGBBacklightApp* app) { // if rgb_mod_installed then apply rainbow colors to backlight and start/restart/stop rainbow_timer void rainbow_timer_starter(RGBBacklightApp* app) { - if(app->settings->rgb_mod_installed) { - if(app->settings->rainbow_mode > 0) { - rainbow_timer_start(app); - } else { - if(furi_timer_is_running(app->rainbow_timer)) { - rainbow_timer_stop(app); - } + + if((app->settings->rainbow_mode > 0) && (app->settings->rgb_mod_installed)) { + rainbow_timer_start(app); + } else { + if(furi_timer_is_running(app->rainbow_timer)) { + rainbow_timer_stop(app); } } } - static void rainbow_timer_callback(void* context) { furi_assert(context); RGBBacklightApp* app = context; @@ -171,60 +172,50 @@ static void rainbow_timer_callback(void* context) { default: break; } - } - //rgb_backlight_set_custom_color(app->current_red,app->current_green,app->current_blue); - rgb_backlight_update (app->settings->brightness); + } + rgb_backlight_update(app->settings->brightness); // if rainbow_mode is ..... do another effect // if(app->settings.rainbow_mode == ...) { // } } -int32_t rgb_backlight_srv (void* p) { +int32_t rgb_backlight_srv(void* p) { UNUSED(p); - // Define object app (full app with settings and running variables), + // Define object app (full app with settings and running variables), // allocate memory and create RECORD for access to app structure from outside RGBBacklightApp* app = malloc(sizeof(RGBBacklightApp)); - furi_record_create(RECORD_RGB_BACKLIGHT, app); //define rainbow_timer and they callback - app->rainbow_timer = - furi_timer_alloc(rainbow_timer_callback, FuriTimerTypePeriodic, app); + app->rainbow_timer = furi_timer_alloc(rainbow_timer_callback, FuriTimerTypePeriodic, app); // settings load or create default app->settings = malloc(sizeof(RGBBacklightSettings)); - rgb_backlight_settings_load (app->settings); + rgb_backlight_settings_load(app->settings); // Init app variables app->rainbow_stage = 1; - // app->current_red = 255; - // app->current_green = 60; - // app->current_blue = 0; - - // а нужно ли это все при старте сервиса, если мы получим сигнал на подсветку от Нотификейшена. Может вынести в ИНИТ и при старте нотиф дернуть его 1 раз - // if rgb_mod_installed start rainbow or set static color from settings (default index = 0) + // if rgb mod installed - start rainbow or set static color from settings (default index = 0) if(app->settings->rgb_mod_installed) { - if(app->settings->rainbow_mode > 0 ) { - // app->current_red = 255; - // app->current_green = 0; - // app->current_blue = 0; + if(app->settings->rainbow_mode > 0) { rainbow_timer_starter(app); } else { rgb_backlight_set_static_color(app->settings->static_color_index); - rgb_backlight_update (app->settings->brightness); + rgb_backlight_update(app->settings->brightness); } - // if not rgb_mod_installed set default static orange color (index=0) + // if rgb mod not installed - set default static orange color (index=0) } else { rgb_backlight_set_static_color(0); - rgb_backlight_update (app->settings->brightness); - } + rgb_backlight_update(app->settings->brightness); + } while(1) { - FURI_LOG_I(TAG, "working"); - furi_delay_ms(2000); + // place for message queue and other future options + furi_delay_ms(5000); + FURI_LOG_I(TAG, "Service is running"); } return 0; -} \ No newline at end of file +} diff --git a/applications/services/rgb_backlight/rgb_backlight.h b/applications/services/rgb_backlight/rgb_backlight.h index b0f5530ee..fd683bf16 100644 --- a/applications/services/rgb_backlight/rgb_backlight.h +++ b/applications/services/rgb_backlight/rgb_backlight.h @@ -47,7 +47,8 @@ typedef struct { #define RECORD_RGB_BACKLIGHT "rgb_backlight" -void rgb_backlight_update (float brightness); +void rgb_backlight_update(float brightness); +void rgb_backlight_set_custom_color(uint8_t red, uint8_t green, uint8_t blue); void rgb_backlight_set_static_color(uint8_t index); void rainbow_timer_stop(RGBBacklightApp* app); void rainbow_timer_start(RGBBacklightApp* app); diff --git a/applications/services/rgb_backlight/rgb_backlight_settings.c b/applications/services/rgb_backlight/rgb_backlight_settings.c index 45a7ae65e..7e69eb0dc 100644 --- a/applications/services/rgb_backlight/rgb_backlight_settings.c +++ b/applications/services/rgb_backlight/rgb_backlight_settings.c @@ -48,7 +48,8 @@ void rgb_backlight_settings_load(RGBBacklightSettings* settings) { RGB_BACKLIGHT_SETTINGS_VER); // if config previous version - load it and inicialize new settings } else if(version == RGB_BACKLIGHT_SETTINGS_VER_PREV) { - RGBBacklightSettingsPrevious* settings_previous = malloc(sizeof(RGBBacklightSettingsPrevious)); + RGBBacklightSettingsPrevious* settings_previous = + malloc(sizeof(RGBBacklightSettingsPrevious)); success = saved_struct_load( RGB_BACKLIGHT_SETTINGS_PATH, @@ -73,8 +74,11 @@ void rgb_backlight_settings_load(RGBBacklightSettings* settings) { // fill settings with 0 and save to settings file; // Orange color (index=0) will be default if(!success) { - FURI_LOG_W(TAG, "Failed to load file, using defaults 0"); + FURI_LOG_W(TAG, "Failed to load file, using defaults"); memset(settings, 0, sizeof(RGBBacklightSettings)); + settings->brightness = 1.0f; + settings->rainbow_speed_ms = 100; + settings->rainbow_step = 1; rgb_backlight_settings_save(settings); } } diff --git a/applications/settings/notification_settings/notification_settings_app.c b/applications/settings/notification_settings/notification_settings_app.c index c62c78dd8..82e4526ae 100644 --- a/applications/settings/notification_settings/notification_settings_app.c +++ b/applications/settings/notification_settings/notification_settings_app.c @@ -4,6 +4,7 @@ #include #include #include +#include "applications/services/rgb_backlight/rgb_backlight.h" #define MAX_NOTIFICATION_SETTINGS 4 @@ -108,13 +109,13 @@ const char* const vibro_text[VIBRO_COUNT] = { }; const bool vibro_value[VIBRO_COUNT] = {false, true}; -// --- RGB MOD RAINBOW --- -#define RGB_MOD_COUNT 2 -const char* const rgb_mod_text[RGB_MOD_COUNT] = { +// --- RGB BACKLIGHT --- +#define RGB_MOD_INSTALLED_COUNT 2 +const char* const rgb_mod_installed_text[RGB_MOD_INSTALLED_COUNT] = { "OFF", "ON", }; -const bool rgb_mod_value[RGB_MOD_COUNT] = {false, true}; +const bool rgb_mod_installed_value[RGB_MOD_INSTALLED_COUNT] = {false, true}; #define RGB_MOD_RAINBOW_MODE_COUNT 2 const char* const rgb_mod_rainbow_mode_text[RGB_MOD_RAINBOW_MODE_COUNT] = { @@ -152,7 +153,7 @@ typedef enum { RGBViewId, } ViewId; -// --- END OF RGB MOD RAINBOW --- +// --- RGB BACKLIGHT END --- static void contrast_changed(VariableItem* item) { NotificationAppSettings* app = variable_item_get_context(item); @@ -169,8 +170,13 @@ static void backlight_changed(VariableItem* item) { variable_item_set_current_value_text(item, backlight_text[index]); app->notification->settings.display_brightness = backlight_value[index]; - //save brightness to rgb backlight settings too + + //--- RGB BACKLIGHT --- + //set selected brightness to current rgb backlight service settings and save settings app->notification->rgb_srv->settings->brightness = backlight_value[index]; + rgb_backlight_settings_save(app->notification->rgb_srv->settings); + //--- RGB BACKLIGHT END --- + notification_message(app->notification, &sequence_display_backlight_on); } @@ -220,130 +226,155 @@ static void vibro_changed(VariableItem* item) { notification_message(app->notification, &sequence_single_vibro); } -// --- RGB MOD AND RAINBOW --- +//--- RGB BACKLIGHT --- static void rgb_mod_installed_changed(VariableItem* item) { NotificationAppSettings* app = variable_item_get_context(item); uint8_t index = variable_item_get_current_value_index(item); - variable_item_set_current_value_text(item, rgb_mod_text[index]); - app->notification->rgb_srv->settings->rgb_mod_installed = rgb_mod_value[index]; + variable_item_set_current_value_text(item, rgb_mod_installed_text[index]); + app->notification->rgb_srv->settings->rgb_mod_installed = rgb_mod_installed_value[index]; + rgb_backlight_settings_save(app->notification->rgb_srv->settings); + // Lock/Unlock rgb settings depent from rgb_mod_installed switch + int slide = 0; + if(furi_hal_rtc_is_flag_set(FuriHalRtcFlagDebug)) { + slide = 1; + } + for(int i = slide; i < (slide + 7); i++) { + VariableItem* t_item = variable_item_list_get(app->variable_item_list_rgb, i); + if(index == 0) { + variable_item_set_locked(t_item, true, "RGB MOD\nOFF!"); + } else { + variable_item_set_locked(t_item, false, "RGB MOD\nOFF!"); + } + } } static void rgb_mod_rainbow_changed(VariableItem* item) { NotificationAppSettings* app = variable_item_get_context(item); uint8_t index = variable_item_get_current_value_index(item); + variable_item_set_current_value_text(item, rgb_mod_rainbow_mode_text[index]); app->notification->rgb_srv->settings->rainbow_mode = rgb_mod_rainbow_mode_value[index]; + rainbow_timer_starter(app->notification->rgb_srv); + rgb_backlight_settings_save(app->notification->rgb_srv->settings); + // Lock/Unlock color settings if rainbow mode Enabled/Disabled (0-3 index if debug off and 1-4 index if debug on) int slide = 0; - if (furi_hal_rtc_is_flag_set(FuriHalRtcFlagDebug)) {slide = 1;} - for(int i = slide; i < (slide+4); i++) { + if(furi_hal_rtc_is_flag_set(FuriHalRtcFlagDebug)) { + slide = 1; + } + for(int i = slide; i < (slide + 4); i++) { VariableItem* t_item = variable_item_list_get(app->variable_item_list_rgb, i); - if(app->notification->rgb_srv->settings->rainbow_mode > 0) { + if(index > 0) { variable_item_set_locked(t_item, true, "Rainbow mode\nenabled!"); } else { variable_item_set_locked(t_item, false, "Rainbow mode\nenabled!"); } } - - rainbow_timer_starter(app->notification->rgb_srv); - rgb_backlight_settings_save(app->notification->rgb_srv->settings); // restore saved rgb backlight settings if we switch_off rainbow mode if(app->notification->rgb_srv->settings->rainbow_mode == 0) { - rgb_backlight_update(app->notification->rgb_srv->settings->brightness); + rgb_backlight_set_static_color(app->notification->rgb_srv->settings->static_color_index); + rgb_backlight_update(app->notification->rgb_srv->settings->brightness); } } static void rgb_mod_rainbow_speed_changed(VariableItem* item) { NotificationAppSettings* app = variable_item_get_context(item); uint8_t index = variable_item_get_current_value_index(item); + variable_item_set_current_value_text(item, rgb_mod_rainbow_speed_text[index]); app->notification->rgb_srv->settings->rainbow_speed_ms = rgb_mod_rainbow_speed_value[index]; - //use message for restart rgb_mod_rainbow_timer with new delay - notification_message_save_settings(app->notification); + + //save settings and restart timer with new speed value + rgb_backlight_settings_save(app->notification->rgb_srv->settings); + rainbow_timer_starter(app->notification->rgb_srv); } static void rgb_mod_rainbow_step_changed(VariableItem* item) { NotificationAppSettings* app = variable_item_get_context(item); uint8_t index = variable_item_get_current_value_index(item); + variable_item_set_current_value_text(item, rgb_mod_rainbow_step_text[index]); app->notification->rgb_srv->settings->rainbow_step = rgb_mod_rainbow_step_value[index]; + + rgb_backlight_settings_save(app->notification->rgb_srv->settings); } // Set rgb_backlight colors static and custom + static void color_changed(VariableItem* item) { NotificationAppSettings* app = variable_item_get_context(item); uint8_t index = variable_item_get_current_value_index(item); - rgb_backlight_set_static_color(index); - variable_item_set_current_value_text(item, rgb_backlight_get_color_text(index)); + variable_item_set_current_value_text(item, rgb_backlight_get_color_text(index)); + app->notification->rgb_srv->settings->static_color_index = index; + + rgb_backlight_set_static_color(index); rgb_backlight_update(app->notification->rgb_srv->settings->brightness); - notification_message(app->notification, &sequence_display_backlight_on); + rgb_backlight_settings_save(app->notification->rgb_srv->settings); } static void color_set_custom_red(VariableItem* item) { NotificationAppSettings* app = variable_item_get_context(item); uint8_t index = variable_item_get_current_value_index(item); - + //Set custom red to settings and current color app->notification->rgb_srv->settings->custom_red = index; app->notification->rgb_srv->current_red = index; - app->notification->rgb_srv->settings->static_color_index=13; - + app->notification->rgb_srv->settings->static_color_index = 13; + char valtext[4] = {}; snprintf(valtext, sizeof(valtext), "%d", index); variable_item_set_current_value_text(item, valtext); - - + // Set to custom color explicitly variable_item_set_current_value_index(temp_item, 13); variable_item_set_current_value_text(temp_item, rgb_backlight_get_color_text(13)); - + rgb_backlight_update(app->notification->rgb_srv->settings->brightness); - notification_message(app->notification, &sequence_display_backlight_on); + rgb_backlight_settings_save(app->notification->rgb_srv->settings); } static void color_set_custom_green(VariableItem* item) { NotificationAppSettings* app = variable_item_get_context(item); uint8_t index = variable_item_get_current_value_index(item); - - //Set custom green to settings and current color - app->notification->rgb_srv->settings->custom_green = index; - app->notification->rgb_srv->current_green = index; - app->notification->rgb_srv->settings->static_color_index=13; - rgb_backlight_update(app->notification->rgb_srv->settings->brightness); - - char valtext[4] = {}; - snprintf(valtext, sizeof(valtext), "%d", index); - variable_item_set_current_value_text(item, valtext); - // rgb_backlight_set_color(13); - // rgb_backlight_update(app->rgb_srv->settings->brightness); - // Set to custom color explicitly - variable_item_set_current_value_index(temp_item, 13); - variable_item_set_current_value_text(temp_item, rgb_backlight_get_color_text(13)); - notification_message(app->notification, &sequence_display_backlight_on); -} -static void color_set_custom_blue(VariableItem* item) { - NotificationAppSettings* app = variable_item_get_context(item); - uint8_t index = variable_item_get_current_value_index(item); - //Set custom blue to settings and current color - app->notification->rgb_srv->settings->custom_blue = index; - app->notification->rgb_srv->current_blue = index; - app->notification->rgb_srv->settings->static_color_index=13; - rgb_backlight_update(app->notification->rgb_srv->settings->brightness); + //Set custom green to settings and current color + app->notification->rgb_srv->settings->custom_green = index; + app->notification->rgb_srv->current_green = index; + app->notification->rgb_srv->settings->static_color_index = 13; char valtext[4] = {}; snprintf(valtext, sizeof(valtext), "%d", index); variable_item_set_current_value_text(item, valtext); - // rgb_backlight_set_color(13); - // rgb_backlight_update(app->rgb_srv->settings->brightness); - + // Set to custom color explicitly variable_item_set_current_value_index(temp_item, 13); variable_item_set_current_value_text(temp_item, rgb_backlight_get_color_text(13)); - notification_message(app->notification, &sequence_display_backlight_on); + + rgb_backlight_update(app->notification->rgb_srv->settings->brightness); + rgb_backlight_settings_save(app->notification->rgb_srv->settings); +} +static void color_set_custom_blue(VariableItem* item) { + NotificationAppSettings* app = variable_item_get_context(item); + uint8_t index = variable_item_get_current_value_index(item); + + //Set custom blue to settings and current color + app->notification->rgb_srv->settings->custom_blue = index; + app->notification->rgb_srv->current_blue = index; + app->notification->rgb_srv->settings->static_color_index = 13; + + char valtext[4] = {}; + snprintf(valtext, sizeof(valtext), "%d", index); + variable_item_set_current_value_text(item, valtext); + + // Set to custom color explicitly + variable_item_set_current_value_index(temp_item, 13); + variable_item_set_current_value_text(temp_item, rgb_backlight_get_color_text(13)); + + rgb_backlight_update(app->notification->rgb_srv->settings->brightness); + rgb_backlight_settings_save(app->notification->rgb_srv->settings); } // open rgb_settings_view if user press OK on first (index=0) menu string and (debug mode or rgb_mod_install is true) @@ -363,7 +394,7 @@ static uint32_t notification_app_rgb_settings_exit(void* context) { UNUSED(context); return MainViewId; } -// --- END OF RGB MOD AND RAINBOW --- +//--- RGB BACKLIGHT END --- static uint32_t notification_app_settings_exit(void* context) { UNUSED(context); @@ -378,21 +409,23 @@ static NotificationAppSettings* alloc_settings(void) { app->variable_item_list = variable_item_list_alloc(); View* view = variable_item_list_get_view(app->variable_item_list); - //set callback for exit from view - view_set_previous_callback(view, notification_app_settings_exit); - - // set callback for OK pressed in menu - variable_item_list_set_enter_callback( - app->variable_item_list, variable_item_list_enter_callback, app); - VariableItem* item; uint8_t value_index; + //set callback for exit from main view + view_set_previous_callback(view, notification_app_settings_exit); + + //--- RGB BACKLIGHT --- + // set callback for OK pressed in notification settings menu + variable_item_list_set_enter_callback( + app->variable_item_list, variable_item_list_enter_callback, app); + //Show RGB settings only when debug_mode or rgb_mod_installed is active if((app->notification->rgb_srv->settings->rgb_mod_installed) || (furi_hal_rtc_is_flag_set(FuriHalRtcFlagDebug))) { item = variable_item_list_add(app->variable_item_list, "RGB mod settings", 0, NULL, app); } + //--- RGB BACKLIGHT END --- item = variable_item_list_add( app->variable_item_list, "LCD Contrast", CONTRAST_COUNT, contrast_changed, app); @@ -450,9 +483,10 @@ static NotificationAppSettings* alloc_settings(void) { variable_item_set_current_value_text(item, vibro_text[value_index]); } - // --- RGB SETTINGS VIEW --- + //--- RGB BACKLIGHT --- app->variable_item_list_rgb = variable_item_list_alloc(); View* view_rgb = variable_item_list_get_view(app->variable_item_list_rgb); + // set callback for OK pressed in rgb_settings_menu view_set_previous_callback(view_rgb, notification_app_rgb_settings_exit); @@ -461,16 +495,18 @@ static NotificationAppSettings* alloc_settings(void) { item = variable_item_list_add( app->variable_item_list_rgb, "RGB MOD Installed", - RGB_MOD_COUNT, + RGB_MOD_INSTALLED_COUNT, rgb_mod_installed_changed, app); value_index = value_index_bool( - app->notification->rgb_srv->settings->rgb_mod_installed, rgb_mod_value, RGB_MOD_COUNT); + app->notification->rgb_srv->settings->rgb_mod_installed, + rgb_mod_installed_value, + RGB_MOD_INSTALLED_COUNT); variable_item_set_current_value_index(item, value_index); - variable_item_set_current_value_text(item, rgb_mod_text[value_index]); + variable_item_set_current_value_text(item, rgb_mod_installed_text[value_index]); } - // RGB Colors settings + // Static Colors settings item = variable_item_list_add( app->variable_item_list_rgb, "LCD Color", @@ -482,6 +518,9 @@ static NotificationAppSettings* alloc_settings(void) { variable_item_set_current_value_text(item, rgb_backlight_get_color_text(value_index)); variable_item_set_locked( item, (app->notification->rgb_srv->settings->rainbow_mode > 0), "Rainbow mode\nenabled!"); + variable_item_set_locked( + item, (app->notification->rgb_srv->settings->rgb_mod_installed == 0), "RGB MOD \nOFF!"); + temp_item = item; // Custom Color - REFACTOR THIS @@ -494,6 +533,8 @@ static NotificationAppSettings* alloc_settings(void) { variable_item_set_current_value_text(item, valtext); variable_item_set_locked( item, (app->notification->rgb_srv->settings->rainbow_mode > 0), "Rainbow mode\nenabled!"); + variable_item_set_locked( + item, (app->notification->rgb_srv->settings->rgb_mod_installed == 0), "RGB MOD \nOFF!"); item = variable_item_list_add( app->variable_item_list_rgb, "Custom Green", 255, color_set_custom_green, app); @@ -503,6 +544,8 @@ static NotificationAppSettings* alloc_settings(void) { variable_item_set_current_value_text(item, valtext); variable_item_set_locked( item, (app->notification->rgb_srv->settings->rainbow_mode > 0), "Rainbow mode\nenabled!"); + variable_item_set_locked( + item, (app->notification->rgb_srv->settings->rgb_mod_installed == 0), "RGB MOD \nOFF!"); item = variable_item_list_add( app->variable_item_list_rgb, "Custom Blue", 255, color_set_custom_blue, app); @@ -512,6 +555,8 @@ static NotificationAppSettings* alloc_settings(void) { variable_item_set_current_value_text(item, valtext); variable_item_set_locked( item, (app->notification->rgb_srv->settings->rainbow_mode > 0), "Rainbow mode\nenabled!"); + variable_item_set_locked( + item, (app->notification->rgb_srv->settings->rgb_mod_installed == 0), "RGB MOD \nOFF!"); // Rainbow (based on Willy-JL idea) settings item = variable_item_list_add( @@ -526,6 +571,8 @@ static NotificationAppSettings* alloc_settings(void) { RGB_MOD_RAINBOW_MODE_COUNT); variable_item_set_current_value_index(item, value_index); variable_item_set_current_value_text(item, rgb_mod_rainbow_mode_text[value_index]); + variable_item_set_locked( + item, (app->notification->rgb_srv->settings->rgb_mod_installed == 0), "RGB MOD \nOFF!"); item = variable_item_list_add( app->variable_item_list_rgb, @@ -539,6 +586,8 @@ static NotificationAppSettings* alloc_settings(void) { RGB_MOD_RAINBOW_SPEED_COUNT); variable_item_set_current_value_index(item, value_index); variable_item_set_current_value_text(item, rgb_mod_rainbow_speed_text[value_index]); + variable_item_set_locked( + item, (app->notification->rgb_srv->settings->rgb_mod_installed == 0), "RGB MOD \nOFF!"); item = variable_item_list_add( app->variable_item_list_rgb, @@ -552,8 +601,10 @@ static NotificationAppSettings* alloc_settings(void) { RGB_MOD_RAINBOW_SPEED_COUNT); variable_item_set_current_value_index(item, value_index); variable_item_set_current_value_text(item, rgb_mod_rainbow_step_text[value_index]); + variable_item_set_locked( + item, (app->notification->rgb_srv->settings->rgb_mod_installed == 0), "RGB MOD \nOFF!"); - // --- End of RGB SETTING VIEW --- + //--- RGB BACKLIGHT END --- app->view_dispatcher = view_dispatcher_alloc(); view_dispatcher_attach_to_gui(app->view_dispatcher, app->gui, ViewDispatcherTypeFullscreen); diff --git a/targets/f7/api_symbols.csv b/targets/f7/api_symbols.csv old mode 100644 new mode 100755 index ecb5047e8..07bbe80f7 --- a/targets/f7/api_symbols.csv +++ b/targets/f7/api_symbols.csv @@ -1,5 +1,5 @@ entry,status,name,type,params -Version,+,86.0,, +Version,+,83.1,, Header,+,applications/drivers/subghz/cc1101_ext/cc1101_ext_interconnect.h,, Header,+,applications/services/bt/bt_service/bt.h,, Header,+,applications/services/bt/bt_service/bt_keys_storage.h,, @@ -3148,6 +3148,7 @@ Function,-,renameat,int,"int, const char*, int, const char*" Function,-,rewind,void,FILE* Function,+,rgb_backlight_get_color_count,uint8_t, Function,+,rgb_backlight_get_color_text,const char*,uint8_t +Function,+,rgb_backlight_set_custom_color,void,"uint8_t, uint8_t, uint8_t" Function,+,rgb_backlight_set_static_color,void,uint8_t Function,+,rgb_backlight_settings_load,void,RGBBacklightSettings* Function,+,rgb_backlight_settings_save,void,const RGBBacklightSettings* diff --git a/targets/f7/furi_hal/furi_hal_light.c b/targets/f7/furi_hal/furi_hal_light.c index 6cc60da11..2d6b4bbfe 100644 --- a/targets/f7/furi_hal/furi_hal_light.c +++ b/targets/f7/furi_hal/furi_hal_light.c @@ -32,23 +32,25 @@ void furi_hal_light_init(void) { } void furi_hal_light_set(Light light, uint8_t value) { - furi_hal_i2c_acquire(&furi_hal_i2c_handle_power); - if(light & LightRed) { - lp5562_set_channel_value(&furi_hal_i2c_handle_power, LP5562ChannelRed, value); - } - if(light & LightGreen) { - lp5562_set_channel_value(&furi_hal_i2c_handle_power, LP5562ChannelGreen, value); - } - if(light & LightBlue) { - lp5562_set_channel_value(&furi_hal_i2c_handle_power, LP5562ChannelBlue, value); - } - if(light & LightBacklight) { - uint8_t prev = lp5562_get_channel_value(&furi_hal_i2c_handle_power, LP5562ChannelWhite); - lp5562_execute_ramp( - &furi_hal_i2c_handle_power, LP5562Engine1, LP5562ChannelWhite, prev, value, 100); - rgb_backlight_update(value); - } - furi_hal_i2c_release(&furi_hal_i2c_handle_power); + furi_hal_i2c_acquire(&furi_hal_i2c_handle_power); + if(light & LightRed) { + lp5562_set_channel_value(&furi_hal_i2c_handle_power, LP5562ChannelRed, value); + } + if(light & LightGreen) { + lp5562_set_channel_value(&furi_hal_i2c_handle_power, LP5562ChannelGreen, value); + } + if(light & LightBlue) { + lp5562_set_channel_value(&furi_hal_i2c_handle_power, LP5562ChannelBlue, value); + } + if(light & LightBacklight) { + uint8_t prev = lp5562_get_channel_value(&furi_hal_i2c_handle_power, LP5562ChannelWhite); + lp5562_execute_ramp( + &furi_hal_i2c_handle_power, LP5562Engine1, LP5562ChannelWhite, prev, value, 100); + // --- RGB BACKLIGHT --- + rgb_backlight_update(value / 255.0f); + // --- RGB BACKLIGHT END --- + } + furi_hal_i2c_release(&furi_hal_i2c_handle_power); } void furi_hal_light_blink_start(Light light, uint8_t brightness, uint16_t on_time, uint16_t period) { From cf63e9c03623ba0ea400303d7aeb756d216cef24 Mon Sep 17 00:00:00 2001 From: Dmitry422 Date: Fri, 14 Mar 2025 18:40:19 +0700 Subject: [PATCH 119/268] Restore Input_vibro_touch compability with rgb_backlight. rgb_backlight driver litle bit improvements --- applications/services/input/input.c | 4 +++- lib/drivers/SK6805.c | 1 - 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/applications/services/input/input.c b/applications/services/input/input.c index 41759a1dd..93c5d1867 100644 --- a/applications/services/input/input.c +++ b/applications/services/input/input.c @@ -157,8 +157,10 @@ int32_t input_srv(void* p) { // Send Press/Release event event.type = pin_states[i].state ? InputTypePress : InputTypeRelease; furi_pubsub_publish(event_pubsub, &event); - // do vibro if user setup vibro touch level in Settings-Input. + // vibro signal if user setup vibro touch level in Settings-Input. if(settings->vibro_touch_level) { + //delay 1 ticks for compatibility with rgb_backlight_mod + furi_delay_tick(1); furi_hal_vibro_on(true); furi_delay_tick(settings->vibro_touch_level); furi_hal_vibro_on(false); diff --git a/lib/drivers/SK6805.c b/lib/drivers/SK6805.c index b6f525eb8..2ad8e18d3 100644 --- a/lib/drivers/SK6805.c +++ b/lib/drivers/SK6805.c @@ -98,6 +98,5 @@ void SK6805_update(void) { } } } - furi_delay_us(100); FURI_CRITICAL_EXIT(); } From b2185594f21359ba3fae2c86446a62262f596f9c Mon Sep 17 00:00:00 2001 From: doomwastaken Date: Fri, 14 Mar 2025 20:19:24 +0300 Subject: [PATCH 120/268] increased hid remote stack, increased swipe speed, added enterprise sleep --- applications/system/hid_app/application.fam | 4 ++-- .../system/hid_app/views/hid_tiktok.c | 24 +++++++++++-------- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/applications/system/hid_app/application.fam b/applications/system/hid_app/application.fam index cc218c31a..5e7e421e3 100644 --- a/applications/system/hid_app/application.fam +++ b/applications/system/hid_app/application.fam @@ -3,7 +3,7 @@ App( name="Remote", apptype=FlipperAppType.EXTERNAL, entry_point="hid_usb_app", - stack_size=1 * 1024, + stack_size=1 * 1024 + 512, sources=["*.c", "!transport_ble.c"], cdefines=["HID_TRANSPORT_USB"], fap_description="Use Flipper as a HID remote control over USB", @@ -20,7 +20,7 @@ App( name="Remote", apptype=FlipperAppType.EXTERNAL, entry_point="hid_ble_app", - stack_size=1 * 1024, + stack_size=1 * 1024 + 512, sources=["*.c", "!transport_usb.c"], cdefines=["HID_TRANSPORT_BLE"], fap_libs=["ble_profile"], diff --git a/applications/system/hid_app/views/hid_tiktok.c b/applications/system/hid_app/views/hid_tiktok.c index f1501027c..f0997f72e 100644 --- a/applications/system/hid_app/views/hid_tiktok.c +++ b/applications/system/hid_app/views/hid_tiktok.c @@ -103,7 +103,10 @@ static void hid_tiktok_reset_cursor(HidTikTok* hid_tiktok) { furi_delay_ms(50); } // Move cursor from the corner - hid_hal_mouse_move(hid_tiktok->hid, 20, 120); + // Actions split for some mobiles to properly process mouse movements + hid_hal_mouse_move(hid_tiktok->hid, 10, 60); + furi_delay_ms(3); + hid_hal_mouse_move(hid_tiktok->hid, 0, 60); furi_delay_ms(50); } @@ -162,29 +165,30 @@ static bool hid_tiktok_input_callback(InputEvent* event, void* context) { consumed = true; } else if(event->type == InputTypeShort) { if(event->key == InputKeyOk) { + // delays adjusted for emulation of a finger tap hid_hal_mouse_press(hid_tiktok->hid, HID_MOUSE_BTN_LEFT); - furi_delay_ms(50); + furi_delay_ms(25); hid_hal_mouse_release(hid_tiktok->hid, HID_MOUSE_BTN_LEFT); - furi_delay_ms(50); + furi_delay_ms(75); hid_hal_mouse_press(hid_tiktok->hid, HID_MOUSE_BTN_LEFT); - furi_delay_ms(50); + furi_delay_ms(25); hid_hal_mouse_release(hid_tiktok->hid, HID_MOUSE_BTN_LEFT); consumed = true; } else if(event->key == InputKeyUp) { // Emulate up swipe - hid_hal_mouse_scroll(hid_tiktok->hid, -6); hid_hal_mouse_scroll(hid_tiktok->hid, -12); - hid_hal_mouse_scroll(hid_tiktok->hid, -19); + hid_hal_mouse_scroll(hid_tiktok->hid, -24); + hid_hal_mouse_scroll(hid_tiktok->hid, -38); + hid_hal_mouse_scroll(hid_tiktok->hid, -24); hid_hal_mouse_scroll(hid_tiktok->hid, -12); - hid_hal_mouse_scroll(hid_tiktok->hid, -6); consumed = true; } else if(event->key == InputKeyDown) { // Emulate down swipe - hid_hal_mouse_scroll(hid_tiktok->hid, 6); hid_hal_mouse_scroll(hid_tiktok->hid, 12); - hid_hal_mouse_scroll(hid_tiktok->hid, 19); + hid_hal_mouse_scroll(hid_tiktok->hid, 24); + hid_hal_mouse_scroll(hid_tiktok->hid, 38); + hid_hal_mouse_scroll(hid_tiktok->hid, 24); hid_hal_mouse_scroll(hid_tiktok->hid, 12); - hid_hal_mouse_scroll(hid_tiktok->hid, 6); consumed = true; } else if(event->key == InputKeyBack) { hid_hal_consumer_key_release_all(hid_tiktok->hid); From 61a54a1b06fb22ae16b72785c4279064e6cdc980 Mon Sep 17 00:00:00 2001 From: doomwastaken Date: Fri, 14 Mar 2025 20:25:38 +0300 Subject: [PATCH 121/268] decreased extra stack by 256b --- applications/system/hid_app/application.fam | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/applications/system/hid_app/application.fam b/applications/system/hid_app/application.fam index 5e7e421e3..3a5e0951f 100644 --- a/applications/system/hid_app/application.fam +++ b/applications/system/hid_app/application.fam @@ -3,7 +3,7 @@ App( name="Remote", apptype=FlipperAppType.EXTERNAL, entry_point="hid_usb_app", - stack_size=1 * 1024 + 512, + stack_size=1 * 1024 + 256, sources=["*.c", "!transport_ble.c"], cdefines=["HID_TRANSPORT_USB"], fap_description="Use Flipper as a HID remote control over USB", @@ -20,7 +20,7 @@ App( name="Remote", apptype=FlipperAppType.EXTERNAL, entry_point="hid_ble_app", - stack_size=1 * 1024 + 512, + stack_size=1 * 1024 + 256, sources=["*.c", "!transport_usb.c"], cdefines=["HID_TRANSPORT_BLE"], fap_libs=["ble_profile"], From 8ca3581fb049559d7c7c42bf9cd151929f40b292 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Sat, 15 Mar 2025 07:23:23 +0300 Subject: [PATCH 122/268] subghz bugfixes and experimental options --- .../scenes/subghz_scene_radio_settings.c | 16 +- .../subghz/scenes/subghz_scene_set_type.c | 858 ++++++++++-------- .../subghz/scenes/subghz_scene_transmitter.c | 2 +- lib/subghz/protocols/alutech_at_4n.c | 2 +- lib/subghz/protocols/came_atomo.c | 2 +- lib/subghz/protocols/faac_slh.c | 68 +- lib/subghz/protocols/hay21.c | 3 + lib/subghz/protocols/keeloq.c | 4 +- lib/subghz/protocols/kinggates_stylo_4k.c | 2 +- lib/subghz/protocols/nice_flor_s.c | 2 +- lib/subghz/protocols/somfy_keytis.c | 2 +- lib/subghz/protocols/somfy_telis.c | 2 +- lib/subghz/protocols/star_line.c | 2 +- targets/f7/api_symbols.csv | 4 +- targets/f7/furi_hal/furi_hal_subghz.c | 6 +- targets/f7/furi_hal/furi_hal_subghz.h | 8 +- 16 files changed, 554 insertions(+), 429 deletions(-) diff --git a/applications/main/subghz/scenes/subghz_scene_radio_settings.c b/applications/main/subghz/scenes/subghz_scene_radio_settings.c index 974a2f564..8e808618b 100644 --- a/applications/main/subghz/scenes/subghz_scene_radio_settings.c +++ b/applications/main/subghz/scenes/subghz_scene_radio_settings.c @@ -26,7 +26,7 @@ const char* const debug_pin_text[DEBUG_P_COUNT] = { "17(1W)", }; -#define DEBUG_COUNTER_COUNT 13 +#define DEBUG_COUNTER_COUNT 16 const char* const debug_counter_text[DEBUG_COUNTER_COUNT] = { "+1", "+2", @@ -34,21 +34,26 @@ const char* const debug_counter_text[DEBUG_COUNTER_COUNT] = { "+4", "+5", "+10", - "0", + "+50", + "OVFL", + "No", "-1", "-2", "-3", "-4", "-5", "-10", + "-50", }; -const uint32_t debug_counter_val[DEBUG_COUNTER_COUNT] = { +const int32_t debug_counter_val[DEBUG_COUNTER_COUNT] = { 1, 2, 3, 4, 5, 10, + 50, + 65535, 0, -1, -2, @@ -56,6 +61,7 @@ const uint32_t debug_counter_val[DEBUG_COUNTER_COUNT] = { -4, -5, -10, + -50, }; static void subghz_scene_radio_settings_set_device(VariableItem* item) { @@ -118,7 +124,7 @@ void subghz_scene_radio_settings_on_enter(void* context) { SubGhz* subghz = context; VariableItemList* variable_item_list = subghz->variable_item_list; - uint8_t value_index; + int32_t value_index; VariableItem* item; uint8_t value_count_device = RADIO_DEVICE_COUNT; @@ -152,7 +158,7 @@ void subghz_scene_radio_settings_on_enter(void* context) { furi_hal_rtc_is_flag_set(FuriHalRtcFlagDebug) ? DEBUG_COUNTER_COUNT : 3, subghz_scene_receiver_config_set_debug_counter, subghz); - value_index = value_index_uint32( + value_index = value_index_int32( furi_hal_subghz_get_rolling_counter_mult(), debug_counter_val, furi_hal_rtc_is_flag_set(FuriHalRtcFlagDebug) ? DEBUG_COUNTER_COUNT : 3); diff --git a/applications/main/subghz/scenes/subghz_scene_set_type.c b/applications/main/subghz/scenes/subghz_scene_set_type.c index 20cef68df..3155f9f1e 100644 --- a/applications/main/subghz/scenes/subghz_scene_set_type.c +++ b/applications/main/subghz/scenes/subghz_scene_set_type.c @@ -193,104 +193,114 @@ bool subghz_scene_set_type_on_event(void* context, SceneManagerEvent event) { GenInfo gen_info = {0}; switch(event.event) { case SetTypePricenton433: - gen_info = (GenInfo){.type = GenData, - .mod = "AM650", - .freq = 433920000, - .data.name = SUBGHZ_PROTOCOL_PRINCETON_NAME, - .data.key = (key & 0x00FFFFF0) | 0x4, // btn 0x1, 0x2, 0x4, 0x8 - .data.bits = 24, - .data.te = 400}; + gen_info = (GenInfo){ + .type = GenData, + .mod = "AM650", + .freq = 433920000, + .data.name = SUBGHZ_PROTOCOL_PRINCETON_NAME, + .data.key = (key & 0x00FFFFF0) | 0x4, // btn 0x1, 0x2, 0x4, 0x8 + .data.bits = 24, + .data.te = 400}; break; case SetTypePricenton315: - gen_info = (GenInfo){.type = GenData, - .mod = "AM650", - .freq = 315000000, - .data.name = SUBGHZ_PROTOCOL_PRINCETON_NAME, - .data.key = (key & 0x00FFFFF0) | 0x4, // btn 0x1, 0x2, 0x4, 0x8 - .data.bits = 24, - .data.te = 400}; + gen_info = (GenInfo){ + .type = GenData, + .mod = "AM650", + .freq = 315000000, + .data.name = SUBGHZ_PROTOCOL_PRINCETON_NAME, + .data.key = (key & 0x00FFFFF0) | 0x4, // btn 0x1, 0x2, 0x4, 0x8 + .data.bits = 24, + .data.te = 400}; break; case SetTypeNiceFlo12bit: - gen_info = (GenInfo){.type = GenData, - .mod = "AM650", - .freq = 433920000, - .data.name = SUBGHZ_PROTOCOL_NICE_FLO_NAME, - .data.key = (key & 0x00000FF0) | 0x1, // btn 0x1, 0x2, 0x4 - .data.bits = 12, - .data.te = 0}; + gen_info = (GenInfo){ + .type = GenData, + .mod = "AM650", + .freq = 433920000, + .data.name = SUBGHZ_PROTOCOL_NICE_FLO_NAME, + .data.key = (key & 0x00000FF0) | 0x1, // btn 0x1, 0x2, 0x4 + .data.bits = 12, + .data.te = 0}; break; case SetTypeNiceFlo24bit: - gen_info = (GenInfo){.type = GenData, - .mod = "AM650", - .freq = 433920000, - .data.name = SUBGHZ_PROTOCOL_NICE_FLO_NAME, - .data.key = (key & 0x00FFFFF0) | 0x4, // btn 0x1, 0x2, 0x4, 0x8 - .data.bits = 24, - .data.te = 0}; + gen_info = (GenInfo){ + .type = GenData, + .mod = "AM650", + .freq = 433920000, + .data.name = SUBGHZ_PROTOCOL_NICE_FLO_NAME, + .data.key = (key & 0x00FFFFF0) | 0x4, // btn 0x1, 0x2, 0x4, 0x8 + .data.bits = 24, + .data.te = 0}; break; case SetTypeCAME12bit: - gen_info = (GenInfo){.type = GenData, - .mod = "AM650", - .freq = 433920000, - .data.name = SUBGHZ_PROTOCOL_CAME_NAME, - .data.key = (key & 0x00000FF0) | 0x1, // btn 0x1, 0x2, 0x4 - .data.bits = 12, - .data.te = 0}; + gen_info = (GenInfo){ + .type = GenData, + .mod = "AM650", + .freq = 433920000, + .data.name = SUBGHZ_PROTOCOL_CAME_NAME, + .data.key = (key & 0x00000FF0) | 0x1, // btn 0x1, 0x2, 0x4 + .data.bits = 12, + .data.te = 0}; break; case SetTypeCAME24bit: - gen_info = (GenInfo){.type = GenData, - .mod = "AM650", - .freq = 433920000, - .data.name = SUBGHZ_PROTOCOL_CAME_NAME, - .data.key = (key & 0x00FFFFF0) | 0x4, // btn 0x1, 0x2, 0x4, 0x8 - .data.bits = 24, - .data.te = 0}; + gen_info = (GenInfo){ + .type = GenData, + .mod = "AM650", + .freq = 433920000, + .data.name = SUBGHZ_PROTOCOL_CAME_NAME, + .data.key = (key & 0x00FFFFF0) | 0x4, // btn 0x1, 0x2, 0x4, 0x8 + .data.bits = 24, + .data.te = 0}; break; case SetTypeCAME12bit868: - gen_info = (GenInfo){.type = GenData, - .mod = "AM650", - .freq = 868350000, - .data.name = SUBGHZ_PROTOCOL_CAME_NAME, - .data.key = (key & 0x00000FF0) | 0x1, // btn 0x1, 0x2, 0x4 - .data.bits = 12, - .data.te = 0}; + gen_info = (GenInfo){ + .type = GenData, + .mod = "AM650", + .freq = 868350000, + .data.name = SUBGHZ_PROTOCOL_CAME_NAME, + .data.key = (key & 0x00000FF0) | 0x1, // btn 0x1, 0x2, 0x4 + .data.bits = 12, + .data.te = 0}; break; case SetTypeCAME24bit868: - gen_info = (GenInfo){.type = GenData, - .mod = "AM650", - .freq = 868350000, - .data.name = SUBGHZ_PROTOCOL_CAME_NAME, - .data.key = (key & 0x00FFFFF0) | 0x4, // btn 0x1, 0x2, 0x4, 0x8 - .data.bits = 24, - .data.te = 0}; + gen_info = (GenInfo){ + .type = GenData, + .mod = "AM650", + .freq = 868350000, + .data.name = SUBGHZ_PROTOCOL_CAME_NAME, + .data.key = (key & 0x00FFFFF0) | 0x4, // btn 0x1, 0x2, 0x4, 0x8 + .data.bits = 24, + .data.te = 0}; break; case SetTypeLinear_300_00: - gen_info = (GenInfo){.type = GenData, - .mod = "AM650", - .freq = 300000000, - .data.name = SUBGHZ_PROTOCOL_LINEAR_NAME, - .data.key = (key & 0x3FF), - .data.bits = 10, - .data.te = 0}; + gen_info = (GenInfo){ + .type = GenData, + .mod = "AM650", + .freq = 300000000, + .data.name = SUBGHZ_PROTOCOL_LINEAR_NAME, + .data.key = (key & 0x3FF), + .data.bits = 10, + .data.te = 0}; break; case SetTypeBETT_433: - gen_info = (GenInfo){.type = GenData, - .mod = "AM650", - .freq = 433920000, - .data.name = SUBGHZ_PROTOCOL_BETT_NAME, - .data.key = (key & 0x0000FFF0), - .data.bits = 18, - .data.te = 0}; + gen_info = (GenInfo){ + .type = GenData, + .mod = "AM650", + .freq = 433920000, + .data.name = SUBGHZ_PROTOCOL_BETT_NAME, + .data.key = (key & 0x0000FFF0), + .data.bits = 18, + .data.te = 0}; break; case SetTypeCAMETwee: - gen_info = (GenInfo){.type = GenData, - .mod = "AM650", - .freq = 433920000, - .data.name = SUBGHZ_PROTOCOL_CAME_TWEE_NAME, - .data.key = 0x003FFF7200000000 | - ((key & 0x0FFFFFF0) ^ 0xE0E0E0EE), // ???? - .data.bits = 54, - .data.te = 0}; + gen_info = (GenInfo){ + .type = GenData, + .mod = "AM650", + .freq = 433920000, + .data.name = SUBGHZ_PROTOCOL_CAME_TWEE_NAME, + .data.key = 0x003FFF7200000000 | ((key & 0x0FFFFFF0) ^ 0xE0E0E0EE), // ???? + .data.bits = 54, + .data.te = 0}; break; case SetTypeGateTX: gen_info = (GenInfo){ @@ -329,14 +339,14 @@ bool subghz_scene_set_type_on_event(void* context, SceneManagerEvent event) { .data.te = 0}; break; case SetTypeReversRB2_433: - gen_info = (GenInfo){.type = GenData, - .mod = "AM650", - .freq = 433920000, - .data.name = SUBGHZ_PROTOCOL_REVERSRB2_NAME, // 64bits no buttons - .data.key = (key & 0x00000FFFFFFFF000) | 0xFFFFF00000000000 | - 0x0000000000000A00, - .data.bits = 64, - .data.te = 0}; + gen_info = (GenInfo){ + .type = GenData, + .mod = "AM650", + .freq = 433920000, + .data.name = SUBGHZ_PROTOCOL_REVERSRB2_NAME, // 64bits no buttons + .data.key = (key & 0x00000FFFFFFFF000) | 0xFFFFF00000000000 | 0x0000000000000A00, + .data.bits = 64, + .data.te = 0}; break; case SetTypeMarantec24_868: gen_info = (GenInfo){ @@ -349,379 +359,421 @@ bool subghz_scene_set_type_on_event(void* context, SceneManagerEvent event) { .data.te = 0}; break; case SetTypeFaacSLH_433: - gen_info = (GenInfo){.type = GenFaacSLH, - .mod = "AM650", - .freq = 433920000, - .faac_slh.serial = ((key & 0x00FFFFF0) | 0xA0000006) >> 4, - .faac_slh.btn = 0x06, - .faac_slh.cnt = 0x02, - .faac_slh.seed = key, - .faac_slh.manuf = "FAAC_SLH"}; + gen_info = (GenInfo){ + .type = GenFaacSLH, + .mod = "AM650", + .freq = 433920000, + .faac_slh.serial = ((key & 0x00FFFFF0) | 0xA0000006) >> 4, + .faac_slh.btn = 0x06, + .faac_slh.cnt = 0x02, + .faac_slh.seed = key, + .faac_slh.manuf = "FAAC_SLH"}; break; case SetTypeFaacSLH_868: - gen_info = (GenInfo){.type = GenFaacSLH, - .mod = "AM650", - .freq = 868350000, - .faac_slh.serial = ((key & 0x00FFFFF0) | 0xA0000006) >> 4, - .faac_slh.btn = 0x06, - .faac_slh.cnt = 0x02, - .faac_slh.seed = (key & 0x0FFFFFFF), - .faac_slh.manuf = "FAAC_SLH"}; + gen_info = (GenInfo){ + .type = GenFaacSLH, + .mod = "AM650", + .freq = 868350000, + .faac_slh.serial = ((key & 0x00FFFFF0) | 0xA0000006) >> 4, + .faac_slh.btn = 0x06, + .faac_slh.cnt = 0x02, + .faac_slh.seed = (key & 0x0FFFFFFF), + .faac_slh.manuf = "FAAC_SLH"}; break; case SetTypeBeninca433: - gen_info = (GenInfo){.type = GenKeeloq, - .mod = "AM650", - .freq = 433920000, - .keeloq.serial = (key & 0x000FFF00) | 0x00800080, - .keeloq.btn = 0x01, - .keeloq.cnt = 0x05, - .keeloq.manuf = "Beninca"}; + gen_info = (GenInfo){ + .type = GenKeeloq, + .mod = "AM650", + .freq = 433920000, + .keeloq.serial = (key & 0x000FFF00) | 0x00800080, + .keeloq.btn = 0x01, + .keeloq.cnt = 0x05, + .keeloq.manuf = "Beninca"}; break; case SetTypeBeninca868: - gen_info = (GenInfo){.type = GenKeeloq, - .mod = "AM650", - .freq = 868350000, - .keeloq.serial = (key & 0x000FFF00) | 0x00800080, - .keeloq.btn = 0x01, - .keeloq.cnt = 0x05, - .keeloq.manuf = "Beninca"}; + gen_info = (GenInfo){ + .type = GenKeeloq, + .mod = "AM650", + .freq = 868350000, + .keeloq.serial = (key & 0x000FFF00) | 0x00800080, + .keeloq.btn = 0x01, + .keeloq.cnt = 0x05, + .keeloq.manuf = "Beninca"}; break; case SetTypeAllmatic433: - gen_info = (GenInfo){.type = GenKeeloq, - .mod = "AM650", - .freq = 433920000, - .keeloq.serial = (key & 0x00FFFF00) | 0x01000011, - .keeloq.btn = 0x0C, - .keeloq.cnt = 0x05, - .keeloq.manuf = "Beninca"}; + gen_info = (GenInfo){ + .type = GenKeeloq, + .mod = "AM650", + .freq = 433920000, + .keeloq.serial = (key & 0x00FFFF00) | 0x01000011, + .keeloq.btn = 0x0C, + .keeloq.cnt = 0x05, + .keeloq.manuf = "Beninca"}; break; case SetTypeAllmatic868: - gen_info = (GenInfo){.type = GenKeeloq, - .mod = "AM650", - .freq = 868350000, - .keeloq.serial = (key & 0x00FFFF00) | 0x01000011, - .keeloq.btn = 0x0C, - .keeloq.cnt = 0x05, - .keeloq.manuf = "Beninca"}; + gen_info = (GenInfo){ + .type = GenKeeloq, + .mod = "AM650", + .freq = 868350000, + .keeloq.serial = (key & 0x00FFFF00) | 0x01000011, + .keeloq.btn = 0x0C, + .keeloq.cnt = 0x05, + .keeloq.manuf = "Beninca"}; break; case SetTypeCenturion433: - gen_info = (GenInfo){.type = GenKeeloq, - .mod = "AM650", - .freq = 433920000, - .keeloq.serial = (key & 0x0000FFFF), - .keeloq.btn = 0x02, - .keeloq.cnt = 0x03, - .keeloq.manuf = "Centurion"}; + gen_info = (GenInfo){ + .type = GenKeeloq, + .mod = "AM650", + .freq = 433920000, + .keeloq.serial = (key & 0x0000FFFF), + .keeloq.btn = 0x02, + .keeloq.cnt = 0x03, + .keeloq.manuf = "Centurion"}; break; case SetTypeMonarch433: - gen_info = (GenInfo){.type = GenKeeloq, - .mod = "AM650", - .freq = 433920000, - .keeloq.serial = (key & 0x0000FFFF), - .keeloq.btn = 0x0A, - .keeloq.cnt = 0x03, - .keeloq.manuf = "Monarch"}; + gen_info = (GenInfo){ + .type = GenKeeloq, + .mod = "AM650", + .freq = 433920000, + .keeloq.serial = (key & 0x0000FFFF), + .keeloq.btn = 0x0A, + .keeloq.cnt = 0x03, + .keeloq.manuf = "Monarch"}; break; case SetTypeJollyMotors433: - gen_info = (GenInfo){.type = GenKeeloq, - .mod = "AM650", - .freq = 433920000, - .keeloq.serial = (key & 0x000FFFFF), - .keeloq.btn = 0x02, - .keeloq.cnt = 0x03, - .keeloq.manuf = "Jolly_Motors"}; + gen_info = (GenInfo){ + .type = GenKeeloq, + .mod = "AM650", + .freq = 433920000, + .keeloq.serial = (key & 0x000FFFFF), + .keeloq.btn = 0x02, + .keeloq.cnt = 0x03, + .keeloq.manuf = "Jolly_Motors"}; break; case SetTypeElmesElectronic: - gen_info = (GenInfo){.type = GenKeeloq, - .mod = "AM650", - .freq = 433920000, - .keeloq.serial = (key & 0x00FFFFFF) | 0x02000000, - .keeloq.btn = 0x02, - .keeloq.cnt = 0x03, - .keeloq.manuf = "Elmes_Poland"}; + gen_info = (GenInfo){ + .type = GenKeeloq, + .mod = "AM650", + .freq = 433920000, + .keeloq.serial = (key & 0x00FFFFFF) | 0x02000000, + .keeloq.btn = 0x02, + .keeloq.cnt = 0x03, + .keeloq.manuf = "Elmes_Poland"}; break; case SetTypeANMotorsAT4: - gen_info = (GenInfo){.type = GenKeeloq, - .mod = "AM650", - .freq = 433920000, - .keeloq.serial = (key & 0x000FFFFF) | 0x04700000, - .keeloq.btn = 0x02, - .keeloq.cnt = 0x21, - .keeloq.manuf = "AN-Motors"}; + gen_info = (GenInfo){ + .type = GenKeeloq, + .mod = "AM650", + .freq = 433920000, + .keeloq.serial = (key & 0x000FFFFF) | 0x04700000, + .keeloq.btn = 0x02, + .keeloq.cnt = 0x21, + .keeloq.manuf = "AN-Motors"}; break; case SetTypeAprimatic: - gen_info = (GenInfo){.type = GenKeeloq, - .mod = "AM650", - .freq = 433920000, - .keeloq.serial = (key & 0x000FFFFF) | 0x00600000, - .keeloq.btn = 0x08, - .keeloq.cnt = 0x03, - .keeloq.manuf = "Aprimatic"}; + gen_info = (GenInfo){ + .type = GenKeeloq, + .mod = "AM650", + .freq = 433920000, + .keeloq.serial = (key & 0x000FFFFF) | 0x00600000, + .keeloq.btn = 0x08, + .keeloq.cnt = 0x03, + .keeloq.manuf = "Aprimatic"}; break; case SetTypeGibidi433: - gen_info = (GenInfo){.type = GenKeeloq, - .mod = "AM650", - .freq = 433920000, - .keeloq.serial = key & 0x00FFFFFF, - .keeloq.btn = 0x02, - .keeloq.cnt = 0x03, - .keeloq.manuf = "Gibidi"}; + gen_info = (GenInfo){ + .type = GenKeeloq, + .mod = "AM650", + .freq = 433920000, + .keeloq.serial = key & 0x00FFFFFF, + .keeloq.btn = 0x02, + .keeloq.cnt = 0x03, + .keeloq.manuf = "Gibidi"}; break; case SetTypeGSN: - gen_info = (GenInfo){.type = GenKeeloq, - .mod = "AM650", - .freq = 433920000, - .keeloq.serial = key & 0x0FFFFFFF, - .keeloq.btn = 0x02, - .keeloq.cnt = 0x03, - .keeloq.manuf = "GSN"}; + gen_info = (GenInfo){ + .type = GenKeeloq, + .mod = "AM650", + .freq = 433920000, + .keeloq.serial = key & 0x0FFFFFFF, + .keeloq.btn = 0x02, + .keeloq.cnt = 0x03, + .keeloq.manuf = "GSN"}; break; case SetTypeIronLogic: - gen_info = (GenInfo){.type = GenKeeloq, - .mod = "AM650", - .freq = 433920000, - .keeloq.serial = key & 0x00FFFFF0, - .keeloq.btn = 0x04, - .keeloq.cnt = 0x05, - .keeloq.manuf = "IronLogic"}; + gen_info = (GenInfo){ + .type = GenKeeloq, + .mod = "AM650", + .freq = 433920000, + .keeloq.serial = key & 0x00FFFFF0, + .keeloq.btn = 0x04, + .keeloq.cnt = 0x05, + .keeloq.manuf = "IronLogic"}; break; case SetTypeStilmatic: - gen_info = (GenInfo){.type = GenKeeloq, - .mod = "AM650", - .freq = 433920000, - .keeloq.serial = key & 0x0FFFFFFF, - .keeloq.btn = 0x01, - .keeloq.cnt = 0x03, - .keeloq.manuf = "Stilmatic"}; + gen_info = (GenInfo){ + .type = GenKeeloq, + .mod = "AM650", + .freq = 433920000, + .keeloq.serial = key & 0x0FFFFFFF, + .keeloq.btn = 0x01, + .keeloq.cnt = 0x03, + .keeloq.manuf = "Stilmatic"}; break; case SetTypeSommer_FM_434: - gen_info = (GenInfo){.type = GenKeeloq, - .mod = "FM476", - .freq = 434420000, - .keeloq.serial = (key & 0x0000FFFF) | 0x01700000, - .keeloq.btn = 0x02, - .keeloq.cnt = 0x03, - .keeloq.manuf = "Sommer(fsk476)"}; + gen_info = (GenInfo){ + .type = GenKeeloq, + .mod = "FM476", + .freq = 434420000, + .keeloq.serial = (key & 0x0000FFFF) | 0x01700000, + .keeloq.btn = 0x02, + .keeloq.cnt = 0x03, + .keeloq.manuf = "Sommer(fsk476)"}; break; case SetTypeSommer_FM_868: - gen_info = (GenInfo){.type = GenKeeloq, - .mod = "FM476", - .freq = 868800000, - .keeloq.serial = (key & 0x0000FFFF) | 0x01700000, - .keeloq.btn = 0x02, - .keeloq.cnt = 0x03, - .keeloq.manuf = "Sommer(fsk476)"}; + gen_info = (GenInfo){ + .type = GenKeeloq, + .mod = "FM476", + .freq = 868800000, + .keeloq.serial = (key & 0x0000FFFF) | 0x01700000, + .keeloq.btn = 0x02, + .keeloq.cnt = 0x03, + .keeloq.manuf = "Sommer(fsk476)"}; break; case SetTypeSommer_FM238_434: - gen_info = (GenInfo){.type = GenKeeloq, - .mod = "FM238", - .freq = 434420000, - .keeloq.serial = key & 0x0000FFFF, - .keeloq.btn = 0x02, - .keeloq.cnt = 0x03, - .keeloq.manuf = "Sommer(fsk476)"}; + gen_info = (GenInfo){ + .type = GenKeeloq, + .mod = "FM238", + .freq = 434420000, + .keeloq.serial = key & 0x0000FFFF, + .keeloq.btn = 0x02, + .keeloq.cnt = 0x03, + .keeloq.manuf = "Sommer(fsk476)"}; break; case SetTypeSommer_FM238_868: - gen_info = (GenInfo){.type = GenKeeloq, - .mod = "FM238", - .freq = 868800000, - .keeloq.serial = key & 0x0000FFFF, - .keeloq.btn = 0x02, - .keeloq.cnt = 0x03, - .keeloq.manuf = "Sommer(fsk476)"}; + gen_info = (GenInfo){ + .type = GenKeeloq, + .mod = "FM238", + .freq = 868800000, + .keeloq.serial = key & 0x0000FFFF, + .keeloq.btn = 0x02, + .keeloq.cnt = 0x03, + .keeloq.manuf = "Sommer(fsk476)"}; break; case SetTypeDTMNeo433: - gen_info = (GenInfo){.type = GenKeeloq, - .mod = "AM650", - .freq = 433920000, - .keeloq.serial = key & 0x000FFFFF, - .keeloq.btn = 0x02, - .keeloq.cnt = 0x05, - .keeloq.manuf = "DTM_Neo"}; + gen_info = (GenInfo){ + .type = GenKeeloq, + .mod = "AM650", + .freq = 433920000, + .keeloq.serial = key & 0x000FFFFF, + .keeloq.btn = 0x02, + .keeloq.cnt = 0x05, + .keeloq.manuf = "DTM_Neo"}; break; case SetTypeCAMESpace: - gen_info = (GenInfo){.type = GenKeeloq, - .mod = "AM650", - .freq = 433920000, - .keeloq.serial = key & 0x00FFFFFF, - .keeloq.btn = 0x04, - .keeloq.cnt = 0x03, - .keeloq.manuf = "Came_Space"}; + gen_info = (GenInfo){ + .type = GenKeeloq, + .mod = "AM650", + .freq = 433920000, + .keeloq.serial = key & 0x00FFFFFF, + .keeloq.btn = 0x04, + .keeloq.cnt = 0x03, + .keeloq.manuf = "Came_Space"}; break; case SetTypeCameAtomo433: - gen_info = (GenInfo){.type = GenCameAtomo, - .mod = "AM650", - .freq = 433920000, - .keeloq.serial = (key & 0x0FFFFFFF) | 0x10000000, - .keeloq.cnt = 0x03}; + gen_info = (GenInfo){ + .type = GenCameAtomo, + .mod = "AM650", + .freq = 433920000, + .keeloq.serial = (key & 0x0FFFFFFF) | 0x10000000, + .keeloq.cnt = 0x03}; break; case SetTypeCameAtomo868: - gen_info = (GenInfo){.type = GenCameAtomo, - .mod = "AM650", - .freq = 868350000, - .keeloq.serial = (key & 0x0FFFFFFF) | 0x10000000, - .keeloq.cnt = 0x03}; + gen_info = (GenInfo){ + .type = GenCameAtomo, + .mod = "AM650", + .freq = 868350000, + .keeloq.serial = (key & 0x0FFFFFFF) | 0x10000000, + .keeloq.cnt = 0x03}; break; case SetTypeBFTMitto: - gen_info = (GenInfo){.type = GenKeeloqBFT, - .mod = "AM650", - .freq = 433920000, - .keeloq_bft.serial = key & 0x000FFFFF, - .keeloq_bft.btn = 0x02, - .keeloq_bft.cnt = 0x02, - .keeloq_bft.seed = key & 0x000FFFFF, - .keeloq_bft.manuf = "BFT"}; + gen_info = (GenInfo){ + .type = GenKeeloqBFT, + .mod = "AM650", + .freq = 433920000, + .keeloq_bft.serial = key & 0x000FFFFF, + .keeloq_bft.btn = 0x02, + .keeloq_bft.cnt = 0x02, + .keeloq_bft.seed = key & 0x000FFFFF, + .keeloq_bft.manuf = "BFT"}; break; case SetTypeAlutechAT4N: - gen_info = (GenInfo){.type = GenAlutechAt4n, - .mod = "AM650", - .freq = 433920000, - .alutech_at_4n.serial = (key & 0x000FFFFF) | 0x00100000, - .alutech_at_4n.btn = 0x44, - .alutech_at_4n.cnt = 0x03}; + gen_info = (GenInfo){ + .type = GenAlutechAt4n, + .mod = "AM650", + .freq = 433920000, + .alutech_at_4n.serial = (key & 0x000FFFFF) | 0x00100000, + .alutech_at_4n.btn = 0x44, + .alutech_at_4n.cnt = 0x03}; break; case SetTypeSomfyTelis: - gen_info = (GenInfo){.type = GenSomfyTelis, - .mod = "AM650", - .freq = 433420000, - .somfy_telis.serial = key & 0x00FFFFFF, - .somfy_telis.btn = 0x02, - .somfy_telis.cnt = 0x03}; + gen_info = (GenInfo){ + .type = GenSomfyTelis, + .mod = "AM650", + .freq = 433420000, + .somfy_telis.serial = key & 0x00FFFFFF, + .somfy_telis.btn = 0x02, + .somfy_telis.cnt = 0x03}; break; case SetTypeDoorHan_433_92: - gen_info = (GenInfo){.type = GenKeeloq, - .mod = "AM650", - .freq = 433920000, - .keeloq.serial = key & 0x0FFFFFFF, - .keeloq.btn = 0x02, - .keeloq.cnt = 0x03, - .keeloq.manuf = "DoorHan"}; + gen_info = (GenInfo){ + .type = GenKeeloq, + .mod = "AM650", + .freq = 433920000, + .keeloq.serial = key & 0x0FFFFFFF, + .keeloq.btn = 0x02, + .keeloq.cnt = 0x03, + .keeloq.manuf = "DoorHan"}; break; case SetTypeDoorHan_315_00: - gen_info = (GenInfo){.type = GenKeeloq, - .mod = "AM650", - .freq = 315000000, - .keeloq.serial = key & 0x0FFFFFFF, - .keeloq.btn = 0x02, - .keeloq.cnt = 0x03, - .keeloq.manuf = "DoorHan"}; + gen_info = (GenInfo){ + .type = GenKeeloq, + .mod = "AM650", + .freq = 315000000, + .keeloq.serial = key & 0x0FFFFFFF, + .keeloq.btn = 0x02, + .keeloq.cnt = 0x03, + .keeloq.manuf = "DoorHan"}; break; case SetTypeNiceFlorS_433_92: - gen_info = (GenInfo){.type = GenNiceFlorS, - .mod = "AM650", - .freq = 433920000, - .nice_flor_s.serial = key & 0x0FFFFFFF, - .nice_flor_s.btn = 0x01, - .nice_flor_s.cnt = 0x03, - .nice_flor_s.nice_one = false}; + gen_info = (GenInfo){ + .type = GenNiceFlorS, + .mod = "AM650", + .freq = 433920000, + .nice_flor_s.serial = key & 0x0FFFFFFF, + .nice_flor_s.btn = 0x01, + .nice_flor_s.cnt = 0x03, + .nice_flor_s.nice_one = false}; break; case SetTypeNiceOne_433_92: - gen_info = (GenInfo){.type = GenNiceFlorS, - .mod = "AM650", - .freq = 433920000, - .nice_flor_s.serial = key & 0x0FFFFFFF, - .nice_flor_s.btn = 0x01, - .nice_flor_s.cnt = 0x03, - .nice_flor_s.nice_one = true}; + gen_info = (GenInfo){ + .type = GenNiceFlorS, + .mod = "AM650", + .freq = 433920000, + .nice_flor_s.serial = key & 0x0FFFFFFF, + .nice_flor_s.btn = 0x01, + .nice_flor_s.cnt = 0x03, + .nice_flor_s.nice_one = true}; break; case SetTypeNiceSmilo_433_92: - gen_info = (GenInfo){.type = GenKeeloq, - .mod = "AM650", - .freq = 433920000, - .keeloq.serial = key & 0x00FFFFFF, - .keeloq.btn = 0x02, - .keeloq.cnt = 0x03, - .keeloq.manuf = "NICE_Smilo"}; + gen_info = (GenInfo){ + .type = GenKeeloq, + .mod = "AM650", + .freq = 433920000, + .keeloq.serial = key & 0x00FFFFFF, + .keeloq.btn = 0x02, + .keeloq.cnt = 0x03, + .keeloq.manuf = "NICE_Smilo"}; break; case SetTypeNiceMHouse_433_92: - gen_info = (GenInfo){.type = GenKeeloq, - .mod = "AM650", - .freq = 433920000, - .keeloq.serial = key & 0x00FFFFFF, - .keeloq.btn = 0x09, - .keeloq.cnt = 0x03, - .keeloq.manuf = "NICE_MHOUSE"}; + gen_info = (GenInfo){ + .type = GenKeeloq, + .mod = "AM650", + .freq = 433920000, + .keeloq.serial = key & 0x00FFFFFF, + .keeloq.btn = 0x09, + .keeloq.cnt = 0x03, + .keeloq.manuf = "NICE_MHOUSE"}; break; case SetTypeDeaMio433: - gen_info = (GenInfo){.type = GenKeeloq, - .mod = "AM650", - .freq = 433920000, - .keeloq.serial = (key & 0x0FFFF000) | 0x00000869, - .keeloq.btn = 0x02, - .keeloq.cnt = 0x03, - .keeloq.manuf = "Dea_Mio"}; + gen_info = (GenInfo){ + .type = GenKeeloq, + .mod = "AM650", + .freq = 433920000, + .keeloq.serial = (key & 0x0FFFF000) | 0x00000869, + .keeloq.btn = 0x02, + .keeloq.cnt = 0x03, + .keeloq.manuf = "Dea_Mio"}; break; case SetTypeGeniusBravo433: - gen_info = (GenInfo){.type = GenKeeloq, - .mod = "AM650", - .freq = 433920000, - .keeloq.serial = key & 0x00FFFFFF, - .keeloq.btn = 0x06, - .keeloq.cnt = 0x03, - .keeloq.manuf = "Genius_Bravo"}; + gen_info = (GenInfo){ + .type = GenKeeloq, + .mod = "AM650", + .freq = 433920000, + .keeloq.serial = key & 0x00FFFFFF, + .keeloq.btn = 0x06, + .keeloq.cnt = 0x03, + .keeloq.manuf = "Genius_Bravo"}; break; case SetTypeJCM_433_92: - gen_info = (GenInfo){.type = GenKeeloq, - .mod = "AM650", - .freq = 433920000, - .keeloq.serial = key & 0x00FFFFFF, - .keeloq.btn = 0x02, - .keeloq.cnt = 0x03, - .keeloq.manuf = "JCM_Tech"}; + gen_info = (GenInfo){ + .type = GenKeeloq, + .mod = "AM650", + .freq = 433920000, + .keeloq.serial = key & 0x00FFFFFF, + .keeloq.btn = 0x02, + .keeloq.cnt = 0x03, + .keeloq.manuf = "JCM_Tech"}; break; case SetTypeNovoferm_433_92: - gen_info = (GenInfo){.type = GenKeeloq, - .mod = "AM650", - .freq = 433920000, - .keeloq.serial = (key & 0x0000FFFF) | 0x018F0000, - .keeloq.btn = 0x01, - .keeloq.cnt = 0x03, - .keeloq.manuf = "Novoferm"}; + gen_info = (GenInfo){ + .type = GenKeeloq, + .mod = "AM650", + .freq = 433920000, + .keeloq.serial = (key & 0x0000FFFF) | 0x018F0000, + .keeloq.btn = 0x01, + .keeloq.cnt = 0x03, + .keeloq.manuf = "Novoferm"}; break; case SetTypeHormannEcoStar_433_92: - gen_info = (GenInfo){.type = GenKeeloq, - .mod = "AM650", - .freq = 433920000, - .keeloq.serial = (key & 0x000FFFFF) | 0x02200000, - .keeloq.btn = 0x04, - .keeloq.cnt = 0x03, - .keeloq.manuf = "EcoStar"}; + gen_info = (GenInfo){ + .type = GenKeeloq, + .mod = "AM650", + .freq = 433920000, + .keeloq.serial = (key & 0x000FFFFF) | 0x02200000, + .keeloq.btn = 0x04, + .keeloq.cnt = 0x03, + .keeloq.manuf = "EcoStar"}; break; case SetTypeFAACRCXT_433_92: - gen_info = (GenInfo){.type = GenKeeloq, - .mod = "AM650", - .freq = 433920000, - .keeloq.serial = (key & 0x0000FFFF) | 0x00100000, - .keeloq.btn = 0x02, - .keeloq.cnt = 0x03, - .keeloq.manuf = "FAAC_RC,XT"}; + gen_info = (GenInfo){ + .type = GenKeeloq, + .mod = "AM650", + .freq = 433920000, + .keeloq.serial = (key & 0x0000FFFF) | 0x00100000, + .keeloq.btn = 0x02, + .keeloq.cnt = 0x03, + .keeloq.manuf = "FAAC_RC,XT"}; break; case SetTypeFAACRCXT_868: - gen_info = (GenInfo){.type = GenKeeloq, - .mod = "AM650", - .freq = 868350000, - .keeloq.serial = (key & 0x0000FFFF) | 0x00100000, - .keeloq.btn = 0x02, - .keeloq.cnt = 0x03, - .keeloq.manuf = "FAAC_RC,XT"}; + gen_info = (GenInfo){ + .type = GenKeeloq, + .mod = "AM650", + .freq = 868350000, + .keeloq.serial = (key & 0x0000FFFF) | 0x00100000, + .keeloq.btn = 0x02, + .keeloq.cnt = 0x03, + .keeloq.manuf = "FAAC_RC,XT"}; break; case SetTypeNormstahl_433_92: - gen_info = (GenInfo){.type = GenKeeloq, - .mod = "AM650", - .freq = 433920000, - .keeloq.serial = key & 0x0000FFFF, - .keeloq.btn = 0x04, - .keeloq.cnt = 0x03, - .keeloq.manuf = "Normstahl"}; + gen_info = (GenInfo){ + .type = GenKeeloq, + .mod = "AM650", + .freq = 433920000, + .keeloq.serial = key & 0x0000FFFF, + .keeloq.btn = 0x04, + .keeloq.cnt = 0x03, + .keeloq.manuf = "Normstahl"}; break; case SetTypeHCS101_433_92: - gen_info = (GenInfo){.type = GenKeeloq, - .mod = "AM650", - .freq = 433920000, - .keeloq.serial = key & 0x000FFFFF, - .keeloq.btn = 0x02, - .keeloq.cnt = 0x03, - .keeloq.manuf = "HCS101"}; + gen_info = (GenInfo){ + .type = GenKeeloq, + .mod = "AM650", + .freq = 433920000, + .keeloq.serial = key & 0x000FFFFF, + .keeloq.btn = 0x02, + .keeloq.cnt = 0x03, + .keeloq.manuf = "HCS101"}; break; case SetTypeSecPlus_v1_315_00: gen_info = (GenInfo){.type = GenSecPlus1, .mod = "AM650", .freq = 315000000}; @@ -733,36 +785,40 @@ bool subghz_scene_set_type_on_event(void* context, SceneManagerEvent event) { gen_info = (GenInfo){.type = GenSecPlus1, .mod = "AM650", .freq = 433920000}; break; case SetTypeSecPlus_v2_310_00: - gen_info = (GenInfo){.type = GenSecPlus2, - .mod = "AM650", - .freq = 310000000, - .sec_plus_2.serial = (key & 0x7FFFF3FC), // 850LM pairing - .sec_plus_2.btn = 0x68, - .sec_plus_2.cnt = 0xE500000}; + gen_info = (GenInfo){ + .type = GenSecPlus2, + .mod = "AM650", + .freq = 310000000, + .sec_plus_2.serial = (key & 0x7FFFF3FC), // 850LM pairing + .sec_plus_2.btn = 0x68, + .sec_plus_2.cnt = 0xE500000}; break; case SetTypeSecPlus_v2_315_00: - gen_info = (GenInfo){.type = GenSecPlus2, - .mod = "AM650", - .freq = 315000000, - .sec_plus_2.serial = (key & 0x7FFFF3FC), // 850LM pairing - .sec_plus_2.btn = 0x68, - .sec_plus_2.cnt = 0xE500000}; + gen_info = (GenInfo){ + .type = GenSecPlus2, + .mod = "AM650", + .freq = 315000000, + .sec_plus_2.serial = (key & 0x7FFFF3FC), // 850LM pairing + .sec_plus_2.btn = 0x68, + .sec_plus_2.cnt = 0xE500000}; break; case SetTypeSecPlus_v2_390_00: - gen_info = (GenInfo){.type = GenSecPlus2, - .mod = "AM650", - .freq = 390000000, - .sec_plus_2.serial = (key & 0x7FFFF3FC), // 850LM pairing - .sec_plus_2.btn = 0x68, - .sec_plus_2.cnt = 0xE500000}; + gen_info = (GenInfo){ + .type = GenSecPlus2, + .mod = "AM650", + .freq = 390000000, + .sec_plus_2.serial = (key & 0x7FFFF3FC), // 850LM pairing + .sec_plus_2.btn = 0x68, + .sec_plus_2.cnt = 0xE500000}; break; case SetTypeSecPlus_v2_433_00: - gen_info = (GenInfo){.type = GenSecPlus2, - .mod = "AM650", - .freq = 433920000, - .sec_plus_2.serial = (key & 0x7FFFF3FC), // 850LM pairing - .sec_plus_2.btn = 0x68, - .sec_plus_2.cnt = 0xE500000}; + gen_info = (GenInfo){ + .type = GenSecPlus2, + .mod = "AM650", + .freq = 433920000, + .sec_plus_2.serial = (key & 0x7FFFF3FC), // 850LM pairing + .sec_plus_2.btn = 0x68, + .sec_plus_2.cnt = 0xE500000}; break; default: furi_crash("Not implemented"); diff --git a/applications/main/subghz/scenes/subghz_scene_transmitter.c b/applications/main/subghz/scenes/subghz_scene_transmitter.c index 07c7b6041..ebd69059f 100644 --- a/applications/main/subghz/scenes/subghz_scene_transmitter.c +++ b/applications/main/subghz/scenes/subghz_scene_transmitter.c @@ -79,7 +79,7 @@ bool subghz_scene_transmitter_on_event(void* context, SceneManagerEvent event) { subghz_txrx_stop(subghz->txrx); if(subghz_custom_btn_get() != SUBGHZ_CUSTOM_BTN_OK) { subghz_custom_btn_set(SUBGHZ_CUSTOM_BTN_OK); - int8_t tmp_counter = furi_hal_subghz_get_rolling_counter_mult(); + int32_t tmp_counter = furi_hal_subghz_get_rolling_counter_mult(); furi_hal_subghz_set_rolling_counter_mult(0); // Calling restore! subghz_tx_start(subghz, subghz_txrx_get_fff_data(subghz->txrx)); diff --git a/lib/subghz/protocols/alutech_at_4n.c b/lib/subghz/protocols/alutech_at_4n.c index 71e1aca32..873f61ba9 100644 --- a/lib/subghz/protocols/alutech_at_4n.c +++ b/lib/subghz/protocols/alutech_at_4n.c @@ -279,7 +279,7 @@ static bool subghz_protocol_alutech_at_4n_gen_data( } else { instance->generic.cnt += furi_hal_subghz_get_rolling_counter_mult(); } - } else if(instance->generic.cnt >= 0xFFFF) { + } else if((instance->generic.cnt >= 0xFFFF) && (furi_hal_subghz_get_rolling_counter_mult() != 0)) { instance->generic.cnt = 0; } crc = subghz_protocol_alutech_at_4n_decrypt_data_crc((uint8_t)(instance->generic.cnt & 0xFF)); diff --git a/lib/subghz/protocols/came_atomo.c b/lib/subghz/protocols/came_atomo.c index a89a05d8d..5b9e6defd 100644 --- a/lib/subghz/protocols/came_atomo.c +++ b/lib/subghz/protocols/came_atomo.c @@ -191,7 +191,7 @@ static void subghz_protocol_encoder_came_atomo_get_upload( } else { instance->generic.cnt += furi_hal_subghz_get_rolling_counter_mult(); } - } else if(instance->generic.cnt >= 0xFFFF) { + } else if((instance->generic.cnt >= 0xFFFF) && (furi_hal_subghz_get_rolling_counter_mult() != 0)) { instance->generic.cnt = 0; } diff --git a/lib/subghz/protocols/faac_slh.c b/lib/subghz/protocols/faac_slh.c index b095977e1..489fbdbc2 100644 --- a/lib/subghz/protocols/faac_slh.c +++ b/lib/subghz/protocols/faac_slh.c @@ -140,9 +140,40 @@ static bool subghz_protocol_faac_slh_gen_data(SubGhzProtocolEncoderFaacSLH* inst data_prg[0] = 0x00; if(allow_zero_seed || (instance->generic.seed != 0x0)) { - instance->generic.cnt += furi_hal_subghz_get_rolling_counter_mult(); + if(!(furi_hal_subghz_get_rolling_counter_mult() >= 0xFFFF)) { + if(instance->generic.cnt < 0xFFFFF) { + if((instance->generic.cnt + furi_hal_subghz_get_rolling_counter_mult()) > + 0xFFFFF) { + instance->generic.cnt = 0; + } else { + instance->generic.cnt += furi_hal_subghz_get_rolling_counter_mult(); + } + } else if( + (instance->generic.cnt >= 0xFFFFF) && + (furi_hal_subghz_get_rolling_counter_mult() != 0)) { + instance->generic.cnt = 0; + } + } else { + instance->generic.cnt += 1; + } + if(temp_counter_backup != 0x0) { - temp_counter_backup += furi_hal_subghz_get_rolling_counter_mult(); + if(!(furi_hal_subghz_get_rolling_counter_mult() >= 0xFFFF)) { + if(temp_counter_backup < 0xFFFFF) { + if((temp_counter_backup + furi_hal_subghz_get_rolling_counter_mult()) > + 0xFFFFF) { + temp_counter_backup = 0; + } else { + temp_counter_backup += furi_hal_subghz_get_rolling_counter_mult(); + } + } else if( + (temp_counter_backup >= 0xFFFFF) && + (furi_hal_subghz_get_rolling_counter_mult() != 0)) { + temp_counter_backup = 0; + } + } else { + temp_counter_backup += 1; + } } } @@ -193,7 +224,9 @@ static bool subghz_protocol_faac_slh_gen_data(SubGhzProtocolEncoderFaacSLH* inst (temp_fix_backup != 0x0) && !faac_prog_mode) { instance->generic.serial = temp_fix_backup >> 4; instance->generic.btn = temp_fix_backup & 0xF; - instance->generic.cnt = temp_counter_backup; + if(temp_counter_backup != 0x0) { + instance->generic.cnt = temp_counter_backup; + } } uint32_t fix = instance->generic.serial << 4 | instance->generic.btn; uint32_t hop = 0; @@ -207,7 +240,32 @@ static bool subghz_protocol_faac_slh_gen_data(SubGhzProtocolEncoderFaacSLH* inst } if(allow_zero_seed || (instance->generic.seed != 0x0)) { - instance->generic.cnt += furi_hal_subghz_get_rolling_counter_mult(); + if(!(furi_hal_subghz_get_rolling_counter_mult() >= 0xFFFF)) { + if(instance->generic.cnt < 0xFFFFF) { + if((instance->generic.cnt + furi_hal_subghz_get_rolling_counter_mult()) > + 0xFFFFF) { + instance->generic.cnt = 0; + } else { + instance->generic.cnt += furi_hal_subghz_get_rolling_counter_mult(); + } + } else if( + (instance->generic.cnt >= 0xFFFFF) && + (furi_hal_subghz_get_rolling_counter_mult() != 0)) { + instance->generic.cnt = 0; + } + } else { + if(instance->generic.cnt < 0xFFFFF) { + if((instance->generic.cnt + 0xFFFFF) > 0xFFFFF) { + instance->generic.cnt = 0; + } else { + instance->generic.cnt += 0xFFFFF; + } + } else if( + (instance->generic.cnt >= 0xFFFFF) && + (furi_hal_subghz_get_rolling_counter_mult() != 0)) { + instance->generic.cnt = 0; + } + } } if((instance->generic.cnt % 2) == 0) { @@ -248,7 +306,7 @@ bool subghz_protocol_faac_slh_create_data( const char* manufacture_name, SubGhzRadioPreset* preset) { furi_assert(context); - // roguemaster don't steal!!! + // OwO SubGhzProtocolEncoderFaacSLH* instance = context; instance->generic.serial = serial; instance->generic.btn = btn; diff --git a/lib/subghz/protocols/hay21.c b/lib/subghz/protocols/hay21.c index 21d186df8..1e3576459 100644 --- a/lib/subghz/protocols/hay21.c +++ b/lib/subghz/protocols/hay21.c @@ -151,6 +151,9 @@ static void subghz_protocol_encoder_hay21_get_upload(SubGhzProtocolEncoderHay21* } else { instance->generic.cnt += furi_hal_subghz_get_rolling_counter_mult(); } + if(furi_hal_subghz_get_rolling_counter_mult() >= 0xF) { + instance->generic.cnt = 0xF; + } } else if(instance->generic.cnt >= 0xF) { instance->generic.cnt = 0; } diff --git a/lib/subghz/protocols/keeloq.c b/lib/subghz/protocols/keeloq.c index 12d739dec..a774e5825 100644 --- a/lib/subghz/protocols/keeloq.c +++ b/lib/subghz/protocols/keeloq.c @@ -190,7 +190,9 @@ static bool subghz_protocol_keeloq_gen_data( } else { instance->generic.cnt += furi_hal_subghz_get_rolling_counter_mult(); } - } else if(instance->generic.cnt >= 0xFFFF) { + } else if( + (instance->generic.cnt >= 0xFFFF) && + (furi_hal_subghz_get_rolling_counter_mult() != 0)) { instance->generic.cnt = 0; } } diff --git a/lib/subghz/protocols/kinggates_stylo_4k.c b/lib/subghz/protocols/kinggates_stylo_4k.c index 56795d2dd..119a198bc 100644 --- a/lib/subghz/protocols/kinggates_stylo_4k.c +++ b/lib/subghz/protocols/kinggates_stylo_4k.c @@ -161,7 +161,7 @@ static bool subghz_protocol_kinggates_stylo_4k_gen_data( } else { instance->generic.cnt += furi_hal_subghz_get_rolling_counter_mult(); } - } else if(instance->generic.cnt >= 0xFFFF) { + } else if((instance->generic.cnt >= 0xFFFF) && (furi_hal_subghz_get_rolling_counter_mult() != 0)) { instance->generic.cnt = 0; } diff --git a/lib/subghz/protocols/nice_flor_s.c b/lib/subghz/protocols/nice_flor_s.c index 8e007582b..50adb43dc 100644 --- a/lib/subghz/protocols/nice_flor_s.c +++ b/lib/subghz/protocols/nice_flor_s.c @@ -157,7 +157,7 @@ static void subghz_protocol_encoder_nice_flor_s_get_upload( } else { instance->generic.cnt += furi_hal_subghz_get_rolling_counter_mult(); } - } else if(instance->generic.cnt >= 0xFFFF) { + } else if((instance->generic.cnt >= 0xFFFF) && (furi_hal_subghz_get_rolling_counter_mult() != 0)) { instance->generic.cnt = 0; } uint64_t decrypt = ((uint64_t)instance->generic.serial << 16) | instance->generic.cnt; diff --git a/lib/subghz/protocols/somfy_keytis.c b/lib/subghz/protocols/somfy_keytis.c index 22d2b5e9f..0606b9bf5 100644 --- a/lib/subghz/protocols/somfy_keytis.c +++ b/lib/subghz/protocols/somfy_keytis.c @@ -136,7 +136,7 @@ static bool } else { instance->generic.cnt += furi_hal_subghz_get_rolling_counter_mult(); } - } else if(instance->generic.cnt >= 0xFFFF) { + } else if((instance->generic.cnt >= 0xFFFF) && (furi_hal_subghz_get_rolling_counter_mult() != 0)) { instance->generic.cnt = 0; } diff --git a/lib/subghz/protocols/somfy_telis.c b/lib/subghz/protocols/somfy_telis.c index a1308dd6d..fd41180d5 100644 --- a/lib/subghz/protocols/somfy_telis.c +++ b/lib/subghz/protocols/somfy_telis.c @@ -130,7 +130,7 @@ static bool subghz_protocol_somfy_telis_gen_data( } else { instance->generic.cnt += furi_hal_subghz_get_rolling_counter_mult(); } - } else if(instance->generic.cnt >= 0xFFFF) { + } else if((instance->generic.cnt >= 0xFFFF) && (furi_hal_subghz_get_rolling_counter_mult() != 0)) { instance->generic.cnt = 0; } diff --git a/lib/subghz/protocols/star_line.c b/lib/subghz/protocols/star_line.c index 75a7fd471..0005ad5fc 100644 --- a/lib/subghz/protocols/star_line.c +++ b/lib/subghz/protocols/star_line.c @@ -135,7 +135,7 @@ static bool } else { instance->generic.cnt += furi_hal_subghz_get_rolling_counter_mult(); } - } else if(instance->generic.cnt >= 0xFFFF) { + } else if((instance->generic.cnt >= 0xFFFF) && (furi_hal_subghz_get_rolling_counter_mult() != 0)) { instance->generic.cnt = 0; } uint32_t fix = btn << 24 | instance->generic.serial; diff --git a/targets/f7/api_symbols.csv b/targets/f7/api_symbols.csv index 7ad21efb5..45f912861 100644 --- a/targets/f7/api_symbols.csv +++ b/targets/f7/api_symbols.csv @@ -1716,7 +1716,7 @@ Function,+,furi_hal_subghz_flush_tx,void, Function,+,furi_hal_subghz_get_data_gpio,const GpioPin*, Function,+,furi_hal_subghz_get_ext_leds_and_amp,_Bool, Function,+,furi_hal_subghz_get_lqi,uint8_t, -Function,+,furi_hal_subghz_get_rolling_counter_mult,int8_t, +Function,+,furi_hal_subghz_get_rolling_counter_mult,int32_t, Function,+,furi_hal_subghz_get_rssi,float, Function,+,furi_hal_subghz_idle,void, Function,-,furi_hal_subghz_init,void, @@ -1736,7 +1736,7 @@ Function,+,furi_hal_subghz_set_ext_leds_and_amp,void,_Bool Function,+,furi_hal_subghz_set_frequency,uint32_t,uint32_t Function,+,furi_hal_subghz_set_frequency_and_path,uint32_t,uint32_t Function,+,furi_hal_subghz_set_path,void,FuriHalSubGhzPath -Function,+,furi_hal_subghz_set_rolling_counter_mult,void,int8_t +Function,+,furi_hal_subghz_set_rolling_counter_mult,void,int32_t Function,+,furi_hal_subghz_shutdown,void, Function,+,furi_hal_subghz_sleep,void, Function,+,furi_hal_subghz_start_async_rx,void,"FuriHalSubGhzCaptureCallback, void*" diff --git a/targets/f7/furi_hal/furi_hal_subghz.c b/targets/f7/furi_hal/furi_hal_subghz.c index 19aa0f766..dc6add277 100644 --- a/targets/f7/furi_hal/furi_hal_subghz.c +++ b/targets/f7/furi_hal/furi_hal_subghz.c @@ -51,7 +51,7 @@ typedef struct { volatile SubGhzRegulation regulation; const GpioPin* async_mirror_pin; - int8_t rolling_counter_mult; + int32_t rolling_counter_mult; bool ext_leds_and_amp : 1; bool dangerous_frequency_i : 1; } FuriHalSubGhz; @@ -65,11 +65,11 @@ volatile FuriHalSubGhz furi_hal_subghz = { .dangerous_frequency_i = false, }; -int8_t furi_hal_subghz_get_rolling_counter_mult(void) { +int32_t furi_hal_subghz_get_rolling_counter_mult(void) { return furi_hal_subghz.rolling_counter_mult; } -void furi_hal_subghz_set_rolling_counter_mult(int8_t mult) { +void furi_hal_subghz_set_rolling_counter_mult(int32_t mult) { furi_hal_subghz.rolling_counter_mult = mult; } diff --git a/targets/f7/furi_hal/furi_hal_subghz.h b/targets/f7/furi_hal/furi_hal_subghz.h index 20843520a..b08d41ba4 100644 --- a/targets/f7/furi_hal/furi_hal_subghz.h +++ b/targets/f7/furi_hal/furi_hal_subghz.h @@ -174,14 +174,14 @@ uint32_t furi_hal_subghz_set_frequency_and_path(uint32_t value); bool furi_hal_subghz_is_tx_allowed(uint32_t value); /** Get the current rolling protocols counter ++/-- value - * @return int8_t current value + * @return int32_t current value */ -int8_t furi_hal_subghz_get_rolling_counter_mult(void); +int32_t furi_hal_subghz_get_rolling_counter_mult(void); /** Set the current rolling protocols counter ++/-- value - * @param mult int8_t = -1, -10, -100, 0, 1, 10, 100 + * @param mult int32_t = -1, -10, -50, 0, 1, 10, 50 */ -void furi_hal_subghz_set_rolling_counter_mult(int8_t mult); +void furi_hal_subghz_set_rolling_counter_mult(int32_t mult); /** Set frequency * From 9da510389a7ec95afa8379e7ad96cb5687145192 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Sat, 15 Mar 2025 07:58:51 +0300 Subject: [PATCH 123/268] small fixes --- .../services/notification/notification_app.c | 128 +----------------- .../services/notification/notification_app.h | 15 -- .../notification_settings_app.c | 2 +- 3 files changed, 3 insertions(+), 142 deletions(-) diff --git a/applications/services/notification/notification_app.c b/applications/services/notification/notification_app.c index 4bd93cfbc..cf03d4389 100644 --- a/applications/services/notification/notification_app.c +++ b/applications/services/notification/notification_app.c @@ -189,111 +189,6 @@ static void notification_display_timer(void* ctx) { notification_message(app, &sequence_display_backlight_off); } -// // --- RGB MOD RAINBOW SECTION --- - -// //start furi timer for rgb_mod_rainbow -// static void rgb_mod_rainbow_timer_start(NotificationApp* app) { -// furi_timer_start( -// app->rgb_mod_rainbow_timer, furi_ms_to_ticks(app->settings.rgb_mod_rainbow_speed_ms)); -// } - -// //stop furi timer for rgb_mod_rainbow -// static void rgb_mod_rainbow_timer_stop(NotificationApp* app) { -// furi_timer_stop(app->rgb_mod_rainbow_timer); -// } - -// // start/restart/stop rgb_mod_rainbow_timer only if rgb_mod_installed and apply rainbow colors to backlight -// static void rgb_mod_rainbow_timer_starter(NotificationApp* app) { -// if(app->settings.rgb_mod_installed) { -// if(app->settings.rgb_mod_rainbow_mode > 0) { -// rgb_mod_rainbow_update( -// app->rgb_mod_rainbow_red, -// app->rgb_mod_rainbow_green, -// app->rgb_mod_rainbow_blue, -// app->settings.display_brightness); -// rgb_mod_rainbow_timer_start(app); -// } else { -// if(furi_timer_is_running(app->rgb_mod_rainbow_timer)) { -// rgb_mod_rainbow_timer_stop(app); -// } -// } -// } -// } - -// // callback for rgb_mod_rainbow_timer (what we do when timer end) -// static void rgb_mod_rainbow_timer_callback(void* context) { -// furi_assert(context); -// NotificationApp* app = context; - -// // if rgb_mode_rainbow_mode is rainbow do rainbow effect -// if(app->settings.rgb_mod_rainbow_mode == 1) { -// switch(app->rgb_mod_rainbow_stage) { -// // from red to yellow -// case 1: -// app->rgb_mod_rainbow_green += app->settings.rgb_mod_rainbow_step; -// if(app->rgb_mod_rainbow_green >= 255) { -// app->rgb_mod_rainbow_green = 255; -// app->rgb_mod_rainbow_stage++; -// } -// break; -// // yellow red to green -// case 2: -// app->rgb_mod_rainbow_red -= app->settings.rgb_mod_rainbow_step; -// if(app->rgb_mod_rainbow_red <= 0) { -// app->rgb_mod_rainbow_red = 0; -// app->rgb_mod_rainbow_stage++; -// } -// break; -// // from green to light blue -// case 3: -// app->rgb_mod_rainbow_blue += app->settings.rgb_mod_rainbow_step; -// if(app->rgb_mod_rainbow_blue >= 255) { -// app->rgb_mod_rainbow_blue = 255; -// app->rgb_mod_rainbow_stage++; -// } -// break; -// //from light blue to blue -// case 4: -// app->rgb_mod_rainbow_green -= app->settings.rgb_mod_rainbow_step; -// if(app->rgb_mod_rainbow_green <= 0) { -// app->rgb_mod_rainbow_green = 0; -// app->rgb_mod_rainbow_stage++; -// } -// break; -// //from blue to violet -// case 5: -// app->rgb_mod_rainbow_red += app->settings.rgb_mod_rainbow_step; -// if(app->rgb_mod_rainbow_red >= 255) { -// app->rgb_mod_rainbow_red = 255; -// app->rgb_mod_rainbow_stage++; -// } -// break; -// //from violet to red -// case 6: -// app->rgb_mod_rainbow_blue -= app->settings.rgb_mod_rainbow_step; -// if(app->rgb_mod_rainbow_blue <= 0) { -// app->rgb_mod_rainbow_blue = 0; -// app->rgb_mod_rainbow_stage = 1; -// } -// break; -// default: -// break; -// } - -// rgb_mod_rainbow_update( -// app->rgb_mod_rainbow_red, -// app->rgb_mod_rainbow_green, -// app->rgb_mod_rainbow_blue, -// app->settings.display_brightness); -// } - -// // if rgb_mode_rainbow_mode is ..... do another effect -// // if(app->settings.rgb_mod_rainbow_mode == 2) { -// // } -// } - -// // --- END OF RGB MOD RAINBOW SECTION --- - // message processing static void notification_process_notification_message( NotificationApp* app, @@ -629,7 +524,7 @@ static void input_event_callback(const void* value, void* context) { static NotificationApp* notification_app_alloc(void) { NotificationApp* app = malloc(sizeof(NotificationApp)); app->queue = furi_message_queue_alloc(8, sizeof(NotificationAppMessage)); - app->display_timer = furi_timer_alloc(notification_display_timer, FuriTimerTypePeriodic, app); + app->display_timer = furi_timer_alloc(notification_display_timer, FuriTimerTypeOnce, app); app->settings.speaker_volume = 1.0f; app->settings.display_brightness = 1.0f; @@ -664,26 +559,9 @@ static NotificationApp* notification_app_alloc(void) { furi_pubsub_subscribe(app->event_record, input_event_callback, app); notification_message(app, &sequence_display_backlight_on); - // // --- RGB MOD INIT SETTINGS SECTION --- - - // app->settings.rgb_mod_installed = false; - // app->settings.rgb_mod_rainbow_mode = 0; - // app->settings.rgb_mod_rainbow_speed_ms = 100; - // app->settings.rgb_mod_rainbow_step = 5; - // app->rgb_mod_rainbow_red = 255; - // app->rgb_mod_rainbow_green = 0; - // app->rgb_mod_rainbow_blue = 0; - // app->rgb_mod_rainbow_stage = 1; - - // //define rgb_mod_rainbow_timer and they callback - // app->rgb_mod_rainbow_timer = - // furi_timer_alloc(rgb_mod_rainbow_timer_callback, FuriTimerTypePeriodic, app); - // // --- END OF RGB MOD INIT SETTINGS SECTION --- - return app; } - static void notification_storage_callback(const void* message, void* context) { furi_assert(context); NotificationApp* app = context; @@ -704,8 +582,6 @@ static void notification_apply_settings(NotificationApp* app) { } notification_apply_lcd_contrast(app); - // //start rgb_mod_rainbow_timer on system init if they ON in config - // rgb_mod_rainbow_timer_starter(app); } static void notification_init_settings(NotificationApp* app) { @@ -724,7 +600,7 @@ static void notification_init_settings(NotificationApp* app) { int32_t notification_srv(void* p) { UNUSED(p); NotificationApp* app = notification_app_alloc(); - app->rgb_srv = furi_record_open (RECORD_RGB_BACKLIGHT); + app->rgb_srv = furi_record_open(RECORD_RGB_BACKLIGHT); notification_init_settings(app); notification_vibro_off(); diff --git a/applications/services/notification/notification_app.h b/applications/services/notification/notification_app.h index 4383ca6bc..0209ffa0b 100644 --- a/applications/services/notification/notification_app.h +++ b/applications/services/notification/notification_app.h @@ -45,13 +45,6 @@ typedef struct { uint32_t display_off_delay_ms; int8_t contrast; bool vibro_on; - /// --- RGB MOD SETTINGS SECTION --- - // bool rgb_mod_installed; - // uint32_t rgb_mod_rainbow_mode; - // uint32_t rgb_mod_rainbow_speed_ms; - // uint16_t rgb_mod_rainbow_step; - /// --- END OF RGB MOD SETTINGS SECTION --- - } NotificationSettings; struct NotificationApp { @@ -63,14 +56,6 @@ struct NotificationApp { NotificationLedLayer led[NOTIFICATION_LED_COUNT]; uint8_t display_led_lock; - // --- RGB RAINBOW MODE VARIABLES SECTION --- - // FuriTimer* rgb_mod_rainbow_timer; - // int16_t rgb_mod_rainbow_red; - // int16_t rgb_mod_rainbow_green; - // int16_t rgb_mod_rainbow_blue; - // uint8_t rgb_mod_rainbow_stage; - // --- ENd OF RGB RAINBOW MODE VARIABLES SECTION --- - NotificationSettings settings; RGBBacklightApp* rgb_srv; }; diff --git a/applications/settings/notification_settings/notification_settings_app.c b/applications/settings/notification_settings/notification_settings_app.c index 82e4526ae..ff90e96e7 100644 --- a/applications/settings/notification_settings/notification_settings_app.c +++ b/applications/settings/notification_settings/notification_settings_app.c @@ -435,7 +435,7 @@ static NotificationAppSettings* alloc_settings(void) { variable_item_set_current_value_text(item, contrast_text[value_index]); item = variable_item_list_add( - app->variable_item_list, "LCD Brightness", BACKLIGHT_COUNT, backlight_changed, app); + app->variable_item_list, "LCD Backlight", BACKLIGHT_COUNT, backlight_changed, app); value_index = value_index_float( app->notification->settings.display_brightness, backlight_value, BACKLIGHT_COUNT); variable_item_set_current_value_index(item, value_index); From 7f1c8684c666f780562748070e31c484e7c117bf Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Sat, 15 Mar 2025 08:10:25 +0300 Subject: [PATCH 124/268] remove build [ci skip] --- .ci_files/devbuild_msg_discord.txt | 5 +- .ci_files/devbuild_msg_telegram.txt | 4 +- .ci_files/release_msg_discord.txt | 5 +- .ci_files/release_msg_telegram.txt | 4 +- .ci_files/rgb.patch | 677 ---------------------------- .drone.yml | 76 ---- ReadMe.md | 2 +- documentation/FAQ.md | 9 +- 8 files changed, 14 insertions(+), 768 deletions(-) delete mode 100644 .ci_files/rgb.patch diff --git a/.ci_files/devbuild_msg_discord.txt b/.ci_files/devbuild_msg_discord.txt index 1b7b7bd7f..0bc802364 100644 --- a/.ci_files/devbuild_msg_discord.txt +++ b/.ci_files/devbuild_msg_discord.txt @@ -9,8 +9,7 @@ How to [install firmware](https://github.com/DarkFlippers/unleashed-firmware/blo [Default](https://lab.flipper.net/?url=https://unleashedflip.com/fw/dev/flipper-z-f7-update-(buildnum).tgz&channel=dev-cfw&version=(buildnum)) > ` ` [Extra apps](https://lab.flipper.net/?url=https://unleashedflip.com/fw_extra_apps/flipper-z-f7-update-(buildnum)e.tgz&channel=dev-cfw&version=(buildnum)e) > `e` [No apps](https://lab.flipper.net/?url=https://unleashedflip.com/fw_extra_apps/flipper-z-f7-update-(buildnum)c.tgz&channel=dev-cfw&version=(buildnum)c) > `c` -[RGB patch - only for hardware mod!](https://lab.flipper.net/?url=https://unleashedflip.com/fw_extra_apps/flipper-z-f7-update-(buildnum)r.tgz&channel=dev-cfw&version=(buildnum)r) > `r` -What ` `, `e`, `c`, `r` means? -> [versions info](https://github.com/DarkFlippers/unleashed-firmware/blob/dev/CHANGELOG.md#what-n-r-e---c-means-what-i-need-to-download-if-i-dont-want-to-use-web-updater) +What ` `, `e`, `c` means? -> [versions info](https://github.com/DarkFlippers/unleashed-firmware/blob/dev/CHANGELOG.md#what-n-r-e---c-means-what-i-need-to-download-if-i-dont-want-to-use-web-updater) ### Direct tgz download links: -[Default](https://unleashedflip.com/fw/dev/flipper-z-f7-update-(buildnum).tgz) > ` ` - [Extra apps](https://unleashedflip.com/fw_extra_apps/flipper-z-f7-update-(buildnum)e.tgz) > `e` - [No apps](https://unleashedflip.com/fw_extra_apps/flipper-z-f7-update-(buildnum)c.tgz) > `c` - [RGB patch - only for hardware mod!](https://unleashedflip.com/fw_extra_apps/flipper-z-f7-update-(buildnum)r.tgz) > `r` +[Default](https://unleashedflip.com/fw/dev/flipper-z-f7-update-(buildnum).tgz) > ` ` - [Extra apps](https://unleashedflip.com/fw_extra_apps/flipper-z-f7-update-(buildnum)e.tgz) > `e` - [No apps](https://unleashedflip.com/fw_extra_apps/flipper-z-f7-update-(buildnum)c.tgz) > `c` diff --git a/.ci_files/devbuild_msg_telegram.txt b/.ci_files/devbuild_msg_telegram.txt index e03d19c6c..5a3051471 100644 --- a/.ci_files/devbuild_msg_telegram.txt +++ b/.ci_files/devbuild_msg_telegram.txt @@ -10,13 +10,11 @@ How to [install firmware](https://github.com/DarkFlippers/unleashed-firmware/blo [Default](https://lab.flipper.net/?url=https://unleashedflip.com/fw/dev/flipper-z-f7-update-(buildnum).tgz&channel=dev-cfw&version=(buildnum)) > ` ` [Extra apps](https://lab.flipper.net/?url=https://unleashedflip.com/fw_extra_apps/flipper-z-f7-update-(buildnum)e.tgz&channel=dev-cfw&version=(buildnum)e) > `e` [No apps](https://lab.flipper.net/?url=https://unleashedflip.com/fw_extra_apps/flipper-z-f7-update-(buildnum)c.tgz&channel=dev-cfw&version=(buildnum)c) > `c` -[RGB patch - only for hardware mod!](https://lab.flipper.net/?url=https://unleashedflip.com/fw_extra_apps/flipper-z-f7-update-(buildnum)r.tgz&channel=dev-cfw&version=(buildnum)r) > `r` -What ` `, `e`, `c`, `r` means? -> [versions info](https://github.com/DarkFlippers/unleashed-firmware/blob/dev/CHANGELOG.md#what-n-r-e---c-means-what-i-need-to-download-if-i-dont-want-to-use-web-updater) +What ` `, `e`, `c` means? -> [versions info](https://github.com/DarkFlippers/unleashed-firmware/blob/dev/CHANGELOG.md#what-n-r-e---c-means-what-i-need-to-download-if-i-dont-want-to-use-web-updater) **Direct tgz download links:** [Default](https://unleashedflip.com/fw/dev/flipper-z-f7-update-(buildnum).tgz) > ` ` [Extra apps](https://unleashedflip.com/fw_extra_apps/flipper-z-f7-update-(buildnum)e.tgz) > `e` [No apps](https://unleashedflip.com/fw_extra_apps/flipper-z-f7-update-(buildnum)c.tgz) > `c` -[RGB patch - only for hardware mod!](https://unleashedflip.com/fw_extra_apps/flipper-z-f7-update-(buildnum)r.tgz) > `r` diff --git a/.ci_files/release_msg_discord.txt b/.ci_files/release_msg_discord.txt index 079135f40..e25e0d95b 100644 --- a/.ci_files/release_msg_discord.txt +++ b/.ci_files/release_msg_discord.txt @@ -9,8 +9,7 @@ How to [install firmware](https://github.com/DarkFlippers/unleashed-firmware/blo [Default](https://lab.flipper.net/?url=https://unleashedflip.com/fw/(releasever)/flipper-z-f7-update-(releasever).tgz&channel=release-cfw&version=(releasever)) > ` ` [Extra apps](https://lab.flipper.net/?url=https://unleashedflip.com/fw_extra_apps/flipper-z-f7-update-(releasever)e.tgz&channel=release-cfw&version=(releasever)e) > `e` [No apps](https://lab.flipper.net/?url=https://unleashedflip.com/fw_extra_apps/flipper-z-f7-update-(releasever)c.tgz&channel=release-cfw&version=(releasever)c) > `c` -[RGB patch - only for hardware mod!](https://lab.flipper.net/?url=https://unleashedflip.com/fw_extra_apps/flipper-z-f7-update-(releasever)r.tgz&channel=release-cfw&version=(releasever)r) > `r` -What ` `, `e`, `c`, `r` means? -> [versions info](https://github.com/DarkFlippers/unleashed-firmware/blob/dev/CHANGELOG.md#what-n-r-e---c-means-what-i-need-to-download-if-i-dont-want-to-use-web-updater) +What ` `, `e`, `c` means? -> [versions info](https://github.com/DarkFlippers/unleashed-firmware/blob/dev/CHANGELOG.md#what-n-r-e---c-means-what-i-need-to-download-if-i-dont-want-to-use-web-updater) ### Direct tgz download links: -[Default](https://unleashedflip.com/fw/(releasever)/flipper-z-f7-update-(releasever).tgz) > ` ` - [Extra apps](https://unleashedflip.com/fw_extra_apps/flipper-z-f7-update-(releasever)e.tgz) > `e` - [No apps](https://unleashedflip.com/fw_extra_apps/flipper-z-f7-update-(releasever)c.tgz) > `c` - [RGB patch - only for hardware mod!](https://unleashedflip.com/fw_extra_apps/flipper-z-f7-update-(releasever)r.tgz) > `r` +[Default](https://unleashedflip.com/fw/(releasever)/flipper-z-f7-update-(releasever).tgz) > ` ` - [Extra apps](https://unleashedflip.com/fw_extra_apps/flipper-z-f7-update-(releasever)e.tgz) > `e` - [No apps](https://unleashedflip.com/fw_extra_apps/flipper-z-f7-update-(releasever)c.tgz) > `c` diff --git a/.ci_files/release_msg_telegram.txt b/.ci_files/release_msg_telegram.txt index f561fee21..fd8c13bcf 100644 --- a/.ci_files/release_msg_telegram.txt +++ b/.ci_files/release_msg_telegram.txt @@ -10,13 +10,11 @@ How to [install firmware](https://github.com/DarkFlippers/unleashed-firmware/blo [Default](https://lab.flipper.net/?url=https://unleashedflip.com/fw/(releasever)/flipper-z-f7-update-(releasever).tgz&channel=release-cfw&version=(releasever)) > ` ` [Extra apps](https://lab.flipper.net/?url=https://unleashedflip.com/fw_extra_apps/flipper-z-f7-update-(releasever)e.tgz&channel=release-cfw&version=(releasever)e) > `e` [No apps](https://lab.flipper.net/?url=https://unleashedflip.com/fw_extra_apps/flipper-z-f7-update-(releasever)c.tgz&channel=release-cfw&version=(releasever)c) > `c` -[RGB patch - only for hardware mod!](https://lab.flipper.net/?url=https://unleashedflip.com/fw_extra_apps/flipper-z-f7-update-(releasever)r.tgz&channel=release-cfw&version=(releasever)r) > `r` -What ` `, `e`, `c`, `r` means? -> [versions info](https://github.com/DarkFlippers/unleashed-firmware/blob/dev/CHANGELOG.md#what-n-r-e---c-means-what-i-need-to-download-if-i-dont-want-to-use-web-updater) +What ` `, `e`, `c` means? -> [versions info](https://github.com/DarkFlippers/unleashed-firmware/blob/dev/CHANGELOG.md#what-n-r-e---c-means-what-i-need-to-download-if-i-dont-want-to-use-web-updater) **Direct tgz download links:** [Default](https://unleashedflip.com/fw/(releasever)/flipper-z-f7-update-(releasever).tgz) > ` ` [Extra apps](https://unleashedflip.com/fw_extra_apps/flipper-z-f7-update-(releasever)e.tgz) > `e` [No apps](https://unleashedflip.com/fw_extra_apps/flipper-z-f7-update-(releasever)c.tgz) > `c` -[RGB patch - only for hardware mod!](https://unleashedflip.com/fw_extra_apps/flipper-z-f7-update-(releasever)r.tgz) > `r` diff --git a/.ci_files/rgb.patch b/.ci_files/rgb.patch deleted file mode 100644 index 81783325f..000000000 --- a/.ci_files/rgb.patch +++ /dev/null @@ -1,677 +0,0 @@ -diff --git a/applications/services/notification/notification_app.c b/applications/services/notification/notification_app.c -index 35d2fe6..1af97e2 100644 ---- a/applications/services/notification/notification_app.c -+++ b/applications/services/notification/notification_app.c -@@ -9,6 +9,7 @@ - #include "notification.h" - #include "notification_messages.h" - #include "notification_app.h" -+#include "applications/settings/notification_settings/rgb_backlight.h" - - #define TAG "NotificationSrv" - -@@ -616,6 +617,7 @@ int32_t notification_srv(void* p) { - break; - case SaveSettingsMessage: - notification_save_settings(app); -+ rgb_backlight_save_settings(); - break; - case LoadSettingsMessage: - notification_load_settings(app); -diff --git a/applications/settings/notification_settings/notification_settings_app.c b/applications/settings/notification_settings/notification_settings_app.c -index 2462b32..8e045ce 100644 ---- a/applications/settings/notification_settings/notification_settings_app.c -+++ b/applications/settings/notification_settings/notification_settings_app.c -@@ -3,6 +3,7 @@ - #include - #include - #include -+#include - - #define MAX_NOTIFICATION_SETTINGS 4 - -@@ -13,6 +14,8 @@ typedef struct { - VariableItemList* variable_item_list; - } NotificationAppSettings; - -+static VariableItem* temp_item; -+ - static const NotificationSequence sequence_note_c = { - &message_note_c5, - &message_delay_100, -@@ -168,6 +171,59 @@ static void vibro_changed(VariableItem* item) { - notification_message(app->notification, &sequence_single_vibro); - } - -+// Set RGB backlight color -+static void color_changed(VariableItem* item) { -+ NotificationAppSettings* app = variable_item_get_context(item); -+ uint8_t index = variable_item_get_current_value_index(item); -+ rgb_backlight_set_color(index); -+ variable_item_set_current_value_text(item, rgb_backlight_get_color_text(index)); -+ notification_message(app->notification, &sequence_display_backlight_on); -+} -+ -+// TODO: refactor and fix this -+static void color_set_custom_red(VariableItem* item) { -+ NotificationAppSettings* app = variable_item_get_context(item); -+ uint8_t index = variable_item_get_current_value_index(item); -+ rgb_backlight_set_custom_color(index, 0); -+ char valtext[4] = {}; -+ snprintf(valtext, sizeof(valtext), "%d", index); -+ variable_item_set_current_value_text(item, valtext); -+ rgb_backlight_set_color(13); -+ rgb_backlight_update(app->notification->settings.display_brightness * 0xFF, true); -+ // Set to custom color explicitly -+ variable_item_set_current_value_index(temp_item, 13); -+ variable_item_set_current_value_text(temp_item, rgb_backlight_get_color_text(13)); -+ notification_message(app->notification, &sequence_display_backlight_on); -+} -+static void color_set_custom_green(VariableItem* item) { -+ NotificationAppSettings* app = variable_item_get_context(item); -+ uint8_t index = variable_item_get_current_value_index(item); -+ rgb_backlight_set_custom_color(index, 1); -+ char valtext[4] = {}; -+ snprintf(valtext, sizeof(valtext), "%d", index); -+ variable_item_set_current_value_text(item, valtext); -+ rgb_backlight_set_color(13); -+ rgb_backlight_update(app->notification->settings.display_brightness * 0xFF, true); -+ // Set to custom color explicitly -+ variable_item_set_current_value_index(temp_item, 13); -+ variable_item_set_current_value_text(temp_item, rgb_backlight_get_color_text(13)); -+ notification_message(app->notification, &sequence_display_backlight_on); -+} -+static void color_set_custom_blue(VariableItem* item) { -+ NotificationAppSettings* app = variable_item_get_context(item); -+ uint8_t index = variable_item_get_current_value_index(item); -+ rgb_backlight_set_custom_color(index, 2); -+ char valtext[4] = {}; -+ snprintf(valtext, sizeof(valtext), "%d", index); -+ variable_item_set_current_value_text(item, valtext); -+ rgb_backlight_set_color(13); -+ rgb_backlight_update(app->notification->settings.display_brightness * 0xFF, true); -+ // Set to custom color explicitly -+ variable_item_set_current_value_index(temp_item, 13); -+ variable_item_set_current_value_text(temp_item, rgb_backlight_get_color_text(13)); -+ notification_message(app->notification, &sequence_display_backlight_on); -+} -+ - static uint32_t notification_app_settings_exit(void* context) { - UNUSED(context); - return VIEW_NONE; -@@ -192,8 +248,40 @@ static NotificationAppSettings* alloc_settings(void) { - variable_item_set_current_value_index(item, value_index); - variable_item_set_current_value_text(item, contrast_text[value_index]); - -+ // RGB Colors -+ item = variable_item_list_add( -+ app->variable_item_list, "LCD Color", rgb_backlight_get_color_count(), color_changed, app); -+ value_index = rgb_backlight_get_settings()->display_color_index; -+ variable_item_set_current_value_index(item, value_index); -+ variable_item_set_current_value_text(item, rgb_backlight_get_color_text(value_index)); -+ temp_item = item; -+ -+ // Custom Color - REFACTOR THIS -+ item = variable_item_list_add( -+ app->variable_item_list, "Custom Red", 255, color_set_custom_red, app); -+ value_index = rgb_backlight_get_settings()->custom_r; -+ variable_item_set_current_value_index(item, value_index); -+ char valtext[4] = {}; -+ snprintf(valtext, sizeof(valtext), "%d", value_index); -+ variable_item_set_current_value_text(item, valtext); -+ -+ item = variable_item_list_add( -+ app->variable_item_list, "Custom Green", 255, color_set_custom_green, app); -+ value_index = rgb_backlight_get_settings()->custom_g; -+ variable_item_set_current_value_index(item, value_index); -+ snprintf(valtext, sizeof(valtext), "%d", value_index); -+ variable_item_set_current_value_text(item, valtext); -+ -+ item = variable_item_list_add( -+ app->variable_item_list, "Custom Blue", 255, color_set_custom_blue, app); -+ value_index = rgb_backlight_get_settings()->custom_b; -+ variable_item_set_current_value_index(item, value_index); -+ snprintf(valtext, sizeof(valtext), "%d", value_index); -+ variable_item_set_current_value_text(item, valtext); -+ // End of RGB -+ - item = variable_item_list_add( -- app->variable_item_list, "LCD Backlight", BACKLIGHT_COUNT, backlight_changed, app); -+ app->variable_item_list, "LCD Brightness", BACKLIGHT_COUNT, backlight_changed, app); - value_index = value_index_float( - app->notification->settings.display_brightness, backlight_value, BACKLIGHT_COUNT); - variable_item_set_current_value_index(item, value_index); -diff --git a/applications/settings/notification_settings/rgb_backlight.c b/applications/settings/notification_settings/rgb_backlight.c -new file mode 100644 -index 0000000..4edd775 ---- /dev/null -+++ b/applications/settings/notification_settings/rgb_backlight.c -@@ -0,0 +1,217 @@ -+/* -+ RGB backlight FlipperZero driver -+ Copyright (C) 2022-2023 Victor Nikitchuk (https://github.com/quen0n) -+ -+ This program is free software: you can redistribute it and/or modify -+ it under the terms of the GNU General Public License as published by -+ the Free Software Foundation, either version 3 of the License, or -+ (at your option) any later version. -+ -+ This program is distributed in the hope that it will be useful, -+ but WITHOUT ANY WARRANTY; without even the implied warranty of -+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -+ GNU General Public License for more details. -+ -+ You should have received a copy of the GNU General Public License -+ along with this program. If not, see . -+*/ -+ -+#include "rgb_backlight.h" -+#include -+#include -+ -+#define RGB_BACKLIGHT_SETTINGS_VERSION 6 -+#define RGB_BACKLIGHT_SETTINGS_FILE_NAME ".rgb_backlight.settings" -+#define RGB_BACKLIGHT_SETTINGS_PATH INT_PATH(RGB_BACKLIGHT_SETTINGS_FILE_NAME) -+ -+#define COLOR_COUNT (sizeof(colors) / sizeof(RGBBacklightColor)) -+ -+#define TAG "RGB Backlight" -+ -+static RGBBacklightSettings rgb_settings = { -+ .version = RGB_BACKLIGHT_SETTINGS_VERSION, -+ .display_color_index = 0, -+ .custom_r = 254, -+ .custom_g = 254, -+ .custom_b = 254, -+ .settings_is_loaded = false}; -+ -+static const RGBBacklightColor colors[] = { -+ {"Orange", 255, 60, 0}, -+ {"Yellow", 255, 144, 0}, -+ {"Spring", 167, 255, 0}, -+ {"Lime", 0, 255, 0}, -+ {"Aqua", 0, 255, 127}, -+ {"Cyan", 0, 210, 210}, -+ {"Azure", 0, 127, 255}, -+ {"Blue", 0, 0, 255}, -+ {"Purple", 127, 0, 255}, -+ {"Magenta", 210, 0, 210}, -+ {"Pink", 255, 0, 127}, -+ {"Red", 255, 0, 0}, -+ {"White", 254, 210, 200}, -+ {"Custom", 0, 0, 0}, -+}; -+ -+uint8_t rgb_backlight_get_color_count(void) { -+ return COLOR_COUNT; -+} -+ -+const char* rgb_backlight_get_color_text(uint8_t index) { -+ return colors[index].name; -+} -+ -+void rgb_backlight_load_settings(void) { -+ // Do not load settings if we are in other boot modes than normal -+ if(furi_hal_rtc_get_boot_mode() != FuriHalRtcBootModeNormal) { -+ rgb_settings.settings_is_loaded = true; -+ return; -+ } -+ -+ // Wait for all required services to start and create their records -+ uint8_t timeout = 0; -+ while(!furi_record_exists(RECORD_STORAGE)) { -+ timeout++; -+ if(timeout > 150) { -+ rgb_settings.settings_is_loaded = true; -+ return; -+ } -+ furi_delay_ms(5); -+ } -+ -+ RGBBacklightSettings settings; -+ File* file = storage_file_alloc(furi_record_open(RECORD_STORAGE)); -+ const size_t settings_size = sizeof(RGBBacklightSettings); -+ -+ FURI_LOG_D(TAG, "loading settings from \"%s\"", RGB_BACKLIGHT_SETTINGS_PATH); -+ bool fs_result = -+ storage_file_open(file, RGB_BACKLIGHT_SETTINGS_PATH, FSAM_READ, FSOM_OPEN_EXISTING); -+ -+ if(fs_result) { -+ uint16_t bytes_count = storage_file_read(file, &settings, settings_size); -+ -+ if(bytes_count != settings_size) { -+ fs_result = false; -+ } -+ } -+ -+ if(fs_result) { -+ FURI_LOG_D(TAG, "load success"); -+ if(settings.version != RGB_BACKLIGHT_SETTINGS_VERSION) { -+ FURI_LOG_E( -+ TAG, -+ "version(%d != %d) mismatch", -+ settings.version, -+ RGB_BACKLIGHT_SETTINGS_VERSION); -+ } else { -+ memcpy(&rgb_settings, &settings, settings_size); -+ } -+ } else { -+ FURI_LOG_E(TAG, "load failed, %s", storage_file_get_error_desc(file)); -+ } -+ -+ storage_file_close(file); -+ storage_file_free(file); -+ furi_record_close(RECORD_STORAGE); -+ rgb_settings.settings_is_loaded = true; -+} -+ -+void rgb_backlight_save_settings(void) { -+ RGBBacklightSettings settings; -+ File* file = storage_file_alloc(furi_record_open(RECORD_STORAGE)); -+ const size_t settings_size = sizeof(RGBBacklightSettings); -+ -+ FURI_LOG_D(TAG, "saving settings to \"%s\"", RGB_BACKLIGHT_SETTINGS_PATH); -+ -+ memcpy(&settings, &rgb_settings, settings_size); -+ -+ bool fs_result = -+ storage_file_open(file, RGB_BACKLIGHT_SETTINGS_PATH, FSAM_WRITE, FSOM_CREATE_ALWAYS); -+ -+ if(fs_result) { -+ uint16_t bytes_count = storage_file_write(file, &settings, settings_size); -+ -+ if(bytes_count != settings_size) { -+ fs_result = false; -+ } -+ } -+ -+ if(fs_result) { -+ FURI_LOG_D(TAG, "save success"); -+ } else { -+ FURI_LOG_E(TAG, "save failed, %s", storage_file_get_error_desc(file)); -+ } -+ -+ storage_file_close(file); -+ storage_file_free(file); -+ furi_record_close(RECORD_STORAGE); -+} -+ -+RGBBacklightSettings* rgb_backlight_get_settings(void) { -+ if(!rgb_settings.settings_is_loaded) { -+ rgb_backlight_load_settings(); -+ } -+ return &rgb_settings; -+} -+ -+void rgb_backlight_set_color(uint8_t color_index) { -+ if(color_index > (rgb_backlight_get_color_count() - 1)) color_index = 0; -+ rgb_settings.display_color_index = color_index; -+} -+ -+void rgb_backlight_set_custom_color(uint8_t color, uint8_t index) { -+ if(index > 2) return; -+ if(index == 0) { -+ rgb_settings.custom_r = color; -+ } else if(index == 1) { -+ rgb_settings.custom_g = color; -+ } else if(index == 2) { -+ rgb_settings.custom_b = color; -+ } -+} -+ -+void rgb_backlight_update(uint8_t brightness, bool bypass) { -+ if(!rgb_settings.settings_is_loaded) { -+ rgb_backlight_load_settings(); -+ } -+ -+ if(!bypass) { -+ static uint8_t last_color_index = 255; -+ static uint8_t last_brightness = 123; -+ -+ if(last_brightness == brightness && last_color_index == rgb_settings.display_color_index) { -+ return; -+ } -+ -+ last_brightness = brightness; -+ last_color_index = rgb_settings.display_color_index; -+ } -+ -+ for(uint8_t i = 0; i < SK6805_get_led_count(); i++) { -+ if(rgb_settings.display_color_index == 13) { -+ uint8_t r = rgb_settings.custom_r * (brightness / 255.0f); -+ uint8_t g = rgb_settings.custom_g * (brightness / 255.0f); -+ uint8_t b = rgb_settings.custom_b * (brightness / 255.0f); -+ -+ SK6805_set_led_color(i, r, g, b); -+ } else { -+ if((colors[rgb_settings.display_color_index].red == 0) && -+ (colors[rgb_settings.display_color_index].green == 0) && -+ (colors[rgb_settings.display_color_index].blue == 0)) { -+ uint8_t r = colors[0].red * (brightness / 255.0f); -+ uint8_t g = colors[0].green * (brightness / 255.0f); -+ uint8_t b = colors[0].blue * (brightness / 255.0f); -+ -+ SK6805_set_led_color(i, r, g, b); -+ } else { -+ uint8_t r = colors[rgb_settings.display_color_index].red * (brightness / 255.0f); -+ uint8_t g = colors[rgb_settings.display_color_index].green * (brightness / 255.0f); -+ uint8_t b = colors[rgb_settings.display_color_index].blue * (brightness / 255.0f); -+ -+ SK6805_set_led_color(i, r, g, b); -+ } -+ } -+ } -+ -+ SK6805_update(); -+} -diff --git a/applications/settings/notification_settings/rgb_backlight.h b/applications/settings/notification_settings/rgb_backlight.h -new file mode 100644 -index 0000000..f215ed3 ---- /dev/null -+++ b/applications/settings/notification_settings/rgb_backlight.h -@@ -0,0 +1,91 @@ -+/* -+ RGB backlight FlipperZero driver -+ Copyright (C) 2022-2023 Victor Nikitchuk (https://github.com/quen0n) -+ -+ This program is free software: you can redistribute it and/or modify -+ it under the terms of the GNU General Public License as published by -+ the Free Software Foundation, either version 3 of the License, or -+ (at your option) any later version. -+ -+ This program is distributed in the hope that it will be useful, -+ but WITHOUT ANY WARRANTY; without even the implied warranty of -+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -+ GNU General Public License for more details. -+ -+ You should have received a copy of the GNU General Public License -+ along with this program. If not, see . -+*/ -+ -+#include -+#include "SK6805.h" -+ -+typedef struct { -+ char* name; -+ uint8_t red; -+ uint8_t green; -+ uint8_t blue; -+} RGBBacklightColor; -+ -+typedef struct { -+ uint8_t version; -+ uint8_t display_color_index; -+ uint8_t custom_r; -+ uint8_t custom_g; -+ uint8_t custom_b; -+ bool settings_is_loaded; -+} RGBBacklightSettings; -+ -+/** -+ * @brief Получить текущие настройки RGB-подсветки -+ * -+ * @return Указатель на структуру настроек -+ */ -+RGBBacklightSettings* rgb_backlight_get_settings(void); -+ -+/** -+ * @brief Загрузить настройки подсветки с SD-карты -+ */ -+void rgb_backlight_load_settings(void); -+ -+/** -+ * @brief Сохранить текущие настройки RGB-подсветки -+ */ -+void rgb_backlight_save_settings(void); -+ -+/** -+ * @brief Применить текущие настройки RGB-подсветки -+ * -+ * @param brightness Яркость свечения (0-255) -+ * @param bypass Применить настройки принудительно -+ */ -+void rgb_backlight_update(uint8_t brightness, bool bypass); -+ -+/** -+ * @brief Установить цвет RGB-подсветки -+ * -+ * @param color_index Индекс цвета (0 - rgb_backlight_get_color_count()) -+ */ -+void rgb_backlight_set_color(uint8_t color_index); -+ -+/** -+ * @brief Set custom color values by index - 0=R 1=G 2=B -+ * -+ * @param color - color value (0-255) -+ * @param index - color index (0-2) 0=R 1=G 2=B -+ */ -+void rgb_backlight_set_custom_color(uint8_t color, uint8_t index); -+ -+/** -+ * @brief Получить количество доступных цветов -+ * -+ * @return Число доступных вариантов цвета -+ */ -+uint8_t rgb_backlight_get_color_count(void); -+ -+/** -+ * @brief Получить текстовое название цвета -+ * -+ * @param index Индекс из доступных вариантов цвета -+ * @return Указатель на строку с названием цвета -+ */ -+const char* rgb_backlight_get_color_text(uint8_t index); -diff --git a/lib/drivers/SK6805.c b/lib/drivers/SK6805.c -new file mode 100644 -index 0000000..b89f82a ---- /dev/null -+++ b/lib/drivers/SK6805.c -@@ -0,0 +1,103 @@ -+/* -+ SK6805 FlipperZero driver -+ Copyright (C) 2022-2023 Victor Nikitchuk (https://github.com/quen0n) -+ -+ This program is free software: you can redistribute it and/or modify -+ it under the terms of the GNU General Public License as published by -+ the Free Software Foundation, either version 3 of the License, or -+ (at your option) any later version. -+ -+ This program is distributed in the hope that it will be useful, -+ but WITHOUT ANY WARRANTY; without even the implied warranty of -+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -+ GNU General Public License for more details. -+ -+ You should have received a copy of the GNU General Public License -+ along with this program. If not, see . -+*/ -+ -+#include "SK6805.h" -+#include -+ -+/* Настройки */ -+#define SK6805_LED_COUNT 3 //Количество светодиодов на плате подсветки -+#define SK6805_LED_PIN &led_pin //Порт подключения светодиодов -+ -+#ifdef FURI_DEBUG -+#define DEBUG_PIN &gpio_ext_pa7 -+#define DEBUG_INIT() \ -+ furi_hal_gpio_init(DEBUG_PIN, GpioModeOutputPushPull, GpioPullNo, GpioSpeedVeryHigh) -+#define DEBUG_SET_HIGH() furi_hal_gpio_write(DEBUG_PIN, true) -+#define DEBUG_SET_LOW() furi_hal_gpio_write(DEBUG_PIN, false) -+#else -+#define DEBUG_INIT() -+#define DEBUG_SET_HIGH() -+#define DEBUG_SET_LOW() -+#endif -+ -+static const GpioPin led_pin = {.port = GPIOA, .pin = LL_GPIO_PIN_8}; -+static uint8_t led_buffer[SK6805_LED_COUNT][3]; -+ -+void SK6805_init(void) { -+ DEBUG_INIT(); -+ furi_hal_gpio_write(SK6805_LED_PIN, false); -+ furi_hal_gpio_init(SK6805_LED_PIN, GpioModeOutputPushPull, GpioPullNo, GpioSpeedVeryHigh); -+} -+ -+uint8_t SK6805_get_led_count(void) { -+ return (const uint8_t)SK6805_LED_COUNT; -+} -+void SK6805_set_led_color(uint8_t led_index, uint8_t r, uint8_t g, uint8_t b) { -+ furi_check(led_index < SK6805_LED_COUNT); -+ -+ led_buffer[led_index][0] = g; -+ led_buffer[led_index][1] = r; -+ led_buffer[led_index][2] = b; -+} -+ -+void SK6805_update(void) { -+ SK6805_init(); -+ FURI_CRITICAL_ENTER(); -+ furi_delay_us(150); -+ uint32_t end; -+ /* Последовательная отправка цветов светодиодов */ -+ for(uint8_t lednumber = 0; lednumber < SK6805_LED_COUNT; lednumber++) { -+ //Последовательная отправка цветов светодиода -+ for(uint8_t color = 0; color < 3; color++) { -+ //Последовательная отправка битов цвета -+ uint8_t i = 0b10000000; -+ while(i != 0) { -+ if(led_buffer[lednumber][color] & (i)) { -+ furi_hal_gpio_write(SK6805_LED_PIN, true); -+ DEBUG_SET_HIGH(); -+ end = DWT->CYCCNT + 30; -+ //T1H 600 us (615 us) -+ while(DWT->CYCCNT < end) { -+ } -+ furi_hal_gpio_write(SK6805_LED_PIN, false); -+ DEBUG_SET_LOW(); -+ end = DWT->CYCCNT + 26; -+ //T1L 600 us (587 us) -+ while(DWT->CYCCNT < end) { -+ } -+ } else { -+ furi_hal_gpio_write(SK6805_LED_PIN, true); -+ DEBUG_SET_HIGH(); -+ end = DWT->CYCCNT + 11; -+ //T0H 300 ns (312 ns) -+ while(DWT->CYCCNT < end) { -+ } -+ furi_hal_gpio_write(SK6805_LED_PIN, false); -+ DEBUG_SET_LOW(); -+ end = DWT->CYCCNT + 43; -+ //T0L 900 ns (890 ns) -+ while(DWT->CYCCNT < end) { -+ } -+ } -+ i >>= 1; -+ } -+ } -+ } -+ furi_delay_us(150); -+ FURI_CRITICAL_EXIT(); -+} -diff --git a/lib/drivers/SK6805.h b/lib/drivers/SK6805.h -new file mode 100644 -index 0000000..c97054f ---- /dev/null -+++ b/lib/drivers/SK6805.h -@@ -0,0 +1,51 @@ -+/* -+ SK6805 FlipperZero driver -+ Copyright (C) 2022-2023 Victor Nikitchuk (https://github.com/quen0n) -+ -+ This program is free software: you can redistribute it and/or modify -+ it under the terms of the GNU General Public License as published by -+ the Free Software Foundation, either version 3 of the License, or -+ (at your option) any later version. -+ -+ This program is distributed in the hope that it will be useful, -+ but WITHOUT ANY WARRANTY; without even the implied warranty of -+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -+ GNU General Public License for more details. -+ -+ You should have received a copy of the GNU General Public License -+ along with this program. If not, see . -+*/ -+ -+#ifndef SK6805_H_ -+#define SK6805_H_ -+ -+#include -+ -+/** -+ * @brief Инициализация линии управления подсветкой -+ */ -+void SK6805_init(void); -+ -+/** -+ * @brief Получить количество светодиодов в подсветке -+ * -+ * @return Количество светодиодов -+ */ -+uint8_t SK6805_get_led_count(void); -+ -+/** -+ * @brief Установить цвет свечения светодиода -+ * -+ * @param led_index номер светодиода (от 0 до SK6805_get_led_count()) -+ * @param r значение красного (0-255) -+ * @param g значение зелёного (0-255) -+ * @param b значение синего (0-255) -+ */ -+void SK6805_set_led_color(uint8_t led_index, uint8_t r, uint8_t g, uint8_t b); -+ -+/** -+ * @brief Обновление состояния подсветки дисплея -+ */ -+void SK6805_update(void); -+ -+#endif /* SK6805_H_ */ -diff --git a/targets/f7/furi_hal/furi_hal_light.c b/targets/f7/furi_hal/furi_hal_light.c -index 621478d..ef15153 100644 ---- a/targets/f7/furi_hal/furi_hal_light.c -+++ b/targets/f7/furi_hal/furi_hal_light.c -@@ -3,6 +3,7 @@ - #include - #include - #include -+#include - - #define LED_CURRENT_RED (50u) - #define LED_CURRENT_GREEN (50u) -@@ -31,22 +32,21 @@ void furi_hal_light_init(void) { - } - - void furi_hal_light_set(Light light, uint8_t value) { -- furi_hal_i2c_acquire(&furi_hal_i2c_handle_power); -- if(light & LightRed) { -- lp5562_set_channel_value(&furi_hal_i2c_handle_power, LP5562ChannelRed, value); -- } -- if(light & LightGreen) { -- lp5562_set_channel_value(&furi_hal_i2c_handle_power, LP5562ChannelGreen, value); -- } -- if(light & LightBlue) { -- lp5562_set_channel_value(&furi_hal_i2c_handle_power, LP5562ChannelBlue, value); -- } - if(light & LightBacklight) { -- uint8_t prev = lp5562_get_channel_value(&furi_hal_i2c_handle_power, LP5562ChannelWhite); -- lp5562_execute_ramp( -- &furi_hal_i2c_handle_power, LP5562Engine1, LP5562ChannelWhite, prev, value, 100); -+ rgb_backlight_update(value, false); -+ } else { -+ furi_hal_i2c_acquire(&furi_hal_i2c_handle_power); -+ if(light & LightRed) { -+ lp5562_set_channel_value(&furi_hal_i2c_handle_power, LP5562ChannelRed, value); -+ } -+ if(light & LightGreen) { -+ lp5562_set_channel_value(&furi_hal_i2c_handle_power, LP5562ChannelGreen, value); -+ } -+ if(light & LightBlue) { -+ lp5562_set_channel_value(&furi_hal_i2c_handle_power, LP5562ChannelBlue, value); -+ } -+ furi_hal_i2c_release(&furi_hal_i2c_handle_power); - } -- furi_hal_i2c_release(&furi_hal_i2c_handle_power); - } - - void furi_hal_light_blink_start(Light light, uint8_t brightness, uint16_t on_time, uint16_t period) { diff --git a/.drone.yml b/.drone.yml index 008713039..15c66192c 100644 --- a/.drone.yml +++ b/.drone.yml @@ -81,25 +81,6 @@ steps: - mv dist/f7-C/* artifacts-extra-apps/ - ls -laS artifacts-extra-apps - ls -laS artifacts-extra-apps/f7-update-${DRONE_TAG}e - environment: - FBT_TOOLS_CUSTOM_LINK: - from_secret: fbt_link - - - name: "Build with RGB patch" - image: hfdj/fztools - pull: never - commands: - - git apply .ci_files/rgb.patch - - export DIST_SUFFIX=${DRONE_TAG}r - - export WORKFLOW_BRANCH_OR_TAG=release-cfw-rgb - - export FORCE_NO_DIRTY=yes - - export FBT_GIT_SUBMODULE_SHALLOW=1 - - rm -f build/f7-firmware-C/toolbox/version.* - - ./fbt COMPACT=1 DEBUG=0 updater_package - - mkdir artifacts-rgb-patch - - mv dist/f7-C/* artifacts-rgb-patch/ - - ls -laS artifacts-rgb-patch - - ls -laS artifacts-rgb-patch/f7-update-${DRONE_TAG}r - sed -i 's/(version)/'${DRONE_TAG}'/g' CHANGELOG.md - echo '# Install FW via Web Updater:' >> CHANGELOG.md - echo '### [Default](https://lab.flipper.net/?url=https://unleashedflip.com/fw/${DRONE_TAG}/flipper-z-f7-update-'${DRONE_TAG}'.tgz&channel=release-cfw&version='${DRONE_TAG}') > ` `' >> CHANGELOG.md @@ -107,8 +88,6 @@ steps: - echo '### [Extra apps](https://lab.flipper.net/?url=https://unleashedflip.com/fw_extra_apps/flipper-z-f7-update-'${DRONE_TAG}'e.tgz&channel=release-cfw&version='${DRONE_TAG}'e) > `e`' >> CHANGELOG.md - echo '' >> CHANGELOG.md - echo '### [No apps](https://lab.flipper.net/?url=https://unleashedflip.com/fw_extra_apps/flipper-z-f7-update-'${DRONE_TAG}'c.tgz&channel=release-cfw&version='${DRONE_TAG}'c) > `c`' >> CHANGELOG.md - - echo '' >> CHANGELOG.md - - echo '### [RGB patch - only for hardware mod!](https://lab.flipper.net/?url=https://unleashedflip.com/fw_extra_apps/flipper-z-f7-update-'${DRONE_TAG}'r.tgz&channel=release-cfw&version='${DRONE_TAG}'r) > `r`' >> CHANGELOG.md environment: FBT_TOOLS_CUSTOM_LINK: from_secret: fbt_link @@ -117,20 +96,16 @@ steps: image: joshkeegan/zip commands: - cp artifacts-extra-apps/flipper-z-f7-update-${DRONE_TAG}e.tgz . - - cp artifacts-rgb-patch/flipper-z-f7-update-${DRONE_TAG}r.tgz . - cp artifacts-clean/flipper-z-f7-update-${DRONE_TAG}c.tgz . - cp artifacts-default/flipper-z-f7-update-${DRONE_TAG}.tgz . - zip -r artifacts-extra-apps/flipper-z-f7-update-${DRONE_TAG}e.zip artifacts-extra-apps/f7-update-${DRONE_TAG}e - - zip -r artifacts-rgb-patch/flipper-z-f7-update-${DRONE_TAG}r.zip artifacts-rgb-patch/f7-update-${DRONE_TAG}r - zip -r artifacts-clean/flipper-z-f7-update-${DRONE_TAG}c.zip artifacts-clean/f7-update-${DRONE_TAG}c - zip -r artifacts-default/flipper-z-f7-update-${DRONE_TAG}.zip artifacts-default/f7-update-${DRONE_TAG} - tar czpf artifacts-default/flipper-z-any-scripts-${DRONE_TAG}.tgz scripts - rm -rf artifacts-extra-apps/f7-update-${DRONE_TAG} - - rm -rf artifacts-rgb-patch/f7-update-${DRONE_TAG} - rm -rf artifacts-clean/f7-update-${DRONE_TAG} - rm -rf artifacts-default/f7-update-${DRONE_TAG} - ls -laS artifacts-extra-apps - - ls -laS artifacts-rgb-patch - ls -laS artifacts-clean - ls -laS artifacts-default - mv artifacts-default/ ${DRONE_TAG} @@ -172,21 +147,6 @@ steps: from_secret: dep_target_extra source: flipper-z-f7-update-${DRONE_TAG}e.tgz - - name: "Upload rgb patch version to updates srv" - image: appleboy/drone-scp:linux-amd64 - settings: - host: - from_secret: dep_host - username: - from_secret: dep_user - password: - from_secret: dep_passwd - port: - from_secret: dep_port - target: - from_secret: dep_target_extra - source: flipper-z-f7-update-${DRONE_TAG}r.tgz - - name: "Upload clean version to updates srv" image: appleboy/drone-scp:linux-amd64 settings: @@ -215,7 +175,6 @@ steps: - ${DRONE_TAG}/*.tgz - ${DRONE_TAG}/*.zip - artifacts-extra-apps/*.tgz - - artifacts-rgb-patch/*.tgz - artifacts-clean/*.tgz title: ${DRONE_TAG} note: CHANGELOG.md @@ -399,30 +358,10 @@ steps: FBT_TOOLS_CUSTOM_LINK: from_secret: fbt_link - - name: "Build dev with rgb patch" - image: hfdj/fztools - pull: never - commands: - - git apply .ci_files/rgb.patch - - export DIST_SUFFIX=${DRONE_BUILD_NUMBER}r - - export WORKFLOW_BRANCH_OR_TAG=dev-cfw-rgb - - export FORCE_NO_DIRTY=yes - - export FBT_GIT_SUBMODULE_SHALLOW=1 - - rm -f build/f7-firmware-C/toolbox/version.* - - ./fbt COMPACT=1 DEBUG=0 updater_package - - mkdir artifacts-rgb-patch - - mv dist/f7-C/* artifacts-rgb-patch/ - - ls -laS artifacts-rgb-patch - - ls -laS artifacts-rgb-patch/f7-update-${DRONE_BUILD_NUMBER}r - environment: - FBT_TOOLS_CUSTOM_LINK: - from_secret: fbt_link - - name: "Bundle self-update packages" image: joshkeegan/zip commands: - cp artifacts-extra-apps/flipper-z-f7-update-${DRONE_BUILD_NUMBER}e.tgz . - - cp artifacts-rgb-patch/flipper-z-f7-update-${DRONE_BUILD_NUMBER}r.tgz . - cp artifacts-clean/flipper-z-f7-update-${DRONE_BUILD_NUMBER}c.tgz . - cp artifacts-default/flipper-z-f7-update-${DRONE_BUILD_NUMBER}.tgz . - rm -rf artifacts-default/f7-update-${DRONE_BUILD_NUMBER} @@ -481,21 +420,6 @@ steps: from_secret: dep_target_extra source: flipper-z-f7-update-${DRONE_BUILD_NUMBER}e.tgz - - name: "Upload rgb patch version to updates srv" - image: appleboy/drone-scp:linux-amd64 - settings: - host: - from_secret: dep_host - username: - from_secret: dep_user - password: - from_secret: dep_passwd - port: - from_secret: dep_port - target: - from_secret: dep_target_extra - source: flipper-z-f7-update-${DRONE_BUILD_NUMBER}r.tgz - - name: "Upload clean version to updates srv" image: appleboy/drone-scp:linux-amd64 settings: diff --git a/ReadMe.md b/ReadMe.md index 1ead8d451..c40fb71c4 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -28,7 +28,7 @@ Before getting started: - **Review the Official Documentation:** [docs.flipper.net](https://docs.flipper.net) - **Installation Guide & Version Info:** - How to install the firmware by following the [Installation Guide](/documentation/HowToInstall.md) and check the [version information](/CHANGELOG.md#recommended-update-option---web-updater) (`r`, `e`, ` `, `c`) + How to install the firmware by following the [Installation Guide](/documentation/HowToInstall.md) and check the [version information](/CHANGELOG.md#recommended-update-option---web-updater) (`e`, ` `, `c`) - **FAQ:** Find answers to common questions in the [FAQ](/documentation/FAQ.md) diff --git a/documentation/FAQ.md b/documentation/FAQ.md index a180c827d..cae0ba61c 100644 --- a/documentation/FAQ.md +++ b/documentation/FAQ.md @@ -9,15 +9,20 @@ See [this](https://github.com/DarkFlippers/unleashed-firmware/blob/dev/documentation/HowToInstall.md) -## What version should I install? What do the letters `e`, `r`, `c`... mean? +## What version should I install? What do the letters `e`, `c`... mean? Follow this link for [details](https://github.com/DarkFlippers/unleashed-firmware/blob/dev/CHANGELOG.md#recommended-update-option---web-updater). ## I installed Unleashed and now the backlight doesn't work -You’ve installed a version made for custom RGB modded flippers. The version ending in `r` is specifically for `RGB` modded flippers.
+You’ve enabled RGB backlight mod in settings made for custom RGB modded flippers.
Please, do not use that version if your flipper isn’t modded! +Disable in Settings -> Notifications -> RGB mod settings + +Make sure to have System -> Debug = ON before, otherwise first option (is mod installed) will not appear + +If you have RGB backlight mod do the same but enable the mod instead ## What apps (plugins) are included with Unleashed Firmware? From 21e2e9c148bdad581cafe8d8c85cac2bfab4112e Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Sat, 15 Mar 2025 08:14:00 +0300 Subject: [PATCH 125/268] fmt and return settings to original value [ci skip] --- applications/services/notification/notification_app.h | 4 ++-- applications/services/rgb_backlight/rgb_backlight.c | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/applications/services/notification/notification_app.h b/applications/services/notification/notification_app.h index 0209ffa0b..798b01ab6 100644 --- a/applications/services/notification/notification_app.h +++ b/applications/services/notification/notification_app.h @@ -34,7 +34,7 @@ typedef struct { Light light; } NotificationLedLayer; -#define NOTIFICATION_SETTINGS_VERSION 0x03 +#define NOTIFICATION_SETTINGS_VERSION 0x02 #define NOTIFICATION_SETTINGS_PATH INT_PATH(NOTIFICATION_SETTINGS_FILE_NAME) typedef struct { @@ -60,4 +60,4 @@ struct NotificationApp { RGBBacklightApp* rgb_srv; }; -void notification_message_save_settings(NotificationApp* app); \ No newline at end of file +void notification_message_save_settings(NotificationApp* app); diff --git a/applications/services/rgb_backlight/rgb_backlight.c b/applications/services/rgb_backlight/rgb_backlight.c index 5510ea916..4104c20d3 100644 --- a/applications/services/rgb_backlight/rgb_backlight.c +++ b/applications/services/rgb_backlight/rgb_backlight.c @@ -105,7 +105,6 @@ void rainbow_timer_stop(RGBBacklightApp* app) { // if rgb_mod_installed then apply rainbow colors to backlight and start/restart/stop rainbow_timer void rainbow_timer_starter(RGBBacklightApp* app) { - if((app->settings->rainbow_mode > 0) && (app->settings->rgb_mod_installed)) { rainbow_timer_start(app); } else { From 16b8ec5a9c526757e9a8e557328bd269cbe37399 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Sat, 15 Mar 2025 08:37:37 +0300 Subject: [PATCH 126/268] upd changelog --- CHANGELOG.md | 40 +++++++++++++++++++++++----------------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c95c0d38..ff4ea9470 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,24 +1,28 @@ ## Main changes - Current API: 83.0 -* SubGHz: Add ReversRB2 / RB2M Protocol (static 64 bit) full support with add manually (by @xMasterX) -* SubGHz: Fix Hollarm protocol with more verification -* SubGHz: Fix GangQi protocol (by @DoberBit and @mishamyte (who spent 2 weeks on this)) -* SubGHz: Came Atomo button hold simulation with full cycle simulation (to allow proper pairing with receiver) +* SubGHz: Add **ReversRB2 / RB2M Protocol** (static 64 bit) **full support** with add manually (by @xMasterX) +* SubGHz: **Fix Hollarm protocol with more verification** +* SubGHz: **Fix GangQi protocol** (by @DoberBit and @mishamyte ( who spent 2 weeks on this :O )) +* SubGHz: **Came Atomo button hold simulation with full cycle** simulation (to allow proper pairing with receiver) +* System: **Сombining RGB Backlight mod** (by @quen0n) and original backlight support **in one firmware** (+ Rainbow effect (based on @Willy-JL idea)) (PR #877 | by @Dmitry422) - (**To enable RGB Backlight support go into Notifications settings with Debug mode = ON**) * OFW: LFRFID - **EM4305 support** -* OFW: Universal IR signal selection -* OFW: BadUSB: Mouse control +* OFW: **Universal IR signal selection** +* OFW: **BadUSB: Mouse control** * OFW: NFC - Added naming for DESFire cards + fix MF3ICD40 cards unable to be read -* Apps: Add FindMyFlipper to system apps and allow autostart on system boot [app by @MatthewKuKanich](https://github.com/MatthewKuKanich/FindMyFlipper) and autoloader by @Willy-JL - to use app please check how to add keys in [app repo](https://github.com/MatthewKuKanich/FindMyFlipper) +* Apps: Add **FindMyFlipper to system apps and allow autostart** on system boot [app by @MatthewKuKanich](https://github.com/MatthewKuKanich/FindMyFlipper) and autoloader by @Willy-JL - to use app please check how to add keys in [app repo](https://github.com/MatthewKuKanich/FindMyFlipper) * README Update: Enhanced Visuals & Navigation (PR #871 #872 | by @m-xim) * Docs: Update FAQ.md (PR #865 | by @mi-lrn) * Input: Vibro on Button press option (PR #867 | by @Dmitry422) -* Power: Option to limit battery charging (suppress charging on selected charge level) (PR #867 | by @Dmitry422) +* Power: Option to limit battery charging (suppress charging on selected charge level) (PR #867 | by @Dmitry422) (idea and example by @oltenxyz) * Apps: **Check out more Apps updates and fixes by following** [this link](https://github.com/xMasterX/all-the-plugins/commits/dev) ## Other changes +* SubGHz: Various bugfixes and experimental options (rolling counter overflow) (by @xMasterX) * Anims: Disable winter anims * NFC: mfclassic poller fix early key reuse in dictionary attack state machine (by @noproto) +* OFW PR 4149: HID Ble: increased stack and improvements (by @doomwastaken) * OFW PR 4126: Stricter constness for const data (by @hedger) * OFW PR 4017: Alarm improvements: Snooze, timeouts, and dismissing from the locked state (by @Astrrra) +* OFW: fix: flipper detected before it was rebooted * OFW: NFC: FeliCa Protocol Expose Read Block API and Allow Specifying Service * OFW: LFRFID: Fix Detection Conflict Between Securakey and Noralsy Format (by @zinongli) * OFW: Stdio API improvements @@ -78,20 +82,22 @@ and all other great people who supported our project and me (xMasterX), thanks t ## **Recommended update option - Web Updater** -### What `r`, `e`, ` `, `c` means? What I need to download if I don't want to use Web updater? -What build I should download and what this name means - `flipper-z-f7-update-(version)(r / e / c).tgz` ?
+### What `e`, ` `, `c` means? What I need to download if I don't want to use Web updater? +What build I should download and what this name means - `flipper-z-f7-update-(version)(e / c).tgz` ?
`flipper-z` = for Flipper Zero device
`f7` = Hardware version - same for all flipper zero devices
`update` = Update package, contains updater, all assets (plugins, IR libs, etc.), and firmware itself
`(version)` = Firmware version
-| Designation | [Base Apps](https://github.com/xMasterX/all-the-plugins#default-pack) | [Extra Apps](https://github.com/xMasterX/all-the-plugins#extra-pack) | ⚠️RGB mode* | -|-----|:---:|:---:|:---:| -| ` ` | ✅ | | | -| `c` | | | | -| `e` | ✅ | ✅ | | -| `r` | ✅ | ✅ | ⚠️ | +| Designation | [Base Apps](https://github.com/xMasterX/all-the-plugins#default-pack) | [Extra Apps](https://github.com/xMasterX/all-the-plugins#extra-pack) | +|-----|:---:|:---:| +| ` ` | ✅ | | +| `c` | | | +| `e` | ✅ | ✅ | + +**To enable RGB Backlight support go into Notifications settings with Debug mode = ON** + +⚠️RGB backlight [hardware mod](https://github.com/quen0n/flipperzero-firmware-rgb#readme), works only on modded flippers! do not enable on non modded device! -⚠️This is [hardware mod](https://github.com/quen0n/flipperzero-firmware-rgb#readme), works only on modded flippers! do not install on non modded device! Firmware Self-update package (update from microSD) - `flipper-z-f7-update-(version).tgz` for mobile app / qFlipper / web
Archive of `scripts` folder (contains scripts for FW/plugins development) - `flipper-z-any-scripts-(version).tgz`
From c04ce78fcd8e815f52c48c69551be355037c056f Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Sat, 15 Mar 2025 09:02:21 +0300 Subject: [PATCH 127/268] fix messages [ci skip] --- .ci_files/devbuild_msg_discord.txt | 2 +- .ci_files/devbuild_msg_telegram.txt | 2 +- .ci_files/release_msg_discord.txt | 2 +- .ci_files/release_msg_telegram.txt | 2 +- CHANGELOG.md | 8 ++++---- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.ci_files/devbuild_msg_discord.txt b/.ci_files/devbuild_msg_discord.txt index 0bc802364..41a70e45e 100644 --- a/.ci_files/devbuild_msg_discord.txt +++ b/.ci_files/devbuild_msg_discord.txt @@ -9,7 +9,7 @@ How to [install firmware](https://github.com/DarkFlippers/unleashed-firmware/blo [Default](https://lab.flipper.net/?url=https://unleashedflip.com/fw/dev/flipper-z-f7-update-(buildnum).tgz&channel=dev-cfw&version=(buildnum)) > ` ` [Extra apps](https://lab.flipper.net/?url=https://unleashedflip.com/fw_extra_apps/flipper-z-f7-update-(buildnum)e.tgz&channel=dev-cfw&version=(buildnum)e) > `e` [No apps](https://lab.flipper.net/?url=https://unleashedflip.com/fw_extra_apps/flipper-z-f7-update-(buildnum)c.tgz&channel=dev-cfw&version=(buildnum)c) > `c` -What ` `, `e`, `c` means? -> [versions info](https://github.com/DarkFlippers/unleashed-firmware/blob/dev/CHANGELOG.md#what-n-r-e---c-means-what-i-need-to-download-if-i-dont-want-to-use-web-updater) +What ` `, `e`, `c` means? -> [versions info](https://github.com/DarkFlippers/unleashed-firmware/blob/dev/CHANGELOG.md#what-e---c-means-what-i-need-to-download-if-i-dont-want-to-use-web-updater) ### Direct tgz download links: [Default](https://unleashedflip.com/fw/dev/flipper-z-f7-update-(buildnum).tgz) > ` ` - [Extra apps](https://unleashedflip.com/fw_extra_apps/flipper-z-f7-update-(buildnum)e.tgz) > `e` - [No apps](https://unleashedflip.com/fw_extra_apps/flipper-z-f7-update-(buildnum)c.tgz) > `c` diff --git a/.ci_files/devbuild_msg_telegram.txt b/.ci_files/devbuild_msg_telegram.txt index 5a3051471..13d0545dd 100644 --- a/.ci_files/devbuild_msg_telegram.txt +++ b/.ci_files/devbuild_msg_telegram.txt @@ -11,7 +11,7 @@ How to [install firmware](https://github.com/DarkFlippers/unleashed-firmware/blo [Extra apps](https://lab.flipper.net/?url=https://unleashedflip.com/fw_extra_apps/flipper-z-f7-update-(buildnum)e.tgz&channel=dev-cfw&version=(buildnum)e) > `e` [No apps](https://lab.flipper.net/?url=https://unleashedflip.com/fw_extra_apps/flipper-z-f7-update-(buildnum)c.tgz&channel=dev-cfw&version=(buildnum)c) > `c` -What ` `, `e`, `c` means? -> [versions info](https://github.com/DarkFlippers/unleashed-firmware/blob/dev/CHANGELOG.md#what-n-r-e---c-means-what-i-need-to-download-if-i-dont-want-to-use-web-updater) +What ` `, `e`, `c` means? -> [versions info](https://github.com/DarkFlippers/unleashed-firmware/blob/dev/CHANGELOG.md#what-e---c-means-what-i-need-to-download-if-i-dont-want-to-use-web-updater) **Direct tgz download links:** [Default](https://unleashedflip.com/fw/dev/flipper-z-f7-update-(buildnum).tgz) > ` ` diff --git a/.ci_files/release_msg_discord.txt b/.ci_files/release_msg_discord.txt index e25e0d95b..8eadaaf1f 100644 --- a/.ci_files/release_msg_discord.txt +++ b/.ci_files/release_msg_discord.txt @@ -9,7 +9,7 @@ How to [install firmware](https://github.com/DarkFlippers/unleashed-firmware/blo [Default](https://lab.flipper.net/?url=https://unleashedflip.com/fw/(releasever)/flipper-z-f7-update-(releasever).tgz&channel=release-cfw&version=(releasever)) > ` ` [Extra apps](https://lab.flipper.net/?url=https://unleashedflip.com/fw_extra_apps/flipper-z-f7-update-(releasever)e.tgz&channel=release-cfw&version=(releasever)e) > `e` [No apps](https://lab.flipper.net/?url=https://unleashedflip.com/fw_extra_apps/flipper-z-f7-update-(releasever)c.tgz&channel=release-cfw&version=(releasever)c) > `c` -What ` `, `e`, `c` means? -> [versions info](https://github.com/DarkFlippers/unleashed-firmware/blob/dev/CHANGELOG.md#what-n-r-e---c-means-what-i-need-to-download-if-i-dont-want-to-use-web-updater) +What ` `, `e`, `c` means? -> [versions info](https://github.com/DarkFlippers/unleashed-firmware/blob/dev/CHANGELOG.md#what-e---c-means-what-i-need-to-download-if-i-dont-want-to-use-web-updater) ### Direct tgz download links: [Default](https://unleashedflip.com/fw/(releasever)/flipper-z-f7-update-(releasever).tgz) > ` ` - [Extra apps](https://unleashedflip.com/fw_extra_apps/flipper-z-f7-update-(releasever)e.tgz) > `e` - [No apps](https://unleashedflip.com/fw_extra_apps/flipper-z-f7-update-(releasever)c.tgz) > `c` diff --git a/.ci_files/release_msg_telegram.txt b/.ci_files/release_msg_telegram.txt index fd8c13bcf..6d527970f 100644 --- a/.ci_files/release_msg_telegram.txt +++ b/.ci_files/release_msg_telegram.txt @@ -11,7 +11,7 @@ How to [install firmware](https://github.com/DarkFlippers/unleashed-firmware/blo [Extra apps](https://lab.flipper.net/?url=https://unleashedflip.com/fw_extra_apps/flipper-z-f7-update-(releasever)e.tgz&channel=release-cfw&version=(releasever)e) > `e` [No apps](https://lab.flipper.net/?url=https://unleashedflip.com/fw_extra_apps/flipper-z-f7-update-(releasever)c.tgz&channel=release-cfw&version=(releasever)c) > `c` -What ` `, `e`, `c` means? -> [versions info](https://github.com/DarkFlippers/unleashed-firmware/blob/dev/CHANGELOG.md#what-n-r-e---c-means-what-i-need-to-download-if-i-dont-want-to-use-web-updater) +What ` `, `e`, `c` means? -> [versions info](https://github.com/DarkFlippers/unleashed-firmware/blob/dev/CHANGELOG.md#what-e---c-means-what-i-need-to-download-if-i-dont-want-to-use-web-updater) **Direct tgz download links:** [Default](https://unleashedflip.com/fw/(releasever)/flipper-z-f7-update-(releasever).tgz) > ` ` diff --git a/CHANGELOG.md b/CHANGELOG.md index ff4ea9470..34e119d3e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,10 @@ ## Main changes -- Current API: 83.0 -* SubGHz: Add **ReversRB2 / RB2M Protocol** (static 64 bit) **full support** with add manually (by @xMasterX) +- Current API: 83.1 +* SubGHz: Add **Revers RB2 / RB2M Protocol** (static 64 bit) **full support** with add manually (by @xMasterX) * SubGHz: **Fix Hollarm protocol with more verification** -* SubGHz: **Fix GangQi protocol** (by @DoberBit and @mishamyte ( who spent 2 weeks on this :O )) +* SubGHz: **Fix GangQi protocol** (by @DoberBit and @mishamyte (who spent 2 weeks on this)) * SubGHz: **Came Atomo button hold simulation with full cycle** simulation (to allow proper pairing with receiver) -* System: **Сombining RGB Backlight mod** (by @quen0n) and original backlight support **in one firmware** (+ Rainbow effect (based on @Willy-JL idea)) (PR #877 | by @Dmitry422) - (**To enable RGB Backlight support go into Notifications settings with Debug mode = ON**) +* System: **Сombining RGB Backlight mod** (by @quen0n) and original backlight support **in one firmware** (+ Rainbow effect (based on @Willy-JL idea)) (PR #877 | by @Dmitry422) - (**To enable RGB Backlight support go into Notifications settings with Debug mode - ON**) * OFW: LFRFID - **EM4305 support** * OFW: **Universal IR signal selection** * OFW: **BadUSB: Mouse control** From 94b369657d8c411c7e101312e824c8012f023d66 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Sat, 15 Mar 2025 09:32:19 +0300 Subject: [PATCH 128/268] upd changelog and add fix --- CHANGELOG.md | 1 + applications/main/infrared/infrared_brute_force.c | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 34e119d3e..adb257d38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ * SubGHz: Various bugfixes and experimental options (rolling counter overflow) (by @xMasterX) * Anims: Disable winter anims * NFC: mfclassic poller fix early key reuse in dictionary attack state machine (by @noproto) +* OFW PR 4132: Infrared: Fix universals sending (by @Willy-JL) * OFW PR 4149: HID Ble: increased stack and improvements (by @doomwastaken) * OFW PR 4126: Stricter constness for const data (by @hedger) * OFW PR 4017: Alarm improvements: Snooze, timeouts, and dismissing from the locked state (by @Astrrra) diff --git a/applications/main/infrared/infrared_brute_force.c b/applications/main/infrared/infrared_brute_force.c index 636635894..1ec4645e9 100644 --- a/applications/main/infrared/infrared_brute_force.c +++ b/applications/main/infrared/infrared_brute_force.c @@ -104,9 +104,9 @@ InfraredErrorCode infrared_brute_force_calculate_messages(InfraredBruteForce* br break; } - size_t signal_start = flipper_format_tell(ff); bool signal_valid = false; while(infrared_signal_read_name(ff, signal_name) == InfraredErrorCodeNone) { + size_t signal_start = flipper_format_tell(ff); error = infrared_signal_read_body(signal, ff); signal_valid = (!INFRARED_ERROR_PRESENT(error)) && infrared_signal_is_valid(signal); if(!signal_valid) break; From edd75a9b01d2cc7fd06328c0eb4dabc0087c1407 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Sun, 16 Mar 2025 04:21:02 +0300 Subject: [PATCH 129/268] init rgb only once on boot if mod is not present testing required!!! --- applications/services/application.fam | 1 + applications/services/region/application.fam | 10 -- applications/services/region/region.c | 140 ------------------ .../services/rgb_backlight/application.fam | 12 +- .../services/rgb_backlight/rgb_backlight.c | 34 +++-- .../rgb_backlight/rgb_backlight_on_boot.c | 56 +++++++ 6 files changed, 88 insertions(+), 165 deletions(-) delete mode 100644 applications/services/region/application.fam delete mode 100644 applications/services/region/region.c create mode 100644 applications/services/rgb_backlight/rgb_backlight_on_boot.c diff --git a/applications/services/application.fam b/applications/services/application.fam index 8cfb22cdb..f23b905e5 100644 --- a/applications/services/application.fam +++ b/applications/services/application.fam @@ -12,5 +12,6 @@ App( "power", "namechanger_srv", "rgb_backlight", + "rgb_backlight_startup", ], ) diff --git a/applications/services/region/application.fam b/applications/services/region/application.fam deleted file mode 100644 index a4cdc94ea..000000000 --- a/applications/services/region/application.fam +++ /dev/null @@ -1,10 +0,0 @@ -App( - appid="region", - name="RegionSrv", - apptype=FlipperAppType.STARTUP, - targets=["f7"], - entry_point="region_on_system_start", - cdefines=["SRV_REGION"], - requires=["storage"], - order=170, -) diff --git a/applications/services/region/region.c b/applications/services/region/region.c deleted file mode 100644 index bed676f9b..000000000 --- a/applications/services/region/region.c +++ /dev/null @@ -1,140 +0,0 @@ -#include - -#include -#include - -#include -#include - -#define TAG "RegionSrv" - -#define SUBGHZ_REGION_FILENAME INT_PATH(".region_data") - -static bool region_istream_read(pb_istream_t* istream, pb_byte_t* buf, size_t count) { - File* file = istream->state; - size_t ret = storage_file_read(file, buf, count); - return count == ret; -} - -static bool region_istream_decode_band(pb_istream_t* stream, const pb_field_t* field, void** arg) { - UNUSED(field); - - FuriHalRegion* region = *arg; - - PB_Region_Band band = {0}; - if(!pb_decode(stream, PB_Region_Band_fields, &band)) { - FURI_LOG_E(TAG, "PB Region band decode error: %s", PB_GET_ERROR(stream)); - return false; - } - - region->bands_count += 1; - region = realloc( //-V701 - region, - sizeof(FuriHalRegion) + sizeof(FuriHalRegionBand) * region->bands_count); - size_t pos = region->bands_count - 1; - region->bands[pos].start = band.start; - region->bands[pos].end = band.end; - region->bands[pos].power_limit = band.power_limit; - region->bands[pos].duty_cycle = band.duty_cycle; - *arg = region; - - FURI_LOG_I( - TAG, - "Add allowed band: start %luHz, stop %luHz, power_limit %ddBm, duty_cycle %u%%", - band.start, - band.end, - band.power_limit, - band.duty_cycle); - return true; -} - -static int32_t region_load_file(void* context) { - UNUSED(context); - - Storage* storage = furi_record_open(RECORD_STORAGE); - File* file = storage_file_alloc(storage); - - PB_Region pb_region = {0}; - pb_region.bands.funcs.decode = region_istream_decode_band; - - do { - FileInfo fileinfo = {0}; - - if(storage_common_stat(storage, SUBGHZ_REGION_FILENAME, &fileinfo) != FSE_OK || - fileinfo.size == 0) { - FURI_LOG_W(TAG, "Region file missing or empty"); - break; - - } else if(!storage_file_open(file, SUBGHZ_REGION_FILENAME, FSAM_READ, FSOM_OPEN_EXISTING)) { - FURI_LOG_E(TAG, "Failed to open region file"); - break; - } - - pb_istream_t istream = { - .callback = region_istream_read, - .state = file, - .errmsg = NULL, - .bytes_left = fileinfo.size, - }; - - pb_region.bands.arg = malloc(sizeof(FuriHalRegion)); - - if(!pb_decode(&istream, PB_Region_fields, &pb_region)) { - FURI_LOG_E(TAG, "Failed to decode region file"); - free(pb_region.bands.arg); - break; - } - - FuriHalRegion* region = pb_region.bands.arg; - - memcpy( - region->country_code, - pb_region.country_code->bytes, - MIN(pb_region.country_code->size, sizeof(region->country_code) - 1)); - - furi_hal_region_set(region); - - FURI_LOG_I(TAG, "Dynamic region set: %s", region->country_code); - } while(0); - - pb_release(PB_Region_fields, &pb_region); - storage_file_free(file); - furi_record_close(RECORD_STORAGE); - - return 0; -} - -static void - region_loader_release_callback(FuriThread* thread, FuriThreadState state, void* context) { - UNUSED(context); - - if(state == FuriThreadStateStopped) { - furi_thread_free(thread); - } -} - -static void region_storage_callback(const void* message, void* context) { - UNUSED(context); - const StorageEvent* event = message; - - if(event->type == StorageEventTypeCardMount) { - FuriThread* loader = furi_thread_alloc_ex(NULL, 2048, region_load_file, NULL); - furi_thread_set_state_callback(loader, region_loader_release_callback); - furi_thread_start(loader); - } -} - -int32_t region_on_system_start(void* p) { - UNUSED(p); - - Storage* storage = furi_record_open(RECORD_STORAGE); - furi_pubsub_subscribe(storage_get_pubsub(storage), region_storage_callback, NULL); - - if(storage_sd_status(storage) != FSE_OK) { - FURI_LOG_D(TAG, "SD Card not ready, skipping dynamic region"); - return 0; - } - - region_load_file(NULL); - return 0; -} diff --git a/applications/services/rgb_backlight/application.fam b/applications/services/rgb_backlight/application.fam index 5e05233db..95da20f61 100644 --- a/applications/services/rgb_backlight/application.fam +++ b/applications/services/rgb_backlight/application.fam @@ -7,4 +7,14 @@ App( stack_size=1 * 1024, order=95, sdk_headers=["rgb_backlight.h"], -) \ No newline at end of file +) + +App( + appid="rgb_backlight_startup", + name="RgbBackLightBootSrv", + apptype=FlipperAppType.STARTUP, + targets=["f7"], + entry_point="rgb_backlight_on_system_start", + cdefines=["SRV_RGB_BACKLIGHT"], + order=270, +) diff --git a/applications/services/rgb_backlight/rgb_backlight.c b/applications/services/rgb_backlight/rgb_backlight.c index 4104c20d3..d74c54a02 100644 --- a/applications/services/rgb_backlight/rgb_backlight.c +++ b/applications/services/rgb_backlight/rgb_backlight.c @@ -82,14 +82,15 @@ void rgb_backlight_set_custom_color(uint8_t red, uint8_t green, uint8_t blue) { // use RECORD for acces to rgb service instance, use current_* colors and update backlight void rgb_backlight_update(float brightness) { RGBBacklightApp* app = furi_record_open(RECORD_RGB_BACKLIGHT); - - for(uint8_t i = 0; i < SK6805_get_led_count(); i++) { - uint8_t r = app->current_red * (brightness / 1.0f); - uint8_t g = app->current_green * (brightness / 1.0f); - uint8_t b = app->current_blue * (brightness / 1.0f); - SK6805_set_led_color(i, r, g, b); + if(app->settings->rgb_mod_installed) { + for(uint8_t i = 0; i < SK6805_get_led_count(); i++) { + uint8_t r = app->current_red * (brightness / 1.0f); + uint8_t g = app->current_green * (brightness / 1.0f); + uint8_t b = app->current_blue * (brightness / 1.0f); + SK6805_set_led_color(i, r, g, b); + } + SK6805_update(); } - SK6805_update(); furi_record_close(RECORD_RGB_BACKLIGHT); } @@ -171,8 +172,8 @@ static void rainbow_timer_callback(void* context) { default: break; } + rgb_backlight_update(app->settings->brightness); } - rgb_backlight_update(app->settings->brightness); // if rainbow_mode is ..... do another effect // if(app->settings.rainbow_mode == ...) { @@ -185,7 +186,6 @@ int32_t rgb_backlight_srv(void* p) { // Define object app (full app with settings and running variables), // allocate memory and create RECORD for access to app structure from outside RGBBacklightApp* app = malloc(sizeof(RGBBacklightApp)); - furi_record_create(RECORD_RGB_BACKLIGHT, app); //define rainbow_timer and they callback app->rainbow_timer = furi_timer_alloc(rainbow_timer_callback, FuriTimerTypePeriodic, app); @@ -197,6 +197,8 @@ int32_t rgb_backlight_srv(void* p) { // Init app variables app->rainbow_stage = 1; + furi_record_create(RECORD_RGB_BACKLIGHT, app); + // if rgb mod installed - start rainbow or set static color from settings (default index = 0) if(app->settings->rgb_mod_installed) { if(app->settings->rainbow_mode > 0) { @@ -206,15 +208,19 @@ int32_t rgb_backlight_srv(void* p) { rgb_backlight_update(app->settings->brightness); } // if rgb mod not installed - set default static orange color (index=0) - } else { - rgb_backlight_set_static_color(0); - rgb_backlight_update(app->settings->brightness); - } + } //else { + // rgb_backlight_set_static_color(0); + // rgb_backlight_update(app->settings->brightness); + //} while(1) { // place for message queue and other future options furi_delay_ms(5000); - FURI_LOG_I(TAG, "Service is running"); + if(app->settings->rgb_mod_installed) { + FURI_LOG_D(TAG, "Mod is enabled - serivce is running"); + } else { + FURI_LOG_D(TAG, "Mod is DISABLED - serivce is running"); + } } return 0; } diff --git a/applications/services/rgb_backlight/rgb_backlight_on_boot.c b/applications/services/rgb_backlight/rgb_backlight_on_boot.c new file mode 100644 index 000000000..8a55426ca --- /dev/null +++ b/applications/services/rgb_backlight/rgb_backlight_on_boot.c @@ -0,0 +1,56 @@ + +#include +#include +#include "applications/services/rgb_backlight/rgb_backlight.h" + +static int32_t boot_rgb_backlight_update(void* context) { + UNUSED(context); + RGBBacklightApp* app = furi_record_open(RECORD_RGB_BACKLIGHT); + if(!app->settings->rgb_mod_installed) { + rgb_backlight_set_static_color(0); + for(uint8_t i = 0; i < SK6805_get_led_count(); i++) { + uint8_t r = app->current_red * (1.0f / 1.0f); + uint8_t g = app->current_green * (1.0f / 1.0f); + uint8_t b = app->current_blue * (1.0f / 1.0f); + SK6805_set_led_color(i, r, g, b); + } + SK6805_update(); + } + furi_record_close(RECORD_RGB_BACKLIGHT); + return 0; +} + +static void + rgb_boot_loader_release_callback(FuriThread* thread, FuriThreadState state, void* context) { + UNUSED(context); + + if(state == FuriThreadStateStopped) { + furi_thread_free(thread); + } +} + +static void rgb_boot_storage_callback(const void* message, void* context) { + UNUSED(context); + const StorageEvent* event = message; + + if(event->type == StorageEventTypeCardMount) { + FuriThread* loader = furi_thread_alloc_ex(NULL, 2048, boot_rgb_backlight_update, NULL); + furi_thread_set_state_callback(loader, rgb_boot_loader_release_callback); + furi_thread_start(loader); + } +} + +int32_t rgb_backlight_on_system_start(void* p) { + UNUSED(p); + + Storage* storage = furi_record_open(RECORD_STORAGE); + furi_pubsub_subscribe(storage_get_pubsub(storage), rgb_boot_storage_callback, NULL); + + if(storage_sd_status(storage) != FSE_OK) { + FURI_LOG_D("RGB_Boot_Init", "SD Card not ready, skipping rgb backlight init"); + return 0; + } + + boot_rgb_backlight_update(NULL); + return 0; +} From b48e00928a9593ed16f89d1c84472d1d5679cbcb Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Sun, 16 Mar 2025 05:24:20 +0300 Subject: [PATCH 130/268] remove overkill service --- applications/services/application.fam | 1 - .../services/rgb_backlight/application.fam | 10 ---- .../services/rgb_backlight/rgb_backlight.c | 16 ++++-- .../rgb_backlight/rgb_backlight_on_boot.c | 56 ------------------- 4 files changed, 12 insertions(+), 71 deletions(-) delete mode 100644 applications/services/rgb_backlight/rgb_backlight_on_boot.c diff --git a/applications/services/application.fam b/applications/services/application.fam index f23b905e5..8cfb22cdb 100644 --- a/applications/services/application.fam +++ b/applications/services/application.fam @@ -12,6 +12,5 @@ App( "power", "namechanger_srv", "rgb_backlight", - "rgb_backlight_startup", ], ) diff --git a/applications/services/rgb_backlight/application.fam b/applications/services/rgb_backlight/application.fam index 95da20f61..631ca01d5 100644 --- a/applications/services/rgb_backlight/application.fam +++ b/applications/services/rgb_backlight/application.fam @@ -8,13 +8,3 @@ App( order=95, sdk_headers=["rgb_backlight.h"], ) - -App( - appid="rgb_backlight_startup", - name="RgbBackLightBootSrv", - apptype=FlipperAppType.STARTUP, - targets=["f7"], - entry_point="rgb_backlight_on_system_start", - cdefines=["SRV_RGB_BACKLIGHT"], - order=270, -) diff --git a/applications/services/rgb_backlight/rgb_backlight.c b/applications/services/rgb_backlight/rgb_backlight.c index d74c54a02..03f8f9af0 100644 --- a/applications/services/rgb_backlight/rgb_backlight.c +++ b/applications/services/rgb_backlight/rgb_backlight.c @@ -208,10 +208,18 @@ int32_t rgb_backlight_srv(void* p) { rgb_backlight_update(app->settings->brightness); } // if rgb mod not installed - set default static orange color (index=0) - } //else { - // rgb_backlight_set_static_color(0); - // rgb_backlight_update(app->settings->brightness); - //} + } else { + //rgb_backlight_set_static_color(0); + //rgb_backlight_update(app->settings->brightness); + rgb_backlight_set_static_color(0); + for(uint8_t i = 0; i < SK6805_get_led_count(); i++) { + uint8_t r = app->current_red * (1.0f / 1.0f); + uint8_t g = app->current_green * (1.0f / 1.0f); + uint8_t b = app->current_blue * (1.0f / 1.0f); + SK6805_set_led_color(i, r, g, b); + } + SK6805_update(); + } while(1) { // place for message queue and other future options diff --git a/applications/services/rgb_backlight/rgb_backlight_on_boot.c b/applications/services/rgb_backlight/rgb_backlight_on_boot.c deleted file mode 100644 index 8a55426ca..000000000 --- a/applications/services/rgb_backlight/rgb_backlight_on_boot.c +++ /dev/null @@ -1,56 +0,0 @@ - -#include -#include -#include "applications/services/rgb_backlight/rgb_backlight.h" - -static int32_t boot_rgb_backlight_update(void* context) { - UNUSED(context); - RGBBacklightApp* app = furi_record_open(RECORD_RGB_BACKLIGHT); - if(!app->settings->rgb_mod_installed) { - rgb_backlight_set_static_color(0); - for(uint8_t i = 0; i < SK6805_get_led_count(); i++) { - uint8_t r = app->current_red * (1.0f / 1.0f); - uint8_t g = app->current_green * (1.0f / 1.0f); - uint8_t b = app->current_blue * (1.0f / 1.0f); - SK6805_set_led_color(i, r, g, b); - } - SK6805_update(); - } - furi_record_close(RECORD_RGB_BACKLIGHT); - return 0; -} - -static void - rgb_boot_loader_release_callback(FuriThread* thread, FuriThreadState state, void* context) { - UNUSED(context); - - if(state == FuriThreadStateStopped) { - furi_thread_free(thread); - } -} - -static void rgb_boot_storage_callback(const void* message, void* context) { - UNUSED(context); - const StorageEvent* event = message; - - if(event->type == StorageEventTypeCardMount) { - FuriThread* loader = furi_thread_alloc_ex(NULL, 2048, boot_rgb_backlight_update, NULL); - furi_thread_set_state_callback(loader, rgb_boot_loader_release_callback); - furi_thread_start(loader); - } -} - -int32_t rgb_backlight_on_system_start(void* p) { - UNUSED(p); - - Storage* storage = furi_record_open(RECORD_STORAGE); - furi_pubsub_subscribe(storage_get_pubsub(storage), rgb_boot_storage_callback, NULL); - - if(storage_sd_status(storage) != FSE_OK) { - FURI_LOG_D("RGB_Boot_Init", "SD Card not ready, skipping rgb backlight init"); - return 0; - } - - boot_rgb_backlight_update(NULL); - return 0; -} From 05423d5cb357876b5b89dc519bc5d28c2e5da994 Mon Sep 17 00:00:00 2001 From: Dmitry422 Date: Sun, 16 Mar 2025 22:42:22 +0700 Subject: [PATCH 131/268] Start rgb backlight settings refactoring and adding new options; --- .../services/rgb_backlight/rgb_backlight.c | 36 +-- .../rgb_backlight/rgb_backlight_settings.c | 22 +- .../rgb_backlight/rgb_backlight_settings.h | 14 +- .../notification_settings_app.c | 266 ++++++++++++------ 4 files changed, 226 insertions(+), 112 deletions(-) diff --git a/applications/services/rgb_backlight/rgb_backlight.c b/applications/services/rgb_backlight/rgb_backlight.c index 03f8f9af0..9b25ded5b 100644 --- a/applications/services/rgb_backlight/rgb_backlight.c +++ b/applications/services/rgb_backlight/rgb_backlight.c @@ -42,7 +42,7 @@ static const RGBBacklightColor colors[] = { {"Pink", 255, 0, 127}, {"Red", 255, 0, 0}, {"White", 254, 210, 200}, - {"Custom", 0, 0, 0}, + {"Custom", 255, 255, 255}, }; uint8_t rgb_backlight_get_color_count(void) { @@ -82,7 +82,7 @@ void rgb_backlight_set_custom_color(uint8_t red, uint8_t green, uint8_t blue) { // use RECORD for acces to rgb service instance, use current_* colors and update backlight void rgb_backlight_update(float brightness) { RGBBacklightApp* app = furi_record_open(RECORD_RGB_BACKLIGHT); - if(app->settings->rgb_mod_installed) { + if(app->settings->rgb_backlight_mode > 0) { for(uint8_t i = 0; i < SK6805_get_led_count(); i++) { uint8_t r = app->current_red * (brightness / 1.0f); uint8_t g = app->current_green * (brightness / 1.0f); @@ -104,9 +104,9 @@ void rainbow_timer_stop(RGBBacklightApp* app) { furi_timer_stop(app->rainbow_timer); } -// if rgb_mod_installed then apply rainbow colors to backlight and start/restart/stop rainbow_timer +// if rgb_backlight_installed then apply rainbow colors to backlight and start/restart/stop rainbow_timer void rainbow_timer_starter(RGBBacklightApp* app) { - if((app->settings->rainbow_mode > 0) && (app->settings->rgb_mod_installed)) { + if((app->settings->rainbow_mode > 0) && (app->settings->rgb_backlight_installed)) { rainbow_timer_start(app); } else { if(furi_timer_is_running(app->rainbow_timer)) { @@ -200,22 +200,22 @@ int32_t rgb_backlight_srv(void* p) { furi_record_create(RECORD_RGB_BACKLIGHT, app); // if rgb mod installed - start rainbow or set static color from settings (default index = 0) - if(app->settings->rgb_mod_installed) { - if(app->settings->rainbow_mode > 0) { - rainbow_timer_starter(app); - } else { - rgb_backlight_set_static_color(app->settings->static_color_index); - rgb_backlight_update(app->settings->brightness); - } - // if rgb mod not installed - set default static orange color (index=0) + // + // TODO запуск сохраненного режима + if(app->settings->rgb_backlight_mode > 0) { + // if(app->settings->rainbow_mode > 0) { + // rainbow_timer_starter(app); + // } else { + // rgb_backlight_set_static_color(app->settings->static_color_index); + // rgb_backlight_update(app->settings->brightness); + // } + // if rgb mode = 0 (rgb_backlight not installed) then set default static orange color (index=0) and light on default color } else { - //rgb_backlight_set_static_color(0); - //rgb_backlight_update(app->settings->brightness); rgb_backlight_set_static_color(0); for(uint8_t i = 0; i < SK6805_get_led_count(); i++) { - uint8_t r = app->current_red * (1.0f / 1.0f); - uint8_t g = app->current_green * (1.0f / 1.0f); - uint8_t b = app->current_blue * (1.0f / 1.0f); + uint8_t r = app->current_red * 1.0f; + uint8_t g = app->current_green * 1.0f; + uint8_t b = app->current_blue * 1.0f; SK6805_set_led_color(i, r, g, b); } SK6805_update(); @@ -224,7 +224,7 @@ int32_t rgb_backlight_srv(void* p) { while(1) { // place for message queue and other future options furi_delay_ms(5000); - if(app->settings->rgb_mod_installed) { + if(app->settings->rgb_backlight_mode > 0) { FURI_LOG_D(TAG, "Mod is enabled - serivce is running"); } else { FURI_LOG_D(TAG, "Mod is DISABLED - serivce is running"); diff --git a/applications/services/rgb_backlight/rgb_backlight_settings.c b/applications/services/rgb_backlight/rgb_backlight_settings.c index 7e69eb0dc..7c73ddf19 100644 --- a/applications/services/rgb_backlight/rgb_backlight_settings.c +++ b/applications/services/rgb_backlight/rgb_backlight_settings.c @@ -10,18 +10,27 @@ #define RGB_BACKLIGHT_SETTINGS_MAGIC (0x30) #define RGB_BACKLIGHT_SETTINGS_VER_PREV (0) // Previous version number -#define RGB_BACKLIGHT_SETTINGS_VER (1) // New version number +#define RGB_BACKLIGHT_SETTINGS_VER (1) // Current version number //pervious settings must be copyed from previous rgb_backlight_settings.h file typedef struct { + //Common settings uint8_t version; - bool rgb_mod_installed; + uint8_t rgb_backlight_mode; + float brightness; + //static and custom colors mode settings uint8_t static_color_index; - uint8_t custom_r; - uint8_t custom_g; - uint8_t custom_b; + uint8_t custom_red; + uint8_t custom_green; + uint8_t custom_blue; + // static gradient mode settings + uint8_t static_vd1_index; + uint8_t static_vd2_index; + uint8_t static_vd3_index; + + // rainbow mode setings uint32_t rainbow_mode; uint32_t rainbow_speed_ms; uint16_t rainbow_step; @@ -79,6 +88,9 @@ void rgb_backlight_settings_load(RGBBacklightSettings* settings) { settings->brightness = 1.0f; settings->rainbow_speed_ms = 100; settings->rainbow_step = 1; + settings->custom_red=255; + settings->custom_green = 255; + settings->custom_blue=255; rgb_backlight_settings_save(settings); } } diff --git a/applications/services/rgb_backlight/rgb_backlight_settings.h b/applications/services/rgb_backlight/rgb_backlight_settings.h index 48b0c43b9..478048039 100644 --- a/applications/services/rgb_backlight/rgb_backlight_settings.h +++ b/applications/services/rgb_backlight/rgb_backlight_settings.h @@ -4,15 +4,23 @@ #include typedef struct { + //Common settings uint8_t version; - bool rgb_mod_installed; + uint8_t rgb_backlight_mode; + float brightness; + //static and custom colors mode settings uint8_t static_color_index; uint8_t custom_red; uint8_t custom_green; uint8_t custom_blue; - float brightness; - + + // static gradient mode settings + uint8_t static_vd1_index; + uint8_t static_vd2_index; + uint8_t static_vd3_index; + + // rainbow mode setings uint32_t rainbow_mode; uint32_t rainbow_speed_ms; uint16_t rainbow_step; diff --git a/applications/settings/notification_settings/notification_settings_app.c b/applications/settings/notification_settings/notification_settings_app.c index ff90e96e7..cc316e6aa 100644 --- a/applications/settings/notification_settings/notification_settings_app.c +++ b/applications/settings/notification_settings/notification_settings_app.c @@ -110,30 +110,40 @@ const char* const vibro_text[VIBRO_COUNT] = { const bool vibro_value[VIBRO_COUNT] = {false, true}; // --- RGB BACKLIGHT --- -#define RGB_MOD_INSTALLED_COUNT 2 -const char* const rgb_mod_installed_text[RGB_MOD_INSTALLED_COUNT] = { - "OFF", - "ON", -}; -const bool rgb_mod_installed_value[RGB_MOD_INSTALLED_COUNT] = {false, true}; -#define RGB_MOD_RAINBOW_MODE_COUNT 2 -const char* const rgb_mod_rainbow_mode_text[RGB_MOD_RAINBOW_MODE_COUNT] = { - "OFF", +// #define RGB_BACKLIGHT_INSTALLED_COUNT 2 +// const char* const rgb_backlight_installed_text[RGB_BACKLIGHT_INSTALLED_COUNT] = { +// "OFF", +// "ON", +// }; +// const bool rgb_backlight_installed_value[RGB_BACKLIGHT_INSTALLED_COUNT] = {false, true}; + +#define RGB_BACKLIGHT_MODE_COUNT 4 +const char* const rgb_backlight_mode_text[RGB_BACKLIGHT_MODE_COUNT] = { + "OFF" + "OneColor", + 'Gradient', "Rainbow", }; -const uint32_t rgb_mod_rainbow_mode_value[RGB_MOD_RAINBOW_MODE_COUNT] = {0, 1}; +const uint32_t rgb_backlight_mode_value[RGB_BACKLIGHT_MODE_COUNT] = {0, 1, 3, 4}; -#define RGB_MOD_RAINBOW_SPEED_COUNT 20 -const char* const rgb_mod_rainbow_speed_text[RGB_MOD_RAINBOW_SPEED_COUNT] = { +#define RGB_BACKLIGHT_RAINBOW_MODE_COUNT 2 +const char* const rgb_backlight_rainbow_mode_text[RGB_BACKLIGHT_RAINBOW_MODE_COUNT] = { + "Rainbow", + "Wave", +}; +const uint32_t rgb_backlight_rainbow_mode_value[RGB_BACKLIGHT_RAINBOW_MODE_COUNT] = {1, 2}; + +#define RGB_BACKLIGHT_RAINBOW_SPEED_COUNT 20 +const char* const rgb_backlight_rainbow_speed_text[RGB_BACKLIGHT_RAINBOW_SPEED_COUNT] = { "0.1s", "0.2s", "0.3s", "0.4s", "0.5s", "0.6s", "0.7", "0.8", "0.9", "1s", "1.1s", "1.2s", "1.3s", "1.4s", "1.5s", "1.6s", "1.7s", "1.8s", "1.9s", "2s"}; -const uint32_t rgb_mod_rainbow_speed_value[RGB_MOD_RAINBOW_SPEED_COUNT] = { +const uint32_t rgb_backlight_rainbow_speed_value[RGB_BACKLIGHT_RAINBOW_SPEED_COUNT] = { 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000, 1100, 1200, 1300, 1400, 1500, 1600, 1700, 1800, 1900, 2000}; -#define RGB_MOD_RAINBOW_STEP_COUNT 10 -const char* const rgb_mod_rainbow_step_text[RGB_MOD_RAINBOW_SPEED_COUNT] = { +#define RGB_BACKLIGHT_RAINBOW_STEP_COUNT 10 +const char* const rgb_backlight_rainbow_step_text[RGB_BACKLIGHT_RAINBOW_STEP_COUNT] = { "1", "2", "3", @@ -145,7 +155,7 @@ const char* const rgb_mod_rainbow_step_text[RGB_MOD_RAINBOW_SPEED_COUNT] = { "9", "10", }; -const uint32_t rgb_mod_rainbow_step_value[RGB_MOD_RAINBOW_STEP_COUNT] = +const uint32_t rgb_backlight_rainbow_step_value[RGB_BACKLIGHT_RAINBOW_STEP_COUNT] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; typedef enum { @@ -228,33 +238,76 @@ static void vibro_changed(VariableItem* item) { //--- RGB BACKLIGHT --- -static void rgb_mod_installed_changed(VariableItem* item) { +// static void rgb_backlight_installed_changed(VariableItem* item) { +// NotificationAppSettings* app = variable_item_get_context(item); +// uint8_t index = variable_item_get_current_value_index(item); +// variable_item_set_current_value_text(item, rgb_backlight_installed_text[index]); +// app->notification->rgb_srv->settings->rgb_backlight_installed = rgb_backlight_installed_value[index]; +// rgb_backlight_settings_save(app->notification->rgb_srv->settings); +// // Lock/Unlock all rgb settings depent from rgb_backlight_installed switch +// int slide = 0; +// if(furi_hal_rtc_is_flag_set(FuriHalRtcFlagDebug)) { +// slide = 1; +// } +// for(int i = slide; i < (slide + 7); i++) { +// VariableItem* t_item = variable_item_list_get(app->variable_item_list_rgb, i); +// if(index == 0) { +// variable_item_set_locked(t_item, true, "RGB\nOFF!"); +// } else { +// variable_item_set_locked(t_item, false, "RGB\nOFF!"); +// } +// } + +//TODO по умолчанию все пункты ВЫКЛ, когда включаем РГБ установлен, то разблокируем только +// переключатель режима РГБ и в зависимости от его значения остальные пункты +// когда переключталь РГБ выключен, то выключаем все пункты меню. +// } + +static void rgb_backlight_mode_changed(VariableItem* item) { NotificationAppSettings* app = variable_item_get_context(item); uint8_t index = variable_item_get_current_value_index(item); - variable_item_set_current_value_text(item, rgb_mod_installed_text[index]); - app->notification->rgb_srv->settings->rgb_mod_installed = rgb_mod_installed_value[index]; + variable_item_set_current_value_text(item, rgb_backlight_mode_text[index]); + app->notification->rgb_srv->settings->rgb_backlight_mode = rgb_backlight_mode_value[index]; rgb_backlight_settings_save(app->notification->rgb_srv->settings); - // Lock/Unlock rgb settings depent from rgb_mod_installed switch + + // Lock/Unlock rgb settings depent from selected mode int slide = 0; if(furi_hal_rtc_is_flag_set(FuriHalRtcFlagDebug)) { slide = 1; } - for(int i = slide; i < (slide + 7); i++) { - VariableItem* t_item = variable_item_list_get(app->variable_item_list_rgb, i); - if(index == 0) { - variable_item_set_locked(t_item, true, "RGB MOD\nOFF!"); - } else { - variable_item_set_locked(t_item, false, "RGB MOD\nOFF!"); - } + + VariableItem* t_item = variable_item_list_get(app->variable_item_list_rgb, i); + + switch(index) { + // OneColor + case 0: + break; + // Gradient + case 1: + break; + // Rainbow + case 2: + break; + default: + break; } + + // for(int i = slide; i < (slide + 7); i++) { + // VariableItem* t_item = variable_item_list_get(app->variable_item_list_rgb, i); + // if(index == 0) { + // variable_item_set_locked(t_item, true, "RGB MOD\nOFF!"); + // } else { + // variable_item_set_locked(t_item, false, "RGB MOD\nOFF!"); + // } + // } } -static void rgb_mod_rainbow_changed(VariableItem* item) { +static void rgb_backlight_rainbow_changed(VariableItem* item) { NotificationAppSettings* app = variable_item_get_context(item); uint8_t index = variable_item_get_current_value_index(item); - variable_item_set_current_value_text(item, rgb_mod_rainbow_mode_text[index]); - app->notification->rgb_srv->settings->rainbow_mode = rgb_mod_rainbow_mode_value[index]; + variable_item_set_current_value_text(item, rgb_backlight_rainbow_mode_text[index]); + app->notification->rgb_srv->settings->rainbow_mode = rgb_backlight_rainbow_mode_value[index]; rainbow_timer_starter(app->notification->rgb_srv); rgb_backlight_settings_save(app->notification->rgb_srv->settings); @@ -280,24 +333,25 @@ static void rgb_mod_rainbow_changed(VariableItem* item) { } } -static void rgb_mod_rainbow_speed_changed(VariableItem* item) { +static void rgb_backlight_rainbow_speed_changed(VariableItem* item) { NotificationAppSettings* app = variable_item_get_context(item); uint8_t index = variable_item_get_current_value_index(item); - variable_item_set_current_value_text(item, rgb_mod_rainbow_speed_text[index]); - app->notification->rgb_srv->settings->rainbow_speed_ms = rgb_mod_rainbow_speed_value[index]; + variable_item_set_current_value_text(item, rgb_backlight_rainbow_speed_text[index]); + app->notification->rgb_srv->settings->rainbow_speed_ms = + rgb_backlight_rainbow_speed_value[index]; //save settings and restart timer with new speed value rgb_backlight_settings_save(app->notification->rgb_srv->settings); rainbow_timer_starter(app->notification->rgb_srv); } -static void rgb_mod_rainbow_step_changed(VariableItem* item) { +static void rgb_backlight_rainbow_step_changed(VariableItem* item) { NotificationAppSettings* app = variable_item_get_context(item); uint8_t index = variable_item_get_current_value_index(item); - variable_item_set_current_value_text(item, rgb_mod_rainbow_step_text[index]); - app->notification->rgb_srv->settings->rainbow_step = rgb_mod_rainbow_step_value[index]; + variable_item_set_current_value_text(item, rgb_backlight_rainbow_step_text[index]); + app->notification->rgb_srv->settings->rainbow_step = rgb_backlight_rainbow_step_value[index]; rgb_backlight_settings_save(app->notification->rgb_srv->settings); } @@ -320,9 +374,12 @@ static void color_set_custom_red(VariableItem* item) { NotificationAppSettings* app = variable_item_get_context(item); uint8_t index = variable_item_get_current_value_index(item); - //Set custom red to settings and current color - app->notification->rgb_srv->settings->custom_red = index; + // Update all current colors with selected customs and save changed custom color to settings app->notification->rgb_srv->current_red = index; + app->notification->rgb_srv->current_green = app->notification->rgb_srv->settings->custom_green; + app->notification->rgb_srv->current_blue = app->notification->rgb_srv->settings->custom_blue; + + app->notification->rgb_srv->settings->custom_red = index; app->notification->rgb_srv->settings->static_color_index = 13; char valtext[4] = {}; @@ -340,9 +397,12 @@ static void color_set_custom_green(VariableItem* item) { NotificationAppSettings* app = variable_item_get_context(item); uint8_t index = variable_item_get_current_value_index(item); - //Set custom green to settings and current color - app->notification->rgb_srv->settings->custom_green = index; + // Update all current colors with selected customs and save changed custom color to settings + app->notification->rgb_srv->current_red = app->notification->rgb_srv->settings->custom_red; app->notification->rgb_srv->current_green = index; + app->notification->rgb_srv->current_blue = app->notification->rgb_srv->settings->custom_blue; + + app->notification->rgb_srv->settings->custom_green = index; app->notification->rgb_srv->settings->static_color_index = 13; char valtext[4] = {}; @@ -360,9 +420,12 @@ static void color_set_custom_blue(VariableItem* item) { NotificationAppSettings* app = variable_item_get_context(item); uint8_t index = variable_item_get_current_value_index(item); - //Set custom blue to settings and current color - app->notification->rgb_srv->settings->custom_blue = index; + // Update all current colors with selected customs and save changed custom color to settings + app->notification->rgb_srv->current_red = app->notification->rgb_srv->settings->custom_red; + app->notification->rgb_srv->current_green = app->notification->rgb_srv->settings->custom_green; app->notification->rgb_srv->current_blue = index; + + app->notification->rgb_srv->settings->custom_blue = index; app->notification->rgb_srv->settings->static_color_index = 13; char valtext[4] = {}; @@ -377,12 +440,12 @@ static void color_set_custom_blue(VariableItem* item) { rgb_backlight_settings_save(app->notification->rgb_srv->settings); } -// open rgb_settings_view if user press OK on first (index=0) menu string and (debug mode or rgb_mod_install is true) +// open rgb_settings_view if user press OK on first (index=0) menu string and (debug mode or rgb_backlight_installed is true) void variable_item_list_enter_callback(void* context, uint32_t index) { UNUSED(context); NotificationAppSettings* app = context; - if(((app->notification->rgb_srv->settings->rgb_mod_installed) || + if(((app->notification->rgb_srv->settings->rgb_backlight_installed) || (furi_hal_rtc_is_flag_set(FuriHalRtcFlagDebug))) && (index == 0)) { view_dispatcher_switch_to_view(app->view_dispatcher, RGBViewId); @@ -420,10 +483,10 @@ static NotificationAppSettings* alloc_settings(void) { variable_item_list_set_enter_callback( app->variable_item_list, variable_item_list_enter_callback, app); - //Show RGB settings only when debug_mode or rgb_mod_installed is active - if((app->notification->rgb_srv->settings->rgb_mod_installed) || + //Show RGB settings only when debug_mode or rgb_backlight_installed is active + if((app->notification->rgb_srv->settings->rgb_backlight_installed) || (furi_hal_rtc_is_flag_set(FuriHalRtcFlagDebug))) { - item = variable_item_list_add(app->variable_item_list, "RGB mod settings", 0, NULL, app); + item = variable_item_list_add(app->variable_item_list, "RGB settings", 0, NULL, app); } //--- RGB BACKLIGHT END --- @@ -490,22 +553,39 @@ static NotificationAppSettings* alloc_settings(void) { // set callback for OK pressed in rgb_settings_menu view_set_previous_callback(view_rgb, notification_app_rgb_settings_exit); - // Show RGB_MOD_Installed_Swith only in Debug mode - if(furi_hal_rtc_is_flag_set(FuriHalRtcFlagDebug)) { - item = variable_item_list_add( - app->variable_item_list_rgb, - "RGB MOD Installed", - RGB_MOD_INSTALLED_COUNT, - rgb_mod_installed_changed, - app); - value_index = value_index_bool( - app->notification->rgb_srv->settings->rgb_mod_installed, - rgb_mod_installed_value, - RGB_MOD_INSTALLED_COUNT); - variable_item_set_current_value_index(item, value_index); - variable_item_set_current_value_text(item, rgb_mod_installed_text[value_index]); - } + // // Show rgb_backlight_Installed_Swith only in Debug mode + // if(furi_hal_rtc_is_flag_set(FuriHalRtcFlagDebug)) { + // item = variable_item_list_add( + // app->variable_item_list_rgb, + // "RGB mod installed", + // RGB_BACKLIGHT_INSTALLED_COUNT, + // rgb_backlight_installed_changed, + // app); + // value_index = value_index_bool( + // app->notification->rgb_srv->settings->rgb_backlight_installed, + // rgb_backlight_installed_value, + // RGB_BACKLIGHT_INSTALLED_COUNT); + // variable_item_set_current_value_index(item, value_index); + // variable_item_set_current_value_text(item, rgb_backlight_installed_text[value_index]); + // } + // Show rgb_backlight_mode_swith only in Debug mode or when rgb_mode is not OFF. + if(furi_hal_rtc_is_flag_set(FuriHalRtcFlagDebug) || + (app->notification->rgb_srv->settings->rgb_backlight_mode > 0)) { + item = variable_item_list_add( + app->variable_item_list_rgb, + "RGB MODE", + RGB_BACKLIGHT_MODE_COUNT, + rgb_backlight_mode_changed, + app); + value_index = value_index_uint32( + app->notification->rgb_srv->settings->rgb_backlight_mode, + rgb_backlight_mode_value, + RGB_BACKLIGHT_MODE_COUNT); + variable_item_set_current_value_index(item, value_index); + variable_item_set_current_value_text(item, rgb_backlight_mode_text[value_index]); + } + // Static Colors settings item = variable_item_list_add( app->variable_item_list_rgb, @@ -519,7 +599,9 @@ static NotificationAppSettings* alloc_settings(void) { variable_item_set_locked( item, (app->notification->rgb_srv->settings->rainbow_mode > 0), "Rainbow mode\nenabled!"); variable_item_set_locked( - item, (app->notification->rgb_srv->settings->rgb_mod_installed == 0), "RGB MOD \nOFF!"); + item, + (app->notification->rgb_srv->settings->rgb_backlight_installed == 0), + "RGB MOD \nOFF!"); temp_item = item; @@ -534,7 +616,9 @@ static NotificationAppSettings* alloc_settings(void) { variable_item_set_locked( item, (app->notification->rgb_srv->settings->rainbow_mode > 0), "Rainbow mode\nenabled!"); variable_item_set_locked( - item, (app->notification->rgb_srv->settings->rgb_mod_installed == 0), "RGB MOD \nOFF!"); + item, + (app->notification->rgb_srv->settings->rgb_backlight_installed == 0), + "RGB MOD \nOFF!"); item = variable_item_list_add( app->variable_item_list_rgb, "Custom Green", 255, color_set_custom_green, app); @@ -545,7 +629,9 @@ static NotificationAppSettings* alloc_settings(void) { variable_item_set_locked( item, (app->notification->rgb_srv->settings->rainbow_mode > 0), "Rainbow mode\nenabled!"); variable_item_set_locked( - item, (app->notification->rgb_srv->settings->rgb_mod_installed == 0), "RGB MOD \nOFF!"); + item, + (app->notification->rgb_srv->settings->rgb_backlight_installed == 0), + "RGB MOD \nOFF!"); item = variable_item_list_add( app->variable_item_list_rgb, "Custom Blue", 255, color_set_custom_blue, app); @@ -556,53 +642,61 @@ static NotificationAppSettings* alloc_settings(void) { variable_item_set_locked( item, (app->notification->rgb_srv->settings->rainbow_mode > 0), "Rainbow mode\nenabled!"); variable_item_set_locked( - item, (app->notification->rgb_srv->settings->rgb_mod_installed == 0), "RGB MOD \nOFF!"); + item, + (app->notification->rgb_srv->settings->rgb_backlight_installed == 0), + "RGB MOD \nOFF!"); // Rainbow (based on Willy-JL idea) settings item = variable_item_list_add( app->variable_item_list_rgb, "Rainbow mode", - RGB_MOD_RAINBOW_MODE_COUNT, - rgb_mod_rainbow_changed, + RGB_BACKLIGHT_RAINBOW_MODE_COUNT, + rgb_backlight_rainbow_changed, app); value_index = value_index_uint32( app->notification->rgb_srv->settings->rainbow_mode, - rgb_mod_rainbow_mode_value, - RGB_MOD_RAINBOW_MODE_COUNT); + rgb_backlight_rainbow_mode_value, + RGB_BACKLIGHT_RAINBOW_MODE_COUNT); variable_item_set_current_value_index(item, value_index); - variable_item_set_current_value_text(item, rgb_mod_rainbow_mode_text[value_index]); + variable_item_set_current_value_text(item, rgb_backlight_rainbow_mode_text[value_index]); variable_item_set_locked( - item, (app->notification->rgb_srv->settings->rgb_mod_installed == 0), "RGB MOD \nOFF!"); + item, + (app->notification->rgb_srv->settings->rgb_backlight_installed == 0), + "RGB MOD \nOFF!"); item = variable_item_list_add( app->variable_item_list_rgb, "Rainbow speed", - RGB_MOD_RAINBOW_SPEED_COUNT, - rgb_mod_rainbow_speed_changed, + RGB_BACKLIGHT_RAINBOW_SPEED_COUNT, + rgb_backlight_rainbow_speed_changed, app); value_index = value_index_uint32( app->notification->rgb_srv->settings->rainbow_speed_ms, - rgb_mod_rainbow_speed_value, - RGB_MOD_RAINBOW_SPEED_COUNT); + rgb_backlight_rainbow_speed_value, + RGB_BACKLIGHT_RAINBOW_SPEED_COUNT); variable_item_set_current_value_index(item, value_index); - variable_item_set_current_value_text(item, rgb_mod_rainbow_speed_text[value_index]); + variable_item_set_current_value_text(item, rgb_backlight_rainbow_speed_text[value_index]); variable_item_set_locked( - item, (app->notification->rgb_srv->settings->rgb_mod_installed == 0), "RGB MOD \nOFF!"); + item, + (app->notification->rgb_srv->settings->rgb_backlight_installed == 0), + "RGB MOD \nOFF!"); item = variable_item_list_add( app->variable_item_list_rgb, "Rainbow step", - RGB_MOD_RAINBOW_STEP_COUNT, - rgb_mod_rainbow_step_changed, + RGB_BACKLIGHT_RAINBOW_STEP_COUNT, + rgb_backlight_rainbow_step_changed, app); value_index = value_index_uint32( app->notification->rgb_srv->settings->rainbow_step, - rgb_mod_rainbow_step_value, - RGB_MOD_RAINBOW_SPEED_COUNT); + rgb_backlight_rainbow_step_value, + RGB_BACKLIGHT_RAINBOW_SPEED_COUNT); variable_item_set_current_value_index(item, value_index); - variable_item_set_current_value_text(item, rgb_mod_rainbow_step_text[value_index]); + variable_item_set_current_value_text(item, rgb_backlight_rainbow_step_text[value_index]); variable_item_set_locked( - item, (app->notification->rgb_srv->settings->rgb_mod_installed == 0), "RGB MOD \nOFF!"); + item, + (app->notification->rgb_srv->settings->rgb_backlight_installed == 0), + "RGB MOD \nOFF!"); //--- RGB BACKLIGHT END --- @@ -632,8 +726,8 @@ int32_t notification_settings_app(void* p) { view_dispatcher_run(app->view_dispatcher); notification_message_save_settings(app->notification); - // Automaticaly switch_off debug_mode when user exit from settings with enabled rgb_mod_installed - // if(app->notification->settings.rgb_mod_installed) { + // Automaticaly switch_off debug_mode when user exit from settings with enabled rgb_backlight_installed + // if(app->notification->settings.rgb_backlight_installed) { // furi_hal_rtc_reset_flag(FuriHalRtcFlagDebug); // } From c66b332a7dd369b90e42cb66aaee7f0857749830 Mon Sep 17 00:00:00 2001 From: Dmitry422 Date: Mon, 17 Mar 2025 23:56:39 +0700 Subject: [PATCH 132/268] refactor rgb_backlight --- .../services/rgb_backlight/rgb_backlight.c | 30 +- .../rgb_backlight/rgb_backlight_settings.c | 2 +- .../rgb_backlight/rgb_backlight_settings.h | 8 +- .../notification_settings_app.c | 287 ++++-------------- 4 files changed, 87 insertions(+), 240 deletions(-) diff --git a/applications/services/rgb_backlight/rgb_backlight.c b/applications/services/rgb_backlight/rgb_backlight.c index 9b25ded5b..f3d3cfb35 100644 --- a/applications/services/rgb_backlight/rgb_backlight.c +++ b/applications/services/rgb_backlight/rgb_backlight.c @@ -42,7 +42,7 @@ static const RGBBacklightColor colors[] = { {"Pink", 255, 0, 127}, {"Red", 255, 0, 0}, {"White", 254, 210, 200}, - {"Custom", 255, 255, 255}, + {"White1", 255, 255, 255}, }; uint8_t rgb_backlight_get_color_count(void) { @@ -54,19 +54,27 @@ const char* rgb_backlight_get_color_text(uint8_t index) { } // use RECORD for acces to rgb service instance and update current colors by static -void rgb_backlight_set_static_color(uint8_t index) { +void rgb_backlight_set_static_color(uint8_t index, uint8_t vd) { RGBBacklightApp* app = furi_record_open(RECORD_RGB_BACKLIGHT); - //if user select "custom" value then set current colors by custom values - if(index == 13) { - app->current_red = app->settings->custom_red; - app->current_green = app->settings->custom_green; - app->current_blue = app->settings->custom_blue; - } else { - app->current_red = colors[index].red; - app->current_green = colors[index].green; - app->current_blue = colors[index].blue; + if(vd < SK6805_get_led_count()) { + uint8_t r = colors[index].red; + uint8_t g = colors[index].green; + uint8_t b = colors[index].blue; + SK6805_set_led_color(vd, r, g, b); + SK6805_update(); } + + //if user select "custom" value then set current colors by custom values + if(index == 13) { + app->current_red = app->settings->custom_red; + app->current_green = app->settings->custom_green; + app->current_blue = app->settings->custom_blue; + } else { + app->current_red = colors[index].red; + app->current_green = colors[index].green; + app->current_blue = colors[index].blue; + } furi_record_close(RECORD_RGB_BACKLIGHT); } diff --git a/applications/services/rgb_backlight/rgb_backlight_settings.c b/applications/services/rgb_backlight/rgb_backlight_settings.c index 7c73ddf19..e2a2e5df7 100644 --- a/applications/services/rgb_backlight/rgb_backlight_settings.c +++ b/applications/services/rgb_backlight/rgb_backlight_settings.c @@ -16,7 +16,7 @@ typedef struct { //Common settings uint8_t version; - uint8_t rgb_backlight_mode; + bool rgb_backlight_installed; float brightness; //static and custom colors mode settings diff --git a/applications/services/rgb_backlight/rgb_backlight_settings.h b/applications/services/rgb_backlight/rgb_backlight_settings.h index 478048039..694161c2b 100644 --- a/applications/services/rgb_backlight/rgb_backlight_settings.h +++ b/applications/services/rgb_backlight/rgb_backlight_settings.h @@ -6,14 +6,8 @@ typedef struct { //Common settings uint8_t version; - uint8_t rgb_backlight_mode; + uint8_t rgb_backlight_installed; float brightness; - - //static and custom colors mode settings - uint8_t static_color_index; - uint8_t custom_red; - uint8_t custom_green; - uint8_t custom_blue; // static gradient mode settings uint8_t static_vd1_index; diff --git a/applications/settings/notification_settings/notification_settings_app.c b/applications/settings/notification_settings/notification_settings_app.c index cc316e6aa..1a738b492 100644 --- a/applications/settings/notification_settings/notification_settings_app.c +++ b/applications/settings/notification_settings/notification_settings_app.c @@ -111,28 +111,20 @@ const bool vibro_value[VIBRO_COUNT] = {false, true}; // --- RGB BACKLIGHT --- -// #define RGB_BACKLIGHT_INSTALLED_COUNT 2 -// const char* const rgb_backlight_installed_text[RGB_BACKLIGHT_INSTALLED_COUNT] = { -// "OFF", -// "ON", -// }; -// const bool rgb_backlight_installed_value[RGB_BACKLIGHT_INSTALLED_COUNT] = {false, true}; - -#define RGB_BACKLIGHT_MODE_COUNT 4 -const char* const rgb_backlight_mode_text[RGB_BACKLIGHT_MODE_COUNT] = { - "OFF" - "OneColor", - 'Gradient', - "Rainbow", +#define RGB_BACKLIGHT_INSTALLED_COUNT 2 +const char* const rgb_backlight_installed_text[RGB_BACKLIGHT_INSTALLED_COUNT] = { + "OFF", + "ON", }; -const uint32_t rgb_backlight_mode_value[RGB_BACKLIGHT_MODE_COUNT] = {0, 1, 3, 4}; +const bool rgb_backlight_installed_value[RGB_BACKLIGHT_INSTALLED_COUNT] = {false, true}; -#define RGB_BACKLIGHT_RAINBOW_MODE_COUNT 2 +#define RGB_BACKLIGHT_RAINBOW_MODE_COUNT 3 const char* const rgb_backlight_rainbow_mode_text[RGB_BACKLIGHT_RAINBOW_MODE_COUNT] = { + "OFF", "Rainbow", "Wave", }; -const uint32_t rgb_backlight_rainbow_mode_value[RGB_BACKLIGHT_RAINBOW_MODE_COUNT] = {1, 2}; +const uint32_t rgb_backlight_rainbow_mode_value[RGB_BACKLIGHT_RAINBOW_MODE_COUNT] = {0, 1, 2}; #define RGB_BACKLIGHT_RAINBOW_SPEED_COUNT 20 const char* const rgb_backlight_rainbow_speed_text[RGB_BACKLIGHT_RAINBOW_SPEED_COUNT] = { @@ -238,68 +230,25 @@ static void vibro_changed(VariableItem* item) { //--- RGB BACKLIGHT --- -// static void rgb_backlight_installed_changed(VariableItem* item) { -// NotificationAppSettings* app = variable_item_get_context(item); -// uint8_t index = variable_item_get_current_value_index(item); -// variable_item_set_current_value_text(item, rgb_backlight_installed_text[index]); -// app->notification->rgb_srv->settings->rgb_backlight_installed = rgb_backlight_installed_value[index]; -// rgb_backlight_settings_save(app->notification->rgb_srv->settings); -// // Lock/Unlock all rgb settings depent from rgb_backlight_installed switch -// int slide = 0; -// if(furi_hal_rtc_is_flag_set(FuriHalRtcFlagDebug)) { -// slide = 1; -// } -// for(int i = slide; i < (slide + 7); i++) { -// VariableItem* t_item = variable_item_list_get(app->variable_item_list_rgb, i); -// if(index == 0) { -// variable_item_set_locked(t_item, true, "RGB\nOFF!"); -// } else { -// variable_item_set_locked(t_item, false, "RGB\nOFF!"); -// } -// } - -//TODO по умолчанию все пункты ВЫКЛ, когда включаем РГБ установлен, то разблокируем только -// переключатель режима РГБ и в зависимости от его значения остальные пункты -// когда переключталь РГБ выключен, то выключаем все пункты меню. -// } - -static void rgb_backlight_mode_changed(VariableItem* item) { +static void rgb_backlight_installed_changed(VariableItem* item) { NotificationAppSettings* app = variable_item_get_context(item); uint8_t index = variable_item_get_current_value_index(item); - variable_item_set_current_value_text(item, rgb_backlight_mode_text[index]); - app->notification->rgb_srv->settings->rgb_backlight_mode = rgb_backlight_mode_value[index]; + variable_item_set_current_value_text(item, rgb_backlight_installed_text[index]); + app->notification->rgb_srv->settings->rgb_backlight_installed = rgb_backlight_installed_value[index]; rgb_backlight_settings_save(app->notification->rgb_srv->settings); - - // Lock/Unlock rgb settings depent from selected mode + // Lock/Unlock all rgb settings depent from rgb_backlight_installed switch int slide = 0; if(furi_hal_rtc_is_flag_set(FuriHalRtcFlagDebug)) { slide = 1; } - - VariableItem* t_item = variable_item_list_get(app->variable_item_list_rgb, i); - - switch(index) { - // OneColor - case 0: - break; - // Gradient - case 1: - break; - // Rainbow - case 2: - break; - default: - break; + for(int i = slide; i < (slide + 7); i++) { + VariableItem* t_item = variable_item_list_get(app->variable_item_list_rgb, i); + if(index == 0) { + variable_item_set_locked(t_item, true, "RGB\nOFF!"); + } else { + variable_item_set_locked(t_item, false, "RGB\nOFF!"); + } } - - // for(int i = slide; i < (slide + 7); i++) { - // VariableItem* t_item = variable_item_list_get(app->variable_item_list_rgb, i); - // if(index == 0) { - // variable_item_set_locked(t_item, true, "RGB MOD\nOFF!"); - // } else { - // variable_item_set_locked(t_item, false, "RGB MOD\nOFF!"); - // } - // } } static void rgb_backlight_rainbow_changed(VariableItem* item) { @@ -312,20 +261,6 @@ static void rgb_backlight_rainbow_changed(VariableItem* item) { rainbow_timer_starter(app->notification->rgb_srv); rgb_backlight_settings_save(app->notification->rgb_srv->settings); - // Lock/Unlock color settings if rainbow mode Enabled/Disabled (0-3 index if debug off and 1-4 index if debug on) - int slide = 0; - if(furi_hal_rtc_is_flag_set(FuriHalRtcFlagDebug)) { - slide = 1; - } - for(int i = slide; i < (slide + 4); i++) { - VariableItem* t_item = variable_item_list_get(app->variable_item_list_rgb, i); - if(index > 0) { - variable_item_set_locked(t_item, true, "Rainbow mode\nenabled!"); - } else { - variable_item_set_locked(t_item, false, "Rainbow mode\nenabled!"); - } - } - // restore saved rgb backlight settings if we switch_off rainbow mode if(app->notification->rgb_srv->settings->rainbow_mode == 0) { rgb_backlight_set_static_color(app->notification->rgb_srv->settings->static_color_index); @@ -356,9 +291,8 @@ static void rgb_backlight_rainbow_step_changed(VariableItem* item) { rgb_backlight_settings_save(app->notification->rgb_srv->settings); } -// Set rgb_backlight colors static and custom -static void color_changed(VariableItem* item) { +static void vd1_color_changed(VariableItem* item) { NotificationAppSettings* app = variable_item_get_context(item); uint8_t index = variable_item_get_current_value_index(item); @@ -370,72 +304,26 @@ static void color_changed(VariableItem* item) { rgb_backlight_settings_save(app->notification->rgb_srv->settings); } -static void color_set_custom_red(VariableItem* item) { +static void vd2_color_changed(VariableItem* item) { NotificationAppSettings* app = variable_item_get_context(item); uint8_t index = variable_item_get_current_value_index(item); - // Update all current colors with selected customs and save changed custom color to settings - app->notification->rgb_srv->current_red = index; - app->notification->rgb_srv->current_green = app->notification->rgb_srv->settings->custom_green; - app->notification->rgb_srv->current_blue = app->notification->rgb_srv->settings->custom_blue; - - app->notification->rgb_srv->settings->custom_red = index; - app->notification->rgb_srv->settings->static_color_index = 13; - - char valtext[4] = {}; - snprintf(valtext, sizeof(valtext), "%d", index); - variable_item_set_current_value_text(item, valtext); - - // Set to custom color explicitly - variable_item_set_current_value_index(temp_item, 13); - variable_item_set_current_value_text(temp_item, rgb_backlight_get_color_text(13)); + variable_item_set_current_value_text(item, rgb_backlight_get_color_text(index)); + app->notification->rgb_srv->settings->static_color_index = index; + rgb_backlight_set_static_color(index); rgb_backlight_update(app->notification->rgb_srv->settings->brightness); rgb_backlight_settings_save(app->notification->rgb_srv->settings); } -static void color_set_custom_green(VariableItem* item) { + +static void vd3_color_changed(VariableItem* item) { NotificationAppSettings* app = variable_item_get_context(item); uint8_t index = variable_item_get_current_value_index(item); - // Update all current colors with selected customs and save changed custom color to settings - app->notification->rgb_srv->current_red = app->notification->rgb_srv->settings->custom_red; - app->notification->rgb_srv->current_green = index; - app->notification->rgb_srv->current_blue = app->notification->rgb_srv->settings->custom_blue; - - app->notification->rgb_srv->settings->custom_green = index; - app->notification->rgb_srv->settings->static_color_index = 13; - - char valtext[4] = {}; - snprintf(valtext, sizeof(valtext), "%d", index); - variable_item_set_current_value_text(item, valtext); - - // Set to custom color explicitly - variable_item_set_current_value_index(temp_item, 13); - variable_item_set_current_value_text(temp_item, rgb_backlight_get_color_text(13)); - - rgb_backlight_update(app->notification->rgb_srv->settings->brightness); - rgb_backlight_settings_save(app->notification->rgb_srv->settings); -} -static void color_set_custom_blue(VariableItem* item) { - NotificationAppSettings* app = variable_item_get_context(item); - uint8_t index = variable_item_get_current_value_index(item); - - // Update all current colors with selected customs and save changed custom color to settings - app->notification->rgb_srv->current_red = app->notification->rgb_srv->settings->custom_red; - app->notification->rgb_srv->current_green = app->notification->rgb_srv->settings->custom_green; - app->notification->rgb_srv->current_blue = index; - - app->notification->rgb_srv->settings->custom_blue = index; - app->notification->rgb_srv->settings->static_color_index = 13; - - char valtext[4] = {}; - snprintf(valtext, sizeof(valtext), "%d", index); - variable_item_set_current_value_text(item, valtext); - - // Set to custom color explicitly - variable_item_set_current_value_index(temp_item, 13); - variable_item_set_current_value_text(temp_item, rgb_backlight_get_color_text(13)); + variable_item_set_current_value_text(item, rgb_backlight_get_color_text(index)); + app->notification->rgb_srv->settings->static_color_index = index; + rgb_backlight_set_static_color(index); rgb_backlight_update(app->notification->rgb_srv->settings->brightness); rgb_backlight_settings_save(app->notification->rgb_srv->settings); } @@ -547,106 +435,63 @@ static NotificationAppSettings* alloc_settings(void) { } //--- RGB BACKLIGHT --- + app->variable_item_list_rgb = variable_item_list_alloc(); View* view_rgb = variable_item_list_get_view(app->variable_item_list_rgb); - // set callback for OK pressed in rgb_settings_menu + // set callback for exit from rgb_settings_menu view_set_previous_callback(view_rgb, notification_app_rgb_settings_exit); // // Show rgb_backlight_Installed_Swith only in Debug mode - // if(furi_hal_rtc_is_flag_set(FuriHalRtcFlagDebug)) { - // item = variable_item_list_add( - // app->variable_item_list_rgb, - // "RGB mod installed", - // RGB_BACKLIGHT_INSTALLED_COUNT, - // rgb_backlight_installed_changed, - // app); - // value_index = value_index_bool( - // app->notification->rgb_srv->settings->rgb_backlight_installed, - // rgb_backlight_installed_value, - // RGB_BACKLIGHT_INSTALLED_COUNT); - // variable_item_set_current_value_index(item, value_index); - // variable_item_set_current_value_text(item, rgb_backlight_installed_text[value_index]); - // } - - // Show rgb_backlight_mode_swith only in Debug mode or when rgb_mode is not OFF. - if(furi_hal_rtc_is_flag_set(FuriHalRtcFlagDebug) || - (app->notification->rgb_srv->settings->rgb_backlight_mode > 0)) { - item = variable_item_list_add( - app->variable_item_list_rgb, - "RGB MODE", - RGB_BACKLIGHT_MODE_COUNT, - rgb_backlight_mode_changed, - app); - value_index = value_index_uint32( - app->notification->rgb_srv->settings->rgb_backlight_mode, - rgb_backlight_mode_value, - RGB_BACKLIGHT_MODE_COUNT); - variable_item_set_current_value_index(item, value_index); - variable_item_set_current_value_text(item, rgb_backlight_mode_text[value_index]); + if(furi_hal_rtc_is_flag_set(FuriHalRtcFlagDebug)) { + item = variable_item_list_add( + app->variable_item_list_rgb, + "RGB backlight installed", + RGB_BACKLIGHT_INSTALLED_COUNT, + rgb_backlight_installed_changed, + app); + value_index = value_index_bool( + app->notification->rgb_srv->settings->rgb_backlight_installed, + rgb_backlight_installed_value, + RGB_BACKLIGHT_INSTALLED_COUNT); + variable_item_set_current_value_index(item, value_index); + variable_item_set_current_value_text(item, rgb_backlight_installed_text[value_index]); } - // Static Colors settings + // vd1 color item = variable_item_list_add( app->variable_item_list_rgb, - "LCD Color", + "VD1 Color", rgb_backlight_get_color_count(), - color_changed, + vd1_color_changed, app); value_index = app->notification->rgb_srv->settings->static_color_index; variable_item_set_current_value_index(item, value_index); variable_item_set_current_value_text(item, rgb_backlight_get_color_text(value_index)); - variable_item_set_locked( - item, (app->notification->rgb_srv->settings->rainbow_mode > 0), "Rainbow mode\nenabled!"); - variable_item_set_locked( - item, - (app->notification->rgb_srv->settings->rgb_backlight_installed == 0), - "RGB MOD \nOFF!"); - temp_item = item; - - // Custom Color - REFACTOR THIS + // vd2 color item = variable_item_list_add( - app->variable_item_list_rgb, "Custom Red", 255, color_set_custom_red, app); - value_index = app->notification->rgb_srv->settings->custom_red; + app->variable_item_list_rgb, + "VD2 Color", + rgb_backlight_get_color_count(), + vd2_color_changed, + app); + value_index = app->notification->rgb_srv->settings->static_color_index; variable_item_set_current_value_index(item, value_index); - char valtext[4] = {}; - snprintf(valtext, sizeof(valtext), "%d", value_index); - variable_item_set_current_value_text(item, valtext); - variable_item_set_locked( - item, (app->notification->rgb_srv->settings->rainbow_mode > 0), "Rainbow mode\nenabled!"); - variable_item_set_locked( - item, - (app->notification->rgb_srv->settings->rgb_backlight_installed == 0), - "RGB MOD \nOFF!"); - + variable_item_set_current_value_text(item, rgb_backlight_get_color_text(value_index)); + + // vd 3 color item = variable_item_list_add( - app->variable_item_list_rgb, "Custom Green", 255, color_set_custom_green, app); - value_index = app->notification->rgb_srv->settings->custom_green; + app->variable_item_list_rgb, + "VD3 Color", + rgb_backlight_get_color_count(), + vd3_color_changed, + app); + value_index = app->notification->rgb_srv->settings->static_color_index; variable_item_set_current_value_index(item, value_index); - snprintf(valtext, sizeof(valtext), "%d", value_index); - variable_item_set_current_value_text(item, valtext); - variable_item_set_locked( - item, (app->notification->rgb_srv->settings->rainbow_mode > 0), "Rainbow mode\nenabled!"); - variable_item_set_locked( - item, - (app->notification->rgb_srv->settings->rgb_backlight_installed == 0), - "RGB MOD \nOFF!"); - - item = variable_item_list_add( - app->variable_item_list_rgb, "Custom Blue", 255, color_set_custom_blue, app); - value_index = app->notification->rgb_srv->settings->custom_blue; - variable_item_set_current_value_index(item, value_index); - snprintf(valtext, sizeof(valtext), "%d", value_index); - variable_item_set_current_value_text(item, valtext); - variable_item_set_locked( - item, (app->notification->rgb_srv->settings->rainbow_mode > 0), "Rainbow mode\nenabled!"); - variable_item_set_locked( - item, - (app->notification->rgb_srv->settings->rgb_backlight_installed == 0), - "RGB MOD \nOFF!"); - - // Rainbow (based on Willy-JL idea) settings + variable_item_set_current_value_text(item, rgb_backlight_get_color_text(value_index)); + + // Rainbow mode item = variable_item_list_add( app->variable_item_list_rgb, "Rainbow mode", From a4d0c467f91e8e2bdbc3278ef6f8997bbe224a46 Mon Sep 17 00:00:00 2001 From: Dmitry422 Date: Tue, 18 Mar 2025 18:41:50 +0700 Subject: [PATCH 133/268] End of static colors settings, next rainbow --- .../services/rgb_backlight/rgb_backlight.c | 173 +++++++----------- .../services/rgb_backlight/rgb_backlight.h | 18 +- .../rgb_backlight/rgb_backlight_settings.c | 21 +-- .../rgb_backlight/rgb_backlight_settings.h | 6 +- .../notification_settings_app.c | 149 +++++++++------ targets/f7/api_symbols.csv | 5 +- 6 files changed, 180 insertions(+), 192 deletions(-) diff --git a/applications/services/rgb_backlight/rgb_backlight.c b/applications/services/rgb_backlight/rgb_backlight.c index f3d3cfb35..323cc5d48 100644 --- a/applications/services/rgb_backlight/rgb_backlight.c +++ b/applications/services/rgb_backlight/rgb_backlight.c @@ -28,6 +28,20 @@ #define TAG "RGB_BACKLIGHT_SRV" +typedef struct { + char* name; + uint8_t red; + uint8_t green; + uint8_t blue; +} RGBBacklightColor; + +//use one type RGBBacklightColor for current_leds_settings and for static colors definition +static RGBBacklightColor current_led [] = { + {"LED0",255,60,0}, + {"LED1",255,60,0}, + {"LED2",255,60,0}, +}; + static const RGBBacklightColor colors[] = { {"Orange", 255, 60, 0}, {"Yellow", 255, 144, 0}, @@ -42,7 +56,7 @@ static const RGBBacklightColor colors[] = { {"Pink", 255, 0, 127}, {"Red", 255, 0, 0}, {"White", 254, 210, 200}, - {"White1", 255, 255, 255}, + {"OFF", 0, 0, 0}, }; uint8_t rgb_backlight_get_color_count(void) { @@ -54,47 +68,43 @@ const char* rgb_backlight_get_color_text(uint8_t index) { } // use RECORD for acces to rgb service instance and update current colors by static -void rgb_backlight_set_static_color(uint8_t index, uint8_t vd) { - RGBBacklightApp* app = furi_record_open(RECORD_RGB_BACKLIGHT); +void rgb_backlight_set_led_static_color(uint8_t led, uint8_t index) { + // RGBBacklightApp* app = furi_record_open(RECORD_RGB_BACKLIGHT); + // float brightness = app->settings->brightness; - if(vd < SK6805_get_led_count()) { + if(led < SK6805_get_led_count()) { uint8_t r = colors[index].red; uint8_t g = colors[index].green; uint8_t b = colors[index].blue; - SK6805_set_led_color(vd, r, g, b); - SK6805_update(); + + current_led[led].red = r; + current_led[led].green =g; + current_led[led].blue = b; + + SK6805_set_led_color(led, r, g, b); } - //if user select "custom" value then set current colors by custom values - if(index == 13) { - app->current_red = app->settings->custom_red; - app->current_green = app->settings->custom_green; - app->current_blue = app->settings->custom_blue; - } else { - app->current_red = colors[index].red; - app->current_green = colors[index].green; - app->current_blue = colors[index].blue; - } - furi_record_close(RECORD_RGB_BACKLIGHT); + // furi_record_close(RECORD_RGB_BACKLIGHT); } // use RECORD for acces to rgb service instance and update current colors by custom -void rgb_backlight_set_custom_color(uint8_t red, uint8_t green, uint8_t blue) { - RGBBacklightApp* app = furi_record_open(RECORD_RGB_BACKLIGHT); - app->current_red = red; - app->current_green = green; - app->current_blue = blue; - furi_record_close(RECORD_RGB_BACKLIGHT); -} +// void rgb_backlight_set_custom_color(uint8_t red, uint8_t green, uint8_t blue) { +// RGBBacklightApp* app = furi_record_open(RECORD_RGB_BACKLIGHT); +// app->current_red = red; +// app->current_green = green; +// app->current_blue = blue; +// furi_record_close(RECORD_RGB_BACKLIGHT); +// } // use RECORD for acces to rgb service instance, use current_* colors and update backlight void rgb_backlight_update(float brightness) { RGBBacklightApp* app = furi_record_open(RECORD_RGB_BACKLIGHT); - if(app->settings->rgb_backlight_mode > 0) { + + if(app->settings->rgb_backlight_installed) { for(uint8_t i = 0; i < SK6805_get_led_count(); i++) { - uint8_t r = app->current_red * (brightness / 1.0f); - uint8_t g = app->current_green * (brightness / 1.0f); - uint8_t b = app->current_blue * (brightness / 1.0f); + uint8_t r = current_led[i].red * (brightness * 1.0f); + uint8_t g = current_led[i].green * (brightness * 1.0f); + uint8_t b = current_led[i].blue * (brightness * 1.0f); SK6805_set_led_color(i, r, g, b); } SK6805_update(); @@ -126,59 +136,14 @@ static void rainbow_timer_callback(void* context) { furi_assert(context); RGBBacklightApp* app = context; - // if rainbow_mode is rainbow do rainbow effect - if(app->settings->rainbow_mode == 1) { - switch(app->rainbow_stage) { - // from red to yellow (255,0,0) - (255,255,0) - case 1: - app->current_green += app->settings->rainbow_step; - if(app->current_green >= 255) { - app->current_green = 255; - app->rainbow_stage++; - } - break; - // yellow to green (255,255,0) - (0,255,0) - case 2: - app->current_red -= app->settings->rainbow_step; - if(app->current_red <= 0) { - app->current_red = 0; - app->rainbow_stage++; - } - break; - // from green to light blue (0,255,0) - (0,255,255) - case 3: - app->current_blue += app->settings->rainbow_step; - if(app->current_blue >= 255) { - app->current_blue = 255; - app->rainbow_stage++; - } - break; - //from light blue to blue (0,255,255) - (0,0,255) - case 4: - app->current_green -= app->settings->rainbow_step; - if(app->current_green <= 0) { - app->current_green = 0; - app->rainbow_stage++; - } - break; - //from blue to violet (0,0,255) - (255,0,255) - case 5: - app->current_red += app->settings->rainbow_step; - if(app->current_red >= 255) { - app->current_red = 255; - app->rainbow_stage++; - } - break; - //from violet to red (255,0,255) - (255,0,0) - case 6: - app->current_blue -= app->settings->rainbow_step; - if(app->current_blue <= 0) { - app->current_blue = 0; - app->rainbow_stage = 1; - } - break; - default: - break; + if (app->settings->rgb_backlight_installed) { + switch(app->settings->rainbow_mode) { + case 1: + break; + case 2: + break; + default: + break; } rgb_backlight_update(app->settings->brightness); } @@ -186,6 +151,7 @@ static void rainbow_timer_callback(void* context) { // if rainbow_mode is ..... do another effect // if(app->settings.rainbow_mode == ...) { // } + } int32_t rgb_backlight_srv(void* p) { @@ -194,7 +160,7 @@ int32_t rgb_backlight_srv(void* p) { // Define object app (full app with settings and running variables), // allocate memory and create RECORD for access to app structure from outside RGBBacklightApp* app = malloc(sizeof(RGBBacklightApp)); - + //define rainbow_timer and they callback app->rainbow_timer = furi_timer_alloc(rainbow_timer_callback, FuriTimerTypePeriodic, app); @@ -202,40 +168,33 @@ int32_t rgb_backlight_srv(void* p) { app->settings = malloc(sizeof(RGBBacklightSettings)); rgb_backlight_settings_load(app->settings); - // Init app variables - app->rainbow_stage = 1; - furi_record_create(RECORD_RGB_BACKLIGHT, app); - // if rgb mod installed - start rainbow or set static color from settings (default index = 0) - // - // TODO запуск сохраненного режима - if(app->settings->rgb_backlight_mode > 0) { - // if(app->settings->rainbow_mode > 0) { - // rainbow_timer_starter(app); - // } else { - // rgb_backlight_set_static_color(app->settings->static_color_index); - // rgb_backlight_update(app->settings->brightness); - // } - // if rgb mode = 0 (rgb_backlight not installed) then set default static orange color (index=0) and light on default color - } else { - rgb_backlight_set_static_color(0); - for(uint8_t i = 0; i < SK6805_get_led_count(); i++) { - uint8_t r = app->current_red * 1.0f; - uint8_t g = app->current_green * 1.0f; - uint8_t b = app->current_blue * 1.0f; - SK6805_set_led_color(i, r, g, b); + //if rgb_backlight_installed then start rainbow or set leds colors from saved settings (default index = 0) + if(app->settings->rgb_backlight_installed) { + if(app->settings->rainbow_mode > 0) { + // rainbow_timer_starter(app); + } else { + rgb_backlight_set_led_static_color (2,app->settings->led_2_color_index); + rgb_backlight_set_led_static_color (1,app->settings->led_1_color_index); + rgb_backlight_set_led_static_color (0,app->settings->led_0_color_index); + rgb_backlight_update (app->settings->brightness); } + // if rgb_backlight not installed then set default static orange color(index=0) to all leds (0-2) and force light on + } else { + rgb_backlight_set_led_static_color (2,0); + rgb_backlight_set_led_static_color (1,0); + rgb_backlight_set_led_static_color (0,0); SK6805_update(); } while(1) { // place for message queue and other future options - furi_delay_ms(5000); - if(app->settings->rgb_backlight_mode > 0) { - FURI_LOG_D(TAG, "Mod is enabled - serivce is running"); + furi_delay_ms (5000); + if(app->settings->rgb_backlight_installed) { + FURI_LOG_D(TAG, "RGB backlight enabled - serivce is running"); } else { - FURI_LOG_D(TAG, "Mod is DISABLED - serivce is running"); + FURI_LOG_D(TAG, "RGB backlight DISABLED - serivce is running"); } } return 0; diff --git a/applications/services/rgb_backlight/rgb_backlight.h b/applications/services/rgb_backlight/rgb_backlight.h index fd683bf16..0661886c5 100644 --- a/applications/services/rgb_backlight/rgb_backlight.h +++ b/applications/services/rgb_backlight/rgb_backlight.h @@ -26,20 +26,12 @@ extern "C" { #endif -typedef struct { - char* name; - uint8_t red; - uint8_t green; - uint8_t blue; -} RGBBacklightColor; - typedef struct { FuriTimer* rainbow_timer; - int16_t current_red; - int16_t current_green; - int16_t current_blue; - uint8_t rainbow_stage; + // int16_t current_red; + // int16_t current_green; + // int16_t current_blue; RGBBacklightSettings* settings; @@ -48,8 +40,8 @@ typedef struct { #define RECORD_RGB_BACKLIGHT "rgb_backlight" void rgb_backlight_update(float brightness); -void rgb_backlight_set_custom_color(uint8_t red, uint8_t green, uint8_t blue); -void rgb_backlight_set_static_color(uint8_t index); +// void rgb_backlight_set_custom_color(uint8_t red, uint8_t green, uint8_t blue); +void rgb_backlight_set_led_static_color(uint8_t led, uint8_t index); void rainbow_timer_stop(RGBBacklightApp* app); void rainbow_timer_start(RGBBacklightApp* app); void rainbow_timer_starter(RGBBacklightApp* app); diff --git a/applications/services/rgb_backlight/rgb_backlight_settings.c b/applications/services/rgb_backlight/rgb_backlight_settings.c index e2a2e5df7..4ca7140fd 100644 --- a/applications/services/rgb_backlight/rgb_backlight_settings.c +++ b/applications/services/rgb_backlight/rgb_backlight_settings.c @@ -16,20 +16,14 @@ typedef struct { //Common settings uint8_t version; - bool rgb_backlight_installed; + uint8_t rgb_backlight_installed; float brightness; - - //static and custom colors mode settings - uint8_t static_color_index; - uint8_t custom_red; - uint8_t custom_green; - uint8_t custom_blue; - + // static gradient mode settings - uint8_t static_vd1_index; - uint8_t static_vd2_index; - uint8_t static_vd3_index; - + uint8_t led_2_color_index; + uint8_t led_1_color_index; + uint8_t led_0_color_index; + // rainbow mode setings uint32_t rainbow_mode; uint32_t rainbow_speed_ms; @@ -88,9 +82,6 @@ void rgb_backlight_settings_load(RGBBacklightSettings* settings) { settings->brightness = 1.0f; settings->rainbow_speed_ms = 100; settings->rainbow_step = 1; - settings->custom_red=255; - settings->custom_green = 255; - settings->custom_blue=255; rgb_backlight_settings_save(settings); } } diff --git a/applications/services/rgb_backlight/rgb_backlight_settings.h b/applications/services/rgb_backlight/rgb_backlight_settings.h index 694161c2b..fe504879f 100644 --- a/applications/services/rgb_backlight/rgb_backlight_settings.h +++ b/applications/services/rgb_backlight/rgb_backlight_settings.h @@ -10,9 +10,9 @@ typedef struct { float brightness; // static gradient mode settings - uint8_t static_vd1_index; - uint8_t static_vd2_index; - uint8_t static_vd3_index; + uint8_t led_2_color_index; + uint8_t led_1_color_index; + uint8_t led_0_color_index; // rainbow mode setings uint32_t rainbow_mode; diff --git a/applications/settings/notification_settings/notification_settings_app.c b/applications/settings/notification_settings/notification_settings_app.c index 1a738b492..75f462ce4 100644 --- a/applications/settings/notification_settings/notification_settings_app.c +++ b/applications/settings/notification_settings/notification_settings_app.c @@ -16,7 +16,7 @@ typedef struct { VariableItemList* variable_item_list_rgb; } NotificationAppSettings; -static VariableItem* temp_item; +//static VariableItem* temp_item; static const NotificationSequence sequence_note_c = { &message_note_c5, @@ -236,12 +236,33 @@ static void rgb_backlight_installed_changed(VariableItem* item) { variable_item_set_current_value_text(item, rgb_backlight_installed_text[index]); app->notification->rgb_srv->settings->rgb_backlight_installed = rgb_backlight_installed_value[index]; rgb_backlight_settings_save(app->notification->rgb_srv->settings); + + // In case of user playing with rgb_backlight_installed swith: + // if user swith_off rgb_backlight_installed then force set default orange color + if (index == 0) { + rgb_backlight_set_led_static_color (2,0); + rgb_backlight_set_led_static_color (1,0); + rgb_backlight_set_led_static_color (0,0); + SK6805_update(); + // if user swith_on rgb_backlight_installed then start rainbow if its ON or set saved static colors + } else { + + if (app->notification->rgb_srv->settings->rainbow_mode >0) { + rainbow_timer_starter (app->notification->rgb_srv); + } else { + rgb_backlight_set_led_static_color (2,app->notification->rgb_srv->settings->led_2_color_index); + rgb_backlight_set_led_static_color (1,app->notification->rgb_srv->settings->led_1_color_index); + rgb_backlight_set_led_static_color (0,app->notification->rgb_srv->settings->led_0_color_index); + rgb_backlight_update (app->notification->settings.display_brightness); + } + } + // Lock/Unlock all rgb settings depent from rgb_backlight_installed switch int slide = 0; if(furi_hal_rtc_is_flag_set(FuriHalRtcFlagDebug)) { slide = 1; } - for(int i = slide; i < (slide + 7); i++) { + for(int i = slide; i < (slide + 6); i++) { VariableItem* t_item = variable_item_list_get(app->variable_item_list_rgb, i); if(index == 0) { variable_item_set_locked(t_item, true, "RGB\nOFF!"); @@ -251,6 +272,54 @@ static void rgb_backlight_installed_changed(VariableItem* item) { } } +static void led_2_color_changed(VariableItem* item) { + NotificationAppSettings* app = variable_item_get_context(item); + uint8_t index = variable_item_get_current_value_index(item); + + variable_item_set_current_value_text(item, rgb_backlight_get_color_text(index)); + app->notification->rgb_srv->settings->led_2_color_index = index; + + rgb_backlight_set_led_static_color(2,index); + rgb_backlight_update(app->notification->rgb_srv->settings->brightness); + + // dont update display color if rainbow working + if (!furi_timer_is_running(app->notification->rgb_srv->rainbow_timer)) { + rgb_backlight_settings_save(app->notification->rgb_srv->settings); + } +} + +static void led_1_color_changed(VariableItem* item) { + NotificationAppSettings* app = variable_item_get_context(item); + uint8_t index = variable_item_get_current_value_index(item); + + variable_item_set_current_value_text(item, rgb_backlight_get_color_text(index)); + app->notification->rgb_srv->settings->led_1_color_index = index; + + rgb_backlight_set_led_static_color(1,index); + rgb_backlight_settings_save(app->notification->rgb_srv->settings); + + // dont update display color if rainbow working + if (!furi_timer_is_running(app->notification->rgb_srv->rainbow_timer)) { + rgb_backlight_settings_save(app->notification->rgb_srv->settings); + } +} + +static void led_0_color_changed(VariableItem* item) { + NotificationAppSettings* app = variable_item_get_context(item); + uint8_t index = variable_item_get_current_value_index(item); + + variable_item_set_current_value_text(item, rgb_backlight_get_color_text(index)); + app->notification->rgb_srv->settings->led_0_color_index = index; + + rgb_backlight_set_led_static_color(0,index); + rgb_backlight_settings_save(app->notification->rgb_srv->settings); + + // dont update display color if rainbow working + if (!furi_timer_is_running(app->notification->rgb_srv->rainbow_timer)) { + rgb_backlight_settings_save(app->notification->rgb_srv->settings); + } +} + static void rgb_backlight_rainbow_changed(VariableItem* item) { NotificationAppSettings* app = variable_item_get_context(item); uint8_t index = variable_item_get_current_value_index(item); @@ -263,7 +332,9 @@ static void rgb_backlight_rainbow_changed(VariableItem* item) { // restore saved rgb backlight settings if we switch_off rainbow mode if(app->notification->rgb_srv->settings->rainbow_mode == 0) { - rgb_backlight_set_static_color(app->notification->rgb_srv->settings->static_color_index); + rgb_backlight_set_led_static_color (2,app->notification->rgb_srv->settings->led_2_color_index); + rgb_backlight_set_led_static_color (1,app->notification->rgb_srv->settings->led_1_color_index); + rgb_backlight_set_led_static_color (0,app->notification->rgb_srv->settings->led_0_color_index); rgb_backlight_update(app->notification->rgb_srv->settings->brightness); } } @@ -292,42 +363,6 @@ static void rgb_backlight_rainbow_step_changed(VariableItem* item) { } -static void vd1_color_changed(VariableItem* item) { - NotificationAppSettings* app = variable_item_get_context(item); - uint8_t index = variable_item_get_current_value_index(item); - - variable_item_set_current_value_text(item, rgb_backlight_get_color_text(index)); - app->notification->rgb_srv->settings->static_color_index = index; - - rgb_backlight_set_static_color(index); - rgb_backlight_update(app->notification->rgb_srv->settings->brightness); - rgb_backlight_settings_save(app->notification->rgb_srv->settings); -} - -static void vd2_color_changed(VariableItem* item) { - NotificationAppSettings* app = variable_item_get_context(item); - uint8_t index = variable_item_get_current_value_index(item); - - variable_item_set_current_value_text(item, rgb_backlight_get_color_text(index)); - app->notification->rgb_srv->settings->static_color_index = index; - - rgb_backlight_set_static_color(index); - rgb_backlight_update(app->notification->rgb_srv->settings->brightness); - rgb_backlight_settings_save(app->notification->rgb_srv->settings); -} - -static void vd3_color_changed(VariableItem* item) { - NotificationAppSettings* app = variable_item_get_context(item); - uint8_t index = variable_item_get_current_value_index(item); - - variable_item_set_current_value_text(item, rgb_backlight_get_color_text(index)); - app->notification->rgb_srv->settings->static_color_index = index; - - rgb_backlight_set_static_color(index); - rgb_backlight_update(app->notification->rgb_srv->settings->brightness); - rgb_backlight_settings_save(app->notification->rgb_srv->settings); -} - // open rgb_settings_view if user press OK on first (index=0) menu string and (debug mode or rgb_backlight_installed is true) void variable_item_list_enter_callback(void* context, uint32_t index) { UNUSED(context); @@ -458,38 +493,50 @@ static NotificationAppSettings* alloc_settings(void) { variable_item_set_current_value_text(item, rgb_backlight_installed_text[value_index]); } - // vd1 color + // led_1 color item = variable_item_list_add( app->variable_item_list_rgb, - "VD1 Color", + "LED 1 Color", rgb_backlight_get_color_count(), - vd1_color_changed, + led_2_color_changed, app); - value_index = app->notification->rgb_srv->settings->static_color_index; + value_index = app->notification->rgb_srv->settings->led_2_color_index; variable_item_set_current_value_index(item, value_index); variable_item_set_current_value_text(item, rgb_backlight_get_color_text(value_index)); + variable_item_set_locked( + item, + (app->notification->rgb_srv->settings->rgb_backlight_installed == 0), + "RGB MOD \nOFF!"); - // vd2 color + // led_2 color item = variable_item_list_add( app->variable_item_list_rgb, - "VD2 Color", + "LED 2 Color", rgb_backlight_get_color_count(), - vd2_color_changed, + led_1_color_changed, app); - value_index = app->notification->rgb_srv->settings->static_color_index; + value_index = app->notification->rgb_srv->settings->led_1_color_index; variable_item_set_current_value_index(item, value_index); variable_item_set_current_value_text(item, rgb_backlight_get_color_text(value_index)); + variable_item_set_locked( + item, + (app->notification->rgb_srv->settings->rgb_backlight_installed == 0), + "RGB MOD \nOFF!"); - // vd 3 color + // led 3 color item = variable_item_list_add( app->variable_item_list_rgb, - "VD3 Color", + "LED 3 Color", rgb_backlight_get_color_count(), - vd3_color_changed, + led_0_color_changed, app); - value_index = app->notification->rgb_srv->settings->static_color_index; + value_index = app->notification->rgb_srv->settings->led_0_color_index; variable_item_set_current_value_index(item, value_index); variable_item_set_current_value_text(item, rgb_backlight_get_color_text(value_index)); + variable_item_set_locked( + item, + (app->notification->rgb_srv->settings->rgb_backlight_installed == 0), + "RGB MOD \nOFF!"); // Rainbow mode item = variable_item_list_add( diff --git a/targets/f7/api_symbols.csv b/targets/f7/api_symbols.csv index ce3992704..9cc8e15f6 100755 --- a/targets/f7/api_symbols.csv +++ b/targets/f7/api_symbols.csv @@ -1,5 +1,5 @@ entry,status,name,type,params -Version,+,83.1,, +Version,+,86.0,, Header,+,applications/drivers/subghz/cc1101_ext/cc1101_ext_interconnect.h,, Header,+,applications/services/bt/bt_service/bt.h,, Header,+,applications/services/bt/bt_service/bt_keys_storage.h,, @@ -3148,8 +3148,7 @@ Function,-,renameat,int,"int, const char*, int, const char*" Function,-,rewind,void,FILE* Function,+,rgb_backlight_get_color_count,uint8_t, Function,+,rgb_backlight_get_color_text,const char*,uint8_t -Function,+,rgb_backlight_set_custom_color,void,"uint8_t, uint8_t, uint8_t" -Function,+,rgb_backlight_set_static_color,void,uint8_t +Function,+,rgb_backlight_set_led_static_color,void,"uint8_t, uint8_t" Function,+,rgb_backlight_settings_load,void,RGBBacklightSettings* Function,+,rgb_backlight_settings_save,void,const RGBBacklightSettings* Function,+,rgb_backlight_update,void,float From 02dedd60f3c757a44647e1d342721e735e1942e1 Mon Sep 17 00:00:00 2001 From: Ruslan Nadyrshin <110516632+rnadyrshin@users.noreply.github.com> Date: Tue, 18 Mar 2025 20:08:23 +0400 Subject: [PATCH 134/268] Add guides on Getting Started with JS (#4150) - Get started section added to the JS docs - Small fixes in the JS modules docs --- applications/system/js_app/js_modules.h | 5 +- documentation/doxygen/Doxyfile.cfg | 2 +- documentation/doxygen/js.dox | 47 +++++----- documentation/images/js_first_app_on_cli.jpg | Bin 0 -> 190222 bytes documentation/images/js_first_app_on_fz.jpg | Bin 0 -> 145295 bytes .../images/js_sdk_code_completion.jpg | Bin 0 -> 306903 bytes documentation/images/js_sdk_npm_start.jpg | Bin 0 -> 351477 bytes documentation/js/js_about.md | 16 ++++ documentation/js/js_badusb.md | 13 ++- .../js/js_developing_apps_using_js_sdk.md | 77 ++++++++++++++++ documentation/js/js_event_loop.md | 5 +- documentation/js/js_gpio.md | 22 +++-- documentation/js/js_gui.md | 65 ++++++++------ documentation/js/js_gui__dialog.md | 9 +- documentation/js/js_gui__empty_screen.md | 7 +- documentation/js/js_gui__loading.md | 6 +- documentation/js/js_gui__submenu.md | 9 +- documentation/js/js_gui__text_box.md | 5 +- documentation/js/js_gui__text_input.md | 13 ++- documentation/js/js_gui__widget.md | 7 +- documentation/js/js_math.md | 5 +- documentation/js/js_notification.md | 9 +- documentation/js/js_serial.md | 19 ++-- documentation/js/js_using_js_modules.md | 42 +++++++++ documentation/js/js_your_first_js_app.md | 83 ++++++++++++++++++ 25 files changed, 343 insertions(+), 123 deletions(-) create mode 100644 documentation/images/js_first_app_on_cli.jpg create mode 100644 documentation/images/js_first_app_on_fz.jpg create mode 100644 documentation/images/js_sdk_code_completion.jpg create mode 100644 documentation/images/js_sdk_npm_start.jpg create mode 100644 documentation/js/js_about.md create mode 100644 documentation/js/js_developing_apps_using_js_sdk.md create mode 100644 documentation/js/js_using_js_modules.md create mode 100644 documentation/js/js_your_first_js_app.md diff --git a/applications/system/js_app/js_modules.h b/applications/system/js_app/js_modules.h index 2babe231e..892e43d4e 100644 --- a/applications/system/js_app/js_modules.h +++ b/applications/system/js_app/js_modules.h @@ -25,14 +25,13 @@ /** * @brief Syntax sugar for constructing an object * - * @example - * ```c + * Example: + * * mjs_val_t my_obj = mjs_mk_object(mjs); * JS_ASSIGN_MULTI(mjs, my_obj) { * JS_FIELD("method1", MJS_MK_FN(js_storage_file_is_open)); * JS_FIELD("method2", MJS_MK_FN(js_storage_file_is_open)); * } - * ``` */ #define JS_ASSIGN_MULTI(mjs, object) \ for(struct { \ diff --git a/documentation/doxygen/Doxyfile.cfg b/documentation/doxygen/Doxyfile.cfg index 3df12f08f..e229209de 100644 --- a/documentation/doxygen/Doxyfile.cfg +++ b/documentation/doxygen/Doxyfile.cfg @@ -759,7 +759,7 @@ GENERATE_BUGLIST = YES # the documentation. # The default value is: YES. -GENERATE_DEPRECATEDLIST= YES +GENERATE_DEPRECATEDLIST= NO # The ENABLED_SECTIONS tag can be used to enable conditional documentation # sections, marked by \if ... \endif and \cond diff --git a/documentation/doxygen/js.dox b/documentation/doxygen/js.dox index f5c609dd1..71eb37b17 100644 --- a/documentation/doxygen/js.dox +++ b/documentation/doxygen/js.dox @@ -1,30 +1,37 @@ /** @page js JavaScript -This page contains some information on the Flipper Zero scripting engine, which is based on a modified mJS library. +Flipper Zero's built-in JavaScript engine enables you to run lightweight scripts, similar to full-fledged C/C++ apps. Scripts can be shared, copied to a microSD card, and launched directly from the Flipper Zero menu — no precompilation needed. -- [Brief mJS description](https://github.com/cesanta/mjs/blob/master/README.md) -- @subpage js_data_types -- @subpage js_builtin +## Get started with JavaScript -## JavaScript modules +- @subpage js_about_js_engine — Learn about the implementation, advantages and limitations of our JavaScript engine -JS modules use the Flipper app plugin system. Each module is compiled into a `.fal` library file and is located on a microSD card. Here is a list of implemented modules: +- @subpage js_your_first_js_app — Create a simple app and run it using the Flipper Zero UI or CLI -- @subpage js_badusb - BadUSB module -- @subpage js_serial - Serial module -- @subpage js_math - Math module -- @subpage js_notification - Notifications module -- @subpage js_event_loop - Event Loop module -- @subpage js_gpio - GPIO module -- @subpage js_gui - GUI module and its submodules: - - @subpage js_gui__submenu - Submenu view - - @subpage js_gui__loading - Hourglass (Loading) view - - @subpage js_gui__empty_screen - Empty view - - @subpage js_gui__text_input - Keyboard-like text input - - @subpage js_gui__text_box - Simple multiline text box - - @subpage js_gui__dialog - Dialog with up to 3 options +- @subpage js_developing_apps_using_js_sdk — Learn how to install and use the JavaScript SDK for fast app debugging -All modules have corresponding TypeScript declaration files, so you can set up your IDE to show suggestions when writing JS scripts. +- @subpage js_using_js_modules — Learn how you can use JS modules in your apps + +## JavaScript modules {#js_modules} + +- @subpage js_badusb — This module allows you to emulate a standard USB keyboard +- @subpage js_serial — The module for interaction with external devices via UART +- @subpage js_math — This module contains mathematical methods and constants +- @subpage js_notification — This module allows you to use LED, speaker and vibro for notifications +- @subpage js_event_loop — The module for easy event-based developing +- @subpage js_gpio — This module allows you to control GPIO pins +- @subpage js_gui — This module allows you to use GUI (graphical user interface) + +## Examples {#js_examples} + +- [Our examples (GitHub)](https://github.com/flipperdevices/flipperzero-firmware/tree/dev/applications/system/js_app/examples/apps/Scripts) — Pre-installed with the firmware, so you can run them directly from the Flipper Zero menu (**Apps → Scripts**) +- [Featured: Derek Jamison's examples (GitHub)](https://github.com/jamisonderek/flipper-zero-tutorials/tree/main/js) — Come with detailed video guides for most scripts on [his YouTube channel](https://www.youtube.com/@MrDerekJamison) +- Just google "flipper zero javascript examples" + +## Other resources + +- @subpage js_data_types — A list of data types you can use in your JS scripts +- @subpage js_builtin — A list of functions you can use without including any JS modules */ diff --git a/documentation/images/js_first_app_on_cli.jpg b/documentation/images/js_first_app_on_cli.jpg new file mode 100644 index 0000000000000000000000000000000000000000..a59de6a592805617062a817a4f9f1fcf1ad9726b GIT binary patch literal 190222 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 +k^U5Md4T*%cAq0R3|i>93Zn_W&{ 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 literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_Doom_128x64/frame_35.png b/assets/dolphin/external/L1_Doom_128x64/frame_35.png new file mode 100644 index 0000000000000000000000000000000000000000..fe9e9f81fd1b0cece9ce3dd1231cd0fe1f867760 GIT binary patch literal 1237 zcmV;`1SwtDh}i<|=11s2S^kLj!te zvt)egJ-i|~0_emm0;w%Fqz3i?_&W{o zmP9Zt1hg36+GZE5p@1_!w*Y#L-*e9$Xq~xH zIl$QrN(KtMwOS|j%XtFpg!)KiW&aRs zf$EKwDS_AZPcasSb@_%jYPe;bm#7@zUCQr|(W*|K$`|*7yN8OFfyW}yX0f#|?p{mZ zFUt71gj<&>+H!?5Dwm`@*?UA_^#Iyr{YnC<%?66BSlv_yK2dFnkR;gOhmZK|+!a8o$G7$D5ea{t>JT{}+|CEAfxM?WByBS~lO*qk`LWi`( zldf9{-42evw`G@X6kd<1t+1qiq(rfISMXl|QMKs^RyKSo-^aK-)O#Yl>{90eB3aq+ zUM$5kPhXH_&I3U1WIT>lY7!|qfVEiTjaN@3JDCp80ajwwf{}OjXBNNGasY4PB8#0X zhn_PQz0XL^0W6)5aCN(iAu}AN)&Y7t<+hO{*g*)R4$BP70X!Y!ws7`7O$gca$StR@cWWDh=4C;cW^E|$a)l>?a01K@2$ZpTX7()F@SIIz|SsNFxU4HN-vKYTC1 z`T%Rv=NYus6F$$5U@7t`AM63RzC-H(Q9D$%uZ#mGI|xC7n#J%tEDrd8Y5Ull{p0p& zrC{v`v@%l&5fcS_N9^M9xN*<)fk?(Ew2Hd^yle*5$`maI|zL-UZ7nnt~2rS-!dI#+~mphmXK@m_(#$9JWgqlql zZy8+@0UpQRI>3+|0Hgh=-M}jzh?c!mNFjmir>72ZUDy0S6bXOqu28>0OUkqgA8bPv z;kajcL+x|{+zC&yObdg!4VBQJq9=Lj|b00000NkvXXu0mjf)6zQq literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_Doom_128x64/frame_36.png b/assets/dolphin/external/L1_Doom_128x64/frame_36.png new file mode 100644 index 0000000000000000000000000000000000000000..cc80bc5434c63f72a426e6f727156b26b8ae2ae6 GIT binary patch literal 1288 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 Uz_Q-&rT_o{07*qoM6N<$g6}FMl>h($ literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_Doom_128x64/frame_38.png b/assets/dolphin/external/L1_Doom_128x64/frame_38.png new file mode 100644 index 0000000000000000000000000000000000000000..43e0f8c6938872fd8dc10338d77340cffbe9a38d GIT binary patch literal 1240 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 btiRwJSI;U{*S+3j00000NkvXXu0mjf{QeJ7 literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_Doom_128x64/frame_6.png b/assets/dolphin/external/L1_Doom_128x64/frame_6.png new file mode 100644 index 0000000000000000000000000000000000000000..b724d0c2022023b0dd71af650d8ff26ff0d333e6 GIT binary patch literal 1181 zcmV;O1Y-M%P)d4cNvvC?l;_Y6&6nczl6A zrIfc^d@cS(FW@2;pXdebhW8x{=JPyGE78Fv00Zy)P7O>MlLrCb910E^!crxM~OfrVb*S8#d(*+WTs1(=mw8uL@@d+s$mAT0qXB^L&cVO9)z z3JKB?pfx<#(A4qLa&0Ghryvypu%^psECwnub~l_BQV;-o{c0mcssI#1oaX(d1QUQN z_i)T?-0WV|?@ur;1z`l>$bD-Jt$02HHs^_?2q8c2 ztKnjXN?@p9yKMqhxOW0M-rqvvWgdnZt!viLh{je5z)jR<3q_DfColr2xjZ%D#kAYWHCOxp5on-`%t~#TP51= zDYasfw%vb1fYw84oX4Ami=LEoj3o^ly>XV{C;%6q(WG2hAI9uq6x%t*+9A2r?hdLP z2+=gWb(OF}h~D+1`*FjPch2$2U@%Q!FOfb67Hs=nT}KaCGFU(VvnL-&imequs~*R? zZPb=4b&W75psdA1{!39HKo(i^XhN+dzV$s}29A7pDHH&{oc43>JX}z{U&A9?5@di1 z0W?%#wdd}+L9>Dp3B2XMlq3q!gLTiHTP0{AlM;xl04$DKGq+~eu%v>2miLAKHRts7 zb_}T%V1^Zi$l!f#?&B^0r6?01x+2gB8IjKrp$5x84a?w3JxI4{+vl3Pe6FlWs0aMyQ*1$u7?Sq~{1UCgt=mWS4W;XjT*NsN!J@2U4>Agll|CD#Z_V0Hp|iU`69 zpbtDX(Aev_EwEEVBsA?7dd~3~y370Cl0Zx?V~gv-JY@EP)rM4zM%(u|Nw; z>>sc7ZctkC7DNgMz*Y;`YcS7}8gDdCqvG)*>)UP$_VC3rF`!2j7ZTU@Ql~ejs(1PvV&;py>krxFiSg zDB=3+HQwc07>Nq)01~}>$R?>R-PzcuAngLsSW5?x_+z1iAKXUHsQlAt*qp$s5}OJ@ zqi(NI7E72-oo{rB6__#{6oB@o$jANQC=FC{Vxgqhp#U`g7*61^0mIJzjA^4f zPv=3uK%yK&=SQ!?V(%nIk10j-u z*RB$F1OeSYdmc3{S+6;MA_PnutROJoKgV17-99#woU_KP6bC6)Jzzw_ocSGt8gr4e<^_k;LT{}QO+|^ znl>}60$YL zKoyb4MhtfLudd4&{8EGjAh8yR&~t|vecLI{7s3G`w0rJSHMiC-ss(HT^s`>7B3M1M z#mXUO+p<}$zY^L3R_q9ZD%O$ZXAgcU!UC{xfz|U{I02h|gH+2H!edPlq7kG8cbj(h zU8h)oMLY#W76sSxM-{*+Y;6aB24VKmvtW_?y-G^8MPT+Gd5d=L_hefD(CO96sB{|r zmW7P(JVWeEASU;F9;UpE5laDB5wu7hrnKNh`!A)G{~G1|JJya8BLDyZ07*qoM6N<$ Ef~R`{ivR!s literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_Doom_128x64/frame_8.png b/assets/dolphin/external/L1_Doom_128x64/frame_8.png new file mode 100644 index 0000000000000000000000000000000000000000..a18485d6f511e2e951800e13b367b46804f0145e GIT binary patch literal 1193 zcmV;a1XlZrP)E<*>SmOv{KF6XHu3gab{)B=6A z*6#M=Yw;)h0bb?Z35G04Ppby&iaTfPWZQ z@3)ZO+ADz88Xmt#JXsK5p9FI2`wmGj;Dk_8+5uK2m&g3v`d)g?DJUrbPRWHqLX3sT zCyAhx03h*FqPgR><=ScX&OsRgU`?0VSPWcYJb^ebqzhnN_0~p;)&V$zc;f1BEl~hm zyC27_#;xwf{rxHAa*!ebiQPjoyyE!`*ql!l#dZO7@;SswOSC?J1V+b#&wsKw(sKZB zSi{8%UBIJ*owy0qv9%K@S^W^2Rknke*>kP>85MAh14t9K)p##)%<={9S%OS#a{$Zg z=YmQsxgS32*{HBoK@jB~fLkqa$8hfDH6J!lwS9#x0*F}s+A>mej}e~DstV@^%by6c zWZ4lPcB}xb*bx?2_YW69)Js+=zpP@&5<$POs{lGFBRSbIE&|y8h2#J!HeZ$#xKsgD zDU@w-5YqDk*mV^79-CUba)R+5>LjoVsS;>+3$g3G$=`)v$K9LI0k|(H-R1MR=Mxa= z0@%E42goSl;P{AV`4&*3VmW|E@1C+rX-juD_fz2Q0<*cM4&d=&se>QdMh;f~MKo-l z08S-F3Lv6x?~o)*oK2l?bcq$XWjHE;=uHvJ{M^;QYd6pvRLRMObhGJ~03tq&hIlSv z+|_?#*yv8V5`txa;lCs>BGcE*%RQy5oln~j*n!y@g4J_t9OFj-RmdN&=d5nceYmTC zw@bk8DYs&>wB0}B1Xg&Q$6JL9%E~2VX~PCJE)o(4(BM;vd<|x?T|(9g$z?=$(0m}o zllbT=!6FFi`q}-YVJRc$FbTkeFe|hn1OCl<2Wd~@gv}wf0KCbU%xAIPSq>q67f3b~ zRJHAJ3@QO+t$Zp5TEfsQgO==5U}E5kAX6kIP6cxX#c6PORwA=G#uX38z>P176YM$^ z(oz0U=njk@eI#|2B#uN=Rl_*nE>a~>3&87zq~XMS9Cn>Fr&LqDz7WXe8y|#x~@$liNh-ZB|e{zNU-LaYWS5sfD@o4_PQ?7C92_d00BLr z9KlT>O$4Vn!3A#+$O04Di06pyNk2sd?-szICr~2uMEeJL1wi7I)f<5~2KWYEy-y(@ z;1vJ>(EB&WodxmuAW)m%H&|)`Cu}999Y9LCBHGvH_ttk#K}i8VrGcElL$%) zpeNo+wDx&#x^|kqYfwf2RMBPhECsDF?m%1=(glE5{cIt{>i`--+;Q~-Bnm)l_xKs< zIjJx0|KEjN4N?SPvHPBksQr8ds?EEKV!Hqu`5NM^DOziP1+ZiF<3Cv(=`lbwX15Co zO~BZ}PCNu^*jfp+to|OJRknke(YT~~Mg<(>0M1!j@;8EJqU;qD zi!*>(?AR8i{+A12)kjuIzh=ddC4$y(r~n!%n4IiAZUw0G3(Eyks=X{H2+0DtR4Cix zdRXrZQ15Z%M;vPHiV1KZ?jW!VsTO!o3#s>_$=`(^*WJPB0NM|1A*bO{OCG@KN`>O+y4_O+{j*J)rhxRg^1>tWLm0j!u95Aj;Uw5$Kb zZKFA*B!rpsh5v%U6_LKDUF|JhZ+qT)zz&S=5J=T0X+DsM7{^3z1>3A3Bl!x>frQ+kVxWJ zX9+Wc;NBngXWf=EVvZ>RL=ZL$X~MOF(0oI`A4`Hb;c&<;fN1i)sUj65d8}Sb7AKls z5KRo#Mc}#+Qf1}032y)Jq-((idt9H~JDjjIA#5a9yen){B4jOar%KNmGN|*4>mjzffq-G)ejJGBWM&0jH+s{ zAZj4wPM{_rJUx$(MupW65VQqoMS$Ur(oaVf>u(5)0X!nWc+z%JNd&(E;?-|NxisUV P00000NkvXXu0mjfXqO`W literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_Doom_128x64/meta.txt b/assets/dolphin/external/L1_Doom_128x64/meta.txt new file mode 100644 index 000000000..838239623 --- /dev/null +++ b/assets/dolphin/external/L1_Doom_128x64/meta.txt @@ -0,0 +1,14 @@ +Filetype: Flipper Animation +Version: 1 + +Width: 128 +Height: 64 +Passive frames: 15 +Active frames: 24 +Frames order: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 +Active cycles: 1 +Frame rate: 2 +Duration: 3600 +Active cooldown: 7 + +Bubble slots: 0 \ No newline at end of file diff --git a/assets/dolphin/external/manifest.txt b/assets/dolphin/external/manifest.txt index 0b4b9a426..bd3050b51 100644 --- a/assets/dolphin/external/manifest.txt +++ b/assets/dolphin/external/manifest.txt @@ -10,9 +10,9 @@ Weight: 3 Name: L1_Laptop_128x51 Min butthurt: 0 -Max butthurt: 7 +Max butthurt: 9 Min level: 1 -Max level: 1 +Max level: 3 Weight: 3 Name: L1_Sleep_128x64 @@ -36,6 +36,20 @@ Min level: 1 Max level: 1 Weight: 3 +Name: L2_Furippa2_128x64 +Min butthurt: 0 +Max butthurt: 6 +Min level: 2 +Max level: 2 +Weight: 3 + +Name: L3_Furippa3_128x64 +Min butthurt: 0 +Max butthurt: 6 +Min level: 3 +Max level: 3 +Weight: 3 + Name: L1_Read_books_128x64 Min butthurt: 0 Max butthurt: 8 @@ -43,6 +57,13 @@ Min level: 1 Max level: 1 Weight: 3 +Name: L2_Hacking_pc_128x64 +Min butthurt: 0 +Max butthurt: 8 +Min level: 2 +Max level: 2 +Weight: 3 + Name: L1_Cry_128x64 Min butthurt: 8 Max butthurt: 13 @@ -57,20 +78,6 @@ Min level: 1 Max level: 3 Weight: 3 -Name: L1_Mad_fist_128x64 -Min butthurt: 9 -Max butthurt: 13 -Min level: 1 -Max level: 3 -Weight: 3 - -Name: L1_Mods_128x64 -Min butthurt: 0 -Max butthurt: 9 -Min level: 1 -Max level: 3 -Weight: 3 - Name: L1_Painting_128x64 Min butthurt: 0 Max butthurt: 7 @@ -78,145 +85,26 @@ Min level: 1 Max level: 3 Weight: 3 -Name: L1_Leaving_sad_128x64 -Min butthurt: 14 -Max butthurt: 14 -Min level: 1 -Max level: 3 -Weight: 3 - -Name: L1_Senpai_128x64 -Min butthurt: 0 -Max butthurt: 5 -Min level: 1 -Max level: 3 -Weight: 3 - -Name: L1_Kaiju_128x64 +Name: L1_Mods_128x64 Min butthurt: 0 Max butthurt: 10 Min level: 1 Max level: 3 Weight: 3 -Name: L1_My_dude_128x64 -Min butthurt: 0 -Max butthurt: 8 -Min level: 1 -Max level: 3 -Weight: 3 - -Name: L2_Wake_up_128x64 +Name: L1_Wake_up_128x64 Min butthurt: 0 Max butthurt: 12 Min level: 2 Max level: 3 -Weight: 3 - -Name: L2_Furippa2_128x64 -Min butthurt: 0 -Max butthurt: 6 -Min level: 2 -Max level: 2 -Weight: 3 - -Name: L2_Hacking_pc_128x64 -Min butthurt: 0 -Max butthurt: 8 -Min level: 2 -Max level: 2 -Weight: 3 - -Name: L2_Soldering_128x64 -Min butthurt: 0 -Max butthurt: 10 -Min level: 2 -Max level: 2 -Weight: 3 - -Name: L2_Dj_128x64 -Min butthurt: 0 -Max butthurt: 8 -Min level: 2 -Max level: 3 -Weight: 3 - -Name: L3_Furippa3_128x64 -Min butthurt: 0 -Max butthurt: 6 -Min level: 3 -Max level: 3 -Weight: 3 - -Name: L3_Hijack_radio_128x64 -Min butthurt: 0 -Max butthurt: 8 -Min level: 3 -Max level: 3 -Weight: 3 - -Name: L3_Lab_research_128x54 -Min butthurt: 0 -Max butthurt: 10 -Min level: 3 -Max level: 3 -Weight: 3 - -Name: L1_Sad_song_128x64 -Min butthurt: 8 -Max butthurt: 13 -Min level: 1 -Max level: 3 -Weight: 3 - -Name: L2_Coding_in_the_shell_128x64 -Min butthurt: 0 -Max butthurt: 12 -Min level: 2 -Max level: 3 -Weight: 3 - -Name: L2_Secret_door_128x64 -Min butthurt: 0 -Max butthurt: 12 -Min level: 2 -Max level: 3 -Weight: 3 - -Name: L3_Freedom_2_dolphins_128x64 -Min butthurt: 0 -Max butthurt: 12 -Min level: 3 -Max level: 3 -Weight: 3 - -Name: L1_Akira_128x64 -Min butthurt: 0 -Max butthurt: 8 -Min level: 1 -Max level: 3 -Weight: 3 - -Name: L3_Intruder_alert_128x64 -Min butthurt: 0 -Max butthurt: 12 -Min level: 3 -Max level: 3 -Weight: 3 - -Name: L1_Procrastinating_128x64 -Min butthurt: 0 -Max butthurt: 8 -Min level: 1 -Max level: 3 -Weight: 3 +Weight: 4 Name: L1_Happy_holidays_128x64 Min butthurt: 0 Max butthurt: 14 Min level: 1 Max level: 3 -Weight: 3 +Weight: 4 Name: L1_Sleigh_ride_128x64 Min butthurt: 0 @@ -225,6 +113,83 @@ Min level: 1 Max level: 3 Weight: 4 +Name: L1_Senpai_128x64 +Min butthurt: 0 +Max butthurt: 5 +Min level: 1 +Max level: 3 +Weight: 4 + +Name: L1_Kaiju_128x64 +Min butthurt: 0 +Max butthurt: 10 +Min level: 1 +Max level: 3 +Weight: 4 + +Name: L1_My_dude_128x64 +Min butthurt: 0 +Max butthurt: 8 +Min level: 1 +Max level: 3 +Weight: 4 + +Name: L1_Sad_song_128x64 +Min butthurt: 8 +Max butthurt: 13 +Min level: 1 +Max level: 3 +Weight: 4 + +Name: L2_Coding_in_the_shell_128x64 +Min butthurt: 0 +Max butthurt: 12 +Min level: 2 +Max level: 3 +Weight: 4 + +Name: L2_Secret_door_128x64 +Min butthurt: 0 +Max butthurt: 12 +Min level: 2 +Max level: 3 +Weight: 4 + +Name: L3_Freedom_2_dolphins_128x64 +Min butthurt: 0 +Max butthurt: 12 +Min level: 3 +Max level: 3 +Weight: 4 + +Name: L1_Akira_128x64 +Min butthurt: 0 +Max butthurt: 8 +Min level: 1 +Max level: 3 +Weight: 4 + +Name: L3_Intruder_alert_128x64 +Min butthurt: 0 +Max butthurt: 12 +Min level: 3 +Max level: 3 +Weight: 4 + +Name: L1_Procrastinating_128x64 +Min butthurt: 0 +Max butthurt: 8 +Min level: 1 +Max level: 3 +Weight: 4 + +Name: L1_Hackspace_128x64 +Min butthurt: 0 +Max butthurt: 12 +Min level: 1 +Max level: 3 +Weight: 4 + Name: L1_Showtime_128x64 Min butthurt: 0 Max butthurt: 10 From 6cc49765687089b92e3293db521f0bd860eb9e10 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Fri, 28 Mar 2025 14:10:16 +0300 Subject: [PATCH 156/268] NFC: Fix NDEF parser for MIFARE Classic [ci skip] by Willy-JL in OFW PR 4153 --- .../main/nfc/plugins/supported_cards/ndef.c | 61 +++++++++++-------- lib/nfc/helpers/nfc_data_generator.c | 36 +++-------- lib/toolbox/pretty_format.c | 8 ++- 3 files changed, 49 insertions(+), 56 deletions(-) diff --git a/applications/main/nfc/plugins/supported_cards/ndef.c b/applications/main/nfc/plugins/supported_cards/ndef.c index fb2c4da48..06982e111 100644 --- a/applications/main/nfc/plugins/supported_cards/ndef.c +++ b/applications/main/nfc/plugins/supported_cards/ndef.c @@ -22,6 +22,7 @@ #include #include +#include #define TAG "NDEF" @@ -181,30 +182,34 @@ static bool ndef_get(Ndef* ndef, size_t pos, size_t len, void* buf) { // So the first 93 (31*3) data blocks correspond to 128 real blocks. // Last 128 blocks are 8 sectors: 15 data blocks, 1 sector trailer. // So the last 120 (8*15) data blocks correspond to 128 real blocks. - div_t small_sector_data_blocks = div(pos, MF_CLASSIC_BLOCK_SIZE); + const size_t real_block_data_offset = pos % MF_CLASSIC_BLOCK_SIZE; + size_t small_sector_data_blocks = pos / MF_CLASSIC_BLOCK_SIZE; size_t large_sector_data_blocks = 0; - if(small_sector_data_blocks.quot > 93) { - large_sector_data_blocks = small_sector_data_blocks.quot - 93; - small_sector_data_blocks.quot = 93; + if(small_sector_data_blocks > 93) { + large_sector_data_blocks = small_sector_data_blocks - 93; + small_sector_data_blocks = 93; } - div_t small_sectors = div(small_sector_data_blocks.quot, 3); - size_t real_block = small_sectors.quot * 4 + small_sectors.rem; - if(small_sectors.quot >= 16) { + const size_t small_sector_block_offset = small_sector_data_blocks % 3; + const size_t small_sectors = small_sector_data_blocks / 3; + size_t real_block = small_sectors * 4 + small_sector_block_offset; + if(small_sectors >= 16) { real_block += 4; // Skip MAD2 } if(large_sector_data_blocks) { - div_t large_sectors = div(large_sector_data_blocks, 15); - real_block += large_sectors.quot * 16 + large_sectors.rem; + const size_t large_sector_block_offset = large_sector_data_blocks % 15; + const size_t large_sectors = large_sector_data_blocks / 15; + real_block += large_sectors * 16 + large_sector_block_offset; } - const uint8_t* cur = &ndef->mfc.blocks[real_block].data[small_sector_data_blocks.rem]; + const uint8_t* cur = &ndef->mfc.blocks[real_block].data[real_block_data_offset]; while(len) { size_t sector_trailer = mf_classic_get_sector_trailer_num_by_block(real_block); const uint8_t* end = &ndef->mfc.blocks[sector_trailer].data[0]; - size_t chunk_len = MIN((size_t)(end - cur), len); + const size_t chunk_len = MIN((size_t)(end - cur), len); memcpy(buf, cur, chunk_len); + buf += chunk_len; len -= chunk_len; if(len) { @@ -244,7 +249,9 @@ static inline bool is_printable(char c) { static bool is_text(const uint8_t* buf, size_t len) { for(size_t i = 0; i < len; i++) { - if(!is_printable(buf[i])) return false; + if(!is_printable(buf[i]) && !(buf[i] == '\0' && i == len - 1)) { + return false; + } } return true; } @@ -260,7 +267,7 @@ static bool ndef_dump(Ndef* ndef, const char* prefix, size_t pos, size_t len, bo for(size_t i = 0; i < len; i++) { char c; if(!ndef_get(ndef, pos + i, 1, &c)) return false; - if(!is_printable(c)) { + if(!is_printable(c) && !(c == '\0' && i == len - 1)) { furi_string_left(ndef->output, string_prev); force_hex = true; break; @@ -268,14 +275,18 @@ static bool ndef_dump(Ndef* ndef, const char* prefix, size_t pos, size_t len, bo furi_string_push_back(ndef->output, c); } } - if(force_hex) { - for(size_t i = 0; i < len; i++) { - uint8_t b; - if(!ndef_get(ndef, pos + i, 1, &b)) return false; - furi_string_cat_printf(ndef->output, "%02X ", b); + if(!force_hex) { + furi_string_cat(ndef->output, "\n"); + } else { + uint8_t buf[4]; + for(size_t i = 0; i < len; i += sizeof(buf)) { + uint8_t buf_len = MIN(sizeof(buf), len - i); + if(!ndef_get(ndef, pos + i, buf_len, &buf)) return false; + pretty_format_bytes_hex_canonical( + ndef->output, 4, PRETTY_FORMAT_FONT_MONOSPACE, buf, buf_len); + furi_string_cat(ndef->output, "\n"); } } - furi_string_cat(ndef->output, "\n"); return true; } @@ -285,9 +296,7 @@ static void if(!force_hex && is_text(buf, len)) { furi_string_cat_printf(ndef->output, "%.*s", len, (const char*)buf); } else { - for(size_t i = 0; i < len; i++) { - furi_string_cat_printf(ndef->output, "%02X ", ((const uint8_t*)buf)[i]); - } + pretty_format_bytes_hex_canonical(ndef->output, 4, PRETTY_FORMAT_FONT_MONOSPACE, buf, len); } furi_string_cat(ndef->output, "\n"); } @@ -582,7 +591,7 @@ bool ndef_parse_record( NdefTnf tnf, const char* type, uint8_t type_len) { - FURI_LOG_D(TAG, "payload type: %.*s len: %hu", type_len, type, len); + FURI_LOG_D(TAG, "payload type: %.*s len: %hu pos: %zu", type_len, type, len, pos); if(!len) { furi_string_cat(ndef->output, "Empty\n"); return true; @@ -887,13 +896,13 @@ static bool ndef_mfc_parse(const NfcDevice* device, FuriString* parsed_data) { for(uint8_t mad = 0; mad < COUNT_OF(mads); mad++) { const size_t block = mads[mad].block; const size_t sector = mf_classic_get_sector_by_block(block); - if(sector_count <= sector) break; // Skip this MAD if not present + if(sector_count <= sector) continue; // Skip this MAD if not present // Check MAD key const MfClassicSectorTrailer* sector_trailer = mf_classic_get_sector_trailer_by_sector(data, sector); const uint64_t sector_key_a = bit_lib_bytes_to_num_be( sector_trailer->key_a.data, COUNT_OF(sector_trailer->key_a.data)); - if(sector_key_a != mad_key) return false; + if(sector_key_a != mad_key) continue; // Find NDEF AIDs for(uint8_t aid_index = 0; aid_index < mads[mad].aid_count; aid_index++) { const uint8_t* aid = &data->block[block].data[2 + aid_index * AID_SIZE]; @@ -917,7 +926,7 @@ static bool ndef_mfc_parse(const NfcDevice* device, FuriString* parsed_data) { data_size = 93 + (sector_count - 32) * 15; } else { data_size = sector_count * 3; - if(sector_count >= 16) { + if(sector_count > 16) { data_size -= 3; // Skip MAD2 } } diff --git a/lib/nfc/helpers/nfc_data_generator.c b/lib/nfc/helpers/nfc_data_generator.c index 7914c1f7f..2143f0f5f 100644 --- a/lib/nfc/helpers/nfc_data_generator.c +++ b/lib/nfc/helpers/nfc_data_generator.c @@ -392,37 +392,15 @@ static void nfc_generate_mf_classic(NfcDevice* nfc_device, uint8_t uid_len, MfCl mf_classic_set_block_read(mfc_data, 0, &mfc_data->block[0]); + // Set every block to 0x00 uint16_t block_num = mf_classic_get_total_block_num(type); - if(type == MfClassicType4k) { - // Set every block to 0x00 - for(uint16_t i = 1; i < block_num; i++) { - if(mf_classic_is_sector_trailer(i)) { - nfc_generate_mf_classic_sector_trailer(mfc_data, i); - } else { - memset(&mfc_data->block[i].data, 0x00, 16); - } - mf_classic_set_block_read(mfc_data, i, &mfc_data->block[i]); - } - } else if(type == MfClassicType1k) { - // Set every block to 0x00 - for(uint16_t i = 1; i < block_num; i++) { - if(mf_classic_is_sector_trailer(i)) { - nfc_generate_mf_classic_sector_trailer(mfc_data, i); - } else { - memset(&mfc_data->block[i].data, 0x00, 16); - } - mf_classic_set_block_read(mfc_data, i, &mfc_data->block[i]); - } - } else if(type == MfClassicTypeMini) { - // Set every block to 0x00 - for(uint16_t i = 1; i < block_num; i++) { - if(mf_classic_is_sector_trailer(i)) { - nfc_generate_mf_classic_sector_trailer(mfc_data, i); - } else { - memset(&mfc_data->block[i].data, 0x00, 16); - } - mf_classic_set_block_read(mfc_data, i, &mfc_data->block[i]); + for(uint16_t i = 1; i < block_num; i++) { + if(mf_classic_is_sector_trailer(i)) { + nfc_generate_mf_classic_sector_trailer(mfc_data, i); + } else { + memset(&mfc_data->block[i].data, 0x00, MF_CLASSIC_BLOCK_SIZE); } + mf_classic_set_block_read(mfc_data, i, &mfc_data->block[i]); } nfc_generate_mf_classic_block_0( diff --git a/lib/toolbox/pretty_format.c b/lib/toolbox/pretty_format.c index f8319b69d..496738c4d 100644 --- a/lib/toolbox/pretty_format.c +++ b/lib/toolbox/pretty_format.c @@ -36,11 +36,17 @@ void pretty_format_bytes_hex_canonical( } const size_t begin_idx = i; - const size_t end_idx = MIN(i + num_places, data_size); + const size_t wrap_idx = i + num_places; + const size_t end_idx = MIN(wrap_idx, data_size); for(size_t j = begin_idx; j < end_idx; j++) { furi_string_cat_printf(result, "%02X ", data[j]); } + if(end_idx < wrap_idx) { + for(size_t j = end_idx; j < wrap_idx; j++) { + furi_string_cat_printf(result, " "); + } + } furi_string_push_back(result, '|'); From bfe9f206500c9efa719cb49b0da10a5ed13451ea Mon Sep 17 00:00:00 2001 From: doomwastaken Date: Fri, 28 Mar 2025 14:11:22 +0300 Subject: [PATCH 157/268] added doom animation to manifest --- assets/dolphin/external/manifest.txt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/assets/dolphin/external/manifest.txt b/assets/dolphin/external/manifest.txt index bd3050b51..67c5c1eb6 100644 --- a/assets/dolphin/external/manifest.txt +++ b/assets/dolphin/external/manifest.txt @@ -141,6 +141,13 @@ Min level: 1 Max level: 3 Weight: 4 +Name: L1_Doom_128x64 +Min butthurt: 0 +Max butthurt: 13 +Min level: 1 +Max level: 3 +Weight: 4 + Name: L2_Coding_in_the_shell_128x64 Min butthurt: 0 Max butthurt: 12 From dd3a3a02c99fe4b77e9c524a61db7714370e7103 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Fri, 28 Mar 2025 14:13:31 +0300 Subject: [PATCH 158/268] NFC: Support DESFire Transaction MAC file type [ci skip] by Willy-JL in OFW PR 4159 --- .../mf_desfire/mf_desfire_render.c | 12 +++ lib/nfc/protocols/mf_desfire/mf_desfire.h | 6 ++ lib/nfc/protocols/mf_desfire/mf_desfire_i.c | 82 ++++++++++++++++--- .../mf_desfire/mf_desfire_poller_i.c | 2 +- 4 files changed, 89 insertions(+), 13 deletions(-) diff --git a/applications/main/nfc/helpers/protocol_support/mf_desfire/mf_desfire_render.c b/applications/main/nfc/helpers/protocol_support/mf_desfire/mf_desfire_render.c index 783cbb871..96e4a30f9 100644 --- a/applications/main/nfc/helpers/protocol_support/mf_desfire/mf_desfire_render.c +++ b/applications/main/nfc/helpers/protocol_support/mf_desfire/mf_desfire_render.c @@ -180,6 +180,9 @@ void nfc_render_mf_desfire_file_settings_data( case MfDesfireFileTypeCyclicRecord: type = "cyclic"; break; + case MfDesfireFileTypeTransactionMac: + type = "txn-mac"; + break; default: type = "unknown"; } @@ -237,6 +240,15 @@ void nfc_render_mf_desfire_file_settings_data( furi_string_cat_printf(str, "size %lu\n", record_size); furi_string_cat_printf(str, "num %lu max %lu\n", record_count, settings->record.max); break; + case MfDesfireFileTypeTransactionMac: + record_count = 0; + furi_string_cat_printf( + str, + "key opt %02X ver %02X\n", + settings->transaction_mac.key_option, + settings->transaction_mac.key_version); + furi_string_cat_printf(str, "cnt limit %lu\n", settings->transaction_mac.counter_limit); + break; } bool is_auth_required = true; diff --git a/lib/nfc/protocols/mf_desfire/mf_desfire.h b/lib/nfc/protocols/mf_desfire/mf_desfire.h index fb50008db..ec60b336b 100644 --- a/lib/nfc/protocols/mf_desfire/mf_desfire.h +++ b/lib/nfc/protocols/mf_desfire/mf_desfire.h @@ -97,6 +97,7 @@ typedef enum { MfDesfireFileTypeValue = 2, MfDesfireFileTypeLinearRecord = 3, MfDesfireFileTypeCyclicRecord = 4, + MfDesfireFileTypeTransactionMac = 5, } MfDesfireFileType; typedef enum { @@ -128,6 +129,11 @@ typedef struct { uint32_t max; uint32_t cur; } record; + struct { + uint8_t key_option; + uint8_t key_version; + uint32_t counter_limit; + } transaction_mac; }; } MfDesfireFileSettings; diff --git a/lib/nfc/protocols/mf_desfire/mf_desfire_i.c b/lib/nfc/protocols/mf_desfire/mf_desfire_i.c index d83a91ad1..eba9c4312 100644 --- a/lib/nfc/protocols/mf_desfire/mf_desfire_i.c +++ b/lib/nfc/protocols/mf_desfire/mf_desfire_i.c @@ -1,5 +1,7 @@ #include "mf_desfire_i.h" +#include + #define TAG "MfDesfire" #define BITS_IN_BYTE (8U) @@ -47,6 +49,10 @@ #define MF_DESFIRE_FFF_FILE_MAX_KEY "Max" #define MF_DESFIRE_FFF_FILE_CUR_KEY "Cur" +#define MF_DESFIRE_FFF_FILE_KEY_OPTION_KEY "Key Option" +#define MF_DESFIRE_FFF_FILE_KEY_VERSION_KEY "Key Version" +#define MF_DESFIRE_FFF_FILE_COUNTER_LIMIT_KEY "Counter Limit" + bool mf_desfire_version_parse(MfDesfireVersion* data, const BitBuffer* buf) { const bool can_parse = bit_buffer_get_size_bytes(buf) == sizeof(MfDesfireVersion); @@ -168,12 +174,19 @@ bool mf_desfire_file_settings_parse(MfDesfireFileSettings* data, const BitBuffer uint32_t cur : 3 * BITS_IN_BYTE; } MfDesfireFileSettingsRecord; + typedef struct FURI_PACKED { + uint8_t key_option; + uint8_t key_version; + uint8_t counter_limit[]; + } MfDesfireFileSettingsTransactionMac; + typedef struct FURI_PACKED { MfDesfireFileSettingsHeader header; union { MfDesfireFileSettingsData data; MfDesfireFileSettingsValue value; MfDesfireFileSettingsRecord record; + MfDesfireFileSettingsTransactionMac transaction_mac; }; } MfDesfireFileSettingsLayout; @@ -182,7 +195,7 @@ bool mf_desfire_file_settings_parse(MfDesfireFileSettings* data, const BitBuffer const size_t data_size = bit_buffer_get_size_bytes(buf); const uint8_t* data_ptr = bit_buffer_get_data(buf); const size_t min_data_size = - sizeof(MfDesfireFileSettingsHeader) + sizeof(MfDesfireFileSettingsData); + sizeof(MfDesfireFileSettingsHeader) + sizeof(MfDesfireFileSettingsTransactionMac); if(data_size < min_data_size) { FURI_LOG_E( @@ -202,17 +215,11 @@ bool mf_desfire_file_settings_parse(MfDesfireFileSettings* data, const BitBuffer if(file_settings_temp.type == MfDesfireFileTypeStandard || file_settings_temp.type == MfDesfireFileTypeBackup) { - memcpy( - &layout.data, - &data_ptr[sizeof(MfDesfireFileSettingsHeader)], - sizeof(MfDesfireFileSettingsData)); + memcpy(&layout.data, &data_ptr[bytes_processed], sizeof(MfDesfireFileSettingsData)); file_settings_temp.data.size = layout.data.size; bytes_processed += sizeof(MfDesfireFileSettingsData); } else if(file_settings_temp.type == MfDesfireFileTypeValue) { - memcpy( - &layout.value, - &data_ptr[sizeof(MfDesfireFileSettingsHeader)], - sizeof(MfDesfireFileSettingsValue)); + memcpy(&layout.value, &data_ptr[bytes_processed], sizeof(MfDesfireFileSettingsValue)); file_settings_temp.value.lo_limit = layout.value.lo_limit; file_settings_temp.value.hi_limit = layout.value.hi_limit; file_settings_temp.value.limited_credit_value = layout.value.limited_credit_value; @@ -222,13 +229,34 @@ bool mf_desfire_file_settings_parse(MfDesfireFileSettings* data, const BitBuffer file_settings_temp.type == MfDesfireFileTypeLinearRecord || file_settings_temp.type == MfDesfireFileTypeCyclicRecord) { memcpy( - &layout.record, - &data_ptr[sizeof(MfDesfireFileSettingsHeader)], - sizeof(MfDesfireFileSettingsRecord)); + &layout.record, &data_ptr[bytes_processed], sizeof(MfDesfireFileSettingsRecord)); file_settings_temp.record.size = layout.record.size; file_settings_temp.record.max = layout.record.max; file_settings_temp.record.cur = layout.record.cur; bytes_processed += sizeof(MfDesfireFileSettingsRecord); + } else if(file_settings_temp.type == MfDesfireFileTypeTransactionMac) { + const bool has_counter_limit = (layout.header.comm & 0x20) != 0; + memcpy( + &layout.transaction_mac, + &data_ptr[bytes_processed], + sizeof(MfDesfireFileSettingsTransactionMac)); + file_settings_temp.transaction_mac.key_option = layout.transaction_mac.key_option; + file_settings_temp.transaction_mac.key_version = layout.transaction_mac.key_version; + if(!has_counter_limit) { + file_settings_temp.transaction_mac.counter_limit = 0; + } else { + // AES (4b) or LRP (2b) + const size_t counter_limit_size = (layout.transaction_mac.key_option & 0x02) ? 4 : + 2; + memcpy( + &layout.transaction_mac, + &data_ptr[bytes_processed], + sizeof(MfDesfireFileSettingsTransactionMac) + counter_limit_size); + file_settings_temp.transaction_mac.counter_limit = bit_lib_bytes_to_num_be( + layout.transaction_mac.counter_limit, counter_limit_size); + bytes_processed += counter_limit_size; + } + bytes_processed += sizeof(MfDesfireFileSettingsTransactionMac); } else { FURI_LOG_W(TAG, "Unknown file type: %02x", file_settings_temp.type); break; @@ -468,6 +496,21 @@ bool mf_desfire_file_settings_load( furi_string_printf(key, "%s %s", prefix, MF_DESFIRE_FFF_FILE_CUR_KEY); if(!flipper_format_read_uint32(ff, furi_string_get_cstr(key), &data->record.cur, 1)) break; + } else if(data->type == MfDesfireFileTypeTransactionMac) { + furi_string_printf(key, "%s %s", prefix, MF_DESFIRE_FFF_FILE_KEY_OPTION_KEY); + if(!flipper_format_read_hex( + ff, furi_string_get_cstr(key), &data->transaction_mac.key_option, 1)) + break; + + furi_string_printf(key, "%s %s", prefix, MF_DESFIRE_FFF_FILE_KEY_VERSION_KEY); + if(!flipper_format_read_hex( + ff, furi_string_get_cstr(key), &data->transaction_mac.key_version, 1)) + break; + + furi_string_printf(key, "%s %s", prefix, MF_DESFIRE_FFF_FILE_COUNTER_LIMIT_KEY); + if(!flipper_format_read_uint32( + ff, furi_string_get_cstr(key), &data->transaction_mac.counter_limit, 1)) + break; } success = true; @@ -716,6 +759,21 @@ bool mf_desfire_file_settings_save( furi_string_printf(key, "%s %s", prefix, MF_DESFIRE_FFF_FILE_CUR_KEY); if(!flipper_format_write_uint32(ff, furi_string_get_cstr(key), &data->record.cur, 1)) break; + } else if(data->type == MfDesfireFileTypeTransactionMac) { + furi_string_printf(key, "%s %s", prefix, MF_DESFIRE_FFF_FILE_KEY_OPTION_KEY); + if(!flipper_format_write_hex( + ff, furi_string_get_cstr(key), &data->transaction_mac.key_option, 1)) + break; + + furi_string_printf(key, "%s %s", prefix, MF_DESFIRE_FFF_FILE_KEY_VERSION_KEY); + if(!flipper_format_write_hex( + ff, furi_string_get_cstr(key), &data->transaction_mac.key_version, 1)) + break; + + furi_string_printf(key, "%s %s", prefix, MF_DESFIRE_FFF_FILE_COUNTER_LIMIT_KEY); + if(!flipper_format_write_uint32( + ff, furi_string_get_cstr(key), &data->transaction_mac.counter_limit, 1)) + break; } success = true; diff --git a/lib/nfc/protocols/mf_desfire/mf_desfire_poller_i.c b/lib/nfc/protocols/mf_desfire/mf_desfire_poller_i.c index 6d8dfda16..8b57fcc4c 100644 --- a/lib/nfc/protocols/mf_desfire/mf_desfire_poller_i.c +++ b/lib/nfc/protocols/mf_desfire/mf_desfire_poller_i.c @@ -468,7 +468,7 @@ MfDesfireError mf_desfire_poller_read_file_data_multi( file_type == MfDesfireFileTypeLinearRecord || file_type == MfDesfireFileTypeCyclicRecord) { error = mf_desfire_poller_read_file_records( - instance, file_id, 0, file_settings_cur->data.size, file_data); + instance, file_id, 0, file_settings_cur->record.size, file_data); } } From 1c080ecc63eefb9f150319012385fca4073882f7 Mon Sep 17 00:00:00 2001 From: doomwastaken Date: Fri, 28 Mar 2025 14:15:36 +0300 Subject: [PATCH 159/268] reverted manifest to original, new animation added --- assets/dolphin/external/manifest.txt | 167 ++++++++++++++++----------- 1 file changed, 101 insertions(+), 66 deletions(-) diff --git a/assets/dolphin/external/manifest.txt b/assets/dolphin/external/manifest.txt index 67c5c1eb6..e0240dd98 100644 --- a/assets/dolphin/external/manifest.txt +++ b/assets/dolphin/external/manifest.txt @@ -10,9 +10,9 @@ Weight: 3 Name: L1_Laptop_128x51 Min butthurt: 0 -Max butthurt: 9 +Max butthurt: 7 Min level: 1 -Max level: 3 +Max level: 1 Weight: 3 Name: L1_Sleep_128x64 @@ -36,20 +36,6 @@ Min level: 1 Max level: 1 Weight: 3 -Name: L2_Furippa2_128x64 -Min butthurt: 0 -Max butthurt: 6 -Min level: 2 -Max level: 2 -Weight: 3 - -Name: L3_Furippa3_128x64 -Min butthurt: 0 -Max butthurt: 6 -Min level: 3 -Max level: 3 -Weight: 3 - Name: L1_Read_books_128x64 Min butthurt: 0 Max butthurt: 8 @@ -57,13 +43,6 @@ Min level: 1 Max level: 1 Weight: 3 -Name: L2_Hacking_pc_128x64 -Min butthurt: 0 -Max butthurt: 8 -Min level: 2 -Max level: 2 -Weight: 3 - Name: L1_Cry_128x64 Min butthurt: 8 Max butthurt: 13 @@ -78,6 +57,20 @@ Min level: 1 Max level: 3 Weight: 3 +Name: L1_Mad_fist_128x64 +Min butthurt: 9 +Max butthurt: 13 +Min level: 1 +Max level: 3 +Weight: 3 + +Name: L1_Mods_128x64 +Min butthurt: 0 +Max butthurt: 9 +Min level: 1 +Max level: 3 +Weight: 3 + Name: L1_Painting_128x64 Min butthurt: 0 Max butthurt: 7 @@ -85,114 +78,149 @@ Min level: 1 Max level: 3 Weight: 3 -Name: L1_Mods_128x64 -Min butthurt: 0 -Max butthurt: 10 +Name: L1_Leaving_sad_128x64 +Min butthurt: 14 +Max butthurt: 14 Min level: 1 Max level: 3 Weight: 3 -Name: L1_Wake_up_128x64 -Min butthurt: 0 -Max butthurt: 12 -Min level: 2 -Max level: 3 -Weight: 4 - -Name: L1_Happy_holidays_128x64 -Min butthurt: 0 -Max butthurt: 14 -Min level: 1 -Max level: 3 -Weight: 4 - -Name: L1_Sleigh_ride_128x64 -Min butthurt: 0 -Max butthurt: 14 -Min level: 1 -Max level: 3 -Weight: 4 - Name: L1_Senpai_128x64 Min butthurt: 0 Max butthurt: 5 Min level: 1 Max level: 3 -Weight: 4 +Weight: 3 Name: L1_Kaiju_128x64 Min butthurt: 0 Max butthurt: 10 Min level: 1 Max level: 3 -Weight: 4 +Weight: 3 Name: L1_My_dude_128x64 Min butthurt: 0 Max butthurt: 8 Min level: 1 Max level: 3 -Weight: 4 +Weight: 3 + +Name: L2_Wake_up_128x64 +Min butthurt: 0 +Max butthurt: 12 +Min level: 2 +Max level: 3 +Weight: 3 + +Name: L2_Furippa2_128x64 +Min butthurt: 0 +Max butthurt: 6 +Min level: 2 +Max level: 2 +Weight: 3 + +Name: L2_Hacking_pc_128x64 +Min butthurt: 0 +Max butthurt: 8 +Min level: 2 +Max level: 2 +Weight: 3 + +Name: L2_Soldering_128x64 +Min butthurt: 0 +Max butthurt: 10 +Min level: 2 +Max level: 2 +Weight: 3 + +Name: L2_Dj_128x64 +Min butthurt: 0 +Max butthurt: 8 +Min level: 2 +Max level: 3 +Weight: 3 + +Name: L3_Furippa3_128x64 +Min butthurt: 0 +Max butthurt: 6 +Min level: 3 +Max level: 3 +Weight: 3 + +Name: L3_Hijack_radio_128x64 +Min butthurt: 0 +Max butthurt: 8 +Min level: 3 +Max level: 3 +Weight: 3 + +Name: L3_Lab_research_128x54 +Min butthurt: 0 +Max butthurt: 10 +Min level: 3 +Max level: 3 +Weight: 3 Name: L1_Sad_song_128x64 Min butthurt: 8 Max butthurt: 13 Min level: 1 Max level: 3 -Weight: 4 - -Name: L1_Doom_128x64 -Min butthurt: 0 -Max butthurt: 13 -Min level: 1 -Max level: 3 -Weight: 4 +Weight: 3 Name: L2_Coding_in_the_shell_128x64 Min butthurt: 0 Max butthurt: 12 Min level: 2 Max level: 3 -Weight: 4 +Weight: 3 Name: L2_Secret_door_128x64 Min butthurt: 0 Max butthurt: 12 Min level: 2 Max level: 3 -Weight: 4 +Weight: 3 Name: L3_Freedom_2_dolphins_128x64 Min butthurt: 0 Max butthurt: 12 Min level: 3 Max level: 3 -Weight: 4 +Weight: 3 Name: L1_Akira_128x64 Min butthurt: 0 Max butthurt: 8 Min level: 1 Max level: 3 -Weight: 4 +Weight: 3 Name: L3_Intruder_alert_128x64 Min butthurt: 0 Max butthurt: 12 Min level: 3 Max level: 3 -Weight: 4 +Weight: 3 Name: L1_Procrastinating_128x64 Min butthurt: 0 Max butthurt: 8 Min level: 1 Max level: 3 -Weight: 4 +Weight: 3 -Name: L1_Hackspace_128x64 +Name: L1_Happy_holidays_128x64 Min butthurt: 0 -Max butthurt: 12 +Max butthurt: 14 +Min level: 1 +Max level: 3 +Weight: 3 + +Name: L1_Sleigh_ride_128x64 +Min butthurt: 0 +Max butthurt: 14 Min level: 1 Max level: 3 Weight: 4 @@ -203,3 +231,10 @@ Max butthurt: 10 Min level: 1 Max level: 3 Weight: 4 + +Name: L1_Doom_128x64 +Min butthurt: 0 +Max butthurt: 13 +Min level: 1 +Max level: 3 +Weight: 4 From 2828ffb0f4a1c3ee8e4a05ad1accee6c1d9abb05 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Fri, 28 Mar 2025 14:18:34 +0300 Subject: [PATCH 160/268] add nfc apdu cli command back [ci skip] by leommxj in OFW PR 4133 --- applications/main/nfc/nfc_cli.c | 163 ++++++++++++++++++++++++++++++++ 1 file changed, 163 insertions(+) diff --git a/applications/main/nfc/nfc_cli.c b/applications/main/nfc/nfc_cli.c index fae8ca933..ea686834f 100644 --- a/applications/main/nfc/nfc_cli.c +++ b/applications/main/nfc/nfc_cli.c @@ -3,15 +3,36 @@ #include #include #include +#include +#include +#include +#include +#include #include #define FLAG_EVENT (1 << 10) +#define NFC_MAX_BUFFER_SIZE (256) +#define NFC_BASE_PROTOCOL_MAX (3) +#define POLLER_DONE (1 << 0) +#define POLLER_ERR (1 << 1) +static NfcProtocol BASE_PROTOCOL[NFC_BASE_PROTOCOL_MAX] = { + NfcProtocolIso14443_4a, + NfcProtocolIso14443_4b, + NfcProtocolIso15693_3}; +typedef struct ApduContext { + BitBuffer* tx_buffer; + BitBuffer* rx_buffer; + bool ready; + FuriThreadId thread_id; +} ApduContext; + static void nfc_cli_print_usage(void) { printf("Usage:\r\n"); printf("nfc \r\n"); printf("Cmd list:\r\n"); + printf("\tapdu\t - Send APDU and print response \r\n"); if(furi_hal_rtc_is_flag_set(FuriHalRtcFlagDebug)) { printf("\tfield\t - turn field on\r\n"); } @@ -40,6 +61,144 @@ static void nfc_cli_field(Cli* cli, FuriString* args) { furi_hal_nfc_release(); } +static NfcCommand trx_callback(NfcGenericEvent event, void* context) { + furi_check(context); + ApduContext* apdu_context = (ApduContext*)context; + + if(apdu_context->ready) { + apdu_context->ready = false; + if(NfcProtocolIso14443_4a == event.protocol) { + Iso14443_4aError err = iso14443_4a_poller_send_block( + event.instance, apdu_context->tx_buffer, apdu_context->rx_buffer); + if(Iso14443_4aErrorNone == err) { + furi_thread_flags_set(apdu_context->thread_id, POLLER_DONE); + return NfcCommandContinue; + } else { + furi_thread_flags_set(apdu_context->thread_id, POLLER_ERR); + return NfcCommandStop; + } + } else if(NfcProtocolIso14443_4b == event.protocol) { + Iso14443_4bError err = iso14443_4b_poller_send_block( + event.instance, apdu_context->tx_buffer, apdu_context->rx_buffer); + if(Iso14443_4bErrorNone == err) { + furi_thread_flags_set(apdu_context->thread_id, POLLER_DONE); + return NfcCommandContinue; + } else { + furi_thread_flags_set(apdu_context->thread_id, POLLER_ERR); + return NfcCommandStop; + } + } else if(NfcProtocolIso15693_3 == event.protocol) { + Iso15693_3Error err = iso15693_3_poller_send_frame( + event.instance, + apdu_context->tx_buffer, + apdu_context->rx_buffer, + ISO15693_3_FDT_POLL_FC); + if(Iso15693_3ErrorNone == err) { + furi_thread_flags_set(apdu_context->thread_id, POLLER_DONE); + return NfcCommandContinue; + } else { + furi_thread_flags_set(apdu_context->thread_id, POLLER_ERR); + return NfcCommandStop; + } + } else { + // should never reach here + furi_crash("Unknown protocol"); + } + } else { + furi_delay_ms(100); + } + + return NfcCommandContinue; +} + +static void nfc_cli_apdu(Cli* cli, FuriString* args) { + UNUSED(cli); + Nfc* nfc = NULL; + NfcPoller* poller = NULL; + FuriString* data = furi_string_alloc(); + uint8_t* req_buffer = NULL; + uint8_t* resp_buffer = NULL; + size_t apdu_size = 0; + size_t resp_size = 0; + NfcProtocol current_protocol = NfcProtocolInvalid; + + do { + if(0 == args_get_first_word_length(args)) { + printf( + "Use like `nfc apdu 00a404000e325041592e5359532e444446303100 00a4040008a0000003010102` \r\n"); + break; + } + nfc = nfc_alloc(); + + printf("detecting tag\r\n"); + for(int i = 0; i < NFC_BASE_PROTOCOL_MAX; i++) { + poller = nfc_poller_alloc(nfc, BASE_PROTOCOL[i]); + bool is_detected = nfc_poller_detect(poller); + nfc_poller_free(poller); + if(is_detected) { + current_protocol = BASE_PROTOCOL[i]; + printf("detected tag:%d\r\n", BASE_PROTOCOL[i]); + break; + } + } + if(NfcProtocolInvalid == current_protocol) { + nfc_free(nfc); + printf("Can not find any tag\r\n"); + break; + } + poller = nfc_poller_alloc(nfc, current_protocol); + ApduContext* apdu_context = malloc(sizeof(ApduContext)); + apdu_context->tx_buffer = bit_buffer_alloc(NFC_MAX_BUFFER_SIZE); + apdu_context->rx_buffer = bit_buffer_alloc(NFC_MAX_BUFFER_SIZE); + apdu_context->ready = false; + apdu_context->thread_id = furi_thread_get_current_id(); + + nfc_poller_start(poller, trx_callback, apdu_context); + while(args_read_string_and_trim(args, data)) { + bit_buffer_reset(apdu_context->tx_buffer); + bit_buffer_reset(apdu_context->rx_buffer); + apdu_size = furi_string_size(data) / 2; + req_buffer = malloc(apdu_size); + + hex_chars_to_uint8(furi_string_get_cstr(data), req_buffer); + printf("Sending APDU:%s to Tag\r\n", furi_string_get_cstr(data)); + bit_buffer_copy_bytes(apdu_context->tx_buffer, req_buffer, apdu_size); + apdu_context->ready = true; + uint32_t flags = furi_thread_flags_wait(POLLER_DONE, FuriFlagWaitAny, 3000); + if(0 == (flags & POLLER_DONE)) { + printf("Error or Timeout"); + free(req_buffer); + break; + } + furi_assert(apdu_context->ready == false); + resp_size = bit_buffer_get_size_bytes(apdu_context->rx_buffer) * 2; + if(!resp_size) { + printf("No response\r\n"); + free(req_buffer); + continue; + } + resp_buffer = malloc(resp_size); + uint8_to_hex_chars( + bit_buffer_get_data(apdu_context->rx_buffer), resp_buffer, resp_size); + resp_buffer[resp_size] = 0; + printf("Response: %s\r\n", resp_buffer); + + free(req_buffer); + free(resp_buffer); + } + + nfc_poller_stop(poller); + nfc_poller_free(poller); + nfc_free(nfc); + + bit_buffer_free(apdu_context->tx_buffer); + bit_buffer_free(apdu_context->rx_buffer); + free(apdu_context); + } while(false); + + furi_string_free(data); +} + static void nfc_cli(Cli* cli, FuriString* args, void* context) { UNUSED(context); FuriString* cmd; @@ -50,6 +209,10 @@ static void nfc_cli(Cli* cli, FuriString* args, void* context) { nfc_cli_print_usage(); break; } + if(furi_string_cmp_str(cmd, "apdu") == 0) { + nfc_cli_apdu(cli, args); + break; + } if(furi_hal_rtc_is_flag_set(FuriHalRtcFlagDebug)) { if(furi_string_cmp_str(cmd, "field") == 0) { nfc_cli_field(cli, args); From 0563697eab5567e624b1c4792e084071c88a2b25 Mon Sep 17 00:00:00 2001 From: doomwastaken Date: Fri, 28 Mar 2025 14:32:47 +0300 Subject: [PATCH 161/268] fixed metadata and images --- .../external/L1_Doom_128x64/frame_0.png | Bin 1495 -> 733 bytes .../external/L1_Doom_128x64/frame_1.png | Bin 1502 -> 757 bytes .../external/L1_Doom_128x64/frame_10.png | Bin 973 -> 498 bytes .../external/L1_Doom_128x64/frame_11.png | Bin 1070 -> 548 bytes .../external/L1_Doom_128x64/frame_12.png | Bin 1138 -> 578 bytes .../external/L1_Doom_128x64/frame_13.png | Bin 1069 -> 567 bytes .../external/L1_Doom_128x64/frame_14.png | Bin 1065 -> 558 bytes .../external/L1_Doom_128x64/frame_15.png | Bin 898 -> 488 bytes .../external/L1_Doom_128x64/frame_16.png | Bin 930 -> 496 bytes .../external/L1_Doom_128x64/frame_17.png | Bin 937 -> 498 bytes .../external/L1_Doom_128x64/frame_18.png | Bin 931 -> 497 bytes .../external/L1_Doom_128x64/frame_19.png | Bin 923 -> 492 bytes .../external/L1_Doom_128x64/frame_2.png | Bin 1498 -> 749 bytes .../external/L1_Doom_128x64/frame_20.png | Bin 1159 -> 621 bytes .../external/L1_Doom_128x64/frame_21.png | Bin 1284 -> 645 bytes .../external/L1_Doom_128x64/frame_22.png | Bin 1165 -> 612 bytes .../external/L1_Doom_128x64/frame_23.png | Bin 1306 -> 655 bytes .../external/L1_Doom_128x64/frame_24.png | Bin 1159 -> 621 bytes .../external/L1_Doom_128x64/frame_25.png | Bin 1307 -> 661 bytes .../external/L1_Doom_128x64/frame_26.png | Bin 1069 -> 561 bytes .../external/L1_Doom_128x64/frame_27.png | Bin 1131 -> 583 bytes .../external/L1_Doom_128x64/frame_28.png | Bin 1101 -> 592 bytes .../external/L1_Doom_128x64/frame_29.png | Bin 1035 -> 563 bytes .../external/L1_Doom_128x64/frame_3.png | Bin 1558 -> 757 bytes .../external/L1_Doom_128x64/frame_30.png | Bin 909 -> 532 bytes .../external/L1_Doom_128x64/frame_31.png | Bin 736 -> 389 bytes .../external/L1_Doom_128x64/frame_32.png | Bin 1169 -> 575 bytes .../external/L1_Doom_128x64/frame_33.png | Bin 1212 -> 602 bytes .../external/L1_Doom_128x64/frame_34.png | Bin 1269 -> 601 bytes .../external/L1_Doom_128x64/frame_35.png | Bin 1237 -> 623 bytes .../external/L1_Doom_128x64/frame_36.png | Bin 1288 -> 623 bytes .../external/L1_Doom_128x64/frame_37.png | Bin 1206 -> 580 bytes .../external/L1_Doom_128x64/frame_38.png | Bin 1240 -> 604 bytes .../external/L1_Doom_128x64/frame_4.png | Bin 1487 -> 739 bytes .../external/L1_Doom_128x64/frame_5.png | Bin 1161 -> 596 bytes .../external/L1_Doom_128x64/frame_6.png | Bin 1181 -> 618 bytes .../external/L1_Doom_128x64/frame_7.png | Bin 1138 -> 597 bytes .../external/L1_Doom_128x64/frame_8.png | Bin 1193 -> 610 bytes .../external/L1_Doom_128x64/frame_9.png | Bin 1201 -> 611 bytes 39 files changed, 0 insertions(+), 0 deletions(-) diff --git a/assets/dolphin/external/L1_Doom_128x64/frame_0.png b/assets/dolphin/external/L1_Doom_128x64/frame_0.png index 937996c8c0cc0f45cabedf1b348a94fc9cfc017e..974fda986836d451e417269256139d92264c407d 100644 GIT binary patch delta 721 zcmV;?0xtd63*7~f7=Hl(0002`twPcO00N{*L_t(|oK=!th>T$r$A9N}U(N3N*kLzq zN$+e?lFUX@$h=ERLMV6C$c-CvXOr@bi3{?PJ9oPzn_{FTW%2sDqlSFk*bGri*kxy! znVp&A;+7U`hSs_gy2Ac#Pj=)ce zv4rRsh#m6pK}1!3@Ic}P78+VY{Ra>`)Ln)48tT{gQP^`?X#bJchXH`OR_+rQ5N&jM zx~5`?{<9`_$e$MmiCo=Bi(>#Z)e|ai*G-9j=?ZJS27kQjG1WY*!DI$i!K($TW!Qnl z3#tZW0Ki!t)&Nh_s%6|cR^oqD4R8cnaFgNILm$;5`*Nu5X@EavC+Z_M*#Pg5pB6nD z{0e~{%tuZ~kwc8f48TOdbQ?oY;EbE_Hb&>p21)VBuaBNxUIb110fuov2NoSlB8Q{J zJE3B-_kSj!#}=Hcr+iO8wIGK-k7FnT%ck`vK+kv7js4u%mal;v$pwL-f<|MV*$U_h zuLl^=^3Ytxf|NE#+u~j?~WBdKZ`ZCQ4ESaQD0#e$e>+S{~ZAZyX8!o!lsh zd+}PTaSFvO$jR2->kO5U^aH&Ywj>&g7e3J4yJX-mgtQd>O9||400000NkvXXu0mjf D&hlF? delta 1489 zcmV;?1upvC1=kCZ7=H)`0002e)_;Eh000SaNLh0L01FcU01FcV0GgZ_000GvNkl(_e^ z|L*M}z>)A8p7sUfdXN8QO!h$@uIplr-+TXKeCPw741J=ca0qZTjzJ0G@pKB@j(_8E;rycj8^I|6 zP1OKddp>P{6kro1Bg5~A=STK@+CHrSp8bcLz=L-Mz=j=Ff+&7QCypnO-TxavD{@vl zjS28EV1x`I1%JTo97V<$p8`z*0Fta{!KDBrMzabVrxIb6T3HG}`@wv&NuUxS)9})m zClP@Qq^joSF+QFLT$2qk3wXUPN;E$0i|^qrL3B0=SOSbNe{`WR5q6P) zGk!G*Miihz3El{>()iqcSCc?0$KvCPd5tJQ#XSBHV1JkK>HT*ofppKRxr{16>>dd??hvu2xb(3tMPjP&x7wfRSMo6xMctqLsNhX=a^LhJdWoxm)*SH z1)SpjfYmBsJ$uF4WGDbPKWc1l?s%MG1^5K---#>*kosBUy%gXy?+5Ho4Xcr<09-$7 zyeS3vRDYwaM5Y2r<46R<6zcl`-BwD40k(7?U6^xXtxohybq3;8%G+tKT>jC#% zD_#iU&(_*%WEiiwUBTE4yu?mm@6R0!> z$V6nJ?tsh~fa~iW^JoG@-+G-|)#NZ+2grKXqX`h*CRWxAW>Jv^(7oS-twCURt-td9 z0p33|#?OiYX6I5y09tvXsPfVZ65wZnplf#gj4UAI8A}zwBEiX-0QPq3ff*#5Re-Fu zD}RpxBBjeypv{@9r;n&v=G(v-EC~WDjh(%fNTt(k%wz$XI64c^kQra#F{Ep=7eA^1 zRl)7;2%s4r+C2;FF*@|C2w1%eU_I|FZbN{~h0szddcG=K@fv@m76R~Gdo%%}593)> zOHNmjK~jL3{a$+Whwn*o904+m92DH!G=I;1DwIGXYi5ecy*8%f9``qbMyqX}mn4$$ zVX;<3|I&Q8wg~Cy^U(1m9H#&yh#DWxX=7Ex`&Q4RQZwolBEYJ}@j_!goX7p7#q?wp zt6~6do>oPbyH+|!ZFoxspo?Ku?Qmc2`W^5p06o^8-BZD&xD^3BhKF`fq`#90(SIy} zzK5_p-HR{*R&N8G7kkq@E2F^U(e~=+l;)^yQ+Q6Ewea9L1@P?U5j#6xfKI& zgp|h7+O4)B0%R>vqz>-wC-wJw%I+)xOFYe5G@^_u!tK8nj`u|^2=dexxH>_`+~^Oq zX>|b49Psmw)C^iXpb%sk1_wXfb$^_<(0bFkncJ@^5hSBaafUXB zuOfit{i}@+kVxPm2bUfLq)P3Aj@R0t0<2!BO6)Q|Kr@h}22vkN0&Q!6KDVd?WIzgt rb{e0~ff~va8_?^i^lL%lbzSX05MgtY=X?gZ00000NkvXXu0mjfr?0ya diff --git a/assets/dolphin/external/L1_Doom_128x64/frame_1.png b/assets/dolphin/external/L1_Doom_128x64/frame_1.png index 9996dfd6740c5f568cd30f0e9ecd48e1b0c8e2eb..3a9a3a71a77cdfaf53541c616b9ce4c9b6bf9c95 100644 GIT binary patch delta 745 zcmV+Ba&N_kT|Q_|q0abb7SX0@9tj z&;lm={?X>W92n9TV1Dh-v?>N}s5W0KU%mxEZ385Ws)jZR8o-VN>|{%`0?s?{0Jeh3 z+*3ttyy^p8AjyJ;7}A;m_E=2ju2yjw(K+CRha~e_rXAHK;6?(IxuX?gqM8S$ZX(J2 zNP&c-S^=&lFn^iR^y&uq!bg(%fp6W!Yj6O+8?o77%;0Ver}kLcF&-ecK1-IGp}kpA z_{dn+53PdO?5+L$Db+gW8#8 z49Flr=Uc>PU0fgpFdv|Hw%EwfPM1aOco&dXEmbS>@EDR@srN&NDxHHM*00Ad_25i{ z+SKYgsEy})_j|~9xgY^))z`Xowj4qFr`|+ea?r!l*-t?(`W;^VSDq~7Krh8QfVirL zN8*($n18#U*|zmyYlmk6{c+FFo*@91f9eKg_|KpoeP7q6D(LlXU?{=ANB7Q^Kz_*i zz5z6rIOzwVm*Xj1KyZF}u@@;%XU|R2OQP#tMI={M24q!oq7bK z8eaSO-8T_=ke;6Z8k|tWu&sC}A}=RDpV$s&nfD)K&(z&kNB2AUOGZu~ztN>~W(C>VaJ8ICprxbgBzPM1T7}^{mn3+W@-< b7jL}>UAZp%UI*(&00000NkvXXu0mjf$0lk4 delta 1496 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 lcq;-qTn9ihpA3ND`~fh54`KcrgnHA z(XmchgbdoGi;GJZG17m)F;hhmk_-XC#nCM47;tD-ovtnxx(E)6m3pf~MTcIo#WV@| zIoyxDuXv`*xew3zcn`4nOi7IT;K5Qr5{eV2wKQQGG4awQQGfa$k%Ohz8qLG;)$Wgs zn4|gW{gry^2*6yrQQaF@Rm|>n;}9I(kwb7;n{$bald((GekTrK<+i2bj0Ru48&e*Kthq1dfAyg^g3k5jM>Wrw^^N?hvmc7My9L4*olvz=N{wxSNBrP1Fjo zJR`>FJLG1a{(q4qP1dqbhXDAU%rW5XvGcYeJU25J-KY+?+#$R?%`dN%R#pZf?R z*gdm}z#@Q^hNQe{N$E~Efpysq(1%G7JAlO}K*a}&{dC&auaIy1nWOgkWcxWck2X5# zK7MXLg>zf#)ni9*|9%DX-i2a%>(Q_Rqu$q6Ql+N{A%B=R*U~BtOdAMUtEYYV08NVh z_}1TQxB%>FbSvHIF~ET}O99}-+WG;2Fk1m0=N|002ovPDHLkV1nQi00;m8 delta 1061 zcmV+=1ls$g1g;2>7=H)`0002e)_;Eh000SaNLh0L01FcU01FcV0GgZ_000BvNkl$BWT$l&&m z+fNVtxJiP#^V9KmtZ+Vna{#O|`Ub2@re9BCTH4Bh;DMc+41aJrUd;e_eGhgOmDBq=TXbm9zAp&U0bfpJ?u>CTK zft_|qD7H%Wa(|h9Dg(T_1_01IzksXqTe7i^tN{Wtz|7!F1|W#U05!cd7+b+v3tld> zlK~(m18|@nfE8H-R1k>)S~~GL-y`Qu3~>A;P@-_|Z}=I>E<7io&u|Q&`P3li08Zfa zx9ri@4pdIg0U%}#py};cFkC8 zwILz{9N7ins@-Cj_gX9PUPwAtbpgjs0(%@3fdQy|X+N?CXvlv9q~X)5_GmXpEa^DNBASl=L z*l4{AekK_J^t4Ik)u?*bnav#oIGbw>P-{nbGeC$*V1f0hz0Pz?Se;*%+nrm$1T4$=O=nO$N|X z%n6yf60-wTJ2Ka-V;LZtLuU+;W&_(BWk0#S`^NyI1;q@U;&2M?3#;AW{OdXhnuUmi z9}P}mt$$wzMazDGn3>drNPCO-(RB{sjguWt5%pa{2LM2<%(23AGB^w21E3&k1|FS` fV*=n*L=W6QqvX9sJo0*p00000NkvXXu0mjfGNa@> diff --git a/assets/dolphin/external/L1_Doom_128x64/frame_12.png b/assets/dolphin/external/L1_Doom_128x64/frame_12.png index 133374545beeab08a20c801467dee2404a503d0c..973f97378170fa9d9d633dd3f4d80c2c90ab8fa4 100644 GIT binary patch delta 565 zcmV-50?PgJ2*L!A7=Hl(0002`twPcO00Id~L_t(|oNbdmXj4%bhM#k9ey&NS1r?eM zNd_0UI5=rBf{0@#sXEj#dqFA=iGwIO=%x|Tx&>Ua3W6scwBTT&i%`Kd9aq?}xh5~e zX_ZyKA9tjQcX7sCj5*viZZYcMo~7Q!9Nik{VmUsRwok-}iG5`5M5nbMw<*VN-MT|L z>UeHymO^5x?SIpdP^$o(xhcc1ukVUDxvHOZu=~Rmi6m|3kmE|^ZM)g?j|!T=;mWjk zY9`XGdc;&L_!PdrcGauiUnlZ?eB&xyj}SI_Z5~GmjhkRp3A(I`3@sMJE>~bUN|hrH z_*Lw0&K*GJ1<+jp^4=^Rsej%aKHZbL|1Nv$1vuDR@qY=uWZr@sxvjdN!!~Mga9DT{ zBpr95-|PhrS#LnvSjo4%q@)0|LC(JqQh*!IvL@dF1~Lwa4*(mHSxNvqkz3yYa8S23 z28&NL6@Vk8RmPzF^R2Q0W-8rYAfa@}`@w8Tn2;6`R|A|B5$PJsfD6D7VA zlv2tviY_AJXaGb+M#ek=01W^mSeOy+8w6#9+j*~l|5qC04`<`g00000NkvXXu0mjf DHNF1x delta 1129 zcmV-v1eW{41o8-w7=H)`0002e)_;Eh000SaNLh0L01FcU01FcV0GgZ_000CeNklO<&+|Yj<zij)G5M02+|9K|uF*>8+!IFw2J71uTR_ z|B{~E`@M(yYM9wu*Jc-Bpbv(|>D-<-&%t#E8YBa30H>=(5YNa+1~AkKUuo;s+GnZ% zXd)y7bk5OK_ndALu%4>|ww|HU*DINh>jF3L`7ldJ!yPMVgEkqUAzy-&VbUHv z#ObZI27etQao;Bc{D&sHse#_oxnyk3Fv$Wr2_!N#z-LNSD^>aQGj1*ZZdA8Xc)U&l zf-}I3fm_2DI?bc63;bE{+|}L9)eNAeZ_oLHwyv@r zXA4m2-m4b4f&q3oUt>UNLg-ltUcmsC&^TXXJZ?6?$zKgGI~IU_|3ioeMqdXzbmY#y zq6^Swq;{Pocf?>^JF2Pr-^>kqGl^^F02%|f*yN4lTj$`jTJwrl0mfRn*h>`wK5WQdg~QI^{DsU^ViRMjPli80JpaXJfx}jSjMV%w^SI?HTbi7&*@d~ zXBfZ|VHf~F|I;pf-pJm!-~x~8BQXHiR^lsn)>RCEjihA)eC`g=AZRfFMHB`wZ3BCS zi&cxYvsI%ySn9#mHrFtK77?6J&tEYh`hRK$m{OGN-yHxIR3{4uB?B;WTlYJ$O1CS! zfS&BAXbp$n$V>)+*x4_^LdRY*wOY&(3U6 zaX1AX1xEtsU-MrCSt8=#M}w1C>*EI@QL-N(b|#G=l1(eHJx%8TRyf(=6w#k0WB>rf v%p5INOa@mWYyeb5&!E#0J^)U|DS`V75FJ$vH&SPt00000NkvXXu0mjfOW^nP diff --git a/assets/dolphin/external/L1_Doom_128x64/frame_13.png b/assets/dolphin/external/L1_Doom_128x64/frame_13.png index 00ee5d0474308cad4065cd0b4e6b550c09cd2edf..1a7e06051403e2412c3cd3773a46bc2182dddcf9 100644 GIT binary patch delta 554 zcmV+_0@eMk2)6`~7=Hl(0002`twPcO00I69V(D_R5#PJJqZ|vD%|WxPP!SpY}c8PC>2uARrzWz`SU`Q2W=fa<1Fzvpg@*Hs0hAcn~#(H4L352*HE=dIdQR07*qoM6N<$g5bsm;Q#;t delta 1060 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 QS^xk507*qoM6N<$f^AsUqyPW_ delta 887 zcmV--1Bm?S1A+&T7=H)`0002e)_;Eh000SaNLh0L01FcU01FcV0GgZ_0009tNkl$LVgV-C9yF{!uqT^r= z($wlb(op$l0E@KN5}(u7F@ZkQwt-qH8l^B4WjpxC0J9f0lC}^`5CQWHU>PMVO%TO^ z!;geX8QBijp7NP3#K@y&$oy^tk@vLaV6W_JP59NOl)$b7tSbGkq^C}i%5|B;svF?;c3amxHm@->DB8ub0gh9yRqR zpm07AQ4=WtFhtM)bFO{p{7xX!z;RaptJ6Jt^b>)u0b10ihmzI8d%~Roa*ze~5dB18 zYk(+t5ufN7V0(@Ml<^e(M8^QCcoUzf^IO4(cRY$ubTWVnUd1OO`UV6@jF2cIyaNCL N002ovPDHLkV1oMym6`wm diff --git a/assets/dolphin/external/L1_Doom_128x64/frame_16.png b/assets/dolphin/external/L1_Doom_128x64/frame_16.png index 6aa7a7dd328af467ca5aa59a6739ef8f1c8d311d..4dfc8453509c9fa6c8f6282923515a9f508ce0d7 100644 GIT binary patch delta 482 zcmV<80UiFL2k--s7=Hl(0002`twPcO00Fm2L_t(|oL!Q!j?+LCMbC^4j?fB1mm*T8 zqosplrFB8ERQ$q+B+UnpQwK2d zdT=v1=!q_UNIU0ro27#rf$}m&b`950FfyjuoG{Z zKOsIAzIK(5+6R?ZAt_J+_j_Of#!wH@*%c$myla}Mo^7O?U6?|>uyL@IKkrTOgi)q% z=%W<1IC2O5g@2?YA*vT~bV}-}-w{hoO9R~WU<_~p9K8z)_zZk{*n=sMIHZ3aa1#K> zrnp!5DaTN9P4^I%oaOcyF5%28n&c&_;3@J7>I*OUxn1mo6P*t&>oL2spJD(uSKRK6U0Xfi7vuw|3~!+`HIkZF`XK3h zP$=hF$YUfemp{Hp$^+2f-?M$tBB|}})OK{RNVdLgll^VkaR4|3cZXng2)b2l8^9cZ Y|BnO7Unek3ng9R*07*qoM6N<$f?7)4i2wiq delta 920 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 Z@E^)|&6f*yjcWh^002ovPDHLkV1nx3+s*(0 delta 921 zcmV;K17`g31EU9!7=H)`0002e)_;Eh000SaNLh0L01FcU01FcV0GgZ_000A3Nkl@5?PToy?qTk=-vF|N38(`-sImrY~29SvFeeg~(f!Y8kh#xt@1S$iZBz|Of6Nn9P zqWF=WO&~JB&VLiW)9G3Ua?ql~+OWH3fvN$#rv96?WSC_DbTnp&$EN4(m<5Uk@EZLQ z-v65s-q}6_MBz1&TZ;7EvOxBcFz?aNY3Zkz-`0zH29T!N7?n3IujSPMt@Top8s9{( z83D~CJC%X1yMaV)vuo}D?*wif-H(N*GwmFuW4O4dOn<&m_%8#rO#VU$CgPX=Chglr z$FyajHgeHTuuDezV}RL%i#R7hFxO0mXMo-)shMDvV7nNl>4_ikmV6&TOB&@hBUoIEx2N5t^7OtTv;nxFT$s_3 z-kVW8-G71n5Xn2by6&aIj50!RyS$(1CK$d7cpCuC4WnF}E$vCJ7e%uyC#wUe#MBa? zi7aW9TMX{VlEO`ZmA=s5c-VUZt2^*UX>G0DiOiW`cB)4dp9s(ZZ-Vs< zTz_hU(`tN;q5)U2A-myXyI4FcEp?b vM8+SBM!xAW8js==8E1eizAy2Kh`s=V?h?3|j{YfZ00000NkvXXu0mjftS_^g diff --git a/assets/dolphin/external/L1_Doom_128x64/frame_19.png b/assets/dolphin/external/L1_Doom_128x64/frame_19.png index e47cbcd36a58027b073d1a2ad56e76062f8d8582..efd1d6b0e6aedef3b671eaac65f797a2fb3e1771 100644 GIT binary patch delta 478 zcmV<40U`dI2kZlo7=Hl(0002`twPcO00FZ}L_t(|oMn=+ZWBQiMbC~`*g`;-ZVRL= ze?SKXlD9$fH&9tL&_Rh%P!=jm{s2BB9YaM)J|I%Ih=i-eQY^BLy?ZHk#xrYPHFM{@ zH}}mO0H$h)V{92$eq(@#CF^A@%*N8ANJxrj@H#4CkhHZyNPkC?R0JU(OVVkmG0Oq+ zAQT0_yVxypuE8v?-e-}K$>{vs=q4sa(WT)* z%p{ph!7ql?T7N!O&;=hIKMVNlkYcPO4*zJ=HIA47iyS^bh?;u|(siFw9_WbXSZj5a zC!Uj9F5Msu8CToVa^ePIfM)ap)ou``gql}us_@9F$hpJ~!mv!ha1wec0Mph%0AN)4 z2^yB}Xno-pQ}yv{yBG>ny$Lne8Lf?rZZ5bP+_JjM8CR4+s1x47VkfM>Bpk delta 913 zcmV;C18)561Dgks7=H)`0002e)_;Eh000SaNLh0L01FcU01FcV0GgZ_0009`Nklu$p!428|q_kU&fqoSiMfDHzG;YgLHHY3#Mv$-Tq0DuSr6eC|ZLI`YaS=Z~t zNuSK-G_7p^0#R3hjTImnv)hmyhzrBFwp0MB@tng!dEQeHN`KYT3P1@qx9K&dzFZLA zS_VLjQIzqeE(mWe0RUtEDdS7!Ab5m;6##5DNijcH2*D);EC6UWK9@%brU8&X)>GvT zf}fdxwq`!S-*?i~=NSzHVA1>}PY?_NVA=d6FA$Ic@L+zD2M9<2crri9Is|9{Jer?m z83Gglo|Z-4G=H@WEM+l6lB`yNE&!fOzX!2a0PC_?1)2b8&;C1a>C1ElSTT9ERt2g6 zXwUv9QD8{NTCXC6I{JDd`D3Jcgy_QEa^dT3Ir6pqSM?3EynX721`v)sAcCqgpj?^W z)986LZGSz#0AzQxoNxl{0bypG&-*Dkwg4L@flmS8*w*E2FK;KA#sQUvy#mq@u=9cp zHp@AJyRwqx5J;>9+vWh&OvsO|E%Q!XAsmrp2wtl@#ZdyEX4M>RfgjcAwYWm0H^4Gn<_lZawS1vf!VW3i7p7Lr>=3}EQ6tOkV5w3Gz{suTqvw!p9*5Kc&@*P%I4{O0 zsR96F&S>dfWxVtYLTUgQ*@VIbe&@Xf()LmSgaXi7TT?G6)gahi>Lr>FAP|5j!)5|k zLVw_GjPD5rz|LK@)=if+5G0lQr}Ik9egI()i0GM<-vVB4%(URZw(n7>r-e%m944zy z-^DCB`vC;b0Vt9RrY4CR0xU^G_5)l1mPm{F02cuFx&SasQ}zQ~09d8Xd;sLLg0K40 nXg&Z_1<>}Txuw;70DxaKkfD#qS{f$+015yANkvXXu0mjf4@H=X diff --git a/assets/dolphin/external/L1_Doom_128x64/frame_2.png b/assets/dolphin/external/L1_Doom_128x64/frame_2.png index 218fdedbe5dbf80882b89df9cd844684547dd739..d740f5e33c8db728d55b3d46d1737a598858d66b 100644 GIT binary patch delta 737 zcmV<70v`R^3+)Av7=Hl(0002`twPcO00Oj0L_t(|oK=(2i&SM8#((d3j?He&Hlurr zt*kj^H)b?akkCalnP5VpK|wd+ivK`h7oypG>qbQ^5!H2!K}12!-gF^p4oTFFtcp~G zvJ-B)tSrvzi0g9BJYAfbVfMRwe?0HQ^S&=148P>*AKY*qbbnqy7Y>8`#pl;GP#L>& zWnIg$v2zz~sJ0!Y_>*A|JG}PbItzQ@uT;apefO*c$oCcHpCw1m+I`>`5@3S-Krtu5 zWN^Fh(YP)-i}Rz+49MpJM@cb|1J}4A5mM24V8mnPRB|CzCcthrF~1BHkvial7%L|= z5hJaE)91LxQh!Cn)&u9Ua&ksgO-cufvCCA03zVf+f}E0L1l4w4n|be)Hp$1E|4#73|Z|cXmP`Njgnr`sL{MKl>l~G{6%t|9j$pJ^(Qt T0G+e~00000NkvXXu0mjfM(ktV delta 1492 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 QsQ>@~07*qoM6N<$g8LX6WdHyG diff --git a/assets/dolphin/external/L1_Doom_128x64/frame_21.png b/assets/dolphin/external/L1_Doom_128x64/frame_21.png index 36d818beb107acdbad519271c35ae9723a7345b4..f08e98d81b16b096a8eb1cf3ed48097ef55f89c7 100644 GIT binary patch delta 632 zcmV-;0*C#C3WWub7=Hl(0002`twPcO00K-&L_t(|oQ;z`Z`42#hM!#@u|Ysg3KA)R zUC--l9Gy#jsoc%1qnLRQo)a)2o;Mc(vd4tfCR!v0!bGmBJdryUkdw< zkBX^Q&(6H>tab+eKN9y3NMisj;RF;N=)y$z0O(N?lz@4)+kb&Btm}0p6@T4KHEvEp z(;lL!zcm41$mb@0Ch!U@;oNVHqmi=&(~ih47RhnWPcoEoW_ERVDhZf7BoXJQz#VZh zMKQI_(xZ)$^k`z6#`6`Cv^NpW0e0b7-hrZ6@G{`UsjeglFR1h%N`-4Rdn`*^B+{8y z6PBC4@T5-wHh+f|#-ffn3qS}jHLAc@lYzAr#xVm3KJ{O{3@!SN>2xX z#bkV4%W(nHLEgFe0S!=)WLSDB+|5{mg?w0aefU8IgpPGA#K7GQ_?ZH>Q&|A~2LP%M zBuSf6<+Zl$a>YRH05w%#l6ItqC9r61x+%N|RJ|=)2Ns_BVLN<=+VKnHr6#gb S)wpK>0000w0=wY8 z$BOd0u1PbA#g702-1p52Skk5g5ic(?7}BzyoP z0d{nDC}8%x#R+R1AyorZCkZyc{2JdFgY1b@J^1Si-&_Qv`gJi@Z2yyD^2M8d5MGHvPecOg>$BiZZ{3PK@z!JdAce*Mdvq`Z6uNy%XZ~Az=?2L_VBY>#> zR_*yS?tj~ruxCP%PEy7qtAt-sHIUhBX|?^woxt`&gSON5@9FC_VD$^l1yZcPm6~9f z2asm>K00l}#R?}CK!0BVdyW!*L{V#2h~1Ad%mZj^@0DS#7Glpu3x5{SD{!F91WKT- z@0NF(mPyf=>KdTxT z4Jzc6!eT1k2#}fU*7PkEnDq4TXg2z+tRy72L=kVrpeT55=19F-Yk$^!zzmG;5ODLh zW`AE_<=`lIuVsxiMnqytdirN;i9UNOjhMV?_rIt`Mcl{Z%mt;&HNvu*4XRxxG#bE) zkS~Tr2;YK{ZPy5E2j?=YM&SNx*aPq4v!etpAwF{E0xe_RkCk-<*Vn@V)_Tv}hD!-S zdCpudqwm9-?^p@*FN6TZ3EvtjVos7p)qh(~Za+3gh8;*?`jIGr^(X(?;PwKGwUS1? z*7QS>k(0ix_%1#^;t zR^YvPvN(#Cq?D-^uoL=I<;gl`03!i67p=MA#+N)}Ccr8$w;dPr>MLmzjHI)YW`A2~ zfYy`m-L3TvWO7&bMcm&IMnAAP}QG_CW6Ze meBg}=Dw&IF&L9Ub5&i?q)~b|h&F*{v0000@ zXf7Y~@OW2S6%mOETKn*BW4<8Gt=+ z>{?ozB574pDY7eTK#tf>yLkBdC}{Vz4sdC#d`j0H`~KzP>vS##<^k3FA5^=I4`sXD zObtIJzGXUtx(9mV@wPQvk690dVH-eai_)^C*iRE+^H*x@ zNs4YarBHRRT^QIe`@nbrN{%H-vGg@PzGmCtPpghTuT}jaKn~r#pA8yNG99_;mZKK9 lp_LRgT3|$BMT(^m;2%4eDDky;SD63+002ovPDHLkV1hnF7SjL# delta 1156 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 JPDHLkV1h%bW~KlD diff --git a/assets/dolphin/external/L1_Doom_128x64/frame_24.png b/assets/dolphin/external/L1_Doom_128x64/frame_24.png index efda32966d363c04d85345d37a0b943b37269516..24693dfe47354b65563067efdc4c433d45bd0877 100644 GIT binary patch delta 608 zcmV-m0-ybd3GD=s7=Hl(0002`twPcO00J~gL_t(|oOP11YZGA@#(&@4wU?rB8N{|? zOw>Q1bt}a-adHv-2Lx9ap`8Q~a!wulC$tW34sI%_IohHQ9ipqW=BQv6X-ZR*i=ZZvt{vXXYFPje@HT>(k-{u~S-RaHQ<%m(AG8TiN~eJ*eIC`=zQ_sO@&|p)A>WYe4ii{V zQUxPz55Pl7V1H_=r%8H6Nnk%#GX<+PNiFl~yV_uZNota`t2b(c18!2KEJ;!iat?F! zwKx1|&Y{GX6F&<8Y{(m=NAtPmRe;1Munf%l>mN#~!vvNEe{iRE0ODZU$5nDaDaNaw zq-bp!JSFAWXtyOP+T6#qL=!g@4_u&b$DsI#Q&rmv|dGOA~uV z4|m;!>vRF6vQDLLQC{^oV4(_aaM$>D-3L%C{gQO7c(ahD-UN7VzkZqR2f(pbc25D+ zuWC!ufxfE8*jgTPnIr{+8UJh!r_T-GeXpK@5-TMj9Au>a4=GgA`B+vLurPTjl;B!r z@R#IRQd!vVG^ADvI+BvY-+l)4M3RpBk<>_EaR9EGr)1CGNhfk?(6Ur3N|HKKkSex! u0B8*@^+E|CHv~;H+6J(o&3pxDq~IUEZyT!rs`VED0000$}>Q3BzRrG0=y0VpXK1dd@&4EYoiq$9v=c&?$Tiy(no7J#Kf(GoX<+b;n94l};Sqn1}p(APK$ z0<##E1ApmjA@n7PZx<_XwkVoL&8l_ItesY6D9-so=O*kN_4RMq+rX!GEZ?zoOgdO1UF~^nKw^5NHwUOXj6^ z=}PC5)&o4S>LJ+eo2wYV2$042G=j$F>2pEN zA`tl^zh#=P;LL|Qp`}7x9Q6chu{7iDpG&Y9 zMnB(wcXA&0EWmZOq=2ezl3-Ai$X-ft+N!6IwVptV?~iPz)Ix13X_AS!Nq) QsQ>@~07*qoM6N<$g8LX6WdHyG diff --git a/assets/dolphin/external/L1_Doom_128x64/frame_25.png b/assets/dolphin/external/L1_Doom_128x64/frame_25.png index f409d10d3d0cc70c1e46cd0e3d72d388772923b7..2f12df072a240ce300a71801f9859580713e41f2 100644 GIT binary patch delta 649 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 diff --git a/assets/dolphin/external/L1_Doom_128x64/frame_28.png b/assets/dolphin/external/L1_Doom_128x64/frame_28.png index e756f177eca42ebc14500853a7d3b82d399fd151..6e4a776794e70af090439b3419f5cffa4d4a843e 100644 GIT binary patch delta 579 zcmV-J0=)gr2+#zO7=Hl(0002`twPcO00I|DL_t(|oRyP5XcJ)=#(#IYHt8Q|gH+Rk zn%x8BV<7)S*oEF}U4)9A{8rFTIx|1lQ*yFr=G!@{*}c?K zd2zz@$P}$?cbAZ;t4p6p!zAL(1&*CCh~EmFyh-$Ch<8k)KA1~&5lWRV?HEbt=Ty!5 z@~M+FRv_-Uz3!R zg0zSz4?LhpM5Jct0HvoEAg8Ah09&tl5ou}%5s}yiXbE1zSsjnoMx*``{s1t#KlNuk RJ9+>B002ovPDHLkV1l#i72p5> delta 1092 zcmV-K1iSmt1kDJL7=H)`0002e)_;Eh000SaNLh0L01FcU01FcV0GgZ_000C3Nkl?T+Oj2!`oq@Bfy&f5wDiUlc4_+VUn7=bZXMMIO;fb`K&tDNyHm(s3OBmF-z+ zEHr;;J|pxetS8c)Uyk={hx3Vo7r-u~zo1>g^f$6IE4FGuV1IGv76Cr4T?Dv22WZc% z-xW{qi>6_M`9lIDBzM zypK1SX7xG($bU%WZP1p0oWa{^?WJ8$CBW-)05nOn6Q^wl@^j??KgvLW-Z)p=6#)#& zL;z3b-|J5szX%|-lK`WHs+YZc)cR;YGFND25#UEV2w-g&Jy}hfBZ>f<2w*9-NwVAn zjHLBl1fcr~V961*dY+sC)hz-fA;3u7CV83Tk*r??pnqo&0A=#p=UzX(v1K&(R0P;e zfGDAHxf7+siU0`+z-_dCul3QB7S*1*MSw&(Ko9w-^pK!uV-Y~eN`R=z(oO~*+cnzA z=S6^oIY5ixEA|uNE9Ft&MF456Bcs;(x8?|=HnV4bq#}T!uh@I*dp>#=wA zoqQgtB0x~&_ae)CY_l9SivU0o1h^9Uybom8%Y=*Vy4{dy*DHJPM7O@I}V&->-S zpXWLJ;cZsh$4hN)UQd7>(ry7q^>^Bc#{L8WTAN2;l2qW|NxL*6$oH_! z&3_I8r1|#&&}1N7B8seXycAyu$WCsT{LI@~=f=C8&L|B5c-ea{qwS7lO>R*Z0&rV1 z5g<(tpc>f80q$@sl{}_S> z8|_(F7eV&uZ;Fi4*-JI=*ybd#x5`#K=P3(-XrHa4sJGhCC4dzzPjb*3WbuoY2#^3P z&b2eje)Q9{Dr3JYG>^ZMz^e&Gq@Lzz}zOY{@vi#?uM zWt!6_KDVLw08x62^NI4mOUea6^c;k0%9sQqrCk7}J*oxpas2~>HNyXii61Ti0000< KMNUMnLSTa0OZ&k9 diff --git a/assets/dolphin/external/L1_Doom_128x64/frame_29.png b/assets/dolphin/external/L1_Doom_128x64/frame_29.png index cf1f5278786c76be112118fd296403e554ba2982..06fa8b6e498d56e15a2e908e7cd148459e190834 100644 GIT binary patch delta 550 zcmV+>0@?kG2(tu`7=Hl(0002`twPcO00H_*L_t(|oPCo&XcJ)=#(&?}(0*J+V?l{k zyt9LfF5;qFql+L8aj5~9;N<9J-TF}nHA3gKiy4nBW>o2Lo2xj8lbiu{QBs7Ike>HC z+@IW~;yb?l?s?zud7lHESCZDm2Mvn>X-U!;)w70bCd6BnmVf&%q5_LSZ)^Z-hdxwb zR3}~o98|MkoZ`()v0*+X06bGB9O#ybS&jYn4rSs~>(G=kp>1qS#93$+3H=;QLI6%D zb8N>?U%NDpAJzt@Tbwd!rG5Y>ng>62A4MB7*>0qMye95{{BXfc{V;XoqN7~nI80hp z!+eh+#Ma*Qa(|+NMiYF_U0;Ltxmz7?{Te86woXRTyFud&KaV!kDc}Cw-u-p=j%0U< zPVYbd^7L}yF4pLp;Xkjf>iEe6-%cIVGx-R{5XQv>88p}e5fX2fGUh>Siy9XFEV6|? zfa`O>a<)g$uDkVt?+r7ey>cFEn-`b;)b-v<2j-pLc7G!yE_tA|$u+Kyy%2ffcB+xxRsrngS${91Bfx=7?q$RtKwL;r1t2P9ECdkL{Q%9emjNdE zMEonjs)Z_mhPeoo1k=WXz=DWK7eH-@NQf&U;sK~t5gF^RN-5<5$PJ~G(XNO{a;Jny oHb{SmL?0SRevn|=DAb|GUw{z^P|9lG0ssI207*qoM6N<$f`~5!QUCw| delta 1025 zcmV+c1pfQ81d9le7=H)`0002e)_;Eh000SaNLh0L01FcU01FcV0GgZ_000BMNklq2w01$%#q~OX5?JZ_80K{g1 z)hGsl7z}`;bZfdc`@cmo0K{ehYg%VTF#yD1fKw<2fLIKGDcuU|QBVv3XBhw+z-vtH z5m5{Pdl^8wlYd_YPX1dN;DQ5h1oe}M5FnlgwLYWGj#p{bwMyXh#!Jbw~WF+jtPlx}4VFw)m5KP3Zj z)V%_uW1)wZuBAGw;Zia{gNK(tm0=Cow0$gWn2rHfK*}JbooaDjO{SxLCN%>{@~L{R z5A~ZWHL|oAJ#*jpNBbx!Ee7a7<>k_@YKuL3wlDinFRQP^-$(jz?Ue;6;iR@sDJ6dQ`eOD08j7Lx&E!LeQ-`Mq4n)$ zjP$YUInuk0zk>l>kANpiTMQ#T=ec`(8NFQXktX%y+S4#V|Lp9T0H6QbS$4Vwm=#(b zr4RQT*Pmv9Fj;_hVtB#&_+pjeF?vU!+Nj^!`i6A(09tUAeT+=`)aPdy9P8+|6hc29Rr(ggpA{ifG{{2A?U(Endu+phK;4#-? zfPwtCz~eZmEMPE3RIFgSVgPv8?r(Uav$UDoWmJDDH3Rci@aXbm_4Q@+-QVCbXX`r7 zW?1SVaO7pXjPe!-09dc9wClBF4;jF6IePOJ!+ZlQ|E)-NAsN6s(tL{OsUL?`k^3yD z_4^0{p9T~w@HV4Uh%yJtzV2@Xt-u?1e?xTLKVpDryB{FV?s>pg%szQy zv@(Nw2+d(jReC9z3C%%bDtIWgw}^j$O1&6TI%%PYUIZ^9sEK;8O3U_;7KEkQMzkK3 zE+wE)lU*y)f)-b@!QFIs9uKoyH}CEF@Vvhs7_82ruic=|HGfb1Bo!Qoc#{nslEtbM^|@#sM1i*N6ZA)W7hvJ57r<;ujPyv?l~IQe}BRyRN0 z&f7Q9>e?B|X(dhHy7zA0el5RweV@a_7uPy3!D8sb$?&+xFwem(W8l59R$hUE(z6}i zsphTw_tAZ89$?{vgH*x1&#x{(L8=F;#d*`&`@eta0D?NT7?}L%L}U!L0B#=7|C`p= bmk#^~W4A8W7wHGZ00000NkvXXu0mjfF_Us0 delta 1553 zcmV+s2JZRw1(pnu7=H)`0002e)_;Eh000SaNLh0L01FcU01FcV0GgZ_000HZNklAzQd4B$va6_0&MxzX#Nip4&t!+qbP@~`XqqkQT^o=knR*MAQK9D)qh)BTL6Yj_x7 zGc3hR9#3tSXKQkK|JS|tFu*o&rJ;hcA8+gNbPW#!YyywQInW>?XF86R0!3+#)*c4f z3W)(QRU?dgx`u}VHbGM|{k1$@>My}#g4PNcAYzq~g?uBB0cLr*h2Ne*R=`rx{~Lf6 zISEmN7&$Id41d7Ssa{mb_!LMCz}r?J!~i`?^9mc^ZU`C!Xoa3xFb$wm@tVw&48aPt zs^QW9s+d4E0Bdp9m~3B-0#`$uf7ZGpF@RPms)QH=WFNX>ao%RkgjbQ9Z3O(g#X?K! z06Ub==Utr!Szb2tJkCDv>-~MPA^tIdQ)qa#5zyS3`+uXzFdi=p)7UD!3}E(|pOLxC z2v#EVoKi(_)Ch=8x=X>IuAtfIWdO|xP>xkx;%z@%@G02A0weYG9*i)6rg+N;wD_Tm z0Y1t5En1zK1ynGACO66kt+n=Zn)ee{vH%(Z-^3MlfXMpzIxE+cei`&h-p}FX;IroI zWq_)6{eRB(eyzH8B33eCMw$2{k!;1%eB;W`{eh+~AkztXtAaE4- zeZM~6ii#K@;~3gpT|Wqk!lI;a$IKWYV;Eru$SRE%ss&av>e}ywQ3f#2b>lwHxAjM% zpL(`OP+14Cj#JOTwi&>;9|JS%0NPtkE`vT(V}F23WH#y@P{{yzPpSk;CFE@e5cPg; z`BAy=Vg#dgfDD6L26Hq6^ah^qU<4IeK=xUs{?lRr>lZ9|rd7}R4)H}r44}OU)MWDd z0bv>gSPw^rG5ZH46-*$}X9X%5AbX5(Ol1J8?H#BZy{*z{E9wA}O#IH*Esfh61Dr_J zQhzkfH?E3ZdKduLf_)-}fjBw{jG&T5B5&HNj+iWc(-`ZXrFe5*PDLfQaq%Ro}Z zOopij^*h1jpBTA=! zGlSG91JE_CN$RVO{ED?OJUN9{qa(9rm%-X)$cL4lrXAs2)I{5m?@jo2pePwQ)1xDcW13RiMhS=12UK zsI2n&eQBdt)F&dxm1GD#xxO`j$^cHQ++mdqA$gg`Hmje_VgOWngb}RxN&0?V8h@Ap zQk>?&ypqT7V?RlLRl!F?gaM>$>HN=vKigXwfTwtB^CJBdO3+>wpjGZ7Y>=7%cmt4> z>7{v*T$o?)fR?UVY{j3=+Y3pibR4D)V0OA@RRkl28;b!XdA()UQCLTewk}PM)$SR{ z)+~TW;To+)QzGw7jnL*Q<@33`_^}-GZHs0BT3lcmP-I_Il|V^AB|J~TK3+N`<$OL7X>X?)Z zW1d`nyx}}_?ahUKBu|!POL8>XX#?nul=19d8AwLP&s09nV;x=Cm!PqM#SmVGa9tZJ zfzic#=oz~*&(|Q)wnpXX{eQfV#st=U9CW?C4v;w>ID3qGJ<6T!{Rk_twT=qxqs2p! zwD!Iac^TwcDfSzk2j^I=e7+vZ>-oB{uQV3-PrHwEw{}<>IE$V&>ffn+f|e#q^e?5l z7$D-gZv~rOS-{TCs>BJ(Cur-?lqVenTH(iw5B>om2Uilo%qkB6015yANkvXXu0mjf D_#ocz diff --git a/assets/dolphin/external/L1_Doom_128x64/frame_30.png b/assets/dolphin/external/L1_Doom_128x64/frame_30.png index 1b16b867ad77265a1031c071b0cf4e7706c8af67..6d6717c40339d2179ad3eeee66288e6245688f27 100644 GIT binary patch delta 518 zcmV+h0{Q)o2b2Vm7=Hl(0002`twPcO00G-cL_t(|oPCoqh!a5+hQFPxmmw>7S1jBe zV%D{>k}Ize3|wJjVQ0Y3POuPc1cgjFporz!C|CunbcY}~*w~3!T7=~qOM`GY?vPk^r0xY9Ja0#`~zM{(mY)42gT^t?>r?a|}T5 zux9LzD~9z3&}M5DLqX#5j12Nz{-a^N_G>g^NL*ePs7s39Yo}NP7;Q)EH7Et1ypJ;)5u^uKHII!otjr^Z#Hp-2E^}7jC5FUl!|9Wrp?%wnk@@#7 zE}N;WrO|=&fdQmL%X8>x`>NS?6EMxTD&GV?<;zUVX@3Sxr}bNRFaTz)Z6g4^^zm6v zH-JAfzf=DP4vG8&%79T))16UdD0jps8tJxV>wqirj;KdCEaDR@K0vyi)N*(ui{oHIn zv;9lU(s2t`!Ia>@DIgSq2K&d&DL@)ox3j7og-`&8xKsfAtfV|++fKA1zYq%G5SI#| z9r>q~B65IGfN5MR0De|d-i>tp$N|0x1(?UB0^An)HLE#DPy zNEP7p!kvclg@2_29QhugrglQF+!|AA<1R+_bLZzq0fHagDgb^B)SCrE)wSBUUNf~F zy)1mxW)`k0x>A7Fu=m=%HIK;odP7;@yKyp8#!=KFI4}WoTc=A`X1o; zH-V*&YRj3h)FGA$AZPrMD$N|JB^*}(>RDXI^>zDRd4D%}yqvSTeQp#0KU?=f)pN_c z!Q-Wr)a`Sl0IQKBt>396nhWGc0VI*ra&WIrZ3B0_S!Z)L0jF;(1yDT(j*c#zV_@&h zj@#C4dO1Tc!R$G-b;qzuD==#RxVi@j;vRr3OWRAMb+x^AxnPw899IDA+__klYuqS6@I&MPjQYp{LIFM&mkQA9Yo#phf44nSdqV*n5()rz zk(P5F{+zTg6kr3`*&G|J?RBjH7LlG(`u^ci+KyDAHSUbPc-mw&DKMGj!70H+pT+lEkpg)vW~Jx?nI zIQ~uG3b!nNL30yW1=Epl0$16+u~u1~FMef)0uW=o!iud{3h-nOaC7tn=)4Ld2Wa&F z8nZT!f(c(wuWUB)H-Xe(qj#3}WyuR0VRhb8Kh7Sp*-2n;msu{zD;oe{udSSmBo18# z;Co8TON;UeaMp#Z2&4k+yttX=_M=}5i_0)Bl<$5efvX9{E?!%7iW}xYt*`q30(S9Y z^cQZn{Z|ffWAp>u5j`5*;$Q4f)q8*)e9Gbj+&P!)Hvquh63XG0B#`2X4G`=jHh^>f Y0TZ8-9v%E)=l}o!07*qoM6N<$f+Yc-7ytkO diff --git a/assets/dolphin/external/L1_Doom_128x64/frame_31.png b/assets/dolphin/external/L1_Doom_128x64/frame_31.png index cec7329b4d49b99143949f2bd171eb81e1d82a5c..25770b6fe24922bfe1d08e85a8b641a27af899ba 100644 GIT binary patch delta 374 zcmV-+0g3+L1%(5U7=Hl(0002`twPcO00B%%L_t(|oYj#%N&`_8g}*x)oFNenLePki zS%UUL;ttX|VB-QTbOFIB1vik+lqqfPUYE|o6~qllECOSkSn09{Zz~!he6F}epoz=q0LF0fs;1tnh)4vWXCe}MEmc*!v1Ql<05**W*!Eybz`1Y07oj80 U74}f^X8-^I07*qoM6N<$f=+m#bN~PV delta 724 zcmV;_0xSK61KvHWN2!-)X-~W|8zwEeE5-|eeW_>fAPIK`FtRP7X5mi#8)=H(6Kfl{KFos({ zZap>lr`8hP8y|+B$HVwU$`0T)=nQxT(~o9jQrUt-@bt!o0)HgOO97Ar1O*C^94J6? zpa98%0wf0tkQ^vLa-aaofdV843XmKqKysh}$$TgJ=gN}n>JQ}0xX~zz1&M7kNH3WUiMF9 zXCTj)TG$0l()#h}tMy~Ky+s}1MZexy&J7A6WcMz^$_bzV6aDSRVvbON0a1+K&~^f^ zVA}Fc;C~8{ic1_CbM1E zb?J7N=v%)#yh`Cxxb^!;0#_3X3r;gSN0d2m_Dxm)-#`majQ&RSv47+M(W0LyPV}r2 zX>I#8RjmVP^DV|FiaVFk4nP!NLPtbO0y)Av0I&rPkevTvWXcgyR;El5r6Eh4CGY?Akau=YL;~gZqby0P`idyzKzk zFEdse3Sgq+97sB`10c0D2bH9u2uQ5Oo}M^>c*++Faz$!Q86(>;l{C9yq^LsDwqXP& z9>dgEI}EN=RM1<`rbiY?H=}P>Pp*y>+L3+`Y^j)WaA?-Ei-l=4tCl4b4MSe0a$|3& zselp}o?CAk@_)gUO`tyGQs$%<$}X2aYAgW5;y}#Z9<-$v21%|kvl`RZS_n~EblJ$k zu~y4TWBIMi#Pwm3+e5&^*`lvsL{0#2i>nX^wRk-Y-a0$-$RP0A9iMtQ;J~GYhsw&l z8uuv=Srq3r;2G{uEATLQSCxJDj!r(hep~CI93<{`f`13jf4|eN3dtGUS_YtBTXSHE zehff>-N_f)67aER`4rFJ_pW1M@W*3-y)TFQ6bnBK@N({Gvq2O3j|y;bPep*$1_Xzp zwcQIe;X&}wL3V#V$=@lRJVfhPHclUZf{K}3%0c2GB{r)!n+RYWaKwV?><|f1alkNG zY|CBH6e~__2Enk~j9v2YkILd`)8?Q!QHehD7ge&VxtK(3qW}N^07*qoM6N<$g4eMH AIsgCw delta 1161 zcmV;41a|ws1d$1l7=H)`0002e)_;Eh000SaNLh0L01FcU01FcV0GgZ_000C-NklM*7^%%#dwyl803>xre)LI5&bPDyjsVd}Fsu?7 zalUr$6gcOXQh&@&?3Duy5kS=W9-EW!IKEQbsI{%S3!v5c5!;=K_S{}MKz9N3aDK}* zZvjUD+*a1M`$+;@7ah3XpN{NLw>kiHPRpW0I{#U;?B6;w_~M$fM6}+_y_|*CXPrvd z9N@n13r7NYRH_|zpttu|`*ds^%Phds(Z1L~syAzPR)1i4@+^KN|FA{?Z7BFq#+Dmv zHr^>;Mgtix%b!{Yh&~)UoOAx0hY36^V>~&4G!%Te@NOlv4e9R-;R9o_IRUOWOODg( z02uCGQ+5TnIPz2xKwyOA#_+Cd0g>rluaa-_i?y;B$pNS+@ZWOqN(tF2?=c!`SrYl)n!-RG3pk<1*JJM8nn>~SE zJlmn9^VWOq%8>m4ma;507I7~ZLCb2bHE_>@(~fxS035^W03&*zchkekA{e$OaQ5O1 zmjp)N=Ts3qvKBDAVkeb zB?o|Q@R33+b%CW3dvEueotKOzveMbo^PvyL2++Fkuk~7~Dj0pvj3{7T()KKScgq2^ z2C;5A!dQE^#ct=&U4I`wDByWb43E|Us6-5($A5biiWfbsqKK-0mY}dhsrQG!AGZl) zSbrUW!?RBS9~NhE6-Mg1T}!-@L+kg}Sx(nFK+hgQIosN+r82V?K39hlH2jW>fLyyp zvwv*=Xel@<0_xZmDZJzFrDB&S2cSA&z35;S4drc9*YL3TfJ(IJK!X=E74gA|HRQS&2x_X*Ky-$HZKQPqs>VXGOcH2ir`|6iInSCLQlItS bR{j7JB!$y7OT-EQ0000gtSXr4P9EhN?v9OZW%EBTxlENP=2?%nM z2;o?g%Z$bDU2=bdQ-1iq@B7Z1H}HR+1%nI#l?(#U-X5$dc7JaUg46cRAb8N$DDVz@ zV0EW64~!~nCRv}(ozfHy3K)~6g2`jRL`R+22f>Zas`6q6N66N*8>?Lg31>}P)2 zNo1{XoI#k^w5>1 zW6SSCQAT6a8A!6lR1fb}5J2TmtD+X30DcY#hs`;i!hV>IIIRRGIsT)%fwvN7AbTb& b-e>*+7)z~ooY_2E00000NkvXXu0mjfc&`pn delta 1204 zcmV;l1WWtc1iT557=H)`0002e)_;Eh000SaNLh0L01FcU01FcV0GgZ_000DTNklnhh_TRx*Ja2U%Gz~&qQ zY|atD<{SZR&Jn=o906?35y0jg0c_59bOjDJrRd4p`@XB+eSZWnIi(cSz#xzsA>7wl z3z!mYRLKLtHGEH01V;dqK@F)HA3uZdkBHz1U?Qj?EhG;R5y6Q8CcxwF>2)wLJx1v^ z4J@U&RJ=RTTdlF48W`@)wsBJrfGfkfvdnbljZpVZ(b_B9^MPgp;Km*LVMh!FjbIsX8j*anbe)TQ zrLt?MHapNX4U z(Orb#^#GO#KHSZo7e^|2T|a9BmeD2ST0@nUPhTCWgAL6CXn-+1i$Yo=g!X)IxvS&p zEcmFQwaG_{AhURl5Gm`eB9MBs_8nFpfFiwXXd(!E3GJTUz(SYC@TO}4wcCj@;Bs*J z^dBYSS$|gY0)RKOR}%wpZSuzhJl$xL7r=2rCSSgS2xu?o0$$4l)G}-cka{^bJidqX z7#>y;0f)9*C>jGu{1-r0Ntx%l>0iw*nU#r->C@~7$RN%6Tld&iy@2NRrpyD>Y-+$C z5Lu0kvZ@Fqy2Jq5!VF-|6lyBe)9~~?ZxKQB7=K_i+Op>1FWq7Qj~7^(w|Id^VgS#} zTQ+89x)GPNM4%ZI&QlK{wNcYNK+BK!en6BbgwRt2+`2wKN5z4E*D|Eh=%3!-byKKRXolWG z=_saPXaO8;6D~H;#O{p&G$GyhJ#H~* zrl}xDwtn6i$Kws8XYrA{F7^6fi6DBh7rp1&J;)@zYJz5b4%b1dn{{1^9q@N`_W<0K zarWMi;d+fmt)JZC-A8>@2q&TrPeGVTlNqSpanNOk{J@BQ%(S$`6Ynp3u}>9!7|UUf|lCN z#s$(dxNOQa+roFkO51AC8G$4wJS}u+S;c9BKrbuzevIV_;Aal%{;6k(w9!9;EO^Zc S39okm0000C?%OZD8TZ~@rpq1nW=-iS2^;DAs8To*#%$P1klrfEBG?F zWCi0fJ3t4_?LM@GuGU~6Hj6e90KFX2=&pyBrL7)p_>V*m07|d`(udU2trfCK1T&zu zdK|zi5n?V>1b#Cbm8-Ks6&}f`DH)gF$s0cjJ+RxbG zHljcQc6a^k9pT3zM`76KPTa~7ij%#%10xtW);&0)EgeAuujk*|If2@dLIv_>yT5Ef z6@eNP&#$$f{`09~%r$wqN(Osc1Kz&-zG6jy7XTENxj|nA_yfRJQpRh!3PA_#eYLX} zH;$QKokf`Wy8#qolBRb(3)T)&Fr7^z0pbb3&H3}TghN*WP-clQj&D?zL{D4*VPR^C azVR0$Z?WLk6O=^&0000kC*MH?6`xroS-1jXixEhGZ z5Uz4+0Vx5M$}vEF2isVU;21zMU`4i+5BtFO79%(YkO)|j5;z917{SySK(nb*`huDr zp`m;HzRC#yS~1N4T2_fS-4oR8294KKa)6$n`WDbqro;%no68PN-x#gE_j-OsXd{jLs|}We6a#42{lNf{MTZfBw9*}H-xG*t z#IP6u3L&!SFcbLpf!FH^LbL5ziUG9o!5in4Tafb2t$*j5aW!ovj8>L6#~zJQF~GZ& zZ=rJ@*qsL$!30s!cqUNnQVr~jv8kd z<(D!p251I>lJ`jq==p%eQe((;WX(cI41fo>ECd58CA)WJUmt~0F@Sgt^wilo=7gO$eQ&u}r6M2(IT5ElZVm(J?PX-Y7Yu*B+a->H=RwMWYcw!lACeVwQ zq>QL?ZYJ-JG)VYSid58s@1?xQv>2c+O!LhieAlC3coqF6roj85WK`Qg8B%hXUJKAp z04=v=2vY2UZxPX$aV$xO4P^#pNMUFmV1MN1Da##^IGVu*(MLg@gL=lvtmP<*tRl6R6XhVo*f)>5w=a3Dkaj0KzPOO7EZfH4JiW7PSy@L@*Y(}v`ogcR2)=2$Q zb8&`rU03e!0ajv!fl92WWdp7+{H5M=UJv`c@XILR>(CC-y?33KBTvf))Y_!?Tz~8U z_Z7|n)W&vDeg)+*>D>TB+UMxPa>e=f*ghzy+{YNVzsGV}eIVE323$2-IR{?}w0>p#Gf#rhm)~78(5m Xu62k2)5_9M00000NkvXXu0mjfWcW)@ diff --git a/assets/dolphin/external/L1_Doom_128x64/frame_35.png b/assets/dolphin/external/L1_Doom_128x64/frame_35.png index fe9e9f81fd1b0cece9ce3dd1231cd0fe1f867760..6e6ac6a021668b6d6751d6601c539fdd97178f5c 100644 GIT binary patch delta 610 zcmV-o0-gQU3GW1u7=Hl(0002`twPcO00K5iL_t(|oVAlbXcR#dhrfM0cRLB2%MuHd zBHWrPg2;h|7J>^lcG?Kq1#B#W+G!CklbDb$SfogKh654AG*(t?K(Gszf`2wB+MIGl z$ZhnpV==2YYo|e`p#$>;k_fQLN+mKyJb~s%TWJZ0)OY02`k9;H532`@lY!BhCQi2 zEG2i7P$jY_P=DTxi(;Uo>f1I22zo)NVwnS{V2os|)fFJwPXKhHiy7n)CF4r_}&{%w>9IcZgT+Tz2uZ~pbMa=P+QWBtMV(V6jDG`EL0WkJ^6|!WAQ1KUe}uV~ z=6R-F>qBP56v~3~+@S8+!c-+SsYnwbQePHZki`Ch1&EUM*^U|(vLVk|Hr)#r4aE3O z+XW&XO+Ha$0!2ay&k?~_TL8l5+gT+Y1l0+U7=H)`0002e)_;Eh000SaNLh0L01FcU01FcV0GgZ_000DsNkl5p-~W|6SI(l8qk@OT@1}b<#-#EKh_1OxDW7t9EU(!waG1*xz{5EL zcsNG@59bIVna^kK9Qz31;T!=BU}Z0G1dz;iU9(?7wtDh}i<|=11s2S^kLj!tevt)egJ-i|~0_emm0;w%F zqz3i?_&W0ECjR|-`Zvutf7E2 zKIQ7AXHlPCLIlui{Fb(>(_#pZ;VV%&K(_#Tjo)+69cZ1oQ8~cb3`zzHyR}*;^~-qz z>xBAR1$Yy-sM&v4pmDg$T`N&Jz`LhR8BNse*n!@OLVsod5Nm{90eB3aq+UM$5kPhXH_&I3U1 zWIT>lY7!|qfVEiTjaN@3JDCp80ajwwf{}OjXBNNGasY4PB8#0Xhn_PQz0XL^0W6)5 zaCN(iAu}AN)&Y7t<+hO{*g*)R4$BP70X!Y!ws7`7O$gcaGwd69Md?zdi&}MY+kt2!-5v(Q->0}Q+ zQYZZ;ST2^t5S0U%&I90WL~h4Q+tT&2OE|FB2dLdYtql|bY(IQ2!1@4d(&rhp)e}C? zj$kSBDIe?sxV}T{08u+swXci=COZg0f`6LD@H;FH_AJ3Yn?aFbXC#sDZS@A+ zz3`K|&dvI-LJ+=~MTQrcMYISk-hg@s?K+n`mP)o*LXFr6RO&D((T@nEv$A8{B zz>pjOqy4Dez$+ezmc3I*A%W|srw(vk*Ze;e34iRaP`^P-%Crg}Y(o{{xMz7o?Q{X$ z2~V+13xl`~mC)7iEyCkiMgZQtVE6EWi#?V$Qc1X#sb>OrukQ6?sg`Q1gx+UQ@c@*P zpG;3k@Wgy7RFHolBHJ+1E?DdRyG9_Dc6;*z?jHDT%#z2#?~b-YkiaVfPE0U#NK5Wz rl0Yjv&whyJ3F^Nou=~fJ!81mG7>Jq9=Lj|b00000NkvXXu0mjf7EL~u diff --git a/assets/dolphin/external/L1_Doom_128x64/frame_36.png b/assets/dolphin/external/L1_Doom_128x64/frame_36.png index cc80bc5434c63f72a426e6f727156b26b8ae2ae6..2f37e6371c5542b5d84042d92642f6363c8f3100 100644 GIT binary patch delta 610 zcmV-o0-gPc3hxAv7=Hl(0002`twPcO00K5iL_t(|oYm5?Pm@6y$MNrbectv$nzs#v zSQ4QxD-z;n2LcrrM;8S)0umZ5^EP&wmPRp5_4#WP&8Wgv3&H@4W zL$x&6zMPA|i@+y%0L9&$iy-qA{K;t_x%rg*7U&(|b+FjtP!*p?5Nv}#RFkqgRKz(L zSpvQ+8x({>oqwK9LT-#1tL9+5RQ$j(C&O#aR30AZ~beh2AkrA>w#s~kll$2!sh$pQW=1A`!j{K!sZ6UN6>=9%(8)y)WKc^ z0x$Y&(k&3{Y*JLcdRh5E2uH^;DAb#84S90<`VQqys{8ZgA=-<9x?bPBBZ@rC6=R?@ zxBfGzdRq#>0({Ch{e@YlAPJP;M!GJobbNm0_1{jw#sGLrQ+X6gS^!?TN=gC#2@rOh wK54?AumwOfuZ=xhw53z4VFFD6VwL*LKS-~%hXKT`UjP6A07*qoM6N<$f{e8m%K!iX delta 1280 zcmV+b1^@c*1c(Zd7=H)`0002e)_;Eh000SaNLh0L01FcU01FcV0GgZ_000EKNkl2=H$+3}H-hvK zp_X$NkP^_STm)!s@ER`(4grz@8&Wbpya!(IA;BR)B49&m;39yB1V;lTfF<1H_GV!G z=|w03@WYI#)o+Ex>#22vo}VfUfDeHuCbhj0IDBJBYc%Tl4O0ogv5|Vfk<{fIAQ|7X zCO880MuKUTK!1<%J?-BJ&iGi_Q=joObnjU<4mwXz1~49pS3i(vwA z#_#FpDVRA|i`P@eo}~fKVUQ6pqDAQzPZZF$2rEf%BG7y2Fw6Thfy-2)#VRN2IFUE4 z4&X`6qCH?Vr|lUJV^>zvqX8uKZ_NyTsIa0<^64oYXMgw>=2t(L)%znfK>a_yT(P6t zt!bZki&znAsWDepEk|;9Oag8jwjK_BIOF1h;E!c;_&XRivP2FlqP3sZTE>$Blwywn z+OyoU0PDH(j4;XF-^k~wBUnI@TAD|@XBDh_e?yu&K!ax@-fKr6vJn>R9ZdES;*HFa z@^&~6Nq?+b98rn_A5KBe1eL<9@-8lYg6+NK>ZsZPG_Iz#tdW3Q^JVV?WU3E%PT-_H zc&5=MLsBG|RtIP|?@ECnR*sDYV1q=|$oX-kDkI=jcSkg$jLM~@UJbzEmjF>|oqxBF9R|n@Eh>&if;15z3OkBAJ)jH< z9lcV#}hyq9e#&BUL$<3`p4IYm4Z2AYvRs&%~OGijw1p#yc7Yb zPi{=SuC3qDvw%o%&0ZJq8icCyNo2Qb0EtMo)|`hy88AWuZ@Q~R1MXhr<(xHr{S?`wP)hX6!K+R2S=d=S{XoUH$W@fO28QbM<#s0#`T*< q1X}TA?}yMLLHnHoR|M+WTK@r2MlI>x4$6=K0000vU=5hZ!* z>%9(nd73}LGk!Sd`~9AK5B#51H^>xF(KPVv<>`q+a&a2$vwtt9!BSTvKy?C6wG4pw zWa3dPXaHUAjDh@o{}cgT2~*cEAFBX?8;n8jQEwGs&c!J>nYv_kMOttqI-hl%Y0n^= z^Ptm?3+L)c`CTO&>05)o`gnG8Aq9J-o-kC6&v&DVhOWpE<8(05Fwj-`L7FVL)YYNf zQ#jI&K$-&qf`6STRkc;*Eijt+iiyPslIU*^0iU3+TkBY_>22VmnwRSQ5OrOqUW zKuoO158by0`Jq7v&j*C+VDYH2+5v)5z#Vrso>3L8Uj7dJZw{6}9e>|{L)d<0h+}{Qv)8K#(VjU? z;tUM)D+WTmo&yZ5mD;)}V0{wkfINHCyrjXAetE7`JNS?ghR#9=>+`)vfgl9|tZ(1a zB4a%OMX}X%*}=r9I9IDW0jvQMMDIGiiU3Im04=I?b8Hw{u=r|uF+c;#0M10Ja_yKW zK+XWpg)oFga|j=9m&a$g)*li5d#8!-H9|>409f(1_ybG|w*|}}bm{;A002ovPDHLk FV1k+H02KfL delta 1198 zcmV;f1X26M1hxr~7=H)`0002e)_;Eh000SaNLh0L01FcU01FcV0GgZ_000DNNkl3D03?)_m|Ce(guBbe&@rDCt;7Fq=iOGVGjhD1nDdi~#+u~>T102l7A;9K1 z1lSyh0Gs0wU~?P-Y>q>K&2b2@ISv6f$36Q24rDwJU#z{Z%YS|CLxALXo<~%0C5VRz zS2?|alz>WYd4PHj%2-Ko2#^d|k(~137$|Qc!685*U`0x3c>oIuP7II$8h4M+R|Vs( z6+_a%=Xq3&-VxPWrL`U_xXSj<>uVkWJ{afLdQzCxf!#L-H#fbOU-1e7s5Pkdl07NR z%HWhw84NX_IDZE4%whNvU{StR=3CHGK6Le1`IIcRwIY-NUgdjazX@3Jcpj*EiHZTj z2+*tio-r%K8y`i*0H+z004;0gi~*0ajD`3J0=-R#DC^%9c#W6#>oIC;#mz;U7^5O~ zZ$!H)BMwj`0iM2cj|;MTg>NN+HwI{( zvtVc%c(Mf^o+VW?hVue9mtt`61b>n{YK_?i)~czc7toR!`aOW<1vL$koe|m#$duB- z-uPJ_+JErG0F(#*69degbWZWJi8%tA=pUa;*9+(&P)qULOF~OnJ!OpS1!(b001J~w zEESUC>vO>8fxnfA5K#w6@jXBkv{?wZzY-C?st(ZW89d6DuIEWVXJC2^z@ zD}fp%uRwySF@P6uO)BNOaWMnC`QEztEToD7G=E+|LZpY_(!v$icMa0U0GTEiH>teO zmemWCk{|}q+;}#$%3G->&?0q;7+{47q=3I)z~{2P7yi})pQGfdtp@Pj|9lWa-4r)u zA|@}=%V>&5nE^c&^&VUs-J=VrL$(_@!&2bOYVPe-yrzX31X&mCI{Tt zWC7Y|!1Awk1Ebbx%A-VD?PxE+xp#b=>wj@r>Hw4h*6Y674t~=TGq3d$D1}h50Y`Ri z48RfTx~`nx1B}M3;8E)hT4UjsI^VMTk3tYWxhrOpK;sQCB+KWjpzvK^_W&6KvQe-Q zN+N+&3AB=TNDQzVBE5hVy>nwCV*n0QV}PhqW;7y5Am#582{hiIl@VDpttkYMDu0P9 zL!$tX3>Cp+PvJ?k)BLMgtL42(8gFggGDg$%!ZK7bsdpZx5`cQ4EP#a6SgS^k8^@6V z%S1H2>b0g2AhY5jBn!oIUcio?RtD}{ybgZmtCY`w7azec&5b!33E7DQK`Dxh>@{T5 z%qP4*CwtPs3GikJScdBpz@U(;Dkr810<~Dl`Z;jQ(*}pswq9F{e?i-tz_Q-&rT_o{ M07*qoM6N<$f;xgH#Q*>R diff --git a/assets/dolphin/external/L1_Doom_128x64/frame_38.png b/assets/dolphin/external/L1_Doom_128x64/frame_38.png index 43e0f8c6938872fd8dc10338d77340cffbe9a38d..189a16f7b09a3e7820f116e16d307c111c6755c3 100644 GIT binary patch delta 591 zcmV-V0T&1aS-{P=ulgE`pTQMF$bxi+qingcz6f zYn$FVTw=|i;4KF~p68z9x%c4zTosE90Fw@aoSPkV6uLJC!G9_Db`aceDgunOd*I@l z4SUqfSS%<2i3>pXzSIz4+VoO?elG))bPS6h>ml~ih9=VSRr|GPxxQ=>sMDV8 z5oc3*L`u@{O@FFw0nFc?Tp9VqH0?%TVKXL-@&0T`Stw{E0EcN195kv>661a7FhA1N zND$yz2EYTcP*UT&Is}lfG&Q0SP5bPG0mzTHYT-~$h7!$G8Qm9f@lG9JuGCpk!wlxo zRspzEQgYE_xBHMHkRNK*BEbUNHUrSjvdZG5ntma3=YOqJkK=g#oB|7eDjPD1%Dcm> zTiS)C#)Z>&Nn$GgJp#O#Z|bm+j>L2U=p0+N5R?1mL4ftK4ILG5kq-)a`JsA~7=G_K z4DjklQ=bw;XFPz{6Rp((McDN{usyFLfCaq8FuGRt0_*|QF;>ozkm&H`m80QL zQ88op0!T<5WJG4^RuKV=1zOCSq()AFyaf(?Ogw7OXa@Un7rbG;7}@mSAJvU=t8oC~ dC7tBH@drg^wqKLThvxtQ002ovPDHLkV1jIS621Tc delta 1232 zcmV;>1TXvC1lS3X7=H)`0002e)_;Eh000SaNLh0L01FcU01FcV0GgZ_000DvNklu&2H41hz`_kZQ=7nAGy4g^#9q@$iSKmZ$K8@6&?*L@wfTmEb5qGO!>e;lnzxyhjO+0we+pQiHPq9wj(FKmsi09zU-F#-Cmu zSAdZo?TT1dJvAqA@J!bAc^D zbm0uo1bT(k1PQBDMe)t3mN$0eXdK-0J^zLAF38bEs$VJL=QRTk6!=N%nR%s{N%JNvVZ!yckPHJH$au;l)z#IUX<{0 zC{n`|7_LbBk`?Y=&KA1*kq&h$J$ zG>+xk;%K!tzlO&LkhX#s=Rrn06yf9s{AhUq47X6H-Ur|u$%9VU`vA}x^ET9)^7ROO zce4+PoPPpR<-aU|r@<%k0bHJ9H)Y@=g(dC-MA$Vlkj9M{Ly!9a{|eys=0~so$Ss*w z@nmChoi09rm#0&~t=YxwM5CaVm0wA}1yE>%-r-8wkoy3ZefNpb%_3aQ0`yE!jYi7& zad`6p9BC~81$9g+OGR_WyfeBB6|?SFk!2ShFXS-}!etC>21mFh$huY_3> zIioIs^fL1+D1ijEAcpt=xbe|=5>BvABy*Wr3AB?xie5f|dMPx^q+!8Vys6|Y&@tt)u8Uosk~)RaJ_$SX1@P=;a@(6VBzbvcd4*P}Q^ ziGSWgdNMNbEQpG>vb&uHXcM9dlB|8>nz|Uy(9t^&O|Sp z)b?Pzs|ueII{Xf=c=)p=3b6J8mmii2rhk;m;}>vkz-KFhg=Yr3G|NR5g$N$c@~d;lz$Rv z-n&$%rw<_Uhy}28B7-9IMDxfsC9qh7T8@jftV~yc4D_@D76C-cP#!$C6cHaT&6O~( z5PDk;%0u?z5r7uwQ)Q%-aFpr48+cuL97QS(Yau!gE#1qN=@G5fwo>H*vfw?t=phVC z;)_xOuhqeQF4y(>=i*1?0<@DoTuDYFtKv{lf|5LYZ60tLi!ULSJx<^Rq7ecv!zm1Q u3Z+OZjT2~PrPVJX3D27b#@mBuj{X2sO=w^UAY$tP0000L_t(|oK=!fXk1ko#ed&7k7Qb7XKWu- z{FkR@BUL5{q9{7gx@gp5yD-p=wBp8{3&H=8J1T7vL8$IC7DVF01Ue!@Vg|d2yEIZt zEkZ`9nyxx=rgS1Rb6mVknB3j{aqhX_IrqZw>X!cV;eD<>)qk^LP`Gd--gTmU=el-* z#nSS3!+U$`U1`|E7kba^@~|hQT^@cu0}2DhW4bM8)S3hE{z4wOo3R0SL=v3JwE*^k zfB%-`mW=JG136IT65y1T022Y9|3V^SMwftBQmoXH$Rlk4vk5*wCs8!14J=o&Qd^3O zEf@h-&tajqQh(fOtWibbC`8FWkOS)bu~2KQIr*J%3P(5b6tFCwg&H`;Va1Ka14ID- zf`!_T36v$JFb-B~hvE(Ze14961ab{LekF2Jh3yV4T%Z#Hwxd`{CL9=yZ=sgv9Ed14 zw&x&m097Wp74?P#s_1H_c$vHdU9y+3P^(XRcF73VK!4$-5ge=sU<+7S1P4_B?zxIW z6C4c46FrK}GzQV{=!`rJn)$7nf!=M)mBHSj7@#bzA8X$ieT9{@sch0_d-PMV_h@E~ zt!d-rW{e%u^@0OD$HuD$R^fUY0W?{=xB~j*%NK!hN&Um!r(=BXSQyQ~*5~D0F?Q(f z;t?oF`hWYb)mt(C%?XZV^*#Q;1fX*V!0Eu|)I)naU>`N=b*GDp$?<9vbb5S}2r%sY zwJNst%lxUX1JwU|Hd#RWc=ZfmQvatjOV?uiVENwZixB{0Q~M4=Iefc+7IJ;{yC<(~ z#P-MH8$ZNOWT|Luy$J^@)5!}?p0iNrI7F{so={zef;#uNv|5d=ck{{kG8CjnLF;L3 z{nqcNryXFt$$AR**`C)9XKAG+V$;tbPF{(&Kg$xpgU|o@_5b*PBK{S-mrnoy002ov JPDHLkV1i$VVyplF delta 1481 zcmV;)1vdKQ1M~p@52C%pmfb0%H`<2l}tx?7@!HLYUcqVt?>vb(>3{r z0U9AO04?M)Gacb!fF?*zwyOCG{JX2b+jw~J6UfP^%8%-G%a4U6Kr2`>-J}RmlX=^LuVbRzUs;fPXIM^YJ^D12krpEfL1!ZQ;xy z_zr+>=W{!D2Z6N4R;{~Z++zT06Z&1dM)C2j;5n%p160%i84QqtFRIv!>g?Rj0-jT| zY-BNj$HpJ$|LiAx7SO$2`zyH10O+}Buzq!Iy)1MVggcPU0ILYq0DsjP9muQ!=rwwe zKPTZ|b$|9F{ig=gE;jVgIkgGNqt-G19o7!Rc^cT|u$>)3=^eYz0wjB+=c40H-2Oty zU&78>D1!l_i6M-_P176cr#R@d&{0oAO^UT z_0bmb7|sCLQLIU0ph9dF9;efBE0NjO<$5E!j$!}~sSvZ|J!|>=%*T=Bs-Q8zY7j_v zTLE!g07N6$18WP_5eymw+-2OUgX@;`wtojeX7qa|)Vq{aGl8}XSXuJ4UWN89?E#RH zI8Z^ggn?+?0@U8gC&Wec0kWtV~mmi&0>wAymnFBq%9)?l}CZF8~`4A z$z%yBqrEgQ*`!t973_w#v)aE`!*>qX+2@2SBu1=IeGz3buIjruvJ(E4VOXC zUicj#(Ke!Tp8Zq?F@ZK7#r%BKe-1@PdY)Y!cUSgHws;&aIbPZoO&PAs+ASS*7EqPd zXcD@dKY@aP+Qwz2XL**R>q30&G2UbwSb=WmPaqpmy&wezstv5=a(Nf}x|2To00000NkvXXu0mjf>_^t= diff --git a/assets/dolphin/external/L1_Doom_128x64/frame_5.png b/assets/dolphin/external/L1_Doom_128x64/frame_5.png index a128010008a4fffe60f915fe5d716ab65f9a29ea..fa7e6ad10fa29f1a3b3ab562cad5e78649489895 100644 GIT binary patch delta 583 zcmV-N0=WH&3Dg9T7=Hl(0002`twPcO00J9HL_t(|oQ;yfOB7)kho6~Qn-$C1yl4c@ z5FWae9)gghnZTg`LLundvExo*SrF8vyMKm3;ie0AbI(~YMqn7_Zd#5yJHHOIGqdig z?=s9Y&%E#N`@R?cf4R{B@M-{)QxW9A9~l>$WO*?$`5z>^z;zzGMyY;@Gu zA;zFOTpB}8WR(4YOR@Wmu|8we{_y)u`&C>g94r5@{8v&uHC%v-kkl-}NQxsJyyypp z6dxBw&l?TEu7+jwIkO2zQ4S`szP0&MniCj(EtNrO-jM4 zYb6sVeJO==+ka|-X~IGPMr192W&%vGx^+of)U6Q0jsF}=gs%o*X8q|}U z#F=sefoX)i(RhBsMu4T{RPDsi3=3=}yOO;jNeAI5W))VuLFcUpXz>AN4WguJkE{cR zKLx&G9k>1Xwj=FDi+D_(SOMf&_Zsv~^1F{~-eG2h z83}k?T+}@15yp6y<8)1luk@ER-O3GX2?5WuE~W>w0YEVyU@fI7%(np4-ItWy`Xg-z zc`&eERzRQ=04P0@Bz2{qz35N!uyL>GP|yPx04nItUHX^Og~ZQlw0mG$n;D1!{sDgi V2-y6DBVPaj002ovPDHLkV1h+56R7|I delta 1152 zcmV-`1b_R~1c?cd7=H)`0002e)_;Eh000SaNLh0L01FcU01FcV0GgZ_000C#Nkl~&p3m&k_W00Y#7wtomN0x2Tc#R)DrSwIt* z$U=OJ*qZcRL~yo%25JIFWbSDH07n2Mc3Hg^I5EIK;MMyU@&S$j0D#`VF`g`l*9U>z z{Jua^3)o>RN$miml*^)jZhkNQ%`QkO08Yw9K|;(7kxvpqDgo5QONr*T*QRT`**gbm z1V9yCdTl9ig@5q`;;axZ0KDpF3n^L$;0WT0s~;dx09?DrZAR@zV{yNK3b`DF2tZ=@ zH5pd>c@I>dPZh;-0Tl8%#7R@M(!U3=W7YHDSsdXpfHh{fixG-|v4ib+2vo4N5-3^y zH8iVi1u?yOjp`X0u#E#q1GQ0mD{=JV1@2vfNGx-JnSa&KMJur6zId;>wYH@Uf=KTG z+-!l{hBKB`e^fu&&J~sjz+?4m(@4!27?#DV4EG1q-w2wCvOQib&H!eyVp|-I?=AqZ zhs=_G&59vP1Z7-P0Tfa&InjHZ2dMK4$puoXzbGd#$pTubP_)I>klq)de%B)3;!tZ< zOn}Ew2Y-QCNR>c)T1frQn*3GxbKM<;4#0gt=_#MZm`{MG3sC*C9UvlwtJ{0b7Tp$SDgl0qm^=UAw6vRBYyyo7o#DbOBi?c@3?IgryL1k=6vB- z5aR;^=b@h~+G0l|SKe&Tg zSlq`OZ5J&omyo61Hd^gGA#nf=KAFhZptrY6$l4*e^r#L_UkI@z-aAW}5k%|vz44^m zQh!9uF(m*C!o0y11m_v*{dgpZ9S(=o0aSovJE zf;|fOxkpFFxb0}~juc6XJAs)6RaQ1(AZ{ zyf`wfegK0TLA_X@S5-TL$N`f(ft-MF_dMPj8CE}l(H6iJ0fsh8-yK=3zu+5J&ni{d Sz20L00000dL_t(|oQ;w_PZUuYg`Yb!FiSLd2-%!&wbwu|G$Fz7|7=UaE2fq1J5tUz~cN}3zM6=tA8-PeE=Se13w;%QCjk% zp(2IS=}3v|*G=S%fT?!j05=03fbS+R`d$veD}t?fYN#*^CreMeBMMXf9O0e~BTNWi zY9Ec{pbhS)LCGXl!1YS~l6$fyk9ZH*uBpcEJnQ?Gbl3TT7wSE%a*}N zyFIWf3CzCEvVTqn1xet(RxJZ7`;zMB_=g(cfU*0Mbf}MNfCs)UQ zH0KerPl@zN?K*z1b;D<^;2!N0vl}UChORHI`|tV z*`Ng=6?LsxvjJYAbTm< zf^Al0*H#8VNw-;%@)>AKvU9f;Df=)c0?e@!L+=(3;E~zQ5+UtngWZ>^nZgYjUD9eI rNh(V9OmQ|BLeBsksG0t6$N~QVbv-DIZox|z00000NkvXXu0mjfH;))A delta 1173 zcmV;G1Zw;01f2;35{E=mqSC z_ZrCb910E^!crxM~O zfrVb*S8#d(*+WTs1(=mw8uL@@d+s$mAT0qXB^L&cVO9)z3JKB?pfx<#(A4qLa&0Gh zryvypu%^psEPn``BhDuxh0!7WY%tz_92FBVn+tMCYmBe)R2 z>LIg~U#nz@BEkE<`UI#(fyRkWbS!}FUvLhPV)I3iK&=Ysl|s=H*MfUq0K2Yde2q;l zubiOXV}B_L%wkjyq`QUKb=t=F0wkD70jLki-R0A`=LxV>fp%V&0z{N>?Rbl|d3q_DfColr2xjZ%D#kAYWHCOxp5on-`%t~#TP51=DYasfw%vb1fYw84 zoX4Ami=LEoj3o^ly>XV{C;%6q(WG2hAI9uq6x%t*+9A2r?hdLP2+=gWb(OF}h~D+1 z`+sr6l6TJW$zU)|U@wtM2+A{XdlK0^enx^QlB78mEcO1;7>neV188`T5_la7DJ2mBEWjZFHkQ(h7?ovh zXvr83md*rfv6LH5diA*`+7@j4U0p{HSbs8DKmW5QA4rO=6+o*V$GUCQmMe9QFejj_ z#Y6r}Q6NATS@UQ@tt7tnJz)lpe0M1n0KS~|bM8D`P`zKnBU=(=fC>RLRA9B|?zus; zf)NS4<-e393ebaf&z)N(Xd#mlh^qiBj#)FeX4bHzf`69xh5t3@^z?QNsTE*`6@P`u z;C*fG<1PQCC=(#MBG3pKkGV8{U6|$~N n18N~a(k_)|=_kWm1^xp=SV>rJnGXH100000NkvXXu0mjf3W5g`8?OqX(L`=70@AI^JP+IM zmY$r;B)`1(=FNWw{(pttF;EHsa0-x)fk#JUU}5fhgbAjuD}T&j&%y0+;KyS%YKvY} zC{m~$j96U1Ya(X^OeN(eZU#I6-)%o0W;p;)2{+@ZLSg1VFFv>$QJCZR5pJufFd=*? z*&XRY0`9xqGRZOEdbOeDhOB8z>HH8(OJ{K??H3u1o1DTzH0UvdB16#2Com^l2O~8G z;F%;a+iGQ#41dd#z+KZx23EEubf;Zw}Pb=0P7Mv2BxEpkCn_}V#k3$_!CWlG@gv`WuMq`x)MrC)|ViZ zt)@mhT}jES1H2Np#%1O3-bs(N{Tiw|Ria4kUR@Vq41aa~P@-OF!%bae6MI7+f5Wsl z=mAJook_j2!qqjvK{d|7U+3#w13;gK1IrkEHUCwnQ$ z&i+lLV?n^dM1a|I`V2!ofIH@84jyJPCCDteS)3Pe6FlWs0aMyQ z*1$u7?Sq~{1UCgt=mWS4W;XjT*NsN!J@2U4>Agll|CD#Z_V0Hp|iU`69pbtDX(Aev_cC<+$=)Zk+RM=jCn_!_j1)xUpMafDa{NVA(RR#XAE4wh{bsAAp;q|E*v z&MdPC%x=DR6fSNB2nw--=dXEC;}TAa$2d;+ZF)=>q+@BnR*);ri<}-sM{ui3;ri z61{uKCaEpm+1RHb?E=tPO9znnW1)i|+(yo*{L^UIoWQCQn+iaqZm&=lOPEcaZ*++j zm@*s`fcB=y&iL5bZ?zjJ4ODVsp`_QL05tv>PJiIB0mIJzjA^4fPv=3uK%yK&=SQ!?V(%nIk10j-u*RB$F1OeSYdmc3{ zS%0rNej)@+8>}EO-$3n2K-=ck;=40Wm zprLWzI(eksBNuyBFZ^IJFpn|*^+%NW9AO%b9Iqy=}IcJ^JTSbs%41!6=N z1=sRN6~HNMZ3lk_VfN9pV3GU1N=mgwVD=t)i+1k!WLp5x>D9`pbQ=AZg^ce!L+nf- vCiiu){YV*00000NkvXXu0mjfSat-% diff --git a/assets/dolphin/external/L1_Doom_128x64/frame_8.png b/assets/dolphin/external/L1_Doom_128x64/frame_8.png index a18485d6f511e2e951800e13b367b46804f0145e..f5911b1b87fdf8c1cf2f0e6c72bf8372bf0a4de8 100644 GIT binary patch delta 597 zcmV-b0;>I~3E~8h7=Hl(0002`twPcO00JpVL_t(|oQ;xAYZFlzM$er|n<*`w&`s+? z6QoN&x+sWPoDmfK7cB^G`x7!qYY_z9h}*jJCsaWi+du^u&AN)6ico16Nk1TyWHOJ7 zNhWDl-p%D6&VAqKe!%~aVmJay1pur8h(?V4>eaPbxCuZm1An1)wT3k^vp5ReU*B7;{52=m%VjiZ7WNGN$ui&#$!KqUZ)QgB`Z_MvCSqGLRFJssk{Y;7S*- zhJlfyR#tS%RDiq_45BYtRcMBNFo*r;ONsoQf#J{b8=m!xE^A5PDhV>9BDlm|6yX z$M=X*r47Di0^Vu1f5XPdR@_k|_EN)!yy9aizbi=>!8DcwTV4O`Qwh+@bF2b{aWy}= z0~l`}JVk0Xyy&4TorEh`KBu(Zo&5R{;FUny(}fzvxPR#C1VAe4STZ^~(*an- z!&KPhR_QV|0>~{P9J~Ob_ z5@4~G8cuad@q@Z_>i2_z{W^k{51^zeNoq=Gz349hh{5Uou`wUO0UIEP9z9`rC|Qm@ jNi8YqfxFt1)a`+PO$i&&=mT9^00000NkvXXu0mjfVI~#I delta 1185 zcmV;S1YY~%1gQy-7=H)`0002e)_;Eh000SaNLh0L01FcU01FcV0GgZ_000DANkl4Q~iwfJ^LkT}zj!hSvcWfPaKi1b2aS5uD-#S9_}f z5+jkFc#2p^`Y9rKLjVCt;EK!>?Z35G04Ppby&iaTfPWZQ@3)ZO+ADz88Xmt#JXsK5 zp9FI2`wmGj;Dk_8+5uK2m&g3v`d)g?DJUrbPRWHqLX3sTCyAhx03h*FqPgR><=ScX z&OsRgU`?0VSbq#$VLX92FQf}#T=mvQiq-)*f_UQUZ!J*(T)Q8~tj4YG#r^#$PL*)c8x*#3p&04X+KmJ_&C0aPiJZE+CN^8(m)6!{*T zTDx+B@qZrbB(Msp5@>e|vFp6a--Tbt-J8$>xGyN(<@31b6A`Y;PGLpgCE*P4p#m}G;E##P9;VPAfj&XkR(f- zO`UIai50kII4XeXO%cod+||EpH_#hY$;pLuvw!KA03tq&hIlSv+|_?#*yv8V5`txa z;lCs>BGcE*%RQy5oln~j*n!y@g4J_t9OFj-RmdN&=d5nceYmTCw@bk8DYs&>wB0}B z1Xg&Q$6JL9%E~2VX~PCJE)o(4(BM;vd<|x?T|(9g$z?=$(0m}ollbT=!6FFi`q}-Y zVSgzj=P(JtgD@+!Ap`!+c?W4v;)Km1wE(=ym&|9e-B}JHeHTbJ6jZhCa11H|WUYKE z23o?>S}}2k0N)@&UapchQ_0PT~Cr_^;8M2VN$$btDqHuJ|zlK$dXCt)Rw9)Sb3D52jy8m}@)mX);pWg+s}t2t&!BM8gXPr(j?DpdyN6G5|F0A{+>Z!hw2-3BIL zER#%CSrEGpyd3WU9V;impv}^Epc+DQwhMm&aLs2zx*-x^00000NkvXXu0mjfcLX8Q diff --git a/assets/dolphin/external/L1_Doom_128x64/frame_9.png b/assets/dolphin/external/L1_Doom_128x64/frame_9.png index 4030284bf7ae552950626bc39147e8f8dcb54128..57491c2edbb3da617cf629e659e4315fa895dd1b 100644 GIT binary patch delta 598 zcmV-c0;&D63F8Ei7=Hl(0002`twPcO00JsWL_t(|oOP1HOB7)kho5g|ZB{I2^WsL} z4&k9o=|Kodnh6a0FBF2V9Xsw6mIXmwy8CAs6mGg;H}{+dV+4j#?xy9av-9gPJG<*b z-{oT-o_XKj_kAz?cU7VR;FSQddLSM!w(94<&Ga<@3SAIc=YMNh13NQ=zzqk$Y;@e$ zA;wS~?ioXFWP1I8D{A<22ubxG7)f!Z!*U30$RN*Qg1OFf}j$Yf=h! zRV$e=;Y%r8*nd_7EDII_Fp<5yLm4cv+f_+g)+2p~BDhIPRg$D5C|OK%regP8C8#F1 zh%@CB0?UNF)p&lxCcsv5re@-2h7FFAJ;~XSq{DC&s|c&@p!LoJwD=IK1W{6VM%Dr2 zPlB&lCrv-T<4XI{B38f;PTh6q*)HIFAZ_YU%}P>nwSNyFRkSA=?Y8t8u+Spl9Rycz zWZ9HRTj`!Et&M7Cv;oaf`#TXt`JFb}O}Kv9JEd>NzPxeXWba~N1(0XouaQr(De?Z( zns<~LAy_*Q@TB`t^QNrFnDH#f*_skx=`U%jksJ0B0$yZYEDvS_fbM*Ny_BXf-vChi zKvHu1k59B6(EB&WodxmuAW)m% zH&|)`Cu}999Y9LCBHGvH_ttk#K}i8VrGcElL$%)peNo+wDx&#x^|kqYfwf2 zRMBPhECsDF?tegB6w(ENSN&`u#p?hXLELfm10)JSYxnpW={cz{?f>6}Tn$nLV6pq2 zjHvy51gg!uiekF}8u=RHtSMS+e+95(_2WNT9O*GYG-kI82~EJ*!A?8`YS>x{w5jJkn9;bTdPW5t;{et`O?o~`9HV$adzT;++Z0Uo|#vTdE+4@(!TQ z7PQZ3`ig3gYNy({!WIFnSpD8K($fb^*J;sPhZU1yZWLEGG!b0=QHt+v0jy?+Z}xapXrFYVC>% za3AgGhf4oe6m0GQO(w<)U`5{EUT0X+DsM7{^3z1>3A3Bl!x>frQ+kVxWJX9+Wc;NBngXWf=E zVtw>Nr^jwp3^~UVpjx7SYXl^@&DoRnF=$TE`>Cd5I{Dl z6^LD>!4fC*MD8Tj0=EF7KB&TW(xWfI0)N=XXAwbmjBvCANc*q`R3K?RP?5%_T`arp zA7C;K%!+bxzm#=Hk7uSmY8%Nano&sq(=Z&#_rGi2qkh+7)#|6lI1+6prUek;0^XYY zj){!uLo+`a_770N0XVF#x$oM#I2Wh_mjLtFmqwRu?*1maqF|J##+7e|HF4-jx8XcP;Ks%ozwY9QoJpe7(Z zJ&%t@h1Cxbv;}BIfZ>hOPe&E&Zz2eb0X!nWc+z%JNd&(E;?-|NxisUV00000NkvXX Hu0mjfJ)tH- From d267665310b31c915f1dfc9e4bbb04b358409c05 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Fri, 28 Mar 2025 14:39:58 +0300 Subject: [PATCH 162/268] upd changelog --- CHANGELOG.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 72d4dcb03..fb3e03aa6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,5 @@ ## Main changes -- Current API: 83.1 +- Current API: 83.2 * SubGHz: Add **Revers RB2 / RB2M Protocol** (static 64 bit) **full support** with add manually (by @xMasterX) * SubGHz: **Fix Hollarm protocol with more verification** * SubGHz: **Fix GangQi protocol** (by @DoberBit and @mishamyte (who spent 2 weeks on this)) @@ -20,6 +20,11 @@ * SubGHz: Various bugfixes and experimental options (rolling counter overflow) (by @xMasterX) * Anims: Disable winter anims * NFC: mfclassic poller fix early key reuse in dictionary attack state machine (by @noproto) +* OFW PR 4164: Added Doom animation (by @doomwastaken) +* OFW PR 4133: add nfc apdu cli command back (by @leommxj) +* OFW PR 4159: NFC: Support DESFire Transaction MAC file type (by @Willy-JL) +* OFW PR 4153: NFC: Fix NDEF parser for MIFARE Classic (by @Willy-JL) +* OFW PR 4160: GUI: Fix widget text scroll with 256+ lines (by @Willy-JL) * OFW PR 4132: Infrared: Fix universals sending (by @Willy-JL) * OFW PR 4149: HID Ble: increased stack and improvements (by @doomwastaken) * OFW PR 4126: Stricter constness for const data (by @hedger) From 7f135dae0354d812200b2a096471a053c998bc2f Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Fri, 28 Mar 2025 14:47:33 +0300 Subject: [PATCH 163/268] little checks for timers --- applications/services/power/power_service/power.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/applications/services/power/power_service/power.c b/applications/services/power/power_service/power.c index cca15f0c9..31fcfcbd2 100644 --- a/applications/services/power/power_service/power.c +++ b/applications/services/power/power_service/power.c @@ -416,13 +416,18 @@ void power_api_set_settings(Power* power, const PowerSettings* settings) { //start furi timer for autopoweroff static void power_start_auto_poweroff_timer(Power* power) { + if(furi_timer_is_running(power->auto_poweroff_timer)) { + furi_timer_stop(power->auto_poweroff_timer); + } furi_timer_start( power->auto_poweroff_timer, furi_ms_to_ticks(power->settings.auto_poweroff_delay_ms)); } //stop furi timer for autopoweroff static void power_stop_auto_poweroff_timer(Power* power) { - furi_timer_stop(power->auto_poweroff_timer); + if(furi_timer_is_running(power->auto_poweroff_timer)) { + furi_timer_stop(power->auto_poweroff_timer); + } } static uint32_t power_is_running_auto_poweroff_timer(Power* power) { From 79bb24406e7508f037f8c06b69466f07c128c146 Mon Sep 17 00:00:00 2001 From: Anna Antonenko Date: Mon, 31 Mar 2025 08:24:27 +0400 Subject: [PATCH 164/268] Reduced ieee754 parser size (#4154) Co-authored-by: hedger --- lib/SConscript | 1 + lib/ieee754_parse_wrap/SConscript | 31 +++++++++++++++++++++++++++++++ lib/ieee754_parse_wrap/wrappers.c | 14 ++++++++++++++ lib/ieee754_parse_wrap/wrappers.h | 14 ++++++++++++++ targets/f18/api_symbols.csv | 7 +++++-- targets/f18/target.json | 3 ++- targets/f7/api_symbols.csv | 7 +++++-- targets/f7/target.json | 3 ++- 8 files changed, 74 insertions(+), 6 deletions(-) create mode 100644 lib/ieee754_parse_wrap/SConscript create mode 100644 lib/ieee754_parse_wrap/wrappers.c create mode 100644 lib/ieee754_parse_wrap/wrappers.h diff --git a/lib/SConscript b/lib/SConscript index fb0473f8d..4e6171aba 100644 --- a/lib/SConscript +++ b/lib/SConscript @@ -43,6 +43,7 @@ libs = env.BuildModules( "ble_profile", "bit_lib", "datetime", + "ieee754_parse_wrap", ], ) diff --git a/lib/ieee754_parse_wrap/SConscript b/lib/ieee754_parse_wrap/SConscript new file mode 100644 index 000000000..dc60036e0 --- /dev/null +++ b/lib/ieee754_parse_wrap/SConscript @@ -0,0 +1,31 @@ +Import("env") + +wrapped_fn_list = [ + "strtof", + "strtod", +] + +for wrapped_fn in wrapped_fn_list: + env.Append( + LINKFLAGS=[ + "-Wl,--wrap," + wrapped_fn, + ] + ) + +env.Append( + SDK_HEADERS=[ + File("wrappers.h"), + ], + LINT_SOURCES=[ + Dir("."), + ], +) + +libenv = env.Clone(FW_LIB_NAME="ieee754_parse_wrap") +libenv.ApplyLibFlags() + +sources = libenv.GlobRecursive("*.c*", ".") + +lib = libenv.StaticLibrary("${FW_LIB_NAME}", sources) +libenv.Install("${LIB_DIST_DIR}", lib) +Return("lib") diff --git a/lib/ieee754_parse_wrap/wrappers.c b/lib/ieee754_parse_wrap/wrappers.c new file mode 100644 index 000000000..33bd38c35 --- /dev/null +++ b/lib/ieee754_parse_wrap/wrappers.c @@ -0,0 +1,14 @@ +#include "wrappers.h" + +// Based on the disassembly, providing NULL as `locale` is fine. +// The default `strtof` and `strtod` provided in the same libc_nano also just +// call these functions, but with an actual locale structure which was taking up +// lots of .data space (364 bytes). + +float __wrap_strtof(const char* in, char** tail) { + return strtof_l(in, tail, NULL); +} + +double __wrap_strtod(const char* in, char** tail) { + return strtod_l(in, tail, NULL); +} diff --git a/lib/ieee754_parse_wrap/wrappers.h b/lib/ieee754_parse_wrap/wrappers.h new file mode 100644 index 000000000..17e7acf83 --- /dev/null +++ b/lib/ieee754_parse_wrap/wrappers.h @@ -0,0 +1,14 @@ +#pragma once + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +float __wrap_strtof(const char* in, char** tail); +double __wrap_strtod(const char* in, char** tail); + +#ifdef __cplusplus +} +#endif diff --git a/targets/f18/api_symbols.csv b/targets/f18/api_symbols.csv index 2074fa131..81ecb9c7e 100644 --- a/targets/f18/api_symbols.csv +++ b/targets/f18/api_symbols.csv @@ -1,5 +1,5 @@ entry,status,name,type,params -Version,+,82.1,, +Version,+,82.2,, Header,+,applications/services/bt/bt_service/bt.h,, Header,+,applications/services/bt/bt_service/bt_keys_storage.h,, Header,+,applications/services/cli/cli.h,, @@ -57,6 +57,7 @@ Header,+,lib/flipper_application/plugins/plugin_manager.h,, Header,+,lib/flipper_format/flipper_format.h,, Header,+,lib/flipper_format/flipper_format_i.h,, Header,+,lib/flipper_format/flipper_format_stream.h,, +Header,+,lib/ieee754_parse_wrap/wrappers.h,, Header,+,lib/libusb_stm32/inc/hid_usage_button.h,, Header,+,lib/libusb_stm32/inc/hid_usage_consumer.h,, Header,+,lib/libusb_stm32/inc/hid_usage_desktop.h,, @@ -382,6 +383,8 @@ Function,+,__wrap_putc,int,"int, FILE*" Function,+,__wrap_putchar,int,int Function,+,__wrap_puts,int,const char* Function,+,__wrap_snprintf,int,"char*, size_t, const char*, ..." +Function,+,__wrap_strtod,double,"const char*, char**" +Function,+,__wrap_strtof,float,"const char*, char**" Function,+,__wrap_ungetc,int,"int, FILE*" Function,+,__wrap_vsnprintf,int,"char*, size_t, const char*, va_list" Function,-,_asiprintf_r,int,"_reent*, char**, const char*, ..." @@ -2361,13 +2364,13 @@ Function,-,pow,double,"double, double" Function,-,pow10,double,double Function,-,pow10f,float,float Function,+,power_enable_low_battery_level_notification,void,"Power*, _Bool" +Function,+,power_enable_otg,void,"Power*, _Bool" Function,+,power_get_info,void,"Power*, PowerInfo*" Function,+,power_get_pubsub,FuriPubSub*,Power* Function,+,power_is_battery_healthy,_Bool,Power* Function,+,power_is_otg_enabled,_Bool,Power* Function,+,power_off,void,Power* Function,+,power_reboot,void,"Power*, PowerBootMode" -Function,+,power_enable_otg,void,"Power*, _Bool" Function,+,powf,float,"float, float" Function,-,powl,long double,"long double, long double" Function,+,pretty_format_bytes_hex_canonical,void,"FuriString*, size_t, const char*, const uint8_t*, size_t" diff --git a/targets/f18/target.json b/targets/f18/target.json index 3452c6707..1a8306596 100644 --- a/targets/f18/target.json +++ b/targets/f18/target.json @@ -36,7 +36,8 @@ "flipper18", "bit_lib", "toolbox", - "datetime" + "datetime", + "ieee754_parse_wrap" ], "excluded_sources": [ "furi_hal_infrared.c", diff --git a/targets/f7/api_symbols.csv b/targets/f7/api_symbols.csv index 1168a6eea..ba1603b0b 100644 --- a/targets/f7/api_symbols.csv +++ b/targets/f7/api_symbols.csv @@ -1,5 +1,5 @@ entry,status,name,type,params -Version,+,82.1,, +Version,+,82.2,, Header,+,applications/drivers/subghz/cc1101_ext/cc1101_ext_interconnect.h,, Header,+,applications/services/bt/bt_service/bt.h,, Header,+,applications/services/bt/bt_service/bt_keys_storage.h,, @@ -61,6 +61,7 @@ Header,+,lib/flipper_format/flipper_format_stream.h,, Header,+,lib/ibutton/ibutton_key.h,, Header,+,lib/ibutton/ibutton_protocols.h,, Header,+,lib/ibutton/ibutton_worker.h,, +Header,+,lib/ieee754_parse_wrap/wrappers.h,, Header,+,lib/infrared/encoder_decoder/infrared.h,, Header,+,lib/infrared/worker/infrared_transmit.h,, Header,+,lib/infrared/worker/infrared_worker.h,, @@ -459,6 +460,8 @@ Function,+,__wrap_putc,int,"int, FILE*" Function,+,__wrap_putchar,int,int Function,+,__wrap_puts,int,const char* Function,+,__wrap_snprintf,int,"char*, size_t, const char*, ..." +Function,+,__wrap_strtod,double,"const char*, char**" +Function,+,__wrap_strtof,float,"const char*, char**" Function,+,__wrap_ungetc,int,"int, FILE*" Function,+,__wrap_vsnprintf,int,"char*, size_t, const char*, va_list" Function,-,_asiprintf_r,int,"_reent*, char**, const char*, ..." @@ -2999,13 +3002,13 @@ Function,-,pow,double,"double, double" Function,-,pow10,double,double Function,-,pow10f,float,float Function,+,power_enable_low_battery_level_notification,void,"Power*, _Bool" +Function,+,power_enable_otg,void,"Power*, _Bool" Function,+,power_get_info,void,"Power*, PowerInfo*" Function,+,power_get_pubsub,FuriPubSub*,Power* Function,+,power_is_battery_healthy,_Bool,Power* Function,+,power_is_otg_enabled,_Bool,Power* Function,+,power_off,void,Power* Function,+,power_reboot,void,"Power*, PowerBootMode" -Function,+,power_enable_otg,void,"Power*, _Bool" Function,+,powf,float,"float, float" Function,-,powl,long double,"long double, long double" Function,+,pretty_format_bytes_hex_canonical,void,"FuriString*, size_t, const char*, const uint8_t*, size_t" diff --git a/targets/f7/target.json b/targets/f7/target.json index f5b3cf3b6..911aa0822 100644 --- a/targets/f7/target.json +++ b/targets/f7/target.json @@ -50,6 +50,7 @@ "flipper7", "bit_lib", "toolbox", - "datetime" + "datetime", + "ieee754_parse_wrap" ] } From bcbf78a45dd2c4fd398a1954c36ac9883e929da5 Mon Sep 17 00:00:00 2001 From: Evgeny E <10674163+ssecsd@users.noreply.github.com> Date: Mon, 31 Mar 2025 14:20:25 +0100 Subject: [PATCH 165/268] github: support bound and symlinked devices (#4163) * Fix unaccessible flipper for binded access points workaround to work with symlinked devices * Fix return None if Flipper not started * exception handling * decreased timeouts * Check environment variables for flipper path --- .github/workflows/unit_tests.yml | 6 +++--- .github/workflows/updater_test.yml | 4 ++-- scripts/flipper/utils/cdc.py | 6 ++++++ scripts/power.py | 20 +++++++++++++++----- scripts/testops.py | 25 +++++++++++++++++++------ 5 files changed, 45 insertions(+), 16 deletions(-) diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index 37df8e099..d8d83abb8 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -21,7 +21,7 @@ jobs: - name: 'Flash unit tests firmware' id: flashing if: success() - timeout-minutes: 10 + timeout-minutes: 5 run: | source scripts/toolchain/fbtenv.sh ./fbt resources firmware_latest flash LIB_DEBUG=1 FIRMWARE_APP_SET=unit_tests FORCE=1 @@ -30,7 +30,7 @@ jobs: - name: 'Copy assets and unit data, reboot and wait for flipper' id: copy if: steps.flashing.outcome == 'success' - timeout-minutes: 7 + timeout-minutes: 5 run: | source scripts/toolchain/fbtenv.sh python3 scripts/testops.py -t=15 await_flipper @@ -42,7 +42,7 @@ jobs: - name: 'Run units and validate results' id: run_units if: steps.copy.outcome == 'success' - timeout-minutes: 7 + timeout-minutes: 5 run: | source scripts/toolchain/fbtenv.sh python3 scripts/testops.py run_units diff --git a/.github/workflows/updater_test.yml b/.github/workflows/updater_test.yml index df62daf58..b5265df9c 100644 --- a/.github/workflows/updater_test.yml +++ b/.github/workflows/updater_test.yml @@ -20,7 +20,7 @@ jobs: - name: 'Flashing target firmware' id: first_full_flash - timeout-minutes: 10 + timeout-minutes: 5 run: | source scripts/toolchain/fbtenv.sh python3 scripts/testops.py -t=180 await_flipper @@ -29,7 +29,7 @@ jobs: - name: 'Validating updater' id: second_full_flash - timeout-minutes: 10 + timeout-minutes: 5 if: success() run: | source scripts/toolchain/fbtenv.sh diff --git a/scripts/flipper/utils/cdc.py b/scripts/flipper/utils/cdc.py index ee1125f77..00b20d6fb 100644 --- a/scripts/flipper/utils/cdc.py +++ b/scripts/flipper/utils/cdc.py @@ -1,3 +1,4 @@ +import os import serial.tools.list_ports as list_ports @@ -15,3 +16,8 @@ def resolve_port(logger, portname: str = "auto"): logger.error("Failed to find connected Flipper") elif len(flippers) > 1: logger.error("More than one Flipper is attached") + env_path = os.environ.get("FLIPPER_PATH") + if env_path: + if os.path.exists(env_path): + logger.info(f"Using FLIPPER_PATH from environment: {env_path}") + return env_path diff --git a/scripts/power.py b/scripts/power.py index 50bb2d4f7..b13e63bd1 100755 --- a/scripts/power.py +++ b/scripts/power.py @@ -3,6 +3,8 @@ import time from typing import Optional +from serial.serialutil import SerialException + from flipper.app import App from flipper.storage import FlipperStorage from flipper.utils.cdc import resolve_port @@ -32,11 +34,11 @@ class Main(App): def _get_flipper(self, retry_count: Optional[int] = 1): port = None - self.logger.info(f"Attempting to find flipper with {retry_count} attempts.") - for i in range(retry_count): time.sleep(1) - self.logger.info(f"Attempting to find flipper #{i}.") + self.logger.info( + f"Attempting to find flipper (Attempt {i + 1}/{retry_count})." + ) if port := resolve_port(self.logger, self.args.port): self.logger.info(f"Found flipper at {port}") @@ -47,8 +49,16 @@ class Main(App): return None flipper = FlipperStorage(port) - flipper.start() - return flipper + for i in range(retry_count): + try: + flipper.start() + self.logger.info("Flipper successfully started.") + return flipper + except IOError as e: + self.logger.info( + f"Failed to start flipper (Attempt {i + 1}/{retry_count}): {e}" + ) + time.sleep(1) def power_off(self): if not (flipper := self._get_flipper(retry_count=10)): diff --git a/scripts/testops.py b/scripts/testops.py index 3dce51c22..119453448 100644 --- a/scripts/testops.py +++ b/scripts/testops.py @@ -4,6 +4,8 @@ import time from datetime import datetime from typing import Optional +from serial.serialutil import SerialException + from flipper.app import App from flipper.storage import FlipperStorage from flipper.utils.cdc import resolve_port @@ -34,23 +36,34 @@ class Main(App): def _get_flipper(self, retry_count: Optional[int] = 1): port = None - self.logger.info(f"Attempting to find flipper with {retry_count} attempts.") - for i in range(retry_count): time.sleep(1) - self.logger.info(f"Attempt to find flipper #{i}.") + self.logger.info( + f"Attempting to find flipper (Attempt {i + 1}/{retry_count})." + ) if port := resolve_port(self.logger, self.args.port): self.logger.info(f"Found flipper at {port}") break if not port: - self.logger.info(f"Failed to find flipper {port}") + self.logger.info(f"Failed to find flipper") return None flipper = FlipperStorage(port) - flipper.start() - return flipper + for i in range(retry_count): + try: + flipper.start() + self.logger.info("Flipper successfully started.") + return flipper + except IOError as e: + self.logger.info( + f"Failed to start flipper (Attempt {i + 1}/{retry_count}): {e}" + ) + time.sleep(1) + + self.logger.error("Flipper failed to start after all retries.") + return None def await_flipper(self): if not (flipper := self._get_flipper(retry_count=self.args.timeout)): From cf63b38ce0615336c811a2b4bf7a806f5f2d995a Mon Sep 17 00:00:00 2001 From: Konstantin Volkov <72250702+doomwastaken@users.noreply.github.com> Date: Mon, 31 Mar 2025 17:36:17 +0300 Subject: [PATCH 166/268] [FL-3972]: Added Doom animation (#4164) * added new doom animation, updated manifests, re-enabled some animations * added doom animation to manifest * reverted manifest to original, new animation added * fixed metadata and images * removed old holiday animation --------- Co-authored-by: doomwastaken Co-authored-by: hedger --- .../external/L1_Doom_128x64/frame_0.png | Bin 0 -> 733 bytes .../external/L1_Doom_128x64/frame_1.png | Bin 0 -> 757 bytes .../external/L1_Doom_128x64/frame_10.png | Bin 0 -> 498 bytes .../external/L1_Doom_128x64/frame_11.png | Bin 0 -> 548 bytes .../external/L1_Doom_128x64/frame_12.png | Bin 0 -> 578 bytes .../external/L1_Doom_128x64/frame_13.png | Bin 0 -> 567 bytes .../external/L1_Doom_128x64/frame_14.png | Bin 0 -> 558 bytes .../external/L1_Doom_128x64/frame_15.png | Bin 0 -> 488 bytes .../external/L1_Doom_128x64/frame_16.png | Bin 0 -> 496 bytes .../external/L1_Doom_128x64/frame_17.png | Bin 0 -> 498 bytes .../external/L1_Doom_128x64/frame_18.png | Bin 0 -> 497 bytes .../external/L1_Doom_128x64/frame_19.png | Bin 0 -> 492 bytes .../external/L1_Doom_128x64/frame_2.png | Bin 0 -> 749 bytes .../external/L1_Doom_128x64/frame_20.png | Bin 0 -> 621 bytes .../external/L1_Doom_128x64/frame_21.png | Bin 0 -> 645 bytes .../external/L1_Doom_128x64/frame_22.png | Bin 0 -> 612 bytes .../external/L1_Doom_128x64/frame_23.png | Bin 0 -> 655 bytes .../external/L1_Doom_128x64/frame_24.png | Bin 0 -> 621 bytes .../external/L1_Doom_128x64/frame_25.png | Bin 0 -> 661 bytes .../external/L1_Doom_128x64/frame_26.png | Bin 0 -> 561 bytes .../external/L1_Doom_128x64/frame_27.png | Bin 0 -> 583 bytes .../external/L1_Doom_128x64/frame_28.png | Bin 0 -> 592 bytes .../external/L1_Doom_128x64/frame_29.png | Bin 0 -> 563 bytes .../external/L1_Doom_128x64/frame_3.png | Bin 0 -> 757 bytes .../external/L1_Doom_128x64/frame_30.png | Bin 0 -> 532 bytes .../external/L1_Doom_128x64/frame_31.png | Bin 0 -> 389 bytes .../external/L1_Doom_128x64/frame_32.png | Bin 0 -> 575 bytes .../external/L1_Doom_128x64/frame_33.png | Bin 0 -> 602 bytes .../external/L1_Doom_128x64/frame_34.png | Bin 0 -> 601 bytes .../external/L1_Doom_128x64/frame_35.png | Bin 0 -> 623 bytes .../external/L1_Doom_128x64/frame_36.png | Bin 0 -> 623 bytes .../external/L1_Doom_128x64/frame_37.png | Bin 0 -> 580 bytes .../external/L1_Doom_128x64/frame_38.png | Bin 0 -> 604 bytes .../external/L1_Doom_128x64/frame_4.png | Bin 0 -> 739 bytes .../external/L1_Doom_128x64/frame_5.png | Bin 0 -> 596 bytes .../external/L1_Doom_128x64/frame_6.png | Bin 0 -> 618 bytes .../external/L1_Doom_128x64/frame_7.png | Bin 0 -> 597 bytes .../external/L1_Doom_128x64/frame_8.png | Bin 0 -> 610 bytes .../external/L1_Doom_128x64/frame_9.png | Bin 0 -> 611 bytes .../dolphin/external/L1_Doom_128x64/meta.txt | 14 +++++++++++ .../L1_Happy_holidays_128x64/frame_0.png | Bin 858 -> 0 bytes .../L1_Happy_holidays_128x64/frame_1.png | Bin 855 -> 0 bytes .../L1_Happy_holidays_128x64/frame_10.png | Bin 872 -> 0 bytes .../L1_Happy_holidays_128x64/frame_11.png | Bin 861 -> 0 bytes .../L1_Happy_holidays_128x64/frame_12.png | Bin 853 -> 0 bytes .../L1_Happy_holidays_128x64/frame_2.png | Bin 851 -> 0 bytes .../L1_Happy_holidays_128x64/frame_3.png | Bin 852 -> 0 bytes .../L1_Happy_holidays_128x64/frame_4.png | Bin 856 -> 0 bytes .../L1_Happy_holidays_128x64/frame_5.png | Bin 850 -> 0 bytes .../L1_Happy_holidays_128x64/frame_6.png | Bin 851 -> 0 bytes .../L1_Happy_holidays_128x64/frame_7.png | Bin 860 -> 0 bytes .../L1_Happy_holidays_128x64/frame_8.png | Bin 857 -> 0 bytes .../L1_Happy_holidays_128x64/frame_9.png | Bin 863 -> 0 bytes .../L1_Happy_holidays_128x64/meta.txt | 23 ------------------ .../L1_Sleigh_ride_128x64/frame_0.png | Bin 820 -> 0 bytes .../L1_Sleigh_ride_128x64/frame_1.png | Bin 881 -> 0 bytes .../L1_Sleigh_ride_128x64/frame_10.png | Bin 788 -> 0 bytes .../L1_Sleigh_ride_128x64/frame_11.png | Bin 816 -> 0 bytes .../L1_Sleigh_ride_128x64/frame_12.png | Bin 864 -> 0 bytes .../L1_Sleigh_ride_128x64/frame_13.png | Bin 798 -> 0 bytes .../L1_Sleigh_ride_128x64/frame_14.png | Bin 813 -> 0 bytes .../L1_Sleigh_ride_128x64/frame_15.png | Bin 879 -> 0 bytes .../L1_Sleigh_ride_128x64/frame_16.png | Bin 855 -> 0 bytes .../L1_Sleigh_ride_128x64/frame_17.png | Bin 772 -> 0 bytes .../L1_Sleigh_ride_128x64/frame_18.png | Bin 817 -> 0 bytes .../L1_Sleigh_ride_128x64/frame_19.png | Bin 867 -> 0 bytes .../L1_Sleigh_ride_128x64/frame_2.png | Bin 866 -> 0 bytes .../L1_Sleigh_ride_128x64/frame_20.png | Bin 809 -> 0 bytes .../L1_Sleigh_ride_128x64/frame_21.png | Bin 795 -> 0 bytes .../L1_Sleigh_ride_128x64/frame_22.png | Bin 870 -> 0 bytes .../L1_Sleigh_ride_128x64/frame_23.png | Bin 852 -> 0 bytes .../L1_Sleigh_ride_128x64/frame_24.png | Bin 805 -> 0 bytes .../L1_Sleigh_ride_128x64/frame_25.png | Bin 858 -> 0 bytes .../L1_Sleigh_ride_128x64/frame_26.png | Bin 830 -> 0 bytes .../L1_Sleigh_ride_128x64/frame_27.png | Bin 828 -> 0 bytes .../L1_Sleigh_ride_128x64/frame_28.png | Bin 585 -> 0 bytes .../L1_Sleigh_ride_128x64/frame_29.png | Bin 431 -> 0 bytes .../L1_Sleigh_ride_128x64/frame_3.png | Bin 812 -> 0 bytes .../L1_Sleigh_ride_128x64/frame_30.png | Bin 281 -> 0 bytes .../L1_Sleigh_ride_128x64/frame_31.png | Bin 270 -> 0 bytes .../L1_Sleigh_ride_128x64/frame_32.png | Bin 236 -> 0 bytes .../L1_Sleigh_ride_128x64/frame_33.png | Bin 485 -> 0 bytes .../L1_Sleigh_ride_128x64/frame_34.png | Bin 771 -> 0 bytes .../L1_Sleigh_ride_128x64/frame_35.png | Bin 887 -> 0 bytes .../L1_Sleigh_ride_128x64/frame_36.png | Bin 809 -> 0 bytes .../L1_Sleigh_ride_128x64/frame_4.png | Bin 890 -> 0 bytes .../L1_Sleigh_ride_128x64/frame_5.png | Bin 819 -> 0 bytes .../L1_Sleigh_ride_128x64/frame_6.png | Bin 799 -> 0 bytes .../L1_Sleigh_ride_128x64/frame_7.png | Bin 817 -> 0 bytes .../L1_Sleigh_ride_128x64/frame_8.png | Bin 875 -> 0 bytes .../L1_Sleigh_ride_128x64/frame_9.png | Bin 823 -> 0 bytes .../external/L1_Sleigh_ride_128x64/meta.txt | 23 ------------------ assets/dolphin/external/manifest.txt | 21 ++++++---------- 93 files changed, 21 insertions(+), 60 deletions(-) create mode 100644 assets/dolphin/external/L1_Doom_128x64/frame_0.png create mode 100644 assets/dolphin/external/L1_Doom_128x64/frame_1.png create mode 100644 assets/dolphin/external/L1_Doom_128x64/frame_10.png create mode 100644 assets/dolphin/external/L1_Doom_128x64/frame_11.png create mode 100644 assets/dolphin/external/L1_Doom_128x64/frame_12.png create mode 100644 assets/dolphin/external/L1_Doom_128x64/frame_13.png create mode 100644 assets/dolphin/external/L1_Doom_128x64/frame_14.png create mode 100644 assets/dolphin/external/L1_Doom_128x64/frame_15.png create mode 100644 assets/dolphin/external/L1_Doom_128x64/frame_16.png create mode 100644 assets/dolphin/external/L1_Doom_128x64/frame_17.png create mode 100644 assets/dolphin/external/L1_Doom_128x64/frame_18.png create mode 100644 assets/dolphin/external/L1_Doom_128x64/frame_19.png create mode 100644 assets/dolphin/external/L1_Doom_128x64/frame_2.png create mode 100644 assets/dolphin/external/L1_Doom_128x64/frame_20.png create mode 100644 assets/dolphin/external/L1_Doom_128x64/frame_21.png create mode 100644 assets/dolphin/external/L1_Doom_128x64/frame_22.png create mode 100644 assets/dolphin/external/L1_Doom_128x64/frame_23.png create mode 100644 assets/dolphin/external/L1_Doom_128x64/frame_24.png create mode 100644 assets/dolphin/external/L1_Doom_128x64/frame_25.png create mode 100644 assets/dolphin/external/L1_Doom_128x64/frame_26.png create mode 100644 assets/dolphin/external/L1_Doom_128x64/frame_27.png create mode 100644 assets/dolphin/external/L1_Doom_128x64/frame_28.png create mode 100644 assets/dolphin/external/L1_Doom_128x64/frame_29.png create mode 100644 assets/dolphin/external/L1_Doom_128x64/frame_3.png create mode 100644 assets/dolphin/external/L1_Doom_128x64/frame_30.png create mode 100644 assets/dolphin/external/L1_Doom_128x64/frame_31.png create mode 100644 assets/dolphin/external/L1_Doom_128x64/frame_32.png create mode 100644 assets/dolphin/external/L1_Doom_128x64/frame_33.png create mode 100644 assets/dolphin/external/L1_Doom_128x64/frame_34.png create mode 100644 assets/dolphin/external/L1_Doom_128x64/frame_35.png create mode 100644 assets/dolphin/external/L1_Doom_128x64/frame_36.png create mode 100644 assets/dolphin/external/L1_Doom_128x64/frame_37.png create mode 100644 assets/dolphin/external/L1_Doom_128x64/frame_38.png create mode 100644 assets/dolphin/external/L1_Doom_128x64/frame_4.png create mode 100644 assets/dolphin/external/L1_Doom_128x64/frame_5.png create mode 100644 assets/dolphin/external/L1_Doom_128x64/frame_6.png create mode 100644 assets/dolphin/external/L1_Doom_128x64/frame_7.png create mode 100644 assets/dolphin/external/L1_Doom_128x64/frame_8.png create mode 100644 assets/dolphin/external/L1_Doom_128x64/frame_9.png create mode 100644 assets/dolphin/external/L1_Doom_128x64/meta.txt delete mode 100755 assets/dolphin/external/L1_Happy_holidays_128x64/frame_0.png delete mode 100755 assets/dolphin/external/L1_Happy_holidays_128x64/frame_1.png delete mode 100755 assets/dolphin/external/L1_Happy_holidays_128x64/frame_10.png delete mode 100755 assets/dolphin/external/L1_Happy_holidays_128x64/frame_11.png delete mode 100755 assets/dolphin/external/L1_Happy_holidays_128x64/frame_12.png delete mode 100755 assets/dolphin/external/L1_Happy_holidays_128x64/frame_2.png delete mode 100755 assets/dolphin/external/L1_Happy_holidays_128x64/frame_3.png delete mode 100755 assets/dolphin/external/L1_Happy_holidays_128x64/frame_4.png delete mode 100755 assets/dolphin/external/L1_Happy_holidays_128x64/frame_5.png delete mode 100755 assets/dolphin/external/L1_Happy_holidays_128x64/frame_6.png delete mode 100755 assets/dolphin/external/L1_Happy_holidays_128x64/frame_7.png delete mode 100755 assets/dolphin/external/L1_Happy_holidays_128x64/frame_8.png delete mode 100755 assets/dolphin/external/L1_Happy_holidays_128x64/frame_9.png delete mode 100755 assets/dolphin/external/L1_Happy_holidays_128x64/meta.txt delete mode 100755 assets/dolphin/external/L1_Sleigh_ride_128x64/frame_0.png delete mode 100755 assets/dolphin/external/L1_Sleigh_ride_128x64/frame_1.png delete mode 100755 assets/dolphin/external/L1_Sleigh_ride_128x64/frame_10.png delete mode 100755 assets/dolphin/external/L1_Sleigh_ride_128x64/frame_11.png delete mode 100755 assets/dolphin/external/L1_Sleigh_ride_128x64/frame_12.png delete mode 100755 assets/dolphin/external/L1_Sleigh_ride_128x64/frame_13.png delete mode 100755 assets/dolphin/external/L1_Sleigh_ride_128x64/frame_14.png delete mode 100755 assets/dolphin/external/L1_Sleigh_ride_128x64/frame_15.png delete mode 100755 assets/dolphin/external/L1_Sleigh_ride_128x64/frame_16.png delete mode 100755 assets/dolphin/external/L1_Sleigh_ride_128x64/frame_17.png delete mode 100755 assets/dolphin/external/L1_Sleigh_ride_128x64/frame_18.png delete mode 100755 assets/dolphin/external/L1_Sleigh_ride_128x64/frame_19.png delete mode 100755 assets/dolphin/external/L1_Sleigh_ride_128x64/frame_2.png delete mode 100755 assets/dolphin/external/L1_Sleigh_ride_128x64/frame_20.png delete mode 100755 assets/dolphin/external/L1_Sleigh_ride_128x64/frame_21.png delete mode 100755 assets/dolphin/external/L1_Sleigh_ride_128x64/frame_22.png delete mode 100755 assets/dolphin/external/L1_Sleigh_ride_128x64/frame_23.png delete mode 100755 assets/dolphin/external/L1_Sleigh_ride_128x64/frame_24.png delete mode 100755 assets/dolphin/external/L1_Sleigh_ride_128x64/frame_25.png delete mode 100755 assets/dolphin/external/L1_Sleigh_ride_128x64/frame_26.png delete mode 100755 assets/dolphin/external/L1_Sleigh_ride_128x64/frame_27.png delete mode 100755 assets/dolphin/external/L1_Sleigh_ride_128x64/frame_28.png delete mode 100755 assets/dolphin/external/L1_Sleigh_ride_128x64/frame_29.png delete mode 100755 assets/dolphin/external/L1_Sleigh_ride_128x64/frame_3.png delete mode 100755 assets/dolphin/external/L1_Sleigh_ride_128x64/frame_30.png delete mode 100755 assets/dolphin/external/L1_Sleigh_ride_128x64/frame_31.png delete mode 100755 assets/dolphin/external/L1_Sleigh_ride_128x64/frame_32.png delete mode 100755 assets/dolphin/external/L1_Sleigh_ride_128x64/frame_33.png delete mode 100755 assets/dolphin/external/L1_Sleigh_ride_128x64/frame_34.png delete mode 100755 assets/dolphin/external/L1_Sleigh_ride_128x64/frame_35.png delete mode 100755 assets/dolphin/external/L1_Sleigh_ride_128x64/frame_36.png delete mode 100755 assets/dolphin/external/L1_Sleigh_ride_128x64/frame_4.png delete mode 100755 assets/dolphin/external/L1_Sleigh_ride_128x64/frame_5.png delete mode 100755 assets/dolphin/external/L1_Sleigh_ride_128x64/frame_6.png delete mode 100755 assets/dolphin/external/L1_Sleigh_ride_128x64/frame_7.png delete mode 100755 assets/dolphin/external/L1_Sleigh_ride_128x64/frame_8.png delete mode 100755 assets/dolphin/external/L1_Sleigh_ride_128x64/frame_9.png delete mode 100755 assets/dolphin/external/L1_Sleigh_ride_128x64/meta.txt diff --git a/assets/dolphin/external/L1_Doom_128x64/frame_0.png b/assets/dolphin/external/L1_Doom_128x64/frame_0.png new file mode 100644 index 0000000000000000000000000000000000000000..974fda986836d451e417269256139d92264c407d GIT binary patch literal 733 zcmV<30wVp1P)7U`jMD~;6Q-H^ZSqEi)s~EYB8C?Ze#erRkfQt z9peKMjwg2LTh-t;E4;L^gyeu&C*mGHE z|B=>*0f4zy?h_XfZFG6MrecWxvnF=PpBDy+T-`^DV*oVO6Dn@kO^JT#3TwOuyy`L4 zJgmWF235hU1*&D(fy4`{24n!hSsm5@Pt&Sp+&Na_e^d={1X^&D;nqVR)gt?HsO@Qh zKV>KCBR1Ip?~tDsJsSK9fga39PDhbLjK>VXM8I?#Lr>t0oA5S9=gtO6@yV}`o?Tu9 zP5S|caX<$a9ZDjHqs2R+VzT!ppvM-RtEYTVKeZr-KaXQ50?VfLCP2@3)Q$bz*p{z> z9LWWNp@K$Zo!JWL39kni5E#7l8!0`?BrD*k277xZ@2<0@wX5Dbg4EFag-ws2XlcW) zRR_YhkvHmD686f`8{p{sntG+LCiQN3@2yS)7%k;!gO1eB*m@V2`6f~XaQD0#e$e>+ zS{~ZAZyX8!o!lshd+}PTaSFvO$jR2->kO5U^aH&Ywj>&g7e3J4yJX-mgtQd>O9||4 P00000NkvXXu0mjf9B)~h literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_Doom_128x64/frame_1.png b/assets/dolphin/external/L1_Doom_128x64/frame_1.png new file mode 100644 index 0000000000000000000000000000000000000000..3a9a3a71a77cdfaf53541c616b9ce4c9b6bf9c95 GIT binary patch literal 757 zcmVW92n9TV1Dh-v?>N}s5W0KU%mxEZ385Ws)jZR z8o-VN>|{%`0?s?{0Jeh3+*3ttyy^p8AjyJ;7}A;m_E=2ju2yjw(K+CRha~e_rXAHK z;6?(IxuX?gqM8S$ZX(J2NP&c-S^=&lFqzTx>IV42N0Rx0Z{5UeZ~(s>vDskE;BE}3 z_E_059w4?pOO~3Uy;)KC$XM17t%BI>t`8xqsvg8al6h~(#)~b2*sNDD#8m$GK!x}p zm-L|zV8*JaHoJgmLoq|z01Y3p*;LJtz$Kk)c=Q?oy_l*ia&KG!(Z6@rJsbn9S++oc z*ojvMT#!Ss899U6nPm*fAVB9^#AaPwAOtWUpmw&{$k0xgMeKMNkX9{KEAsFdl3c0x zLx(DzgCN$g$1e5YOoQ6g>N=>6=X>{i$alFQ0cq9Ox^%W2LHeiOL|t;w!_wJLK`#0o zUj0{|EaX5h#X5kvs)k46l`ELLpV_wcU~7kG0sV2$&z>OwmVfF7WcbgZ9(`ZerYh+5 zZD1(Dzeo4Zl|X*T`Mv=(mN@ALpqJw*TtIMsd9fEMPiM|o!BO?E&TL(dP(Jj@a|ayN z@T>5@N<@aIcAa_zq8eWN_}w=Vd61r-{~DZ7!?3M*Cn7H=KcCnRZ8}oZcyTTwD+ljC zJ?#Q97IP)Qy)ZvH1yK!%>&nfD)K&(z&kNB2AUOGZu~ztN>~W(C>VaJ8ICprxbgBzP nM1T7}^{mn3+W@-<7jL}>UAZp%UI*(&00000NkvXXu0mjfgaK$a literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_Doom_128x64/frame_10.png b/assets/dolphin/external/L1_Doom_128x64/frame_10.png new file mode 100644 index 0000000000000000000000000000000000000000..7cf69cc1be648bf150be3e49b8b7117a28786877 GIT binary patch literal 498 zcmVK<|}Sahie$>y}lhN*qOX!b~dh-mGHu zFR`yuQg&T&BTNF{ZZ*QfRb$4V+NWGI7)MYdN0@+fj_{#yM7d1d8o{o0Yp~U+!FH!c z_@;9Y|Kj+oBbGBp51mt9C*1D7*sv{TJK#r30qE^XPnMGGuu5FMr?MKu-ez4x?weZX z-MK8fBJYZ=j*#VKw=D`Np0z~@dC(T0A;{Yzhso=Y5+<}`18?~^Z{{yCjgE)l%j-osWVg6vc zeDQA8%RB;onKd=^7~ttfJ(md{K*XS#iwBT3-5%;^ANZ})yS2yw7Oh?`035fvwE!jN zzQyCxb4ytLqVxtZPT&ea0R94u>{((>uqH_nfG0_r*N1vv5}<$fi2=YHNh+&!Ea}** oZU9i-hLNOYmH5zzcKnt20|-L>+2w=a^8f$<07*qoM6N<$f-#HXjsO4v literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_Doom_128x64/frame_11.png b/assets/dolphin/external/L1_Doom_128x64/frame_11.png new file mode 100644 index 0000000000000000000000000000000000000000..99eb9b2a10690b3de7b7ab835e4a63cd55f42196 GIT binary patch literal 548 zcmV+<0^9wGP)GD77BSL)z%f%r5t0l6!Nt)m>KJfnR-LXc z7P<%yij{h+Lq&&PvBfkA`8nK=ysvns%efEF`FIbo_)JNR`ryG*KoW`*r?oU;8Zq(G zBvJYwk%Ohz8qLG;)$Wgsn4|gW{gry^2*6yrQQaF@Rm|>n;}9I(kwb7;n{$bald((G zekTrK<+i2bj0Ru48&e*Kthq1dfAyg^g3k5jM>Wrw^^N?hvmc7My9L z4*olvz=N{wxSNBrP1FjoJR`>FJLG1a{*fe2*0N5A0QjBEG2ra6^R^-5ht}H$Y7^yb zVhp*+CZ-U2Hu3SF`v@Y~J+q0xB7l{Kq`YZK=}tF+b=eNkhe;4SfW;?3#RrQ0blTRh zkZ=2$qxSh^`#Cp{Hah7(er`X7b6e`wV@GfQeg*R0g<^W^(Xaxe-q%)ArKblWm^atb zDh*5<2wAJAefR)Piv9T3-)gu3>}hl>-RUvFfi+73;KbVc0e~=D0UqZc1uO%MP`?aR z+pr2S5B>y3mN|F=-5EF3+0E7UP mBu(SbFBy^iBPpIm8{jXnu@WzQ)N+FW0000w literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_Doom_128x64/frame_12.png b/assets/dolphin/external/L1_Doom_128x64/frame_12.png new file mode 100644 index 0000000000000000000000000000000000000000..973f97378170fa9d9d633dd3f4d80c2c90ab8fa4 GIT binary patch literal 578 zcmV-I0=@l-P)1v7Z+@;xr3Dq53`qtTw>UUyF@lI=CaF5qF?&HO4vB*(IOwJk(Ygg(vI>GH z9kk$Jp^H$#G#y$Zm|H~icXL07++Xk;4u`|T^S$3W_X3$W;?<-D24po}$^76?#XnH1 zbK<@7s`x)50ojgBQZTsmupbafVE01RcW6*bUH;OJc6oMI#fj}}5+@F8$r!q?}xh5~eX_ZyKA9tjQcX7sCj5*viZZYcMo~7Q!9Nik{VmUsRwok-} ziG5`5M5nbMw<*VN-MT|L>UeHymO^5x?bDD@s{ow2DZ{U??}|9Ns-JYQ`@)P3Yx&-%CvWCCeo~W#8fQ!6u!Q8)vMlLC-QxK<0@Q_5H@*j9!Cg`n_yH4 zx~z%}Ef&KrS710wl_L)LRqSuh9YE#<&|LuX-Ygxdf8HEE-IKciE_>?*IM`b83B6?A zf*ZN5x}U=~YH)B^cn~BVccI_x1rAwnK-yTzx4fjJ0JA~PzYkJ?8_u#O-vI_P4u}r` z8w>1WfPc#*PBcxTvp#1Z#vH@l)-CiJ}bjSO_Y)F`p77o1 z8q0tSzz|^B*jGv=0Wg$O$})-~;%ERwL`KFu0RRmEBUqRb?i&PUgxh(qfB#n+;}2)! Q&j0`b07*qoM6N<$g2}@D{r~^~ literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_Doom_128x64/frame_13.png b/assets/dolphin/external/L1_Doom_128x64/frame_13.png new file mode 100644 index 0000000000000000000000000000000000000000..1a7e06051403e2412c3cd3773a46bc2182dddcf9 GIT binary patch literal 567 zcmV-70?7S|P)XyNTq{Fb}3ax@C7$ui@ zZwxDQ6cK}@vpFyU+isKrn0%#yq!0JBR(6T)Q!D|wiUI;KA_Sn15#q+_@hGvp_cKbY z?~izV&y09%#78{p9ecClb-Pf!4UAtZ8YM7b=K3vIf}GuWx8?T|cb;S-9-Tt&e)uaI zv#n-?=-atjGfd1~v1Emb?&9lwK0LAGj-_vFmy_5zyJJgGmH7!LZ*7$QV?|Zxi9!>!1vbSUQ%9p=8)vQ0U+M`9durr_b zJ>E`1t@m;eRN-Sl?d1gQJFzN`HdaH#Gb zkEjFm{Cjao0JK9FShX% zdxJKR3LY^4Ff)<{u53wan{EOCHw_s{eOK7_iE#ZV`~$l6L_Tk&gT(*<002ovPDHLk FV1g8e21ft@ literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_Doom_128x64/frame_14.png b/assets/dolphin/external/L1_Doom_128x64/frame_14.png new file mode 100644 index 0000000000000000000000000000000000000000..428ac9f1f2359cd1bfe8715e21d05026656a3117 GIT binary patch literal 558 zcmV+}0@3}6P)|4u4(!MGzbA3C>L-Lw=6=2v}sYWV-_)`tDB2c2NiRbI_V!^mg*!KGZcy- zc)B=r5nP1Udj3MFLsM+=nzVTici+q1;hT={_rvGC_kHhxsplF+zXKLbdqkn(!SRAQ zkY-4{k|>=25o0j@R{P^{Pgi0w5CI6-Yq6tC?NMsBK9^rdlLLy& zL)G1Ds@gV=0n9EXrtb|Qu#%2XUR?y62R0m zplSo9j_I~_=<|JNRO#-~>I-iEnqM=${9fIFb1T|zgpT&ky(;9b*^=4yUQmU8`&%o@ z($)t)$h$d{rHQzJ&rHMAVFTQy)CqU}y#|xOw)StCH$4E@OY|}UaFlrUJpe^kJS@&X z@hAZF(M|=ZyKoU;9PCpN^`Zj{@(OuX)h>YDQdJ*ERkZ>jbyW?luOcGhAG0VT6PBZ@ w+5;dY!U_QJ0dQ1x7=C_P)8S%6oo&JO>Cr)EZr6qE~uzdAhDfYP^9GzqBQUb2nnsl6YvJ)F}8F^Nge^rlX~UVx@IKu~huRiiRz=phO4>i-MMx4nAW$+|lL(kg&uS zsWwJLyUr%@rLbo_$yCor_uY5ji~%@NLwvH5tneQec$~AYV{n>D(+C*%9Nt707D?N3 z0BIsgZUB>+B+UnpQwK2ddT=v1=!q_UNIU0ro27# zrf$}m&b`950FfyjuoG{ZKOsIAzIK(5+6R?ZAt_J+_j_Of#!wH@*%c$myla}Mo^7O? zU6?|>uyL@IKkrTOgi)q%=%W<1IC2O5g`^`Psuyu|O6sZK5lc%;1Kjjr3~&J)y$cHX z419XngDH?Wq<VDQ z1E>sdp))m-npgTD>3dKp=UK>OBrTUezDUXg(BI#)eb6GQ?e5febg)RazHF2IZP{@E mI0ScxV08$(Rcsr;9Dx6i1Ib?}Fie^N00008S%6otPT8*HI~Oy?p}hBu%~ib(5%A}w#Qq6sA(C=yzWijp^gC!kL0j*>h;BrK6) zt7Ijj#GCc*p|HneXR4V`=l;yO7l4@>#wV#rO8>CH%YyZ43{Dbhe*}y>g40n7i=?&+ zAdV!-1&}9_bP;l#T-a_Cf z@a4r20*OQV*8$f6I5y>z(kqUk;<_J%CFQ0)g7bb5mW(EPi)y+-*aB+au_=SeD#^K~ zUl9uiEa$-w2iR>L002?hO>h9f`l`Rox;g1ryPSfV-iMHN#_ODF=U z4If}Rfc01Nx#}CS04g{Pofgo{t4rX=pvbKIHvP-lHcU oW4$r(@W>$i=E|Ro$BGX82Z`9c`b{iY5dZ)H07*qoM6N<$f=y20{Qv*} literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_Doom_128x64/frame_18.png b/assets/dolphin/external/L1_Doom_128x64/frame_18.png new file mode 100644 index 0000000000000000000000000000000000000000..78ad1c754f16682e9eeed168d77c26f049aa40cd GIT binary patch literal 497 zcmV0DA?M}riXX%IbCN;)VhQY^dxj}RUsT}(xr2SCC_BvR!L zaY}su{84z=jG(lBx1j|HC9td}V`ok=f~4C5sIOMuNJ?V1Sk zV@awan9d~WIu<>x0HzUC4ZxGw#7L4hqrwb9HUewC8{d;$189PxD2C`PD9S;>JSY;2 z!3WTf&T?u&=shR^Xt%+tL6Y=wvpQ7FvqvFk7-byBLbrEm-`p&gvnxsaaX7bJCgv>6 z7oW={rI_dhrh|TYzMti#2HY;-C+ppyx-0?!BZs);Sk{bl-Q$EU%_yU!O)` zUcixm0JbWD=Z>vAazXJ%dBdGQgasD6y5Z6v!qU-YZ&B+HVQXl4&o=DDQp+*r4u1$s zXTWk9_b~_TzD*VckU1zl2-Vg%KJZu2&)w}IIMIh#V_mXu(xTfN9uB&!9`g(82wKA@ zh()`1I+yf)RH){4aEg(%SX_ORRF6P^f6opj=g&+cE&SnUNv*)yf^pF8vv$ih+}LSSAJuFhb8M}EX>Bzqew`KXYe{IVUV=7 zK}biER0JU(OVVkmG0Oq+AQT0_yVxypuE8v? z-e-}K$>{vs=q4sa(WT)*%p{ph!7ql?T0T|K1s@$h3;64hVyq($|7g=Sj+g+896mpY zntKV-b)Qlm=!oW6Yju?;o|9TG-5?AZSKHEZ;s#-WX7mEpZV;w~npbS9@W`sjxx@{^ zuuQ;k5_%~B)7C)%U{v`D8kX*8ec={U_3>-F7z$Ip2{qOkt&NLrF1Q)ovbxI|ltHKy z-oaugtiL25tutZ(RPr=9Euop)GvND<$V~Bd*vtLLH7L3z2|qEKs;WJq-6#G_x!o65 izLt+uPgr--3;zIv)xDbej*Wr<0000KYimj|UWjAItQjpL^GnrsQp+P}6;fnu2U>BmWJ%d&OBY5nPK+3dw)Fd!}Go`9}K_b=^xy1 z9duql7Y>8`#pl;GP#L>&WnIg$v2zz~sJ0!Y_>*A|JG}PbItzQ@uT;apefO*c$oCcH zpCw1m+I`>`5@3S-Krtu5WN^Fh(YP)-i}Rz+49MpJM@cb|1J}4A5mM24V8mnPRB|Cz zCcthrF~1BHkvial7%L|=5hJaE)91LxQbokp1Lv`Faz<25N(YLu%T$951Tj`l_xynm zq^`%-s<_6|$cQ6bkshcbzsUnR1zq1uy0l>*~V zJo?tz4E%O|xf9vR%CDCWL7-YUDP3EP?DDCDKSd!Bi^zVf+f}E0L1l4w4n|be)Hp$1E|4#73|Z|cXmP`Njgnr`sL{MKl>l~ fG{6%t|9j$pJ^(Qt0G+e~00000NkvXXu0mjfJ-A{f literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_Doom_128x64/frame_20.png b/assets/dolphin/external/L1_Doom_128x64/frame_20.png new file mode 100644 index 0000000000000000000000000000000000000000..24693dfe47354b65563067efdc4c433d45bd0877 GIT binary patch literal 621 zcmV-z0+RiSP)=ZZvt{vXXYFPje@HT>( zk-{u~S-RaHQ<%m(AG8TiN~eJ*eIC`= zzQ_sO@&|p)A>WYe4ii{VQUxPz55Pl7U}~zTNqR*|U_Vwf1*j$`9&(u^1%ny?Y!0W-4d8vRo`Di8B_JGRr2Y>n zRMPoaRu`}^c_@_NT4nH;v literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_Doom_128x64/frame_21.png b/assets/dolphin/external/L1_Doom_128x64/frame_21.png new file mode 100644 index 0000000000000000000000000000000000000000..f08e98d81b16b096a8eb1cf3ed48097ef55f89c7 GIT binary patch literal 645 zcmV;00($+4P)OL~BO>q}wqFYSj*p6|R?p77@2qwP{y!4;4@hGGEa3zc9q7VD_W zwcCL%tm}0p6@T4KHEvEp(;lL!zcm41$mb@0Ch!U@;oNVHqmi=&(~ih47RhnWPcoEo zW_ERVDhZf7BoXJQz#VZhMKQI_(xZ)$^k`z6#`6`Cv^NpW0e0b7-hrZ6@G{`Usjegl zFR1h%N`-4Rdn`*^B+{8y6PBC4@T5-wHis3)qK-KWKnO22s=!y1fwdLJF#`zWz}oLz zYTf(|R@&E(CCT}Vw>5wO{IMiyw)v<5gb0lekxd;su`(-3Ktl>MSiBz<=Kkf>DQYxcFIFUb-%WrX)G z8@`C)V09g&P3<+F9uB4L1dtwUPkB;ajwo_RE{09K44A#4W_4FAXYr7BuwBAyb{0{@DiLmO)z4ff}W#xo*B$%*<>Kpf zE(YcS)%zb*yNwTJyWC6-KPA3pI)l0gdgAf6HCvBa4}@VGKxd26vf^E7vJRm5hZMQ- zuJo=>`@Pst6JYaKYV1jhZa1ZMuU#0}FZ;lF07{M}NwM@bJ-%k!;7_ZLKCe~%AwUk@ yy`K#lP%<64>6W7wxS^F4G+JOpVMU6i5a1s>mniYIcvqPK0000lmB5QRUlFJ~tL?f{8IN{C(1gG51rz()uFfR2<@q(MM~?1&|)F4Tw>{^U9n>DUB=Oa!Xn#kY*}RxK)P2mOfR3f;swP;4=+?4_)O2I4UDBCc}zp0>X;H znD1P!%$-%{s)zI?DR!pkwE-?TeMwTQ+|mZL!OII&#hw-U5avC#YA{O&l-;q?3LGRv z*vg-ko>KvAaFqYjb{1BkiE-gQXD3~N9%sN)9%Nyf1(LqRdk`p7>Zr9Q>1%KS(;>(H z5N*HkVSix>q}SRlZ7ps~n*ks_(XNspyXt8ZKxLqHmA2MCYXv}sv|a{DSX|KxU>LxL zmS$u7Ml~1pr04xh>Z%vs9WKmrKS7O&&H+cZ@a03xCxZqCb$~&6?R?bA+`Ga9_*&)s zGD-a_9Jg?>-n_#H9d1d=?(a)aBf!)yQ^2>!02=H|lGdfS&8lo=(}tuzb$=(6 pfI*K&KZR95MK=p2J(n_?@Ee=sN0C`K?L+_o002ovPDHLkV1mMWAb|h? literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_Doom_128x64/frame_24.png b/assets/dolphin/external/L1_Doom_128x64/frame_24.png new file mode 100644 index 0000000000000000000000000000000000000000..24693dfe47354b65563067efdc4c433d45bd0877 GIT binary patch literal 621 zcmV-z0+RiSP)=ZZvt{vXXYFPje@HT>( zk-{u~S-RaHQ<%m(AG8TiN~eJ*eIC`= zzQ_sO@&|p)A>WYe4ii{VQUxPz55Pl7U}~zTNqR*|U_Vwf1*j$`9&(u^1%ny?Y!0W-4d8vRo`Di8B_JGRr2Y>n zRMPoaRu`}^c_@_NT4nH;v literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_Doom_128x64/frame_25.png b/assets/dolphin/external/L1_Doom_128x64/frame_25.png new file mode 100644 index 0000000000000000000000000000000000000000..2f12df072a240ce300a71801f9859580713e41f2 GIT binary patch literal 661 zcmV;G0&4wIz`CGIv!mp(c!dw^_KQ< zI;Fw}3Yci^F$^pQIQ0e?a{$n9$<6&?sC>KK0gH+GvC;u-+z#WiitAB;Y12(X$LmrS z9lw&Euhos=dO2{j0s(M82KH|NwsM)d0<#f-hWnDFP3eFHEN1i%J(j`3cwB*kFJRG% v2arL7BS86j7D?(!IV4zEbDKa87FzrPH%T`RSkgG=00000NkvXXu0mjfNsch> literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_Doom_128x64/frame_26.png b/assets/dolphin/external/L1_Doom_128x64/frame_26.png new file mode 100644 index 0000000000000000000000000000000000000000..e90099bd680c83b7933d3aef2eb5669f1571096c GIT binary patch literal 561 zcmV-10?z%3P)E^W?K>`AG%0ZCWsl10p*HXXz@gJL9ug5V|yLI)9Y9h@9oO^4bsAV^oY z)5W2SOQ%Y`T13$yC$xA?VxGf&7rzU>>3H|!|9{{AeIHPIrKb8_a9}3Hl%|d2de)F; zO1u?QmH!b1n0cp(2fZuLevF9|5t+Nx$TU(H-Pz8&$RyFu5ruoFujhzq|7aF1p3*+#LNn ziy!lIaIR_-zkS?lUZd_b>^x3Z>vA(oJ$!(Jz{87|v$OOLquHYBMmHm3ck}+dz1xV# z3&FqLhJ?Cawzsdr`8Sbg5ekfKVgv=hY7s*UOIo*x1ctG7gTfd$Ah3vtKLj=rA~yy` z11P-&YA#St8}oOx{VU>dPdg2x@#$!7i(B8Wtv3zvb8QpOw6xPOdF(x@LD9KT&UPCo zHRyM~w8z_j8iBmOnz=NQ0Ek#@WOKLxk0_f=KWK6a*wOy&EYlvqzLlv5aAc*v51>dZ z#9{V%h!4<5yH#K%fCYeQaEBlonuY^Kg`%qJHh|k!)rg9!Y860MR5fu9L`2dzwJIW> zQ&Clo0Z55-0stZaDylk4KR=HN^ABS_t^nXKv&axj@~zH#00000NkvXXu0mjf9nk)= literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_Doom_128x64/frame_27.png b/assets/dolphin/external/L1_Doom_128x64/frame_27.png new file mode 100644 index 0000000000000000000000000000000000000000..aaf9e6654113cd8359034b5a1337a076cee19a74 GIT binary patch literal 583 zcmV-N0=WH&P)FCGDMUu~$mP1WMxIqJ!xmWYFgHFUX+S(#6G5aOxmp641$^j?JPS0}gg| zk+TRDy0{dCNCJHrF)XACLnl5^ z3z~SPqN)6kD8l#~2`t#V^7v;$94n=!FE`^1ZIP|@yrq*Pink{kIbzhFx|1UmoaBg+ zm2H)`bnJgDBboqa7R8@Nc^6)Gn+b6anagIF@1UQ{7}_?6S<4Tr8eYs+`V<%1@GnZ>^RDO!O@(lj6( z5;$pw&xsXO+%&`ED4>?HxcKJK(-vTC6{r`0axdOTYv1GRPHOphu)N02@AI9w$FJpQ zaC%9)O`Xx*xnBp#h^zw? zS|Z|65fP~YsD_9H#%HCJ(y#rRQpz$aA|gHjn$QgZ@BpZYNT@$QB}DX(NS;g^;4gYG VAlUOnajgIV002ovPDHLkV1k2>5$gZ| literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_Doom_128x64/frame_28.png b/assets/dolphin/external/L1_Doom_128x64/frame_28.png new file mode 100644 index 0000000000000000000000000000000000000000..6e4a776794e70af090439b3419f5cffa4d4a843e GIT binary patch literal 592 zcmV-W0iDY$fLDT;_VJ6J(+@=Bd5NOZDU+HPkL zG>eE+PDK=)6-1#~rQ#(*>(Qp)>+mI)FX-Yse8a=m1tqN z*cPlsO}w&NmH!vvz(BYaX8_NOd;t2_qz+hDrR-~>n%x8BV<7)S*oEF}U4)9A{8rFT zIx|1lQ*yFr=G!@{*}c?Kd2zz@$P}$?cbAZ;t4p6p!zAL(1&*CCh~EmFyh-$Ch<8k) zKA1~&5lWRV?HEbt=Ty!5@~M+FRv_ub zsa4$M#=hRl!Sy#&W64>b&u;Ky{6?gO-I>XS4E#8E`S4*y6OE_apk_BZx4r~e_SBdb zMiy}ZJVxi*f87BHlWBM_9{?iRPNpFQh!Y}Y7@(ec zaGR?*h?ATFbx~4;l#rhHI^3V!rQ$oj{O)<*?|GjCoL7?8#0L$F0clCn7}c|eY9_>6 zm6rQ2q5_LSZ)^Z-hdxwbR3}~o98|MkoZ`()v0*+X06bGB9O#ybS&jYn4rSs~>(G=k zp>1qS#93$+3H=;QLI6%Db8N>?U%NDpAJzt@Tbwd!rG5Y>ng>62A4MB7*>0qMye95{ z{BXfc{V;XoqN7~nI80hp!+eh+#Ma*Qa-xDp6MW8HUxW6!TODux8YpnKPDatYLE{WR zk2cdO-~Qa*{dM<_WOs>9??3+X^m5@Y*65nyKd-Io_{js`P94)T`3S}k#>E5~G}r>vOavo}%7nl9i_1;Pc=AGVlBO@+( zptQ*~u8zGBdEw#s^F ze}_aGNPduD+9=ea#$SLD2vEvu-vR&t002ovPDHLkV1f=V B1L6Px literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_Doom_128x64/frame_3.png b/assets/dolphin/external/L1_Doom_128x64/frame_3.png new file mode 100644 index 0000000000000000000000000000000000000000..81bff4b920923df18ae7515b430e50a9aab88409 GIT binary patch literal 757 zcmVQaWj&hh79P zBB+Uauu9AJkQRic*+#S;lrANpP?KFN(t;LOvccVScODP3TQ~3R`S85I9vH07p|9Pb z&NWZ`b-tI$@Nal z>ut>z-JTtD@Z@+)mc1wwQ9X0als6pUY2k3s!Gr@a{gOFe#fK<$Swy(fW$yt*-L|w1 z4Xn5|^|J%F3&2ic$&J0I9AE&u|3b;J{V_z6gg%ag=3){4M3m$SO=T)Ya39I)V>&OU&6-E@NCxd===|W&HaI8;7DOFpbtiQ z$I*g+UfHv&12$i;Bo2@tj#XQri^1Vm@BP;+4Xk~^X7T7i<%@6cjUk=ye6kECrRB=R zqrAUSXzYT8cTz4IOO6!i`mVtT;eN#{`bHC{ci^7y^*TsV^EMds*(Vl3~C@62FClS zs{SfQ42gT^t?>r?a|}T5ux9LzD~9z3&}M5DLqX#5j12Nz{-a^N_G>g^NL*ePs7s39 zYo}NP7;Q)EH7E64zJecOwX`S&g^o2jg&(Sh@U0i;9AbLeRMs@ZlEFwM3q-vmD8%S_8@227{* zTX!%3X02@_0KN3_Sxz^AKQg~l{{{~rG#Uz^Z2hPi04BQYP+WNCh(n8v_kbG)G(ZHM z1w`T+pexW75%~(hM-eI1Hh=@50JcMz84&!V`XV9*fWC-$6&wHrH>%ZAG(fA)0Dl1} Wb?(#s!l`Ef0000&Xg%_?OvD8!WF~~NGt+l zoa9=(naoQh2x8+c59gh~2UH(4zu6Exs2TE83rsr&AhBgUNq((OjGs0BGox5+$td>N ztZ_3sy3Ts@REOZhv~#zA)qi;okoYlo2@OiNSZcf{0QFMi1Axmi#|(hMdeVv0B|VJ< z-;)@CM!XYLlB_pRn_U2^{UYh%#^3)%+do=8$krGqLCydgF^FLdPM0k>Sn09{Zz~!h ze6F}epoz=q0LF000000NkvXXu0mjfq4S`R literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_Doom_128x64/frame_32.png b/assets/dolphin/external/L1_Doom_128x64/frame_32.png new file mode 100644 index 0000000000000000000000000000000000000000..24ee19f40b1f136720f6017b04747419d95221fc GIT binary patch literal 575 zcmV-F0>J%=P)jDZOTmzvd`KD#E6Z?%KoBH0 zf)*Ch%EChX6blL#h8R)qmfSjHvAY-VPjJc~&pglj%m@6RtLu<4V3KhV`GxTvjqKVu zIOkuDgZqby0P`idyzKzkFEdse3Sgq+97sB`10c0D2bH9u2uQ5Oo}M^>c*++Faz$!Q z86(>;l{C9yq^LsDwqXP&9>dgEI}EN=RM1<`rbiY?H=}P>Pp*y>+L3+`Y^j)WaA?-E zi-l=4tCl4b4MSe0a$|3&selp}o?CAk^1+l%pg!YL=A;(NE|)%PEC9peK+N49w51jX zNv<%n8q?NV2vJ&e*~r1MR?A6a`K`;u^P5^IIy>^n zAn@89pL#gpz@>$U%F4VN_bCrq6z4VI8SYOj@Gy8+m3{Y)PCmMRTkD}5B<^;C2hM-L z)2<518QWS0pkG^aV2OSVK!DxJ7upi=v1a)c&)@g1V`1>eV}QLchx!x?KMU}3?r5_? z6Z($|aBxpWfYk;BhoQCI3pC+D@X$eae?7_HDV;n->sK~TAAf?1nOw?2;vpqAt2dhn zU>tD7g6ZrK2~cssFj#EMUCVC@zVG|an>X-(o&|#p0F?{^&)y!a zD0XiSg46cRAb8N$DDVz@V0EW64~!~nCRv}(ozfHy3K)~6g2`jRL`R+22f>Z zas`q%15w20tdj*q)80gUYh(hns0wFw|Y3m^&IjK|(yU-~t8VK9D0NpJ%~OFt&H_fi{yL*YfLQ<%&G1^&32+L4 zcR}hm>h5TRwl?(8m8E0L??O>y(-}yz#Z(XPRS-bsPphI9o&bIh2#3u%ox*;YjX13Y oCOQ72x`DS6W*~beE8b`R0T@fIcAVKfTL1t607*qoM6N<$f+dae=lEW!<`Q`l)`uC$AdpdhkXh(f^DBIM>`)JD)kEaf-?A_z9t z@l2&)p}oi^v9L&SML5^6*=I3_F!F;Qu^bk4ynmOoOPtI{hN8u1te7 z^?Lx?-S0KnKk2KD30c)?gqui#8Dey&Tf$u7{STtsZRnk3

khz8atJ+}K^P2mZs`phzpKz_ zoHsXStxBi}JkZ+D*x@#!Kmm4l{p=m#$00{y*ym2%$`Oi_y}AP<7&q2EIHD~bK?1Mm z-`Y8W+L1y9@@BigY(W))8WYd2wVwX-sbb7EdALdjds+kDzWcsnMSvFo6qdO`Uj_IB zz*bVmYq<(R2kd>dvllmxnO~il`MUuWVUnhIJqy+jQZSuOA_3wFz|HydwuD1h0Z?X% nFOF|il|)Zm0AXQjiN5g{BX6ZK z#57h`Ye29ImV$paDB7HIMaXURvSTr;UV_*;-F)+Y@6EjT8~D%50LZ^0(xV{xt7H(w z%$p+-UUnGx#g)NR?u7kIhwjxlRy;dYwQ;+n5b%rxnQm+t6pZwLW%;n014P#CBgt!R zfD_hfbR`QF0#oi0YYo|e`p#$>;k_fQLN+mKyJb~s%TWJZ0)OY0 z2`k9;H532`@lY!BhCQi2EG2i7P$jY_P~MD-VxXhy+cpIVdO@gSnFFU_jAX0T6(HG9 z0Cb{@8RQkPodU$YlT#*F8pvM&B(rCB*A?f~1zQ04-WyK0HRGyoa{%SNmkE`ossq~thys@*&?~l=0a4ZE=o2K215`m; zc>eP7#&jSM_4j{-xt8X6rd{hpX2cZAg7Vy;?%BdrB{iu?6ChGw7F&?S{(%LElJ(h+ z8Wyr4&sjFz3l~`K{LW1Q-Fp?&Ou`j1?$93eVN>*`+~CeG+f-rUO~w{IGB> zLipotFKOcoP$>cNn3+&He2<;m^GKU)(5HRvufz_xSry{H<|qHivb~mq*5CjD002ov JPDHLkV1g5?AF}`e literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_Doom_128x64/frame_36.png b/assets/dolphin/external/L1_Doom_128x64/frame_36.png new file mode 100644 index 0000000000000000000000000000000000000000..2f37e6371c5542b5d84042d92642f6363c8f3100 GIT binary patch literal 623 zcmV-#0+9WQP)W4b&_BT&Ke>CJdwzHDf38J?>HyDnLMp!2c_Q1o z+6g`J+fKOK5EOW}4Hwr8Kybd@V<9U5`Nb1Z&7cYpI+K9^qb2}XT>S)0umZ5^EP&wm zPRp5_4#WP&8Wgv3&H@4WL$x&6zMPA|i@+y%0L9&$iy-qA{K;t_x%rg*7U&(|b+Fjt zP!*p?5Nv}#RFkqgRKz(LSpvQ+8x({>ot{lXZj2eL=3xLR)i2L-JW+`R7zJ9t-ibdv zP|B*0z*d>RxgD@;rl~aG42*{`IAfqB<1PjR_!ma1S}d0B_J9jhLP3hoLK*N0R!SAX`RxOMYNg-)|{g#QvfmR*>_ZTy2RX26z0ZowIi3-Bz`{Gg=fOGpZg|x!v z2E#|tg2T+RfsoX}UIYR!`fJiH5bJDGRK0px`9KIq$1y0>n{N$ya{BrXA2xk^d_{s|Cvn?7m6pRff$Gp~(3TePK9t6>670AiK;%s)u4w1)x2tzQ5D002ov JPDHLkV1jvB732T_ literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_Doom_128x64/frame_37.png b/assets/dolphin/external/L1_Doom_128x64/frame_37.png new file mode 100644 index 0000000000000000000000000000000000000000..e6f374c4fd914bb6ff37b0d1e4a8e873c6de2d2b GIT binary patch literal 580 zcmV-K0=xZ*P)g&A@d3l;Y!83k1=llJhdk_4dRX4~KP|-B-?B(f+LUM5$ z?6WVX!BSTvKy?C6wG4pwWa3dPXaHUAjDh@o{}cgT2~*cEAFBX?8;n8jQEwGs&c!J> znYv_kMOttqI-hl%Y0n^=^Ptm?3+L)c`CTO&>05)o`gnG8Aq9J-o-kC6&v&DVhOWpE z<8(05Fwj-`L7FVL)YYNfQ#jI&K$-&qf}JQ;wN>OTFq-&^iNyz!=x+`IpP;gl=4hfL zfgP0xVBw?r4u+Rix@u;!d0fJG$9d|aKQ5CIT{to>46K#fx+q|M66ku2ehtkPwE> zLI~^gy+(l`1p%yY-_jyuJpe_q)pXgx#Hct|t2zO!0TM*-I=zYjNe2Kes&sQ~7+J9R zYI!k01Ihr-M5=P_m?uEa0M3PkMRN!rZkNYrxYi#L{d=d0?=?b6LjYLuw)g`~3bzH! SA9U&f0000;Fgq literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_Doom_128x64/frame_38.png b/assets/dolphin/external/L1_Doom_128x64/frame_38.png new file mode 100644 index 0000000000000000000000000000000000000000..189a16f7b09a3e7820f116e16d307c111c6755c3 GIT binary patch literal 604 zcmV-i0;BzjP)f3f}EQjbQHQb z2Ei%!b`aceDgunOd*I@l4SUqfSS%<2i3>pXzSIz4+VoO?elG))bPS6h>ml~i zh9=VSRr|GPxxQ=>sMDV85oc3*L`u@{O{#4H%-^0|8TrIC?M7c=GbW7j{%lBDC}<=A zhiMQTG^$V%<9+BbKho4l5a3w`zyq;RQscWi1dy*ZHKGtr`|N}P$d9*b;ZRP763tW@ z-4}52P90#b)LBu(4Cc^Q0k~6Aa?xYA`;a1#A8OSi!2;Vh1JKQ~%HpJ&ej#(`ty7QV zc>SCL3w|maGKtE&!>e1`g{8)Y(|1W?D*inJyqIt5u#k?#bOGobTec9B`{hA^^|1{d z6>yOc3VHdVdXpG_?>G$b>PJ(b5<_P^fY%eP)dEG>^*yjXuOfg2yu~oOR`mkx0n{;B zezEKY=natQ@a2`G;ZIR9WA_3`9b`mi=~fW|j0IZEnxsZffV>3`d`vuQ&u9kwaTmN{ qy%^c_-yhYDa;tFw;U%5qzVQb|Wwu|F$cN_u0000eP`Gd--gTmU=el-*#nSS3!+U$`U1`|E7kba^@~|hQT^@cu0}2DhW4bM8)S3hE z{z4wOo3R0SL=v3JwE*^kfB%-`mW=JG136IT65y1T022Y9|3V^SMwftBQmoXH$Rlk4 zvk5*wCs8!14J=o&Qd^3OEf@h-&tajqQru~*QAOb>M9Dvp1M2&+P;0C?`JHeIM>p{l zuq>X18aTya#f`)RL;(MSh1!n^lqIDw4pwT1;tl|OevW(uat%CwC2~@Q?G7zmpc4VM zqgY8M92ksmp_b+xh$uI<=OA$aRVKF;^@ang=xU~TnY;sCvX`(>t514%$q3d!;iVBA ztOsBVSXcxHRRHd}ib4|{49F8bip?|z(eLPtJPexot(k$|ZOfIx-k}(vEUh1F-xqy_ zm9(jB(q?<~Q?U1FW{s_B=%FWc5A%zyzRk2f*pT=F~%bJ76C*>UF1! ziplY66Lflfk_a&D{Ix2!^~?OJt^?HndNx@=`grvWU{e35GfUTE`(XLr>5CBnV^jMM zLOFc9e-?6m^}8ppY{d4*;u}B2PGqTQY`qBwD$~gePM)(+=Qu>KU!Gltf;#uNv|5d= zck{{kG8CjnLF;L3{nqcNryXFt$$AR**`C)9XKAG+V$;tbPF{(&Kg$xpgU|o@_5b*P VBK{S-mrnoy002ovPDHLkV1lzhVH5xW literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_Doom_128x64/frame_5.png b/assets/dolphin/external/L1_Doom_128x64/frame_5.png new file mode 100644 index 0000000000000000000000000000000000000000..fa7e6ad10fa29f1a3b3ab562cad5e78649489895 GIT binary patch literal 596 zcmV-a0;~OrP)A#kS(_Eh*}P~3&JZ5DlpcbRq?y2=|3V??+OgwKVObE=rMrKILE)wgc5}~J zFh*b)^z;zzGMyY;@GuA;zFOTpB}8WR(4YOR@Wmu|8we{_y)u`&C>g94r5@{8v&u zHC%v-kkl-}NQxsJyyypp6dxBw&l?TEu7+jwIkO2zQ4S`szP0&MniCj(EtNrO-jM4Yb6sVeJO==+iHPn!a@K>WG#PY0!*;FbxB&*Lw$!TI7v!f zlB7eZnapsiYO${x)RUXUnQ{VwX@tDdcz(i0fTiSA?ZnRv3v4C3lD#2G2jM7Y6;``J z=dA~5@d0KHqNHh$tOJHW1-@b(xBd9GBke_tm;paHan|jpJAm(jw5bEN-NbdY2OzoH zl??54^a(K0BEWud`C67uiL@oj?QGODqnmwb{GAM<{7hQ_S1)=e^v&3p*Y@k|UJO_P z%-7=@oZGcZdune70T>_mS6&=I2Sb~Yyd0TWsp6X;2HtSIpxfW3vCAHgsIf)o_Q z*1+z>M7J?4VR1j0y~kqqi>3;(}@`WVRP0C0vN9RtrV#=zqIT?><& zx~nj~eE=Se13w;%QCjk%p(2IS=}3v|*G=S%fT?!j05=03fbS+R`d$veD}t?fYN#*^ zCreMeBMMXf9O0e~BTNWiY9Ec{pbhS)LCGXl!1YS~l6$fyk9ZH*uBpc zEJnQ?Gbl3TT7wSE%a*}NyFIWf3CzCEvQ7pCN#MR#Edwk2lIrI8hZ^93vHOyAsE=xZ z2fi&+lq9JGIgc5-IvPAQ=Ml2wrJFH;3weXIHk(^s0kAHyV_-Vk_!?#vCUzY7i$8Gy zkj9e{zU)g|NLK<$?e%2{WQ(cMVO>&t`xGyaEzu0mpVV5U$vY_ON^M0-T4mh?F_iUF zZM6a$Z0RQJ*n2wo8z$MH1t1l5t!AsBp!^4LP>Du&(_iO%HU_2A@6^?$(#m$TLv zn~AsG;V8vrdV7)`yJhWIbl%p(F9U^1xajbqlYtWO3mVMEeZ^yD*sBsm83=h6hEQX& zk(pMcM7FuDv<4u1DcOQ;R%F)(KuNb*k@6X6O0sje6)F2LCIZZ{6GQJ758#p6%@QH) zWrN+9s+qzK8C}w9B1tMr^-OU#7edbf9H^Q8Z^!}v0ChbmjBde87XSbN07*qoM6N<$ Eg7`NVIsgCw literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_Doom_128x64/frame_7.png b/assets/dolphin/external/L1_Doom_128x64/frame_7.png new file mode 100644 index 0000000000000000000000000000000000000000..0995b83bf71231a6cabfc9ae841919723320c08c GIT binary patch literal 597 zcmV-b0;>IqP)$iIG7M3w4S_}_ytVl=!FFCCf$PxM?V7f?p;kZYzl%0 z5*x1yq0vNcEdtW5%RCR;?v|dM%Ot$QJCZR5pJufFd=*?*&XRY0`9xqGRZOEdbOeDhOB8z>HH8(OJ{K??H3u1o1DTz zH0UvdB16#2Com^l2O~8G;F%;a+iGQ#49k+hUDHVhR<Kr`q zZJnwlNqq=BX1LOBe$l|A!j_l5hyh$E8Kmd8f~6Gz>k>N#rlXCImCRvc$ALfi6HS0L zo{aEipV)G`5=u(emmrj_rbatmNy)1Nyb`v?W##bRNsqMs8mc-~qDbvtT^C^tb^TDH zUTDKjU1Sq`Lmz*`v^VGhNL8Ily|Tj9HNZhN&cR>j>s$jssq{-yM``ub5PXr;iBkP$ zxLKpsT+wJ|&zytS jY=vSuaAI~L3jqEBB1;39drJkr00000NkvXXu0mjf*B7;{52=m%VjiZ7WNGN$ui&#$!KqUZ)QgB`Z_ zMvCSqGLRFJssk{Y;7S*-hJlfyR#tS%RDiq_45BYtRcMBNFo*r;ONsoQf#J{b8=m!x zE^A5PDhV>9BDlmk{_lQ!Z4ZdXp-f6af!^X!}+)*R;Qp1M4;$tblD@hl@G?oKf zUH|M;3DC-OtOA5_H9xrn7;hdtMQSy?=%Fi}gezD+r?lOj{Q42#l|b6lg&M`U=;{PO zD(YA=Iy%z*yL8}GBpCoEm2_HWDoV&LIL({nxnI=*i&+-WVO~e>^0QD zd`pe)p%i=Fy7WFXu-6h`v6dQjN%4cabn5qmf&DsymJgt$DM@NdXT9hz0Eofq{jo70 wzyTW|haNp)cqmzpJxMJo>4Cf2lho~je@zJ+(C7nQS^xk507*qoM6N<$f-@-;-2eap literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_Doom_128x64/frame_9.png b/assets/dolphin/external/L1_Doom_128x64/frame_9.png new file mode 100644 index 0000000000000000000000000000000000000000..57491c2edbb3da617cf629e659e4315fa895dd1b GIT binary patch literal 611 zcmV-p0-XJcP)A#4W^Gn1XY=Al;11!TOX)!fNty`^`Y#lMt{pq>6qW@+UAp^c7!+>0U^n-i z24e(YD0190Y zS?6n513NQ=zzqk$Y;@e$A;wS~?ioXFWP1I8D{A<22ubxG7)f!Z!*U z30$RN*Qg1OFf}j$Yf=h!RV$e=;Y%r8*j5893l;(}k-fY_87#2dRY_XbBYlS=xJgP? zlB6RjSxj@LV)tAns3y0FGvyQl%Y?kucz(ksz*cgmX5wdt4UUpM$=Q&k!*CR<2&?U& z_09vd_zXvP4v zCmHRw^ck?wBH$eaS8rt5lt^3Yo+_=4YG$+n%~1P05k&c&Hrh?Ne%U*vZ^pj7ao%L_ zVqgW3XWy@pPqHcT{?nRwlo=sdI}q@s`%v?ytjCz~EXUcJ5?|>rX{wPM_7VbKWL+!| zW&?oke1N@_rZC?CQ2Rhqa{G_89pu5lepP^00HE|(lGK)dcA`Jcj}v!_4h0=>37~-P x-lKmhT}b?_MzaH^w3&gp1I86srM8p+`~!VI7l!Vb3G}_zPLJOisK}_wXw<>5vl)fnyEcmLJ)0YM`-qsd9LG<*g zg3uHHfWB#8LN=zM6$A}^iEZj_uEAVvNj6Dq_mbTmAFd!cFTa@&W`4{t&64fs z1O6+|kM;zum_y0;LR;%E{4|RT5jh|65*n~`w!6HK#2L%5CukdP2q1}?sRvZR12EsS zP6BKKYE)f>e;Pi(R|nZq8>DGO9+1VXzqs;Y z>zzX{C5FwW0Jpr#S{j^JpU8;?=-uOueU6(!BS9RndoqvP1aOo&p}tH{j*$Y>F2CbY zJTRxpb}`rb=#NmV8-pBq^GnU*vUM}Lrs&3DVX2NWQRPbV9Eu_gHSp)RdG_xa2L$n& zAzo{&Ly7kt@}aaYyloeLFiehuLtK&CfvZv9i~0aRdxU@_`$?=4uDcSHyHB@B=3sjv z1Yp-C*-y?RIqd_Gb4_y*`NdA5GkU`(19o4%V_h1`c<^#Zb!*T(J_a>H!@b?Pw{we% z1dHI&$2X)22_qD0fIX;@)4QwO+?wU}hx4OTV?~Yn&m|Y9uV0s?!lVme6X@3g$i9tJ zAw<+xNi6@`UU{o9>1u#x3Q(N{@}pCiFtmK`&hbpo$P?pJF1bH9`LkVT#6U2cFbE**&wk(4~aXcues_UO>rxTx;jl=<04a40ce_veR#+-lBGlmLLRKtj8 k04biGE0u;J2K9h{0X->d_eOezlK=n!07*qoM6N<$g2W|{hX4Qo diff --git a/assets/dolphin/external/L1_Happy_holidays_128x64/frame_1.png b/assets/dolphin/external/L1_Happy_holidays_128x64/frame_1.png deleted file mode 100755 index 9d9012281f0193620bc458455f6a93538fad2e96..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 855 zcmV-d1E~CoP);NOWB)|bSkT1aRXRBA7yY|TxF{FSfXu8*lUk&&qCXhO8F)FkoA>H2*0;2g2fv& z(d#Xs;?yt-?E~*0`D{sLX^yAU0@$ekWb2zI3N8X{Fs83orr0sQ#X|guwcpNP1=Z{G zSdat3yA1rXr`2nki#Nt*x!NSyKj4u+UwHlRdp$A`_S7#~2^ znalc2P!kjpqG|x})i68iBGTxNe8ACYahNl>a$BAkJMa8EWISSW9x^|7!7He$DqzaK zHH~UJtLg*3q0pI4GT^O->aCwBq?&T_iw|3e_BWDe59<_EmFTb&R`!3M<{$oTix6M< z>)gGM21gU)@>GCLe`>uG{-^#CUE~ZdT5WwA&`g90koSOKH57+|(uvFv@ zu5jrw5_rGj>oQ@wX!V1Y;}RF$?qIcGbR!}y4R^HsbN*(0SXHAg%c)Z)Sl#Bty=5=L zsgMqRmyS4iNFy-1b&V$Edv0=qawEuV>%O>0>(QCF8Rm}903b;}$!G~1tcE58ti4NG z#Li1EDY$>yq#tK|B{A92+mN-R_2;-oJ?~InHFlt@zLj6-(yP6H#HwvaCccFQEMv5L zf=vPm?`HFc&NPYoHp(3VcND9qw+G8F!`Z&vP|e+|N$-{9)>3?5mJ2g}w){{ph6J4J z%MI1syar5eO=xJuFUo-&*Dz1D2A`uKvLp&GC&Xeh15D$Dcrr0Le)4jNZU3t?tW+aRjFfo7> hF5f7XLU{2m{sTJ-XIlu4y4L^z002ovPDHLkV1fh!ld%8* diff --git a/assets/dolphin/external/L1_Happy_holidays_128x64/frame_10.png b/assets/dolphin/external/L1_Happy_holidays_128x64/frame_10.png deleted file mode 100755 index cb8f173b0bdd8ae661a0e3c000fa88fe256b3a30..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 872 zcmV-u1DE`XP)?*-2wV(L$SR8{9F5vK7gWF*K9z%sdYhFTTrr?}P9A_;@_Xs~B$& zdb=C`2i$J}PvkF<5Lip!C9yJmJk zyD)1^<~b8;r*9}UQQZePJA_5QLN`m13s@3OkFb+2eNEr3q|e{3Do1W(`LOHQ*{Ccc zB5$Q4jUTjXj@h}Yt9&X@PQ4@to~kt#+CNgxBxB&i*V>Ql>6XdOn;M1U`%k`JdYzb# zxvXjd@yA2gSI!^4_f;b4ZVha>V{2(}QKcwlYoN5(9sVe;;QMGaV0UFMDS=xLDQI_k zl`c30L1j1u&AC6(zViz%&P+gL(A6Pd>t%<3qVh2Bofy|h5cY3#mMO5li|Y^PWBb5vv#*6Jr_FPc<~!% zzI?osA0I4#%i;aLLUqKAW@~uXa`(Ts#~MgIadhi(T6M8oL-0000;RtW zYC;GgHWEVi?&N>ACW2t*m9`GWO9_C1mL9w88*tuE7R2n%(<4e*f~ghW9``l2w{(Idzmy zutB!vuh7(q6FumKEzuwW3j>H)yTC$Q)<$cl-5z|y-D(1|M{-+S4~zn(Om^kCIMPHS9NRn-wo zb-G{73WfIdhPR*xoUE3%!LeNDq5+&-b6)u9wbmmCRLA3PDHsgjUH*FU4P5pLwkgFV zmxg|AeEVE-xl4Bs3UJRKUC+WFO%Y=k1n4^CS3kxxCIG~RLNWKb&+NDt@uDzso1(=) ziO{b`1j1wkskuLG`gr?8T!vo%MDt|Wxs$H6RMRzvDm|*B)6Vsz{HJQVCiK<`7I%66 zpD+s`5q~AZZ&Y`vK%F#^#?`Zh(igxfh#;bQczHYO!l%F7Qcd2Iw2#!O6k`4kFx=;dmqLL2aI=r`kQ=1B2Is`ynS&V z<-m2T+}iw1!W-ao64<|YDyCoJj>kKX`oA%;cSfI$NThnHV{pp1@th#<3}a7jmBu`P zDqsa5kKKJg0*?!;XMmeM@@8qw(-6&dL7+AUbPP_-V{~{se>K-hd3t2Z!}?=`PrJ;# zQnh39NRQzpt(~lq3*y4nYpOBlET<#Dr92W2XR56aRW)_yVsXJ!W4uiuWfyz#sBd~p naeM(t04ZO&R;l#2#RBjz<}zf`VzpON00000NkvXXu0mjf*Yuj_ diff --git a/assets/dolphin/external/L1_Happy_holidays_128x64/frame_12.png b/assets/dolphin/external/L1_Happy_holidays_128x64/frame_12.png deleted file mode 100755 index 5d4c7e7c55fb7012fa223c9084bfe754c5b50e4f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 853 zcmV-b1FHOqP)24#t z)$%{!Q45Ty-Up3=wft=c7FG4Oco@|*;BzJ3rR*z7I#_C0{0L^xB_+HdKGwmDt*hX6 zDH20Ibn!pi0Fyv8^LjZu2{ML(&ZIO+ZL(v#u~+Zi{QU%ZB`c)=A=;6(xnT!)w=IGt z=rYN(4d4fF8i~Na_14g#ID(kx5x^!L#~NS7XoQad>-6bk!-Etiv{*>*vHHuoDNw!k z6Bgu&=ryXNZat%G8DUk&U%K4Vh#e5;u5{tcO?gnuU;Vpo0uJJxMn=zn8dOzP6(9`7 zEDrjmYFK79AIVDKNJDk`d#Z&N9J_G3ad7`tJ->0As;ZJa<+8$t^j++69Ulc2wFZz}~l6 z2i~c<%n15&MiH}mc>V#t(I9v-Q%ybCJHJ&K+pAM;f5?b#dvfM$7{k&>F~d5MxP|iG zuzq5)*a~gZ@(lPVvHC{youmzh+FR<={yxpxF6ZCPB+GWTdbU|UpW2_RHsYTJ;tjyJJ$FNDVQx?~LS0I5z~ f@9uVx0t);KGZb(kKhPw)00000NkvXXu0mjfXi1u-?7W>nA=QF^N=Sn#Tt)k_1K?$nA#5M6H- z3SIFJs3+S?$SlbLxQwjlT}@4XMa_vL+g&bDiVrlO%h|I11tkAJR*h4?o`ocsAzjOk`v+05A z6S0AbLLD1$CbL%`nE^E%K>+(g<*^MDqF^JyR=fZHhu$#kT7e)w$r|myavG?9auyq; zKYZndksi}ZIGTPdw6*@?p1uO!w znz&u}0w4q@tEFA=Y*Y2x6`WjihCh3K<3L|C-CkX!q^iUR3udD4T<)wc*+BBe^$%O` z9DO;pT($(b6I9nTb45d9FA<=3UoiGLeh!Tk@xblL1OCRq3Fd|R3OxmeO3e8Dibr{1 zUeld&q4m*kk=8c%bL`D8HBUyJ+v!b3H;0AgI@Uyu>*@0-ig2hwxUj=>f6jU!NYIQ3 zT4Nn5ydO}Eq;>UexAYxw0t`u2rUq`rdtV~p$Dq)4{V=Ihabs3ZrTcVC0t^1*bx8)pZ36wySQ=&?|`G43`_qCmSb|uFJ9h)-AGPdfGG$gIuCG%s+c7RI1Xef zDMOT|0C&IEz1}^e&Fwi(K3p7`9xLnjeV5W}Gq-NZa%s{B7{ln-2ISGra}oGVt&uw6 z&4cn*Y0_7N<_b`o1d1cmKVj+UrGHN4dS0F!pY|#IzQs2U8)eLn$s>IRC)sFNgSj9c zT%A{qnQ&zVJj&x{G*jL9R6CvI^js_+_-a}HHpK^%`VRKOQ-O7;6vtCo$t)n{i}RJr dAS9px@GqP{Wg0BXhk5`2002ovPDHLkV1gwEk39eY diff --git a/assets/dolphin/external/L1_Happy_holidays_128x64/frame_3.png b/assets/dolphin/external/L1_Happy_holidays_128x64/frame_3.png deleted file mode 100755 index 3a47dfc170224127ad04528accd7b47104c33ae9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 852 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 diff --git a/assets/dolphin/external/L1_Happy_holidays_128x64/frame_7.png b/assets/dolphin/external/L1_Happy_holidays_128x64/frame_7.png deleted file mode 100755 index da3a78f4c7d2364f251efcf0d311cfeadf3693ab..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 860 zcmV-i1Ec(jP)HGw^KWKzD1O2?a4gD9ViVO(ALi=Ao3~!3nM+a`evB?;V{tr$^<9Ht z@Ozx-`5N$}TShE+x8EB(Sg}yy2m+X>`*`iE3Tp8XV3Q$zVtka;2@D3}hg5HzodeaM zuV6r)vTsm07j7o>+()SD%qy4MY8GPL03l%(8fhil9EnrbU|)z+i~uB2(+ps1v~_mb zU^43xhMmAih-NLo^Z-r$6p@vVC;(iIW>)CMlbf<#Y`*@W-|d2p} z`R|OgGP|3p27rY^b0W!r7i;O6Uw)vFYRd8R@6`71T~K%Z4h2;uI^c%mdrNwLec366 z#9s{jc5kkyehI4U0&E4N8_n>0h2Q;HfR25^&}VTGcrHNzuRRTD68cmrOE9xWI~;@g zRa*h!!Xw1`zY<8^2~%tI?Zu-K7rhR99V&Q&Uq>%2}c?`l}k+I{g3Ek&naV}M(`v%`QS-6Vs37`vu}7BFU+ zw1_z;d5(bhha}y&C&I*$mHU1m6Kn? z6o#RHp1Ib6S_zl4d*i*e8be^Bwr9Z`#3bzima(q3{<4?X#IMh*M(>-2!l_2~{L;4& z!N*-~{betw7I$4WbWE^ts-d8%#zHc2259RqPvK~3t8?OkUX;Tl>$Z m(G*Yzq;TDI@s{A_#w=kO&avP_}B;zHLpOl zBas;Lp^N{;2ABlG>9-nlBtgb7(4I6xQk!hsPVA5OZ~bv>g;d?_qHs8 zCFn58i#6aoZwd*)zy03OfjESiml41wZAWWg#%PF-0BiK>lf#1)##t<+_gVSn>=jVG z{sR`|nea`@qtRMM#}dMdj=y@fsTM+t=MXX`pxCl2s58CN>+<(53EMTR1?@1s!UhNz zA0T1yGfqsHXe9d1ERt5*nnQEm^aDj_SFZivmu{xz3DwlI96M&h#chtg~lh@tevO^(pN5SMgwUHpS{$p^2~#jlQbK(aQno=$AI7%fJCweu7e?5MO* zfPHAQHoOzFnGy80j3Q>`;M^m8qe1X&rkZ-Ndv3Edwp*{X{2?Q{<;lBW!5EfaifPse zC7K5Zz}oT2V$<2A=>_n6u=;lW{iFqkTAQj<{vOR*uI87flO;P-K3Q)(pW^Z8`A<00 z+EktLOB!;xepw@9-OS0lf);uk82K1zs!okzwEJFuxzGuDX<*9X{k6sy6*H-vkc1-A z4-Y&=ZNpqEPzm7r4b_BUlYaz4c_8Y}RcoQDmkiA$5`a@kJquYuQ+46qht*5 j04Y!0=|GNY91Bxel{hfU)#i;bjeXFf0r)cg|m z+s-(^7BF=7szJD24*=ER!!^!mfoCz4qOTO&dXl>^D)ywH&zYgxW*M#RUNTZ zr~AaLQfyyu=r(kNlhx8TIFjpJ)Q^*E&T}8W*1G?Y>Udn1qQUU(#jh9Nz-7N^o35DT z!r-rsZ=OjmcIw`J0^IUPH?!~uQ^MFq0Xh%+!yn-p69D2uv6TD#!|b>h@q#e1M#-YT zi_jmA2!zQ7QgeUW_VEscxD3AjvF6E;b0b}CsiqqYR=ZV4r=9tv@`q}=A@tM<7Wa7e z?=TA>5q~wpZw&8Jg*s^>jmvKpyS^|s#~^};=EKW7Q72{kDlOIIJxO~>ohqL9R~WaS zZiU>X=tpQTfTX=xKIz$j{av76>=oq4FoWFzCqO2oyoPZ?PlCeq8MbyKj(@ehbAG|7 zcWB1v7=iJOBZEdWHjA-aY7b&xlmQc{Dndu)E{2@XnsnFVNDlS}PQBY`^8LX}% znuTQJqM~4B#zm-92_+Bf zi2p4g0OtWXwe8q|fXG%h&?xNYu0n&b4sL z^QO4X%7Pd6w}FAu%DJt*<9W?LZ}sJzDy)43;DZ5hUZWUqm44ih4)g{eVM}X9JCviI{E9d-bNa6iy*vm1^Sk%H{e(E@vBc# z-aP=|TlA@2q#79;JlIL1;H0w(ER>@j&yA3SNAp3&T#H2>$DO}UvOsY1@;7Fh)u(4; zt7iZxPyTM3-}yp`+0emQD`kSyQQMw0=@Wb0>0bbdoVK6z&aSpu)4j*~fWh82J(ahs zk^ZhEfYWDrc$Q;hgYH!iU><9-BSbx#yM_UN8nQ>e1vk4G9Zmr(R&7rJ9b9?yXnzv? z9;|Z+f{l5Tx$42x*?ZPK)0H*h3u1W~-EK=+bL4|;=wAanQcUf8|2Udo2oy+Cdv8L! zW>BpbXhV_;PA=Cmfr9i4`d*SM(!HKE(xp(jyCf-y=RThl2`wDk>F<@KOZp)req5=I zt7oqy&E?_}H9tt7zmbrnS|wGI<6b=68QGTva1ZnsG5TzOwD^4e+eWX)-a-qNT3C{T yjOM;xOQ2ei-mUtO+R?u!EKPKl0XTtM6#oJKCt7uy9%sA&0000O9aKtjFBFS<5rrbC%}m9M zUgRQo7XRX1b*D;^N?Ky=V?0!jd8EhM#x;n(iGKn8%i01^Q66Wj%mffr}U0-1Ob zAb^oTl>oMustNt26ldfm(WIn8cS^nw*cymV$9HzJdP{oZX z0DX}26@XXvB7mt|#QDeoiB>l%gnMp9<1RSsACR#L4*RrA4QTF}P@O3{P!=X&k|;Kw z{GG>YUjb)6wR^FoRzzG(K&R@4c1B+{Ck6u80E)uu#8e?{x_(D|nn3F|y%Owv`RPM< z0svYxdV1{gIr|r%6wR|rG z_Ipb9%K2T_WLJOdjU!Ia9LO0bFr9z*qjY7Kfbu;WBwM0Eytoa zv${bA+da&=CaBL3UV&D9ecr}20W+2Rq}Z_9H}LJJs7bb1)9v4MC!20Be7t`SouEjX zTsClwx2-l*f?XqA@%P8snc^`e*gjXTY;jv*Dp1Iz~P7fSP6>B%@=;M*NoN z&=$~kJR8Xvu~Q%XbJo)dAR^srD5aiZ-)UR74#8G8#=Rxu>vO8x(`%goMkdxKM;_5P zZ5{Tx07J%srCf@u9_7-oIH4>{e&LkJ13AL?UJoUdiY%}6iF|KsmFfb)$i%E)32)oT z?B9E2TqG~)Q3Yqo~o zxmYX{aZx*b>OB*Z?-*%R3J$iGAKipVIPm_Zv=l_ja&*?#pgOGkGbAD6?ryXZk;1r$ zgpSDY)KMFee7R0UWLTsi-{lZl4&zc)SuWa$RAgPDvy}TEA-!}aN+5?L00000NkvXX Hu0mjfdXb+( diff --git a/assets/dolphin/external/L1_Sleigh_ride_128x64/frame_10.png b/assets/dolphin/external/L1_Sleigh_ride_128x64/frame_10.png deleted file mode 100755 index 5459ae27c3e0b2d87cd3a2b68996ceda57cd6f41..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 788 zcmV+v1MB>WP)EtRqp8xfd%lOk=O*D>zE{8R{hs%EA5QbX z69Qmufyo`u1_Y$GvzL2?9p9?;NLR-=tK3H!AMk?h0wr)JIMuBH8gL~tfxn}}F~9Hl z<$3d+?Z7x3h(1Z$TV(8#I-a(CS;x3zBfP(Wy?+u2f9ZjcsCemK z<&Wal6rHP)KJnQ2Yvtay%j@gj8B=M*sn}#EX+NR&Um>=qX!_pf8Yg>hEzIEY$uM!~ zuUHdl*=yJLoQZl3Qs7L?KGb=y3kHxh*8cX{mbzm{kTmB!c5`-UTn&J< zi;4=Jp|JJtQ2@uGxF8J={k*IYuw2(H9Nf*1CmUz4QUwRhM%rSr=%v%@1-VHpG zX>Y!S*k6sK>@W1)wyDj22^axEz0^)k#2Eth(ON0dDZdreV{iwM$xVfJ3=R;nmUjlM zkIP-KkC1@=DueNqUDFnguRnB%j=2~?_}!KM4^rcK@M}knfZv+b-#PpJX`*(@2BJ)} zaNeHDRUsAFK-0he>Q9Cj2YSXfuX=A1S-C3gx@!a32OlmiuH_RBGzvD5ICz5x0uoat z{>dS!r6pY}w)Cs(4!d3ZqOY|2)f=COOUEq0l(VRO$UN>m{f%P*rjW;&`K2T7Tmw8Z6lW$uEFf_3*{&fP*BdvUl3oztSDYF#=e*{#<6Y z@2PT-f}$j~78AujRW#=~%x_4F-kx6gRJYs_*EOv~0=S@wNBws2I@rmXS`A)kr86X- z{0xjPjE+M4?q1+r4UAUOdqB_spqcWYb+E}M%Vo@AOo3b#kW`T*X<5;T>;4B1IZ1|x S#su9y zXzB&EP@D+}ih{(@X_M%bTA~QHMVd@Y+lgp0)1+x;&YU^F4|6efz3jcey;y7gKP>j} zzX$+u{sV*8eH#$awvz2^UTou3wn^%BjQ2M!)KLP8Whk`@B*5FwGi6BBK^~5Vrc%Bx z!!zfWgB5Ly#h+_Mtr*Q?Q|*O~S36pyZ&YD)F=)-@3=BUtO)xW*`MN8{(m6=&19!`NGz28WWYy2D z+1q#;8#7%4>R+{grs+~ag<|;qeU-2dT)UpNLrnkp%Qp?vkksdoM7O&2zCB2q^rAsE zNbCd5P9!bNM#nGd%QDb@1W7?6sy8)N0=&nOq>e}!Nh$?S`fDTL)RSjdZ#ZYc!?#qd zxhr<#Lh17CddFkQqNHjmoZb7Atjo}DRGw6jONIUf+tP50#f}GqIZXdUSuX>36-<}@nO@B3Kfof;!_8)rw+(TC;ykfz3 z+Zs!+ik#i9-MjZss&8!74os3!|1P@JqO^C)hRgXd3V3~VaW*aQ(dK( zBVB#P?3!PZr114b5K-1ir#FsS0MmVwb-Aa;(!pa$k_xATeam5Q`irBg?^UPoj(3>X zyvk%>=v;R5m{&()cOXqk3LlW9 u3nR;Mq(w=3Ly{^F&W|AlYDO5H8~Yc`rGxxvN7noR0000kltwRJ zHqlzsn8JD1t4U*p9c#2VF|pAm3NBb#Y=vFe5@y-ic^+mVzRUaPd*A#M}wUV0b+eehAL4h6(YK+)S~90XEeon$0@jRSee z1**;&2U2LCpW?xeSzJ_+`+MV|XB|L)n`aCBzmD9N9DuHZ(CO(LZ-gH-r2tr3l^L;t zh=jz-8J6!X#34GaY(O_2UWzjVn5E+Yni{+@Dhu{LyZNn9T?(s@Y97h_%fF3;*|l64 z4QBKGnSYn#VJt@ClE2XvRIamHipXqg_Ee4_ZP<)L#To26V6Da&ME`Geo*HOF!ECLr_vT<^KPyZ&y? zf)c1S`fIsY%e9pgb_i6X!JW^z8JU}?A3#?OD*E0FdvVCq0_AEJVIuEarTvP0NgR%&>G&UbFoz$}i=uVA#~b z@Jo4rqi^uVLmy*$I?%p8W;R6omrW~_o$vgsqp~|Tvx_aeVN(MJ(5tCT&tl7lb->bL z&s5C(Vk+OL&9>t;O9{dBF{%qaEO$IV zv>!x*Ga}WfB&%{VxnU`R>F;g-aq}-PM6T`+1B%6<^tj6iUP-m5bf9+=RR4pYg2rr{ zr37mC`h49p*P9mG&Y}c37`tV9rq~y6ndm7OrU1`WT}F2X?v_Q$YsH{bq_CcF994-# zsQJ&G+jo{N?>y+n71xansHHc-xMG2>;u-iTC;$NOo|M2iWDSBIL?2C`f~p6notk`0000R1tyh>M^I#_qv{QP$XrggLvOd7m!M?9MzFFYo`y^Z8emhj(nv z&MH@wAp>JwRUy-zK)tsfn@+-UZl|( zT7umoary&@;Oto&cW3Bxp*q?DA-WPrA>Hx(-0YX#T9+ErYr6N+8=_3c0}@aG`layq zN0$!)qX#{0U+lCF<*WJUhC+sxnbha4L=+=ZR}5>LR2JCsG3^VgcA{F1 zl`I{k9ksc1ZQRVK zs9{9@mEvq55&=BuE5Rz;SH`(93Ah+Q>i_(-uGBK1U6f$$-P@7 z(E61dDTey6mx6n*{JdiTU@i-Y$m03`yu#^%@1Mr^mB8YP>BfrpKk}dvmX*M-&%Chl z^X>1_z%FK#(CW1d&HkQIfJ|&a3Ab}S3uDRBV(L7#cer#4ps%S;ym8{*ajG1z0Yr|u z{Xpp)Y_$i+0q*3CF-`)`7eyD~@B#Cz2I%*C_Lm^JS}}PwK;(S(miZxq>)*I$GhQa# zoxo!sB6A1#DPd~L8DH6BTi&P74SqA!tz1T%l3RUw>0QI)JDF~1od#ln~EgM8``UT%NteSRUuc||C zBnU9%Ym+e8N}%_NW1xh^KT1|xb;RS-5DZ*8Rsxg*uV%r(BToac4{`w@QomVk$$b$K cTk!q=0mCXct2Z>fF#rGn07*qoM6N<$g2`}%OaK4? diff --git a/assets/dolphin/external/L1_Sleigh_ride_128x64/frame_14.png b/assets/dolphin/external/L1_Sleigh_ride_128x64/frame_14.png deleted file mode 100755 index a5d6c1a7a59b8116e393bfade90b5891981070b4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 813 zcmV+|1JeA7P)1v7b7Rs(o7gnOg*DS66-E$$HkCr=ieeTj1et54L@9!bl#=W!D4i)HB3-mD zBK|A}E3_q3jZ`XySZ7>F0;NuAp%t~swa`vAaemsAnaSKauZzi~iDz@Z?>R3o@Aq*S z=YJ;zz_|-X*F75$(7BfFY8CFvt!#@leT=`rD(Wo(#p+rE5BxZLniW6*8EdNR6~6g% zU6f*RCS2RQZ?Wy0b&8$SvTJiO;lI}MFaX>rG!-oiw{H~M8ml10#-{6slXoSKdF6}U zzj|z0Wy%){3Wx>Gtr(Y0qc}Bo9ARYw*+gAcqm|aUnEQ9r;m&Er6R9{RyRj9f&4}QK ztYe+X#{l%Xz{2$p7vpS50&I2w9r`r-`sFNud7MylDk&`m0A0t8yl*#Mc-|xO$9I6g zOe|Mc?Dup=A>d3-ncTx&#%>q^(%`dfZg;_ro*&JK{&~PT!5#NrtgZLNIfm#<)8&E* zFAn(aFOoeu0sTg(_Rd%F);6L>F!u6u#~{8~orUv8Fm>_F7J$%2U%G&0>g%ntfW-- zec$&Vk}J=g8cHOS-9Et0s%s*2!V{5TuLt1}uGy0fVQ`ky0sQvnlH2_0r6`XaLVs`e z9athzww-R@e{A1pB}yHO`%?`mq5hGW?z`%Al-tgfY^~QLeQIyIH=w4criPVln^##{ z8ajX-yn47^O?;G>jZPlTh*-XmYuriYYEnwSB*aYZ)|G?ji}N<2Ym-sQ+3|VzCzWvm z<1sM&XaM&8L{W{63FTzBrKg3Yq8j_8+tS3Y{qxj%ic;ePrNV@+ovg&j-+C{jM#a8u rQ>r~%IE&P{qDHY?HF?>=f6x90pDlLgl6o*(00000NkvXXu0mjf1J{uq diff --git a/assets/dolphin/external/L1_Sleigh_ride_128x64/frame_15.png b/assets/dolphin/external/L1_Sleigh_ride_128x64/frame_15.png deleted file mode 100755 index 57b2c9bbee26fbc31ea9e384d8ca8c44936db89f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 879 zcmV-#1CacQP)~9or#-tqXwE=trZzs6*sn#Qc^H<9#z~Z znvGTpHdzRj*EHGvpJk|zVmyz3{er~ zRJ4A-qSzE22f*LtK!6Znr32yxpa5V8kOUDc9n>+g7yva{Xara*@Sg)>UsD<0H9F4%#7+c6@3i;;3lqVlFP?gN7EtTWC;%2# z#Rt^ml^V7kc?m2Jsjg(W_|fU|hYloy3n=+GP3T~(?GFi-licd6WZ5MW!Uk-bp0CnH zdV;G!!aw|$6jWZssSq8i``+Ff`{ua=Pdyq(|Kd!Z+7C|A0as;{qC>(E#(S*jb* zz%}pic+asr#b5aDQ=TphvIIU13}^V}&P1 z7AI2-Uz|81a{PEhru0d^T$|K(b8`s#qVomo%#_-Z(oa63UFf(JZk5-&^^##mem^L( zQkLlI9`QY+yJ@_a<)q6fgQ!Q1hAV%kJr^Dbe-7`Zln}2uGrz_1J!{)7|137+9bI#E zCp@g!Y1DQ{b8Xl6lSN-SjJjF%k+ErRSGv>zbOO`Mn$Eh=J2wY_`qSCMt47NYttvkR zpdL=MC505m65=&TyGYK@|CmHX65<^qCsLH7w@vUlBc2OY&dz#+Nb$0G;`s@YvCL6} z$U?t(;>nmuPA()h6rTVf4oyT-h~#c1JSmHa_=&s8e*u(ie_+i17}Nj&002ovPDHLk FV1imcqT>Jn diff --git a/assets/dolphin/external/L1_Sleigh_ride_128x64/frame_16.png b/assets/dolphin/external/L1_Sleigh_ride_128x64/frame_16.png deleted file mode 100755 index 4be832c648910992ec977cd567913fc99db14545..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 855 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 nfJ)I<216I^jeL^CZ$d11@;kWVw|A_%ItK^W7EB z2jgA3l%Z1jQH?Ge_7uDKzug6#rBm-o<-5Lk-#Z@2?=%7|twr@NBbLC+Rx;hHQD*uD ze|wjxIr5P{yUD$wxhVH7j_J5H_964YtMPjLiaJwT z*7BuKEV*h6#E=>zi_P3y?IG+yQY_t^y_0KI>_uwCYQaJfxd3JyX~TrEbGqdNY&(PI zHcxB)u-+U4@Sd}*?3>sOyv>c01;^{LKq~gcuB(@i0nZmVjOOF_r#PQU=w`oDVBM1X z=@2{|9YO3UvdxHdhkkPtE88iLo`pUm_FWh|A75&BJ7mGF`jx(cLiX>*MYv?a)nvbI z9xW1D^s&+PT5o@l)EAku^Lkz*Mr${E_f)g95^L}I(p_Si#fUT zpLd~H1V&H%ZWJDP$8A2gP)boH+IZHsesm_51t4+468n4!v$y`G4FQ7@t6n*@&D}15 zQ?T&I7+}8k0sg{>@<7Y&!lSBjCxNLXhV6eXKv@8UepLL-izjJl_~_k)&GfiYsYKKjefYg<<;ilyvRvkXk?o z(^{5-Ti!w&T+AqtqDP9;YVGErOHUs2a_KuCX-<-)Dd}3NoLUANfIZOAa`EHXP`8`Pm&ZK4P(h<0c%ii$YU%_p4?zWA_o8@|Yp zf>ie)v-71k0U3(S(Mq!y6%{KunKE4OgKf~dCT+UqCb_xChopF3J~-cx!^h#{Bu$yI z=CtRUgALhN00=iY5D*0-`4;g!-~%`do859zHX z01xIuqcI@iHwpXUb5v9+jdv zfJ667{=}xp!B#6++WM!CtT!AOYZ(`}Zylub_fsBpfI)A1k#eo$-iGpQ0c#n)Cyd2L zW*Qyg)pki2z8TeR*CkvSkZL!irJK_#WwR*t ztyFJUs{V$nYMYDXXH+i+^NkaGHT!AS0^?9B^lsYx;B4 z%D?)F(^6{XS%nQ3DY)j-F3qn@N&Q(Q?K@PsE|n`I(pRnnOxC;qfmq~c!rLyA%4^~A zjFg&D_?v5iSkyd{fbqaH6Rrb9hT1;+19k&5%MQQ?wmS)37oOSqTGFP0`}-D;fq5WL zXO8d6QV=B0N-BhvAF@;fNka)qs_N}UmoSr(zE4R~nE8Ay>zL#?>`4QdG6Qa~idZU6uP07*qoM6N<$g5|EIEdT%j diff --git a/assets/dolphin/external/L1_Sleigh_ride_128x64/frame_23.png b/assets/dolphin/external/L1_Sleigh_ride_128x64/frame_23.png deleted file mode 100755 index 639834612c846903283f6ed6628f2c79c3fd6a1e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 852 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 IM6N<$f@c1SEdT%j diff --git a/assets/dolphin/external/L1_Sleigh_ride_128x64/frame_27.png b/assets/dolphin/external/L1_Sleigh_ride_128x64/frame_27.png deleted file mode 100755 index 962349bfdec5503429689825577af262921ac6c3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 828 zcmV-C1H=4@P);MYf0)t1eLxntVNLyloX+DW?F4xn>Lx4kC|lVz2hP?>BPG^=lt&B&;Nhe z$Nydh0DT7xZunLpKw~}YuQjGxC8&`#9c5=&N6iIbu2e6O2D^vhwGJQ;rz2I`xM9`h z4OZsney!Bc=1tm9ZrFI?M~Uiv@ueNAX(#}uX4f`q5^7~o&di2&DB5-Eug~SqtX1DP z_x?f9>aIt8y%2v0r+v736~o;DEi^xOh-v}}MRKyT9$ND;fSLNc>P3gEkVkZT&0JYT zM56wYHIUNPsBVi}1ts_tc>iJmpgIjuT269gUOB)ft8$JgKlS47!h_?0g`9*kG0=KK zDuDJpm2)Cp87qz(e6XN$&Y7-CY44@1+-%XKU?6$C>!-d;eF*pXpmNUCzFW^uFOL#* zdw|Lnw*J5;@+}xLYJiw^qRKm&Htcw_)b33 zUNqEb3X6BW3)_NBrX2OtSTd4OJrK_;W?Lp)P+=G^$00KuI7fbI-c;0)xY~v9)6S5* zXSZ8bRxvLAY@zUQF?w^c$I(=<;fF$we>%ki@tEa4`EDLf|WcomHtkl$Pt=2S9cG-e) z5_8wBH44S%G)78~Q10~6%8j{+4DxU($qj3*IpoZ5j|~@^Ui)`TxV0C|wSjr>!qu-y zZkvx?IS1WaHM2&Rw-*#8yCq(l6|wakAriAN@4v~-jJ0000C6`P9 diff --git a/assets/dolphin/external/L1_Sleigh_ride_128x64/frame_28.png b/assets/dolphin/external/L1_Sleigh_ride_128x64/frame_28.png deleted file mode 100755 index 2eb4456cc780a288fda6b71ff3ca99b931c51b2d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 585 zcmV-P0=E5$P)>Nh!OB=PyYTx622!q|kdqdsU?O6C zY`-77ka~b3JP3L3LgIs!0Zd;Hk+vjd7p0MO?%AFTaRBBC085+0@+}?!{nZ#{lBAI| zk}kVDr6OOCFON@Iau4&u`Bx3c7{kY_)lM4#Ej_Q+6-Yh8Z=k>B*GUKAX*Ese$ICP0 z8L+!^i5ZYC-uYR90EnFzb<-`4*OguP)Ht&3V|Fb%C>5PO>Cx+2p|}NS-`J?gV=)ir zz70x62H<0TQ-CQh41ETce^co0FIr!Wq z)xD}&RR9ok+5ix`BC#9JkkfQycE+FiSLaaE>t?%f+t<`R>!|U-2%8EfD`o&RA>zGy z-TmLTSAiX{>=J&-=7C$F==OWd64&-!+h`ToJoNh)^6=0kLjn7IB#T*5XBMEG*2HBJ~;}B+3L_Rues8tiw8w|~g@?NH`D5Z=;plA# Z{Rey_OQicOEv5hf002ovPDHLkV1i=g!{7h_ diff --git a/assets/dolphin/external/L1_Sleigh_ride_128x64/frame_3.png b/assets/dolphin/external/L1_Sleigh_ride_128x64/frame_3.png deleted file mode 100755 index e5772a672f9a63d511a03cd14cba48df73df3662..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 812 zcmV+{1JnG8P)Nkl)On1-IBH1q<;PWKD5!)^K#Gm-NXO<@41Ze zzwiNY{sXUWyEY)8t(lEB61#OO-ymB$!zBLg?gY45Y!*ns=rxXRW&kDlp=4^?JLamh z+pLsx{?psQ;nd2H&9$@0>|@QooFtWY03Qs1^AczMhH1c3pIA$Wnni;CTdm=A6HvBD zGSA)yb9kU=Vo_p8H@9Leot2K0zwqQFTQ5l6j6QXzX0%lrC%R+u;nrjJ>a~VMdA&GS z4(?H~2|`92GnJ8N11I`10J~7|-FPYNg|#{NK?{Ww{z7T#*F1oE#NjSICp*rq_k97d za|n?Nb}N4A-5~(?5K6>r4=ISt2ORHP%UI$1<@7syMgR_H)=m8Lea!rPr*}v=q>$rkwl*0<*&ZC zU?yImHA4m%OwQOyZvL-(F;7H0sSqT)ZQ<>F^-XwX41l(z{m9Jo9PDVVMF`%KQW{s)bzRqu$o?Gz2qm&E!1bC94yDt;WaxMT`~j@f z9R;OVIa7k-8>=Q5U;fO!(|0web&Z8)^pVL4F?q0000P43(XaExcZC)XO5g_}>Ap4BQE&%nc>iptZDLR|u?6uJVm%UC@+kLF@2I$vP@~#&R zxZV{tB$?U`%t5S1;qJ5m9kDuJM}JpjA48EIcNKk+9(EOFv?MK_o~iwzsEP7#?DW_H f$=?+J;Lm^?$5Vh#T320I00000NkvXXu0mjfxI%Y8 diff --git a/assets/dolphin/external/L1_Sleigh_ride_128x64/frame_31.png b/assets/dolphin/external/L1_Sleigh_ride_128x64/frame_31.png deleted file mode 100755 index ec47b899c994843115098e2b481d4be9e3545bd9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 270 zcmV+p0rCEcP)g#Y&IE8RMH&I=ZT-N9#$G7Z;2?CV z!MD6x1bD!iYcNdCSF^9tY4hauHh`CC?40-Z-=cvn*3{YeK2>8 ze+2U>lk@GCfAh=B#t>UH24K-BvC9r%eZAQz=j37B8kTNI77Uy6BwY4=Pwc+n50F}o UZhAxmRsaA107*qoM6N<$g4SGi=l}o! diff --git a/assets/dolphin/external/L1_Sleigh_ride_128x64/frame_32.png b/assets/dolphin/external/L1_Sleigh_ride_128x64/frame_32.png deleted file mode 100755 index 3f345b563e9eeed887691ea64fd476c58e9eade1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 236 zcmVukXtLRdY<7&Yil?6l5smS!)4=ixyV{H|i zEVZ~qQTSxJvPvwAS+^ZYlHpRc4b{SFksE~%lY85BfJbk@bKKYrf4%(Tbt~+$){Jx@>H0 z9v!U*E>kPOV&QLk{US7P`x)X2@FjRl)L3A-;}s^}z6-}5I>m6R*COBYii7&ERd`uB zg+3&W`T8Obyggs*z8c*$1-S7%TAE9JE`yu<`gW@2bNRjVnA)?Z&*lEIf=!Xm246;w!wPWp`HmW)LcC6j8v((m0$X#-)(gTGH^FsQna8H2~_{aHwIgSCq bjE?>S{e=)!nb_>V00000NkvXXu0mjfi(20( diff --git a/assets/dolphin/external/L1_Sleigh_ride_128x64/frame_34.png b/assets/dolphin/external/L1_Sleigh_ride_128x64/frame_34.png deleted file mode 100755 index 44c4d0d498b2ebfd6caf104f011430dfe5b1d362..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 771 zcmV+e1N{7nP)-)V`@Jcg?b! z8h15C(1l>d;+xnmB8bn0s0B?%Nll~j^76-I($3t^#mqxoUG9(XoO|xacaS8dU}7Ul zvH+yz0pJ4=0SEvKI~0J8h$~QMwbUk6iDv-FZCWX+W})l<4*=N!%mPs`Nk=`_H&U(RQN03dn~zOR4?0F^bcZ+3Jd|f@+2ZE!M+C?0$2hh zw{b3pwgYGh$>~DExEw%JKEP9k2~3drd00M~y~*R%-#{v~l9@dz#;0AHy!)E}^O+Rx z94I<&WVukn2$BWI?n_UP21jE!C^3x zDZI1qZ-3U!AASDMd$7RXZN&e z0fPM>xds;872w$+fB2Cr%~H`{g2wRTyVJ=QSO^(AJ6nsq`o(wFD^F{B%*=)68BWF} zXvTF8XWqhPlMHg|MfEYgan$}7+&OQ0>r%*vAZ@7aw5@LMqSO@9nv+wm?Y$}K6O~oM zi|WC5xD2E}p3qgPtJartKM85w>$fDS`}vAa$%oo~qjz(2j6=ZJSTHBHSJMXE^+B4= zU?IC%uSc~Qb0kp_Bdvd-<4w&=Yi?5=q~4jgms(nr_H|mausIPt6w!cMd<>9Q)~-r& zZE1c*ve0jw1WX8%DOgyo1(dtlNXk_={mO$H@Gr*&ZJJBPG>ZTL002ovPDHLkV1id3 BYq9_U diff --git a/assets/dolphin/external/L1_Sleigh_ride_128x64/frame_35.png b/assets/dolphin/external/L1_Sleigh_ride_128x64/frame_35.png deleted file mode 100755 index f70ecb3abe0a34f98debe1b004eb08b12c98673b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 887 zcmV--1Bm>IP)I|X#-uh;n6?-_SZ&e{l2{{$MlW29fAFB}G}ef6 z^We24CdMpP8)HLjLOaozka(ay2$(iyAm9RpvIS%ZcJ_NcEbH}`=Y5{{AKi!E42k#v zphU=jbn`Zm>HmS%9n|H~b!8xcSy~v6Rp-lqj970})rLVV1{%PFlE?{~Lcju=gp(qd z*%bni;C>+|5^&gTdAuyV+9n|%4%uZeCHnQFxLmLD%HZub4lT)5>VmBj+N=Ys2_K~^ zV1&lW)zNY+0M!g#3me@gLq(%!W}f@y-)j(p62OXcD-EhLnzo)IU-P&fxLXyWwT&xb z|HtPlrdaioIrn2=-l|Ag9$u{a^!EC)Ppq60V{voLo_a4%xm$}zC)9i+&-Yb4NaVNu zT}A86)|x5aNgZC!$fT(ViYpi2T@%Uml)-}(Hzy{iL<&;QO|N1JE$x+v{x@M(!=v#?_jqm~nttmr=t z7iljxDh0nY&TkKQ|yw=xuTui4JdN{KB-Euw&K_M zu{BbrM#JrEfIY|m{;WB-CK-FYS(nvM?GAtt1i*TU3t>YvU|1~Pc4N&XpYNDWq}(Rp z%1DKNpNATlm3g=#xh%PX6<18Wv~9zwZ|N?vGQrSH_Q|xNkpJuzI{_f%wM-vl7TrJv3XRa0I{>wWXqt=#1!scW6rfAj(5QHzka zQyVTW<<9`DcMJh@nWN{PdHpKD^JPY&Lac7bwMEwITfBfU#P$ zpzZ?at5EsKk!a|~d!n)&`)d%L`FKEdSYMII!X7zGr3=u>>pS#Q)ytacEojC;7Z+o- zf~u--d0yT_qhcE=ol>1wU2q&x&iL;z`(Ogy;s!}joq9G5+oR*Er}dKKpt^WLd)oE1 z>NPL#d7`Mhy!(hh-O5rh2nM8MqRZx9h;M!aL}7YtJLn0QcYQGMO@3zpl7-zF5M|-c ndusK)F7<&Z%d5Xbx48TZM|mrzzUzGF00000NkvXXu0mjf3B8d` diff --git a/assets/dolphin/external/L1_Sleigh_ride_128x64/frame_4.png b/assets/dolphin/external/L1_Sleigh_ride_128x64/frame_4.png deleted file mode 100755 index 7d970234f57cdecc7e45bf765ed313ad92c3161b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 890 zcmV-=1BLvFP)6U8pcMp@S4V4`@4ocyo`72k70Lo9~?a<@?V4 z7-HkNVF>`RHUVn@0J9D#kj-8!oah5e0M0sEsTMByP~%y4 zifHE_S9;&`9T#56Ak&f!I@>Q6lLt#!o~PvQmWV0$Q@q{!jiW({!Iqm>)!xxJ?AFuZ zU`dwo>wNprw)yQDKtzs+Bql2VEZ#_Xp67&V7#vKE*?+fZ!Vmso8&H_w08_NvAH?=jCdf)!o1&-%i|OKByIox zqfF}OBHD?>C(oWM%kkR4&uP=|I_j5b%BsXW8h&_k_ouieho*b(@fTn6Bc%mb16K9q z*3XMWKdF`T9i{?#J9BX{Js+F+dx0w|yy5+vx-q!{q<+4=$2V`|kdXo3F66A36uoXB} z6B%s-2h^jkvB2)S^h~NLg^1+yYf==Mu3qsHo)V~txG|rV+P@lK2N5wJ)=f>1h#6iy z<#|>LREs*&0;(mlE0f{!s>rQ;P#O`*uOvKAm2H~}_jk`hrE~}|BS|sCby*QHA9$YU zsQ3v~3!Q-4l>sFSHQSiqlS7WB^FM<_LnNJkFb*ot#_xYEA|jF(lkmKM0Xq7HHc@&K QWdHyG07*qoM6N<$g4?&KI{*Lx diff --git a/assets/dolphin/external/L1_Sleigh_ride_128x64/frame_5.png b/assets/dolphin/external/L1_Sleigh_ride_128x64/frame_5.png deleted file mode 100755 index 70eab1f3e8c945560bd8aac429f4ae7e15401456..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 819 zcmV-31I+x1P) zUuYav6vlt|=B93H!FAKbeW-EU1Z_*GEfkSfldZI*f?}ae8v4g9A}AVcAQbvgb_ad1 z_+L=a2Qj|sjFwP*$U~(g*ir=9^q~+?$RMFRB|@f9lG)Ad93N&k+xu|ge4Ou`?|%0j zHPQd_kqn)qmV5r}K8jA-fW8>+@Sz(>OpOMw#lnx|~>+7u6}GfM*!KBRqQW$9C?c4A!&7q&C) zZR#>l`I#;9n;`}zfIja1F(kDxeFzxcYip8?Y`a8Eaw+*|eJ)F*YtkW27#2cQ3fD9xxO7i8G3U60x|3hHSFj?51%w@g*Fe6W3Pen(-lWthqH zIKkiFo>@#YdUcaWUK;ygeb245ZyzF3y)D)2H^w|bSsEA$o9Abri#(vmLPTOE(QB~) zSe5~l5WL-02egF&)bGsRKK>VQ(gpckWJY9Pc_ctPj$;TQ18@D9pQ6_VgMkYJ99z_qrZHz^_5-O!*)p0$SA@8>``u9&5i?P zA|=hi@vU}}3!lBz`j-#=wU1y@SuW7CZiv^m0KM@Q0KcH*jeYOjEG9ac0J!&rVWd>M zTm-QI%2S3C^+SMqw+Qi_2_+D0d{@?S)rZC17$6rG63sIoBP%l{B_LDHuT0c^g~i>i z$@@l4X0l=b;Z?7{h=@pS`7uw#(~h&(K}4h;O*1O@MIKm|5nrOJfLwFl+f(jocm#${{bV`LSb#O+VcPa002ovPDHLkV1lqyd=>xz diff --git a/assets/dolphin/external/L1_Sleigh_ride_128x64/frame_6.png b/assets/dolphin/external/L1_Sleigh_ride_128x64/frame_6.png deleted file mode 100755 index 8169e0766fd617850ef249e6919e2c7715e8cd9e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 799 zcmV+)1K|9LP)@2%14c(S3tq0sqQ7jEH5Dt=*W+!+tAtp8Hr$`!T0+9nA>H+_Nu>*%{ z!r3O^VVig%GbQn&7sQSz>BdX-(8LRNqlAUFbXQ8s>@Yj8hn=OZ-{pJr``+()pXX(q z|D6y3V*`wCc~&4mdNX^kU6}DLUYoRZlwRfq%GQCJzgfTnJ;GS40;s@jr2P7p4z2EO z`BkgUH8%sL>5Hx;%?&bmOPV#=@MWC5ermDatP_F);K8ay`L*yx0CU$U5dPW*AyM(x z^U`0%jVUtakvX+@=#1S?TU}kPT~eh=ycLt~QG(ax)Axz(Ni^|%eTCC)w+3qW$n?oI2)|Li?GX$SGl8*dSc?ud<`f8{IV6KN8%j`Qq+U;K{fgcF08uYQ&u>V>*Dtc2!ZX}?$gX!6H# zq9Cn==(qf}v5Vg(raVadCdiQojiEb=w{x+vg#B^nIU=LSh3(Hwpb#Cg{#fZvXs8rS zpx-R8%_ni*;!%$Q97uyP7Vy2+OWFi7VFxK|`I`@}h6^VQKwSwCd!Kq$AG)axK=pN@ z)ZD^x=k{?|2~f>$uM}>t@@GFFBGOowS((iZ7bT}nfaD|=o_p+uo70GhNU3w8F8_)= z)mjO_!)u>sms1C9KaF7lba$l=XVnG0*L9g&6OrikiTN+(Z)d=9R3ni9re$PLJ@C(h znY`?I@Kz&}CAt4AP%?e+AOz1kfhiA^Jk0C_dGjZ^Z2waRlkTK#qmH2p^tdhSB@q#k dszd{h^B;3hM?PLq!U_NY002ovPDHLkV1lI=dW!%6 diff --git a/assets/dolphin/external/L1_Sleigh_ride_128x64/frame_7.png b/assets/dolphin/external/L1_Sleigh_ride_128x64/frame_7.png deleted file mode 100755 index adee1a63dac8b87d57862f233107c1f961f39cac..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 817 zcmV-11J3-3P)RhGNuC&1^3lbT18NI;gl(OH8CIa|J4-6 z(9S=ILoqD~ih^##tZOy%4^>3nkhO7aYed^*?YbskzI=}tNn_`GbI$pl!}Gk)3x{3& zuL1y^|G>Z{*9HW%EN2fcZERyzc8Sz=jQclD6e|J6GL)JEDR8&*NEuRfkcY2BQz>8a z!ZoKBgJrhG;k3$Z5YIixT?A2j=N`iCBxBp4$A1Hidih4IN`Gk{$) z08G7~Xn=q)d8oQ~xAB^ZOz%*CwyJlUMANgg7yseme0?g$(l$VCe`M3^GyRY*IssWhRbTRzmwo2GAv1_?vkjcY8eA*Zql6w4+_~uC6_XZ@5yYZkJ zq@DvzFOud8@ndK8K^bWM5J^EQt`{{?0^IwMq*yeJrsaW?`NRnL@qv@8)}6B8;#(@# z+?ZG~S2|Z%>$ogflvd5d*|{gpnqgXv%Hs-hUg(dpH3L^!?6F`Vhv~a3>kdP=Q7sal z+%a0YJGrx*gI|q6zmGkh+uTcF(E%j+XHF=YZ_SK!XFi^>K()=g_RFq6cF{f@p10sT zky|(I&Ck1peII>hq4v@}eWf8H!?1Pm7bD8w_(u0hj%`@qWPBZb26TCpn%6&SC_-{V4gwZyxYn-U8Fd&v>xl0)?^# zuHM(>!OCF>?!W@(WZ7Za!0EEuiE^`mY8|g zU7rO>QutJQ;uW3FbW3%h_uie0s&d|)QeA^W<+}BGZFnta8^B+T0{cf3+mI$Cg?CEQ v>5;|Tk>(}o1xc!GnLUCOXsN>Z%vb*cYgu*d%gmGW00000NkvXXu0mjfmz0(k diff --git a/assets/dolphin/external/L1_Sleigh_ride_128x64/frame_8.png b/assets/dolphin/external/L1_Sleigh_ride_128x64/frame_8.png deleted file mode 100755 index db6e667fb77d119f4150c6a1ec875be023846c19..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 875 zcmV-x1C;!UP)~KgG zR}iEKYkd&s5>XHzgbo{2);@?U76v}7jm|c0SDM(WxlQlAUmue0ynNw%zURyLoI^kL zGTAy>AJ&1*viblBH+3K&3PcJG#WBDGup1&ZI*~%d$8{V4nu?GFD0|!@zybGp`T!?U z0Vu+FXuJxL@-`K&x$fN}fO2^v@PK`OBbv*7;lLJ~Gm(Khvu{j6SulUGk@WbdCub`` z#Z=lc0L-r_Os)Cmk-CR%xAg-4&)Qm_@CkdyHb!T9$TTis^htr($(@hgP-8ijR#wVq z%mpcm4H(sWsqG$YTgG7ffWpkif6KfeWw8MZbNlgLKhph95n^DFO6A(*_O}BS*bbHr zEidX={A}`M!Uli0op_nh zy7S_t!gp1q>P8?#z*R_CPxQ`+cX9UxkHv&I6 zXB;Pcc;Wu^DaQzc_;96iITfyx{hJFr)~AcC`;s&|vYODkUY~ObGd+?@ML(r5^Yv=ZHt8XtwHhg6(hr?2qSD`z z!Y34FRMNLOi`H#fFb6PcT4~E8X;k$v3Nx#@{{d={ku%1$CRzXh002ovPDHLkV1k;- BrV{`F diff --git a/assets/dolphin/external/L1_Sleigh_ride_128x64/frame_9.png b/assets/dolphin/external/L1_Sleigh_ride_128x64/frame_9.png deleted file mode 100755 index a6a37fe29c7f17cec2d4824961e1acfe37f0d353..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 823 zcmV-71IYY|P)de9(=SZW1EY1mdVOhDM_c6MG5vwyzZ`^%H}ee=A}$LQ95 zF=jyHg=34CF#l@Q#%R2kI}nFKWUgz1neW&VR+pM! zBwEhe<@Uy2y7&2nVtj@elmP1LJezOG3#aM=kNA}QW9}aFhB*PvWR_uB5A7lEkYhOWwzPxbk_-U#y zg(S~e8g@uwO?$n#9ByJQ zGvG#6dKXzeCxaQ8l3B+|Bi?fK&dC{e`mUQ!fzM=bPgsdwY<$iqS6e%2d0>LNqEgNQ z9~D6fzibBC`8{xD4m8{csK)>{_^E|vBliO?{{st?Q~gh3V Date: Mon, 31 Mar 2025 15:11:05 +0000 Subject: [PATCH 167/268] GUI: Fix widget text scroll with 256+ lines (#4160) Co-authored-by: hedger --- .../gui/modules/widget_elements/widget_element_text_scroll.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/applications/services/gui/modules/widget_elements/widget_element_text_scroll.c b/applications/services/gui/modules/widget_elements/widget_element_text_scroll.c index 4c9c39dff..491ffc6bc 100644 --- a/applications/services/gui/modules/widget_elements/widget_element_text_scroll.c +++ b/applications/services/gui/modules/widget_elements/widget_element_text_scroll.c @@ -19,8 +19,8 @@ typedef struct { uint8_t width; uint8_t height; FuriString* text; - uint8_t scroll_pos_total; - uint8_t scroll_pos_current; + uint16_t scroll_pos_total; + uint16_t scroll_pos_current; bool text_formatted; } WidgetElementTextScrollModel; From 5fcaef25b014d99c40e7fc0687c3c5525dca9c1b Mon Sep 17 00:00:00 2001 From: hedger Date: Mon, 31 Mar 2025 16:23:32 +0100 Subject: [PATCH 168/268] Stricter constness for const data (#4126) * libs: stricter constness for saving RAM with .rodata section; fbt: sdk: fixed signature generation for nested const params * hal: additional fixes for constness in USB subsystem * debug apps: additional usb-related fixes * mjs: more consts for token parser * fatfs: const driver struct * hal: more consts for ble & nfc vars * hal: made FuriHalSpiBusHandle static * hal: made FuriHalI2cBusHandle static * usb: restored previous api * linter fixes * API fixes --- .../drivers/subghz/cc1101_ext/cc1101_ext.c | 2 +- applications/services/cli/cli.c | 2 +- applications/services/cli/cli.h | 2 +- applications/services/cli/cli_i.h | 2 +- applications/services/cli/cli_vcp.c | 2 +- applications/services/cli/cli_vcp.h | 2 +- lib/drivers/bq25896.c | 38 ++--- lib/drivers/bq25896.h | 38 ++--- lib/drivers/bq27220.c | 52 +++---- lib/drivers/bq27220.h | 40 +++--- lib/drivers/cc1101.c | 48 ++++--- lib/drivers/cc1101.h | 45 +++--- lib/drivers/lp5562.c | 31 +++-- lib/drivers/lp5562.h | 31 +++-- lib/drivers/st25r3916.c | 8 +- lib/drivers/st25r3916.h | 8 +- lib/drivers/st25r3916_reg.c | 49 ++++--- lib/drivers/st25r3916_reg.h | 47 ++++--- .../dallas/protocol_group_dallas_defs.c | 2 +- .../dallas/protocol_group_dallas_defs.h | 2 +- .../protocols/misc/protocol_group_misc_defs.c | 2 +- .../protocols/misc/protocol_group_misc_defs.h | 2 +- lib/ibutton/protocols/protocol_group_defs.c | 2 +- lib/ibutton/protocols/protocol_group_defs.h | 2 +- lib/lfrfid/protocols/lfrfid_protocols.c | 2 +- lib/lfrfid/protocols/lfrfid_protocols.h | 2 +- lib/lfrfid/protocols/protocol_nexwatch.c | 2 +- lib/mjs/mjs_parser.c | 12 +- .../mf_classic/mf_classic_listener.c | 18 +-- lib/nfc/protocols/nfc_device_defs.c | 2 +- lib/nfc/protocols/nfc_device_defs.h | 2 +- lib/nfc/protocols/nfc_listener_defs.c | 2 +- lib/nfc/protocols/nfc_listener_defs.h | 2 +- lib/nfc/protocols/nfc_poller_defs.c | 2 +- lib/nfc/protocols/nfc_poller_defs.h | 2 +- lib/subghz/protocols/protocol_items.c | 2 +- lib/subghz/registry.h | 2 +- lib/toolbox/protocols/protocol_dict.c | 4 +- lib/toolbox/protocols/protocol_dict.h | 2 +- scripts/fbt/sdk/collector.py | 2 +- targets/f18/api_symbols.csv | 120 ++++++++-------- targets/f18/furi_hal/furi_hal_spi_config.c | 22 +-- targets/f18/furi_hal/furi_hal_spi_config.h | 8 +- targets/f7/api_symbols.csv | 130 +++++++++--------- targets/f7/ble_glue/profiles/serial_profile.c | 4 +- targets/f7/ble_glue/profiles/serial_profile.h | 2 +- targets/f7/fatfs/user_diskio.c | 2 +- targets/f7/fatfs/user_diskio.h | 2 +- targets/f7/furi_hal/furi_hal_i2c.c | 31 +++-- targets/f7/furi_hal/furi_hal_i2c_config.c | 8 +- targets/f7/furi_hal/furi_hal_i2c_config.h | 4 +- targets/f7/furi_hal/furi_hal_i2c_types.h | 4 +- targets/f7/furi_hal/furi_hal_nfc.c | 50 +++---- targets/f7/furi_hal/furi_hal_nfc_event.c | 4 +- targets/f7/furi_hal/furi_hal_nfc_felica.c | 16 +-- targets/f7/furi_hal/furi_hal_nfc_i.h | 12 +- targets/f7/furi_hal/furi_hal_nfc_irq.c | 2 +- targets/f7/furi_hal/furi_hal_nfc_iso14443a.c | 26 ++-- targets/f7/furi_hal/furi_hal_nfc_iso14443b.c | 6 +- targets/f7/furi_hal/furi_hal_nfc_iso15693.c | 26 ++-- targets/f7/furi_hal/furi_hal_nfc_tech_i.h | 12 +- targets/f7/furi_hal/furi_hal_sd.c | 2 +- targets/f7/furi_hal/furi_hal_spi.c | 18 +-- targets/f7/furi_hal/furi_hal_spi_config.c | 32 ++--- targets/f7/furi_hal/furi_hal_spi_config.h | 12 +- targets/f7/furi_hal/furi_hal_spi_types.h | 4 +- targets/furi_hal_include/furi_hal_i2c.h | 31 +++-- targets/furi_hal_include/furi_hal_spi.h | 16 +-- 68 files changed, 594 insertions(+), 531 deletions(-) diff --git a/applications/drivers/subghz/cc1101_ext/cc1101_ext.c b/applications/drivers/subghz/cc1101_ext/cc1101_ext.c index ae3556396..357214505 100644 --- a/applications/drivers/subghz/cc1101_ext/cc1101_ext.c +++ b/applications/drivers/subghz/cc1101_ext/cc1101_ext.c @@ -85,7 +85,7 @@ typedef struct { volatile SubGhzDeviceCC1101ExtState state; volatile SubGhzDeviceCC1101ExtRegulation regulation; const GpioPin* async_mirror_pin; - FuriHalSpiBusHandle* spi_bus_handle; + const FuriHalSpiBusHandle* spi_bus_handle; const GpioPin* g0_pin; SubGhzDeviceCC1101ExtAsyncTx async_tx; SubGhzDeviceCC1101ExtAsyncRx async_rx; diff --git a/applications/services/cli/cli.c b/applications/services/cli/cli.c index 28ba417c2..c2a0b9cb1 100644 --- a/applications/services/cli/cli.c +++ b/applications/services/cli/cli.c @@ -424,7 +424,7 @@ void cli_delete_command(Cli* cli, const char* name) { furi_string_free(name_str); } -void cli_session_open(Cli* cli, void* session) { +void cli_session_open(Cli* cli, const void* session) { furi_check(cli); furi_check(furi_mutex_acquire(cli->mutex, FuriWaitForever) == FuriStatusOk); diff --git a/applications/services/cli/cli.h b/applications/services/cli/cli.h index bb84670a7..fe8f09032 100644 --- a/applications/services/cli/cli.h +++ b/applications/services/cli/cli.h @@ -123,7 +123,7 @@ char cli_getc(Cli* cli); */ void cli_nl(Cli* cli); -void cli_session_open(Cli* cli, void* session); +void cli_session_open(Cli* cli, const void* session); void cli_session_close(Cli* cli); diff --git a/applications/services/cli/cli_i.h b/applications/services/cli/cli_i.h index d7351b9ff..ca126dacd 100644 --- a/applications/services/cli/cli_i.h +++ b/applications/services/cli/cli_i.h @@ -50,7 +50,7 @@ struct Cli { FuriSemaphore* idle_sem; FuriString* last_line; FuriString* line; - CliSession* session; + const CliSession* session; size_t cursor_position; }; diff --git a/applications/services/cli/cli_vcp.c b/applications/services/cli/cli_vcp.c index aa399e78a..39802bd79 100644 --- a/applications/services/cli/cli_vcp.c +++ b/applications/services/cli/cli_vcp.c @@ -312,7 +312,7 @@ static bool cli_vcp_is_connected(void) { return vcp->connected; } -CliSession cli_vcp = { +const CliSession cli_vcp = { cli_vcp_init, cli_vcp_deinit, cli_vcp_rx, diff --git a/applications/services/cli/cli_vcp.h b/applications/services/cli/cli_vcp.h index 3aef2ef70..f51625342 100644 --- a/applications/services/cli/cli_vcp.h +++ b/applications/services/cli/cli_vcp.h @@ -11,7 +11,7 @@ extern "C" { typedef struct CliSession CliSession; -extern CliSession cli_vcp; +extern const CliSession cli_vcp; #ifdef __cplusplus } diff --git a/lib/drivers/bq25896.c b/lib/drivers/bq25896.c index 76aae5e82..a44ff8c39 100644 --- a/lib/drivers/bq25896.c +++ b/lib/drivers/bq25896.c @@ -35,7 +35,7 @@ typedef struct { static bq25896_regs_t bq25896_regs; -bool bq25896_init(FuriHalI2cBusHandle* handle) { +bool bq25896_init(const FuriHalI2cBusHandle* handle) { bool result = true; bq25896_regs.r14.REG_RST = 1; @@ -78,19 +78,19 @@ bool bq25896_init(FuriHalI2cBusHandle* handle) { return result; } -void bq25896_set_boost_lim(FuriHalI2cBusHandle* handle, BoostLim boost_lim) { +void bq25896_set_boost_lim(const FuriHalI2cBusHandle* handle, BoostLim boost_lim) { bq25896_regs.r0A.BOOST_LIM = boost_lim; furi_hal_i2c_write_reg_8( handle, BQ25896_ADDRESS, 0x0A, *(uint8_t*)&bq25896_regs.r0A, BQ25896_I2C_TIMEOUT); } -void bq25896_poweroff(FuriHalI2cBusHandle* handle) { +void bq25896_poweroff(const FuriHalI2cBusHandle* handle) { bq25896_regs.r09.BATFET_DIS = 1; furi_hal_i2c_write_reg_8( handle, BQ25896_ADDRESS, 0x09, *(uint8_t*)&bq25896_regs.r09, BQ25896_I2C_TIMEOUT); } -ChrgStat bq25896_get_charge_status(FuriHalI2cBusHandle* handle) { +ChrgStat bq25896_get_charge_status(const FuriHalI2cBusHandle* handle) { furi_hal_i2c_read_mem( handle, BQ25896_ADDRESS, @@ -103,52 +103,52 @@ ChrgStat bq25896_get_charge_status(FuriHalI2cBusHandle* handle) { return bq25896_regs.r0B.CHRG_STAT; } -bool bq25896_is_charging(FuriHalI2cBusHandle* handle) { +bool bq25896_is_charging(const FuriHalI2cBusHandle* handle) { // Include precharge, fast charging, and charging termination done as "charging" return bq25896_get_charge_status(handle) != ChrgStatNo; } -bool bq25896_is_charging_done(FuriHalI2cBusHandle* handle) { +bool bq25896_is_charging_done(const FuriHalI2cBusHandle* handle) { return bq25896_get_charge_status(handle) == ChrgStatDone; } -void bq25896_enable_charging(FuriHalI2cBusHandle* handle) { +void bq25896_enable_charging(const FuriHalI2cBusHandle* handle) { bq25896_regs.r03.CHG_CONFIG = 1; furi_hal_i2c_write_reg_8( handle, BQ25896_ADDRESS, 0x03, *(uint8_t*)&bq25896_regs.r03, BQ25896_I2C_TIMEOUT); } -void bq25896_disable_charging(FuriHalI2cBusHandle* handle) { +void bq25896_disable_charging(const FuriHalI2cBusHandle* handle) { bq25896_regs.r03.CHG_CONFIG = 0; furi_hal_i2c_write_reg_8( handle, BQ25896_ADDRESS, 0x03, *(uint8_t*)&bq25896_regs.r03, BQ25896_I2C_TIMEOUT); } -void bq25896_enable_otg(FuriHalI2cBusHandle* handle) { +void bq25896_enable_otg(const FuriHalI2cBusHandle* handle) { bq25896_regs.r03.OTG_CONFIG = 1; furi_hal_i2c_write_reg_8( handle, BQ25896_ADDRESS, 0x03, *(uint8_t*)&bq25896_regs.r03, BQ25896_I2C_TIMEOUT); } -void bq25896_disable_otg(FuriHalI2cBusHandle* handle) { +void bq25896_disable_otg(const FuriHalI2cBusHandle* handle) { bq25896_regs.r03.OTG_CONFIG = 0; furi_hal_i2c_write_reg_8( handle, BQ25896_ADDRESS, 0x03, *(uint8_t*)&bq25896_regs.r03, BQ25896_I2C_TIMEOUT); } -bool bq25896_is_otg_enabled(FuriHalI2cBusHandle* handle) { +bool bq25896_is_otg_enabled(const FuriHalI2cBusHandle* handle) { furi_hal_i2c_read_reg_8( handle, BQ25896_ADDRESS, 0x03, (uint8_t*)&bq25896_regs.r03, BQ25896_I2C_TIMEOUT); return bq25896_regs.r03.OTG_CONFIG; } -uint16_t bq25896_get_vreg_voltage(FuriHalI2cBusHandle* handle) { +uint16_t bq25896_get_vreg_voltage(const FuriHalI2cBusHandle* handle) { furi_hal_i2c_read_reg_8( handle, BQ25896_ADDRESS, 0x06, (uint8_t*)&bq25896_regs.r06, BQ25896_I2C_TIMEOUT); return (uint16_t)bq25896_regs.r06.VREG * 16 + 3840; } -void bq25896_set_vreg_voltage(FuriHalI2cBusHandle* handle, uint16_t vreg_voltage) { +void bq25896_set_vreg_voltage(const FuriHalI2cBusHandle* handle, uint16_t vreg_voltage) { if(vreg_voltage < 3840) { // Minimum valid value is 3840 mV vreg_voltage = 3840; @@ -166,13 +166,13 @@ void bq25896_set_vreg_voltage(FuriHalI2cBusHandle* handle, uint16_t vreg_voltage handle, BQ25896_ADDRESS, 0x06, *(uint8_t*)&bq25896_regs.r06, BQ25896_I2C_TIMEOUT); } -bool bq25896_check_otg_fault(FuriHalI2cBusHandle* handle) { +bool bq25896_check_otg_fault(const FuriHalI2cBusHandle* handle) { furi_hal_i2c_read_reg_8( handle, BQ25896_ADDRESS, 0x0C, (uint8_t*)&bq25896_regs.r0C, BQ25896_I2C_TIMEOUT); return bq25896_regs.r0C.BOOST_FAULT; } -uint16_t bq25896_get_vbus_voltage(FuriHalI2cBusHandle* handle) { +uint16_t bq25896_get_vbus_voltage(const FuriHalI2cBusHandle* handle) { furi_hal_i2c_read_reg_8( handle, BQ25896_ADDRESS, 0x11, (uint8_t*)&bq25896_regs.r11, BQ25896_I2C_TIMEOUT); if(bq25896_regs.r11.VBUS_GD) { @@ -182,25 +182,25 @@ uint16_t bq25896_get_vbus_voltage(FuriHalI2cBusHandle* handle) { } } -uint16_t bq25896_get_vsys_voltage(FuriHalI2cBusHandle* handle) { +uint16_t bq25896_get_vsys_voltage(const FuriHalI2cBusHandle* handle) { furi_hal_i2c_read_reg_8( handle, BQ25896_ADDRESS, 0x0F, (uint8_t*)&bq25896_regs.r0F, BQ25896_I2C_TIMEOUT); return (uint16_t)bq25896_regs.r0F.SYSV * 20 + 2304; } -uint16_t bq25896_get_vbat_voltage(FuriHalI2cBusHandle* handle) { +uint16_t bq25896_get_vbat_voltage(const FuriHalI2cBusHandle* handle) { furi_hal_i2c_read_reg_8( handle, BQ25896_ADDRESS, 0x0E, (uint8_t*)&bq25896_regs.r0E, BQ25896_I2C_TIMEOUT); return (uint16_t)bq25896_regs.r0E.BATV * 20 + 2304; } -uint16_t bq25896_get_vbat_current(FuriHalI2cBusHandle* handle) { +uint16_t bq25896_get_vbat_current(const FuriHalI2cBusHandle* handle) { furi_hal_i2c_read_reg_8( handle, BQ25896_ADDRESS, 0x12, (uint8_t*)&bq25896_regs.r12, BQ25896_I2C_TIMEOUT); return (uint16_t)bq25896_regs.r12.ICHGR * 50; } -uint32_t bq25896_get_ntc_mpct(FuriHalI2cBusHandle* handle) { +uint32_t bq25896_get_ntc_mpct(const FuriHalI2cBusHandle* handle) { furi_hal_i2c_read_reg_8( handle, BQ25896_ADDRESS, 0x10, (uint8_t*)&bq25896_regs.r10, BQ25896_I2C_TIMEOUT); return (uint32_t)bq25896_regs.r10.TSPCT * 465 + 21000; diff --git a/lib/drivers/bq25896.h b/lib/drivers/bq25896.h index d35625ab3..69c19868c 100644 --- a/lib/drivers/bq25896.h +++ b/lib/drivers/bq25896.h @@ -7,61 +7,61 @@ #include /** Initialize Driver */ -bool bq25896_init(FuriHalI2cBusHandle* handle); +bool bq25896_init(const FuriHalI2cBusHandle* handle); /** Set boost lim*/ -void bq25896_set_boost_lim(FuriHalI2cBusHandle* handle, BoostLim boost_lim); +void bq25896_set_boost_lim(const FuriHalI2cBusHandle* handle, BoostLim boost_lim); /** Send device into shipping mode */ -void bq25896_poweroff(FuriHalI2cBusHandle* handle); +void bq25896_poweroff(const FuriHalI2cBusHandle* handle); /** Get charging status */ -ChrgStat bq25896_get_charge_status(FuriHalI2cBusHandle* handle); +ChrgStat bq25896_get_charge_status(const FuriHalI2cBusHandle* handle); /** Is currently charging */ -bool bq25896_is_charging(FuriHalI2cBusHandle* handle); +bool bq25896_is_charging(const FuriHalI2cBusHandle* handle); /** Is charging completed while connected to charger */ -bool bq25896_is_charging_done(FuriHalI2cBusHandle* handle); +bool bq25896_is_charging_done(const FuriHalI2cBusHandle* handle); /** Enable charging */ -void bq25896_enable_charging(FuriHalI2cBusHandle* handle); +void bq25896_enable_charging(const FuriHalI2cBusHandle* handle); /** Disable charging */ -void bq25896_disable_charging(FuriHalI2cBusHandle* handle); +void bq25896_disable_charging(const FuriHalI2cBusHandle* handle); /** Enable otg */ -void bq25896_enable_otg(FuriHalI2cBusHandle* handle); +void bq25896_enable_otg(const FuriHalI2cBusHandle* handle); /** Disable otg */ -void bq25896_disable_otg(FuriHalI2cBusHandle* handle); +void bq25896_disable_otg(const FuriHalI2cBusHandle* handle); /** Is otg enabled */ -bool bq25896_is_otg_enabled(FuriHalI2cBusHandle* handle); +bool bq25896_is_otg_enabled(const FuriHalI2cBusHandle* handle); /** Get VREG (charging limit) voltage in mV */ -uint16_t bq25896_get_vreg_voltage(FuriHalI2cBusHandle* handle); +uint16_t bq25896_get_vreg_voltage(const FuriHalI2cBusHandle* handle); /** Set VREG (charging limit) voltage in mV * * Valid range: 3840mV - 4208mV, in steps of 16mV */ -void bq25896_set_vreg_voltage(FuriHalI2cBusHandle* handle, uint16_t vreg_voltage); +void bq25896_set_vreg_voltage(const FuriHalI2cBusHandle* handle, uint16_t vreg_voltage); /** Check OTG BOOST Fault status */ -bool bq25896_check_otg_fault(FuriHalI2cBusHandle* handle); +bool bq25896_check_otg_fault(const FuriHalI2cBusHandle* handle); /** Get VBUS Voltage in mV */ -uint16_t bq25896_get_vbus_voltage(FuriHalI2cBusHandle* handle); +uint16_t bq25896_get_vbus_voltage(const FuriHalI2cBusHandle* handle); /** Get VSYS Voltage in mV */ -uint16_t bq25896_get_vsys_voltage(FuriHalI2cBusHandle* handle); +uint16_t bq25896_get_vsys_voltage(const FuriHalI2cBusHandle* handle); /** Get VBAT Voltage in mV */ -uint16_t bq25896_get_vbat_voltage(FuriHalI2cBusHandle* handle); +uint16_t bq25896_get_vbat_voltage(const FuriHalI2cBusHandle* handle); /** Get VBAT current in mA */ -uint16_t bq25896_get_vbat_current(FuriHalI2cBusHandle* handle); +uint16_t bq25896_get_vbat_current(const FuriHalI2cBusHandle* handle); /** Get NTC voltage in mpct of REGN */ -uint32_t bq25896_get_ntc_mpct(FuriHalI2cBusHandle* handle); +uint32_t bq25896_get_ntc_mpct(const FuriHalI2cBusHandle* handle); diff --git a/lib/drivers/bq27220.c b/lib/drivers/bq27220.c index d60e287da..127f7c6b9 100644 --- a/lib/drivers/bq27220.c +++ b/lib/drivers/bq27220.c @@ -43,7 +43,7 @@ #endif static inline bool bq27220_read_reg( - FuriHalI2cBusHandle* handle, + const FuriHalI2cBusHandle* handle, uint8_t address, uint8_t* buffer, size_t buffer_size) { @@ -52,7 +52,7 @@ static inline bool bq27220_read_reg( } static inline bool bq27220_write( - FuriHalI2cBusHandle* handle, + const FuriHalI2cBusHandle* handle, uint8_t address, const uint8_t* buffer, size_t buffer_size) { @@ -60,11 +60,11 @@ static inline bool bq27220_write( handle, BQ27220_ADDRESS, address, buffer, buffer_size, BQ27220_I2C_TIMEOUT); } -static inline bool bq27220_control(FuriHalI2cBusHandle* handle, uint16_t control) { +static inline bool bq27220_control(const FuriHalI2cBusHandle* handle, uint16_t control) { return bq27220_write(handle, CommandControl, (uint8_t*)&control, 2); } -static uint16_t bq27220_read_word(FuriHalI2cBusHandle* handle, uint8_t address) { +static uint16_t bq27220_read_word(const FuriHalI2cBusHandle* handle, uint8_t address) { uint16_t buf = BQ27220_ERROR; if(!bq27220_read_reg(handle, address, (uint8_t*)&buf, 2)) { @@ -83,7 +83,7 @@ static uint8_t bq27220_get_checksum(uint8_t* data, uint16_t len) { } static bool bq27220_parameter_check( - FuriHalI2cBusHandle* handle, + const FuriHalI2cBusHandle* handle, uint16_t address, uint32_t value, size_t size, @@ -163,7 +163,7 @@ static bool bq27220_parameter_check( } static bool bq27220_data_memory_check( - FuriHalI2cBusHandle* handle, + const FuriHalI2cBusHandle* handle, const BQ27220DMData* data_memory, bool update) { if(update) { @@ -268,7 +268,7 @@ static bool bq27220_data_memory_check( return result; } -bool bq27220_init(FuriHalI2cBusHandle* handle, const BQ27220DMData* data_memory) { +bool bq27220_init(const FuriHalI2cBusHandle* handle, const BQ27220DMData* data_memory) { bool result = false; bool reset_and_provisioning_required = false; @@ -365,7 +365,7 @@ bool bq27220_init(FuriHalI2cBusHandle* handle, const BQ27220DMData* data_memory) return result; } -bool bq27220_reset(FuriHalI2cBusHandle* handle) { +bool bq27220_reset(const FuriHalI2cBusHandle* handle) { bool result = false; do { if(!bq27220_control(handle, Control_RESET)) { @@ -396,7 +396,7 @@ bool bq27220_reset(FuriHalI2cBusHandle* handle) { return result; } -bool bq27220_seal(FuriHalI2cBusHandle* handle) { +bool bq27220_seal(const FuriHalI2cBusHandle* handle) { Bq27220OperationStatus operation_status = {0}; bool result = false; do { @@ -431,7 +431,7 @@ bool bq27220_seal(FuriHalI2cBusHandle* handle) { return result; } -bool bq27220_unseal(FuriHalI2cBusHandle* handle) { +bool bq27220_unseal(const FuriHalI2cBusHandle* handle) { Bq27220OperationStatus operation_status = {0}; bool result = false; do { @@ -465,7 +465,7 @@ bool bq27220_unseal(FuriHalI2cBusHandle* handle) { return result; } -bool bq27220_full_access(FuriHalI2cBusHandle* handle) { +bool bq27220_full_access(const FuriHalI2cBusHandle* handle) { bool result = false; do { @@ -518,29 +518,35 @@ bool bq27220_full_access(FuriHalI2cBusHandle* handle) { return result; } -uint16_t bq27220_get_voltage(FuriHalI2cBusHandle* handle) { +uint16_t bq27220_get_voltage(const FuriHalI2cBusHandle* handle) { return bq27220_read_word(handle, CommandVoltage); } -int16_t bq27220_get_current(FuriHalI2cBusHandle* handle) { +int16_t bq27220_get_current(const FuriHalI2cBusHandle* handle) { return bq27220_read_word(handle, CommandCurrent); } -bool bq27220_get_control_status(FuriHalI2cBusHandle* handle, Bq27220ControlStatus* control_status) { +bool bq27220_get_control_status( + const FuriHalI2cBusHandle* handle, + Bq27220ControlStatus* control_status) { return bq27220_read_reg(handle, CommandControl, (uint8_t*)control_status, 2); } -bool bq27220_get_battery_status(FuriHalI2cBusHandle* handle, Bq27220BatteryStatus* battery_status) { +bool bq27220_get_battery_status( + const FuriHalI2cBusHandle* handle, + Bq27220BatteryStatus* battery_status) { return bq27220_read_reg(handle, CommandBatteryStatus, (uint8_t*)battery_status, 2); } bool bq27220_get_operation_status( - FuriHalI2cBusHandle* handle, + const FuriHalI2cBusHandle* handle, Bq27220OperationStatus* operation_status) { return bq27220_read_reg(handle, CommandOperationStatus, (uint8_t*)operation_status, 2); } -bool bq27220_get_gauging_status(FuriHalI2cBusHandle* handle, Bq27220GaugingStatus* gauging_status) { +bool bq27220_get_gauging_status( + const FuriHalI2cBusHandle* handle, + Bq27220GaugingStatus* gauging_status) { // Request gauging data to be loaded to MAC if(!bq27220_control(handle, Control_GAUGING_STATUS)) { FURI_LOG_E(TAG, "DM SelectSubclass for read failed"); @@ -552,26 +558,26 @@ bool bq27220_get_gauging_status(FuriHalI2cBusHandle* handle, Bq27220GaugingStatu return bq27220_read_reg(handle, CommandMACData, (uint8_t*)gauging_status, 2); } -uint16_t bq27220_get_temperature(FuriHalI2cBusHandle* handle) { +uint16_t bq27220_get_temperature(const FuriHalI2cBusHandle* handle) { return bq27220_read_word(handle, CommandTemperature); } -uint16_t bq27220_get_full_charge_capacity(FuriHalI2cBusHandle* handle) { +uint16_t bq27220_get_full_charge_capacity(const FuriHalI2cBusHandle* handle) { return bq27220_read_word(handle, CommandFullChargeCapacity); } -uint16_t bq27220_get_design_capacity(FuriHalI2cBusHandle* handle) { +uint16_t bq27220_get_design_capacity(const FuriHalI2cBusHandle* handle) { return bq27220_read_word(handle, CommandDesignCapacity); } -uint16_t bq27220_get_remaining_capacity(FuriHalI2cBusHandle* handle) { +uint16_t bq27220_get_remaining_capacity(const FuriHalI2cBusHandle* handle) { return bq27220_read_word(handle, CommandRemainingCapacity); } -uint16_t bq27220_get_state_of_charge(FuriHalI2cBusHandle* handle) { +uint16_t bq27220_get_state_of_charge(const FuriHalI2cBusHandle* handle) { return bq27220_read_word(handle, CommandStateOfCharge); } -uint16_t bq27220_get_state_of_health(FuriHalI2cBusHandle* handle) { +uint16_t bq27220_get_state_of_health(const FuriHalI2cBusHandle* handle) { return bq27220_read_word(handle, CommandStateOfHealth); } diff --git a/lib/drivers/bq27220.h b/lib/drivers/bq27220.h index cdfcb20b1..addbf08f5 100644 --- a/lib/drivers/bq27220.h +++ b/lib/drivers/bq27220.h @@ -136,7 +136,7 @@ typedef struct BQ27220DMData BQ27220DMData; * * @return true on success, false otherwise */ -bool bq27220_init(FuriHalI2cBusHandle* handle, const BQ27220DMData* data_memory); +bool bq27220_init(const FuriHalI2cBusHandle* handle, const BQ27220DMData* data_memory); /** Reset gauge * @@ -144,7 +144,7 @@ bool bq27220_init(FuriHalI2cBusHandle* handle, const BQ27220DMData* data_memory) * * @return true on success, false otherwise */ -bool bq27220_reset(FuriHalI2cBusHandle* handle); +bool bq27220_reset(const FuriHalI2cBusHandle* handle); /** Seal gauge access * @@ -152,7 +152,7 @@ bool bq27220_reset(FuriHalI2cBusHandle* handle); * * @return true on success, false otherwise */ -bool bq27220_seal(FuriHalI2cBusHandle* handle); +bool bq27220_seal(const FuriHalI2cBusHandle* handle); /** Unseal gauge access * @@ -160,7 +160,7 @@ bool bq27220_seal(FuriHalI2cBusHandle* handle); * * @return true on success, false otherwise */ -bool bq27220_unseal(FuriHalI2cBusHandle* handle); +bool bq27220_unseal(const FuriHalI2cBusHandle* handle); /** Get full access * @@ -170,7 +170,7 @@ bool bq27220_unseal(FuriHalI2cBusHandle* handle); * * @return true on success, false otherwise */ -bool bq27220_full_access(FuriHalI2cBusHandle* handle); +bool bq27220_full_access(const FuriHalI2cBusHandle* handle); /** Get battery voltage * @@ -178,7 +178,7 @@ bool bq27220_full_access(FuriHalI2cBusHandle* handle); * * @return voltage in mV or BQ27220_ERROR */ -uint16_t bq27220_get_voltage(FuriHalI2cBusHandle* handle); +uint16_t bq27220_get_voltage(const FuriHalI2cBusHandle* handle); /** Get current * @@ -186,7 +186,7 @@ uint16_t bq27220_get_voltage(FuriHalI2cBusHandle* handle); * * @return current in mA or BQ27220_ERROR */ -int16_t bq27220_get_current(FuriHalI2cBusHandle* handle); +int16_t bq27220_get_current(const FuriHalI2cBusHandle* handle); /** Get control status * @@ -195,7 +195,9 @@ int16_t bq27220_get_current(FuriHalI2cBusHandle* handle); * * @return true on success, false otherwise */ -bool bq27220_get_control_status(FuriHalI2cBusHandle* handle, Bq27220ControlStatus* control_status); +bool bq27220_get_control_status( + const FuriHalI2cBusHandle* handle, + Bq27220ControlStatus* control_status); /** Get battery status * @@ -204,7 +206,9 @@ bool bq27220_get_control_status(FuriHalI2cBusHandle* handle, Bq27220ControlStatu * * @return true on success, false otherwise */ -bool bq27220_get_battery_status(FuriHalI2cBusHandle* handle, Bq27220BatteryStatus* battery_status); +bool bq27220_get_battery_status( + const FuriHalI2cBusHandle* handle, + Bq27220BatteryStatus* battery_status); /** Get operation status * @@ -214,7 +218,7 @@ bool bq27220_get_battery_status(FuriHalI2cBusHandle* handle, Bq27220BatteryStatu * @return true on success, false otherwise */ bool bq27220_get_operation_status( - FuriHalI2cBusHandle* handle, + const FuriHalI2cBusHandle* handle, Bq27220OperationStatus* operation_status); /** Get gauging status @@ -224,7 +228,9 @@ bool bq27220_get_operation_status( * * @return true on success, false otherwise */ -bool bq27220_get_gauging_status(FuriHalI2cBusHandle* handle, Bq27220GaugingStatus* gauging_status); +bool bq27220_get_gauging_status( + const FuriHalI2cBusHandle* handle, + Bq27220GaugingStatus* gauging_status); /** Get temperature * @@ -232,7 +238,7 @@ bool bq27220_get_gauging_status(FuriHalI2cBusHandle* handle, Bq27220GaugingStatu * * @return temperature in units of 0.1°K */ -uint16_t bq27220_get_temperature(FuriHalI2cBusHandle* handle); +uint16_t bq27220_get_temperature(const FuriHalI2cBusHandle* handle); /** Get compensated full charge capacity * @@ -240,7 +246,7 @@ uint16_t bq27220_get_temperature(FuriHalI2cBusHandle* handle); * * @return full charge capacity in mAh or BQ27220_ERROR */ -uint16_t bq27220_get_full_charge_capacity(FuriHalI2cBusHandle* handle); +uint16_t bq27220_get_full_charge_capacity(const FuriHalI2cBusHandle* handle); /** Get design capacity * @@ -248,7 +254,7 @@ uint16_t bq27220_get_full_charge_capacity(FuriHalI2cBusHandle* handle); * * @return design capacity in mAh or BQ27220_ERROR */ -uint16_t bq27220_get_design_capacity(FuriHalI2cBusHandle* handle); +uint16_t bq27220_get_design_capacity(const FuriHalI2cBusHandle* handle); /** Get remaining capacity * @@ -256,7 +262,7 @@ uint16_t bq27220_get_design_capacity(FuriHalI2cBusHandle* handle); * * @return remaining capacity in mAh or BQ27220_ERROR */ -uint16_t bq27220_get_remaining_capacity(FuriHalI2cBusHandle* handle); +uint16_t bq27220_get_remaining_capacity(const FuriHalI2cBusHandle* handle); /** Get predicted remaining battery capacity * @@ -264,7 +270,7 @@ uint16_t bq27220_get_remaining_capacity(FuriHalI2cBusHandle* handle); * * @return state of charge in percents or BQ27220_ERROR */ -uint16_t bq27220_get_state_of_charge(FuriHalI2cBusHandle* handle); +uint16_t bq27220_get_state_of_charge(const FuriHalI2cBusHandle* handle); /** Get ratio of full charge capacity over design capacity * @@ -272,4 +278,4 @@ uint16_t bq27220_get_state_of_charge(FuriHalI2cBusHandle* handle); * * @return state of health in percents or BQ27220_ERROR */ -uint16_t bq27220_get_state_of_health(FuriHalI2cBusHandle* handle); +uint16_t bq27220_get_state_of_health(const FuriHalI2cBusHandle* handle); diff --git a/lib/drivers/cc1101.c b/lib/drivers/cc1101.c index 40b286a9b..ff2f0d610 100644 --- a/lib/drivers/cc1101.c +++ b/lib/drivers/cc1101.c @@ -3,7 +3,8 @@ #include #include -static bool cc1101_spi_trx(FuriHalSpiBusHandle* handle, uint8_t* tx, uint8_t* rx, uint8_t size) { +static bool + cc1101_spi_trx(const FuriHalSpiBusHandle* handle, uint8_t* tx, uint8_t* rx, uint8_t size) { FuriHalCortexTimer timer = furi_hal_cortex_timer_get(CC1101_TIMEOUT * 1000); while(furi_hal_gpio_read(handle->miso)) { @@ -16,7 +17,7 @@ static bool cc1101_spi_trx(FuriHalSpiBusHandle* handle, uint8_t* tx, uint8_t* rx return true; } -CC1101Status cc1101_strobe(FuriHalSpiBusHandle* handle, uint8_t strobe) { +CC1101Status cc1101_strobe(const FuriHalSpiBusHandle* handle, uint8_t strobe) { uint8_t tx[1] = {strobe}; CC1101Status rx[1] = {0}; rx[0].CHIP_RDYn = 1; @@ -27,7 +28,7 @@ CC1101Status cc1101_strobe(FuriHalSpiBusHandle* handle, uint8_t strobe) { return rx[0]; } -CC1101Status cc1101_write_reg(FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t data) { +CC1101Status cc1101_write_reg(const FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t data) { uint8_t tx[2] = {reg, data}; CC1101Status rx[2] = {0}; rx[0].CHIP_RDYn = 1; @@ -39,7 +40,7 @@ CC1101Status cc1101_write_reg(FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t return rx[1]; } -CC1101Status cc1101_read_reg(FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t* data) { +CC1101Status cc1101_read_reg(const FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t* data) { assert(sizeof(CC1101Status) == 1); uint8_t tx[2] = {reg | CC1101_READ, 0}; CC1101Status rx[2] = {0}; @@ -52,33 +53,36 @@ CC1101Status cc1101_read_reg(FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t* return rx[0]; } -uint8_t cc1101_get_partnumber(FuriHalSpiBusHandle* handle) { +uint8_t cc1101_get_partnumber(const FuriHalSpiBusHandle* handle) { uint8_t partnumber = 0; cc1101_read_reg(handle, CC1101_STATUS_PARTNUM | CC1101_BURST, &partnumber); return partnumber; } -uint8_t cc1101_get_version(FuriHalSpiBusHandle* handle) { +uint8_t cc1101_get_version(const FuriHalSpiBusHandle* handle) { uint8_t version = 0; cc1101_read_reg(handle, CC1101_STATUS_VERSION | CC1101_BURST, &version); return version; } -uint8_t cc1101_get_rssi(FuriHalSpiBusHandle* handle) { +uint8_t cc1101_get_rssi(const FuriHalSpiBusHandle* handle) { uint8_t rssi = 0; cc1101_read_reg(handle, CC1101_STATUS_RSSI | CC1101_BURST, &rssi); return rssi; } -CC1101Status cc1101_reset(FuriHalSpiBusHandle* handle) { +CC1101Status cc1101_reset(const FuriHalSpiBusHandle* handle) { return cc1101_strobe(handle, CC1101_STROBE_SRES); } -CC1101Status cc1101_get_status(FuriHalSpiBusHandle* handle) { +CC1101Status cc1101_get_status(const FuriHalSpiBusHandle* handle) { return cc1101_strobe(handle, CC1101_STROBE_SNOP); } -bool cc1101_wait_status_state(FuriHalSpiBusHandle* handle, CC1101State state, uint32_t timeout_us) { +bool cc1101_wait_status_state( + const FuriHalSpiBusHandle* handle, + CC1101State state, + uint32_t timeout_us) { bool result = false; CC1101Status status = {0}; FuriHalCortexTimer timer = furi_hal_cortex_timer_get(timeout_us); @@ -92,35 +96,35 @@ bool cc1101_wait_status_state(FuriHalSpiBusHandle* handle, CC1101State state, ui return result; } -CC1101Status cc1101_shutdown(FuriHalSpiBusHandle* handle) { +CC1101Status cc1101_shutdown(const FuriHalSpiBusHandle* handle) { return cc1101_strobe(handle, CC1101_STROBE_SPWD); } -CC1101Status cc1101_calibrate(FuriHalSpiBusHandle* handle) { +CC1101Status cc1101_calibrate(const FuriHalSpiBusHandle* handle) { return cc1101_strobe(handle, CC1101_STROBE_SCAL); } -CC1101Status cc1101_switch_to_idle(FuriHalSpiBusHandle* handle) { +CC1101Status cc1101_switch_to_idle(const FuriHalSpiBusHandle* handle) { return cc1101_strobe(handle, CC1101_STROBE_SIDLE); } -CC1101Status cc1101_switch_to_rx(FuriHalSpiBusHandle* handle) { +CC1101Status cc1101_switch_to_rx(const FuriHalSpiBusHandle* handle) { return cc1101_strobe(handle, CC1101_STROBE_SRX); } -CC1101Status cc1101_switch_to_tx(FuriHalSpiBusHandle* handle) { +CC1101Status cc1101_switch_to_tx(const FuriHalSpiBusHandle* handle) { return cc1101_strobe(handle, CC1101_STROBE_STX); } -CC1101Status cc1101_flush_rx(FuriHalSpiBusHandle* handle) { +CC1101Status cc1101_flush_rx(const FuriHalSpiBusHandle* handle) { return cc1101_strobe(handle, CC1101_STROBE_SFRX); } -CC1101Status cc1101_flush_tx(FuriHalSpiBusHandle* handle) { +CC1101Status cc1101_flush_tx(const FuriHalSpiBusHandle* handle) { return cc1101_strobe(handle, CC1101_STROBE_SFTX); } -uint32_t cc1101_set_frequency(FuriHalSpiBusHandle* handle, uint32_t value) { +uint32_t cc1101_set_frequency(const FuriHalSpiBusHandle* handle, uint32_t value) { uint64_t real_value = (uint64_t)value * CC1101_FDIV / CC1101_QUARTZ; // Sanity check @@ -135,7 +139,7 @@ uint32_t cc1101_set_frequency(FuriHalSpiBusHandle* handle, uint32_t value) { return (uint32_t)real_frequency; } -uint32_t cc1101_set_intermediate_frequency(FuriHalSpiBusHandle* handle, uint32_t value) { +uint32_t cc1101_set_intermediate_frequency(const FuriHalSpiBusHandle* handle, uint32_t value) { uint64_t real_value = value * CC1101_IFDIV / CC1101_QUARTZ; assert((real_value & 0xFF) == real_value); @@ -146,7 +150,7 @@ uint32_t cc1101_set_intermediate_frequency(FuriHalSpiBusHandle* handle, uint32_t return (uint32_t)real_frequency; } -void cc1101_set_pa_table(FuriHalSpiBusHandle* handle, const uint8_t value[8]) { +void cc1101_set_pa_table(const FuriHalSpiBusHandle* handle, const uint8_t value[8]) { uint8_t tx[9] = {CC1101_PATABLE | CC1101_BURST}; //-V1009 CC1101Status rx[9] = {0}; rx[0].CHIP_RDYn = 1; @@ -159,7 +163,7 @@ void cc1101_set_pa_table(FuriHalSpiBusHandle* handle, const uint8_t value[8]) { assert((rx[0].CHIP_RDYn | rx[8].CHIP_RDYn) == 0); } -uint8_t cc1101_write_fifo(FuriHalSpiBusHandle* handle, const uint8_t* data, uint8_t size) { +uint8_t cc1101_write_fifo(const FuriHalSpiBusHandle* handle, const uint8_t* data, uint8_t size) { uint8_t buff_tx[64]; uint8_t buff_rx[64]; buff_tx[0] = CC1101_FIFO | CC1101_BURST; @@ -170,7 +174,7 @@ uint8_t cc1101_write_fifo(FuriHalSpiBusHandle* handle, const uint8_t* data, uint return size; } -uint8_t cc1101_read_fifo(FuriHalSpiBusHandle* handle, uint8_t* data, uint8_t* size) { +uint8_t cc1101_read_fifo(const FuriHalSpiBusHandle* handle, uint8_t* data, uint8_t* size) { uint8_t buff_trx[2]; buff_trx[0] = CC1101_FIFO | CC1101_READ | CC1101_BURST; diff --git a/lib/drivers/cc1101.h b/lib/drivers/cc1101.h index c8c552bec..2828f1cdf 100644 --- a/lib/drivers/cc1101.h +++ b/lib/drivers/cc1101.h @@ -19,7 +19,7 @@ extern "C" { * * @return device status */ -CC1101Status cc1101_strobe(FuriHalSpiBusHandle* handle, uint8_t strobe); +CC1101Status cc1101_strobe(const FuriHalSpiBusHandle* handle, uint8_t strobe); /** Write device register * @@ -29,7 +29,7 @@ CC1101Status cc1101_strobe(FuriHalSpiBusHandle* handle, uint8_t strobe); * * @return device status */ -CC1101Status cc1101_write_reg(FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t data); +CC1101Status cc1101_write_reg(const FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t data); /** Read device register * @@ -39,7 +39,7 @@ CC1101Status cc1101_write_reg(FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t * * @return device status */ -CC1101Status cc1101_read_reg(FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t* data); +CC1101Status cc1101_read_reg(const FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t* data); /* High level API */ @@ -49,7 +49,7 @@ CC1101Status cc1101_read_reg(FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t* * * @return CC1101Status structure */ -CC1101Status cc1101_reset(FuriHalSpiBusHandle* handle); +CC1101Status cc1101_reset(const FuriHalSpiBusHandle* handle); /** Get status * @@ -57,7 +57,7 @@ CC1101Status cc1101_reset(FuriHalSpiBusHandle* handle); * * @return CC1101Status structure */ -CC1101Status cc1101_get_status(FuriHalSpiBusHandle* handle); +CC1101Status cc1101_get_status(const FuriHalSpiBusHandle* handle); /** Wait specific chip state * @@ -67,7 +67,10 @@ CC1101Status cc1101_get_status(FuriHalSpiBusHandle* handle); * * @return true on success, false otherwise */ -bool cc1101_wait_status_state(FuriHalSpiBusHandle* handle, CC1101State state, uint32_t timeout_us); +bool cc1101_wait_status_state( + const FuriHalSpiBusHandle* handle, + CC1101State state, + uint32_t timeout_us); /** Enable shutdown mode * @@ -75,7 +78,7 @@ bool cc1101_wait_status_state(FuriHalSpiBusHandle* handle, CC1101State state, ui * * @return CC1101Status structure */ -CC1101Status cc1101_shutdown(FuriHalSpiBusHandle* handle); +CC1101Status cc1101_shutdown(const FuriHalSpiBusHandle* handle); /** Get Partnumber * @@ -83,7 +86,7 @@ CC1101Status cc1101_shutdown(FuriHalSpiBusHandle* handle); * * @return part number id */ -uint8_t cc1101_get_partnumber(FuriHalSpiBusHandle* handle); +uint8_t cc1101_get_partnumber(const FuriHalSpiBusHandle* handle); /** Get Version * @@ -91,7 +94,7 @@ uint8_t cc1101_get_partnumber(FuriHalSpiBusHandle* handle); * * @return version */ -uint8_t cc1101_get_version(FuriHalSpiBusHandle* handle); +uint8_t cc1101_get_version(const FuriHalSpiBusHandle* handle); /** Get raw RSSI value * @@ -99,7 +102,7 @@ uint8_t cc1101_get_version(FuriHalSpiBusHandle* handle); * * @return rssi value */ -uint8_t cc1101_get_rssi(FuriHalSpiBusHandle* handle); +uint8_t cc1101_get_rssi(const FuriHalSpiBusHandle* handle); /** Calibrate oscillator * @@ -107,13 +110,13 @@ uint8_t cc1101_get_rssi(FuriHalSpiBusHandle* handle); * * @return CC1101Status structure */ -CC1101Status cc1101_calibrate(FuriHalSpiBusHandle* handle); +CC1101Status cc1101_calibrate(const FuriHalSpiBusHandle* handle); /** Switch to idle * * @param handle - pointer to FuriHalSpiHandle */ -CC1101Status cc1101_switch_to_idle(FuriHalSpiBusHandle* handle); +CC1101Status cc1101_switch_to_idle(const FuriHalSpiBusHandle* handle); /** Switch to RX * @@ -121,7 +124,7 @@ CC1101Status cc1101_switch_to_idle(FuriHalSpiBusHandle* handle); * * @return CC1101Status structure */ -CC1101Status cc1101_switch_to_rx(FuriHalSpiBusHandle* handle); +CC1101Status cc1101_switch_to_rx(const FuriHalSpiBusHandle* handle); /** Switch to TX * @@ -129,7 +132,7 @@ CC1101Status cc1101_switch_to_rx(FuriHalSpiBusHandle* handle); * * @return CC1101Status structure */ -CC1101Status cc1101_switch_to_tx(FuriHalSpiBusHandle* handle); +CC1101Status cc1101_switch_to_tx(const FuriHalSpiBusHandle* handle); /** Flush RX FIFO * @@ -137,13 +140,13 @@ CC1101Status cc1101_switch_to_tx(FuriHalSpiBusHandle* handle); * * @return CC1101Status structure */ -CC1101Status cc1101_flush_rx(FuriHalSpiBusHandle* handle); +CC1101Status cc1101_flush_rx(const FuriHalSpiBusHandle* handle); /** Flush TX FIFO * * @param handle - pointer to FuriHalSpiHandle */ -CC1101Status cc1101_flush_tx(FuriHalSpiBusHandle* handle); +CC1101Status cc1101_flush_tx(const FuriHalSpiBusHandle* handle); /** Set Frequency * @@ -152,7 +155,7 @@ CC1101Status cc1101_flush_tx(FuriHalSpiBusHandle* handle); * * @return real frequency that were synthesized */ -uint32_t cc1101_set_frequency(FuriHalSpiBusHandle* handle, uint32_t value); +uint32_t cc1101_set_frequency(const FuriHalSpiBusHandle* handle, uint32_t value); /** Set Intermediate Frequency * @@ -161,14 +164,14 @@ uint32_t cc1101_set_frequency(FuriHalSpiBusHandle* handle, uint32_t value); * * @return real inermediate frequency that were synthesized */ -uint32_t cc1101_set_intermediate_frequency(FuriHalSpiBusHandle* handle, uint32_t value); +uint32_t cc1101_set_intermediate_frequency(const FuriHalSpiBusHandle* handle, uint32_t value); /** Set Power Amplifier level table, ramp * * @param handle - pointer to FuriHalSpiHandle * @param value - array of power level values */ -void cc1101_set_pa_table(FuriHalSpiBusHandle* handle, const uint8_t value[8]); +void cc1101_set_pa_table(const FuriHalSpiBusHandle* handle, const uint8_t value[8]); /** Write FIFO * @@ -178,7 +181,7 @@ void cc1101_set_pa_table(FuriHalSpiBusHandle* handle, const uint8_t value[8]); * * @return size, written bytes count */ -uint8_t cc1101_write_fifo(FuriHalSpiBusHandle* handle, const uint8_t* data, uint8_t size); +uint8_t cc1101_write_fifo(const FuriHalSpiBusHandle* handle, const uint8_t* data, uint8_t size); /** Read FIFO * @@ -188,7 +191,7 @@ uint8_t cc1101_write_fifo(FuriHalSpiBusHandle* handle, const uint8_t* data, uint * * @return size, read bytes count */ -uint8_t cc1101_read_fifo(FuriHalSpiBusHandle* handle, uint8_t* data, uint8_t* size); +uint8_t cc1101_read_fifo(const FuriHalSpiBusHandle* handle, uint8_t* data, uint8_t* size); #ifdef __cplusplus } diff --git a/lib/drivers/lp5562.c b/lib/drivers/lp5562.c index 30a5b559a..7db9bbce4 100644 --- a/lib/drivers/lp5562.c +++ b/lib/drivers/lp5562.c @@ -3,12 +3,12 @@ #include "lp5562_reg.h" #include -void lp5562_reset(FuriHalI2cBusHandle* handle) { +void lp5562_reset(const FuriHalI2cBusHandle* handle) { Reg0D_Reset reg = {.value = 0xFF}; furi_hal_i2c_write_reg_8(handle, LP5562_ADDRESS, 0x0D, *(uint8_t*)®, LP5562_I2C_TIMEOUT); } -void lp5562_configure(FuriHalI2cBusHandle* handle) { +void lp5562_configure(const FuriHalI2cBusHandle* handle) { Reg08_Config config = {.INT_CLK_EN = true, .PS_EN = true, .PWM_HF = true}; furi_hal_i2c_write_reg_8(handle, LP5562_ADDRESS, 0x08, *(uint8_t*)&config, LP5562_I2C_TIMEOUT); @@ -21,14 +21,17 @@ void lp5562_configure(FuriHalI2cBusHandle* handle) { furi_hal_i2c_write_reg_8(handle, LP5562_ADDRESS, 0x70, *(uint8_t*)&map, LP5562_I2C_TIMEOUT); } -void lp5562_enable(FuriHalI2cBusHandle* handle) { +void lp5562_enable(const FuriHalI2cBusHandle* handle) { Reg00_Enable reg = {.CHIP_EN = true, .LOG_EN = true}; furi_hal_i2c_write_reg_8(handle, LP5562_ADDRESS, 0x00, *(uint8_t*)®, LP5562_I2C_TIMEOUT); //>488μs delay is required after writing to 0x00 register, otherwise program engine will not work furi_delay_us(500); } -void lp5562_set_channel_current(FuriHalI2cBusHandle* handle, LP5562Channel channel, uint8_t value) { +void lp5562_set_channel_current( + const FuriHalI2cBusHandle* handle, + LP5562Channel channel, + uint8_t value) { uint8_t reg_no; if(channel == LP5562ChannelRed) { reg_no = LP5562_CHANNEL_RED_CURRENT_REGISTER; @@ -44,7 +47,10 @@ void lp5562_set_channel_current(FuriHalI2cBusHandle* handle, LP5562Channel chann furi_hal_i2c_write_reg_8(handle, LP5562_ADDRESS, reg_no, value, LP5562_I2C_TIMEOUT); } -void lp5562_set_channel_value(FuriHalI2cBusHandle* handle, LP5562Channel channel, uint8_t value) { +void lp5562_set_channel_value( + const FuriHalI2cBusHandle* handle, + LP5562Channel channel, + uint8_t value) { uint8_t reg_no; if(channel == LP5562ChannelRed) { reg_no = LP5562_CHANNEL_RED_VALUE_REGISTER; @@ -60,7 +66,7 @@ void lp5562_set_channel_value(FuriHalI2cBusHandle* handle, LP5562Channel channel furi_hal_i2c_write_reg_8(handle, LP5562_ADDRESS, reg_no, value, LP5562_I2C_TIMEOUT); } -uint8_t lp5562_get_channel_value(FuriHalI2cBusHandle* handle, LP5562Channel channel) { +uint8_t lp5562_get_channel_value(const FuriHalI2cBusHandle* handle, LP5562Channel channel) { uint8_t reg_no; uint8_t value; if(channel == LP5562ChannelRed) { @@ -78,7 +84,10 @@ uint8_t lp5562_get_channel_value(FuriHalI2cBusHandle* handle, LP5562Channel chan return value; } -void lp5562_set_channel_src(FuriHalI2cBusHandle* handle, LP5562Channel channel, LP5562Engine src) { +void lp5562_set_channel_src( + const FuriHalI2cBusHandle* handle, + LP5562Channel channel, + LP5562Engine src) { uint8_t reg_val = 0; uint8_t bit_offset = 0; @@ -107,7 +116,7 @@ void lp5562_set_channel_src(FuriHalI2cBusHandle* handle, LP5562Channel channel, } void lp5562_execute_program( - FuriHalI2cBusHandle* handle, + const FuriHalI2cBusHandle* handle, LP5562Engine eng, LP5562Channel ch, uint16_t* program) { @@ -155,7 +164,7 @@ void lp5562_execute_program( furi_hal_i2c_write_reg_8(handle, LP5562_ADDRESS, 0x00, enable_reg, LP5562_I2C_TIMEOUT); } -void lp5562_stop_program(FuriHalI2cBusHandle* handle, LP5562Engine eng) { +void lp5562_stop_program(const FuriHalI2cBusHandle* handle, LP5562Engine eng) { if((eng < LP5562Engine1) || (eng > LP5562Engine3)) return; uint8_t reg_val = 0; uint8_t bit_offset = 0; @@ -169,7 +178,7 @@ void lp5562_stop_program(FuriHalI2cBusHandle* handle, LP5562Engine eng) { } void lp5562_execute_ramp( - FuriHalI2cBusHandle* handle, + const FuriHalI2cBusHandle* handle, LP5562Engine eng, LP5562Channel ch, uint8_t val_start, @@ -213,7 +222,7 @@ void lp5562_execute_ramp( } void lp5562_execute_blink( - FuriHalI2cBusHandle* handle, + const FuriHalI2cBusHandle* handle, LP5562Engine eng, LP5562Channel ch, uint16_t on_time, diff --git a/lib/drivers/lp5562.h b/lib/drivers/lp5562.h index f5ebeeae2..2e54e1ce3 100644 --- a/lib/drivers/lp5562.h +++ b/lib/drivers/lp5562.h @@ -20,39 +20,48 @@ typedef enum { } LP5562Engine; /** Initialize Driver */ -void lp5562_reset(FuriHalI2cBusHandle* handle); +void lp5562_reset(const FuriHalI2cBusHandle* handle); /** Configure Driver */ -void lp5562_configure(FuriHalI2cBusHandle* handle); +void lp5562_configure(const FuriHalI2cBusHandle* handle); /** Enable Driver */ -void lp5562_enable(FuriHalI2cBusHandle* handle); +void lp5562_enable(const FuriHalI2cBusHandle* handle); /** Set channel current */ -void lp5562_set_channel_current(FuriHalI2cBusHandle* handle, LP5562Channel channel, uint8_t value); +void lp5562_set_channel_current( + const FuriHalI2cBusHandle* handle, + LP5562Channel channel, + uint8_t value); /** Set channel PWM value */ -void lp5562_set_channel_value(FuriHalI2cBusHandle* handle, LP5562Channel channel, uint8_t value); +void lp5562_set_channel_value( + const FuriHalI2cBusHandle* handle, + LP5562Channel channel, + uint8_t value); /** Get channel PWM value */ -uint8_t lp5562_get_channel_value(FuriHalI2cBusHandle* handle, LP5562Channel channel); +uint8_t lp5562_get_channel_value(const FuriHalI2cBusHandle* handle, LP5562Channel channel); /** Set channel source */ -void lp5562_set_channel_src(FuriHalI2cBusHandle* handle, LP5562Channel channel, LP5562Engine src); +void lp5562_set_channel_src( + const FuriHalI2cBusHandle* handle, + LP5562Channel channel, + LP5562Engine src); /** Execute program sequence */ void lp5562_execute_program( - FuriHalI2cBusHandle* handle, + const FuriHalI2cBusHandle* handle, LP5562Engine eng, LP5562Channel ch, uint16_t* program); /** Stop program sequence */ -void lp5562_stop_program(FuriHalI2cBusHandle* handle, LP5562Engine eng); +void lp5562_stop_program(const FuriHalI2cBusHandle* handle, LP5562Engine eng); /** Execute ramp program sequence */ void lp5562_execute_ramp( - FuriHalI2cBusHandle* handle, + const FuriHalI2cBusHandle* handle, LP5562Engine eng, LP5562Channel ch, uint8_t val_start, @@ -61,7 +70,7 @@ void lp5562_execute_ramp( /** Start blink program sequence */ void lp5562_execute_blink( - FuriHalI2cBusHandle* handle, + const FuriHalI2cBusHandle* handle, LP5562Engine eng, LP5562Channel ch, uint16_t on_time, diff --git a/lib/drivers/st25r3916.c b/lib/drivers/st25r3916.c index f8dc9a5eb..0721a52c7 100644 --- a/lib/drivers/st25r3916.c +++ b/lib/drivers/st25r3916.c @@ -2,7 +2,7 @@ #include -void st25r3916_mask_irq(FuriHalSpiBusHandle* handle, uint32_t mask) { +void st25r3916_mask_irq(const FuriHalSpiBusHandle* handle, uint32_t mask) { furi_assert(handle); uint8_t irq_mask_regs[4] = { @@ -14,7 +14,7 @@ void st25r3916_mask_irq(FuriHalSpiBusHandle* handle, uint32_t mask) { st25r3916_write_burst_regs(handle, ST25R3916_REG_IRQ_MASK_MAIN, irq_mask_regs, 4); } -uint32_t st25r3916_get_irq(FuriHalSpiBusHandle* handle) { +uint32_t st25r3916_get_irq(const FuriHalSpiBusHandle* handle) { furi_assert(handle); uint8_t irq_regs[4] = {}; @@ -32,7 +32,7 @@ uint32_t st25r3916_get_irq(FuriHalSpiBusHandle* handle) { return irq; } -void st25r3916_write_fifo(FuriHalSpiBusHandle* handle, const uint8_t* buff, size_t bits) { +void st25r3916_write_fifo(const FuriHalSpiBusHandle* handle, const uint8_t* buff, size_t bits) { furi_assert(handle); furi_assert(buff); @@ -45,7 +45,7 @@ void st25r3916_write_fifo(FuriHalSpiBusHandle* handle, const uint8_t* buff, size } bool st25r3916_read_fifo( - FuriHalSpiBusHandle* handle, + const FuriHalSpiBusHandle* handle, uint8_t* buff, size_t buff_size, size_t* buff_bits) { diff --git a/lib/drivers/st25r3916.h b/lib/drivers/st25r3916.h index 0e77b6317..3eddaa430 100644 --- a/lib/drivers/st25r3916.h +++ b/lib/drivers/st25r3916.h @@ -75,7 +75,7 @@ extern "C" { * @param handle - pointer to FuriHalSpiBusHandle instance * @param mask - mask of interrupts to be disabled */ -void st25r3916_mask_irq(FuriHalSpiBusHandle* handle, uint32_t mask); +void st25r3916_mask_irq(const FuriHalSpiBusHandle* handle, uint32_t mask); /** Get st25r3916 interrupts * @@ -83,7 +83,7 @@ void st25r3916_mask_irq(FuriHalSpiBusHandle* handle, uint32_t mask); * * @return received interrupts */ -uint32_t st25r3916_get_irq(FuriHalSpiBusHandle* handle); +uint32_t st25r3916_get_irq(const FuriHalSpiBusHandle* handle); /** Write FIFO * @@ -91,7 +91,7 @@ uint32_t st25r3916_get_irq(FuriHalSpiBusHandle* handle); * @param buff - buffer to write to FIFO * @param bits - number of bits to write */ -void st25r3916_write_fifo(FuriHalSpiBusHandle* handle, const uint8_t* buff, size_t bits); +void st25r3916_write_fifo(const FuriHalSpiBusHandle* handle, const uint8_t* buff, size_t bits); /** Read FIFO * @@ -103,7 +103,7 @@ void st25r3916_write_fifo(FuriHalSpiBusHandle* handle, const uint8_t* buff, size * @return true if read success, false otherwise */ bool st25r3916_read_fifo( - FuriHalSpiBusHandle* handle, + const FuriHalSpiBusHandle* handle, uint8_t* buff, size_t buff_size, size_t* buff_bits); diff --git a/lib/drivers/st25r3916_reg.c b/lib/drivers/st25r3916_reg.c index f7a47d463..cdbf0fd3d 100644 --- a/lib/drivers/st25r3916_reg.c +++ b/lib/drivers/st25r3916_reg.c @@ -28,18 +28,18 @@ (ST25R3916_CMD_LEN + \ ST25R3916_FIFO_DEPTH) /*!< ST25R3916 communication buffer: CMD + FIFO length */ -static void st25r3916_reg_tx_byte(FuriHalSpiBusHandle* handle, uint8_t byte) { +static void st25r3916_reg_tx_byte(const FuriHalSpiBusHandle* handle, uint8_t byte) { uint8_t val = byte; furi_hal_spi_bus_tx(handle, &val, 1, 5); } -void st25r3916_read_reg(FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t* val) { +void st25r3916_read_reg(const FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t* val) { furi_check(handle); st25r3916_read_burst_regs(handle, reg, val, 1); } void st25r3916_read_burst_regs( - FuriHalSpiBusHandle* handle, + const FuriHalSpiBusHandle* handle, uint8_t reg_start, uint8_t* values, uint8_t length) { @@ -59,14 +59,14 @@ void st25r3916_read_burst_regs( furi_hal_gpio_write(handle->cs, true); } -void st25r3916_write_reg(FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t val) { +void st25r3916_write_reg(const FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t val) { furi_check(handle); uint8_t reg_val = val; st25r3916_write_burst_regs(handle, reg, ®_val, 1); } void st25r3916_write_burst_regs( - FuriHalSpiBusHandle* handle, + const FuriHalSpiBusHandle* handle, uint8_t reg_start, const uint8_t* values, uint8_t length) { @@ -86,7 +86,10 @@ void st25r3916_write_burst_regs( furi_hal_gpio_write(handle->cs, true); } -void st25r3916_reg_write_fifo(FuriHalSpiBusHandle* handle, const uint8_t* buff, size_t length) { +void st25r3916_reg_write_fifo( + const FuriHalSpiBusHandle* handle, + const uint8_t* buff, + size_t length) { furi_check(handle); furi_check(buff); furi_check(length); @@ -98,7 +101,7 @@ void st25r3916_reg_write_fifo(FuriHalSpiBusHandle* handle, const uint8_t* buff, furi_hal_gpio_write(handle->cs, true); } -void st25r3916_reg_read_fifo(FuriHalSpiBusHandle* handle, uint8_t* buff, size_t length) { +void st25r3916_reg_read_fifo(const FuriHalSpiBusHandle* handle, uint8_t* buff, size_t length) { furi_check(handle); furi_check(buff); furi_check(length); @@ -110,7 +113,10 @@ void st25r3916_reg_read_fifo(FuriHalSpiBusHandle* handle, uint8_t* buff, size_t furi_hal_gpio_write(handle->cs, true); } -void st25r3916_write_pta_mem(FuriHalSpiBusHandle* handle, const uint8_t* values, size_t length) { +void st25r3916_write_pta_mem( + const FuriHalSpiBusHandle* handle, + const uint8_t* values, + size_t length) { furi_check(handle); furi_check(values); furi_check(length); @@ -122,7 +128,7 @@ void st25r3916_write_pta_mem(FuriHalSpiBusHandle* handle, const uint8_t* values, furi_hal_gpio_write(handle->cs, true); } -void st25r3916_read_pta_mem(FuriHalSpiBusHandle* handle, uint8_t* buff, size_t length) { +void st25r3916_read_pta_mem(const FuriHalSpiBusHandle* handle, uint8_t* buff, size_t length) { furi_check(handle); furi_check(buff); furi_check(length); @@ -136,7 +142,10 @@ void st25r3916_read_pta_mem(FuriHalSpiBusHandle* handle, uint8_t* buff, size_t l memcpy(buff, tmp_buff + 1, length); } -void st25r3916_write_ptf_mem(FuriHalSpiBusHandle* handle, const uint8_t* values, size_t length) { +void st25r3916_write_ptf_mem( + const FuriHalSpiBusHandle* handle, + const uint8_t* values, + size_t length) { furi_check(handle); furi_check(values); @@ -146,7 +155,7 @@ void st25r3916_write_ptf_mem(FuriHalSpiBusHandle* handle, const uint8_t* values, furi_hal_gpio_write(handle->cs, true); } -void st25r3916_write_pttsn_mem(FuriHalSpiBusHandle* handle, uint8_t* buff, size_t length) { +void st25r3916_write_pttsn_mem(const FuriHalSpiBusHandle* handle, uint8_t* buff, size_t length) { furi_check(handle); furi_check(buff); @@ -156,7 +165,7 @@ void st25r3916_write_pttsn_mem(FuriHalSpiBusHandle* handle, uint8_t* buff, size_ furi_hal_gpio_write(handle->cs, true); } -void st25r3916_direct_cmd(FuriHalSpiBusHandle* handle, uint8_t cmd) { +void st25r3916_direct_cmd(const FuriHalSpiBusHandle* handle, uint8_t cmd) { furi_check(handle); furi_hal_gpio_write(handle->cs, false); @@ -164,7 +173,7 @@ void st25r3916_direct_cmd(FuriHalSpiBusHandle* handle, uint8_t cmd) { furi_hal_gpio_write(handle->cs, true); } -void st25r3916_read_test_reg(FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t* val) { +void st25r3916_read_test_reg(const FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t* val) { furi_check(handle); furi_hal_gpio_write(handle->cs, false); @@ -174,7 +183,7 @@ void st25r3916_read_test_reg(FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t* furi_hal_gpio_write(handle->cs, true); } -void st25r3916_write_test_reg(FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t val) { +void st25r3916_write_test_reg(const FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t val) { furi_check(handle); furi_hal_gpio_write(handle->cs, false); @@ -184,7 +193,7 @@ void st25r3916_write_test_reg(FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t furi_hal_gpio_write(handle->cs, true); } -void st25r3916_clear_reg_bits(FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t clr_mask) { +void st25r3916_clear_reg_bits(const FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t clr_mask) { furi_check(handle); uint8_t reg_val = 0; @@ -195,7 +204,7 @@ void st25r3916_clear_reg_bits(FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t } } -void st25r3916_set_reg_bits(FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t set_mask) { +void st25r3916_set_reg_bits(const FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t set_mask) { furi_check(handle); uint8_t reg_val = 0; @@ -207,7 +216,7 @@ void st25r3916_set_reg_bits(FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t se } void st25r3916_change_reg_bits( - FuriHalSpiBusHandle* handle, + const FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t mask, uint8_t value) { @@ -217,7 +226,7 @@ void st25r3916_change_reg_bits( } void st25r3916_modify_reg( - FuriHalSpiBusHandle* handle, + const FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t clr_mask, uint8_t set_mask) { @@ -233,7 +242,7 @@ void st25r3916_modify_reg( } void st25r3916_change_test_reg_bits( - FuriHalSpiBusHandle* handle, + const FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t mask, uint8_t value) { @@ -248,7 +257,7 @@ void st25r3916_change_test_reg_bits( } } -bool st25r3916_check_reg(FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t mask, uint8_t val) { +bool st25r3916_check_reg(const FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t mask, uint8_t val) { furi_check(handle); uint8_t reg_val = 0; diff --git a/lib/drivers/st25r3916_reg.h b/lib/drivers/st25r3916_reg.h index 5163c4423..524f93cc7 100644 --- a/lib/drivers/st25r3916_reg.h +++ b/lib/drivers/st25r3916_reg.h @@ -967,7 +967,7 @@ extern "C" { * @param reg - register address * @param val - pointer to the variable to store the read value */ -void st25r3916_read_reg(FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t* val); +void st25r3916_read_reg(const FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t* val); /** Read multiple registers * @@ -977,7 +977,7 @@ void st25r3916_read_reg(FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t* val); * @param length - number of registers to read */ void st25r3916_read_burst_regs( - FuriHalSpiBusHandle* handle, + const FuriHalSpiBusHandle* handle, uint8_t reg_start, uint8_t* values, uint8_t length); @@ -988,7 +988,7 @@ void st25r3916_read_burst_regs( * @param reg - register address * @param val - value to write */ -void st25r3916_write_reg(FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t val); +void st25r3916_write_reg(const FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t val); /** Write multiple registers * @@ -998,7 +998,7 @@ void st25r3916_write_reg(FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t val); * @param length - number of registers to write */ void st25r3916_write_burst_regs( - FuriHalSpiBusHandle* handle, + const FuriHalSpiBusHandle* handle, uint8_t reg_start, const uint8_t* values, uint8_t length); @@ -1009,7 +1009,10 @@ void st25r3916_write_burst_regs( * @param buff - buffer to write to FIFO * @param length - number of bytes to write */ -void st25r3916_reg_write_fifo(FuriHalSpiBusHandle* handle, const uint8_t* buff, size_t length); +void st25r3916_reg_write_fifo( + const FuriHalSpiBusHandle* handle, + const uint8_t* buff, + size_t length); /** Read fifo register * @@ -1017,7 +1020,7 @@ void st25r3916_reg_write_fifo(FuriHalSpiBusHandle* handle, const uint8_t* buff, * @param buff - buffer to store the read values * @param length - number of bytes to read */ -void st25r3916_reg_read_fifo(FuriHalSpiBusHandle* handle, uint8_t* buff, size_t length); +void st25r3916_reg_read_fifo(const FuriHalSpiBusHandle* handle, uint8_t* buff, size_t length); /** Write PTA memory register * @@ -1025,7 +1028,10 @@ void st25r3916_reg_read_fifo(FuriHalSpiBusHandle* handle, uint8_t* buff, size_t * @param values - pointer to buffer to write * @param length - number of bytes to write */ -void st25r3916_write_pta_mem(FuriHalSpiBusHandle* handle, const uint8_t* values, size_t length); +void st25r3916_write_pta_mem( + const FuriHalSpiBusHandle* handle, + const uint8_t* values, + size_t length); /** Read PTA memory register * @@ -1033,7 +1039,7 @@ void st25r3916_write_pta_mem(FuriHalSpiBusHandle* handle, const uint8_t* values, * @param values - buffer to store the read values * @param length - number of bytes to read */ -void st25r3916_read_pta_mem(FuriHalSpiBusHandle* handle, uint8_t* values, size_t length); +void st25r3916_read_pta_mem(const FuriHalSpiBusHandle* handle, uint8_t* values, size_t length); /** Write PTF memory register * @@ -1041,7 +1047,10 @@ void st25r3916_read_pta_mem(FuriHalSpiBusHandle* handle, uint8_t* values, size_t * @param values - pointer to buffer to write * @param length - number of bytes to write */ -void st25r3916_write_ptf_mem(FuriHalSpiBusHandle* handle, const uint8_t* values, size_t length); +void st25r3916_write_ptf_mem( + const FuriHalSpiBusHandle* handle, + const uint8_t* values, + size_t length); /** Read PTTSN memory register * @@ -1049,21 +1058,21 @@ void st25r3916_write_ptf_mem(FuriHalSpiBusHandle* handle, const uint8_t* values, * @param values - pointer to buffer to write * @param length - number of bytes to write */ -void st25r3916_write_pttsn_mem(FuriHalSpiBusHandle* handle, uint8_t* values, size_t length); +void st25r3916_write_pttsn_mem(const FuriHalSpiBusHandle* handle, uint8_t* values, size_t length); /** Send Direct command * * @param handle - pointer to FuriHalSpiBusHandle instance * @param cmd - direct command */ -void st25r3916_direct_cmd(FuriHalSpiBusHandle* handle, uint8_t cmd); +void st25r3916_direct_cmd(const FuriHalSpiBusHandle* handle, uint8_t cmd); /** Read test register * @param handle - pointer to FuriHalSpiBusHandle instance * @param reg - register address * @param val - pointer to the variable to store the read value */ -void st25r3916_read_test_reg(FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t* val); +void st25r3916_read_test_reg(const FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t* val); /** Write test register * @@ -1071,7 +1080,7 @@ void st25r3916_read_test_reg(FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t* * @param reg - register address * @param val - value to write */ -void st25r3916_write_test_reg(FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t val); +void st25r3916_write_test_reg(const FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t val); /** Clear register bits * @@ -1079,7 +1088,7 @@ void st25r3916_write_test_reg(FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t * @param reg - register address * @param clr_mask - bit mask to clear */ -void st25r3916_clear_reg_bits(FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t clr_mask); +void st25r3916_clear_reg_bits(const FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t clr_mask); /** Set register bits * @@ -1087,7 +1096,7 @@ void st25r3916_clear_reg_bits(FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t * @param reg - register address * @param set_mask - bit mask to set */ -void st25r3916_set_reg_bits(FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t set_mask); +void st25r3916_set_reg_bits(const FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t set_mask); /** Change register bits * @@ -1097,7 +1106,7 @@ void st25r3916_set_reg_bits(FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t se * @param value - new register value to write */ void st25r3916_change_reg_bits( - FuriHalSpiBusHandle* handle, + const FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t mask, uint8_t value); @@ -1110,7 +1119,7 @@ void st25r3916_change_reg_bits( * @param set_mask - bit mask to set */ void st25r3916_modify_reg( - FuriHalSpiBusHandle* handle, + const FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t clr_mask, uint8_t set_mask); @@ -1123,7 +1132,7 @@ void st25r3916_modify_reg( * @param value - new register value to write */ void st25r3916_change_test_reg_bits( - FuriHalSpiBusHandle* handle, + const FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t mask, uint8_t value); @@ -1137,7 +1146,7 @@ void st25r3916_change_test_reg_bits( * * @return true if register value matches the expected value, false otherwise */ -bool st25r3916_check_reg(FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t mask, uint8_t val); +bool st25r3916_check_reg(const FuriHalSpiBusHandle* handle, uint8_t reg, uint8_t mask, uint8_t val); #ifdef __cplusplus } diff --git a/lib/ibutton/protocols/dallas/protocol_group_dallas_defs.c b/lib/ibutton/protocols/dallas/protocol_group_dallas_defs.c index b4dd51ce7..df758283b 100644 --- a/lib/ibutton/protocols/dallas/protocol_group_dallas_defs.c +++ b/lib/ibutton/protocols/dallas/protocol_group_dallas_defs.c @@ -6,7 +6,7 @@ #include "protocol_ds1971.h" #include "protocol_ds_generic.h" -const iButtonProtocolDallasBase* ibutton_protocols_dallas[] = { +const iButtonProtocolDallasBase* const ibutton_protocols_dallas[] = { [iButtonProtocolDS1990] = &ibutton_protocol_ds1990, [iButtonProtocolDS1992] = &ibutton_protocol_ds1992, [iButtonProtocolDS1996] = &ibutton_protocol_ds1996, diff --git a/lib/ibutton/protocols/dallas/protocol_group_dallas_defs.h b/lib/ibutton/protocols/dallas/protocol_group_dallas_defs.h index 2ba1dd39a..71571d91b 100644 --- a/lib/ibutton/protocols/dallas/protocol_group_dallas_defs.h +++ b/lib/ibutton/protocols/dallas/protocol_group_dallas_defs.h @@ -14,4 +14,4 @@ typedef enum { iButtonProtocolDSMax, } iButtonProtocolDallas; -extern const iButtonProtocolDallasBase* ibutton_protocols_dallas[]; +extern const iButtonProtocolDallasBase* const ibutton_protocols_dallas[]; diff --git a/lib/ibutton/protocols/misc/protocol_group_misc_defs.c b/lib/ibutton/protocols/misc/protocol_group_misc_defs.c index 09ae0bdc7..f8cae0463 100644 --- a/lib/ibutton/protocols/misc/protocol_group_misc_defs.c +++ b/lib/ibutton/protocols/misc/protocol_group_misc_defs.c @@ -3,7 +3,7 @@ #include "protocol_cyfral.h" #include "protocol_metakom.h" -const ProtocolBase* ibutton_protocols_misc[] = { +const ProtocolBase* const ibutton_protocols_misc[] = { [iButtonProtocolMiscCyfral] = &ibutton_protocol_misc_cyfral, [iButtonProtocolMiscMetakom] = &ibutton_protocol_misc_metakom, /* Add new misc protocols here */ diff --git a/lib/ibutton/protocols/misc/protocol_group_misc_defs.h b/lib/ibutton/protocols/misc/protocol_group_misc_defs.h index 0a7f92847..cde6b0aa9 100644 --- a/lib/ibutton/protocols/misc/protocol_group_misc_defs.h +++ b/lib/ibutton/protocols/misc/protocol_group_misc_defs.h @@ -8,4 +8,4 @@ typedef enum { iButtonProtocolMiscMax, } iButtonProtocolMisc; -extern const ProtocolBase* ibutton_protocols_misc[]; +extern const ProtocolBase* const ibutton_protocols_misc[]; diff --git a/lib/ibutton/protocols/protocol_group_defs.c b/lib/ibutton/protocols/protocol_group_defs.c index 40a360f0e..ac428f410 100644 --- a/lib/ibutton/protocols/protocol_group_defs.c +++ b/lib/ibutton/protocols/protocol_group_defs.c @@ -3,7 +3,7 @@ #include "dallas/protocol_group_dallas.h" #include "misc/protocol_group_misc.h" -const iButtonProtocolGroupBase* ibutton_protocol_groups[] = { +const iButtonProtocolGroupBase* const ibutton_protocol_groups[] = { [iButtonProtocolGroupDallas] = &ibutton_protocol_group_dallas, [iButtonProtocolGroupMisc] = &ibutton_protocol_group_misc, }; diff --git a/lib/ibutton/protocols/protocol_group_defs.h b/lib/ibutton/protocols/protocol_group_defs.h index 2d41e3cb8..2c00dfab4 100644 --- a/lib/ibutton/protocols/protocol_group_defs.h +++ b/lib/ibutton/protocols/protocol_group_defs.h @@ -8,4 +8,4 @@ typedef enum { iButtonProtocolGroupMax } iButtonProtocolGroup; -extern const iButtonProtocolGroupBase* ibutton_protocol_groups[]; +extern const iButtonProtocolGroupBase* const ibutton_protocol_groups[]; diff --git a/lib/lfrfid/protocols/lfrfid_protocols.c b/lib/lfrfid/protocols/lfrfid_protocols.c index 238f8e0cd..c6cc57a68 100644 --- a/lib/lfrfid/protocols/lfrfid_protocols.c +++ b/lib/lfrfid/protocols/lfrfid_protocols.c @@ -22,7 +22,7 @@ #include "protocol_gproxii.h" #include "protocol_noralsy.h" -const ProtocolBase* lfrfid_protocols[] = { +const ProtocolBase* const lfrfid_protocols[] = { [LFRFIDProtocolEM4100] = &protocol_em4100, [LFRFIDProtocolEM410032] = &protocol_em4100_32, [LFRFIDProtocolEM410016] = &protocol_em4100_16, diff --git a/lib/lfrfid/protocols/lfrfid_protocols.h b/lib/lfrfid/protocols/lfrfid_protocols.h index 86c31c60b..a33e87990 100644 --- a/lib/lfrfid/protocols/lfrfid_protocols.h +++ b/lib/lfrfid/protocols/lfrfid_protocols.h @@ -37,7 +37,7 @@ typedef enum { LFRFIDProtocolMax, } LFRFIDProtocol; -extern const ProtocolBase* lfrfid_protocols[]; +extern const ProtocolBase* const lfrfid_protocols[]; typedef enum { LFRFIDWriteTypeT5577, diff --git a/lib/lfrfid/protocols/protocol_nexwatch.c b/lib/lfrfid/protocols/protocol_nexwatch.c index a794a4a8e..08324803f 100644 --- a/lib/lfrfid/protocols/protocol_nexwatch.c +++ b/lib/lfrfid/protocols/protocol_nexwatch.c @@ -21,7 +21,7 @@ typedef struct { uint8_t chk; } ProtocolNexwatchMagic; -ProtocolNexwatchMagic magic_items[] = { +static ProtocolNexwatchMagic magic_items[] = { {0xBE, "Quadrakey", 0}, {0x88, "Nexkey", 0}, {0x86, "Honeywell", 0}}; diff --git a/lib/mjs/mjs_parser.c b/lib/mjs/mjs_parser.c index 212804a86..98f60459d 100644 --- a/lib/mjs/mjs_parser.c +++ b/lib/mjs/mjs_parser.c @@ -47,7 +47,7 @@ static int ptest(struct pstate* p) { return tok; } -static int s_unary_ops[] = { +static const int s_unary_ops[] = { TOK_NOT, TOK_TILDA, TOK_PLUS_PLUS, @@ -56,10 +56,10 @@ static int s_unary_ops[] = { TOK_MINUS, TOK_PLUS, TOK_EOF}; -static int s_comparison_ops[] = {TOK_LT, TOK_LE, TOK_GT, TOK_GE, TOK_EOF}; -static int s_postfix_ops[] = {TOK_PLUS_PLUS, TOK_MINUS_MINUS, TOK_EOF}; -static int s_equality_ops[] = {TOK_EQ, TOK_NE, TOK_EQ_EQ, TOK_NE_NE, TOK_EOF}; -static int s_assign_ops[] = { +static const int s_comparison_ops[] = {TOK_LT, TOK_LE, TOK_GT, TOK_GE, TOK_EOF}; +static const int s_postfix_ops[] = {TOK_PLUS_PLUS, TOK_MINUS_MINUS, TOK_EOF}; +static const int s_equality_ops[] = {TOK_EQ, TOK_NE, TOK_EQ_EQ, TOK_NE_NE, TOK_EOF}; +static const int s_assign_ops[] = { TOK_ASSIGN, TOK_PLUS_ASSIGN, TOK_MINUS_ASSIGN, @@ -74,7 +74,7 @@ static int s_assign_ops[] = { TOK_OR_ASSIGN, TOK_EOF}; -static int findtok(int* toks, int tok) { +static int findtok(int const* toks, int tok) { int i = 0; while(tok != toks[i] && toks[i] != TOK_EOF) i++; diff --git a/lib/nfc/protocols/mf_classic/mf_classic_listener.c b/lib/nfc/protocols/mf_classic/mf_classic_listener.c index ef571117a..1f5eea271 100644 --- a/lib/nfc/protocols/mf_classic/mf_classic_listener.c +++ b/lib/nfc/protocols/mf_classic/mf_classic_listener.c @@ -19,7 +19,7 @@ typedef struct { uint8_t cmd_start_byte; size_t cmd_len_bits; size_t command_num; - MfClassicListenerCommandHandler* handler; + const MfClassicListenerCommandHandler* handler; } MfClassicListenerCmd; static void mf_classic_listener_prepare_emulation(MfClassicListener* instance) { @@ -441,42 +441,42 @@ static MfClassicListenerCommand return command; } -static MfClassicListenerCommandHandler mf_classic_listener_halt_handlers[] = { +static const MfClassicListenerCommandHandler mf_classic_listener_halt_handlers[] = { mf_classic_listener_halt_handler, }; -static MfClassicListenerCommandHandler mf_classic_listener_auth_key_a_handlers[] = { +static const MfClassicListenerCommandHandler mf_classic_listener_auth_key_a_handlers[] = { mf_classic_listener_auth_key_a_handler, mf_classic_listener_auth_second_part_handler, }; -static MfClassicListenerCommandHandler mf_classic_listener_auth_key_b_handlers[] = { +static const MfClassicListenerCommandHandler mf_classic_listener_auth_key_b_handlers[] = { mf_classic_listener_auth_key_b_handler, mf_classic_listener_auth_second_part_handler, }; -static MfClassicListenerCommandHandler mf_classic_listener_read_block_handlers[] = { +static const MfClassicListenerCommandHandler mf_classic_listener_read_block_handlers[] = { mf_classic_listener_read_block_handler, }; -static MfClassicListenerCommandHandler mf_classic_listener_write_block_handlers[] = { +static const MfClassicListenerCommandHandler mf_classic_listener_write_block_handlers[] = { mf_classic_listener_write_block_first_part_handler, mf_classic_listener_write_block_second_part_handler, }; -static MfClassicListenerCommandHandler mf_classic_listener_value_dec_handlers[] = { +static const MfClassicListenerCommandHandler mf_classic_listener_value_dec_handlers[] = { mf_classic_listener_value_dec_handler, mf_classic_listener_value_data_receive_handler, mf_classic_listener_value_transfer_handler, }; -static MfClassicListenerCommandHandler mf_classic_listener_value_inc_handlers[] = { +static const MfClassicListenerCommandHandler mf_classic_listener_value_inc_handlers[] = { mf_classic_listener_value_inc_handler, mf_classic_listener_value_data_receive_handler, mf_classic_listener_value_transfer_handler, }; -static MfClassicListenerCommandHandler mf_classic_listener_value_restore_handlers[] = { +static const MfClassicListenerCommandHandler mf_classic_listener_value_restore_handlers[] = { mf_classic_listener_value_restore_handler, mf_classic_listener_value_data_receive_handler, mf_classic_listener_value_transfer_handler, diff --git a/lib/nfc/protocols/nfc_device_defs.c b/lib/nfc/protocols/nfc_device_defs.c index 6a145445c..3f508a1fb 100644 --- a/lib/nfc/protocols/nfc_device_defs.c +++ b/lib/nfc/protocols/nfc_device_defs.c @@ -31,7 +31,7 @@ * When implementing a new protocol, add its implementation * here under its own index defined in nfc_protocol.h. */ -const NfcDeviceBase* nfc_devices[NfcProtocolNum] = { +const NfcDeviceBase* const nfc_devices[NfcProtocolNum] = { [NfcProtocolIso14443_3a] = &nfc_device_iso14443_3a, [NfcProtocolIso14443_3b] = &nfc_device_iso14443_3b, [NfcProtocolIso14443_4a] = &nfc_device_iso14443_4a, diff --git a/lib/nfc/protocols/nfc_device_defs.h b/lib/nfc/protocols/nfc_device_defs.h index f5ba2563f..e5d2707fd 100644 --- a/lib/nfc/protocols/nfc_device_defs.h +++ b/lib/nfc/protocols/nfc_device_defs.h @@ -6,7 +6,7 @@ extern "C" { #endif -extern const NfcDeviceBase* nfc_devices[]; +extern const NfcDeviceBase* const nfc_devices[]; #ifdef __cplusplus } diff --git a/lib/nfc/protocols/nfc_listener_defs.c b/lib/nfc/protocols/nfc_listener_defs.c index 2a6167e9c..c27079f5a 100644 --- a/lib/nfc/protocols/nfc_listener_defs.c +++ b/lib/nfc/protocols/nfc_listener_defs.c @@ -8,7 +8,7 @@ #include #include -const NfcListenerBase* nfc_listeners_api[NfcProtocolNum] = { +const NfcListenerBase* const nfc_listeners_api[NfcProtocolNum] = { [NfcProtocolIso14443_3a] = &nfc_listener_iso14443_3a, [NfcProtocolIso14443_3b] = NULL, [NfcProtocolIso14443_4a] = &nfc_listener_iso14443_4a, diff --git a/lib/nfc/protocols/nfc_listener_defs.h b/lib/nfc/protocols/nfc_listener_defs.h index 4d88cc098..7bd4b49b0 100644 --- a/lib/nfc/protocols/nfc_listener_defs.h +++ b/lib/nfc/protocols/nfc_listener_defs.h @@ -7,7 +7,7 @@ extern "C" { #endif -extern const NfcListenerBase* nfc_listeners_api[NfcProtocolNum]; +extern const NfcListenerBase* const nfc_listeners_api[NfcProtocolNum]; #ifdef __cplusplus } diff --git a/lib/nfc/protocols/nfc_poller_defs.c b/lib/nfc/protocols/nfc_poller_defs.c index c007740b7..f3ad15e7d 100644 --- a/lib/nfc/protocols/nfc_poller_defs.c +++ b/lib/nfc/protocols/nfc_poller_defs.c @@ -13,7 +13,7 @@ #include #include -const NfcPollerBase* nfc_pollers_api[NfcProtocolNum] = { +const NfcPollerBase* const nfc_pollers_api[NfcProtocolNum] = { [NfcProtocolIso14443_3a] = &nfc_poller_iso14443_3a, [NfcProtocolIso14443_3b] = &nfc_poller_iso14443_3b, [NfcProtocolIso14443_4a] = &nfc_poller_iso14443_4a, diff --git a/lib/nfc/protocols/nfc_poller_defs.h b/lib/nfc/protocols/nfc_poller_defs.h index a406a5f08..0156cc8b2 100644 --- a/lib/nfc/protocols/nfc_poller_defs.h +++ b/lib/nfc/protocols/nfc_poller_defs.h @@ -7,7 +7,7 @@ extern "C" { #endif -extern const NfcPollerBase* nfc_pollers_api[NfcProtocolNum]; +extern const NfcPollerBase* const nfc_pollers_api[NfcProtocolNum]; #ifdef __cplusplus } diff --git a/lib/subghz/protocols/protocol_items.c b/lib/subghz/protocols/protocol_items.c index 0d9c2a088..d7a14cd1c 100644 --- a/lib/subghz/protocols/protocol_items.c +++ b/lib/subghz/protocols/protocol_items.c @@ -1,6 +1,6 @@ #include "protocol_items.h" // IWYU pragma: keep -const SubGhzProtocol* subghz_protocol_registry_items[] = { +const SubGhzProtocol* const subghz_protocol_registry_items[] = { &subghz_protocol_gate_tx, &subghz_protocol_keeloq, &subghz_protocol_star_line, diff --git a/lib/subghz/registry.h b/lib/subghz/registry.h index 8529c1097..8de376b16 100644 --- a/lib/subghz/registry.h +++ b/lib/subghz/registry.h @@ -12,7 +12,7 @@ typedef struct SubGhzProtocolRegistry SubGhzProtocolRegistry; typedef struct SubGhzProtocol SubGhzProtocol; struct SubGhzProtocolRegistry { - const SubGhzProtocol** items; + const SubGhzProtocol* const* items; const size_t size; }; diff --git a/lib/toolbox/protocols/protocol_dict.c b/lib/toolbox/protocols/protocol_dict.c index 5680be18d..5cc46d6eb 100644 --- a/lib/toolbox/protocols/protocol_dict.c +++ b/lib/toolbox/protocols/protocol_dict.c @@ -2,12 +2,12 @@ #include "protocol_dict.h" struct ProtocolDict { - const ProtocolBase** base; + const ProtocolBase* const* base; size_t count; void* data[]; }; -ProtocolDict* protocol_dict_alloc(const ProtocolBase** protocols, size_t count) { +ProtocolDict* protocol_dict_alloc(const ProtocolBase* const* protocols, size_t count) { furi_check(protocols); ProtocolDict* dict = malloc(sizeof(ProtocolDict) + (sizeof(void*) * count)); diff --git a/lib/toolbox/protocols/protocol_dict.h b/lib/toolbox/protocols/protocol_dict.h index 543b3ead2..a7e02a988 100644 --- a/lib/toolbox/protocols/protocol_dict.h +++ b/lib/toolbox/protocols/protocol_dict.h @@ -12,7 +12,7 @@ typedef int32_t ProtocolId; #define PROTOCOL_NO (-1) #define PROTOCOL_ALL_FEATURES (0xFFFFFFFF) -ProtocolDict* protocol_dict_alloc(const ProtocolBase** protocols, size_t protocol_count); +ProtocolDict* protocol_dict_alloc(const ProtocolBase* const* protocols, size_t protocol_count); void protocol_dict_free(ProtocolDict* dict); diff --git a/scripts/fbt/sdk/collector.py b/scripts/fbt/sdk/collector.py index 5615f105e..14653eb44 100644 --- a/scripts/fbt/sdk/collector.py +++ b/scripts/fbt/sdk/collector.py @@ -113,7 +113,7 @@ def stringify_descr(type_descr): # Hack if isinstance(type_descr.ptr_to, FunctionType): return stringify_descr(type_descr.ptr_to) - return f"{stringify_descr(type_descr.ptr_to)}*" + return f"{stringify_descr(type_descr.ptr_to)}*{' const' if type_descr.const else ''}" elif isinstance(type_descr, Type): return ( f"{'const ' if type_descr.const else ''}" diff --git a/targets/f18/api_symbols.csv b/targets/f18/api_symbols.csv index 81ecb9c7e..981340138 100644 --- a/targets/f18/api_symbols.csv +++ b/targets/f18/api_symbols.csv @@ -1,5 +1,5 @@ entry,status,name,type,params -Version,+,82.2,, +Version,+,83.0,, Header,+,applications/services/bt/bt_service/bt.h,, Header,+,applications/services/bt/bt_service/bt_keys_storage.h,, Header,+,applications/services/cli/cli.h,, @@ -788,7 +788,7 @@ Function,+,cli_print_usage,void,"const char*, const char*, const char*" Function,+,cli_read,size_t,"Cli*, uint8_t*, size_t" Function,+,cli_read_timeout,size_t,"Cli*, uint8_t*, size_t, uint32_t" Function,+,cli_session_close,void,Cli* -Function,+,cli_session_open,void,"Cli*, void*" +Function,+,cli_session_open,void,"Cli*, const void*" Function,+,cli_write,void,"Cli*, const uint8_t*, size_t" Function,+,composite_api_resolver_add,void,"CompositeApiResolver*, const ElfApiInterface*" Function,+,composite_api_resolver_alloc,CompositeApiResolver*, @@ -1290,23 +1290,23 @@ Function,+,furi_hal_hid_u2f_get_request,uint32_t,uint8_t* Function,+,furi_hal_hid_u2f_is_connected,_Bool, Function,+,furi_hal_hid_u2f_send_response,void,"uint8_t*, uint8_t" Function,+,furi_hal_hid_u2f_set_callback,void,"HidU2fCallback, void*" -Function,+,furi_hal_i2c_acquire,void,FuriHalI2cBusHandle* +Function,+,furi_hal_i2c_acquire,void,const FuriHalI2cBusHandle* Function,-,furi_hal_i2c_deinit_early,void, Function,-,furi_hal_i2c_init,void, Function,-,furi_hal_i2c_init_early,void, -Function,+,furi_hal_i2c_is_device_ready,_Bool,"FuriHalI2cBusHandle*, uint8_t, uint32_t" -Function,+,furi_hal_i2c_read_mem,_Bool,"FuriHalI2cBusHandle*, uint8_t, uint8_t, uint8_t*, size_t, uint32_t" -Function,+,furi_hal_i2c_read_reg_16,_Bool,"FuriHalI2cBusHandle*, uint8_t, uint8_t, uint16_t*, uint32_t" -Function,+,furi_hal_i2c_read_reg_8,_Bool,"FuriHalI2cBusHandle*, uint8_t, uint8_t, uint8_t*, uint32_t" -Function,+,furi_hal_i2c_release,void,FuriHalI2cBusHandle* -Function,+,furi_hal_i2c_rx,_Bool,"FuriHalI2cBusHandle*, uint8_t, uint8_t*, size_t, uint32_t" -Function,+,furi_hal_i2c_rx_ext,_Bool,"FuriHalI2cBusHandle*, uint16_t, _Bool, uint8_t*, size_t, FuriHalI2cBegin, FuriHalI2cEnd, uint32_t" -Function,+,furi_hal_i2c_trx,_Bool,"FuriHalI2cBusHandle*, uint8_t, const uint8_t*, size_t, uint8_t*, size_t, uint32_t" -Function,+,furi_hal_i2c_tx,_Bool,"FuriHalI2cBusHandle*, uint8_t, const uint8_t*, size_t, uint32_t" -Function,+,furi_hal_i2c_tx_ext,_Bool,"FuriHalI2cBusHandle*, uint16_t, _Bool, const uint8_t*, size_t, FuriHalI2cBegin, FuriHalI2cEnd, uint32_t" -Function,+,furi_hal_i2c_write_mem,_Bool,"FuriHalI2cBusHandle*, uint8_t, uint8_t, const uint8_t*, size_t, uint32_t" -Function,+,furi_hal_i2c_write_reg_16,_Bool,"FuriHalI2cBusHandle*, uint8_t, uint8_t, uint16_t, uint32_t" -Function,+,furi_hal_i2c_write_reg_8,_Bool,"FuriHalI2cBusHandle*, uint8_t, uint8_t, uint8_t, uint32_t" +Function,+,furi_hal_i2c_is_device_ready,_Bool,"const FuriHalI2cBusHandle*, uint8_t, uint32_t" +Function,+,furi_hal_i2c_read_mem,_Bool,"const FuriHalI2cBusHandle*, uint8_t, uint8_t, uint8_t*, size_t, uint32_t" +Function,+,furi_hal_i2c_read_reg_16,_Bool,"const FuriHalI2cBusHandle*, uint8_t, uint8_t, uint16_t*, uint32_t" +Function,+,furi_hal_i2c_read_reg_8,_Bool,"const FuriHalI2cBusHandle*, uint8_t, uint8_t, uint8_t*, uint32_t" +Function,+,furi_hal_i2c_release,void,const FuriHalI2cBusHandle* +Function,+,furi_hal_i2c_rx,_Bool,"const FuriHalI2cBusHandle*, uint8_t, uint8_t*, size_t, uint32_t" +Function,+,furi_hal_i2c_rx_ext,_Bool,"const FuriHalI2cBusHandle*, uint16_t, _Bool, uint8_t*, size_t, FuriHalI2cBegin, FuriHalI2cEnd, uint32_t" +Function,+,furi_hal_i2c_trx,_Bool,"const FuriHalI2cBusHandle*, uint8_t, const uint8_t*, size_t, uint8_t*, size_t, uint32_t" +Function,+,furi_hal_i2c_tx,_Bool,"const FuriHalI2cBusHandle*, uint8_t, const uint8_t*, size_t, uint32_t" +Function,+,furi_hal_i2c_tx_ext,_Bool,"const FuriHalI2cBusHandle*, uint16_t, _Bool, const uint8_t*, size_t, FuriHalI2cBegin, FuriHalI2cEnd, uint32_t" +Function,+,furi_hal_i2c_write_mem,_Bool,"const FuriHalI2cBusHandle*, uint8_t, uint8_t, const uint8_t*, size_t, uint32_t" +Function,+,furi_hal_i2c_write_reg_16,_Bool,"const FuriHalI2cBusHandle*, uint8_t, uint8_t, uint16_t, uint32_t" +Function,+,furi_hal_i2c_write_reg_8,_Bool,"const FuriHalI2cBusHandle*, uint8_t, uint8_t, uint8_t, uint32_t" Function,+,furi_hal_info_get,void,"PropertyValueCallback, char, void*" Function,+,furi_hal_info_get_api_version,void,"uint16_t*, uint16_t*" Function,-,furi_hal_init,void, @@ -1472,20 +1472,20 @@ Function,+,furi_hal_speaker_release,void, Function,+,furi_hal_speaker_set_volume,void,float Function,+,furi_hal_speaker_start,void,"float, float" Function,+,furi_hal_speaker_stop,void, -Function,+,furi_hal_spi_acquire,void,FuriHalSpiBusHandle* +Function,+,furi_hal_spi_acquire,void,const FuriHalSpiBusHandle* Function,+,furi_hal_spi_bus_deinit,void,FuriHalSpiBus* -Function,+,furi_hal_spi_bus_handle_deinit,void,FuriHalSpiBusHandle* -Function,+,furi_hal_spi_bus_handle_init,void,FuriHalSpiBusHandle* +Function,+,furi_hal_spi_bus_handle_deinit,void,const FuriHalSpiBusHandle* +Function,+,furi_hal_spi_bus_handle_init,void,const FuriHalSpiBusHandle* Function,+,furi_hal_spi_bus_init,void,FuriHalSpiBus* -Function,+,furi_hal_spi_bus_rx,_Bool,"FuriHalSpiBusHandle*, uint8_t*, size_t, uint32_t" -Function,+,furi_hal_spi_bus_trx,_Bool,"FuriHalSpiBusHandle*, const uint8_t*, uint8_t*, size_t, uint32_t" -Function,+,furi_hal_spi_bus_trx_dma,_Bool,"FuriHalSpiBusHandle*, uint8_t*, uint8_t*, size_t, uint32_t" -Function,+,furi_hal_spi_bus_tx,_Bool,"FuriHalSpiBusHandle*, const uint8_t*, size_t, uint32_t" +Function,+,furi_hal_spi_bus_rx,_Bool,"const FuriHalSpiBusHandle*, uint8_t*, size_t, uint32_t" +Function,+,furi_hal_spi_bus_trx,_Bool,"const FuriHalSpiBusHandle*, const uint8_t*, uint8_t*, size_t, uint32_t" +Function,+,furi_hal_spi_bus_trx_dma,_Bool,"const FuriHalSpiBusHandle*, uint8_t*, uint8_t*, size_t, uint32_t" +Function,+,furi_hal_spi_bus_tx,_Bool,"const FuriHalSpiBusHandle*, const uint8_t*, size_t, uint32_t" Function,-,furi_hal_spi_config_deinit_early,void, Function,-,furi_hal_spi_config_init,void, Function,-,furi_hal_spi_config_init_early,void, Function,-,furi_hal_spi_dma_init,void, -Function,+,furi_hal_spi_release,void,FuriHalSpiBusHandle* +Function,+,furi_hal_spi_release,void,const FuriHalSpiBusHandle* Function,+,furi_hal_switch,void,void* Function,+,furi_hal_usb_ccid_insert_smartcard,void, Function,+,furi_hal_usb_ccid_remove_smartcard,void, @@ -1498,7 +1498,7 @@ Function,+,furi_hal_usb_is_locked,_Bool, Function,+,furi_hal_usb_lock,void, Function,+,furi_hal_usb_reinit,void, Function,+,furi_hal_usb_set_config,_Bool,"FuriHalUsbInterface*, void*" -Function,-,furi_hal_usb_set_state_callback,void,"FuriHalUsbStateCallback, void*" +Function,+,furi_hal_usb_set_state_callback,void,"FuriHalUsbStateCallback, void*" Function,+,furi_hal_usb_unlock,void, Function,+,furi_hal_version_do_i_belong_here,_Bool, Function,+,furi_hal_version_get_ble_local_device_name_ptr,const char*, @@ -1736,7 +1736,7 @@ Function,-,getchar,int, Function,-,getchar_unlocked,int, Function,-,getenv,char*,const char* Function,-,gets,char*,char* -Function,-,getsubopt,int,"char**, char**, char**" +Function,-,getsubopt,int,"char**, char* const*, char**" Function,-,getw,int,FILE* Function,+,gui_add_framebuffer_callback,void,"Gui*, GuiCanvasCommitCallback, void*" Function,+,gui_add_view_port,void,"Gui*, ViewPort*, GuiLayer" @@ -2376,7 +2376,7 @@ Function,-,powl,long double,"long double, long double" Function,+,pretty_format_bytes_hex_canonical,void,"FuriString*, size_t, const char*, const uint8_t*, size_t" Function,-,printf,int,"const char*, ..." Function,+,property_value_out,void,"PropertyValueContext*, const char*, unsigned int, ..." -Function,+,protocol_dict_alloc,ProtocolDict*,"const ProtocolBase**, size_t" +Function,+,protocol_dict_alloc,ProtocolDict*,"const ProtocolBase* const*, size_t" Function,+,protocol_dict_decoders_feed,ProtocolId,"ProtocolDict*, _Bool, uint32_t" Function,+,protocol_dict_decoders_feed_by_feature,ProtocolId,"ProtocolDict*, uint32_t, _Bool, uint32_t" Function,+,protocol_dict_decoders_feed_by_id,ProtocolId,"ProtocolDict*, size_t, _Bool, uint32_t" @@ -2539,29 +2539,29 @@ Function,+,srand,void,unsigned Function,-,srand48,void,long Function,-,srandom,void,unsigned Function,+,sscanf,int,"const char*, const char*, ..." -Function,+,st25r3916_change_reg_bits,void,"FuriHalSpiBusHandle*, uint8_t, uint8_t, uint8_t" -Function,+,st25r3916_change_test_reg_bits,void,"FuriHalSpiBusHandle*, uint8_t, uint8_t, uint8_t" -Function,+,st25r3916_check_reg,_Bool,"FuriHalSpiBusHandle*, uint8_t, uint8_t, uint8_t" -Function,+,st25r3916_clear_reg_bits,void,"FuriHalSpiBusHandle*, uint8_t, uint8_t" -Function,+,st25r3916_direct_cmd,void,"FuriHalSpiBusHandle*, uint8_t" -Function,+,st25r3916_get_irq,uint32_t,FuriHalSpiBusHandle* -Function,+,st25r3916_mask_irq,void,"FuriHalSpiBusHandle*, uint32_t" -Function,+,st25r3916_modify_reg,void,"FuriHalSpiBusHandle*, uint8_t, uint8_t, uint8_t" -Function,+,st25r3916_read_burst_regs,void,"FuriHalSpiBusHandle*, uint8_t, uint8_t*, uint8_t" -Function,+,st25r3916_read_fifo,_Bool,"FuriHalSpiBusHandle*, uint8_t*, size_t, size_t*" -Function,+,st25r3916_read_pta_mem,void,"FuriHalSpiBusHandle*, uint8_t*, size_t" -Function,+,st25r3916_read_reg,void,"FuriHalSpiBusHandle*, uint8_t, uint8_t*" -Function,+,st25r3916_read_test_reg,void,"FuriHalSpiBusHandle*, uint8_t, uint8_t*" -Function,+,st25r3916_reg_read_fifo,void,"FuriHalSpiBusHandle*, uint8_t*, size_t" -Function,+,st25r3916_reg_write_fifo,void,"FuriHalSpiBusHandle*, const uint8_t*, size_t" -Function,+,st25r3916_set_reg_bits,void,"FuriHalSpiBusHandle*, uint8_t, uint8_t" -Function,+,st25r3916_write_burst_regs,void,"FuriHalSpiBusHandle*, uint8_t, const uint8_t*, uint8_t" -Function,+,st25r3916_write_fifo,void,"FuriHalSpiBusHandle*, const uint8_t*, size_t" -Function,+,st25r3916_write_pta_mem,void,"FuriHalSpiBusHandle*, const uint8_t*, size_t" -Function,+,st25r3916_write_ptf_mem,void,"FuriHalSpiBusHandle*, const uint8_t*, size_t" -Function,+,st25r3916_write_pttsn_mem,void,"FuriHalSpiBusHandle*, uint8_t*, size_t" -Function,+,st25r3916_write_reg,void,"FuriHalSpiBusHandle*, uint8_t, uint8_t" -Function,+,st25r3916_write_test_reg,void,"FuriHalSpiBusHandle*, uint8_t, uint8_t" +Function,+,st25r3916_change_reg_bits,void,"const FuriHalSpiBusHandle*, uint8_t, uint8_t, uint8_t" +Function,+,st25r3916_change_test_reg_bits,void,"const FuriHalSpiBusHandle*, uint8_t, uint8_t, uint8_t" +Function,+,st25r3916_check_reg,_Bool,"const FuriHalSpiBusHandle*, uint8_t, uint8_t, uint8_t" +Function,+,st25r3916_clear_reg_bits,void,"const FuriHalSpiBusHandle*, uint8_t, uint8_t" +Function,+,st25r3916_direct_cmd,void,"const FuriHalSpiBusHandle*, uint8_t" +Function,+,st25r3916_get_irq,uint32_t,const FuriHalSpiBusHandle* +Function,+,st25r3916_mask_irq,void,"const FuriHalSpiBusHandle*, uint32_t" +Function,+,st25r3916_modify_reg,void,"const FuriHalSpiBusHandle*, uint8_t, uint8_t, uint8_t" +Function,+,st25r3916_read_burst_regs,void,"const FuriHalSpiBusHandle*, uint8_t, uint8_t*, uint8_t" +Function,+,st25r3916_read_fifo,_Bool,"const FuriHalSpiBusHandle*, uint8_t*, size_t, size_t*" +Function,+,st25r3916_read_pta_mem,void,"const FuriHalSpiBusHandle*, uint8_t*, size_t" +Function,+,st25r3916_read_reg,void,"const FuriHalSpiBusHandle*, uint8_t, uint8_t*" +Function,+,st25r3916_read_test_reg,void,"const FuriHalSpiBusHandle*, uint8_t, uint8_t*" +Function,+,st25r3916_reg_read_fifo,void,"const FuriHalSpiBusHandle*, uint8_t*, size_t" +Function,+,st25r3916_reg_write_fifo,void,"const FuriHalSpiBusHandle*, const uint8_t*, size_t" +Function,+,st25r3916_set_reg_bits,void,"const FuriHalSpiBusHandle*, uint8_t, uint8_t" +Function,+,st25r3916_write_burst_regs,void,"const FuriHalSpiBusHandle*, uint8_t, const uint8_t*, uint8_t" +Function,+,st25r3916_write_fifo,void,"const FuriHalSpiBusHandle*, const uint8_t*, size_t" +Function,+,st25r3916_write_pta_mem,void,"const FuriHalSpiBusHandle*, const uint8_t*, size_t" +Function,+,st25r3916_write_ptf_mem,void,"const FuriHalSpiBusHandle*, const uint8_t*, size_t" +Function,+,st25r3916_write_pttsn_mem,void,"const FuriHalSpiBusHandle*, uint8_t*, size_t" +Function,+,st25r3916_write_reg,void,"const FuriHalSpiBusHandle*, uint8_t, uint8_t" +Function,+,st25r3916_write_test_reg,void,"const FuriHalSpiBusHandle*, uint8_t, uint8_t" Function,+,storage_common_copy,FS_Error,"Storage*, const char*, const char*" Function,+,storage_common_equivalent_path,_Bool,"Storage*, const char*, const char*" Function,+,storage_common_exists,_Bool,"Storage*, const char*" @@ -2935,22 +2935,22 @@ Variable,-,__stdio_exit_handler,void (*)(), Variable,+,_ctype_,const char[], Variable,+,_impure_data,_reent, Variable,+,_impure_ptr,_reent*, -Variable,-,_sys_errlist,const char*[], +Variable,-,_sys_errlist,const char* const[], Variable,-,_sys_nerr,int, Variable,-,ble_profile_hid,const FuriHalBleProfileTemplate*, -Variable,-,ble_profile_serial,const FuriHalBleProfileTemplate*, -Variable,+,cli_vcp,CliSession, +Variable,+,ble_profile_serial,const FuriHalBleProfileTemplate* const, +Variable,+,cli_vcp,const CliSession, Variable,+,compress_config_heatshrink_default,const CompressConfigHeatshrink, -Variable,+,firmware_api_interface,const ElfApiInterface*, +Variable,+,firmware_api_interface,const ElfApiInterface* const, Variable,+,furi_hal_i2c_bus_external,FuriHalI2cBus, Variable,+,furi_hal_i2c_bus_power,FuriHalI2cBus, -Variable,+,furi_hal_i2c_handle_external,FuriHalI2cBusHandle, -Variable,+,furi_hal_i2c_handle_power,FuriHalI2cBusHandle, +Variable,+,furi_hal_i2c_handle_external,const FuriHalI2cBusHandle, +Variable,+,furi_hal_i2c_handle_power,const FuriHalI2cBusHandle, Variable,+,furi_hal_spi_bus_d,FuriHalSpiBus, -Variable,+,furi_hal_spi_bus_handle_display,FuriHalSpiBusHandle, -Variable,+,furi_hal_spi_bus_handle_external,FuriHalSpiBusHandle, -Variable,+,furi_hal_spi_bus_handle_sd_fast,FuriHalSpiBusHandle, -Variable,+,furi_hal_spi_bus_handle_sd_slow,FuriHalSpiBusHandle, +Variable,+,furi_hal_spi_bus_handle_display,const FuriHalSpiBusHandle, +Variable,+,furi_hal_spi_bus_handle_external,const FuriHalSpiBusHandle, +Variable,+,furi_hal_spi_bus_handle_sd_fast,const FuriHalSpiBusHandle, +Variable,+,furi_hal_spi_bus_handle_sd_slow,const FuriHalSpiBusHandle, Variable,+,furi_hal_spi_bus_r,FuriHalSpiBus, Variable,+,furi_hal_spi_preset_1edge_low_16m,const LL_SPI_InitTypeDef, Variable,+,furi_hal_spi_preset_1edge_low_2m,const LL_SPI_InitTypeDef, diff --git a/targets/f18/furi_hal/furi_hal_spi_config.c b/targets/f18/furi_hal/furi_hal_spi_config.c index 8957bfe3a..a7393d3f0 100644 --- a/targets/f18/furi_hal/furi_hal_spi_config.c +++ b/targets/f18/furi_hal/furi_hal_spi_config.c @@ -143,7 +143,7 @@ FuriHalSpiBus furi_hal_spi_bus_d = { /* SPI Bus Handles */ inline static void furi_hal_spi_bus_r_handle_event_callback( - FuriHalSpiBusHandle* handle, + const FuriHalSpiBusHandle* handle, FuriHalSpiBusHandleEvent event, const LL_SPI_InitTypeDef* preset) { if(event == FuriHalSpiBusHandleEventInit) { @@ -189,7 +189,7 @@ inline static void furi_hal_spi_bus_r_handle_event_callback( } inline static void furi_hal_spi_bus_nfc_handle_event_callback( - FuriHalSpiBusHandle* handle, + const FuriHalSpiBusHandle* handle, FuriHalSpiBusHandleEvent event, const LL_SPI_InitTypeDef* preset) { if(event == FuriHalSpiBusHandleEventInit) { @@ -255,12 +255,12 @@ inline static void furi_hal_spi_bus_nfc_handle_event_callback( } static void furi_hal_spi_bus_handle_external_event_callback( - FuriHalSpiBusHandle* handle, + const FuriHalSpiBusHandle* handle, FuriHalSpiBusHandleEvent event) { furi_hal_spi_bus_r_handle_event_callback(handle, event, &furi_hal_spi_preset_1edge_low_2m); } -FuriHalSpiBusHandle furi_hal_spi_bus_handle_external = { +const FuriHalSpiBusHandle furi_hal_spi_bus_handle_external = { .bus = &furi_hal_spi_bus_r, .callback = furi_hal_spi_bus_handle_external_event_callback, .miso = &gpio_ext_pa6, @@ -270,7 +270,7 @@ FuriHalSpiBusHandle furi_hal_spi_bus_handle_external = { }; inline static void furi_hal_spi_bus_d_handle_event_callback( - FuriHalSpiBusHandle* handle, + const FuriHalSpiBusHandle* handle, FuriHalSpiBusHandleEvent event, const LL_SPI_InitTypeDef* preset) { if(event == FuriHalSpiBusHandleEventInit) { @@ -311,12 +311,12 @@ inline static void furi_hal_spi_bus_d_handle_event_callback( } static void furi_hal_spi_bus_handle_display_event_callback( - FuriHalSpiBusHandle* handle, + const FuriHalSpiBusHandle* handle, FuriHalSpiBusHandleEvent event) { furi_hal_spi_bus_d_handle_event_callback(handle, event, &furi_hal_spi_preset_1edge_low_4m); } -FuriHalSpiBusHandle furi_hal_spi_bus_handle_display = { +const FuriHalSpiBusHandle furi_hal_spi_bus_handle_display = { .bus = &furi_hal_spi_bus_d, .callback = furi_hal_spi_bus_handle_display_event_callback, .miso = &gpio_spi_d_miso, @@ -326,12 +326,12 @@ FuriHalSpiBusHandle furi_hal_spi_bus_handle_display = { }; static void furi_hal_spi_bus_handle_sd_fast_event_callback( - FuriHalSpiBusHandle* handle, + const FuriHalSpiBusHandle* handle, FuriHalSpiBusHandleEvent event) { furi_hal_spi_bus_d_handle_event_callback(handle, event, &furi_hal_spi_preset_1edge_low_16m); } -FuriHalSpiBusHandle furi_hal_spi_bus_handle_sd_fast = { +const FuriHalSpiBusHandle furi_hal_spi_bus_handle_sd_fast = { .bus = &furi_hal_spi_bus_d, .callback = furi_hal_spi_bus_handle_sd_fast_event_callback, .miso = &gpio_spi_d_miso, @@ -341,12 +341,12 @@ FuriHalSpiBusHandle furi_hal_spi_bus_handle_sd_fast = { }; static void furi_hal_spi_bus_handle_sd_slow_event_callback( - FuriHalSpiBusHandle* handle, + const FuriHalSpiBusHandle* handle, FuriHalSpiBusHandleEvent event) { furi_hal_spi_bus_d_handle_event_callback(handle, event, &furi_hal_spi_preset_1edge_low_2m); } -FuriHalSpiBusHandle furi_hal_spi_bus_handle_sd_slow = { +const FuriHalSpiBusHandle furi_hal_spi_bus_handle_sd_slow = { .bus = &furi_hal_spi_bus_d, .callback = furi_hal_spi_bus_handle_sd_slow_event_callback, .miso = &gpio_spi_d_miso, diff --git a/targets/f18/furi_hal/furi_hal_spi_config.h b/targets/f18/furi_hal/furi_hal_spi_config.h index da39fbfa6..2ff8b41b1 100644 --- a/targets/f18/furi_hal/furi_hal_spi_config.h +++ b/targets/f18/furi_hal/furi_hal_spi_config.h @@ -39,16 +39,16 @@ extern FuriHalSpiBus furi_hal_spi_bus_d; * Bus pins are floating on inactive state, CS high after initialization * */ -extern FuriHalSpiBusHandle furi_hal_spi_bus_handle_external; +extern const FuriHalSpiBusHandle furi_hal_spi_bus_handle_external; /** ST7567(Display) on `furi_hal_spi_bus_d` */ -extern FuriHalSpiBusHandle furi_hal_spi_bus_handle_display; +extern const FuriHalSpiBusHandle furi_hal_spi_bus_handle_display; /** SdCard in fast mode on `furi_hal_spi_bus_d` */ -extern FuriHalSpiBusHandle furi_hal_spi_bus_handle_sd_fast; +extern const FuriHalSpiBusHandle furi_hal_spi_bus_handle_sd_fast; /** SdCard in slow mode on `furi_hal_spi_bus_d` */ -extern FuriHalSpiBusHandle furi_hal_spi_bus_handle_sd_slow; +extern const FuriHalSpiBusHandle furi_hal_spi_bus_handle_sd_slow; #ifdef __cplusplus } diff --git a/targets/f7/api_symbols.csv b/targets/f7/api_symbols.csv index ba1603b0b..da275594b 100644 --- a/targets/f7/api_symbols.csv +++ b/targets/f7/api_symbols.csv @@ -1,5 +1,5 @@ entry,status,name,type,params -Version,+,82.2,, +Version,+,83.0,, Header,+,applications/drivers/subghz/cc1101_ext/cc1101_ext_interconnect.h,, Header,+,applications/services/bt/bt_service/bt.h,, Header,+,applications/services/bt/bt_service/bt_keys_storage.h,, @@ -865,7 +865,7 @@ Function,+,cli_print_usage,void,"const char*, const char*, const char*" Function,+,cli_read,size_t,"Cli*, uint8_t*, size_t" Function,+,cli_read_timeout,size_t,"Cli*, uint8_t*, size_t, uint32_t" Function,+,cli_session_close,void,Cli* -Function,+,cli_session_open,void,"Cli*, void*" +Function,+,cli_session_open,void,"Cli*, const void*" Function,+,cli_write,void,"Cli*, const uint8_t*, size_t" Function,+,composite_api_resolver_add,void,"CompositeApiResolver*, const ElfApiInterface*" Function,+,composite_api_resolver_alloc,CompositeApiResolver*, @@ -1052,7 +1052,7 @@ Function,+,felica_get_uid,const uint8_t*,"const FelicaData*, size_t*" Function,+,felica_is_equal,_Bool,"const FelicaData*, const FelicaData*" Function,+,felica_load,_Bool,"FelicaData*, FlipperFormat*, uint32_t" Function,+,felica_poller_activate,FelicaError,"FelicaPoller*, FelicaData*" -Function,+,felica_poller_read_blocks,FelicaError,"FelicaPoller*, const uint8_t, const uint8_t*, uint16_t, FelicaPollerReadCommandResponse**" +Function,+,felica_poller_read_blocks,FelicaError,"FelicaPoller*, const uint8_t, const uint8_t* const, uint16_t, FelicaPollerReadCommandResponse** const" Function,+,felica_poller_sync_read,FelicaError,"Nfc*, FelicaData*, const FelicaCardKey*" Function,+,felica_reset,void,FelicaData* Function,+,felica_save,_Bool,"const FelicaData*, FlipperFormat*" @@ -1402,23 +1402,23 @@ Function,+,furi_hal_hid_u2f_get_request,uint32_t,uint8_t* Function,+,furi_hal_hid_u2f_is_connected,_Bool, Function,+,furi_hal_hid_u2f_send_response,void,"uint8_t*, uint8_t" Function,+,furi_hal_hid_u2f_set_callback,void,"HidU2fCallback, void*" -Function,+,furi_hal_i2c_acquire,void,FuriHalI2cBusHandle* +Function,+,furi_hal_i2c_acquire,void,const FuriHalI2cBusHandle* Function,-,furi_hal_i2c_deinit_early,void, Function,-,furi_hal_i2c_init,void, Function,-,furi_hal_i2c_init_early,void, -Function,+,furi_hal_i2c_is_device_ready,_Bool,"FuriHalI2cBusHandle*, uint8_t, uint32_t" -Function,+,furi_hal_i2c_read_mem,_Bool,"FuriHalI2cBusHandle*, uint8_t, uint8_t, uint8_t*, size_t, uint32_t" -Function,+,furi_hal_i2c_read_reg_16,_Bool,"FuriHalI2cBusHandle*, uint8_t, uint8_t, uint16_t*, uint32_t" -Function,+,furi_hal_i2c_read_reg_8,_Bool,"FuriHalI2cBusHandle*, uint8_t, uint8_t, uint8_t*, uint32_t" -Function,+,furi_hal_i2c_release,void,FuriHalI2cBusHandle* -Function,+,furi_hal_i2c_rx,_Bool,"FuriHalI2cBusHandle*, uint8_t, uint8_t*, size_t, uint32_t" -Function,+,furi_hal_i2c_rx_ext,_Bool,"FuriHalI2cBusHandle*, uint16_t, _Bool, uint8_t*, size_t, FuriHalI2cBegin, FuriHalI2cEnd, uint32_t" -Function,+,furi_hal_i2c_trx,_Bool,"FuriHalI2cBusHandle*, uint8_t, const uint8_t*, size_t, uint8_t*, size_t, uint32_t" -Function,+,furi_hal_i2c_tx,_Bool,"FuriHalI2cBusHandle*, uint8_t, const uint8_t*, size_t, uint32_t" -Function,+,furi_hal_i2c_tx_ext,_Bool,"FuriHalI2cBusHandle*, uint16_t, _Bool, const uint8_t*, size_t, FuriHalI2cBegin, FuriHalI2cEnd, uint32_t" -Function,+,furi_hal_i2c_write_mem,_Bool,"FuriHalI2cBusHandle*, uint8_t, uint8_t, const uint8_t*, size_t, uint32_t" -Function,+,furi_hal_i2c_write_reg_16,_Bool,"FuriHalI2cBusHandle*, uint8_t, uint8_t, uint16_t, uint32_t" -Function,+,furi_hal_i2c_write_reg_8,_Bool,"FuriHalI2cBusHandle*, uint8_t, uint8_t, uint8_t, uint32_t" +Function,+,furi_hal_i2c_is_device_ready,_Bool,"const FuriHalI2cBusHandle*, uint8_t, uint32_t" +Function,+,furi_hal_i2c_read_mem,_Bool,"const FuriHalI2cBusHandle*, uint8_t, uint8_t, uint8_t*, size_t, uint32_t" +Function,+,furi_hal_i2c_read_reg_16,_Bool,"const FuriHalI2cBusHandle*, uint8_t, uint8_t, uint16_t*, uint32_t" +Function,+,furi_hal_i2c_read_reg_8,_Bool,"const FuriHalI2cBusHandle*, uint8_t, uint8_t, uint8_t*, uint32_t" +Function,+,furi_hal_i2c_release,void,const FuriHalI2cBusHandle* +Function,+,furi_hal_i2c_rx,_Bool,"const FuriHalI2cBusHandle*, uint8_t, uint8_t*, size_t, uint32_t" +Function,+,furi_hal_i2c_rx_ext,_Bool,"const FuriHalI2cBusHandle*, uint16_t, _Bool, uint8_t*, size_t, FuriHalI2cBegin, FuriHalI2cEnd, uint32_t" +Function,+,furi_hal_i2c_trx,_Bool,"const FuriHalI2cBusHandle*, uint8_t, const uint8_t*, size_t, uint8_t*, size_t, uint32_t" +Function,+,furi_hal_i2c_tx,_Bool,"const FuriHalI2cBusHandle*, uint8_t, const uint8_t*, size_t, uint32_t" +Function,+,furi_hal_i2c_tx_ext,_Bool,"const FuriHalI2cBusHandle*, uint16_t, _Bool, const uint8_t*, size_t, FuriHalI2cBegin, FuriHalI2cEnd, uint32_t" +Function,+,furi_hal_i2c_write_mem,_Bool,"const FuriHalI2cBusHandle*, uint8_t, uint8_t, const uint8_t*, size_t, uint32_t" +Function,+,furi_hal_i2c_write_reg_16,_Bool,"const FuriHalI2cBusHandle*, uint8_t, uint8_t, uint16_t, uint32_t" +Function,+,furi_hal_i2c_write_reg_8,_Bool,"const FuriHalI2cBusHandle*, uint8_t, uint8_t, uint8_t, uint32_t" Function,+,furi_hal_ibutton_emulate_set_next,void,uint32_t Function,+,furi_hal_ibutton_emulate_start,void,"uint32_t, FuriHalIbuttonEmulateCallback, void*" Function,+,furi_hal_ibutton_emulate_stop,void, @@ -1663,20 +1663,20 @@ Function,+,furi_hal_speaker_release,void, Function,+,furi_hal_speaker_set_volume,void,float Function,+,furi_hal_speaker_start,void,"float, float" Function,+,furi_hal_speaker_stop,void, -Function,+,furi_hal_spi_acquire,void,FuriHalSpiBusHandle* +Function,+,furi_hal_spi_acquire,void,const FuriHalSpiBusHandle* Function,+,furi_hal_spi_bus_deinit,void,FuriHalSpiBus* -Function,+,furi_hal_spi_bus_handle_deinit,void,FuriHalSpiBusHandle* -Function,+,furi_hal_spi_bus_handle_init,void,FuriHalSpiBusHandle* +Function,+,furi_hal_spi_bus_handle_deinit,void,const FuriHalSpiBusHandle* +Function,+,furi_hal_spi_bus_handle_init,void,const FuriHalSpiBusHandle* Function,+,furi_hal_spi_bus_init,void,FuriHalSpiBus* -Function,+,furi_hal_spi_bus_rx,_Bool,"FuriHalSpiBusHandle*, uint8_t*, size_t, uint32_t" -Function,+,furi_hal_spi_bus_trx,_Bool,"FuriHalSpiBusHandle*, const uint8_t*, uint8_t*, size_t, uint32_t" -Function,+,furi_hal_spi_bus_trx_dma,_Bool,"FuriHalSpiBusHandle*, uint8_t*, uint8_t*, size_t, uint32_t" -Function,+,furi_hal_spi_bus_tx,_Bool,"FuriHalSpiBusHandle*, const uint8_t*, size_t, uint32_t" +Function,+,furi_hal_spi_bus_rx,_Bool,"const FuriHalSpiBusHandle*, uint8_t*, size_t, uint32_t" +Function,+,furi_hal_spi_bus_trx,_Bool,"const FuriHalSpiBusHandle*, const uint8_t*, uint8_t*, size_t, uint32_t" +Function,+,furi_hal_spi_bus_trx_dma,_Bool,"const FuriHalSpiBusHandle*, uint8_t*, uint8_t*, size_t, uint32_t" +Function,+,furi_hal_spi_bus_tx,_Bool,"const FuriHalSpiBusHandle*, const uint8_t*, size_t, uint32_t" Function,-,furi_hal_spi_config_deinit_early,void, Function,-,furi_hal_spi_config_init,void, Function,-,furi_hal_spi_config_init_early,void, Function,-,furi_hal_spi_dma_init,void, -Function,+,furi_hal_spi_release,void,FuriHalSpiBusHandle* +Function,+,furi_hal_spi_release,void,const FuriHalSpiBusHandle* Function,-,furi_hal_subghz_dump_state,void, Function,+,furi_hal_subghz_flush_rx,void, Function,+,furi_hal_subghz_flush_tx,void, @@ -1719,7 +1719,7 @@ Function,+,furi_hal_usb_is_locked,_Bool, Function,+,furi_hal_usb_lock,void, Function,+,furi_hal_usb_reinit,void, Function,+,furi_hal_usb_set_config,_Bool,"FuriHalUsbInterface*, void*" -Function,-,furi_hal_usb_set_state_callback,void,"FuriHalUsbStateCallback, void*" +Function,+,furi_hal_usb_set_state_callback,void,"FuriHalUsbStateCallback, void*" Function,+,furi_hal_usb_unlock,void, Function,+,furi_hal_version_do_i_belong_here,_Bool, Function,+,furi_hal_version_get_ble_local_device_name_ptr,const char*, @@ -1957,7 +1957,7 @@ Function,-,getchar,int, Function,-,getchar_unlocked,int, Function,-,getenv,char*,const char* Function,-,gets,char*,char* -Function,-,getsubopt,int,"char**, char**, char**" +Function,-,getsubopt,int,"char**, char* const*, char**" Function,-,getw,int,FILE* Function,+,gui_add_framebuffer_callback,void,"Gui*, GuiCanvasCommitCallback, void*" Function,+,gui_add_view_port,void,"Gui*, ViewPort*, GuiLayer" @@ -2659,7 +2659,7 @@ Function,+,mf_ultralight_3des_decrypt,void,"mbedtls_des3_context*, const uint8_t Function,+,mf_ultralight_3des_encrypt,void,"mbedtls_des3_context*, const uint8_t*, const uint8_t*, const uint8_t*, const uint8_t, uint8_t*" Function,+,mf_ultralight_3des_get_key,const uint8_t*,const MfUltralightData* Function,+,mf_ultralight_3des_key_valid,_Bool,const MfUltralightData* -Function,+,mf_ultralight_3des_shift_data,void,uint8_t* +Function,+,mf_ultralight_3des_shift_data,void,uint8_t* const Function,+,mf_ultralight_alloc,MfUltralightData*, Function,+,mf_ultralight_copy,void,"MfUltralightData*, const MfUltralightData*" Function,+,mf_ultralight_detect_protocol,_Bool,const Iso14443_3aData* @@ -3014,7 +3014,7 @@ Function,-,powl,long double,"long double, long double" Function,+,pretty_format_bytes_hex_canonical,void,"FuriString*, size_t, const char*, const uint8_t*, size_t" Function,-,printf,int,"const char*, ..." Function,+,property_value_out,void,"PropertyValueContext*, const char*, unsigned int, ..." -Function,+,protocol_dict_alloc,ProtocolDict*,"const ProtocolBase**, size_t" +Function,+,protocol_dict_alloc,ProtocolDict*,"const ProtocolBase* const*, size_t" Function,+,protocol_dict_decoders_feed,ProtocolId,"ProtocolDict*, _Bool, uint32_t" Function,+,protocol_dict_decoders_feed_by_feature,ProtocolId,"ProtocolDict*, uint32_t, _Bool, uint32_t" Function,+,protocol_dict_decoders_feed_by_id,ProtocolId,"ProtocolDict*, size_t, _Bool, uint32_t" @@ -3202,29 +3202,29 @@ Function,+,srand,void,unsigned Function,-,srand48,void,long Function,-,srandom,void,unsigned Function,+,sscanf,int,"const char*, const char*, ..." -Function,+,st25r3916_change_reg_bits,void,"FuriHalSpiBusHandle*, uint8_t, uint8_t, uint8_t" -Function,+,st25r3916_change_test_reg_bits,void,"FuriHalSpiBusHandle*, uint8_t, uint8_t, uint8_t" -Function,+,st25r3916_check_reg,_Bool,"FuriHalSpiBusHandle*, uint8_t, uint8_t, uint8_t" -Function,+,st25r3916_clear_reg_bits,void,"FuriHalSpiBusHandle*, uint8_t, uint8_t" -Function,+,st25r3916_direct_cmd,void,"FuriHalSpiBusHandle*, uint8_t" -Function,+,st25r3916_get_irq,uint32_t,FuriHalSpiBusHandle* -Function,+,st25r3916_mask_irq,void,"FuriHalSpiBusHandle*, uint32_t" -Function,+,st25r3916_modify_reg,void,"FuriHalSpiBusHandle*, uint8_t, uint8_t, uint8_t" -Function,+,st25r3916_read_burst_regs,void,"FuriHalSpiBusHandle*, uint8_t, uint8_t*, uint8_t" -Function,+,st25r3916_read_fifo,_Bool,"FuriHalSpiBusHandle*, uint8_t*, size_t, size_t*" -Function,+,st25r3916_read_pta_mem,void,"FuriHalSpiBusHandle*, uint8_t*, size_t" -Function,+,st25r3916_read_reg,void,"FuriHalSpiBusHandle*, uint8_t, uint8_t*" -Function,+,st25r3916_read_test_reg,void,"FuriHalSpiBusHandle*, uint8_t, uint8_t*" -Function,+,st25r3916_reg_read_fifo,void,"FuriHalSpiBusHandle*, uint8_t*, size_t" -Function,+,st25r3916_reg_write_fifo,void,"FuriHalSpiBusHandle*, const uint8_t*, size_t" -Function,+,st25r3916_set_reg_bits,void,"FuriHalSpiBusHandle*, uint8_t, uint8_t" -Function,+,st25r3916_write_burst_regs,void,"FuriHalSpiBusHandle*, uint8_t, const uint8_t*, uint8_t" -Function,+,st25r3916_write_fifo,void,"FuriHalSpiBusHandle*, const uint8_t*, size_t" -Function,+,st25r3916_write_pta_mem,void,"FuriHalSpiBusHandle*, const uint8_t*, size_t" -Function,+,st25r3916_write_ptf_mem,void,"FuriHalSpiBusHandle*, const uint8_t*, size_t" -Function,+,st25r3916_write_pttsn_mem,void,"FuriHalSpiBusHandle*, uint8_t*, size_t" -Function,+,st25r3916_write_reg,void,"FuriHalSpiBusHandle*, uint8_t, uint8_t" -Function,+,st25r3916_write_test_reg,void,"FuriHalSpiBusHandle*, uint8_t, uint8_t" +Function,+,st25r3916_change_reg_bits,void,"const FuriHalSpiBusHandle*, uint8_t, uint8_t, uint8_t" +Function,+,st25r3916_change_test_reg_bits,void,"const FuriHalSpiBusHandle*, uint8_t, uint8_t, uint8_t" +Function,+,st25r3916_check_reg,_Bool,"const FuriHalSpiBusHandle*, uint8_t, uint8_t, uint8_t" +Function,+,st25r3916_clear_reg_bits,void,"const FuriHalSpiBusHandle*, uint8_t, uint8_t" +Function,+,st25r3916_direct_cmd,void,"const FuriHalSpiBusHandle*, uint8_t" +Function,+,st25r3916_get_irq,uint32_t,const FuriHalSpiBusHandle* +Function,+,st25r3916_mask_irq,void,"const FuriHalSpiBusHandle*, uint32_t" +Function,+,st25r3916_modify_reg,void,"const FuriHalSpiBusHandle*, uint8_t, uint8_t, uint8_t" +Function,+,st25r3916_read_burst_regs,void,"const FuriHalSpiBusHandle*, uint8_t, uint8_t*, uint8_t" +Function,+,st25r3916_read_fifo,_Bool,"const FuriHalSpiBusHandle*, uint8_t*, size_t, size_t*" +Function,+,st25r3916_read_pta_mem,void,"const FuriHalSpiBusHandle*, uint8_t*, size_t" +Function,+,st25r3916_read_reg,void,"const FuriHalSpiBusHandle*, uint8_t, uint8_t*" +Function,+,st25r3916_read_test_reg,void,"const FuriHalSpiBusHandle*, uint8_t, uint8_t*" +Function,+,st25r3916_reg_read_fifo,void,"const FuriHalSpiBusHandle*, uint8_t*, size_t" +Function,+,st25r3916_reg_write_fifo,void,"const FuriHalSpiBusHandle*, const uint8_t*, size_t" +Function,+,st25r3916_set_reg_bits,void,"const FuriHalSpiBusHandle*, uint8_t, uint8_t" +Function,+,st25r3916_write_burst_regs,void,"const FuriHalSpiBusHandle*, uint8_t, const uint8_t*, uint8_t" +Function,+,st25r3916_write_fifo,void,"const FuriHalSpiBusHandle*, const uint8_t*, size_t" +Function,+,st25r3916_write_pta_mem,void,"const FuriHalSpiBusHandle*, const uint8_t*, size_t" +Function,+,st25r3916_write_ptf_mem,void,"const FuriHalSpiBusHandle*, const uint8_t*, size_t" +Function,+,st25r3916_write_pttsn_mem,void,"const FuriHalSpiBusHandle*, uint8_t*, size_t" +Function,+,st25r3916_write_reg,void,"const FuriHalSpiBusHandle*, uint8_t, uint8_t" +Function,+,st25r3916_write_test_reg,void,"const FuriHalSpiBusHandle*, uint8_t, uint8_t" Function,+,st25tb_alloc,St25tbData*, Function,+,st25tb_copy,void,"St25tbData*, const St25tbData*" Function,+,st25tb_free,void,St25tbData* @@ -3787,24 +3787,24 @@ Variable,-,__stdio_exit_handler,void (*)(), Variable,+,_ctype_,const char[], Variable,+,_impure_data,_reent, Variable,+,_impure_ptr,_reent*, -Variable,-,_sys_errlist,const char*[], +Variable,-,_sys_errlist,const char* const[], Variable,-,_sys_nerr,int, Variable,-,ble_profile_hid,const FuriHalBleProfileTemplate*, -Variable,-,ble_profile_serial,const FuriHalBleProfileTemplate*, -Variable,+,cli_vcp,CliSession, +Variable,+,ble_profile_serial,const FuriHalBleProfileTemplate* const, +Variable,+,cli_vcp,const CliSession, Variable,+,compress_config_heatshrink_default,const CompressConfigHeatshrink, -Variable,+,firmware_api_interface,const ElfApiInterface*, +Variable,+,firmware_api_interface,const ElfApiInterface* const, Variable,+,furi_hal_i2c_bus_external,FuriHalI2cBus, Variable,+,furi_hal_i2c_bus_power,FuriHalI2cBus, -Variable,+,furi_hal_i2c_handle_external,FuriHalI2cBusHandle, -Variable,+,furi_hal_i2c_handle_power,FuriHalI2cBusHandle, +Variable,+,furi_hal_i2c_handle_external,const FuriHalI2cBusHandle, +Variable,+,furi_hal_i2c_handle_power,const FuriHalI2cBusHandle, Variable,+,furi_hal_spi_bus_d,FuriHalSpiBus, -Variable,+,furi_hal_spi_bus_handle_display,FuriHalSpiBusHandle, -Variable,+,furi_hal_spi_bus_handle_external,FuriHalSpiBusHandle, -Variable,+,furi_hal_spi_bus_handle_nfc,FuriHalSpiBusHandle, -Variable,+,furi_hal_spi_bus_handle_sd_fast,FuriHalSpiBusHandle, -Variable,+,furi_hal_spi_bus_handle_sd_slow,FuriHalSpiBusHandle, -Variable,+,furi_hal_spi_bus_handle_subghz,FuriHalSpiBusHandle, +Variable,+,furi_hal_spi_bus_handle_display,const FuriHalSpiBusHandle, +Variable,+,furi_hal_spi_bus_handle_external,const FuriHalSpiBusHandle, +Variable,+,furi_hal_spi_bus_handle_nfc,const FuriHalSpiBusHandle, +Variable,+,furi_hal_spi_bus_handle_sd_fast,const FuriHalSpiBusHandle, +Variable,+,furi_hal_spi_bus_handle_sd_slow,const FuriHalSpiBusHandle, +Variable,+,furi_hal_spi_bus_handle_subghz,const FuriHalSpiBusHandle, Variable,+,furi_hal_spi_bus_r,FuriHalSpiBus, Variable,+,furi_hal_spi_preset_1edge_low_16m,const LL_SPI_InitTypeDef, Variable,+,furi_hal_spi_preset_1edge_low_2m,const LL_SPI_InitTypeDef, @@ -3862,7 +3862,7 @@ Variable,+,gpio_usb_dp,const GpioPin, Variable,+,gpio_vibro,const GpioPin, Variable,+,input_pins,const InputPin[], Variable,+,input_pins_count,const size_t, -Variable,+,lfrfid_protocols,const ProtocolBase*[], +Variable,+,lfrfid_protocols,const ProtocolBase* const[], Variable,+,message_blink_set_color_blue,const NotificationMessage, Variable,+,message_blink_set_color_cyan,const NotificationMessage, Variable,+,message_blink_set_color_green,const NotificationMessage, diff --git a/targets/f7/ble_glue/profiles/serial_profile.c b/targets/f7/ble_glue/profiles/serial_profile.c index 1d414889f..427539427 100644 --- a/targets/f7/ble_glue/profiles/serial_profile.c +++ b/targets/f7/ble_glue/profiles/serial_profile.c @@ -46,7 +46,7 @@ static void ble_profile_serial_stop(FuriHalBleProfileBase* profile) { // Up to 45 ms #define CONNECTION_INTERVAL_MAX (0x24) -static GapConfig serial_template_config = { +static const GapConfig serial_template_config = { .adv_service_uuid = 0x3080, .appearance_char = 0x8600, .bonding_mode = true, @@ -80,7 +80,7 @@ static const FuriHalBleProfileTemplate profile_callbacks = { .get_gap_config = ble_profile_serial_get_config, }; -const FuriHalBleProfileTemplate* ble_profile_serial = &profile_callbacks; +const FuriHalBleProfileTemplate* const ble_profile_serial = &profile_callbacks; void ble_profile_serial_set_event_callback( FuriHalBleProfileBase* profile, diff --git a/targets/f7/ble_glue/profiles/serial_profile.h b/targets/f7/ble_glue/profiles/serial_profile.h index e07eaef03..7938bfc33 100644 --- a/targets/f7/ble_glue/profiles/serial_profile.h +++ b/targets/f7/ble_glue/profiles/serial_profile.h @@ -19,7 +19,7 @@ typedef enum { typedef SerialServiceEventCallback FuriHalBtSerialCallback; /** Serial profile descriptor */ -extern const FuriHalBleProfileTemplate* ble_profile_serial; +extern const FuriHalBleProfileTemplate* const ble_profile_serial; /** Send data through BLE * diff --git a/targets/f7/fatfs/user_diskio.c b/targets/f7/fatfs/user_diskio.c index 85e5cad4f..963cb595f 100644 --- a/targets/f7/fatfs/user_diskio.c +++ b/targets/f7/fatfs/user_diskio.c @@ -9,7 +9,7 @@ static DRESULT driver_read(BYTE pdrv, BYTE* buff, DWORD sector, UINT count); static DRESULT driver_write(BYTE pdrv, const BYTE* buff, DWORD sector, UINT count); static DRESULT driver_ioctl(BYTE pdrv, BYTE cmd, void* buff); -Diskio_drvTypeDef sd_fatfs_driver = { +const Diskio_drvTypeDef sd_fatfs_driver = { driver_initialize, driver_status, driver_read, diff --git a/targets/f7/fatfs/user_diskio.h b/targets/f7/fatfs/user_diskio.h index 009a17d4b..2505de704 100644 --- a/targets/f7/fatfs/user_diskio.h +++ b/targets/f7/fatfs/user_diskio.h @@ -6,7 +6,7 @@ extern "C" { #include "fatfs/ff_gen_drv.h" -extern Diskio_drvTypeDef sd_fatfs_driver; +extern const Diskio_drvTypeDef sd_fatfs_driver; #ifdef __cplusplus } diff --git a/targets/f7/furi_hal/furi_hal_i2c.c b/targets/f7/furi_hal/furi_hal_i2c.c index 71e1a5814..7eb9b4928 100644 --- a/targets/f7/furi_hal/furi_hal_i2c.c +++ b/targets/f7/furi_hal/furi_hal_i2c.c @@ -22,7 +22,7 @@ void furi_hal_i2c_init(void) { FURI_LOG_I(TAG, "Init OK"); } -void furi_hal_i2c_acquire(FuriHalI2cBusHandle* handle) { +void furi_hal_i2c_acquire(const FuriHalI2cBusHandle* handle) { furi_hal_power_insomnia_enter(); // Lock bus access handle->bus->callback(handle->bus, FuriHalI2cBusEventLock); @@ -36,7 +36,7 @@ void furi_hal_i2c_acquire(FuriHalI2cBusHandle* handle) { handle->callback(handle, FuriHalI2cBusHandleEventActivate); } -void furi_hal_i2c_release(FuriHalI2cBusHandle* handle) { +void furi_hal_i2c_release(const FuriHalI2cBusHandle* handle) { // Ensure that current handle is our handle furi_check(handle->bus->current_handle == handle); // Deactivate handle @@ -196,7 +196,7 @@ static bool furi_hal_i2c_transaction( } bool furi_hal_i2c_rx_ext( - FuriHalI2cBusHandle* handle, + const FuriHalI2cBusHandle* handle, uint16_t address, bool ten_bit, uint8_t* data, @@ -213,7 +213,7 @@ bool furi_hal_i2c_rx_ext( } bool furi_hal_i2c_tx_ext( - FuriHalI2cBusHandle* handle, + const FuriHalI2cBusHandle* handle, uint16_t address, bool ten_bit, const uint8_t* data, @@ -230,7 +230,7 @@ bool furi_hal_i2c_tx_ext( } bool furi_hal_i2c_tx( - FuriHalI2cBusHandle* handle, + const FuriHalI2cBusHandle* handle, uint8_t address, const uint8_t* data, size_t size, @@ -242,7 +242,7 @@ bool furi_hal_i2c_tx( } bool furi_hal_i2c_rx( - FuriHalI2cBusHandle* handle, + const FuriHalI2cBusHandle* handle, uint8_t address, uint8_t* data, size_t size, @@ -254,7 +254,7 @@ bool furi_hal_i2c_rx( } bool furi_hal_i2c_trx( - FuriHalI2cBusHandle* handle, + const FuriHalI2cBusHandle* handle, uint8_t address, const uint8_t* tx_data, size_t tx_size, @@ -281,7 +281,10 @@ bool furi_hal_i2c_trx( timeout); } -bool furi_hal_i2c_is_device_ready(FuriHalI2cBusHandle* handle, uint8_t i2c_addr, uint32_t timeout) { +bool furi_hal_i2c_is_device_ready( + const FuriHalI2cBusHandle* handle, + uint8_t i2c_addr, + uint32_t timeout) { furi_check(handle); furi_check(handle->bus->current_handle == handle); furi_check(timeout > 0); @@ -314,7 +317,7 @@ bool furi_hal_i2c_is_device_ready(FuriHalI2cBusHandle* handle, uint8_t i2c_addr, } bool furi_hal_i2c_read_reg_8( - FuriHalI2cBusHandle* handle, + const FuriHalI2cBusHandle* handle, uint8_t i2c_addr, uint8_t reg_addr, uint8_t* data, @@ -325,7 +328,7 @@ bool furi_hal_i2c_read_reg_8( } bool furi_hal_i2c_read_reg_16( - FuriHalI2cBusHandle* handle, + const FuriHalI2cBusHandle* handle, uint8_t i2c_addr, uint8_t reg_addr, uint16_t* data, @@ -340,7 +343,7 @@ bool furi_hal_i2c_read_reg_16( } bool furi_hal_i2c_read_mem( - FuriHalI2cBusHandle* handle, + const FuriHalI2cBusHandle* handle, uint8_t i2c_addr, uint8_t mem_addr, uint8_t* data, @@ -352,7 +355,7 @@ bool furi_hal_i2c_read_mem( } bool furi_hal_i2c_write_reg_8( - FuriHalI2cBusHandle* handle, + const FuriHalI2cBusHandle* handle, uint8_t i2c_addr, uint8_t reg_addr, uint8_t data, @@ -368,7 +371,7 @@ bool furi_hal_i2c_write_reg_8( } bool furi_hal_i2c_write_reg_16( - FuriHalI2cBusHandle* handle, + const FuriHalI2cBusHandle* handle, uint8_t i2c_addr, uint8_t reg_addr, uint16_t data, @@ -385,7 +388,7 @@ bool furi_hal_i2c_write_reg_16( } bool furi_hal_i2c_write_mem( - FuriHalI2cBusHandle* handle, + const FuriHalI2cBusHandle* handle, uint8_t i2c_addr, uint8_t mem_addr, const uint8_t* data, diff --git a/targets/f7/furi_hal/furi_hal_i2c_config.c b/targets/f7/furi_hal/furi_hal_i2c_config.c index f9d88abb3..b10c53d32 100644 --- a/targets/f7/furi_hal/furi_hal_i2c_config.c +++ b/targets/f7/furi_hal/furi_hal_i2c_config.c @@ -74,7 +74,7 @@ FuriHalI2cBus furi_hal_i2c_bus_external = { }; void furi_hal_i2c_bus_handle_power_event( - FuriHalI2cBusHandle* handle, + const FuriHalI2cBusHandle* handle, FuriHalI2cBusHandleEvent event) { if(event == FuriHalI2cBusHandleEventActivate) { furi_hal_gpio_init_ex( @@ -120,13 +120,13 @@ void furi_hal_i2c_bus_handle_power_event( } } -FuriHalI2cBusHandle furi_hal_i2c_handle_power = { +const FuriHalI2cBusHandle furi_hal_i2c_handle_power = { .bus = &furi_hal_i2c_bus_power, .callback = furi_hal_i2c_bus_handle_power_event, }; void furi_hal_i2c_bus_handle_external_event( - FuriHalI2cBusHandle* handle, + const FuriHalI2cBusHandle* handle, FuriHalI2cBusHandleEvent event) { if(event == FuriHalI2cBusHandleEventActivate) { furi_hal_gpio_init_ex( @@ -160,7 +160,7 @@ void furi_hal_i2c_bus_handle_external_event( } } -FuriHalI2cBusHandle furi_hal_i2c_handle_external = { +const FuriHalI2cBusHandle furi_hal_i2c_handle_external = { .bus = &furi_hal_i2c_bus_external, .callback = furi_hal_i2c_bus_handle_external_event, }; diff --git a/targets/f7/furi_hal/furi_hal_i2c_config.h b/targets/f7/furi_hal/furi_hal_i2c_config.h index a8fb91835..28bd09a0a 100644 --- a/targets/f7/furi_hal/furi_hal_i2c_config.h +++ b/targets/f7/furi_hal/furi_hal_i2c_config.h @@ -17,14 +17,14 @@ extern FuriHalI2cBus furi_hal_i2c_bus_external; * Pins: PA9(SCL) / PA10(SDA), float on release * Params: 400khz */ -extern FuriHalI2cBusHandle furi_hal_i2c_handle_power; +extern const FuriHalI2cBusHandle furi_hal_i2c_handle_power; /** Handle for external i2c bus * Bus: furi_hal_i2c_bus_external * Pins: PC0(SCL) / PC1(SDA), float on release * Params: 100khz */ -extern FuriHalI2cBusHandle furi_hal_i2c_handle_external; +extern const FuriHalI2cBusHandle furi_hal_i2c_handle_external; #ifdef __cplusplus } diff --git a/targets/f7/furi_hal/furi_hal_i2c_types.h b/targets/f7/furi_hal/furi_hal_i2c_types.h index 13f361054..0a35137b0 100644 --- a/targets/f7/furi_hal/furi_hal_i2c_types.h +++ b/targets/f7/furi_hal/furi_hal_i2c_types.h @@ -25,7 +25,7 @@ typedef void (*FuriHalI2cBusEventCallback)(FuriHalI2cBus* bus, FuriHalI2cBusEven /** FuriHal i2c bus */ struct FuriHalI2cBus { I2C_TypeDef* i2c; - FuriHalI2cBusHandle* current_handle; + const FuriHalI2cBusHandle* current_handle; FuriHalI2cBusEventCallback callback; }; @@ -37,7 +37,7 @@ typedef enum { /** FuriHal i2c handle event callback */ typedef void (*FuriHalI2cBusHandleEventCallback)( - FuriHalI2cBusHandle* handle, + const FuriHalI2cBusHandle* handle, FuriHalI2cBusHandleEvent event); /** FuriHal i2c handle */ diff --git a/targets/f7/furi_hal/furi_hal_nfc.c b/targets/f7/furi_hal/furi_hal_nfc.c index ee7a04e45..d8dc0c618 100644 --- a/targets/f7/furi_hal/furi_hal_nfc.c +++ b/targets/f7/furi_hal/furi_hal_nfc.c @@ -8,7 +8,7 @@ #define TAG "FuriHalNfc" -const FuriHalNfcTechBase* furi_hal_nfc_tech[FuriHalNfcTechNum] = { +const FuriHalNfcTechBase* const furi_hal_nfc_tech[FuriHalNfcTechNum] = { [FuriHalNfcTechIso14443a] = &furi_hal_nfc_iso14443a, [FuriHalNfcTechIso14443b] = &furi_hal_nfc_iso14443b, [FuriHalNfcTechIso15693] = &furi_hal_nfc_iso15693, @@ -18,7 +18,7 @@ const FuriHalNfcTechBase* furi_hal_nfc_tech[FuriHalNfcTechNum] = { FuriHalNfc furi_hal_nfc; -static FuriHalNfcError furi_hal_nfc_turn_on_osc(FuriHalSpiBusHandle* handle) { +static FuriHalNfcError furi_hal_nfc_turn_on_osc(const FuriHalSpiBusHandle* handle) { FuriHalNfcError error = FuriHalNfcErrorNone; furi_hal_nfc_event_start(); @@ -53,7 +53,7 @@ FuriHalNfcError furi_hal_nfc_is_hal_ready(void) { error = furi_hal_nfc_acquire(); if(error != FuriHalNfcErrorNone) break; - FuriHalSpiBusHandle* handle = &furi_hal_spi_bus_handle_nfc; + const FuriHalSpiBusHandle* handle = &furi_hal_spi_bus_handle_nfc; uint8_t chip_id = 0; st25r3916_read_reg(handle, ST25R3916_REG_IC_IDENTITY, &chip_id); if((chip_id & ST25R3916_REG_IC_IDENTITY_ic_type_mask) != @@ -83,7 +83,7 @@ FuriHalNfcError furi_hal_nfc_init(void) { furi_hal_nfc_low_power_mode_start(); } - FuriHalSpiBusHandle* handle = &furi_hal_spi_bus_handle_nfc; + const FuriHalSpiBusHandle* handle = &furi_hal_spi_bus_handle_nfc; // Set default state st25r3916_direct_cmd(handle, ST25R3916_CMD_SET_DEFAULT); // Increase IO driver strength of MISO and IRQ @@ -282,7 +282,7 @@ FuriHalNfcError furi_hal_nfc_release(void) { FuriHalNfcError furi_hal_nfc_low_power_mode_start(void) { FuriHalNfcError error = FuriHalNfcErrorNone; - FuriHalSpiBusHandle* handle = &furi_hal_spi_bus_handle_nfc; + const FuriHalSpiBusHandle* handle = &furi_hal_spi_bus_handle_nfc; st25r3916_direct_cmd(handle, ST25R3916_CMD_STOP); st25r3916_clear_reg_bits( @@ -300,7 +300,7 @@ FuriHalNfcError furi_hal_nfc_low_power_mode_start(void) { FuriHalNfcError furi_hal_nfc_low_power_mode_stop(void) { FuriHalNfcError error = FuriHalNfcErrorNone; - FuriHalSpiBusHandle* handle = &furi_hal_spi_bus_handle_nfc; + const FuriHalSpiBusHandle* handle = &furi_hal_spi_bus_handle_nfc; do { furi_hal_nfc_init_gpio_isr(); @@ -318,7 +318,7 @@ FuriHalNfcError furi_hal_nfc_low_power_mode_stop(void) { return error; } -static FuriHalNfcError furi_hal_nfc_poller_init_common(FuriHalSpiBusHandle* handle) { +static FuriHalNfcError furi_hal_nfc_poller_init_common(const FuriHalSpiBusHandle* handle) { // Disable wake up st25r3916_clear_reg_bits(handle, ST25R3916_REG_OP_CONTROL, ST25R3916_REG_OP_CONTROL_wu); // Enable correlator @@ -339,7 +339,7 @@ static FuriHalNfcError furi_hal_nfc_poller_init_common(FuriHalSpiBusHandle* hand return FuriHalNfcErrorNone; } -static FuriHalNfcError furi_hal_nfc_listener_init_common(FuriHalSpiBusHandle* handle) { +static FuriHalNfcError furi_hal_nfc_listener_init_common(const FuriHalSpiBusHandle* handle) { UNUSED(handle); // No common listener configuration return FuriHalNfcErrorNone; @@ -349,7 +349,7 @@ FuriHalNfcError furi_hal_nfc_set_mode(FuriHalNfcMode mode, FuriHalNfcTech tech) furi_check(mode < FuriHalNfcModeNum); furi_check(tech < FuriHalNfcTechNum); - FuriHalSpiBusHandle* handle = &furi_hal_spi_bus_handle_nfc; + const FuriHalSpiBusHandle* handle = &furi_hal_spi_bus_handle_nfc; FuriHalNfcError error = FuriHalNfcErrorNone; @@ -375,7 +375,7 @@ FuriHalNfcError furi_hal_nfc_set_mode(FuriHalNfcMode mode, FuriHalNfcTech tech) FuriHalNfcError furi_hal_nfc_reset_mode(void) { FuriHalNfcError error = FuriHalNfcErrorNone; - FuriHalSpiBusHandle* handle = &furi_hal_spi_bus_handle_nfc; + const FuriHalSpiBusHandle* handle = &furi_hal_spi_bus_handle_nfc; st25r3916_direct_cmd(handle, ST25R3916_CMD_STOP); @@ -415,7 +415,7 @@ FuriHalNfcError furi_hal_nfc_reset_mode(void) { FuriHalNfcError furi_hal_nfc_field_detect_start(void) { FuriHalNfcError error = FuriHalNfcErrorNone; - FuriHalSpiBusHandle* handle = &furi_hal_spi_bus_handle_nfc; + const FuriHalSpiBusHandle* handle = &furi_hal_spi_bus_handle_nfc; st25r3916_write_reg( handle, @@ -429,7 +429,7 @@ FuriHalNfcError furi_hal_nfc_field_detect_start(void) { FuriHalNfcError furi_hal_nfc_field_detect_stop(void) { FuriHalNfcError error = FuriHalNfcErrorNone; - FuriHalSpiBusHandle* handle = &furi_hal_spi_bus_handle_nfc; + const FuriHalSpiBusHandle* handle = &furi_hal_spi_bus_handle_nfc; st25r3916_clear_reg_bits( handle, @@ -441,7 +441,7 @@ FuriHalNfcError furi_hal_nfc_field_detect_stop(void) { bool furi_hal_nfc_field_is_present(void) { bool is_present = false; - FuriHalSpiBusHandle* handle = &furi_hal_spi_bus_handle_nfc; + const FuriHalSpiBusHandle* handle = &furi_hal_spi_bus_handle_nfc; if(st25r3916_check_reg( handle, @@ -456,7 +456,7 @@ bool furi_hal_nfc_field_is_present(void) { FuriHalNfcError furi_hal_nfc_poller_field_on(void) { FuriHalNfcError error = FuriHalNfcErrorNone; - FuriHalSpiBusHandle* handle = &furi_hal_spi_bus_handle_nfc; + const FuriHalSpiBusHandle* handle = &furi_hal_spi_bus_handle_nfc; if(!st25r3916_check_reg( handle, @@ -476,7 +476,7 @@ FuriHalNfcError furi_hal_nfc_poller_field_on(void) { } FuriHalNfcError furi_hal_nfc_poller_tx_common( - FuriHalSpiBusHandle* handle, + const FuriHalSpiBusHandle* handle, const uint8_t* tx_data, size_t tx_bits) { furi_check(tx_data); @@ -508,7 +508,7 @@ FuriHalNfcError furi_hal_nfc_poller_tx_common( } FuriHalNfcError furi_hal_nfc_common_fifo_tx( - FuriHalSpiBusHandle* handle, + const FuriHalSpiBusHandle* handle, const uint8_t* tx_data, size_t tx_bits) { FuriHalNfcError err = FuriHalNfcErrorNone; @@ -523,7 +523,7 @@ FuriHalNfcError furi_hal_nfc_common_fifo_tx( FuriHalNfcError furi_hal_nfc_poller_tx(const uint8_t* tx_data, size_t tx_bits) { furi_check(furi_hal_nfc.mode == FuriHalNfcModePoller); furi_check(furi_hal_nfc.tech < FuriHalNfcTechNum); - FuriHalSpiBusHandle* handle = &furi_hal_spi_bus_handle_nfc; + const FuriHalSpiBusHandle* handle = &furi_hal_spi_bus_handle_nfc; return furi_hal_nfc_tech[furi_hal_nfc.tech]->poller.tx(handle, tx_data, tx_bits); } @@ -531,7 +531,7 @@ FuriHalNfcError furi_hal_nfc_poller_tx(const uint8_t* tx_data, size_t tx_bits) { FuriHalNfcError furi_hal_nfc_poller_rx(uint8_t* rx_data, size_t rx_data_size, size_t* rx_bits) { furi_check(furi_hal_nfc.mode == FuriHalNfcModePoller); furi_check(furi_hal_nfc.tech < FuriHalNfcTechNum); - FuriHalSpiBusHandle* handle = &furi_hal_spi_bus_handle_nfc; + const FuriHalSpiBusHandle* handle = &furi_hal_spi_bus_handle_nfc; return furi_hal_nfc_tech[furi_hal_nfc.tech]->poller.rx(handle, rx_data, rx_data_size, rx_bits); } @@ -556,12 +556,12 @@ FuriHalNfcError furi_hal_nfc_listener_tx(const uint8_t* tx_data, size_t tx_bits) furi_check(furi_hal_nfc.mode == FuriHalNfcModeListener); furi_check(furi_hal_nfc.tech < FuriHalNfcTechNum); - FuriHalSpiBusHandle* handle = &furi_hal_spi_bus_handle_nfc; + const FuriHalSpiBusHandle* handle = &furi_hal_spi_bus_handle_nfc; return furi_hal_nfc_tech[furi_hal_nfc.tech]->listener.tx(handle, tx_data, tx_bits); } FuriHalNfcError furi_hal_nfc_common_fifo_rx( - FuriHalSpiBusHandle* handle, + const FuriHalSpiBusHandle* handle, uint8_t* rx_data, size_t rx_data_size, size_t* rx_bits) { @@ -581,13 +581,13 @@ FuriHalNfcError furi_hal_nfc_listener_rx(uint8_t* rx_data, size_t rx_data_size, furi_check(furi_hal_nfc.mode == FuriHalNfcModeListener); furi_check(furi_hal_nfc.tech < FuriHalNfcTechNum); - FuriHalSpiBusHandle* handle = &furi_hal_spi_bus_handle_nfc; + const FuriHalSpiBusHandle* handle = &furi_hal_spi_bus_handle_nfc; return furi_hal_nfc_tech[furi_hal_nfc.tech]->listener.rx( handle, rx_data, rx_data_size, rx_bits); } FuriHalNfcError furi_hal_nfc_trx_reset(void) { - FuriHalSpiBusHandle* handle = &furi_hal_spi_bus_handle_nfc; + const FuriHalSpiBusHandle* handle = &furi_hal_spi_bus_handle_nfc; st25r3916_direct_cmd(handle, ST25R3916_CMD_STOP); @@ -598,7 +598,7 @@ FuriHalNfcError furi_hal_nfc_listener_sleep(void) { furi_check(furi_hal_nfc.mode == FuriHalNfcModeListener); furi_check(furi_hal_nfc.tech < FuriHalNfcTechNum); - FuriHalSpiBusHandle* handle = &furi_hal_spi_bus_handle_nfc; + const FuriHalSpiBusHandle* handle = &furi_hal_spi_bus_handle_nfc; return furi_hal_nfc_tech[furi_hal_nfc.tech]->listener.sleep(handle); } @@ -607,13 +607,13 @@ FuriHalNfcError furi_hal_nfc_listener_idle(void) { furi_check(furi_hal_nfc.mode == FuriHalNfcModeListener); furi_check(furi_hal_nfc.tech < FuriHalNfcTechNum); - FuriHalSpiBusHandle* handle = &furi_hal_spi_bus_handle_nfc; + const FuriHalSpiBusHandle* handle = &furi_hal_spi_bus_handle_nfc; return furi_hal_nfc_tech[furi_hal_nfc.tech]->listener.idle(handle); } FuriHalNfcError furi_hal_nfc_listener_enable_rx(void) { - FuriHalSpiBusHandle* handle = &furi_hal_spi_bus_handle_nfc; + const FuriHalSpiBusHandle* handle = &furi_hal_spi_bus_handle_nfc; st25r3916_direct_cmd(handle, ST25R3916_CMD_UNMASK_RECEIVE_DATA); diff --git a/targets/f7/furi_hal/furi_hal_nfc_event.c b/targets/f7/furi_hal/furi_hal_nfc_event.c index 9bcd2f1fe..56681b4fe 100644 --- a/targets/f7/furi_hal/furi_hal_nfc_event.c +++ b/targets/f7/furi_hal/furi_hal_nfc_event.c @@ -48,7 +48,7 @@ FuriHalNfcEvent furi_hal_nfc_wait_event_common(uint32_t timeout_ms) { if(event_flag != (unsigned)FuriFlagErrorTimeout) { if(event_flag & FuriHalNfcEventInternalTypeIrq) { furi_thread_flags_clear(FuriHalNfcEventInternalTypeIrq); - FuriHalSpiBusHandle* handle = &furi_hal_spi_bus_handle_nfc; + const FuriHalSpiBusHandle* handle = &furi_hal_spi_bus_handle_nfc; uint32_t irq = furi_hal_nfc_get_irq(handle); if(irq & ST25R3916_IRQ_MASK_OSC) { event |= FuriHalNfcEventOscOn; @@ -101,7 +101,7 @@ FuriHalNfcEvent furi_hal_nfc_wait_event_common(uint32_t timeout_ms) { } bool furi_hal_nfc_event_wait_for_specific_irq( - FuriHalSpiBusHandle* handle, + const FuriHalSpiBusHandle* handle, uint32_t mask, uint32_t timeout_ms) { furi_check(furi_hal_nfc_event); diff --git a/targets/f7/furi_hal/furi_hal_nfc_felica.c b/targets/f7/furi_hal/furi_hal_nfc_felica.c index 6c3f55525..1267b7b13 100644 --- a/targets/f7/furi_hal/furi_hal_nfc_felica.c +++ b/targets/f7/furi_hal/furi_hal_nfc_felica.c @@ -18,7 +18,7 @@ typedef struct { } FuriHalFelicaPtMemory; #pragma pack(pop) -static FuriHalNfcError furi_hal_nfc_felica_poller_init(FuriHalSpiBusHandle* handle) { +static FuriHalNfcError furi_hal_nfc_felica_poller_init(const FuriHalSpiBusHandle* handle) { // Enable Felica mode, AM modulation st25r3916_change_reg_bits( handle, @@ -61,13 +61,13 @@ static FuriHalNfcError furi_hal_nfc_felica_poller_init(FuriHalSpiBusHandle* hand return FuriHalNfcErrorNone; } -static FuriHalNfcError furi_hal_nfc_felica_poller_deinit(FuriHalSpiBusHandle* handle) { +static FuriHalNfcError furi_hal_nfc_felica_poller_deinit(const FuriHalSpiBusHandle* handle) { UNUSED(handle); return FuriHalNfcErrorNone; } -static FuriHalNfcError furi_hal_nfc_felica_listener_init(FuriHalSpiBusHandle* handle) { +static FuriHalNfcError furi_hal_nfc_felica_listener_init(const FuriHalSpiBusHandle* handle) { furi_assert(handle); st25r3916_direct_cmd(handle, ST25R3916_CMD_SET_DEFAULT); st25r3916_write_reg( @@ -141,7 +141,7 @@ static FuriHalNfcError furi_hal_nfc_felica_listener_init(FuriHalSpiBusHandle* ha return FuriHalNfcErrorNone; } -static FuriHalNfcError furi_hal_nfc_felica_listener_deinit(FuriHalSpiBusHandle* handle) { +static FuriHalNfcError furi_hal_nfc_felica_listener_deinit(const FuriHalSpiBusHandle* handle) { UNUSED(handle); return FuriHalNfcErrorNone; } @@ -154,19 +154,19 @@ static FuriHalNfcEvent furi_hal_nfc_felica_listener_wait_event(uint32_t timeout_ } FuriHalNfcError furi_hal_nfc_felica_listener_tx( - FuriHalSpiBusHandle* handle, + const FuriHalSpiBusHandle* handle, const uint8_t* tx_data, size_t tx_bits) { furi_hal_nfc_common_fifo_tx(handle, tx_data, tx_bits); return FuriHalNfcErrorNone; } -FuriHalNfcError furi_hal_nfc_felica_listener_sleep(FuriHalSpiBusHandle* handle) { +FuriHalNfcError furi_hal_nfc_felica_listener_sleep(const FuriHalSpiBusHandle* handle) { UNUSED(handle); return FuriHalNfcErrorNone; } -FuriHalNfcError furi_hal_nfc_felica_listener_idle(FuriHalSpiBusHandle* handle) { +FuriHalNfcError furi_hal_nfc_felica_listener_idle(const FuriHalSpiBusHandle* handle) { UNUSED(handle); return FuriHalNfcErrorNone; } @@ -182,7 +182,7 @@ FuriHalNfcError furi_hal_nfc_felica_listener_set_sensf_res_data( furi_check(idm_len == FURI_HAL_FELICA_IDM_PMM_LENGTH); furi_check(pmm_len == FURI_HAL_FELICA_IDM_PMM_LENGTH); - FuriHalSpiBusHandle* handle = &furi_hal_spi_bus_handle_nfc; + const FuriHalSpiBusHandle* handle = &furi_hal_spi_bus_handle_nfc; // Write PT Memory FuriHalFelicaPtMemory pt; pt.system_code = sys_code; diff --git a/targets/f7/furi_hal/furi_hal_nfc_i.h b/targets/f7/furi_hal/furi_hal_nfc_i.h index 084196451..5b0f8e68d 100644 --- a/targets/f7/furi_hal/furi_hal_nfc_i.h +++ b/targets/f7/furi_hal/furi_hal_nfc_i.h @@ -104,7 +104,7 @@ void furi_hal_nfc_timers_deinit(void); * @param[in,out] handle pointer to the SPI handle associated with the NFC chip. * @returns bitmask of zero or more occurred interrupts. */ -uint32_t furi_hal_nfc_get_irq(FuriHalSpiBusHandle* handle); +uint32_t furi_hal_nfc_get_irq(const FuriHalSpiBusHandle* handle); /** * @brief Wait until a specified type of interrupt occurs. @@ -115,7 +115,7 @@ uint32_t furi_hal_nfc_get_irq(FuriHalSpiBusHandle* handle); * @returns true if specified interrupt(s) have occured within timeout, false otherwise. */ bool furi_hal_nfc_event_wait_for_specific_irq( - FuriHalSpiBusHandle* handle, + const FuriHalSpiBusHandle* handle, uint32_t mask, uint32_t timeout_ms); @@ -137,7 +137,7 @@ FuriHalNfcEvent furi_hal_nfc_wait_event_common(uint32_t timeout_ms); * @param[in,out] handle pointer to the SPI handle associated with the NFC chip. * @returns FuriHalNfcErrorNone on success, any other error code on failure. */ -FuriHalNfcError furi_hal_nfc_common_listener_rx_start(FuriHalSpiBusHandle* handle); +FuriHalNfcError furi_hal_nfc_common_listener_rx_start(const FuriHalSpiBusHandle* handle); /** * @brief Transmit data using on-chip FIFO. @@ -150,7 +150,7 @@ FuriHalNfcError furi_hal_nfc_common_listener_rx_start(FuriHalSpiBusHandle* handl * @returns FuriHalNfcErrorNone on success, any other error code on failure. */ FuriHalNfcError furi_hal_nfc_common_fifo_tx( - FuriHalSpiBusHandle* handle, + const FuriHalSpiBusHandle* handle, const uint8_t* tx_data, size_t tx_bits); @@ -166,7 +166,7 @@ FuriHalNfcError furi_hal_nfc_common_fifo_tx( * @returns FuriHalNfcErrorNone on success, any other error code on failure. */ FuriHalNfcError furi_hal_nfc_common_fifo_rx( - FuriHalSpiBusHandle* handle, + const FuriHalSpiBusHandle* handle, uint8_t* rx_data, size_t rx_data_size, size_t* rx_bits); @@ -182,7 +182,7 @@ FuriHalNfcError furi_hal_nfc_common_fifo_rx( * @returns FuriHalNfcErrorNone on success, any other error code on failure. */ FuriHalNfcError furi_hal_nfc_poller_tx_common( - FuriHalSpiBusHandle* handle, + const FuriHalSpiBusHandle* handle, const uint8_t* tx_data, size_t tx_bits); diff --git a/targets/f7/furi_hal/furi_hal_nfc_irq.c b/targets/f7/furi_hal/furi_hal_nfc_irq.c index 90373955f..50a0139fd 100644 --- a/targets/f7/furi_hal/furi_hal_nfc_irq.c +++ b/targets/f7/furi_hal/furi_hal_nfc_irq.c @@ -8,7 +8,7 @@ static void furi_hal_nfc_int_callback(void* context) { furi_hal_nfc_event_set(FuriHalNfcEventInternalTypeIrq); } -uint32_t furi_hal_nfc_get_irq(FuriHalSpiBusHandle* handle) { +uint32_t furi_hal_nfc_get_irq(const FuriHalSpiBusHandle* handle) { uint32_t irq = 0; while(furi_hal_gpio_read_port_pin(gpio_nfc_irq_rfid_pull.port, gpio_nfc_irq_rfid_pull.pin)) { irq |= st25r3916_get_irq(handle); diff --git a/targets/f7/furi_hal/furi_hal_nfc_iso14443a.c b/targets/f7/furi_hal/furi_hal_nfc_iso14443a.c index 1ef23dfa4..be7a17264 100644 --- a/targets/f7/furi_hal/furi_hal_nfc_iso14443a.c +++ b/targets/f7/furi_hal/furi_hal_nfc_iso14443a.c @@ -13,7 +13,7 @@ static Iso14443_3aSignal* iso14443_3a_signal = NULL; -static FuriHalNfcError furi_hal_nfc_iso14443a_common_init(FuriHalSpiBusHandle* handle) { +static FuriHalNfcError furi_hal_nfc_iso14443a_common_init(const FuriHalSpiBusHandle* handle) { // Common NFC-A settings, 106 kbps // 1st stage zero = 600kHz, 3rd stage zero = 200 kHz @@ -40,7 +40,7 @@ static FuriHalNfcError furi_hal_nfc_iso14443a_common_init(FuriHalSpiBusHandle* h return FuriHalNfcErrorNone; } -static FuriHalNfcError furi_hal_nfc_iso14443a_poller_init(FuriHalSpiBusHandle* handle) { +static FuriHalNfcError furi_hal_nfc_iso14443a_poller_init(const FuriHalSpiBusHandle* handle) { // Enable ISO14443A mode, OOK modulation st25r3916_change_reg_bits( handle, @@ -57,7 +57,7 @@ static FuriHalNfcError furi_hal_nfc_iso14443a_poller_init(FuriHalSpiBusHandle* h return furi_hal_nfc_iso14443a_common_init(handle); } -static FuriHalNfcError furi_hal_nfc_iso14443a_poller_deinit(FuriHalSpiBusHandle* handle) { +static FuriHalNfcError furi_hal_nfc_iso14443a_poller_deinit(const FuriHalSpiBusHandle* handle) { st25r3916_change_reg_bits( handle, ST25R3916_REG_ISO14443A_NFC, @@ -67,7 +67,7 @@ static FuriHalNfcError furi_hal_nfc_iso14443a_poller_deinit(FuriHalSpiBusHandle* return FuriHalNfcErrorNone; } -static FuriHalNfcError furi_hal_nfc_iso14443a_listener_init(FuriHalSpiBusHandle* handle) { +static FuriHalNfcError furi_hal_nfc_iso14443a_listener_init(const FuriHalSpiBusHandle* handle) { furi_check(iso14443_3a_signal == NULL); iso14443_3a_signal = iso14443_3a_signal_alloc(&gpio_spi_r_mosi); @@ -105,7 +105,7 @@ static FuriHalNfcError furi_hal_nfc_iso14443a_listener_init(FuriHalSpiBusHandle* return furi_hal_nfc_iso14443a_common_init(handle); } -static FuriHalNfcError furi_hal_nfc_iso14443a_listener_deinit(FuriHalSpiBusHandle* handle) { +static FuriHalNfcError furi_hal_nfc_iso14443a_listener_deinit(const FuriHalSpiBusHandle* handle) { UNUSED(handle); if(iso14443_3a_signal) { @@ -118,7 +118,7 @@ static FuriHalNfcError furi_hal_nfc_iso14443a_listener_deinit(FuriHalSpiBusHandl static FuriHalNfcEvent furi_hal_nfc_iso14443_3a_listener_wait_event(uint32_t timeout_ms) { FuriHalNfcEvent event = furi_hal_nfc_wait_event_common(timeout_ms); - FuriHalSpiBusHandle* handle = &furi_hal_spi_bus_handle_nfc; + const FuriHalSpiBusHandle* handle = &furi_hal_spi_bus_handle_nfc; if(event & FuriHalNfcEventListenerActive) { st25r3916_set_reg_bits( @@ -131,7 +131,7 @@ static FuriHalNfcEvent furi_hal_nfc_iso14443_3a_listener_wait_event(uint32_t tim FuriHalNfcError furi_hal_nfc_iso14443a_poller_trx_short_frame(FuriHalNfcaShortFrame frame) { FuriHalNfcError error = FuriHalNfcErrorNone; - FuriHalSpiBusHandle* handle = &furi_hal_spi_bus_handle_nfc; + const FuriHalSpiBusHandle* handle = &furi_hal_spi_bus_handle_nfc; // Disable crc check st25r3916_set_reg_bits(handle, ST25R3916_REG_AUX, ST25R3916_REG_AUX_no_crc_rx); @@ -185,7 +185,7 @@ FuriHalNfcError furi_check(tx_data); FuriHalNfcError err = FuriHalNfcErrorNone; - FuriHalSpiBusHandle* handle = &furi_hal_spi_bus_handle_nfc; + const FuriHalSpiBusHandle* handle = &furi_hal_spi_bus_handle_nfc; // Prepare tx st25r3916_direct_cmd(handle, ST25R3916_CMD_CLEAR_FIFO); @@ -220,7 +220,7 @@ FuriHalNfcError furi_hal_nfc_iso14443a_listener_set_col_res_data( FuriHalNfcError error = FuriHalNfcErrorNone; - FuriHalSpiBusHandle* handle = &furi_hal_spi_bus_handle_nfc; + const FuriHalSpiBusHandle* handle = &furi_hal_spi_bus_handle_nfc; // Set 4 or 7 bytes UID if(uid_len == 4) { @@ -255,7 +255,7 @@ FuriHalNfcError furi_hal_nfc_iso14443a_listener_set_col_res_data( } FuriHalNfcError furi_hal_nfc_iso4443a_listener_tx( - FuriHalSpiBusHandle* handle, + const FuriHalSpiBusHandle* handle, const uint8_t* tx_data, size_t tx_bits) { FuriHalNfcError error = FuriHalNfcErrorNone; @@ -284,7 +284,7 @@ FuriHalNfcError furi_hal_nfc_iso14443a_listener_tx_custom_parity( furi_check(iso14443_3a_signal); - FuriHalSpiBusHandle* handle = &furi_hal_spi_bus_handle_nfc; + const FuriHalSpiBusHandle* handle = &furi_hal_spi_bus_handle_nfc; st25r3916_direct_cmd(handle, ST25R3916_CMD_TRANSPARENT_MODE); // Reconfigure gpio for Transparent mode @@ -303,7 +303,7 @@ FuriHalNfcError furi_hal_nfc_iso14443a_listener_tx_custom_parity( return FuriHalNfcErrorNone; } -FuriHalNfcError furi_hal_nfc_iso14443_3a_listener_sleep(FuriHalSpiBusHandle* handle) { +FuriHalNfcError furi_hal_nfc_iso14443_3a_listener_sleep(const FuriHalSpiBusHandle* handle) { // Enable auto collision resolution st25r3916_clear_reg_bits( handle, ST25R3916_REG_PASSIVE_TARGET, ST25R3916_REG_PASSIVE_TARGET_d_106_ac_a); @@ -313,7 +313,7 @@ FuriHalNfcError furi_hal_nfc_iso14443_3a_listener_sleep(FuriHalSpiBusHandle* han return FuriHalNfcErrorNone; } -FuriHalNfcError furi_hal_nfc_iso14443_3a_listener_idle(FuriHalSpiBusHandle* handle) { +FuriHalNfcError furi_hal_nfc_iso14443_3a_listener_idle(const FuriHalSpiBusHandle* handle) { // Enable auto collision resolution st25r3916_clear_reg_bits( handle, ST25R3916_REG_PASSIVE_TARGET, ST25R3916_REG_PASSIVE_TARGET_d_106_ac_a); diff --git a/targets/f7/furi_hal/furi_hal_nfc_iso14443b.c b/targets/f7/furi_hal/furi_hal_nfc_iso14443b.c index bb1a63515..315223e2f 100644 --- a/targets/f7/furi_hal/furi_hal_nfc_iso14443b.c +++ b/targets/f7/furi_hal/furi_hal_nfc_iso14443b.c @@ -1,7 +1,7 @@ #include "furi_hal_nfc_i.h" #include "furi_hal_nfc_tech_i.h" -static FuriHalNfcError furi_hal_nfc_iso14443b_common_init(FuriHalSpiBusHandle* handle) { +static FuriHalNfcError furi_hal_nfc_iso14443b_common_init(const FuriHalSpiBusHandle* handle) { // Common NFC-B settings, 106kbps // 1st stage zero = 60kHz, 3rd stage zero = 200 kHz @@ -40,7 +40,7 @@ static FuriHalNfcError furi_hal_nfc_iso14443b_common_init(FuriHalSpiBusHandle* h return FuriHalNfcErrorNone; } -static FuriHalNfcError furi_hal_nfc_iso14443b_poller_init(FuriHalSpiBusHandle* handle) { +static FuriHalNfcError furi_hal_nfc_iso14443b_poller_init(const FuriHalSpiBusHandle* handle) { // Enable ISO14443B mode, AM modulation st25r3916_change_reg_bits( handle, @@ -84,7 +84,7 @@ static FuriHalNfcError furi_hal_nfc_iso14443b_poller_init(FuriHalSpiBusHandle* h return furi_hal_nfc_iso14443b_common_init(handle); } -static FuriHalNfcError furi_hal_nfc_iso14443b_poller_deinit(FuriHalSpiBusHandle* handle) { +static FuriHalNfcError furi_hal_nfc_iso14443b_poller_deinit(const FuriHalSpiBusHandle* handle) { UNUSED(handle); return FuriHalNfcErrorNone; } diff --git a/targets/f7/furi_hal/furi_hal_nfc_iso15693.c b/targets/f7/furi_hal/furi_hal_nfc_iso15693.c index 3245b67cc..d35b160f4 100644 --- a/targets/f7/furi_hal/furi_hal_nfc_iso15693.c +++ b/targets/f7/furi_hal/furi_hal_nfc_iso15693.c @@ -74,7 +74,7 @@ static void furi_hal_nfc_iso15693_poller_free(FuriHalNfcIso15693Poller* instance free(instance); } -static FuriHalNfcError furi_hal_nfc_iso15693_common_init(FuriHalSpiBusHandle* handle) { +static FuriHalNfcError furi_hal_nfc_iso15693_common_init(const FuriHalSpiBusHandle* handle) { // Common NFC-V settings, 26.48 kbps // 1st stage zero = 12 kHz, 3rd stage zero = 80 kHz, low-pass = 600 kHz @@ -112,7 +112,7 @@ static FuriHalNfcError furi_hal_nfc_iso15693_common_init(FuriHalSpiBusHandle* ha return FuriHalNfcErrorNone; } -static FuriHalNfcError furi_hal_nfc_iso15693_poller_init(FuriHalSpiBusHandle* handle) { +static FuriHalNfcError furi_hal_nfc_iso15693_poller_init(const FuriHalSpiBusHandle* handle) { furi_check(furi_hal_nfc_iso15693_poller == NULL); furi_hal_nfc_iso15693_poller = furi_hal_nfc_iso15693_poller_alloc(); @@ -141,7 +141,7 @@ static FuriHalNfcError furi_hal_nfc_iso15693_poller_init(FuriHalSpiBusHandle* ha return furi_hal_nfc_iso15693_common_init(handle); } -static FuriHalNfcError furi_hal_nfc_iso15693_poller_deinit(FuriHalSpiBusHandle* handle) { +static FuriHalNfcError furi_hal_nfc_iso15693_poller_deinit(const FuriHalSpiBusHandle* handle) { UNUSED(handle); furi_check(furi_hal_nfc_iso15693_poller); @@ -238,7 +238,7 @@ static FuriHalNfcError iso15693_3_poller_decode_frame( } static FuriHalNfcError furi_hal_nfc_iso15693_poller_tx( - FuriHalSpiBusHandle* handle, + const FuriHalSpiBusHandle* handle, const uint8_t* tx_data, size_t tx_bits) { FuriHalNfcIso15693Poller* instance = furi_hal_nfc_iso15693_poller; @@ -252,7 +252,7 @@ static FuriHalNfcError furi_hal_nfc_iso15693_poller_tx( } static FuriHalNfcError furi_hal_nfc_iso15693_poller_rx( - FuriHalSpiBusHandle* handle, + const FuriHalSpiBusHandle* handle, uint8_t* rx_data, size_t rx_data_size, size_t* rx_bits) { @@ -284,14 +284,16 @@ static FuriHalNfcError furi_hal_nfc_iso15693_poller_rx( return error; } -static void furi_hal_nfc_iso15693_listener_transparent_mode_enter(FuriHalSpiBusHandle* handle) { +static void + furi_hal_nfc_iso15693_listener_transparent_mode_enter(const FuriHalSpiBusHandle* handle) { st25r3916_direct_cmd(handle, ST25R3916_CMD_TRANSPARENT_MODE); furi_hal_spi_bus_handle_deinit(handle); furi_hal_nfc_deinit_gpio_isr(); } -static void furi_hal_nfc_iso15693_listener_transparent_mode_exit(FuriHalSpiBusHandle* handle) { +static void + furi_hal_nfc_iso15693_listener_transparent_mode_exit(const FuriHalSpiBusHandle* handle) { // Configure gpio back to SPI and exit transparent mode furi_hal_nfc_init_gpio_isr(); furi_hal_spi_bus_handle_init(handle); @@ -299,7 +301,7 @@ static void furi_hal_nfc_iso15693_listener_transparent_mode_exit(FuriHalSpiBusHa st25r3916_direct_cmd(handle, ST25R3916_CMD_UNMASK_RECEIVE_DATA); } -static FuriHalNfcError furi_hal_nfc_iso15693_listener_init(FuriHalSpiBusHandle* handle) { +static FuriHalNfcError furi_hal_nfc_iso15693_listener_init(const FuriHalSpiBusHandle* handle) { furi_check(furi_hal_nfc_iso15693_listener == NULL); furi_hal_nfc_iso15693_listener = furi_hal_nfc_iso15693_listener_alloc(); @@ -328,7 +330,7 @@ static FuriHalNfcError furi_hal_nfc_iso15693_listener_init(FuriHalSpiBusHandle* return error; } -static FuriHalNfcError furi_hal_nfc_iso15693_listener_deinit(FuriHalSpiBusHandle* handle) { +static FuriHalNfcError furi_hal_nfc_iso15693_listener_deinit(const FuriHalSpiBusHandle* handle) { furi_check(furi_hal_nfc_iso15693_listener); furi_hal_nfc_iso15693_listener_transparent_mode_exit(handle); @@ -387,7 +389,7 @@ static FuriHalNfcEvent furi_hal_nfc_iso15693_wait_event(uint32_t timeout_ms) { } static FuriHalNfcError furi_hal_nfc_iso15693_listener_tx( - FuriHalSpiBusHandle* handle, + const FuriHalSpiBusHandle* handle, const uint8_t* tx_data, size_t tx_bits) { UNUSED(handle); @@ -407,7 +409,7 @@ FuriHalNfcError furi_hal_nfc_iso15693_listener_tx_sof(void) { } static FuriHalNfcError furi_hal_nfc_iso15693_listener_rx( - FuriHalSpiBusHandle* handle, + const FuriHalSpiBusHandle* handle, uint8_t* rx_data, size_t rx_data_size, size_t* rx_bits) { @@ -425,7 +427,7 @@ static FuriHalNfcError furi_hal_nfc_iso15693_listener_rx( return FuriHalNfcErrorNone; } -FuriHalNfcError furi_hal_nfc_iso15693_listener_sleep(FuriHalSpiBusHandle* handle) { +FuriHalNfcError furi_hal_nfc_iso15693_listener_sleep(const FuriHalSpiBusHandle* handle) { UNUSED(handle); return FuriHalNfcErrorNone; diff --git a/targets/f7/furi_hal/furi_hal_nfc_tech_i.h b/targets/f7/furi_hal/furi_hal_nfc_tech_i.h index e36dc852e..4a62f67c9 100644 --- a/targets/f7/furi_hal/furi_hal_nfc_tech_i.h +++ b/targets/f7/furi_hal/furi_hal_nfc_tech_i.h @@ -25,7 +25,7 @@ extern "C" { * @param[in,out] handle pointer to the NFC chip SPI handle. * @returns FuriHalNfcErrorNone on success, any other error code on failure. */ -typedef FuriHalNfcError (*FuriHalNfcChipConfig)(FuriHalSpiBusHandle* handle); +typedef FuriHalNfcError (*FuriHalNfcChipConfig)(const FuriHalSpiBusHandle* handle); /** * @brief Transmit data using technology-specific framing and timings. @@ -36,7 +36,7 @@ typedef FuriHalNfcError (*FuriHalNfcChipConfig)(FuriHalSpiBusHandle* handle); * @returns FuriHalNfcErrorNone on success, any other error code on failure. */ typedef FuriHalNfcError ( - *FuriHalNfcTx)(FuriHalSpiBusHandle* handle, const uint8_t* tx_data, size_t tx_bits); + *FuriHalNfcTx)(const FuriHalSpiBusHandle* handle, const uint8_t* tx_data, size_t tx_bits); /** * @brief Receive data using technology-specific framing and timings. @@ -48,7 +48,7 @@ typedef FuriHalNfcError ( * @returns FuriHalNfcErrorNone on success, any other error code on failure. */ typedef FuriHalNfcError (*FuriHalNfcRx)( - FuriHalSpiBusHandle* handle, + const FuriHalSpiBusHandle* handle, uint8_t* rx_data, size_t rx_data_size, size_t* rx_bits); @@ -69,7 +69,7 @@ typedef FuriHalNfcEvent (*FuriHalNfcWaitEvent)(uint32_t timeout_ms); * @param[in,out] handle pointer to the NFC chip SPI handle. * @returns FuriHalNfcErrorNone on success, any other error code on failure. */ -typedef FuriHalNfcError (*FuriHalNfcSleep)(FuriHalSpiBusHandle* handle); +typedef FuriHalNfcError (*FuriHalNfcSleep)(const FuriHalSpiBusHandle* handle); /** * @brief Go to idle in listener mode. @@ -79,7 +79,7 @@ typedef FuriHalNfcError (*FuriHalNfcSleep)(FuriHalSpiBusHandle* handle); * @param[in,out] handle pointer to the NFC chip SPI handle. * @returns FuriHalNfcErrorNone on success, any other error code on failure. */ -typedef FuriHalNfcError (*FuriHalNfcIdle)(FuriHalSpiBusHandle* handle); +typedef FuriHalNfcError (*FuriHalNfcIdle)(const FuriHalSpiBusHandle* handle); /** * @brief Technology-specific compenstaion values for pollers. @@ -160,7 +160,7 @@ extern const FuriHalNfcTechBase furi_hal_nfc_felica; * This variable is defined in furi_hal_nfc.c. It will need to be modified * in case when a new technology is to be added. */ -extern const FuriHalNfcTechBase* furi_hal_nfc_tech[]; +extern const FuriHalNfcTechBase* const furi_hal_nfc_tech[]; #ifdef __cplusplus } diff --git a/targets/f7/furi_hal/furi_hal_sd.c b/targets/f7/furi_hal/furi_hal_sd.c index eca5b6da9..152192736 100644 --- a/targets/f7/furi_hal/furi_hal_sd.c +++ b/targets/f7/furi_hal/furi_hal_sd.c @@ -204,7 +204,7 @@ typedef struct { } SD_CardInfo; /** Pointer to currently used SPI Handle */ -FuriHalSpiBusHandle* furi_hal_sd_spi_handle = NULL; +const FuriHalSpiBusHandle* furi_hal_sd_spi_handle = NULL; static inline void sd_spi_select_card(void) { furi_hal_gpio_write(furi_hal_sd_spi_handle->cs, false); diff --git a/targets/f7/furi_hal/furi_hal_spi.c b/targets/f7/furi_hal/furi_hal_spi.c index 2a7cb7c25..9997d278d 100644 --- a/targets/f7/furi_hal/furi_hal_spi.c +++ b/targets/f7/furi_hal/furi_hal_spi.c @@ -38,17 +38,17 @@ void furi_hal_spi_bus_deinit(FuriHalSpiBus* bus) { bus->callback(bus, FuriHalSpiBusEventDeinit); } -void furi_hal_spi_bus_handle_init(FuriHalSpiBusHandle* handle) { +void furi_hal_spi_bus_handle_init(const FuriHalSpiBusHandle* handle) { furi_check(handle); handle->callback(handle, FuriHalSpiBusHandleEventInit); } -void furi_hal_spi_bus_handle_deinit(FuriHalSpiBusHandle* handle) { +void furi_hal_spi_bus_handle_deinit(const FuriHalSpiBusHandle* handle) { furi_check(handle); handle->callback(handle, FuriHalSpiBusHandleEventDeinit); } -void furi_hal_spi_acquire(FuriHalSpiBusHandle* handle) { +void furi_hal_spi_acquire(const FuriHalSpiBusHandle* handle) { furi_check(handle); furi_hal_power_insomnia_enter(); @@ -62,7 +62,7 @@ void furi_hal_spi_acquire(FuriHalSpiBusHandle* handle) { handle->callback(handle, FuriHalSpiBusHandleEventActivate); } -void furi_hal_spi_release(FuriHalSpiBusHandle* handle) { +void furi_hal_spi_release(const FuriHalSpiBusHandle* handle) { furi_check(handle); furi_check(handle->bus->current_handle == handle); @@ -77,7 +77,7 @@ void furi_hal_spi_release(FuriHalSpiBusHandle* handle) { furi_hal_power_insomnia_exit(); } -static void furi_hal_spi_bus_end_txrx(FuriHalSpiBusHandle* handle, uint32_t timeout) { +static void furi_hal_spi_bus_end_txrx(const FuriHalSpiBusHandle* handle, uint32_t timeout) { UNUSED(timeout); // FIXME while(LL_SPI_GetTxFIFOLevel(handle->bus->spi) != LL_SPI_TX_FIFO_EMPTY) ; @@ -89,7 +89,7 @@ static void furi_hal_spi_bus_end_txrx(FuriHalSpiBusHandle* handle, uint32_t time } bool furi_hal_spi_bus_rx( - FuriHalSpiBusHandle* handle, + const FuriHalSpiBusHandle* handle, uint8_t* buffer, size_t size, uint32_t timeout) { @@ -102,7 +102,7 @@ bool furi_hal_spi_bus_rx( } bool furi_hal_spi_bus_tx( - FuriHalSpiBusHandle* handle, + const FuriHalSpiBusHandle* handle, const uint8_t* buffer, size_t size, uint32_t timeout) { @@ -128,7 +128,7 @@ bool furi_hal_spi_bus_tx( } bool furi_hal_spi_bus_trx( - FuriHalSpiBusHandle* handle, + const FuriHalSpiBusHandle* handle, const uint8_t* tx_buffer, uint8_t* rx_buffer, size_t size, @@ -192,7 +192,7 @@ static void spi_dma_isr(void* context) { } bool furi_hal_spi_bus_trx_dma( - FuriHalSpiBusHandle* handle, + const FuriHalSpiBusHandle* handle, uint8_t* tx_buffer, uint8_t* rx_buffer, size_t size, diff --git a/targets/f7/furi_hal/furi_hal_spi_config.c b/targets/f7/furi_hal/furi_hal_spi_config.c index 8a694961a..ece0c05f7 100644 --- a/targets/f7/furi_hal/furi_hal_spi_config.c +++ b/targets/f7/furi_hal/furi_hal_spi_config.c @@ -147,7 +147,7 @@ FuriHalSpiBus furi_hal_spi_bus_d = { /* SPI Bus Handles */ inline static void furi_hal_spi_bus_r_handle_event_callback( - FuriHalSpiBusHandle* handle, + const FuriHalSpiBusHandle* handle, FuriHalSpiBusHandleEvent event, const LL_SPI_InitTypeDef* preset) { if(event == FuriHalSpiBusHandleEventInit) { @@ -193,7 +193,7 @@ inline static void furi_hal_spi_bus_r_handle_event_callback( } inline static void furi_hal_spi_bus_external_handle_event_callback( - FuriHalSpiBusHandle* handle, + const FuriHalSpiBusHandle* handle, FuriHalSpiBusHandleEvent event, const LL_SPI_InitTypeDef* preset) { if(event == FuriHalSpiBusHandleEventInit) { @@ -239,7 +239,7 @@ inline static void furi_hal_spi_bus_external_handle_event_callback( } inline static void furi_hal_spi_bus_nfc_handle_event_callback( - FuriHalSpiBusHandle* handle, + const FuriHalSpiBusHandle* handle, FuriHalSpiBusHandleEvent event, const LL_SPI_InitTypeDef* preset) { if(event == FuriHalSpiBusHandleEventInit) { @@ -305,12 +305,12 @@ inline static void furi_hal_spi_bus_nfc_handle_event_callback( } static void furi_hal_spi_bus_handle_subghz_event_callback( - FuriHalSpiBusHandle* handle, + const FuriHalSpiBusHandle* handle, FuriHalSpiBusHandleEvent event) { furi_hal_spi_bus_r_handle_event_callback(handle, event, &furi_hal_spi_preset_1edge_low_8m); } -FuriHalSpiBusHandle furi_hal_spi_bus_handle_subghz = { +const FuriHalSpiBusHandle furi_hal_spi_bus_handle_subghz = { .bus = &furi_hal_spi_bus_r, .callback = furi_hal_spi_bus_handle_subghz_event_callback, .miso = &gpio_spi_r_miso, @@ -320,12 +320,12 @@ FuriHalSpiBusHandle furi_hal_spi_bus_handle_subghz = { }; static void furi_hal_spi_bus_handle_nfc_event_callback( - FuriHalSpiBusHandle* handle, + const FuriHalSpiBusHandle* handle, FuriHalSpiBusHandleEvent event) { furi_hal_spi_bus_nfc_handle_event_callback(handle, event, &furi_hal_spi_preset_2edge_low_8m); } -FuriHalSpiBusHandle furi_hal_spi_bus_handle_nfc = { +const FuriHalSpiBusHandle furi_hal_spi_bus_handle_nfc = { .bus = &furi_hal_spi_bus_r, .callback = furi_hal_spi_bus_handle_nfc_event_callback, .miso = &gpio_spi_r_miso, @@ -335,13 +335,13 @@ FuriHalSpiBusHandle furi_hal_spi_bus_handle_nfc = { }; static void furi_hal_spi_bus_handle_external_event_callback( - FuriHalSpiBusHandle* handle, + const FuriHalSpiBusHandle* handle, FuriHalSpiBusHandleEvent event) { furi_hal_spi_bus_external_handle_event_callback( handle, event, &furi_hal_spi_preset_1edge_low_2m); } -FuriHalSpiBusHandle furi_hal_spi_bus_handle_external = { +const FuriHalSpiBusHandle furi_hal_spi_bus_handle_external = { .bus = &furi_hal_spi_bus_r, .callback = furi_hal_spi_bus_handle_external_event_callback, .miso = &gpio_ext_pa6, @@ -351,7 +351,7 @@ FuriHalSpiBusHandle furi_hal_spi_bus_handle_external = { }; inline static void furi_hal_spi_bus_d_handle_event_callback( - FuriHalSpiBusHandle* handle, + const FuriHalSpiBusHandle* handle, FuriHalSpiBusHandleEvent event, const LL_SPI_InitTypeDef* preset) { if(event == FuriHalSpiBusHandleEventInit) { @@ -392,12 +392,12 @@ inline static void furi_hal_spi_bus_d_handle_event_callback( } static void furi_hal_spi_bus_handle_display_event_callback( - FuriHalSpiBusHandle* handle, + const FuriHalSpiBusHandle* handle, FuriHalSpiBusHandleEvent event) { furi_hal_spi_bus_d_handle_event_callback(handle, event, &furi_hal_spi_preset_1edge_low_4m); } -FuriHalSpiBusHandle furi_hal_spi_bus_handle_display = { +const FuriHalSpiBusHandle furi_hal_spi_bus_handle_display = { .bus = &furi_hal_spi_bus_d, .callback = furi_hal_spi_bus_handle_display_event_callback, .miso = &gpio_spi_d_miso, @@ -407,12 +407,12 @@ FuriHalSpiBusHandle furi_hal_spi_bus_handle_display = { }; static void furi_hal_spi_bus_handle_sd_fast_event_callback( - FuriHalSpiBusHandle* handle, + const FuriHalSpiBusHandle* handle, FuriHalSpiBusHandleEvent event) { furi_hal_spi_bus_d_handle_event_callback(handle, event, &furi_hal_spi_preset_1edge_low_16m); } -FuriHalSpiBusHandle furi_hal_spi_bus_handle_sd_fast = { +const FuriHalSpiBusHandle furi_hal_spi_bus_handle_sd_fast = { .bus = &furi_hal_spi_bus_d, .callback = furi_hal_spi_bus_handle_sd_fast_event_callback, .miso = &gpio_spi_d_miso, @@ -422,12 +422,12 @@ FuriHalSpiBusHandle furi_hal_spi_bus_handle_sd_fast = { }; static void furi_hal_spi_bus_handle_sd_slow_event_callback( - FuriHalSpiBusHandle* handle, + const FuriHalSpiBusHandle* handle, FuriHalSpiBusHandleEvent event) { furi_hal_spi_bus_d_handle_event_callback(handle, event, &furi_hal_spi_preset_1edge_low_2m); } -FuriHalSpiBusHandle furi_hal_spi_bus_handle_sd_slow = { +const FuriHalSpiBusHandle furi_hal_spi_bus_handle_sd_slow = { .bus = &furi_hal_spi_bus_d, .callback = furi_hal_spi_bus_handle_sd_slow_event_callback, .miso = &gpio_spi_d_miso, diff --git a/targets/f7/furi_hal/furi_hal_spi_config.h b/targets/f7/furi_hal/furi_hal_spi_config.h index eab633a19..e90cd7061 100644 --- a/targets/f7/furi_hal/furi_hal_spi_config.h +++ b/targets/f7/furi_hal/furi_hal_spi_config.h @@ -28,10 +28,10 @@ extern FuriHalSpiBus furi_hal_spi_bus_r; extern FuriHalSpiBus furi_hal_spi_bus_d; /** CC1101 on `furi_hal_spi_bus_r` */ -extern FuriHalSpiBusHandle furi_hal_spi_bus_handle_subghz; +extern const FuriHalSpiBusHandle furi_hal_spi_bus_handle_subghz; /** ST25R3916 on `furi_hal_spi_bus_r` */ -extern FuriHalSpiBusHandle furi_hal_spi_bus_handle_nfc; +extern const FuriHalSpiBusHandle furi_hal_spi_bus_handle_nfc; /** External on `furi_hal_spi_bus_r` * Preset: `furi_hal_spi_preset_1edge_low_2m` @@ -45,16 +45,16 @@ extern FuriHalSpiBusHandle furi_hal_spi_bus_handle_nfc; * Bus pins are floating on inactive state, CS high after initialization * */ -extern FuriHalSpiBusHandle furi_hal_spi_bus_handle_external; +extern const FuriHalSpiBusHandle furi_hal_spi_bus_handle_external; /** ST7567(Display) on `furi_hal_spi_bus_d` */ -extern FuriHalSpiBusHandle furi_hal_spi_bus_handle_display; +extern const FuriHalSpiBusHandle furi_hal_spi_bus_handle_display; /** SdCard in fast mode on `furi_hal_spi_bus_d` */ -extern FuriHalSpiBusHandle furi_hal_spi_bus_handle_sd_fast; +extern const FuriHalSpiBusHandle furi_hal_spi_bus_handle_sd_fast; /** SdCard in slow mode on `furi_hal_spi_bus_d` */ -extern FuriHalSpiBusHandle furi_hal_spi_bus_handle_sd_slow; +extern const FuriHalSpiBusHandle furi_hal_spi_bus_handle_sd_slow; #ifdef __cplusplus } diff --git a/targets/f7/furi_hal/furi_hal_spi_types.h b/targets/f7/furi_hal/furi_hal_spi_types.h index ecc18d50d..9bf138ac0 100644 --- a/targets/f7/furi_hal/furi_hal_spi_types.h +++ b/targets/f7/furi_hal/furi_hal_spi_types.h @@ -31,7 +31,7 @@ typedef void (*FuriHalSpiBusEventCallback)(FuriHalSpiBus* bus, FuriHalSpiBusEven struct FuriHalSpiBus { SPI_TypeDef* spi; FuriHalSpiBusEventCallback callback; - FuriHalSpiBusHandle* current_handle; + const FuriHalSpiBusHandle* current_handle; }; /** FuriHal spi handle states */ @@ -44,7 +44,7 @@ typedef enum { /** FuriHal spi handle event callback */ typedef void (*FuriHalSpiBusHandleEventCallback)( - FuriHalSpiBusHandle* handle, + const FuriHalSpiBusHandle* handle, FuriHalSpiBusHandleEvent event); /** FuriHal spi handle */ diff --git a/targets/furi_hal_include/furi_hal_i2c.h b/targets/furi_hal_include/furi_hal_i2c.h index 7d69cd74d..fe9f0949c 100644 --- a/targets/furi_hal_include/furi_hal_i2c.h +++ b/targets/furi_hal_include/furi_hal_i2c.h @@ -55,14 +55,14 @@ void furi_hal_i2c_init(void); * * @param handle Pointer to FuriHalI2cBusHandle instance */ -void furi_hal_i2c_acquire(FuriHalI2cBusHandle* handle); +void furi_hal_i2c_acquire(const FuriHalI2cBusHandle* handle); /** Release I2C bus handle * * @param handle Pointer to FuriHalI2cBusHandle instance acquired in * `furi_hal_i2c_acquire` */ -void furi_hal_i2c_release(FuriHalI2cBusHandle* handle); +void furi_hal_i2c_release(const FuriHalI2cBusHandle* handle); /** Perform I2C TX transfer * @@ -75,7 +75,7 @@ void furi_hal_i2c_release(FuriHalI2cBusHandle* handle); * @return true on successful transfer, false otherwise */ bool furi_hal_i2c_tx( - FuriHalI2cBusHandle* handle, + const FuriHalI2cBusHandle* handle, uint8_t address, const uint8_t* data, size_t size, @@ -96,7 +96,7 @@ bool furi_hal_i2c_tx( * @return true on successful transfer, false otherwise */ bool furi_hal_i2c_tx_ext( - FuriHalI2cBusHandle* handle, + const FuriHalI2cBusHandle* handle, uint16_t address, bool ten_bit, const uint8_t* data, @@ -116,7 +116,7 @@ bool furi_hal_i2c_tx_ext( * @return true on successful transfer, false otherwise */ bool furi_hal_i2c_rx( - FuriHalI2cBusHandle* handle, + const FuriHalI2cBusHandle* handle, uint8_t address, uint8_t* data, size_t size, @@ -136,7 +136,7 @@ bool furi_hal_i2c_rx( * @return true on successful transfer, false otherwise */ bool furi_hal_i2c_rx_ext( - FuriHalI2cBusHandle* handle, + const FuriHalI2cBusHandle* handle, uint16_t address, bool ten_bit, uint8_t* data, @@ -158,7 +158,7 @@ bool furi_hal_i2c_rx_ext( * @return true on successful transfer, false otherwise */ bool furi_hal_i2c_trx( - FuriHalI2cBusHandle* handle, + const FuriHalI2cBusHandle* handle, uint8_t address, const uint8_t* tx_data, size_t tx_size, @@ -174,7 +174,10 @@ bool furi_hal_i2c_trx( * * @return true if device present and is ready, false otherwise */ -bool furi_hal_i2c_is_device_ready(FuriHalI2cBusHandle* handle, uint8_t i2c_addr, uint32_t timeout); +bool furi_hal_i2c_is_device_ready( + const FuriHalI2cBusHandle* handle, + uint8_t i2c_addr, + uint32_t timeout); /** Perform I2C device register read (8-bit) * @@ -187,7 +190,7 @@ bool furi_hal_i2c_is_device_ready(FuriHalI2cBusHandle* handle, uint8_t i2c_addr, * @return true on successful transfer, false otherwise */ bool furi_hal_i2c_read_reg_8( - FuriHalI2cBusHandle* handle, + const FuriHalI2cBusHandle* handle, uint8_t i2c_addr, uint8_t reg_addr, uint8_t* data, @@ -204,7 +207,7 @@ bool furi_hal_i2c_read_reg_8( * @return true on successful transfer, false otherwise */ bool furi_hal_i2c_read_reg_16( - FuriHalI2cBusHandle* handle, + const FuriHalI2cBusHandle* handle, uint8_t i2c_addr, uint8_t reg_addr, uint16_t* data, @@ -222,7 +225,7 @@ bool furi_hal_i2c_read_reg_16( * @return true on successful transfer, false otherwise */ bool furi_hal_i2c_read_mem( - FuriHalI2cBusHandle* handle, + const FuriHalI2cBusHandle* handle, uint8_t i2c_addr, uint8_t mem_addr, uint8_t* data, @@ -240,7 +243,7 @@ bool furi_hal_i2c_read_mem( * @return true on successful transfer, false otherwise */ bool furi_hal_i2c_write_reg_8( - FuriHalI2cBusHandle* handle, + const FuriHalI2cBusHandle* handle, uint8_t i2c_addr, uint8_t reg_addr, uint8_t data, @@ -257,7 +260,7 @@ bool furi_hal_i2c_write_reg_8( * @return true on successful transfer, false otherwise */ bool furi_hal_i2c_write_reg_16( - FuriHalI2cBusHandle* handle, + const FuriHalI2cBusHandle* handle, uint8_t i2c_addr, uint8_t reg_addr, uint16_t data, @@ -275,7 +278,7 @@ bool furi_hal_i2c_write_reg_16( * @return true on successful transfer, false otherwise */ bool furi_hal_i2c_write_mem( - FuriHalI2cBusHandle* handle, + const FuriHalI2cBusHandle* handle, uint8_t i2c_addr, uint8_t mem_addr, const uint8_t* data, diff --git a/targets/furi_hal_include/furi_hal_spi.h b/targets/furi_hal_include/furi_hal_spi.h index d497dff5c..41f3abdaa 100644 --- a/targets/furi_hal_include/furi_hal_spi.h +++ b/targets/furi_hal_include/furi_hal_spi.h @@ -35,13 +35,13 @@ void furi_hal_spi_bus_deinit(FuriHalSpiBus* bus); * * @param handle pointer to FuriHalSpiBusHandle instance */ -void furi_hal_spi_bus_handle_init(FuriHalSpiBusHandle* handle); +void furi_hal_spi_bus_handle_init(const FuriHalSpiBusHandle* handle); /** Deinitialize SPI Bus Handle * * @param handle pointer to FuriHalSpiBusHandle instance */ -void furi_hal_spi_bus_handle_deinit(FuriHalSpiBusHandle* handle); +void furi_hal_spi_bus_handle_deinit(const FuriHalSpiBusHandle* handle); /** Acquire SPI bus * @@ -49,7 +49,7 @@ void furi_hal_spi_bus_handle_deinit(FuriHalSpiBusHandle* handle); * * @param handle pointer to FuriHalSpiBusHandle instance */ -void furi_hal_spi_acquire(FuriHalSpiBusHandle* handle); +void furi_hal_spi_acquire(const FuriHalSpiBusHandle* handle); /** Release SPI bus * @@ -57,7 +57,7 @@ void furi_hal_spi_acquire(FuriHalSpiBusHandle* handle); * * @param handle pointer to FuriHalSpiBusHandle instance */ -void furi_hal_spi_release(FuriHalSpiBusHandle* handle); +void furi_hal_spi_release(const FuriHalSpiBusHandle* handle); /** SPI Receive * @@ -69,7 +69,7 @@ void furi_hal_spi_release(FuriHalSpiBusHandle* handle); * @return true on sucess */ bool furi_hal_spi_bus_rx( - FuriHalSpiBusHandle* handle, + const FuriHalSpiBusHandle* handle, uint8_t* buffer, size_t size, uint32_t timeout); @@ -84,7 +84,7 @@ bool furi_hal_spi_bus_rx( * @return true on success */ bool furi_hal_spi_bus_tx( - FuriHalSpiBusHandle* handle, + const FuriHalSpiBusHandle* handle, const uint8_t* buffer, size_t size, uint32_t timeout); @@ -100,7 +100,7 @@ bool furi_hal_spi_bus_tx( * @return true on success */ bool furi_hal_spi_bus_trx( - FuriHalSpiBusHandle* handle, + const FuriHalSpiBusHandle* handle, const uint8_t* tx_buffer, uint8_t* rx_buffer, size_t size, @@ -117,7 +117,7 @@ bool furi_hal_spi_bus_trx( * @return true on success */ bool furi_hal_spi_bus_trx_dma( - FuriHalSpiBusHandle* handle, + const FuriHalSpiBusHandle* handle, uint8_t* tx_buffer, uint8_t* rx_buffer, size_t size, From 6962e9ce34d74720ef239591d982a3b1c4126108 Mon Sep 17 00:00:00 2001 From: WillyJL <49810075+Willy-JL@users.noreply.github.com> Date: Mon, 31 Mar 2025 16:52:15 +0000 Subject: [PATCH 169/268] Infrared: Fix universals sending (#4132) Co-authored-by: hedger --- applications/main/infrared/infrared_brute_force.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/applications/main/infrared/infrared_brute_force.c b/applications/main/infrared/infrared_brute_force.c index 636635894..1ec4645e9 100644 --- a/applications/main/infrared/infrared_brute_force.c +++ b/applications/main/infrared/infrared_brute_force.c @@ -104,9 +104,9 @@ InfraredErrorCode infrared_brute_force_calculate_messages(InfraredBruteForce* br break; } - size_t signal_start = flipper_format_tell(ff); bool signal_valid = false; while(infrared_signal_read_name(ff, signal_name) == InfraredErrorCodeNone) { + size_t signal_start = flipper_format_tell(ff); error = infrared_signal_read_body(signal, ff); signal_valid = (!INFRARED_ERROR_PRESENT(error)) && infrared_signal_is_valid(signal); if(!signal_valid) break; From 17759a9e4b5df521f33c62de6c56d1dcbf572cea Mon Sep 17 00:00:00 2001 From: WillyJL <49810075+Willy-JL@users.noreply.github.com> Date: Mon, 31 Mar 2025 16:59:12 +0000 Subject: [PATCH 170/268] NFC: Fix crash on ISO15693-3 save when memory is empty or cannot be read (#4165) * NFC: Possibly fix ISO15693-3 save crash with no data * Also prevent malloc(0) if block size or count is 0 --------- Co-authored-by: hedger --- .../iso15693_3/iso15693_3_render.c | 8 ++- lib/nfc/protocols/iso15693_3/iso15693_3.c | 68 ++++++++++--------- .../iso15693_3/iso15693_3_poller_i.c | 8 +-- 3 files changed, 45 insertions(+), 39 deletions(-) diff --git a/applications/main/nfc/helpers/protocol_support/iso15693_3/iso15693_3_render.c b/applications/main/nfc/helpers/protocol_support/iso15693_3/iso15693_3_render.c index ba8f10b93..fbf331d3b 100644 --- a/applications/main/nfc/helpers/protocol_support/iso15693_3/iso15693_3_render.c +++ b/applications/main/nfc/helpers/protocol_support/iso15693_3/iso15693_3_render.c @@ -37,11 +37,13 @@ void nfc_render_iso15693_3_brief(const Iso15693_3Data* data, FuriString* str) { } void nfc_render_iso15693_3_system_info(const Iso15693_3Data* data, FuriString* str) { - if(data->system_info.flags & ISO15693_3_SYSINFO_FLAG_MEMORY) { + const uint16_t block_count = iso15693_3_get_block_count(data); + const uint8_t block_size = iso15693_3_get_block_size(data); + + if((data->system_info.flags & ISO15693_3_SYSINFO_FLAG_MEMORY) && + (block_count > 0 && block_size > 0)) { furi_string_cat(str, "\e#Memory data\n\e*--------------------\n"); - const uint16_t block_count = iso15693_3_get_block_count(data); - const uint8_t block_size = iso15693_3_get_block_size(data); const uint16_t display_block_count = MIN(NFC_RENDER_ISO15693_3_MAX_BYTES / block_size, block_count); diff --git a/lib/nfc/protocols/iso15693_3/iso15693_3.c b/lib/nfc/protocols/iso15693_3/iso15693_3.c index e2628b258..802d87f03 100644 --- a/lib/nfc/protocols/iso15693_3/iso15693_3.c +++ b/lib/nfc/protocols/iso15693_3/iso15693_3.c @@ -173,33 +173,35 @@ bool iso15693_3_load(Iso15693_3Data* data, FlipperFormat* ff, uint32_t version) if(flipper_format_key_exist(ff, ISO15693_3_BLOCK_COUNT_KEY) && flipper_format_key_exist(ff, ISO15693_3_BLOCK_SIZE_KEY)) { + data->system_info.flags |= ISO15693_3_SYSINFO_FLAG_MEMORY; + uint32_t block_count; if(!flipper_format_read_uint32(ff, ISO15693_3_BLOCK_COUNT_KEY, &block_count, 1)) break; - data->system_info.block_count = block_count; - data->system_info.flags |= ISO15693_3_SYSINFO_FLAG_MEMORY; if(!flipper_format_read_hex( ff, ISO15693_3_BLOCK_SIZE_KEY, &(data->system_info.block_size), 1)) break; - simple_array_init( - data->block_data, data->system_info.block_size * data->system_info.block_count); - - if(!flipper_format_read_hex( - ff, - ISO15693_3_DATA_CONTENT_KEY, - simple_array_get_data(data->block_data), - simple_array_get_count(data->block_data))) - break; - - if(flipper_format_key_exist(ff, ISO15693_3_SECURITY_STATUS_KEY)) { + if(data->system_info.block_count > 0 && data->system_info.block_size > 0) { + simple_array_init( + data->block_data, + data->system_info.block_size * data->system_info.block_count); simple_array_init(data->block_security, data->system_info.block_count); - const bool security_loaded = has_lock_bits ? - iso15693_3_load_security(data, ff) : - iso15693_3_load_security_legacy(data, ff); - if(!security_loaded) break; + if(!flipper_format_read_hex( + ff, + ISO15693_3_DATA_CONTENT_KEY, + simple_array_get_data(data->block_data), + simple_array_get_count(data->block_data))) + break; + + if(flipper_format_key_exist(ff, ISO15693_3_SECURITY_STATUS_KEY)) { + const bool security_loaded = has_lock_bits ? + iso15693_3_load_security(data, ff) : + iso15693_3_load_security_legacy(data, ff); + if(!security_loaded) break; + } } } @@ -260,22 +262,24 @@ bool iso15693_3_save(const Iso15693_3Data* data, FlipperFormat* ff) { ff, ISO15693_3_BLOCK_SIZE_KEY, &data->system_info.block_size, 1)) break; - if(!flipper_format_write_hex( - ff, - ISO15693_3_DATA_CONTENT_KEY, - simple_array_cget_data(data->block_data), - simple_array_get_count(data->block_data))) - break; + if(data->system_info.block_count > 0 && data->system_info.block_size > 0) { + if(!flipper_format_write_hex( + ff, + ISO15693_3_DATA_CONTENT_KEY, + simple_array_cget_data(data->block_data), + simple_array_get_count(data->block_data))) + break; - if(!flipper_format_write_comment_cstr( - ff, "Block Security Status: 01 = locked, 00 = not locked")) - break; - if(!flipper_format_write_hex( - ff, - ISO15693_3_SECURITY_STATUS_KEY, - simple_array_cget_data(data->block_security), - simple_array_get_count(data->block_security))) - break; + if(!flipper_format_write_comment_cstr( + ff, "Block Security Status: 01 = locked, 00 = not locked")) + break; + if(!flipper_format_write_hex( + ff, + ISO15693_3_SECURITY_STATUS_KEY, + simple_array_cget_data(data->block_security), + simple_array_get_count(data->block_security))) + break; + } } saved = true; } while(false); diff --git a/lib/nfc/protocols/iso15693_3/iso15693_3_poller_i.c b/lib/nfc/protocols/iso15693_3/iso15693_3_poller_i.c index 6aee84a3f..bc677ce67 100644 --- a/lib/nfc/protocols/iso15693_3/iso15693_3_poller_i.c +++ b/lib/nfc/protocols/iso15693_3/iso15693_3_poller_i.c @@ -100,10 +100,12 @@ Iso15693_3Error iso15693_3_poller_activate(Iso15693_3Poller* instance, Iso15693_ break; } - if(system_info->block_count > 0) { - // Read blocks: Optional command + if(system_info->block_count > 0 && system_info->block_size > 0) { simple_array_init( data->block_data, system_info->block_count * system_info->block_size); + simple_array_init(data->block_security, system_info->block_count); + + // Read blocks: Optional command ret = iso15693_3_poller_read_blocks( instance, simple_array_get_data(data->block_data), @@ -115,8 +117,6 @@ Iso15693_3Error iso15693_3_poller_activate(Iso15693_3Poller* instance, Iso15693_ } // Get block security status: Optional command - simple_array_init(data->block_security, system_info->block_count); - ret = iso15693_3_poller_get_blocks_security( instance, simple_array_get_data(data->block_security), system_info->block_count); if(ret != Iso15693_3ErrorNone) { From 8871df863b2c840e8e67f2cde09d730c20cc81d2 Mon Sep 17 00:00:00 2001 From: WillyJL <49810075+Willy-JL@users.noreply.github.com> Date: Mon, 31 Mar 2025 17:22:16 +0000 Subject: [PATCH 171/268] NFC: Support DESFire Transaction MAC file type (#4159) * NFC: Support DESFire Transaction MAC file type * Fix typo --------- Co-authored-by: hedger --- .../mf_desfire/mf_desfire_render.c | 12 +++ lib/nfc/protocols/mf_desfire/mf_desfire.h | 6 ++ lib/nfc/protocols/mf_desfire/mf_desfire_i.c | 82 ++++++++++++++++--- .../mf_desfire/mf_desfire_poller_i.c | 2 +- 4 files changed, 89 insertions(+), 13 deletions(-) diff --git a/applications/main/nfc/helpers/protocol_support/mf_desfire/mf_desfire_render.c b/applications/main/nfc/helpers/protocol_support/mf_desfire/mf_desfire_render.c index 783cbb871..96e4a30f9 100644 --- a/applications/main/nfc/helpers/protocol_support/mf_desfire/mf_desfire_render.c +++ b/applications/main/nfc/helpers/protocol_support/mf_desfire/mf_desfire_render.c @@ -180,6 +180,9 @@ void nfc_render_mf_desfire_file_settings_data( case MfDesfireFileTypeCyclicRecord: type = "cyclic"; break; + case MfDesfireFileTypeTransactionMac: + type = "txn-mac"; + break; default: type = "unknown"; } @@ -237,6 +240,15 @@ void nfc_render_mf_desfire_file_settings_data( furi_string_cat_printf(str, "size %lu\n", record_size); furi_string_cat_printf(str, "num %lu max %lu\n", record_count, settings->record.max); break; + case MfDesfireFileTypeTransactionMac: + record_count = 0; + furi_string_cat_printf( + str, + "key opt %02X ver %02X\n", + settings->transaction_mac.key_option, + settings->transaction_mac.key_version); + furi_string_cat_printf(str, "cnt limit %lu\n", settings->transaction_mac.counter_limit); + break; } bool is_auth_required = true; diff --git a/lib/nfc/protocols/mf_desfire/mf_desfire.h b/lib/nfc/protocols/mf_desfire/mf_desfire.h index fb50008db..ec60b336b 100644 --- a/lib/nfc/protocols/mf_desfire/mf_desfire.h +++ b/lib/nfc/protocols/mf_desfire/mf_desfire.h @@ -97,6 +97,7 @@ typedef enum { MfDesfireFileTypeValue = 2, MfDesfireFileTypeLinearRecord = 3, MfDesfireFileTypeCyclicRecord = 4, + MfDesfireFileTypeTransactionMac = 5, } MfDesfireFileType; typedef enum { @@ -128,6 +129,11 @@ typedef struct { uint32_t max; uint32_t cur; } record; + struct { + uint8_t key_option; + uint8_t key_version; + uint32_t counter_limit; + } transaction_mac; }; } MfDesfireFileSettings; diff --git a/lib/nfc/protocols/mf_desfire/mf_desfire_i.c b/lib/nfc/protocols/mf_desfire/mf_desfire_i.c index d83a91ad1..eba9c4312 100644 --- a/lib/nfc/protocols/mf_desfire/mf_desfire_i.c +++ b/lib/nfc/protocols/mf_desfire/mf_desfire_i.c @@ -1,5 +1,7 @@ #include "mf_desfire_i.h" +#include + #define TAG "MfDesfire" #define BITS_IN_BYTE (8U) @@ -47,6 +49,10 @@ #define MF_DESFIRE_FFF_FILE_MAX_KEY "Max" #define MF_DESFIRE_FFF_FILE_CUR_KEY "Cur" +#define MF_DESFIRE_FFF_FILE_KEY_OPTION_KEY "Key Option" +#define MF_DESFIRE_FFF_FILE_KEY_VERSION_KEY "Key Version" +#define MF_DESFIRE_FFF_FILE_COUNTER_LIMIT_KEY "Counter Limit" + bool mf_desfire_version_parse(MfDesfireVersion* data, const BitBuffer* buf) { const bool can_parse = bit_buffer_get_size_bytes(buf) == sizeof(MfDesfireVersion); @@ -168,12 +174,19 @@ bool mf_desfire_file_settings_parse(MfDesfireFileSettings* data, const BitBuffer uint32_t cur : 3 * BITS_IN_BYTE; } MfDesfireFileSettingsRecord; + typedef struct FURI_PACKED { + uint8_t key_option; + uint8_t key_version; + uint8_t counter_limit[]; + } MfDesfireFileSettingsTransactionMac; + typedef struct FURI_PACKED { MfDesfireFileSettingsHeader header; union { MfDesfireFileSettingsData data; MfDesfireFileSettingsValue value; MfDesfireFileSettingsRecord record; + MfDesfireFileSettingsTransactionMac transaction_mac; }; } MfDesfireFileSettingsLayout; @@ -182,7 +195,7 @@ bool mf_desfire_file_settings_parse(MfDesfireFileSettings* data, const BitBuffer const size_t data_size = bit_buffer_get_size_bytes(buf); const uint8_t* data_ptr = bit_buffer_get_data(buf); const size_t min_data_size = - sizeof(MfDesfireFileSettingsHeader) + sizeof(MfDesfireFileSettingsData); + sizeof(MfDesfireFileSettingsHeader) + sizeof(MfDesfireFileSettingsTransactionMac); if(data_size < min_data_size) { FURI_LOG_E( @@ -202,17 +215,11 @@ bool mf_desfire_file_settings_parse(MfDesfireFileSettings* data, const BitBuffer if(file_settings_temp.type == MfDesfireFileTypeStandard || file_settings_temp.type == MfDesfireFileTypeBackup) { - memcpy( - &layout.data, - &data_ptr[sizeof(MfDesfireFileSettingsHeader)], - sizeof(MfDesfireFileSettingsData)); + memcpy(&layout.data, &data_ptr[bytes_processed], sizeof(MfDesfireFileSettingsData)); file_settings_temp.data.size = layout.data.size; bytes_processed += sizeof(MfDesfireFileSettingsData); } else if(file_settings_temp.type == MfDesfireFileTypeValue) { - memcpy( - &layout.value, - &data_ptr[sizeof(MfDesfireFileSettingsHeader)], - sizeof(MfDesfireFileSettingsValue)); + memcpy(&layout.value, &data_ptr[bytes_processed], sizeof(MfDesfireFileSettingsValue)); file_settings_temp.value.lo_limit = layout.value.lo_limit; file_settings_temp.value.hi_limit = layout.value.hi_limit; file_settings_temp.value.limited_credit_value = layout.value.limited_credit_value; @@ -222,13 +229,34 @@ bool mf_desfire_file_settings_parse(MfDesfireFileSettings* data, const BitBuffer file_settings_temp.type == MfDesfireFileTypeLinearRecord || file_settings_temp.type == MfDesfireFileTypeCyclicRecord) { memcpy( - &layout.record, - &data_ptr[sizeof(MfDesfireFileSettingsHeader)], - sizeof(MfDesfireFileSettingsRecord)); + &layout.record, &data_ptr[bytes_processed], sizeof(MfDesfireFileSettingsRecord)); file_settings_temp.record.size = layout.record.size; file_settings_temp.record.max = layout.record.max; file_settings_temp.record.cur = layout.record.cur; bytes_processed += sizeof(MfDesfireFileSettingsRecord); + } else if(file_settings_temp.type == MfDesfireFileTypeTransactionMac) { + const bool has_counter_limit = (layout.header.comm & 0x20) != 0; + memcpy( + &layout.transaction_mac, + &data_ptr[bytes_processed], + sizeof(MfDesfireFileSettingsTransactionMac)); + file_settings_temp.transaction_mac.key_option = layout.transaction_mac.key_option; + file_settings_temp.transaction_mac.key_version = layout.transaction_mac.key_version; + if(!has_counter_limit) { + file_settings_temp.transaction_mac.counter_limit = 0; + } else { + // AES (4b) or LRP (2b) + const size_t counter_limit_size = (layout.transaction_mac.key_option & 0x02) ? 4 : + 2; + memcpy( + &layout.transaction_mac, + &data_ptr[bytes_processed], + sizeof(MfDesfireFileSettingsTransactionMac) + counter_limit_size); + file_settings_temp.transaction_mac.counter_limit = bit_lib_bytes_to_num_be( + layout.transaction_mac.counter_limit, counter_limit_size); + bytes_processed += counter_limit_size; + } + bytes_processed += sizeof(MfDesfireFileSettingsTransactionMac); } else { FURI_LOG_W(TAG, "Unknown file type: %02x", file_settings_temp.type); break; @@ -468,6 +496,21 @@ bool mf_desfire_file_settings_load( furi_string_printf(key, "%s %s", prefix, MF_DESFIRE_FFF_FILE_CUR_KEY); if(!flipper_format_read_uint32(ff, furi_string_get_cstr(key), &data->record.cur, 1)) break; + } else if(data->type == MfDesfireFileTypeTransactionMac) { + furi_string_printf(key, "%s %s", prefix, MF_DESFIRE_FFF_FILE_KEY_OPTION_KEY); + if(!flipper_format_read_hex( + ff, furi_string_get_cstr(key), &data->transaction_mac.key_option, 1)) + break; + + furi_string_printf(key, "%s %s", prefix, MF_DESFIRE_FFF_FILE_KEY_VERSION_KEY); + if(!flipper_format_read_hex( + ff, furi_string_get_cstr(key), &data->transaction_mac.key_version, 1)) + break; + + furi_string_printf(key, "%s %s", prefix, MF_DESFIRE_FFF_FILE_COUNTER_LIMIT_KEY); + if(!flipper_format_read_uint32( + ff, furi_string_get_cstr(key), &data->transaction_mac.counter_limit, 1)) + break; } success = true; @@ -716,6 +759,21 @@ bool mf_desfire_file_settings_save( furi_string_printf(key, "%s %s", prefix, MF_DESFIRE_FFF_FILE_CUR_KEY); if(!flipper_format_write_uint32(ff, furi_string_get_cstr(key), &data->record.cur, 1)) break; + } else if(data->type == MfDesfireFileTypeTransactionMac) { + furi_string_printf(key, "%s %s", prefix, MF_DESFIRE_FFF_FILE_KEY_OPTION_KEY); + if(!flipper_format_write_hex( + ff, furi_string_get_cstr(key), &data->transaction_mac.key_option, 1)) + break; + + furi_string_printf(key, "%s %s", prefix, MF_DESFIRE_FFF_FILE_KEY_VERSION_KEY); + if(!flipper_format_write_hex( + ff, furi_string_get_cstr(key), &data->transaction_mac.key_version, 1)) + break; + + furi_string_printf(key, "%s %s", prefix, MF_DESFIRE_FFF_FILE_COUNTER_LIMIT_KEY); + if(!flipper_format_write_uint32( + ff, furi_string_get_cstr(key), &data->transaction_mac.counter_limit, 1)) + break; } success = true; diff --git a/lib/nfc/protocols/mf_desfire/mf_desfire_poller_i.c b/lib/nfc/protocols/mf_desfire/mf_desfire_poller_i.c index 6d8dfda16..8b57fcc4c 100644 --- a/lib/nfc/protocols/mf_desfire/mf_desfire_poller_i.c +++ b/lib/nfc/protocols/mf_desfire/mf_desfire_poller_i.c @@ -468,7 +468,7 @@ MfDesfireError mf_desfire_poller_read_file_data_multi( file_type == MfDesfireFileTypeLinearRecord || file_type == MfDesfireFileTypeCyclicRecord) { error = mf_desfire_poller_read_file_records( - instance, file_id, 0, file_settings_cur->data.size, file_data); + instance, file_id, 0, file_settings_cur->record.size, file_data); } } From a0645bc6f411f7a21c4815d284acadf88d190289 Mon Sep 17 00:00:00 2001 From: Konstantin Volkov <72250702+doomwastaken@users.noreply.github.com> Date: Mon, 31 Mar 2025 20:33:17 +0300 Subject: [PATCH 172/268] HID Ble: increased stack and improvements (#4149) * increased hid remote stack, increased swipe speed, added enterprise sleep * decreased extra stack by 256b * app: remote: bumped version and increased stack sizes --------- Co-authored-by: doomwastaken Co-authored-by: hedger Co-authored-by: hedger --- applications/system/hid_app/application.fam | 8 +++---- .../system/hid_app/views/hid_tiktok.c | 24 +++++++++++-------- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/applications/system/hid_app/application.fam b/applications/system/hid_app/application.fam index cc218c31a..aae5ebf9d 100644 --- a/applications/system/hid_app/application.fam +++ b/applications/system/hid_app/application.fam @@ -3,11 +3,11 @@ App( name="Remote", apptype=FlipperAppType.EXTERNAL, entry_point="hid_usb_app", - stack_size=1 * 1024, + stack_size=2 * 1024, sources=["*.c", "!transport_ble.c"], cdefines=["HID_TRANSPORT_USB"], fap_description="Use Flipper as a HID remote control over USB", - fap_version="1.0", + fap_version="1.1", fap_category="USB", fap_icon="hid_usb_10px.png", fap_icon_assets="assets", @@ -20,12 +20,12 @@ App( name="Remote", apptype=FlipperAppType.EXTERNAL, entry_point="hid_ble_app", - stack_size=1 * 1024, + stack_size=2 * 1024, sources=["*.c", "!transport_usb.c"], cdefines=["HID_TRANSPORT_BLE"], fap_libs=["ble_profile"], fap_description="Use Flipper as a HID remote control over Bluetooth", - fap_version="1.0", + fap_version="1.1", fap_category="Bluetooth", fap_icon="hid_ble_10px.png", fap_icon_assets="assets", diff --git a/applications/system/hid_app/views/hid_tiktok.c b/applications/system/hid_app/views/hid_tiktok.c index f1501027c..f0997f72e 100644 --- a/applications/system/hid_app/views/hid_tiktok.c +++ b/applications/system/hid_app/views/hid_tiktok.c @@ -103,7 +103,10 @@ static void hid_tiktok_reset_cursor(HidTikTok* hid_tiktok) { furi_delay_ms(50); } // Move cursor from the corner - hid_hal_mouse_move(hid_tiktok->hid, 20, 120); + // Actions split for some mobiles to properly process mouse movements + hid_hal_mouse_move(hid_tiktok->hid, 10, 60); + furi_delay_ms(3); + hid_hal_mouse_move(hid_tiktok->hid, 0, 60); furi_delay_ms(50); } @@ -162,29 +165,30 @@ static bool hid_tiktok_input_callback(InputEvent* event, void* context) { consumed = true; } else if(event->type == InputTypeShort) { if(event->key == InputKeyOk) { + // delays adjusted for emulation of a finger tap hid_hal_mouse_press(hid_tiktok->hid, HID_MOUSE_BTN_LEFT); - furi_delay_ms(50); + furi_delay_ms(25); hid_hal_mouse_release(hid_tiktok->hid, HID_MOUSE_BTN_LEFT); - furi_delay_ms(50); + furi_delay_ms(75); hid_hal_mouse_press(hid_tiktok->hid, HID_MOUSE_BTN_LEFT); - furi_delay_ms(50); + furi_delay_ms(25); hid_hal_mouse_release(hid_tiktok->hid, HID_MOUSE_BTN_LEFT); consumed = true; } else if(event->key == InputKeyUp) { // Emulate up swipe - hid_hal_mouse_scroll(hid_tiktok->hid, -6); hid_hal_mouse_scroll(hid_tiktok->hid, -12); - hid_hal_mouse_scroll(hid_tiktok->hid, -19); + hid_hal_mouse_scroll(hid_tiktok->hid, -24); + hid_hal_mouse_scroll(hid_tiktok->hid, -38); + hid_hal_mouse_scroll(hid_tiktok->hid, -24); hid_hal_mouse_scroll(hid_tiktok->hid, -12); - hid_hal_mouse_scroll(hid_tiktok->hid, -6); consumed = true; } else if(event->key == InputKeyDown) { // Emulate down swipe - hid_hal_mouse_scroll(hid_tiktok->hid, 6); hid_hal_mouse_scroll(hid_tiktok->hid, 12); - hid_hal_mouse_scroll(hid_tiktok->hid, 19); + hid_hal_mouse_scroll(hid_tiktok->hid, 24); + hid_hal_mouse_scroll(hid_tiktok->hid, 38); + hid_hal_mouse_scroll(hid_tiktok->hid, 24); hid_hal_mouse_scroll(hid_tiktok->hid, 12); - hid_hal_mouse_scroll(hid_tiktok->hid, 6); consumed = true; } else if(event->key == InputKeyBack) { hid_hal_consumer_key_release_all(hid_tiktok->hid); From f6916fe616465125b77c70dc54f025df7a4b013a Mon Sep 17 00:00:00 2001 From: Yukai Li Date: Mon, 31 Mar 2025 12:32:58 -0600 Subject: [PATCH 173/268] Fix DWARF dead code elimination and linking (#4144) Co-authored-by: hedger --- site_scons/cc.scons | 1 + targets/f7/application_ext.ld | 54 +++++++++++++++++++++++++++++--- targets/f7/stm32wb55xx_flash.ld | 51 ++++++++++++++++++++++++++++++ targets/f7/stm32wb55xx_ram_fw.ld | 51 ++++++++++++++++++++++++++++++ 4 files changed, 153 insertions(+), 4 deletions(-) diff --git a/site_scons/cc.scons b/site_scons/cc.scons index c5d99b896..9960d350c 100644 --- a/site_scons/cc.scons +++ b/site_scons/cc.scons @@ -31,6 +31,7 @@ ENV.AppendUnique( "-Wundef", "-fdata-sections", "-ffunction-sections", + "-Wa,-gdwarf-sections", "-fsingle-precision-constant", "-fno-math-errno", # Generates .su files with stack usage information diff --git a/targets/f7/application_ext.ld b/targets/f7/application_ext.ld index b6496290a..456947db1 100644 --- a/targets/f7/application_ext.ld +++ b/targets/f7/application_ext.ld @@ -33,10 +33,56 @@ SECTIONS { *(COMMON) } - .ARM.attributes : { - *(.ARM.attributes) - *(.ARM.attributes.*) - } + /* Default debug-related rules from ld */ + + /* Stabs debugging sections. */ + .stab 0 : { *(.stab) } + .stabstr 0 : { *(.stabstr) } + .stab.excl 0 : { *(.stab.excl) } + .stab.exclstr 0 : { *(.stab.exclstr) } + .stab.index 0 : { *(.stab.index) } + .stab.indexstr 0 : { *(.stab.indexstr) } + .gnu.build.attributes : { *(.gnu.build.attributes .gnu.build.attributes.*) } + /* DWARF debug sections. + Symbols in the DWARF debugging sections are relative to the beginning + of the section so we begin them at 0. */ + /* DWARF 1. */ + .debug 0 : { *(.debug) } + .line 0 : { *(.line) } + /* GNU DWARF 1 extensions. */ + .debug_srcinfo 0 : { *(.debug_srcinfo) } + .debug_sfnames 0 : { *(.debug_sfnames) } + /* DWARF 1.1 and DWARF 2. */ + .debug_aranges 0 : { *(.debug_aranges) } + .debug_pubnames 0 : { *(.debug_pubnames) } + /* DWARF 2. */ + .debug_info 0 : { *(.debug_info .gnu.linkonce.wi.*) } + .debug_abbrev 0 : { *(.debug_abbrev) } + .debug_line 0 : { *(.debug_line .debug_line.* .debug_line_end) } + .debug_frame 0 : { *(.debug_frame) } + .debug_str 0 : { *(.debug_str) } + .debug_loc 0 : { *(.debug_loc) } + .debug_macinfo 0 : { *(.debug_macinfo) } + /* SGI/MIPS DWARF 2 extensions. */ + .debug_weaknames 0 : { *(.debug_weaknames) } + .debug_funcnames 0 : { *(.debug_funcnames) } + .debug_typenames 0 : { *(.debug_typenames) } + .debug_varnames 0 : { *(.debug_varnames) } + /* DWARF 3. */ + .debug_pubtypes 0 : { *(.debug_pubtypes) } + .debug_ranges 0 : { *(.debug_ranges) } + /* DWARF 5. */ + .debug_addr 0 : { *(.debug_addr) } + .debug_line_str 0 : { *(.debug_line_str) } + .debug_loclists 0 : { *(.debug_loclists) } + .debug_macro 0 : { *(.debug_macro) } + .debug_names 0 : { *(.debug_names) } + .debug_rnglists 0 : { *(.debug_rnglists) } + .debug_str_offsets 0 : { *(.debug_str_offsets) } + .debug_sup 0 : { *(.debug_sup) } + .ARM.attributes 0 : { KEEP (*(.ARM.attributes)) KEEP (*(.gnu.attributes)) } + .note.gnu.arm.ident 0 : { KEEP (*(.note.gnu.arm.ident)) } + /DISCARD/ : { *(.note.GNU-stack) *(.gnu_debuglink) *(.gnu.lto_*) } /DISCARD/ : { *(.comment) diff --git a/targets/f7/stm32wb55xx_flash.ld b/targets/f7/stm32wb55xx_flash.ld index ef61bb238..c31aee863 100644 --- a/targets/f7/stm32wb55xx_flash.ld +++ b/targets/f7/stm32wb55xx_flash.ld @@ -131,4 +131,55 @@ SECTIONS { MB_MEM2 (NOLOAD) : { _sMB_MEM2 = . ; *(MB_MEM2) ; _eMB_MEM2 = . ; } >RAM2A ._sram2a_free : { . = ALIGN(4); __sram2a_free__ = .; } >RAM2A ._sram2b_start : { . = ALIGN(4); __sram2b_start__ = .; } >RAM2B + + /* Default debug-related rules from ld */ + + /* Stabs debugging sections. */ + .stab 0 : { *(.stab) } + .stabstr 0 : { *(.stabstr) } + .stab.excl 0 : { *(.stab.excl) } + .stab.exclstr 0 : { *(.stab.exclstr) } + .stab.index 0 : { *(.stab.index) } + .stab.indexstr 0 : { *(.stab.indexstr) } + .comment 0 : { *(.comment) } + .gnu.build.attributes : { *(.gnu.build.attributes .gnu.build.attributes.*) } + /* DWARF debug sections. + Symbols in the DWARF debugging sections are relative to the beginning + of the section so we begin them at 0. */ + /* DWARF 1. */ + .debug 0 : { *(.debug) } + .line 0 : { *(.line) } + /* GNU DWARF 1 extensions. */ + .debug_srcinfo 0 : { *(.debug_srcinfo) } + .debug_sfnames 0 : { *(.debug_sfnames) } + /* DWARF 1.1 and DWARF 2. */ + .debug_aranges 0 : { *(.debug_aranges) } + .debug_pubnames 0 : { *(.debug_pubnames) } + /* DWARF 2. */ + .debug_info 0 : { *(.debug_info .gnu.linkonce.wi.*) } + .debug_abbrev 0 : { *(.debug_abbrev) } + .debug_line 0 : { *(.debug_line .debug_line.* .debug_line_end) } + .debug_frame 0 : { *(.debug_frame) } + .debug_str 0 : { *(.debug_str) } + .debug_loc 0 : { *(.debug_loc) } + .debug_macinfo 0 : { *(.debug_macinfo) } + /* SGI/MIPS DWARF 2 extensions. */ + .debug_weaknames 0 : { *(.debug_weaknames) } + .debug_funcnames 0 : { *(.debug_funcnames) } + .debug_typenames 0 : { *(.debug_typenames) } + .debug_varnames 0 : { *(.debug_varnames) } + /* DWARF 3. */ + .debug_pubtypes 0 : { *(.debug_pubtypes) } + .debug_ranges 0 : { *(.debug_ranges) } + /* DWARF 5. */ + .debug_addr 0 : { *(.debug_addr) } + .debug_line_str 0 : { *(.debug_line_str) } + .debug_loclists 0 : { *(.debug_loclists) } + .debug_macro 0 : { *(.debug_macro) } + .debug_names 0 : { *(.debug_names) } + .debug_rnglists 0 : { *(.debug_rnglists) } + .debug_str_offsets 0 : { *(.debug_str_offsets) } + .debug_sup 0 : { *(.debug_sup) } + .note.gnu.arm.ident 0 : { KEEP (*(.note.gnu.arm.ident)) } + /DISCARD/ : { *(.note.GNU-stack) *(.gnu_debuglink) *(.gnu.lto_*) } } diff --git a/targets/f7/stm32wb55xx_ram_fw.ld b/targets/f7/stm32wb55xx_ram_fw.ld index 93579788d..c3f5f8a6a 100644 --- a/targets/f7/stm32wb55xx_ram_fw.ld +++ b/targets/f7/stm32wb55xx_ram_fw.ld @@ -129,4 +129,55 @@ SECTIONS { MB_MEM2 (NOLOAD) : { _sMB_MEM2 = . ; *(MB_MEM2) ; _eMB_MEM2 = . ; } >RAM2A ._sram2a_free : { . = ALIGN(4); __sram2a_free__ = .; } >RAM2A ._sram2b_start : { . = ALIGN(4); __sram2b_start__ = .; } >RAM2B + + /* Default debug-related rules from ld */ + + /* Stabs debugging sections. */ + .stab 0 : { *(.stab) } + .stabstr 0 : { *(.stabstr) } + .stab.excl 0 : { *(.stab.excl) } + .stab.exclstr 0 : { *(.stab.exclstr) } + .stab.index 0 : { *(.stab.index) } + .stab.indexstr 0 : { *(.stab.indexstr) } + .comment 0 : { *(.comment) } + .gnu.build.attributes : { *(.gnu.build.attributes .gnu.build.attributes.*) } + /* DWARF debug sections. + Symbols in the DWARF debugging sections are relative to the beginning + of the section so we begin them at 0. */ + /* DWARF 1. */ + .debug 0 : { *(.debug) } + .line 0 : { *(.line) } + /* GNU DWARF 1 extensions. */ + .debug_srcinfo 0 : { *(.debug_srcinfo) } + .debug_sfnames 0 : { *(.debug_sfnames) } + /* DWARF 1.1 and DWARF 2. */ + .debug_aranges 0 : { *(.debug_aranges) } + .debug_pubnames 0 : { *(.debug_pubnames) } + /* DWARF 2. */ + .debug_info 0 : { *(.debug_info .gnu.linkonce.wi.*) } + .debug_abbrev 0 : { *(.debug_abbrev) } + .debug_line 0 : { *(.debug_line .debug_line.* .debug_line_end) } + .debug_frame 0 : { *(.debug_frame) } + .debug_str 0 : { *(.debug_str) } + .debug_loc 0 : { *(.debug_loc) } + .debug_macinfo 0 : { *(.debug_macinfo) } + /* SGI/MIPS DWARF 2 extensions. */ + .debug_weaknames 0 : { *(.debug_weaknames) } + .debug_funcnames 0 : { *(.debug_funcnames) } + .debug_typenames 0 : { *(.debug_typenames) } + .debug_varnames 0 : { *(.debug_varnames) } + /* DWARF 3. */ + .debug_pubtypes 0 : { *(.debug_pubtypes) } + .debug_ranges 0 : { *(.debug_ranges) } + /* DWARF 5. */ + .debug_addr 0 : { *(.debug_addr) } + .debug_line_str 0 : { *(.debug_line_str) } + .debug_loclists 0 : { *(.debug_loclists) } + .debug_macro 0 : { *(.debug_macro) } + .debug_names 0 : { *(.debug_names) } + .debug_rnglists 0 : { *(.debug_rnglists) } + .debug_str_offsets 0 : { *(.debug_str_offsets) } + .debug_sup 0 : { *(.debug_sup) } + .note.gnu.arm.ident 0 : { KEEP (*(.note.gnu.arm.ident)) } + /DISCARD/ : { *(.note.GNU-stack) *(.gnu_debuglink) *(.gnu.lto_*) } } From 0eb3fc33dd3cccb0eb3667149b83d9e9267475c0 Mon Sep 17 00:00:00 2001 From: WillyJL <49810075+Willy-JL@users.noreply.github.com> Date: Mon, 31 Mar 2025 20:34:54 +0000 Subject: [PATCH 174/268] NFC: Fix NDEF parser for MIFARE Classic (#4153) * Add div() to API * Revert "Add div() to API" This reverts commit e03b5c42449365735ce3d2fc73a4e801c4d5f91f. * Use / and % * NFC: More MFC NDEF fixes * Simplify duplicated code in MFC data generator * NFC: Print NDEF hex data with pretty format * NFC: Consider NDEF strings with last \0 byte as text * Pretty Format: Add padding to last line to keep table width --------- Co-authored-by: hedger --- .../main/nfc/plugins/supported_cards/ndef.c | 61 +++++++++++-------- lib/nfc/helpers/nfc_data_generator.c | 36 +++-------- lib/toolbox/pretty_format.c | 8 ++- 3 files changed, 49 insertions(+), 56 deletions(-) diff --git a/applications/main/nfc/plugins/supported_cards/ndef.c b/applications/main/nfc/plugins/supported_cards/ndef.c index fb2c4da48..06982e111 100644 --- a/applications/main/nfc/plugins/supported_cards/ndef.c +++ b/applications/main/nfc/plugins/supported_cards/ndef.c @@ -22,6 +22,7 @@ #include #include +#include #define TAG "NDEF" @@ -181,30 +182,34 @@ static bool ndef_get(Ndef* ndef, size_t pos, size_t len, void* buf) { // So the first 93 (31*3) data blocks correspond to 128 real blocks. // Last 128 blocks are 8 sectors: 15 data blocks, 1 sector trailer. // So the last 120 (8*15) data blocks correspond to 128 real blocks. - div_t small_sector_data_blocks = div(pos, MF_CLASSIC_BLOCK_SIZE); + const size_t real_block_data_offset = pos % MF_CLASSIC_BLOCK_SIZE; + size_t small_sector_data_blocks = pos / MF_CLASSIC_BLOCK_SIZE; size_t large_sector_data_blocks = 0; - if(small_sector_data_blocks.quot > 93) { - large_sector_data_blocks = small_sector_data_blocks.quot - 93; - small_sector_data_blocks.quot = 93; + if(small_sector_data_blocks > 93) { + large_sector_data_blocks = small_sector_data_blocks - 93; + small_sector_data_blocks = 93; } - div_t small_sectors = div(small_sector_data_blocks.quot, 3); - size_t real_block = small_sectors.quot * 4 + small_sectors.rem; - if(small_sectors.quot >= 16) { + const size_t small_sector_block_offset = small_sector_data_blocks % 3; + const size_t small_sectors = small_sector_data_blocks / 3; + size_t real_block = small_sectors * 4 + small_sector_block_offset; + if(small_sectors >= 16) { real_block += 4; // Skip MAD2 } if(large_sector_data_blocks) { - div_t large_sectors = div(large_sector_data_blocks, 15); - real_block += large_sectors.quot * 16 + large_sectors.rem; + const size_t large_sector_block_offset = large_sector_data_blocks % 15; + const size_t large_sectors = large_sector_data_blocks / 15; + real_block += large_sectors * 16 + large_sector_block_offset; } - const uint8_t* cur = &ndef->mfc.blocks[real_block].data[small_sector_data_blocks.rem]; + const uint8_t* cur = &ndef->mfc.blocks[real_block].data[real_block_data_offset]; while(len) { size_t sector_trailer = mf_classic_get_sector_trailer_num_by_block(real_block); const uint8_t* end = &ndef->mfc.blocks[sector_trailer].data[0]; - size_t chunk_len = MIN((size_t)(end - cur), len); + const size_t chunk_len = MIN((size_t)(end - cur), len); memcpy(buf, cur, chunk_len); + buf += chunk_len; len -= chunk_len; if(len) { @@ -244,7 +249,9 @@ static inline bool is_printable(char c) { static bool is_text(const uint8_t* buf, size_t len) { for(size_t i = 0; i < len; i++) { - if(!is_printable(buf[i])) return false; + if(!is_printable(buf[i]) && !(buf[i] == '\0' && i == len - 1)) { + return false; + } } return true; } @@ -260,7 +267,7 @@ static bool ndef_dump(Ndef* ndef, const char* prefix, size_t pos, size_t len, bo for(size_t i = 0; i < len; i++) { char c; if(!ndef_get(ndef, pos + i, 1, &c)) return false; - if(!is_printable(c)) { + if(!is_printable(c) && !(c == '\0' && i == len - 1)) { furi_string_left(ndef->output, string_prev); force_hex = true; break; @@ -268,14 +275,18 @@ static bool ndef_dump(Ndef* ndef, const char* prefix, size_t pos, size_t len, bo furi_string_push_back(ndef->output, c); } } - if(force_hex) { - for(size_t i = 0; i < len; i++) { - uint8_t b; - if(!ndef_get(ndef, pos + i, 1, &b)) return false; - furi_string_cat_printf(ndef->output, "%02X ", b); + if(!force_hex) { + furi_string_cat(ndef->output, "\n"); + } else { + uint8_t buf[4]; + for(size_t i = 0; i < len; i += sizeof(buf)) { + uint8_t buf_len = MIN(sizeof(buf), len - i); + if(!ndef_get(ndef, pos + i, buf_len, &buf)) return false; + pretty_format_bytes_hex_canonical( + ndef->output, 4, PRETTY_FORMAT_FONT_MONOSPACE, buf, buf_len); + furi_string_cat(ndef->output, "\n"); } } - furi_string_cat(ndef->output, "\n"); return true; } @@ -285,9 +296,7 @@ static void if(!force_hex && is_text(buf, len)) { furi_string_cat_printf(ndef->output, "%.*s", len, (const char*)buf); } else { - for(size_t i = 0; i < len; i++) { - furi_string_cat_printf(ndef->output, "%02X ", ((const uint8_t*)buf)[i]); - } + pretty_format_bytes_hex_canonical(ndef->output, 4, PRETTY_FORMAT_FONT_MONOSPACE, buf, len); } furi_string_cat(ndef->output, "\n"); } @@ -582,7 +591,7 @@ bool ndef_parse_record( NdefTnf tnf, const char* type, uint8_t type_len) { - FURI_LOG_D(TAG, "payload type: %.*s len: %hu", type_len, type, len); + FURI_LOG_D(TAG, "payload type: %.*s len: %hu pos: %zu", type_len, type, len, pos); if(!len) { furi_string_cat(ndef->output, "Empty\n"); return true; @@ -887,13 +896,13 @@ static bool ndef_mfc_parse(const NfcDevice* device, FuriString* parsed_data) { for(uint8_t mad = 0; mad < COUNT_OF(mads); mad++) { const size_t block = mads[mad].block; const size_t sector = mf_classic_get_sector_by_block(block); - if(sector_count <= sector) break; // Skip this MAD if not present + if(sector_count <= sector) continue; // Skip this MAD if not present // Check MAD key const MfClassicSectorTrailer* sector_trailer = mf_classic_get_sector_trailer_by_sector(data, sector); const uint64_t sector_key_a = bit_lib_bytes_to_num_be( sector_trailer->key_a.data, COUNT_OF(sector_trailer->key_a.data)); - if(sector_key_a != mad_key) return false; + if(sector_key_a != mad_key) continue; // Find NDEF AIDs for(uint8_t aid_index = 0; aid_index < mads[mad].aid_count; aid_index++) { const uint8_t* aid = &data->block[block].data[2 + aid_index * AID_SIZE]; @@ -917,7 +926,7 @@ static bool ndef_mfc_parse(const NfcDevice* device, FuriString* parsed_data) { data_size = 93 + (sector_count - 32) * 15; } else { data_size = sector_count * 3; - if(sector_count >= 16) { + if(sector_count > 16) { data_size -= 3; // Skip MAD2 } } diff --git a/lib/nfc/helpers/nfc_data_generator.c b/lib/nfc/helpers/nfc_data_generator.c index 7914c1f7f..2143f0f5f 100644 --- a/lib/nfc/helpers/nfc_data_generator.c +++ b/lib/nfc/helpers/nfc_data_generator.c @@ -392,37 +392,15 @@ static void nfc_generate_mf_classic(NfcDevice* nfc_device, uint8_t uid_len, MfCl mf_classic_set_block_read(mfc_data, 0, &mfc_data->block[0]); + // Set every block to 0x00 uint16_t block_num = mf_classic_get_total_block_num(type); - if(type == MfClassicType4k) { - // Set every block to 0x00 - for(uint16_t i = 1; i < block_num; i++) { - if(mf_classic_is_sector_trailer(i)) { - nfc_generate_mf_classic_sector_trailer(mfc_data, i); - } else { - memset(&mfc_data->block[i].data, 0x00, 16); - } - mf_classic_set_block_read(mfc_data, i, &mfc_data->block[i]); - } - } else if(type == MfClassicType1k) { - // Set every block to 0x00 - for(uint16_t i = 1; i < block_num; i++) { - if(mf_classic_is_sector_trailer(i)) { - nfc_generate_mf_classic_sector_trailer(mfc_data, i); - } else { - memset(&mfc_data->block[i].data, 0x00, 16); - } - mf_classic_set_block_read(mfc_data, i, &mfc_data->block[i]); - } - } else if(type == MfClassicTypeMini) { - // Set every block to 0x00 - for(uint16_t i = 1; i < block_num; i++) { - if(mf_classic_is_sector_trailer(i)) { - nfc_generate_mf_classic_sector_trailer(mfc_data, i); - } else { - memset(&mfc_data->block[i].data, 0x00, 16); - } - mf_classic_set_block_read(mfc_data, i, &mfc_data->block[i]); + for(uint16_t i = 1; i < block_num; i++) { + if(mf_classic_is_sector_trailer(i)) { + nfc_generate_mf_classic_sector_trailer(mfc_data, i); + } else { + memset(&mfc_data->block[i].data, 0x00, MF_CLASSIC_BLOCK_SIZE); } + mf_classic_set_block_read(mfc_data, i, &mfc_data->block[i]); } nfc_generate_mf_classic_block_0( diff --git a/lib/toolbox/pretty_format.c b/lib/toolbox/pretty_format.c index f8319b69d..496738c4d 100644 --- a/lib/toolbox/pretty_format.c +++ b/lib/toolbox/pretty_format.c @@ -36,11 +36,17 @@ void pretty_format_bytes_hex_canonical( } const size_t begin_idx = i; - const size_t end_idx = MIN(i + num_places, data_size); + const size_t wrap_idx = i + num_places; + const size_t end_idx = MIN(wrap_idx, data_size); for(size_t j = begin_idx; j < end_idx; j++) { furi_string_cat_printf(result, "%02X ", data[j]); } + if(end_idx < wrap_idx) { + for(size_t j = end_idx; j < wrap_idx; j++) { + furi_string_cat_printf(result, " "); + } + } furi_string_push_back(result, '|'); From d6f9d7e994f7a6ecd2f81bcac75aeb2e214f801b Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Tue, 1 Apr 2025 00:18:09 +0300 Subject: [PATCH 175/268] upd changelog --- CHANGELOG.md | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fb3e03aa6..618589cf0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,5 @@ ## Main changes -- Current API: 83.2 +- Current API: 83.0 * SubGHz: Add **Revers RB2 / RB2M Protocol** (static 64 bit) **full support** with add manually (by @xMasterX) * SubGHz: **Fix Hollarm protocol with more verification** * SubGHz: **Fix GangQi protocol** (by @DoberBit and @mishamyte (who spent 2 weeks on this)) @@ -20,14 +20,17 @@ * SubGHz: Various bugfixes and experimental options (rolling counter overflow) (by @xMasterX) * Anims: Disable winter anims * NFC: mfclassic poller fix early key reuse in dictionary attack state machine (by @noproto) -* OFW PR 4164: Added Doom animation (by @doomwastaken) +* OFW: Fix DWARF dead code elimination and linking +* OFW: NFC: Fix crash on ISO15693-3 save when memory is empty or cannot be read +* OFW: Reduced ieee754 parser size +* OFW: Added Doom animation (by @doomwastaken) * OFW PR 4133: add nfc apdu cli command back (by @leommxj) -* OFW PR 4159: NFC: Support DESFire Transaction MAC file type (by @Willy-JL) -* OFW PR 4153: NFC: Fix NDEF parser for MIFARE Classic (by @Willy-JL) -* OFW PR 4160: GUI: Fix widget text scroll with 256+ lines (by @Willy-JL) -* OFW PR 4132: Infrared: Fix universals sending (by @Willy-JL) -* OFW PR 4149: HID Ble: increased stack and improvements (by @doomwastaken) -* OFW PR 4126: Stricter constness for const data (by @hedger) +* OFW: NFC: Support DESFire Transaction MAC file type (by @Willy-JL) +* OFW: NFC: Fix NDEF parser for MIFARE Classic (by @Willy-JL) +* OFW: GUI: Fix widget text scroll with 256+ lines (by @Willy-JL) +* OFW: Infrared: Fix universals sending (by @Willy-JL) +* OFW: HID Ble: increased stack and improvements (by @doomwastaken) +* OFW: Stricter constness for const data (by @hedger) * OFW PR 4017: Alarm improvements: Snooze, timeouts, and dismissing from the locked state (by @Astrrra) * OFW: fix: flipper detected before it was rebooted * OFW: NFC: FeliCa Protocol Expose Read Block API and Allow Specifying Service From 933134ed94428d1fb92081c970e0f824d12060a8 Mon Sep 17 00:00:00 2001 From: Anna Antonenko Date: Tue, 1 Apr 2025 14:35:11 +0400 Subject: [PATCH 176/268] [FL-3962] BadUSB arbitrary key combinations (#4156) * badusb: arbitrary modifier key combinations * badusb: format code * badusb: add const * Revert "badusb: add const" This reverts commit 6ae909fd5d3a5b614712fc92fadda98a6ced2893. --------- Co-authored-by: hedger --- .../main/bad_usb/helpers/ducky_script.c | 42 +++++++++++-------- .../main/bad_usb/helpers/ducky_script_i.h | 2 + .../bad_usb/helpers/ducky_script_keycodes.c | 28 +++++++++---- 3 files changed, 46 insertions(+), 26 deletions(-) diff --git a/applications/main/bad_usb/helpers/ducky_script.c b/applications/main/bad_usb/helpers/ducky_script.c index e71c03c48..99034028d 100644 --- a/applications/main/bad_usb/helpers/ducky_script.c +++ b/applications/main/bad_usb/helpers/ducky_script.c @@ -40,11 +40,8 @@ static const uint8_t numpad_keys[10] = { }; uint32_t ducky_get_command_len(const char* line) { - uint32_t len = strlen(line); - for(uint32_t i = 0; i < len; i++) { - if(line[i] == ' ') return i; - } - return 0; + char* first_space = strchr(line, ' '); + return first_space ? (first_space - line) : 0; } bool ducky_is_line_end(const char chr) { @@ -180,37 +177,46 @@ static bool ducky_string_next(BadUsbScript* bad_usb) { static int32_t ducky_parse_line(BadUsbScript* bad_usb, FuriString* line) { uint32_t line_len = furi_string_size(line); - const char* line_tmp = furi_string_get_cstr(line); + const char* line_cstr = furi_string_get_cstr(line); if(line_len == 0) { return SCRIPT_STATE_NEXT_LINE; // Skip empty lines } - FURI_LOG_D(WORKER_TAG, "line:%s", line_tmp); + FURI_LOG_D(WORKER_TAG, "line:%s", line_cstr); // Ducky Lang Functions - int32_t cmd_result = ducky_execute_cmd(bad_usb, line_tmp); + int32_t cmd_result = ducky_execute_cmd(bad_usb, line_cstr); if(cmd_result != SCRIPT_STATE_CMD_UNKNOWN) { return cmd_result; } // Mouse Keys - uint16_t key = ducky_get_mouse_keycode_by_name(line_tmp); + uint16_t key = ducky_get_mouse_keycode_by_name(line_cstr); if(key != HID_MOUSE_INVALID) { bad_usb->hid->mouse_press(bad_usb->hid_inst, key); bad_usb->hid->mouse_release(bad_usb->hid_inst, key); return 0; } - // Special keys + modifiers - key = ducky_get_keycode(bad_usb, line_tmp, false); - if(key == HID_KEYBOARD_NONE) { - return ducky_error(bad_usb, "No keycode defined for %s", line_tmp); - } - if((key & 0xFF00) != 0) { - // It's a modifier key - line_tmp = &line_tmp[ducky_get_command_len(line_tmp) + 1]; - key |= ducky_get_keycode(bad_usb, line_tmp, true); + // Parse chain of modifiers linked by spaces and hyphens + uint16_t modifiers = 0; + while(1) { + key = ducky_get_next_modifier_keycode_by_name(&line_cstr); + if(key == HID_KEYBOARD_NONE) break; + + modifiers |= key; + char next_char = *line_cstr; + if(next_char == ' ' || next_char == '-') line_cstr++; } + + // Main key + char next_char = *line_cstr; + uint16_t main_key = ducky_get_keycode_by_name(line_cstr); + if(!main_key && next_char) main_key = BADUSB_ASCII_TO_KEY(bad_usb, next_char); + key = modifiers | main_key; + + if(key == 0 && next_char) ducky_error(bad_usb, "No keycode defined for %s", line_cstr); + bad_usb->hid->kb_press(bad_usb->hid_inst, key); bad_usb->hid->kb_release(bad_usb->hid_inst, key); return 0; diff --git a/applications/main/bad_usb/helpers/ducky_script_i.h b/applications/main/bad_usb/helpers/ducky_script_i.h index fd95ecf58..2b15c2586 100644 --- a/applications/main/bad_usb/helpers/ducky_script_i.h +++ b/applications/main/bad_usb/helpers/ducky_script_i.h @@ -54,6 +54,8 @@ uint32_t ducky_get_command_len(const char* line); bool ducky_is_line_end(const char chr); +uint16_t ducky_get_next_modifier_keycode_by_name(const char** param); + uint16_t ducky_get_keycode_by_name(const char* param); uint16_t ducky_get_media_keycode_by_name(const char* param); diff --git a/applications/main/bad_usb/helpers/ducky_script_keycodes.c b/applications/main/bad_usb/helpers/ducky_script_keycodes.c index 7dd2e4d16..ce957bb4e 100644 --- a/applications/main/bad_usb/helpers/ducky_script_keycodes.c +++ b/applications/main/bad_usb/helpers/ducky_script_keycodes.c @@ -6,21 +6,16 @@ typedef struct { uint16_t keycode; } DuckyKey; -static const DuckyKey ducky_keys[] = { - {"CTRL-ALT", KEY_MOD_LEFT_CTRL | KEY_MOD_LEFT_ALT}, - {"CTRL-SHIFT", KEY_MOD_LEFT_CTRL | KEY_MOD_LEFT_SHIFT}, - {"ALT-SHIFT", KEY_MOD_LEFT_ALT | KEY_MOD_LEFT_SHIFT}, - {"ALT-GUI", KEY_MOD_LEFT_ALT | KEY_MOD_LEFT_GUI}, - {"GUI-SHIFT", KEY_MOD_LEFT_GUI | KEY_MOD_LEFT_SHIFT}, - {"GUI-CTRL", KEY_MOD_LEFT_GUI | KEY_MOD_LEFT_CTRL}, - +static const DuckyKey ducky_modifier_keys[] = { {"CTRL", KEY_MOD_LEFT_CTRL}, {"CONTROL", KEY_MOD_LEFT_CTRL}, {"SHIFT", KEY_MOD_LEFT_SHIFT}, {"ALT", KEY_MOD_LEFT_ALT}, {"GUI", KEY_MOD_LEFT_GUI}, {"WINDOWS", KEY_MOD_LEFT_GUI}, +}; +static const DuckyKey ducky_keys[] = { {"DOWNARROW", HID_KEYBOARD_DOWN_ARROW}, {"DOWN", HID_KEYBOARD_DOWN_ARROW}, {"LEFTARROW", HID_KEYBOARD_LEFT_ARROW}, @@ -119,6 +114,23 @@ static const DuckyKey ducky_mouse_keys[] = { {"WHEEL_CLICK", HID_MOUSE_BTN_WHEEL}, }; +uint16_t ducky_get_next_modifier_keycode_by_name(const char** param) { + const char* input_str = *param; + + for(size_t i = 0; i < COUNT_OF(ducky_modifier_keys); i++) { + size_t key_cmd_len = strlen(ducky_modifier_keys[i].name); + if((strncmp(input_str, ducky_modifier_keys[i].name, key_cmd_len) == 0)) { + char next_char_after_key = input_str[key_cmd_len]; + if(ducky_is_line_end(next_char_after_key) || (next_char_after_key == '-')) { + *param = &input_str[key_cmd_len]; + return ducky_modifier_keys[i].keycode; + } + } + } + + return HID_KEYBOARD_NONE; +} + uint16_t ducky_get_keycode_by_name(const char* param) { for(size_t i = 0; i < COUNT_OF(ducky_keys); i++) { size_t key_cmd_len = strlen(ducky_keys[i].name); From d34ff3310dd187e91f2c345fa7fa8b8136670551 Mon Sep 17 00:00:00 2001 From: WillyJL <49810075+Willy-JL@users.noreply.github.com> Date: Tue, 1 Apr 2025 11:02:12 +0000 Subject: [PATCH 177/268] JS: Update and fix docs, fix Number.toString() with decimals (#4168) * Update and fix JS docs This could really use some automation, atleast for API reference There are TypeScript definitions and typedocs, we don't need to be monkeys copying and reformatting this to API reference by hand * Fix bugged character * JS: Fix Number.toString() with decimals * Fix * Forgot this one * docs: mention per-view child format * Added @portasynthinca3 to docs' codeowners * Updated CODEOWNERS --------- Co-authored-by: Anna Antonenko Co-authored-by: hedger Co-authored-by: hedger --- .github/CODEOWNERS | 8 +- .../file_formats/SubGhzFileFormats.md | 2 +- documentation/js/js_badusb.md | 47 ++- documentation/js/js_builtin.md | 281 +++++++++++++++++- documentation/js/js_data_types.md | 4 +- .../js/js_developing_apps_using_js_sdk.md | 21 ++ documentation/js/js_flipper.md | 52 ++++ documentation/js/js_gpio.md | 32 +- documentation/js/js_gui.md | 41 ++- documentation/js/js_gui__byte_input.md | 32 ++ documentation/js/js_gui__file_picker.md | 20 ++ documentation/js/js_gui__icon.md | 32 ++ documentation/js/js_gui__text_box.md | 4 +- documentation/js/js_gui__text_input.md | 8 +- documentation/js/js_gui__widget.md | 13 + documentation/js/js_math.md | 20 +- documentation/js/js_serial.md | 46 ++- documentation/js/js_storage.md | 32 +- lib/mjs/mjs_primitive.c | 25 +- 19 files changed, 675 insertions(+), 45 deletions(-) create mode 100644 documentation/js/js_flipper.md create mode 100644 documentation/js/js_gui__byte_input.md create mode 100644 documentation/js/js_gui__file_picker.md create mode 100644 documentation/js/js_gui__icon.md diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index dcf964add..ef8b79370 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -18,7 +18,7 @@ /applications/main/gpio/ @skotopes @DrZlo13 @hedger @gsurkov @nminaylov /applications/main/ibutton/ @skotopes @DrZlo13 @hedger @gsurkov /applications/main/infrared/ @skotopes @DrZlo13 @hedger @gsurkov -/applications/main/nfc/ @skotopes @DrZlo13 @hedger @gsurkov @gornekich @Astrrra +/applications/main/nfc/ @skotopes @DrZlo13 @hedger @gsurkov @gornekich /applications/main/subghz/ @skotopes @DrZlo13 @hedger @gsurkov @Skorpionm /applications/main/u2f/ @skotopes @DrZlo13 @hedger @gsurkov @nminaylov @@ -38,7 +38,7 @@ /applications/system/storage_move_to_sd/ @skotopes @DrZlo13 @hedger @gsurkov @nminaylov /applications/system/js_app/ @skotopes @DrZlo13 @hedger @gsurkov @nminaylov @portasynthinca3 -/applications/debug/unit_tests/ @skotopes @DrZlo13 @hedger @gsurkov @nminaylov @gornekich @Astrrra @Skorpionm +/applications/debug/unit_tests/ @skotopes @DrZlo13 @hedger @gsurkov @nminaylov @gornekich @Skorpionm /applications/examples/example_thermo/ @skotopes @DrZlo13 @hedger @gsurkov @@ -49,7 +49,7 @@ /applications/main/infrared/resources/ @skotopes @DrZlo13 @hedger @gsurkov # Documentation -/documentation/ @skotopes @DrZlo13 @hedger @gsurkov +/documentation/ @skotopes @DrZlo13 @hedger @gsurkov @portasynthinca3 /scripts/toolchain/ @skotopes @DrZlo13 @hedger @gsurkov # Lib @@ -61,7 +61,7 @@ /lib/mbedtls/ @skotopes @DrZlo13 @hedger @gsurkov @nminaylov /lib/mjs/ @skotopes @DrZlo13 @hedger @gsurkov @nminaylov @portasynthinca3 /lib/nanopb/ @skotopes @DrZlo13 @hedger @gsurkov @nminaylov -/lib/nfc/ @skotopes @DrZlo13 @hedger @gsurkov @gornekich @Astrrra +/lib/nfc/ @skotopes @DrZlo13 @hedger @gsurkov @gornekich /lib/one_wire/ @skotopes @DrZlo13 @hedger @gsurkov /lib/subghz/ @skotopes @DrZlo13 @hedger @gsurkov @Skorpionm diff --git a/documentation/file_formats/SubGhzFileFormats.md b/documentation/file_formats/SubGhzFileFormats.md index c4d63835e..80047faf7 100644 --- a/documentation/file_formats/SubGhzFileFormats.md +++ b/documentation/file_formats/SubGhzFileFormats.md @@ -178,7 +178,7 @@ Data_RAW: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 DE 02 D3 54 D5 4C D2 C Frequency: 433920000 Preset: FuriHalSubGhzPresetCustom Custom_preset_module: CC1101 - Сustom_preset_data: 02 0D 03 07 08 32 0B 06 14 00 13 00 12 30 11 32 10 17 18 18 19 18 1D 91 1C 00 1B 07 20 FB 22 11 21 B6 00 00 00 C0 00 00 00 00 00 00 + Custom_preset_data: 02 0D 03 07 08 32 0B 06 14 00 13 00 12 30 11 32 10 17 18 18 19 18 1D 91 1C 00 1B 07 20 FB 22 11 21 B6 00 00 00 C0 00 00 00 00 00 00 Protocol: RAW RAW_Data: 29262 361 -68 2635 -66 24113 -66 11 ... RAW_Data: -424 205 -412 159 -412 381 -240 181 ... diff --git a/documentation/js/js_badusb.md b/documentation/js/js_badusb.md index d1d1e558d..79ae53f55 100644 --- a/documentation/js/js_badusb.md +++ b/documentation/js/js_badusb.md @@ -11,8 +11,9 @@ Start USB HID with optional parameters. Should be called before all other method Configuration object *(optional)*: - vid, pid (number): VID and PID values, both are mandatory -- mfr_name (string): Manufacturer name (32 ASCII characters max), optional -- prod_name (string): Product name (32 ASCII characters max), optional +- mfrName (string): Manufacturer name (32 ASCII characters max), optional +- prodName (string): Product name (32 ASCII characters max), optional +- layoutPath (string): Path to keyboard layout file, optional **Examples** ```js @@ -21,7 +22,7 @@ badusb.setup(); // Start USB HID with custom vid:pid = AAAA:BBBB, manufacturer and product strings not defined badusb.setup({ vid: 0xAAAA, pid: 0xBBBB }); // Start USB HID with custom vid:pid = AAAA:BBBB, manufacturer string = "Flipper Devices", product string = "Flipper Zero" -badusb.setup({ vid: 0xAAAA, pid: 0xBBBB, mfr_name: "Flipper Devices", prod_name: "Flipper Zero" }); +badusb.setup({ vid: 0xAAAA, pid: 0xBBBB, mfrName: "Flipper Devices", prodName: "Flipper Zero" }); ```
@@ -122,6 +123,45 @@ badusb.println("Hello, world!"); // print "Hello, world!" and press "ENTER" ```
+## altPrint() +Prints a string by Alt+Numpad method - works only on Windows! + +**Parameters** + +- A string to print +- *(optional)* delay between key presses + +**Examples** +```js +badusb.altPrint("Hello, world!"); // print "Hello, world!" +badusb.altPrint("Hello, world!", 100); // Add 100ms delay between key presses +``` +
+ +## altPrintln() +Same as `altPrint` but ended with "ENTER" press. + +**Parameters** + +- A string to print +- *(optional)* delay between key presses + +**Examples** +```js +badusb.altPrintln("Hello, world!"); // print "Hello, world!" and press "ENTER" +``` +
+ +## quit() +Releases usb, optional, but allows to interchange with usbdisk. + +**Examples** +```js +badusb.quit(); +usbdisk.start(...) +``` +
+ # Key names list {#js_badusb_keynames} ## Modifier keys @@ -159,3 +199,4 @@ badusb.println("Hello, world!"); // print "Hello, world!" and press "ENTER" | TAB | | | MENU | Context menu key | | Fx | F1-F24 keys | +| NUMx | NUM0-NUM9 keys | diff --git a/documentation/js/js_builtin.md b/documentation/js/js_builtin.md index 0a128859a..2d2f9417a 100644 --- a/documentation/js/js_builtin.md +++ b/documentation/js/js_builtin.md @@ -39,28 +39,277 @@ print("string1", "string2", 123); ```
-## console.log() - -
- -## console.warn() - -
- -## console.error() - -
- -## console.debug() +## Console object Same as `print`, but output to serial console only, with corresponding log level. +### console.log() +
-## to_string() +### console.warn() + +
+ +### console.error() + +
+ +### console.debug() + +
+ +## load() +Runs a JS file and returns value from it. + +**Parameters** +- The path to the file +- An optional object to use as the global scope while running this file + +**Examples** +```js +load("/ext/apps/Scripts/script.js"); +``` +
+ +## chr() +Convert an ASCII character number to string. + +**Examples** +```js +chr(65); // "A" +``` +
+ +## die() +Exit JavaScript with given message. + +**Examples** +```js +die("Some error occurred"); +``` +
+ +## parseInt() +Convert a string to number with an optional base. + +**Examples** +```js +parseInt("123"); // 123 +parseInt("7b", 16); // 123 +``` +
+ +## Number object + +### Number.toString() Convert a number to string with an optional base. **Examples** ```js -to_string(123) // "123" -to_string(123, 16) // "0x7b" +let num = 123; +num.toString(); // "123" +num.toString(16); // "0x7b" +``` +
+ +## ArrayBuffer object + +**Fields** + +- byteLength: The length of the buffer in bytes +
+ +### ArrayBuffer.slice() +Creates an `ArrayBuffer` that contains a sub-part of the buffer. + +**Parameters** +- The index to start the new buffer at +- An optional non-inclusive index of where to stop the new buffer + +**Examples** +```js +Uint8Array([1, 2, 3]).buffer.slice(0, 1) // ArrayBuffer([1]) +``` +
+ +## DataView objects +Wrappers around `ArrayBuffer` objects, with dedicated types such as: +- `Uint8Array` +- `Int8Array` +- `Uint16Array` +- `Int16Array` +- `Uint32Array` +- `Int32Array` + +**Fields** + +- byteLength: The length of the buffer in bytes +- length: The length of the buffer in typed elements +- buffer: The underlying `ArrayBuffer` +
+ +## Array object + +**Fields** + +- length: How many elements there are in the array +
+ +### Array.splice() +Removes elements from the array and returns them in a new array. + +**Parameters** +- The index to start taking elements from +- An optional count of how many elements to take + +**Examples** +```js +let arr = [1, 2, 3]; +arr.splice(1); // [2, 3] +arr; // [1] +``` +
+ +### Array.push() +Adds a value to the end of the array. + +**Examples** +```js +let arr = [1, 2]; +arr.push(3); +arr; // [1, 2, 3] +``` +
+ +## String object + +**Fields** + +- length: How many characters there are in the string +
+ +### String.charCodeAt() +Returns the character code at an index in the string. + +**Examples** +```js +"A".charCodeAt(0) // 65 +``` +
+ +### String.at() +Same as `String.charCodeAt()`. +
+ +### String.indexOf() +Return index of first occurrence of substr within the string or `-1` if not found. + +**Parameters** +- Substring to search for +- Optional index to start searching from + +**Examples** +```js +"Example".indexOf("amp") // 2 +``` +
+ +### String.slice() +Return a substring between two indices. + +**Parameters** +- The index to start the new string at +- An optional non-inclusive index of where to stop the new string + +**Examples** +```js +"Example".slice(2) // "ample" +``` +
+ +### String.toUpperCase() +Transforms the string to upper case. + +**Examples** +```js +"Example".toUpperCase() // "EXAMPLE" +``` +
+ +### String.toLowerCase() +Transforms the string to lower case. + +**Examples** +```js +"Example".toLowerCase() // "example" +``` +
+ +## __dirname +Path to the directory containing the current script. + +**Examples** +```js +print(__dirname); // /ext/apps/Scripts +``` +
+ +## __filename +Path to the current script file. + +**Examples** +```js +print(__filename); // /ext/apps/Scripts/path.js +``` +
+ +# SDK compatibility methods {#js_builtin_sdk_compatibility} + +## sdkCompatibilityStatus() +Checks compatibility between the script and the JS SDK that the firmware provides. + +**Returns** +- `"compatible"` if the script and the JS SDK are compatible +- `"firmwareTooOld"` if the expected major version is larger than the version of the firmware, or if the expected minor version is larger than the version of the firmware +- `"firmwareTooNew"` if the expected major version is lower than the version of the firmware + +**Examples** +```js +sdkCompatibilityStatus(0, 3); // "compatible" +``` +
+ +## isSdkCompatible() +Checks compatibility between the script and the JS SDK that the firmware provides in a boolean fashion. + +**Examples** +```js +isSdkCompatible(0, 3); // true +``` +
+ +## checkSdkCompatibility() +Asks the user whether to continue executing the script if the versions are not compatible. Does nothing if they are. + +**Examples** +```js +checkSdkCompatibility(0, 3); +``` +
+ +## doesSdkSupport() +Checks whether all of the specified extra features are supported by the interpreter. + +**Examples** +```js +doesSdkSupport(["gui-widget"]); // true +``` +
+ +## checkSdkFeatures() +Checks whether all of the specified extra features are supported by the interpreter, asking the user if they want to continue running the script if they're not. + +**Examples** +```js +checkSdkFeatures(["gui-widget"]); ``` diff --git a/documentation/js/js_data_types.md b/documentation/js/js_data_types.md index bd3bb1f42..2a94ba5d2 100644 --- a/documentation/js/js_data_types.md +++ b/documentation/js/js_data_types.md @@ -7,7 +7,7 @@ Here is a list of common data types used by mJS. - foreign — C function or data pointer - undefined - null -- object — a data structure with named fields -- array — special type of object, all items have indexes and equal types +- Object — a data structure with named fields +- Array — special type of object, all items have indexes and equal types - ArrayBuffer — raw data buffer - DataView — provides interface for accessing ArrayBuffer contents diff --git a/documentation/js/js_developing_apps_using_js_sdk.md b/documentation/js/js_developing_apps_using_js_sdk.md index 952993566..1a764e1df 100644 --- a/documentation/js/js_developing_apps_using_js_sdk.md +++ b/documentation/js/js_developing_apps_using_js_sdk.md @@ -70,6 +70,27 @@ The JS minifier reduces the size of JavaScript files by removing unnecessary cha However, it has a drawback — it can make debugging harder, as error messages in minified files are harder to read in larger applications. For this reason, it's recommended to disable the JS minifier during debugging and it's disabled by default. To enable it, set the `minify` parameter to `true` in the `fz-sdk.config.json5` file in your app folder. This will minify your JavaScript app before loading it onto Flipper Zero. +## Differences with normal Flipper JavaScript + +With the Flipper JavaScript SDK, you will be developing in **TypeScript**. This means that you get a better development experience, with more accurate code completion and warnings when variable types are incompatible, but it also means your code will be different from basic Flipper JS. + +Some things to look out for: +- Importing modules: + - Instead of `let module = require("module");` + - You will use `import * as module from "@flipperdevices/fz-sdk/module";` +- Multiple source code files: + - The Flipper JavaScript SDK does not yet support having multiple `.ts` files and importing them + - You can use `load()`, but this will not benefit from TypeScript type checking +- Casting values: + - Some Flipper JavaScript functions will return generic types + - For example `eventLoop.subscribe()` will run your callback with a generic `Item` type + - In some cases you might need to cast these values before using them, you can do this by: + - Inline casting: `item` + - Declare with new type: `let text = item as string;` + +When you upload the script to Flipper with `npm start`, it gets transpiled to normal JavaScript and optionally minified (see below). If you're looking to share your script with others, this is what you should give them to run. + + ## What's next? You've learned how to run and debug simple JavaScript apps. But how can you access Flipper Zero's hardware from your JS code? For that, you'll need to use JS modules — which we'll cover in the next guide. diff --git a/documentation/js/js_flipper.md b/documentation/js/js_flipper.md new file mode 100644 index 000000000..a4da23868 --- /dev/null +++ b/documentation/js/js_flipper.md @@ -0,0 +1,52 @@ +# Flipper module {#js_flipper} + +The module contains methods and values to query device information and properties. Call the `require` function to load the module before first using its methods: + +```js +let flipper = require("flipper"); +``` + +# Values + +## firmwareVendor +String representing the firmware installed on the device. +Original firmware reports `"flipperdevices"`. +Do **NOT** use this to check the presence or absence of features, refer to [other ways to check SDK compatibility](#js_builtin_sdk_compatibility). + +## jsSdkVersion +Version of the JavaScript SDK. +Do **NOT** use this to check the presence or absence of features, refer to [other ways to check SDK compatibility](#js_builtin_sdk_compatibility). + +
+ +--- + +# Methods + +## getModel() +Returns the device model. + +**Example** +```js +flipper.getModel(); // "Flipper Zero" +``` + +
+ +## getName() +Returns the name of the virtual dolphin. + +**Example** +```js +flipper.getName(); // "Fur1pp44" +``` + +
+ +## getBatteryCharge() +Returns the battery charge percentage. + +**Example** +```js +flipper.getBatteryCharge(); // 100 +``` diff --git a/documentation/js/js_gpio.md b/documentation/js/js_gpio.md index 59a5504b0..d058d7329 100644 --- a/documentation/js/js_gpio.md +++ b/documentation/js/js_gpio.md @@ -24,7 +24,7 @@ delay(1000); --- # API reference -## get +## get() Gets a `Pin` object that can be used to manage a pin. **Parameters** @@ -89,3 +89,33 @@ Attaches an interrupt to a pin configured with `direction: "in"` and An event loop `Contract` object that identifies the interrupt event source. The event does not produce any extra data. + +### Pin.isPwmSupported() +Determines whether this pin supports PWM. +If `false`, all other PWM-related methods on this pin will throw an error when called. + +**Returns** + +Boolean value. + +### Pin.pwmWrite() +Sets PWM parameters and starts the PWM. +Configures the pin with `{ direction: "out", outMode: "push_pull" }`. +Throws an error if PWM is not supported on this pin. + +**Parameters** + - `freq`: Frequency in Hz + - `duty`: Duty cycle in % + +### Pin.isPwmRunning() +Determines whether PWM is running. +Throws an error if PWM is not supported on this pin. + +**Returns** + +Boolean value. + +### Pin.pwmStop() +Stops PWM. +Does not restore previous pin configuration. +Throws an error if PWM is not supported on this pin. diff --git a/documentation/js/js_gui.md b/documentation/js/js_gui.md index 2bb2a2eee..afca1434e 100644 --- a/documentation/js/js_gui.md +++ b/documentation/js/js_gui.md @@ -173,6 +173,11 @@ triggered when the back key is pressed.
+### viewDispatcher.currentView +The `View` object currently being shown. + +
+ ## ViewFactory When you import a module implementing a view, a `ViewFactory` is instantiated. For example, in the example above, `loadingView`, `submenuView` and `emptyView` are view factories. @@ -183,8 +188,40 @@ Creates an instance of a `View`.
-### ViewFactory.make(props) -Creates an instance of a `View` and assigns initial properties from `props`. +### ViewFactory.makeWith(props, children) +Creates an instance of a `View` and assigns initial properties from `props` and optionally a list of children. **Parameters** - `props`: simple key-value object, e.g. `{ header: "Header" }` + - `children`: optional array of children, e.g. `[ { element: "button", button: "right", text: "Back" } ]` + +## View +When you call `ViewFactory.make()` or `ViewFactory.makeWith()`, a `View` is instantiated. For example, in the example above, `views.loading`, `views.demos` and `views.empty` are views. + +
+ +### View.set(property, value) +Assign value to property by name. + +**Parameters** + - `property`: name of the property to change + - `value`: value to assign to the property + +
+ +### View.addChild(child) +Adds a child to the `View`. + +**Parameters** + - `child`: the child to add, e.g. `{ element: "button", button: "right", text: "Back" }` + +The format of the `child` parameter depends on the type of View that you're working with. Look in the View documentation. + +### View.resetChildren() +Removes all children from the `View`. + +### View.setChildren(children) +Removes all previous children from the `View` and assigns new children. + +**Parameters** + - `children`: the array of new children, e.g. `[ { element: "button", button: "right", text: "Back" } ]` diff --git a/documentation/js/js_gui__byte_input.md b/documentation/js/js_gui__byte_input.md new file mode 100644 index 000000000..abbf1ccad --- /dev/null +++ b/documentation/js/js_gui__byte_input.md @@ -0,0 +1,32 @@ +# Byte input GUI view {#js_gui__byte_input} + +Displays a hexadecimal keyboard. + +Sample screenshot of the view + +```js +let eventLoop = require("event_loop"); +let gui = require("gui"); +let byteInputView = require("gui/byte_input"); +``` + +This module depends on the `gui` module, which in turn depends on the +`event_loop` module, so they **must** be imported in this order. It is also +recommended to conceptualize these modules first before using this one. + +## Example +For an example refer to the `gui.js` example script. + +## View props + +| Prop | Type | Description | +|-------------|--------|--------------------------------------------------| +| `length` | `number` | The length in bytes of the buffer to modify. | +| `header` | `string` | A single line of text that appears above the keyboard. | +| `defaultData` | `string` | Data to show by default. | + +## View events + +| Item | Type | Description | +|-------------|--------|--------------------------------------------------| +| `input` | `ArrayBuffer` | Fires when the user selects the "Save" button. | diff --git a/documentation/js/js_gui__file_picker.md b/documentation/js/js_gui__file_picker.md new file mode 100644 index 000000000..a0854f0de --- /dev/null +++ b/documentation/js/js_gui__file_picker.md @@ -0,0 +1,20 @@ +# File Picker GUI prompt {#js_gui__file_picker} + +Allows asking the user to select a file. +It is not GUI view like other JS GUI views, rather just a function that shows a prompt. + +# Example +For an example, refer to the `gui.js` example script. + +# API reference + +## pickFile() +Displays a file picker and returns the selected file, or undefined if cancelled. + +**Parameters** + - `basePath`: the path to start at + - `extension`: the file extension(s) to show (like `.sub`, `.iso|.img`, `*`) + +**Returns** + +A `string` path, or `undefined`. diff --git a/documentation/js/js_gui__icon.md b/documentation/js/js_gui__icon.md new file mode 100644 index 000000000..2c6c4c5b7 --- /dev/null +++ b/documentation/js/js_gui__icon.md @@ -0,0 +1,32 @@ +# GUI Icons {#js_gui__icon} + +Retrieves and loads icons for use with GUI views such as [Dialog](#js_gui__dialog). + +# Example +For an example, refer to the `gui.js` example script. + +# API reference + +## getBuiltin() +Gets a built-in firmware icon by its name. +Not all icons are supported, currently only `"DolphinWait_59x54"` and `"js_script_10px"` are available. + +**Parameters** + - `icon`: name of the icon + +**Returns** + +An `IconData` object. + +
+ +## loadFxbm() +Loads a `.fxbm` icon (XBM Flipper sprite, from `flipperzero-game-engine`) from file. +It will be automatically unloaded when the script exits. + +**Parameters** + - `path`: path to the `.fxbm` file + +**Returns** + +An `IconData` object. diff --git a/documentation/js/js_gui__text_box.md b/documentation/js/js_gui__text_box.md index cdbeec7b7..4c94b8c3c 100644 --- a/documentation/js/js_gui__text_box.md +++ b/documentation/js/js_gui__text_box.md @@ -21,4 +21,6 @@ For an example, refer to the `gui.js` example script. | Prop | Type | Description | |----------|---------|------------------------------------| -| `text` | `string`| Text to show in the text box. | +| `text` | `string`| Text to show in the text box. | +| `font` | `string`| The font to display the text in (`"text"` or `"hex"`). | +| `focus` | `string`| The initial focus of the text box (`"start"` or `"end"`). | diff --git a/documentation/js/js_gui__text_input.md b/documentation/js/js_gui__text_input.md index 3b8aafaad..c63297c12 100644 --- a/documentation/js/js_gui__text_input.md +++ b/documentation/js/js_gui__text_input.md @@ -21,9 +21,11 @@ For an example, refer to the `gui.js` example script. | Prop | Type | Description | |-------------|--------|--------------------------------------------------| -| `minLength` | `number` | The shortest allowed text length. | -| `maxLength` | `number` | The longest allowed text length.
Default: `32` | -| `header` | `string` | A single line of text that appears above the keyboard. | +| `minLength` | `number` | The shortest allowed text length. | +| `maxLength` | `number` | The longest allowed text length.
Default: `32` | +| `header` | `string` | A single line of text that appears above the keyboard. | +| `defaultText` | `string` | Text to show by default. | +| `defaultTextClear` | `boolean` | Whether to clear the default text on next character typed. | ## View events diff --git a/documentation/js/js_gui__widget.md b/documentation/js/js_gui__widget.md index 2c31327d2..9ea3e4dfa 100644 --- a/documentation/js/js_gui__widget.md +++ b/documentation/js/js_gui__widget.md @@ -22,3 +22,16 @@ This view does not have any props. ## Children This view has the elements as its children. +Elements are objects with properties to define them, in the form `{ element: "type", ...properties }` (e.g. `{ element: "button", button: "right", text: "Back" }`). + +| **Element Type** | **Properties** | **Description** | +|------------------|----------------|------------------------------------------------| +| `string_multiline` | `x` (number), `y` (number)
`align` ((`"t"`, `"c"`, `"b"`) + (`"l"`, `"m"`, `"r"`))
`font` (`"primary"`, `"secondary"`, `"keyboard"`, `"big_numbers"`)
`text` (string) | String of text that can span multiple lines. | +| `string` | `x` (number), `y` (number)
`align` ((`"t"`, `"c"`, `"b"`) + (`"l"`, `"m"`, `"r"`))
`font` (`"primary"`, `"secondary"`, `"keyboard"`, `"big_numbers"`)
`text` (string) | String of text. | +| `text_box` | `x` (number), `y` (number)
`w` (number), `h` (number)
`align` ((`"t"`, `"c"`, `"b"`) + (`"l"`, `"m"`, `"r"`))
`text` (string)
`stripToDots` (boolean) | Box of with text that can be scrolled vertically. | +| `text_scroll` | `x` (number), `y` (number)
`w` (number), `h` (number)
`text` (string) | Text that can be scrolled vertically. | +| `button` | `text` (string)
`button` (`"left"`, `"center"`, `"right"`) | Button at the bottom of the screen. | +| `icon` | `x` (number), `y` (number)
`iconData` ([IconData](#js_gui__icon)) | Display an icon. | +| `rect` | `x` (number), `y` (number)
`w` (number), `h` (number)
`radius` (number), `fill` (boolean) | Draw a rectangle, optionally rounded and filled. | +| `circle` | `x` (number), `y` (number)
`radius` (number), `fill` (boolean) | Draw a circle, optionally filled. | +| `line` | `x1` (number), `y1` (number)
`x2` (number), `y2` (number) | Draw a line between 2 points. | diff --git a/documentation/js/js_math.md b/documentation/js/js_math.md index 78357bbd7..fd7bede17 100644 --- a/documentation/js/js_math.md +++ b/documentation/js/js_math.md @@ -271,13 +271,29 @@ math.floor(45.95); // 45
+## log() +Return the natural logarithm of x. + +**Parameters** +- x: A number + +**Returns** + +The natural logarithm of `x`, as in `ln(x)` where `e` is the base of the natural logarithm. + +**Example** +```js +math.log(1); // 0 +math.log(3); // 1.0986122886681098 +``` + ## isEqual() -Return true if the difference between numbers `a` and `b` is less than the specified parameter `e`. +Return true if the difference between numbers `a` and `b` is less than the specified `tolerance`. **Parameters** - a: A number a - b: A number b -- e: An epsilon parameter +- tolerance: How much difference is allowed between the numbers to be considered equal **Returns** diff --git a/documentation/js/js_serial.md b/documentation/js/js_serial.md index 01a72b97b..d06c799ac 100644 --- a/documentation/js/js_serial.md +++ b/documentation/js/js_serial.md @@ -10,8 +10,15 @@ Configure serial port. Should be called before all other methods. **Parameters** -- Serial port name (usart, lpuart) +- Serial port name (`"usart"`, `"lpuart"`) - Baudrate +- Optional framing configuration object (e.g. `{ dataBits: "8", parity: "even", stopBits: "1" }`): + - `dataBits`: `"6"`, `"7"`, `"8"`, `"9"` + - 6 data bits can only be selected when parity is enabled (even or odd) + - 9 data bits can only be selected when parity is disabled (none) + - `parity`: `"none"`, `"even"`, `"odd"` + - `stopBits`: `"0.5"`, `"1"`, `"1.5"`, `"2"` + - LPUART only supports whole stop bit lengths (i.e. 1 and 2 but not 0.5 and 1.5) **Example** @@ -69,7 +76,7 @@ Read from serial port until line break character. **Parameters** -*(optional)* Timeout value in ms. +- *(optional)* Timeout value in ms. **Returns** @@ -84,6 +91,26 @@ serial.readln(5000); // Read with 5s timeout
+## readAny() +Read any available amount of data from serial port, can help avoid starving your loop with small reads. + +**Parameters** + +- *(optional)* Timeout value in ms + +**Returns** + +A sting of received characters or undefined if nothing was received before timeout. + +**Example** + +```js +serial.readAny(); // Read without timeout +serial.readAny(5000); // Read with 5s timeout +``` + +
+ ## readBytes() Read from serial port until line break character. @@ -125,8 +152,21 @@ Index of matched pattern in input patterns list, undefined if nothing was found. ```js // Wait for root shell prompt with 1s timeout, returns 0 if it was received before timeout, undefined if not -serial.expect("# ", 1000); +serial.expect("# ", 1000); // Infinitely wait for one of two strings, should return 0 if the first string got matched, 1 if the second one serial.expect([": not found", "Usage: "]); +``` + +
+ +## end() +Deinitializes serial port, allowing multiple initializations per script run. + +**Example** + +```js +serial.end(); +// Configure LPUART port with baudrate = 115200 +serial.setup("lpuart", 115200); ``` \ No newline at end of file diff --git a/documentation/js/js_storage.md b/documentation/js/js_storage.md index acfa4e331..0e435be5b 100644 --- a/documentation/js/js_storage.md +++ b/documentation/js/js_storage.md @@ -52,7 +52,7 @@ File information structure. ## File -This class implements methods for working with file. To get an object of the File class, use the `OpenFile` method. +This class implements methods for working with file. To get an object of the File class, use the `openFile` method.
@@ -186,6 +186,36 @@ Copies bytes from the R/W pointer in the current file to the R/W pointer in anot # Methods +## openFile() + +Opens a file. + +**Parameters** + +- path: The path to the file +- accessMode: How to access the file (`"r"`, `"w"` or `"rw"`) +- openMode: How to open the file (`"open_existing"`, `"open_always"`, `"open_append"`, `"create_new"` or `"create_always"`) + +**Returns** + +A `File` object on success, `undefined` otherwise. + +
+ +## fileExists() + +Detects whether a file exists. + +**Parameters** + +- path: The path to the file + +**Returns** + +`true` if file exists, `false` otherwise. + +
+ ## arePathsEqual() Determines whether the two paths are equivalent. Respects filesystem-defined path equivalence rules. diff --git a/lib/mjs/mjs_primitive.c b/lib/mjs/mjs_primitive.c index e73ae892d..20d2a8c66 100644 --- a/lib/mjs/mjs_primitive.c +++ b/lib/mjs/mjs_primitive.c @@ -9,6 +9,8 @@ #include "mjs_string_public.h" #include "mjs_util.h" +#include + mjs_val_t mjs_mk_null(void) { return MJS_NULL; } @@ -94,7 +96,7 @@ int mjs_is_boolean(mjs_val_t v) { } #define MJS_IS_POINTER_LEGIT(n) \ - (((n)&MJS_TAG_MASK) == 0 || ((n)&MJS_TAG_MASK) == (~0 & MJS_TAG_MASK)) + (((n) & MJS_TAG_MASK) == 0 || ((n) & MJS_TAG_MASK) == (~0 & MJS_TAG_MASK)) MJS_PRIVATE mjs_val_t mjs_pointer_to_value(struct mjs* mjs, void* p) { uint64_t n = ((uint64_t)(uintptr_t)p); @@ -165,13 +167,13 @@ MJS_PRIVATE void mjs_number_to_string(struct mjs* mjs) { mjs_val_t ret = MJS_UNDEFINED; mjs_val_t base_v = MJS_UNDEFINED; int32_t base = 10; - int32_t num; + double num; /* get number from `this` */ if(!mjs_check_arg(mjs, -1 /*this*/, "this", MJS_TYPE_NUMBER, NULL)) { goto clean; } - num = mjs_get_int32(mjs, mjs->vals.this_obj); + num = mjs_get_double(mjs, mjs->vals.this_obj); if(mjs_nargs(mjs) >= 1) { /* get base from arg 0 */ @@ -181,9 +183,20 @@ MJS_PRIVATE void mjs_number_to_string(struct mjs* mjs) { base = mjs_get_int(mjs, base_v); } - char tmp_str[] = "-2147483648"; - itoa(num, tmp_str, base); - ret = mjs_mk_string(mjs, tmp_str, ~0, true); + if(base != 10 || floor(num) == num) { + char tmp_str[] = "-2147483648"; + itoa((int32_t)num, tmp_str, base); + ret = mjs_mk_string(mjs, tmp_str, ~0, true); + } else { + char tmp_str[] = "2.22514337200000e-308"; + snprintf(tmp_str, sizeof(tmp_str), "%.*g", DBL_DIG, num); + size_t len = strlen(tmp_str); + while(len && tmp_str[len - 1] == '0') { + len--; + } + tmp_str[len] = '\0'; + ret = mjs_mk_string(mjs, tmp_str, ~0, true); + } clean: mjs_return(mjs, ret); From 0103b8c7c55a73384b272acf0bb26f86121c7ceb Mon Sep 17 00:00:00 2001 From: Anna Antonenko Date: Tue, 1 Apr 2025 18:50:08 +0400 Subject: [PATCH 178/268] [FL-3961] New JS value destructuring (#4135) * js: value destructuring and tests * js: temporary fix to see size impact * js_val: reduce code size 1 * i may be stupid. * test: js_value args * Revert "js: temporary fix to see size impact" This reverts commit f51d726dbafc4300d3552020de1c3b8f9ecd3ac1. * pvs: silence warnings * style: formatting * pvs: silence warnings? * pvs: silence warnings?? * js_value: redesign declaration types for less code * js: temporary fix to see size impact * style: formatting * pvs: fix helpful warnings * js_value: reduce .rodata size * pvs: fix helpful warning * js_value: reduce code size 1 * fix build error * style: format * Revert "js: temporary fix to see size impact" This reverts commit d6a46f01794132e882e03fd273dec24386a4f8ba. * style: format --------- Co-authored-by: hedger --- .../debug/unit_tests/tests/js/js_test.c | 307 ++++++++++++++++++ applications/debug/unit_tests/tests/minunit.h | 4 + .../debug/unit_tests/unit_test_api_table_i.h | 13 + applications/system/js_app/application.fam | 1 + applications/system/js_app/js_modules.h | 1 + applications/system/js_app/js_value.c | 291 +++++++++++++++++ applications/system/js_app/js_value.h | 212 ++++++++++++ 7 files changed, 829 insertions(+) create mode 100644 applications/system/js_app/js_value.c create mode 100644 applications/system/js_app/js_value.h diff --git a/applications/debug/unit_tests/tests/js/js_test.c b/applications/debug/unit_tests/tests/js/js_test.c index af590e899..dd695a3a1 100644 --- a/applications/debug/unit_tests/tests/js/js_test.c +++ b/applications/debug/unit_tests/tests/js/js_test.c @@ -6,9 +6,12 @@ #include #include +#include #include +#define TAG "JsUnitTests" + #define JS_SCRIPT_PATH(name) EXT_PATH("unit_tests/js/" name ".js") typedef enum { @@ -73,7 +76,311 @@ MU_TEST(js_test_storage) { js_test_run(JS_SCRIPT_PATH("storage")); } +static void js_value_test_compatibility_matrix(struct mjs* mjs) { + static const JsValueType types[] = { + JsValueTypeAny, + JsValueTypeAnyArray, + JsValueTypeAnyObject, + JsValueTypeFunction, + JsValueTypeRawPointer, + JsValueTypeInt32, + JsValueTypeDouble, + JsValueTypeString, + JsValueTypeBool, + }; + + mjs_val_t values[] = { + mjs_mk_undefined(), + mjs_mk_foreign(mjs, (void*)0xDEADBEEF), + mjs_mk_array(mjs), + mjs_mk_object(mjs), + mjs_mk_number(mjs, 123.456), + mjs_mk_string(mjs, "test", ~0, false), + mjs_mk_boolean(mjs, true), + }; + +// for proper matrix formatting and better readability +#define YES true +#define NO_ false + static const bool success_matrix[COUNT_OF(types)][COUNT_OF(values)] = { + // types: + {YES, YES, YES, YES, YES, YES, YES}, // any + {NO_, NO_, YES, NO_, NO_, NO_, NO_}, // array + {NO_, NO_, YES, YES, NO_, NO_, NO_}, // obj + {NO_, NO_, NO_, NO_, NO_, NO_, NO_}, // fn + {NO_, YES, NO_, NO_, NO_, NO_, NO_}, // ptr + {NO_, NO_, NO_, NO_, YES, NO_, NO_}, // int32 + {NO_, NO_, NO_, NO_, YES, NO_, NO_}, // double + {NO_, NO_, NO_, NO_, NO_, YES, NO_}, // str + {NO_, NO_, NO_, NO_, NO_, NO_, YES}, // bool + // + //und ptr arr obj num str bool <- values + }; +#undef NO_ +#undef YES + + for(size_t i = 0; i < COUNT_OF(types); i++) { + for(size_t j = 0; j < COUNT_OF(values); j++) { + const JsValueDeclaration declaration = { + .type = types[i], + .n_children = 0, + }; + // we only care about the status, not the result. double has the largest size out of + // all the results + uint8_t result[sizeof(double)]; + JsValueParseStatus status; + JS_VALUE_PARSE( + mjs, + JS_VALUE_PARSE_SOURCE_VALUE(&declaration), + JsValueParseFlagNone, + &status, + &values[j], + result); + if((status == JsValueParseStatusOk) != success_matrix[i][j]) { + FURI_LOG_E(TAG, "type %zu, value %zu", i, j); + mu_fail("see serial logs"); + } + } + } +} + +static void js_value_test_literal(struct mjs* mjs) { + static const JsValueType types[] = { + JsValueTypeAny, + JsValueTypeAnyArray, + JsValueTypeAnyObject, + }; + + mjs_val_t values[] = { + mjs_mk_undefined(), + mjs_mk_array(mjs), + mjs_mk_object(mjs), + }; + + mu_assert_int_eq(COUNT_OF(types), COUNT_OF(values)); + for(size_t i = 0; i < COUNT_OF(types); i++) { + const JsValueDeclaration declaration = { + .type = types[i], + .n_children = 0, + }; + mjs_val_t result; + JsValueParseStatus status; + JS_VALUE_PARSE( + mjs, + JS_VALUE_PARSE_SOURCE_VALUE(&declaration), + JsValueParseFlagNone, + &status, + &values[i], + &result); + mu_assert_int_eq(JsValueParseStatusOk, status); + mu_assert(result == values[i], "wrong result"); + } +} + +static void js_value_test_primitive( + struct mjs* mjs, + JsValueType type, + const void* c_value, + size_t c_value_size, + mjs_val_t js_val) { + const JsValueDeclaration declaration = { + .type = type, + .n_children = 0, + }; + uint8_t result[c_value_size]; + JsValueParseStatus status; + JS_VALUE_PARSE( + mjs, + JS_VALUE_PARSE_SOURCE_VALUE(&declaration), + JsValueParseFlagNone, + &status, + &js_val, + result); + mu_assert_int_eq(JsValueParseStatusOk, status); + if(type == JsValueTypeString) { + const char* result_str = *(const char**)&result; + mu_assert_string_eq(c_value, result_str); + } else { + mu_assert_mem_eq(c_value, result, c_value_size); + } +} + +static void js_value_test_primitives(struct mjs* mjs) { + int32_t i32 = 123; + js_value_test_primitive(mjs, JsValueTypeInt32, &i32, sizeof(i32), mjs_mk_number(mjs, i32)); + + double dbl = 123.456; + js_value_test_primitive(mjs, JsValueTypeDouble, &dbl, sizeof(dbl), mjs_mk_number(mjs, dbl)); + + const char* str = "test"; + js_value_test_primitive( + mjs, JsValueTypeString, str, strlen(str) + 1, mjs_mk_string(mjs, str, ~0, false)); + + bool boolean = true; + js_value_test_primitive( + mjs, JsValueTypeBool, &boolean, sizeof(boolean), mjs_mk_boolean(mjs, boolean)); +} + +static uint32_t + js_value_test_enum(struct mjs* mjs, const JsValueDeclaration* decl, const char* value) { + mjs_val_t str = mjs_mk_string(mjs, value, ~0, false); + uint32_t result; + JsValueParseStatus status; + JS_VALUE_PARSE( + mjs, JS_VALUE_PARSE_SOURCE_VALUE(decl), JsValueParseFlagNone, &status, &str, &result); + if(status != JsValueParseStatusOk) return 0; + return result; +} + +static void js_value_test_enums(struct mjs* mjs) { + static const JsValueEnumVariant enum_1_variants[] = { + {"variant 1", 1}, + {"variant 2", 2}, + {"variant 3", 3}, + }; + static const JsValueDeclaration enum_1 = JS_VALUE_ENUM(uint32_t, enum_1_variants); + + static const JsValueEnumVariant enum_2_variants[] = { + {"read", 4}, + {"write", 8}, + }; + static const JsValueDeclaration enum_2 = JS_VALUE_ENUM(uint32_t, enum_2_variants); + + mu_assert_int_eq(1, js_value_test_enum(mjs, &enum_1, "variant 1")); + mu_assert_int_eq(2, js_value_test_enum(mjs, &enum_1, "variant 2")); + mu_assert_int_eq(3, js_value_test_enum(mjs, &enum_1, "variant 3")); + mu_assert_int_eq(0, js_value_test_enum(mjs, &enum_1, "not a thing")); + + mu_assert_int_eq(0, js_value_test_enum(mjs, &enum_2, "variant 1")); + mu_assert_int_eq(0, js_value_test_enum(mjs, &enum_2, "variant 2")); + mu_assert_int_eq(0, js_value_test_enum(mjs, &enum_2, "variant 3")); + mu_assert_int_eq(0, js_value_test_enum(mjs, &enum_2, "not a thing")); + mu_assert_int_eq(4, js_value_test_enum(mjs, &enum_2, "read")); + mu_assert_int_eq(8, js_value_test_enum(mjs, &enum_2, "write")); +} + +static void js_value_test_object(struct mjs* mjs) { + static const JsValueDeclaration int_decl = JS_VALUE_SIMPLE(JsValueTypeInt32); + + static const JsValueDeclaration str_decl = JS_VALUE_SIMPLE(JsValueTypeString); + + static const JsValueEnumVariant enum_variants[] = { + {"variant 1", 1}, + {"variant 2", 2}, + {"variant 3", 3}, + }; + static const JsValueDeclaration enum_decl = JS_VALUE_ENUM(uint32_t, enum_variants); + + static const JsValueObjectField fields[] = { + {"int", &int_decl}, + {"str", &str_decl}, + {"enum", &enum_decl}, + }; + static const JsValueDeclaration object_decl = JS_VALUE_OBJECT(fields); + + mjs_val_t object = mjs_mk_object(mjs); + JS_ASSIGN_MULTI(mjs, object) { + JS_FIELD("str", mjs_mk_string(mjs, "Helloooo!", ~0, false)); + JS_FIELD("int", mjs_mk_number(mjs, 123)); + JS_FIELD("enum", mjs_mk_string(mjs, "variant 2", ~0, false)); + } + + const char* result_str; + int32_t result_int; + uint32_t result_enum; + JsValueParseStatus status; + JS_VALUE_PARSE( + mjs, + JS_VALUE_PARSE_SOURCE_VALUE(&object_decl), + JsValueParseFlagNone, + &status, + &object, + &result_int, + &result_str, + &result_enum); + mu_assert_int_eq(JsValueParseStatusOk, status); + mu_assert_string_eq("Helloooo!", result_str); + mu_assert_int_eq(123, result_int); + mu_assert_int_eq(2, result_enum); +} + +static void js_value_test_default(struct mjs* mjs) { + static const JsValueDeclaration int_decl = + JS_VALUE_SIMPLE_W_DEFAULT(JsValueTypeInt32, int32_val, 123); + static const JsValueDeclaration str_decl = JS_VALUE_SIMPLE(JsValueTypeString); + + static const JsValueObjectField fields[] = { + {"int", &int_decl}, + {"str", &str_decl}, + }; + static const JsValueDeclaration object_decl = JS_VALUE_OBJECT(fields); + + mjs_val_t object = mjs_mk_object(mjs); + JS_ASSIGN_MULTI(mjs, object) { + JS_FIELD("str", mjs_mk_string(mjs, "Helloooo!", ~0, false)); + JS_FIELD("int", mjs_mk_undefined()); + } + + const char* result_str; + int32_t result_int; + JsValueParseStatus status; + JS_VALUE_PARSE( + mjs, + JS_VALUE_PARSE_SOURCE_VALUE(&object_decl), + JsValueParseFlagNone, + &status, + &object, + &result_int, + &result_str); + mu_assert_string_eq("Helloooo!", result_str); + mu_assert_int_eq(123, result_int); +} + +static void js_value_test_args_fn(struct mjs* mjs) { + static const JsValueDeclaration arg_list[] = { + JS_VALUE_SIMPLE(JsValueTypeInt32), + JS_VALUE_SIMPLE(JsValueTypeInt32), + JS_VALUE_SIMPLE(JsValueTypeInt32), + }; + static const JsValueArguments args = JS_VALUE_ARGS(arg_list); + + int32_t a, b, c; + JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &args, &a, &b, &c); + + mu_assert_int_eq(123, a); + mu_assert_int_eq(456, b); + mu_assert_int_eq(-420, c); +} + +static void js_value_test_args(struct mjs* mjs) { + mjs_val_t function = MJS_MK_FN(js_value_test_args_fn); + + mjs_val_t result; + mjs_val_t args[] = { + mjs_mk_number(mjs, 123), + mjs_mk_number(mjs, 456), + mjs_mk_number(mjs, -420), + }; + mu_assert_int_eq( + MJS_OK, mjs_apply(mjs, &result, function, MJS_UNDEFINED, COUNT_OF(args), args)); +} + +MU_TEST(js_value_test) { + struct mjs* mjs = mjs_create(NULL); + + js_value_test_compatibility_matrix(mjs); + js_value_test_literal(mjs); + js_value_test_primitives(mjs); + js_value_test_enums(mjs); + js_value_test_object(mjs); + js_value_test_default(mjs); + js_value_test_args(mjs); + + mjs_destroy(mjs); +} + MU_TEST_SUITE(test_js) { + MU_RUN_TEST(js_value_test); MU_RUN_TEST(js_test_basic); MU_RUN_TEST(js_test_math); MU_RUN_TEST(js_test_event_loop); diff --git a/applications/debug/unit_tests/tests/minunit.h b/applications/debug/unit_tests/tests/minunit.h index 9ca3bb403..c854c4673 100644 --- a/applications/debug/unit_tests/tests/minunit.h +++ b/applications/debug/unit_tests/tests/minunit.h @@ -396,6 +396,8 @@ void minunit_printf_warning(const char* format, ...); return; \ } else { minunit_print_progress(); }) +//-V:mu_assert_string_eq:526, 547 + #define mu_assert_string_eq(expected, result) \ MU__SAFE_BLOCK( \ const char* minunit_tmp_e = expected; const char* minunit_tmp_r = result; \ @@ -416,6 +418,8 @@ void minunit_printf_warning(const char* format, ...); return; \ } else { minunit_print_progress(); }) +//-V:mu_assert_mem_eq:526 + #define mu_assert_mem_eq(expected, result, size) \ MU__SAFE_BLOCK( \ const void* minunit_tmp_e = expected; const void* minunit_tmp_r = result; \ diff --git a/applications/debug/unit_tests/unit_test_api_table_i.h b/applications/debug/unit_tests/unit_test_api_table_i.h index 10b089022..4f0e4dec9 100644 --- a/applications/debug/unit_tests/unit_test_api_table_i.h +++ b/applications/debug/unit_tests/unit_test_api_table_i.h @@ -8,6 +8,7 @@ #include #include #include +#include static constexpr auto unit_tests_api_table = sort(create_array_t( API_METHOD(resource_manifest_reader_alloc, ResourceManifestReader*, (Storage*)), @@ -38,4 +39,16 @@ static constexpr auto unit_tests_api_table = sort(create_array_t( JsThread*, (const char* script_path, JsThreadCallback callback, void* context)), API_METHOD(js_thread_stop, void, (JsThread * worker)), + API_METHOD(js_value_buffer_size, size_t, (const JsValueParseDeclaration declaration)), + API_METHOD( + js_value_parse, + JsValueParseStatus, + (struct mjs * mjs, + const JsValueParseDeclaration declaration, + JsValueParseFlag flags, + mjs_val_t* buffer, + size_t buf_size, + mjs_val_t* source, + size_t n_c_vals, + ...)), API_VARIABLE(PB_Main_msg, PB_Main_msg_t))); diff --git a/applications/system/js_app/application.fam b/applications/system/js_app/application.fam index db1521b9d..66ec221ec 100644 --- a/applications/system/js_app/application.fam +++ b/applications/system/js_app/application.fam @@ -11,6 +11,7 @@ App( "js_app.c", "js_modules.c", "js_thread.c", + "js_value.c", "plugin_api/app_api_table.cpp", "views/console_view.c", "modules/js_flipper.c", diff --git a/applications/system/js_app/js_modules.h b/applications/system/js_app/js_modules.h index 892e43d4e..fb1cca915 100644 --- a/applications/system/js_app/js_modules.h +++ b/applications/system/js_app/js_modules.h @@ -2,6 +2,7 @@ #include #include "js_thread_i.h" +#include "js_value.h" #include #include #include diff --git a/applications/system/js_app/js_value.c b/applications/system/js_app/js_value.c new file mode 100644 index 000000000..6ce1cf37a --- /dev/null +++ b/applications/system/js_app/js_value.c @@ -0,0 +1,291 @@ +#include "js_value.h" +#include + +#ifdef APP_UNIT_TESTS +#define JS_VAL_DEBUG +#endif + +size_t js_value_buffer_size(const JsValueParseDeclaration declaration) { + if(declaration.source == JsValueParseSourceValue) { + const JsValueDeclaration* value_decl = declaration.value_decl; + JsValueType type = value_decl->type & JsValueTypeMask; + + if(type == JsValueTypeString) return 1; + + if(type == JsValueTypeObject) { + size_t total = 0; + for(size_t i = 0; i < value_decl->n_children; i++) + total += js_value_buffer_size( + JS_VALUE_PARSE_SOURCE_VALUE(value_decl->object_fields[i].value)); + return total; + } + + return 0; + + } else { + const JsValueArguments* arg_decl = declaration.argument_decl; + size_t total = 0; + for(size_t i = 0; i < arg_decl->n_children; i++) + total += js_value_buffer_size(JS_VALUE_PARSE_SOURCE_VALUE(&arg_decl->arguments[i])); + return total; + } +} + +static size_t js_value_resulting_c_values_count(const JsValueParseDeclaration declaration) { + if(declaration.source == JsValueParseSourceValue) { + const JsValueDeclaration* value_decl = declaration.value_decl; + JsValueType type = value_decl->type & JsValueTypeMask; + + if(type == JsValueTypeObject) { + size_t total = 0; + for(size_t i = 0; i < value_decl->n_children; i++) + total += js_value_resulting_c_values_count( + JS_VALUE_PARSE_SOURCE_VALUE(value_decl->object_fields[i].value)); + return total; + } + + return 1; + + } else { + const JsValueArguments* arg_decl = declaration.argument_decl; + size_t total = 0; + for(size_t i = 0; i < arg_decl->n_children; i++) + total += js_value_resulting_c_values_count( + JS_VALUE_PARSE_SOURCE_VALUE(&arg_decl->arguments[i])); + return total; + } +} + +#define PREPEND_JS_ERROR_AND_RETURN(mjs, flags, ...) \ + do { \ + if((flags) & JsValueParseFlagReturnOnError) \ + mjs_prepend_errorf((mjs), MJS_BAD_ARGS_ERROR, __VA_ARGS__); \ + return JsValueParseStatusJsError; \ + } while(0) + +#define PREPEND_JS_EXPECTED_ERROR_AND_RETURN(mjs, flags, type) \ + PREPEND_JS_ERROR_AND_RETURN(mjs, flags, "expected %s", type) + +static void js_value_assign_enum_val(void* destination, JsValueType type_w_flags, uint32_t value) { + if(type_w_flags & JsValueTypeEnumSize1) { + *(uint8_t*)destination = value; + } else if(type_w_flags & JsValueTypeEnumSize2) { + *(uint16_t*)destination = value; + } else if(type_w_flags & JsValueTypeEnumSize4) { + *(uint32_t*)destination = value; + } +} + +static bool js_value_is_null_or_undefined(mjs_val_t* val_ptr) { + return mjs_is_null(*val_ptr) || mjs_is_undefined(*val_ptr); +} + +static bool js_value_maybe_assign_default( + const JsValueDeclaration* declaration, + mjs_val_t* val_ptr, + void* destination, + size_t size) { + if((declaration->type & JsValueTypePermitNull) && js_value_is_null_or_undefined(val_ptr)) { + memcpy(destination, &declaration->default_value, size); + return true; + } + return false; +} + +typedef int (*MjsTypecheckFn)(mjs_val_t value); + +static JsValueParseStatus js_value_parse_literal( + struct mjs* mjs, + JsValueParseFlag flags, + mjs_val_t* destination, + mjs_val_t* source, + MjsTypecheckFn typecheck, + const char* type_name) { + if(!typecheck(*source)) PREPEND_JS_EXPECTED_ERROR_AND_RETURN(mjs, flags, type_name); + *destination = *source; + return JsValueParseStatusOk; +} + +static JsValueParseStatus js_value_parse_va( + struct mjs* mjs, + const JsValueParseDeclaration declaration, + JsValueParseFlag flags, + mjs_val_t* source, + mjs_val_t* buffer, + size_t* buffer_index, + va_list* out_pointers) { + if(declaration.source == JsValueParseSourceArguments) { + const JsValueArguments* arg_decl = declaration.argument_decl; + + for(size_t i = 0; i < arg_decl->n_children; i++) { + mjs_val_t arg_val = mjs_arg(mjs, i); + JsValueParseStatus status = js_value_parse_va( + mjs, + JS_VALUE_PARSE_SOURCE_VALUE(&arg_decl->arguments[i]), + flags, + &arg_val, + buffer, + buffer_index, + out_pointers); + if(status != JsValueParseStatusOk) return status; + } + + return JsValueParseStatusOk; + } + + const JsValueDeclaration* value_decl = declaration.value_decl; + JsValueType type_w_flags = value_decl->type; + JsValueType type_noflags = type_w_flags & JsValueTypeMask; + bool is_null_but_allowed = (type_w_flags & JsValueTypePermitNull) && + js_value_is_null_or_undefined(source); + + void* destination = NULL; + if(type_noflags != JsValueTypeObject) destination = va_arg(*out_pointers, void*); + + switch(type_noflags) { + // Literal terms + case JsValueTypeAny: + *(mjs_val_t*)destination = *source; + break; + case JsValueTypeAnyArray: + return js_value_parse_literal(mjs, flags, destination, source, mjs_is_array, "array"); + case JsValueTypeAnyObject: + return js_value_parse_literal(mjs, flags, destination, source, mjs_is_object, "array"); + case JsValueTypeFunction: + return js_value_parse_literal( + mjs, flags, destination, source, mjs_is_function, "function"); + + // Primitive types + case JsValueTypeRawPointer: { + if(js_value_maybe_assign_default(value_decl, source, destination, sizeof(void*))) break; + if(!mjs_is_foreign(*source)) PREPEND_JS_EXPECTED_ERROR_AND_RETURN(mjs, flags, "pointer"); + *(void**)destination = mjs_get_ptr(mjs, *source); + break; + } + case JsValueTypeInt32: { + if(js_value_maybe_assign_default(value_decl, source, destination, sizeof(int32_t))) break; + if(!mjs_is_number(*source)) PREPEND_JS_EXPECTED_ERROR_AND_RETURN(mjs, flags, "number"); + *(int32_t*)destination = mjs_get_int32(mjs, *source); + break; + } + case JsValueTypeDouble: { + if(js_value_maybe_assign_default(value_decl, source, destination, sizeof(double))) break; + if(!mjs_is_number(*source)) PREPEND_JS_EXPECTED_ERROR_AND_RETURN(mjs, flags, "number"); + *(double*)destination = mjs_get_double(mjs, *source); + break; + } + case JsValueTypeBool: { + if(js_value_maybe_assign_default(value_decl, source, destination, sizeof(bool))) break; + if(!mjs_is_boolean(*source)) PREPEND_JS_EXPECTED_ERROR_AND_RETURN(mjs, flags, "bool"); + *(bool*)destination = mjs_get_bool(mjs, *source); + break; + } + case JsValueTypeString: { + if(js_value_maybe_assign_default(value_decl, source, destination, sizeof(const char*))) + break; + if(!mjs_is_string(*source)) PREPEND_JS_EXPECTED_ERROR_AND_RETURN(mjs, flags, "string"); + buffer[*buffer_index] = *source; + *(const char**)destination = mjs_get_string(mjs, &buffer[*buffer_index], NULL); + (*buffer_index)++; + break; + } + + // Types with children + case JsValueTypeEnum: { + if(is_null_but_allowed) { + js_value_assign_enum_val( + destination, type_w_flags, value_decl->default_value.enum_val); + + } else if(mjs_is_string(*source)) { + const char* str = mjs_get_string(mjs, source, NULL); + furi_check(str); + + bool match_found = false; + for(size_t i = 0; i < value_decl->n_children; i++) { + const JsValueEnumVariant* variant = &value_decl->enum_variants[i]; + if(strcmp(str, variant->string_value) == 0) { + js_value_assign_enum_val(destination, type_w_flags, variant->num_value); + match_found = true; + break; + } + } + + if(!match_found) + PREPEND_JS_EXPECTED_ERROR_AND_RETURN(mjs, flags, "one of permitted strings"); + + } else { + PREPEND_JS_EXPECTED_ERROR_AND_RETURN(mjs, flags, "string"); + } + break; + } + + case JsValueTypeObject: { + if(!(is_null_but_allowed || mjs_is_object(*source))) + PREPEND_JS_EXPECTED_ERROR_AND_RETURN(mjs, flags, "object"); + for(size_t i = 0; i < value_decl->n_children; i++) { + const JsValueObjectField* field = &value_decl->object_fields[i]; + mjs_val_t field_val = mjs_get(mjs, *source, field->field_name, ~0); + JsValueParseStatus status = js_value_parse_va( + mjs, + JS_VALUE_PARSE_SOURCE_VALUE(field->value), + flags, + &field_val, + buffer, + buffer_index, + out_pointers); + if(status != JsValueParseStatusOk) + PREPEND_JS_ERROR_AND_RETURN(mjs, flags, "field %s: ", field->field_name); + } + break; + } + + case JsValueTypeMask: + case JsValueTypeEnumSize1: + case JsValueTypeEnumSize2: + case JsValueTypeEnumSize4: + case JsValueTypePermitNull: + furi_crash(); + } + + return JsValueParseStatusOk; +} + +JsValueParseStatus js_value_parse( + struct mjs* mjs, + const JsValueParseDeclaration declaration, + JsValueParseFlag flags, + mjs_val_t* buffer, + size_t buf_size, + mjs_val_t* source, + size_t n_c_vals, + ...) { + furi_check(mjs); + furi_check(buffer); + + if(declaration.source == JsValueParseSourceValue) { + furi_check(source); + furi_check(declaration.value_decl); + } else { + furi_check(source == NULL); + furi_check(declaration.argument_decl); + } + +#ifdef JS_VAL_DEBUG + furi_check(buf_size == js_value_buffer_size(declaration)); + furi_check(n_c_vals == js_value_resulting_c_values_count(declaration)); +#else + UNUSED(js_value_resulting_c_values_count); +#endif + + va_list out_pointers; + va_start(out_pointers, n_c_vals); + + size_t buffer_index = 0; + JsValueParseStatus status = + js_value_parse_va(mjs, declaration, flags, source, buffer, &buffer_index, &out_pointers); + furi_check(buffer_index <= buf_size); + + va_end(out_pointers); + + return status; +} diff --git a/applications/system/js_app/js_value.h b/applications/system/js_app/js_value.h new file mode 100644 index 000000000..765bcb3bb --- /dev/null +++ b/applications/system/js_app/js_value.h @@ -0,0 +1,212 @@ +#pragma once + +#include +#include "js_modules.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef enum { + // literal types + JsValueTypeAny, // Date: Tue, 1 Apr 2025 16:16:14 +0000 Subject: [PATCH 179/268] Docs: Fix doxygen references from PR 4168 (#4169) * Docs: Fix doxygen references from PR 4168 * Update JS GUI adapter table --------- Co-authored-by: hedger --- documentation/doxygen/js.dox | 1 + documentation/js/js_gui.md | 47 +++++++++++++++++++----------------- 2 files changed, 26 insertions(+), 22 deletions(-) diff --git a/documentation/doxygen/js.dox b/documentation/doxygen/js.dox index 7abde9389..fadf0c023 100644 --- a/documentation/doxygen/js.dox +++ b/documentation/doxygen/js.dox @@ -17,6 +17,7 @@ Flipper Zero's built-in JavaScript engine enables you to run lightweight scripts - @subpage js_badusb — This module allows you to emulate a standard USB keyboard - @subpage js_event_loop — The module for easy event-based developing +- @subpage js_flipper — This module allows to query device information - @subpage js_gpio — This module allows you to control GPIO pins - @subpage js_gui — This module allows you to use GUI (graphical user interface) - @subpage js_math — This module contains mathematical methods and constants diff --git a/documentation/js/js_gui.md b/documentation/js/js_gui.md index afca1434e..b04255008 100644 --- a/documentation/js/js_gui.md +++ b/documentation/js/js_gui.md @@ -10,12 +10,15 @@ let gui = require("gui"); GUI module has several submodules: -- @subpage js_gui__submenu — Displays a scrollable list of clickable textual entries -- @subpage js_gui__loading — Displays an animated hourglass icon -- @subpage js_gui__empty_screen — Just empty screen -- @subpage js_gui__text_input — Keyboard-like text input -- @subpage js_gui__text_box — Simple multiline text box +- @subpage js_gui__byte_input — Keyboard-like hex input - @subpage js_gui__dialog — Dialog with up to 3 options +- @subpage js_gui__empty_screen — Just empty screen +- @subpage js_gui__file_picker — Displays a file selection prompt +- @subpage js_gui__icon — Retrieves and loads icons for use in GUI +- @subpage js_gui__loading — Displays an animated hourglass icon +- @subpage js_gui__submenu — Displays a scrollable list of clickable textual entries +- @subpage js_gui__text_box — Simple multiline text box +- @subpage js_gui__text_input — Keyboard-like text input - @subpage js_gui__widget — Displays a combination of custom elements on one screen --- @@ -38,23 +41,23 @@ always access the canvas through a viewport. In Flipper's terminology, a "View" is a fullscreen design element that assumes control over the entire viewport and all input events. Different types of views are available (not all of which are unfortunately currently implemented in JS): -| View | Has JS adapter? | -|----------------------|------------------| -| `button_menu` | ❌ | -| `button_panel` | ❌ | -| `byte_input` | ❌ | -| `dialog_ex` | ✅ (as `dialog`) | -| `empty_screen` | ✅ | -| `file_browser` | ❌ | -| `loading` | ✅ | -| `menu` | ❌ | -| `number_input` | ❌ | -| `popup` | ❌ | -| `submenu` | ✅ | -| `text_box` | ✅ | -| `text_input` | ✅ | -| `variable_item_list` | ❌ | -| `widget` | ❌ | +| View | Has JS adapter? | +|----------------------|-----------------------| +| `button_menu` | ❌ | +| `button_panel` | ❌ | +| `byte_input` | ✅ | +| `dialog_ex` | ✅ (as `dialog`) | +| `empty_screen` | ✅ | +| `file_browser` | ✅ (as `file_picker`) | +| `loading` | ✅ | +| `menu` | ❌ | +| `number_input` | ❌ | +| `popup` | ❌ | +| `submenu` | ✅ | +| `text_box` | ✅ | +| `text_input` | ✅ | +| `variable_item_list` | ❌ | +| `widget` | ✅ | In JS, each view has its own set of properties (or just "props"). The programmer can manipulate these properties in two ways: From f783e0b3ab6c28cd84c323e7e8be5faff35fb57a Mon Sep 17 00:00:00 2001 From: DrEverr Date: Tue, 1 Apr 2025 18:39:42 +0200 Subject: [PATCH 180/268] add support for Favorite App Ok Long --- applications/services/desktop/desktop_settings.h | 1 + applications/services/desktop/scenes/desktop_scene_main.c | 5 +++++ applications/services/desktop/views/desktop_events.h | 1 + applications/services/desktop/views/desktop_view_main.c | 1 + .../desktop_settings/scenes/desktop_settings_scene_start.c | 1 + 5 files changed, 9 insertions(+) diff --git a/applications/services/desktop/desktop_settings.h b/applications/services/desktop/desktop_settings.h index 784b1eeba..6f81d99cb 100644 --- a/applications/services/desktop/desktop_settings.h +++ b/applications/services/desktop/desktop_settings.h @@ -14,6 +14,7 @@ typedef enum { FavoriteAppLeftLong, FavoriteAppRightShort, FavoriteAppRightLong, + FavoriteAppOkLong, FavoriteAppNumber, } FavoriteAppShortcut; diff --git a/applications/services/desktop/scenes/desktop_scene_main.c b/applications/services/desktop/scenes/desktop_scene_main.c index 4fdcc3400..b329cc16d 100644 --- a/applications/services/desktop/scenes/desktop_scene_main.c +++ b/applications/services/desktop/scenes/desktop_scene_main.c @@ -174,6 +174,11 @@ bool desktop_scene_main_on_event(void* context, SceneManagerEvent event) { desktop, &desktop->settings.favorite_apps[FavoriteAppRightLong]); consumed = true; break; + case DesktopMainEventOpenFavoriteOkLong: + desktop_scene_main_start_favorite( + desktop, &desktop->settings.favorite_apps[FavoriteAppOkLong]); + consumed = true; + break; case DesktopAnimationEventCheckAnimation: animation_manager_check_blocking_process(desktop->animation_manager); diff --git a/applications/services/desktop/views/desktop_events.h b/applications/services/desktop/views/desktop_events.h index ba91a30cc..8eeea00e2 100644 --- a/applications/services/desktop/views/desktop_events.h +++ b/applications/services/desktop/views/desktop_events.h @@ -8,6 +8,7 @@ typedef enum { DesktopMainEventOpenFavoriteLeftLong, DesktopMainEventOpenFavoriteRightShort, DesktopMainEventOpenFavoriteRightLong, + DesktopMainEventOpenFavoriteOkLong, DesktopMainEventOpenMenu, DesktopMainEventOpenDebug, DesktopMainEventOpenPowerOff, diff --git a/applications/services/desktop/views/desktop_view_main.c b/applications/services/desktop/views/desktop_view_main.c index 57cfa1a3e..9ea144364 100644 --- a/applications/services/desktop/views/desktop_view_main.c +++ b/applications/services/desktop/views/desktop_view_main.c @@ -74,6 +74,7 @@ bool desktop_main_input_callback(InputEvent* event, void* context) { if(furi_hal_rtc_is_flag_set(FuriHalRtcFlagDebug)) { main_view->callback(DesktopAnimationEventNewIdleAnimation, main_view->context); } + main_view->callback(DesktopMainEventOpenFavoriteOkLong, main_view->context); } } } else { diff --git a/applications/settings/desktop_settings/scenes/desktop_settings_scene_start.c b/applications/settings/desktop_settings/scenes/desktop_settings_scene_start.c index dfcac3eed..23309bd74 100644 --- a/applications/settings/desktop_settings/scenes/desktop_settings_scene_start.c +++ b/applications/settings/desktop_settings/scenes/desktop_settings_scene_start.c @@ -182,6 +182,7 @@ void desktop_settings_scene_start_on_enter(void* context) { variable_item_list_add(variable_item_list, "Favorite App - Left Long", 1, NULL, NULL); variable_item_list_add(variable_item_list, "Favorite App - Right Short", 1, NULL, NULL); variable_item_list_add(variable_item_list, "Favorite App - Right Long", 1, NULL, NULL); + variable_item_list_add(variable_item_list, "Favorite App - Ok Long", 1, NULL, NULL); variable_item_list_add(variable_item_list, "DummyMode - Left", 1, NULL, NULL); variable_item_list_add(variable_item_list, "DummyMode - Left Long", 1, NULL, NULL); From d08d38f3b2a05fd55e3c50ca9be4ca378fc0c6de Mon Sep 17 00:00:00 2001 From: DrEverr Date: Tue, 1 Apr 2025 18:48:14 +0200 Subject: [PATCH 181/268] fix --- .../scenes/desktop_settings_scene_start.c | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/applications/settings/desktop_settings/scenes/desktop_settings_scene_start.c b/applications/settings/desktop_settings/scenes/desktop_settings_scene_start.c index 23309bd74..8555bbf71 100644 --- a/applications/settings/desktop_settings/scenes/desktop_settings_scene_start.c +++ b/applications/settings/desktop_settings/scenes/desktop_settings_scene_start.c @@ -18,6 +18,7 @@ typedef enum { DesktopSettingsFavoriteLeftLong, DesktopSettingsFavoriteRightShort, DesktopSettingsFavoriteRightLong, + DesktopSettingsFavoriteOkLong, DesktopSettingsDummyLeft, DesktopSettingsDummyLeftLong, DesktopSettingsDummyRight, @@ -247,6 +248,13 @@ bool desktop_settings_scene_start_on_event(void* context, SceneManagerEvent even SCENE_STATE_SET_FAVORITE_APP | FavoriteAppRightLong); scene_manager_next_scene(app->scene_manager, DesktopSettingsAppSceneFavorite); break; + case DesktopSettingsFavoriteOkLong: + scene_manager_set_scene_state( + app->scene_manager, + DesktopSettingsAppSceneFavorite, + SCENE_STATE_SET_FAVORITE_APP | FavoriteAppOkLong); + scene_manager_next_scene(app->scene_manager, DesktopSettingsAppSceneFavorite); + break; case DesktopSettingsDummyLeft: scene_manager_set_scene_state( From 5786066512f65a3fe32072e8601086e71840c164 Mon Sep 17 00:00:00 2001 From: Eric Betts Date: Tue, 1 Apr 2025 11:37:40 -0700 Subject: [PATCH 182/268] BLE advertising improvements (#4151) * Support longer advertised BLE UUID * BLE: support manufacturer data * Don't pair when GapPairingNone * Add PR feedback --------- Co-authored-by: hedger --- lib/ble_profile/extra_profiles/hid_profile.c | 5 +- targets/f7/ble_glue/gap.c | 46 ++++++++++++++++--- targets/f7/ble_glue/gap.h | 8 +++- targets/f7/ble_glue/profiles/serial_profile.c | 10 +++- 4 files changed, 58 insertions(+), 11 deletions(-) diff --git a/lib/ble_profile/extra_profiles/hid_profile.c b/lib/ble_profile/extra_profiles/hid_profile.c index f559a741a..ea90d3114 100644 --- a/lib/ble_profile/extra_profiles/hid_profile.c +++ b/lib/ble_profile/extra_profiles/hid_profile.c @@ -380,7 +380,10 @@ bool ble_profile_hid_mouse_scroll(FuriHalBleProfileBase* profile, int8_t delta) #define CONNECTION_INTERVAL_MAX (0x24) static GapConfig template_config = { - .adv_service_uuid = HUMAN_INTERFACE_DEVICE_SERVICE_UUID, + .adv_service = { + .UUID_Type = UUID_TYPE_16, + .Service_UUID_16 = HUMAN_INTERFACE_DEVICE_SERVICE_UUID, + }, .appearance_char = GAP_APPEARANCE_KEYBOARD, .bonding_mode = true, .pairing_method = GapPairingPinCodeVerifyYesNo, diff --git a/targets/f7/ble_glue/gap.c b/targets/f7/ble_glue/gap.c index 732440ccf..1fe898ea9 100644 --- a/targets/f7/ble_glue/gap.c +++ b/targets/f7/ble_glue/gap.c @@ -23,6 +23,8 @@ typedef struct { uint16_t connection_handle; uint8_t adv_svc_uuid_len; uint8_t adv_svc_uuid[20]; + uint8_t mfg_data_len; + uint8_t mfg_data[20]; char* adv_name; } GapSvc; @@ -198,8 +200,10 @@ BleEventFlowStatus ble_event_app_notification(void* pckt) { gap->service.connection_handle = event->Connection_Handle; gap_verify_connection_parameters(gap); - // Start pairing by sending security request - aci_gap_slave_security_req(event->Connection_Handle); + if(gap->config->pairing_method != GapPairingNone) { + // Start pairing by sending security request + aci_gap_slave_security_req(event->Connection_Handle); + } } break; default: @@ -321,6 +325,14 @@ static void set_advertisment_service_uid(uint8_t* uid, uint8_t uid_len) { gap->service.adv_svc_uuid_len += uid_len; } +static void set_manufacturer_data(uint8_t* mfg_data, uint8_t mfg_data_len) { + furi_check(mfg_data_len < sizeof(gap->service.mfg_data) - 2); + gap->service.mfg_data[0] = mfg_data_len + 1; + gap->service.mfg_data[1] = AD_TYPE_MANUFACTURER_SPECIFIC_DATA; + memcpy(&gap->service.mfg_data[gap->service.mfg_data_len], mfg_data, mfg_data_len); + gap->service.mfg_data_len += mfg_data_len; +} + static void gap_init_svc(Gap* gap) { tBleStatus status; uint32_t srd_bd_addr[2]; @@ -440,6 +452,11 @@ static void gap_advertise_start(GapState new_state) { FURI_LOG_D(TAG, "set_non_discoverable success"); } } + + if(gap->service.mfg_data_len > 0) { + hci_le_set_scan_response_data(gap->service.mfg_data_len, gap->service.mfg_data); + } + // Configure advertising status = aci_gap_set_discoverable( ADV_IND, @@ -550,11 +567,26 @@ bool gap_init(GapConfig* config, GapEventCallback on_event_cb, void* context) { gap->is_secure = false; gap->negotiation_round = 0; - uint8_t adv_service_uid[2]; - gap->service.adv_svc_uuid_len = 1; - adv_service_uid[0] = gap->config->adv_service_uuid & 0xff; - adv_service_uid[1] = gap->config->adv_service_uuid >> 8; - set_advertisment_service_uid(adv_service_uid, sizeof(adv_service_uid)); + if(gap->config->mfg_data_len > 0) { + // Offset by 2 for length + AD_TYPE_MANUFACTURER_SPECIFIC_DATA + gap->service.mfg_data_len = 2; + set_manufacturer_data(gap->config->mfg_data, gap->config->mfg_data_len); + } + + if(gap->config->adv_service.UUID_Type == UUID_TYPE_16) { + uint8_t adv_service_uid[2]; + gap->service.adv_svc_uuid_len = 1; + adv_service_uid[0] = gap->config->adv_service.Service_UUID_16 & 0xff; + adv_service_uid[1] = gap->config->adv_service.Service_UUID_16 >> 8; + set_advertisment_service_uid(adv_service_uid, sizeof(adv_service_uid)); + } else if(gap->config->adv_service.UUID_Type == UUID_TYPE_128) { + gap->service.adv_svc_uuid_len = 1; + set_advertisment_service_uid( + gap->config->adv_service.Service_UUID_128, + sizeof(gap->config->adv_service.Service_UUID_128)); + } else { + furi_crash("Invalid UUID type"); + } // Set callback gap->on_event_cb = on_event_cb; diff --git a/targets/f7/ble_glue/gap.h b/targets/f7/ble_glue/gap.h index a90d07304..d37fd9a1e 100644 --- a/targets/f7/ble_glue/gap.h +++ b/targets/f7/ble_glue/gap.h @@ -68,7 +68,13 @@ typedef struct { } GapConnectionParamsRequest; typedef struct { - uint16_t adv_service_uuid; + struct { + uint8_t UUID_Type; + uint16_t Service_UUID_16; + uint8_t Service_UUID_128[16]; + } adv_service; + uint8_t mfg_data[20]; + uint8_t mfg_data_len; uint16_t appearance_char; bool bonding_mode; GapPairing pairing_method; diff --git a/targets/f7/ble_glue/profiles/serial_profile.c b/targets/f7/ble_glue/profiles/serial_profile.c index 427539427..9bba0d897 100644 --- a/targets/f7/ble_glue/profiles/serial_profile.c +++ b/targets/f7/ble_glue/profiles/serial_profile.c @@ -6,6 +6,7 @@ #include #include #include +#include typedef struct { FuriHalBleProfileBase base; @@ -47,7 +48,11 @@ static void ble_profile_serial_stop(FuriHalBleProfileBase* profile) { #define CONNECTION_INTERVAL_MAX (0x24) static const GapConfig serial_template_config = { - .adv_service_uuid = 0x3080, + .adv_service = + { + .UUID_Type = UUID_TYPE_16, + .Service_UUID_16 = 0x3080, + }, .appearance_char = 0x8600, .bonding_mode = true, .pairing_method = GapPairingPinCodeShow, @@ -71,7 +76,8 @@ static void config->adv_name, furi_hal_version_get_ble_local_device_name_ptr(), FURI_HAL_VERSION_DEVICE_NAME_LENGTH); - config->adv_service_uuid |= furi_hal_version_get_hw_color(); + config->adv_service.UUID_Type = UUID_TYPE_16; + config->adv_service.Service_UUID_16 |= furi_hal_version_get_hw_color(); } static const FuriHalBleProfileTemplate profile_callbacks = { From 13333edd308ccb1322cb1ab201d1c8e319e028a4 Mon Sep 17 00:00:00 2001 From: Anna Antonenko Date: Wed, 2 Apr 2025 22:10:10 +0400 Subject: [PATCH 183/268] [FL-3954, FL-3955] New CLI architecture (#4111) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: FuriThread stdin * ci: fix f18 * feat: stdio callback context * feat: FuriPipe * POTENTIALLY EXPLOSIVE pipe welding * fix: non-explosive welding * Revert welding * docs: furi_pipe * feat: pipe event loop integration * update f18 sdk * f18 * docs: make doxygen happy * fix: event loop not triggering when pipe attached to stdio * fix: partial stdout in pipe * allow simultaneous in and out subscription in event loop * feat: vcp i/o * feat: cli ansi stuffs and history * feat: more line editing * working but slow cli rewrite * restore previous speed after 4 days of debugging 🥲 * fix: cli_app_should_stop * fix: cli and event_loop memory leaks * style: remove commented out code * ci: fix pvs warnings * fix: unit tests, event_loop crash * ci: fix build * ci: silence pvs warning * feat: cli gpio * ci: fix formatting * Fix memory leak during event loop unsubscription * Event better memory leak fix * feat: cli completions * Merge remote-tracking branch 'origin/dev' into portasynthinca3/3928-cli-threads * merge fixups * temporarily exclude speaker_debug app * pvs and unit tests fixups * feat: commands in fals * move commands out of flash, code cleanup * ci: fix errors * fix: run commands in buffer when stopping session * speedup cli file transfer * fix f18 * separate cli_shell into modules * fix pvs warning * fix qflipper refusing to connect * remove temp debug logs * remove erroneous conclusion * Fix memory leak during event loop unsubscription * Event better memory leak fix * unit test for the fix * improve thread stdio callback signatures * pipe stdout timeout * update api symbols * fix f18, formatting * fix pvs warnings * increase stack size, hope to fix unit tests * cli: revert flag changes * cli: fix formatting * cli, fbt: loopback perf benchmark * thread, event_loop: subscribing to thread flags * cli: signal internal events using thread flags, improve performance * fix f18, formatting * event_loop: fix crash * storage_cli: increase write_chunk buffer size again * cli: explanation for order=0 * thread, event_loop: thread flags callback refactor * cli: increase stack size * cli: rename cli_app_should_stop -> cli_is_pipe_broken_or_is_etx_next_char * cli: use plain array instead of mlib for history * cli: prepend file name to static fns * cli: fix formatting * cli_shell: increase stack size * cli: fix rpc lockup * cli: better lockup fix * cli: fix f18 * fix merge --------- Co-authored-by: Georgii Surkov Co-authored-by: あく --- SConstruct | 15 + .../debug/speaker_debug/speaker_debug.c | 5 +- applications/debug/unit_tests/test_runner.c | 9 +- applications/debug/unit_tests/test_runner.h | 8 +- .../debug/unit_tests/tests/pipe/pipe_test.c | 28 +- .../unit_tests/tests/storage/storage_test.c | 5 - applications/debug/unit_tests/unit_tests.c | 6 +- applications/main/application.fam | 1 + applications/main/gpio/usb_uart_bridge.c | 24 +- applications/main/ibutton/ibutton_cli.c | 48 +- applications/main/infrared/infrared_cli.c | 56 +- applications/main/lfrfid/lfrfid_cli.c | 60 +- applications/main/nfc/nfc_cli.c | 10 +- applications/main/onewire/onewire_cli.c | 33 +- .../main/subghz/helpers/subghz_chat.c | 9 +- .../main/subghz/helpers/subghz_chat.h | 3 +- applications/main/subghz/subghz_cli.c | 81 +-- applications/services/application.fam | 1 + applications/services/bt/bt_cli.c | 33 +- applications/services/cli/application.fam | 36 +- applications/services/cli/cli.c | 493 +++------------- applications/services/cli/cli.h | 170 +++--- applications/services/cli/cli_ansi.c | 123 ++++ applications/services/cli/cli_ansi.h | 153 +++++ applications/services/cli/cli_command_gpio.c | 24 +- applications/services/cli/cli_command_gpio.h | 3 +- applications/services/cli/cli_commands.c | 179 +++--- applications/services/cli/cli_commands.h | 31 +- applications/services/cli/cli_i.h | 63 +- applications/services/cli/cli_vcp.c | 539 +++++++++--------- applications/services/cli/cli_vcp.h | 7 +- applications/services/cli/shell/cli_shell.c | 251 ++++++++ applications/services/cli/shell/cli_shell.h | 16 + applications/services/cli/shell/cli_shell_i.h | 32 ++ .../services/cli/shell/cli_shell_line.c | 238 ++++++++ .../services/cli/shell/cli_shell_line.h | 36 ++ applications/services/crypto/crypto_cli.c | 35 +- applications/services/desktop/desktop.c | 12 +- applications/services/input/input.c | 3 +- applications/services/input/input_cli.c | 16 +- applications/services/loader/loader_cli.c | 5 +- applications/services/power/power_cli.c | 33 +- applications/services/rpc/rpc_cli.c | 19 +- applications/services/rpc/rpc_i.h | 3 +- applications/services/storage/storage_cli.c | 106 ++-- applications/system/js_app/js_app.c | 13 +- applications/system/updater/cli/updater_cli.c | 5 +- furi/core/core_defines.h | 10 + furi/core/event_loop.c | 42 ++ furi/core/event_loop.h | 23 + furi/core/event_loop_i.h | 10 +- furi/core/event_loop_thread_flag_interface.h | 10 + furi/core/thread.c | 4 + lib/toolbox/pipe.c | 66 ++- lib/toolbox/pipe.h | 32 +- scripts/serial_cli_perf.py | 59 ++ scripts/update.py | 2 +- targets/f18/api_symbols.csv | 31 +- targets/f7/api_symbols.csv | 31 +- targets/f7/furi_hal/furi_hal_usb_cdc.h | 14 +- 60 files changed, 2074 insertions(+), 1339 deletions(-) create mode 100644 applications/services/cli/cli_ansi.c create mode 100644 applications/services/cli/cli_ansi.h create mode 100644 applications/services/cli/shell/cli_shell.c create mode 100644 applications/services/cli/shell/cli_shell.h create mode 100644 applications/services/cli/shell/cli_shell_i.h create mode 100644 applications/services/cli/shell/cli_shell_line.c create mode 100644 applications/services/cli/shell/cli_shell_line.h create mode 100644 furi/core/event_loop_thread_flag_interface.h create mode 100644 scripts/serial_cli_perf.py diff --git a/SConstruct b/SConstruct index 127967968..44aa3bc17 100644 --- a/SConstruct +++ b/SConstruct @@ -412,6 +412,21 @@ distenv.PhonyTarget( ], ) + +# Measure CLI loopback performance +distenv.PhonyTarget( + "cli_perf", + [ + [ + "${PYTHON3}", + "${FBT_SCRIPT_DIR}/serial_cli_perf.py", + "-p", + "${FLIP_PORT}", + "${ARGS}", + ] + ], +) + # Update WiFi devboard firmware with release channel distenv.PhonyTarget( "devboard_flash", diff --git a/applications/debug/speaker_debug/speaker_debug.c b/applications/debug/speaker_debug/speaker_debug.c index 3f685ab30..d2a40a2a4 100644 --- a/applications/debug/speaker_debug/speaker_debug.c +++ b/applications/debug/speaker_debug/speaker_debug.c @@ -3,6 +3,7 @@ #include #include #include +#include #define TAG "SpeakerDebug" @@ -37,8 +38,8 @@ static void speaker_app_free(SpeakerDebugApp* app) { free(app); } -static void speaker_app_cli(Cli* cli, FuriString* args, void* context) { - UNUSED(cli); +static void speaker_app_cli(PipeSide* pipe, FuriString* args, void* context) { + UNUSED(pipe); SpeakerDebugApp* app = (SpeakerDebugApp*)context; SpeakerDebugAppMessage message; diff --git a/applications/debug/unit_tests/test_runner.c b/applications/debug/unit_tests/test_runner.c index de29e91b3..4de051125 100644 --- a/applications/debug/unit_tests/test_runner.c +++ b/applications/debug/unit_tests/test_runner.c @@ -4,6 +4,7 @@ #include #include +#include #include #include #include @@ -25,7 +26,7 @@ struct TestRunner { NotificationApp* notification; // Temporary used things - Cli* cli; + PipeSide* pipe; FuriString* args; // ELF related stuff @@ -38,14 +39,14 @@ struct TestRunner { int minunit_status; }; -TestRunner* test_runner_alloc(Cli* cli, FuriString* args) { +TestRunner* test_runner_alloc(PipeSide* pipe, FuriString* args) { TestRunner* instance = malloc(sizeof(TestRunner)); instance->storage = furi_record_open(RECORD_STORAGE); instance->loader = furi_record_open(RECORD_LOADER); instance->notification = furi_record_open(RECORD_NOTIFICATION); - instance->cli = cli; + instance->pipe = pipe; instance->args = args; instance->composite_resolver = composite_api_resolver_alloc(); @@ -147,7 +148,7 @@ static void test_runner_run_internal(TestRunner* instance) { } while(true) { - if(cli_cmd_interrupt_received(instance->cli)) { + if(cli_is_pipe_broken_or_is_etx_next_char(instance->pipe)) { break; } diff --git a/applications/debug/unit_tests/test_runner.h b/applications/debug/unit_tests/test_runner.h index 43aba8bb1..0e9495263 100644 --- a/applications/debug/unit_tests/test_runner.h +++ b/applications/debug/unit_tests/test_runner.h @@ -1,12 +1,12 @@ #pragma once #include +#include typedef struct TestRunner TestRunner; -typedef struct Cli Cli; -TestRunner* test_runner_alloc(Cli* cli, FuriString* args); +TestRunner* test_runner_alloc(PipeSide* pipe, FuriString* args); -void test_runner_free(TestRunner* isntance); +void test_runner_free(TestRunner* instance); -void test_runner_run(TestRunner* isntance); +void test_runner_run(TestRunner* instance); diff --git a/applications/debug/unit_tests/tests/pipe/pipe_test.c b/applications/debug/unit_tests/tests/pipe/pipe_test.c index d440a04ee..4eae39636 100644 --- a/applications/debug/unit_tests/tests/pipe/pipe_test.c +++ b/applications/debug/unit_tests/tests/pipe/pipe_test.c @@ -25,16 +25,13 @@ MU_TEST(pipe_test_trivial) { mu_assert_int_eq(PIPE_SIZE - i, pipe_spaces_available(alice)); mu_assert_int_eq(i, pipe_bytes_available(bob)); - if(pipe_send(alice, &i, sizeof(uint8_t), 0) != sizeof(uint8_t)) { - break; - } + if(pipe_spaces_available(alice) == 0) break; + furi_check(pipe_send(alice, &i, sizeof(uint8_t)) == sizeof(uint8_t)); mu_assert_int_eq(PIPE_SIZE - i, pipe_spaces_available(bob)); mu_assert_int_eq(i, pipe_bytes_available(alice)); - if(pipe_send(bob, &i, sizeof(uint8_t), 0) != sizeof(uint8_t)) { - break; - } + furi_check(pipe_send(bob, &i, sizeof(uint8_t)) == sizeof(uint8_t)); } pipe_free(alice); @@ -43,10 +40,9 @@ MU_TEST(pipe_test_trivial) { for(uint8_t i = 0;; ++i) { mu_assert_int_eq(PIPE_SIZE - i, pipe_bytes_available(bob)); + if(pipe_bytes_available(bob) == 0) break; uint8_t value; - if(pipe_receive(bob, &value, sizeof(uint8_t), 0) != sizeof(uint8_t)) { - break; - } + furi_check(pipe_receive(bob, &value, sizeof(uint8_t)) == sizeof(uint8_t)); mu_assert_int_eq(i, value); } @@ -68,16 +64,16 @@ typedef struct { static void on_data_arrived(PipeSide* pipe, void* context) { AncillaryThreadContext* ctx = context; ctx->flag |= TestFlagDataArrived; - uint8_t buffer[PIPE_SIZE]; - size_t size = pipe_receive(pipe, buffer, sizeof(buffer), 0); - pipe_send(pipe, buffer, size, 0); + uint8_t input; + size_t size = pipe_receive(pipe, &input, sizeof(input)); + pipe_send(pipe, &input, size); } static void on_space_freed(PipeSide* pipe, void* context) { AncillaryThreadContext* ctx = context; ctx->flag |= TestFlagSpaceFreed; const char* message = "Hi!"; - pipe_send(pipe, message, strlen(message), 0); + pipe_send(pipe, message, strlen(message)); } static void on_became_broken(PipeSide* pipe, void* context) { @@ -117,15 +113,15 @@ MU_TEST(pipe_test_event_loop) { furi_thread_start(thread); const char* message = "Hello!"; - pipe_send(alice, message, strlen(message), FuriWaitForever); + pipe_send(alice, message, strlen(message)); char buffer_1[16]; - size_t size = pipe_receive(alice, buffer_1, sizeof(buffer_1), FuriWaitForever); + size_t size = pipe_receive(alice, buffer_1, strlen(message)); buffer_1[size] = 0; char buffer_2[16]; const char* expected_reply = "Hi!"; - size = pipe_receive(alice, buffer_2, sizeof(buffer_2), FuriWaitForever); + size = pipe_receive(alice, buffer_2, strlen(expected_reply)); buffer_2[size] = 0; pipe_free(alice); diff --git a/applications/debug/unit_tests/tests/storage/storage_test.c b/applications/debug/unit_tests/tests/storage/storage_test.c index 75c52ef9a..9d5c68a44 100644 --- a/applications/debug/unit_tests/tests/storage/storage_test.c +++ b/applications/debug/unit_tests/tests/storage/storage_test.c @@ -521,11 +521,6 @@ MU_TEST(test_storage_data_path) { // check that appsdata folder exists mu_check(storage_dir_exists(storage, APPS_DATA_PATH)); - // check that cli folder exists - mu_check(storage_dir_exists(storage, APPSDATA_APP_PATH("cli"))); - - storage_simply_remove(storage, APPSDATA_APP_PATH("cli")); - furi_record_close(RECORD_STORAGE); } diff --git a/applications/debug/unit_tests/unit_tests.c b/applications/debug/unit_tests/unit_tests.c index 237cb9080..db78d8ced 100644 --- a/applications/debug/unit_tests/unit_tests.c +++ b/applications/debug/unit_tests/unit_tests.c @@ -1,13 +1,13 @@ #include #include +#include #include "test_runner.h" -void unit_tests_cli(Cli* cli, FuriString* args, void* context) { - UNUSED(cli); +void unit_tests_cli(PipeSide* pipe, FuriString* args, void* context) { UNUSED(context); - TestRunner* test_runner = test_runner_alloc(cli, args); + TestRunner* test_runner = test_runner_alloc(pipe, args); test_runner_run(test_runner); test_runner_free(test_runner); } diff --git a/applications/main/application.fam b/applications/main/application.fam index 0a90ee224..4d3162337 100644 --- a/applications/main/application.fam +++ b/applications/main/application.fam @@ -21,6 +21,7 @@ App( name="On start hooks", apptype=FlipperAppType.METAPACKAGE, provides=[ + "cli", "ibutton_start", "onewire_start", "subghz_start", diff --git a/applications/main/gpio/usb_uart_bridge.c b/applications/main/gpio/usb_uart_bridge.c index e6b71cb34..15170f0d0 100644 --- a/applications/main/gpio/usb_uart_bridge.c +++ b/applications/main/gpio/usb_uart_bridge.c @@ -106,15 +106,15 @@ static void usb_uart_on_irq_rx_dma_cb( static void usb_uart_vcp_init(UsbUartBridge* usb_uart, uint8_t vcp_ch) { furi_hal_usb_unlock(); if(vcp_ch == 0) { - Cli* cli = furi_record_open(RECORD_CLI); - cli_session_close(cli); - furi_record_close(RECORD_CLI); + CliVcp* cli_vcp = furi_record_open(RECORD_CLI_VCP); + cli_vcp_disable(cli_vcp); + furi_record_close(RECORD_CLI_VCP); furi_check(furi_hal_usb_set_config(&usb_cdc_single, NULL) == true); } else { furi_check(furi_hal_usb_set_config(&usb_cdc_dual, NULL) == true); - Cli* cli = furi_record_open(RECORD_CLI); - cli_session_open(cli, &cli_vcp); - furi_record_close(RECORD_CLI); + CliVcp* cli_vcp = furi_record_open(RECORD_CLI_VCP); + cli_vcp_enable(cli_vcp); + furi_record_close(RECORD_CLI_VCP); } furi_hal_cdc_set_callbacks(vcp_ch, (CdcCallbacks*)&cdc_cb, usb_uart); } @@ -123,9 +123,9 @@ static void usb_uart_vcp_deinit(UsbUartBridge* usb_uart, uint8_t vcp_ch) { UNUSED(usb_uart); furi_hal_cdc_set_callbacks(vcp_ch, NULL, NULL); if(vcp_ch != 0) { - Cli* cli = furi_record_open(RECORD_CLI); - cli_session_close(cli); - furi_record_close(RECORD_CLI); + CliVcp* cli_vcp = furi_record_open(RECORD_CLI_VCP); + cli_vcp_disable(cli_vcp); + furi_record_close(RECORD_CLI_VCP); } } @@ -309,9 +309,9 @@ static int32_t usb_uart_worker(void* context) { furi_hal_usb_unlock(); furi_check(furi_hal_usb_set_config(&usb_cdc_single, NULL) == true); - Cli* cli = furi_record_open(RECORD_CLI); - cli_session_open(cli, &cli_vcp); - furi_record_close(RECORD_CLI); + CliVcp* cli_vcp = furi_record_open(RECORD_CLI_VCP); + cli_vcp_enable(cli_vcp); + furi_record_close(RECORD_CLI_VCP); return 0; } diff --git a/applications/main/ibutton/ibutton_cli.c b/applications/main/ibutton/ibutton_cli.c index 2338ca3c3..e11ace1d0 100644 --- a/applications/main/ibutton/ibutton_cli.c +++ b/applications/main/ibutton/ibutton_cli.c @@ -1,26 +1,14 @@ #include #include -#include +#include #include +#include #include #include #include -static void ibutton_cli(Cli* cli, FuriString* args, void* context); - -// app cli function -void ibutton_on_system_start(void) { -#ifdef SRV_CLI - Cli* cli = furi_record_open(RECORD_CLI); - cli_add_command(cli, "ikey", CliCommandFlagDefault, ibutton_cli, cli); - furi_record_close(RECORD_CLI); -#else - UNUSED(ibutton_cli); -#endif -} - static void ibutton_cli_print_usage(void) { printf("Usage:\r\n"); printf("ikey read\r\n"); @@ -92,7 +80,7 @@ static void ibutton_cli_worker_read_cb(void* context) { furi_event_flag_set(event, EVENT_FLAG_IBUTTON_COMPLETE); } -static void ibutton_cli_read(Cli* cli) { +static void ibutton_cli_read(PipeSide* pipe) { iButtonProtocols* protocols = ibutton_protocols_alloc(); iButtonWorker* worker = ibutton_worker_alloc(protocols); iButtonKey* key = ibutton_key_alloc(ibutton_protocols_get_max_data_size(protocols)); @@ -113,7 +101,7 @@ static void ibutton_cli_read(Cli* cli) { break; } - if(cli_cmd_interrupt_received(cli)) break; + if(cli_is_pipe_broken_or_is_etx_next_char(pipe)) break; } ibutton_worker_stop(worker); @@ -138,7 +126,7 @@ static void ibutton_cli_worker_write_cb(void* context, iButtonWorkerWriteResult furi_event_flag_set(write_context->event, EVENT_FLAG_IBUTTON_COMPLETE); } -void ibutton_cli_write(Cli* cli, FuriString* args) { +void ibutton_cli_write(PipeSide* pipe, FuriString* args) { iButtonProtocols* protocols = ibutton_protocols_alloc(); iButtonWorker* worker = ibutton_worker_alloc(protocols); iButtonKey* key = ibutton_key_alloc(ibutton_protocols_get_max_data_size(protocols)); @@ -181,7 +169,7 @@ void ibutton_cli_write(Cli* cli, FuriString* args) { } } - if(cli_cmd_interrupt_received(cli)) break; + if(cli_is_pipe_broken_or_is_etx_next_char(pipe)) break; } } while(false); @@ -195,7 +183,7 @@ void ibutton_cli_write(Cli* cli, FuriString* args) { furi_event_flag_free(write_context.event); } -void ibutton_cli_emulate(Cli* cli, FuriString* args) { +void ibutton_cli_emulate(PipeSide* pipe, FuriString* args) { iButtonProtocols* protocols = ibutton_protocols_alloc(); iButtonWorker* worker = ibutton_worker_alloc(protocols); iButtonKey* key = ibutton_key_alloc(ibutton_protocols_get_max_data_size(protocols)); @@ -214,7 +202,7 @@ void ibutton_cli_emulate(Cli* cli, FuriString* args) { ibutton_worker_emulate_start(worker, key); - while(!cli_cmd_interrupt_received(cli)) { + while(!cli_is_pipe_broken_or_is_etx_next_char(pipe)) { furi_delay_ms(100); }; @@ -228,8 +216,8 @@ void ibutton_cli_emulate(Cli* cli, FuriString* args) { ibutton_protocols_free(protocols); } -void ibutton_cli(Cli* cli, FuriString* args, void* context) { - UNUSED(cli); +void ibutton_cli(PipeSide* pipe, FuriString* args, void* context) { + UNUSED(pipe); UNUSED(context); FuriString* cmd; cmd = furi_string_alloc(); @@ -241,14 +229,24 @@ void ibutton_cli(Cli* cli, FuriString* args, void* context) { } if(furi_string_cmp_str(cmd, "read") == 0) { - ibutton_cli_read(cli); + ibutton_cli_read(pipe); } else if(furi_string_cmp_str(cmd, "write") == 0) { - ibutton_cli_write(cli, args); + ibutton_cli_write(pipe, args); } else if(furi_string_cmp_str(cmd, "emulate") == 0) { - ibutton_cli_emulate(cli, args); + ibutton_cli_emulate(pipe, args); } else { ibutton_cli_print_usage(); } furi_string_free(cmd); } + +void ibutton_on_system_start(void) { +#ifdef SRV_CLI + Cli* cli = furi_record_open(RECORD_CLI); + cli_add_command(cli, "ikey", CliCommandFlagDefault, ibutton_cli, cli); + furi_record_close(RECORD_CLI); +#else + UNUSED(ibutton_cli); +#endif +} diff --git a/applications/main/infrared/infrared_cli.c b/applications/main/infrared/infrared_cli.c index 85ae95658..e62da5fd2 100644 --- a/applications/main/infrared/infrared_cli.c +++ b/applications/main/infrared/infrared_cli.c @@ -1,11 +1,11 @@ -#include -#include +#include #include #include #include #include #include #include +#include #include #include "infrared_signal.h" @@ -19,14 +19,14 @@ DICT_DEF2(dict_signals, FuriString*, FURI_STRING_OPLIST, int, M_DEFAULT_OPLIST) -static void infrared_cli_start_ir_rx(Cli* cli, FuriString* args); -static void infrared_cli_start_ir_tx(Cli* cli, FuriString* args); -static void infrared_cli_process_decode(Cli* cli, FuriString* args); -static void infrared_cli_process_universal(Cli* cli, FuriString* args); +static void infrared_cli_start_ir_rx(PipeSide* pipe, FuriString* args); +static void infrared_cli_start_ir_tx(PipeSide* pipe, FuriString* args); +static void infrared_cli_process_decode(PipeSide* pipe, FuriString* args); +static void infrared_cli_process_universal(PipeSide* pipe, FuriString* args); static const struct { const char* cmd; - void (*process_function)(Cli* cli, FuriString* args); + void (*process_function)(PipeSide* pipe, FuriString* args); } infrared_cli_commands[] = { {.cmd = "rx", .process_function = infrared_cli_start_ir_rx}, {.cmd = "tx", .process_function = infrared_cli_start_ir_tx}, @@ -38,7 +38,7 @@ static void signal_received_callback(void* context, InfraredWorkerSignal* receiv furi_assert(received_signal); char buf[100]; size_t buf_cnt; - Cli* cli = (Cli*)context; + PipeSide* pipe = (PipeSide*)context; if(infrared_worker_signal_is_decoded(received_signal)) { const InfraredMessage* message = infrared_worker_get_decoded_signal(received_signal); @@ -52,20 +52,20 @@ static void signal_received_callback(void* context, InfraredWorkerSignal* receiv ROUND_UP_TO(infrared_get_protocol_command_length(message->protocol), 4), message->command, message->repeat ? " R" : ""); - cli_write(cli, (uint8_t*)buf, buf_cnt); + pipe_send(pipe, buf, buf_cnt); } else { const uint32_t* timings; size_t timings_cnt; infrared_worker_get_raw_signal(received_signal, &timings, &timings_cnt); buf_cnt = snprintf(buf, sizeof(buf), "RAW, %zu samples:\r\n", timings_cnt); - cli_write(cli, (uint8_t*)buf, buf_cnt); + pipe_send(pipe, buf, buf_cnt); for(size_t i = 0; i < timings_cnt; ++i) { buf_cnt = snprintf(buf, sizeof(buf), "%lu ", timings[i]); - cli_write(cli, (uint8_t*)buf, buf_cnt); + pipe_send(pipe, buf, buf_cnt); } buf_cnt = snprintf(buf, sizeof(buf), "\r\n"); - cli_write(cli, (uint8_t*)buf, buf_cnt); + pipe_send(pipe, buf, buf_cnt); } } @@ -124,9 +124,7 @@ static void infrared_cli_print_usage(void) { infrared_cli_print_universal_remotes(); } -static void infrared_cli_start_ir_rx(Cli* cli, FuriString* args) { - UNUSED(cli); - +static void infrared_cli_start_ir_rx(PipeSide* pipe, FuriString* args) { bool enable_decoding = true; if(!furi_string_empty(args)) { @@ -142,10 +140,10 @@ static void infrared_cli_start_ir_rx(Cli* cli, FuriString* args) { InfraredWorker* worker = infrared_worker_alloc(); infrared_worker_rx_enable_signal_decoding(worker, enable_decoding); infrared_worker_rx_start(worker); - infrared_worker_rx_set_received_signal_callback(worker, signal_received_callback, cli); + infrared_worker_rx_set_received_signal_callback(worker, signal_received_callback, pipe); printf("Receiving %s INFRARED...\r\nPress Ctrl+C to abort\r\n", enable_decoding ? "" : "RAW"); - while(!cli_cmd_interrupt_received(cli)) { + while(!cli_is_pipe_broken_or_is_etx_next_char(pipe)) { furi_delay_ms(50); } @@ -214,8 +212,8 @@ static bool infrared_cli_parse_raw(const char* str, InfraredSignal* signal) { return infrared_signal_is_valid(signal); } -static void infrared_cli_start_ir_tx(Cli* cli, FuriString* args) { - UNUSED(cli); +static void infrared_cli_start_ir_tx(PipeSide* pipe, FuriString* args) { + UNUSED(pipe); const char* str = furi_string_get_cstr(args); InfraredSignal* signal = infrared_signal_alloc(); @@ -335,8 +333,8 @@ static bool infrared_cli_decode_file(FlipperFormat* input_file, FlipperFormat* o return ret; } -static void infrared_cli_process_decode(Cli* cli, FuriString* args) { - UNUSED(cli); +static void infrared_cli_process_decode(PipeSide* pipe, FuriString* args) { + UNUSED(pipe); Storage* storage = furi_record_open(RECORD_STORAGE); FlipperFormat* input_file = flipper_format_buffered_file_alloc(storage); FlipperFormat* output_file = NULL; @@ -455,8 +453,10 @@ static void infrared_cli_list_remote_signals(FuriString* remote_name) { furi_record_close(RECORD_STORAGE); } -static void - infrared_cli_brute_force_signals(Cli* cli, FuriString* remote_name, FuriString* signal_name) { +static void infrared_cli_brute_force_signals( + PipeSide* pipe, + FuriString* remote_name, + FuriString* signal_name) { InfraredBruteForce* brute_force = infrared_brute_force_alloc(); FuriString* remote_path = furi_string_alloc_printf( "%s/%s.ir", INFRARED_ASSETS_FOLDER, furi_string_get_cstr(remote_name)); @@ -490,7 +490,7 @@ static void while(running) { running = infrared_brute_force_send(brute_force, current_signal); - if(cli_cmd_interrupt_received(cli)) break; + if(cli_is_pipe_broken_or_is_etx_next_char(pipe)) break; printf("\r%d%% complete.", (int)((float)current_signal++ / (float)signal_count * 100)); fflush(stdout); @@ -504,7 +504,7 @@ static void infrared_brute_force_free(brute_force); } -static void infrared_cli_process_universal(Cli* cli, FuriString* args) { +static void infrared_cli_process_universal(PipeSide* pipe, FuriString* args) { FuriString* arg1 = furi_string_alloc(); FuriString* arg2 = furi_string_alloc(); @@ -519,14 +519,14 @@ static void infrared_cli_process_universal(Cli* cli, FuriString* args) { } else if(furi_string_equal_str(arg1, "list")) { infrared_cli_list_remote_signals(arg2); } else { - infrared_cli_brute_force_signals(cli, arg1, arg2); + infrared_cli_brute_force_signals(pipe, arg1, arg2); } furi_string_free(arg1); furi_string_free(arg2); } -static void infrared_cli_start_ir(Cli* cli, FuriString* args, void* context) { +static void infrared_cli_start_ir(PipeSide* pipe, FuriString* args, void* context) { UNUSED(context); if(furi_hal_infrared_is_busy()) { printf("INFRARED is busy. Exiting."); @@ -546,7 +546,7 @@ static void infrared_cli_start_ir(Cli* cli, FuriString* args, void* context) { } if(i < COUNT_OF(infrared_cli_commands)) { - infrared_cli_commands[i].process_function(cli, args); + infrared_cli_commands[i].process_function(pipe, args); } else { infrared_cli_print_usage(); } diff --git a/applications/main/lfrfid/lfrfid_cli.c b/applications/main/lfrfid/lfrfid_cli.c index a25032d6a..fa74906c0 100644 --- a/applications/main/lfrfid/lfrfid_cli.c +++ b/applications/main/lfrfid/lfrfid_cli.c @@ -1,11 +1,12 @@ #include #include #include -#include +#include #include #include #include #include +#include #include @@ -14,15 +15,6 @@ #include #include -static void lfrfid_cli(Cli* cli, FuriString* args, void* context); - -// app cli function -void lfrfid_on_system_start(void) { - Cli* cli = furi_record_open(RECORD_CLI); - cli_add_command(cli, "rfid", CliCommandFlagDefault, lfrfid_cli, NULL); - furi_record_close(RECORD_CLI); -} - static void lfrfid_cli_print_usage(void) { printf("Usage:\r\n"); printf("rfid read - read in ASK/PSK mode\r\n"); @@ -49,7 +41,7 @@ static void lfrfid_cli_read_callback(LFRFIDWorkerReadResult result, ProtocolId p furi_event_flag_set(context->event, 1 << result); } -static void lfrfid_cli_read(Cli* cli, FuriString* args) { +static void lfrfid_cli_read(PipeSide* pipe, FuriString* args) { FuriString* type_string; type_string = furi_string_alloc(); LFRFIDWorkerReadType type = LFRFIDWorkerReadTypeAuto; @@ -96,7 +88,7 @@ static void lfrfid_cli_read(Cli* cli, FuriString* args) { } } - if(cli_cmd_interrupt_received(cli)) break; + if(cli_is_pipe_broken_or_is_etx_next_char(pipe)) break; } lfrfid_worker_stop(worker); @@ -192,7 +184,7 @@ static void lfrfid_cli_write_callback(LFRFIDWorkerWriteResult result, void* ctx) furi_event_flag_set(events, 1 << result); } -static void lfrfid_cli_write(Cli* cli, FuriString* args) { +static void lfrfid_cli_write(PipeSide* pipe, FuriString* args) { ProtocolDict* dict = protocol_dict_alloc(lfrfid_protocols, LFRFIDProtocolMax); ProtocolId protocol; @@ -212,7 +204,7 @@ static void lfrfid_cli_write(Cli* cli, FuriString* args) { (1 << LFRFIDWorkerWriteProtocolCannotBeWritten) | (1 << LFRFIDWorkerWriteFobCannotBeWritten); - while(!cli_cmd_interrupt_received(cli)) { + while(!cli_is_pipe_broken_or_is_etx_next_char(pipe)) { uint32_t flags = furi_event_flag_wait(event, available_flags, FuriFlagWaitAny, 100); if(flags != (unsigned)FuriFlagErrorTimeout) { if(FURI_BIT(flags, LFRFIDWorkerWriteOK)) { @@ -239,7 +231,7 @@ static void lfrfid_cli_write(Cli* cli, FuriString* args) { furi_event_flag_free(event); } -static void lfrfid_cli_emulate(Cli* cli, FuriString* args) { +static void lfrfid_cli_emulate(PipeSide* pipe, FuriString* args) { ProtocolDict* dict = protocol_dict_alloc(lfrfid_protocols, LFRFIDProtocolMax); ProtocolId protocol; @@ -254,7 +246,7 @@ static void lfrfid_cli_emulate(Cli* cli, FuriString* args) { lfrfid_worker_emulate_start(worker, protocol); printf("Emulating RFID...\r\nPress Ctrl+C to abort\r\n"); - while(!cli_cmd_interrupt_received(cli)) { + while(!cli_is_pipe_broken_or_is_etx_next_char(pipe)) { furi_delay_ms(100); } printf("Emulation stopped\r\n"); @@ -265,8 +257,8 @@ static void lfrfid_cli_emulate(Cli* cli, FuriString* args) { protocol_dict_free(dict); } -static void lfrfid_cli_raw_analyze(Cli* cli, FuriString* args) { - UNUSED(cli); +static void lfrfid_cli_raw_analyze(PipeSide* pipe, FuriString* args) { + UNUSED(pipe); FuriString *filepath, *info_string; filepath = furi_string_alloc(); info_string = furi_string_alloc(); @@ -392,9 +384,7 @@ static void lfrfid_cli_raw_read_callback(LFRFIDWorkerReadRawResult result, void* furi_event_flag_set(event, 1 << result); } -static void lfrfid_cli_raw_read(Cli* cli, FuriString* args) { - UNUSED(cli); - +static void lfrfid_cli_raw_read(PipeSide* pipe, FuriString* args) { FuriString *filepath, *type_string; filepath = furi_string_alloc(); type_string = furi_string_alloc(); @@ -452,7 +442,7 @@ static void lfrfid_cli_raw_read(Cli* cli, FuriString* args) { } } - if(cli_cmd_interrupt_received(cli)) break; + if(cli_is_pipe_broken_or_is_etx_next_char(pipe)) break; } if(overrun) { @@ -479,9 +469,7 @@ static void lfrfid_cli_raw_emulate_callback(LFRFIDWorkerEmulateRawResult result, furi_event_flag_set(event, 1 << result); } -static void lfrfid_cli_raw_emulate(Cli* cli, FuriString* args) { - UNUSED(cli); - +static void lfrfid_cli_raw_emulate(PipeSide* pipe, FuriString* args) { FuriString* filepath; filepath = furi_string_alloc(); Storage* storage = furi_record_open(RECORD_STORAGE); @@ -527,7 +515,7 @@ static void lfrfid_cli_raw_emulate(Cli* cli, FuriString* args) { } } - if(cli_cmd_interrupt_received(cli)) break; + if(cli_is_pipe_broken_or_is_etx_next_char(pipe)) break; } if(overrun) { @@ -548,7 +536,7 @@ static void lfrfid_cli_raw_emulate(Cli* cli, FuriString* args) { furi_string_free(filepath); } -static void lfrfid_cli(Cli* cli, FuriString* args, void* context) { +static void lfrfid_cli(PipeSide* pipe, FuriString* args, void* context) { UNUSED(context); FuriString* cmd; cmd = furi_string_alloc(); @@ -560,20 +548,26 @@ static void lfrfid_cli(Cli* cli, FuriString* args, void* context) { } if(furi_string_cmp_str(cmd, "read") == 0) { - lfrfid_cli_read(cli, args); + lfrfid_cli_read(pipe, args); } else if(furi_string_cmp_str(cmd, "write") == 0) { - lfrfid_cli_write(cli, args); + lfrfid_cli_write(pipe, args); } else if(furi_string_cmp_str(cmd, "emulate") == 0) { - lfrfid_cli_emulate(cli, args); + lfrfid_cli_emulate(pipe, args); } else if(furi_string_cmp_str(cmd, "raw_read") == 0) { - lfrfid_cli_raw_read(cli, args); + lfrfid_cli_raw_read(pipe, args); } else if(furi_string_cmp_str(cmd, "raw_emulate") == 0) { - lfrfid_cli_raw_emulate(cli, args); + lfrfid_cli_raw_emulate(pipe, args); } else if(furi_string_cmp_str(cmd, "raw_analyze") == 0) { - lfrfid_cli_raw_analyze(cli, args); + lfrfid_cli_raw_analyze(pipe, args); } else { lfrfid_cli_print_usage(); } furi_string_free(cmd); } + +void lfrfid_on_system_start(void) { + Cli* cli = furi_record_open(RECORD_CLI); + cli_add_command(cli, "rfid", CliCommandFlagDefault, lfrfid_cli, NULL); + furi_record_close(RECORD_CLI); +} diff --git a/applications/main/nfc/nfc_cli.c b/applications/main/nfc/nfc_cli.c index 90ac26d7c..8a9b1fec4 100644 --- a/applications/main/nfc/nfc_cli.c +++ b/applications/main/nfc/nfc_cli.c @@ -1,8 +1,10 @@ #include #include #include +#include #include #include +#include #include @@ -17,7 +19,7 @@ static void nfc_cli_print_usage(void) { } } -static void nfc_cli_field(Cli* cli, FuriString* args) { +static void nfc_cli_field(PipeSide* pipe, FuriString* args) { UNUSED(args); // Check if nfc worker is not busy if(furi_hal_nfc_is_hal_ready() != FuriHalNfcErrorNone) { @@ -32,7 +34,7 @@ static void nfc_cli_field(Cli* cli, FuriString* args) { printf("Field is on. Don't leave device in this mode for too long.\r\n"); printf("Press Ctrl+C to abort\r\n"); - while(!cli_cmd_interrupt_received(cli)) { + while(!cli_is_pipe_broken_or_is_etx_next_char(pipe)) { furi_delay_ms(50); } @@ -40,7 +42,7 @@ static void nfc_cli_field(Cli* cli, FuriString* args) { furi_hal_nfc_release(); } -static void nfc_cli(Cli* cli, FuriString* args, void* context) { +static void nfc_cli(PipeSide* pipe, FuriString* args, void* context) { UNUSED(context); FuriString* cmd; cmd = furi_string_alloc(); @@ -52,7 +54,7 @@ static void nfc_cli(Cli* cli, FuriString* args, void* context) { } if(furi_hal_rtc_is_flag_set(FuriHalRtcFlagDebug)) { if(furi_string_cmp_str(cmd, "field") == 0) { - nfc_cli_field(cli, args); + nfc_cli_field(pipe, args); break; } } diff --git a/applications/main/onewire/onewire_cli.c b/applications/main/onewire/onewire_cli.c index 74ca6bc1f..63e3d696f 100644 --- a/applications/main/onewire/onewire_cli.c +++ b/applications/main/onewire/onewire_cli.c @@ -2,31 +2,18 @@ #include #include - -#include +#include #include #include -static void onewire_cli(Cli* cli, FuriString* args, void* context); - -void onewire_on_system_start(void) { -#ifdef SRV_CLI - Cli* cli = furi_record_open(RECORD_CLI); - cli_add_command(cli, "onewire", CliCommandFlagDefault, onewire_cli, cli); - furi_record_close(RECORD_CLI); -#else - UNUSED(onewire_cli); -#endif -} - static void onewire_cli_print_usage(void) { printf("Usage:\r\n"); printf("onewire search\r\n"); } -static void onewire_cli_search(Cli* cli) { - UNUSED(cli); +static void onewire_cli_search(PipeSide* pipe) { + UNUSED(pipe); OneWireHost* onewire = onewire_host_alloc(&gpio_ibutton); Power* power = furi_record_open(RECORD_POWER); uint8_t address[8]; @@ -58,7 +45,7 @@ static void onewire_cli_search(Cli* cli) { furi_record_close(RECORD_POWER); } -void onewire_cli(Cli* cli, FuriString* args, void* context) { +static void onewire_cli(PipeSide* pipe, FuriString* args, void* context) { UNUSED(context); FuriString* cmd; cmd = furi_string_alloc(); @@ -70,8 +57,18 @@ void onewire_cli(Cli* cli, FuriString* args, void* context) { } if(furi_string_cmp_str(cmd, "search") == 0) { - onewire_cli_search(cli); + onewire_cli_search(pipe); } furi_string_free(cmd); } + +void onewire_on_system_start(void) { +#ifdef SRV_CLI + Cli* cli = furi_record_open(RECORD_CLI); + cli_add_command(cli, "onewire", CliCommandFlagDefault, onewire_cli, cli); + furi_record_close(RECORD_CLI); +#else + UNUSED(onewire_cli); +#endif +} diff --git a/applications/main/subghz/helpers/subghz_chat.c b/applications/main/subghz/helpers/subghz_chat.c index 9945b69c8..5c55aedeb 100644 --- a/applications/main/subghz/helpers/subghz_chat.c +++ b/applications/main/subghz/helpers/subghz_chat.c @@ -1,5 +1,6 @@ #include "subghz_chat.h" #include +#include #define TAG "SubGhzChat" @@ -14,7 +15,7 @@ struct SubGhzChatWorker { FuriMessageQueue* event_queue; uint32_t last_time_rx_data; - Cli* cli; + PipeSide* pipe; }; /** Worker thread @@ -30,7 +31,7 @@ static int32_t subghz_chat_worker_thread(void* context) { event.event = SubGhzChatEventUserEntrance; furi_message_queue_put(instance->event_queue, &event, 0); while(instance->worker_running) { - if(cli_read_timeout(instance->cli, (uint8_t*)&c, 1, 1000) == 1) { + if(pipe_receive(instance->pipe, (uint8_t*)&c, 1) == 1) { event.event = SubGhzChatEventInputData; event.c = c; furi_message_queue_put(instance->event_queue, &event, FuriWaitForever); @@ -55,10 +56,10 @@ static void subghz_chat_worker_update_rx_event_chat(void* context) { furi_message_queue_put(instance->event_queue, &event, FuriWaitForever); } -SubGhzChatWorker* subghz_chat_worker_alloc(Cli* cli) { +SubGhzChatWorker* subghz_chat_worker_alloc(PipeSide* pipe) { SubGhzChatWorker* instance = malloc(sizeof(SubGhzChatWorker)); - instance->cli = cli; + instance->pipe = pipe; instance->thread = furi_thread_alloc_ex("SubGhzChat", 2048, subghz_chat_worker_thread, instance); diff --git a/applications/main/subghz/helpers/subghz_chat.h b/applications/main/subghz/helpers/subghz_chat.h index 2c454b75d..0d1497506 100644 --- a/applications/main/subghz/helpers/subghz_chat.h +++ b/applications/main/subghz/helpers/subghz_chat.h @@ -2,6 +2,7 @@ #include "../subghz_i.h" #include #include +#include typedef struct SubGhzChatWorker SubGhzChatWorker; @@ -19,7 +20,7 @@ typedef struct { char c; } SubGhzChatEvent; -SubGhzChatWorker* subghz_chat_worker_alloc(Cli* cli); +SubGhzChatWorker* subghz_chat_worker_alloc(PipeSide* pipe); void subghz_chat_worker_free(SubGhzChatWorker* instance); bool subghz_chat_worker_start( SubGhzChatWorker* instance, diff --git a/applications/main/subghz/subghz_cli.c b/applications/main/subghz/subghz_cli.c index 54e02196d..a07ea5a7e 100644 --- a/applications/main/subghz/subghz_cli.c +++ b/applications/main/subghz/subghz_cli.c @@ -4,6 +4,7 @@ #include #include +#include #include #include @@ -16,6 +17,7 @@ #include #include +#include #include "helpers/subghz_chat.h" @@ -61,7 +63,7 @@ static SubGhzEnvironment* subghz_cli_environment_init(void) { return environment; } -void subghz_cli_command_tx_carrier(Cli* cli, FuriString* args, void* context) { +void subghz_cli_command_tx_carrier(PipeSide* pipe, FuriString* args, void* context) { UNUSED(context); uint32_t frequency = 433920000; @@ -91,7 +93,7 @@ void subghz_cli_command_tx_carrier(Cli* cli, FuriString* args, void* context) { if(furi_hal_subghz_tx()) { printf("Transmitting at frequency %lu Hz\r\n", frequency); printf("Press CTRL+C to stop\r\n"); - while(!cli_cmd_interrupt_received(cli)) { + while(!cli_is_pipe_broken_or_is_etx_next_char(pipe)) { furi_delay_ms(250); } } else { @@ -104,7 +106,7 @@ void subghz_cli_command_tx_carrier(Cli* cli, FuriString* args, void* context) { furi_hal_power_suppress_charge_exit(); } -void subghz_cli_command_rx_carrier(Cli* cli, FuriString* args, void* context) { +void subghz_cli_command_rx_carrier(PipeSide* pipe, FuriString* args, void* context) { UNUSED(context); uint32_t frequency = 433920000; @@ -132,7 +134,7 @@ void subghz_cli_command_rx_carrier(Cli* cli, FuriString* args, void* context) { furi_hal_subghz_rx(); - while(!cli_cmd_interrupt_received(cli)) { + while(!cli_is_pipe_broken_or_is_etx_next_char(pipe)) { furi_delay_ms(250); printf("RSSI: %03.1fdbm\r", (double)furi_hal_subghz_get_rssi()); fflush(stdout); @@ -165,7 +167,7 @@ static const SubGhzDevice* subghz_cli_command_get_device(uint32_t* device_ind) { return device; } -void subghz_cli_command_tx(Cli* cli, FuriString* args, void* context) { +void subghz_cli_command_tx(PipeSide* pipe, FuriString* args, void* context) { UNUSED(context); uint32_t frequency = 433920000; uint32_t key = 0x0074BADE; @@ -235,7 +237,9 @@ void subghz_cli_command_tx(Cli* cli, FuriString* args, void* context) { furi_hal_power_suppress_charge_enter(); if(subghz_devices_start_async_tx(device, subghz_transmitter_yield, transmitter)) { - while(!(subghz_devices_is_async_complete_tx(device) || cli_cmd_interrupt_received(cli))) { + while( + !(subghz_devices_is_async_complete_tx(device) || + cli_is_pipe_broken_or_is_etx_next_char(pipe))) { printf("."); fflush(stdout); furi_delay_ms(333); @@ -292,7 +296,7 @@ static void subghz_cli_command_rx_callback( furi_string_free(text); } -void subghz_cli_command_rx(Cli* cli, FuriString* args, void* context) { +void subghz_cli_command_rx(PipeSide* pipe, FuriString* args, void* context) { UNUSED(context); uint32_t frequency = 433920000; uint32_t device_ind = 0; // 0 - CC1101_INT, 1 - CC1101_EXT @@ -348,7 +352,7 @@ void subghz_cli_command_rx(Cli* cli, FuriString* args, void* context) { frequency, device_ind); LevelDuration level_duration; - while(!cli_cmd_interrupt_received(cli)) { + while(!cli_is_pipe_broken_or_is_etx_next_char(pipe)) { int ret = furi_stream_buffer_receive( instance->stream, &level_duration, sizeof(LevelDuration), 10); if(ret == sizeof(LevelDuration)) { @@ -381,7 +385,7 @@ void subghz_cli_command_rx(Cli* cli, FuriString* args, void* context) { free(instance); } -void subghz_cli_command_rx_raw(Cli* cli, FuriString* args, void* context) { +void subghz_cli_command_rx_raw(PipeSide* pipe, FuriString* args, void* context) { UNUSED(context); uint32_t frequency = 433920000; @@ -419,7 +423,7 @@ void subghz_cli_command_rx_raw(Cli* cli, FuriString* args, void* context) { printf("Listening at %lu. Press CTRL+C to stop\r\n", frequency); LevelDuration level_duration; size_t counter = 0; - while(!cli_cmd_interrupt_received(cli)) { + while(!cli_is_pipe_broken_or_is_etx_next_char(pipe)) { int ret = furi_stream_buffer_receive( instance->stream, &level_duration, sizeof(LevelDuration), 10); if(ret == 0) { @@ -455,7 +459,7 @@ void subghz_cli_command_rx_raw(Cli* cli, FuriString* args, void* context) { free(instance); } -void subghz_cli_command_decode_raw(Cli* cli, FuriString* args, void* context) { +void subghz_cli_command_decode_raw(PipeSide* pipe, FuriString* args, void* context) { UNUSED(context); FuriString* file_name; file_name = furi_string_alloc(); @@ -525,7 +529,7 @@ void subghz_cli_command_decode_raw(Cli* cli, FuriString* args, void* context) { furi_string_get_cstr(file_name)); LevelDuration level_duration; - while(!cli_cmd_interrupt_received(cli)) { + while(!cli_is_pipe_broken_or_is_etx_next_char(pipe)) { furi_delay_us(500); //you need to have time to read from the file from the SD card level_duration = subghz_file_encoder_worker_get_level_duration(file_worker_encoder); if(!level_duration_is_reset(level_duration)) { @@ -570,7 +574,7 @@ static FuriHalSubGhzPreset subghz_cli_get_preset_name(const char* preset_name) { return preset; } -void subghz_cli_command_tx_from_file(Cli* cli, FuriString* args, void* context) { // -V524 +void subghz_cli_command_tx_from_file(PipeSide* pipe, FuriString* args, void* context) { // -V524 UNUSED(context); FuriString* file_name; file_name = furi_string_alloc(); @@ -774,7 +778,7 @@ void subghz_cli_command_tx_from_file(Cli* cli, FuriString* args, void* context) if(subghz_devices_start_async_tx(device, subghz_transmitter_yield, transmitter)) { while( !(subghz_devices_is_async_complete_tx(device) || - cli_cmd_interrupt_received(cli))) { + cli_is_pipe_broken_or_is_etx_next_char(pipe))) { printf("."); fflush(stdout); furi_delay_ms(333); @@ -788,11 +792,11 @@ void subghz_cli_command_tx_from_file(Cli* cli, FuriString* args, void* context) if(!strcmp(furi_string_get_cstr(temp_str), "RAW")) { subghz_transmitter_stop(transmitter); repeat--; - if(!cli_cmd_interrupt_received(cli) && repeat) + if(!cli_is_pipe_broken_or_is_etx_next_char(pipe) && repeat) subghz_transmitter_deserialize(transmitter, fff_data_raw); } - } while(!cli_cmd_interrupt_received(cli) && + } while(!cli_is_pipe_broken_or_is_etx_next_char(pipe) && (repeat && !strcmp(furi_string_get_cstr(temp_str), "RAW"))); subghz_devices_sleep(device); @@ -837,8 +841,8 @@ static void subghz_cli_command_print_usage(void) { } } -static void subghz_cli_command_encrypt_keeloq(Cli* cli, FuriString* args) { - UNUSED(cli); +static void subghz_cli_command_encrypt_keeloq(PipeSide* pipe, FuriString* args) { + UNUSED(pipe); uint8_t iv[16]; FuriString* source; @@ -880,8 +884,8 @@ static void subghz_cli_command_encrypt_keeloq(Cli* cli, FuriString* args) { furi_string_free(source); } -static void subghz_cli_command_encrypt_raw(Cli* cli, FuriString* args) { - UNUSED(cli); +static void subghz_cli_command_encrypt_raw(PipeSide* pipe, FuriString* args) { + UNUSED(pipe); uint8_t iv[16]; FuriString* source; @@ -917,7 +921,7 @@ static void subghz_cli_command_encrypt_raw(Cli* cli, FuriString* args) { furi_string_free(source); } -static void subghz_cli_command_chat(Cli* cli, FuriString* args) { +static void subghz_cli_command_chat(PipeSide* pipe, FuriString* args) { uint32_t frequency = 433920000; uint32_t device_ind = 0; // 0 - CC1101_INT, 1 - CC1101_EXT @@ -951,7 +955,7 @@ static void subghz_cli_command_chat(Cli* cli, FuriString* args) { return; } - SubGhzChatWorker* subghz_chat = subghz_chat_worker_alloc(cli); + SubGhzChatWorker* subghz_chat = subghz_chat_worker_alloc(pipe); if(!subghz_chat_worker_start(subghz_chat, device, frequency)) { printf("Startup error SubGhzChatWorker\r\n"); @@ -992,13 +996,12 @@ static void subghz_cli_command_chat(Cli* cli, FuriString* args) { chat_event = subghz_chat_worker_get_event_chat(subghz_chat); switch(chat_event.event) { case SubGhzChatEventInputData: - if(chat_event.c == CliSymbolAsciiETX) { + if(chat_event.c == CliKeyETX) { printf("\r\n"); chat_event.event = SubGhzChatEventUserExit; subghz_chat_worker_put_event_chat(subghz_chat, &chat_event); break; - } else if( - (chat_event.c == CliSymbolAsciiBackspace) || (chat_event.c == CliSymbolAsciiDel)) { + } else if((chat_event.c == CliKeyBackspace) || (chat_event.c == CliKeyDEL)) { size_t len = furi_string_utf8_length(input); if(len > furi_string_utf8_length(name)) { printf("%s", "\e[D\e[1P"); @@ -1020,7 +1023,7 @@ static void subghz_cli_command_chat(Cli* cli, FuriString* args) { } furi_string_set(input, sysmsg); } - } else if(chat_event.c == CliSymbolAsciiCR) { + } else if(chat_event.c == CliKeyCR) { printf("\r\n"); furi_string_push_back(input, '\r'); furi_string_push_back(input, '\n'); @@ -1034,7 +1037,7 @@ static void subghz_cli_command_chat(Cli* cli, FuriString* args) { furi_string_printf(input, "%s", furi_string_get_cstr(name)); printf("%s", furi_string_get_cstr(input)); fflush(stdout); - } else if(chat_event.c == CliSymbolAsciiLF) { + } else if(chat_event.c == CliKeyLF) { //cut out the symbol \n } else { putc(chat_event.c, stdout); @@ -1088,7 +1091,7 @@ static void subghz_cli_command_chat(Cli* cli, FuriString* args) { break; } } - if(!cli_is_connected(cli)) { + if(!cli_is_pipe_broken_or_is_etx_next_char(pipe)) { printf("\r\n"); chat_event.event = SubGhzChatEventUserExit; subghz_chat_worker_put_event_chat(subghz_chat, &chat_event); @@ -1113,7 +1116,7 @@ static void subghz_cli_command_chat(Cli* cli, FuriString* args) { printf("\r\nExit chat\r\n"); } -static void subghz_cli_command(Cli* cli, FuriString* args, void* context) { +static void subghz_cli_command(PipeSide* pipe, FuriString* args, void* context) { FuriString* cmd; cmd = furi_string_alloc(); @@ -1124,53 +1127,53 @@ static void subghz_cli_command(Cli* cli, FuriString* args, void* context) { } if(furi_string_cmp_str(cmd, "chat") == 0) { - subghz_cli_command_chat(cli, args); + subghz_cli_command_chat(pipe, args); break; } if(furi_string_cmp_str(cmd, "tx") == 0) { - subghz_cli_command_tx(cli, args, context); + subghz_cli_command_tx(pipe, args, context); break; } if(furi_string_cmp_str(cmd, "rx") == 0) { - subghz_cli_command_rx(cli, args, context); + subghz_cli_command_rx(pipe, args, context); break; } if(furi_string_cmp_str(cmd, "rx_raw") == 0) { - subghz_cli_command_rx_raw(cli, args, context); + subghz_cli_command_rx_raw(pipe, args, context); break; } if(furi_string_cmp_str(cmd, "decode_raw") == 0) { - subghz_cli_command_decode_raw(cli, args, context); + subghz_cli_command_decode_raw(pipe, args, context); break; } if(furi_string_cmp_str(cmd, "tx_from_file") == 0) { - subghz_cli_command_tx_from_file(cli, args, context); + subghz_cli_command_tx_from_file(pipe, args, context); break; } if(furi_hal_rtc_is_flag_set(FuriHalRtcFlagDebug)) { if(furi_string_cmp_str(cmd, "encrypt_keeloq") == 0) { - subghz_cli_command_encrypt_keeloq(cli, args); + subghz_cli_command_encrypt_keeloq(pipe, args); break; } if(furi_string_cmp_str(cmd, "encrypt_raw") == 0) { - subghz_cli_command_encrypt_raw(cli, args); + subghz_cli_command_encrypt_raw(pipe, args); break; } if(furi_string_cmp_str(cmd, "tx_carrier") == 0) { - subghz_cli_command_tx_carrier(cli, args, context); + subghz_cli_command_tx_carrier(pipe, args, context); break; } if(furi_string_cmp_str(cmd, "rx_carrier") == 0) { - subghz_cli_command_rx_carrier(cli, args, context); + subghz_cli_command_rx_carrier(pipe, args, context); break; } } diff --git a/applications/services/application.fam b/applications/services/application.fam index 9ffb26dd6..a1a0429fa 100644 --- a/applications/services/application.fam +++ b/applications/services/application.fam @@ -3,6 +3,7 @@ App( name="Basic services", apptype=FlipperAppType.METAPACKAGE, provides=[ + "cli_vcp", "crypto_start", "rpc_start", "expansion_start", diff --git a/applications/services/bt/bt_cli.c b/applications/services/bt/bt_cli.c index 7505c424d..9ef3ef8a2 100644 --- a/applications/services/bt/bt_cli.c +++ b/applications/services/bt/bt_cli.c @@ -2,14 +2,15 @@ #include #include #include +#include #include #include "bt_settings.h" #include "bt_service/bt.h" #include -static void bt_cli_command_hci_info(Cli* cli, FuriString* args, void* context) { - UNUSED(cli); +static void bt_cli_command_hci_info(PipeSide* pipe, FuriString* args, void* context) { + UNUSED(pipe); UNUSED(args); UNUSED(context); FuriString* buffer; @@ -19,7 +20,7 @@ static void bt_cli_command_hci_info(Cli* cli, FuriString* args, void* context) { furi_string_free(buffer); } -static void bt_cli_command_carrier_tx(Cli* cli, FuriString* args, void* context) { +static void bt_cli_command_carrier_tx(PipeSide* pipe, FuriString* args, void* context) { UNUSED(context); int channel = 0; int power = 0; @@ -41,7 +42,7 @@ static void bt_cli_command_carrier_tx(Cli* cli, FuriString* args, void* context) printf("Press CTRL+C to stop\r\n"); furi_hal_bt_start_tone_tx(channel, 0x19 + power); - while(!cli_cmd_interrupt_received(cli)) { + while(!cli_is_pipe_broken_or_is_etx_next_char(pipe)) { furi_delay_ms(250); } furi_hal_bt_stop_tone_tx(); @@ -51,7 +52,7 @@ static void bt_cli_command_carrier_tx(Cli* cli, FuriString* args, void* context) } while(false); } -static void bt_cli_command_carrier_rx(Cli* cli, FuriString* args, void* context) { +static void bt_cli_command_carrier_rx(PipeSide* pipe, FuriString* args, void* context) { UNUSED(context); int channel = 0; @@ -69,7 +70,7 @@ static void bt_cli_command_carrier_rx(Cli* cli, FuriString* args, void* context) furi_hal_bt_start_packet_rx(channel, 1); - while(!cli_cmd_interrupt_received(cli)) { + while(!cli_is_pipe_broken_or_is_etx_next_char(pipe)) { furi_delay_ms(250); printf("RSSI: %6.1f dB\r", (double)furi_hal_bt_get_rssi()); fflush(stdout); @@ -82,7 +83,7 @@ static void bt_cli_command_carrier_rx(Cli* cli, FuriString* args, void* context) } while(false); } -static void bt_cli_command_packet_tx(Cli* cli, FuriString* args, void* context) { +static void bt_cli_command_packet_tx(PipeSide* pipe, FuriString* args, void* context) { UNUSED(context); int channel = 0; int pattern = 0; @@ -119,7 +120,7 @@ static void bt_cli_command_packet_tx(Cli* cli, FuriString* args, void* context) printf("Press CTRL+C to stop\r\n"); furi_hal_bt_start_packet_tx(channel, pattern, datarate); - while(!cli_cmd_interrupt_received(cli)) { + while(!cli_is_pipe_broken_or_is_etx_next_char(pipe)) { furi_delay_ms(250); } furi_hal_bt_stop_packet_test(); @@ -130,7 +131,7 @@ static void bt_cli_command_packet_tx(Cli* cli, FuriString* args, void* context) } while(false); } -static void bt_cli_command_packet_rx(Cli* cli, FuriString* args, void* context) { +static void bt_cli_command_packet_rx(PipeSide* pipe, FuriString* args, void* context) { UNUSED(context); int channel = 0; int datarate = 1; @@ -152,7 +153,7 @@ static void bt_cli_command_packet_rx(Cli* cli, FuriString* args, void* context) printf("Press CTRL+C to stop\r\n"); furi_hal_bt_start_packet_rx(channel, datarate); - while(!cli_cmd_interrupt_received(cli)) { + while(!cli_is_pipe_broken_or_is_etx_next_char(pipe)) { furi_delay_ms(250); printf("RSSI: %03.1f dB\r", (double)furi_hal_bt_get_rssi()); fflush(stdout); @@ -179,7 +180,7 @@ static void bt_cli_print_usage(void) { } } -static void bt_cli(Cli* cli, FuriString* args, void* context) { +static void bt_cli(PipeSide* pipe, FuriString* args, void* context) { UNUSED(context); furi_record_open(RECORD_BT); @@ -194,24 +195,24 @@ static void bt_cli(Cli* cli, FuriString* args, void* context) { break; } if(furi_string_cmp_str(cmd, "hci_info") == 0) { - bt_cli_command_hci_info(cli, args, NULL); + bt_cli_command_hci_info(pipe, args, NULL); break; } if(furi_hal_rtc_is_flag_set(FuriHalRtcFlagDebug) && furi_hal_bt_is_testing_supported()) { if(furi_string_cmp_str(cmd, "tx_carrier") == 0) { - bt_cli_command_carrier_tx(cli, args, NULL); + bt_cli_command_carrier_tx(pipe, args, NULL); break; } if(furi_string_cmp_str(cmd, "rx_carrier") == 0) { - bt_cli_command_carrier_rx(cli, args, NULL); + bt_cli_command_carrier_rx(pipe, args, NULL); break; } if(furi_string_cmp_str(cmd, "tx_packet") == 0) { - bt_cli_command_packet_tx(cli, args, NULL); + bt_cli_command_packet_tx(pipe, args, NULL); break; } if(furi_string_cmp_str(cmd, "rx_packet") == 0) { - bt_cli_command_packet_rx(cli, args, NULL); + bt_cli_command_packet_rx(pipe, args, NULL); break; } } diff --git a/applications/services/cli/application.fam b/applications/services/cli/application.fam index 7a57bb607..60af336e5 100644 --- a/applications/services/cli/application.fam +++ b/applications/services/cli/application.fam @@ -1,10 +1,34 @@ App( appid="cli", - name="CliSrv", - apptype=FlipperAppType.SERVICE, - entry_point="cli_srv", + apptype=FlipperAppType.STARTUP, + entry_point="cli_on_system_start", cdefines=["SRV_CLI"], - stack_size=4 * 1024, - order=30, - sdk_headers=["cli.h", "cli_vcp.h"], + sources=[ + "cli.c", + "shell/cli_shell.c", + "shell/cli_shell_line.c", + "cli_commands.c", + "cli_command_gpio.c", + "cli_ansi.c", + ], + sdk_headers=[ + "cli.h", + "cli_ansi.h", + ], + # This STARTUP has to be processed before those that depend on the "cli" record. + # "cli" used to be a SERVICE, but it's been converted into a STARTUP in order to + # reduce RAM usage. The "block until record has been created" mechanism + # unfortunately leads to a deadlock if the STARTUPs are processed sequentially. + order=0, +) + +App( + appid="cli_vcp", + name="CliVcpSrv", + apptype=FlipperAppType.SERVICE, + entry_point="cli_vcp_srv", + stack_size=1024, + order=40, + sdk_headers=["cli_vcp.h"], + sources=["cli_vcp.c"], ) diff --git a/applications/services/cli/cli.c b/applications/services/cli/cli.c index c2a0b9cb1..b51715660 100644 --- a/applications/services/cli/cli.c +++ b/applications/services/cli/cli.c @@ -1,406 +1,57 @@ +#include "cli.h" #include "cli_i.h" #include "cli_commands.h" -#include "cli_vcp.h" -#include -#include +#include "cli_ansi.h" +#include -#define TAG "CliSrv" +#define TAG "cli" -#define CLI_INPUT_LEN_LIMIT 256 +struct Cli { + CliCommandTree_t commands; + FuriMutex* mutex; +}; Cli* cli_alloc(void) { Cli* cli = malloc(sizeof(Cli)); - CliCommandTree_init(cli->commands); - - cli->last_line = furi_string_alloc(); - cli->line = furi_string_alloc(); - - cli->session = NULL; - cli->mutex = furi_mutex_alloc(FuriMutexTypeNormal); - - cli->idle_sem = furi_semaphore_alloc(1, 0); - return cli; } -void cli_putc(Cli* cli, char c) { - furi_check(cli); - if(cli->session != NULL) { - cli->session->tx((uint8_t*)&c, 1); - } -} - -char cli_getc(Cli* cli) { - furi_check(cli); - char c = 0; - if(cli->session != NULL) { - if(cli->session->rx((uint8_t*)&c, 1, FuriWaitForever) == 0) { - cli_reset(cli); - furi_delay_tick(10); - } - } else { - cli_reset(cli); - furi_delay_tick(10); - } - return c; -} - -void cli_write(Cli* cli, const uint8_t* buffer, size_t size) { - furi_check(cli); - if(cli->session != NULL) { - cli->session->tx(buffer, size); - } -} - -size_t cli_read(Cli* cli, uint8_t* buffer, size_t size) { - furi_check(cli); - if(cli->session != NULL) { - return cli->session->rx(buffer, size, FuriWaitForever); - } else { - return 0; - } -} - -size_t cli_read_timeout(Cli* cli, uint8_t* buffer, size_t size, uint32_t timeout) { - furi_check(cli); - if(cli->session != NULL) { - return cli->session->rx(buffer, size, timeout); - } else { - return 0; - } -} - -bool cli_is_connected(Cli* cli) { - furi_check(cli); - if(cli->session != NULL) { - return cli->session->is_connected(); - } - return false; -} - -bool cli_cmd_interrupt_received(Cli* cli) { - furi_check(cli); - char c = '\0'; - if(cli_is_connected(cli)) { - if(cli->session->rx((uint8_t*)&c, 1, 0) == 1) { - return c == CliSymbolAsciiETX; - } - } else { - return true; - } - return false; -} - -void cli_print_usage(const char* cmd, const char* usage, const char* arg) { - furi_check(cmd); - furi_check(arg); - furi_check(usage); - - printf("%s: illegal option -- %s\r\nusage: %s %s", cmd, arg, cmd, usage); -} - -void cli_motd(void) { - printf("\r\n" - " _.-------.._ -,\r\n" - " .-\"```\"--..,,_/ /`-, -, \\ \r\n" - " .:\" /:/ /'\\ \\ ,_..., `. | |\r\n" - " / ,----/:/ /`\\ _\\~`_-\"` _;\r\n" - " ' / /`\"\"\"'\\ \\ \\.~`_-' ,-\"'/ \r\n" - " | | | 0 | | .-' ,/` /\r\n" - " | ,..\\ \\ ,.-\"` ,/` /\r\n" - " ; : `/`\"\"\\` ,/--==,/-----,\r\n" - " | `-...| -.___-Z:_______J...---;\r\n" - " : ` _-'\r\n" - " _L_ _ ___ ___ ___ ___ ____--\"`___ _ ___\r\n" - "| __|| | |_ _|| _ \\| _ \\| __|| _ \\ / __|| | |_ _|\r\n" - "| _| | |__ | | | _/| _/| _| | / | (__ | |__ | |\r\n" - "|_| |____||___||_| |_| |___||_|_\\ \\___||____||___|\r\n" - "\r\n" - "Welcome to Flipper Zero Command Line Interface!\r\n" - "Read the manual: https://docs.flipper.net/development/cli\r\n" - "Run `help` or `?` to list available commands\r\n" - "\r\n"); - - const Version* firmware_version = furi_hal_version_get_firmware_version(); - if(firmware_version) { - printf( - "Firmware version: %s %s (%s%s built on %s)\r\n", - version_get_gitbranch(firmware_version), - version_get_version(firmware_version), - version_get_githash(firmware_version), - version_get_dirty_flag(firmware_version) ? "-dirty" : "", - version_get_builddate(firmware_version)); - } -} - -void cli_nl(Cli* cli) { - UNUSED(cli); - printf("\r\n"); -} - -void cli_prompt(Cli* cli) { - UNUSED(cli); - printf("\r\n>: %s", furi_string_get_cstr(cli->line)); - fflush(stdout); -} - -void cli_reset(Cli* cli) { - // cli->last_line is cleared and cli->line's buffer moved to cli->last_line - furi_string_move(cli->last_line, cli->line); - // Reiniting cli->line - cli->line = furi_string_alloc(); - cli->cursor_position = 0; -} - -static void cli_handle_backspace(Cli* cli) { - if(cli->cursor_position > 0) { - furi_assert(furi_string_size(cli->line) > 0); - // Other side - printf("\e[D\e[1P"); - fflush(stdout); - // Our side - furi_string_replace_at(cli->line, cli->cursor_position - 1, 1, ""); - - cli->cursor_position--; - } else { - cli_putc(cli, CliSymbolAsciiBell); - } -} - -static void cli_normalize_line(Cli* cli) { - furi_string_trim(cli->line); - cli->cursor_position = furi_string_size(cli->line); -} - -static void cli_execute_command(Cli* cli, CliCommand* command, FuriString* args) { - if(!(command->flags & CliCommandFlagInsomniaSafe)) { - furi_hal_power_insomnia_enter(); - } - - // Ensure that we running alone - if(!(command->flags & CliCommandFlagParallelSafe)) { - Loader* loader = furi_record_open(RECORD_LOADER); - bool safety_lock = loader_lock(loader); - if(safety_lock) { - // Execute command - command->callback(cli, args, command->context); - loader_unlock(loader); - } else { - printf("Other application is running, close it first"); - } - furi_record_close(RECORD_LOADER); - } else { - // Execute command - command->callback(cli, args, command->context); - } - - if(!(command->flags & CliCommandFlagInsomniaSafe)) { - furi_hal_power_insomnia_exit(); - } -} - -static void cli_handle_enter(Cli* cli) { - cli_normalize_line(cli); - - if(furi_string_size(cli->line) == 0) { - cli_prompt(cli); - return; - } - - // Command and args container - FuriString* command; - command = furi_string_alloc(); - FuriString* args; - args = furi_string_alloc(); - - // Split command and args - size_t ws = furi_string_search_char(cli->line, ' '); - if(ws == FURI_STRING_FAILURE) { - furi_string_set(command, cli->line); - } else { - furi_string_set_n(command, cli->line, 0, ws); - furi_string_set_n(args, cli->line, ws, furi_string_size(cli->line)); - furi_string_trim(args); - } - - // Search for command - furi_check(furi_mutex_acquire(cli->mutex, FuriWaitForever) == FuriStatusOk); - CliCommand* cli_command_ptr = CliCommandTree_get(cli->commands, command); - - if(cli_command_ptr) { //-V547 - CliCommand cli_command; - memcpy(&cli_command, cli_command_ptr, sizeof(CliCommand)); - furi_check(furi_mutex_release(cli->mutex) == FuriStatusOk); - cli_nl(cli); - cli_execute_command(cli, &cli_command, args); - } else { - furi_check(furi_mutex_release(cli->mutex) == FuriStatusOk); - cli_nl(cli); - printf( - "`%s` command not found, use `help` or `?` to list all available commands", - furi_string_get_cstr(command)); - cli_putc(cli, CliSymbolAsciiBell); - } - - cli_reset(cli); - cli_prompt(cli); - - // Cleanup command and args - furi_string_free(command); - furi_string_free(args); -} - -static void cli_handle_autocomplete(Cli* cli) { - cli_normalize_line(cli); - - if(furi_string_size(cli->line) == 0) { - return; - } - - cli_nl(cli); - - // Prepare common base for autocomplete - FuriString* common; - common = furi_string_alloc(); - // Iterate throw commands - for - M_EACH(cli_command, cli->commands, CliCommandTree_t) { - // Process only if starts with line buffer - if(furi_string_start_with(*cli_command->key_ptr, cli->line)) { - // Show autocomplete option - printf("%s\r\n", furi_string_get_cstr(*cli_command->key_ptr)); - // Process common base for autocomplete - if(furi_string_size(common) > 0) { - // Choose shortest string - const size_t key_size = furi_string_size(*cli_command->key_ptr); - const size_t common_size = furi_string_size(common); - const size_t min_size = key_size > common_size ? common_size : key_size; - size_t i = 0; - while(i < min_size) { - // Stop when do not match - if(furi_string_get_char(*cli_command->key_ptr, i) != - furi_string_get_char(common, i)) { - break; - } - i++; - } - // Cut right part if any - furi_string_left(common, i); - } else { - // Start with something - furi_string_set(common, *cli_command->key_ptr); - } - } - } - // Replace line buffer if autocomplete better - if(furi_string_size(common) > furi_string_size(cli->line)) { - furi_string_set(cli->line, common); - cli->cursor_position = furi_string_size(cli->line); - } - // Cleanup - furi_string_free(common); - // Show prompt - cli_prompt(cli); -} - -static void cli_handle_escape(Cli* cli, char c) { - if(c == 'A') { - // Use previous command if line buffer is empty - if(furi_string_size(cli->line) == 0 && furi_string_cmp(cli->line, cli->last_line) != 0) { - // Set line buffer and cursor position - furi_string_set(cli->line, cli->last_line); - cli->cursor_position = furi_string_size(cli->line); - // Show new line to user - printf("%s", furi_string_get_cstr(cli->line)); - } - } else if(c == 'B') { - } else if(c == 'C') { - if(cli->cursor_position < furi_string_size(cli->line)) { - cli->cursor_position++; - printf("\e[C"); - } - } else if(c == 'D') { - if(cli->cursor_position > 0) { - cli->cursor_position--; - printf("\e[D"); - } - } - fflush(stdout); -} - -void cli_process_input(Cli* cli) { - char in_chr = cli_getc(cli); - size_t rx_len; - - if(in_chr == CliSymbolAsciiTab) { - cli_handle_autocomplete(cli); - } else if(in_chr == CliSymbolAsciiSOH) { - furi_delay_ms(33); // We are too fast, Minicom is not ready yet - cli_motd(); - cli_prompt(cli); - } else if(in_chr == CliSymbolAsciiETX) { - cli_reset(cli); - cli_prompt(cli); - } else if(in_chr == CliSymbolAsciiEOT) { - cli_reset(cli); - } else if(in_chr == CliSymbolAsciiEsc) { - rx_len = cli_read(cli, (uint8_t*)&in_chr, 1); - if((rx_len > 0) && (in_chr == '[')) { - cli_read(cli, (uint8_t*)&in_chr, 1); - cli_handle_escape(cli, in_chr); - } else { - cli_putc(cli, CliSymbolAsciiBell); - } - } else if(in_chr == CliSymbolAsciiBackspace || in_chr == CliSymbolAsciiDel) { - cli_handle_backspace(cli); - } else if(in_chr == CliSymbolAsciiCR) { - cli_handle_enter(cli); - } else if( - (in_chr >= 0x20 && in_chr < 0x7F) && //-V560 - (furi_string_size(cli->line) < CLI_INPUT_LEN_LIMIT)) { - if(cli->cursor_position == furi_string_size(cli->line)) { - furi_string_push_back(cli->line, in_chr); - cli_putc(cli, in_chr); - } else { - // Insert character to line buffer - const char in_str[2] = {in_chr, 0}; - furi_string_replace_at(cli->line, cli->cursor_position, 0, in_str); - - // Print character in replace mode - printf("\e[4h%c\e[4l", in_chr); - fflush(stdout); - } - cli->cursor_position++; - } else { - cli_putc(cli, CliSymbolAsciiBell); - } -} - void cli_add_command( Cli* cli, const char* name, CliCommandFlag flags, - CliCallback callback, + CliExecuteCallback callback, void* context) { + cli_add_command_ex(cli, name, flags, callback, context, CLI_BUILTIN_COMMAND_STACK_SIZE); +} + +void cli_add_command_ex( + Cli* cli, + const char* name, + CliCommandFlag flags, + CliExecuteCallback callback, + void* context, + size_t stack_size) { furi_check(cli); + furi_check(name); + furi_check(callback); + FuriString* name_str; name_str = furi_string_alloc_set(name); - furi_string_trim(name_str); + // command cannot contain spaces + furi_check(furi_string_search_char(name_str, ' ') == FURI_STRING_FAILURE); - size_t name_replace; - do { - name_replace = furi_string_replace(name_str, " ", "_"); - } while(name_replace != FURI_STRING_FAILURE); - - CliCommand c; - c.callback = callback; - c.context = context; - c.flags = flags; + CliCommand command = { + .context = context, + .execute_callback = callback, + .flags = flags, + .stack_depth = stack_size, + }; furi_check(furi_mutex_acquire(cli->mutex, FuriWaitForever) == FuriStatusOk); - CliCommandTree_set_at(cli->commands, name_str, c); + CliCommandTree_set_at(cli->commands, name_str, command); furi_check(furi_mutex_release(cli->mutex) == FuriStatusOk); furi_string_free(name_str); @@ -424,61 +75,49 @@ void cli_delete_command(Cli* cli, const char* name) { furi_string_free(name_str); } -void cli_session_open(Cli* cli, const void* session) { - furi_check(cli); - +bool cli_get_command(Cli* cli, FuriString* command, CliCommand* result) { + furi_assert(cli); furi_check(furi_mutex_acquire(cli->mutex, FuriWaitForever) == FuriStatusOk); - cli->session = session; - if(cli->session != NULL) { - cli->session->init(); - furi_thread_set_stdout_callback(cli->session->tx_stdout, NULL); - } else { - furi_thread_set_stdout_callback(NULL, NULL); - } - furi_semaphore_release(cli->idle_sem); + CliCommand* data = CliCommandTree_get(cli->commands, command); + if(data) *result = *data; + furi_check(furi_mutex_release(cli->mutex) == FuriStatusOk); + + return !!data; } -void cli_session_close(Cli* cli) { - furi_check(cli); - +void cli_lock_commands(Cli* cli) { + furi_assert(cli); furi_check(furi_mutex_acquire(cli->mutex, FuriWaitForever) == FuriStatusOk); - if(cli->session != NULL) { - cli->session->deinit(); - } - cli->session = NULL; - furi_thread_set_stdout_callback(NULL, NULL); - furi_check(furi_mutex_release(cli->mutex) == FuriStatusOk); } -int32_t cli_srv(void* p) { - UNUSED(p); +void cli_unlock_commands(Cli* cli) { + furi_assert(cli); + furi_mutex_release(cli->mutex); +} + +CliCommandTree_t* cli_get_commands(Cli* cli) { + furi_assert(cli); + return &cli->commands; +} + +bool cli_is_pipe_broken_or_is_etx_next_char(PipeSide* side) { + if(pipe_state(side) == PipeStateBroken) return true; + if(!pipe_bytes_available(side)) return false; + char c = getchar(); + return c == CliKeyETX; +} + +void cli_print_usage(const char* cmd, const char* usage, const char* arg) { + furi_check(cmd); + furi_check(arg); + furi_check(usage); + + printf("%s: illegal option -- %s\r\nusage: %s %s", cmd, arg, cmd, usage); +} + +void cli_on_system_start(void) { Cli* cli = cli_alloc(); - - // Init basic cli commands cli_commands_init(cli); - furi_record_create(RECORD_CLI, cli); - - if(cli->session != NULL) { - furi_thread_set_stdout_callback(cli->session->tx_stdout, NULL); - } else { - furi_thread_set_stdout_callback(NULL, NULL); - } - - if(furi_hal_rtc_get_boot_mode() == FuriHalRtcBootModeNormal) { - cli_session_open(cli, &cli_vcp); - } else { - FURI_LOG_W(TAG, "Skipping start in special boot mode"); - } - - while(1) { - if(cli->session != NULL) { - cli_process_input(cli); - } else { - furi_check(furi_semaphore_acquire(cli->idle_sem, FuriWaitForever) == FuriStatusOk); - } - } - - return 0; } diff --git a/applications/services/cli/cli.h b/applications/services/cli/cli.h index fe8f09032..211e89d88 100644 --- a/applications/services/cli/cli.h +++ b/applications/services/cli/cli.h @@ -1,64 +1,102 @@ /** * @file cli.h - * Cli API + * API for registering commands with the CLI */ #pragma once #include +#include +#include "cli_ansi.h" +#include #ifdef __cplusplus extern "C" { #endif -typedef enum { - CliSymbolAsciiSOH = 0x01, - CliSymbolAsciiETX = 0x03, - CliSymbolAsciiEOT = 0x04, - CliSymbolAsciiBell = 0x07, - CliSymbolAsciiBackspace = 0x08, - CliSymbolAsciiTab = 0x09, - CliSymbolAsciiLF = 0x0A, - CliSymbolAsciiCR = 0x0D, - CliSymbolAsciiEsc = 0x1B, - CliSymbolAsciiUS = 0x1F, - CliSymbolAsciiSpace = 0x20, - CliSymbolAsciiDel = 0x7F, -} CliSymbols; - -typedef enum { - CliCommandFlagDefault = 0, /**< Default, loader lock is used */ - CliCommandFlagParallelSafe = - (1 << 0), /**< Safe to run in parallel with other apps, loader lock is not used */ - CliCommandFlagInsomniaSafe = (1 << 1), /**< Safe to run with insomnia mode on */ -} CliCommandFlag; - #define RECORD_CLI "cli" +typedef enum { + CliCommandFlagDefault = 0, /**< Default */ + CliCommandFlagParallelSafe = (1 << 0), /**< Safe to run in parallel with other apps */ + CliCommandFlagInsomniaSafe = (1 << 1), /**< Safe to run with insomnia mode on */ + CliCommandFlagDontAttachStdio = (1 << 2), /**< Do no attach I/O pipe to thread stdio */ +} CliCommandFlag; + /** Cli type anonymous structure */ typedef struct Cli Cli; -/** Cli callback function pointer. Implement this interface and use - * add_cli_command - * @param args string with what was passed after command - * @param context pointer to whatever you gave us on cli_add_command +/** + * @brief CLI execution callback pointer. Implement this interface and use + * `add_cli_command`. + * + * This callback will be called from a separate thread spawned just for your + * command. The pipe will be installed as the thread's stdio, so you can use + * `printf`, `getchar` and other standard functions to communicate with the + * user. + * + * @param [in] pipe Pipe that can be used to send and receive data. If + * `CliCommandFlagDontAttachStdio` was not set, you can + * also use standard C functions (printf, getc, etc.) to + * access this pipe. + * @param [in] args String with what was passed after the command + * @param [in] context Whatever you provided to `cli_add_command` */ -typedef void (*CliCallback)(Cli* cli, FuriString* args, void* context); +typedef void (*CliExecuteCallback)(PipeSide* pipe, FuriString* args, void* context); -/** Add cli command Registers you command callback +/** + * @brief Registers a command with the CLI. Provides less options than the `_ex` + * counterpart. * - * @param cli pointer to cli instance - * @param name command name - * @param flags CliCommandFlag - * @param callback callback function - * @param context pointer to whatever we need to pass to callback + * @param [in] cli Pointer to CLI instance + * @param [in] name Command name + * @param [in] flags CliCommandFlag + * @param [in] callback Callback function + * @param [in] context Custom context */ void cli_add_command( Cli* cli, const char* name, CliCommandFlag flags, - CliCallback callback, + CliExecuteCallback callback, void* context); +/** + * @brief Registers a command with the CLI. Provides more options than the + * non-`_ex` counterpart. + * + * @param [in] cli Pointer to CLI instance + * @param [in] name Command name + * @param [in] flags CliCommandFlag + * @param [in] callback Callback function + * @param [in] context Custom context + * @param [in] stack_size Thread stack size + */ +void cli_add_command_ex( + Cli* cli, + const char* name, + CliCommandFlag flags, + CliExecuteCallback callback, + void* context, + size_t stack_size); + +/** + * @brief Deletes a cli command + * + * @param [in] cli pointer to cli instance + * @param [in] name command name + */ +void cli_delete_command(Cli* cli, const char* name); + +/** + * @brief Detects if Ctrl+C has been pressed or session has been terminated + * + * @param [in] side Pointer to pipe side given to the command thread + * @warning This function also assumes that the pipe is installed as the + * thread's stdio + * @warning This function will consume 1 byte from the pipe + */ +bool cli_is_pipe_broken_or_is_etx_next_char(PipeSide* side); + /** Print unified cmd usage tip * * @param cmd cmd name @@ -67,68 +105,6 @@ void cli_add_command( */ void cli_print_usage(const char* cmd, const char* usage, const char* arg); -/** Delete cli command - * - * @param cli pointer to cli instance - * @param name command name - */ -void cli_delete_command(Cli* cli, const char* name); - -/** Read from terminal - * - * @param cli Cli instance - * @param buffer pointer to buffer - * @param size size of buffer in bytes - * - * @return bytes read - */ -size_t cli_read(Cli* cli, uint8_t* buffer, size_t size); - -/** Non-blocking read from terminal - * - * @param cli Cli instance - * @param buffer pointer to buffer - * @param size size of buffer in bytes - * @param timeout timeout value in ms - * - * @return bytes read - */ -size_t cli_read_timeout(Cli* cli, uint8_t* buffer, size_t size, uint32_t timeout); - -/** Non-blocking check for interrupt command received - * - * @param cli Cli instance - * - * @return true if received - */ -bool cli_cmd_interrupt_received(Cli* cli); - -/** Write to terminal Do it only from inside of cli call. - * - * @param cli Cli instance - * @param buffer pointer to buffer - * @param size size of buffer in bytes - */ -void cli_write(Cli* cli, const uint8_t* buffer, size_t size); - -/** Read character - * - * @param cli Cli instance - * - * @return char - */ -char cli_getc(Cli* cli); - -/** New line Send new ine sequence - */ -void cli_nl(Cli* cli); - -void cli_session_open(Cli* cli, const void* session); - -void cli_session_close(Cli* cli); - -bool cli_is_connected(Cli* cli); - #ifdef __cplusplus } #endif diff --git a/applications/services/cli/cli_ansi.c b/applications/services/cli/cli_ansi.c new file mode 100644 index 000000000..81167643b --- /dev/null +++ b/applications/services/cli/cli_ansi.c @@ -0,0 +1,123 @@ +#include "cli_ansi.h" + +typedef enum { + CliAnsiParserStateInitial, + CliAnsiParserStateEscape, + CliAnsiParserStateEscapeBrace, + CliAnsiParserStateEscapeBraceOne, + CliAnsiParserStateEscapeBraceOneSemicolon, + CliAnsiParserStateEscapeBraceOneSemicolonModifiers, +} CliAnsiParserState; + +struct CliAnsiParser { + CliAnsiParserState state; + CliModKey modifiers; +}; + +CliAnsiParser* cli_ansi_parser_alloc(void) { + CliAnsiParser* parser = malloc(sizeof(CliAnsiParser)); + return parser; +} + +void cli_ansi_parser_free(CliAnsiParser* parser) { + free(parser); +} + +/** + * @brief Converts a single character representing a special key into the enum + * representation + */ +static CliKey cli_ansi_key_from_mnemonic(char c) { + switch(c) { + case 'A': + return CliKeyUp; + case 'B': + return CliKeyDown; + case 'C': + return CliKeyRight; + case 'D': + return CliKeyLeft; + case 'F': + return CliKeyEnd; + case 'H': + return CliKeyHome; + default: + return CliKeyUnrecognized; + } +} + +#define PARSER_RESET_AND_RETURN(parser, modifiers_val, key_val) \ + do { \ + parser->state = CliAnsiParserStateInitial; \ + return (CliAnsiParserResult){ \ + .is_done = true, \ + .result = (CliKeyCombo){ \ + .modifiers = modifiers_val, \ + .key = key_val, \ + }}; \ + } while(0); + +CliAnsiParserResult cli_ansi_parser_feed(CliAnsiParser* parser, char c) { + switch(parser->state) { + case CliAnsiParserStateInitial: + // -> + if(c != CliKeyEsc) PARSER_RESET_AND_RETURN(parser, CliModKeyNo, c); // -V1048 + + // ... + parser->state = CliAnsiParserStateEscape; + break; + + case CliAnsiParserStateEscape: + // -> + if(c == CliKeyEsc) PARSER_RESET_AND_RETURN(parser, CliModKeyNo, c); + + // -> Alt + + if(c != '[') PARSER_RESET_AND_RETURN(parser, CliModKeyAlt, c); + + // [ ... + parser->state = CliAnsiParserStateEscapeBrace; + break; + + case CliAnsiParserStateEscapeBrace: + // [ -> + if(c != '1') PARSER_RESET_AND_RETURN(parser, CliModKeyNo, cli_ansi_key_from_mnemonic(c)); + + // [ 1 ... + parser->state = CliAnsiParserStateEscapeBraceOne; + break; + + case CliAnsiParserStateEscapeBraceOne: + // [ 1 -> error + if(c != ';') PARSER_RESET_AND_RETURN(parser, CliModKeyNo, CliKeyUnrecognized); + + // [ 1 ; ... + parser->state = CliAnsiParserStateEscapeBraceOneSemicolon; + break; + + case CliAnsiParserStateEscapeBraceOneSemicolon: + // [ 1 ; ... + parser->modifiers = (c - '0'); + parser->modifiers &= ~1; + parser->state = CliAnsiParserStateEscapeBraceOneSemicolonModifiers; + break; + + case CliAnsiParserStateEscapeBraceOneSemicolonModifiers: + // [ 1 ; -> + + PARSER_RESET_AND_RETURN(parser, parser->modifiers, cli_ansi_key_from_mnemonic(c)); + } + + return (CliAnsiParserResult){.is_done = false}; +} + +CliAnsiParserResult cli_ansi_parser_feed_timeout(CliAnsiParser* parser) { + CliAnsiParserResult result = {.is_done = false}; + + if(parser->state == CliAnsiParserStateEscape) { + result.is_done = true; + result.result.key = CliKeyEsc; + result.result.modifiers = CliModKeyNo; + } + + parser->state = CliAnsiParserStateInitial; + return result; +} diff --git a/applications/services/cli/cli_ansi.h b/applications/services/cli/cli_ansi.h new file mode 100644 index 000000000..20bf33d8e --- /dev/null +++ b/applications/services/cli/cli_ansi.h @@ -0,0 +1,153 @@ +#pragma once + +#include "cli.h" + +#ifdef __cplusplus +extern "C" { +#endif + +// text styling + +#define ANSI_RESET "\e[0m" +#define ANSI_BOLD "\e[1m" +#define ANSI_FAINT "\e[2m" +#define ANSI_INVERT "\e[7m" + +#define ANSI_FG_BLACK "\e[30m" +#define ANSI_FG_RED "\e[31m" +#define ANSI_FG_GREEN "\e[32m" +#define ANSI_FG_YELLOW "\e[33m" +#define ANSI_FG_BLUE "\e[34m" +#define ANSI_FG_MAGENTA "\e[35m" +#define ANSI_FG_CYAN "\e[36m" +#define ANSI_FG_WHITE "\e[37m" +#define ANSI_FG_BR_BLACK "\e[90m" +#define ANSI_FG_BR_RED "\e[91m" +#define ANSI_FG_BR_GREEN "\e[92m" +#define ANSI_FG_BR_YELLOW "\e[93m" +#define ANSI_FG_BR_BLUE "\e[94m" +#define ANSI_FG_BR_MAGENTA "\e[95m" +#define ANSI_FG_BR_CYAN "\e[96m" +#define ANSI_FG_BR_WHITE "\e[97m" + +#define ANSI_BG_BLACK "\e[40m" +#define ANSI_BG_RED "\e[41m" +#define ANSI_BG_GREEN "\e[42m" +#define ANSI_BG_YELLOW "\e[43m" +#define ANSI_BG_BLUE "\e[44m" +#define ANSI_BG_MAGENTA "\e[45m" +#define ANSI_BG_CYAN "\e[46m" +#define ANSI_BG_WHITE "\e[47m" +#define ANSI_BG_BR_BLACK "\e[100m" +#define ANSI_BG_BR_RED "\e[101m" +#define ANSI_BG_BR_GREEN "\e[102m" +#define ANSI_BG_BR_YELLOW "\e[103m" +#define ANSI_BG_BR_BLUE "\e[104m" +#define ANSI_BG_BR_MAGENTA "\e[105m" +#define ANSI_BG_BR_CYAN "\e[106m" +#define ANSI_BG_BR_WHITE "\e[107m" + +#define ANSI_FLIPPER_BRAND_ORANGE "\e[38;2;255;130;0m" + +// cursor positioning + +#define ANSI_CURSOR_UP_BY(rows) "\e[" rows "A" +#define ANSI_CURSOR_DOWN_BY(rows) "\e[" rows "B" +#define ANSI_CURSOR_RIGHT_BY(cols) "\e[" cols "C" +#define ANSI_CURSOR_LEFT_BY(cols) "\e[" cols "D" +#define ANSI_CURSOR_DOWN_BY_AND_FIRST_COLUMN(rows) "\e[" rows "E" +#define ANSI_CURSOR_UP_BY_AND_FIRST_COLUMN(rows) "\e[" rows "F" +#define ANSI_CURSOR_HOR_POS(pos) "\e[" pos "G" +#define ANSI_CURSOR_POS(row, col) "\e[" row ";" col "H" + +// erasing + +#define ANSI_ERASE_FROM_CURSOR_TO_END "0" +#define ANSI_ERASE_FROM_START_TO_CURSOR "1" +#define ANSI_ERASE_ENTIRE "2" + +#define ANSI_ERASE_DISPLAY(portion) "\e[" portion "J" +#define ANSI_ERASE_LINE(portion) "\e[" portion "K" +#define ANSI_ERASE_SCROLLBACK_BUFFER ANSI_ERASE_DISPLAY("3") + +// misc + +#define ANSI_INSERT_MODE_ENABLE "\e[4h" +#define ANSI_INSERT_MODE_DISABLE "\e[4l" + +typedef enum { + CliKeyUnrecognized = 0, + + CliKeySOH = 0x01, + CliKeyETX = 0x03, + CliKeyEOT = 0x04, + CliKeyBell = 0x07, + CliKeyBackspace = 0x08, + CliKeyTab = 0x09, + CliKeyLF = 0x0A, + CliKeyFF = 0x0C, + CliKeyCR = 0x0D, + CliKeyETB = 0x17, + CliKeyEsc = 0x1B, + CliKeyUS = 0x1F, + CliKeySpace = 0x20, + CliKeyDEL = 0x7F, + + CliKeySpecial = 0x80, + CliKeyLeft, + CliKeyRight, + CliKeyUp, + CliKeyDown, + CliKeyHome, + CliKeyEnd, +} CliKey; + +typedef enum { + CliModKeyNo = 0, + CliModKeyAlt = 2, + CliModKeyCtrl = 4, + CliModKeyMeta = 8, +} CliModKey; + +typedef struct { + CliModKey modifiers; + CliKey key; +} CliKeyCombo; + +typedef struct CliAnsiParser CliAnsiParser; + +typedef struct { + bool is_done; + CliKeyCombo result; +} CliAnsiParserResult; + +/** + * @brief Allocates an ANSI parser + */ +CliAnsiParser* cli_ansi_parser_alloc(void); + +/** + * @brief Frees an ANSI parser + */ +void cli_ansi_parser_free(CliAnsiParser* parser); + +/** + * @brief Feeds an ANSI parser a character + */ +CliAnsiParserResult cli_ansi_parser_feed(CliAnsiParser* parser, char c); + +/** + * @brief Feeds an ANSI parser a timeout event + * + * As a user of the ANSI parser API, you are responsible for calling this + * function some time after the last character was fed into the parser. The + * recommended timeout is about 10 ms. The exact value does not matter as long + * as it is small enough for the user not notice a delay, but big enough that + * when a terminal is sending an escape sequence, this function does not get + * called in between the characters of the sequence. + */ +CliAnsiParserResult cli_ansi_parser_feed_timeout(CliAnsiParser* parser); + +#ifdef __cplusplus +} +#endif diff --git a/applications/services/cli/cli_command_gpio.c b/applications/services/cli/cli_command_gpio.c index 010a7dfbe..f37e6387f 100644 --- a/applications/services/cli/cli_command_gpio.c +++ b/applications/services/cli/cli_command_gpio.c @@ -3,6 +3,7 @@ #include #include #include +#include void cli_command_gpio_print_usage(void) { printf("Usage:\r\n"); @@ -70,8 +71,8 @@ static GpioParseReturn gpio_command_parse(FuriString* args, size_t* pin_num, uin return ret; } -void cli_command_gpio_mode(Cli* cli, FuriString* args, void* context) { - UNUSED(cli); +void cli_command_gpio_mode(PipeSide* pipe, FuriString* args, void* context) { + UNUSED(pipe); UNUSED(context); size_t num = 0; @@ -93,7 +94,7 @@ void cli_command_gpio_mode(Cli* cli, FuriString* args, void* context) { if(gpio_pins[num].debug) { //-V779 printf( "Changing this pin mode may damage hardware. Are you sure you want to continue? (y/n)?\r\n"); - char c = cli_getc(cli); + char c = getchar(); if(c != 'y' && c != 'Y') { printf("Cancelled.\r\n"); return; @@ -110,8 +111,8 @@ void cli_command_gpio_mode(Cli* cli, FuriString* args, void* context) { } } -void cli_command_gpio_read(Cli* cli, FuriString* args, void* context) { - UNUSED(cli); +void cli_command_gpio_read(PipeSide* pipe, FuriString* args, void* context) { + UNUSED(pipe); UNUSED(context); size_t num = 0; @@ -131,7 +132,8 @@ void cli_command_gpio_read(Cli* cli, FuriString* args, void* context) { printf("Pin %s <= %u", gpio_pins[num].name, val); } -void cli_command_gpio_set(Cli* cli, FuriString* args, void* context) { +void cli_command_gpio_set(PipeSide* pipe, FuriString* args, void* context) { + UNUSED(pipe); UNUSED(context); size_t num = 0; @@ -159,7 +161,7 @@ void cli_command_gpio_set(Cli* cli, FuriString* args, void* context) { if(gpio_pins[num].debug) { printf( "Setting this pin may damage hardware. Are you sure you want to continue? (y/n)?\r\n"); - char c = cli_getc(cli); + char c = getchar(); if(c != 'y' && c != 'Y') { printf("Cancelled.\r\n"); return; @@ -170,7 +172,7 @@ void cli_command_gpio_set(Cli* cli, FuriString* args, void* context) { printf("Pin %s => %u", gpio_pins[num].name, !!value); } -void cli_command_gpio(Cli* cli, FuriString* args, void* context) { +void cli_command_gpio(PipeSide* pipe, FuriString* args, void* context) { FuriString* cmd; cmd = furi_string_alloc(); @@ -181,17 +183,17 @@ void cli_command_gpio(Cli* cli, FuriString* args, void* context) { } if(furi_string_cmp_str(cmd, "mode") == 0) { - cli_command_gpio_mode(cli, args, context); + cli_command_gpio_mode(pipe, args, context); break; } if(furi_string_cmp_str(cmd, "set") == 0) { - cli_command_gpio_set(cli, args, context); + cli_command_gpio_set(pipe, args, context); break; } if(furi_string_cmp_str(cmd, "read") == 0) { - cli_command_gpio_read(cli, args, context); + cli_command_gpio_read(pipe, args, context); break; } diff --git a/applications/services/cli/cli_command_gpio.h b/applications/services/cli/cli_command_gpio.h index 7ae5aa625..0290949e0 100644 --- a/applications/services/cli/cli_command_gpio.h +++ b/applications/services/cli/cli_command_gpio.h @@ -1,5 +1,6 @@ #pragma once #include "cli_i.h" +#include -void cli_command_gpio(Cli* cli, FuriString* args, void* context); +void cli_command_gpio(PipeSide* pipe, FuriString* args, void* context); diff --git a/applications/services/cli/cli_commands.c b/applications/services/cli/cli_commands.c index e4503b274..24917afa9 100644 --- a/applications/services/cli/cli_commands.c +++ b/applications/services/cli/cli_commands.c @@ -1,5 +1,7 @@ #include "cli_commands.h" #include "cli_command_gpio.h" +#include "cli_ansi.h" +#include "cli.h" #include #include @@ -11,6 +13,7 @@ #include #include #include +#include // Close to ISO, `date +'%Y-%m-%d %H:%M:%S %u'` #define CLI_DATE_FORMAT "%.4d-%.2d-%.2d %.2d:%.2d:%.2d %d" @@ -34,8 +37,8 @@ void cli_command_info_callback(const char* key, const char* value, bool last, vo * @param args The arguments * @param context The context */ -void cli_command_info(Cli* cli, FuriString* args, void* context) { - UNUSED(cli); +void cli_command_info(PipeSide* pipe, FuriString* args, void* context) { + UNUSED(pipe); if(context) { furi_hal_info_get(cli_command_info_callback, '_', NULL); @@ -53,56 +56,57 @@ void cli_command_info(Cli* cli, FuriString* args, void* context) { } } -void cli_command_help(Cli* cli, FuriString* args, void* context) { +void cli_command_help(PipeSide* pipe, FuriString* args, void* context) { + UNUSED(pipe); UNUSED(args); UNUSED(context); - printf("Commands available:"); + printf("Available commands:" ANSI_FG_GREEN); - // Command count - const size_t commands_count = CliCommandTree_size(cli->commands); - const size_t commands_count_mid = commands_count / 2 + commands_count % 2; + // count non-hidden commands + Cli* cli = furi_record_open(RECORD_CLI); + cli_lock_commands(cli); + CliCommandTree_t* commands = cli_get_commands(cli); + size_t commands_count = CliCommandTree_size(*commands); - // Use 2 iterators from start and middle to show 2 columns - CliCommandTree_it_t it_left; - CliCommandTree_it(it_left, cli->commands); - CliCommandTree_it_t it_right; - CliCommandTree_it(it_right, cli->commands); - for(size_t i = 0; i < commands_count_mid; i++) - CliCommandTree_next(it_right); - - // Iterate throw tree - for(size_t i = 0; i < commands_count_mid; i++) { - printf("\r\n"); - // Left Column - if(!CliCommandTree_end_p(it_left)) { - printf("%-30s", furi_string_get_cstr(*CliCommandTree_ref(it_left)->key_ptr)); - CliCommandTree_next(it_left); - } - // Right Column - if(!CliCommandTree_end_p(it_right)) { - printf("%s", furi_string_get_cstr(*CliCommandTree_ref(it_right)->key_ptr)); - CliCommandTree_next(it_right); - } - }; - - if(furi_string_size(args) > 0) { - cli_nl(cli); - printf("`"); - printf("%s", furi_string_get_cstr(args)); - printf("` command not found"); + // create iterators starting at different positions + const size_t columns = 3; + const size_t commands_per_column = (commands_count / columns) + (commands_count % columns); + CliCommandTree_it_t iterators[columns]; + for(size_t c = 0; c < columns; c++) { + CliCommandTree_it(iterators[c], *commands); + for(size_t i = 0; i < c * commands_per_column; i++) + CliCommandTree_next(iterators[c]); } + + // print commands + for(size_t r = 0; r < commands_per_column; r++) { + printf("\r\n"); + + for(size_t c = 0; c < columns; c++) { + if(!CliCommandTree_end_p(iterators[c])) { + const CliCommandTree_itref_t* item = CliCommandTree_cref(iterators[c]); + printf("%-30s", furi_string_get_cstr(*item->key_ptr)); + CliCommandTree_next(iterators[c]); + } + } + } + + printf(ANSI_RESET "\r\nFind out more: https://docs.flipper.net/development/cli"); + + cli_unlock_commands(cli); + furi_record_close(RECORD_CLI); } -void cli_command_uptime(Cli* cli, FuriString* args, void* context) { - UNUSED(cli); +void cli_command_uptime(PipeSide* pipe, FuriString* args, void* context) { + UNUSED(pipe); UNUSED(args); UNUSED(context); uint32_t uptime = furi_get_tick() / furi_kernel_get_tick_frequency(); printf("Uptime: %luh%lum%lus", uptime / 60 / 60, uptime / 60 % 60, uptime % 60); } -void cli_command_date(Cli* cli, FuriString* args, void* context) { - UNUSED(cli); +void cli_command_date(PipeSide* pipe, FuriString* args, void* context) { + UNUSED(pipe); UNUSED(context); DateTime datetime = {0}; @@ -174,7 +178,8 @@ void cli_command_date(Cli* cli, FuriString* args, void* context) { #define CLI_COMMAND_LOG_BUFFER_SIZE 64 void cli_command_log_tx_callback(const uint8_t* buffer, size_t size, void* context) { - furi_stream_buffer_send(context, buffer, size, 0); + PipeSide* pipe = context; + pipe_send(pipe, buffer, size); } bool cli_command_log_level_set_from_string(FuriString* level) { @@ -196,16 +201,13 @@ bool cli_command_log_level_set_from_string(FuriString* level) { return false; } -void cli_command_log(Cli* cli, FuriString* args, void* context) { +void cli_command_log(PipeSide* pipe, FuriString* args, void* context) { UNUSED(context); - FuriStreamBuffer* ring = furi_stream_buffer_alloc(CLI_COMMAND_LOG_RING_SIZE, 1); - uint8_t buffer[CLI_COMMAND_LOG_BUFFER_SIZE]; FuriLogLevel previous_level = furi_log_get_level(); bool restore_log_level = false; if(furi_string_size(args) > 0) { if(!cli_command_log_level_set_from_string(args)) { - furi_stream_buffer_free(ring); return; } restore_log_level = true; @@ -217,16 +219,15 @@ void cli_command_log(Cli* cli, FuriString* args, void* context) { FuriLogHandler log_handler = { .callback = cli_command_log_tx_callback, - .context = ring, + .context = pipe, }; furi_log_add_handler(log_handler); printf("Use to list available log levels\r\n"); printf("Press CTRL+C to stop...\r\n"); - while(!cli_cmd_interrupt_received(cli)) { - size_t ret = furi_stream_buffer_receive(ring, buffer, CLI_COMMAND_LOG_BUFFER_SIZE, 50); - cli_write(cli, buffer, ret); + while(!cli_is_pipe_broken_or_is_etx_next_char(pipe)) { + furi_delay_ms(100); } furi_log_remove_handler(log_handler); @@ -235,12 +236,10 @@ void cli_command_log(Cli* cli, FuriString* args, void* context) { // There will be strange behaviour if log level is set from settings while log command is running furi_log_set_level(previous_level); } - - furi_stream_buffer_free(ring); } -void cli_command_sysctl_debug(Cli* cli, FuriString* args, void* context) { - UNUSED(cli); +void cli_command_sysctl_debug(PipeSide* pipe, FuriString* args, void* context) { + UNUSED(pipe); UNUSED(context); if(!furi_string_cmp(args, "0")) { furi_hal_rtc_reset_flag(FuriHalRtcFlagDebug); @@ -253,8 +252,8 @@ void cli_command_sysctl_debug(Cli* cli, FuriString* args, void* context) { } } -void cli_command_sysctl_heap_track(Cli* cli, FuriString* args, void* context) { - UNUSED(cli); +void cli_command_sysctl_heap_track(PipeSide* pipe, FuriString* args, void* context) { + UNUSED(pipe); UNUSED(context); if(!furi_string_cmp(args, "none")) { furi_hal_rtc_set_heap_track_mode(FuriHalRtcHeapTrackModeNone); @@ -288,7 +287,7 @@ void cli_command_sysctl_print_usage(void) { #endif } -void cli_command_sysctl(Cli* cli, FuriString* args, void* context) { +void cli_command_sysctl(PipeSide* pipe, FuriString* args, void* context) { FuriString* cmd; cmd = furi_string_alloc(); @@ -299,12 +298,12 @@ void cli_command_sysctl(Cli* cli, FuriString* args, void* context) { } if(furi_string_cmp_str(cmd, "debug") == 0) { - cli_command_sysctl_debug(cli, args, context); + cli_command_sysctl_debug(pipe, args, context); break; } if(furi_string_cmp_str(cmd, "heap_track") == 0) { - cli_command_sysctl_heap_track(cli, args, context); + cli_command_sysctl_heap_track(pipe, args, context); break; } @@ -314,8 +313,8 @@ void cli_command_sysctl(Cli* cli, FuriString* args, void* context) { furi_string_free(cmd); } -void cli_command_vibro(Cli* cli, FuriString* args, void* context) { - UNUSED(cli); +void cli_command_vibro(PipeSide* pipe, FuriString* args, void* context) { + UNUSED(pipe); UNUSED(context); if(!furi_string_cmp(args, "0")) { @@ -341,8 +340,8 @@ void cli_command_vibro(Cli* cli, FuriString* args, void* context) { } } -void cli_command_led(Cli* cli, FuriString* args, void* context) { - UNUSED(cli); +void cli_command_led(PipeSide* pipe, FuriString* args, void* context) { + UNUSED(pipe); UNUSED(context); // Get first word as light name NotificationMessage notification_led_message; @@ -396,23 +395,23 @@ void cli_command_led(Cli* cli, FuriString* args, void* context) { furi_record_close(RECORD_NOTIFICATION); } -static void cli_command_top(Cli* cli, FuriString* args, void* context) { - UNUSED(cli); +static void cli_command_top(PipeSide* pipe, FuriString* args, void* context) { UNUSED(context); int interval = 1000; args_read_int_and_trim(args, &interval); FuriThreadList* thread_list = furi_thread_list_alloc(); - while(!cli_cmd_interrupt_received(cli)) { + while(!cli_is_pipe_broken_or_is_etx_next_char(pipe)) { uint32_t tick = furi_get_tick(); furi_thread_enumerate(thread_list); - if(interval) printf("\e[2J\e[0;0f"); // Clear display and return to 0 + if(interval) printf(ANSI_CURSOR_POS("1", "1")); uint32_t uptime = tick / furi_kernel_get_tick_frequency(); printf( - "Threads: %zu, ISR Time: %0.2f%%, Uptime: %luh%lum%lus\r\n", + "Threads: %zu, ISR Time: %0.2f%%, Uptime: %luh%lum%lus" ANSI_ERASE_LINE( + ANSI_ERASE_FROM_CURSOR_TO_END) "\r\n", furi_thread_list_size(thread_list), (double)furi_thread_list_get_isr_time(thread_list), uptime / 60 / 60, @@ -420,14 +419,16 @@ static void cli_command_top(Cli* cli, FuriString* args, void* context) { uptime % 60); printf( - "Heap: total %zu, free %zu, minimum %zu, max block %zu\r\n\r\n", + "Heap: total %zu, free %zu, minimum %zu, max block %zu" ANSI_ERASE_LINE( + ANSI_ERASE_FROM_CURSOR_TO_END) "\r\n" ANSI_ERASE_LINE(ANSI_ERASE_FROM_CURSOR_TO_END) "\r\n", memmgr_get_total_heap(), memmgr_get_free_heap(), memmgr_get_minimum_free_heap(), memmgr_heap_get_max_free_block()); printf( - "%-17s %-20s %-10s %5s %12s %6s %10s %7s %5s\r\n", + "%-17s %-20s %-10s %5s %12s %6s %10s %7s %5s" ANSI_ERASE_LINE( + ANSI_ERASE_FROM_CURSOR_TO_END) "\r\n", "AppID", "Name", "State", @@ -436,12 +437,13 @@ static void cli_command_top(Cli* cli, FuriString* args, void* context) { "Stack", "Stack Min", "Heap", - "CPU"); + "%CPU"); for(size_t i = 0; i < furi_thread_list_size(thread_list); i++) { const FuriThreadListItem* item = furi_thread_list_get_at(thread_list, i); printf( - "%-17s %-20s %-10s %5d 0x%08lx %6lu %10lu %7zu %5.1f\r\n", + "%-17s %-20s %-10s %5d 0x%08lx %6lu %10lu %7zu %5.1f" ANSI_ERASE_LINE( + ANSI_ERASE_FROM_CURSOR_TO_END) "\r\n", item->app_id, item->name, item->state, @@ -453,6 +455,9 @@ static void cli_command_top(Cli* cli, FuriString* args, void* context) { (double)item->cpu); } + printf(ANSI_ERASE_DISPLAY(ANSI_ERASE_FROM_CURSOR_TO_END)); + fflush(stdout); + if(interval > 0) { furi_delay_ms(interval); } else { @@ -462,8 +467,8 @@ static void cli_command_top(Cli* cli, FuriString* args, void* context) { furi_thread_list_free(thread_list); } -void cli_command_free(Cli* cli, FuriString* args, void* context) { - UNUSED(cli); +void cli_command_free(PipeSide* pipe, FuriString* args, void* context) { + UNUSED(pipe); UNUSED(args); UNUSED(context); @@ -476,16 +481,16 @@ void cli_command_free(Cli* cli, FuriString* args, void* context) { printf("Maximum pool block: %zu\r\n", memmgr_pool_get_max_block()); } -void cli_command_free_blocks(Cli* cli, FuriString* args, void* context) { - UNUSED(cli); +void cli_command_free_blocks(PipeSide* pipe, FuriString* args, void* context) { + UNUSED(pipe); UNUSED(args); UNUSED(context); memmgr_heap_printf_free_blocks(); } -void cli_command_i2c(Cli* cli, FuriString* args, void* context) { - UNUSED(cli); +void cli_command_i2c(PipeSide* pipe, FuriString* args, void* context) { + UNUSED(pipe); UNUSED(args); UNUSED(context); @@ -507,6 +512,27 @@ void cli_command_i2c(Cli* cli, FuriString* args, void* context) { furi_hal_i2c_release(&furi_hal_i2c_handle_external); } +/** + * Echoes any bytes it receives except ASCII ETX (0x03, Ctrl+C) + */ +void cli_command_echo(PipeSide* pipe, FuriString* args, void* context) { + UNUSED(args); + UNUSED(context); + + uint8_t buffer[256]; + + while(true) { + size_t to_read = CLAMP(pipe_bytes_available(pipe), sizeof(buffer), 1UL); + size_t read = pipe_receive(pipe, buffer, to_read); + if(read < to_read) break; + + if(memchr(buffer, CliKeyETX, read)) break; + + size_t written = pipe_send(pipe, buffer, read); + if(written < read) break; + } +} + void cli_commands_init(Cli* cli) { cli_add_command(cli, "!", CliCommandFlagParallelSafe, cli_command_info, (void*)true); cli_add_command(cli, "info", CliCommandFlagParallelSafe, cli_command_info, NULL); @@ -522,6 +548,7 @@ void cli_commands_init(Cli* cli) { cli_add_command(cli, "top", CliCommandFlagParallelSafe, cli_command_top, NULL); cli_add_command(cli, "free", CliCommandFlagParallelSafe, cli_command_free, NULL); cli_add_command(cli, "free_blocks", CliCommandFlagParallelSafe, cli_command_free_blocks, NULL); + cli_add_command(cli, "echo", CliCommandFlagParallelSafe, cli_command_echo, NULL); cli_add_command(cli, "vibro", CliCommandFlagDefault, cli_command_vibro, NULL); cli_add_command(cli, "led", CliCommandFlagDefault, cli_command_led, NULL); diff --git a/applications/services/cli/cli_commands.h b/applications/services/cli/cli_commands.h index 184eeb373..77d9930af 100644 --- a/applications/services/cli/cli_commands.h +++ b/applications/services/cli/cli_commands.h @@ -1,5 +1,34 @@ #pragma once -#include "cli_i.h" +#include "cli.h" +#include void cli_commands_init(Cli* cli); + +#define PLUGIN_APP_ID "cli" +#define PLUGIN_API_VERSION 1 + +typedef struct { + char* name; + CliExecuteCallback execute_callback; + CliCommandFlag flags; + size_t stack_depth; +} CliCommandDescriptor; + +#define CLI_COMMAND_INTERFACE(name, execute_callback, flags, stack_depth) \ + static const CliCommandDescriptor cli_##name##_desc = { \ + #name, \ + &execute_callback, \ + flags, \ + stack_depth, \ + }; \ + \ + static const FlipperAppPluginDescriptor plugin_descriptor = { \ + .appid = PLUGIN_APP_ID, \ + .ep_api_version = PLUGIN_API_VERSION, \ + .entry_point = &cli_##name##_desc, \ + }; \ + \ + const FlipperAppPluginDescriptor* cli_##name##_ep(void) { \ + return &plugin_descriptor; \ + } diff --git a/applications/services/cli/cli_i.h b/applications/services/cli/cli_i.h index ca126dacd..b990e9960 100644 --- a/applications/services/cli/cli_i.h +++ b/applications/services/cli/cli_i.h @@ -1,67 +1,50 @@ +/** + * @file cli_i.h + * Internal API for getting commands registered with the CLI + */ + #pragma once -#include "cli.h" - #include -#include - -#include #include -#include - -#include "cli_vcp.h" - -#define CLI_LINE_SIZE_MAX -#define CLI_COMMANDS_TREE_RANK 4 +#include "cli.h" #ifdef __cplusplus extern "C" { #endif +#define CLI_BUILTIN_COMMAND_STACK_SIZE (3 * 1024U) + typedef struct { - CliCallback callback; - void* context; - uint32_t flags; + void* context; // #include #include +#include +#include #define TAG "CliVcp" -#define USB_CDC_PKT_LEN CDC_DATA_SZ -#define VCP_RX_BUF_SIZE (USB_CDC_PKT_LEN * 3) -#define VCP_TX_BUF_SIZE (USB_CDC_PKT_LEN * 3) +#define USB_CDC_PKT_LEN CDC_DATA_SZ +#define VCP_BUF_SIZE (USB_CDC_PKT_LEN * 3) +#define VCP_IF_NUM 0 +#define VCP_MESSAGE_Q_LEN 8 -#define VCP_IF_NUM 0 - -#ifdef CLI_VCP_DEBUG -#define VCP_DEBUG(...) FURI_LOG_D(TAG, __VA_ARGS__) +#ifdef CLI_VCP_TRACE +#define VCP_TRACE(...) FURI_LOG_T(__VA_ARGS__) #else -#define VCP_DEBUG(...) +#define VCP_TRACE(...) #endif -typedef enum { - VcpEvtStop = (1 << 0), - VcpEvtConnect = (1 << 1), - VcpEvtDisconnect = (1 << 2), - VcpEvtStreamRx = (1 << 3), - VcpEvtRx = (1 << 4), - VcpEvtStreamTx = (1 << 5), - VcpEvtTx = (1 << 6), -} WorkerEvtFlags; - -#define VCP_THREAD_FLAG_ALL \ - (VcpEvtStop | VcpEvtConnect | VcpEvtDisconnect | VcpEvtRx | VcpEvtTx | VcpEvtStreamRx | \ - VcpEvtStreamTx) - typedef struct { - FuriThread* thread; + enum { + CliVcpMessageTypeEnable, + CliVcpMessageTypeDisable, + } type; + union {}; +} CliVcpMessage; - FuriStreamBuffer* tx_stream; - FuriStreamBuffer* rx_stream; +typedef enum { + CliVcpInternalEventConnected = (1 << 0), + CliVcpInternalEventDisconnected = (1 << 1), + CliVcpInternalEventTxDone = (1 << 2), + CliVcpInternalEventRx = (1 << 3), +} CliVcpInternalEvent; - volatile bool connected; - volatile bool running; +#define CliVcpInternalEventAll \ + (CliVcpInternalEventConnected | CliVcpInternalEventDisconnected | CliVcpInternalEventTxDone | \ + CliVcpInternalEventRx) - FuriHalUsbInterface* usb_if_prev; +struct CliVcp { + FuriEventLoop* event_loop; + FuriMessageQueue* message_queue; // is_currently_transmitting) return; + if(!cli_vcp->own_pipe) return; -static void cli_vcp_init(void) { - if(vcp == NULL) { - vcp = malloc(sizeof(CliVcp)); - vcp->tx_stream = furi_stream_buffer_alloc(VCP_TX_BUF_SIZE, 1); - vcp->rx_stream = furi_stream_buffer_alloc(VCP_RX_BUF_SIZE, 1); + uint8_t buf[USB_CDC_PKT_LEN]; + size_t to_receive_from_pipe = MIN(sizeof(buf), pipe_bytes_available(cli_vcp->own_pipe)); + size_t length = pipe_receive(cli_vcp->own_pipe, buf, to_receive_from_pipe); + if(length > 0 || cli_vcp->previous_tx_length == USB_CDC_PKT_LEN) { + VCP_TRACE(TAG, "cdc_send length=%zu", length); + cli_vcp->is_currently_transmitting = true; + furi_hal_cdc_send(VCP_IF_NUM, buf, length); } - furi_assert(vcp->thread == NULL); - - vcp->connected = false; - - vcp->thread = furi_thread_alloc_ex("CliVcpWorker", 1024, vcp_worker, NULL); - furi_thread_start(vcp->thread); - - FURI_LOG_I(TAG, "Init OK"); + cli_vcp->previous_tx_length = length; } -static void cli_vcp_deinit(void) { - furi_thread_flags_set(furi_thread_get_id(vcp->thread), VcpEvtStop); - furi_thread_join(vcp->thread); - furi_thread_free(vcp->thread); - vcp->thread = NULL; +/** + * Called in the following cases: + * - new data arrived at the endpoint; + * - data was read out of the pipe. + */ +static void cli_vcp_maybe_receive_data(CliVcp* cli_vcp) { + if(!cli_vcp->own_pipe) return; + if(pipe_spaces_available(cli_vcp->own_pipe) < USB_CDC_PKT_LEN) return; + + uint8_t buf[USB_CDC_PKT_LEN]; + size_t length = furi_hal_cdc_receive(VCP_IF_NUM, buf, sizeof(buf)); + VCP_TRACE(TAG, "cdc_receive length=%zu", length); + furi_check(pipe_send(cli_vcp->own_pipe, buf, length) == length); } -static int32_t vcp_worker(void* context) { - UNUSED(context); - bool tx_idle = true; - size_t missed_rx = 0; - uint8_t last_tx_pkt_len = 0; +// ============= +// CDC callbacks +// ============= - // Switch USB to VCP mode (if it is not set yet) - vcp->usb_if_prev = furi_hal_usb_get_config(); - if((vcp->usb_if_prev != &usb_cdc_single) && (vcp->usb_if_prev != &usb_cdc_dual)) { +static void cli_vcp_signal_internal_event(CliVcp* cli_vcp, CliVcpInternalEvent event) { + furi_thread_flags_set(cli_vcp->thread_id, event); +} + +static void cli_vcp_cdc_tx_done(void* context) { + CliVcp* cli_vcp = context; + cli_vcp_signal_internal_event(cli_vcp, CliVcpInternalEventTxDone); +} + +static void cli_vcp_cdc_rx(void* context) { + CliVcp* cli_vcp = context; + cli_vcp_signal_internal_event(cli_vcp, CliVcpInternalEventRx); +} + +static void cli_vcp_cdc_state_callback(void* context, CdcState state) { + CliVcp* cli_vcp = context; + if(state == CdcStateDisconnected) { + cli_vcp_signal_internal_event(cli_vcp, CliVcpInternalEventDisconnected); + } + // `Connected` events are generated by DTR going active +} + +static void cli_vcp_cdc_ctrl_line_callback(void* context, CdcCtrlLine ctrl_lines) { + CliVcp* cli_vcp = context; + if(ctrl_lines & CdcCtrlLineDTR) { + cli_vcp_signal_internal_event(cli_vcp, CliVcpInternalEventConnected); + } else { + cli_vcp_signal_internal_event(cli_vcp, CliVcpInternalEventDisconnected); + } +} + +static CdcCallbacks cdc_callbacks = { + .tx_ep_callback = cli_vcp_cdc_tx_done, + .rx_ep_callback = cli_vcp_cdc_rx, + .state_callback = cli_vcp_cdc_state_callback, + .ctrl_line_callback = cli_vcp_cdc_ctrl_line_callback, + .config_callback = NULL, +}; + +// ====================== +// Pipe callback handlers +// ====================== + +static void cli_vcp_data_from_shell(PipeSide* pipe, void* context) { + UNUSED(pipe); + CliVcp* cli_vcp = context; + cli_vcp_maybe_send_data(cli_vcp); +} + +static void cli_vcp_shell_ready(PipeSide* pipe, void* context) { + UNUSED(pipe); + CliVcp* cli_vcp = context; + cli_vcp_maybe_receive_data(cli_vcp); +} + +/** + * Processes messages arriving from other threads + */ +static void cli_vcp_message_received(FuriEventLoopObject* object, void* context) { + CliVcp* cli_vcp = context; + CliVcpMessage message; + furi_check(furi_message_queue_get(object, &message, 0) == FuriStatusOk); + + switch(message.type) { + case CliVcpMessageTypeEnable: + if(cli_vcp->is_enabled) return; + FURI_LOG_D(TAG, "Enabling"); + cli_vcp->is_enabled = true; + + // switch usb mode + cli_vcp->previous_interface = furi_hal_usb_get_config(); furi_hal_usb_set_config(&usb_cdc_single, NULL); + furi_hal_cdc_set_callbacks(VCP_IF_NUM, &cdc_callbacks, cli_vcp); + break; + + case CliVcpMessageTypeDisable: + if(!cli_vcp->is_enabled) return; + FURI_LOG_D(TAG, "Disabling"); + cli_vcp->is_enabled = false; + + // restore usb mode + furi_hal_cdc_set_callbacks(VCP_IF_NUM, NULL, NULL); + furi_hal_usb_set_config(cli_vcp->previous_interface, NULL); + break; } - furi_hal_cdc_set_callbacks(VCP_IF_NUM, &cdc_cb, NULL); - - FURI_LOG_D(TAG, "Start"); - vcp->running = true; - - while(1) { - uint32_t flags = - furi_thread_flags_wait(VCP_THREAD_FLAG_ALL, FuriFlagWaitAny, FuriWaitForever); - furi_assert(!(flags & FuriFlagError)); - - // VCP session opened - if(flags & VcpEvtConnect) { - VCP_DEBUG("Connect"); - - if(vcp->connected == false) { - vcp->connected = true; - furi_stream_buffer_send(vcp->rx_stream, &ascii_soh, 1, FuriWaitForever); - } - } - - // VCP session closed - if(flags & VcpEvtDisconnect) { - VCP_DEBUG("Disconnect"); - - if(vcp->connected == true) { - vcp->connected = false; - furi_stream_buffer_receive(vcp->tx_stream, vcp->data_buffer, USB_CDC_PKT_LEN, 0); - furi_stream_buffer_send(vcp->rx_stream, &ascii_eot, 1, FuriWaitForever); - } - } - - // Rx buffer was read, maybe there is enough space for new data? - if((flags & VcpEvtStreamRx) && (missed_rx > 0)) { - VCP_DEBUG("StreamRx"); - - if(furi_stream_buffer_spaces_available(vcp->rx_stream) >= USB_CDC_PKT_LEN) { - flags |= VcpEvtRx; - missed_rx--; - } - } - - // New data received - if(flags & VcpEvtRx) { - if(furi_stream_buffer_spaces_available(vcp->rx_stream) >= USB_CDC_PKT_LEN) { - int32_t len = furi_hal_cdc_receive(VCP_IF_NUM, vcp->data_buffer, USB_CDC_PKT_LEN); - VCP_DEBUG("Rx %ld", len); - - if(len > 0) { - furi_check( - furi_stream_buffer_send( - vcp->rx_stream, vcp->data_buffer, len, FuriWaitForever) == - (size_t)len); - } - } else { - VCP_DEBUG("Rx missed"); - missed_rx++; - } - } - - // New data in Tx buffer - if(flags & VcpEvtStreamTx) { - VCP_DEBUG("StreamTx"); - - if(tx_idle) { - flags |= VcpEvtTx; - } - } - - // CDC write transfer done - if(flags & VcpEvtTx) { - size_t len = - furi_stream_buffer_receive(vcp->tx_stream, vcp->data_buffer, USB_CDC_PKT_LEN, 0); - - VCP_DEBUG("Tx %d", len); - - if(len > 0) { // Some data left in Tx buffer. Sending it now - tx_idle = false; - furi_hal_cdc_send(VCP_IF_NUM, vcp->data_buffer, len); - last_tx_pkt_len = len; - } else { // There is nothing to send. - if(last_tx_pkt_len == 64) { - // Send extra zero-length packet if last packet len is 64 to indicate transfer end - furi_hal_cdc_send(VCP_IF_NUM, NULL, 0); - } else { - // Set flag to start next transfer instantly - tx_idle = true; - } - last_tx_pkt_len = 0; - } - } - - if(flags & VcpEvtStop) { - vcp->connected = false; - vcp->running = false; - furi_hal_cdc_set_callbacks(VCP_IF_NUM, NULL, NULL); - // Restore previous USB mode (if it was set during init) - if((vcp->usb_if_prev != &usb_cdc_single) && (vcp->usb_if_prev != &usb_cdc_dual)) { - furi_hal_usb_unlock(); - furi_hal_usb_set_config(vcp->usb_if_prev, NULL); - } - furi_stream_buffer_receive(vcp->tx_stream, vcp->data_buffer, USB_CDC_PKT_LEN, 0); - furi_stream_buffer_send(vcp->rx_stream, &ascii_eot, 1, FuriWaitForever); - break; - } - } - FURI_LOG_D(TAG, "End"); - return 0; } -static size_t cli_vcp_rx(uint8_t* buffer, size_t size, uint32_t timeout) { - furi_assert(vcp); - furi_assert(buffer); +/** + * Processes messages arriving from CDC event callbacks + */ +static void cli_vcp_internal_event_happened(void* context) { + CliVcp* cli_vcp = context; + CliVcpInternalEvent event = furi_thread_flags_wait(CliVcpInternalEventAll, FuriFlagWaitAny, 0); + furi_check(!(event & FuriFlagError)); - if(vcp->running == false) { + if(event & CliVcpInternalEventDisconnected) { + if(!cli_vcp->is_connected) return; + FURI_LOG_D(TAG, "Disconnected"); + cli_vcp->is_connected = false; + + // disconnect our side of the pipe + pipe_detach_from_event_loop(cli_vcp->own_pipe); + pipe_free(cli_vcp->own_pipe); + cli_vcp->own_pipe = NULL; + } + + if(event & CliVcpInternalEventConnected) { + if(cli_vcp->is_connected) return; + FURI_LOG_D(TAG, "Connected"); + cli_vcp->is_connected = true; + + // wait for previous shell to stop + furi_check(!cli_vcp->own_pipe); + if(cli_vcp->shell) { + furi_thread_join(cli_vcp->shell); + furi_thread_free(cli_vcp->shell); + } + + // start shell thread + PipeSideBundle bundle = pipe_alloc(VCP_BUF_SIZE, 1); + cli_vcp->own_pipe = bundle.alices_side; + pipe_attach_to_event_loop(cli_vcp->own_pipe, cli_vcp->event_loop); + pipe_set_callback_context(cli_vcp->own_pipe, cli_vcp); + pipe_set_data_arrived_callback( + cli_vcp->own_pipe, cli_vcp_data_from_shell, FuriEventLoopEventFlagEdge); + pipe_set_space_freed_callback( + cli_vcp->own_pipe, cli_vcp_shell_ready, FuriEventLoopEventFlagEdge); + furi_delay_ms(33); // we are too fast, minicom isn't ready yet + cli_vcp->shell = cli_shell_start(bundle.bobs_side); + } + + if(event & CliVcpInternalEventRx) { + VCP_TRACE(TAG, "Rx"); + cli_vcp_maybe_receive_data(cli_vcp); + } + + if(event & CliVcpInternalEventTxDone) { + VCP_TRACE(TAG, "TxDone"); + cli_vcp->is_currently_transmitting = false; + cli_vcp_maybe_send_data(cli_vcp); + } +} + +// ============ +// Thread stuff +// ============ + +static CliVcp* cli_vcp_alloc(void) { + CliVcp* cli_vcp = malloc(sizeof(CliVcp)); + cli_vcp->thread_id = furi_thread_get_current_id(); + + cli_vcp->event_loop = furi_event_loop_alloc(); + + cli_vcp->message_queue = furi_message_queue_alloc(VCP_MESSAGE_Q_LEN, sizeof(CliVcpMessage)); + furi_event_loop_subscribe_message_queue( + cli_vcp->event_loop, + cli_vcp->message_queue, + FuriEventLoopEventIn, + cli_vcp_message_received, + cli_vcp); + + furi_event_loop_subscribe_thread_flags( + cli_vcp->event_loop, cli_vcp_internal_event_happened, cli_vcp); + + return cli_vcp; +} + +int32_t cli_vcp_srv(void* p) { + UNUSED(p); + + if(furi_hal_rtc_get_boot_mode() != FuriHalRtcBootModeNormal) { + FURI_LOG_W(TAG, "Skipping start in special boot mode"); + furi_thread_suspend(furi_thread_get_current_id()); return 0; } - VCP_DEBUG("rx %u start", size); + CliVcp* cli_vcp = cli_vcp_alloc(); + furi_record_create(RECORD_CLI_VCP, cli_vcp); + furi_event_loop_run(cli_vcp->event_loop); - size_t rx_cnt = 0; - - while(size > 0) { - size_t batch_size = size; - if(batch_size > VCP_RX_BUF_SIZE) batch_size = VCP_RX_BUF_SIZE; - - size_t len = furi_stream_buffer_receive(vcp->rx_stream, buffer, batch_size, timeout); - VCP_DEBUG("rx %u ", batch_size); - - if(len == 0) break; - if(vcp->running == false) { - // EOT command is received after VCP session close - rx_cnt += len; - break; - } - furi_thread_flags_set(furi_thread_get_id(vcp->thread), VcpEvtStreamRx); - size -= len; - buffer += len; - rx_cnt += len; - } - - VCP_DEBUG("rx %u end", size); - return rx_cnt; + return 0; } -static size_t cli_vcp_rx_stdin(uint8_t* data, size_t size, uint32_t timeout, void* context) { - UNUSED(context); - return cli_vcp_rx(data, size, timeout); +// ========== +// Public API +// ========== + +void cli_vcp_enable(CliVcp* cli_vcp) { + CliVcpMessage message = { + .type = CliVcpMessageTypeEnable, + }; + furi_message_queue_put(cli_vcp->message_queue, &message, FuriWaitForever); } -static void cli_vcp_tx(const uint8_t* buffer, size_t size) { - furi_assert(vcp); - furi_assert(buffer); - - if(vcp->running == false) { - return; - } - - VCP_DEBUG("tx %u start", size); - - while(size > 0 && vcp->connected) { - size_t batch_size = size; - if(batch_size > USB_CDC_PKT_LEN) batch_size = USB_CDC_PKT_LEN; - - furi_stream_buffer_send(vcp->tx_stream, buffer, batch_size, FuriWaitForever); - furi_thread_flags_set(furi_thread_get_id(vcp->thread), VcpEvtStreamTx); - VCP_DEBUG("tx %u", batch_size); - - size -= batch_size; - buffer += batch_size; - } - - VCP_DEBUG("tx %u end", size); +void cli_vcp_disable(CliVcp* cli_vcp) { + CliVcpMessage message = { + .type = CliVcpMessageTypeDisable, + }; + furi_message_queue_put(cli_vcp->message_queue, &message, FuriWaitForever); } - -static void cli_vcp_tx_stdout(const char* data, size_t size, void* context) { - UNUSED(context); - cli_vcp_tx((const uint8_t*)data, size); -} - -static void vcp_state_callback(void* context, uint8_t state) { - UNUSED(context); - if(state == 0) { - furi_thread_flags_set(furi_thread_get_id(vcp->thread), VcpEvtDisconnect); - } -} - -static void vcp_on_cdc_control_line(void* context, uint8_t state) { - UNUSED(context); - // bit 0: DTR state, bit 1: RTS state - bool dtr = state & (1 << 0); - - if(dtr == true) { - furi_thread_flags_set(furi_thread_get_id(vcp->thread), VcpEvtConnect); - } else { - furi_thread_flags_set(furi_thread_get_id(vcp->thread), VcpEvtDisconnect); - } -} - -static void vcp_on_cdc_rx(void* context) { - UNUSED(context); - uint32_t ret = furi_thread_flags_set(furi_thread_get_id(vcp->thread), VcpEvtRx); - furi_check(!(ret & FuriFlagError)); -} - -static void vcp_on_cdc_tx_complete(void* context) { - UNUSED(context); - furi_thread_flags_set(furi_thread_get_id(vcp->thread), VcpEvtTx); -} - -static bool cli_vcp_is_connected(void) { - furi_assert(vcp); - return vcp->connected; -} - -const CliSession cli_vcp = { - cli_vcp_init, - cli_vcp_deinit, - cli_vcp_rx, - cli_vcp_rx_stdin, - cli_vcp_tx, - cli_vcp_tx_stdout, - cli_vcp_is_connected, -}; diff --git a/applications/services/cli/cli_vcp.h b/applications/services/cli/cli_vcp.h index f51625342..10e286183 100644 --- a/applications/services/cli/cli_vcp.h +++ b/applications/services/cli/cli_vcp.h @@ -9,9 +9,12 @@ extern "C" { #endif -typedef struct CliSession CliSession; +#define RECORD_CLI_VCP "cli_vcp" -extern const CliSession cli_vcp; +typedef struct CliVcp CliVcp; + +void cli_vcp_enable(CliVcp* cli_vcp); +void cli_vcp_disable(CliVcp* cli_vcp); #ifdef __cplusplus } diff --git a/applications/services/cli/shell/cli_shell.c b/applications/services/cli/shell/cli_shell.c new file mode 100644 index 000000000..62cbbd403 --- /dev/null +++ b/applications/services/cli/shell/cli_shell.c @@ -0,0 +1,251 @@ +#include "cli_shell.h" +#include "cli_shell_i.h" +#include "../cli_ansi.h" +#include "../cli_i.h" +#include "../cli_commands.h" +#include "cli_shell_line.h" +#include +#include +#include +#include +#include +#include +#include + +#define TAG "CliShell" + +#define ANSI_TIMEOUT_MS 10 + +typedef enum { + CliShellComponentLine, + CliShellComponentMAX, //cli, command_name, &command_data)) { + printf( + ANSI_FG_RED "could not find command `%s`, try `help`" ANSI_RESET, + furi_string_get_cstr(command_name)); + break; + } + + // lock loader + if(!(command_data.flags & CliCommandFlagParallelSafe)) { + loader = furi_record_open(RECORD_LOADER); + bool success = loader_lock(loader); + if(!success) { + printf(ANSI_FG_RED + "this command cannot be run while an application is open" ANSI_RESET); + break; + } + } + + command_data.execute_callback(cli_shell->pipe, args, command_data.context); + } while(0); + + furi_string_free(command_name); + furi_string_free(args); + + // unlock loader + if(loader) loader_unlock(loader); + furi_record_close(RECORD_LOADER); +} + +// ============== +// Event handlers +// ============== + +static void cli_shell_process_key(CliShell* cli_shell, CliKeyCombo key_combo) { + for(size_t i = 0; i < CliShellComponentMAX; i++) { // -V1008 + CliShellKeyComboSet* set = component_key_combo_sets[i]; + void* component_context = cli_shell->components[i]; + + for(size_t j = 0; j < set->count; j++) { + if(set->records[j].combo.modifiers == key_combo.modifiers && + set->records[j].combo.key == key_combo.key) + if(set->records[j].action(key_combo, component_context)) return; + } + + if(set->fallback) + if(set->fallback(key_combo, component_context)) return; + } +} + +static void cli_shell_pipe_broken(PipeSide* pipe, void* context) { + // allow commands to be processed before we stop the shell + if(pipe_bytes_available(pipe)) return; + + CliShell* cli_shell = context; + furi_event_loop_stop(cli_shell->event_loop); +} + +static void cli_shell_data_available(PipeSide* pipe, void* context) { + UNUSED(pipe); + CliShell* cli_shell = context; + + furi_event_loop_timer_start(cli_shell->ansi_parsing_timer, furi_ms_to_ticks(ANSI_TIMEOUT_MS)); + + // process ANSI escape sequences + int c = getchar(); + furi_assert(c >= 0); + CliAnsiParserResult parse_result = cli_ansi_parser_feed(cli_shell->ansi_parser, c); + if(!parse_result.is_done) return; + CliKeyCombo key_combo = parse_result.result; + if(key_combo.key == CliKeyUnrecognized) return; + + cli_shell_process_key(cli_shell, key_combo); +} + +static void cli_shell_timer_expired(void* context) { + CliShell* cli_shell = context; + CliAnsiParserResult parse_result = cli_ansi_parser_feed_timeout(cli_shell->ansi_parser); + if(!parse_result.is_done) return; + CliKeyCombo key_combo = parse_result.result; + if(key_combo.key == CliKeyUnrecognized) return; + + cli_shell_process_key(cli_shell, key_combo); +} + +// ======= +// Helpers +// ======= + +static CliShell* cli_shell_alloc(PipeSide* pipe) { + CliShell* cli_shell = malloc(sizeof(CliShell)); + + cli_shell->cli = furi_record_open(RECORD_CLI); + cli_shell->ansi_parser = cli_ansi_parser_alloc(); + cli_shell->pipe = pipe; + pipe_install_as_stdio(cli_shell->pipe); + + cli_shell->components[CliShellComponentLine] = cli_shell_line_alloc(cli_shell); + + cli_shell->event_loop = furi_event_loop_alloc(); + cli_shell->ansi_parsing_timer = furi_event_loop_timer_alloc( + cli_shell->event_loop, cli_shell_timer_expired, FuriEventLoopTimerTypeOnce, cli_shell); + pipe_attach_to_event_loop(cli_shell->pipe, cli_shell->event_loop); + + pipe_set_callback_context(cli_shell->pipe, cli_shell); + pipe_set_data_arrived_callback(cli_shell->pipe, cli_shell_data_available, 0); + pipe_set_broken_callback(cli_shell->pipe, cli_shell_pipe_broken, 0); + + return cli_shell; +} + +static void cli_shell_free(CliShell* cli_shell) { + cli_shell_line_free(cli_shell->components[CliShellComponentLine]); + + pipe_detach_from_event_loop(cli_shell->pipe); + furi_event_loop_timer_free(cli_shell->ansi_parsing_timer); + furi_event_loop_free(cli_shell->event_loop); + pipe_free(cli_shell->pipe); + cli_ansi_parser_free(cli_shell->ansi_parser); + furi_record_close(RECORD_CLI); + free(cli_shell); +} + +static void cli_shell_motd(void) { + printf(ANSI_FLIPPER_BRAND_ORANGE + "\r\n" + " _.-------.._ -,\r\n" + " .-\"```\"--..,,_/ /`-, -, \\ \r\n" + " .:\" /:/ /'\\ \\ ,_..., `. | |\r\n" + " / ,----/:/ /`\\ _\\~`_-\"` _;\r\n" + " ' / /`\"\"\"'\\ \\ \\.~`_-' ,-\"'/ \r\n" + " | | | 0 | | .-' ,/` /\r\n" + " | ,..\\ \\ ,.-\"` ,/` /\r\n" + " ; : `/`\"\"\\` ,/--==,/-----,\r\n" + " | `-...| -.___-Z:_______J...---;\r\n" + " : ` _-'\r\n" + " _L_ _ ___ ___ ___ ___ ____--\"`___ _ ___\r\n" + "| __|| | |_ _|| _ \\| _ \\| __|| _ \\ / __|| | |_ _|\r\n" + "| _| | |__ | | | _/| _/| _| | / | (__ | |__ | |\r\n" + "|_| |____||___||_| |_| |___||_|_\\ \\___||____||___|\r\n" + "\r\n" ANSI_FG_BR_WHITE "Welcome to Flipper Zero Command Line Interface!\r\n" + "Read the manual: https://docs.flipper.net/development/cli\r\n" + "Run `help` or `?` to list available commands\r\n" + "\r\n" ANSI_RESET); + + const Version* firmware_version = furi_hal_version_get_firmware_version(); + if(firmware_version) { + printf( + "Firmware version: %s %s (%s%s built on %s)\r\n", + version_get_gitbranch(firmware_version), + version_get_version(firmware_version), + version_get_githash(firmware_version), + version_get_dirty_flag(firmware_version) ? "-dirty" : "", + version_get_builddate(firmware_version)); + } +} + +static int32_t cli_shell_thread(void* context) { + PipeSide* pipe = context; + + // Sometimes, the other side closes the pipe even before our thread is started. Although the + // rest of the code will eventually find this out if this check is removed, there's no point in + // wasting time. + if(pipe_state(pipe) == PipeStateBroken) return 0; + + CliShell* cli_shell = cli_shell_alloc(pipe); + + FURI_LOG_D(TAG, "Started"); + cli_shell_motd(); + cli_shell_line_prompt(cli_shell->components[CliShellComponentLine]); + + furi_event_loop_run(cli_shell->event_loop); + + FURI_LOG_D(TAG, "Stopped"); + + cli_shell_free(cli_shell); + return 0; +} + +// ========== +// Public API +// ========== + +FuriThread* cli_shell_start(PipeSide* pipe) { + FuriThread* thread = + furi_thread_alloc_ex("CliShell", CLI_SHELL_STACK_SIZE, cli_shell_thread, pipe); + furi_thread_start(thread); + return thread; +} diff --git a/applications/services/cli/shell/cli_shell.h b/applications/services/cli/shell/cli_shell.h new file mode 100644 index 000000000..e60eefc77 --- /dev/null +++ b/applications/services/cli/shell/cli_shell.h @@ -0,0 +1,16 @@ +#pragma once + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define CLI_SHELL_STACK_SIZE (4 * 1024U) + +FuriThread* cli_shell_start(PipeSide* pipe); + +#ifdef __cplusplus +} +#endif diff --git a/applications/services/cli/shell/cli_shell_i.h b/applications/services/cli/shell/cli_shell_i.h new file mode 100644 index 000000000..e8eae92c6 --- /dev/null +++ b/applications/services/cli/shell/cli_shell_i.h @@ -0,0 +1,32 @@ +#pragma once + +#include "../cli_ansi.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct CliShell CliShell; + +/** + * @brief Key combo handler + * @return true if the event was handled, false otherwise + */ +typedef bool (*CliShellKeyComboAction)(CliKeyCombo combo, void* context); + +typedef struct { + CliKeyCombo combo; + CliShellKeyComboAction action; +} CliShellKeyComboRecord; + +typedef struct { + CliShellKeyComboAction fallback; + size_t count; + CliShellKeyComboRecord records[]; +} CliShellKeyComboSet; + +void cli_shell_execute_command(CliShell* cli_shell, FuriString* command); + +#ifdef __cplusplus +} +#endif diff --git a/applications/services/cli/shell/cli_shell_line.c b/applications/services/cli/shell/cli_shell_line.c new file mode 100644 index 000000000..45bc19d9d --- /dev/null +++ b/applications/services/cli/shell/cli_shell_line.c @@ -0,0 +1,238 @@ +#include "cli_shell_line.h" + +#define HISTORY_DEPTH 10 + +struct CliShellLine { + size_t history_position; + size_t line_position; + FuriString* history[HISTORY_DEPTH]; + size_t history_entries; + CliShell* shell; +}; + +// ========== +// Public API +// ========== + +CliShellLine* cli_shell_line_alloc(CliShell* shell) { + CliShellLine* line = malloc(sizeof(CliShellLine)); + line->shell = shell; + + line->history[0] = furi_string_alloc(); + line->history_entries = 1; + + return line; +} + +void cli_shell_line_free(CliShellLine* line) { + for(size_t i = 0; i < line->history_entries; i++) + furi_string_free(line->history[i]); + + free(line); +} + +FuriString* cli_shell_line_get_selected(CliShellLine* line) { + return line->history[line->history_position]; +} + +FuriString* cli_shell_line_get_editing(CliShellLine* line) { + return line->history[0]; +} + +size_t cli_shell_line_prompt_length(CliShellLine* line) { + UNUSED(line); + return strlen(">: "); +} + +void cli_shell_line_format_prompt(CliShellLine* line, char* buf, size_t length) { + UNUSED(line); + snprintf(buf, length - 1, ">: "); +} + +void cli_shell_line_prompt(CliShellLine* line) { + char buffer[32]; + cli_shell_line_format_prompt(line, buffer, sizeof(buffer)); + printf("\r\n%s", buffer); + fflush(stdout); +} + +void cli_shell_line_ensure_not_overwriting_history(CliShellLine* line) { + if(line->history_position > 0) { + FuriString* source = cli_shell_line_get_selected(line); + FuriString* destination = cli_shell_line_get_editing(line); + furi_string_set(destination, source); + line->history_position = 0; + } +} + +// ============== +// Input handlers +// ============== + +static bool cli_shell_line_input_ctrl_c(CliKeyCombo combo, void* context) { + UNUSED(combo); + CliShellLine* line = context; + // reset input + furi_string_reset(cli_shell_line_get_editing(line)); + line->line_position = 0; + line->history_position = 0; + printf("^C"); + cli_shell_line_prompt(line); + return true; +} + +static bool cli_shell_line_input_cr(CliKeyCombo combo, void* context) { + UNUSED(combo); + CliShellLine* line = context; + + FuriString* command = cli_shell_line_get_selected(line); + furi_string_trim(command); + FuriString* command_copy = furi_string_alloc_set(command); + + if(line->history_position > 0) { + // move selected command to the front + memmove( + &line->history[1], &line->history[0], line->history_position * sizeof(FuriString*)); + line->history[0] = command; + } + + // insert empty command + if(line->history_entries == HISTORY_DEPTH) { + furi_string_free(line->history[HISTORY_DEPTH - 1]); + line->history_entries--; + } + memmove(&line->history[1], &line->history[0], line->history_entries * sizeof(FuriString*)); + line->history[0] = furi_string_alloc(); + line->history_entries++; + line->line_position = 0; + line->history_position = 0; + + // execute command + printf("\r\n"); + if(!furi_string_empty(command_copy)) cli_shell_execute_command(line->shell, command_copy); + furi_string_free(command_copy); + + cli_shell_line_prompt(line); + return true; +} + +static bool cli_shell_line_input_up_down(CliKeyCombo combo, void* context) { + CliShellLine* line = context; + // go up and down in history + int increment = (combo.key == CliKeyUp) ? 1 : -1; + size_t new_pos = + CLAMP((int)line->history_position + increment, (int)line->history_entries - 1, 0); + + // print prompt with selected command + if(new_pos != line->history_position) { + line->history_position = new_pos; + FuriString* command = cli_shell_line_get_selected(line); + printf( + ANSI_CURSOR_HOR_POS("1") ">: %s" ANSI_ERASE_LINE(ANSI_ERASE_FROM_CURSOR_TO_END), + furi_string_get_cstr(command)); + fflush(stdout); + line->line_position = furi_string_size(command); + } + return true; +} + +static bool cli_shell_line_input_left_right(CliKeyCombo combo, void* context) { + CliShellLine* line = context; + // go left and right in the current line + FuriString* command = cli_shell_line_get_selected(line); + int increment = (combo.key == CliKeyRight) ? 1 : -1; + size_t new_pos = + CLAMP((int)line->line_position + increment, (int)furi_string_size(command), 0); + + // move cursor + if(new_pos != line->line_position) { + line->line_position = new_pos; + printf("%s", (increment == 1) ? ANSI_CURSOR_RIGHT_BY("1") : ANSI_CURSOR_LEFT_BY("1")); + fflush(stdout); + } + return true; +} + +static bool cli_shell_line_input_home(CliKeyCombo combo, void* context) { + UNUSED(combo); + CliShellLine* line = context; + // go to the start + line->line_position = 0; + printf(ANSI_CURSOR_HOR_POS("%zu"), cli_shell_line_prompt_length(line) + 1); + fflush(stdout); + return true; +} + +static bool cli_shell_line_input_end(CliKeyCombo combo, void* context) { + UNUSED(combo); + CliShellLine* line = context; + // go to the end + line->line_position = furi_string_size(cli_shell_line_get_selected(line)); + printf( + ANSI_CURSOR_HOR_POS("%zu"), cli_shell_line_prompt_length(line) + line->line_position + 1); + fflush(stdout); + return true; +} + +static bool cli_shell_line_input_bksp(CliKeyCombo combo, void* context) { + UNUSED(combo); + CliShellLine* line = context; + // erase one character + cli_shell_line_ensure_not_overwriting_history(line); + FuriString* editing_line = cli_shell_line_get_editing(line); + if(line->line_position == 0) { + putc(CliKeyBell, stdout); + fflush(stdout); + return true; + } + line->line_position--; + furi_string_replace_at(editing_line, line->line_position, 1, ""); + + // move cursor, print the rest of the line, restore cursor + printf( + ANSI_CURSOR_LEFT_BY("1") "%s" ANSI_ERASE_LINE(ANSI_ERASE_FROM_CURSOR_TO_END), + furi_string_get_cstr(editing_line) + line->line_position); + size_t left_by = furi_string_size(editing_line) - line->line_position; + if(left_by) // apparently LEFT_BY("0") still shifts left by one ._ . + printf(ANSI_CURSOR_LEFT_BY("%zu"), left_by); + fflush(stdout); + return true; +} + +static bool cli_shell_line_input_fallback(CliKeyCombo combo, void* context) { + CliShellLine* line = context; + if(combo.modifiers != CliModKeyNo) return false; + if(combo.key < CliKeySpace || combo.key >= CliKeyDEL) return false; + // insert character + cli_shell_line_ensure_not_overwriting_history(line); + FuriString* editing_line = cli_shell_line_get_editing(line); + if(line->line_position == furi_string_size(editing_line)) { + furi_string_push_back(editing_line, combo.key); + printf("%c", combo.key); + } else { + const char in_str[2] = {combo.key, 0}; + furi_string_replace_at(editing_line, line->line_position, 0, in_str); + printf(ANSI_INSERT_MODE_ENABLE "%c" ANSI_INSERT_MODE_DISABLE, combo.key); + } + fflush(stdout); + line->line_position++; + return true; +} + +CliShellKeyComboSet cli_shell_line_key_combo_set = { + .fallback = cli_shell_line_input_fallback, + .count = 10, + .records = + { + {{CliModKeyNo, CliKeyETX}, cli_shell_line_input_ctrl_c}, + {{CliModKeyNo, CliKeyCR}, cli_shell_line_input_cr}, + {{CliModKeyNo, CliKeyUp}, cli_shell_line_input_up_down}, + {{CliModKeyNo, CliKeyDown}, cli_shell_line_input_up_down}, + {{CliModKeyNo, CliKeyLeft}, cli_shell_line_input_left_right}, + {{CliModKeyNo, CliKeyRight}, cli_shell_line_input_left_right}, + {{CliModKeyNo, CliKeyHome}, cli_shell_line_input_home}, + {{CliModKeyNo, CliKeyEnd}, cli_shell_line_input_end}, + {{CliModKeyNo, CliKeyBackspace}, cli_shell_line_input_bksp}, + {{CliModKeyNo, CliKeyDEL}, cli_shell_line_input_bksp}, + }, +}; diff --git a/applications/services/cli/shell/cli_shell_line.h b/applications/services/cli/shell/cli_shell_line.h new file mode 100644 index 000000000..c1c810ee4 --- /dev/null +++ b/applications/services/cli/shell/cli_shell_line.h @@ -0,0 +1,36 @@ +#pragma once + +#include + +#include "cli_shell_i.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct CliShellLine CliShellLine; + +CliShellLine* cli_shell_line_alloc(CliShell* shell); + +void cli_shell_line_free(CliShellLine* line); + +FuriString* cli_shell_line_get_selected(CliShellLine* line); + +FuriString* cli_shell_line_get_editing(CliShellLine* line); + +size_t cli_shell_line_prompt_length(CliShellLine* line); + +void cli_shell_line_format_prompt(CliShellLine* line, char* buf, size_t length); + +void cli_shell_line_prompt(CliShellLine* line); + +/** + * @brief If a line from history has been selected, moves it into the active line + */ +void cli_shell_line_ensure_not_overwriting_history(CliShellLine* line); + +extern CliShellKeyComboSet cli_shell_line_key_combo_set; + +#ifdef __cplusplus +} +#endif diff --git a/applications/services/crypto/crypto_cli.c b/applications/services/crypto/crypto_cli.c index 744fa7151..bfefaf0f1 100644 --- a/applications/services/crypto/crypto_cli.c +++ b/applications/services/crypto/crypto_cli.c @@ -2,6 +2,7 @@ #include #include +#include #include void crypto_cli_print_usage(void) { @@ -17,7 +18,7 @@ void crypto_cli_print_usage(void) { "\tstore_key \t - Store key in secure enclave. !!! NON-REVERSABLE OPERATION - READ MANUAL FIRST !!!\r\n"); } -void crypto_cli_encrypt(Cli* cli, FuriString* args) { +void crypto_cli_encrypt(PipeSide* pipe, FuriString* args) { int key_slot = 0; bool key_loaded = false; uint8_t iv[16]; @@ -44,15 +45,15 @@ void crypto_cli_encrypt(Cli* cli, FuriString* args) { FuriString* input; input = furi_string_alloc(); char c; - while(cli_read(cli, (uint8_t*)&c, 1) == 1) { - if(c == CliSymbolAsciiETX) { + while(pipe_receive(pipe, (uint8_t*)&c, 1) == 1) { + if(c == CliKeyETX) { printf("\r\n"); break; } else if(c >= 0x20 && c < 0x7F) { putc(c, stdout); fflush(stdout); furi_string_push_back(input, c); - } else if(c == CliSymbolAsciiCR) { + } else if(c == CliKeyCR) { printf("\r\n"); furi_string_cat(input, "\r\n"); } @@ -92,7 +93,7 @@ void crypto_cli_encrypt(Cli* cli, FuriString* args) { } } -void crypto_cli_decrypt(Cli* cli, FuriString* args) { +void crypto_cli_decrypt(PipeSide* pipe, FuriString* args) { int key_slot = 0; bool key_loaded = false; uint8_t iv[16]; @@ -119,15 +120,15 @@ void crypto_cli_decrypt(Cli* cli, FuriString* args) { FuriString* hex_input; hex_input = furi_string_alloc(); char c; - while(cli_read(cli, (uint8_t*)&c, 1) == 1) { - if(c == CliSymbolAsciiETX) { + while(pipe_receive(pipe, (uint8_t*)&c, 1) == 1) { + if(c == CliKeyETX) { printf("\r\n"); break; } else if(c >= 0x20 && c < 0x7F) { putc(c, stdout); fflush(stdout); furi_string_push_back(hex_input, c); - } else if(c == CliSymbolAsciiCR) { + } else if(c == CliKeyCR) { printf("\r\n"); } } @@ -164,8 +165,8 @@ void crypto_cli_decrypt(Cli* cli, FuriString* args) { } } -void crypto_cli_has_key(Cli* cli, FuriString* args) { - UNUSED(cli); +void crypto_cli_has_key(PipeSide* pipe, FuriString* args) { + UNUSED(pipe); int key_slot = 0; uint8_t iv[16] = {0}; @@ -186,8 +187,8 @@ void crypto_cli_has_key(Cli* cli, FuriString* args) { } while(0); } -void crypto_cli_store_key(Cli* cli, FuriString* args) { - UNUSED(cli); +void crypto_cli_store_key(PipeSide* pipe, FuriString* args) { + UNUSED(pipe); int key_slot = 0; int key_size = 0; FuriString* key_type; @@ -279,7 +280,7 @@ void crypto_cli_store_key(Cli* cli, FuriString* args) { furi_string_free(key_type); } -static void crypto_cli(Cli* cli, FuriString* args, void* context) { +static void crypto_cli(PipeSide* pipe, FuriString* args, void* context) { UNUSED(context); FuriString* cmd; cmd = furi_string_alloc(); @@ -291,22 +292,22 @@ static void crypto_cli(Cli* cli, FuriString* args, void* context) { } if(furi_string_cmp_str(cmd, "encrypt") == 0) { - crypto_cli_encrypt(cli, args); + crypto_cli_encrypt(pipe, args); break; } if(furi_string_cmp_str(cmd, "decrypt") == 0) { - crypto_cli_decrypt(cli, args); + crypto_cli_decrypt(pipe, args); break; } if(furi_string_cmp_str(cmd, "has_key") == 0) { - crypto_cli_has_key(cli, args); + crypto_cli_has_key(pipe, args); break; } if(furi_string_cmp_str(cmd, "store_key") == 0) { - crypto_cli_store_key(cli, args); + crypto_cli_store_key(pipe, args); break; } diff --git a/applications/services/desktop/desktop.c b/applications/services/desktop/desktop.c index 1132760d5..185fb9c3b 100644 --- a/applications/services/desktop/desktop.c +++ b/applications/services/desktop/desktop.c @@ -396,8 +396,8 @@ void desktop_lock(Desktop* desktop) { furi_hal_rtc_set_flag(FuriHalRtcFlagLock); if(desktop_pin_code_is_set()) { - Cli* cli = furi_record_open(RECORD_CLI); - cli_session_close(cli); + CliVcp* cli_vcp = furi_record_open(RECORD_CLI_VCP); + cli_vcp_disable(cli_vcp); furi_record_close(RECORD_CLI); } @@ -426,8 +426,8 @@ void desktop_unlock(Desktop* desktop) { furi_hal_rtc_set_pin_fails(0); if(desktop_pin_code_is_set()) { - Cli* cli = furi_record_open(RECORD_CLI); - cli_session_open(cli, &cli_vcp); + CliVcp* cli_vcp = furi_record_open(RECORD_CLI_VCP); + cli_vcp_enable(cli_vcp); furi_record_close(RECORD_CLI); } @@ -525,6 +525,10 @@ int32_t desktop_srv(void* p) { if(desktop_pin_code_is_set()) { desktop_lock(desktop); + } else { + CliVcp* cli_vcp = furi_record_open(RECORD_CLI_VCP); + cli_vcp_enable(cli_vcp); + furi_record_close(RECORD_CLI); } if(storage_file_exists(desktop->storage, SLIDESHOW_FS_PATH)) { diff --git a/applications/services/input/input.c b/applications/services/input/input.c index 6cbafb795..c77771f3e 100644 --- a/applications/services/input/input.c +++ b/applications/services/input/input.c @@ -6,6 +6,7 @@ #include #include #include +#include #define INPUT_DEBOUNCE_TICKS_HALF (INPUT_DEBOUNCE_TICKS / 2) #define INPUT_PRESS_TICKS 150 @@ -25,7 +26,7 @@ typedef struct { } InputPinState; /** Input CLI command handler */ -void input_cli(Cli* cli, FuriString* args, void* context); +void input_cli(PipeSide* pipe, FuriString* args, void* context); // #define INPUT_DEBUG diff --git a/applications/services/input/input_cli.c b/applications/services/input/input_cli.c index 8e711c895..a34ec3c36 100644 --- a/applications/services/input/input_cli.c +++ b/applications/services/input/input_cli.c @@ -3,6 +3,7 @@ #include #include #include +#include static void input_cli_usage(void) { printf("Usage:\r\n"); @@ -19,7 +20,7 @@ static void input_cli_dump_events_callback(const void* value, void* ctx) { furi_message_queue_put(input_queue, value, FuriWaitForever); } -static void input_cli_dump(Cli* cli, FuriString* args, FuriPubSub* event_pubsub) { +static void input_cli_dump(PipeSide* pipe, FuriString* args, FuriPubSub* event_pubsub) { UNUSED(args); FuriMessageQueue* input_queue = furi_message_queue_alloc(8, sizeof(InputEvent)); FuriPubSubSubscription* input_subscription = @@ -27,7 +28,7 @@ static void input_cli_dump(Cli* cli, FuriString* args, FuriPubSub* event_pubsub) InputEvent input_event; printf("Press CTRL+C to stop\r\n"); - while(!cli_cmd_interrupt_received(cli)) { + while(!cli_is_pipe_broken_or_is_etx_next_char(pipe)) { if(furi_message_queue_get(input_queue, &input_event, 100) == FuriStatusOk) { printf( "key: %s type: %s\r\n", @@ -47,8 +48,8 @@ static void input_cli_send_print_usage(void) { printf("\t\t \t - one of 'press', 'release', 'short', 'long'\r\n"); } -static void input_cli_send(Cli* cli, FuriString* args, FuriPubSub* event_pubsub) { - UNUSED(cli); +static void input_cli_send(PipeSide* pipe, FuriString* args, FuriPubSub* event_pubsub) { + UNUSED(pipe); InputEvent event; FuriString* key_str; key_str = furi_string_alloc(); @@ -97,8 +98,7 @@ static void input_cli_send(Cli* cli, FuriString* args, FuriPubSub* event_pubsub) furi_string_free(key_str); } -void input_cli(Cli* cli, FuriString* args, void* context) { - furi_assert(cli); +void input_cli(PipeSide* pipe, FuriString* args, void* context) { furi_assert(context); FuriPubSub* event_pubsub = context; FuriString* cmd; @@ -110,11 +110,11 @@ void input_cli(Cli* cli, FuriString* args, void* context) { break; } if(furi_string_cmp_str(cmd, "dump") == 0) { - input_cli_dump(cli, args, event_pubsub); + input_cli_dump(pipe, args, event_pubsub); break; } if(furi_string_cmp_str(cmd, "send") == 0) { - input_cli_send(cli, args, event_pubsub); + input_cli_send(pipe, args, event_pubsub); break; } diff --git a/applications/services/loader/loader_cli.c b/applications/services/loader/loader_cli.c index f3ea30df2..40312d8b3 100644 --- a/applications/services/loader/loader_cli.c +++ b/applications/services/loader/loader_cli.c @@ -6,6 +6,7 @@ #include #include #include +#include static void loader_cli_print_usage(void) { printf("Usage:\r\n"); @@ -110,8 +111,8 @@ static void loader_cli_signal(FuriString* args, Loader* loader) { } } -static void loader_cli(Cli* cli, FuriString* args, void* context) { - UNUSED(cli); +static void loader_cli(PipeSide* pipe, FuriString* args, void* context) { + UNUSED(pipe); UNUSED(context); Loader* loader = furi_record_open(RECORD_LOADER); diff --git a/applications/services/power/power_cli.c b/applications/services/power/power_cli.c index 121552768..f1771d9f1 100644 --- a/applications/services/power/power_cli.c +++ b/applications/services/power/power_cli.c @@ -4,9 +4,10 @@ #include #include #include +#include -void power_cli_off(Cli* cli, FuriString* args) { - UNUSED(cli); +void power_cli_off(PipeSide* pipe, FuriString* args) { + UNUSED(pipe); UNUSED(args); Power* power = furi_record_open(RECORD_POWER); printf("It's now safe to disconnect USB from your flipper\r\n"); @@ -14,22 +15,22 @@ void power_cli_off(Cli* cli, FuriString* args) { power_off(power); } -void power_cli_reboot(Cli* cli, FuriString* args) { - UNUSED(cli); +void power_cli_reboot(PipeSide* pipe, FuriString* args) { + UNUSED(pipe); UNUSED(args); Power* power = furi_record_open(RECORD_POWER); power_reboot(power, PowerBootModeNormal); } -void power_cli_reboot2dfu(Cli* cli, FuriString* args) { - UNUSED(cli); +void power_cli_reboot2dfu(PipeSide* pipe, FuriString* args) { + UNUSED(pipe); UNUSED(args); Power* power = furi_record_open(RECORD_POWER); power_reboot(power, PowerBootModeDfu); } -void power_cli_5v(Cli* cli, FuriString* args) { - UNUSED(cli); +void power_cli_5v(PipeSide* pipe, FuriString* args) { + UNUSED(pipe); Power* power = furi_record_open(RECORD_POWER); if(!furi_string_cmp(args, "0")) { power_enable_otg(power, false); @@ -42,8 +43,8 @@ void power_cli_5v(Cli* cli, FuriString* args) { furi_record_close(RECORD_POWER); } -void power_cli_3v3(Cli* cli, FuriString* args) { - UNUSED(cli); +void power_cli_3v3(PipeSide* pipe, FuriString* args) { + UNUSED(pipe); if(!furi_string_cmp(args, "0")) { furi_hal_power_disable_external_3_3v(); } else if(!furi_string_cmp(args, "1")) { @@ -67,7 +68,7 @@ static void power_cli_command_print_usage(void) { } } -void power_cli(Cli* cli, FuriString* args, void* context) { +void power_cli(PipeSide* pipe, FuriString* args, void* context) { UNUSED(context); FuriString* cmd; cmd = furi_string_alloc(); @@ -79,28 +80,28 @@ void power_cli(Cli* cli, FuriString* args, void* context) { } if(furi_string_cmp_str(cmd, "off") == 0) { - power_cli_off(cli, args); + power_cli_off(pipe, args); break; } if(furi_string_cmp_str(cmd, "reboot") == 0) { - power_cli_reboot(cli, args); + power_cli_reboot(pipe, args); break; } if(furi_string_cmp_str(cmd, "reboot2dfu") == 0) { - power_cli_reboot2dfu(cli, args); + power_cli_reboot2dfu(pipe, args); break; } if(furi_string_cmp_str(cmd, "5v") == 0) { - power_cli_5v(cli, args); + power_cli_5v(pipe, args); break; } if(furi_hal_rtc_is_flag_set(FuriHalRtcFlagDebug)) { if(furi_string_cmp_str(cmd, "3v3") == 0) { - power_cli_3v3(cli, args); + power_cli_3v3(pipe, args); break; } } diff --git a/applications/services/rpc/rpc_cli.c b/applications/services/rpc/rpc_cli.c index 4612752a8..3280fe702 100644 --- a/applications/services/rpc/rpc_cli.c +++ b/applications/services/rpc/rpc_cli.c @@ -2,24 +2,24 @@ #include #include #include +#include #define TAG "RpcCli" typedef struct { - Cli* cli; + PipeSide* pipe; bool session_close_request; FuriSemaphore* terminate_semaphore; } CliRpc; -#define CLI_READ_BUFFER_SIZE 64 +#define CLI_READ_BUFFER_SIZE 64UL static void rpc_cli_send_bytes_callback(void* context, uint8_t* bytes, size_t bytes_len) { furi_assert(context); furi_assert(bytes); furi_assert(bytes_len > 0); CliRpc* cli_rpc = context; - - cli_write(cli_rpc->cli, bytes, bytes_len); + pipe_send(cli_rpc->pipe, bytes, bytes_len); } static void rpc_cli_session_close_callback(void* context) { @@ -36,9 +36,9 @@ static void rpc_cli_session_terminated_callback(void* context) { furi_semaphore_release(cli_rpc->terminate_semaphore); } -void rpc_cli_command_start_session(Cli* cli, FuriString* args, void* context) { +void rpc_cli_command_start_session(PipeSide* pipe, FuriString* args, void* context) { UNUSED(args); - furi_assert(cli); + furi_assert(pipe); furi_assert(context); Rpc* rpc = context; @@ -53,7 +53,7 @@ void rpc_cli_command_start_session(Cli* cli, FuriString* args, void* context) { return; } - CliRpc cli_rpc = {.cli = cli, .session_close_request = false}; + CliRpc cli_rpc = {.pipe = pipe, .session_close_request = false}; cli_rpc.terminate_semaphore = furi_semaphore_alloc(1, 0); rpc_session_set_context(rpc_session, &cli_rpc); rpc_session_set_send_bytes_callback(rpc_session, rpc_cli_send_bytes_callback); @@ -64,8 +64,9 @@ void rpc_cli_command_start_session(Cli* cli, FuriString* args, void* context) { size_t size_received = 0; while(1) { - size_received = cli_read_timeout(cli_rpc.cli, buffer, CLI_READ_BUFFER_SIZE, 50); - if(!cli_is_connected(cli_rpc.cli) || cli_rpc.session_close_request) { + size_t to_receive = CLAMP(pipe_bytes_available(cli_rpc.pipe), CLI_READ_BUFFER_SIZE, 1UL); + size_received = pipe_receive(cli_rpc.pipe, buffer, to_receive); + if(size_received < to_receive || cli_rpc.session_close_request) { break; } diff --git a/applications/services/rpc/rpc_i.h b/applications/services/rpc/rpc_i.h index 0342df2b6..51903a276 100644 --- a/applications/services/rpc/rpc_i.h +++ b/applications/services/rpc/rpc_i.h @@ -6,6 +6,7 @@ #include #include #include +#include #ifdef __cplusplus extern "C" { @@ -46,7 +47,7 @@ void rpc_desktop_free(void* ctx); void rpc_debug_print_message(const PB_Main* message); void rpc_debug_print_data(const char* prefix, uint8_t* buffer, size_t size); -void rpc_cli_command_start_session(Cli* cli, FuriString* args, void* context); +void rpc_cli_command_start_session(PipeSide* pipe, FuriString* args, void* context); PB_CommandStatus rpc_system_storage_get_error(FS_Error fs_error); diff --git a/applications/services/storage/storage_cli.c b/applications/services/storage/storage_cli.c index 441b58da6..416ecce0e 100644 --- a/applications/services/storage/storage_cli.c +++ b/applications/services/storage/storage_cli.c @@ -10,6 +10,7 @@ #include #include #include +#include #define MAX_NAME_LENGTH 255 @@ -19,8 +20,8 @@ static void storage_cli_print_error(FS_Error error) { printf("Storage error: %s\r\n", storage_error_get_desc(error)); } -static void storage_cli_info(Cli* cli, FuriString* path, FuriString* args) { - UNUSED(cli); +static void storage_cli_info(PipeSide* pipe, FuriString* path, FuriString* args) { + UNUSED(pipe); UNUSED(args); Storage* api = furi_record_open(RECORD_STORAGE); @@ -69,13 +70,14 @@ static void storage_cli_info(Cli* cli, FuriString* path, FuriString* args) { furi_record_close(RECORD_STORAGE); } -static void storage_cli_format(Cli* cli, FuriString* path, FuriString* args) { +static void storage_cli_format(PipeSide* pipe, FuriString* path, FuriString* args) { + UNUSED(pipe); UNUSED(args); if(furi_string_cmp_str(path, STORAGE_INT_PATH_PREFIX) == 0) { storage_cli_print_error(FSE_NOT_IMPLEMENTED); } else if(furi_string_cmp_str(path, STORAGE_EXT_PATH_PREFIX) == 0) { printf("Formatting SD card, All data will be lost! Are you sure (y/n)?\r\n"); - char answer = cli_getc(cli); + char answer = getchar(); if(answer == 'y' || answer == 'Y') { Storage* api = furi_record_open(RECORD_STORAGE); printf("Formatting, please wait...\r\n"); @@ -96,8 +98,8 @@ static void storage_cli_format(Cli* cli, FuriString* path, FuriString* args) { } } -static void storage_cli_list(Cli* cli, FuriString* path, FuriString* args) { - UNUSED(cli); +static void storage_cli_list(PipeSide* pipe, FuriString* path, FuriString* args) { + UNUSED(pipe); UNUSED(args); if(furi_string_cmp_str(path, "/") == 0) { printf("\t[D] int\r\n"); @@ -134,13 +136,13 @@ static void storage_cli_list(Cli* cli, FuriString* path, FuriString* args) { } } -static void storage_cli_tree(Cli* cli, FuriString* path, FuriString* args) { +static void storage_cli_tree(PipeSide* pipe, FuriString* path, FuriString* args) { UNUSED(args); if(furi_string_cmp_str(path, "/") == 0) { furi_string_set(path, STORAGE_INT_PATH_PREFIX); - storage_cli_tree(cli, path, NULL); + storage_cli_tree(pipe, path, NULL); furi_string_set(path, STORAGE_EXT_PATH_PREFIX); - storage_cli_tree(cli, path, NULL); + storage_cli_tree(pipe, path, NULL); } else { Storage* api = furi_record_open(RECORD_STORAGE); DirWalk* dir_walk = dir_walk_alloc(api); @@ -176,8 +178,8 @@ static void storage_cli_tree(Cli* cli, FuriString* path, FuriString* args) { } } -static void storage_cli_read(Cli* cli, FuriString* path, FuriString* args) { - UNUSED(cli); +static void storage_cli_read(PipeSide* pipe, FuriString* path, FuriString* args) { + UNUSED(pipe); UNUSED(args); Storage* api = furi_record_open(RECORD_STORAGE); File* file = storage_file_alloc(api); @@ -208,7 +210,8 @@ static void storage_cli_read(Cli* cli, FuriString* path, FuriString* args) { furi_record_close(RECORD_STORAGE); } -static void storage_cli_write(Cli* cli, FuriString* path, FuriString* args) { +static void storage_cli_write(PipeSide* pipe, FuriString* path, FuriString* args) { + UNUSED(pipe); UNUSED(args); Storage* api = furi_record_open(RECORD_STORAGE); File* file = storage_file_alloc(api); @@ -222,9 +225,9 @@ static void storage_cli_write(Cli* cli, FuriString* path, FuriString* args) { uint32_t read_index = 0; while(true) { - uint8_t symbol = cli_getc(cli); + uint8_t symbol = getchar(); - if(symbol == CliSymbolAsciiETX) { + if(symbol == CliKeyETX) { size_t write_size = read_index % buffer_size; if(write_size > 0) { @@ -263,7 +266,8 @@ static void storage_cli_write(Cli* cli, FuriString* path, FuriString* args) { furi_record_close(RECORD_STORAGE); } -static void storage_cli_read_chunks(Cli* cli, FuriString* path, FuriString* args) { +static void storage_cli_read_chunks(PipeSide* pipe, FuriString* path, FuriString* args) { + UNUSED(pipe); Storage* api = furi_record_open(RECORD_STORAGE); File* file = storage_file_alloc(api); @@ -280,7 +284,7 @@ static void storage_cli_read_chunks(Cli* cli, FuriString* path, FuriString* args uint8_t* data = malloc(buffer_size); while(file_size > 0) { printf("\r\nReady?\r\n"); - cli_getc(cli); + getchar(); size_t read_size = storage_file_read(file, data, buffer_size); for(size_t i = 0; i < read_size; i++) { @@ -302,31 +306,32 @@ static void storage_cli_read_chunks(Cli* cli, FuriString* path, FuriString* args furi_record_close(RECORD_STORAGE); } -static void storage_cli_write_chunk(Cli* cli, FuriString* path, FuriString* args) { +static void storage_cli_write_chunk(PipeSide* pipe, FuriString* path, FuriString* args) { Storage* api = furi_record_open(RECORD_STORAGE); File* file = storage_file_alloc(api); - uint32_t buffer_size; - if(strint_to_uint32(furi_string_get_cstr(args), NULL, &buffer_size, 10) != + uint32_t need_to_read; + if(strint_to_uint32(furi_string_get_cstr(args), NULL, &need_to_read, 10) != StrintParseNoError) { storage_cli_print_usage(); } else { if(storage_file_open(file, furi_string_get_cstr(path), FSAM_WRITE, FSOM_OPEN_APPEND)) { printf("Ready\r\n"); + const size_t buffer_size = 1024; + uint8_t* buffer = malloc(buffer_size); - if(buffer_size) { - uint8_t* buffer = malloc(buffer_size); + while(need_to_read) { + size_t read_this_time = pipe_receive(pipe, buffer, MIN(buffer_size, need_to_read)); + size_t wrote_this_time = storage_file_write(file, buffer, read_this_time); - size_t read_bytes = cli_read(cli, buffer, buffer_size); - - size_t written_size = storage_file_write(file, buffer, read_bytes); - - if(written_size != buffer_size) { + if(wrote_this_time != read_this_time) { storage_cli_print_error(storage_file_get_error(file)); + break; } - - free(buffer); + need_to_read -= read_this_time; } + + free(buffer); } else { storage_cli_print_error(storage_file_get_error(file)); } @@ -337,8 +342,8 @@ static void storage_cli_write_chunk(Cli* cli, FuriString* path, FuriString* args furi_record_close(RECORD_STORAGE); } -static void storage_cli_stat(Cli* cli, FuriString* path, FuriString* args) { - UNUSED(cli); +static void storage_cli_stat(PipeSide* pipe, FuriString* path, FuriString* args) { + UNUSED(pipe); UNUSED(args); Storage* api = furi_record_open(RECORD_STORAGE); @@ -379,8 +384,8 @@ static void storage_cli_stat(Cli* cli, FuriString* path, FuriString* args) { furi_record_close(RECORD_STORAGE); } -static void storage_cli_timestamp(Cli* cli, FuriString* path, FuriString* args) { - UNUSED(cli); +static void storage_cli_timestamp(PipeSide* pipe, FuriString* path, FuriString* args) { + UNUSED(pipe); UNUSED(args); Storage* api = furi_record_open(RECORD_STORAGE); @@ -396,8 +401,8 @@ static void storage_cli_timestamp(Cli* cli, FuriString* path, FuriString* args) furi_record_close(RECORD_STORAGE); } -static void storage_cli_copy(Cli* cli, FuriString* old_path, FuriString* args) { - UNUSED(cli); +static void storage_cli_copy(PipeSide* pipe, FuriString* old_path, FuriString* args) { + UNUSED(pipe); Storage* api = furi_record_open(RECORD_STORAGE); FuriString* new_path; new_path = furi_string_alloc(); @@ -417,8 +422,8 @@ static void storage_cli_copy(Cli* cli, FuriString* old_path, FuriString* args) { furi_record_close(RECORD_STORAGE); } -static void storage_cli_remove(Cli* cli, FuriString* path, FuriString* args) { - UNUSED(cli); +static void storage_cli_remove(PipeSide* pipe, FuriString* path, FuriString* args) { + UNUSED(pipe); UNUSED(args); Storage* api = furi_record_open(RECORD_STORAGE); FS_Error error = storage_common_remove(api, furi_string_get_cstr(path)); @@ -430,8 +435,8 @@ static void storage_cli_remove(Cli* cli, FuriString* path, FuriString* args) { furi_record_close(RECORD_STORAGE); } -static void storage_cli_rename(Cli* cli, FuriString* old_path, FuriString* args) { - UNUSED(cli); +static void storage_cli_rename(PipeSide* pipe, FuriString* old_path, FuriString* args) { + UNUSED(pipe); Storage* api = furi_record_open(RECORD_STORAGE); FuriString* new_path; new_path = furi_string_alloc(); @@ -451,8 +456,8 @@ static void storage_cli_rename(Cli* cli, FuriString* old_path, FuriString* args) furi_record_close(RECORD_STORAGE); } -static void storage_cli_mkdir(Cli* cli, FuriString* path, FuriString* args) { - UNUSED(cli); +static void storage_cli_mkdir(PipeSide* pipe, FuriString* path, FuriString* args) { + UNUSED(pipe); UNUSED(args); Storage* api = furi_record_open(RECORD_STORAGE); FS_Error error = storage_common_mkdir(api, furi_string_get_cstr(path)); @@ -464,8 +469,8 @@ static void storage_cli_mkdir(Cli* cli, FuriString* path, FuriString* args) { furi_record_close(RECORD_STORAGE); } -static void storage_cli_md5(Cli* cli, FuriString* path, FuriString* args) { - UNUSED(cli); +static void storage_cli_md5(PipeSide* pipe, FuriString* path, FuriString* args) { + UNUSED(pipe); UNUSED(args); Storage* api = furi_record_open(RECORD_STORAGE); File* file = storage_file_alloc(api); @@ -491,8 +496,8 @@ static bool tar_extract_file_callback(const char* name, bool is_directory, void* return true; } -static void storage_cli_extract(Cli* cli, FuriString* old_path, FuriString* args) { - UNUSED(cli); +static void storage_cli_extract(PipeSide* pipe, FuriString* old_path, FuriString* args) { + UNUSED(pipe); FuriString* new_path = furi_string_alloc(); if(!args_read_probably_quoted_string_and_trim(args, new_path)) { @@ -526,7 +531,7 @@ static void storage_cli_extract(Cli* cli, FuriString* old_path, FuriString* args furi_record_close(RECORD_STORAGE); } -typedef void (*StorageCliCommandCallback)(Cli* cli, FuriString* path, FuriString* args); +typedef void (*StorageCliCommandCallback)(PipeSide* pipe, FuriString* path, FuriString* args); typedef struct { const char* command; @@ -631,7 +636,7 @@ static void storage_cli_print_usage(void) { } } -void storage_cli(Cli* cli, FuriString* args, void* context) { +void storage_cli(PipeSide* pipe, FuriString* args, void* context) { UNUSED(context); FuriString* cmd; FuriString* path; @@ -653,7 +658,7 @@ void storage_cli(Cli* cli, FuriString* args, void* context) { for(; i < COUNT_OF(storage_cli_commands); ++i) { const StorageCliCommand* command_descr = &storage_cli_commands[i]; if(furi_string_cmp_str(cmd, command_descr->command) == 0) { - command_descr->impl(cli, path, args); + command_descr->impl(pipe, path, args); break; } } @@ -667,11 +672,12 @@ void storage_cli(Cli* cli, FuriString* args, void* context) { furi_string_free(cmd); } -static void storage_cli_factory_reset(Cli* cli, FuriString* args, void* context) { +static void storage_cli_factory_reset(PipeSide* pipe, FuriString* args, void* context) { + UNUSED(pipe); UNUSED(args); UNUSED(context); printf("All data will be lost! Are you sure (y/n)?\r\n"); - char c = cli_getc(cli); + char c = getchar(); if(c == 'y' || c == 'Y') { printf("Data will be wiped after reboot.\r\n"); @@ -688,7 +694,7 @@ static void storage_cli_factory_reset(Cli* cli, FuriString* args, void* context) void storage_on_system_start(void) { #ifdef SRV_CLI Cli* cli = furi_record_open(RECORD_CLI); - cli_add_command(cli, RECORD_STORAGE, CliCommandFlagParallelSafe, storage_cli, NULL); + cli_add_command_ex(cli, "storage", CliCommandFlagParallelSafe, storage_cli, NULL, 512); cli_add_command( cli, "factory_reset", CliCommandFlagParallelSafe, storage_cli_factory_reset, NULL); furi_record_close(RECORD_CLI); diff --git a/applications/system/js_app/js_app.c b/applications/system/js_app/js_app.c index c321150df..bdb2ccc28 100644 --- a/applications/system/js_app/js_app.c +++ b/applications/system/js_app/js_app.c @@ -5,6 +5,7 @@ #include #include #include +#include #define TAG "JS app" @@ -131,12 +132,14 @@ int32_t js_app(void* arg) { } //-V773 typedef struct { - Cli* cli; + PipeSide* pipe; FuriSemaphore* exit_sem; } JsCliContext; static void js_cli_print(JsCliContext* ctx, const char* msg) { - cli_write(ctx->cli, (uint8_t*)msg, strlen(msg)); + UNUSED(ctx); + UNUSED(msg); + pipe_send(ctx->pipe, msg, strlen(msg)); } static void js_cli_exit(JsCliContext* ctx) { @@ -170,7 +173,7 @@ static void js_cli_callback(JsThreadEvent event, const char* msg, void* context) } } -void js_cli_execute(Cli* cli, FuriString* args, void* context) { +void js_cli_execute(PipeSide* pipe, FuriString* args, void* context) { UNUSED(context); const char* path = furi_string_get_cstr(args); @@ -187,14 +190,14 @@ void js_cli_execute(Cli* cli, FuriString* args, void* context) { break; } - JsCliContext ctx = {.cli = cli}; + JsCliContext ctx = {.pipe = pipe}; ctx.exit_sem = furi_semaphore_alloc(1, 0); printf("Running script %s, press CTRL+C to stop\r\n", path); JsThread* js_thread = js_thread_run(path, js_cli_callback, &ctx); while(furi_semaphore_acquire(ctx.exit_sem, 100) != FuriStatusOk) { - if(cli_cmd_interrupt_received(cli)) break; + if(cli_is_pipe_broken_or_is_etx_next_char(pipe)) break; } js_thread_stop(js_thread); diff --git a/applications/system/updater/cli/updater_cli.c b/applications/system/updater/cli/updater_cli.c index 56a16bd9d..01949269f 100644 --- a/applications/system/updater/cli/updater_cli.c +++ b/applications/system/updater/cli/updater_cli.c @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -63,8 +64,8 @@ static const CliSubcommand update_cli_subcommands[] = { {.command = "help", .handler = updater_cli_help}, }; -static void updater_cli_ep(Cli* cli, FuriString* args, void* context) { - UNUSED(cli); +static void updater_cli_ep(PipeSide* pipe, FuriString* args, void* context) { + UNUSED(pipe); UNUSED(context); FuriString* subcommand; subcommand = furi_string_alloc(); diff --git a/furi/core/core_defines.h b/furi/core/core_defines.h index fa56150ce..2d6ced025 100644 --- a/furi/core/core_defines.h +++ b/furi/core/core_defines.h @@ -41,6 +41,16 @@ extern "C" { #define CLAMP(x, upper, lower) (MIN(upper, MAX(x, lower))) #endif +#ifndef CLAMP_WRAPAROUND +#define CLAMP_WRAPAROUND(x, upper, lower) \ + ({ \ + __typeof__(x) _x = (x); \ + __typeof__(upper) _upper = (upper); \ + __typeof__(lower) _lower = (lower); \ + (_x > _upper) ? _lower : ((_x < _lower) ? _upper : _x); \ + }) +#endif + #ifndef COUNT_OF #define COUNT_OF(x) (sizeof(x) / sizeof(x[0])) #endif diff --git a/furi/core/event_loop.c b/furi/core/event_loop.c index e09be0ca4..800fea86c 100644 --- a/furi/core/event_loop.c +++ b/furi/core/event_loop.c @@ -78,6 +78,7 @@ void furi_event_loop_free(FuriEventLoop* instance) { furi_event_loop_process_timer_queue(instance); furi_check(TimerList_empty_p(instance->timer_list)); furi_check(WaitingList_empty_p(instance->waiting_list)); + furi_check(!instance->are_thread_flags_subscribed); FuriEventLoopTree_clear(instance->tree); PendingQueue_clear(instance->pending_queue); @@ -243,6 +244,10 @@ void furi_event_loop_run(FuriEventLoop* instance) { } else if(flags & FuriEventLoopFlagPending) { furi_event_loop_process_pending_callbacks(instance); + } else if(flags & FuriEventLoopFlagThreadFlag) { + if(instance->are_thread_flags_subscribed) + instance->thread_flags_callback(instance->thread_flags_callback_context); + } else { furi_crash(); } @@ -416,6 +421,24 @@ void furi_event_loop_subscribe_mutex( instance, mutex, &furi_mutex_event_loop_contract, event, callback, context); } +void furi_event_loop_subscribe_thread_flags( + FuriEventLoop* instance, + FuriEventLoopThreadFlagsCallback callback, + void* context) { + furi_check(instance); + furi_check(callback); + furi_check(!instance->are_thread_flags_subscribed); + instance->are_thread_flags_subscribed = true; + instance->thread_flags_callback = callback; + instance->thread_flags_callback_context = context; +} + +void furi_event_loop_unsubscribe_thread_flags(FuriEventLoop* instance) { + furi_check(instance); + furi_check(instance->are_thread_flags_subscribed); + instance->are_thread_flags_subscribed = false; +} + /** * Public generic unsubscription API */ @@ -538,6 +561,25 @@ static bool furi_event_loop_item_is_waiting(FuriEventLoopItem* instance) { return instance->WaitingList.prev || instance->WaitingList.next; } +void furi_event_loop_thread_flag_callback(FuriThreadId thread_id) { + TaskHandle_t hTask = (TaskHandle_t)thread_id; + BaseType_t yield; + + if(FURI_IS_IRQ_MODE()) { + yield = pdFALSE; + (void)xTaskNotifyIndexedFromISR( + hTask, + FURI_EVENT_LOOP_FLAG_NOTIFY_INDEX, + FuriEventLoopFlagThreadFlag, + eSetBits, + &yield); + portYIELD_FROM_ISR(yield); + } else { + (void)xTaskNotifyIndexed( + hTask, FURI_EVENT_LOOP_FLAG_NOTIFY_INDEX, FuriEventLoopFlagThreadFlag, eSetBits); + } +} + /* * Internal event loop link API, used by supported primitives */ diff --git a/furi/core/event_loop.h b/furi/core/event_loop.h index d5e8710a6..63f020f78 100644 --- a/furi/core/event_loop.h +++ b/furi/core/event_loop.h @@ -203,6 +203,12 @@ typedef void FuriEventLoopObject; */ typedef void (*FuriEventLoopEventCallback)(FuriEventLoopObject* object, void* context); +/** Callback type for event loop thread flag events + * + * @param context The context that was provided upon subscription + */ +typedef void (*FuriEventLoopThreadFlagsCallback)(void* context); + /** Opaque event flag type */ typedef struct FuriEventFlag FuriEventFlag; @@ -304,6 +310,23 @@ void furi_event_loop_subscribe_mutex( FuriEventLoopEventCallback callback, void* context); +/** Subscribe to thread flag events of the current thread + * + * @param instance The Event Loop instance + * @param callback The callback to call when a flag has been set + * @param context The context for callback + */ +void furi_event_loop_subscribe_thread_flags( + FuriEventLoop* instance, + FuriEventLoopThreadFlagsCallback callback, + void* context); + +/** Unsubscribe from thread flag events of the current thread + * + * @param instance The Event Loop instance + */ +void furi_event_loop_unsubscribe_thread_flags(FuriEventLoop* instance); + /** Unsubscribe from events (common) * * @param instance The Event Loop instance diff --git a/furi/core/event_loop_i.h b/furi/core/event_loop_i.h index ef2774b97..c2f04a359 100644 --- a/furi/core/event_loop_i.h +++ b/furi/core/event_loop_i.h @@ -4,12 +4,14 @@ #include "event_loop_link_i.h" #include "event_loop_timer_i.h" #include "event_loop_tick_i.h" +#include "event_loop_thread_flag_interface.h" #include #include #include #include "thread.h" +#include "thread_i.h" struct FuriEventLoopItem { // Source @@ -50,11 +52,12 @@ typedef enum { FuriEventLoopFlagStop = (1 << 1), FuriEventLoopFlagTimer = (1 << 2), FuriEventLoopFlagPending = (1 << 3), + FuriEventLoopFlagThreadFlag = (1 << 4), } FuriEventLoopFlag; #define FuriEventLoopFlagAll \ (FuriEventLoopFlagEvent | FuriEventLoopFlagStop | FuriEventLoopFlagTimer | \ - FuriEventLoopFlagPending) + FuriEventLoopFlagPending | FuriEventLoopFlagThreadFlag) typedef enum { FuriEventLoopProcessStatusComplete, @@ -94,4 +97,9 @@ struct FuriEventLoop { PendingQueue_t pending_queue; // Tick event FuriEventLoopTick tick; + + // Thread flags callback + bool are_thread_flags_subscribed; + FuriEventLoopThreadFlagsCallback thread_flags_callback; + void* thread_flags_callback_context; }; diff --git a/furi/core/event_loop_thread_flag_interface.h b/furi/core/event_loop_thread_flag_interface.h new file mode 100644 index 000000000..05fcd47de --- /dev/null +++ b/furi/core/event_loop_thread_flag_interface.h @@ -0,0 +1,10 @@ +#pragma once + +#include "thread.h" + +/** + * @brief Notify `FuriEventLoop` that `furi_thread_flags_set` has been called + * + * @param thread_id Thread id + */ +extern void furi_event_loop_thread_flag_callback(FuriThreadId thread_id); diff --git a/furi/core/thread.c b/furi/core/thread.c index d6132cdc2..e29a8711e 100644 --- a/furi/core/thread.c +++ b/furi/core/thread.c @@ -7,6 +7,7 @@ #include "check.h" #include "common_defines.h" #include "string.h" +#include "event_loop_thread_flag_interface.h" #include "log.h" #include @@ -501,6 +502,9 @@ uint32_t furi_thread_flags_set(FuriThreadId thread_id, uint32_t flags) { (void)xTaskNotifyAndQueryIndexed(hTask, THREAD_NOTIFY_INDEX, 0, eNoAction, &rflags); } } + + furi_event_loop_thread_flag_callback(thread_id); + /* Return flags after setting */ return rflags; } diff --git a/lib/toolbox/pipe.c b/lib/toolbox/pipe.c index c6a45abf1..59b2f63f4 100644 --- a/lib/toolbox/pipe.c +++ b/lib/toolbox/pipe.c @@ -1,6 +1,8 @@ #include "pipe.h" #include +#define PIPE_DEFAULT_STATE_CHECK_PERIOD furi_ms_to_ticks(100) + /** * Data shared between both sides. */ @@ -23,7 +25,7 @@ struct PipeSide { PipeSideDataArrivedCallback on_data_arrived; PipeSideSpaceFreedCallback on_space_freed; PipeSideBrokenCallback on_pipe_broken; - FuriWait stdout_timeout; + FuriWait state_check_period; }; PipeSideBundle pipe_alloc(size_t capacity, size_t trigger_level) { @@ -53,14 +55,14 @@ PipeSideBundle pipe_alloc_ex(PipeSideReceiveSettings alice, PipeSideReceiveSetti .shared = shared, .sending = alice_to_bob, .receiving = bob_to_alice, - .stdout_timeout = FuriWaitForever, + .state_check_period = PIPE_DEFAULT_STATE_CHECK_PERIOD, }; *bobs_side = (PipeSide){ .role = PipeRoleBob, .shared = shared, .sending = bob_to_alice, .receiving = alice_to_bob, - .stdout_timeout = FuriWaitForever, + .state_check_period = PIPE_DEFAULT_STATE_CHECK_PERIOD, }; return (PipeSideBundle){.alices_side = alices_side, .bobs_side = bobs_side}; @@ -99,42 +101,62 @@ void pipe_free(PipeSide* pipe) { } } -static void _pipe_stdout_cb(const char* data, size_t size, void* context) { +static void pipe_stdout_cb(const char* data, size_t size, void* context) { furi_assert(context); PipeSide* pipe = context; - while(size) { - size_t sent = pipe_send(pipe, data, size, pipe->stdout_timeout); - if(!sent) break; - data += sent; - size -= sent; - } + pipe_send(pipe, data, size); } -static size_t _pipe_stdin_cb(char* data, size_t size, FuriWait timeout, void* context) { +static size_t pipe_stdin_cb(char* data, size_t size, FuriWait timeout, void* context) { + UNUSED(timeout); furi_assert(context); PipeSide* pipe = context; - return pipe_receive(pipe, data, size, timeout); + return pipe_receive(pipe, data, size); } void pipe_install_as_stdio(PipeSide* pipe) { furi_check(pipe); - furi_thread_set_stdout_callback(_pipe_stdout_cb, pipe); - furi_thread_set_stdin_callback(_pipe_stdin_cb, pipe); + furi_thread_set_stdout_callback(pipe_stdout_cb, pipe); + furi_thread_set_stdin_callback(pipe_stdin_cb, pipe); } -void pipe_set_stdout_timeout(PipeSide* pipe, FuriWait timeout) { +void pipe_set_state_check_period(PipeSide* pipe, FuriWait check_period) { furi_check(pipe); - pipe->stdout_timeout = timeout; + pipe->state_check_period = check_period; } -size_t pipe_receive(PipeSide* pipe, void* data, size_t length, FuriWait timeout) { +size_t pipe_receive(PipeSide* pipe, void* data, size_t length) { furi_check(pipe); - return furi_stream_buffer_receive(pipe->receiving, data, length, timeout); + + size_t received = 0; + while(length) { + size_t received_this_time = + furi_stream_buffer_receive(pipe->receiving, data, length, pipe->state_check_period); + if(!received_this_time && pipe_state(pipe) == PipeStateBroken) break; + + received += received_this_time; + length -= received_this_time; + data += received_this_time; + } + + return received; } -size_t pipe_send(PipeSide* pipe, const void* data, size_t length, FuriWait timeout) { +size_t pipe_send(PipeSide* pipe, const void* data, size_t length) { furi_check(pipe); - return furi_stream_buffer_send(pipe->sending, data, length, timeout); + + size_t sent = 0; + while(length) { + size_t sent_this_time = + furi_stream_buffer_send(pipe->sending, data, length, pipe->state_check_period); + if(!sent_this_time && pipe_state(pipe) == PipeStateBroken) break; + + sent += sent_this_time; + length -= sent_this_time; + data += sent_this_time; + } + + return sent; } size_t pipe_bytes_available(PipeSide* pipe) { @@ -151,14 +173,14 @@ static void pipe_receiving_buffer_callback(FuriEventLoopObject* buffer, void* co UNUSED(buffer); PipeSide* pipe = context; furi_assert(pipe); - if(pipe->on_space_freed) pipe->on_data_arrived(pipe, pipe->callback_context); + if(pipe->on_data_arrived) pipe->on_data_arrived(pipe, pipe->callback_context); } static void pipe_sending_buffer_callback(FuriEventLoopObject* buffer, void* context) { UNUSED(buffer); PipeSide* pipe = context; furi_assert(pipe); - if(pipe->on_data_arrived) pipe->on_space_freed(pipe, pipe->callback_context); + if(pipe->on_space_freed) pipe->on_space_freed(pipe, pipe->callback_context); } static void pipe_semaphore_callback(FuriEventLoopObject* semaphore, void* context) { diff --git a/lib/toolbox/pipe.h b/lib/toolbox/pipe.h index c05e60d0c..b071975f7 100644 --- a/lib/toolbox/pipe.h +++ b/lib/toolbox/pipe.h @@ -148,38 +148,48 @@ void pipe_free(PipeSide* pipe); void pipe_install_as_stdio(PipeSide* pipe); /** - * @brief Sets the timeout for `stdout` write operations + * @brief Sets the state check period for `send` and `receive` operations * - * @note This value is set to `FuriWaitForever` when the pipe is created + * @note This value is set to 100 ms when the pipe is created * - * @param [in] pipe Pipe side to set the timeout of - * @param [in] timeout Timeout value in ticks + * `send` and `receive` will check the state of the pipe if exactly 0 bytes were + * sent or received during any given `check_period`. Read the documentation for + * `pipe_send` and `pipe_receive` for more info. + * + * @param [in] pipe Pipe side to set the check period of + * @param [in] check_period Period in ticks */ -void pipe_set_stdout_timeout(PipeSide* pipe, FuriWait timeout); +void pipe_set_state_check_period(PipeSide* pipe, FuriWait check_period); /** * @brief Receives data from the pipe. * + * This function will try to receive all of the requested bytes from the pipe. + * If at some point during the operation the pipe becomes broken, this function + * will return prematurely, in which case the return value will be less than the + * requested `length`. + * * @param [in] pipe The pipe side to read data out of * @param [out] data The buffer to fill with data * @param length Maximum length of data to read - * @param timeout The timeout (in ticks) after which the read operation is - * interrupted * @returns The number of bytes actually written into the provided buffer */ -size_t pipe_receive(PipeSide* pipe, void* data, size_t length, FuriWait timeout); +size_t pipe_receive(PipeSide* pipe, void* data, size_t length); /** * @brief Sends data into the pipe. * + * This function will try to send all of the requested bytes to the pipe. + * If at some point during the operation the pipe becomes broken, this function + * will return prematurely, in which case the return value will be less than the + * requested `length`. + * * @param [in] pipe The pipe side to send data into * @param [out] data The buffer to get data from * @param length Maximum length of data to send - * @param timeout The timeout (in ticks) after which the write operation is - * interrupted * @returns The number of bytes actually read from the provided buffer */ -size_t pipe_send(PipeSide* pipe, const void* data, size_t length, FuriWait timeout); +size_t pipe_send(PipeSide* pipe, const void* data, size_t length); /** * @brief Determines how many bytes there are in the pipe available to be read. diff --git a/scripts/serial_cli_perf.py b/scripts/serial_cli_perf.py new file mode 100644 index 000000000..0fed7c393 --- /dev/null +++ b/scripts/serial_cli_perf.py @@ -0,0 +1,59 @@ +import argparse +import logging +from serial import Serial +from random import randint +from time import time + +from flipper.utils.cdc import resolve_port + + +def main(): + logger = logging.getLogger() + parser = argparse.ArgumentParser() + parser.add_argument("-p", "--port", help="CDC Port", default="auto") + parser.add_argument( + "-l", "--length", type=int, help="Number of bytes to send", default=1024**2 + ) + args = parser.parse_args() + + if not (port := resolve_port(logger, args.port)): + logger.error("Is Flipper connected via USB and not in DFU mode?") + return 1 + port = Serial(port, 230400) + port.timeout = 2 + + port.read_until(b">: ") + port.write(b"echo\r") + port.read_until(b">: ") + + print(f"Transferring {args.length} bytes. Hang tight...") + + start_time = time() + + bytes_to_send = args.length + block_size = 1024 + while bytes_to_send: + actual_size = min(block_size, bytes_to_send) + # can't use 0x03 because that's ASCII ETX, or Ctrl+C + block = bytes([randint(4, 255) for _ in range(actual_size)]) + + port.write(block) + return_block = port.read(actual_size) + + if return_block != block: + logger.error("Incorrect block received") + break + + bytes_to_send -= actual_size + + end_time = time() + delta = end_time - start_time + speed = args.length / delta + print(f"Speed: {speed/1024:.2f} KiB/s") + + port.write(b"\x03") # Ctrl+C + port.close() + + +if __name__ == "__main__": + main() diff --git a/scripts/update.py b/scripts/update.py index 992f75603..1c877c23e 100755 --- a/scripts/update.py +++ b/scripts/update.py @@ -34,7 +34,7 @@ class Main(App): FLASH_BASE = 0x8000000 FLASH_PAGE_SIZE = 4 * 1024 - MIN_GAP_PAGES = 1 + MIN_GAP_PAGES = 0 # Update stage file larger than that is not loadable without fix # https://github.com/flipperdevices/flipperzero-firmware/pull/3676 diff --git a/targets/f18/api_symbols.csv b/targets/f18/api_symbols.csv index 981340138..bfe7afbcd 100644 --- a/targets/f18/api_symbols.csv +++ b/targets/f18/api_symbols.csv @@ -1,8 +1,9 @@ entry,status,name,type,params -Version,+,83.0,, +Version,+,84.0,, Header,+,applications/services/bt/bt_service/bt.h,, Header,+,applications/services/bt/bt_service/bt_keys_storage.h,, Header,+,applications/services/cli/cli.h,, +Header,+,applications/services/cli/cli_ansi.h,, Header,+,applications/services/cli/cli_vcp.h,, Header,+,applications/services/dialogs/dialogs.h,, Header,+,applications/services/dolphin/dolphin.h,, @@ -778,18 +779,17 @@ Function,-,ceill,long double,long double Function,-,cfree,void,void* Function,-,clearerr,void,FILE* Function,-,clearerr_unlocked,void,FILE* -Function,+,cli_add_command,void,"Cli*, const char*, CliCommandFlag, CliCallback, void*" -Function,+,cli_cmd_interrupt_received,_Bool,Cli* +Function,+,cli_add_command,void,"Cli*, const char*, CliCommandFlag, CliExecuteCallback, void*" +Function,+,cli_add_command_ex,void,"Cli*, const char*, CliCommandFlag, CliExecuteCallback, void*, size_t" +Function,+,cli_ansi_parser_alloc,CliAnsiParser*, +Function,+,cli_ansi_parser_feed,CliAnsiParserResult,"CliAnsiParser*, char" +Function,+,cli_ansi_parser_feed_timeout,CliAnsiParserResult,CliAnsiParser* +Function,+,cli_ansi_parser_free,void,CliAnsiParser* Function,+,cli_delete_command,void,"Cli*, const char*" -Function,+,cli_getc,char,Cli* -Function,+,cli_is_connected,_Bool,Cli* -Function,+,cli_nl,void,Cli* +Function,+,cli_is_pipe_broken_or_is_etx_next_char,_Bool,PipeSide* Function,+,cli_print_usage,void,"const char*, const char*, const char*" -Function,+,cli_read,size_t,"Cli*, uint8_t*, size_t" -Function,+,cli_read_timeout,size_t,"Cli*, uint8_t*, size_t, uint32_t" -Function,+,cli_session_close,void,Cli* -Function,+,cli_session_open,void,"Cli*, const void*" -Function,+,cli_write,void,"Cli*, const uint8_t*, size_t" +Function,+,cli_vcp_disable,void,CliVcp* +Function,+,cli_vcp_enable,void,CliVcp* Function,+,composite_api_resolver_add,void,"CompositeApiResolver*, const ElfApiInterface*" Function,+,composite_api_resolver_alloc,CompositeApiResolver*, Function,+,composite_api_resolver_free,void,CompositeApiResolver* @@ -1138,6 +1138,7 @@ Function,+,furi_event_loop_subscribe_message_queue,void,"FuriEventLoop*, FuriMes Function,+,furi_event_loop_subscribe_mutex,void,"FuriEventLoop*, FuriMutex*, FuriEventLoopEvent, FuriEventLoopEventCallback, void*" Function,+,furi_event_loop_subscribe_semaphore,void,"FuriEventLoop*, FuriSemaphore*, FuriEventLoopEvent, FuriEventLoopEventCallback, void*" Function,+,furi_event_loop_subscribe_stream_buffer,void,"FuriEventLoop*, FuriStreamBuffer*, FuriEventLoopEvent, FuriEventLoopEventCallback, void*" +Function,+,furi_event_loop_subscribe_thread_flags,void,"FuriEventLoop*, FuriEventLoopThreadFlagsCallback, void*" Function,+,furi_event_loop_tick_set,void,"FuriEventLoop*, uint32_t, FuriEventLoopTickCallback, void*" Function,+,furi_event_loop_timer_alloc,FuriEventLoopTimer*,"FuriEventLoop*, FuriEventLoopTimerCallback, FuriEventLoopTimerType, void*" Function,+,furi_event_loop_timer_free,void,FuriEventLoopTimer* @@ -1148,6 +1149,7 @@ Function,+,furi_event_loop_timer_restart,void,FuriEventLoopTimer* Function,+,furi_event_loop_timer_start,void,"FuriEventLoopTimer*, uint32_t" Function,+,furi_event_loop_timer_stop,void,FuriEventLoopTimer* Function,+,furi_event_loop_unsubscribe,void,"FuriEventLoop*, FuriEventLoopObject*" +Function,+,furi_event_loop_unsubscribe_thread_flags,void,FuriEventLoop* Function,+,furi_get_tick,uint32_t, Function,+,furi_hal_adc_acquire,FuriHalAdcHandle*, Function,+,furi_hal_adc_configure,void,FuriHalAdcHandle* @@ -2329,14 +2331,14 @@ Function,+,pipe_bytes_available,size_t,PipeSide* Function,+,pipe_detach_from_event_loop,void,PipeSide* Function,+,pipe_free,void,PipeSide* Function,+,pipe_install_as_stdio,void,PipeSide* -Function,+,pipe_receive,size_t,"PipeSide*, void*, size_t, FuriWait" +Function,+,pipe_receive,size_t,"PipeSide*, void*, size_t" Function,+,pipe_role,PipeRole,PipeSide* -Function,+,pipe_send,size_t,"PipeSide*, const void*, size_t, FuriWait" +Function,+,pipe_send,size_t,"PipeSide*, const void*, size_t" Function,+,pipe_set_broken_callback,void,"PipeSide*, PipeSideBrokenCallback, FuriEventLoopEvent" Function,+,pipe_set_callback_context,void,"PipeSide*, void*" Function,+,pipe_set_data_arrived_callback,void,"PipeSide*, PipeSideDataArrivedCallback, FuriEventLoopEvent" Function,+,pipe_set_space_freed_callback,void,"PipeSide*, PipeSideSpaceFreedCallback, FuriEventLoopEvent" -Function,+,pipe_set_stdout_timeout,void,"PipeSide*, FuriWait" +Function,+,pipe_set_state_check_period,void,"PipeSide*, FuriWait" Function,+,pipe_spaces_available,size_t,PipeSide* Function,+,pipe_state,PipeState,PipeSide* Function,+,plugin_manager_alloc,PluginManager*,"const char*, uint32_t, const ElfApiInterface*" @@ -2939,7 +2941,6 @@ Variable,-,_sys_errlist,const char* const[], Variable,-,_sys_nerr,int, Variable,-,ble_profile_hid,const FuriHalBleProfileTemplate*, Variable,+,ble_profile_serial,const FuriHalBleProfileTemplate* const, -Variable,+,cli_vcp,const CliSession, Variable,+,compress_config_heatshrink_default,const CompressConfigHeatshrink, Variable,+,firmware_api_interface,const ElfApiInterface* const, Variable,+,furi_hal_i2c_bus_external,FuriHalI2cBus, diff --git a/targets/f7/api_symbols.csv b/targets/f7/api_symbols.csv index da275594b..1a8c46f10 100644 --- a/targets/f7/api_symbols.csv +++ b/targets/f7/api_symbols.csv @@ -1,9 +1,10 @@ entry,status,name,type,params -Version,+,83.0,, +Version,+,84.0,, Header,+,applications/drivers/subghz/cc1101_ext/cc1101_ext_interconnect.h,, Header,+,applications/services/bt/bt_service/bt.h,, Header,+,applications/services/bt/bt_service/bt_keys_storage.h,, Header,+,applications/services/cli/cli.h,, +Header,+,applications/services/cli/cli_ansi.h,, Header,+,applications/services/cli/cli_vcp.h,, Header,+,applications/services/dialogs/dialogs.h,, Header,+,applications/services/dolphin/dolphin.h,, @@ -855,18 +856,17 @@ Function,-,ceill,long double,long double Function,-,cfree,void,void* Function,-,clearerr,void,FILE* Function,-,clearerr_unlocked,void,FILE* -Function,+,cli_add_command,void,"Cli*, const char*, CliCommandFlag, CliCallback, void*" -Function,+,cli_cmd_interrupt_received,_Bool,Cli* +Function,+,cli_add_command,void,"Cli*, const char*, CliCommandFlag, CliExecuteCallback, void*" +Function,+,cli_add_command_ex,void,"Cli*, const char*, CliCommandFlag, CliExecuteCallback, void*, size_t" +Function,+,cli_ansi_parser_alloc,CliAnsiParser*, +Function,+,cli_ansi_parser_feed,CliAnsiParserResult,"CliAnsiParser*, char" +Function,+,cli_ansi_parser_feed_timeout,CliAnsiParserResult,CliAnsiParser* +Function,+,cli_ansi_parser_free,void,CliAnsiParser* Function,+,cli_delete_command,void,"Cli*, const char*" -Function,+,cli_getc,char,Cli* -Function,+,cli_is_connected,_Bool,Cli* -Function,+,cli_nl,void,Cli* +Function,+,cli_is_pipe_broken_or_is_etx_next_char,_Bool,PipeSide* Function,+,cli_print_usage,void,"const char*, const char*, const char*" -Function,+,cli_read,size_t,"Cli*, uint8_t*, size_t" -Function,+,cli_read_timeout,size_t,"Cli*, uint8_t*, size_t, uint32_t" -Function,+,cli_session_close,void,Cli* -Function,+,cli_session_open,void,"Cli*, const void*" -Function,+,cli_write,void,"Cli*, const uint8_t*, size_t" +Function,+,cli_vcp_disable,void,CliVcp* +Function,+,cli_vcp_enable,void,CliVcp* Function,+,composite_api_resolver_add,void,"CompositeApiResolver*, const ElfApiInterface*" Function,+,composite_api_resolver_alloc,CompositeApiResolver*, Function,+,composite_api_resolver_free,void,CompositeApiResolver* @@ -1250,6 +1250,7 @@ Function,+,furi_event_loop_subscribe_message_queue,void,"FuriEventLoop*, FuriMes Function,+,furi_event_loop_subscribe_mutex,void,"FuriEventLoop*, FuriMutex*, FuriEventLoopEvent, FuriEventLoopEventCallback, void*" Function,+,furi_event_loop_subscribe_semaphore,void,"FuriEventLoop*, FuriSemaphore*, FuriEventLoopEvent, FuriEventLoopEventCallback, void*" Function,+,furi_event_loop_subscribe_stream_buffer,void,"FuriEventLoop*, FuriStreamBuffer*, FuriEventLoopEvent, FuriEventLoopEventCallback, void*" +Function,+,furi_event_loop_subscribe_thread_flags,void,"FuriEventLoop*, FuriEventLoopThreadFlagsCallback, void*" Function,+,furi_event_loop_tick_set,void,"FuriEventLoop*, uint32_t, FuriEventLoopTickCallback, void*" Function,+,furi_event_loop_timer_alloc,FuriEventLoopTimer*,"FuriEventLoop*, FuriEventLoopTimerCallback, FuriEventLoopTimerType, void*" Function,+,furi_event_loop_timer_free,void,FuriEventLoopTimer* @@ -1260,6 +1261,7 @@ Function,+,furi_event_loop_timer_restart,void,FuriEventLoopTimer* Function,+,furi_event_loop_timer_start,void,"FuriEventLoopTimer*, uint32_t" Function,+,furi_event_loop_timer_stop,void,FuriEventLoopTimer* Function,+,furi_event_loop_unsubscribe,void,"FuriEventLoop*, FuriEventLoopObject*" +Function,+,furi_event_loop_unsubscribe_thread_flags,void,FuriEventLoop* Function,+,furi_get_tick,uint32_t, Function,+,furi_hal_adc_acquire,FuriHalAdcHandle*, Function,+,furi_hal_adc_configure,void,FuriHalAdcHandle* @@ -2967,14 +2969,14 @@ Function,+,pipe_bytes_available,size_t,PipeSide* Function,+,pipe_detach_from_event_loop,void,PipeSide* Function,+,pipe_free,void,PipeSide* Function,+,pipe_install_as_stdio,void,PipeSide* -Function,+,pipe_receive,size_t,"PipeSide*, void*, size_t, FuriWait" +Function,+,pipe_receive,size_t,"PipeSide*, void*, size_t" Function,+,pipe_role,PipeRole,PipeSide* -Function,+,pipe_send,size_t,"PipeSide*, const void*, size_t, FuriWait" +Function,+,pipe_send,size_t,"PipeSide*, const void*, size_t" Function,+,pipe_set_broken_callback,void,"PipeSide*, PipeSideBrokenCallback, FuriEventLoopEvent" Function,+,pipe_set_callback_context,void,"PipeSide*, void*" Function,+,pipe_set_data_arrived_callback,void,"PipeSide*, PipeSideDataArrivedCallback, FuriEventLoopEvent" Function,+,pipe_set_space_freed_callback,void,"PipeSide*, PipeSideSpaceFreedCallback, FuriEventLoopEvent" -Function,+,pipe_set_stdout_timeout,void,"PipeSide*, FuriWait" +Function,+,pipe_set_state_check_period,void,"PipeSide*, FuriWait" Function,+,pipe_spaces_available,size_t,PipeSide* Function,+,pipe_state,PipeState,PipeSide* Function,+,plugin_manager_alloc,PluginManager*,"const char*, uint32_t, const ElfApiInterface*" @@ -3791,7 +3793,6 @@ Variable,-,_sys_errlist,const char* const[], Variable,-,_sys_nerr,int, Variable,-,ble_profile_hid,const FuriHalBleProfileTemplate*, Variable,+,ble_profile_serial,const FuriHalBleProfileTemplate* const, -Variable,+,cli_vcp,const CliSession, Variable,+,compress_config_heatshrink_default,const CompressConfigHeatshrink, Variable,+,firmware_api_interface,const ElfApiInterface* const, Variable,+,furi_hal_i2c_bus_external,FuriHalI2cBus, diff --git a/targets/f7/furi_hal/furi_hal_usb_cdc.h b/targets/f7/furi_hal/furi_hal_usb_cdc.h index 89b68991b..50d456698 100644 --- a/targets/f7/furi_hal/furi_hal_usb_cdc.h +++ b/targets/f7/furi_hal/furi_hal_usb_cdc.h @@ -9,11 +9,21 @@ extern "C" { #endif +typedef enum { + CdcStateDisconnected, + CdcStateConnected, +} CdcState; + +typedef enum { + CdcCtrlLineDTR = (1 << 0), + CdcCtrlLineRTS = (1 << 1), +} CdcCtrlLine; + typedef struct { void (*tx_ep_callback)(void* context); void (*rx_ep_callback)(void* context); - void (*state_callback)(void* context, uint8_t state); - void (*ctrl_line_callback)(void* context, uint8_t state); + void (*state_callback)(void* context, CdcState state); + void (*ctrl_line_callback)(void* context, CdcCtrlLine ctrl_lines); void (*config_callback)(void* context, struct usb_cdc_line_coding* config); } CdcCallbacks; From 24fbfd16636ec82febfb099341b2a8f4930258e8 Mon Sep 17 00:00:00 2001 From: Anna Antonenko Date: Thu, 3 Apr 2025 17:07:47 +0400 Subject: [PATCH 184/268] [FL-3956] CLI autocomplete and other sugar (#4115) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: FuriThread stdin * ci: fix f18 * feat: stdio callback context * feat: FuriPipe * POTENTIALLY EXPLOSIVE pipe welding * fix: non-explosive welding * Revert welding * docs: furi_pipe * feat: pipe event loop integration * update f18 sdk * f18 * docs: make doxygen happy * fix: event loop not triggering when pipe attached to stdio * fix: partial stdout in pipe * allow simultaneous in and out subscription in event loop * feat: vcp i/o * feat: cli ansi stuffs and history * feat: more line editing * working but slow cli rewrite * restore previous speed after 4 days of debugging 🥲 * fix: cli_app_should_stop * fix: cli and event_loop memory leaks * style: remove commented out code * ci: fix pvs warnings * fix: unit tests, event_loop crash * ci: fix build * ci: silence pvs warning * feat: cli gpio * ci: fix formatting * Fix memory leak during event loop unsubscription * Event better memory leak fix * feat: cli completions * Merge remote-tracking branch 'origin/dev' into portasynthinca3/3928-cli-threads * merge fixups * temporarily exclude speaker_debug app * pvs and unit tests fixups * feat: commands in fals * move commands out of flash, code cleanup * ci: fix errors * fix: run commands in buffer when stopping session * speedup cli file transfer * fix f18 * separate cli_shell into modules * fix pvs warning * fix qflipper refusing to connect * remove temp debug logs * remove erroneous conclusion * Fix memory leak during event loop unsubscription * Event better memory leak fix * unit test for the fix * improve thread stdio callback signatures * pipe stdout timeout * update api symbols * fix f18, formatting * fix pvs warnings * increase stack size, hope to fix unit tests * cli completions * more key combos * cli: revert flag changes * cli: fix formatting * cli, fbt: loopback perf benchmark * thread, event_loop: subscribing to thread flags * cli: signal internal events using thread flags, improve performance * fix f18, formatting * event_loop: fix crash * storage_cli: increase write_chunk buffer size again * cli: explanation for order=0 * thread, event_loop: thread flags callback refactor * cli: increase stack size * cli: rename cli_app_should_stop -> cli_is_pipe_broken_or_is_etx_next_char * cli: use plain array instead of mlib for history * cli: prepend file name to static fns * cli: fix formatting * cli_shell: increase stack size * fix merge * fix merge * cli_shell: fix autocomplete up/down logic * cli_shell: don't add empty line to history * cli_shell: fix history recall * cli_shell: find manually typed command in history * cli_shell: different up/down completions navigation * fix formatting * cli_shell: fix memory leak * cli_shell: silence pvs warning * test_pipe: fix race condition * storage_cli: terminate on pipe broken --------- Co-authored-by: Georgii Surkov Co-authored-by: あく --- .../debug/unit_tests/tests/pipe/pipe_test.c | 9 +- applications/services/cli/application.fam | 1 + applications/services/cli/shell/cli_shell.c | 6 + .../cli/shell/cli_shell_completions.c | 362 ++++++++++++++++++ .../cli/shell/cli_shell_completions.h | 24 ++ .../services/cli/shell/cli_shell_line.c | 145 ++++++- .../services/cli/shell/cli_shell_line.h | 4 + applications/services/storage/storage_cli.c | 6 +- 8 files changed, 540 insertions(+), 17 deletions(-) create mode 100644 applications/services/cli/shell/cli_shell_completions.c create mode 100644 applications/services/cli/shell/cli_shell_completions.h diff --git a/applications/debug/unit_tests/tests/pipe/pipe_test.c b/applications/debug/unit_tests/tests/pipe/pipe_test.c index 4eae39636..f0227b353 100644 --- a/applications/debug/unit_tests/tests/pipe/pipe_test.c +++ b/applications/debug/unit_tests/tests/pipe/pipe_test.c @@ -70,10 +70,9 @@ static void on_data_arrived(PipeSide* pipe, void* context) { } static void on_space_freed(PipeSide* pipe, void* context) { + UNUSED(pipe); AncillaryThreadContext* ctx = context; ctx->flag |= TestFlagSpaceFreed; - const char* message = "Hi!"; - pipe_send(pipe, message, strlen(message)); } static void on_became_broken(PipeSide* pipe, void* context) { @@ -119,16 +118,10 @@ MU_TEST(pipe_test_event_loop) { size_t size = pipe_receive(alice, buffer_1, strlen(message)); buffer_1[size] = 0; - char buffer_2[16]; - const char* expected_reply = "Hi!"; - size = pipe_receive(alice, buffer_2, strlen(expected_reply)); - buffer_2[size] = 0; - pipe_free(alice); furi_thread_join(thread); mu_assert_string_eq(message, buffer_1); - mu_assert_string_eq(expected_reply, buffer_2); mu_assert_int_eq( TestFlagDataArrived | TestFlagSpaceFreed | TestFlagBecameBroken, furi_thread_get_return_code(thread)); diff --git a/applications/services/cli/application.fam b/applications/services/cli/application.fam index 60af336e5..9c00f442b 100644 --- a/applications/services/cli/application.fam +++ b/applications/services/cli/application.fam @@ -6,6 +6,7 @@ App( sources=[ "cli.c", "shell/cli_shell.c", + "shell/cli_shell_completions.c", "shell/cli_shell_line.c", "cli_commands.c", "cli_command_gpio.c", diff --git a/applications/services/cli/shell/cli_shell.c b/applications/services/cli/shell/cli_shell.c index 62cbbd403..2e95c767b 100644 --- a/applications/services/cli/shell/cli_shell.c +++ b/applications/services/cli/shell/cli_shell.c @@ -4,6 +4,7 @@ #include "../cli_i.h" #include "../cli_commands.h" #include "cli_shell_line.h" +#include "cli_shell_completions.h" #include #include #include @@ -17,11 +18,13 @@ #define ANSI_TIMEOUT_MS 10 typedef enum { + CliShellComponentCompletions, CliShellComponentLine, CliShellComponentMAX, //pipe); cli_shell->components[CliShellComponentLine] = cli_shell_line_alloc(cli_shell); + cli_shell->components[CliShellComponentCompletions] = cli_shell_completions_alloc( + cli_shell->cli, cli_shell, cli_shell->components[CliShellComponentLine]); cli_shell->event_loop = furi_event_loop_alloc(); cli_shell->ansi_parsing_timer = furi_event_loop_timer_alloc( @@ -172,6 +177,7 @@ static CliShell* cli_shell_alloc(PipeSide* pipe) { } static void cli_shell_free(CliShell* cli_shell) { + cli_shell_completions_free(cli_shell->components[CliShellComponentCompletions]); cli_shell_line_free(cli_shell->components[CliShellComponentLine]); pipe_detach_from_event_loop(cli_shell->pipe); diff --git a/applications/services/cli/shell/cli_shell_completions.c b/applications/services/cli/shell/cli_shell_completions.c new file mode 100644 index 000000000..6edb6eaf1 --- /dev/null +++ b/applications/services/cli/shell/cli_shell_completions.c @@ -0,0 +1,362 @@ +#include "cli_shell_completions.h" + +ARRAY_DEF(CommandCompletions, FuriString*, FURI_STRING_OPLIST); // -V524 +#define M_OPL_CommandCompletions_t() ARRAY_OPLIST(CommandCompletions) + +struct CliShellCompletions { + Cli* cli; + CliShell* shell; + CliShellLine* line; + CommandCompletions_t variants; + size_t selected; + bool is_displaying; +}; + +#define COMPLETION_COLUMNS 3 +#define COMPLETION_COLUMN_WIDTH "30" +#define COMPLETION_COLUMN_WIDTH_I 30 + +/** + * @brief Update for the completions menu + */ +typedef enum { + CliShellCompletionsActionOpen, + CliShellCompletionsActionClose, + CliShellCompletionsActionUp, + CliShellCompletionsActionDown, + CliShellCompletionsActionLeft, + CliShellCompletionsActionRight, + CliShellCompletionsActionSelect, + CliShellCompletionsActionSelectNoClose, +} CliShellCompletionsAction; + +typedef enum { + CliShellCompletionSegmentTypeCommand, + CliShellCompletionSegmentTypeArguments, +} CliShellCompletionSegmentType; + +typedef struct { + CliShellCompletionSegmentType type; + size_t start; + size_t length; +} CliShellCompletionSegment; + +// ========== +// Public API +// ========== + +CliShellCompletions* cli_shell_completions_alloc(Cli* cli, CliShell* shell, CliShellLine* line) { + CliShellCompletions* completions = malloc(sizeof(CliShellCompletions)); + + completions->cli = cli; + completions->shell = shell; + completions->line = line; + CommandCompletions_init(completions->variants); + + return completions; +} + +void cli_shell_completions_free(CliShellCompletions* completions) { + CommandCompletions_clear(completions->variants); + free(completions); +} + +// ======= +// Helpers +// ======= + +CliShellCompletionSegment cli_shell_completions_segment(CliShellCompletions* completions) { + furi_assert(completions); + CliShellCompletionSegment segment; + + FuriString* input = furi_string_alloc_set(cli_shell_line_get_editing(completions->line)); + furi_string_left(input, cli_shell_line_get_line_position(completions->line)); + + // find index of first non-space character + size_t first_non_space = 0; + while(1) { + size_t ret = furi_string_search_char(input, ' ', first_non_space); + if(ret == FURI_STRING_FAILURE) break; + if(ret - first_non_space > 1) break; + first_non_space++; + } + + size_t first_space_in_command = furi_string_search_char(input, ' ', first_non_space); + + if(first_space_in_command == FURI_STRING_FAILURE) { + segment.type = CliShellCompletionSegmentTypeCommand; + segment.start = first_non_space; + segment.length = furi_string_size(input) - first_non_space; + } else { + segment.type = CliShellCompletionSegmentTypeArguments; + segment.start = 0; + segment.length = 0; + // support removed, might reimplement in the future + } + + furi_string_free(input); + return segment; +} + +void cli_shell_completions_fill_variants(CliShellCompletions* completions) { + furi_assert(completions); + CommandCompletions_reset(completions->variants); + + CliShellCompletionSegment segment = cli_shell_completions_segment(completions); + FuriString* input = furi_string_alloc_set(cli_shell_line_get_editing(completions->line)); + furi_string_right(input, segment.start); + furi_string_left(input, segment.length); + + if(segment.type == CliShellCompletionSegmentTypeCommand) { + CliCommandTree_t* commands = cli_get_commands(completions->cli); + for + M_EACH(registered_command, *commands, CliCommandTree_t) { + FuriString* command_name = *registered_command->key_ptr; + if(furi_string_start_with(command_name, input)) { + CommandCompletions_push_back(completions->variants, command_name); + } + } + + } else { + // support removed, might reimplement in the future + } + + furi_string_free(input); +} + +static size_t cli_shell_completions_rows_at_column(CliShellCompletions* completions, size_t x) { + size_t completions_size = CommandCompletions_size(completions->variants); + size_t n_full_rows = completions_size / COMPLETION_COLUMNS; + size_t n_cols_in_last_row = completions_size % COMPLETION_COLUMNS; + size_t n_rows_at_x = n_full_rows + ((x >= n_cols_in_last_row) ? 0 : 1); + return n_rows_at_x; +} + +void cli_shell_completions_render( + CliShellCompletions* completions, + CliShellCompletionsAction action) { + furi_assert(completions); + if(action == CliShellCompletionsActionOpen) furi_check(!completions->is_displaying); + if(action == CliShellCompletionsActionClose) furi_check(completions->is_displaying); + + char prompt[64]; + cli_shell_line_format_prompt(completions->line, prompt, sizeof(prompt)); + + if(action == CliShellCompletionsActionOpen) { + cli_shell_completions_fill_variants(completions); + completions->selected = 0; + + if(CommandCompletions_size(completions->variants) == 1) { + cli_shell_completions_render(completions, CliShellCompletionsActionSelectNoClose); + return; + } + + // show completions menu (full re-render) + printf("\n\r"); + size_t position = 0; + for + M_EACH(completion, completions->variants, CommandCompletions_t) { + if(position == completions->selected) printf(ANSI_INVERT); + printf("%-" COMPLETION_COLUMN_WIDTH "s", furi_string_get_cstr(*completion)); + if(position == completions->selected) printf(ANSI_RESET); + if((position % COMPLETION_COLUMNS == COMPLETION_COLUMNS - 1) && + position != CommandCompletions_size(completions->variants)) { + printf("\r\n"); + } + position++; + } + + if(!position) { + printf(ANSI_FG_RED "no completions" ANSI_RESET); + } + + size_t total_rows = (position / COMPLETION_COLUMNS) + 1; + printf( + ANSI_ERASE_DISPLAY(ANSI_ERASE_FROM_CURSOR_TO_END) ANSI_CURSOR_UP_BY("%zu") + ANSI_CURSOR_HOR_POS("%zu"), + total_rows, + strlen(prompt) + cli_shell_line_get_line_position(completions->line) + 1); + + completions->is_displaying = true; + + } else if(action == CliShellCompletionsActionClose) { + // clear completions menu + printf( + ANSI_CURSOR_HOR_POS("%zu") ANSI_ERASE_DISPLAY(ANSI_ERASE_FROM_CURSOR_TO_END) + ANSI_CURSOR_HOR_POS("%zu"), + strlen(prompt) + furi_string_size(cli_shell_line_get_selected(completions->line)) + 1, + strlen(prompt) + cli_shell_line_get_line_position(completions->line) + 1); + completions->is_displaying = false; + + } else if( + action == CliShellCompletionsActionUp || action == CliShellCompletionsActionDown || + action == CliShellCompletionsActionLeft || action == CliShellCompletionsActionRight) { + if(CommandCompletions_empty_p(completions->variants)) return; + + // move selection + size_t completions_size = CommandCompletions_size(completions->variants); + size_t old_selection = completions->selected; + int n_columns = (completions_size >= COMPLETION_COLUMNS) ? COMPLETION_COLUMNS : + completions_size; + int selection_unclamped = old_selection; + if(action == CliShellCompletionsActionLeft) { + selection_unclamped--; + } else if(action == CliShellCompletionsActionRight) { + selection_unclamped++; + } else { + int selection_x = old_selection % COMPLETION_COLUMNS; + int selection_y_unclamped = old_selection / COMPLETION_COLUMNS; + if(action == CliShellCompletionsActionUp) selection_y_unclamped--; + if(action == CliShellCompletionsActionDown) selection_y_unclamped++; + size_t selection_y = 0; + if(selection_y_unclamped < 0) { + selection_x = CLAMP_WRAPAROUND(selection_x - 1, n_columns - 1, 0); + selection_y = + cli_shell_completions_rows_at_column(completions, selection_x) - 1; // -V537 + } else if( + (size_t)selection_y_unclamped > + cli_shell_completions_rows_at_column(completions, selection_x) - 1) { + selection_x = CLAMP_WRAPAROUND(selection_x + 1, n_columns - 1, 0); + selection_y = 0; + } else { + selection_y = selection_y_unclamped; + } + selection_unclamped = (selection_y * COMPLETION_COLUMNS) + selection_x; + } + size_t new_selection = CLAMP_WRAPAROUND(selection_unclamped, (int)completions_size - 1, 0); + completions->selected = new_selection; + + if(new_selection != old_selection) { + // determine selection coordinates relative to top-left of suggestion menu + size_t old_x = (old_selection % COMPLETION_COLUMNS) * COMPLETION_COLUMN_WIDTH_I; + size_t old_y = old_selection / COMPLETION_COLUMNS; + size_t new_x = (new_selection % COMPLETION_COLUMNS) * COMPLETION_COLUMN_WIDTH_I; + size_t new_y = new_selection / COMPLETION_COLUMNS; + printf("\n\r"); + + // print old selection in normal colors + if(old_y) printf(ANSI_CURSOR_DOWN_BY("%zu"), old_y); + printf(ANSI_CURSOR_HOR_POS("%zu"), old_x + 1); + printf( + "%-" COMPLETION_COLUMN_WIDTH "s", + furi_string_get_cstr( + *CommandCompletions_cget(completions->variants, old_selection))); + if(old_y) printf(ANSI_CURSOR_UP_BY("%zu"), old_y); + printf(ANSI_CURSOR_HOR_POS("1")); + + // print new selection in inverted colors + if(new_y) printf(ANSI_CURSOR_DOWN_BY("%zu"), new_y); + printf(ANSI_CURSOR_HOR_POS("%zu"), new_x + 1); + printf( + ANSI_INVERT "%-" COMPLETION_COLUMN_WIDTH "s" ANSI_RESET, + furi_string_get_cstr( + *CommandCompletions_cget(completions->variants, new_selection))); + + // return cursor + printf(ANSI_CURSOR_UP_BY("%zu"), new_y + 1); + printf( + ANSI_CURSOR_HOR_POS("%zu"), + strlen(prompt) + furi_string_size(cli_shell_line_get_selected(completions->line)) + + 1); + } + + } else if(action == CliShellCompletionsActionSelectNoClose) { + // insert selection into prompt + CliShellCompletionSegment segment = cli_shell_completions_segment(completions); + FuriString* input = cli_shell_line_get_selected(completions->line); + FuriString* completion = + *CommandCompletions_cget(completions->variants, completions->selected); + furi_string_replace_at( + input, segment.start, segment.length, furi_string_get_cstr(completion)); + printf( + ANSI_CURSOR_HOR_POS("%zu") "%s" ANSI_ERASE_LINE(ANSI_ERASE_FROM_CURSOR_TO_END), + strlen(prompt) + 1, + furi_string_get_cstr(input)); + + int position_change = (int)furi_string_size(completion) - (int)segment.length; + cli_shell_line_set_line_position( + completions->line, + MAX(0, (int)cli_shell_line_get_line_position(completions->line) + position_change)); + + } else if(action == CliShellCompletionsActionSelect) { + cli_shell_completions_render(completions, CliShellCompletionsActionSelectNoClose); + cli_shell_completions_render(completions, CliShellCompletionsActionClose); + + } else { + furi_crash(); + } + + fflush(stdout); +} + +// ============== +// Input handlers +// ============== + +static bool hide_if_open_and_continue_handling(CliKeyCombo combo, void* context) { + UNUSED(combo); + CliShellCompletions* completions = context; + if(completions->is_displaying) + cli_shell_completions_render(completions, CliShellCompletionsActionClose); + return false; // process other home events +} + +static bool key_combo_cr(CliKeyCombo combo, void* context) { + UNUSED(combo); + CliShellCompletions* completions = context; + if(!completions->is_displaying) return false; + cli_shell_completions_render(completions, CliShellCompletionsActionSelect); + return true; +} + +static bool key_combo_up_down(CliKeyCombo combo, void* context) { + CliShellCompletions* completions = context; + if(!completions->is_displaying) return false; + cli_shell_completions_render( + completions, + (combo.key == CliKeyUp) ? CliShellCompletionsActionUp : CliShellCompletionsActionDown); + return true; +} + +static bool key_combo_left_right(CliKeyCombo combo, void* context) { + CliShellCompletions* completions = context; + if(!completions->is_displaying) return false; + cli_shell_completions_render( + completions, + (combo.key == CliKeyLeft) ? CliShellCompletionsActionLeft : + CliShellCompletionsActionRight); + return true; +} + +static bool key_combo_tab(CliKeyCombo combo, void* context) { + UNUSED(combo); + CliShellCompletions* completions = context; + cli_shell_completions_render( + completions, + completions->is_displaying ? CliShellCompletionsActionRight : + CliShellCompletionsActionOpen); + return true; +} + +static bool key_combo_esc(CliKeyCombo combo, void* context) { + UNUSED(combo); + CliShellCompletions* completions = context; + if(!completions->is_displaying) return false; + cli_shell_completions_render(completions, CliShellCompletionsActionClose); + return true; +} + +CliShellKeyComboSet cli_shell_completions_key_combo_set = { + .fallback = hide_if_open_and_continue_handling, + .count = 7, + .records = + { + {{CliModKeyNo, CliKeyCR}, key_combo_cr}, + {{CliModKeyNo, CliKeyUp}, key_combo_up_down}, + {{CliModKeyNo, CliKeyDown}, key_combo_up_down}, + {{CliModKeyNo, CliKeyLeft}, key_combo_left_right}, + {{CliModKeyNo, CliKeyRight}, key_combo_left_right}, + {{CliModKeyNo, CliKeyTab}, key_combo_tab}, + {{CliModKeyNo, CliKeyEsc}, key_combo_esc}, + }, +}; diff --git a/applications/services/cli/shell/cli_shell_completions.h b/applications/services/cli/shell/cli_shell_completions.h new file mode 100644 index 000000000..6353bde71 --- /dev/null +++ b/applications/services/cli/shell/cli_shell_completions.h @@ -0,0 +1,24 @@ +#pragma once + +#include +#include +#include "cli_shell_i.h" +#include "cli_shell_line.h" +#include "../cli.h" +#include "../cli_i.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct CliShellCompletions CliShellCompletions; + +CliShellCompletions* cli_shell_completions_alloc(Cli* cli, CliShell* shell, CliShellLine* line); + +void cli_shell_completions_free(CliShellCompletions* completions); + +extern CliShellKeyComboSet cli_shell_completions_key_combo_set; + +#ifdef __cplusplus +} +#endif diff --git a/applications/services/cli/shell/cli_shell_line.c b/applications/services/cli/shell/cli_shell_line.c index 45bc19d9d..959cd0b3b 100644 --- a/applications/services/cli/shell/cli_shell_line.c +++ b/applications/services/cli/shell/cli_shell_line.c @@ -65,6 +65,64 @@ void cli_shell_line_ensure_not_overwriting_history(CliShellLine* line) { } } +size_t cli_shell_line_get_line_position(CliShellLine* line) { + return line->line_position; +} + +void cli_shell_line_set_line_position(CliShellLine* line, size_t position) { + line->line_position = position; +} + +// ======= +// Helpers +// ======= + +typedef enum { + CliCharClassWord, + CliCharClassSpace, + CliCharClassOther, +} CliCharClass; + +typedef enum { + CliSkipDirectionLeft, + CliSkipDirectionRight, +} CliSkipDirection; + +CliCharClass cli_shell_line_char_class(char c) { + if((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '_') { + return CliCharClassWord; + } else if(c == ' ') { + return CliCharClassSpace; + } else { + return CliCharClassOther; + } +} + +size_t + cli_shell_line_skip_run(FuriString* string, size_t original_pos, CliSkipDirection direction) { + if(furi_string_size(string) == 0) return original_pos; + if(direction == CliSkipDirectionLeft && original_pos == 0) return original_pos; + if(direction == CliSkipDirectionRight && original_pos == furi_string_size(string)) + return original_pos; + + int8_t look_offset = (direction == CliSkipDirectionLeft) ? -1 : 0; + int8_t increment = (direction == CliSkipDirectionLeft) ? -1 : 1; + int32_t position = original_pos; + CliCharClass start_class = + cli_shell_line_char_class(furi_string_get_char(string, position + look_offset)); + + while(true) { + position += increment; + if(position < 0) break; + if(position >= (int32_t)furi_string_size(string)) break; + if(cli_shell_line_char_class(furi_string_get_char(string, position + look_offset)) != + start_class) + break; + } + + return MAX(0, position); +} + // ============== // Input handlers // ============== @@ -87,13 +145,32 @@ static bool cli_shell_line_input_cr(CliKeyCombo combo, void* context) { FuriString* command = cli_shell_line_get_selected(line); furi_string_trim(command); + if(furi_string_empty(command)) { + cli_shell_line_prompt(line); + return true; + } + FuriString* command_copy = furi_string_alloc_set(command); + if(line->history_position == 0) { + for(size_t i = 1; i < line->history_entries; i++) { + if(furi_string_cmp(line->history[i], command) == 0) { + line->history_position = i; + command = cli_shell_line_get_selected(line); + furi_string_trim(command); + break; + } + } + } + + // move selected command to the front if(line->history_position > 0) { - // move selected command to the front + size_t pos = line->history_position; + size_t len = line->history_entries; memmove( - &line->history[1], &line->history[0], line->history_position * sizeof(FuriString*)); - line->history[0] = command; + &line->history[pos], &line->history[pos + 1], (len - pos - 1) * sizeof(FuriString*)); + furi_string_move(line->history[0], command); + line->history_entries--; } // insert empty command @@ -109,7 +186,7 @@ static bool cli_shell_line_input_cr(CliKeyCombo combo, void* context) { // execute command printf("\r\n"); - if(!furi_string_empty(command_copy)) cli_shell_execute_command(line->shell, command_copy); + cli_shell_execute_command(line->shell, command_copy); furi_string_free(command_copy); cli_shell_line_prompt(line); @@ -199,7 +276,57 @@ static bool cli_shell_line_input_bksp(CliKeyCombo combo, void* context) { return true; } -static bool cli_shell_line_input_fallback(CliKeyCombo combo, void* context) { +static bool cli_shell_line_input_ctrl_l(CliKeyCombo combo, void* context) { + UNUSED(combo); + CliShellLine* line = context; + // clear screen + FuriString* command = cli_shell_line_get_selected(line); + char prompt[64]; + cli_shell_line_format_prompt(line, prompt, sizeof(prompt)); + printf( + ANSI_ERASE_DISPLAY(ANSI_ERASE_ENTIRE) ANSI_ERASE_SCROLLBACK_BUFFER ANSI_CURSOR_POS( + "1", "1") "%s%s" ANSI_CURSOR_HOR_POS("%zu"), + prompt, + furi_string_get_cstr(command), + strlen(prompt) + line->line_position + 1 /* 1-based column indexing */); + fflush(stdout); + return true; +} + +static bool cli_shell_line_input_ctrl_left_right(CliKeyCombo combo, void* context) { + CliShellLine* line = context; + // skip run of similar chars to the left or right + FuriString* selected_line = cli_shell_line_get_selected(line); + CliSkipDirection direction = (combo.key == CliKeyLeft) ? CliSkipDirectionLeft : + CliSkipDirectionRight; + line->line_position = cli_shell_line_skip_run(selected_line, line->line_position, direction); + printf( + ANSI_CURSOR_HOR_POS("%zu"), cli_shell_line_prompt_length(line) + line->line_position + 1); + fflush(stdout); + return true; +} + +static bool cli_shell_line_input_ctrl_bksp(CliKeyCombo combo, void* context) { + UNUSED(combo); + CliShellLine* line = context; + // delete run of similar chars to the left + cli_shell_line_ensure_not_overwriting_history(line); + FuriString* selected_line = cli_shell_line_get_selected(line); + size_t run_start = + cli_shell_line_skip_run(selected_line, line->line_position, CliSkipDirectionLeft); + furi_string_replace_at(selected_line, run_start, line->line_position - run_start, ""); + line->line_position = run_start; + printf( + ANSI_CURSOR_HOR_POS("%zu") "%s" ANSI_ERASE_LINE(ANSI_ERASE_FROM_CURSOR_TO_END) + ANSI_CURSOR_HOR_POS("%zu"), + cli_shell_line_prompt_length(line) + line->line_position + 1, + furi_string_get_cstr(selected_line) + run_start, + cli_shell_line_prompt_length(line) + run_start + 1); + fflush(stdout); + return true; +} + +static bool cli_shell_line_input_normal(CliKeyCombo combo, void* context) { CliShellLine* line = context; if(combo.modifiers != CliModKeyNo) return false; if(combo.key < CliKeySpace || combo.key >= CliKeyDEL) return false; @@ -220,8 +347,8 @@ static bool cli_shell_line_input_fallback(CliKeyCombo combo, void* context) { } CliShellKeyComboSet cli_shell_line_key_combo_set = { - .fallback = cli_shell_line_input_fallback, - .count = 10, + .fallback = cli_shell_line_input_normal, + .count = 14, .records = { {{CliModKeyNo, CliKeyETX}, cli_shell_line_input_ctrl_c}, @@ -234,5 +361,9 @@ CliShellKeyComboSet cli_shell_line_key_combo_set = { {{CliModKeyNo, CliKeyEnd}, cli_shell_line_input_end}, {{CliModKeyNo, CliKeyBackspace}, cli_shell_line_input_bksp}, {{CliModKeyNo, CliKeyDEL}, cli_shell_line_input_bksp}, + {{CliModKeyNo, CliKeyFF}, cli_shell_line_input_ctrl_l}, + {{CliModKeyCtrl, CliKeyLeft}, cli_shell_line_input_ctrl_left_right}, + {{CliModKeyCtrl, CliKeyRight}, cli_shell_line_input_ctrl_left_right}, + {{CliModKeyNo, CliKeyETB}, cli_shell_line_input_ctrl_bksp}, }, }; diff --git a/applications/services/cli/shell/cli_shell_line.h b/applications/services/cli/shell/cli_shell_line.h index c1c810ee4..1e4b9e32a 100644 --- a/applications/services/cli/shell/cli_shell_line.h +++ b/applications/services/cli/shell/cli_shell_line.h @@ -24,6 +24,10 @@ void cli_shell_line_format_prompt(CliShellLine* line, char* buf, size_t length); void cli_shell_line_prompt(CliShellLine* line); +size_t cli_shell_line_get_line_position(CliShellLine* line); + +void cli_shell_line_set_line_position(CliShellLine* line, size_t position); + /** * @brief If a line from history has been selected, moves it into the active line */ diff --git a/applications/services/storage/storage_cli.c b/applications/services/storage/storage_cli.c index 416ecce0e..903aa1644 100644 --- a/applications/services/storage/storage_cli.c +++ b/applications/services/storage/storage_cli.c @@ -321,9 +321,11 @@ static void storage_cli_write_chunk(PipeSide* pipe, FuriString* path, FuriString uint8_t* buffer = malloc(buffer_size); while(need_to_read) { - size_t read_this_time = pipe_receive(pipe, buffer, MIN(buffer_size, need_to_read)); - size_t wrote_this_time = storage_file_write(file, buffer, read_this_time); + size_t to_read_this_time = MIN(buffer_size, need_to_read); + size_t read_this_time = pipe_receive(pipe, buffer, to_read_this_time); + if(read_this_time != to_read_this_time) break; + size_t wrote_this_time = storage_file_write(file, buffer, read_this_time); if(wrote_this_time != read_this_time) { storage_cli_print_error(storage_file_get_error(file)); break; From fa09a18483f15709ecb37c3ec003943e70127c71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francesco=20Pomp=C3=B2?= Date: Thu, 3 Apr 2025 15:14:47 +0200 Subject: [PATCH 185/268] Added Vivax and Sansui under Elitelux section (#4173) Apparently both the mentioned Elitelux and Vivax models are the same, so they share the same codes. Sansui is a brand that is also associated with them as one of the available Sansui codes is the same as this one. This code might also work for the non-smart version of the Vivax TV, the TV-32LE114T2S2, but it has not been tested. Co-authored-by: hedger --- applications/main/infrared/resources/infrared/assets/tv.ir | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/applications/main/infrared/resources/infrared/assets/tv.ir b/applications/main/infrared/resources/infrared/assets/tv.ir index 6311f500c..bff8adfa0 100644 --- a/applications/main/infrared/resources/infrared/assets/tv.ir +++ b/applications/main/infrared/resources/infrared/assets/tv.ir @@ -2474,7 +2474,7 @@ protocol: RC5 address: 01 00 00 00 command: 14 00 00 00 # -# Model: Elitelux L32HD1000 +# Model: Elitelux L32HD1000 / Vivax TV-32LE114T2S2SM / Sansui # name: Power type: parsed From 5dcf6b55ef07482189c724cfccd19628968fdef5 Mon Sep 17 00:00:00 2001 From: Anna Antonenko Date: Thu, 3 Apr 2025 21:39:53 +0400 Subject: [PATCH 186/268] [FL-3928, FL-3929] CLI commands in fals and threads (#4116) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: FuriThread stdin * ci: fix f18 * feat: stdio callback context * feat: FuriPipe * POTENTIALLY EXPLOSIVE pipe welding * fix: non-explosive welding * Revert welding * docs: furi_pipe * feat: pipe event loop integration * update f18 sdk * f18 * docs: make doxygen happy * fix: event loop not triggering when pipe attached to stdio * fix: partial stdout in pipe * allow simultaneous in and out subscription in event loop * feat: vcp i/o * feat: cli ansi stuffs and history * feat: more line editing * working but slow cli rewrite * restore previous speed after 4 days of debugging 🥲 * fix: cli_app_should_stop * fix: cli and event_loop memory leaks * style: remove commented out code * ci: fix pvs warnings * fix: unit tests, event_loop crash * ci: fix build * ci: silence pvs warning * feat: cli gpio * ci: fix formatting * Fix memory leak during event loop unsubscription * Event better memory leak fix * feat: cli completions * Merge remote-tracking branch 'origin/dev' into portasynthinca3/3928-cli-threads * merge fixups * temporarily exclude speaker_debug app * pvs and unit tests fixups * feat: commands in fals * move commands out of flash, code cleanup * ci: fix errors * fix: run commands in buffer when stopping session * speedup cli file transfer * fix f18 * separate cli_shell into modules * fix pvs warning * fix qflipper refusing to connect * remove temp debug logs * remove erroneous conclusion * Fix memory leak during event loop unsubscription * Event better memory leak fix * unit test for the fix * improve thread stdio callback signatures * pipe stdout timeout * update api symbols * fix f18, formatting * fix pvs warnings * increase stack size, hope to fix unit tests * cli completions * more key combos * commands in fals * move commands out of flash * ci: fix errors * speedup cli file transfer * merge fixups * fix f18 * cli: revert flag changes * cli: fix formatting * cli, fbt: loopback perf benchmark * thread, event_loop: subscribing to thread flags * cli: signal internal events using thread flags, improve performance * fix f18, formatting * event_loop: fix crash * storage_cli: increase write_chunk buffer size again * cli: explanation for order=0 * thread, event_loop: thread flags callback refactor * cli: increase stack size * cli: rename cli_app_should_stop -> cli_is_pipe_broken_or_is_etx_next_char * cli: use plain array instead of mlib for history * cli: prepend file name to static fns * cli: fix formatting * cli_shell: increase stack size * cli_shell: give up pipe to command thread * fix formatting * fix: format * fix merge * fix. merge. * cli_shell: fix detach ordering * desktop: record_cli -> record_cli_vcp * cli: fix spelling, reload/remove ext cmds on card mount/unmount * cli: fix race conditions and formatting * scripts: wait for CTS to go high before starting flipper * scripts: better race condition fix * REVERT THIS: test script race condition fix * Revert "REVERT THIS: test script race condition fix" This reverts commit 3b028d29b07212755872c5706c8c6a58be551636. * REVERT THIS: test script fix * scripts: sleep? * cli: updated oplist for CliCommandTree * Revert "REVERT THIS: test script fix" This reverts commit e9846318549ce092ef422ff97522ba51916163be. * cli: mention memory leak in FL ticket --------- Co-authored-by: Georgii Surkov Co-authored-by: あく Co-authored-by: hedger --- applications/debug/unit_tests/application.fam | 2 +- applications/main/application.fam | 6 - applications/main/ibutton/application.fam | 8 +- applications/main/ibutton/ibutton_cli.c | 13 +- applications/main/infrared/application.fam | 8 +- applications/main/infrared/infrared_cli.c | 13 +- applications/main/lfrfid/application.fam | 8 +- applications/main/lfrfid/lfrfid_cli.c | 8 +- applications/main/nfc/application.fam | 8 +- applications/main/nfc/nfc_cli.c | 12 +- applications/main/onewire/application.fam | 10 +- applications/main/onewire/onewire_cli.c | 13 +- applications/main/subghz/application.fam | 8 +- applications/main/subghz/subghz_cli.c | 14 +- applications/services/cli/application.fam | 16 ++ applications/services/cli/cli.c | 71 +++++++- applications/services/cli/cli.h | 21 +++ applications/services/cli/cli_commands.c | 14 ++ applications/services/cli/cli_i.h | 4 +- .../services/cli/commands/hello_world.c | 10 ++ applications/services/cli/commands/neofetch.c | 159 ++++++++++++++++ applications/services/cli/shell/cli_shell.c | 170 +++++++++++++++--- .../cli/shell/cli_shell_completions.c | 2 + applications/services/desktop/desktop.c | 6 +- applications/services/storage/storage_cli.c | 8 +- scripts/flipper/storage.py | 2 + targets/f18/api_symbols.csv | 4 +- targets/f7/api_symbols.csv | 18 +- 28 files changed, 507 insertions(+), 129 deletions(-) create mode 100644 applications/services/cli/commands/hello_world.c create mode 100644 applications/services/cli/commands/neofetch.c diff --git a/applications/debug/unit_tests/application.fam b/applications/debug/unit_tests/application.fam index f92d7e66f..05e834402 100644 --- a/applications/debug/unit_tests/application.fam +++ b/applications/debug/unit_tests/application.fam @@ -4,7 +4,7 @@ App( entry_point="unit_tests_on_system_start", sources=["unit_tests.c", "test_runner.c", "unit_test_api_table.cpp"], cdefines=["APP_UNIT_TESTS"], - requires=["system_settings", "subghz_start"], + requires=["system_settings", "cli_subghz"], provides=["delay_test"], resources="resources", order=100, diff --git a/applications/main/application.fam b/applications/main/application.fam index 4d3162337..9d8604206 100644 --- a/applications/main/application.fam +++ b/applications/main/application.fam @@ -22,11 +22,5 @@ App( apptype=FlipperAppType.METAPACKAGE, provides=[ "cli", - "ibutton_start", - "onewire_start", - "subghz_start", - "infrared_start", - "lfrfid_start", - "nfc_start", ], ) diff --git a/applications/main/ibutton/application.fam b/applications/main/ibutton/application.fam index 01c02ec23..84afe0f02 100644 --- a/applications/main/ibutton/application.fam +++ b/applications/main/ibutton/application.fam @@ -13,10 +13,10 @@ App( ) App( - appid="ibutton_start", - apptype=FlipperAppType.STARTUP, + appid="cli_ikey", targets=["f7"], - entry_point="ibutton_on_system_start", + apptype=FlipperAppType.PLUGIN, + entry_point="cli_ikey_ep", + requires=["cli"], sources=["ibutton_cli.c"], - order=60, ) diff --git a/applications/main/ibutton/ibutton_cli.c b/applications/main/ibutton/ibutton_cli.c index e11ace1d0..2ff0860bb 100644 --- a/applications/main/ibutton/ibutton_cli.c +++ b/applications/main/ibutton/ibutton_cli.c @@ -216,8 +216,7 @@ void ibutton_cli_emulate(PipeSide* pipe, FuriString* args) { ibutton_protocols_free(protocols); } -void ibutton_cli(PipeSide* pipe, FuriString* args, void* context) { - UNUSED(pipe); +static void execute(PipeSide* pipe, FuriString* args, void* context) { UNUSED(context); FuriString* cmd; cmd = furi_string_alloc(); @@ -241,12 +240,4 @@ void ibutton_cli(PipeSide* pipe, FuriString* args, void* context) { furi_string_free(cmd); } -void ibutton_on_system_start(void) { -#ifdef SRV_CLI - Cli* cli = furi_record_open(RECORD_CLI); - cli_add_command(cli, "ikey", CliCommandFlagDefault, ibutton_cli, cli); - furi_record_close(RECORD_CLI); -#else - UNUSED(ibutton_cli); -#endif -} +CLI_COMMAND_INTERFACE(ikey, execute, CliCommandFlagDefault, 1024); diff --git a/applications/main/infrared/application.fam b/applications/main/infrared/application.fam index 575bebbe4..79b3fdbfa 100644 --- a/applications/main/infrared/application.fam +++ b/applications/main/infrared/application.fam @@ -15,14 +15,14 @@ App( ) App( - appid="infrared_start", - apptype=FlipperAppType.STARTUP, + appid="cli_ir", targets=["f7"], - entry_point="infrared_on_system_start", + apptype=FlipperAppType.PLUGIN, + entry_point="cli_ir_ep", + requires=["cli"], sources=[ "infrared_cli.c", "infrared_brute_force.c", "infrared_signal.c", ], - order=20, ) diff --git a/applications/main/infrared/infrared_cli.c b/applications/main/infrared/infrared_cli.c index e62da5fd2..22d916d15 100644 --- a/applications/main/infrared/infrared_cli.c +++ b/applications/main/infrared/infrared_cli.c @@ -526,7 +526,7 @@ static void infrared_cli_process_universal(PipeSide* pipe, FuriString* args) { furi_string_free(arg2); } -static void infrared_cli_start_ir(PipeSide* pipe, FuriString* args, void* context) { +static void execute(PipeSide* pipe, FuriString* args, void* context) { UNUSED(context); if(furi_hal_infrared_is_busy()) { printf("INFRARED is busy. Exiting."); @@ -553,12 +553,5 @@ static void infrared_cli_start_ir(PipeSide* pipe, FuriString* args, void* contex furi_string_free(command); } -void infrared_on_system_start(void) { -#ifdef SRV_CLI - Cli* cli = (Cli*)furi_record_open(RECORD_CLI); - cli_add_command(cli, "ir", CliCommandFlagDefault, infrared_cli_start_ir, NULL); - furi_record_close(RECORD_CLI); -#else - UNUSED(infrared_cli_start_ir); -#endif -} + +CLI_COMMAND_INTERFACE(ir, execute, CliCommandFlagDefault, 2048); diff --git a/applications/main/lfrfid/application.fam b/applications/main/lfrfid/application.fam index c067d786f..d6fca74f4 100644 --- a/applications/main/lfrfid/application.fam +++ b/applications/main/lfrfid/application.fam @@ -13,10 +13,10 @@ App( ) App( - appid="lfrfid_start", + appid="cli_rfid", targets=["f7"], - apptype=FlipperAppType.STARTUP, - entry_point="lfrfid_on_system_start", + apptype=FlipperAppType.PLUGIN, + entry_point="cli_rfid_ep", + requires=["cli"], sources=["lfrfid_cli.c"], - order=50, ) diff --git a/applications/main/lfrfid/lfrfid_cli.c b/applications/main/lfrfid/lfrfid_cli.c index fa74906c0..cefc55f65 100644 --- a/applications/main/lfrfid/lfrfid_cli.c +++ b/applications/main/lfrfid/lfrfid_cli.c @@ -536,7 +536,7 @@ static void lfrfid_cli_raw_emulate(PipeSide* pipe, FuriString* args) { furi_string_free(filepath); } -static void lfrfid_cli(PipeSide* pipe, FuriString* args, void* context) { +static void execute(PipeSide* pipe, FuriString* args, void* context) { UNUSED(context); FuriString* cmd; cmd = furi_string_alloc(); @@ -566,8 +566,4 @@ static void lfrfid_cli(PipeSide* pipe, FuriString* args, void* context) { furi_string_free(cmd); } -void lfrfid_on_system_start(void) { - Cli* cli = furi_record_open(RECORD_CLI); - cli_add_command(cli, "rfid", CliCommandFlagDefault, lfrfid_cli, NULL); - furi_record_close(RECORD_CLI); -} +CLI_COMMAND_INTERFACE(rfid, execute, CliCommandFlagDefault, 2048); diff --git a/applications/main/nfc/application.fam b/applications/main/nfc/application.fam index 29bdf390a..f645033b2 100644 --- a/applications/main/nfc/application.fam +++ b/applications/main/nfc/application.fam @@ -258,10 +258,10 @@ App( ) App( - appid="nfc_start", + appid="cli_nfc", targets=["f7"], - apptype=FlipperAppType.STARTUP, - entry_point="nfc_on_system_start", + apptype=FlipperAppType.PLUGIN, + entry_point="cli_nfc_ep", + requires=["cli"], sources=["nfc_cli.c"], - order=30, ) diff --git a/applications/main/nfc/nfc_cli.c b/applications/main/nfc/nfc_cli.c index 8a9b1fec4..fd5598fc6 100644 --- a/applications/main/nfc/nfc_cli.c +++ b/applications/main/nfc/nfc_cli.c @@ -42,7 +42,7 @@ static void nfc_cli_field(PipeSide* pipe, FuriString* args) { furi_hal_nfc_release(); } -static void nfc_cli(PipeSide* pipe, FuriString* args, void* context) { +static void execute(PipeSide* pipe, FuriString* args, void* context) { UNUSED(context); FuriString* cmd; cmd = furi_string_alloc(); @@ -65,12 +65,4 @@ static void nfc_cli(PipeSide* pipe, FuriString* args, void* context) { furi_string_free(cmd); } -void nfc_on_system_start(void) { -#ifdef SRV_CLI - Cli* cli = furi_record_open(RECORD_CLI); - cli_add_command(cli, "nfc", CliCommandFlagDefault, nfc_cli, NULL); - furi_record_close(RECORD_CLI); -#else - UNUSED(nfc_cli); -#endif -} +CLI_COMMAND_INTERFACE(nfc, execute, CliCommandFlagDefault, 1024); diff --git a/applications/main/onewire/application.fam b/applications/main/onewire/application.fam index 3d35abce9..e38bcdfef 100644 --- a/applications/main/onewire/application.fam +++ b/applications/main/onewire/application.fam @@ -1,6 +1,8 @@ App( - appid="onewire_start", - apptype=FlipperAppType.STARTUP, - entry_point="onewire_on_system_start", - order=60, + appid="cli_onewire", + targets=["f7"], + apptype=FlipperAppType.PLUGIN, + entry_point="cli_onewire_ep", + requires=["cli"], + sources=["onewire_cli.c"], ) diff --git a/applications/main/onewire/onewire_cli.c b/applications/main/onewire/onewire_cli.c index 63e3d696f..83bbc6770 100644 --- a/applications/main/onewire/onewire_cli.c +++ b/applications/main/onewire/onewire_cli.c @@ -1,6 +1,7 @@ #include #include +#include #include #include #include @@ -45,7 +46,7 @@ static void onewire_cli_search(PipeSide* pipe) { furi_record_close(RECORD_POWER); } -static void onewire_cli(PipeSide* pipe, FuriString* args, void* context) { +static void execute(PipeSide* pipe, FuriString* args, void* context) { UNUSED(context); FuriString* cmd; cmd = furi_string_alloc(); @@ -63,12 +64,4 @@ static void onewire_cli(PipeSide* pipe, FuriString* args, void* context) { furi_string_free(cmd); } -void onewire_on_system_start(void) { -#ifdef SRV_CLI - Cli* cli = furi_record_open(RECORD_CLI); - cli_add_command(cli, "onewire", CliCommandFlagDefault, onewire_cli, cli); - furi_record_close(RECORD_CLI); -#else - UNUSED(onewire_cli); -#endif -} +CLI_COMMAND_INTERFACE(onewire, execute, CliCommandFlagDefault, 1024); diff --git a/applications/main/subghz/application.fam b/applications/main/subghz/application.fam index 1abcf7f54..fe7b07b1e 100644 --- a/applications/main/subghz/application.fam +++ b/applications/main/subghz/application.fam @@ -20,10 +20,10 @@ App( ) App( - appid="subghz_start", + appid="cli_subghz", targets=["f7"], - apptype=FlipperAppType.STARTUP, - entry_point="subghz_on_system_start", + apptype=FlipperAppType.PLUGIN, + entry_point="cli_subghz_ep", + requires=["cli"], sources=["subghz_cli.c", "helpers/subghz_chat.c"], - order=40, ) diff --git a/applications/main/subghz/subghz_cli.c b/applications/main/subghz/subghz_cli.c index a07ea5a7e..3c29aeeaf 100644 --- a/applications/main/subghz/subghz_cli.c +++ b/applications/main/subghz/subghz_cli.c @@ -1116,7 +1116,7 @@ static void subghz_cli_command_chat(PipeSide* pipe, FuriString* args) { printf("\r\nExit chat\r\n"); } -static void subghz_cli_command(PipeSide* pipe, FuriString* args, void* context) { +static void execute(PipeSide* pipe, FuriString* args, void* context) { FuriString* cmd; cmd = furi_string_alloc(); @@ -1184,14 +1184,4 @@ static void subghz_cli_command(PipeSide* pipe, FuriString* args, void* context) furi_string_free(cmd); } -void subghz_on_system_start(void) { -#ifdef SRV_CLI - Cli* cli = furi_record_open(RECORD_CLI); - - cli_add_command(cli, "subghz", CliCommandFlagDefault, subghz_cli_command, NULL); - - furi_record_close(RECORD_CLI); -#else - UNUSED(subghz_cli_command); -#endif -} +CLI_COMMAND_INTERFACE(subghz, execute, CliCommandFlagDefault, 2048); diff --git a/applications/services/cli/application.fam b/applications/services/cli/application.fam index 9c00f442b..6e2e393e0 100644 --- a/applications/services/cli/application.fam +++ b/applications/services/cli/application.fam @@ -33,3 +33,19 @@ App( sdk_headers=["cli_vcp.h"], sources=["cli_vcp.c"], ) + +App( + appid="cli_hello_world", + apptype=FlipperAppType.PLUGIN, + entry_point="cli_hello_world_ep", + requires=["cli"], + sources=["commands/hello_world.c"], +) + +App( + appid="cli_neofetch", + apptype=FlipperAppType.PLUGIN, + entry_point="cli_neofetch_ep", + requires=["cli"], + sources=["commands/neofetch.c"], +) diff --git a/applications/services/cli/cli.c b/applications/services/cli/cli.c index b51715660..2bfce3a63 100644 --- a/applications/services/cli/cli.c +++ b/applications/services/cli/cli.c @@ -14,7 +14,7 @@ struct Cli { Cli* cli_alloc(void) { Cli* cli = malloc(sizeof(Cli)); CliCommandTree_init(cli->commands); - cli->mutex = furi_mutex_alloc(FuriMutexTypeNormal); + cli->mutex = furi_mutex_alloc(FuriMutexTypeRecursive); return cli; } @@ -38,6 +38,9 @@ void cli_add_command_ex( furi_check(name); furi_check(callback); + // the shell always attaches the pipe to the stdio, thus both flags can't be used at once + if(flags & CliCommandFlagUseShellThread) furi_check(!(flags & CliCommandFlagDontAttachStdio)); + FuriString* name_str; name_str = furi_string_alloc_set(name); // command cannot contain spaces @@ -86,18 +89,75 @@ bool cli_get_command(Cli* cli, FuriString* command, CliCommand* result) { return !!data; } +void cli_remove_external_commands(Cli* cli) { + furi_check(cli); + furi_check(furi_mutex_acquire(cli->mutex, FuriWaitForever) == FuriStatusOk); + + // FIXME FL-3977: memory leak somewhere within this function + + CliCommandTree_t internal_cmds; + CliCommandTree_init(internal_cmds); + for + M_EACH(item, cli->commands, CliCommandTree_t) { + if(!(item->value_ptr->flags & CliCommandFlagExternal)) + CliCommandTree_set_at(internal_cmds, *item->key_ptr, *item->value_ptr); + } + CliCommandTree_move(cli->commands, internal_cmds); + + furi_check(furi_mutex_release(cli->mutex) == FuriStatusOk); +} + +void cli_enumerate_external_commands(Cli* cli) { + furi_check(cli); + furi_check(furi_mutex_acquire(cli->mutex, FuriWaitForever) == FuriStatusOk); + FURI_LOG_D(TAG, "Enumerating external commands"); + + cli_remove_external_commands(cli); + + // iterate over files in plugin directory + Storage* storage = furi_record_open(RECORD_STORAGE); + File* plugin_dir = storage_file_alloc(storage); + + if(storage_dir_open(plugin_dir, CLI_COMMANDS_PATH)) { + char plugin_filename[64]; + FuriString* plugin_name = furi_string_alloc(); + + while(storage_dir_read(plugin_dir, NULL, plugin_filename, sizeof(plugin_filename))) { + FURI_LOG_T(TAG, "Plugin: %s", plugin_filename); + furi_string_set_str(plugin_name, plugin_filename); + furi_string_replace_all_str(plugin_name, ".fal", ""); + furi_string_replace_at(plugin_name, 0, 4, ""); // remove "cli_" in the beginning + CliCommand command = { + .context = NULL, + .execute_callback = NULL, + .flags = CliCommandFlagExternal, + }; + CliCommandTree_set_at(cli->commands, plugin_name, command); + } + + furi_string_free(plugin_name); + } + + storage_file_free(plugin_dir); + furi_record_close(RECORD_STORAGE); + + FURI_LOG_D(TAG, "Finished enumerating external commands"); + furi_check(furi_mutex_release(cli->mutex) == FuriStatusOk); +} + void cli_lock_commands(Cli* cli) { - furi_assert(cli); + furi_check(cli); furi_check(furi_mutex_acquire(cli->mutex, FuriWaitForever) == FuriStatusOk); } void cli_unlock_commands(Cli* cli) { - furi_assert(cli); - furi_mutex_release(cli->mutex); + furi_check(cli); + furi_check(furi_mutex_release(cli->mutex) == FuriStatusOk); } CliCommandTree_t* cli_get_commands(Cli* cli) { - furi_assert(cli); + furi_check(cli); + furi_check(furi_mutex_get_owner(cli->mutex) == furi_thread_get_current_id()); return &cli->commands; } @@ -119,5 +179,6 @@ void cli_print_usage(const char* cmd, const char* usage, const char* arg) { void cli_on_system_start(void) { Cli* cli = cli_alloc(); cli_commands_init(cli); + cli_enumerate_external_commands(cli); furi_record_create(RECORD_CLI, cli); } diff --git a/applications/services/cli/cli.h b/applications/services/cli/cli.h index 211e89d88..2352e1806 100644 --- a/applications/services/cli/cli.h +++ b/applications/services/cli/cli.h @@ -20,6 +20,13 @@ typedef enum { CliCommandFlagParallelSafe = (1 << 0), /**< Safe to run in parallel with other apps */ CliCommandFlagInsomniaSafe = (1 << 1), /**< Safe to run with insomnia mode on */ CliCommandFlagDontAttachStdio = (1 << 2), /**< Do no attach I/O pipe to thread stdio */ + CliCommandFlagUseShellThread = + (1 + << 3), /**< Don't start a separate thread to run the command in. Incompatible with DontAttachStdio */ + + // internal flags (do not set them yourselves!) + + CliCommandFlagExternal = (1 << 4), /**< The command comes from a .fal file */ } CliCommandFlag; /** Cli type anonymous structure */ @@ -87,6 +94,20 @@ void cli_add_command_ex( */ void cli_delete_command(Cli* cli, const char* name); +/** + * @brief Unregisters all external commands + * + * @param [in] cli pointer to the cli instance + */ +void cli_remove_external_commands(Cli* cli); + +/** + * @brief Reloads the list of externally available commands + * + * @param [in] cli pointer to cli instance + */ +void cli_enumerate_external_commands(Cli* cli); + /** * @brief Detects if Ctrl+C has been pressed or session has been terminated * diff --git a/applications/services/cli/cli_commands.c b/applications/services/cli/cli_commands.c index 24917afa9..723a4d556 100644 --- a/applications/services/cli/cli_commands.c +++ b/applications/services/cli/cli_commands.c @@ -91,6 +91,8 @@ void cli_command_help(PipeSide* pipe, FuriString* args, void* context) { } } + printf(ANSI_RESET + "\r\nIf you just added a new command and can't see it above, run `reload_ext_cmds`"); printf(ANSI_RESET "\r\nFind out more: https://docs.flipper.net/development/cli"); cli_unlock_commands(cli); @@ -512,6 +514,16 @@ void cli_command_i2c(PipeSide* pipe, FuriString* args, void* context) { furi_hal_i2c_release(&furi_hal_i2c_handle_external); } +void cli_command_reload_external(PipeSide* pipe, FuriString* args, void* context) { + UNUSED(pipe); + UNUSED(args); + UNUSED(context); + Cli* cli = furi_record_open(RECORD_CLI); + cli_enumerate_external_commands(cli); + furi_record_close(RECORD_CLI); + printf("OK!"); +} + /** * Echoes any bytes it receives except ASCII ETX (0x03, Ctrl+C) */ @@ -537,6 +549,8 @@ void cli_commands_init(Cli* cli) { cli_add_command(cli, "!", CliCommandFlagParallelSafe, cli_command_info, (void*)true); cli_add_command(cli, "info", CliCommandFlagParallelSafe, cli_command_info, NULL); cli_add_command(cli, "device_info", CliCommandFlagParallelSafe, cli_command_info, (void*)true); + cli_add_command( + cli, "reload_ext_cmds", CliCommandFlagDefault, cli_command_reload_external, NULL); cli_add_command(cli, "?", CliCommandFlagParallelSafe, cli_command_help, NULL); cli_add_command(cli, "help", CliCommandFlagParallelSafe, cli_command_help, NULL); diff --git a/applications/services/cli/cli_i.h b/applications/services/cli/cli_i.h index b990e9960..3e948c345 100644 --- a/applications/services/cli/cli_i.h +++ b/applications/services/cli/cli_i.h @@ -14,6 +14,7 @@ extern "C" { #endif #define CLI_BUILTIN_COMMAND_STACK_SIZE (3 * 1024U) +#define CLI_COMMANDS_PATH "/ext/apps_data/cli/plugins" typedef struct { void* context; // +#include +#include +#include +#include + +static void execute(PipeSide* pipe, FuriString* args, void* context) { + UNUSED(pipe); + UNUSED(args); + UNUSED(context); + + static const char* const neofetch_logo[] = { + " _.-------.._ -,", + " .-\"```\"--..,,_/ /`-, -, \\ ", + " .:\" /:/ /'\\ \\ ,_..., `. | |", + " / ,----/:/ /`\\ _\\~`_-\"` _;", + " ' / /`\"\"\"'\\ \\ \\.~`_-' ,-\"'/ ", + " | | | 0 | | .-' ,/` /", + " | ,..\\ \\ ,.-\"` ,/` /", + "; : `/`\"\"\\` ,/--==,/-----,", + "| `-...| -.___-Z:_______J...---;", + ": ` _-'", + }; +#define NEOFETCH_COLOR ANSI_FLIPPER_BRAND_ORANGE + + // Determine logo parameters + size_t logo_height = COUNT_OF(neofetch_logo), logo_width = 0; + for(size_t i = 0; i < logo_height; i++) + logo_width = MAX(logo_width, strlen(neofetch_logo[i])); + logo_width += 4; // space between logo and info + + // Format hostname delimiter + const size_t size_of_hostname = 4 + strlen(furi_hal_version_get_name_ptr()); + char delimiter[64]; + memset(delimiter, '-', size_of_hostname); + delimiter[size_of_hostname] = '\0'; + + // Get heap info + size_t heap_total = memmgr_get_total_heap(); + size_t heap_used = heap_total - memmgr_get_free_heap(); + uint16_t heap_percent = (100 * heap_used) / heap_total; + + // Get storage info + Storage* storage = furi_record_open(RECORD_STORAGE); + uint64_t ext_total, ext_free, ext_used, ext_percent; + storage_common_fs_info(storage, "/ext", &ext_total, &ext_free); + ext_used = ext_total - ext_free; + ext_percent = (100 * ext_used) / ext_total; + ext_used /= 1024 * 1024; + ext_total /= 1024 * 1024; + furi_record_close(RECORD_STORAGE); + + // Get battery info + uint16_t charge_percent = furi_hal_power_get_pct(); + const char* charge_state; + if(furi_hal_power_is_charging()) { + if((charge_percent < 100) && (!furi_hal_power_is_charging_done())) { + charge_state = "charging"; + } else { + charge_state = "charged"; + } + } else { + charge_state = "discharging"; + } + + // Get misc info + uint32_t uptime = furi_get_tick() / furi_kernel_get_tick_frequency(); + const Version* version = version_get(); + uint16_t major, minor; + furi_hal_info_get_api_version(&major, &minor); + + // Print ASCII art with info + const size_t info_height = 16; + for(size_t i = 0; i < MAX(logo_height, info_height); i++) { + printf(NEOFETCH_COLOR "%-*s", logo_width, (i < logo_height) ? neofetch_logo[i] : ""); + switch(i) { + case 0: // you@ + printf("you" ANSI_RESET "@" NEOFETCH_COLOR "%s", furi_hal_version_get_name_ptr()); + break; + case 1: // delimiter + printf(ANSI_RESET "%s", delimiter); + break; + case 2: // OS: FURI (SDK .) + printf( + "OS" ANSI_RESET ": FURI %s %s %s %s (SDK %hu.%hu)", + version_get_version(version), + version_get_gitbranch(version), + version_get_version(version), + version_get_githash(version), + major, + minor); + break; + case 3: // Host: + printf( + "Host" ANSI_RESET ": %s %s", + furi_hal_version_get_model_code(), + furi_hal_version_get_device_name_ptr()); + break; + case 4: // Kernel: FreeRTOS .. + printf( + "Kernel" ANSI_RESET ": FreeRTOS %d.%d.%d", + tskKERNEL_VERSION_MAJOR, + tskKERNEL_VERSION_MINOR, + tskKERNEL_VERSION_BUILD); + break; + case 5: // Uptime: ?h?m?s + printf( + "Uptime" ANSI_RESET ": %luh%lum%lus", + uptime / 60 / 60, + uptime / 60 % 60, + uptime % 60); + break; + case 6: // ST7567 128x64 @ 1 bpp in 1.4" + printf("Display" ANSI_RESET ": ST7567 128x64 @ 1 bpp in 1.4\""); + break; + case 7: // DE: GuiSrv + printf("DE" ANSI_RESET ": GuiSrv"); + break; + case 8: // Shell: CliSrv + printf("Shell" ANSI_RESET ": CliShell"); + break; + case 9: // CPU: STM32WB55RG @ 64 MHz + printf("CPU" ANSI_RESET ": STM32WB55RG @ 64 MHz"); + break; + case 10: // Memory: / B (??%) + printf( + "Memory" ANSI_RESET ": %zu / %zu B (%hu%%)", heap_used, heap_total, heap_percent); + break; + case 11: // Disk (/ext): / MiB (??%) + printf( + "Disk (/ext)" ANSI_RESET ": %llu / %llu MiB (%llu%%)", + ext_used, + ext_total, + ext_percent); + break; + case 12: // Battery: ??% () + printf("Battery" ANSI_RESET ": %hu%% (%s)" ANSI_RESET, charge_percent, charge_state); + break; + case 13: // empty space + break; + case 14: // Colors (line 1) + for(size_t j = 30; j <= 37; j++) + printf("\e[%dm███", j); + break; + case 15: // Colors (line 2) + for(size_t j = 90; j <= 97; j++) + printf("\e[%dm███", j); + break; + default: + break; + } + printf("\r\n"); + } + printf(ANSI_RESET); +#undef NEOFETCH_COLOR +} + +CLI_COMMAND_INTERFACE(neofetch, execute, CliCommandFlagDefault, 2048); diff --git a/applications/services/cli/shell/cli_shell.c b/applications/services/cli/shell/cli_shell.c index 2e95c767b..22a5e7e78 100644 --- a/applications/services/cli/shell/cli_shell.c +++ b/applications/services/cli/shell/cli_shell.c @@ -12,6 +12,7 @@ #include #include #include +#include #define TAG "CliShell" @@ -29,6 +30,11 @@ CliShellKeyComboSet* component_key_combo_sets[] = { }; static_assert(CliShellComponentMAX == COUNT_OF(component_key_combo_sets)); +typedef enum { + CliShellStorageEventMount, + CliShellStorageEventUnmount, +} CliShellStorageEvent; + struct CliShell { Cli* cli; FuriEventLoop* event_loop; @@ -37,6 +43,10 @@ struct CliShell { CliAnsiParser* ansi_parser; FuriEventLoopTimer* ansi_parsing_timer; + Storage* storage; + FuriPubSubSubscription* storage_subscription; + FuriMessageQueue* storage_event_queue; + void* components[CliShellComponentMAX]; }; @@ -46,10 +56,39 @@ typedef struct { FuriString* args; } CliCommandThreadData; +static void cli_shell_data_available(PipeSide* pipe, void* context); +static void cli_shell_pipe_broken(PipeSide* pipe, void* context); + +static void cli_shell_install_pipe(CliShell* cli_shell) { + pipe_install_as_stdio(cli_shell->pipe); + pipe_attach_to_event_loop(cli_shell->pipe, cli_shell->event_loop); + pipe_set_callback_context(cli_shell->pipe, cli_shell); + pipe_set_data_arrived_callback(cli_shell->pipe, cli_shell_data_available, 0); + pipe_set_broken_callback(cli_shell->pipe, cli_shell_pipe_broken, 0); +} + +static void cli_shell_detach_pipe(CliShell* cli_shell) { + pipe_detach_from_event_loop(cli_shell->pipe); + furi_thread_set_stdin_callback(NULL, NULL); + furi_thread_set_stdout_callback(NULL, NULL); +} + // ========= // Execution // ========= +static int32_t cli_command_thread(void* context) { + CliCommandThreadData* thread_data = context; + if(!(thread_data->command->flags & CliCommandFlagDontAttachStdio)) + pipe_install_as_stdio(thread_data->pipe); + + thread_data->command->execute_callback( + thread_data->pipe, thread_data->args, thread_data->command->context); + + fflush(stdout); + return 0; +} + void cli_shell_execute_command(CliShell* cli_shell, FuriString* command) { // split command into command and args size_t space = furi_string_search_char(command, ' '); @@ -59,6 +98,7 @@ void cli_shell_execute_command(CliShell* cli_shell, FuriString* command) { FuriString* args = furi_string_alloc_set(command); furi_string_right(args, space + 1); + PluginManager* plugin_manager = NULL; Loader* loader = NULL; CliCommand command_data; @@ -71,6 +111,34 @@ void cli_shell_execute_command(CliShell* cli_shell, FuriString* command) { break; } + // load external command + if(command_data.flags & CliCommandFlagExternal) { + plugin_manager = + plugin_manager_alloc(PLUGIN_APP_ID, PLUGIN_API_VERSION, firmware_api_interface); + FuriString* path = furi_string_alloc_printf( + "%s/cli_%s.fal", CLI_COMMANDS_PATH, furi_string_get_cstr(command_name)); + uint32_t plugin_cnt_last = plugin_manager_get_count(plugin_manager); + PluginManagerError error = + plugin_manager_load_single(plugin_manager, furi_string_get_cstr(path)); + furi_string_free(path); + + if(error != PluginManagerErrorNone) { + printf(ANSI_FG_RED "failed to load external command" ANSI_RESET); + break; + } + + const CliCommandDescriptor* plugin = + plugin_manager_get_ep(plugin_manager, plugin_cnt_last); + furi_assert(plugin); + furi_check(furi_string_cmp_str(command_name, plugin->name) == 0); + command_data.execute_callback = plugin->execute_callback; + command_data.flags = plugin->flags | CliCommandFlagExternal; + command_data.stack_depth = plugin->stack_depth; + + // external commands have to run in an external thread + furi_check(!(command_data.flags & CliCommandFlagUseShellThread)); + } + // lock loader if(!(command_data.flags & CliCommandFlagParallelSafe)) { loader = furi_record_open(RECORD_LOADER); @@ -82,7 +150,27 @@ void cli_shell_execute_command(CliShell* cli_shell, FuriString* command) { } } - command_data.execute_callback(cli_shell->pipe, args, command_data.context); + if(command_data.flags & CliCommandFlagUseShellThread) { + // run command in this thread + command_data.execute_callback(cli_shell->pipe, args, command_data.context); + } else { + // run command in separate thread + cli_shell_detach_pipe(cli_shell); + CliCommandThreadData thread_data = { + .command = &command_data, + .pipe = cli_shell->pipe, + .args = args, + }; + FuriThread* thread = furi_thread_alloc_ex( + furi_string_get_cstr(command_name), + command_data.stack_depth, + cli_command_thread, + &thread_data); + furi_thread_start(thread); + furi_thread_join(thread); + furi_thread_free(thread); + cli_shell_install_pipe(cli_shell); + } } while(0); furi_string_free(command_name); @@ -91,13 +179,51 @@ void cli_shell_execute_command(CliShell* cli_shell, FuriString* command) { // unlock loader if(loader) loader_unlock(loader); furi_record_close(RECORD_LOADER); + + // unload external command + if(plugin_manager) plugin_manager_free(plugin_manager); } // ============== // Event handlers // ============== -static void cli_shell_process_key(CliShell* cli_shell, CliKeyCombo key_combo) { +static void cli_shell_storage_event(const void* message, void* context) { + CliShell* cli_shell = context; + const StorageEvent* event = message; + + if(event->type == StorageEventTypeCardMount) { + CliShellStorageEvent cli_event = CliShellStorageEventMount; + furi_check( + furi_message_queue_put(cli_shell->storage_event_queue, &cli_event, 0) == FuriStatusOk); + } else if(event->type == StorageEventTypeCardUnmount) { + CliShellStorageEvent cli_event = CliShellStorageEventUnmount; + furi_check( + furi_message_queue_put(cli_shell->storage_event_queue, &cli_event, 0) == FuriStatusOk); + } +} + +static void cli_shell_storage_internal_event(FuriEventLoopObject* object, void* context) { + CliShell* cli_shell = context; + FuriMessageQueue* queue = object; + CliShellStorageEvent event; + furi_check(furi_message_queue_get(queue, &event, 0) == FuriStatusOk); + + if(event == CliShellStorageEventMount) { + cli_enumerate_external_commands(cli_shell->cli); + } else if(event == CliShellStorageEventUnmount) { + cli_remove_external_commands(cli_shell->cli); + } else { + furi_crash(); + } +} + +static void + cli_shell_process_parser_result(CliShell* cli_shell, CliAnsiParserResult parse_result) { + if(!parse_result.is_done) return; + CliKeyCombo key_combo = parse_result.result; + if(key_combo.key == CliKeyUnrecognized) return; + for(size_t i = 0; i < CliShellComponentMAX; i++) { // -V1008 CliShellKeyComboSet* set = component_key_combo_sets[i]; void* component_context = cli_shell->components[i]; @@ -130,22 +256,13 @@ static void cli_shell_data_available(PipeSide* pipe, void* context) { // process ANSI escape sequences int c = getchar(); furi_assert(c >= 0); - CliAnsiParserResult parse_result = cli_ansi_parser_feed(cli_shell->ansi_parser, c); - if(!parse_result.is_done) return; - CliKeyCombo key_combo = parse_result.result; - if(key_combo.key == CliKeyUnrecognized) return; - - cli_shell_process_key(cli_shell, key_combo); + cli_shell_process_parser_result(cli_shell, cli_ansi_parser_feed(cli_shell->ansi_parser, c)); } static void cli_shell_timer_expired(void* context) { CliShell* cli_shell = context; - CliAnsiParserResult parse_result = cli_ansi_parser_feed_timeout(cli_shell->ansi_parser); - if(!parse_result.is_done) return; - CliKeyCombo key_combo = parse_result.result; - if(key_combo.key == CliKeyUnrecognized) return; - - cli_shell_process_key(cli_shell, key_combo); + cli_shell_process_parser_result( + cli_shell, cli_ansi_parser_feed_timeout(cli_shell->ansi_parser)); } // ======= @@ -158,7 +275,6 @@ static CliShell* cli_shell_alloc(PipeSide* pipe) { cli_shell->cli = furi_record_open(RECORD_CLI); cli_shell->ansi_parser = cli_ansi_parser_alloc(); cli_shell->pipe = pipe; - pipe_install_as_stdio(cli_shell->pipe); cli_shell->components[CliShellComponentLine] = cli_shell_line_alloc(cli_shell); cli_shell->components[CliShellComponentCompletions] = cli_shell_completions_alloc( @@ -167,20 +283,34 @@ static CliShell* cli_shell_alloc(PipeSide* pipe) { cli_shell->event_loop = furi_event_loop_alloc(); cli_shell->ansi_parsing_timer = furi_event_loop_timer_alloc( cli_shell->event_loop, cli_shell_timer_expired, FuriEventLoopTimerTypeOnce, cli_shell); - pipe_attach_to_event_loop(cli_shell->pipe, cli_shell->event_loop); - pipe_set_callback_context(cli_shell->pipe, cli_shell); - pipe_set_data_arrived_callback(cli_shell->pipe, cli_shell_data_available, 0); - pipe_set_broken_callback(cli_shell->pipe, cli_shell_pipe_broken, 0); + cli_shell_install_pipe(cli_shell); + + cli_shell->storage_event_queue = furi_message_queue_alloc(1, sizeof(CliShellStorageEvent)); + furi_event_loop_subscribe_message_queue( + cli_shell->event_loop, + cli_shell->storage_event_queue, + FuriEventLoopEventIn, + cli_shell_storage_internal_event, + cli_shell); + cli_shell->storage = furi_record_open(RECORD_STORAGE); + cli_shell->storage_subscription = furi_pubsub_subscribe( + storage_get_pubsub(cli_shell->storage), cli_shell_storage_event, cli_shell); return cli_shell; } static void cli_shell_free(CliShell* cli_shell) { + furi_pubsub_unsubscribe( + storage_get_pubsub(cli_shell->storage), cli_shell->storage_subscription); + furi_record_close(RECORD_STORAGE); + furi_event_loop_unsubscribe(cli_shell->event_loop, cli_shell->storage_event_queue); + furi_message_queue_free(cli_shell->storage_event_queue); + cli_shell_completions_free(cli_shell->components[CliShellComponentCompletions]); cli_shell_line_free(cli_shell->components[CliShellComponentLine]); - pipe_detach_from_event_loop(cli_shell->pipe); + cli_shell_detach_pipe(cli_shell); furi_event_loop_timer_free(cli_shell->ansi_parsing_timer); furi_event_loop_free(cli_shell->event_loop); pipe_free(cli_shell->pipe); diff --git a/applications/services/cli/shell/cli_shell_completions.c b/applications/services/cli/shell/cli_shell_completions.c index 6edb6eaf1..0b32c18a2 100644 --- a/applications/services/cli/shell/cli_shell_completions.c +++ b/applications/services/cli/shell/cli_shell_completions.c @@ -108,6 +108,7 @@ void cli_shell_completions_fill_variants(CliShellCompletions* completions) { furi_string_left(input, segment.length); if(segment.type == CliShellCompletionSegmentTypeCommand) { + cli_lock_commands(completions->cli); CliCommandTree_t* commands = cli_get_commands(completions->cli); for M_EACH(registered_command, *commands, CliCommandTree_t) { @@ -116,6 +117,7 @@ void cli_shell_completions_fill_variants(CliShellCompletions* completions) { CommandCompletions_push_back(completions->variants, command_name); } } + cli_unlock_commands(completions->cli); } else { // support removed, might reimplement in the future diff --git a/applications/services/desktop/desktop.c b/applications/services/desktop/desktop.c index 185fb9c3b..0f6304823 100644 --- a/applications/services/desktop/desktop.c +++ b/applications/services/desktop/desktop.c @@ -398,7 +398,7 @@ void desktop_lock(Desktop* desktop) { if(desktop_pin_code_is_set()) { CliVcp* cli_vcp = furi_record_open(RECORD_CLI_VCP); cli_vcp_disable(cli_vcp); - furi_record_close(RECORD_CLI); + furi_record_close(RECORD_CLI_VCP); } desktop_auto_lock_inhibit(desktop); @@ -428,7 +428,7 @@ void desktop_unlock(Desktop* desktop) { if(desktop_pin_code_is_set()) { CliVcp* cli_vcp = furi_record_open(RECORD_CLI_VCP); cli_vcp_enable(cli_vcp); - furi_record_close(RECORD_CLI); + furi_record_close(RECORD_CLI_VCP); } DesktopStatus status = {.locked = false}; @@ -528,7 +528,7 @@ int32_t desktop_srv(void* p) { } else { CliVcp* cli_vcp = furi_record_open(RECORD_CLI_VCP); cli_vcp_enable(cli_vcp); - furi_record_close(RECORD_CLI); + furi_record_close(RECORD_CLI_VCP); } if(storage_file_exists(desktop->storage, SLIDESHOW_FS_PATH)) { diff --git a/applications/services/storage/storage_cli.c b/applications/services/storage/storage_cli.c index 903aa1644..58b851926 100644 --- a/applications/services/storage/storage_cli.c +++ b/applications/services/storage/storage_cli.c @@ -696,7 +696,13 @@ static void storage_cli_factory_reset(PipeSide* pipe, FuriString* args, void* co void storage_on_system_start(void) { #ifdef SRV_CLI Cli* cli = furi_record_open(RECORD_CLI); - cli_add_command_ex(cli, "storage", CliCommandFlagParallelSafe, storage_cli, NULL, 512); + cli_add_command_ex( + cli, + "storage", + CliCommandFlagParallelSafe | CliCommandFlagUseShellThread, + storage_cli, + NULL, + 512); cli_add_command( cli, "factory_reset", CliCommandFlagParallelSafe, storage_cli_factory_reset, NULL); furi_record_close(RECORD_CLI); diff --git a/scripts/flipper/storage.py b/scripts/flipper/storage.py index 40af5cebc..0182cf45f 100644 --- a/scripts/flipper/storage.py +++ b/scripts/flipper/storage.py @@ -109,6 +109,8 @@ class FlipperStorage: def start(self): self.port.open() + time.sleep(0.5) + self.read.until(self.CLI_PROMPT) self.port.reset_input_buffer() # Send a command with a known syntax to make sure the buffer is flushed self.send("device_info\r") diff --git a/targets/f18/api_symbols.csv b/targets/f18/api_symbols.csv index bfe7afbcd..72512a46f 100644 --- a/targets/f18/api_symbols.csv +++ b/targets/f18/api_symbols.csv @@ -1,5 +1,5 @@ entry,status,name,type,params -Version,+,84.0,, +Version,+,84.1,, Header,+,applications/services/bt/bt_service/bt.h,, Header,+,applications/services/bt/bt_service/bt_keys_storage.h,, Header,+,applications/services/cli/cli.h,, @@ -786,8 +786,10 @@ Function,+,cli_ansi_parser_feed,CliAnsiParserResult,"CliAnsiParser*, char" Function,+,cli_ansi_parser_feed_timeout,CliAnsiParserResult,CliAnsiParser* Function,+,cli_ansi_parser_free,void,CliAnsiParser* Function,+,cli_delete_command,void,"Cli*, const char*" +Function,+,cli_enumerate_external_commands,void,Cli* Function,+,cli_is_pipe_broken_or_is_etx_next_char,_Bool,PipeSide* Function,+,cli_print_usage,void,"const char*, const char*, const char*" +Function,+,cli_remove_external_commands,void,Cli* Function,+,cli_vcp_disable,void,CliVcp* Function,+,cli_vcp_enable,void,CliVcp* Function,+,composite_api_resolver_add,void,"CompositeApiResolver*, const ElfApiInterface*" diff --git a/targets/f7/api_symbols.csv b/targets/f7/api_symbols.csv index 1a8c46f10..73ad2dcd5 100644 --- a/targets/f7/api_symbols.csv +++ b/targets/f7/api_symbols.csv @@ -1,5 +1,5 @@ entry,status,name,type,params -Version,+,84.0,, +Version,+,84.1,, Header,+,applications/drivers/subghz/cc1101_ext/cc1101_ext_interconnect.h,, Header,+,applications/services/bt/bt_service/bt.h,, Header,+,applications/services/bt/bt_service/bt_keys_storage.h,, @@ -863,8 +863,10 @@ Function,+,cli_ansi_parser_feed,CliAnsiParserResult,"CliAnsiParser*, char" Function,+,cli_ansi_parser_feed_timeout,CliAnsiParserResult,CliAnsiParser* Function,+,cli_ansi_parser_free,void,CliAnsiParser* Function,+,cli_delete_command,void,"Cli*, const char*" +Function,+,cli_enumerate_external_commands,void,Cli* Function,+,cli_is_pipe_broken_or_is_etx_next_char,_Bool,PipeSide* Function,+,cli_print_usage,void,"const char*, const char*, const char*" +Function,+,cli_remove_external_commands,void,Cli* Function,+,cli_vcp_disable,void,CliVcp* Function,+,cli_vcp_enable,void,CliVcp* Function,+,composite_api_resolver_add,void,"CompositeApiResolver*, const ElfApiInterface*" @@ -3454,13 +3456,13 @@ Function,+,subghz_file_encoder_worker_get_level_duration,LevelDuration,void* Function,+,subghz_file_encoder_worker_is_running,_Bool,SubGhzFileEncoderWorker* Function,+,subghz_file_encoder_worker_start,_Bool,"SubGhzFileEncoderWorker*, const char*, const char*" Function,+,subghz_file_encoder_worker_stop,void,SubGhzFileEncoderWorker* -Function,-,subghz_keystore_alloc,SubGhzKeystore*, -Function,-,subghz_keystore_free,void,SubGhzKeystore* -Function,-,subghz_keystore_get_data,SubGhzKeyArray_t*,SubGhzKeystore* -Function,-,subghz_keystore_load,_Bool,"SubGhzKeystore*, const char*" -Function,-,subghz_keystore_raw_encrypted_save,_Bool,"const char*, const char*, uint8_t*" -Function,-,subghz_keystore_raw_get_data,_Bool,"const char*, size_t, uint8_t*, size_t" -Function,-,subghz_keystore_save,_Bool,"SubGhzKeystore*, const char*, uint8_t*" +Function,+,subghz_keystore_alloc,SubGhzKeystore*, +Function,+,subghz_keystore_free,void,SubGhzKeystore* +Function,+,subghz_keystore_get_data,SubGhzKeyArray_t*,SubGhzKeystore* +Function,+,subghz_keystore_load,_Bool,"SubGhzKeystore*, const char*" +Function,+,subghz_keystore_raw_encrypted_save,_Bool,"const char*, const char*, uint8_t*" +Function,+,subghz_keystore_raw_get_data,_Bool,"const char*, size_t, uint8_t*, size_t" +Function,+,subghz_keystore_save,_Bool,"SubGhzKeystore*, const char*, uint8_t*" Function,+,subghz_protocol_blocks_add_bit,void,"SubGhzBlockDecoder*, uint8_t" Function,+,subghz_protocol_blocks_add_bytes,uint8_t,"const uint8_t[], size_t" Function,+,subghz_protocol_blocks_add_to_128_bit,void,"SubGhzBlockDecoder*, uint8_t, uint64_t*" From 09c61ecbdeab8d1b1ffc46f814fb4a26302347ed Mon Sep 17 00:00:00 2001 From: hedger Date: Thu, 3 Apr 2025 20:42:40 +0100 Subject: [PATCH 187/268] cli: fixed `free_blocks` command (#4174) * cli: fixed free_blocks command - regression after new heap implementation * github: updated codeowners --- .github/CODEOWNERS | 100 ++++++++++++++++++++-------------------- furi/core/memmgr_heap.c | 8 ++-- 2 files changed, 55 insertions(+), 53 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index ef8b79370..675679080 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,69 +1,69 @@ # Who owns all the fish by default -* @skotopes @DrZlo13 @hedger @gsurkov +* @DrZlo13 @hedger @gsurkov # Apps -/applications/debug/bt_debug_app/ @skotopes @DrZlo13 @hedger @gsurkov @gornekich -/applications/debug/accessor/ @skotopes @DrZlo13 @hedger @gsurkov @nminaylov -/applications/debug/battery_test_app/ @skotopes @DrZlo13 @hedger @gsurkov @gornekich -/applications/debug/bt_debug_app/ @skotopes @DrZlo13 @hedger @gsurkov @gornekich -/applications/debug/file_browser_test/ @skotopes @DrZlo13 @hedger @gsurkov @nminaylov -/applications/debug/lfrfid_debug/ @skotopes @DrZlo13 @hedger @gsurkov @nminaylov -/applications/debug/text_box_test/ @skotopes @DrZlo13 @hedger @gsurkov @nminaylov -/applications/debug/uart_echo/ @skotopes @DrZlo13 @hedger @gsurkov @nminaylov -/applications/debug/usb_mouse/ @skotopes @DrZlo13 @hedger @gsurkov @nminaylov -/applications/debug/usb_test/ @skotopes @DrZlo13 @hedger @gsurkov @nminaylov +/applications/debug/bt_debug_app/ @DrZlo13 @hedger @gsurkov @gornekich +/applications/debug/accessor/ @DrZlo13 @hedger @gsurkov @nminaylov +/applications/debug/battery_test_app/ @DrZlo13 @hedger @gsurkov @gornekich +/applications/debug/bt_debug_app/ @DrZlo13 @hedger @gsurkov @gornekich +/applications/debug/file_browser_test/ @DrZlo13 @hedger @gsurkov @nminaylov +/applications/debug/lfrfid_debug/ @DrZlo13 @hedger @gsurkov @nminaylov +/applications/debug/text_box_test/ @DrZlo13 @hedger @gsurkov @nminaylov +/applications/debug/uart_echo/ @DrZlo13 @hedger @gsurkov @nminaylov +/applications/debug/usb_mouse/ @DrZlo13 @hedger @gsurkov @nminaylov +/applications/debug/usb_test/ @DrZlo13 @hedger @gsurkov @nminaylov -/applications/main/archive/ @skotopes @DrZlo13 @hedger @gsurkov @nminaylov -/applications/main/bad_usb/ @skotopes @DrZlo13 @hedger @gsurkov @nminaylov -/applications/main/gpio/ @skotopes @DrZlo13 @hedger @gsurkov @nminaylov -/applications/main/ibutton/ @skotopes @DrZlo13 @hedger @gsurkov -/applications/main/infrared/ @skotopes @DrZlo13 @hedger @gsurkov -/applications/main/nfc/ @skotopes @DrZlo13 @hedger @gsurkov @gornekich -/applications/main/subghz/ @skotopes @DrZlo13 @hedger @gsurkov @Skorpionm -/applications/main/u2f/ @skotopes @DrZlo13 @hedger @gsurkov @nminaylov +/applications/main/archive/ @DrZlo13 @hedger @gsurkov @nminaylov +/applications/main/bad_usb/ @DrZlo13 @hedger @gsurkov @nminaylov +/applications/main/gpio/ @DrZlo13 @hedger @gsurkov @nminaylov +/applications/main/ibutton/ @DrZlo13 @hedger @gsurkov +/applications/main/infrared/ @DrZlo13 @hedger @gsurkov +/applications/main/nfc/ @DrZlo13 @hedger @gsurkov @gornekich +/applications/main/subghz/ @DrZlo13 @hedger @gsurkov @Skorpionm +/applications/main/u2f/ @DrZlo13 @hedger @gsurkov @nminaylov -/applications/services/bt/ @skotopes @DrZlo13 @hedger @gsurkov @gornekich -/applications/services/cli/ @skotopes @DrZlo13 @hedger @gsurkov @nminaylov -/applications/services/crypto/ @skotopes @DrZlo13 @hedger @gsurkov @nminaylov -/applications/services/desktop/ @skotopes @DrZlo13 @hedger @gsurkov @nminaylov -/applications/services/dolphin/ @skotopes @DrZlo13 @hedger @gsurkov @nminaylov -/applications/services/power/ @skotopes @DrZlo13 @hedger @gsurkov @gornekich -/applications/services/rpc/ @skotopes @DrZlo13 @hedger @gsurkov @nminaylov +/applications/services/bt/ @DrZlo13 @hedger @gsurkov @gornekich +/applications/services/cli/ @DrZlo13 @hedger @gsurkov @nminaylov +/applications/services/crypto/ @DrZlo13 @hedger @gsurkov @nminaylov +/applications/services/desktop/ @DrZlo13 @hedger @gsurkov @nminaylov +/applications/services/dolphin/ @DrZlo13 @hedger @gsurkov @nminaylov +/applications/services/power/ @DrZlo13 @hedger @gsurkov @gornekich +/applications/services/rpc/ @DrZlo13 @hedger @gsurkov @nminaylov -/applications/services/bt_settings_app/ @skotopes @DrZlo13 @hedger @gsurkov @gornekich -/applications/services/desktop_settings/ @skotopes @DrZlo13 @hedger @gsurkov @nminaylov -/applications/services/dolphin_passport/ @skotopes @DrZlo13 @hedger @gsurkov @nminaylov -/applications/services/power_settings_app/ @skotopes @DrZlo13 @hedger @gsurkov @gornekich +/applications/services/bt_settings_app/ @DrZlo13 @hedger @gsurkov @gornekich +/applications/services/desktop_settings/ @DrZlo13 @hedger @gsurkov @nminaylov +/applications/services/dolphin_passport/ @DrZlo13 @hedger @gsurkov @nminaylov +/applications/services/power_settings_app/ @DrZlo13 @hedger @gsurkov @gornekich -/applications/system/storage_move_to_sd/ @skotopes @DrZlo13 @hedger @gsurkov @nminaylov -/applications/system/js_app/ @skotopes @DrZlo13 @hedger @gsurkov @nminaylov @portasynthinca3 +/applications/system/storage_move_to_sd/ @DrZlo13 @hedger @gsurkov @nminaylov +/applications/system/js_app/ @DrZlo13 @hedger @gsurkov @nminaylov @portasynthinca3 -/applications/debug/unit_tests/ @skotopes @DrZlo13 @hedger @gsurkov @nminaylov @gornekich @Skorpionm +/applications/debug/unit_tests/ @DrZlo13 @hedger @gsurkov @nminaylov @gornekich @Skorpionm -/applications/examples/example_thermo/ @skotopes @DrZlo13 @hedger @gsurkov +/applications/examples/example_thermo/ @DrZlo13 @hedger @gsurkov # Firmware targets -/targets/ @skotopes @DrZlo13 @hedger @gsurkov @nminaylov +/targets/ @DrZlo13 @hedger @gsurkov @nminaylov # Assets -/applications/main/infrared/resources/ @skotopes @DrZlo13 @hedger @gsurkov +/applications/main/infrared/resources/ @DrZlo13 @hedger @gsurkov # Documentation -/documentation/ @skotopes @DrZlo13 @hedger @gsurkov @portasynthinca3 -/scripts/toolchain/ @skotopes @DrZlo13 @hedger @gsurkov +/documentation/ @DrZlo13 @hedger @gsurkov @portasynthinca3 +/scripts/toolchain/ @DrZlo13 @hedger @gsurkov # Lib -/lib/stm32wb_copro/ @skotopes @DrZlo13 @hedger @gsurkov @gornekich -/lib/digital_signal/ @skotopes @DrZlo13 @hedger @gsurkov @gornekich -/lib/infrared/ @skotopes @DrZlo13 @hedger @gsurkov -/lib/lfrfid/ @skotopes @DrZlo13 @hedger @gsurkov @nminaylov -/lib/libusb_stm32/ @skotopes @DrZlo13 @hedger @gsurkov @nminaylov -/lib/mbedtls/ @skotopes @DrZlo13 @hedger @gsurkov @nminaylov -/lib/mjs/ @skotopes @DrZlo13 @hedger @gsurkov @nminaylov @portasynthinca3 -/lib/nanopb/ @skotopes @DrZlo13 @hedger @gsurkov @nminaylov -/lib/nfc/ @skotopes @DrZlo13 @hedger @gsurkov @gornekich -/lib/one_wire/ @skotopes @DrZlo13 @hedger @gsurkov -/lib/subghz/ @skotopes @DrZlo13 @hedger @gsurkov @Skorpionm +/lib/stm32wb_copro/ @DrZlo13 @hedger @gsurkov @gornekich +/lib/digital_signal/ @DrZlo13 @hedger @gsurkov @gornekich +/lib/infrared/ @DrZlo13 @hedger @gsurkov +/lib/lfrfid/ @DrZlo13 @hedger @gsurkov @nminaylov +/lib/libusb_stm32/ @DrZlo13 @hedger @gsurkov @nminaylov +/lib/mbedtls/ @DrZlo13 @hedger @gsurkov @nminaylov +/lib/mjs/ @DrZlo13 @hedger @gsurkov @nminaylov @portasynthinca3 +/lib/nanopb/ @DrZlo13 @hedger @gsurkov @nminaylov +/lib/nfc/ @DrZlo13 @hedger @gsurkov @gornekich +/lib/one_wire/ @DrZlo13 @hedger @gsurkov +/lib/subghz/ @DrZlo13 @hedger @gsurkov @Skorpionm # CI/CD -/.github/workflows/ @skotopes @DrZlo13 @hedger @gsurkov +/.github/workflows/ @DrZlo13 @hedger @gsurkov diff --git a/furi/core/memmgr_heap.c b/furi/core/memmgr_heap.c index c8a72bc8c..3ce0558a3 100644 --- a/furi/core/memmgr_heap.c +++ b/furi/core/memmgr_heap.c @@ -295,10 +295,12 @@ void memmgr_heap_printf_free_blocks(void) { //can be enabled once we can do printf with a locked scheduler //vTaskSuspendAll(); - pxBlock = xStart.pxNextFreeBlock; - while(pxBlock->pxNextFreeBlock != NULL) { + pxBlock = heapPROTECT_BLOCK_POINTER(xStart.pxNextFreeBlock); + heapVALIDATE_BLOCK_POINTER(pxBlock); + while(pxBlock->pxNextFreeBlock != heapPROTECT_BLOCK_POINTER(NULL)) { printf("A %p S %lu\r\n", (void*)pxBlock, (uint32_t)pxBlock->xBlockSize); - pxBlock = pxBlock->pxNextFreeBlock; + pxBlock = heapPROTECT_BLOCK_POINTER(pxBlock->pxNextFreeBlock); + heapVALIDATE_BLOCK_POINTER(pxBlock); } //xTaskResumeAll(); From 6f852e646c8ed0c4bed70274adc06974fa8b533d Mon Sep 17 00:00:00 2001 From: Anna Antonenko Date: Fri, 4 Apr 2025 02:48:09 +0400 Subject: [PATCH 188/268] docs: badusb arbitrary modkey chains (#4176) --- .../file_formats/BadUsbScriptFormat.md | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/documentation/file_formats/BadUsbScriptFormat.md b/documentation/file_formats/BadUsbScriptFormat.md index 11977c9cb..a26f12489 100644 --- a/documentation/file_formats/BadUsbScriptFormat.md +++ b/documentation/file_formats/BadUsbScriptFormat.md @@ -57,19 +57,17 @@ Pause script execution by a defined time. ### Modifier keys -Can be combined with a special key command or a single character. -| Command | Notes | -| -------------- | ---------- | -| CONTROL / CTRL | | -| SHIFT | | -| ALT | | -| WINDOWS / GUI | | -| CTRL-ALT | CTRL+ALT | -| CTRL-SHIFT | CTRL+SHIFT | -| ALT-SHIFT | ALT+SHIFT | -| ALT-GUI | ALT+WIN | -| GUI-SHIFT | WIN+SHIFT | -| GUI-CTRL | WIN+CTRL | +The following modifier keys are recognized: +| Command | Notes | +| ------- | ------------ | +| CTRL | | +| CONTROL | Same as CTRL | +| SHIFT | | +| ALT | | +| GUI | | +| WINDOWS | Same as GUI | + +You can chain multiple modifier keys together using hyphens (`-`) or spaces. ## Key hold and release From 7192c9e68b47e1a52444ba056a55b5ca32c221c6 Mon Sep 17 00:00:00 2001 From: Anna Antonenko Date: Sat, 5 Apr 2025 02:58:58 +0400 Subject: [PATCH 189/268] [FL-3965] Separate cli_shell into toolbox (#4175) * cli_shell: separate into toolbox * fix: cmd flags * fix formatting * cli: increase default stack depth * cli_shell: fix loader lock logic * cli: fix command flags * fix f18 * speaker_debug: fix * cli_registry: fix docs * ufbt: rename cli target back * cli: rename app and record * cli: fix and simplify help command * cli_master_shell: fix ext commands * fix formatting * cli: rename master to main * fix formatting --------- Co-authored-by: hedger --- .../debug/speaker_debug/speaker_debug.c | 12 +- applications/debug/unit_tests/test_runner.c | 2 +- .../debug/unit_tests/tests/rpc/rpc_test.c | 1 - applications/debug/unit_tests/unit_tests.c | 9 +- applications/main/gpio/usb_uart_bridge.c | 1 - applications/main/ibutton/ibutton_cli.c | 4 +- applications/main/infrared/infrared_cli.c | 4 +- applications/main/lfrfid/lfrfid_cli.c | 4 +- applications/main/lfrfid/lfrfid_i.h | 1 - applications/main/nfc/nfc_app_i.h | 1 - applications/main/nfc/nfc_cli.c | 5 +- applications/main/onewire/onewire_cli.c | 6 +- .../main/subghz/helpers/subghz_chat.h | 1 - applications/main/subghz/subghz_cli.c | 5 +- applications/main/subghz/subghz_cli.h | 2 - applications/services/bt/bt_cli.c | 7 +- applications/services/cli/application.fam | 20 +- applications/services/cli/cli.c | 184 ----------- applications/services/cli/cli.h | 131 -------- applications/services/cli/cli_command_gpio.c | 1 + applications/services/cli/cli_command_gpio.h | 1 - applications/services/cli/cli_commands.h | 34 -- .../{cli_commands.c => cli_main_commands.c} | 105 ++---- applications/services/cli/cli_main_commands.h | 9 + applications/services/cli/cli_main_shell.c | 46 +++ applications/services/cli/cli_main_shell.h | 7 + applications/services/cli/cli_vcp.c | 20 +- .../services/cli/commands/hello_world.c | 4 +- applications/services/cli/commands/neofetch.c | 5 +- .../services/cli/commands/subshell_demo.c | 43 +++ applications/services/cli/shell/cli_shell.h | 16 - applications/services/crypto/crypto_cli.c | 8 +- applications/services/desktop/desktop.c | 1 - applications/services/input/input.c | 9 +- applications/services/input/input_cli.c | 2 +- applications/services/loader/loader_cli.c | 8 +- applications/services/power/power_cli.c | 9 +- applications/services/rpc/rpc.c | 14 +- applications/services/rpc/rpc_cli.c | 3 +- applications/services/rpc/rpc_i.h | 1 - applications/services/storage/storage_cli.c | 17 +- applications/system/js_app/js_app.c | 7 +- applications/system/updater/cli/updater_cli.c | 7 +- lib/toolbox/SConscript | 4 + .../services => lib/toolbox}/cli/cli_ansi.c | 0 .../services => lib/toolbox}/cli/cli_ansi.h | 2 +- lib/toolbox/cli/cli_command.c | 17 + lib/toolbox/cli/cli_command.h | 103 ++++++ lib/toolbox/cli/cli_registry.c | 178 +++++++++++ lib/toolbox/cli/cli_registry.h | 92 ++++++ .../toolbox/cli/cli_registry_i.h | 26 +- .../toolbox}/cli/shell/cli_shell.c | 301 ++++++++++++------ lib/toolbox/cli/shell/cli_shell.h | 75 +++++ .../cli/shell/cli_shell_completions.c | 14 +- .../cli/shell/cli_shell_completions.h | 7 +- .../toolbox}/cli/shell/cli_shell_i.h | 2 + .../toolbox}/cli/shell/cli_shell_line.c | 27 +- .../toolbox}/cli/shell/cli_shell_line.h | 2 + targets/f18/api_symbols.csv | 25 +- targets/f7/api_symbols.csv | 25 +- 60 files changed, 994 insertions(+), 683 deletions(-) delete mode 100644 applications/services/cli/cli.c delete mode 100644 applications/services/cli/cli.h delete mode 100644 applications/services/cli/cli_commands.h rename applications/services/cli/{cli_commands.c => cli_main_commands.c} (82%) create mode 100644 applications/services/cli/cli_main_commands.h create mode 100644 applications/services/cli/cli_main_shell.c create mode 100644 applications/services/cli/cli_main_shell.h create mode 100644 applications/services/cli/commands/subshell_demo.c delete mode 100644 applications/services/cli/shell/cli_shell.h rename {applications/services => lib/toolbox}/cli/cli_ansi.c (100%) rename {applications/services => lib/toolbox}/cli/cli_ansi.h (99%) create mode 100644 lib/toolbox/cli/cli_command.c create mode 100644 lib/toolbox/cli/cli_command.h create mode 100644 lib/toolbox/cli/cli_registry.c create mode 100644 lib/toolbox/cli/cli_registry.h rename applications/services/cli/cli_i.h => lib/toolbox/cli/cli_registry_i.h (51%) rename {applications/services => lib/toolbox}/cli/shell/cli_shell.c (57%) create mode 100644 lib/toolbox/cli/shell/cli_shell.h rename {applications/services => lib/toolbox}/cli/shell/cli_shell_completions.c (97%) rename {applications/services => lib/toolbox}/cli/shell/cli_shell_completions.h (67%) rename {applications/services => lib/toolbox}/cli/shell/cli_shell_i.h (91%) rename {applications/services => lib/toolbox}/cli/shell/cli_shell_line.c (95%) rename {applications/services => lib/toolbox}/cli/shell/cli_shell_line.h (94%) diff --git a/applications/debug/speaker_debug/speaker_debug.c b/applications/debug/speaker_debug/speaker_debug.c index d2a40a2a4..6a9956b07 100644 --- a/applications/debug/speaker_debug/speaker_debug.c +++ b/applications/debug/speaker_debug/speaker_debug.c @@ -1,9 +1,10 @@ #include #include #include -#include #include #include +#include +#include #define TAG "SpeakerDebug" @@ -20,14 +21,14 @@ typedef struct { typedef struct { MusicWorker* music_worker; FuriMessageQueue* message_queue; - Cli* cli; + CliRegistry* cli_registry; } SpeakerDebugApp; static SpeakerDebugApp* speaker_app_alloc(void) { SpeakerDebugApp* app = (SpeakerDebugApp*)malloc(sizeof(SpeakerDebugApp)); app->music_worker = music_worker_alloc(); app->message_queue = furi_message_queue_alloc(8, sizeof(SpeakerDebugAppMessage)); - app->cli = furi_record_open(RECORD_CLI); + app->cli_registry = furi_record_open(RECORD_CLI); return app; } @@ -96,7 +97,8 @@ static void speaker_app_run(SpeakerDebugApp* app, const char* arg) { return; } - cli_add_command(app->cli, CLI_COMMAND, CliCommandFlagParallelSafe, speaker_app_cli, app); + cli_registry_add_command( + app->cli_registry, CLI_COMMAND, CliCommandFlagParallelSafe, speaker_app_cli, app); SpeakerDebugAppMessage message; FuriStatus status; @@ -111,7 +113,7 @@ static void speaker_app_run(SpeakerDebugApp* app, const char* arg) { } } - cli_delete_command(app->cli, CLI_COMMAND); + cli_registry_delete_command(app->cli_registry, CLI_COMMAND); } int32_t speaker_debug_app(void* arg) { diff --git a/applications/debug/unit_tests/test_runner.c b/applications/debug/unit_tests/test_runner.c index 4de051125..ad7d31b02 100644 --- a/applications/debug/unit_tests/test_runner.c +++ b/applications/debug/unit_tests/test_runner.c @@ -2,7 +2,7 @@ #include "tests/test_api.h" -#include +#include #include #include #include diff --git a/applications/debug/unit_tests/tests/rpc/rpc_test.c b/applications/debug/unit_tests/tests/rpc/rpc_test.c index 5d26bdb30..82ab872ce 100644 --- a/applications/debug/unit_tests/tests/rpc/rpc_test.c +++ b/applications/debug/unit_tests/tests/rpc/rpc_test.c @@ -3,7 +3,6 @@ #include #include -#include #include #include #include diff --git a/applications/debug/unit_tests/unit_tests.c b/applications/debug/unit_tests/unit_tests.c index db78d8ced..8605cb781 100644 --- a/applications/debug/unit_tests/unit_tests.c +++ b/applications/debug/unit_tests/unit_tests.c @@ -1,6 +1,8 @@ #include -#include #include +#include +#include +#include #include "test_runner.h" @@ -14,8 +16,9 @@ void unit_tests_cli(PipeSide* pipe, FuriString* args, void* context) { void unit_tests_on_system_start(void) { #ifdef SRV_CLI - Cli* cli = furi_record_open(RECORD_CLI); - cli_add_command(cli, "unit_tests", CliCommandFlagParallelSafe, unit_tests_cli, NULL); + CliRegistry* registry = furi_record_open(RECORD_CLI); + cli_registry_add_command( + registry, "unit_tests", CliCommandFlagParallelSafe, unit_tests_cli, NULL); furi_record_close(RECORD_CLI); #endif } diff --git a/applications/main/gpio/usb_uart_bridge.c b/applications/main/gpio/usb_uart_bridge.c index 15170f0d0..77cd02f63 100644 --- a/applications/main/gpio/usb_uart_bridge.c +++ b/applications/main/gpio/usb_uart_bridge.c @@ -1,7 +1,6 @@ #include "usb_uart_bridge.h" #include "usb_cdc.h" #include -#include #include #include #include diff --git a/applications/main/ibutton/ibutton_cli.c b/applications/main/ibutton/ibutton_cli.c index 2ff0860bb..0b9a59586 100644 --- a/applications/main/ibutton/ibutton_cli.c +++ b/applications/main/ibutton/ibutton_cli.c @@ -1,7 +1,7 @@ #include #include -#include +#include #include #include @@ -240,4 +240,4 @@ static void execute(PipeSide* pipe, FuriString* args, void* context) { furi_string_free(cmd); } -CLI_COMMAND_INTERFACE(ikey, execute, CliCommandFlagDefault, 1024); +CLI_COMMAND_INTERFACE(ikey, execute, CliCommandFlagDefault, 1024, CLI_APPID); diff --git a/applications/main/infrared/infrared_cli.c b/applications/main/infrared/infrared_cli.c index 22d916d15..eb13bcd79 100644 --- a/applications/main/infrared/infrared_cli.c +++ b/applications/main/infrared/infrared_cli.c @@ -1,4 +1,4 @@ -#include +#include #include #include #include @@ -554,4 +554,4 @@ static void execute(PipeSide* pipe, FuriString* args, void* context) { furi_string_free(command); } -CLI_COMMAND_INTERFACE(ir, execute, CliCommandFlagDefault, 2048); +CLI_COMMAND_INTERFACE(ir, execute, CliCommandFlagDefault, 2048, CLI_APPID); diff --git a/applications/main/lfrfid/lfrfid_cli.c b/applications/main/lfrfid/lfrfid_cli.c index cefc55f65..63ca046b9 100644 --- a/applications/main/lfrfid/lfrfid_cli.c +++ b/applications/main/lfrfid/lfrfid_cli.c @@ -1,7 +1,7 @@ #include #include #include -#include +#include #include #include #include @@ -566,4 +566,4 @@ static void execute(PipeSide* pipe, FuriString* args, void* context) { furi_string_free(cmd); } -CLI_COMMAND_INTERFACE(rfid, execute, CliCommandFlagDefault, 2048); +CLI_COMMAND_INTERFACE(rfid, execute, CliCommandFlagDefault, 2048, CLI_APPID); diff --git a/applications/main/lfrfid/lfrfid_i.h b/applications/main/lfrfid/lfrfid_i.h index 2fcedcd7f..29a5eb902 100644 --- a/applications/main/lfrfid/lfrfid_i.h +++ b/applications/main/lfrfid/lfrfid_i.h @@ -8,7 +8,6 @@ #include #include #include -#include #include #include diff --git a/applications/main/nfc/nfc_app_i.h b/applications/main/nfc/nfc_app_i.h index 14e484622..1101b7bb2 100644 --- a/applications/main/nfc/nfc_app_i.h +++ b/applications/main/nfc/nfc_app_i.h @@ -10,7 +10,6 @@ #include #include #include -#include #include #include diff --git a/applications/main/nfc/nfc_cli.c b/applications/main/nfc/nfc_cli.c index fd5598fc6..af3fd62eb 100644 --- a/applications/main/nfc/nfc_cli.c +++ b/applications/main/nfc/nfc_cli.c @@ -1,7 +1,6 @@ #include #include -#include -#include +#include #include #include #include @@ -65,4 +64,4 @@ static void execute(PipeSide* pipe, FuriString* args, void* context) { furi_string_free(cmd); } -CLI_COMMAND_INTERFACE(nfc, execute, CliCommandFlagDefault, 1024); +CLI_COMMAND_INTERFACE(nfc, execute, CliCommandFlagDefault, 1024, CLI_APPID); diff --git a/applications/main/onewire/onewire_cli.c b/applications/main/onewire/onewire_cli.c index 83bbc6770..193de76e4 100644 --- a/applications/main/onewire/onewire_cli.c +++ b/applications/main/onewire/onewire_cli.c @@ -1,9 +1,9 @@ #include #include -#include +#include #include -#include +#include #include #include @@ -64,4 +64,4 @@ static void execute(PipeSide* pipe, FuriString* args, void* context) { furi_string_free(cmd); } -CLI_COMMAND_INTERFACE(onewire, execute, CliCommandFlagDefault, 1024); +CLI_COMMAND_INTERFACE(onewire, execute, CliCommandFlagDefault, 1024, CLI_APPID); diff --git a/applications/main/subghz/helpers/subghz_chat.h b/applications/main/subghz/helpers/subghz_chat.h index 0d1497506..25fce0ecf 100644 --- a/applications/main/subghz/helpers/subghz_chat.h +++ b/applications/main/subghz/helpers/subghz_chat.h @@ -1,7 +1,6 @@ #pragma once #include "../subghz_i.h" #include -#include #include typedef struct SubGhzChatWorker SubGhzChatWorker; diff --git a/applications/main/subghz/subghz_cli.c b/applications/main/subghz/subghz_cli.c index 3c29aeeaf..674738851 100644 --- a/applications/main/subghz/subghz_cli.c +++ b/applications/main/subghz/subghz_cli.c @@ -4,7 +4,8 @@ #include #include -#include +#include +#include #include #include @@ -1184,4 +1185,4 @@ static void execute(PipeSide* pipe, FuriString* args, void* context) { furi_string_free(cmd); } -CLI_COMMAND_INTERFACE(subghz, execute, CliCommandFlagDefault, 2048); +CLI_COMMAND_INTERFACE(subghz, execute, CliCommandFlagDefault, 2048, CLI_APPID); diff --git a/applications/main/subghz/subghz_cli.h b/applications/main/subghz/subghz_cli.h index f6388218f..275135581 100644 --- a/applications/main/subghz/subghz_cli.h +++ b/applications/main/subghz/subghz_cli.h @@ -1,5 +1,3 @@ #pragma once -#include - void subghz_on_system_start(void); diff --git a/applications/services/bt/bt_cli.c b/applications/services/bt/bt_cli.c index 9ef3ef8a2..d81bc0505 100644 --- a/applications/services/bt/bt_cli.c +++ b/applications/services/bt/bt_cli.c @@ -1,8 +1,9 @@ #include #include -#include #include #include +#include +#include #include #include "bt_settings.h" @@ -230,8 +231,8 @@ static void bt_cli(PipeSide* pipe, FuriString* args, void* context) { void bt_on_system_start(void) { #ifdef SRV_CLI - Cli* cli = furi_record_open(RECORD_CLI); - cli_add_command(cli, RECORD_BT, CliCommandFlagDefault, bt_cli, NULL); + CliRegistry* registry = furi_record_open(RECORD_CLI); + cli_registry_add_command(registry, "bt", CliCommandFlagDefault, bt_cli, NULL); furi_record_close(RECORD_CLI); #else UNUSED(bt_cli); diff --git a/applications/services/cli/application.fam b/applications/services/cli/application.fam index 6e2e393e0..e1e79f43d 100644 --- a/applications/services/cli/application.fam +++ b/applications/services/cli/application.fam @@ -4,17 +4,9 @@ App( entry_point="cli_on_system_start", cdefines=["SRV_CLI"], sources=[ - "cli.c", - "shell/cli_shell.c", - "shell/cli_shell_completions.c", - "shell/cli_shell_line.c", - "cli_commands.c", "cli_command_gpio.c", - "cli_ansi.c", - ], - sdk_headers=[ - "cli.h", - "cli_ansi.h", + "cli_main_commands.c", + "cli_main_shell.c", ], # This STARTUP has to be processed before those that depend on the "cli" record. # "cli" used to be a SERVICE, but it's been converted into a STARTUP in order to @@ -49,3 +41,11 @@ App( requires=["cli"], sources=["commands/neofetch.c"], ) + +App( + appid="cli_subshell_demo", + apptype=FlipperAppType.PLUGIN, + entry_point="cli_subshell_demo_ep", + requires=["cli"], + sources=["commands/subshell_demo.c"], +) diff --git a/applications/services/cli/cli.c b/applications/services/cli/cli.c deleted file mode 100644 index 2bfce3a63..000000000 --- a/applications/services/cli/cli.c +++ /dev/null @@ -1,184 +0,0 @@ -#include "cli.h" -#include "cli_i.h" -#include "cli_commands.h" -#include "cli_ansi.h" -#include - -#define TAG "cli" - -struct Cli { - CliCommandTree_t commands; - FuriMutex* mutex; -}; - -Cli* cli_alloc(void) { - Cli* cli = malloc(sizeof(Cli)); - CliCommandTree_init(cli->commands); - cli->mutex = furi_mutex_alloc(FuriMutexTypeRecursive); - return cli; -} - -void cli_add_command( - Cli* cli, - const char* name, - CliCommandFlag flags, - CliExecuteCallback callback, - void* context) { - cli_add_command_ex(cli, name, flags, callback, context, CLI_BUILTIN_COMMAND_STACK_SIZE); -} - -void cli_add_command_ex( - Cli* cli, - const char* name, - CliCommandFlag flags, - CliExecuteCallback callback, - void* context, - size_t stack_size) { - furi_check(cli); - furi_check(name); - furi_check(callback); - - // the shell always attaches the pipe to the stdio, thus both flags can't be used at once - if(flags & CliCommandFlagUseShellThread) furi_check(!(flags & CliCommandFlagDontAttachStdio)); - - FuriString* name_str; - name_str = furi_string_alloc_set(name); - // command cannot contain spaces - furi_check(furi_string_search_char(name_str, ' ') == FURI_STRING_FAILURE); - - CliCommand command = { - .context = context, - .execute_callback = callback, - .flags = flags, - .stack_depth = stack_size, - }; - - furi_check(furi_mutex_acquire(cli->mutex, FuriWaitForever) == FuriStatusOk); - CliCommandTree_set_at(cli->commands, name_str, command); - furi_check(furi_mutex_release(cli->mutex) == FuriStatusOk); - - furi_string_free(name_str); -} - -void cli_delete_command(Cli* cli, const char* name) { - furi_check(cli); - FuriString* name_str; - name_str = furi_string_alloc_set(name); - furi_string_trim(name_str); - - size_t name_replace; - do { - name_replace = furi_string_replace(name_str, " ", "_"); - } while(name_replace != FURI_STRING_FAILURE); - - furi_check(furi_mutex_acquire(cli->mutex, FuriWaitForever) == FuriStatusOk); - CliCommandTree_erase(cli->commands, name_str); - furi_check(furi_mutex_release(cli->mutex) == FuriStatusOk); - - furi_string_free(name_str); -} - -bool cli_get_command(Cli* cli, FuriString* command, CliCommand* result) { - furi_assert(cli); - furi_check(furi_mutex_acquire(cli->mutex, FuriWaitForever) == FuriStatusOk); - CliCommand* data = CliCommandTree_get(cli->commands, command); - if(data) *result = *data; - - furi_check(furi_mutex_release(cli->mutex) == FuriStatusOk); - - return !!data; -} - -void cli_remove_external_commands(Cli* cli) { - furi_check(cli); - furi_check(furi_mutex_acquire(cli->mutex, FuriWaitForever) == FuriStatusOk); - - // FIXME FL-3977: memory leak somewhere within this function - - CliCommandTree_t internal_cmds; - CliCommandTree_init(internal_cmds); - for - M_EACH(item, cli->commands, CliCommandTree_t) { - if(!(item->value_ptr->flags & CliCommandFlagExternal)) - CliCommandTree_set_at(internal_cmds, *item->key_ptr, *item->value_ptr); - } - CliCommandTree_move(cli->commands, internal_cmds); - - furi_check(furi_mutex_release(cli->mutex) == FuriStatusOk); -} - -void cli_enumerate_external_commands(Cli* cli) { - furi_check(cli); - furi_check(furi_mutex_acquire(cli->mutex, FuriWaitForever) == FuriStatusOk); - FURI_LOG_D(TAG, "Enumerating external commands"); - - cli_remove_external_commands(cli); - - // iterate over files in plugin directory - Storage* storage = furi_record_open(RECORD_STORAGE); - File* plugin_dir = storage_file_alloc(storage); - - if(storage_dir_open(plugin_dir, CLI_COMMANDS_PATH)) { - char plugin_filename[64]; - FuriString* plugin_name = furi_string_alloc(); - - while(storage_dir_read(plugin_dir, NULL, plugin_filename, sizeof(plugin_filename))) { - FURI_LOG_T(TAG, "Plugin: %s", plugin_filename); - furi_string_set_str(plugin_name, plugin_filename); - furi_string_replace_all_str(plugin_name, ".fal", ""); - furi_string_replace_at(plugin_name, 0, 4, ""); // remove "cli_" in the beginning - CliCommand command = { - .context = NULL, - .execute_callback = NULL, - .flags = CliCommandFlagExternal, - }; - CliCommandTree_set_at(cli->commands, plugin_name, command); - } - - furi_string_free(plugin_name); - } - - storage_file_free(plugin_dir); - furi_record_close(RECORD_STORAGE); - - FURI_LOG_D(TAG, "Finished enumerating external commands"); - furi_check(furi_mutex_release(cli->mutex) == FuriStatusOk); -} - -void cli_lock_commands(Cli* cli) { - furi_check(cli); - furi_check(furi_mutex_acquire(cli->mutex, FuriWaitForever) == FuriStatusOk); -} - -void cli_unlock_commands(Cli* cli) { - furi_check(cli); - furi_check(furi_mutex_release(cli->mutex) == FuriStatusOk); -} - -CliCommandTree_t* cli_get_commands(Cli* cli) { - furi_check(cli); - furi_check(furi_mutex_get_owner(cli->mutex) == furi_thread_get_current_id()); - return &cli->commands; -} - -bool cli_is_pipe_broken_or_is_etx_next_char(PipeSide* side) { - if(pipe_state(side) == PipeStateBroken) return true; - if(!pipe_bytes_available(side)) return false; - char c = getchar(); - return c == CliKeyETX; -} - -void cli_print_usage(const char* cmd, const char* usage, const char* arg) { - furi_check(cmd); - furi_check(arg); - furi_check(usage); - - printf("%s: illegal option -- %s\r\nusage: %s %s", cmd, arg, cmd, usage); -} - -void cli_on_system_start(void) { - Cli* cli = cli_alloc(); - cli_commands_init(cli); - cli_enumerate_external_commands(cli); - furi_record_create(RECORD_CLI, cli); -} diff --git a/applications/services/cli/cli.h b/applications/services/cli/cli.h deleted file mode 100644 index 2352e1806..000000000 --- a/applications/services/cli/cli.h +++ /dev/null @@ -1,131 +0,0 @@ -/** - * @file cli.h - * API for registering commands with the CLI - */ - -#pragma once -#include -#include -#include "cli_ansi.h" -#include - -#ifdef __cplusplus -extern "C" { -#endif - -#define RECORD_CLI "cli" - -typedef enum { - CliCommandFlagDefault = 0, /**< Default */ - CliCommandFlagParallelSafe = (1 << 0), /**< Safe to run in parallel with other apps */ - CliCommandFlagInsomniaSafe = (1 << 1), /**< Safe to run with insomnia mode on */ - CliCommandFlagDontAttachStdio = (1 << 2), /**< Do no attach I/O pipe to thread stdio */ - CliCommandFlagUseShellThread = - (1 - << 3), /**< Don't start a separate thread to run the command in. Incompatible with DontAttachStdio */ - - // internal flags (do not set them yourselves!) - - CliCommandFlagExternal = (1 << 4), /**< The command comes from a .fal file */ -} CliCommandFlag; - -/** Cli type anonymous structure */ -typedef struct Cli Cli; - -/** - * @brief CLI execution callback pointer. Implement this interface and use - * `add_cli_command`. - * - * This callback will be called from a separate thread spawned just for your - * command. The pipe will be installed as the thread's stdio, so you can use - * `printf`, `getchar` and other standard functions to communicate with the - * user. - * - * @param [in] pipe Pipe that can be used to send and receive data. If - * `CliCommandFlagDontAttachStdio` was not set, you can - * also use standard C functions (printf, getc, etc.) to - * access this pipe. - * @param [in] args String with what was passed after the command - * @param [in] context Whatever you provided to `cli_add_command` - */ -typedef void (*CliExecuteCallback)(PipeSide* pipe, FuriString* args, void* context); - -/** - * @brief Registers a command with the CLI. Provides less options than the `_ex` - * counterpart. - * - * @param [in] cli Pointer to CLI instance - * @param [in] name Command name - * @param [in] flags CliCommandFlag - * @param [in] callback Callback function - * @param [in] context Custom context - */ -void cli_add_command( - Cli* cli, - const char* name, - CliCommandFlag flags, - CliExecuteCallback callback, - void* context); - -/** - * @brief Registers a command with the CLI. Provides more options than the - * non-`_ex` counterpart. - * - * @param [in] cli Pointer to CLI instance - * @param [in] name Command name - * @param [in] flags CliCommandFlag - * @param [in] callback Callback function - * @param [in] context Custom context - * @param [in] stack_size Thread stack size - */ -void cli_add_command_ex( - Cli* cli, - const char* name, - CliCommandFlag flags, - CliExecuteCallback callback, - void* context, - size_t stack_size); - -/** - * @brief Deletes a cli command - * - * @param [in] cli pointer to cli instance - * @param [in] name command name - */ -void cli_delete_command(Cli* cli, const char* name); - -/** - * @brief Unregisters all external commands - * - * @param [in] cli pointer to the cli instance - */ -void cli_remove_external_commands(Cli* cli); - -/** - * @brief Reloads the list of externally available commands - * - * @param [in] cli pointer to cli instance - */ -void cli_enumerate_external_commands(Cli* cli); - -/** - * @brief Detects if Ctrl+C has been pressed or session has been terminated - * - * @param [in] side Pointer to pipe side given to the command thread - * @warning This function also assumes that the pipe is installed as the - * thread's stdio - * @warning This function will consume 1 byte from the pipe - */ -bool cli_is_pipe_broken_or_is_etx_next_char(PipeSide* side); - -/** Print unified cmd usage tip - * - * @param cmd cmd name - * @param usage usage tip - * @param arg arg passed by user - */ -void cli_print_usage(const char* cmd, const char* usage, const char* arg); - -#ifdef __cplusplus -} -#endif diff --git a/applications/services/cli/cli_command_gpio.c b/applications/services/cli/cli_command_gpio.c index f37e6387f..f6337265d 100644 --- a/applications/services/cli/cli_command_gpio.c +++ b/applications/services/cli/cli_command_gpio.c @@ -4,6 +4,7 @@ #include #include #include +#include void cli_command_gpio_print_usage(void) { printf("Usage:\r\n"); diff --git a/applications/services/cli/cli_command_gpio.h b/applications/services/cli/cli_command_gpio.h index 0290949e0..c1911fb65 100644 --- a/applications/services/cli/cli_command_gpio.h +++ b/applications/services/cli/cli_command_gpio.h @@ -1,6 +1,5 @@ #pragma once -#include "cli_i.h" #include void cli_command_gpio(PipeSide* pipe, FuriString* args, void* context); diff --git a/applications/services/cli/cli_commands.h b/applications/services/cli/cli_commands.h deleted file mode 100644 index 77d9930af..000000000 --- a/applications/services/cli/cli_commands.h +++ /dev/null @@ -1,34 +0,0 @@ -#pragma once - -#include "cli.h" -#include - -void cli_commands_init(Cli* cli); - -#define PLUGIN_APP_ID "cli" -#define PLUGIN_API_VERSION 1 - -typedef struct { - char* name; - CliExecuteCallback execute_callback; - CliCommandFlag flags; - size_t stack_depth; -} CliCommandDescriptor; - -#define CLI_COMMAND_INTERFACE(name, execute_callback, flags, stack_depth) \ - static const CliCommandDescriptor cli_##name##_desc = { \ - #name, \ - &execute_callback, \ - flags, \ - stack_depth, \ - }; \ - \ - static const FlipperAppPluginDescriptor plugin_descriptor = { \ - .appid = PLUGIN_APP_ID, \ - .ep_api_version = PLUGIN_API_VERSION, \ - .entry_point = &cli_##name##_desc, \ - }; \ - \ - const FlipperAppPluginDescriptor* cli_##name##_ep(void) { \ - return &plugin_descriptor; \ - } diff --git a/applications/services/cli/cli_commands.c b/applications/services/cli/cli_main_commands.c similarity index 82% rename from applications/services/cli/cli_commands.c rename to applications/services/cli/cli_main_commands.c index 723a4d556..f84c03d8a 100644 --- a/applications/services/cli/cli_commands.c +++ b/applications/services/cli/cli_main_commands.c @@ -1,7 +1,6 @@ -#include "cli_commands.h" +#include "cli_main_commands.h" #include "cli_command_gpio.h" -#include "cli_ansi.h" -#include "cli.h" +#include #include #include @@ -56,49 +55,6 @@ void cli_command_info(PipeSide* pipe, FuriString* args, void* context) { } } -void cli_command_help(PipeSide* pipe, FuriString* args, void* context) { - UNUSED(pipe); - UNUSED(args); - UNUSED(context); - printf("Available commands:" ANSI_FG_GREEN); - - // count non-hidden commands - Cli* cli = furi_record_open(RECORD_CLI); - cli_lock_commands(cli); - CliCommandTree_t* commands = cli_get_commands(cli); - size_t commands_count = CliCommandTree_size(*commands); - - // create iterators starting at different positions - const size_t columns = 3; - const size_t commands_per_column = (commands_count / columns) + (commands_count % columns); - CliCommandTree_it_t iterators[columns]; - for(size_t c = 0; c < columns; c++) { - CliCommandTree_it(iterators[c], *commands); - for(size_t i = 0; i < c * commands_per_column; i++) - CliCommandTree_next(iterators[c]); - } - - // print commands - for(size_t r = 0; r < commands_per_column; r++) { - printf("\r\n"); - - for(size_t c = 0; c < columns; c++) { - if(!CliCommandTree_end_p(iterators[c])) { - const CliCommandTree_itref_t* item = CliCommandTree_cref(iterators[c]); - printf("%-30s", furi_string_get_cstr(*item->key_ptr)); - CliCommandTree_next(iterators[c]); - } - } - } - - printf(ANSI_RESET - "\r\nIf you just added a new command and can't see it above, run `reload_ext_cmds`"); - printf(ANSI_RESET "\r\nFind out more: https://docs.flipper.net/development/cli"); - - cli_unlock_commands(cli); - furi_record_close(RECORD_CLI); -} - void cli_command_uptime(PipeSide* pipe, FuriString* args, void* context) { UNUSED(pipe); UNUSED(args); @@ -514,16 +470,6 @@ void cli_command_i2c(PipeSide* pipe, FuriString* args, void* context) { furi_hal_i2c_release(&furi_hal_i2c_handle_external); } -void cli_command_reload_external(PipeSide* pipe, FuriString* args, void* context) { - UNUSED(pipe); - UNUSED(args); - UNUSED(context); - Cli* cli = furi_record_open(RECORD_CLI); - cli_enumerate_external_commands(cli); - furi_record_close(RECORD_CLI); - printf("OK!"); -} - /** * Echoes any bytes it receives except ASCII ETX (0x03, Ctrl+C) */ @@ -545,27 +491,32 @@ void cli_command_echo(PipeSide* pipe, FuriString* args, void* context) { } } -void cli_commands_init(Cli* cli) { - cli_add_command(cli, "!", CliCommandFlagParallelSafe, cli_command_info, (void*)true); - cli_add_command(cli, "info", CliCommandFlagParallelSafe, cli_command_info, NULL); - cli_add_command(cli, "device_info", CliCommandFlagParallelSafe, cli_command_info, (void*)true); - cli_add_command( - cli, "reload_ext_cmds", CliCommandFlagDefault, cli_command_reload_external, NULL); +void cli_main_commands_init(CliRegistry* registry) { + cli_registry_add_command( + registry, "!", CliCommandFlagParallelSafe, cli_command_info, (void*)true); + cli_registry_add_command(registry, "info", CliCommandFlagParallelSafe, cli_command_info, NULL); + cli_registry_add_command( + registry, "device_info", CliCommandFlagParallelSafe, cli_command_info, (void*)true); - cli_add_command(cli, "?", CliCommandFlagParallelSafe, cli_command_help, NULL); - cli_add_command(cli, "help", CliCommandFlagParallelSafe, cli_command_help, NULL); + cli_registry_add_command( + registry, "uptime", CliCommandFlagParallelSafe, cli_command_uptime, NULL); + cli_registry_add_command(registry, "date", CliCommandFlagParallelSafe, cli_command_date, NULL); + cli_registry_add_command(registry, "log", CliCommandFlagParallelSafe, cli_command_log, NULL); + cli_registry_add_command(registry, "sysctl", CliCommandFlagDefault, cli_command_sysctl, NULL); + cli_registry_add_command(registry, "top", CliCommandFlagParallelSafe, cli_command_top, NULL); + cli_registry_add_command(registry, "free", CliCommandFlagParallelSafe, cli_command_free, NULL); + cli_registry_add_command( + registry, "free_blocks", CliCommandFlagDefault, cli_command_free_blocks, NULL); + cli_registry_add_command(registry, "echo", CliCommandFlagParallelSafe, cli_command_echo, NULL); - cli_add_command(cli, "uptime", CliCommandFlagDefault, cli_command_uptime, NULL); - cli_add_command(cli, "date", CliCommandFlagParallelSafe, cli_command_date, NULL); - cli_add_command(cli, "log", CliCommandFlagParallelSafe, cli_command_log, NULL); - cli_add_command(cli, "sysctl", CliCommandFlagDefault, cli_command_sysctl, NULL); - cli_add_command(cli, "top", CliCommandFlagParallelSafe, cli_command_top, NULL); - cli_add_command(cli, "free", CliCommandFlagParallelSafe, cli_command_free, NULL); - cli_add_command(cli, "free_blocks", CliCommandFlagParallelSafe, cli_command_free_blocks, NULL); - cli_add_command(cli, "echo", CliCommandFlagParallelSafe, cli_command_echo, NULL); - - cli_add_command(cli, "vibro", CliCommandFlagDefault, cli_command_vibro, NULL); - cli_add_command(cli, "led", CliCommandFlagDefault, cli_command_led, NULL); - cli_add_command(cli, "gpio", CliCommandFlagDefault, cli_command_gpio, NULL); - cli_add_command(cli, "i2c", CliCommandFlagDefault, cli_command_i2c, NULL); + cli_registry_add_command(registry, "vibro", CliCommandFlagDefault, cli_command_vibro, NULL); + cli_registry_add_command(registry, "led", CliCommandFlagDefault, cli_command_led, NULL); + cli_registry_add_command(registry, "gpio", CliCommandFlagDefault, cli_command_gpio, NULL); + cli_registry_add_command(registry, "i2c", CliCommandFlagDefault, cli_command_i2c, NULL); +} + +void cli_on_system_start(void) { + CliRegistry* registry = cli_registry_alloc(); + cli_main_commands_init(registry); + furi_record_create(RECORD_CLI, registry); } diff --git a/applications/services/cli/cli_main_commands.h b/applications/services/cli/cli_main_commands.h new file mode 100644 index 000000000..d8058f467 --- /dev/null +++ b/applications/services/cli/cli_main_commands.h @@ -0,0 +1,9 @@ +#pragma once + +#include +#include + +#define RECORD_CLI "cli" +#define CLI_APPID "cli" + +void cli_main_commands_init(CliRegistry* registry); diff --git a/applications/services/cli/cli_main_shell.c b/applications/services/cli/cli_main_shell.c new file mode 100644 index 000000000..7550bef04 --- /dev/null +++ b/applications/services/cli/cli_main_shell.c @@ -0,0 +1,46 @@ +#include "cli_main_shell.h" +#include "cli_main_commands.h" +#include +#include +#include + +void cli_main_motd(void* context) { + UNUSED(context); + printf(ANSI_FLIPPER_BRAND_ORANGE + "\r\n" + " _.-------.._ -,\r\n" + " .-\"```\"--..,,_/ /`-, -, \\ \r\n" + " .:\" /:/ /'\\ \\ ,_..., `. | |\r\n" + " / ,----/:/ /`\\ _\\~`_-\"` _;\r\n" + " ' / /`\"\"\"'\\ \\ \\.~`_-' ,-\"'/ \r\n" + " | | | 0 | | .-' ,/` /\r\n" + " | ,..\\ \\ ,.-\"` ,/` /\r\n" + " ; : `/`\"\"\\` ,/--==,/-----,\r\n" + " | `-...| -.___-Z:_______J...---;\r\n" + " : ` _-'\r\n" + " _L_ _ ___ ___ ___ ___ ____--\"`___ _ ___\r\n" + "| __|| | |_ _|| _ \\| _ \\| __|| _ \\ / __|| | |_ _|\r\n" + "| _| | |__ | | | _/| _/| _| | / | (__ | |__ | |\r\n" + "|_| |____||___||_| |_| |___||_|_\\ \\___||____||___|\r\n" + "\r\n" ANSI_FG_BR_WHITE "Welcome to Flipper Zero Command Line Interface!\r\n" + "Read the manual: https://docs.flipper.net/development/cli\r\n" + "Run `help` or `?` to list available commands\r\n" + "\r\n" ANSI_RESET); + + const Version* firmware_version = furi_hal_version_get_firmware_version(); + if(firmware_version) { + printf( + "Firmware version: %s %s (%s%s built on %s)\r\n", + version_get_gitbranch(firmware_version), + version_get_version(firmware_version), + version_get_githash(firmware_version), + version_get_dirty_flag(firmware_version) ? "-dirty" : "", + version_get_builddate(firmware_version)); + } +} + +const CliCommandExternalConfig cli_main_ext_config = { + .search_directory = "/ext/apps_data/cli/plugins", + .fal_prefix = "cli_", + .appid = CLI_APPID, +}; diff --git a/applications/services/cli/cli_main_shell.h b/applications/services/cli/cli_main_shell.h new file mode 100644 index 000000000..576839990 --- /dev/null +++ b/applications/services/cli/cli_main_shell.h @@ -0,0 +1,7 @@ +#pragma once + +#include + +void cli_main_motd(void* context); + +extern const CliCommandExternalConfig cli_main_ext_config; diff --git a/applications/services/cli/cli_vcp.c b/applications/services/cli/cli_vcp.c index 03ad6c610..f4b539e26 100644 --- a/applications/services/cli/cli_vcp.c +++ b/applications/services/cli/cli_vcp.c @@ -1,10 +1,12 @@ #include "cli_vcp.h" -#include "shell/cli_shell.h" #include #include #include #include #include +#include +#include "cli_main_shell.h" +#include "cli_main_commands.h" #define TAG "CliVcp" @@ -47,10 +49,12 @@ struct CliVcp { FuriHalUsbInterface* previous_interface; PipeSide* own_pipe; + PipeSide* shell_pipe; bool is_currently_transmitting; size_t previous_tx_length; - FuriThread* shell; + CliRegistry* main_registry; + CliShell* shell; }; // ============ @@ -210,13 +214,15 @@ static void cli_vcp_internal_event_happened(void* context) { // wait for previous shell to stop furi_check(!cli_vcp->own_pipe); if(cli_vcp->shell) { - furi_thread_join(cli_vcp->shell); - furi_thread_free(cli_vcp->shell); + cli_shell_join(cli_vcp->shell); + cli_shell_free(cli_vcp->shell); + pipe_free(cli_vcp->shell_pipe); } // start shell thread PipeSideBundle bundle = pipe_alloc(VCP_BUF_SIZE, 1); cli_vcp->own_pipe = bundle.alices_side; + cli_vcp->shell_pipe = bundle.bobs_side; pipe_attach_to_event_loop(cli_vcp->own_pipe, cli_vcp->event_loop); pipe_set_callback_context(cli_vcp->own_pipe, cli_vcp); pipe_set_data_arrived_callback( @@ -224,7 +230,9 @@ static void cli_vcp_internal_event_happened(void* context) { pipe_set_space_freed_callback( cli_vcp->own_pipe, cli_vcp_shell_ready, FuriEventLoopEventFlagEdge); furi_delay_ms(33); // we are too fast, minicom isn't ready yet - cli_vcp->shell = cli_shell_start(bundle.bobs_side); + cli_vcp->shell = cli_shell_alloc( + cli_main_motd, NULL, cli_vcp->shell_pipe, cli_vcp->main_registry, &cli_main_ext_config); + cli_shell_start(cli_vcp->shell); } if(event & CliVcpInternalEventRx) { @@ -260,6 +268,8 @@ static CliVcp* cli_vcp_alloc(void) { furi_event_loop_subscribe_thread_flags( cli_vcp->event_loop, cli_vcp_internal_event_happened, cli_vcp); + cli_vcp->main_registry = furi_record_open(RECORD_CLI); + return cli_vcp; } diff --git a/applications/services/cli/commands/hello_world.c b/applications/services/cli/commands/hello_world.c index 81be97298..b77f3e663 100644 --- a/applications/services/cli/commands/hello_world.c +++ b/applications/services/cli/commands/hello_world.c @@ -1,4 +1,4 @@ -#include "../cli_commands.h" +#include "../cli_main_commands.h" static void execute(PipeSide* pipe, FuriString* args, void* context) { UNUSED(pipe); @@ -7,4 +7,4 @@ static void execute(PipeSide* pipe, FuriString* args, void* context) { puts("Hello, World!"); } -CLI_COMMAND_INTERFACE(hello_world, execute, CliCommandFlagDefault, 768); +CLI_COMMAND_INTERFACE(hello_world, execute, CliCommandFlagParallelSafe, 768, CLI_APPID); diff --git a/applications/services/cli/commands/neofetch.c b/applications/services/cli/commands/neofetch.c index e652212eb..0e50a0d8d 100644 --- a/applications/services/cli/commands/neofetch.c +++ b/applications/services/cli/commands/neofetch.c @@ -1,4 +1,5 @@ -#include "../cli_commands.h" +#include "../cli_main_commands.h" +#include #include #include #include @@ -156,4 +157,4 @@ static void execute(PipeSide* pipe, FuriString* args, void* context) { #undef NEOFETCH_COLOR } -CLI_COMMAND_INTERFACE(neofetch, execute, CliCommandFlagDefault, 2048); +CLI_COMMAND_INTERFACE(neofetch, execute, CliCommandFlagParallelSafe, 2048, CLI_APPID); diff --git a/applications/services/cli/commands/subshell_demo.c b/applications/services/cli/commands/subshell_demo.c new file mode 100644 index 000000000..f0013c4a0 --- /dev/null +++ b/applications/services/cli/commands/subshell_demo.c @@ -0,0 +1,43 @@ +#include "../cli_main_commands.h" +#include +#include +#include + +#define RAINBOW_SUBCOMMAND \ + ANSI_FG_RED "s" ANSI_FG_YELLOW "u" ANSI_FG_BLUE "b" ANSI_FG_GREEN "c" ANSI_FG_MAGENTA \ + "o" ANSI_FG_RED "m" ANSI_FG_YELLOW "m" ANSI_FG_BLUE "a" ANSI_FG_GREEN \ + "n" ANSI_FG_MAGENTA "d" + +static void subcommand(PipeSide* pipe, FuriString* args, void* context) { + UNUSED(pipe); + UNUSED(args); + UNUSED(context); + printf("This is a ✨" RAINBOW_SUBCOMMAND ANSI_RESET "✨!"); +} + +static void motd(void* context) { + UNUSED(context); + printf("\r\n"); + printf("+------------------------------------+\r\n"); + printf("| Hello world! |\r\n"); + printf("| This is the " ANSI_FG_GREEN "MOTD" ANSI_RESET " for our " ANSI_FG_BLUE + "subshell" ANSI_RESET "! |\r\n"); + printf("+------------------------------------+\r\n"); +} + +static void execute(PipeSide* pipe, FuriString* args, void* context) { + UNUSED(args); + UNUSED(context); + CliRegistry* registry = cli_registry_alloc(); + cli_registry_add_command(registry, "subcommand", CliCommandFlagParallelSafe, subcommand, NULL); + + CliShell* shell = cli_shell_alloc(motd, NULL, pipe, registry, NULL); + cli_shell_set_prompt(shell, "subshell"); + cli_shell_start(shell); + cli_shell_join(shell); + + cli_shell_free(shell); + cli_registry_free(registry); +} + +CLI_COMMAND_INTERFACE(subshell_demo, execute, CliCommandFlagParallelSafe, 2048, CLI_APPID); diff --git a/applications/services/cli/shell/cli_shell.h b/applications/services/cli/shell/cli_shell.h deleted file mode 100644 index e60eefc77..000000000 --- a/applications/services/cli/shell/cli_shell.h +++ /dev/null @@ -1,16 +0,0 @@ -#pragma once - -#include -#include - -#ifdef __cplusplus -extern "C" { -#endif - -#define CLI_SHELL_STACK_SIZE (4 * 1024U) - -FuriThread* cli_shell_start(PipeSide* pipe); - -#ifdef __cplusplus -} -#endif diff --git a/applications/services/crypto/crypto_cli.c b/applications/services/crypto/crypto_cli.c index bfefaf0f1..32b4199f5 100644 --- a/applications/services/crypto/crypto_cli.c +++ b/applications/services/crypto/crypto_cli.c @@ -3,7 +3,9 @@ #include #include -#include +#include +#include +#include void crypto_cli_print_usage(void) { printf("Usage:\r\n"); @@ -319,8 +321,8 @@ static void crypto_cli(PipeSide* pipe, FuriString* args, void* context) { void crypto_on_system_start(void) { #ifdef SRV_CLI - Cli* cli = furi_record_open(RECORD_CLI); - cli_add_command(cli, "crypto", CliCommandFlagDefault, crypto_cli, NULL); + CliRegistry* registry = furi_record_open(RECORD_CLI); + cli_registry_add_command(registry, "crypto", CliCommandFlagDefault, crypto_cli, NULL); furi_record_close(RECORD_CLI); #else UNUSED(crypto_cli); diff --git a/applications/services/desktop/desktop.c b/applications/services/desktop/desktop.c index 0f6304823..36536b99f 100644 --- a/applications/services/desktop/desktop.c +++ b/applications/services/desktop/desktop.c @@ -1,6 +1,5 @@ #include "desktop_i.h" -#include #include #include diff --git a/applications/services/input/input.c b/applications/services/input/input.c index c77771f3e..d7e9f3d31 100644 --- a/applications/services/input/input.c +++ b/applications/services/input/input.c @@ -4,8 +4,9 @@ #include #include #include -#include #include +#include +#include #include #define INPUT_DEBOUNCE_TICKS_HALF (INPUT_DEBOUNCE_TICKS / 2) @@ -93,8 +94,10 @@ int32_t input_srv(void* p) { #endif #ifdef SRV_CLI - Cli* cli = furi_record_open(RECORD_CLI); - cli_add_command(cli, "input", CliCommandFlagParallelSafe, input_cli, event_pubsub); + CliRegistry* registry = furi_record_open(RECORD_CLI); + cli_registry_add_command( + registry, "input", CliCommandFlagParallelSafe, input_cli, event_pubsub); + furi_record_close(RECORD_CLI); #endif InputPinState pin_states[input_pins_count]; diff --git a/applications/services/input/input_cli.c b/applications/services/input/input_cli.c index a34ec3c36..d5ea57418 100644 --- a/applications/services/input/input_cli.c +++ b/applications/services/input/input_cli.c @@ -1,7 +1,7 @@ #include "input.h" #include -#include +#include #include #include diff --git a/applications/services/loader/loader_cli.c b/applications/services/loader/loader_cli.c index 40312d8b3..265779e8f 100644 --- a/applications/services/loader/loader_cli.c +++ b/applications/services/loader/loader_cli.c @@ -1,7 +1,8 @@ #include "loader.h" #include -#include +#include +#include #include #include #include @@ -141,8 +142,9 @@ static void loader_cli(PipeSide* pipe, FuriString* args, void* context) { void loader_on_system_start(void) { #ifdef SRV_CLI - Cli* cli = furi_record_open(RECORD_CLI); - cli_add_command(cli, RECORD_LOADER, CliCommandFlagParallelSafe, loader_cli, NULL); + CliRegistry* registry = furi_record_open(RECORD_CLI); + cli_registry_add_command( + registry, RECORD_LOADER, CliCommandFlagParallelSafe, loader_cli, NULL); furi_record_close(RECORD_CLI); #else UNUSED(loader_cli); diff --git a/applications/services/power/power_cli.c b/applications/services/power/power_cli.c index f1771d9f1..f630cb2e8 100644 --- a/applications/services/power/power_cli.c +++ b/applications/services/power/power_cli.c @@ -1,7 +1,8 @@ #include "power_cli.h" #include -#include +#include +#include #include #include #include @@ -114,10 +115,8 @@ void power_cli(PipeSide* pipe, FuriString* args, void* context) { void power_on_system_start(void) { #ifdef SRV_CLI - Cli* cli = furi_record_open(RECORD_CLI); - - cli_add_command(cli, "power", CliCommandFlagParallelSafe, power_cli, NULL); - + CliRegistry* registry = furi_record_open(RECORD_CLI); + cli_registry_add_command(registry, "power", CliCommandFlagParallelSafe, power_cli, NULL); furi_record_close(RECORD_CLI); #else UNUSED(power_cli); diff --git a/applications/services/rpc/rpc.c b/applications/services/rpc/rpc.c index 41d55841e..1a19348ff 100644 --- a/applications/services/rpc/rpc.c +++ b/applications/services/rpc/rpc.c @@ -9,7 +9,8 @@ #include -#include +#include +#include #include #include #include @@ -435,9 +436,14 @@ void rpc_on_system_start(void* p) { rpc->busy_mutex = furi_mutex_alloc(FuriMutexTypeNormal); - Cli* cli = furi_record_open(RECORD_CLI); - cli_add_command( - cli, "start_rpc_session", CliCommandFlagParallelSafe, rpc_cli_command_start_session, rpc); + CliRegistry* registry = furi_record_open(RECORD_CLI); + cli_registry_add_command( + registry, + "start_rpc_session", + CliCommandFlagParallelSafe, + rpc_cli_command_start_session, + rpc); + furi_record_close(RECORD_CLI); furi_record_create(RECORD_RPC, rpc); } diff --git a/applications/services/rpc/rpc_cli.c b/applications/services/rpc/rpc_cli.c index 3280fe702..fda059ec8 100644 --- a/applications/services/rpc/rpc_cli.c +++ b/applications/services/rpc/rpc_cli.c @@ -1,4 +1,5 @@ -#include +#include +#include #include #include #include diff --git a/applications/services/rpc/rpc_i.h b/applications/services/rpc/rpc_i.h index 51903a276..df1f17de4 100644 --- a/applications/services/rpc/rpc_i.h +++ b/applications/services/rpc/rpc_i.h @@ -5,7 +5,6 @@ #include #include #include -#include #include #ifdef __cplusplus diff --git a/applications/services/storage/storage_cli.c b/applications/services/storage/storage_cli.c index 58b851926..2dab63e53 100644 --- a/applications/services/storage/storage_cli.c +++ b/applications/services/storage/storage_cli.c @@ -1,7 +1,9 @@ #include #include -#include +#include +#include +#include #include #include #include @@ -695,16 +697,15 @@ static void storage_cli_factory_reset(PipeSide* pipe, FuriString* args, void* co void storage_on_system_start(void) { #ifdef SRV_CLI - Cli* cli = furi_record_open(RECORD_CLI); - cli_add_command_ex( - cli, + CliRegistry* registry = furi_record_open(RECORD_CLI); + cli_registry_add_command( + registry, "storage", CliCommandFlagParallelSafe | CliCommandFlagUseShellThread, storage_cli, - NULL, - 512); - cli_add_command( - cli, "factory_reset", CliCommandFlagParallelSafe, storage_cli_factory_reset, NULL); + NULL); + cli_registry_add_command( + registry, "factory_reset", CliCommandFlagParallelSafe, storage_cli_factory_reset, NULL); furi_record_close(RECORD_CLI); #else UNUSED(storage_cli_factory_reset); diff --git a/applications/system/js_app/js_app.c b/applications/system/js_app/js_app.c index bdb2ccc28..3084b9b2a 100644 --- a/applications/system/js_app/js_app.c +++ b/applications/system/js_app/js_app.c @@ -4,7 +4,8 @@ #include "js_app_i.h" #include #include -#include +#include +#include #include #define TAG "JS app" @@ -209,8 +210,8 @@ void js_cli_execute(PipeSide* pipe, FuriString* args, void* context) { void js_app_on_system_start(void) { #ifdef SRV_CLI - Cli* cli = furi_record_open(RECORD_CLI); - cli_add_command(cli, "js", CliCommandFlagDefault, js_cli_execute, NULL); + CliRegistry* registry = furi_record_open(RECORD_CLI); + cli_registry_add_command(registry, "js", CliCommandFlagDefault, js_cli_execute, NULL); furi_record_close(RECORD_CLI); #endif } diff --git a/applications/system/updater/cli/updater_cli.c b/applications/system/updater/cli/updater_cli.c index 01949269f..bad8f0cd6 100644 --- a/applications/system/updater/cli/updater_cli.c +++ b/applications/system/updater/cli/updater_cli.c @@ -1,7 +1,8 @@ #include #include -#include +#include +#include #include #include #include @@ -106,8 +107,8 @@ static void updater_start_app(void* context, uint32_t arg) { void updater_on_system_start(void) { #ifdef SRV_CLI - Cli* cli = (Cli*)furi_record_open(RECORD_CLI); - cli_add_command(cli, "update", CliCommandFlagDefault, updater_cli_ep, NULL); + CliRegistry* registry = furi_record_open(RECORD_CLI); + cli_registry_add_command(registry, "update", CliCommandFlagDefault, updater_cli_ep, NULL); furi_record_close(RECORD_CLI); #else UNUSED(updater_cli_ep); diff --git a/lib/toolbox/SConscript b/lib/toolbox/SConscript index 8a1c4a8c5..ad368e2a1 100644 --- a/lib/toolbox/SConscript +++ b/lib/toolbox/SConscript @@ -12,6 +12,10 @@ env.Append( ], SDK_HEADERS=[ File("api_lock.h"), + File("cli/cli_ansi.h"), + File("cli/cli_command.h"), + File("cli/cli_registry.h"), + File("cli/shell/cli_shell.h"), File("compress.h"), File("manchester_decoder.h"), File("manchester_encoder.h"), diff --git a/applications/services/cli/cli_ansi.c b/lib/toolbox/cli/cli_ansi.c similarity index 100% rename from applications/services/cli/cli_ansi.c rename to lib/toolbox/cli/cli_ansi.c diff --git a/applications/services/cli/cli_ansi.h b/lib/toolbox/cli/cli_ansi.h similarity index 99% rename from applications/services/cli/cli_ansi.h rename to lib/toolbox/cli/cli_ansi.h index 20bf33d8e..04b6d7759 100644 --- a/applications/services/cli/cli_ansi.h +++ b/lib/toolbox/cli/cli_ansi.h @@ -1,6 +1,6 @@ #pragma once -#include "cli.h" +#include #ifdef __cplusplus extern "C" { diff --git a/lib/toolbox/cli/cli_command.c b/lib/toolbox/cli/cli_command.c new file mode 100644 index 000000000..a3c9ff292 --- /dev/null +++ b/lib/toolbox/cli/cli_command.c @@ -0,0 +1,17 @@ +#include "cli_command.h" +#include "cli_ansi.h" + +bool cli_is_pipe_broken_or_is_etx_next_char(PipeSide* side) { + if(pipe_state(side) == PipeStateBroken) return true; + if(!pipe_bytes_available(side)) return false; + char c = getchar(); + return c == CliKeyETX; +} + +void cli_print_usage(const char* cmd, const char* usage, const char* arg) { + furi_check(cmd); + furi_check(arg); + furi_check(usage); + + printf("%s: illegal option -- %s\r\nusage: %s %s", cmd, arg, cmd, usage); +} diff --git a/lib/toolbox/cli/cli_command.h b/lib/toolbox/cli/cli_command.h new file mode 100644 index 000000000..2d1d851d6 --- /dev/null +++ b/lib/toolbox/cli/cli_command.h @@ -0,0 +1,103 @@ +/** + * @file cli_command.h + * Command metadata and helpers + */ + +#pragma once + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define CLI_PLUGIN_API_VERSION 1 + +typedef enum { + CliCommandFlagDefault = 0, /**< Default */ + CliCommandFlagParallelSafe = (1 << 0), /**< Safe to run in parallel with other apps */ + CliCommandFlagInsomniaSafe = (1 << 1), /**< Safe to run with insomnia mode on */ + CliCommandFlagDontAttachStdio = (1 << 2), /**< Do no attach I/O pipe to thread stdio */ + CliCommandFlagUseShellThread = + (1 + << 3), /**< Don't start a separate thread to run the command in. Incompatible with DontAttachStdio */ + + // internal flags (do not set them yourselves!) + + CliCommandFlagExternal = (1 << 4), /**< The command comes from a .fal file */ +} CliCommandFlag; + +/** + * @brief CLI command execution callback pointer + * + * This callback will be called from a separate thread spawned just for your + * command. The pipe will be installed as the thread's stdio, so you can use + * `printf`, `getchar` and other standard functions to communicate with the + * user. + * + * @param [in] pipe Pipe that can be used to send and receive data. If + * `CliCommandFlagDontAttachStdio` was not set, you can + * also use standard C functions (printf, getc, etc.) to + * access this pipe. + * @param [in] args String with what was passed after the command + * @param [in] context Whatever you provided to `cli_add_command` + */ +typedef void (*CliCommandExecuteCallback)(PipeSide* pipe, FuriString* args, void* context); + +typedef struct { + char* name; + CliCommandExecuteCallback execute_callback; + CliCommandFlag flags; + size_t stack_depth; +} CliCommandDescriptor; + +/** + * @brief Configuration for locating external commands + */ +typedef struct { + const char* search_directory; // +#include + +#define TAG "cli" + +struct CliRegistry { + CliCommandTree_t commands; + FuriMutex* mutex; +}; + +CliRegistry* cli_registry_alloc(void) { + CliRegistry* registry = malloc(sizeof(CliRegistry)); + CliCommandTree_init(registry->commands); + registry->mutex = furi_mutex_alloc(FuriMutexTypeRecursive); + return registry; +} + +void cli_registry_free(CliRegistry* registry) { + furi_check(furi_mutex_acquire(registry->mutex, FuriWaitForever) == FuriStatusOk); + furi_mutex_free(registry->mutex); + CliCommandTree_clear(registry->commands); + free(registry); +} + +void cli_registry_add_command( + CliRegistry* registry, + const char* name, + CliCommandFlag flags, + CliCommandExecuteCallback callback, + void* context) { + cli_registry_add_command_ex( + registry, name, flags, callback, context, CLI_BUILTIN_COMMAND_STACK_SIZE); +} + +void cli_registry_add_command_ex( + CliRegistry* registry, + const char* name, + CliCommandFlag flags, + CliCommandExecuteCallback callback, + void* context, + size_t stack_size) { + furi_check(registry); + furi_check(name); + furi_check(callback); + + // the shell always attaches the pipe to the stdio, thus both flags can't be used at once + if(flags & CliCommandFlagUseShellThread) furi_check(!(flags & CliCommandFlagDontAttachStdio)); + + FuriString* name_str; + name_str = furi_string_alloc_set(name); + // command cannot contain spaces + furi_check(furi_string_search_char(name_str, ' ') == FURI_STRING_FAILURE); + + CliRegistryCommand command = { + .context = context, + .execute_callback = callback, + .flags = flags, + .stack_depth = stack_size, + }; + + furi_check(furi_mutex_acquire(registry->mutex, FuriWaitForever) == FuriStatusOk); + CliCommandTree_set_at(registry->commands, name_str, command); + furi_check(furi_mutex_release(registry->mutex) == FuriStatusOk); + + furi_string_free(name_str); +} + +void cli_registry_delete_command(CliRegistry* registry, const char* name) { + furi_check(registry); + FuriString* name_str; + name_str = furi_string_alloc_set(name); + furi_string_trim(name_str); + + size_t name_replace; + do { + name_replace = furi_string_replace(name_str, " ", "_"); + } while(name_replace != FURI_STRING_FAILURE); + + furi_check(furi_mutex_acquire(registry->mutex, FuriWaitForever) == FuriStatusOk); + CliCommandTree_erase(registry->commands, name_str); + furi_check(furi_mutex_release(registry->mutex) == FuriStatusOk); + + furi_string_free(name_str); +} + +bool cli_registry_get_command( + CliRegistry* registry, + FuriString* command, + CliRegistryCommand* result) { + furi_assert(registry); + furi_check(furi_mutex_acquire(registry->mutex, FuriWaitForever) == FuriStatusOk); + CliRegistryCommand* data = CliCommandTree_get(registry->commands, command); + if(data) *result = *data; + + furi_check(furi_mutex_release(registry->mutex) == FuriStatusOk); + + return !!data; +} + +void cli_registry_remove_external_commands(CliRegistry* registry) { + furi_check(registry); + furi_check(furi_mutex_acquire(registry->mutex, FuriWaitForever) == FuriStatusOk); + + // FIXME FL-3977: memory leak somewhere within this function + + CliCommandTree_t internal_cmds; + CliCommandTree_init(internal_cmds); + for + M_EACH(item, registry->commands, CliCommandTree_t) { + if(!(item->value_ptr->flags & CliCommandFlagExternal)) + CliCommandTree_set_at(internal_cmds, *item->key_ptr, *item->value_ptr); + } + CliCommandTree_move(registry->commands, internal_cmds); + + furi_check(furi_mutex_release(registry->mutex) == FuriStatusOk); +} + +void cli_registry_reload_external_commands( + CliRegistry* registry, + const CliCommandExternalConfig* config) { + furi_check(registry); + furi_check(furi_mutex_acquire(registry->mutex, FuriWaitForever) == FuriStatusOk); + FURI_LOG_D(TAG, "Reloading ext commands"); + + cli_registry_remove_external_commands(registry); + + // iterate over files in plugin directory + Storage* storage = furi_record_open(RECORD_STORAGE); + File* plugin_dir = storage_file_alloc(storage); + + if(storage_dir_open(plugin_dir, config->search_directory)) { + char plugin_filename[64]; + FuriString* plugin_name = furi_string_alloc(); + + while(storage_dir_read(plugin_dir, NULL, plugin_filename, sizeof(plugin_filename))) { + FURI_LOG_T(TAG, "Plugin: %s", plugin_filename); + furi_string_set_str(plugin_name, plugin_filename); + + furi_check(furi_string_end_with_str(plugin_name, ".fal")); + furi_string_replace_all_str(plugin_name, ".fal", ""); + furi_check(furi_string_start_with_str(plugin_name, config->fal_prefix)); + furi_string_replace_at(plugin_name, 0, strlen(config->fal_prefix), ""); + + CliRegistryCommand command = { + .context = NULL, + .execute_callback = NULL, + .flags = CliCommandFlagExternal, + }; + CliCommandTree_set_at(registry->commands, plugin_name, command); + } + + furi_string_free(plugin_name); + } + + storage_dir_close(plugin_dir); + storage_file_free(plugin_dir); + furi_record_close(RECORD_STORAGE); + + FURI_LOG_D(TAG, "Done reloading ext commands"); + furi_check(furi_mutex_release(registry->mutex) == FuriStatusOk); +} + +void cli_registry_lock(CliRegistry* registry) { + furi_assert(registry); + furi_check(furi_mutex_acquire(registry->mutex, FuriWaitForever) == FuriStatusOk); +} + +void cli_registry_unlock(CliRegistry* registry) { + furi_assert(registry); + furi_mutex_release(registry->mutex); +} + +CliCommandTree_t* cli_registry_get_commands(CliRegistry* registry) { + furi_assert(registry); + return ®istry->commands; +} diff --git a/lib/toolbox/cli/cli_registry.h b/lib/toolbox/cli/cli_registry.h new file mode 100644 index 000000000..44650e79b --- /dev/null +++ b/lib/toolbox/cli/cli_registry.h @@ -0,0 +1,92 @@ +/** + * @file cli_registry.h + * API for registering commands with a CLI shell + */ + +#pragma once + +#include +#include +#include +#include "cli_command.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct CliRegistry CliRegistry; + +/** + * @brief Allocates a `CliRegistry`. + */ +CliRegistry* cli_registry_alloc(void); + +/** + * @brief Frees a `CliRegistry`. + */ +void cli_registry_free(CliRegistry* registry); + +/** + * @brief Registers a command with the registry. Provides less options than the + * `_ex` counterpart. + * + * @param [in] registry Pointer to registry instance + * @param [in] name Command name + * @param [in] flags see CliCommandFlag + * @param [in] callback Callback function + * @param [in] context Custom context + */ +void cli_registry_add_command( + CliRegistry* registry, + const char* name, + CliCommandFlag flags, + CliCommandExecuteCallback callback, + void* context); + +/** + * @brief Registers a command with the registry. Provides more options than the + * non-`_ex` counterpart. + * + * @param [in] registry Pointer to registry instance + * @param [in] name Command name + * @param [in] flags see CliCommandFlag + * @param [in] callback Callback function + * @param [in] context Custom context + * @param [in] stack_size Thread stack size + */ +void cli_registry_add_command_ex( + CliRegistry* registry, + const char* name, + CliCommandFlag flags, + CliCommandExecuteCallback callback, + void* context, + size_t stack_size); + +/** + * @brief Deletes a cli command + * + * @param [in] registry Pointer to registry instance + * @param [in] name Command name + */ +void cli_registry_delete_command(CliRegistry* registry, const char* name); + +/** + * @brief Unregisters all external commands + * + * @param [in] registry Pointer to registry instance + */ +void cli_registry_remove_external_commands(CliRegistry* registry); + +/** + * @brief Reloads the list of externally available commands + * + * @param [in] registry Pointer to registry instance + * @param [in] config See `CliCommandExternalConfig` + */ +void cli_registry_reload_external_commands( + CliRegistry* registry, + const CliCommandExternalConfig* config); + +#ifdef __cplusplus +} +#endif diff --git a/applications/services/cli/cli_i.h b/lib/toolbox/cli/cli_registry_i.h similarity index 51% rename from applications/services/cli/cli_i.h rename to lib/toolbox/cli/cli_registry_i.h index 3e948c345..95b7c55da 100644 --- a/applications/services/cli/cli_i.h +++ b/lib/toolbox/cli/cli_registry_i.h @@ -1,5 +1,5 @@ /** - * @file cli_i.h + * @file cli_registry_i.h * Internal API for getting commands registered with the CLI */ @@ -7,21 +7,20 @@ #include #include -#include "cli.h" +#include "cli_registry.h" #ifdef __cplusplus extern "C" { #endif -#define CLI_BUILTIN_COMMAND_STACK_SIZE (3 * 1024U) -#define CLI_COMMANDS_PATH "/ext/apps_data/cli/plugins" +#define CLI_BUILTIN_COMMAND_STACK_SIZE (4 * 1024U) typedef struct { void* context; // @@ -36,10 +36,17 @@ typedef enum { } CliShellStorageEvent; struct CliShell { - Cli* cli; - FuriEventLoop* event_loop; + // Set and freed by external thread + CliShellMotd motd; + void* callback_context; PipeSide* pipe; + CliRegistry* registry; + const CliCommandExternalConfig* ext_config; + FuriThread* thread; + const char* prompt; + // Set and freed by shell thread + FuriEventLoop* event_loop; CliAnsiParser* ansi_parser; FuriEventLoopTimer* ansi_parsing_timer; @@ -51,7 +58,7 @@ struct CliShell { }; typedef struct { - CliCommand* command; + CliRegistryCommand* command; PipeSide* pipe; FuriString* args; } CliCommandThreadData; @@ -73,9 +80,62 @@ static void cli_shell_detach_pipe(CliShell* cli_shell) { furi_thread_set_stdout_callback(NULL, NULL); } -// ========= -// Execution -// ========= +// ================= +// Built-in commands +// ================= + +void cli_command_reload_external(PipeSide* pipe, FuriString* args, void* context) { + UNUSED(pipe); + UNUSED(args); + CliShell* shell = context; + furi_check(shell->ext_config); + cli_registry_reload_external_commands(shell->registry, shell->ext_config); + printf("OK!"); +} + +void cli_command_help(PipeSide* pipe, FuriString* args, void* context) { + UNUSED(pipe); + UNUSED(args); + CliShell* shell = context; + CliRegistry* registry = shell->registry; + + const size_t columns = 3; + + printf("Available commands:\r\n" ANSI_FG_GREEN); + cli_registry_lock(registry); + CliCommandTree_t* commands = cli_registry_get_commands(registry); + size_t commands_count = CliCommandTree_size(*commands); + + CliCommandTree_it_t iterator; + CliCommandTree_it(iterator, *commands); + for(size_t i = 0; i < commands_count; i++) { + const CliCommandTree_itref_t* item = CliCommandTree_cref(iterator); + printf("%-30s", furi_string_get_cstr(*item->key_ptr)); + CliCommandTree_next(iterator); + + if(i % columns == columns - 1) printf("\r\n"); + } + + if(shell->ext_config) + printf( + ANSI_RESET + "\r\nIf you added a new external command and can't see it above, run `reload_ext_cmds`"); + printf(ANSI_RESET "\r\nFind out more: https://docs.flipper.net/development/cli"); + + cli_registry_unlock(registry); +} + +void cli_command_exit(PipeSide* pipe, FuriString* args, void* context) { + UNUSED(pipe); + UNUSED(args); + CliShell* shell = context; + cli_shell_line_set_about_to_exit(shell->components[CliShellComponentLine]); + furi_event_loop_stop(shell->event_loop); +} + +// ================== +// Internal functions +// ================== static int32_t cli_command_thread(void* context) { CliCommandThreadData* thread_data = context; @@ -99,12 +159,13 @@ void cli_shell_execute_command(CliShell* cli_shell, FuriString* command) { furi_string_right(args, space + 1); PluginManager* plugin_manager = NULL; - Loader* loader = NULL; - CliCommand command_data; + Loader* loader = furi_record_open(RECORD_LOADER); + bool loader_locked = false; + CliRegistryCommand command_data; do { // find handler - if(!cli_get_command(cli_shell->cli, command_name, &command_data)) { + if(!cli_registry_get_command(cli_shell->registry, command_name, &command_data)) { printf( ANSI_FG_RED "could not find command `%s`, try `help`" ANSI_RESET, furi_string_get_cstr(command_name)); @@ -113,10 +174,14 @@ void cli_shell_execute_command(CliShell* cli_shell, FuriString* command) { // load external command if(command_data.flags & CliCommandFlagExternal) { - plugin_manager = - plugin_manager_alloc(PLUGIN_APP_ID, PLUGIN_API_VERSION, firmware_api_interface); + const CliCommandExternalConfig* ext_config = cli_shell->ext_config; + plugin_manager = plugin_manager_alloc( + ext_config->appid, CLI_PLUGIN_API_VERSION, firmware_api_interface); FuriString* path = furi_string_alloc_printf( - "%s/cli_%s.fal", CLI_COMMANDS_PATH, furi_string_get_cstr(command_name)); + "%s/%s%s.fal", + ext_config->search_directory, + ext_config->fal_prefix, + furi_string_get_cstr(command_name)); uint32_t plugin_cnt_last = plugin_manager_get_count(plugin_manager); PluginManagerError error = plugin_manager_load_single(plugin_manager, furi_string_get_cstr(path)); @@ -141,9 +206,8 @@ void cli_shell_execute_command(CliShell* cli_shell, FuriString* command) { // lock loader if(!(command_data.flags & CliCommandFlagParallelSafe)) { - loader = furi_record_open(RECORD_LOADER); - bool success = loader_lock(loader); - if(!success) { + loader_locked = loader_lock(loader); + if(!loader_locked) { printf(ANSI_FG_RED "this command cannot be run while an application is open" ANSI_RESET); break; @@ -177,13 +241,17 @@ void cli_shell_execute_command(CliShell* cli_shell, FuriString* command) { furi_string_free(args); // unlock loader - if(loader) loader_unlock(loader); + if(loader_locked) loader_unlock(loader); furi_record_close(RECORD_LOADER); // unload external command if(plugin_manager) plugin_manager_free(plugin_manager); } +const char* cli_shell_get_prompt(CliShell* cli_shell) { + return cli_shell->prompt; +} + // ============== // Event handlers // ============== @@ -210,9 +278,9 @@ static void cli_shell_storage_internal_event(FuriEventLoopObject* object, void* furi_check(furi_message_queue_get(queue, &event, 0) == FuriStatusOk); if(event == CliShellStorageEventMount) { - cli_enumerate_external_commands(cli_shell->cli); + cli_registry_reload_external_commands(cli_shell->registry, cli_shell->ext_config); } else if(event == CliShellStorageEventUnmount) { - cli_remove_external_commands(cli_shell->cli); + cli_registry_remove_external_commands(cli_shell->registry); } else { furi_crash(); } @@ -265,113 +333,97 @@ static void cli_shell_timer_expired(void* context) { cli_shell, cli_ansi_parser_feed_timeout(cli_shell->ansi_parser)); } -// ======= -// Helpers -// ======= +// =========== +// Thread code +// =========== -static CliShell* cli_shell_alloc(PipeSide* pipe) { - CliShell* cli_shell = malloc(sizeof(CliShell)); +static void cli_shell_init(CliShell* shell) { + cli_registry_add_command( + shell->registry, + "help", + CliCommandFlagUseShellThread | CliCommandFlagParallelSafe, + cli_command_help, + shell); + cli_registry_add_command( + shell->registry, + "?", + CliCommandFlagUseShellThread | CliCommandFlagParallelSafe, + cli_command_help, + shell); + cli_registry_add_command( + shell->registry, + "exit", + CliCommandFlagUseShellThread | CliCommandFlagParallelSafe, + cli_command_exit, + shell); - cli_shell->cli = furi_record_open(RECORD_CLI); - cli_shell->ansi_parser = cli_ansi_parser_alloc(); - cli_shell->pipe = pipe; + if(shell->ext_config) { + cli_registry_add_command( + shell->registry, + "reload_ext_cmds", + CliCommandFlagUseShellThread, + cli_command_reload_external, + shell); + cli_registry_reload_external_commands(shell->registry, shell->ext_config); + } - cli_shell->components[CliShellComponentLine] = cli_shell_line_alloc(cli_shell); - cli_shell->components[CliShellComponentCompletions] = cli_shell_completions_alloc( - cli_shell->cli, cli_shell, cli_shell->components[CliShellComponentLine]); + shell->components[CliShellComponentLine] = cli_shell_line_alloc(shell); + shell->components[CliShellComponentCompletions] = cli_shell_completions_alloc( + shell->registry, shell, shell->components[CliShellComponentLine]); - cli_shell->event_loop = furi_event_loop_alloc(); - cli_shell->ansi_parsing_timer = furi_event_loop_timer_alloc( - cli_shell->event_loop, cli_shell_timer_expired, FuriEventLoopTimerTypeOnce, cli_shell); + shell->ansi_parser = cli_ansi_parser_alloc(); - cli_shell_install_pipe(cli_shell); + shell->event_loop = furi_event_loop_alloc(); + shell->ansi_parsing_timer = furi_event_loop_timer_alloc( + shell->event_loop, cli_shell_timer_expired, FuriEventLoopTimerTypeOnce, shell); - cli_shell->storage_event_queue = furi_message_queue_alloc(1, sizeof(CliShellStorageEvent)); + shell->storage_event_queue = furi_message_queue_alloc(1, sizeof(CliShellStorageEvent)); furi_event_loop_subscribe_message_queue( - cli_shell->event_loop, - cli_shell->storage_event_queue, + shell->event_loop, + shell->storage_event_queue, FuriEventLoopEventIn, cli_shell_storage_internal_event, - cli_shell); - cli_shell->storage = furi_record_open(RECORD_STORAGE); - cli_shell->storage_subscription = furi_pubsub_subscribe( - storage_get_pubsub(cli_shell->storage), cli_shell_storage_event, cli_shell); + shell); + shell->storage = furi_record_open(RECORD_STORAGE); + shell->storage_subscription = + furi_pubsub_subscribe(storage_get_pubsub(shell->storage), cli_shell_storage_event, shell); - return cli_shell; + cli_shell_install_pipe(shell); } -static void cli_shell_free(CliShell* cli_shell) { - furi_pubsub_unsubscribe( - storage_get_pubsub(cli_shell->storage), cli_shell->storage_subscription); +static void cli_shell_deinit(CliShell* shell) { + furi_pubsub_unsubscribe(storage_get_pubsub(shell->storage), shell->storage_subscription); furi_record_close(RECORD_STORAGE); - furi_event_loop_unsubscribe(cli_shell->event_loop, cli_shell->storage_event_queue); - furi_message_queue_free(cli_shell->storage_event_queue); + furi_event_loop_unsubscribe(shell->event_loop, shell->storage_event_queue); + furi_message_queue_free(shell->storage_event_queue); - cli_shell_completions_free(cli_shell->components[CliShellComponentCompletions]); - cli_shell_line_free(cli_shell->components[CliShellComponentLine]); + cli_shell_completions_free(shell->components[CliShellComponentCompletions]); + cli_shell_line_free(shell->components[CliShellComponentLine]); - cli_shell_detach_pipe(cli_shell); - furi_event_loop_timer_free(cli_shell->ansi_parsing_timer); - furi_event_loop_free(cli_shell->event_loop); - pipe_free(cli_shell->pipe); - cli_ansi_parser_free(cli_shell->ansi_parser); - furi_record_close(RECORD_CLI); - free(cli_shell); -} - -static void cli_shell_motd(void) { - printf(ANSI_FLIPPER_BRAND_ORANGE - "\r\n" - " _.-------.._ -,\r\n" - " .-\"```\"--..,,_/ /`-, -, \\ \r\n" - " .:\" /:/ /'\\ \\ ,_..., `. | |\r\n" - " / ,----/:/ /`\\ _\\~`_-\"` _;\r\n" - " ' / /`\"\"\"'\\ \\ \\.~`_-' ,-\"'/ \r\n" - " | | | 0 | | .-' ,/` /\r\n" - " | ,..\\ \\ ,.-\"` ,/` /\r\n" - " ; : `/`\"\"\\` ,/--==,/-----,\r\n" - " | `-...| -.___-Z:_______J...---;\r\n" - " : ` _-'\r\n" - " _L_ _ ___ ___ ___ ___ ____--\"`___ _ ___\r\n" - "| __|| | |_ _|| _ \\| _ \\| __|| _ \\ / __|| | |_ _|\r\n" - "| _| | |__ | | | _/| _/| _| | / | (__ | |__ | |\r\n" - "|_| |____||___||_| |_| |___||_|_\\ \\___||____||___|\r\n" - "\r\n" ANSI_FG_BR_WHITE "Welcome to Flipper Zero Command Line Interface!\r\n" - "Read the manual: https://docs.flipper.net/development/cli\r\n" - "Run `help` or `?` to list available commands\r\n" - "\r\n" ANSI_RESET); - - const Version* firmware_version = furi_hal_version_get_firmware_version(); - if(firmware_version) { - printf( - "Firmware version: %s %s (%s%s built on %s)\r\n", - version_get_gitbranch(firmware_version), - version_get_version(firmware_version), - version_get_githash(firmware_version), - version_get_dirty_flag(firmware_version) ? "-dirty" : "", - version_get_builddate(firmware_version)); - } + cli_shell_detach_pipe(shell); + furi_event_loop_timer_free(shell->ansi_parsing_timer); + furi_event_loop_free(shell->event_loop); + cli_ansi_parser_free(shell->ansi_parser); } static int32_t cli_shell_thread(void* context) { - PipeSide* pipe = context; + CliShell* shell = context; // Sometimes, the other side closes the pipe even before our thread is started. Although the // rest of the code will eventually find this out if this check is removed, there's no point in // wasting time. - if(pipe_state(pipe) == PipeStateBroken) return 0; - - CliShell* cli_shell = cli_shell_alloc(pipe); + if(pipe_state(shell->pipe) == PipeStateBroken) return 0; + cli_shell_init(shell); FURI_LOG_D(TAG, "Started"); - cli_shell_motd(); - cli_shell_line_prompt(cli_shell->components[CliShellComponentLine]); - furi_event_loop_run(cli_shell->event_loop); + shell->motd(shell->callback_context); + cli_shell_line_prompt(shell->components[CliShellComponentLine]); + + furi_event_loop_run(shell->event_loop); FURI_LOG_D(TAG, "Stopped"); - - cli_shell_free(cli_shell); + cli_shell_deinit(shell); return 0; } @@ -379,9 +431,48 @@ static int32_t cli_shell_thread(void* context) { // Public API // ========== -FuriThread* cli_shell_start(PipeSide* pipe) { - FuriThread* thread = - furi_thread_alloc_ex("CliShell", CLI_SHELL_STACK_SIZE, cli_shell_thread, pipe); - furi_thread_start(thread); - return thread; +CliShell* cli_shell_alloc( + CliShellMotd motd, + void* context, + PipeSide* pipe, + CliRegistry* registry, + const CliCommandExternalConfig* ext_config) { + furi_check(motd); + furi_check(pipe); + furi_check(registry); + + CliShell* shell = malloc(sizeof(CliShell)); + *shell = (CliShell){ + .motd = motd, + .callback_context = context, + .pipe = pipe, + .registry = registry, + .ext_config = ext_config, + }; + + shell->thread = + furi_thread_alloc_ex("CliShell", CLI_SHELL_STACK_SIZE, cli_shell_thread, shell); + + return shell; +} + +void cli_shell_free(CliShell* shell) { + furi_check(shell); + furi_thread_free(shell->thread); + free(shell); +} + +void cli_shell_start(CliShell* shell) { + furi_check(shell); + furi_thread_start(shell->thread); +} + +void cli_shell_join(CliShell* shell) { + furi_check(shell); + furi_thread_join(shell->thread); +} + +void cli_shell_set_prompt(CliShell* shell, const char* prompt) { + furi_check(shell); + shell->prompt = prompt; } diff --git a/lib/toolbox/cli/shell/cli_shell.h b/lib/toolbox/cli/shell/cli_shell.h new file mode 100644 index 000000000..74f71273e --- /dev/null +++ b/lib/toolbox/cli/shell/cli_shell.h @@ -0,0 +1,75 @@ +#pragma once + +#include +#include +#include "../cli_registry.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#define CLI_SHELL_STACK_SIZE (4 * 1024U) + +typedef struct CliShell CliShell; + +/** + * Called from the shell thread to print the Message of the Day when the shell + * is started. + */ +typedef void (*CliShellMotd)(void* context); + +/** + * @brief Allocates a shell + * + * @param [in] motd Message of the Day callback + * @param [in] context Callback context + * @param [in] pipe Pipe side to be used by the shell + * @param [in] registry Command registry + * @param [in] ext_config External command configuration. See + * `CliCommandExternalConfig`. May be NULL if support for + * external commands is not required. + * + * @return Shell instance + */ +CliShell* cli_shell_alloc( + CliShellMotd motd, + void* context, + PipeSide* pipe, + CliRegistry* registry, + const CliCommandExternalConfig* ext_config); + +/** + * @brief Frees a shell + * + * @param [in] shell Shell instance + */ +void cli_shell_free(CliShell* shell); + +/** + * @brief Starts a shell + * + * The shell runs in a separate thread. This call is non-blocking. + * + * @param [in] shell Shell instance + */ +void cli_shell_start(CliShell* shell); + +/** + * @brief Joins the shell thread + * + * @warning This call is blocking. + * + * @param [in] shell Shell instance + */ +void cli_shell_join(CliShell* shell); + +/** + * @brief Sets optional text before prompt (`>:`) + * + * @param [in] shell Shell instance + */ +void cli_shell_set_prompt(CliShell* shell, const char* prompt); + +#ifdef __cplusplus +} +#endif diff --git a/applications/services/cli/shell/cli_shell_completions.c b/lib/toolbox/cli/shell/cli_shell_completions.c similarity index 97% rename from applications/services/cli/shell/cli_shell_completions.c rename to lib/toolbox/cli/shell/cli_shell_completions.c index 0b32c18a2..823f91fb9 100644 --- a/applications/services/cli/shell/cli_shell_completions.c +++ b/lib/toolbox/cli/shell/cli_shell_completions.c @@ -4,7 +4,7 @@ ARRAY_DEF(CommandCompletions, FuriString*, FURI_STRING_OPLIST); // -V524 #define M_OPL_CommandCompletions_t() ARRAY_OPLIST(CommandCompletions) struct CliShellCompletions { - Cli* cli; + CliRegistry* registry; CliShell* shell; CliShellLine* line; CommandCompletions_t variants; @@ -45,10 +45,11 @@ typedef struct { // Public API // ========== -CliShellCompletions* cli_shell_completions_alloc(Cli* cli, CliShell* shell, CliShellLine* line) { +CliShellCompletions* + cli_shell_completions_alloc(CliRegistry* registry, CliShell* shell, CliShellLine* line) { CliShellCompletions* completions = malloc(sizeof(CliShellCompletions)); - completions->cli = cli; + completions->registry = registry; completions->shell = shell; completions->line = line; CommandCompletions_init(completions->variants); @@ -108,8 +109,9 @@ void cli_shell_completions_fill_variants(CliShellCompletions* completions) { furi_string_left(input, segment.length); if(segment.type == CliShellCompletionSegmentTypeCommand) { - cli_lock_commands(completions->cli); - CliCommandTree_t* commands = cli_get_commands(completions->cli); + CliRegistry* registry = completions->registry; + cli_registry_lock(registry); + CliCommandTree_t* commands = cli_registry_get_commands(registry); for M_EACH(registered_command, *commands, CliCommandTree_t) { FuriString* command_name = *registered_command->key_ptr; @@ -117,7 +119,7 @@ void cli_shell_completions_fill_variants(CliShellCompletions* completions) { CommandCompletions_push_back(completions->variants, command_name); } } - cli_unlock_commands(completions->cli); + cli_registry_unlock(registry); } else { // support removed, might reimplement in the future diff --git a/applications/services/cli/shell/cli_shell_completions.h b/lib/toolbox/cli/shell/cli_shell_completions.h similarity index 67% rename from applications/services/cli/shell/cli_shell_completions.h rename to lib/toolbox/cli/shell/cli_shell_completions.h index 6353bde71..d49a1982d 100644 --- a/applications/services/cli/shell/cli_shell_completions.h +++ b/lib/toolbox/cli/shell/cli_shell_completions.h @@ -4,8 +4,8 @@ #include #include "cli_shell_i.h" #include "cli_shell_line.h" -#include "../cli.h" -#include "../cli_i.h" +#include "../cli_registry.h" +#include "../cli_registry_i.h" #ifdef __cplusplus extern "C" { @@ -13,7 +13,8 @@ extern "C" { typedef struct CliShellCompletions CliShellCompletions; -CliShellCompletions* cli_shell_completions_alloc(Cli* cli, CliShell* shell, CliShellLine* line); +CliShellCompletions* + cli_shell_completions_alloc(CliRegistry* registry, CliShell* shell, CliShellLine* line); void cli_shell_completions_free(CliShellCompletions* completions); diff --git a/applications/services/cli/shell/cli_shell_i.h b/lib/toolbox/cli/shell/cli_shell_i.h similarity index 91% rename from applications/services/cli/shell/cli_shell_i.h rename to lib/toolbox/cli/shell/cli_shell_i.h index e8eae92c6..0b676b7de 100644 --- a/applications/services/cli/shell/cli_shell_i.h +++ b/lib/toolbox/cli/shell/cli_shell_i.h @@ -27,6 +27,8 @@ typedef struct { void cli_shell_execute_command(CliShell* cli_shell, FuriString* command); +const char* cli_shell_get_prompt(CliShell* cli_shell); + #ifdef __cplusplus } #endif diff --git a/applications/services/cli/shell/cli_shell_line.c b/lib/toolbox/cli/shell/cli_shell_line.c similarity index 95% rename from applications/services/cli/shell/cli_shell_line.c rename to lib/toolbox/cli/shell/cli_shell_line.c index 959cd0b3b..4826ba252 100644 --- a/applications/services/cli/shell/cli_shell_line.c +++ b/lib/toolbox/cli/shell/cli_shell_line.c @@ -8,6 +8,7 @@ struct CliShellLine { FuriString* history[HISTORY_DEPTH]; size_t history_entries; CliShell* shell; + bool about_to_exit; }; // ========== @@ -39,14 +40,16 @@ FuriString* cli_shell_line_get_editing(CliShellLine* line) { return line->history[0]; } -size_t cli_shell_line_prompt_length(CliShellLine* line) { - UNUSED(line); - return strlen(">: "); -} - void cli_shell_line_format_prompt(CliShellLine* line, char* buf, size_t length) { UNUSED(line); - snprintf(buf, length - 1, ">: "); + const char* prompt = cli_shell_get_prompt(line->shell); + snprintf(buf, length - 1, "%s>: ", prompt ? prompt : ""); +} + +size_t cli_shell_line_prompt_length(CliShellLine* line) { + char buffer[128]; + cli_shell_line_format_prompt(line, buffer, sizeof(buffer)); + return strlen(buffer); } void cli_shell_line_prompt(CliShellLine* line) { @@ -65,6 +68,10 @@ void cli_shell_line_ensure_not_overwriting_history(CliShellLine* line) { } } +void cli_shell_line_set_about_to_exit(CliShellLine* line) { + line->about_to_exit = true; +} + size_t cli_shell_line_get_line_position(CliShellLine* line) { return line->line_position; } @@ -188,8 +195,7 @@ static bool cli_shell_line_input_cr(CliKeyCombo combo, void* context) { printf("\r\n"); cli_shell_execute_command(line->shell, command_copy); furi_string_free(command_copy); - - cli_shell_line_prompt(line); + if(!line->about_to_exit) cli_shell_line_prompt(line); return true; } @@ -202,10 +208,13 @@ static bool cli_shell_line_input_up_down(CliKeyCombo combo, void* context) { // print prompt with selected command if(new_pos != line->history_position) { + char prompt[64]; + cli_shell_line_format_prompt(line, prompt, sizeof(prompt)); line->history_position = new_pos; FuriString* command = cli_shell_line_get_selected(line); printf( - ANSI_CURSOR_HOR_POS("1") ">: %s" ANSI_ERASE_LINE(ANSI_ERASE_FROM_CURSOR_TO_END), + ANSI_CURSOR_HOR_POS("1") "%s%s" ANSI_ERASE_LINE(ANSI_ERASE_FROM_CURSOR_TO_END), + prompt, furi_string_get_cstr(command)); fflush(stdout); line->line_position = furi_string_size(command); diff --git a/applications/services/cli/shell/cli_shell_line.h b/lib/toolbox/cli/shell/cli_shell_line.h similarity index 94% rename from applications/services/cli/shell/cli_shell_line.h rename to lib/toolbox/cli/shell/cli_shell_line.h index 1e4b9e32a..e40a12bd6 100644 --- a/applications/services/cli/shell/cli_shell_line.h +++ b/lib/toolbox/cli/shell/cli_shell_line.h @@ -33,6 +33,8 @@ void cli_shell_line_set_line_position(CliShellLine* line, size_t position); */ void cli_shell_line_ensure_not_overwriting_history(CliShellLine* line); +void cli_shell_line_set_about_to_exit(CliShellLine* line); + extern CliShellKeyComboSet cli_shell_line_key_combo_set; #ifdef __cplusplus diff --git a/targets/f18/api_symbols.csv b/targets/f18/api_symbols.csv index 72512a46f..ce47fe5c2 100644 --- a/targets/f18/api_symbols.csv +++ b/targets/f18/api_symbols.csv @@ -1,9 +1,7 @@ entry,status,name,type,params -Version,+,84.1,, +Version,+,85.0,, Header,+,applications/services/bt/bt_service/bt.h,, Header,+,applications/services/bt/bt_service/bt_keys_storage.h,, -Header,+,applications/services/cli/cli.h,, -Header,+,applications/services/cli/cli_ansi.h,, Header,+,applications/services/cli/cli_vcp.h,, Header,+,applications/services/dialogs/dialogs.h,, Header,+,applications/services/dolphin/dolphin.h,, @@ -153,6 +151,10 @@ Header,+,lib/stm32wb_hal/Inc/stm32wbxx_ll_wwdg.h,, Header,+,lib/toolbox/api_lock.h,, Header,+,lib/toolbox/args.h,, Header,+,lib/toolbox/bit_buffer.h,, +Header,+,lib/toolbox/cli/cli_ansi.h,, +Header,+,lib/toolbox/cli/cli_command.h,, +Header,+,lib/toolbox/cli/cli_registry.h,, +Header,+,lib/toolbox/cli/shell/cli_shell.h,, Header,+,lib/toolbox/compress.h,, Header,+,lib/toolbox/crc32_calc.h,, Header,+,lib/toolbox/dir_walk.h,, @@ -779,17 +781,24 @@ Function,-,ceill,long double,long double Function,-,cfree,void,void* Function,-,clearerr,void,FILE* Function,-,clearerr_unlocked,void,FILE* -Function,+,cli_add_command,void,"Cli*, const char*, CliCommandFlag, CliExecuteCallback, void*" -Function,+,cli_add_command_ex,void,"Cli*, const char*, CliCommandFlag, CliExecuteCallback, void*, size_t" Function,+,cli_ansi_parser_alloc,CliAnsiParser*, Function,+,cli_ansi_parser_feed,CliAnsiParserResult,"CliAnsiParser*, char" Function,+,cli_ansi_parser_feed_timeout,CliAnsiParserResult,CliAnsiParser* Function,+,cli_ansi_parser_free,void,CliAnsiParser* -Function,+,cli_delete_command,void,"Cli*, const char*" -Function,+,cli_enumerate_external_commands,void,Cli* Function,+,cli_is_pipe_broken_or_is_etx_next_char,_Bool,PipeSide* Function,+,cli_print_usage,void,"const char*, const char*, const char*" -Function,+,cli_remove_external_commands,void,Cli* +Function,+,cli_registry_add_command,void,"CliRegistry*, const char*, CliCommandFlag, CliCommandExecuteCallback, void*" +Function,+,cli_registry_add_command_ex,void,"CliRegistry*, const char*, CliCommandFlag, CliCommandExecuteCallback, void*, size_t" +Function,+,cli_registry_alloc,CliRegistry*, +Function,+,cli_registry_delete_command,void,"CliRegistry*, const char*" +Function,+,cli_registry_free,void,CliRegistry* +Function,+,cli_registry_reload_external_commands,void,"CliRegistry*, const CliCommandExternalConfig*" +Function,+,cli_registry_remove_external_commands,void,CliRegistry* +Function,+,cli_shell_alloc,CliShell*,"CliShellMotd, void*, PipeSide*, CliRegistry*, const CliCommandExternalConfig*" +Function,+,cli_shell_free,void,CliShell* +Function,+,cli_shell_join,void,CliShell* +Function,+,cli_shell_set_prompt,void,"CliShell*, const char*" +Function,+,cli_shell_start,void,CliShell* Function,+,cli_vcp_disable,void,CliVcp* Function,+,cli_vcp_enable,void,CliVcp* Function,+,composite_api_resolver_add,void,"CompositeApiResolver*, const ElfApiInterface*" diff --git a/targets/f7/api_symbols.csv b/targets/f7/api_symbols.csv index 73ad2dcd5..e19281e2a 100644 --- a/targets/f7/api_symbols.csv +++ b/targets/f7/api_symbols.csv @@ -1,10 +1,8 @@ entry,status,name,type,params -Version,+,84.1,, +Version,+,85.0,, Header,+,applications/drivers/subghz/cc1101_ext/cc1101_ext_interconnect.h,, Header,+,applications/services/bt/bt_service/bt.h,, Header,+,applications/services/bt/bt_service/bt_keys_storage.h,, -Header,+,applications/services/cli/cli.h,, -Header,+,applications/services/cli/cli_ansi.h,, Header,+,applications/services/cli/cli_vcp.h,, Header,+,applications/services/dialogs/dialogs.h,, Header,+,applications/services/dolphin/dolphin.h,, @@ -225,6 +223,10 @@ Header,+,lib/subghz/transmitter.h,, Header,+,lib/toolbox/api_lock.h,, Header,+,lib/toolbox/args.h,, Header,+,lib/toolbox/bit_buffer.h,, +Header,+,lib/toolbox/cli/cli_ansi.h,, +Header,+,lib/toolbox/cli/cli_command.h,, +Header,+,lib/toolbox/cli/cli_registry.h,, +Header,+,lib/toolbox/cli/shell/cli_shell.h,, Header,+,lib/toolbox/compress.h,, Header,+,lib/toolbox/crc32_calc.h,, Header,+,lib/toolbox/dir_walk.h,, @@ -856,17 +858,24 @@ Function,-,ceill,long double,long double Function,-,cfree,void,void* Function,-,clearerr,void,FILE* Function,-,clearerr_unlocked,void,FILE* -Function,+,cli_add_command,void,"Cli*, const char*, CliCommandFlag, CliExecuteCallback, void*" -Function,+,cli_add_command_ex,void,"Cli*, const char*, CliCommandFlag, CliExecuteCallback, void*, size_t" Function,+,cli_ansi_parser_alloc,CliAnsiParser*, Function,+,cli_ansi_parser_feed,CliAnsiParserResult,"CliAnsiParser*, char" Function,+,cli_ansi_parser_feed_timeout,CliAnsiParserResult,CliAnsiParser* Function,+,cli_ansi_parser_free,void,CliAnsiParser* -Function,+,cli_delete_command,void,"Cli*, const char*" -Function,+,cli_enumerate_external_commands,void,Cli* Function,+,cli_is_pipe_broken_or_is_etx_next_char,_Bool,PipeSide* Function,+,cli_print_usage,void,"const char*, const char*, const char*" -Function,+,cli_remove_external_commands,void,Cli* +Function,+,cli_registry_add_command,void,"CliRegistry*, const char*, CliCommandFlag, CliCommandExecuteCallback, void*" +Function,+,cli_registry_add_command_ex,void,"CliRegistry*, const char*, CliCommandFlag, CliCommandExecuteCallback, void*, size_t" +Function,+,cli_registry_alloc,CliRegistry*, +Function,+,cli_registry_delete_command,void,"CliRegistry*, const char*" +Function,+,cli_registry_free,void,CliRegistry* +Function,+,cli_registry_reload_external_commands,void,"CliRegistry*, const CliCommandExternalConfig*" +Function,+,cli_registry_remove_external_commands,void,CliRegistry* +Function,+,cli_shell_alloc,CliShell*,"CliShellMotd, void*, PipeSide*, CliRegistry*, const CliCommandExternalConfig*" +Function,+,cli_shell_free,void,CliShell* +Function,+,cli_shell_join,void,CliShell* +Function,+,cli_shell_set_prompt,void,"CliShell*, const char*" +Function,+,cli_shell_start,void,CliShell* Function,+,cli_vcp_disable,void,CliVcp* Function,+,cli_vcp_enable,void,CliVcp* Function,+,composite_api_resolver_add,void,"CompositeApiResolver*, const ElfApiInterface*" From dac1457f0a2a4befc4d8e3441adc02d98b896da9 Mon Sep 17 00:00:00 2001 From: Anna Antonenko Date: Sat, 5 Apr 2025 03:17:30 +0400 Subject: [PATCH 190/268] [FL-3963] Move JS modules to new arg parser (#4139) * js: value destructuring and tests * js: temporary fix to see size impact * js_val: reduce code size 1 * i may be stupid. * test: js_value args * Revert "js: temporary fix to see size impact" This reverts commit f51d726dbafc4300d3552020de1c3b8f9ecd3ac1. * pvs: silence warnings * style: formatting * pvs: silence warnings? * pvs: silence warnings?? * js_value: redesign declaration types for less code * js: temporary fix to see size impact * style: formatting * pvs: fix helpful warnings * js_value: reduce .rodata size * pvs: fix helpful warning * js_value: reduce code size 1 * fix build error * style: format * Revert "js: temporary fix to see size impact" This reverts commit d6a46f01794132e882e03fd273dec24386a4f8ba. * style: format * js: move to new arg parser * style: format --------- Co-authored-by: hedger --- applications/system/js_app/js_modules.c | 26 +- applications/system/js_app/js_modules.h | 228 +------------- applications/system/js_app/js_thread.c | 19 +- applications/system/js_app/js_thread_i.h | 8 + .../modules/js_event_loop/js_event_loop.c | 52 +++- applications/system/js_app/modules/js_gpio.c | 196 +++++++----- .../js_app/modules/js_gui/file_picker.c | 8 +- .../system/js_app/modules/js_gui/icon.c | 9 +- .../system/js_app/modules/js_gui/js_gui.c | 90 ++++-- .../system/js_app/modules/js_serial.c | 283 +++++++----------- .../system/js_app/modules/js_storage.c | 187 ++++++++---- .../js_app/plugin_api/app_api_table_i.h | 17 +- .../system/js_app/plugin_api/js_plugin_api.h | 22 -- 13 files changed, 525 insertions(+), 620 deletions(-) delete mode 100644 applications/system/js_app/plugin_api/js_plugin_api.h diff --git a/applications/system/js_app/js_modules.c b/applications/system/js_app/js_modules.c index a8480e6a2..f9c08058f 100644 --- a/applications/system/js_app/js_modules.c +++ b/applications/system/js_app/js_modules.c @@ -202,12 +202,15 @@ static JsSdkCompatStatus return JsSdkCompatStatusCompatible; } -#define JS_SDK_COMPAT_ARGS \ - int32_t major, minor; \ - JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_INT32(&major), JS_ARG_INT32(&minor)); +static const JsValueDeclaration js_sdk_version_arg_list[] = { + JS_VALUE_SIMPLE(JsValueTypeInt32), + JS_VALUE_SIMPLE(JsValueTypeInt32), +}; +static const JsValueArguments js_sdk_version_args = JS_VALUE_ARGS(js_sdk_version_arg_list); void js_sdk_compatibility_status(struct mjs* mjs) { - JS_SDK_COMPAT_ARGS; + int32_t major, minor; + JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_sdk_version_args, &major, &minor); JsSdkCompatStatus status = js_internal_sdk_compatibility_status(major, minor); switch(status) { case JsSdkCompatStatusCompatible: @@ -223,7 +226,8 @@ void js_sdk_compatibility_status(struct mjs* mjs) { } void js_is_sdk_compatible(struct mjs* mjs) { - JS_SDK_COMPAT_ARGS; + int32_t major, minor; + JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_sdk_version_args, &major, &minor); JsSdkCompatStatus status = js_internal_sdk_compatibility_status(major, minor); mjs_return(mjs, mjs_mk_boolean(mjs, status == JsSdkCompatStatusCompatible)); } @@ -246,7 +250,8 @@ static bool js_internal_compat_ask_user(const char* message) { } void js_check_sdk_compatibility(struct mjs* mjs) { - JS_SDK_COMPAT_ARGS; + int32_t major, minor; + JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_sdk_version_args, &major, &minor); JsSdkCompatStatus status = js_internal_sdk_compatibility_status(major, minor); if(status != JsSdkCompatStatusCompatible) { FURI_LOG_E( @@ -300,15 +305,20 @@ static bool js_internal_supports_all_of(struct mjs* mjs, mjs_val_t feature_arr) return true; } +static const JsValueDeclaration js_sdk_features_arg_list[] = { + JS_VALUE_SIMPLE(JsValueTypeAnyArray), +}; +static const JsValueArguments js_sdk_features_args = JS_VALUE_ARGS(js_sdk_features_arg_list); + void js_does_sdk_support(struct mjs* mjs) { mjs_val_t features; - JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_ARR(&features)); + JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_sdk_features_args, &features); mjs_return(mjs, mjs_mk_boolean(mjs, js_internal_supports_all_of(mjs, features))); } void js_check_sdk_features(struct mjs* mjs) { mjs_val_t features; - JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_ARR(&features)); + JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_sdk_features_args, &features); if(!js_internal_supports_all_of(mjs, features)) { FURI_LOG_E(TAG, "Script requests unsupported features"); diff --git a/applications/system/js_app/js_modules.h b/applications/system/js_app/js_modules.h index fb1cca915..c6f72bbe2 100644 --- a/applications/system/js_app/js_modules.h +++ b/applications/system/js_app/js_modules.h @@ -7,6 +7,10 @@ #include #include +#ifdef __cplusplus +extern "C" { +#endif + #define PLUGIN_APP_ID "js" #define PLUGIN_API_VERSION 1 @@ -64,226 +68,6 @@ typedef enum { JsForeignMagic_JsEventLoopContract, } JsForeignMagic; -// Are you tired of your silly little JS+C glue code functions being 75% -// argument validation code and 25% actual logic? Introducing: ASS (Argument -// Schema for Scripts)! ASS is a set of macros that reduce the typical -// boilerplate code of "check argument count, get arguments, validate arguments, -// extract C values from arguments" down to just one line! - -/** - * When passed as the second argument to `JS_FETCH_ARGS_OR_RETURN`, signifies - * that the function requires exactly as many arguments as were specified. - */ -#define JS_EXACTLY == -/** - * When passed as the second argument to `JS_FETCH_ARGS_OR_RETURN`, signifies - * that the function requires at least as many arguments as were specified. - */ -#define JS_AT_LEAST >= - -typedef struct { - const char* name; - size_t value; -} JsEnumMapping; - -#define JS_ENUM_MAP(var_name, ...) \ - static const JsEnumMapping var_name##_mapping[] = { \ - {NULL, sizeof(var_name)}, \ - __VA_ARGS__, \ - {NULL, 0}, \ - }; - -typedef struct { - const char* name; - size_t offset; -} JsObjectMapping; - -#define JS_OBJ_MAP(var_name, ...) \ - static const JsObjectMapping var_name##_mapping[] = { \ - __VA_ARGS__, \ - {NULL, 0}, \ - }; - -typedef struct { - void* out; - int (*validator)(mjs_val_t); - void (*converter)(struct mjs*, mjs_val_t*, void* out, const void* extra); - const char* expected_type; - bool (*extended_validator)(struct mjs*, mjs_val_t, const void* extra); - const void* extra_data; -} _js_arg_decl; - -static inline void _js_to_int32(struct mjs* mjs, mjs_val_t* in, void* out, const void* extra) { - UNUSED(extra); - *(int32_t*)out = mjs_get_int32(mjs, *in); -} -#define JS_ARG_INT32(out) ((_js_arg_decl){out, mjs_is_number, _js_to_int32, "number", NULL, NULL}) - -static inline void _js_to_ptr(struct mjs* mjs, mjs_val_t* in, void* out, const void* extra) { - UNUSED(extra); - *(void**)out = mjs_get_ptr(mjs, *in); -} -#define JS_ARG_PTR(out) \ - ((_js_arg_decl){out, mjs_is_foreign, _js_to_ptr, "opaque pointer", NULL, NULL}) - -static inline void _js_to_string(struct mjs* mjs, mjs_val_t* in, void* out, const void* extra) { - UNUSED(extra); - *(const char**)out = mjs_get_string(mjs, in, NULL); -} -#define JS_ARG_STR(out) ((_js_arg_decl){out, mjs_is_string, _js_to_string, "string", NULL, NULL}) - -static inline void _js_to_bool(struct mjs* mjs, mjs_val_t* in, void* out, const void* extra) { - UNUSED(extra); - *(bool*)out = !!mjs_get_bool(mjs, *in); -} -#define JS_ARG_BOOL(out) ((_js_arg_decl){out, mjs_is_boolean, _js_to_bool, "boolean", NULL, NULL}) - -static inline void _js_passthrough(struct mjs* mjs, mjs_val_t* in, void* out, const void* extra) { - UNUSED(extra); - UNUSED(mjs); - *(mjs_val_t*)out = *in; -} -#define JS_ARG_ANY(out) ((_js_arg_decl){out, NULL, _js_passthrough, "any", NULL, NULL}) -#define JS_ARG_OBJ(out) ((_js_arg_decl){out, mjs_is_object, _js_passthrough, "any", NULL, NULL}) -#define JS_ARG_FN(out) \ - ((_js_arg_decl){out, mjs_is_function, _js_passthrough, "function", NULL, NULL}) -#define JS_ARG_ARR(out) ((_js_arg_decl){out, mjs_is_array, _js_passthrough, "array", NULL, NULL}) - -static inline bool _js_validate_struct(struct mjs* mjs, mjs_val_t val, const void* extra) { - JsForeignMagic expected_magic = (JsForeignMagic)(size_t)extra; - JsForeignMagic struct_magic = *(JsForeignMagic*)mjs_get_ptr(mjs, val); - return struct_magic == expected_magic; -} -#define JS_ARG_STRUCT(type, out) \ - ((_js_arg_decl){ \ - out, \ - mjs_is_foreign, \ - _js_to_ptr, \ - #type, \ - _js_validate_struct, \ - (void*)JsForeignMagic##_##type}) - -static inline bool _js_validate_obj_w_struct(struct mjs* mjs, mjs_val_t val, const void* extra) { - JsForeignMagic expected_magic = (JsForeignMagic)(size_t)extra; - JsForeignMagic struct_magic = *(JsForeignMagic*)JS_GET_INST(mjs, val); - return struct_magic == expected_magic; -} -#define JS_ARG_OBJ_WITH_STRUCT(type, out) \ - ((_js_arg_decl){ \ - out, \ - mjs_is_object, \ - _js_passthrough, \ - #type, \ - _js_validate_obj_w_struct, \ - (void*)JsForeignMagic##_##type}) - -static inline bool _js_validate_enum(struct mjs* mjs, mjs_val_t val, const void* extra) { - for(const JsEnumMapping* mapping = (JsEnumMapping*)extra + 1; mapping->name; mapping++) - if(strcmp(mapping->name, mjs_get_string(mjs, &val, NULL)) == 0) return true; - return false; -} -static inline void - _js_convert_enum(struct mjs* mjs, mjs_val_t* val, void* out, const void* extra) { - const JsEnumMapping* mapping = (JsEnumMapping*)extra; - size_t size = mapping->value; // get enum size from first entry - for(mapping++; mapping->name; mapping++) { - if(strcmp(mapping->name, mjs_get_string(mjs, val, NULL)) == 0) { - if(size == 1) - *(uint8_t*)out = mapping->value; - else if(size == 2) - *(uint16_t*)out = mapping->value; - else if(size == 4) - *(uint32_t*)out = mapping->value; - else if(size == 8) - *(uint64_t*)out = mapping->value; - return; - } - } - // unreachable, thanks to _js_validate_enum -} -#define JS_ARG_ENUM(var_name, name) \ - ((_js_arg_decl){ \ - &var_name, \ - mjs_is_string, \ - _js_convert_enum, \ - name " enum", \ - _js_validate_enum, \ - var_name##_mapping}) - -static inline bool _js_validate_object(struct mjs* mjs, mjs_val_t val, const void* extra) { - for(const JsObjectMapping* mapping = (JsObjectMapping*)extra; mapping->name; mapping++) - if(mjs_get(mjs, val, mapping->name, ~0) == MJS_UNDEFINED) return false; - return true; -} -static inline void - _js_convert_object(struct mjs* mjs, mjs_val_t* val, void* out, const void* extra) { - const JsObjectMapping* mapping = (JsObjectMapping*)extra; - for(; mapping->name; mapping++) { - mjs_val_t field_val = mjs_get(mjs, *val, mapping->name, ~0); - *(mjs_val_t*)((uint8_t*)out + mapping->offset) = field_val; - } -} -#define JS_ARG_OBJECT(var_name, name) \ - ((_js_arg_decl){ \ - &var_name, \ - mjs_is_object, \ - _js_convert_object, \ - name " object", \ - _js_validate_object, \ - var_name##_mapping}) - -/** - * @brief Validates and converts a JS value with a declarative interface - * - * Example: `int32_t my_value; JS_CONVERT_OR_RETURN(mjs, &mjs_val, JS_ARG_INT32(&my_value), "value source");` - * - * @warning This macro executes `return;` by design in case of a validation failure - */ -#define JS_CONVERT_OR_RETURN(mjs, value, decl, source, ...) \ - if(decl.validator) \ - if(!decl.validator(*value)) \ - JS_ERROR_AND_RETURN( \ - mjs, \ - MJS_BAD_ARGS_ERROR, \ - source ": expected %s", \ - ##__VA_ARGS__, \ - decl.expected_type); \ - if(decl.extended_validator) \ - if(!decl.extended_validator(mjs, *value, decl.extra_data)) \ - JS_ERROR_AND_RETURN( \ - mjs, \ - MJS_BAD_ARGS_ERROR, \ - source ": expected %s", \ - ##__VA_ARGS__, \ - decl.expected_type); \ - decl.converter(mjs, value, decl.out, decl.extra_data); - -//-V:JS_FETCH_ARGS_OR_RETURN:1008 -/** - * @brief Fetches and validates the arguments passed to a JS function - * - * Example: `int32_t my_arg; JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_INT32(&my_arg));` - * - * @warning This macro executes `return;` by design in case of an argument count - * mismatch or a validation failure - */ -#define JS_FETCH_ARGS_OR_RETURN(mjs, arg_operator, ...) \ - _js_arg_decl _js_args[] = {__VA_ARGS__}; \ - int _js_arg_cnt = COUNT_OF(_js_args); \ - mjs_val_t _js_arg_vals[_js_arg_cnt]; \ - if(!(mjs_nargs(mjs) arg_operator _js_arg_cnt)) \ - JS_ERROR_AND_RETURN( \ - mjs, \ - MJS_BAD_ARGS_ERROR, \ - "expected %s%d arguments, got %d", \ - #arg_operator, \ - _js_arg_cnt, \ - mjs_nargs(mjs)); \ - for(int _i = 0; _i < _js_arg_cnt; _i++) { \ - _js_arg_vals[_i] = mjs_arg(mjs, _i); \ - JS_CONVERT_OR_RETURN(mjs, &_js_arg_vals[_i], _js_args[_i], "argument %d", _i); \ - } - /** * @brief Prepends an error, sets the JS return value to `undefined` and returns * from the C function @@ -358,3 +142,7 @@ void js_does_sdk_support(struct mjs* mjs); * @brief `checkSdkFeatures` function */ void js_check_sdk_features(struct mjs* mjs); + +#ifdef __cplusplus +} +#endif diff --git a/applications/system/js_app/js_thread.c b/applications/system/js_app/js_thread.c index 4a6d23011..a41a28d11 100644 --- a/applications/system/js_app/js_thread.c +++ b/applications/system/js_app/js_thread.c @@ -198,18 +198,15 @@ static void js_require(struct mjs* mjs) { } static void js_parse_int(struct mjs* mjs) { - const char* str; - JS_FETCH_ARGS_OR_RETURN(mjs, JS_AT_LEAST, JS_ARG_STR(&str)); + static const JsValueDeclaration js_parse_int_arg_list[] = { + JS_VALUE_SIMPLE(JsValueTypeString), + JS_VALUE_SIMPLE_W_DEFAULT(JsValueTypeInt32, int32_val, 10), + }; + static const JsValueArguments js_parse_int_args = JS_VALUE_ARGS(js_parse_int_arg_list); - int32_t base = 10; - if(mjs_nargs(mjs) >= 2) { - mjs_val_t base_arg = mjs_arg(mjs, 1); - if(!mjs_is_number(base_arg)) { - mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Base must be a number"); - mjs_return(mjs, MJS_UNDEFINED); - } - base = mjs_get_int(mjs, base_arg); - } + const char* str; + int32_t base; + JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_parse_int_args, &str, &base); int32_t num; if(strint_to_int32(str, NULL, &num, base) != StrintParseNoError) { diff --git a/applications/system/js_app/js_thread_i.h b/applications/system/js_app/js_thread_i.h index a73cbb4bc..5fbdb06d0 100644 --- a/applications/system/js_app/js_thread_i.h +++ b/applications/system/js_app/js_thread_i.h @@ -11,6 +11,10 @@ #include #include +#ifdef __cplusplus +extern "C" { +#endif + #define INST_PROP_NAME "_" typedef enum { @@ -23,3 +27,7 @@ bool js_delay_with_flags(struct mjs* mjs, uint32_t time); void js_flags_set(struct mjs* mjs, uint32_t flags); uint32_t js_flags_wait(struct mjs* mjs, uint32_t flags, uint32_t timeout); + +#ifdef __cplusplus +} +#endif diff --git a/applications/system/js_app/modules/js_event_loop/js_event_loop.c b/applications/system/js_app/modules/js_event_loop/js_event_loop.c index 625301ad1..1aded8de4 100644 --- a/applications/system/js_app/modules/js_event_loop/js_event_loop.c +++ b/applications/system/js_app/modules/js_event_loop/js_event_loop.c @@ -144,10 +144,16 @@ static void js_event_loop_subscribe(struct mjs* mjs) { JsEventLoop* module = JS_GET_CONTEXT(mjs); // get arguments + static const JsValueDeclaration js_loop_subscribe_arg_list[] = { + JS_VALUE_SIMPLE(JsValueTypeRawPointer), + JS_VALUE_SIMPLE(JsValueTypeFunction), + }; + static const JsValueArguments js_loop_subscribe_args = + JS_VALUE_ARGS(js_loop_subscribe_arg_list); + JsEventLoopContract* contract; mjs_val_t callback; - JS_FETCH_ARGS_OR_RETURN( - mjs, JS_AT_LEAST, JS_ARG_STRUCT(JsEventLoopContract, &contract), JS_ARG_FN(&callback)); + JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_loop_subscribe_args, &contract, &callback); // create subscription object JsEventLoopSubscription* subscription = malloc(sizeof(JsEventLoopSubscription)); @@ -242,20 +248,22 @@ static void js_event_loop_stop(struct mjs* mjs) { * event */ static void js_event_loop_timer(struct mjs* mjs) { - // get arguments - const char* mode_str; - int32_t interval; - JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&mode_str), JS_ARG_INT32(&interval)); - JsEventLoop* module = JS_GET_CONTEXT(mjs); + static const JsValueEnumVariant js_loop_timer_mode_variants[] = { + {"periodic", FuriEventLoopTimerTypePeriodic}, + {"oneshot", FuriEventLoopTimerTypeOnce}, + }; + + static const JsValueDeclaration js_loop_timer_arg_list[] = { + JS_VALUE_ENUM(FuriEventLoopTimerType, js_loop_timer_mode_variants), + JS_VALUE_SIMPLE(JsValueTypeInt32), + }; + static const JsValueArguments js_loop_timer_args = JS_VALUE_ARGS(js_loop_timer_arg_list); FuriEventLoopTimerType mode; - if(strcasecmp(mode_str, "periodic") == 0) { - mode = FuriEventLoopTimerTypePeriodic; - } else if(strcasecmp(mode_str, "oneshot") == 0) { - mode = FuriEventLoopTimerTypeOnce; - } else { - JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "argument 0: unknown mode"); - } + int32_t interval; + JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_loop_timer_args, &mode, &interval); + + JsEventLoop* module = JS_GET_CONTEXT(mjs); // make timer contract JsEventLoopContract* contract = malloc(sizeof(JsEventLoopContract)); @@ -293,8 +301,14 @@ static mjs_val_t */ static void js_event_loop_queue_send(struct mjs* mjs) { // get arguments + static const JsValueDeclaration js_loop_q_send_arg_list[] = { + JS_VALUE_SIMPLE(JsValueTypeAny), + }; + static const JsValueArguments js_loop_q_send_args = JS_VALUE_ARGS(js_loop_q_send_arg_list); + mjs_val_t message; - JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_ANY(&message)); + JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_loop_q_send_args, &message); + JsEventLoopContract* contract = JS_GET_CONTEXT(mjs); // send message @@ -311,8 +325,14 @@ static void js_event_loop_queue_send(struct mjs* mjs) { */ static void js_event_loop_queue(struct mjs* mjs) { // get arguments + static const JsValueDeclaration js_loop_q_arg_list[] = { + JS_VALUE_SIMPLE(JsValueTypeInt32), + }; + static const JsValueArguments js_loop_q_args = JS_VALUE_ARGS(js_loop_q_arg_list); + int32_t length; - JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_INT32(&length)); + JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_loop_q_args, &length); + JsEventLoop* module = JS_GET_CONTEXT(mjs); // make queue contract diff --git a/applications/system/js_app/modules/js_gpio.c b/applications/system/js_app/modules/js_gpio.c index 2a559570f..63de6900a 100644 --- a/applications/system/js_app/modules/js_gpio.c +++ b/applications/system/js_app/modules/js_gpio.c @@ -54,83 +54,114 @@ static void js_gpio_int_cb(void* arg) { * ``` */ static void js_gpio_init(struct mjs* mjs) { - // deconstruct mode object - mjs_val_t mode_arg; - JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_OBJ(&mode_arg)); - mjs_val_t direction_arg = mjs_get(mjs, mode_arg, "direction", ~0); - mjs_val_t out_mode_arg = mjs_get(mjs, mode_arg, "outMode", ~0); - mjs_val_t in_mode_arg = mjs_get(mjs, mode_arg, "inMode", ~0); - mjs_val_t edge_arg = mjs_get(mjs, mode_arg, "edge", ~0); - mjs_val_t pull_arg = mjs_get(mjs, mode_arg, "pull", ~0); + // direction variants + typedef enum { + JsGpioDirectionIn, + JsGpioDirectionOut, + } JsGpioDirection; + static const JsValueEnumVariant js_gpio_direction_variants[] = { + {"in", JsGpioDirectionIn}, + {"out", JsGpioDirectionOut}, + }; + static const JsValueDeclaration js_gpio_direction = + JS_VALUE_ENUM(JsGpioDirection, js_gpio_direction_variants); - // get strings - const char* direction = mjs_get_string(mjs, &direction_arg, NULL); - const char* out_mode = mjs_get_string(mjs, &out_mode_arg, NULL); - const char* in_mode = mjs_get_string(mjs, &in_mode_arg, NULL); - const char* edge = mjs_get_string(mjs, &edge_arg, NULL); - const char* pull = mjs_get_string(mjs, &pull_arg, NULL); - if(!direction) - JS_ERROR_AND_RETURN( - mjs, MJS_BAD_ARGS_ERROR, "Expected string in \"direction\" field of mode object"); - if(!out_mode) out_mode = "open_drain"; - if(!in_mode) in_mode = "plain_digital"; - if(!edge) edge = "rising"; + // inMode variants + typedef enum { + JsGpioInModeAnalog = (0 << 0), + JsGpioInModePlainDigital = (1 << 0), + JsGpioInModeInterrupt = (2 << 0), + JsGpioInModeEvent = (3 << 0), + } JsGpioInMode; + static const JsValueEnumVariant js_gpio_in_mode_variants[] = { + {"analog", JsGpioInModeAnalog}, + {"plain_digital", JsGpioInModePlainDigital}, + {"interrupt", JsGpioInModeInterrupt}, + {"event", JsGpioInModeEvent}, + }; + static const JsValueDeclaration js_gpio_in_mode = + JS_VALUE_ENUM_W_DEFAULT(JsGpioInMode, js_gpio_in_mode_variants, JsGpioInModePlainDigital); + + // outMode variants + typedef enum { + JsGpioOutModePushPull, + JsGpioOutModeOpenDrain, + } JsGpioOutMode; + static const JsValueEnumVariant js_gpio_out_mode_variants[] = { + {"push_pull", JsGpioOutModePushPull}, + {"open_drain", JsGpioOutModeOpenDrain}, + }; + static const JsValueDeclaration js_gpio_out_mode = + JS_VALUE_ENUM_W_DEFAULT(JsGpioOutMode, js_gpio_out_mode_variants, JsGpioOutModeOpenDrain); + + // edge variants + typedef enum { + JsGpioEdgeRising = (0 << 2), + JsGpioEdgeFalling = (1 << 2), + JsGpioEdgeBoth = (2 << 2), + } JsGpioEdge; + static const JsValueEnumVariant js_gpio_edge_variants[] = { + {"rising", JsGpioEdgeRising}, + {"falling", JsGpioEdgeFalling}, + {"both", JsGpioEdgeBoth}, + }; + static const JsValueDeclaration js_gpio_edge = + JS_VALUE_ENUM_W_DEFAULT(JsGpioEdge, js_gpio_edge_variants, JsGpioEdgeRising); + + // pull variants + static const JsValueEnumVariant js_gpio_pull_variants[] = { + {"up", GpioPullUp}, + {"down", GpioPullDown}, + }; + static const JsValueDeclaration js_gpio_pull = + JS_VALUE_ENUM_W_DEFAULT(GpioPull, js_gpio_pull_variants, GpioPullNo); + + // complete mode object + static const JsValueObjectField js_gpio_mode_object_fields[] = { + {"direction", &js_gpio_direction}, + {"inMode", &js_gpio_in_mode}, + {"outMode", &js_gpio_out_mode}, + {"edge", &js_gpio_edge}, + {"pull", &js_gpio_pull}, + }; + + // function args + static const JsValueDeclaration js_gpio_init_arg_list[] = { + JS_VALUE_OBJECT_W_DEFAULTS(js_gpio_mode_object_fields), + }; + static const JsValueArguments js_gpio_init_args = JS_VALUE_ARGS(js_gpio_init_arg_list); + + JsGpioDirection direction; + JsGpioInMode in_mode; + JsGpioOutMode out_mode; + JsGpioEdge edge; + GpioPull pull; + JS_VALUE_PARSE_ARGS_OR_RETURN( + mjs, &js_gpio_init_args, &direction, &in_mode, &out_mode, &edge, &pull); - // convert strings to mode GpioMode mode; - if(strcmp(direction, "out") == 0) { - if(strcmp(out_mode, "push_pull") == 0) - mode = GpioModeOutputPushPull; - else if(strcmp(out_mode, "open_drain") == 0) - mode = GpioModeOutputOpenDrain; - else - JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "invalid outMode"); - } else if(strcmp(direction, "in") == 0) { - if(strcmp(in_mode, "analog") == 0) { - mode = GpioModeAnalog; - } else if(strcmp(in_mode, "plain_digital") == 0) { - mode = GpioModeInput; - } else if(strcmp(in_mode, "interrupt") == 0) { - if(strcmp(edge, "rising") == 0) - mode = GpioModeInterruptRise; - else if(strcmp(edge, "falling") == 0) - mode = GpioModeInterruptFall; - else if(strcmp(edge, "both") == 0) - mode = GpioModeInterruptRiseFall; - else - JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "invalid edge"); - } else if(strcmp(in_mode, "event") == 0) { - if(strcmp(edge, "rising") == 0) - mode = GpioModeEventRise; - else if(strcmp(edge, "falling") == 0) - mode = GpioModeEventFall; - else if(strcmp(edge, "both") == 0) - mode = GpioModeEventRiseFall; - else - JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "invalid edge"); - } else { - JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "invalid inMode"); - } + if(direction == JsGpioDirectionOut) { + static const GpioMode js_gpio_out_mode_lut[] = { + [JsGpioOutModePushPull] = GpioModeOutputPushPull, + [JsGpioOutModeOpenDrain] = GpioModeOutputOpenDrain, + }; + mode = js_gpio_out_mode_lut[out_mode]; } else { - JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "invalid direction"); + static const GpioMode js_gpio_in_mode_lut[] = { + [JsGpioInModeAnalog] = GpioModeAnalog, + [JsGpioInModePlainDigital] = GpioModeInput, + [JsGpioInModeInterrupt | JsGpioEdgeRising] = GpioModeInterruptRise, + [JsGpioInModeInterrupt | JsGpioEdgeFalling] = GpioModeInterruptFall, + [JsGpioInModeInterrupt | JsGpioEdgeBoth] = GpioModeInterruptRiseFall, + [JsGpioInModeEvent | JsGpioEdgeRising] = GpioModeEventRise, + [JsGpioInModeEvent | JsGpioEdgeFalling] = GpioModeEventFall, + [JsGpioInModeEvent | JsGpioEdgeBoth] = GpioModeEventRiseFall, + }; + mode = js_gpio_in_mode_lut[in_mode | edge]; } - // convert pull - GpioPull pull_mode; - if(!pull) { - pull_mode = GpioPullNo; - } else if(strcmp(pull, "up") == 0) { - pull_mode = GpioPullUp; - } else if(strcmp(pull, "down") == 0) { - pull_mode = GpioPullDown; - } else { - JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "invalid pull"); - } - - // init GPIO JsGpioPinInst* manager_data = JS_GET_CONTEXT(mjs); - furi_hal_gpio_init(manager_data->pin, mode, pull_mode, GpioSpeedVeryHigh); - mjs_return(mjs, MJS_UNDEFINED); + furi_hal_gpio_init(manager_data->pin, mode, pull, GpioSpeedVeryHigh); } /** @@ -146,8 +177,13 @@ static void js_gpio_init(struct mjs* mjs) { * ``` */ static void js_gpio_write(struct mjs* mjs) { + static const JsValueDeclaration js_gpio_write_arg_list[] = { + JS_VALUE_SIMPLE(JsValueTypeBool), + }; + static const JsValueArguments js_gpio_write_args = JS_VALUE_ARGS(js_gpio_write_arg_list); bool level; - JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_BOOL(&level)); + JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_gpio_write_args, &level); + JsGpioPinInst* manager_data = JS_GET_CONTEXT(mjs); furi_hal_gpio_write(manager_data->pin, level); mjs_return(mjs, MJS_UNDEFINED); @@ -261,9 +297,16 @@ static void js_gpio_is_pwm_supported(struct mjs* mjs) { * ``` */ static void js_gpio_pwm_write(struct mjs* mjs) { - JsGpioPinInst* manager_data = JS_GET_CONTEXT(mjs); + static const JsValueDeclaration js_gpio_pwm_write_arg_list[] = { + JS_VALUE_SIMPLE(JsValueTypeInt32), + JS_VALUE_SIMPLE(JsValueTypeInt32), + }; + static const JsValueArguments js_gpio_pwm_write_args = + JS_VALUE_ARGS(js_gpio_pwm_write_arg_list); int32_t frequency, duty; - JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_INT32(&frequency), JS_ARG_INT32(&duty)); + JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_gpio_pwm_write_args, &frequency, &duty); + + JsGpioPinInst* manager_data = JS_GET_CONTEXT(mjs); if(manager_data->pwm_output == FuriHalPwmOutputIdNone) { JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "PWM is not supported on this pin"); } @@ -326,8 +369,13 @@ static void js_gpio_pwm_stop(struct mjs* mjs) { * ``` */ static void js_gpio_get(struct mjs* mjs) { + static const JsValueDeclaration js_gpio_get_arg_list[] = { + JS_VALUE_SIMPLE(JsValueTypeAny), + }; + static const JsValueArguments js_gpio_get_args = JS_VALUE_ARGS(js_gpio_get_arg_list); mjs_val_t name_arg; - JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_ANY(&name_arg)); + JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_gpio_get_args, &name_arg); + const char* name_string = mjs_get_string(mjs, &name_arg, NULL); const GpioPinRecord* pin_record = NULL; diff --git a/applications/system/js_app/modules/js_gui/file_picker.c b/applications/system/js_app/modules/js_gui/file_picker.c index 49cf5e89d..7b36596cd 100644 --- a/applications/system/js_app/modules/js_gui/file_picker.c +++ b/applications/system/js_app/modules/js_gui/file_picker.c @@ -3,8 +3,14 @@ #include static void js_gui_file_picker_pick_file(struct mjs* mjs) { + static const JsValueDeclaration js_picker_arg_list[] = { + JS_VALUE_SIMPLE(JsValueTypeString), + JS_VALUE_SIMPLE(JsValueTypeString), + }; + static const JsValueArguments js_picker_args = JS_VALUE_ARGS(js_picker_arg_list); + const char *base_path, *extension; - JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&base_path), JS_ARG_STR(&extension)); + JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_picker_args, &base_path, &extension); DialogsApp* dialogs = furi_record_open(RECORD_DIALOGS); const DialogsFileBrowserOptions browser_options = { diff --git a/applications/system/js_app/modules/js_gui/icon.c b/applications/system/js_app/modules/js_gui/icon.c index 3d8a67a8b..4fc6da2e0 100644 --- a/applications/system/js_app/modules/js_gui/icon.c +++ b/applications/system/js_app/modules/js_gui/icon.c @@ -39,9 +39,14 @@ typedef struct { FxbmIconWrapperList_t fxbm_list; } JsGuiIconInst; +static const JsValueDeclaration js_icon_get_arg_list[] = { + JS_VALUE_SIMPLE(JsValueTypeString), +}; +static const JsValueArguments js_icon_get_args = JS_VALUE_ARGS(js_icon_get_arg_list); + static void js_gui_icon_get_builtin(struct mjs* mjs) { const char* icon_name; - JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&icon_name)); + JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_icon_get_args, &icon_name); for(size_t i = 0; i < COUNT_OF(builtin_icons); i++) { if(strcmp(icon_name, builtin_icons[i].name) == 0) { @@ -55,7 +60,7 @@ static void js_gui_icon_get_builtin(struct mjs* mjs) { static void js_gui_icon_load_fxbm(struct mjs* mjs) { const char* fxbm_path; - JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&fxbm_path)); + JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_icon_get_args, &fxbm_path); Storage* storage = furi_record_open(RECORD_STORAGE); File* file = storage_file_alloc(storage); diff --git a/applications/system/js_app/modules/js_gui/js_gui.c b/applications/system/js_app/modules/js_gui/js_gui.c index e505681df..c20d980aa 100644 --- a/applications/system/js_app/modules/js_gui/js_gui.c +++ b/applications/system/js_app/modules/js_gui/js_gui.c @@ -68,8 +68,14 @@ static bool js_gui_vd_nav_callback(void* context) { * @brief `viewDispatcher.sendCustom` */ static void js_gui_vd_send_custom(struct mjs* mjs) { + static const JsValueDeclaration js_gui_vd_send_custom_arg_list[] = { + JS_VALUE_SIMPLE(JsValueTypeInt32), + }; + static const JsValueArguments js_gui_vd_send_custom_args = + JS_VALUE_ARGS(js_gui_vd_send_custom_arg_list); + int32_t event; - JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_INT32(&event)); + JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_gui_vd_send_custom_args, &event); JsGui* module = JS_GET_CONTEXT(mjs); view_dispatcher_send_custom_event(module->dispatcher, (uint32_t)event); @@ -79,15 +85,25 @@ static void js_gui_vd_send_custom(struct mjs* mjs) { * @brief `viewDispatcher.sendTo` */ static void js_gui_vd_send_to(struct mjs* mjs) { - enum { - SendDirToFront, - SendDirToBack, - } send_direction; - JS_ENUM_MAP(send_direction, {"front", SendDirToFront}, {"back", SendDirToBack}); - JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_ENUM(send_direction, "SendDirection")); + typedef enum { + JsSendDirToFront, + JsSendDirToBack, + } JsSendDir; + static const JsValueEnumVariant js_send_dir_variants[] = { + {"front", JsSendDirToFront}, + {"back", JsSendDirToBack}, + }; + static const JsValueDeclaration js_gui_vd_send_to_arg_list[] = { + JS_VALUE_ENUM(JsSendDir, js_send_dir_variants), + }; + static const JsValueArguments js_gui_vd_send_to_args = + JS_VALUE_ARGS(js_gui_vd_send_to_arg_list); + + JsSendDir send_direction; + JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_gui_vd_send_to_args, &send_direction); JsGui* module = JS_GET_CONTEXT(mjs); - if(send_direction == SendDirToBack) { + if(send_direction == JsSendDirToBack) { view_dispatcher_send_to_back(module->dispatcher); } else { view_dispatcher_send_to_front(module->dispatcher); @@ -98,8 +114,15 @@ static void js_gui_vd_send_to(struct mjs* mjs) { * @brief `viewDispatcher.switchTo` */ static void js_gui_vd_switch_to(struct mjs* mjs) { + static const JsValueDeclaration js_gui_vd_switch_to_arg_list[] = { + JS_VALUE_SIMPLE(JsValueTypeAny), + }; + static const JsValueArguments js_gui_vd_switch_to_args = + JS_VALUE_ARGS(js_gui_vd_switch_to_arg_list); + mjs_val_t view; - JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_OBJ(&view)); + JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_gui_vd_switch_to_args, &view); + JsGuiViewData* view_data = JS_GET_INST(mjs, view); mjs_val_t vd_obj = mjs_get_this(mjs); JsGui* module = JS_GET_INST(mjs, vd_obj); @@ -267,9 +290,16 @@ static bool * @brief `View.set` */ static void js_gui_view_set(struct mjs* mjs) { + static const JsValueDeclaration js_gui_view_set_arg_list[] = { + JS_VALUE_SIMPLE(JsValueTypeString), + JS_VALUE_SIMPLE(JsValueTypeAny), + }; + static const JsValueArguments js_gui_view_set_args = JS_VALUE_ARGS(js_gui_view_set_arg_list); + const char* name; mjs_val_t value; - JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&name), JS_ARG_ANY(&value)); + JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_gui_view_set_args, &name, &value); + JsGuiViewData* data = JS_GET_CONTEXT(mjs); bool success = js_gui_view_assign(mjs, name, value, data); UNUSED(success); @@ -280,12 +310,19 @@ static void js_gui_view_set(struct mjs* mjs) { * @brief `View.addChild` */ static void js_gui_view_add_child(struct mjs* mjs) { + static const JsValueDeclaration js_gui_view_add_child_arg_list[] = { + JS_VALUE_SIMPLE(JsValueTypeAny), + }; + static const JsValueArguments js_gui_view_add_child_args = + JS_VALUE_ARGS(js_gui_view_add_child_arg_list); + + mjs_val_t child; + JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_gui_view_add_child_args, &child); + JsGuiViewData* data = JS_GET_CONTEXT(mjs); if(!data->descriptor->add_child || !data->descriptor->reset_children) JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "this View can't have children"); - mjs_val_t child; - JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_ANY(&child)); bool success = data->descriptor->add_child(mjs, data->specific_view, data->custom_data, child); UNUSED(success); mjs_return(mjs, MJS_UNDEFINED); @@ -307,12 +344,19 @@ static void js_gui_view_reset_children(struct mjs* mjs) { * @brief `View.setChildren` */ static void js_gui_view_set_children(struct mjs* mjs) { + static const JsValueDeclaration js_gui_view_set_children_arg_list[] = { + JS_VALUE_SIMPLE(JsValueTypeAnyArray), + }; + static const JsValueArguments js_gui_view_set_children_args = + JS_VALUE_ARGS(js_gui_view_set_children_arg_list); + + mjs_val_t children; + JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_gui_view_set_children_args, &children); + JsGuiViewData* data = JS_GET_CONTEXT(mjs); if(!data->descriptor->add_child || !data->descriptor->reset_children) JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "this View can't have children"); - mjs_val_t children; - JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_ARR(&children)); js_gui_view_internal_set_children(mjs, children, data); } @@ -365,7 +409,6 @@ static mjs_val_t js_gui_make_view(struct mjs* mjs, const JsViewDescriptor* descr * @brief `ViewFactory.make` */ static void js_gui_vf_make(struct mjs* mjs) { - JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY); // 0 args const JsViewDescriptor* descriptor = JS_GET_CONTEXT(mjs); mjs_return(mjs, js_gui_make_view(mjs, descriptor)); } @@ -374,8 +417,15 @@ static void js_gui_vf_make(struct mjs* mjs) { * @brief `ViewFactory.makeWith` */ static void js_gui_vf_make_with(struct mjs* mjs) { - mjs_val_t props; - JS_FETCH_ARGS_OR_RETURN(mjs, JS_AT_LEAST, JS_ARG_OBJ(&props)); + static const JsValueDeclaration js_gui_vf_make_with_arg_list[] = { + JS_VALUE_SIMPLE(JsValueTypeAnyObject), + JS_VALUE_SIMPLE(JsValueTypeAny), + }; + static const JsValueArguments js_gui_vf_make_with_args = + JS_VALUE_ARGS(js_gui_vf_make_with_arg_list); + + mjs_val_t props, children; + JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_gui_vf_make_with_args, &props, &children); const JsViewDescriptor* descriptor = JS_GET_CONTEXT(mjs); // make the object like normal @@ -396,14 +446,10 @@ static void js_gui_vf_make_with(struct mjs* mjs) { } // assign children - if(mjs_nargs(mjs) >= 2) { + if(mjs_is_array(children)) { if(!data->descriptor->add_child || !data->descriptor->reset_children) JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "this View can't have children"); - mjs_val_t children = mjs_arg(mjs, 1); - if(!mjs_is_array(children)) - JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "argument 1: expected array"); - if(!js_gui_view_internal_set_children(mjs, children, data)) return; } diff --git a/applications/system/js_app/modules/js_serial.c b/applications/system/js_app/modules/js_serial.c index 20b18a4f1..d903939ce 100644 --- a/applications/system/js_app/modules/js_serial.c +++ b/applications/system/js_app/modules/js_serial.c @@ -35,60 +35,62 @@ static void } static void js_serial_setup(struct mjs* mjs) { + static const JsValueEnumVariant js_serial_id_variants[] = { + {"lpuart", FuriHalSerialIdLpuart}, + {"usart", FuriHalSerialIdUsart}, + }; + + static const JsValueEnumVariant js_serial_data_bit_variants[] = { + {"6", FuriHalSerialDataBits6}, + {"7", FuriHalSerialDataBits7}, + {"8", FuriHalSerialDataBits8}, + {"9", FuriHalSerialDataBits9}, + }; + static const JsValueDeclaration js_serial_data_bits = JS_VALUE_ENUM_W_DEFAULT( + FuriHalSerialDataBits, js_serial_data_bit_variants, FuriHalSerialDataBits8); + + static const JsValueEnumVariant js_serial_parity_variants[] = { + {"none", FuriHalSerialParityNone}, + {"even", FuriHalSerialParityEven}, + {"odd", FuriHalSerialParityOdd}, + }; + static const JsValueDeclaration js_serial_parity = JS_VALUE_ENUM_W_DEFAULT( + FuriHalSerialParity, js_serial_parity_variants, FuriHalSerialParityNone); + + static const JsValueEnumVariant js_serial_stop_bit_variants[] = { + {"0.5", FuriHalSerialStopBits0_5}, + {"1", FuriHalSerialStopBits1}, + {"1.5", FuriHalSerialStopBits1_5}, + {"2", FuriHalSerialStopBits2}, + }; + static const JsValueDeclaration js_serial_stop_bits = JS_VALUE_ENUM_W_DEFAULT( + FuriHalSerialStopBits, js_serial_stop_bit_variants, FuriHalSerialStopBits1); + + static const JsValueObjectField js_serial_framing_fields[] = { + {"dataBits", &js_serial_data_bits}, + {"parity", &js_serial_parity}, + {"stopBits", &js_serial_stop_bits}, + }; + + static const JsValueDeclaration js_serial_setup_arg_list[] = { + JS_VALUE_ENUM(FuriHalSerialId, js_serial_id_variants), + JS_VALUE_SIMPLE(JsValueTypeInt32), + JS_VALUE_OBJECT_W_DEFAULTS(js_serial_framing_fields), + }; + static const JsValueArguments js_serial_setup_args = JS_VALUE_ARGS(js_serial_setup_arg_list); + FuriHalSerialId serial_id; int32_t baudrate; - JS_ENUM_MAP(serial_id, {"lpuart", FuriHalSerialIdLpuart}, {"usart", FuriHalSerialIdUsart}); - JS_FETCH_ARGS_OR_RETURN( - mjs, JS_AT_LEAST, JS_ARG_ENUM(serial_id, "SerialId"), JS_ARG_INT32(&baudrate)); - FuriHalSerialDataBits data_bits = FuriHalSerialDataBits8; FuriHalSerialParity parity = FuriHalSerialParityNone; FuriHalSerialStopBits stop_bits = FuriHalSerialStopBits1; - if(mjs_nargs(mjs) > 2) { - struct framing { - mjs_val_t data_bits; - mjs_val_t parity; - mjs_val_t stop_bits; - } framing; - JS_OBJ_MAP( - framing, - {"dataBits", offsetof(struct framing, data_bits)}, - {"parity", offsetof(struct framing, parity)}, - {"stopBits", offsetof(struct framing, stop_bits)}); - JS_ENUM_MAP( - data_bits, - {"6", FuriHalSerialDataBits6}, - {"7", FuriHalSerialDataBits7}, - {"8", FuriHalSerialDataBits8}, - {"9", FuriHalSerialDataBits9}); - JS_ENUM_MAP( - parity, - {"none", FuriHalSerialParityNone}, - {"even", FuriHalSerialParityEven}, - {"odd", FuriHalSerialParityOdd}); - JS_ENUM_MAP( - stop_bits, - {"0.5", FuriHalSerialStopBits0_5}, - {"1", FuriHalSerialStopBits1}, - {"1.5", FuriHalSerialStopBits1_5}, - {"2", FuriHalSerialStopBits2}); - mjs_val_t framing_obj = mjs_arg(mjs, 2); - JS_CONVERT_OR_RETURN(mjs, &framing_obj, JS_ARG_OBJECT(framing, "Framing"), "argument 2"); - JS_CONVERT_OR_RETURN( - mjs, &framing.data_bits, JS_ARG_ENUM(data_bits, "DataBits"), "argument 2: dataBits"); - JS_CONVERT_OR_RETURN( - mjs, &framing.parity, JS_ARG_ENUM(parity, "Parity"), "argument 2: parity"); - JS_CONVERT_OR_RETURN( - mjs, &framing.stop_bits, JS_ARG_ENUM(stop_bits, "StopBits"), "argument 2: stopBits"); - } + JS_VALUE_PARSE_ARGS_OR_RETURN( + mjs, &js_serial_setup_args, &serial_id, &baudrate, &data_bits, &parity, &stop_bits); JsSerialInst* serial = JS_GET_CONTEXT(mjs); - if(serial->setup_done) { - mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "Serial is already configured"); - mjs_return(mjs, MJS_UNDEFINED); - return; - } + if(serial->setup_done) + JS_ERROR_AND_RETURN(mjs, MJS_INTERNAL_ERROR, "Serial is already configured"); expansion_disable(furi_record_open(RECORD_EXPANSION)); furi_record_close(RECORD_EXPANSION); @@ -123,28 +125,20 @@ static void js_serial_deinit(JsSerialInst* js_serial) { } static void js_serial_end(struct mjs* mjs) { - mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0); - JsSerialInst* serial = mjs_get_ptr(mjs, obj_inst); + JsSerialInst* serial = JS_GET_CONTEXT(mjs); furi_assert(serial); - if(!serial->setup_done) { - mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "Serial is not configured"); - mjs_return(mjs, MJS_UNDEFINED); - return; - } + if(!serial->setup_done) + JS_ERROR_AND_RETURN(mjs, MJS_INTERNAL_ERROR, "Serial is not configured"); js_serial_deinit(serial); } static void js_serial_write(struct mjs* mjs) { - mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0); - JsSerialInst* serial = mjs_get_ptr(mjs, obj_inst); + JsSerialInst* serial = JS_GET_CONTEXT(mjs); furi_assert(serial); - if(!serial->setup_done) { - mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "Serial is not configured"); - mjs_return(mjs, MJS_UNDEFINED); - return; - } + if(!serial->setup_done) + JS_ERROR_AND_RETURN(mjs, MJS_INTERNAL_ERROR, "Serial is not configured"); bool args_correct = true; @@ -228,43 +222,20 @@ static size_t js_serial_receive(JsSerialInst* serial, char* buf, size_t len, uin return bytes_read; } +static const JsValueDeclaration js_serial_read_arg_list[] = { + JS_VALUE_SIMPLE(JsValueTypeInt32), + JS_VALUE_SIMPLE_W_DEFAULT(JsValueTypeInt32, int32_val, INT32_MAX), +}; +static const JsValueArguments js_serial_read_args = JS_VALUE_ARGS(js_serial_read_arg_list); + static void js_serial_read(struct mjs* mjs) { - mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0); - JsSerialInst* serial = mjs_get_ptr(mjs, obj_inst); + JsSerialInst* serial = JS_GET_CONTEXT(mjs); furi_assert(serial); - if(!serial->setup_done) { - mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "Serial is not configured"); - mjs_return(mjs, MJS_UNDEFINED); - return; - } + if(!serial->setup_done) + JS_ERROR_AND_RETURN(mjs, MJS_INTERNAL_ERROR, "Serial is not configured"); - size_t read_len = 0; - uint32_t timeout = FuriWaitForever; - - do { - size_t num_args = mjs_nargs(mjs); - if(num_args == 1) { - mjs_val_t arg = mjs_arg(mjs, 0); - if(!mjs_is_number(arg)) { - break; - } - read_len = mjs_get_int32(mjs, arg); - } else if(num_args == 2) { - mjs_val_t len_arg = mjs_arg(mjs, 0); - mjs_val_t timeout_arg = mjs_arg(mjs, 1); - if((!mjs_is_number(len_arg)) || (!mjs_is_number(timeout_arg))) { - break; - } - read_len = mjs_get_int32(mjs, len_arg); - timeout = mjs_get_int32(mjs, timeout_arg); - } - } while(0); - - if(read_len == 0) { - mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, ""); - mjs_return(mjs, MJS_UNDEFINED); - return; - } + int32_t read_len, timeout; + JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_serial_read_args, &read_len, &timeout); char* read_buf = malloc(read_len); size_t bytes_read = js_serial_receive(serial, read_buf, read_len, timeout); @@ -278,37 +249,19 @@ static void js_serial_read(struct mjs* mjs) { } static void js_serial_readln(struct mjs* mjs) { - mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0); - JsSerialInst* serial = mjs_get_ptr(mjs, obj_inst); + JsSerialInst* serial = JS_GET_CONTEXT(mjs); furi_assert(serial); - if(!serial->setup_done) { - mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "Serial is not configured"); - mjs_return(mjs, MJS_UNDEFINED); - return; - } + if(!serial->setup_done) + JS_ERROR_AND_RETURN(mjs, MJS_INTERNAL_ERROR, "Serial is not configured"); - bool args_correct = false; - uint32_t timeout = FuriWaitForever; + static const JsValueDeclaration js_serial_readln_arg_list[] = { + JS_VALUE_SIMPLE(JsValueTypeInt32), + }; + static const JsValueArguments js_serial_readln_args = JS_VALUE_ARGS(js_serial_readln_arg_list); - do { - size_t num_args = mjs_nargs(mjs); - if(num_args > 1) { - break; - } else if(num_args == 1) { - mjs_val_t arg = mjs_arg(mjs, 0); - if(!mjs_is_number(arg)) { - break; - } - timeout = mjs_get_int32(mjs, arg); - } - args_correct = true; - } while(0); + int32_t timeout; + JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_serial_readln_args, &timeout); - if(!args_correct) { - mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, ""); - mjs_return(mjs, MJS_UNDEFINED); - return; - } FuriString* rx_buf = furi_string_alloc(); size_t bytes_read = 0; char read_char = 0; @@ -335,42 +288,13 @@ static void js_serial_readln(struct mjs* mjs) { } static void js_serial_read_bytes(struct mjs* mjs) { - mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0); - JsSerialInst* serial = mjs_get_ptr(mjs, obj_inst); + JsSerialInst* serial = JS_GET_CONTEXT(mjs); furi_assert(serial); - if(!serial->setup_done) { - mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "Serial is not configured"); - mjs_return(mjs, MJS_UNDEFINED); - return; - } + if(!serial->setup_done) + JS_ERROR_AND_RETURN(mjs, MJS_INTERNAL_ERROR, "Serial is not configured"); - size_t read_len = 0; - uint32_t timeout = FuriWaitForever; - - do { - size_t num_args = mjs_nargs(mjs); - if(num_args == 1) { - mjs_val_t arg = mjs_arg(mjs, 0); - if(!mjs_is_number(arg)) { - break; - } - read_len = mjs_get_int32(mjs, arg); - } else if(num_args == 2) { - mjs_val_t len_arg = mjs_arg(mjs, 0); - mjs_val_t timeout_arg = mjs_arg(mjs, 1); - if((!mjs_is_number(len_arg)) || (!mjs_is_number(timeout_arg))) { - break; - } - read_len = mjs_get_int32(mjs, len_arg); - timeout = mjs_get_int32(mjs, timeout_arg); - } - } while(0); - - if(read_len == 0) { - mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, ""); - mjs_return(mjs, MJS_UNDEFINED); - return; - } + int32_t read_len, timeout; + JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_serial_read_args, &read_len, &timeout); char* read_buf = malloc(read_len); size_t bytes_read = js_serial_receive(serial, read_buf, read_len, timeout); @@ -399,27 +323,19 @@ static char* js_serial_receive_any(JsSerialInst* serial, size_t* len, uint32_t t } static void js_serial_read_any(struct mjs* mjs) { - mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0); - JsSerialInst* serial = mjs_get_ptr(mjs, obj_inst); + JsSerialInst* serial = JS_GET_CONTEXT(mjs); furi_assert(serial); - if(!serial->setup_done) { - mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "Serial is not configured"); - mjs_return(mjs, MJS_UNDEFINED); - return; - } + if(!serial->setup_done) + JS_ERROR_AND_RETURN(mjs, MJS_INTERNAL_ERROR, "Serial is not configured"); - uint32_t timeout = FuriWaitForever; + static const JsValueDeclaration js_serial_read_any_arg_list[] = { + JS_VALUE_SIMPLE_W_DEFAULT(JsValueTypeInt32, int32_val, INT32_MAX), + }; + static const JsValueArguments js_serial_read_any_args = + JS_VALUE_ARGS(js_serial_read_any_arg_list); - do { - size_t num_args = mjs_nargs(mjs); - if(num_args == 1) { - mjs_val_t timeout_arg = mjs_arg(mjs, 0); - if(!mjs_is_number(timeout_arg)) { - break; - } - timeout = mjs_get_int32(mjs, timeout_arg); - } - } while(0); + int32_t timeout; + JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_serial_read_any_args, &timeout); size_t bytes_read = 0; char* read_buf = js_serial_receive_any(serial, &bytes_read, timeout); @@ -663,16 +579,19 @@ static void* js_serial_create(struct mjs* mjs, mjs_val_t* object, JsModules* mod UNUSED(modules); JsSerialInst* js_serial = malloc(sizeof(JsSerialInst)); js_serial->mjs = mjs; + mjs_val_t serial_obj = mjs_mk_object(mjs); - mjs_set(mjs, serial_obj, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, js_serial)); - mjs_set(mjs, serial_obj, "setup", ~0, MJS_MK_FN(js_serial_setup)); - mjs_set(mjs, serial_obj, "end", ~0, MJS_MK_FN(js_serial_end)); - mjs_set(mjs, serial_obj, "write", ~0, MJS_MK_FN(js_serial_write)); - mjs_set(mjs, serial_obj, "read", ~0, MJS_MK_FN(js_serial_read)); - mjs_set(mjs, serial_obj, "readln", ~0, MJS_MK_FN(js_serial_readln)); - mjs_set(mjs, serial_obj, "readBytes", ~0, MJS_MK_FN(js_serial_read_bytes)); - mjs_set(mjs, serial_obj, "readAny", ~0, MJS_MK_FN(js_serial_read_any)); - mjs_set(mjs, serial_obj, "expect", ~0, MJS_MK_FN(js_serial_expect)); + JS_ASSIGN_MULTI(mjs, serial_obj) { + JS_FIELD(INST_PROP_NAME, mjs_mk_foreign(mjs, js_serial)); + JS_FIELD("setup", MJS_MK_FN(js_serial_setup)); + JS_FIELD("end", MJS_MK_FN(js_serial_end)); + JS_FIELD("write", MJS_MK_FN(js_serial_write)); + JS_FIELD("read", MJS_MK_FN(js_serial_read)); + JS_FIELD("readln", MJS_MK_FN(js_serial_readln)); + JS_FIELD("readBytes", MJS_MK_FN(js_serial_read_bytes)); + JS_FIELD("readAny", MJS_MK_FN(js_serial_read_any)); + JS_FIELD("expect", MJS_MK_FN(js_serial_expect)); + } *object = serial_obj; return js_serial; diff --git a/applications/system/js_app/modules/js_storage.c b/applications/system/js_app/modules/js_storage.c index 1d4053a5f..66d002f33 100644 --- a/applications/system/js_app/modules/js_storage.c +++ b/applications/system/js_app/modules/js_storage.c @@ -1,42 +1,79 @@ #include "../js_modules.h" // IWYU pragma: keep #include -// ---=== file ops ===--- +// ========================== +// Common argument signatures +// ========================== + +static const JsValueDeclaration js_storage_1_int_arg_list[] = { + JS_VALUE_SIMPLE(JsValueTypeInt32), +}; +static const JsValueArguments js_storage_1_int_args = JS_VALUE_ARGS(js_storage_1_int_arg_list); + +static const JsValueDeclaration js_storage_1_str_arg_list[] = { + JS_VALUE_SIMPLE(JsValueTypeString), +}; +static const JsValueArguments js_storage_1_str_args = JS_VALUE_ARGS(js_storage_1_str_arg_list); + +static const JsValueDeclaration js_storage_2_str_arg_list[] = { + JS_VALUE_SIMPLE(JsValueTypeString), + JS_VALUE_SIMPLE(JsValueTypeString), +}; +static const JsValueArguments js_storage_2_str_args = JS_VALUE_ARGS(js_storage_2_str_arg_list); + +// ====================== +// File object operations +// ====================== static void js_storage_file_close(struct mjs* mjs) { - JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY); // 0 args File* file = JS_GET_CONTEXT(mjs); mjs_return(mjs, mjs_mk_boolean(mjs, storage_file_close(file))); } static void js_storage_file_is_open(struct mjs* mjs) { - JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY); // 0 args File* file = JS_GET_CONTEXT(mjs); mjs_return(mjs, mjs_mk_boolean(mjs, storage_file_is_open(file))); } static void js_storage_file_read(struct mjs* mjs) { - enum { - ReadModeAscii, - ReadModeBinary, - } read_mode; - JS_ENUM_MAP(read_mode, {"ascii", ReadModeAscii}, {"binary", ReadModeBinary}); + typedef enum { + JsStorageReadModeAscii, + JsStorageReadModeBinary, + } JsStorageReadMode; + static const JsValueEnumVariant js_storage_read_mode_variants[] = { + {"ascii", JsStorageReadModeAscii}, + {"binary", JsStorageReadModeBinary}, + }; + static const JsValueDeclaration js_storage_read_arg_list[] = { + JS_VALUE_ENUM(JsStorageReadMode, js_storage_read_mode_variants), + JS_VALUE_SIMPLE(JsValueTypeInt32), + }; + static const JsValueArguments js_storage_read_args = JS_VALUE_ARGS(js_storage_read_arg_list); + + JsStorageReadMode read_mode; int32_t length; - JS_FETCH_ARGS_OR_RETURN( - mjs, JS_EXACTLY, JS_ARG_ENUM(read_mode, "ReadMode"), JS_ARG_INT32(&length)); + JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_storage_read_args, &read_mode, &length); + File* file = JS_GET_CONTEXT(mjs); char buffer[length]; size_t actually_read = storage_file_read(file, buffer, length); - if(read_mode == ReadModeAscii) { + if(read_mode == JsStorageReadModeAscii) { mjs_return(mjs, mjs_mk_string(mjs, buffer, actually_read, true)); - } else if(read_mode == ReadModeBinary) { + } else if(read_mode == JsStorageReadModeBinary) { mjs_return(mjs, mjs_mk_array_buf(mjs, buffer, actually_read)); } } static void js_storage_file_write(struct mjs* mjs) { + static const JsValueDeclaration js_storage_file_write_arg_list[] = { + JS_VALUE_SIMPLE(JsValueTypeAny), + }; + static const JsValueArguments js_storage_file_write_args = + JS_VALUE_ARGS(js_storage_file_write_arg_list); + mjs_val_t data; - JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_ANY(&data)); + JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_storage_file_write_args, &data); + const void* buf; size_t len; if(mjs_is_string(data)) { @@ -52,52 +89,58 @@ static void js_storage_file_write(struct mjs* mjs) { static void js_storage_file_seek_relative(struct mjs* mjs) { int32_t offset; - JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_INT32(&offset)); + JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_storage_1_int_args, &offset); File* file = JS_GET_CONTEXT(mjs); mjs_return(mjs, mjs_mk_boolean(mjs, storage_file_seek(file, offset, false))); } static void js_storage_file_seek_absolute(struct mjs* mjs) { int32_t offset; - JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_INT32(&offset)); + JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_storage_1_int_args, &offset); File* file = JS_GET_CONTEXT(mjs); mjs_return(mjs, mjs_mk_boolean(mjs, storage_file_seek(file, offset, true))); } static void js_storage_file_tell(struct mjs* mjs) { - JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY); // 0 args File* file = JS_GET_CONTEXT(mjs); mjs_return(mjs, mjs_mk_number(mjs, storage_file_tell(file))); } static void js_storage_file_truncate(struct mjs* mjs) { - JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY); // 0 args File* file = JS_GET_CONTEXT(mjs); mjs_return(mjs, mjs_mk_boolean(mjs, storage_file_truncate(file))); } static void js_storage_file_size(struct mjs* mjs) { - JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY); // 0 args File* file = JS_GET_CONTEXT(mjs); mjs_return(mjs, mjs_mk_number(mjs, storage_file_size(file))); } static void js_storage_file_eof(struct mjs* mjs) { - JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY); // 0 args File* file = JS_GET_CONTEXT(mjs); mjs_return(mjs, mjs_mk_boolean(mjs, storage_file_eof(file))); } static void js_storage_file_copy_to(struct mjs* mjs) { - File* source = JS_GET_CONTEXT(mjs); + static const JsValueDeclaration js_storage_file_write_arg_list[] = { + JS_VALUE_SIMPLE(JsValueTypeAny), + JS_VALUE_SIMPLE(JsValueTypeInt32), + }; + static const JsValueArguments js_storage_file_write_args = + JS_VALUE_ARGS(js_storage_file_write_arg_list); + mjs_val_t dest_obj; int32_t bytes; - JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_OBJ(&dest_obj), JS_ARG_INT32(&bytes)); + JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_storage_file_write_args, &dest_obj, &bytes); + + File* source = JS_GET_CONTEXT(mjs); File* destination = JS_GET_INST(mjs, dest_obj); mjs_return(mjs, mjs_mk_boolean(mjs, storage_file_copy_to_file(source, destination, bytes))); } -// ---=== top-level file ops ===--- +// ========================= +// Top-level file operations +// ========================= // common destructor for file and dir objects static void js_storage_file_destructor(struct mjs* mjs, mjs_val_t obj) { @@ -106,23 +149,33 @@ static void js_storage_file_destructor(struct mjs* mjs, mjs_val_t obj) { } static void js_storage_open_file(struct mjs* mjs) { - const char* path; - FS_AccessMode access_mode; - FS_OpenMode open_mode; - JS_ENUM_MAP(access_mode, {"r", FSAM_READ}, {"w", FSAM_WRITE}, {"rw", FSAM_READ_WRITE}); - JS_ENUM_MAP( - open_mode, + static const JsValueEnumVariant js_storage_fsam_variants[] = { + {"r", FSAM_READ}, + {"w", FSAM_WRITE}, + {"rw", FSAM_READ_WRITE}, + }; + + static const JsValueEnumVariant js_storage_fsom_variants[] = { {"open_existing", FSOM_OPEN_EXISTING}, {"open_always", FSOM_OPEN_ALWAYS}, {"open_append", FSOM_OPEN_APPEND}, {"create_new", FSOM_CREATE_NEW}, - {"create_always", FSOM_CREATE_ALWAYS}); - JS_FETCH_ARGS_OR_RETURN( - mjs, - JS_EXACTLY, - JS_ARG_STR(&path), - JS_ARG_ENUM(access_mode, "AccessMode"), - JS_ARG_ENUM(open_mode, "OpenMode")); + {"create_always", FSOM_CREATE_ALWAYS}, + }; + + static const JsValueDeclaration js_storage_open_file_arg_list[] = { + JS_VALUE_SIMPLE(JsValueTypeString), + JS_VALUE_ENUM(FS_AccessMode, js_storage_fsam_variants), + JS_VALUE_ENUM(FS_OpenMode, js_storage_fsom_variants), + }; + static const JsValueArguments js_storage_open_file_args = + JS_VALUE_ARGS(js_storage_open_file_arg_list); + + const char* path; + FS_AccessMode access_mode; + FS_OpenMode open_mode; + JS_VALUE_PARSE_ARGS_OR_RETURN( + mjs, &js_storage_open_file_args, &path, &access_mode, &open_mode); Storage* storage = JS_GET_CONTEXT(mjs); File* file = storage_file_alloc(storage); @@ -152,16 +205,18 @@ static void js_storage_open_file(struct mjs* mjs) { static void js_storage_file_exists(struct mjs* mjs) { const char* path; - JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&path)); + JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_storage_1_str_args, &path); Storage* storage = JS_GET_CONTEXT(mjs); mjs_return(mjs, mjs_mk_boolean(mjs, storage_file_exists(storage, path))); } -// ---=== dir ops ===--- +// ==================== +// Directory operations +// ==================== static void js_storage_read_directory(struct mjs* mjs) { const char* path; - JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&path)); + JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_storage_1_str_args, &path); Storage* storage = JS_GET_CONTEXT(mjs); File* dir = storage_file_alloc(storage); @@ -200,30 +255,32 @@ static void js_storage_read_directory(struct mjs* mjs) { static void js_storage_directory_exists(struct mjs* mjs) { const char* path; - JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&path)); + JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_storage_1_str_args, &path); Storage* storage = JS_GET_CONTEXT(mjs); mjs_return(mjs, mjs_mk_boolean(mjs, storage_dir_exists(storage, path))); } static void js_storage_make_directory(struct mjs* mjs) { const char* path; - JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&path)); + JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_storage_1_str_args, &path); Storage* storage = JS_GET_CONTEXT(mjs); mjs_return(mjs, mjs_mk_boolean(mjs, storage_simply_mkdir(storage, path))); } -// ---=== common ops ===--- +// ================= +// Common operations +// ================= static void js_storage_file_or_dir_exists(struct mjs* mjs) { const char* path; - JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&path)); + JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_storage_1_str_args, &path); Storage* storage = JS_GET_CONTEXT(mjs); mjs_return(mjs, mjs_mk_boolean(mjs, storage_common_exists(storage, path))); } static void js_storage_stat(struct mjs* mjs) { const char* path; - JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&path)); + JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_storage_1_str_args, &path); Storage* storage = JS_GET_CONTEXT(mjs); FileInfo file_info; uint32_t timestamp; @@ -244,21 +301,21 @@ static void js_storage_stat(struct mjs* mjs) { static void js_storage_remove(struct mjs* mjs) { const char* path; - JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&path)); + JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_storage_1_str_args, &path); Storage* storage = JS_GET_CONTEXT(mjs); mjs_return(mjs, mjs_mk_boolean(mjs, storage_simply_remove(storage, path))); } static void js_storage_rmrf(struct mjs* mjs) { const char* path; - JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&path)); + JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_storage_1_str_args, &path); Storage* storage = JS_GET_CONTEXT(mjs); mjs_return(mjs, mjs_mk_boolean(mjs, storage_simply_remove_recursive(storage, path))); } static void js_storage_rename(struct mjs* mjs) { const char *old, *new; - JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&old), JS_ARG_STR(&new)); + JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_storage_2_str_args, &old, &new); Storage* storage = JS_GET_CONTEXT(mjs); FS_Error status = storage_common_rename(storage, old, new); mjs_return(mjs, mjs_mk_boolean(mjs, status == FSE_OK)); @@ -266,7 +323,7 @@ static void js_storage_rename(struct mjs* mjs) { static void js_storage_copy(struct mjs* mjs) { const char *source, *dest; - JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&source), JS_ARG_STR(&dest)); + JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_storage_2_str_args, &source, &dest); Storage* storage = JS_GET_CONTEXT(mjs); FS_Error status = storage_common_copy(storage, source, dest); mjs_return(mjs, mjs_mk_boolean(mjs, status == FSE_OK || status == FSE_EXIST)); @@ -274,7 +331,7 @@ static void js_storage_copy(struct mjs* mjs) { static void js_storage_fs_info(struct mjs* mjs) { const char* fs; - JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&fs)); + JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_storage_1_str_args, &fs); Storage* storage = JS_GET_CONTEXT(mjs); uint64_t total_space, free_space; if(storage_common_fs_info(storage, fs, &total_space, &free_space) != FSE_OK) { @@ -290,15 +347,19 @@ static void js_storage_fs_info(struct mjs* mjs) { } static void js_storage_next_available_filename(struct mjs* mjs) { + static const JsValueDeclaration js_storage_naf_arg_list[] = { + JS_VALUE_SIMPLE(JsValueTypeString), + JS_VALUE_SIMPLE(JsValueTypeString), + JS_VALUE_SIMPLE(JsValueTypeString), + JS_VALUE_SIMPLE(JsValueTypeInt32), + }; + static const JsValueArguments js_storage_naf_args = JS_VALUE_ARGS(js_storage_naf_arg_list); + const char *dir_path, *file_name, *file_ext; int32_t max_len; - JS_FETCH_ARGS_OR_RETURN( - mjs, - JS_EXACTLY, - JS_ARG_STR(&dir_path), - JS_ARG_STR(&file_name), - JS_ARG_STR(&file_ext), - JS_ARG_INT32(&max_len)); + JS_VALUE_PARSE_ARGS_OR_RETURN( + mjs, &js_storage_naf_args, &dir_path, &file_name, &file_ext, &max_len); + Storage* storage = JS_GET_CONTEXT(mjs); FuriString* next_name = furi_string_alloc(); storage_get_next_filename(storage, dir_path, file_name, file_ext, next_name, max_len); @@ -306,23 +367,27 @@ static void js_storage_next_available_filename(struct mjs* mjs) { furi_string_free(next_name); } -// ---=== path ops ===--- +// =============== +// Path operations +// =============== static void js_storage_are_paths_equal(struct mjs* mjs) { const char *path1, *path2; - JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&path1), JS_ARG_STR(&path2)); + JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_storage_2_str_args, &path1, &path2); Storage* storage = JS_GET_CONTEXT(mjs); mjs_return(mjs, mjs_mk_boolean(mjs, storage_common_equivalent_path(storage, path1, path2))); } static void js_storage_is_subpath_of(struct mjs* mjs) { const char *parent, *child; - JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&parent), JS_ARG_STR(&child)); + JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_storage_2_str_args, &parent, &child); Storage* storage = JS_GET_CONTEXT(mjs); mjs_return(mjs, mjs_mk_boolean(mjs, storage_common_is_subdir(storage, parent, child))); } -// ---=== module ctor & dtor ===--- +// ================== +// Module ctor & dtor +// ================== static void* js_storage_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) { UNUSED(modules); @@ -363,7 +428,9 @@ static void js_storage_destroy(void* data) { furi_record_close(RECORD_STORAGE); } -// ---=== boilerplate ===--- +// =========== +// Boilerplate +// =========== static const JsModuleDescriptor js_storage_desc = { "storage", diff --git a/applications/system/js_app/plugin_api/app_api_table_i.h b/applications/system/js_app/plugin_api/app_api_table_i.h index b2debbde8..76556bcdd 100644 --- a/applications/system/js_app/plugin_api/app_api_table_i.h +++ b/applications/system/js_app/plugin_api/app_api_table_i.h @@ -1,4 +1,5 @@ -#include "js_plugin_api.h" +#include "../js_modules.h" + /* * A list of app's private functions and objects to expose for plugins. * It is used to generate a table of symbols for import resolver to use. @@ -8,4 +9,16 @@ static constexpr auto app_api_table = sort(create_array_t( API_METHOD(js_delay_with_flags, bool, (struct mjs*, uint32_t)), API_METHOD(js_flags_set, void, (struct mjs*, uint32_t)), API_METHOD(js_flags_wait, uint32_t, (struct mjs*, uint32_t, uint32_t)), - API_METHOD(js_module_get, void*, (JsModules*, const char*)))); + API_METHOD(js_module_get, void*, (JsModules*, const char*)), + API_METHOD(js_value_buffer_size, size_t, (const JsValueParseDeclaration declaration)), + API_METHOD( + js_value_parse, + JsValueParseStatus, + (struct mjs * mjs, + const JsValueParseDeclaration declaration, + JsValueParseFlag flags, + mjs_val_t* buffer, + size_t buf_size, + mjs_val_t* source, + size_t n_c_vals, + ...)))); diff --git a/applications/system/js_app/plugin_api/js_plugin_api.h b/applications/system/js_app/plugin_api/js_plugin_api.h deleted file mode 100644 index 421b68576..000000000 --- a/applications/system/js_app/plugin_api/js_plugin_api.h +++ /dev/null @@ -1,22 +0,0 @@ -#pragma once - -#include -#include - -#ifdef __cplusplus -extern "C" { -#endif - -typedef void JsModules; - -bool js_delay_with_flags(struct mjs* mjs, uint32_t time); - -void js_flags_set(struct mjs* mjs, uint32_t flags); - -uint32_t js_flags_wait(struct mjs* mjs, uint32_t flags, uint32_t timeout); - -void* js_module_get(JsModules* modules, const char* name); - -#ifdef __cplusplus -} -#endif From 6b5d0066902cb03bf2d981f6117e4da34323fc87 Mon Sep 17 00:00:00 2001 From: Anna Antonenko Date: Sat, 5 Apr 2025 20:22:05 +0400 Subject: [PATCH 191/268] [FL-3953] Application chaining (#4105) * feat: app chaining * add `launch_current_app_after_deferred`, remove `get_referring_application` * fix naming * new api * fix f18 * fix deferred launches after errors * fix: memory leak * Updater: MIN_GAP_PAGES = 0 * loader: loader_get_application_launch_path doc * loader: fix freeze * loader: reject mlib, reduce code size * loader: generic synchronous call, reduce size * loader: reject furi_string, reduce size * apps: debug: removed order field from manifests since it is no longer meaningful --------- Co-authored-by: Aleksandr Kutuzov Co-authored-by: hedger --- applications/debug/accessor/application.fam | 1 - .../debug/battery_test_app/application.fam | 1 - applications/debug/blink_test/application.fam | 1 - .../debug/bt_debug_app/application.fam | 1 - applications/debug/ccid_test/application.fam | 1 - .../debug/direct_draw/application.fam | 1 - .../debug/display_test/application.fam | 1 - .../event_loop_blink_test/application.fam | 1 - .../debug/expansion_test/application.fam | 1 - .../debug/file_browser_test/application.fam | 1 - .../debug/keypad_test/application.fam | 1 - .../debug/lfrfid_debug/application.fam | 1 - .../debug/loader_chaining_a/application.fam | 8 + .../loader_chaining_a/loader_chaining_a.c | 164 +++++++++++++++ .../debug/loader_chaining_b/application.fam | 8 + .../loader_chaining_b/loader_chaining_b.c | 27 +++ .../debug/locale_test/application.fam | 1 - .../debug/rpc_debug_app/application.fam | 1 - .../debug/speaker_debug/application.fam | 1 - .../debug/subghz_test/application.fam | 1 - .../text_box_element_test/application.fam | 1 - .../debug/text_box_view_test/application.fam | 1 - applications/debug/uart_echo/application.fam | 1 - applications/debug/unit_tests/application.fam | 1 - applications/debug/usb_mouse/application.fam | 1 - applications/debug/usb_test/application.fam | 1 - applications/debug/vibro_test/application.fam | 1 - .../archive/scenes/archive_scene_browser.c | 2 +- applications/services/desktop/desktop.c | 4 +- applications/services/loader/loader.c | 187 ++++++++++++++---- applications/services/loader/loader.h | 50 ++++- .../services/loader/loader_applications.c | 2 +- applications/services/loader/loader_i.h | 19 ++ applications/services/loader/loader_queue.c | 32 +++ applications/services/loader/loader_queue.h | 53 +++++ targets/f18/api_symbols.csv | 3 + targets/f7/api_symbols.csv | 3 + 37 files changed, 512 insertions(+), 73 deletions(-) create mode 100644 applications/debug/loader_chaining_a/application.fam create mode 100644 applications/debug/loader_chaining_a/loader_chaining_a.c create mode 100644 applications/debug/loader_chaining_b/application.fam create mode 100644 applications/debug/loader_chaining_b/loader_chaining_b.c create mode 100644 applications/services/loader/loader_queue.c create mode 100644 applications/services/loader/loader_queue.h diff --git a/applications/debug/accessor/application.fam b/applications/debug/accessor/application.fam index 65a6c8666..4b24f98eb 100644 --- a/applications/debug/accessor/application.fam +++ b/applications/debug/accessor/application.fam @@ -6,6 +6,5 @@ App( entry_point="accessor_app", requires=["gui"], stack_size=4 * 1024, - order=40, fap_category="Debug", ) diff --git a/applications/debug/battery_test_app/application.fam b/applications/debug/battery_test_app/application.fam index 5f4acd83d..0ab68c086 100644 --- a/applications/debug/battery_test_app/application.fam +++ b/applications/debug/battery_test_app/application.fam @@ -8,7 +8,6 @@ App( "power", ], stack_size=1 * 1024, - order=130, fap_category="Debug", fap_libs=["assets"], ) diff --git a/applications/debug/blink_test/application.fam b/applications/debug/blink_test/application.fam index d7d873fb9..066e7a207 100644 --- a/applications/debug/blink_test/application.fam +++ b/applications/debug/blink_test/application.fam @@ -5,6 +5,5 @@ App( entry_point="blink_test_app", requires=["gui"], stack_size=1 * 1024, - order=10, fap_category="Debug", ) diff --git a/applications/debug/bt_debug_app/application.fam b/applications/debug/bt_debug_app/application.fam index 8ed1ccc05..831b51ade 100644 --- a/applications/debug/bt_debug_app/application.fam +++ b/applications/debug/bt_debug_app/application.fam @@ -13,6 +13,5 @@ App( "bt_debug", ], stack_size=1 * 1024, - order=110, fap_category="Debug", ) diff --git a/applications/debug/ccid_test/application.fam b/applications/debug/ccid_test/application.fam index ad9076770..dfd6de05f 100644 --- a/applications/debug/ccid_test/application.fam +++ b/applications/debug/ccid_test/application.fam @@ -10,6 +10,5 @@ App( "ccid_test", ], stack_size=1 * 1024, - order=120, fap_category="Debug", ) diff --git a/applications/debug/direct_draw/application.fam b/applications/debug/direct_draw/application.fam index 11b3bc6ba..1e7d4b1c4 100644 --- a/applications/debug/direct_draw/application.fam +++ b/applications/debug/direct_draw/application.fam @@ -5,6 +5,5 @@ App( entry_point="direct_draw_app", requires=["gui", "input"], stack_size=2 * 1024, - order=70, fap_category="Debug", ) diff --git a/applications/debug/display_test/application.fam b/applications/debug/display_test/application.fam index 7b2357b01..1e0d3f775 100644 --- a/applications/debug/display_test/application.fam +++ b/applications/debug/display_test/application.fam @@ -6,6 +6,5 @@ App( requires=["gui"], fap_libs=["u8g2"], stack_size=1 * 1024, - order=120, fap_category="Debug", ) diff --git a/applications/debug/event_loop_blink_test/application.fam b/applications/debug/event_loop_blink_test/application.fam index 7d42ad339..6e4aaa48d 100644 --- a/applications/debug/event_loop_blink_test/application.fam +++ b/applications/debug/event_loop_blink_test/application.fam @@ -5,6 +5,5 @@ App( entry_point="event_loop_blink_test_app", requires=["input"], stack_size=1 * 1024, - order=20, fap_category="Debug", ) diff --git a/applications/debug/expansion_test/application.fam b/applications/debug/expansion_test/application.fam index 9bc4b2fc2..30f325a92 100644 --- a/applications/debug/expansion_test/application.fam +++ b/applications/debug/expansion_test/application.fam @@ -6,7 +6,6 @@ App( requires=["expansion_start"], fap_libs=["assets"], stack_size=1 * 1024, - order=20, fap_category="Debug", fap_file_assets="assets", ) diff --git a/applications/debug/file_browser_test/application.fam b/applications/debug/file_browser_test/application.fam index bb08ad2c5..b610558e9 100644 --- a/applications/debug/file_browser_test/application.fam +++ b/applications/debug/file_browser_test/application.fam @@ -5,7 +5,6 @@ App( entry_point="file_browser_app", requires=["gui"], stack_size=2 * 1024, - order=150, fap_category="Debug", fap_icon_assets="icons", ) diff --git a/applications/debug/keypad_test/application.fam b/applications/debug/keypad_test/application.fam index 90851950b..ed7408e71 100644 --- a/applications/debug/keypad_test/application.fam +++ b/applications/debug/keypad_test/application.fam @@ -5,6 +5,5 @@ App( entry_point="keypad_test_app", requires=["gui"], stack_size=1 * 1024, - order=30, fap_category="Debug", ) diff --git a/applications/debug/lfrfid_debug/application.fam b/applications/debug/lfrfid_debug/application.fam index 323f77818..d312dbda2 100644 --- a/applications/debug/lfrfid_debug/application.fam +++ b/applications/debug/lfrfid_debug/application.fam @@ -11,6 +11,5 @@ App( "lfrfid_debug", ], stack_size=1 * 1024, - order=100, fap_category="Debug", ) diff --git a/applications/debug/loader_chaining_a/application.fam b/applications/debug/loader_chaining_a/application.fam new file mode 100644 index 000000000..408efdcb1 --- /dev/null +++ b/applications/debug/loader_chaining_a/application.fam @@ -0,0 +1,8 @@ +App( + appid="loader_chaining_a", + name="Loader Chaining Test: App A", + apptype=FlipperAppType.DEBUG, + entry_point="chaining_test_app_a", + stack_size=1 * 1024, + fap_category="Debug", +) diff --git a/applications/debug/loader_chaining_a/loader_chaining_a.c b/applications/debug/loader_chaining_a/loader_chaining_a.c new file mode 100644 index 000000000..b3f303e2d --- /dev/null +++ b/applications/debug/loader_chaining_a/loader_chaining_a.c @@ -0,0 +1,164 @@ +#include +#include +#include +#include +#include +#include + +#define TAG "LoaderChainingA" +#define CHAINING_TEST_B "/ext/apps/Debug/loader_chaining_b.fap" +#define NONEXISTENT_APP "Some nonexistent app" + +typedef struct { + Gui* gui; + ViewDispatcher* view_dispatcher; + Submenu* submenu; + + Loader* loader; + + DialogsApp* dialogs; +} LoaderChainingA; + +typedef enum { + LoaderChainingASubmenuLaunchB, + LoaderChainingASubmenuLaunchBThenA, + LoaderChainingASubmenuLaunchNonexistentSilent, + LoaderChainingASubmenuLaunchNonexistentGui, + LoaderChainingASubmenuLaunchNonexistentGuiThenA, +} LoaderChainingASubmenu; + +static void loader_chaining_a_submenu_callback(void* context, uint32_t index) { + LoaderChainingA* app = context; + + FuriString* self_path = furi_string_alloc(); + furi_check(loader_get_application_launch_path(app->loader, self_path)); + + switch(index) { + case LoaderChainingASubmenuLaunchB: + loader_enqueue_launch(app->loader, CHAINING_TEST_B, "Hello", LoaderDeferredLaunchFlagGui); + view_dispatcher_stop(app->view_dispatcher); + break; + + case LoaderChainingASubmenuLaunchBThenA: + loader_enqueue_launch(app->loader, CHAINING_TEST_B, "Hello", LoaderDeferredLaunchFlagGui); + loader_enqueue_launch( + app->loader, + furi_string_get_cstr(self_path), + "Hello to you from the future", + LoaderDeferredLaunchFlagGui); + + break; + + case LoaderChainingASubmenuLaunchNonexistentSilent: + loader_enqueue_launch(app->loader, NONEXISTENT_APP, NULL, LoaderDeferredLaunchFlagNone); + break; + + case LoaderChainingASubmenuLaunchNonexistentGui: + loader_enqueue_launch(app->loader, NONEXISTENT_APP, NULL, LoaderDeferredLaunchFlagGui); + break; + + case LoaderChainingASubmenuLaunchNonexistentGuiThenA: + loader_enqueue_launch(app->loader, NONEXISTENT_APP, NULL, LoaderDeferredLaunchFlagGui); + loader_enqueue_launch( + app->loader, + furi_string_get_cstr(self_path), + "Hello to you from the future", + LoaderDeferredLaunchFlagGui); + break; + } + + furi_string_free(self_path); + view_dispatcher_stop(app->view_dispatcher); +} + +static bool loader_chaining_a_nav_callback(void* context) { + LoaderChainingA* app = context; + view_dispatcher_stop(app->view_dispatcher); + return true; +} + +LoaderChainingA* loader_chaining_a_alloc(void) { + LoaderChainingA* app = malloc(sizeof(LoaderChainingA)); + app->gui = furi_record_open(RECORD_GUI); + app->loader = furi_record_open(RECORD_LOADER); + app->dialogs = furi_record_open(RECORD_DIALOGS); + app->view_dispatcher = view_dispatcher_alloc(); + app->submenu = submenu_alloc(); + + submenu_add_item( + app->submenu, + "Launch B", + LoaderChainingASubmenuLaunchB, + loader_chaining_a_submenu_callback, + app); + submenu_add_item( + app->submenu, + "Launch B, then A", + LoaderChainingASubmenuLaunchBThenA, + loader_chaining_a_submenu_callback, + app); + submenu_add_item( + app->submenu, + "Trigger error: silent", + LoaderChainingASubmenuLaunchNonexistentSilent, + loader_chaining_a_submenu_callback, + app); + submenu_add_item( + app->submenu, + "Trigger error: GUI", + LoaderChainingASubmenuLaunchNonexistentGui, + loader_chaining_a_submenu_callback, + app); + submenu_add_item( + app->submenu, + "Error, then launch A", + LoaderChainingASubmenuLaunchNonexistentGuiThenA, + loader_chaining_a_submenu_callback, + app); + + view_dispatcher_add_view(app->view_dispatcher, 0, submenu_get_view(app->submenu)); + view_dispatcher_set_navigation_event_callback( + app->view_dispatcher, loader_chaining_a_nav_callback); + view_dispatcher_set_event_callback_context(app->view_dispatcher, app); + + view_dispatcher_attach_to_gui(app->view_dispatcher, app->gui, ViewDispatcherTypeFullscreen); + view_dispatcher_switch_to_view(app->view_dispatcher, 0); + + return app; +} + +void loader_chaining_a_free(LoaderChainingA* app) { + furi_record_close(RECORD_DIALOGS); + furi_record_close(RECORD_LOADER); + furi_record_close(RECORD_GUI); + view_dispatcher_remove_view(app->view_dispatcher, 0); + submenu_free(app->submenu); + view_dispatcher_free(app->view_dispatcher); + free(app); +} + +int32_t chaining_test_app_a(const char* arg) { + LoaderChainingA* app = loader_chaining_a_alloc(); + + if(arg) { + if(strlen(arg)) { + DialogMessage* message = dialog_message_alloc(); + FuriString* text; + + dialog_message_set_header(message, "Hi, I am A", 64, 0, AlignCenter, AlignTop); + text = furi_string_alloc_printf("Me from the past says:\n%s", arg); + dialog_message_set_buttons(message, NULL, "ok!", NULL); + + dialog_message_set_text( + message, furi_string_get_cstr(text), 64, 32, AlignCenter, AlignCenter); + dialog_message_show(app->dialogs, message); + dialog_message_free(message); + furi_string_free(text); + } + } + + view_dispatcher_run(app->view_dispatcher); + + loader_chaining_a_free(app); + return 0; +} diff --git a/applications/debug/loader_chaining_b/application.fam b/applications/debug/loader_chaining_b/application.fam new file mode 100644 index 000000000..5b8767e50 --- /dev/null +++ b/applications/debug/loader_chaining_b/application.fam @@ -0,0 +1,8 @@ +App( + appid="loader_chaining_b", + name="Loader Chaining Test: App B", + apptype=FlipperAppType.DEBUG, + entry_point="chaining_test_app_b", + stack_size=1 * 1024, + fap_category="Debug", +) diff --git a/applications/debug/loader_chaining_b/loader_chaining_b.c b/applications/debug/loader_chaining_b/loader_chaining_b.c new file mode 100644 index 000000000..439e6e25e --- /dev/null +++ b/applications/debug/loader_chaining_b/loader_chaining_b.c @@ -0,0 +1,27 @@ +#include +#include +#include + +int32_t chaining_test_app_b(const char* arg) { + if(!arg) return 0; + + Loader* loader = furi_record_open(RECORD_LOADER); + DialogsApp* dialogs = furi_record_open(RECORD_DIALOGS); + + DialogMessage* message = dialog_message_alloc(); + dialog_message_set_header(message, "Hi, I am B", 64, 0, AlignCenter, AlignTop); + FuriString* text = furi_string_alloc_printf("And A told me:\n%s", arg); + dialog_message_set_text(message, furi_string_get_cstr(text), 64, 32, AlignCenter, AlignCenter); + dialog_message_set_buttons(message, "Just quit", NULL, "Launch A"); + DialogMessageButton result = dialog_message_show(dialogs, message); + dialog_message_free(message); + furi_string_free(text); + + if(result == DialogMessageButtonRight) + loader_enqueue_launch( + loader, "/ext/apps/Debug/loader_chaining_a.fap", NULL, LoaderDeferredLaunchFlagGui); + + furi_record_close(RECORD_LOADER); + furi_record_close(RECORD_DIALOGS); + return 0; +} diff --git a/applications/debug/locale_test/application.fam b/applications/debug/locale_test/application.fam index d341122f9..757be7155 100644 --- a/applications/debug/locale_test/application.fam +++ b/applications/debug/locale_test/application.fam @@ -5,6 +5,5 @@ App( entry_point="locale_test_app", requires=["gui", "locale"], stack_size=2 * 1024, - order=70, fap_category="Debug", ) diff --git a/applications/debug/rpc_debug_app/application.fam b/applications/debug/rpc_debug_app/application.fam index d71065afa..795f83287 100644 --- a/applications/debug/rpc_debug_app/application.fam +++ b/applications/debug/rpc_debug_app/application.fam @@ -5,6 +5,5 @@ App( entry_point="rpc_debug_app", requires=["gui", "rpc_start", "notification"], stack_size=2 * 1024, - order=10, fap_category="Debug", ) diff --git a/applications/debug/speaker_debug/application.fam b/applications/debug/speaker_debug/application.fam index 68d8b188b..c7f5629a7 100644 --- a/applications/debug/speaker_debug/application.fam +++ b/applications/debug/speaker_debug/application.fam @@ -5,7 +5,6 @@ App( entry_point="speaker_debug_app", requires=["gui", "notification"], stack_size=2 * 1024, - order=10, fap_category="Debug", fap_libs=["music_worker"], ) diff --git a/applications/debug/subghz_test/application.fam b/applications/debug/subghz_test/application.fam index 1b3e19d73..927ca7f89 100644 --- a/applications/debug/subghz_test/application.fam +++ b/applications/debug/subghz_test/application.fam @@ -6,7 +6,6 @@ App( entry_point="subghz_test_app", requires=["gui"], stack_size=4 * 1024, - order=50, fap_icon="subghz_test_10px.png", fap_category="Debug", fap_icon_assets="images", diff --git a/applications/debug/text_box_element_test/application.fam b/applications/debug/text_box_element_test/application.fam index 5e1abcddc..78dfe75f6 100644 --- a/applications/debug/text_box_element_test/application.fam +++ b/applications/debug/text_box_element_test/application.fam @@ -5,6 +5,5 @@ App( entry_point="text_box_element_test_app", requires=["gui"], stack_size=1 * 1024, - order=140, fap_category="Debug", ) diff --git a/applications/debug/text_box_view_test/application.fam b/applications/debug/text_box_view_test/application.fam index e356a278e..6a3225d88 100644 --- a/applications/debug/text_box_view_test/application.fam +++ b/applications/debug/text_box_view_test/application.fam @@ -5,6 +5,5 @@ App( entry_point="text_box_view_test_app", requires=["gui"], stack_size=1 * 1024, - order=140, fap_category="Debug", ) diff --git a/applications/debug/uart_echo/application.fam b/applications/debug/uart_echo/application.fam index 7b030bcfa..d95302364 100644 --- a/applications/debug/uart_echo/application.fam +++ b/applications/debug/uart_echo/application.fam @@ -5,6 +5,5 @@ App( entry_point="uart_echo_app", requires=["gui"], stack_size=2 * 1024, - order=70, fap_category="Debug", ) diff --git a/applications/debug/unit_tests/application.fam b/applications/debug/unit_tests/application.fam index 05e834402..ed5f8c9da 100644 --- a/applications/debug/unit_tests/application.fam +++ b/applications/debug/unit_tests/application.fam @@ -7,7 +7,6 @@ App( requires=["system_settings", "cli_subghz"], provides=["delay_test"], resources="resources", - order=100, ) App( diff --git a/applications/debug/usb_mouse/application.fam b/applications/debug/usb_mouse/application.fam index 7747613d5..e57b3f108 100644 --- a/applications/debug/usb_mouse/application.fam +++ b/applications/debug/usb_mouse/application.fam @@ -5,6 +5,5 @@ App( entry_point="usb_mouse_app", requires=["gui"], stack_size=1 * 1024, - order=60, fap_category="Debug", ) diff --git a/applications/debug/usb_test/application.fam b/applications/debug/usb_test/application.fam index 463bb4a26..6481518b4 100644 --- a/applications/debug/usb_test/application.fam +++ b/applications/debug/usb_test/application.fam @@ -5,6 +5,5 @@ App( entry_point="usb_test_app", requires=["gui"], stack_size=1 * 1024, - order=50, fap_category="Debug", ) diff --git a/applications/debug/vibro_test/application.fam b/applications/debug/vibro_test/application.fam index c35a7223f..dafa83eac 100644 --- a/applications/debug/vibro_test/application.fam +++ b/applications/debug/vibro_test/application.fam @@ -5,6 +5,5 @@ App( entry_point="vibro_test_app", requires=["gui"], stack_size=1 * 1024, - order=20, fap_category="Debug", ) diff --git a/applications/main/archive/scenes/archive_scene_browser.c b/applications/main/archive/scenes/archive_scene_browser.c index d09595037..1b6088035 100644 --- a/applications/main/archive/scenes/archive_scene_browser.c +++ b/applications/main/archive/scenes/archive_scene_browser.c @@ -42,7 +42,7 @@ static void archive_loader_callback(const void* message, void* context) { const LoaderEvent* event = message; ArchiveApp* archive = (ArchiveApp*)context; - if(event->type == LoaderEventTypeApplicationStopped) { + if(event->type == LoaderEventTypeNoMoreAppsInQueue) { view_dispatcher_send_custom_event( archive->view_dispatcher, ArchiveBrowserEventListRefresh); } diff --git a/applications/services/desktop/desktop.c b/applications/services/desktop/desktop.c index 36536b99f..60f1c21b9 100644 --- a/applications/services/desktop/desktop.c +++ b/applications/services/desktop/desktop.c @@ -27,9 +27,7 @@ static void desktop_loader_callback(const void* message, void* context) { if(event->type == LoaderEventTypeApplicationBeforeLoad) { view_dispatcher_send_custom_event(desktop->view_dispatcher, DesktopGlobalBeforeAppStarted); furi_check(furi_semaphore_acquire(desktop->animation_semaphore, 3000) == FuriStatusOk); - } else if( - event->type == LoaderEventTypeApplicationLoadFailed || - event->type == LoaderEventTypeApplicationStopped) { + } else if(event->type == LoaderEventTypeNoMoreAppsInQueue) { view_dispatcher_send_custom_event(desktop->view_dispatcher, DesktopGlobalAfterAppFinished); } } diff --git a/applications/services/loader/loader.c b/applications/services/loader/loader.c index 72cac4b62..d3cd0022e 100644 --- a/applications/services/loader/loader.c +++ b/applications/services/loader/loader.c @@ -167,6 +167,13 @@ static void loader_show_gui_error( furi_record_close(RECORD_DIALOGS); } +static void loader_generic_synchronous_request(Loader* loader, LoaderMessage* message) { + furi_check(loader); + message->api_lock = api_lock_alloc_locked(); + furi_message_queue_put(loader->queue, message, FuriWaitForever); + api_lock_wait_unlock_and_free(message->api_lock); +} + LoaderStatus loader_start(Loader* loader, const char* name, const char* args, FuriString* error_message) { furi_check(loader); @@ -202,16 +209,12 @@ void loader_start_detached_with_gui_error(Loader* loader, const char* name, cons } bool loader_lock(Loader* loader) { - furi_check(loader); - - LoaderMessage message; LoaderMessageBoolResult result; - message.type = LoaderMessageTypeLock; - message.api_lock = api_lock_alloc_locked(); - message.bool_value = &result; - furi_message_queue_put(loader->queue, &message, FuriWaitForever); - api_lock_wait_unlock_and_free(message.api_lock); - + LoaderMessage message = { + .type = LoaderMessageTypeLock, + .bool_value = &result, + }; + loader_generic_synchronous_request(loader, &message); return result.value; } @@ -225,16 +228,12 @@ void loader_unlock(Loader* loader) { } bool loader_is_locked(Loader* loader) { - furi_check(loader); - - LoaderMessage message; LoaderMessageBoolResult result; - message.type = LoaderMessageTypeIsLocked; - message.api_lock = api_lock_alloc_locked(); - message.bool_value = &result; - furi_message_queue_put(loader->queue, &message, FuriWaitForever); - api_lock_wait_unlock_and_free(message.api_lock); - + LoaderMessage message = { + .type = LoaderMessageTypeIsLocked, + .bool_value = &result, + }; + loader_generic_synchronous_request(loader, &message); return result.value; } @@ -256,42 +255,63 @@ FuriPubSub* loader_get_pubsub(Loader* loader) { } bool loader_signal(Loader* loader, uint32_t signal, void* arg) { - furi_check(loader); - LoaderMessageBoolResult result; - LoaderMessage message = { .type = LoaderMessageTypeSignal, - .api_lock = api_lock_alloc_locked(), .signal.signal = signal, .signal.arg = arg, .bool_value = &result, }; - - furi_message_queue_put(loader->queue, &message, FuriWaitForever); - api_lock_wait_unlock_and_free(message.api_lock); - + loader_generic_synchronous_request(loader, &message); return result.value; } bool loader_get_application_name(Loader* loader, FuriString* name) { - furi_check(loader); - LoaderMessageBoolResult result; - LoaderMessage message = { .type = LoaderMessageTypeGetApplicationName, - .api_lock = api_lock_alloc_locked(), .application_name = name, .bool_value = &result, }; - - furi_message_queue_put(loader->queue, &message, FuriWaitForever); - api_lock_wait_unlock_and_free(message.api_lock); - + loader_generic_synchronous_request(loader, &message); return result.value; } +bool loader_get_application_launch_path(Loader* loader, FuriString* name) { + LoaderMessageBoolResult result; + LoaderMessage message = { + .type = LoaderMessageTypeGetApplicationLaunchPath, + .application_name = name, + .bool_value = &result, + }; + loader_generic_synchronous_request(loader, &message); + return result.value; +} + +void loader_enqueue_launch( + Loader* loader, + const char* name, + const char* args, + LoaderDeferredLaunchFlag flags) { + LoaderMessage message = { + .type = LoaderMessageTypeEnqueueLaunch, + .defer_start = + { + .name_or_path = strdup(name), + .args = args ? strdup(args) : NULL, + .flags = flags, + }, + }; + loader_generic_synchronous_request(loader, &message); +} + +void loader_clear_launch_queue(Loader* loader) { + LoaderMessage message = { + .type = LoaderMessageTypeClearLaunchQueue, + }; + loader_generic_synchronous_request(loader, &message); +} + // callbacks static void loader_menu_closed_callback(void* context) { @@ -328,12 +348,10 @@ static Loader* loader_alloc(void) { Loader* loader = malloc(sizeof(Loader)); loader->pubsub = furi_pubsub_alloc(); loader->queue = furi_message_queue_alloc(1, sizeof(LoaderMessage)); - loader->loader_menu = NULL; - loader->loader_applications = NULL; - loader->app.args = NULL; - loader->app.thread = NULL; - loader->app.insomniac = false; - loader->app.fap = NULL; + loader->gui = furi_record_open(RECORD_GUI); + loader->view_holder = view_holder_alloc(); + loader->loading = loading_alloc(); + view_holder_attach_to_gui(loader->view_holder, loader->gui); return loader; } @@ -656,6 +674,10 @@ static LoaderMessageLoaderStatusResult loader_do_start_by_name( LoaderStatusErrorUnknownApp, error_message, "Application \"%s\" not found", name); } while(false); + if(status.value == LoaderStatusOk) { + loader->app.launch_path = furi_string_alloc_set_str(name); + } + return status; } @@ -673,6 +695,57 @@ static void loader_do_unlock(Loader* loader) { loader->app.thread = NULL; } +static void loader_do_emit_queue_empty_event(Loader* loader) { + FURI_LOG_I(TAG, "Launch queue empty"); + LoaderEvent event; + event.type = LoaderEventTypeNoMoreAppsInQueue; + furi_pubsub_publish(loader->pubsub, &event); +} + +static bool loader_do_deferred_launch(Loader* loader, LoaderDeferredLaunchRecord* record); + +static void loader_do_next_deferred_launch_if_available(Loader* loader) { + LoaderDeferredLaunchRecord record; + if(loader_queue_pop(&loader->launch_queue, &record)) { + loader_do_deferred_launch(loader, &record); + loader_queue_item_clear(&record); + } else { + loader_do_emit_queue_empty_event(loader); + } +} + +static bool loader_do_deferred_launch(Loader* loader, LoaderDeferredLaunchRecord* record) { + furi_assert(loader); + furi_assert(record); + + bool is_successful = false; + FuriString* error_message = furi_string_alloc(); + view_holder_set_view(loader->view_holder, loading_get_view(loader->loading)); + view_holder_send_to_front(loader->view_holder); + + do { + const char* app_name_str = record->name_or_path; + const char* app_args = record->args; + FURI_LOG_I(TAG, "Deferred launch: %s", app_name_str); + + LoaderMessageLoaderStatusResult result = + loader_do_start_by_name(loader, app_name_str, app_args, error_message); + if(result.value == LoaderStatusOk) { + is_successful = true; + break; + } + + if(record->flags & LoaderDeferredLaunchFlagGui) + loader_show_gui_error(result, app_name_str, error_message); + + loader_do_next_deferred_launch_if_available(loader); + } while(false); + + view_holder_set_view(loader->view_holder, NULL); + furi_string_free(error_message); + return is_successful; +} + static void loader_do_app_closed(Loader* loader) { furi_assert(loader->app.thread); @@ -697,11 +770,15 @@ static void loader_do_app_closed(Loader* loader) { loader->app.thread = NULL; } + furi_string_free(loader->app.launch_path); + FURI_LOG_I(TAG, "Application stopped. Free heap: %zu", memmgr_get_free_heap()); LoaderEvent event; event.type = LoaderEventTypeApplicationStopped; furi_pubsub_publish(loader->pubsub, &event); + + loader_do_next_deferred_launch_if_available(loader); } static bool loader_is_application_running(Loader* loader) { @@ -726,6 +803,15 @@ static bool loader_do_get_application_name(Loader* loader, FuriString* name) { return false; } +static bool loader_do_get_application_launch_path(Loader* loader, FuriString* path) { + if(loader_is_application_running(loader)) { + furi_string_set(path, loader->app.launch_path); + return true; + } + + return false; +} + // app int32_t loader_srv(void* p) { @@ -748,16 +834,20 @@ int32_t loader_srv(void* p) { while(true) { if(furi_message_queue_get(loader->queue, &message, FuriWaitForever) == FuriStatusOk) { switch(message.type) { - case LoaderMessageTypeStartByName: - *(message.status_value) = loader_do_start_by_name( + case LoaderMessageTypeStartByName: { + LoaderMessageLoaderStatusResult status = loader_do_start_by_name( loader, message.start.name, message.start.args, message.start.error_message); + *(message.status_value) = status; + if(status.value != LoaderStatusOk) loader_do_emit_queue_empty_event(loader); api_lock_unlock(message.api_lock); break; + } case LoaderMessageTypeStartByNameDetachedWithGuiError: { FuriString* error_message = furi_string_alloc(); LoaderMessageLoaderStatusResult status = loader_do_start_by_name( loader, message.start.name, message.start.args, error_message); loader_show_gui_error(status, message.start.name, error_message); + if(status.value != LoaderStatusOk) loader_do_emit_queue_empty_event(loader); if(message.start.name) free((void*)message.start.name); if(message.start.args) free((void*)message.start.args); furi_string_free(error_message); @@ -796,6 +886,19 @@ int32_t loader_srv(void* p) { loader_do_get_application_name(loader, message.application_name); api_lock_unlock(message.api_lock); break; + case LoaderMessageTypeGetApplicationLaunchPath: + message.bool_value->value = + loader_do_get_application_launch_path(loader, message.application_name); + api_lock_unlock(message.api_lock); + break; + case LoaderMessageTypeEnqueueLaunch: + furi_check(loader_queue_push(&loader->launch_queue, &message.defer_start)); + api_lock_unlock(message.api_lock); + break; + case LoaderMessageTypeClearLaunchQueue: + loader_queue_clear(&loader->launch_queue); + api_lock_unlock(message.api_lock); + break; } } } diff --git a/applications/services/loader/loader.h b/applications/services/loader/loader.h index cacfbff68..d732379a7 100644 --- a/applications/services/loader/loader.h +++ b/applications/services/loader/loader.h @@ -20,13 +20,19 @@ typedef enum { typedef enum { LoaderEventTypeApplicationBeforeLoad, LoaderEventTypeApplicationLoadFailed, - LoaderEventTypeApplicationStopped + LoaderEventTypeApplicationStopped, + LoaderEventTypeNoMoreAppsInQueue, //type == LoaderEventTypeApplicationStopped) { + if(event->type == LoaderEventTypeNoMoreAppsInQueue) { furi_thread_flags_set(thread_id, APPLICATION_STOP_EVENT); } } diff --git a/applications/services/loader/loader_i.h b/applications/services/loader/loader_i.h index 92f1e88e0..2bf42c655 100644 --- a/applications/services/loader/loader_i.h +++ b/applications/services/loader/loader_i.h @@ -2,11 +2,20 @@ #include #include #include + +#include +#include +#include + +#include + #include "loader.h" #include "loader_menu.h" #include "loader_applications.h" +#include "loader_queue.h" typedef struct { + FuriString* launch_path; char* args; FuriThread* thread; bool insomniac; @@ -19,6 +28,12 @@ struct Loader { LoaderMenu* loader_menu; LoaderApplications* loader_applications; LoaderAppData app; + + LoaderLaunchQueue launch_queue; + + Gui* gui; + ViewHolder* view_holder; + Loading* loading; }; typedef enum { @@ -33,6 +48,9 @@ typedef enum { LoaderMessageTypeStartByNameDetachedWithGuiError, LoaderMessageTypeSignal, LoaderMessageTypeGetApplicationName, + LoaderMessageTypeGetApplicationLaunchPath, + LoaderMessageTypeEnqueueLaunch, + LoaderMessageTypeClearLaunchQueue, } LoaderMessageType; typedef struct { @@ -72,6 +90,7 @@ typedef struct { union { LoaderMessageStartByName start; + LoaderDeferredLaunchRecord defer_start; LoaderMessageSignal signal; FuriString* application_name; }; diff --git a/applications/services/loader/loader_queue.c b/applications/services/loader/loader_queue.c new file mode 100644 index 000000000..517dcad75 --- /dev/null +++ b/applications/services/loader/loader_queue.c @@ -0,0 +1,32 @@ +#include "loader_queue.h" + +void loader_queue_item_clear(LoaderDeferredLaunchRecord* item) { + free(item->args); + free(item->name_or_path); +} + +bool loader_queue_pop(LoaderLaunchQueue* queue, LoaderDeferredLaunchRecord* item) { + if(!queue->item_cnt) return false; + + *item = queue->items[0]; + queue->item_cnt--; + memmove( + &queue->items[0], &queue->items[1], queue->item_cnt * sizeof(LoaderDeferredLaunchRecord)); + + return true; +} + +bool loader_queue_push(LoaderLaunchQueue* queue, LoaderDeferredLaunchRecord* item) { + if(queue->item_cnt == LOADER_QUEUE_MAX_SIZE) return false; + + queue->items[queue->item_cnt] = *item; + queue->item_cnt++; + + return true; +} + +void loader_queue_clear(LoaderLaunchQueue* queue) { + for(size_t i = 0; i < queue->item_cnt; i++) + loader_queue_item_clear(&queue->items[i]); + queue->item_cnt = 0; +} diff --git a/applications/services/loader/loader_queue.h b/applications/services/loader/loader_queue.h new file mode 100644 index 000000000..c40130e39 --- /dev/null +++ b/applications/services/loader/loader_queue.h @@ -0,0 +1,53 @@ +#pragma once + +#include + +#include "loader.h" + +#define LOADER_QUEUE_MAX_SIZE 4 + +typedef struct { + char* name_or_path; + char* args; + LoaderDeferredLaunchFlag flags; +} LoaderDeferredLaunchRecord; + +typedef struct { + LoaderDeferredLaunchRecord items[LOADER_QUEUE_MAX_SIZE]; + size_t item_cnt; +} LoaderLaunchQueue; + +/** + * @brief Frees internal data in a `DeferredLaunchRecord` + * + * @param[out] item Record to clear + */ +void loader_queue_item_clear(LoaderDeferredLaunchRecord* item); + +/** + * @brief Fetches the next item from the launch queue + * + * @param[inout] queue Queue instance + * @param[out] item Item output + * + * @return `true` if `item` was populated, `false` if queue is empty + */ +bool loader_queue_pop(LoaderLaunchQueue* queue, LoaderDeferredLaunchRecord* item); + +/** + * @brief Puts an item into the launch queue + * + * @param[inout] queue Queue instance + * @param[in] item Item to put in the queue + * + * @return `true` if the item was put into the queue, `false` if there's no more + * space left + */ +bool loader_queue_push(LoaderLaunchQueue* queue, LoaderDeferredLaunchRecord* item); + +/** + * @brief Clears the launch queue + * + * @param[inout] queue Queue instance + */ +void loader_queue_clear(LoaderLaunchQueue* queue); diff --git a/targets/f18/api_symbols.csv b/targets/f18/api_symbols.csv index ce47fe5c2..aa3a3813a 100644 --- a/targets/f18/api_symbols.csv +++ b/targets/f18/api_symbols.csv @@ -1860,6 +1860,9 @@ Function,-,llrintl,long long int,long double Function,-,llround,long long int,double Function,-,llroundf,long long int,float Function,-,llroundl,long long int,long double +Function,+,loader_clear_launch_queue,void,Loader* +Function,+,loader_enqueue_launch,void,"Loader*, const char*, const char*, LoaderDeferredLaunchFlag" +Function,+,loader_get_application_launch_path,_Bool,"Loader*, FuriString*" Function,+,loader_get_application_name,_Bool,"Loader*, FuriString*" Function,+,loader_get_pubsub,FuriPubSub*,Loader* Function,+,loader_is_locked,_Bool,Loader* diff --git a/targets/f7/api_symbols.csv b/targets/f7/api_symbols.csv index e19281e2a..437a86a49 100644 --- a/targets/f7/api_symbols.csv +++ b/targets/f7/api_symbols.csv @@ -2288,6 +2288,9 @@ Function,-,llrintl,long long int,long double Function,-,llround,long long int,double Function,-,llroundf,long long int,float Function,-,llroundl,long long int,long double +Function,+,loader_clear_launch_queue,void,Loader* +Function,+,loader_enqueue_launch,void,"Loader*, const char*, const char*, LoaderDeferredLaunchFlag" +Function,+,loader_get_application_launch_path,_Bool,"Loader*, FuriString*" Function,+,loader_get_application_name,_Bool,"Loader*, FuriString*" Function,+,loader_get_pubsub,FuriPubSub*,Loader* Function,+,loader_is_locked,_Bool,Loader* From 380402a55cca7c0ee07a0b3ccd8d2d5f17ed653b Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Sat, 5 Apr 2025 23:40:13 +0300 Subject: [PATCH 192/268] revert cli changes before apply of new ones --- applications/main/ibutton/application.fam | 11 +------ applications/main/ibutton/ibutton_cli.c | 34 ++++++++++----------- applications/main/ibutton/ibutton_start.c | 11 ------- applications/main/infrared/application.fam | 15 ++------- applications/main/infrared/infrared_cli.c | 20 +++++------- applications/main/infrared/infrared_start.c | 11 ------- applications/main/lfrfid/application.fam | 11 +------ applications/main/lfrfid/lfrfid_cli.c | 22 ++++++------- applications/main/lfrfid/lfrfid_start.c | 11 ------- applications/main/nfc/application.fam | 11 +------ applications/main/nfc/nfc_cli.c | 19 +++++------- applications/main/nfc/nfc_start.c | 11 ------- applications/main/onewire/application.fam | 10 ------ applications/main/onewire/onewire_cli.c | 27 ++++++++-------- applications/main/onewire/onewire_start.c | 11 ------- applications/services/cli/cli.c | 20 ------------ applications/services/cli/cli_i.h | 7 ----- 17 files changed, 61 insertions(+), 201 deletions(-) delete mode 100644 applications/main/ibutton/ibutton_start.c delete mode 100644 applications/main/infrared/infrared_start.c delete mode 100644 applications/main/lfrfid/lfrfid_start.c delete mode 100644 applications/main/nfc/nfc_start.c delete mode 100644 applications/main/onewire/onewire_start.c diff --git a/applications/main/ibutton/application.fam b/applications/main/ibutton/application.fam index 06455aeb9..01c02ec23 100644 --- a/applications/main/ibutton/application.fam +++ b/applications/main/ibutton/application.fam @@ -12,20 +12,11 @@ App( fap_category="iButton", ) -App( - appid="ibutton_cli", - targets=["f7"], - apptype=FlipperAppType.PLUGIN, - entry_point="ibutton_cli_plugin_ep", - requires=["cli"], - sources=["ibutton_cli.c"], -) - App( appid="ibutton_start", apptype=FlipperAppType.STARTUP, targets=["f7"], entry_point="ibutton_on_system_start", - sources=["ibutton_start.c"], + sources=["ibutton_cli.c"], order=60, ) diff --git a/applications/main/ibutton/ibutton_cli.c b/applications/main/ibutton/ibutton_cli.c index dcac8f963..2be75cb75 100644 --- a/applications/main/ibutton/ibutton_cli.c +++ b/applications/main/ibutton/ibutton_cli.c @@ -8,6 +8,19 @@ #include #include +static void ibutton_cli(Cli* cli, FuriString* args, void* context); + +// app cli function +void ibutton_on_system_start(void) { +#ifdef SRV_CLI + Cli* cli = furi_record_open(RECORD_CLI); + cli_add_command(cli, "ikey", CliCommandFlagDefault, ibutton_cli, cli); + furi_record_close(RECORD_CLI); +#else + UNUSED(ibutton_cli); +#endif +} + static void ibutton_cli_print_usage(void) { printf("Usage:\r\n"); printf("ikey read\r\n"); @@ -18,7 +31,7 @@ static void ibutton_cli_print_usage(void) { printf("\tCyfral (2 bytes key_data)\r\n"); printf("\tMetakom (4 bytes key_data), must contain correct parity\r\n"); printf("\t are hex-formatted\r\n"); -} +}; static bool ibutton_cli_parse_key(iButtonProtocols* protocols, iButtonKey* key, FuriString* args) { bool result = false; @@ -111,7 +124,7 @@ static void ibutton_cli_read(Cli* cli) { ibutton_protocols_free(protocols); furi_event_flag_free(event); -} +}; typedef struct { FuriEventFlag* event; @@ -203,7 +216,7 @@ void ibutton_cli_emulate(Cli* cli, FuriString* args) { while(!cli_cmd_interrupt_received(cli)) { furi_delay_ms(100); - } + }; } while(false); @@ -213,7 +226,7 @@ void ibutton_cli_emulate(Cli* cli, FuriString* args) { ibutton_key_free(key); ibutton_worker_free(worker); ibutton_protocols_free(protocols); -} +}; void ibutton_cli(Cli* cli, FuriString* args, void* context) { UNUSED(cli); @@ -239,16 +252,3 @@ void ibutton_cli(Cli* cli, FuriString* args, void* context) { furi_string_free(cmd); } - -#include -#include - -static const FlipperAppPluginDescriptor plugin_descriptor = { - .appid = CLI_PLUGIN_APP_ID, - .ep_api_version = CLI_PLUGIN_API_VERSION, - .entry_point = &ibutton_cli, -}; - -const FlipperAppPluginDescriptor* ibutton_cli_plugin_ep(void) { - return &plugin_descriptor; -} diff --git a/applications/main/ibutton/ibutton_start.c b/applications/main/ibutton/ibutton_start.c deleted file mode 100644 index d252bed7f..000000000 --- a/applications/main/ibutton/ibutton_start.c +++ /dev/null @@ -1,11 +0,0 @@ -#include - -static void ibutton_cli_wrapper(Cli* cli, FuriString* args, void* context) { - cli_plugin_wrapper("ibutton", cli, args, context); -} - -void ibutton_on_system_start(void) { - Cli* cli = furi_record_open(RECORD_CLI); - cli_add_command(cli, "ikey", CliCommandFlagDefault, ibutton_cli_wrapper, cli); - furi_record_close(RECORD_CLI); -} diff --git a/applications/main/infrared/application.fam b/applications/main/infrared/application.fam index 586adf110..575bebbe4 100644 --- a/applications/main/infrared/application.fam +++ b/applications/main/infrared/application.fam @@ -15,23 +15,14 @@ App( ) App( - appid="infrared_cli", + appid="infrared_start", + apptype=FlipperAppType.STARTUP, targets=["f7"], - apptype=FlipperAppType.PLUGIN, - entry_point="infrared_cli_plugin_ep", - requires=["cli"], + entry_point="infrared_on_system_start", sources=[ "infrared_cli.c", "infrared_brute_force.c", "infrared_signal.c", ], -) - -App( - appid="infrared_start", - apptype=FlipperAppType.STARTUP, - targets=["f7"], - entry_point="infrared_on_system_start", - sources=["infrared_start.c"], order=20, ) diff --git a/applications/main/infrared/infrared_cli.c b/applications/main/infrared/infrared_cli.c index d735635e2..85ae95658 100644 --- a/applications/main/infrared/infrared_cli.c +++ b/applications/main/infrared/infrared_cli.c @@ -553,16 +553,12 @@ static void infrared_cli_start_ir(Cli* cli, FuriString* args, void* context) { furi_string_free(command); } - -#include -#include - -static const FlipperAppPluginDescriptor plugin_descriptor = { - .appid = CLI_PLUGIN_APP_ID, - .ep_api_version = CLI_PLUGIN_API_VERSION, - .entry_point = &infrared_cli_start_ir, -}; - -const FlipperAppPluginDescriptor* infrared_cli_plugin_ep(void) { - return &plugin_descriptor; +void infrared_on_system_start(void) { +#ifdef SRV_CLI + Cli* cli = (Cli*)furi_record_open(RECORD_CLI); + cli_add_command(cli, "ir", CliCommandFlagDefault, infrared_cli_start_ir, NULL); + furi_record_close(RECORD_CLI); +#else + UNUSED(infrared_cli_start_ir); +#endif } diff --git a/applications/main/infrared/infrared_start.c b/applications/main/infrared/infrared_start.c deleted file mode 100644 index 6de11b677..000000000 --- a/applications/main/infrared/infrared_start.c +++ /dev/null @@ -1,11 +0,0 @@ -#include - -static void infrared_cli_start_ir_wrapper(Cli* cli, FuriString* args, void* context) { - cli_plugin_wrapper("infrared", cli, args, context); -} - -void infrared_on_system_start(void) { - Cli* cli = (Cli*)furi_record_open(RECORD_CLI); - cli_add_command(cli, "ir", CliCommandFlagDefault, infrared_cli_start_ir_wrapper, NULL); - furi_record_close(RECORD_CLI); -} diff --git a/applications/main/lfrfid/application.fam b/applications/main/lfrfid/application.fam index efded8001..c067d786f 100644 --- a/applications/main/lfrfid/application.fam +++ b/applications/main/lfrfid/application.fam @@ -12,20 +12,11 @@ App( fap_category="RFID", ) -App( - appid="lfrfid_cli", - targets=["f7"], - apptype=FlipperAppType.PLUGIN, - entry_point="lfrfid_cli_plugin_ep", - requires=["cli"], - sources=["lfrfid_cli.c"], -) - App( appid="lfrfid_start", targets=["f7"], apptype=FlipperAppType.STARTUP, entry_point="lfrfid_on_system_start", - sources=["lfrfid_start.c"], + sources=["lfrfid_cli.c"], order=50, ) diff --git a/applications/main/lfrfid/lfrfid_cli.c b/applications/main/lfrfid/lfrfid_cli.c index eaafcda92..a25032d6a 100644 --- a/applications/main/lfrfid/lfrfid_cli.c +++ b/applications/main/lfrfid/lfrfid_cli.c @@ -14,6 +14,15 @@ #include #include +static void lfrfid_cli(Cli* cli, FuriString* args, void* context); + +// app cli function +void lfrfid_on_system_start(void) { + Cli* cli = furi_record_open(RECORD_CLI); + cli_add_command(cli, "rfid", CliCommandFlagDefault, lfrfid_cli, NULL); + furi_record_close(RECORD_CLI); +} + static void lfrfid_cli_print_usage(void) { printf("Usage:\r\n"); printf("rfid read - read in ASK/PSK mode\r\n"); @@ -568,16 +577,3 @@ static void lfrfid_cli(Cli* cli, FuriString* args, void* context) { furi_string_free(cmd); } - -#include -#include - -static const FlipperAppPluginDescriptor plugin_descriptor = { - .appid = CLI_PLUGIN_APP_ID, - .ep_api_version = CLI_PLUGIN_API_VERSION, - .entry_point = &lfrfid_cli, -}; - -const FlipperAppPluginDescriptor* lfrfid_cli_plugin_ep(void) { - return &plugin_descriptor; -} diff --git a/applications/main/lfrfid/lfrfid_start.c b/applications/main/lfrfid/lfrfid_start.c deleted file mode 100644 index faf275355..000000000 --- a/applications/main/lfrfid/lfrfid_start.c +++ /dev/null @@ -1,11 +0,0 @@ -#include - -static void lfrfid_cli_wrapper(Cli* cli, FuriString* args, void* context) { - cli_plugin_wrapper("lfrfid", cli, args, context); -} - -void lfrfid_on_system_start(void) { - Cli* cli = furi_record_open(RECORD_CLI); - cli_add_command(cli, "rfid", CliCommandFlagDefault, lfrfid_cli_wrapper, NULL); - furi_record_close(RECORD_CLI); -} diff --git a/applications/main/nfc/application.fam b/applications/main/nfc/application.fam index 639a3fca7..bcc2bcbd4 100644 --- a/applications/main/nfc/application.fam +++ b/applications/main/nfc/application.fam @@ -310,15 +310,6 @@ App( sources=["plugins/supported_cards/trt.c"], ) -App( - appid="nfc_cli", - targets=["f7"], - apptype=FlipperAppType.PLUGIN, - entry_point="nfc_cli_plugin_ep", - requires=["cli"], - sources=["nfc_cli.c"], -) - App( appid="disney_infinity_parser", apptype=FlipperAppType.PLUGIN, @@ -334,6 +325,6 @@ App( targets=["f7"], apptype=FlipperAppType.STARTUP, entry_point="nfc_on_system_start", - sources=["nfc_start.c"], + sources=["nfc_cli.c"], order=30, ) diff --git a/applications/main/nfc/nfc_cli.c b/applications/main/nfc/nfc_cli.c index ea686834f..276b53e56 100644 --- a/applications/main/nfc/nfc_cli.c +++ b/applications/main/nfc/nfc_cli.c @@ -226,15 +226,12 @@ static void nfc_cli(Cli* cli, FuriString* args, void* context) { furi_string_free(cmd); } -#include -#include - -static const FlipperAppPluginDescriptor plugin_descriptor = { - .appid = CLI_PLUGIN_APP_ID, - .ep_api_version = CLI_PLUGIN_API_VERSION, - .entry_point = &nfc_cli, -}; - -const FlipperAppPluginDescriptor* nfc_cli_plugin_ep(void) { - return &plugin_descriptor; +void nfc_on_system_start(void) { +#ifdef SRV_CLI + Cli* cli = furi_record_open(RECORD_CLI); + cli_add_command(cli, "nfc", CliCommandFlagDefault, nfc_cli, NULL); + furi_record_close(RECORD_CLI); +#else + UNUSED(nfc_cli); +#endif } diff --git a/applications/main/nfc/nfc_start.c b/applications/main/nfc/nfc_start.c deleted file mode 100644 index d38a956c3..000000000 --- a/applications/main/nfc/nfc_start.c +++ /dev/null @@ -1,11 +0,0 @@ -#include - -static void nfc_cli_wrapper(Cli* cli, FuriString* args, void* context) { - cli_plugin_wrapper("nfc", cli, args, context); -} - -void nfc_on_system_start(void) { - Cli* cli = furi_record_open(RECORD_CLI); - cli_add_command(cli, "nfc", CliCommandFlagDefault, nfc_cli_wrapper, NULL); - furi_record_close(RECORD_CLI); -} diff --git a/applications/main/onewire/application.fam b/applications/main/onewire/application.fam index 9fac2ff21..3d35abce9 100644 --- a/applications/main/onewire/application.fam +++ b/applications/main/onewire/application.fam @@ -1,16 +1,6 @@ -App( - appid="onewire_cli", - targets=["f7"], - apptype=FlipperAppType.PLUGIN, - entry_point="onewire_cli_plugin_ep", - requires=["cli"], - sources=["onewire_cli.c"], -) - App( appid="onewire_start", apptype=FlipperAppType.STARTUP, entry_point="onewire_on_system_start", - sources=["onewire_start.c"], order=60, ) diff --git a/applications/main/onewire/onewire_cli.c b/applications/main/onewire/onewire_cli.c index 9114f3cf1..f7e15c295 100644 --- a/applications/main/onewire/onewire_cli.c +++ b/applications/main/onewire/onewire_cli.c @@ -8,10 +8,22 @@ #include +static void onewire_cli(Cli* cli, FuriString* args, void* context); + +void onewire_on_system_start(void) { +#ifdef SRV_CLI + Cli* cli = furi_record_open(RECORD_CLI); + cli_add_command(cli, "onewire", CliCommandFlagDefault, onewire_cli, cli); + furi_record_close(RECORD_CLI); +#else + UNUSED(onewire_cli); +#endif +} + static void onewire_cli_print_usage(void) { printf("Usage:\r\n"); printf("onewire search\r\n"); -} +}; static void onewire_cli_search(Cli* cli) { UNUSED(cli); @@ -63,16 +75,3 @@ void onewire_cli(Cli* cli, FuriString* args, void* context) { furi_string_free(cmd); } - -#include -#include - -static const FlipperAppPluginDescriptor plugin_descriptor = { - .appid = CLI_PLUGIN_APP_ID, - .ep_api_version = CLI_PLUGIN_API_VERSION, - .entry_point = &onewire_cli, -}; - -const FlipperAppPluginDescriptor* onewire_cli_plugin_ep(void) { - return &plugin_descriptor; -} diff --git a/applications/main/onewire/onewire_start.c b/applications/main/onewire/onewire_start.c deleted file mode 100644 index 219335411..000000000 --- a/applications/main/onewire/onewire_start.c +++ /dev/null @@ -1,11 +0,0 @@ -#include - -static void onewire_cli_wrapper(Cli* cli, FuriString* args, void* context) { - cli_plugin_wrapper("onewire", cli, args, context); -} - -void onewire_on_system_start(void) { - Cli* cli = furi_record_open(RECORD_CLI); - cli_add_command(cli, "onewire", CliCommandFlagDefault, onewire_cli_wrapper, cli); - furi_record_close(RECORD_CLI); -} diff --git a/applications/services/cli/cli.c b/applications/services/cli/cli.c index c9d074257..c2a0b9cb1 100644 --- a/applications/services/cli/cli.c +++ b/applications/services/cli/cli.c @@ -4,10 +4,6 @@ #include #include -#include -#include -#include - #define TAG "CliSrv" #define CLI_INPUT_LEN_LIMIT 256 @@ -486,19 +482,3 @@ int32_t cli_srv(void* p) { return 0; } - -void cli_plugin_wrapper(const char* name, Cli* cli, FuriString* args, void* context) { - PluginManager* manager = - plugin_manager_alloc(CLI_PLUGIN_APP_ID, CLI_PLUGIN_API_VERSION, firmware_api_interface); - FuriString* path = - furi_string_alloc_printf(EXT_PATH("apps_data/cli/plugins/%s_cli.fal"), name); - PluginManagerError error = plugin_manager_load_single(manager, furi_string_get_cstr(path)); - if(error == PluginManagerErrorNone) { - const CliCallback handler = plugin_manager_get_ep(manager, 0); - handler(cli, args, context); - } else { - printf("CLI plugin failed (code %" PRIu16 "), update firmware or check logs\r\n", error); - } - furi_string_free(path); - plugin_manager_free(manager); -} diff --git a/applications/services/cli/cli_i.h b/applications/services/cli/cli_i.h index c7ec8cf6e..ca126dacd 100644 --- a/applications/services/cli/cli_i.h +++ b/applications/services/cli/cli_i.h @@ -63,13 +63,6 @@ void cli_putc(Cli* cli, char c); void cli_stdout_callback(void* _cookie, const char* data, size_t size); -// Wraps CLI commands to load from plugin file -// Must call from CLI context, like dummy CLI command callback -// You need to setup the plugin to compile correctly separately -#define CLI_PLUGIN_APP_ID "cli" -#define CLI_PLUGIN_API_VERSION 1 -void cli_plugin_wrapper(const char* name, Cli* cli, FuriString* args, void* context); - #ifdef __cplusplus } #endif From 4f5cba4cd162045e5b70f528d8f9ed80508f3668 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Sat, 5 Apr 2025 23:41:23 +0300 Subject: [PATCH 193/268] Revert "Merge remote-tracking branch 'OFW/gsurkov/vcp_break_support' into dev [ci skip]" This reverts commit fc25c9fba02abb640eaed84988f7184fbfb823c6, reversing changes made to 41ae5d8981aa7ffe776b018c7574aa6ba7723e72. --- applications/main/gpio/usb_uart_bridge.c | 15 ++------------- applications/services/cli/cli_vcp.c | 1 - targets/f18/api_symbols.csv | 1 - targets/f7/api_symbols.csv | 1 - targets/f7/furi_hal/furi_hal_serial.c | 10 ---------- targets/f7/furi_hal/furi_hal_serial.h | 6 ------ targets/f7/furi_hal/furi_hal_usb_cdc.c | 13 +++---------- targets/f7/furi_hal/furi_hal_usb_cdc.h | 1 - 8 files changed, 5 insertions(+), 43 deletions(-) diff --git a/applications/main/gpio/usb_uart_bridge.c b/applications/main/gpio/usb_uart_bridge.c index b755a89c2..e6b71cb34 100644 --- a/applications/main/gpio/usb_uart_bridge.c +++ b/applications/main/gpio/usb_uart_bridge.c @@ -35,12 +35,12 @@ typedef enum { WorkerEvtLineCfgSet = (1 << 6), WorkerEvtCtrlLineSet = (1 << 7), - WorkerEvtSendBreak = (1 << 8), + } WorkerEvtFlags; #define WORKER_ALL_RX_EVENTS \ (WorkerEvtStop | WorkerEvtRxDone | WorkerEvtCfgChange | WorkerEvtLineCfgSet | \ - WorkerEvtCtrlLineSet | WorkerEvtCdcTxComplete | WorkerEvtSendBreak) + WorkerEvtCtrlLineSet | WorkerEvtCdcTxComplete) #define WORKER_ALL_TX_EVENTS (WorkerEvtTxStop | WorkerEvtCdcRx) struct UsbUartBridge { @@ -69,7 +69,6 @@ static void vcp_on_cdc_rx(void* context); static void vcp_state_callback(void* context, uint8_t state); static void vcp_on_cdc_control_line(void* context, uint8_t state); static void vcp_on_line_config(void* context, struct usb_cdc_line_coding* config); -static void vcp_on_cdc_break(void* context, uint16_t duration); static const CdcCallbacks cdc_cb = { vcp_on_cdc_tx_complete, @@ -77,7 +76,6 @@ static const CdcCallbacks cdc_cb = { vcp_state_callback, vcp_on_cdc_control_line, vcp_on_line_config, - vcp_on_cdc_break, }; /* USB UART worker */ @@ -289,9 +287,6 @@ static int32_t usb_uart_worker(void* context) { if(events & WorkerEvtCtrlLineSet) { usb_uart_update_ctrl_lines(usb_uart); } - if(events & WorkerEvtSendBreak) { - furi_hal_serial_send_break(usb_uart->serial_handle); - } } furi_hal_gpio_init(USB_USART_DE_RE_PIN, GpioModeAnalog, GpioPullNo, GpioSpeedLow); @@ -383,12 +378,6 @@ static void vcp_on_line_config(void* context, struct usb_cdc_line_coding* config furi_thread_flags_set(furi_thread_get_id(usb_uart->thread), WorkerEvtLineCfgSet); } -static void vcp_on_cdc_break(void* context, uint16_t duration) { - UNUSED(duration); - UsbUartBridge* usb_uart = (UsbUartBridge*)context; - furi_thread_flags_set(furi_thread_get_id(usb_uart->thread), WorkerEvtSendBreak); -} - UsbUartBridge* usb_uart_enable(UsbUartConfig* cfg) { UsbUartBridge* usb_uart = malloc(sizeof(UsbUartBridge)); diff --git a/applications/services/cli/cli_vcp.c b/applications/services/cli/cli_vcp.c index 488455ad6..39802bd79 100644 --- a/applications/services/cli/cli_vcp.c +++ b/applications/services/cli/cli_vcp.c @@ -57,7 +57,6 @@ static CdcCallbacks cdc_cb = { vcp_state_callback, vcp_on_cdc_control_line, NULL, - NULL, }; static CliVcp* vcp = NULL; diff --git a/targets/f18/api_symbols.csv b/targets/f18/api_symbols.csv index 04a7852e0..6f9c88732 100644 --- a/targets/f18/api_symbols.csv +++ b/targets/f18/api_symbols.csv @@ -1460,7 +1460,6 @@ Function,+,furi_hal_serial_get_gpio_pin,const GpioPin*,"FuriHalSerialHandle*, Fu Function,+,furi_hal_serial_init,void,"FuriHalSerialHandle*, uint32_t" Function,+,furi_hal_serial_is_baud_rate_supported,_Bool,"FuriHalSerialHandle*, uint32_t" Function,+,furi_hal_serial_resume,void,FuriHalSerialHandle* -Function,+,furi_hal_serial_send_break,void,FuriHalSerialHandle* Function,+,furi_hal_serial_set_br,void,"FuriHalSerialHandle*, uint32_t" Function,+,furi_hal_serial_suspend,void,FuriHalSerialHandle* Function,+,furi_hal_serial_tx,void,"FuriHalSerialHandle*, const uint8_t*, size_t" diff --git a/targets/f7/api_symbols.csv b/targets/f7/api_symbols.csv index b497c94b6..3bc8e0947 100755 --- a/targets/f7/api_symbols.csv +++ b/targets/f7/api_symbols.csv @@ -1691,7 +1691,6 @@ Function,+,furi_hal_serial_get_gpio_pin,const GpioPin*,"FuriHalSerialHandle*, Fu Function,+,furi_hal_serial_init,void,"FuriHalSerialHandle*, uint32_t" Function,+,furi_hal_serial_is_baud_rate_supported,_Bool,"FuriHalSerialHandle*, uint32_t" Function,+,furi_hal_serial_resume,void,FuriHalSerialHandle* -Function,+,furi_hal_serial_send_break,void,FuriHalSerialHandle* Function,+,furi_hal_serial_set_br,void,"FuriHalSerialHandle*, uint32_t" Function,+,furi_hal_serial_suspend,void,FuriHalSerialHandle* Function,+,furi_hal_serial_tx,void,"FuriHalSerialHandle*, const uint8_t*, size_t" diff --git a/targets/f7/furi_hal/furi_hal_serial.c b/targets/f7/furi_hal/furi_hal_serial.c index b4d47727f..8ad9794a8 100644 --- a/targets/f7/furi_hal/furi_hal_serial.c +++ b/targets/f7/furi_hal/furi_hal_serial.c @@ -1036,13 +1036,3 @@ const GpioPin* return furi_hal_serial_config[handle->id].gpio[direction]; } - -void furi_hal_serial_send_break(FuriHalSerialHandle* handle) { - furi_check(handle); - - if(handle->id == FuriHalSerialIdUsart) { - LL_USART_RequestBreakSending(USART1); - } else { - LL_LPUART_RequestBreakSending(LPUART1); - } -} diff --git a/targets/f7/furi_hal/furi_hal_serial.h b/targets/f7/furi_hal/furi_hal_serial.h index 191cf9f77..ca8860a60 100644 --- a/targets/f7/furi_hal/furi_hal_serial.h +++ b/targets/f7/furi_hal/furi_hal_serial.h @@ -256,12 +256,6 @@ void furi_hal_serial_dma_rx_stop(FuriHalSerialHandle* handle); */ size_t furi_hal_serial_dma_rx(FuriHalSerialHandle* handle, uint8_t* data, size_t len); -/** Send a break sequence (low level for the whole character duration) - * - * @param handle Serial handle - */ -void furi_hal_serial_send_break(FuriHalSerialHandle* handle); - #ifdef __cplusplus } #endif diff --git a/targets/f7/furi_hal/furi_hal_usb_cdc.c b/targets/f7/furi_hal/furi_hal_usb_cdc.c index 055618fe6..f9c1d3a42 100644 --- a/targets/f7/furi_hal/furi_hal_usb_cdc.c +++ b/targets/f7/furi_hal/furi_hal_usb_cdc.c @@ -122,7 +122,7 @@ static const struct CdcConfigDescriptorSingle cdc_cfg_desc_single = { .bFunctionLength = sizeof(struct usb_cdc_acm_desc), .bDescriptorType = USB_DTYPE_CS_INTERFACE, .bDescriptorSubType = USB_DTYPE_CDC_ACM, - .bmCapabilities = USB_CDC_CAP_BRK, + .bmCapabilities = 0, }, .cdc_union = { @@ -235,7 +235,7 @@ static const struct CdcConfigDescriptorDual .bFunctionLength = sizeof(struct usb_cdc_acm_desc), .bDescriptorType = USB_DTYPE_CS_INTERFACE, .bDescriptorSubType = USB_DTYPE_CDC_ACM, - .bmCapabilities = USB_CDC_CAP_BRK, + .bmCapabilities = 0, }, .cdc_union = { @@ -330,7 +330,7 @@ static const struct CdcConfigDescriptorDual .bFunctionLength = sizeof(struct usb_cdc_acm_desc), .bDescriptorType = USB_DTYPE_CS_INTERFACE, .bDescriptorSubType = USB_DTYPE_CDC_ACM, - .bmCapabilities = USB_CDC_CAP_BRK, + .bmCapabilities = 0, }, .cdc_union = { @@ -685,13 +685,6 @@ static usbd_respond cdc_control(usbd_device* dev, usbd_ctlreq* req, usbd_rqc_cal dev->status.data_ptr = &cdc_config[if_num]; dev->status.data_count = sizeof(cdc_config[0]); return usbd_ack; - case USB_CDC_SEND_BREAK: - if(callbacks[if_num] != NULL) { - if(callbacks[if_num]->break_callback != NULL) { - callbacks[if_num]->break_callback(cb_ctx[if_num], req->wValue); - } - } - return usbd_ack; default: return usbd_fail; } diff --git a/targets/f7/furi_hal/furi_hal_usb_cdc.h b/targets/f7/furi_hal/furi_hal_usb_cdc.h index 995e9009a..89b68991b 100644 --- a/targets/f7/furi_hal/furi_hal_usb_cdc.h +++ b/targets/f7/furi_hal/furi_hal_usb_cdc.h @@ -15,7 +15,6 @@ typedef struct { void (*state_callback)(void* context, uint8_t state); void (*ctrl_line_callback)(void* context, uint8_t state); void (*config_callback)(void* context, struct usb_cdc_line_coding* config); - void (*break_callback)(void* context, uint16_t duration); } CdcCallbacks; void furi_hal_cdc_set_callbacks(uint8_t if_num, CdcCallbacks* cb, void* context); From a57f9e22b5a1f6d9f69b3c5002d2b43a1822d0d1 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Sun, 6 Apr 2025 01:57:23 +0300 Subject: [PATCH 194/268] fix nfc --- applications/main/nfc/nfc_cli.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/applications/main/nfc/nfc_cli.c b/applications/main/nfc/nfc_cli.c index ba4d800f8..5b54d38db 100644 --- a/applications/main/nfc/nfc_cli.c +++ b/applications/main/nfc/nfc_cli.c @@ -112,8 +112,8 @@ static NfcCommand trx_callback(NfcGenericEvent event, void* context) { return NfcCommandContinue; } -static void nfc_cli_apdu(Cli* cli, FuriString* args) { - UNUSED(cli); +static void nfc_cli_apdu(PipeSide* pipe, FuriString* args) { + UNUSED(pipe); Nfc* nfc = NULL; NfcPoller* poller = NULL; FuriString* data = furi_string_alloc(); @@ -211,7 +211,7 @@ static void execute(PipeSide* pipe, FuriString* args, void* context) { break; } if(furi_string_cmp_str(cmd, "apdu") == 0) { - nfc_cli_apdu(cli, args); + nfc_cli_apdu(pipe, args); break; } if(furi_hal_rtc_is_flag_set(FuriHalRtcFlagDebug)) { From 1385ea0ea019f1cf448feb8684f802ad190f3a01 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Sun, 6 Apr 2025 03:00:37 +0300 Subject: [PATCH 195/268] some code cleanup by Willy-JL --- applications/services/bt/bt_service/bt.c | 13 ------ applications/services/bt/bt_service/bt.h | 15 ------- applications/services/bt/bt_service/bt_i.h | 12 +++++ targets/f7/api_symbols.csv | 9 +--- targets/f7/ble_glue/gap.c | 37 +-------------- targets/f7/ble_glue/gap.h | 2 - targets/f7/furi_hal/furi_hal_bt.c | 52 ---------------------- targets/f7/furi_hal/furi_hal_flash.c | 3 +- targets/furi_hal_include/furi_hal_bt.h | 9 ---- 9 files changed, 16 insertions(+), 136 deletions(-) diff --git a/applications/services/bt/bt_service/bt.c b/applications/services/bt/bt_service/bt.c index d04490502..d2e5ce2a0 100644 --- a/applications/services/bt/bt_service/bt.c +++ b/applications/services/bt/bt_service/bt.c @@ -527,19 +527,6 @@ static void bt_init_keys_settings(Bt* bt) { bt_handle_reload_keys_settings(bt); } -bool bt_remote_rssi(Bt* bt, uint8_t* rssi) { - furi_assert(bt); - - uint8_t rssi_val; - uint32_t since = furi_hal_bt_get_conn_rssi(&rssi_val); - - if(since == 0) return false; - - *rssi = rssi_val; - - return true; -} - int32_t bt_srv(void* p) { UNUSED(p); Bt* bt = bt_alloc(); diff --git a/applications/services/bt/bt_service/bt.h b/applications/services/bt/bt_service/bt.h index d49b0b3ba..403f4eb88 100644 --- a/applications/services/bt/bt_service/bt.h +++ b/applications/services/bt/bt_service/bt.h @@ -84,21 +84,6 @@ void bt_keys_storage_set_storage_path(Bt* bt, const char* keys_storage_path); */ void bt_keys_storage_set_default_path(Bt* bt); -bool bt_remote_rssi(Bt* bt, uint8_t* rssi); - -/** - * - * (Probably bad) way of opening the RPC connection, everywhereTM -*/ - -void bt_open_rpc_connection(Bt* bt); - -/** - * - * Closing the RPC connection, everywhereTM -*/ -void bt_close_rpc_connection(Bt* bt); - #ifdef __cplusplus } #endif diff --git a/applications/services/bt/bt_service/bt_i.h b/applications/services/bt/bt_service/bt_i.h index 58a60e275..fa2a0740d 100644 --- a/applications/services/bt/bt_service/bt_i.h +++ b/applications/services/bt/bt_service/bt_i.h @@ -91,3 +91,15 @@ struct Bt { uint32_t pin; bool suppress_pin_screen; }; + +/** Open a new RPC connection + * + * @param bt Bt instance + */ +void bt_open_rpc_connection(Bt* bt); + +/** Close the active RPC connection + * + * @param bt Bt instance + */ +void bt_close_rpc_connection(Bt* bt); diff --git a/targets/f7/api_symbols.csv b/targets/f7/api_symbols.csv index a5f56bf41..5cd3c1276 100755 --- a/targets/f7/api_symbols.csv +++ b/targets/f7/api_symbols.csv @@ -782,7 +782,6 @@ Function,+,ble_svc_serial_start,BleServiceSerial*, Function,+,ble_svc_serial_stop,void,BleServiceSerial* Function,+,ble_svc_serial_update_tx,_Bool,"BleServiceSerial*, uint8_t*, uint16_t" Function,-,bsearch,void*,"const void*, const void*, size_t, size_t, __compar_fn_t" -Function,-,bt_close_rpc_connection,void,Bt* Function,+,bt_disconnect,void,Bt* Function,+,bt_forget_bonded_devices,void,Bt* Function,+,bt_keys_storage_alloc,BtKeysStorage*,const char* @@ -795,10 +794,8 @@ Function,+,bt_keys_storage_set_file_path,void,"BtKeysStorage*, const char*" Function,+,bt_keys_storage_set_ram_params,void,"BtKeysStorage*, uint8_t*, uint16_t" Function,+,bt_keys_storage_set_storage_path,void,"Bt*, const char*" Function,+,bt_keys_storage_update,_Bool,"BtKeysStorage*, uint8_t*, uint32_t" -Function,-,bt_open_rpc_connection,void,Bt* Function,+,bt_profile_restore_default,_Bool,Bt* Function,+,bt_profile_start,FuriHalBleProfileBase*,"Bt*, const FuriHalBleProfileTemplate*, FuriHalBleProfileParams" -Function,+,bt_remote_rssi,_Bool,"Bt*, uint8_t*" Function,+,bt_set_status_changed_callback,void,"Bt*, BtStatusChangedCallback, void*" Function,+,buffered_file_stream_alloc,Stream*,Storage* Function,+,buffered_file_stream_close,_Bool,Stream* @@ -1331,7 +1328,6 @@ Function,+,furi_hal_bt_extra_beacon_set_config,_Bool,const GapExtraBeaconConfig* Function,+,furi_hal_bt_extra_beacon_set_data,_Bool,"const uint8_t*, uint8_t" Function,+,furi_hal_bt_extra_beacon_start,_Bool, Function,+,furi_hal_bt_extra_beacon_stop,_Bool, -Function,-,furi_hal_bt_get_conn_rssi,uint32_t,uint8_t* Function,+,furi_hal_bt_get_key_storage_buff,void,"uint8_t**, uint16_t*" Function,+,furi_hal_bt_get_radio_stack,FuriHalBtStack, Function,+,furi_hal_bt_get_rssi,float, @@ -1339,14 +1335,12 @@ Function,+,furi_hal_bt_get_transmitted_packets,uint32_t, Function,-,furi_hal_bt_init,void, Function,+,furi_hal_bt_is_active,_Bool, Function,+,furi_hal_bt_is_alive,_Bool, -Function,+,furi_hal_bt_is_connected,_Bool, Function,+,furi_hal_bt_is_gatt_gap_supported,_Bool, Function,+,furi_hal_bt_is_testing_supported,_Bool, Function,+,furi_hal_bt_lock_core2,void, Function,+,furi_hal_bt_nvm_sram_sem_acquire,void, Function,+,furi_hal_bt_nvm_sram_sem_release,void, Function,+,furi_hal_bt_reinit,void, -Function,+,furi_hal_bt_reverse_mac_addr,void,uint8_t[( 6 )] Function,+,furi_hal_bt_set_key_storage_change_callback,void,"BleGlueKeyStorageChangedCallback, void*" Function,+,furi_hal_bt_start_advertising,void, Function,+,furi_hal_bt_start_app,FuriHalBleProfileBase*,"const FuriHalBleProfileTemplate*, FuriHalBleProfileParams, GapEventCallback, void*" @@ -2008,7 +2002,6 @@ Function,-,gap_extra_beacon_set_config,_Bool,const GapExtraBeaconConfig* Function,-,gap_extra_beacon_set_data,_Bool,"const uint8_t*, uint8_t" Function,-,gap_extra_beacon_start,_Bool, Function,-,gap_extra_beacon_stop,_Bool, -Function,-,gap_get_remote_conn_rssi,uint32_t,int8_t* Function,-,gap_get_state,GapState, Function,-,gap_init,_Bool,"GapConfig*, GapEventCallback, void*" Function,-,gap_start_advertising,void, @@ -3558,9 +3551,9 @@ Function,+,subghz_keystore_get_data,SubGhzKeyArray_t*,SubGhzKeystore* Function,+,subghz_keystore_load,_Bool,"SubGhzKeystore*, const char*" Function,+,subghz_keystore_raw_encrypted_save,_Bool,"const char*, const char*, uint8_t*" Function,+,subghz_keystore_raw_get_data,_Bool,"const char*, size_t, uint8_t*, size_t" +Function,+,subghz_keystore_reset_kl,void,SubGhzKeystore* Function,+,subghz_keystore_save,_Bool,"SubGhzKeystore*, const char*, uint8_t*" Function,+,subghz_protocol_alutech_at_4n_create_data,_Bool,"void*, FlipperFormat*, uint32_t, uint8_t, uint16_t, SubGhzRadioPreset*" -Function,+,subghz_keystore_reset_kl,void,SubGhzKeystore* Function,+,subghz_protocol_blocks_add_bit,void,"SubGhzBlockDecoder*, uint8_t" Function,+,subghz_protocol_blocks_add_bytes,uint8_t,"const uint8_t[], size_t" Function,+,subghz_protocol_blocks_add_to_128_bit,void,"SubGhzBlockDecoder*, uint8_t, uint64_t*" diff --git a/targets/f7/ble_glue/gap.c b/targets/f7/ble_glue/gap.c index 538f033f4..2f4e661b9 100644 --- a/targets/f7/ble_glue/gap.c +++ b/targets/f7/ble_glue/gap.c @@ -33,8 +33,6 @@ typedef struct { GapConfig* config; GapConnectionParams connection_params; GapState state; - int8_t conn_rssi; - uint32_t time_rssi_sample; FuriMutex* state_mutex; GapEventCallback on_event_cb; void* context; @@ -65,19 +63,6 @@ static Gap* gap = NULL; static void gap_advertise_start(GapState new_state); static int32_t gap_app(void* context); -/** function for updating rssi informations in global Gap object - * -*/ -static inline void fetch_rssi(void) { - uint8_t ret_rssi = 127; - if(hci_read_rssi(gap->service.connection_handle, &ret_rssi) == BLE_STATUS_SUCCESS) { - gap->conn_rssi = (int8_t)ret_rssi; - gap->time_rssi_sample = furi_get_tick(); - return; - } - FURI_LOG_D(TAG, "Failed to read RSSI"); -} - static void gap_verify_connection_parameters(Gap* gap) { furi_check(gap); @@ -182,8 +167,6 @@ BleEventFlowStatus ble_event_app_notification(void* pckt) { FURI_LOG_I(TAG, "Connection parameters event complete"); gap_verify_connection_parameters(gap); - // Save rssi for current connection - fetch_rssi(); break; } @@ -218,8 +201,7 @@ BleEventFlowStatus ble_event_app_notification(void* pckt) { gap->service.connection_handle = event->Connection_Handle; gap_verify_connection_parameters(gap); - // Save rssi for current connection - fetch_rssi(); + if(gap->config->pairing_method != GapPairingNone) { // Start pairing by sending security request aci_gap_slave_security_req(event->Connection_Handle); @@ -303,9 +285,6 @@ BleEventFlowStatus ble_event_app_notification(void* pckt) { pairing_complete->Status); aci_gap_terminate(gap->service.connection_handle, 5); } else { - // Save RSSI - fetch_rssi(); - FURI_LOG_I(TAG, "Pairing complete"); GapEvent event = {.type = GapEventTypeConnected}; gap->on_event_cb(event, gap->context); //-V595 @@ -579,9 +558,6 @@ bool gap_init(GapConfig* config, GapEventCallback on_event_cb, void* context) { gap->service.connection_handle = 0xFFFF; gap->enable_adv = true; - gap->conn_rssi = 127; - gap->time_rssi_sample = 0; - // Command queue allocation gap->command_queue = furi_message_queue_alloc(8, sizeof(GapCommand)); @@ -621,17 +597,6 @@ bool gap_init(GapConfig* config, GapEventCallback on_event_cb, void* context) { return true; } -// Get RSSI -uint32_t gap_get_remote_conn_rssi(int8_t* rssi) { - if(gap && gap->state == GapStateConnected) { - fetch_rssi(); - *rssi = gap->conn_rssi; - - if(gap->time_rssi_sample) return furi_get_tick() - gap->time_rssi_sample; - } - return 0; -} - GapState gap_get_state(void) { GapState state; if(gap) { diff --git a/targets/f7/ble_glue/gap.h b/targets/f7/ble_glue/gap.h index 096dcb46a..2f0b097e4 100644 --- a/targets/f7/ble_glue/gap.h +++ b/targets/f7/ble_glue/gap.h @@ -96,8 +96,6 @@ void gap_thread_stop(void); void gap_emit_ble_beacon_status_event(bool active); -uint32_t gap_get_remote_conn_rssi(int8_t* rssi); - #ifdef __cplusplus } #endif diff --git a/targets/f7/furi_hal/furi_hal_bt.c b/targets/f7/furi_hal/furi_hal_bt.c index 05a066630..2d4984746 100644 --- a/targets/f7/furi_hal/furi_hal_bt.c +++ b/targets/f7/furi_hal/furi_hal_bt.c @@ -251,10 +251,6 @@ bool furi_hal_bt_is_active(void) { return gap_get_state() > GapStateIdle; } -bool furi_hal_bt_is_connected() { - return gap_get_state() == GapStateConnected; -} - void furi_hal_bt_start_advertising(void) { if(gap_get_state() == GapStateIdle) { gap_start_advertising(); @@ -367,54 +363,6 @@ void furi_hal_bt_start_rx(uint8_t channel) { aci_hal_rx_start(channel); } -float furi_hal_bt_get_rssi(void) { - float val; - uint8_t rssi_raw[3]; - - if(aci_hal_read_raw_rssi(rssi_raw) != BLE_STATUS_SUCCESS) { - return 0.0f; - } - - // Some ST magic with rssi - uint8_t agc = rssi_raw[2] & 0xFF; - int rssi = (((int)rssi_raw[1] << 8) & 0xFF00) + (rssi_raw[0] & 0xFF); - if(rssi == 0 || agc > 11) { - val = -127.0; - } else { - val = agc * 6.0f - 127.0f; - while(rssi > 30) { - val += 6.0; - rssi >>= 1; - } - val += (float)((417 * rssi + 18080) >> 10); - } - return val; -} - -/** fill the RSSI of the remote host of the bt connection and returns the last - * time the RSSI was updated - * -*/ -uint32_t furi_hal_bt_get_conn_rssi(uint8_t* rssi) { - int8_t ret_rssi = 0; - uint32_t since = gap_get_remote_conn_rssi(&ret_rssi); - - if(ret_rssi == 127 || since == 0) return 0; - - *rssi = (uint8_t)abs(ret_rssi); - - return since; -} - -void furi_hal_bt_reverse_mac_addr(uint8_t mac_addr[GAP_MAC_ADDR_SIZE]) { - uint8_t tmp; - for(size_t i = 0; i < GAP_MAC_ADDR_SIZE / 2; i++) { - tmp = mac_addr[i]; - mac_addr[i] = mac_addr[GAP_MAC_ADDR_SIZE - 1 - i]; - mac_addr[GAP_MAC_ADDR_SIZE - 1 - i] = tmp; - } -} - uint32_t furi_hal_bt_get_transmitted_packets(void) { uint32_t packets = 0; aci_hal_le_tx_test_packet_number(&packets); diff --git a/targets/f7/furi_hal/furi_hal_flash.c b/targets/f7/furi_hal/furi_hal_flash.c index 28aa45173..c28f6b520 100644 --- a/targets/f7/furi_hal/furi_hal_flash.c +++ b/targets/f7/furi_hal/furi_hal_flash.c @@ -51,7 +51,8 @@ // Changing furi_assert() to furi_check() brought timeout crashes // Internal storage is very slow, and "big" files will often cause a "timeout" with 3 seconds // 10 seconds seems fine, the file operations complete successfully, albeit slowly -#define FURI_HAL_FLASH_C2_LOCK_TIMEOUT_MS (10000U) /* 10 seconds */ +//#define FURI_HAL_FLASH_C2_LOCK_TIMEOUT_MS (10000U) /* 10 seconds */ +#define FURI_HAL_FLASH_C2_LOCK_TIMEOUT_MS (3000U) /* 3 seconds */ #define IS_ADDR_ALIGNED_64BITS(__VALUE__) (((__VALUE__) & 0x7U) == (0x00UL)) #define IS_FLASH_PROGRAM_ADDRESS(__VALUE__) \ diff --git a/targets/furi_hal_include/furi_hal_bt.h b/targets/furi_hal_include/furi_hal_bt.h index 14c2975ac..6da723311 100644 --- a/targets/furi_hal_include/furi_hal_bt.h +++ b/targets/furi_hal_include/furi_hal_bt.h @@ -231,15 +231,6 @@ float furi_hal_bt_get_rssi(void); */ uint32_t furi_hal_bt_get_transmitted_packets(void); -/** Reverse a MAC address byte order in-place - * @param[in] mac mac address to reverse -*/ -void furi_hal_bt_reverse_mac_addr(uint8_t mac_addr[GAP_MAC_ADDR_SIZE]); - -uint32_t furi_hal_bt_get_conn_rssi(uint8_t* rssi); - -bool furi_hal_bt_is_connected(void); - /** Check & switch C2 to given mode * * @param[in] mode mode to switch into From 324b8ddb95992c3296b1f59b10a7853e27fef7d6 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Sun, 6 Apr 2025 04:44:09 +0300 Subject: [PATCH 196/268] oops --- targets/f7/furi_hal/furi_hal_bt.c | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/targets/f7/furi_hal/furi_hal_bt.c b/targets/f7/furi_hal/furi_hal_bt.c index 2d4984746..2c1a9367b 100644 --- a/targets/f7/furi_hal/furi_hal_bt.c +++ b/targets/f7/furi_hal/furi_hal_bt.c @@ -363,6 +363,30 @@ void furi_hal_bt_start_rx(uint8_t channel) { aci_hal_rx_start(channel); } +float furi_hal_bt_get_rssi(void) { + float val; + uint8_t rssi_raw[3]; + + if(aci_hal_read_raw_rssi(rssi_raw) != BLE_STATUS_SUCCESS) { + return 0.0f; + } + + // Some ST magic with rssi + uint8_t agc = rssi_raw[2] & 0xFF; + int rssi = (((int)rssi_raw[1] << 8) & 0xFF00) + (rssi_raw[0] & 0xFF); + if(rssi == 0 || agc > 11) { + val = -127.0; + } else { + val = agc * 6.0f - 127.0f; + while(rssi > 30) { + val += 6.0; + rssi >>= 1; + } + val += (float)((417 * rssi + 18080) >> 10); + } + return val; +} + uint32_t furi_hal_bt_get_transmitted_packets(void) { uint32_t packets = 0; aci_hal_le_tx_test_packet_number(&packets); From 3745ae22418b1a6b3860ff1c2c75454479a336a8 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Sun, 6 Apr 2025 04:56:08 +0300 Subject: [PATCH 197/268] merge ofw pr 4136 [ci skip] BadUSB: Full USB/BLE parameter customization, UI improvements, and more by Willy-JL --- applications/main/bad_usb/application.fam | 2 +- applications/main/bad_usb/bad_usb_app.c | 160 +++++-- applications/main/bad_usb/bad_usb_app_i.h | 13 + .../main/bad_usb/helpers/bad_usb_hid.c | 56 ++- .../main/bad_usb/helpers/bad_usb_hid.h | 11 +- .../main/bad_usb/helpers/ble_hid_profile.c | 429 ++++++++++++++++++ .../main/bad_usb/helpers/ble_hid_profile.h | 109 +++++ .../main/bad_usb/helpers/ble_hid_service.c | 325 +++++++++++++ .../main/bad_usb/helpers/ble_hid_service.h | 31 ++ .../main/bad_usb/helpers/ducky_script.c | 174 ++++--- .../main/bad_usb/helpers/ducky_script.h | 7 +- .../bad_usb/helpers/ducky_script_commands.c | 2 + .../main/bad_usb/helpers/ducky_script_i.h | 6 +- .../bad_usb/helpers/ducky_script_keycodes.c | 28 +- .../badusb/assets/layouts/de-DE-mac.kl | Bin 0 -> 256 bytes .../bad_usb/scenes/bad_usb_scene_config.c | 219 ++++++++- .../bad_usb/scenes/bad_usb_scene_config.h | 6 +- .../scenes/bad_usb_scene_config_ble_mac.c | 73 +++ .../scenes/bad_usb_scene_config_ble_name.c | 62 +++ .../scenes/bad_usb_scene_config_layout.c | 10 +- .../scenes/bad_usb_scene_config_usb_name.c | 86 ++++ .../scenes/bad_usb_scene_config_usb_vidpid.c | 59 +++ .../scenes/bad_usb_scene_confirm_unpair.c | 3 +- ...ene_unpair_done.c => bad_usb_scene_done.c} | 12 +- .../scenes/bad_usb_scene_file_select.c | 4 +- .../main/bad_usb/scenes/bad_usb_scene_work.c | 25 +- .../main/bad_usb/views/bad_usb_view.c | 78 ++-- lib/ble_profile/extra_profiles/hid_profile.c | 33 +- targets/f7/furi_hal/furi_hal_usb_hid.c | 3 - targets/furi_hal_include/furi_hal_usb_hid.h | 10 +- targets/furi_hal_include/furi_hal_version.h | 11 +- 31 files changed, 1841 insertions(+), 206 deletions(-) create mode 100644 applications/main/bad_usb/helpers/ble_hid_profile.c create mode 100644 applications/main/bad_usb/helpers/ble_hid_profile.h create mode 100644 applications/main/bad_usb/helpers/ble_hid_service.c create mode 100644 applications/main/bad_usb/helpers/ble_hid_service.h create mode 100755 applications/main/bad_usb/resources/badusb/assets/layouts/de-DE-mac.kl create mode 100644 applications/main/bad_usb/scenes/bad_usb_scene_config_ble_mac.c create mode 100644 applications/main/bad_usb/scenes/bad_usb_scene_config_ble_name.c create mode 100644 applications/main/bad_usb/scenes/bad_usb_scene_config_usb_name.c create mode 100644 applications/main/bad_usb/scenes/bad_usb_scene_config_usb_vidpid.c rename applications/main/bad_usb/scenes/{bad_usb_scene_unpair_done.c => bad_usb_scene_done.c} (67%) diff --git a/applications/main/bad_usb/application.fam b/applications/main/bad_usb/application.fam index 8d3909fcc..9844e248d 100644 --- a/applications/main/bad_usb/application.fam +++ b/applications/main/bad_usb/application.fam @@ -7,7 +7,7 @@ App( icon="A_BadUsb_14", order=70, resources="resources", - fap_libs=["assets", "ble_profile"], + fap_libs=["assets"], fap_icon="icon.png", fap_category="USB", ) diff --git a/applications/main/bad_usb/bad_usb_app.c b/applications/main/bad_usb/bad_usb_app.c index eda702cf4..96ccb14ed 100644 --- a/applications/main/bad_usb/bad_usb_app.c +++ b/applications/main/bad_usb/bad_usb_app.c @@ -31,52 +31,123 @@ static void bad_usb_app_tick_event_callback(void* context) { static void bad_usb_load_settings(BadUsbApp* app) { Storage* storage = furi_record_open(RECORD_STORAGE); FlipperFormat* fff = flipper_format_file_alloc(storage); - bool state = false; + bool loaded = false; + BadUsbHidConfig* hid_cfg = &app->user_hid_cfg; FuriString* temp_str = furi_string_alloc(); - uint32_t version = 0; - uint32_t interface = 0; + uint32_t temp_uint = 0; if(flipper_format_file_open_existing(fff, BAD_USB_SETTINGS_PATH)) { do { - if(!flipper_format_read_header(fff, temp_str, &version)) break; + if(!flipper_format_read_header(fff, temp_str, &temp_uint)) break; if((strcmp(furi_string_get_cstr(temp_str), BAD_USB_SETTINGS_FILE_TYPE) != 0) || - (version != BAD_USB_SETTINGS_VERSION)) + (temp_uint != BAD_USB_SETTINGS_VERSION)) break; - if(!flipper_format_read_string(fff, "layout", temp_str)) break; - if(!flipper_format_read_uint32(fff, "interface", &interface, 1)) break; - if(interface > BadUsbHidInterfaceBle) break; + if(flipper_format_read_string(fff, "layout", temp_str)) { + furi_string_set(app->keyboard_layout, temp_str); + FileInfo layout_file_info; + FS_Error file_check_err = storage_common_stat( + storage, furi_string_get_cstr(app->keyboard_layout), &layout_file_info); + if((file_check_err != FSE_OK) || (layout_file_info.size != 256)) { + furi_string_set(app->keyboard_layout, BAD_USB_SETTINGS_DEFAULT_LAYOUT); + } + } else { + furi_string_set(app->keyboard_layout, BAD_USB_SETTINGS_DEFAULT_LAYOUT); + flipper_format_rewind(fff); + } - state = true; + if(!flipper_format_read_uint32(fff, "interface", &temp_uint, 1) || + temp_uint >= BadUsbHidInterfaceMAX) { + temp_uint = BadUsbHidInterfaceUsb; + flipper_format_rewind(fff); + } + app->interface = temp_uint; + + if(!flipper_format_read_bool(fff, "ble_bonding", &hid_cfg->ble.bonding, 1)) { + hid_cfg->ble.bonding = true; + flipper_format_rewind(fff); + } + + if(!flipper_format_read_uint32(fff, "ble_pairing", &temp_uint, 1) || + temp_uint >= GapPairingCount) { + temp_uint = GapPairingPinCodeVerifyYesNo; + flipper_format_rewind(fff); + } + hid_cfg->ble.pairing = temp_uint; + + if(flipper_format_read_string(fff, "ble_name", temp_str)) { + strlcpy( + hid_cfg->ble.name, furi_string_get_cstr(temp_str), sizeof(hid_cfg->ble.name)); + } else { + hid_cfg->ble.name[0] = '\0'; + flipper_format_rewind(fff); + } + + if(!flipper_format_read_hex( + fff, "ble_mac", hid_cfg->ble.mac, sizeof(hid_cfg->ble.mac))) { + memset(hid_cfg->ble.mac, 0, sizeof(hid_cfg->ble.mac)); + flipper_format_rewind(fff); + } + + if(flipper_format_read_string(fff, "usb_manuf", temp_str)) { + strlcpy( + hid_cfg->usb.manuf, + furi_string_get_cstr(temp_str), + sizeof(hid_cfg->usb.manuf)); + } else { + hid_cfg->usb.manuf[0] = '\0'; + flipper_format_rewind(fff); + } + + if(flipper_format_read_string(fff, "usb_product", temp_str)) { + strlcpy( + hid_cfg->usb.product, + furi_string_get_cstr(temp_str), + sizeof(hid_cfg->usb.product)); + } else { + hid_cfg->usb.product[0] = '\0'; + flipper_format_rewind(fff); + } + + if(!flipper_format_read_uint32(fff, "usb_vid", &hid_cfg->usb.vid, 1)) { + hid_cfg->usb.vid = 0; + flipper_format_rewind(fff); + } + + if(!flipper_format_read_uint32(fff, "usb_pid", &hid_cfg->usb.pid, 1)) { + hid_cfg->usb.pid = 0; + flipper_format_rewind(fff); + } + + loaded = true; } while(0); } - flipper_format_free(fff); - furi_record_close(RECORD_STORAGE); - - if(state) { - furi_string_set(app->keyboard_layout, temp_str); - app->interface = interface; - - Storage* fs_api = furi_record_open(RECORD_STORAGE); - FileInfo layout_file_info; - FS_Error file_check_err = storage_common_stat( - fs_api, furi_string_get_cstr(app->keyboard_layout), &layout_file_info); - furi_record_close(RECORD_STORAGE); - if((file_check_err != FSE_OK) || (layout_file_info.size != 256)) { - furi_string_set(app->keyboard_layout, BAD_USB_SETTINGS_DEFAULT_LAYOUT); - } - } else { - furi_string_set(app->keyboard_layout, BAD_USB_SETTINGS_DEFAULT_LAYOUT); - app->interface = BadUsbHidInterfaceUsb; - } furi_string_free(temp_str); + + flipper_format_free(fff); + furi_record_close(RECORD_STORAGE); + + if(!loaded) { + furi_string_set(app->keyboard_layout, BAD_USB_SETTINGS_DEFAULT_LAYOUT); + app->interface = BadUsbHidInterfaceUsb; + hid_cfg->ble.name[0] = '\0'; + memset(hid_cfg->ble.mac, 0, sizeof(hid_cfg->ble.mac)); + hid_cfg->ble.bonding = true; + hid_cfg->ble.pairing = GapPairingPinCodeVerifyYesNo; + hid_cfg->usb.vid = 0; + hid_cfg->usb.pid = 0; + hid_cfg->usb.manuf[0] = '\0'; + hid_cfg->usb.product[0] = '\0'; + } } static void bad_usb_save_settings(BadUsbApp* app) { Storage* storage = furi_record_open(RECORD_STORAGE); FlipperFormat* fff = flipper_format_file_alloc(storage); + BadUsbHidConfig* hid_cfg = &app->user_hid_cfg; + uint32_t temp_uint = 0; if(flipper_format_file_open_always(fff, BAD_USB_SETTINGS_PATH)) { do { @@ -84,9 +155,19 @@ static void bad_usb_save_settings(BadUsbApp* app) { fff, BAD_USB_SETTINGS_FILE_TYPE, BAD_USB_SETTINGS_VERSION)) break; if(!flipper_format_write_string(fff, "layout", app->keyboard_layout)) break; - uint32_t interface_id = app->interface; - if(!flipper_format_write_uint32(fff, "interface", (const uint32_t*)&interface_id, 1)) + temp_uint = app->interface; + if(!flipper_format_write_uint32(fff, "interface", &temp_uint, 1)) break; + if(!flipper_format_write_bool(fff, "ble_bonding", &hid_cfg->ble.bonding, 1)) break; + temp_uint = hid_cfg->ble.pairing; + if(!flipper_format_write_uint32(fff, "ble_pairing", &temp_uint, 1)) break; + if(!flipper_format_write_string_cstr(fff, "ble_name", hid_cfg->ble.name)) break; + if(!flipper_format_write_hex( + fff, "ble_mac", (uint8_t*)&hid_cfg->ble.mac, sizeof(hid_cfg->ble.mac))) break; + if(!flipper_format_write_string_cstr(fff, "usb_manuf", hid_cfg->usb.manuf)) break; + if(!flipper_format_write_string_cstr(fff, "usb_product", hid_cfg->usb.product)) break; + if(!flipper_format_write_uint32(fff, "usb_vid", &hid_cfg->usb.vid, 1)) break; + if(!flipper_format_write_uint32(fff, "usb_pid", &hid_cfg->usb.pid, 1)) break; } while(0); } @@ -121,7 +202,7 @@ BadUsbApp* bad_usb_app_alloc(char* arg) { view_dispatcher_set_event_callback_context(app->view_dispatcher, app); view_dispatcher_set_tick_event_callback( - app->view_dispatcher, bad_usb_app_tick_event_callback, 500); + app->view_dispatcher, bad_usb_app_tick_event_callback, 250); view_dispatcher_set_custom_event_callback( app->view_dispatcher, bad_usb_app_custom_event_callback); view_dispatcher_set_navigation_event_callback( @@ -146,6 +227,14 @@ BadUsbApp* bad_usb_app_alloc(char* arg) { view_dispatcher_add_view( app->view_dispatcher, BadUsbAppViewWork, bad_usb_view_get_view(app->bad_usb_view)); + app->text_input = text_input_alloc(); + view_dispatcher_add_view( + app->view_dispatcher, BadUsbAppViewTextInput, text_input_get_view(app->text_input)); + + app->byte_input = byte_input_alloc(); + view_dispatcher_add_view( + app->view_dispatcher, BadUsbAppViewByteInput, byte_input_get_view(app->byte_input)); + view_dispatcher_attach_to_gui(app->view_dispatcher, app->gui, ViewDispatcherTypeFullscreen); if(furi_hal_usb_is_locked()) { @@ -157,6 +246,7 @@ BadUsbApp* bad_usb_app_alloc(char* arg) { furi_check(furi_hal_usb_set_config(NULL, NULL)); if(!furi_string_empty(app->file_path)) { + scene_manager_set_scene_state(app->scene_manager, BadUsbSceneWork, true); scene_manager_next_scene(app->scene_manager, BadUsbSceneWork); } else { furi_string_set(app->file_path, BAD_USB_APP_BASE_FOLDER); @@ -191,6 +281,14 @@ void bad_usb_app_free(BadUsbApp* app) { view_dispatcher_remove_view(app->view_dispatcher, BadUsbAppViewConfig); variable_item_list_free(app->var_item_list); + // Text Input + view_dispatcher_remove_view(app->view_dispatcher, BadUsbAppViewTextInput); + text_input_free(app->text_input); + + // Byte Input + view_dispatcher_remove_view(app->view_dispatcher, BadUsbAppViewByteInput); + byte_input_free(app->byte_input); + // View dispatcher view_dispatcher_free(app->view_dispatcher); scene_manager_free(app->scene_manager); diff --git a/applications/main/bad_usb/bad_usb_app_i.h b/applications/main/bad_usb/bad_usb_app_i.h index b34bd5de6..06a798706 100644 --- a/applications/main/bad_usb/bad_usb_app_i.h +++ b/applications/main/bad_usb/bad_usb_app_i.h @@ -12,6 +12,8 @@ #include #include #include +#include +#include #include #include #include "views/bad_usb_view.h" @@ -36,6 +38,13 @@ struct BadUsbApp { Widget* widget; Popup* popup; VariableItemList* var_item_list; + TextInput* text_input; + ByteInput* byte_input; + + char ble_name_buf[FURI_HAL_BT_ADV_NAME_LENGTH]; + uint8_t ble_mac_buf[GAP_MAC_ADDR_SIZE]; + char usb_name_buf[HID_MANUF_PRODUCT_NAME_LEN]; + uint16_t usb_vidpid_buf[2]; BadUsbAppError error; FuriString* file_path; @@ -44,6 +53,8 @@ struct BadUsbApp { BadUsbScript* bad_usb_script; BadUsbHidInterface interface; + BadUsbHidConfig user_hid_cfg; + BadUsbHidConfig script_hid_cfg; FuriHalUsbInterface* usb_if_prev; }; @@ -52,6 +63,8 @@ typedef enum { BadUsbAppViewPopup, BadUsbAppViewWork, BadUsbAppViewConfig, + BadUsbAppViewByteInput, + BadUsbAppViewTextInput, } BadUsbAppView; void bad_usb_set_interface(BadUsbApp* app, BadUsbHidInterface interface); diff --git a/applications/main/bad_usb/helpers/bad_usb_hid.c b/applications/main/bad_usb/helpers/bad_usb_hid.c index c6226cf37..5ae4146e8 100644 --- a/applications/main/bad_usb/helpers/bad_usb_hid.c +++ b/applications/main/bad_usb/helpers/bad_usb_hid.c @@ -1,5 +1,5 @@ #include "bad_usb_hid.h" -#include +#include "ble_hid_profile.h" #include #include @@ -7,8 +7,14 @@ #define HID_BT_KEYS_STORAGE_NAME ".bt_hid.keys" -void* hid_usb_init(FuriHalUsbHidConfig* hid_cfg) { - furi_check(furi_hal_usb_set_config(&usb_hid, hid_cfg)); +void hid_usb_adjust_config(BadUsbHidConfig* hid_cfg) { + if(hid_cfg->usb.vid == 0) hid_cfg->usb.vid = HID_VID_DEFAULT; + if(hid_cfg->usb.pid == 0) hid_cfg->usb.pid = HID_PID_DEFAULT; +} + +void* hid_usb_init(BadUsbHidConfig* hid_cfg) { + hid_usb_adjust_config(hid_cfg); + furi_check(furi_hal_usb_set_config(&usb_hid, &hid_cfg->usb)); return NULL; } @@ -86,6 +92,7 @@ uint8_t hid_usb_get_led_state(void* inst) { } static const BadUsbHidApi hid_api_usb = { + .adjust_config = hid_usb_adjust_config, .init = hid_usb_init, .deinit = hid_usb_deinit, .set_state_callback = hid_usb_set_state_callback, @@ -111,11 +118,6 @@ typedef struct { bool is_connected; } BleHidInstance; -static const BleProfileHidParams ble_hid_params = { - .device_name_prefix = "BadUSB", - .mac_xor = 0x0002, -}; - static void hid_ble_connection_status_callback(BtStatus status, void* context) { furi_assert(context); BleHidInstance* ble_hid = context; @@ -125,8 +127,38 @@ static void hid_ble_connection_status_callback(BtStatus status, void* context) { } } -void* hid_ble_init(FuriHalUsbHidConfig* hid_cfg) { - UNUSED(hid_cfg); +void hid_ble_adjust_config(BadUsbHidConfig* hid_cfg) { + const uint8_t* normal_mac = furi_hal_version_get_ble_mac(); + uint8_t empty_mac[GAP_MAC_ADDR_SIZE] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + uint8_t default_mac[GAP_MAC_ADDR_SIZE] = {0x6c, 0x7a, 0xd8, 0xac, 0x57, 0x72}; // furi_hal_bt + if(memcmp(hid_cfg->ble.mac, empty_mac, sizeof(hid_cfg->ble.mac)) == 0 || + memcmp(hid_cfg->ble.mac, normal_mac, sizeof(hid_cfg->ble.mac)) == 0 || + memcmp(hid_cfg->ble.mac, default_mac, sizeof(hid_cfg->ble.mac)) == 0) { + // Derive badusb MAC from Flipper MAC + memcpy(hid_cfg->ble.mac, normal_mac, sizeof(hid_cfg->ble.mac)); + hid_cfg->ble.mac[2]++; + uint16_t badusb_mac_xor = 0x0002; + hid_cfg->ble.mac[0] ^= badusb_mac_xor; + hid_cfg->ble.mac[1] ^= badusb_mac_xor >> 8; + } + + if(hid_cfg->ble.name[0] == '\0') { + // Derive badusb name from Flipper name + const char* badusb_device_name_prefix = "BadUSB"; + snprintf( + hid_cfg->ble.name, + sizeof(hid_cfg->ble.name), + "%s %s", + badusb_device_name_prefix, + furi_hal_version_get_name_ptr()); + } + + if(hid_cfg->ble.pairing >= GapPairingCount) { + hid_cfg->ble.pairing = GapPairingPinCodeVerifyYesNo; + } +} + +void* hid_ble_init(BadUsbHidConfig* hid_cfg) { BleHidInstance* ble_hid = malloc(sizeof(BleHidInstance)); ble_hid->bt = furi_record_open(RECORD_BT); bt_disconnect(ble_hid->bt); @@ -136,7 +168,8 @@ void* hid_ble_init(FuriHalUsbHidConfig* hid_cfg) { bt_keys_storage_set_storage_path(ble_hid->bt, APP_DATA_PATH(HID_BT_KEYS_STORAGE_NAME)); - ble_hid->profile = bt_profile_start(ble_hid->bt, ble_profile_hid, (void*)&ble_hid_params); + hid_ble_adjust_config(hid_cfg); + ble_hid->profile = bt_profile_start(ble_hid->bt, ble_profile_hid, &hid_cfg->ble); furi_check(ble_hid->profile); furi_hal_bt_start_advertising(); @@ -236,6 +269,7 @@ uint8_t hid_ble_get_led_state(void* inst) { } static const BadUsbHidApi hid_api_ble = { + .adjust_config = hid_ble_adjust_config, .init = hid_ble_init, .deinit = hid_ble_deinit, .set_state_callback = hid_ble_set_state_callback, diff --git a/applications/main/bad_usb/helpers/bad_usb_hid.h b/applications/main/bad_usb/helpers/bad_usb_hid.h index e4758ab68..8749bdc3b 100644 --- a/applications/main/bad_usb/helpers/bad_usb_hid.h +++ b/applications/main/bad_usb/helpers/bad_usb_hid.h @@ -7,13 +7,22 @@ extern "C" { #include #include +#include "ble_hid_profile.h" + typedef enum { BadUsbHidInterfaceUsb, BadUsbHidInterfaceBle, + BadUsbHidInterfaceMAX, } BadUsbHidInterface; typedef struct { - void* (*init)(FuriHalUsbHidConfig* hid_cfg); + BleProfileHidParams ble; + FuriHalUsbHidConfig usb; +} BadUsbHidConfig; + +typedef struct { + void (*adjust_config)(BadUsbHidConfig* hid_cfg); + void* (*init)(BadUsbHidConfig* hid_cfg); void (*deinit)(void* inst); void (*set_state_callback)(void* inst, HidStateCallback cb, void* context); bool (*is_connected)(void* inst); diff --git a/applications/main/bad_usb/helpers/ble_hid_profile.c b/applications/main/bad_usb/helpers/ble_hid_profile.c new file mode 100644 index 000000000..a4f32159e --- /dev/null +++ b/applications/main/bad_usb/helpers/ble_hid_profile.c @@ -0,0 +1,429 @@ +#include "ble_hid_profile.h" + +// Based on + +#include +#include +#include +#include "ble_hid_service.h" + +#include +#include +#include + +#define HID_INFO_BASE_USB_SPECIFICATION (0x0101) +#define HID_INFO_COUNTRY_CODE (0x00) +#define BLE_PROFILE_HID_INFO_FLAG_REMOTE_WAKE_MSK (0x01) +#define BLE_PROFILE_HID_INFO_FLAG_NORMALLY_CONNECTABLE_MSK (0x02) + +#define BLE_PROFILE_HID_KB_MAX_KEYS (6) +#define BLE_PROFILE_CONSUMER_MAX_KEYS (1) + +// Report ids cant be 0 +enum HidReportId { + ReportIdKeyboard = 1, + ReportIdMouse = 2, + ReportIdConsumer = 3, +}; +// Report numbers corresponded to the report id with an offset of 1 +enum HidInputNumber { + ReportNumberKeyboard = 0, + ReportNumberMouse = 1, + ReportNumberConsumer = 2, +}; + +typedef struct { + uint8_t mods; + uint8_t reserved; + uint8_t key[BLE_PROFILE_HID_KB_MAX_KEYS]; +} FURI_PACKED FuriHalBtHidKbReport; + +typedef struct { + uint8_t btn; + int8_t x; + int8_t y; + int8_t wheel; +} FURI_PACKED FuriHalBtHidMouseReport; + +typedef struct { + uint16_t key[BLE_PROFILE_CONSUMER_MAX_KEYS]; +} FURI_PACKED FuriHalBtHidConsumerReport; + +// keyboard+mouse+consumer hid report +static const uint8_t ble_profile_hid_report_map_data[] = { + // Keyboard Report + HID_USAGE_PAGE(HID_PAGE_DESKTOP), + HID_USAGE(HID_DESKTOP_KEYBOARD), + HID_COLLECTION(HID_APPLICATION_COLLECTION), + HID_REPORT_ID(ReportIdKeyboard), + HID_USAGE_PAGE(HID_DESKTOP_KEYPAD), + HID_USAGE_MINIMUM(HID_KEYBOARD_L_CTRL), + HID_USAGE_MAXIMUM(HID_KEYBOARD_R_GUI), + HID_LOGICAL_MINIMUM(0), + HID_LOGICAL_MAXIMUM(1), + HID_REPORT_SIZE(1), + HID_REPORT_COUNT(8), + HID_INPUT(HID_IOF_DATA | HID_IOF_VARIABLE | HID_IOF_ABSOLUTE), + HID_REPORT_COUNT(1), + HID_REPORT_SIZE(8), + HID_INPUT(HID_IOF_CONSTANT | HID_IOF_VARIABLE | HID_IOF_ABSOLUTE), + HID_USAGE_PAGE(HID_PAGE_LED), + HID_REPORT_COUNT(8), + HID_REPORT_SIZE(1), + HID_USAGE_MINIMUM(1), + HID_USAGE_MAXIMUM(8), + HID_OUTPUT(HID_IOF_DATA | HID_IOF_VARIABLE | HID_IOF_ABSOLUTE), + HID_REPORT_COUNT(BLE_PROFILE_HID_KB_MAX_KEYS), + HID_REPORT_SIZE(8), + HID_LOGICAL_MINIMUM(0), + HID_LOGICAL_MAXIMUM(101), + HID_USAGE_PAGE(HID_DESKTOP_KEYPAD), + HID_USAGE_MINIMUM(0), + HID_USAGE_MAXIMUM(101), + HID_INPUT(HID_IOF_DATA | HID_IOF_ARRAY | HID_IOF_ABSOLUTE), + HID_END_COLLECTION, + // Mouse Report + HID_USAGE_PAGE(HID_PAGE_DESKTOP), + HID_USAGE(HID_DESKTOP_MOUSE), + HID_COLLECTION(HID_APPLICATION_COLLECTION), + HID_USAGE(HID_DESKTOP_POINTER), + HID_COLLECTION(HID_PHYSICAL_COLLECTION), + HID_REPORT_ID(ReportIdMouse), + HID_USAGE_PAGE(HID_PAGE_BUTTON), + HID_USAGE_MINIMUM(1), + HID_USAGE_MAXIMUM(3), + HID_LOGICAL_MINIMUM(0), + HID_LOGICAL_MAXIMUM(1), + HID_REPORT_COUNT(3), + HID_REPORT_SIZE(1), + HID_INPUT(HID_IOF_DATA | HID_IOF_VARIABLE | HID_IOF_ABSOLUTE), + HID_REPORT_SIZE(1), + HID_REPORT_COUNT(5), + HID_INPUT(HID_IOF_CONSTANT | HID_IOF_VARIABLE | HID_IOF_ABSOLUTE), + HID_USAGE_PAGE(HID_PAGE_DESKTOP), + HID_USAGE(HID_DESKTOP_X), + HID_USAGE(HID_DESKTOP_Y), + HID_USAGE(HID_DESKTOP_WHEEL), + HID_LOGICAL_MINIMUM(-127), + HID_LOGICAL_MAXIMUM(127), + HID_REPORT_SIZE(8), + HID_REPORT_COUNT(3), + HID_INPUT(HID_IOF_DATA | HID_IOF_VARIABLE | HID_IOF_RELATIVE), + HID_END_COLLECTION, + HID_END_COLLECTION, + // Consumer Report + HID_USAGE_PAGE(HID_PAGE_CONSUMER), + HID_USAGE(HID_CONSUMER_CONTROL), + HID_COLLECTION(HID_APPLICATION_COLLECTION), + HID_REPORT_ID(ReportIdConsumer), + HID_LOGICAL_MINIMUM(0), + HID_RI_LOGICAL_MAXIMUM(16, 0x3FF), + HID_USAGE_MINIMUM(0), + HID_RI_USAGE_MAXIMUM(16, 0x3FF), + HID_REPORT_COUNT(BLE_PROFILE_CONSUMER_MAX_KEYS), + HID_REPORT_SIZE(16), + HID_INPUT(HID_IOF_DATA | HID_IOF_ARRAY | HID_IOF_ABSOLUTE), + HID_END_COLLECTION, +}; + +typedef struct { + FuriHalBleProfileBase base; + + FuriHalBtHidKbReport* kb_report; + FuriHalBtHidMouseReport* mouse_report; + FuriHalBtHidConsumerReport* consumer_report; + + BleServiceBattery* battery_svc; + BleServiceDevInfo* dev_info_svc; + BleServiceHid* hid_svc; +} BleProfileHid; +_Static_assert(offsetof(BleProfileHid, base) == 0, "Wrong layout"); + +static FuriHalBleProfileBase* ble_profile_hid_start(FuriHalBleProfileParams profile_params) { + UNUSED(profile_params); + + BleProfileHid* profile = malloc(sizeof(BleProfileHid)); + + profile->base.config = ble_profile_hid; + + profile->battery_svc = ble_svc_battery_start(true); + profile->dev_info_svc = ble_svc_dev_info_start(); + profile->hid_svc = ble_svc_hid_start(); + + // Configure HID Keyboard + profile->kb_report = malloc(sizeof(FuriHalBtHidKbReport)); + profile->mouse_report = malloc(sizeof(FuriHalBtHidMouseReport)); + profile->consumer_report = malloc(sizeof(FuriHalBtHidConsumerReport)); + + // Configure Report Map characteristic + ble_svc_hid_update_report_map( + profile->hid_svc, + ble_profile_hid_report_map_data, + sizeof(ble_profile_hid_report_map_data)); + // Configure HID Information characteristic + uint8_t hid_info_val[4] = { + HID_INFO_BASE_USB_SPECIFICATION & 0x00ff, + (HID_INFO_BASE_USB_SPECIFICATION & 0xff00) >> 8, + HID_INFO_COUNTRY_CODE, + BLE_PROFILE_HID_INFO_FLAG_REMOTE_WAKE_MSK | + BLE_PROFILE_HID_INFO_FLAG_NORMALLY_CONNECTABLE_MSK, + }; + ble_svc_hid_update_info(profile->hid_svc, hid_info_val); + + return &profile->base; +} + +static void ble_profile_hid_stop(FuriHalBleProfileBase* profile) { + furi_check(profile); + furi_check(profile->config == ble_profile_hid); + + BleProfileHid* hid_profile = (BleProfileHid*)profile; + ble_svc_battery_stop(hid_profile->battery_svc); + ble_svc_dev_info_stop(hid_profile->dev_info_svc); + ble_svc_hid_stop(hid_profile->hid_svc); + + free(hid_profile->kb_report); + free(hid_profile->mouse_report); + free(hid_profile->consumer_report); +} + +bool ble_profile_hid_kb_press(FuriHalBleProfileBase* profile, uint16_t button) { + furi_check(profile); + furi_check(profile->config == ble_profile_hid); + + BleProfileHid* hid_profile = (BleProfileHid*)profile; + FuriHalBtHidKbReport* kb_report = hid_profile->kb_report; + for(uint8_t i = 0; i < BLE_PROFILE_HID_KB_MAX_KEYS; i++) { + if(kb_report->key[i] == 0) { + kb_report->key[i] = button & 0xFF; + break; + } + } + kb_report->mods |= (button >> 8); + return ble_svc_hid_update_input_report( + hid_profile->hid_svc, + ReportNumberKeyboard, + (uint8_t*)kb_report, + sizeof(FuriHalBtHidKbReport)); +} + +bool ble_profile_hid_kb_release(FuriHalBleProfileBase* profile, uint16_t button) { + furi_check(profile); + furi_check(profile->config == ble_profile_hid); + + BleProfileHid* hid_profile = (BleProfileHid*)profile; + + FuriHalBtHidKbReport* kb_report = hid_profile->kb_report; + for(uint8_t i = 0; i < BLE_PROFILE_HID_KB_MAX_KEYS; i++) { + if(kb_report->key[i] == (button & 0xFF)) { + kb_report->key[i] = 0; + break; + } + } + kb_report->mods &= ~(button >> 8); + return ble_svc_hid_update_input_report( + hid_profile->hid_svc, + ReportNumberKeyboard, + (uint8_t*)kb_report, + sizeof(FuriHalBtHidKbReport)); +} + +bool ble_profile_hid_kb_release_all(FuriHalBleProfileBase* profile) { + furi_check(profile); + furi_check(profile->config == ble_profile_hid); + + BleProfileHid* hid_profile = (BleProfileHid*)profile; + FuriHalBtHidKbReport* kb_report = hid_profile->kb_report; + for(uint8_t i = 0; i < BLE_PROFILE_HID_KB_MAX_KEYS; i++) { + kb_report->key[i] = 0; + } + kb_report->mods = 0; + return ble_svc_hid_update_input_report( + hid_profile->hid_svc, + ReportNumberKeyboard, + (uint8_t*)kb_report, + sizeof(FuriHalBtHidKbReport)); +} + +bool ble_profile_hid_consumer_key_press(FuriHalBleProfileBase* profile, uint16_t button) { + furi_check(profile); + furi_check(profile->config == ble_profile_hid); + + BleProfileHid* hid_profile = (BleProfileHid*)profile; + FuriHalBtHidConsumerReport* consumer_report = hid_profile->consumer_report; + for(uint8_t i = 0; i < BLE_PROFILE_CONSUMER_MAX_KEYS; i++) { //-V1008 + if(consumer_report->key[i] == 0) { + consumer_report->key[i] = button; + break; + } + } + return ble_svc_hid_update_input_report( + hid_profile->hid_svc, + ReportNumberConsumer, + (uint8_t*)consumer_report, + sizeof(FuriHalBtHidConsumerReport)); +} + +bool ble_profile_hid_consumer_key_release(FuriHalBleProfileBase* profile, uint16_t button) { + furi_check(profile); + furi_check(profile->config == ble_profile_hid); + + BleProfileHid* hid_profile = (BleProfileHid*)profile; + FuriHalBtHidConsumerReport* consumer_report = hid_profile->consumer_report; + for(uint8_t i = 0; i < BLE_PROFILE_CONSUMER_MAX_KEYS; i++) { //-V1008 + if(consumer_report->key[i] == button) { + consumer_report->key[i] = 0; + break; + } + } + return ble_svc_hid_update_input_report( + hid_profile->hid_svc, + ReportNumberConsumer, + (uint8_t*)consumer_report, + sizeof(FuriHalBtHidConsumerReport)); +} + +bool ble_profile_hid_consumer_key_release_all(FuriHalBleProfileBase* profile) { + furi_check(profile); + furi_check(profile->config == ble_profile_hid); + + BleProfileHid* hid_profile = (BleProfileHid*)profile; + FuriHalBtHidConsumerReport* consumer_report = hid_profile->consumer_report; + for(uint8_t i = 0; i < BLE_PROFILE_CONSUMER_MAX_KEYS; i++) { //-V1008 + consumer_report->key[i] = 0; + } + return ble_svc_hid_update_input_report( + hid_profile->hid_svc, + ReportNumberConsumer, + (uint8_t*)consumer_report, + sizeof(FuriHalBtHidConsumerReport)); +} + +bool ble_profile_hid_mouse_move(FuriHalBleProfileBase* profile, int8_t dx, int8_t dy) { + furi_check(profile); + furi_check(profile->config == ble_profile_hid); + + BleProfileHid* hid_profile = (BleProfileHid*)profile; + FuriHalBtHidMouseReport* mouse_report = hid_profile->mouse_report; + mouse_report->x = dx; + mouse_report->y = dy; + bool state = ble_svc_hid_update_input_report( + hid_profile->hid_svc, + ReportNumberMouse, + (uint8_t*)mouse_report, + sizeof(FuriHalBtHidMouseReport)); + mouse_report->x = 0; + mouse_report->y = 0; + return state; +} + +bool ble_profile_hid_mouse_press(FuriHalBleProfileBase* profile, uint8_t button) { + furi_check(profile); + furi_check(profile->config == ble_profile_hid); + + BleProfileHid* hid_profile = (BleProfileHid*)profile; + FuriHalBtHidMouseReport* mouse_report = hid_profile->mouse_report; + mouse_report->btn |= button; + return ble_svc_hid_update_input_report( + hid_profile->hid_svc, + ReportNumberMouse, + (uint8_t*)mouse_report, + sizeof(FuriHalBtHidMouseReport)); +} + +bool ble_profile_hid_mouse_release(FuriHalBleProfileBase* profile, uint8_t button) { + furi_check(profile); + furi_check(profile->config == ble_profile_hid); + + BleProfileHid* hid_profile = (BleProfileHid*)profile; + FuriHalBtHidMouseReport* mouse_report = hid_profile->mouse_report; + mouse_report->btn &= ~button; + return ble_svc_hid_update_input_report( + hid_profile->hid_svc, + ReportNumberMouse, + (uint8_t*)mouse_report, + sizeof(FuriHalBtHidMouseReport)); +} + +bool ble_profile_hid_mouse_release_all(FuriHalBleProfileBase* profile) { + furi_check(profile); + furi_check(profile->config == ble_profile_hid); + + BleProfileHid* hid_profile = (BleProfileHid*)profile; + FuriHalBtHidMouseReport* mouse_report = hid_profile->mouse_report; + mouse_report->btn = 0; + return ble_svc_hid_update_input_report( + hid_profile->hid_svc, + ReportNumberMouse, + (uint8_t*)mouse_report, + sizeof(FuriHalBtHidMouseReport)); +} + +bool ble_profile_hid_mouse_scroll(FuriHalBleProfileBase* profile, int8_t delta) { + furi_check(profile); + furi_check(profile->config == ble_profile_hid); + + BleProfileHid* hid_profile = (BleProfileHid*)profile; + FuriHalBtHidMouseReport* mouse_report = hid_profile->mouse_report; + mouse_report->wheel = delta; + bool state = ble_svc_hid_update_input_report( + hid_profile->hid_svc, + ReportNumberMouse, + (uint8_t*)mouse_report, + sizeof(FuriHalBtHidMouseReport)); + mouse_report->wheel = 0; + return state; +} + +// AN5289: 4.7, in order to use flash controller interval must be at least 25ms + advertisement, which is 30 ms +// Since we don't use flash controller anymore interval can be lowered to 7.5ms +#define CONNECTION_INTERVAL_MIN (0x0006) +// Up to 45 ms +#define CONNECTION_INTERVAL_MAX (0x24) + +static GapConfig template_config = { + .adv_service = + { + .UUID_Type = UUID_TYPE_16, + .Service_UUID_16 = HUMAN_INTERFACE_DEVICE_SERVICE_UUID, + }, + .appearance_char = GAP_APPEARANCE_KEYBOARD, + .bonding_mode = true, + .pairing_method = GapPairingPinCodeVerifyYesNo, + .conn_param = + { + .conn_int_min = CONNECTION_INTERVAL_MIN, + .conn_int_max = CONNECTION_INTERVAL_MAX, + .slave_latency = 0, + .supervisor_timeout = 0, + }, +}; + +static void ble_profile_hid_get_config(GapConfig* config, FuriHalBleProfileParams profile_params) { + furi_check(profile_params); + BleProfileHidParams* hid_profile_params = profile_params; + + furi_check(config); + memcpy(config, &template_config, sizeof(GapConfig)); + + // Set MAC address + memcpy(config->mac_address, hid_profile_params->mac, sizeof(config->mac_address)); + + // Set advertise name + config->adv_name[0] = furi_hal_version_get_ble_local_device_name_ptr()[0]; + strlcpy(config->adv_name + 1, hid_profile_params->name, sizeof(config->adv_name) - 1); + + // Set bonding mode + config->bonding_mode = hid_profile_params->bonding; + + // Set pairing method + config->pairing_method = hid_profile_params->pairing; +} + +static const FuriHalBleProfileTemplate profile_callbacks = { + .start = ble_profile_hid_start, + .stop = ble_profile_hid_stop, + .get_gap_config = ble_profile_hid_get_config, +}; + +const FuriHalBleProfileTemplate* ble_profile_hid = &profile_callbacks; diff --git a/applications/main/bad_usb/helpers/ble_hid_profile.h b/applications/main/bad_usb/helpers/ble_hid_profile.h new file mode 100644 index 000000000..2302aa581 --- /dev/null +++ b/applications/main/bad_usb/helpers/ble_hid_profile.h @@ -0,0 +1,109 @@ +#pragma once + +// Based on + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Optional arguments to pass along with profile template as + * FuriHalBleProfileParams for tuning profile behavior + **/ +typedef struct { + char name[FURI_HAL_BT_ADV_NAME_LENGTH]; /**< Full device name */ + uint8_t mac[GAP_MAC_ADDR_SIZE]; /**< Full device address */ + bool bonding; /**< Save paired devices */ + GapPairing pairing; /**< Pairing security method */ +} BleProfileHidParams; + +/** Hid Keyboard Profile descriptor */ +extern const FuriHalBleProfileTemplate* ble_profile_hid; + +/** Press keyboard button + * + * @param profile profile instance + * @param button button code from HID specification + * + * @return true on success + */ +bool ble_profile_hid_kb_press(FuriHalBleProfileBase* profile, uint16_t button); + +/** Release keyboard button + * + * @param profile profile instance + * @param button button code from HID specification + * + * @return true on success + */ +bool ble_profile_hid_kb_release(FuriHalBleProfileBase* profile, uint16_t button); + +/** Release all keyboard buttons + * + * @param profile profile instance + * @return true on success + */ +bool ble_profile_hid_kb_release_all(FuriHalBleProfileBase* profile); + +/** Set the following consumer key to pressed state and send HID report + * + * @param profile profile instance + * @param button key code + */ +bool ble_profile_hid_consumer_key_press(FuriHalBleProfileBase* profile, uint16_t button); + +/** Set the following consumer key to released state and send HID report + * + * @param profile profile instance + * @param button key code + */ +bool ble_profile_hid_consumer_key_release(FuriHalBleProfileBase* profile, uint16_t button); + +/** Set consumer key to released state and send HID report + * + * @param profile profile instance + * @param button key code + */ +bool ble_profile_hid_consumer_key_release_all(FuriHalBleProfileBase* profile); + +/** Set mouse movement and send HID report + * + * @param profile profile instance + * @param dx x coordinate delta + * @param dy y coordinate delta + */ +bool ble_profile_hid_mouse_move(FuriHalBleProfileBase* profile, int8_t dx, int8_t dy); + +/** Set mouse button to pressed state and send HID report + * + * @param profile profile instance + * @param button key code + */ +bool ble_profile_hid_mouse_press(FuriHalBleProfileBase* profile, uint8_t button); + +/** Set mouse button to released state and send HID report + * + * @param profile profile instance + * @param button key code + */ +bool ble_profile_hid_mouse_release(FuriHalBleProfileBase* profile, uint8_t button); + +/** Set mouse button to released state and send HID report + * + * @param profile profile instance + * @param button key code + */ +bool ble_profile_hid_mouse_release_all(FuriHalBleProfileBase* profile); + +/** Set mouse wheel position and send HID report + * + * @param profile profile instance + * @param delta number of scroll steps + */ +bool ble_profile_hid_mouse_scroll(FuriHalBleProfileBase* profile, int8_t delta); + +#ifdef __cplusplus +} +#endif diff --git a/applications/main/bad_usb/helpers/ble_hid_service.c b/applications/main/bad_usb/helpers/ble_hid_service.c new file mode 100644 index 000000000..b546368dd --- /dev/null +++ b/applications/main/bad_usb/helpers/ble_hid_service.c @@ -0,0 +1,325 @@ +#include "ble_hid_service.h" + +// Based on + +#include "app_common.h" // IWYU pragma: keep +#include +#include +#include + +#include +#include + +#define TAG "BleHid" + +#define BLE_SVC_HID_REPORT_MAP_MAX_LEN (255) +#define BLE_SVC_HID_REPORT_MAX_LEN (255) +#define BLE_SVC_HID_REPORT_REF_LEN (2) +#define BLE_SVC_HID_INFO_LEN (4) +#define BLE_SVC_HID_CONTROL_POINT_LEN (1) + +#define BLE_SVC_HID_INPUT_REPORT_COUNT (3) +#define BLE_SVC_HID_OUTPUT_REPORT_COUNT (0) +#define BLE_SVC_HID_FEATURE_REPORT_COUNT (0) +#define BLE_SVC_HID_REPORT_COUNT \ + (BLE_SVC_HID_INPUT_REPORT_COUNT + BLE_SVC_HID_OUTPUT_REPORT_COUNT + \ + BLE_SVC_HID_FEATURE_REPORT_COUNT) + +typedef enum { + HidSvcGattCharacteristicProtocolMode = 0, + HidSvcGattCharacteristicReportMap, + HidSvcGattCharacteristicInfo, + HidSvcGattCharacteristicCtrlPoint, + HidSvcGattCharacteristicCount, +} HidSvcGattCharacteristicId; + +typedef struct { + uint8_t report_idx; + uint8_t report_type; +} HidSvcReportId; + +static_assert(sizeof(HidSvcReportId) == sizeof(uint16_t), "HidSvcReportId must be 2 bytes"); + +static const Service_UUID_t ble_svc_hid_uuid = { + .Service_UUID_16 = HUMAN_INTERFACE_DEVICE_SERVICE_UUID, +}; + +static bool ble_svc_hid_char_desc_data_callback( + const void* context, + const uint8_t** data, + uint16_t* data_len) { + const HidSvcReportId* report_id = context; + *data_len = sizeof(HidSvcReportId); + if(data) { + *data = (const uint8_t*)report_id; + } + return false; +} + +typedef struct { + const void* data_ptr; + uint16_t data_len; +} HidSvcDataWrapper; + +static bool ble_svc_hid_report_data_callback( + const void* context, + const uint8_t** data, + uint16_t* data_len) { + const HidSvcDataWrapper* report_data = context; + if(data) { + *data = report_data->data_ptr; + *data_len = report_data->data_len; + } else { + *data_len = BLE_SVC_HID_REPORT_MAP_MAX_LEN; + } + return false; +} + +static const BleGattCharacteristicParams ble_svc_hid_chars[HidSvcGattCharacteristicCount] = { + [HidSvcGattCharacteristicProtocolMode] = + {.name = "Protocol Mode", + .data_prop_type = FlipperGattCharacteristicDataFixed, + .data.fixed.length = 1, + .uuid.Char_UUID_16 = PROTOCOL_MODE_CHAR_UUID, + .uuid_type = UUID_TYPE_16, + .char_properties = CHAR_PROP_READ | CHAR_PROP_WRITE_WITHOUT_RESP, + .security_permissions = ATTR_PERMISSION_NONE, + .gatt_evt_mask = GATT_NOTIFY_ATTRIBUTE_WRITE, + .is_variable = CHAR_VALUE_LEN_CONSTANT}, + [HidSvcGattCharacteristicReportMap] = + {.name = "Report Map", + .data_prop_type = FlipperGattCharacteristicDataCallback, + .data.callback.fn = ble_svc_hid_report_data_callback, + .data.callback.context = NULL, + .uuid.Char_UUID_16 = REPORT_MAP_CHAR_UUID, + .uuid_type = UUID_TYPE_16, + .char_properties = CHAR_PROP_READ, + .security_permissions = ATTR_PERMISSION_NONE, + .gatt_evt_mask = GATT_DONT_NOTIFY_EVENTS, + .is_variable = CHAR_VALUE_LEN_VARIABLE}, + [HidSvcGattCharacteristicInfo] = + {.name = "HID Information", + .data_prop_type = FlipperGattCharacteristicDataFixed, + .data.fixed.length = BLE_SVC_HID_INFO_LEN, + .data.fixed.ptr = NULL, + .uuid.Char_UUID_16 = HID_INFORMATION_CHAR_UUID, + .uuid_type = UUID_TYPE_16, + .char_properties = CHAR_PROP_READ, + .security_permissions = ATTR_PERMISSION_NONE, + .gatt_evt_mask = GATT_DONT_NOTIFY_EVENTS, + .is_variable = CHAR_VALUE_LEN_CONSTANT}, + [HidSvcGattCharacteristicCtrlPoint] = + {.name = "HID Control Point", + .data_prop_type = FlipperGattCharacteristicDataFixed, + .data.fixed.length = BLE_SVC_HID_CONTROL_POINT_LEN, + .uuid.Char_UUID_16 = HID_CONTROL_POINT_CHAR_UUID, + .uuid_type = UUID_TYPE_16, + .char_properties = CHAR_PROP_WRITE_WITHOUT_RESP, + .security_permissions = ATTR_PERMISSION_NONE, + .gatt_evt_mask = GATT_NOTIFY_ATTRIBUTE_WRITE, + .is_variable = CHAR_VALUE_LEN_CONSTANT}, +}; + +static const BleGattCharacteristicDescriptorParams ble_svc_hid_char_descr_template = { + .uuid_type = UUID_TYPE_16, + .uuid.Char_UUID_16 = REPORT_REFERENCE_DESCRIPTOR_UUID, + .max_length = BLE_SVC_HID_REPORT_REF_LEN, + .data_callback.fn = ble_svc_hid_char_desc_data_callback, + .security_permissions = ATTR_PERMISSION_NONE, + .access_permissions = ATTR_ACCESS_READ_WRITE, + .gatt_evt_mask = GATT_DONT_NOTIFY_EVENTS, + .is_variable = CHAR_VALUE_LEN_CONSTANT, +}; + +static const BleGattCharacteristicParams ble_svc_hid_report_template = { + .name = "Report", + .data_prop_type = FlipperGattCharacteristicDataCallback, + .data.callback.fn = ble_svc_hid_report_data_callback, + .data.callback.context = NULL, + .uuid.Char_UUID_16 = REPORT_CHAR_UUID, + .uuid_type = UUID_TYPE_16, + .char_properties = CHAR_PROP_READ | CHAR_PROP_NOTIFY, + .security_permissions = ATTR_PERMISSION_NONE, + .gatt_evt_mask = GATT_DONT_NOTIFY_EVENTS, + .is_variable = CHAR_VALUE_LEN_VARIABLE, +}; + +struct BleServiceHid { + uint16_t svc_handle; + BleGattCharacteristicInstance chars[HidSvcGattCharacteristicCount]; + BleGattCharacteristicInstance input_report_chars[BLE_SVC_HID_INPUT_REPORT_COUNT]; + BleGattCharacteristicInstance output_report_chars[BLE_SVC_HID_OUTPUT_REPORT_COUNT]; + BleGattCharacteristicInstance feature_report_chars[BLE_SVC_HID_FEATURE_REPORT_COUNT]; + GapSvcEventHandler* event_handler; +}; + +static BleEventAckStatus ble_svc_hid_event_handler(void* event, void* context) { + UNUSED(context); + + BleEventAckStatus ret = BleEventNotAck; + hci_event_pckt* event_pckt = (hci_event_pckt*)(((hci_uart_pckt*)event)->data); + evt_blecore_aci* blecore_evt = (evt_blecore_aci*)event_pckt->data; + // aci_gatt_attribute_modified_event_rp0* attribute_modified; + + if(event_pckt->evt == HCI_VENDOR_SPECIFIC_DEBUG_EVT_CODE) { + if(blecore_evt->ecode == ACI_GATT_ATTRIBUTE_MODIFIED_VSEVT_CODE) { + // Process modification events + ret = BleEventAckFlowEnable; + } else if(blecore_evt->ecode == ACI_GATT_SERVER_CONFIRMATION_VSEVT_CODE) { + // Process notification confirmation + ret = BleEventAckFlowEnable; + } + } + return ret; +} + +BleServiceHid* ble_svc_hid_start(void) { + BleServiceHid* hid_svc = malloc(sizeof(BleServiceHid)); + + // Register event handler + hid_svc->event_handler = + ble_event_dispatcher_register_svc_handler(ble_svc_hid_event_handler, hid_svc); + /** + * Add Human Interface Device Service + */ + if(!ble_gatt_service_add( + UUID_TYPE_16, + &ble_svc_hid_uuid, + PRIMARY_SERVICE, + 2 + /* protocol mode */ + (4 * BLE_SVC_HID_INPUT_REPORT_COUNT) + (3 * BLE_SVC_HID_OUTPUT_REPORT_COUNT) + + (3 * BLE_SVC_HID_FEATURE_REPORT_COUNT) + 1 + 2 + 2 + + 2, /* Service + Report Map + HID Information + HID Control Point */ + &hid_svc->svc_handle)) { + free(hid_svc); + return NULL; + } + + // Maintain previously defined characteristic order + ble_gatt_characteristic_init( + hid_svc->svc_handle, + &ble_svc_hid_chars[HidSvcGattCharacteristicProtocolMode], + &hid_svc->chars[HidSvcGattCharacteristicProtocolMode]); + + uint8_t protocol_mode = 1; + ble_gatt_characteristic_update( + hid_svc->svc_handle, + &hid_svc->chars[HidSvcGattCharacteristicProtocolMode], + &protocol_mode); + + // reports + BleGattCharacteristicDescriptorParams ble_svc_hid_char_descr; + BleGattCharacteristicParams report_char; + HidSvcReportId report_id; + + memcpy( + &ble_svc_hid_char_descr, &ble_svc_hid_char_descr_template, sizeof(ble_svc_hid_char_descr)); + memcpy(&report_char, &ble_svc_hid_report_template, sizeof(report_char)); + + ble_svc_hid_char_descr.data_callback.context = &report_id; + report_char.descriptor_params = &ble_svc_hid_char_descr; + + typedef struct { + uint8_t report_type; + uint8_t report_count; + BleGattCharacteristicInstance* chars; + } HidSvcReportCharProps; + + HidSvcReportCharProps hid_report_chars[] = { + {0x01, BLE_SVC_HID_INPUT_REPORT_COUNT, hid_svc->input_report_chars}, + {0x02, BLE_SVC_HID_OUTPUT_REPORT_COUNT, hid_svc->output_report_chars}, + {0x03, BLE_SVC_HID_FEATURE_REPORT_COUNT, hid_svc->feature_report_chars}, + }; + + for(size_t report_type_idx = 0; report_type_idx < COUNT_OF(hid_report_chars); + report_type_idx++) { + report_id.report_type = hid_report_chars[report_type_idx].report_type; + for(size_t report_idx = 0; report_idx < hid_report_chars[report_type_idx].report_count; + report_idx++) { + report_id.report_idx = report_idx + 1; + ble_gatt_characteristic_init( + hid_svc->svc_handle, + &report_char, + &hid_report_chars[report_type_idx].chars[report_idx]); + } + } + + // Setup remaining characteristics + for(size_t i = HidSvcGattCharacteristicReportMap; i < HidSvcGattCharacteristicCount; i++) { + ble_gatt_characteristic_init( + hid_svc->svc_handle, &ble_svc_hid_chars[i], &hid_svc->chars[i]); + } + + return hid_svc; +} + +bool ble_svc_hid_update_report_map(BleServiceHid* hid_svc, const uint8_t* data, uint16_t len) { + furi_assert(data); + furi_assert(hid_svc); + + HidSvcDataWrapper report_data = { + .data_ptr = data, + .data_len = len, + }; + return ble_gatt_characteristic_update( + hid_svc->svc_handle, &hid_svc->chars[HidSvcGattCharacteristicReportMap], &report_data); +} + +bool ble_svc_hid_update_input_report( + BleServiceHid* hid_svc, + uint8_t input_report_num, + uint8_t* data, + uint16_t len) { + furi_assert(data); + furi_assert(hid_svc); + furi_assert(input_report_num < BLE_SVC_HID_INPUT_REPORT_COUNT); + + HidSvcDataWrapper report_data = { + .data_ptr = data, + .data_len = len, + }; + + return ble_gatt_characteristic_update( + hid_svc->svc_handle, &hid_svc->input_report_chars[input_report_num], &report_data); +} + +bool ble_svc_hid_update_info(BleServiceHid* hid_svc, uint8_t* data) { + furi_assert(data); + furi_assert(hid_svc); + + return ble_gatt_characteristic_update( + hid_svc->svc_handle, &hid_svc->chars[HidSvcGattCharacteristicInfo], &data); +} + +void ble_svc_hid_stop(BleServiceHid* hid_svc) { + furi_assert(hid_svc); + ble_event_dispatcher_unregister_svc_handler(hid_svc->event_handler); + // Delete characteristics + for(size_t i = 0; i < HidSvcGattCharacteristicCount; i++) { + ble_gatt_characteristic_delete(hid_svc->svc_handle, &hid_svc->chars[i]); + } + + typedef struct { + uint8_t report_count; + BleGattCharacteristicInstance* chars; + } HidSvcReportCharProps; + + HidSvcReportCharProps hid_report_chars[] = { + {BLE_SVC_HID_INPUT_REPORT_COUNT, hid_svc->input_report_chars}, + {BLE_SVC_HID_OUTPUT_REPORT_COUNT, hid_svc->output_report_chars}, + {BLE_SVC_HID_FEATURE_REPORT_COUNT, hid_svc->feature_report_chars}, + }; + + for(size_t report_type_idx = 0; report_type_idx < COUNT_OF(hid_report_chars); + report_type_idx++) { + for(size_t report_idx = 0; report_idx < hid_report_chars[report_type_idx].report_count; + report_idx++) { + ble_gatt_characteristic_delete( + hid_svc->svc_handle, &hid_report_chars[report_type_idx].chars[report_idx]); + } + } + + // Delete service + ble_gatt_service_delete(hid_svc->svc_handle); + free(hid_svc); +} diff --git a/applications/main/bad_usb/helpers/ble_hid_service.h b/applications/main/bad_usb/helpers/ble_hid_service.h new file mode 100644 index 000000000..e1ac3b0be --- /dev/null +++ b/applications/main/bad_usb/helpers/ble_hid_service.h @@ -0,0 +1,31 @@ +#pragma once + +// Based on + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct BleServiceHid BleServiceHid; + +BleServiceHid* ble_svc_hid_start(void); + +void ble_svc_hid_stop(BleServiceHid* service); + +bool ble_svc_hid_update_report_map(BleServiceHid* service, const uint8_t* data, uint16_t len); + +bool ble_svc_hid_update_input_report( + BleServiceHid* service, + uint8_t input_report_num, + uint8_t* data, + uint16_t len); + +// Expects data to be of length BLE_SVC_HID_INFO_LEN (4 bytes) +bool ble_svc_hid_update_info(BleServiceHid* service, uint8_t* data); + +#ifdef __cplusplus +} +#endif diff --git a/applications/main/bad_usb/helpers/ducky_script.c b/applications/main/bad_usb/helpers/ducky_script.c index 379c3e24a..a64629af2 100644 --- a/applications/main/bad_usb/helpers/ducky_script.c +++ b/applications/main/bad_usb/helpers/ducky_script.c @@ -25,6 +25,8 @@ typedef enum { } WorkerEvtFlags; static const char ducky_cmd_id[] = {"ID"}; +static const char ducky_cmd_bt_id[] = {"BT_ID"}; +static const char ducky_cmd_ble_id[] = {"BLE_ID"}; static const uint8_t numpad_keys[10] = { HID_KEYPAD_0, @@ -40,11 +42,8 @@ static const uint8_t numpad_keys[10] = { }; uint32_t ducky_get_command_len(const char* line) { - uint32_t len = strlen(line); - for(uint32_t i = 0; i < len; i++) { - if(line[i] == ' ') return i; - } - return 0; + char* first_space = strchr(line, ' '); + return first_space ? (first_space - line) : 0; } bool ducky_is_line_end(const char chr) { @@ -180,71 +179,100 @@ static bool ducky_string_next(BadUsbScript* bad_usb) { static int32_t ducky_parse_line(BadUsbScript* bad_usb, FuriString* line) { uint32_t line_len = furi_string_size(line); - const char* line_tmp = furi_string_get_cstr(line); + const char* line_cstr = furi_string_get_cstr(line); if(line_len == 0) { return SCRIPT_STATE_NEXT_LINE; // Skip empty lines } - FURI_LOG_D(WORKER_TAG, "line:%s", line_tmp); + FURI_LOG_D(WORKER_TAG, "line:%s", line_cstr); // Ducky Lang Functions - int32_t cmd_result = ducky_execute_cmd(bad_usb, line_tmp); + int32_t cmd_result = ducky_execute_cmd(bad_usb, line_cstr); if(cmd_result != SCRIPT_STATE_CMD_UNKNOWN) { return cmd_result; } // Mouse Keys - uint16_t key = ducky_get_mouse_keycode_by_name(line_tmp); + uint16_t key = ducky_get_mouse_keycode_by_name(line_cstr); if(key != HID_MOUSE_INVALID) { bad_usb->hid->mouse_press(bad_usb->hid_inst, key); bad_usb->hid->mouse_release(bad_usb->hid_inst, key); return 0; } - // Special keys + modifiers - key = ducky_get_keycode(bad_usb, line_tmp, false); - if(key == HID_KEYBOARD_NONE) { - return ducky_error(bad_usb, "No keycode defined for %s", line_tmp); - } - if((key & 0xFF00) != 0) { - // It's a modifier key - uint32_t offset = ducky_get_command_len(line_tmp) + 1; - // ducky_get_command_len() returns 0 without space, so check for != 1 - if(offset != 1 && line_len > offset) { - // It's also a key combination - key |= ducky_get_keycode(bad_usb, line_tmp + offset, true); - } + // Parse chain of modifiers linked by spaces and hyphens + uint16_t modifiers = 0; + while(1) { + key = ducky_get_next_modifier_keycode_by_name(&line_cstr); + if(key == HID_KEYBOARD_NONE) break; + + modifiers |= key; + char next_char = *line_cstr; + if(next_char == ' ' || next_char == '-') line_cstr++; } + + // Main key + char next_char = *line_cstr; + uint16_t main_key = ducky_get_keycode_by_name(line_cstr); + if(!main_key && next_char) main_key = BADUSB_ASCII_TO_KEY(bad_usb, next_char); + key = modifiers | main_key; + + if(key == 0 && next_char) ducky_error(bad_usb, "No keycode defined for %s", line_cstr); + bad_usb->hid->kb_press(bad_usb->hid_inst, key); bad_usb->hid->kb_release(bad_usb->hid_inst, key); return 0; } static bool ducky_set_usb_id(BadUsbScript* bad_usb, const char* line) { - if(sscanf(line, "%lX:%lX", &bad_usb->hid_cfg.vid, &bad_usb->hid_cfg.pid) == 2) { - bad_usb->hid_cfg.manuf[0] = '\0'; - bad_usb->hid_cfg.product[0] = '\0'; + FuriHalUsbHidConfig* usb_hid_cfg = &bad_usb->hid_cfg->usb; + + if(sscanf(line, "%lX:%lX", &usb_hid_cfg->vid, &usb_hid_cfg->pid) == 2) { + usb_hid_cfg->manuf[0] = '\0'; + usb_hid_cfg->product[0] = '\0'; uint8_t id_len = ducky_get_command_len(line); if(!ducky_is_line_end(line[id_len + 1])) { sscanf( &line[id_len + 1], "%31[^\r\n:]:%31[^\r\n]", - bad_usb->hid_cfg.manuf, - bad_usb->hid_cfg.product); + usb_hid_cfg->manuf, + usb_hid_cfg->product); } FURI_LOG_D( WORKER_TAG, "set id: %04lX:%04lX mfr:%s product:%s", - bad_usb->hid_cfg.vid, - bad_usb->hid_cfg.pid, - bad_usb->hid_cfg.manuf, - bad_usb->hid_cfg.product); + usb_hid_cfg->vid, + usb_hid_cfg->pid, + usb_hid_cfg->manuf, + usb_hid_cfg->product); return true; } return false; } +static bool ducky_set_ble_id(BadUsbScript* bad_usb, const char* line) { + BleProfileHidParams* ble_hid_cfg = &bad_usb->hid_cfg->ble; + + size_t line_len = strlen(line); + size_t mac_len = sizeof(ble_hid_cfg->mac) * 3; // 2 hex chars + separator per byte + if(line_len < mac_len + 1) return false; // MAC + at least 1 char for name + + for(size_t i = 0; i < sizeof(ble_hid_cfg->mac); i++) { + const char* hex_byte = &line[i * 3]; + // This sscanf() doesn't work well with %02hhX, need to use a u32 + uint32_t temp_uint; + if(sscanf(hex_byte, "%02lX", &temp_uint) != 1) { + return false; + } + ble_hid_cfg->mac[sizeof(ble_hid_cfg->mac) - 1 - i] = temp_uint; + } + + strlcpy(ble_hid_cfg->name, line + mac_len, sizeof(ble_hid_cfg->name)); + FURI_LOG_D(WORKER_TAG, "set ble id: %s", line); + return true; +} + static void bad_usb_hid_state_callback(bool state, void* context) { furi_assert(context); BadUsbScript* bad_usb = context; @@ -283,17 +311,30 @@ static bool ducky_script_preload(BadUsbScript* bad_usb, File* script_file) { } } while(ret > 0); - const char* line_tmp = furi_string_get_cstr(bad_usb->line); - bool id_set = false; // Looking for ID command at first line - if(strncmp(line_tmp, ducky_cmd_id, strlen(ducky_cmd_id)) == 0) { - id_set = ducky_set_usb_id(bad_usb, &line_tmp[strlen(ducky_cmd_id) + 1]); + if(bad_usb->load_id_cfg) { + const char* line_tmp = furi_string_get_cstr(bad_usb->line); + BadUsbHidInterface interface = *bad_usb->interface; + // Look for ID/BLE_ID/BT_ID command on first line + if(strncmp(line_tmp, ducky_cmd_id, strlen(ducky_cmd_id)) == 0) { + if(ducky_set_usb_id(bad_usb, &line_tmp[strlen(ducky_cmd_id) + 1])) { + interface = BadUsbHidInterfaceUsb; + } + } else if( + strncmp(line_tmp, ducky_cmd_ble_id, strlen(ducky_cmd_ble_id)) == 0 || + strncmp(line_tmp, ducky_cmd_bt_id, strlen(ducky_cmd_bt_id)) == 0) { + if(ducky_set_ble_id(bad_usb, &line_tmp[ducky_get_command_len(line_tmp) + 1])) { + interface = BadUsbHidInterfaceBle; + } + } + + // Auto-switch based on ID/BLE_ID/BT_ID command, user can override manually after + if(interface != *bad_usb->interface) { + *bad_usb->interface = interface; + bad_usb->hid = bad_usb_hid_get_interface(*bad_usb->interface); + } } - if(id_set) { - bad_usb->hid_inst = bad_usb->hid->init(&bad_usb->hid_cfg); - } else { - bad_usb->hid_inst = bad_usb->hid->init(NULL); - } + bad_usb->hid_inst = bad_usb->hid->init(bad_usb->hid_cfg); bad_usb->hid->set_state_callback(bad_usb->hid_inst, bad_usb_hid_state_callback, bad_usb); storage_file_seek(script_file, 0, true); @@ -396,9 +437,12 @@ static int32_t bad_usb_worker(void* context) { bad_usb->line = furi_string_alloc(); bad_usb->line_prev = furi_string_alloc(); bad_usb->string_print = furi_string_alloc(); + bad_usb->st.elapsed = 0; while(1) { + uint32_t start = furi_get_tick(); if(worker_state == BadUsbStateInit) { // State: initialization + start = 0; if(storage_file_open( script_file, furi_string_get_cstr(bad_usb->file_path), @@ -408,7 +452,7 @@ static int32_t bad_usb_worker(void* context) { if(bad_usb->hid->is_connected(bad_usb->hid_inst)) { worker_state = BadUsbStateIdle; // Ready to run } else { - worker_state = BadUsbStateNotConnected; // USB not connected + worker_state = BadUsbStateNotConnected; // Not connected } } else { worker_state = BadUsbStateScriptError; // Script preload error @@ -419,7 +463,8 @@ static int32_t bad_usb_worker(void* context) { } bad_usb->st.state = worker_state; - } else if(worker_state == BadUsbStateNotConnected) { // State: USB not connected + } else if(worker_state == BadUsbStateNotConnected) { // State: Not connected + start = 0; uint32_t flags = bad_usb_flags_get( WorkerEvtEnd | WorkerEvtConnect | WorkerEvtDisconnect | WorkerEvtStartStop, FuriWaitForever); @@ -429,11 +474,12 @@ static int32_t bad_usb_worker(void* context) { } else if(flags & WorkerEvtConnect) { worker_state = BadUsbStateIdle; // Ready to run } else if(flags & WorkerEvtStartStop) { - worker_state = BadUsbStateWillRun; // Will run when USB is connected + worker_state = BadUsbStateWillRun; // Will run when connected } bad_usb->st.state = worker_state; } else if(worker_state == BadUsbStateIdle) { // State: ready to start + start = 0; uint32_t flags = bad_usb_flags_get( WorkerEvtEnd | WorkerEvtStartStop | WorkerEvtDisconnect, FuriWaitForever); @@ -452,12 +498,14 @@ static int32_t bad_usb_worker(void* context) { bad_usb->file_end = false; storage_file_seek(script_file, 0, true); worker_state = BadUsbStateRunning; + bad_usb->st.elapsed = 0; } else if(flags & WorkerEvtDisconnect) { - worker_state = BadUsbStateNotConnected; // USB disconnected + worker_state = BadUsbStateNotConnected; // Disconnected } bad_usb->st.state = worker_state; } else if(worker_state == BadUsbStateWillRun) { // State: start on connection + start = 0; uint32_t flags = bad_usb_flags_get( WorkerEvtEnd | WorkerEvtConnect | WorkerEvtStartStop, FuriWaitForever); @@ -482,6 +530,7 @@ static int32_t bad_usb_worker(void* context) { if(flags == (unsigned)FuriFlagErrorTimeout) { // If nothing happened - start script execution worker_state = BadUsbStateRunning; + bad_usb->st.elapsed = 0; } else if(flags & WorkerEvtStartStop) { worker_state = BadUsbStateIdle; furi_thread_flags_clear(WorkerEvtStartStop); @@ -492,7 +541,7 @@ static int32_t bad_usb_worker(void* context) { bad_usb->st.state = worker_state; } else if(worker_state == BadUsbStateRunning) { // State: running - uint16_t delay_cur = (delay_val > 1000) ? (1000) : (delay_val); + uint16_t delay_cur = (delay_val > 100) ? (100) : (delay_val); uint32_t flags = furi_thread_flags_wait( WorkerEvtEnd | WorkerEvtStartStop | WorkerEvtPauseResume | WorkerEvtDisconnect, FuriFlagWaitAny, @@ -506,19 +555,21 @@ static int32_t bad_usb_worker(void* context) { worker_state = BadUsbStateIdle; // Stop executing script bad_usb->hid->release_all(bad_usb->hid_inst); } else if(flags & WorkerEvtDisconnect) { - worker_state = BadUsbStateNotConnected; // USB disconnected + worker_state = BadUsbStateNotConnected; // Disconnected bad_usb->hid->release_all(bad_usb->hid_inst); } else if(flags & WorkerEvtPauseResume) { pause_state = BadUsbStateRunning; worker_state = BadUsbStatePaused; // Pause } bad_usb->st.state = worker_state; + bad_usb->st.elapsed += (furi_get_tick() - start); continue; } else if( (flags == (unsigned)FuriFlagErrorTimeout) || (flags == (unsigned)FuriFlagErrorResource)) { if(delay_val > 0) { bad_usb->st.delay_remain--; + bad_usb->st.elapsed += (furi_get_tick() - start); continue; } bad_usb->st.state = BadUsbStateRunning; @@ -533,6 +584,7 @@ static int32_t bad_usb_worker(void* context) { worker_state = BadUsbStateIdle; bad_usb->st.state = BadUsbStateDone; bad_usb->hid->release_all(bad_usb->hid_inst); + bad_usb->st.elapsed += (furi_get_tick() - start); continue; } else if(delay_val == SCRIPT_STATE_STRING_START) { // Start printing string with delays delay_val = bad_usb->defdelay; @@ -541,14 +593,15 @@ static int32_t bad_usb_worker(void* context) { } else if(delay_val == SCRIPT_STATE_WAIT_FOR_BTN) { // set state to wait for user input worker_state = BadUsbStateWaitForBtn; bad_usb->st.state = BadUsbStateWaitForBtn; // Show long delays - } else if(delay_val > 1000) { + } else if(delay_val > 100) { bad_usb->st.state = BadUsbStateDelay; // Show long delays - bad_usb->st.delay_remain = delay_val / 1000; + bad_usb->st.delay_remain = delay_val / 100; } } else { furi_check((flags & FuriFlagError) == 0); } } else if(worker_state == BadUsbStateWaitForBtn) { // State: Wait for button Press + start = 0; uint32_t flags = bad_usb_flags_get( WorkerEvtEnd | WorkerEvtStartStop | WorkerEvtPauseResume | WorkerEvtDisconnect, FuriWaitForever); @@ -559,13 +612,14 @@ static int32_t bad_usb_worker(void* context) { delay_val = 0; worker_state = BadUsbStateRunning; } else if(flags & WorkerEvtDisconnect) { - worker_state = BadUsbStateNotConnected; // USB disconnected + worker_state = BadUsbStateNotConnected; // Disconnected bad_usb->hid->release_all(bad_usb->hid_inst); } bad_usb->st.state = worker_state; continue; } } else if(worker_state == BadUsbStatePaused) { // State: Paused + start = 0; uint32_t flags = bad_usb_flags_get( WorkerEvtEnd | WorkerEvtStartStop | WorkerEvtPauseResume | WorkerEvtDisconnect, FuriWaitForever); @@ -577,14 +631,14 @@ static int32_t bad_usb_worker(void* context) { bad_usb->st.state = worker_state; bad_usb->hid->release_all(bad_usb->hid_inst); } else if(flags & WorkerEvtDisconnect) { - worker_state = BadUsbStateNotConnected; // USB disconnected + worker_state = BadUsbStateNotConnected; // Disconnected bad_usb->st.state = worker_state; bad_usb->hid->release_all(bad_usb->hid_inst); } else if(flags & WorkerEvtPauseResume) { if(pause_state == BadUsbStateRunning) { if(delay_val > 0) { bad_usb->st.state = BadUsbStateDelay; - bad_usb->st.delay_remain = delay_val / 1000; + bad_usb->st.delay_remain = delay_val / 100; } else { bad_usb->st.state = BadUsbStateRunning; delay_val = 0; @@ -611,13 +665,14 @@ static int32_t bad_usb_worker(void* context) { worker_state = BadUsbStateIdle; // Stop executing script bad_usb->hid->release_all(bad_usb->hid_inst); } else if(flags & WorkerEvtDisconnect) { - worker_state = BadUsbStateNotConnected; // USB disconnected + worker_state = BadUsbStateNotConnected; // Disconnected bad_usb->hid->release_all(bad_usb->hid_inst); } else if(flags & WorkerEvtPauseResume) { pause_state = BadUsbStateStringDelay; worker_state = BadUsbStatePaused; // Pause } bad_usb->st.state = worker_state; + bad_usb->st.elapsed += (furi_get_tick() - start); continue; } else if( (flags == (unsigned)FuriFlagErrorTimeout) || @@ -633,6 +688,7 @@ static int32_t bad_usb_worker(void* context) { } else if( (worker_state == BadUsbStateFileError) || (worker_state == BadUsbStateScriptError)) { // State: error + start = 0; uint32_t flags = bad_usb_flags_get(WorkerEvtEnd, FuriWaitForever); // Waiting for exit command @@ -640,6 +696,9 @@ static int32_t bad_usb_worker(void* context) { break; } } + if(start) { + bad_usb->st.elapsed += (furi_get_tick() - start); + } } bad_usb->hid->set_state_callback(bad_usb->hid_inst, NULL, NULL); @@ -662,7 +721,11 @@ static void bad_usb_script_set_default_keyboard_layout(BadUsbScript* bad_usb) { memcpy(bad_usb->layout, hid_asciimap, MIN(sizeof(hid_asciimap), sizeof(bad_usb->layout))); } -BadUsbScript* bad_usb_script_open(FuriString* file_path, BadUsbHidInterface interface) { +BadUsbScript* bad_usb_script_open( + FuriString* file_path, + BadUsbHidInterface* interface, + BadUsbHidConfig* hid_cfg, + bool load_id_cfg) { furi_assert(file_path); BadUsbScript* bad_usb = malloc(sizeof(BadUsbScript)); @@ -672,7 +735,10 @@ BadUsbScript* bad_usb_script_open(FuriString* file_path, BadUsbHidInterface inte bad_usb->st.state = BadUsbStateInit; bad_usb->st.error[0] = '\0'; - bad_usb->hid = bad_usb_hid_get_interface(interface); + bad_usb->interface = interface; + bad_usb->hid_cfg = hid_cfg; + bad_usb->load_id_cfg = load_id_cfg; + bad_usb->hid = bad_usb_hid_get_interface(*bad_usb->interface); bad_usb->thread = furi_thread_alloc_ex("BadUsbWorker", 2048, bad_usb_worker, bad_usb); furi_thread_start(bad_usb->thread); diff --git a/applications/main/bad_usb/helpers/ducky_script.h b/applications/main/bad_usb/helpers/ducky_script.h index 9519623f6..9131ef43e 100644 --- a/applications/main/bad_usb/helpers/ducky_script.h +++ b/applications/main/bad_usb/helpers/ducky_script.h @@ -30,11 +30,16 @@ typedef struct { uint32_t delay_remain; size_t error_line; char error[64]; + uint32_t elapsed; } BadUsbState; typedef struct BadUsbScript BadUsbScript; -BadUsbScript* bad_usb_script_open(FuriString* file_path, BadUsbHidInterface interface); +BadUsbScript* bad_usb_script_open( + FuriString* file_path, + BadUsbHidInterface* interface, + BadUsbHidConfig* hid_cfg, + bool load_id_cfg); void bad_usb_script_close(BadUsbScript* bad_usb); diff --git a/applications/main/bad_usb/helpers/ducky_script_commands.c b/applications/main/bad_usb/helpers/ducky_script_commands.c index 1b4ff55cb..6c6fe36c7 100644 --- a/applications/main/bad_usb/helpers/ducky_script_commands.c +++ b/applications/main/bad_usb/helpers/ducky_script_commands.c @@ -256,6 +256,8 @@ static int32_t ducky_fnc_mouse_move(BadUsbScript* bad_usb, const char* line, int static const DuckyCmd ducky_commands[] = { {"REM", NULL, -1}, {"ID", NULL, -1}, + {"BT_ID", NULL, -1}, + {"BLE_ID", NULL, -1}, {"DELAY", ducky_fnc_delay, -1}, {"STRING", ducky_fnc_string, 0}, {"STRINGLN", ducky_fnc_string, 1}, diff --git a/applications/main/bad_usb/helpers/ducky_script_i.h b/applications/main/bad_usb/helpers/ducky_script_i.h index fd95ecf58..d735a8407 100644 --- a/applications/main/bad_usb/helpers/ducky_script_i.h +++ b/applications/main/bad_usb/helpers/ducky_script_i.h @@ -22,7 +22,9 @@ extern "C" { #define HID_MOUSE_NONE 0 struct BadUsbScript { - FuriHalUsbHidConfig hid_cfg; + BadUsbHidInterface* interface; + BadUsbHidConfig* hid_cfg; + bool load_id_cfg; const BadUsbHidApi* hid; void* hid_inst; FuriThread* thread; @@ -54,6 +56,8 @@ uint32_t ducky_get_command_len(const char* line); bool ducky_is_line_end(const char chr); +uint16_t ducky_get_next_modifier_keycode_by_name(const char** param); + uint16_t ducky_get_keycode_by_name(const char* param); uint16_t ducky_get_media_keycode_by_name(const char* param); diff --git a/applications/main/bad_usb/helpers/ducky_script_keycodes.c b/applications/main/bad_usb/helpers/ducky_script_keycodes.c index 7dd2e4d16..ce957bb4e 100644 --- a/applications/main/bad_usb/helpers/ducky_script_keycodes.c +++ b/applications/main/bad_usb/helpers/ducky_script_keycodes.c @@ -6,21 +6,16 @@ typedef struct { uint16_t keycode; } DuckyKey; -static const DuckyKey ducky_keys[] = { - {"CTRL-ALT", KEY_MOD_LEFT_CTRL | KEY_MOD_LEFT_ALT}, - {"CTRL-SHIFT", KEY_MOD_LEFT_CTRL | KEY_MOD_LEFT_SHIFT}, - {"ALT-SHIFT", KEY_MOD_LEFT_ALT | KEY_MOD_LEFT_SHIFT}, - {"ALT-GUI", KEY_MOD_LEFT_ALT | KEY_MOD_LEFT_GUI}, - {"GUI-SHIFT", KEY_MOD_LEFT_GUI | KEY_MOD_LEFT_SHIFT}, - {"GUI-CTRL", KEY_MOD_LEFT_GUI | KEY_MOD_LEFT_CTRL}, - +static const DuckyKey ducky_modifier_keys[] = { {"CTRL", KEY_MOD_LEFT_CTRL}, {"CONTROL", KEY_MOD_LEFT_CTRL}, {"SHIFT", KEY_MOD_LEFT_SHIFT}, {"ALT", KEY_MOD_LEFT_ALT}, {"GUI", KEY_MOD_LEFT_GUI}, {"WINDOWS", KEY_MOD_LEFT_GUI}, +}; +static const DuckyKey ducky_keys[] = { {"DOWNARROW", HID_KEYBOARD_DOWN_ARROW}, {"DOWN", HID_KEYBOARD_DOWN_ARROW}, {"LEFTARROW", HID_KEYBOARD_LEFT_ARROW}, @@ -119,6 +114,23 @@ static const DuckyKey ducky_mouse_keys[] = { {"WHEEL_CLICK", HID_MOUSE_BTN_WHEEL}, }; +uint16_t ducky_get_next_modifier_keycode_by_name(const char** param) { + const char* input_str = *param; + + for(size_t i = 0; i < COUNT_OF(ducky_modifier_keys); i++) { + size_t key_cmd_len = strlen(ducky_modifier_keys[i].name); + if((strncmp(input_str, ducky_modifier_keys[i].name, key_cmd_len) == 0)) { + char next_char_after_key = input_str[key_cmd_len]; + if(ducky_is_line_end(next_char_after_key) || (next_char_after_key == '-')) { + *param = &input_str[key_cmd_len]; + return ducky_modifier_keys[i].keycode; + } + } + } + + return HID_KEYBOARD_NONE; +} + uint16_t ducky_get_keycode_by_name(const char* param) { for(size_t i = 0; i < COUNT_OF(ducky_keys); i++) { size_t key_cmd_len = strlen(ducky_keys[i].name); diff --git a/applications/main/bad_usb/resources/badusb/assets/layouts/de-DE-mac.kl b/applications/main/bad_usb/resources/badusb/assets/layouts/de-DE-mac.kl new file mode 100755 index 0000000000000000000000000000000000000000..471b7143e24c8351d253885436bd60fb7cc4a641 GIT binary patch literal 256 zcmaLL#|^?z0Kibog3t+}_mT#p2@aG2{wVA}17|My_6)xrIdS-1e|2W<#ydMxGw&9b z-n{YT&5PVUYc_1zk&=;9Q1bHWgS`(g#-U=>$eMKinterface == BadUsbHidInterfaceBle ? BadUsbHidInterfaceUsb : + BadUsbHidInterfaceBle); + variable_item_set_current_value_text( + item, bad_usb->interface == BadUsbHidInterfaceBle ? "BLE" : "USB"); + view_dispatcher_send_custom_event(bad_usb->view_dispatcher, ConfigIndexConnection); +} + +void bad_usb_scene_config_ble_persist_pairing_callback(VariableItem* item) { + BadUsbApp* bad_usb = variable_item_get_context(item); + bool value = variable_item_get_current_value_index(item); + const BadUsbHidApi* hid = bad_usb_hid_get_interface(bad_usb->interface); + // Apply to current script config + bad_usb->script_hid_cfg.ble.bonding = value; + hid->adjust_config(&bad_usb->script_hid_cfg); + // Set in user config to save in settings file + bad_usb->user_hid_cfg.ble.bonding = value; + variable_item_set_current_value_text(item, value ? "ON" : "OFF"); +} + +const char* const ble_pairing_mode_names[GapPairingCount] = { + "YesNo", + "PIN Type", + "PIN Y/N", +}; +void bad_usb_scene_config_ble_pairing_mode_callback(VariableItem* item) { + BadUsbApp* bad_usb = variable_item_get_context(item); + uint8_t index = variable_item_get_current_value_index(item); + const BadUsbHidApi* hid = bad_usb_hid_get_interface(bad_usb->interface); + // Apply to current script config + bad_usb->script_hid_cfg.ble.pairing = index; + hid->adjust_config(&bad_usb->script_hid_cfg); + // Set in user config to save in settings file + bad_usb->user_hid_cfg.ble.pairing = index; + variable_item_set_current_value_text(item, ble_pairing_mode_names[index]); +} + void bad_usb_scene_config_select_callback(void* context, uint32_t index) { BadUsbApp* bad_usb = context; @@ -13,12 +71,59 @@ void bad_usb_scene_config_select_callback(void* context, uint32_t index) { static void draw_menu(BadUsbApp* bad_usb) { VariableItemList* var_item_list = bad_usb->var_item_list; + VariableItem* item; variable_item_list_reset(var_item_list); variable_item_list_add(var_item_list, "Keyboard Layout (global)", 0, NULL, NULL); - variable_item_list_add(var_item_list, "Remove Pairing", 0, NULL, NULL); + item = variable_item_list_add( + var_item_list, "Connection", 2, bad_usb_scene_config_connection_callback, bad_usb); + variable_item_set_current_value_index(item, bad_usb->interface == BadUsbHidInterfaceBle); + variable_item_set_current_value_text( + item, bad_usb->interface == BadUsbHidInterfaceBle ? "BLE" : "USB"); + + if(bad_usb->interface == BadUsbHidInterfaceBle) { + BleProfileHidParams* ble_hid_cfg = &bad_usb->script_hid_cfg.ble; + + item = variable_item_list_add( + var_item_list, + "Persist Pairing", + 2, + bad_usb_scene_config_ble_persist_pairing_callback, + bad_usb); + variable_item_set_current_value_index(item, ble_hid_cfg->bonding); + variable_item_set_current_value_text(item, ble_hid_cfg->bonding ? "ON" : "OFF"); + + item = variable_item_list_add( + var_item_list, + "Pairing Mode", + GapPairingCount, + bad_usb_scene_config_ble_pairing_mode_callback, + bad_usb); + variable_item_set_current_value_index(item, ble_hid_cfg->pairing); + variable_item_set_current_value_text(item, ble_pairing_mode_names[ble_hid_cfg->pairing]); + + variable_item_list_add(var_item_list, "Set Device Name", 0, NULL, NULL); + + variable_item_list_add(var_item_list, "Set MAC Address", 0, NULL, NULL); + + variable_item_list_add(var_item_list, "Randomize MAC Address", 0, NULL, NULL); + + variable_item_list_add(var_item_list, "Restore BLE Defaults", 0, NULL, NULL); + + variable_item_list_add(var_item_list, "Remove BLE Pairing", 0, NULL, NULL); + } else { + variable_item_list_add(var_item_list, "Set Manufacturer Name", 0, NULL, NULL); + + variable_item_list_add(var_item_list, "Set Product Name", 0, NULL, NULL); + + variable_item_list_add(var_item_list, "Set VID and PID", 0, NULL, NULL); + + variable_item_list_add(var_item_list, "Randomize VID and PID", 0, NULL, NULL); + + variable_item_list_add(var_item_list, "Restore USB Defaults", 0, NULL, NULL); + } } void bad_usb_scene_config_on_enter(void* context) { @@ -28,7 +133,8 @@ void bad_usb_scene_config_on_enter(void* context) { variable_item_list_set_enter_callback( var_item_list, bad_usb_scene_config_select_callback, bad_usb); draw_menu(bad_usb); - variable_item_list_set_selected_item(var_item_list, 0); + variable_item_list_set_selected_item( + var_item_list, scene_manager_get_scene_state(bad_usb->scene_manager, BadUsbSceneConfig)); view_dispatcher_switch_to_view(bad_usb->view_dispatcher, BadUsbAppViewConfig); } @@ -38,13 +144,110 @@ bool bad_usb_scene_config_on_event(void* context, SceneManagerEvent event) { bool consumed = false; if(event.type == SceneManagerEventTypeCustom) { + scene_manager_set_scene_state(bad_usb->scene_manager, BadUsbSceneConfig, event.event); consumed = true; - if(event.event == ConfigIndexKeyboardLayout) { + const BadUsbHidApi* hid = bad_usb_hid_get_interface(bad_usb->interface); + + switch(event.event) { + case ConfigIndexKeyboardLayout: scene_manager_next_scene(bad_usb->scene_manager, BadUsbSceneConfigLayout); - } else if(event.event == ConfigIndexBleUnpair) { - scene_manager_next_scene(bad_usb->scene_manager, BadUsbSceneConfirmUnpair); + break; + case ConfigIndexConnection: + // Refresh default values for new interface + hid->adjust_config(&bad_usb->script_hid_cfg); + // Redraw menu with new interface options + draw_menu(bad_usb); + break; + default: + break; + } + if(bad_usb->interface == BadUsbHidInterfaceBle) { + switch(event.event) { + case ConfigIndexBleSetDeviceName: + scene_manager_next_scene(bad_usb->scene_manager, BadUsbSceneConfigBleName); + break; + case ConfigIndexBleSetMacAddress: + scene_manager_next_scene(bad_usb->scene_manager, BadUsbSceneConfigBleMac); + break; + case ConfigIndexBleRandomizeMacAddress: + // Apply to current script config + furi_hal_random_fill_buf( + bad_usb->script_hid_cfg.ble.mac, sizeof(bad_usb->script_hid_cfg.ble.mac)); + bad_usb->script_hid_cfg.ble.mac[sizeof(bad_usb->script_hid_cfg.ble.mac) - 1] |= + 0b11 << 6; // Set 2 MSB for Random Static Address + hid->adjust_config(&bad_usb->script_hid_cfg); + // Set in user config to save in settings file + memcpy( + bad_usb->user_hid_cfg.ble.mac, + bad_usb->script_hid_cfg.ble.mac, + sizeof(bad_usb->user_hid_cfg.ble.mac)); + scene_manager_next_scene(bad_usb->scene_manager, BadUsbSceneDone); + break; + case ConfigIndexBleRestoreDefaults: + // Apply to current script config + bad_usb->script_hid_cfg.ble.name[0] = '\0'; + memset( + bad_usb->script_hid_cfg.ble.mac, 0, sizeof(bad_usb->script_hid_cfg.ble.mac)); + bad_usb->script_hid_cfg.ble.bonding = true; + bad_usb->script_hid_cfg.ble.pairing = GapPairingPinCodeVerifyYesNo; + hid->adjust_config(&bad_usb->script_hid_cfg); + // Set in user config to save in settings file + memcpy( + &bad_usb->user_hid_cfg.ble, + &bad_usb->script_hid_cfg.ble, + sizeof(bad_usb->user_hid_cfg.ble)); + scene_manager_next_scene(bad_usb->scene_manager, BadUsbSceneDone); + break; + case ConfigIndexBleRemovePairing: + scene_manager_next_scene(bad_usb->scene_manager, BadUsbSceneConfirmUnpair); + break; + default: + break; + } } else { - furi_crash("Unknown key type"); + switch(event.event) { + case ConfigIndexUsbSetManufacturerName: + scene_manager_set_scene_state( + bad_usb->scene_manager, BadUsbSceneConfigUsbName, true); + scene_manager_next_scene(bad_usb->scene_manager, BadUsbSceneConfigUsbName); + break; + case ConfigIndexUsbSetProductName: + scene_manager_set_scene_state( + bad_usb->scene_manager, BadUsbSceneConfigUsbName, false); + scene_manager_next_scene(bad_usb->scene_manager, BadUsbSceneConfigUsbName); + break; + case ConfigIndexUsbSetVidPid: + scene_manager_next_scene(bad_usb->scene_manager, BadUsbSceneConfigUsbVidPid); + break; + case ConfigIndexUsbRandomizeVidPid: + furi_hal_random_fill_buf( + (void*)bad_usb->usb_vidpid_buf, sizeof(bad_usb->usb_vidpid_buf)); + // Apply to current script config + bad_usb->script_hid_cfg.usb.vid = bad_usb->usb_vidpid_buf[0]; + bad_usb->script_hid_cfg.usb.pid = bad_usb->usb_vidpid_buf[1]; + hid->adjust_config(&bad_usb->script_hid_cfg); + // Set in user config to save in settings file + bad_usb->user_hid_cfg.usb.vid = bad_usb->script_hid_cfg.usb.vid; + bad_usb->user_hid_cfg.usb.pid = bad_usb->script_hid_cfg.usb.pid; + scene_manager_next_scene(bad_usb->scene_manager, BadUsbSceneDone); + break; + case ConfigIndexUsbRestoreDefaults: + // Apply to current script config + bad_usb->script_hid_cfg.usb.vid = 0; + bad_usb->script_hid_cfg.usb.pid = 0; + bad_usb->script_hid_cfg.usb.manuf[0] = '\0'; + bad_usb->script_hid_cfg.usb.product[0] = '\0'; + hid->adjust_config(&bad_usb->script_hid_cfg); + // Set in user config to save in settings file + memcpy( + &bad_usb->user_hid_cfg.usb, + &bad_usb->script_hid_cfg.usb, + sizeof(bad_usb->user_hid_cfg.usb)); + scene_manager_next_scene(bad_usb->scene_manager, BadUsbSceneDone); + break; + default: + break; + } } } diff --git a/applications/main/bad_usb/scenes/bad_usb_scene_config.h b/applications/main/bad_usb/scenes/bad_usb_scene_config.h index 3d1b8b1a7..2ea25e134 100644 --- a/applications/main/bad_usb/scenes/bad_usb_scene_config.h +++ b/applications/main/bad_usb/scenes/bad_usb_scene_config.h @@ -3,5 +3,9 @@ ADD_SCENE(bad_usb, work, Work) ADD_SCENE(bad_usb, error, Error) ADD_SCENE(bad_usb, config, Config) ADD_SCENE(bad_usb, config_layout, ConfigLayout) +ADD_SCENE(bad_usb, config_ble_name, ConfigBleName) +ADD_SCENE(bad_usb, config_ble_mac, ConfigBleMac) +ADD_SCENE(bad_usb, config_usb_name, ConfigUsbName) +ADD_SCENE(bad_usb, config_usb_vidpid, ConfigUsbVidPid) ADD_SCENE(bad_usb, confirm_unpair, ConfirmUnpair) -ADD_SCENE(bad_usb, unpair_done, UnpairDone) +ADD_SCENE(bad_usb, done, Done) diff --git a/applications/main/bad_usb/scenes/bad_usb_scene_config_ble_mac.c b/applications/main/bad_usb/scenes/bad_usb_scene_config_ble_mac.c new file mode 100644 index 000000000..7ad4e3ed4 --- /dev/null +++ b/applications/main/bad_usb/scenes/bad_usb_scene_config_ble_mac.c @@ -0,0 +1,73 @@ +#include "../bad_usb_app_i.h" + +enum ByteInputResult { + ByteInputResultOk, +}; + +static void reverse_mac_addr(uint8_t mac_addr[GAP_MAC_ADDR_SIZE]) { + uint8_t tmp; + for(size_t i = 0; i < GAP_MAC_ADDR_SIZE / 2; i++) { + tmp = mac_addr[i]; + mac_addr[i] = mac_addr[GAP_MAC_ADDR_SIZE - 1 - i]; + mac_addr[GAP_MAC_ADDR_SIZE - 1 - i] = tmp; + } +} + +void bad_usb_scene_config_ble_mac_byte_input_callback(void* context) { + BadUsbApp* bad_usb = context; + + view_dispatcher_send_custom_event(bad_usb->view_dispatcher, ByteInputResultOk); +} + +void bad_usb_scene_config_ble_mac_on_enter(void* context) { + BadUsbApp* bad_usb = context; + ByteInput* byte_input = bad_usb->byte_input; + + memcpy(bad_usb->ble_mac_buf, bad_usb->script_hid_cfg.ble.mac, sizeof(bad_usb->ble_mac_buf)); + reverse_mac_addr(bad_usb->ble_mac_buf); + byte_input_set_header_text(byte_input, "Set BLE MAC address"); + + byte_input_set_result_callback( + byte_input, + bad_usb_scene_config_ble_mac_byte_input_callback, + NULL, + bad_usb, + bad_usb->ble_mac_buf, + sizeof(bad_usb->ble_mac_buf)); + + view_dispatcher_switch_to_view(bad_usb->view_dispatcher, BadUsbAppViewByteInput); +} + +bool bad_usb_scene_config_ble_mac_on_event(void* context, SceneManagerEvent event) { + BadUsbApp* bad_usb = context; + bool consumed = false; + + if(event.type == SceneManagerEventTypeCustom) { + consumed = true; + if(event.event == ByteInputResultOk) { + const BadUsbHidApi* hid = bad_usb_hid_get_interface(bad_usb->interface); + reverse_mac_addr(bad_usb->ble_mac_buf); + // Apply to current script config + memcpy( + bad_usb->script_hid_cfg.ble.mac, + bad_usb->ble_mac_buf, + sizeof(bad_usb->script_hid_cfg.ble.mac)); + hid->adjust_config(&bad_usb->script_hid_cfg); + // Set in user config to save in settings file + memcpy( + bad_usb->user_hid_cfg.ble.mac, + bad_usb->ble_mac_buf, + sizeof(bad_usb->user_hid_cfg.ble.mac)); + } + scene_manager_previous_scene(bad_usb->scene_manager); + } + return consumed; +} + +void bad_usb_scene_config_ble_mac_on_exit(void* context) { + BadUsbApp* bad_usb = context; + ByteInput* byte_input = bad_usb->byte_input; + + byte_input_set_result_callback(byte_input, NULL, NULL, NULL, NULL, 0); + byte_input_set_header_text(byte_input, ""); +} diff --git a/applications/main/bad_usb/scenes/bad_usb_scene_config_ble_name.c b/applications/main/bad_usb/scenes/bad_usb_scene_config_ble_name.c new file mode 100644 index 000000000..af7913e7d --- /dev/null +++ b/applications/main/bad_usb/scenes/bad_usb_scene_config_ble_name.c @@ -0,0 +1,62 @@ +#include "../bad_usb_app_i.h" + +enum TextInputResult { + TextInputResultOk, +}; + +static void bad_usb_scene_config_ble_name_text_input_callback(void* context) { + BadUsbApp* bad_usb = context; + + view_dispatcher_send_custom_event(bad_usb->view_dispatcher, TextInputResultOk); +} + +void bad_usb_scene_config_ble_name_on_enter(void* context) { + BadUsbApp* bad_usb = context; + TextInput* text_input = bad_usb->text_input; + + strlcpy( + bad_usb->ble_name_buf, bad_usb->script_hid_cfg.ble.name, sizeof(bad_usb->ble_name_buf)); + text_input_set_header_text(text_input, "Set BLE device name"); + + text_input_set_result_callback( + text_input, + bad_usb_scene_config_ble_name_text_input_callback, + bad_usb, + bad_usb->ble_name_buf, + sizeof(bad_usb->ble_name_buf), + true); + + view_dispatcher_switch_to_view(bad_usb->view_dispatcher, BadUsbAppViewTextInput); +} + +bool bad_usb_scene_config_ble_name_on_event(void* context, SceneManagerEvent event) { + BadUsbApp* bad_usb = context; + bool consumed = false; + + if(event.type == SceneManagerEventTypeCustom) { + consumed = true; + if(event.event == TextInputResultOk) { + const BadUsbHidApi* hid = bad_usb_hid_get_interface(bad_usb->interface); + // Apply to current script config + strlcpy( + bad_usb->script_hid_cfg.ble.name, + bad_usb->ble_name_buf, + sizeof(bad_usb->script_hid_cfg.ble.name)); + hid->adjust_config(&bad_usb->script_hid_cfg); + // Set in user config to save in settings file + strlcpy( + bad_usb->user_hid_cfg.ble.name, + bad_usb->ble_name_buf, + sizeof(bad_usb->user_hid_cfg.ble.name)); + } + scene_manager_previous_scene(bad_usb->scene_manager); + } + return consumed; +} + +void bad_usb_scene_config_ble_name_on_exit(void* context) { + BadUsbApp* bad_usb = context; + TextInput* text_input = bad_usb->text_input; + + text_input_reset(text_input); +} diff --git a/applications/main/bad_usb/scenes/bad_usb_scene_config_layout.c b/applications/main/bad_usb/scenes/bad_usb_scene_config_layout.c index 3f01d7090..80ab44ab3 100644 --- a/applications/main/bad_usb/scenes/bad_usb_scene_config_layout.c +++ b/applications/main/bad_usb/scenes/bad_usb_scene_config_layout.c @@ -29,21 +29,17 @@ static bool bad_usb_layout_select(BadUsbApp* bad_usb) { void bad_usb_scene_config_layout_on_enter(void* context) { BadUsbApp* bad_usb = context; - if(bad_usb_layout_select(bad_usb)) { - scene_manager_search_and_switch_to_previous_scene(bad_usb->scene_manager, BadUsbSceneWork); - } else { - scene_manager_previous_scene(bad_usb->scene_manager); - } + bad_usb_layout_select(bad_usb); + + scene_manager_previous_scene(bad_usb->scene_manager); } bool bad_usb_scene_config_layout_on_event(void* context, SceneManagerEvent event) { UNUSED(context); UNUSED(event); - // BadUsbApp* bad_usb = context; return false; } void bad_usb_scene_config_layout_on_exit(void* context) { UNUSED(context); - // BadUsbApp* bad_usb = context; } diff --git a/applications/main/bad_usb/scenes/bad_usb_scene_config_usb_name.c b/applications/main/bad_usb/scenes/bad_usb_scene_config_usb_name.c new file mode 100644 index 000000000..d0e136634 --- /dev/null +++ b/applications/main/bad_usb/scenes/bad_usb_scene_config_usb_name.c @@ -0,0 +1,86 @@ +#include "../bad_usb_app_i.h" + +enum TextInputResult { + TextInputResultOk, +}; + +static void bad_usb_scene_config_usb_name_text_input_callback(void* context) { + BadUsbApp* bad_usb = context; + + view_dispatcher_send_custom_event(bad_usb->view_dispatcher, TextInputResultOk); +} + +void bad_usb_scene_config_usb_name_on_enter(void* context) { + BadUsbApp* bad_usb = context; + TextInput* text_input = bad_usb->text_input; + + if(scene_manager_get_scene_state(bad_usb->scene_manager, BadUsbSceneConfigUsbName)) { + strlcpy( + bad_usb->usb_name_buf, + bad_usb->script_hid_cfg.usb.manuf, + sizeof(bad_usb->usb_name_buf)); + text_input_set_header_text(text_input, "Set USB manufacturer name"); + } else { + strlcpy( + bad_usb->usb_name_buf, + bad_usb->script_hid_cfg.usb.product, + sizeof(bad_usb->usb_name_buf)); + text_input_set_header_text(text_input, "Set USB product name"); + } + + text_input_set_result_callback( + text_input, + bad_usb_scene_config_usb_name_text_input_callback, + bad_usb, + bad_usb->usb_name_buf, + sizeof(bad_usb->usb_name_buf), + true); + + view_dispatcher_switch_to_view(bad_usb->view_dispatcher, BadUsbAppViewTextInput); +} + +bool bad_usb_scene_config_usb_name_on_event(void* context, SceneManagerEvent event) { + BadUsbApp* bad_usb = context; + bool consumed = false; + + if(event.type == SceneManagerEventTypeCustom) { + consumed = true; + if(event.event == TextInputResultOk) { + const BadUsbHidApi* hid = bad_usb_hid_get_interface(bad_usb->interface); + if(scene_manager_get_scene_state(bad_usb->scene_manager, BadUsbSceneConfigUsbName)) { + // Apply to current script config + strlcpy( + bad_usb->script_hid_cfg.usb.manuf, + bad_usb->usb_name_buf, + sizeof(bad_usb->script_hid_cfg.usb.manuf)); + hid->adjust_config(&bad_usb->script_hid_cfg); + // Set in user config to save in settings file + strlcpy( + bad_usb->user_hid_cfg.usb.manuf, + bad_usb->usb_name_buf, + sizeof(bad_usb->user_hid_cfg.usb.manuf)); + } else { + // Apply to current script config + strlcpy( + bad_usb->script_hid_cfg.usb.product, + bad_usb->usb_name_buf, + sizeof(bad_usb->script_hid_cfg.usb.product)); + hid->adjust_config(&bad_usb->script_hid_cfg); + // Set in user config to save in settings file + strlcpy( + bad_usb->user_hid_cfg.usb.product, + bad_usb->usb_name_buf, + sizeof(bad_usb->user_hid_cfg.usb.product)); + } + } + scene_manager_previous_scene(bad_usb->scene_manager); + } + return consumed; +} + +void bad_usb_scene_config_usb_name_on_exit(void* context) { + BadUsbApp* bad_usb = context; + TextInput* text_input = bad_usb->text_input; + + text_input_reset(text_input); +} diff --git a/applications/main/bad_usb/scenes/bad_usb_scene_config_usb_vidpid.c b/applications/main/bad_usb/scenes/bad_usb_scene_config_usb_vidpid.c new file mode 100644 index 000000000..ce0c51a47 --- /dev/null +++ b/applications/main/bad_usb/scenes/bad_usb_scene_config_usb_vidpid.c @@ -0,0 +1,59 @@ +#include "../bad_usb_app_i.h" + +enum ByteInputResult { + ByteInputResultOk, +}; + +void bad_usb_scene_config_usb_vidpid_byte_input_callback(void* context) { + BadUsbApp* bad_usb = context; + + view_dispatcher_send_custom_event(bad_usb->view_dispatcher, ByteInputResultOk); +} + +void bad_usb_scene_config_usb_vidpid_on_enter(void* context) { + BadUsbApp* bad_usb = context; + ByteInput* byte_input = bad_usb->byte_input; + + bad_usb->usb_vidpid_buf[0] = __builtin_bswap16(bad_usb->script_hid_cfg.usb.vid); + bad_usb->usb_vidpid_buf[1] = __builtin_bswap16(bad_usb->script_hid_cfg.usb.pid); + byte_input_set_header_text(byte_input, "Set USB VID:PID"); + + byte_input_set_result_callback( + byte_input, + bad_usb_scene_config_usb_vidpid_byte_input_callback, + NULL, + bad_usb, + (void*)bad_usb->usb_vidpid_buf, + sizeof(bad_usb->usb_vidpid_buf)); + + view_dispatcher_switch_to_view(bad_usb->view_dispatcher, BadUsbAppViewByteInput); +} + +bool bad_usb_scene_config_usb_vidpid_on_event(void* context, SceneManagerEvent event) { + BadUsbApp* bad_usb = context; + bool consumed = false; + + if(event.type == SceneManagerEventTypeCustom) { + consumed = true; + if(event.event == ByteInputResultOk) { + const BadUsbHidApi* hid = bad_usb_hid_get_interface(bad_usb->interface); + // Apply to current script config + bad_usb->script_hid_cfg.usb.vid = __builtin_bswap16(bad_usb->usb_vidpid_buf[0]); + bad_usb->script_hid_cfg.usb.pid = __builtin_bswap16(bad_usb->usb_vidpid_buf[1]); + hid->adjust_config(&bad_usb->script_hid_cfg); + // Set in user config to save in settings file + bad_usb->user_hid_cfg.usb.vid = bad_usb->script_hid_cfg.usb.vid; + bad_usb->user_hid_cfg.usb.pid = bad_usb->script_hid_cfg.usb.pid; + } + scene_manager_previous_scene(bad_usb->scene_manager); + } + return consumed; +} + +void bad_usb_scene_config_usb_vidpid_on_exit(void* context) { + BadUsbApp* bad_usb = context; + ByteInput* byte_input = bad_usb->byte_input; + + byte_input_set_result_callback(byte_input, NULL, NULL, NULL, NULL, 0); + byte_input_set_header_text(byte_input, ""); +} diff --git a/applications/main/bad_usb/scenes/bad_usb_scene_confirm_unpair.c b/applications/main/bad_usb/scenes/bad_usb_scene_confirm_unpair.c index b8fd993e2..cd600386c 100644 --- a/applications/main/bad_usb/scenes/bad_usb_scene_confirm_unpair.c +++ b/applications/main/bad_usb/scenes/bad_usb_scene_confirm_unpair.c @@ -36,7 +36,8 @@ bool bad_usb_scene_confirm_unpair_on_event(void* context, SceneManagerEvent even if(event.type == SceneManagerEventTypeCustom) { consumed = true; if(event.event == GuiButtonTypeRight) { - scene_manager_next_scene(scene_manager, BadUsbSceneUnpairDone); + bad_usb_hid_ble_remove_pairing(); + scene_manager_next_scene(scene_manager, BadUsbSceneDone); } else if(event.event == GuiButtonTypeLeft) { scene_manager_previous_scene(scene_manager); } diff --git a/applications/main/bad_usb/scenes/bad_usb_scene_unpair_done.c b/applications/main/bad_usb/scenes/bad_usb_scene_done.c similarity index 67% rename from applications/main/bad_usb/scenes/bad_usb_scene_unpair_done.c rename to applications/main/bad_usb/scenes/bad_usb_scene_done.c index 9583f9bfb..9a878d889 100644 --- a/applications/main/bad_usb/scenes/bad_usb_scene_unpair_done.c +++ b/applications/main/bad_usb/scenes/bad_usb_scene_done.c @@ -1,19 +1,17 @@ #include "../bad_usb_app_i.h" -static void bad_usb_scene_unpair_done_popup_callback(void* context) { +static void bad_usb_scene_done_popup_callback(void* context) { BadUsbApp* bad_usb = context; scene_manager_search_and_switch_to_previous_scene(bad_usb->scene_manager, BadUsbSceneConfig); } -void bad_usb_scene_unpair_done_on_enter(void* context) { +void bad_usb_scene_done_on_enter(void* context) { BadUsbApp* bad_usb = context; Popup* popup = bad_usb->popup; - bad_usb_hid_ble_remove_pairing(); - popup_set_icon(popup, 48, 4, &I_DolphinDone_80x58); popup_set_header(popup, "Done", 20, 19, AlignLeft, AlignBottom); - popup_set_callback(popup, bad_usb_scene_unpair_done_popup_callback); + popup_set_callback(popup, bad_usb_scene_done_popup_callback); popup_set_context(popup, bad_usb); popup_set_timeout(popup, 1500); popup_enable_timeout(popup); @@ -21,7 +19,7 @@ void bad_usb_scene_unpair_done_on_enter(void* context) { view_dispatcher_switch_to_view(bad_usb->view_dispatcher, BadUsbAppViewPopup); } -bool bad_usb_scene_unpair_done_on_event(void* context, SceneManagerEvent event) { +bool bad_usb_scene_done_on_event(void* context, SceneManagerEvent event) { BadUsbApp* bad_usb = context; UNUSED(bad_usb); UNUSED(event); @@ -30,7 +28,7 @@ bool bad_usb_scene_unpair_done_on_event(void* context, SceneManagerEvent event) return consumed; } -void bad_usb_scene_unpair_done_on_exit(void* context) { +void bad_usb_scene_done_on_exit(void* context) { BadUsbApp* bad_usb = context; Popup* popup = bad_usb->popup; UNUSED(popup); diff --git a/applications/main/bad_usb/scenes/bad_usb_scene_file_select.c b/applications/main/bad_usb/scenes/bad_usb_scene_file_select.c index 5e2c3f14b..9aa6f3eea 100644 --- a/applications/main/bad_usb/scenes/bad_usb_scene_file_select.c +++ b/applications/main/bad_usb/scenes/bad_usb_scene_file_select.c @@ -1,5 +1,4 @@ #include "../bad_usb_app_i.h" -#include #include static bool bad_usb_file_select(BadUsbApp* bad_usb) { @@ -27,6 +26,7 @@ void bad_usb_scene_file_select_on_enter(void* context) { } if(bad_usb_file_select(bad_usb)) { + scene_manager_set_scene_state(bad_usb->scene_manager, BadUsbSceneWork, true); scene_manager_next_scene(bad_usb->scene_manager, BadUsbSceneWork); } else { view_dispatcher_stop(bad_usb->view_dispatcher); @@ -36,11 +36,9 @@ void bad_usb_scene_file_select_on_enter(void* context) { bool bad_usb_scene_file_select_on_event(void* context, SceneManagerEvent event) { UNUSED(context); UNUSED(event); - // BadUsbApp* bad_usb = context; return false; } void bad_usb_scene_file_select_on_exit(void* context) { UNUSED(context); - // BadUsbApp* bad_usb = context; } diff --git a/applications/main/bad_usb/scenes/bad_usb_scene_work.c b/applications/main/bad_usb/scenes/bad_usb_scene_work.c index 0382b0f8e..d57f7eb33 100644 --- a/applications/main/bad_usb/scenes/bad_usb_scene_work.c +++ b/applications/main/bad_usb/scenes/bad_usb_scene_work.c @@ -20,11 +20,8 @@ bool bad_usb_scene_work_on_event(void* context, SceneManagerEvent event) { bad_usb_script_close(app->bad_usb_script); app->bad_usb_script = NULL; - if(app->interface == BadUsbHidInterfaceBle) { - scene_manager_next_scene(app->scene_manager, BadUsbSceneConfig); - } else { - scene_manager_next_scene(app->scene_manager, BadUsbSceneConfigLayout); - } + scene_manager_set_scene_state(app->scene_manager, BadUsbSceneConfig, 0); + scene_manager_next_scene(app->scene_manager, BadUsbSceneConfig); } consumed = true; } else if(event.event == InputKeyOk) { @@ -37,7 +34,9 @@ bool bad_usb_scene_work_on_event(void* context, SceneManagerEvent event) { app->interface == BadUsbHidInterfaceBle ? BadUsbHidInterfaceUsb : BadUsbHidInterfaceBle); bad_usb_script_close(app->bad_usb_script); - app->bad_usb_script = bad_usb_script_open(app->file_path, app->interface); + app->bad_usb_script = bad_usb_script_open( + app->file_path, &app->interface, &app->script_hid_cfg, false); + bad_usb_script_set_keyboard_layout(app->bad_usb_script, app->keyboard_layout); } else { bad_usb_script_pause_resume(app->bad_usb_script); } @@ -45,6 +44,7 @@ bool bad_usb_scene_work_on_event(void* context, SceneManagerEvent event) { } } else if(event.type == SceneManagerEventTypeTick) { bad_usb_view_set_state(app->bad_usb_view, bad_usb_script_get_state(app->bad_usb_script)); + bad_usb_view_set_interface(app->bad_usb_view, app->interface); } return consumed; } @@ -54,7 +54,18 @@ void bad_usb_scene_work_on_enter(void* context) { bad_usb_view_set_interface(app->bad_usb_view, app->interface); - app->bad_usb_script = bad_usb_script_open(app->file_path, app->interface); + // Opening script the first time: + // - copy user settings as base config + // - load ID/BLE_ID/BT_ID config if present + // Then disable this until next script selected so user can customize options + bool first_script_load = scene_manager_get_scene_state(app->scene_manager, BadUsbSceneWork); + if(first_script_load) { + memcpy(&app->script_hid_cfg, &app->user_hid_cfg, sizeof(app->script_hid_cfg)); + scene_manager_set_scene_state(app->scene_manager, BadUsbSceneWork, false); + } + // Interface and config are passed as pointers as ID/BLE_ID/BT_ID config can modify them + app->bad_usb_script = bad_usb_script_open( + app->file_path, &app->interface, &app->script_hid_cfg, first_script_load); bad_usb_script_set_keyboard_layout(app->bad_usb_script, app->keyboard_layout); FuriString* file_name; diff --git a/applications/main/bad_usb/views/bad_usb_view.c b/applications/main/bad_usb/views/bad_usb_view.c index 1a6f77958..4032ea974 100644 --- a/applications/main/bad_usb/views/bad_usb_view.c +++ b/applications/main/bad_usb/views/bad_usb_view.c @@ -24,8 +24,7 @@ typedef struct { static void bad_usb_draw_callback(Canvas* canvas, void* _model) { BadUsbModel* model = _model; - FuriString* disp_str; - disp_str = furi_string_alloc_set(model->file_name); + FuriString* disp_str = furi_string_alloc_set(model->file_name); elements_string_fit_width(canvas, disp_str, 128 - 2); canvas_set_font(canvas, FontSecondary); canvas_draw_str(canvas, 2, 8, furi_string_get_cstr(disp_str)); @@ -35,6 +34,8 @@ static void bad_usb_draw_callback(Canvas* canvas, void* _model) { } else { furi_string_printf(disp_str, "(%s)", model->layout); } + uint32_t e = model->state.elapsed; + furi_string_cat_printf(disp_str, " %02lu:%02lu.%ld", e / 60 / 1000, e / 1000, e % 1000); elements_string_fit_width(canvas, disp_str, 128 - 2); canvas_draw_str( canvas, 2, 8 + canvas_current_font_height(canvas), furi_string_get_cstr(disp_str)); @@ -52,13 +53,8 @@ static void bad_usb_draw_callback(Canvas* canvas, void* _model) { if((state == BadUsbStateIdle) || (state == BadUsbStateDone) || (state == BadUsbStateNotConnected)) { elements_button_center(canvas, "Run"); - if(model->interface == BadUsbHidInterfaceBle) { - elements_button_right(canvas, "USB"); - elements_button_left(canvas, "Config"); - } else { - elements_button_right(canvas, "BLE"); - elements_button_left(canvas, "Layout"); - } + elements_button_left(canvas, "Config"); + elements_button_right(canvas, model->interface == BadUsbHidInterfaceBle ? "USB" : "BLE"); } else if((state == BadUsbStateRunning) || (state == BadUsbStateDelay)) { elements_button_center(canvas, "Stop"); if(!model->pause_wait) { @@ -90,77 +86,85 @@ static void bad_usb_draw_callback(Canvas* canvas, void* _model) { canvas_draw_str_aligned(canvas, 127, 43, AlignRight, AlignBottom, "ERROR"); } else if(state == BadUsbStateScriptError) { canvas_draw_icon(canvas, 4, 26, &I_Error_18x18); - canvas_set_font(canvas, FontPrimary); - canvas_draw_str_aligned(canvas, 127, 33, AlignRight, AlignBottom, "ERROR:"); - canvas_set_font(canvas, FontSecondary); furi_string_printf(disp_str, "line %zu", model->state.error_line); canvas_draw_str_aligned( canvas, 127, 46, AlignRight, AlignBottom, furi_string_get_cstr(disp_str)); - furi_string_reset(disp_str); - furi_string_set_str(disp_str, model->state.error); elements_string_fit_width(canvas, disp_str, canvas_width(canvas)); canvas_draw_str_aligned( canvas, 127, 56, AlignRight, AlignBottom, furi_string_get_cstr(disp_str)); - furi_string_reset(disp_str); + canvas_set_font(canvas, FontPrimary); + canvas_draw_str_aligned(canvas, 127, 33, AlignRight, AlignBottom, "ERROR:"); } else if(state == BadUsbStateIdle) { canvas_draw_icon(canvas, 4, 26, &I_Smile_18x18); + furi_string_printf(disp_str, "0/%zu", model->state.line_nb); + canvas_draw_str_aligned( + canvas, 124, 47, AlignRight, AlignBottom, furi_string_get_cstr(disp_str)); canvas_set_font(canvas, FontBigNumbers); - canvas_draw_str_aligned(canvas, 114, 40, AlignRight, AlignBottom, "0"); - canvas_draw_icon(canvas, 117, 26, &I_Percent_10x14); + canvas_draw_str_aligned(canvas, 112, 37, AlignRight, AlignBottom, "0"); + canvas_draw_icon(canvas, 115, 23, &I_Percent_10x14); } else if(state == BadUsbStateRunning) { if(model->anim_frame == 0) { canvas_draw_icon(canvas, 4, 23, &I_EviSmile1_18x21); } else { canvas_draw_icon(canvas, 4, 23, &I_EviSmile2_18x21); } + furi_string_printf(disp_str, "%zu/%zu", model->state.line_cur, model->state.line_nb); + canvas_draw_str_aligned( + canvas, 124, 47, AlignRight, AlignBottom, furi_string_get_cstr(disp_str)); canvas_set_font(canvas, FontBigNumbers); furi_string_printf( disp_str, "%zu", ((model->state.line_cur - 1) * 100) / model->state.line_nb); canvas_draw_str_aligned( - canvas, 114, 40, AlignRight, AlignBottom, furi_string_get_cstr(disp_str)); - furi_string_reset(disp_str); - canvas_draw_icon(canvas, 117, 26, &I_Percent_10x14); + canvas, 112, 37, AlignRight, AlignBottom, furi_string_get_cstr(disp_str)); + canvas_draw_icon(canvas, 115, 23, &I_Percent_10x14); } else if(state == BadUsbStateDone) { canvas_draw_icon(canvas, 4, 23, &I_EviSmile1_18x21); + furi_string_printf(disp_str, "%zu/%zu", model->state.line_nb, model->state.line_nb); + canvas_draw_str_aligned( + canvas, 124, 47, AlignRight, AlignBottom, furi_string_get_cstr(disp_str)); canvas_set_font(canvas, FontBigNumbers); - canvas_draw_str_aligned(canvas, 114, 40, AlignRight, AlignBottom, "100"); - furi_string_reset(disp_str); - canvas_draw_icon(canvas, 117, 26, &I_Percent_10x14); + canvas_draw_str_aligned(canvas, 112, 37, AlignRight, AlignBottom, "100"); + canvas_draw_icon(canvas, 115, 23, &I_Percent_10x14); } else if(state == BadUsbStateDelay) { if(model->anim_frame == 0) { canvas_draw_icon(canvas, 4, 23, &I_EviWaiting1_18x21); } else { canvas_draw_icon(canvas, 4, 23, &I_EviWaiting2_18x21); } + uint32_t delay = model->state.delay_remain / 10; + if(delay) { + furi_string_printf(disp_str, "Delay %lus", delay); + canvas_draw_str_aligned( + canvas, 4, 61, AlignLeft, AlignBottom, furi_string_get_cstr(disp_str)); + } + furi_string_printf(disp_str, "%zu/%zu", model->state.line_cur, model->state.line_nb); + canvas_draw_str_aligned( + canvas, 124, 47, AlignRight, AlignBottom, furi_string_get_cstr(disp_str)); canvas_set_font(canvas, FontBigNumbers); furi_string_printf( disp_str, "%zu", ((model->state.line_cur - 1) * 100) / model->state.line_nb); canvas_draw_str_aligned( - canvas, 114, 40, AlignRight, AlignBottom, furi_string_get_cstr(disp_str)); - furi_string_reset(disp_str); - canvas_draw_icon(canvas, 117, 26, &I_Percent_10x14); - canvas_set_font(canvas, FontSecondary); - furi_string_printf(disp_str, "delay %lus", model->state.delay_remain); - canvas_draw_str_aligned( - canvas, 127, 50, AlignRight, AlignBottom, furi_string_get_cstr(disp_str)); - furi_string_reset(disp_str); + canvas, 112, 37, AlignRight, AlignBottom, furi_string_get_cstr(disp_str)); + canvas_draw_icon(canvas, 115, 23, &I_Percent_10x14); } else if((state == BadUsbStatePaused) || (state == BadUsbStateWaitForBtn)) { if(model->anim_frame == 0) { canvas_draw_icon(canvas, 4, 23, &I_EviWaiting1_18x21); } else { canvas_draw_icon(canvas, 4, 23, &I_EviWaiting2_18x21); } + if(state != BadUsbStateWaitForBtn) { + canvas_draw_str_aligned(canvas, 4, 61, AlignLeft, AlignBottom, "Paused"); + } + furi_string_printf(disp_str, "%zu/%zu", model->state.line_cur, model->state.line_nb); + canvas_draw_str_aligned( + canvas, 124, 47, AlignRight, AlignBottom, furi_string_get_cstr(disp_str)); canvas_set_font(canvas, FontBigNumbers); furi_string_printf( disp_str, "%zu", ((model->state.line_cur - 1) * 100) / model->state.line_nb); canvas_draw_str_aligned( - canvas, 114, 40, AlignRight, AlignBottom, furi_string_get_cstr(disp_str)); - furi_string_reset(disp_str); - canvas_draw_icon(canvas, 117, 26, &I_Percent_10x14); - canvas_set_font(canvas, FontSecondary); - canvas_draw_str_aligned(canvas, 127, 50, AlignRight, AlignBottom, "Paused"); - furi_string_reset(disp_str); + canvas, 112, 37, AlignRight, AlignBottom, furi_string_get_cstr(disp_str)); + canvas_draw_icon(canvas, 115, 23, &I_Percent_10x14); } else { canvas_draw_icon(canvas, 4, 26, &I_Clock_18x18); } diff --git a/lib/ble_profile/extra_profiles/hid_profile.c b/lib/ble_profile/extra_profiles/hid_profile.c index ea90d3114..ed67f44a1 100644 --- a/lib/ble_profile/extra_profiles/hid_profile.c +++ b/lib/ble_profile/extra_profiles/hid_profile.c @@ -9,12 +9,12 @@ #include #include -#define HID_INFO_BASE_USB_SPECIFICATION (0x0101) -#define HID_INFO_COUNTRY_CODE (0x00) -#define BLE_PROFILE_HID_INFO_FLAG_REMOTE_WAKE_MSK (0x01) +#define HID_INFO_BASE_USB_SPECIFICATION (0x0101) +#define HID_INFO_COUNTRY_CODE (0x00) +#define BLE_PROFILE_HID_INFO_FLAG_REMOTE_WAKE_MSK (0x01) #define BLE_PROFILE_HID_INFO_FLAG_NORMALLY_CONNECTABLE_MSK (0x02) -#define BLE_PROFILE_HID_KB_MAX_KEYS (6) +#define BLE_PROFILE_HID_KB_MAX_KEYS (6) #define BLE_PROFILE_CONSUMER_MAX_KEYS (1) // Report ids cant be 0 @@ -380,10 +380,11 @@ bool ble_profile_hid_mouse_scroll(FuriHalBleProfileBase* profile, int8_t delta) #define CONNECTION_INTERVAL_MAX (0x24) static GapConfig template_config = { - .adv_service = { - .UUID_Type = UUID_TYPE_16, - .Service_UUID_16 = HUMAN_INTERFACE_DEVICE_SERVICE_UUID, - }, + .adv_service = + { + .UUID_Type = UUID_TYPE_16, + .Service_UUID_16 = HUMAN_INTERFACE_DEVICE_SERVICE_UUID, + }, .appearance_char = GAP_APPEARANCE_KEYBOARD, .bonding_mode = true, .pairing_method = GapPairingPinCodeVerifyYesNo, @@ -412,19 +413,17 @@ static void ble_profile_hid_get_config(GapConfig* config, FuriHalBleProfileParam } // Set advertise name - memset(config->adv_name, 0, sizeof(config->adv_name)); - FuriString* name = furi_string_alloc_set(furi_hal_version_get_ble_local_device_name_ptr()); - const char* clicker_str = "Control"; if(hid_profile_params && hid_profile_params->device_name_prefix) { clicker_str = hid_profile_params->device_name_prefix; } - furi_string_replace_str(name, "Flipper", clicker_str); - if(furi_string_size(name) >= sizeof(config->adv_name)) { - furi_string_left(name, sizeof(config->adv_name) - 1); - } - memcpy(config->adv_name, furi_string_get_cstr(name), furi_string_size(name)); - furi_string_free(name); + snprintf( + config->adv_name, + sizeof(config->adv_name), + "%c%s %s", + furi_hal_version_get_ble_local_device_name_ptr()[0], + clicker_str, + furi_hal_version_get_name_ptr()); } static const FuriHalBleProfileTemplate profile_callbacks = { diff --git a/targets/f7/furi_hal/furi_hal_usb_hid.c b/targets/f7/furi_hal/furi_hal_usb_hid.c index c83261226..04f2ae400 100644 --- a/targets/f7/furi_hal/furi_hal_usb_hid.c +++ b/targets/f7/furi_hal/furi_hal_usb_hid.c @@ -12,9 +12,6 @@ #define HID_INTERVAL 2 -#define HID_VID_DEFAULT 0x046D -#define HID_PID_DEFAULT 0xC529 - struct HidIntfDescriptor { struct usb_interface_descriptor hid; struct usb_hid_descriptor hid_desc; diff --git a/targets/furi_hal_include/furi_hal_usb_hid.h b/targets/furi_hal_include/furi_hal_usb_hid.h index af4a542de..19c9ed659 100644 --- a/targets/furi_hal_include/furi_hal_usb_hid.h +++ b/targets/furi_hal_include/furi_hal_usb_hid.h @@ -9,6 +9,11 @@ extern "C" { #endif +#define HID_MANUF_PRODUCT_NAME_LEN 32 + +#define HID_VID_DEFAULT 0x046D +#define HID_PID_DEFAULT 0xC529 + /** Max number of simultaneously pressed keys (keyboard) */ #define HID_KB_MAX_KEYS 6 /** Max number of simultaneously pressed keys (consumer control) */ @@ -166,10 +171,11 @@ static const uint16_t hid_asciimap[] = { }; typedef struct { + // Note: vid/pid should be uint16_t and are treated as such uint32_t vid; uint32_t pid; - char manuf[32]; - char product[32]; + char manuf[HID_MANUF_PRODUCT_NAME_LEN]; + char product[HID_MANUF_PRODUCT_NAME_LEN]; } FuriHalUsbHidConfig; typedef void (*HidStateCallback)(bool state, void* context); diff --git a/targets/furi_hal_include/furi_hal_version.h b/targets/furi_hal_include/furi_hal_version.h index 2c098d482..1d026539a 100644 --- a/targets/furi_hal_include/furi_hal_version.h +++ b/targets/furi_hal_include/furi_hal_version.h @@ -14,11 +14,12 @@ extern "C" { #endif -#define FURI_HAL_VERSION_NAME_LENGTH 8 -#define FURI_HAL_VERSION_ARRAY_NAME_LENGTH (FURI_HAL_VERSION_NAME_LENGTH + 1) -#define FURI_HAL_BT_ADV_NAME_LENGTH (18 + 1) // 18 characters + null terminator -#define FURI_HAL_VERSION_DEVICE_NAME_LENGTH \ - (1 + FURI_HAL_BT_ADV_NAME_LENGTH) // Used for custom BT name, BLE symbol + name +#define FURI_HAL_VERSION_NAME_LENGTH (8) +#define FURI_HAL_VERSION_ARRAY_NAME_LENGTH (FURI_HAL_VERSION_NAME_LENGTH + 1) +/** 31b BLE Adv - 3b flags - 2b name prefix - 4b service uuid - 3b tx power = 19, + 1b null terminator (not present in packet) */ +#define FURI_HAL_BT_ADV_NAME_LENGTH (20) +/** BLE symbol + name */ +#define FURI_HAL_VERSION_DEVICE_NAME_LENGTH (1 + FURI_HAL_BT_ADV_NAME_LENGTH) /** OTP Versions enum */ typedef enum { From 831e46c052a9256fd3baa2109c3f43e6b0106a0c Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Sun, 6 Apr 2025 06:18:30 +0300 Subject: [PATCH 198/268] upd changelog --- CHANGELOG.md | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 618589cf0..e98d1eeb6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,5 @@ ## Main changes -- Current API: 83.0 +- Current API: 85.0 * SubGHz: Add **Revers RB2 / RB2M Protocol** (static 64 bit) **full support** with add manually (by @xMasterX) * SubGHz: **Fix Hollarm protocol with more verification** * SubGHz: **Fix GangQi protocol** (by @DoberBit and @mishamyte (who spent 2 weeks on this)) @@ -9,6 +9,7 @@ * OFW: LFRFID - **EM4305 support** * OFW: **Universal IR signal selection** * OFW: **BadUSB: Mouse control** +* OFW PR 4136: BadUSB: Full USB/BLE parameter customization, UI improvements, and more (by @Willy-JL) * OFW: NFC - Added naming for DESFire cards + fix MF3ICD40 cards unable to be read * Apps: Add **FindMyFlipper to system apps and allow autostart** on system boot [app by @MatthewKuKanich](https://github.com/MatthewKuKanich/FindMyFlipper) and autoloader by @Willy-JL - to use app please check how to add keys in [app repo](https://github.com/MatthewKuKanich/FindMyFlipper) * README Update: Enhanced Visuals & Navigation (PR #871 #872 | by @m-xim) @@ -20,6 +21,19 @@ * SubGHz: Various bugfixes and experimental options (rolling counter overflow) (by @xMasterX) * Anims: Disable winter anims * NFC: mfclassic poller fix early key reuse in dictionary attack state machine (by @noproto) +* OFW: BadUSB arbitrary key combinations +* OFW: JS: Update and fix docs, fix Number.toString() with decimals +* OFW: New JS value destructuring +* OFW: Docs: Fix doxygen references from PR 4168 +* OFW: BLE advertising improvements +* OFW: **New CLI architecture** +* OFW: **CLI autocomplete and other sugar** +* OFW: CLI commands in fals and threads +* OFW: cli: fixed `free_blocks` command +* OFW: docs: badusb arbitrary modkey chains +* OFW: Separate cli_shell into toolbox +* OFW: Move JS modules to new arg parser +* OFW: Application chaining * OFW: Fix DWARF dead code elimination and linking * OFW: NFC: Fix crash on ISO15693-3 save when memory is empty or cannot be read * OFW: Reduced ieee754 parser size From f07048d1b095c39d978fb1e43056c2b511839e64 Mon Sep 17 00:00:00 2001 From: RebornedBrain Date: Mon, 7 Apr 2025 00:49:22 +0300 Subject: [PATCH 199/268] [FL-3902] NFC app now can launch MFKey32 (#4117) * feat: app chaining * add `launch_current_app_after_deferred`, remove `get_referring_application` * fix naming * new api * fix f18 * Added new function which enqueues external app from nfc app * Added path to MFKey32 app * Button "Crack nonces in MFKey32" added to ReadMenu scene * SaveConfirm scene adjusted to move to different scenes depending on state * SaveSuccess scene now moves to different scenes depending on SaveConfirm scene state * MfKeyComplete scene shows different text when MFKey.fap present on the device * Now MfKeyClassic scene can run external app * fix deferred launches after errors Co-authored-by: Anna Antonenko Co-authored-by: Aleksandr Kutuzov Co-authored-by: hedger --- .../protocol_support/mf_classic/mf_classic.c | 20 +++++- applications/main/nfc/nfc_app.c | 20 ++++++ applications/main/nfc/nfc_app_i.h | 10 +++ .../nfc_scene_mf_classic_mfkey_complete.c | 71 ++++++++++++++----- .../main/nfc/scenes/nfc_scene_save_confirm.c | 9 ++- .../main/nfc/scenes/nfc_scene_save_success.c | 8 ++- 6 files changed, 118 insertions(+), 20 deletions(-) diff --git a/applications/main/nfc/helpers/protocol_support/mf_classic/mf_classic.c b/applications/main/nfc/helpers/protocol_support/mf_classic/mf_classic.c index 6f7be7f4c..5c668d530 100644 --- a/applications/main/nfc/helpers/protocol_support/mf_classic/mf_classic.c +++ b/applications/main/nfc/helpers/protocol_support/mf_classic/mf_classic.c @@ -14,7 +14,8 @@ enum { SubmenuIndexDetectReader = SubmenuIndexCommonMax, SubmenuIndexWrite, SubmenuIndexUpdate, - SubmenuIndexDictAttack + SubmenuIndexDictAttack, + SubmenuIndexCrackNonces, }; static void nfc_scene_info_on_enter_mf_classic(NfcApp* instance) { @@ -128,6 +129,13 @@ static void nfc_scene_read_menu_on_enter_mf_classic(NfcApp* instance) { SubmenuIndexDictAttack, nfc_protocol_support_common_submenu_callback, instance); + + submenu_add_item( + submenu, + "Crack nonces in MFKey32", + SubmenuIndexCrackNonces, + nfc_protocol_support_common_submenu_callback, + instance); } } @@ -193,6 +201,11 @@ static bool nfc_scene_read_menu_on_event_mf_classic(NfcApp* instance, SceneManag if(event.type == SceneManagerEventTypeCustom) { if(event.event == SubmenuIndexDetectReader) { + scene_manager_set_scene_state( + instance->scene_manager, + NfcSceneSaveConfirm, + NfcSceneSaveConfirmStateDetectReader); + scene_manager_next_scene(instance->scene_manager, NfcSceneSaveConfirm); dolphin_deed(DolphinDeedNfcDetectReader); consumed = true; @@ -205,6 +218,11 @@ static bool nfc_scene_read_menu_on_event_mf_classic(NfcApp* instance, SceneManag } else if(event.event == SubmenuIndexCommonEdit) { scene_manager_next_scene(instance->scene_manager, NfcSceneSetUid); consumed = true; + } else if(event.event == SubmenuIndexCrackNonces) { + scene_manager_set_scene_state( + instance->scene_manager, NfcSceneSaveConfirm, NfcSceneSaveConfirmStateCrackNonces); + scene_manager_next_scene(instance->scene_manager, NfcSceneSaveConfirm); + consumed = true; } } diff --git a/applications/main/nfc/nfc_app.c b/applications/main/nfc/nfc_app.c index 5365d6bef..7a829c329 100644 --- a/applications/main/nfc/nfc_app.c +++ b/applications/main/nfc/nfc_app.c @@ -470,6 +470,26 @@ static void nfc_show_initial_scene_for_device(NfcApp* nfc) { scene_manager_next_scene(nfc->scene_manager, scene); } +void nfc_app_run_external(NfcApp* nfc, const char* app_path) { + furi_assert(nfc); + furi_assert(app_path); + + Loader* loader = furi_record_open(RECORD_LOADER); + + loader_enqueue_launch(loader, app_path, NULL, LoaderDeferredLaunchFlagGui); + + FuriString* self_path = furi_string_alloc(); + furi_check(loader_get_application_launch_path(loader, self_path)); + + loader_enqueue_launch( + loader, furi_string_get_cstr(self_path), NULL, LoaderDeferredLaunchFlagGui); + furi_string_free(self_path); + + furi_record_close(RECORD_LOADER); + + view_dispatcher_stop(nfc->view_dispatcher); +} + int32_t nfc_app(void* p) { if(!nfc_is_hal_ready()) return 0; diff --git a/applications/main/nfc/nfc_app_i.h b/applications/main/nfc/nfc_app_i.h index 1101b7bb2..920127fef 100644 --- a/applications/main/nfc/nfc_app_i.h +++ b/applications/main/nfc/nfc_app_i.h @@ -35,6 +35,7 @@ #include "helpers/felica_auth.h" #include "helpers/slix_unlock.h" +#include #include #include #include @@ -80,6 +81,8 @@ #define NFC_APP_MF_CLASSIC_DICT_SYSTEM_NESTED_PATH \ (NFC_APP_FOLDER "/assets/mf_classic_dict_nested.nfc") +#define NFC_MFKEY32_APP_PATH (EXT_PATH("apps/NFC/mfkey.fap")) + typedef enum { NfcRpcStateIdle, NfcRpcStateEmulating, @@ -167,6 +170,11 @@ typedef enum { NfcViewDetectReader, } NfcView; +typedef enum { + NfcSceneSaveConfirmStateDetectReader, + NfcSceneSaveConfirmStateCrackNonces, +} NfcSceneSaveConfirmState; + int32_t nfc_task(void* p); void nfc_text_store_set(NfcApp* nfc, const char* text, ...); @@ -202,3 +210,5 @@ bool nfc_save_file(NfcApp* instance, FuriString* path); void nfc_make_app_folder(NfcApp* instance); void nfc_append_filename_string_when_present(NfcApp* instance, FuriString* string); + +void nfc_app_run_external(NfcApp* nfc, const char* app_path); diff --git a/applications/main/nfc/scenes/nfc_scene_mf_classic_mfkey_complete.c b/applications/main/nfc/scenes/nfc_scene_mf_classic_mfkey_complete.c index 660674ceb..3311ef0e0 100644 --- a/applications/main/nfc/scenes/nfc_scene_mf_classic_mfkey_complete.c +++ b/applications/main/nfc/scenes/nfc_scene_mf_classic_mfkey_complete.c @@ -1,4 +1,10 @@ #include "../nfc_app_i.h" +#include "loader/loader.h" + +typedef enum { + NfcSceneMfClassicMfKeyCompleteStateAppMissing, + NfcSceneMfClassicMfKeyCompleteStateAppPresent, +} NfcSceneMfClassicMfKeyCompleteState; void nfc_scene_mf_classic_mfkey_complete_callback( GuiButtonType result, @@ -15,22 +21,47 @@ void nfc_scene_mf_classic_mfkey_complete_on_enter(void* context) { widget_add_string_element( instance->widget, 64, 0, AlignCenter, AlignTop, FontPrimary, "Completed!"); - widget_add_string_multiline_element( - instance->widget, - 64, - 13, - AlignCenter, - AlignTop, - FontSecondary, - "Now use Mfkey32 to extract \nkeys: r.flipper.net/nfc-tools"); - widget_add_icon_element(instance->widget, 50, 39, &I_MFKey_qr_25x25); - widget_add_button_element( - instance->widget, - GuiButtonTypeRight, - "Finish", - nfc_scene_mf_classic_mfkey_complete_callback, - instance); + NfcSceneMfClassicMfKeyCompleteState scene_state = + storage_common_exists(instance->storage, NFC_MFKEY32_APP_PATH) ? + NfcSceneMfClassicMfKeyCompleteStateAppPresent : + NfcSceneMfClassicMfKeyCompleteStateAppMissing; + scene_manager_set_scene_state( + instance->scene_manager, NfcSceneMfClassicMfkeyComplete, scene_state); + + if(scene_state == NfcSceneMfClassicMfKeyCompleteStateAppMissing) { + widget_add_string_multiline_element( + instance->widget, + 64, + 13, + AlignCenter, + AlignTop, + FontSecondary, + "Now use Mfkey32 to extract \nkeys: r.flipper.net/nfc-tools"); + widget_add_icon_element(instance->widget, 50, 39, &I_MFKey_qr_25x25); + widget_add_button_element( + instance->widget, + GuiButtonTypeRight, + "Finish", + nfc_scene_mf_classic_mfkey_complete_callback, + instance); + } else { + widget_add_string_multiline_element( + instance->widget, + 60, + 16, + AlignLeft, + AlignTop, + FontSecondary, + "Now run Mfkey32\n to extract \nkeys"); + widget_add_icon_element(instance->widget, 5, 18, &I_WarningDolphin_45x42); + widget_add_button_element( + instance->widget, + GuiButtonTypeRight, + "Run", + nfc_scene_mf_classic_mfkey_complete_callback, + instance); + } view_dispatcher_switch_to_view(instance->view_dispatcher, NfcViewWidget); } @@ -40,8 +71,14 @@ bool nfc_scene_mf_classic_mfkey_complete_on_event(void* context, SceneManagerEve if(event.type == SceneManagerEventTypeCustom) { if(event.event == GuiButtonTypeRight) { - consumed = scene_manager_search_and_switch_to_previous_scene( - instance->scene_manager, NfcSceneStart); + NfcSceneMfClassicMfKeyCompleteState scene_state = scene_manager_get_scene_state( + instance->scene_manager, NfcSceneMfClassicMfkeyComplete); + if(scene_state == NfcSceneMfClassicMfKeyCompleteStateAppMissing) { + consumed = scene_manager_search_and_switch_to_previous_scene( + instance->scene_manager, NfcSceneStart); + } else { + nfc_app_run_external(instance, NFC_MFKEY32_APP_PATH); + } } } else if(event.type == SceneManagerEventTypeBack) { const uint32_t prev_scenes[] = {NfcSceneSavedMenu, NfcSceneStart}; diff --git a/applications/main/nfc/scenes/nfc_scene_save_confirm.c b/applications/main/nfc/scenes/nfc_scene_save_confirm.c index 9d0a206d3..d8fb20642 100644 --- a/applications/main/nfc/scenes/nfc_scene_save_confirm.c +++ b/applications/main/nfc/scenes/nfc_scene_save_confirm.c @@ -29,7 +29,14 @@ bool nfc_scene_save_confirm_on_event(void* context, SceneManagerEvent event) { scene_manager_next_scene(nfc->scene_manager, NfcSceneSaveName); consumed = true; } else if(event.event == DialogExResultLeft) { - scene_manager_next_scene(nfc->scene_manager, NfcSceneMfClassicDetectReader); + NfcSceneSaveConfirmState scene_state = + scene_manager_get_scene_state(nfc->scene_manager, NfcSceneSaveConfirm); + + NfcScene scene = scene_state == NfcSceneSaveConfirmStateCrackNonces ? + NfcSceneMfClassicMfkeyComplete : + NfcSceneMfClassicDetectReader; + + scene_manager_next_scene(nfc->scene_manager, scene); consumed = true; } } diff --git a/applications/main/nfc/scenes/nfc_scene_save_success.c b/applications/main/nfc/scenes/nfc_scene_save_success.c index 1c76b3f87..5f812ba9c 100644 --- a/applications/main/nfc/scenes/nfc_scene_save_success.c +++ b/applications/main/nfc/scenes/nfc_scene_save_success.c @@ -29,7 +29,13 @@ bool nfc_scene_save_success_on_event(void* context, SceneManagerEvent event) { consumed = scene_manager_search_and_switch_to_previous_scene( nfc->scene_manager, NfcSceneMfClassicKeys); } else if(scene_manager_has_previous_scene(nfc->scene_manager, NfcSceneSaveConfirm)) { - scene_manager_next_scene(nfc->scene_manager, NfcSceneMfClassicDetectReader); + NfcSceneSaveConfirmState scene_state = + scene_manager_get_scene_state(nfc->scene_manager, NfcSceneSaveConfirm); + + NfcScene scene = scene_state == NfcSceneSaveConfirmStateCrackNonces ? + NfcSceneMfClassicMfkeyComplete : + NfcSceneMfClassicDetectReader; + scene_manager_next_scene(nfc->scene_manager, scene); consumed = true; } else if(scene_manager_has_previous_scene(nfc->scene_manager, NfcSceneSetType)) { consumed = scene_manager_search_and_switch_to_another_scene( From ada8473932d804e17049fa05468498c9e9425c67 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Mon, 7 Apr 2025 01:06:14 +0300 Subject: [PATCH 200/268] upd changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e98d1eeb6..7a7ca55f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ * SubGHz: Various bugfixes and experimental options (rolling counter overflow) (by @xMasterX) * Anims: Disable winter anims * NFC: mfclassic poller fix early key reuse in dictionary attack state machine (by @noproto) +* OFW: NFC app now can launch MFKey32 * OFW: BadUSB arbitrary key combinations * OFW: JS: Update and fix docs, fix Number.toString() with decimals * OFW: New JS value destructuring From eb0f5ef8c01bc20aa99d9728bb0f6994141167d4 Mon Sep 17 00:00:00 2001 From: Anna Antonenko Date: Mon, 7 Apr 2025 03:17:58 +0400 Subject: [PATCH 201/268] [FL-3947] Pinning of settings options (#4077) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: pinning settings in favorites * include archive in unit tests fw * change settings icon * update text with suggestions from the ui team * Small touch of constness --------- Co-authored-by: あく Co-authored-by: hedger Co-authored-by: hedger --- .../main/archive/helpers/archive_apps.c | 10 +- .../main/archive/helpers/archive_apps.h | 2 + .../main/archive/helpers/archive_browser.h | 5 +- .../main/archive/helpers/archive_favorites.c | 46 ++++++- .../main/archive/helpers/archive_favorites.h | 10 ++ .../main/archive/helpers/archive_files.h | 1 + .../archive/scenes/archive_scene_browser.c | 39 ++++-- .../main/archive/views/archive_browser_view.c | 1 + applications/services/gui/modules/submenu.c | 50 ++++++-- applications/services/gui/modules/submenu.h | 17 +++ applications/services/loader/loader_menu.c | 15 ++- .../power_settings_app/power_settings_app.c | 38 +++++- .../power_settings_app/power_settings_app.h | 3 + .../scenes/power_settings_scene_start.c | 54 +------- .../scenes/storage_settings_scene_start.c | 121 +----------------- .../storage_settings/storage_settings.c | 35 ++++- .../storage_settings/storage_settings.h | 8 ++ assets/icons/Archive/settings_10px.png | Bin 0 -> 96 bytes fbt_options.py | 1 + lib/toolbox/settings_helpers/submenu_based.c | 103 +++++++++++++++ lib/toolbox/settings_helpers/submenu_based.h | 89 +++++++++++++ targets/f18/api_symbols.csv | 1 + targets/f7/api_symbols.csv | 1 + 23 files changed, 447 insertions(+), 203 deletions(-) create mode 100644 assets/icons/Archive/settings_10px.png create mode 100644 lib/toolbox/settings_helpers/submenu_based.c create mode 100644 lib/toolbox/settings_helpers/submenu_based.h diff --git a/applications/main/archive/helpers/archive_apps.c b/applications/main/archive/helpers/archive_apps.c index 7aca29364..65d5a5af9 100644 --- a/applications/main/archive/helpers/archive_apps.c +++ b/applications/main/archive/helpers/archive_apps.c @@ -1,8 +1,9 @@ #include "archive_apps.h" #include "archive_browser.h" -static const char* known_apps[] = { +static const char* const known_apps[] = { [ArchiveAppTypeU2f] = "u2f", + [ArchiveAppTypeSetting] = "setting", }; ArchiveAppTypeEnum archive_get_app_type(const char* path) { @@ -36,6 +37,8 @@ bool archive_app_is_available(void* context, const char* path) { furi_record_close(RECORD_STORAGE); return file_exists; + } else if(app == ArchiveAppTypeSetting) { + return true; } else { return false; } @@ -53,6 +56,9 @@ bool archive_app_read_dir(void* context, const char* path) { if(app == ArchiveAppTypeU2f) { archive_add_app_item(browser, "/app:u2f/U2F Token"); return true; + } else if(app == ArchiveAppTypeSetting) { + archive_add_app_item(browser, path); + return true; } else { return false; } @@ -75,6 +81,8 @@ void archive_app_delete_file(void* context, const char* path) { if(archive_is_favorite("/app:u2f/U2F Token")) { archive_favorites_delete("/app:u2f/U2F Token"); } + } else if(app == ArchiveAppTypeSetting) { + // can't delete a setting! } if(res) { diff --git a/applications/main/archive/helpers/archive_apps.h b/applications/main/archive/helpers/archive_apps.h index d9d1dec34..f5d050387 100644 --- a/applications/main/archive/helpers/archive_apps.h +++ b/applications/main/archive/helpers/archive_apps.h @@ -4,12 +4,14 @@ typedef enum { ArchiveAppTypeU2f, + ArchiveAppTypeSetting, ArchiveAppTypeUnknown, ArchiveAppsTotal, } ArchiveAppTypeEnum; static const ArchiveFileTypeEnum app_file_types[] = { [ArchiveAppTypeU2f] = ArchiveFileTypeU2f, + [ArchiveAppTypeSetting] = ArchiveFileTypeSetting, [ArchiveAppTypeUnknown] = ArchiveFileTypeUnknown, }; diff --git a/applications/main/archive/helpers/archive_browser.h b/applications/main/archive/helpers/archive_browser.h index fea6ddf7f..b5dc37b1d 100644 --- a/applications/main/archive/helpers/archive_browser.h +++ b/applications/main/archive/helpers/archive_browser.h @@ -7,7 +7,7 @@ #define TAB_DEFAULT ArchiveTabFavorites // Start tab #define FILE_LIST_BUF_LEN 50 -static const char* tab_default_paths[] = { +static const char* const tab_default_paths[] = { [ArchiveTabFavorites] = "/app:favorites", [ArchiveTabIButton] = EXT_PATH("ibutton"), [ArchiveTabNFC] = EXT_PATH("nfc"), @@ -20,7 +20,7 @@ static const char* tab_default_paths[] = { [ArchiveTabBrowser] = STORAGE_EXT_PATH_PREFIX, }; -static const char* known_ext[] = { +static const char* const known_ext[] = { [ArchiveFileTypeIButton] = ".ibtn", [ArchiveFileTypeNFC] = ".nfc", [ArchiveFileTypeSubGhz] = ".sub", @@ -34,6 +34,7 @@ static const char* known_ext[] = { [ArchiveFileTypeFolder] = "?", [ArchiveFileTypeUnknown] = "*", [ArchiveFileTypeAppOrJs] = ".fap|.js", + [ArchiveFileTypeSetting] = "?", }; static const ArchiveFileTypeEnum known_type[] = { diff --git a/applications/main/archive/helpers/archive_favorites.c b/applications/main/archive/helpers/archive_favorites.c index 351d68c1d..5ed3adfe3 100644 --- a/applications/main/archive/helpers/archive_favorites.c +++ b/applications/main/archive/helpers/archive_favorites.c @@ -1,9 +1,10 @@ - #include "archive_favorites.h" #include "archive_files.h" #include "archive_apps.h" #include "archive_browser.h" +#include + #define ARCHIVE_FAV_FILE_BUF_LEN 32 static bool archive_favorites_read_line(File* file, FuriString* str_result) { @@ -337,3 +338,46 @@ void archive_favorites_save(void* context) { storage_file_free(file); furi_record_close(RECORD_STORAGE); } + +void archive_favorites_handle_setting_pin_unpin(const char* app_name, const char* setting) { + DialogMessage* message = dialog_message_alloc(); + + FuriString* setting_path = furi_string_alloc_set_str(app_name); + if(setting) { + furi_string_push_back(setting_path, '/'); + furi_string_cat_str(setting_path, setting); + } + const char* setting_path_str = furi_string_get_cstr(setting_path); + + bool is_favorite = archive_is_favorite("/app:setting/%s", setting_path_str); + dialog_message_set_header( + message, + is_favorite ? "Unpin This Setting?" : "Pin This Setting?", + 64, + 0, + AlignCenter, + AlignTop); + dialog_message_set_text( + message, + is_favorite ? "It will no longer be\naccessible from the\nFavorites menu" : + "It will be accessible from the\nFavorites menu", + 64, + 32, + AlignCenter, + AlignCenter); + dialog_message_set_buttons( + message, is_favorite ? "Unpin" : "Go back", NULL, is_favorite ? "Keep pinned" : "Pin"); + + DialogsApp* dialogs = furi_record_open(RECORD_DIALOGS); + DialogMessageButton button = dialog_message_show(dialogs, message); + furi_record_close(RECORD_DIALOGS); + + if(is_favorite && button == DialogMessageButtonLeft) { + archive_favorites_delete("/app:setting/%s", setting_path_str); + } else if(!is_favorite && button == DialogMessageButtonRight) { + archive_file_append(ARCHIVE_FAV_PATH, "/app:setting/%s\n", setting_path_str); + } + + furi_string_free(setting_path); + dialog_message_free(message); +} diff --git a/applications/main/archive/helpers/archive_favorites.h b/applications/main/archive/helpers/archive_favorites.h index 75070c44d..3a43a0e75 100644 --- a/applications/main/archive/helpers/archive_favorites.h +++ b/applications/main/archive/helpers/archive_favorites.h @@ -12,3 +12,13 @@ bool archive_is_favorite(const char* format, ...) _ATTRIBUTE((__format__(__print bool archive_favorites_rename(const char* src, const char* dst); void archive_add_to_favorites(const char* file_path); void archive_favorites_save(void* context); + +/** + * Intended to be called by settings apps to handle long presses, as well as + * internally from within the archive + * + * @param app_name name of the referring application + * @param setting name of the setting, which will be both displayed to the user + * and passed to the application as an argument upon recall + */ +void archive_favorites_handle_setting_pin_unpin(const char* app_name, const char* setting); diff --git a/applications/main/archive/helpers/archive_files.h b/applications/main/archive/helpers/archive_files.h index ad7662342..ea354bd5a 100644 --- a/applications/main/archive/helpers/archive_files.h +++ b/applications/main/archive/helpers/archive_files.h @@ -20,6 +20,7 @@ typedef enum { ArchiveFileTypeFolder, ArchiveFileTypeUnknown, ArchiveFileTypeAppOrJs, + ArchiveFileTypeSetting, ArchiveFileTypeLoading, } ArchiveFileTypeEnum; diff --git a/applications/main/archive/scenes/archive_scene_browser.c b/applications/main/archive/scenes/archive_scene_browser.c index 1b6088035..d600815f6 100644 --- a/applications/main/archive/scenes/archive_scene_browser.c +++ b/applications/main/archive/scenes/archive_scene_browser.c @@ -52,20 +52,37 @@ static void archive_run_in_app(ArchiveBrowserView* browser, ArchiveFile_t* selec UNUSED(browser); Loader* loader = furi_record_open(RECORD_LOADER); - const char* app_name = archive_get_flipper_app_name(selected->type); - - if(app_name) { - if(selected->is_app) { - char* param = strrchr(furi_string_get_cstr(selected->path), '/'); - if(param != NULL) { - param++; - } - loader_start_with_gui_error(loader, app_name, param); + if(selected->type == ArchiveFileTypeSetting) { + FuriString* app_name = furi_string_alloc_set(selected->path); + furi_string_right(app_name, furi_string_search_char(app_name, '/', 1) + 1); + size_t slash = furi_string_search_char(app_name, '/', 1); + if(slash != FURI_STRING_FAILURE) { + furi_string_left(app_name, slash); + FuriString* app_args = + furi_string_alloc_set_str(furi_string_get_cstr(app_name) + slash + 1); + loader_start_with_gui_error( + loader, furi_string_get_cstr(app_name), furi_string_get_cstr(app_args)); + furi_string_free(app_args); } else { - loader_start_with_gui_error(loader, app_name, furi_string_get_cstr(selected->path)); + loader_start_with_gui_error(loader, furi_string_get_cstr(app_name), NULL); } + furi_string_free(app_name); } else { - loader_start_with_gui_error(loader, furi_string_get_cstr(selected->path), NULL); + const char* app_name = archive_get_flipper_app_name(selected->type); + if(app_name) { + if(selected->is_app) { + char* param = strrchr(furi_string_get_cstr(selected->path), '/'); + if(param != NULL) { + param++; + } + loader_start_with_gui_error(loader, app_name, param); + } else { + loader_start_with_gui_error( + loader, app_name, furi_string_get_cstr(selected->path)); + } + } else { + loader_start_with_gui_error(loader, furi_string_get_cstr(selected->path), NULL); + } } furi_record_close(RECORD_LOADER); diff --git a/applications/main/archive/views/archive_browser_view.c b/applications/main/archive/views/archive_browser_view.c index 9e0918373..06e3bca29 100644 --- a/applications/main/archive/views/archive_browser_view.c +++ b/applications/main/archive/views/archive_browser_view.c @@ -28,6 +28,7 @@ static const Icon* ArchiveItemIcons[] = { [ArchiveFileTypeInfrared] = &I_ir_10px, [ArchiveFileTypeBadUsb] = &I_badusb_10px, [ArchiveFileTypeU2f] = &I_u2f_10px, + [ArchiveFileTypeSetting] = &I_settings_10px, [ArchiveFileTypeUpdateManifest] = &I_update_10px, [ArchiveFileTypeFolder] = &I_dir_10px, [ArchiveFileTypeUnknown] = &I_unknown_10px, diff --git a/applications/services/gui/modules/submenu.c b/applications/services/gui/modules/submenu.c index 5b1bdccda..104044cd7 100644 --- a/applications/services/gui/modules/submenu.c +++ b/applications/services/gui/modules/submenu.c @@ -11,8 +11,12 @@ struct Submenu { typedef struct { FuriString* label; uint32_t index; - SubmenuItemCallback callback; + union { + SubmenuItemCallback callback; + SubmenuItemCallbackEx callback_ex; + }; void* callback_context; + bool has_extended_events; } SubmenuItem; static void SubmenuItem_init(SubmenuItem* item) { @@ -57,7 +61,7 @@ typedef struct { static void submenu_process_up(Submenu* submenu); static void submenu_process_down(Submenu* submenu); -static void submenu_process_ok(Submenu* submenu); +static void submenu_process_ok(Submenu* submenu, InputType input_type); static void submenu_view_draw_callback(Canvas* canvas, void* _model) { SubmenuModel* model = _model; @@ -120,7 +124,10 @@ static bool submenu_view_input_callback(InputEvent* event, void* context) { furi_assert(submenu); bool consumed = false; - if(event->type == InputTypeShort) { + if(event->key == InputKeyOk) { + consumed = true; + submenu_process_ok(submenu, event->type); + } else if(event->type == InputTypeShort) { switch(event->key) { case InputKeyUp: consumed = true; @@ -130,10 +137,6 @@ static bool submenu_view_input_callback(InputEvent* event, void* context) { consumed = true; submenu_process_down(submenu); break; - case InputKeyOk: - consumed = true; - submenu_process_ok(submenu); - break; default: break; } @@ -211,6 +214,31 @@ void submenu_add_item( item->index = index; item->callback = callback; item->callback_context = callback_context; + item->has_extended_events = false; + }, + true); +} + +void submenu_add_item_ex( + Submenu* submenu, + const char* label, + uint32_t index, + SubmenuItemCallbackEx callback, + void* callback_context) { + SubmenuItem* item = NULL; + furi_check(label); + furi_check(submenu); + + with_view_model( + submenu->view, + SubmenuModel * model, + { + item = SubmenuItemArray_push_new(model->items); + furi_string_set_str(item->label, label); + item->index = index; + item->callback_ex = callback; + item->callback_context = callback_context; + item->has_extended_events = true; }, true); } @@ -357,7 +385,7 @@ void submenu_process_down(Submenu* submenu) { true); } -void submenu_process_ok(Submenu* submenu) { +void submenu_process_ok(Submenu* submenu, InputType input_type) { SubmenuItem* item = NULL; with_view_model( @@ -371,8 +399,12 @@ void submenu_process_ok(Submenu* submenu) { }, true); - if(item && item->callback) { + if(!item) return; + + if(!item->has_extended_events && input_type == InputTypeShort && item->callback) { item->callback(item->callback_context, item->index); + } else if(item->has_extended_events && item->callback_ex) { + item->callback_ex(item->callback_context, input_type, item->index); } } diff --git a/applications/services/gui/modules/submenu.h b/applications/services/gui/modules/submenu.h index e435f94a2..b4bfc226f 100644 --- a/applications/services/gui/modules/submenu.h +++ b/applications/services/gui/modules/submenu.h @@ -14,6 +14,7 @@ extern "C" { /** Submenu anonymous structure */ typedef struct Submenu Submenu; typedef void (*SubmenuItemCallback)(void* context, uint32_t index); +typedef void (*SubmenuItemCallbackEx)(void* context, InputType input_type, uint32_t index); /** Allocate and initialize submenu * @@ -53,6 +54,22 @@ void submenu_add_item( SubmenuItemCallback callback, void* callback_context); +/** Add item to submenu with extended press events + * + * @param submenu Submenu instance + * @param label menu item label + * @param index menu item index, used for callback, may be + * the same with other items + * @param callback menu item extended callback + * @param callback_context menu item callback context + */ +void submenu_add_item_ex( + Submenu* submenu, + const char* label, + uint32_t index, + SubmenuItemCallbackEx callback, + void* callback_context); + /** Change label of an existing item * * @param submenu Submenu instance diff --git a/applications/services/loader/loader_menu.c b/applications/services/loader/loader_menu.c index 25763a0bc..dc1b673d5 100644 --- a/applications/services/loader/loader_menu.c +++ b/applications/services/loader/loader_menu.c @@ -4,6 +4,7 @@ #include #include #include +#include #include "loader.h" #include "loader_menu.h" @@ -71,10 +72,16 @@ static void loader_menu_applications_callback(void* context, uint32_t index) { loader_menu_start(name); } -static void loader_menu_settings_menu_callback(void* context, uint32_t index) { +static void + loader_menu_settings_menu_callback(void* context, InputType input_type, uint32_t index) { UNUSED(context); - const char* name = FLIPPER_SETTINGS_APPS[index].name; - loader_menu_start(name); + if(input_type == InputTypeShort) { + const char* name = FLIPPER_SETTINGS_APPS[index].name; + loader_menu_start(name); + } else if(input_type == InputTypeLong) { + const char* name = FLIPPER_SETTINGS_APPS[index].name; + archive_favorites_handle_setting_pin_unpin(name, NULL); + } } static void loader_menu_switch_to_settings(void* context, uint32_t index) { @@ -128,7 +135,7 @@ static void loader_menu_build_menu(LoaderMenuApp* app, LoaderMenu* menu) { static void loader_menu_build_submenu(LoaderMenuApp* app, LoaderMenu* loader_menu) { for(size_t i = 0; i < FLIPPER_SETTINGS_APPS_COUNT; i++) { - submenu_add_item( + submenu_add_item_ex( app->settings_menu, FLIPPER_SETTINGS_APPS[i].name, i, diff --git a/applications/settings/power_settings_app/power_settings_app.c b/applications/settings/power_settings_app/power_settings_app.c index d43bd4108..6959bdf41 100644 --- a/applications/settings/power_settings_app/power_settings_app.c +++ b/applications/settings/power_settings_app/power_settings_app.c @@ -1,5 +1,16 @@ #include "power_settings_app.h" +const SubmenuSettingsHelperDescriptor settings_helper_descriptor = { + .app_name = "Power", + .options_cnt = 3, + .options = + { + {.name = "Battery Info", .scene_id = PowerSettingsAppSceneBatteryInfo}, + {.name = "Reboot", .scene_id = PowerSettingsAppSceneReboot}, + {.name = "Power OFF", .scene_id = PowerSettingsAppScenePowerOff}, + }, +}; + static bool power_settings_custom_event_callback(void* context, uint32_t event) { furi_assert(context); PowerSettingsApp* app = context; @@ -18,7 +29,7 @@ static void power_settings_tick_event_callback(void* context) { scene_manager_handle_tick_event(app->scene_manager); } -PowerSettingsApp* power_settings_app_alloc(uint32_t first_scene) { +PowerSettingsApp* power_settings_app_alloc(void) { PowerSettingsApp* app = malloc(sizeof(PowerSettingsApp)); // Records @@ -50,13 +61,23 @@ PowerSettingsApp* power_settings_app_alloc(uint32_t first_scene) { view_dispatcher_add_view( app->view_dispatcher, PowerSettingsAppViewDialog, dialog_ex_get_view(app->dialog)); - // Set first scene - scene_manager_next_scene(app->scene_manager, first_scene); + // Helper + app->settings_helper = submenu_settings_helpers_alloc(&settings_helper_descriptor); + submenu_settings_helpers_assign_objects( + app->settings_helper, + app->view_dispatcher, + app->scene_manager, + app->submenu, + PowerSettingsAppViewSubmenu, + PowerSettingsAppSceneStart); + return app; } void power_settings_app_free(PowerSettingsApp* app) { furi_assert(app); + // Helper + submenu_settings_helpers_free(app->settings_helper); // Views view_dispatcher_remove_view(app->view_dispatcher, PowerSettingsAppViewBatteryInfo); battery_info_free(app->batery_info); @@ -74,11 +95,14 @@ void power_settings_app_free(PowerSettingsApp* app) { } int32_t power_settings_app(void* p) { - uint32_t first_scene = PowerSettingsAppSceneStart; - if(p && strlen(p) && !strcmp(p, "off")) { - first_scene = PowerSettingsAppScenePowerOff; + PowerSettingsApp* app = power_settings_app_alloc(); + if(!submenu_settings_helpers_app_start(app->settings_helper, p)) { + uint32_t first_scene = PowerSettingsAppSceneStart; + if(p && strlen(p) && !strcmp(p, "off")) { + first_scene = PowerSettingsAppScenePowerOff; + } + scene_manager_next_scene(app->scene_manager, first_scene); } - PowerSettingsApp* app = power_settings_app_alloc(first_scene); view_dispatcher_run(app->view_dispatcher); power_settings_app_free(app); return 0; diff --git a/applications/settings/power_settings_app/power_settings_app.h b/applications/settings/power_settings_app/power_settings_app.h index e19e7c983..b33d63db2 100644 --- a/applications/settings/power_settings_app/power_settings_app.h +++ b/applications/settings/power_settings_app/power_settings_app.h @@ -14,6 +14,8 @@ #include "scenes/power_settings_scene.h" +#include + typedef struct { Power* power; Gui* gui; @@ -23,6 +25,7 @@ typedef struct { Submenu* submenu; DialogEx* dialog; PowerInfo info; + SubmenuSettingsHelper* settings_helper; } PowerSettingsApp; typedef enum { diff --git a/applications/settings/power_settings_app/scenes/power_settings_scene_start.c b/applications/settings/power_settings_app/scenes/power_settings_scene_start.c index 63f123d89..f8a5a7edb 100644 --- a/applications/settings/power_settings_app/scenes/power_settings_scene_start.c +++ b/applications/settings/power_settings_app/scenes/power_settings_scene_start.c @@ -1,64 +1,16 @@ #include "../power_settings_app.h" -enum PowerSettingsSubmenuIndex { - PowerSettingsSubmenuIndexBatteryInfo, - PowerSettingsSubmenuIndexReboot, - PowerSettingsSubmenuIndexOff, -}; - -static void power_settings_scene_start_submenu_callback(void* context, uint32_t index) { - furi_assert(context); - PowerSettingsApp* app = context; - view_dispatcher_send_custom_event(app->view_dispatcher, index); -} - void power_settings_scene_start_on_enter(void* context) { PowerSettingsApp* app = context; - Submenu* submenu = app->submenu; - - submenu_add_item( - submenu, - "Battery Info", - PowerSettingsSubmenuIndexBatteryInfo, - power_settings_scene_start_submenu_callback, - app); - submenu_add_item( - submenu, - "Reboot", - PowerSettingsSubmenuIndexReboot, - power_settings_scene_start_submenu_callback, - app); - submenu_add_item( - submenu, - "Power OFF", - PowerSettingsSubmenuIndexOff, - power_settings_scene_start_submenu_callback, - app); - submenu_set_selected_item( - submenu, scene_manager_get_scene_state(app->scene_manager, PowerSettingsAppSceneStart)); - - view_dispatcher_switch_to_view(app->view_dispatcher, PowerSettingsAppViewSubmenu); + submenu_settings_helpers_scene_enter(app->settings_helper); } bool power_settings_scene_start_on_event(void* context, SceneManagerEvent event) { PowerSettingsApp* app = context; - bool consumed = false; - - if(event.type == SceneManagerEventTypeCustom) { - if(event.event == PowerSettingsSubmenuIndexBatteryInfo) { - scene_manager_next_scene(app->scene_manager, PowerSettingsAppSceneBatteryInfo); - } else if(event.event == PowerSettingsSubmenuIndexReboot) { - scene_manager_next_scene(app->scene_manager, PowerSettingsAppSceneReboot); - } else if(event.event == PowerSettingsSubmenuIndexOff) { - scene_manager_next_scene(app->scene_manager, PowerSettingsAppScenePowerOff); - } - scene_manager_set_scene_state(app->scene_manager, PowerSettingsAppSceneStart, event.event); - consumed = true; - } - return consumed; + return submenu_settings_helpers_scene_event(app->settings_helper, event); } void power_settings_scene_start_on_exit(void* context) { PowerSettingsApp* app = context; - submenu_reset(app->submenu); + submenu_settings_helpers_scene_exit(app->settings_helper); } diff --git a/applications/settings/storage_settings/scenes/storage_settings_scene_start.c b/applications/settings/storage_settings/scenes/storage_settings_scene_start.c index e351a2ef7..f03474ac1 100644 --- a/applications/settings/storage_settings/scenes/storage_settings_scene_start.c +++ b/applications/settings/storage_settings/scenes/storage_settings_scene_start.c @@ -1,131 +1,20 @@ #include "../storage_settings.h" -enum StorageSettingsStartSubmenuIndex { - StorageSettingsStartSubmenuIndexInternalInfo, - StorageSettingsStartSubmenuIndexSDInfo, - StorageSettingsStartSubmenuIndexUnmount, - StorageSettingsStartSubmenuIndexFormat, - StorageSettingsStartSubmenuIndexBenchy, - StorageSettingsStartSubmenuIndexFactoryReset -}; - -static void storage_settings_scene_start_submenu_callback(void* context, uint32_t index) { - StorageSettings* app = context; - - view_dispatcher_send_custom_event(app->view_dispatcher, index); -} - void storage_settings_scene_start_on_enter(void* context) { StorageSettings* app = context; - Submenu* submenu = app->submenu; - - submenu_add_item( - submenu, - "About Internal Storage", - StorageSettingsStartSubmenuIndexInternalInfo, - storage_settings_scene_start_submenu_callback, - app); - submenu_add_item( - submenu, - "About SD Card", - StorageSettingsStartSubmenuIndexSDInfo, - storage_settings_scene_start_submenu_callback, - app); FS_Error sd_status = storage_sd_status(app->fs_api); - if(sd_status != FSE_OK) { - submenu_add_item( - submenu, - "Mount SD Card", - StorageSettingsStartSubmenuIndexUnmount, - storage_settings_scene_start_submenu_callback, - app); - } else { - submenu_add_item( - submenu, - "Unmount SD Card", - StorageSettingsStartSubmenuIndexUnmount, - storage_settings_scene_start_submenu_callback, - app); - } - - submenu_add_item( - submenu, - "Format SD Card", - StorageSettingsStartSubmenuIndexFormat, - storage_settings_scene_start_submenu_callback, - app); - submenu_add_item( - submenu, - "Benchmark SD Card", - StorageSettingsStartSubmenuIndexBenchy, - storage_settings_scene_start_submenu_callback, - app); - submenu_add_item( - submenu, - "Factory Reset", - StorageSettingsStartSubmenuIndexFactoryReset, - storage_settings_scene_start_submenu_callback, - app); - - submenu_set_selected_item( - submenu, scene_manager_get_scene_state(app->scene_manager, StorageSettingsStart)); - - view_dispatcher_switch_to_view(app->view_dispatcher, StorageSettingsViewSubmenu); + app->helper_descriptor->options[STORAGE_SETTINGS_MOUNT_INDEX].name = + (sd_status != FSE_OK) ? "Mount SD Card" : "Unmount SD Card"; + submenu_settings_helpers_scene_enter(app->settings_helper); } bool storage_settings_scene_start_on_event(void* context, SceneManagerEvent event) { StorageSettings* app = context; - bool consumed = false; - - if(event.type == SceneManagerEventTypeCustom) { - switch(event.event) { - case StorageSettingsStartSubmenuIndexSDInfo: - scene_manager_set_scene_state( - app->scene_manager, StorageSettingsStart, StorageSettingsStartSubmenuIndexSDInfo); - scene_manager_next_scene(app->scene_manager, StorageSettingsSDInfo); - consumed = true; - break; - case StorageSettingsStartSubmenuIndexInternalInfo: - scene_manager_set_scene_state( - app->scene_manager, - StorageSettingsStart, - StorageSettingsStartSubmenuIndexInternalInfo); - scene_manager_next_scene(app->scene_manager, StorageSettingsInternalInfo); - consumed = true; - break; - case StorageSettingsStartSubmenuIndexUnmount: - scene_manager_set_scene_state( - app->scene_manager, StorageSettingsStart, StorageSettingsStartSubmenuIndexUnmount); - scene_manager_next_scene(app->scene_manager, StorageSettingsUnmountConfirm); - consumed = true; - break; - case StorageSettingsStartSubmenuIndexFormat: - scene_manager_set_scene_state( - app->scene_manager, StorageSettingsStart, StorageSettingsStartSubmenuIndexFormat); - scene_manager_next_scene(app->scene_manager, StorageSettingsFormatConfirm); - consumed = true; - break; - case StorageSettingsStartSubmenuIndexBenchy: - scene_manager_set_scene_state( - app->scene_manager, StorageSettingsStart, StorageSettingsStartSubmenuIndexBenchy); - scene_manager_next_scene(app->scene_manager, StorageSettingsBenchmarkConfirm); - consumed = true; - break; - case StorageSettingsStartSubmenuIndexFactoryReset: - scene_manager_set_scene_state( - app->scene_manager, - StorageSettingsStart, - StorageSettingsStartSubmenuIndexFactoryReset); - scene_manager_next_scene(app->scene_manager, StorageSettingsFactoryReset); - consumed = true; - break; - } - } - return consumed; + return submenu_settings_helpers_scene_event(app->settings_helper, event); } void storage_settings_scene_start_on_exit(void* context) { StorageSettings* app = context; - submenu_reset(app->submenu); + submenu_settings_helpers_scene_exit(app->settings_helper); } diff --git a/applications/settings/storage_settings/storage_settings.c b/applications/settings/storage_settings/storage_settings.c index 354632890..b4cde7b6b 100644 --- a/applications/settings/storage_settings/storage_settings.c +++ b/applications/settings/storage_settings/storage_settings.c @@ -1,5 +1,19 @@ #include "storage_settings.h" +const SubmenuSettingsHelperDescriptor descriptor_template = { + .app_name = "Storage", + .options_cnt = 6, + .options = + { + {.name = "About Internal Storage", .scene_id = StorageSettingsSDInfo}, + {.name = "About SD Card", .scene_id = StorageSettingsInternalInfo}, + {.name = "Unmount SD Card", .scene_id = StorageSettingsUnmountConfirm}, + {.name = "Format SD Card", .scene_id = StorageSettingsFormatConfirm}, + {.name = "Benchmark SD Card", .scene_id = StorageSettingsBenchmarkConfirm}, + {.name = "Factory Reset", .scene_id = StorageSettingsFactoryReset}, + }, +}; + static bool storage_settings_custom_event_callback(void* context, uint32_t event) { furi_assert(context); StorageSettings* app = context; @@ -40,12 +54,27 @@ static StorageSettings* storage_settings_alloc(void) { view_dispatcher_add_view( app->view_dispatcher, StorageSettingsViewDialogEx, dialog_ex_get_view(app->dialog_ex)); - scene_manager_next_scene(app->scene_manager, StorageSettingsStart); + size_t descriptor_size = + sizeof(SubmenuSettingsHelperDescriptor) + + (descriptor_template.options_cnt * sizeof(SubmenuSettingsHelperOption)); + app->helper_descriptor = malloc(descriptor_size); + memcpy(app->helper_descriptor, &descriptor_template, descriptor_size); + app->settings_helper = submenu_settings_helpers_alloc(app->helper_descriptor); + submenu_settings_helpers_assign_objects( + app->settings_helper, + app->view_dispatcher, + app->scene_manager, + app->submenu, + StorageSettingsViewSubmenu, + StorageSettingsStart); return app; } static void storage_settings_free(StorageSettings* app) { + submenu_settings_helpers_free(app->settings_helper); + free(app->helper_descriptor); + view_dispatcher_remove_view(app->view_dispatcher, StorageSettingsViewSubmenu); submenu_free(app->submenu); @@ -68,6 +97,10 @@ int32_t storage_settings_app(void* p) { UNUSED(p); StorageSettings* app = storage_settings_alloc(); + if(!submenu_settings_helpers_app_start(app->settings_helper, p)) { + scene_manager_next_scene(app->scene_manager, StorageSettingsStart); + } + view_dispatcher_run(app->view_dispatcher); storage_settings_free(app); diff --git a/applications/settings/storage_settings/storage_settings.h b/applications/settings/storage_settings/storage_settings.h index fd841623e..5f4c6404e 100644 --- a/applications/settings/storage_settings/storage_settings.h +++ b/applications/settings/storage_settings/storage_settings.h @@ -16,6 +16,10 @@ #include "scenes/storage_settings_scene.h" +#include + +#define STORAGE_SETTINGS_MOUNT_INDEX 2 + #ifdef __cplusplus extern "C" { #endif @@ -36,6 +40,10 @@ typedef struct { // text FuriString* text_string; + + // helpers + SubmenuSettingsHelperDescriptor* helper_descriptor; + SubmenuSettingsHelper* settings_helper; } StorageSettings; typedef enum { diff --git a/assets/icons/Archive/settings_10px.png b/assets/icons/Archive/settings_10px.png new file mode 100644 index 0000000000000000000000000000000000000000..2c1e4a2627f5a8ba4bacd3d9cf247c6397f6a855 GIT binary patch literal 96 zcmeAS@N?(olHy`uVBq!ia0vp^AT}2xkYHHq`AGmsse8IOhE&W+USWA4<@5oSf`*4| s%!k?8kEKPkZDa1yESH#PXs|bhfuTu~<2|RzJbRE + +struct SubmenuSettingsHelper { + const SubmenuSettingsHelperDescriptor* descriptor; + ViewDispatcher* view_dispatcher; + SceneManager* scene_manager; + Submenu* submenu; + uint32_t submenu_view_id; + uint32_t main_scene_id; +}; + +SubmenuSettingsHelper* + submenu_settings_helpers_alloc(const SubmenuSettingsHelperDescriptor* descriptor) { + furi_check(descriptor); + SubmenuSettingsHelper* helper = malloc(sizeof(SubmenuSettingsHelper)); + helper->descriptor = descriptor; + return helper; +} + +void submenu_settings_helpers_assign_objects( + SubmenuSettingsHelper* helper, + ViewDispatcher* view_dispatcher, + SceneManager* scene_manager, + Submenu* submenu, + uint32_t submenu_view_id, + uint32_t main_scene_id) { + furi_check(helper); + furi_check(view_dispatcher); + furi_check(scene_manager); + furi_check(submenu); + helper->view_dispatcher = view_dispatcher; + helper->scene_manager = scene_manager; + helper->submenu = submenu; + helper->submenu_view_id = submenu_view_id; + helper->main_scene_id = main_scene_id; +} + +void submenu_settings_helpers_free(SubmenuSettingsHelper* helper) { + free(helper); +} + +bool submenu_settings_helpers_app_start(SubmenuSettingsHelper* helper, void* arg) { + furi_check(helper); + if(!arg) return false; + + const char* option = arg; + for(size_t i = 0; i < helper->descriptor->options_cnt; i++) { + if(strcmp(helper->descriptor->options[i].name, option) == 0) { + scene_manager_next_scene( + helper->scene_manager, helper->descriptor->options[i].scene_id); + return true; + } + } + + return false; +} + +static void + submenu_settings_helpers_callback(void* context, InputType input_type, uint32_t index) { + SubmenuSettingsHelper* helper = context; + if(input_type == InputTypeShort) { + view_dispatcher_send_custom_event(helper->view_dispatcher, index); + } else if(input_type == InputTypeLong) { + archive_favorites_handle_setting_pin_unpin( + helper->descriptor->app_name, helper->descriptor->options[index].name); + } +} + +void submenu_settings_helpers_scene_enter(SubmenuSettingsHelper* helper) { + furi_check(helper); + for(size_t i = 0; i < helper->descriptor->options_cnt; i++) { + submenu_add_item_ex( + helper->submenu, + helper->descriptor->options[i].name, + i, + submenu_settings_helpers_callback, + helper); + } + + submenu_set_selected_item( + helper->submenu, + scene_manager_get_scene_state(helper->scene_manager, helper->main_scene_id)); + view_dispatcher_switch_to_view(helper->view_dispatcher, helper->submenu_view_id); +} + +bool submenu_settings_helpers_scene_event(SubmenuSettingsHelper* helper, SceneManagerEvent event) { + furi_check(helper); + + if(event.type == SceneManagerEventTypeCustom) { + scene_manager_next_scene( + helper->scene_manager, helper->descriptor->options[event.event].scene_id); + scene_manager_set_scene_state(helper->scene_manager, helper->main_scene_id, event.event); + return true; + } + + return false; +} + +void submenu_settings_helpers_scene_exit(SubmenuSettingsHelper* helper) { + furi_check(helper); + submenu_reset(helper->submenu); +} diff --git a/lib/toolbox/settings_helpers/submenu_based.h b/lib/toolbox/settings_helpers/submenu_based.h new file mode 100644 index 000000000..6929cb04a --- /dev/null +++ b/lib/toolbox/settings_helpers/submenu_based.h @@ -0,0 +1,89 @@ +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef void (*SubmenuSettingsHelpherCallback)(void* context, uint32_t index); + +typedef struct { + const char* name; + uint32_t scene_id; +} SubmenuSettingsHelperOption; + +typedef struct { + const char* app_name; + size_t options_cnt; + SubmenuSettingsHelperOption options[]; +} SubmenuSettingsHelperDescriptor; + +typedef struct SubmenuSettingsHelper SubmenuSettingsHelper; + +/** + * @brief Allocates a submenu-based settings helper + * @param descriptor settings descriptor + */ +SubmenuSettingsHelper* + submenu_settings_helpers_alloc(const SubmenuSettingsHelperDescriptor* descriptor); + +/** + * @brief Assigns dynamic objects to the submenu-based settings helper + * @param helper helper object + * @param view_dispatcher ViewDispatcher + * @param scene_manager SceneManager + * @param submenu Submenu + * @param submenu_view_id Submenu view id in the ViewDispatcher + * @param main_scene_id Main scene id in the SceneManager + */ +void submenu_settings_helpers_assign_objects( + SubmenuSettingsHelper* helper, + ViewDispatcher* view_dispatcher, + SceneManager* scene_manager, + Submenu* submenu, + uint32_t submenu_view_id, + uint32_t main_scene_id); + +/** + * @brief Frees a submenu-based settings helper + * @param helper helper object + */ +void submenu_settings_helpers_free(SubmenuSettingsHelper* helper); + +/** + * @brief App start callback for the submenu-based settings helper + * + * If an argument containing one of the options was provided, launches the + * corresponding scene. + * + * @param helper helper object + * @param arg app argument, may be NULL + * @returns true if a setting name was provided in the argument, false if normal + * app operation shall commence + */ +bool submenu_settings_helpers_app_start(SubmenuSettingsHelper* helper, void* arg); + +/** + * @brief Main scene enter callback for the submenu-based settings helper + * @param helper helper object + */ +void submenu_settings_helpers_scene_enter(SubmenuSettingsHelper* helper); + +/** + * @brief Main scene event callback for the submenu-based settings helper + * @param helper helper object + * @param event event data + * @returns true if the event was consumed, false otherwise + */ +bool submenu_settings_helpers_scene_event(SubmenuSettingsHelper* helper, SceneManagerEvent event); + +/** + * @brief Main scene exit callback for the submenu-based settings helper + * @param helper helper object + */ +void submenu_settings_helpers_scene_exit(SubmenuSettingsHelper* helper); + +#ifdef __cplusplus +} +#endif diff --git a/targets/f18/api_symbols.csv b/targets/f18/api_symbols.csv index aa3a3813a..d4cc2bec5 100644 --- a/targets/f18/api_symbols.csv +++ b/targets/f18/api_symbols.csv @@ -2726,6 +2726,7 @@ Function,-,strverscmp,int,"const char*, const char*" Function,-,strxfrm,size_t,"char*, const char*, size_t" Function,-,strxfrm_l,size_t,"char*, const char*, size_t, locale_t" Function,+,submenu_add_item,void,"Submenu*, const char*, uint32_t, SubmenuItemCallback, void*" +Function,+,submenu_add_item_ex,void,"Submenu*, const char*, uint32_t, SubmenuItemCallbackEx, void*" Function,+,submenu_alloc,Submenu*, Function,+,submenu_change_item_label,void,"Submenu*, uint32_t, const char*" Function,+,submenu_free,void,Submenu* diff --git a/targets/f7/api_symbols.csv b/targets/f7/api_symbols.csv index 437a86a49..f5f18bc43 100644 --- a/targets/f7/api_symbols.csv +++ b/targets/f7/api_symbols.csv @@ -3575,6 +3575,7 @@ Function,+,subghz_worker_set_pair_callback,void,"SubGhzWorker*, SubGhzWorkerPair Function,+,subghz_worker_start,void,SubGhzWorker* Function,+,subghz_worker_stop,void,SubGhzWorker* Function,+,submenu_add_item,void,"Submenu*, const char*, uint32_t, SubmenuItemCallback, void*" +Function,+,submenu_add_item_ex,void,"Submenu*, const char*, uint32_t, SubmenuItemCallbackEx, void*" Function,+,submenu_alloc,Submenu*, Function,+,submenu_change_item_label,void,"Submenu*, uint32_t, const char*" Function,+,submenu_free,void,Submenu* From 3d02063bce0c93dda960935a4df11f605aca246e Mon Sep 17 00:00:00 2001 From: Anna Antonenko Date: Mon, 7 Apr 2025 16:45:09 +0400 Subject: [PATCH 202/268] fbt: Deterministic STARTUP order & additional checks (#4179) * unit_tests: clear startup order * fam: ensure unique STARTUP order * fbt: warn on same .order values within a group leading to non-determinitic builds * fbt: better formatting for app order warning --------- Co-authored-by: hedger --- applications/debug/unit_tests/application.fam | 3 ++- applications/main/archive/application.fam | 2 +- applications/services/bt/application.fam | 2 +- applications/services/cli/application.fam | 2 +- applications/services/crypto/application.fam | 2 +- applications/services/expansion/application.fam | 2 +- applications/services/loader/application.fam | 2 +- applications/services/locale/application.fam | 2 +- applications/services/power/application.fam | 2 +- applications/services/region/application.fam | 2 +- applications/services/storage/application.fam | 2 +- applications/system/js_app/application.fam | 4 ++-- applications/system/updater/application.fam | 4 ++-- scripts/fbt_tools/fbt_apps.py | 13 ++++++++++++- 14 files changed, 28 insertions(+), 16 deletions(-) diff --git a/applications/debug/unit_tests/application.fam b/applications/debug/unit_tests/application.fam index ed5f8c9da..72b8cafcb 100644 --- a/applications/debug/unit_tests/application.fam +++ b/applications/debug/unit_tests/application.fam @@ -7,6 +7,7 @@ App( requires=["system_settings", "cli_subghz"], provides=["delay_test"], resources="resources", + order=30, ) App( @@ -17,7 +18,7 @@ App( entry_point="delay_test_app", stack_size=1 * 1024, requires=["unit_tests"], - order=110, + order=30, ) App( diff --git a/applications/main/archive/application.fam b/applications/main/archive/application.fam index f0a980ab0..00075107a 100644 --- a/applications/main/archive/application.fam +++ b/applications/main/archive/application.fam @@ -7,5 +7,5 @@ App( requires=["gui"], stack_size=4 * 1024, icon="A_FileManager_14", - order=0, + order=10, ) diff --git a/applications/services/bt/application.fam b/applications/services/bt/application.fam index 2d2840e3a..0e4cc918f 100644 --- a/applications/services/bt/application.fam +++ b/applications/services/bt/application.fam @@ -21,5 +21,5 @@ App( appid="bt_start", apptype=FlipperAppType.STARTUP, entry_point="bt_on_system_start", - order=70, + order=40, ) diff --git a/applications/services/cli/application.fam b/applications/services/cli/application.fam index e1e79f43d..6e0a28164 100644 --- a/applications/services/cli/application.fam +++ b/applications/services/cli/application.fam @@ -21,7 +21,7 @@ App( apptype=FlipperAppType.SERVICE, entry_point="cli_vcp_srv", stack_size=1024, - order=40, + order=10, sdk_headers=["cli_vcp.h"], sources=["cli_vcp.c"], ) diff --git a/applications/services/crypto/application.fam b/applications/services/crypto/application.fam index 7771c5ed2..1d07ca409 100644 --- a/applications/services/crypto/application.fam +++ b/applications/services/crypto/application.fam @@ -2,5 +2,5 @@ App( appid="crypto_start", apptype=FlipperAppType.STARTUP, entry_point="crypto_on_system_start", - order=10, + order=20, ) diff --git a/applications/services/expansion/application.fam b/applications/services/expansion/application.fam index dbdde0a52..f85450e40 100644 --- a/applications/services/expansion/application.fam +++ b/applications/services/expansion/application.fam @@ -8,5 +8,5 @@ App( ], requires=["rpc_start"], provides=["expansion_settings"], - order=150, + order=100, ) diff --git a/applications/services/loader/application.fam b/applications/services/loader/application.fam index f4d006e07..5600bdf62 100644 --- a/applications/services/loader/application.fam +++ b/applications/services/loader/application.fam @@ -19,5 +19,5 @@ App( apptype=FlipperAppType.STARTUP, entry_point="loader_on_system_start", requires=["loader"], - order=90, + order=80, ) diff --git a/applications/services/locale/application.fam b/applications/services/locale/application.fam index c762d02d6..09f49161f 100644 --- a/applications/services/locale/application.fam +++ b/applications/services/locale/application.fam @@ -4,6 +4,6 @@ App( apptype=FlipperAppType.STARTUP, entry_point="locale_on_system_start", cdefines=["SRV_LOCALE"], - order=90, + order=70, sdk_headers=["locale.h"], ) diff --git a/applications/services/power/application.fam b/applications/services/power/application.fam index f14d88c54..0e69ec6ec 100644 --- a/applications/services/power/application.fam +++ b/applications/services/power/application.fam @@ -22,5 +22,5 @@ App( apptype=FlipperAppType.STARTUP, entry_point="power_on_system_start", requires=["power"], - order=80, + order=50, ) diff --git a/applications/services/region/application.fam b/applications/services/region/application.fam index a4cdc94ea..9b2451213 100644 --- a/applications/services/region/application.fam +++ b/applications/services/region/application.fam @@ -6,5 +6,5 @@ App( entry_point="region_on_system_start", cdefines=["SRV_REGION"], requires=["storage"], - order=170, + order=120, ) diff --git a/applications/services/storage/application.fam b/applications/services/storage/application.fam index 7aa721cc3..047500fa3 100644 --- a/applications/services/storage/application.fam +++ b/applications/services/storage/application.fam @@ -16,5 +16,5 @@ App( apptype=FlipperAppType.STARTUP, entry_point="storage_on_system_start", requires=["storage"], - order=90, + order=60, ) diff --git a/applications/system/js_app/application.fam b/applications/system/js_app/application.fam index 66ec221ec..843ab5543 100644 --- a/applications/system/js_app/application.fam +++ b/applications/system/js_app/application.fam @@ -5,7 +5,7 @@ App( entry_point="js_app", stack_size=2 * 1024, resources="examples", - order=0, + order=10, provides=["js_app_start"], sources=[ "js_app.c", @@ -23,7 +23,7 @@ App( appid="js_app_start", apptype=FlipperAppType.STARTUP, entry_point="js_app_on_system_start", - order=160, + order=110, sources=["js_app.c"], ) diff --git a/applications/system/updater/application.fam b/applications/system/updater/application.fam index a693fa6f5..0c56a06ef 100644 --- a/applications/system/updater/application.fam +++ b/applications/system/updater/application.fam @@ -27,7 +27,7 @@ App( provides=["updater_start"], entry_point="updater_srv", stack_size=2 * 1024, - order=10, + order=20, ) App( @@ -35,5 +35,5 @@ App( apptype=FlipperAppType.STARTUP, entry_point="updater_on_system_start", requires=["updater_app"], - order=110, + order=90, ) diff --git a/scripts/fbt_tools/fbt_apps.py b/scripts/fbt_tools/fbt_apps.py index 3a17a79a3..c48e4ba80 100644 --- a/scripts/fbt_tools/fbt_apps.py +++ b/scripts/fbt_tools/fbt_apps.py @@ -150,11 +150,22 @@ def DumpApplicationConfig(target, source, env): print(fg.boldgreen("Firmware modules configuration:")) for apptype in FlipperAppType: app_sublist = env["APPBUILD"].get_apps_of_type(apptype) - if app_sublist: + # Print a warning if any apps in the list have same .order value + unique_order_values = set(app.order for app in app_sublist) + if len(app_sublist) != len(unique_order_values) and max(unique_order_values): + print( + fg.red(f"{apptype.value}: ") + + fg.yellow( + "Duplicate .order values in group:\n\t" + + ", ".join(f"{app.appid} ({app.order})" for app in app_sublist) + ) + ) + elif app_sublist: print( fg.green(f"{apptype.value}:\n\t"), ", ".join(app.appid for app in app_sublist), ) + if incompatible_ext_apps := env["APPBUILD"].get_incompatible_ext_apps(): print( fg.blue("Incompatible apps (skipped):\n\t"), From 4dd123fd8a9f33769348a29c9f90d00a6ad8341f Mon Sep 17 00:00:00 2001 From: Eric Betts Date: Mon, 7 Apr 2025 08:16:42 -0700 Subject: [PATCH 203/268] BLE: Slightly increase mfg_data size (#4177) Co-authored-by: hedger --- targets/f7/ble_glue/gap.c | 4 ++-- targets/f7/ble_glue/gap.h | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/targets/f7/ble_glue/gap.c b/targets/f7/ble_glue/gap.c index 1fe898ea9..70c9d1c6f 100644 --- a/targets/f7/ble_glue/gap.c +++ b/targets/f7/ble_glue/gap.c @@ -24,7 +24,7 @@ typedef struct { uint8_t adv_svc_uuid_len; uint8_t adv_svc_uuid[20]; uint8_t mfg_data_len; - uint8_t mfg_data[20]; + uint8_t mfg_data[23]; char* adv_name; } GapSvc; @@ -326,7 +326,7 @@ static void set_advertisment_service_uid(uint8_t* uid, uint8_t uid_len) { } static void set_manufacturer_data(uint8_t* mfg_data, uint8_t mfg_data_len) { - furi_check(mfg_data_len < sizeof(gap->service.mfg_data) - 2); + furi_check(mfg_data_len <= sizeof(gap->service.mfg_data) - 2); gap->service.mfg_data[0] = mfg_data_len + 1; gap->service.mfg_data[1] = AD_TYPE_MANUFACTURER_SPECIFIC_DATA; memcpy(&gap->service.mfg_data[gap->service.mfg_data_len], mfg_data, mfg_data_len); diff --git a/targets/f7/ble_glue/gap.h b/targets/f7/ble_glue/gap.h index d37fd9a1e..b2bf0ffc1 100644 --- a/targets/f7/ble_glue/gap.h +++ b/targets/f7/ble_glue/gap.h @@ -73,7 +73,7 @@ typedef struct { uint16_t Service_UUID_16; uint8_t Service_UUID_128[16]; } adv_service; - uint8_t mfg_data[20]; + uint8_t mfg_data[23]; uint8_t mfg_data_len; uint16_t appearance_char; bool bonding_mode; From fa0962efebf9318066a771aa8dbd300f0b5fa8d0 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Mon, 7 Apr 2025 18:24:33 +0300 Subject: [PATCH 204/268] integrate mfkey to system apps --- applications/system/mfkey/application.fam | 28 + applications/system/mfkey/crypto1.c | 22 + applications/system/mfkey/crypto1.h | 256 ++++++ applications/system/mfkey/images/mfkey.png | Bin 0 -> 107 bytes applications/system/mfkey/init_plugin.c | 356 ++++++++ applications/system/mfkey/mfkey.c | 915 +++++++++++++++++++ applications/system/mfkey/mfkey.h | 108 +++ applications/system/mfkey/mfkey.png | Bin 0 -> 107 bytes applications/system/mfkey/plugin_interface.h | 13 + 9 files changed, 1698 insertions(+) create mode 100644 applications/system/mfkey/application.fam create mode 100644 applications/system/mfkey/crypto1.c create mode 100644 applications/system/mfkey/crypto1.h create mode 100644 applications/system/mfkey/images/mfkey.png create mode 100644 applications/system/mfkey/init_plugin.c create mode 100644 applications/system/mfkey/mfkey.c create mode 100644 applications/system/mfkey/mfkey.h create mode 100644 applications/system/mfkey/mfkey.png create mode 100644 applications/system/mfkey/plugin_interface.h diff --git a/applications/system/mfkey/application.fam b/applications/system/mfkey/application.fam new file mode 100644 index 000000000..dfd513847 --- /dev/null +++ b/applications/system/mfkey/application.fam @@ -0,0 +1,28 @@ +App( + appid="mfkey", + name="MFKey", + apptype=FlipperAppType.EXTERNAL, + targets=["f7"], + entry_point="mfkey_main", + requires=[ + "gui", + "storage", + ], + stack_size=1 * 1024, + fap_icon="mfkey.png", + fap_category="NFC", + fap_author="@noproto", + fap_icon_assets="images", + fap_weburl="https://github.com/noproto/FlipperMfkey", + fap_description="MIFARE Classic key recovery tool", + fap_version="3.0", +) + +App( + appid="mfkey_init_plugin", + apptype=FlipperAppType.PLUGIN, + entry_point="init_plugin_ep", + requires=["mfkey"], + sources=["init_plugin.c"], + fal_embedded=True, +) diff --git a/applications/system/mfkey/crypto1.c b/applications/system/mfkey/crypto1.c new file mode 100644 index 000000000..e862b14d1 --- /dev/null +++ b/applications/system/mfkey/crypto1.c @@ -0,0 +1,22 @@ +#pragma GCC optimize("O3") +#pragma GCC optimize("-funroll-all-loops") + +#include +#include "crypto1.h" +#include "mfkey.h" + +#define BIT(x, n) ((x) >> (n) & 1) + +void crypto1_get_lfsr(struct Crypto1State* state, MfClassicKey* lfsr) { + int i; + uint64_t lfsr_value = 0; + for(i = 23; i >= 0; --i) { + lfsr_value = lfsr_value << 1 | BIT(state->odd, i ^ 3); + lfsr_value = lfsr_value << 1 | BIT(state->even, i ^ 3); + } + + // Assign the key value to the MfClassicKey struct + for(i = 0; i < 6; ++i) { + lfsr->data[i] = (lfsr_value >> ((5 - i) * 8)) & 0xFF; + } +} diff --git a/applications/system/mfkey/crypto1.h b/applications/system/mfkey/crypto1.h new file mode 100644 index 000000000..9caf5b35e --- /dev/null +++ b/applications/system/mfkey/crypto1.h @@ -0,0 +1,256 @@ +#ifndef CRYPTO1_H +#define CRYPTO1_H + +#include +#include "mfkey.h" +#include +#include + +#define LF_POLY_ODD (0x29CE5C) +#define LF_POLY_EVEN (0x870804) +#define BIT(x, n) ((x) >> (n) & 1) +#define BEBIT(x, n) BIT(x, (n) ^ 24) +#define SWAPENDIAN(x) \ + ((x) = ((x) >> 8 & 0xff00ff) | ((x) & 0xff00ff) << 8, (x) = (x) >> 16 | (x) << 16) + +static inline uint32_t prng_successor(uint32_t x, uint32_t n); +static inline int filter(uint32_t const x); +static inline uint8_t evenparity32(uint32_t x); +static inline void update_contribution(unsigned int data[], int item, int mask1, int mask2); +void crypto1_get_lfsr(struct Crypto1State* state, MfClassicKey* lfsr); +static inline uint32_t crypt_word(struct Crypto1State* s); +static inline void crypt_word_noret(struct Crypto1State* s, uint32_t in, int x); +static inline uint32_t crypt_word_ret(struct Crypto1State* s, uint32_t in, int x); +static uint32_t crypt_word_par( + struct Crypto1State* s, + uint32_t in, + int is_encrypted, + uint32_t nt_plain, + uint8_t* parity_keystream_bits); +static inline void rollback_word_noret(struct Crypto1State* s, uint32_t in, int x); +static inline uint8_t napi_lfsr_rollback_bit(struct Crypto1State* s, uint32_t in, int fb); +static inline uint32_t napi_lfsr_rollback_word(struct Crypto1State* s, uint32_t in, int fb); + +static const uint8_t lookup1[256] = { + 0, 0, 16, 16, 0, 16, 0, 0, 0, 16, 0, 0, 16, 16, 16, 16, 0, 0, 16, 16, 0, 16, 0, 0, + 0, 16, 0, 0, 16, 16, 16, 16, 0, 0, 16, 16, 0, 16, 0, 0, 0, 16, 0, 0, 16, 16, 16, 16, + 8, 8, 24, 24, 8, 24, 8, 8, 8, 24, 8, 8, 24, 24, 24, 24, 8, 8, 24, 24, 8, 24, 8, 8, + 8, 24, 8, 8, 24, 24, 24, 24, 8, 8, 24, 24, 8, 24, 8, 8, 8, 24, 8, 8, 24, 24, 24, 24, + 0, 0, 16, 16, 0, 16, 0, 0, 0, 16, 0, 0, 16, 16, 16, 16, 0, 0, 16, 16, 0, 16, 0, 0, + 0, 16, 0, 0, 16, 16, 16, 16, 8, 8, 24, 24, 8, 24, 8, 8, 8, 24, 8, 8, 24, 24, 24, 24, + 0, 0, 16, 16, 0, 16, 0, 0, 0, 16, 0, 0, 16, 16, 16, 16, 0, 0, 16, 16, 0, 16, 0, 0, + 0, 16, 0, 0, 16, 16, 16, 16, 8, 8, 24, 24, 8, 24, 8, 8, 8, 24, 8, 8, 24, 24, 24, 24, + 8, 8, 24, 24, 8, 24, 8, 8, 8, 24, 8, 8, 24, 24, 24, 24, 0, 0, 16, 16, 0, 16, 0, 0, + 0, 16, 0, 0, 16, 16, 16, 16, 8, 8, 24, 24, 8, 24, 8, 8, 8, 24, 8, 8, 24, 24, 24, 24, + 8, 8, 24, 24, 8, 24, 8, 8, 8, 24, 8, 8, 24, 24, 24, 24}; +static const uint8_t lookup2[256] = { + 0, 0, 4, 4, 0, 4, 0, 0, 0, 4, 0, 0, 4, 4, 4, 4, 0, 0, 4, 4, 0, 4, 0, 0, 0, 4, 0, 0, 4, + 4, 4, 4, 2, 2, 6, 6, 2, 6, 2, 2, 2, 6, 2, 2, 6, 6, 6, 6, 2, 2, 6, 6, 2, 6, 2, 2, 2, 6, + 2, 2, 6, 6, 6, 6, 0, 0, 4, 4, 0, 4, 0, 0, 0, 4, 0, 0, 4, 4, 4, 4, 2, 2, 6, 6, 2, 6, 2, + 2, 2, 6, 2, 2, 6, 6, 6, 6, 0, 0, 4, 4, 0, 4, 0, 0, 0, 4, 0, 0, 4, 4, 4, 4, 0, 0, 4, 4, + 0, 4, 0, 0, 0, 4, 0, 0, 4, 4, 4, 4, 0, 0, 4, 4, 0, 4, 0, 0, 0, 4, 0, 0, 4, 4, 4, 4, 2, + 2, 6, 6, 2, 6, 2, 2, 2, 6, 2, 2, 6, 6, 6, 6, 0, 0, 4, 4, 0, 4, 0, 0, 0, 4, 0, 0, 4, 4, + 4, 4, 0, 0, 4, 4, 0, 4, 0, 0, 0, 4, 0, 0, 4, 4, 4, 4, 2, 2, 6, 6, 2, 6, 2, 2, 2, 6, 2, + 2, 6, 6, 6, 6, 2, 2, 6, 6, 2, 6, 2, 2, 2, 6, 2, 2, 6, 6, 6, 6, 2, 2, 6, 6, 2, 6, 2, 2, + 2, 6, 2, 2, 6, 6, 6, 6, 2, 2, 6, 6, 2, 6, 2, 2, 2, 6, 2, 2, 6, 6, 6, 6}; + +static inline int filter(uint32_t const x) { + uint32_t f; + f = lookup1[x & 0xff] | lookup2[(x >> 8) & 0xff]; + f |= 0x0d938 >> (x >> 16 & 0xf) & 1; + return BIT(0xEC57E80A, f); +} + +#ifndef __ARM_ARCH_7EM__ +static inline uint8_t evenparity32(uint32_t x) { + return __builtin_parity(x); +} +#endif + +#ifdef __ARM_ARCH_7EM__ +static inline uint8_t evenparity32(uint32_t x) { + uint32_t result; + __asm__ volatile("eor r1, %[x], %[x], lsr #16 \n\t" // r1 = x ^ (x >> 16) + "eor r1, r1, r1, lsr #8 \n\t" // r1 = r1 ^ (r1 >> 8) + "eor r1, r1, r1, lsr #4 \n\t" // r1 = r1 ^ (r1 >> 4) + "eor r1, r1, r1, lsr #2 \n\t" // r1 = r1 ^ (r1 >> 2) + "eor r1, r1, r1, lsr #1 \n\t" // r1 = r1 ^ (r1 >> 1) + "and %[result], r1, #1 \n\t" // result = r1 & 1 + : [result] "=r"(result) + : [x] "r"(x) + : "r1"); + return result; +} +#endif + +static inline void update_contribution(unsigned int data[], int item, int mask1, int mask2) { + int p = data[item] >> 25; + p = p << 1 | evenparity32(data[item] & mask1); + p = p << 1 | evenparity32(data[item] & mask2); + data[item] = p << 24 | (data[item] & 0xffffff); +} + +static inline uint32_t crypt_word(struct Crypto1State* s) { + // "in" and "x" are always 0 (last iteration) + uint32_t res_ret = 0; + uint32_t feedin, t; + for(int i = 0; i <= 31; i++) { + res_ret |= (filter(s->odd) << (24 ^ i)); //-V629 + feedin = LF_POLY_EVEN & s->even; + feedin ^= LF_POLY_ODD & s->odd; + s->even = s->even << 1 | (evenparity32(feedin)); + t = s->odd, s->odd = s->even, s->even = t; + } + return res_ret; +} + +static inline void crypt_word_noret(struct Crypto1State* s, uint32_t in, int x) { + uint8_t ret; + uint32_t feedin, t, next_in; + for(int i = 0; i <= 31; i++) { + next_in = BEBIT(in, i); + ret = filter(s->odd); + feedin = ret & (!!x); + feedin ^= LF_POLY_EVEN & s->even; + feedin ^= LF_POLY_ODD & s->odd; + feedin ^= !!next_in; + s->even = s->even << 1 | (evenparity32(feedin)); + t = s->odd, s->odd = s->even, s->even = t; + } + return; +} + +static inline uint32_t crypt_word_ret(struct Crypto1State* s, uint32_t in, int x) { + uint32_t ret = 0; + uint32_t feedin, t, next_in; + uint8_t next_ret; + for(int i = 0; i <= 31; i++) { + next_in = BEBIT(in, i); + next_ret = filter(s->odd); + feedin = next_ret & (!!x); + feedin ^= LF_POLY_EVEN & s->even; + feedin ^= LF_POLY_ODD & s->odd; + feedin ^= !!next_in; + s->even = s->even << 1 | (evenparity32(feedin)); + t = s->odd, s->odd = s->even, s->even = t; + ret |= next_ret << (24 ^ i); + } + return ret; +} + +static uint8_t get_nth_byte(uint32_t value, int n) { + if(n < 0 || n > 3) { + // Handle invalid input + return 0; + } + return (value >> (8 * (3 - n))) & 0xFF; +} + +static uint8_t crypt_bit(struct Crypto1State* s, uint8_t in, int is_encrypted) { + uint32_t feedin, t; + uint8_t ret = filter(s->odd); + feedin = ret & !!is_encrypted; + feedin ^= !!in; + feedin ^= LF_POLY_ODD & s->odd; + feedin ^= LF_POLY_EVEN & s->even; + s->even = s->even << 1 | evenparity32(feedin); + t = s->odd, s->odd = s->even, s->even = t; + return ret; +} + +static inline uint32_t crypt_word_par( + struct Crypto1State* s, + uint32_t in, + int is_encrypted, + uint32_t nt_plain, + uint8_t* parity_keystream_bits) { + uint32_t ret = 0; + *parity_keystream_bits = 0; // Reset parity keystream bits + + for(int i = 0; i < 32; i++) { + uint8_t bit = crypt_bit(s, BEBIT(in, i), is_encrypted); + ret |= bit << (24 ^ i); + // Save keystream parity bit + if((i + 1) % 8 == 0) { + *parity_keystream_bits |= + (filter(s->odd) ^ nfc_util_even_parity8(get_nth_byte(nt_plain, i / 8))) + << (3 - (i / 8)); + } + } + return ret; +} + +static inline void rollback_word_noret(struct Crypto1State* s, uint32_t in, int x) { + uint8_t ret; + uint32_t feedin, t, next_in; + for(int i = 31; i >= 0; i--) { + next_in = BEBIT(in, i); + s->odd &= 0xffffff; + t = s->odd, s->odd = s->even, s->even = t; + ret = filter(s->odd); + feedin = ret & (!!x); + feedin ^= s->even & 1; + feedin ^= LF_POLY_EVEN & (s->even >>= 1); + feedin ^= LF_POLY_ODD & s->odd; + feedin ^= !!next_in; + s->even |= (evenparity32(feedin)) << 23; + } + return; +} + +// TODO: +/* +uint32_t rollback_word(struct Crypto1State *s, uint32_t in, int x) { + uint32_t res_ret = 0; + uint8_t ret; + uint32_t feedin, t, next_in; + for (int i = 31; i >= 0; i--) { + next_in = BEBIT(in, i); + s->odd &= 0xffffff; + t = s->odd, s->odd = s->even, s->even = t; + ret = filter(s->odd); + feedin = ret & (!!x); + feedin ^= s->even & 1; + feedin ^= LF_POLY_EVEN & (s->even >>= 1); + feedin ^= LF_POLY_ODD & s->odd; + feedin ^= !!next_in; + s->even |= (evenparity32(feedin)) << 23; + res_ret |= (ret << (24 ^ i)); + } + return res_ret; +} +*/ + +uint8_t napi_lfsr_rollback_bit(struct Crypto1State* s, uint32_t in, int fb) { + int out; + uint8_t ret; + uint32_t t; + s->odd &= 0xffffff; + t = s->odd, s->odd = s->even, s->even = t; + + out = s->even & 1; + out ^= LF_POLY_EVEN & (s->even >>= 1); + out ^= LF_POLY_ODD & s->odd; + out ^= !!in; + out ^= (ret = filter(s->odd)) & !!fb; + + s->even |= evenparity32(out) << 23; + return ret; +} + +uint32_t napi_lfsr_rollback_word(struct Crypto1State* s, uint32_t in, int fb) { + int i; + uint32_t ret = 0; + for(i = 31; i >= 0; --i) + ret |= napi_lfsr_rollback_bit(s, BEBIT(in, i), fb) << (i ^ 24); + return ret; +} + +static inline uint32_t prng_successor(uint32_t x, uint32_t n) { + SWAPENDIAN(x); + while(n--) + x = x >> 1 | (x >> 16 ^ x >> 18 ^ x >> 19 ^ x >> 21) << 31; + return SWAPENDIAN(x); +} + +#endif // CRYPTO1_H diff --git a/applications/system/mfkey/images/mfkey.png b/applications/system/mfkey/images/mfkey.png new file mode 100644 index 0000000000000000000000000000000000000000..f1694bb335a8619e53cd3b98651ba995cc7a1caf GIT binary patch literal 107 zcmeAS@N?(olHy`uVBq!ia0vp^AT}2xGmzZ=C-xtZVhivIasB`QKad%E=yDy9Qt)(f z4B@z*{KD=)L3iUrapuFX*xK~i1d{R-5*QT~m>F7Tu$Q|1zuOE{%i!ti=d#Wzp$P!I C{~rec literal 0 HcmV?d00001 diff --git a/applications/system/mfkey/init_plugin.c b/applications/system/mfkey/init_plugin.c new file mode 100644 index 000000000..8540a8f2d --- /dev/null +++ b/applications/system/mfkey/init_plugin.c @@ -0,0 +1,356 @@ +#include +#include +#include +#include +#include +#include +#include "mfkey.h" +#include "crypto1.h" +#include "plugin_interface.h" +#include + +#define TAG "MFKey" + +// TODO: Remove defines that are not needed +#define MF_CLASSIC_NONCE_PATH EXT_PATH("nfc/.mfkey32.log") +#define MF_CLASSIC_NESTED_NONCE_PATH EXT_PATH("nfc/.nested.log") +#define MAX_NAME_LEN 32 +#define MAX_PATH_LEN 64 + +#define LF_POLY_ODD (0x29CE5C) +#define LF_POLY_EVEN (0x870804) +#define CONST_M1_1 (LF_POLY_EVEN << 1 | 1) +#define CONST_M2_1 (LF_POLY_ODD << 1) +#define CONST_M1_2 (LF_POLY_ODD) +#define CONST_M2_2 (LF_POLY_EVEN << 1 | 1) +#define BIT(x, n) ((x) >> (n) & 1) +#define BEBIT(x, n) BIT(x, (n) ^ 24) +#define SWAPENDIAN(x) \ + ((x) = ((x) >> 8 & 0xff00ff) | ((x) & 0xff00ff) << 8, (x) = (x) >> 16 | (x) << 16) + +bool key_already_found_for_nonce_in_dict(KeysDict* dict, MfClassicNonce* nonce) { + // This function must not be passed the CUID dictionary + bool found = false; + uint8_t key_bytes[sizeof(MfClassicKey)]; + keys_dict_rewind(dict); + while(keys_dict_get_next_key(dict, key_bytes, sizeof(MfClassicKey))) { + uint64_t k = bit_lib_bytes_to_num_be(key_bytes, sizeof(MfClassicKey)); + struct Crypto1State temp = {0, 0}; + for(int i = 0; i < 24; i++) { + (&temp)->odd |= (BIT(k, 2 * i + 1) << (i ^ 3)); + (&temp)->even |= (BIT(k, 2 * i) << (i ^ 3)); + } + if(nonce->attack == mfkey32) { + crypt_word_noret(&temp, nonce->uid_xor_nt1, 0); + crypt_word_noret(&temp, nonce->nr1_enc, 1); + if(nonce->ar1_enc == (crypt_word(&temp) ^ nonce->p64b)) { + found = true; + break; + } + } else if(nonce->attack == static_nested || nonce->attack == static_encrypted) { + uint32_t expected_ks1 = crypt_word_ret(&temp, nonce->uid_xor_nt0, 0); + if(nonce->ks1_1_enc == expected_ks1) { + found = true; + break; + } + } + } + return found; +} + +bool napi_mf_classic_mfkey32_nonces_check_presence() { + Storage* storage = furi_record_open(RECORD_STORAGE); + + bool nonces_present = storage_common_stat(storage, MF_CLASSIC_NONCE_PATH, NULL) == FSE_OK; + + furi_record_close(RECORD_STORAGE); + + return nonces_present; +} + +bool napi_mf_classic_nested_nonces_check_presence() { + Storage* storage = furi_record_open(RECORD_STORAGE); + Stream* stream = buffered_file_stream_alloc(storage); + bool nonces_present = false; + FuriString* line = furi_string_alloc(); + + do { + if(!buffered_file_stream_open( + stream, MF_CLASSIC_NESTED_NONCE_PATH, FSAM_READ, FSOM_OPEN_EXISTING)) { + break; + } + + while(stream_read_line(stream, line)) { + if(furi_string_search_str(line, "dist 0") != FURI_STRING_FAILURE) { + nonces_present = true; + break; + } + } + + } while(false); + + furi_string_free(line); + buffered_file_stream_close(stream); + stream_free(stream); + furi_record_close(RECORD_STORAGE); + + return nonces_present; +} + +int binaryStringToInt(const char* binStr) { + int result = 0; + while(*binStr) { + result <<= 1; + if(*binStr == '1') { + result |= 1; + } + binStr++; + } + return result; +} + +bool load_mfkey32_nonces( + MfClassicNonceArray* nonce_array, + ProgramState* program_state, + KeysDict* system_dict, + bool system_dict_exists, + KeysDict* user_dict) { + bool array_loaded = false; + + do { + // https://github.com/flipperdevices/flipperzero-firmware/blob/5134f44c09d39344a8747655c0d59864bb574b96/applications/services/storage/filesystem_api_defines.h#L8-L22 + if(!buffered_file_stream_open( + nonce_array->stream, MF_CLASSIC_NONCE_PATH, FSAM_READ_WRITE, FSOM_OPEN_EXISTING)) { + buffered_file_stream_close(nonce_array->stream); + break; + } + + // Check for newline ending + if(!stream_eof(nonce_array->stream)) { + if(!stream_seek(nonce_array->stream, -1, StreamOffsetFromEnd)) break; + uint8_t last_char = 0; + if(stream_read(nonce_array->stream, &last_char, 1) != 1) break; + if(last_char != '\n') { + //FURI_LOG_D(TAG, "Adding new line ending"); + if(stream_write_char(nonce_array->stream, '\n') != 1) break; + } + if(!stream_rewind(nonce_array->stream)) break; + } + + // Read total amount of nonces + FuriString* next_line; + next_line = furi_string_alloc(); + while(!(program_state->close_thread_please)) { + if(!stream_read_line(nonce_array->stream, next_line)) { + //FURI_LOG_T(TAG, "No nonces left"); + break; + } + /* + FURI_LOG_T( + TAG, + "Read line: %s, len: %zu", + furi_string_get_cstr(next_line), + furi_string_size(next_line)); + */ + if(!furi_string_start_with_str(next_line, "Sec")) continue; + const char* next_line_cstr = furi_string_get_cstr(next_line); + MfClassicNonce res = {0}; + res.attack = mfkey32; + int i = 0; + char* endptr; + for(i = 0; i <= 17; i++) { + if(i != 0) { + next_line_cstr = strchr(next_line_cstr, ' '); + if(next_line_cstr) { + next_line_cstr++; + } else { + break; + } + } + unsigned long value = strtoul(next_line_cstr, &endptr, 16); + switch(i) { + case 5: + res.uid = value; + break; + case 7: + res.nt0 = value; + break; + case 9: + res.nr0_enc = value; + break; + case 11: + res.ar0_enc = value; + break; + case 13: + res.nt1 = value; + break; + case 15: + res.nr1_enc = value; + break; + case 17: + res.ar1_enc = value; + break; + default: + break; // Do nothing + } + next_line_cstr = endptr; + } + res.p64 = prng_successor(res.nt0, 64); + res.p64b = prng_successor(res.nt1, 64); + res.uid_xor_nt0 = res.uid ^ res.nt0; + res.uid_xor_nt1 = res.uid ^ res.nt1; + + (program_state->total)++; + if((system_dict_exists && key_already_found_for_nonce_in_dict(system_dict, &res)) || + (key_already_found_for_nonce_in_dict(user_dict, &res))) { + (program_state->cracked)++; + (program_state->num_completed)++; + continue; + } + //FURI_LOG_I(TAG, "No key found for %8lx %8lx", res.uid, res.ar1_enc); + // TODO: Refactor + nonce_array->remaining_nonce_array = realloc( //-V701 + nonce_array->remaining_nonce_array, + sizeof(MfClassicNonce) * ((nonce_array->remaining_nonces) + 1)); + nonce_array->remaining_nonces++; + nonce_array->remaining_nonce_array[(nonce_array->remaining_nonces) - 1] = res; + nonce_array->total_nonces++; + } + furi_string_free(next_line); + buffered_file_stream_close(nonce_array->stream); + + array_loaded = true; + //FURI_LOG_I(TAG, "Loaded %lu Mfkey32 nonces", nonce_array->total_nonces); + } while(false); + + return array_loaded; +} + +bool load_nested_nonces( + MfClassicNonceArray* nonce_array, + ProgramState* program_state, + KeysDict* system_dict, + bool system_dict_exists, + KeysDict* user_dict) { + if(!buffered_file_stream_open( + nonce_array->stream, MF_CLASSIC_NESTED_NONCE_PATH, FSAM_READ, FSOM_OPEN_EXISTING)) { + return false; + } + + FuriString* next_line = furi_string_alloc(); + bool array_loaded = false; + + while(stream_read_line(nonce_array->stream, next_line)) { + const char* line = furi_string_get_cstr(next_line); + + // Only process lines ending with "dist 0" + if(!strstr(line, "dist 0")) { + continue; + } + + MfClassicNonce res = {0}; + res.attack = static_encrypted; + + int parsed = sscanf( + line, + "Sec %*d key %*c cuid %" PRIx32 " nt0 %" PRIx32 " ks0 %" PRIx32 + " par0 %4[01] nt1 %" PRIx32 " ks1 %" PRIx32 " par1 %4[01]", + &res.uid, + &res.nt0, + &res.ks1_1_enc, + res.par_1_str, + &res.nt1, + &res.ks1_2_enc, + res.par_2_str); + + if(parsed >= 4) { // At least one nonce is present + res.par_1 = binaryStringToInt(res.par_1_str); + res.uid_xor_nt0 = res.uid ^ res.nt0; + + if(parsed == 7) { // Both nonces are present + res.attack = static_nested; + res.par_2 = binaryStringToInt(res.par_2_str); + res.uid_xor_nt1 = res.uid ^ res.nt1; + } + + (program_state->total)++; + if((system_dict_exists && key_already_found_for_nonce_in_dict(system_dict, &res)) || + (key_already_found_for_nonce_in_dict(user_dict, &res))) { + (program_state->cracked)++; + (program_state->num_completed)++; + continue; + } + + nonce_array->remaining_nonce_array = realloc( + nonce_array->remaining_nonce_array, + sizeof(MfClassicNonce) * (nonce_array->remaining_nonces + 1)); + nonce_array->remaining_nonce_array[nonce_array->remaining_nonces] = res; + nonce_array->remaining_nonces++; + nonce_array->total_nonces++; + array_loaded = true; + } + } + + furi_string_free(next_line); + buffered_file_stream_close(nonce_array->stream); + + //FURI_LOG_I(TAG, "Loaded %lu Static Nested nonces", nonce_array->total_nonces); + return array_loaded; +} + +MfClassicNonceArray* napi_mf_classic_nonce_array_alloc( + KeysDict* system_dict, + bool system_dict_exists, + KeysDict* user_dict, + ProgramState* program_state) { + MfClassicNonceArray* nonce_array = malloc(sizeof(MfClassicNonceArray)); + MfClassicNonce* remaining_nonce_array_init = malloc(sizeof(MfClassicNonce) * 1); + nonce_array->remaining_nonce_array = remaining_nonce_array_init; + Storage* storage = furi_record_open(RECORD_STORAGE); + nonce_array->stream = buffered_file_stream_alloc(storage); + furi_record_close(RECORD_STORAGE); + + if(program_state->mfkey32_present) { + load_mfkey32_nonces( + nonce_array, program_state, system_dict, system_dict_exists, user_dict); + } + + if(program_state->nested_present) { + load_nested_nonces(nonce_array, program_state, system_dict, system_dict_exists, user_dict); + } + + return nonce_array; +} + +void napi_mf_classic_nonce_array_free(MfClassicNonceArray* nonce_array) { + // TODO: Track free state at the time this is called to ensure double free does not happen + furi_assert(nonce_array); + furi_assert(nonce_array->stream); + + // TODO: Already closed? + buffered_file_stream_close(nonce_array->stream); + stream_free(nonce_array->stream); + free(nonce_array); +} + +/* Actual implementation of app<>plugin interface */ +static const MfkeyPlugin init_plugin = { + .name = "Initialization Plugin", + .napi_mf_classic_mfkey32_nonces_check_presence = + &napi_mf_classic_mfkey32_nonces_check_presence, + .napi_mf_classic_nested_nonces_check_presence = &napi_mf_classic_nested_nonces_check_presence, + .napi_mf_classic_nonce_array_alloc = &napi_mf_classic_nonce_array_alloc, + .napi_mf_classic_nonce_array_free = &napi_mf_classic_nonce_array_free, +}; + +/* Plugin descriptor to comply with basic plugin specification */ +static const FlipperAppPluginDescriptor init_plugin_descriptor = { + .appid = PLUGIN_APP_ID, + .ep_api_version = PLUGIN_API_VERSION, + .entry_point = &init_plugin, +}; + +/* Plugin entry point - must return a pointer to const descriptor */ +const FlipperAppPluginDescriptor* init_plugin_ep() { + return &init_plugin_descriptor; +} diff --git a/applications/system/mfkey/mfkey.c b/applications/system/mfkey/mfkey.c new file mode 100644 index 000000000..ae5cc90f2 --- /dev/null +++ b/applications/system/mfkey/mfkey.c @@ -0,0 +1,915 @@ +#pragma GCC optimize("O3") +#pragma GCC optimize("-funroll-all-loops") + +// TODO: More efficient dictionary bruteforce by scanning through hardcoded very common keys and previously found dictionary keys first? +// (a cache for key_already_found_for_nonce_in_dict) +// TODO: Selectively unroll loops to reduce binary size +// TODO: Collect parity during Mfkey32 attacks to further optimize the attack +// TODO: Why different sscanf between Mfkey32 and Nested? +// TODO: "Read tag again with NFC app" message upon completion, "Complete. Keys added: " +// TODO: Separate Mfkey32 and Nested functions where possible to reduce branch statements +// TODO: Find ~1 KB memory leak +// TODO: Use seednt16 to reduce static encrypted key candidates: https://gist.github.com/noproto/8102f8f32546564cd674256e62ff76ea +// https://eprint.iacr.org/2024/1275.pdf section X +// TODO: Static Encrypted: Minimum RAM for adding to keys dict (avoid crashes) +// TODO: Static Encrypted: Optimize KeysDict or buffer keys to write in chunks + +#include +#include +#include +#include +#include "mfkey_icons.h" +#include +#include +#include +#include +#include +#include +#include +#include "mfkey.h" +#include "crypto1.h" +#include "plugin_interface.h" +#include +#include +#include + +#define TAG "MFKey" + +// TODO: Remove defines that are not needed +#define KEYS_DICT_SYSTEM_PATH EXT_PATH("nfc/assets/mf_classic_dict.nfc") +#define KEYS_DICT_USER_PATH EXT_PATH("nfc/assets/mf_classic_dict_user.nfc") +#define MAX_NAME_LEN 32 +#define MAX_PATH_LEN 64 + +#define LF_POLY_ODD (0x29CE5C) +#define LF_POLY_EVEN (0x870804) +#define CONST_M1_1 (LF_POLY_EVEN << 1 | 1) +#define CONST_M2_1 (LF_POLY_ODD << 1) +#define CONST_M1_2 (LF_POLY_ODD) +#define CONST_M2_2 (LF_POLY_EVEN << 1 | 1) +#define BIT(x, n) ((x) >> (n) & 1) +#define BEBIT(x, n) BIT(x, (n) ^ 24) +#define SWAPENDIAN(x) \ + ((x) = ((x) >> 8 & 0xff00ff) | ((x) & 0xff00ff) << 8, (x) = (x) >> 16 | (x) << 16) +//#define SIZEOF(arr) sizeof(arr) / sizeof(*arr) + +static int eta_round_time = 44; +static int eta_total_time = 705; +// MSB_LIMIT: Chunk size (out of 256) +static int MSB_LIMIT = 16; + +static inline int + check_state(struct Crypto1State* t, MfClassicNonce* n, ProgramState* program_state) { + if(!(t->odd | t->even)) return 0; + if(n->attack == mfkey32) { + uint32_t rb = (napi_lfsr_rollback_word(t, 0, 0) ^ n->p64); + if(rb != n->ar0_enc) { + return 0; + } + rollback_word_noret(t, n->nr0_enc, 1); + rollback_word_noret(t, n->uid_xor_nt0, 0); + struct Crypto1State temp = {t->odd, t->even}; + crypt_word_noret(t, n->uid_xor_nt1, 0); + crypt_word_noret(t, n->nr1_enc, 1); + if(n->ar1_enc == (crypt_word(t) ^ n->p64b)) { + crypto1_get_lfsr(&temp, &(n->key)); + return 1; + } + } else if(n->attack == static_nested) { + struct Crypto1State temp = {t->odd, t->even}; + rollback_word_noret(t, n->uid_xor_nt1, 0); + if(n->ks1_1_enc == crypt_word_ret(t, n->uid_xor_nt0, 0)) { + rollback_word_noret(&temp, n->uid_xor_nt1, 0); + crypto1_get_lfsr(&temp, &(n->key)); + return 1; + } + } else if(n->attack == static_encrypted) { + // TODO: Parity bits from rollback_word? + if(n->ks1_1_enc == napi_lfsr_rollback_word(t, n->uid_xor_nt0, 0)) { + // Reduce with parity + uint8_t local_parity_keystream_bits; + struct Crypto1State temp = {t->odd, t->even}; + if((crypt_word_par(&temp, n->uid_xor_nt0, 0, n->nt0, &local_parity_keystream_bits) == + n->ks1_1_enc) && + (local_parity_keystream_bits == n->par_1)) { + // Found key candidate + crypto1_get_lfsr(t, &(n->key)); + program_state->num_candidates++; + keys_dict_add_key(program_state->cuid_dict, n->key.data, sizeof(MfClassicKey)); + } + } + } + return 0; +} + +static inline int state_loop( + unsigned int* states_buffer, + int xks, + int m1, + int m2, + unsigned int in, + uint8_t and_val) { + int states_tail = 0; + int round = 0, s = 0, xks_bit = 0, round_in = 0; + + for(round = 1; round <= 12; round++) { + xks_bit = BIT(xks, round); + if(round > 4) { + round_in = ((in >> (2 * (round - 4))) & and_val) << 24; + } + + for(s = 0; s <= states_tail; s++) { + states_buffer[s] <<= 1; + + if((filter(states_buffer[s]) ^ filter(states_buffer[s] | 1)) != 0) { + states_buffer[s] |= filter(states_buffer[s]) ^ xks_bit; + if(round > 4) { + update_contribution(states_buffer, s, m1, m2); + states_buffer[s] ^= round_in; + } + } else if(filter(states_buffer[s]) == xks_bit) { + // TODO: Refactor + if(round > 4) { + states_buffer[++states_tail] = states_buffer[s + 1]; + states_buffer[s + 1] = states_buffer[s] | 1; + update_contribution(states_buffer, s, m1, m2); + states_buffer[s++] ^= round_in; + update_contribution(states_buffer, s, m1, m2); + states_buffer[s] ^= round_in; + } else { + states_buffer[++states_tail] = states_buffer[++s]; + states_buffer[s] = states_buffer[s - 1] | 1; + } + } else { + states_buffer[s--] = states_buffer[states_tail--]; + } + } + } + + return states_tail; +} + +int binsearch(unsigned int data[], int start, int stop) { + int mid, val = data[stop] & 0xff000000; + while(start != stop) { + mid = (stop - start) >> 1; + if((data[start + mid] ^ 0x80000000) > (val ^ 0x80000000)) + stop = start + mid; + else + start += mid + 1; + } + return start; +} +void quicksort(unsigned int array[], int low, int high) { + //if (SIZEOF(array) == 0) + // return; + if(low >= high) return; + int middle = low + (high - low) / 2; + unsigned int pivot = array[middle]; + int i = low, j = high; + while(i <= j) { + while(array[i] < pivot) { + i++; + } + while(array[j] > pivot) { + j--; + } + if(i <= j) { // swap + int temp = array[i]; + array[i] = array[j]; + array[j] = temp; + i++; + j--; + } + } + if(low < j) { + quicksort(array, low, j); + } + if(high > i) { + quicksort(array, i, high); + } +} +int extend_table(unsigned int data[], int tbl, int end, int bit, int m1, int m2, unsigned int in) { + in <<= 24; + for(data[tbl] <<= 1; tbl <= end; data[++tbl] <<= 1) { + if((filter(data[tbl]) ^ filter(data[tbl] | 1)) != 0) { + data[tbl] |= filter(data[tbl]) ^ bit; + update_contribution(data, tbl, m1, m2); + data[tbl] ^= in; + } else if(filter(data[tbl]) == bit) { + data[++end] = data[tbl + 1]; + data[tbl + 1] = data[tbl] | 1; + update_contribution(data, tbl, m1, m2); + data[tbl++] ^= in; + update_contribution(data, tbl, m1, m2); + data[tbl] ^= in; + } else { + data[tbl--] = data[end--]; + } + } + return end; +} + +int old_recover( + unsigned int odd[], + int o_head, + int o_tail, + int oks, + unsigned int even[], + int e_head, + int e_tail, + int eks, + int rem, + int s, + MfClassicNonce* n, + unsigned int in, + int first_run, + ProgramState* program_state) { + int o, e, i; + if(rem == -1) { + for(e = e_head; e <= e_tail; ++e) { + even[e] = (even[e] << 1) ^ evenparity32(even[e] & LF_POLY_EVEN) ^ (!!(in & 4)); + for(o = o_head; o <= o_tail; ++o, ++s) { + struct Crypto1State temp = {0, 0}; + temp.even = odd[o]; + temp.odd = even[e] ^ evenparity32(odd[o] & LF_POLY_ODD); + if(check_state(&temp, n, program_state)) { + return -1; + } + } + } + return s; + } + if(first_run == 0) { + for(i = 0; (i < 4) && (rem-- != 0); i++) { + oks >>= 1; + eks >>= 1; + in >>= 2; + o_tail = extend_table( + odd, o_head, o_tail, oks & 1, LF_POLY_EVEN << 1 | 1, LF_POLY_ODD << 1, 0); + if(o_head > o_tail) return s; + e_tail = extend_table( + even, e_head, e_tail, eks & 1, LF_POLY_ODD, LF_POLY_EVEN << 1 | 1, in & 3); + if(e_head > e_tail) return s; + } + } + first_run = 0; + quicksort(odd, o_head, o_tail); + quicksort(even, e_head, e_tail); + while(o_tail >= o_head && e_tail >= e_head) { + if(((odd[o_tail] ^ even[e_tail]) >> 24) == 0) { + o_tail = binsearch(odd, o_head, o = o_tail); + e_tail = binsearch(even, e_head, e = e_tail); + s = old_recover( + odd, + o_tail--, + o, + oks, + even, + e_tail--, + e, + eks, + rem, + s, + n, + in, + first_run, + program_state); + if(s == -1) { + break; + } + } else if((odd[o_tail] ^ 0x80000000) > (even[e_tail] ^ 0x80000000)) { + o_tail = binsearch(odd, o_head, o_tail) - 1; + } else { + e_tail = binsearch(even, e_head, e_tail) - 1; + } + } + return s; +} + +static inline int sync_state(ProgramState* program_state) { + int ts = furi_hal_rtc_get_timestamp(); + int elapsed_time = ts - program_state->eta_timestamp; + if(elapsed_time < program_state->eta_round) { + program_state->eta_round -= elapsed_time; + } else { + program_state->eta_round = 0; + } + if(elapsed_time < program_state->eta_total) { + program_state->eta_total -= elapsed_time; + } else { + program_state->eta_total = 0; + } + program_state->eta_timestamp = ts; + if(program_state->close_thread_please) { + return 1; + } + return 0; +} + +int calculate_msb_tables( + int oks, + int eks, + int msb_round, + MfClassicNonce* n, + unsigned int* states_buffer, + struct Msb* odd_msbs, + struct Msb* even_msbs, + unsigned int* temp_states_odd, + unsigned int* temp_states_even, + unsigned int in, + ProgramState* program_state) { + //FURI_LOG_I(TAG, "MSB GO %i", msb_iter); // DEBUG + unsigned int msb_head = (MSB_LIMIT * msb_round); // msb_iter ranges from 0 to (256/MSB_LIMIT)-1 + unsigned int msb_tail = (MSB_LIMIT * (msb_round + 1)); + int states_tail = 0, tail = 0; + int i = 0, j = 0, semi_state = 0, found = 0; + unsigned int msb = 0; + in = ((in >> 16 & 0xff) | (in << 16) | (in & 0xff00)) << 1; + // TODO: Why is this necessary? + memset(odd_msbs, 0, MSB_LIMIT * sizeof(struct Msb)); + memset(even_msbs, 0, MSB_LIMIT * sizeof(struct Msb)); + + for(semi_state = 1 << 20; semi_state >= 0; semi_state--) { + if(semi_state % 32768 == 0) { + if(sync_state(program_state) == 1) { + return 0; + } + } + + if(filter(semi_state) == (oks & 1)) { //-V547 + states_buffer[0] = semi_state; + states_tail = state_loop(states_buffer, oks, CONST_M1_1, CONST_M2_1, 0, 0); + + for(i = states_tail; i >= 0; i--) { + msb = states_buffer[i] >> 24; + if((msb >= msb_head) && (msb < msb_tail)) { + found = 0; + for(j = 0; j < odd_msbs[msb - msb_head].tail - 1; j++) { + if(odd_msbs[msb - msb_head].states[j] == states_buffer[i]) { + found = 1; + break; + } + } + + if(!found) { + tail = odd_msbs[msb - msb_head].tail++; + odd_msbs[msb - msb_head].states[tail] = states_buffer[i]; + } + } + } + } + + if(filter(semi_state) == (eks & 1)) { //-V547 + states_buffer[0] = semi_state; + states_tail = state_loop(states_buffer, eks, CONST_M1_2, CONST_M2_2, in, 3); + + for(i = 0; i <= states_tail; i++) { + msb = states_buffer[i] >> 24; + if((msb >= msb_head) && (msb < msb_tail)) { + found = 0; + + for(j = 0; j < even_msbs[msb - msb_head].tail; j++) { + if(even_msbs[msb - msb_head].states[j] == states_buffer[i]) { + found = 1; + break; + } + } + + if(!found) { + tail = even_msbs[msb - msb_head].tail++; + even_msbs[msb - msb_head].states[tail] = states_buffer[i]; + } + } + } + } + } + + oks >>= 12; + eks >>= 12; + + for(i = 0; i < MSB_LIMIT; i++) { + if(sync_state(program_state) == 1) { + return 0; + } + // TODO: Why is this necessary? + memset(temp_states_even, 0, sizeof(unsigned int) * (1280)); + memset(temp_states_odd, 0, sizeof(unsigned int) * (1280)); + memcpy(temp_states_odd, odd_msbs[i].states, odd_msbs[i].tail * sizeof(unsigned int)); + memcpy(temp_states_even, even_msbs[i].states, even_msbs[i].tail * sizeof(unsigned int)); + int res = old_recover( + temp_states_odd, + 0, + odd_msbs[i].tail, + oks, + temp_states_even, + 0, + even_msbs[i].tail, + eks, + 3, + 0, + n, + in >> 16, + 1, + program_state); + if(res == -1) { + return 1; + } + //odd_msbs[i].tail = 0; + //even_msbs[i].tail = 0; + } + + return 0; +} + +void** allocate_blocks(const size_t* block_sizes, int num_blocks) { + void** block_pointers = malloc(num_blocks * sizeof(void*)); + + for(int i = 0; i < num_blocks; i++) { + if(memmgr_heap_get_max_free_block() < block_sizes[i]) { + // Not enough memory, free previously allocated blocks + for(int j = 0; j < i; j++) { + free(block_pointers[j]); + } + free(block_pointers); + return NULL; + } + + block_pointers[i] = malloc(block_sizes[i]); + } + + return block_pointers; +} + +bool is_full_speed() { + return MSB_LIMIT == 16; +} + +bool recover(MfClassicNonce* n, int ks2, unsigned int in, ProgramState* program_state) { + bool found = false; + const size_t block_sizes[] = {49216, 49216, 5120, 5120, 4096}; + const size_t reduced_block_sizes[] = {24608, 24608, 5120, 5120, 4096}; + const int num_blocks = sizeof(block_sizes) / sizeof(block_sizes[0]); + void** block_pointers = allocate_blocks(block_sizes, num_blocks); + if(block_pointers == NULL) { + // System has less than the guaranteed amount of RAM (140 KB) - adjust some parameters to run anyway at half speed + if(is_full_speed()) { + //eta_round_time *= 2; + eta_total_time *= 2; + MSB_LIMIT /= 2; + } + block_pointers = allocate_blocks(reduced_block_sizes, num_blocks); + if(block_pointers == NULL) { + // System has less than 70 KB of RAM - should never happen so we don't reduce speed further + program_state->err = InsufficientRAM; + program_state->mfkey_state = Error; + return false; + } + } + // Adjust estimates for static encrypted attacks + if(n->attack == static_encrypted) { + eta_round_time *= 4; + eta_total_time *= 4; + if(is_full_speed()) { + eta_round_time *= 4; + eta_total_time *= 4; + } + } + struct Msb* odd_msbs = block_pointers[0]; + struct Msb* even_msbs = block_pointers[1]; + unsigned int* temp_states_odd = block_pointers[2]; + unsigned int* temp_states_even = block_pointers[3]; + unsigned int* states_buffer = block_pointers[4]; + int oks = 0, eks = 0; + int i = 0, msb = 0; + for(i = 31; i >= 0; i -= 2) { + oks = oks << 1 | BEBIT(ks2, i); + } + for(i = 30; i >= 0; i -= 2) { + eks = eks << 1 | BEBIT(ks2, i); + } + int bench_start = furi_hal_rtc_get_timestamp(); + program_state->eta_total = eta_total_time; + program_state->eta_timestamp = bench_start; + for(msb = 0; msb <= ((256 / MSB_LIMIT) - 1); msb++) { + program_state->search = msb; + program_state->eta_round = eta_round_time; + program_state->eta_total = eta_total_time - (eta_round_time * msb); + if(calculate_msb_tables( + oks, + eks, + msb, + n, + states_buffer, + odd_msbs, + even_msbs, + temp_states_odd, + temp_states_even, + in, + program_state)) { + //int bench_stop = furi_hal_rtc_get_timestamp(); + //FURI_LOG_I(TAG, "Cracked in %i seconds", bench_stop - bench_start); + found = true; + break; + } + if(program_state->close_thread_please) { + break; + } + } + // Free the allocated blocks + for(int i = 0; i < num_blocks; i++) { + free(block_pointers[i]); + } + free(block_pointers); + return found; +} + +bool key_already_found_for_nonce_in_solved( + MfClassicKey* keyarray, + int keyarray_size, + MfClassicNonce* nonce) { + for(int k = 0; k < keyarray_size; k++) { + uint64_t key_as_int = bit_lib_bytes_to_num_be(keyarray[k].data, sizeof(MfClassicKey)); + struct Crypto1State temp = {0, 0}; + for(int i = 0; i < 24; i++) { + (&temp)->odd |= (BIT(key_as_int, 2 * i + 1) << (i ^ 3)); + (&temp)->even |= (BIT(key_as_int, 2 * i) << (i ^ 3)); + } + if(nonce->attack == mfkey32) { + crypt_word_noret(&temp, nonce->uid_xor_nt1, 0); + crypt_word_noret(&temp, nonce->nr1_enc, 1); + if(nonce->ar1_enc == (crypt_word(&temp) ^ nonce->p64b)) { + return true; + } + } else if(nonce->attack == static_nested) { + uint32_t expected_ks1 = crypt_word_ret(&temp, nonce->uid_xor_nt0, 0); + if(nonce->ks1_1_enc == expected_ks1) { + return true; + } + } + } + return false; +} + +#pragma GCC push_options +#pragma GCC optimize("Os") +static void finished_beep() { + // Beep to indicate completion + NotificationApp* notification = furi_record_open("notification"); + notification_message(notification, &sequence_audiovisual_alert); + notification_message(notification, &sequence_display_backlight_on); + furi_record_close("notification"); +} + +void mfkey(ProgramState* program_state) { + uint32_t ks_enc = 0, nt_xor_uid = 0; + MfClassicKey found_key; // Recovered key + size_t keyarray_size = 0; + MfClassicKey* keyarray = malloc(sizeof(MfClassicKey) * 1); + uint32_t i = 0, j = 0; + //FURI_LOG_I(TAG, "Free heap before alloc(): %zub", memmgr_get_free_heap()); + Storage* storage = furi_record_open(RECORD_STORAGE); + FlipperApplication* app = flipper_application_alloc(storage, firmware_api_interface); + flipper_application_preload(app, APP_ASSETS_PATH("plugins/mfkey_init_plugin.fal")); + flipper_application_map_to_memory(app); + const FlipperAppPluginDescriptor* app_descriptor = + flipper_application_plugin_get_descriptor(app); + const MfkeyPlugin* init_plugin = app_descriptor->entry_point; + // Check for nonces + program_state->mfkey32_present = init_plugin->napi_mf_classic_mfkey32_nonces_check_presence(); + program_state->nested_present = init_plugin->napi_mf_classic_nested_nonces_check_presence(); + if(!(program_state->mfkey32_present) && !(program_state->nested_present)) { + program_state->err = MissingNonces; + program_state->mfkey_state = Error; + flipper_application_free(app); + furi_record_close(RECORD_STORAGE); + free(keyarray); + return; + } + // Read dictionaries (optional) + KeysDict* system_dict = {0}; + bool system_dict_exists = keys_dict_check_presence(KEYS_DICT_SYSTEM_PATH); + KeysDict* user_dict = {0}; + bool user_dict_exists = keys_dict_check_presence(KEYS_DICT_USER_PATH); + uint32_t total_dict_keys = 0; + if(system_dict_exists) { + system_dict = + keys_dict_alloc(KEYS_DICT_SYSTEM_PATH, KeysDictModeOpenExisting, sizeof(MfClassicKey)); + total_dict_keys += keys_dict_get_total_keys(system_dict); + } + user_dict = keys_dict_alloc(KEYS_DICT_USER_PATH, KeysDictModeOpenAlways, sizeof(MfClassicKey)); + if(user_dict_exists) { + total_dict_keys += keys_dict_get_total_keys(user_dict); + } + user_dict_exists = true; + program_state->dict_count = total_dict_keys; + program_state->mfkey_state = DictionaryAttack; + // Read nonces + MfClassicNonceArray* nonce_arr; + nonce_arr = init_plugin->napi_mf_classic_nonce_array_alloc( + system_dict, system_dict_exists, user_dict, program_state); + if(system_dict_exists) { + keys_dict_free(system_dict); + } + if(nonce_arr->total_nonces == 0) { + // Nothing to crack + program_state->err = ZeroNonces; + program_state->mfkey_state = Error; + init_plugin->napi_mf_classic_nonce_array_free(nonce_arr); + flipper_application_free(app); + furi_record_close(RECORD_STORAGE); + keys_dict_free(user_dict); + free(keyarray); + return; + } + flipper_application_free(app); + furi_record_close(RECORD_STORAGE); + // TODO: Track free state at the time this is called to ensure double free does not happen + furi_assert(nonce_arr); + furi_assert(nonce_arr->stream); + // TODO: Already closed? + buffered_file_stream_close(nonce_arr->stream); + stream_free(nonce_arr->stream); + //FURI_LOG_I(TAG, "Free heap after free(): %zub", memmgr_get_free_heap()); + program_state->mfkey_state = MFKeyAttack; + // TODO: Work backwards on this array and free memory + for(i = 0; i < nonce_arr->total_nonces; i++) { + MfClassicNonce next_nonce = nonce_arr->remaining_nonce_array[i]; + if(key_already_found_for_nonce_in_solved(keyarray, keyarray_size, &next_nonce)) { + nonce_arr->remaining_nonces--; + (program_state->cracked)++; + (program_state->num_completed)++; + continue; + } + //FURI_LOG_I(TAG, "Beginning recovery for %8lx", next_nonce.uid); + FuriString* cuid_dict_path; + switch(next_nonce.attack) { + case mfkey32: + ks_enc = next_nonce.ar0_enc ^ next_nonce.p64; + nt_xor_uid = 0; + break; + case static_nested: + ks_enc = next_nonce.ks1_2_enc; + nt_xor_uid = next_nonce.uid_xor_nt1; + break; + case static_encrypted: + ks_enc = next_nonce.ks1_1_enc; + nt_xor_uid = next_nonce.uid_xor_nt0; + cuid_dict_path = furi_string_alloc_printf( + "%s/mf_classic_dict_%08lx.nfc", EXT_PATH("nfc/assets"), next_nonce.uid); + // May need RECORD_STORAGE? + program_state->cuid_dict = keys_dict_alloc( + furi_string_get_cstr(cuid_dict_path), + KeysDictModeOpenAlways, + sizeof(MfClassicKey)); + break; + } + + if(!recover(&next_nonce, ks_enc, nt_xor_uid, program_state)) { + if((next_nonce.attack == static_encrypted) && (program_state->cuid_dict)) { + keys_dict_free(program_state->cuid_dict); + } + if(program_state->close_thread_please) { + break; + } + // No key found in recover() or static encrypted + (program_state->num_completed)++; + continue; + } + (program_state->cracked)++; + (program_state->num_completed)++; + found_key = next_nonce.key; + bool already_found = false; + for(j = 0; j < keyarray_size; j++) { + if(memcmp(keyarray[j].data, found_key.data, MF_CLASSIC_KEY_SIZE) == 0) { + already_found = true; + break; + } + } + if(already_found == false) { + // New key + keyarray = realloc(keyarray, sizeof(MfClassicKey) * (keyarray_size + 1)); //-V701 + keyarray_size += 1; + keyarray[keyarray_size - 1] = found_key; + (program_state->unique_cracked)++; + } + } + // TODO: Update display to show all keys were found + // TODO: Prepend found key(s) to user dictionary file + //FURI_LOG_I(TAG, "Unique keys found:"); + for(i = 0; i < keyarray_size; i++) { + //FURI_LOG_I(TAG, "%012" PRIx64, keyarray[i]); + keys_dict_add_key(user_dict, keyarray[i].data, sizeof(MfClassicKey)); + } + if(keyarray_size > 0) { + dolphin_deed(DolphinDeedNfcMfcAdd); + } + free(nonce_arr); + keys_dict_free(user_dict); + free(keyarray); + if(program_state->mfkey_state == Error) { + return; + } + //FURI_LOG_I(TAG, "mfkey function completed normally"); // DEBUG + program_state->mfkey_state = Complete; + // No need to alert the user if they asked it to stop + if(!(program_state->close_thread_please)) { + finished_beep(); + } + return; +} + +// Screen is 128x64 px +static void render_callback(Canvas* const canvas, void* ctx) { + furi_assert(ctx); + ProgramState* program_state = ctx; + furi_mutex_acquire(program_state->mutex, FuriWaitForever); + char draw_str[44] = {}; + + canvas_draw_frame(canvas, 0, 0, 128, 64); + canvas_draw_frame(canvas, 0, 15, 128, 64); + + // FontSecondary by default, title is drawn at the end + snprintf(draw_str, sizeof(draw_str), "RAM: %zub", memmgr_get_free_heap()); + canvas_draw_str_aligned(canvas, 48, 5, AlignLeft, AlignTop, draw_str); + canvas_draw_icon(canvas, 114, 4, &I_mfkey); + if(program_state->mfkey_state == MFKeyAttack) { + float eta_round = (float)1 - ((float)program_state->eta_round / (float)eta_round_time); + float eta_total = (float)1 - ((float)program_state->eta_total / (float)eta_total_time); + float progress = (float)program_state->num_completed / (float)program_state->total; + if(eta_round < 0 || eta_round > 1) { + // Round ETA miscalculated + eta_round = 1; + program_state->eta_round = 0; + } + if(eta_total < 0 || eta_round > 1) { + // Total ETA miscalculated + eta_total = 1; + program_state->eta_total = 0; + } + snprintf( + draw_str, + sizeof(draw_str), + "Cracking: %d/%d - in prog.", + program_state->num_completed, + program_state->total); + elements_progress_bar_with_text(canvas, 5, 18, 118, progress, draw_str); + snprintf( + draw_str, + sizeof(draw_str), + "Round: %d/%d - ETA %02d Sec", + (program_state->search) + 1, // Zero indexed + 256 / MSB_LIMIT, + program_state->eta_round); + elements_progress_bar_with_text(canvas, 5, 31, 118, eta_round, draw_str); + snprintf(draw_str, sizeof(draw_str), "Total ETA %03d Sec", program_state->eta_total); + elements_progress_bar_with_text(canvas, 5, 44, 118, eta_total, draw_str); + } else if(program_state->mfkey_state == DictionaryAttack) { + snprintf( + draw_str, sizeof(draw_str), "Dict solves: %d (in progress)", program_state->cracked); + canvas_draw_str_aligned(canvas, 10, 18, AlignLeft, AlignTop, draw_str); + snprintf(draw_str, sizeof(draw_str), "Keys in dict: %d", program_state->dict_count); + canvas_draw_str_aligned(canvas, 26, 28, AlignLeft, AlignTop, draw_str); + } else if(program_state->mfkey_state == Complete) { + // TODO: Scrollable list view to see cracked keys if user presses down + elements_progress_bar(canvas, 5, 18, 118, 1); + canvas_draw_str_aligned(canvas, 64, 31, AlignCenter, AlignTop, "Complete"); + snprintf( + draw_str, + sizeof(draw_str), + "Keys added to user dict: %d", + program_state->unique_cracked); + canvas_draw_str_aligned(canvas, 64, 41, AlignCenter, AlignTop, draw_str); + if(program_state->num_candidates > 0) { + snprintf( + draw_str, + sizeof(draw_str), + "SEN key candidates: %d", + program_state->num_candidates); + canvas_draw_str_aligned(canvas, 64, 51, AlignCenter, AlignTop, draw_str); + } + } else if(program_state->mfkey_state == Ready) { + canvas_draw_str_aligned(canvas, 50, 30, AlignLeft, AlignTop, "Ready"); + elements_button_center(canvas, "Start"); + elements_button_right(canvas, "Help"); + } else if(program_state->mfkey_state == Help) { + canvas_draw_str_aligned(canvas, 7, 20, AlignLeft, AlignTop, "Collect nonces by reading"); + canvas_draw_str_aligned(canvas, 7, 30, AlignLeft, AlignTop, "tag or reader in NFC app:"); + canvas_draw_str_aligned(canvas, 7, 40, AlignLeft, AlignTop, "https://docs.flipper.net/"); + canvas_draw_str_aligned(canvas, 7, 50, AlignLeft, AlignTop, "nfc/mfkey32"); + } else if(program_state->mfkey_state == Error) { + canvas_draw_str_aligned(canvas, 50, 25, AlignLeft, AlignTop, "Error"); + if(program_state->err == MissingNonces) { + canvas_draw_str_aligned(canvas, 25, 36, AlignLeft, AlignTop, "No nonces found"); + } else if(program_state->err == ZeroNonces) { + canvas_draw_str_aligned(canvas, 15, 36, AlignLeft, AlignTop, "Nonces already cracked"); + } else if(program_state->err == InsufficientRAM) { + canvas_draw_str_aligned(canvas, 35, 36, AlignLeft, AlignTop, "No free RAM"); + } else { + // Unhandled error + } + } else { + // Unhandled program state + } + // Title + canvas_set_font(canvas, FontPrimary); + canvas_draw_str_aligned(canvas, 5, 4, AlignLeft, AlignTop, "MFKey"); + furi_mutex_release(program_state->mutex); +} + +static void input_callback(InputEvent* input_event, void* event_queue) { + furi_assert(event_queue); + furi_message_queue_put((FuriMessageQueue*)event_queue, input_event, FuriWaitForever); +} + +static void mfkey_state_init(ProgramState* program_state) { + program_state->mfkey_state = Ready; + program_state->cracked = 0; + program_state->unique_cracked = 0; + program_state->num_completed = 0; + program_state->num_candidates = 0; + program_state->total = 0; + program_state->dict_count = 0; +} + +// Entrypoint for worker thread +static int32_t mfkey_worker_thread(void* ctx) { + ProgramState* program_state = ctx; + program_state->mfkey_state = Initializing; + mfkey(program_state); + return 0; +} + +int32_t mfkey_main() { + FuriMessageQueue* event_queue = furi_message_queue_alloc(8, sizeof(InputEvent)); + + ProgramState* program_state = malloc(sizeof(ProgramState)); + + mfkey_state_init(program_state); + + program_state->mutex = furi_mutex_alloc(FuriMutexTypeNormal); + + // Set system callbacks + ViewPort* view_port = view_port_alloc(); + view_port_draw_callback_set(view_port, render_callback, program_state); + view_port_input_callback_set(view_port, input_callback, event_queue); + + // Open GUI and register view_port + Gui* gui = furi_record_open(RECORD_GUI); + gui_add_view_port(gui, view_port, GuiLayerFullscreen); + + program_state->mfkeythread = + furi_thread_alloc_ex("MFKeyWorker", 2048, mfkey_worker_thread, program_state); + + InputEvent input_event; + for(bool main_loop = true; main_loop;) { + FuriStatus event_status = furi_message_queue_get(event_queue, &input_event, 100); + + furi_mutex_acquire(program_state->mutex, FuriWaitForever); + + if(event_status == FuriStatusOk) { + if(input_event.type == InputTypePress) { + switch(input_event.key) { + case InputKeyRight: + if(program_state->mfkey_state == Ready) { + program_state->mfkey_state = Help; + } + break; + case InputKeyOk: + if(program_state->mfkey_state == Ready) { + furi_thread_start(program_state->mfkeythread); + } + break; + case InputKeyBack: + if(program_state->mfkey_state == Help) { + program_state->mfkey_state = Ready; + } else { + program_state->close_thread_please = true; + // Wait until thread is finished + furi_thread_join(program_state->mfkeythread); + main_loop = false; + } + break; + default: + break; + } + } + } + + furi_mutex_release(program_state->mutex); + view_port_update(view_port); + } + + // Thread joined in back event handler + furi_thread_free(program_state->mfkeythread); + view_port_enabled_set(view_port, false); + gui_remove_view_port(gui, view_port); + furi_record_close(RECORD_GUI); + view_port_free(view_port); + furi_message_queue_free(event_queue); + furi_mutex_free(program_state->mutex); + free(program_state); + + return 0; +} +#pragma GCC pop_options diff --git a/applications/system/mfkey/mfkey.h b/applications/system/mfkey/mfkey.h new file mode 100644 index 000000000..4a7ab3423 --- /dev/null +++ b/applications/system/mfkey/mfkey.h @@ -0,0 +1,108 @@ +#ifndef MFKEY_H +#define MFKEY_H + +#include +#include +#include +#include +#include +#include +#include + +struct Crypto1State { + uint32_t odd, even; +}; +struct Msb { + int tail; + uint32_t states[768]; +}; + +typedef enum { + MissingNonces, + ZeroNonces, + InsufficientRAM, +} MFKeyError; + +typedef enum { + Ready, + Initializing, + DictionaryAttack, + MFKeyAttack, + Complete, + Error, + Help, +} MFKeyState; + +// TODO: Can we eliminate any of the members of this struct? +typedef struct { + FuriMutex* mutex; + MFKeyError err; + MFKeyState mfkey_state; + int cracked; + int unique_cracked; + int num_completed; + int num_candidates; + int total; + int dict_count; + int search; + int eta_timestamp; + int eta_total; + int eta_round; + bool mfkey32_present; + bool nested_present; + bool close_thread_please; + FuriThread* mfkeythread; + KeysDict* cuid_dict; +} ProgramState; + +typedef enum { + mfkey32, + static_nested, + static_encrypted +} AttackType; + +typedef struct { + AttackType attack; + MfClassicKey key; // key + uint32_t uid; // serial number + uint32_t nt0; // tag challenge first + uint32_t nt1; // tag challenge second + uint32_t uid_xor_nt0; // uid ^ nt0 + uint32_t uid_xor_nt1; // uid ^ nt1 + union { + // Mfkey32 + struct { + uint32_t p64; // 64th successor of nt0 + uint32_t p64b; // 64th successor of nt1 + uint32_t nr0_enc; // first encrypted reader challenge + uint32_t ar0_enc; // first encrypted reader response + uint32_t nr1_enc; // second encrypted reader challenge + uint32_t ar1_enc; // second encrypted reader response + }; + // Nested + struct { + uint32_t ks1_1_enc; // first encrypted keystream + uint32_t ks1_2_enc; // second encrypted keystream + char par_1_str[5]; // first parity bits (string representation) + char par_2_str[5]; // second parity bits (string representation) + uint8_t par_1; // first parity bits + uint8_t par_2; // second parity bits + }; + }; +} MfClassicNonce; + +typedef struct { + Stream* stream; + uint32_t total_nonces; + MfClassicNonce* remaining_nonce_array; + size_t remaining_nonces; +} MfClassicNonceArray; + +struct KeysDict { + Stream* stream; + size_t key_size; + size_t key_size_symbols; + size_t total_keys; +}; + +#endif // MFKEY_H diff --git a/applications/system/mfkey/mfkey.png b/applications/system/mfkey/mfkey.png new file mode 100644 index 0000000000000000000000000000000000000000..f1694bb335a8619e53cd3b98651ba995cc7a1caf GIT binary patch literal 107 zcmeAS@N?(olHy`uVBq!ia0vp^AT}2xGmzZ=C-xtZVhivIasB`QKad%E=yDy9Qt)(f z4B@z*{KD=)L3iUrapuFX*xK~i1d{R-5*QT~m>F7Tu$Q|1zuOE{%i!ti=d#Wzp$P!I C{~rec literal 0 HcmV?d00001 diff --git a/applications/system/mfkey/plugin_interface.h b/applications/system/mfkey/plugin_interface.h new file mode 100644 index 000000000..e7ca438b8 --- /dev/null +++ b/applications/system/mfkey/plugin_interface.h @@ -0,0 +1,13 @@ +#pragma once + +#define PLUGIN_APP_ID "mfkey" +#define PLUGIN_API_VERSION 1 + +typedef struct { + const char* name; + bool (*napi_mf_classic_mfkey32_nonces_check_presence)(); + bool (*napi_mf_classic_nested_nonces_check_presence)(); + MfClassicNonceArray* ( + *napi_mf_classic_nonce_array_alloc)(KeysDict*, bool, KeysDict*, ProgramState*); + void (*napi_mf_classic_nonce_array_free)(MfClassicNonceArray*); +} MfkeyPlugin; From efcad3a11884d6a971e440f4756692e20269a1e5 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Mon, 7 Apr 2025 18:53:52 +0300 Subject: [PATCH 205/268] ofw pr 4178 [ci skip] add prastel 42 by pmazzini --- lib/subghz/protocols/came.c | 44 +++++++++++++++++++++++-------------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/lib/subghz/protocols/came.c b/lib/subghz/protocols/came.c index 645f5eee1..bbe3e487f 100644 --- a/lib/subghz/protocols/came.c +++ b/lib/subghz/protocols/came.c @@ -14,12 +14,13 @@ #define TAG "SubGhzProtocolCame" -#define CAME_12_COUNT_BIT 12 -#define CAME_24_COUNT_BIT 24 -#define PRASTEL_COUNT_BIT 25 -#define PRASTEL_NAME "Prastel" -#define AIRFORCE_COUNT_BIT 18 -#define AIRFORCE_NAME "Airforce" +#define CAME_12_COUNT_BIT 12 +#define CAME_24_COUNT_BIT 24 +#define PRASTEL_25_COUNT_BIT 25 +#define PRASTEL_42_COUNT_BIT 42 +#define PRASTEL_NAME "Prastel" +#define AIRFORCE_COUNT_BIT 18 +#define AIRFORCE_NAME "Airforce" static const SubGhzBlockConst subghz_protocol_came_const = { .te_short = 320, @@ -123,6 +124,7 @@ static bool subghz_protocol_encoder_came_get_upload(SubGhzProtocolEncoderCame* i switch(instance->generic.data_count_bit) { case CAME_24_COUNT_BIT: + case PRASTEL_42_COUNT_BIT: // CAME 24 Bit = 24320 us header_te = 76; break; @@ -131,7 +133,7 @@ static bool subghz_protocol_encoder_came_get_upload(SubGhzProtocolEncoderCame* i // CAME 12 Bit Original only! and Airforce protocol = 15040 us header_te = 47; break; - case PRASTEL_COUNT_BIT: + case PRASTEL_25_COUNT_BIT: // PRASTEL = 11520 us header_te = 36; break; @@ -174,7 +176,7 @@ SubGhzProtocolStatus if(ret != SubGhzProtocolStatusOk) { break; } - if(instance->generic.data_count_bit > PRASTEL_COUNT_BIT) { + if(instance->generic.data_count_bit > PRASTEL_42_COUNT_BIT) { FURI_LOG_E(TAG, "Wrong number of bits in key"); ret = SubGhzProtocolStatusErrorValueBitCount; break; @@ -268,7 +270,8 @@ void subghz_protocol_decoder_came_feed(void* context, bool level, uint32_t durat if((instance->decoder.decode_count_bit == subghz_protocol_came_const.min_count_bit_for_found) || (instance->decoder.decode_count_bit == AIRFORCE_COUNT_BIT) || - (instance->decoder.decode_count_bit == PRASTEL_COUNT_BIT) || + (instance->decoder.decode_count_bit == PRASTEL_25_COUNT_BIT) || + (instance->decoder.decode_count_bit == PRASTEL_42_COUNT_BIT) || (instance->decoder.decode_count_bit == CAME_24_COUNT_BIT)) { instance->generic.serial = 0x0; instance->generic.btn = 0x0; @@ -337,7 +340,7 @@ SubGhzProtocolStatus if(ret != SubGhzProtocolStatusOk) { break; } - if(instance->generic.data_count_bit > PRASTEL_COUNT_BIT) { + if(instance->generic.data_count_bit > PRASTEL_42_COUNT_BIT) { FURI_LOG_E(TAG, "Wrong number of bits in key"); ret = SubGhzProtocolStatusErrorValueBitCount; break; @@ -350,23 +353,30 @@ void subghz_protocol_decoder_came_get_string(void* context, FuriString* output) furi_assert(context); SubGhzProtocolDecoderCame* instance = context; - uint32_t code_found_lo = instance->generic.data & 0x00000000ffffffff; + uint32_t code_found_lo = instance->generic.data & 0x000003ffffffffff; uint64_t code_found_reverse = subghz_protocol_blocks_reverse_key( instance->generic.data, instance->generic.data_count_bit); - uint32_t code_found_reverse_lo = code_found_reverse & 0x00000000ffffffff; + uint32_t code_found_reverse_lo = code_found_reverse & 0x000003ffffffffff; + + const char* name = instance->generic.protocol_name; + switch(instance->generic.data_count_bit) { + case PRASTEL_25_COUNT_BIT: + case PRASTEL_42_COUNT_BIT: + name = PRASTEL_NAME; + break; + case AIRFORCE_COUNT_BIT: + name = AIRFORCE_NAME; + break; + } furi_string_cat_printf( output, "%s %dbit\r\n" "Key:0x%08lX\r\n" "Yek:0x%08lX\r\n", - (instance->generic.data_count_bit == PRASTEL_COUNT_BIT ? - PRASTEL_NAME : - (instance->generic.data_count_bit == AIRFORCE_COUNT_BIT ? - AIRFORCE_NAME : - instance->generic.protocol_name)), + name, instance->generic.data_count_bit, code_found_lo, code_found_reverse_lo); From b90e7ab43c96a9b90c45b729ab445b05cbcf404e Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Mon, 7 Apr 2025 18:54:09 +0300 Subject: [PATCH 206/268] fmt [ci skip] --- applications/main/ibutton/ibutton_cli.c | 6 +++--- applications/main/onewire/onewire_cli.c | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/applications/main/ibutton/ibutton_cli.c b/applications/main/ibutton/ibutton_cli.c index 7765c0cae..0b9a59586 100644 --- a/applications/main/ibutton/ibutton_cli.c +++ b/applications/main/ibutton/ibutton_cli.c @@ -19,7 +19,7 @@ static void ibutton_cli_print_usage(void) { printf("\tCyfral (2 bytes key_data)\r\n"); printf("\tMetakom (4 bytes key_data), must contain correct parity\r\n"); printf("\t are hex-formatted\r\n"); -}; +} static bool ibutton_cli_parse_key(iButtonProtocols* protocols, iButtonKey* key, FuriString* args) { bool result = false; @@ -112,7 +112,7 @@ static void ibutton_cli_read(PipeSide* pipe) { ibutton_protocols_free(protocols); furi_event_flag_free(event); -}; +} typedef struct { FuriEventFlag* event; @@ -214,7 +214,7 @@ void ibutton_cli_emulate(PipeSide* pipe, FuriString* args) { ibutton_key_free(key); ibutton_worker_free(worker); ibutton_protocols_free(protocols); -}; +} static void execute(PipeSide* pipe, FuriString* args, void* context) { UNUSED(context); diff --git a/applications/main/onewire/onewire_cli.c b/applications/main/onewire/onewire_cli.c index 78ac75bac..193de76e4 100644 --- a/applications/main/onewire/onewire_cli.c +++ b/applications/main/onewire/onewire_cli.c @@ -11,7 +11,7 @@ static void onewire_cli_print_usage(void) { printf("Usage:\r\n"); printf("onewire search\r\n"); -}; +} static void onewire_cli_search(PipeSide* pipe) { UNUSED(pipe); From 5f664256718e2a19e7a90daf73b33b95077dc966 Mon Sep 17 00:00:00 2001 From: Pablo Mazzini Date: Mon, 7 Apr 2025 17:13:45 +0100 Subject: [PATCH 207/268] SubGhz: added support for 42-bit Prastel variation (#4178) Co-authored-by: hedger --- lib/subghz/protocols/came.c | 44 +++++++++++++++++++++++-------------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/lib/subghz/protocols/came.c b/lib/subghz/protocols/came.c index 645f5eee1..bbe3e487f 100644 --- a/lib/subghz/protocols/came.c +++ b/lib/subghz/protocols/came.c @@ -14,12 +14,13 @@ #define TAG "SubGhzProtocolCame" -#define CAME_12_COUNT_BIT 12 -#define CAME_24_COUNT_BIT 24 -#define PRASTEL_COUNT_BIT 25 -#define PRASTEL_NAME "Prastel" -#define AIRFORCE_COUNT_BIT 18 -#define AIRFORCE_NAME "Airforce" +#define CAME_12_COUNT_BIT 12 +#define CAME_24_COUNT_BIT 24 +#define PRASTEL_25_COUNT_BIT 25 +#define PRASTEL_42_COUNT_BIT 42 +#define PRASTEL_NAME "Prastel" +#define AIRFORCE_COUNT_BIT 18 +#define AIRFORCE_NAME "Airforce" static const SubGhzBlockConst subghz_protocol_came_const = { .te_short = 320, @@ -123,6 +124,7 @@ static bool subghz_protocol_encoder_came_get_upload(SubGhzProtocolEncoderCame* i switch(instance->generic.data_count_bit) { case CAME_24_COUNT_BIT: + case PRASTEL_42_COUNT_BIT: // CAME 24 Bit = 24320 us header_te = 76; break; @@ -131,7 +133,7 @@ static bool subghz_protocol_encoder_came_get_upload(SubGhzProtocolEncoderCame* i // CAME 12 Bit Original only! and Airforce protocol = 15040 us header_te = 47; break; - case PRASTEL_COUNT_BIT: + case PRASTEL_25_COUNT_BIT: // PRASTEL = 11520 us header_te = 36; break; @@ -174,7 +176,7 @@ SubGhzProtocolStatus if(ret != SubGhzProtocolStatusOk) { break; } - if(instance->generic.data_count_bit > PRASTEL_COUNT_BIT) { + if(instance->generic.data_count_bit > PRASTEL_42_COUNT_BIT) { FURI_LOG_E(TAG, "Wrong number of bits in key"); ret = SubGhzProtocolStatusErrorValueBitCount; break; @@ -268,7 +270,8 @@ void subghz_protocol_decoder_came_feed(void* context, bool level, uint32_t durat if((instance->decoder.decode_count_bit == subghz_protocol_came_const.min_count_bit_for_found) || (instance->decoder.decode_count_bit == AIRFORCE_COUNT_BIT) || - (instance->decoder.decode_count_bit == PRASTEL_COUNT_BIT) || + (instance->decoder.decode_count_bit == PRASTEL_25_COUNT_BIT) || + (instance->decoder.decode_count_bit == PRASTEL_42_COUNT_BIT) || (instance->decoder.decode_count_bit == CAME_24_COUNT_BIT)) { instance->generic.serial = 0x0; instance->generic.btn = 0x0; @@ -337,7 +340,7 @@ SubGhzProtocolStatus if(ret != SubGhzProtocolStatusOk) { break; } - if(instance->generic.data_count_bit > PRASTEL_COUNT_BIT) { + if(instance->generic.data_count_bit > PRASTEL_42_COUNT_BIT) { FURI_LOG_E(TAG, "Wrong number of bits in key"); ret = SubGhzProtocolStatusErrorValueBitCount; break; @@ -350,23 +353,30 @@ void subghz_protocol_decoder_came_get_string(void* context, FuriString* output) furi_assert(context); SubGhzProtocolDecoderCame* instance = context; - uint32_t code_found_lo = instance->generic.data & 0x00000000ffffffff; + uint32_t code_found_lo = instance->generic.data & 0x000003ffffffffff; uint64_t code_found_reverse = subghz_protocol_blocks_reverse_key( instance->generic.data, instance->generic.data_count_bit); - uint32_t code_found_reverse_lo = code_found_reverse & 0x00000000ffffffff; + uint32_t code_found_reverse_lo = code_found_reverse & 0x000003ffffffffff; + + const char* name = instance->generic.protocol_name; + switch(instance->generic.data_count_bit) { + case PRASTEL_25_COUNT_BIT: + case PRASTEL_42_COUNT_BIT: + name = PRASTEL_NAME; + break; + case AIRFORCE_COUNT_BIT: + name = AIRFORCE_NAME; + break; + } furi_string_cat_printf( output, "%s %dbit\r\n" "Key:0x%08lX\r\n" "Yek:0x%08lX\r\n", - (instance->generic.data_count_bit == PRASTEL_COUNT_BIT ? - PRASTEL_NAME : - (instance->generic.data_count_bit == AIRFORCE_COUNT_BIT ? - AIRFORCE_NAME : - instance->generic.protocol_name)), + name, instance->generic.data_count_bit, code_found_lo, code_found_reverse_lo); From c4c5148992a08106fe51053ec7c3768c98364c53 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Mon, 7 Apr 2025 19:34:32 +0300 Subject: [PATCH 208/268] upd changelog --- CHANGELOG.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a7ca55f6..6561f4662 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,11 +4,15 @@ * SubGHz: **Fix Hollarm protocol with more verification** * SubGHz: **Fix GangQi protocol** (by @DoberBit and @mishamyte (who spent 2 weeks on this)) * SubGHz: **Came Atomo button hold simulation with full cycle** simulation (to allow proper pairing with receiver) +* SubGHz: Add **Prastel (42bit static code)** support (OFW PR 4178 by @pmazzini) * System: **Night Shift Feature** (dimming backlight in selected time interval) (PR #885 | by @Dmitry422) * System: **Сombining RGB Backlight mod** (by @quen0n) and original backlight support **in one firmware** (+ Rainbow/Wave effect (based on @Willy-JL idea)) (PR #877 #881 | by @Dmitry422) - (**To enable RGB Backlight support go into Notifications settings with Debug mode - ON**) * OFW: LFRFID - **EM4305 support** * OFW: **Universal IR signal selection** * OFW: **BadUSB: Mouse control** +* OFW: **Pinning of settings options** +* OFW: NFC app now can launch MFKey32 +* OFW: BadUSB arbitrary key combinations * OFW PR 4136: BadUSB: Full USB/BLE parameter customization, UI improvements, and more (by @Willy-JL) * OFW: NFC - Added naming for DESFire cards + fix MF3ICD40 cards unable to be read * Apps: Add **FindMyFlipper to system apps and allow autostart** on system boot [app by @MatthewKuKanich](https://github.com/MatthewKuKanich/FindMyFlipper) and autoloader by @Willy-JL - to use app please check how to add keys in [app repo](https://github.com/MatthewKuKanich/FindMyFlipper) @@ -21,8 +25,8 @@ * SubGHz: Various bugfixes and experimental options (rolling counter overflow) (by @xMasterX) * Anims: Disable winter anims * NFC: mfclassic poller fix early key reuse in dictionary attack state machine (by @noproto) -* OFW: NFC app now can launch MFKey32 -* OFW: BadUSB arbitrary key combinations +* OFW: BLE: Slightly increase mfg_data size +* OFW: fbt: Deterministic STARTUP order & additional checks * OFW: JS: Update and fix docs, fix Number.toString() with decimals * OFW: New JS value destructuring * OFW: Docs: Fix doxygen references from PR 4168 From 16e48c8994143f07d2322ecf69e8e1cd195bb988 Mon Sep 17 00:00:00 2001 From: Anna Antonenko Date: Mon, 7 Apr 2025 20:39:58 +0400 Subject: [PATCH 209/268] cli_vcp: handle tx/rx before connext/disconnect --- applications/services/cli/cli_vcp.c | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/applications/services/cli/cli_vcp.c b/applications/services/cli/cli_vcp.c index f4b539e26..97cd15df3 100644 --- a/applications/services/cli/cli_vcp.c +++ b/applications/services/cli/cli_vcp.c @@ -15,6 +15,7 @@ #define VCP_IF_NUM 0 #define VCP_MESSAGE_Q_LEN 8 +#define CLI_VCP_TRACE #ifdef CLI_VCP_TRACE #define VCP_TRACE(...) FURI_LOG_T(__VA_ARGS__) #else @@ -195,6 +196,17 @@ static void cli_vcp_internal_event_happened(void* context) { CliVcpInternalEvent event = furi_thread_flags_wait(CliVcpInternalEventAll, FuriFlagWaitAny, 0); furi_check(!(event & FuriFlagError)); + if(event & CliVcpInternalEventRx) { + VCP_TRACE(TAG, "Rx"); + cli_vcp_maybe_receive_data(cli_vcp); + } + + if(event & CliVcpInternalEventTxDone) { + VCP_TRACE(TAG, "TxDone"); + cli_vcp->is_currently_transmitting = false; + cli_vcp_maybe_send_data(cli_vcp); + } + if(event & CliVcpInternalEventDisconnected) { if(!cli_vcp->is_connected) return; FURI_LOG_D(TAG, "Disconnected"); @@ -234,17 +246,6 @@ static void cli_vcp_internal_event_happened(void* context) { cli_main_motd, NULL, cli_vcp->shell_pipe, cli_vcp->main_registry, &cli_main_ext_config); cli_shell_start(cli_vcp->shell); } - - if(event & CliVcpInternalEventRx) { - VCP_TRACE(TAG, "Rx"); - cli_vcp_maybe_receive_data(cli_vcp); - } - - if(event & CliVcpInternalEventTxDone) { - VCP_TRACE(TAG, "TxDone"); - cli_vcp->is_currently_transmitting = false; - cli_vcp_maybe_send_data(cli_vcp); - } } // ============ From 7f45050c87d0ea58283e02c98b6794fe85ddc867 Mon Sep 17 00:00:00 2001 From: Anna Antonenko Date: Mon, 7 Apr 2025 20:40:34 +0400 Subject: [PATCH 210/268] cli_vcp: disable trace --- applications/services/cli/cli_vcp.c | 1 - 1 file changed, 1 deletion(-) diff --git a/applications/services/cli/cli_vcp.c b/applications/services/cli/cli_vcp.c index 97cd15df3..8c32c1bfa 100644 --- a/applications/services/cli/cli_vcp.c +++ b/applications/services/cli/cli_vcp.c @@ -15,7 +15,6 @@ #define VCP_IF_NUM 0 #define VCP_MESSAGE_Q_LEN 8 -#define CLI_VCP_TRACE #ifdef CLI_VCP_TRACE #define VCP_TRACE(...) FURI_LOG_T(__VA_ARGS__) #else From b39912af11a8b3efe8c6a8b6a78e12dc85b80e19 Mon Sep 17 00:00:00 2001 From: Anna Antonenko Date: Mon, 7 Apr 2025 23:30:39 +0400 Subject: [PATCH 211/268] cli_perf: advanced error reporting --- .gitignore | 4 ++++ scripts/serial_cli_perf.py | 16 +++++++++++++--- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 84b8e8319..79f2a8058 100644 --- a/.gitignore +++ b/.gitignore @@ -67,3 +67,7 @@ PVS-Studio.log # JS packages node_modules/ + +# cli_perf script output in case of errors +/block.bin +/return_block.bin diff --git a/scripts/serial_cli_perf.py b/scripts/serial_cli_perf.py index 0fed7c393..0db53dd6a 100644 --- a/scripts/serial_cli_perf.py +++ b/scripts/serial_cli_perf.py @@ -32,16 +32,25 @@ def main(): bytes_to_send = args.length block_size = 1024 + success = True while bytes_to_send: actual_size = min(block_size, bytes_to_send) # can't use 0x03 because that's ASCII ETX, or Ctrl+C - block = bytes([randint(4, 255) for _ in range(actual_size)]) + # block = bytes([randint(4, 255) for _ in range(actual_size)]) + block = bytes([4 + (i // 64) for i in range(actual_size)]) port.write(block) return_block = port.read(actual_size) if return_block != block: - logger.error("Incorrect block received") + with open("block.bin", "wb") as f: + f.write(block) + with open("return_block.bin", "wb") as f: + f.write(return_block) + + logger.error("Incorrect block received. Saved to `block.bin' and `return_block.bin'.") + logger.error(f"{bytes_to_send} bytes left. Aborting.") + success = False break bytes_to_send -= actual_size @@ -49,7 +58,8 @@ def main(): end_time = time() delta = end_time - start_time speed = args.length / delta - print(f"Speed: {speed/1024:.2f} KiB/s") + if success: + print(f"Speed: {speed/1024:.2f} KiB/s") port.write(b"\x03") # Ctrl+C port.close() From 33fa903f7ae7d4f14b48622187be7ccdad6f56c5 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Mon, 7 Apr 2025 22:33:56 +0300 Subject: [PATCH 212/268] fix swapped settings --- applications/settings/storage_settings/storage_settings.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/applications/settings/storage_settings/storage_settings.c b/applications/settings/storage_settings/storage_settings.c index b4cde7b6b..07656431c 100644 --- a/applications/settings/storage_settings/storage_settings.c +++ b/applications/settings/storage_settings/storage_settings.c @@ -5,8 +5,8 @@ const SubmenuSettingsHelperDescriptor descriptor_template = { .options_cnt = 6, .options = { - {.name = "About Internal Storage", .scene_id = StorageSettingsSDInfo}, - {.name = "About SD Card", .scene_id = StorageSettingsInternalInfo}, + {.name = "About Internal Storage", .scene_id = StorageSettingsInternalInfo}, + {.name = "About SD Card", .scene_id = StorageSettingsSDInfo}, {.name = "Unmount SD Card", .scene_id = StorageSettingsUnmountConfirm}, {.name = "Format SD Card", .scene_id = StorageSettingsFormatConfirm}, {.name = "Benchmark SD Card", .scene_id = StorageSettingsBenchmarkConfirm}, From 29b865ed21d5e33cab14e444235218cfdd05ac9f Mon Sep 17 00:00:00 2001 From: Anna Antonenko Date: Mon, 7 Apr 2025 23:54:13 +0400 Subject: [PATCH 213/268] cli_vcp: reset tx flag directly in event handler --- applications/services/cli/cli_vcp.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/applications/services/cli/cli_vcp.c b/applications/services/cli/cli_vcp.c index 8c32c1bfa..f23f813a2 100644 --- a/applications/services/cli/cli_vcp.c +++ b/applications/services/cli/cli_vcp.c @@ -106,6 +106,7 @@ static void cli_vcp_signal_internal_event(CliVcp* cli_vcp, CliVcpInternalEvent e static void cli_vcp_cdc_tx_done(void* context) { CliVcp* cli_vcp = context; + cli_vcp->is_currently_transmitting = false; cli_vcp_signal_internal_event(cli_vcp, CliVcpInternalEventTxDone); } @@ -202,7 +203,6 @@ static void cli_vcp_internal_event_happened(void* context) { if(event & CliVcpInternalEventTxDone) { VCP_TRACE(TAG, "TxDone"); - cli_vcp->is_currently_transmitting = false; cli_vcp_maybe_send_data(cli_vcp); } From c8cf76f75e2bac68deeb99e646d2b147de8d8be5 Mon Sep 17 00:00:00 2001 From: Anna Antonenko Date: Mon, 7 Apr 2025 23:56:09 +0400 Subject: [PATCH 214/268] fix formatting --- scripts/serial_cli_perf.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/serial_cli_perf.py b/scripts/serial_cli_perf.py index 0db53dd6a..3d612e6de 100644 --- a/scripts/serial_cli_perf.py +++ b/scripts/serial_cli_perf.py @@ -48,7 +48,9 @@ def main(): with open("return_block.bin", "wb") as f: f.write(return_block) - logger.error("Incorrect block received. Saved to `block.bin' and `return_block.bin'.") + logger.error( + "Incorrect block received. Saved to `block.bin' and `return_block.bin'." + ) logger.error(f"{bytes_to_send} bytes left. Aborting.") success = False break From e712375edc9515fae16f3909325055ed4c0ce65c Mon Sep 17 00:00:00 2001 From: Anna Antonenko Date: Mon, 7 Apr 2025 23:59:10 +0400 Subject: [PATCH 215/268] cli_vcp: make tx flag volatile --- applications/services/cli/cli_vcp.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/applications/services/cli/cli_vcp.c b/applications/services/cli/cli_vcp.c index f23f813a2..99f2d7880 100644 --- a/applications/services/cli/cli_vcp.c +++ b/applications/services/cli/cli_vcp.c @@ -50,7 +50,7 @@ struct CliVcp { PipeSide* own_pipe; PipeSide* shell_pipe; - bool is_currently_transmitting; + volatile bool is_currently_transmitting; size_t previous_tx_length; CliRegistry* main_registry; From 6b7a7c570922c50b4aee1b45eaae22fd05cb361f Mon Sep 17 00:00:00 2001 From: Anna Antonenko Date: Tue, 8 Apr 2025 00:25:31 +0400 Subject: [PATCH 216/268] storage_settings: fix scene ids --- applications/settings/storage_settings/storage_settings.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/applications/settings/storage_settings/storage_settings.c b/applications/settings/storage_settings/storage_settings.c index b4cde7b6b..07656431c 100644 --- a/applications/settings/storage_settings/storage_settings.c +++ b/applications/settings/storage_settings/storage_settings.c @@ -5,8 +5,8 @@ const SubmenuSettingsHelperDescriptor descriptor_template = { .options_cnt = 6, .options = { - {.name = "About Internal Storage", .scene_id = StorageSettingsSDInfo}, - {.name = "About SD Card", .scene_id = StorageSettingsInternalInfo}, + {.name = "About Internal Storage", .scene_id = StorageSettingsInternalInfo}, + {.name = "About SD Card", .scene_id = StorageSettingsSDInfo}, {.name = "Unmount SD Card", .scene_id = StorageSettingsUnmountConfirm}, {.name = "Format SD Card", .scene_id = StorageSettingsFormatConfirm}, {.name = "Benchmark SD Card", .scene_id = StorageSettingsBenchmarkConfirm}, From 75070588701cbb03338dded6107409b3b6e59c15 Mon Sep 17 00:00:00 2001 From: Dmitry422 Date: Tue, 8 Apr 2025 18:00:10 +0700 Subject: [PATCH 217/268] Start working with LCD color inversion --- applications/services/gui/canvas.c | 47 +++++++++++++++++++ .../services/notification/notification_app.c | 10 ---- .../services/notification/notification_app.h | 5 +- .../notification_settings_app.c | 24 ++++++++++ lib/u8g2/u8g2.h | 1 + lib/u8g2/u8g2_buffer.c | 7 +++ 6 files changed, 83 insertions(+), 11 deletions(-) diff --git a/applications/services/gui/canvas.c b/applications/services/gui/canvas.c index 337789dd3..22addd9ff 100644 --- a/applications/services/gui/canvas.c +++ b/applications/services/gui/canvas.c @@ -6,6 +6,8 @@ #include #include +#include + const CanvasFontParameters canvas_font_params[FontTotalNumber] = { [FontPrimary] = {.leading_default = 12, .leading_min = 11, .height = 8, .descender = 2}, [FontSecondary] = {.leading_default = 11, .leading_min = 9, .height = 7, .descender = 2}, @@ -141,11 +143,39 @@ const CanvasFontParameters* canvas_get_font_params(const Canvas* canvas, Font fo void canvas_clear(Canvas* canvas) { furi_check(canvas); + furi_delay_ms (500); + NotificationApp* app = malloc (sizeof(NotificationApp)); + app = furi_record_open(RECORD_NOTIFICATION); + + // open Notification record for access to NotificationApp settings + // NotificationApp* app = malloc (sizeof(NotificationApp)); + // app = furi_record_open(RECORD_NOTIFICATION); + + if(app->settings.lcd_inverse) { + u8g2_FillBuffer(&canvas->fb); + } else { + u8g2_ClearBuffer(&canvas->fb); + } + u8g2_ClearBuffer(&canvas->fb); + // furi_record_close (RECORD_NOTIFICATION); + // free (app); } void canvas_set_color(Canvas* canvas, Color color) { furi_check(canvas); + furi_delay_ms (500); + // open Notification record for access to NotificationApp settings + NotificationApp* app = malloc (sizeof(NotificationApp)); + app = furi_record_open(RECORD_NOTIFICATION); + + if(app->settings.lcd_inverse) { + if(color == ColorBlack) { + color = ColorWhite; + } else if(color == ColorWhite) { + color = ColorBlack; + } + } u8g2_SetDrawColor(&canvas->fb, color); } @@ -155,6 +185,23 @@ void canvas_set_font_direction(Canvas* canvas, CanvasDirection dir) { } void canvas_invert_color(Canvas* canvas) { + + // // open Notification record for access to NotificationApp settings + // NotificationApp* app = malloc (sizeof(NotificationApp)); + // app = furi_record_open(RECORD_NOTIFICATION); + + // if((canvas->fb.draw_color == ColorXOR) && app->settings.lcd_inverse) { + // // ColorXOR = 0x02, inversion change it to White = 0x00 + // // but if we have lcd_inverse ON then we need Black =0x01 instead White 0x00 + // // so we force changing color to black + // canvas->fb.draw_color = ColorBlack; + // } else { + // canvas->fb.draw_color = !canvas->fb.draw_color; + // } + + // furi_record_close (RECORD_NOTIFICATION); + // free (app); + canvas->fb.draw_color = !canvas->fb.draw_color; } diff --git a/applications/services/notification/notification_app.c b/applications/services/notification/notification_app.c index b50017963..9a6b4a3da 100644 --- a/applications/services/notification/notification_app.c +++ b/applications/services/notification/notification_app.c @@ -56,10 +56,6 @@ void night_shift_timer_callback(void* context) { NotificationApp* app = context; DateTime current_date_time; - // IN DEVELOPMENT - // // save current night_shift; - // float old_night_shift = app->current_night_shift; - // take system time and convert to minutes furi_hal_rtc_get_datetime(¤t_date_time); uint32_t time = current_date_time.hour * 60 + current_date_time.minute; @@ -73,12 +69,6 @@ void night_shift_timer_callback(void* context) { app->current_night_shift = app->settings.night_shift; app->rgb_srv->current_night_shift = app->settings.night_shift; } - - // IN DEVELOPMENT - // // if night shift was changed then update stock and rgb backlight to new value - // if(old_night_shift != app->current_night_shift) { - // notification_message(app, &sequence_display_backlight_on); - // } } // --- NIGHT SHIFT END --- diff --git a/applications/services/notification/notification_app.h b/applications/services/notification/notification_app.h index 2ef84ba95..d027bf200 100644 --- a/applications/services/notification/notification_app.h +++ b/applications/services/notification/notification_app.h @@ -34,7 +34,7 @@ typedef struct { Light light; } NotificationLedLayer; -#define NOTIFICATION_SETTINGS_VERSION 0x03 +#define NOTIFICATION_SETTINGS_VERSION 0x04 #define NOTIFICATION_SETTINGS_PATH INT_PATH(NOTIFICATION_SETTINGS_FILE_NAME) typedef struct { @@ -48,8 +48,11 @@ typedef struct { float night_shift; uint32_t night_shift_start; uint32_t night_shift_end; + bool lcd_inverse; } NotificationSettings; +//extern NotificationSettings settings; + struct NotificationApp { FuriMessageQueue* queue; FuriPubSub* event_record; diff --git a/applications/settings/notification_settings/notification_settings_app.c b/applications/settings/notification_settings/notification_settings_app.c index 86176c8e5..7b239a077 100644 --- a/applications/settings/notification_settings/notification_settings_app.c +++ b/applications/settings/notification_settings/notification_settings_app.c @@ -270,6 +270,13 @@ const uint32_t night_shift_end_value[NIGHT_SHIFT_END_COUNT] = { // --- NIGHT SHIFT END --- +#define LCD_INVERSE_COUNT 2 +const char* const lcd_inverse_text[LCD_INVERSE_COUNT] = { + "OFF", + "ON", +}; +const bool lcd_inverse_value[LCD_INVERSE_COUNT] = {false, true}; + static void contrast_changed(VariableItem* item) { NotificationAppSettings* app = variable_item_get_context(item); uint8_t index = variable_item_get_current_value_index(item); @@ -341,6 +348,16 @@ static void vibro_changed(VariableItem* item) { notification_message(app->notification, &sequence_single_vibro); } +static void lcd_inverse_changed(VariableItem* item) { + NotificationAppSettings* app = variable_item_get_context(item); + uint8_t index = variable_item_get_current_value_index(item); + + variable_item_set_current_value_text(item, lcd_inverse_text[index]); + app->notification->settings.lcd_inverse = lcd_inverse_value[index]; + notification_message(app->notification, &sequence_display_backlight_on); + +} + //--- RGB BACKLIGHT --- static void rgb_backlight_installed_changed(VariableItem* item) { @@ -721,6 +738,13 @@ static NotificationAppSettings* alloc_settings(void) { variable_item_set_current_value_text(item, vibro_text[value_index]); } + item = variable_item_list_add( + app->variable_item_list, "LCD Inverse", LCD_INVERSE_COUNT, lcd_inverse_changed, app); + value_index = value_index_bool( + app->notification->settings.lcd_inverse, lcd_inverse_value, LCD_INVERSE_COUNT); + variable_item_set_current_value_index(item, value_index); + variable_item_set_current_value_text(item, lcd_inverse_text[value_index]); + //--- RGB BACKLIGHT --- app->variable_item_list_rgb = variable_item_list_alloc(); diff --git a/lib/u8g2/u8g2.h b/lib/u8g2/u8g2.h index c37f3b931..100b56d57 100644 --- a/lib/u8g2/u8g2.h +++ b/lib/u8g2/u8g2.h @@ -3548,6 +3548,7 @@ void u8g2_Setup_a2printer_384x240_f( void u8g2_SendBuffer(u8g2_t* u8g2); void u8g2_ClearBuffer(u8g2_t* u8g2); +void u8g2_FillBuffer(u8g2_t* u8g2); void u8g2_SetBufferCurrTileRow(u8g2_t* u8g2, uint8_t row) U8G2_NOINLINE; diff --git a/lib/u8g2/u8g2_buffer.c b/lib/u8g2/u8g2_buffer.c index 45855bd5d..57f4f84be 100644 --- a/lib/u8g2/u8g2_buffer.c +++ b/lib/u8g2/u8g2_buffer.c @@ -45,6 +45,13 @@ void u8g2_ClearBuffer(u8g2_t* u8g2) { memset(u8g2->tile_buf_ptr, 0, cnt); } +void u8g2_FillBuffer(u8g2_t* u8g2) { + size_t cnt; + cnt = u8g2_GetU8x8(u8g2)->display_info->tile_width; + cnt *= u8g2->tile_buf_height; + cnt *= 8; + memset(u8g2->tile_buf_ptr, 255, cnt); +} /*============================================*/ static void u8g2_send_tile_row(u8g2_t* u8g2, uint8_t src_tile_row, uint8_t dest_tile_row) { From 9a6aa17beee3e4c9cfe34dd3dc84cc445133aa0a Mon Sep 17 00:00:00 2001 From: Anna Antonenko Date: Tue, 8 Apr 2025 16:02:33 +0400 Subject: [PATCH 218/268] cli_shell: add safety check to set_prompt --- lib/toolbox/cli/shell/cli_shell.c | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/toolbox/cli/shell/cli_shell.c b/lib/toolbox/cli/shell/cli_shell.c index daf5065ec..445a5f2aa 100644 --- a/lib/toolbox/cli/shell/cli_shell.c +++ b/lib/toolbox/cli/shell/cli_shell.c @@ -474,5 +474,6 @@ void cli_shell_join(CliShell* shell) { void cli_shell_set_prompt(CliShell* shell, const char* prompt) { furi_check(shell); + furi_check(furi_thread_get_state(shell->thread) == FuriThreadStateStopped); shell->prompt = prompt; } From c77c2d2bb0aec8f1dc825012a242c8a82bcaba9f Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Tue, 8 Apr 2025 16:49:08 +0300 Subject: [PATCH 219/268] fix order --- applications/system/find_my_flipper/application.fam | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/applications/system/find_my_flipper/application.fam b/applications/system/find_my_flipper/application.fam index 380e2a941..c9b870282 100644 --- a/applications/system/find_my_flipper/application.fam +++ b/applications/system/find_my_flipper/application.fam @@ -20,5 +20,5 @@ App( apptype=FlipperAppType.STARTUP, entry_point="findmy_startup", sources=["findmy_startup.c", "findmy_state.c"], - order=1000, + order=1210, ) From 4140952605a60b42a8c282bc46a5ca6505eb9bf2 Mon Sep 17 00:00:00 2001 From: Anna Antonenko Date: Tue, 8 Apr 2025 18:05:08 +0400 Subject: [PATCH 220/268] cli_registry: move from bptree to dict, fix memory leak --- lib/toolbox/cli/cli_registry.c | 32 +++++++++---------- lib/toolbox/cli/cli_registry_i.h | 18 +++-------- lib/toolbox/cli/shell/cli_shell.c | 14 ++++---- lib/toolbox/cli/shell/cli_shell_completions.c | 6 ++-- 4 files changed, 29 insertions(+), 41 deletions(-) diff --git a/lib/toolbox/cli/cli_registry.c b/lib/toolbox/cli/cli_registry.c index 35bed19f2..91f7c4046 100644 --- a/lib/toolbox/cli/cli_registry.c +++ b/lib/toolbox/cli/cli_registry.c @@ -3,16 +3,16 @@ #include #include -#define TAG "cli" +#define TAG "CliRegistry" struct CliRegistry { - CliCommandTree_t commands; + CliCommandDict_t commands; FuriMutex* mutex; }; CliRegistry* cli_registry_alloc(void) { CliRegistry* registry = malloc(sizeof(CliRegistry)); - CliCommandTree_init(registry->commands); + CliCommandDict_init(registry->commands); registry->mutex = furi_mutex_alloc(FuriMutexTypeRecursive); return registry; } @@ -20,7 +20,7 @@ CliRegistry* cli_registry_alloc(void) { void cli_registry_free(CliRegistry* registry) { furi_check(furi_mutex_acquire(registry->mutex, FuriWaitForever) == FuriStatusOk); furi_mutex_free(registry->mutex); - CliCommandTree_clear(registry->commands); + CliCommandDict_clear(registry->commands); free(registry); } @@ -61,7 +61,7 @@ void cli_registry_add_command_ex( }; furi_check(furi_mutex_acquire(registry->mutex, FuriWaitForever) == FuriStatusOk); - CliCommandTree_set_at(registry->commands, name_str, command); + CliCommandDict_set_at(registry->commands, name_str, command); furi_check(furi_mutex_release(registry->mutex) == FuriStatusOk); furi_string_free(name_str); @@ -79,7 +79,7 @@ void cli_registry_delete_command(CliRegistry* registry, const char* name) { } while(name_replace != FURI_STRING_FAILURE); furi_check(furi_mutex_acquire(registry->mutex, FuriWaitForever) == FuriStatusOk); - CliCommandTree_erase(registry->commands, name_str); + CliCommandDict_erase(registry->commands, name_str); furi_check(furi_mutex_release(registry->mutex) == FuriStatusOk); furi_string_free(name_str); @@ -91,7 +91,7 @@ bool cli_registry_get_command( CliRegistryCommand* result) { furi_assert(registry); furi_check(furi_mutex_acquire(registry->mutex, FuriWaitForever) == FuriStatusOk); - CliRegistryCommand* data = CliCommandTree_get(registry->commands, command); + CliRegistryCommand* data = CliCommandDict_get(registry->commands, command); if(data) *result = *data; furi_check(furi_mutex_release(registry->mutex) == FuriStatusOk); @@ -103,16 +103,14 @@ void cli_registry_remove_external_commands(CliRegistry* registry) { furi_check(registry); furi_check(furi_mutex_acquire(registry->mutex, FuriWaitForever) == FuriStatusOk); - // FIXME FL-3977: memory leak somewhere within this function - - CliCommandTree_t internal_cmds; - CliCommandTree_init(internal_cmds); + CliCommandDict_t internal_cmds; + CliCommandDict_init(internal_cmds); for - M_EACH(item, registry->commands, CliCommandTree_t) { - if(!(item->value_ptr->flags & CliCommandFlagExternal)) - CliCommandTree_set_at(internal_cmds, *item->key_ptr, *item->value_ptr); + M_EACH(item, registry->commands, CliCommandDict_t) { + if(!(item->value.flags & CliCommandFlagExternal)) + CliCommandDict_set_at(internal_cmds, item->key, item->value); } - CliCommandTree_move(registry->commands, internal_cmds); + CliCommandDict_move(registry->commands, internal_cmds); furi_check(furi_mutex_release(registry->mutex) == FuriStatusOk); } @@ -148,7 +146,7 @@ void cli_registry_reload_external_commands( .execute_callback = NULL, .flags = CliCommandFlagExternal, }; - CliCommandTree_set_at(registry->commands, plugin_name, command); + CliCommandDict_set_at(registry->commands, plugin_name, command); } furi_string_free(plugin_name); @@ -172,7 +170,7 @@ void cli_registry_unlock(CliRegistry* registry) { furi_mutex_release(registry->mutex); } -CliCommandTree_t* cli_registry_get_commands(CliRegistry* registry) { +CliCommandDict_t* cli_registry_get_commands(CliRegistry* registry) { furi_assert(registry); return ®istry->commands; } diff --git a/lib/toolbox/cli/cli_registry_i.h b/lib/toolbox/cli/cli_registry_i.h index 95b7c55da..31995832f 100644 --- a/lib/toolbox/cli/cli_registry_i.h +++ b/lib/toolbox/cli/cli_registry_i.h @@ -6,7 +6,7 @@ #pragma once #include -#include +#include #include "cli_registry.h" #ifdef __cplusplus @@ -22,19 +22,9 @@ typedef struct { size_t stack_depth; } CliRegistryCommand; -#define CLI_COMMANDS_TREE_RANK 4 +DICT_DEF2(CliCommandDict, FuriString*, FURI_STRING_OPLIST, CliRegistryCommand, M_POD_OPLIST); -// -V:BPTREE_DEF2:1103 -// -V:BPTREE_DEF2:524 -BPTREE_DEF2( - CliCommandTree, - CLI_COMMANDS_TREE_RANK, - FuriString*, - FURI_STRING_OPLIST, - CliRegistryCommand, - M_POD_OPLIST); - -#define M_OPL_CliCommandTree_t() BPTREE_OPLIST2(CliCommandTree, FURI_STRING_OPLIST, M_POD_OPLIST) +#define M_OPL_CliCommandDict_t() DICT_OPLIST(CliCommandDict, FURI_STRING_OPLIST, M_POD_OPLIST) bool cli_registry_get_command( CliRegistry* registry, @@ -48,7 +38,7 @@ void cli_registry_unlock(CliRegistry* registry); /** * @warning Surround calls to this function with `cli_registry_[un]lock` */ -CliCommandTree_t* cli_registry_get_commands(CliRegistry* registry); +CliCommandDict_t* cli_registry_get_commands(CliRegistry* registry); #ifdef __cplusplus } diff --git a/lib/toolbox/cli/shell/cli_shell.c b/lib/toolbox/cli/shell/cli_shell.c index 445a5f2aa..8aa7c387a 100644 --- a/lib/toolbox/cli/shell/cli_shell.c +++ b/lib/toolbox/cli/shell/cli_shell.c @@ -103,15 +103,15 @@ void cli_command_help(PipeSide* pipe, FuriString* args, void* context) { printf("Available commands:\r\n" ANSI_FG_GREEN); cli_registry_lock(registry); - CliCommandTree_t* commands = cli_registry_get_commands(registry); - size_t commands_count = CliCommandTree_size(*commands); + CliCommandDict_t* commands = cli_registry_get_commands(registry); + size_t commands_count = CliCommandDict_size(*commands); - CliCommandTree_it_t iterator; - CliCommandTree_it(iterator, *commands); + CliCommandDict_it_t iterator; + CliCommandDict_it(iterator, *commands); for(size_t i = 0; i < commands_count; i++) { - const CliCommandTree_itref_t* item = CliCommandTree_cref(iterator); - printf("%-30s", furi_string_get_cstr(*item->key_ptr)); - CliCommandTree_next(iterator); + const CliCommandDict_itref_t* item = CliCommandDict_cref(iterator); + printf("%-30s", furi_string_get_cstr(item->key)); + CliCommandDict_next(iterator); if(i % columns == columns - 1) printf("\r\n"); } diff --git a/lib/toolbox/cli/shell/cli_shell_completions.c b/lib/toolbox/cli/shell/cli_shell_completions.c index 823f91fb9..7a178705d 100644 --- a/lib/toolbox/cli/shell/cli_shell_completions.c +++ b/lib/toolbox/cli/shell/cli_shell_completions.c @@ -111,10 +111,10 @@ void cli_shell_completions_fill_variants(CliShellCompletions* completions) { if(segment.type == CliShellCompletionSegmentTypeCommand) { CliRegistry* registry = completions->registry; cli_registry_lock(registry); - CliCommandTree_t* commands = cli_registry_get_commands(registry); + CliCommandDict_t* commands = cli_registry_get_commands(registry); for - M_EACH(registered_command, *commands, CliCommandTree_t) { - FuriString* command_name = *registered_command->key_ptr; + M_EACH(registered_command, *commands, CliCommandDict_t) { + FuriString* command_name = registered_command->key; if(furi_string_start_with(command_name, input)) { CommandCompletions_push_back(completions->variants, command_name); } From bdf2c4ce241e9daaab8a5426bcd7c347db0ac7bc Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Tue, 8 Apr 2025 17:50:30 +0300 Subject: [PATCH 221/268] more order changes --- applications/services/namechanger/application.fam | 4 ++-- applications/services/rgb_backlight/application.fam | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/applications/services/namechanger/application.fam b/applications/services/namechanger/application.fam index 0eaeab987..2edeb22df 100644 --- a/applications/services/namechanger/application.fam +++ b/applications/services/namechanger/application.fam @@ -4,5 +4,5 @@ App( entry_point="namechanger_on_system_start", requires=["storage", "cli", "bt"], conflicts=["updater"], - order=600, -) \ No newline at end of file + order=1300, +) diff --git a/applications/services/rgb_backlight/application.fam b/applications/services/rgb_backlight/application.fam index 631ca01d5..39954efaa 100644 --- a/applications/services/rgb_backlight/application.fam +++ b/applications/services/rgb_backlight/application.fam @@ -5,6 +5,6 @@ App( entry_point="rgb_backlight_srv", cdefines=["SRV_RGB_BACKLIGHT"], stack_size=1 * 1024, - order=95, + order=1290, sdk_headers=["rgb_backlight.h"], ) From 7286560c231ee69d699eeac8677bc728589c84f6 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Tue, 8 Apr 2025 17:52:27 +0300 Subject: [PATCH 222/268] upd changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6561f4662..26c36a642 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ * SubGHz: Various bugfixes and experimental options (rolling counter overflow) (by @xMasterX) * Anims: Disable winter anims * NFC: mfclassic poller fix early key reuse in dictionary attack state machine (by @noproto) +* OFW PR 4181: vcp, cli: Handle Tx/Rx events before Connect/Disconnect + extra fixes (by @portasynthinca3) * OFW: BLE: Slightly increase mfg_data size * OFW: fbt: Deterministic STARTUP order & additional checks * OFW: JS: Update and fix docs, fix Number.toString() with decimals From 4ec8f21e0928d3734c85f5923dab6f0b76cf2750 Mon Sep 17 00:00:00 2001 From: Dmitry422 Date: Wed, 9 Apr 2025 22:23:55 +0700 Subject: [PATCH 223/268] still under construction --- applications/services/gui/canvas.c | 45 +++++-------------- .../services/notification/notification_app.c | 5 +++ .../services/notification/notification_app.h | 5 ++- .../notification_settings_app.c | 4 ++ 4 files changed, 22 insertions(+), 37 deletions(-) diff --git a/applications/services/gui/canvas.c b/applications/services/gui/canvas.c index 22addd9ff..33402ab3c 100644 --- a/applications/services/gui/canvas.c +++ b/applications/services/gui/canvas.c @@ -143,33 +143,18 @@ const CanvasFontParameters* canvas_get_font_params(const Canvas* canvas, Font fo void canvas_clear(Canvas* canvas) { furi_check(canvas); - furi_delay_ms (500); - NotificationApp* app = malloc (sizeof(NotificationApp)); - app = furi_record_open(RECORD_NOTIFICATION); - - // open Notification record for access to NotificationApp settings - // NotificationApp* app = malloc (sizeof(NotificationApp)); - // app = furi_record_open(RECORD_NOTIFICATION); - if(app->settings.lcd_inverse) { + if(lcd_inverted) { u8g2_FillBuffer(&canvas->fb); } else { u8g2_ClearBuffer(&canvas->fb); } - - u8g2_ClearBuffer(&canvas->fb); - // furi_record_close (RECORD_NOTIFICATION); - // free (app); } void canvas_set_color(Canvas* canvas, Color color) { furi_check(canvas); - furi_delay_ms (500); - // open Notification record for access to NotificationApp settings - NotificationApp* app = malloc (sizeof(NotificationApp)); - app = furi_record_open(RECORD_NOTIFICATION); - if(app->settings.lcd_inverse) { + if(lcd_inverted) { if(color == ColorBlack) { color = ColorWhite; } else if(color == ColorWhite) { @@ -185,24 +170,14 @@ void canvas_set_font_direction(Canvas* canvas, CanvasDirection dir) { } void canvas_invert_color(Canvas* canvas) { - - // // open Notification record for access to NotificationApp settings - // NotificationApp* app = malloc (sizeof(NotificationApp)); - // app = furi_record_open(RECORD_NOTIFICATION); - - // if((canvas->fb.draw_color == ColorXOR) && app->settings.lcd_inverse) { - // // ColorXOR = 0x02, inversion change it to White = 0x00 - // // but if we have lcd_inverse ON then we need Black =0x01 instead White 0x00 - // // so we force changing color to black - // canvas->fb.draw_color = ColorBlack; - // } else { - // canvas->fb.draw_color = !canvas->fb.draw_color; - // } - - // furi_record_close (RECORD_NOTIFICATION); - // free (app); - - canvas->fb.draw_color = !canvas->fb.draw_color; + if((canvas->fb.draw_color == ColorXOR) && lcd_inverted) { + // ColorXOR = 0x02, inversion change it to White = 0x00 + // but if we have lcd_inverse ON then we need Black =0x01 instead White 0x00 + // so we force changing color to black + canvas->fb.draw_color = ColorBlack; + } else { + canvas->fb.draw_color = !canvas->fb.draw_color; + } } void canvas_set_font(Canvas* canvas, Font font) { diff --git a/applications/services/notification/notification_app.c b/applications/services/notification/notification_app.c index 9a6b4a3da..8a059586d 100644 --- a/applications/services/notification/notification_app.c +++ b/applications/services/notification/notification_app.c @@ -620,6 +620,8 @@ static NotificationApp* notification_app_alloc(void) { furi_timer_alloc(night_shift_timer_callback, FuriTimerTypePeriodic, app); // --- NIGHT SHIFT END --- + lcd_inverted = false; + return app; } @@ -650,6 +652,9 @@ static void notification_apply_settings(NotificationApp* app) { night_shift_timer_start(app); } // --- NIGHT SHIFT END --- + + //setup global variable "inverted" by settings value; + lcd_inverted = app->settings.lcd_inverse; } static void notification_init_settings(NotificationApp* app) { diff --git a/applications/services/notification/notification_app.h b/applications/services/notification/notification_app.h index d027bf200..30673f470 100644 --- a/applications/services/notification/notification_app.h +++ b/applications/services/notification/notification_app.h @@ -51,8 +51,6 @@ typedef struct { bool lcd_inverse; } NotificationSettings; -//extern NotificationSettings settings; - struct NotificationApp { FuriMessageQueue* queue; FuriPubSub* event_record; @@ -72,3 +70,6 @@ struct NotificationApp { void notification_message_save_settings(NotificationApp* app); void night_shift_timer_start(NotificationApp* app); void night_shift_timer_stop(NotificationApp* app); + +//global variable for using in canvac.c +extern bool lcd_inverted; diff --git a/applications/settings/notification_settings/notification_settings_app.c b/applications/settings/notification_settings/notification_settings_app.c index 7b239a077..25fc264f8 100644 --- a/applications/settings/notification_settings/notification_settings_app.c +++ b/applications/settings/notification_settings/notification_settings_app.c @@ -354,6 +354,10 @@ static void lcd_inverse_changed(VariableItem* item) { variable_item_set_current_value_text(item, lcd_inverse_text[index]); app->notification->settings.lcd_inverse = lcd_inverse_value[index]; + + //setup global variable for using in canvas.c + lcd_inverted = lcd_inverse_value[index]; + notification_message(app->notification, &sequence_display_backlight_on); } From 224d4e060bea27a8ede8b45a27fc25d032401762 Mon Sep 17 00:00:00 2001 From: Dmitry422 Date: Wed, 9 Apr 2025 23:37:30 +0700 Subject: [PATCH 224/268] LCD Inversion finished by MMX --- applications/services/gui/canvas.c | 22 +++++++++++++------ applications/services/gui/canvas.h | 4 ++++ applications/services/gui/canvas_i.h | 1 + .../services/notification/notification_app.c | 14 +++++++++--- .../services/notification/notification_app.h | 3 --- .../notification_settings_app.c | 6 ++--- targets/f7/api_symbols.csv | 4 +++- 7 files changed, 37 insertions(+), 17 deletions(-) diff --git a/applications/services/gui/canvas.c b/applications/services/gui/canvas.c index 33402ab3c..9c19853d3 100644 --- a/applications/services/gui/canvas.c +++ b/applications/services/gui/canvas.c @@ -6,8 +6,6 @@ #include #include -#include - const CanvasFontParameters canvas_font_params[FontTotalNumber] = { [FontPrimary] = {.leading_default = 12, .leading_min = 11, .height = 8, .descender = 2}, [FontSecondary] = {.leading_default = 11, .leading_min = 9, .height = 7, .descender = 2}, @@ -96,6 +94,16 @@ size_t canvas_get_buffer_size(const Canvas* canvas) { return u8g2_GetBufferTileWidth(&canvas->fb) * u8g2_GetBufferTileHeight(&canvas->fb) * 8; } +bool canvas_is_inverted_lcd(Canvas* canvas) { + furi_assert(canvas); + return canvas->lcd_inverse; +} + +void canvas_set_inverted_lcd(Canvas* canvas, bool inverted) { + furi_assert(canvas); + canvas->lcd_inverse = inverted; +} + void canvas_frame_set( Canvas* canvas, int32_t offset_x, @@ -143,8 +151,8 @@ const CanvasFontParameters* canvas_get_font_params(const Canvas* canvas, Font fo void canvas_clear(Canvas* canvas) { furi_check(canvas); - - if(lcd_inverted) { + + if(canvas->lcd_inverse) { u8g2_FillBuffer(&canvas->fb); } else { u8g2_ClearBuffer(&canvas->fb); @@ -154,7 +162,7 @@ void canvas_clear(Canvas* canvas) { void canvas_set_color(Canvas* canvas, Color color) { furi_check(canvas); - if(lcd_inverted) { + if(canvas->lcd_inverse) { if(color == ColorBlack) { color = ColorWhite; } else if(color == ColorWhite) { @@ -170,10 +178,10 @@ void canvas_set_font_direction(Canvas* canvas, CanvasDirection dir) { } void canvas_invert_color(Canvas* canvas) { - if((canvas->fb.draw_color == ColorXOR) && lcd_inverted) { + if((canvas->fb.draw_color == ColorXOR) && canvas->lcd_inverse) { // ColorXOR = 0x02, inversion change it to White = 0x00 // but if we have lcd_inverse ON then we need Black =0x01 instead White 0x00 - // so we force changing color to black + // so we force changing color to black canvas->fb.draw_color = ColorBlack; } else { canvas->fb.draw_color = !canvas->fb.draw_color; diff --git a/applications/services/gui/canvas.h b/applications/services/gui/canvas.h index efd314687..8257c481d 100644 --- a/applications/services/gui/canvas.h +++ b/applications/services/gui/canvas.h @@ -447,6 +447,10 @@ void canvas_draw_icon_bitmap( int16_t h, const Icon* icon); +bool canvas_is_inverted_lcd(Canvas* canvas); + +void canvas_set_inverted_lcd(Canvas* canvas, bool inverted); + #ifdef __cplusplus } #endif diff --git a/applications/services/gui/canvas_i.h b/applications/services/gui/canvas_i.h index 449db71db..d342c07f5 100644 --- a/applications/services/gui/canvas_i.h +++ b/applications/services/gui/canvas_i.h @@ -47,6 +47,7 @@ struct Canvas { CompressIcon* compress_icon; CanvasCallbackPairArray_t canvas_callback_pair; FuriMutex* mutex; + bool lcd_inverse; }; /** Allocate memory and initialize canvas diff --git a/applications/services/notification/notification_app.c b/applications/services/notification/notification_app.c index 8a059586d..f57900fc5 100644 --- a/applications/services/notification/notification_app.c +++ b/applications/services/notification/notification_app.c @@ -620,7 +620,11 @@ static NotificationApp* notification_app_alloc(void) { furi_timer_alloc(night_shift_timer_callback, FuriTimerTypePeriodic, app); // --- NIGHT SHIFT END --- - lcd_inverted = false; + Gui* tmp_gui = furi_record_open(RECORD_GUI); + Canvas* tmp_canvas = gui_direct_draw_acquire(tmp_gui); + canvas_set_inverted_lcd(tmp_canvas, false); + gui_direct_draw_release(tmp_gui); + furi_record_close(RECORD_GUI); return app; } @@ -653,8 +657,12 @@ static void notification_apply_settings(NotificationApp* app) { } // --- NIGHT SHIFT END --- - //setup global variable "inverted" by settings value; - lcd_inverted = app->settings.lcd_inverse; + //setup canvas variable "inverse" by settings value; + Gui* tmp_gui = furi_record_open(RECORD_GUI); + Canvas* tmp_canvas = gui_direct_draw_acquire(tmp_gui); + canvas_set_inverted_lcd(tmp_canvas, app->settings.lcd_inverse); + gui_direct_draw_release(tmp_gui); + furi_record_close(RECORD_GUI); } static void notification_init_settings(NotificationApp* app) { diff --git a/applications/services/notification/notification_app.h b/applications/services/notification/notification_app.h index 30673f470..c0b64b807 100644 --- a/applications/services/notification/notification_app.h +++ b/applications/services/notification/notification_app.h @@ -70,6 +70,3 @@ struct NotificationApp { void notification_message_save_settings(NotificationApp* app); void night_shift_timer_start(NotificationApp* app); void night_shift_timer_stop(NotificationApp* app); - -//global variable for using in canvac.c -extern bool lcd_inverted; diff --git a/applications/settings/notification_settings/notification_settings_app.c b/applications/settings/notification_settings/notification_settings_app.c index 25fc264f8..d313714fc 100644 --- a/applications/settings/notification_settings/notification_settings_app.c +++ b/applications/settings/notification_settings/notification_settings_app.c @@ -355,11 +355,11 @@ static void lcd_inverse_changed(VariableItem* item) { variable_item_set_current_value_text(item, lcd_inverse_text[index]); app->notification->settings.lcd_inverse = lcd_inverse_value[index]; - //setup global variable for using in canvas.c - lcd_inverted = lcd_inverse_value[index]; + Canvas* tmp_canvas = gui_direct_draw_acquire(app->gui); + canvas_set_inverted_lcd(tmp_canvas, lcd_inverse_value[index]); + gui_direct_draw_release(app->gui); notification_message(app->notification, &sequence_display_backlight_on); - } //--- RGB BACKLIGHT --- diff --git a/targets/f7/api_symbols.csv b/targets/f7/api_symbols.csv index 384ae1ea4..db6a1c242 100755 --- a/targets/f7/api_symbols.csv +++ b/targets/f7/api_symbols.csv @@ -852,12 +852,14 @@ Function,+,canvas_get_font_params,const CanvasFontParameters*,"const Canvas*, Fo Function,+,canvas_glyph_width,size_t,"Canvas*, uint16_t" Function,+,canvas_height,size_t,const Canvas* Function,+,canvas_invert_color,void,Canvas* +Function,+,canvas_is_inverted_lcd,_Bool,Canvas* Function,+,canvas_reset,void,Canvas* Function,+,canvas_set_bitmap_mode,void,"Canvas*, _Bool" Function,+,canvas_set_color,void,"Canvas*, Color" Function,+,canvas_set_custom_u8g2_font,void,"Canvas*, const uint8_t*" Function,+,canvas_set_font,void,"Canvas*, Font" Function,+,canvas_set_font_direction,void,"Canvas*, CanvasDirection" +Function,+,canvas_set_inverted_lcd,void,"Canvas*, _Bool" Function,+,canvas_string_width,uint16_t,"Canvas*, const char*" Function,+,canvas_width,size_t,const Canvas* Function,-,cbrt,double,double @@ -3663,8 +3665,8 @@ Function,+,subghz_worker_set_pair_callback,void,"SubGhzWorker*, SubGhzWorkerPair Function,+,subghz_worker_start,void,SubGhzWorker* Function,+,subghz_worker_stop,void,SubGhzWorker* Function,+,submenu_add_item,void,"Submenu*, const char*, uint32_t, SubmenuItemCallback, void*" -Function,+,submenu_add_lockable_item,void,"Submenu*, const char*, uint32_t, SubmenuItemCallback, void*, _Bool, const char*" Function,+,submenu_add_item_ex,void,"Submenu*, const char*, uint32_t, SubmenuItemCallbackEx, void*" +Function,+,submenu_add_lockable_item,void,"Submenu*, const char*, uint32_t, SubmenuItemCallback, void*, _Bool, const char*" Function,+,submenu_alloc,Submenu*, Function,+,submenu_change_item_label,void,"Submenu*, uint32_t, const char*" Function,+,submenu_free,void,Submenu* From 75c8cb9715244ea0213e8bdc6a2cea9129c6bc64 Mon Sep 17 00:00:00 2001 From: Dmitry422 Date: Thu, 10 Apr 2025 00:55:02 +0700 Subject: [PATCH 225/268] Cosmetic code changes --- applications/services/gui/canvas.c | 12 +++++------ applications/services/gui/canvas_i.h | 2 +- .../services/notification/notification_app.c | 4 ++-- .../services/notification/notification_app.h | 2 +- .../notification_settings_app.c | 20 +++++++++---------- 5 files changed, 20 insertions(+), 20 deletions(-) diff --git a/applications/services/gui/canvas.c b/applications/services/gui/canvas.c index 9c19853d3..573ad185e 100644 --- a/applications/services/gui/canvas.c +++ b/applications/services/gui/canvas.c @@ -96,12 +96,12 @@ size_t canvas_get_buffer_size(const Canvas* canvas) { bool canvas_is_inverted_lcd(Canvas* canvas) { furi_assert(canvas); - return canvas->lcd_inverse; + return canvas->lcd_inversion; } void canvas_set_inverted_lcd(Canvas* canvas, bool inverted) { furi_assert(canvas); - canvas->lcd_inverse = inverted; + canvas->lcd_inversion = inverted; } void canvas_frame_set( @@ -152,7 +152,7 @@ const CanvasFontParameters* canvas_get_font_params(const Canvas* canvas, Font fo void canvas_clear(Canvas* canvas) { furi_check(canvas); - if(canvas->lcd_inverse) { + if(canvas->lcd_inversion) { u8g2_FillBuffer(&canvas->fb); } else { u8g2_ClearBuffer(&canvas->fb); @@ -162,7 +162,7 @@ void canvas_clear(Canvas* canvas) { void canvas_set_color(Canvas* canvas, Color color) { furi_check(canvas); - if(canvas->lcd_inverse) { + if(canvas->lcd_inversion) { if(color == ColorBlack) { color = ColorWhite; } else if(color == ColorWhite) { @@ -178,9 +178,9 @@ void canvas_set_font_direction(Canvas* canvas, CanvasDirection dir) { } void canvas_invert_color(Canvas* canvas) { - if((canvas->fb.draw_color == ColorXOR) && canvas->lcd_inverse) { + if((canvas->fb.draw_color == ColorXOR) && canvas->lcd_inversion) { // ColorXOR = 0x02, inversion change it to White = 0x00 - // but if we have lcd_inverse ON then we need Black =0x01 instead White 0x00 + // but if we have lcd_inversion ON then we need Black =0x01 instead White 0x00 // so we force changing color to black canvas->fb.draw_color = ColorBlack; } else { diff --git a/applications/services/gui/canvas_i.h b/applications/services/gui/canvas_i.h index d342c07f5..420b97e0f 100644 --- a/applications/services/gui/canvas_i.h +++ b/applications/services/gui/canvas_i.h @@ -47,7 +47,7 @@ struct Canvas { CompressIcon* compress_icon; CanvasCallbackPairArray_t canvas_callback_pair; FuriMutex* mutex; - bool lcd_inverse; + bool lcd_inversion; }; /** Allocate memory and initialize canvas diff --git a/applications/services/notification/notification_app.c b/applications/services/notification/notification_app.c index f57900fc5..d416c89f9 100644 --- a/applications/services/notification/notification_app.c +++ b/applications/services/notification/notification_app.c @@ -657,10 +657,10 @@ static void notification_apply_settings(NotificationApp* app) { } // --- NIGHT SHIFT END --- - //setup canvas variable "inverse" by settings value; + //setup canvas variable "inversion" by settings value; Gui* tmp_gui = furi_record_open(RECORD_GUI); Canvas* tmp_canvas = gui_direct_draw_acquire(tmp_gui); - canvas_set_inverted_lcd(tmp_canvas, app->settings.lcd_inverse); + canvas_set_inverted_lcd(tmp_canvas, app->settings.lcd_inversion); gui_direct_draw_release(tmp_gui); furi_record_close(RECORD_GUI); } diff --git a/applications/services/notification/notification_app.h b/applications/services/notification/notification_app.h index c0b64b807..7ac8528f2 100644 --- a/applications/services/notification/notification_app.h +++ b/applications/services/notification/notification_app.h @@ -48,7 +48,7 @@ typedef struct { float night_shift; uint32_t night_shift_start; uint32_t night_shift_end; - bool lcd_inverse; + bool lcd_inversion; } NotificationSettings; struct NotificationApp { diff --git a/applications/settings/notification_settings/notification_settings_app.c b/applications/settings/notification_settings/notification_settings_app.c index d313714fc..4362039f0 100644 --- a/applications/settings/notification_settings/notification_settings_app.c +++ b/applications/settings/notification_settings/notification_settings_app.c @@ -270,12 +270,12 @@ const uint32_t night_shift_end_value[NIGHT_SHIFT_END_COUNT] = { // --- NIGHT SHIFT END --- -#define LCD_INVERSE_COUNT 2 -const char* const lcd_inverse_text[LCD_INVERSE_COUNT] = { +#define LCD_INVERSION_COUNT 2 +const char* const lcd_inversion_text[LCD_INVERSION_COUNT] = { "OFF", "ON", }; -const bool lcd_inverse_value[LCD_INVERSE_COUNT] = {false, true}; +const bool lcd_inversion_value[LCD_INVERSION_COUNT] = {false, true}; static void contrast_changed(VariableItem* item) { NotificationAppSettings* app = variable_item_get_context(item); @@ -348,15 +348,15 @@ static void vibro_changed(VariableItem* item) { notification_message(app->notification, &sequence_single_vibro); } -static void lcd_inverse_changed(VariableItem* item) { +static void lcd_inversion_changed(VariableItem* item) { NotificationAppSettings* app = variable_item_get_context(item); uint8_t index = variable_item_get_current_value_index(item); - variable_item_set_current_value_text(item, lcd_inverse_text[index]); - app->notification->settings.lcd_inverse = lcd_inverse_value[index]; + variable_item_set_current_value_text(item, lcd_inversion_text[index]); + app->notification->settings.lcd_inversion = lcd_inversion_value[index]; Canvas* tmp_canvas = gui_direct_draw_acquire(app->gui); - canvas_set_inverted_lcd(tmp_canvas, lcd_inverse_value[index]); + canvas_set_inverted_lcd(tmp_canvas, lcd_inversion_value[index]); gui_direct_draw_release(app->gui); notification_message(app->notification, &sequence_display_backlight_on); @@ -743,11 +743,11 @@ static NotificationAppSettings* alloc_settings(void) { } item = variable_item_list_add( - app->variable_item_list, "LCD Inverse", LCD_INVERSE_COUNT, lcd_inverse_changed, app); + app->variable_item_list,"LCD inversion",LCD_INVERSION_COUNT,lcd_inversion_changed,app); value_index = value_index_bool( - app->notification->settings.lcd_inverse, lcd_inverse_value, LCD_INVERSE_COUNT); + app->notification->settings.lcd_inversion, lcd_inversion_value, LCD_INVERSION_COUNT); variable_item_set_current_value_index(item, value_index); - variable_item_set_current_value_text(item, lcd_inverse_text[value_index]); + variable_item_set_current_value_text(item, lcd_inversion_text[value_index]); //--- RGB BACKLIGHT --- From 5e7a8cf94fc0bab6a905c256765b5376cd4a6464 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Thu, 10 Apr 2025 03:08:36 +0300 Subject: [PATCH 226/268] bump desktop setting ver [ci skip] --- applications/services/desktop/desktop_settings.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/applications/services/desktop/desktop_settings.c b/applications/services/desktop/desktop_settings.c index f5d0cf896..bc85b29b5 100644 --- a/applications/services/desktop/desktop_settings.c +++ b/applications/services/desktop/desktop_settings.c @@ -7,7 +7,7 @@ #define TAG "DesktopSettings" #define DESKTOP_SETTINGS_VER_14 (14) -#define DESKTOP_SETTINGS_VER (16) +#define DESKTOP_SETTINGS_VER (17) #define DESKTOP_SETTINGS_PATH INT_PATH(DESKTOP_SETTINGS_FILE_NAME) #define DESKTOP_SETTINGS_MAGIC (0x17) From 1b8e87ad53a69ee937a02a18dd9cd70591e3b455 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Thu, 10 Apr 2025 03:26:01 +0300 Subject: [PATCH 227/268] upd changelog --- CHANGELOG.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 26c36a642..baeba24f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,15 @@ ## Main changes - Current API: 85.0 +**WARNING! After install of this version your Desktop (fav apps) and LCD & Notifications settings will be reset to default values, please configure them again after this update!** (this is required due to big updates on that parts and config struct changes) * SubGHz: Add **Revers RB2 / RB2M Protocol** (static 64 bit) **full support** with add manually (by @xMasterX) * SubGHz: **Fix Hollarm protocol with more verification** * SubGHz: **Fix GangQi protocol** (by @DoberBit and @mishamyte (who spent 2 weeks on this)) * SubGHz: **Came Atomo button hold simulation with full cycle** simulation (to allow proper pairing with receiver) * SubGHz: Add **Prastel (42bit static code)** support (OFW PR 4178 by @pmazzini) -* System: **Night Shift Feature** (dimming backlight in selected time interval) (PR #885 | by @Dmitry422) -* System: **Сombining RGB Backlight mod** (by @quen0n) and original backlight support **in one firmware** (+ Rainbow/Wave effect (based on @Willy-JL idea)) (PR #877 #881 | by @Dmitry422) - (**To enable RGB Backlight support go into Notifications settings with Debug mode - ON**) +* Desktop: **Add support for Favorite App - Ok Long** (Warning! Old favourites apps list will be reset!) (PR #886 | by @DrEverr) +* Display: **LCD Color Inversion** (Settings - LCD and Notifications - LCD inversion.) (PR #887 | by @Dmitry422) +* Display: **Night Shift Feature** (dimming backlight in selected time interval) (PR #885 | by @Dmitry422) +* Display: **Сombining RGB Backlight mod** (by @quen0n) and original backlight support **in one firmware** (+ Rainbow/Wave effect (based on @Willy-JL idea)) (PR #877 #881 | by @Dmitry422) - (**To enable RGB Backlight support go into Notifications settings with Debug mode - ON**) * OFW: LFRFID - **EM4305 support** * OFW: **Universal IR signal selection** * OFW: **BadUSB: Mouse control** From 5eacafa16dd7033547b82fec52205dc34c51b190 Mon Sep 17 00:00:00 2001 From: Dmitry422 Date: Thu, 10 Apr 2025 18:01:47 +0700 Subject: [PATCH 228/268] Start moving RGB backlight back to Notification service --- applications/services/application.fam | 1 - .../services/notification/application.fam | 2 +- .../services/notification/notification_app.c | 241 +++++++++++++++- .../services/notification/notification_app.h | 35 ++- .../services/rgb_backlight/application.fam | 10 - .../services/rgb_backlight/rgb_backlight.c | 270 ------------------ .../services/rgb_backlight/rgb_backlight.h | 101 ------- .../rgb_backlight/rgb_backlight_settings.c | 109 ------- .../rgb_backlight/rgb_backlight_settings.h | 35 --- .../notification_settings_app.c | 154 +++++----- targets/f7/api_symbols.csv | 16 +- targets/f7/furi_hal/furi_hal_light.c | 2 +- 12 files changed, 333 insertions(+), 643 deletions(-) delete mode 100644 applications/services/rgb_backlight/application.fam delete mode 100644 applications/services/rgb_backlight/rgb_backlight.c delete mode 100644 applications/services/rgb_backlight/rgb_backlight.h delete mode 100644 applications/services/rgb_backlight/rgb_backlight_settings.c delete mode 100644 applications/services/rgb_backlight/rgb_backlight_settings.h diff --git a/applications/services/application.fam b/applications/services/application.fam index 04a2dd607..1b69d2388 100644 --- a/applications/services/application.fam +++ b/applications/services/application.fam @@ -12,6 +12,5 @@ App( "loader", "power", "namechanger_srv", - "rgb_backlight", ], ) diff --git a/applications/services/notification/application.fam b/applications/services/notification/application.fam index fbfdca848..82f94085a 100644 --- a/applications/services/notification/application.fam +++ b/applications/services/notification/application.fam @@ -4,7 +4,7 @@ App( apptype=FlipperAppType.SERVICE, entry_point="notification_srv", cdefines=["SRV_NOTIFICATION"], - requires=["input","rgb_backlight"], + requires=["input"], provides=["notification_settings"], stack_size=int(1.5 * 1024), order=100, diff --git a/applications/services/notification/notification_app.c b/applications/services/notification/notification_app.c index d416c89f9..2b0fe40f7 100644 --- a/applications/services/notification/notification_app.c +++ b/applications/services/notification/notification_app.c @@ -9,9 +9,9 @@ #include "notification.h" #include "notification_messages.h" #include "notification_app.h" -#include "applications/services/rgb_backlight/rgb_backlight.h" #define TAG "NotificationSrv" +#define COLOR_COUNT (sizeof(colors) / sizeof(RGBBacklightColor)) static const uint8_t minimal_delay = 100; static const uint8_t led_off_values[NOTIFICATION_LED_COUNT] = {0x00, 0x00, 0x00}; @@ -33,6 +33,204 @@ static uint8_t notification_settings_get_display_brightness(NotificationApp* app static uint8_t notification_settings_get_rgb_led_brightness(NotificationApp* app, uint8_t value); static uint32_t notification_settings_display_off_delay_ticks(NotificationApp* app); + +// --- RGB BACKLIGHT --- + +typedef struct { + char* name; + uint8_t red; + uint8_t green; + uint8_t blue; +} RGBBacklightColor; + +// use one type RGBBacklightColor for current_leds_settings and for static colors definition +static RGBBacklightColor current_led[] = { + {"LED0", 0, 0, 0}, + {"LED1", 0, 0, 0}, + {"LED2", 0, 0, 0}, +}; + +static const RGBBacklightColor colors[] = { + {"Orange", 255, 60, 0}, + {"Yellow", 255, 144, 0}, + {"Spring", 167, 255, 0}, + {"Lime", 0, 255, 0}, + {"Aqua", 0, 255, 127}, + {"Cyan", 0, 210, 210}, + {"Azure", 0, 127, 255}, + {"Blue", 0, 0, 255}, + {"Purple", 127, 0, 255}, + {"Magenta", 210, 0, 210}, + {"Pink", 255, 0, 127}, + {"Red", 255, 0, 0}, + {"White", 254, 210, 200}, + {"OFF", 0, 0, 0}, +}; + +uint8_t rgb_backlight_get_color_count(void) { + return COLOR_COUNT; +} + +const char* rgb_backlight_get_color_text(uint8_t index) { + return colors[index].name; +} + +// use RECORD for acces to rgb service instance and update current colors by static +void rgb_backlight_set_led_static_color(uint8_t led, uint8_t index) { + if(led < SK6805_get_led_count()) { + uint8_t r = colors[index].red; + uint8_t g = colors[index].green; + uint8_t b = colors[index].blue; + + current_led[led].red = r; + current_led[led].green = g; + current_led[led].blue = b; + + SK6805_set_led_color(led, r, g, b); + } +} + +// HSV to RGB based on +// https://www.radiokot.ru/forum/viewtopic.php?p=3000181&ysclid=m88wvoz34w244644702 +// https://radiolaba.ru/microcotrollers/tsvetnaya-lampa.html#comment-1790 +// https://alexgyver.ru/lessons/arduino-rgb/?ysclid=m88voflppa24464916 +// led number (0-2), hue (0..255), sat (0..255), val (0...1) +void rgb_backlight_set_led_custom_hsv_color(uint8_t led, uint16_t hue, uint8_t sat, float V) { + // init value + float r = 1.0f; + float g = 1.0f; + float b = 1.0f; + + // from (0..255) to (0..1) + float H = hue / 255.0f; + float S = sat / 255.0f; + + uint8_t i = trunc(H * 6); + float f = H * 6 - i; + float p = V * (1 - S); + float q = V * (1 - f * S); + float t = V * (1 - (1 - f) * S); + + switch(i) { + case 0: + r = V, g = t, b = p; + break; + case 1: + r = q, g = V, b = p; + break; + case 2: + r = p, g = V, b = t; + break; + case 3: + r = p, g = q, b = V; + break; + case 4: + r = t, g = p, b = V; + break; + case 5: + r = V, g = p, b = q; + break; + } + + // from (0..1) to (0..255) + current_led[led].red = r * 255; + current_led[led].green = g * 255; + current_led[led].blue = b * 255; +} + +// use RECORD for acces to rgb service instance, set current_* colors to led and update backlight +void rgb_backlight_update(float brightness) { + // RGBBacklightApp* app = furi_record_open(RECORD_RGB_BACKLIGHT); + + // if(app->settings.rgb.rgb_backlight_installed) { + for(uint8_t i = 0; i < SK6805_get_led_count(); i++) { + uint8_t r = current_led[i].red * brightness * 1.0f; + uint8_t g = current_led[i].green * brightness * 1.0f; + uint8_t b = current_led[i].blue * brightness * 1.0f; + SK6805_set_led_color(i, r, g, b); + } + SK6805_update(); + // } + // furi_record_close(RECORD_RGB_BACKLIGHT); +} + +// start furi timer for rainbow +void rainbow_timer_start(NotificationApp* app) { + if(furi_timer_is_running(app->rainbow_timer)) { + furi_timer_stop(app->rainbow_timer); + } + furi_timer_start(app->rainbow_timer, furi_ms_to_ticks(app->settings.rgb.rainbow_speed_ms)); +} + +// stop furi timer for rainbow +void rainbow_timer_stop(NotificationApp* app) { + if(furi_timer_is_running(app->rainbow_timer)) { + furi_timer_stop(app->rainbow_timer); + } +} + +// if rgb_backlight_installed then apply rainbow colors to backlight and start/restart/stop rainbow_timer +void rainbow_timer_starter(NotificationApp* app) { + if((app->settings.rgb.rainbow_mode > 0) && (app->settings.rgb.rgb_backlight_installed)) { + rainbow_timer_start(app); + } +} + +static void rainbow_timer_callback(void* context) { + furi_assert(context); + NotificationApp* app = context; + + if(app->settings.rgb.rgb_backlight_installed) { + app->rainbow_hue += app->settings.rgb.rainbow_step; + if(app->rainbow_hue > 254) { + app->rainbow_hue = 0; + } + + uint8_t wide = app->settings.rgb.rainbow_wide; + + switch(app->settings.rgb.rainbow_mode) { + //rainbow mode + case 1: + for(uint8_t i = 0; i < SK6805_get_led_count(); i++) { + rgb_backlight_set_led_custom_hsv_color( + i, + app->rainbow_hue, + app->settings.rgb.rainbow_saturation, + app->settings.display_brightness); + } + break; + + //wave mode + case 2: + uint16_t j = app->rainbow_hue + wide; + uint16_t k = app->rainbow_hue + wide * 2; + + if(app->rainbow_hue > (254 - wide)) { + j = j - 255; + } + if(app->rainbow_hue > (254 - wide * 2)) { + k = k - 255; + } + + rgb_backlight_set_led_custom_hsv_color( + 0, app->rainbow_hue, app->settings.rgb.rainbow_saturation, app->settings.display_brightness); + rgb_backlight_set_led_custom_hsv_color( + 1, j, app->settings.rgb.rainbow_saturation, app->settings.display_brightness); + rgb_backlight_set_led_custom_hsv_color( + 2, k, app->settings.rgb.rainbow_saturation, app->settings.display_brightness); + break; + + default: + break; + } + + rgb_backlight_update(app->settings.led_brightness * app->current_night_shift); + } +} + +// --- RGB BACKLIGHT ENF--- + + // --- NIGHT SHIFT --- void night_shift_timer_start(NotificationApp* app) { @@ -64,10 +262,8 @@ void night_shift_timer_callback(void* context) { // set values to stock and rgb backlights if((time > app->settings.night_shift_end) && (time < app->settings.night_shift_start)) { app->current_night_shift = 1.0f; - app->rgb_srv->current_night_shift = 1.0f; } else { app->current_night_shift = app->settings.night_shift; - app->rgb_srv->current_night_shift = app->settings.night_shift; } } @@ -267,7 +463,7 @@ static void notification_process_notification_message( reset_mask |= reset_display_mask; //start rgb_mod_rainbow_timer when display backlight is ON and all corresponding settings is ON too - rainbow_timer_starter(app->rgb_srv); + rainbow_timer_starter(app); // --- NIGHT SHIFT END --- } else { reset_mask &= ~reset_display_mask; @@ -276,8 +472,8 @@ static void notification_process_notification_message( furi_timer_stop(app->display_timer); } //stop rgb_mod_rainbow_timer when display backlight is OFF - if(furi_timer_is_running(app->rgb_srv->rainbow_timer)) { - rainbow_timer_stop(app->rgb_srv); + if(furi_timer_is_running(app->rainbow_timer)) { + rainbow_timer_stop(app); } } break; @@ -610,8 +806,7 @@ static NotificationApp* notification_app_alloc(void) { notification_message(app, &sequence_display_backlight_on); // --- NIGHT SHIFT --- - app->rgb_srv = furi_record_open(RECORD_RGB_BACKLIGHT); - app->rgb_srv->current_night_shift = 1.0f; + app->current_night_shift = 1.0f; app->current_night_shift = 1.0f; app->settings.night_shift = 1.0f; app->settings.night_shift_start = 1020; @@ -620,6 +815,7 @@ static NotificationApp* notification_app_alloc(void) { furi_timer_alloc(night_shift_timer_callback, FuriTimerTypePeriodic, app); // --- NIGHT SHIFT END --- + // use RECORD for setup init values to canvas lcd_inverted Gui* tmp_gui = furi_record_open(RECORD_GUI); Canvas* tmp_canvas = gui_direct_draw_acquire(tmp_gui); canvas_set_inverted_lcd(tmp_canvas, false); @@ -682,6 +878,35 @@ int32_t notification_srv(void* p) { UNUSED(p); NotificationApp* app = notification_app_alloc(); + // --- RGB BACKLIGHT SECTION --- + + // define rainbow_timer and they callback + app->rainbow_timer = furi_timer_alloc(rainbow_timer_callback, FuriTimerTypePeriodic, app); + + // init values + app->rainbow_hue = 1; + app->current_night_shift = 1.0f; + + // if rgb_backlight_installed then start rainbow or set leds colors from saved settings (default index = 0) + if(app->settings.rgb.rgb_backlight_installed) { + if(app->settings.rgb.rainbow_mode > 0) { + rainbow_timer_start(app); + } else { + rgb_backlight_set_led_static_color(2, app->settings.rgb.led_2_color_index); + rgb_backlight_set_led_static_color(1, app->settings.rgb.led_1_color_index); + rgb_backlight_set_led_static_color(0, app->settings.rgb.led_0_color_index); + rgb_backlight_update(app->settings.display_brightness * app->current_night_shift); + } + // if rgb_backlight not installed then set default static orange color(index=0) to all leds (0-2) and force light on + } else { + rgb_backlight_set_led_static_color(2, 0); + rgb_backlight_set_led_static_color(1, 0); + rgb_backlight_set_led_static_color(0, 0); + SK6805_update(); + } + + // --- RGB BACKLIGHT SECTION END --- + notification_init_settings(app); notification_vibro_off(); diff --git a/applications/services/notification/notification_app.h b/applications/services/notification/notification_app.h index 7ac8528f2..7396e695e 100644 --- a/applications/services/notification/notification_app.h +++ b/applications/services/notification/notification_app.h @@ -3,7 +3,8 @@ #include "notification.h" #include "notification_messages.h" #include "notification_settings_filename.h" -#include "applications/services/rgb_backlight/rgb_backlight.h" +#include +#include #define NOTIFICATION_LED_COUNT 3 #define NOTIFICATION_EVENT_COMPLETE 0x00000001U @@ -37,6 +38,23 @@ typedef struct { #define NOTIFICATION_SETTINGS_VERSION 0x04 #define NOTIFICATION_SETTINGS_PATH INT_PATH(NOTIFICATION_SETTINGS_FILE_NAME) +typedef struct { + //Common settings + uint8_t rgb_backlight_installed; + + // static gradient mode settings + uint8_t led_2_color_index; + uint8_t led_1_color_index; + uint8_t led_0_color_index; + + // rainbow mode setings + uint32_t rainbow_mode; + uint32_t rainbow_speed_ms; + uint16_t rainbow_step; + uint8_t rainbow_saturation; + uint8_t rainbow_wide; +} RGBBacklightSettings; + typedef struct { uint8_t version; float display_brightness; @@ -49,6 +67,7 @@ typedef struct { uint32_t night_shift_start; uint32_t night_shift_end; bool lcd_inversion; + RGBBacklightSettings rgb; } NotificationSettings; struct NotificationApp { @@ -61,12 +80,24 @@ struct NotificationApp { uint8_t display_led_lock; NotificationSettings settings; - RGBBacklightApp* rgb_srv; FuriTimer* night_shift_timer; float current_night_shift; + + FuriTimer* rainbow_timer; + uint16_t rainbow_hue; + uint8_t rainbow_red; + uint8_t rainbow_green; + uint8_t rainbow_blue; }; void notification_message_save_settings(NotificationApp* app); void night_shift_timer_start(NotificationApp* app); void night_shift_timer_stop(NotificationApp* app); +void rgb_backlight_update(float brightness); +void rgb_backlight_set_led_static_color(uint8_t led, uint8_t index); +void rainbow_timer_start(NotificationApp* app); +void rainbow_timer_stop(NotificationApp* app); +void rainbow_timer_starter(NotificationApp* app); +const char* rgb_backlight_get_color_text(uint8_t index); +uint8_t rgb_backlight_get_color_count(void); \ No newline at end of file diff --git a/applications/services/rgb_backlight/application.fam b/applications/services/rgb_backlight/application.fam deleted file mode 100644 index 39954efaa..000000000 --- a/applications/services/rgb_backlight/application.fam +++ /dev/null @@ -1,10 +0,0 @@ -App( - appid="rgb_backlight", - name="RgbBackLightSrv", - apptype=FlipperAppType.SERVICE, - entry_point="rgb_backlight_srv", - cdefines=["SRV_RGB_BACKLIGHT"], - stack_size=1 * 1024, - order=1290, - sdk_headers=["rgb_backlight.h"], -) diff --git a/applications/services/rgb_backlight/rgb_backlight.c b/applications/services/rgb_backlight/rgb_backlight.c deleted file mode 100644 index 22628e3af..000000000 --- a/applications/services/rgb_backlight/rgb_backlight.c +++ /dev/null @@ -1,270 +0,0 @@ -/* - RGB BackLight Service based on - RGB backlight FlipperZero driver - Copyright (C) 2022-2023 Victor Nikitchuk (https://github.com/quen0n) - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . -*/ - -#include -#include -#include -#include -#include -#include "rgb_backlight.h" -#include - -#define COLOR_COUNT (sizeof(colors) / sizeof(RGBBacklightColor)) - -#define TAG "RGB_BACKLIGHT_SRV" - -typedef struct { - char* name; - uint8_t red; - uint8_t green; - uint8_t blue; -} RGBBacklightColor; - -// use one type RGBBacklightColor for current_leds_settings and for static colors definition -static RGBBacklightColor current_led[] = { - {"LED0", 0, 0, 0}, - {"LED1", 0, 0, 0}, - {"LED2", 0, 0, 0}, -}; - -static const RGBBacklightColor colors[] = { - {"Orange", 255, 60, 0}, - {"Yellow", 255, 144, 0}, - {"Spring", 167, 255, 0}, - {"Lime", 0, 255, 0}, - {"Aqua", 0, 255, 127}, - {"Cyan", 0, 210, 210}, - {"Azure", 0, 127, 255}, - {"Blue", 0, 0, 255}, - {"Purple", 127, 0, 255}, - {"Magenta", 210, 0, 210}, - {"Pink", 255, 0, 127}, - {"Red", 255, 0, 0}, - {"White", 254, 210, 200}, - {"OFF", 0, 0, 0}, -}; - -uint8_t rgb_backlight_get_color_count(void) { - return COLOR_COUNT; -} - -const char* rgb_backlight_get_color_text(uint8_t index) { - return colors[index].name; -} - -// use RECORD for acces to rgb service instance and update current colors by static -void rgb_backlight_set_led_static_color(uint8_t led, uint8_t index) { - if(led < SK6805_get_led_count()) { - uint8_t r = colors[index].red; - uint8_t g = colors[index].green; - uint8_t b = colors[index].blue; - - current_led[led].red = r; - current_led[led].green = g; - current_led[led].blue = b; - - SK6805_set_led_color(led, r, g, b); - } -} - -// HSV to RGB based on -// https://www.radiokot.ru/forum/viewtopic.php?p=3000181&ysclid=m88wvoz34w244644702 -// https://radiolaba.ru/microcotrollers/tsvetnaya-lampa.html#comment-1790 -// https://alexgyver.ru/lessons/arduino-rgb/?ysclid=m88voflppa24464916 -// led number (0-2), hue (0..255), sat (0..255), val (0...1) -void rgb_backlight_set_led_custom_hsv_color(uint8_t led, uint16_t hue, uint8_t sat, float V) { - // init value - float r = 1.0f; - float g = 1.0f; - float b = 1.0f; - - // from (0..255) to (0..1) - float H = hue / 255.0f; - float S = sat / 255.0f; - - uint8_t i = trunc(H * 6); - float f = H * 6 - i; - float p = V * (1 - S); - float q = V * (1 - f * S); - float t = V * (1 - (1 - f) * S); - - switch(i) { - case 0: - r = V, g = t, b = p; - break; - case 1: - r = q, g = V, b = p; - break; - case 2: - r = p, g = V, b = t; - break; - case 3: - r = p, g = q, b = V; - break; - case 4: - r = t, g = p, b = V; - break; - case 5: - r = V, g = p, b = q; - break; - } - - // from (0..1) to (0..255) - current_led[led].red = r * 255; - current_led[led].green = g * 255; - current_led[led].blue = b * 255; -} - -// use RECORD for acces to rgb service instance, set current_* colors to led and update backlight -void rgb_backlight_update(float brightness) { - RGBBacklightApp* app = furi_record_open(RECORD_RGB_BACKLIGHT); - - if(app->settings->rgb_backlight_installed) { - for(uint8_t i = 0; i < SK6805_get_led_count(); i++) { - uint8_t r = current_led[i].red * brightness * 1.0f; - uint8_t g = current_led[i].green * brightness * 1.0f; - uint8_t b = current_led[i].blue * brightness * 1.0f; - SK6805_set_led_color(i, r, g, b); - } - SK6805_update(); - } - furi_record_close(RECORD_RGB_BACKLIGHT); -} - -// start furi timer for rainbow -void rainbow_timer_start(RGBBacklightApp* app) { - if(furi_timer_is_running(app->rainbow_timer)) { - furi_timer_stop(app->rainbow_timer); - } - furi_timer_start(app->rainbow_timer, furi_ms_to_ticks(app->settings->rainbow_speed_ms)); -} - -// stop furi timer for rainbow -void rainbow_timer_stop(RGBBacklightApp* app) { - if(furi_timer_is_running(app->rainbow_timer)) { - furi_timer_stop(app->rainbow_timer); - } -} - -// if rgb_backlight_installed then apply rainbow colors to backlight and start/restart/stop rainbow_timer -void rainbow_timer_starter(RGBBacklightApp* app) { - if((app->settings->rainbow_mode > 0) && (app->settings->rgb_backlight_installed)) { - rainbow_timer_start(app); - } -} - -static void rainbow_timer_callback(void* context) { - furi_assert(context); - RGBBacklightApp* app = context; - - if(app->settings->rgb_backlight_installed) { - app->rainbow_hue += app->settings->rainbow_step; - if(app->rainbow_hue > 254) { - app->rainbow_hue = 0; - } - - uint8_t wide = app->settings->rainbow_wide; - - switch(app->settings->rainbow_mode) { - //rainbow mode - case 1: - for(uint8_t i = 0; i < SK6805_get_led_count(); i++) { - rgb_backlight_set_led_custom_hsv_color( - i, - app->rainbow_hue, - app->settings->rainbow_saturation, - app->settings->brightness); - } - break; - - //wave mode - case 2: - uint16_t j = app->rainbow_hue + wide; - uint16_t k = app->rainbow_hue + wide * 2; - - if(app->rainbow_hue > (254 - wide)) { - j = j - 255; - } - if(app->rainbow_hue > (254 - wide * 2)) { - k = k - 255; - } - - rgb_backlight_set_led_custom_hsv_color( - 0, app->rainbow_hue, app->settings->rainbow_saturation, app->settings->brightness); - rgb_backlight_set_led_custom_hsv_color( - 1, j, app->settings->rainbow_saturation, app->settings->brightness); - rgb_backlight_set_led_custom_hsv_color( - 2, k, app->settings->rainbow_saturation, app->settings->brightness); - break; - - default: - break; - } - - rgb_backlight_update(app->settings->brightness * app->current_night_shift); - } -} - -int32_t rgb_backlight_srv(void* p) { - UNUSED(p); - - // Define object app (full app with settings and running variables), - // allocate memory and create RECORD for access to app structure from outside - RGBBacklightApp* app = malloc(sizeof(RGBBacklightApp)); - - // define rainbow_timer and they callback - app->rainbow_timer = furi_timer_alloc(rainbow_timer_callback, FuriTimerTypePeriodic, app); - - // settings load or create default - app->settings = malloc(sizeof(RGBBacklightSettings)); - rgb_backlight_settings_load(app->settings); - - app->rainbow_hue = 1; - app->current_night_shift = 1.0f; - - furi_record_create(RECORD_RGB_BACKLIGHT, app); - - // if rgb_backlight_installed then start rainbow or set leds colors from saved settings (default index = 0) - if(app->settings->rgb_backlight_installed) { - if(app->settings->rainbow_mode > 0) { - rainbow_timer_start(app); - } else { - rgb_backlight_set_led_static_color(2, app->settings->led_2_color_index); - rgb_backlight_set_led_static_color(1, app->settings->led_1_color_index); - rgb_backlight_set_led_static_color(0, app->settings->led_0_color_index); - rgb_backlight_update(app->settings->brightness * app->current_night_shift); - } - // if rgb_backlight not installed then set default static orange color(index=0) to all leds (0-2) and force light on - } else { - rgb_backlight_set_led_static_color(2, 0); - rgb_backlight_set_led_static_color(1, 0); - rgb_backlight_set_led_static_color(0, 0); - SK6805_update(); - } - - while(1) { - furi_delay_ms(5000); - if(app->settings->rgb_backlight_installed) { - FURI_LOG_D(TAG, "RGB backlight enabled - serivce is running"); - } else { - FURI_LOG_D(TAG, "RGB backlight DISABLED - serivce is running"); - } - } - return 0; -} diff --git a/applications/services/rgb_backlight/rgb_backlight.h b/applications/services/rgb_backlight/rgb_backlight.h deleted file mode 100644 index 1be252dc1..000000000 --- a/applications/services/rgb_backlight/rgb_backlight.h +++ /dev/null @@ -1,101 +0,0 @@ -/* - RGB BackLight Service based on - RGB backlight FlipperZero driver - Copyright (C) 2022-2023 Victor Nikitchuk (https://github.com/quen0n) - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . -*/ - -#pragma once -#include -#include "rgb_backlight_settings.h" -#include "SK6805.h" - -#ifdef __cplusplus -extern "C" { -#endif - -typedef struct { - FuriTimer* rainbow_timer; - uint16_t rainbow_hue; - uint8_t rainbow_red; - uint8_t rainbow_green; - uint8_t rainbow_blue; - RGBBacklightSettings* settings; - // night_shift multiplicator for leds brightnes coming from Notificatoin app. - float current_night_shift; - -} RGBBacklightApp; - -#define RECORD_RGB_BACKLIGHT "rgb_backlight" - -/** Update leds colors from current_led[i].color and selected bright - * - * @param brightness - Brightness 0..1 - * @return - */ -void rgb_backlight_update(float brightness); - -/** Set current_led[i].color for one led by static color index - * - * @param led - Led number (0..2) - * @param index - Static color index number - * @return - */ -void rgb_backlight_set_led_static_color(uint8_t led, uint8_t index); - -/** Stop rainbow timer - * - * @param app - Instance of RGBBacklightApp from FURI RECORD - * @return - */ -void rainbow_timer_stop(RGBBacklightApp* app); - -/** Start rainbow timer - * - * @param app - Instance of RGBBacklightApp from FURI RECORD - * @return - */ - -/** Start rainbow timer - * - * @param app - Instance of RGBBacklightApp from FURI RECORD - * @return - */ -void rainbow_timer_start(RGBBacklightApp* app); - -/** Start rainbow timer only if all conditions meet (rgb_backlight_installed && rainbow ON) - * - * @param app - Instance of RGBBacklightApp from FURI RECORD - * @return - */ -void rainbow_timer_starter(RGBBacklightApp* app); - -/** Get name of static color by index - * - * @param index - Static colors index number - * @return - color name - */ -const char* rgb_backlight_get_color_text(uint8_t index); - -/** Get static colors count - * - * @param - * @return - colors count - */ -uint8_t rgb_backlight_get_color_count(void); - -#ifdef __cplusplus -} -#endif diff --git a/applications/services/rgb_backlight/rgb_backlight_settings.c b/applications/services/rgb_backlight/rgb_backlight_settings.c deleted file mode 100644 index baa573a77..000000000 --- a/applications/services/rgb_backlight/rgb_backlight_settings.c +++ /dev/null @@ -1,109 +0,0 @@ -#include "rgb_backlight_settings.h" - -#include -#include - -#define TAG "RGBBackligthSettings" - -#define RGB_BACKLIGHT_SETTINGS_FILE_NAME ".rgb_backlight.settings" -#define RGB_BACKLIGHT_SETTINGS_PATH INT_PATH(RGB_BACKLIGHT_SETTINGS_FILE_NAME) - -#define RGB_BACKLIGHT_SETTINGS_MAGIC (0x30) -#define RGB_BACKLIGHT_SETTINGS_VER_PREV (4) // Previous version number -#define RGB_BACKLIGHT_SETTINGS_VER (5) // Current version number - -//pervious settings must be copyed from previous rgb_backlight_settings.h file -typedef struct { - //Common settings - uint8_t version; - uint8_t rgb_backlight_installed; - float brightness; - - // static gradient mode settings - uint8_t led_2_color_index; - uint8_t led_1_color_index; - uint8_t led_0_color_index; - - // rainbow mode setings - uint32_t rainbow_mode; - uint32_t rainbow_speed_ms; - uint16_t rainbow_step; - uint8_t rainbow_saturation; - uint8_t rainbow_wide; - -} RGBBacklightSettingsPrevious; - -void rgb_backlight_settings_load(RGBBacklightSettings* settings) { - furi_assert(settings); - - bool success = false; - - //a useless cycle do-while, may will be used in future with anoter condition - do { - // take version from settings file metadata, if cant then break and fill settings with 0 and save to settings file; - uint8_t version; - if(!saved_struct_get_metadata(RGB_BACKLIGHT_SETTINGS_PATH, NULL, &version, NULL)) break; - - // if config actual version - load it directly - if(version == RGB_BACKLIGHT_SETTINGS_VER) { - success = saved_struct_load( - RGB_BACKLIGHT_SETTINGS_PATH, - settings, - sizeof(RGBBacklightSettings), - RGB_BACKLIGHT_SETTINGS_MAGIC, - RGB_BACKLIGHT_SETTINGS_VER); - // if config previous version - load it and inicialize new settings - } else if(version == RGB_BACKLIGHT_SETTINGS_VER_PREV) { - RGBBacklightSettingsPrevious* settings_previous = - malloc(sizeof(RGBBacklightSettingsPrevious)); - - success = saved_struct_load( - RGB_BACKLIGHT_SETTINGS_PATH, - settings_previous, - sizeof(RGBBacklightSettingsPrevious), - RGB_BACKLIGHT_SETTINGS_MAGIC, - RGB_BACKLIGHT_SETTINGS_VER_PREV); - // new settings initialization - if(success) { - // copy loaded old settings as part of new - uint32_t size = sizeof(settings); - memcpy(settings, settings_previous, size); - // set new options to initial value - // settings.something = something; - } - - free(settings_previous); - } - // in case of another config version we exit from useless cycle to next step - } while(false); - - // fill settings with 0 and save to settings file; - // Orange color (index=0) will be default - if(!success) { - FURI_LOG_W(TAG, "Failed to load file, using defaults"); - memset(settings, 0, sizeof(RGBBacklightSettings)); - settings->brightness = 1.0f; - settings->rainbow_speed_ms = 100; - settings->rainbow_step = 1; - settings->rainbow_saturation = 255; - settings->rainbow_wide = 50; - rgb_backlight_settings_save(settings); - } -} - -void rgb_backlight_settings_save(const RGBBacklightSettings* settings) { - furi_assert(settings); - - const bool success = saved_struct_save( - RGB_BACKLIGHT_SETTINGS_PATH, - settings, - sizeof(RGBBacklightSettings), - RGB_BACKLIGHT_SETTINGS_MAGIC, - RGB_BACKLIGHT_SETTINGS_VER); - - if(!success) { - FURI_LOG_E(TAG, "Failed to save rgb_backlight_settings file"); - } else { - FURI_LOG_I(TAG, "Settings saved"); - } -} diff --git a/applications/services/rgb_backlight/rgb_backlight_settings.h b/applications/services/rgb_backlight/rgb_backlight_settings.h deleted file mode 100644 index 3f3af005f..000000000 --- a/applications/services/rgb_backlight/rgb_backlight_settings.h +++ /dev/null @@ -1,35 +0,0 @@ -#pragma once - -#include -#include - -typedef struct { - //Common settings - uint8_t version; - uint8_t rgb_backlight_installed; - float brightness; - - // static gradient mode settings - uint8_t led_2_color_index; - uint8_t led_1_color_index; - uint8_t led_0_color_index; - - // rainbow mode setings - uint32_t rainbow_mode; - uint32_t rainbow_speed_ms; - uint16_t rainbow_step; - uint8_t rainbow_saturation; - uint8_t rainbow_wide; - -} RGBBacklightSettings; - -#ifdef __cplusplus -extern "C" { -#endif - -void rgb_backlight_settings_load(RGBBacklightSettings* settings); -void rgb_backlight_settings_save(const RGBBacklightSettings* settings); - -#ifdef __cplusplus -} -#endif diff --git a/applications/settings/notification_settings/notification_settings_app.c b/applications/settings/notification_settings/notification_settings_app.c index 4362039f0..7907d167d 100644 --- a/applications/settings/notification_settings/notification_settings_app.c +++ b/applications/settings/notification_settings/notification_settings_app.c @@ -4,9 +4,9 @@ #include #include #include -#include "applications/services/rgb_backlight/rgb_backlight.h" -#define MAX_NOTIFICATION_SETTINGS 4 + +#define MAX_NOTIFICATION_SETTINGS 5 typedef struct { NotificationApp* notification; @@ -293,12 +293,6 @@ static void backlight_changed(VariableItem* item) { variable_item_set_current_value_text(item, backlight_text[index]); app->notification->settings.display_brightness = backlight_value[index]; - //--- RGB BACKLIGHT --- - // set selected brightness to current rgb backlight service settings and save settings - app->notification->rgb_srv->settings->brightness = backlight_value[index]; - rgb_backlight_settings_save(app->notification->rgb_srv->settings); - //--- RGB BACKLIGHT END --- - notification_message(app->notification, &sequence_display_backlight_on); } @@ -368,12 +362,9 @@ static void rgb_backlight_installed_changed(VariableItem* item) { NotificationAppSettings* app = variable_item_get_context(item); uint8_t index = variable_item_get_current_value_index(item); variable_item_set_current_value_text(item, rgb_backlight_installed_text[index]); - app->notification->rgb_srv->settings->rgb_backlight_installed = + app->notification->settings.rgb.rgb_backlight_installed = rgb_backlight_installed_value[index]; - app->notification->rgb_srv->settings->brightness = - app->notification->settings.display_brightness; - // In case of user playing with rgb_backlight_installed swith: // if user swith_off rgb_backlight_installed (but may be he have mod installed) // then force set default orange color and stop rainbow timer @@ -382,18 +373,18 @@ static void rgb_backlight_installed_changed(VariableItem* item) { rgb_backlight_set_led_static_color(1, 0); rgb_backlight_set_led_static_color(0, 0); SK6805_update(); - rainbow_timer_stop(app->notification->rgb_srv); + rainbow_timer_stop(app->notification); // start rainbow (if its Enabled) or set saved static colors if user swith_on rgb_backlight_installed switch } else { - if(app->notification->rgb_srv->settings->rainbow_mode > 0) { - rainbow_timer_starter(app->notification->rgb_srv); + if(app->notification->settings.rgb.rainbow_mode > 0) { + rainbow_timer_starter(app->notification); } else { rgb_backlight_set_led_static_color( - 2, app->notification->rgb_srv->settings->led_2_color_index); + 2, app->notification->settings.rgb.led_2_color_index); rgb_backlight_set_led_static_color( - 1, app->notification->rgb_srv->settings->led_1_color_index); + 1, app->notification->settings.rgb.led_1_color_index); rgb_backlight_set_led_static_color( - 0, app->notification->rgb_srv->settings->led_0_color_index); + 0, app->notification->settings.rgb.led_0_color_index); rgb_backlight_update( app->notification->settings.display_brightness * app->notification->current_night_shift); @@ -413,8 +404,7 @@ static void rgb_backlight_installed_changed(VariableItem* item) { variable_item_set_locked(t_item, false, "RGB\nOFF!"); } } - - rgb_backlight_settings_save(app->notification->rgb_srv->settings); + notification_message_save_settings(app->notification); } static void led_2_color_changed(VariableItem* item) { @@ -422,17 +412,17 @@ static void led_2_color_changed(VariableItem* item) { uint8_t index = variable_item_get_current_value_index(item); variable_item_set_current_value_text(item, rgb_backlight_get_color_text(index)); - app->notification->rgb_srv->settings->led_2_color_index = index; + app->notification->settings.rgb.led_2_color_index = index; // dont update screen color if rainbow timer working - if(!furi_timer_is_running(app->notification->rgb_srv->rainbow_timer)) { + if(!furi_timer_is_running(app->notification->rainbow_timer)) { rgb_backlight_set_led_static_color(2, index); rgb_backlight_update( - app->notification->rgb_srv->settings->brightness * + app->notification->settings.display_brightness * app->notification->current_night_shift); } - rgb_backlight_settings_save(app->notification->rgb_srv->settings); + notification_message_save_settings(app->notification); } static void led_1_color_changed(VariableItem* item) { @@ -440,17 +430,17 @@ static void led_1_color_changed(VariableItem* item) { uint8_t index = variable_item_get_current_value_index(item); variable_item_set_current_value_text(item, rgb_backlight_get_color_text(index)); - app->notification->rgb_srv->settings->led_1_color_index = index; + app->notification->settings.rgb.led_1_color_index = index; // dont update screen color if rainbow timer working - if(!furi_timer_is_running(app->notification->rgb_srv->rainbow_timer)) { + if(!furi_timer_is_running(app->notification->rainbow_timer)) { rgb_backlight_set_led_static_color(1, index); rgb_backlight_update( - app->notification->rgb_srv->settings->brightness * + app->notification->settings.display_brightness * app->notification->current_night_shift); } - rgb_backlight_settings_save(app->notification->rgb_srv->settings); + notification_message_save_settings(app->notification); } static void led_0_color_changed(VariableItem* item) { @@ -458,17 +448,17 @@ static void led_0_color_changed(VariableItem* item) { uint8_t index = variable_item_get_current_value_index(item); variable_item_set_current_value_text(item, rgb_backlight_get_color_text(index)); - app->notification->rgb_srv->settings->led_0_color_index = index; + app->notification->settings.rgb.led_0_color_index = index; // dont update screen color if rainbow timer working - if(!furi_timer_is_running(app->notification->rgb_srv->rainbow_timer)) { + if(!furi_timer_is_running(app->notification->rainbow_timer)) { rgb_backlight_set_led_static_color(0, index); rgb_backlight_update( - app->notification->rgb_srv->settings->brightness * + app->notification->settings.display_brightness * app->notification->current_night_shift); } - rgb_backlight_settings_save(app->notification->rgb_srv->settings); + notification_message_save_settings(app->notification); } static void rgb_backlight_rainbow_changed(VariableItem* item) { @@ -476,25 +466,25 @@ static void rgb_backlight_rainbow_changed(VariableItem* item) { uint8_t index = variable_item_get_current_value_index(item); variable_item_set_current_value_text(item, rgb_backlight_rainbow_mode_text[index]); - app->notification->rgb_srv->settings->rainbow_mode = rgb_backlight_rainbow_mode_value[index]; + app->notification->settings.rgb.rainbow_mode = rgb_backlight_rainbow_mode_value[index]; // restore saved rgb backlight settings if we switch_off effects if(index == 0) { rgb_backlight_set_led_static_color( - 2, app->notification->rgb_srv->settings->led_2_color_index); + 2, app->notification->settings.rgb.led_2_color_index); rgb_backlight_set_led_static_color( - 1, app->notification->rgb_srv->settings->led_1_color_index); + 1, app->notification->settings.rgb.led_1_color_index); rgb_backlight_set_led_static_color( - 0, app->notification->rgb_srv->settings->led_0_color_index); + 0, app->notification->settings.rgb.led_0_color_index); rgb_backlight_update( - app->notification->rgb_srv->settings->brightness * + app->notification->settings.display_brightness * app->notification->current_night_shift); - rainbow_timer_stop(app->notification->rgb_srv); + rainbow_timer_stop(app->notification); } else { - rainbow_timer_starter(app->notification->rgb_srv); + rainbow_timer_starter(app->notification); } - rgb_backlight_settings_save(app->notification->rgb_srv->settings); + notification_message_save_settings(app->notification); } static void rgb_backlight_rainbow_speed_changed(VariableItem* item) { @@ -502,12 +492,12 @@ static void rgb_backlight_rainbow_speed_changed(VariableItem* item) { uint8_t index = variable_item_get_current_value_index(item); variable_item_set_current_value_text(item, rgb_backlight_rainbow_speed_text[index]); - app->notification->rgb_srv->settings->rainbow_speed_ms = + app->notification->settings.rgb.rainbow_speed_ms = rgb_backlight_rainbow_speed_value[index]; // save settings and restart timer with new speed value - rainbow_timer_starter(app->notification->rgb_srv); - rgb_backlight_settings_save(app->notification->rgb_srv->settings); + rainbow_timer_starter(app->notification); + notification_message_save_settings(app->notification); } static void rgb_backlight_rainbow_step_changed(VariableItem* item) { @@ -515,9 +505,9 @@ static void rgb_backlight_rainbow_step_changed(VariableItem* item) { uint8_t index = variable_item_get_current_value_index(item); variable_item_set_current_value_text(item, rgb_backlight_rainbow_step_text[index]); - app->notification->rgb_srv->settings->rainbow_step = rgb_backlight_rainbow_step_value[index]; + app->notification->settings.rgb.rainbow_step = rgb_backlight_rainbow_step_value[index]; - rgb_backlight_settings_save(app->notification->rgb_srv->settings); + notification_message_save_settings(app->notification); } static void rgb_backlight_rainbow_saturation_changed(VariableItem* item) { @@ -528,9 +518,9 @@ static void rgb_backlight_rainbow_saturation_changed(VariableItem* item) { char valtext[4] = {}; snprintf(valtext, sizeof(valtext), "%d", index); variable_item_set_current_value_text(item, valtext); - app->notification->rgb_srv->settings->rainbow_saturation = index; + app->notification->settings.rgb.rainbow_saturation = index; - rgb_backlight_settings_save(app->notification->rgb_srv->settings); + notification_message_save_settings(app->notification); } static void rgb_backlight_rainbow_wide_changed(VariableItem* item) { @@ -538,24 +528,24 @@ static void rgb_backlight_rainbow_wide_changed(VariableItem* item) { uint8_t index = variable_item_get_current_value_index(item); variable_item_set_current_value_text(item, rgb_backlight_rainbow_wide_text[index]); - app->notification->rgb_srv->settings->rainbow_wide = rgb_backlight_rainbow_wide_value[index]; + app->notification->settings.rgb.rainbow_wide = rgb_backlight_rainbow_wide_value[index]; - rgb_backlight_settings_save(app->notification->rgb_srv->settings); + notification_message_save_settings(app->notification); } -// open rgb_settings_view if user press OK on first (index=0) menu string and (debug mode or rgb_backlight_installed is true) +// open settings.rgb_view if user press OK on first (index=0) menu string and (debug mode or rgb_backlight_installed is true) void variable_item_list_enter_callback(void* context, uint32_t index) { UNUSED(context); NotificationAppSettings* app = context; - if(((app->notification->rgb_srv->settings->rgb_backlight_installed) || + if(((app->notification->settings.rgb.rgb_backlight_installed) || (furi_hal_rtc_is_flag_set(FuriHalRtcFlagDebug))) && (index == 0)) { view_dispatcher_switch_to_view(app->view_dispatcher, RGBViewId); } } -// switch to main view on exit from rgb_settings_view +// switch to main view on exit from settings.rgb_view static uint32_t notification_app_rgb_settings_exit(void* context) { UNUSED(context); return MainViewId; @@ -571,14 +561,14 @@ static void night_shift_changed(VariableItem* item) { variable_item_set_current_value_text(item, night_shift_text[index]); app->notification->settings.night_shift = night_shift_value[index]; app->notification->current_night_shift = night_shift_value[index]; - app->notification->rgb_srv->current_night_shift = night_shift_value[index]; + app->notification->current_night_shift = night_shift_value[index]; // force demo night_shift brightness ot rgb backlight and stock backlight notification_message(app->notification, &sequence_display_backlight_on); int slide = 0; if(furi_hal_rtc_is_flag_set(FuriHalRtcFlagDebug) || - (app->notification->rgb_srv->settings->rgb_backlight_installed)) { + (app->notification->settings.rgb.rgb_backlight_installed)) { slide = 1; } for(int i = 4 + slide; i < (6 + slide); i++) { @@ -646,7 +636,7 @@ static NotificationAppSettings* alloc_settings(void) { app->variable_item_list, variable_item_list_enter_callback, app); // Show RGB settings only when debug_mode or rgb_backlight_installed is active - if((app->notification->rgb_srv->settings->rgb_backlight_installed) || + if((app->notification->settings.rgb.rgb_backlight_installed) || (furi_hal_rtc_is_flag_set(FuriHalRtcFlagDebug))) { item = variable_item_list_add(app->variable_item_list, "RGB settings", 0, NULL, app); } @@ -743,7 +733,7 @@ static NotificationAppSettings* alloc_settings(void) { } item = variable_item_list_add( - app->variable_item_list,"LCD inversion",LCD_INVERSION_COUNT,lcd_inversion_changed,app); + app->variable_item_list, "LCD inversion", LCD_INVERSION_COUNT, lcd_inversion_changed, app); value_index = value_index_bool( app->notification->settings.lcd_inversion, lcd_inversion_value, LCD_INVERSION_COUNT); variable_item_set_current_value_index(item, value_index); @@ -754,7 +744,7 @@ static NotificationAppSettings* alloc_settings(void) { app->variable_item_list_rgb = variable_item_list_alloc(); View* view_rgb = variable_item_list_get_view(app->variable_item_list_rgb); - // set callback for exit from rgb_settings_menu + // set callback for exit from settings.rgb_menu view_set_previous_callback(view_rgb, notification_app_rgb_settings_exit); // Show rgb_backlight_installed swith only in Debug mode @@ -766,7 +756,7 @@ static NotificationAppSettings* alloc_settings(void) { rgb_backlight_installed_changed, app); value_index = value_index_bool( - app->notification->rgb_srv->settings->rgb_backlight_installed, + app->notification->settings.rgb.rgb_backlight_installed, rgb_backlight_installed_value, RGB_BACKLIGHT_INSTALLED_COUNT); variable_item_set_current_value_index(item, value_index); @@ -781,13 +771,11 @@ static NotificationAppSettings* alloc_settings(void) { rgb_backlight_get_color_count(), led_2_color_changed, app); - value_index = app->notification->rgb_srv->settings->led_2_color_index; + value_index = app->notification->settings.rgb.led_2_color_index; variable_item_set_current_value_index(item, value_index); variable_item_set_current_value_text(item, rgb_backlight_get_color_text(value_index)); variable_item_set_locked( - item, - (app->notification->rgb_srv->settings->rgb_backlight_installed == 0), - "RGB MOD \nOFF!"); + item, (app->notification->settings.rgb.rgb_backlight_installed == 0), "RGB MOD \nOFF!"); // led_2 color item = variable_item_list_add( @@ -796,13 +784,11 @@ static NotificationAppSettings* alloc_settings(void) { rgb_backlight_get_color_count(), led_1_color_changed, app); - value_index = app->notification->rgb_srv->settings->led_1_color_index; + value_index = app->notification->settings.rgb.led_1_color_index; variable_item_set_current_value_index(item, value_index); variable_item_set_current_value_text(item, rgb_backlight_get_color_text(value_index)); variable_item_set_locked( - item, - (app->notification->rgb_srv->settings->rgb_backlight_installed == 0), - "RGB MOD \nOFF!"); + item, (app->notification->settings.rgb.rgb_backlight_installed == 0), "RGB MOD \nOFF!"); // led 3 color item = variable_item_list_add( @@ -811,13 +797,11 @@ static NotificationAppSettings* alloc_settings(void) { rgb_backlight_get_color_count(), led_0_color_changed, app); - value_index = app->notification->rgb_srv->settings->led_0_color_index; + value_index = app->notification->settings.rgb.led_0_color_index; variable_item_set_current_value_index(item, value_index); variable_item_set_current_value_text(item, rgb_backlight_get_color_text(value_index)); variable_item_set_locked( - item, - (app->notification->rgb_srv->settings->rgb_backlight_installed == 0), - "RGB MOD \nOFF!"); + item, (app->notification->settings.rgb.rgb_backlight_installed == 0), "RGB MOD \nOFF!"); // Efects item = variable_item_list_add( @@ -827,15 +811,13 @@ static NotificationAppSettings* alloc_settings(void) { rgb_backlight_rainbow_changed, app); value_index = value_index_uint32( - app->notification->rgb_srv->settings->rainbow_mode, + app->notification->settings.rgb.rainbow_mode, rgb_backlight_rainbow_mode_value, RGB_BACKLIGHT_RAINBOW_MODE_COUNT); variable_item_set_current_value_index(item, value_index); variable_item_set_current_value_text(item, rgb_backlight_rainbow_mode_text[value_index]); variable_item_set_locked( - item, - (app->notification->rgb_srv->settings->rgb_backlight_installed == 0), - "RGB MOD \nOFF!"); + item, (app->notification->settings.rgb.rgb_backlight_installed == 0), "RGB MOD \nOFF!"); item = variable_item_list_add( app->variable_item_list_rgb, @@ -844,15 +826,13 @@ static NotificationAppSettings* alloc_settings(void) { rgb_backlight_rainbow_speed_changed, app); value_index = value_index_uint32( - app->notification->rgb_srv->settings->rainbow_speed_ms, + app->notification->settings.rgb.rainbow_speed_ms, rgb_backlight_rainbow_speed_value, RGB_BACKLIGHT_RAINBOW_SPEED_COUNT); variable_item_set_current_value_index(item, value_index); variable_item_set_current_value_text(item, rgb_backlight_rainbow_speed_text[value_index]); variable_item_set_locked( - item, - (app->notification->rgb_srv->settings->rgb_backlight_installed == 0), - "RGB MOD \nOFF!"); + item, (app->notification->settings.rgb.rgb_backlight_installed == 0), "RGB MOD \nOFF!"); item = variable_item_list_add( app->variable_item_list_rgb, @@ -861,15 +841,13 @@ static NotificationAppSettings* alloc_settings(void) { rgb_backlight_rainbow_step_changed, app); value_index = value_index_uint32( - app->notification->rgb_srv->settings->rainbow_step, + app->notification->settings.rgb.rainbow_step, rgb_backlight_rainbow_step_value, RGB_BACKLIGHT_RAINBOW_STEP_COUNT); variable_item_set_current_value_index(item, value_index); variable_item_set_current_value_text(item, rgb_backlight_rainbow_step_text[value_index]); variable_item_set_locked( - item, - (app->notification->rgb_srv->settings->rgb_backlight_installed == 0), - "RGB MOD \nOFF!"); + item, (app->notification->settings.rgb.rgb_backlight_installed == 0), "RGB MOD \nOFF!"); item = variable_item_list_add( app->variable_item_list_rgb, @@ -877,15 +855,13 @@ static NotificationAppSettings* alloc_settings(void) { 255, rgb_backlight_rainbow_saturation_changed, app); - value_index = app->notification->rgb_srv->settings->rainbow_saturation; + value_index = app->notification->settings.rgb.rainbow_saturation; variable_item_set_current_value_index(item, value_index); char valtext[4] = {}; snprintf(valtext, sizeof(valtext), "%d", value_index); variable_item_set_current_value_text(item, valtext); variable_item_set_locked( - item, - (app->notification->rgb_srv->settings->rgb_backlight_installed == 0), - "RGB MOD \nOFF!"); + item, (app->notification->settings.rgb.rgb_backlight_installed == 0), "RGB MOD \nOFF!"); item = variable_item_list_add( app->variable_item_list_rgb, @@ -894,15 +870,13 @@ static NotificationAppSettings* alloc_settings(void) { rgb_backlight_rainbow_wide_changed, app); value_index = value_index_uint32( - app->notification->rgb_srv->settings->rainbow_wide, + app->notification->settings.rgb.rainbow_wide, rgb_backlight_rainbow_wide_value, RGB_BACKLIGHT_RAINBOW_WIDE_COUNT); variable_item_set_current_value_index(item, value_index); variable_item_set_current_value_text(item, rgb_backlight_rainbow_wide_text[value_index]); variable_item_set_locked( - item, - (app->notification->rgb_srv->settings->rgb_backlight_installed == 0), - "RGB MOD \nOFF!"); + item, (app->notification->settings.rgb.rgb_backlight_installed == 0), "RGB MOD \nOFF!"); //--- RGB BACKLIGHT END --- diff --git a/targets/f7/api_symbols.csv b/targets/f7/api_symbols.csv index db6a1c242..beffffd35 100755 --- a/targets/f7/api_symbols.csv +++ b/targets/f7/api_symbols.csv @@ -1,5 +1,5 @@ entry,status,name,type,params -Version,+,85.0,, +Version,+,86.0,, Header,+,applications/drivers/subghz/cc1101_ext/cc1101_ext_interconnect.h,, Header,+,applications/services/bt/bt_service/bt.h,, Header,+,applications/services/bt/bt_service/bt_keys_storage.h,, @@ -38,7 +38,6 @@ Header,+,applications/services/locale/locale.h,, Header,+,applications/services/notification/notification.h,, Header,+,applications/services/notification/notification_messages.h,, Header,+,applications/services/power/power_service/power.h,, -Header,+,applications/services/rgb_backlight/rgb_backlight.h,, Header,+,applications/services/rpc/rpc_app.h,, Header,+,applications/services/storage/storage.h,, Header,+,lib/bit_lib/bit_lib.h,, @@ -415,10 +414,6 @@ Function,-,LL_mDelay,void,uint32_t Function,-,Osal_MemCmp,int,"const void*, const void*, unsigned int" Function,-,Osal_MemCpy,void*,"void*, const void*, unsigned int" Function,-,Osal_MemSet,void*,"void*, int, unsigned int" -Function,+,SK6805_get_led_count,uint8_t, -Function,+,SK6805_init,void, -Function,+,SK6805_set_led_color,void,"uint8_t, uint8_t, uint8_t, uint8_t" -Function,+,SK6805_update,void, Function,-,SystemCoreClockUpdate,void, Function,-,SystemInit,void, Function,-,_Exit,void,int @@ -3138,9 +3133,6 @@ Function,-,putw,int,"int, FILE*" Function,-,qsort,void,"void*, size_t, size_t, __compar_fn_t" Function,-,qsort_r,void,"void*, size_t, size_t, int (*)(const void*, const void*, void*), void*" Function,-,quick_exit,void,int -Function,+,rainbow_timer_start,void,RGBBacklightApp* -Function,+,rainbow_timer_starter,void,RGBBacklightApp* -Function,+,rainbow_timer_stop,void,RGBBacklightApp* Function,+,rand,int, Function,-,rand_r,int,unsigned* Function,+,random,long, @@ -3159,12 +3151,6 @@ Function,-,remquol,long double,"long double, long double, int*" Function,-,rename,int,"const char*, const char*" Function,-,renameat,int,"int, const char*, int, const char*" Function,-,rewind,void,FILE* -Function,+,rgb_backlight_get_color_count,uint8_t, -Function,+,rgb_backlight_get_color_text,const char*,uint8_t -Function,+,rgb_backlight_set_led_static_color,void,"uint8_t, uint8_t" -Function,+,rgb_backlight_settings_load,void,RGBBacklightSettings* -Function,+,rgb_backlight_settings_save,void,const RGBBacklightSettings* -Function,+,rgb_backlight_update,void,float Function,-,rindex,char*,"const char*, int" Function,-,rint,double,double Function,-,rintf,float,float diff --git a/targets/f7/furi_hal/furi_hal_light.c b/targets/f7/furi_hal/furi_hal_light.c index 2d6b4bbfe..b9f34e445 100644 --- a/targets/f7/furi_hal/furi_hal_light.c +++ b/targets/f7/furi_hal/furi_hal_light.c @@ -3,7 +3,7 @@ #include #include #include -#include "applications/services/rgb_backlight/rgb_backlight.h" +#include #define LED_CURRENT_RED (50u) #define LED_CURRENT_GREEN (50u) From c88eaffa66986210367f8b68a3520e85ed6b3103 Mon Sep 17 00:00:00 2001 From: Anna Antonenko Date: Thu, 10 Apr 2025 17:52:19 +0400 Subject: [PATCH 229/268] cli_vcp: go back to message queue for event signaling --- applications/services/cli/cli_vcp.c | 47 +++++++++++++++++------------ 1 file changed, 27 insertions(+), 20 deletions(-) diff --git a/applications/services/cli/cli_vcp.c b/applications/services/cli/cli_vcp.c index 99f2d7880..def1949e2 100644 --- a/applications/services/cli/cli_vcp.c +++ b/applications/services/cli/cli_vcp.c @@ -30,20 +30,16 @@ typedef struct { } CliVcpMessage; typedef enum { - CliVcpInternalEventConnected = (1 << 0), - CliVcpInternalEventDisconnected = (1 << 1), - CliVcpInternalEventTxDone = (1 << 2), - CliVcpInternalEventRx = (1 << 3), + CliVcpInternalEventConnected, + CliVcpInternalEventDisconnected, + CliVcpInternalEventTxDone, + CliVcpInternalEventRx, } CliVcpInternalEvent; -#define CliVcpInternalEventAll \ - (CliVcpInternalEventConnected | CliVcpInternalEventDisconnected | CliVcpInternalEventTxDone | \ - CliVcpInternalEventRx) - struct CliVcp { FuriEventLoop* event_loop; FuriMessageQueue* message_queue; // thread_id, event); + furi_check(furi_message_queue_put(cli_vcp->internal_evt_queue, &event, 0) == FuriStatusOk); } static void cli_vcp_cdc_tx_done(void* context) { @@ -191,22 +187,25 @@ static void cli_vcp_message_received(FuriEventLoopObject* object, void* context) /** * Processes messages arriving from CDC event callbacks */ -static void cli_vcp_internal_event_happened(void* context) { +static void cli_vcp_internal_event_happened(FuriEventLoopObject* object, void* context) { CliVcp* cli_vcp = context; - CliVcpInternalEvent event = furi_thread_flags_wait(CliVcpInternalEventAll, FuriFlagWaitAny, 0); - furi_check(!(event & FuriFlagError)); + CliVcpInternalEvent event; + furi_check(furi_message_queue_get(object, &event, 0) == FuriStatusOk); - if(event & CliVcpInternalEventRx) { + switch(event) { + case CliVcpInternalEventRx: { VCP_TRACE(TAG, "Rx"); cli_vcp_maybe_receive_data(cli_vcp); + break; } - if(event & CliVcpInternalEventTxDone) { + case CliVcpInternalEventTxDone: { VCP_TRACE(TAG, "TxDone"); cli_vcp_maybe_send_data(cli_vcp); + break; } - if(event & CliVcpInternalEventDisconnected) { + case CliVcpInternalEventDisconnected: { if(!cli_vcp->is_connected) return; FURI_LOG_D(TAG, "Disconnected"); cli_vcp->is_connected = false; @@ -215,9 +214,10 @@ static void cli_vcp_internal_event_happened(void* context) { pipe_detach_from_event_loop(cli_vcp->own_pipe); pipe_free(cli_vcp->own_pipe); cli_vcp->own_pipe = NULL; + break; } - if(event & CliVcpInternalEventConnected) { + case CliVcpInternalEventConnected: { if(cli_vcp->is_connected) return; FURI_LOG_D(TAG, "Connected"); cli_vcp->is_connected = true; @@ -244,6 +244,8 @@ static void cli_vcp_internal_event_happened(void* context) { cli_vcp->shell = cli_shell_alloc( cli_main_motd, NULL, cli_vcp->shell_pipe, cli_vcp->main_registry, &cli_main_ext_config); cli_shell_start(cli_vcp->shell); + break; + } } } @@ -253,7 +255,6 @@ static void cli_vcp_internal_event_happened(void* context) { static CliVcp* cli_vcp_alloc(void) { CliVcp* cli_vcp = malloc(sizeof(CliVcp)); - cli_vcp->thread_id = furi_thread_get_current_id(); cli_vcp->event_loop = furi_event_loop_alloc(); @@ -265,8 +266,14 @@ static CliVcp* cli_vcp_alloc(void) { cli_vcp_message_received, cli_vcp); - furi_event_loop_subscribe_thread_flags( - cli_vcp->event_loop, cli_vcp_internal_event_happened, cli_vcp); + cli_vcp->internal_evt_queue = + furi_message_queue_alloc(VCP_MESSAGE_Q_LEN, sizeof(CliVcpInternalEvent)); + furi_event_loop_subscribe_message_queue( + cli_vcp->event_loop, + cli_vcp->internal_evt_queue, + FuriEventLoopEventIn, + cli_vcp_internal_event_happened, + cli_vcp); cli_vcp->main_registry = furi_record_open(RECORD_CLI); From 3c18097025bf6554e1976044bea1b8dadb4d6fb7 Mon Sep 17 00:00:00 2001 From: Dmitry422 Date: Thu, 10 Apr 2025 23:44:57 +0700 Subject: [PATCH 230/268] Moving RGB backlight back to Notification service finished. --- .../services/notification/notification_app.c | 77 ++++++++++++------- .../services/notification/notification_app.h | 5 +- .../notification_settings_app.c | 17 ++-- 3 files changed, 57 insertions(+), 42 deletions(-) diff --git a/applications/services/notification/notification_app.c b/applications/services/notification/notification_app.c index 2b0fe40f7..e57ebbda2 100644 --- a/applications/services/notification/notification_app.c +++ b/applications/services/notification/notification_app.c @@ -10,7 +10,7 @@ #include "notification_messages.h" #include "notification_app.h" -#define TAG "NotificationSrv" +#define TAG "NotificationSrv" #define COLOR_COUNT (sizeof(colors) / sizeof(RGBBacklightColor)) static const uint8_t minimal_delay = 100; @@ -33,9 +33,11 @@ static uint8_t notification_settings_get_display_brightness(NotificationApp* app static uint8_t notification_settings_get_rgb_led_brightness(NotificationApp* app, uint8_t value); static uint32_t notification_settings_display_off_delay_ticks(NotificationApp* app); - // --- RGB BACKLIGHT --- +// local variable for local use +uint8_t rgb_backlight_installed_variable = 0; + typedef struct { char* name; uint8_t red; @@ -75,7 +77,12 @@ const char* rgb_backlight_get_color_text(uint8_t index) { return colors[index].name; } -// use RECORD for acces to rgb service instance and update current colors by static +// function for changind local variable from outside; +void set_rgb_backlight_installed_variable(uint8_t var) { + rgb_backlight_installed_variable = var; +} + +// update led current colors by static void rgb_backlight_set_led_static_color(uint8_t led, uint8_t index) { if(led < SK6805_get_led_count()) { uint8_t r = colors[index].red; @@ -138,11 +145,9 @@ void rgb_backlight_set_led_custom_hsv_color(uint8_t led, uint16_t hue, uint8_t s current_led[led].blue = b * 255; } -// use RECORD for acces to rgb service instance, set current_* colors to led and update backlight +// set current_* colors to led and update backlight void rgb_backlight_update(float brightness) { - // RGBBacklightApp* app = furi_record_open(RECORD_RGB_BACKLIGHT); - - // if(app->settings.rgb.rgb_backlight_installed) { + if(rgb_backlight_installed_variable > 0) { for(uint8_t i = 0; i < SK6805_get_led_count(); i++) { uint8_t r = current_led[i].red * brightness * 1.0f; uint8_t g = current_led[i].green * brightness * 1.0f; @@ -150,8 +155,7 @@ void rgb_backlight_update(float brightness) { SK6805_set_led_color(i, r, g, b); } SK6805_update(); - // } - // furi_record_close(RECORD_RGB_BACKLIGHT); + } } // start furi timer for rainbow @@ -213,7 +217,10 @@ static void rainbow_timer_callback(void* context) { } rgb_backlight_set_led_custom_hsv_color( - 0, app->rainbow_hue, app->settings.rgb.rainbow_saturation, app->settings.display_brightness); + 0, + app->rainbow_hue, + app->settings.rgb.rainbow_saturation, + app->settings.display_brightness); rgb_backlight_set_led_custom_hsv_color( 1, j, app->settings.rgb.rainbow_saturation, app->settings.display_brightness); rgb_backlight_set_led_custom_hsv_color( @@ -228,8 +235,7 @@ static void rainbow_timer_callback(void* context) { } } -// --- RGB BACKLIGHT ENF--- - +// --- RGB BACKLIGHT END--- // --- NIGHT SHIFT --- @@ -815,6 +821,20 @@ static NotificationApp* notification_app_alloc(void) { furi_timer_alloc(night_shift_timer_callback, FuriTimerTypePeriodic, app); // --- NIGHT SHIFT END --- + // init working variables + app->rainbow_hue = 1; + app->current_night_shift = 1.0f; + + // init rgb.segings values + app->settings.rgb.rgb_backlight_installed = 0; + app->settings.rgb.led_2_color_index = 0; + app->settings.rgb.led_1_color_index = 0; + app->settings.rgb.led_0_color_index = 0; + app->settings.rgb.rainbow_speed_ms = 100; + app->settings.rgb.rainbow_step = 1; + app->settings.rgb.rainbow_saturation = 255; + app->settings.rgb.rainbow_wide = 50; + // use RECORD for setup init values to canvas lcd_inverted Gui* tmp_gui = furi_record_open(RECORD_GUI); Canvas* tmp_canvas = gui_direct_draw_acquire(tmp_gui); @@ -847,7 +867,7 @@ static void notification_apply_settings(NotificationApp* app) { notification_apply_lcd_contrast(app); // --- NIGHT SHIFT --- - // if night_shift_enabled start timer for controlling current_night_shift multiplicator value depent from current time + // if night_shift enabled then start timer for controlling current_night_shift multiplicator value depent from current time if(app->settings.night_shift != 1) { night_shift_timer_start(app); } @@ -878,14 +898,24 @@ int32_t notification_srv(void* p) { UNUSED(p); NotificationApp* app = notification_app_alloc(); + notification_init_settings(app); + + notification_vibro_off(); + notification_sound_off(); + notification_apply_internal_led_layer(&app->display, 0x00); + notification_apply_internal_led_layer(&app->led[0], 0x00); + notification_apply_internal_led_layer(&app->led[1], 0x00); + notification_apply_internal_led_layer(&app->led[2], 0x00); + + furi_record_create(RECORD_NOTIFICATION, app); + // --- RGB BACKLIGHT SECTION --- + //setup local variable + set_rgb_backlight_installed_variable(app->settings.rgb.rgb_backlight_installed); + // define rainbow_timer and they callback app->rainbow_timer = furi_timer_alloc(rainbow_timer_callback, FuriTimerTypePeriodic, app); - - // init values - app->rainbow_hue = 1; - app->current_night_shift = 1.0f; // if rgb_backlight_installed then start rainbow or set leds colors from saved settings (default index = 0) if(app->settings.rgb.rgb_backlight_installed) { @@ -897,7 +927,7 @@ int32_t notification_srv(void* p) { rgb_backlight_set_led_static_color(0, app->settings.rgb.led_0_color_index); rgb_backlight_update(app->settings.display_brightness * app->current_night_shift); } - // if rgb_backlight not installed then set default static orange color(index=0) to all leds (0-2) and force light on + // if rgb_backlight not installed then set default static orange color(index=0) to all leds (0-2) and force light on } else { rgb_backlight_set_led_static_color(2, 0); rgb_backlight_set_led_static_color(1, 0); @@ -907,17 +937,6 @@ int32_t notification_srv(void* p) { // --- RGB BACKLIGHT SECTION END --- - notification_init_settings(app); - - notification_vibro_off(); - notification_sound_off(); - notification_apply_internal_led_layer(&app->display, 0x00); - notification_apply_internal_led_layer(&app->led[0], 0x00); - notification_apply_internal_led_layer(&app->led[1], 0x00); - notification_apply_internal_led_layer(&app->led[2], 0x00); - - furi_record_create(RECORD_NOTIFICATION, app); - NotificationAppMessage message; while(1) { furi_check(furi_message_queue_get(app->queue, &message, FuriWaitForever) == FuriStatusOk); diff --git a/applications/services/notification/notification_app.h b/applications/services/notification/notification_app.h index 7396e695e..239bf69c0 100644 --- a/applications/services/notification/notification_app.h +++ b/applications/services/notification/notification_app.h @@ -35,7 +35,7 @@ typedef struct { Light light; } NotificationLedLayer; -#define NOTIFICATION_SETTINGS_VERSION 0x04 +#define NOTIFICATION_SETTINGS_VERSION 0x05 #define NOTIFICATION_SETTINGS_PATH INT_PATH(NOTIFICATION_SETTINGS_FILE_NAME) typedef struct { @@ -100,4 +100,5 @@ void rainbow_timer_start(NotificationApp* app); void rainbow_timer_stop(NotificationApp* app); void rainbow_timer_starter(NotificationApp* app); const char* rgb_backlight_get_color_text(uint8_t index); -uint8_t rgb_backlight_get_color_count(void); \ No newline at end of file +uint8_t rgb_backlight_get_color_count(void); +void set_rgb_backlight_installed_variable(uint8_t var); diff --git a/applications/settings/notification_settings/notification_settings_app.c b/applications/settings/notification_settings/notification_settings_app.c index 7907d167d..8794c53f6 100644 --- a/applications/settings/notification_settings/notification_settings_app.c +++ b/applications/settings/notification_settings/notification_settings_app.c @@ -5,7 +5,6 @@ #include #include - #define MAX_NOTIFICATION_SETTINGS 5 typedef struct { @@ -362,8 +361,8 @@ static void rgb_backlight_installed_changed(VariableItem* item) { NotificationAppSettings* app = variable_item_get_context(item); uint8_t index = variable_item_get_current_value_index(item); variable_item_set_current_value_text(item, rgb_backlight_installed_text[index]); - app->notification->settings.rgb.rgb_backlight_installed = - rgb_backlight_installed_value[index]; + app->notification->settings.rgb.rgb_backlight_installed = rgb_backlight_installed_value[index]; + set_rgb_backlight_installed_variable(rgb_backlight_installed_value[index]); // In case of user playing with rgb_backlight_installed swith: // if user swith_off rgb_backlight_installed (but may be he have mod installed) @@ -470,12 +469,9 @@ static void rgb_backlight_rainbow_changed(VariableItem* item) { // restore saved rgb backlight settings if we switch_off effects if(index == 0) { - rgb_backlight_set_led_static_color( - 2, app->notification->settings.rgb.led_2_color_index); - rgb_backlight_set_led_static_color( - 1, app->notification->settings.rgb.led_1_color_index); - rgb_backlight_set_led_static_color( - 0, app->notification->settings.rgb.led_0_color_index); + rgb_backlight_set_led_static_color(2, app->notification->settings.rgb.led_2_color_index); + rgb_backlight_set_led_static_color(1, app->notification->settings.rgb.led_1_color_index); + rgb_backlight_set_led_static_color(0, app->notification->settings.rgb.led_0_color_index); rgb_backlight_update( app->notification->settings.display_brightness * app->notification->current_night_shift); @@ -492,8 +488,7 @@ static void rgb_backlight_rainbow_speed_changed(VariableItem* item) { uint8_t index = variable_item_get_current_value_index(item); variable_item_set_current_value_text(item, rgb_backlight_rainbow_speed_text[index]); - app->notification->settings.rgb.rainbow_speed_ms = - rgb_backlight_rainbow_speed_value[index]; + app->notification->settings.rgb.rainbow_speed_ms = rgb_backlight_rainbow_speed_value[index]; // save settings and restart timer with new speed value rainbow_timer_starter(app->notification); From b4450946a4f05209415e09b7aa05efdb8e871109 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Thu, 10 Apr 2025 20:01:08 +0300 Subject: [PATCH 231/268] upd changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index baeba24f7..6a73e6936 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ * Desktop: **Add support for Favorite App - Ok Long** (Warning! Old favourites apps list will be reset!) (PR #886 | by @DrEverr) * Display: **LCD Color Inversion** (Settings - LCD and Notifications - LCD inversion.) (PR #887 | by @Dmitry422) * Display: **Night Shift Feature** (dimming backlight in selected time interval) (PR #885 | by @Dmitry422) -* Display: **Сombining RGB Backlight mod** (by @quen0n) and original backlight support **in one firmware** (+ Rainbow/Wave effect (based on @Willy-JL idea)) (PR #877 #881 | by @Dmitry422) - (**To enable RGB Backlight support go into Notifications settings with Debug mode - ON**) +* Display: **Сombining RGB Backlight mod** (by @quen0n) and original backlight support **in one firmware** (+ Rainbow/Wave effect (based on @Willy-JL idea)) (PR #877 #881 #890 | by @Dmitry422) - (**To enable RGB Backlight support go into Notifications settings with Debug mode - ON**) * OFW: LFRFID - **EM4305 support** * OFW: **Universal IR signal selection** * OFW: **BadUSB: Mouse control** From 98728fe93ce9d46a52a8873d0cbb402d2f0418f9 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Thu, 10 Apr 2025 21:03:59 +0300 Subject: [PATCH 232/268] fix api to ofw --- targets/f7/api_symbols.csv | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/targets/f7/api_symbols.csv b/targets/f7/api_symbols.csv index beffffd35..60df2aeb1 100755 --- a/targets/f7/api_symbols.csv +++ b/targets/f7/api_symbols.csv @@ -1,5 +1,5 @@ entry,status,name,type,params -Version,+,86.0,, +Version,+,85.0,, Header,+,applications/drivers/subghz/cc1101_ext/cc1101_ext_interconnect.h,, Header,+,applications/services/bt/bt_service/bt.h,, Header,+,applications/services/bt/bt_service/bt_keys_storage.h,, From 3c25c29decde28f7207dd1dcf4151e4b52c8c24e Mon Sep 17 00:00:00 2001 From: Anna Antonenko Date: Fri, 11 Apr 2025 03:21:53 +0400 Subject: [PATCH 233/268] loader: move BeforeLoad event even earlier --- applications/services/loader/loader.c | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/applications/services/loader/loader.c b/applications/services/loader/loader.c index d3cd0022e..737d03dd7 100644 --- a/applications/services/loader/loader.c +++ b/applications/services/loader/loader.c @@ -418,9 +418,6 @@ static void loader_start_internal_app( const FlipperInternalApplication* app, const char* args) { FURI_LOG_I(TAG, "Starting %s", app->name); - LoaderEvent event; - event.type = LoaderEventTypeApplicationBeforeLoad; - furi_pubsub_publish(loader->pubsub, &event); // store args furi_assert(loader->app.args == NULL); @@ -508,9 +505,6 @@ static LoaderMessageLoaderStatusResult loader_start_external_app( result.value = loader_make_success_status(error_message); result.error = LoaderStatusErrorUnknown; - LoaderEvent event; - event.type = LoaderEventTypeApplicationBeforeLoad; - furi_pubsub_publish(loader->pubsub, &event); do { loader->app.fap = flipper_application_alloc(storage, firmware_api_interface); @@ -566,6 +560,7 @@ static LoaderMessageLoaderStatusResult loader_start_external_app( if(result.value != LoaderStatusOk) { flipper_application_free(loader->app.fap); loader->app.fap = NULL; + LoaderEvent event; event.type = LoaderEventTypeApplicationLoadFailed; furi_pubsub_publish(loader->pubsub, &event); } @@ -615,6 +610,10 @@ static LoaderMessageLoaderStatusResult loader_do_start_by_name( status.value = loader_make_success_status(error_message); status.error = LoaderStatusErrorUnknown; + LoaderEvent event; + event.type = LoaderEventTypeApplicationBeforeLoad; + furi_pubsub_publish(loader->pubsub, &event); + do { // check lock if(loader_do_is_locked(loader)) { From 1b5a2496f58459d2c93b369fa324b2b26f2ae73d Mon Sep 17 00:00:00 2001 From: Anna Antonenko Date: Fri, 11 Apr 2025 03:23:38 +0400 Subject: [PATCH 234/268] fix formatting --- applications/services/loader/loader.c | 1 - 1 file changed, 1 deletion(-) diff --git a/applications/services/loader/loader.c b/applications/services/loader/loader.c index 737d03dd7..136b3c20b 100644 --- a/applications/services/loader/loader.c +++ b/applications/services/loader/loader.c @@ -505,7 +505,6 @@ static LoaderMessageLoaderStatusResult loader_start_external_app( result.value = loader_make_success_status(error_message); result.error = LoaderStatusErrorUnknown; - do { loader->app.fap = flipper_application_alloc(storage, firmware_api_interface); size_t start = furi_get_tick(); From 837abd7d51d4fee45fb5e57432b440fb18b48344 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Fri, 11 Apr 2025 03:00:03 +0300 Subject: [PATCH 235/268] upd changelog --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a73e6936..13cc992c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,8 +21,8 @@ * Apps: Add **FindMyFlipper to system apps and allow autostart** on system boot [app by @MatthewKuKanich](https://github.com/MatthewKuKanich/FindMyFlipper) and autoloader by @Willy-JL - to use app please check how to add keys in [app repo](https://github.com/MatthewKuKanich/FindMyFlipper) * README Update: Enhanced Visuals & Navigation (PR #871 #872 | by @m-xim) * Docs: Update FAQ.md (PR #865 | by @mi-lrn) -* Input: Vibro on Button press option (PR #867 | by @Dmitry422) -* Power: Option to limit battery charging (suppress charging on selected charge level) (PR #867 | by @Dmitry422) (idea and example by @oltenxyz) +* Input: **Vibro on Button press option** (PR #867 | by @Dmitry422) +* Power: **Option to limit battery charging** (suppress charging on selected charge level) (PR #867 | by @Dmitry422) (idea and example by @oltenxyz) * Apps: **Check out more Apps updates and fixes by following** [this link](https://github.com/xMasterX/all-the-plugins/commits/dev) ## Other changes * SubGHz: Various bugfixes and experimental options (rolling counter overflow) (by @xMasterX) From 096c088bf18b280774291e88ba00fd539c5adb2b Mon Sep 17 00:00:00 2001 From: Anna Antonenko Date: Fri, 11 Apr 2025 14:53:10 +0400 Subject: [PATCH 236/268] vcp, cli: Handle Tx/Rx events before Connect/Disconnect + extra fixes (#4181) * cli_vcp: handle tx/rx before connext/disconnect * cli_vcp: disable trace * cli_perf: advanced error reporting * cli_vcp: reset tx flag directly in event handler * fix formatting * cli_vcp: make tx flag volatile * storage_settings: fix scene ids * cli_shell: add safety check to set_prompt * cli_registry: move from bptree to dict, fix memory leak * cli_vcp: go back to message queue for event signaling * loader: move BeforeLoad event even earlier * fix formatting --- .gitignore | 4 ++ applications/services/cli/cli_vcp.c | 65 ++++++++++--------- applications/services/loader/loader.c | 12 ++-- .../storage_settings/storage_settings.c | 4 +- lib/toolbox/cli/cli_registry.c | 32 +++++---- lib/toolbox/cli/cli_registry_i.h | 18 ++--- lib/toolbox/cli/shell/cli_shell.c | 15 +++-- lib/toolbox/cli/shell/cli_shell_completions.c | 6 +- scripts/serial_cli_perf.py | 18 ++++- 9 files changed, 92 insertions(+), 82 deletions(-) diff --git a/.gitignore b/.gitignore index 84b8e8319..79f2a8058 100644 --- a/.gitignore +++ b/.gitignore @@ -67,3 +67,7 @@ PVS-Studio.log # JS packages node_modules/ + +# cli_perf script output in case of errors +/block.bin +/return_block.bin diff --git a/applications/services/cli/cli_vcp.c b/applications/services/cli/cli_vcp.c index f4b539e26..def1949e2 100644 --- a/applications/services/cli/cli_vcp.c +++ b/applications/services/cli/cli_vcp.c @@ -30,27 +30,23 @@ typedef struct { } CliVcpMessage; typedef enum { - CliVcpInternalEventConnected = (1 << 0), - CliVcpInternalEventDisconnected = (1 << 1), - CliVcpInternalEventTxDone = (1 << 2), - CliVcpInternalEventRx = (1 << 3), + CliVcpInternalEventConnected, + CliVcpInternalEventDisconnected, + CliVcpInternalEventTxDone, + CliVcpInternalEventRx, } CliVcpInternalEvent; -#define CliVcpInternalEventAll \ - (CliVcpInternalEventConnected | CliVcpInternalEventDisconnected | CliVcpInternalEventTxDone | \ - CliVcpInternalEventRx) - struct CliVcp { FuriEventLoop* event_loop; FuriMessageQueue* message_queue; // thread_id, event); + furi_check(furi_message_queue_put(cli_vcp->internal_evt_queue, &event, 0) == FuriStatusOk); } static void cli_vcp_cdc_tx_done(void* context) { CliVcp* cli_vcp = context; + cli_vcp->is_currently_transmitting = false; cli_vcp_signal_internal_event(cli_vcp, CliVcpInternalEventTxDone); } @@ -190,12 +187,25 @@ static void cli_vcp_message_received(FuriEventLoopObject* object, void* context) /** * Processes messages arriving from CDC event callbacks */ -static void cli_vcp_internal_event_happened(void* context) { +static void cli_vcp_internal_event_happened(FuriEventLoopObject* object, void* context) { CliVcp* cli_vcp = context; - CliVcpInternalEvent event = furi_thread_flags_wait(CliVcpInternalEventAll, FuriFlagWaitAny, 0); - furi_check(!(event & FuriFlagError)); + CliVcpInternalEvent event; + furi_check(furi_message_queue_get(object, &event, 0) == FuriStatusOk); - if(event & CliVcpInternalEventDisconnected) { + switch(event) { + case CliVcpInternalEventRx: { + VCP_TRACE(TAG, "Rx"); + cli_vcp_maybe_receive_data(cli_vcp); + break; + } + + case CliVcpInternalEventTxDone: { + VCP_TRACE(TAG, "TxDone"); + cli_vcp_maybe_send_data(cli_vcp); + break; + } + + case CliVcpInternalEventDisconnected: { if(!cli_vcp->is_connected) return; FURI_LOG_D(TAG, "Disconnected"); cli_vcp->is_connected = false; @@ -204,9 +214,10 @@ static void cli_vcp_internal_event_happened(void* context) { pipe_detach_from_event_loop(cli_vcp->own_pipe); pipe_free(cli_vcp->own_pipe); cli_vcp->own_pipe = NULL; + break; } - if(event & CliVcpInternalEventConnected) { + case CliVcpInternalEventConnected: { if(cli_vcp->is_connected) return; FURI_LOG_D(TAG, "Connected"); cli_vcp->is_connected = true; @@ -233,17 +244,8 @@ static void cli_vcp_internal_event_happened(void* context) { cli_vcp->shell = cli_shell_alloc( cli_main_motd, NULL, cli_vcp->shell_pipe, cli_vcp->main_registry, &cli_main_ext_config); cli_shell_start(cli_vcp->shell); + break; } - - if(event & CliVcpInternalEventRx) { - VCP_TRACE(TAG, "Rx"); - cli_vcp_maybe_receive_data(cli_vcp); - } - - if(event & CliVcpInternalEventTxDone) { - VCP_TRACE(TAG, "TxDone"); - cli_vcp->is_currently_transmitting = false; - cli_vcp_maybe_send_data(cli_vcp); } } @@ -253,7 +255,6 @@ static void cli_vcp_internal_event_happened(void* context) { static CliVcp* cli_vcp_alloc(void) { CliVcp* cli_vcp = malloc(sizeof(CliVcp)); - cli_vcp->thread_id = furi_thread_get_current_id(); cli_vcp->event_loop = furi_event_loop_alloc(); @@ -265,8 +266,14 @@ static CliVcp* cli_vcp_alloc(void) { cli_vcp_message_received, cli_vcp); - furi_event_loop_subscribe_thread_flags( - cli_vcp->event_loop, cli_vcp_internal_event_happened, cli_vcp); + cli_vcp->internal_evt_queue = + furi_message_queue_alloc(VCP_MESSAGE_Q_LEN, sizeof(CliVcpInternalEvent)); + furi_event_loop_subscribe_message_queue( + cli_vcp->event_loop, + cli_vcp->internal_evt_queue, + FuriEventLoopEventIn, + cli_vcp_internal_event_happened, + cli_vcp); cli_vcp->main_registry = furi_record_open(RECORD_CLI); diff --git a/applications/services/loader/loader.c b/applications/services/loader/loader.c index d3cd0022e..136b3c20b 100644 --- a/applications/services/loader/loader.c +++ b/applications/services/loader/loader.c @@ -418,9 +418,6 @@ static void loader_start_internal_app( const FlipperInternalApplication* app, const char* args) { FURI_LOG_I(TAG, "Starting %s", app->name); - LoaderEvent event; - event.type = LoaderEventTypeApplicationBeforeLoad; - furi_pubsub_publish(loader->pubsub, &event); // store args furi_assert(loader->app.args == NULL); @@ -508,10 +505,6 @@ static LoaderMessageLoaderStatusResult loader_start_external_app( result.value = loader_make_success_status(error_message); result.error = LoaderStatusErrorUnknown; - LoaderEvent event; - event.type = LoaderEventTypeApplicationBeforeLoad; - furi_pubsub_publish(loader->pubsub, &event); - do { loader->app.fap = flipper_application_alloc(storage, firmware_api_interface); size_t start = furi_get_tick(); @@ -566,6 +559,7 @@ static LoaderMessageLoaderStatusResult loader_start_external_app( if(result.value != LoaderStatusOk) { flipper_application_free(loader->app.fap); loader->app.fap = NULL; + LoaderEvent event; event.type = LoaderEventTypeApplicationLoadFailed; furi_pubsub_publish(loader->pubsub, &event); } @@ -615,6 +609,10 @@ static LoaderMessageLoaderStatusResult loader_do_start_by_name( status.value = loader_make_success_status(error_message); status.error = LoaderStatusErrorUnknown; + LoaderEvent event; + event.type = LoaderEventTypeApplicationBeforeLoad; + furi_pubsub_publish(loader->pubsub, &event); + do { // check lock if(loader_do_is_locked(loader)) { diff --git a/applications/settings/storage_settings/storage_settings.c b/applications/settings/storage_settings/storage_settings.c index b4cde7b6b..07656431c 100644 --- a/applications/settings/storage_settings/storage_settings.c +++ b/applications/settings/storage_settings/storage_settings.c @@ -5,8 +5,8 @@ const SubmenuSettingsHelperDescriptor descriptor_template = { .options_cnt = 6, .options = { - {.name = "About Internal Storage", .scene_id = StorageSettingsSDInfo}, - {.name = "About SD Card", .scene_id = StorageSettingsInternalInfo}, + {.name = "About Internal Storage", .scene_id = StorageSettingsInternalInfo}, + {.name = "About SD Card", .scene_id = StorageSettingsSDInfo}, {.name = "Unmount SD Card", .scene_id = StorageSettingsUnmountConfirm}, {.name = "Format SD Card", .scene_id = StorageSettingsFormatConfirm}, {.name = "Benchmark SD Card", .scene_id = StorageSettingsBenchmarkConfirm}, diff --git a/lib/toolbox/cli/cli_registry.c b/lib/toolbox/cli/cli_registry.c index 35bed19f2..91f7c4046 100644 --- a/lib/toolbox/cli/cli_registry.c +++ b/lib/toolbox/cli/cli_registry.c @@ -3,16 +3,16 @@ #include #include -#define TAG "cli" +#define TAG "CliRegistry" struct CliRegistry { - CliCommandTree_t commands; + CliCommandDict_t commands; FuriMutex* mutex; }; CliRegistry* cli_registry_alloc(void) { CliRegistry* registry = malloc(sizeof(CliRegistry)); - CliCommandTree_init(registry->commands); + CliCommandDict_init(registry->commands); registry->mutex = furi_mutex_alloc(FuriMutexTypeRecursive); return registry; } @@ -20,7 +20,7 @@ CliRegistry* cli_registry_alloc(void) { void cli_registry_free(CliRegistry* registry) { furi_check(furi_mutex_acquire(registry->mutex, FuriWaitForever) == FuriStatusOk); furi_mutex_free(registry->mutex); - CliCommandTree_clear(registry->commands); + CliCommandDict_clear(registry->commands); free(registry); } @@ -61,7 +61,7 @@ void cli_registry_add_command_ex( }; furi_check(furi_mutex_acquire(registry->mutex, FuriWaitForever) == FuriStatusOk); - CliCommandTree_set_at(registry->commands, name_str, command); + CliCommandDict_set_at(registry->commands, name_str, command); furi_check(furi_mutex_release(registry->mutex) == FuriStatusOk); furi_string_free(name_str); @@ -79,7 +79,7 @@ void cli_registry_delete_command(CliRegistry* registry, const char* name) { } while(name_replace != FURI_STRING_FAILURE); furi_check(furi_mutex_acquire(registry->mutex, FuriWaitForever) == FuriStatusOk); - CliCommandTree_erase(registry->commands, name_str); + CliCommandDict_erase(registry->commands, name_str); furi_check(furi_mutex_release(registry->mutex) == FuriStatusOk); furi_string_free(name_str); @@ -91,7 +91,7 @@ bool cli_registry_get_command( CliRegistryCommand* result) { furi_assert(registry); furi_check(furi_mutex_acquire(registry->mutex, FuriWaitForever) == FuriStatusOk); - CliRegistryCommand* data = CliCommandTree_get(registry->commands, command); + CliRegistryCommand* data = CliCommandDict_get(registry->commands, command); if(data) *result = *data; furi_check(furi_mutex_release(registry->mutex) == FuriStatusOk); @@ -103,16 +103,14 @@ void cli_registry_remove_external_commands(CliRegistry* registry) { furi_check(registry); furi_check(furi_mutex_acquire(registry->mutex, FuriWaitForever) == FuriStatusOk); - // FIXME FL-3977: memory leak somewhere within this function - - CliCommandTree_t internal_cmds; - CliCommandTree_init(internal_cmds); + CliCommandDict_t internal_cmds; + CliCommandDict_init(internal_cmds); for - M_EACH(item, registry->commands, CliCommandTree_t) { - if(!(item->value_ptr->flags & CliCommandFlagExternal)) - CliCommandTree_set_at(internal_cmds, *item->key_ptr, *item->value_ptr); + M_EACH(item, registry->commands, CliCommandDict_t) { + if(!(item->value.flags & CliCommandFlagExternal)) + CliCommandDict_set_at(internal_cmds, item->key, item->value); } - CliCommandTree_move(registry->commands, internal_cmds); + CliCommandDict_move(registry->commands, internal_cmds); furi_check(furi_mutex_release(registry->mutex) == FuriStatusOk); } @@ -148,7 +146,7 @@ void cli_registry_reload_external_commands( .execute_callback = NULL, .flags = CliCommandFlagExternal, }; - CliCommandTree_set_at(registry->commands, plugin_name, command); + CliCommandDict_set_at(registry->commands, plugin_name, command); } furi_string_free(plugin_name); @@ -172,7 +170,7 @@ void cli_registry_unlock(CliRegistry* registry) { furi_mutex_release(registry->mutex); } -CliCommandTree_t* cli_registry_get_commands(CliRegistry* registry) { +CliCommandDict_t* cli_registry_get_commands(CliRegistry* registry) { furi_assert(registry); return ®istry->commands; } diff --git a/lib/toolbox/cli/cli_registry_i.h b/lib/toolbox/cli/cli_registry_i.h index 95b7c55da..31995832f 100644 --- a/lib/toolbox/cli/cli_registry_i.h +++ b/lib/toolbox/cli/cli_registry_i.h @@ -6,7 +6,7 @@ #pragma once #include -#include +#include #include "cli_registry.h" #ifdef __cplusplus @@ -22,19 +22,9 @@ typedef struct { size_t stack_depth; } CliRegistryCommand; -#define CLI_COMMANDS_TREE_RANK 4 +DICT_DEF2(CliCommandDict, FuriString*, FURI_STRING_OPLIST, CliRegistryCommand, M_POD_OPLIST); -// -V:BPTREE_DEF2:1103 -// -V:BPTREE_DEF2:524 -BPTREE_DEF2( - CliCommandTree, - CLI_COMMANDS_TREE_RANK, - FuriString*, - FURI_STRING_OPLIST, - CliRegistryCommand, - M_POD_OPLIST); - -#define M_OPL_CliCommandTree_t() BPTREE_OPLIST2(CliCommandTree, FURI_STRING_OPLIST, M_POD_OPLIST) +#define M_OPL_CliCommandDict_t() DICT_OPLIST(CliCommandDict, FURI_STRING_OPLIST, M_POD_OPLIST) bool cli_registry_get_command( CliRegistry* registry, @@ -48,7 +38,7 @@ void cli_registry_unlock(CliRegistry* registry); /** * @warning Surround calls to this function with `cli_registry_[un]lock` */ -CliCommandTree_t* cli_registry_get_commands(CliRegistry* registry); +CliCommandDict_t* cli_registry_get_commands(CliRegistry* registry); #ifdef __cplusplus } diff --git a/lib/toolbox/cli/shell/cli_shell.c b/lib/toolbox/cli/shell/cli_shell.c index daf5065ec..8aa7c387a 100644 --- a/lib/toolbox/cli/shell/cli_shell.c +++ b/lib/toolbox/cli/shell/cli_shell.c @@ -103,15 +103,15 @@ void cli_command_help(PipeSide* pipe, FuriString* args, void* context) { printf("Available commands:\r\n" ANSI_FG_GREEN); cli_registry_lock(registry); - CliCommandTree_t* commands = cli_registry_get_commands(registry); - size_t commands_count = CliCommandTree_size(*commands); + CliCommandDict_t* commands = cli_registry_get_commands(registry); + size_t commands_count = CliCommandDict_size(*commands); - CliCommandTree_it_t iterator; - CliCommandTree_it(iterator, *commands); + CliCommandDict_it_t iterator; + CliCommandDict_it(iterator, *commands); for(size_t i = 0; i < commands_count; i++) { - const CliCommandTree_itref_t* item = CliCommandTree_cref(iterator); - printf("%-30s", furi_string_get_cstr(*item->key_ptr)); - CliCommandTree_next(iterator); + const CliCommandDict_itref_t* item = CliCommandDict_cref(iterator); + printf("%-30s", furi_string_get_cstr(item->key)); + CliCommandDict_next(iterator); if(i % columns == columns - 1) printf("\r\n"); } @@ -474,5 +474,6 @@ void cli_shell_join(CliShell* shell) { void cli_shell_set_prompt(CliShell* shell, const char* prompt) { furi_check(shell); + furi_check(furi_thread_get_state(shell->thread) == FuriThreadStateStopped); shell->prompt = prompt; } diff --git a/lib/toolbox/cli/shell/cli_shell_completions.c b/lib/toolbox/cli/shell/cli_shell_completions.c index 823f91fb9..7a178705d 100644 --- a/lib/toolbox/cli/shell/cli_shell_completions.c +++ b/lib/toolbox/cli/shell/cli_shell_completions.c @@ -111,10 +111,10 @@ void cli_shell_completions_fill_variants(CliShellCompletions* completions) { if(segment.type == CliShellCompletionSegmentTypeCommand) { CliRegistry* registry = completions->registry; cli_registry_lock(registry); - CliCommandTree_t* commands = cli_registry_get_commands(registry); + CliCommandDict_t* commands = cli_registry_get_commands(registry); for - M_EACH(registered_command, *commands, CliCommandTree_t) { - FuriString* command_name = *registered_command->key_ptr; + M_EACH(registered_command, *commands, CliCommandDict_t) { + FuriString* command_name = registered_command->key; if(furi_string_start_with(command_name, input)) { CommandCompletions_push_back(completions->variants, command_name); } diff --git a/scripts/serial_cli_perf.py b/scripts/serial_cli_perf.py index 0fed7c393..3d612e6de 100644 --- a/scripts/serial_cli_perf.py +++ b/scripts/serial_cli_perf.py @@ -32,16 +32,27 @@ def main(): bytes_to_send = args.length block_size = 1024 + success = True while bytes_to_send: actual_size = min(block_size, bytes_to_send) # can't use 0x03 because that's ASCII ETX, or Ctrl+C - block = bytes([randint(4, 255) for _ in range(actual_size)]) + # block = bytes([randint(4, 255) for _ in range(actual_size)]) + block = bytes([4 + (i // 64) for i in range(actual_size)]) port.write(block) return_block = port.read(actual_size) if return_block != block: - logger.error("Incorrect block received") + with open("block.bin", "wb") as f: + f.write(block) + with open("return_block.bin", "wb") as f: + f.write(return_block) + + logger.error( + "Incorrect block received. Saved to `block.bin' and `return_block.bin'." + ) + logger.error(f"{bytes_to_send} bytes left. Aborting.") + success = False break bytes_to_send -= actual_size @@ -49,7 +60,8 @@ def main(): end_time = time() delta = end_time - start_time speed = args.length / delta - print(f"Speed: {speed/1024:.2f} KiB/s") + if success: + print(f"Speed: {speed/1024:.2f} KiB/s") port.write(b"\x03") # Ctrl+C port.close() From 936096d2fc3f6d150b53a5e8bf4a6ce735d717cb Mon Sep 17 00:00:00 2001 From: Anna Antonenko Date: Sat, 12 Apr 2025 01:58:28 +0400 Subject: [PATCH 237/268] cli_completions: fix null dereference --- lib/toolbox/cli/shell/cli_shell_completions.c | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/toolbox/cli/shell/cli_shell_completions.c b/lib/toolbox/cli/shell/cli_shell_completions.c index 7a178705d..6b6634dbb 100644 --- a/lib/toolbox/cli/shell/cli_shell_completions.c +++ b/lib/toolbox/cli/shell/cli_shell_completions.c @@ -265,6 +265,7 @@ void cli_shell_completions_render( } } else if(action == CliShellCompletionsActionSelectNoClose) { + if(!CommandCompletions_size(completions->variants)) return; // insert selection into prompt CliShellCompletionSegment segment = cli_shell_completions_segment(completions); FuriString* input = cli_shell_line_get_selected(completions->line); From e1bccf66b32918c5324109b44c9a4e899ef36801 Mon Sep 17 00:00:00 2001 From: Anna Antonenko Date: Sat, 12 Apr 2025 02:38:28 +0400 Subject: [PATCH 238/268] Fix NULL dereference in CLI completions (#4184) * cli_completions: fix null dereference * cli: mark free_blocks as parallel safe * codeowners: add me to co-owners of cli --- .github/CODEOWNERS | 4 +++- applications/services/cli/cli_main_commands.c | 2 +- lib/toolbox/cli/shell/cli_shell_completions.c | 1 + 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 675679080..5af8ceedb 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -23,7 +23,7 @@ /applications/main/u2f/ @DrZlo13 @hedger @gsurkov @nminaylov /applications/services/bt/ @DrZlo13 @hedger @gsurkov @gornekich -/applications/services/cli/ @DrZlo13 @hedger @gsurkov @nminaylov +/applications/services/cli/ @DrZlo13 @hedger @gsurkov @nminaylov @portasynthinca3 /applications/services/crypto/ @DrZlo13 @hedger @gsurkov @nminaylov /applications/services/desktop/ @DrZlo13 @hedger @gsurkov @nminaylov /applications/services/dolphin/ @DrZlo13 @hedger @gsurkov @nminaylov @@ -64,6 +64,8 @@ /lib/nfc/ @DrZlo13 @hedger @gsurkov @gornekich /lib/one_wire/ @DrZlo13 @hedger @gsurkov /lib/subghz/ @DrZlo13 @hedger @gsurkov @Skorpionm +/lib/toolbox/ @DrZlo13 @hedger @gsurkov +/lib/toolbox/cli @DrZlo13 @hedger @gsurkov @portasynthinca3 # CI/CD /.github/workflows/ @DrZlo13 @hedger @gsurkov diff --git a/applications/services/cli/cli_main_commands.c b/applications/services/cli/cli_main_commands.c index f84c03d8a..508a650de 100644 --- a/applications/services/cli/cli_main_commands.c +++ b/applications/services/cli/cli_main_commands.c @@ -506,7 +506,7 @@ void cli_main_commands_init(CliRegistry* registry) { cli_registry_add_command(registry, "top", CliCommandFlagParallelSafe, cli_command_top, NULL); cli_registry_add_command(registry, "free", CliCommandFlagParallelSafe, cli_command_free, NULL); cli_registry_add_command( - registry, "free_blocks", CliCommandFlagDefault, cli_command_free_blocks, NULL); + registry, "free_blocks", CliCommandFlagParallelSafe, cli_command_free_blocks, NULL); cli_registry_add_command(registry, "echo", CliCommandFlagParallelSafe, cli_command_echo, NULL); cli_registry_add_command(registry, "vibro", CliCommandFlagDefault, cli_command_vibro, NULL); diff --git a/lib/toolbox/cli/shell/cli_shell_completions.c b/lib/toolbox/cli/shell/cli_shell_completions.c index 7a178705d..6b6634dbb 100644 --- a/lib/toolbox/cli/shell/cli_shell_completions.c +++ b/lib/toolbox/cli/shell/cli_shell_completions.c @@ -265,6 +265,7 @@ void cli_shell_completions_render( } } else if(action == CliShellCompletionsActionSelectNoClose) { + if(!CommandCompletions_size(completions->variants)) return; // insert selection into prompt CliShellCompletionSegment segment = cli_shell_completions_segment(completions); FuriString* input = cli_shell_line_get_selected(completions->line); From 66a705022176af8faf7dada882fff4551aa302bc Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Sat, 12 Apr 2025 03:03:50 +0300 Subject: [PATCH 239/268] free up some ram for now, use settings user --- .../resources/subghz/assets/setting_user.example | 13 +++++++++++++ applications/main/subghz/subghz.c | 7 ++++--- lib/subghz/subghz_setting.c | 1 + 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/applications/main/subghz/resources/subghz/assets/setting_user.example b/applications/main/subghz/resources/subghz/assets/setting_user.example index 5034659be..9f74ee379 100644 --- a/applications/main/subghz/resources/subghz/assets/setting_user.example +++ b/applications/main/subghz/resources/subghz/assets/setting_user.example @@ -20,6 +20,19 @@ Version: 1 # Custom preset # format for CC1101 "Custom_preset_data:" XX YY XX YY .. 00 00 ZZ ZZ ZZ ZZ ZZ ZZ ZZ ZZ, where: XX-register, YY - register data, 00 00 - end load register, ZZ - 8 byte Pa table register +#Custom_preset_name: FM95 +#Custom_preset_module: CC1101 +#Custom_preset_data: 02 0D 0B 06 08 32 07 04 14 00 13 02 12 04 11 83 10 67 15 24 18 18 19 16 1D 91 1C 00 1B 07 20 FB 22 10 21 56 00 00 C0 00 00 00 00 00 00 00 + +#2-FSK 200khz BW / 135kHz Filter/ 15.86Khz Deviation + Ramping +#Custom_preset_name: FM15k +#Custom_preset_module: CC1101 +#Custom_preset_data: 02 0D 03 47 08 32 0B 06 15 32 14 00 13 00 12 00 11 32 10 A7 18 18 19 1D 1D 92 1C 00 1B 04 20 FB 22 17 21 B6 00 00 00 12 0E 34 60 C5 C1 C0 + +#Custom_preset_name: Pagers +#Custom_preset_module: CC1101 +#Custom_preset_data: 02 0D 07 04 08 32 0B 06 10 64 11 93 12 0C 13 02 14 00 15 15 18 18 19 16 1B 07 1C 00 1D 91 20 FB 21 56 22 10 00 00 C0 00 00 00 00 00 00 00 + #Custom_preset_name: AM_1 #Custom_preset_module: CC1101 #Custom_preset_data: 02 0D 03 07 08 32 0B 06 14 00 13 00 12 30 11 32 10 17 18 18 19 18 1D 91 1C 00 1B 07 20 FB 22 11 21 B6 00 00 00 C0 00 00 00 00 00 00 diff --git a/applications/main/subghz/subghz.c b/applications/main/subghz/subghz.c index cb4fc5504..d07eff2b3 100644 --- a/applications/main/subghz/subghz.c +++ b/applications/main/subghz/subghz.c @@ -56,11 +56,11 @@ static void subghz_rpc_command_callback(const RpcAppSystemEvent* event, void* co rpc_system_app_confirm(subghz->rpc_ctx, false); } } - +/* static void subghz_load_custom_presets(SubGhzSetting* setting) { furi_assert(setting); - const char* presets[][2] = { + const char* presets[3][2] = { {"FM95", "02 0D 0B 06 08 32 07 04 14 00 13 02 12 04 11 83 10 67 15 24 18 18 19 16 1D 91 1C 00 1B 07 20 FB 22 10 21 56 00 00 C0 00 00 00 00 00 00 00"}, @@ -88,6 +88,7 @@ static void subghz_load_custom_presets(SubGhzSetting* setting) { subghz_setting_customs_presets_to_log(setting); #endif } +*/ SubGhz* subghz_alloc(bool alloc_for_tx_only) { SubGhz* subghz = malloc(sizeof(SubGhz)); @@ -194,7 +195,7 @@ SubGhz* subghz_alloc(bool alloc_for_tx_only) { SubGhzSetting* setting = subghz_txrx_get_setting(subghz->txrx); - subghz_load_custom_presets(setting); + //subghz_load_custom_presets(setting); // Load last used values for Read, Read RAW, etc. or default subghz->last_settings = subghz_last_settings_alloc(); diff --git a/lib/subghz/subghz_setting.c b/lib/subghz/subghz_setting.c index 2ee02c7f9..ab8c77910 100644 --- a/lib/subghz/subghz_setting.c +++ b/lib/subghz/subghz_setting.c @@ -296,6 +296,7 @@ void subghz_setting_load(SubGhzSetting* instance, const char* file_path) { FURI_LOG_E(TAG, "Rewind error"); break; } + furi_string_reset(temp_str); while(flipper_format_read_string(fff_data_file, "Custom_preset_name", temp_str)) { FURI_LOG_I(TAG, "Custom preset loaded %s", furi_string_get_cstr(temp_str)); subghz_setting_load_custom_preset( From d67734674ef0d502aa2ddb768bfc033e785a79cb Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Sat, 12 Apr 2025 03:04:04 +0300 Subject: [PATCH 240/268] ci fix ? --- .drone.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.drone.yml b/.drone.yml index 15c66192c..347ab1ce1 100644 --- a/.drone.yml +++ b/.drone.yml @@ -204,7 +204,7 @@ steps: - wget "https://raw.githubusercontent.com/fieu/discord.sh/2253303efc0e7211ac2777d2535054cbb872f1e0/discord.sh" - chmod +x ./discord.sh - sed -n '/## Main changes/,/## Other changes/p' CHANGELOG.md | sed -e 's/## Main changes//' -e 's/## Other changes//' > changelogcut.txt - - head -c 1544 changelogcut.txt > changelogcutfin.txt + - head -c 1200 changelogcut.txt > changelogcutfin.txt - truncate -s -1 changelogcutfin.txt - tail -c +2 changelogcutfin.txt > changelogready.txt - rm -f changelogcut.txt @@ -456,7 +456,7 @@ steps: - wget "https://raw.githubusercontent.com/fieu/discord.sh/2253303efc0e7211ac2777d2535054cbb872f1e0/discord.sh" - chmod +x ./discord.sh - sed -n '/## Main changes/,/## Other changes/p' CHANGELOG.md | sed -e 's/## Main changes//' -e 's/## Other changes//' > changelogcut.txt - - head -c 1544 changelogcut.txt > changelogcutfin.txt + - head -c 1200 changelogcut.txt > changelogcutfin.txt - truncate -s -1 changelogcutfin.txt - tail -c +2 changelogcutfin.txt > changelogready.txt - rm -f changelogcut.txt From 455bb8ab1fe6e85e5aa91f0c33b9ad42f62042bc Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Sat, 12 Apr 2025 03:07:04 +0300 Subject: [PATCH 241/268] upd changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 13cc992c6..d462edff2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ * SubGHz: Various bugfixes and experimental options (rolling counter overflow) (by @xMasterX) * Anims: Disable winter anims * NFC: mfclassic poller fix early key reuse in dictionary attack state machine (by @noproto) +* OFW: Fix NULL dereference in CLI completions * OFW PR 4181: vcp, cli: Handle Tx/Rx events before Connect/Disconnect + extra fixes (by @portasynthinca3) * OFW: BLE: Slightly increase mfg_data size * OFW: fbt: Deterministic STARTUP order & additional checks From 75f5d6fec7715413c3792afa1c3633305abf2814 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Sat, 12 Apr 2025 03:11:03 +0300 Subject: [PATCH 242/268] force only default mods --- applications/main/subghz/subghz.c | 4 ++-- applications/main/subghz/subghz_last_settings.c | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/applications/main/subghz/subghz.c b/applications/main/subghz/subghz.c index d07eff2b3..c68639c0d 100644 --- a/applications/main/subghz/subghz.c +++ b/applications/main/subghz/subghz.c @@ -199,8 +199,8 @@ SubGhz* subghz_alloc(bool alloc_for_tx_only) { // Load last used values for Read, Read RAW, etc. or default subghz->last_settings = subghz_last_settings_alloc(); - size_t preset_count = subghz_setting_get_preset_count(setting); - subghz_last_settings_load(subghz->last_settings, preset_count); + //size_t preset_count = subghz_setting_get_preset_count(setting); + subghz_last_settings_load(subghz->last_settings, 0); // Set LED and Amp GPIO control state furi_hal_subghz_set_ext_leds_and_amp(subghz->last_settings->leds_and_amp); diff --git a/applications/main/subghz/subghz_last_settings.c b/applications/main/subghz/subghz_last_settings.c index 174a15a5b..6ffdc858e 100644 --- a/applications/main/subghz/subghz_last_settings.c +++ b/applications/main/subghz/subghz_last_settings.c @@ -31,6 +31,7 @@ void subghz_last_settings_free(SubGhzLastSettings* instance) { } void subghz_last_settings_load(SubGhzLastSettings* instance, size_t preset_count) { + UNUSED(preset_count); furi_assert(instance); // Default values (all others set to 0, if read from file fails these are used) @@ -148,7 +149,7 @@ void subghz_last_settings_load(SubGhzLastSettings* instance, size_t preset_count instance->frequency = SUBGHZ_LAST_SETTING_DEFAULT_FREQUENCY; } - if(instance->preset_index > (uint32_t)preset_count - 1) { + if(instance->preset_index > 3) { instance->preset_index = SUBGHZ_LAST_SETTING_DEFAULT_PRESET; } } From d5935dc814291a807e1a989b8b727e32461071c6 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Sat, 12 Apr 2025 03:11:35 +0300 Subject: [PATCH 243/268] fmt --- applications/services/notification/notification_app.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/applications/services/notification/notification_app.c b/applications/services/notification/notification_app.c index e57ebbda2..2df27110e 100644 --- a/applications/services/notification/notification_app.c +++ b/applications/services/notification/notification_app.c @@ -927,7 +927,7 @@ int32_t notification_srv(void* p) { rgb_backlight_set_led_static_color(0, app->settings.rgb.led_0_color_index); rgb_backlight_update(app->settings.display_brightness * app->current_night_shift); } - // if rgb_backlight not installed then set default static orange color(index=0) to all leds (0-2) and force light on + // if rgb_backlight not installed then set default static orange color(index=0) to all leds (0-2) and force light on } else { rgb_backlight_set_led_static_color(2, 0); rgb_backlight_set_led_static_color(1, 0); From a72c5238faf77b36ae4ffa5205fd0cd6598d7b52 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Sat, 12 Apr 2025 03:19:57 +0300 Subject: [PATCH 244/268] oops --- applications/main/subghz/subghz.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/applications/main/subghz/subghz.c b/applications/main/subghz/subghz.c index c68639c0d..83e68229b 100644 --- a/applications/main/subghz/subghz.c +++ b/applications/main/subghz/subghz.c @@ -193,7 +193,7 @@ SubGhz* subghz_alloc(bool alloc_for_tx_only) { //init TxRx & Protocol & History & KeyBoard subghz_unlock(subghz); - SubGhzSetting* setting = subghz_txrx_get_setting(subghz->txrx); + //SubGhzSetting* setting = subghz_txrx_get_setting(subghz->txrx); //subghz_load_custom_presets(setting); From 868eb1038162305b5e1e3193f6150173e9e95f2e Mon Sep 17 00:00:00 2001 From: WillyJL <49810075+Willy-JL@users.noreply.github.com> Date: Sat, 12 Apr 2025 04:21:39 +0100 Subject: [PATCH 245/268] SDK: Fix missing RECORD_CLI define (#4185) * SDK: Fix missing RECORD_CLI define * sdk: added compatibility `cli.h` header * cli: updated porting comments --------- Co-authored-by: hedger --- applications/services/cli/application.fam | 2 +- applications/services/cli/cli.h | 13 +++++++++++++ applications/services/cli/cli_main_commands.h | 4 ++-- targets/f18/api_symbols.csv | 1 + targets/f7/api_symbols.csv | 1 + 5 files changed, 18 insertions(+), 3 deletions(-) create mode 100644 applications/services/cli/cli.h diff --git a/applications/services/cli/application.fam b/applications/services/cli/application.fam index 6e0a28164..b305fb6b0 100644 --- a/applications/services/cli/application.fam +++ b/applications/services/cli/application.fam @@ -22,7 +22,7 @@ App( entry_point="cli_vcp_srv", stack_size=1024, order=10, - sdk_headers=["cli_vcp.h"], + sdk_headers=["cli_vcp.h", "cli.h"], sources=["cli_vcp.c"], ) diff --git a/applications/services/cli/cli.h b/applications/services/cli/cli.h new file mode 100644 index 000000000..ca787d3db --- /dev/null +++ b/applications/services/cli/cli.h @@ -0,0 +1,13 @@ +#pragma once + +/* + * Compatibility header for ease of porting existing apps. + * In short: + * Cli* is replaced with with CliRegistry* + * cli_* functions are replaced with cli_registry_* functions + * (i.e., cli_add_command() is now cli_registry_add_command()) +*/ + +#include + +#define RECORD_CLI "cli" diff --git a/applications/services/cli/cli_main_commands.h b/applications/services/cli/cli_main_commands.h index d8058f467..ebee4ba1e 100644 --- a/applications/services/cli/cli_main_commands.h +++ b/applications/services/cli/cli_main_commands.h @@ -1,9 +1,9 @@ #pragma once +#include "cli.h" #include #include -#define RECORD_CLI "cli" -#define CLI_APPID "cli" +#define CLI_APPID "cli" void cli_main_commands_init(CliRegistry* registry); diff --git a/targets/f18/api_symbols.csv b/targets/f18/api_symbols.csv index d4cc2bec5..f1ebda528 100644 --- a/targets/f18/api_symbols.csv +++ b/targets/f18/api_symbols.csv @@ -2,6 +2,7 @@ entry,status,name,type,params Version,+,85.0,, Header,+,applications/services/bt/bt_service/bt.h,, Header,+,applications/services/bt/bt_service/bt_keys_storage.h,, +Header,+,applications/services/cli/cli.h,, Header,+,applications/services/cli/cli_vcp.h,, Header,+,applications/services/dialogs/dialogs.h,, Header,+,applications/services/dolphin/dolphin.h,, diff --git a/targets/f7/api_symbols.csv b/targets/f7/api_symbols.csv index f5f18bc43..2e23dc56b 100644 --- a/targets/f7/api_symbols.csv +++ b/targets/f7/api_symbols.csv @@ -3,6 +3,7 @@ Version,+,85.0,, Header,+,applications/drivers/subghz/cc1101_ext/cc1101_ext_interconnect.h,, Header,+,applications/services/bt/bt_service/bt.h,, Header,+,applications/services/bt/bt_service/bt_keys_storage.h,, +Header,+,applications/services/cli/cli.h,, Header,+,applications/services/cli/cli_vcp.h,, Header,+,applications/services/dialogs/dialogs.h,, Header,+,applications/services/dolphin/dolphin.h,, From ddf77f58e383bc688d8df6aa179c91d18c128ce1 Mon Sep 17 00:00:00 2001 From: hedger Date: Sat, 12 Apr 2025 12:27:58 +0100 Subject: [PATCH 246/268] sdk: bump API to force re-upload for the catalog (#4186) --- targets/f18/api_symbols.csv | 2 +- targets/f7/api_symbols.csv | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/targets/f18/api_symbols.csv b/targets/f18/api_symbols.csv index f1ebda528..23c9edb63 100644 --- a/targets/f18/api_symbols.csv +++ b/targets/f18/api_symbols.csv @@ -1,5 +1,5 @@ entry,status,name,type,params -Version,+,85.0,, +Version,+,86.0,, Header,+,applications/services/bt/bt_service/bt.h,, Header,+,applications/services/bt/bt_service/bt_keys_storage.h,, Header,+,applications/services/cli/cli.h,, diff --git a/targets/f7/api_symbols.csv b/targets/f7/api_symbols.csv index 2e23dc56b..d142a6374 100644 --- a/targets/f7/api_symbols.csv +++ b/targets/f7/api_symbols.csv @@ -1,5 +1,5 @@ entry,status,name,type,params -Version,+,85.0,, +Version,+,86.0,, Header,+,applications/drivers/subghz/cc1101_ext/cc1101_ext_interconnect.h,, Header,+,applications/services/bt/bt_service/bt.h,, Header,+,applications/services/bt/bt_service/bt_keys_storage.h,, From 1eb57ba4626c11e432ec44b6c9df470bc8b795d7 Mon Sep 17 00:00:00 2001 From: WillyJL <49810075+Willy-JL@users.noreply.github.com> Date: Sat, 12 Apr 2025 12:35:19 +0100 Subject: [PATCH 247/268] FBT: Fix for Python 3.13 (#4187) Co-authored-by: hedger --- scripts/fbt/appmanifest.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/scripts/fbt/appmanifest.py b/scripts/fbt/appmanifest.py index b5b5d6e12..4da23b3dd 100644 --- a/scripts/fbt/appmanifest.py +++ b/scripts/fbt/appmanifest.py @@ -158,7 +158,7 @@ class AppManager: f"App {kw.get('appid')} cannot have fal_embedded set" ) - if apptype in AppBuildset.dist_app_types: + if apptype in AppBuildset.DIST_APP_TYPES: # For distributing .fap's resources, there's "fap_file_assets" for app_property in ("resources",): if kw.get(app_property): @@ -258,14 +258,12 @@ class AppBuildset: FlipperAppType.DEBUG: True, FlipperAppType.MENUEXTERNAL: False, } - - @classmethod - @property - def dist_app_types(cls): - """Applications that are installed on SD card""" - return list( - entry[0] for entry in cls.EXTERNAL_APP_TYPES_MAP.items() if entry[1] - ) + DIST_APP_TYPES = list( + # Applications that are installed on SD card + entry[0] + for entry in EXTERNAL_APP_TYPES_MAP.items() + if entry[1] + ) @staticmethod def print_writer(message): From d542d7d75fc3c6c92ceeba9d045c08340c08f275 Mon Sep 17 00:00:00 2001 From: Mykhailo Shevchuk Date: Sat, 12 Apr 2025 19:25:45 +0300 Subject: [PATCH 248/268] Use default UL/UL-C pwd/key as default value for key input --- applications/main/nfc/helpers/mf_ultralight_auth.c | 6 ++++-- lib/nfc/protocols/mf_ultralight/mf_ultralight.h | 6 ++++++ lib/nfc/protocols/mf_ultralight/mf_ultralight_poller_i.h | 2 -- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/applications/main/nfc/helpers/mf_ultralight_auth.c b/applications/main/nfc/helpers/mf_ultralight_auth.c index e97649cb3..fccf50cdd 100644 --- a/applications/main/nfc/helpers/mf_ultralight_auth.c +++ b/applications/main/nfc/helpers/mf_ultralight_auth.c @@ -18,9 +18,11 @@ void mf_ultralight_auth_free(MfUltralightAuth* instance) { void mf_ultralight_auth_reset(MfUltralightAuth* instance) { furi_assert(instance); + uint32_t default_password = MF_ULTRALIGHT_DEFAULT_PASSWORD; + instance->type = MfUltralightAuthTypeNone; - memset(&instance->password, 0, sizeof(MfUltralightAuthPassword)); - memset(&instance->tdes_key, 0, sizeof(MfUltralightC3DesAuthKey)); + memcpy(&instance->password, &default_password, sizeof(MfUltralightAuthPassword)); + memcpy(&instance->tdes_key, MF_ULTRALIGHT_C_DEFAULT_KEY, sizeof(MfUltralightC3DesAuthKey)); memset(&instance->pack, 0, sizeof(MfUltralightAuthPack)); } diff --git a/lib/nfc/protocols/mf_ultralight/mf_ultralight.h b/lib/nfc/protocols/mf_ultralight/mf_ultralight.h index caf25ceee..191deeda6 100644 --- a/lib/nfc/protocols/mf_ultralight/mf_ultralight.h +++ b/lib/nfc/protocols/mf_ultralight/mf_ultralight.h @@ -38,6 +38,7 @@ extern "C" { #define MF_ULTRALIGHT_TEARING_FLAG_NUM (3) #define MF_ULTRALIGHT_AUTH_PASSWORD_SIZE (4) #define MF_ULTRALIGHT_AUTH_PACK_SIZE (2) +#define MF_ULTRALIGHT_DEFAULT_PASSWORD (0xffffffffUL) #define MF_ULTRALIGHT_C_AUTH_RESPONSE_SIZE (9) #define MF_ULTRALIGHT_C_AUTH_DES_KEY_SIZE (16) @@ -47,6 +48,11 @@ extern "C" { #define MF_ULTRALIGHT_C_AUTH_RND_A_BLOCK_OFFSET (0) #define MF_ULTRALIGHT_C_AUTH_RND_B_BLOCK_OFFSET (8) #define MF_ULTRALIGHT_C_ENCRYPTED_PACK_SIZE (MF_ULTRALIGHT_C_AUTH_DATA_SIZE + 1) +#define MF_ULTRALIGHT_C_DEFAULT_KEY \ + (uint8_t[]) { \ + 0x49, 0x45, 0x4D, 0x4B, 0x41, 0x45, 0x52, 0x42, 0x21, 0x4E, 0x41, 0x43, 0x55, 0x4F, 0x59, \ + 0x46 \ + } typedef enum { MfUltralightErrorNone, diff --git a/lib/nfc/protocols/mf_ultralight/mf_ultralight_poller_i.h b/lib/nfc/protocols/mf_ultralight/mf_ultralight_poller_i.h index b35c49aea..6880a0c43 100644 --- a/lib/nfc/protocols/mf_ultralight/mf_ultralight_poller_i.h +++ b/lib/nfc/protocols/mf_ultralight/mf_ultralight_poller_i.h @@ -11,8 +11,6 @@ extern "C" { #define MF_ULTRALIGHT_POLLER_STANDARD_FWT_FC (60000) #define MF_ULTRALIGHT_MAX_BUFF_SIZE (64) -#define MF_ULTRALIGHT_DEFAULT_PASSWORD (0xffffffffUL) - #define MF_ULTRALIGHT_IS_NTAG_I2C(type) \ (((type) == MfUltralightTypeNTAGI2C1K) || ((type) == MfUltralightTypeNTAGI2C2K) || \ ((type) == MfUltralightTypeNTAGI2CPlus1K) || ((type) == MfUltralightTypeNTAGI2CPlus2K)) From ab26469a05b8dca713e31d65ed82aed9e1d2751e Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Sat, 12 Apr 2025 20:15:54 +0300 Subject: [PATCH 249/268] upd changelog --- CHANGELOG.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d462edff2..0b679620f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,5 @@ ## Main changes -- Current API: 85.0 +- Current API: 86.0 **WARNING! After install of this version your Desktop (fav apps) and LCD & Notifications settings will be reset to default values, please configure them again after this update!** (this is required due to big updates on that parts and config struct changes) * SubGHz: Add **Revers RB2 / RB2M Protocol** (static 64 bit) **full support** with add manually (by @xMasterX) * SubGHz: **Fix Hollarm protocol with more verification** @@ -10,6 +10,7 @@ * Display: **LCD Color Inversion** (Settings - LCD and Notifications - LCD inversion.) (PR #887 | by @Dmitry422) * Display: **Night Shift Feature** (dimming backlight in selected time interval) (PR #885 | by @Dmitry422) * Display: **Сombining RGB Backlight mod** (by @quen0n) and original backlight support **in one firmware** (+ Rainbow/Wave effect (based on @Willy-JL idea)) (PR #877 #881 #890 | by @Dmitry422) - (**To enable RGB Backlight support go into Notifications settings with Debug mode - ON**) +* NFC: Use default UL/UL-C pwd/key as default value for key input (PR #891 | by @mishamyte) * OFW: LFRFID - **EM4305 support** * OFW: **Universal IR signal selection** * OFW: **BadUSB: Mouse control** @@ -25,9 +26,13 @@ * Power: **Option to limit battery charging** (suppress charging on selected charge level) (PR #867 | by @Dmitry422) (idea and example by @oltenxyz) * Apps: **Check out more Apps updates and fixes by following** [this link](https://github.com/xMasterX/all-the-plugins/commits/dev) ## Other changes +* SubGHz: Move hardcoded extra modulations to user config - uncomment them in setting_user.example and remove .example from filename * SubGHz: Various bugfixes and experimental options (rolling counter overflow) (by @xMasterX) * Anims: Disable winter anims * NFC: mfclassic poller fix early key reuse in dictionary attack state machine (by @noproto) +* OFW: FBT: Fix for Python 3.13 +* OFW: sdk: bump API to force re-upload for the catalog +* OFW: SDK: Fix missing RECORD_CLI define * OFW: Fix NULL dereference in CLI completions * OFW PR 4181: vcp, cli: Handle Tx/Rx events before Connect/Disconnect + extra fixes (by @portasynthinca3) * OFW: BLE: Slightly increase mfg_data size From 535afc41f30a3de5d89dc368a8f7b9db590a3fc9 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Sun, 13 Apr 2025 00:37:20 +0300 Subject: [PATCH 250/268] fix subghz cli --- applications/main/subghz/subghz_cli.c | 1 - 1 file changed, 1 deletion(-) diff --git a/applications/main/subghz/subghz_cli.c b/applications/main/subghz/subghz_cli.c index 5158e17b8..947920964 100644 --- a/applications/main/subghz/subghz_cli.c +++ b/applications/main/subghz/subghz_cli.c @@ -823,7 +823,6 @@ void subghz_cli_command_tx_from_file(PipeSide* pipe, FuriString* args, void* con subghz_devices_deinit(); // Reset custom settings subghz_environment_reset_keeloq(environment); - faac_slh_reset_prog_mode(); subghz_custom_btns_reset(); // Free environment subghz_environment_free(environment); From 8a0fb5df36e3c6fb99b63205ddbaf917459ef695 Mon Sep 17 00:00:00 2001 From: Dmitry422 Date: Sun, 13 Apr 2025 23:43:43 +0700 Subject: [PATCH 251/268] LCD Inversion refactoring --- applications/services/gui/canvas.c | 34 ++----------------- applications/services/gui/canvas.h | 3 -- .../services/notification/notification_app.c | 20 +++++------ .../notification_settings_app.c | 10 +++--- lib/u8g2/u8g2_buffer.c | 8 +---- lib/u8g2/u8g2_glue.c | 14 ++++++++ lib/u8g2/u8g2_glue.h | 2 ++ targets/f7/api_symbols.csv | 2 -- 8 files changed, 33 insertions(+), 60 deletions(-) diff --git a/applications/services/gui/canvas.c b/applications/services/gui/canvas.c index 573ad185e..337789dd3 100644 --- a/applications/services/gui/canvas.c +++ b/applications/services/gui/canvas.c @@ -94,16 +94,6 @@ size_t canvas_get_buffer_size(const Canvas* canvas) { return u8g2_GetBufferTileWidth(&canvas->fb) * u8g2_GetBufferTileHeight(&canvas->fb) * 8; } -bool canvas_is_inverted_lcd(Canvas* canvas) { - furi_assert(canvas); - return canvas->lcd_inversion; -} - -void canvas_set_inverted_lcd(Canvas* canvas, bool inverted) { - furi_assert(canvas); - canvas->lcd_inversion = inverted; -} - void canvas_frame_set( Canvas* canvas, int32_t offset_x, @@ -151,24 +141,11 @@ const CanvasFontParameters* canvas_get_font_params(const Canvas* canvas, Font fo void canvas_clear(Canvas* canvas) { furi_check(canvas); - - if(canvas->lcd_inversion) { - u8g2_FillBuffer(&canvas->fb); - } else { - u8g2_ClearBuffer(&canvas->fb); - } + u8g2_ClearBuffer(&canvas->fb); } void canvas_set_color(Canvas* canvas, Color color) { furi_check(canvas); - - if(canvas->lcd_inversion) { - if(color == ColorBlack) { - color = ColorWhite; - } else if(color == ColorWhite) { - color = ColorBlack; - } - } u8g2_SetDrawColor(&canvas->fb, color); } @@ -178,14 +155,7 @@ void canvas_set_font_direction(Canvas* canvas, CanvasDirection dir) { } void canvas_invert_color(Canvas* canvas) { - if((canvas->fb.draw_color == ColorXOR) && canvas->lcd_inversion) { - // ColorXOR = 0x02, inversion change it to White = 0x00 - // but if we have lcd_inversion ON then we need Black =0x01 instead White 0x00 - // so we force changing color to black - canvas->fb.draw_color = ColorBlack; - } else { - canvas->fb.draw_color = !canvas->fb.draw_color; - } + canvas->fb.draw_color = !canvas->fb.draw_color; } void canvas_set_font(Canvas* canvas, Font font) { diff --git a/applications/services/gui/canvas.h b/applications/services/gui/canvas.h index 8257c481d..88ae82283 100644 --- a/applications/services/gui/canvas.h +++ b/applications/services/gui/canvas.h @@ -447,9 +447,6 @@ void canvas_draw_icon_bitmap( int16_t h, const Icon* icon); -bool canvas_is_inverted_lcd(Canvas* canvas); - -void canvas_set_inverted_lcd(Canvas* canvas, bool inverted); #ifdef __cplusplus } diff --git a/applications/services/notification/notification_app.c b/applications/services/notification/notification_app.c index 2df27110e..ef9677f31 100644 --- a/applications/services/notification/notification_app.c +++ b/applications/services/notification/notification_app.c @@ -835,12 +835,8 @@ static NotificationApp* notification_app_alloc(void) { app->settings.rgb.rainbow_saturation = 255; app->settings.rgb.rainbow_wide = 50; - // use RECORD for setup init values to canvas lcd_inverted - Gui* tmp_gui = furi_record_open(RECORD_GUI); - Canvas* tmp_canvas = gui_direct_draw_acquire(tmp_gui); - canvas_set_inverted_lcd(tmp_canvas, false); - gui_direct_draw_release(tmp_gui); - furi_record_close(RECORD_GUI); + // set inital value, later it will be rewriten by loading settings from file + app->settings.lcd_inversion = false; return app; } @@ -873,12 +869,12 @@ static void notification_apply_settings(NotificationApp* app) { } // --- NIGHT SHIFT END --- - //setup canvas variable "inversion" by settings value; - Gui* tmp_gui = furi_record_open(RECORD_GUI); - Canvas* tmp_canvas = gui_direct_draw_acquire(tmp_gui); - canvas_set_inverted_lcd(tmp_canvas, app->settings.lcd_inversion); - gui_direct_draw_release(tmp_gui); - furi_record_close(RECORD_GUI); + // check RECORD_GUI is exist (insurance on boot time) then use it to setup lcd inversion mode from loaded settings; + if(furi_record_exists(RECORD_GUI)) { + Gui* gui = furi_record_open(RECORD_GUI); + u8x8_d_st756x_set_inversion(&gui->canvas->fb.u8x8, app->settings.lcd_inversion); + furi_record_close(RECORD_GUI); + } } static void notification_init_settings(NotificationApp* app) { diff --git a/applications/settings/notification_settings/notification_settings_app.c b/applications/settings/notification_settings/notification_settings_app.c index 8794c53f6..4139967c4 100644 --- a/applications/settings/notification_settings/notification_settings_app.c +++ b/applications/settings/notification_settings/notification_settings_app.c @@ -4,6 +4,8 @@ #include #include #include +#include +#include #define MAX_NOTIFICATION_SETTINGS 5 @@ -348,11 +350,11 @@ static void lcd_inversion_changed(VariableItem* item) { variable_item_set_current_value_text(item, lcd_inversion_text[index]); app->notification->settings.lcd_inversion = lcd_inversion_value[index]; - Canvas* tmp_canvas = gui_direct_draw_acquire(app->gui); - canvas_set_inverted_lcd(tmp_canvas, lcd_inversion_value[index]); - gui_direct_draw_release(app->gui); + Gui* gui = furi_record_open(RECORD_GUI); + u8x8_d_st756x_set_inversion(&gui->canvas->fb.u8x8, lcd_inversion_value[index]); + furi_record_close(RECORD_GUI); - notification_message(app->notification, &sequence_display_backlight_on); + notification_message_save_settings(app->notification); } //--- RGB BACKLIGHT --- diff --git a/lib/u8g2/u8g2_buffer.c b/lib/u8g2/u8g2_buffer.c index 57f4f84be..cd4e67972 100644 --- a/lib/u8g2/u8g2_buffer.c +++ b/lib/u8g2/u8g2_buffer.c @@ -37,6 +37,7 @@ #include /*============================================*/ + void u8g2_ClearBuffer(u8g2_t* u8g2) { size_t cnt; cnt = u8g2_GetU8x8(u8g2)->display_info->tile_width; @@ -45,13 +46,6 @@ void u8g2_ClearBuffer(u8g2_t* u8g2) { memset(u8g2->tile_buf_ptr, 0, cnt); } -void u8g2_FillBuffer(u8g2_t* u8g2) { - size_t cnt; - cnt = u8g2_GetU8x8(u8g2)->display_info->tile_width; - cnt *= u8g2->tile_buf_height; - cnt *= 8; - memset(u8g2->tile_buf_ptr, 255, cnt); -} /*============================================*/ static void u8g2_send_tile_row(u8g2_t* u8g2, uint8_t src_tile_row, uint8_t dest_tile_row) { diff --git a/lib/u8g2/u8g2_glue.c b/lib/u8g2/u8g2_glue.c index b3e74c229..57d73b22e 100644 --- a/lib/u8g2/u8g2_glue.c +++ b/lib/u8g2/u8g2_glue.c @@ -280,3 +280,17 @@ void u8g2_Setup_st756x_flipper( buf = u8g2_m_16_8_f(&tile_buf_height); u8g2_SetupBuffer(u8g2, buf, tile_buf_height, u8g2_ll_hvline_vertical_top_lsb, rotation); } + +void u8x8_d_st756x_set_inversion(u8x8_t* u8x8, bool arg) { + // if arg is true - set last bit to 1 (inversion ON) + if(arg) { + u8x8_cad_StartTransfer(u8x8); + u8x8_cad_SendCmd(u8x8, ST756X_CMD_INVERSE_DISPLAY | 0b01); + u8x8_cad_EndTransfer(u8x8); + // else use standart command with 0 in last bit + } else { + u8x8_cad_StartTransfer(u8x8); + u8x8_cad_SendCmd(u8x8, ST756X_CMD_INVERSE_DISPLAY); + u8x8_cad_EndTransfer(u8x8); + } +} diff --git a/lib/u8g2/u8g2_glue.h b/lib/u8g2/u8g2_glue.h index af236279e..16e7df75e 100644 --- a/lib/u8g2/u8g2_glue.h +++ b/lib/u8g2/u8g2_glue.h @@ -16,3 +16,5 @@ void u8g2_Setup_st756x_flipper( void u8x8_d_st756x_init(u8x8_t* u8x8, uint8_t contrast, uint8_t regulation_ratio, bool bias); void u8x8_d_st756x_set_contrast(u8x8_t* u8x8, int8_t contrast_offset); + +void u8x8_d_st756x_set_inversion(u8x8_t* u8x8, bool arg); \ No newline at end of file diff --git a/targets/f7/api_symbols.csv b/targets/f7/api_symbols.csv index af74003de..7c4dca564 100755 --- a/targets/f7/api_symbols.csv +++ b/targets/f7/api_symbols.csv @@ -848,14 +848,12 @@ Function,+,canvas_get_font_params,const CanvasFontParameters*,"const Canvas*, Fo Function,+,canvas_glyph_width,size_t,"Canvas*, uint16_t" Function,+,canvas_height,size_t,const Canvas* Function,+,canvas_invert_color,void,Canvas* -Function,+,canvas_is_inverted_lcd,_Bool,Canvas* Function,+,canvas_reset,void,Canvas* Function,+,canvas_set_bitmap_mode,void,"Canvas*, _Bool" Function,+,canvas_set_color,void,"Canvas*, Color" Function,+,canvas_set_custom_u8g2_font,void,"Canvas*, const uint8_t*" Function,+,canvas_set_font,void,"Canvas*, Font" Function,+,canvas_set_font_direction,void,"Canvas*, CanvasDirection" -Function,+,canvas_set_inverted_lcd,void,"Canvas*, _Bool" Function,+,canvas_string_width,uint16_t,"Canvas*, const char*" Function,+,canvas_width,size_t,const Canvas* Function,-,cbrt,double,double From 8a5129449d7b6a19f7cdba6635d5271782b05bda Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Mon, 14 Apr 2025 00:24:17 +0300 Subject: [PATCH 252/268] fix --- applications/services/gui/canvas.h | 1 - applications/services/gui/canvas_i.h | 1 - lib/u8g2/u8g2_glue.h | 2 +- 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/applications/services/gui/canvas.h b/applications/services/gui/canvas.h index 88ae82283..efd314687 100644 --- a/applications/services/gui/canvas.h +++ b/applications/services/gui/canvas.h @@ -447,7 +447,6 @@ void canvas_draw_icon_bitmap( int16_t h, const Icon* icon); - #ifdef __cplusplus } #endif diff --git a/applications/services/gui/canvas_i.h b/applications/services/gui/canvas_i.h index 420b97e0f..449db71db 100644 --- a/applications/services/gui/canvas_i.h +++ b/applications/services/gui/canvas_i.h @@ -47,7 +47,6 @@ struct Canvas { CompressIcon* compress_icon; CanvasCallbackPairArray_t canvas_callback_pair; FuriMutex* mutex; - bool lcd_inversion; }; /** Allocate memory and initialize canvas diff --git a/lib/u8g2/u8g2_glue.h b/lib/u8g2/u8g2_glue.h index 16e7df75e..858e8ec7f 100644 --- a/lib/u8g2/u8g2_glue.h +++ b/lib/u8g2/u8g2_glue.h @@ -17,4 +17,4 @@ void u8x8_d_st756x_init(u8x8_t* u8x8, uint8_t contrast, uint8_t regulation_ratio void u8x8_d_st756x_set_contrast(u8x8_t* u8x8, int8_t contrast_offset); -void u8x8_d_st756x_set_inversion(u8x8_t* u8x8, bool arg); \ No newline at end of file +void u8x8_d_st756x_set_inversion(u8x8_t* u8x8, bool arg); From e8e87c66e3dfebd3ac9f9415113f613dee8b8484 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Mon, 14 Apr 2025 00:29:39 +0300 Subject: [PATCH 253/268] upd changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b679620f..5a6477637 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ * SubGHz: **Came Atomo button hold simulation with full cycle** simulation (to allow proper pairing with receiver) * SubGHz: Add **Prastel (42bit static code)** support (OFW PR 4178 by @pmazzini) * Desktop: **Add support for Favorite App - Ok Long** (Warning! Old favourites apps list will be reset!) (PR #886 | by @DrEverr) -* Display: **LCD Color Inversion** (Settings - LCD and Notifications - LCD inversion.) (PR #887 | by @Dmitry422) +* Display: **LCD Color Inversion** (Settings - LCD and Notifications - LCD inversion.) (PR #887 #893 | by @Dmitry422) * Display: **Night Shift Feature** (dimming backlight in selected time interval) (PR #885 | by @Dmitry422) * Display: **Сombining RGB Backlight mod** (by @quen0n) and original backlight support **in one firmware** (+ Rainbow/Wave effect (based on @Willy-JL idea)) (PR #877 #881 #890 | by @Dmitry422) - (**To enable RGB Backlight support go into Notifications settings with Debug mode - ON**) * NFC: Use default UL/UL-C pwd/key as default value for key input (PR #891 | by @mishamyte) From 63cfa2d6845575b25217295b9e7838f94b48e5c4 Mon Sep 17 00:00:00 2001 From: Anna Antonenko Date: Tue, 15 Apr 2025 03:45:16 +0400 Subject: [PATCH 254/268] cli_vcp: make enable/disable requests synchronous --- applications/services/cli/cli_vcp.c | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/applications/services/cli/cli_vcp.c b/applications/services/cli/cli_vcp.c index def1949e2..cea4de248 100644 --- a/applications/services/cli/cli_vcp.c +++ b/applications/services/cli/cli_vcp.c @@ -5,6 +5,7 @@ #include #include #include +#include #include "cli_main_shell.h" #include "cli_main_commands.h" @@ -26,6 +27,7 @@ typedef struct { CliVcpMessageTypeEnable, CliVcpMessageTypeDisable, } type; + FuriApiLock api_lock; union {}; } CliVcpMessage; @@ -182,6 +184,8 @@ static void cli_vcp_message_received(FuriEventLoopObject* object, void* context) furi_hal_usb_set_config(cli_vcp->previous_interface, NULL); break; } + + api_lock_unlock(message.api_lock); } /** @@ -300,16 +304,24 @@ int32_t cli_vcp_srv(void* p) { // Public API // ========== +static void cli_vcp_synchronous_request(CliVcp* cli_vcp, CliVcpMessage* message) { + message->api_lock = api_lock_alloc_locked(); + furi_message_queue_put(cli_vcp->message_queue, message, FuriWaitForever); + api_lock_wait_unlock_and_free(message->api_lock); +} + void cli_vcp_enable(CliVcp* cli_vcp) { + furi_check(cli_vcp); CliVcpMessage message = { .type = CliVcpMessageTypeEnable, }; - furi_message_queue_put(cli_vcp->message_queue, &message, FuriWaitForever); + cli_vcp_synchronous_request(cli_vcp, &message); } void cli_vcp_disable(CliVcp* cli_vcp) { + furi_check(cli_vcp); CliVcpMessage message = { .type = CliVcpMessageTypeDisable, }; - furi_message_queue_put(cli_vcp->message_queue, &message, FuriWaitForever); + cli_vcp_synchronous_request(cli_vcp, &message); } From 086a9185fc0d438c020ca027997a22ab52f88abb Mon Sep 17 00:00:00 2001 From: Anna Antonenko Date: Tue, 15 Apr 2025 03:45:37 +0400 Subject: [PATCH 255/268] usb_uart_bridge: open and close vcp record once --- applications/main/gpio/usb_uart_bridge.c | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/applications/main/gpio/usb_uart_bridge.c b/applications/main/gpio/usb_uart_bridge.c index 77cd02f63..3e1cefb93 100644 --- a/applications/main/gpio/usb_uart_bridge.c +++ b/applications/main/gpio/usb_uart_bridge.c @@ -60,6 +60,8 @@ struct UsbUartBridge { FuriApiLock cfg_lock; + CliVcp* cli_vcp; + uint8_t rx_buf[USB_CDC_PKT_LEN]; }; @@ -105,15 +107,11 @@ static void usb_uart_on_irq_rx_dma_cb( static void usb_uart_vcp_init(UsbUartBridge* usb_uart, uint8_t vcp_ch) { furi_hal_usb_unlock(); if(vcp_ch == 0) { - CliVcp* cli_vcp = furi_record_open(RECORD_CLI_VCP); - cli_vcp_disable(cli_vcp); - furi_record_close(RECORD_CLI_VCP); + cli_vcp_disable(usb_uart->cli_vcp); furi_check(furi_hal_usb_set_config(&usb_cdc_single, NULL) == true); } else { furi_check(furi_hal_usb_set_config(&usb_cdc_dual, NULL) == true); - CliVcp* cli_vcp = furi_record_open(RECORD_CLI_VCP); - cli_vcp_enable(cli_vcp); - furi_record_close(RECORD_CLI_VCP); + cli_vcp_enable(usb_uart->cli_vcp); } furi_hal_cdc_set_callbacks(vcp_ch, (CdcCallbacks*)&cdc_cb, usb_uart); } @@ -122,9 +120,7 @@ static void usb_uart_vcp_deinit(UsbUartBridge* usb_uart, uint8_t vcp_ch) { UNUSED(usb_uart); furi_hal_cdc_set_callbacks(vcp_ch, NULL, NULL); if(vcp_ch != 0) { - CliVcp* cli_vcp = furi_record_open(RECORD_CLI_VCP); - cli_vcp_disable(cli_vcp); - furi_record_close(RECORD_CLI_VCP); + cli_vcp_disable(usb_uart->cli_vcp); } } @@ -176,6 +172,8 @@ static int32_t usb_uart_worker(void* context) { memcpy(&usb_uart->cfg, &usb_uart->cfg_new, sizeof(UsbUartConfig)); + usb_uart->cli_vcp = furi_record_open(RECORD_CLI_VCP); + usb_uart->rx_stream = furi_stream_buffer_alloc(USB_UART_RX_BUF_SIZE, 1); usb_uart->tx_sem = furi_semaphore_alloc(1, 1); @@ -308,8 +306,8 @@ static int32_t usb_uart_worker(void* context) { furi_hal_usb_unlock(); furi_check(furi_hal_usb_set_config(&usb_cdc_single, NULL) == true); - CliVcp* cli_vcp = furi_record_open(RECORD_CLI_VCP); - cli_vcp_enable(cli_vcp); + cli_vcp_enable(usb_uart->cli_vcp); + furi_record_close(RECORD_CLI_VCP); return 0; From 008c5a8828993140aa48ff445973ae0692428bf0 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Tue, 15 Apr 2025 04:38:01 +0300 Subject: [PATCH 256/268] upd changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a6477637..2f96c8929 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ * SubGHz: Various bugfixes and experimental options (rolling counter overflow) (by @xMasterX) * Anims: Disable winter anims * NFC: mfclassic poller fix early key reuse in dictionary attack state machine (by @noproto) +* OFW PR 4189: USB-UART bridge fix (by @portasynthinca3) * OFW: FBT: Fix for Python 3.13 * OFW: sdk: bump API to force re-upload for the catalog * OFW: SDK: Fix missing RECORD_CLI define From 34a3222ec4082786392bc60f0d4ea1069077d5b4 Mon Sep 17 00:00:00 2001 From: Anna Antonenko Date: Wed, 16 Apr 2025 07:20:31 +0400 Subject: [PATCH 257/268] [FL-3979] USB-UART bridge fix (#4189) * cli_vcp: make enable/disable requests synchronous * usb_uart_bridge: open and close vcp record once --- applications/main/gpio/usb_uart_bridge.c | 20 +++++++++----------- applications/services/cli/cli_vcp.c | 16 ++++++++++++++-- 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/applications/main/gpio/usb_uart_bridge.c b/applications/main/gpio/usb_uart_bridge.c index 77cd02f63..3e1cefb93 100644 --- a/applications/main/gpio/usb_uart_bridge.c +++ b/applications/main/gpio/usb_uart_bridge.c @@ -60,6 +60,8 @@ struct UsbUartBridge { FuriApiLock cfg_lock; + CliVcp* cli_vcp; + uint8_t rx_buf[USB_CDC_PKT_LEN]; }; @@ -105,15 +107,11 @@ static void usb_uart_on_irq_rx_dma_cb( static void usb_uart_vcp_init(UsbUartBridge* usb_uart, uint8_t vcp_ch) { furi_hal_usb_unlock(); if(vcp_ch == 0) { - CliVcp* cli_vcp = furi_record_open(RECORD_CLI_VCP); - cli_vcp_disable(cli_vcp); - furi_record_close(RECORD_CLI_VCP); + cli_vcp_disable(usb_uart->cli_vcp); furi_check(furi_hal_usb_set_config(&usb_cdc_single, NULL) == true); } else { furi_check(furi_hal_usb_set_config(&usb_cdc_dual, NULL) == true); - CliVcp* cli_vcp = furi_record_open(RECORD_CLI_VCP); - cli_vcp_enable(cli_vcp); - furi_record_close(RECORD_CLI_VCP); + cli_vcp_enable(usb_uart->cli_vcp); } furi_hal_cdc_set_callbacks(vcp_ch, (CdcCallbacks*)&cdc_cb, usb_uart); } @@ -122,9 +120,7 @@ static void usb_uart_vcp_deinit(UsbUartBridge* usb_uart, uint8_t vcp_ch) { UNUSED(usb_uart); furi_hal_cdc_set_callbacks(vcp_ch, NULL, NULL); if(vcp_ch != 0) { - CliVcp* cli_vcp = furi_record_open(RECORD_CLI_VCP); - cli_vcp_disable(cli_vcp); - furi_record_close(RECORD_CLI_VCP); + cli_vcp_disable(usb_uart->cli_vcp); } } @@ -176,6 +172,8 @@ static int32_t usb_uart_worker(void* context) { memcpy(&usb_uart->cfg, &usb_uart->cfg_new, sizeof(UsbUartConfig)); + usb_uart->cli_vcp = furi_record_open(RECORD_CLI_VCP); + usb_uart->rx_stream = furi_stream_buffer_alloc(USB_UART_RX_BUF_SIZE, 1); usb_uart->tx_sem = furi_semaphore_alloc(1, 1); @@ -308,8 +306,8 @@ static int32_t usb_uart_worker(void* context) { furi_hal_usb_unlock(); furi_check(furi_hal_usb_set_config(&usb_cdc_single, NULL) == true); - CliVcp* cli_vcp = furi_record_open(RECORD_CLI_VCP); - cli_vcp_enable(cli_vcp); + cli_vcp_enable(usb_uart->cli_vcp); + furi_record_close(RECORD_CLI_VCP); return 0; diff --git a/applications/services/cli/cli_vcp.c b/applications/services/cli/cli_vcp.c index def1949e2..cea4de248 100644 --- a/applications/services/cli/cli_vcp.c +++ b/applications/services/cli/cli_vcp.c @@ -5,6 +5,7 @@ #include #include #include +#include #include "cli_main_shell.h" #include "cli_main_commands.h" @@ -26,6 +27,7 @@ typedef struct { CliVcpMessageTypeEnable, CliVcpMessageTypeDisable, } type; + FuriApiLock api_lock; union {}; } CliVcpMessage; @@ -182,6 +184,8 @@ static void cli_vcp_message_received(FuriEventLoopObject* object, void* context) furi_hal_usb_set_config(cli_vcp->previous_interface, NULL); break; } + + api_lock_unlock(message.api_lock); } /** @@ -300,16 +304,24 @@ int32_t cli_vcp_srv(void* p) { // Public API // ========== +static void cli_vcp_synchronous_request(CliVcp* cli_vcp, CliVcpMessage* message) { + message->api_lock = api_lock_alloc_locked(); + furi_message_queue_put(cli_vcp->message_queue, message, FuriWaitForever); + api_lock_wait_unlock_and_free(message->api_lock); +} + void cli_vcp_enable(CliVcp* cli_vcp) { + furi_check(cli_vcp); CliVcpMessage message = { .type = CliVcpMessageTypeEnable, }; - furi_message_queue_put(cli_vcp->message_queue, &message, FuriWaitForever); + cli_vcp_synchronous_request(cli_vcp, &message); } void cli_vcp_disable(CliVcp* cli_vcp) { + furi_check(cli_vcp); CliVcpMessage message = { .type = CliVcpMessageTypeDisable, }; - furi_message_queue_put(cli_vcp->message_queue, &message, FuriWaitForever); + cli_vcp_synchronous_request(cli_vcp, &message); } From 1d4db1ee2c3357c20dae0619d5e16bbcfd00dc90 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Fri, 18 Apr 2025 00:59:24 +0300 Subject: [PATCH 258/268] unhide rgb from debug --- .../notification_settings_app.c | 67 ++++++++----------- 1 file changed, 28 insertions(+), 39 deletions(-) diff --git a/applications/settings/notification_settings/notification_settings_app.c b/applications/settings/notification_settings/notification_settings_app.c index 4139967c4..5ab15514b 100644 --- a/applications/settings/notification_settings/notification_settings_app.c +++ b/applications/settings/notification_settings/notification_settings_app.c @@ -393,10 +393,8 @@ static void rgb_backlight_installed_changed(VariableItem* item) { } // Lock/Unlock all rgb settings depent from rgb_backlight_installed switch - int slide = 0; - if(furi_hal_rtc_is_flag_set(FuriHalRtcFlagDebug)) { - slide = 1; - } + int slide = 1; + for(int i = slide; i < (slide + 8); i++) { VariableItem* t_item = variable_item_list_get(app->variable_item_list_rgb, i); if(index == 0) { @@ -535,9 +533,7 @@ void variable_item_list_enter_callback(void* context, uint32_t index) { UNUSED(context); NotificationAppSettings* app = context; - if(((app->notification->settings.rgb.rgb_backlight_installed) || - (furi_hal_rtc_is_flag_set(FuriHalRtcFlagDebug))) && - (index == 0)) { + if(index == 0) { view_dispatcher_switch_to_view(app->view_dispatcher, RGBViewId); } } @@ -563,17 +559,14 @@ static void night_shift_changed(VariableItem* item) { // force demo night_shift brightness ot rgb backlight and stock backlight notification_message(app->notification, &sequence_display_backlight_on); - int slide = 0; - if(furi_hal_rtc_is_flag_set(FuriHalRtcFlagDebug) || - (app->notification->settings.rgb.rgb_backlight_installed)) { - slide = 1; - } + int slide = 1; + for(int i = 4 + slide; i < (6 + slide); i++) { VariableItem* t_item = variable_item_list_get(app->variable_item_list, i); if(index == 0) { - variable_item_set_locked(t_item, true, "Night shift\nOFF!"); + variable_item_set_locked(t_item, true, "Night Shift\nOFF!"); } else { - variable_item_set_locked(t_item, false, "Night shift\nOFF!"); + variable_item_set_locked(t_item, false, "Night Shift\nOFF!"); } } @@ -633,10 +626,7 @@ static NotificationAppSettings* alloc_settings(void) { app->variable_item_list, variable_item_list_enter_callback, app); // Show RGB settings only when debug_mode or rgb_backlight_installed is active - if((app->notification->settings.rgb.rgb_backlight_installed) || - (furi_hal_rtc_is_flag_set(FuriHalRtcFlagDebug))) { - item = variable_item_list_add(app->variable_item_list, "RGB settings", 0, NULL, app); - } + item = variable_item_list_add(app->variable_item_list, "RGB Mod Settings", 0, NULL, app); //--- RGB BACKLIGHT END --- item = variable_item_list_add( @@ -662,7 +652,7 @@ static NotificationAppSettings* alloc_settings(void) { // --- NIGHT SHIFT --- item = variable_item_list_add( - app->variable_item_list, "Night shift", NIGHT_SHIFT_COUNT, night_shift_changed, app); + app->variable_item_list, "Night Shift", NIGHT_SHIFT_COUNT, night_shift_changed, app); value_index = value_index_float( app->notification->settings.night_shift, night_shift_value, NIGHT_SHIFT_COUNT); variable_item_set_current_value_index(item, value_index); @@ -681,7 +671,7 @@ static NotificationAppSettings* alloc_settings(void) { variable_item_set_current_value_index(item, value_index); variable_item_set_current_value_text(item, night_shift_start_text[value_index]); variable_item_set_locked( - item, (app->notification->settings.night_shift == 1), "Night shift \nOFF!"); + item, (app->notification->settings.night_shift == 1), "Night Shift \nOFF!"); item = variable_item_list_add( app->variable_item_list, " . End", NIGHT_SHIFT_END_COUNT, night_shift_end_changed, app); @@ -690,7 +680,7 @@ static NotificationAppSettings* alloc_settings(void) { variable_item_set_current_value_index(item, value_index); variable_item_set_current_value_text(item, night_shift_end_text[value_index]); variable_item_set_locked( - item, (app->notification->settings.night_shift == 1), "Night shift \nOFF!"); + item, (app->notification->settings.night_shift == 1), "Night Shift \nOFF!"); // --- NIGHT SHIFT END--- @@ -730,7 +720,7 @@ static NotificationAppSettings* alloc_settings(void) { } item = variable_item_list_add( - app->variable_item_list, "LCD inversion", LCD_INVERSION_COUNT, lcd_inversion_changed, app); + app->variable_item_list, "LCD Inversion", LCD_INVERSION_COUNT, lcd_inversion_changed, app); value_index = value_index_bool( app->notification->settings.lcd_inversion, lcd_inversion_value, LCD_INVERSION_COUNT); variable_item_set_current_value_index(item, value_index); @@ -745,26 +735,25 @@ static NotificationAppSettings* alloc_settings(void) { view_set_previous_callback(view_rgb, notification_app_rgb_settings_exit); // Show rgb_backlight_installed swith only in Debug mode - if(furi_hal_rtc_is_flag_set(FuriHalRtcFlagDebug)) { - item = variable_item_list_add( - app->variable_item_list_rgb, - "RGB backlight installed", - RGB_BACKLIGHT_INSTALLED_COUNT, - rgb_backlight_installed_changed, - app); - value_index = value_index_bool( - app->notification->settings.rgb.rgb_backlight_installed, - rgb_backlight_installed_value, - RGB_BACKLIGHT_INSTALLED_COUNT); - variable_item_set_current_value_index(item, value_index); - variable_item_set_current_value_text(item, rgb_backlight_installed_text[value_index]); - } + + item = variable_item_list_add( + app->variable_item_list_rgb, + "RGB backlight installed", + RGB_BACKLIGHT_INSTALLED_COUNT, + rgb_backlight_installed_changed, + app); + value_index = value_index_bool( + app->notification->settings.rgb.rgb_backlight_installed, + rgb_backlight_installed_value, + RGB_BACKLIGHT_INSTALLED_COUNT); + variable_item_set_current_value_index(item, value_index); + variable_item_set_current_value_text(item, rgb_backlight_installed_text[value_index]); // We (humans) are numbering LEDs from left to right as 1..3, but hardware have another order from right to left 2..0 // led_1 color item = variable_item_list_add( app->variable_item_list_rgb, - "Led 1 Color", + "LED 1 Color", rgb_backlight_get_color_count(), led_2_color_changed, app); @@ -777,7 +766,7 @@ static NotificationAppSettings* alloc_settings(void) { // led_2 color item = variable_item_list_add( app->variable_item_list_rgb, - "Led 2 Color", + "LED 2 Color", rgb_backlight_get_color_count(), led_1_color_changed, app); @@ -790,7 +779,7 @@ static NotificationAppSettings* alloc_settings(void) { // led 3 color item = variable_item_list_add( app->variable_item_list_rgb, - "Led 3 Color", + "LED 3 Color", rgb_backlight_get_color_count(), led_0_color_changed, app); From 3fd059a45fbcd8a6f0c3adcdc7c2af7ddb544119 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Fri, 18 Apr 2025 01:06:10 +0300 Subject: [PATCH 259/268] upd changelog and docs --- CHANGELOG.md | 6 +++--- documentation/FAQ.md | 4 +--- documentation/HowToBuild.md | 14 ++------------ 3 files changed, 6 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f96c8929..e693c777a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,9 +7,9 @@ * SubGHz: **Came Atomo button hold simulation with full cycle** simulation (to allow proper pairing with receiver) * SubGHz: Add **Prastel (42bit static code)** support (OFW PR 4178 by @pmazzini) * Desktop: **Add support for Favorite App - Ok Long** (Warning! Old favourites apps list will be reset!) (PR #886 | by @DrEverr) -* Display: **LCD Color Inversion** (Settings - LCD and Notifications - LCD inversion.) (PR #887 #893 | by @Dmitry422) +* Display: **LCD Color Inversion** (Settings - LCD and Notifications - LCD Inversion) (PR #887 #893 | by @Dmitry422) * Display: **Night Shift Feature** (dimming backlight in selected time interval) (PR #885 | by @Dmitry422) -* Display: **Сombining RGB Backlight mod** (by @quen0n) and original backlight support **in one firmware** (+ Rainbow/Wave effect (based on @Willy-JL idea)) (PR #877 #881 #890 | by @Dmitry422) - (**To enable RGB Backlight support go into Notifications settings with Debug mode - ON**) +* Display: **Сombining RGB Backlight mod** (by @quen0n) and original backlight support **in one firmware** (+ Rainbow/Wave effect (based on @Willy-JL idea)) (PR #877 #881 #890 | by @Dmitry422) - (**To enable RGB Backlight support go into Notifications settings**) * NFC: Use default UL/UL-C pwd/key as default value for key input (PR #891 | by @mishamyte) * OFW: LFRFID - **EM4305 support** * OFW: **Universal IR signal selection** @@ -134,7 +134,7 @@ What build I should download and what this name means - `flipper-z-f7-update-(ve | `c` | | | | `e` | ✅ | ✅ | -**To enable RGB Backlight support go into Notifications settings with Debug mode = ON** +**To enable RGB Backlight support go into LCD & Notifications settings** ⚠️RGB backlight [hardware mod](https://github.com/quen0n/flipperzero-firmware-rgb#readme), works only on modded flippers! do not enable on non modded device! diff --git a/documentation/FAQ.md b/documentation/FAQ.md index cae0ba61c..7dea58c5d 100644 --- a/documentation/FAQ.md +++ b/documentation/FAQ.md @@ -18,9 +18,7 @@ Follow this link for [details](https://github.com/DarkFlippers/unleashed-firmwar You’ve enabled RGB backlight mod in settings made for custom RGB modded flippers.
Please, do not use that version if your flipper isn’t modded! -Disable in Settings -> Notifications -> RGB mod settings - -Make sure to have System -> Debug = ON before, otherwise first option (is mod installed) will not appear +Disable in Settings -> LCD & Notifications -> RGB mod settings If you have RGB backlight mod do the same but enable the mod instead diff --git a/documentation/HowToBuild.md b/documentation/HowToBuild.md index eec2e632a..b10c7493b 100644 --- a/documentation/HowToBuild.md +++ b/documentation/HowToBuild.md @@ -29,13 +29,8 @@ Check out `documentation/fbt.md` for details on building and flashing firmware. ./fbt COMPACT=1 DEBUG=0 launch_app APPSRC=applications_user/yourplugin ``` -### Compile everything for development -```sh -./fbt updater_package -``` - -### Compile everything for release + get updater package to update from microSD card +### Compile everything + get updater package to update from microSD card ```sh ./fbt COMPACT=1 DEBUG=0 updater_package @@ -49,13 +44,8 @@ Use `flipper-z-{target}-update-{suffix}.tgz` to flash your device. Check out `documentation/fbt.md` for details on building and flashing firmware. -### Compile everything for development -```powershell -./fbt.cmd updater_package -``` - -### Compile everything for release + get updater package to update from microSD card +### Compile everything + get updater package to update from microSD card ```powershell ./fbt.cmd COMPACT=1 DEBUG=0 updater_package From c420cbb1a57d72c5f8802c0212abfe250cd87123 Mon Sep 17 00:00:00 2001 From: WillyJL <49810075+Willy-JL@users.noreply.github.com> Date: Fri, 18 Apr 2025 05:52:15 +0100 Subject: [PATCH 260/268] Desktop: Fix freeze on boot if PIN set (#4193) --- applications/services/cli/cli_vcp.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/applications/services/cli/cli_vcp.c b/applications/services/cli/cli_vcp.c index cea4de248..1f9c77b08 100644 --- a/applications/services/cli/cli_vcp.c +++ b/applications/services/cli/cli_vcp.c @@ -164,7 +164,7 @@ static void cli_vcp_message_received(FuriEventLoopObject* object, void* context) switch(message.type) { case CliVcpMessageTypeEnable: - if(cli_vcp->is_enabled) return; + if(cli_vcp->is_enabled) break; FURI_LOG_D(TAG, "Enabling"); cli_vcp->is_enabled = true; @@ -175,7 +175,7 @@ static void cli_vcp_message_received(FuriEventLoopObject* object, void* context) break; case CliVcpMessageTypeDisable: - if(!cli_vcp->is_enabled) return; + if(!cli_vcp->is_enabled) break; FURI_LOG_D(TAG, "Disabling"); cli_vcp->is_enabled = false; From 5b911f5405f104ad3e3051aa70e2a50a5b2a6d16 Mon Sep 17 00:00:00 2001 From: Anna Antonenko Date: Fri, 18 Apr 2025 16:41:13 +0400 Subject: [PATCH 261/268] RC fixes (#4192) * cli_shell: fix FL-3983 * fix formatting and submodules * loader: fix FL-3986 * fix submodules --------- Co-authored-by: hedger --- applications/services/loader/loader.c | 1 + lib/toolbox/cli/shell/cli_shell.c | 62 +++++++++++++++------------ 2 files changed, 35 insertions(+), 28 deletions(-) diff --git a/applications/services/loader/loader.c b/applications/services/loader/loader.c index 136b3c20b..7b37f2510 100644 --- a/applications/services/loader/loader.c +++ b/applications/services/loader/loader.c @@ -694,6 +694,7 @@ static void loader_do_unlock(Loader* loader) { } static void loader_do_emit_queue_empty_event(Loader* loader) { + if(loader_do_is_locked(loader)) return; FURI_LOG_I(TAG, "Launch queue empty"); LoaderEvent event; event.type = LoaderEventTypeNoMoreAppsInQueue; diff --git a/lib/toolbox/cli/shell/cli_shell.c b/lib/toolbox/cli/shell/cli_shell.c index 8aa7c387a..7a4c7ec1f 100644 --- a/lib/toolbox/cli/shell/cli_shell.c +++ b/lib/toolbox/cli/shell/cli_shell.c @@ -31,10 +31,18 @@ CliShellKeyComboSet* component_key_combo_sets[] = { static_assert(CliShellComponentMAX == COUNT_OF(component_key_combo_sets)); typedef enum { - CliShellStorageEventMount, - CliShellStorageEventUnmount, + CliShellStorageEventMount = (1 << 0), + CliShellStorageEventUnmount = (1 << 1), } CliShellStorageEvent; +#define CliShellStorageEventAll (CliShellStorageEventMount | CliShellStorageEventUnmount) + +typedef struct { + Storage* storage; + FuriPubSubSubscription* subscription; + FuriEventFlag* event_flag; +} CliShellStorage; + struct CliShell { // Set and freed by external thread CliShellMotd motd; @@ -49,11 +57,7 @@ struct CliShell { FuriEventLoop* event_loop; CliAnsiParser* ansi_parser; FuriEventLoopTimer* ansi_parsing_timer; - - Storage* storage; - FuriPubSubSubscription* storage_subscription; - FuriMessageQueue* storage_event_queue; - + CliShellStorage storage; void* components[CliShellComponentMAX]; }; @@ -256,31 +260,32 @@ const char* cli_shell_get_prompt(CliShell* cli_shell) { // Event handlers // ============== +static void cli_shell_signal_storage_event(CliShell* cli_shell, CliShellStorageEvent event) { + furi_check(!(furi_event_flag_set(cli_shell->storage.event_flag, event) & FuriFlagError)); +} + static void cli_shell_storage_event(const void* message, void* context) { CliShell* cli_shell = context; const StorageEvent* event = message; if(event->type == StorageEventTypeCardMount) { - CliShellStorageEvent cli_event = CliShellStorageEventMount; - furi_check( - furi_message_queue_put(cli_shell->storage_event_queue, &cli_event, 0) == FuriStatusOk); + cli_shell_signal_storage_event(cli_shell, CliShellStorageEventMount); } else if(event->type == StorageEventTypeCardUnmount) { - CliShellStorageEvent cli_event = CliShellStorageEventUnmount; - furi_check( - furi_message_queue_put(cli_shell->storage_event_queue, &cli_event, 0) == FuriStatusOk); + cli_shell_signal_storage_event(cli_shell, CliShellStorageEventUnmount); } } static void cli_shell_storage_internal_event(FuriEventLoopObject* object, void* context) { CliShell* cli_shell = context; - FuriMessageQueue* queue = object; - CliShellStorageEvent event; - furi_check(furi_message_queue_get(queue, &event, 0) == FuriStatusOk); + FuriEventFlag* event_flag = object; + CliShellStorageEvent event = + furi_event_flag_wait(event_flag, FuriFlagWaitAll, FuriFlagWaitAny, 0); + furi_check(!(event & FuriFlagError)); - if(event == CliShellStorageEventMount) { - cli_registry_reload_external_commands(cli_shell->registry, cli_shell->ext_config); - } else if(event == CliShellStorageEventUnmount) { + if(event & CliShellStorageEventUnmount) { cli_registry_remove_external_commands(cli_shell->registry); + } else if(event & CliShellStorageEventMount) { + cli_registry_reload_external_commands(cli_shell->registry, cli_shell->ext_config); } else { furi_crash(); } @@ -377,25 +382,26 @@ static void cli_shell_init(CliShell* shell) { shell->ansi_parsing_timer = furi_event_loop_timer_alloc( shell->event_loop, cli_shell_timer_expired, FuriEventLoopTimerTypeOnce, shell); - shell->storage_event_queue = furi_message_queue_alloc(1, sizeof(CliShellStorageEvent)); - furi_event_loop_subscribe_message_queue( + shell->storage.event_flag = furi_event_flag_alloc(); + furi_event_loop_subscribe_event_flag( shell->event_loop, - shell->storage_event_queue, + shell->storage.event_flag, FuriEventLoopEventIn, cli_shell_storage_internal_event, shell); - shell->storage = furi_record_open(RECORD_STORAGE); - shell->storage_subscription = - furi_pubsub_subscribe(storage_get_pubsub(shell->storage), cli_shell_storage_event, shell); + shell->storage.storage = furi_record_open(RECORD_STORAGE); + shell->storage.subscription = furi_pubsub_subscribe( + storage_get_pubsub(shell->storage.storage), cli_shell_storage_event, shell); cli_shell_install_pipe(shell); } static void cli_shell_deinit(CliShell* shell) { - furi_pubsub_unsubscribe(storage_get_pubsub(shell->storage), shell->storage_subscription); + furi_pubsub_unsubscribe( + storage_get_pubsub(shell->storage.storage), shell->storage.subscription); furi_record_close(RECORD_STORAGE); - furi_event_loop_unsubscribe(shell->event_loop, shell->storage_event_queue); - furi_message_queue_free(shell->storage_event_queue); + furi_event_loop_unsubscribe(shell->event_loop, shell->storage.event_flag); + furi_event_flag_free(shell->storage.event_flag); cli_shell_completions_free(shell->components[CliShellComponentCompletions]); cli_shell_line_free(shell->components[CliShellComponentLine]); From a92400b9b628206052c5ef39e66f8f4a637a87db Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Fri, 18 Apr 2025 20:45:48 +0300 Subject: [PATCH 262/268] upd changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e693c777a..128d64ca0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,8 @@ * SubGHz: Various bugfixes and experimental options (rolling counter overflow) (by @xMasterX) * Anims: Disable winter anims * NFC: mfclassic poller fix early key reuse in dictionary attack state machine (by @noproto) +* OFW: RC fixes +* OFW: Desktop: Fix freeze on boot if PIN set * OFW PR 4189: USB-UART bridge fix (by @portasynthinca3) * OFW: FBT: Fix for Python 3.13 * OFW: sdk: bump API to force re-upload for the catalog From 19351655910a0e9695e16bcfcb7643e30cecb355 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Sat, 19 Apr 2025 17:31:16 +0300 Subject: [PATCH 263/268] move to the last position --- .../notification_settings/notification_settings_app.c | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/applications/settings/notification_settings/notification_settings_app.c b/applications/settings/notification_settings/notification_settings_app.c index 5ab15514b..cb1d1048d 100644 --- a/applications/settings/notification_settings/notification_settings_app.c +++ b/applications/settings/notification_settings/notification_settings_app.c @@ -533,7 +533,7 @@ void variable_item_list_enter_callback(void* context, uint32_t index) { UNUSED(context); NotificationAppSettings* app = context; - if(index == 0) { + if(index == 10) { view_dispatcher_switch_to_view(app->view_dispatcher, RGBViewId); } } @@ -625,10 +625,6 @@ static NotificationAppSettings* alloc_settings(void) { variable_item_list_set_enter_callback( app->variable_item_list, variable_item_list_enter_callback, app); - // Show RGB settings only when debug_mode or rgb_backlight_installed is active - item = variable_item_list_add(app->variable_item_list, "RGB Mod Settings", 0, NULL, app); - //--- RGB BACKLIGHT END --- - item = variable_item_list_add( app->variable_item_list, "LCD Contrast", CONTRAST_COUNT, contrast_changed, app); value_index = @@ -727,6 +723,9 @@ static NotificationAppSettings* alloc_settings(void) { variable_item_set_current_value_text(item, lcd_inversion_text[value_index]); //--- RGB BACKLIGHT --- + // Show RGB settings only when debug_mode or rgb_backlight_installed is active + item = variable_item_list_add(app->variable_item_list, "RGB Mod Settings", 0, NULL, app); + //--- RGB BACKLIGHT END --- app->variable_item_list_rgb = variable_item_list_alloc(); View* view_rgb = variable_item_list_get_view(app->variable_item_list_rgb); From c5d8bdf500ea4feb0cce0023892f5f0c43441780 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Mon, 21 Apr 2025 19:23:51 +0300 Subject: [PATCH 264/268] add feron protocol [ci skip] --- lib/subghz/protocols/feron.c | 350 ++++++++++++++++++++++++++ lib/subghz/protocols/feron.h | 109 ++++++++ lib/subghz/protocols/protocol_items.c | 1 + lib/subghz/protocols/protocol_items.h | 1 + 4 files changed, 461 insertions(+) create mode 100644 lib/subghz/protocols/feron.c create mode 100644 lib/subghz/protocols/feron.h diff --git a/lib/subghz/protocols/feron.c b/lib/subghz/protocols/feron.c new file mode 100644 index 000000000..1096f07a7 --- /dev/null +++ b/lib/subghz/protocols/feron.c @@ -0,0 +1,350 @@ +#include "feron.h" +#include "../blocks/const.h" +#include "../blocks/decoder.h" +#include "../blocks/encoder.h" +#include "../blocks/generic.h" +#include "../blocks/math.h" + +#define TAG "SubGhzProtocolFeron" + +static const SubGhzBlockConst subghz_protocol_feron_const = { + .te_short = 350, + .te_long = 750, + .te_delta = 150, + .min_count_bit_for_found = 32, +}; + +struct SubGhzProtocolDecoderFeron { + SubGhzProtocolDecoderBase base; + + SubGhzBlockDecoder decoder; + SubGhzBlockGeneric generic; +}; + +struct SubGhzProtocolEncoderFeron { + SubGhzProtocolEncoderBase base; + + SubGhzProtocolBlockEncoder encoder; + SubGhzBlockGeneric generic; +}; + +typedef enum { + FeronDecoderStepReset = 0, + FeronDecoderStepSaveDuration, + FeronDecoderStepCheckDuration, +} FeronDecoderStep; + +const SubGhzProtocolDecoder subghz_protocol_feron_decoder = { + .alloc = subghz_protocol_decoder_feron_alloc, + .free = subghz_protocol_decoder_feron_free, + + .feed = subghz_protocol_decoder_feron_feed, + .reset = subghz_protocol_decoder_feron_reset, + + .get_hash_data = subghz_protocol_decoder_feron_get_hash_data, + .serialize = subghz_protocol_decoder_feron_serialize, + .deserialize = subghz_protocol_decoder_feron_deserialize, + .get_string = subghz_protocol_decoder_feron_get_string, +}; + +const SubGhzProtocolEncoder subghz_protocol_feron_encoder = { + .alloc = subghz_protocol_encoder_feron_alloc, + .free = subghz_protocol_encoder_feron_free, + + .deserialize = subghz_protocol_encoder_feron_deserialize, + .stop = subghz_protocol_encoder_feron_stop, + .yield = subghz_protocol_encoder_feron_yield, +}; + +const SubGhzProtocol subghz_protocol_feron = { + .name = SUBGHZ_PROTOCOL_FERON_NAME, + .type = SubGhzProtocolTypeStatic, + .flag = SubGhzProtocolFlag_433 | SubGhzProtocolFlag_AM | SubGhzProtocolFlag_Decodable | + SubGhzProtocolFlag_Load | SubGhzProtocolFlag_Save | SubGhzProtocolFlag_Send, + + .decoder = &subghz_protocol_feron_decoder, + .encoder = &subghz_protocol_feron_encoder, +}; + +void* subghz_protocol_encoder_feron_alloc(SubGhzEnvironment* environment) { + UNUSED(environment); + SubGhzProtocolEncoderFeron* instance = malloc(sizeof(SubGhzProtocolEncoderFeron)); + + instance->base.protocol = &subghz_protocol_feron; + instance->generic.protocol_name = instance->base.protocol->name; + + instance->encoder.repeat = 10; + instance->encoder.size_upload = 256; + instance->encoder.upload = malloc(instance->encoder.size_upload * sizeof(LevelDuration)); + instance->encoder.is_running = false; + return instance; +} + +void subghz_protocol_encoder_feron_free(void* context) { + furi_assert(context); + SubGhzProtocolEncoderFeron* instance = context; + free(instance->encoder.upload); + free(instance); +} + +/** + * Generating an upload from data. + * @param instance Pointer to a SubGhzProtocolEncoderFeron instance + */ +static void subghz_protocol_encoder_feron_get_upload(SubGhzProtocolEncoderFeron* instance) { + furi_assert(instance); + size_t index = 0; + + // Send key and GAP + for(uint8_t i = instance->generic.data_count_bit; i > 0; i--) { + if(bit_read(instance->generic.data, i - 1)) { + // Send bit 1 + instance->encoder.upload[index++] = + level_duration_make(true, (uint32_t)subghz_protocol_feron_const.te_long); + if(i == 1) { + //Send 500/500 and gap if bit was last + instance->encoder.upload[index++] = level_duration_make( + false, (uint32_t)subghz_protocol_feron_const.te_short + 150); + instance->encoder.upload[index++] = level_duration_make( + true, (uint32_t)subghz_protocol_feron_const.te_short + 150); + // Gap + instance->encoder.upload[index++] = + level_duration_make(false, (uint32_t)subghz_protocol_feron_const.te_long * 6); + } else { + instance->encoder.upload[index++] = + level_duration_make(false, (uint32_t)subghz_protocol_feron_const.te_short); + } + } else { + // Send bit 0 + instance->encoder.upload[index++] = + level_duration_make(true, (uint32_t)subghz_protocol_feron_const.te_short); + if(i == 1) { + //Send 500/500 and gap if bit was last + instance->encoder.upload[index++] = level_duration_make( + false, (uint32_t)subghz_protocol_feron_const.te_short + 150); + instance->encoder.upload[index++] = level_duration_make( + true, (uint32_t)subghz_protocol_feron_const.te_short + 150); + // Gap + instance->encoder.upload[index++] = + level_duration_make(false, (uint32_t)subghz_protocol_feron_const.te_long * 6); + } else { + instance->encoder.upload[index++] = + level_duration_make(false, (uint32_t)subghz_protocol_feron_const.te_long); + } + } + } + + instance->encoder.size_upload = index; + return; +} + +/** + * Analysis of received data + * @param instance Pointer to a SubGhzBlockGeneric* instance + */ +static void subghz_protocol_feron_check_remote_controller(SubGhzBlockGeneric* instance) { + instance->serial = instance->data >> 16; +} + +SubGhzProtocolStatus + subghz_protocol_encoder_feron_deserialize(void* context, FlipperFormat* flipper_format) { + furi_assert(context); + SubGhzProtocolEncoderFeron* instance = context; + SubGhzProtocolStatus ret = SubGhzProtocolStatusError; + do { + ret = subghz_block_generic_deserialize_check_count_bit( + &instance->generic, + flipper_format, + subghz_protocol_feron_const.min_count_bit_for_found); + if(ret != SubGhzProtocolStatusOk) { + break; + } + //optional parameter parameter + flipper_format_read_uint32( + flipper_format, "Repeat", (uint32_t*)&instance->encoder.repeat, 1); + + subghz_protocol_feron_check_remote_controller(&instance->generic); + subghz_protocol_encoder_feron_get_upload(instance); + instance->encoder.is_running = true; + } while(false); + + return ret; +} + +void subghz_protocol_encoder_feron_stop(void* context) { + SubGhzProtocolEncoderFeron* instance = context; + instance->encoder.is_running = false; +} + +LevelDuration subghz_protocol_encoder_feron_yield(void* context) { + SubGhzProtocolEncoderFeron* instance = context; + + if(instance->encoder.repeat == 0 || !instance->encoder.is_running) { + instance->encoder.is_running = false; + return level_duration_reset(); + } + + LevelDuration ret = instance->encoder.upload[instance->encoder.front]; + + if(++instance->encoder.front == instance->encoder.size_upload) { + instance->encoder.repeat--; + instance->encoder.front = 0; + } + + return ret; +} + +void* subghz_protocol_decoder_feron_alloc(SubGhzEnvironment* environment) { + UNUSED(environment); + SubGhzProtocolDecoderFeron* instance = malloc(sizeof(SubGhzProtocolDecoderFeron)); + instance->base.protocol = &subghz_protocol_feron; + instance->generic.protocol_name = instance->base.protocol->name; + return instance; +} + +void subghz_protocol_decoder_feron_free(void* context) { + furi_assert(context); + SubGhzProtocolDecoderFeron* instance = context; + free(instance); +} + +void subghz_protocol_decoder_feron_reset(void* context) { + furi_assert(context); + SubGhzProtocolDecoderFeron* instance = context; + instance->decoder.parser_step = FeronDecoderStepReset; +} + +void subghz_protocol_decoder_feron_feed(void* context, bool level, volatile uint32_t duration) { + furi_assert(context); + SubGhzProtocolDecoderFeron* instance = context; + + // Feron Decoder + // 2025.04 - @xMasterX (MMX) + + // Key samples + /* + 0110001100111000 1000010101111010 - ON + 0110001100111000 1000010001111011 - OFF + + 0110001100111000 1000011001111001 - brightness up + 0110001100111000 1000011101111000 - brightness down + + 0110001100111000 1000001001111101 - scroll mode command + + ------------------------------------------ + 0110001100111000 0111000010001111 - R + 0110001100111000 0001101011100101 - B + 0110001100111000 0100000010111111 - G + */ + + switch(instance->decoder.parser_step) { + case FeronDecoderStepReset: + if((!level) && (DURATION_DIFF(duration, subghz_protocol_feron_const.te_long * 6) < + subghz_protocol_feron_const.te_delta * 4)) { + //Found GAP + instance->decoder.decode_data = 0; + instance->decoder.decode_count_bit = 0; + instance->decoder.parser_step = FeronDecoderStepSaveDuration; + } + break; + case FeronDecoderStepSaveDuration: + if(level) { + instance->decoder.te_last = duration; + instance->decoder.parser_step = FeronDecoderStepCheckDuration; + } else { + instance->decoder.parser_step = FeronDecoderStepReset; + } + break; + case FeronDecoderStepCheckDuration: + if(!level) { + // Bit 0 is short and long timing = 350us HIGH (te_last) and 750us LOW + if((DURATION_DIFF(instance->decoder.te_last, subghz_protocol_feron_const.te_short) < + subghz_protocol_feron_const.te_delta) && + (DURATION_DIFF(duration, subghz_protocol_feron_const.te_long) < + subghz_protocol_feron_const.te_delta)) { + subghz_protocol_blocks_add_bit(&instance->decoder, 0); + instance->decoder.parser_step = FeronDecoderStepSaveDuration; + // Bit 1 is long and short timing = 750us HIGH (te_last) and 350us LOW + } else if( + (DURATION_DIFF(instance->decoder.te_last, subghz_protocol_feron_const.te_long) < + subghz_protocol_feron_const.te_delta) && + (DURATION_DIFF(duration, subghz_protocol_feron_const.te_short) < + subghz_protocol_feron_const.te_delta)) { + subghz_protocol_blocks_add_bit(&instance->decoder, 1); + instance->decoder.parser_step = FeronDecoderStepSaveDuration; + } else if( + // End of the key 500Low(we are here)/500High us + DURATION_DIFF( + duration, (uint16_t)(subghz_protocol_feron_const.te_short + (uint16_t)150)) < + subghz_protocol_feron_const.te_delta) { + if((DURATION_DIFF(instance->decoder.te_last, subghz_protocol_feron_const.te_short) < + subghz_protocol_feron_const.te_delta)) { + subghz_protocol_blocks_add_bit(&instance->decoder, 0); + } + if((DURATION_DIFF(instance->decoder.te_last, subghz_protocol_feron_const.te_long) < + subghz_protocol_feron_const.te_delta)) { + subghz_protocol_blocks_add_bit(&instance->decoder, 1); + } + // If got 32 bits key reading is finished + if(instance->decoder.decode_count_bit == + subghz_protocol_feron_const.min_count_bit_for_found) { + instance->generic.data = instance->decoder.decode_data; + instance->generic.data_count_bit = instance->decoder.decode_count_bit; + if(instance->base.callback) + instance->base.callback(&instance->base, instance->base.context); + } + instance->decoder.decode_data = 0; + instance->decoder.decode_count_bit = 0; + instance->decoder.parser_step = FeronDecoderStepReset; + } else { + instance->decoder.parser_step = FeronDecoderStepReset; + } + } else { + instance->decoder.parser_step = FeronDecoderStepReset; + } + break; + } +} + +uint8_t subghz_protocol_decoder_feron_get_hash_data(void* context) { + furi_assert(context); + SubGhzProtocolDecoderFeron* instance = context; + return subghz_protocol_blocks_get_hash_data( + &instance->decoder, (instance->decoder.decode_count_bit / 8) + 1); +} + +SubGhzProtocolStatus subghz_protocol_decoder_feron_serialize( + void* context, + FlipperFormat* flipper_format, + SubGhzRadioPreset* preset) { + furi_assert(context); + SubGhzProtocolDecoderFeron* instance = context; + return subghz_block_generic_serialize(&instance->generic, flipper_format, preset); +} + +SubGhzProtocolStatus + subghz_protocol_decoder_feron_deserialize(void* context, FlipperFormat* flipper_format) { + furi_assert(context); + SubGhzProtocolDecoderFeron* instance = context; + return subghz_block_generic_deserialize_check_count_bit( + &instance->generic, flipper_format, subghz_protocol_feron_const.min_count_bit_for_found); +} + +void subghz_protocol_decoder_feron_get_string(void* context, FuriString* output) { + furi_assert(context); + SubGhzProtocolDecoderFeron* instance = context; + + subghz_protocol_feron_check_remote_controller(&instance->generic); + + furi_string_cat_printf( + output, + "%s %db\r\n" + "Key: 0x%08lX\r\n" + "Serial: 0x%04lX\r\n" + "Command: 0x%04lX\r\n", + instance->generic.protocol_name, + instance->generic.data_count_bit, + (uint32_t)(instance->generic.data & 0xFFFFFFFF), + instance->generic.serial, + (uint32_t)(instance->generic.data & 0xFFFF)); +} diff --git a/lib/subghz/protocols/feron.h b/lib/subghz/protocols/feron.h new file mode 100644 index 000000000..97f0eb6fe --- /dev/null +++ b/lib/subghz/protocols/feron.h @@ -0,0 +1,109 @@ +#pragma once + +#include "base.h" + +#define SUBGHZ_PROTOCOL_FERON_NAME "Feron" + +typedef struct SubGhzProtocolDecoderFeron SubGhzProtocolDecoderFeron; +typedef struct SubGhzProtocolEncoderFeron SubGhzProtocolEncoderFeron; + +extern const SubGhzProtocolDecoder subghz_protocol_feron_decoder; +extern const SubGhzProtocolEncoder subghz_protocol_feron_encoder; +extern const SubGhzProtocol subghz_protocol_feron; + +/** + * Allocate SubGhzProtocolEncoderFeron. + * @param environment Pointer to a SubGhzEnvironment instance + * @return SubGhzProtocolEncoderFeron* pointer to a SubGhzProtocolEncoderFeron instance + */ +void* subghz_protocol_encoder_feron_alloc(SubGhzEnvironment* environment); + +/** + * Free SubGhzProtocolEncoderFeron. + * @param context Pointer to a SubGhzProtocolEncoderFeron instance + */ +void subghz_protocol_encoder_feron_free(void* context); + +/** + * Deserialize and generating an upload to send. + * @param context Pointer to a SubGhzProtocolEncoderFeron instance + * @param flipper_format Pointer to a FlipperFormat instance + * @return status + */ +SubGhzProtocolStatus + subghz_protocol_encoder_feron_deserialize(void* context, FlipperFormat* flipper_format); + +/** + * Forced transmission stop. + * @param context Pointer to a SubGhzProtocolEncoderFeron instance + */ +void subghz_protocol_encoder_feron_stop(void* context); + +/** + * Getting the level and duration of the upload to be loaded into DMA. + * @param context Pointer to a SubGhzProtocolEncoderFeron instance + * @return LevelDuration + */ +LevelDuration subghz_protocol_encoder_feron_yield(void* context); + +/** + * Allocate SubGhzProtocolDecoderFeron. + * @param environment Pointer to a SubGhzEnvironment instance + * @return SubGhzProtocolDecoderFeron* pointer to a SubGhzProtocolDecoderFeron instance + */ +void* subghz_protocol_decoder_feron_alloc(SubGhzEnvironment* environment); + +/** + * Free SubGhzProtocolDecoderFeron. + * @param context Pointer to a SubGhzProtocolDecoderFeron instance + */ +void subghz_protocol_decoder_feron_free(void* context); + +/** + * Reset decoder SubGhzProtocolDecoderFeron. + * @param context Pointer to a SubGhzProtocolDecoderFeron instance + */ +void subghz_protocol_decoder_feron_reset(void* context); + +/** + * Parse a raw sequence of levels and durations received from the air. + * @param context Pointer to a SubGhzProtocolDecoderFeron instance + * @param level Signal level true-high false-low + * @param duration Duration of this level in, us + */ +void subghz_protocol_decoder_feron_feed(void* context, bool level, uint32_t duration); + +/** + * Getting the hash sum of the last randomly received parcel. + * @param context Pointer to a SubGhzProtocolDecoderFeron instance + * @return hash Hash sum + */ +uint8_t subghz_protocol_decoder_feron_get_hash_data(void* context); + +/** + * Serialize data SubGhzProtocolDecoderFeron. + * @param context Pointer to a SubGhzProtocolDecoderFeron instance + * @param flipper_format Pointer to a FlipperFormat instance + * @param preset The modulation on which the signal was received, SubGhzRadioPreset + * @return status + */ +SubGhzProtocolStatus subghz_protocol_decoder_feron_serialize( + void* context, + FlipperFormat* flipper_format, + SubGhzRadioPreset* preset); + +/** + * Deserialize data SubGhzProtocolDecoderFeron. + * @param context Pointer to a SubGhzProtocolDecoderFeron instance + * @param flipper_format Pointer to a FlipperFormat instance + * @return status + */ +SubGhzProtocolStatus + subghz_protocol_decoder_feron_deserialize(void* context, FlipperFormat* flipper_format); + +/** + * Getting a textual representation of the received data. + * @param context Pointer to a SubGhzProtocolDecoderFeron instance + * @param output Resulting text + */ +void subghz_protocol_decoder_feron_get_string(void* context, FuriString* output); diff --git a/lib/subghz/protocols/protocol_items.c b/lib/subghz/protocols/protocol_items.c index ea3e88bf0..c73923c7a 100644 --- a/lib/subghz/protocols/protocol_items.c +++ b/lib/subghz/protocols/protocol_items.c @@ -52,6 +52,7 @@ const SubGhzProtocol* const subghz_protocol_registry_items[] = { &subghz_protocol_hollarm, &subghz_protocol_hay21, &subghz_protocol_revers_rb2, + &subghz_protocol_feron, }; const SubGhzProtocolRegistry subghz_protocol_registry = { diff --git a/lib/subghz/protocols/protocol_items.h b/lib/subghz/protocols/protocol_items.h index d06882466..6165d748a 100644 --- a/lib/subghz/protocols/protocol_items.h +++ b/lib/subghz/protocols/protocol_items.h @@ -53,3 +53,4 @@ #include "hollarm.h" #include "hay21.h" #include "revers_rb2.h" +#include "feron.h" From e4e8f18a9427ae29b001518301980be3d9bbaa84 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Mon, 21 Apr 2025 20:03:02 +0300 Subject: [PATCH 265/268] upd changelog --- CHANGELOG.md | 1 + ReadMe.md | 1 + 2 files changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 128d64ca0..31c20e193 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## Main changes - Current API: 86.0 **WARNING! After install of this version your Desktop (fav apps) and LCD & Notifications settings will be reset to default values, please configure them again after this update!** (this is required due to big updates on that parts and config struct changes) +* SubGHz: Add **Feron** protocol (static 32 bit) **full support** (by @xMasterX) * SubGHz: Add **Revers RB2 / RB2M Protocol** (static 64 bit) **full support** with add manually (by @xMasterX) * SubGHz: **Fix Hollarm protocol with more verification** * SubGHz: **Fix GangQi protocol** (by @DoberBit and @mishamyte (who spent 2 weeks on this)) diff --git a/ReadMe.md b/ReadMe.md index c40fb71c4..6ee86e881 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -165,6 +165,7 @@ Thanks to Official team (to their SubGHz Developer, Skorp) for implementing supp Decoders/Encoders or emulation (+ programming mode) support made by @xMasterX
+- Feron (static 32 bit) - ReversRB2 / RB2M (static 64 bit) with add manually support - Marantec24 (static 24 bit) with add manually support - GangQi (static 34 bit) with button parsing and add manually support (thanks to @mishamyte for captures and testing, thanks @Skorpionm for help) From 9f2b202b7dd7b87e2b20111d6990f46b12ab4306 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Tue, 22 Apr 2025 01:22:02 +0300 Subject: [PATCH 266/268] fix nightshift settings lock bug --- .../settings/notification_settings/notification_settings_app.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/applications/settings/notification_settings/notification_settings_app.c b/applications/settings/notification_settings/notification_settings_app.c index cb1d1048d..3713d7e26 100644 --- a/applications/settings/notification_settings/notification_settings_app.c +++ b/applications/settings/notification_settings/notification_settings_app.c @@ -559,7 +559,7 @@ static void night_shift_changed(VariableItem* item) { // force demo night_shift brightness ot rgb backlight and stock backlight notification_message(app->notification, &sequence_display_backlight_on); - int slide = 1; + int slide = 0; for(int i = 4 + slide; i < (6 + slide); i++) { VariableItem* t_item = variable_item_list_get(app->variable_item_list, i); From 925b95007aa62174f688a47c7e91d28ad3d5ed49 Mon Sep 17 00:00:00 2001 From: Dmitry422 Date: Tue, 22 Apr 2025 10:23:05 +0700 Subject: [PATCH 267/268] Code cleanup --- .../notification_settings_app.c | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/applications/settings/notification_settings/notification_settings_app.c b/applications/settings/notification_settings/notification_settings_app.c index 3713d7e26..be7af4c42 100644 --- a/applications/settings/notification_settings/notification_settings_app.c +++ b/applications/settings/notification_settings/notification_settings_app.c @@ -393,9 +393,7 @@ static void rgb_backlight_installed_changed(VariableItem* item) { } // Lock/Unlock all rgb settings depent from rgb_backlight_installed switch - int slide = 1; - - for(int i = slide; i < (slide + 8); i++) { + for(int i = 1; i < 9; i++) { VariableItem* t_item = variable_item_list_get(app->variable_item_list_rgb, i); if(index == 0) { variable_item_set_locked(t_item, true, "RGB\nOFF!"); @@ -528,7 +526,7 @@ static void rgb_backlight_rainbow_wide_changed(VariableItem* item) { notification_message_save_settings(app->notification); } -// open settings.rgb_view if user press OK on first (index=0) menu string and (debug mode or rgb_backlight_installed is true) +// open settings.rgb_view if user press OK on last (index=10) menu string void variable_item_list_enter_callback(void* context, uint32_t index) { UNUSED(context); NotificationAppSettings* app = context; @@ -556,12 +554,10 @@ static void night_shift_changed(VariableItem* item) { app->notification->current_night_shift = night_shift_value[index]; app->notification->current_night_shift = night_shift_value[index]; - // force demo night_shift brightness ot rgb backlight and stock backlight + // force demo night_shift brightness to rgb backlight and stock backlight notification_message(app->notification, &sequence_display_backlight_on); - - int slide = 0; - - for(int i = 4 + slide; i < (6 + slide); i++) { + + for(int i = 4; i < 6; i++) { VariableItem* t_item = variable_item_list_get(app->variable_item_list, i); if(index == 0) { variable_item_set_locked(t_item, true, "Night Shift\nOFF!"); @@ -723,18 +719,15 @@ static NotificationAppSettings* alloc_settings(void) { variable_item_set_current_value_text(item, lcd_inversion_text[value_index]); //--- RGB BACKLIGHT --- - // Show RGB settings only when debug_mode or rgb_backlight_installed is active item = variable_item_list_add(app->variable_item_list, "RGB Mod Settings", 0, NULL, app); //--- RGB BACKLIGHT END --- app->variable_item_list_rgb = variable_item_list_alloc(); View* view_rgb = variable_item_list_get_view(app->variable_item_list_rgb); - // set callback for exit from settings.rgb_menu + // set callback for exit from rgb settings menu view_set_previous_callback(view_rgb, notification_app_rgb_settings_exit); - // Show rgb_backlight_installed swith only in Debug mode - item = variable_item_list_add( app->variable_item_list_rgb, "RGB backlight installed", From 384cd47b1be10a57e6ba9d6894470b987dc37a1b Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Wed, 23 Apr 2025 03:16:53 +0300 Subject: [PATCH 268/268] upd changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 31c20e193..0dd43e977 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ * SubGHz: Add **Prastel (42bit static code)** support (OFW PR 4178 by @pmazzini) * Desktop: **Add support for Favorite App - Ok Long** (Warning! Old favourites apps list will be reset!) (PR #886 | by @DrEverr) * Display: **LCD Color Inversion** (Settings - LCD and Notifications - LCD Inversion) (PR #887 #893 | by @Dmitry422) -* Display: **Night Shift Feature** (dimming backlight in selected time interval) (PR #885 | by @Dmitry422) +* Display: **Night Shift Feature** (dimming backlight in selected time interval) (PR #885 #896 | by @Dmitry422) * Display: **Сombining RGB Backlight mod** (by @quen0n) and original backlight support **in one firmware** (+ Rainbow/Wave effect (based on @Willy-JL idea)) (PR #877 #881 #890 | by @Dmitry422) - (**To enable RGB Backlight support go into Notifications settings**) * NFC: Use default UL/UL-C pwd/key as default value for key input (PR #891 | by @mishamyte) * OFW: LFRFID - **EM4305 support**

)D9xLa-N2-BdzSWn{ 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 z%0DBzX1`CLpkgP{T&~fXy&^q&wuqqXYL1&H{lg>zAL;d3FEt9*Q?gcWCQRA6u!07b zzAaotzif3os{bs_dTQJ-SCC*v`f~?JhY;YXnRe%w?mRm^kehV^NqYcI5?$AMCw#T-4vzKMLwsxoIz?JSz4JZKa}MWzpWk^7_w_pWzV09Tk1w6iuC>>G zueJAHd%eLt@OOX244xBMs5Fqc*je1v>JlQr))h^fN4T+OGvh-W+~yRz_}1rXE$AB` zZYiykEtg1&7O^@XAhzGtt>rMda5=7%=y%>54j~`S5z_wpwE6QrDYdBBs@Uc~(CpL~ zQTw2xZQfM0b@8kEFF;_xE4f(GLrEGQ*@PNW7G%$7<%F{77-;Dp>b&ODfbF9@yo_No;C(BJWgvLH#7#_E6d+e zSZ#J>5^fN^=LgF@*D@@>3DTa5c%!jZ)Pzo`*{9IzdIz7R!a9gl2U;nA!VqP5=eGag zK9X!ZbGIxG3yDmU>2B%<3(qy}YGa6R-FAK8a36pBVQQF(_K5Zxi8hEYU!6hAB4(eD zm&4KSxNBxssn;^kSi#TAN;3AFpTelHvRB)lbyE85Hk#)0qCs zBJmuq$)2^uCdl8G2kwvNb&RXuLZK2n1y+_I1R9E8!B)EK43BR0FZP}&mi)Z+>~xcK zSfzz9^Y3B6F*G<3?_~4+uT24~v_+a>t$Ha3a`b8RRk<}Sxv@*?3TNt0b(-RHf z%`i_o*o-jDvhqhkA1Z#_!_mE6!wVoN;pjk7{Lm40-%60MGI@|Pd9-FhfB&_-DjSp_ zkc;!U`LINy$X=)kl_{1ucUNx&khO)%zdJ6t^YM|EczL_@0fIOGUWC?Y9@C&XYphet z$hM&R%lfiH7h-9jM_R5NzJa!sE}V8iHRc}6(yzI!F(E*(Zk3g8zbT1N&g>qVj!2E! zy18ROn8#g0(rm6V7sw^oED}Qf{1?{BjL-zxlYZdCahv5HWF*@Ys=9*m$x%F<7UFh6%aENm3DD@^?foCcb)h2)w<+<#IaHl$757PS`qOcyF2C&aSb zcfvihEVgOS8w;tGOzYzeqctkV$51S@prD+$XUtJ7IjiNwg4GQ_;_)ENS zw9(USb;EFF-qeV+gO(we$g~b8lKW|fL!%_T_Ru0Ft@oHnx*6-42+u;mVUhfoKN?VgF)rI&v}rzB~rE!3v-JP7iLI z$+MYt$JehaIvjBba4W-~8R^trlCL|%A?+V!*mg>fdrFm`7AII4)`8Xk^vvd~C7c~) zk|i`2@~R~&XLwh>d7-1o-&*yqu)puBHtBHJF*z5_dI;n_Hw1j8$6UVcV7QKR_x4h- z`(;mte};a4)gR){1!Hvmg_YeN@fBN63b6Pib0|*`-~2(5*8WlR%j%dV20yKyv%U_V zR7P6ru#{k{pbI65jmo9?M=IKR?37vy*xO<$R%c4-Vr-dEAQ{j5K*4OGnKBs1?mOfy zQjLT`AE6 z-hHdJSNq(TOLY=>$-o`>bU!|fIfb?{BDZf}-K2o`r7>w0R6vkd{$tBB86i)a?F8CZ zgHpj2O6vd;d|1obfv-*}pkbTSX>x~nKvpr3nl^;1n+$h9b)fG;OIhP!t@2Pf8pl{x zbrPwzojUZWSz4Q_Z(g6%a{JqEiC!GWJ&_TCm*z}L8Kzg5GR?lyxA(eX)4(7kQtUx& zCo~%<|IG%v`?yq^4E2L57TTuXK}|TCM`$am^v343@Xq*3#Z24Bk6-i^*;Bl<7kB(M z%$AJ;dg^=aATEO)!m1@tD%^zkEvY3a>`_nsnVIKircJBchLl zup!~+xC7ol07qI08$aYAR_YVGgVwS81Yi{qJu9#_PIFt?P_HNIM^&uJhnv$jws~@7 zp;~iJwB}wk1Noe*O+-{3MH8dS$Ae{@00~;uy1f@ zEI`GkJzFKA$3|yx(>dzl_X93%Y$d5q4WTd7%T7@%WU9ZgSPY`*6w$S)qWdhZWwiXa zjz~Jg3CLk0j&k~a_kg3xeZW=aC1cIM?PCQ=CDKDvH>^uA0x43w4$|fP|j4GOZ zwNL?<$lT+*HUs_3R5iTQZZWlHw;5Gjbjd7hCF{5*QowoqVvr``URwLobKH95SsN9i z_VY7@MziPZNb?7jM_eNm*B%NRL`Rd)XNs-F*B`MDG=B)Vh>`keYC zdxM$gNGDl#u=ZuPsX06{ue-=P(7m?q&QKha7p)+dS%h~q#{WMa)uc(}V$4e)Ov2rn z4cf`?Tx1lJ)#ZBCv1L~Z%)?6_Ffa_)>8Q^c*Z)qQhm|5$Mnt0VEqK&*^HB9b&r&8U ze;z_-0-+CQsoLF52xN|SKkw*U9dM1vrB7FQ+{fztt@x!`a>6T4dl)fiJ4?@aX$|)- z<`F&Z9n~g=pDE=wWgHw{O<-;wf$1XLCgI#7HPISrSl$>f$}YUQfa-_`Lz==>wC@VL zqo)3acIh*W(}N#Y$5tOxZNTrQy5k9IqW*xx?VH-Nxc7RfxMwm4fHJsuCfg?>w(Zy@ zoKU1BiT0eau6=xtv%8h{-GJn@m%^G(IC5m(BPZZ-NTa>bioI#dtK>&%TqE7g4YN}2 z>17s5Di;=j4bs#X7rq73)GvP=JW`FKXk9xeIjG94o_fLJ=bRq&=1IsetWM;Qqz*i6 z!~fGwab!S!U2SdSOeELixHwoXvLW&{I-yJ=-zrM$vL(Kl*))H2MPMywO-WN-bUI75 zIpH$|XzAObP}6EUAwbw0EzUdXE7hMT#MR~kh;(v2EEUjLH#UgNWliv%+0E~SQTIp7 zg&2^T=kq}YL_R+hFJLwW4Ek%j#F79<$0dwNbWYzW@2l#v!SL~5z1 z+>%8$yM4c17gIu;^(>1^-FwtZVc5c2(Az_OX(%Eg|L&(X)kg^vsfjFP$FsEv4xPa| zGTNl7xU!`E;7XE5&B??rEW~d+X2KsGm4odfU?~a=CNu|%$CNcLxU4|-+4=QU#jgKH6zo+ zrF@=j{EP^&R(6`%6J$%Y!E!O^K4tuT#<=JnP1>;)onlpx0=^%~x)7TwV@fY<>v=%G zXDnx!d{*qU*%})nJDXWXTK?|lKc?8{Y@M`7c}j;%=wce;5?Rj)kv$!?W5L8^zA}qT z7$#>IvNGTps6~#t6W;9S^OkC-U4`*TzexXyMy z*P6cP-yfxW+ij!&8Av$?{)1)N=DB}Foy){TA}4GpUW3AvLQ}~uN1@M?Dg8yazF}1< zm_a75N10eWPME^71_Zgg0I>Op{wc6Qpse<$n3D>0$MM#y>k6KiXW>3iHr<<&BxsOno*%I zCiosqc|CP$Q#({r&5h0U@4<;3(`692V6gM`b&-)GKY$JM$#(FrQBGK`_LOFGuLxz{ zu6^A0MEeLzHJAdomQ;Ob&bbMEMb1E3F(%(*_q0rO4xlD^md6QlA2sN}Tocx`==LZ% z5d-+@r@~Dx5aPAZSuUrPH6*z+cE72k3@aT>k zhR(0S9qEn=A5+mPYzF6}m?zfdf)8fXe7+L(Y)tHT+44IvkB0bWz+Ji^3Wa%jCwN?- zDJyUu+1+KDn8xi_AV#5Y2agTEBtrPWGkCWg%7IF(F7|^CO~pzWk0+yjP;`@O2C(rV z{VsrwY#e#6&8V$ru=Hj6l|D)c|0Uolh94u(Sbo02-n`mP;(63KJz>YUDCU$9vU^)% z+_8y!Kb0nAlx&rCH%3?sa}VY`;@>mU6^cmRk|SI$QZ`sq*jDU`Iwc)b+^QB08Dq&R z1$HPtzSNNwP~Yw4Hj*q_Wai?aaT+I?-`%DfC*G$*M{1)^ zI^-3aS5ru+AQpiyjznbfv_{cK;9H}&(1qTitDo%eq2nR5b@Pd|-r&H|b($hV{mv<_ zZIVocs9E0d&g=5p{DBO|&Jq?V*=ngiGFknbow?s3+nCmb`wn3v^830O_2E#gTOOAq z=R_2%V|jSt?6w`yS*8n3jXF!&%WauiX~mTd+-I3- z=SBoHdj9Y$VP$WzHYXpASv>602KU;@78c2b-Dl3JBW42_#k!X-tU3C*KR=ygf4Grg zvU>N24DA z!~{>B2Wdq{+fDNm{7G@)@lkm@+Jtfz)A#yFKnx$R4WzajG%=~9c3O#dzs+6*CDS%6 zd?c)lNRm$J3DXDcQg8b!8A6ZZjP&pLcaYGFqWZu6!cy}mPHO94AC92{*Wr1`VYo94KP zEOUYCm@efxXq=sh)m|n}mj$2>|0DS2D8=I)p$tX^aBp^A{7u-CwL^{da>v4+LFopw zROJqqpQ1}EaoMAMW)UfY4QiB2Us%2*ZCkz6n1$7%w$hO`k9TRa6BZ}Rb?27(hvjQJUmu!0Ev8FX!L-x*3NYC%!PyJ-dF7VIf4=3 zLGH~WOfEhv708;vD>WDB2>wegiBSExr$VfSHB(B0QG&WJ55VJhX^l)*t%`C~K!vq3 zVzrI41G&h7RG0e`5>~pByF>g$y zX7Oa^Du#u-k=8*!3~(`ARqWp+zauA z*@~_SXer2zLc>)7tnR~6oH}5)-k#47TObZys11ozG9;>W4X(v!PKUQ+mAmK zo!Mpga#k6I_v(~;F_lZa45Bl=ml@?*oP&mYqL&bY(EA{k7Ly*_jGLxX-rpsqmX(5P zl#d=yM!#&I7cRIEZUu{6VhQD12@W4o$4j|8C#i+n+USK6!P!!l@Kp8{m3vufsmrSo z)-~Qqi98pw#ys>dU!#vDS(}~v!R>YtNHLrk>rd`e)KZQ&FBnxV`N5k>9 zU;1bwZO2`6-z3>;Xu7fhK!Bzr#cE^FZ^?Xx!D1StUyE5w#~=RL;XYob3i_F0*7=)zY(2R(anp@KXxr zJ4d8lcxrc+(o~AGBkag^H3i_-EDRD9&+ZM(Hhl$5YJYlnlf(k!QCbP;eClkhYxKDF z0g3hkvB34oya|{kgsmbYki$CJtIrzuDEQN>r_1Dh3zd~wASPziOH3Aw!Z!=IFBPK7 zDo3+pW&75L80>fD#g`4hHK9B=* zauzklPh{_Vhi0utsoac0BqZZqVIy8dv50-Gb-YQq4d0|L>Vc(zz;wucP;Tw^$d75^ zP3Pfp_p0~0RQ6nSg20((!|!$J74|D7j--cw2nQB%1u!!Uv}7kXk)Unrz^bFUvGEQd zFNyV3tunAt#r6lKxf`+lwNFRPpM|?^xK~ML2=(n`vo^ylR3skkf|@7f26E{3(*`Z9 z$GkUJdWS%K7UVmI+ZXE$&?1FpA&NV%9#Us5r1pMOOLLFK2G|M;_Bt9@&|#LfykR6B zB-5vjwDGq7$ZNXz7kcRgI$bgkD0n^UWV(eH%0(084LW~NRKL4nrFFnaSeu`cUEo>u z@Z|>&XTB(?WB%>7xNl2g_f(kugwB7u;l@vBEfRu5l@Qy()fiVLRSlI zbP=i-zBt#VXIsQ#tgWdgv!B`9!63U~pc#-4o=_N;5BNqc#dgFO{SgnJ+s?{&=j-Dx zV8Qo^o=h{PrZ{ALZ1(J_p;WF=^6f7aG;;c#0@<3KVv_J0ZrSV(?xUi@+!WdDOKOrq zJI?|UN-YUzaAJl2Dl2l(359tV;>sda*Hb-ZLFw#m!4MUfF(fdN8xz_q<7cs8F*ml}P+?pIcTs{f-~xqKNNeNdG=6Q#9*qo4piS*&`~N z?w|x3(v0Sl^}vALoJu?ic2jw|HRJDlB4Dpb7Z)*+CjOLFtM0L7c7#%+Lw>qrg2kaJ z$`-Rec~08!x7yT4C`;JD%@3P+VICRu<_T-+h8nuf>l>tlA9s^N1_9Q9yEZW%vXaqycQ?}f!ovCyy7}-neM%pM99!dKF#**AqqLo*)HNWx5nuzHr}WO$XZn^MF%;pvYy_8UO&8 zKY@No^Km}Ebgvmr5<9smC-ms(P~ltT-M4dl$?*Kql(c280-h~Yls6&@5zVDb^Kq}? z^{!`CAX~J@lE1JxV}^3_hETDoS>}`@#oewgMTwUHw3lEB;?erL=4$K*Y^>K#MDM)~+%q9D$a@6o!m3{NTp03eCjsUB7S>xkee zj8;$s6=Fmg^**g#pqfSWOeqNr)?wkbpDG*Rsq%<4yep)#1nQ-ea=sfUSi>Su2seczfh_K6n1^a9ysrzK zi(;}^X|o2s?kER~LDzK;MiJj#^NH#TxA!LzVyV|rIl1s>RQp`9EUFI- zhwjBdSb?|*jhKW(a&1?BY~t`}a{Kef$rEKgRVzGweML{8K{#JR?AB$Q?$tDqT&NY1{kF$jU?^qDi!WS+$?s!$YxoMbn65he7f|JUkMu4g z<4{3t{q!zmrKP>!Ppz;IKW=d`^DQhghkaIn7v?*C(2MWYrCvf+G4|9E9=n17Q*1cb zS=V)38k9;($OHvf5GG8eHfC9kdyJ=;34*a6o|E>uOIly3!aE)PX8Fw6-(Oj_tD66! zvf*Uo+Q!C%G$BRBy+|{;mRFomD82!ym8HQdldrhjRn**Cow$8+P^NZlsZcL^J&-Qk5C`%wbx zqE5|IYl~h?fn%}az?~f*-cKrjnu5tcO~Df3-vFcnK{&)QlN}UVIzBSS2~ILSB7pVj z_RcG-(E=^!t8yb)KukWx;cv_hnz)6gU%H-3`LmcOsg$$?OKDCSHsHM~C$gPEqDzf} zg(L@tp1k{ob!l_e^S{EC-ycvd@+o#$_2y0+HPN!j1L6UV;TeoGJ_x}iu##&?fws$m zKT6MOG02umk0MkXM+doNBFtiwEX}R2YW>%%k7g`#su7|7WDM?B9b8{=r!UbjNO{tK zxvNvIQvv+o4n6sf?AMIy&mLBFg=YhwgeWAggJ8DU0vP@b7IA&LOIBHHck%Ynkb-wG z6p5-t=R4KH)xkb%kAWpFqS{ovL^+N)TM*{H$kDt(sw3|@s(e?a*dWS{c#dKI4N5@( zGv+!9>gB@6Gm|V(6xr0kJ3zkn{nVaiLlh)mP(V=Cw>xW2nCBfU+ruk`yK3eyh92sh z-Ngeffvs{CmY`}Kn5iqs8wTRC>q;$kT3`8`r7J_#Qk>{q9-VlFchIr^EMLL$Onz`;h8%9VSyzC9j!snqf70G=L|>wr!Um{?+&vfdb7 zC@GiDLN|dHdkMXS8Jmb5X!=~91Y*3g3^fZY;<7OHI#RBt=U)RF z4SDY~iYoE1m~4RSj^VMkhkz-oOVS>X^sszFT>x1Jt8@xerkh-s*7HRpQ}KZ#F`vuFCqk)R6h65GNvI)S2$ zl=rTo&(+lNBUN!f@HZ5Da2v9+>5>%_!t|YoYcokFEMX{I5uq*yXj7NOQRa;^2YihbZ{0VHnyPV%2vK!L2RJ+;<$w4e3wQQq0(ig~&y0u6O=lIT zRf;*m@~;iL?dihjPMKt#tk{yTx_H&tU$FGaS54{kp1Wd23`?R<_h+_40#s>dT~bxb zr5xHB_rq;(qNjG`(jSnNloUg6zic>)w>axazN+nCFMV3MF1hXwJ}z<2jpR9rqHY~~ zgSTrwDJ4l-9)z(C`tj;k0z|D&BBWKyHp$OgSWzanyXzDL*U=QCy0XrqZb~~1aO9wes_7@^b#;h!Rn8Z^+s`?4{=leNr+eD|I3zG-nQj|`Q^!-SXDt{_iYIgO>&#F zF$*T(=|Z@V3mL!Ib~z&^Um`m1?gP}Cx%G9hlPIr7KP`hJ2S&kBh)Jddk-}IxeX?C% zOB7?6<%Ew!Msrw!8+`L+WPvt8INY!p;zou^OL9FY9^-3jDM3)Qj6MCfG_3$q?ylja z*|LI78O6ASe9L_@G6xo3Gv0r~nJZ2G%VI*LJ8%EO3KGFdSU3!+x0Os+XOtal>Tvkj z)&Ec3YBRsih>7!rrsskIi9Kukai`G$SE^&-9ParpTTl~;zDT#ENxJeFH_Du0b4rU{ zD+YQS*{mumw2u(F4lwe$bT-$CZ=vf-2bDCU)~x(g2ArB}7-9QaVKj%F{W@dNyJ35+xn9*g z+d-&tLBykzUw44Nv~`r_=u@{zX>!l3H>5$|9ggtxu`rJ}Zx@S{{TUt&O&`EG&O#KJ z>H7Pj)W4Ra?alx8oaxE%$j6VFFpr9B=Sn>_RFKs|%--8!CKqRglKH=|(ty`Fw}SF| zzj!JZFSBiX9+zDeJCFeaeQ*14%_Fbt9oEpw$ag6(QQX0%J`&oH$m=o^=9OCJ>G4B6 zm9k(>C4B>%iWGN@40fE*#zZ zKk#-~$;-=QOf4;5-gQ=w(TmPYXi^_T?4!w={5QsG*{K|LckS)}ZY||!BnYMJtL~lvi4~XYaWE{jbt;EaA(eiZYmt9{=$y?9(;6tT zE4I~9oj0sr9uhm(`1=3ZPan8CUoj zE7S<}Xw+{b8RaQT@8m$G`#pL@TMTzL@0-hanG>Sy!$f>_l9 zZEX_eaZcadRK7o8Fj+3acALJ_!8`SHCwKwZQ8KY4HMRrYAud%B02%HGZQi|MrvK6!2i&xkCkNw%qCasl;yFOrSCPwG;|Ha^$}Ezs$eIB#!#uEv1f z_O!#FrFct4IsJo(V5CgCAKXp9mKhi1h$mE4WY7oQnU`d|n?=ksOa!iyw{fK;|8^7iU%%lXx;~kUw=Q~TP6?JXSIs$`07`!1Abz~0 zTf(lAym>;O`;$a{m!{|D-Nn0=pV`0p2HA8y`uElThrht?G*n>jH1zz6{mn%i?#iJ3 z=@}_;-CpCiiQt+9)gtpm`8VRc9m=6KPsvWD8XhEux9Yiu#3Clm`eFFw17u4U_~b|W zBK7y^jR;GfF${0jcLGbb_JtTshIOxpM4d@OcGb+j;R~(IFyxF6R+aqu7&Xo3f(1-w zMZ}krpeepCr1Y4RaCy44LbAxTXJNb|9f&|+J`p;Px3!- zz8i8;?Olzh8Z?V1+yWrlS>v=z<{{&I>PyEE5@~rL z=eaald(=t$;PXevih{E9yu3?@qhMAnSFaoipj1KB-Ve(YXXPNRmy}XVqw^e@icCef zIs7{F-a(~fz|V=*j;t*UYWF|?!Ya)e!uwaPV*gj884|6>@zWo9e`pXsIgL>E&JMWe z`;X=ylVd%Q7)g)I80+@>S?wnVY%P8q&a`j)j8Z+9y=hLe-ix-WcAm33cmR z4OiWMaW&{$idZXq@}5e5>WmBLy*rCZKd-tib=5oqiy?hus8`)~8S?-}Un-4!;YwII zSM8JCyBYtUcA9fn((AX>0`C2m8sg};uKix2F~4!`w>9`}Lx0=Q-`3!_zlQtU8vJj2 z1ClWTEP`y-f4|GaUi#;lREOj{hK@U2rB9Ze2kIw$wc783Bo?&~K^z)V|b6&YOzeru{*lLivseQ}e3BL$9{?h{a|3`g) zYTR}H9C+94=<5gRu051@wl{Uiqz;JS=HI&P-WVcsz?#ayz91S6!YMdGT4G1J`vfH{ zc5vZM1-w<=nrtdr;#-G8G=5{_8E3tlsV^9r{Ag*&i zB<^&;{0*zb(Kvaw7jEsjU`F#d(x+j{sD(C;^l8VXy1`(?g29aiGKbD;cJ5zT5fN#R z>g61yzY%~T24_bEtX7xEr%UyBx2W|#7yRVA(M?~u+o%WdiY2)8>?FA5H1;5FY@1Hn)RqXF$ko`xofa0H-eF3m7DfQy^X3Ezq zXZlC6UEQOfn%n%)tpFXy;oJqF`&F@G0zEIOc>ki|_Y$???a7*~!`po?!fBxxnQv^^amR6`Pj@ z6Hjxlg#3?5{kxE_ib;~{OjQkzy z{{gTekN?T!5D#QgR~e!G6Z{cQK&e)j*Tn;PHNf43)$Gmu((_lb^!-EK22 z3*pz3B5_-#tw@Piiy6mF#M(_Hb3owP_o%opo(~lpQsAHX;2D_~T@y3Eu*?{b-Dv$@ zvteP^XfFQyGX)a%2d)eRty9p7biXtz1mpV0V)SN?d{Fc za#$GC|9DmOZ1D4si0!+gsu!!4Qtqkyf2r^rPqh?r*0Pc@n?!{>+R@# zoGAB8JokFO^>i?7h%;^0^^>5+TSM+}YawZ8%;`rORO-tbv%zL!HKqEq0SxH?vpu3N z+EZu1n1#2-3;yR}$eQJi5zOoG!CHL_kkFmN^spe@>Mka$8=dr({eosuQ!vzN$oYM< zy|Yftp<6rl3-`JPeV@Ab3xA~@sB6{tski=_`M3WcFPH^)A1r)$uK(yydE6>rytr^K zktVb_&E`NDe>^?;qT^4~y$8OX5HRkz6Fotr7yWJg=VkFcY36!3`=QO=tR?s{-*=gy z435mfoL0wJI)SAW@u9b|8ngM(vdW4L)cPS=R5SOfYI5tbOIQ$*dh4-u8KW?;P%!w( zxUH^gTES+w%3!<_K{NtlX$L7@Nm%hgF?N|gdz8+|Cio1JN7F6tX*Esj!hQ$0Oxvj$ zlKn8vmseuDv6Q2YE{M>kAT7BH#C&acwY!Wwi;cBkfAJ5{7`kg4^*?t=3*} zD@Qhl=qu_o_JU!RJ%^2zH5rq1mGuA|L1K%?9fO7_E&^=`T-GYsK-1Aw9CzV7pQ15I4!kk)-w3B*FkWFr34i+@Kgz z`n782`Lp(mM+go(0Jm= zvD{=SV|d?Kh!-4(4k$7-I9q@kEisb77^fukus9zP#{%m*UlUYfHr zb#W*|=daBi3Sr{MVGSTs5yY)EHvd!@!tJXn6;)3k6^D@X{KPnVeAaK(l=>S*dD5^K z-nX^Hup)P4dCIGR!y#J15aK?SzLi(yD|zL!j)5VYekY|_LVoGiwa zLxAP?Pc$C*gT-0K2)cn$z(g`y`hI9q7K*8VuAa%1*&FY20!eqroHkt~Y$~pUXLk&3 zfp{JNwK03(nBE}TghlF=k5Pwt8-qkUUZ}ueVW~l}CRc3#Ot=g{YagT>&_JF*hC(?) z3NNwv!~oeCni={++EpK};(MzDy)0=VMiK*@b}kTGVk2aNPOqimeL_0X&pCqxFDaww zwLlKMC9l&IvzHlMr8xyT-BaDCWwg@Gg&J%i4{;;+8*je3c=D{V3nbvgT*rA!QsVvea~(BEdXF``?I?JAAJ{Ggs74N)nmFwT-*z1B|HF2GphL_~1BDDngal%f$rru^ zKsg3_`rpS?*^Gx)s~C4d7;i$wFPteTJ2}Tep877%U&VB!&v9?_V?WGqCRw~q|0&yg zDjhBvp5~?9fUm>Zg2VMBL= z88O7YCPctauA|g+xq5?W@IW6U*bl852d{hfuJ41$!A(<=Z+>Q_e{f3`_0>kFRL)6^ zR5#FQsNU`~oKUMuQj&;WGb|PPi9yVp;*eUc%pvz6ue+$?dm}f{k}8is(Bm)j^hZ2S zYN@M13r)XsxB2wgG}7(P6A|QbFe`7{eZnHwL~@n*t{gWcH!5+rIGSdPAvok?iYjeJ zc3z^r(=b;EQf4iWTt3dxsnIEu*3U9W-*N6Hbdinio@rv+f@!Zm{p3DpaLHJt?u<}J4I~UgEY>zabRkBnXm~;~+;Ly_e@E#M#?T_Z} z@*@C3`jD8VjC;(QjiGU^2dFHW5+Tq8+h}&RlRVhUFqCK1LKc@$G}m|I%d6eqPBBHt z$yA|bty%lJbeXXLs~tS#tyA+;RV9PjXla_E{)`#iZiF&GejJmU>K3A)6Uv){a?WmxM+Ds@wBhmjj4VxA-DYvHMbXY`uQ$YmeJdk} zY%W$d4a{cgK6sknCN$#WL4fM}D8iupA^*{06- zbysVG2zz8Jy%YKuR=kG#GsV=}z}p-45$;rvwao@e&qsK|0wnE6ppHev@pc+22sIQV zvRveYD8(Cvy7RX`<9}f_>JbGg)P@3<^c+e<=MBh>Z{@I1E|uF~ZqlB$XV#jZg=*MY zWmn|7D)l*NG_hJ57(^jO;_#B9PPIR=V5c4d+x$VD;rV&V<_`2Oq{E<-j1XWa=W8)1 zy+ft&GeRH&WNR#8qQ0UbNB8EQ#k@Jqpur@DNSEhyg>Cn4R%nFBB^bXfBCpY{t9K8Z$eyU#NeMe_SvRrCPC#lsaH%|v39mC{&D|L8QeoIIZ z*RDz1$jE{i@_^1vjnOyPYEvq}Dd^CaHj)ltlNBZiDG*dAn|Af0w#uLJwR+Okr$RZTiKJX!FS--YE6(0wJP@?eZ;bXcix2%XL|y*$;D7-PI+d9AD6uq=CH8W z+sQyDn9R?3(HJTXlFfbSVlF;6u-X|)iFAHF;at7~`ZN)TtQJl`5FjhPZ9FomAfBVh z6+7)-&MZJ)6j;1Q_LX&z_su~0tHVZERK2hhk&Py<&1nHcCTawTP zPP!8=3YIxeUV!(d9Cqo{wTeZy`pw^(TSBzfbZ_sNv5>o`)Rn%5pgz~=pX7TEz;EGV z5C)sw-F&2g$VEIn#ay0Uml2$(42U9gEL)qh1w)j|3n*bF0S={p&*kq?97i$#@|T+8=~Z3H{+Xr!#fnq^wqJBTW$ z^XZN4#2$58Zuu2GO6v8PKIu-NCm8va7(p6#_ zvQyxq$NCO+5Uy$sg<0C-$%jpXOkHxLHz}6(y{HO2BEA+q`6}uB4I*OnI_xouHR80^IY!Z-Zvq9z9qU4rzt)a--2aI;U+xPNmIHwd z7nklUZxN{NO`ZB#f=DxK+~xXRGx{l8sg4G!V%YarH-u~%`Q;*fqKbmz1dZ zyBRcI3z^vojVsv1Rc&pRJZxcJ5zxfX*)JdiPWkYp*6_hoL@y9*nsPi#U8oQG5u0XV0sxBo^b4RcvDgT&2 z6-+72tN?5`(CX{8E>`f5v3rPx%Nf}S5&S5A!15|pRWGHLxc}MyPQ8s1!mD21dO(tN z+A}5vl6`E5AQMV^(h+KTINn|x#;nbhgpAQ0m(m2yx{)T+7ZGyixkwYshSrH&9W6)7 z7JaXMr$$a)K&eJu#k3Nq-gaaB#digPuFJ|-DzMst9-!?S?mNTwy!4F3Ti z-D?e=nsIUfWC0y}9R6!Y=i zjdqcdC4Xt33eCI4MRbdv_w(rD4Ivc+0JK~IB?M}Y#Z|Y zyZ!S|J*@v1Z|@b=WY_ll1`E9<^xk_<0O<-OfrQ>WN+&c4y~88DOQ@kr?+8d&K_MW$ zgx*v@P>>=Zh$y=9d~5Bo-!=9a`#ad>KFhdIl9@TLYhLaD>&O{Yo5FOtZW+}+Z9#m)PM@@2&y zoi@6Xp`gM;yC8wPI5tcG`RK6OnbqszdwOf6LvcQ78zwmM``4M*w!W%{x+Jd%a?g(3XWfy)v>~g*&f3hr=4aQ)352a z3B>9?65v`)p@^W@n)ir8CGTs@dc)DHJGaQkw$@UM-VFAaxV}$Db>^9B6lp?vAqu zX6vnkh@h#G2_rU;etwXny?-6(1B3O|f>wpo1hkeRXqL+uWpxou2IH{4Oez3%7l z-Q`@-gxy5ye(s#Gl6n?TG4tik$QZff+hY4KR{EbYx*z=Y7}9<{Un}Ach)CrilB6{lxG!9o ztyQ>KyUo*raIQoO#1K> z{fncUW;X&HwR3W{{LC?1xfg70SxplmlZCP$8E(hFKrMR5U-$``9_f${c^$kmpz}oe zcv?BT`5h&_P~J!*ZaY;%5a~Ipn&^)g$$zZ&FIZ6sU`?BnkUzr{pr4^Y8nEiI#6)Gy z)xH&S_h}1mczBVH*wUq{FmHR2z8z@{!(t0^JMvHFzX;>GQ8$-8uREnwPDQ{u(m@m7dX!WenBTC6td@7s>;>7?JCv6Di~(RVFia5GvF zla>ScUw{~CJeOzC=!O0=@8rvEOx|p^mbP}~kabPF>0qy^2u?&CUgY7eoGt;(o#?ko zBYq7hh^7FQ z&%Vt4(Fwn~R>EbYvg^0mqEJ#+FBqDoBbE{=_>i59P`Fk{6AwG6t_uyU9_iZ2>2tL} z3p6*?*+pVGZXWC5KHzL=XAk`CX7)PGB_JelcBO-MjBJ*D9?A9~?Gfgm}OqUTq=LsDj>s4tXVajGu z#)3cmWRp2!YY3TZssfQ&^t$Cgpk%4OWSb3fNJ*~!c6w){ZIrl79{=-|w(k_w83}=` zKtoGUV@KTZ-Nf~W)VREQRu8bKAaIm;yR(8a9a=2{b$NdTdkJTkdb-0XGwv>$ySyHg z?kMt(ez56f*Z5KqS0w*!`j{2*&l-B{zT=LE=X8y`gNO=7y*Go6^|mq}mVd zvyh2$1{z@EjH3^csTL$Q5oJU|UXWqDJ|%?e+G4@hz362*CL)U=M5i3=(it}CJ!KRc zdNoBK8* zQP}T5U|EdbYBWg{!>Eg)Hm=wVtD#qslr&F%s4T%cb8M{7gH-lvkjJHFeDui;4#`}!&)Q5Ieb*lT0E>8 z-rtg_2~SFUWBtIuiZeDuCLw1*{D(IA&l*QZu#HM5tw~FcMi0%!9uqO5_4S?w^d*+b zK>Ox9DQFvYZ>Np)@4XoIWC1qe!+%(65=qN&-H}zxHk_|-=1CPFo_26B{X|DrP(Fkn zTq&{h!fE9X34#@q%`9_n7U&kOenLAu6 z+rs*u?;}fHSo2U?WOZ)LU}UkH;?m^eN3YC5>AR5zL&kx)KLxR&K5Zgc=v(>i8$Zl? z3WDYPN{l76Vb<-A^bQNMNBlunF>aUgttjO<&q&V;|I8(|Iav6CkoP6EzbfMwcL|^Y zhfQXt;%AesimOVCGwW%!byxGf_Q~*mN?EoHM@{D-IfytSBvhN!$$G-Uxijb5;8v`z zEakK)OAdBpj5H8|c=!q`Ug==c%ot!(Eo!+v!VXJR{C+8Og-`TzHLMy_I>;#k3TLby z84g~XeTjHaIkxuvOLAqlsKYYtI&SN$B=eH9wyNXBC}D%Dc7uuHrMrpSD46%ga||&| zPNrkEIckxO(w~Ccn#1YBne*vqh||E%qAAVjTLKUXP2aWrjwjyjix)MX0<0N7CY$VT z-TP>d>5L*m9x$`QEpB~>USSDPvZTIT>Y6bqJm-2b?0iiyD3&?bW;;^GdZw{3MbeWI z?M#45K{OHDH=d}KxbF-}#J!>jV}3LGCGGW^ZI{#MgVk=G(Dj6(vZ%pOcdzj^p1 zl~6qP4*mR+1zF>}kp^)N`MTRS#>;dq7^4z=`y%fYzd7HSR>}OMsg?h12-c~)5&eSv zOf8C{Ut^TiQb@zVTPO7S-#dKH8^q`E;21_HCtrsdTOG*&A9x879Ik_?&3SNi)WIbBA)y*yv;2WWY+&L) zI^A!L|JO1I1e1JNkGJ~f#Pu2oMIO@(bd$gMIC4LXTekAJ@v(DM>mV*V;rWkU^KWr~ z0V+oKi?pKF`N+PE(b14)cBtr@^;GE<$vt!(VtElK^m9A;_nPwMD$uyB1a6W&%5r!+ zAt{M8PHi=71Jux}1QG7ha z#4~Mpn0{Z^yN&T)q`YCw>H_w*sz;hH( zn7^^&Bjf|wYjv?SZbAF$ zj&efkW#8J@>5X4^qscy;H&gs%K3N*=_~vrczxaVf^UvBF$FUdgZu&y3UQIcU#vs_F zoe@vrm0kl`p{uE#{DMu=Kj?zGS}TE|Mzr9n0NnNy(jv_P^`RSk@)v+20PjMcj&BHl z@R5DECllQhO*?3W@GSO6)uGp%~1GyBEHrhKQMSB*eF#61OEntQvHNku7 znVbJ;-kAB3t@mdtV%RiX6aUTergNDKs~$B!#JqJZfNZVQ9ow{0QAI?r%C1ohtgokd zujJEKm#HS5JOX`1Q1ZS_Gj1vj2B9Ot?ggX)U6Y+g!s@cQ3>Texu&-T*^vp~e?&mR` zcTCT)?}ROL;F|2pvza5$3N5_gI$hl@Vl2QvGMPj*BWZy({APw%oe8v}WxPtj8po~j z4P>5m971DVBB8`~Muu~riXtGsVGj`#loY1Bvgkj-Gwb@?5`(b~p_-93rv^)YPq|l^9=l zF&L^A#8?R*7f-&Jbey6UAumk8Zx_^x?#n`3oQI4W{8-L-t+hXvf+5`s6p5t8*DhQc z>4?%4w4w373Gt9%%OZt!eoKX<|Dr*X>wB8f+MR&Nrs`8kkrAtcVLFu221bi~b)`4u z=M|i2>&_((7QsMqy}|N1c49@c`wA}fr8(x^221;>dzwZ=je-1Haa?YfF82BpYjGPk zc=S-(HP?(CU-fQn>}N4@^HcV{avpr;Ux55Ly>m`}NCN!h4K7)gDotwav%x*;DLqm{ z;mBc(aiVdvGw{Pn#_$#=0qsK5rh!U@kPlJU|ER^Rf$5c;&c(US0JiOyS8}Lp zcCP-x(v}qf6i{PJ*NGuGmlZRTzko_VsgGmb_%hZDHnzDv!zSZdb7kuNg1TTg0Sd^I z?Y!5!v$-Yhd;(P`_}jKm0(>3E9(_n}5hYwg;V>zYjJ!~ z0y~;LtcIJ0L8^@}Ye z)h^kf-OcsaUT)K0z$y_}+frSjIrCfd>Ae%yZ3el>Co}xz)kP@Yy(2>n-!C?-uMl${ zmE~>;gi@GCY913i+}XP?)~+R7yYpyPT0bj{(-FrgHZ4fc!NsJ6(RVasqx>#|>FHDm zwzdejytv4G<;-u#$14Gng%$Q%s7;uBsdG=v!83iW*z5(*J?^z5gUo=XS?5!|3R6O> z4mDE{A(R0bj}?6Sx(z%QJnoZa0B#w73;d-l_HmhTSJIFm(|Nmmc=$Z_;Y~bX%35c? zGKpC(r7cJH2|^+{E-?}HspsY*Z87}#3o{$=`ynn(r+n|b_?o(?Beg%x*n4#2*XnzG z6O#XE)edw?y8TSMcVL+D?6!l_o$!Z^1N--X#ixgpoxdGY&YP+${6OPaSqOF37+b>r zdg0<-$kcodDtH(WaeF%HU?YwYNcF#8`m#M0b=jG(6@XJgeJwNUEGq#ptS0x(7RCCmUwz6|$pNJ$-m=j4O8eot;2C(}|YvNK(#gzpTN* z;X=i5Ni{}EX1sH$9K4z*xU0Sz-V(Jma{K-)x{W#Y?I|RvT6S1b5;JLt&$DRL@XQt} zJrkTSd(h_V)QiKf#9chEXS(&A?lBD_*|3!r>~6Dki~`|5Llo}5sgF~RbT246-HYv< z5|L|~5Fbq}`8dSiP>8$JxK!*+#1)QJBw|9gT3>XP zVf600dMLEZqT<(l;K)5uKgYwjXp=iw5Ee;=(5Z<|Dn4Iz$l#MTpEFmJS-F}F_g+cd zxW~9%$p(>Q*_gf_{QNA}LC<1E%;*vOGz0@L0yeMYSs!Z-gD{=XGQ;#DRR{zjYc{7N zh~Y5uk+AUoD~P8WuOunHYXdgDip>k^JR_1m^$3jvl`wqn@s`5<^txD$sP}#=@ujNg zdP%3xj}HRjRc2BrRjoMbB7bt_NB5(i$)g)uZ2PSBy%4BTm>FTqubf_))w5r0;2n;w zN*VTKJb(RzLO->CMU-M|)Zl&9o$M{i=33Gs6MAuRkM(?!M5hW?1lF?;jzE+tdeoiK z1{Nf4O)gUZN@q>NyT-Hn*shX=4w@WRb6?3k?8u^dL$G?A_QuBxAFSP`&AMtAm` z@UB)_g_N%zsoddAyq1|iY2TROVh&PFB)wQqMv=5=P3D$l-0;nR!whx3;;pjR{EdPr ziYdN!_7Z>o8|pK-NqO4d-29Qdp-@dtOH??nmy3fLrQhh>*k}P+dMNYwGsubHSFmrd zU1}tTEBp|8U6xq0m- zNqU~Dnyg`uz?Q`%49b3ad+>o3)u&cHq==ZFzhfY=WNC#?@m}b1Hzqv;kDg#{xLMbU z4#i&a;9h|}7_KifGBNj9E1^migtJYVvIldL*&(dkU3@{UU}vPJWc8;s^xV02-WJbX z&r&A{`Fq%ZP3SmRPP|)7mF-i35c2-;q>wl-)o|5?tspJ5wge(kQw=hV`1FGLL+o?1 zqTL;+k^-4GQ`!_^GU|xF0MA9Y?U7@loHkE5>ia3PrQ{AlH|spnfn@V_uC?= z*OoRsKD;MNB29-QBx-`#c<_>HqA9aMWlx?y&pKQ6YT!5))|I((&2J_^7b1I zY{+yTdE~I#TOojTy9*X{PHiEvZ~(ypclH=}0P}QkI_2D=22>NMX>o_?fgFIGBUAoRrgx7*Ihem|4m%C+^Z%k1Azh^}Ri{r*&A`=* zm}GQ>P4V2Z81O;#PqwH&Ci}6@aQkQR(B%q|fZ%s;*30yEQPD_qUJPbLhkHFtpqs4S z{H1f}z3{A{afs^61fo3m-in1n2O7D&ook2fEn6Y$z@8<1m3YP{=dhuTZW0OnUjS5F z$*(XIh5c0BMQb=7kZLT+!dj1aa77}*OMLr)8@T5%k;K6m0wg)62BDt}2=8yqW4 zUKxW>Kd9~~Skxl+i^EAWqjQh=0ixoo%Nx2E7_0JvU_IBRmR*)Cx5u<& zN`P(SFOw*k3=?hD#6dxz2CLfVoXRe5t@j9phev>hSuJu$w}J~gnPT;4s}%?zHKfXV zssO7^w`w{?&v~J*qcxg>L$LKJTE(ls_%C2gm!Y|#>nwcWce$91vK##-q|%Nfg*l%% zA>AY?Uoc6-q^=6v8kY?XKi;-|_{&vJii?8@lAeUsYtj(<**vm+sj@F8Qqc90 zDjc-+dx##*2-BQcGtPyiA~}ttwsJ0~MSnHLHEhb$@foCP=FbK&C|2GGo`R7MH1BvS z7iEXKtedMQ959P5uC`xzmf!wNG`@yo>Q2uZ+zVZLf~{iFE?=3wI^SCX^WHD=!?mz1 zN%3h{sT?Uixhl<0#d7ps$P|uy5PooWJ85sq>W|4cAz3`K6qnQU4~ulwlD=d*DY@&Z zQafEKE=z2FLM zN;`ufu43FcX->R~Yd^011#A>l&UdeKss4V?c1c2Y*UfmJGYikOXQbDfl*jA9W;~7a z=~=BNJHZx=_qn^zr!i!JVM{q?Jaj5~y5$8(Gg9a3t`5O*fvmOF!?jxshm#5KeEVh6 zg}y`5D=Ui$(!FdH|1A5y_B^b$%gyp`ldr1nhs*u4G?KIDnl>{q_fx>?j!{)FYC?ga zG4j`$e5zeCyb5ZkihatAo0T;(caq54a)$Wx+y}QNm zuiiuuN%g4uuEpa-njH(53ge1$D5N8bE6Ct_UyJ&d+VVDyPbuD!nlo+v;T#wK@ukn2 z+VGRJt}&OXqHxb3(%4zcUo~Hvw#o|EeTmw@Jw8eB^e;@g85TTn zN6Eq^QwK|=zfqfM!OUsTu#LK{K42Y$Dh23?*lH(h;Cxn|l`G$S9^c^YTs|^7l?qW| zvR|}y_dw(HUA#*+7bf0%`M=t>-1^v2EHFQ-rWEM+DH*D4!h|{$Ti=0w*mY53Pmyd^ zPNiC<2R4b@p2(Wbnb&0!bo6-bHum*{9XSNDz>j(Ef4{f$1yReGsfwef2q!!tzhs}_*eAcv%_ zn6~=JJSrvFw7F@krvqE2htcHxVo-6(Gtd0YGLz6hXPUtHV5pY3AQxnMe#|`^%J54= zBhNK>-6678=9W!TF*mXR$(ZjTfY}{i4-{R}d1bY+V9$UJ0ayyr3D<-m7@-G;=x>IE z#+Y~w8NULgjpWQlicT(xJxBRPbG!7`oCZ#K*BPno!C~`@DSyVi24drQ&G@2l_CDtf7r%=LOVSZHI3JTC= zQ|DZ^55x)2ycw6+;#}9U#&glgHV%0VN)L?f9${YvcQtyx@UV!XV?xoDS(H}laEbwu=VW+xhfCE{+SS-G6zrB<;#b1-O zpfw3st)gNRi`(MT1!{n6xcmg&J1m<=8W@(z%s&gP4F6D%}p*X_pA{4Lhhh2b=k%{PP5`v z&?27IUB8iR^=&G>DtG~&8#QF1r-az_&;YTpx$`>an;p;?%;&aT{y&2z?L zn$%iBR!>LW@tgvUk%@|DtBWi7HO%kedkWre_BOIsNB@XvlFr= z2(f5tcqH&6^Jo1l^R155;^y|LEB9ct4{iD1c(Si#{%ndxKiP^7D^KsZx*XOWkV(Ap zWD$zxI>L8fNizLtAYyGJWE1-PuEHQc8aM&rzoRc7xAb$S-5eyw5Q!@&s20|6-RZp^ zk;1R9lUYWE|0WGr1$ba16*F6B=M_fEv4wB$ldksWy%<3)Ah`6MR`xwBT4W4lFlh5R z**M)xGX;~!T|E#!8;=EEEAfn_qogD3?jzbmR|#6x_71K=u^U1xjWf!H3Rw2B@*-%H zf1xAd4T)hR*+06Ox|aJjwA|lS*4#5zIM*`Gm1LXRJsTz6Rip!*8Ed0djK@E__+vxf zL~Jo$1|(c@l&xEyU*G=aTK{26kPGJPQljwf9^)wu(@Lq4EdTUl0MuVlX8xhcima-T?ZlYmI+y=+o@NCpY$ik`<)scPxP<#>#T;%?*VXuo2OhAU68isI zY~Kkv&va_!NBZ;bFY@GG$q}k|+odgELOzGAI&%poxpcZU)Q&AEegva#Ki4l|IVJCI zd3MT0<7`AXn3@Qs_mWjjA2D6!Y&Z<+)Bo(>zfR*)Vb7%Hz8HI9&g1B*s#P#KKk}0D zJU*zvMRYsac0x`fl;LcV==mfv#?dUP)3k-5qr5=7)Xc*&D_;lyGR7 zOpDTatt(bVKz+(fE<)bXzkGO8?g;?hf57FXOY(N^{^cFt|M&o~4bi+QW4}^j`+)*R z7;u2UdkjyS4lgb&*eMycpFeS_@K0Mj!aU&J>z@tRdoHaxP^Y_J zG%ZFRU8-qqZQ6Rjsqw`>#;n{ zgR$ghN89QZUY-m7Buy?K5q;y3pzucbbL#d-A$Qw$kt0t!TgPOC(Ra`4yIcJq84CT( z=ljaES$>zQ@z`dR$$#Ww5#X#BGR*Vn{=m```knSoYc}bZTLI51HXoIFtpyZW<@M2c z7!=2Sm{*38zi(+_Na?W=H9g}DSdZvpWP#Qip91q)OGaW5{C$+&AH>OSR}jnHq#JmQ zV~XtVOJzNH$=a!f1$|i1yD)@BcCESPYf}o!i&O}8G&KFL`tU=9^(!0w9Nn6kd6e~ORxEHIWoq!*?N>D^;FgcCvxrVz+ueUb0WnV? z1SVza%Rw=x`|TH=-+o7*Eb%`l&Q+I)!I9ULrX=g2O_KddC|8es3yr6OPC%of5uS4t zYrk3yn&7`HT^`vM3oV*qR|PL@Rr{Sg_jKaZxH;kJWiY zY*o7cAzgQ;JObIEc%v8lD#tQoaVn7>J zr=3;eoT*`qFE#bcP-6?zL@NTViI1D<7_!$Az3?T0oReeRa;@@IZ#xeot^Nc(?4wPk zZpF6^TV2-|@|vPg_Z^JLy>h+H@y_r{wd>Rod69BXs^u#kEmM5op24W2An;BX-Sg%q z>%5ysa_^F&A79)X0uBsV7Kf4lBD??Xlg05GaGgoka-^Oz(!yT1-pNN7=j{CUaduqL zqYdMW<{Nfg^8d4vx^z@7v*l4RoVOyU16gDexAO^uC@hPY-5eQBN}GbBklF8zB4?)o zx6NYnS@-!;gc$0g{Ucs!ilcbqHoe8naO2#14N*R}5x{ct?G^j5L)*!7J_$#u*G4uD z>!292!17C`K(GZcIXlFqM)Xmx#)NUlx;f`=%RcbLI0Y9Smc3EV`7M}gM+V;aPtCdz z@D3bJM}jZPmfoMTI+o*jY9}hO%*GHGmlzZ0dW$MYj)r>88r8voiQ{j|$?^vRX~|;< zS*`zHA9jWge|FQ*W1@Gij~f31aFOf=pNW=U*M8veJfR+12`91swN6UV?jmK?XgN7K zd>{r^KBo|O{l-Y}TH?T7+iMtMX!Wp-f}jp3{)$^#KCk*O0E_B|{_pzZY(V_-ioI9R zzYya;rEiu4|5ufro6+#!B_a~*QTwwYIupx$^CgAX!Z-4!$E=K4=GPoiHCLcbINwDA zBj0bi=mF90|EQMJ>h%BT6zei9>P!2NKP}QHTlGKwJoisvUQV`DWj*vJGPy4)G<_nC z(rM}mm-KE*zt?5ac@1|i_!hr&=uE;Nmz48Xwb93TftCK{`6?9kGtQC7!AW*{6oIWg+gn-y z6Gmg@K$Uz(RvlHDgihFP07=i_AqtnqlA4h|RDbRASodkdwyAH^KuZp5x_}#~(Z9H7y9Fv#PRbGeY}_ESCntt0i26ArP69)|cA0HgA`fsWcX!532Vyy9_F=aXFv& zu2O>tMaGE?Jim#2}9%7Ruyq<{klA0MwP@Ua&B zRd-pEb#dTu$$x>buC%lP-vcCh%?nUUdg@S9Ah-@M#=2&PZ{M@l-!}?iVIH5}AFEoQ zY~h&HR4+%#*7aqRV7UzFrKJJAV+V@3gl7p20%*eMB8a=9Fx*IVF(Bi5tp=`Dx+6w8 zzq(lU7Z3pdoptw1!fgP{`HM!xpD*b@c`4>-D4U{&%^dwha1FWlUDCPNp}~BPxNNU= z@b-nmfMunW%`2xjMEh+tATf5+x@B__JCNgNXqv1(bO6=d)U! z7kTojm+7^rSOTVUsj=2DGh>AVpHO>V-iP2sTUi~LcpUttC!YXgyA7XMR;jZS<=c_r ziHgFEn3Nz^fq2l`zaYtY#vwlO(RbQ_tbpm#LiKq(J`ZZJC7A3@=AG~Oas3`GMvTcT zC#&w-%%6FH9z8_YmLXHu_opuEQGp-WC9YZmy!z1JoflEI!}72y6kmhI8V(~G^kLk2wV{cxC?4h)2*%ucYi634himVKs|c{S}ocMl7jaxz6G zrALk?rlsIKt<#ZUiB)XO1#^CCL2T-L-1OIjUsY$L3Mo*iYM?m_bo1HDsFVik5rIMMO?Y9?B8ZVACeoqTywAF<% z3F`m08&onRSpa`_K!p>%zg&~Q6>sEQWrv$L&m%DSEUpO=)vf585`h7A%`@z+ppIn|u~Z0O=yNn*rQR?!zahHSG;c-&>dT>FFrtCxRvN7Z9uiYYp|B$w9bO$l69y+JVk4y>5Zr*T&9H9NV1txBEp+&aSO{>#r*%a(fIkUW~E0&$c6~(iY-Cxml(2 zGMR+F@HHtN_NbwsVx~8cBp}s;R&!KOfvcn5-AQC1Iao4v#gUunX`2=o+JdG0V>z0! zT6eDAZ{ZUdNtGZ+U(r@}=B{`4nfW^a5QzZRB_rEd_JUT_0A#f?cnEpxGK^Tdo7^n} zy5P?YN=$tS>R@G$cn;d@JkCDKZ!FbvpwOLydl+G{C6hBDSAg3aTRw7*aH>hWf>}Ab zRuZ6(YPN=*%lm6ac+vN@8@?d`bKV8ALS-?{^g*mf`#Z+mLOs~;LX+@=TJK95N`R_9 zmt$mTe0E05Y#_U7gZTKw_GHd_{7bt|V0qB=GT#~`)s{!;!959Gwk&)%FPXeTr@Fij zZ!}e2GAS*s*8LWgndaLL^h}i_Gm|^qs4I5*7R0c)i5lJ|I;bVl)P+S&RaRDIGjs; zjj=Z3wN!jykYu353!@*CBUAv+GTJuI_F<1KGc&ZTNY3KO)3>j5`oT~BEq56Yx?n|A&lpcZK3|0E&5s!9*T z`qk1!WR{cjs$pbkhM^G=UU<2mIU^z~gf2t+^BJW|;|=SK1LL$yEOx^r=0}K4`bvH* zjTX*f>H*ez59KWb_e~pU(Oz040AO>i*Tz?;l}~lJWK39AViIs3SrGVcs?F0XLob=S zY9J`{r>x*F3io%<01!bC${|hRp7;UPJr$^KwV(%--V+f>Rkn@IYu9;}mio-0m8it+ z+5>f0x!%FU{Og@JFK;CRDTip&ii8CYzOs}fz*d*lPG*xYI$N9=mIE)$lFdx*Jgsz7 zl+bL$*{65LS(g?x3`-a9AjAYj#8Z9G-`rB4yAD9zMrg=M*}y4;)xMFzQ zg7y)@zXcsrfdO;xBH+3AdDH5FV)-!c*}ucFS>>tYI&ruhN+mX%;eqb1_QwOJ6QPuP>e~Vj6LKMe@oaLRAg-9I z8Xn#9!`!ZY-?WU&Jx(t`xOZD{=iP~avSUfnRp%iD`BNooQ| zv7ju{dyD{+h1kXo+xlu?4!x6hk~)wPfoMwCHqg*bBA9x<20gWaxYEI5e)=bCtF})7j+MnoQoKt@Uwri;@$S*i+#K{GdOU zbyO8elIf}DEvwQV(3QQy)AM*_R?QrUWc=o(0oI>oA0wgUYVMLHos#6tV=6t;! z8Z6+k;W%3*eD&H+7hfsnal$DK2t)n_&>n0U)^jv^qRMpzGs`8Kw_Unga^4R&JmJrq z-;fM>n0A#OYt%MRsZ=It{(G$I1*kIK<>vp0*07~-w*hwtpOxu}CVz_5QX$Odk>zM? zSJ*8e12?QTwVbnnwlfY4@vi_R0sPLUcT9|0Wh94i;0ha`ju*(;iWLI?k@OxpISUKnNwANc0uoxz6bg)vJpR3k$#*Ui*KOqiPmyf zR#|oFi2X(I>y&L}$EEC$*J-n3dFBi}#ciF1bGIq+EFPa9+ciKvSM%CRUUbdvqBZG6 zZEc!(8?BqYMAbw@)K4WaAcX#4TkyMVA|Bs!4Z@gXp_-lbDB-9PyT&VwDT6TP^}_Sm zhtI;KOlmpqFC+R*%bFY-g0u$GJjZnGdRJ+dMTVb>lvryIYd~r(S{jA zyk1uwOl;5AR{I(#G_i60E~V+~_z>-0`gEg5ijqJ3ya09L_Is059cNc(3}iD<_z3?9 zeBx%FeKW^)24$+Co^BTURdMfAk`z>$8PGT55xR>o95(Y0*;SNFi_UpT8<*|sv%3!D zPQrvTPEa0c@9$=Ibd>ZepBE_hF(Y{Loywoq8b0CWgbK#Mya8-M>FOSr z=|YpmXhoO8X9cw36|juA7uYXKzbX<+sdY6O;TAAzE;6ERHTM7 z(-HxM%iKMJM`GrFw3ev=Zprc_;mHu8=rS0E?P` zRll66DLtHJ`s+PD)KwJ^jr4ahQS=}Z-~O^DLwd$aMEUDO9uTQ8Oa(-BOQ zWEH{WX;j*EGUQl~dF5Df;$?&a7%oB4Tcgi6JLE3q$(}uLC5Yls!=jGL4d)-eynjL3 zT!LR6fG+*J(9cRopiKbX?SM%n(XO4m~>9!;8F5?D8~Imsvkzj9*66XqSh-60~PobkAxI#|KPk`+5Xu;ZL8d- zb!3Nbw8U>xFD;#i4*!?DERoB9Ed=Og?8(t!)0Ir-`=pBh7;U0uHTq%J-G1NF!SVi( z#jmr*kpELDkWG3p*`D9=PDZ?s!m~xCR9z7$tK8Mgtdb+Y{8LHG=tuk7l;VZ^AF-7Ej0JtM*69fZn7B} z{pohz)Al}LP6Vp=DvH$74n59MmmA*5@t83G{Mcq%W;BP@8#%9nFUN{*%Fj#t+@fEC z`6Cn#vZL>g=wrWmjqjc-xF@0|4A10et4I2Oi6BQO9QaQhPfFgX1J^0@EI@cihK9Lj z{!V|vpJhpU2Sc$-`9&s=r=@LZH;Q0G&nC-v*u8PDDx|D+yt3=u@NgdSaXU7l0w{9N z@T;RJ!?5hRrx+`}mJ=V5LLCQ32hVdy7Yvys?^LB*+a&iiu&%ReYRXtH%dSE^;}TIlQ7Sme~58~zh458DZzEb^2r}x6d2^QoB#R-TBYny1$&#~Bc z7F)5fd?&588Qq3iqV+&w!vg;`9!@uzZzPE6EK)k{o#N=&i*#&!5W}~rpC;gC5=B^G??|;A z%L9H>TD=Jj$#m+qM0@uLpRuKNT)B*@y%@J1AXGiw6nU&$kEvu-&bhdvT3&XRfp~4W zQzg2i>WLz5Ea4s{>9WgONhA4XhTWqOO3Nw%_R-zb4>*(m?8Mw@_Rh+AyHtxn23I@z zUkZkxS;Qz+D2(0$1@^h*Ci)c{t6W86lc~H9xW>y#&wB;Q@JSW~W_Z~c#Q%_0S)|;X zUi;?lTW*nhrf7xcL%$9t7^0!KoudQg-sneR2ep7eXIES8GhbRrc`d&|J6*d+=&Pqn6;5e!m$yYbiBhK(8_Y zu}D(9CNic9?|tr@{zIJ0hHp&*o8W|gb6iiclbXE73DKWQ)hk{{|@AL=J!yM01 zN_`&!MVPLf89wS}1SI;iGz}ZK68Sl}d`Swk%v15wGD@=Fx`)($m2&+<#aKHcJ1+m! z0Aa3#chtqxmAKHjL+dNZxkVF7;>~n@m*u>sV-IW(lW6sgrbKevQJ*&>wlm>YD=0?m zk?Cj0^q{N#>jrbjbTz3)Lm=Jp`EL~rcB=0}NlR_0T)6CqUmUk|Z*x2`_M_Y8b2901 zeq(j1P9QoKL;_&|k-6kO#G)BOZv0HVg(;w%hW7R5nV11Gn0{h*c>R+Hzj!OE`KeiW+ zb2@t!c0dQxj`eoX%tsI*s_U-BE;Lc5(so(SBq@XCrgNX-`vA`KA(n9&eP+u=4!F0{ z-FzO2Ho6E&SAvZM$7T|&;hYqHKs8Yydbb#qzLcO`S{xw#|c}nl_g_q0ZLqI-}y}8}6v(E5_^s8#SxZ$=;A;9Yxe9 zZ}p+oXTf^7(|`cxc-5g^d+{Vv(jQrF)qCi@PPuoMZo}xV49g7LZ|LypBHB#qpla!N zLv~uR)j&2g1F{6GgorSKd(sEFgfsbV9yeZ+J;qn)rUN82en!EA1nB)bO@-Uc`PO9} z`e34L5m#OfPtr>ZAXvBavl8>O7mVp4z!RvPS$TaRp}nHRF~Bn0&uuxoo1}5W>zK1b z=&z@&f3+)&Rm!7lLnO3BVLqoInPfi{U>_MX_*!zXfZpq9rpoe#Te`_YbRA>P;Euqn z^4f<*M(+V3=Jg7!NkyjyJqwyV>#F1^(;WdGcQnl2KkQxSub6&-yem(t8f6fXa-`*i zukcTt?)u&R`cSZcmR2lE1@9#;m-@_2@tbnwOa`msgSepD+7@*VzImhPjAKkQ(a=g7 z#D3ef?S)>zrYaLE|50qKmUi(XEC=Dys4G_csKEED@-ixWT7D_{qYB?8)6MOah|mPC z%cp)flKrMO?W3!*X$<33@QW~tUgik!D#pc8tPHf2g*&pB@X>;naCq(F!c^Q1P4M>i zqsL0i81aLciREivKcJffKG)mmzI<`ZY@yqd9@H@I=^63yacrY_^PUvl)hMwg<0}hc zrK>jtV$vUQGiuktg;2&9K)v9(<*VcC>zl=8El1ikxuY9!$3|0EaqHUaNP~yH9lxP` zwVx)k=P8u5EE$|5SMb4jq=TcfT17!d#eN{qYktj33N+0l<2gYxetvrTdKE=!>5&p% zE;L~;lb1(I41zdbYF)F1u)s1$T<5Wt45l3%$Ml9>TayycXqxG+TvqtXUT)=7zO^k( zY_|yJhYlgOIB7C5k}7@;Ig;8|JS_p!US|SIUkYg}xH)gu@>;wCC+8RjsZwgc?F~OG z8d87kTg4cD>0^D>Uctw4@%i1ui$6a3>|L{GjRW|sv(h#0Rj}L z_w7Wlo>D2jr0CAb0*X0QJNK#=!epK=oH281;`^(%uIU24(x;L;PU0rsA|Q26GvD4ZuWokG?>!E3ukFP%0Ppi#hrh45+@thROvt6^(n{X& zNWI?}6n^!p*?H3(uq?;E!WzNETv^@xV!o52e1V_;L-lo!Io$e*ALq4+hnN1lD8~8t zKYJ)lTD{D-Qe@J|lY%mC-bA@r}G@VPJU{LuoCSuCZ!5~dW461e0( zmtLQrA$zgel>BGLKHEb_kq6Uh_l4QdB_N)3W}H^1 zdteo3q*6rEmK>MjU>}a_Z~`c8*seyX+HNoDTFpA%6VZ=5zPkHMN8Ipj4hW)G9QnI@%+^O-san50 zAn=6Ui(lISHt{4MIv4O_^C0uf+tA@6U{nM=+-#8v#cv#8g6UZWsU9M%7p#pg zirNxNlD3o1q|! z+D->57RBxM>cP-D|4KY94aJ$Alxfwd0ynAVSQ?A+Z5-v{`dt_)gA_2O>rLrc&|H4E zPaS`9qw1q~v3RX?NPql`x$Bw&Fzd-ML5td(mog2Ns_B`7)$(2ATVvE7BjpH)l~jbZ z9bPDO_iWs3P#JH6JIj*#H7qgH7s(m7tT%hiFy$a9Wq}1Tv#q)@xcHWd(Yyo0?AYgyPqtda@$(=~z_`Dt)cr2h5*>1;#4k~05t5PQ9nEO+v`@zwG;*BALM z_U6qy5-KBi0Uwjq^F6xJiHq;vdBellYY4GA(I`H*Yc<5in-zP1^GcVJO{OFQaY<2u z$w&=zl6NoOcaA91Msz^01Agi>+t-$td;Y9+fVoy2To6j{X7kOoZ)6t~BTKDO7Jdl+ z+{k0rYq6*8$*<;?Rcj&_i{g@SX=wTpriP8efD{kd_Z{}?2kw`e?E1eaouZHy8%-d6 zy2{4hzfZ2PkQ-4L!bi%s%_<5@^%%1j>%WIby*P9sNMB4 z&X*cbT8vZ{GXJ&s&MR+4>RHIHz?pDrN3bX#t%0{UgIiV6VfPn_5B_$%s;Taiya=<( zO=YQ`)TS^V5jKHwtoBI$j=u!hQ$9;yzl&k2Wv60s*1E&NHF9+}MrOOrb94@yHGB{*ykHmVW?EGg)F zXX_snvCM0aDHfUK(AzF)`iMz!%M%rCo>|c}Zxv~9Zvub0b`luX!UI(~{-1~6#Ibh9 z0htV@gHDWUUF9Li&)>OX>{sd8ifsfj?KPQb2b4GH&WM{VTHm3=hn2K=ToZw|-fc1! zN5zXtR7y=Q*y2(k3L?`6|4{j=Uc0Z&coq~kvn|WC)+!pB@|w+B)RxobdP^@!(K_o{HQ4c& zNZcIUUlfvJ!>7A zQk=rgVn~nS_e7ku3B!o8Id-;6MVS8tZDrC0f zwYn|ELl$j%dL?^4KW_-a%Rw52$FCYzN}X@JaR*{84j^(KPo~5_u7r-6)Q*TbqKqf2 zd3J82OWc&D4L&I1f2u^M%ig2_gTApTtkIqZOIKhQ(RK2nm ziE4=!ApvwlIm`CBo4DLrl3h6u+hlXNk?mIlz1jSN$t-T=l*h=dLIwwDeA%bG20zI) z2zA|EDvGv%Ub?}cz8)vd!lNlN{})Iw?V)9>;#qkZR*sdhuN7YoZ$2#Z3`7gH*Z00u zq?rb*huj1$DlK;Qo}Py9b?jYgVvI|EYA1tncCEO~N}^!EzE;YhPN!9Tz?7Z>Kav1? zE^ZOdyH0G511r26M43$EdUIteMuvH6Ky z5@Z@O#_UPLbHys-Lsf$M9QGs-xV1fT?w z_1FF=?n!IU{8C(4ci$vY`BmSWj<37+Q!-l71*78SJUt-^H4zc2p&3>J3pYd%|9(I*QiGx9F~U7wOx{(-()h4G`FDq@LJ0-IUG;LtbaA{`hhycV7mM{rz%Yz z{wrQgFP|!(X|gLw)D7+=!BZaPfrGYT3j!Iq=t?L4J@J>g6c;A_{(YuZ9(<^fAgq-y zYor~&4IFAV_?_rGBaxLpb8MuM+xJZDL(WYlzp=7XSKr5Mi7QCuY=F)JqQ_3st!HU8 z)rEzXu6^XI;B6ydYO;jB&>{PNYgy^^&zOVdKYGe9~Ivqv-Tcf5mQWBvV$aX=D zefo;Lw|!IeHy6nh;ew%!rgRmFYYQ@#Awrj*!38#ReN^;&7duOd-@wr*FbN7(2rG=B z#VJJ0C^l>P4kl*)|rKq9$Vt&VMW)AFA_z@nit%vh`;>Sx0R9NkUAJSY>N9S4BhaJAq(W z*76VV8Q%vauxNUdQ$7n0KqI{YDBJkQ6txz+BN>l}jK;t}FkeUCd;Gig$86~^_iXsP zZyi64DI^kcy}-~Jm132Y<1fqUS;g?u)oLdU%kXQB0j@q&{|-JE571;U?qza$4^&uV zm#|&8C4=U?B>zqh zg~N!vIu3@~a}bWt3m??xgKB~)hOOqGj?R_#~TdmhaSvzq+Q?_FeJ|2tCs6`8ZG?P(j0y1$L7Nt1U>-f~Bu zExzqCeI1d1hj}m$LIEn>0VDDw&ahws&Oe6*${7`_)$P&+7D&ikiKcXH3Mwi;x>R=+l57_RF19XPgtl zUe(RJdh}`BoSfs%gz=AJ#ZliE^AnAyhcDacyV~E4GX3&wk|N;kzV+7Yopq)q;Rk2C zY9&%1v04Pxdp#jyp-_cDWv)Q;m_ZMgW-&@Fj^Dd!I&L{vb1{dS>lO_>G?=c;tW|=& z;+K!ir69Wv8YnqeGIC71H@Px8uzf4mO(n7dn?li#jm#51yj9ZW&vKm5$>cPOlU>{h z*lwjT#Cq9&u>D=z2}lGye9)q*rd})1S|De54ycltZL(m#E}`zr*?_HbtlBCL$?a&e z=~Aa7;?&%dWE<;jNiO^Fnov_WAxXFeo0qcSgc4E~wb&U>t!)_L})PQluVHxpr%TPnPK> zEBPLk?<3>B8yqSc`>S%#uXZHmS2U!Na#wg>KHm^FJqaG>4-ML9OTHAc1v8*%OeNU8 zU0{8PP2&y~zv6F~d^Vwoi2kQ9m_3xlAIR`rtvQOAN0(!Bq$_34O~`Y>pA~&SzD<(S zl4ul9Z8A0MxVSyat1T%K({IZ9$=2&a&q5*nvA1KQ@%WC`;$faT2rm0$)J{$PPr~l2 zgq#ygsyJ>ZPvm@PyPv0WLEql{rMcu!8rul{_~#X~Q4Q|ao&DM&Um`6`xiQitWWffn zQl1akj#yAy0?|Anvc0_@@Z0SlsvPHC#pX$s=<1V)1E}e;zRONA|2n^_N>>K7CuUp> z!_ISudxwXV8_zN+rM~>Jn?Xx&ZK;+F6LjXiH`i_dp(-212?NV_mWf?XRbMeL7MS=! z3%6eP{t}&bk$RKeu@@DY*srWvC1WR_yiimXY;N)q@Ps%5uCdKG5(p&~L9Q4Qq;TZa z(FyU@yDFoXXGYfh#Kxb#&o}uZlgz>$U+_Nqm&1PNG6GWzhq3d*GSn{rdpcuJ9HPmN zc5{!`EpU@~r1Z`}d|ZMP?XkbioyXbW-?_efgP6t9VHL&qcJ&9V&$25&wnLa(J6B(I zUAYF=6E&%ffQ)v`<4vdDdFC^hxs+UM;)O0hAXI)HfJGBBaM=F1=0e3=s? zEy<@MZ~X30&r@572v zO{Q{x0)E6Sr#>ihR!xHLd-Ejeu<$xj)uVNUNOZjukQY^{efkn>a_`e;jE@1q({YQe zeSA$5DF_IOAw^UN&@zhWsRSwMD(vZ=CZ-jz7mp@JHoW zrPwB&e!f=!viR@7T!G9Qc@pZZ_FInd-q~c~U?REdb6_^lvUU9nM4p9baS}%79Zgt| zWAZ1W`*p)GPQn8Ba7dem^DU~de;En><6>dquVheKhN3U$Gt-w8-*Y6cJ!{nehic<~ zf3K;KxAc3b7~4?G`D|LZ?KbZ_4*Y^#`yFf1qao5hR=DJCwC6fC{y*N2%Evr@&G;g) zpX^!nmC|b>vQitIQYDafPlPjN0ehU1q}y_;nPy(O(0uyi?JMN03x`sHmj2E%@lQ`Xqc*(p*^>=yNDRn$EiF3RpA^ zZa*Ih0z%Q~NQ!CgH5)DWPp5-{+@c<-frgv;)(x%b1d|Q7dqk#=;PxEl%@lJue$8M) zaig}6-#|vas-B5%A=n3VS7Z4)v9g+9+-?Qi2)w^_8GTpr1HD_P;8$9ME~oX1 zPuMqFm_btO@qJ4Z!7V6yZt_W^RcZ!6naI!2pX(<6pVRO^?ih@}{FJujf=+qzk%3$Q z!~=1$AM~9>%oT6cVx=t}FAjk~TG*-TskdrhSP%T3au2$9lS3M_7pYpE&f}o=I60ya zAg;2b+GbtLpEI>ce%we!P5<+@Vj))`Il3${~xN4a*EH-s9rDIpEFKUYAku@?ISe}w{LySlVfn;1zZc|jdZm+FB5_=WL2^tpdzeBdntei-_VE&Qqq8V~ zD5*6f8IyBs)B_2s+Yr577K!{V?n4FShU&Nt>RUd)V@ffj#)dyEPPs|g*IBF^4G<+#1THH$?WS}C$(gXxyK0M?!UctpKj&1obq^@szW zuuebBU=5$ghhNW`&SsiXChLDiiA=aOLMb^qwv=0!OjHKgD;O>KM*rN4Bn?$cONEPC z`zgTy@^6v`lwKb9luQm%-CD3vkG4%7BPd|^hl?UXUXXw^m4cUkXO3r%b}?5fP7T{* zz?qgA*^Vbq?*r^_l`P31#+rU3d0x{{{C$$RU^(|kBWj7jPsoXxnsIi9vP2~cNM+_( z@YqoF;tC_pb=Omyv>X@f(Ck3Ed@dO=@Rjj`03-t{1%OKbVfS4`l*;1;|7G|4!O&3Wynvl`-Td!GXe?|E zpu*427}#S0p^%Kqb1V;-JG+U{VD`Pr0f=4^hSUhGx7`oPc@57EX`N$bSs%|VK^fzc z<9vydZXT=}rxPv(Z(dq?kbw^bt-CX%W(Mj=dqHUuXij+~KhyZ!&>`FKEL-9jEzt>P z-x#d3bQj0Mv0ND#_Qa(jT>^Y{8Jwd=;ON<_hi+%Wpd!s9Y$nUXkQSB!+sX9b50#bYc^O3qy3>Xj2aGtkU2`YUCSg z1>1Eg()@Tteu0wV9y!Z31z_(0atPohE}wTiPOvV{KRW4+luJv=HfYn)Z`2;?ubCh8 z7oGL;o&cl7k;^|0a0gT%URqR|y?%G4FnLR{%Jc7vKG5~7K&>#&cExh$&6}XFFYr8K z9SS5kQ<0AyL267EaM}gYba*hstH3EBVaw`N94tc^G;vnrz1y81hXhy+!iZ;8Mi1WD zDVnowtJU9`tbf6m<1%YU|-OK~P1zN40JE3J=s3uWS9rbB+%@_|4Wl@54s9q?s28f{o%Z#1C;Q}v;f zYnw$LvD#2OAfBSB--88+cv0PxVR=<|Fk9|gSB`GFWg>QtvJVCU$xS&tN1ANMPcNB2 z9{^>1*OO;n^0tCu+x+0_!3D#H);It7&m>TSrMe>CdWVLuG*5-{RcGJ93|sxX>e>s; z#ZhKwkPlk7sR#)JDi!p;u@xzQ8^dQaQr3A#esEOCFyLEpqyJDH`IHb0UQ-7Zm~UIa z6P0SPv#vU-ild*JM7{tuIts`$#iLD;XyBb|2antg#Hg92N+95xQn9%@M6zBMN9+4= zI`XSt02@_S#H(r+sf{6ZK_eMwSpuwz9tc$);Oer*BZWK2qm4n=dsY?G**5*6Vj-WV zy&~iMYESUuE>CnDAJ00_!vKKs&&@wFu26w4-o_VlmU(!xN|$eCT3nSFPqwR_UyH;m zbq(`%*3Vt$m-SUk8x+V@vg4P}$E%y5N^{2F)@?Wf-o9N}#-A7G@flOmUZB{cD zp|3|&qg&XWx{&wZw10=~DRIC`IDBoO6-y5Ynd(XlEe3dNO5M{a_~v)IL@Mo0nN9Af zR7$kaDzFnUUaafgJcyZh71o?x_74NB5Q!OxxZh^3hIDh;aQjqOlWytfpV`^4&Gn(~ zFZGPP-6?yULH96_N~joX*4<_0=TS2s_E|AcQ4%hP7&6hxTdy=D5tWkM$bDl21C3p2 zreelst!ZdOYr8cu+^zo_Q_Q)fDWpMahbo%mVn85^H*81GPLVpTz~{0sYObR z@GO%gN~QFT5u=;L$10l-_rhTSspMk)X`vADV(V7p(FTmYuTx8E)(&OP?6|l=TW_PK zD1RUNZv{f!tVK~4aHS|(lPc#Mi9sBq>mP;m;}f#7c8J~XMsC~eAE{_8`u9PKHkaRA zfVov&ZY$C@bxSR#Evo#zz#AGGt@G|V&e8H<>oT0!Z3jn@@{amQ!@%1tk){?G$C+L9rY$@122=M>Pn3_#5qQF2{O@G|hX`wYIj2J&C-THS>F1HG zkRG#_!LL&YWfAUEDU~Q=#F8TR0@D^-ZhIv>vVY6UNHf;CY--(8Os42D9ERoo7t!bc z?U(*99!RN2z?;0TXqDHqit-lodk_ot4e$tOq6agLp$#C^V3*(?E-@Q^zNyR4|Hkc( zjKSQu9Bvb+poMXw+Luw%cA!bk9-%s1;Klw!d;DT2rjG_wp+spa(hC1fT&0viAQ1Au z9?4`2rg^q7M?Ok==#G~=&PLpEp$qrsaZv*XVNs#1*e^N-AxU+h%X zNlHH+wqmChnqPF{=e#V^FJPaX5V8_VuU!k`+3a+W(!5dw16+*{jR}li77u2)O|BC*lT9Re1Rn$#P$71oH?R; zHnbBr>Y8n&Wm@Z3Ny>fhTg$L-ojp43H9ZKu5Gz@|54-rr_=WA`4M<$XW@7$7REQyK zTMMKNRyr=jvE}mu&4pr_7<1=FR^1Ut`M!wnHv|9Ww>`oQYBQDAG+%xx<&GV?wgj2- zs=8nm(yAP|xrbGAyov8~7Y#}_l@x`$MYS7MBu}>?f7YlJ0+=4Q-VwQfmHQ-?`sTqg z+=PBeKq3-1pD3o=R8;k01Kn~ssd))!<;{Ux80Uy)dNY zRE%yGs3sJ!$TOIH=gd9YRN?ZNg$bk&Ec9`sQop zXMcCR#G9@R+cWkjx2Dv_`gI#$v~$K+Hn*8aH=QGY8qzgY=RUUc|6=}L zfZ1A>m|M%A={G@hlF)zgFh^T;AbFW3=yt)2CSEJwA-y$EzQOpMiqz5396yVerKv%2 z&A*t3Gm5`fU+=lr9SCw(kWafm=q8*1tCF!aNkPFXY`cC1o?e8j3B(@1rS}I^y**&_L`sTzUp#_Zq zmC}TZ>-kgqD*hETGNr@5g4J4D8R-vwqbA^++=&Eqg4y8BR6%6M?^<3dDZqm14t zAqV))(5QKIf<8c4KyOfT7K6XzSiG$#QOk*UPA+dhaeN0>MI>Fg*NvOQjlD(EKDNw; zRX-tv<|1Gq6}gy{Fn&&T0!CU9qc;iM1S=r|XY1K>dqh(+yy#zEU$>l5RZ?3QM~6Z$ zL&KzMl4R1#zqWa!C{+#M9UeCw9yiC7y%fg!NIzYgLe5H-HUyb}57E$&G@SY?7Y`9A zADLp(GU?ZXW9z@pWpWSybA5rxZL-l=4M zAjl_8Hp(*68W43|@pqr)a?kI++6X+C1t)_?QkwMwK&dD5%*xMPrtB+h(oHKWId`=3kW+1hQQMl| z&rI&GjLCz0El+r5R)a`MQ=mm`leVxb4z=vQs&a#t#qhqRrp?kahdcf=d1@Pt77Wfl zsQ8ktOssO|L|SqTQyNzq48xo@fwb7E8AMay>LJS)|_>>4i$b?Ez3;$l9bdJ zN&K$4w;@!Zq2f~>A6{wMC`fDgX8#n6698Kln?SMyYQ-XC^SeeSvNTgeA8*>-wBPm8 zN3?QxNVJ)?pltvEE&ikv9%5^h&FKb;+fV za*S$e-K6}Co+$Ngs0?;ye+KdUDk>&kn|*b4UGWEX-F=OF;C!{N(s5M>o?m!}$EkOJ z4ih?rPj92M^%i;cxca=&~2gk{VCFf03iOFVC<&yEWzHG8$i7QR(R8jbY9z?-W;3c1IT- zw;0r+>gp8wy7CqXFXl4MF-sc{1SEv#y9vF^UD6U5EUg=Wq5#QOBB$P4H69Z3nyu!@ zC(h4sGL)3%H(WaAtoscKsh_7odYV+m4Q zj`nn9yjcs^fW6%}hDl!&SStg*@QmT202G)_pdeM$I$_*e9m@LV@Bz zpCI;cLG0R|7Z*)dEH$HvETph-i*}5)lD1V%jvJt(O{AWxJ!vovSd07J#VkyezvLhk z^d&AmUX{n36XS$y3I|(Ne2?BV&i5!c%lvtPt9#6b-=xD>ooP*?pQ#wzjNtR$9|5NC}va-Sm^Ozg;rZUhCbPiyzJK8dkHaYD()~u+zWI zhsDVf&D{h-x5XTD4E3=|ei9w;%#{g5yzzfq{%8NT{9en&wcZSdzN^3UiVVi9OgfKE zhXyqKdY7FrrfM)Gy0Fy8bV5_=bpR)~@;_8F!Ss?yuK8m&7L00~T^A=kD8Vs+qjfn) z4Zo2@p)r{CweZ`HCeU0vNcDPx3yzTHk{8tmyD$IYQUNW5YY$p6kBRISG$l<1j z+DCl~X}eWI(uOre82TP*_!sAy@=Fn;f|t4q9!Te~3kbsnJSHur(!Z+Mk{iNWvWh}U z_qFO3O1cWy-v`U7-#?;AHQ0HEd*pDFz=~QKIYN#Y3}w=Z$Gv`#!O`9bY?mVBaF0@$ zNsgQO30mB?Jz69=>eTW&zA4lxy@E+6{Sbe_IQ8(DZ>h~njr$HKIliKA%k(ZaZMUg1 zvBHZiQ224^hSk7LJ1a&sgDMU8tiv0nW9@LlX_M)0C8Zo>jjWtgl4U2Fv6mK0x9gJr z@GbxfTn&)sQ+d`m`Fx+tCF3Fo9klF=EUJ!;Cmn;#w!C|Gohz+~ zShU{v>r&KexB9p={DOI8*__*Zye}y=$GAAzQJ>>WcLzzXC?T?92Q=yqJon`A4CR4F zh4DH4bo-O0aJ;_stUNQtkdE)BO^7S07}w1j!#_n%~wJZ%Ey27H1=|`ri9HQn;h6Ia-nKvF(QD(z|JfTb_Iyp|%TVK8tpMj;U>fOy(J@ z3yEG3D%i$@X%V7+wB*dQIh*8`pH!aw)XJ@u|UJEeYN(*O_GF2 zae>OX-8H5?A^n8nvDQ#&L>1H!ZCa3;0uh%RZhQZ+ZkAXlJ>I-hX(fd7x0t_TQluXH z+@a9mB9G1Aze~_(Pdg76*n&Jtic@Sxvi1-6YvtL8b_n{n?r?|HO4wCG%K(67!&^5I zu8BUXxyGMbIFnY#d7P+e-o0e!<~p+X$er-c?cfIi9k;{y)(l>tGKPz<)f6ELYjaFI zIcB9kDOu~45wX5nW!HP2r^)U2o~^2s4qz+(=;9(875W*%yf?WLgpORw?L5%1e3MzI zf9=XO-`t#w0+a_nv%@o}*|hFe{2VmdO#Ph?cf;_5T^fx2)=`&}k8Kxd2+!8{+hlH5 zo{N?4LsHJgBvDf>v4_b~F9|FWb!AHs4*7h4^qtW;Lafn`GlYS}EbYO*;1kLPl122B zWSBYm@~uef+-!JQtz0YIuoVRddlZZ|g~<|Stp_X96C>ivQpfFkPAC{Zb2CEcM@cq6 z>Gr}JaTVob?^j&Ug-1IGa2S8qDgnk!0sSDsVrN2#G!48xPXaqdt=_}W?*|ZDRjKa3 zQIVjpmR#^<-_ly6| z$^4V4xg|i-5l-=9=@6dqUtfH-{vz!9zYN>el)jViF0OY|%mE}Bn`R5m&Xpi)Qy&*;pLhd6(=hwrzFS88AULnU`{_+&-w zKQWpO{b}}+)=Of>6T1wFq2QFq`D(ed^QxV_OtVJCM{C`84_p4St=-)TKkj_x0&u)E zFhymFczTCO7OQMt+${T`&%C;4KaON~9&q(QtqKNrRi=?;Um$YEP3Q5m`PP%Oa1-2K zdGPILA9_M!5}txOK?nC1PsoYl<%7Qq80sGHOQr2j8TdJB;&q2b z-hI3*3(2j6bvLSQ!U~!5`*rrL8t190`JB})Vc?X$doUJZ^C2>s;E2r1;&hLbe`gex zJJ4#jZRD!(__c(pL|~XD*CdD=^(M4j1(4|{I1%pv9AAY;hg1A}**d>6AH(69<#4=v znI=C!+VJv6diqE9ysp-e@fi!PJ*`;1uN?U)seLct4}P@+(?3zpN#D9BEYg`hS95zq z9>~5nk?^a&AR&Yt?yZc@$Jyl#QF9*PagRx<@M6rOnt&-&;Q zvXyjv<40$s494@WQ9M&h{(j7$OaurxZyEKA*zE(E*9$LB|4bp2Q#UGb?DBT&)Y%JU zq%=q+cN7qbQAX^XELBN)rg+&})1^t71vzr+gt(^To*sZwcclQ&at#eB`Ds(0De(;h zbv=@yJbUd{e+{TF7GUAt*4e_IfvxyTm~FWvyZ2cfUJJI2u;!y}-vTWmr%4t?o!#kyXyQJV!r87WM*U&FpGVmS+pkK@vdz z<5uHao1qC$tN31qC-EK~gg3ZJvqr;kJvMi{DiRv)?$lyCt$9V8T2^u7R&ODb)X#zs z*|jUR9!F=BKZk!dNpC3|1Y?1Q7$iG7^1})p$u(sw1cG?21&h>Xx5^gbOHSma2kx)YDq0L$1r zCfnneQN>kgJmip#Rp=5{_rPaxP`A@cRY6zRs1_7f@ECOC8QHJ{NPcmdWpgTVDJ?c= zt=08`K(r{JttB&j@o~>mH;KY3{GOFFQSut_lPRbCFXQm$(7h1lE9*p`%1b8!B0HwA zHZ;B?<%#n)1y$Jp(n8)tz$!Xgo-kn-o{|{d?$iCt_PL*xcNX^%1xrqqcnheLsloqN z4Ipza6Q6;8T0L8aXu17WxqFDx{YLZ$KaqRZvpphBBv425T0O2n{!$33Wi&Fl?Q-ON z0d9;%HO+zNAF5A`q|3Z9#P%OFrzU&uE{j$dy~s-|!}GFf<*C@Cmayfz1#tkRbLCUB zo3`kQMpLuW9V=OE!!+g?3BqQ6Rc?%fv|w@C@%OjTwGKc;E1oT);?0kdA6Wy@4cnw9_AxcI-Jhl zYEU{`{zw+8*}?7%G-jg$4^8zo)iYeFSvA7+y+a3drL9-(-zxCmFd)-Bnq}!GqAcil zkE%X;_~dgol**_GIQa>5YOjs+|D=r@VgRd!<2c8q*&p)|p~%7PVQZBf-01!~ClBt8 z5wDkBcAGt`pvP>@=X_P+d-VPs^D{LBjHaZ=Z1w>gj;{y!#3DcYX4d?1T*2hsjmhqs zIf#z-y^4Epl>kfbmq)2;b0^$BvmYGQIs#gdS^l6ZevHNh799r63V?8IKl=E&q3Q@N zsg`8(dQK>a-aAoTvIKpe8xC}SQBKWz3!dux#_;K&fU3r;Y9CC=)eH%#|nMP*MW!0K*U=>G7z2l*RCHqiT4GL zwv=y=539M83DyW&20X03@a^vX0rkz{G~2is>^`^g96P1$r1zjnDbxJ;g3pfdXqrrm zh@tg1ggVL4u%Z3I5S%78=Gjhiq(Uo<@+YCJ3+;UA8Vam%l7>*i5kIjk1y-oki=fig zhNWj9e0-Vu5+D)R_sWYP(iL&~*F~+RjjBxX#^o)wI}4_?*BxtXB&HO15B;&=${Q4- z&t=qIV7mGjtYGXs;Pm;zNu7712qrtxb*satA^TrlW|-FvuR%AeM903+sS}hh&xH&5 zDJs&quiQ%m55B}ryMou0sIx;HW?G#qWW4*FkR*6|F`9q)XVq+3$otJ8&Al$JJBkV3 z{K(PjV|B6T!>2lmUreL!eFLKlfBH&tUn`|iUO(oP^wJ87%^|p9J|$e*Oxnc9TGz^q zy>F-wC+jhka40#WOrJ_9I#$UGsGm9C`?2YWNCwI31Osl;G2Ugx#^=bNxF+RQ zzBNw5va{49^U>e^LQ7Uc*!fTBZKr9H$LWa4LpZ4Ed?7(Ytm)m8`{W$|;@fP#G{(cL zSDOyRpF#_o6EJ~?8Y5S)u{5H^QPzEpd+r`VJ^Z{WihCgsXAGk<+T*{~W?KekG7t=e z#1wvWp?7|^+M)59ync82_^!;BMck;ifC1Q5hD8U)Bxa=En!HSeor-pW(CDIHr~Xa= z>aSPJC!NYqjY)O)H<7KsV!5`i^k>)}0w?Swj0!AmOeZ{jtsztW$+!M*SIPc&-jC+r zT(6Cq?k}fr9?U5NKW_Uj6^A74pf=COPsI;Se}uFY>i48g_=(D%8haXL85)W- zTqG`^O%h08u8iI=rZti^OfiP~u9ufhcYvGQ!#P;dheqSw+Ja5@x=Q7}qjqII;~+Wz z%X9(VmoxQaJmD3zWxr!hiKAQg>VeRc zfQr%P^57TZ>kOe~5K=1OxSA&Rlr~LD1UzSAEhJ`S{#%Slmn%8#4w<|6xjx0cJ&vm@RgH0PQ{9E-Okliyz6cAYXw@W3g`0! zl7zd$02^Iy@!=cu@?Jn!UKK3;%>eUOkIuFudU>d%Ja7VSrtq-&0zhrvC?mUQ>!n+}fq>$IB@_lV31e=|*%we7wfVrV(#^w) zPyNNc@u@bbw2j4sM_!9NOHFvLb2TaYN?wQzu`xS6p5)fT z<@3@#eaO5de4@v}hzoSD3m7c$SLDj^$Gp7nKzll{GQaUjCF@7E#d7i(!ZVsMj2dr? zPa(u}q{x&SuNeSa)iTnFz@agNXoJ?ZU&4MJ6mU(<*c*1L7G3wj_$NNI& zrm7v!uAj=9XLe}nRrMCCqn-h?^IM5e%~4146GhbmCSTjzI7l1}M3^AEy4?+%7(N(wngxSWd!pUtJ9h^bZYmb1)yTpiq8#U2BuOma`$q z)L-M{<36ucSQxsL%c5CSv{}3=zht(uuiozt_)t*dr%CkiGh@{oS{BcLvLZ2e;U#A0 zb;jiW+hDM5_eWE*X`5V}^{AVcS)6B1?7FV!eTN&k3;krN@s_+7NH=SCxf$0ul|BvS z0#X8BBHHmelCD7&k(z;r=Y9{qYtcId%F{Wuv-hE$iLBK}Dl1T}Uaqav?s>qu#IwIU zUEm^69{Q>^IJFmuNa5rJDZ1s_Cm_8c@98ag#nYfZy4P`A+T$_S=xOa zqX$JgFF)Acv!HIEYFa8}fAoz3&*~2~W*k35H9fdx%BCZJ53#Gh2|^QYTMP-Z#8x!p z#9nM<^j>0hyL5g6mV&bj;09gdc9(U@6QTp%bU%48h}G1ji|rJa-K-KVgjO2DVncnh z9HDP1b$CZQ>x_XE;)7gt&x$cz$#quV5zDP=_qB-= zWg)0PJkJw;O%>@DRmH?T4a0h#zj`qtkW};*%<5d}manGTNIf_qvL`z$?p|du7etpl zP&}-W)zu&$Skdc#_nP1+la)Eb8Lwa=7!AKEls6##JFSB@)@{g!hvkQswz~xek55IV zgkf@(e9CoE7fIeI<}4spMfOMi;{eG&>vgK`NAC59Q<9pzF*@^pWI{%`X$`fup89hh zQtH^%J+7BF;BC~h)->^TnUg2m^ogar0D(8fBTK+>epFrra&widKaCOWsmf0$CZolaLU-kbHdZJ%$`xU{LZE{F$xKP5j~O?`#6VKq5Zsb?*h4B0Z~ znvB&d194gUBZ+;N!}EuJyWmIFI@Y&m#sK0%)DRO6i)4n+P=f~)i!Kr){SgLw517zd zvR?GbB2sw4%~p|W09tSpnjRCYFA zTG7^ElYlS5pV52OrZF5PzsyK-Ra!%$AU?IMnq zJ>|)HO^`NmzDcs9se-3+}_rvqwlz+?Ts%VzgQ(5=(j;c&EKJw zs9zYOaV2p$+^mUWPpfd|*y^_?)QYaeC(wf}IiALd@y7swW;Csj+dL%NiSl&8ue?@8 zrM9v@dxjz;*?7f)8lX*Lh0m7#rM7!x`){jN=&+yLl>P&#&;C#$O?E1UKpqPU7GjW+ z{mt0OGX2}j4D3Scj%}lKaD?^?FiG|#huyS429TY&k*xaq(R8Gox`gvn?(@pkr|+r$ z5);z=EOq`oOqPmQV2qYEdz7#I(7(fTS!L>}tj260fcX1H{`9=6ve`lgsG{mL*8BaN z@l6GSWWV@egC}A1vjpO|I-RZn%#c-YL(Pp~bzxV=sz*MZS^4T4dmY`HcCkP#iu+I5 z50p(0{$QrB@#@Q;8D@#Ql$cyeQ@J^Y1Ih%Lf5FV@3;t+9`$ge#Ymcs)zgi!pKyCNO ztiUkK27fSYXqnCSv%Ku##6N(~;xW?kz0?P?{jbJL6$@!3BQ)TKNK-n&AUBu}(dEt! zl1xv+Z?^pJ)XF`I2W@+F?a+b-_fx$n+o5z-XwQFh+3>bRrnWX&v#dUocR8UH{}N}qZe~wiHFQ=7}kehw+Gz0 zDFSn!y|AY`NRqE~5KPG}ibR~5Ck9QZ-(NQE$4zwGI1Hze64J~hmT=@bmvWPWVJ7!L zgO+*$vc=saU+o}9yzA&a3~UbeWFGCPxtEH%p-vjjNcMe5w11nf!sY94WRNS8FA3&e zwL&FgiUKFJEUc3TpR5=OilAQ)RCghH?@~Q&uH=lAV?6Z-hCE+iiU3~-x|{|9KKouB z?y!*ezthsukBL_X<-1P?kGVldbOQR-*Fcx|xk|ivyIW*l{Ru7kqwtk2=<5Vbfto+1 zWH<@`rkqxCIypcFJI^gHHZ_z*gnW{AV z9YgSu=uPAH*wwJ3YA@IwSBt!jws?4pt~D$kxNqi4N-%i4BM8MgUOz~X^>P!9MO)Kw z*-e6_1TwXak4sC#QvknkpZ)ZNCx$GLb3qN71?iuFj%q4Psj}8PWEp||Qe4S%GXb4C zJpR1GO~`zH2Q?Lgc4Bw1g-W+aosjLP{o%OJU%wEouF=OEnBB~UWGSQHrbE`m+AJ-6OLshhitz>sMd;kYpb$O+TOlynf&p< zFv^KboF{dhypjN0D@mLYNp!2Q5Es@h@a-fbx8s)v<1P}85~$znL`@%ZxLYRQA_uXg z2BS&HcW12}G&h1jT}pu07+2Bv4?yzuXC70D=HkKsmmTfDt4?;44)4`rKhw|e6=?(y z?{@R5uAKw+%wmFGy^&+|!qVVvKQ`|#lhs5>i0gb`O3uYt#lzkbZgOM28T5UI&E(n) zx>tl_{)+}?cR1$T*&lfE^Nu5&GtB!C(5CM5_NiFQpqG67F5lBc-!>7nM_{2&2&~doZ19>|{azLdRBqyu}2c55nGDhv&L8kfy%aLU7mbMyrIx zq8W69`>oj9Wsw5lv_60Kl2nmgGD#Vh3?X+WlQ<$}WXVpw%S!*_nW9hrcktgT$F2?xI@=)OiNsiS6X zpKnn*Q0V+zB?v{*S7Wh(wCN}R{f%6L?%QS8cjNZ|0Nh@3X2g2qYxqVR1Ridz2r6FZ zs5VbvL-{VR&2KU13A=YLj8Kj_tBCX)9+@cSWX+1dT#C;(&5}4U-M6eZwx6-z9M-a> zdfeW%e!?%h82W@IP5-XCn<1>u{)~WC_q)fuSz~=>$l5CZ1^}rnS*Ur?TdRTXt)<-J zY0$|>e)eZLld`iskijiUZo0`~)@bdE`>&KMT*OXxIP9bo(-H#GR3-&HxXsof8jcOG zhxK$H9liBQGR}RNe5x%}*oUyg7*fVVBUA{MSAJG*z^un*oAT@QkyPPGJIPvBa ziVF9(KH(aKV!)@Sj(n<&^3)Z@r?@i~xdMeD4b548RMeQ2g*z#pEM#131$d=b`;C4y z_<8ok+I^=(qinmp>#$khGGoqC zwCo#8#1`TF-cEFJS)O1%V>P~8+O>13I2JtjFg3)CGS=Ngc9o>AAZ zGsFC;sQ*i>Aj6h#L*Xr^)ppnU_Qr+-?`IeSLIHz&dh_WbC_&OLK3O9(Kf0n%He29S z09!zXdhJ(0&|3Wx!o-v8mdB(nxQNF7nFb7m-}~*n?K4>xjXIiO2j|zB_1O!*x(hMN zM+q47G!xD$Sq7x5IQ@p`!Ur|6K}7v)!O7=Ww^lb#M(}$_uu+Ouec2H6RP7gN%K`!m#5HRg^hi`_i5_u3A$Ii7}mBUE0K#xC!%il zyTxmDq@EII-)(R+FH`&TJw3wgY=#GndUz^V#<3o4)68Op3;h9;fmIL*$>$CE>Z)yo z?S}z3Deyp#`xt!onSRi4@>kifrx2olWsp(w5YX-`N0Ora#7N;}J-U8!hvc=78pzC< z^=6$zo2HpaQKL+_8Rho1d2c)4AD>tIBvBz9OA`0bNoHkXQ3|+o9*0!Z2gXG6%je}W z?sWG1@;BIpzV~&E;K#9&7369Sj=NYPnB2;m(^p0=DApeYP$m~PE!{D zX3WFR5@p(n&BBBMlC3%1B;9D&81WL!#7F5<`!hpK?mL(Vn$uGO)r5yO@-x(8vvQ-J z7<^kE1K#{Zu72!|n#@tiZwGrtvh+XeDlhblq^oQ8eKYDliU#=?q_uOzM^R(TZKz7` zD8Kk(Lms>-aSaqGTwBRJSAM3Scvjvxo(1KDxP^nl_pJ0K%i-2PtAtpVj|sh3oW9l1 zh$^MeeCGRx%L{aW75=Erin z*@wu^9fEhmfg)kZF7#5$bJCH%oPbu`QU809_CR9OkW#4hO*ZVtsY|ur?Iy1U@=D&* z9qKoqD%|X--AmOp+c4&X zpO#lVPn)s|2~u`nJXm&b%8sG9D~p9{cpo2E`^C2v#3$P-m?o>iYPs`^Q!|}yo9|0} zxMc$rs%Z{Ze2Gh?WWt9eAUjJ2Mk*%2}V`ZzAs`JlLnW z)0IoomGdrsoRg4PRv;(qa|m3F;?R3iq!S^YBaK`RFU2FX9aHx`EV6d9tWSi8a&zpu zeH5!U=Zss5(t?e6T#M6F*HQ~y)6}w|UU@LnNqa>**$dgPrKMt~uS!OrEE=B~Jy?Wf zYb{#>X%-9MCs_j8%>me$A^(FLr>OAhoGYXFyaC{6UToo^&^illFpy}EDOPf|5z94& zk@Fdi$n#u#$f^FZOp#*cK5GSYx_^eZ#hBd6QXUHas0;SQHtf^r`40tFJA4>Y2@)w*jC-5RBl%%I8`=9_Sz7IjIFL|7ew~4det|NJv|89AmRJ-ZeGGTbUAq0bo{-?Tj`fCbg&yIEX$EbX789z5%YyV zQ}+ez?AKn}1Ml6W+m|~ip4!uvgg&(579Ior7exxNwZS{#vhcWID#kx0o9bf#055d7IZO>UR#@>@)aEKQGg(?0->@Ipq*XWGq` z%aI-l5vKXZ!Xq~#P-hg%osFXQ!jrF}TWuVq1!A*Y81fz>QQ6@i!E@Ym%}%-Xd?Il` z1M_F))cWyF(>3mrh51UIQ4MG$6Wk2W)J(x4-IqLQVyU3qeLsD-3p@#=9UM(^H`%wa zik_nXspSwqn0CjB5YImSkn0x3Z2yblS#9;D5IIK#(%aFxANqzhi!#19c@z?julXc= zMLT>?KFXboT#_$uN_-NMzfdh*u`xjMQ1%(6GrnV(b6`@Wd1@Daxat(e2ms+9rjHb$Hu($fUpoBbsp7$%ysF*Vuc zfjPUEL$_ryTKbJfUpUmm;Scd?fALU_sbU(jS*U zD*mf}DKbarSJUi22CY40TWmD7Rth!w)6^{MTN|8YS1)JtekcqC05x31Q;Kx#d9%KI zykpoc@w39G9_ERAgO^qIEKH5u=iO`2BjF8YJBf+#^HlEq&hg{U!$Tm@T#Yk(eI{i| zohfoY@HG5Sz~B75-&;hF(HECPnzVqMPV6+OSw(^-f^pyhExG{f$L9pPzdMm4_jV1RRqeoaCz@FPfZ-iNhNNYnABCoc- z+_PElfAae9QGDALXNZxQV~2^08{)7++} zK@!?}@9yq4rv={&pX^K~Y$ zV72^fS$^VEBY&I3u>R`2KV+7{F2yNpm;dZlqO#_7lwXB*?{_-4; zGyS)goClflmH6M0(6A#0Ga|(ex9Je|PbFygdbdm4=>D$qOl>T7tT2U}-@L6bA(7Qj zO#{b&mPwLcWtk6;DL5|kvU4py(XRB!eRY#Jqd`zIr|E}pV@qSUpUCdQrB0V8Y>TX_ zqzRc(IyeS+U(|zW@*}SGrwMd2pVfhAX{o zGUp)0)L7fDvdWr?!_07l#sRJ~vx_rEgdtMdgnaRL+nYM;p65FrNeTQDB;=6Oqci)d z28DrZl-WZdORQQGmUa4`v4$IE2O4d)e`$(E1z2K_;^WixRu@gO*!a@zK~*V-**TUr z;J?{)QDkB7$+Q@5kvC$b+9s;qaw!Icu2%r%xMVY&C_Ot~Ff3wr(@A_CICbTARax=f zGCbY>^A{-N$uLPtIoV?QY0&Txon~zuF7={4E6BZU%gc#{M3r>8e~sgmIIZ!x!kJxV z#EzGiSX-%dxwer*5yx5xuSXTzO;}YI`evtKNfwqij`YEtIqToCd)m9vPii7Aq{P#} zep+4^6a-gH5m+9LP))4WY{MtZQvwnZGiboeC(5h*js)?vu%ToL45^ z;SAj2e#cNEfY26@vp^v6!|l9V{zv!^bDgYO!`~k~0mzn4dG)o$=Xs)eGTy35WmM{uo480fIOo48Z)4yYqzk-U z=ib4;U6yN_z5_E#Nia&l%r2%*SXRlC`Q2L2(IX}QsbkO3+d*MFWzS5w4aVek6|+5F0k^@p+V_+ay$+e>#m(;7jB zLY`?*0g~eGgN^gqnO2Vdk`f^14J)hIIwoU|BSdUqk-M5o@BS0>XJ*D<*9dd%>y<5S za&?jm=odC#o!0s<0oT|h-%V8}6I-lqiUqrNg}*{edMZt_IizW)vJ`a69W3LQ=3%7B zB?0mZ7V9+lLtcR7Gm9g4w;;F<*zT+EF=zR6Ck@>p1aIb;>cAxCvn8kK9YBpP|ljP}RjZ0{`?S*IHQ_GP}|97mwQVt#X5Q;cx*w zX{0VwD8Q@Xh{LiPYdAVOI;#eKCF6P$ubw=o$fY#2ZpvAiQHO;kW;}P7q8N7bsTH(PnqK>sd4BrQcz0J>$GF_`o!@uR4%Ff1qs61*q=uz=IfDP7&>k!I5 z2EkP#jQM{%G4<3_Nq#+^xviYUfIIeyQPAY`g&$!V3L*DxOV+z zX5i;XWXzx1mqvnaKjx3pz)W~}iAbZf@$|3iJ>*e!k~~v6@MRD&IJI9#txpkWbevv& zJUm%H_GRtjQ=~TZ9{}&zp2g=OS___oh+ot8KlU7~vwxB3;*N?;~bb-fdM%rxdx zs%=_puS;T3l79DtAMxa^hX-+v*Bw-Y{TavXP5q)=_BUPSl#E045XM2SUvhPG9jqBP z9$$nO`quyeW-Dzd`?qktRR(9y0S&xJf!OZ=O8K6_3AgwZ^mDCI;`=ru4}ZntI`irUwP(~Y>n4-SYqTVR$9RhyfV1-Rz# z8U4xg&(syTtDJAL&}~Haq)YYh^5d=C@D0xOV;||XabEoZRa?3St{(wh+m_P$ap)?u zatO?2&4KmX(bO9&go`n0YLiy8#FQJxd{3!?DiGZh#SJmp zyX;7OKI?Q=|J|j-bl{B_Er7h>$H!Qb)x2Pc&5yjX@+c33aLF}Zbh-wd`{WQA2%GjM zES{OsA#|s3#b`CB2o_-4D1hqb%N6GN3x#rZp~Q~1-RVRAOb4QEVr7zbS*mL9*k)OKk;Fobo|Pj8;W>F#0%BoHv)we6KvIp5 zaU)H*W60C@qN%%~+n-`HrwGAw@Y}g8U}Z z69Vw+i-V+j@Z8yupM(C;4Yr=p>2Y$EvSe+Hw0nmM4b9yfgV7okW007Rd7bhZ6<&Hrn4L3@Y)zmDnOEE?)S zA%f!HWx)@z%zw6Hh#6fC-C37?cYuFc-+jro`ZrKR-Zh45=Izjp`{?QrgB_l87ToZ` z38mY=t$~u=e)XBj_8-99Ajg%IBIy;jO34x5P;;VdFb-PG%Oj}2L%(IYp z!db)ewNGO-D9wHG*=|d1(VQXec4BaV7%Cr%V=1sQniL?3=f{WBOqnvTftw!b*28;7 zxQ**|*cCJ(iSP&1cbV6+s~sQPGgvI7fc1J=y#0F3Y5-5|FYHNN3BGoJ2$2V3{jDzG z5k;&L+0u+mjdYctH~qalK)+)zt9yWnYV`y)VYPbJtBzKlMnOvsk;_qvHy{Bgo0^ife^~`gKP4v%;YVg8qkF6qYOIaB4+6v{& zN^^}pW)uy%fU!wl`xs{qt~Y!N`||v1_2u$Z1CbcZiL}mySoYL-)yKw|`Xxl46EF-B?lP$5os- z>XtoJSx$=bDs-vVYNpeEWGS|vV5jiB`bj0jSeEWY7DIdwJBWW}eL}rji)Yh)z8T2m z41Io}HA{mGvc}Xpvxw8cE5&ARv{bm~B#b3l@JtSQ>@J>PgT4$^Yj#FIS68rDeM6U39~X|_=69(pZA@WRPs<`DVu z)DCkyUKy39W1qXDyY&o?n*vbBA0A#C1zn~%++PRg5U#*+QlPS0f$U>Yd}YmA2g@Cr zA6*@(sRu#sxFqQg4<8DO?*IVVqMeAaNvM>$>bx;p#gJ~A&AV;YrlU}y+VCzDfX^xt z(v&`y41REj&G<9_fvza9wZZ2tY#x7kDzCS6o>}AeG4U3LZKxIjylePc!@S@*s zjaseb(RTdz2v4vp0By~IklLYHo5LfSXGoLR6Xq|f*!L*?f^jchW>Y1>28k& z(hY5?_qy@N#Sp!wK1RWCn)32e7vzq$cfHV#ZQU@uJ)TY{gBuK!Ka3o_M!cib^@ZK( zk$vizMV6LSl3t(KYKq_59mycm_0^NP9~cr|Wp?RN1B+?T;tdo%)Y3^M1bGz3<|Q zjBE`xc+uR~k)t0MxZ7{pU)fT8gqqPxUa_xkS_Gl{284;}pGjo_CSp(lK(G9kbODvm zdr)prz(#jc1K-7b{YmL%d1ASD4ABq&|~#b%~!UB6bBWL1|t&XxaK z!pUHgRgYtU-~m~>Il`4fi`2n*mB zR{HzDRQClsjbWJs_pb+zUm#XT0bjCK1KbLQUpw{7s(s>8AUc=-pfW_U`k7ti#KVLo zY6M0+(ski2(awnsd2(jRP2DBqZl?w!w<~~>>-nYcn#NsXyj;UjTsmb38^ zIfm6U_1T@d%Bl!pZ?6KQvITk(mg=@Tu= zgp89<=yNN?d%dFk-eV&*t7K*A;=tMsH_-u5()%0JK5@im=POeNEnrkvb9(MyJGjIu zxYpS~oK|F2K`}eYOSi_lW=5jZ^7jqW1LGgSE%8Qt4{LpGvaEo>1Fgc8_yFlL%kGKW zqU4%w84`5GjdW*%k4M2xEzFx6g>>0}K3xngMfOs~P`yYmQ2uECOWxVV9SAM&Agr{yNO@_4|2Lwb|+_S7Mn=fdP1wn=3LVRFo--vOjgRY6+ zAH!mHk4d^9T6J1n>CJI8Mok>FbIXeB>bc?NEd~By<@uGb=;&u35YjepbDwk+$aAy@ z8C~cE@a^?`W3pD}K5!0d?9nSREoi+%S3AhtmpV!sWur&f!frC@|A_0*jc!HGzx`nC z@Ve!8yXY^f@>7~{-4 zCnv!~|55+3F_x3x%n#}!ii3@G(N1Qq^CwCG{^}=yzyT?G+L2!6`yc5UaDA#TUj@mo zB~b{sP9VUeTjLrIi-%4o_y*~~)A4@Ici5F==dQK<;|fJ86+!wtZaDtK!Lg+z__sZq z*GUU5osJ64o(#w^M|q6VP5xM9r#zLM?}UftB?=CC&!VJxRhL5Mm_|g7$ppo0W!k%Rz_|AldT1 zE8oqsC@Y29(q{3+Vxvxfr_J7P5DHzOtoHmoU(x(oJ067~SrPvKT-X`HHXf!6E;YT6 z$T3BDyG+V{yaEJ?KB)|Ft+8o%*DFxGgo*mfkREuOFWS!X zyJnlAeAHjB80ZhK_g3I`u2AbW38vvfm)B5|b=-=|!Xe}zKuXH2USIn$llge+$u)=c zg{NXo8iyZ3-)Zd2J?0kfIjmEFPfBV)cQaSuA<4>~b`FXx-8p%9#s78*Cn)BMvm}fT6IGX!=Rl7p_ zVk?>N#Px8=QGHYeUGz~(vMa0VlXfdh`M_POcp+nS>z2aOjq?9?m}{_3TP*>VuY@x` z8SIt43$%#%2Vlk=ax_aK-kEjn1Bl#5#xK781NaBPf0Q3vP9wEibSKBMAT^1f{N1E_ z-%~*%Y{l{Md+jJ$$=HENu2(fX(p<9@(6}@-yU{_Bklx zL%&)cLF#Xc&`_+63YRW@pGn@j5n&A54N{#z6y*^HXE}}OGAF#P7KT|fYxiMg?Zg) zt|wRTA#u){=c8Eki8Lx@;M?qFHZqBnkh%z`=q%@AyZ&R%r1Vu%t8up#W(=Rj)gJs9 z_080b_P_#;pYYUOUU$fmGNzVBx$;>gyXnhuCTe|3MCc@s$sNdTSm_OUB?$T5jXk`_ zpX?7V%^kll2#)jo9L`F4n-8xIn}ctbZ{G(7-XvnG%H0uPbctw2h~^<&d`q)txkdNE3sv3l>fSG2^A^%)LDW2?NxG+zs$cA);}_QDbB)YP z;RA@$i{peMZOr<%)cs-Ny1OKHi5o1(az`Z3|4k6a*k5wJ=$OWZbpuv)o#4Evg8alP zH9XP+UUXX|1H`;D2n_r{>%pSV$ZO@6UesU780)*AZP{);7#=@$i|s|d;vl>C&1&N& z{nxMu6bs5+%Nrq5(E=`>?=^hsw3#I728zTg%iIZ})v^htg11Io-jF z!3$;zaoj0X3Jkw}rzQkG+E|cu$BqBxcCi{CzhzFTtKl2A4$W5>1zxq|xuCy$9aWY?c+keP#_m z+X8?K0GBtAsn;yxw#IYHcSYQF1yNvWduNW)uABwj%l!&IT+6j*0EGKi?1y_h4`C-* zop~b#9YKb@39&{9+)Dt4OtC(}x6`IL$3vVKNtpx+M%>F!WMU={x1Cb!a64U%q`si* zXGDIGYG0p>#=2V=DJaJ&GFz>2;T#CWZm5D)k384&3+(KCk;|J@U!j8qDmp|yp7%Pt!YQ*fN%k@eJUj666qTu*qtCt9z5);km)H-iE?Lv5 zx?m?_sH>T7uT3YuG9QcXg{mdktu9G5Xill; z-+4mm2LAeWkP=NOt326}th_%{Z!l)q6|y<6ykO_)JbeN@l75I0dT5AdN8&-{pAHs+ zOkr~nCbc?XN#k-Vx#YD1u|oCm6P6u{(e{-5F)n__nvN&4Kj?Q1c{eOO-gy;w?mN)x zK&G=hQ*~+;IXnk?Nj+oxLt zt<`m0k;;({(|72^klC`MRVjILH}uQcM-sJW5f%$A1S%%QES6$aqq8DaXW1Y_XUf7D zZ`HEr+_mdb$b3!H71vgYFMEVh6ic15 z#4I|REsASl*+HbJhWSd&*NtZzWsmhF^Bk;P_e&JiEYq-_2efdBzxh4PqU9K#_1ZP| zvX`ZkG?cTgj10ebR4id&g@h1O%?Mg5>+6pPjy&uO#Wb#?;~r+@yyfdO+s6 zckar%B*fP>290)Wkxx}qj|1bG5W-_FGUpfiWf&d@4$6DBM8Q~pIy!*`MUY2pogbx2 zD(cmE%eD9zZA#HwBH}tYH|#hcU6QB$l(x>DS}9S9?liSv?rnwVifh4)^8|I|L+$zF z#QSAL%ZWMe>6N!9jc zz1InjYR(#kj1Bc>ETT&FAYqk(*g9T=9e+bONL<7N$ZA6OItMPHP~k|3I?e2G>q&J< zX6~lv!wmRkyIZF}rh%JGb)d#S+jO3#PCcEv&s;~<6hhk^?(Sc}q;%?^=`@!OQk%FUaE*s!Cq|8_zbJcPy81b4Mywt ze)g}t?d!CVUI2};+U?NamG<{v0S^>Ta!lac7O^5H&c!M}07s!I@pF?5lgX+!S|ykP zC~rIdk4JSkJ~LuCOgYEz9p`R+SpI0`oFf%I(4U~EJ*dhvGvpFrnPSIs!iWlE1;^Ei zvqlfVG)C;511Mrg+6~<2pu2{DMzzCA)ULn3ZiA4KZg!WxgVq&v<7wwMy(%^fJe(jg z@Omyj#xVXRn*co#GB=gMCNldP3d?G%j;J?=h0h$MaF3HqT|4x3xg{9sQ+H1b+1U4h zX<2X$UG=fH*cc1^+l=1+OxE03TQdI5Pe%puTZW6+*R?@rIG?edi+?C#O}WN^!mvUuc!4wwgb1=n8kLk<84~Hna1~fW?^GebB=~x>PC^c}odVjvBgs zu;_=qE1H1~3afYhB1kZbDpXGGiX)!25-4b-C1qXUntDcSbHfVY80j&s7!u1I0_m4o zY-e)0D`UjZAH1rIm-K$nXa+OX7(b3NAlPErI7KvYb3|;=MRY7++x7f8O%6Q zJ0Q_vKxY-H*<}v%A)ahOH=zPV6_C2!TZB4RQpR*yqnuPDT^C4lQgWpxO}8M!D~jxL zlxuycP;tG2cWk^0-fnzI6M7M&ZjsuhXvhT=Dp2mGWT9BYMv)ZBRvsj&ks+s}W0-kF zq~4c-3Tvmv8bec$O^ zpC-$UXu_#RTJWR7KGYn0E29w&^2KTS;0eVCNtwMydj_apjkXn~55E&y}h7DFm~*JZ@mE6NQ{hlgxQFu5m2VF70yf+4`8l7GM?$`+D*8NW{o+qv z)kLLskU(1p)fMKui&s(F*$*_o8Byh35*1qETUZ6ayM^P;yIANmga{PE3&KBoh!<{I}wQ)Mk*QW z>dDvQGZlB1HxE-k3*C0h$D~+Av<2==`qS}{kES!FBTsn*X5m-9;T0~N7Z0#48>Q(J zR9uyG78Tp(2%K*+zXrXUIlg8dU0_sKDD*iE2sT#p)pL~PGrwBUw@M*b+?J^`fgFjd zSWM7TUPf<*2d5xg+?EA@(Z%Hq_%JrPQwn)iA^m-fUB{mkB|qy*9}cWn$v4D(yA(x1 zE;aOwLDo5jrG=P;cpJr0W85sh?mv_sT8Vg^vT9R8NmjdT@wl7$d2)7>4iV7REcI?y zJ*DK7BH@l8rTmUv!b25Ph{f~~%PuaP4zuoDx+=*10514am#=AM5=F-Vm#uaK* z!@N*6)J{`j<0Fih(r?6Iu8Gn(!TbYV=)PIc2Au|qrM}wr3UD7=wKWVrVFVGr^bt2K zW6w$>o2hDsYrp->Oy`=)DO)QNByZ#_2;11O!9e*j%4_7N;rDb;d6>c2*gnK=@Gbv$ zfwv3Z=KWhP?vnhwx=Pby&bH`VhNB=s0vTF?g1k7rW83&cNAe*tR8^!oKZNfx{rbFH z;H~jw@a^#QIknWU4c~YxW}J2V?j5U5PR8;y2HVG ze$=5H4)98!aRYn@sZ#erGJ>u`YQSW|i?bU=4M9iV6~xNft2 z%OA~5?Y8bjI6-6|IMh&Mj(09Go>M4dezS}GBpY3fsQ2F}lUA@|75m=|m7}VUz2S!C z!(gzA6v|`(<~-CE&rF{moLHFRgdLhF{AB>!rre$PhBS}qd-pZ(1R7$5kQo|=S!C;; zF+<64G2z=gL4`(1>4cTHRg_zY!tQRe2d_Drg(7y-s=hSt7j53TDa>-e=s{A3!LTk3 z4c+-6z0mgAkB(M6SrlwyaLi?5J~RB_S@jlM5@g0_%q>MMjsd z`-dU+qMeeCG`dV>8!ov0`4Z+S1qEf;n=f39T!+1tMEUBj;-KRL*}K0T+qr8-c)Guw zogUKBkrh?8E-lj2GXt1b1VIv^+nEMFt?aXSzFq;qpUf#U0~q1%*rz@HSv2kxdMESF8m?`uFeoL74n(Z1g8Fv-n)~y0JXn%%O!xAsb*nkG8miP?d zDazPs_H2_RX15m&TU&;zo@3OckS$WqUm7ey0!mVWmy+A_z#6R+u zP7qd^ME7ueFM~ohPo)@Uai(k6WXko1}w}}jKt=qEsD%^9b_pe4uk&py+88isDM?x zSX(dFd6j-A*|Cj!_Hd_Ej6(%!rgv`oM(rX6#j@9b;e!76E&Yp6S^Sd<%Ooy4%P~>dG25Du!mbSg z?r#{Gh&JXU0^ z%Z$`GIMz(zXLM0j98yNePZ-GW!!ipgV6IAg#4-AG(V9-Ti~u$Yih1*sL2}&n?ze~4 zeQ8Mz47_v7%i%_b*tGOah&T2eWOeTS^9d7Kz4@k)E*VE@wlidqe z{nDC=rv(?6;`JVonIRmZ3&KOx+oi=h-|D5rLRV0T&z0^$Gy7n;IxS(f+P-uy|>6=r4*pmvt$Ud#W7cG?s6w=iS($>ghEG z9a{00A^jhkI_S(YLYGksv*7DY!_7JLkSI-X>-()3@18dRa=86Zm%rs|dG307(+btw zemnh@UsQdgGP7hn8hS*CCeYiIPrR&L^y|?>7(ljs1LM zy%ECOAF1}(?IY*TYTO0~*YehtD=3dAKXVG)$#}J_3yo(Ddoy3kTz>j})NuxqZ9)Hk zHUH%D()IJ!r8;cB?WQAtNS3}(hu<#Hf4mH$_o)AZDfz>pzh4zReXmQho9tkQRXg*H zAb;Km8t$|~H|DHFizkoR&A&sgQVDqSO0|{;3$N$YFIS~_*5}CX;2YZ+TZQ}bKA|O_ z7R*j>4YL{P$o`sCPRzoj>$%1b?c;gBIQOhQRfcT^it^mWvtE;pj{~LV*Ui-Eg_n+f z(FP0TBZwzYrplk%z8GuR6Q=5x-&q7_aOf}wa8Zpc@tt#7yj|6t4O zhdWkdw#TLjELgHAM2}c;bDK=F?pQzIjY93#2i@aBlg?ZZ{{BB2m&!lj zc`i5Onb|o`g)lqTyVCqSE|LPS{w?v7|(bVn3u>%^WhkO{U_Jg`JvWBw~L;+4^%pTmfkqCWNH~GPB=rQ z<8+uBxiGV8Qa9@|`Q>&61fEc zdleiee+kG;T)dima&BmPPK>o&Q#wbN{Y4>{cv126Q8FXey1t18*jVlrx^J`vyjJy~ zG>4-FjS_iCtmnMl`GvNh6u!>joY@03i*F06p~1B=Qfa2dp=cyn?qXu_f(}>7OLFLZ z(e~~}p4Q`;UWtuc%@=_eZh04CZ}?tbJ$k~%FLyZ>zg9augNQ5|y+1-D=PCR!3gaXE zzG2+KtgSOPU`_Enq?~+12^SU7j&EPPktuP*lE2KH?;hqyyo8gz+WQ~R+>zx-__L63 zWv$`~CcE(F!nXb076AJ-J(Dsa(d20JAbTT;*f^`@eCW&UeGJiX`rb10op8m;BS3BR zi$&++Gwwf!Z;%?7<@jp*ZD||uwTmy0vp-tKlC{C~*2i;Fn{DYPQQl3|C#5IXM5*G93;n#bxbM|gC(dMOI0F9Jw^4CS}?&r_&!@KVIf=Bub)Li>$gkQ zkr&NIeyza!q`aAFp7^1`56zyq0^n|3$IAn6mBncrk(EQbw!Cn95$Y6whU(@m~~0(zC^6Kp5ah z;jG$sI%n}uh@ZF#jVcvIL5Ptvc6P>p7p#m-?RsUy%_{n1 z6WjM9OY6=6e~i!=woo3C==kZ8?dfEpj`$1KpmaQ{Gwag5!C4w@OBQm7P; zlx_LmN^#|$=(v1PT<__$z++!{z4l&7X{BrpVhI{CjLeXsCyqK-WS|Vo_TS?g#2_Pl zMhd$kJOc7MNjPk=8`5e{ZMjF!X6fTiM=V#UWIvrxrI^YLuszkMV*4eSX+#~w6{z`)IAjm?WPXQNu;wVuJoZp9WKgseusN9Tarnq( znMR5N?sAqX=dZiSMkmCD0CtqOleWyWvalzn0&ri=v;K1Y{lX^U7etx1Ua>Wa7_=ccfWhxs|5R)) zTwf5>dgXI8QBC8&VnO)zO@?A3U)=)U1>fnvO9FqcP04*Lv}o3poCaKhx7W>di$ld! zzRGvR{~`l%Yg`Oe6I^7Y?EC}Uz}%NlQY;3}a3#l!0SNY-M};u@tDl;bQRVf9_sm~! z`ZG^68UxL$74u|AZDY$pE66DMZO~070Ud|=S?;FP%p^1U%+hV|BW=x!GqOu9a}gma z%Ge9m)J;)ROS#9jIq^{(IR2$BFWiGS-^UjQ!OHHh}=X>5ah| z5H#OhPtOLxbw;jjEYTLf_rW={uYF<% z6KzRwJ*eG$LVmvhudr)~79b6nIx0DGeTaodM$}lg1vkCG~>-#8Xo%f!@yD)ftlwsV%=>K9xsQ@879d?#_%>?L+xE zd_m5wUqTVpZ_FgTtS#G9s%g&Zt3HvYWngjdZ>O6(homttZCZAIRm&ZnGvS-hsz^8< zap&QRi?4EP-ofCAvb&P=9YF<2D)pWE)#Q|+=CpFrOP>Yn@EV!tKGTRvei|{r1H;CP zD~gglq4|b3BHk&XwBQYnv=jQ)V zmyWK*Z!mOp8X*$+yl5WJ4ZQeV|6_P;=$BD@{I=$FB27~cv)KNVrwP#^hMWw!3umrr z+~`^;Cr6v_$Wkq6v33&u0!#aL`1F?ZRd$dVirANTG<^ign2WoQZOe3@c+kXYdS{X4 zD1-74QHgK$aYFfIX??Texph4;1{^lq@g#gLKl71Poqi9IY1`OGuxodYwq%#9_nbYg z%ja`z1kPo5=v}s6TzP}xEv6iM>^6i1HEL@9hL0!SWa?ebLF27pzr29}atq3XbW>Cb`<=d6ud$(FL2_=PRZkY=U=M>%G+{w02grs~E=C@}5}Kl!HJE zZs9|&4NUR0WsR4MpHFO_-N8KfA_mzbVwQ#%qOS6z77nWMb3dV+t|-8De z>De8#^hFV_nMvLFEEWda!xR$<4#!dVF?RqLVz(y| z!TMnPQ|p#Xjl7bjLJ!Bw>~pwsXov%ETWAz~Z1Hu_pke&Y$@~N6A44&NQGbvK2d?dw zD5yl5$rdw*#;?XP-X15z6z|~1DVUwsEF)oM_>7vy8UEv|5K-0j-aeuHkinM9(4lar z9x;+)I&3@y@js29Yv8&Smyy|3JA9ARKy!<>WW>{^( z#p(|9y)$2_Jrq3w;OG>bYIf5jqqdMQxOyMvL3ZnP&1Zm-;^OrQTEvbD7oll)=7#r8 zdwKkLV7etiPuD3*6#7TxuE|Bgl;8NLU61j$fnGwnmsB-;VP=En$yLQ4{he7(Vt6ka z9$qafzCC{I8kZRwf(&`rJ(l&p>A@R4^}EEbzbII?nWi5m@_aWRv0}J;)vwhfSPZ&l z2%5D0(S6);G)~xP5UNQ155G+q!b(A50b)m-SraPVjhE6gu}H77;FmJlTI_QbhD?v1Vy|9C){|A-qG(nieS_GY@>ivb|rXfF0@*)vaWh%rN9 zVql^#?zxf;x!uX}OCAb9Lb6Ha(k@?<&=3e2$dv|{u6|c+)3HAL04z%OO$^_1hVNb^ z#@Mb0DhgHzCY!;OysJeu=ir8fo-u4X6v`P+w7#UQ=s3d{nRAzJjYFxe|BZH|f9g@P zY~?}-*9dV20|h4ZFYU`W`2fUS${gc6268bg>Q(nYmapG6)PlK8wYJO*H&`B~(LXkB z9Apv<07RH7;ngBwe2$I;XR51I3oeC$Q_ByN|1h=NNnXb-Jx514*t<|ZlBpvT z3}k$hTujH*zB~GIb12Fbtc6#o;KpoXa$zgbVxRiCdSYG@hFuY1Ti$^}1}uAA2|*3V(cN~RYdurimej)! z^lw#crLoxTcb;+W=oa|Negj*^aNgOzVQMNAaHdi>V!=*NBl~Xih3X<+#?#Ku^4zD+ zQI^FeDr0d;;-huSlEG=d@2(aGL1%#J15!qC3Pfcpj+CwQbM|*Kea09TcO?J9!35e> zxQ@bkiYKP;T_!#z>8a1qz%0`&LaW9mGYK|-pf@EIiUi~y$~Ge&ajD)@jE*;X>_qUOpO-TK>zj0m+5) z9hD|#^0~omKEuo{v zE-hVM4Z3w}Om-i)36L)RwL<`C0jaI{bV-`iu;N51mSxwHa-2JRB1_U1Xm?$B9&S6b zk!W|ohW{KgIEiN5s;uy(9}BBiaB!pcu1yXeSTQ$ONkbyn3&0{)#arqcml7T>J{$6M zF&rEhLY%Ey#kViOdx^|j`gzu0r(A`7`<2b_1FlEiOlu-!OtggKGJ$qQy=*Ht&UScbSk3lG z!5Sx9M1TUekdG3U2cR=md9o~CH7i!RUcJtIw}yTpMvkIzD1$C}?o~Sghnd8Ux%hLNk#4bX zTkDpy)jTkMRe{W)wx;n`+tpl*vI7n@?+WO?5)j*o$7Ks?yQt^qSJ&hf?%5#vg|E#! zKfgu8P`96R-TGToambROWo~Qqdd;+^-uG-~I0KATIyr&{1ZKU)fP$xSECbW|330R0 z@9RjE8RpHrhQ4r~l@YNJ4RgiCm4ipvANM@yqI!iOqhf`w(Z!>A!ddV)-zv!*(1%38 z&AAU6l_c3%%15%(e^FfT`!kgqF2uS+lPzK3dN@su{%KS-%@ny5g?4RCJEPNA=SlW- zNSMuZ*+fR0@FPNgyVKkzUd68|ocrO(?VJavL^0Na=m~u}`-#*lU`+6kkQ`HQkv1JE zMvxSm=V~S7hNip|HWJ8zmQKyCFgAS08gEa_fv@1>ARAR=x(BLp9LwE0(<%-DU9QTF zTS2UZhy%P3$y<0HRqWV6H)XH#orq^1_a^usV@sI${x2QU#fqY{ehVi@9z%Z7ogX(r z+effib352va5kyPMKRA?VG&)|_2}mxo1YuE3EH8D1sAo`&!20Yw_Ni0cs3BORV&f? zh1+s=dgdX!ApUd* z>MyrkBo2KafsK1EKlm|D=Y!WLq&J$tRFWZT&a*Yb4YX1bTQiLt@RH^X!TNRoc6nlB0_^c#x5R0j9 z5|g0v1!Qop#giay-C+~?kzA`1o8@6l^)3qVh(DvgmpJYq>&5*!tjbTkq$-5W2Dg&* zM@JM_Ld(2K;m#Sg&I+&Vxy&hTdW{_WDJ48DwZ}tMAsrR6V{Br&wTbKFy&&VxuB+HJ zl|Mn`b|i@T*AT>B?9>R~!x?k24(a;G_g9rlb3ne$=_&|9oRC<`{S8EOm-#@aS6WWk z1~T6k!L?|2w;6ojXzmYx8%x>RP3NiJ=N?mGHA4BA%=FQ=*SBQnaFUWnL56qzy`{E4 z*eES~NK|_&kg2T4fEo-b%D{*n)fExns-+`aWLYA@G-7Kf9jO58@#oD@*CxH7DRse} zsJp?bE^&;MyoU_8^TfbK>s+&LczCuQgScdUZe2z@0WH_qwFd;Iu{wnycZTz&oVf7W zm>XQHBfE;r@|{bc@>aYUa5;T2Pcd8r{Y(fS1NbZ?+daaSADQ_)8uQfdse3*5cSJsl zQLJFR#Ir((e#DuX4%f-xn?DYnf88L#2uR7j#re$o=_e|HiZ61VACUNo+A9$xB9qnd zNNVB=54lY+_6vB9^e~-t{H0J%M>kpef`E+vee$(j^)l2y(q+!iV3vfvRbB}X$j-T( zhjlq@0CMcs_m_$)NbcJ^{6(?ES#$**S<>J2xT22-lJq$2t|aSFnCw}fxk6l(kLV;A z>He{a&aKy2-^2+m4zH2X_ehbhDTFi0;R;|ez~ayJghSh0Yi1jOGiJhnfBHlSq{OH6k>!a#%N<925)}1Y8!fQ#Q zN+L`(W*Bl`763GACLfbq66y$&u(6^38bM%8{HmcaGTn|f*1j{HjqDSdZ71WkTw?!S z7E|fe8e36Vb(Zeupi5inQk7;&exRvIK3tFL0S zm&PzB7LUB&yV#^F1IRKV)Rm>qACkO&y0VG0RkGIp(5)@Lqr_U_%eM=LTlmiu$|UFY zn$5Ov zDMNvgO*?tMuB4eq2KJhU0hpiV3YKfw)C%O`$gR8Sqn4W}`O!kGp*<|6vNdrU=!5}V zNF%Ig@6^citTuIwj^_b@oX!fc3zS=)C}rat3JTN5bjluq&l<`mR>B;jeAIu%8Y=u8 zil%1y996)KZ=Dx5+KDgU95zpVzKL}Iyly8C2d%TlJx=ty^z`UJT>KWm^DLJLbTl6) z!EfkddYt(`LK0^DsP}-J0O2{Y(dXvLr8aun{X#8Dq*@ls#o|xQX?!6k{Uq6-$mAN& zV)fl!I@d6#e7mnw71*gI7Atr@fjtp-E!X?W9T~4S!=Een4zONvNFA4@OOG6C?oF(d z{i^SSx6LA&jbCd%`H{eV>Eo!Bw^D7!Bgc^RzbL*1(LXW&;GN+&`TSx%_KmVaM3VSl z6tRgrJC~rMA2<69b_c~DaJUF#OJ~q$mH>|=sKU4~u5Mi8Xu5(R13T7i6pD8WM<4aagYq?wBF{No>=|nr9bzj z1K@KA46SF-$$5oJ$^eeJN7jUOwwSWZyyCEg0X@TjO;_25EK+s((j7~a7O4a@ZElAv zuDKjES^P%!P1-}Lkf-Nk1_n7VZD67uan19K!kkw2AM{fO<6BT*mN-^G za%?;%LTA=2cR0a3yq-nXmykDFVhgX9-LH!mQ&>(*rBCtrTyEhIb*5wG^!-<`SYmRO zfJ10psrg!Kq#3n$yzMZko8+Fxrwx7)Wym>{`ML#WbXwFglmd~TkIPN6w+Pc%zxt=F z+C`X^i~8L(J<~VL{4|>cOhB@8Y_gX>unV#gFzi#yQYoBekL@5Wu3u6K;(n$goy@M9 zuh;;Nj~;H@^E2~Vysa_NvB&A~_=!TqH+l5XV0N>{2skgdD4a(*Mk;T3uTVOl_$0fl zp{g;%(oeA#zwtG#n)m!Wbtz7meKyxB{&*O&<@k;NUSfbat>Ch}&z3#BDoLb8BHxIx zc?FOZ=^?Q%_!mWA+P#+^S_ZVL)p^4ko?HMrm*(LBnxWigkKqcEyR(MN(D4@Q3$1%j zB7b-iE|#@RD_(~@*<-dIJp6O!gQ2+AE&e~f(LZz9p%_$di`i-Tw37>ah_Q{lCNf;? zUzf8yHo-e1c|td_SvO8Q)(mDT4;&lN*42>7yYu=AFI~L>#yWbhgG5*u4Fw=!66u(s zp)oAh*p87KqwI8(C~7$YHq{#AHyGhi+?n#x&_=((E*#10Af zj#^V>(jySbJu9eg{RmUA0paqw<&;GXP2DGbsg0X=tyBsD+^K}^d#ehBcQ!13X`=nl z+PQ*q|~nM<&w4OBYW3PiYwRFDH#8QXW;HE zRiT2(+lrGZuYXtIn(n30HJd$^`Jbr4VHm=unrx8949MH(ggF;hd3ns^);iz1FX8r_Ylx43FtBd3WhAyQQVXOu6{19AsNb0KFw$#GTxlTPg-HWtc5893EhL zhl1jo-~dW8vuUIvL)2QykZdr^^v%vRNe`laDh88KIpsA^57(&55~yCszbG@4g)ub< zNSKee$lHi1ftdv--U%qkk6{L4`0zpTgZW!dG^|4PpYzLTKK1Dz)f4q^)3#O(8=&EMgCbh(*bKd}xLZ+>w~Tt$>=eG-Etf0zzB9lp^t}g@HG>`Xw9N+=X7%>93#Hy6>MCWRaDH3qjt>-pSyp{lzW7$$e!=woPVyz?yoA{BaY=~cS~k+ahWC~B%D9{0tgCF3D<@4iep1~H$`6zii>%W z>1Q^df%W%ql!n?bS*S%p28uZ+1ad~M8(?37GDY}W4(N7?8H1;ii z-e=)~4jNjnCo?!t^ZBT+?w65?sNDfZ2N@vlFTJmHQ`fh6W)`xHfn7!LJGX!&4p(v# zI=qAtY=>;bXbD!(rza&86zVGW-@Co0pNJTNtMhT6uC*-yvX3k7;sLmmKgXQE6; zzOsH6>)~Dl8?u8+KKZF!t;%~e;wcc+T3oYgSFM&o>FOFwa)p_Czx>PsXA_eozt`4^ z#iW$Vu2)w(t(l`RVP+y2tPZRRkUmRdgQ@}e9$}w8PQ=fkT-SNZg;+(ccY=AzVgc!U zbYu^|40-mb<8p0hq{K%H>*^wAGm~BD?Db!1MoMihb%ePBIGseBKjuI$j>V0zdCRa= zFDq5mY%L=rG+JHpngc(YDdUXmQHCo%tpHBXoR_DxCPWrrQ>?yjJch}1;^&QsrB3=$ z;GH4>u1feLi#ojlGGpZL1vTZf>H9Ig6QLVi+TwWpNa3wQA(*eLnaGO+H!lK7!1?~x3J1a;VJ265$FDw=@_B}T`?7D_~Ifeay{ zhU8XfMQ9Gjl2Gbz9%Jr=hReSYQZA2c-2xdD1Xlr*?HJ|VDt#KJRU_f0mSm4pWCp0y%G z1{Z_qpL(&j{zb7Rs~wE4zxWk?U)Cg=%f;^FRuWq_@48?iK5*A1%P}P zc{>WGwR#=8S>CoEnk2Ie{*u=J zB)+@YU;_U|(OdbYHRX)f?ws~glg4#L{q&tmnCZ>MS{T0AJWWzfE(sOsmdT%PBTr(k zD{p@Ij&9aH5bP~M!}WPz7UK76*)^289%6Nr^x#k_0_S|MmuzIS(j}&h)O;t~a=h4n zu=dp3Cl~YJD^YVf|7Gxhgj}OenV~L`V6~7;afS`$OtuFr&*R|_ybazEW2mQGguOXUSkGyVQ57_{x;V+ zE4kgzuxW3um+u^o}wO5^%{C8Q;HnI$A^B=na z^<4d@GL-m2&V8z~0KfaDQ^t$5j4fIP_dl^4Mf#bV@Ec1PO7^lZ7-Xk-T2wW|GA8t$ zT9fH~WIeHP(S5ppn$a2PY}xgkp{+C%;JQHR-7>>E^_Fojy=Jy7-dSA!*U!{rO4MRV zRxTPN@r{bK#zX&c_La~dh-DAyEYTs1bCOtVOuENpv;vQQUY~%HhKcZH(@XWWBO8T6 zKqmLr)O6T#%Co9v8#{(X4peG*rV27{d@e{f05Dme`hHTaO zSG`bTvq6$sh-Buhwl!$Mk5vMZFYzfj9k!+0I)qH3YTa*W3?XDX{Q;Rb1!6VxiMK8{ zr-ZNCXU@B3YL3i5Fcede3i(=FTZc;%eR+RnI*-|Yl79-bBf@3?v7<#a3y4sU^+&Nz z{zajbh06ITN`CQC_c*Mvr!BrO+yLK1$t+>ky1CBaVW73JBJ2nozww2|Sd-~Sx#&;q z)e3g>jPk-mWn{vE`gyfaD<>?|6(ZFPoKLDL3QcgX&oJSMgNkk5(Qjj_7&-;U)2cCl zPohUj;32|^`5pA>?24;IqnzJc z%?k5PwFH!>+ot+rwTO#iN0ef{3R&ScEt@QF>%PL6i?SFI4DJyd`xvU8P@-((-PFI5 zMfTq4@{XRFi3K zf^@bG*leB!sy(hs%>5Z9yX7|NR@DqVU*n#ssVfsVD7^GGex^D1OfNN;2nzzQOVftF6tARc>dG+i0bU4RH}Zz>I#5Q@U#J+*S+R^8&Vdy~vL( zd^>B-YjHU}G|n#A{~({yc`3N11*2v?F0PoDmbNd??fLNs>6Z%*&n{b0JIHmejAxnd z?^%Mjj+Jr-K%h69VZ*vLYl3aQ9&o#R7W`5U|9)icpHaM;<1%{t-M=41!gu11>E zy>-nJw)DxUj+ni907FB~P{t$uwe=mCzEFjc_t*+P2$<}2;3nN+r46c}m&J3cu*~W` zqI0-eUb1z!UjL463p>Lvnt@`sURxgMKEm7v5S03RVM0V9A;Fzhtx9-0Lj~(RK7{EQ zoIReD`I_C9SFKM%-g+B5SETj~$Tgi+kK8N$)Y?xao`4k|O7aR!2mel|-q&mEDAP9h z5-dufnDAANwKHpw64Rz8JNWLCXfRQ8S7t0LKJJ z?!FhvJaLd$E{|Mq3+|Y>ZI(DUH)(rHxPM!BTz%#Xe#_o4sa_CQFA@Of>jO#Q)byUU zJ|KPO`tJ8!yPsr1XB7TSqSZ}>m#OUyM9Dqavt(>I=T-(rKwj;h%`NCx5n8l#_JEOk znzI9dqw=bx7Nq1DO8qL&PG{aliB^JlTicaQVKfMNey}q8b`uuKP zz&RaU?Flt4b@EAngNVd3+SPfR$>H)MYtb)*Yh~i|3M>Di7kj@OvhvlXgN(9oyv`HS zf1NJmJp1Tz(n$>m^*W(FoUG3@v*73xarEuw@9uv>>{ZDS`|&Q&rzfWWU$kPM{I^yt z)Bi&&b~`$FlnWKFcoE7fV;_hxAohy`7+BbLnE}?Bt|7}~N=^<=(!A?-Q)%qu@?&#D zSL>O!HX1;QLI5D9jm<9Dko=PVB4Odu%KPh|x);A(dnn>YwtCf9x@1-X*K}Qm7TY4X*%^ zXdJ>igM)pOC7<)ygtnajDC^1SLLkDlp_`T0N!mErgKla*6*mN0_ssh&Qr4NLubD{h+o-V_IZ*S_VZ z81Vi*V({Llxw$Uoj-&pZg4?ej0kjVj5t(Dhsia&O#?~e~)3~=+c}0HysA>|c1~Sj^ zneYCAd13C11?wcIarMOO)HPmcuk4o?lfdbSZ}#gaU%XBU-O(remTwrg4)2L!zCZiN zP#qf_9PG;DHxgx(o;?1pL#l;_`lg`+sPiI3SOvwzZ@}VraNPsw`~u1Jwl+<)e%VAC z%LVTqVC=xj2x5|<80*U`)xwf(+ycvdprBZE;h*=9k*KjZUg>|g#;T$cojUwWG4qnT$U=8?B(V(o-0 z&BZnQ93fje^Kf4yp5!|JN~npdlH>5Q(~#0MRwyJI%~9ji^7!9nZgY4NLWFEjxmo^= z$vYK@oTkdU{qMPQX`cM|Sgr)(dDkNw`5? zFZ&%gE5i|)q{UocPUrEr1CD+EQ7C0{LGoKMr@NdnlE!6iE` zRJfuqnGHRjr6v}O#-TrJ&m+qaOi++KoTG0+D*x`rcgobP=&s6~88nOQoworxZadMh zJT+W2ywYLIkg0!TOy5nWtzb}Oa_Y>fFk7_w{Pl;{yhlZsW91(kgImgQx6|dE+0+^ZDqsdC{ea1 zo1vW@Ls9=n7@qRy_{R)yRPR7FrS6ZZ|8k*T^y+Hy&PVqy^hnK0Sm-+|5UduRhYX5t z$SwIKd}lYjH(retTY7Bm38~NAS0bCod4j#w0038r4xzN~)$Ll)9Jty87UB+k=|zN$ zETHG?iu4MeM>`1MNL5|XCAyCQ=jWkOG4?&o>Uj-}GSn_L{7vmszSeQA(fGt*vl~^+~Zsi@z*e z?K6_a6;!ywX_TCKKqg&_=L4j4K|)672au)pMU7CBNPS4u^dZ_wKKVr4@(zlHu3I7@C#G?Se51 zv~u{&UexeIAYL|gL!Dy>O78#Iul~*u%5M9vu6BLaK@LanKNmYM62h!uN30Z^xr+T{ z?hC6}Y@`$XGZr?r)=16>EkW#y+Q}@UKliPFBkWIm{F7T!*Djt&)Qa0Wbw%EzXt8Vl zL9hD4t?>jiI-n?t@B!g4;Vj3g2yM9-ES90bu}ZZEwQ$16*qA{DF&S@+T+t~_my;ZOPcpTxoBXkYi#*ARNeWS1BChgUS*&y z$;uLz5F2K1e(!%w{R=DUhWcsZf1hzSmyLFID+)hct9BQ)O~}#J7kFoeRr2#xoo`|m za4U&mC&R-n9$NSZg>wAdW^i(#+=qp7uDkG)^TF+f+R8uRQwpN zsL*)#yM(b$_hUJn9x zF57WPt9w4jy{eG`yMJ$ES@?6ql;?U~n~R*twFUdP1X5JQ03h+5yyU*8Ub9(5G}48* zAzUzj(}gHS-WTZgX2o>y(65Q}seuDo+Ocwqd|zp+(5m3?ArkRbjnwr^j%B)_Jl zq+lIj^9qVs>K>NPh{n|{H%rFl4t5JSzUB~h!S6~kKf{+N3$-U`q{j)dARZ`^4N;a% zHP15tvB3YbjyYVSLy#oJdTlfK#M=QnO|p|>(fOD{QA>kalt2%%O|bsM|6=d0!=mc@ ze$nAZ5d@@>p_CZ9TSBB6x*L=l8l({&?lcG~X%Hkmb3e~>UcI0F z-ske{>)q!%@7}IIxUL^-)>^;%#_wC7s-U@OL#0E~#&jK5Pi1E!`GPk-_6hLQd}*J% zGkx~VdtYr_Mdc^S2?w0P|dCbp-7vWJEt5M7j? zk$QRgCth#&{%0jO_yWAeu5N?%IMkZ~4-xmsxin^QqRl~5w0_3lt<}w#m zlu!VHBKI`O{GdqMauO`CbX!cxdbE^cq8W@yl1=F@>qD`Mu1iM3#@W;I8P^;Zx46ukjzt8 z?e^m&nH~<1;wq8Z3~e(rwgOBh7N^C)(Rx;ywKO3LhX>wrcQ;MVz6)`8Q=g9WcIca* zGVYol1luWh~fn&7_D292t>Q9NcV^~W~jsAsR^E9ZHEfKBu;i{0=>QZwpcK< zL&;u+>-YOHq3kCwtm~An@J#-8+2Y_kh`rqzS{@lyP$TY%2fvzC)q5;(dUQjn&PMY zw8ce!eD1GD#UO89^B`(8GxYj=_s1v8MGuXF)^^&JiX&y^%F8S)TT@=Vu|^8L)J7tv zUtqJjm`qJiHKkZlG7`U!16LPAD{E7JNx5F=BYcYPmf87|BO6{|n|}8dq7WA;bhM7= z;;I3YPGf9zkV|YngGv0t3hX>LqMftWkSq)bOliCZXJ_AKk5U2i^75>FD>pP6I&~t8 zr*cd0EYr+>TEHxd`>J65Ui)Nm3UB|{n1r%O@Gb>-UD~qIE5Y~&1Nht7sf;m8(81j_ zm7X1|SzjIaL!Fb+G+l(Kyc{eUb{RDw@<3duhs|dH)yWCZ_Fc`}sX@iG`1zG#mPuN& zWg~k9HTP} zd@r_3wf1bZ8}bL~*w-Keg1<7WI%k-^*y>Lt3)|o6JzBEEtZrH@E3eTxmJnWur1w1p zavu{hztpSH)SBRvA}+&{XvBHWcIaJLhk)4a62=WB$kaa1xc)JSzn#*qCWj&h3dyk= zmOF$8If!^dJ%;Q_{QQOsV&(ieg_W5=9ud;j`6LsbGYfYz}0gI^*UITA>e zisXiYYkIs2GCc601&UbVLK2^ct=7RyQERpaqgoeiV#0j-?-*v1DY{Znu6)^ersCH; zkNuAW+XZD`-h&IPDlrL|87a7fReF%9<5Y#CTgqR>)C>SO?excK@F!mUR|a@r;vi<= zlSPd_k)Gah)!6f0NjU~uer&~EPz`uHDMxQ;(05?APgi>DIPfGWXA@PJ3eyXSv=ewf z)}^-F(1xs)#9RUccj#zhE`QMa-%d=0!H2qH?t6$79TI8NZY78+^G32AfzgqIqBChH zhbhGLEGN^EfofXdivlReMuc2T&IN^G%3b^aXj1!^T4QP}7{mf>;UP@y7G)19Ne_}V zSZzWQQYpy880v%)V@vbPxFE4!9dhPWs;rsnO`f>&T~FQ}>E`G$Kr^3`6*z}>=YV{e z!g<;y9e*0Y-_9JmdaC)SNM7&&^La>Q+*XiM3vRKeS#rO!DmybRe`YPii`}6iALd@n z3g8r1q~gZhEmtxfoXA46l{t4z9#+y4t?^jPBq_RISS9ZV=If7_Z&-RYofAxmr8F2p zP|fjM9W_Sz5fc`2-8mxsst%{&4TmP9sZKy}@xfum{}(3Eo5OrPX`&3$*~~!IG4AZrTR`7arx^=# z=0MdWQ0b?SP5kQ=Tmvc!k+(fO}E*CQSKLx zb2&-zdLN}$+a&JjXinj1iwH>K7H{a1C94x>HS>b$%W_A>&O|=a?AAS?F*d1{&SP^zlrOo1*GCnY_RneZQ;Ix1R&{KZ?Yqk za|sX2zIC%2ti?4})Q6D$Y_tfhp^(`@4~E(@m+>d3&Knqkg}#i?4@G@;LqHPmKLP@& zk8pkdVYB|1!EoWK11RlwOuF^Rtu3+TXgz{0E@A1bTDiG60a?NuhVik%;>~kgBMz}* zxl9euIO0}8Gam@ z_A47OIxs59MJ_O2IG1KoPz=sKWoIF;C}@A$4w zC#N))v&)r&B3|B-_$$>Z8r^FpoIt3NAf2fK*cnx0jI^4%ZDb>3e}a3UPVXHF(^Q#I z;Gg~JmNptac~$B8vvEZ4(=REQyB0quH86z!*z5PbRRL8YA;r&1S~OrG$-@PMf;72m zmjjx%e5p!(3AJ?nvieB}v#mdy0c|I+6;f5P4?$i|&~LuD>U|%A%pMh#W9rw`YD{=a zPUPyAv+6;QMSBRkcSS9or<8Oad@3MIwIyZ8wsf{pAfIyZYYlV20@KJsd>ee6>f|qu zF%CwFiY(P<-SR~jER+gf`}P`399lpu(rBa&wR{?*AN`~S{6+UatjYO>MTzPaTrpExT4XoV+Cb%V?3!hgg(uydxkbh=Fv9MsGfWa(t%2;%UXwv%uJTZ~ z_s-T1jj8nNtoVaGa`nVv%#JaPH zEnY)n?=Vpnryrd_uwNi0p6S1ZU>q4Kt#Qnyt_ zn-{|ajYO1z_-&BShhMOk$X?U%ErcT%Wb_ssB0g?3|!%<0_MCp;Bx=wm^3PkCus zm%>GiqVcU!kTCBpxT#6sJmOZyAG{}3K@l;<5AtY^-4hMk|49)3G^)QW>DlX@nenh} zx>T(u=6mq4QDciHQtE&8)l8Z8WFopd)u?Jav~XNmQ`Uwlh!FbT7n6tYVZD4@kP&o8 z<|VY!F1A1F7Y1eb5M%gP6ppf2h79S$%=2~hM_|ew%9q#(KdRHH0S6nA*yO1f)Lj%T zzc8#*-rR4JTj0;)cn^EZgdrAR7<8~9RHPWNFVKdr;xQiP4NIT1I!M~QGgJ2%UD1)MQVUjSU(Amo%O zXD<*I)tD^tHkIifN#>7Ys%sfC(`OhASKwEQWb`+5W@`Nfm9DHW8T>8uDg9i!L9(M= z1~PEmNs;lyCosJqQg?iOt~4(1-ee&iZzNUVq7k-cX-SZHbQwq3^`pOk%lgNCu=vXI zxzg^lDoG@mF4-2igLCW^e80*C$VEiky?>{~_4g2P=G_<+!xYXN_F`ZpSwu_c3F}_3t3&VR* zqPSHn*21#N>(#G0oC;r8ve@&NWZRJ|m^D3$Ko6>8s#Szt=Cd_0Gw1`o6ndo19Khe6 z928b1R^x`J=nNhz(O0z7G!<&Ktp~9)SRbaG{T=h`{nL@_`cIGX>78C*zT(u;dkjKP zL*`tj3Ng&2lOOQq#ATJBzAi65-#?k!M{{#Z;rSJ|- z3ZK`Ha&ePp>Is^1Y59$1t>E;Hd-w)Y`Lp(B8z*)wEU*#Ath}_j&bMX)p5YuvDe1?m z&j1wa=KmIK@jo>0P`G*ZzRr4Xp9n^YWXypS(Fx8M+)qGKidcV@x zMu)y1O`%B6j!0!^hQW?hMas1%wJfzD9n}kbO)9L#)5n`mdkUr;sEBjOG+%@K+1@;A z52IPX*N#()s!p_XeEj6|8>442g6$L754@xotoML6kA@?Bn%~OuEjfNwah2v$!idzo zt+)Pl!d>M|38yY5cfDrG*~0(6e~|Y%2io#fxl$Vy2Q=ex$9oQ?%vHj?=Jmoju=VM_ z?8*mSAZ2A8o>&*de&CX0`l)~x^5&82ezK29d1ThLO(^N2Lp}&Mhgnh{#!QP!-0Y;K z3iv}cC+1(%fsJcpsFC8SET^N5yt$`HpOl>*78&lU-q1G$4NsqfSV+e;!{I2joH_mr!}LmRggu{PmPHCE)5B5{1yr zd}f7kxLnG_QA0tX!lCnFtX1~mo~$R~Eq6D!l()E}9+L~>_L#c59YWBoy$9kSHii~>iE{uA22*z?mtk#OxWSw5kAtWU9bq2$}B&14!>MZ zY~pg}kkhwr1m&C3Ef}pt!ori4Xd*jT)k0}0eUte;wkaIrz=pWWW#Ka!)pu|>Kz&3q z>Ru9flDRcf*1i%0&aB{c(Y&HD7|K;Qf`6qY42|UEzZd+ta7wP81?4(# zj7|ACI2KdcS%I|)SqgQ=n3&hV9Loplp+dwX$`eC3j%83a(@jlc-EMn|K^Z`CTZ7<8 zuW1x4)QW)7^HDgnv%=%o+;3+YjtVVVvP5`-rR(uGNS(u8(06cwX9Sf8x3_Ii7iQ~g z(ayh3xCACGC&g%AV}mT<$qGf)@~^GKkW<3b6%*JwOa_YY1mZiy)ul)3v>(~++0ja9 zDuIR)uWgA#%#_;s6rwx%P4grD(ZYnjn&=)Sk{2AqcQnTod551PYo!!6TL|MDzRWoo zzRrciVm5Q|c%kOu$}(z04E9+cb3}~<8l!+RpU}8vIVU`dBlGw?kzuI@TlK)(N=hU+ z%T$q#37v7l_rc84erO38k0y9xqARQJ<8RJui8G}rBM?ENL{GoaDhH>556a@6h>J>{ zJPmAepzWmODm>WEcx_8g2oon zBU?n53JXnFoOfT#=`_e$amS7_^_Z*U)Mw#vL90+96BUT1Bdrw)5Wl6}i7Il_#eLcwoRvBJ))R*og&Jo? z-z~U__eV&0wxCGIGLV2Or=6mW zc)8prILad8jHWqA0pH|Se)v0j>&SG<50jKaR|$jH8^rV}fN>|x(MCan?*Ml8?*Na^ z$SqvbgNRF4=LbgTy~^i!eUKT_y%pveq$b zWO)t@Z8bub*It>|V#^o62bn*$KT)`U8a_=`7f)CA|GY9exjR4Q zGLU}83M4{sv>TE<*8}&eO|enV-{CN9+%$e#5J1=x zW!(gzSHfN!s(A-+$Gg~9`9T*mGTtfI^E}Z zAx|3G<);$tjc@~H7D?!7Y;4N-=$#o*-$AQFs)DPg9RF=rII&VDE%OEzZjCc8Gdr`b zC`{CeR2C(}WUZPUZxDzzElUyZQWl#u!gHlUptG~HDqCDgs)W(d(2b*OHQ;J%1(zq4 zr43;{CETbjqt(FRAN>99VNssXCO%R&wcR z%nBh!yU6Z0#|j5^s}OW0q5YkYQ2&|c#z`s)n@x zKG5$@@xHL8yIh|_?|a_1ypw7bIMvQTd%GsRpRqWxgoc?`BYOqtt{@q06Du?ci4#j5 zY^)IJ5WB^A$hV$MW<`awjzB7ppuJeLhFSIy%YAa~H5K~y)R_m27ChBJR7*Xv;re32 zic_kflfVASF?_8j65y8gs-IwDzE42mR+a_7xw6HSe|p9 zRL|TQlQw3Md2=gcIS#Qht6^GC8XCz}b-;Zr(;yeO_KjF>4$jqCXm z*%?fl>)yoCiI7jdUaYozJfS|V|#Kvu@)kb2x!0&kV`^Sx<{e9=J|U-@0S*F%vE z7QTn&!M3TK%`|-%@jl$#bbt`0T2U6cM^@wMAqWfbj?^^BVjZC&&8LP?DMAVvc7`q(W7(M zC}fABT-dw*0;-3m_5*8LX-9nwGqnb>_mIGb1!cL^h`2b`Oo+J16G8KAzc3-YU@u-p zU4kj(t)AKv(DP%j`E+|8BX24KOvU05JA*3?wZw;srREyYp)&%7shUAM&0-rY4Jb^3 zJ{A~j-+Y3{80d%O6aVU{f1=Y{4FC{pg45f@Yq#ky{nl*5<$>Fqoim_NtT)G>NM5(+@E1R#(ZElsl9>_n7l{vO*~!B_6< zo99@~w^bzBBE|cb>Q{8)=T-~j)c5J*h|UBb*>%|Dno3qhGT5D)#*Y@*PpDNknoHi> zauMn~D{YYSCEp&^$M{2M(=v~;R+fz}q{ze}w4`|?-(8OUqrHms_GuZ7;evIc46blL-sVa=#AyOLJ`ZoK(7J?X;EmsoyP#R%-Izd}n81plKt6SQtw2pHehLSFyS4w7qfX*1OZaFDW zYtuD%9dCs<(SxM3`Ba_8=J_G+SNFbxPS6RnfP0?)HhG+4Lp5nv!#jf35c6GEY;mzP zzdbTZWxVGKPxDD*_(rHyb&lvOnCG?-br3}@R@}5O`-obV<~YL{w#iDDp`gX(luM(| z(NW09)(|qH*P)9B(c4R1U+d5vryQxqQeMzQQgaYk(os5HMw-vZ)SxyD85c9G#i;ZU z?V*2MQtkH@k~oy}+QgOKwJ7t8B2uI!HDlS6^j>tzG&hZ)5`pGvgry%YJLc1Ev!>y17B`p!>yOMrGVJ6V=Wv?r;*B9?62I7lkWil99K^D}qawwjVpA@qI)h1lYE~#xulIDA3b8 zWJB#(r!$HUQ<7bc_Z$Us`{fN53??k=5@Z>B(XUx4*g9Am%3+qu^`gZvsJXgfgd^Lk#0}FVOywN zRW74hhYi)Plj?O2b#F7$ZjkCNF1^#s^PuVr4iWbo#YACFi{n@I?AO10=D_Y7yk6vV z{<3mw2iH?4%Yo>@p#L8f1pfe{PQGf_Jvqf_EmduRMSp(#3Q!a9=LoRt#dN0K;zy-5 z>iHIwb|k^~nmHp~fGYG+YYHgxvh?j?e<(aFBM|#x7T5)YrVN;jR?2dn>6N`{CfNuz zi7?%()_>u`I!euTCv>dh+Q>09>s8IMx$FGb%^vD6B+1FNZw|hUM~Ka^a&pMLSnUD$ z-rb*`Y{5gfTJPDY{c&e`Hjm7wOvl71?6@}qa6$61w8)JbV{|xj)au8bt{L3P|%B>OsPnEz<2ZK)y+E>Kkm5y$7CF?0v-o@Zg0K6^Q!juH2t2Y-@EBI(f#I4zghTiN%vbJ z_^lB9)|>wSrd~$=Q+Gz4D*-$v*?*oG{$E{~>YYw`Bu}IGa9%fcAuOb4vU8W9jF!*0 zKqA0fH#toXNz+XWso*$levqTVHht{2MnV~D8EeTR$_BGDlUfXlV$byhc5_{_d@Xg5 ziCwf|zRQZjDi)WQ9Ouj5vRU1PWlU-y0*5bpX|osI!TaRUp#U4w;QW)S$%&d|Fyt${ zjChj78>2!eUV{rbv&6+2n~qZiPg~6ES#4+o0sL9&dUX2oS1ZvRTT(AXhBy~AshgqK zk-G>N!_pUl3hj6SB)fhnB_$zmU76BOGxG>u#kFlN18#c>_%C<*w4lJ zCwJNSd1p7CMN8xI9IA#A+nvIQ9PZxR^mRAP10%k)2~$U>D+Tp9sm$vJ&h-mBh|`G3 zdmwO4ntJSeLDs|sKTxl2%V{1+cx@p!OW9ZfbIP&1N2b+`mb1k$4CTw5>~rFj(DU+Y z!+X9!&K&Z=vR?{j=t!TCc%R9Wj;^l{GH=YO)VHKLq2QqpSh;ptHl?th{>T?=bc^rP z;@GMGD?(rties*(Z{DFP& z?$Qtu{RPQ5Mi*dPi_5i8+TGGa`a0)REQM&a8S|!)wvfC~pcz%~)PmB{EFWJk7zHO& zrpp=z`>Y*4wbagty{(W`nfR`Q6$&lNGdI(>jOT1JVwnP~dQI@0-LY!q#QhEsO7U;a zl|5jn8y0+QK$$T)F(0T3sVb9R$*Qud zU5eAmHqy~vWA}?8-HiI8snOue6iExcRi}xe0+w1=*lJwJvZ?$W0si7Mgq%Cm zS!zW01vx1xR*;lLS6vqqE2tp`te}`3PD}L(06^DBb$jlkb5d@~-~AG=ke0EoZiPlY z*5!pAiwB3+#ibQL5DY92)Sfa3f`o6w2{jaij8)7?8B90Wk|wmfRh14)HE5SVPYOZV z-IHx|o!JqBqGc8@XCCBrSfx(zY6pGMFtrS_6+!fV)U|!hE0VgTPBQ9f?v_eyU4Wk3LoULcA|oX?J&hjO zJ-V7gw-PB&$B?tYOUvs}qp<^#w$+#^B>>Xr;}GA%X#nE8d=oI$lPkAL>c>>|3a2G` z$p5yBL+(^CIIMauudv(yCY|fu8)AB^0$ZhUOF^!;J=TuafIBF^Pv0OVH%>&ErS@=p z%H}l*0CTZpNY(&o+5t2?XHLExU;;k`pylon#PLjupnN>6|Mc-}24FJ3yWVvx@vo>B zmv~K71_1cIs7pQVTY39+#pmQKbkhPLGlH|iGd2Nhc7Ue&zcIsj+oaVus3?D#G*8^{ zkwD5V0NvOKH^|?VEXiZAWa8gzIED0KG=BT3q?27O&OvHR4_5kwTxgv3@On+NVXgZU z9#1ILw2?xxKjAyz3#s%nt6D9OLb7&n%3TlYqyuJ1oUF0F z>n(?|MA@QycZ!%&#L|MN%bK1BYm+`7+S*xOm}**A)Ac82ZCnsNG|mVqQ{@qGwY~bb z){o{pz+nBOlADoHwsS2ObE+$lzCbhq%%UeF!cm+*&dE2gKj~o4zFy`aq-ftamdCms zn>fhr*y|6ulDet?a}}4e?%3fVH}ykN=1iKIbTdd|jtvMT?aEXd8@v0E92^l{$~q8` zl_EP_!o1dvY*2jBYqqjtBpR2NSu;}}Bmx-%8(r=Em zxN5;~6Kq5)WiaA>^M%r+lq0utj=QlMqa*D>)@?0g*M3>lp@C7s<^p z9xX}d(U~rjKcB=C3UN>wT#0{~u()Q>yyjgpt#4Ej$9;iE<;b@3>P$`#OqNVuS73 zwBJp6_YpM}R3{z}XV8J~b&c8rm>KL!K>Ci_hX^`~YuicNoE{~<`Q<@6_k}rtTI3Gq zAZD=(Pj#Xo+u%*u-04HWA4~^nL+cvD1T1DrJ#TRbRtv_)F(!r?R#~7bHN=)@jAnG~ zsNhU9T>#$Dr*cQnN0xPULm&7tB?vVVkZdq|L(H&qyooBn)$~_CpKrF}pII-E+n++< z!*tHu<+ePg#FsYtR63;<2r$TH&X6kNbOFVI7u5i>S(qlTNEnz^vwGP`k{Ja2j+vw{ zWD`NyQ7aQ*6q3u1?AAjVs|4JRP=3P$x2JDo>Evjd14_extq`_ zl~Q86c+zERx_j12UJ3{8n*Y*#xB8RAXyK zXl5Mev605|e46M072Tk=y!C`4-8zp*WZ-l8_w@!XvgQhF?6(nRm0PteqCZw=hAR=9jBu|^^ zuVnZv{mo~v2r~;?KbVd^a6AAPZ;LZWmfLz&G}1*t2KYE%&DMo5FCfG)=1Td9DN^IH z$};JMuC0}?Hd8HG;v4O53`AZYIWnw(_`I>3vVa%*I*`v`;f+drL3v`i9o~3HED#7Z zinhU~OJFBFvC^W>%a4-ruheRbcU`qz!hqFzjiUokqO&ABC+gO;TCxmbpA(5+3NOA2 zwcCrlE8_g6p=aE+IdPaClKX*!*ff$m(94syOIU!)e!zmO;9h^l!d~&v)M_bD-%LHi z4iyAwEN6#jl2E`*52BZrF{5RJqAIWc^+M~AgP(PEOcz2fE*HLRwn@;{`|_{9mf*ff z9T^CLPUM)U!0p&MH7wQ~+?#k=xpzEWFx zut6Rcq~mcO-Sc<{b5_z_v%_#S^HW?W&E=&d&M5f^!ifhuAiHDhX*>G>=%XIc5kh`ULCzk{SHW_`srn`FSC~z ztvL$q!tL2#t^Vrl$Qo+xNNy zwj8*PlkJ&IBtIVbX%vi=Pj|@of?2+8Uj$a3;(AIZzH~=LVR49TE;MQ%rT!O<+ld?f zgP6pht9o*9jIZi|r}p6505ykddo7p-KO|FqJ1 zr$Wk#>>jxkP7O`N63D3plxLhuE2L#D;jTk zi<+&kG$+WbP5TZw^#2aX{0{gINS9BSyp%xr-HEs!=&5~TpN7Mlhb$q~HAtvLyjNd+ z+uw8}629`m(C@9bV0*zS>7rY-0k=wcW{2D8wcu7X4$7_f*Nd;-_`uo5N4SE88QF-l zY!Vcm+KudLp5F3yUo;%hQ0X}Y8bw6)=u#8_$ik&}#7myH%Gpu}lxGv6i^eNXA?<}t zq>u~48zc^Hzyf`y24Ku!HsBAI=i#3q3^wSmdj2kz^z`Gig}%QI;on-A+MSwtewkADgGelYdJGsE`04Xt=1I`Cw&B$#KOe;OVI_6~#k+po_s*%7~&Em;09q*!y@|gMO ze|~3DmhHLwmXqd6qPcoM*8(Z6u|U=kT@KZwoj-(nj9~_2<=UbOtOe2@Jap`DkzVQ@KlRU5Wq)I7EJgL%2!@DMt}=QeLU%GM9@JP5OL9i!J>Oq2 z;<_+|cie6fUHXa+mAzQKv2;YI%kiQAJAm2SW@t3J{JQ#v>npjw^=I1gZFl;lzM}DC zf%TXYU)ykcI~wVFl5lNMk`x-OvD4{vTK*;V#y%;{!0>cfd|sFln= zz79zj{^Iz!yM9dGvX`eB=pfVJM8v`w7>@Ociox7j!L2LAV4;G8iqX-B{FcR7>*6EH z8&{GOxe(LI?*QCt!t!3sM+K~sTD(xc$+5B$+CL0Vq%4+2&r{uF;#p&XR4wv6fscGZ z2VT!s)Cx%1hh6BzkH(INM$Ic%?)~vg{=BrGFcUwi^ON5N%A*8XNGv30gES?oe zbwm?3{aR=wD0Mby8b%+sZbb*HzK$NQT(l{#2&YF1)A@iXyi$WMCf*o;r$obK zwLL$6n{3jGM!oO0(y=%`>cK&B=CK41dUK=PxJoj{{BgBIyB8QosVy;`Q&IB_vc}b< zhm!@jvE-LWxRh;dsf25i@D~{D_v%IdBw3{B8t|5xxVJSW->}<-vQbTsx*qy ze0;8o?iLAOnEIKuk7#+hNl)TB&js*S>Uu(By2T0Z+?{l!ak{u!_zqA*^`ZNnm=`Pt-8P1e#KCDF#HGq!J z5?O2ai$I4OG1RQs{Q*e^Qk!1_>ehkXD}MKMcZUdW9piAI6;Da$5BCzL!M~8lStzu% z+)~EL9(e*l{V{oa_n^I~dwT>UtbnH9S>3iZs&s~H98yFwCgXQL*9gmu!c!uPmFket znQIgXNkzsosu$&?N`dcpJ5WgWiK@#SCVd?1eKWdmkHaFtD{i?w3PVm6@7Hvoj z$w@CkwO<}$l{S}Dj=agsLbz6!a1vre&u~d7D{CP$^5@oJvf<6CTSPA?F5T+ys%^2c zW0+ZmaR^h6LYniT5z*<;5S45p(XEn?xb)g_c8GM*mwW1XT#5#fY06Dsq-#{m+Y!J; zhaR2r3k$EUoGD{&bz~dq2_L0aF3kohq;{IlfOj#DI4D^H&tF=%=8Atj=~c=40^*gK zA_xNNrh*lwOUY}DUeH7Q00TIm5hY1kaE;pY;73BCh6Hh&!Ch|_26__dcT?h1WO5P$ z3926L=tP$m;#b?)f~KT+f}A}Z1ZhR`M@7%VlQI2>YH3R?Q|dvcAPxjHB}@Yo1|kOY z(_bjtfZ$mx5tq3i>*hZWt)e`4D3Y|h1wj9EWB&g0UpWUVRJd)>_utIuPRa4T{?W+% zM}vH^B})KZ{5|$}fa{A7-dSjmUc z#NCWeu?zjT-YR)nnuh({g-NORYT|R z3|B;{u2<#>ee8WIW3ki!%u{P0^a?PbMJ2hh#U?dxyfImE%9I==v&ww=iA^0P&e^yZ zPq9zj%h8Ve9kACiPyc0b`T8?#jy1$z> zu3hxtxSa0%jnyt6cV@WB9696igUk8|_<#EEsEkqZxcHsr&r;5rsrwvyU2=oy}Lf?d{^3yhdoBOnWCg?e*Gsz_*o);AwUB zP2=749REjQ7j&J*Him_`Hm}IwcruemSTF5)Cz9xfd#5aN4VK&)^a(yg27BljB2M@f zLxrD?Pn8=2p1U$xL*t$EMCPJ!FCn~;G;$%c88)*kc2%j#H!UVhQPcz~hp7$j9B=YC z2`5+4mQ9vH-FM)IjPhV;Qam_|to-vefp$k~o(_w4nAj1m_;&!4z4^UPG1}|T{;2+i zJpHsAqs(mNRDoz-5+~qFQqaaGoZc^9p;c4h1OncX>-w16?T1iaC&1J@l2NzB^oJgxKZ7QmJ$Y!OsfFn@Ix`@V2JDD*tS3T8w_ zkwY5IU4y8*hR*Mpis9$y_hZAtoo~g_GxE|GFoTWu;lTZBj5pk|)}HrEJt=tjIP&1k z3`V_#pF^Xl#$#i7F?&~6O*F>|IG8t#bf(MY+<|99Lv8^8>_jn@QI`H8c5xL&lN2R$ z9p$br9$mvQF(kT(HSR8|%nNUdFG_6bvvh~rp^{G>)fRab$1&zUuE`s z(QUc{TWfcovm?jBTwns}1 zWg$@1Zt7iNwY{&BQ+$O!vUNXagiSEqRp{c6Xg??V4C!-e1J9BCMtwQ?k@dC}MHhzbndyiD-d$^Te2*3!+@18aH6qA3s=!hA;S zUNg-~bXdtO3#M%w)n)+mUKy^j>ouZ44ENNp4dVT}p0Il{^S+v>l)-8sSEXVs!NPeY zMPn;cmM&o@)m-K?WzAPSXbL=zw#)K_=uSIIsQJGV1?TZ%Y@Z@F5U8K+5g#Jv{iS!C&bv-eh%|Qzeq-`}I2j{vDv) z83349_WOm79M1)x25Ma40WT9HA(3#i`wO^=2clMcI{c}B_-!`t+*kSznDk3I80aGY zx%KWBUj1MD6CwD<*rR(ai=WVrt-PE?94X6u?vOa3&6J;U;(swdi|TgjYlx~3*ftsJ z{^u!H68^poz)ZD&m$AL=b^XXeu8-;3A0P4AV?}lwvyE1SWoj5s4*+#CR(^iYmJuIg zv8QypNcDz`Vz4P4yGSwL7k#|{?R|)Uu^wFm_3kGwj-<@aV?&>RhNFG?SIRXmbjx9f zUEu2GKMPX(pE|x`eo@fo9bm|PRkGDo6AT|TUd_oAm{_Lq;At;`_8svqNXq;;>tePG zdy3u|wfZn#6>JrW`x{=GUzuiIiqvK71@y*#y2khLv=Moa7ERZ2K@$Kl*1q5FL&eR1 zrD9HIoRz)ZWX@|V7+WJPYpsEIE@mvXtCRb+wMSxSZ^E+$omwvNL<#^vVMfot&);{$ z5RJaG85){j7t|d_-H>TU#joCo{kVA8iW?@xK*<>C=p0r<9H`)!`_@*P`yF6Z-N40$ zpKKt-2R{a^t~cClM2pvx)YtQ{)J~zIHI3ti?r0vQ8im?f&B^SIxSWbzi(8D>5E41`Wjk{7MnyKogD|14iymUN(D0i6qep#(g^X*Tagi{1mH0d0 zUP>ptTy3##NR$`fp(6oDSSi&4nx;x756Y-l6j?fpcm{Y}pdbmeRxbdH#Kz9`^*D&} z)`bcC?-*+j+9tc0$aRY;#E4cMf{;=P3T0Tt6xLf7SYDTe6mTugB)7 zhTAXxH`V#I8=tQUvCN_!4CYyX^TG7pcz|bpuU_vw)Zo;l*Us_JXkF^1P=l7o&!`mV zm@^hLKg4RZ8s5;thYOw&S6r)-~OStM=s!hAT6$5r{51?PA}bvTaW(Wo^_>qq#n z8#p8xwd=mU@q0JxZgV=$ebsnCQ;qv)v5MyV7ym3)G4rot6=;_K*ky!)F@0 z4dOlb9T@nWeB^BEa`ir|!26X=6VBOknlcobChhE4E-^@moPs{?mc)D$8~wm0DdDbV zGKq2UdB_miOifkK%j{jm(KIie_R>_acw(u&eBh>L(GzA}ffI`FVZu=tzy3)7{`KZN z@B2PhOixG5+Mx%7Zyvg)o}yD`rxZeJzgbHal&ZBu%dvS?-!6s-99n#x$i8C=(3pZOU~d4B`vNASq1OP!QRA|>9pb#)<--f7<4(jPv+XKtgj zbIhKpZ6cOb(X!_oat&Hw1M|?ze03PN!wS#)>f)`lxhouF#>UDWk7+5w4l{;Et};v% zeyH}IHr5&V(3p==`A&Cq?U3)}$oyesBU4g$E9P$db$zP#acmfhcfQByG&7qE(fEor zOBkqr-49&&#N!vC_f&P2lxv_^IF}X#dud0YBGlv_rz5+_Vqg!EYoiygi|qnZj2Lw6U__LoA?lR$J(7ESDd4uioaY$w9t&*^hysI|`u#E5 z(^#(;RKeehxbq?Wd`9P+9*g=t-OLKk-m`tfvQ~)!<`3^LP)BB%;p(&H+00ykLXi`JpRPMgbo6obO2e!1Yww4xqhQ6US~FJ|32Q~{+m6w00Tdi)-NXu? zR|NVK7Dq@kBP)PC21dO7lJC`M7gT<4s-7b|aVQb7M&8bRZx5&C z@lygKU0qHdh44TI%OQk}uU7LSJem76X&(sP&wtD}{dI&(wMeaAX}p+8zFTT=-jPPR zkg8z>38sgW?3ZUQodz^-5#$;>1z0S--|Q*UI{vbt&T<(Fr)AFa!A+RY6w= zOP8<6`8=^!CE3h-l~#g}as5w~FeG#_6vn(uyVv7h_OjA`2ec7uR^X20gI5dNJmy_v zr9=o2QxohC(y8Tsf~<31*Mf{*u-nm-vdrggvPht$RI2(N{0jrR>JP}ks&@q1vMhyE zU6I8lHP0q2HYn~BAxeClT8S#I z6p{m@cf2$HFY?|ytf_8K8wLwXmEMa$=sf{MszN9U%}|A)NbkLORC+=Qy(5s&q)0D{ zD7}OhiZtm(svwF;(KkNdocBCuzW19m*UUNZH*?MY2lkb)vNvqjTEF{umu%g$y}IxE zPYMvLZMl#SzuRveENS}|B+(Vf@*}OnM*@*5hMqP6+-Eihnlii-Xqs1JB0Au~e~WdN zP{v#OgezzzCUM68{>qHy_|(L4?^T~$ zE#ktp>qq3XJE1|p{hmPY9U9x0gz%7$*q_3xXA;se_`)ur+WgB8^NIgTpxV1rW%16A znhM#+yzv;T&0&>$RksA0n7boA;^a8OyH)pG`*b!z9b>-;y7Bc#@n0KU5i>Y-t>SmW z_bg-1ZwZvLiP&279kn%mfX7ZXe)kBIi8kfe*Wc09WENG|gBJ0O?{$NZkcxSRC5MUg zoaqhUI)yc>YpW@|G{)-%DoFs8l>(=}-;#BBwy+bSo;(c5>m%F|{Yw_TfpaL27|Er? zEz#3sGp51m9%!xEzd&kF*n2O$4y@!ZZ(V?+mzUD8fXqHqDDPN;a_{?r_~=>p%8~^w zO>12)bFCT9b_U67Wo(B14P6=bc+x}nOsk#9ww`W z@P-gsR4>>{=8JtGN;LIh9sGEpw|rDo;RHnFVplhh4m?n>(N^j&`@6ZO@lYkEObh`5 z9VPYYk4m`S4>K~bwf^9gON?*o8bZ+;gRdtR32#dhoeb+QWnmTLmUkwa=NV)#J|<3D zjoaV3ZwwUSwgw4oFeqdw-yvq#j6r0`4>^FMOsJSslet>+q95l-lKaT_{pjx0R2?m$ zd>L(>d?DPr1&5*7_R#45cm5%iLghppR+Y&|ISJ{97pi^VIZIFV?8?Hy& znuYr+3E$u{2vx(e=-&6zOT4kBsgYC+jxPMK4bX#q!(O1p+uC%IhWjCgufY=r7b^AB zOz_=j{)24Y82a{sF73o7;l$jaSeiM*72!bi7k>4*BsGV)C}UCQf*RR z@9!XWKO69>6U7IJW{gw>DBUQHqoCPv2YG1Dg$Bq0yuY}G+lyAoz&7ZC*dDd2r(R^1OebQ$4Gv{kxlo4a; zpHBY%xc}gH5ca^;IeXyuJcexgdR*{ch;vt~>_%*5gnssKR-f(~E8dgM$a13{*&mYs zP7=A-um8F^_d0hg`#2bHfaq%mQ|J(Bm;uz0pvR z`9+ZDz%jh0V=bT7s0-?3R6s7*Dc)-5I5{802c1p5JZPG+aWe?g;BePd z)b^XW`HJArqeV#;dgpfs%Xd3w&;rE2*ysord#|NXku<(CJxGb0gb2qg)VhKCEyW4> zE&Wv+p2gVDY0}p7hWSpm<6!}+la^mSlg|!yAwe5R#^JJ%!;8&e^&WT z5FW;_{n+$8yZ4J==R=4%;@(wWsANPp6T3*C75LQ#c9Q0*F?({fF&_{HhzGa$OW)+AbF1q)<#4K0tkyh`b)27*9( zRhI4h>G%MQ=cheJ%V`gMe3&QO``2u=nAGY@S*Bo2><~SjAIFI#1PY;otMQshsR&g! zqdV7Pk(%}nHbl4_*MV8D-*=T2XX%$sDMpm@ zh23nnjN&nP*leO^ET!pgpV>!Ey{#Z|HqqY$@^ljy0&PgkJ}>7VP4? z@uBmPG)sqc)u(rl@p}48+bfH|2&(D(b2d%ZmA9z!aV<^YjUaylQ!YP*uJIO; zqJ>R-`{)x=T35W6djKh!>pAI#Z`Xn_RZq}DIKFi7dhbxZ^!0D+18Gi7>8m$$KOQUo z(V_TD-{N1aV*g_uieCix#_NTZD{TzS3a7J?IZEA<%{u*%nUSq!ZtAN#N?WGt$Nr;p z_Erk??XM(tJC(jWvB~9!i9Tvi`KkAHqM+jsZrw(rsl1`7k`|_JF5iKWxiyFA92y~I z0)R@+eD2_k1JAGAId?=W-tzj6piIJ-4i%jj3fZew0Rmk=0GgCX6Va?Lr5vD1NMWOl zU$Zh9L|0#*KIHqv;vL08gtzF)$b*jDo)D^)oUdPcgxLBWK zWBMv}Nc~h(ke2Bv1Q`qj${UplhaDH35)-~}WMcyLob_6X$C!b&v{K>&rt&5mb2V*k z@eAgaE}7-k-m;s93c}g_o31G9A-5yjP`a779^th@HzgQumc*uh_&TZ8_<>|!#uqMn zyd#@as0QB)Ap3|WLS=qXak%<$NT5X$wq5n`KR-c4CwrEqNlaqq;8#)<7UDJRFRn{4wyRwSmyc=C+e_@fTX7O1S7vuGC~lkt{v36${^WD*pWzbS1&H+sH&Wg53%}#CP*x?e!p5XI>)|CwhPNh67EktcaEgtIQX7C9QW}r7t zan3*RzD$r)vOsonW(v4Op26vJdK_C!8fVK*OFg$9e3Xz>hQtnf=#8y}4oX&M)kXy| z5nWy}tw;nDK*$6*iNBN{d#0vVEV2V?AAJ$-f8jpv&mj&HXSAzw9KrCz$P~k$SJU$^{fFiw*7Td z2Sg<&!!R3gta{qgq-~lIrm1f*ipE-FIm>Ols}o!NK)DJ%Dy@K&PN)3ts_8jptx=Ch zczQ%>X21F}*W$A{7wtEllXJMD+T@r$D;bNQF^sV>nU)6v&=S2~p5ekR;*9iQ%j8+J zQDMZ-tGMm02-WD*N+0KH#uWaF0T@J*!7=6;-ghQ1U1)LLtlJXysd-=8?-;*#njx&D z{O_=&{c(-|!|||jtcTIZcQOURbYZ7Yzeo3&-7rNsIAeV(Yg&ATLSmv#_cJLld!I@C zd{JfwVoyr^TuuDm>B)s!fDevLx4oRPFu4wq_S+&y9Pi*<&VHZq_ArXdvox>1d~CU1 z#QS3l-*X>X{YI0;_5;r#_y3ok`xXvQ8^6Ux&$z#l5kW+Rw>dX;8s`u?3;I{=GKL&{ zAd6VqrNYzdor(P-s~>L5_k>^k{D z9j-rTFX>D@ei6vxlyZ7QCsdd|HSRwx3dI|py?!{5{r~a8_QLTG&;7lEFBv7dmzQq) ziANF9e_o|@4yZ|N3kwXs<|La4TYH*@n;JO-G0D0}BXtXM*@X{pkMS9Z0P{)F3IyO! z{4x&*Ds&Uo3Y~wCh}p`J*DRuLvzUgu%+?I(py>fXRPhEd(+2s)Xcm5LQ#8a#(Od^U z52WX8$*i)?yN^-*sd0jnNRd$y5G)Evcm+##xEAbmU8l3jRlGAj8KyvKYs@vD<^L|I zEdH*fr!U)d%CpMk^yd%`&ggz@#|wEoB9G)+qe>WE%sSZSOH?;}*X!~5XwC&H>_O@U zbJ94af1`1yYF*P~TlKOvCm{hr{-^^rIFg7Q_j;biJXm~d4-IuC!*u6HH8|K-Yw5mn z5Si;ua|-T8g@29h#Hv+#Bdc{Xib{?F3Nnw2R#xa6vY7bfP|n-b=jvy4!?X>)dAc4{ zCr>tEL|+Gan7eI<=b>=NjfQjh{1LL5;6oIMT9aY-nR=CAf{p8U0eAnEM+&*B2B6l6 zpRx-jHne;2>DlVlVQ6y%_Cfe9v~3Cx0E?{W7U^0(OD$J}K(+AGI3+RYMV_{vRs$;O zKGu;q)j+n88#@;$((_`UY$Xk6XJP-pQ8CNndr|CaDUxUDgE+Hfi!AiePbeNVtBTG* z8*FRXIEw1@TBj)VUinbyqoS)Hcu&L1Q586Ui>KfBk#Bc;zG0)G*DMN85`aDu^LFdS z>Q-h~zS=1^FsnxuvWBmvj?$uk5xfMfzua%#@}i_jF0L4y_LwwPgs_0JI2#oo99EUP zChOVwP|*jNMLobYq~O$vEp-ZPD{`9Lb!c4QYcugjaxI;8Y{Wt#uC+`LucuJ)oc#(L?Lw$N5Dq#fRV&+*$-iDJ~;4~d_@-zGUin6Wk?Y1beeF&=96E53m^#@&6k}*{&9E zrYr|bf}@<_L_AHoobLeALZLQUlUuX0zV;W>4VDvf7gEe}Z%48_?}G>pkQ&P#nJ_Vf0P{30rCJk!ol6EMt^ zsXh~rP=N?hle>HbKc)s9hT}f{o`n8;9Dibv{STy*<$rvhv|M8rvP^gczcTwUy2lQ8 zPj)JOWx6V7=#AVh;NV)-6WMeV)6Tr%H(l+%wQjTr>y_ULFn+)pe)C14N52RnduU$Y zvL_8zk8W>)b7^wRk>;vh^i%_CJe&5yO)WjSu_jj1dHs)zblo)S)J(TL39aJ4mHg&V zU*b*ASc2F?C7vWKx#K<1^qNSAJ%15o4F1gjTSA3GWXY^^V%n0}Z+`W+f8ke$o80^T zfL%);d%j;$a;i|UcoX+0R<(Z6i!jl}5Y$=cRH0Jn$IJRN!9Qu9u5~#s@{#XmDR|wu zGUiJM0S4P%J4|2LI_#?Xw9F0S2|wtuvP^>en& zSxIAb^WJ{B*h8T4$(EBa|wC)fKrwcMsjEZNG#6!&DOZCvyuXlhF*9nM1 zaHLwypV=_wCB|2}6Y@19R+kH>9q&x0v}yOS?4u&3(6$n zWLs#_^t5Tk25dHvE=&FyTd<|g;jR?TOKv%l9`*^YMudf(OVNhGcI<;GMh4=iHL&DU ze;Jctz?PA`gD+rfSd0KT~hfLzVt8a;<1>r2WB%TFmbt|_;XL;~hDdVgifZN<$ z)N^7Hj!lW^JWR3siQZ1ZQyD(45vh+pkC;p9g-p-8m4E;8|C=2D@SQ*5T1BO4R|S_B zDgI&Cl6&Mdr{~~|>_3L3cd>Va6(kN9El6dfyJs}-5;8`;;HFlc zmpp@*BIFn<^)%wPYE|9ZxCj9+1(0uDq8hLD8nWGDBBtU&!gagaGFUw1p~uiY3r?lg)Um$g0+B2ix_?s9KME2`-zAkHJ}%}SR(T?Fm-4*K zrDTG&vC5GPss~HV%F5c~;{3!TGZmRK7*C5VY79qa%i5;TJDJV94|%dptxg75gNs2Q zJqWmVq1Dpa&x%+1YV80SyGNA7kBb409y(0r0!^*E0%~e6vmWZ5mX^$^-Xb)Uk%J5-NpR4iUT`){;?Q`4*xt~Bev6I{$IoU#WDcHYW!mq>MUZtfBpeK9{+OC*&4mNdi@=>rP&Ppy70iLX6HM0`a1T zt2hCG<6X&;ZoK=e&M$%=YIk3Fhjs@;EA1qCBnU~?=kPZw)m$(MB^Cm(CCr}mDr9XB zp(|o=KJ!Mv=3{TAOkl?i)do5g!+xBb2FM zHx!Q=g3%kVE(oN8ufCNf;f1qMiN@Q{$3*%#w>_UrLt|4P10}`?2;?|3%?wl?48tF7 zU>cTkN4l>rJV~cjUwh2?Ze_2@ak7o;Zs|uYm8zUrh~!+ljU{jn(lkDW%rpVMZ#$8G zo{cq02qhOVXUCBXm>LFI+xpIq020_fnPVsz4aOQVOu&?T+J$fra@kT-?_{0ZxS}aT zJPlps`IT+98?g^$UP?swWJSoksA)I3z3F_Qq31PK%iSolED=M}s#Rg7kfj3Ipmgm~ zQ*F@(4nd>@92FwK%6hm3)|MW*bSXBuTqwXzHqTY-EPe{zY&Pc0-L(IX%b#pFKAXcb z>Sq5!MX8SFCbgPD)JH^=cg$F0S#t6uXnM{jFNy>|RnP&qy@MAO*^PxFd<8`Xx*LW> z_)J_y)GfJ0Pm}6G2)@SVYG!4Y!1yEhlA*koYp2cnmw=r-q%}J^Rw{kearS@)|nqs-Q1Q;xKZ#8-U*F|hNG=C)l}$8tv;Y5ml0elCRPnDXTj7W+n&2{Dut)t zi_^Ql6TuVau0li2e-V61h>d!Bc7Nyzm#u_e01QNeTXY|k-Go4x3KotjhfLuPn(_Wt z@ifd(WX1S(VN0fH7VG6K&p{a5xeWko!G9_3ose~}TeE04eX?60YBQPombE&P4q2@+ z#7&s^s^;ddymc9a);J|niFuy)EOyJkAz8De7r69<9u@W!qa=p^?@~uEubkLq1~Yc7 zDoCdV8d`<2Sefmhig&v2LsuvV8V242H_OYf+06^^+GUa%OOF|A5CbZJUTpknCCpdY z`ZMAsnt{sHjxdzt7K$;^>aMo;1WwRP7?G+aXI#C*GFglCO07;ut%vDzSHO&^%rD zF=EYLlRgaRX>`cf7pvX8w&^nlZP~f59m$+3F7ddN{l;r>yPL1Xc# zCxSe<)7gejW@FK4Gq9Oef3yWUaEsM6RsiCil|?yIO=W>4VjQQWU6<4TQh#4TJV5So zER4Q&DS)9$G$JXnjeSKQm2#t2&9$d+MQQ~Dfte${`*CaS>luz_n;7S*5wpK{xBvLp zSF2usVU5PCg!;HSXicmY3w|=)iW?GVb8KA#wd}%{?8Ntsx<)M$8uze>PQ3rcol{j% zE8ei;LHXn1c9!d)*J1AoFy8f>v|?^pqt__Yb37v;C`?!5k5I?F8m}WnuKM$iH+VaY zZePXRnCnG+q2qtB=v7fDDu2~<@jbizLwf#?(OF+f$wDvR5{AXkguQ#njc`@X#Pb@; zm#%NlZVhb&g(*F8dfRYrpZODhN{Q$CmEyU64o&!8-rovq|1*pK-{_pB6hu~V*|WnAcZ+% z66i~ciG7jAR4qds9wH1&R%q!}USZ1U>#0kTx>bqCU{4F>CnvX6WuprzxTP3&2ec~ zuD5aMevNouBcS4g88S|cUz7CPQ;X4AjnoS!516R_RPNQN;4CUK8GLT^ASC&%by7UD zuKw|M6YAR>Px8Z0^`4$}x-=Ax3)mEw2;R`x27qs(sglkPR>c8W*1ypX^k_!LYSn3D zL@*(|)^>|9f)67=PV&`nuJhL52m>zw8j)FK_Ufo}OJA40yaG}Mp`PX3u5>!81|*zS z_}JD9eV-HwznVP%kyE zpu7?ozmBe(Y}1w=%0=El)JW)aprBxje4f71z&!dLHfcUG8J$Os)Cia+*S>4=Y*Nov z+tUs@?QQZRNn3z{_b7~e{;ABVRp5e?n6$MuL@Gsg}BY$dm_{1I>B;fmr?|%Pt}oPm5>WQ~v=^kPvsM|3zSwJPFH) zw8lGGFa}6=<(ic_w6r{Yrd(_mNY_(*aqeVPDk+QFc@= z4Ql3I158TOw&LnHjO%j>h&s-B0qKc#BemJ=s*`ELaI!H874)W+Zoc<_A2-LSb2uF$ z*oBpP7GAb$(a3WoGMJ3Z_IjAGUNuXVc%!tY3F!7C_s)(4+dECszi{xl?pY=9eR`_1 zD&K1u_t5V;s4QF4j)r>uRi0k|g!f5bGbhLCUr2q4bfrp7%EcGCN(cQ!_j@b#$L2z! zK(lJhD{pAqFtH8|?^N#yX+j;qJLGE6k8{b0POh8(_(}SoGU7i2+uROFFEWMSYRX{t z;f*YG;D#pKSp7~P!Gqh(oYUm1H*RBQR8lLolxZ`!EOXX4P2`vd>_I&I6Z2G#sD;=& zz}7k2a8g4lU1<4TGV(jz(W|#@_Au$Npge>_)@P7hR{k-F;r|D%04v=aFIKu{xrYS7 zavLon9CY>KY|vSJ0cmPLlRkhsxZj7p&?W6X8ifGrUMG$XS}7n7$l0m*D>`{cJUB+O&1jdY0+5 zJGif1#DklSY{MgwOINpKuC8ANs2Tlzg#Q4;q592pM*I(H>$ettqy93}6MaL2)OoM% z;Px**+d8EYbhSQdlD4#KogT+hS}6qaP~QX3Bj2MC_Btx$?>Q#$La>LV_@+eq@{U0gLYvt10MUbO9DH~4uVRauq3@Ihob zk%Q0y^Em;vKBNj*DzhT&m{795#aWVV^ty^AdsLM_VPG0qOMSD-8bv?R{-rr;yp-jI zJAH?t9mnlZS6rc4^eR(uw&A3jaVzQ=sX+iVN@{7m5ucgGWPWcTKdq6rOPdx{LldV{ z$R5}YhX06?A|TB4w(hehBdl6=oQ;&fSZQ7{B=4>}GlelN$z^fK)LI<~bXrOCVUMnm zJlLzv{MwY-CA%$yc0?njsHiw%tX}<7=GA|~p#N~R^`CH!|E?kNbZO5`*68al3xoxC zurd|%&Wd@>iP5jZ-C-_6{yUFV-}v%ZtC>(VI=9yW2`AgLNbKA~o?(KdDe>fqFc}hc z;71Gawsk;=W$U=|xBC8Y_^MK`;cEdT83f_j$gfDNNPquic_}oB-`&Nls_K zwYaF77Pr}I;f(9th>%O%4_9jSe=oH7W$J+b!>KtP`Sr&WFlxVImOBqshLjs8yoAz= zkWHvWe%>YGj(J1Q!=QpU4((bl8r%b2=AYQw06g2KA_ZL>srU~TusXtTzX(fTb?w@6{=$l zSdDj6+~EVl-zHv4gyp(m=^CKUA*-wKmWr%yc@Y;##M868a#X>bV33YBn#_ClU4Pr0 zHjk}2?0w<%d}z08XAdW!*u~apjoLvnKX}85VEP_Ke~e$DxhFkOqx7zj^n!BJZ+l9m zo4W)Atg|-#(Mes9X?Pp}(2B)FjMFP>j0Q4YaODTy^{?WwR`7u7DcX8XUl*cAvwq|W z;5)?|-7ZrEvD3{q-ZB}l#OHu=$7qxCMyfp^h)ZFiOKwFm;mmmDy4`hioHrS8^MQ-( zyaN&h_w^u(d?X;5w^zI%YJXSJYGM)0f zwWQwC*Hn6_N#~etGuT^DR9p-grXGgbOH$uVGKS4es`+wnvhQlibISAdGCda;@14x{ zc1+Bem4@mS@8^hy=mU>pv;Jz7NA&96ziVp!ukfaSD9`-{7FqqoXn}sPS44_UmlKU~ z)o~PfDNd=)M4jscHwdCm7M?6#NahADs9+5B!G2S^plpZit#*tLZS655u>r7{7dCd7 zw2vxxeY*-*)`-DkCyVh>Z=>(H9|d5@&Mc_EQgGZadUOqJ&wtt!>mej}b}|o=yj?eB zmTIe>trtKBx|=)VC{vtJRx+M)=UcpWOLOXGd0tIL%0BE>9fay(Of$J({CCYx@rshc zPTT303t7;&(d)Z64#Hwr4SK4ojl6Z&CSJ!`VUIo#V>G3Ug!~yM^WGpc=;2>p3&6~| z>E8}$-JYI0=R!<1emhHHa9W&eT5YW9uxSI|pQ?RWN1mev+YhRJyji(-FxefrX&Z)H zvRkOz$5Zgun=9<5s95!(#tAc;((c)oO;0V<>(i-MNPEBN!TTDXB|Ohk;R0R}$V_K_ zTU^&5d86;}!gsWD!G)`)SC&_y7WaX%ggEqll|0**5r@&DK+BMnu{Q*#im~X2e|gHU z>NNV1h^BsVtoGr58jp!%C$gX+8xYh{vyPBKmu%H!&BPF`hFcCZ98nkd zh&HtR0DbW60|509yJr{NA0z$X3g{CLM)(}61R~+OwfW1swz;>t%PTw|@u| zDr-}~2`*|&P5C=j9P|?eC`5c~s|4Ecw7{a;hu59$H*t}4Y|-IC!CVKsF5 zDb8m1>Lj7jyr5g|){|Xhi39w;u;2(EwkSlBwkMXUFV~!To%^-4StQLO+iK&y3$3p6 zAzo%qYG%|mvMmCPT!zV8OB8b6n1zR&a zdS_y=k%Z>oj}QI<+3)I5mDPA%{3kHK3jSL8b$RU+qNMF!#pzl=FF`6Y)HwV*2gB4;jn1 z6=XTw(!KKl)5ufg@d;dcZU{!vm%~HVDv^+VpreAIwG={hrNXuxLq!rzy;n_{JLPq7 zI<=F9TJqU+GZcScK`1%N<(qZF0-TyT&@x^&n7Y_W~Br$dE0(+X584X$Wx=eAJ(s z3js?o&Kr-$0kRFMPwEi8oFmfX<6qeYCnvEzLTj9x6jefcfP(@o8a8%i%?vLDe{pAvjYcUOg1lfu*jI4>FVudu*#d~bvYga_P8QE znTkX5VMcSr-0j|}`t??l1uuydKfPVsNKWb1WLx!oJeK2yN-F8;xa9Cm+?mWdVJ39w zodpx6%?S*7C@bUW6ic%=5H7EoW5)6xMv=pv!M63=_iyPJ&B9gu!a#|aPoMhB*ZO2B zm`HSzJpE$Ogt5_lrV?eF91CG8zXrdVy2NNV;=l=f;r^3_>@c(pvrOkN*~~$8Yed6? zd0O#k>HQOCkAHij`>*sWO|*)eCEr`dn9HaL_CK9OH6E0H$NSrPOW*Kvd)slWwB7Q6 zfJLC@-M?}1goh7wc)x64YvXd0LORluP}O^EJ(A-Q5IeFIDe>iEbF2_ZI4hN)G1wrb z=d`3nP*|(Wn>;7N``; zSi|b{iDjX(bDM|NK{sCuoh{nMG^%`)d@p1{3N?8iA>~9XbiI~gu~@ELyd?Aj0&>;Wy?2+1eq$6y&jp2kkmCOWg>GbJ(P0q*A}b zsX+48pqN}5eC7*Xuw7GoEGl-27@lt}NqQ`N!S^gKL|=mDbwM)^aH);`hJ|?m-SA_) zQt}tUqnP$g=OvFECpJdAFC`3YP;Dwn7h)+vwn+~+n^rzA=&0+*#|R|-D-llk^NGGr2+gpB;y{AiNv(y&?}iFY5k zuZb_O&(-o0ufSEfeg{04li4ygsZMjVk*TW(&@2<5(FCrmi~GTjlVk7?Zt`Qy^yLfe ztlEC_Q7GFnAvO^g#%R6t>ijLAmEqzyu%XruzVF!F%zzl6yc#NXRcEs}K8s6`3Mwj1 zHy=3Vh1%zF1|FgUt4cktl?m>+MEl6m376Lg2#E3mnNRpLy@1nd3#mqJx}JytCe-jY zV3X)Y>iv<8grip;dv~Y@Pnhrx2ch(}QQ(3iW^7-e4i|(pJyxxfpHi7($gs*FaTmPB|B2S=?LR9c9^lqTHmqL)7hk>J>Xxp z1wAaavRsnH@a~ROPB+d1z{YyWJ+Nv%_-ar#@j2|UZ`tee_&MRWL$PQSV;*8cF#9!* zOaGpcD;FD0=5?Lr2d?~C>a?pf{sS(Rv#ZCgROuB}ULg4&FA5qt$U<)@Ey+k3cdG2; zy3Mzt9UmWrOTR2UVO|zxu!vBV3zw>-gA3n{`@Jr92PY<3+xntHo z+8C}OyRLI?)`~Y&-Jaw!zA}+rrKZ-_$)SHh6}SeGjR2&1>J%X>9p$2W`Ai+`OKeOXQvG~a&kbv-|@SgFu2cJF?GG35{Dp*{|YYD~K zY?_r7Qa4Sf)Fl^5Eg-omUyQ8E;Q0fOS5b5E~*4mBLoqCX4G*E)4w@At5WKOl6F*F zz}9nVV|x`!8%D-$%d)w&`T7JO!4g`urDI}>qoC74)M;-ZP~<3A?PdVw#Ck#$u3q~t z0#?Vwx>$cspWZph63`!IQFwdS0bNL*l2}Df69-711P2a+q?p(aBt`@*C;-b?vN`yGLiTMk&TtIpI$Ydb2_~1BYzVCzAuokjHu&={mRHt|zZJ15iOf z89AJMBrg3U&aS!N*#l(Fcc=UP?0qh0ky3OG4pi>r!8|cZZLa)N@#tmNy_Gc^Z)s280Yl0BwqJ3Ic!Rndi%a?Z$-(ol(9xOT{@Ah z7Tng6wM&<3gRj1Bwa76qc6u(USf{nD+QupAjj=rQoZbTigAgx?QuO#CuEnQE_5Dx@ z_$yrNMPquMS~>L|*v?u2I>aT;l3V<;O^9-XN9o1Rtdy#VR8DZUG3$apxNa^*-_{x` zvL2AMX8Pf)G%GYa-}1TzZ^_1}Vy4mRX6wu%6lCHPy zoX}|?Bd9Xm0U zxXx=m4T~KT|Eb+LA%qrK!rb?*cGl%?9N~^lwzmD&-ONr1`gHfcTA#Y&f}}8rj!f`l zz(!ee1SjBiITVKa4HW|YFL25|>QG*?-cH#)VP~2FA56(MT&K{OTVx$S%WsidXIV3wprm=YNdm~Ystm5JpZ>;6Q_qbf2+%Y+_|ud0l#8}|F1Z8*-fxZ{42yhU&~#}^!P_lT z@d$9W{=pxQ+7SUsnqVv!!gJQD_}v<+3`?j%X*(4sK+uzPuyv58?@<*6Wr)O1?vrjv zFYS1J^Ku!9tB5B|Ft+o$dyXfT_wPChAXbXTMN^g1!~_$r-bXYQSZ zym#;K;uAsKYDp<{OgAH5Q#NoyxoWfZY;8Q-tBuJwEq*4+aL3i!iFIN1OG?Mld0x*W zGo?LbJ3_j%lJ_T^qre=w+2<6s+}DLTYO_!Bvh<~{-D04fV^dH?J^tiVN@<%$H^ft@ zW(-L?Pboo4M|>n}-x_JI-9>8+=)s8st;BeEh+LThx8SBpYE>0boShGK zs)g7~XYdR6IkWCH%9juksjHMfR6sP+iH>y5Y?9fr#QITS$0=w+PhLz997k%Vsw<4d#EKV53+SP7gZve-LVk38PVL_9|nC^sBa&`*kxnEaNF46Xs< z3AR)A%X^e|<=sA$5uG|P*6O?8F8q9DeL=^oBNbr zCgs2Yx{R8+{q0xN6sQ?7v>LsExDN%&vwwPv&aogQb7r7h{{5#$P?-*M`b0I8<54_{ zx(bD&2m6vuypN6Z?zw@!w(1t^|5Pk#Fdx0TG9R0ZouKF}kZDonxmIcbk;>Mq=bE>a zXpDn%ypnFsSDB!FEIytx`{r?LEum;$7Z@z70jUbCDzwFz2b$5_51qG(*_ZadJ(dW`<-l2{cFTji znl^yMaIY(IaROM<&F8+?Pm)aI9^+C$$|-6rOeJ@6LCT87l?a)w$0`ECjAgqBzmLcs9kEQAEa0?I9!Jlx1$ zHNfYzUmG9fJ{p$SdFvA%mHEkjTbkp;X)$-osQV}dufBA4%@pxf@u%s9(3s34Cv)0P zA5yb#sXH;a+)1cp9$p=ECmOb$QS?zj(k$I4!Jv+t*$d%8d8AIQT@z?f0)ar%yIR+S z)Km&3c*DE5)s0_$sZTZV`~u^28?vH2gB#XVwCjVEJLrrQpB4*#SI*;qHJA`n^U{nv z^Im@;3!kMW92psFAZO?Lu{X=dm6IoU7{VoT!fr(%G1oM{Ms3p)sn#c(ud@Ra4__k} z##)G76jP+*wM#R5wrM7+Sp=8lNOLyyN&J_YnON^G_OhGyWyVHbd#_>2JT5Z$8QDPX zHR|_qe9Hr3fQy?>Pno7>Oi(nD54W@;+FGhRwr=3)f!!J<5)BfX?wDpu#F-JEag_?1 zhSnr^8Kk1WT=5V{xL-{#OT>BW)fbInUVBqATv02Nb&y66xr6yQqlbWFP>d8ZJ10Z_ z%mR+c?z=-WxTt4PZpYWO#{fzKQrq&0+lK2^^nPF?5eMjLFEy>MEcjM zK-qmN@85Sel@^3@b~jCEqpL9jP#^$>UoCu@#g@OHtXb4-)rQ@Vp?MwAbzL&K*IGZx zUsE(b3kRHG%q(AL@rvj_~fxwgUwpuR5dmAEGD+zxNC?~e}#s&igoJ2B9%yuSp0TmHuJu`*Q}$=G~0cV zM#qZVsX2c{*$HieullEcQ@{NZjo%Z9$UV&&EFSMw?z`8`bHbsDZA@xl!l-%z`@b`` zk>Pt2R}C$q61=6Z!Zq6p{-2OQ3HkYM-0zS;-$}F(552XmidcAIj`oHglQVcT2o#Z_ zxjcWzlB;M@Q@HNg9HvSQf+r^@4u3P0jVl+7?NMc}GXvukxuiCz7E+td^d|lO7kh6V z)zb$eQlM}4 z@0_!{_j%=>ao;<}J%8-=4{I@2)|$TNoX_)oKi{#P6F#bkuA^#9nh`#U4@ufp`$#CMlyfsKDdUyv@hTjkrHuT+W_b+ zMWdk)dbj709P) ziS)rW|ATAiR9Ra^DilZ`?#ZBxHceX(MiF$E5-`8%hwv%#vvyY6O218ob51XCHa$3 z6-V6C;T|ce$ufd;v1z1`k=Z((N2A};N=QRd1F@~xRI8WAmc}NTEdnbf1j(3e2+!Me zK90^6C5fXI;%&bvIL|xF(4oc=Q;HKNnuBW~BKnVL60+J?y}_|SOl+j*kCtZ$t}mOi zcgJ#GI(w_3Jc*UzMGXa8uzQ_pFv&cP#e8BZJhW4PB4Ur{A&yO>l3_sGDxz^o`N(F_ z2GW&v@KB|EN=D!*U?v9)_{O;b}z*P5g4)8tzN%3_^+Zqb1@jH~N zSc_D!*^!?WC9AFJHSL0og##lJ2J*DI?T*dinCCPsHO4S)hWMa7m7MGQinUks++6bTat5F?*j)tTy6)e$FL4nNT{sEH zr!SW=2!i_zT_ZR-N3Y`NPlsBhCx(4VIxzOhTNYnvaqgYJ05nv)w$@p`iF@k13;rj7 zT-TE?rvx0{2p;q94$8TMKiC1QG~L;xW-rrf9VJhJ>p*-37GIu|___vdhl^IRRHzVILy>Cbv z2XHq~-IDQ~B?j&~hRxnl?MDsCttyHAKe&5VNWayTqvBC9eqPkJiGI8y?M0^tsbqk@ zZr--+I}B&Ca|sOSDqvI%Ib6Dl({|({7Ujj@Qj2}5gp1Y+CXf&Uao{mb4e=R&Q4MKV0&N?C$ro#n}qobij3$5V`NO4VqJaFOZ? zDICC-1hPyLcGRY6m4V!028c}Fv*D*&oQ$R9RZJWuhi5O*d!dYj@8EfbquNJ=p?X@n7qUMyV$;6KStOs*cVQ!KdS>XPwX zSCj7HxYBnPKF&F(?w;D=IDND?4|Z_X*i;4;^GBN!#=Bz!R(>@x&S%dIw}h+Cga?|` zXj6PTUjgpU+dhjxX&48i&uNw_v$#tjx!6c{50?a|2)}6y&$_oKV^M*5bxdsOq19RJwu#$@&AU&h4F=2JkIoT0nA1EGG%@FU zn;$^Tne>rw$|{A^MJO;c(YDfc!exAC_7n$X!HJQ*5X^JH_kwYG&P60b>Zp2AJ98`$ zZ}~Sh(V3ke5f?0{_hquIb1Tp$c_KzL4q2Z?KM8fXZMt;Y`gj>`=V7VEo0#ixZOvZ^M&75Al}?qFq>Ny9&DB)R{macp(b>7Xe!Gb&jM#* z&E0X2h4UJ~&Qer&tu48Yzf7vj>_*${!kGn3s_ zh%OfRj(gf3e?TiqfaLT`y0=S?*%B}e{UHgjM_WagV>}3y(XVxI9$y{6`oQnP4aQKj z8OyX_!OZd^Y`cN=mzXl*J8yIzF1AbNn)OHYPQoIBG}o&hexRM*^j#qmrPGVkc{JeC zSlNPPHPL}JJlT2QDSkA@Fc#tcUS?o4<9M(vA{&(9g2Z6f_C0FM*YJk7=~8x28sBfh z{X8%!4QERrR?f?V+!8i5q2ZEFrIPrubv2am0uD)h8Uf*B$#d12*NoHy!EVvEY4d~G zAdtn_JQeE&zs9aXgnbhnvvRHg=^m7KrW>cC0^#3j*r_l=qi5|CA;u^}iwT#>Bd5+h z-zsMz&AU`HJ4#3p{LZ_7=ghac--)o{JQ@;r$GJhD?ck*SX4z=MCgK$ugfm%};%ie?^uSodt5Wu2`5A^| zM?OMO-DlqRfc3i<_pvSQ#TaU+Bb@$qq=I}Nb$7D+j_h;1$tr#{i}&z7hZ1_as$s<%?CJ_9O2hlb*IW99(D=9`@TgvB1UXOs&D4qfmQHI@&mTSj;Qj z$Rqer-GD{^$iI?6g^&dMI#e>A^DT%MJrI4SjNOL8T@VO}(!qqeKB>V`vyus<(pm0) zO2@!Gr-@Q*!GdnE;TIc&Vfw*gt=Z_{`#0WxPopbdWI5ij7C6ka+{G%?XxzNSp4$A7 zx;pz*)I!1Jl8B3ghU;H#240i!VUl|{@#LuIdzK;*b!%3R*Asyb&UEHn3dztQA=x*< z4$hPWGbP{loN5zQ7eE7irPUJlaxN8gnq9@E>3{h#zEO$yC!>^o`@jmHNESp<5?3u~ zWhC}y+olAaS}2@iCv+>TJw3LxwhIFUjF6Dn?DBnOJMy^TDOqM4{7|Ir+EKL4Yog;z z+0PqESksT{uZb)1%Qvi_&WM)ft)9?kxTUsyB-$Zymn^pub65d4Ynt>WZ|aShx02li zOB&Djut9D3Y^Lx!qw8D8%_fr*5^m_o!gW|}2gnsASG^@3?P_4mcKt1Bau#yv97?%R zVeA!ewPRz;E26vE-E_LFSq#hi`m83z9i2^w5plwDE4DWdgNVsV=}E@TSa(|5y4513 zXG2v@h;TFSMBJj@U6?7Yo!z&4)z_4 z4=2pJDOwT6*yHu2t>PCz)rfe+^~o5;<`boJ@U*0N%*EH`{Maaonl(e3O`SZ?I8`LW z3Rzp4;CNFd4<2XHNVn&`PGj98p@Qe@$)Q2MU6f;9iq+4r`G0Nhnq|!V2=gXW=jNp4 z&WH^SM+yoF{?_MwudLYtA;Z-=$FN5{IkO8jfB z3s+Va%{eIXsw?xeW_98`w+aPhb2N6*Y`|DYvOCS6aMPK%jW@x2WV>AKhyD;ycgqGz ztL#kd#gk+$m%mt20-NX#Bb@c~zOhgeEmX_N_j@h~85~7sGdSba+TaJuiM}#`t2V z#ycluq)ef*LSzrCI5Nwl@%8C%e(s^fzp$q$Ao&SrK+ACWn&YIG*dmmL7N3m5AC3L! zP+c73l}%tC*6Hr>4mPORQfAm+r=^6bOXLW7sBW^7x)Jb%#%S1>eM2tS$Dx*{-Nu!+ zcKF4dvzj8}{f=Sih~O0=0_7ZwhJ8jWh%#+ahHk4u-AQiCL zlO)i<_b$yLiPVkXD@4%CUo^o%YbHSYQi|Hns6Hga&hdl12Jl7o^*D;+&71P>vJ&DyGD)NqAlxYF!Mp5tVYI`SDT+UUpX*OR!9KTW$j%nx7FN|j$4wtd$ zal?!2YkG8Ftj}cLB+U)S`oekp%3$6~MGtF+9!6A#j1&{IQ6$?6&gcq~M^gxcA??CR zUo3xWHZC=>Dv0TeiRW5f z(Z}mc>MuZ}&CRci0ZEXK$2%J8v#(dRGA zijvzEdLJ!+4hj!7js-E)CqK1{oWN`=`s~;-F0l&IpE14N5lVOd%(bVDb3#+r>&oqK z=ow=bxE33;;N=yb?2N+8T=1IYG1;8Z4O!GWj;(7};lmul?A*1bgwtzWq!Gbnj@HWT{te zOw4$h(yD4>zrXIA=9^;0oF48>+74tJx1B@y^;N3&Z>z)tcdwN(0>2;BD_jEq(CumK zB>q~S53V6e_@O;^$9qBVwF;9+F!-|XbI#3dzE#M1L__TkNGPq|{=?wn?L+%NenU!b z_+Z|ypj-yV{k*t%nfx|OGyXGTC-Gq3&9hCumtt2 zSaN*2-o+)LYgrOKZmkJR5{kw6sH|PDMoVZIh|Ibr(4{7*FJ;Cco{F%i>kzDcn2a7K z;jr1gyJ&Dv-A(wJ5!@`XO_A?Z(ORgEn(^C*IAo-*oEIMyVp0cc9&Cr&94tzvC~L8g zRAHB4?foFLFrQHaJ}>Nev8P!--odH7y%6Ok$&X2Ct~Qg|*Ws^O?>B`+qO5LxRkO9Y zmUqwW*s%0Pi3n3}y+l%$9SczD@w?#a!)IoCDi7zP+^Tr!NVwpk6Tr> ziY84&Ku*ze-Vc+Q38*rUMz6g3-MWebzG;*G=l{RB=5rxSdY3F0Qj1M9Xnz&#g!?s)dw> z3yb5dL~!`FE$)CSsmn4O&1l+`p+!u>BBLDSmB6nRf9kk2Zm{)% z+0fau*VSD`hq8NRsE|P|HHw9WX3sR}-s?%KkOg7G(;?2WIL1J*cDXBBB(1xS-)Jw@cB(XqQx=3+wVPO4or%F?Jv~A@Vw~$jyxhksfM~s9UBOe9Q zNIm@UC^NNUnfNVlz1t0MMT99s}=tb96KLieOU&m(lE0FpuQG~0l^wW~6OtRJI| z0<)i&2`@1yh9~!^^0v!L2!sSKMU4?vSh1I z?J>U>a5}v6zUp|S5Lb4gu^a<)u)1um%Jud}^4x7DK5B0P67 z_J}OhjKg9cUUk4_IGjQ;^=fE3rTMBtMlk&l0D9P%B>i8`bVlxX<<8f92*cqNdPT za5eK!hr$+8`;*tHpKqLbA8bcY(v;%=JpR7(c0|8`>7bPLtjT^yloirq&Lkd*Fy(pU z$Is1T5-HqE>}b+}3KtOqAO4oT5gu_x9lp#~?T%>7`LO@L0#y8{;R70sysIHL(ADLV zd6yG`Bror?Q`6XS2a}tXL3vx9T~onaMTLZH;W*+|L-~i(aI$!FeQMimEwQ)#MsYph z5uv>ib|iw4p~|g`2-V-i#XAHN`9W$rDFXbNs4owHHo(}YxDQ53D3Qkk-IGf{q{yGgm7@|;Bj8(+r&%WseCuCLsC`HJIJa=)@iYj1wXFF^0Y zfpVCdl7arcL3TCfy!<%YQ(+NQaBz@mR5ip~&NkMps!xb_qSelXi#?-O$G`W>M)AQd z%iB$7vsDaRn~D4kwdt7NDVo}`l!tj8%>Ch=*Eux>3n;lx^;NVuffqNW;hWy<*Ct1m zzX+G_zp;`mENGu2uCUI`WEEVp6iV#LzfJIDKaN9hh8&1t8*^N%TsjN&(GT=9TckX# z2q7c-q^DlKm^Gf6YDqUkUKr9NqaN02>YXG>>1;~dNH zr8>Km@`=akB-+NuI*Z7Clp|rXI?@fJhR88MKT_FFa*;@mu`e>m$1{(Y4Dq5OT+6WV zx5S&ODVpz+?okcWbS^`)^-;$_pslS4&FztToFmh8kU+aJf_Px{iGuJ?WQcE`cmic< zAs)gOYG-kziy;sZaSA(@!H}=s0KQxatP zg!64WAj(r)Z-`EaXlN)2Cq}p^NgFC=Q_Txu8;;C%G#7|IT9-49zW-G6uuYA-E*nQp zsJt?F-l{Z|A*V+*-r0R7;I{M;*TSC_YTmMeoL?&}Ng~+9u1k!S%>CFeNOQIugtJX| zIt?@30!t>Kw7twV0H_9Y;lRc@O`6KPGzc?e&PIYw{`igjRgI;aL)kNayZ*%)0-)|Le7ZjXYE zqTu3Wsv-;;I>1U5XROpJSYRd$YRD*39RkrabQXChjut@K-AvTj*b8EY`BfCqIvG;@ z;(6dsqO(YtjHOq!PgD~F5?(PdV9ojq(DQERD)eQmxsA}<20PuAf~JvA7v!C^2s0F_ zjUc@?1y<^)SFyyF8@7t@YamNIS4P(9JSyYp9Cq)eGZnI?4o>G-2-TrNRLYjXh))SYBvRx2ZnQ(#C@%$#oSP0fNrzo^ovO*Is>2NBT~h*AgsD{a>( z12Ll4b}SwwV4`fhJBOe&b?g3NoI)Z(xv(15e*don@_*m+zXU}5-$qAVvnyVWf^ot6 zH?nuW?Vl#++WF5p!+Gnr$!HiV5e)gL5>-VVi|dpH)a0Fuk!B;djxqfd7PMX{K^0GQ zxK$*}XLMs>_7+4-bim_#)QP}E-TMS>^$FiHKbz+0iEaR1sEc9iccTC%X&3&(39g3* zOwK|~~8rPeOC{5}n`U;H(ub#QSxP0yyZw92Zw1ci=cZ5}t6RT6=D2DWik4YW2$ zdf)0ILTBi;#-zTL?%mMx??*v=)3P1Gp(`Gtp6Zku$bp| z=pU?6L(N7K0~mmcF8aw{DpTsfA+UPO{@gf3hfsfoW=B8K*SXei5DL=dv^s>|(;I|M zZhi^$V%w(Q1WlSEyemr+E7c>S-?CO%v|2qJ#m;~k*yGfRqS#4p1v6C4hpH-&$1?7- z@>pg*D&@^)F%&LfNim_O7!%_0VhgAM!<}O6M?A|uFVj;tij#Xbf6WlBt_pK5{BHjP zGic4uvDYVi*~|ZO>1lDT5N!bUfPwzgk?9`_T%^+Vp0+-|!SyRn{OxraZk`!WXK+N?Ml^)?*81jw1wwyE|L z6?lfX8h6r;^Y2@jz@A^1Or6prvUw_=0cx=5#CdS`uIr4sH+RPE8A2F9uCJe8MCG@h zt=MRkjQ02H-`sYhf{?gcu$FlowTXSJrtrnnAFO%6{A&98BF|#r86QgFjsjIgJgr zUYrapy z&LyER86X3$Ez?boom>~{tQ=ixF61JjE3-$V+7jI#pE=6_sfmtDYN-jvy=n)zE77^> z(X6nnFbB+yh) zm&NgQfv9Rx233aW|_p4)Vbtaj?|W7;6%;H0uK_5%9KIc;+!sP`vP z@Yj#G*TP7YbM^A^J-1slP{R+Sa_7tl1@{JOtG7i{Z6Q!|8sP0)xQ!((+BMg5R9jCe zABSlnXJLoJoqBd-N1jsRdYIcmZL0zhxPm}8B%T0nN4Khr$2>ZwADgyOcMIVwc%+Rt z7DZ06plvp}xeYPdGA?weFxeD3+wUN?ZLC|wB{!G%d9=zJA(b3@^Gk@$RZKaM+be59 zqHVERDBLpv%w#ZnB-%jUgNjq)XIU4UA6?ce4=qQ>waUpU zNQjFW5gX79KF@Rl@-_HNyC8!Uuhx&EjXUl-IIzW<#lU4q3A}EbEztmUM;-7YGRDX7 zcsPv;tl0L~ax9d~_du{8K8fGNXo2>O{>aMk^y2;0hCi+`$8Ul~wnI;2#e zJ?$(%t5HXoJe)Hn?2;;ZX{)fRS>doYy3O(!1?eEQ0^xH%b}%-dX4b&al&_F(LyV3J^1Oi~ zp+tfy$0^?tG_pmMTh-&K*JyQ370!~q2*T)UDK(*>qqXyjsQ4hZ?(R`rDDDt8I+)n8 z;c07R%zC$0(jmv6pXa9gxOPz|LAurmsmU7y7mdY^##PXv-Z^5NubJwMRfaXDr|2Zy z1X<_BC3EEMmGIbOTqu8~B_0GUyt1{K)g z3R_|LJIZ1oDWW`VXtT1ex=tS?M@)*G>Ssm+d_yN3j1D$@k9%Pafe=M>jXVecBQ1#LPp5zIK6KN_}%dj$mI^yTZcElB85OC`{=*Hy*>= z!^0N)nVWTLGEY0FQ)VP~NNv-fq2v*RmA^@RgUBXQ_5V4+K1a@ncTu zl5J1taerfSD_v+}nlaezKoa~>p<1N|?z;rn?7*PC68fPC$TPp;IY=m8ST*bZSR}$( ze~7rQAPypqRE%Wvi`kA{)nGL?5UIe9I|%cjby;5GiH!2xvTanbx}^|Qm4qLpK0Wm+ zTv%i!wlt%P)Tl}r9P3E#48zt%a#}})NUJXL?sIEtTd7~kp==rvXrJ3TnT*hHd8@;= zhI2*D`ZR|zU#0;UXI2Ygvr5(xpF@?wfnMCF-@x4A;V<1O$g@R``T|Db6w0+z4b$6D zVfOov$smAS(4c$@R#!9HKE;3A2MX zj4N=$bHz&C4nJU*3!7S?z3#=?-6P#CEQ*<~nNt9wHK1(ufQwzdM)~DZCdQT8yic3G z*N#7ne~3r7)6rzhc!#sm-Q&lJHAf?Xp~RVZ(bHdU=MGSt_5a8(NYdRmN)E9>h~PWM zM2IISHU-XTqP>QUkv!~c$@A-gNlT)Wvw7yLh(ESh7nevLi3280AVc=!34nTgj?qy1 zs`|$qlN#}E;y?Kc<0W(0vb>)e)s)1I3ybi~Ys&v^KbQM!`0vDVV(I^D7_eVW-*{%)_GfmihFsSG}8~SC2#PgaPJ2oQu5pcl_ZZU z$U->L3wh`&qRym^%+r({>YSFk%i?zU?LBuT0bbQ(&fXXkRWq`ya zJ@@E0SWTnEVH)GM${zjw;Xa=M0Afz0U8aGcl{`q3-RIeZflU)e8=7ssGLRO*icO3Y z_NYE&&MDG4_2~o{2+8i#ZooYAbTAYodLAn7+{ZiE1?4Y3Udt=aquCAu4n>h z4BX_-xSeAmxG8V|KxaEFbIq+cdbIZ?c5|M#EEk>3v7A83BvfF-l7#_3fPmTS%$(4v zg)o@efe2{U#^Vt{8pJXkX4P$!XzG;|<6J)%3I-#ED{{N{g@wDj9W2!tiTokCMX(^d zoWrcyl*CA{?wmNm+<4EjRfIDOfv{w7*fBJ39)z>!&u-5704~;`k6MJ(mU^$>$1mo5 z9M_HUQ=$qXy^K@tM1IU5H3zw&JqRZpa``*)YhDerg`LLm4MI_kS+8hhQEEoGHY9OsPQRR`9O&Z@{1dLr9H ziDft=Q~H++PZO;kWhU!>Z&^%KReH>d6THHimhHY?J3A?;wi{K(TvSYX2DB#*qdN8S z%HpGO*5}FwOsWBBgDLYRfG%K>@?*bKX?0Oyk+j6^E#HfLb`q0Bn>^18Gc%dsWGFgY zEwVNjgGP1to&oV?K52v`zX~z3GWoc4i}ax`xsbW+m*1bgWqHLcolv!j18KD46h)hA5z7R2SaN^vpQQ1%J_o&M}{98jmf92b|yI@Guo%Zekr+@c@rW-vsic`Ncs5rps zitav>stq64{7J}*c-nm{O2%r37ztX?{`)?$r%MZ2pJ1Lj~zw8vnoXQO? z@G2^ZpK4sq7LvZ1zg%i(yQ{C9D*BsXD_M>D%>!(RhT16 zY?I7&`i_Gl0NzS1E4)hDe5U4kdB_h^4BcKY@z=5wlj=W&OO9}6aj)g^Br3Oc)US~f zU3qWPe|=WjAMNRU_NJq0_I~ zmJEvpQo1zbHA4zzh~bJys3G52qnB0}>SIh~74}3q&+X}!@o_IddB zV-fxS0rBQ;mAwjcX4m(46>TchX~;GyUaCrA0-$z(yHXpKd?VRYuL61v)P%KxcLT%w zHuDF6h@7O+k@2-Iel`|j712}gNKF#Yrc%5nJK3p z;J>K~xaXjH=Y1g-m}VwoYwtcTiAef7fWRTK|CGjthUgr-euwk~8B3TL1!K~jM&R0} z`ZqJ!*UniOPJd%AC=3}~?<9!tbX&>bL&L zN6zIW@^)xlRw-FU>uXkh_noKgkKOpCN|bG2qEKJpcb4etqvKAEUjUKdODJnSzTKy` z?LNU%p%?9{W-cEGsET=&lZW>BTcQ((zNpzzl8ko{gV|t}&vcPcV^7r4pLhF|N?le` zGp#KC0(d_V78idc9x9r6cgW$MTsttZHR}zs=0Tg3uu`ELbdw@mG!9}vn-Yz5BKz^T zk?_Cog(d@WGb-cMn)R5D%;zr`bW@FQ=S41urx^xXa1vPkzX17*4**X2AN~(nxka>O z(!QWi&m9KiN}hhoGSA8rn}-xl?|TjvJDhVPidE{U&VTE8Ch;&ALuZf{|3_pQ5NOod zuH`a<@0Ojc&lMVnrBDoG6k8~4O+iE8N~;bAg)}SgJK5@Hz+dDvK0A^W9RwRMtVdih zm|bC$`;K}l-(xHb%iA5_>)*fX^=+;gI+ry9kfzOjJK%}P$tgKk(=yZ+itCAxB_vTx zx8j#yWWigkpGB4VlFxAO5t%7JYQQ(6r!2<%VEpBAK6eU-Y(ciRPYXSi&%NxMS--Md zqT7lSQ34M}FyR*<4pO=3$o5kotUT0jlO{{mLR3mf&;~w5v0Y^WSlG#b5H2&bTC+@@ z3`piOMMN09*BjsCOBJG~8kih65as9VAW!n?cRrUX`&<-t{{HPnv%bn!qO-B3`&Pf! zGkd?NlPSt`1ZEZg0eAe zsS><%yt%uHDH=x)RX!m9P!5-&X+8Mq!Ajf^8z!EJoH&4kQt|USD?^8xLx-d;5dQCz z!-F*$;Koz#gq`}`!bpwWCKm*5L?0! z&e%a>Q2Uf|3a`+>5k+n~0 z&}8zEbk2QSk0EP%#t*t7R<%n6Ip zk{4z8^3>kySWfh$f}At~@od*O0B~*kZ>`QDtm11zmtWYs3_UWhOT2m6K|4y?QS;CU za2|Bp<_+5QnP8C;;Kx}dBJMlWoIR2EZ(<*Ze2uTNa*!~1pE((i z?d|2uRpR*l{Jqon$`ET!Y6Hn<`1j^1k7R@8HOJLZVp$0g!P-y<7|vGo9&=al7;rh0@Pi6UENxKU9(28?YWTuC zV+1{xq2MPXI+J$plNA_z!QZd`%NdB~tskQ`UczQpf|8!+Vez&Z4sxq&&!htE^%c)A zpql;e!eZE5oA54q&yVymd6DRfgNMg*60ZY%x_e@n-FO@>mKjP?U=^-X3ClTH}P0|xp$RyQ& z(}q9qfcJi@Iwi0dKLF%xpH zT0j1g)ZQc- zGTE(XViqxahawt+vOG7-8!dKz&cUrcB8t+gFv)AEzAXFE0m_a&UW?fS?~3o(V%`GD zh<#Kv@7fqnlnT|T{8%JxPp~3k<=KJCqg3%}T{Nn49EM46wwFrY`vtJOptpRJu#!9^ zXnpmmr@+%li-TwM_M?@tMqZA%gpFGT@f!DLZM@8=2cTW&p65xe=Bf}rF|9Ec*{CskDB2L*74)?1+j_Q|-9WdC^H?lG?GTwtfZ}xSfuA zwvb&VHu(%SfpaYF16(D1fvSdTW_eFI|*$_hF5! z!!-e(NHb|0%V5yD8q}`H{H7As@xPk%ANBIP6z^p}ULB(^`syHle_Zq2r#U!QT5r)) zo+=UV5gX_{xpsu;!b&(l)j1yH$Rgbit>K;&LqMT8_t!nn-w*k}tqL2tdliFrFOvfY z^hrsu*gJ`XG&Te(HePRpWgAtyGI&cn8O5j_ zpFXFC`Pzuv(^8_l;rJ`x{2u(SefQ9uRCSl)S6H02L0e{H>DWYfC-AsQ38_tv0n*qz0!`W|*4@I!4alssi~M%2x2YN)YF2ehkSuqVA%#H` z3c&A-Ilmjl@7I2#R1r`9Y+nD13+9h5~>wU}U8g;qWD+X*Zw z_p&{VwJQdddvnNogg0kQaq>BLqHB!F69aCr{_c>!>+C;OeD*26!tb2??+Y;eEBN;i z^}p>JVFEAX|I#RaxA4D@Gn_5gAO4fJzYHn=l-qyqu>Z8TzYoy=^wZn_j3Iwa5C0i^ zeg5eS|MZ1_EtUT13;%!h1rEI6cKblhIf07`95I*l8g2?HaJkfahFfFU`%|=W{78v5 z=OdSJvCZq0qS0c>d0bMRS>vTXgm@>vr?UGduU=l{&A{xk^OvbFkZRr`akS@+@|O|B9hBZxY?KyqYUE>(XFEMA3rXl z^pYVecI&DAY0Q83kSKhLs3Z3DHsy>>|D&b5^t|vb^U(#7{QBfX{mwIF$O326^q{`3 zkkGDU8#tB=r>b~qunqQ43f@M7O4{T(wjP~2ltSuh^9{k6i)t!iW zN$jZMq%erLzaQRA?|L+Ak?dWZDhx{=zd0AlHfo#C++8+H6j27(E`;k>3XlIB33ZjY zGlinGC>{E0_LDxTB;vIFxZ-=B@w^DQ+vOPS`3NwM$J*!{K<6_Qjm#OE0E#vIsXFy$%AVldqePk(vgwr zJF+^(QFvCuPG5pw1_!ybvvi{R>)X_9{X!dnvX!T@~D+-bC>#Ho|V4J8>`G!|2qjSZl4-)DUF4 zzW%(EhP3@=RF0geV3eJ0?E(@BW7RJvR&4-jL(H8 zOBw;5?-0vWyDI^UI?XtvZzkvGjlD3AT7s!(OOc=78WDOzFRrp2LkGWE&%`uBUZ|Ra zylDv`u$|9te-JHJ1aRt##x@eG&jB=&bJO1s!f$&2ToD+VBYjlf#kg?#2UGdQs)-XW z{v@gQ`~ETB`z@{7(CBX;UJ;`$$Lj0TdUw{noLfFA=p*142GHy|JOPbKsA8h@1&zd3ND%PE&sLO`$ z^J1>z&p!3-_`f)B1OBX4exfb<{KYQ-UHnzU@-xV>E25=(^hvAshxzk2rJ|`n;-8$z zXYB&SNL7v>?<_fHNK|Xt-@G;YhS7nSs6*nhw4VlFbfC+1M$U{#4#@%gJGUdyo_p84p(?9&w)p4G z?H+(}4W^kwu_5|dj;hyCZ|A9n!(2v=iywd9yd?F~fjxSAJY_9%hjQiJi;Ph4WA0{L^y&tq=awt^OHv{v2cf z8IbR<{qwc@=TrXY)a9>}s(ff0?U>o&5|eqX=PB0hRNQONst&@ zcx`i}J)hbW!87NEJG@U-%1o4=p3Cj0{Q}@eCBNK>PE#Q@1+77J$=lR+!yAI3r%ey39pcp}5A&*LzSrpeU-{Hz5Ki z4Quac#T^HoS4T_dBq)6vC!6h^a3^1~TY^0?)x?mS)qx#l-%kxEuXzxel9o9(bOlu% zdFX$SA|88l0%?0P-XT%=!|VK9Zmc6*WbK+q@@S3TTdrmvnOCgFnF-_qzW6!~=p4Tf0{hol5s%XZHq*%Yt>Ia%}rIb2vU@kx&>BAjiIuD_FSTwpH zhb~s)S+oEQIog|~>8nHFg;x_uMbrr8Jww(?HAiQR#g=S;X_4_3&Ew3&fUL(IVoY?c z1#t%3*oP+=ycMb0D9wcZsuyIN7;K4Px{1=d%^L=&iyY=e*U)8&}>YS=n9oC*T ztNYj8zn)oZrUxb9h`FrIlDbhpTGa6?uOb0DfAiM^vtH6IKTFSatM3jn&*WG~`B(@` z+I}fQmK0X$?qbwQBQe>8{Twm!eI+6E6fPoE>UNjsNsSD%w9ykImRb9rpTN5b5Xg4> zfw6`d?GU&$kIIc(b1NpE<^fL(xF268%_P5Is<0fqJViQr$b&Q*am zDsCel2yataQ`^O>6<1r zOsxo*h^eLL4xh6ZSl?$IeReV~rahCmCo9aiuq-r5oGg_u$%=oeh$bia73?=|Qv;NX zue0Lo)R5Q`qBh9p&<`dJsOWY{%%Ad(70kzv*G*GZBULA;Dqwp7-^#*ScJ1(d*uu!! zv0RW9)c#spy5Bv~K^oXl%X`x8iT#z~m~Z0KXrYYKC^LGD;xNw2F#c4Rhe@p)Jn)Tw zg*)8}TCFpwcjPBqOc7~tea7>B!#92kN}1>p5B({s!Z9un{6)iOI_7QmvC9&i$?_y!H;Vn&Ss& zXom@wejQawK2b7?+FCe9I7_{}7tARoNMy4nB7Ci*RuUreg$%_$Vc2z2 zcMO7VSbm39z{_D@p@TK|%kBv3Fe{Igzs+KLgZ&4x;!r1 z7~E$c5A|PO?&&#h?FlgD>wnld=+Ge2K8nt@gd-xo)Zd>Hxg(RogQY7T_MS~S;$4M8 zC-*uV?h;eJe56e&HQ7_63m)^_QyHy!;=TAGIjmJ!wWd?H<`8C;(@q%-($&6?yV;5D z7G0B~n-H>c9Q$GWY|VAMjcYnt&18IC*v{McSbM**pz+);_{3x&xu$ssOT9DOCu7w) zNe<75h2B?V*_+2aL8?m7Ax{}Nl>o^D@7oYuGE;Z0&W^K~k0N%i*QHx;aj_b{*BGkz zy;w|KzcxViB9sl&gel=wm`zd7VySJQpW{83tq$r~WPd5;#dWeMNs{e&s5Bm^LI3S` zI^1Zc|GZ*P%wc$wBHV<9Uxz~bHIF#!x5kVwI%jL^`NgK)U}$1GF$-m6F0P9V9$$QeW$R1f zI3F=p-5B4l_${Zz>Y4DM5YVrzD9U4V#f) zV;7&PAJL*BUP%9@LC1(x-D#P@T^^S+-8;!Uvr-+cU1Y1N{PjUuVqPCbi6^jT^=ybi z=7U|XG`>AD$$U(X{`UnE{h%SqHMVxjkVHlWEL=m0 zq1g_RByw-VP|?j>P<5ModnQ#95x;yW$Ll2ftLAPyuc=LWqqA9vr-i+odDvLVBV}mq zLcnnaZ3i+_7GYjiK?Dn(zn_nDIn>8FN9*-*Rc^3^f+j7lwE?+iAniu3xyi>s$i)jL zM*PJ}U(9*Da%z#+v`EY@I2gy`UGhkQftFHZ&RGSlmE)=*)5Y+ZlCMH3f2Ye)0cR35k3|h&3pZNS#!a$b)>pVU` zgL0qJ!w5E7O$3?;xJ3ROuQ6mj<1Mk9v4U%=%Bu~=ccKkKXmn*U6)~}%y#{JbP+&e1 z6alss6LH`y_A@H<6B}xkE4(Ekh28stkSmQc2kFS?mRD^K+@~z?kKAEekuF~(GWEz)i|%VMKS_;}I?t|`GlKT3 z)}V@wmg62qMJZQSwlEL*FXG7$$mMKJ*pg*V)c~uRuh+^G8s0eu@yfF33v=@MMDtU& zLlWcJWO|Z9Ia#@7@RpOH-@tqt@i9(3_Jm)a2g=IOlCr)As>&ZG#z;{_KRY#n{m@;; zUH*pl5qI`o*JDUfq|QpmV`6e9Di)D`nh=^ zyf(e6sIs$wHEPHcy)v!vc|L80w~ud3S|_;zjG~uL5L|_x3x)SesTU?03ETAK{gDXQ8|+IT4-3}TyW$;m?D zG3X>6EvHj0(MtK!V+alOF||P}w6*&5Q!;;}(wlz-UTADc#vZ4xui!#GR9gJ`Ui`EM zH>6vhiqa9oR2Bkt7fryGOe&`>(#lisRuB^L8Z@$Q#mlO)W_Ds$EqF@eryexqHFR_b;JPVeAe88?^Ct5nkiyWIxG4L+-RGwIbE%}g7=IKjo zrF-uNwxv!rAl=*~)V0sL%(^JjDZwA?wa6g9%8;q{6(L}7e>DU;^mbFJgtApMST*Ur zDiKKmQ+BSVuO-fFVW|{SP}IJ7F(ho-z@xipe0w zqz55|`ZUJGWYD0n)@&3koUaREd-7dj7L|!pl>TC7q-0sqw2q;V=XD-dM?d%GgT_nk zJ~~y<&LX0Skr;@h2 zOL8LA6jcn9j!&q6b}w|v^S+o+A7l}ucrp)n*z}DOTYR<|v+&$qz$HW8)}M4mXs9?0 zZycduN_WmGQQBrr@R7IDutGq+(Zj6%^o0q^iD+I3>#j|gyjg=;bsfE-DMTrPHBYQa zBG<*i)XJVAKbc3#%@O`fQs${Z`p7&`^z*KJ`tWPXZ}N&yo$tz1qSPhc6T#IcX~wc* z&%|iJ{kHSO?G0@!v${UtTe=ntdrpa;t6fETGD5==ezvd(QUz70!)Kn~_WlNP_Z`N46`=wxzc!zE#Uof$x+k}Wc zbJ)tgdB;N8r=8^_k}>X8QKD63&QJPvlb#UxstSnCO${-l+eS_KK32onH#YXEAEp-S z<>*&lbZT0E0(MD1H9j(j?$Om;fokwRwcviRRMG9dudKw}P;APbwWRp`XRt^{Fw_OT z246d00@z(E2GpjaT@<-H1sWlYuD4y4@pBPXi4w&L=$A5#a4t_vO?yh?WfN}^7D={F z??biE0*Awu=RF_y(MZ3c(T;0-#zL2pF83JN<%0QeIHZw@44tnlULH?EFt@5IDzaBu zk}N_jb@JVlM?$NESq=DY`O*tjVmYB(T8gq&r{t-F7lV8b59jWs7+6Y-Jy`(jHgzTX zXq4v-Ka{%9qG(iDtz}9kAvN(aD?BwK8snLIrlEQZKj=YLW#%02P(o6BLLjms6F8BN z+eLrlX<`Baa~Uwy;CuaP@DVF6R?_Rtfg#y$y1pOmLw5z`)h$i+uyNy#Y?lk9a(N=u z11KhUsy~nsee^Z^()aA#A#KG%Sbrb>&0)Pbjrl>XQ(87!(Ud>Ls52s})=-5{<>9X& zXw_ilgZa+zTG__hXp?1yk~CHhQK^w~v(8V^qm_&Hw&i(nJmwwwhy0^sT7vYCf7am` z)tbKjxIXEx>|sQ6`t&7v^WBm%Oy>(j>z}!JcW^I$I3ALe2owfiVBXaQrf>i8zq=o* zSKwSBG(3bm_;L;WvT1OZLX%iN=b@Vj}g_jx{x3T**X z-Fi`$_@pb)tH811`nOpY7&CzBM$=nKC0#izrY=>{j(KVH(M{?_VWzh8=#`u4x$axVdD zYpEItIKdu?0afp9TM0IU{Eu-Fe^H6)QM?mwEDIO7`|>jDA^bn$@;~#+-Gl!XtUshs^W48~M>dz(;;&4i$nr2cty=YyP+mvjMpRC)YrDKhXf}_DXwHQ(wf~_BSN1u@Jv~-3e=uXIvq3IJZ5IFf zBPB?&TJB7B9j!a8d$RDwC7FV_&^N^drI@T51Ps$RV{a7_o~T9h6oLiSUc5|Y0%q?D%;smjCDLFpH!FcP(Nk_K?m@Dt<9oUR#X+6Wt#$XxMa75 z-CxpKm4^!srM2VgYVyxUpGFy>JZ2h zeH6c-aR0Psy()Bp8UD$E{-gSv)dRiLuM~!Z_F7NK`VV1x1QdLK8W6vi7sXX^3E&3D zAHj!NCcbp8=Qm>3_qubnyQb4XH|)OzVJp+il0-8FJ3W@8C&QNtMz;+qQW>?xhLw}! zLiEp<+kh+AW;W9&EFQ+SjH@{rl60NRerbM_CZD^?LmT8fTj zqfPBS%8ZHU`a(xO)>6zpzp_cqowA2aD{?+jC8j&q?f2vesQ09E5_AS}_w8#XzSh#& z>oedA@vx})+4Y=t@(LuL>6r!8owDP7;LqO!KPN&5&V6;gV7~$lt`ewY}JALH|c_Y-oZ;7f>Imv)IRM(EHq`7GQ;_U#B&Ry@x3Gs-$ zq>;Abr14`5YB)qqPZ`zG*_yUOU^gC*3;P8*dsw7j zecsOBIRU=&p*_Bek47jQIz z-3*&+HWYfpPwd<0PzQVP?f`3sSKkUZ6?$lbi+zHJ#je~ZBm%E)mSv+#{Ro_T?WnSX)?8lpc39y1p`3La{pHf!(1dZn)Y$|jRlT*S}`ObV|Ke^hth z_uX5@Kr`+;zBzOA&(*bzma_`eEDJ*xGjk*Dz`QV+&@^u2C)1SG4tLB!e&~ka^n;0^ zAV`2|zCHzX**~RI>vPKVMiJBLdU0HQz)G^dz(e6+77CO`D2BgYt?>wJW|Gibj6~;F z`D4k-W2kE|61gQCWJK|U_+B{6gE^&Qhsr@Jpc?5{q9B85W_1N-$dk8qTogv4gC;4o zZnK>=Ck*1M|ihrn| zCEp&4%9><=a>(dw&Z3XQD?+=OQNzlZ)|k#f{nuf{H=+2s(i+mt3|+x7O)arp;aUA> zcPuA5UsMO|th?eE6;m3Z?aI%}n08cz(leiSBfX@FZw1z(DJ^|p zWW8uT*^JBa%-zwE4Z(Yl>+vtuf*&jF!5WZL`f4jq8e$JK9th1r57E+_i;NC9I{E&k zq%Rm7S-jK-^TM1lt?90M+#{d<1!b@TrzTuzZtxX+!q?yVfk<^)z`DC>9gaD9Ca0m8 zG7Aau=XY92m1^D@vVO9N3L!6P@FI$-$6d|hsHz;_^{E9ZoJKq*psfDMrg;QiH}76bG!bMvtbLo0v~lhg{UKDTf4}8SNGoF{GC!(pB}a z5zAXqBW+XDKmt?0KtfRI`!S74l1yn6oI&gJY1%Q5)lR#Bq>ttXQ=-ry9N?$-<8qF5|&PdXZJn1y*P!An+&_<(mZCmhKibZB&U^QeQoz_Agxo(YJ zIcaKfM)d8F*OzlVwxG_4p|vodk8XoV@@#mNA)@fw&RCQhG;PELBb|+7k*R;FSB}b) zaddxE@x%8X$b6I&10*|0vCQvTEqj(Mdre@Vx}&7nT)4}YW!s?5motsL(Dio80a7os zkoV~{$dDq#sQa`ie#`sE%c(C0s?0KiY(+%ORHibk0DVvj)`AR%2+RbPbk9U zE{OnW<_j`$& zJjR05E;IW1KkS_U@|yMyz(xPzj{2c;vZ@JtxQb1VC#Hh=lW5=4##ic^6pI*(s?N-= z$`{dj6X8HHM`L5QlpO6Rith+AgGI&V2NFYsF-BO2bdc=9zLtlukqK!yd?@x&s~p%u z!}nm3uI348tZ=z{t{Hk1SsgS?L|8VFc4h+DRjIsm1u7aK(8P6N9Xvks-|yNY?vZEB zd6O&g&Zj3)KGIe`v`D0dTXa2vtb8w+=b2#yPmXGcvQ5y>9B=S6zVY`+A-(nwamfm( zLi~PY5zDFF6a?RWzuV?043g85P-2uTKu0vSps*-I58?M|+1pHlJ6yWAQN~GyU6?%=4Gbv9H6ctwuZ+>-w{93^L zsVAI7z$xuyrj^Jjf%de6{#44~Kv?Ob5U6y)1ihH>@~{y9X)m^I-h>65`LLKGXz20o z>%U)k5M4QcI5F?pR&eq05pj!ax-a4U2!UwO^EBDk5j2c<2VGgz6(j`XE zzakNnyMSJv=?jkRbUB_aNCP`^j_Hi1I+ZzPMZ%MF?WHC;Lb_0$T=xT}0V zP(iykr4-@nKBl(J4;FUAt!pI&sXLO*6rw~O2`^IF&$x!Mz29TJsb{A#mlo+x5-i_9 z=EHfGYn0hNMj}o9VsA!#hB^1~T)kGpSgNrf)Q%)kWQZR8HW3n0^TL10_OU#jzliZn zUOZJvf86(8^e+pK^EA+(hX!q@>&`!t6Y9$HXODkf0nv6;v2X6kX<(c4j~b~CY!4_7 ziIm0E(2Q$IO5#k?eX&aMjxfMx8~BDw80;)t+fBjLr4X{x zQN!#ngw!AbQP8@__PNz@v%Nq79PR!nt*rQofNAjk6UmyhJ}UFr|6j9Q|4q0ZK4t@A zZ}Tx~zzVM)@va8QkydY|)Um|a$m4jEKYXMpl{>anv95;sRnvRf<>AuD9Il734q*Jd zz^uC2aL0ek9zhWO$9ABm2wsrvleeVlh*>LfLjz=5O4ff@p!Mo}H!8lV)^ ze!mUUGs_Ln<3QxM(}qGo-T82$9ju2NCDC7i!U1wuAn>ro6=-R%adsbA5nS;1ieU2% zdba*c9okdZyIP|;lOkXDX1{o;z5G2fvpDjOojAS%{qkiwvIy3Dl5Araj};m^bp^U0 z4!W3pLH^=_<|2-L*b_|~KUV`1j|cBBAn0+D$!JptLH;#A-Vu$Y;duQ$$@`!Ee;)Dw zC#pQa7+U!r@A**pvH~qWJIUQ8sImq#KTD08g$Jk=zf=ni>QDVRKD5G1u66~wp4X;f z?ih)w7D%DHkMu^RNaB3&X*aNW^SXJklt;s&V%s=x8l9dayIMfvE8T{DPg8(_8=Q|0 zCGxliK`!e3(>Eqnxbxw-`?$N*GKJzmDPbJ!X=(~*O*q`(!DL*YF|}!?h`6{opJ66+ z%5g=x1}P`L=BgY4tEPNMcG zcP?30=n(BQjdjJo99qKs7?V6Z7ShqGqaZwba4Q4WYjsVleVHsR<|q;A(`Cwx=`9oW zH^$5!Rq1^v)|hI;gDfm8=xhEVvG|y&Q*DL3F~fs~5h1Mk6s$v{R$OiM0ZTPXg@N0n zwNMwS*%C)7&nBN!S`s&nPGKUY?@xSQ2W`=0aBZTTq^T%r@gx%Bj_6(mbh(P3AQmZWN<=(C%z3fZ8%CQEB`uP@i4pGj2L{HVGwH z;z^DGnaemc#|SOqJeoD+3S`VV3!iJ&l?4dv0T2Kmo*QY(stDv$R|JKqV@xDbeVZ7A zK-$%F)fFpJMc;HX8+@3}-nV|rdQ^cwZqmryL$OF&9KtqY;Is+c>~A=U3ft04 zvx>}5hkMa`xug#kmzUm;ntbTQewWX(ZA@X1%(ZMx!E(-G&YqM@5tX@zk{%(li#~u7 z2`_Fp6mJLu3Y+4D$1=zDM)1+tLelaKBOHH`Jb~r~u`uGZ(p1GvBtewK%+)vTSm1>Q zT>S?ot&e@lzmF=Ky@xuhuWRB9O-QB)zh$NT5ov*)OZSSHOc-G z%(9co6s%VHRv5;b^x0AoIHrOsFwUukCI@e@N0VLPBFcohxhZ7EMAIyxl#ismo$@kz zDeP+UG8I~l1b3pXGIe^T8Y)lJ}*3AA1)^L~{ zQ(P23gkF^0bc|dlVSLK`3WT#>T>CwcC8dos=fzO^M=w&~um+|XN#M;zRnW+1`@(jt zk8@Spww%?#ass2LESo=}hbQh`n;h-ApXZt$06RXS3){sjvQ5SpkIJ2VmG&0QPNpI0 zm$F)SpoX(9Gg@0(ino*oQNQ+T7BFYj}_lw%kA4&wr2|IQL7h)+j03Hd?Wte|%*l#Dq9! z3%lQNPK)Wf#8=5+qhxP65-<>w`SS-hWGU#6?W~6v_mQe(UCsT;E{%I$rwJ*vMoy)9Fp3sQaRqk2N}A$!+2dhMWI3%e&Rerb@Y zLE|()thxe)SbU;L)PRNVP!swfg_G62ZiHwtdv*&J-XQp4r(Y--$Ht}NeLEdjn{1{M z#*iaZnY&t?h^^tenW=k5e-WYsy=a2$x9+cTeO>|r>N>8bwkoIN;EPk=;bR8A+z3XW zY)^?-hmOAQxhcMlN{>|*FlA=sJgu!E&6s!j3^osOZyQtpg$IQ^wdWZp6CD62W6#Ol z_kQ+~@4jz2N`{1qJg7fDo6z@j85$ppIqWAH5YAoGi+M z!`}Ntx%Rx6uOxk9T z`^oX6>>MSdr&u!IB{D?2XuuHQ6&d$eBVrNPP}On_h-2IR5N4(+CBG*>^IW^d@{`?} z<=J^$7BiCsu|>CsZ8>-EdY_4Gh{k>9Oqf%mtLFQaFy$ii?BI@)No8+#l#FIS&zknx z*J!UFp9dd2IcP+ekX;DVQ1@`fkmSDtVPzJqAov}#A4GLFcyy{y!=*IrZuI@S2>u7*rR!Y=ki5W?8SrMMtro8sP6*SECKSq@g(${U__)to~T1S zj`pIVKL6<_ZmhS@uRuoumRF!@Q20>FaxG);)!eLdj$3>!|Lf6r$JUFljNiy+pc3Pl6FN1yz7z?0 z;8_J%JU>)Cs|}~%6l}~Yo zgU*liP|JHUFZ5$B$jeTN^F78Wu0U}j9-G2LDBm1r-Zs)N~D6)L2*=0Y9H2X@(=o2cTqd3-K2S_C?>_q z;Ufj)WGeE&DqI#OT+-pdNpm1Upp+bBeLz2p^)A!#z8kXV?&Fg1kQ?%Adaxfc*wMVj~bk|Q=ha(7nQCVNTscOF1J^# zp{2fEWZth-K31&;YU3p*9GVikjLNuJf5^I@QB#eG+*}OS$*$zs_E)tO)&GUf+}0o( z@k>4BlPx7SVz;(I(0r0`?xlP*v1vtc>Ylvz7*BXSvVg3O#~VLMC2)Oqax1?SYou5i z(|fm-VGhn*Uu7vvmjZv*{BB7CU`>Pt40cxz&X}y<8R+QnAucwb+m?~axt)w#qStEA zM4?9E16{9iKVE)BFQm*GmoJkhzW0f;q`A7|K^a?^H1V>HEHznX+M%XYaD3(-cuZ0= z^vwmu!)^bGyKia&pThF9)7w6_$dZPtQ!vOsKfuN<%C2IasLfD|k58y#3p$MVdXG7a zq|E)A)qZ4?LsGG_y_-ISgv9z`PO|go=%{S8icwR|&LVsI@$cRE%B+@pA~Lu!V|!F5 z(yd^0{|KC5i-GkZ{tvb_$f5k>ly3W^bp$~{zT2h!FDF%d&UK7PiZu^C%@{c~o8s%@Psk#DGkyo|}R<-_tR9+|1V z=q4D!fJRlt;{vB%2O+-yF?<*|pcD1Uvti!1?%lt9S10g|GZC4)kB#6L=Sa3DDnQE- zf`J;<01%f|eU8d`Mz?z3x~)k<$hR_8DAXWPcA!Qq(Wo%pd_%HL;ZuKxhH1|HX&IB> zH>o#dFRF0dAJUSOsZsJ6-{G*~K0{YY856qamwb+2fu)|NhAyiR!mfsF_(Mt*}nZD`F7IboYp=Mtn+06Vur9SO{YNCI;h`1W}Z-4)9{-MW5Kae#?`!Oo1 zn;*ZgV@RJ)ci1Y1W~jNVggOIpUCy}d33M$Ypi{(XhUifx`c#BAv3YA{x=jQ3=Z|V{ zrw`pumSSvU#S{lOm6)}#fbXNCSD@CT#x2cplVrsdS=&=CtX86z_vNksgFJ{xmf#5k zly5G~1JO&8=t`oZz;_+ykCE=RfD?GxhnPZZa1}N6Tt;!o?->I}nV3{U zjT=r{zs?dKzH2<}PWYKVq)*#t*7J!g)+hUkZB>52dWC-i2}$C~9`vU>u+|7`?f@j7 z^&iyyKfQpDS!1)znLL0mcdqHCJb0w)*S72R!)=UstjWF(ZDTiV-QM)b%Yo6jbqB?d zJ!KaYX*|XQUA8EdngFOF5NIV-wfTuLEZsHx45Ts7u5krIF!v{gLmPyCUV+&5uxg8d z^(jcwvc0W6&$Vki)@b#BQ!9;~L1kiCVrs@hZFs-|j_0P9SkH%ew~X)JSv5?uc%G2C zM;Zs&zXAa@1I|=?^!l>na5~9CLw+tZgqPCOm$#T!>+GFqUx7}}?rK&2d4;Ix2*#W4 zI%z6hGvxRjx$mw&1!CZn_C2!murB0|f3mfBKC4;-LJCzRZk$T_w2qQ3>{=(lF-HI8 z&cX=H`Yq-1XhfiG8?z9Egpsv%hkYhpXbxj z+YX@$m}2qKj_Q5VYIw)f&vdj}{0380Gw=!oYfqlIs`gEuqaZC3_CP~mV)ZKyC=-OE z8MHq>izV?H6Xvr=t2b*RQQoP;Lv;R9HN%PU3<=rPvg{_sQw>cfpMk!(s7LQ8TUx*E zzD)k|&B*j{koml3hqz3JzWX~hI}g$6O|@~yvES9k5sd!t0`lLbH-uB;|1P5ZCvxDw zuCxRGA+~gAh%@en9j6i)J1-j)xr*x@9fnkGPU&(dRviYIu)h<9oLw@@u870$giklO z%kwoeYi@I9ybCIcLvo$}hN0m)*)c?FBF=?mC}{}?z9Tz%dSksQFU!@Qmm-~@8#Idd zD(%FC?PF05!M6DB=^4JoqH0g}{Br2^gJ|zddc{uBO z?R3JQwbOr1#Qqa){wH7h?@cfB&YBxsC^Qpy14Sp!Vhsa3CwA)KgNx-M8i4^HoDXYcn0n`#bMU zo#;5iHS{I+=??9YGM3!aC(%A>-cKhh2V=b}Z`=SD)^CxMTLpjU$BOKy`=V;_GBBHmsl^ zHB$8I^M!$q^lx=;^SxL>p|oCmm}KdMZ>fVL#a=|UJMe_{p-1<;Hf2qL8>k+43Bogn zsp{M`XqL+Tl)8HqRf691ttv<*j9X3_n1%;0J3nuGKIv@}_a14K+Lj$YLqHLg+f(IE z(OSubGIm6+Z?SO3A`AAk5K zF8*PHe;Dzf%kI;94re8wT8pG_vctf~y>UIj zgb8RqYv;-IHHb z)8z+C)#g9#9J8YeV~nqyi>8y8?CA0?YVf!!(gxCLXxN?jWd*#c;IW3Q&Byb?0(DWb z=4M0pQseM4GeP6y^ zO>6}CRHP-6a*;OBfRY(#XeqoD7s_e$l~^uLp$=)#=6RATB?SAtK9%8onUrxGC8Nbm z$5dR2Pz}28`&yH{Y0;thX=1|*X0uY`IK~rq!6j%Yr!6mDw&z17?B%s~@kH>4G#6wW zPGP<;%JZz3nyB(4&1;q1TrV!&TfIXm|lXoBPUj=N9MV>|cp6$lWq z4~HfEK(+p5YL8WMZz^+&+Jv=<5)CSyh;z%BQ{pUG0e%#+} zcpI&TP$WI;YLQLGoIcT){X+2k21pKcwRyhq3j4wyeFZu0l{=nrgmvk4lSF~vIC1w4 zdw!(3Go5&iEOI=oC(b(p-5vI)%}dW;fUE>2rj1Fja(T;uUgBY=yu@AHs4t%P7jANR zt~~Z`90l;c)gSe;4zP*zV$8Jl2!2pChE(c}a+hc~_adrISRF0>LW}{BkL03cww%Ea ztH}gf@W}uzz<+5Ec+Hzz)r>BHhD2iS#ITd@=^fySOI%xkO>RNjKA(H#tbc=c?2I2- zcYDgQSEsuPb$=`7^&@@&s3v#$2&PsyLXyWCy(X3|sFcGdmzj5rbaDSA`m2wf(xL?cDZS4TKHV_!MCi~qFcwBcehR^Fp-rv|>D}If5#kIA%9-m~kg{Lsx z_{BNtE20Zq^c6(4Zu@D#4*EBC5cLkfWcYGp&g1J)xYp$lhyeWL)i1b+tt9`JQ~w59 z;0dUgwKu@JBXl9f0cPZq3BLky-oz!~pqxflxk`kpH!X~;H7aWm$<8&su7hUWGW3r> zSnv4U=w#R@rUB)mI0Ic9umUJkK5~8MwVR6qR(@v!SiCDStgaY8WY&KFx{>KkquUq( zqUt(6O>ScvK#kja2tQ)-;offpn4ugglb~J>I3{KT+Zz6uI^Jp*k(55>He#;B8*nAS zNHa4)i{DbF;PB?g^%haUh-uW^1Q=CJ<)z`JcyE&znZHFI<& zh8%4=A(Az<{^bj|EO5&wH!c^m=tst&`{yA7r}NihM2!>e|3OB4ND$WjZS@Ov)xBkriEGH`65DUMh%fxt zU%G@$)&r8R$Q(`miP>vDx+RfoKKgA+r|;rncCw*pfJ50O{wr*M8QcAy9_*C9_PQ5Y z%bRcqU+y?2=LkH!F)Bb0MvcU<{%eo9byR>>5PocO)Br}mrgUje{y;?1#;qIR*~NcH zIe^K^4cT6!8gcfJi*Qr(nu15huRxms+P9qWzaY+#X@I?ecmNp4T2K0hih&peSN#%> zz-B@=e#avd^ZnEd_z(^xg`4m}&vm;FAAm9u{z$rU%Ust#uUvtmXpoyv0BeKwS`IY1 zaImM&zg)+l#i97fjZ>Oj`~YZeJ5C7)*ueqV%dk(fS^2Cjd?}ppI+y_~1Os;Tz5-PO z!S5lGf}PbVHh+J!^NaK*hfu^e=wrg0_VpfI5x5D}g|` zO)tN7zGY3nM}r<-*XZ4{CM=uncf|i5X55A~r_@S;`yA{P`9`VV*#k%uHoyk4KM5Nc z^LR&K9%}x3lL?#szgiH4>e|UV0MY>be3R0Crwa63p6S~}5IenqBB9ZJ?F`-4){K9G z(xUnB)}V%c;;x5jUF=JhH&5CAPZEXXaorUN4TzD|KV*0JH!NJT9%z3L!GOqqMtJcG zAl6?T^+1BZrT3N3zccVN@_>oY90-Cj^GtuZ_cwb3(Kzup#fPS%JasP_xN-5Dfe9hb ztwU7w0D*8jJm^XcO$D-!)l24hfXABtdf4n7u2=K=P4tVy#rXFGIuL^D`xJph1!xYi z-ZdTll>j9)zoCd*PTm-MZsp%B`DbvF(~2KfckSOd)^Uc5tx4=7m_fy+eCmmtB7b804eIwZL}DFHHlDszo!6MO!>Wxr3_K8a z^+>aTnp?f(-H_5;x&on_1{h?#IVhtGs}f-zwDKY}roi8NTU#X}JaNSM@L-qe%jc9Z030EVOV!r4Purly@I< zq_Hp6q#`ERJ+%yIjfY8X5r)I~-^(r%y0U}z13xy+4dt>d*Ki&{KAdY^A`IEM-*ab2 zSQ$mEpC*0ZvQ+AF_rtVRcw5*3s1%<_{2a0ENr9{&nFadEb1pX9w#)UIWLL0bE($szoML~+Iu>d-gbpMDSGFix&gE&}2 z2jwDqiG)qo^kebu6k`Jr73lWXJD@T<*&y!-tE!HJ)I*QYh$P>YSHItBf0fvd_z14E zgFWhzU16pY$PlIgSmRymxNlp$8 z@k6b&@IZhVVuD@t_48-2%_Q$|4=O;6pD2=jVvG$TU(0U+j{|T!D|iRYECRQIAH{30 z``Qu?U#Z}0J$r%3vXKJedmoO+D1 zGQ$tFkQ}sGjQuVI_y7{`A)qVJDE7M`;Eg)YID|hq=?{0}uU#q-&5r-Q`=Z9BE70b~ zUy;OJW&h-uEe z0G#DUMc{tG!IC#Zm|*>h=kMQhZ~8WIRi{0OE`d0Y{s{ z;U?uLTi1Rt=;}vSy$*!e3VkC2@CpFX9Gl6G?{F9JLO0Itnm zqv2TX09ZlV8)$k>5u7B~kGmc&ufcSjF>5rM1YsrNF~iqaW6#1ClSs~BN0ZBd8{IMq zk@<1%4UH7N%qTZ~`v1O{hT0Fu!sfpJ_o_CW7KK>PZ!Ya9{p*Hi(G%+6-xxQO}y zYRPEUM-ksY02;{#I^43E5g=8@wI>3?3!ni=9HNLfuK-w)P8ZnR2v8IYOzHT&u3f(c zn6uuphlZ&}(@P*^E)ihcM}U-o)H7a-ZzpsBvLLG-LXpsHJp({_{P3Dih+f*sUH~2z zZ)9;&hz~v7$G&OhbR8Z5-=QE^YuEs{O#`zn9DpbV8i5(24&w;We<)pGCw^o)&{q>M zAah&%9Ubq0&JJ{V5J_eM&zI0_=@P(N1bXt^K@qhms`#yq%;AkM1yxUG-VY4Fq_Pu0 z^wN=iJXtjRV$o?rbRMC6;J?hJ|Brt}=+HpKvk!rM=eM7IL4?_#w80s#qq}GnqZUG0 zp^H-G5PvML9(azn$sfs}*Yc8jOl_tu&1i^5S(&b{EzfA+j(_V6wU!*$afiKsvuBEK zwk`X2cOH<2mUTwEkk8>^U_tI=OpDka`^palWB2BVHT;A7L|!_CZ}x#D4zGCoCehK5 z?g^8(lrBV<}!P45Dx?9>hM;B!A#K@wp-q7y7==7rsX=KqV8j z6If9#V#*?AZ%rH*=1BwTfE~u!z#@71x9L*gGH*M6K!<*{*4+U+ybG8e5SuF}=(Bpy z?MPSY&k$1=MUR@9fEy-3`{w~?(6+FpcvL}Hpe67IfSBa$lPb`I_~k<`B0f1!q%9nK=KKrq6^NMM8qE%mu$9m|?2_TfqA2`s{~qb~@!fDD4yeojSKPM;Lb z|LYUbj5(G{gU|7P2hF9=?S&B=?(FEp^9u@Za0(~0X1?5-`zK`yt=waT%kp7FE!vG>_C`Dg zrv9&#F!N!2S_cyvpI2C(0)|8z(ZLO!k%Pnu3wDB zDd5GKH^bC53YbGKman4ewG&s(;F(hnq!s=>3ZtE>!S5D+#Mr)w(;34ouWs-XkOZl~ z*3n8F%Zp|dvu*~TR2ixCpKR-z!$h)yKLyHiI$fNQhBK8SxmnR~g4r8I`HVD2+omn0 zhHxDQ`<5@g_9HN9T+$`03Dk!bBNvLIccOKSennWA-%Z2p4L2zRU}t|&8X5znjFbtYf^s#WI1La7h{<%0_i0s+eBP@>xx&WcU@H^S71~iBytZ zNeQX%WUC|5`GU>WI$RbaS$lQ>7}Tej2Ps@6P9gBXwy86?B+`mp*G-7lT+F2zxXsGIWwe|d4{KSk@C6U^|Hk^BFnmus^WV*!;Fi_|S$%N9JiZwjox zBJe(!)v?BIX6GX81 zJzS(9YJ!EN>iEjQB54HvZqkt6g+6!02W1|Ey4uJeDz)IzmR2N;w{|v zyn?XnXHq|z447>?rF7ShuW4{lqM_nU!aUZO(wnxsvPQiR@=0BK8p)39#)8dhqF!aC zq%rl9$`gD(kzO&T-dX8po|zJ)N-17-JL)EzxT>~>w{JWCSBoU&YeJG#1(yZ$5vt~y ztVF#(sow6&iDNOmzw2CTSH`5_tY$FHX}Mj?3&OST{Gw(tK{WB(u9?`M@pA0uMZ?Ks zp~-GK;R2=3>gA_n$lm*_$f1*)e&Zfs>#@xW-8c1b864geRh}Ga8afeRiPq)qHOqlF2#K6UKCwfR zy0|^XD#$}~N<3T=UKzXeh_R@pW|3AF!;p+&r)Y~9X7l;6uc#*FgtSesoxySzN2Ap2FUM|M`{UM28b89 zk`BgE(u&N135-olx($rXGU~2Tf>qEm+S-9UWyY|x$bl-u?T+UwD|h6rQbp@Dvpe8>fv<7{L4Oi2%6t-_k@r#`@-C2Ws`|*l{gqhXpQdyR7&!Ml<@Y z`ZQ6VRD8qfDO&l3l_5T<<4Zv7uwYAg4S>Ubaj!z4$g5xHIjFDxG8sD( zFR=U$&x$Ld-UC6be>biT&{CoAfpjriuge6e+d>T~gOGqEY-cxYd8ewX=~ztUWnsRP zSRNV!;GKD!#BkRfM80xXAFxd#wB(G#mmbsJo}@-eD6H;+Ka z76R*~Xdv4=A`;R<^nuRYQ)iK9s>f8kz_k}1s@Kg?M={I0CLKuFxJQW)29B3WCuvk> zd>J;xGsExE^muve0B+@3(Eze~g^WhTz7TA^_6h+}&C(XwRc{W$!W$kkDO@5VRmQCh zFF;79IgnOQ9{iMnTeR;+rn&I{`3oXUiB$?+CXTR z3WhMBfAtQTEKp^usrsqN`+=9)+JT&Aa6fofZ%;kp&WLr!GP(vW1V<` z#W>70Xu6e-Q{c6Y6V8`OAY_Eo#ceWfqk57uGc}I~PZ!DR(GH?KB`kRX-jUe_bpVm+ z1KXI+XA4_E=2^8*xL;A~NV1|3=@Rczt8qJ4!K?Q}^PP;P%Hb!(*q!`bKpcSEYwURt zR--W%V;fjK2Bv>q26m&okxqiCzl|w>!aX?KL%5Cba(%1zHW0QE1FHzN5IAguO|6b~bI%kK`@$Npd+Ev$sV!F9wVw6%mwRgF2T zsc$m6vgm@C;@poYpmm}D8obzt6eP-rt6$++S;ha`S1Tq=aMS074m68t-FJxDMDfG; zL~2xR7L=LUt*2j#hgs21h55=C;7)7q{ssZHQ>EXj0dk0FL1fqr>6(B{+Jf9luj>P) zxic9rC8rnG+kZgHkKI8NZ9{L;&8Q|KI^z)FBNs3_Ct zEel-XwIUtj2z-@J3@IL;6bW1-+KFq!z!5D_Z z^AtvGRIkA7jkzj;s4HLTNYi|-NA3s7ND3;^I*kKMNE~sIBMO*vbD#2r$gq8iJZgSg>60IGT7LK>XoNQ>JZ3O>G&GM~oBrap z`*KqGW2W*}q2@C;P2YnNm0r8m^X`Qaw)$r${>+N#*!}y}0Z(o9D|&>?zSP3wM0z6a zDo@iOFkiX;*}U?PzfmI`s)`#Zuk?y+s4$-@YyQlgx?N_G9yPz*jr_miD?gwzf*h0T)3 zO-DP`rtCM_=bptiUm*OE)|wtr>1CL6dAcSdKnwGI@eMmkdK@u{zRd=XTbvTO_1ca? zDSt=IxtaxMx;o$^Dp%9;;00H2Pn|4LlpY!}iBFYXhDKOVGg){UBe*`HEw~;%lmkuu zpo~SLRsFPyUYic=1uR`=H0fn#Ie%N)w%fwH&7@bc4Jl$IEk0{Vk1#H4PRI5iWgJ6x zh7;06l~XaWMUTIF=b0kcRb6ty@xnoBK>MuLV94&6snvC zN4Q)S6XgGj5oAwTFO^3?X7oZ3v5=b<4IZzxhb{ShLneBz(HOrpsN4l08#WzNQ6nCQ zgE2HBJ+-)E6ayz|lvDF(!|P!!Rz&uW5o8YRTLSVdDCW846fEp-gSg-eJWMPe#|2>z zI?mnV&d%QL1Te9T{Q2d|SCOq#IMM)djZHE2^#&l)+3VO$dcPi}1i8m;=@KzIvEv#|$1oV_r)aC;+1cBlfwz+)eey|#Q)cF0 zo;T&UXM5F8?}sh<{gw0ut79b11_5bP=i7WwnNENM&Bn3?$?l>ACSY~1M{o^}6~uca z9az@is@FL131eS-#8bBEy>ll%m1TYua>n~o>Odp^c3z5XRB4eSftc@EkjIHnOU%=3 z-ue6|+Hgpss!swU7r627k5>6oU)GR5vY#>qK(zSKeSGyhBsTj_2MS@>7<(9g0zlpr z6eY|a4UQ4q+_XmV!qABhAi7;-3v#g&rTbKjzQbRI`&HCnVFCe;D{-)|5s;^yIiN=$ zX~QZ=W?BrazgxGFUP~C|v^^Y*J^L}P!GYpO22G@@5@8hti`nP)xe{06`0~A0)VFvc z(>ddCnR`k2gek@Cz z7}*7@L6@uR^cA^55f-oMP5B^PPuW)M)A-CgqY9_Cna6TzmKhH9zi-`v*4MMlHO3y? z7as*XP7U325Lq0t8+H*8s)i=RnRv6Eu^5Ak3;%Xf&Mc7{N&E?C=yq5Jrhz+WGE%GSq@ z(GI-n$^rqYO<4^CD+?S^dI-GW18T68&+|HxX7M{DX~>iWcGgb`VhmFSXQX+?_0u-O z>QJWsSQ$Ve){j*o-g+4$h~;DKJM)x*EI_3<^q>kLV}}KK$sQ;O%3`E4oIL`mm8^X!;3CuofG~6bl^^AcLpy}p% zIJH864d8OT*6sjh^vk+$G}#Dcv(Vc&-Vc7^OS!VEukfiUVeJLL^5{oc!=nFA&xFlgu;_W zA_Jtd1I~VI&8kF$08dxBofSMHsItz_#PRH#)E&0N6u3mRG$fu4m z;&kIJBp2|*1{&N4PX`(qRHkE{2CDr>@og%LkCu?Y_D<*|@P_PS>kFNs5)QYYE{1K$ zIKo1oYM)KuL$p3m$(-?Det7@?hUeernF(ymf=t*}%O-cx)L)PrFQYyDN0k=w(@toyN~(FTBEK>@ZTp(+1A$>hl_SqeK<}^THxPx{EGUm z^?16~cI>(CNAA*Oj@}(_%wxSQWtIM}Go8DSytPkq%cgT)*%lqhG*+ec$tjp?CPqL1 zQhH)k)A!eaW9w9pXXy>>{{7jRjF+K@E#4RJ4?U&$=3dNo!I_%oA{UwG8{EH*DW8V$ zW+|Kj#qN7Cet68rsr{VZ-iJs3YG!U7Gm2#p+_SS-f%ISjr+&+c4?&BVUsISE3Rn5w zNR0(n3$rmewPu?Br(GR%p>WphMCDmo^VQG*oTqcH&x} z8s=yB{^iOK(uMf!e2*yNobk3pgK;zk-))ph*dooG^6!HFJQ;$!F`f2~kHHJsj z4(jO_g1e1pB9$LpJuZtx^PD=|aGp*>Lg+T*D{7tUzn)4os>?A4UKri|9@fJ=`ccQL ziFQr5!ZldG)J#+7PLXqV(Ul9+|1KBcGfz{EYjU;+Vw`AwkDB1RB$AO^SuM?5l73A> zxW37X;GQAt55gP8J@Sa!l~O?3j-vzMx!NafQJl9|vBv_mqKM1}#n)|7tWm9>ai3YY zY%)HMUPo^s&IKy?3L05nfB8`j+RXj1`g*N|dDcRcC zTqjs?uEz?w9#Pu^r_R}8J%+I*2VUTRuP!mF%MAMy{!!b9daQ0VQOVpqR#gKLVAL6a z*BN*U9|;o_MD%AZv^mLFsXA|Nf5Y6;-URoVQ%+NquF?mF3G|GikYM0va1gF-p%~$_ z{&Y1=f=DEjN{Iufj0Q5ZcZhFAu^Er2DRVMrr_)@eShaKuaq1CfT_x%<4YrSqU@_Zh zAp1_hlz4;{Fez4b+fw;4RIM)iPEgG>MWgs-s;te)fbp2-$%<6E8md}k3uWH4QlQw= z=5%AEE#hb;tLSMaPg<@OWvuF^Z}~UinP?Z{JbykmK-!@>D?~0lZQVxeg;8?og1fQV zyCHLgc5go+kjx)|1k6E>(*VvPVSC+@W{lcbEwsfyVLZwq+Fytjur0G?i7y}y<`z9* z|E{#MiWYD#Ou#9N6%=+rmcF7mjFIA+rIZAW%-r&Gt?cx|<~q3dC!~M+i>DAKs7JYQ z;vl%qJZ06F!C`_ytWqK&*ATeg?pFq5VWqgk7e&2vvqLaItqBzDzrBIdCHkwU70$w- zx7s55fs+#r&u=P;;7bjc!NBqA3nOhm&$2s=>V5}}*`0v9hjtE|9TMUK4m_8nB&xVs zR?i@ksow~FX7YML>5+2pHkOy_zigOWhUCiKF#je`pJ3ZZf_O*EpR`5q@++%VH*)=A zJGyyH!58{sGe|~LVcSk9UL56uJwnl&2lg5js>aLFyr1JQ1Hn8+hk6~ru$gX+b@ zQ_L-fOT7DELxyCPZV{t5cNNwtc4q~g$2iTgg*(Cojl<3Va4utvG!7dLfOK>u>C&d- zS8#;fRRVScMV9zBJtNj~V>8k!Bl6X95I_F_kH0|_!9BB3h;cgE2P||ksw0oOAK-%vAaE@_YMwlei+`m9o@PnORp`GZE^uoqX^Tby;Y7*^#wlK|{5E|wCIZGq)~QOI@`^D|uO+Ibo1Htzw1D55 z2=we6l9A7~#Qt>2nPDvzY`^)_ktX}tJmOp@fUMur$LS~dKKP& zZil-+tYx`UP%axcKaWe@S>Bg1z=<;TOXjVXB#ZnMY&oImRrIHg$z4G*&f=~PMdl^* zi85JBo^dI^x=V;LC{RYPiE?N1^t!A$X^P&Hh|h zjJ`Crgug!_xhs$Xz0#kwT%W8e*}@)Jk&~3vo-E}RCO%aw zuEMWyKl~pVEDjQCHxxOWP?v| zJT4)L>@B@yF=_W|iPo(~4Cq*d(Qgkbf#@HZJzxN>Su3s*p4KKe!1w8zmE^5bS>ppy z{P+&;7=;ZVv*NefaFZmm`O=Sr!5&DC@d$g+)_>YVr{zJGTQGSoW~*iXwgXqrLq_^r z45~^}p?e9a%=v7_697=I`%tjd8p1_jcHr%IK=SKTgFUpjMFKxf%ua0?LhcYi0B#RP zrJ>iHdE^#M3MdQqFpow7Z^wEe8YH;V4!rAtLG~8Bl^A?91-=TP+&qSznYPOdX zp^@>=_>O*;}!m(2MQvw zyamEXs2wQ_*3@4T&Z^)o6jd)sNr*_HT=Bwn%YhM&)((~TzeX`NYKGw+;ncY@`df_w z>U+>{rWqiXv>T=J{|yN9)&-NKK$wL9?t9T8XT~QvhzY+u~B3v|>1Fox6 zmPG2cCHyFRjuUJeh(d^Ugsu_Q`fJFMzB!?ceE$B~ey@d=>z_0RoRiG{tN@8F*(PsS zl3TDF#-NZaRsnT*qu2P9u_rZzodeCumVZDTKlWz@Z|yMntt2nYEFYoyBXdoRp+QxF ze57R#NvXsi0~w51Lx*qDU&`toq4rK^(1wTQMy2EiF(M&9W4?26;8Zf$zUhYO6m)CJ zY1H20?2{Ow(c#SrotXfkubMgls(rW`%C$IzWe9&rJxHWw`A--VEJA+s*_5n{Kr`+Obf6H29~}<{dRoS;5_v!xC z#P7PPCqzDcsuD+hBDZCULRE4NtCLKLb;`SNLl|QAXO-U$rb!Ryz!BVCI4ECoO~w3F zyHwxR0SX5Xz)?4j;D-YnW1ifjTxJ~h4T;O=i>fYjtZ&O(>CG3FD7$jN!b6P2r&d2scy{F7bI(88Wc4*55>&eAC99YOGH&y5gliK(z{vNj#YE zyL#eZs6V>@5_)q+0nCBFC1|YMU(Xb?`i;&>S)$~dP8V+lDjX;p7Nc>zF^vs5LhB;G zN>m|VuD}`ftJ`R|4}KK)&Sz+t?%j~MxctZm94S=o;(7APDu}uDc@Q7Yp(Oui!z14y zyc|~84?og&9%JI-J~*!}sj!s#`k^YcTy*ho|VA_&2tr zxL2O54;OXi(M@;7qtpexz5$~$jy-OkIcQ^k2+xL{D(3oPo><2VdZ+kw<)e9wi(aL~WmIfOF2co8@AEjSY!aNwM7xUj~QO-`&(H z%C)(3xL_rYI}xGa%#qeU;Y@1mh8-^jaauqMN`)3EUlkm`>%x(#i2lwE89c>o)&SmP z_KaM4AEJntU0J|y;WNA^mH)Kx@RMOJvqnTG@q0QqWOEMw!3TO(&#r=N;4s%pn=z2h z!Slx5jdIlCVERbWBBAVvFE(~cdbpq*Y{SI83^;elfo&x#>aYUHpBIDp^U4SBN+-2U zcUv9)5TezA_xy;9Jo15>B}(aL^!{wSgA7={?W4|?Lad#_q3iiP1DSWH|;yz#wm z!^&lM;pBfRgkI0X5al6Y&eU$*F)<>)zh8TSl%KskT-3QCvC|#yYlOM8sD^)sMVK{T zc1ufcbvQf9e+mfsta8(7)3MN_rbw(8^NYF(7kr+V(Ypx^sz~2Hb*Gl~g+|)#yR|9C ze(*p6%H`{9NKn7po0A~`GppPzaW~g)E$o?G? zmp`qxe^!M&$BwxC0^)DNcUL7U!v?oHbte_tVl!t~7M4ok1=2~?ud zN6Mu98YDvlM?U!LNL(taRAQa8^G!(|M?U!cnrSBE^eXSco$L$bgL*mEJ1I%bS9TLf zRA0P{KKp5WcmAQDR+xEf6%2N>90P?=dEXzy-MU@6+Uj!8c$c;D@BI_^{n4LNQ#7g# zg^5*%_Yhs{ioDk?3b}Ycsv!uB?tFPgqZq2%pe9yp#l{~Yn4(u3n0zQm*~tTMK&3)1 zV94+^(K2gD7bjn}j%Yp{q!MS;A7FJ>_phggEW=5%FV#C$wd{ZJi2CneGIzG21AZF4 zY2E?7qvbVUK}*zGMDd?ve zpGWkE@l7EQ1>ve#v1W9=p0Lnr;!ymPlpD2^N~K4b&*1}NhlzhSyZI+K?S4$x-;gM+ z*L&2SPk)H|*h4I77XCpoG7obyY<=qJ*Hx!fdN?P+MRqy-1J1!0&Y~+laBXFu?o5;X zOOJh|dRDTmD87ji<~wtm{2?7$UV8zGaL1x+hO6s3v<(G`ga>@t-^dW9o1?Q%bUwyvyFSk6?*Qwzo}Yy&~fa_ zbI;VCF8`)5Us(2>k{+pvX)e1Ov(9&h@$sh+9E0AY)BoVrhQzKav}Y0a{kWDXp(^}? zsPTd+9ksj7QUV1tdr^t)R$v-!74~5@ls>1duB`C>Lg7kk4*FPOzNwS{<<`?h=eA}G z6?*RNg=fy9QiU6#awW9~x-7Ml-KT`W5Ra-EuU`jgMkkg(vlSNk^##MISosNsw*WB< zuJJPu3wbQPShB$Q&_m$qNZsxA)FtVKeJxTr53k1s6meAGz<|8r*w!TpI|Kz!j9F*dD?K zbJK{6r4E>SkJ+h|8+R+R!aqzb>O6R*1cts$?=cYCR=CB=k%k9ugpvEX-u(|6MjpI= z5BKjQe@M${?g;nAk;Od&@b>`Q$)b?;ODRGQNk2Hkinw6vv}f2nEZWxXv!Ax+>D zNx~E#>V;7Q$j}fw@}OaHm1cCtVCtOX*pDFCC6q_1L9rjwCN*yq!uBHw<3o?Z@z#@E z=Ab7{^F{-z$5=D^ZpUd5wehE(@u_8{2N+>DR4wXkg{AvWF7a90;H4Sefa)d)PFDP* zlxwqUH!f-Zk(&-u6+)!f^(KopJ?VA)vEwqWXPl>W=SX!*j;WIu(j&rV@gbwQG^v|q zhECr-_XXx4#b4p|eGi>`?t|aN7c#E!_P%`?7^3f_$$)uS{5sR9YANGmPd{ztp`j4X z8{cAaVW;|LB6|Jf2||}!KE20~jTrDCVHY{GGDb zuj@<$%h0Yx0bm)hlxkk&5)C?1ml}KTt*5;K5+m%xB7ynTF;vnU{vlTk{k^?MjVAkM zj3EB0Jva=C$(kVh<^f5@Jz}UhMpwZ+QQrnH!_*%V0(Ew+r%N4tCzsSFJyKjYX>PjT zG-d%6=9{(-8G5Cq&i#loOfj`(C2RtLbYd?qsdc$j?Z#cXeL$(hnvi!OPKWAdxBT!m z5d(-VeKVack(t=DCC51DSylrtdMghN*atw?|0xF)ZY&_uyiw~HzS1*Q^LSzhb%IfVzp|cIpr&F%8u4H^Xtz_TXo+l z2}@22i&6?dUgIgFWGDAvtWMFJ$3KaeO^Z;n6T%(haO|S^BVp*3v07;)C98yF@D>+I z7m66NfRyDvkCI{+?na_H@i)kG%B&lM!|YDwuc%WAIVd;{8$hcR^QbR%rZ$x^VjqUM zf6vN#LODF7O<)e89!8$;^X$vQwn$qw=6j?~w8v~pR_v#^8P#9-rJk|FT7K?N{Ay(i z6Gmc#v(n3oZx>P@25RjpY#?D@1F%32Ci~1Qv%+U8hzZ zpc)!^&a$oN^3<|>?)or3Ef+;MklA#l0T2~l%u;)Y6ZuX%RkqP7b-;SWc8L}EkEnU zHnqDIO=epcUto@fL_QtpV-7|(2Y*F5DE3XX=NMA{B;!hUe2yzy7=}?J&T~4*dJ&Oi zP@vMM8WU|c>F8Ri&sbS;RP6>UVx{eM%n9V@f=`4Q&v5bUB=< z`(^&-1+y_@F!0x#*Nk~{OWI@ z%*wh<-`BMwrNqxcxkj-iLPyM&J|RD6%Bd)=65?1&A{=nlvKicHukUXJ`e6=f!-sLRp8*{5xFrN;(4h3Gay{;X z$p9HwN)`*^9(#zjEH;wPb9h>+5s|TkLY}s2WD=p%Dz93kmee<5tQF&VLe;0DMYY$u z3k2k+8pw{MFQk2@hVf9ky6;@Suk2OzBFaY@O@#`oblYvFH@g~j#_=oXxXkjI(^})h%s;1FgW}W`?PE>uA2HjXr8i`hXTbL zx;OK`M5#4~45KO+1H39-dlWy9i!gg=xpNrTQLPwiz2ZBwKo`tDcL5O7p>{T+_31*s zmVd|1b0@UOq9#&o8)FVj%!{1+?UFd9wyRDdaJ51v|C5%gccc|t+7!!`x)U`1jYbN0_idBkp|@zvN@Wt7{)d6Z$07Z?3nSBS~=TF66feXFn7cf2n# ziN25ydapvaZxuOw&&n3!Bks{$Ux^r$XI?%X$5sWn0mVSfAA7O#L!(;GXBffzTIQ@k zgMmEk7?=F9N%^6s1ZC0%SMOW#xpfGJS&r_;ntVj<(&q+aEn0sE_OCbJed&rsR1Mbh7%q>SVZMToa&S Developing applications for Flipper Zero is now much more accessible with the introduction of JavaScript support. + +Previously, building an app for Flipper Zero required C/C++ skills, setting up a development environment, and studying the code of existing applications and documentation. While embedded developers are very familiar with all of this, we wanted to make it easier for people from all backgrounds to create apps for Flipper Zero. + +Flipper firmware now includes a built-in scripting engine that runs JavaScript, one of the most widely used programming languages. You can create script files, share them with others, and launch them directly from the **Apps/Scripts** menu on your Flipper Zero — no need for compiling on a PC. + +JavaScript support is based on the [mJS scripting engine](https://github.com/cesanta/mjs). Originally designed for microcontrollers, mJS makes efficient use of system resources, requiring less than 50k of flash space and 2k of RAM. We've kept the core features of mJS and also added some useful improvements, such as support for compact binary arrays. + +> [!note] +> mJS has some limitations compared to JavaScript engines built into modern browsers. For details on capabilities and limitations, refer to the [mJS documentation on GitHub](https://github.com/cesanta/mjs). + +JavaScript apps can interact with Flipper Zero's resources, including its GUI, buttons, USB-HID device, GPIO, UART interfaces, and more. Let's go through the steps to create your first JavaScript app for Flipper Zero. + +**Next step:** [Your first JavaScript app](#js_your_first_js_app) diff --git a/documentation/js/js_badusb.md b/documentation/js/js_badusb.md index b21126dfc..df02dbdb0 100644 --- a/documentation/js/js_badusb.md +++ b/documentation/js/js_badusb.md @@ -1,6 +1,5 @@ -# js_badusb {#js_badusb} +# BadUSB module {#js_badusb} -# BadUSB module ```js let badusb = require("badusb"); ``` @@ -58,7 +57,7 @@ badusb.press(0x47); // Press key with HID code (hex) 0x47 (Scroll lock) Hold a key. Up to 5 keys (excluding modifiers) can be held simultaneously. ### Parameters -Same as `press` +Same as `press`. ### Examples: ```js @@ -70,9 +69,9 @@ badusb.hold("CTRL", "v"); // Press and hold CTRL + "v" combo Release a previously held key. ### Parameters -Same as `press` +Same as `press`. -Release all keys if called without parameters +Release all keys if called without parameters. ### Examples: ```js @@ -85,7 +84,7 @@ Print a string. ### Parameters - A string to print -- (optional) delay between key presses +- *(optional)* Delay between key presses ### Examples: ```js @@ -98,7 +97,7 @@ Same as `print` but ended with "ENTER" press. ### Parameters - A string to print -- (optional) delay between key presses +- *(optional)* Delay between key presses ### Examples: ```js diff --git a/documentation/js/js_developing_apps_using_js_sdk.md b/documentation/js/js_developing_apps_using_js_sdk.md new file mode 100644 index 000000000..952993566 --- /dev/null +++ b/documentation/js/js_developing_apps_using_js_sdk.md @@ -0,0 +1,77 @@ +# Developing apps using JavaScript SDK {#js_developing_apps_using_js_sdk} + +In the [previous guide](#js_your_first_js_app), we learned how to create and run a JavaScript app on Flipper Zero. However, when debugging a script, you often need to repeatedly modify the code and test it on the device. While you can use qFlipper for this, it involves a lot of repetitive steps. Fortunately, there's a more efficient alternative — the Flipper Zero JavaScript SDK, a set of tools that simplify app development in JavaScript. + +Main features of the Flipper Zero JavaScript SDK: + +* [Loading and running an app with a single command](#js_sdk_run_app) +* [Code completion](#js_sdk_code_completion) +* [JS code minifier (compressor)](#js_sdk_js_minifier) + +In this guide, we'll install the JavaScript SDK and learn how to run JavaScript apps on Flipper Zero using it. + +## How to get JavaScript SDK + +The JavaScript SDK for Flipper Zero is distributed as an [NPM package](npmjs.com/package/\@flipperdevices/fz-sdk), so you can install it using a package manager like npm, pnpm, or yarn. You'll also need Node.js, a JavaScript runtime environment required for the NPM package manager to work. + +> [!note] +> In this guide, we'll use **npm**, the default package manager for Node.js. + +Follow these steps: + +1. Install **Node.js + npm** on your PC. Check out this [official Downloads page](https://nodejs.org/en/download/package-manager), select your OS and preferences, and run the provided commands in your terminal. + +2. Open a terminal in the folder where you want to store your project. + +3. Run the `npx @flipperdevices/create-fz-app@latest` command to create a JavaScript app template and include the JavaScript SDK into it. This command will launch an interactive wizard. You'll need to specify the project name and choose a package manager (in our case, **npm**). + +You'll now find a JavaScript app template in your project folder, alongside the JavaScript SDK package, all necessary dependencies and configs. The app code will be in the `index.ts` file. + +Now, let's take a look at the main features of the Flipper Zero JavaScript SDK. + +## Running your app {#js_sdk_run_app} + +To run the application: + +1. Connect your Flipper Zero to your PC via USB. + +2. Open a terminal in your app's folder. + +3. Run the `npm start` command to copy the JS file to Flipper Zero and run it. + +\image html js_sdk_npm_start.jpg width=800 + +You'll see output messages from the `print()` function in the terminal. + +## Updating your app {#js_sdk_update_app} + +After making changes to your app's code, simply run `npm start` again. As long as your Flipper Zero is still connected, the updated app will launch, and the old `.js` file on Flipper Zero will be replaced with the new version. + + +## Other JavaScript SDK features + +As you can see, it's quite easy to launch and update your app with a single command. Now let's explore two more important features of the Flipper Zero JavaScript SDK: **code completion** and **JS minifier**. + + +### Code completion {#js_sdk_code_completion} + +Code completion helps speed up the development process by automatically suggesting code as you type, reducing the need to refer to documentation. + +\image html js_sdk_code_completion.jpg width=800 + +> [!note] +> Code completion works in code editors and IDEs that support Language Server, for example, [VS Code](https://code.visualstudio.com/). + + +### JS minifier {#js_sdk_js_minifier} + +The JS minifier reduces the size of JavaScript files by removing unnecessary characters (like spaces, tabs and line breaks) and shortening variable names. This can make your scripts run a bit faster without changing their logic. + +However, it has a drawback — it can make debugging harder, as error messages in minified files are harder to read in larger applications. For this reason, it's recommended to disable the JS minifier during debugging and it's disabled by default. To enable it, set the `minify` parameter to `true` in the `fz-sdk.config.json5` file in your app folder. This will minify your JavaScript app before loading it onto Flipper Zero. + + +## What's next? + +You've learned how to run and debug simple JavaScript apps. But how can you access Flipper Zero's hardware from your JS code? For that, you'll need to use JS modules — which we'll cover in the next guide. + +**Next step:** [Using JavaScript modules](#js_using_js_modules) diff --git a/documentation/js/js_event_loop.md b/documentation/js/js_event_loop.md index 9519478c0..04192b88f 100644 --- a/documentation/js/js_event_loop.md +++ b/documentation/js/js_event_loop.md @@ -1,6 +1,5 @@ -# js_event_loop {#js_event_loop} +# Event Loop module {#js_event_loop} -# Event Loop module ```js let eventLoop = require("event_loop"); ``` @@ -84,7 +83,7 @@ Because we have two extra arguments, if we return anything other than an array of length 2, the arguments will be kept as-is for the next call. The first two arguments that get passed to our callback are: - - The subscription manager that lets us `.cancel()` our subscription + - The subscription manager that lets us `.cancel()` our subscription. - The event item, used for events that have extra data. Timer events do not, they just produce `undefined`. diff --git a/documentation/js/js_gpio.md b/documentation/js/js_gpio.md index aa444bacd..bb60ac5f3 100644 --- a/documentation/js/js_gpio.md +++ b/documentation/js/js_gpio.md @@ -1,14 +1,12 @@ -# js_gpio {#js_gpio} +# GPIO module {#js_gpio} + +The module allows you to control GPIO pins of the expansion connector on Flipper Zero. Call the `require` function to load the module before first using its methods. This module depends on the `event_loop` module, so it **must** be imported after `event_loop` is imported: -# GPIO module ```js let eventLoop = require("event_loop"); let gpio = require("gpio"); ``` -This module depends on the `event_loop` module, so it _must_ only be imported -after `event_loop` is imported. - # Example ```js let eventLoop = require("event_loop"); @@ -31,11 +29,11 @@ Gets a `Pin` object that can be used to manage a pin. - `pin`: pin identifier (examples: `"pc3"`, `7`, `"pa6"`, `3`) ### Returns -A `Pin` object +A `Pin` object. ## `Pin` object ### `Pin.init()` -Configures a pin +Configures a pin. #### Parameters - `mode`: `Mode` object: @@ -49,28 +47,28 @@ Configures a pin - `pull` (optional): either `"up"`, `"down"` or unset ### `Pin.write()` -Writes a digital value to a pin configured with `direction: "out"` +Writes a digital value to a pin configured with `direction: "out"`. #### Parameters - `value`: boolean logic level to write ### `Pin.read()` Reads a digital value from a pin configured with `direction: "in"` and any -`inMode` except `"analog"` +`inMode` except `"analog"`. #### Returns Boolean logic level ### `Pin.readAnalog()` Reads an analog voltage level in millivolts from a pin configured with -`direction: "in"` and `inMode: "analog"` +`direction: "in"` and `inMode: "analog"`. #### Returns -Voltage on pin in millivolts +Voltage on pin in millivolts. ### `Pin.interrupt()` Attaches an interrupt to a pin configured with `direction: "in"` and -`inMode: "interrupt"` or `"event"` +`inMode: "interrupt"` or `"event"`. #### Returns An event loop `Contract` object that identifies the interrupt event source. The diff --git a/documentation/js/js_gui.md b/documentation/js/js_gui.md index 2efe10c05..c30447c54 100644 --- a/documentation/js/js_gui.md +++ b/documentation/js/js_gui.md @@ -1,13 +1,22 @@ -# js_gui {#js_gui} +# GUI module {#js_gui} + +The module allows you to use GUI (graphical user interface) in concepts off the Flipper Zero firmware. Call the `require` function to load the module before first using its methods. This module depends on the `event_loop` module, so it **must** be imported after the `event_loop` import: -# GUI module ```js let eventLoop = require("event_loop"); let gui = require("gui"); ``` +## Submodules -This module depends on the `event_loop` module, so it _must_ only be imported -after `event_loop` is imported. +GUI module has several submodules: + +- @subpage js_gui__submenu — Displays a scrollable list of clickable textual entries +- @subpage js_gui__loading — Displays an animated hourglass icon +- @subpage js_gui__empty_screen — Just empty screen +- @subpage js_gui__text_input — Keyboard-like text input +- @subpage js_gui__text_box — Simple multiline text box +- @subpage js_gui__dialog — Dialog with up to 3 options +- @subpage js_gui__widget — Displays a combination of custom elements on one screen ## Conceptualizing GUI ### Event loop @@ -27,23 +36,23 @@ always access the canvas through a viewport. In Flipper's terminology, a "View" is a fullscreen design element that assumes control over the entire viewport and all input events. Different types of views are available (not all of which are unfortunately currently implemented in JS): -| View | Has JS adapter? | -|----------------------|-----------------------| -| `button_menu` | ❌ | -| `button_panel` | ❌ | -| `byte_input` | ✅ | -| `dialog_ex` | ✅ (as `dialog`) | -| `empty_screen` | ✅ | -| `file_browser` | ✅ (as `file_picker`) | -| `loading` | ✅ | -| `menu` | ❌ | -| `number_input` | ❌ | -| `popup` | ❌ | -| `submenu` | ✅ | -| `text_box` | ✅ | -| `text_input` | ✅ | -| `variable_item_list` | ❌ | -| `widget` | ✅ | +| View | Has JS adapter? | +|----------------------|------------------| +| `button_menu` | ❌ | +| `button_panel` | ❌ | +| `byte_input` | ❌ | +| `dialog_ex` | ✅ (as `dialog`) | +| `empty_screen` | ✅ | +| `file_browser` | ❌ | +| `loading` | ✅ | +| `menu` | ❌ | +| `number_input` | ❌ | +| `popup` | ❌ | +| `submenu` | ✅ | +| `text_box` | ✅ | +| `text_input` | ✅ | +| `variable_item_list` | ❌ | +| `widget` | ❌ | In JS, each view has its own set of properties (or just "props"). The programmer can manipulate these properties in two ways: @@ -119,7 +128,7 @@ eventLoop.run(); The `viewDispatcher` constant holds the `ViewDispatcher` singleton. ### `viewDispatcher.switchTo(view)` -Switches to a view, giving it control over the display and input +Switches to a view, giving it control over the display and input. #### Parameters - `view`: the `View` to switch to @@ -127,24 +136,24 @@ Switches to a view, giving it control over the display and input ### `viewDispatcher.sendTo(direction)` Sends the viewport that the dispatcher manages to the front of the stackup (effectively making it visible), or to the back (effectively making it -invisible) +invisible). #### Parameters - `direction`: either `"front"` or `"back"` ### `viewDispatcher.sendCustom(event)` -Sends a custom number to the `custom` event handler +Sends a custom number to the `custom` event handler. #### Parameters - `event`: number to send ### `viewDispatcher.custom` An event loop `Contract` object that identifies the custom event source, -triggered by `ViewDispatcher.sendCustom(event)` +triggered by `ViewDispatcher.sendCustom(event)`. ### `viewDispatcher.navigation` An event loop `Contract` object that identifies the navigation event source, -triggered when the back key is pressed +triggered when the back key is pressed. ## `ViewFactory` When you import a module implementing a view, a `ViewFactory` is instantiated. @@ -152,10 +161,10 @@ For example, in the example above, `loadingView`, `submenuView` and `emptyView` are view factories. ### `ViewFactory.make()` -Creates an instance of a `View` +Creates an instance of a `View`. ### `ViewFactory.make(props)` -Creates an instance of a `View` and assigns initial properties from `props` +Creates an instance of a `View` and assigns initial properties from `props`. #### Parameters - `props`: simple key-value object, e.g. `{ header: "Header" }` diff --git a/documentation/js/js_gui__dialog.md b/documentation/js/js_gui__dialog.md index 445e71128..dce45c49d 100644 --- a/documentation/js/js_gui__dialog.md +++ b/documentation/js/js_gui__dialog.md @@ -1,6 +1,5 @@ -# js_gui__dialog {#js_gui__dialog} +# Dialog GUI view {#js_gui__dialog} -# Dialog GUI view Displays a dialog with up to three options. Sample screenshot of the view @@ -16,16 +15,16 @@ This module depends on the `gui` module, which in turn depends on the recommended to conceptualize these modules first before using this one. # Example -For an example refer to the `gui.js` example script. +For an example, refer to the `gui.js` example script. # View props ## `header` -Text that appears in bold at the top of the screen +Text that appears in bold at the top of the screen. Type: `string` ## `text` -Text that appears in the middle of the screen +Text that appears in the middle of the screen. Type: `string` diff --git a/documentation/js/js_gui__empty_screen.md b/documentation/js/js_gui__empty_screen.md index f9fd12553..68b7e3124 100644 --- a/documentation/js/js_gui__empty_screen.md +++ b/documentation/js/js_gui__empty_screen.md @@ -1,7 +1,6 @@ -# js_gui__empty_screen {#js_gui__empty_screen} +# Empty Screen GUI view {#js_gui__empty_screen} -# Empty Screen GUI View -Displays nothing. +Displays an empty screen. Sample screenshot of the view @@ -16,7 +15,7 @@ This module depends on the `gui` module, which in turn depends on the recommended to conceptualize these modules first before using this one. # Example -For an example refer to the GUI example. +For an example, refer to the GUI example. # View props This view does not have any props. diff --git a/documentation/js/js_gui__loading.md b/documentation/js/js_gui__loading.md index 52f1cea49..0d3c18644 100644 --- a/documentation/js/js_gui__loading.md +++ b/documentation/js/js_gui__loading.md @@ -1,8 +1,6 @@ -# js_gui__loading {#js_gui__loading} +# Loading GUI view {#js_gui__loading} -# Loading GUI View -Displays an animated hourglass icon. Suppresses all `navigation` events, making -it impossible for the user to exit the view by pressing the back key. +Displays an animated hourglass icon. Suppresses all `navigation` events, making it impossible for the user to exit the view by pressing the BACK key. Sample screenshot of the view diff --git a/documentation/js/js_gui__submenu.md b/documentation/js/js_gui__submenu.md index 28c1e65af..755d87d8b 100644 --- a/documentation/js/js_gui__submenu.md +++ b/documentation/js/js_gui__submenu.md @@ -1,6 +1,5 @@ -# js_gui__submenu {#js_gui__submenu} +# Submenu GUI view {#js_gui__submenu} -# Submenu GUI view Displays a scrollable list of clickable textual entries. Sample screenshot of the view @@ -16,16 +15,16 @@ This module depends on the `gui` module, which in turn depends on the recommended to conceptualize these modules first before using this one. # Example -For an example refer to the GUI example. +For an example, refer to the GUI example. # View props ## `header` -Single line of text that appears above the list +A single line of text that appears above the list. Type: `string` ## `items` -The list of options +The list of options. Type: `string[]` diff --git a/documentation/js/js_gui__text_box.md b/documentation/js/js_gui__text_box.md index bdad8d8b3..62828a37f 100644 --- a/documentation/js/js_gui__text_box.md +++ b/documentation/js/js_gui__text_box.md @@ -1,6 +1,5 @@ -# js_gui__text_box {#js_gui__text_box} +# Text box GUI view {#js_gui__text_box} -# Text box GUI view Displays a scrollable read-only text field. Sample screenshot of the view @@ -16,7 +15,7 @@ This module depends on the `gui` module, which in turn depends on the recommended to conceptualize these modules first before using this one. # Example -For an example refer to the `gui.js` example script. +For an example, refer to the `gui.js` example script. # View props ## `text` diff --git a/documentation/js/js_gui__text_input.md b/documentation/js/js_gui__text_input.md index 030579e2e..44752ab55 100644 --- a/documentation/js/js_gui__text_input.md +++ b/documentation/js/js_gui__text_input.md @@ -1,6 +1,5 @@ -# js_gui__text_input {#js_gui__text_input} +# Text input GUI view {#js_gui__text_input} -# Text input GUI view Displays a keyboard. Sample screenshot of the view @@ -16,29 +15,29 @@ This module depends on the `gui` module, which in turn depends on the recommended to conceptualize these modules first before using this one. # Example -For an example refer to the `gui.js` example script. +For an example, refer to the `gui.js` example script. # View props ## `minLength` -Smallest allowed text length +The shortest allowed text length. Type: `number` ## `maxLength` -Biggest allowed text length +The longest allowed text length. Type: `number` Default: `32` ## `header` -Single line of text that appears above the keyboard +A single line of text that appears above the keyboard. Type: `string` # View events ## `input` -Fires when the user selects the "save" button and the text matches the length +Fires when the user selects the "Save" button and the text matches the length constrained by `minLength` and `maxLength`. Item type: `string` diff --git a/documentation/js/js_gui__widget.md b/documentation/js/js_gui__widget.md index b871595b6..f0409e87b 100644 --- a/documentation/js/js_gui__widget.md +++ b/documentation/js/js_gui__widget.md @@ -1,7 +1,6 @@ -# js_gui__widget {#js_gui__widget} +# Widget GUI view {#js_gui__widget} -# Widget GUI view -Displays a combination of custom elements on one screen +Displays a combination of custom elements on one screen. Sample screenshot of the view @@ -16,7 +15,7 @@ This module depends on the `gui` module, which in turn depends on the recommended to conceptualize these modules first before using this one. # Example -For an example refer to the `gui.js` example script. +For an example, refer to the `gui.js` example script. # View props This view does not have any props. diff --git a/documentation/js/js_math.md b/documentation/js/js_math.md index ca16a9111..cab9ac0c7 100644 --- a/documentation/js/js_math.md +++ b/documentation/js/js_math.md @@ -1,6 +1,7 @@ -# js_math {#js_math} +# Math module {#js_math} + +The module contains mathematical methods and constants. Call the `require` function to load the module before first using its methods: -# Math module ```js let math = require("math"); ``` diff --git a/documentation/js/js_notification.md b/documentation/js/js_notification.md index 100da4414..543df3504 100644 --- a/documentation/js/js_notification.md +++ b/documentation/js/js_notification.md @@ -1,13 +1,12 @@ -# js_notification {#js_notification} +# Notification module {#js_notification} -# Notification module ```js let notify = require("notification"); ``` # Methods ## success -"Success" flipper notification message +"Success" flipper notification message. ### Examples: ```js @@ -15,7 +14,7 @@ notify.success(); ``` ## error -"Error" flipper notification message +"Error" flipper notification message. ### Examples: ```js @@ -23,7 +22,7 @@ notify.error(); ``` ## blink -Blink notification LED +Blink notification LED. ### Parameters - Blink color (blue/red/green/yellow/cyan/magenta) diff --git a/documentation/js/js_serial.md b/documentation/js/js_serial.md index 9d7938044..ffaee89bc 100644 --- a/documentation/js/js_serial.md +++ b/documentation/js/js_serial.md @@ -1,6 +1,5 @@ -# js_serial {#js_serial} +# Serial module {#js_serial} -# Serial module ```js let serial = require("serial"); ``` @@ -20,7 +19,7 @@ serial.setup("lpuart", 115200); ``` ## write -Write data to serial port +Write data to serial port. ### Parameters One or more arguments of the following types: @@ -41,7 +40,7 @@ Read a fixed number of characters from serial port. ### Parameters - Number of bytes to read -- (optional) Timeout value in ms +- *(optional)* Timeout value in ms ### Returns A sting of received characters or undefined if nothing was received before timeout. @@ -53,10 +52,10 @@ serial.read(10, 5000); // Read 10 bytes, with 5s timeout ``` ## readln -Read from serial port until line break character +Read from serial port until line break character. ### Parameters -(optional) Timeout value in ms +*(optional)* Timeout value in ms. ### Returns A sting of received characters or undefined if nothing was received before timeout. @@ -68,11 +67,11 @@ serial.readln(5000); // Read with 5s timeout ``` ## readBytes -Read from serial port until line break character +Read from serial port until line break character. ### Parameters - Number of bytes to read -- (optional) Timeout value in ms +- *(optional)* Timeout value in ms ### Returns ArrayBuffer with received data or undefined if nothing was received before timeout. @@ -86,13 +85,13 @@ serial.readBytes(1, 0); ``` ## expect -Search for a string pattern in received data stream +Search for a string pattern in received data stream. ### Parameters - Single argument or array of the following types: - A string - Array of numbers, each number is interpreted as a byte -- (optional) Timeout value in ms +- *(optional)* Timeout value in ms ### Returns Index of matched pattern in input patterns list, undefined if nothing was found. diff --git a/documentation/js/js_using_js_modules.md b/documentation/js/js_using_js_modules.md new file mode 100644 index 000000000..e7d69ef76 --- /dev/null +++ b/documentation/js/js_using_js_modules.md @@ -0,0 +1,42 @@ +# Using JavaScript modules {#js_using_js_modules} + +In the previous guides, we learned how to write a basic JavaScript app using [built-in functions](#js_builtin). However, the set of built-in functions is limited, so when developing your JS apps, you'll likely want to use external JS modules. These modules offer a wide range of functions (methods) for various tasks. + +For example: +* The `serial` module enables transmitting and receiving data via a serial interface +* The `badusb` module enables USB keyboard emulation and sending key press events via USB +* The `math` module provides mathematical functions + +JS modules are written in C/C++, making them fast and efficient. They come with Flipper Zero firmware and are stored on the microSD card in compiled form as **FAL (Flipper Application File)** files. + +> [!note] +> You can find the implementation of all supported JS modules in the [Flipper Zero firmware repository](https://github.com/flipperdevices/flipperzero-firmware/tree/dev/applications/system/js_app/modules). Also, check out the [docs for JS modules](#js_modules) for more details. + +## How to use JS modules in your app + +Before using any of the JS module methods, you **must** import the module using the `require()` function. This loads the module into RAM, allowing you to access its methods. + +To save RAM and improve performance, avoid loading modules you don't plan to use. Also, all loaded modules will be automatically unloaded from RAM after the app execution ends. + +To load a module, call the `require()` function with the module name in quotes. For example, to load the `notification` module, write this: + +\code{.js} +let notify = require("notification"); +\endcode + +Now you can call methods of the `notification` module using the `notify` variable to access them: + +\code{.js} +let notify = require("notification"); + +notify.success(); +print("success notification"); +\endcode + +## What's next? + +Congratulations, you've completed the **Getting Started** section of our JS docs. You've learned how to run and debug JS apps, and how to use JS modules. Now, we invite you to check out the [main JavaScript page](#js) where you'll find: + +* JavaScript app examples +* Documentation on JS modules +* Additional resources related to JavaScript on Flipper Zero diff --git a/documentation/js/js_your_first_js_app.md b/documentation/js/js_your_first_js_app.md new file mode 100644 index 000000000..ee003ea77 --- /dev/null +++ b/documentation/js/js_your_first_js_app.md @@ -0,0 +1,83 @@ +# Your first JavaScript app {#js_your_first_js_app} + +In this guide, we'll create a simple script that outputs ordinal numbers with a delay and learn how to run it on Flipper Zero in different ways. All you need is your Flipper Zero, a PC, and a USB cable. + +## Step 1. Create the script file + +Create a new text file `first_app.js`. Paste the code below into it and save the file: + +\code{.js} +print("start"); +delay(1000); +print("1"); +delay(500); +print("2"); +delay(500); +print("3"); +delay(500); +print("end"); +\endcode + +What the code does: +* Outputs the text **start**, waits 1 second +* Outputs the numbers **1**, **2** and **3**, with a 0.5-second pause after each number +* Outputs the text **end** + +The `print()` function is used to output text. The string to be output is in the brackets. This is a built-in function, so you don't need to include additional JS modules. You can use this function anywhere in your application. + +Another built-in function, `delay()`, implements delay. The delay time in milliseconds is given in the brackets. Since 1000 milliseconds equals 1 second, a 1-second delay is written as 1000, and a 0.5-second delay as 500. + +> [!note] +> Find the list of built-in functions in [Built-in functions](#js_builtin). + +## Step 2. Copy the file to Flipper Zero + +To copy the JavaScript file to Flipper Zero, follow these steps: +1. Connect your Flipper Zero to your PC via USB. +2. Open the **qFlipper** application. +3. Go to the **File manager** tab and open the path `SD Card/apps/Scripts/`. +4. Drag and drop the file into the qFlipper window. + +> [!note] +> To learn more about qFlipper, visit the [dedicated section in our user documentation](https://docs.flipper.net/qflipper). + +Your script is now ready to run on Flipper Zero. + +## Step 3. Run your script + +You can launch your app in two ways: + +* From the Flipper Zero menu +* Remotely from your PC using the CLI (command-line interface) + +Let's explore them both. + +### How to run a script from Flipper Zero's menu + +1. Go to **Apps → Scripts** in your Flipper Zero's menu. Here, you'll see a list of scripts located in the `SD Card/apps/Scripts/` folder. +2. Select the script you want to run. +3. Press the **OK** button to run the script. + +\image html js_first_app_on_fz.jpg width=500 + +The Flipper Zero screen will display the strings with the specified delay, as defined by the `print()` and `delay()` functions. + +### How to run script using CLI + +The command-line interface (CLI) is a text-based interface that lets you control your Flipper Zero from your computer, including running scripts. Running JavaScript apps via CLI is useful for debugging, as it lets you write and test code remotely, without switching between your PC and the device. + +To run the script via CLI: + +1. Connect your Flipper Zero to your PC via USB. +2. Access the CLI using one of the [recommended methods](https://docs.flipper.net/development/cli#HfXTy). +3. Enter the `js path` command, replacing `path` with the path to the script file on your Flipper Zero: + +\code{.sh} +js /ext/apps/Scripts/first_app.js +\endcode + +\image html js_first_app_on_cli.jpg width=700 + +As you can see, unlike running JavaScript apps from the Flipper Zero UI, all output from the `print()` function is sent to the CLI, not the device screen. + +**Next step:** [Developing apps using JavaScript SDK](#js_developing_apps_using_js_sdk) From a629118aaa7ee78220ccde21107dedc3f2db9a52 Mon Sep 17 00:00:00 2001 From: Dmitry422 Date: Wed, 19 Mar 2025 00:46:00 +0700 Subject: [PATCH 135/268] Rainbow mode in progress --- .../services/rgb_backlight/rgb_backlight.c | 87 ++- .../services/rgb_backlight/rgb_backlight.h | 8 +- .../rgb_backlight/rgb_backlight_settings.c | 6 +- .../rgb_backlight/rgb_backlight_settings.h | 1 + .../notification_settings_app.c | 495 +++++++++--------- 5 files changed, 348 insertions(+), 249 deletions(-) diff --git a/applications/services/rgb_backlight/rgb_backlight.c b/applications/services/rgb_backlight/rgb_backlight.c index 323cc5d48..6f9940ad0 100644 --- a/applications/services/rgb_backlight/rgb_backlight.c +++ b/applications/services/rgb_backlight/rgb_backlight.c @@ -23,6 +23,7 @@ #include #include #include "rgb_backlight.h" +#include #define COLOR_COUNT (sizeof(colors) / sizeof(RGBBacklightColor)) @@ -87,14 +88,22 @@ void rgb_backlight_set_led_static_color(uint8_t led, uint8_t index) { // furi_record_close(RECORD_RGB_BACKLIGHT); } -// use RECORD for acces to rgb service instance and update current colors by custom -// void rgb_backlight_set_custom_color(uint8_t red, uint8_t green, uint8_t blue) { -// RGBBacklightApp* app = furi_record_open(RECORD_RGB_BACKLIGHT); -// app->current_red = red; -// app->current_green = green; -// app->current_blue = blue; -// furi_record_close(RECORD_RGB_BACKLIGHT); -// } +// use RECORD for acces to rgb service instance and update current colors by custom value +void rgb_backlight_set_led_custom_color(uint8_t led, uint8_t red, uint8_t green, uint8_t blue) { + // RGBBacklightApp* app = furi_record_open(RECORD_RGB_BACKLIGHT); + // float brightness = app->settings->brightness; + + if(led < SK6805_get_led_count()) { + + current_led[led].red = red; + current_led[led].green = green; + current_led[led].blue = blue; + + SK6805_set_led_color(led, red, green, blue); + } + + // furi_record_close(RECORD_RGB_BACKLIGHT); +} // use RECORD for acces to rgb service instance, use current_* colors and update backlight void rgb_backlight_update(float brightness) { @@ -132,19 +141,77 @@ void rainbow_timer_starter(RGBBacklightApp* app) { } } } + +// HSV to RGB based on +// https://www.radiokot.ru/forum/viewtopic.php?p=3000181&ysclid=m88wvoz34w244644702 +// https://radiolaba.ru/microcotrollers/tsvetnaya-lampa.html#comment-1790 +// https://alexgyver.ru/lessons/arduino-rgb/?ysclid=m88voflppa24464916 +void hsv_to_rgb(uint8_t red, uint8_t green, uint8_t blue, uint16_t hue ,uint8_t sat ,uint8_t val) { + float r = 1.0f; + float g = 1.0f; + float b = 1.0f; + + float H = hue / 255.0f; + float S = sat / 255.0f; + float V = val / 255.0f; + + uint8_t i = trunc(H * 6); + float f = H * 6 - i; + float p = V * (1 - S); + float q = V * (1 - f * S); + float t = V * (1 - (1 - f) * S); + + switch(i) { + case 0: + r = V, g = t, b = p; + break; + case 1: + r = q, g = V, b = p; + break; + case 2: + r = p, g = V, b = t; + break; + case 3: + r = p, g = q, b = V; + break; + case 4: + r = t, g = p, b = V; + break; + case 5: + r = V, g = p, b = q; + break; + } + red = r * 255; + green = g * 255; + blue = b * 255; +} + + static void rainbow_timer_callback(void* context) { furi_assert(context); RGBBacklightApp* app = context; + uint8_t r = 0; + uint8_t g = 0; + uint8_t b = 0; - if (app->settings->rgb_backlight_installed) { + if(app->settings->rgb_backlight_installed) { switch(app->settings->rainbow_mode) { case 1: + for(uint8_t i = 0; i < SK6805_get_led_count(); i++) { + hsv_to_rgb(r,g,b,app->rainbow_hue, app->settings->rainbow_saturation,app->settings->brightness*255); + FURI_LOG_D( + TAG, "rgb %d,%d,%d", r, g, b); + //rgb_backlight_set_led_custom_color (i,*r,*g,*b); + //SK6805_update(); + } + break; case 2: break; default: break; } + app->rainbow_hue++; rgb_backlight_update(app->settings->brightness); } @@ -168,6 +235,8 @@ int32_t rgb_backlight_srv(void* p) { app->settings = malloc(sizeof(RGBBacklightSettings)); rgb_backlight_settings_load(app->settings); + app->rainbow_hue = 1; + furi_record_create(RECORD_RGB_BACKLIGHT, app); //if rgb_backlight_installed then start rainbow or set leds colors from saved settings (default index = 0) diff --git a/applications/services/rgb_backlight/rgb_backlight.h b/applications/services/rgb_backlight/rgb_backlight.h index 0661886c5..e8b78f461 100644 --- a/applications/services/rgb_backlight/rgb_backlight.h +++ b/applications/services/rgb_backlight/rgb_backlight.h @@ -28,10 +28,10 @@ extern "C" { typedef struct { FuriTimer* rainbow_timer; - - // int16_t current_red; - // int16_t current_green; - // int16_t current_blue; + uint8_t rainbow_hue; + uint8_t rainbow_red; + uint8_t rainbow_green; + uint8_t rainbow_blue; RGBBacklightSettings* settings; diff --git a/applications/services/rgb_backlight/rgb_backlight_settings.c b/applications/services/rgb_backlight/rgb_backlight_settings.c index 4ca7140fd..0b0388cae 100644 --- a/applications/services/rgb_backlight/rgb_backlight_settings.c +++ b/applications/services/rgb_backlight/rgb_backlight_settings.c @@ -18,16 +18,17 @@ typedef struct { uint8_t version; uint8_t rgb_backlight_installed; float brightness; - + // static gradient mode settings uint8_t led_2_color_index; uint8_t led_1_color_index; uint8_t led_0_color_index; - + // rainbow mode setings uint32_t rainbow_mode; uint32_t rainbow_speed_ms; uint16_t rainbow_step; + uint8_t rainbow_saturation; } RGBBacklightSettingsPrevious; void rgb_backlight_settings_load(RGBBacklightSettings* settings) { @@ -82,6 +83,7 @@ void rgb_backlight_settings_load(RGBBacklightSettings* settings) { settings->brightness = 1.0f; settings->rainbow_speed_ms = 100; settings->rainbow_step = 1; + settings->rainbow_saturation = 255; rgb_backlight_settings_save(settings); } } diff --git a/applications/services/rgb_backlight/rgb_backlight_settings.h b/applications/services/rgb_backlight/rgb_backlight_settings.h index fe504879f..7c6e08c95 100644 --- a/applications/services/rgb_backlight/rgb_backlight_settings.h +++ b/applications/services/rgb_backlight/rgb_backlight_settings.h @@ -18,6 +18,7 @@ typedef struct { uint32_t rainbow_mode; uint32_t rainbow_speed_ms; uint16_t rainbow_step; + uint8_t rainbow_saturation; } RGBBacklightSettings; diff --git a/applications/settings/notification_settings/notification_settings_app.c b/applications/settings/notification_settings/notification_settings_app.c index 75f462ce4..7e8536ee7 100644 --- a/applications/settings/notification_settings/notification_settings_app.c +++ b/applications/settings/notification_settings/notification_settings_app.c @@ -262,7 +262,7 @@ static void rgb_backlight_installed_changed(VariableItem* item) { if(furi_hal_rtc_is_flag_set(FuriHalRtcFlagDebug)) { slide = 1; } - for(int i = slide; i < (slide + 6); i++) { + for(int i = slide; i < (slide + 7); i++) { VariableItem* t_item = variable_item_list_get(app->variable_item_list_rgb, i); if(index == 0) { variable_item_set_locked(t_item, true, "RGB\nOFF!"); @@ -362,267 +362,294 @@ static void rgb_backlight_rainbow_step_changed(VariableItem* item) { rgb_backlight_settings_save(app->notification->rgb_srv->settings); } +static void rgb_backlight_rainbow_saturation_changed (VariableItem* item) { + NotificationAppSettings* app = variable_item_get_context(item); -// open rgb_settings_view if user press OK on first (index=0) menu string and (debug mode or rgb_backlight_installed is true) -void variable_item_list_enter_callback(void* context, uint32_t index) { - UNUSED(context); - NotificationAppSettings* app = context; + //saturation must be 1..255, so (0..254)+1 + uint8_t index = variable_item_get_current_value_index(item)+1; + char valtext[4] = {}; + snprintf(valtext, sizeof(valtext), "%d", index); + variable_item_set_current_value_text(item, valtext); + app->notification->rgb_srv->settings->rainbow_saturation = index; + rgb_backlight_settings_save(app->notification->rgb_srv->settings); +} + // open rgb_settings_view if user press OK on first (index=0) menu string and (debug mode or rgb_backlight_installed is true) + void variable_item_list_enter_callback(void* context, uint32_t index) { + UNUSED(context); + NotificationAppSettings* app = context; - if(((app->notification->rgb_srv->settings->rgb_backlight_installed) || - (furi_hal_rtc_is_flag_set(FuriHalRtcFlagDebug))) && - (index == 0)) { - view_dispatcher_switch_to_view(app->view_dispatcher, RGBViewId); + if(((app->notification->rgb_srv->settings->rgb_backlight_installed) || + (furi_hal_rtc_is_flag_set(FuriHalRtcFlagDebug))) && + (index == 0)) { + view_dispatcher_switch_to_view(app->view_dispatcher, RGBViewId); + } } -} -// switch to main view on exit from rgb_settings_view -static uint32_t notification_app_rgb_settings_exit(void* context) { - UNUSED(context); - return MainViewId; -} -//--- RGB BACKLIGHT END --- - -static uint32_t notification_app_settings_exit(void* context) { - UNUSED(context); - return VIEW_NONE; -} - -static NotificationAppSettings* alloc_settings(void) { - NotificationAppSettings* app = malloc(sizeof(NotificationAppSettings)); - app->notification = furi_record_open(RECORD_NOTIFICATION); - app->gui = furi_record_open(RECORD_GUI); - - app->variable_item_list = variable_item_list_alloc(); - View* view = variable_item_list_get_view(app->variable_item_list); - - VariableItem* item; - uint8_t value_index; - - //set callback for exit from main view - view_set_previous_callback(view, notification_app_settings_exit); - - //--- RGB BACKLIGHT --- - // set callback for OK pressed in notification settings menu - variable_item_list_set_enter_callback( - app->variable_item_list, variable_item_list_enter_callback, app); - - //Show RGB settings only when debug_mode or rgb_backlight_installed is active - if((app->notification->rgb_srv->settings->rgb_backlight_installed) || - (furi_hal_rtc_is_flag_set(FuriHalRtcFlagDebug))) { - item = variable_item_list_add(app->variable_item_list, "RGB settings", 0, NULL, app); + // switch to main view on exit from rgb_settings_view + static uint32_t notification_app_rgb_settings_exit(void* context) { + UNUSED(context); + return MainViewId; } //--- RGB BACKLIGHT END --- - item = variable_item_list_add( - app->variable_item_list, "LCD Contrast", CONTRAST_COUNT, contrast_changed, app); - value_index = - value_index_int32(app->notification->settings.contrast, contrast_value, CONTRAST_COUNT); - variable_item_set_current_value_index(item, value_index); - variable_item_set_current_value_text(item, contrast_text[value_index]); + static uint32_t notification_app_settings_exit(void* context) { + UNUSED(context); + return VIEW_NONE; + } - item = variable_item_list_add( - app->variable_item_list, "LCD Backlight", BACKLIGHT_COUNT, backlight_changed, app); - value_index = value_index_float( - app->notification->settings.display_brightness, backlight_value, BACKLIGHT_COUNT); - variable_item_set_current_value_index(item, value_index); - variable_item_set_current_value_text(item, backlight_text[value_index]); + static NotificationAppSettings* alloc_settings(void) { + NotificationAppSettings* app = malloc(sizeof(NotificationAppSettings)); + app->notification = furi_record_open(RECORD_NOTIFICATION); + app->gui = furi_record_open(RECORD_GUI); - item = variable_item_list_add( - app->variable_item_list, "Backlight Time", DELAY_COUNT, screen_changed, app); - value_index = value_index_uint32( - app->notification->settings.display_off_delay_ms, delay_value, DELAY_COUNT); - variable_item_set_current_value_index(item, value_index); - variable_item_set_current_value_text(item, delay_text[value_index]); + app->variable_item_list = variable_item_list_alloc(); + View* view = variable_item_list_get_view(app->variable_item_list); - item = variable_item_list_add( - app->variable_item_list, "LED Brightness", BACKLIGHT_COUNT, led_changed, app); - value_index = value_index_float( - app->notification->settings.led_brightness, backlight_value, BACKLIGHT_COUNT); - variable_item_set_current_value_index(item, value_index); - variable_item_set_current_value_text(item, backlight_text[value_index]); + VariableItem* item; + uint8_t value_index; + + //set callback for exit from main view + view_set_previous_callback(view, notification_app_settings_exit); + + //--- RGB BACKLIGHT --- + // set callback for OK pressed in notification settings menu + variable_item_list_set_enter_callback( + app->variable_item_list, variable_item_list_enter_callback, app); + + //Show RGB settings only when debug_mode or rgb_backlight_installed is active + if((app->notification->rgb_srv->settings->rgb_backlight_installed) || + (furi_hal_rtc_is_flag_set(FuriHalRtcFlagDebug))) { + item = variable_item_list_add(app->variable_item_list, "RGB settings", 0, NULL, app); + } + //--- RGB BACKLIGHT END --- - if(furi_hal_rtc_is_flag_set(FuriHalRtcFlagStealthMode)) { - item = variable_item_list_add(app->variable_item_list, "Volume", 1, NULL, app); - value_index = 0; - variable_item_set_current_value_index(item, value_index); - variable_item_set_current_value_text(item, "Stealth"); - } else { item = variable_item_list_add( - app->variable_item_list, "Volume", VOLUME_COUNT, volume_changed, app); + app->variable_item_list, "LCD Contrast", CONTRAST_COUNT, contrast_changed, app); + value_index = value_index_int32( + app->notification->settings.contrast, contrast_value, CONTRAST_COUNT); + variable_item_set_current_value_index(item, value_index); + variable_item_set_current_value_text(item, contrast_text[value_index]); + + item = variable_item_list_add( + app->variable_item_list, "LCD Backlight", BACKLIGHT_COUNT, backlight_changed, app); value_index = value_index_float( - app->notification->settings.speaker_volume, volume_value, VOLUME_COUNT); + app->notification->settings.display_brightness, backlight_value, BACKLIGHT_COUNT); variable_item_set_current_value_index(item, value_index); - variable_item_set_current_value_text(item, volume_text[value_index]); - } + variable_item_set_current_value_text(item, backlight_text[value_index]); - if(furi_hal_rtc_is_flag_set(FuriHalRtcFlagStealthMode)) { - item = variable_item_list_add(app->variable_item_list, "Vibro", 1, NULL, app); - value_index = 0; - variable_item_set_current_value_index(item, value_index); - variable_item_set_current_value_text(item, "Stealth"); - } else { item = variable_item_list_add( - app->variable_item_list, "Vibro", VIBRO_COUNT, vibro_changed, app); - value_index = - value_index_bool(app->notification->settings.vibro_on, vibro_value, VIBRO_COUNT); + app->variable_item_list, "Backlight Time", DELAY_COUNT, screen_changed, app); + value_index = value_index_uint32( + app->notification->settings.display_off_delay_ms, delay_value, DELAY_COUNT); variable_item_set_current_value_index(item, value_index); - variable_item_set_current_value_text(item, vibro_text[value_index]); - } + variable_item_set_current_value_text(item, delay_text[value_index]); - //--- RGB BACKLIGHT --- + item = variable_item_list_add( + app->variable_item_list, "LED Brightness", BACKLIGHT_COUNT, led_changed, app); + value_index = value_index_float( + app->notification->settings.led_brightness, backlight_value, BACKLIGHT_COUNT); + variable_item_set_current_value_index(item, value_index); + variable_item_set_current_value_text(item, backlight_text[value_index]); - app->variable_item_list_rgb = variable_item_list_alloc(); - View* view_rgb = variable_item_list_get_view(app->variable_item_list_rgb); + if(furi_hal_rtc_is_flag_set(FuriHalRtcFlagStealthMode)) { + item = variable_item_list_add(app->variable_item_list, "Volume", 1, NULL, app); + value_index = 0; + variable_item_set_current_value_index(item, value_index); + variable_item_set_current_value_text(item, "Stealth"); + } else { + item = variable_item_list_add( + app->variable_item_list, "Volume", VOLUME_COUNT, volume_changed, app); + value_index = value_index_float( + app->notification->settings.speaker_volume, volume_value, VOLUME_COUNT); + variable_item_set_current_value_index(item, value_index); + variable_item_set_current_value_text(item, volume_text[value_index]); + } - // set callback for exit from rgb_settings_menu - view_set_previous_callback(view_rgb, notification_app_rgb_settings_exit); + if(furi_hal_rtc_is_flag_set(FuriHalRtcFlagStealthMode)) { + item = variable_item_list_add(app->variable_item_list, "Vibro", 1, NULL, app); + value_index = 0; + variable_item_set_current_value_index(item, value_index); + variable_item_set_current_value_text(item, "Stealth"); + } else { + item = variable_item_list_add( + app->variable_item_list, "Vibro", VIBRO_COUNT, vibro_changed, app); + value_index = + value_index_bool(app->notification->settings.vibro_on, vibro_value, VIBRO_COUNT); + variable_item_set_current_value_index(item, value_index); + variable_item_set_current_value_text(item, vibro_text[value_index]); + } - // // Show rgb_backlight_Installed_Swith only in Debug mode - if(furi_hal_rtc_is_flag_set(FuriHalRtcFlagDebug)) { + //--- RGB BACKLIGHT --- + + app->variable_item_list_rgb = variable_item_list_alloc(); + View* view_rgb = variable_item_list_get_view(app->variable_item_list_rgb); + + // set callback for exit from rgb_settings_menu + view_set_previous_callback(view_rgb, notification_app_rgb_settings_exit); + + // // Show rgb_backlight_Installed_Swith only in Debug mode + if(furi_hal_rtc_is_flag_set(FuriHalRtcFlagDebug)) { + item = variable_item_list_add( + app->variable_item_list_rgb, + "RGB backlight installed", + RGB_BACKLIGHT_INSTALLED_COUNT, + rgb_backlight_installed_changed, + app); + value_index = value_index_bool( + app->notification->rgb_srv->settings->rgb_backlight_installed, + rgb_backlight_installed_value, + RGB_BACKLIGHT_INSTALLED_COUNT); + variable_item_set_current_value_index(item, value_index); + variable_item_set_current_value_text(item, rgb_backlight_installed_text[value_index]); + } + + // led_1 color item = variable_item_list_add( app->variable_item_list_rgb, - "RGB backlight installed", - RGB_BACKLIGHT_INSTALLED_COUNT, - rgb_backlight_installed_changed, + "LED 1 Color", + rgb_backlight_get_color_count(), + led_2_color_changed, app); - value_index = value_index_bool( - app->notification->rgb_srv->settings->rgb_backlight_installed, - rgb_backlight_installed_value, - RGB_BACKLIGHT_INSTALLED_COUNT); + value_index = app->notification->rgb_srv->settings->led_2_color_index; variable_item_set_current_value_index(item, value_index); - variable_item_set_current_value_text(item, rgb_backlight_installed_text[value_index]); + variable_item_set_current_value_text(item, rgb_backlight_get_color_text(value_index)); + variable_item_set_locked( + item, + (app->notification->rgb_srv->settings->rgb_backlight_installed == 0), + "RGB MOD \nOFF!"); + + // led_2 color + item = variable_item_list_add( + app->variable_item_list_rgb, + "LED 2 Color", + rgb_backlight_get_color_count(), + led_1_color_changed, + app); + value_index = app->notification->rgb_srv->settings->led_1_color_index; + variable_item_set_current_value_index(item, value_index); + variable_item_set_current_value_text(item, rgb_backlight_get_color_text(value_index)); + variable_item_set_locked( + item, + (app->notification->rgb_srv->settings->rgb_backlight_installed == 0), + "RGB MOD \nOFF!"); + + // led 3 color + item = variable_item_list_add( + app->variable_item_list_rgb, + "LED 3 Color", + rgb_backlight_get_color_count(), + led_0_color_changed, + app); + value_index = app->notification->rgb_srv->settings->led_0_color_index; + variable_item_set_current_value_index(item, value_index); + variable_item_set_current_value_text(item, rgb_backlight_get_color_text(value_index)); + variable_item_set_locked( + item, + (app->notification->rgb_srv->settings->rgb_backlight_installed == 0), + "RGB MOD \nOFF!"); + + // Rainbow mode + item = variable_item_list_add( + app->variable_item_list_rgb, + "Rainbow mode", + RGB_BACKLIGHT_RAINBOW_MODE_COUNT, + rgb_backlight_rainbow_changed, + app); + value_index = value_index_uint32( + app->notification->rgb_srv->settings->rainbow_mode, + rgb_backlight_rainbow_mode_value, + RGB_BACKLIGHT_RAINBOW_MODE_COUNT); + variable_item_set_current_value_index(item, value_index); + variable_item_set_current_value_text(item, rgb_backlight_rainbow_mode_text[value_index]); + variable_item_set_locked( + item, + (app->notification->rgb_srv->settings->rgb_backlight_installed == 0), + "RGB MOD \nOFF!"); + + item = variable_item_list_add( + app->variable_item_list_rgb, + "Rainbow speed", + RGB_BACKLIGHT_RAINBOW_SPEED_COUNT, + rgb_backlight_rainbow_speed_changed, + app); + value_index = value_index_uint32( + app->notification->rgb_srv->settings->rainbow_speed_ms, + rgb_backlight_rainbow_speed_value, + RGB_BACKLIGHT_RAINBOW_SPEED_COUNT); + variable_item_set_current_value_index(item, value_index); + variable_item_set_current_value_text(item, rgb_backlight_rainbow_speed_text[value_index]); + variable_item_set_locked( + item, + (app->notification->rgb_srv->settings->rgb_backlight_installed == 0), + "RGB MOD \nOFF!"); + + item = variable_item_list_add( + app->variable_item_list_rgb, + "Rainbow step", + RGB_BACKLIGHT_RAINBOW_STEP_COUNT, + rgb_backlight_rainbow_step_changed, + app); + value_index = value_index_uint32( + app->notification->rgb_srv->settings->rainbow_step, + rgb_backlight_rainbow_step_value, + RGB_BACKLIGHT_RAINBOW_SPEED_COUNT); + variable_item_set_current_value_index(item, value_index); + variable_item_set_current_value_text(item, rgb_backlight_rainbow_step_text[value_index]); + variable_item_set_locked( + item, + (app->notification->rgb_srv->settings->rgb_backlight_installed == 0), + "RGB MOD \nOFF!"); + + item = variable_item_list_add( + app->variable_item_list_rgb, + "Saturation", + 255, + rgb_backlight_rainbow_saturation_changed, + app); + value_index = app->notification->rgb_srv->settings->rainbow_saturation; + variable_item_set_current_value_index(item, value_index); + char valtext[4] = {}; + snprintf(valtext, sizeof(valtext), "%d", value_index); + variable_item_set_current_value_text(item, valtext); + variable_item_set_locked( + item, + (app->notification->rgb_srv->settings->rgb_backlight_installed == 0), + "RGB MOD \nOFF!"); + + //--- RGB BACKLIGHT END --- + + app->view_dispatcher = view_dispatcher_alloc(); + view_dispatcher_attach_to_gui( + app->view_dispatcher, app->gui, ViewDispatcherTypeFullscreen); + view_dispatcher_add_view(app->view_dispatcher, MainViewId, view); + view_dispatcher_add_view(app->view_dispatcher, RGBViewId, view_rgb); + view_dispatcher_switch_to_view(app->view_dispatcher, MainViewId); + return app; } - - // led_1 color - item = variable_item_list_add( - app->variable_item_list_rgb, - "LED 1 Color", - rgb_backlight_get_color_count(), - led_2_color_changed, - app); - value_index = app->notification->rgb_srv->settings->led_2_color_index; - variable_item_set_current_value_index(item, value_index); - variable_item_set_current_value_text(item, rgb_backlight_get_color_text(value_index)); - variable_item_set_locked( - item, - (app->notification->rgb_srv->settings->rgb_backlight_installed == 0), - "RGB MOD \nOFF!"); - // led_2 color - item = variable_item_list_add( - app->variable_item_list_rgb, - "LED 2 Color", - rgb_backlight_get_color_count(), - led_1_color_changed, - app); - value_index = app->notification->rgb_srv->settings->led_1_color_index; - variable_item_set_current_value_index(item, value_index); - variable_item_set_current_value_text(item, rgb_backlight_get_color_text(value_index)); - variable_item_set_locked( - item, - (app->notification->rgb_srv->settings->rgb_backlight_installed == 0), - "RGB MOD \nOFF!"); - - // led 3 color - item = variable_item_list_add( - app->variable_item_list_rgb, - "LED 3 Color", - rgb_backlight_get_color_count(), - led_0_color_changed, - app); - value_index = app->notification->rgb_srv->settings->led_0_color_index; - variable_item_set_current_value_index(item, value_index); - variable_item_set_current_value_text(item, rgb_backlight_get_color_text(value_index)); - variable_item_set_locked( - item, - (app->notification->rgb_srv->settings->rgb_backlight_installed == 0), - "RGB MOD \nOFF!"); - - // Rainbow mode - item = variable_item_list_add( - app->variable_item_list_rgb, - "Rainbow mode", - RGB_BACKLIGHT_RAINBOW_MODE_COUNT, - rgb_backlight_rainbow_changed, - app); - value_index = value_index_uint32( - app->notification->rgb_srv->settings->rainbow_mode, - rgb_backlight_rainbow_mode_value, - RGB_BACKLIGHT_RAINBOW_MODE_COUNT); - variable_item_set_current_value_index(item, value_index); - variable_item_set_current_value_text(item, rgb_backlight_rainbow_mode_text[value_index]); - variable_item_set_locked( - item, - (app->notification->rgb_srv->settings->rgb_backlight_installed == 0), - "RGB MOD \nOFF!"); + static void free_settings(NotificationAppSettings * app) { + view_dispatcher_remove_view(app->view_dispatcher, MainViewId); + view_dispatcher_remove_view(app->view_dispatcher, RGBViewId); + variable_item_list_free(app->variable_item_list); + variable_item_list_free(app->variable_item_list_rgb); + view_dispatcher_free(app->view_dispatcher); - item = variable_item_list_add( - app->variable_item_list_rgb, - "Rainbow speed", - RGB_BACKLIGHT_RAINBOW_SPEED_COUNT, - rgb_backlight_rainbow_speed_changed, - app); - value_index = value_index_uint32( - app->notification->rgb_srv->settings->rainbow_speed_ms, - rgb_backlight_rainbow_speed_value, - RGB_BACKLIGHT_RAINBOW_SPEED_COUNT); - variable_item_set_current_value_index(item, value_index); - variable_item_set_current_value_text(item, rgb_backlight_rainbow_speed_text[value_index]); - variable_item_set_locked( - item, - (app->notification->rgb_srv->settings->rgb_backlight_installed == 0), - "RGB MOD \nOFF!"); + furi_record_close(RECORD_GUI); + furi_record_close(RECORD_NOTIFICATION); + free(app); + } - item = variable_item_list_add( - app->variable_item_list_rgb, - "Rainbow step", - RGB_BACKLIGHT_RAINBOW_STEP_COUNT, - rgb_backlight_rainbow_step_changed, - app); - value_index = value_index_uint32( - app->notification->rgb_srv->settings->rainbow_step, - rgb_backlight_rainbow_step_value, - RGB_BACKLIGHT_RAINBOW_SPEED_COUNT); - variable_item_set_current_value_index(item, value_index); - variable_item_set_current_value_text(item, rgb_backlight_rainbow_step_text[value_index]); - variable_item_set_locked( - item, - (app->notification->rgb_srv->settings->rgb_backlight_installed == 0), - "RGB MOD \nOFF!"); + int32_t notification_settings_app(void* p) { + UNUSED(p); + NotificationAppSettings* app = alloc_settings(); + view_dispatcher_run(app->view_dispatcher); + notification_message_save_settings(app->notification); - //--- RGB BACKLIGHT END --- + // Automaticaly switch_off debug_mode when user exit from settings with enabled rgb_backlight_installed + // if(app->notification->settings.rgb_backlight_installed) { + // furi_hal_rtc_reset_flag(FuriHalRtcFlagDebug); + // } - app->view_dispatcher = view_dispatcher_alloc(); - view_dispatcher_attach_to_gui(app->view_dispatcher, app->gui, ViewDispatcherTypeFullscreen); - view_dispatcher_add_view(app->view_dispatcher, MainViewId, view); - view_dispatcher_add_view(app->view_dispatcher, RGBViewId, view_rgb); - view_dispatcher_switch_to_view(app->view_dispatcher, MainViewId); - return app; -} - -static void free_settings(NotificationAppSettings* app) { - view_dispatcher_remove_view(app->view_dispatcher, MainViewId); - view_dispatcher_remove_view(app->view_dispatcher, RGBViewId); - variable_item_list_free(app->variable_item_list); - variable_item_list_free(app->variable_item_list_rgb); - view_dispatcher_free(app->view_dispatcher); - - furi_record_close(RECORD_GUI); - furi_record_close(RECORD_NOTIFICATION); - free(app); -} - -int32_t notification_settings_app(void* p) { - UNUSED(p); - NotificationAppSettings* app = alloc_settings(); - view_dispatcher_run(app->view_dispatcher); - notification_message_save_settings(app->notification); - - // Automaticaly switch_off debug_mode when user exit from settings with enabled rgb_backlight_installed - // if(app->notification->settings.rgb_backlight_installed) { - // furi_hal_rtc_reset_flag(FuriHalRtcFlagDebug); - // } - - free_settings(app); - return 0; -} + free_settings(app); + return 0; + } From 5eb38b786b72d28ef6d5a3e68e1c979f8b06207e Mon Sep 17 00:00:00 2001 From: Dmitry422 Date: Wed, 19 Mar 2025 18:53:02 +0700 Subject: [PATCH 136/268] RGB BACKLIGHT refactoring finished. - rgb_backlight by @Quen0n - rgb_backlight_settings and effects (like rainbow) idea by @Willy-JL For access to rgb backlight settings enable Debug mode and go to Settings-LCD and Notification-RGB backlight --- .../services/rgb_backlight/rgb_backlight.c | 219 +++--- .../services/rgb_backlight/rgb_backlight.h | 3 +- .../rgb_backlight/rgb_backlight_settings.c | 2 + .../rgb_backlight/rgb_backlight_settings.h | 7 +- .../notification_settings_app.c | 661 ++++++++++-------- 5 files changed, 475 insertions(+), 417 deletions(-) diff --git a/applications/services/rgb_backlight/rgb_backlight.c b/applications/services/rgb_backlight/rgb_backlight.c index 6f9940ad0..8d05e68de 100644 --- a/applications/services/rgb_backlight/rgb_backlight.c +++ b/applications/services/rgb_backlight/rgb_backlight.c @@ -37,10 +37,10 @@ typedef struct { } RGBBacklightColor; //use one type RGBBacklightColor for current_leds_settings and for static colors definition -static RGBBacklightColor current_led [] = { - {"LED0",255,60,0}, - {"LED1",255,60,0}, - {"LED2",255,60,0}, +static RGBBacklightColor current_led[] = { + {"LED0", 255, 60, 0}, + {"LED1", 255, 60, 0}, + {"LED2", 255, 60, 0}, }; static const RGBBacklightColor colors[] = { @@ -70,45 +70,83 @@ const char* rgb_backlight_get_color_text(uint8_t index) { // use RECORD for acces to rgb service instance and update current colors by static void rgb_backlight_set_led_static_color(uint8_t led, uint8_t index) { - // RGBBacklightApp* app = furi_record_open(RECORD_RGB_BACKLIGHT); - // float brightness = app->settings->brightness; - if(led < SK6805_get_led_count()) { uint8_t r = colors[index].red; uint8_t g = colors[index].green; uint8_t b = colors[index].blue; - - current_led[led].red = r; - current_led[led].green =g; + + current_led[led].red = r; + current_led[led].green = g; current_led[led].blue = b; SK6805_set_led_color(led, r, g, b); } - - // furi_record_close(RECORD_RGB_BACKLIGHT); } -// use RECORD for acces to rgb service instance and update current colors by custom value -void rgb_backlight_set_led_custom_color(uint8_t led, uint8_t red, uint8_t green, uint8_t blue) { - // RGBBacklightApp* app = furi_record_open(RECORD_RGB_BACKLIGHT); - // float brightness = app->settings->brightness; +// --- NOT USED IN CURRENT RELEASE, FOR FUTURE USAGE--- +// Update current colors by custom rgb value +// void rgb_backlight_set_led_custom_color(uint8_t led, uint8_t red, uint8_t green, uint8_t blue) { +// if(led < SK6805_get_led_count()) { +// current_led[led].red = red; +// current_led[led].green = green; +// current_led[led].blue = blue; +// SK6805_set_led_color(led, red, green, blue); +// } +// } +// --- NOT USED IN CURRENT RELEASE, FOR FUTURE USAGE--- - if(led < SK6805_get_led_count()) { +// HSV to RGB based on +// https://www.radiokot.ru/forum/viewtopic.php?p=3000181&ysclid=m88wvoz34w244644702 +// https://radiolaba.ru/microcotrollers/tsvetnaya-lampa.html#comment-1790 +// https://alexgyver.ru/lessons/arduino-rgb/?ysclid=m88voflppa24464916 +// led number (0-2), hue (0..255), sat (0..255), val (0...1) +void rgb_backlight_set_led_custom_hsv_color(uint8_t led, uint16_t hue, uint8_t sat, float V) { + //init value + float r = 1.0f; + float g = 1.0f; + float b = 1.0f; - current_led[led].red = red; - current_led[led].green = green; - current_led[led].blue = blue; + //from (0..255) to (0..1) + float H = hue / 255.0f; + float S = sat / 255.0f; - SK6805_set_led_color(led, red, green, blue); + uint8_t i = trunc(H * 6); + float f = H * 6 - i; + float p = V * (1 - S); + float q = V * (1 - f * S); + float t = V * (1 - (1 - f) * S); + + switch(i) { + case 0: + r = V, g = t, b = p; + break; + case 1: + r = q, g = V, b = p; + break; + case 2: + r = p, g = V, b = t; + break; + case 3: + r = p, g = q, b = V; + break; + case 4: + r = t, g = p, b = V; + break; + case 5: + r = V, g = p, b = q; + break; } - // furi_record_close(RECORD_RGB_BACKLIGHT); + //from (0..1) to (0..255) + current_led[led].red = r * 255; + current_led[led].green = g * 255; + current_led[led].blue = b * 255; } -// use RECORD for acces to rgb service instance, use current_* colors and update backlight +// use RECORD for acces to rgb service instance, set current_* colors to led and update backlight void rgb_backlight_update(float brightness) { RGBBacklightApp* app = furi_record_open(RECORD_RGB_BACKLIGHT); - + if(app->settings->rgb_backlight_installed) { for(uint8_t i = 0; i < SK6805_get_led_count(); i++) { uint8_t r = current_led[i].red * (brightness * 1.0f); @@ -142,83 +180,56 @@ void rainbow_timer_starter(RGBBacklightApp* app) { } } -// HSV to RGB based on -// https://www.radiokot.ru/forum/viewtopic.php?p=3000181&ysclid=m88wvoz34w244644702 -// https://radiolaba.ru/microcotrollers/tsvetnaya-lampa.html#comment-1790 -// https://alexgyver.ru/lessons/arduino-rgb/?ysclid=m88voflppa24464916 -void hsv_to_rgb(uint8_t red, uint8_t green, uint8_t blue, uint16_t hue ,uint8_t sat ,uint8_t val) { - float r = 1.0f; - float g = 1.0f; - float b = 1.0f; - - float H = hue / 255.0f; - float S = sat / 255.0f; - float V = val / 255.0f; - - uint8_t i = trunc(H * 6); - float f = H * 6 - i; - float p = V * (1 - S); - float q = V * (1 - f * S); - float t = V * (1 - (1 - f) * S); - - switch(i) { - case 0: - r = V, g = t, b = p; - break; - case 1: - r = q, g = V, b = p; - break; - case 2: - r = p, g = V, b = t; - break; - case 3: - r = p, g = q, b = V; - break; - case 4: - r = t, g = p, b = V; - break; - case 5: - r = V, g = p, b = q; - break; - } - red = r * 255; - green = g * 255; - blue = b * 255; -} - - static void rainbow_timer_callback(void* context) { furi_assert(context); RGBBacklightApp* app = context; - uint8_t r = 0; - uint8_t g = 0; - uint8_t b = 0; if(app->settings->rgb_backlight_installed) { - switch(app->settings->rainbow_mode) { - case 1: - for(uint8_t i = 0; i < SK6805_get_led_count(); i++) { - hsv_to_rgb(r,g,b,app->rainbow_hue, app->settings->rainbow_saturation,app->settings->brightness*255); - FURI_LOG_D( - TAG, "rgb %d,%d,%d", r, g, b); - //rgb_backlight_set_led_custom_color (i,*r,*g,*b); - //SK6805_update(); - } - - break; - case 2: - break; - default: - break; + app->rainbow_hue += app->settings->rainbow_step; + if(app->rainbow_hue > 254) { + app->rainbow_hue = 0; } - app->rainbow_hue++; + + uint8_t wide = app->settings->rainbow_wide; + + switch(app->settings->rainbow_mode) { + //rainbow mode + case 1: + for(uint8_t i = 0; i < SK6805_get_led_count(); i++) { + rgb_backlight_set_led_custom_hsv_color( + i, + app->rainbow_hue, + app->settings->rainbow_saturation, + app->settings->brightness); + } + break; + + //wave mode + case 2: + uint16_t j = app->rainbow_hue + wide; + uint16_t k = app->rainbow_hue + wide * 2; + + if(app->rainbow_hue > (254 - wide)) { + j = j - 255; + } + if(app->rainbow_hue > (254 - wide * 2)) { + k = k - 255; + } + + rgb_backlight_set_led_custom_hsv_color( + 0, app->rainbow_hue, app->settings->rainbow_saturation, app->settings->brightness); + rgb_backlight_set_led_custom_hsv_color( + 1, j, app->settings->rainbow_saturation, app->settings->brightness); + rgb_backlight_set_led_custom_hsv_color( + 2, k, app->settings->rainbow_saturation, app->settings->brightness); + break; + + default: + break; + } + rgb_backlight_update(app->settings->brightness); } - - // if rainbow_mode is ..... do another effect - // if(app->settings.rainbow_mode == ...) { - // } - } int32_t rgb_backlight_srv(void* p) { @@ -227,7 +238,7 @@ int32_t rgb_backlight_srv(void* p) { // Define object app (full app with settings and running variables), // allocate memory and create RECORD for access to app structure from outside RGBBacklightApp* app = malloc(sizeof(RGBBacklightApp)); - + //define rainbow_timer and they callback app->rainbow_timer = furi_timer_alloc(rainbow_timer_callback, FuriTimerTypePeriodic, app); @@ -242,24 +253,24 @@ int32_t rgb_backlight_srv(void* p) { //if rgb_backlight_installed then start rainbow or set leds colors from saved settings (default index = 0) if(app->settings->rgb_backlight_installed) { if(app->settings->rainbow_mode > 0) { - // rainbow_timer_starter(app); + rainbow_timer_starter(app); } else { - rgb_backlight_set_led_static_color (2,app->settings->led_2_color_index); - rgb_backlight_set_led_static_color (1,app->settings->led_1_color_index); - rgb_backlight_set_led_static_color (0,app->settings->led_0_color_index); - rgb_backlight_update (app->settings->brightness); + rgb_backlight_set_led_static_color(2, app->settings->led_2_color_index); + rgb_backlight_set_led_static_color(1, app->settings->led_1_color_index); + rgb_backlight_set_led_static_color(0, app->settings->led_0_color_index); + rgb_backlight_update(app->settings->brightness); } - // if rgb_backlight not installed then set default static orange color(index=0) to all leds (0-2) and force light on + // if rgb_backlight not installed then set default static orange color(index=0) to all leds (0-2) and force light on } else { - rgb_backlight_set_led_static_color (2,0); - rgb_backlight_set_led_static_color (1,0); - rgb_backlight_set_led_static_color (0,0); + rgb_backlight_set_led_static_color(2, 0); + rgb_backlight_set_led_static_color(1, 0); + rgb_backlight_set_led_static_color(0, 0); SK6805_update(); } while(1) { // place for message queue and other future options - furi_delay_ms (5000); + furi_delay_ms(5000); if(app->settings->rgb_backlight_installed) { FURI_LOG_D(TAG, "RGB backlight enabled - serivce is running"); } else { diff --git a/applications/services/rgb_backlight/rgb_backlight.h b/applications/services/rgb_backlight/rgb_backlight.h index e8b78f461..10aa9341f 100644 --- a/applications/services/rgb_backlight/rgb_backlight.h +++ b/applications/services/rgb_backlight/rgb_backlight.h @@ -28,7 +28,7 @@ extern "C" { typedef struct { FuriTimer* rainbow_timer; - uint8_t rainbow_hue; + uint16_t rainbow_hue; uint8_t rainbow_red; uint8_t rainbow_green; uint8_t rainbow_blue; @@ -40,6 +40,7 @@ typedef struct { #define RECORD_RGB_BACKLIGHT "rgb_backlight" void rgb_backlight_update(float brightness); +//not used now, for future use // void rgb_backlight_set_custom_color(uint8_t red, uint8_t green, uint8_t blue); void rgb_backlight_set_led_static_color(uint8_t led, uint8_t index); void rainbow_timer_stop(RGBBacklightApp* app); diff --git a/applications/services/rgb_backlight/rgb_backlight_settings.c b/applications/services/rgb_backlight/rgb_backlight_settings.c index 0b0388cae..f612507e4 100644 --- a/applications/services/rgb_backlight/rgb_backlight_settings.c +++ b/applications/services/rgb_backlight/rgb_backlight_settings.c @@ -29,6 +29,7 @@ typedef struct { uint32_t rainbow_speed_ms; uint16_t rainbow_step; uint8_t rainbow_saturation; + uint8_t rainbow_wide; } RGBBacklightSettingsPrevious; void rgb_backlight_settings_load(RGBBacklightSettings* settings) { @@ -84,6 +85,7 @@ void rgb_backlight_settings_load(RGBBacklightSettings* settings) { settings->rainbow_speed_ms = 100; settings->rainbow_step = 1; settings->rainbow_saturation = 255; + settings->rainbow_wide = 50; rgb_backlight_settings_save(settings); } } diff --git a/applications/services/rgb_backlight/rgb_backlight_settings.h b/applications/services/rgb_backlight/rgb_backlight_settings.h index 7c6e08c95..3f3af005f 100644 --- a/applications/services/rgb_backlight/rgb_backlight_settings.h +++ b/applications/services/rgb_backlight/rgb_backlight_settings.h @@ -8,17 +8,18 @@ typedef struct { uint8_t version; uint8_t rgb_backlight_installed; float brightness; - + // static gradient mode settings uint8_t led_2_color_index; uint8_t led_1_color_index; uint8_t led_0_color_index; - + // rainbow mode setings uint32_t rainbow_mode; uint32_t rainbow_speed_ms; uint16_t rainbow_step; - uint8_t rainbow_saturation; + uint8_t rainbow_saturation; + uint8_t rainbow_wide; } RGBBacklightSettings; diff --git a/applications/settings/notification_settings/notification_settings_app.c b/applications/settings/notification_settings/notification_settings_app.c index 7e8536ee7..98efc54b7 100644 --- a/applications/settings/notification_settings/notification_settings_app.c +++ b/applications/settings/notification_settings/notification_settings_app.c @@ -134,21 +134,29 @@ const uint32_t rgb_backlight_rainbow_speed_value[RGB_BACKLIGHT_RAINBOW_SPEED_COU 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000, 1100, 1200, 1300, 1400, 1500, 1600, 1700, 1800, 1900, 2000}; -#define RGB_BACKLIGHT_RAINBOW_STEP_COUNT 10 +#define RGB_BACKLIGHT_RAINBOW_STEP_COUNT 3 const char* const rgb_backlight_rainbow_step_text[RGB_BACKLIGHT_RAINBOW_STEP_COUNT] = { "1", "2", "3", - "4", - "5", - "6", - "7", - "8", - "9", - "10", }; -const uint32_t rgb_backlight_rainbow_step_value[RGB_BACKLIGHT_RAINBOW_STEP_COUNT] = - {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; +const uint32_t rgb_backlight_rainbow_step_value[RGB_BACKLIGHT_RAINBOW_STEP_COUNT] = { + 1, + 2, + 3, +}; + +#define RGB_BACKLIGHT_RAINBOW_WIDE_COUNT 3 +const char* const rgb_backlight_rainbow_wide_text[RGB_BACKLIGHT_RAINBOW_WIDE_COUNT] = { + "1", + "2", + "3", +}; +const uint32_t rgb_backlight_rainbow_wide_value[RGB_BACKLIGHT_RAINBOW_WIDE_COUNT] = { + 30, + 40, + 50, +}; typedef enum { MainViewId, @@ -234,26 +242,29 @@ static void rgb_backlight_installed_changed(VariableItem* item) { NotificationAppSettings* app = variable_item_get_context(item); uint8_t index = variable_item_get_current_value_index(item); variable_item_set_current_value_text(item, rgb_backlight_installed_text[index]); - app->notification->rgb_srv->settings->rgb_backlight_installed = rgb_backlight_installed_value[index]; + app->notification->rgb_srv->settings->rgb_backlight_installed = + rgb_backlight_installed_value[index]; rgb_backlight_settings_save(app->notification->rgb_srv->settings); - + // In case of user playing with rgb_backlight_installed swith: - // if user swith_off rgb_backlight_installed then force set default orange color - if (index == 0) { - rgb_backlight_set_led_static_color (2,0); - rgb_backlight_set_led_static_color (1,0); - rgb_backlight_set_led_static_color (0,0); + // if user swith_off rgb_backlight_installed then force set default orange color - defence from stupid. + if(index == 0) { + rgb_backlight_set_led_static_color(2, 0); + rgb_backlight_set_led_static_color(1, 0); + rgb_backlight_set_led_static_color(0, 0); SK6805_update(); - // if user swith_on rgb_backlight_installed then start rainbow if its ON or set saved static colors + // start rainbow (if its Enabled) or set saved static colors if user swith_on rgb_backlight_installed switch } else { - - if (app->notification->rgb_srv->settings->rainbow_mode >0) { - rainbow_timer_starter (app->notification->rgb_srv); + if(app->notification->rgb_srv->settings->rainbow_mode > 0) { + rainbow_timer_starter(app->notification->rgb_srv); } else { - rgb_backlight_set_led_static_color (2,app->notification->rgb_srv->settings->led_2_color_index); - rgb_backlight_set_led_static_color (1,app->notification->rgb_srv->settings->led_1_color_index); - rgb_backlight_set_led_static_color (0,app->notification->rgb_srv->settings->led_0_color_index); - rgb_backlight_update (app->notification->settings.display_brightness); + rgb_backlight_set_led_static_color( + 2, app->notification->rgb_srv->settings->led_2_color_index); + rgb_backlight_set_led_static_color( + 1, app->notification->rgb_srv->settings->led_1_color_index); + rgb_backlight_set_led_static_color( + 0, app->notification->rgb_srv->settings->led_0_color_index); + rgb_backlight_update(app->notification->settings.display_brightness); } } @@ -262,7 +273,7 @@ static void rgb_backlight_installed_changed(VariableItem* item) { if(furi_hal_rtc_is_flag_set(FuriHalRtcFlagDebug)) { slide = 1; } - for(int i = slide; i < (slide + 7); i++) { + for(int i = slide; i < (slide + 8); i++) { VariableItem* t_item = variable_item_list_get(app->variable_item_list_rgb, i); if(index == 0) { variable_item_set_locked(t_item, true, "RGB\nOFF!"); @@ -279,13 +290,12 @@ static void led_2_color_changed(VariableItem* item) { variable_item_set_current_value_text(item, rgb_backlight_get_color_text(index)); app->notification->rgb_srv->settings->led_2_color_index = index; - rgb_backlight_set_led_static_color(2,index); - rgb_backlight_update(app->notification->rgb_srv->settings->brightness); - - // dont update display color if rainbow working - if (!furi_timer_is_running(app->notification->rgb_srv->rainbow_timer)) { - rgb_backlight_settings_save(app->notification->rgb_srv->settings); + //dont update screen color if rainbow timer working + if(!furi_timer_is_running(app->notification->rgb_srv->rainbow_timer)) { + rgb_backlight_set_led_static_color(2, index); + rgb_backlight_update(app->notification->rgb_srv->settings->brightness); } + rgb_backlight_settings_save(app->notification->rgb_srv->settings); } static void led_1_color_changed(VariableItem* item) { @@ -295,13 +305,13 @@ static void led_1_color_changed(VariableItem* item) { variable_item_set_current_value_text(item, rgb_backlight_get_color_text(index)); app->notification->rgb_srv->settings->led_1_color_index = index; - rgb_backlight_set_led_static_color(1,index); - rgb_backlight_settings_save(app->notification->rgb_srv->settings); - - // dont update display color if rainbow working - if (!furi_timer_is_running(app->notification->rgb_srv->rainbow_timer)) { - rgb_backlight_settings_save(app->notification->rgb_srv->settings); + //dont update screen color if rainbow timer working + if(!furi_timer_is_running(app->notification->rgb_srv->rainbow_timer)) { + rgb_backlight_set_led_static_color(1, index); + rgb_backlight_update(app->notification->rgb_srv->settings->brightness); } + + rgb_backlight_settings_save(app->notification->rgb_srv->settings); } static void led_0_color_changed(VariableItem* item) { @@ -311,13 +321,13 @@ static void led_0_color_changed(VariableItem* item) { variable_item_set_current_value_text(item, rgb_backlight_get_color_text(index)); app->notification->rgb_srv->settings->led_0_color_index = index; - rgb_backlight_set_led_static_color(0,index); - rgb_backlight_settings_save(app->notification->rgb_srv->settings); - - // dont update display color if rainbow working - if (!furi_timer_is_running(app->notification->rgb_srv->rainbow_timer)) { - rgb_backlight_settings_save(app->notification->rgb_srv->settings); + //dont update screen color if rainbow timer working + if(!furi_timer_is_running(app->notification->rgb_srv->rainbow_timer)) { + rgb_backlight_set_led_static_color(0, index); + rgb_backlight_update(app->notification->rgb_srv->settings->brightness); } + + rgb_backlight_settings_save(app->notification->rgb_srv->settings); } static void rgb_backlight_rainbow_changed(VariableItem* item) { @@ -332,9 +342,12 @@ static void rgb_backlight_rainbow_changed(VariableItem* item) { // restore saved rgb backlight settings if we switch_off rainbow mode if(app->notification->rgb_srv->settings->rainbow_mode == 0) { - rgb_backlight_set_led_static_color (2,app->notification->rgb_srv->settings->led_2_color_index); - rgb_backlight_set_led_static_color (1,app->notification->rgb_srv->settings->led_1_color_index); - rgb_backlight_set_led_static_color (0,app->notification->rgb_srv->settings->led_0_color_index); + rgb_backlight_set_led_static_color( + 2, app->notification->rgb_srv->settings->led_2_color_index); + rgb_backlight_set_led_static_color( + 1, app->notification->rgb_srv->settings->led_1_color_index); + rgb_backlight_set_led_static_color( + 0, app->notification->rgb_srv->settings->led_0_color_index); rgb_backlight_update(app->notification->rgb_srv->settings->brightness); } } @@ -362,294 +375,324 @@ static void rgb_backlight_rainbow_step_changed(VariableItem* item) { rgb_backlight_settings_save(app->notification->rgb_srv->settings); } -static void rgb_backlight_rainbow_saturation_changed (VariableItem* item) { +static void rgb_backlight_rainbow_saturation_changed(VariableItem* item) { NotificationAppSettings* app = variable_item_get_context(item); - //saturation must be 1..255, so (0..254)+1 - uint8_t index = variable_item_get_current_value_index(item)+1; + //saturation must be 1..255, so we do (0..254)+1 + uint8_t index = variable_item_get_current_value_index(item) + 1; char valtext[4] = {}; snprintf(valtext, sizeof(valtext), "%d", index); variable_item_set_current_value_text(item, valtext); app->notification->rgb_srv->settings->rainbow_saturation = index; rgb_backlight_settings_save(app->notification->rgb_srv->settings); } - // open rgb_settings_view if user press OK on first (index=0) menu string and (debug mode or rgb_backlight_installed is true) - void variable_item_list_enter_callback(void* context, uint32_t index) { - UNUSED(context); - NotificationAppSettings* app = context; - if(((app->notification->rgb_srv->settings->rgb_backlight_installed) || - (furi_hal_rtc_is_flag_set(FuriHalRtcFlagDebug))) && - (index == 0)) { - view_dispatcher_switch_to_view(app->view_dispatcher, RGBViewId); - } +static void rgb_backlight_rainbow_wide_changed(VariableItem* item) { + NotificationAppSettings* app = variable_item_get_context(item); + uint8_t index = variable_item_get_current_value_index(item); + + variable_item_set_current_value_text(item, rgb_backlight_rainbow_wide_text[index]); + app->notification->rgb_srv->settings->rainbow_wide = rgb_backlight_rainbow_wide_value[index]; + + //save settings and restart timer with new speed value + rgb_backlight_settings_save(app->notification->rgb_srv->settings); + rainbow_timer_starter(app->notification->rgb_srv); +} + +// open rgb_settings_view if user press OK on first (index=0) menu string and (debug mode or rgb_backlight_installed is true) +void variable_item_list_enter_callback(void* context, uint32_t index) { + UNUSED(context); + NotificationAppSettings* app = context; + + if(((app->notification->rgb_srv->settings->rgb_backlight_installed) || + (furi_hal_rtc_is_flag_set(FuriHalRtcFlagDebug))) && + (index == 0)) { + view_dispatcher_switch_to_view(app->view_dispatcher, RGBViewId); } +} - // switch to main view on exit from rgb_settings_view - static uint32_t notification_app_rgb_settings_exit(void* context) { - UNUSED(context); - return MainViewId; +// switch to main view on exit from rgb_settings_view +static uint32_t notification_app_rgb_settings_exit(void* context) { + UNUSED(context); + return MainViewId; +} +//--- RGB BACKLIGHT END --- + +static uint32_t notification_app_settings_exit(void* context) { + UNUSED(context); + return VIEW_NONE; +} + +static NotificationAppSettings* alloc_settings(void) { + NotificationAppSettings* app = malloc(sizeof(NotificationAppSettings)); + app->notification = furi_record_open(RECORD_NOTIFICATION); + app->gui = furi_record_open(RECORD_GUI); + + app->variable_item_list = variable_item_list_alloc(); + View* view = variable_item_list_get_view(app->variable_item_list); + + VariableItem* item; + uint8_t value_index; + + //set callback for exit from main view + view_set_previous_callback(view, notification_app_settings_exit); + + //--- RGB BACKLIGHT --- + // set callback for OK pressed in notification settings menu + variable_item_list_set_enter_callback( + app->variable_item_list, variable_item_list_enter_callback, app); + + //Show RGB settings only when debug_mode or rgb_backlight_installed is active + if((app->notification->rgb_srv->settings->rgb_backlight_installed) || + (furi_hal_rtc_is_flag_set(FuriHalRtcFlagDebug))) { + item = variable_item_list_add(app->variable_item_list, "RGB settings", 0, NULL, app); } //--- RGB BACKLIGHT END --- - static uint32_t notification_app_settings_exit(void* context) { - UNUSED(context); - return VIEW_NONE; - } + item = variable_item_list_add( + app->variable_item_list, "LCD Contrast", CONTRAST_COUNT, contrast_changed, app); + value_index = + value_index_int32(app->notification->settings.contrast, contrast_value, CONTRAST_COUNT); + variable_item_set_current_value_index(item, value_index); + variable_item_set_current_value_text(item, contrast_text[value_index]); - static NotificationAppSettings* alloc_settings(void) { - NotificationAppSettings* app = malloc(sizeof(NotificationAppSettings)); - app->notification = furi_record_open(RECORD_NOTIFICATION); - app->gui = furi_record_open(RECORD_GUI); + item = variable_item_list_add( + app->variable_item_list, "LCD Backlight", BACKLIGHT_COUNT, backlight_changed, app); + value_index = value_index_float( + app->notification->settings.display_brightness, backlight_value, BACKLIGHT_COUNT); + variable_item_set_current_value_index(item, value_index); + variable_item_set_current_value_text(item, backlight_text[value_index]); - app->variable_item_list = variable_item_list_alloc(); - View* view = variable_item_list_get_view(app->variable_item_list); + item = variable_item_list_add( + app->variable_item_list, "Backlight Time", DELAY_COUNT, screen_changed, app); + value_index = value_index_uint32( + app->notification->settings.display_off_delay_ms, delay_value, DELAY_COUNT); + variable_item_set_current_value_index(item, value_index); + variable_item_set_current_value_text(item, delay_text[value_index]); - VariableItem* item; - uint8_t value_index; + item = variable_item_list_add( + app->variable_item_list, "LED Brightness", BACKLIGHT_COUNT, led_changed, app); + value_index = value_index_float( + app->notification->settings.led_brightness, backlight_value, BACKLIGHT_COUNT); + variable_item_set_current_value_index(item, value_index); + variable_item_set_current_value_text(item, backlight_text[value_index]); - //set callback for exit from main view - view_set_previous_callback(view, notification_app_settings_exit); - - //--- RGB BACKLIGHT --- - // set callback for OK pressed in notification settings menu - variable_item_list_set_enter_callback( - app->variable_item_list, variable_item_list_enter_callback, app); - - //Show RGB settings only when debug_mode or rgb_backlight_installed is active - if((app->notification->rgb_srv->settings->rgb_backlight_installed) || - (furi_hal_rtc_is_flag_set(FuriHalRtcFlagDebug))) { - item = variable_item_list_add(app->variable_item_list, "RGB settings", 0, NULL, app); - } - //--- RGB BACKLIGHT END --- - - item = variable_item_list_add( - app->variable_item_list, "LCD Contrast", CONTRAST_COUNT, contrast_changed, app); - value_index = value_index_int32( - app->notification->settings.contrast, contrast_value, CONTRAST_COUNT); + if(furi_hal_rtc_is_flag_set(FuriHalRtcFlagStealthMode)) { + item = variable_item_list_add(app->variable_item_list, "Volume", 1, NULL, app); + value_index = 0; variable_item_set_current_value_index(item, value_index); - variable_item_set_current_value_text(item, contrast_text[value_index]); - + variable_item_set_current_value_text(item, "Stealth"); + } else { item = variable_item_list_add( - app->variable_item_list, "LCD Backlight", BACKLIGHT_COUNT, backlight_changed, app); + app->variable_item_list, "Volume", VOLUME_COUNT, volume_changed, app); value_index = value_index_float( - app->notification->settings.display_brightness, backlight_value, BACKLIGHT_COUNT); + app->notification->settings.speaker_volume, volume_value, VOLUME_COUNT); variable_item_set_current_value_index(item, value_index); - variable_item_set_current_value_text(item, backlight_text[value_index]); - - item = variable_item_list_add( - app->variable_item_list, "Backlight Time", DELAY_COUNT, screen_changed, app); - value_index = value_index_uint32( - app->notification->settings.display_off_delay_ms, delay_value, DELAY_COUNT); - variable_item_set_current_value_index(item, value_index); - variable_item_set_current_value_text(item, delay_text[value_index]); - - item = variable_item_list_add( - app->variable_item_list, "LED Brightness", BACKLIGHT_COUNT, led_changed, app); - value_index = value_index_float( - app->notification->settings.led_brightness, backlight_value, BACKLIGHT_COUNT); - variable_item_set_current_value_index(item, value_index); - variable_item_set_current_value_text(item, backlight_text[value_index]); - - if(furi_hal_rtc_is_flag_set(FuriHalRtcFlagStealthMode)) { - item = variable_item_list_add(app->variable_item_list, "Volume", 1, NULL, app); - value_index = 0; - variable_item_set_current_value_index(item, value_index); - variable_item_set_current_value_text(item, "Stealth"); - } else { - item = variable_item_list_add( - app->variable_item_list, "Volume", VOLUME_COUNT, volume_changed, app); - value_index = value_index_float( - app->notification->settings.speaker_volume, volume_value, VOLUME_COUNT); - variable_item_set_current_value_index(item, value_index); - variable_item_set_current_value_text(item, volume_text[value_index]); - } - - if(furi_hal_rtc_is_flag_set(FuriHalRtcFlagStealthMode)) { - item = variable_item_list_add(app->variable_item_list, "Vibro", 1, NULL, app); - value_index = 0; - variable_item_set_current_value_index(item, value_index); - variable_item_set_current_value_text(item, "Stealth"); - } else { - item = variable_item_list_add( - app->variable_item_list, "Vibro", VIBRO_COUNT, vibro_changed, app); - value_index = - value_index_bool(app->notification->settings.vibro_on, vibro_value, VIBRO_COUNT); - variable_item_set_current_value_index(item, value_index); - variable_item_set_current_value_text(item, vibro_text[value_index]); - } - - //--- RGB BACKLIGHT --- - - app->variable_item_list_rgb = variable_item_list_alloc(); - View* view_rgb = variable_item_list_get_view(app->variable_item_list_rgb); - - // set callback for exit from rgb_settings_menu - view_set_previous_callback(view_rgb, notification_app_rgb_settings_exit); - - // // Show rgb_backlight_Installed_Swith only in Debug mode - if(furi_hal_rtc_is_flag_set(FuriHalRtcFlagDebug)) { - item = variable_item_list_add( - app->variable_item_list_rgb, - "RGB backlight installed", - RGB_BACKLIGHT_INSTALLED_COUNT, - rgb_backlight_installed_changed, - app); - value_index = value_index_bool( - app->notification->rgb_srv->settings->rgb_backlight_installed, - rgb_backlight_installed_value, - RGB_BACKLIGHT_INSTALLED_COUNT); - variable_item_set_current_value_index(item, value_index); - variable_item_set_current_value_text(item, rgb_backlight_installed_text[value_index]); - } - - // led_1 color - item = variable_item_list_add( - app->variable_item_list_rgb, - "LED 1 Color", - rgb_backlight_get_color_count(), - led_2_color_changed, - app); - value_index = app->notification->rgb_srv->settings->led_2_color_index; - variable_item_set_current_value_index(item, value_index); - variable_item_set_current_value_text(item, rgb_backlight_get_color_text(value_index)); - variable_item_set_locked( - item, - (app->notification->rgb_srv->settings->rgb_backlight_installed == 0), - "RGB MOD \nOFF!"); - - // led_2 color - item = variable_item_list_add( - app->variable_item_list_rgb, - "LED 2 Color", - rgb_backlight_get_color_count(), - led_1_color_changed, - app); - value_index = app->notification->rgb_srv->settings->led_1_color_index; - variable_item_set_current_value_index(item, value_index); - variable_item_set_current_value_text(item, rgb_backlight_get_color_text(value_index)); - variable_item_set_locked( - item, - (app->notification->rgb_srv->settings->rgb_backlight_installed == 0), - "RGB MOD \nOFF!"); - - // led 3 color - item = variable_item_list_add( - app->variable_item_list_rgb, - "LED 3 Color", - rgb_backlight_get_color_count(), - led_0_color_changed, - app); - value_index = app->notification->rgb_srv->settings->led_0_color_index; - variable_item_set_current_value_index(item, value_index); - variable_item_set_current_value_text(item, rgb_backlight_get_color_text(value_index)); - variable_item_set_locked( - item, - (app->notification->rgb_srv->settings->rgb_backlight_installed == 0), - "RGB MOD \nOFF!"); - - // Rainbow mode - item = variable_item_list_add( - app->variable_item_list_rgb, - "Rainbow mode", - RGB_BACKLIGHT_RAINBOW_MODE_COUNT, - rgb_backlight_rainbow_changed, - app); - value_index = value_index_uint32( - app->notification->rgb_srv->settings->rainbow_mode, - rgb_backlight_rainbow_mode_value, - RGB_BACKLIGHT_RAINBOW_MODE_COUNT); - variable_item_set_current_value_index(item, value_index); - variable_item_set_current_value_text(item, rgb_backlight_rainbow_mode_text[value_index]); - variable_item_set_locked( - item, - (app->notification->rgb_srv->settings->rgb_backlight_installed == 0), - "RGB MOD \nOFF!"); - - item = variable_item_list_add( - app->variable_item_list_rgb, - "Rainbow speed", - RGB_BACKLIGHT_RAINBOW_SPEED_COUNT, - rgb_backlight_rainbow_speed_changed, - app); - value_index = value_index_uint32( - app->notification->rgb_srv->settings->rainbow_speed_ms, - rgb_backlight_rainbow_speed_value, - RGB_BACKLIGHT_RAINBOW_SPEED_COUNT); - variable_item_set_current_value_index(item, value_index); - variable_item_set_current_value_text(item, rgb_backlight_rainbow_speed_text[value_index]); - variable_item_set_locked( - item, - (app->notification->rgb_srv->settings->rgb_backlight_installed == 0), - "RGB MOD \nOFF!"); - - item = variable_item_list_add( - app->variable_item_list_rgb, - "Rainbow step", - RGB_BACKLIGHT_RAINBOW_STEP_COUNT, - rgb_backlight_rainbow_step_changed, - app); - value_index = value_index_uint32( - app->notification->rgb_srv->settings->rainbow_step, - rgb_backlight_rainbow_step_value, - RGB_BACKLIGHT_RAINBOW_SPEED_COUNT); - variable_item_set_current_value_index(item, value_index); - variable_item_set_current_value_text(item, rgb_backlight_rainbow_step_text[value_index]); - variable_item_set_locked( - item, - (app->notification->rgb_srv->settings->rgb_backlight_installed == 0), - "RGB MOD \nOFF!"); - - item = variable_item_list_add( - app->variable_item_list_rgb, - "Saturation", - 255, - rgb_backlight_rainbow_saturation_changed, - app); - value_index = app->notification->rgb_srv->settings->rainbow_saturation; - variable_item_set_current_value_index(item, value_index); - char valtext[4] = {}; - snprintf(valtext, sizeof(valtext), "%d", value_index); - variable_item_set_current_value_text(item, valtext); - variable_item_set_locked( - item, - (app->notification->rgb_srv->settings->rgb_backlight_installed == 0), - "RGB MOD \nOFF!"); - - //--- RGB BACKLIGHT END --- - - app->view_dispatcher = view_dispatcher_alloc(); - view_dispatcher_attach_to_gui( - app->view_dispatcher, app->gui, ViewDispatcherTypeFullscreen); - view_dispatcher_add_view(app->view_dispatcher, MainViewId, view); - view_dispatcher_add_view(app->view_dispatcher, RGBViewId, view_rgb); - view_dispatcher_switch_to_view(app->view_dispatcher, MainViewId); - return app; + variable_item_set_current_value_text(item, volume_text[value_index]); } - static void free_settings(NotificationAppSettings * app) { - view_dispatcher_remove_view(app->view_dispatcher, MainViewId); - view_dispatcher_remove_view(app->view_dispatcher, RGBViewId); - variable_item_list_free(app->variable_item_list); - variable_item_list_free(app->variable_item_list_rgb); - view_dispatcher_free(app->view_dispatcher); - - furi_record_close(RECORD_GUI); - furi_record_close(RECORD_NOTIFICATION); - free(app); + if(furi_hal_rtc_is_flag_set(FuriHalRtcFlagStealthMode)) { + item = variable_item_list_add(app->variable_item_list, "Vibro", 1, NULL, app); + value_index = 0; + variable_item_set_current_value_index(item, value_index); + variable_item_set_current_value_text(item, "Stealth"); + } else { + item = variable_item_list_add( + app->variable_item_list, "Vibro", VIBRO_COUNT, vibro_changed, app); + value_index = + value_index_bool(app->notification->settings.vibro_on, vibro_value, VIBRO_COUNT); + variable_item_set_current_value_index(item, value_index); + variable_item_set_current_value_text(item, vibro_text[value_index]); } - int32_t notification_settings_app(void* p) { - UNUSED(p); - NotificationAppSettings* app = alloc_settings(); - view_dispatcher_run(app->view_dispatcher); - notification_message_save_settings(app->notification); + //--- RGB BACKLIGHT --- - // Automaticaly switch_off debug_mode when user exit from settings with enabled rgb_backlight_installed - // if(app->notification->settings.rgb_backlight_installed) { - // furi_hal_rtc_reset_flag(FuriHalRtcFlagDebug); - // } + app->variable_item_list_rgb = variable_item_list_alloc(); + View* view_rgb = variable_item_list_get_view(app->variable_item_list_rgb); - free_settings(app); - return 0; + // set callback for exit from rgb_settings_menu + view_set_previous_callback(view_rgb, notification_app_rgb_settings_exit); + + // // Show rgb_backlight_installed swith only in Debug mode + if(furi_hal_rtc_is_flag_set(FuriHalRtcFlagDebug)) { + item = variable_item_list_add( + app->variable_item_list_rgb, + "RGB backlight installed", + RGB_BACKLIGHT_INSTALLED_COUNT, + rgb_backlight_installed_changed, + app); + value_index = value_index_bool( + app->notification->rgb_srv->settings->rgb_backlight_installed, + rgb_backlight_installed_value, + RGB_BACKLIGHT_INSTALLED_COUNT); + variable_item_set_current_value_index(item, value_index); + variable_item_set_current_value_text(item, rgb_backlight_installed_text[value_index]); } + + // We (humans) are numbering LEDs from left to right as 1..3, but hardware have another order from right to left 2..0 + // led_1 color + item = variable_item_list_add( + app->variable_item_list_rgb, + "LED 1 Color", + rgb_backlight_get_color_count(), + led_2_color_changed, + app); + value_index = app->notification->rgb_srv->settings->led_2_color_index; + variable_item_set_current_value_index(item, value_index); + variable_item_set_current_value_text(item, rgb_backlight_get_color_text(value_index)); + variable_item_set_locked( + item, + (app->notification->rgb_srv->settings->rgb_backlight_installed == 0), + "RGB MOD \nOFF!"); + + // led_2 color + item = variable_item_list_add( + app->variable_item_list_rgb, + "LED 2 Color", + rgb_backlight_get_color_count(), + led_1_color_changed, + app); + value_index = app->notification->rgb_srv->settings->led_1_color_index; + variable_item_set_current_value_index(item, value_index); + variable_item_set_current_value_text(item, rgb_backlight_get_color_text(value_index)); + variable_item_set_locked( + item, + (app->notification->rgb_srv->settings->rgb_backlight_installed == 0), + "RGB MOD \nOFF!"); + + // led 3 color + item = variable_item_list_add( + app->variable_item_list_rgb, + "LED 3 Color", + rgb_backlight_get_color_count(), + led_0_color_changed, + app); + value_index = app->notification->rgb_srv->settings->led_0_color_index; + variable_item_set_current_value_index(item, value_index); + variable_item_set_current_value_text(item, rgb_backlight_get_color_text(value_index)); + variable_item_set_locked( + item, + (app->notification->rgb_srv->settings->rgb_backlight_installed == 0), + "RGB MOD \nOFF!"); + + // Rainbow mode + item = variable_item_list_add( + app->variable_item_list_rgb, + "Rainbow mode", + RGB_BACKLIGHT_RAINBOW_MODE_COUNT, + rgb_backlight_rainbow_changed, + app); + value_index = value_index_uint32( + app->notification->rgb_srv->settings->rainbow_mode, + rgb_backlight_rainbow_mode_value, + RGB_BACKLIGHT_RAINBOW_MODE_COUNT); + variable_item_set_current_value_index(item, value_index); + variable_item_set_current_value_text(item, rgb_backlight_rainbow_mode_text[value_index]); + variable_item_set_locked( + item, + (app->notification->rgb_srv->settings->rgb_backlight_installed == 0), + "RGB MOD \nOFF!"); + + item = variable_item_list_add( + app->variable_item_list_rgb, + "Rainbow speed", + RGB_BACKLIGHT_RAINBOW_SPEED_COUNT, + rgb_backlight_rainbow_speed_changed, + app); + value_index = value_index_uint32( + app->notification->rgb_srv->settings->rainbow_speed_ms, + rgb_backlight_rainbow_speed_value, + RGB_BACKLIGHT_RAINBOW_SPEED_COUNT); + variable_item_set_current_value_index(item, value_index); + variable_item_set_current_value_text(item, rgb_backlight_rainbow_speed_text[value_index]); + variable_item_set_locked( + item, + (app->notification->rgb_srv->settings->rgb_backlight_installed == 0), + "RGB MOD \nOFF!"); + + item = variable_item_list_add( + app->variable_item_list_rgb, + "Rainbow step", + RGB_BACKLIGHT_RAINBOW_STEP_COUNT, + rgb_backlight_rainbow_step_changed, + app); + value_index = value_index_uint32( + app->notification->rgb_srv->settings->rainbow_step, + rgb_backlight_rainbow_step_value, + RGB_BACKLIGHT_RAINBOW_STEP_COUNT); + variable_item_set_current_value_index(item, value_index); + variable_item_set_current_value_text(item, rgb_backlight_rainbow_step_text[value_index]); + variable_item_set_locked( + item, + (app->notification->rgb_srv->settings->rgb_backlight_installed == 0), + "RGB MOD \nOFF!"); + + item = variable_item_list_add( + app->variable_item_list_rgb, + "Saturation", + 255, + rgb_backlight_rainbow_saturation_changed, + app); + value_index = app->notification->rgb_srv->settings->rainbow_saturation; + variable_item_set_current_value_index(item, value_index); + char valtext[4] = {}; + snprintf(valtext, sizeof(valtext), "%d", value_index); + variable_item_set_current_value_text(item, valtext); + variable_item_set_locked( + item, + (app->notification->rgb_srv->settings->rgb_backlight_installed == 0), + "RGB MOD \nOFF!"); + + item = variable_item_list_add( + app->variable_item_list_rgb, + "Wave wide", + RGB_BACKLIGHT_RAINBOW_WIDE_COUNT, + rgb_backlight_rainbow_wide_changed, + app); + value_index = value_index_uint32( + app->notification->rgb_srv->settings->rainbow_wide, + rgb_backlight_rainbow_wide_value, + RGB_BACKLIGHT_RAINBOW_WIDE_COUNT); + variable_item_set_current_value_index(item, value_index); + variable_item_set_current_value_text(item, rgb_backlight_rainbow_wide_text[value_index]); + variable_item_set_locked( + item, + (app->notification->rgb_srv->settings->rgb_backlight_installed == 0), + "RGB MOD \nOFF!"); + + //--- RGB BACKLIGHT END --- + + app->view_dispatcher = view_dispatcher_alloc(); + view_dispatcher_attach_to_gui(app->view_dispatcher, app->gui, ViewDispatcherTypeFullscreen); + view_dispatcher_add_view(app->view_dispatcher, MainViewId, view); + view_dispatcher_add_view(app->view_dispatcher, RGBViewId, view_rgb); + view_dispatcher_switch_to_view(app->view_dispatcher, MainViewId); + return app; +} + +static void free_settings(NotificationAppSettings* app) { + view_dispatcher_remove_view(app->view_dispatcher, MainViewId); + view_dispatcher_remove_view(app->view_dispatcher, RGBViewId); + variable_item_list_free(app->variable_item_list); + variable_item_list_free(app->variable_item_list_rgb); + view_dispatcher_free(app->view_dispatcher); + + furi_record_close(RECORD_GUI); + furi_record_close(RECORD_NOTIFICATION); + free(app); +} + +int32_t notification_settings_app(void* p) { + UNUSED(p); + NotificationAppSettings* app = alloc_settings(); + view_dispatcher_run(app->view_dispatcher); + notification_message_save_settings(app->notification); + + // Automaticaly switch_off debug_mode when user exit from settings with enabled rgb_backlight_installed + // if(app->notification->settings.rgb_backlight_installed) { + // furi_hal_rtc_reset_flag(FuriHalRtcFlagDebug); + // } + + free_settings(app); + return 0; +} From f751b285322fb19ff9ac96cebf901c564f083810 Mon Sep 17 00:00:00 2001 From: Dmitry422 Date: Wed, 19 Mar 2025 19:18:19 +0700 Subject: [PATCH 137/268] Reboot screen color cosmetic changes. --- applications/services/rgb_backlight/rgb_backlight.c | 6 +++--- .../notification_settings/notification_settings_app.c | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/applications/services/rgb_backlight/rgb_backlight.c b/applications/services/rgb_backlight/rgb_backlight.c index 8d05e68de..f42a80dbe 100644 --- a/applications/services/rgb_backlight/rgb_backlight.c +++ b/applications/services/rgb_backlight/rgb_backlight.c @@ -38,9 +38,9 @@ typedef struct { //use one type RGBBacklightColor for current_leds_settings and for static colors definition static RGBBacklightColor current_led[] = { - {"LED0", 255, 60, 0}, - {"LED1", 255, 60, 0}, - {"LED2", 255, 60, 0}, + {"LED0", 0, 0, 0}, + {"LED1", 0, 0, 0}, + {"LED2", 0, 0, 0}, }; static const RGBBacklightColor colors[] = { diff --git a/applications/settings/notification_settings/notification_settings_app.c b/applications/settings/notification_settings/notification_settings_app.c index 98efc54b7..f4d5c40bb 100644 --- a/applications/settings/notification_settings/notification_settings_app.c +++ b/applications/settings/notification_settings/notification_settings_app.c @@ -253,7 +253,7 @@ static void rgb_backlight_installed_changed(VariableItem* item) { rgb_backlight_set_led_static_color(1, 0); rgb_backlight_set_led_static_color(0, 0); SK6805_update(); - // start rainbow (if its Enabled) or set saved static colors if user swith_on rgb_backlight_installed switch + // start rainbow (if its Enabled) or set saved static colors if user swith_on rgb_backlight_installed switch } else { if(app->notification->rgb_srv->settings->rainbow_mode > 0) { rainbow_timer_starter(app->notification->rgb_srv); From dc7e96d1855f53878a28cfd905f367fd7609cd7c Mon Sep 17 00:00:00 2001 From: Dmitry422 Date: Fri, 21 Mar 2025 06:33:27 +0700 Subject: [PATCH 138/268] Code cleanup --- targets/f7/api_symbols.csv | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/targets/f7/api_symbols.csv b/targets/f7/api_symbols.csv index 9cc8e15f6..c94a009ee 100755 --- a/targets/f7/api_symbols.csv +++ b/targets/f7/api_symbols.csv @@ -1,5 +1,5 @@ entry,status,name,type,params -Version,+,86.0,, +Version,+,83.2,, Header,+,applications/drivers/subghz/cc1101_ext/cc1101_ext_interconnect.h,, Header,+,applications/services/bt/bt_service/bt.h,, Header,+,applications/services/bt/bt_service/bt_keys_storage.h,, From f52c9855d62c93176e61d1a311203e18c623a4f9 Mon Sep 17 00:00:00 2001 From: Dmitry422 Date: Sun, 23 Mar 2025 19:36:38 +0700 Subject: [PATCH 139/268] Cosmetic code changes and removing unused parts. --- .../services/rgb_backlight/rgb_backlight.c | 35 ++++-------- .../services/rgb_backlight/rgb_backlight.h | 52 ++++++++++++++++-- .../notification_settings_app.c | 54 ++++++++++++------- 3 files changed, 93 insertions(+), 48 deletions(-) diff --git a/applications/services/rgb_backlight/rgb_backlight.c b/applications/services/rgb_backlight/rgb_backlight.c index f42a80dbe..053654ec2 100644 --- a/applications/services/rgb_backlight/rgb_backlight.c +++ b/applications/services/rgb_backlight/rgb_backlight.c @@ -36,7 +36,7 @@ typedef struct { uint8_t blue; } RGBBacklightColor; -//use one type RGBBacklightColor for current_leds_settings and for static colors definition +// use one type RGBBacklightColor for current_leds_settings and for static colors definition static RGBBacklightColor current_led[] = { {"LED0", 0, 0, 0}, {"LED1", 0, 0, 0}, @@ -83,30 +83,18 @@ void rgb_backlight_set_led_static_color(uint8_t led, uint8_t index) { } } -// --- NOT USED IN CURRENT RELEASE, FOR FUTURE USAGE--- -// Update current colors by custom rgb value -// void rgb_backlight_set_led_custom_color(uint8_t led, uint8_t red, uint8_t green, uint8_t blue) { -// if(led < SK6805_get_led_count()) { -// current_led[led].red = red; -// current_led[led].green = green; -// current_led[led].blue = blue; -// SK6805_set_led_color(led, red, green, blue); -// } -// } -// --- NOT USED IN CURRENT RELEASE, FOR FUTURE USAGE--- - // HSV to RGB based on // https://www.radiokot.ru/forum/viewtopic.php?p=3000181&ysclid=m88wvoz34w244644702 // https://radiolaba.ru/microcotrollers/tsvetnaya-lampa.html#comment-1790 // https://alexgyver.ru/lessons/arduino-rgb/?ysclid=m88voflppa24464916 // led number (0-2), hue (0..255), sat (0..255), val (0...1) void rgb_backlight_set_led_custom_hsv_color(uint8_t led, uint16_t hue, uint8_t sat, float V) { - //init value + // init value float r = 1.0f; float g = 1.0f; float b = 1.0f; - //from (0..255) to (0..1) + // from (0..255) to (0..1) float H = hue / 255.0f; float S = sat / 255.0f; @@ -137,7 +125,7 @@ void rgb_backlight_set_led_custom_hsv_color(uint8_t led, uint16_t hue, uint8_t s break; } - //from (0..1) to (0..255) + // from (0..1) to (0..255) current_led[led].red = r * 255; current_led[led].green = g * 255; current_led[led].blue = b * 255; @@ -159,12 +147,12 @@ void rgb_backlight_update(float brightness) { furi_record_close(RECORD_RGB_BACKLIGHT); } -//start furi timer for rainbow +// start furi timer for rainbow void rainbow_timer_start(RGBBacklightApp* app) { furi_timer_start(app->rainbow_timer, furi_ms_to_ticks(app->settings->rainbow_speed_ms)); } -//stop furi timer for rainbow +// stop furi timer for rainbow void rainbow_timer_stop(RGBBacklightApp* app) { furi_timer_stop(app->rainbow_timer); } @@ -173,10 +161,6 @@ void rainbow_timer_stop(RGBBacklightApp* app) { void rainbow_timer_starter(RGBBacklightApp* app) { if((app->settings->rainbow_mode > 0) && (app->settings->rgb_backlight_installed)) { rainbow_timer_start(app); - } else { - if(furi_timer_is_running(app->rainbow_timer)) { - rainbow_timer_stop(app); - } } } @@ -239,7 +223,7 @@ int32_t rgb_backlight_srv(void* p) { // allocate memory and create RECORD for access to app structure from outside RGBBacklightApp* app = malloc(sizeof(RGBBacklightApp)); - //define rainbow_timer and they callback + // define rainbow_timer and they callback app->rainbow_timer = furi_timer_alloc(rainbow_timer_callback, FuriTimerTypePeriodic, app); // settings load or create default @@ -250,10 +234,10 @@ int32_t rgb_backlight_srv(void* p) { furi_record_create(RECORD_RGB_BACKLIGHT, app); - //if rgb_backlight_installed then start rainbow or set leds colors from saved settings (default index = 0) + // if rgb_backlight_installed then start rainbow or set leds colors from saved settings (default index = 0) if(app->settings->rgb_backlight_installed) { if(app->settings->rainbow_mode > 0) { - rainbow_timer_starter(app); + rainbow_timer_start(app); } else { rgb_backlight_set_led_static_color(2, app->settings->led_2_color_index); rgb_backlight_set_led_static_color(1, app->settings->led_1_color_index); @@ -269,7 +253,6 @@ int32_t rgb_backlight_srv(void* p) { } while(1) { - // place for message queue and other future options furi_delay_ms(5000); if(app->settings->rgb_backlight_installed) { FURI_LOG_D(TAG, "RGB backlight enabled - serivce is running"); diff --git a/applications/services/rgb_backlight/rgb_backlight.h b/applications/services/rgb_backlight/rgb_backlight.h index 10aa9341f..d6f8b5fce 100644 --- a/applications/services/rgb_backlight/rgb_backlight.h +++ b/applications/services/rgb_backlight/rgb_backlight.h @@ -32,21 +32,65 @@ typedef struct { uint8_t rainbow_red; uint8_t rainbow_green; uint8_t rainbow_blue; - RGBBacklightSettings* settings; - } RGBBacklightApp; #define RECORD_RGB_BACKLIGHT "rgb_backlight" +/** Update leds colors from current_led[i].color and selected bright + * + * @param brightness - Brightness 0..1 + * @return + */ void rgb_backlight_update(float brightness); -//not used now, for future use -// void rgb_backlight_set_custom_color(uint8_t red, uint8_t green, uint8_t blue); + +/** Set current_led[i].color for one led by static color index + * + * @param led - Led number (0..2) + * @param index - Static color index number + * @return + */ void rgb_backlight_set_led_static_color(uint8_t led, uint8_t index); + +/** Stop rainbow timer + * + * @param app - Instance of RGBBacklightApp from FURI RECORD + * @return + */ void rainbow_timer_stop(RGBBacklightApp* app); + +/** Start rainbow timer + * + * @param app - Instance of RGBBacklightApp from FURI RECORD + * @return + */ + +/** Start rainbow timer + * + * @param app - Instance of RGBBacklightApp from FURI RECORD + * @return + */ void rainbow_timer_start(RGBBacklightApp* app); + +/** Start rainbow timer only if all conditions meet (rgb_backlight_installed && rainbow ON) + * + * @param app - Instance of RGBBacklightApp from FURI RECORD + * @return + */ void rainbow_timer_starter(RGBBacklightApp* app); + +/** Get name of static color by index + * + * @param index - Static colors index number + * @return - color name + */ const char* rgb_backlight_get_color_text(uint8_t index); + +/** Get static colors count + * + * @param + * @return - colors count + */ uint8_t rgb_backlight_get_color_count(void); #ifdef __cplusplus diff --git a/applications/settings/notification_settings/notification_settings_app.c b/applications/settings/notification_settings/notification_settings_app.c index f4d5c40bb..dd2206dc2 100644 --- a/applications/settings/notification_settings/notification_settings_app.c +++ b/applications/settings/notification_settings/notification_settings_app.c @@ -16,8 +16,6 @@ typedef struct { VariableItemList* variable_item_list_rgb; } NotificationAppSettings; -//static VariableItem* temp_item; - static const NotificationSequence sequence_note_c = { &message_note_c5, &message_delay_100, @@ -126,13 +124,32 @@ const char* const rgb_backlight_rainbow_mode_text[RGB_BACKLIGHT_RAINBOW_MODE_COU }; const uint32_t rgb_backlight_rainbow_mode_value[RGB_BACKLIGHT_RAINBOW_MODE_COUNT] = {0, 1, 2}; -#define RGB_BACKLIGHT_RAINBOW_SPEED_COUNT 20 +#define RGB_BACKLIGHT_RAINBOW_SPEED_COUNT 10 const char* const rgb_backlight_rainbow_speed_text[RGB_BACKLIGHT_RAINBOW_SPEED_COUNT] = { - "0.1s", "0.2s", "0.3s", "0.4s", "0.5s", "0.6s", "0.7", "0.8", "0.9", "1s", - "1.1s", "1.2s", "1.3s", "1.4s", "1.5s", "1.6s", "1.7s", "1.8s", "1.9s", "2s"}; + "0.1s", + "0.2s", + "0.3s", + "0.4s", + "0.5s", + "0.6s", + "0.7", + "0.8", + "0.9", + "1s", +}; + const uint32_t rgb_backlight_rainbow_speed_value[RGB_BACKLIGHT_RAINBOW_SPEED_COUNT] = { - 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000, - 1100, 1200, 1300, 1400, 1500, 1600, 1700, 1800, 1900, 2000}; + 100, + 200, + 300, + 400, + 500, + 600, + 700, + 800, + 900, + 1000, +}; #define RGB_BACKLIGHT_RAINBOW_STEP_COUNT 3 const char* const rgb_backlight_rainbow_step_text[RGB_BACKLIGHT_RAINBOW_STEP_COUNT] = { @@ -182,7 +199,7 @@ static void backlight_changed(VariableItem* item) { app->notification->settings.display_brightness = backlight_value[index]; //--- RGB BACKLIGHT --- - //set selected brightness to current rgb backlight service settings and save settings + // set selected brightness to current rgb backlight service settings and save settings app->notification->rgb_srv->settings->brightness = backlight_value[index]; rgb_backlight_settings_save(app->notification->rgb_srv->settings); //--- RGB BACKLIGHT END --- @@ -247,13 +264,14 @@ static void rgb_backlight_installed_changed(VariableItem* item) { rgb_backlight_settings_save(app->notification->rgb_srv->settings); // In case of user playing with rgb_backlight_installed swith: - // if user swith_off rgb_backlight_installed then force set default orange color - defence from stupid. + // if user swith_off rgb_backlight_installed (but may be he have mod installed) + // then force set default orange color - defence from stupid. if(index == 0) { rgb_backlight_set_led_static_color(2, 0); rgb_backlight_set_led_static_color(1, 0); rgb_backlight_set_led_static_color(0, 0); SK6805_update(); - // start rainbow (if its Enabled) or set saved static colors if user swith_on rgb_backlight_installed switch + // start rainbow (if its Enabled) or set saved static colors if user swith_on rgb_backlight_installed switch } else { if(app->notification->rgb_srv->settings->rainbow_mode > 0) { rainbow_timer_starter(app->notification->rgb_srv); @@ -290,7 +308,7 @@ static void led_2_color_changed(VariableItem* item) { variable_item_set_current_value_text(item, rgb_backlight_get_color_text(index)); app->notification->rgb_srv->settings->led_2_color_index = index; - //dont update screen color if rainbow timer working + // dont update screen color if rainbow timer working if(!furi_timer_is_running(app->notification->rgb_srv->rainbow_timer)) { rgb_backlight_set_led_static_color(2, index); rgb_backlight_update(app->notification->rgb_srv->settings->brightness); @@ -305,7 +323,7 @@ static void led_1_color_changed(VariableItem* item) { variable_item_set_current_value_text(item, rgb_backlight_get_color_text(index)); app->notification->rgb_srv->settings->led_1_color_index = index; - //dont update screen color if rainbow timer working + // dont update screen color if rainbow timer working if(!furi_timer_is_running(app->notification->rgb_srv->rainbow_timer)) { rgb_backlight_set_led_static_color(1, index); rgb_backlight_update(app->notification->rgb_srv->settings->brightness); @@ -321,7 +339,7 @@ static void led_0_color_changed(VariableItem* item) { variable_item_set_current_value_text(item, rgb_backlight_get_color_text(index)); app->notification->rgb_srv->settings->led_0_color_index = index; - //dont update screen color if rainbow timer working + // dont update screen color if rainbow timer working if(!furi_timer_is_running(app->notification->rgb_srv->rainbow_timer)) { rgb_backlight_set_led_static_color(0, index); rgb_backlight_update(app->notification->rgb_srv->settings->brightness); @@ -360,7 +378,7 @@ static void rgb_backlight_rainbow_speed_changed(VariableItem* item) { app->notification->rgb_srv->settings->rainbow_speed_ms = rgb_backlight_rainbow_speed_value[index]; - //save settings and restart timer with new speed value + // save settings and restart timer with new speed value rgb_backlight_settings_save(app->notification->rgb_srv->settings); rainbow_timer_starter(app->notification->rgb_srv); } @@ -378,7 +396,7 @@ static void rgb_backlight_rainbow_step_changed(VariableItem* item) { static void rgb_backlight_rainbow_saturation_changed(VariableItem* item) { NotificationAppSettings* app = variable_item_get_context(item); - //saturation must be 1..255, so we do (0..254)+1 + // saturation must be 1..255, so we do (0..254)+1 uint8_t index = variable_item_get_current_value_index(item) + 1; char valtext[4] = {}; snprintf(valtext, sizeof(valtext), "%d", index); @@ -394,7 +412,7 @@ static void rgb_backlight_rainbow_wide_changed(VariableItem* item) { variable_item_set_current_value_text(item, rgb_backlight_rainbow_wide_text[index]); app->notification->rgb_srv->settings->rainbow_wide = rgb_backlight_rainbow_wide_value[index]; - //save settings and restart timer with new speed value + // save settings and restart timer with new speed value rgb_backlight_settings_save(app->notification->rgb_srv->settings); rainbow_timer_starter(app->notification->rgb_srv); } @@ -442,7 +460,7 @@ static NotificationAppSettings* alloc_settings(void) { variable_item_list_set_enter_callback( app->variable_item_list, variable_item_list_enter_callback, app); - //Show RGB settings only when debug_mode or rgb_backlight_installed is active + // Show RGB settings only when debug_mode or rgb_backlight_installed is active if((app->notification->rgb_srv->settings->rgb_backlight_installed) || (furi_hal_rtc_is_flag_set(FuriHalRtcFlagDebug))) { item = variable_item_list_add(app->variable_item_list, "RGB settings", 0, NULL, app); @@ -513,7 +531,7 @@ static NotificationAppSettings* alloc_settings(void) { // set callback for exit from rgb_settings_menu view_set_previous_callback(view_rgb, notification_app_rgb_settings_exit); - // // Show rgb_backlight_installed swith only in Debug mode + // Show rgb_backlight_installed swith only in Debug mode if(furi_hal_rtc_is_flag_set(FuriHalRtcFlagDebug)) { item = variable_item_list_add( app->variable_item_list_rgb, From e196999b4a27f589ca8c97ecbab3a2ce736e0c18 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Mon, 24 Mar 2025 03:36:26 +0300 Subject: [PATCH 140/268] upd changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index adb257d38..6c89f8dd7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ * SubGHz: **Fix Hollarm protocol with more verification** * SubGHz: **Fix GangQi protocol** (by @DoberBit and @mishamyte (who spent 2 weeks on this)) * SubGHz: **Came Atomo button hold simulation with full cycle** simulation (to allow proper pairing with receiver) -* System: **Сombining RGB Backlight mod** (by @quen0n) and original backlight support **in one firmware** (+ Rainbow effect (based on @Willy-JL idea)) (PR #877 | by @Dmitry422) - (**To enable RGB Backlight support go into Notifications settings with Debug mode - ON**) +* System: **Сombining RGB Backlight mod** (by @quen0n) and original backlight support **in one firmware** (+ Rainbow/Wave effect (based on @Willy-JL idea)) (PR #877 #881 | by @Dmitry422) - (**To enable RGB Backlight support go into Notifications settings with Debug mode - ON**) * OFW: LFRFID - **EM4305 support** * OFW: **Universal IR signal selection** * OFW: **BadUSB: Mouse control** From b792e094fb9003e0f4c131feac16ec8148c835c9 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Mon, 24 Mar 2025 05:08:29 +0300 Subject: [PATCH 141/268] bump version --- applications/services/rgb_backlight/rgb_backlight_settings.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/applications/services/rgb_backlight/rgb_backlight_settings.c b/applications/services/rgb_backlight/rgb_backlight_settings.c index f612507e4..681197284 100644 --- a/applications/services/rgb_backlight/rgb_backlight_settings.c +++ b/applications/services/rgb_backlight/rgb_backlight_settings.c @@ -9,8 +9,8 @@ #define RGB_BACKLIGHT_SETTINGS_PATH INT_PATH(RGB_BACKLIGHT_SETTINGS_FILE_NAME) #define RGB_BACKLIGHT_SETTINGS_MAGIC (0x30) -#define RGB_BACKLIGHT_SETTINGS_VER_PREV (0) // Previous version number -#define RGB_BACKLIGHT_SETTINGS_VER (1) // Current version number +#define RGB_BACKLIGHT_SETTINGS_VER_PREV (2) // Previous version number +#define RGB_BACKLIGHT_SETTINGS_VER (3) // Current version number //pervious settings must be copyed from previous rgb_backlight_settings.h file typedef struct { From 23d2bed66a4f96463a8bbf5c0f1b300cad6dac64 Mon Sep 17 00:00:00 2001 From: Dmitry422 Date: Tue, 25 Mar 2025 15:52:38 +0700 Subject: [PATCH 142/268] Remove Rainbow timer bug (after last code cleanup) --- applications/services/rgb_backlight/rgb_backlight.c | 4 +++- .../notification_settings/notification_settings_app.c | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/applications/services/rgb_backlight/rgb_backlight.c b/applications/services/rgb_backlight/rgb_backlight.c index 053654ec2..9dc60a5ec 100644 --- a/applications/services/rgb_backlight/rgb_backlight.c +++ b/applications/services/rgb_backlight/rgb_backlight.c @@ -154,7 +154,9 @@ void rainbow_timer_start(RGBBacklightApp* app) { // stop furi timer for rainbow void rainbow_timer_stop(RGBBacklightApp* app) { - furi_timer_stop(app->rainbow_timer); + if (furi_timer_is_running (app->rainbow_timer)){ + furi_timer_stop(app->rainbow_timer); + } } // if rgb_backlight_installed then apply rainbow colors to backlight and start/restart/stop rainbow_timer diff --git a/applications/settings/notification_settings/notification_settings_app.c b/applications/settings/notification_settings/notification_settings_app.c index dd2206dc2..2408d4e05 100644 --- a/applications/settings/notification_settings/notification_settings_app.c +++ b/applications/settings/notification_settings/notification_settings_app.c @@ -355,7 +355,6 @@ static void rgb_backlight_rainbow_changed(VariableItem* item) { variable_item_set_current_value_text(item, rgb_backlight_rainbow_mode_text[index]); app->notification->rgb_srv->settings->rainbow_mode = rgb_backlight_rainbow_mode_value[index]; - rainbow_timer_starter(app->notification->rgb_srv); rgb_backlight_settings_save(app->notification->rgb_srv->settings); // restore saved rgb backlight settings if we switch_off rainbow mode @@ -367,6 +366,9 @@ static void rgb_backlight_rainbow_changed(VariableItem* item) { rgb_backlight_set_led_static_color( 0, app->notification->rgb_srv->settings->led_0_color_index); rgb_backlight_update(app->notification->rgb_srv->settings->brightness); + rainbow_timer_stop (app->notification->rgb_srv); + } else { + rainbow_timer_starter(app->notification->rgb_srv); } } From ca2765a3fb1af0aa79410b163b1827ca1755c27b Mon Sep 17 00:00:00 2001 From: Dmitry422 Date: Tue, 25 Mar 2025 18:23:18 +0700 Subject: [PATCH 143/268] Small rgb_backlight settings menu changes --- .../rgb_backlight/rgb_backlight_settings.c | 4 +- .../rgb_backlight/rgb_backlight_settings.h | 1 + .../notification_settings_app.c | 68 +++++++++++++++++-- 3 files changed, 67 insertions(+), 6 deletions(-) diff --git a/applications/services/rgb_backlight/rgb_backlight_settings.c b/applications/services/rgb_backlight/rgb_backlight_settings.c index 681197284..60361923f 100644 --- a/applications/services/rgb_backlight/rgb_backlight_settings.c +++ b/applications/services/rgb_backlight/rgb_backlight_settings.c @@ -9,8 +9,8 @@ #define RGB_BACKLIGHT_SETTINGS_PATH INT_PATH(RGB_BACKLIGHT_SETTINGS_FILE_NAME) #define RGB_BACKLIGHT_SETTINGS_MAGIC (0x30) -#define RGB_BACKLIGHT_SETTINGS_VER_PREV (2) // Previous version number -#define RGB_BACKLIGHT_SETTINGS_VER (3) // Current version number +#define RGB_BACKLIGHT_SETTINGS_VER_PREV (3) // Previous version number +#define RGB_BACKLIGHT_SETTINGS_VER (4) // Current version number //pervious settings must be copyed from previous rgb_backlight_settings.h file typedef struct { diff --git a/applications/services/rgb_backlight/rgb_backlight_settings.h b/applications/services/rgb_backlight/rgb_backlight_settings.h index 3f3af005f..5136a16ca 100644 --- a/applications/services/rgb_backlight/rgb_backlight_settings.h +++ b/applications/services/rgb_backlight/rgb_backlight_settings.h @@ -10,6 +10,7 @@ typedef struct { float brightness; // static gradient mode settings + bool individual_led; uint8_t led_2_color_index; uint8_t led_1_color_index; uint8_t led_0_color_index; diff --git a/applications/settings/notification_settings/notification_settings_app.c b/applications/settings/notification_settings/notification_settings_app.c index 2408d4e05..9a123e014 100644 --- a/applications/settings/notification_settings/notification_settings_app.c +++ b/applications/settings/notification_settings/notification_settings_app.c @@ -175,6 +175,16 @@ const uint32_t rgb_backlight_rainbow_wide_value[RGB_BACKLIGHT_RAINBOW_WIDE_COUNT 50, }; +#define RGB_BACKLIGHT_INDIVIDUAL_LED_COUNT 2 +const char* const rgb_backlight_individual_led_text[RGB_BACKLIGHT_INDIVIDUAL_LED_COUNT] = { + "OFF", + "ON", +}; +const bool rgb_backlight_individual_value[RGB_BACKLIGHT_INDIVIDUAL_LED_COUNT] = { + false, + true, +}; + typedef enum { MainViewId, RGBViewId, @@ -291,7 +301,7 @@ static void rgb_backlight_installed_changed(VariableItem* item) { if(furi_hal_rtc_is_flag_set(FuriHalRtcFlagDebug)) { slide = 1; } - for(int i = slide; i < (slide + 8); i++) { + for(int i = slide; i < (slide + 9); i++) { VariableItem* t_item = variable_item_list_get(app->variable_item_list_rgb, i); if(index == 0) { variable_item_set_locked(t_item, true, "RGB\nOFF!"); @@ -301,6 +311,32 @@ static void rgb_backlight_installed_changed(VariableItem* item) { } } +static void individual_led_changed (VariableItem* item){ + NotificationAppSettings* app = variable_item_get_context(item); + uint8_t index = variable_item_get_current_value_index(item); + + variable_item_set_current_value_text (item,rgb_backlight_individual_led_text[index]); + app->notification->rgb_srv->settings->individual_led = index; + + // if individual led OFF - set led0 and led1 colors indexes as led2 index + if (index == 0) { + app->notification->rgb_srv->settings->led_1_color_index = app->notification->rgb_srv->settings->led_2_color_index; + app->notification->rgb_srv->settings->led_0_color_index = app->notification->rgb_srv->settings->led_2_color_index; + } + + // enable/disable led0 and led1 color settings + for(int i = 2; i < 4; i++) { + VariableItem* t_item = variable_item_list_get(app->variable_item_list_rgb, i); + if(index == 0) { + variable_item_set_locked(t_item, true, "Individual\nleds OFF!"); + } else { + variable_item_set_locked(t_item, false, "Individual\nleds OFF!"); + } + } + rgb_backlight_update(app->notification->rgb_srv->settings->brightness); + //rgb_backlight_settings_save(app->notification->rgb_srv->settings); +} + static void led_2_color_changed(VariableItem* item) { NotificationAppSettings* app = variable_item_get_context(item); uint8_t index = variable_item_get_current_value_index(item); @@ -550,6 +586,22 @@ static NotificationAppSettings* alloc_settings(void) { } // We (humans) are numbering LEDs from left to right as 1..3, but hardware have another order from right to left 2..0 + + // Individual led color switch + item = variable_item_list_add( + app->variable_item_list_rgb, + "Individual leds colors", + RGB_BACKLIGHT_INDIVIDUAL_LED_COUNT, + individual_led_changed, + app); + value_index = app->notification->rgb_srv->settings->individual_led; + variable_item_set_current_value_index(item, value_index); + variable_item_set_current_value_text(item, rgb_backlight_individual_led_text[value_index]); + variable_item_set_locked( + item, + (app->notification->rgb_srv->settings->rgb_backlight_installed == 0), + "RGB MOD \nOFF!"); + // led_1 color item = variable_item_list_add( app->variable_item_list_rgb, @@ -579,6 +631,10 @@ static NotificationAppSettings* alloc_settings(void) { item, (app->notification->rgb_srv->settings->rgb_backlight_installed == 0), "RGB MOD \nOFF!"); + variable_item_set_locked( + item, + (!app->notification->rgb_srv->settings->individual_led), + "Individual\nleds OFF!"); // led 3 color item = variable_item_list_add( @@ -594,11 +650,15 @@ static NotificationAppSettings* alloc_settings(void) { item, (app->notification->rgb_srv->settings->rgb_backlight_installed == 0), "RGB MOD \nOFF!"); + variable_item_set_locked( + item, + (!app->notification->rgb_srv->settings->individual_led), + "Individual\nleds OFF!"); // Rainbow mode item = variable_item_list_add( app->variable_item_list_rgb, - "Rainbow mode", + "Dynamic colors mode", RGB_BACKLIGHT_RAINBOW_MODE_COUNT, rgb_backlight_rainbow_changed, app); @@ -615,7 +675,7 @@ static NotificationAppSettings* alloc_settings(void) { item = variable_item_list_add( app->variable_item_list_rgb, - "Rainbow speed", + "Mode speed", RGB_BACKLIGHT_RAINBOW_SPEED_COUNT, rgb_backlight_rainbow_speed_changed, app); @@ -632,7 +692,7 @@ static NotificationAppSettings* alloc_settings(void) { item = variable_item_list_add( app->variable_item_list_rgb, - "Rainbow step", + "Colors step", RGB_BACKLIGHT_RAINBOW_STEP_COUNT, rgb_backlight_rainbow_step_changed, app); From 8df48988f54b49dad7213e2e910835adc5a8ab31 Mon Sep 17 00:00:00 2001 From: Dmitry422 Date: Tue, 25 Mar 2025 22:34:03 +0700 Subject: [PATCH 144/268] Revert "Small rgb_backlight settings menu changes" This reverts commit ca2765a3fb1af0aa79410b163b1827ca1755c27b. --- .../rgb_backlight/rgb_backlight_settings.c | 4 +- .../rgb_backlight/rgb_backlight_settings.h | 1 - .../notification_settings_app.c | 68 ++----------------- 3 files changed, 6 insertions(+), 67 deletions(-) diff --git a/applications/services/rgb_backlight/rgb_backlight_settings.c b/applications/services/rgb_backlight/rgb_backlight_settings.c index 60361923f..681197284 100644 --- a/applications/services/rgb_backlight/rgb_backlight_settings.c +++ b/applications/services/rgb_backlight/rgb_backlight_settings.c @@ -9,8 +9,8 @@ #define RGB_BACKLIGHT_SETTINGS_PATH INT_PATH(RGB_BACKLIGHT_SETTINGS_FILE_NAME) #define RGB_BACKLIGHT_SETTINGS_MAGIC (0x30) -#define RGB_BACKLIGHT_SETTINGS_VER_PREV (3) // Previous version number -#define RGB_BACKLIGHT_SETTINGS_VER (4) // Current version number +#define RGB_BACKLIGHT_SETTINGS_VER_PREV (2) // Previous version number +#define RGB_BACKLIGHT_SETTINGS_VER (3) // Current version number //pervious settings must be copyed from previous rgb_backlight_settings.h file typedef struct { diff --git a/applications/services/rgb_backlight/rgb_backlight_settings.h b/applications/services/rgb_backlight/rgb_backlight_settings.h index 5136a16ca..3f3af005f 100644 --- a/applications/services/rgb_backlight/rgb_backlight_settings.h +++ b/applications/services/rgb_backlight/rgb_backlight_settings.h @@ -10,7 +10,6 @@ typedef struct { float brightness; // static gradient mode settings - bool individual_led; uint8_t led_2_color_index; uint8_t led_1_color_index; uint8_t led_0_color_index; diff --git a/applications/settings/notification_settings/notification_settings_app.c b/applications/settings/notification_settings/notification_settings_app.c index 9a123e014..2408d4e05 100644 --- a/applications/settings/notification_settings/notification_settings_app.c +++ b/applications/settings/notification_settings/notification_settings_app.c @@ -175,16 +175,6 @@ const uint32_t rgb_backlight_rainbow_wide_value[RGB_BACKLIGHT_RAINBOW_WIDE_COUNT 50, }; -#define RGB_BACKLIGHT_INDIVIDUAL_LED_COUNT 2 -const char* const rgb_backlight_individual_led_text[RGB_BACKLIGHT_INDIVIDUAL_LED_COUNT] = { - "OFF", - "ON", -}; -const bool rgb_backlight_individual_value[RGB_BACKLIGHT_INDIVIDUAL_LED_COUNT] = { - false, - true, -}; - typedef enum { MainViewId, RGBViewId, @@ -301,7 +291,7 @@ static void rgb_backlight_installed_changed(VariableItem* item) { if(furi_hal_rtc_is_flag_set(FuriHalRtcFlagDebug)) { slide = 1; } - for(int i = slide; i < (slide + 9); i++) { + for(int i = slide; i < (slide + 8); i++) { VariableItem* t_item = variable_item_list_get(app->variable_item_list_rgb, i); if(index == 0) { variable_item_set_locked(t_item, true, "RGB\nOFF!"); @@ -311,32 +301,6 @@ static void rgb_backlight_installed_changed(VariableItem* item) { } } -static void individual_led_changed (VariableItem* item){ - NotificationAppSettings* app = variable_item_get_context(item); - uint8_t index = variable_item_get_current_value_index(item); - - variable_item_set_current_value_text (item,rgb_backlight_individual_led_text[index]); - app->notification->rgb_srv->settings->individual_led = index; - - // if individual led OFF - set led0 and led1 colors indexes as led2 index - if (index == 0) { - app->notification->rgb_srv->settings->led_1_color_index = app->notification->rgb_srv->settings->led_2_color_index; - app->notification->rgb_srv->settings->led_0_color_index = app->notification->rgb_srv->settings->led_2_color_index; - } - - // enable/disable led0 and led1 color settings - for(int i = 2; i < 4; i++) { - VariableItem* t_item = variable_item_list_get(app->variable_item_list_rgb, i); - if(index == 0) { - variable_item_set_locked(t_item, true, "Individual\nleds OFF!"); - } else { - variable_item_set_locked(t_item, false, "Individual\nleds OFF!"); - } - } - rgb_backlight_update(app->notification->rgb_srv->settings->brightness); - //rgb_backlight_settings_save(app->notification->rgb_srv->settings); -} - static void led_2_color_changed(VariableItem* item) { NotificationAppSettings* app = variable_item_get_context(item); uint8_t index = variable_item_get_current_value_index(item); @@ -586,22 +550,6 @@ static NotificationAppSettings* alloc_settings(void) { } // We (humans) are numbering LEDs from left to right as 1..3, but hardware have another order from right to left 2..0 - - // Individual led color switch - item = variable_item_list_add( - app->variable_item_list_rgb, - "Individual leds colors", - RGB_BACKLIGHT_INDIVIDUAL_LED_COUNT, - individual_led_changed, - app); - value_index = app->notification->rgb_srv->settings->individual_led; - variable_item_set_current_value_index(item, value_index); - variable_item_set_current_value_text(item, rgb_backlight_individual_led_text[value_index]); - variable_item_set_locked( - item, - (app->notification->rgb_srv->settings->rgb_backlight_installed == 0), - "RGB MOD \nOFF!"); - // led_1 color item = variable_item_list_add( app->variable_item_list_rgb, @@ -631,10 +579,6 @@ static NotificationAppSettings* alloc_settings(void) { item, (app->notification->rgb_srv->settings->rgb_backlight_installed == 0), "RGB MOD \nOFF!"); - variable_item_set_locked( - item, - (!app->notification->rgb_srv->settings->individual_led), - "Individual\nleds OFF!"); // led 3 color item = variable_item_list_add( @@ -650,15 +594,11 @@ static NotificationAppSettings* alloc_settings(void) { item, (app->notification->rgb_srv->settings->rgb_backlight_installed == 0), "RGB MOD \nOFF!"); - variable_item_set_locked( - item, - (!app->notification->rgb_srv->settings->individual_led), - "Individual\nleds OFF!"); // Rainbow mode item = variable_item_list_add( app->variable_item_list_rgb, - "Dynamic colors mode", + "Rainbow mode", RGB_BACKLIGHT_RAINBOW_MODE_COUNT, rgb_backlight_rainbow_changed, app); @@ -675,7 +615,7 @@ static NotificationAppSettings* alloc_settings(void) { item = variable_item_list_add( app->variable_item_list_rgb, - "Mode speed", + "Rainbow speed", RGB_BACKLIGHT_RAINBOW_SPEED_COUNT, rgb_backlight_rainbow_speed_changed, app); @@ -692,7 +632,7 @@ static NotificationAppSettings* alloc_settings(void) { item = variable_item_list_add( app->variable_item_list_rgb, - "Colors step", + "Rainbow step", RGB_BACKLIGHT_RAINBOW_STEP_COUNT, rgb_backlight_rainbow_step_changed, app); From a9288da9ba7ffa0dadc025ccf5ac4778e66dd692 Mon Sep 17 00:00:00 2001 From: Dmitry422 Date: Wed, 26 Mar 2025 08:11:52 +0700 Subject: [PATCH 145/268] RGB backlight bags and code cleanup --- .../notification_settings_app.c | 43 +++++++++++-------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/applications/settings/notification_settings/notification_settings_app.c b/applications/settings/notification_settings/notification_settings_app.c index 2408d4e05..4e14d2012 100644 --- a/applications/settings/notification_settings/notification_settings_app.c +++ b/applications/settings/notification_settings/notification_settings_app.c @@ -261,16 +261,19 @@ static void rgb_backlight_installed_changed(VariableItem* item) { variable_item_set_current_value_text(item, rgb_backlight_installed_text[index]); app->notification->rgb_srv->settings->rgb_backlight_installed = rgb_backlight_installed_value[index]; - rgb_backlight_settings_save(app->notification->rgb_srv->settings); + + app->notification->rgb_srv->settings->brightness = + app->notification->settings.display_brightness; // In case of user playing with rgb_backlight_installed swith: // if user swith_off rgb_backlight_installed (but may be he have mod installed) - // then force set default orange color - defence from stupid. + // then force set default orange color and stop rainbow timer if(index == 0) { rgb_backlight_set_led_static_color(2, 0); rgb_backlight_set_led_static_color(1, 0); rgb_backlight_set_led_static_color(0, 0); SK6805_update(); + rainbow_timer_stop(app->notification->rgb_srv); // start rainbow (if its Enabled) or set saved static colors if user swith_on rgb_backlight_installed switch } else { if(app->notification->rgb_srv->settings->rainbow_mode > 0) { @@ -299,6 +302,8 @@ static void rgb_backlight_installed_changed(VariableItem* item) { variable_item_set_locked(t_item, false, "RGB\nOFF!"); } } + + rgb_backlight_settings_save(app->notification->rgb_srv->settings); } static void led_2_color_changed(VariableItem* item) { @@ -313,6 +318,7 @@ static void led_2_color_changed(VariableItem* item) { rgb_backlight_set_led_static_color(2, index); rgb_backlight_update(app->notification->rgb_srv->settings->brightness); } + rgb_backlight_settings_save(app->notification->rgb_srv->settings); } @@ -355,10 +361,8 @@ static void rgb_backlight_rainbow_changed(VariableItem* item) { variable_item_set_current_value_text(item, rgb_backlight_rainbow_mode_text[index]); app->notification->rgb_srv->settings->rainbow_mode = rgb_backlight_rainbow_mode_value[index]; - rgb_backlight_settings_save(app->notification->rgb_srv->settings); - - // restore saved rgb backlight settings if we switch_off rainbow mode - if(app->notification->rgb_srv->settings->rainbow_mode == 0) { + // restore saved rgb backlight settings if we switch_off effects + if(index == 0) { rgb_backlight_set_led_static_color( 2, app->notification->rgb_srv->settings->led_2_color_index); rgb_backlight_set_led_static_color( @@ -366,10 +370,12 @@ static void rgb_backlight_rainbow_changed(VariableItem* item) { rgb_backlight_set_led_static_color( 0, app->notification->rgb_srv->settings->led_0_color_index); rgb_backlight_update(app->notification->rgb_srv->settings->brightness); - rainbow_timer_stop (app->notification->rgb_srv); + rainbow_timer_stop(app->notification->rgb_srv); } else { rainbow_timer_starter(app->notification->rgb_srv); } + + rgb_backlight_settings_save(app->notification->rgb_srv->settings); } static void rgb_backlight_rainbow_speed_changed(VariableItem* item) { @@ -381,8 +387,8 @@ static void rgb_backlight_rainbow_speed_changed(VariableItem* item) { rgb_backlight_rainbow_speed_value[index]; // save settings and restart timer with new speed value - rgb_backlight_settings_save(app->notification->rgb_srv->settings); rainbow_timer_starter(app->notification->rgb_srv); + rgb_backlight_settings_save(app->notification->rgb_srv->settings); } static void rgb_backlight_rainbow_step_changed(VariableItem* item) { @@ -404,6 +410,7 @@ static void rgb_backlight_rainbow_saturation_changed(VariableItem* item) { snprintf(valtext, sizeof(valtext), "%d", index); variable_item_set_current_value_text(item, valtext); app->notification->rgb_srv->settings->rainbow_saturation = index; + rgb_backlight_settings_save(app->notification->rgb_srv->settings); } @@ -414,9 +421,7 @@ static void rgb_backlight_rainbow_wide_changed(VariableItem* item) { variable_item_set_current_value_text(item, rgb_backlight_rainbow_wide_text[index]); app->notification->rgb_srv->settings->rainbow_wide = rgb_backlight_rainbow_wide_value[index]; - // save settings and restart timer with new speed value rgb_backlight_settings_save(app->notification->rgb_srv->settings); - rainbow_timer_starter(app->notification->rgb_srv); } // open rgb_settings_view if user press OK on first (index=0) menu string and (debug mode or rgb_backlight_installed is true) @@ -553,7 +558,7 @@ static NotificationAppSettings* alloc_settings(void) { // led_1 color item = variable_item_list_add( app->variable_item_list_rgb, - "LED 1 Color", + "Led 1 Color", rgb_backlight_get_color_count(), led_2_color_changed, app); @@ -568,7 +573,7 @@ static NotificationAppSettings* alloc_settings(void) { // led_2 color item = variable_item_list_add( app->variable_item_list_rgb, - "LED 2 Color", + "Led 2 Color", rgb_backlight_get_color_count(), led_1_color_changed, app); @@ -583,7 +588,7 @@ static NotificationAppSettings* alloc_settings(void) { // led 3 color item = variable_item_list_add( app->variable_item_list_rgb, - "LED 3 Color", + "Led 3 Color", rgb_backlight_get_color_count(), led_0_color_changed, app); @@ -595,10 +600,10 @@ static NotificationAppSettings* alloc_settings(void) { (app->notification->rgb_srv->settings->rgb_backlight_installed == 0), "RGB MOD \nOFF!"); - // Rainbow mode + // Efects item = variable_item_list_add( app->variable_item_list_rgb, - "Rainbow mode", + "Effects", RGB_BACKLIGHT_RAINBOW_MODE_COUNT, rgb_backlight_rainbow_changed, app); @@ -615,7 +620,7 @@ static NotificationAppSettings* alloc_settings(void) { item = variable_item_list_add( app->variable_item_list_rgb, - "Rainbow speed", + " . Speed", RGB_BACKLIGHT_RAINBOW_SPEED_COUNT, rgb_backlight_rainbow_speed_changed, app); @@ -632,7 +637,7 @@ static NotificationAppSettings* alloc_settings(void) { item = variable_item_list_add( app->variable_item_list_rgb, - "Rainbow step", + " . Color step", RGB_BACKLIGHT_RAINBOW_STEP_COUNT, rgb_backlight_rainbow_step_changed, app); @@ -649,7 +654,7 @@ static NotificationAppSettings* alloc_settings(void) { item = variable_item_list_add( app->variable_item_list_rgb, - "Saturation", + " . Saturation", 255, rgb_backlight_rainbow_saturation_changed, app); @@ -665,7 +670,7 @@ static NotificationAppSettings* alloc_settings(void) { item = variable_item_list_add( app->variable_item_list_rgb, - "Wave wide", + " . Wave wide", RGB_BACKLIGHT_RAINBOW_WIDE_COUNT, rgb_backlight_rainbow_wide_changed, app); From c86b6e4b56e9af8a3975f2fc1856caa306745929 Mon Sep 17 00:00:00 2001 From: Dmitry422 Date: Wed, 26 Mar 2025 10:43:45 +0700 Subject: [PATCH 146/268] Bump settings for new auto recreation after update --- applications/services/rgb_backlight/rgb_backlight_settings.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/applications/services/rgb_backlight/rgb_backlight_settings.c b/applications/services/rgb_backlight/rgb_backlight_settings.c index 681197284..b76c6598a 100644 --- a/applications/services/rgb_backlight/rgb_backlight_settings.c +++ b/applications/services/rgb_backlight/rgb_backlight_settings.c @@ -9,8 +9,8 @@ #define RGB_BACKLIGHT_SETTINGS_PATH INT_PATH(RGB_BACKLIGHT_SETTINGS_FILE_NAME) #define RGB_BACKLIGHT_SETTINGS_MAGIC (0x30) -#define RGB_BACKLIGHT_SETTINGS_VER_PREV (2) // Previous version number -#define RGB_BACKLIGHT_SETTINGS_VER (3) // Current version number +#define RGB_BACKLIGHT_SETTINGS_VER_PREV (4) // Previous version number +#define RGB_BACKLIGHT_SETTINGS_VER (5) // Current version number //pervious settings must be copyed from previous rgb_backlight_settings.h file typedef struct { From 77865586e13b2bd2d6c2352e2062949ef341d3cf Mon Sep 17 00:00:00 2001 From: Dmitry422 Date: Wed, 26 Mar 2025 18:51:36 +0700 Subject: [PATCH 147/268] Start working with "Night Shift" option --- .../services/notification/notification_app.h | 3 + .../notification_settings_app.c | 90 +++++++++++++++++++ 2 files changed, 93 insertions(+) diff --git a/applications/services/notification/notification_app.h b/applications/services/notification/notification_app.h index 798b01ab6..7ee421b31 100644 --- a/applications/services/notification/notification_app.h +++ b/applications/services/notification/notification_app.h @@ -45,6 +45,9 @@ typedef struct { uint32_t display_off_delay_ms; int8_t contrast; bool vibro_on; + float night_shift; + uint32_t night_shift_start; + uint32_t night_shift_end; } NotificationSettings; struct NotificationApp { diff --git a/applications/settings/notification_settings/notification_settings_app.c b/applications/settings/notification_settings/notification_settings_app.c index 4e14d2012..435a0087c 100644 --- a/applications/settings/notification_settings/notification_settings_app.c +++ b/applications/settings/notification_settings/notification_settings_app.c @@ -182,6 +182,66 @@ typedef enum { // --- RGB BACKLIGHT END --- + // --- NIGHT SHIFT --- + #define NIGHT_SHIFT_COUNT 7 + const char* const night_shift_text[NIGHT_SHIFT_COUNT] = { + "OFF", + "5%", + "10%" + "15%", + "20%", + "25%", + "30%" + + }; + const float night_shift_value[NIGHT_SHIFT_COUNT] = { + 0, 0.05, 0.1, 0.15, 0.2, 0.25, 0.3, + }; + + #define NIGHT_SHIFT_START_COUNT 14 + const char* const night_shift_start_text[NIGHT_SHIFT_START_COUNT] = { + "17:00", + "17:30", + "18:00" + "18:30", + "19:00", + "19:30", + "20:00", + "20:30", + "21:00", + "21:30", + "22:00", + "22:30", + "23:00", + "23:30", + }; + const uint32_t night_shift_start_value[NIGHT_SHIFT_START_COUNT] = { + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + }; + + #define NIGHT_SHIFT_END_COUNT 14 + const char* const night_shift_end_text[NIGHT_SHIFT_END_COUNT] = { + "05:00", + "05:30", + "06:00" + "06:30", + "07:00", + "07:30", + "08:00", + "08:30", + "09:00", + "09:30", + "10:00", + "10:30", + "11:00", + "11:30", + }; + const uint32_t night_shift_end_value[NIGHT_SHIFT_END_COUNT] = { + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + }; + + // --- NIGHT SHIFT END --- + static void contrast_changed(VariableItem* item) { NotificationAppSettings* app = variable_item_get_context(item); uint8_t index = variable_item_get_current_value_index(item); @@ -443,6 +503,11 @@ static uint32_t notification_app_rgb_settings_exit(void* context) { } //--- RGB BACKLIGHT END --- +// --- NIGHT SHIFT --- + + +// --- NIGHT SHIFT END --- + static uint32_t notification_app_settings_exit(void* context) { UNUSED(context); return VIEW_NONE; @@ -495,6 +560,31 @@ static NotificationAppSettings* alloc_settings(void) { variable_item_set_current_value_index(item, value_index); variable_item_set_current_value_text(item, delay_text[value_index]); + // --- NIGHT SHIFT --- + item = variable_item_list_add( + app->variable_item_list, "Night Shift", NIGHT_SHIFT_COUNT, night_shift_changed, app); + value_index = value_index_float( + app->notification->settings.night_shift, night_shift_value, NIGHT_SHIFT_COUNT); + variable_item_set_current_value_index(item, value_index); + variable_item_set_current_value_text(item, night_shift_text[value_index]); + + item = variable_item_list_add( + app->variable_item_list, " . Start", NIGHT_SHIFT_START_COUNT, night_shift_start_changed, app); + value_index = value_index_uint32( + app->notification->settings.night_shift_start, night_shift_start_value, NIGHT_SHIFT_START_COUNT); + variable_item_set_current_value_index(item, value_index); + variable_item_set_current_value_text(item, night_shift_start_text[value_index]); + + item = variable_item_list_add( + app->variable_item_list, " . End", NIGHT_SHIFT_END_COUNT, night_shift_end_changed, app); + value_index = value_index_uint32( + app->notification->settings.night_shift_end, night_shift_end_value, NIGHT_SHIFT_END_COUNT); + variable_item_set_current_value_index(item, value_index); + variable_item_set_current_value_text(item, night_shift_end_text[value_index]); + + // --- NIGHT SHIFT END--- + + item = variable_item_list_add( app->variable_item_list, "LED Brightness", BACKLIGHT_COUNT, led_changed, app); value_index = value_index_float( From 54adc38b3a3e259279d72b4e13dabd54a103f0d6 Mon Sep 17 00:00:00 2001 From: knrn64 <25254561+knrn64@users.noreply.github.com> Date: Wed, 26 Mar 2025 17:29:26 +0300 Subject: [PATCH 148/268] Documentation: new doc on Storage module and JS cleanup (#4161) - Add new doc on Storage module - Improve formatting in the JS section for better readability --- documentation/doxygen/js.dox | 7 +- documentation/js/js_badusb.md | 64 ++-- documentation/js/js_builtin.md | 46 ++- documentation/js/js_event_loop.md | 63 ++-- documentation/js/js_gpio.md | 46 ++- documentation/js/js_gui.md | 54 ++- documentation/js/js_gui__dialog.md | 40 +- documentation/js/js_gui__empty_screen.md | 2 +- documentation/js/js_gui__loading.md | 2 +- documentation/js/js_gui__submenu.md | 27 +- documentation/js/js_gui__text_box.md | 12 +- documentation/js/js_gui__text_input.md | 35 +- documentation/js/js_gui__widget.md | 8 +- documentation/js/js_math.md | 255 ++++++++----- documentation/js/js_notification.md | 20 +- documentation/js/js_serial.md | 70 ++-- documentation/js/js_storage.md | 445 +++++++++++++++++++++++ 17 files changed, 898 insertions(+), 298 deletions(-) create mode 100644 documentation/js/js_storage.md diff --git a/documentation/doxygen/js.dox b/documentation/doxygen/js.dox index 71eb37b17..7abde9389 100644 --- a/documentation/doxygen/js.dox +++ b/documentation/doxygen/js.dox @@ -16,12 +16,13 @@ Flipper Zero's built-in JavaScript engine enables you to run lightweight scripts ## JavaScript modules {#js_modules} - @subpage js_badusb — This module allows you to emulate a standard USB keyboard -- @subpage js_serial — The module for interaction with external devices via UART -- @subpage js_math — This module contains mathematical methods and constants -- @subpage js_notification — This module allows you to use LED, speaker and vibro for notifications - @subpage js_event_loop — The module for easy event-based developing - @subpage js_gpio — This module allows you to control GPIO pins - @subpage js_gui — This module allows you to use GUI (graphical user interface) +- @subpage js_math — This module contains mathematical methods and constants +- @subpage js_notification — This module allows you to use LED, speaker and vibro for notifications +- @subpage js_serial — The module for interaction with external devices via UART +- @subpage js_storage — The module for accessing the filesystem ## Examples {#js_examples} diff --git a/documentation/js/js_badusb.md b/documentation/js/js_badusb.md index df02dbdb0..d1d1e558d 100644 --- a/documentation/js/js_badusb.md +++ b/documentation/js/js_badusb.md @@ -4,16 +4,17 @@ let badusb = require("badusb"); ``` # Methods -## setup +## setup() Start USB HID with optional parameters. Should be called before all other methods. -### Parameters -Configuration object (optional): +**Parameters** + +Configuration object *(optional)*: - vid, pid (number): VID and PID values, both are mandatory - mfr_name (string): Manufacturer name (32 ASCII characters max), optional - prod_name (string): Product name (32 ASCII characters max), optional -### Examples: +**Examples** ```js // Start USB HID with default parameters badusb.setup(); @@ -23,10 +24,12 @@ badusb.setup({ vid: 0xAAAA, pid: 0xBBBB }); badusb.setup({ vid: 0xAAAA, pid: 0xBBBB, mfr_name: "Flipper Devices", prod_name: "Flipper Zero" }); ``` -## isConnected +
+ +## isConnected() Returns USB connection state. -### Example: +**Example** ```js if (badusb.isConnected()) { // Do something @@ -35,15 +38,18 @@ if (badusb.isConnected()) { } ``` -## press +
+ +## press() Press and release a key. -### Parameters +**Parameters** + Key or modifier name, key code. -See a list of key names below. +See a [list of key names below](#js_badusb_keynames). -### Examples: +**Examples** ```js badusb.press("a"); // Press "a" key badusb.press("A"); // SHIFT + "a" @@ -53,58 +59,70 @@ badusb.press(98); // Press key with HID code (dec) 98 (Numpad 0 / Insert) badusb.press(0x47); // Press key with HID code (hex) 0x47 (Scroll lock) ``` -## hold +
+ +## hold() Hold a key. Up to 5 keys (excluding modifiers) can be held simultaneously. -### Parameters +**Parameters** + Same as `press`. -### Examples: +**Examples** ```js badusb.hold("a"); // Press and hold "a" key badusb.hold("CTRL", "v"); // Press and hold CTRL + "v" combo ``` -## release +
+ +## release() Release a previously held key. -### Parameters +**Parameters** + Same as `press`. Release all keys if called without parameters. -### Examples: +**Examples** ```js badusb.release(); // Release all keys badusb.release("a"); // Release "a" key ``` -## print +
+ +## print() Print a string. -### Parameters +**Parameters** + - A string to print - *(optional)* Delay between key presses -### Examples: +**Examples** ```js badusb.print("Hello, world!"); // print "Hello, world!" badusb.print("Hello, world!", 100); // Add 100ms delay between key presses ``` +
-## println +## println() Same as `print` but ended with "ENTER" press. -### Parameters +**Parameters** + - A string to print - *(optional)* Delay between key presses -### Examples: +**Examples** ```js badusb.println("Hello, world!"); // print "Hello, world!" and press "ENTER" ``` +
-# Key names list +# Key names list {#js_badusb_keynames} ## Modifier keys diff --git a/documentation/js/js_builtin.md b/documentation/js/js_builtin.md index 9c59b9822..0a128859a 100644 --- a/documentation/js/js_builtin.md +++ b/documentation/js/js_builtin.md @@ -1,49 +1,65 @@ # Built-in methods {#js_builtin} -## require +## require() Load a module plugin. -### Parameters +**Parameters** - Module name -### Examples: +**Examples** ```js let serial = require("serial"); // Load "serial" module ``` -## delay -### Parameters +
+ +## delay() +**Parameters** - Delay value in ms -### Examples: +**Examples** ```js delay(500); // Delay for 500ms ``` -## print +
+ +## print() Print a message on a screen console. -### Parameters +**Parameters** The following argument types are supported: - String - Number - Bool - undefined -### Examples: +**Examples** ```js print("string1", "string2", 123); ``` +
-## console.log -## console.warn -## console.error -## console.debug +## console.log() + +
+ +## console.warn() + +
+ +## console.error() + +
+ +## console.debug() Same as `print`, but output to serial console only, with corresponding log level. -## to_string +
+ +## to_string() Convert a number to string with an optional base. -### Examples: +**Examples** ```js to_string(123) // "123" to_string(123, 16) // "0x7b" diff --git a/documentation/js/js_event_loop.md b/documentation/js/js_event_loop.md index 04192b88f..7da3f9341 100644 --- a/documentation/js/js_event_loop.md +++ b/documentation/js/js_event_loop.md @@ -1,27 +1,28 @@ # Event Loop module {#js_event_loop} -```js -let eventLoop = require("event_loop"); -``` - The event loop is central to event-based programming in many frameworks, and our JS subsystem is no exception. It is a good idea to familiarize yourself with the event loop first before using any of the advanced modules (e.g. GPIO and GUI). +```js +let eventLoop = require("event_loop"); +``` + ## Conceptualizing the event loop -If you ever wrote JavaScript before, you have definitely seen callbacks. It's -when a function accepts another function (usually an anonymous one) as one of -the arguments, which it will call later on, e.g. when an event happens or when +If you've ever written JavaScript code before, you've definitely seen callbacks. It's +when a function takes another function (usually an anonymous one) as one of +the arguments, which it will call later, e.g. when an event happens or when data becomes ready: ```js setTimeout(function() { console.log("Hello, World!") }, 1000); ``` -Many JavaScript engines employ a queue that the runtime fetches events from as +Many JavaScript engines employ a queue from which the runtime fetches events as they occur, subsequently calling the corresponding callbacks. This is done in a long-running loop, hence the name "event loop". Here's the pseudocode for a typical event loop: -```js + +\code{.js} while(loop_is_running()) { if(event_available_in_queue()) { let event = fetch_event_from_queue(); @@ -33,12 +34,14 @@ while(loop_is_running()) { sleep_until_any_event_becomes_available(); } } -``` +\endcode Most JS runtimes enclose the event loop within themselves, so that most JS -programmers does not even need to be aware of its existence. This is not the +programmers don't even need to be aware of its existence. This is not the case with our JS subsystem. +--- + # Example This is how one would write something similar to the `setTimeout` example above: ```js @@ -87,14 +90,18 @@ The first two arguments that get passed to our callback are: - The event item, used for events that have extra data. Timer events do not, they just produce `undefined`. +--- + # API reference -## `run` +## run() Runs the event loop until it is stopped with `stop`. -## `subscribe` +
+ +## subscribe() Subscribes a function to an event. -### Parameters +**Parameters** - `contract`: an event source identifier - `callback`: the function to call when the event happens - extra arguments: will be passed as extra arguments to the callback @@ -107,35 +114,45 @@ to `undefined`. The callback may return an array of the same length as the count of the extra arguments to modify them for the next time that the event handler is called. Any other returns values are discarded. -### Returns +**Returns** + A `SubscriptionManager` object: - `SubscriptionManager.cancel()`: unsubscribes the callback from the event -### Warning +**Warning** + Each event source may only have one callback associated with it. -## `stop` +
+ +## stop() Stops the event loop. -## `timer` +
+ +## timer() Produces an event source that fires with a constant interval either once or indefinitely. -### Parameters +**Parameters** - `mode`: either `"oneshot"` or `"periodic"` - `interval`: the timeout (for `"oneshot"`) timers or the period (for `"periodic"` timers) -### Returns +**Returns** + A `Contract` object, as expected by `subscribe`'s first parameter. -## `queue` +
+ +## queue() Produces a queue that can be used to exchange messages. -### Parameters +**Parameters** - `length`: the maximum number of items that the queue may contain -### Returns +**Returns** + A `Queue` object: - `Queue.send(message)`: - `message`: a value of any type that will be placed at the end of the queue diff --git a/documentation/js/js_gpio.md b/documentation/js/js_gpio.md index bb60ac5f3..59a5504b0 100644 --- a/documentation/js/js_gpio.md +++ b/documentation/js/js_gpio.md @@ -21,21 +21,26 @@ led.write(false); delay(1000); ``` +--- + # API reference -## `get` +## get Gets a `Pin` object that can be used to manage a pin. -### Parameters +**Parameters** - `pin`: pin identifier (examples: `"pc3"`, `7`, `"pa6"`, `3`) -### Returns +**Returns** + A `Pin` object. -## `Pin` object -### `Pin.init()` +
+ +## Pin object +### Pin.init() Configures a pin. -#### Parameters +**Parameters** - `mode`: `Mode` object: - `direction` (required): either `"in"` or `"out"` - `outMode` (required for `direction: "out"`): either `"open_drain"` or @@ -46,30 +51,41 @@ Configures a pin. `"rising"`, `"falling"` or `"both"` - `pull` (optional): either `"up"`, `"down"` or unset -### `Pin.write()` +
+ +### Pin.write() Writes a digital value to a pin configured with `direction: "out"`. -#### Parameters +**Parameters** - `value`: boolean logic level to write -### `Pin.read()` +
+ +### Pin.read() Reads a digital value from a pin configured with `direction: "in"` and any `inMode` except `"analog"`. -#### Returns -Boolean logic level +**Returns** -### `Pin.readAnalog()` +Boolean logic level. + +
+ +### Pin.readAnalog() Reads an analog voltage level in millivolts from a pin configured with `direction: "in"` and `inMode: "analog"`. -#### Returns +**Returns** + Voltage on pin in millivolts. -### `Pin.interrupt()` +
+ +### Pin.interrupt() Attaches an interrupt to a pin configured with `direction: "in"` and `inMode: "interrupt"` or `"event"`. -#### Returns +**Returns** + An event loop `Contract` object that identifies the interrupt event source. The event does not produce any extra data. diff --git a/documentation/js/js_gui.md b/documentation/js/js_gui.md index c30447c54..2bb2a2eee 100644 --- a/documentation/js/js_gui.md +++ b/documentation/js/js_gui.md @@ -18,6 +18,8 @@ GUI module has several submodules: - @subpage js_gui__dialog — Dialog with up to 3 options - @subpage js_gui__widget — Displays a combination of custom elements on one screen +--- + ## Conceptualizing GUI ### Event loop It is highly recommended to familiarize yourself with the event loop first @@ -78,7 +80,9 @@ a GUI application: | ViewDispatcher | Common UI elements that fit with the overall look of the system | ✅ | | SceneManager | Additional navigation flow management for complex applications | ❌ | -# Example +--- + +## Example An example with three different views using the ViewDispatcher approach: ```js let eventLoop = require("event_loop"); @@ -123,48 +127,64 @@ gui.viewDispatcher.switchTo(views.demos); eventLoop.run(); ``` +--- + # API reference -## `viewDispatcher` +## viewDispatcher The `viewDispatcher` constant holds the `ViewDispatcher` singleton. -### `viewDispatcher.switchTo(view)` +
+ +### viewDispatcher.switchTo(view) Switches to a view, giving it control over the display and input. -#### Parameters +**Parameters** - `view`: the `View` to switch to -### `viewDispatcher.sendTo(direction)` +
+ +### viewDispatcher.sendTo(direction) Sends the viewport that the dispatcher manages to the front of the stackup (effectively making it visible), or to the back (effectively making it invisible). -#### Parameters +**Parameters** - `direction`: either `"front"` or `"back"` -### `viewDispatcher.sendCustom(event)` +
+ +### viewDispatcher.sendCustom(event) Sends a custom number to the `custom` event handler. -#### Parameters +**Parameters** - `event`: number to send -### `viewDispatcher.custom` +
+ +### viewDispatcher.custom An event loop `Contract` object that identifies the custom event source, triggered by `ViewDispatcher.sendCustom(event)`. -### `viewDispatcher.navigation` +
+ +### viewDispatcher.navigation An event loop `Contract` object that identifies the navigation event source, triggered when the back key is pressed. -## `ViewFactory` -When you import a module implementing a view, a `ViewFactory` is instantiated. -For example, in the example above, `loadingView`, `submenuView` and `emptyView` -are view factories. +
-### `ViewFactory.make()` +## ViewFactory +When you import a module implementing a view, a `ViewFactory` is instantiated. For example, in the example above, `loadingView`, `submenuView` and `emptyView` are view factories. + +
+ +### ViewFactory.make() Creates an instance of a `View`. -### `ViewFactory.make(props)` +
+ +### ViewFactory.make(props) Creates an instance of a `View` and assigns initial properties from `props`. -#### Parameters +**Parameters** - `props`: simple key-value object, e.g. `{ header: "Header" }` diff --git a/documentation/js/js_gui__dialog.md b/documentation/js/js_gui__dialog.md index dce45c49d..4f26cfa1b 100644 --- a/documentation/js/js_gui__dialog.md +++ b/documentation/js/js_gui__dialog.md @@ -11,42 +11,24 @@ let dialogView = require("gui/dialog"); ``` This module depends on the `gui` module, which in turn depends on the -`event_loop` module, so they _must_ be imported in this order. It is also +`event_loop` module, so they **must** be imported in this order. It is also recommended to conceptualize these modules first before using this one. # Example For an example, refer to the `gui.js` example script. # View props -## `header` -Text that appears in bold at the top of the screen. -Type: `string` - -## `text` -Text that appears in the middle of the screen. - -Type: `string` - -## `left` -Text for the left button. If unset, the left button does not show up. - -Type: `string` - -## `center` -Text for the center button. If unset, the center button does not show up. - -Type: `string` - -## `right` -Text for the right button. If unset, the right button does not show up. - -Type: `string` +| **Prop** | **Type** | **Description** | +|------------|-----------|----------------------------------------------------------------| +| `header` | string | Text that appears in bold at the top of the screen. | +| `text` | string | Text that appears in the middle of the screen. | +| `left` | string | Text for the left button. If unset, the left button does not show up. | +| `center` | string | Text for the center button. If unset, the center button does not show up. | +| `right` | string | Text for the right button. If unset, the right button does not show up. | # View events -## `input` -Fires when the user presses on either of the three possible buttons. The item -contains one of the strings `"left"`, `"center"` or `"right"` depending on the -button. -Item type: `string` +| Item | Type | Description | +|----------|--------|-----------------------------------------------------------------------------| +| `input` | `string`| Fires when the user presses on either of the three possible buttons. The item contains one of the strings `"left"`, `"center"`, or `"right"` depending on the button. | diff --git a/documentation/js/js_gui__empty_screen.md b/documentation/js/js_gui__empty_screen.md index 68b7e3124..6ee6ba20e 100644 --- a/documentation/js/js_gui__empty_screen.md +++ b/documentation/js/js_gui__empty_screen.md @@ -11,7 +11,7 @@ let emptyView = require("gui/empty_screen"); ``` This module depends on the `gui` module, which in turn depends on the -`event_loop` module, so they _must_ be imported in this order. It is also +`event_loop` module, so they **must** be imported in this order. It is also recommended to conceptualize these modules first before using this one. # Example diff --git a/documentation/js/js_gui__loading.md b/documentation/js/js_gui__loading.md index 0d3c18644..35ea987c1 100644 --- a/documentation/js/js_gui__loading.md +++ b/documentation/js/js_gui__loading.md @@ -11,7 +11,7 @@ let loadingView = require("gui/loading"); ``` This module depends on the `gui` module, which in turn depends on the -`event_loop` module, so they _must_ be imported in this order. It is also +`event_loop` module, so they **must** be imported in this order. It is also recommended to conceptualize these modules first before using this one. # Example diff --git a/documentation/js/js_gui__submenu.md b/documentation/js/js_gui__submenu.md index 755d87d8b..9105330a2 100644 --- a/documentation/js/js_gui__submenu.md +++ b/documentation/js/js_gui__submenu.md @@ -11,26 +11,19 @@ let submenuView = require("gui/submenu"); ``` This module depends on the `gui` module, which in turn depends on the -`event_loop` module, so they _must_ be imported in this order. It is also +`event_loop` module, so they **must** be imported in this order. It is also recommended to conceptualize these modules first before using this one. -# Example -For an example, refer to the GUI example. +## View props -# View props -## `header` -A single line of text that appears above the list. +| Property | Type | Description | +|----------|-----------|-------------------------------------------| +| `header` | `string` | A single line of text that appears above the list. | +| `items` | `string[]`| The list of options. | -Type: `string` -## `items` -The list of options. +## View events -Type: `string[]` - -# View events -## `chosen` -Fires when an entry has been chosen by the user. The item contains the index of -the entry. - -Item type: `number` +| Item | Type | Description | +|----------|---------|---------------------------------------------------------------| +| `chosen` | `number`| Fires when an entry has been chosen by the user. The item contains the index of the entry. | diff --git a/documentation/js/js_gui__text_box.md b/documentation/js/js_gui__text_box.md index 62828a37f..cdbeec7b7 100644 --- a/documentation/js/js_gui__text_box.md +++ b/documentation/js/js_gui__text_box.md @@ -11,14 +11,14 @@ let textBoxView = require("gui/text_box"); ``` This module depends on the `gui` module, which in turn depends on the -`event_loop` module, so they _must_ be imported in this order. It is also +`event_loop` module, so they **must** be imported in this order. It is also recommended to conceptualize these modules first before using this one. -# Example +## Example For an example, refer to the `gui.js` example script. -# View props -## `text` -Text to show in the text box. +## View props -Type: `string` +| Prop | Type | Description | +|----------|---------|------------------------------------| +| `text` | `string`| Text to show in the text box. | diff --git a/documentation/js/js_gui__text_input.md b/documentation/js/js_gui__text_input.md index 44752ab55..3b8aafaad 100644 --- a/documentation/js/js_gui__text_input.md +++ b/documentation/js/js_gui__text_input.md @@ -11,33 +11,22 @@ let textInputView = require("gui/text_input"); ``` This module depends on the `gui` module, which in turn depends on the -`event_loop` module, so they _must_ be imported in this order. It is also +`event_loop` module, so they **must** be imported in this order. It is also recommended to conceptualize these modules first before using this one. -# Example +## Example For an example, refer to the `gui.js` example script. -# View props -## `minLength` -The shortest allowed text length. +## View props -Type: `number` +| Prop | Type | Description | +|-------------|--------|--------------------------------------------------| +| `minLength` | `number` | The shortest allowed text length. | +| `maxLength` | `number` | The longest allowed text length.
Default: `32` | +| `header` | `string` | A single line of text that appears above the keyboard. | -## `maxLength` -The longest allowed text length. +## View events -Type: `number` - -Default: `32` - -## `header` -A single line of text that appears above the keyboard. - -Type: `string` - -# View events -## `input` -Fires when the user selects the "Save" button and the text matches the length -constrained by `minLength` and `maxLength`. - -Item type: `string` +| Item | Type | Description | +|-------------|--------|--------------------------------------------------| +| `input` | `string` | Fires when the user selects the "Save" button and the text matches the length constrained by `minLength` and `maxLength`. | diff --git a/documentation/js/js_gui__widget.md b/documentation/js/js_gui__widget.md index f0409e87b..2c31327d2 100644 --- a/documentation/js/js_gui__widget.md +++ b/documentation/js/js_gui__widget.md @@ -11,14 +11,14 @@ let widgetView = require("gui/widget"); ``` This module depends on the `gui` module, which in turn depends on the -`event_loop` module, so they _must_ be imported in this order. It is also +`event_loop` module, so they **must** be imported in this order. It is also recommended to conceptualize these modules first before using this one. -# Example +## Example For an example, refer to the `gui.js` example script. -# View props +## View props This view does not have any props. -# Children +## Children This view has the elements as its children. diff --git a/documentation/js/js_math.md b/documentation/js/js_math.md index cab9ac0c7..78357bbd7 100644 --- a/documentation/js/js_math.md +++ b/documentation/js/js_math.md @@ -5,6 +5,7 @@ The module contains mathematical methods and constants. Call the `require` funct ```js let math = require("math"); ``` + # Constants ## PI @@ -17,204 +18,248 @@ The number e (Euler's number) = 2.71828182845904523536028747135266250. The smallest number that satisfies the condition: 1.0 + EPSILON != 1.0. EPSILON = 2.2204460492503131e-16. +
+ +--- + # Methods -## abs +## abs() Return the absolute value of a number. -### Parameters +**Parameters** - x: A number -### Returns +**Returns** + The absolute value of `x`. If `x` is negative (including -0), returns `-x`. Otherwise, returns `x`. The result is therefore always a positive number or 0. -### Example +**Example** ```js math.abs(-5); // 5 ``` -## acos +
+ +## acos() Return the inverse cosine (in radians) of a number. -### Parameters +**Parameters** - x: A number between -1 and 1, inclusive, representing the angle's cosine value -### Returns +**Returns** + The inverse cosine (angle in radians between 0 and π, inclusive) of `x`. If `x` is less than -1 or greater than 1, returns `NaN`. -### Example +**Example** ```js math.acos(-1); // 3.141592653589793 ``` -## acosh +
+ +## acosh() Return the inverse hyperbolic cosine of a number. -### Parameters +**Parameters** - x: A number greater than or equal to 1 -### Returns +**Returns** + The inverse hyperbolic cosine of `x`. -### Example +**Example** ```js math.acosh(1); // 0 ``` -## asin +
+ +## asin() Return the inverse sine (in radians) of a number. -### Parameters +**Parameters** - x: A number between -1 and 1, inclusive, representing the angle's sine value -### Returns +**Returns** + The inverse sine (angle in radians between -𝜋/2 and 𝜋/2, inclusive) of `x`. -### Example +**Example** ```js math.asin(0.5); // 0.5235987755982989 ``` -## asinh +
+ +## asinh() Return the inverse hyperbolic sine of a number. -### Parameters +**Parameters** - x: A number -### Returns +**Returns** + The inverse hyperbolic sine of `x`. -### Example +**Example** ```js math.asinh(1); // 0.881373587019543 ``` -## atan +
+ +## atan() Return the inverse tangent (in radians) of a number. -### Parameters +**Parameters** - x: A number -### Returns +**Returns** + The inverse tangent (angle in radians between -𝜋/2 and 𝜋/2, inclusive) of `x`. -### Example +**Example** ```js math.atan(1); // 0.7853981633974483 ``` -## atan2 +
+ +## atan2() Return the angle in the plane (in radians) between the positive x-axis and the ray from (0, 0) to the point (x, y), for math.atan2(y, x). -### Parameters +**Parameters** - y: The y coordinate of the point - x: The x coordinate of the point -### Returns +**Returns** + The angle in radians (between -π and π, inclusive) between the positive x-axis and the ray from (0, 0) to the point (x, y). -### Example +**Example** ```js math.atan2(90, 15); // 1.4056476493802699 ``` -## atanh +
+ +## atanh() The method returns the inverse hyperbolic tangent of a number. -### Parameters +**Parameters** - x: A number between -1 and 1, inclusive -### Returns +**Returns** + The inverse hyperbolic tangent of `x`. -### Example +**Example** ```js math.atanh(0.5); // 0.5493061443340548 ``` -## cbrt +
+ +## cbrt() Return the cube root of a number. -### Parameters +**Parameters** - x: A number -### Returns +**Returns** + The cube root of `x`. -### Example +**Example** ```js math.cbrt(2); // 1.2599210498948732 ``` -## ceil +
+ +## ceil() Round up and return the smallest integer greater than or equal to a given number. -### Parameters +**Parameters** - x: A number -### Returns +**Returns** + The smallest integer greater than or equal to `x`. It's the same value as `-math.floor(-x)`. -### Example +**Example** ```js math.ceil(-7.004); // -7 math.ceil(7.004); // 8 ``` -## clz32 +
+ +## clz32() Return the number of leading zero bits in the 32-bit binary representation of a number. -### Parameters +**Parameters** - x: A number -### Returns +**Returns** + The number of leading zero bits in the 32-bit binary representation of `x`. -### Example +**Example** ```js math.clz32(1); // 31 math.clz32(1000); // 22 ``` -## cos +
+ +## cos() Return the cosine of a number in radians. -### Parameters +**Parameters** - x: A number representing an angle in radians -### Returns +**Returns** + The cosine of `x`, between -1 and 1, inclusive. -### Example +**Example** ```js math.cos(math.PI); // -1 ``` -## exp +
+ +## exp() Return e raised to the power of a number. -### Parameters +**Parameters** - x: A number -### Returns +**Returns** + A nonnegative number representing `e^x`, where `e` is the base of the natural logarithm. -### Example +**Example** ```js math.exp(0); // 1 math.exp(1); // 2.718281828459045 ``` -## floor +
+ +## floor() Round down and return the largest integer less than or equal to a given number. -### Parameters +**Parameters** - x: A number -### Returns +**Returns** + The largest integer smaller than or equal to `x`. It's the same value as `-math.ceil(-x)`. -### Example +**Example** ```js math.floor(-45.95); // -46 math.floor(-45.05); // -46 @@ -224,137 +269,165 @@ math.floor(45.05); // 45 math.floor(45.95); // 45 ``` -## isEqual +
+ +## isEqual() Return true if the difference between numbers `a` and `b` is less than the specified parameter `e`. -### Parameters +**Parameters** - a: A number a - b: A number b - e: An epsilon parameter -### Returns +**Returns** + True if the difference between numbers `a` and `b` is less than the specified parameter `e`. Otherwise, false. -### Example +**Example** ```js math.isEqual(1.4, 1.6, 0.2); // false math.isEqual(3.556, 3.555, 0.01); // true ``` -## max +
+ +## max() Return the largest of two numbers given as input parameters. -### Parameters +**Parameters** - a: A number a - b: A number b -### Returns +**Returns** + The largest of the given numbers. -### Example +**Example** ```js math.max(10, 20); // 20 math.max(-10, -20); // -10 ``` -## min +
+ +## min() Return the smallest of two numbers given as input parameters. -### Parameters +**Parameters** - a: A number a - b: A number b -### Returns +**Returns** + The smallest of the given numbers. -### Example +**Example** ```js math.min(10, 20); // 10 math.min(-10, -20); // -20 ``` -## pow +
+ +## pow() Return the value of a base raised to a power. -### Parameters +**Parameters** - base: The base number - exponent: The exponent number -### Returns +**Returns** + A number representing base taken to the power of exponent. -### Example +**Example** ```js math.pow(7, 2); // 49 math.pow(7, 3); // 343 math.pow(2, 10); // 1024 ``` -## random +
+ +## random() Return a floating-point, pseudo-random number that's greater than or equal to 0 and less than 1, with approximately uniform distribution over that range — which you can then scale to your desired range. -### Returns +**Returns** + A floating-point, pseudo-random number between 0 (inclusive) and 1 (exclusive). -### Example +**Example** ```js let num = math.random(); ``` -## sign +
+ +## sign() Return 1 or -1, indicating the sign of the number passed as argument. -### Parameters +**Parameters** - x: A number -### Returns +**Returns** + -1 if the number is less than 0, and 1 otherwise. -### Example +**Example** ```js math.sign(3); // 1 math.sign(0); // 1 math.sign(-3); // -1 ``` -## sin +
+ +## sin() Return the sine of a number in radians. -### Parameters +**Parameters** - x: A number representing an angle in radians -### Returns +**Returns** + The sine of `x`, between -1 and 1, inclusive. -### Example +**Example** ```js math.sin(math.PI / 2); // 1 ``` -## sqrt +
+ +## sqrt() Return the square root of a number. -### Parameters +**Parameters** - x: A number greater than or equal to 0 -### Returns +**Returns** + + The square root of `x`, a nonnegative number. If `x` < 0, script will fail with an error. -### Example +**Example** ```js math.sqrt(25); // 5 ``` -## trunc +
+ +## trunc() Return the integer part of a number by removing any fractional digits. -### Parameters +**Parameters** - x: A number -### Returns +**Returns** + The integer part of `x`. -### Example +**Example** ```js math.trunc(-1.123); // -1 math.trunc(0.123); // 0 diff --git a/documentation/js/js_notification.md b/documentation/js/js_notification.md index 543df3504..753f4c9e9 100644 --- a/documentation/js/js_notification.md +++ b/documentation/js/js_notification.md @@ -3,32 +3,36 @@ ```js let notify = require("notification"); ``` -# Methods +## Methods -## success +### success() "Success" flipper notification message. -### Examples: +**Example** ```js notify.success(); ``` -## error +
+ +### error() "Error" flipper notification message. -### Examples: +**Example** ```js notify.error(); ``` -## blink +
+ +### blink() Blink notification LED. -### Parameters +**Parameters** - Blink color (blue/red/green/yellow/cyan/magenta) - Blink type (short/long) -### Examples: +**Examples** ```js notify.blink("red", "short"); // Short blink of red LED notify.blink("green", "short"); // Long blink of green LED diff --git a/documentation/js/js_serial.md b/documentation/js/js_serial.md index ffaee89bc..01a72b97b 100644 --- a/documentation/js/js_serial.md +++ b/documentation/js/js_serial.md @@ -5,78 +5,99 @@ let serial = require("serial"); ``` # Methods -## setup +## setup() Configure serial port. Should be called before all other methods. -### Parameters +**Parameters** + - Serial port name (usart, lpuart) - Baudrate -### Examples: +**Example** + ```js // Configure LPUART port with baudrate = 115200 serial.setup("lpuart", 115200); ``` -## write +
+ +## write() Write data to serial port. -### Parameters +**Parameters** + One or more arguments of the following types: - A string - Single number, each number is interpreted as a byte - Array of numbers, each number is interpreted as a byte - ArrayBuffer or DataView -### Examples: +**Example** + ```js serial.write(0x0a); // Write a single byte 0x0A serial.write("Hello, world!"); // Write a string serial.write("Hello, world!", [0x0d, 0x0a]); // Write a string followed by two bytes ``` -## read +
+ +## read() Read a fixed number of characters from serial port. -### Parameters +**Parameters** + - Number of bytes to read - *(optional)* Timeout value in ms -### Returns +**Returns** + A sting of received characters or undefined if nothing was received before timeout. -### Examples: +**Example** + ```js serial.read(1); // Read a single byte, without timeout serial.read(10, 5000); // Read 10 bytes, with 5s timeout ``` -## readln +
+ +## readln() Read from serial port until line break character. -### Parameters +**Parameters** + *(optional)* Timeout value in ms. -### Returns +**Returns** + A sting of received characters or undefined if nothing was received before timeout. -### Examples: +**Example** + ```js serial.readln(); // Read without timeout serial.readln(5000); // Read with 5s timeout ``` -## readBytes +
+ +## readBytes() Read from serial port until line break character. -### Parameters +**Parameters** + - Number of bytes to read - *(optional)* Timeout value in ms -### Returns +**Returns** + ArrayBuffer with received data or undefined if nothing was received before timeout. -### Examples: +**Example** + ```js serial.readBytes(4); // Read 4 bytes, without timeout @@ -84,19 +105,24 @@ serial.readBytes(4); // Read 4 bytes, without timeout serial.readBytes(1, 0); ``` -## expect +
+ +## expect() Search for a string pattern in received data stream. -### Parameters +**Parameters** + - Single argument or array of the following types: - A string - Array of numbers, each number is interpreted as a byte - *(optional)* Timeout value in ms -### Returns +**Returns** + Index of matched pattern in input patterns list, undefined if nothing was found. -### Examples: +**Example** + ```js // Wait for root shell prompt with 1s timeout, returns 0 if it was received before timeout, undefined if not serial.expect("# ", 1000); diff --git a/documentation/js/js_storage.md b/documentation/js/js_storage.md new file mode 100644 index 000000000..acfa4e331 --- /dev/null +++ b/documentation/js/js_storage.md @@ -0,0 +1,445 @@ +# Storage module {#js_storage} + +The module allows you to access files and directories on the Flipper Zero filesystems. Call the `require` function to load the module before first using its methods: + +```js +let storage = require("storage"); +``` + +## Paths + +To work with files and folders, you'll need to specify paths to them. File paths have the following structure: + +``` +/ext/example_subdir_1/example_subdir_2/example_file.txt +\____________________________________/ \__________/ \_/ + dirPath fileName fileExt +``` + +* **dirPath** — directory path starting with `/int/` (small storage in the MCU's flash memory) or `/ext/` (microSD card storage). Specify the sub-directories containing the file using `/` as a separator between directory names. +* **fileName** — file name. +* **fileExt** — file extension (separated from the file name by a period). + +--- + +# Structures + +## FsInfo + +Filesystem information structure. + +**Fields** + +- totalSpace: Total size of the filesystem, in bytes +- freeSpace: Free space in the filesystem, in bytes + +
+ +## FileInfo + +File information structure. + +**Fields** + +- path: Full path (e.g. "/ext/test", returned by `stat`) or file name (e.g. "test", returned by `readDirectory`) +- isDirectory: Returns `true` if path leads to a directory (not a file) +- size: File size in bytes, or 0 in the case of directories +- accessTime: Time of last access as a UNIX timestamp + +--- + +# Classes + +## File + +This class implements methods for working with file. To get an object of the File class, use the `OpenFile` method. + +
+ +### close() + +Closes the file. After this method is called, all other operations related to this file become unavailable. + +**Returns** + +`true` on success, `false` on failure. + +
+ +### isOpen() + +**Returns** + +`true` if file is currently opened, `false` otherwise. + +
+ +### read() + +Reads bytes from a file opened in read-only or read-write mode. + +**Parameters** + +- mode: The data type to interpret the bytes as: a `string` decoded from ASCII data (`"ascii"`), or an `ArrayBuf` (`"binary"`) +- bytes: How many bytes to read from the file + +**Returns** + +An `ArrayBuf` if the mode is `"binary"`, a `string` if the mode is `ascii`. The number of bytes that was actually read may be fewer than requested. + +
+ +### write() + +Writes bytes to a file opened in write-only or read-write mode. + +**Parameters** + +- data: The data to write: a string that will be ASCII-encoded, or an ArrayBuf + +**Returns** + +The amount of bytes that was actually written. + +
+ +### seekRelative() + +Moves the R/W pointer forward. + +**Parameters** + +- bytes: How many bytes to move the pointer forward by + +**Returns** + +`true` on success, `false` on failure. + +
+ +### seekAbsolute() + +Moves the R/W pointer to an absolute position inside the file. + +**Parameters** + +- bytes: The position inside the file + +**Returns** + +`true` on success, `false` on failure. + +
+ +### tell() + +Gets the absolute position of the R/W pointer in bytes. + +**Returns** + +The absolute current position in the file. + +
+ +### truncate() + +Discards the data after the current position of the R/W pointer in a file opened in either write-only or read-write mode. + +**Returns** + +`true` on success, `false` on failure. + +
+ +### size() + +**Returns** + +The total size of the file in bytes. + +
+ +### eof() + +Detects whether the R/W pointer has reached the end of the file. + +**Returns** + +`true` if end of file reached, `false` otherwise. + +
+ +### copyTo() + +Copies bytes from the R/W pointer in the current file to the R/W pointer in another file. + +**Parameters** + +- dest: The file to copy the bytes into +- bytes: The number of bytes to copy + +**Returns** + +`true` on success, `false` on failure. + +--- + +# Methods + +## arePathsEqual() + +Determines whether the two paths are equivalent. Respects filesystem-defined path equivalence rules. + +**Parameters** + +- path1: The first path for comparison +- path2: The second path for comparison + +**Returns** + +`true` if path1 and path2 are equals, `false` otherwise. + +
+ +## isSubpathOf() + +Determines whether a path is a subpath of another path. Respects filesystem-defined path equivalence rules. + +**Parameters** + +- parentPath: The parent path +- childPath: The child path + +**Returns** + +`true` if path1 and path2 are equals, `false` otherwise. + +
+ +## fileOrDirExists() + +Detects whether a file or a directory exists. + +**Parameters** + +- path: The path to the file or directory + +**Returns** + +`true` if file/directory exists, `false` otherwise. + +**Example** +```js +if (storage.fileOrDirExists("/ext/test_dir")) { + print("Test directory exists"); +} +``` + +
+ +## fsInfo() + +Fetches generic information about a filesystem. + +**Parameters** + +- filesystem: The path to the filesystem (e.g. `"/ext"` or `"/int"`) + +**Returns** + +A `fsInfo` structure or `undefined` on failure. + +**Example** +```js +let fsinfo = storage.fsInfo("/ext"); +if (fsinfo === undefined) { + print("Filesystem access error"); +} else { + print("Free space on the /ext filesystem:", fsinfo.freeSpace); +} +``` + +
+ +## stat() + +Acquires metadata about a file or directory. + +**Parameters** + +- path: The path to the file or directory + +**Returns** + +A `FileInfo` structure or `undefined` on failure. + +**Example** +```js +let finfo = storage.stat("/ext/test_file.txt"); +if (finfo === undefined) { + print("File not found"); +} else { + print("File size:", finfo.size); +} +``` + +
+ +## directoryExists() + +Detects whether a directory exists. + +**Parameters** + +- path: The path to the directory + +**Returns** + +`true` if directory exists, `false` otherwise. + +
+ +## makeDirectory() + +Creates an empty directory. + +**Parameters** + +- path: The path to the new directory + +**Returns** + +`true` on success, `false` on failure. + +
+ +## readDirectory() + +Reads the list of files in a directory. + +**Parameters** + +- path: The path to the directory + +**Returns** + +Array of `FileInfo` structures with directory entries, or `undefined` on failure. + +
+ +## nextAvailableFilename() + +Chooses the next available filename with a numeric suffix in a directory. +``` +/ext/example_dir/example_file123.txt +\______________/ \__________/\_/ \_/ + dirPath fileName | | + | +--- fileExt + +------- suffix selected by this function +``` + +**Parameters** + +- dirPath: The directory to look in +- fileName: The base of the filename (the part before the numeric suffix) +- fileExt: The extension of the filename (the part after the numeric suffix) +- maxLen: The maximum length of the filename with the numeric suffix + +**Returns** + +The base of the filename with the next available numeric suffix, without the extension or the base directory. + +
+ +## copy() + +Copies a file or recursively copies a possibly non-empty directory. + +**Parameters** + +- oldPath: The original path to the file or directory +- newPath: The new path that the copy of the file or directory will be accessible under + +**Returns** + +`true` on success, `false` on failure. + +**Example** +```js +if (storage.copy("/ext/src_file.txt", "/ext/dst_file.txt")) { + print("File copied"); +} +``` + +
+ +## rename() + +Renames or moves a file or directory. + +**Parameters** + +- oldPath: The old path to the file or directory +- newPath: The new path that the file or directory will become accessible + +**Returns** + +`true` on success, `false` on failure. + +**Example** +```js +if (storage.rename("/ext/src_file.txt", "/ext/dst_file.txt")) { + print("File moved"); +} +``` + +
+ +## remove() + +Removes a file or an empty directory. + +**Parameters** + +- path: The path to the file or directory + +**Returns** + +`true` on success, `false` on failure. + +**Example** +```js +let path = "/ext/test_file.txt"; + +file = storage.openFile(path, "w", "create_always"); +file.write("Test"); +file.close(); +print("File created"); + +if (storage.remove(path)) { + print("File removed"); +} +``` + +
+ +## rmrf() + +Removes a file or recursively removes a possibly non-empty directory. + +**Parameters** + +- path: The path to the file or directory + +**Returns** + +`true` on success, `false` on failure. + +**Example** +```js +let path = "/ext/test_dir"; + +if (storage.rmrf(path)) { + print("Directory removed"); +} +``` From cd28f7d23223e8dfe675190c0cc09c7e5b5f7f82 Mon Sep 17 00:00:00 2001 From: Dmitry422 Date: Fri, 28 Mar 2025 01:14:57 +0700 Subject: [PATCH 149/268] current working, need find and remove 1 bug --- .../services/notification/notification_app.c | 85 ++++++- .../services/notification/notification_app.h | 7 +- .../services/rgb_backlight/rgb_backlight.c | 13 +- .../services/rgb_backlight/rgb_backlight.h | 3 + .../rgb_backlight/rgb_backlight_settings.c | 1 + .../rgb_backlight/rgb_backlight_settings.h | 2 +- .../notification_settings_app.c | 233 +++++++++++++----- 7 files changed, 264 insertions(+), 80 deletions(-) diff --git a/applications/services/notification/notification_app.c b/applications/services/notification/notification_app.c index cf03d4389..3eca2fd2d 100644 --- a/applications/services/notification/notification_app.c +++ b/applications/services/notification/notification_app.c @@ -33,6 +33,51 @@ static uint8_t notification_settings_get_display_brightness(NotificationApp* app static uint8_t notification_settings_get_rgb_led_brightness(NotificationApp* app, uint8_t value); static uint32_t notification_settings_display_off_delay_ticks(NotificationApp* app); +// --- NIGHT SHIFT --- + +void night_shift_timer_start(NotificationApp* app) { + if(app->settings.night_shift != 1) { + furi_timer_start(app->night_shift_timer, furi_ms_to_ticks(2000)); + } +} + +void night_shift_timer_stop(NotificationApp* app) { + if(furi_timer_is_running(app->night_shift_timer)) { + furi_timer_stop(app->night_shift_timer); + } +} + +// every callback time we check current time and current night_shift_settings value +void night_shift_timer_callback(void* context) { + furi_assert(context); + NotificationApp* app = context; + DateTime current_date_time; + + // save current night_shift; + // float old_night_shift = app->current_night_shift; + + // take system time and convert to minutes + furi_hal_rtc_get_datetime(¤t_date_time); + uint32_t time = current_date_time.hour * 60 + current_date_time.minute; + + // if current time not in night_shift range then current_night_shift = 1 else = settings value; + // set values to stock and rgb backlights + if((time > app->settings.night_shift_end) && (time < app->settings.night_shift_start)) { + app->current_night_shift = 1.0f; + app->rgb_srv->current_night_shift = 1.0f; + } else { + app->current_night_shift = app->settings.night_shift; + app->rgb_srv->current_night_shift = app->settings.night_shift; + } + + // // if night shift was changed then update stock and rgb backlight to new value + // if(old_night_shift != app->current_night_shift) { + // notification_message(app, &sequence_display_backlight_on); + // } +} + +// --- NIGHT SHIFT END --- + void notification_message_save_settings(NotificationApp* app) { NotificationAppMessage m = { .type = SaveSettingsMessage, .back_event = furi_event_flag_alloc()}; @@ -129,7 +174,11 @@ static void notification_reset_notification_layer( } if(reset_mask & reset_display_mask) { if(!float_is_equal(display_brightness_set, app->settings.display_brightness)) { - furi_hal_light_set(LightBacklight, app->settings.display_brightness * 0xFF); + // --- NIGHT SHIFT --- + furi_hal_light_set( + LightBacklight, + app->settings.display_brightness * 0xFF * app->current_night_shift); + // --- NIGHT SHIFT END--- } furi_timer_start(app->display_timer, notification_settings_display_off_delay_ticks(app)); } @@ -214,15 +263,17 @@ static void notification_process_notification_message( // if on - switch on and start timer // if off - switch off and stop timer // on timer - switch off + // --- NIGHT SHIFT --- if(notification_message->data.led.value > 0x00) { notification_apply_notification_led_layer( &app->display, - notification_message->data.led.value * display_brightness_setting); + notification_message->data.led.value * display_brightness_setting * + app->current_night_shift); reset_mask |= reset_display_mask; //start rgb_mod_rainbow_timer when display backlight is ON and all corresponding settings is ON too rainbow_timer_starter(app->rgb_srv); - + // --- NIGHT SHIFT END --- } else { reset_mask &= ~reset_display_mask; notification_reset_notification_led_layer(&app->display); @@ -238,10 +289,12 @@ static void notification_process_notification_message( case NotificationMessageTypeLedDisplayBacklightEnforceOn: furi_check(app->display_led_lock < UINT8_MAX); app->display_led_lock++; + // --- NIGHT SHIFT --- if(app->display_led_lock == 1) { notification_apply_internal_led_layer( &app->display, - notification_message->data.led.value * display_brightness_setting); + notification_message->data.led.value * display_brightness_setting * + app->current_night_shift); } break; case NotificationMessageTypeLedDisplayBacklightEnforceAuto: @@ -250,8 +303,10 @@ static void notification_process_notification_message( if(app->display_led_lock == 0) { notification_apply_internal_led_layer( &app->display, - notification_message->data.led.value * display_brightness_setting); + notification_message->data.led.value * display_brightness_setting * + app->current_night_shift); } + // --- NIGHT SHIFT END --- } else { FURI_LOG_E(TAG, "Incorrect BacklightEnforce use"); } @@ -559,6 +614,17 @@ static NotificationApp* notification_app_alloc(void) { furi_pubsub_subscribe(app->event_record, input_event_callback, app); notification_message(app, &sequence_display_backlight_on); + // --- NIGHT SHIFT --- + app->rgb_srv = furi_record_open(RECORD_RGB_BACKLIGHT); + app->rgb_srv->current_night_shift = 1.0f; + app->current_night_shift = 1.0f; + app->settings.night_shift = 1.0f; + app->settings.night_shift_start = 1020; + app->settings.night_shift_end = 300; + app->night_shift_timer = + furi_timer_alloc(night_shift_timer_callback, FuriTimerTypePeriodic, app); + // --- NIGHT SHIFT END --- + return app; } @@ -582,6 +648,13 @@ static void notification_apply_settings(NotificationApp* app) { } notification_apply_lcd_contrast(app); + + // --- NIGHT SHIFT --- + // if night_shift_enabled start timer for controlling current_night_shift multiplicator value depent from current time + if(app->settings.night_shift != 1) { + night_shift_timer_start(app); + } + // --- NIGHT SHIFT END --- } static void notification_init_settings(NotificationApp* app) { @@ -600,7 +673,7 @@ static void notification_init_settings(NotificationApp* app) { int32_t notification_srv(void* p) { UNUSED(p); NotificationApp* app = notification_app_alloc(); - app->rgb_srv = furi_record_open(RECORD_RGB_BACKLIGHT); + notification_init_settings(app); notification_vibro_off(); diff --git a/applications/services/notification/notification_app.h b/applications/services/notification/notification_app.h index 7ee421b31..0d4404fa5 100644 --- a/applications/services/notification/notification_app.h +++ b/applications/services/notification/notification_app.h @@ -34,7 +34,7 @@ typedef struct { Light light; } NotificationLedLayer; -#define NOTIFICATION_SETTINGS_VERSION 0x02 +#define NOTIFICATION_SETTINGS_VERSION 0x03 #define NOTIFICATION_SETTINGS_PATH INT_PATH(NOTIFICATION_SETTINGS_FILE_NAME) typedef struct { @@ -61,6 +61,11 @@ struct NotificationApp { NotificationSettings settings; RGBBacklightApp* rgb_srv; + + FuriTimer* night_shift_timer; + float current_night_shift; }; void notification_message_save_settings(NotificationApp* app); +void night_shift_timer_start(NotificationApp* app); +void night_shift_timer_stop(NotificationApp* app); diff --git a/applications/services/rgb_backlight/rgb_backlight.c b/applications/services/rgb_backlight/rgb_backlight.c index 9dc60a5ec..8862ac154 100644 --- a/applications/services/rgb_backlight/rgb_backlight.c +++ b/applications/services/rgb_backlight/rgb_backlight.c @@ -137,9 +137,9 @@ void rgb_backlight_update(float brightness) { if(app->settings->rgb_backlight_installed) { for(uint8_t i = 0; i < SK6805_get_led_count(); i++) { - uint8_t r = current_led[i].red * (brightness * 1.0f); - uint8_t g = current_led[i].green * (brightness * 1.0f); - uint8_t b = current_led[i].blue * (brightness * 1.0f); + uint8_t r = current_led[i].red * brightness; + uint8_t g = current_led[i].green * brightness; + uint8_t b = current_led[i].blue * brightness; SK6805_set_led_color(i, r, g, b); } SK6805_update(); @@ -154,7 +154,7 @@ void rainbow_timer_start(RGBBacklightApp* app) { // stop furi timer for rainbow void rainbow_timer_stop(RGBBacklightApp* app) { - if (furi_timer_is_running (app->rainbow_timer)){ + if(furi_timer_is_running(app->rainbow_timer)) { furi_timer_stop(app->rainbow_timer); } } @@ -214,7 +214,7 @@ static void rainbow_timer_callback(void* context) { break; } - rgb_backlight_update(app->settings->brightness); + rgb_backlight_update(app->settings->brightness * app->current_night_shift); } } @@ -233,6 +233,7 @@ int32_t rgb_backlight_srv(void* p) { rgb_backlight_settings_load(app->settings); app->rainbow_hue = 1; + app->current_night_shift = 1.0f; furi_record_create(RECORD_RGB_BACKLIGHT, app); @@ -244,7 +245,7 @@ int32_t rgb_backlight_srv(void* p) { rgb_backlight_set_led_static_color(2, app->settings->led_2_color_index); rgb_backlight_set_led_static_color(1, app->settings->led_1_color_index); rgb_backlight_set_led_static_color(0, app->settings->led_0_color_index); - rgb_backlight_update(app->settings->brightness); + rgb_backlight_update(app->settings->brightness * app->current_night_shift); } // if rgb_backlight not installed then set default static orange color(index=0) to all leds (0-2) and force light on } else { diff --git a/applications/services/rgb_backlight/rgb_backlight.h b/applications/services/rgb_backlight/rgb_backlight.h index d6f8b5fce..1be252dc1 100644 --- a/applications/services/rgb_backlight/rgb_backlight.h +++ b/applications/services/rgb_backlight/rgb_backlight.h @@ -33,6 +33,9 @@ typedef struct { uint8_t rainbow_green; uint8_t rainbow_blue; RGBBacklightSettings* settings; + // night_shift multiplicator for leds brightnes coming from Notificatoin app. + float current_night_shift; + } RGBBacklightApp; #define RECORD_RGB_BACKLIGHT "rgb_backlight" diff --git a/applications/services/rgb_backlight/rgb_backlight_settings.c b/applications/services/rgb_backlight/rgb_backlight_settings.c index b76c6598a..17f6bb271 100644 --- a/applications/services/rgb_backlight/rgb_backlight_settings.c +++ b/applications/services/rgb_backlight/rgb_backlight_settings.c @@ -30,6 +30,7 @@ typedef struct { uint16_t rainbow_step; uint8_t rainbow_saturation; uint8_t rainbow_wide; + } RGBBacklightSettingsPrevious; void rgb_backlight_settings_load(RGBBacklightSettings* settings) { diff --git a/applications/services/rgb_backlight/rgb_backlight_settings.h b/applications/services/rgb_backlight/rgb_backlight_settings.h index 3f3af005f..b1e4a4a01 100644 --- a/applications/services/rgb_backlight/rgb_backlight_settings.h +++ b/applications/services/rgb_backlight/rgb_backlight_settings.h @@ -20,7 +20,7 @@ typedef struct { uint16_t rainbow_step; uint8_t rainbow_saturation; uint8_t rainbow_wide; - + } RGBBacklightSettings; #ifdef __cplusplus diff --git a/applications/settings/notification_settings/notification_settings_app.c b/applications/settings/notification_settings/notification_settings_app.c index 435a0087c..86176c8e5 100644 --- a/applications/settings/notification_settings/notification_settings_app.c +++ b/applications/settings/notification_settings/notification_settings_app.c @@ -182,65 +182,93 @@ typedef enum { // --- RGB BACKLIGHT END --- - // --- NIGHT SHIFT --- - #define NIGHT_SHIFT_COUNT 7 - const char* const night_shift_text[NIGHT_SHIFT_COUNT] = { - "OFF", - "5%", - "10%" - "15%", - "20%", - "25%", - "30%" +// --- NIGHT SHIFT --- +#define NIGHT_SHIFT_COUNT 7 +const char* const night_shift_text[NIGHT_SHIFT_COUNT] = + {"OFF", "-10%", "-20%", "-30%", "-40%", "-50%", "-60%" - }; - const float night_shift_value[NIGHT_SHIFT_COUNT] = { - 0, 0.05, 0.1, 0.15, 0.2, 0.25, 0.3, - }; +}; +const float night_shift_value[NIGHT_SHIFT_COUNT] = { + 1.0f, + 0.9f, + 0.8f, + 0.7f, + 0.6f, + 0.5f, + 0.4f, +}; - #define NIGHT_SHIFT_START_COUNT 14 - const char* const night_shift_start_text[NIGHT_SHIFT_START_COUNT] = { - "17:00", - "17:30", - "18:00" - "18:30", - "19:00", - "19:30", - "20:00", - "20:30", - "21:00", - "21:30", - "22:00", - "22:30", - "23:00", - "23:30", - }; - const uint32_t night_shift_start_value[NIGHT_SHIFT_START_COUNT] = { - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - }; +#define NIGHT_SHIFT_START_COUNT 14 +const char* const night_shift_start_text[NIGHT_SHIFT_START_COUNT] = { + "17:00", + "17:30", + "18:00", + "18:30", + "19:00", + "19:30", + "20:00", + "20:30", + "21:00", + "21:30", + "22:00", + "22:30", + "23:00", + "23:30", +}; +// values in minutes like 23:30 = 23*60+30=1410 +const uint32_t night_shift_start_value[NIGHT_SHIFT_START_COUNT] = { + 1020, + 1050, + 1080, + 1110, + 1140, + 1170, + 1200, + 1230, + 1260, + 1290, + 1320, + 1350, + 1380, + 1410, +}; - #define NIGHT_SHIFT_END_COUNT 14 - const char* const night_shift_end_text[NIGHT_SHIFT_END_COUNT] = { - "05:00", - "05:30", - "06:00" - "06:30", - "07:00", - "07:30", - "08:00", - "08:30", - "09:00", - "09:30", - "10:00", - "10:30", - "11:00", - "11:30", - }; - const uint32_t night_shift_end_value[NIGHT_SHIFT_END_COUNT] = { - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - }; +#define NIGHT_SHIFT_END_COUNT 14 +const char* const night_shift_end_text[NIGHT_SHIFT_END_COUNT] = { + "05:00", + "05:30", + "06:00", + "06:30", + "07:00", + "07:30", + "08:00", + "08:30", + "09:00", + "09:30", + "10:00", + "10:30", + "11:00", + "11:30", +}; +// values in minutes like 6:30 = 6*60+30=390 +const uint32_t night_shift_end_value[NIGHT_SHIFT_END_COUNT] = { + 300, + 330, + 360, + 390, + 410, + 440, + 470, + 500, + 530, + 560, + 590, + 620, + 650, + 680, +}; - // --- NIGHT SHIFT END --- +// --- NIGHT SHIFT END --- static void contrast_changed(VariableItem* item) { NotificationAppSettings* app = variable_item_get_context(item); @@ -345,7 +373,9 @@ static void rgb_backlight_installed_changed(VariableItem* item) { 1, app->notification->rgb_srv->settings->led_1_color_index); rgb_backlight_set_led_static_color( 0, app->notification->rgb_srv->settings->led_0_color_index); - rgb_backlight_update(app->notification->settings.display_brightness); + rgb_backlight_update( + app->notification->settings.display_brightness * + app->notification->current_night_shift); } } @@ -376,7 +406,9 @@ static void led_2_color_changed(VariableItem* item) { // dont update screen color if rainbow timer working if(!furi_timer_is_running(app->notification->rgb_srv->rainbow_timer)) { rgb_backlight_set_led_static_color(2, index); - rgb_backlight_update(app->notification->rgb_srv->settings->brightness); + rgb_backlight_update( + app->notification->rgb_srv->settings->brightness * + app->notification->current_night_shift); } rgb_backlight_settings_save(app->notification->rgb_srv->settings); @@ -392,7 +424,9 @@ static void led_1_color_changed(VariableItem* item) { // dont update screen color if rainbow timer working if(!furi_timer_is_running(app->notification->rgb_srv->rainbow_timer)) { rgb_backlight_set_led_static_color(1, index); - rgb_backlight_update(app->notification->rgb_srv->settings->brightness); + rgb_backlight_update( + app->notification->rgb_srv->settings->brightness * + app->notification->current_night_shift); } rgb_backlight_settings_save(app->notification->rgb_srv->settings); @@ -408,7 +442,9 @@ static void led_0_color_changed(VariableItem* item) { // dont update screen color if rainbow timer working if(!furi_timer_is_running(app->notification->rgb_srv->rainbow_timer)) { rgb_backlight_set_led_static_color(0, index); - rgb_backlight_update(app->notification->rgb_srv->settings->brightness); + rgb_backlight_update( + app->notification->rgb_srv->settings->brightness * + app->notification->current_night_shift); } rgb_backlight_settings_save(app->notification->rgb_srv->settings); @@ -429,7 +465,9 @@ static void rgb_backlight_rainbow_changed(VariableItem* item) { 1, app->notification->rgb_srv->settings->led_1_color_index); rgb_backlight_set_led_static_color( 0, app->notification->rgb_srv->settings->led_0_color_index); - rgb_backlight_update(app->notification->rgb_srv->settings->brightness); + rgb_backlight_update( + app->notification->rgb_srv->settings->brightness * + app->notification->current_night_shift); rainbow_timer_stop(app->notification->rgb_srv); } else { rainbow_timer_starter(app->notification->rgb_srv); @@ -505,6 +543,60 @@ static uint32_t notification_app_rgb_settings_exit(void* context) { // --- NIGHT SHIFT --- +static void night_shift_changed(VariableItem* item) { + NotificationAppSettings* app = variable_item_get_context(item); + uint8_t index = variable_item_get_current_value_index(item); + + variable_item_set_current_value_text(item, night_shift_text[index]); + app->notification->settings.night_shift = night_shift_value[index]; + app->notification->current_night_shift = night_shift_value[index]; + app->notification->rgb_srv->current_night_shift = night_shift_value[index]; + + // force demo night_shift brightness ot rgb backlight and stock backlight + notification_message(app->notification, &sequence_display_backlight_on); + + int slide = 0; + if(furi_hal_rtc_is_flag_set(FuriHalRtcFlagDebug) || + (app->notification->rgb_srv->settings->rgb_backlight_installed)) { + slide = 1; + } + for(int i = 4 + slide; i < (6 + slide); i++) { + VariableItem* t_item = variable_item_list_get(app->variable_item_list, i); + if(index == 0) { + variable_item_set_locked(t_item, true, "Night shift\nOFF!"); + } else { + variable_item_set_locked(t_item, false, "Night shift\nOFF!"); + } + } + + if(night_shift_value[index] != 1) { + night_shift_timer_start(app->notification); + } else { + night_shift_timer_stop(app->notification); + } + + notification_message_save_settings(app->notification); +} + +static void night_shift_start_changed(VariableItem* item) { + NotificationAppSettings* app = variable_item_get_context(item); + uint8_t index = variable_item_get_current_value_index(item); + + variable_item_set_current_value_text(item, night_shift_start_text[index]); + app->notification->settings.night_shift_start = night_shift_start_value[index]; + + notification_message_save_settings(app->notification); +} + +static void night_shift_end_changed(VariableItem* item) { + NotificationAppSettings* app = variable_item_get_context(item); + uint8_t index = variable_item_get_current_value_index(item); + + variable_item_set_current_value_text(item, night_shift_end_text[index]); + app->notification->settings.night_shift_end = night_shift_end_value[index]; + + notification_message_save_settings(app->notification); +} // --- NIGHT SHIFT END --- @@ -562,28 +654,37 @@ static NotificationAppSettings* alloc_settings(void) { // --- NIGHT SHIFT --- item = variable_item_list_add( - app->variable_item_list, "Night Shift", NIGHT_SHIFT_COUNT, night_shift_changed, app); + app->variable_item_list, "Night shift", NIGHT_SHIFT_COUNT, night_shift_changed, app); value_index = value_index_float( app->notification->settings.night_shift, night_shift_value, NIGHT_SHIFT_COUNT); variable_item_set_current_value_index(item, value_index); variable_item_set_current_value_text(item, night_shift_text[value_index]); item = variable_item_list_add( - app->variable_item_list, " . Start", NIGHT_SHIFT_START_COUNT, night_shift_start_changed, app); + app->variable_item_list, + " . Start", + NIGHT_SHIFT_START_COUNT, + night_shift_start_changed, + app); value_index = value_index_uint32( - app->notification->settings.night_shift_start, night_shift_start_value, NIGHT_SHIFT_START_COUNT); + app->notification->settings.night_shift_start, + night_shift_start_value, + NIGHT_SHIFT_START_COUNT); variable_item_set_current_value_index(item, value_index); variable_item_set_current_value_text(item, night_shift_start_text[value_index]); - + variable_item_set_locked( + item, (app->notification->settings.night_shift == 1), "Night shift \nOFF!"); + item = variable_item_list_add( app->variable_item_list, " . End", NIGHT_SHIFT_END_COUNT, night_shift_end_changed, app); value_index = value_index_uint32( app->notification->settings.night_shift_end, night_shift_end_value, NIGHT_SHIFT_END_COUNT); variable_item_set_current_value_index(item, value_index); variable_item_set_current_value_text(item, night_shift_end_text[value_index]); - - // --- NIGHT SHIFT END--- + variable_item_set_locked( + item, (app->notification->settings.night_shift == 1), "Night shift \nOFF!"); + // --- NIGHT SHIFT END--- item = variable_item_list_add( app->variable_item_list, "LED Brightness", BACKLIGHT_COUNT, led_changed, app); From 31b7c4a34fd22c1cedc9c2ed9f98235eb6f3b49c Mon Sep 17 00:00:00 2001 From: Dmitry422 Date: Fri, 28 Mar 2025 13:48:32 +0700 Subject: [PATCH 150/268] Night shift done. --- .../services/notification/notification_app.c | 12 +++++++----- applications/services/rgb_backlight/rgb_backlight.c | 6 +++--- .../services/rgb_backlight/rgb_backlight_settings.c | 2 +- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/applications/services/notification/notification_app.c b/applications/services/notification/notification_app.c index 3eca2fd2d..b5a95b4a3 100644 --- a/applications/services/notification/notification_app.c +++ b/applications/services/notification/notification_app.c @@ -53,7 +53,8 @@ void night_shift_timer_callback(void* context) { NotificationApp* app = context; DateTime current_date_time; - // save current night_shift; + // IN DEVELOPMENT + // // save current night_shift; // float old_night_shift = app->current_night_shift; // take system time and convert to minutes @@ -70,6 +71,7 @@ void night_shift_timer_callback(void* context) { app->rgb_srv->current_night_shift = app->settings.night_shift; } + // IN DEVELOPMENT // // if night shift was changed then update stock and rgb backlight to new value // if(old_night_shift != app->current_night_shift) { // notification_message(app, &sequence_display_backlight_on); @@ -177,7 +179,7 @@ static void notification_reset_notification_layer( // --- NIGHT SHIFT --- furi_hal_light_set( LightBacklight, - app->settings.display_brightness * 0xFF * app->current_night_shift); + app->settings.display_brightness * 0xFF * app->current_night_shift * 1.0f); // --- NIGHT SHIFT END--- } furi_timer_start(app->display_timer, notification_settings_display_off_delay_ticks(app)); @@ -268,7 +270,7 @@ static void notification_process_notification_message( notification_apply_notification_led_layer( &app->display, notification_message->data.led.value * display_brightness_setting * - app->current_night_shift); + app->current_night_shift * 1.0f); reset_mask |= reset_display_mask; //start rgb_mod_rainbow_timer when display backlight is ON and all corresponding settings is ON too @@ -294,7 +296,7 @@ static void notification_process_notification_message( notification_apply_internal_led_layer( &app->display, notification_message->data.led.value * display_brightness_setting * - app->current_night_shift); + app->current_night_shift * 1.0f); } break; case NotificationMessageTypeLedDisplayBacklightEnforceAuto: @@ -304,7 +306,7 @@ static void notification_process_notification_message( notification_apply_internal_led_layer( &app->display, notification_message->data.led.value * display_brightness_setting * - app->current_night_shift); + app->current_night_shift * 1.0f); } // --- NIGHT SHIFT END --- } else { diff --git a/applications/services/rgb_backlight/rgb_backlight.c b/applications/services/rgb_backlight/rgb_backlight.c index 8862ac154..916357b89 100644 --- a/applications/services/rgb_backlight/rgb_backlight.c +++ b/applications/services/rgb_backlight/rgb_backlight.c @@ -137,9 +137,9 @@ void rgb_backlight_update(float brightness) { if(app->settings->rgb_backlight_installed) { for(uint8_t i = 0; i < SK6805_get_led_count(); i++) { - uint8_t r = current_led[i].red * brightness; - uint8_t g = current_led[i].green * brightness; - uint8_t b = current_led[i].blue * brightness; + uint8_t r = current_led[i].red * brightness * 1.0f; + uint8_t g = current_led[i].green * brightness * 1.0f; + uint8_t b = current_led[i].blue * brightness * 1.0f; SK6805_set_led_color(i, r, g, b); } SK6805_update(); diff --git a/applications/services/rgb_backlight/rgb_backlight_settings.c b/applications/services/rgb_backlight/rgb_backlight_settings.c index 17f6bb271..baa573a77 100644 --- a/applications/services/rgb_backlight/rgb_backlight_settings.c +++ b/applications/services/rgb_backlight/rgb_backlight_settings.c @@ -30,7 +30,7 @@ typedef struct { uint16_t rainbow_step; uint8_t rainbow_saturation; uint8_t rainbow_wide; - + } RGBBacklightSettingsPrevious; void rgb_backlight_settings_load(RGBBacklightSettings* settings) { From 3e9bb38cd759deb1df7b9168f7f80a05458b7ee1 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Fri, 28 Mar 2025 14:01:54 +0300 Subject: [PATCH 151/268] just in case --- applications/services/notification/notification_app.c | 3 +++ applications/services/rgb_backlight/rgb_backlight.c | 3 +++ 2 files changed, 6 insertions(+) diff --git a/applications/services/notification/notification_app.c b/applications/services/notification/notification_app.c index b5a95b4a3..b50017963 100644 --- a/applications/services/notification/notification_app.c +++ b/applications/services/notification/notification_app.c @@ -37,6 +37,9 @@ static uint32_t notification_settings_display_off_delay_ticks(NotificationApp* a void night_shift_timer_start(NotificationApp* app) { if(app->settings.night_shift != 1) { + if(furi_timer_is_running(app->night_shift_timer)) { + furi_timer_stop(app->night_shift_timer); + } furi_timer_start(app->night_shift_timer, furi_ms_to_ticks(2000)); } } diff --git a/applications/services/rgb_backlight/rgb_backlight.c b/applications/services/rgb_backlight/rgb_backlight.c index 916357b89..22628e3af 100644 --- a/applications/services/rgb_backlight/rgb_backlight.c +++ b/applications/services/rgb_backlight/rgb_backlight.c @@ -149,6 +149,9 @@ void rgb_backlight_update(float brightness) { // start furi timer for rainbow void rainbow_timer_start(RGBBacklightApp* app) { + if(furi_timer_is_running(app->rainbow_timer)) { + furi_timer_stop(app->rainbow_timer); + } furi_timer_start(app->rainbow_timer, furi_ms_to_ticks(app->settings->rainbow_speed_ms)); } From 82489cf8eb017e52fe3a06351230e132b890addb Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Fri, 28 Mar 2025 14:02:12 +0300 Subject: [PATCH 152/268] fmt --- applications/services/notification/notification_app.h | 2 +- applications/services/rgb_backlight/rgb_backlight_settings.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/applications/services/notification/notification_app.h b/applications/services/notification/notification_app.h index 0d4404fa5..2ef84ba95 100644 --- a/applications/services/notification/notification_app.h +++ b/applications/services/notification/notification_app.h @@ -47,7 +47,7 @@ typedef struct { bool vibro_on; float night_shift; uint32_t night_shift_start; - uint32_t night_shift_end; + uint32_t night_shift_end; } NotificationSettings; struct NotificationApp { diff --git a/applications/services/rgb_backlight/rgb_backlight_settings.h b/applications/services/rgb_backlight/rgb_backlight_settings.h index b1e4a4a01..3f3af005f 100644 --- a/applications/services/rgb_backlight/rgb_backlight_settings.h +++ b/applications/services/rgb_backlight/rgb_backlight_settings.h @@ -20,7 +20,7 @@ typedef struct { uint16_t rainbow_step; uint8_t rainbow_saturation; uint8_t rainbow_wide; - + } RGBBacklightSettings; #ifdef __cplusplus From 17db4dcdacba111ed7501465f52a20c10c531be3 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Fri, 28 Mar 2025 14:03:10 +0300 Subject: [PATCH 153/268] upd changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c89f8dd7..72d4dcb03 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ * SubGHz: **Fix Hollarm protocol with more verification** * SubGHz: **Fix GangQi protocol** (by @DoberBit and @mishamyte (who spent 2 weeks on this)) * SubGHz: **Came Atomo button hold simulation with full cycle** simulation (to allow proper pairing with receiver) +* System: **Night Shift Feature** (dimming backlight in selected time interval) (PR #885 | by @Dmitry422) * System: **Сombining RGB Backlight mod** (by @quen0n) and original backlight support **in one firmware** (+ Rainbow/Wave effect (based on @Willy-JL idea)) (PR #877 #881 | by @Dmitry422) - (**To enable RGB Backlight support go into Notifications settings with Debug mode - ON**) * OFW: LFRFID - **EM4305 support** * OFW: **Universal IR signal selection** From 65b1b943d1e9bc3b38f05140e0e8889fb8921438 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Fri, 28 Mar 2025 14:05:47 +0300 Subject: [PATCH 154/268] GUI: Fix widget text scroll with 256+ lines [ci skip] by Willy-JL in OFW PR 4160 --- .../gui/modules/widget_elements/widget_element_text_scroll.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/applications/services/gui/modules/widget_elements/widget_element_text_scroll.c b/applications/services/gui/modules/widget_elements/widget_element_text_scroll.c index 4c9c39dff..491ffc6bc 100644 --- a/applications/services/gui/modules/widget_elements/widget_element_text_scroll.c +++ b/applications/services/gui/modules/widget_elements/widget_element_text_scroll.c @@ -19,8 +19,8 @@ typedef struct { uint8_t width; uint8_t height; FuriString* text; - uint8_t scroll_pos_total; - uint8_t scroll_pos_current; + uint16_t scroll_pos_total; + uint16_t scroll_pos_current; bool text_formatted; } WidgetElementTextScrollModel; From 0c5cab9f1ae60079f56ac7f8e3a87fca97da88ef Mon Sep 17 00:00:00 2001 From: doomwastaken Date: Fri, 28 Mar 2025 14:09:32 +0300 Subject: [PATCH 155/268] added new doom animation, updated manifests, re-enabled some animations --- .../external/L1_Doom_128x64/frame_0.png | Bin 0 -> 1495 bytes .../external/L1_Doom_128x64/frame_1.png | Bin 0 -> 1502 bytes .../external/L1_Doom_128x64/frame_10.png | Bin 0 -> 973 bytes .../external/L1_Doom_128x64/frame_11.png | Bin 0 -> 1070 bytes .../external/L1_Doom_128x64/frame_12.png | Bin 0 -> 1138 bytes .../external/L1_Doom_128x64/frame_13.png | Bin 0 -> 1069 bytes .../external/L1_Doom_128x64/frame_14.png | Bin 0 -> 1065 bytes .../external/L1_Doom_128x64/frame_15.png | Bin 0 -> 898 bytes .../external/L1_Doom_128x64/frame_16.png | Bin 0 -> 930 bytes .../external/L1_Doom_128x64/frame_17.png | Bin 0 -> 937 bytes .../external/L1_Doom_128x64/frame_18.png | Bin 0 -> 931 bytes .../external/L1_Doom_128x64/frame_19.png | Bin 0 -> 923 bytes .../external/L1_Doom_128x64/frame_2.png | Bin 0 -> 1498 bytes .../external/L1_Doom_128x64/frame_20.png | Bin 0 -> 1159 bytes .../external/L1_Doom_128x64/frame_21.png | Bin 0 -> 1284 bytes .../external/L1_Doom_128x64/frame_22.png | Bin 0 -> 1165 bytes .../external/L1_Doom_128x64/frame_23.png | Bin 0 -> 1306 bytes .../external/L1_Doom_128x64/frame_24.png | Bin 0 -> 1159 bytes .../external/L1_Doom_128x64/frame_25.png | Bin 0 -> 1307 bytes .../external/L1_Doom_128x64/frame_26.png | Bin 0 -> 1069 bytes .../external/L1_Doom_128x64/frame_27.png | Bin 0 -> 1131 bytes .../external/L1_Doom_128x64/frame_28.png | Bin 0 -> 1101 bytes .../external/L1_Doom_128x64/frame_29.png | Bin 0 -> 1035 bytes .../external/L1_Doom_128x64/frame_3.png | Bin 0 -> 1558 bytes .../external/L1_Doom_128x64/frame_30.png | Bin 0 -> 909 bytes .../external/L1_Doom_128x64/frame_31.png | Bin 0 -> 736 bytes .../external/L1_Doom_128x64/frame_32.png | Bin 0 -> 1169 bytes .../external/L1_Doom_128x64/frame_33.png | Bin 0 -> 1212 bytes .../external/L1_Doom_128x64/frame_34.png | Bin 0 -> 1269 bytes .../external/L1_Doom_128x64/frame_35.png | Bin 0 -> 1237 bytes .../external/L1_Doom_128x64/frame_36.png | Bin 0 -> 1288 bytes .../external/L1_Doom_128x64/frame_37.png | Bin 0 -> 1206 bytes .../external/L1_Doom_128x64/frame_38.png | Bin 0 -> 1240 bytes .../external/L1_Doom_128x64/frame_4.png | Bin 0 -> 1487 bytes .../external/L1_Doom_128x64/frame_5.png | Bin 0 -> 1161 bytes .../external/L1_Doom_128x64/frame_6.png | Bin 0 -> 1181 bytes .../external/L1_Doom_128x64/frame_7.png | Bin 0 -> 1138 bytes .../external/L1_Doom_128x64/frame_8.png | Bin 0 -> 1193 bytes .../external/L1_Doom_128x64/frame_9.png | Bin 0 -> 1201 bytes .../dolphin/external/L1_Doom_128x64/meta.txt | 14 + assets/dolphin/external/manifest.txt | 243 ++++++++---------- 41 files changed, 118 insertions(+), 139 deletions(-) create mode 100644 assets/dolphin/external/L1_Doom_128x64/frame_0.png create mode 100644 assets/dolphin/external/L1_Doom_128x64/frame_1.png create mode 100644 assets/dolphin/external/L1_Doom_128x64/frame_10.png create mode 100644 assets/dolphin/external/L1_Doom_128x64/frame_11.png create mode 100644 assets/dolphin/external/L1_Doom_128x64/frame_12.png create mode 100644 assets/dolphin/external/L1_Doom_128x64/frame_13.png create mode 100644 assets/dolphin/external/L1_Doom_128x64/frame_14.png create mode 100644 assets/dolphin/external/L1_Doom_128x64/frame_15.png create mode 100644 assets/dolphin/external/L1_Doom_128x64/frame_16.png create mode 100644 assets/dolphin/external/L1_Doom_128x64/frame_17.png create mode 100644 assets/dolphin/external/L1_Doom_128x64/frame_18.png create mode 100644 assets/dolphin/external/L1_Doom_128x64/frame_19.png create mode 100644 assets/dolphin/external/L1_Doom_128x64/frame_2.png create mode 100644 assets/dolphin/external/L1_Doom_128x64/frame_20.png create mode 100644 assets/dolphin/external/L1_Doom_128x64/frame_21.png create mode 100644 assets/dolphin/external/L1_Doom_128x64/frame_22.png create mode 100644 assets/dolphin/external/L1_Doom_128x64/frame_23.png create mode 100644 assets/dolphin/external/L1_Doom_128x64/frame_24.png create mode 100644 assets/dolphin/external/L1_Doom_128x64/frame_25.png create mode 100644 assets/dolphin/external/L1_Doom_128x64/frame_26.png create mode 100644 assets/dolphin/external/L1_Doom_128x64/frame_27.png create mode 100644 assets/dolphin/external/L1_Doom_128x64/frame_28.png create mode 100644 assets/dolphin/external/L1_Doom_128x64/frame_29.png create mode 100644 assets/dolphin/external/L1_Doom_128x64/frame_3.png create mode 100644 assets/dolphin/external/L1_Doom_128x64/frame_30.png create mode 100644 assets/dolphin/external/L1_Doom_128x64/frame_31.png create mode 100644 assets/dolphin/external/L1_Doom_128x64/frame_32.png create mode 100644 assets/dolphin/external/L1_Doom_128x64/frame_33.png create mode 100644 assets/dolphin/external/L1_Doom_128x64/frame_34.png create mode 100644 assets/dolphin/external/L1_Doom_128x64/frame_35.png create mode 100644 assets/dolphin/external/L1_Doom_128x64/frame_36.png create mode 100644 assets/dolphin/external/L1_Doom_128x64/frame_37.png create mode 100644 assets/dolphin/external/L1_Doom_128x64/frame_38.png create mode 100644 assets/dolphin/external/L1_Doom_128x64/frame_4.png create mode 100644 assets/dolphin/external/L1_Doom_128x64/frame_5.png create mode 100644 assets/dolphin/external/L1_Doom_128x64/frame_6.png create mode 100644 assets/dolphin/external/L1_Doom_128x64/frame_7.png create mode 100644 assets/dolphin/external/L1_Doom_128x64/frame_8.png create mode 100644 assets/dolphin/external/L1_Doom_128x64/frame_9.png create mode 100644 assets/dolphin/external/L1_Doom_128x64/meta.txt diff --git a/assets/dolphin/external/L1_Doom_128x64/frame_0.png b/assets/dolphin/external/L1_Doom_128x64/frame_0.png new file mode 100644 index 0000000000000000000000000000000000000000..937996c8c0cc0f45cabedf1b348a94fc9cfc017e GIT binary patch literal 1495 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 Ef;{o8NB{r; literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_Doom_128x64/frame_10.png b/assets/dolphin/external/L1_Doom_128x64/frame_10.png new file mode 100644 index 0000000000000000000000000000000000000000..a5c9befcf9309d9506662653cdc47b87ce7e11a7 GIT binary patch literal 973 zcmV;;12X)HP)!gDIjvHGzsT8k z0l*E4-1iOFbv=&dz2qHk{kZki;74sFs5?I$-;ag!0h9y4%IFAKCDX6wU{cx2fZ)N- zO$7+f(F%aiAHll5Xa(|!!?v+3~*ArxT7oR*aN-nMOD$4&(h!p$LB z_|a@i1t6?Y0EtbrI6(na1Hhy6Yje-c&e;l(oj`#d0Q33{IP=R9JBA*=&XG01pe=9j zj|S%nHNbHSKm!iX&Dk;foW^6M0yt6t%zfXgLC9r#DggLU016I1tw78gpu%bes0CVi z-b>E?GT+V$aBLr-wiBo=pk!g`SlI_4cyWHQ1>ot}5nJ9ACi?(RY5>mZGa5p8jB>Ad zt{rNCV-;WpIG9H7ITfG-ZVs*np#6cFc2QO6B^97^PU-8?_qC`Mqmt_^)^qD!K+B15 z&Rt!XV7Rr(K7eqpQ^`(X1-j)rl~jPn_@_G`AQj+2gsjJ;0-!OYIUgWD3jlz5Sx@@` z^k)x*R)CpVk3S>eobRP;8MX$PS?Ece>m>-nDS+j(zBY#k#}`5=fW`K%$GILG5JCZxIw2Im0vde11?j5bO$AT}2Vrq=%dXXSXD3hrJSTp( zORl5WTC14VIxhWL zn{c~;9_Q2X%4-(SY3g`dCvZP=w&CFA2-yda1`0l|++lDTPZa@wVXy+U#t)+ac#$O1 zS-xMnSEs?#+1oQqBM7`_ ze?c?od)D#m4oh4FwxZ0!F}nl+fQ_whSudTxsQ^-G*~vL4@a6N88mt0nSDGVu-}=>< zRke@6t>M2SaI~P%W;dsEc)AD9zp9O(8SI$-g&l1FR|9y;egI$DGlJ)JRqX?4a*Fc- veBX=P007uSLR)w%0y$g2FIjF~El`0^;F5g+ z-~mTG&jX*&=j*fFOUU5%kK0cV{J2Siy7SZVcC2tdfO7z>GWrIrN~T{=VOrYCfZ&0h zn+$L{Ud;e_eGhgOmDBq=TXbm9zAp&U0bfpJ?u>CTKft_|qD7H%Wa+!T91H8Hh0MI+XfUELbvayb=0Rl3>%-~A~ zAc({OHN7+#TftciUM{nf0U#y=aG)K46Fs-K0{YyEHNfi`fZA5O2VkEWU~or8 zGC%+Z0Qkv4s(^j>rt~+AaX#JL9ZQ9|>F>2$8#hD;S^$UN&>{0S%G?oX}!{ znqA%j+mi^%0M2&JSZlQ*A_E-R1>maPVwd+?EAU=OI#zW7$4vrz929{8sC;QZvIc0# ze*>i9)2ie22nMiHzE)n5$qjFIBN>3m^mab((+hTYEEw4ZfIsDHoEs$wu4aHTMx9xW z*YrlgwG7}*`B<(N5m?|l2AG}or{O9Fn3eTdKDJS_b~g)L!vLA}Jg#7X70#!@XFZN~ zvA`AI1+2_`Jc8A3;?5%=zYA~x)@hyg(Pvj5Q&%v+%Amjw)DDF3+SLJk1p{a^(K<({ z$+v7A?gq57anSVIOy#}43&78EzjNzqYq8U)<9as^Ka1sZGCA97x+P{=)dhs90W2>_ zGmXpEa^DNBASl=L*l4{AekK_J^t4Ik)u?*bnav#oIGbw>P-{nbGeC$*V1f0hz*a z<&D_jVFwr7*VF)3L~uUU?|DA2ssXkzzzC?E-2qTQZ5nykWPpbJ*7Z(!+jeCapw$r- zn*QG+D;a>uZe2&jh>cP*wOULI%mF|Z)Ans914PdL0xuS@CR1-9sEfds@KxCurSg}s z!cNKATHQ?s&{E6^nYj|P15`UQ*Q{e1AeuvG43TC7+Z$y+xxM?x0HX!P44mR{3hoQ5 z-QfJ|ItZGDh=U&uPGGHnUj{|Xet?*n)PqQSi}%rW4&aTG9ZnJTT|x%{K&;HM!gDe> o3*iHxAZiAkj$;DgR74NlKcnQmMLhC)i2wiq07*qoM6N<$f=QI&yZ`_I literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_Doom_128x64/frame_12.png b/assets/dolphin/external/L1_Doom_128x64/frame_12.png new file mode 100644 index 0000000000000000000000000000000000000000..133374545beeab08a20c801467dee2404a503d0c GIT binary patch literal 1138 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 Y1_VfqkSHR&0{{R307*qoM6N<$g5X(`u>b%7 literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_Doom_128x64/frame_16.png b/assets/dolphin/external/L1_Doom_128x64/frame_16.png new file mode 100644 index 0000000000000000000000000000000000000000..6aa7a7dd328af467ca5aa59a6739ef8f1c8d311d GIT binary patch literal 930 zcmV;T16}-yP)p1on1bv9e z5dhA~RpWUcv9fmOwc?>Eu_Z-|%fE)m^U%Zy;DwnjNN9*_hHs`sfL7rphlBXJt4xTB zY6NI$Y-!PR__|ypyiqfNa}G-Qm}`VLDhA+)ze@O+& zVuEG^Ad4S9Ut)Q8f(e=oU_pG3uWPG-arD^T1Z)FX65r#TYpzr3J$E(%%K$xL)bcWF zXTf8JmVr5TXuP=oyPBf99o!Q7;|-PzKEn|p6uW24wZx85plE=TQlh6aoC=-=iV=5< z0$Br~_5K<4L3-a=4DAJs;FB~hjjnA1sWh@Bk_OOh=#=O!MnH&6I}HOWd4L=>;kZW7 zG(m3)*(zPf!h0Gfm0rz##>ppW_QDf{_xy_h8oetNG|m;2p2bN(tsV}nGi?ay?Nzm5 zAq?3w*<~K^H3ZDw@oN#S)Yz#*sn&&=h5FSn$zD<`>%_#=DBVNAXan^Aes#m|wnF%e z0LT-nc+$91fYfs>Uq|m60%i)3+n1kNht`#frTd0#10W?xax;IeYvrxwTDoueBErAP z%Rysx(pzR20nkmLz6waKmo@(mW(4UIP2szvnt){h$z>e3JpbL)Z!|NN(x97Q+Mh(V z&K*)S|I&AQ>m*T54P+C%v@0bsX_U_N-NUONR3m`&ieYqlwNiN0D4=Qpt%h#hh`BvJ z&yqiil%OW&-5BD*x)@@SRaru7?PWFstfUlOtPM9WqzMT!+#$u;{LlWB5Dbbet6W30a_=k+=idk;}Nrf zM4$#Jt*u)(lyWAR9qJLqCjvA;O0bcJi%oFa7+(X*0J+{;+XO-i^{MBg_I{$tmqTmR z#0k#=#)+AdIk@GYNb%po(Hji?93y(~Cn^j9=;9OEs3SU1L%>d0=>0_304i7!pXeIk zc&-72vDEvCt^q``CO*+MfEX6VCn^}g+Ka5js`x}izx8vkrid*Ood5s;07*qoM6N<$ Ef@X@Ud;kCd literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_Doom_128x64/frame_17.png b/assets/dolphin/external/L1_Doom_128x64/frame_17.png new file mode 100644 index 0000000000000000000000000000000000000000..edf9ee8ba37f0a0b41fdd2587bfe0132e3a4460e GIT binary patch literal 937 zcmV;a16KTrP)+ zL=0eA_>>b(Fl_+zTHniQCipk=VQ=CS{eCA)W1eBy03OAUoMeI_19%oca*7GM4RC_^ zkrPbNWdKX!OD*EOn}BTqOXBCun!&RPSO&1TElS5cI(}>4d4#ELU=G?0iNg2J-jeWM zvq1B4u;tWGG&U};VwVuj07QP$W=VXH#;6*=`s}~75y=1=vmPb0c*_E>Yk=O_pI@+$ z##`V~5W5rDD@^I09;c6M0$GZWgICu6TD#o#5KQ3Zv9b6rphhcLanIhuL*P-EdK}zC zE?5ZR=dbj?oZCe!OmLd9{xiUez@`htT=ZIByG+3C-veqTV6tBIEd>7QMI zc5UtHs}}6irPNVj@`p&?8QKQ+K3I_oWUDQyemL!TDG#cxq_=KdRYz|ENzu9{FU8FO4Mv5`kAqP$ zX#k0<<=j4y#go9C0b0UHojQ6KK4QuM9C1cd?<(QZFOQlrK<{Fe8+ff5k9Z151ZsfV z-nvJqnhCU1J)-zTfCeZDHWRqq1gF*b7Aym3o~qR@UA`FsDb<(yMVO7LFRYm6uo~K%f0YfNOvrP>pE`u@Zpn@*P;1{Y3XQaZ9iwK9QgS z&`-QBWn8~1b_bSbKM~kA&^p${Ct?`j&T1YO#V5KMKm@De6A^s@0XMFSuvu}Q00000 LNkvXXu0mjfZF;WB literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_Doom_128x64/frame_18.png b/assets/dolphin/external/L1_Doom_128x64/frame_18.png new file mode 100644 index 0000000000000000000000000000000000000000..fede8a9bef6d808173b3e292a4328b3814a3508c GIT binary patch literal 931 zcmV;U16=%xP)_x_ zgs2S70FuETC3=q7murM?h8ZA)0403NHNrPT3{Vn(weTt22q&7r)Bv?;LKC0!JkOCP zFfjmPt#9E(6LcE@z1C|v%>@5?PToy?qTk=-vF|N38(`-sImrY~29SvFeeg~(f!Y8k zh#xt@1S$iZBz|Of6Nn9PqWF=WO&~JB&J(`V=~@PI(4xcIu)Ah~ssX&F{+qRAm}LNT zG-im$rswRK1&Rjn8vPO8|C8F?9){A)tkfzxf zl{YP~<<$VK^-_`=-$bt&0nH>km4U9ifkbVyYwiE<1a2JNkA$_Tmn$quPM^0Nv+sjVrJt=x1;I#Qh63Wb9>jyaZmA3#eQA=s5c-VUZt2^*UX>G0DiOiW` zcB)4dp9s(ZZ-VsTYJ82N0cH9w%GvTf}fdxwq`!S-*?i~=NSzHVA1>}PY?_NVA=d6FA$Ic@L+zD z2M9<2crri9Is|9{Jer?m83Gglo|Z-4G_?#YWidmNtX6?80G>;~2eDQF>#|q{ngD3e z{yT5!%X9@;F?qFC1*!mO&;BP-U`WSWuOi1PKnUUUTB#i5yPW;6$IHPhv%l4Dtv!yC zTiybO{hwZ+v;(!Z0`!7a`G;t=oSYL}y)}GCQ2uwz{Mjt&L2wEL zD*ztd2beuJ!M78HnTjV-pC5-xFRQJfm%mHk*fZ#ztM#7bIF=&+I`|~{J%d;E4YRy` z>W2mpjyxcOsxqKlncmarc{FW3zW`)+w487P>;YkBoX`6yIko^BCV@`@;MmsXY%gyo znZ^N?hP?vP5U}%t3^vO-g1fSk?DFcQ@lvY*X%a~7_^LotgUCI(vm|r{kVbCLNUq_+x>RfgjcAwY zWm0H^4Gn<_lZawS1vf!VW3i7p7Lr>=3}EQ6tOkV5w3Gz{suTqvw!p z9*5Kc&@*P%I4{O0sR96F&S>dfWxVtYLTUgQ*@VIbe&@Xf()LmSgaXi7TT?G6)gahi z>Lr>FAP|5j!)5|kLf~zT?+FFK&Rw0$y&+wBW$D z?@_3yg-Z<_CaX^0#Vk4d0R+wgD3S`MCW#sXEJ;K516%->NQ?OZ7XbIV05D5a_5)l1 xSf$N;0OYfRulmwxJ^)h%(DtRdrPX`@fL}C_p^wK}8Ych%002ovPDHLkV1nULmTCY1 literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_Doom_128x64/frame_2.png b/assets/dolphin/external/L1_Doom_128x64/frame_2.png new file mode 100644 index 0000000000000000000000000000000000000000..218fdedbe5dbf80882b89df9cd844684547dd739 GIT binary patch literal 1498 zcmV<01tt24P)#>wguUeNcz%x>)7UIbW0yGO&_CCVKra zz#)iGJ>0*u^5ObnfX&bpZz(^!@?Yv24+CriRtjC6oE-kfvf8zUhXFQ$Q|1_c?;d2O zmF>l20Gq%m4?rD3i2>+0t>1&k05(CQb52I1f31EGZuAJZJ<7Yw13~-=QaFf z9`agYR{k5n?dcjg>i5W+^^O1o;4mbMiaI_8UIrLryvS9;#Q+(lc?a^Xh7e@{>JL;w zWq?Y>|7rL!gb)J&s+mA!0Iy}zXQHxn0#3jRpC!rw(3k3YVi}-Y`D{lT2A2VsSm`zb z{5!&+6)f;453ob|xZP?e$7+*)aM>8OAs1~sTDO(~%;JRsb}9d*LyMTeYGcI~b(aBX zUo-e+fD`B!_hS_ka4fKb2`U(X-p}AxDtIMb??lHqGfamQK~>SfKU(1l*x?Uo=$M-- z26%b92lbQonY*5I=2F39e9r2Zl-unCR4{-PN=kKdvOlM-en2Kpj=+<4UEu&`^8olc zP2m=99|pZMq_cQj)@^_lKEN#}uzU5xUPC{p0u%$E6`8 z039qhcxyFyCzhjot3Q?SFhFJdQa#5Q05@3eygLJr(s8?;(3F2?tX>8M9Af|+yvYqL z@7c@YnzqBqbwOf)D&=$c-r_7EYQY{@;ph%ykc_p`!KC$YbFke2sP$)+ZcQjXB#9
q!bKQT-IWO+30?-E*VfoQ4lu@B8Gtr;xNmv-r=*{?D1c)J zxYpPpW2|8V=uD1C?ExgkLz&0ZKgAF@NwtBcDrt}+a5 zo6$0$Ts>O3RhD`oq-tLTD#ITN%K`GHRINVmU^eXxLb^}uz~!0#zx8Y3o>*G*fbsnf z<%3e_hxVO&PU|n0qt31eSa$m>VV31$=Z8L{t$h5P)&`d+sDeey-Y7uw3p4}rGD6w_ zswc}@DOd)_|5|@xxMZWDL>pj?S5cE)arC7Rm-%1gINFw|oUVQ>BQb&29tK=*a{?_77w&f}ZPn_h;G9EF z;hf^Rs0d_eUD_O8*XI8Q0#+$my1!ca0Lch&->5A2EM4W&ecJXIn^VC%v0M27sSlFB zz!`<)8(3lSk|(35 A=Kufz literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_Doom_128x64/frame_20.png b/assets/dolphin/external/L1_Doom_128x64/frame_20.png new file mode 100644 index 0000000000000000000000000000000000000000..efda32966d363c04d85345d37a0b943b37269516 GIT binary patch literal 1159 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 literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_Doom_128x64/frame_28.png b/assets/dolphin/external/L1_Doom_128x64/frame_28.png new file mode 100644 index 0000000000000000000000000000000000000000..e756f177eca42ebc14500853a7d3b82d399fd151 GIT binary patch literal 1101 zcmV-T1hV^yP)Gb^@g zL11y`76Cr4T?Dv22WZc%-xW{qi>6_M`9lIDBzMypK1SX7xG($VlXE(3XIl!P{x=rCm=Y!0U1VG)c1)r)>xF zbL9X(%0PhLI9J;h0SwAS08i%M>rWfM2q3hR0HcJem%V$``e;8gS7>Ds;72eqrrNfE<2?)S#w0^Jk(UTU{p1MVVL^(hY`Ka`epl4$d zK*&mfsL9e!1|Hir+Q{ccfP^_fi{LBv6X7f6QQt)XX{;lo*7~>R2%|Q$XMUt2fT83s zZ6$z=GPL(|+flpGJ{?hNeHQ_mk%q|Ub&)OFb7YStk0=5FtssCW@(}@0_RVdTpV4@l ziU2^71n@*YYU{Cg^qqVjsUkp7hx~%iU3Sk2oMqZvOjK9&)(6qRnbpW1PG0MUUHjeByBGO7&JqG=*Vy4{dy*D zHJPM7O@I}V&->-SpXWLJ;cZsh$4hN)UQd7>(ry7q^>^Bc#{L8WTAN2; zl2qW|NxL*6$oH_!%?<*j`S$_PWFTB3imY+8kg; znVe6w?v`*@4sZ(rrVL#a0a1!{P!Rz1`xHfP2d_T`%=DdA(a!nAJ%F{(rzyhbQ82YT z5q>u^BlrIpf(9GySyvZ9_ULbljMCXlHSgHwB(S&2Ry*e@3xH^!t)r;7+R!C{6)jJ4 z&>Lj&i1h! literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_Doom_128x64/frame_29.png b/assets/dolphin/external/L1_Doom_128x64/frame_29.png new file mode 100644 index 0000000000000000000000000000000000000000..cf1f5278786c76be112118fd296403e554ba2982 GIT binary patch literal 1035 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 IM6N<$f=ve6N&o-= literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_Doom_128x64/frame_30.png b/assets/dolphin/external/L1_Doom_128x64/frame_30.png new file mode 100644 index 0000000000000000000000000000000000000000..1b16b867ad77265a1031c071b0cf4e7706c8af67 GIT binary patch literal 909 zcmV;819JR{P)`D(?E$M``m5O)SGHn9(BQ=l z1#pg)0vyi)N*(ui{oHInv;9lU(s2t`!Ia>@DIgSq2K&d&DL@)ox3j7og-`&8xKsfA ztfV|++fKA1zYq%G5SI#|9r>q~B65IGfN5MR0De|d-i>tp$N|0x1(?UB0^An)HLE#D zPyNEP7p!kvclg{1-<`5vIAc0#Y*8dGZHE=Klq=jTQNf*;%} z0DcYBn*~GFwc58{GqoMPEPT{v7OpC~Qh?Uvv?R6XJgQFXZ`^0f-vOMR0wme-$Z=d7 zIdD%eRR5furSyIJ9^m*lfu)XW%bBp$A(jasXZ(>W%^ayE99ID9SzN~Tb^Bg_`^ zl>;1C0PEx1ZBV|jRDk3E3*aVQqe~*`^W3>um22E6K=4E40F3&`0YU*j7MBXp>uaSf z?SHpDQhP%I91;ouc9E8I9{!xPFBD({*x4K#tnGEJ02Yy+Qu_YkP}+`Ep*8M|y?E?U zfJHlYjJ0zPE)_sKo3Eo?n_3k(_DITjoKS$tu`SUY)1?CR`jVDUB$uuEMGj!70H+pT z+lEkpg)vW~Jx?nIIQ~uG3b!nNL30yW1=Epl0$16+u~u1~FMef)0uW=o!iud{3h-nO zaC7tn=)4Ld2Wa&F8nZT!f(c(wuWUB)H-Xe(qj#3}WyuR0VRhb8Kh7Sp*-2n;msu{z zD;oe{udSSmBo18#;7ZF&i}DF@)`hDGqyp@`xS8elqhAY)%P=mK?|vnLs|m#}UR!jE z8|FZ*uloN2cJX5L7jCxwR}OGv^aI=xJsRBNU+hoSdw?B$%HjjuIhX4<0KnZ6%HftI jkm89A5bPs1fOGx<6Q7eF9sFVF00000NkvXXu0mjf)Y_bE literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_Doom_128x64/frame_31.png b/assets/dolphin/external/L1_Doom_128x64/frame_31.png new file mode 100644 index 0000000000000000000000000000000000000000..cec7329b4d49b99143949f2bd171eb81e1d82a5c GIT binary patch literal 736 zcmV<60w4W}P)NJ-_U@QxY)(;%0p_olbM{2CN`S z3lUXPq}EEMls~`QIWUG>KW;rW_@~wq-5Vc)i8z!9saBZAIDMfv}DHY&!G@T%=0!Z5LxwfyBORS80D;C9tS+w1tbS` zfaE{{k^==u4iq3cUct0|7qA1#BJLhKmlI%Ph@8x&zD-*1x(WV@#w4dW4XOW9pFX3-dN5J3Ls?nF2l+Rpa2v7 z?Z#q`P=EnZjNj0908{QfOYA1mgrl*JG@HaQn>Z|NCHI#8RjmVP^DV|FiaVFk4nP!NLPtbO0y)Av0I&rPkevTvWXcg< SS}n@}0000)%Snp%*~*2J>G$^#Ge#Zr*W3zA7czU z<+`r>N?yxn_6sC)Nddf^6u`?#0lb_Pz{^Piyqpxk%Si#eoD{&z`ObcUMCZQcVr?lU zeD4&%;@tPGI=BiXB7}0~TELP(r*aXXo`W`C5u^YX13OZ3KD-9ndqj`|SP1M$4K4zB zM36ba0$9R5K3^S-KfOFA0Q|#RGS`!^^m?kTV9#HA7SNKRo(>M*7^%%#dwyl803>xr ze)LI5&bPDyjsVd}Fsu?7alUr$6gcOXQp`^5l>-bBK-Bpjo0IT3zEaz$wXM1fpw;;i z+ntH_++I0AcLDTpe#!`-#Rn+ z;+nEVwBF3UoQ2kBol4gn;J)t*M*?_MsvUNqxA#~3bZi{UEWpyyzSuyjH*0oQV0iK@ zekA{}MgVOn_)x}{8*4V+DPKkd87|A8S_g0PgqZ}N+^vKPq#s3`E? za_~wC*(&cb8fsY=J6MYVQI^~iCQS|BB8uyhH+y#953owZ!UT~zcPid7ed?aTk;8;_ z@1SLhv^&yNd7C|fUOd~Or1RE$?aGk-0G6^WH5PF%7eUKvtu=7Zg42$8>i`_X>Hs5p zpLf&4$s!oGCvf)S43`8(-{({jJhB!ryJ9Dme?4LTL=g<#6Zll}6j{iCj`w81r-|UP z9AK43!;Y`CmO^)>B?o|Q@R33+b%CW3dvEueotKOzveMbo^PvyL2++Fkuk~7~Dj0pv zj3{7T()KKScgq2^2C;5A!dQE^#ct=&U4I`wDByWb43E|Us6-5($A5biiWfbsqKK-0 zmY}dhsrQG!AGZl)SRH`FvrhmY7H4r4M(Vm-W}KPS-j>&mKWJ+uEz8GP4#w zSBDZb{Emx&T)RcHe{BC~DL5(u>ev-2yyNesVwWcepgLf^@6bJg(cWqmN((a+OUfZs zZlF~{sZZ)zoRCT>b2f$U9LjrQjRxGc@RK^v_4=Pe5I*??T5`Daw9kt*8ep`ZFSV_? z5Yz)r#ne3cDd&%VmgnA<1em&abT&|b`;;9)RFXG~+7P{mqOdJYAVsjkI_iX=E%L?YQTn?k)#=p8lS^G8l! zDchDIp|u^R3ZS*YdxIU~ai+(fx=s=Sw&{_x)O$@4z-o@iXg!m&kHRI4k`4UZD^&Q- z<+^_VUHqtw7(ZCD(O6YD6qKW+>YmmP*v3;arED_^oIrGjfNi980;Q98#b4-d6qq6C{PxG)u$^00000NkvXXu0mjfPBJ1) literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_Doom_128x64/frame_33.png b/assets/dolphin/external/L1_Doom_128x64/frame_33.png new file mode 100644 index 0000000000000000000000000000000000000000..5ec336961252cb15c7d5c9a30172b1a2fcd6e723 GIT binary patch literal 1212 zcmV;t1Vj6YP)7wl3z!mYRLKLtHGEH01V;dqK@F)HA3uZdkBHz1U?Qj?EhG;R z5y6Q8CcxwF>2)wLJx1v^4J@U&RJ=RTTdlF48W`@)wsBJrfGfkfvdnbljZpVZ(b_B9 z^MPgp;Km*LVMh!>vTLU{JJ2)_AQ=T{6>-A5BdJeX4@oe)3B@!2_jrLX*N{@a z85hn~i2GeDJZgM6YN^p(gy8i6mIyxF&7K!WDtTQ$YXg?iCF5E{m6cCl9jSv2%>!tF zF+7VxS|Wt@d~dm{#ZV?db9Q&Rvv&Ny=!P92zv?bp54Gg zm&Wj>YXP;}i8A1FaQXBfCE{6D@&bT2vsV)XaBcF(13cYmk{7^nK_*|mf(U3Y=K@~K z1Jp8X2#|U?HaxzE^B5jh5dnv`TPPX>Nc+Op>1FWq7Q zj~7^(w|Id^VgS#}TQ+89x)GavEhTcNx?FlsZPR~nw0pL1!l*;6iY^9XGYXQAP0PpNYD-mb`9BmUWHqgZG zjR7cXjsw+>~+l-jCsWjYh4|EfLUnX7d2>U61~qK>U%|?ahu3mQuY#7^&Bg`NGq`NZG~2>=!b;m}&>4XwCOmX#S;c9BKrbuzevIV_;Aal%{;6k( aw9!9;EO^Zc39okm0000i8I~jkMNcU8m(fz6x3fko5F%3$WIeVS6wA*Kv9UK0;xc70*Tay-wJj2N=Nw zQPFrS6`dWDBU)mB)=}`XQte7mM@6e`)_q`b-rRC7Yb}5qt2+^-wbYT&7{V^fr-1Vp zlrfLW1H52ouh*;+_RK`iEJo30_B=5F#R6XpkSRNcZyzfSEpl$`UNnQVr~jv8kd<(D!p251I>lJ`jq==p%eQe((;WX(cI41fo>ECd58CA)WJUmt~0 zF@Sgt^wilo=7zPVQ3y;uSZD0n>08gTotY9Lj+9)V~>h-QKoz2oPQ4XAObUv5sUgF0?#IarDldIY_L5lC!C zu|b_5w&T`F{ZeyrhICz5?(YFsVuXQ8tfyrIt}pzh-g90L`@HbWDB$bR4$-}Lot7g{ z%Ldfir1xCx0QVKn0My2IP<{pFH7)ce$l$->8t|CCd4RgFCI442x6!PC6iq#^&?sM< zUB$-%x2B#vfS3xN0ZzCX!FeI~o`RGLhf0UujTOxu6IKRVZU8&Od$rZnLvR}^psUX< zH3W<_24D@iGSw11z;v%^Lmdgda#(r|{Iy)a6`D+`bW7-+dkF)yf}1Cz=SZ+#IV)