From f11df494683ecadd20fe760ec27d2e77115aee90 Mon Sep 17 00:00:00 2001 From: Georgii Surkov <37121527+gsurkov@users.noreply.github.com> Date: Fri, 28 Oct 2022 10:18:41 +0300 Subject: [PATCH 01/49] [FL-2828] Dolphin score update take 2 (#1929) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Move DolphinDeedNfcRead * Move DolphinDeedNfcReadSuccess * Move DolphinDeedNfcSave * Move DolphinDeedNfcDetectReader * Move DolphinDeedNfcEmulate * Count DolphinDeedNfcEmulate when launched from file browser * Implement most of the score accounting for NFC * Fully update Nfc icounter handling * Move DolphinDeedSubGhzFrequencyAnalyzer * Update the rest of icounter in SubGHz * Adjust SubGHz icounter handling * Adjust LFRFID icounter handling * Adjust Infrared icounter handling * Don't count renaming RFID tags as saving * Don't count renaming SubGHz signals as saving * Don't count renaming NFC tags as saving * Adjust iButton icounter handling * Minor code refactoring * Correct formatting * Account for emulating iButton keys from file manager/rpc Co-authored-by: あく --- applications/main/ibutton/ibutton.c | 3 +++ .../main/ibutton/scenes/ibutton_scene_add_value.c | 3 --- .../main/ibutton/scenes/ibutton_scene_emulate.c | 3 --- .../main/ibutton/scenes/ibutton_scene_read.c | 3 +-- .../ibutton/scenes/ibutton_scene_read_key_menu.c | 2 ++ .../main/ibutton/scenes/ibutton_scene_save_name.c | 10 ++++++++++ .../main/ibutton/scenes/ibutton_scene_save_success.c | 2 -- .../ibutton/scenes/ibutton_scene_saved_key_menu.c | 2 ++ .../main/ibutton/scenes/ibutton_scene_start.c | 2 ++ .../main/infrared/scenes/infrared_scene_learn.c | 2 ++ .../main/infrared/scenes/infrared_scene_learn_done.c | 3 --- .../scenes/infrared_scene_learn_enter_name.c | 2 ++ .../infrared/scenes/infrared_scene_learn_success.c | 3 --- applications/main/lfrfid/lfrfid.c | 5 ++++- .../main/lfrfid/scenes/lfrfid_scene_emulate.c | 3 --- .../main/lfrfid/scenes/lfrfid_scene_extra_actions.c | 3 +++ applications/main/lfrfid/scenes/lfrfid_scene_read.c | 3 +-- .../main/lfrfid/scenes/lfrfid_scene_read_key_menu.c | 2 ++ .../main/lfrfid/scenes/lfrfid_scene_save_data.c | 2 -- .../main/lfrfid/scenes/lfrfid_scene_save_name.c | 8 ++++++++ .../main/lfrfid/scenes/lfrfid_scene_save_success.c | 2 -- .../main/lfrfid/scenes/lfrfid_scene_saved_key_menu.c | 2 ++ applications/main/lfrfid/scenes/lfrfid_scene_start.c | 2 ++ applications/main/nfc/nfc.c | 4 ++++ .../main/nfc/scenes/nfc_scene_detect_reader.c | 2 -- applications/main/nfc/scenes/nfc_scene_emulate_uid.c | 2 -- .../main/nfc/scenes/nfc_scene_emv_read_success.c | 2 -- .../nfc/scenes/nfc_scene_mf_classic_dict_attack.c | 4 ++++ .../main/nfc/scenes/nfc_scene_mf_classic_emulate.c | 2 -- .../main/nfc/scenes/nfc_scene_mf_classic_keys_add.c | 2 ++ .../main/nfc/scenes/nfc_scene_mf_classic_menu.c | 7 +++++-- .../nfc/scenes/nfc_scene_mf_classic_read_success.c | 3 --- .../main/nfc/scenes/nfc_scene_mf_desfire_menu.c | 6 ++++++ .../nfc/scenes/nfc_scene_mf_ultralight_emulate.c | 2 -- .../main/nfc/scenes/nfc_scene_mf_ultralight_menu.c | 6 ++++++ .../nfc/scenes/nfc_scene_mf_ultralight_read_auth.c | 2 -- .../nfc_scene_mf_ultralight_read_auth_result.c | 4 ---- .../scenes/nfc_scene_mf_ultralight_read_success.c | 2 -- .../nfc/scenes/nfc_scene_mf_ultralight_unlock_warn.c | 2 ++ applications/main/nfc/scenes/nfc_scene_nfca_menu.c | 6 ++++++ .../main/nfc/scenes/nfc_scene_nfca_read_success.c | 3 --- applications/main/nfc/scenes/nfc_scene_read.c | 7 ++++++- .../main/nfc/scenes/nfc_scene_read_card_success.c | 2 -- applications/main/nfc/scenes/nfc_scene_save_name.c | 8 ++++++++ .../main/nfc/scenes/nfc_scene_save_success.c | 2 -- applications/main/nfc/scenes/nfc_scene_saved_menu.c | 2 ++ applications/main/nfc/scenes/nfc_scene_set_uid.c | 2 -- applications/main/nfc/scenes/nfc_scene_start.c | 3 +++ .../subghz/scenes/subghz_scene_frequency_analyzer.c | 2 -- .../main/subghz/scenes/subghz_scene_read_raw.c | 7 ++++++- .../main/subghz/scenes/subghz_scene_receiver.c | 2 ++ .../main/subghz/scenes/subghz_scene_receiver_info.c | 2 -- .../main/subghz/scenes/subghz_scene_save_name.c | 12 ++++++++++++ .../main/subghz/scenes/subghz_scene_save_success.c | 3 --- .../main/subghz/scenes/subghz_scene_set_type.c | 2 -- applications/main/subghz/scenes/subghz_scene_start.c | 2 ++ .../main/subghz/scenes/subghz_scene_transmitter.c | 2 +- applications/services/dolphin/helpers/dolphin_deed.c | 2 +- applications/services/dolphin/helpers/dolphin_deed.h | 2 +- 59 files changed, 125 insertions(+), 72 deletions(-) diff --git a/applications/main/ibutton/ibutton.c b/applications/main/ibutton/ibutton.c index 887fb3e75..b6d8361b3 100644 --- a/applications/main/ibutton/ibutton.c +++ b/applications/main/ibutton/ibutton.c @@ -5,6 +5,7 @@ #include #include #include +#include #define TAG "iButtonApp" @@ -337,11 +338,13 @@ int32_t ibutton_app(void* p) { view_dispatcher_attach_to_gui( ibutton->view_dispatcher, ibutton->gui, ViewDispatcherTypeDesktop); scene_manager_next_scene(ibutton->scene_manager, iButtonSceneRpc); + DOLPHIN_DEED(DolphinDeedIbuttonEmulate); } else { view_dispatcher_attach_to_gui( ibutton->view_dispatcher, ibutton->gui, ViewDispatcherTypeFullscreen); if(key_loaded) { scene_manager_next_scene(ibutton->scene_manager, iButtonSceneEmulate); + DOLPHIN_DEED(DolphinDeedIbuttonEmulate); } else { scene_manager_next_scene(ibutton->scene_manager, iButtonSceneStart); } diff --git a/applications/main/ibutton/scenes/ibutton_scene_add_value.c b/applications/main/ibutton/scenes/ibutton_scene_add_value.c index b3ec11a50..ccac76121 100644 --- a/applications/main/ibutton/scenes/ibutton_scene_add_value.c +++ b/applications/main/ibutton/scenes/ibutton_scene_add_value.c @@ -1,7 +1,5 @@ #include "../ibutton_i.h" -#include - void ibutton_scene_add_type_byte_input_callback(void* context) { iButton* ibutton = context; view_dispatcher_send_custom_event(ibutton->view_dispatcher, iButtonCustomEventByteEditResult); @@ -38,7 +36,6 @@ bool ibutton_scene_add_value_on_event(void* context, SceneManagerEvent event) { consumed = true; if(event.event == iButtonCustomEventByteEditResult) { ibutton_key_set_data(ibutton->key, new_key_data, IBUTTON_KEY_DATA_SIZE); - DOLPHIN_DEED(DolphinDeedIbuttonAdd); scene_manager_next_scene(ibutton->scene_manager, iButtonSceneSaveName); } } diff --git a/applications/main/ibutton/scenes/ibutton_scene_emulate.c b/applications/main/ibutton/scenes/ibutton_scene_emulate.c index b3bc38ead..6f6ffcf57 100644 --- a/applications/main/ibutton/scenes/ibutton_scene_emulate.c +++ b/applications/main/ibutton/scenes/ibutton_scene_emulate.c @@ -1,6 +1,5 @@ #include "../ibutton_i.h" #include -#include #include #define EMULATE_TIMEOUT_TICKS 10 @@ -26,8 +25,6 @@ void ibutton_scene_emulate_on_enter(void* context) { path_extract_filename(ibutton->file_path, key_name, true); } - DOLPHIN_DEED(DolphinDeedIbuttonEmulate); - // check that stored key has name if(!furi_string_empty(key_name)) { ibutton_text_store_set(ibutton, "%s", furi_string_get_cstr(key_name)); diff --git a/applications/main/ibutton/scenes/ibutton_scene_read.c b/applications/main/ibutton/scenes/ibutton_scene_read.c index 05920a0ad..1fe75e45a 100644 --- a/applications/main/ibutton/scenes/ibutton_scene_read.c +++ b/applications/main/ibutton/scenes/ibutton_scene_read.c @@ -11,7 +11,6 @@ void ibutton_scene_read_on_enter(void* context) { Popup* popup = ibutton->popup; iButtonKey* key = ibutton->key; iButtonWorker* worker = ibutton->key_worker; - DOLPHIN_DEED(DolphinDeedIbuttonRead); popup_set_header(popup, "iButton", 95, 26, AlignCenter, AlignBottom); popup_set_text(popup, "Waiting\nfor key ...", 95, 30, AlignCenter, AlignTop); @@ -54,8 +53,8 @@ bool ibutton_scene_read_on_event(void* context, SceneManagerEvent event) { if(success) { ibutton_notification_message(ibutton, iButtonNotificationMessageSuccess); ibutton_notification_message(ibutton, iButtonNotificationMessageGreenOn); - DOLPHIN_DEED(DolphinDeedIbuttonReadSuccess); scene_manager_next_scene(scene_manager, iButtonSceneReadSuccess); + DOLPHIN_DEED(DolphinDeedIbuttonReadSuccess); } } } diff --git a/applications/main/ibutton/scenes/ibutton_scene_read_key_menu.c b/applications/main/ibutton/scenes/ibutton_scene_read_key_menu.c index 921b24fc1..0a8ecfa55 100644 --- a/applications/main/ibutton/scenes/ibutton_scene_read_key_menu.c +++ b/applications/main/ibutton/scenes/ibutton_scene_read_key_menu.c @@ -1,4 +1,5 @@ #include "../ibutton_i.h" +#include typedef enum { SubmenuIndexSave, @@ -49,6 +50,7 @@ bool ibutton_scene_read_key_menu_on_event(void* context, SceneManagerEvent event scene_manager_next_scene(ibutton->scene_manager, iButtonSceneSaveName); } else if(event.event == SubmenuIndexEmulate) { scene_manager_next_scene(ibutton->scene_manager, iButtonSceneEmulate); + DOLPHIN_DEED(DolphinDeedIbuttonEmulate); } else if(event.event == SubmenuIndexWrite) { scene_manager_next_scene(ibutton->scene_manager, iButtonSceneWrite); } diff --git a/applications/main/ibutton/scenes/ibutton_scene_save_name.c b/applications/main/ibutton/scenes/ibutton_scene_save_name.c index 773b93e0c..5f25a0002 100644 --- a/applications/main/ibutton/scenes/ibutton_scene_save_name.c +++ b/applications/main/ibutton/scenes/ibutton_scene_save_name.c @@ -1,6 +1,7 @@ #include "../ibutton_i.h" #include #include +#include static void ibutton_scene_save_name_text_input_callback(void* context) { iButton* ibutton = context; @@ -57,6 +58,15 @@ bool ibutton_scene_save_name_on_event(void* context, SceneManagerEvent event) { if(event.event == iButtonCustomEventTextEditResult) { if(ibutton_save_key(ibutton, ibutton->text_store)) { scene_manager_next_scene(ibutton->scene_manager, iButtonSceneSaveSuccess); + if(scene_manager_has_previous_scene( + ibutton->scene_manager, iButtonSceneSavedKeyMenu)) { + // Nothing, do not count editing as saving + } else if(scene_manager_has_previous_scene( + ibutton->scene_manager, iButtonSceneAddType)) { + DOLPHIN_DEED(DolphinDeedIbuttonAdd); + } else { + DOLPHIN_DEED(DolphinDeedIbuttonSave); + } } else { const uint32_t possible_scenes[] = { iButtonSceneReadKeyMenu, iButtonSceneSavedKeyMenu, iButtonSceneAddType}; diff --git a/applications/main/ibutton/scenes/ibutton_scene_save_success.c b/applications/main/ibutton/scenes/ibutton_scene_save_success.c index 43237f429..e0b9b3c47 100644 --- a/applications/main/ibutton/scenes/ibutton_scene_save_success.c +++ b/applications/main/ibutton/scenes/ibutton_scene_save_success.c @@ -1,5 +1,4 @@ #include "../ibutton_i.h" -#include static void ibutton_scene_save_success_popup_callback(void* context) { iButton* ibutton = context; @@ -9,7 +8,6 @@ static void ibutton_scene_save_success_popup_callback(void* context) { void ibutton_scene_save_success_on_enter(void* context) { iButton* ibutton = context; Popup* popup = ibutton->popup; - DOLPHIN_DEED(DolphinDeedIbuttonSave); popup_set_icon(popup, 32, 5, &I_DolphinNice_96x59); popup_set_header(popup, "Saved!", 5, 7, AlignLeft, AlignTop); diff --git a/applications/main/ibutton/scenes/ibutton_scene_saved_key_menu.c b/applications/main/ibutton/scenes/ibutton_scene_saved_key_menu.c index 3d588dd02..e4c9c350a 100644 --- a/applications/main/ibutton/scenes/ibutton_scene_saved_key_menu.c +++ b/applications/main/ibutton/scenes/ibutton_scene_saved_key_menu.c @@ -1,4 +1,5 @@ #include "../ibutton_i.h" +#include enum SubmenuIndex { SubmenuIndexEmulate, @@ -58,6 +59,7 @@ bool ibutton_scene_saved_key_menu_on_event(void* context, SceneManagerEvent even consumed = true; if(event.event == SubmenuIndexEmulate) { scene_manager_next_scene(ibutton->scene_manager, iButtonSceneEmulate); + DOLPHIN_DEED(DolphinDeedIbuttonEmulate); } else if(event.event == SubmenuIndexWrite) { scene_manager_next_scene(ibutton->scene_manager, iButtonSceneWrite); } else if(event.event == SubmenuIndexEdit) { diff --git a/applications/main/ibutton/scenes/ibutton_scene_start.c b/applications/main/ibutton/scenes/ibutton_scene_start.c index dde224e15..b8f6b07d6 100644 --- a/applications/main/ibutton/scenes/ibutton_scene_start.c +++ b/applications/main/ibutton/scenes/ibutton_scene_start.c @@ -1,5 +1,6 @@ #include "../ibutton_i.h" #include "ibutton/scenes/ibutton_scene.h" +#include enum SubmenuIndex { SubmenuIndexRead, @@ -38,6 +39,7 @@ bool ibutton_scene_start_on_event(void* context, SceneManagerEvent event) { consumed = true; if(event.event == SubmenuIndexRead) { scene_manager_next_scene(ibutton->scene_manager, iButtonSceneRead); + DOLPHIN_DEED(DolphinDeedIbuttonRead); } else if(event.event == SubmenuIndexSaved) { furi_string_set(ibutton->file_path, IBUTTON_APP_FOLDER); scene_manager_next_scene(ibutton->scene_manager, iButtonSceneSelectKey); diff --git a/applications/main/infrared/scenes/infrared_scene_learn.c b/applications/main/infrared/scenes/infrared_scene_learn.c index 37f9b3e05..48699a71f 100644 --- a/applications/main/infrared/scenes/infrared_scene_learn.c +++ b/applications/main/infrared/scenes/infrared_scene_learn.c @@ -1,4 +1,5 @@ #include "../infrared_i.h" +#include void infrared_scene_learn_on_enter(void* context) { Infrared* infrared = context; @@ -27,6 +28,7 @@ bool infrared_scene_learn_on_event(void* context, SceneManagerEvent event) { if(event.event == InfraredCustomEventTypeSignalReceived) { infrared_play_notification_message(infrared, InfraredNotificationMessageSuccess); scene_manager_next_scene(infrared->scene_manager, InfraredSceneLearnSuccess); + DOLPHIN_DEED(DolphinDeedIrLearnSuccess); consumed = true; } } diff --git a/applications/main/infrared/scenes/infrared_scene_learn_done.c b/applications/main/infrared/scenes/infrared_scene_learn_done.c index 7d3571715..54b7da724 100644 --- a/applications/main/infrared/scenes/infrared_scene_learn_done.c +++ b/applications/main/infrared/scenes/infrared_scene_learn_done.c @@ -1,13 +1,10 @@ #include "../infrared_i.h" -#include - void infrared_scene_learn_done_on_enter(void* context) { Infrared* infrared = context; Popup* popup = infrared->popup; popup_set_icon(popup, 32, 5, &I_DolphinNice_96x59); - DOLPHIN_DEED(DolphinDeedIrSave); if(infrared->app_state.is_learning_new_remote) { popup_set_header(popup, "New remote\ncreated!", 0, 0, AlignLeft, AlignTop); diff --git a/applications/main/infrared/scenes/infrared_scene_learn_enter_name.c b/applications/main/infrared/scenes/infrared_scene_learn_enter_name.c index b6a7eac0d..a8772a985 100644 --- a/applications/main/infrared/scenes/infrared_scene_learn_enter_name.c +++ b/applications/main/infrared/scenes/infrared_scene_learn_enter_name.c @@ -1,4 +1,5 @@ #include "../infrared_i.h" +#include void infrared_scene_learn_enter_name_on_enter(void* context) { Infrared* infrared = context; @@ -49,6 +50,7 @@ bool infrared_scene_learn_enter_name_on_event(void* context, SceneManagerEvent e if(success) { scene_manager_next_scene(scene_manager, InfraredSceneLearnDone); + DOLPHIN_DEED(DolphinDeedIrSave); } else { dialog_message_show_storage_error(infrared->dialogs, "Failed to save file"); const uint32_t possible_scenes[] = {InfraredSceneRemoteList, InfraredSceneStart}; diff --git a/applications/main/infrared/scenes/infrared_scene_learn_success.c b/applications/main/infrared/scenes/infrared_scene_learn_success.c index 466627144..469d4de9e 100644 --- a/applications/main/infrared/scenes/infrared_scene_learn_success.c +++ b/applications/main/infrared/scenes/infrared_scene_learn_success.c @@ -1,7 +1,5 @@ #include "../infrared_i.h" -#include - static void infrared_scene_learn_success_dialog_result_callback(DialogExResult result, void* context) { Infrared* infrared = context; @@ -13,7 +11,6 @@ void infrared_scene_learn_success_on_enter(void* context) { DialogEx* dialog_ex = infrared->dialog_ex; InfraredSignal* signal = infrared->received_signal; - DOLPHIN_DEED(DolphinDeedIrLearnSuccess); infrared_play_notification_message(infrared, InfraredNotificationMessageGreenOn); if(infrared_signal_is_raw(signal)) { diff --git a/applications/main/lfrfid/lfrfid.c b/applications/main/lfrfid/lfrfid.c index b0f989374..513227306 100644 --- a/applications/main/lfrfid/lfrfid.c +++ b/applications/main/lfrfid/lfrfid.c @@ -1,4 +1,5 @@ #include "lfrfid_i.h" +#include static bool lfrfid_debug_custom_event_callback(void* context, uint32_t event) { furi_assert(context); @@ -182,12 +183,14 @@ int32_t lfrfid_app(void* p) { view_dispatcher_attach_to_gui( app->view_dispatcher, app->gui, ViewDispatcherTypeDesktop); scene_manager_next_scene(app->scene_manager, LfRfidSceneRpc); + DOLPHIN_DEED(DolphinDeedRfidEmulate); } else { furi_string_set(app->file_path, args); lfrfid_load_key_data(app, app->file_path, true); view_dispatcher_attach_to_gui( app->view_dispatcher, app->gui, ViewDispatcherTypeFullscreen); scene_manager_next_scene(app->scene_manager, LfRfidSceneEmulate); + DOLPHIN_DEED(DolphinDeedRfidEmulate); } } else { @@ -311,4 +314,4 @@ void lfrfid_widget_callback(GuiButtonType result, InputType type, void* context) void lfrfid_text_input_callback(void* context) { LfRfid* app = context; view_dispatcher_send_custom_event(app->view_dispatcher, LfRfidEventNext); -} \ No newline at end of file +} diff --git a/applications/main/lfrfid/scenes/lfrfid_scene_emulate.c b/applications/main/lfrfid/scenes/lfrfid_scene_emulate.c index 2725982f0..dc3918994 100644 --- a/applications/main/lfrfid/scenes/lfrfid_scene_emulate.c +++ b/applications/main/lfrfid/scenes/lfrfid_scene_emulate.c @@ -1,12 +1,9 @@ #include "../lfrfid_i.h" -#include void lfrfid_scene_emulate_on_enter(void* context) { LfRfid* app = context; Popup* popup = app->popup; - DOLPHIN_DEED(DolphinDeedRfidEmulate); - popup_set_header(popup, "Emulating", 89, 30, AlignCenter, AlignTop); if(!furi_string_empty(app->file_name)) { popup_set_text(popup, furi_string_get_cstr(app->file_name), 89, 43, AlignCenter, AlignTop); diff --git a/applications/main/lfrfid/scenes/lfrfid_scene_extra_actions.c b/applications/main/lfrfid/scenes/lfrfid_scene_extra_actions.c index d7fd93e19..fac2ebcec 100644 --- a/applications/main/lfrfid/scenes/lfrfid_scene_extra_actions.c +++ b/applications/main/lfrfid/scenes/lfrfid_scene_extra_actions.c @@ -1,4 +1,5 @@ #include "../lfrfid_i.h" +#include typedef enum { SubmenuIndexASK, @@ -57,10 +58,12 @@ bool lfrfid_scene_extra_actions_on_event(void* context, SceneManagerEvent event) if(event.event == SubmenuIndexASK) { app->read_type = LFRFIDWorkerReadTypeASKOnly; scene_manager_next_scene(app->scene_manager, LfRfidSceneRead); + DOLPHIN_DEED(DolphinDeedRfidRead); consumed = true; } else if(event.event == SubmenuIndexPSK) { app->read_type = LFRFIDWorkerReadTypePSKOnly; scene_manager_next_scene(app->scene_manager, LfRfidSceneRead); + DOLPHIN_DEED(DolphinDeedRfidRead); consumed = true; } else if(event.event == SubmenuIndexRAW) { scene_manager_next_scene(app->scene_manager, LfRfidSceneRawName); diff --git a/applications/main/lfrfid/scenes/lfrfid_scene_read.c b/applications/main/lfrfid/scenes/lfrfid_scene_read.c index 4bdb215d1..5f1959728 100644 --- a/applications/main/lfrfid/scenes/lfrfid_scene_read.c +++ b/applications/main/lfrfid/scenes/lfrfid_scene_read.c @@ -46,7 +46,6 @@ static void void lfrfid_scene_read_on_enter(void* context) { LfRfid* app = context; - DOLPHIN_DEED(DolphinDeedRfidRead); if(app->read_type == LFRFIDWorkerReadTypePSKOnly) { lfrfid_view_read_set_read_mode(app->read_view, LfRfidReadPskOnly); } else if(app->read_type == LFRFIDWorkerReadTypeASKOnly) { @@ -79,10 +78,10 @@ bool lfrfid_scene_read_on_event(void* context, SceneManagerEvent event) { consumed = true; } else if(event.event == LfRfidEventReadDone) { app->protocol_id = app->protocol_id_next; - DOLPHIN_DEED(DolphinDeedRfidReadSuccess); notification_message(app->notifications, &sequence_success); furi_string_reset(app->file_name); scene_manager_next_scene(app->scene_manager, LfRfidSceneReadSuccess); + DOLPHIN_DEED(DolphinDeedRfidReadSuccess); consumed = true; } else if(event.event == LfRfidEventReadStartPSK) { if(app->read_type == LFRFIDWorkerReadTypeAuto) { diff --git a/applications/main/lfrfid/scenes/lfrfid_scene_read_key_menu.c b/applications/main/lfrfid/scenes/lfrfid_scene_read_key_menu.c index 7480304b6..081c47912 100644 --- a/applications/main/lfrfid/scenes/lfrfid_scene_read_key_menu.c +++ b/applications/main/lfrfid/scenes/lfrfid_scene_read_key_menu.c @@ -1,4 +1,5 @@ #include "../lfrfid_i.h" +#include typedef enum { SubmenuIndexSave, @@ -43,6 +44,7 @@ bool lfrfid_scene_read_key_menu_on_event(void* context, SceneManagerEvent event) consumed = true; } else if(event.event == SubmenuIndexEmulate) { scene_manager_next_scene(app->scene_manager, LfRfidSceneEmulate); + DOLPHIN_DEED(DolphinDeedRfidEmulate); consumed = true; } scene_manager_set_scene_state(app->scene_manager, LfRfidSceneReadKeyMenu, event.event); diff --git a/applications/main/lfrfid/scenes/lfrfid_scene_save_data.c b/applications/main/lfrfid/scenes/lfrfid_scene_save_data.c index 6c5ea2f2d..11a687bdd 100644 --- a/applications/main/lfrfid/scenes/lfrfid_scene_save_data.c +++ b/applications/main/lfrfid/scenes/lfrfid_scene_save_data.c @@ -1,5 +1,4 @@ #include "../lfrfid_i.h" -#include void lfrfid_scene_save_data_on_enter(void* context) { LfRfid* app = context; @@ -32,7 +31,6 @@ bool lfrfid_scene_save_data_on_event(void* context, SceneManagerEvent event) { consumed = true; size_t size = protocol_dict_get_data_size(app->dict, app->protocol_id); protocol_dict_set_data(app->dict, app->protocol_id, app->new_key_data, size); - DOLPHIN_DEED(DolphinDeedRfidAdd); scene_manager_next_scene(scene_manager, LfRfidSceneSaveName); scene_manager_set_scene_state(scene_manager, LfRfidSceneSaveData, 1); } diff --git a/applications/main/lfrfid/scenes/lfrfid_scene_save_name.c b/applications/main/lfrfid/scenes/lfrfid_scene_save_name.c index ca9a52de0..87e110f18 100644 --- a/applications/main/lfrfid/scenes/lfrfid_scene_save_name.c +++ b/applications/main/lfrfid/scenes/lfrfid_scene_save_name.c @@ -1,5 +1,6 @@ #include #include "../lfrfid_i.h" +#include void lfrfid_scene_save_name_on_enter(void* context) { LfRfid* app = context; @@ -55,6 +56,13 @@ bool lfrfid_scene_save_name_on_event(void* context, SceneManagerEvent event) { if(lfrfid_save_key(app)) { scene_manager_next_scene(scene_manager, LfRfidSceneSaveSuccess); + if(scene_manager_has_previous_scene(scene_manager, LfRfidSceneSavedKeyMenu)) { + // Nothing, do not count editing as saving + } else if(scene_manager_has_previous_scene(scene_manager, LfRfidSceneSaveType)) { + DOLPHIN_DEED(DolphinDeedRfidAdd); + } else { + DOLPHIN_DEED(DolphinDeedRfidSave); + } } else { scene_manager_search_and_switch_to_previous_scene( scene_manager, LfRfidSceneReadKeyMenu); diff --git a/applications/main/lfrfid/scenes/lfrfid_scene_save_success.c b/applications/main/lfrfid/scenes/lfrfid_scene_save_success.c index e91ad04e3..52aefa848 100644 --- a/applications/main/lfrfid/scenes/lfrfid_scene_save_success.c +++ b/applications/main/lfrfid/scenes/lfrfid_scene_save_success.c @@ -1,5 +1,4 @@ #include "../lfrfid_i.h" -#include void lfrfid_scene_save_success_on_enter(void* context) { LfRfid* app = context; @@ -8,7 +7,6 @@ void lfrfid_scene_save_success_on_enter(void* context) { // Clear state of data enter scene scene_manager_set_scene_state(app->scene_manager, LfRfidSceneSaveData, 0); - DOLPHIN_DEED(DolphinDeedRfidSave); popup_set_icon(popup, 32, 5, &I_DolphinNice_96x59); popup_set_header(popup, "Saved!", 5, 7, AlignLeft, AlignTop); popup_set_context(popup, app); diff --git a/applications/main/lfrfid/scenes/lfrfid_scene_saved_key_menu.c b/applications/main/lfrfid/scenes/lfrfid_scene_saved_key_menu.c index 040b31f10..d3c3d389a 100644 --- a/applications/main/lfrfid/scenes/lfrfid_scene_saved_key_menu.c +++ b/applications/main/lfrfid/scenes/lfrfid_scene_saved_key_menu.c @@ -1,4 +1,5 @@ #include "../lfrfid_i.h" +#include typedef enum { SubmenuIndexEmulate, @@ -42,6 +43,7 @@ bool lfrfid_scene_saved_key_menu_on_event(void* context, SceneManagerEvent event if(event.type == SceneManagerEventTypeCustom) { if(event.event == SubmenuIndexEmulate) { scene_manager_next_scene(app->scene_manager, LfRfidSceneEmulate); + DOLPHIN_DEED(DolphinDeedRfidEmulate); consumed = true; } else if(event.event == SubmenuIndexWrite) { scene_manager_next_scene(app->scene_manager, LfRfidSceneWrite); diff --git a/applications/main/lfrfid/scenes/lfrfid_scene_start.c b/applications/main/lfrfid/scenes/lfrfid_scene_start.c index d6d87f441..8e1c92dbb 100644 --- a/applications/main/lfrfid/scenes/lfrfid_scene_start.c +++ b/applications/main/lfrfid/scenes/lfrfid_scene_start.c @@ -1,4 +1,5 @@ #include "../lfrfid_i.h" +#include typedef enum { SubmenuIndexRead, @@ -47,6 +48,7 @@ bool lfrfid_scene_start_on_event(void* context, SceneManagerEvent event) { if(event.type == SceneManagerEventTypeCustom) { if(event.event == SubmenuIndexRead) { scene_manager_next_scene(app->scene_manager, LfRfidSceneRead); + DOLPHIN_DEED(DolphinDeedRfidRead); consumed = true; } else if(event.event == SubmenuIndexSaved) { furi_string_set(app->file_path, LFRFID_APP_FOLDER); diff --git a/applications/main/nfc/nfc.c b/applications/main/nfc/nfc.c index 0b685f545..55c68a450 100644 --- a/applications/main/nfc/nfc.c +++ b/applications/main/nfc/nfc.c @@ -1,5 +1,6 @@ #include "nfc_i.h" #include "furi_hal_nfc.h" +#include bool nfc_custom_event_callback(void* context, uint32_t event) { furi_assert(context); @@ -275,12 +276,15 @@ int32_t nfc_app(void* p) { if(nfc_device_load(nfc->dev, p, true)) { if(nfc->dev->format == NfcDeviceSaveFormatMifareUl) { scene_manager_next_scene(nfc->scene_manager, NfcSceneMfUltralightEmulate); + DOLPHIN_DEED(DolphinDeedNfcEmulate); } else if(nfc->dev->format == NfcDeviceSaveFormatMifareClassic) { scene_manager_next_scene(nfc->scene_manager, NfcSceneMfClassicEmulate); + DOLPHIN_DEED(DolphinDeedNfcEmulate); } else if(nfc->dev->format == NfcDeviceSaveFormatBankCard) { scene_manager_next_scene(nfc->scene_manager, NfcSceneDeviceInfo); } else { scene_manager_next_scene(nfc->scene_manager, NfcSceneEmulateUid); + DOLPHIN_DEED(DolphinDeedNfcEmulate); } } else { // Exit app diff --git a/applications/main/nfc/scenes/nfc_scene_detect_reader.c b/applications/main/nfc/scenes/nfc_scene_detect_reader.c index f0177f9c1..abf1437d2 100644 --- a/applications/main/nfc/scenes/nfc_scene_detect_reader.c +++ b/applications/main/nfc/scenes/nfc_scene_detect_reader.c @@ -1,5 +1,4 @@ #include "../nfc_i.h" -#include #define NFC_SCENE_DETECT_READER_PAIR_NONCES_MAX (10U) @@ -26,7 +25,6 @@ void nfc_scene_detect_reader_callback(void* context) { void nfc_scene_detect_reader_on_enter(void* context) { Nfc* nfc = context; - DOLPHIN_DEED(DolphinDeedNfcDetectReader); detect_reader_set_callback(nfc->detect_reader, nfc_scene_detect_reader_callback, nfc); detect_reader_set_nonces_max(nfc->detect_reader, NFC_SCENE_DETECT_READER_PAIR_NONCES_MAX); diff --git a/applications/main/nfc/scenes/nfc_scene_emulate_uid.c b/applications/main/nfc/scenes/nfc_scene_emulate_uid.c index 8bb207960..5ddb60992 100644 --- a/applications/main/nfc/scenes/nfc_scene_emulate_uid.c +++ b/applications/main/nfc/scenes/nfc_scene_emulate_uid.c @@ -1,5 +1,4 @@ #include "../nfc_i.h" -#include #define NFC_SCENE_EMULATE_UID_LOG_SIZE_MAX (200) @@ -59,7 +58,6 @@ static void nfc_scene_emulate_uid_widget_config(Nfc* nfc, bool data_received) { void nfc_scene_emulate_uid_on_enter(void* context) { Nfc* nfc = context; - DOLPHIN_DEED(DolphinDeedNfcEmulate); // Setup Widget nfc_scene_emulate_uid_widget_config(nfc, false); diff --git a/applications/main/nfc/scenes/nfc_scene_emv_read_success.c b/applications/main/nfc/scenes/nfc_scene_emv_read_success.c index 6a0b32fad..005b76cb2 100644 --- a/applications/main/nfc/scenes/nfc_scene_emv_read_success.c +++ b/applications/main/nfc/scenes/nfc_scene_emv_read_success.c @@ -1,6 +1,5 @@ #include "../nfc_i.h" #include "../helpers/nfc_emv_parser.h" -#include void nfc_scene_emv_read_success_widget_callback( GuiButtonType result, @@ -15,7 +14,6 @@ void nfc_scene_emv_read_success_widget_callback( void nfc_scene_emv_read_success_on_enter(void* context) { Nfc* nfc = context; EmvData* emv_data = &nfc->dev->dev_data.emv_data; - DOLPHIN_DEED(DolphinDeedNfcReadSuccess); // Setup Custom Widget view widget_add_button_element( diff --git a/applications/main/nfc/scenes/nfc_scene_mf_classic_dict_attack.c b/applications/main/nfc/scenes/nfc_scene_mf_classic_dict_attack.c index 813546905..acb5b783c 100644 --- a/applications/main/nfc/scenes/nfc_scene_mf_classic_dict_attack.c +++ b/applications/main/nfc/scenes/nfc_scene_mf_classic_dict_attack.c @@ -1,4 +1,5 @@ #include "../nfc_i.h" +#include #define TAG "NfcMfClassicDictAttack" @@ -110,6 +111,7 @@ bool nfc_scene_mf_classic_dict_attack_on_event(void* context, SceneManagerEvent } else { notification_message(nfc->notifications, &sequence_success); scene_manager_next_scene(nfc->scene_manager, NfcSceneMfClassicReadSuccess); + DOLPHIN_DEED(DolphinDeedNfcReadSuccess); consumed = true; } } else if(event.event == NfcWorkerEventAborted) { @@ -119,6 +121,8 @@ bool nfc_scene_mf_classic_dict_attack_on_event(void* context, SceneManagerEvent } else { notification_message(nfc->notifications, &sequence_success); scene_manager_next_scene(nfc->scene_manager, NfcSceneMfClassicReadSuccess); + // Counting failed attempts too + DOLPHIN_DEED(DolphinDeedNfcReadSuccess); consumed = true; } } else if(event.event == NfcWorkerEventCardDetected) { diff --git a/applications/main/nfc/scenes/nfc_scene_mf_classic_emulate.c b/applications/main/nfc/scenes/nfc_scene_mf_classic_emulate.c index 65639b2b4..e514fa728 100644 --- a/applications/main/nfc/scenes/nfc_scene_mf_classic_emulate.c +++ b/applications/main/nfc/scenes/nfc_scene_mf_classic_emulate.c @@ -1,5 +1,4 @@ #include "../nfc_i.h" -#include #define NFC_MF_CLASSIC_DATA_NOT_CHANGED (0UL) #define NFC_MF_CLASSIC_DATA_CHANGED (1UL) @@ -15,7 +14,6 @@ bool nfc_mf_classic_emulate_worker_callback(NfcWorkerEvent event, void* context) void nfc_scene_mf_classic_emulate_on_enter(void* context) { Nfc* nfc = context; - DOLPHIN_DEED(DolphinDeedNfcEmulate); // Setup view Popup* popup = nfc->popup; diff --git a/applications/main/nfc/scenes/nfc_scene_mf_classic_keys_add.c b/applications/main/nfc/scenes/nfc_scene_mf_classic_keys_add.c index 2921d21c9..b122aa225 100644 --- a/applications/main/nfc/scenes/nfc_scene_mf_classic_keys_add.c +++ b/applications/main/nfc/scenes/nfc_scene_mf_classic_keys_add.c @@ -1,4 +1,5 @@ #include "../nfc_i.h" +#include void nfc_scene_mf_classic_keys_add_byte_input_callback(void* context) { Nfc* nfc = context; @@ -36,6 +37,7 @@ bool nfc_scene_mf_classic_keys_add_on_event(void* context, SceneManagerEvent eve nfc->scene_manager, NfcSceneMfClassicKeysWarnDuplicate); } else if(mf_classic_dict_add_key(dict, nfc->byte_input_store)) { scene_manager_next_scene(nfc->scene_manager, NfcSceneSaveSuccess); + DOLPHIN_DEED(DolphinDeedNfcMfcAdd); } else { scene_manager_next_scene(nfc->scene_manager, NfcSceneDictNotFound); } diff --git a/applications/main/nfc/scenes/nfc_scene_mf_classic_menu.c b/applications/main/nfc/scenes/nfc_scene_mf_classic_menu.c index 2cba04337..a5bb10ddf 100644 --- a/applications/main/nfc/scenes/nfc_scene_mf_classic_menu.c +++ b/applications/main/nfc/scenes/nfc_scene_mf_classic_menu.c @@ -36,8 +36,6 @@ bool nfc_scene_mf_classic_menu_on_event(void* context, SceneManagerEvent event) if(event.type == SceneManagerEventTypeCustom) { if(event.event == SubmenuIndexSave) { - DOLPHIN_DEED(DolphinDeedNfcMfcAdd); - scene_manager_set_scene_state( nfc->scene_manager, NfcSceneMfClassicMenu, SubmenuIndexSave); nfc->dev->format = NfcDeviceSaveFormatMifareClassic; @@ -49,6 +47,11 @@ bool nfc_scene_mf_classic_menu_on_event(void* context, SceneManagerEvent event) scene_manager_set_scene_state( nfc->scene_manager, NfcSceneMfClassicMenu, SubmenuIndexEmulate); scene_manager_next_scene(nfc->scene_manager, NfcSceneMfClassicEmulate); + if(scene_manager_has_previous_scene(nfc->scene_manager, NfcSceneSetType)) { + DOLPHIN_DEED(DolphinDeedNfcAddEmulate); + } else { + DOLPHIN_DEED(DolphinDeedNfcEmulate); + } consumed = true; } else if(event.event == SubmenuIndexInfo) { scene_manager_set_scene_state( diff --git a/applications/main/nfc/scenes/nfc_scene_mf_classic_read_success.c b/applications/main/nfc/scenes/nfc_scene_mf_classic_read_success.c index 0cdb86464..ae31e92cc 100644 --- a/applications/main/nfc/scenes/nfc_scene_mf_classic_read_success.c +++ b/applications/main/nfc/scenes/nfc_scene_mf_classic_read_success.c @@ -1,5 +1,4 @@ #include "../nfc_i.h" -#include void nfc_scene_mf_classic_read_success_widget_callback( GuiButtonType result, @@ -18,8 +17,6 @@ void nfc_scene_mf_classic_read_success_on_enter(void* context) { NfcDeviceData* dev_data = &nfc->dev->dev_data; MfClassicData* mf_data = &dev_data->mf_classic_data; - DOLPHIN_DEED(DolphinDeedNfcReadSuccess); - // Setup view Widget* widget = nfc->widget; widget_add_button_element( diff --git a/applications/main/nfc/scenes/nfc_scene_mf_desfire_menu.c b/applications/main/nfc/scenes/nfc_scene_mf_desfire_menu.c index 1e2f2d2f2..bee63d775 100644 --- a/applications/main/nfc/scenes/nfc_scene_mf_desfire_menu.c +++ b/applications/main/nfc/scenes/nfc_scene_mf_desfire_menu.c @@ -1,4 +1,5 @@ #include "../nfc_i.h" +#include enum SubmenuIndex { SubmenuIndexSave, @@ -48,6 +49,11 @@ bool nfc_scene_mf_desfire_menu_on_event(void* context, SceneManagerEvent event) consumed = true; } else if(event.event == SubmenuIndexEmulateUid) { scene_manager_next_scene(nfc->scene_manager, NfcSceneEmulateUid); + if(scene_manager_has_previous_scene(nfc->scene_manager, NfcSceneSetType)) { + DOLPHIN_DEED(DolphinDeedNfcAddEmulate); + } else { + DOLPHIN_DEED(DolphinDeedNfcEmulate); + } consumed = true; } else if(event.event == SubmenuIndexInfo) { scene_manager_next_scene(nfc->scene_manager, NfcSceneNfcDataInfo); diff --git a/applications/main/nfc/scenes/nfc_scene_mf_ultralight_emulate.c b/applications/main/nfc/scenes/nfc_scene_mf_ultralight_emulate.c index 712ddc077..e84fb3927 100644 --- a/applications/main/nfc/scenes/nfc_scene_mf_ultralight_emulate.c +++ b/applications/main/nfc/scenes/nfc_scene_mf_ultralight_emulate.c @@ -1,5 +1,4 @@ #include "../nfc_i.h" -#include #define NFC_MF_UL_DATA_NOT_CHANGED (0UL) #define NFC_MF_UL_DATA_CHANGED (1UL) @@ -15,7 +14,6 @@ bool nfc_mf_ultralight_emulate_worker_callback(NfcWorkerEvent event, void* conte void nfc_scene_mf_ultralight_emulate_on_enter(void* context) { Nfc* nfc = context; - DOLPHIN_DEED(DolphinDeedNfcEmulate); // Setup view Popup* popup = nfc->popup; diff --git a/applications/main/nfc/scenes/nfc_scene_mf_ultralight_menu.c b/applications/main/nfc/scenes/nfc_scene_mf_ultralight_menu.c index ba9f22338..ab4d37b09 100644 --- a/applications/main/nfc/scenes/nfc_scene_mf_ultralight_menu.c +++ b/applications/main/nfc/scenes/nfc_scene_mf_ultralight_menu.c @@ -1,4 +1,5 @@ #include "../nfc_i.h" +#include enum SubmenuIndex { SubmenuIndexUnlock, @@ -56,6 +57,11 @@ bool nfc_scene_mf_ultralight_menu_on_event(void* context, SceneManagerEvent even consumed = true; } else if(event.event == SubmenuIndexEmulate) { scene_manager_next_scene(nfc->scene_manager, NfcSceneMfUltralightEmulate); + if(scene_manager_has_previous_scene(nfc->scene_manager, NfcSceneSetType)) { + DOLPHIN_DEED(DolphinDeedNfcAddEmulate); + } else { + DOLPHIN_DEED(DolphinDeedNfcEmulate); + } consumed = true; } else if(event.event == SubmenuIndexUnlock) { scene_manager_next_scene(nfc->scene_manager, NfcSceneMfUltralightUnlockMenu); diff --git a/applications/main/nfc/scenes/nfc_scene_mf_ultralight_read_auth.c b/applications/main/nfc/scenes/nfc_scene_mf_ultralight_read_auth.c index 25008004b..5dbb0c18a 100644 --- a/applications/main/nfc/scenes/nfc_scene_mf_ultralight_read_auth.c +++ b/applications/main/nfc/scenes/nfc_scene_mf_ultralight_read_auth.c @@ -1,5 +1,4 @@ #include "../nfc_i.h" -#include typedef enum { NfcSceneMfUlReadStateIdle, @@ -51,7 +50,6 @@ void nfc_scene_mf_ultralight_read_auth_set_state(Nfc* nfc, NfcSceneMfUlReadState void nfc_scene_mf_ultralight_read_auth_on_enter(void* context) { Nfc* nfc = context; - DOLPHIN_DEED(DolphinDeedNfcRead); nfc_device_clear(nfc->dev); // Setup view diff --git a/applications/main/nfc/scenes/nfc_scene_mf_ultralight_read_auth_result.c b/applications/main/nfc/scenes/nfc_scene_mf_ultralight_read_auth_result.c index 5a690a213..178d03351 100644 --- a/applications/main/nfc/scenes/nfc_scene_mf_ultralight_read_auth_result.c +++ b/applications/main/nfc/scenes/nfc_scene_mf_ultralight_read_auth_result.c @@ -1,5 +1,4 @@ #include "../nfc_i.h" -#include void nfc_scene_mf_ultralight_read_auth_result_widget_callback( GuiButtonType result, @@ -37,7 +36,6 @@ void nfc_scene_mf_ultralight_read_auth_result_on_enter(void* context) { widget_add_string_element( widget, 0, 17, AlignLeft, AlignTop, FontSecondary, furi_string_get_cstr(temp_str)); if(mf_ul_data->auth_success) { - DOLPHIN_DEED(DolphinDeedNfcReadSuccess); furi_string_printf( temp_str, "Password: %02X %02X %02X %02X", @@ -54,8 +52,6 @@ void nfc_scene_mf_ultralight_read_auth_result_on_enter(void* context) { config_pages->auth_data.pack.raw[1]); widget_add_string_element( widget, 0, 39, AlignLeft, AlignTop, FontSecondary, furi_string_get_cstr(temp_str)); - } else { - DOLPHIN_DEED(DolphinDeedNfcMfulError); } furi_string_printf( temp_str, "Pages Read: %d/%d", mf_ul_data->data_read / 4, mf_ul_data->data_size / 4); diff --git a/applications/main/nfc/scenes/nfc_scene_mf_ultralight_read_success.c b/applications/main/nfc/scenes/nfc_scene_mf_ultralight_read_success.c index 77034ea80..63bffbf36 100644 --- a/applications/main/nfc/scenes/nfc_scene_mf_ultralight_read_success.c +++ b/applications/main/nfc/scenes/nfc_scene_mf_ultralight_read_success.c @@ -1,5 +1,4 @@ #include "../nfc_i.h" -#include void nfc_scene_mf_ultralight_read_success_widget_callback( GuiButtonType result, @@ -14,7 +13,6 @@ void nfc_scene_mf_ultralight_read_success_widget_callback( void nfc_scene_mf_ultralight_read_success_on_enter(void* context) { Nfc* nfc = context; - DOLPHIN_DEED(DolphinDeedNfcReadSuccess); // Setup widget view FuriHalNfcDevData* data = &nfc->dev->dev_data.nfc_data; diff --git a/applications/main/nfc/scenes/nfc_scene_mf_ultralight_unlock_warn.c b/applications/main/nfc/scenes/nfc_scene_mf_ultralight_unlock_warn.c index 58e081db9..514cd4e98 100644 --- a/applications/main/nfc/scenes/nfc_scene_mf_ultralight_unlock_warn.c +++ b/applications/main/nfc/scenes/nfc_scene_mf_ultralight_unlock_warn.c @@ -1,4 +1,5 @@ #include "../nfc_i.h" +#include void nfc_scene_mf_ultralight_unlock_warn_dialog_callback(DialogExResult result, void* context) { Nfc* nfc = context; @@ -30,6 +31,7 @@ bool nfc_scene_mf_ultralight_unlock_warn_on_event(void* context, SceneManagerEve if(event.type == SceneManagerEventTypeCustom) { if(event.event == DialogExResultCenter) { scene_manager_next_scene(nfc->scene_manager, NfcSceneMfUltralightReadAuth); + DOLPHIN_DEED(DolphinDeedNfcRead); consumed = true; } } diff --git a/applications/main/nfc/scenes/nfc_scene_nfca_menu.c b/applications/main/nfc/scenes/nfc_scene_nfca_menu.c index 00d0d943d..30f63945c 100644 --- a/applications/main/nfc/scenes/nfc_scene_nfca_menu.c +++ b/applications/main/nfc/scenes/nfc_scene_nfca_menu.c @@ -1,4 +1,5 @@ #include "../nfc_i.h" +#include enum SubmenuIndex { SubmenuIndexSaveUid, @@ -41,6 +42,11 @@ bool nfc_scene_nfca_menu_on_event(void* context, SceneManagerEvent event) { consumed = true; } else if(event.event == SubmenuIndexEmulateUid) { scene_manager_next_scene(nfc->scene_manager, NfcSceneEmulateUid); + if(scene_manager_has_previous_scene(nfc->scene_manager, NfcSceneSetType)) { + DOLPHIN_DEED(DolphinDeedNfcAddEmulate); + } else { + DOLPHIN_DEED(DolphinDeedNfcEmulate); + } consumed = true; } else if(event.event == SubmenuIndexInfo) { scene_manager_next_scene(nfc->scene_manager, NfcSceneNfcDataInfo); diff --git a/applications/main/nfc/scenes/nfc_scene_nfca_read_success.c b/applications/main/nfc/scenes/nfc_scene_nfca_read_success.c index 2ea7c9921..a38f31a98 100644 --- a/applications/main/nfc/scenes/nfc_scene_nfca_read_success.c +++ b/applications/main/nfc/scenes/nfc_scene_nfca_read_success.c @@ -1,5 +1,4 @@ #include "../nfc_i.h" -#include void nfc_scene_nfca_read_success_widget_callback( GuiButtonType result, @@ -16,8 +15,6 @@ void nfc_scene_nfca_read_success_widget_callback( void nfc_scene_nfca_read_success_on_enter(void* context) { Nfc* nfc = context; - DOLPHIN_DEED(DolphinDeedNfcReadSuccess); - // Setup view FuriHalNfcDevData* data = &nfc->dev->dev_data.nfc_data; Widget* widget = nfc->widget; diff --git a/applications/main/nfc/scenes/nfc_scene_read.c b/applications/main/nfc/scenes/nfc_scene_read.c index e6df476f0..1f82aef08 100644 --- a/applications/main/nfc/scenes/nfc_scene_read.c +++ b/applications/main/nfc/scenes/nfc_scene_read.c @@ -39,7 +39,6 @@ void nfc_scene_read_set_state(Nfc* nfc, NfcSceneReadState state) { void nfc_scene_read_on_enter(void* context) { Nfc* nfc = context; - DOLPHIN_DEED(DolphinDeedNfcRead); nfc_device_clear(nfc->dev); // Setup view @@ -62,26 +61,32 @@ bool nfc_scene_read_on_event(void* context, SceneManagerEvent event) { (event.event == NfcWorkerEventReadUidNfcV)) { notification_message(nfc->notifications, &sequence_success); scene_manager_next_scene(nfc->scene_manager, NfcSceneReadCardSuccess); + DOLPHIN_DEED(DolphinDeedNfcReadSuccess); consumed = true; } else if(event.event == NfcWorkerEventReadUidNfcA) { notification_message(nfc->notifications, &sequence_success); scene_manager_next_scene(nfc->scene_manager, NfcSceneNfcaReadSuccess); + DOLPHIN_DEED(DolphinDeedNfcReadSuccess); consumed = true; } else if(event.event == NfcWorkerEventReadMfUltralight) { notification_message(nfc->notifications, &sequence_success); scene_manager_next_scene(nfc->scene_manager, NfcSceneMfUltralightReadSuccess); + DOLPHIN_DEED(DolphinDeedNfcReadSuccess); consumed = true; } else if(event.event == NfcWorkerEventReadMfClassicDone) { notification_message(nfc->notifications, &sequence_success); scene_manager_next_scene(nfc->scene_manager, NfcSceneMfClassicReadSuccess); + DOLPHIN_DEED(DolphinDeedNfcReadSuccess); consumed = true; } else if(event.event == NfcWorkerEventReadMfDesfire) { notification_message(nfc->notifications, &sequence_success); scene_manager_next_scene(nfc->scene_manager, NfcSceneMfDesfireReadSuccess); + DOLPHIN_DEED(DolphinDeedNfcReadSuccess); consumed = true; } else if(event.event == NfcWorkerEventReadBankCard) { notification_message(nfc->notifications, &sequence_success); scene_manager_next_scene(nfc->scene_manager, NfcSceneEmvReadSuccess); + DOLPHIN_DEED(DolphinDeedNfcReadSuccess); consumed = true; } else if(event.event == NfcWorkerEventReadMfClassicDictAttackRequired) { if(mf_classic_dict_check_presence(MfClassicDictTypeFlipper)) { diff --git a/applications/main/nfc/scenes/nfc_scene_read_card_success.c b/applications/main/nfc/scenes/nfc_scene_read_card_success.c index 352cb4a7e..9b2a2188e 100644 --- a/applications/main/nfc/scenes/nfc_scene_read_card_success.c +++ b/applications/main/nfc/scenes/nfc_scene_read_card_success.c @@ -1,5 +1,4 @@ #include "../nfc_i.h" -#include void nfc_scene_read_card_success_widget_callback( GuiButtonType result, @@ -18,7 +17,6 @@ void nfc_scene_read_card_success_on_enter(void* context) { FuriString* temp_str; temp_str = furi_string_alloc(); - DOLPHIN_DEED(DolphinDeedNfcReadSuccess); // Setup view FuriHalNfcDevData* data = &nfc->dev->dev_data.nfc_data; diff --git a/applications/main/nfc/scenes/nfc_scene_save_name.c b/applications/main/nfc/scenes/nfc_scene_save_name.c index 736eab7de..791f8d99b 100644 --- a/applications/main/nfc/scenes/nfc_scene_save_name.c +++ b/applications/main/nfc/scenes/nfc_scene_save_name.c @@ -2,6 +2,7 @@ #include #include #include +#include void nfc_scene_save_name_text_input_callback(void* context) { Nfc* nfc = context; @@ -63,6 +64,13 @@ bool nfc_scene_save_name_on_event(void* context, SceneManagerEvent event) { strlcpy(nfc->dev->dev_name, nfc->text_store, strlen(nfc->text_store) + 1); if(nfc_device_save(nfc->dev, nfc->text_store)) { scene_manager_next_scene(nfc->scene_manager, NfcSceneSaveSuccess); + if(!scene_manager_has_previous_scene(nfc->scene_manager, NfcSceneSavedMenu)) { + // Nothing, do not count editing as saving + } else if(scene_manager_has_previous_scene(nfc->scene_manager, NfcSceneSetType)) { + DOLPHIN_DEED(DolphinDeedNfcAddSave); + } else { + DOLPHIN_DEED(DolphinDeedNfcSave); + } consumed = true; } else { consumed = scene_manager_search_and_switch_to_previous_scene( diff --git a/applications/main/nfc/scenes/nfc_scene_save_success.c b/applications/main/nfc/scenes/nfc_scene_save_success.c index dcd2519f1..34919cbd8 100644 --- a/applications/main/nfc/scenes/nfc_scene_save_success.c +++ b/applications/main/nfc/scenes/nfc_scene_save_success.c @@ -1,5 +1,4 @@ #include "../nfc_i.h" -#include void nfc_scene_save_success_popup_callback(void* context) { Nfc* nfc = context; @@ -8,7 +7,6 @@ void nfc_scene_save_success_popup_callback(void* context) { void nfc_scene_save_success_on_enter(void* context) { Nfc* nfc = context; - DOLPHIN_DEED(DolphinDeedNfcSave); // Setup view Popup* popup = nfc->popup; diff --git a/applications/main/nfc/scenes/nfc_scene_saved_menu.c b/applications/main/nfc/scenes/nfc_scene_saved_menu.c index fe65b5b8a..09d2c2d61 100644 --- a/applications/main/nfc/scenes/nfc_scene_saved_menu.c +++ b/applications/main/nfc/scenes/nfc_scene_saved_menu.c @@ -1,4 +1,5 @@ #include "../nfc_i.h" +#include enum SubmenuIndex { SubmenuIndexEmulate, @@ -76,6 +77,7 @@ bool nfc_scene_saved_menu_on_event(void* context, SceneManagerEvent event) { } else { scene_manager_next_scene(nfc->scene_manager, NfcSceneEmulateUid); } + DOLPHIN_DEED(DolphinDeedNfcEmulate); consumed = true; } else if(event.event == SubmenuIndexRename) { scene_manager_next_scene(nfc->scene_manager, NfcSceneSaveName); diff --git a/applications/main/nfc/scenes/nfc_scene_set_uid.c b/applications/main/nfc/scenes/nfc_scene_set_uid.c index 9622ba213..1d3fb5eb9 100644 --- a/applications/main/nfc/scenes/nfc_scene_set_uid.c +++ b/applications/main/nfc/scenes/nfc_scene_set_uid.c @@ -1,5 +1,4 @@ #include "../nfc_i.h" -#include void nfc_scene_set_uid_byte_input_callback(void* context) { Nfc* nfc = context; @@ -30,7 +29,6 @@ bool nfc_scene_set_uid_on_event(void* context, SceneManagerEvent event) { if(event.type == SceneManagerEventTypeCustom) { if(event.event == NfcCustomEventByteInputDone) { - DOLPHIN_DEED(DolphinDeedNfcAddSave); if(scene_manager_has_previous_scene(nfc->scene_manager, NfcSceneSavedMenu)) { nfc->dev->dev_data.nfc_data = nfc->dev_edit_data; if(nfc_device_save(nfc->dev, nfc->dev->dev_name)) { diff --git a/applications/main/nfc/scenes/nfc_scene_start.c b/applications/main/nfc/scenes/nfc_scene_start.c index 1a9051dfd..0c4ec1cf9 100644 --- a/applications/main/nfc/scenes/nfc_scene_start.c +++ b/applications/main/nfc/scenes/nfc_scene_start.c @@ -1,4 +1,5 @@ #include "../nfc_i.h" +#include enum SubmenuIndex { SubmenuIndexRead, @@ -47,11 +48,13 @@ bool nfc_scene_start_on_event(void* context, SceneManagerEvent event) { if(event.type == SceneManagerEventTypeCustom) { if(event.event == SubmenuIndexRead) { scene_manager_next_scene(nfc->scene_manager, NfcSceneRead); + DOLPHIN_DEED(DolphinDeedNfcRead); consumed = true; } else if(event.event == SubmenuIndexDetectReader) { bool sd_exist = storage_sd_status(nfc->dev->storage) == FSE_OK; if(sd_exist) { scene_manager_next_scene(nfc->scene_manager, NfcSceneDetectReader); + DOLPHIN_DEED(DolphinDeedNfcDetectReader); } else { scene_manager_next_scene(nfc->scene_manager, NfcSceneDictNotFound); } diff --git a/applications/main/subghz/scenes/subghz_scene_frequency_analyzer.c b/applications/main/subghz/scenes/subghz_scene_frequency_analyzer.c index 9595c61be..b48a821da 100644 --- a/applications/main/subghz/scenes/subghz_scene_frequency_analyzer.c +++ b/applications/main/subghz/scenes/subghz_scene_frequency_analyzer.c @@ -1,6 +1,5 @@ #include "../subghz_i.h" #include "../views/subghz_frequency_analyzer.h" -#include void subghz_scene_frequency_analyzer_callback(SubGhzCustomEvent event, void* context) { furi_assert(context); @@ -10,7 +9,6 @@ void subghz_scene_frequency_analyzer_callback(SubGhzCustomEvent event, void* con void subghz_scene_frequency_analyzer_on_enter(void* context) { SubGhz* subghz = context; - DOLPHIN_DEED(DolphinDeedSubGhzFrequencyAnalyzer); subghz_frequency_analyzer_set_callback( subghz->subghz_frequency_analyzer, subghz_scene_frequency_analyzer_callback, subghz); view_dispatcher_switch_to_view(subghz->view_dispatcher, SubGhzViewIdFrequencyAnalyzer); diff --git a/applications/main/subghz/scenes/subghz_scene_read_raw.c b/applications/main/subghz/scenes/subghz_scene_read_raw.c index 8ac9bf5ba..ba9ce803b 100644 --- a/applications/main/subghz/scenes/subghz_scene_read_raw.c +++ b/applications/main/subghz/scenes/subghz_scene_read_raw.c @@ -223,7 +223,12 @@ bool subghz_scene_read_raw_on_event(void* context, SceneManagerEvent event) { subghz->txrx->rx_key_state = SubGhzRxKeyStateBack; scene_manager_next_scene(subghz->scene_manager, SubGhzSceneShowOnlyRx); } else { - DOLPHIN_DEED(DolphinDeedSubGhzSend); + if(scene_manager_has_previous_scene( + subghz->scene_manager, SubGhzSceneSaved) || + !scene_manager_has_previous_scene( + subghz->scene_manager, SubGhzSceneStart)) { + DOLPHIN_DEED(DolphinDeedSubGhzSend); + } // set callback end tx subghz_protocol_raw_file_encoder_worker_set_callback_end( (SubGhzProtocolEncoderRAW*)subghz_transmitter_get_protocol_instance( diff --git a/applications/main/subghz/scenes/subghz_scene_receiver.c b/applications/main/subghz/scenes/subghz_scene_receiver.c index 77a921457..2b01e2975 100644 --- a/applications/main/subghz/scenes/subghz_scene_receiver.c +++ b/applications/main/subghz/scenes/subghz_scene_receiver.c @@ -1,5 +1,6 @@ #include "../subghz_i.h" #include "../views/receiver.h" +#include static const NotificationSequence subghs_sequence_rx = { &message_green_255, @@ -181,6 +182,7 @@ bool subghz_scene_receiver_on_event(void* context, SceneManagerEvent event) { subghz->txrx->idx_menu_chosen = subghz_view_receiver_get_idx_menu(subghz->subghz_receiver); scene_manager_next_scene(subghz->scene_manager, SubGhzSceneReceiverInfo); + DOLPHIN_DEED(DolphinDeedSubGhzReceiverInfo); consumed = true; break; case SubGhzCustomEventViewReceiverConfig: diff --git a/applications/main/subghz/scenes/subghz_scene_receiver_info.c b/applications/main/subghz/scenes/subghz_scene_receiver_info.c index 03a8ebbcb..bc6c8f112 100644 --- a/applications/main/subghz/scenes/subghz_scene_receiver_info.c +++ b/applications/main/subghz/scenes/subghz_scene_receiver_info.c @@ -1,6 +1,5 @@ #include "../subghz_i.h" #include "../helpers/subghz_custom_event.h" -#include void subghz_scene_receiver_info_callback(GuiButtonType result, InputType type, void* context) { furi_assert(context); @@ -45,7 +44,6 @@ static bool subghz_scene_receiver_info_update_parser(void* context) { void subghz_scene_receiver_info_on_enter(void* context) { SubGhz* subghz = context; - DOLPHIN_DEED(DolphinDeedSubGhzReceiverInfo); if(subghz_scene_receiver_info_update_parser(subghz)) { FuriString* frequency_str; FuriString* modulation_str; diff --git a/applications/main/subghz/scenes/subghz_scene_save_name.c b/applications/main/subghz/scenes/subghz_scene_save_name.c index dfcb65865..33846c283 100644 --- a/applications/main/subghz/scenes/subghz_scene_save_name.c +++ b/applications/main/subghz/scenes/subghz_scene_save_name.c @@ -4,6 +4,7 @@ #include "../helpers/subghz_custom_event.h" #include #include +#include #define MAX_TEXT_INPUT_LEN 22 @@ -131,6 +132,17 @@ bool subghz_scene_save_name_on_event(void* context, SceneManagerEvent event) { } scene_manager_next_scene(subghz->scene_manager, SubGhzSceneSaveSuccess); + if(scene_manager_has_previous_scene(subghz->scene_manager, SubGhzSceneSavedMenu)) { + // Nothing, do not count editing as saving + } else if(scene_manager_has_previous_scene( + subghz->scene_manager, SubGhzSceneMoreRAW)) { + // Ditto, for RAW signals + } else if(scene_manager_has_previous_scene( + subghz->scene_manager, SubGhzSceneSetType)) { + DOLPHIN_DEED(DolphinDeedSubGhzAddManually); + } else { + DOLPHIN_DEED(DolphinDeedSubGhzSave); + } return true; } else { furi_string_set(subghz->error_str, "No name file"); diff --git a/applications/main/subghz/scenes/subghz_scene_save_success.c b/applications/main/subghz/scenes/subghz_scene_save_success.c index 3d5c16ca3..d32c9271a 100644 --- a/applications/main/subghz/scenes/subghz_scene_save_success.c +++ b/applications/main/subghz/scenes/subghz_scene_save_success.c @@ -1,7 +1,5 @@ #include "../subghz_i.h" #include "../helpers/subghz_custom_event.h" -#include -#include void subghz_scene_save_success_popup_callback(void* context) { SubGhz* subghz = context; @@ -10,7 +8,6 @@ void subghz_scene_save_success_popup_callback(void* context) { void subghz_scene_save_success_on_enter(void* context) { SubGhz* subghz = context; - DOLPHIN_DEED(DolphinDeedSubGhzSave); // Setup view Popup* popup = subghz->popup; diff --git a/applications/main/subghz/scenes/subghz_scene_set_type.c b/applications/main/subghz/scenes/subghz_scene_set_type.c index 44fe2fc76..2ed537193 100644 --- a/applications/main/subghz/scenes/subghz_scene_set_type.c +++ b/applications/main/subghz/scenes/subghz_scene_set_type.c @@ -3,7 +3,6 @@ #include #include #include -#include #include #include #include @@ -381,7 +380,6 @@ bool subghz_scene_set_type_on_event(void* context, SceneManagerEvent event) { if(generated_protocol) { subghz_file_name_clear(subghz); - DOLPHIN_DEED(DolphinDeedSubGhzAddManually); scene_manager_next_scene(subghz->scene_manager, SubGhzSceneSaveName); return true; } diff --git a/applications/main/subghz/scenes/subghz_scene_start.c b/applications/main/subghz/scenes/subghz_scene_start.c index f37ccae9e..0b1c3c159 100644 --- a/applications/main/subghz/scenes/subghz_scene_start.c +++ b/applications/main/subghz/scenes/subghz_scene_start.c @@ -1,4 +1,5 @@ #include "../subghz_i.h" +#include enum SubmenuIndex { SubmenuIndexRead = 10, @@ -84,6 +85,7 @@ bool subghz_scene_start_on_event(void* context, SceneManagerEvent event) { scene_manager_set_scene_state( subghz->scene_manager, SubGhzSceneStart, SubmenuIndexFrequencyAnalyzer); scene_manager_next_scene(subghz->scene_manager, SubGhzSceneFrequencyAnalyzer); + DOLPHIN_DEED(DolphinDeedSubGhzFrequencyAnalyzer); return true; } else if(event.event == SubmenuIndexTest) { scene_manager_set_scene_state( diff --git a/applications/main/subghz/scenes/subghz_scene_transmitter.c b/applications/main/subghz/scenes/subghz_scene_transmitter.c index 5da6f4300..fbe954f0c 100644 --- a/applications/main/subghz/scenes/subghz_scene_transmitter.c +++ b/applications/main/subghz/scenes/subghz_scene_transmitter.c @@ -50,7 +50,6 @@ bool subghz_scene_transmitter_update_data_show(void* context) { void subghz_scene_transmitter_on_enter(void* context) { SubGhz* subghz = context; - DOLPHIN_DEED(DolphinDeedSubGhzSend); if(!subghz_scene_transmitter_update_data_show(subghz)) { view_dispatcher_send_custom_event( subghz->view_dispatcher, SubGhzCustomEventViewTransmitterError); @@ -78,6 +77,7 @@ bool subghz_scene_transmitter_on_event(void* context, SceneManagerEvent event) { } else { subghz->state_notifications = SubGhzNotificationStateTx; subghz_scene_transmitter_update_data_show(subghz); + DOLPHIN_DEED(DolphinDeedSubGhzSend); } } return true; diff --git a/applications/services/dolphin/helpers/dolphin_deed.c b/applications/services/dolphin/helpers/dolphin_deed.c index ce3e058b5..51db56fdf 100644 --- a/applications/services/dolphin/helpers/dolphin_deed.c +++ b/applications/services/dolphin/helpers/dolphin_deed.c @@ -21,8 +21,8 @@ static const DolphinDeedWeight dolphin_deed_weights[] = { {1, DolphinAppNfc}, // DolphinDeedNfcDetectReader {2, DolphinAppNfc}, // DolphinDeedNfcEmulate {2, DolphinAppNfc}, // DolphinDeedNfcMfcAdd - {1, DolphinAppNfc}, // DolphinDeedNfcMfulError {1, DolphinAppNfc}, // DolphinDeedNfcAddSave + {1, DolphinAppNfc}, // DolphinDeedNfcAddEmulate {1, DolphinAppIr}, // DolphinDeedIrSend {3, DolphinAppIr}, // DolphinDeedIrLearnSuccess diff --git a/applications/services/dolphin/helpers/dolphin_deed.h b/applications/services/dolphin/helpers/dolphin_deed.h index abe027d79..c9cd18f31 100644 --- a/applications/services/dolphin/helpers/dolphin_deed.h +++ b/applications/services/dolphin/helpers/dolphin_deed.h @@ -37,8 +37,8 @@ typedef enum { DolphinDeedNfcDetectReader, DolphinDeedNfcEmulate, DolphinDeedNfcMfcAdd, - DolphinDeedNfcMfulError, DolphinDeedNfcAddSave, + DolphinDeedNfcAddEmulate, DolphinDeedIrSend, DolphinDeedIrLearnSuccess, From 26f852839a93f82a3d863fc6a6e64c9ebd7e6f0a Mon Sep 17 00:00:00 2001 From: Skorpionm <85568270+Skorpionm@users.noreply.github.com> Date: Fri, 28 Oct 2022 15:16:54 +0400 Subject: [PATCH 02/49] WS: fix Acurite-606TX protocol (#1938) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * WS: fix acurite_606tx protocol * WS: update version * WeatherStation: remove break from invalid place Co-authored-by: あく --- .../helpers/weather_station_types.h | 2 +- .../weather_station/protocols/acurite_606tx.c | 64 +++++++++---------- 2 files changed, 31 insertions(+), 35 deletions(-) diff --git a/applications/plugins/weather_station/helpers/weather_station_types.h b/applications/plugins/weather_station/helpers/weather_station_types.h index 479638b98..2976cbce8 100644 --- a/applications/plugins/weather_station/helpers/weather_station_types.h +++ b/applications/plugins/weather_station/helpers/weather_station_types.h @@ -3,7 +3,7 @@ #include #include -#define WS_VERSION_APP "0.3" +#define WS_VERSION_APP "0.3.1" #define WS_DEVELOPED "SkorP" #define WS_GITHUB "https://github.com/flipperdevices/flipperzero-firmware" diff --git a/applications/plugins/weather_station/protocols/acurite_606tx.c b/applications/plugins/weather_station/protocols/acurite_606tx.c index a92ec243c..3c9144057 100644 --- a/applications/plugins/weather_station/protocols/acurite_606tx.c +++ b/applications/plugins/weather_station/protocols/acurite_606tx.c @@ -151,41 +151,37 @@ void ws_protocol_decoder_acurite_606tx_feed(void* context, bool level, uint32_t case Acurite_606TXDecoderStepCheckDuration: if(!level) { - if((DURATION_DIFF(instance->decoder.te_last, ws_protocol_acurite_606tx_const.te_short) < - ws_protocol_acurite_606tx_const.te_delta) && - (DURATION_DIFF(duration, ws_protocol_acurite_606tx_const.te_short) < - ws_protocol_acurite_606tx_const.te_delta)) { - //Found syncPostfix - instance->decoder.parser_step = Acurite_606TXDecoderStepReset; - if((instance->decoder.decode_count_bit == - ws_protocol_acurite_606tx_const.min_count_bit_for_found) && - ws_protocol_acurite_606tx_check(instance)) { - instance->generic.data = instance->decoder.decode_data; - instance->generic.data_count_bit = instance->decoder.decode_count_bit; - ws_protocol_acurite_606tx_remote_controller(&instance->generic); - if(instance->base.callback) - instance->base.callback(&instance->base, instance->base.context); + if(DURATION_DIFF(instance->decoder.te_last, ws_protocol_acurite_606tx_const.te_short) < + ws_protocol_acurite_606tx_const.te_delta) { + if((DURATION_DIFF(duration, ws_protocol_acurite_606tx_const.te_short) < + ws_protocol_acurite_606tx_const.te_delta) || + (duration > ws_protocol_acurite_606tx_const.te_long * 3)) { + //Found syncPostfix + instance->decoder.parser_step = Acurite_606TXDecoderStepReset; + if((instance->decoder.decode_count_bit == + ws_protocol_acurite_606tx_const.min_count_bit_for_found) && + ws_protocol_acurite_606tx_check(instance)) { + instance->generic.data = instance->decoder.decode_data; + instance->generic.data_count_bit = instance->decoder.decode_count_bit; + ws_protocol_acurite_606tx_remote_controller(&instance->generic); + if(instance->base.callback) + instance->base.callback(&instance->base, instance->base.context); + } + instance->decoder.decode_data = 0; + instance->decoder.decode_count_bit = 0; + } else if( + DURATION_DIFF(duration, ws_protocol_acurite_606tx_const.te_long) < + ws_protocol_acurite_606tx_const.te_delta * 2) { + subghz_protocol_blocks_add_bit(&instance->decoder, 0); + instance->decoder.parser_step = Acurite_606TXDecoderStepSaveDuration; + } else if( + DURATION_DIFF(duration, ws_protocol_acurite_606tx_const.te_long * 2) < + ws_protocol_acurite_606tx_const.te_delta * 4) { + subghz_protocol_blocks_add_bit(&instance->decoder, 1); + instance->decoder.parser_step = Acurite_606TXDecoderStepSaveDuration; + } else { + instance->decoder.parser_step = Acurite_606TXDecoderStepReset; } - instance->decoder.decode_data = 0; - instance->decoder.decode_count_bit = 0; - - break; - } else if( - (DURATION_DIFF( - instance->decoder.te_last, ws_protocol_acurite_606tx_const.te_short) < - ws_protocol_acurite_606tx_const.te_delta) && - (DURATION_DIFF(duration, ws_protocol_acurite_606tx_const.te_long) < - ws_protocol_acurite_606tx_const.te_delta * 2)) { - subghz_protocol_blocks_add_bit(&instance->decoder, 0); - instance->decoder.parser_step = Acurite_606TXDecoderStepSaveDuration; - } else if( - (DURATION_DIFF( - instance->decoder.te_last, ws_protocol_acurite_606tx_const.te_short) < - ws_protocol_acurite_606tx_const.te_delta) && - (DURATION_DIFF(duration, ws_protocol_acurite_606tx_const.te_long * 2) < - ws_protocol_acurite_606tx_const.te_delta * 4)) { - subghz_protocol_blocks_add_bit(&instance->decoder, 1); - instance->decoder.parser_step = Acurite_606TXDecoderStepSaveDuration; } else { instance->decoder.parser_step = Acurite_606TXDecoderStepReset; } From be3ee9f2fe7a06fc2cd8e959c3dcb3e7fde374a7 Mon Sep 17 00:00:00 2001 From: Oleg Moiseenko <807634+merlokk@users.noreply.github.com> Date: Fri, 28 Oct 2022 16:42:59 +0300 Subject: [PATCH 03/49] Oregon2 additional sensors defines (#1933) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * added a list of sensors and added several additional temperature and temperature/humidity sensor id's * now here are only sensors that have test data Co-authored-by: あく --- .../weather_station/protocols/oregon2.c | 52 ++++++++++++++++--- 1 file changed, 45 insertions(+), 7 deletions(-) diff --git a/applications/plugins/weather_station/protocols/oregon2.c b/applications/plugins/weather_station/protocols/oregon2.c index 76bc3f0a1..d294548e6 100644 --- a/applications/plugins/weather_station/protocols/oregon2.c +++ b/applications/plugins/weather_station/protocols/oregon2.c @@ -29,6 +29,40 @@ static const SubGhzBlockConst ws_oregon2_const = { // bit indicating the low battery #define OREGON2_FLAG_BAT_LOW 0x4 +/// Documentation for Oregon Scientific protocols can be found here: +/// http://wmrx00.sourceforge.net/Arduino/OregonScientific-RF-Protocols.pdf +// Sensors ID +#define ID_THGR122N 0x1d20 +#define ID_THGR968 0x1d30 +#define ID_BTHR918 0x5d50 +#define ID_BHTR968 0x5d60 +#define ID_RGR968 0x2d10 +#define ID_THR228N 0xec40 +#define ID_THN132N 0xec40 // same as THR228N but different packet size +#define ID_RTGN318 0x0cc3 // warning: id is from 0x0cc3 and 0xfcc3 +#define ID_RTGN129 0x0cc3 // same as RTGN318 but different packet size +#define ID_THGR810 0xf824 // This might be ID_THGR81, but what's true is lost in (git) history +#define ID_THGR810a 0xf8b4 // unconfirmed version +#define ID_THN802 0xc844 +#define ID_PCR800 0x2914 +#define ID_PCR800a 0x2d14 // Different PCR800 ID - AU version I think +#define ID_WGR800 0x1984 +#define ID_WGR800a 0x1994 // unconfirmed version +#define ID_WGR968 0x3d00 +#define ID_UV800 0xd874 +#define ID_THN129 0xcc43 // THN129 Temp only +#define ID_RTHN129 0x0cd3 // RTHN129 Temp, clock sensors +#define ID_BTHGN129 0x5d53 // Baro, Temp, Hygro sensor +#define ID_UVR128 0xec70 +#define ID_THGR328N 0xcc23 // Temp & Hygro sensor similar to THR228N with 5 channel instead of 3 +#define ID_RTGR328N_1 0xdcc3 // RTGR328N_[1-5] RFclock(date &time)&Temp&Hygro sensor +#define ID_RTGR328N_2 0xccc3 +#define ID_RTGR328N_3 0xbcc3 +#define ID_RTGR328N_4 0xacc3 +#define ID_RTGR328N_5 0x9cc3 +#define ID_RTGR328N_6 0x8ce3 // RTGR328N_6&7 RFclock(date &time)&Temp&Hygro sensor like THGR328N +#define ID_RTGR328N_7 0x8ae3 + struct WSProtocolDecoderOregon2 { SubGhzProtocolDecoderBase base; @@ -101,9 +135,12 @@ static ManchesterEvent level_and_duration_to_event(bool level, uint32_t duration } // From sensor id code return amount of bits in variable section +// https://temofeev.ru/info/articles/o-dekodirovanii-protokola-pogodnykh-datchikov-oregon-scientific static uint8_t oregon2_sensor_id_var_bits(uint16_t sensor_id) { - if(sensor_id == 0xEC40) return 16; - if(sensor_id == 0x1D20) return 24; + if(sensor_id == ID_THR228N) return 16; + + if(sensor_id == ID_THGR122N) return 24; + return 0; } @@ -134,15 +171,16 @@ static float ws_oregon2_decode_temp(uint32_t data) { } static void ws_oregon2_decode_var_data(WSBlockGeneric* ws_b, uint16_t sensor_id, uint32_t data) { - switch(sensor_id) { - case 0xEC40: + if(sensor_id == ID_THR228N) { ws_b->temp = ws_oregon2_decode_temp(data); ws_b->humidity = WS_NO_HUMIDITY; - break; - case 0x1D20: + return; + } + + if(sensor_id == ID_THGR122N) { ws_b->humidity = bcd_decode_short(data); ws_b->temp = ws_oregon2_decode_temp(data >> 8); - break; + return; } } From 492f147568a6f04896dfea6fba06a9494dfaa65d Mon Sep 17 00:00:00 2001 From: Konstantin Volkov <72250702+doomwastaken@users.noreply.github.com> Date: Fri, 28 Oct 2022 16:59:09 +0300 Subject: [PATCH 04/49] [FL-2887] actions unit tests runner (#1920) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Konstantin Volkov Co-authored-by: あく --- .github/workflows/unit_tests.yml | 56 ++++++++++++++++++++++ scripts/flipper/storage.py | 13 ++++++ scripts/storage.py | 16 +++++++ scripts/testing/await_flipper.py | 48 +++++++++++++++++++ scripts/testing/units.py | 79 ++++++++++++++++++++++++++++++++ 5 files changed, 212 insertions(+) create mode 100644 .github/workflows/unit_tests.yml create mode 100644 scripts/testing/await_flipper.py create mode 100644 scripts/testing/units.py diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml new file mode 100644 index 000000000..8c5bac2a2 --- /dev/null +++ b/.github/workflows/unit_tests.yml @@ -0,0 +1,56 @@ +name: 'Unit tests' + +on: + pull_request: + +env: + TARGETS: f7 + DEFAULT_TARGET: f7 + +jobs: + main: + runs-on: [self-hosted, FlipperZeroTest] + steps: + - name: Checkout code + uses: actions/checkout@v2 + with: + fetch-depth: 0 + ref: ${{ github.event.pull_request.head.sha }} + + - name: 'Get flipper from device manager (mock)' + id: device + run: | + echo "flipper=/dev/ttyACM0" >> $GITHUB_OUTPUT + + - name: 'Compile unit tests firmware' + id: compile + continue-on-error: true + run: | + FBT_TOOLCHAIN_PATH=/opt ./fbt flash OPENOCD_ADAPTER_SERIAL=2A0906016415303030303032 FIRMWARE_APP_SET=unit_tests FORCE=1 + + - name: 'Wait for flipper to finish updating' + id: connect + if: steps.compile.outcome == 'success' + continue-on-error: true + run: | + python3 ./scripts/testing/await_flipper.py ${{steps.device.outputs.flipper}} + + - name: 'Format flipper SD card' + id: format + if: steps.connect.outcome == 'success' + continue-on-error: true + run: | + ./scripts/storage.py -p ${{steps.device.outputs.flipper}} format_ext + + - name: 'Copy unit tests to flipper' + id: copy + if: steps.format.outcome == 'success' + continue-on-error: true + run: | + ./scripts/storage.py -p ${{steps.device.outputs.flipper}} send assets/unit_tests /ext/unit_tests + + - name: 'Run units and validate results' + if: steps.copy.outcome == 'success' + continue-on-error: true + run: | + python3 ./scripts/testing/units.py ${{steps.device.outputs.flipper}} diff --git a/scripts/flipper/storage.py b/scripts/flipper/storage.py index 5fa8a2c81..6b53f7477 100644 --- a/scripts/flipper/storage.py +++ b/scripts/flipper/storage.py @@ -340,6 +340,19 @@ class FlipperStorage: else: return True + def format_ext(self): + """Create a directory on Flipper""" + self.send_and_wait_eol("storage format /ext\r") + self.send_and_wait_eol("y\r") + answer = self.read.until(self.CLI_EOL) + self.read.until(self.CLI_PROMPT) + + if self.has_error(answer): + self.last_error = self.get_error(answer) + return False + else: + return True + def remove(self, path): """Remove file or directory on Flipper""" self.send_and_wait_eol('storage remove "' + path + '"\r') diff --git a/scripts/storage.py b/scripts/storage.py index 167ba88eb..ee5dabd43 100755 --- a/scripts/storage.py +++ b/scripts/storage.py @@ -21,6 +21,11 @@ class Main(App): self.parser_mkdir.add_argument("flipper_path", help="Flipper path") self.parser_mkdir.set_defaults(func=self.mkdir) + self.parser_format = self.subparsers.add_parser( + "format_ext", help="Format flash card" + ) + self.parser_format.set_defaults(func=self.format_ext) + self.parser_remove = self.subparsers.add_parser( "remove", help="Remove file/directory" ) @@ -275,6 +280,17 @@ class Main(App): storage.stop() return 0 + def format_ext(self): + if not (storage := self._get_storage()): + return 1 + + self.logger.debug("Formatting /ext SD card") + + if not storage.format_ext(): + self.logger.error(f"Error: {storage.last_error}") + storage.stop() + return 0 + def stress(self): self.logger.error("This test is wearing out flash memory.") self.logger.error("Never use it with internal storage(/int)") diff --git a/scripts/testing/await_flipper.py b/scripts/testing/await_flipper.py new file mode 100644 index 000000000..1f0d16194 --- /dev/null +++ b/scripts/testing/await_flipper.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python3 + +import sys, os, time + + +def flp_serial_by_name(flp_name): + if sys.platform == "darwin": # MacOS + flp_serial = "/dev/cu.usbmodemflip_" + flp_name + "1" + elif sys.platform == "linux": # Linux + flp_serial = ( + "/dev/serial/by-id/usb-Flipper_Devices_Inc._Flipper_" + + flp_name + + "_flip_" + + flp_name + + "-if00" + ) + + if os.path.exists(flp_serial): + return flp_serial + else: + if os.path.exists(flp_name): + return flp_name + else: + return "" + + +UPDATE_TIMEOUT = 30 + + +def main(): + flipper_name = sys.argv[1] + elapsed = 0 + flipper = flp_serial_by_name(flipper_name) + + while flipper == "" and elapsed < UPDATE_TIMEOUT: + elapsed += 1 + time.sleep(1) + flipper = flp_serial_by_name(flipper_name) + + if flipper == "": + print(f"Cannot find {flipper_name} flipper. Guess your flipper swam away") + sys.exit(1) + + sys.exit(0) + + +if __name__ == "__main__": + main() diff --git a/scripts/testing/units.py b/scripts/testing/units.py new file mode 100644 index 000000000..83b07899a --- /dev/null +++ b/scripts/testing/units.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python3 + +import sys, os +import serial +import re + +from await_flipper import flp_serial_by_name + + +LEAK_THRESHOLD = 3000 # added until units are fixed + + +def main(): + flp_serial = flp_serial_by_name(sys.argv[1]) + + if flp_serial == "": + print("Name or serial port is invalid") + sys.exit(1) + + with serial.Serial(flp_serial, timeout=1) as flipper: + flipper.baudrate = 230400 + flipper.flushOutput() + flipper.flushInput() + + flipper.timeout = 300 + + flipper.read_until(b">: ").decode("utf-8") + flipper.write(b"unit_tests\r") + data = flipper.read_until(b">: ").decode("utf-8") + + lines = data.split("\r\n") + + tests_re = r"Failed tests: \d{0,}" + time_re = r"Consumed: \d{0,}" + leak_re = r"Leaked: \d{0,}" + status_re = r"Status: \w{3,}" + + tests_pattern = re.compile(tests_re) + time_pattern = re.compile(time_re) + leak_pattern = re.compile(leak_re) + status_pattern = re.compile(status_re) + + tests, time, leak, status = None, None, None, None + + for line in lines: + print(line) + if not tests: + tests = re.match(tests_pattern, line) + if not time: + time = re.match(time_pattern, line) + if not leak: + leak = re.match(leak_pattern, line) + if not status: + status = re.match(status_pattern, line) + + if leak is None or time is None or leak is None or status is None: + print("Failed to get data. Or output is corrupt") + sys.exit(1) + + leak = int(re.findall(r"[- ]\d+", leak.group(0))[0]) + status = re.findall(r"\w+", status.group(0))[1] + tests = int(re.findall(r"\d+", tests.group(0))[0]) + time = int(re.findall(r"\d+", time.group(0))[0]) + + if tests > 0 or leak > LEAK_THRESHOLD or status != "PASSED": + print(f"Got {tests} failed tests.") + print(f"Leaked {leak} bytes.") + print(f"Status by flipper: {status}") + print(f"Time elapsed {time/1000} seconds.") + sys.exit(1) + + print( + f"Tests ran successfully! Time elapsed {time/1000} seconds. Passed {tests} tests." + ) + sys.exit(0) + + +if __name__ == "__main__": + main() From 3434305630b4e76f20d703e119c9f3ad1df9cf4b Mon Sep 17 00:00:00 2001 From: Sergey Gavrilov Date: Sat, 29 Oct 2022 00:08:50 +1000 Subject: [PATCH 05/49] [FL-2937] Remove resources from API to prevent frequent API version increase (#1935) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Remove all icons from API * Music player: icons * Signal generator: icons * Bt hid: icons * Weather station: icons * Picopass: icons * File browser test: icons * Example images: documentation * Remove global assets header * Fix picopass Co-authored-by: あく --- .../debug/file_browser_test/application.fam | 1 + .../file_browser_test/file_browser_app.c | 2 +- .../file_browser_test/icons/badusb_10px.png | Bin 0 -> 576 bytes .../examples/example_images/ReadMe.md | 24 +++ applications/main/bad_usb/bad_usb_app_i.h | 1 + .../main/bad_usb/views/bad_usb_view.c | 1 + applications/main/fap_loader/fap_loader_app.c | 1 + applications/main/gpio/gpio_app_i.h | 1 + applications/main/ibutton/ibutton_i.h | 1 + applications/main/infrared/infrared_i.h | 1 + applications/main/lfrfid/lfrfid_i.h | 1 + .../main/lfrfid/views/lfrfid_view_read.c | 1 + applications/main/nfc/nfc_i.h | 1 + applications/main/nfc/views/detect_reader.c | 2 +- applications/main/subghz/subghz_i.h | 1 + applications/main/u2f/u2f_app_i.h | 1 + applications/main/u2f/views/u2f_view.c | 1 + .../bt_hid_app/assets/ButtonDown_7x4.png | Bin 0 -> 102 bytes .../bt_hid_app/assets/ButtonLeft_4x7.png | Bin 0 -> 1415 bytes .../bt_hid_app/assets/ButtonRight_4x7.png | Bin 0 -> 1839 bytes .../bt_hid_app/assets/ButtonUp_7x4.png | Bin 0 -> 102 bytes .../plugins/bt_hid_app/assets/Ok_btn_9x9.png | Bin 0 -> 3605 bytes .../bt_hid_app/assets/Pin_arrow_down_7x9.png | Bin 0 -> 3607 bytes .../bt_hid_app/assets/Pin_arrow_left_9x7.png | Bin 0 -> 3603 bytes .../bt_hid_app/assets/Pin_arrow_right_9x7.png | Bin 0 -> 3602 bytes .../bt_hid_app/assets/Pin_arrow_up_7x9.png | Bin 0 -> 3603 bytes .../bt_hid_app/assets/Pin_back_arrow_10x8.png | Bin 0 -> 3606 bytes .../plugins/music_player/application.fam | 3 +- .../plugins/music_player/icons/music_10px.png | Bin 0 -> 142 bytes .../plugins/music_player/music_player.c | 2 +- applications/plugins/picopass/application.fam | 1 + .../picopass/icons/DolphinMafia_115x62.png | Bin 0 -> 2504 bytes .../picopass/icons/DolphinNice_96x59.png | Bin 0 -> 2459 bytes .../plugins/picopass/icons/Nfc_10px.png | Bin 0 -> 304 bytes .../icons/RFIDDolphinReceive_97x61.png | Bin 0 -> 1421 bytes .../picopass/icons/RFIDDolphinSend_97x61.png | Bin 0 -> 1418 bytes .../plugins/picopass/picopass_device.c | 1 + applications/plugins/picopass/picopass_i.h | 1 + .../plugins/signal_generator/application.fam | 1 + .../icons/SmallArrowDown_3x5.png | Bin 0 -> 3592 bytes .../icons/SmallArrowUp_3x5.png | Bin 0 -> 7976 bytes .../signal_generator/views/signal_gen_pwm.c | 1 + .../weather_station/images/Lock_7x8.png | Bin 0 -> 3597 bytes .../images/Pin_back_arrow_10x8.png | Bin 0 -> 3606 bytes .../weather_station/images/Quest_7x8.png | Bin 0 -> 3675 bytes .../images/Scanning_123x52.png | Bin 0 -> 1690 bytes .../weather_station/images/Unlock_7x8.png | Bin 0 -> 3598 bytes .../images/WarningDolphin_45x42.png | Bin 0 -> 1139 bytes .../views/weather_station_receiver.c | 3 +- applications/services/bt/bt_service/bt.c | 1 + .../desktop/views/desktop_view_lock_menu.c | 1 + .../desktop/views/desktop_view_locked.c | 1 + .../desktop/views/desktop_view_pin_input.c | 1 + applications/services/dialogs/dialogs_api.c | 1 + applications/services/gui/canvas.h | 2 +- applications/services/gui/gui.c | 1 + applications/services/gui/icon_animation.h | 2 +- .../services/gui/modules/button_menu.c | 1 + .../services/gui/modules/byte_input.c | 5 +- applications/services/gui/modules/menu.c | 1 + .../services/gui/modules/text_input.c | 1 + .../services/power/power_service/power_i.h | 1 + .../power/power_service/views/power_off.c | 1 + .../power_service/views/power_unplug_usb.c | 1 + applications/services/storage/storage.c | 1 + applications/settings/about/about.c | 1 + .../bt_settings_app/bt_settings_app.h | 1 + .../desktop_settings/desktop_settings_app.h | 1 + .../power_settings_app/power_settings_app.h | 1 + .../power_settings_app/views/battery_info.c | 1 + .../storage_settings/storage_settings.h | 1 + .../system/updater/views/updater_main.c | 1 + firmware/targets/f7/api_symbols.csv | 186 +----------------- 73 files changed, 74 insertions(+), 195 deletions(-) create mode 100644 applications/debug/file_browser_test/icons/badusb_10px.png create mode 100644 applications/examples/example_images/ReadMe.md create mode 100644 applications/plugins/bt_hid_app/assets/ButtonDown_7x4.png create mode 100644 applications/plugins/bt_hid_app/assets/ButtonLeft_4x7.png create mode 100644 applications/plugins/bt_hid_app/assets/ButtonRight_4x7.png create mode 100644 applications/plugins/bt_hid_app/assets/ButtonUp_7x4.png create mode 100644 applications/plugins/bt_hid_app/assets/Ok_btn_9x9.png create mode 100644 applications/plugins/bt_hid_app/assets/Pin_arrow_down_7x9.png create mode 100644 applications/plugins/bt_hid_app/assets/Pin_arrow_left_9x7.png create mode 100644 applications/plugins/bt_hid_app/assets/Pin_arrow_right_9x7.png create mode 100644 applications/plugins/bt_hid_app/assets/Pin_arrow_up_7x9.png create mode 100644 applications/plugins/bt_hid_app/assets/Pin_back_arrow_10x8.png create mode 100644 applications/plugins/music_player/icons/music_10px.png create mode 100644 applications/plugins/picopass/icons/DolphinMafia_115x62.png create mode 100644 applications/plugins/picopass/icons/DolphinNice_96x59.png create mode 100644 applications/plugins/picopass/icons/Nfc_10px.png create mode 100644 applications/plugins/picopass/icons/RFIDDolphinReceive_97x61.png create mode 100644 applications/plugins/picopass/icons/RFIDDolphinSend_97x61.png create mode 100644 applications/plugins/signal_generator/icons/SmallArrowDown_3x5.png create mode 100644 applications/plugins/signal_generator/icons/SmallArrowUp_3x5.png create mode 100644 applications/plugins/weather_station/images/Lock_7x8.png create mode 100644 applications/plugins/weather_station/images/Pin_back_arrow_10x8.png create mode 100644 applications/plugins/weather_station/images/Quest_7x8.png create mode 100644 applications/plugins/weather_station/images/Scanning_123x52.png create mode 100644 applications/plugins/weather_station/images/Unlock_7x8.png create mode 100644 applications/plugins/weather_station/images/WarningDolphin_45x42.png diff --git a/applications/debug/file_browser_test/application.fam b/applications/debug/file_browser_test/application.fam index 5e4c7f467..4a401a649 100644 --- a/applications/debug/file_browser_test/application.fam +++ b/applications/debug/file_browser_test/application.fam @@ -8,4 +8,5 @@ App( stack_size=2 * 1024, order=150, fap_category="Debug", + fap_icon_assets="icons", ) diff --git a/applications/debug/file_browser_test/file_browser_app.c b/applications/debug/file_browser_test/file_browser_app.c index 5c7b93bcb..6cb50d385 100644 --- a/applications/debug/file_browser_test/file_browser_app.c +++ b/applications/debug/file_browser_test/file_browser_app.c @@ -1,4 +1,4 @@ -#include "assets_icons.h" +#include #include "file_browser_app_i.h" #include "gui/modules/file_browser.h" #include diff --git a/applications/debug/file_browser_test/icons/badusb_10px.png b/applications/debug/file_browser_test/icons/badusb_10px.png new file mode 100644 index 0000000000000000000000000000000000000000..037474aa3bc9c2e1aca79a68483e69980432bcf5 GIT binary patch literal 576 zcmV-G0>AxEX>4Tx04R}tkv&MmKpe$i(`rSk4t5Z6$WWau6cusQDionYs1;guFuC*#nlvOW zE{=k0!NHHks)LKOt`4q(Aou~|=;Wm6A|?JWDYS_3;J6>}?mh0_0Yan9G%FATG`(u3 z5^*t;T@{0`5D-8=V(6BcWz0!Z5}xDh9zMR_MR}I@xj#prnzI<-6NzV;VOEJZh^IHJ z2Iqa^Fe}O`@j3ChNf#u3C`-Nm{=@yu+qV-Xlle$#1U1~DPPFA zta9Gstd(o5bx;1nP)=W2<~q$0B(R7jND!f*h7!uCB1)@HiiH&I$36VRj$a~|Laq`R zITlcX2HEk0|H1EWt^DMKn-q!zT`#u%F$x5Cfo9#dzmILZc>?&Kfh)c3uQY&}Ptxmc zEph}5Yy%h9ZB5w&E_Z;TCqp)6NAlAY@_FF>jJ_!g4Bi60Yi@6?eVjf3Y3eF@0~{Oz zV+G1y_jq?tXK(+WY4!I5C=YUpXXIhH00006VoOIv0RI600RN!9r;`8x010qNS#tmY z3ljhU3ljkVnw%H_000McNliru<^lu`HWXkp{t5s906j@WK~xyijZi@f05Awj>HlAL zr$MwDdI>{Qf+U53tOUR#xOeyy)jcQo#JNRv)7r6DVVK|+*(cmT+R+EbO(O#X#REG4 O0000 +#include #include #include #include diff --git a/applications/main/bad_usb/views/bad_usb_view.c b/applications/main/bad_usb/views/bad_usb_view.c index 6c6a15847..e5c5d92a3 100644 --- a/applications/main/bad_usb/views/bad_usb_view.c +++ b/applications/main/bad_usb/views/bad_usb_view.c @@ -1,6 +1,7 @@ #include "bad_usb_view.h" #include "../bad_usb_script.h" #include +#include #define MAX_NAME_LEN 64 diff --git a/applications/main/fap_loader/fap_loader_app.c b/applications/main/fap_loader/fap_loader_app.c index faf8eefc8..9b4c92335 100644 --- a/applications/main/fap_loader/fap_loader_app.c +++ b/applications/main/fap_loader/fap_loader_app.c @@ -1,5 +1,6 @@ #include #include +#include #include #include #include diff --git a/applications/main/gpio/gpio_app_i.h b/applications/main/gpio/gpio_app_i.h index fff35c95a..85c5c332e 100644 --- a/applications/main/gpio/gpio_app_i.h +++ b/applications/main/gpio/gpio_app_i.h @@ -15,6 +15,7 @@ #include #include "views/gpio_test.h" #include "views/gpio_usb_uart.h" +#include struct GpioApp { Gui* gui; diff --git a/applications/main/ibutton/ibutton_i.h b/applications/main/ibutton/ibutton_i.h index a9515195e..0a8099351 100644 --- a/applications/main/ibutton/ibutton_i.h +++ b/applications/main/ibutton/ibutton_i.h @@ -4,6 +4,7 @@ #include #include +#include #include #include #include diff --git a/applications/main/infrared/infrared_i.h b/applications/main/infrared/infrared_i.h index 6d25d1609..5b555e4bb 100644 --- a/applications/main/infrared/infrared_i.h +++ b/applications/main/infrared/infrared_i.h @@ -2,6 +2,7 @@ #include #include +#include #include #include #include diff --git a/applications/main/lfrfid/lfrfid_i.h b/applications/main/lfrfid/lfrfid_i.h index 71917c0c2..72b061930 100644 --- a/applications/main/lfrfid/lfrfid_i.h +++ b/applications/main/lfrfid/lfrfid_i.h @@ -5,6 +5,7 @@ #include #include +#include #include #include #include diff --git a/applications/main/lfrfid/views/lfrfid_view_read.c b/applications/main/lfrfid/views/lfrfid_view_read.c index 0d4db6178..094afb617 100644 --- a/applications/main/lfrfid/views/lfrfid_view_read.c +++ b/applications/main/lfrfid/views/lfrfid_view_read.c @@ -1,5 +1,6 @@ #include "lfrfid_view_read.h" #include +#include #define TEMP_STR_LEN 128 diff --git a/applications/main/nfc/nfc_i.h b/applications/main/nfc/nfc_i.h index fa5b54edc..e9b36a3e9 100644 --- a/applications/main/nfc/nfc_i.h +++ b/applications/main/nfc/nfc_i.h @@ -7,6 +7,7 @@ #include #include +#include #include #include #include diff --git a/applications/main/nfc/views/detect_reader.c b/applications/main/nfc/views/detect_reader.c index 2dbb4338b..91537868b 100644 --- a/applications/main/nfc/views/detect_reader.c +++ b/applications/main/nfc/views/detect_reader.c @@ -1,5 +1,5 @@ #include "detect_reader.h" - +#include #include struct DetectReader { diff --git a/applications/main/subghz/subghz_i.h b/applications/main/subghz/subghz_i.h index 09830ba05..46768cf02 100644 --- a/applications/main/subghz/subghz_i.h +++ b/applications/main/subghz/subghz_i.h @@ -13,6 +13,7 @@ #include "views/subghz_test_packet.h" #include +#include #include #include #include diff --git a/applications/main/u2f/u2f_app_i.h b/applications/main/u2f/u2f_app_i.h index 53647859a..2896684c3 100644 --- a/applications/main/u2f/u2f_app_i.h +++ b/applications/main/u2f/u2f_app_i.h @@ -4,6 +4,7 @@ #include "scenes/u2f_scene.h" #include +#include #include #include #include diff --git a/applications/main/u2f/views/u2f_view.c b/applications/main/u2f/views/u2f_view.c index fa3d6cc24..181c495d0 100644 --- a/applications/main/u2f/views/u2f_view.c +++ b/applications/main/u2f/views/u2f_view.c @@ -1,5 +1,6 @@ #include "u2f_view.h" #include +#include struct U2fView { View* view; diff --git a/applications/plugins/bt_hid_app/assets/ButtonDown_7x4.png b/applications/plugins/bt_hid_app/assets/ButtonDown_7x4.png new file mode 100644 index 0000000000000000000000000000000000000000..2954bb6a67d1c23c0bb5d765e8d2aa04b9b5adec GIT binary patch literal 102 zcmeAS@N?(olHy`uVBq!ia0vp^>_E)I!3HFqj;YoHDIHH2#}J9|(o>FH3<^BV2haYO z-y5_sM4;GPjq%Ck6>60csmUj6EiNa>ORduPH4*)h!w|e3sE@(Z)z4*}Q$iC10Gods AV*mgE literal 0 HcmV?d00001 diff --git a/applications/plugins/bt_hid_app/assets/ButtonLeft_4x7.png b/applications/plugins/bt_hid_app/assets/ButtonLeft_4x7.png new file mode 100644 index 0000000000000000000000000000000000000000..0b4655d43247083aa705620e9836ac415b42ca46 GIT binary patch literal 1415 zcmbVM+iKK67*5rq)>aU2M7$VM1Vxif;vTv~W2u`S7ED{V3s&&L*<`XiG|9wd+THd> z5CnY!sdyuJtrvQyAo>KpiLcV|{Tkc)riAbluXfwSZCApL`ztB&p zx6LGKvks4K_4~)qD&oGa-YdJlW)hAKMNJd7<=t?6c^RI1>c$ifyjaM>^|&8!ey zB4!nh9u>5uen6Ve@<H5rru6h<2Ef#GQdQ*CmZOlQi~N!?9H`Rp;C% zU}CB21#?;r`&0|6C0}b-=jODa5|nEJ#ntxQ&{~jpgtwDta4hftr~G=#p@V36e4Zjh zq%J~{y26Jjn=1Nw-l*3%QW5YFE*v4z3gt0$&(*xf2en34c?JpH8+FYldo+Alvg8af-pG4(=!fyUi-Wsg z`g#n9VUcf(DFr{poMSNzw-lz>w+HV+n1ELr&SLA#LHUb0p(xWQ(1*vJ-i+1!`swxZ Z!O7;c$;lT_->m1Ovaz)0yuI`A$q$F8u*d)a literal 0 HcmV?d00001 diff --git a/applications/plugins/bt_hid_app/assets/ButtonRight_4x7.png b/applications/plugins/bt_hid_app/assets/ButtonRight_4x7.png new file mode 100644 index 0000000000000000000000000000000000000000..8e1c74c1c0038ea55172f19ac875003fc80c2d06 GIT binary patch literal 1839 zcmcIlO>f*p7#Yw)M6zw!O+@VZ{?d|D~WYi~8rHRY?X-&T}Yen`g$^+EJ;z+|RV zE@PoDvZ9%#+_}3bC_5Cj8jDGq541mi{7F+&KF}W65sr$Xn5H|YrMQ2(J7%Yc%;(zO z57ax000=TsQ+1Ke@+w#iw3au3cGGQWY740k2ijH>P(6tD)S)be>gX6Tj7`<`b>di- zgWp$8Y+?i31~CzF0&E4uRlA=C(Mp~K`{74jEchB|)4DDK!ZVhSwdFyw0YIZ1cDh0S{OvfO-U_~ zvmRF*m9sWDXNH)GOyqS1Skhxbr6}s*7t&@~kFM(NW5}qh?Lu@lJ}HE;FDiLdGO>LO z5pS*%E2grR)l^;|?O5b_?u0me&c1U}%jrk8*%=Wk%i)8yp2P|kuxmKg<=(u_`oQRI_0 zS`-DNysBx=#3&qSkgA@hJP>~D+ZM(s5jI6Owp`?yE=3e`YGUqkVOp#Cp=3wR3O4hX zX6BLsN3UBzV(vI5;|SZHgOb=HD0VFjpTyfFW}GnQuh>2*Q`k>*cAmA#iUT7EXSpo# zkPm5~#I-o^cpgfe#P$=4-Pi*SpT!-@nJgp8L347xe>5EKl`=_ZFc8XGy+_j=_R_7! z@vZZMowS1GJ?Zw)eetks%~G{BTR>T}9|jt0j3Btyb*C3-`C?fwY3EY`q*oYZ39DpM z&uJ;PCZPLs4QO1Jd_|A1PF)azZJ)RZ`^-VMWr6e#XUOA%3eLG_Ch@BDOHzMk*MF0G zCo7xMd?Mg*HMIXw%nNz?%60fZiZPlqb?GqUpXO`F&Yi!okZl(n>P@r1P2i)yk3DgRwbHeNn6e|;J^SK4TM LH~i+q&mR8;k>NTA literal 0 HcmV?d00001 diff --git a/applications/plugins/bt_hid_app/assets/ButtonUp_7x4.png b/applications/plugins/bt_hid_app/assets/ButtonUp_7x4.png new file mode 100644 index 0000000000000000000000000000000000000000..1be79328b40a93297a5609756328406565c437c0 GIT binary patch literal 102 zcmeAS@N?(olHy`uVBq!ia0vp^>_E)I!3HFqj;YoHDIHH2#}J8d-yTOk1_O>mFaFD) zeWb+ZHz{mGZZ1QpXe09^4tcYT#4oe=UbmGC^A-KE*|F&zP#=S*tDnm{r-UX30HgpM AM*si- literal 0 HcmV?d00001 diff --git a/applications/plugins/bt_hid_app/assets/Ok_btn_9x9.png b/applications/plugins/bt_hid_app/assets/Ok_btn_9x9.png new file mode 100644 index 0000000000000000000000000000000000000000..9a1539da2049f12f7b25f96b11a9c40cd8227302 GIT binary patch literal 3605 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 QX&eA7&1_6djNPvM5BL~PlmGw# literal 0 HcmV?d00001 diff --git a/applications/plugins/bt_hid_app/assets/Pin_arrow_left_9x7.png b/applications/plugins/bt_hid_app/assets/Pin_arrow_left_9x7.png new file mode 100644 index 0000000000000000000000000000000000000000..fb4ded78fde8d1bf4f053ba0b353e7acf14f031a GIT binary patch literal 3603 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 literal 0 HcmV?d00001 diff --git a/applications/plugins/bt_hid_app/assets/Pin_arrow_up_7x9.png b/applications/plugins/bt_hid_app/assets/Pin_arrow_up_7x9.png new file mode 100644 index 0000000000000000000000000000000000000000..a91a6fd5e99a72112e28865cd8a004c7d1933fff GIT binary patch literal 3603 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 TFC+3}9cE|kU{!4CefEC<&8td2 literal 0 HcmV?d00001 diff --git a/applications/plugins/music_player/application.fam b/applications/plugins/music_player/application.fam index 76787e097..a36988983 100644 --- a/applications/plugins/music_player/application.fam +++ b/applications/plugins/music_player/application.fam @@ -11,8 +11,9 @@ App( provides=["music_player_start"], stack_size=2 * 1024, order=20, - fap_icon="../../../assets/icons/Archive/music_10px.png", + fap_icon="icons/music_10px.png", fap_category="Misc", + fap_icon_assets="icons", ) App( diff --git a/applications/plugins/music_player/icons/music_10px.png b/applications/plugins/music_player/icons/music_10px.png new file mode 100644 index 0000000000000000000000000000000000000000..d41eb0db8c822c60be6c097393b3682680b81a6c GIT binary patch literal 142 zcmeAS@N?(olHy`uVBq!ia0vp^AT}2xGmzZ=C-xtZVhivIasA%~WHIdey2}7aaTa() z7Bet#3xhBt!>l_x9Asc&(0a_=e)W&n8K5!-Pgg&ebxsLQ0Ao%f>i_@% literal 0 HcmV?d00001 diff --git a/applications/plugins/music_player/music_player.c b/applications/plugins/music_player/music_player.c index 192500c2e..28872284b 100644 --- a/applications/plugins/music_player/music_player.c +++ b/applications/plugins/music_player/music_player.c @@ -3,7 +3,7 @@ #include #include -#include +#include #include #include #include diff --git a/applications/plugins/picopass/application.fam b/applications/plugins/picopass/application.fam index bbe37e064..88460b4ff 100644 --- a/applications/plugins/picopass/application.fam +++ b/applications/plugins/picopass/application.fam @@ -17,4 +17,5 @@ App( name="loclass", ), ], + fap_icon_assets="icons", ) diff --git a/applications/plugins/picopass/icons/DolphinMafia_115x62.png b/applications/plugins/picopass/icons/DolphinMafia_115x62.png new file mode 100644 index 0000000000000000000000000000000000000000..66fdb40ff2651916faed4a2ae1d564cafdbf7bcb GIT binary patch literal 2504 zcmbVO3se(V8ji5Kg5ZmV3I#ewZHbsZB8b0=g#+k z_y7La$vcsnwa$(njyxXES*27&ad(Ehf@a%szx(??vFC0MMrAy=Img9%&ES885R5X2P@K{dB9p<$p?SQ()g~i~r4cNkC3JdH&i}sg6d%yza(--p8dMv@ zh!njtmnNcfH8EIj8V2M1)j>d@3E>C~1d9SDLpsSICOLnC7va{{Z80C1fUs$Deu(uz zAWj_#gi$mBNJXF!13?Io!6J#&-(L!@03Z+o#bAI~0tqEj1oTHFGGOY%=T4*XWF$)Q z`>C_ICpkZbWsQhfoSmI5%Jvgcv`#F6VOR`8Vh9p)2qBY0vZzT&GE1fz6a<6OdLyf+ zNWjX7YNwhuAF&nut zlTM%T7{|m!I$L;81?0JCCML&7h@%LG%A_%3 zO%`|J5~~^`5=Ij!OVKeDl|G%Q$Z2^1BoRS?PpqEAscc5@GXp|_vV@$^WlbUkA?_Ok zK?n#V5acTX5fGe&s<}GAQ5OB*z!a`e&iO^CEx1S+l}^!W3g`Ur;{!N`BvZ5jW@%VH?>OF2Tk@O zPGOv@&rGEfX|lv0Cxk2gKu)ie6Af#Vr9x}>!CI+Aiv@szVry$~6u{(al2-hTBEgTzn_D^}jklllIvu1V{Q`ig6OgP|0jI zN)sVEE|=@hm?j7H6PqgYzU5==|fB0<6@J90B?N8); z?B48M`Q6&q<>QYftD|a*tJ$!0YduA;TS}(23t@i9jJ}9E&d>+O-{j}lDtd6mP7wiU?pLh0* zla-TQ!!6f>9b(>jct-Z*@vzVmEjaUp9adYyRH)W#u&{1)0G7#K8z}OOe9Z4J`?k~5 z;u#n4^?R%GdBZDjly!H8xtVMF9ud_Q|CsUp%X4BI?jMd19&&9{QqgG_a)Rz9J*BH| z$zM9cbZYA6R(n(=QYD(cO(#Aoy6CQh;hG<}_gRz&>ZIovmNuT&Z9VwM8m5pu&$kG$ zvTJ!+pA|E6E-UBtJJrv;*XaRo7|Z#x4L(qON`UQa?6`jZqnkg3XliTEuJKo%PCa~M z@WlnE3u1ZRT?c;b@m&$07PGImr1km-TQZ8*DS|rZudw{x4R!5F9=$VOt{XWj(Y>BT zd-yG`a(KJ-o0Dfs8h&U=J*C(_ z=8hNq6aC?^r7wqGy5!v`zvX@KNEDDEpXqBVXiB`Z=eNZRgGG2tG`F;x~xDn9)G1Y@4Fl28Px*E!|ivy@~-8Lx%@`DyQ}?V z4f!BGF*jl}N~1D%!=YeZY6W)9lyDw_Uq#NDJx^=CJZDD2|CF# zA7Ixt{Z7BT8@4fZgFkI{D9fJxang<$JS``+d(*81cbB@prG*c!rZ)8U4y-<__Pt)Z zZ3lJfK;Y5eZHd?A3O-!mWX3$UChhmy)r@4iKkvyz(mdTtF7?TWn4`7t4=} zZ`OLe!fHzEo3eUH7jwVD-n?Xnx$AC<-H6`;RB2iYH9UO}ROfZkPOl32mRZ%`xW#FL zD@GqK${E&#=gzidc(qkxLZ^tk7u}u0Uu|;00}}A@rq4$9xE75>Hwj!4$Nk!`)YmDg{{4HeKCy?7Z85xPzg%Peucca}QJ6#D*z!+`G0ZOj literal 0 HcmV?d00001 diff --git a/applications/plugins/picopass/icons/DolphinNice_96x59.png b/applications/plugins/picopass/icons/DolphinNice_96x59.png new file mode 100644 index 0000000000000000000000000000000000000000..a299d3630239b4486e249cc501872bed5996df3b GIT binary patch literal 2459 zcmbVO3s4i+8V(M(gEFORwSrA`4O0uPn|M|5y* zB*aMDxC&7(gP9JN;POOi-9khrC>Z9YJs2U!LnVcQEEC0fDtKo&ILlzb30%M}3J^;~ zv7RzcsilOs4Mq@tD*&R;!LMSk2A~{(`HK9|hQBqEX)3sQr9Je6SZU*F-^fD-p+~Hs; zHLkO%v?>ZoxEv+F#whudr%615FkA0DYR0tMEo}3OOY#xecLWe>xV?u5KtSmC^ z7)Fmj6gjfKstiEV-*Cxbbb+&rRWuI_rBJ)ybs_f1Rn&f2>q3pYwI^|J(hdn{j{0EZIm_F zpIyIWLsRUgOItR-dUbVd|6Zo=_BU_Tj4|{{jxO#=JH4o8er(5{!nZD_j4}MH&zh~9 zVLC~y(0-D6GO0ghZD8BYzP?o{>22~lT6^d@X{SwQ8vrNY-PPIMajIwC)`s14Ep72@ zeq7YOzM`?U{+W)ocXBr`eSOcpk?Rxc=ou5&)fWW|pD};-Z0mvk9}=&`Rb&y<77W~a z(>6YM;6Y5aIU~JKZ}mQZynKHiSTQ#Bczn@&jTiN^?vPJ(jhm7cXLx0oum5P$`TceG zU+wR;OO^)8CVlnM)5p$CO&e94KJt>HccCaHGusmW_b`T6m| z-R6V6Db1pErTot?^d22ojm+2>_)FbD`_+WbDGMx9f@hO27maS2`csiV(D&Fs`PS2& zvrq18du_&zXID(!KIxsU$)iuTYuZ?zmYiP&n&i@Be{IdbS-jA2c0QAlu5NXQv_0K< z3Hvs4eeu6B7yD&CNT~gIkMV&UkRU=V!iQ(+_(O&u^ah$+s{_yn(yBYeD40HeU{xGsIT6W Zfq!wOp!Q<>&pI=m5)b(dHL6nbwD9yPZ!4!j_b)k${QZ;XFmLn zjqNsD+YPq1J8W%_*aXBie!OR3*tC!PwU_7Q9H4U564!{5l*E!$tK_0oAjM#0U}UIk zV5)0q5@Kj%Wo%$&Y@uynU}a#izM}XGiiX_$l+3hBs0L%8o)7~QD^p9LQiz6svOM}g O4Gf;HelF{r5}E+GUQp8j literal 0 HcmV?d00001 diff --git a/applications/plugins/picopass/icons/RFIDDolphinReceive_97x61.png b/applications/plugins/picopass/icons/RFIDDolphinReceive_97x61.png new file mode 100644 index 0000000000000000000000000000000000000000..e1f5f9f8017b49ab1ea78a9394ed8ea7684e3083 GIT binary patch literal 1421 zcmaJ>eQXnD7{Agv#{x2p_yIvKhl^r%z3;AfTbW%yMuEcUiZn{X?&IxtS=&4BZnPU= zlx>i)WI>$aQvw7<5XB`bIuRj@1WD9@Lr2D(5=YPwGc*zsSTf&kEAj{7lDqeL-}m`F zpTFm}Rj;U;Sva>4L6DijCB86RMfkc4?C{(9_MQ1~dCu}jtr{(6r9=ZD9z~M?8cc|F zAPhvM>5U7Z96{_EH4?R=q2+?CB^+W_$B|Cx5RD+^6=_|R8-RsMpiWJ?vC&g!FjQ6C z*cvWGhIB8eSC=#!pr(06L~d@7c?GLjjFzVbXdnSB5ltuJNmEF>u?f2Zl(WYKhEAwh z4Q^~QsA#Af^=bw{oemP0Nz#dy@(x9mL|KwbP@1GEf@BGb#Ys|Nc!6cnsRx7Z3?(Ln zeSs-waOcMAElU>&B9%%xQj9}0>IjPGd4i+~n#Q39ZZ;(?F^wn9g*gj8V9JK7TdI~s zvlc~3YqZ=L40SSxgdPgrH=H!5Dg|psq(z;e93+uQWD}dvHmxxDKa7WJn~^3R5Mf|y zjfM;x5?h!9!{R;KQC1N~Bdj!3*cCDE)8xhkNLoRk8-q6vMO6faWjLq8D>(0>TsY@s zOL2+gT{ut5lFP+&G;q>6I}gJ}uK`3$Ga{N6&(WZ|Ub8f_Uei&p7kz1snpCuuxhUJA$%K8tP}c(` zU}y<+qQrvwF!u}>V`HR<(=n36VBt1B^`_fx&WPzU_AMY;oo7=&mIg2tu^u1f5 z)@y#lGg2HF{icooYxXeey6HJl+%===Q-Yg*f$J(< z+gbGCvVprluc__jmS6m=F>l7JjJ;Cb^sMdho~B4w{1|(u#k_H5R;4;`zs)u0gC*%S zI_>C5rsHbY>U}-r=8b&^Mh7zat>Eaqs$E;p%^t}^&M*C`d_!V*2g<#^ZLQq9;N6x= zv^)OzpYh#+OwHKfQ+kHHZreNi()*6Nw&PX5?kxF@U2EB*+}LH?toC1`{oRjksXb78 zx8u;V!Qv~6!ySjp4u16f-y8F;3}d=*b!=ao^)Gw)nS({6qa!CbyuwrWMvi?_zz4rL rb-KI#{JuTj%qEZPotyLfwj*}ruaRky;O7Gyvp>k7e}(TvWo_$!Vg&g_ literal 0 HcmV?d00001 diff --git a/applications/plugins/picopass/icons/RFIDDolphinSend_97x61.png b/applications/plugins/picopass/icons/RFIDDolphinSend_97x61.png new file mode 100644 index 0000000000000000000000000000000000000000..380a970d9004cba5520560fd9aa24aa42924e2a1 GIT binary patch literal 1418 zcmaJ>eQXnD7{6`EHeeAZ8ii;s2g4C}y=!}S>mBQswzrLLl$EYRfoyOe?`_T2-g$Sk z9pb2CPQ(a0XM$+dKhO~O0nx;1L}kUOGdh8Ve-^?LG)CD7lOfXp&bQl&{IPJ!-TS=n z`~05I-*YefH&^B@S+xW~kUZ~3J^)t%zRsL1_&wM?{Wx46Gs{C}t*V$YK?jISRz-k% zBSHfR06}hjW(brZNLC^o44EO{CQec#79pi$iAOYuMv#)SxF$$Vz(hsR5RN*rYhQeg zp<&sHZKHjpPxFAr@WwqlsNJ(UDD7#ISQ#rTMN8rwG!Ox%fW{-uQG<&+v01wulvBq9 zhR&*(O-^hssF2T(dQ=^tjD^G{l4Q_g)*=g{AcBJ%MzrGu-R~^fg7z+Q;6eHV@=uu4-82U zYi3xDqA81lsJ56+42C+FLqzlW?i!97^Ob@%BjSQaSS=(GiKG&n)i%rk_&3wK+`#f1_%uMx&~s9uHc$EgY5An6W<9p}B;4 zpogCYa)qu&(Ag4m;RW0?c3PnnQowBrN#lw@*>TY-L0(ZbMKQG9>QDeSkC*Q$-5f{Z z2~0stN5WYZfssX-#AS)L;{r{IxG2~K90(F6+7!i3SxJn5ArdLp+{2>u5u|2HygL+d zb9byj6wZ} zqrIB@aESUiV~B&zwY0sUci%;mf;cmkA+7cD0^$ih9{f{w;v_DJ`sY;R`f3( z?7BXf_vMbW zuU1_w753GAG_~{axB58aI?KM!#N|b)zyZV)ZU9QaOj9KuN$fX{&>fy=f`f8Io+CbZIMpovDCx1HL z?$&C^=R1DyispWLc%|FSKGs*ccUMOLz=7=zt7r7(!|y7;X08;c-@aJ>V5pwIR`S;) wTk7+73`}?J{<7dJ@~ literal 0 HcmV?d00001 diff --git a/applications/plugins/picopass/picopass_device.c b/applications/plugins/picopass/picopass_device.c index b6e69cc21..199b79e97 100644 --- a/applications/plugins/picopass/picopass_device.c +++ b/applications/plugins/picopass/picopass_device.c @@ -2,6 +2,7 @@ #include #include +#include #define TAG "PicopassDevice" diff --git a/applications/plugins/picopass/picopass_i.h b/applications/plugins/picopass/picopass_i.h index 8e011f222..469a672b7 100644 --- a/applications/plugins/picopass/picopass_i.h +++ b/applications/plugins/picopass/picopass_i.h @@ -24,6 +24,7 @@ #include #include +#include #define PICOPASS_TEXT_STORE_SIZE 128 diff --git a/applications/plugins/signal_generator/application.fam b/applications/plugins/signal_generator/application.fam index 7794ee492..de915733c 100644 --- a/applications/plugins/signal_generator/application.fam +++ b/applications/plugins/signal_generator/application.fam @@ -9,4 +9,5 @@ App( order=50, fap_icon="signal_gen_10px.png", fap_category="Tools", + fap_icon_assets="icons", ) diff --git a/applications/plugins/signal_generator/icons/SmallArrowDown_3x5.png b/applications/plugins/signal_generator/icons/SmallArrowDown_3x5.png new file mode 100644 index 0000000000000000000000000000000000000000..1912e5d246268d75a20984bdc8b996d503f3d166 GIT binary patch literal 3592 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 BDRuw= literal 0 HcmV?d00001 diff --git a/applications/plugins/signal_generator/icons/SmallArrowUp_3x5.png b/applications/plugins/signal_generator/icons/SmallArrowUp_3x5.png new file mode 100644 index 0000000000000000000000000000000000000000..9c6242078d3bcc9a93eb55b6b5f358720ee6662c GIT binary patch literal 7976 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 +#include typedef enum { LineIndexChannel, diff --git a/applications/plugins/weather_station/images/Lock_7x8.png b/applications/plugins/weather_station/images/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/plugins/weather_station/images/Pin_back_arrow_10x8.png b/applications/plugins/weather_station/images/Pin_back_arrow_10x8.png new file mode 100644 index 0000000000000000000000000000000000000000..3bafabd144864b575144c75b592e5eaf53974566 GIT binary patch literal 3606 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 TFC+3}9cE|kU{!4CefEC<&8td2 literal 0 HcmV?d00001 diff --git a/applications/plugins/weather_station/images/Quest_7x8.png b/applications/plugins/weather_station/images/Quest_7x8.png new file mode 100644 index 0000000000000000000000000000000000000000..6825247fbeaf98b4d0d9f8aa15d4f2c5f45dcf3c GIT binary patch literal 3675 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 N!3~Mzlm%~0{|8*@s)qmo literal 0 HcmV?d00001 diff --git a/applications/plugins/weather_station/images/Unlock_7x8.png b/applications/plugins/weather_station/images/Unlock_7x8.png new file mode 100644 index 0000000000000000000000000000000000000000..9d82b4daf3c722a5244f537f18b147833c2fb4ee GIT binary patch literal 3598 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 MrJY5ondim-0h+K&wEzGB literal 0 HcmV?d00001 diff --git a/applications/plugins/weather_station/images/WarningDolphin_45x42.png b/applications/plugins/weather_station/images/WarningDolphin_45x42.png new file mode 100644 index 0000000000000000000000000000000000000000..d766ffbb444db1739f2ccd030e506e8bada11ee8 GIT binary patch literal 1139 zcmaJ=TWAz#6rQ4%BpO*okt7zz3Bkm=bK9L=XV}qhW_HceY#KHzP4Z$UGyf(-b}pUy z<8ESW=_MtCk6tj|`k)VrCbo(SMH0n`eW+KIU`wG5O`$IeMg)Vzf7adDhv+af=lqBB zo%5Z`zqhqzdu2s+1%_dji6%LPq#u2o%9fzN?;7Dlq6)^^VVjkKImH23RI|DPo-mXi zkOGP}@Wrnnf?-SQ^>jOIPc{pxWsr*JL*@+|p)oA7EpIDoAAoo_=+RA)c=F3Qf$N$` ze9k55q%DD7y=l+^ZG$aob+Aw6HDcRVJdzhs00Te;&l_3O74jlch$|r7GgAa!aDjay z@rG1;vK5ys2jF3n@vAgV<6)izn!k6Qhd>D(EhD7l zcrhJ1i9|1iwm?z2T#n2INXzM=7@p@Tnx$CQk39VDfC-hn-*jtB5oF-1j&4KUGI1}W z(rxuakw9eMRAJc3%8 zlBq3$QTyJX$a6$&1ldyi4Pe5AEE32Sg@vDzY~7qR?1u@oXh zd9(fBtV<@eK%Tm=yy&p7{=h^#@1W)WFFSe!U5pP~o71uR`FW)7xc*=d5_b}EG@XBZ z@<7MRp-;+|o}SzJvMyfx>37Y4etBizf%0H*zaIF?2ObZt?$D;{o|esJ z*PgAOIO^*8tL%z2)|}k$DP5kN5}8qo*wNa8y?R2=%wO{gCD(`sHt}TzWQuK zMDIJap(nb2=7*ns*oDcRBbC23yy`kvKYV`ovU`a=EmxNv-90;;5r6+l8hQTp^u`Hn XX6*+%8gHC`h)Tl}u@-r>vFqE{F~5F& literal 0 HcmV?d00001 diff --git a/applications/plugins/weather_station/views/weather_station_receiver.c b/applications/plugins/weather_station/views/weather_station_receiver.c index d30b7926b..61b152602 100644 --- a/applications/plugins/weather_station/views/weather_station_receiver.c +++ b/applications/plugins/weather_station/views/weather_station_receiver.c @@ -1,11 +1,10 @@ #include "weather_station_receiver.h" #include "../weather_station_app_i.h" -#include "weather_station_icons.h" +#include #include #include #include -#include #include #define FRAME_HEIGHT 12 diff --git a/applications/services/bt/bt_service/bt.c b/applications/services/bt/bt_service/bt.c index c003013e4..62b5ab109 100644 --- a/applications/services/bt/bt_service/bt.c +++ b/applications/services/bt/bt_service/bt.c @@ -4,6 +4,7 @@ #include #include +#include #define TAG "BtSrv" diff --git a/applications/services/desktop/views/desktop_view_lock_menu.c b/applications/services/desktop/views/desktop_view_lock_menu.c index 8cb8a7a12..486be23b5 100644 --- a/applications/services/desktop/views/desktop_view_lock_menu.c +++ b/applications/services/desktop/views/desktop_view_lock_menu.c @@ -1,5 +1,6 @@ #include #include +#include #include "../desktop_i.h" #include "desktop_view_lock_menu.h" diff --git a/applications/services/desktop/views/desktop_view_locked.c b/applications/services/desktop/views/desktop_view_locked.c index 915b26103..d18ed6c98 100644 --- a/applications/services/desktop/views/desktop_view_locked.c +++ b/applications/services/desktop/views/desktop_view_locked.c @@ -4,6 +4,7 @@ #include #include #include +#include #include #include diff --git a/applications/services/desktop/views/desktop_view_pin_input.c b/applications/services/desktop/views/desktop_view_pin_input.c index bf05f06b9..b86bf2929 100644 --- a/applications/services/desktop/views/desktop_view_pin_input.c +++ b/applications/services/desktop/views/desktop_view_pin_input.c @@ -2,6 +2,7 @@ #include #include #include +#include #include #include diff --git a/applications/services/dialogs/dialogs_api.c b/applications/services/dialogs/dialogs_api.c index 6fd51782d..ee959a33c 100644 --- a/applications/services/dialogs/dialogs_api.c +++ b/applications/services/dialogs/dialogs_api.c @@ -1,6 +1,7 @@ #include "dialogs/dialogs_message.h" #include "dialogs_i.h" #include "dialogs_api_lock.h" +#include /****************** File browser ******************/ diff --git a/applications/services/gui/canvas.h b/applications/services/gui/canvas.h index a67e58494..a3df5adc7 100644 --- a/applications/services/gui/canvas.h +++ b/applications/services/gui/canvas.h @@ -7,7 +7,7 @@ #include #include -#include +#include #ifdef __cplusplus extern "C" { diff --git a/applications/services/gui/gui.c b/applications/services/gui/gui.c index 42712ed90..2d06d70c7 100644 --- a/applications/services/gui/gui.c +++ b/applications/services/gui/gui.c @@ -1,5 +1,6 @@ #include "gui/canvas.h" #include "gui_i.h" +#include #define TAG "GuiSrv" diff --git a/applications/services/gui/icon_animation.h b/applications/services/gui/icon_animation.h index dab9d996d..684790353 100644 --- a/applications/services/gui/icon_animation.h +++ b/applications/services/gui/icon_animation.h @@ -7,7 +7,7 @@ #include #include -#include +#include #ifdef __cplusplus extern "C" { diff --git a/applications/services/gui/modules/button_menu.c b/applications/services/gui/modules/button_menu.c index 37a04326a..ff12a9311 100644 --- a/applications/services/gui/modules/button_menu.c +++ b/applications/services/gui/modules/button_menu.c @@ -5,6 +5,7 @@ #include #include #include +#include #define ITEM_FIRST_OFFSET 17 #define ITEM_NEXT_OFFSET 4 diff --git a/applications/services/gui/modules/byte_input.c b/applications/services/gui/modules/byte_input.c index 8d7e7fd4f..bc19f0eee 100644 --- a/applications/services/gui/modules/byte_input.c +++ b/applications/services/gui/modules/byte_input.c @@ -1,6 +1,7 @@ -#include "byte_input.h" -#include #include +#include +#include +#include "byte_input.h" struct ByteInput { View* view; diff --git a/applications/services/gui/modules/menu.c b/applications/services/gui/modules/menu.c index db0717f77..6983e0108 100644 --- a/applications/services/gui/modules/menu.c +++ b/applications/services/gui/modules/menu.c @@ -2,6 +2,7 @@ #include #include +#include #include struct Menu { diff --git a/applications/services/gui/modules/text_input.c b/applications/services/gui/modules/text_input.c index 79fa87728..540e4b7c4 100644 --- a/applications/services/gui/modules/text_input.c +++ b/applications/services/gui/modules/text_input.c @@ -1,5 +1,6 @@ #include "text_input.h" #include +#include #include struct TextInput { diff --git a/applications/services/power/power_service/power_i.h b/applications/services/power/power_service/power_i.h index 66ced885b..8cb5140d7 100644 --- a/applications/services/power/power_service/power_i.h +++ b/applications/services/power/power_service/power_i.h @@ -5,6 +5,7 @@ #include #include #include +#include #include #include "views/power_off.h" diff --git a/applications/services/power/power_service/views/power_off.c b/applications/services/power/power_service/views/power_off.c index b0046325c..f14a18d7e 100644 --- a/applications/services/power/power_service/views/power_off.c +++ b/applications/services/power/power_service/views/power_off.c @@ -1,6 +1,7 @@ #include "power_off.h" #include #include +#include struct PowerOff { View* view; diff --git a/applications/services/power/power_service/views/power_unplug_usb.c b/applications/services/power/power_service/views/power_unplug_usb.c index 5632cd8b0..c2d61139e 100644 --- a/applications/services/power/power_service/views/power_unplug_usb.c +++ b/applications/services/power/power_service/views/power_unplug_usb.c @@ -1,6 +1,7 @@ #include "power_unplug_usb.h" #include #include +#include struct PowerUnplugUsb { View* view; diff --git a/applications/services/storage/storage.c b/applications/services/storage/storage.c index 9079a95ed..700408c9d 100644 --- a/applications/services/storage/storage.c +++ b/applications/services/storage/storage.c @@ -5,6 +5,7 @@ #include "storage/storage_glue.h" #include "storages/storage_int.h" #include "storages/storage_ext.h" +#include #define STORAGE_TICK 1000 diff --git a/applications/settings/about/about.c b/applications/settings/about/about.c index a42969b2b..1719e188d 100644 --- a/applications/settings/about/about.c +++ b/applications/settings/about/about.c @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include diff --git a/applications/settings/bt_settings_app/bt_settings_app.h b/applications/settings/bt_settings_app/bt_settings_app.h index c45ff3db0..b79e36951 100644 --- a/applications/settings/bt_settings_app/bt_settings_app.h +++ b/applications/settings/bt_settings_app/bt_settings_app.h @@ -6,6 +6,7 @@ #include #include #include +#include #include #include diff --git a/applications/settings/desktop_settings/desktop_settings_app.h b/applications/settings/desktop_settings/desktop_settings_app.h index fc56c3253..6f97564c9 100644 --- a/applications/settings/desktop_settings/desktop_settings_app.h +++ b/applications/settings/desktop_settings/desktop_settings_app.h @@ -7,6 +7,7 @@ #include #include #include +#include #include #include diff --git a/applications/settings/power_settings_app/power_settings_app.h b/applications/settings/power_settings_app/power_settings_app.h index 8429b54b4..cd05846c0 100644 --- a/applications/settings/power_settings_app/power_settings_app.h +++ b/applications/settings/power_settings_app/power_settings_app.h @@ -6,6 +6,7 @@ #include #include #include +#include #include "views/battery_info.h" #include diff --git a/applications/settings/power_settings_app/views/battery_info.c b/applications/settings/power_settings_app/views/battery_info.c index 1a8bc71ec..e1b7adb4b 100644 --- a/applications/settings/power_settings_app/views/battery_info.c +++ b/applications/settings/power_settings_app/views/battery_info.c @@ -1,6 +1,7 @@ #include "battery_info.h" #include #include +#include struct BatteryInfo { View* view; diff --git a/applications/settings/storage_settings/storage_settings.h b/applications/settings/storage_settings/storage_settings.h index 4cf185e0c..664e74c84 100644 --- a/applications/settings/storage_settings/storage_settings.h +++ b/applications/settings/storage_settings/storage_settings.h @@ -4,6 +4,7 @@ #include #include #include +#include #include #include diff --git a/applications/system/updater/views/updater_main.c b/applications/system/updater/views/updater_main.c index 5ed3c70aa..1199cc882 100644 --- a/applications/system/updater/views/updater_main.c +++ b/applications/system/updater/views/updater_main.c @@ -2,6 +2,7 @@ #include #include #include +#include #include #include diff --git a/firmware/targets/f7/api_symbols.csv b/firmware/targets/f7/api_symbols.csv index fd11dffe0..00ba30c24 100644 --- a/firmware/targets/f7/api_symbols.csv +++ b/firmware/targets/f7/api_symbols.csv @@ -1,5 +1,5 @@ entry,status,name,type,params -Version,+,6.0,, +Version,+,7.0,, Header,+,applications/services/bt/bt_service/bt.h,, Header,+,applications/services/cli/cli.h,, Header,+,applications/services/cli/cli_vcp.h,, @@ -2661,191 +2661,7 @@ Function,-,yn,double,"int, double" Function,-,ynf,float,"int, float" Variable,-,AHBPrescTable,const uint32_t[16], Variable,-,APBPrescTable,const uint32_t[8], -Variable,+,A_125khz_14,const Icon, -Variable,+,A_BadUsb_14,const Icon, -Variable,+,A_Debug_14,const Icon, -Variable,+,A_FileManager_14,const Icon, -Variable,+,A_GPIO_14,const Icon, -Variable,+,A_Infrared_14,const Icon, -Variable,+,A_Levelup1_128x64,const Icon, -Variable,+,A_Levelup2_128x64,const Icon, -Variable,+,A_Loading_24,const Icon, -Variable,+,A_NFC_14,const Icon, -Variable,+,A_Plugins_14,const Icon, -Variable,+,A_Round_loader_8x8,const Icon, -Variable,+,A_Settings_14,const Icon, -Variable,+,A_Sub1ghz_14,const Icon, -Variable,+,A_U2F_14,const Icon, -Variable,+,A_iButton_14,const Icon, Variable,-,ITM_RxBuffer,volatile int32_t, -Variable,+,I_125_10px,const Icon, -Variable,+,I_ActiveConnection_50x64,const Icon, -Variable,+,I_Alert_9x8,const Icon, -Variable,+,I_ArrowC_1_36x36,const Icon, -Variable,+,I_ArrowDownEmpty_14x15,const Icon, -Variable,+,I_ArrowDownFilled_14x15,const Icon, -Variable,+,I_ArrowUpEmpty_14x15,const Icon, -Variable,+,I_ArrowUpFilled_14x15,const Icon, -Variable,+,I_Attention_5x8,const Icon, -Variable,+,I_Auth_62x31,const Icon, -Variable,+,I_BLE_Pairing_128x64,const Icon, -Variable,+,I_Background_128x11,const Icon, -Variable,+,I_BatteryBody_52x28,const Icon, -Variable,+,I_Battery_16x16,const Icon, -Variable,+,I_Battery_26x8,const Icon, -Variable,+,I_Ble_connected_15x15,const Icon, -Variable,+,I_Ble_disconnected_15x15,const Icon, -Variable,+,I_Bluetooth_Connected_16x8,const Icon, -Variable,+,I_Bluetooth_Idle_5x8,const Icon, -Variable,+,I_ButtonCenter_7x7,const Icon, -Variable,+,I_ButtonDown_7x4,const Icon, -Variable,+,I_ButtonLeftSmall_3x5,const Icon, -Variable,+,I_ButtonLeft_4x7,const Icon, -Variable,+,I_ButtonRightSmall_3x5,const Icon, -Variable,+,I_ButtonRight_4x7,const Icon, -Variable,+,I_ButtonUp_7x4,const Icon, -Variable,+,I_Button_18x18,const Icon, -Variable,+,I_Certification1_103x56,const Icon, -Variable,+,I_Certification2_98x33,const Icon, -Variable,+,I_Charging_lightning_9x10,const Icon, -Variable,+,I_Charging_lightning_mask_9x10,const Icon, -Variable,+,I_Circles_47x47,const Icon, -Variable,+,I_Clock_18x18,const Icon, -Variable,+,I_Connect_me_62x31,const Icon, -Variable,+,I_Connected_62x31,const Icon, -Variable,+,I_CoolHi_25x27,const Icon, -Variable,+,I_CoolHi_hvr_25x27,const Icon, -Variable,+,I_CoolLo_25x27,const Icon, -Variable,+,I_CoolLo_hvr_25x27,const Icon, -Variable,+,I_Cry_dolph_55x52,const Icon, -Variable,+,I_DFU_128x50,const Icon, -Variable,+,I_Dehumidify_25x27,const Icon, -Variable,+,I_Dehumidify_hvr_25x27,const Icon, -Variable,+,I_Detailed_chip_17x13,const Icon, -Variable,+,I_DolphinCommon_56x48,const Icon, -Variable,+,I_DolphinMafia_115x62,const Icon, -Variable,+,I_DolphinNice_96x59,const Icon, -Variable,+,I_DolphinReadingSuccess_59x63,const Icon, -Variable,+,I_DolphinWait_61x59,const Icon, -Variable,+,I_DoorLeft_70x55,const Icon, -Variable,+,I_DoorRight_70x55,const Icon, -Variable,+,I_Down_25x27,const Icon, -Variable,+,I_Down_hvr_25x27,const Icon, -Variable,+,I_Drive_112x35,const Icon, -Variable,+,I_Error_18x18,const Icon, -Variable,+,I_Error_62x31,const Icon, -Variable,+,I_EviSmile1_18x21,const Icon, -Variable,+,I_EviSmile2_18x21,const Icon, -Variable,+,I_EviWaiting1_18x21,const Icon, -Variable,+,I_EviWaiting2_18x21,const Icon, -Variable,+,I_FaceCharging_29x14,const Icon, -Variable,+,I_FaceConfused_29x14,const Icon, -Variable,+,I_FaceNopower_29x14,const Icon, -Variable,+,I_FaceNormal_29x14,const Icon, -Variable,+,I_GameMode_11x8,const Icon, -Variable,+,I_Health_16x16,const Icon, -Variable,+,I_HeatHi_25x27,const Icon, -Variable,+,I_HeatHi_hvr_25x27,const Icon, -Variable,+,I_HeatLo_25x27,const Icon, -Variable,+,I_HeatLo_hvr_25x27,const Icon, -Variable,+,I_Hidden_window_9x8,const Icon, -Variable,+,I_InfraredArrowDown_4x8,const Icon, -Variable,+,I_InfraredArrowUp_4x8,const Icon, -Variable,+,I_InfraredLearnShort_128x31,const Icon, -Variable,+,I_KeyBackspaceSelected_16x9,const Icon, -Variable,+,I_KeyBackspace_16x9,const Icon, -Variable,+,I_KeySaveSelected_24x11,const Icon, -Variable,+,I_KeySave_24x11,const Icon, -Variable,+,I_Keychain_39x36,const Icon, -Variable,+,I_Left_mouse_icon_9x9,const Icon, -Variable,+,I_Lock_7x8,const Icon, -Variable,+,I_Lock_8x8,const Icon, -Variable,+,I_MHz_25x11,const Icon, -Variable,+,I_Medium_chip_22x21,const Icon, -Variable,+,I_Modern_reader_18x34,const Icon, -Variable,+,I_Move_flipper_26x39,const Icon, -Variable,+,I_Mute_25x27,const Icon, -Variable,+,I_Mute_hvr_25x27,const Icon, -Variable,+,I_NFC_manual_60x50,const Icon, -Variable,+,I_Nfc_10px,const Icon, -Variable,+,I_Off_25x27,const Icon, -Variable,+,I_Off_hvr_25x27,const Icon, -Variable,+,I_Ok_btn_9x9,const Icon, -Variable,+,I_Ok_btn_pressed_13x13,const Icon, -Variable,+,I_Percent_10x14,const Icon, -Variable,+,I_Pin_arrow_down_7x9,const Icon, -Variable,+,I_Pin_arrow_left_9x7,const Icon, -Variable,+,I_Pin_arrow_right_9x7,const Icon, -Variable,+,I_Pin_arrow_up_7x9,const Icon, -Variable,+,I_Pin_attention_dpad_29x29,const Icon, -Variable,+,I_Pin_back_arrow_10x8,const Icon, -Variable,+,I_Pin_back_full_40x8,const Icon, -Variable,+,I_Pin_pointer_5x3,const Icon, -Variable,+,I_Pin_star_7x7,const Icon, -Variable,+,I_Power_25x27,const Icon, -Variable,+,I_Power_hvr_25x27,const Icon, -Variable,+,I_Pressed_Button_13x13,const Icon, -Variable,+,I_Quest_7x8,const Icon, -Variable,+,I_RFIDBigChip_37x36,const Icon, -Variable,+,I_RFIDDolphinReceive_97x61,const Icon, -Variable,+,I_RFIDDolphinSend_97x61,const Icon, -Variable,+,I_RFIDDolphinSuccess_108x57,const Icon, -Variable,+,I_Reader_detect_43x40,const Icon, -Variable,+,I_Release_arrow_18x15,const Icon, -Variable,+,I_Restoring_38x32,const Icon, -Variable,+,I_Right_mouse_icon_9x9,const Icon, -Variable,+,I_SDQuestion_35x43,const Icon, -Variable,+,I_SDcardFail_11x8,const Icon, -Variable,+,I_SDcardMounted_11x8,const Icon, -Variable,+,I_Scanning_123x52,const Icon, -Variable,+,I_SmallArrowDown_3x5,const Icon, -Variable,+,I_SmallArrowDown_4x7,const Icon, -Variable,+,I_SmallArrowUp_3x5,const Icon, -Variable,+,I_SmallArrowUp_4x7,const Icon, -Variable,+,I_Smile_18x18,const Icon, -Variable,+,I_Space_65x18,const Icon, -Variable,+,I_Tap_reader_36x38,const Icon, -Variable,+,I_Temperature_16x16,const Icon, -Variable,+,I_Unlock_7x8,const Icon, -Variable,+,I_Unplug_bg_bottom_128x10,const Icon, -Variable,+,I_Unplug_bg_top_128x14,const Icon, -Variable,+,I_Up_25x27,const Icon, -Variable,+,I_Up_hvr_25x27,const Icon, -Variable,+,I_Updating_32x40,const Icon, -Variable,+,I_UsbTree_48x22,const Icon, -Variable,+,I_Vol_down_25x27,const Icon, -Variable,+,I_Vol_down_hvr_25x27,const Icon, -Variable,+,I_Vol_up_25x27,const Icon, -Variable,+,I_Vol_up_hvr_25x27,const Icon, -Variable,+,I_Voldwn_6x6,const Icon, -Variable,+,I_Voltage_16x16,const Icon, -Variable,+,I_Volup_8x6,const Icon, -Variable,+,I_WarningDolphin_45x42,const Icon, -Variable,+,I_Warning_30x23,const Icon, -Variable,+,I_back_10px,const Icon, -Variable,+,I_badusb_10px,const Icon, -Variable,+,I_dir_10px,const Icon, -Variable,+,I_iButtonDolphinVerySuccess_108x52,const Icon, -Variable,+,I_iButtonKey_49x44,const Icon, -Variable,+,I_ibutt_10px,const Icon, -Variable,+,I_ir_10px,const Icon, -Variable,+,I_loading_10px,const Icon, -Variable,+,I_music_10px,const Icon, -Variable,+,I_passport_bad1_46x49,const Icon, -Variable,+,I_passport_bad2_46x49,const Icon, -Variable,+,I_passport_bad3_46x49,const Icon, -Variable,+,I_passport_bottom_128x18,const Icon, -Variable,+,I_passport_happy1_46x49,const Icon, -Variable,+,I_passport_happy2_46x49,const Icon, -Variable,+,I_passport_happy3_46x49,const Icon, -Variable,+,I_passport_left_6x46,const Icon, -Variable,+,I_passport_okay1_46x49,const Icon, -Variable,+,I_passport_okay2_46x49,const Icon, -Variable,+,I_passport_okay3_46x49,const Icon, -Variable,+,I_sub1_10px,const Icon, -Variable,+,I_u2f_10px,const Icon, -Variable,+,I_unknown_10px,const Icon, -Variable,+,I_update_10px,const Icon, Variable,-,MSIRangeTable,const uint32_t[16], Variable,-,SmpsPrescalerTable,const uint32_t[4][6], Variable,+,SystemCoreClock,uint32_t, From 9cd0592aafb826ea08d180c857d399acdf607706 Mon Sep 17 00:00:00 2001 From: Skorpionm <85568270+Skorpionm@users.noreply.github.com> Date: Fri, 28 Oct 2022 18:31:41 +0400 Subject: [PATCH 06/49] SubGhz: add keeloq potocol JCM_Tech (#1939) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * SubGhz: add keeloq potocol JCM_Tech * SubGhz: add new metod decoder Co-authored-by: あく --- assets/resources/subghz/assets/keeloq_mfcodes | 99 ++++++++++--------- lib/subghz/protocols/keeloq.c | 18 ++++ lib/subghz/protocols/keeloq_common.c | 30 +++++- lib/subghz/protocols/keeloq_common.h | 18 ++++ 4 files changed, 117 insertions(+), 48 deletions(-) diff --git a/assets/resources/subghz/assets/keeloq_mfcodes b/assets/resources/subghz/assets/keeloq_mfcodes index b8fc36903..1b27bfb01 100644 --- a/assets/resources/subghz/assets/keeloq_mfcodes +++ b/assets/resources/subghz/assets/keeloq_mfcodes @@ -1,50 +1,55 @@ Filetype: Flipper SubGhz Keystore File Version: 0 Encryption: 1 -IV: 2A 34 F1 5A AF 6F F5 1A 83 A6 1E DA DE B7 3D F1 -06B63DF24AE073A2F2B19C55CA9E8364FBECD26E49C551990153F6513BDE5267 -6139C78C74C341EB7474085CF1D047BD6FB005F80A72AF3EF3F89D58EF5DF500 -D85F11689020ECA47FBE9C2B67EE41A81E1F06DE2A35AF958965E3ECE29EA701 -1AE9073A42FE0E439544FE6945F6B33CF15A7A4A279020B5E0B3BE33FD189A7E -E161F007854BB33E0056FA09A2E2DEE66789B5C87C8D6D3DE2C8C1BD2B48983EB9D1C5697CA6E95996918F7C47B761B0 -59AE4644DCB3D720C38B5115F230DA58E7BE0A697907F6174BB05AB7886ACDB1 -634DF0BCC185C4C1F7E1B1594B4438D051ABAE092433078963063B51D961D08C -1EBEBCB49E498B9BE977D53EC21B9A546155B627737BD0AA832D496035729346 -4DFA93E639197772D57E8ACE04512CEFC045B8CC965C175A25ED525B630CBB63 -C2D5235D1014A319B249EAE8A5EE350F18D5AB8A498EF222704BD4EB1435F388 -F66D1937160E1392197F463A52E87FCE938A92070892113443C348D7553327A5715CF615CE2F2C96284F47759E043419 -841D29E7CBE040188E2283BFBA9F26EF2F65CCB085B56C3515E8C46C3F20BD75BAA963550869435FDAF509CEEE66A2C4 -7D87E24487D307635E7A17B989B8547EE11F3BF3468D055F0B44633B631BA42C -B4916043973501B95A82B329196D6EBA69FBBC3AF8FD914583104E0E18CE82F6 -E4649F9C2A5465D2EA6F3E9724DD06CD6962FE2BAEB14F1453C14D1559232AE1 -96E15D890DF7FD348441F5E429A875754C6BF0520A787F8E9D8C5415674783CC -CB52005EDED47B57F795BC92FB0522EAB18D23EE028B8D10ED57828C250EB285BFEC6E4A4BE8DABCE0D57ECAA20D90C3 -8E5A50C7D5C374445E88752301D20F0B3D6E4988B61D90FD63779B0EDEF9C60D -49D6CB276A0E5FF134A38062503F01351F44CD6455708B50B5F07D03FC477C33 -CB45B56613DF208E79E4E10A6510F07DC1AA49210C7B94E8BBAECD2C35EC6ABC99FB10FD7C96DD6BB6A6685E9FAD93FB -0743F3CC51200F763C242F1956B4D775C092ADF1A5C19ACAE96EB60C2990CF214F8FEA8FC6749286F6BDAB67657C479A -E5608B28A058787D64A145F0362DEFD98CAE0B5A0F22C6DA7C6D278C7B5F95E3 -D4C113D43E7FB6D2EFA9E87471AA76A61B26872607B4AF5B87F9D72113835CE6 -2DC502800BFD21B76126390CA64A08C5432A2254E822F214CDE1EA11430084C5 -CA22C73010B0F1CB8009601BE2AF0B3674D83D5880E4A26C2A3FF0EA0A098CEA -E53B2B102FDB000E9BB747F957156976E5A0C0E3898AA844C13AE8A9CEE7013B -95CF1A46FFC252BE92919531C92BF6A3AA1B16C170DF4461EC54BE07A55C2387 -2EC7E24090F6DFFF6F2F2D8874D2F36AA769995F31F29FBE3B0EA6A16C3EE833 -C1145B1D9AC70761EA902B86455C1BE1BB1153552A1F7327411DECABE538827B -18D596CADD2EE544200A58716C7A4690B658E58CC2B97334740F70894A6C90FA -6A2F8859DFF01E13AC6C5300AD4A2218810FC91A6FB64A560E99FE6C99226AD2 -48D2EB5A08E35AF89A3B7A1CFDEE829FC0C2DDD2E965F4E3D043B0B14CB7825E -91039325D53CDD0236D1CD13047973A013C14B45A32DE0784A73BFABCEAFBCD1 -51B4EAC87C4DC49B007F40D38B8166C388A1AF25E8D2FF6598E8EDE8726E6E14AD88443114D2A0F5E7721E304F3870DA -3A179DDF65B9868CD84C7C04931F40D5D204C97B20DCBF1A70C241E59BFD7F14 -AF538FD16104DCAF03F4DDF05026D6741898DFC247E48A8F72E652DDF2DFD289 -E67F16AEC9D84B6C06F77B806CA6FBC7618BFBECD0D7A04EC3AE1D1DD06BEC5B -FA4D9F8920EBF2F4293C6D4E99083AA4A71A9DDFFDB07EEBDC552DACEC4DA24A -5BF23E630AC81E2CD533803E225BCB3C481B8D650A9858CF2B5219BAE1CDA01A -17B57E8C1032481E69247EA9A0C9EA41F6C0EA9B3F11170CA69C0842423F0455 -96EA848B8527A647DC9DACDB16C5D92B0081EB1CD77B99B47F56C2E249190BD3BE4306333F37487133DD3AD8E57F3092 -B0E9411274D799BE5989D52E74E00DE310CCA2BD47D7A8FA554D66BB04CD787A -D0D28476E3D8832975653D93F545C35278EC1F0B7AD70CA2F36EB476CC207937 -933195E37014619F997B73F5CF4C0110865A822CA8CB0ED1D977D49A1B06A37F -E790CAC2A26452BF941A9E1BABF0A85598EA1CC8F8CFED637C9B40D5E027B518 -49C1F179ABA5BD4F2C45257A33701730E9CC4728677EFF07808ABE31D3CE6FD5C805F43EA5ABB7261B220C82F0794092 +IV: AA FF DE 54 A1 BB F1 21 83 46 FE 2A 1E B7 3D 33 +95B8CD65BBAC95EACE67CA94F679B82877A921396D461ECB479722F8A369454A +61065C41297B9FF8F8168814F49A03D1FE7B4CB79DFFCBBF0402AAA6A2211E84 +A1557AC139188FF105D1081A4B688C5CA440FB5DA7F40901B541120AD08A544F +AF0A6056D7F0D97DAD6C16C4E63204E4B3B1C5A20AC82B983B516F4F718EE29F +6861BFAE46A1AADB1DB2D6DFAA7E39D21D5B3E46A41BD50F4F2828879EB328EF0A406F2B9C79A031AB361257E6D69756 +0DDB3DAC53678541981CC46C22CED245CBA314C9BBE1BA9383B8505B75AC5E40 +99AB5D9404934F2D257ED04D9F8CCEE06D00F38157B121AFD63101E4E5C08268 +5114A6C42B342C7D933A76F9052FF963C2047E85EA524497C21B4C35C38EF6E7 +88CA2A1907D94B972FF93DBB9B88CB576F3E1BB0FE8F85A5B2CCA7D44B00374D +349C4153FE7CA8AE044E9F75F77D9694304474CE3F127CF968662B5F78A7F421 +62AA02E20CA7E691EFC0B55CA41C9BDF889FB23868289284241CD31AA1A0E499AE2A770B6B5AB3170CDCCDB8A246D36C +97901B5EB76228ADF8E5073F1BAB1502878DEFF1C4EBF12A43D105556CB7E80F947A8BD7831666BD838C57CDF64A6F3F +B05959D210B500943A93BDFAF783D9DB215FC84503B152EAFBCFB5B6237E3888 +B393DE4489BCAFD5DB80592A12E329E18913E185D2042580048029A8C4C3A257 +B4B30492A5F0C3C763E2F43C02D1451A5B9CFB468CFE62BE85B1F56FF49DAB9A +CE5D57C0EE3D717FC717EB725970A9F25D211546EE7AC5C237950CEA323D85D4 +4E9028944813FD40A17AF6DF5A97E76179B48EE79265BBD38B07E3A270587A813DADB51B3367479AC5644F754B5613F8 +3B3C3000B9D1361711ECE3DB77C90A059576F738CB167679DA36DD3D128B27A1 +997023148148DE7B9CBA47D3FD48DEF73AA1715FF4BC1E7A1DBA6D52A0DCB2C0 +C8428D18E69FB92486434FCE470F1FF37D40507F27D824679C132A70D516530367277F02DDB5C464D03450FF6B425A24 +3701200DF5DA7235971FD95844056E74C7D61A8EB12A8772E04F52037C63D50B6229A7F905F3E6F84C565FCC7632870C +BB392A464CDC0D5D923AA9EF8ECC3C6F020D0AD82165462DF0DE7C5025AAAAAC +999C82209B30638506E5D708471676D2CBB4A432E5AF86ABD61179111EDAE636 +FDE2A452A6B47261338117EC20FC57731DA492562ECD21BBC61F098A5442CF20 +D923BABB5C4DFB48E3F763898B2796C7830D3EE9A91DF904AC2223A0F4736507 +0987DDAC695DD5E4607048DF1D4EF96599E17ED52F41785E676AA048AB7213FE +26CB3E6CFA10338A8DDD99BFF6957C53DEF435CB0FF977B71B5164ADFE11292A +097908FD07A0A093CA80E6FF59524707C1A11169D0CB6F8E4967D8DAA725FE7A +8C629E70A5CC6FCB039DFA1A6AC58CB7B7E92C85BDA66266AB49E6B1285FC7A6 +39A2052350CD446EDC1B9AD0C2DD51C78B2E5F3A76AAD0EC200F74B40ACD4AC5 +A1685CF8C4A5401F2CA0C8172CB5B4B5726C61CE68A72AE834B0A472CEB2F3DE +1F5ED5793DB381D1B501BA8A4DF3E74FB11FC1A922DDC8AE62E5BA8934C37EA8 +D80EF661BF36E2F6C179E253CE5BC3732684ACBC7C65E526A628442A2EBF8FAA +7785BF721F21E19A8CFBFBBB56BD76B96A4E8EF9F8A2344009B14AB385909598F834A5533B648DA7D62BD6D4314A43A5 +C8F6F943DE615B5827569B283577344C0455B3279C73634FC4E0E9A8088DF633 +FB4F4C786FC51BBDA679A212B4A05EF120AC62F7EBFFE8263BD50A4D9BC9C6E0 +16EEC35CF69BA86DB3BE999CDF9B39F5736F3727B2AA2C5AB9141A48F176D831 +AD1AD6DE813E7710DA3AF546D4F9EA085831E6B3FA17B64F1B8765F48134EA54 +345D743BC35B4A8614632ADD11E809C0D1E6C78F9469256B9A738DA0B648B2B8 +7C876CECAC839EBB4609C3996966C3EC454F51C8ABCC51097E405370C4B6F086 +0F857C031FD3047607647148C534F969567F207FF1691D8D06DCDF4C2514695D +EC0630EDC82241C1952F49B6B1B0C1A954A7DDD6BDB1326ACC54AD449D1BF985 +286EF9F7FD0D09F2604CCE867C52144CD0C4773A3D8183066C61B8BF9860AE7C +EA55424097A08722A66966E3177E09DE91AC65175E5C68CB47B6153E6585DF85 +D54FCDF9EA4BD1FE4F316DB6D5CE4A2675F2D0144772865EDC781FBA7DFD23E4 +7A2F5C5CA9F97FE9527BAA760E64B930C407A27DE036476737E6BDD9422F4056A5F1F414F12F0982109FD7C30E8CC1CB +06BAD9B4EEEEB1BCF8C97672D271534FE84D772282EE9642698788D3842D7641 +101C1B2DBD963E23777294C22E553D145D5B40838F91355CA86D571A0CEFF68F +1B148C2B502B3E0A5BD40858E019C513DD4CCAF2A114CBB29C59BFB018079285 +8DF4D07EC20FF873EA989ACEF4AF96E9787FE6E0F71965858B4186C3AF302A31 +2317DC8C098CD60F3467B3644A19CCE887339708820CD37F6F5277D6648F837512F70CE90E23D7339CDDE002BD8D83DB diff --git a/lib/subghz/protocols/keeloq.c b/lib/subghz/protocols/keeloq.c index ae6588e7a..eef1d0937 100644 --- a/lib/subghz/protocols/keeloq.c +++ b/lib/subghz/protocols/keeloq.c @@ -529,6 +529,24 @@ static uint8_t subghz_protocol_keeloq_check_remote_controller_selector( return 1; } break; + case KEELOQ_LEARNING_MAGIC_SERIAL_TYPE_2: + man = subghz_protocol_keeloq_common_magic_serial_type2_learning( + fix, manufacture_code->key); + decrypt = subghz_protocol_keeloq_common_decrypt(hop, man); + if(subghz_protocol_keeloq_check_decrypt(instance, decrypt, btn, end_serial)) { + *manufacture_name = furi_string_get_cstr(manufacture_code->name); + return 1; + } + break; + case KEELOQ_LEARNING_MAGIC_SERIAL_TYPE_3: + man = subghz_protocol_keeloq_common_magic_serial_type3_learning( + fix, manufacture_code->key); + decrypt = subghz_protocol_keeloq_common_decrypt(hop, man); + if(subghz_protocol_keeloq_check_decrypt(instance, decrypt, btn, end_serial)) { + *manufacture_name = furi_string_get_cstr(manufacture_code->name); + return 1; + } + break; case KEELOQ_LEARNING_UNKNOWN: // Simple Learning decrypt = subghz_protocol_keeloq_common_decrypt(hop, manufacture_code->key); diff --git a/lib/subghz/protocols/keeloq_common.c b/lib/subghz/protocols/keeloq_common.c index 6c9bc461e..ddbf1c917 100644 --- a/lib/subghz/protocols/keeloq_common.c +++ b/lib/subghz/protocols/keeloq_common.c @@ -94,6 +94,34 @@ inline uint64_t inline uint64_t subghz_protocol_keeloq_common_magic_serial_type1_learning(uint32_t data, uint64_t man) { - return man | ((uint64_t)data << 40) | + return (man & 0xFFFFFFFF) | ((uint64_t)data << 40) | ((uint64_t)(((data & 0xff) + ((data >> 8) & 0xFF)) & 0xFF) << 32); } + +/** Magic_serial_type2 Learning + * @param data - btn+serial number (32bit) + * @param man - magic man (64bit) + * @return manufacture for this serial number (64bit) + */ + +inline uint64_t + subghz_protocol_keeloq_common_magic_serial_type2_learning(uint32_t data, uint64_t man) { + uint8_t* p = (uint8_t*)&data; + uint8_t* m = (uint8_t*)&man; + m[7] = p[0]; + m[6] = p[1]; + m[5] = p[2]; + m[4] = p[3]; + return man; +} + +/** Magic_serial_type3 Learning + * @param data - serial number (24bit) + * @param man - magic man (64bit) + * @return manufacture for this serial number (64bit) + */ + +inline uint64_t + subghz_protocol_keeloq_common_magic_serial_type3_learning(uint32_t data, uint64_t man) { + return (man & 0xFFFFFFFFFF000000) | (data & 0xFFFFFF); +} diff --git a/lib/subghz/protocols/keeloq_common.h b/lib/subghz/protocols/keeloq_common.h index 448388f0a..df3d0dbf3 100644 --- a/lib/subghz/protocols/keeloq_common.h +++ b/lib/subghz/protocols/keeloq_common.h @@ -22,6 +22,8 @@ #define KEELOQ_LEARNING_SECURE 3u #define KEELOQ_LEARNING_MAGIC_XOR_TYPE_1 4u #define KEELOQ_LEARNING_MAGIC_SERIAL_TYPE_1 5u +#define KEELOQ_LEARNING_MAGIC_SERIAL_TYPE_2 6u +#define KEELOQ_LEARNING_MAGIC_SERIAL_TYPE_3 7u /** * Simple Learning Encrypt @@ -72,3 +74,19 @@ uint64_t subghz_protocol_keeloq_common_magic_xor_type1_learning(uint32_t data, u */ uint64_t subghz_protocol_keeloq_common_magic_serial_type1_learning(uint32_t data, uint64_t man); + +/** Magic_serial_type2 Learning + * @param data - btn+serial number (32bit) + * @param man - magic man (64bit) + * @return manufacture for this serial number (64bit) + */ + +uint64_t subghz_protocol_keeloq_common_magic_serial_type2_learning(uint32_t data, uint64_t man); + +/** Magic_serial_type3 Learning + * @param data - btn+serial number (32bit) + * @param man - magic man (64bit) + * @return manufacture for this serial number (64bit) + */ + +uint64_t subghz_protocol_keeloq_common_magic_serial_type3_learning(uint32_t data, uint64_t man); From 4b921803cbad19829d0b08e92bab89f29080ffd7 Mon Sep 17 00:00:00 2001 From: hedger Date: Fri, 28 Oct 2022 19:32:06 +0400 Subject: [PATCH 07/49] fbt: fixes for ufbt compat (#1940) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fbt: split sdk management code * scripts: fixed import handling * fbt: sdk: reformatted paths * scrips: dist: bundling libs as a build artifact * fbt: sdk: better path management * typo fix * fbt: sdk: minor path handling fixes * toolchain: fixed windows toolchain download Co-authored-by: あく --- firmware.scons | 17 -- scripts/fbt/sdk/__init__.py | 44 +++ scripts/fbt/{sdk.py => sdk/cache.py} | 280 +----------------- scripts/fbt/sdk/collector.py | 238 +++++++++++++++ scripts/fbt_tools/fbt_extapps.py | 2 +- scripts/fbt_tools/fbt_sdk.py | 49 ++- scripts/sconsdist.py | 66 +++-- .../toolchain/windows-toolchain-download.ps1 | 6 +- site_scons/extapps.scons | 2 +- site_scons/firmwareopts.scons | 20 +- 10 files changed, 383 insertions(+), 341 deletions(-) create mode 100644 scripts/fbt/sdk/__init__.py rename scripts/fbt/{sdk.py => sdk/cache.py} (52%) create mode 100644 scripts/fbt/sdk/collector.py diff --git a/firmware.scons b/firmware.scons index d501996b3..63a1aa3f7 100644 --- a/firmware.scons +++ b/firmware.scons @@ -178,23 +178,6 @@ sources.extend( ) ) - -fwenv.AppendUnique( - LINKFLAGS=[ - "-specs=nano.specs", - "-specs=nosys.specs", - "-Wl,--gc-sections", - "-Wl,--undefined=uxTopUsedPriority", - "-Wl,--wrap,_malloc_r", - "-Wl,--wrap,_free_r", - "-Wl,--wrap,_calloc_r", - "-Wl,--wrap,_realloc_r", - "-n", - "-Xlinker", - "-Map=${TARGET}.map", - ], -) - # Debug # print(fwenv.Dump()) diff --git a/scripts/fbt/sdk/__init__.py b/scripts/fbt/sdk/__init__.py new file mode 100644 index 000000000..27da5f7c8 --- /dev/null +++ b/scripts/fbt/sdk/__init__.py @@ -0,0 +1,44 @@ +from typing import Set, ClassVar +from dataclasses import dataclass, field + + +@dataclass(frozen=True) +class ApiEntryFunction: + name: str + returns: str + params: str + + csv_type: ClassVar[str] = "Function" + + def dictify(self): + return dict(name=self.name, type=self.returns, params=self.params) + + +@dataclass(frozen=True) +class ApiEntryVariable: + name: str + var_type: str + + csv_type: ClassVar[str] = "Variable" + + def dictify(self): + return dict(name=self.name, type=self.var_type, params=None) + + +@dataclass(frozen=True) +class ApiHeader: + name: str + + csv_type: ClassVar[str] = "Header" + + def dictify(self): + return dict(name=self.name, type=None, params=None) + + +@dataclass +class ApiEntries: + # These are sets, to avoid creating duplicates when we have multiple + # declarations with same signature + functions: Set[ApiEntryFunction] = field(default_factory=set) + variables: Set[ApiEntryVariable] = field(default_factory=set) + headers: Set[ApiHeader] = field(default_factory=set) diff --git a/scripts/fbt/sdk.py b/scripts/fbt/sdk/cache.py similarity index 52% rename from scripts/fbt/sdk.py rename to scripts/fbt/sdk/cache.py index 48f935de3..62d42798c 100644 --- a/scripts/fbt/sdk.py +++ b/scripts/fbt/sdk/cache.py @@ -4,284 +4,18 @@ import csv import operator from enum import Enum, auto -from typing import List, Set, ClassVar, Any -from dataclasses import dataclass, field +from typing import Set, ClassVar, Any +from dataclasses import dataclass from ansi.color import fg -from cxxheaderparser.parser import CxxParser - - -# 'Fixing' complaints about typedefs -CxxParser._fundamentals.discard("wchar_t") - -from cxxheaderparser.types import ( - EnumDecl, - Field, - ForwardDecl, - FriendDecl, - Function, - Method, - Typedef, - UsingAlias, - UsingDecl, - Variable, - Pointer, - Type, - PQName, - NameSpecifier, - FundamentalSpecifier, - Parameter, - Array, - Value, - Token, - FunctionType, +from . import ( + ApiEntries, + ApiEntryFunction, + ApiEntryVariable, + ApiHeader, ) -from cxxheaderparser.parserstate import ( - State, - EmptyBlockState, - ClassBlockState, - ExternBlockState, - NamespaceBlockState, -) - - -@dataclass(frozen=True) -class ApiEntryFunction: - name: str - returns: str - params: str - - csv_type: ClassVar[str] = "Function" - - def dictify(self): - return dict(name=self.name, type=self.returns, params=self.params) - - -@dataclass(frozen=True) -class ApiEntryVariable: - name: str - var_type: str - - csv_type: ClassVar[str] = "Variable" - - def dictify(self): - return dict(name=self.name, type=self.var_type, params=None) - - -@dataclass(frozen=True) -class ApiHeader: - name: str - - csv_type: ClassVar[str] = "Header" - - def dictify(self): - return dict(name=self.name, type=None, params=None) - - -@dataclass -class ApiEntries: - # These are sets, to avoid creating duplicates when we have multiple - # declarations with same signature - functions: Set[ApiEntryFunction] = field(default_factory=set) - variables: Set[ApiEntryVariable] = field(default_factory=set) - headers: Set[ApiHeader] = field(default_factory=set) - - -class SymbolManager: - def __init__(self): - self.api = ApiEntries() - self.name_hashes = set() - - # Calculate hash of name and raise exception if it already is in the set - def _name_check(self, name: str): - name_hash = gnu_sym_hash(name) - if name_hash in self.name_hashes: - raise Exception(f"Hash collision on {name}") - self.name_hashes.add(name_hash) - - def add_function(self, function_def: ApiEntryFunction): - if function_def in self.api.functions: - return - self._name_check(function_def.name) - self.api.functions.add(function_def) - - def add_variable(self, variable_def: ApiEntryVariable): - if variable_def in self.api.variables: - return - self._name_check(variable_def.name) - self.api.variables.add(variable_def) - - def add_header(self, header: str): - self.api.headers.add(ApiHeader(header)) - - -def gnu_sym_hash(name: str): - h = 0x1505 - for c in name: - h = (h << 5) + h + ord(c) - return str(hex(h))[-8:] - - -class SdkCollector: - def __init__(self): - self.symbol_manager = SymbolManager() - - def add_header_to_sdk(self, header: str): - self.symbol_manager.add_header(header) - - def process_source_file_for_sdk(self, file_path: str): - visitor = SdkCxxVisitor(self.symbol_manager) - with open(file_path, "rt") as f: - content = f.read() - parser = CxxParser(file_path, content, visitor, None) - parser.parse() - - def get_api(self): - return self.symbol_manager.api - - -def stringify_array_dimension(size_descr): - if not size_descr: - return "" - return stringify_descr(size_descr) - - -def stringify_array_descr(type_descr): - assert isinstance(type_descr, Array) - return ( - stringify_descr(type_descr.array_of), - stringify_array_dimension(type_descr.size), - ) - - -def stringify_descr(type_descr): - if isinstance(type_descr, (NameSpecifier, FundamentalSpecifier)): - return type_descr.name - elif isinstance(type_descr, PQName): - return "::".join(map(stringify_descr, type_descr.segments)) - elif isinstance(type_descr, Pointer): - # Hack - if isinstance(type_descr.ptr_to, FunctionType): - return stringify_descr(type_descr.ptr_to) - return f"{stringify_descr(type_descr.ptr_to)}*" - elif isinstance(type_descr, Type): - return ( - f"{'const ' if type_descr.const else ''}" - f"{'volatile ' if type_descr.volatile else ''}" - f"{stringify_descr(type_descr.typename)}" - ) - elif isinstance(type_descr, Parameter): - return stringify_descr(type_descr.type) - elif isinstance(type_descr, Array): - # Hack for 2d arrays - if isinstance(type_descr.array_of, Array): - argtype, dimension = stringify_array_descr(type_descr.array_of) - return ( - f"{argtype}[{stringify_array_dimension(type_descr.size)}][{dimension}]" - ) - return f"{stringify_descr(type_descr.array_of)}[{stringify_array_dimension(type_descr.size)}]" - elif isinstance(type_descr, Value): - return " ".join(map(stringify_descr, type_descr.tokens)) - elif isinstance(type_descr, FunctionType): - return f"{stringify_descr(type_descr.return_type)} (*)({', '.join(map(stringify_descr, type_descr.parameters))})" - elif isinstance(type_descr, Token): - return type_descr.value - elif type_descr is None: - return "" - else: - raise Exception("unsupported type_descr: %s" % type_descr) - - -class SdkCxxVisitor: - def __init__(self, symbol_manager: SymbolManager): - self.api = symbol_manager - - def on_variable(self, state: State, v: Variable) -> None: - if not v.extern: - return - - self.api.add_variable( - ApiEntryVariable( - stringify_descr(v.name), - stringify_descr(v.type), - ) - ) - - def on_function(self, state: State, fn: Function) -> None: - if fn.inline or fn.has_body: - return - - self.api.add_function( - ApiEntryFunction( - stringify_descr(fn.name), - stringify_descr(fn.return_type), - ", ".join(map(stringify_descr, fn.parameters)) - + (", ..." if fn.vararg else ""), - ) - ) - - def on_define(self, state: State, content: str) -> None: - pass - - def on_pragma(self, state: State, content: str) -> None: - pass - - def on_include(self, state: State, filename: str) -> None: - pass - - def on_empty_block_start(self, state: EmptyBlockState) -> None: - pass - - def on_empty_block_end(self, state: EmptyBlockState) -> None: - pass - - def on_extern_block_start(self, state: ExternBlockState) -> None: - pass - - def on_extern_block_end(self, state: ExternBlockState) -> None: - pass - - def on_namespace_start(self, state: NamespaceBlockState) -> None: - pass - - def on_namespace_end(self, state: NamespaceBlockState) -> None: - pass - - def on_forward_decl(self, state: State, fdecl: ForwardDecl) -> None: - pass - - def on_typedef(self, state: State, typedef: Typedef) -> None: - pass - - def on_using_namespace(self, state: State, namespace: List[str]) -> None: - pass - - def on_using_alias(self, state: State, using: UsingAlias) -> None: - pass - - def on_using_declaration(self, state: State, using: UsingDecl) -> None: - pass - - def on_enum(self, state: State, enum: EnumDecl) -> None: - pass - - def on_class_start(self, state: ClassBlockState) -> None: - pass - - def on_class_field(self, state: State, f: Field) -> None: - pass - - def on_class_method(self, state: ClassBlockState, method: Method) -> None: - pass - - def on_class_friend(self, state: ClassBlockState, friend: FriendDecl) -> None: - pass - - def on_class_end(self, state: ClassBlockState) -> None: - pass - @dataclass(frozen=True) class SdkVersion: diff --git a/scripts/fbt/sdk/collector.py b/scripts/fbt/sdk/collector.py new file mode 100644 index 000000000..578a8c7a6 --- /dev/null +++ b/scripts/fbt/sdk/collector.py @@ -0,0 +1,238 @@ +from typing import List + +from cxxheaderparser.parser import CxxParser +from . import ( + ApiEntries, + ApiEntryFunction, + ApiEntryVariable, + ApiHeader, +) + + +# 'Fixing' complaints about typedefs +CxxParser._fundamentals.discard("wchar_t") + +from cxxheaderparser.types import ( + EnumDecl, + Field, + ForwardDecl, + FriendDecl, + Function, + Method, + Typedef, + UsingAlias, + UsingDecl, + Variable, + Pointer, + Type, + PQName, + NameSpecifier, + FundamentalSpecifier, + Parameter, + Array, + Value, + Token, + FunctionType, +) + +from cxxheaderparser.parserstate import ( + State, + EmptyBlockState, + ClassBlockState, + ExternBlockState, + NamespaceBlockState, +) + + +class SymbolManager: + def __init__(self): + self.api = ApiEntries() + self.name_hashes = set() + + # Calculate hash of name and raise exception if it already is in the set + def _name_check(self, name: str): + name_hash = gnu_sym_hash(name) + if name_hash in self.name_hashes: + raise Exception(f"Hash collision on {name}") + self.name_hashes.add(name_hash) + + def add_function(self, function_def: ApiEntryFunction): + if function_def in self.api.functions: + return + self._name_check(function_def.name) + self.api.functions.add(function_def) + + def add_variable(self, variable_def: ApiEntryVariable): + if variable_def in self.api.variables: + return + self._name_check(variable_def.name) + self.api.variables.add(variable_def) + + def add_header(self, header: str): + self.api.headers.add(ApiHeader(header)) + + +def gnu_sym_hash(name: str): + h = 0x1505 + for c in name: + h = (h << 5) + h + ord(c) + return str(hex(h))[-8:] + + +class SdkCollector: + def __init__(self): + self.symbol_manager = SymbolManager() + + def add_header_to_sdk(self, header: str): + self.symbol_manager.add_header(header) + + def process_source_file_for_sdk(self, file_path: str): + visitor = SdkCxxVisitor(self.symbol_manager) + with open(file_path, "rt") as f: + content = f.read() + parser = CxxParser(file_path, content, visitor, None) + parser.parse() + + def get_api(self): + return self.symbol_manager.api + + +def stringify_array_dimension(size_descr): + if not size_descr: + return "" + return stringify_descr(size_descr) + + +def stringify_array_descr(type_descr): + assert isinstance(type_descr, Array) + return ( + stringify_descr(type_descr.array_of), + stringify_array_dimension(type_descr.size), + ) + + +def stringify_descr(type_descr): + if isinstance(type_descr, (NameSpecifier, FundamentalSpecifier)): + return type_descr.name + elif isinstance(type_descr, PQName): + return "::".join(map(stringify_descr, type_descr.segments)) + elif isinstance(type_descr, Pointer): + # Hack + if isinstance(type_descr.ptr_to, FunctionType): + return stringify_descr(type_descr.ptr_to) + return f"{stringify_descr(type_descr.ptr_to)}*" + elif isinstance(type_descr, Type): + return ( + f"{'const ' if type_descr.const else ''}" + f"{'volatile ' if type_descr.volatile else ''}" + f"{stringify_descr(type_descr.typename)}" + ) + elif isinstance(type_descr, Parameter): + return stringify_descr(type_descr.type) + elif isinstance(type_descr, Array): + # Hack for 2d arrays + if isinstance(type_descr.array_of, Array): + argtype, dimension = stringify_array_descr(type_descr.array_of) + return ( + f"{argtype}[{stringify_array_dimension(type_descr.size)}][{dimension}]" + ) + return f"{stringify_descr(type_descr.array_of)}[{stringify_array_dimension(type_descr.size)}]" + elif isinstance(type_descr, Value): + return " ".join(map(stringify_descr, type_descr.tokens)) + elif isinstance(type_descr, FunctionType): + return f"{stringify_descr(type_descr.return_type)} (*)({', '.join(map(stringify_descr, type_descr.parameters))})" + elif isinstance(type_descr, Token): + return type_descr.value + elif type_descr is None: + return "" + else: + raise Exception("unsupported type_descr: %s" % type_descr) + + +class SdkCxxVisitor: + def __init__(self, symbol_manager: SymbolManager): + self.api = symbol_manager + + def on_variable(self, state: State, v: Variable) -> None: + if not v.extern: + return + + self.api.add_variable( + ApiEntryVariable( + stringify_descr(v.name), + stringify_descr(v.type), + ) + ) + + def on_function(self, state: State, fn: Function) -> None: + if fn.inline or fn.has_body: + return + + self.api.add_function( + ApiEntryFunction( + stringify_descr(fn.name), + stringify_descr(fn.return_type), + ", ".join(map(stringify_descr, fn.parameters)) + + (", ..." if fn.vararg else ""), + ) + ) + + def on_define(self, state: State, content: str) -> None: + pass + + def on_pragma(self, state: State, content: str) -> None: + pass + + def on_include(self, state: State, filename: str) -> None: + pass + + def on_empty_block_start(self, state: EmptyBlockState) -> None: + pass + + def on_empty_block_end(self, state: EmptyBlockState) -> None: + pass + + def on_extern_block_start(self, state: ExternBlockState) -> None: + pass + + def on_extern_block_end(self, state: ExternBlockState) -> None: + pass + + def on_namespace_start(self, state: NamespaceBlockState) -> None: + pass + + def on_namespace_end(self, state: NamespaceBlockState) -> None: + pass + + def on_forward_decl(self, state: State, fdecl: ForwardDecl) -> None: + pass + + def on_typedef(self, state: State, typedef: Typedef) -> None: + pass + + def on_using_namespace(self, state: State, namespace: List[str]) -> None: + pass + + def on_using_alias(self, state: State, using: UsingAlias) -> None: + pass + + def on_using_declaration(self, state: State, using: UsingDecl) -> None: + pass + + def on_enum(self, state: State, enum: EnumDecl) -> None: + pass + + def on_class_start(self, state: ClassBlockState) -> None: + pass + + def on_class_field(self, state: State, f: Field) -> None: + pass + + def on_class_method(self, state: ClassBlockState, method: Method) -> None: + pass + + def on_class_friend(self, state: ClassBlockState, friend: FriendDecl) -> None: + pass + + def on_class_end(self, state: ClassBlockState) -> None: + pass diff --git a/scripts/fbt_tools/fbt_extapps.py b/scripts/fbt_tools/fbt_extapps.py index 5a5dab572..38c943cc5 100644 --- a/scripts/fbt_tools/fbt_extapps.py +++ b/scripts/fbt_tools/fbt_extapps.py @@ -8,7 +8,7 @@ import os import pathlib from fbt.elfmanifest import assemble_manifest_data from fbt.appmanifest import FlipperApplication, FlipperManifestException -from fbt.sdk import SdkCache +from fbt.sdk.cache import SdkCache import itertools from ansi.color import fg diff --git a/scripts/fbt_tools/fbt_sdk.py b/scripts/fbt_tools/fbt_sdk.py index 0b6e22de5..f1f55bdb8 100644 --- a/scripts/fbt_tools/fbt_sdk.py +++ b/scripts/fbt_tools/fbt_sdk.py @@ -4,7 +4,7 @@ from SCons.Action import Action from SCons.Errors import UserError # from SCons.Scanner import C -from SCons.Script import Mkdir, Copy, Delete, Entry +from SCons.Script import Entry from SCons.Util import LogicalLines import os.path @@ -12,7 +12,8 @@ import posixpath import pathlib import json -from fbt.sdk import SdkCollector, SdkCache +from fbt.sdk.collector import SdkCollector +from fbt.sdk.cache import SdkCache def ProcessSdkDepends(env, filename): @@ -49,15 +50,19 @@ def prebuild_sdk_create_origin_file(target, source, env): class SdkMeta: - def __init__(self, env): + def __init__(self, env, tree_builder: "SdkTreeBuilder"): self.env = env + self.treebuilder = tree_builder def save_to(self, json_manifest_path: str): meta_contents = { - "sdk_symbols": self.env["SDK_DEFINITION"].name, + "sdk_symbols": self.treebuilder.build_sdk_file_path( + self.env["SDK_DEFINITION"].path + ), "cc_args": self._wrap_scons_vars("$CCFLAGS $_CCCOMCOM"), "cpp_args": self._wrap_scons_vars("$CXXFLAGS $CCFLAGS $_CCCOMCOM"), "linker_args": self._wrap_scons_vars("$LINKFLAGS"), + "linker_script": self.env.subst("${LINKER_SCRIPT_PATH}"), } with open(json_manifest_path, "wt") as f: json.dump(meta_contents, f, indent=4) @@ -68,6 +73,8 @@ class SdkMeta: class SdkTreeBuilder: + SDK_DIR_SUBST = "SDK_ROOT_DIR" + def __init__(self, env, target, source) -> None: self.env = env self.target = target @@ -88,6 +95,8 @@ class SdkTreeBuilder: self.header_depends = list( filter(lambda fname: fname.endswith(".h"), depends.split()), ) + self.header_depends.append(self.env.subst("${LINKER_SCRIPT_PATH}")) + self.header_depends.append(self.env.subst("${SDK_DEFINITION}")) self.header_dirs = sorted( set(map(os.path.normpath, map(os.path.dirname, self.header_depends))) ) @@ -102,17 +111,33 @@ class SdkTreeBuilder: ) sdk_dirs = ", ".join(f"'{dir}'" for dir in self.header_dirs) - for dir in full_fw_paths: - if dir in sdk_dirs: - filtered_paths.append( - posixpath.normpath(posixpath.join(self.target_sdk_dir_name, dir)) - ) + filtered_paths.extend( + map( + self.build_sdk_file_path, + filter(lambda path: path in sdk_dirs, full_fw_paths), + ) + ) sdk_env = self.env.Clone() - sdk_env.Replace(CPPPATH=filtered_paths) - meta = SdkMeta(sdk_env) + sdk_env.Replace( + CPPPATH=filtered_paths, + LINKER_SCRIPT=self.env.subst("${APP_LINKER_SCRIPT}"), + ORIG_LINKER_SCRIPT_PATH=self.env["LINKER_SCRIPT_PATH"], + LINKER_SCRIPT_PATH=self.build_sdk_file_path("${ORIG_LINKER_SCRIPT_PATH}"), + ) + + meta = SdkMeta(sdk_env, self) meta.save_to(self.target[0].path) + def build_sdk_file_path(self, orig_path: str) -> str: + return posixpath.normpath( + posixpath.join( + self.SDK_DIR_SUBST, + self.target_sdk_dir_name, + orig_path, + ) + ).replace("\\", "/") + def emitter(self, target, source, env): target_folder = target[0] target = [target_folder.File("sdk.opts")] @@ -128,8 +153,6 @@ class SdkTreeBuilder: for sdkdir in dirs_to_create: os.makedirs(sdkdir, exist_ok=True) - shutil.copy2(self.env["SDK_DEFINITION"].path, self.sdk_root_dir.path) - for header in self.header_depends: shutil.copy2(header, self.sdk_deploy_dir.File(header).path) diff --git a/scripts/sconsdist.py b/scripts/sconsdist.py index 4c0427894..7636c87bb 100644 --- a/scripts/sconsdist.py +++ b/scripts/sconsdist.py @@ -48,48 +48,52 @@ class Main(App): ) self.parser_copy.set_defaults(func=self.copy) - def get_project_filename(self, project, filetype): + def get_project_file_name(self, project: ProjectDir, filetype: str) -> str: # Temporary fix project_name = project.project - if project_name == "firmware": - if filetype == "zip": - project_name = "sdk" - elif filetype != "elf": - project_name = "full" + if project_name == "firmware" and filetype != "elf": + project_name = "full" - return f"{self.DIST_FILE_PREFIX}{self.target}-{project_name}-{self.args.suffix}.{filetype}" + return self.get_dist_file_name(project_name, filetype) - def get_dist_filepath(self, filename): + def get_dist_file_name(self, dist_artifact_type: str, filetype: str) -> str: + return f"{self.DIST_FILE_PREFIX}{self.target}-{dist_artifact_type}-{self.args.suffix}.{filetype}" + + def get_dist_file_path(self, filename: str) -> str: return join(self.output_dir_path, filename) - def copy_single_project(self, project): + def copy_single_project(self, project: ProjectDir) -> None: obj_directory = join("build", project.dir) for filetype in ("elf", "bin", "dfu", "json"): if exists(src_file := join(obj_directory, f"{project.project}.{filetype}")): shutil.copyfile( src_file, - self.get_dist_filepath( - self.get_project_filename(project, filetype) + self.get_dist_file_path( + self.get_project_file_name(project, filetype) ), ) - if exists(sdk_folder := join(obj_directory, "sdk")): - with zipfile.ZipFile( - self.get_dist_filepath(self.get_project_filename(project, "zip")), - "w", - zipfile.ZIP_DEFLATED, - ) as zf: - for root, dirs, files in walk(sdk_folder): - for file in files: - zf.write( - join(root, file), - relpath( - join(root, file), - sdk_folder, - ), - ) + for foldertype in ("sdk", "lib"): + if exists(sdk_folder := join(obj_directory, foldertype)): + self.package_zip(foldertype, sdk_folder) - def copy(self): + def package_zip(self, foldertype, sdk_folder): + with zipfile.ZipFile( + self.get_dist_file_path(self.get_dist_file_name(foldertype, "zip")), + "w", + zipfile.ZIP_DEFLATED, + ) as zf: + for root, _, files in walk(sdk_folder): + for file in files: + zf.write( + join(root, file), + relpath( + join(root, file), + sdk_folder, + ), + ) + + def copy(self) -> int: self.projects = dict( map( lambda pd: (pd.project, pd), @@ -144,12 +148,12 @@ class Main(App): "-t", self.target, "--dfu", - self.get_dist_filepath( - self.get_project_filename(self.projects["firmware"], "dfu") + self.get_dist_file_path( + self.get_project_file_name(self.projects["firmware"], "dfu") ), "--stage", - self.get_dist_filepath( - self.get_project_filename(self.projects["updater"], "bin") + self.get_dist_file_path( + self.get_project_file_name(self.projects["updater"], "bin") ), ] if self.args.resources: diff --git a/scripts/toolchain/windows-toolchain-download.ps1 b/scripts/toolchain/windows-toolchain-download.ps1 index 370f1a14a..aaed89856 100644 --- a/scripts/toolchain/windows-toolchain-download.ps1 +++ b/scripts/toolchain/windows-toolchain-download.ps1 @@ -23,12 +23,12 @@ if (!(Test-Path -LiteralPath "$repo_root\toolchain")) { New-Item "$repo_root\toolchain" -ItemType Directory } -Write-Host -NoNewline "Unziping Windows toolchain.." +Write-Host -NoNewline "Extracting Windows toolchain.." Add-Type -Assembly "System.IO.Compression.Filesystem" -[System.IO.Compression.ZipFile]::ExtractToDirectory("$toolchain_zip", "$repo_root\") +[System.IO.Compression.ZipFile]::ExtractToDirectory("$repo_root\$toolchain_zip", "$repo_root\") Move-Item -Path "$repo_root\$toolchain_dir" -Destination "$repo_root\toolchain\x86_64-windows" Write-Host "done!" -Write-Host -NoNewline "Clearing temporary files.." +Write-Host -NoNewline "Cleaning up temporary files.." Remove-Item -LiteralPath "$repo_root\$toolchain_zip" -Force Write-Host "done!" diff --git a/site_scons/extapps.scons b/site_scons/extapps.scons index ee317be3b..90d228e58 100644 --- a/site_scons/extapps.scons +++ b/site_scons/extapps.scons @@ -21,7 +21,7 @@ appenv = ENV.Clone( ) appenv.Replace( - LINKER_SCRIPT="application_ext", + LINKER_SCRIPT=appenv.subst("$APP_LINKER_SCRIPT"), ) appenv.AppendUnique( diff --git a/site_scons/firmwareopts.scons b/site_scons/firmwareopts.scons index f04b55cdd..9f707b4d8 100644 --- a/site_scons/firmwareopts.scons +++ b/site_scons/firmwareopts.scons @@ -32,12 +32,27 @@ else: ], ) -ENV.Append( +ENV.AppendUnique( LINKFLAGS=[ - "-Tfirmware/targets/f${TARGET_HW}/${LINKER_SCRIPT}.ld", + "-specs=nano.specs", + "-specs=nosys.specs", + "-Wl,--gc-sections", + "-Wl,--undefined=uxTopUsedPriority", + "-Wl,--wrap,_malloc_r", + "-Wl,--wrap,_free_r", + "-Wl,--wrap,_calloc_r", + "-Wl,--wrap,_realloc_r", + "-n", + "-Xlinker", + "-Map=${TARGET}.map", + "-T${LINKER_SCRIPT_PATH}", ], ) +ENV.SetDefault( + LINKER_SCRIPT_PATH="firmware/targets/f${TARGET_HW}/${LINKER_SCRIPT}.ld", +) + if ENV["FIRMWARE_BUILD_CFG"] == "updater": ENV.Append( IMAGE_BASE_ADDRESS="0x20000000", @@ -47,4 +62,5 @@ else: ENV.Append( IMAGE_BASE_ADDRESS="0x8000000", LINKER_SCRIPT="stm32wb55xx_flash", + APP_LINKER_SCRIPT="application_ext", ) From 09b622d4ae01ef6d3ffcfd0eefabfa5cf890ffdd Mon Sep 17 00:00:00 2001 From: Konstantin Volkov <72250702+doomwastaken@users.noreply.github.com> Date: Fri, 28 Oct 2022 18:45:22 +0300 Subject: [PATCH 08/49] UnitTests: removed all continue-on-error lines (#1946) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * removed all continue-on-error lines * Github: add assets deployment after format Co-authored-by: Konstantin Volkov Co-authored-by: あく --- .github/workflows/unit_tests.yml | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index 8c5bac2a2..b5bf10004 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -24,33 +24,29 @@ jobs: - name: 'Compile unit tests firmware' id: compile - continue-on-error: true run: | FBT_TOOLCHAIN_PATH=/opt ./fbt flash OPENOCD_ADAPTER_SERIAL=2A0906016415303030303032 FIRMWARE_APP_SET=unit_tests FORCE=1 - name: 'Wait for flipper to finish updating' id: connect if: steps.compile.outcome == 'success' - continue-on-error: true run: | python3 ./scripts/testing/await_flipper.py ${{steps.device.outputs.flipper}} - name: 'Format flipper SD card' id: format if: steps.connect.outcome == 'success' - continue-on-error: true run: | ./scripts/storage.py -p ${{steps.device.outputs.flipper}} format_ext - - name: 'Copy unit tests to flipper' + - name: 'Copy assets and unit tests data to flipper' id: copy if: steps.format.outcome == 'success' - continue-on-error: true run: | + ./scripts/storage.py -p ${{steps.device.outputs.flipper}} send assets/resources /ext ./scripts/storage.py -p ${{steps.device.outputs.flipper}} send assets/unit_tests /ext/unit_tests - name: 'Run units and validate results' if: steps.copy.outcome == 'success' - continue-on-error: true run: | python3 ./scripts/testing/units.py ${{steps.device.outputs.flipper}} From 93a6e17ce57222fa7b92a930dd4f9aaee3a146bf Mon Sep 17 00:00:00 2001 From: gornekich Date: Fri, 28 Oct 2022 20:10:16 +0400 Subject: [PATCH 09/49] [FL-2933] Mf Classic initial write, update, detect reader (#1941) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * nfc: introduce nfc write * nfc: add write logic * nfc worker: add write state * nfc: add mfc update logic * nfc: add update success logic * nfc: add custom card for detect reader * nfc: update write logic * nfc: add halt command, add notifications * nfc: add write fail scene * nfc: fixes and clean up * nfc: fix navigation ad notifications * nfc: fix detect reader nfc data setter Co-authored-by: あく --- .../main/nfc/scenes/nfc_scene_config.h | 6 + .../main/nfc/scenes/nfc_scene_detect_reader.c | 5 + .../nfc/scenes/nfc_scene_mf_classic_update.c | 98 +++++++ .../nfc_scene_mf_classic_update_success.c | 44 +++ .../nfc/scenes/nfc_scene_mf_classic_write.c | 92 ++++++ .../scenes/nfc_scene_mf_classic_write_fail.c | 58 ++++ .../nfc_scene_mf_classic_write_success.c | 44 +++ .../scenes/nfc_scene_mf_classic_wrong_card.c | 53 ++++ .../main/nfc/scenes/nfc_scene_saved_menu.c | 34 +++ .../main/nfc/scenes/nfc_scene_start.c | 1 + applications/main/nfc/views/detect_reader.c | 36 +++ applications/main/nfc/views/detect_reader.h | 2 + firmware/targets/f7/furi_hal/furi_hal_nfc.c | 10 +- lib/nfc/helpers/reader_analyzer.c | 9 +- lib/nfc/helpers/reader_analyzer.h | 2 + lib/nfc/nfc_device.c | 7 + lib/nfc/nfc_worker.c | 150 +++++++++- lib/nfc/nfc_worker.h | 7 +- lib/nfc/nfc_worker_i.h | 4 + lib/nfc/protocols/crypto1.c | 52 ++++ lib/nfc/protocols/crypto1.h | 14 + lib/nfc/protocols/mifare_classic.c | 271 ++++++++++++------ lib/nfc/protocols/mifare_classic.h | 43 +++ 23 files changed, 949 insertions(+), 93 deletions(-) create mode 100644 applications/main/nfc/scenes/nfc_scene_mf_classic_update.c create mode 100644 applications/main/nfc/scenes/nfc_scene_mf_classic_update_success.c create mode 100644 applications/main/nfc/scenes/nfc_scene_mf_classic_write.c create mode 100644 applications/main/nfc/scenes/nfc_scene_mf_classic_write_fail.c create mode 100644 applications/main/nfc/scenes/nfc_scene_mf_classic_write_success.c create mode 100644 applications/main/nfc/scenes/nfc_scene_mf_classic_wrong_card.c diff --git a/applications/main/nfc/scenes/nfc_scene_config.h b/applications/main/nfc/scenes/nfc_scene_config.h index a25850c84..9b922add2 100644 --- a/applications/main/nfc/scenes/nfc_scene_config.h +++ b/applications/main/nfc/scenes/nfc_scene_config.h @@ -36,6 +36,12 @@ ADD_SCENE(nfc, mf_classic_keys_list, MfClassicKeysList) ADD_SCENE(nfc, mf_classic_keys_delete, MfClassicKeysDelete) ADD_SCENE(nfc, mf_classic_keys_warn_duplicate, MfClassicKeysWarnDuplicate) ADD_SCENE(nfc, mf_classic_dict_attack, MfClassicDictAttack) +ADD_SCENE(nfc, mf_classic_write, MfClassicWrite) +ADD_SCENE(nfc, mf_classic_write_success, MfClassicWriteSuccess) +ADD_SCENE(nfc, mf_classic_write_fail, MfClassicWriteFail) +ADD_SCENE(nfc, mf_classic_update, MfClassicUpdate) +ADD_SCENE(nfc, mf_classic_update_success, MfClassicUpdateSuccess) +ADD_SCENE(nfc, mf_classic_wrong_card, MfClassicWrongCard) ADD_SCENE(nfc, emv_read_success, EmvReadSuccess) ADD_SCENE(nfc, emv_menu, EmvMenu) ADD_SCENE(nfc, emulate_apdu_sequence, EmulateApduSequence) diff --git a/applications/main/nfc/scenes/nfc_scene_detect_reader.c b/applications/main/nfc/scenes/nfc_scene_detect_reader.c index abf1437d2..745946157 100644 --- a/applications/main/nfc/scenes/nfc_scene_detect_reader.c +++ b/applications/main/nfc/scenes/nfc_scene_detect_reader.c @@ -28,6 +28,11 @@ void nfc_scene_detect_reader_on_enter(void* context) { detect_reader_set_callback(nfc->detect_reader, nfc_scene_detect_reader_callback, nfc); detect_reader_set_nonces_max(nfc->detect_reader, NFC_SCENE_DETECT_READER_PAIR_NONCES_MAX); + NfcDeviceData* dev_data = &nfc->dev->dev_data; + if(dev_data->nfc_data.uid_len) { + detect_reader_set_uid( + nfc->detect_reader, dev_data->nfc_data.uid, dev_data->nfc_data.uid_len); + } // Store number of collected nonces in scene state scene_manager_set_scene_state(nfc->scene_manager, NfcSceneDetectReader, 0); diff --git a/applications/main/nfc/scenes/nfc_scene_mf_classic_update.c b/applications/main/nfc/scenes/nfc_scene_mf_classic_update.c new file mode 100644 index 000000000..dd3a6f7d5 --- /dev/null +++ b/applications/main/nfc/scenes/nfc_scene_mf_classic_update.c @@ -0,0 +1,98 @@ +#include "../nfc_i.h" +#include + +enum { + NfcSceneMfClassicUpdateStateCardSearch, + NfcSceneMfClassicUpdateStateCardFound, +}; + +bool nfc_mf_classic_update_worker_callback(NfcWorkerEvent event, void* context) { + furi_assert(context); + + Nfc* nfc = context; + view_dispatcher_send_custom_event(nfc->view_dispatcher, event); + + return true; +} + +static void nfc_scene_mf_classic_update_setup_view(Nfc* nfc) { + Popup* popup = nfc->popup; + popup_reset(popup); + uint32_t state = scene_manager_get_scene_state(nfc->scene_manager, NfcSceneMfClassicUpdate); + + if(state == NfcSceneMfClassicUpdateStateCardSearch) { + popup_set_text( + nfc->popup, "Apply the initial\ncard only", 128, 32, AlignRight, AlignCenter); + popup_set_icon(nfc->popup, 0, 8, &I_NFC_manual_60x50); + } else { + popup_set_header(popup, "Updating\nDon't move...", 52, 32, AlignLeft, AlignCenter); + popup_set_icon(popup, 12, 23, &A_Loading_24); + } + + view_dispatcher_switch_to_view(nfc->view_dispatcher, NfcViewPopup); +} + +void nfc_scene_mf_classic_update_on_enter(void* context) { + Nfc* nfc = context; + DOLPHIN_DEED(DolphinDeedNfcEmulate); + + scene_manager_set_scene_state( + nfc->scene_manager, NfcSceneMfClassicUpdate, NfcSceneMfClassicUpdateStateCardSearch); + nfc_scene_mf_classic_update_setup_view(nfc); + + // Setup and start worker + nfc_worker_start( + nfc->worker, + NfcWorkerStateMfClassicUpdate, + &nfc->dev->dev_data, + nfc_mf_classic_update_worker_callback, + nfc); + nfc_blink_emulate_start(nfc); +} + +bool nfc_scene_mf_classic_update_on_event(void* context, SceneManagerEvent event) { + Nfc* nfc = context; + bool consumed = false; + + if(event.type == SceneManagerEventTypeCustom) { + if(event.event == NfcWorkerEventSuccess) { + nfc_worker_stop(nfc->worker); + if(nfc_device_save_shadow(nfc->dev, nfc->dev->dev_name)) { + scene_manager_next_scene(nfc->scene_manager, NfcSceneMfClassicUpdateSuccess); + } else { + scene_manager_next_scene(nfc->scene_manager, NfcSceneMfClassicWrongCard); + } + consumed = true; + } else if(event.event == NfcWorkerEventWrongCard) { + nfc_worker_stop(nfc->worker); + scene_manager_next_scene(nfc->scene_manager, NfcSceneMfClassicWrongCard); + consumed = true; + } else if(event.event == NfcWorkerEventCardDetected) { + scene_manager_set_scene_state( + nfc->scene_manager, + NfcSceneMfClassicUpdate, + NfcSceneMfClassicUpdateStateCardFound); + nfc_scene_mf_classic_update_setup_view(nfc); + consumed = true; + } else if(event.event == NfcWorkerEventNoCardDetected) { + scene_manager_set_scene_state( + nfc->scene_manager, + NfcSceneMfClassicUpdate, + NfcSceneMfClassicUpdateStateCardSearch); + nfc_scene_mf_classic_update_setup_view(nfc); + consumed = true; + } + } + return consumed; +} + +void nfc_scene_mf_classic_update_on_exit(void* context) { + Nfc* nfc = context; + nfc_worker_stop(nfc->worker); + scene_manager_set_scene_state( + nfc->scene_manager, NfcSceneMfClassicUpdate, NfcSceneMfClassicUpdateStateCardSearch); + // Clear view + popup_reset(nfc->popup); + + nfc_blink_stop(nfc); +} diff --git a/applications/main/nfc/scenes/nfc_scene_mf_classic_update_success.c b/applications/main/nfc/scenes/nfc_scene_mf_classic_update_success.c new file mode 100644 index 000000000..fef8fd5e9 --- /dev/null +++ b/applications/main/nfc/scenes/nfc_scene_mf_classic_update_success.c @@ -0,0 +1,44 @@ +#include "../nfc_i.h" +#include + +void nfc_scene_mf_classic_update_success_popup_callback(void* context) { + Nfc* nfc = context; + view_dispatcher_send_custom_event(nfc->view_dispatcher, NfcCustomEventViewExit); +} + +void nfc_scene_mf_classic_update_success_on_enter(void* context) { + Nfc* nfc = context; + DOLPHIN_DEED(DolphinDeedNfcSave); + + notification_message(nfc->notifications, &sequence_success); + + Popup* popup = nfc->popup; + popup_set_icon(popup, 32, 5, &I_DolphinNice_96x59); + popup_set_header(popup, "Updated!", 11, 20, AlignLeft, AlignBottom); + popup_set_timeout(popup, 1500); + popup_set_context(popup, nfc); + popup_set_callback(popup, nfc_scene_mf_classic_update_success_popup_callback); + popup_enable_timeout(popup); + + view_dispatcher_switch_to_view(nfc->view_dispatcher, NfcViewPopup); +} + +bool nfc_scene_mf_classic_update_success_on_event(void* context, SceneManagerEvent event) { + Nfc* nfc = context; + bool consumed = false; + + if(event.type == SceneManagerEventTypeCustom) { + if(event.event == NfcCustomEventViewExit) { + consumed = scene_manager_search_and_switch_to_previous_scene( + nfc->scene_manager, NfcSceneFileSelect); + } + } + return consumed; +} + +void nfc_scene_mf_classic_update_success_on_exit(void* context) { + Nfc* nfc = context; + + // Clear view + popup_reset(nfc->popup); +} diff --git a/applications/main/nfc/scenes/nfc_scene_mf_classic_write.c b/applications/main/nfc/scenes/nfc_scene_mf_classic_write.c new file mode 100644 index 000000000..3543cbc58 --- /dev/null +++ b/applications/main/nfc/scenes/nfc_scene_mf_classic_write.c @@ -0,0 +1,92 @@ +#include "../nfc_i.h" +#include + +enum { + NfcSceneMfClassicWriteStateCardSearch, + NfcSceneMfClassicWriteStateCardFound, +}; + +bool nfc_mf_classic_write_worker_callback(NfcWorkerEvent event, void* context) { + furi_assert(context); + + Nfc* nfc = context; + view_dispatcher_send_custom_event(nfc->view_dispatcher, event); + + return true; +} + +static void nfc_scene_mf_classic_write_setup_view(Nfc* nfc) { + Popup* popup = nfc->popup; + popup_reset(popup); + uint32_t state = scene_manager_get_scene_state(nfc->scene_manager, NfcSceneMfClassicWrite); + + if(state == NfcSceneMfClassicWriteStateCardSearch) { + popup_set_text( + nfc->popup, "Apply the initial\ncard only", 128, 32, AlignRight, AlignCenter); + popup_set_icon(nfc->popup, 0, 8, &I_NFC_manual_60x50); + } else { + popup_set_header(popup, "Writing\nDon't move...", 52, 32, AlignLeft, AlignCenter); + popup_set_icon(popup, 12, 23, &A_Loading_24); + } + + view_dispatcher_switch_to_view(nfc->view_dispatcher, NfcViewPopup); +} + +void nfc_scene_mf_classic_write_on_enter(void* context) { + Nfc* nfc = context; + DOLPHIN_DEED(DolphinDeedNfcEmulate); + + scene_manager_set_scene_state( + nfc->scene_manager, NfcSceneMfClassicWrite, NfcSceneMfClassicWriteStateCardSearch); + nfc_scene_mf_classic_write_setup_view(nfc); + + // Setup and start worker + nfc_worker_start( + nfc->worker, + NfcWorkerStateMfClassicWrite, + &nfc->dev->dev_data, + nfc_mf_classic_write_worker_callback, + nfc); + nfc_blink_emulate_start(nfc); +} + +bool nfc_scene_mf_classic_write_on_event(void* context, SceneManagerEvent event) { + Nfc* nfc = context; + bool consumed = false; + + if(event.type == SceneManagerEventTypeCustom) { + if(event.event == NfcWorkerEventSuccess) { + scene_manager_next_scene(nfc->scene_manager, NfcSceneMfClassicWriteSuccess); + consumed = true; + } else if(event.event == NfcWorkerEventFail) { + scene_manager_next_scene(nfc->scene_manager, NfcSceneMfClassicWriteFail); + consumed = true; + } else if(event.event == NfcWorkerEventWrongCard) { + scene_manager_next_scene(nfc->scene_manager, NfcSceneMfClassicWrongCard); + consumed = true; + } else if(event.event == NfcWorkerEventCardDetected) { + scene_manager_set_scene_state( + nfc->scene_manager, NfcSceneMfClassicWrite, NfcSceneMfClassicWriteStateCardFound); + nfc_scene_mf_classic_write_setup_view(nfc); + consumed = true; + } else if(event.event == NfcWorkerEventNoCardDetected) { + scene_manager_set_scene_state( + nfc->scene_manager, NfcSceneMfClassicWrite, NfcSceneMfClassicWriteStateCardSearch); + nfc_scene_mf_classic_write_setup_view(nfc); + consumed = true; + } + } + return consumed; +} + +void nfc_scene_mf_classic_write_on_exit(void* context) { + Nfc* nfc = context; + + nfc_worker_stop(nfc->worker); + scene_manager_set_scene_state( + nfc->scene_manager, NfcSceneMfClassicWrite, NfcSceneMfClassicWriteStateCardSearch); + // Clear view + popup_reset(nfc->popup); + + nfc_blink_stop(nfc); +} diff --git a/applications/main/nfc/scenes/nfc_scene_mf_classic_write_fail.c b/applications/main/nfc/scenes/nfc_scene_mf_classic_write_fail.c new file mode 100644 index 000000000..aeea6eef0 --- /dev/null +++ b/applications/main/nfc/scenes/nfc_scene_mf_classic_write_fail.c @@ -0,0 +1,58 @@ +#include "../nfc_i.h" + +void nfc_scene_mf_classic_write_fail_widget_callback( + GuiButtonType result, + InputType type, + void* context) { + Nfc* nfc = context; + if(type == InputTypeShort) { + view_dispatcher_send_custom_event(nfc->view_dispatcher, result); + } +} + +void nfc_scene_mf_classic_write_fail_on_enter(void* context) { + Nfc* nfc = context; + Widget* widget = nfc->widget; + + notification_message(nfc->notifications, &sequence_error); + + widget_add_icon_element(widget, 72, 17, &I_DolphinCommon_56x48); + widget_add_string_element( + widget, 7, 4, AlignLeft, AlignTop, FontPrimary, "Writing gone wrong!"); + widget_add_string_multiline_element( + widget, + 7, + 17, + AlignLeft, + AlignTop, + FontSecondary, + "Not all sectors\nwere written\ncorrectly."); + + widget_add_button_element( + widget, GuiButtonTypeLeft, "Finish", nfc_scene_mf_classic_write_fail_widget_callback, nfc); + + // Setup and start worker + view_dispatcher_switch_to_view(nfc->view_dispatcher, NfcViewWidget); +} + +bool nfc_scene_mf_classic_write_fail_on_event(void* context, SceneManagerEvent event) { + Nfc* nfc = context; + bool consumed = false; + + if(event.type == SceneManagerEventTypeCustom) { + if(event.event == GuiButtonTypeLeft) { + consumed = scene_manager_search_and_switch_to_previous_scene( + nfc->scene_manager, NfcSceneFileSelect); + } + } else if(event.type == SceneManagerEventTypeBack) { + consumed = scene_manager_search_and_switch_to_previous_scene( + nfc->scene_manager, NfcSceneSavedMenu); + } + return consumed; +} + +void nfc_scene_mf_classic_write_fail_on_exit(void* context) { + Nfc* nfc = context; + + widget_reset(nfc->widget); +} diff --git a/applications/main/nfc/scenes/nfc_scene_mf_classic_write_success.c b/applications/main/nfc/scenes/nfc_scene_mf_classic_write_success.c new file mode 100644 index 000000000..2f2a3beb1 --- /dev/null +++ b/applications/main/nfc/scenes/nfc_scene_mf_classic_write_success.c @@ -0,0 +1,44 @@ +#include "../nfc_i.h" +#include + +void nfc_scene_mf_classic_write_success_popup_callback(void* context) { + Nfc* nfc = context; + view_dispatcher_send_custom_event(nfc->view_dispatcher, NfcCustomEventViewExit); +} + +void nfc_scene_mf_classic_write_success_on_enter(void* context) { + Nfc* nfc = context; + DOLPHIN_DEED(DolphinDeedNfcSave); + + notification_message(nfc->notifications, &sequence_success); + + Popup* popup = nfc->popup; + popup_set_icon(popup, 32, 5, &I_DolphinNice_96x59); + popup_set_header(popup, "Successfully\nwritten", 13, 22, AlignLeft, AlignBottom); + popup_set_timeout(popup, 1500); + popup_set_context(popup, nfc); + popup_set_callback(popup, nfc_scene_mf_classic_write_success_popup_callback); + popup_enable_timeout(popup); + + view_dispatcher_switch_to_view(nfc->view_dispatcher, NfcViewPopup); +} + +bool nfc_scene_mf_classic_write_success_on_event(void* context, SceneManagerEvent event) { + Nfc* nfc = context; + bool consumed = false; + + if(event.type == SceneManagerEventTypeCustom) { + if(event.event == NfcCustomEventViewExit) { + consumed = scene_manager_search_and_switch_to_previous_scene( + nfc->scene_manager, NfcSceneFileSelect); + } + } + return consumed; +} + +void nfc_scene_mf_classic_write_success_on_exit(void* context) { + Nfc* nfc = context; + + // Clear view + popup_reset(nfc->popup); +} diff --git a/applications/main/nfc/scenes/nfc_scene_mf_classic_wrong_card.c b/applications/main/nfc/scenes/nfc_scene_mf_classic_wrong_card.c new file mode 100644 index 000000000..2c56270e3 --- /dev/null +++ b/applications/main/nfc/scenes/nfc_scene_mf_classic_wrong_card.c @@ -0,0 +1,53 @@ +#include "../nfc_i.h" + +void nfc_scene_mf_classic_wrong_card_widget_callback( + GuiButtonType result, + InputType type, + void* context) { + Nfc* nfc = context; + if(type == InputTypeShort) { + view_dispatcher_send_custom_event(nfc->view_dispatcher, result); + } +} + +void nfc_scene_mf_classic_wrong_card_on_enter(void* context) { + Nfc* nfc = context; + Widget* widget = nfc->widget; + + notification_message(nfc->notifications, &sequence_error); + + widget_add_icon_element(widget, 73, 17, &I_DolphinCommon_56x48); + widget_add_string_element( + widget, 3, 4, AlignLeft, AlignTop, FontPrimary, "This is wrong card"); + widget_add_string_multiline_element( + widget, + 4, + 17, + AlignLeft, + AlignTop, + FontSecondary, + "Data management\nis only possible\nwith initial card"); + widget_add_button_element( + widget, GuiButtonTypeLeft, "Retry", nfc_scene_mf_classic_wrong_card_widget_callback, nfc); + + // Setup and start worker + view_dispatcher_switch_to_view(nfc->view_dispatcher, NfcViewWidget); +} + +bool nfc_scene_mf_classic_wrong_card_on_event(void* context, SceneManagerEvent event) { + Nfc* nfc = context; + bool consumed = false; + + if(event.type == SceneManagerEventTypeCustom) { + if(event.event == GuiButtonTypeLeft) { + consumed = scene_manager_previous_scene(nfc->scene_manager); + } + } + return consumed; +} + +void nfc_scene_mf_classic_wrong_card_on_exit(void* context) { + Nfc* nfc = context; + + widget_reset(nfc->widget); +} \ No newline at end of file diff --git a/applications/main/nfc/scenes/nfc_scene_saved_menu.c b/applications/main/nfc/scenes/nfc_scene_saved_menu.c index 09d2c2d61..231f12089 100644 --- a/applications/main/nfc/scenes/nfc_scene_saved_menu.c +++ b/applications/main/nfc/scenes/nfc_scene_saved_menu.c @@ -4,6 +4,9 @@ enum SubmenuIndex { SubmenuIndexEmulate, SubmenuIndexEditUid, + SubmenuIndexDetectReader, + SubmenuIndexWrite, + SubmenuIndexUpdate, SubmenuIndexRename, SubmenuIndexDelete, SubmenuIndexInfo, @@ -42,6 +45,28 @@ void nfc_scene_saved_menu_on_enter(void* context) { submenu_add_item( submenu, "Emulate", SubmenuIndexEmulate, nfc_scene_saved_menu_submenu_callback, nfc); } + if(nfc->dev->format == NfcDeviceSaveFormatMifareClassic) { + if(!mf_classic_is_card_read(&nfc->dev->dev_data.mf_classic_data)) { + submenu_add_item( + submenu, + "Detect reader", + SubmenuIndexDetectReader, + nfc_scene_saved_menu_submenu_callback, + nfc); + } + submenu_add_item( + submenu, + "Write To Initial Card", + SubmenuIndexWrite, + nfc_scene_saved_menu_submenu_callback, + nfc); + submenu_add_item( + submenu, + "Update From Initial Card", + SubmenuIndexUpdate, + nfc_scene_saved_menu_submenu_callback, + nfc); + } submenu_add_item( submenu, "Info", SubmenuIndexInfo, nfc_scene_saved_menu_submenu_callback, nfc); if(nfc->dev->shadow_file_exist) { @@ -79,6 +104,15 @@ bool nfc_scene_saved_menu_on_event(void* context, SceneManagerEvent event) { } DOLPHIN_DEED(DolphinDeedNfcEmulate); consumed = true; + } else if(event.event == SubmenuIndexDetectReader) { + scene_manager_next_scene(nfc->scene_manager, NfcSceneDetectReader); + consumed = true; + } else if(event.event == SubmenuIndexWrite) { + scene_manager_next_scene(nfc->scene_manager, NfcSceneMfClassicWrite); + consumed = true; + } else if(event.event == SubmenuIndexUpdate) { + scene_manager_next_scene(nfc->scene_manager, NfcSceneMfClassicUpdate); + consumed = true; } else if(event.event == SubmenuIndexRename) { scene_manager_next_scene(nfc->scene_manager, NfcSceneSaveName); consumed = true; diff --git a/applications/main/nfc/scenes/nfc_scene_start.c b/applications/main/nfc/scenes/nfc_scene_start.c index 0c4ec1cf9..028f85ae0 100644 --- a/applications/main/nfc/scenes/nfc_scene_start.c +++ b/applications/main/nfc/scenes/nfc_scene_start.c @@ -53,6 +53,7 @@ bool nfc_scene_start_on_event(void* context, SceneManagerEvent event) { } else if(event.event == SubmenuIndexDetectReader) { bool sd_exist = storage_sd_status(nfc->dev->storage) == FSE_OK; if(sd_exist) { + nfc_device_data_clear(&nfc->dev->dev_data); scene_manager_next_scene(nfc->scene_manager, NfcSceneDetectReader); DOLPHIN_DEED(DolphinDeedNfcDetectReader); } else { diff --git a/applications/main/nfc/views/detect_reader.c b/applications/main/nfc/views/detect_reader.c index 91537868b..e5951beb2 100644 --- a/applications/main/nfc/views/detect_reader.c +++ b/applications/main/nfc/views/detect_reader.c @@ -2,6 +2,8 @@ #include #include +#define DETECT_READER_UID_MAX_LEN (10) + struct DetectReader { View* view; DetectReaderDoneCallback callback; @@ -12,6 +14,7 @@ typedef struct { uint16_t nonces; uint16_t nonces_max; DetectReaderState state; + FuriString* uid_str; } DetectReaderViewModel; static void detect_reader_draw_callback(Canvas* canvas, void* model) { @@ -23,6 +26,10 @@ static void detect_reader_draw_callback(Canvas* canvas, void* model) { if(m->state == DetectReaderStateStart) { snprintf(text, sizeof(text), "Touch the reader"); canvas_draw_icon(canvas, 21, 13, &I_Move_flipper_26x39); + if(furi_string_size(m->uid_str)) { + elements_multiline_text_aligned( + canvas, 64, 64, AlignCenter, AlignBottom, furi_string_get_cstr(m->uid_str)); + } } else if(m->state == DetectReaderStateReaderDetected) { snprintf(text, sizeof(text), "Move the Flipper away"); canvas_draw_icon(canvas, 24, 25, &I_Release_arrow_18x15); @@ -86,12 +93,24 @@ DetectReader* detect_reader_alloc() { view_set_input_callback(detect_reader->view, detect_reader_input_callback); view_set_context(detect_reader->view, detect_reader); + with_view_model( + detect_reader->view, + DetectReaderViewModel * model, + { model->uid_str = furi_string_alloc(); }, + false); + return detect_reader; } void detect_reader_free(DetectReader* detect_reader) { furi_assert(detect_reader); + with_view_model( + detect_reader->view, + DetectReaderViewModel * model, + { furi_string_free(model->uid_str); }, + false); + view_free(detect_reader->view); free(detect_reader); } @@ -106,6 +125,7 @@ void detect_reader_reset(DetectReader* detect_reader) { model->nonces = 0; model->nonces_max = 0; model->state = DetectReaderStateStart; + furi_string_reset(model->uid_str); }, false); } @@ -152,3 +172,19 @@ void detect_reader_set_state(DetectReader* detect_reader, DetectReaderState stat with_view_model( detect_reader->view, DetectReaderViewModel * model, { model->state = state; }, true); } + +void detect_reader_set_uid(DetectReader* detect_reader, uint8_t* uid, uint8_t uid_len) { + furi_assert(detect_reader); + furi_assert(uid); + furi_assert(uid_len < DETECT_READER_UID_MAX_LEN); + with_view_model( + detect_reader->view, + DetectReaderViewModel * model, + { + furi_string_set_str(model->uid_str, "UID:"); + for(size_t i = 0; i < uid_len; i++) { + furi_string_cat_printf(model->uid_str, " %02X", uid[i]); + } + }, + true); +} diff --git a/applications/main/nfc/views/detect_reader.h b/applications/main/nfc/views/detect_reader.h index aabdd7c87..6481216b4 100644 --- a/applications/main/nfc/views/detect_reader.h +++ b/applications/main/nfc/views/detect_reader.h @@ -32,3 +32,5 @@ void detect_reader_set_nonces_max(DetectReader* detect_reader, uint16_t nonces_m void detect_reader_set_nonces_collected(DetectReader* detect_reader, uint16_t nonces_collected); void detect_reader_set_state(DetectReader* detect_reader, DetectReaderState state); + +void detect_reader_set_uid(DetectReader* detect_reader, uint8_t* uid, uint8_t uid_len); diff --git a/firmware/targets/f7/furi_hal/furi_hal_nfc.c b/firmware/targets/f7/furi_hal/furi_hal_nfc.c index 069ac4ea4..3ebf4f82b 100644 --- a/firmware/targets/f7/furi_hal/furi_hal_nfc.c +++ b/firmware/targets/f7/furi_hal/furi_hal_nfc.c @@ -620,6 +620,10 @@ uint16_t furi_hal_nfc_bitstream_to_data_and_parity( uint16_t in_buff_bits, uint8_t* out_data, uint8_t* out_parity) { + if(in_buff_bits < 8) { + out_data[0] = in_buff[0]; + return in_buff_bits; + } if(in_buff_bits % 9 != 0) { return 0; } @@ -635,7 +639,7 @@ uint16_t furi_hal_nfc_bitstream_to_data_and_parity( bit_processed += 9; curr_byte++; } - return curr_byte; + return curr_byte * 8; } bool furi_hal_nfc_tx_rx(FuriHalNfcTxRxContext* tx_rx, uint16_t timeout_ms) { @@ -692,8 +696,8 @@ bool furi_hal_nfc_tx_rx(FuriHalNfcTxRxContext* tx_rx, uint16_t timeout_ms) { if(tx_rx->tx_rx_type == FuriHalNfcTxRxTypeRaw || tx_rx->tx_rx_type == FuriHalNfcTxRxTypeRxRaw) { - tx_rx->rx_bits = 8 * furi_hal_nfc_bitstream_to_data_and_parity( - temp_rx_buff, *temp_rx_bits, tx_rx->rx_data, tx_rx->rx_parity); + tx_rx->rx_bits = furi_hal_nfc_bitstream_to_data_and_parity( + temp_rx_buff, *temp_rx_bits, tx_rx->rx_data, tx_rx->rx_parity); } else { memcpy(tx_rx->rx_data, temp_rx_buff, MIN(*temp_rx_bits / 8, FURI_HAL_NFC_DATA_BUFF_SIZE)); tx_rx->rx_bits = *temp_rx_bits; diff --git a/lib/nfc/helpers/reader_analyzer.c b/lib/nfc/helpers/reader_analyzer.c index 0ba657a2e..7fed9c6f6 100644 --- a/lib/nfc/helpers/reader_analyzer.c +++ b/lib/nfc/helpers/reader_analyzer.c @@ -201,10 +201,17 @@ NfcProtocol FuriHalNfcDevData* reader_analyzer_get_nfc_data(ReaderAnalyzer* instance) { furi_assert(instance); - + instance->nfc_data = reader_analyzer_nfc_data[ReaderAnalyzerNfcDataMfClassic]; return &instance->nfc_data; } +void reader_analyzer_set_nfc_data(ReaderAnalyzer* instance, FuriHalNfcDevData* nfc_data) { + furi_assert(instance); + furi_assert(nfc_data); + + memcpy(&instance->nfc_data, nfc_data, sizeof(FuriHalNfcDevData)); +} + static void reader_analyzer_write( ReaderAnalyzer* instance, uint8_t* data, diff --git a/lib/nfc/helpers/reader_analyzer.h b/lib/nfc/helpers/reader_analyzer.h index cc501f5a6..13bf4d77c 100644 --- a/lib/nfc/helpers/reader_analyzer.h +++ b/lib/nfc/helpers/reader_analyzer.h @@ -35,6 +35,8 @@ NfcProtocol FuriHalNfcDevData* reader_analyzer_get_nfc_data(ReaderAnalyzer* instance); +void reader_analyzer_set_nfc_data(ReaderAnalyzer* instance, FuriHalNfcDevData* nfc_data); + void reader_analyzer_prepare_tx_rx( ReaderAnalyzer* instance, FuriHalNfcTxRxContext* tx_rx, diff --git a/lib/nfc/nfc_device.c b/lib/nfc/nfc_device.c index 740cfae5e..a5e3fc14f 100644 --- a/lib/nfc/nfc_device.c +++ b/lib/nfc/nfc_device.c @@ -1122,6 +1122,13 @@ static bool nfc_device_load_data(NfcDevice* dev, FuriString* path, bool show_dia if(!flipper_format_read_hex(file, "UID", data->uid, data->uid_len)) break; if(!flipper_format_read_hex(file, "ATQA", data->atqa, 2)) break; if(!flipper_format_read_hex(file, "SAK", &data->sak, 1)) break; + // Load CUID + uint8_t* cuid_start = data->uid; + if(data->uid_len == 7) { + cuid_start = &data->uid[3]; + } + data->cuid = (cuid_start[0] << 24) | (cuid_start[1] << 16) | (cuid_start[2] << 8) | + (cuid_start[3]); // Parse other data if(dev->format == NfcDeviceSaveFormatMifareUl) { if(!nfc_device_load_mifare_ul_data(file, dev)) break; diff --git a/lib/nfc/nfc_worker.c b/lib/nfc/nfc_worker.c index ebe203905..e1e379a06 100644 --- a/lib/nfc/nfc_worker.c +++ b/lib/nfc/nfc_worker.c @@ -99,6 +99,10 @@ int32_t nfc_worker_task(void* context) { nfc_worker_emulate_mf_ultralight(nfc_worker); } else if(nfc_worker->state == NfcWorkerStateMfClassicEmulate) { nfc_worker_emulate_mf_classic(nfc_worker); + } else if(nfc_worker->state == NfcWorkerStateMfClassicWrite) { + nfc_worker_write_mf_classic(nfc_worker); + } else if(nfc_worker->state == NfcWorkerStateMfClassicUpdate) { + nfc_worker_update_mf_classic(nfc_worker); } else if(nfc_worker->state == NfcWorkerStateReadMfUltralightReadAuth) { nfc_worker_mf_ultralight_read_auth(nfc_worker); } else if(nfc_worker->state == NfcWorkerStateMfClassicDictAttack) { @@ -666,6 +670,144 @@ void nfc_worker_emulate_mf_classic(NfcWorker* nfc_worker) { rfal_platform_spi_release(); } +void nfc_worker_write_mf_classic(NfcWorker* nfc_worker) { + FuriHalNfcTxRxContext tx_rx = {}; + bool card_found_notified = false; + FuriHalNfcDevData nfc_data = {}; + MfClassicData* src_data = &nfc_worker->dev_data->mf_classic_data; + MfClassicData dest_data = *src_data; + + while(nfc_worker->state == NfcWorkerStateMfClassicWrite) { + if(furi_hal_nfc_detect(&nfc_data, 200)) { + if(!card_found_notified) { + nfc_worker->callback(NfcWorkerEventCardDetected, nfc_worker->context); + card_found_notified = true; + } + furi_hal_nfc_sleep(); + + FURI_LOG_I(TAG, "Check low level nfc data"); + if(memcmp(&nfc_data, &nfc_worker->dev_data->nfc_data, sizeof(FuriHalNfcDevData))) { + FURI_LOG_E(TAG, "Wrong card"); + nfc_worker->callback(NfcWorkerEventWrongCard, nfc_worker->context); + break; + } + + FURI_LOG_I(TAG, "Check mf classic type"); + MfClassicType type = + mf_classic_get_classic_type(nfc_data.atqa[0], nfc_data.atqa[1], nfc_data.sak); + if(type != nfc_worker->dev_data->mf_classic_data.type) { + FURI_LOG_E(TAG, "Wrong mf classic type"); + nfc_worker->callback(NfcWorkerEventWrongCard, nfc_worker->context); + break; + } + + // Set blocks not read + mf_classic_set_sector_data_not_read(&dest_data); + FURI_LOG_I(TAG, "Updating card sectors"); + uint8_t total_sectors = mf_classic_get_total_sectors_num(type); + bool write_success = true; + for(uint8_t i = 0; i < total_sectors; i++) { + FURI_LOG_I(TAG, "Reading sector %d", i); + mf_classic_read_sector(&tx_rx, &dest_data, i); + bool old_data_read = mf_classic_is_sector_data_read(src_data, i); + bool new_data_read = mf_classic_is_sector_data_read(&dest_data, i); + if(old_data_read != new_data_read) { + FURI_LOG_E(TAG, "Failed to update sector %d", i); + write_success = false; + break; + } + if(nfc_worker->state != NfcWorkerStateMfClassicWrite) break; + if(!mf_classic_write_sector(&tx_rx, &dest_data, src_data, i)) { + FURI_LOG_E(TAG, "Failed to write %d sector", i); + write_success = false; + break; + } + } + if(nfc_worker->state != NfcWorkerStateMfClassicWrite) break; + if(write_success) { + nfc_worker->callback(NfcWorkerEventSuccess, nfc_worker->context); + break; + } else { + nfc_worker->callback(NfcWorkerEventFail, nfc_worker->context); + break; + } + + } else { + if(card_found_notified) { + nfc_worker->callback(NfcWorkerEventNoCardDetected, nfc_worker->context); + card_found_notified = false; + } + } + furi_delay_ms(300); + } +} + +void nfc_worker_update_mf_classic(NfcWorker* nfc_worker) { + FuriHalNfcTxRxContext tx_rx = {}; + bool card_found_notified = false; + FuriHalNfcDevData nfc_data = {}; + MfClassicData* old_data = &nfc_worker->dev_data->mf_classic_data; + MfClassicData new_data = *old_data; + + while(nfc_worker->state == NfcWorkerStateMfClassicUpdate) { + if(furi_hal_nfc_detect(&nfc_data, 200)) { + if(!card_found_notified) { + nfc_worker->callback(NfcWorkerEventCardDetected, nfc_worker->context); + card_found_notified = true; + } + furi_hal_nfc_sleep(); + + FURI_LOG_I(TAG, "Check low level nfc data"); + if(memcmp(&nfc_data, &nfc_worker->dev_data->nfc_data, sizeof(FuriHalNfcDevData))) { + FURI_LOG_E(TAG, "Low level nfc data mismatch"); + nfc_worker->callback(NfcWorkerEventWrongCard, nfc_worker->context); + break; + } + + FURI_LOG_I(TAG, "Check MF classic type"); + MfClassicType type = + mf_classic_get_classic_type(nfc_data.atqa[0], nfc_data.atqa[1], nfc_data.sak); + if(type != nfc_worker->dev_data->mf_classic_data.type) { + FURI_LOG_E(TAG, "MF classic type mismatch"); + nfc_worker->callback(NfcWorkerEventWrongCard, nfc_worker->context); + break; + } + + // Set blocks not read + mf_classic_set_sector_data_not_read(&new_data); + FURI_LOG_I(TAG, "Updating card sectors"); + uint8_t total_sectors = mf_classic_get_total_sectors_num(type); + bool update_success = true; + for(uint8_t i = 0; i < total_sectors; i++) { + FURI_LOG_I(TAG, "Reading sector %d", i); + mf_classic_read_sector(&tx_rx, &new_data, i); + bool old_data_read = mf_classic_is_sector_data_read(old_data, i); + bool new_data_read = mf_classic_is_sector_data_read(&new_data, i); + if(old_data_read != new_data_read) { + FURI_LOG_E(TAG, "Failed to update sector %d", i); + update_success = false; + break; + } + if(nfc_worker->state != NfcWorkerStateMfClassicUpdate) break; + } + if(nfc_worker->state != NfcWorkerStateMfClassicUpdate) break; + + // Check updated data + if(update_success) { + *old_data = new_data; + nfc_worker->callback(NfcWorkerEventSuccess, nfc_worker->context); + break; + } + } else { + if(card_found_notified) { + nfc_worker->callback(NfcWorkerEventNoCardDetected, nfc_worker->context); + card_found_notified = false; + } + } + furi_delay_ms(300); + } +} + void nfc_worker_mf_ultralight_read_auth(NfcWorker* nfc_worker) { furi_assert(nfc_worker); furi_assert(nfc_worker->callback); @@ -758,7 +900,13 @@ void nfc_worker_analyze_reader(NfcWorker* nfc_worker) { FuriHalNfcTxRxContext tx_rx = {}; ReaderAnalyzer* reader_analyzer = nfc_worker->reader_analyzer; - FuriHalNfcDevData* nfc_data = reader_analyzer_get_nfc_data(reader_analyzer); + FuriHalNfcDevData* nfc_data = NULL; + if(nfc_worker->dev_data->protocol == NfcDeviceProtocolMifareClassic) { + nfc_data = &nfc_worker->dev_data->nfc_data; + reader_analyzer_set_nfc_data(reader_analyzer, nfc_data); + } else { + nfc_data = reader_analyzer_get_nfc_data(reader_analyzer); + } MfClassicEmulator emulator = { .cuid = nfc_util_bytes2num(&nfc_data->uid[nfc_data->uid_len - 4], 4), .data = nfc_worker->dev_data->mf_classic_data, diff --git a/lib/nfc/nfc_worker.h b/lib/nfc/nfc_worker.h index 84615f5d8..ce3a18241 100644 --- a/lib/nfc/nfc_worker.h +++ b/lib/nfc/nfc_worker.h @@ -14,6 +14,8 @@ typedef enum { NfcWorkerStateUidEmulate, NfcWorkerStateMfUltralightEmulate, NfcWorkerStateMfClassicEmulate, + NfcWorkerStateMfClassicWrite, + NfcWorkerStateMfClassicUpdate, NfcWorkerStateReadMfUltralightReadAuth, NfcWorkerStateMfClassicDictAttack, NfcWorkerStateAnalyzeReader, @@ -48,13 +50,16 @@ typedef enum { NfcWorkerEventNoCardDetected, NfcWorkerEventWrongCardDetected, - // Mifare Classic events + // Read Mifare Classic events NfcWorkerEventNoDictFound, NfcWorkerEventNewSector, NfcWorkerEventNewDictKeyBatch, NfcWorkerEventFoundKeyA, NfcWorkerEventFoundKeyB, + // Write Mifare Classic events + NfcWorkerEventWrongCard, + // Detect Reader events NfcWorkerEventDetectReaderDetected, NfcWorkerEventDetectReaderLost, diff --git a/lib/nfc/nfc_worker_i.h b/lib/nfc/nfc_worker_i.h index 526182f9a..b9f69e620 100644 --- a/lib/nfc/nfc_worker_i.h +++ b/lib/nfc/nfc_worker_i.h @@ -41,6 +41,10 @@ void nfc_worker_emulate_mf_ultralight(NfcWorker* nfc_worker); void nfc_worker_emulate_mf_classic(NfcWorker* nfc_worker); +void nfc_worker_write_mf_classic(NfcWorker* nfc_worker); + +void nfc_worker_update_mf_classic(NfcWorker* nfc_worker); + void nfc_worker_mf_classic_dict_attack(NfcWorker* nfc_worker); void nfc_worker_mf_ultralight_read_auth(NfcWorker* nfc_worker); diff --git a/lib/nfc/protocols/crypto1.c b/lib/nfc/protocols/crypto1.c index f08164ba9..2ac0ff081 100644 --- a/lib/nfc/protocols/crypto1.c +++ b/lib/nfc/protocols/crypto1.c @@ -73,3 +73,55 @@ uint32_t prng_successor(uint32_t x, uint32_t n) { return SWAPENDIAN(x); } + +void crypto1_decrypt( + Crypto1* crypto, + uint8_t* encrypted_data, + uint16_t encrypted_data_bits, + uint8_t* decrypted_data) { + furi_assert(crypto); + furi_assert(encrypted_data); + furi_assert(decrypted_data); + + if(encrypted_data_bits < 8) { + uint8_t decrypted_byte = 0; + decrypted_byte |= (crypto1_bit(crypto, 0, 0) ^ FURI_BIT(encrypted_data[0], 0)) << 0; + decrypted_byte |= (crypto1_bit(crypto, 0, 0) ^ FURI_BIT(encrypted_data[0], 1)) << 1; + decrypted_byte |= (crypto1_bit(crypto, 0, 0) ^ FURI_BIT(encrypted_data[0], 2)) << 2; + decrypted_byte |= (crypto1_bit(crypto, 0, 0) ^ FURI_BIT(encrypted_data[0], 3)) << 3; + decrypted_data[0] = decrypted_byte; + } else { + for(size_t i = 0; i < encrypted_data_bits / 8; i++) { + decrypted_data[i] = crypto1_byte(crypto, 0, 0) ^ encrypted_data[i]; + } + } +} + +void crypto1_encrypt( + Crypto1* crypto, + uint8_t* keystream, + uint8_t* plain_data, + uint16_t plain_data_bits, + uint8_t* encrypted_data, + uint8_t* encrypted_parity) { + furi_assert(crypto); + furi_assert(plain_data); + furi_assert(encrypted_data); + furi_assert(encrypted_parity); + + if(plain_data_bits < 8) { + encrypted_data[0] = 0; + for(size_t i = 0; i < plain_data_bits; i++) { + encrypted_data[0] |= (crypto1_bit(crypto, 0, 0) ^ FURI_BIT(plain_data[0], i)) << i; + } + } else { + memset(encrypted_parity, 0, plain_data_bits / 8 + 1); + for(uint8_t i = 0; i < plain_data_bits / 8; i++) { + encrypted_data[i] = crypto1_byte(crypto, keystream ? keystream[i] : 0, 0) ^ + plain_data[i]; + encrypted_parity[i / 8] |= + (((crypto1_filter(crypto->odd) ^ nfc_util_odd_parity8(plain_data[i])) & 0x01) + << (7 - (i & 0x0007))); + } + } +} diff --git a/lib/nfc/protocols/crypto1.h b/lib/nfc/protocols/crypto1.h index 07b39c22c..450d1534e 100644 --- a/lib/nfc/protocols/crypto1.h +++ b/lib/nfc/protocols/crypto1.h @@ -21,3 +21,17 @@ uint32_t crypto1_word(Crypto1* crypto1, uint32_t in, int is_encrypted); uint32_t crypto1_filter(uint32_t in); uint32_t prng_successor(uint32_t x, uint32_t n); + +void crypto1_decrypt( + Crypto1* crypto, + uint8_t* encrypted_data, + uint16_t encrypted_data_bits, + uint8_t* decrypted_data); + +void crypto1_encrypt( + Crypto1* crypto, + uint8_t* keystream, + uint8_t* plain_data, + uint16_t plain_data_bits, + uint8_t* encrypted_data, + uint8_t* encrypted_parity); diff --git a/lib/nfc/protocols/mifare_classic.c b/lib/nfc/protocols/mifare_classic.c index e879ff4ef..7b0e17975 100644 --- a/lib/nfc/protocols/mifare_classic.c +++ b/lib/nfc/protocols/mifare_classic.c @@ -9,21 +9,8 @@ #define MF_CLASSIC_AUTH_KEY_A_CMD (0x60U) #define MF_CLASSIC_AUTH_KEY_B_CMD (0x61U) -#define MF_CLASSIC_READ_SECT_CMD (0x30) - -typedef enum { - MfClassicActionDataRead, - MfClassicActionDataWrite, - MfClassicActionDataInc, - MfClassicActionDataDec, - - MfClassicActionKeyARead, - MfClassicActionKeyAWrite, - MfClassicActionKeyBRead, - MfClassicActionKeyBWrite, - MfClassicActionACRead, - MfClassicActionACWrite, -} MfClassicAction; +#define MF_CLASSIC_READ_BLOCK_CMD (0x30) +#define MF_CLASSIC_WRITE_BLOCK_CMD (0xA0) const char* mf_classic_get_type_str(MfClassicType type) { if(type == MfClassicType1k) { @@ -122,6 +109,24 @@ void mf_classic_set_block_read(MfClassicData* data, uint8_t block_num, MfClassic FURI_BIT_SET(data->block_read_mask[block_num / 32], block_num % 32); } +bool mf_classic_is_sector_data_read(MfClassicData* data, uint8_t sector_num) { + furi_assert(data); + + uint8_t first_block = mf_classic_get_first_block_num_of_sector(sector_num); + uint8_t total_blocks = mf_classic_get_blocks_num_in_sector(sector_num); + bool data_read = true; + for(size_t i = first_block; i < first_block + total_blocks; i++) { + data_read &= mf_classic_is_block_read(data, i); + } + + return data_read; +} + +void mf_classic_set_sector_data_not_read(MfClassicData* data) { + furi_assert(data); + memset(data->block_read_mask, 0, sizeof(data->block_read_mask)); +} + bool mf_classic_is_key_found(MfClassicData* data, uint8_t sector_num, MfClassicKey key_type) { furi_assert(data); @@ -190,6 +195,9 @@ void mf_classic_get_read_sectors_and_keys( uint8_t* sectors_read, uint8_t* keys_found) { furi_assert(data); + furi_assert(sectors_read); + furi_assert(keys_found); + *sectors_read = 0; *keys_found = 0; uint8_t sectors_total = mf_classic_get_total_sectors_num(data->type); @@ -225,12 +233,12 @@ bool mf_classic_is_card_read(MfClassicData* data) { return card_read; } -static bool mf_classic_is_allowed_access_sector_trailer( - MfClassicEmulator* emulator, +bool mf_classic_is_allowed_access_sector_trailer( + MfClassicData* data, uint8_t block_num, MfClassicKey key, MfClassicAction action) { - uint8_t* sector_trailer = emulator->data.block[block_num].value; + uint8_t* sector_trailer = data->block[block_num].value; uint8_t AC = ((sector_trailer[7] >> 5) & 0x04) | ((sector_trailer[8] >> 2) & 0x02) | ((sector_trailer[8] >> 7) & 0x01); switch(action) { @@ -266,13 +274,13 @@ static bool mf_classic_is_allowed_access_sector_trailer( return true; } -static bool mf_classic_is_allowed_access_data_block( - MfClassicEmulator* emulator, +bool mf_classic_is_allowed_access_data_block( + MfClassicData* data, uint8_t block_num, MfClassicKey key, MfClassicAction action) { uint8_t* sector_trailer = - emulator->data.block[mf_classic_get_sector_trailer_num_by_block(block_num)].value; + data->block[mf_classic_get_sector_trailer_num_by_block(block_num)].value; uint8_t sector_block; if(block_num <= 128) { @@ -336,9 +344,10 @@ static bool mf_classic_is_allowed_access( MfClassicKey key, MfClassicAction action) { if(mf_classic_is_sector_trailer(block_num)) { - return mf_classic_is_allowed_access_sector_trailer(emulator, block_num, key, action); + return mf_classic_is_allowed_access_sector_trailer( + &emulator->data, block_num, key, action); } else { - return mf_classic_is_allowed_access_data_block(emulator, block_num, key, action); + return mf_classic_is_allowed_access_data_block(&emulator->data, block_num, key, action); } } @@ -514,25 +523,17 @@ bool mf_classic_read_block( furi_assert(block); bool read_block_success = false; - uint8_t plain_cmd[4] = {MF_CLASSIC_READ_SECT_CMD, block_num, 0x00, 0x00}; + uint8_t plain_cmd[4] = {MF_CLASSIC_READ_BLOCK_CMD, block_num, 0x00, 0x00}; nfca_append_crc16(plain_cmd, 2); - memset(tx_rx->tx_data, 0, sizeof(tx_rx->tx_data)); - memset(tx_rx->tx_parity, 0, sizeof(tx_rx->tx_parity)); - for(uint8_t i = 0; i < 4; i++) { - tx_rx->tx_data[i] = crypto1_byte(crypto, 0x00, 0) ^ plain_cmd[i]; - tx_rx->tx_parity[0] |= - ((crypto1_filter(crypto->odd) ^ nfc_util_odd_parity8(plain_cmd[i])) & 0x01) << (7 - i); - } + crypto1_encrypt(crypto, NULL, plain_cmd, 4 * 8, tx_rx->tx_data, tx_rx->tx_parity); tx_rx->tx_bits = 4 * 9; tx_rx->tx_rx_type = FuriHalNfcTxRxTypeRaw; if(furi_hal_nfc_tx_rx(tx_rx, 50)) { if(tx_rx->rx_bits == 8 * (MF_CLASSIC_BLOCK_SIZE + 2)) { uint8_t block_received[MF_CLASSIC_BLOCK_SIZE + 2]; - for(uint8_t i = 0; i < MF_CLASSIC_BLOCK_SIZE + 2; i++) { - block_received[i] = crypto1_byte(crypto, 0, 0) ^ tx_rx->rx_data[i]; - } + crypto1_decrypt(crypto, tx_rx->rx_data, tx_rx->rx_bits, block_received); uint16_t crc_calc = nfca_get_crc16(block_received, MF_CLASSIC_BLOCK_SIZE); uint16_t crc_received = (block_received[MF_CLASSIC_BLOCK_SIZE + 1] << 8) | block_received[MF_CLASSIC_BLOCK_SIZE]; @@ -754,49 +755,6 @@ uint8_t mf_classic_update_card(FuriHalNfcTxRxContext* tx_rx, MfClassicData* data return sectors_read; } -void mf_crypto1_decrypt( - Crypto1* crypto, - uint8_t* encrypted_data, - uint16_t encrypted_data_bits, - uint8_t* decrypted_data) { - if(encrypted_data_bits < 8) { - uint8_t decrypted_byte = 0; - decrypted_byte |= (crypto1_bit(crypto, 0, 0) ^ FURI_BIT(encrypted_data[0], 0)) << 0; - decrypted_byte |= (crypto1_bit(crypto, 0, 0) ^ FURI_BIT(encrypted_data[0], 1)) << 1; - decrypted_byte |= (crypto1_bit(crypto, 0, 0) ^ FURI_BIT(encrypted_data[0], 2)) << 2; - decrypted_byte |= (crypto1_bit(crypto, 0, 0) ^ FURI_BIT(encrypted_data[0], 3)) << 3; - decrypted_data[0] = decrypted_byte; - } else { - for(size_t i = 0; i < encrypted_data_bits / 8; i++) { - decrypted_data[i] = crypto1_byte(crypto, 0, 0) ^ encrypted_data[i]; - } - } -} - -void mf_crypto1_encrypt( - Crypto1* crypto, - uint8_t* keystream, - uint8_t* plain_data, - uint16_t plain_data_bits, - uint8_t* encrypted_data, - uint8_t* encrypted_parity) { - if(plain_data_bits < 8) { - encrypted_data[0] = 0; - for(size_t i = 0; i < plain_data_bits; i++) { - encrypted_data[0] |= (crypto1_bit(crypto, 0, 0) ^ FURI_BIT(plain_data[0], i)) << i; - } - } else { - memset(encrypted_parity, 0, plain_data_bits / 8 + 1); - for(uint8_t i = 0; i < plain_data_bits / 8; i++) { - encrypted_data[i] = crypto1_byte(crypto, keystream ? keystream[i] : 0, 0) ^ - plain_data[i]; - encrypted_parity[i / 8] |= - (((crypto1_filter(crypto->odd) ^ nfc_util_odd_parity8(plain_data[i])) & 0x01) - << (7 - (i & 0x0007))); - } - } -} - bool mf_classic_emulator(MfClassicEmulator* emulator, FuriHalNfcTxRxContext* tx_rx) { furi_assert(emulator); furi_assert(tx_rx); @@ -819,7 +777,7 @@ bool mf_classic_emulator(MfClassicEmulator* emulator, FuriHalNfcTxRxContext* tx_ tx_rx->rx_bits); break; } - mf_crypto1_decrypt(&emulator->crypto, tx_rx->rx_data, tx_rx->rx_bits, plain_data); + crypto1_decrypt(&emulator->crypto, tx_rx->rx_data, tx_rx->rx_bits, plain_data); } if(plain_data[0] == 0x50 && plain_data[1] == 0x00) { @@ -857,7 +815,7 @@ bool mf_classic_emulator(MfClassicEmulator* emulator, FuriHalNfcTxRxContext* tx_ tx_rx->tx_bits = sizeof(nt) * 8; tx_rx->tx_rx_type = FuriHalNfcTxRxTransparent; } else { - mf_crypto1_encrypt( + crypto1_encrypt( &emulator->crypto, nt_keystream, nt, @@ -904,7 +862,7 @@ bool mf_classic_emulator(MfClassicEmulator* emulator, FuriHalNfcTxRxContext* tx_ uint32_t ans = prng_successor(nonce, 96); uint8_t responce[4] = {}; nfc_util_num2bytes(ans, 4, responce); - mf_crypto1_encrypt( + crypto1_encrypt( &emulator->crypto, NULL, responce, @@ -938,7 +896,7 @@ bool mf_classic_emulator(MfClassicEmulator* emulator, FuriHalNfcTxRxContext* tx_ // Send NACK uint8_t nack = 0x04; if(is_encrypted) { - mf_crypto1_encrypt( + crypto1_encrypt( &emulator->crypto, NULL, &nack, 4, tx_rx->tx_data, tx_rx->tx_parity); } else { tx_rx->tx_data[0] = nack; @@ -951,7 +909,7 @@ bool mf_classic_emulator(MfClassicEmulator* emulator, FuriHalNfcTxRxContext* tx_ } nfca_append_crc16(block_data, 16); - mf_crypto1_encrypt( + crypto1_encrypt( &emulator->crypto, NULL, block_data, @@ -967,14 +925,14 @@ bool mf_classic_emulator(MfClassicEmulator* emulator, FuriHalNfcTxRxContext* tx_ } // Send ACK uint8_t ack = 0x0A; - mf_crypto1_encrypt(&emulator->crypto, NULL, &ack, 4, tx_rx->tx_data, tx_rx->tx_parity); + crypto1_encrypt(&emulator->crypto, NULL, &ack, 4, tx_rx->tx_data, tx_rx->tx_parity); tx_rx->tx_rx_type = FuriHalNfcTxRxTransparent; tx_rx->tx_bits = 4; if(!furi_hal_nfc_tx_rx(tx_rx, 300)) break; if(tx_rx->rx_bits != 18 * 8) break; - mf_crypto1_decrypt(&emulator->crypto, tx_rx->rx_data, tx_rx->rx_bits, plain_data); + crypto1_decrypt(&emulator->crypto, tx_rx->rx_data, tx_rx->rx_bits, plain_data); uint8_t block_data[16] = {}; memcpy(block_data, emulator->data.block[block].value, MF_CLASSIC_BLOCK_SIZE); if(mf_classic_is_sector_trailer(block)) { @@ -1002,7 +960,7 @@ bool mf_classic_emulator(MfClassicEmulator* emulator, FuriHalNfcTxRxContext* tx_ } // Send ACK ack = 0x0A; - mf_crypto1_encrypt(&emulator->crypto, NULL, &ack, 4, tx_rx->tx_data, tx_rx->tx_parity); + crypto1_encrypt(&emulator->crypto, NULL, &ack, 4, tx_rx->tx_data, tx_rx->tx_parity); tx_rx->tx_rx_type = FuriHalNfcTxRxTransparent; tx_rx->tx_bits = 4; } else { @@ -1015,8 +973,7 @@ bool mf_classic_emulator(MfClassicEmulator* emulator, FuriHalNfcTxRxContext* tx_ // Send NACK uint8_t nack = 0x04; if(is_encrypted) { - mf_crypto1_encrypt( - &emulator->crypto, NULL, &nack, 4, tx_rx->tx_data, tx_rx->tx_parity); + crypto1_encrypt(&emulator->crypto, NULL, &nack, 4, tx_rx->tx_data, tx_rx->tx_parity); } else { tx_rx->tx_data[0] = nack; } @@ -1027,3 +984,143 @@ bool mf_classic_emulator(MfClassicEmulator* emulator, FuriHalNfcTxRxContext* tx_ return true; } + +bool mf_classic_write_block( + FuriHalNfcTxRxContext* tx_rx, + MfClassicBlock* src_block, + uint8_t block_num, + MfClassicKey key_type, + uint64_t key) { + furi_assert(tx_rx); + furi_assert(src_block); + + Crypto1 crypto = {}; + uint8_t plain_data[18] = {}; + uint8_t resp = 0; + bool write_success = false; + + do { + furi_hal_nfc_sleep(); + if(!mf_classic_auth(tx_rx, block_num, key, key_type, &crypto)) { + FURI_LOG_D(TAG, "Auth fail"); + break; + } + // Send write command + plain_data[0] = MF_CLASSIC_WRITE_BLOCK_CMD; + plain_data[1] = block_num; + nfca_append_crc16(plain_data, 2); + crypto1_encrypt(&crypto, NULL, plain_data, 4 * 8, tx_rx->tx_data, tx_rx->tx_parity); + tx_rx->tx_bits = 4 * 8; + tx_rx->tx_rx_type = FuriHalNfcTxRxTypeRaw; + + if(furi_hal_nfc_tx_rx(tx_rx, 50)) { + if(tx_rx->rx_bits == 4) { + crypto1_decrypt(&crypto, tx_rx->rx_data, 4, &resp); + if(resp != 0x0A) { + FURI_LOG_D(TAG, "NACK received on write cmd: %02X", resp); + break; + } + } else { + FURI_LOG_D(TAG, "Not ACK received"); + break; + } + } else { + FURI_LOG_D(TAG, "Failed to send write cmd"); + break; + } + + // Send data + memcpy(plain_data, src_block->value, MF_CLASSIC_BLOCK_SIZE); + nfca_append_crc16(plain_data, MF_CLASSIC_BLOCK_SIZE); + crypto1_encrypt( + &crypto, + NULL, + plain_data, + (MF_CLASSIC_BLOCK_SIZE + 2) * 8, + tx_rx->tx_data, + tx_rx->tx_parity); + tx_rx->tx_bits = (MF_CLASSIC_BLOCK_SIZE + 2) * 8; + tx_rx->tx_rx_type = FuriHalNfcTxRxTypeRaw; + if(furi_hal_nfc_tx_rx(tx_rx, 50)) { + if(tx_rx->rx_bits == 4) { + crypto1_decrypt(&crypto, tx_rx->rx_data, 4, &resp); + if(resp != 0x0A) { + FURI_LOG_D(TAG, "NACK received on sending data"); + break; + } + } else { + FURI_LOG_D(TAG, "Not ACK received"); + break; + } + } else { + FURI_LOG_D(TAG, "Failed to send data"); + break; + } + write_success = true; + + // Send Halt + plain_data[0] = 0x50; + plain_data[1] = 0x00; + nfca_append_crc16(plain_data, 2); + crypto1_encrypt(&crypto, NULL, plain_data, 2 * 8, tx_rx->tx_data, tx_rx->tx_parity); + tx_rx->tx_bits = 2 * 8; + tx_rx->tx_rx_type = FuriHalNfcTxRxTypeRaw; + // No response is expected + furi_hal_nfc_tx_rx(tx_rx, 50); + } while(false); + + return write_success; +} + +bool mf_classic_write_sector( + FuriHalNfcTxRxContext* tx_rx, + MfClassicData* dest_data, + MfClassicData* src_data, + uint8_t sec_num) { + furi_assert(tx_rx); + furi_assert(dest_data); + furi_assert(src_data); + + uint8_t first_block = mf_classic_get_first_block_num_of_sector(sec_num); + uint8_t total_blocks = mf_classic_get_blocks_num_in_sector(sec_num); + MfClassicSectorTrailer* sec_tr = mf_classic_get_sector_trailer_by_sector(dest_data, sec_num); + bool key_a_found = mf_classic_is_key_found(dest_data, sec_num, MfClassicKeyA); + bool key_b_found = mf_classic_is_key_found(dest_data, sec_num, MfClassicKeyB); + + bool write_success = true; + for(size_t i = first_block; i < first_block + total_blocks; i++) { + // Compare blocks + if(memcmp(dest_data->block[i].value, src_data->block[i].value, MF_CLASSIC_BLOCK_SIZE)) { + bool key_a_write_allowed = mf_classic_is_allowed_access_data_block( + dest_data, i, MfClassicKeyA, MfClassicActionDataWrite); + bool key_b_write_allowed = mf_classic_is_allowed_access_data_block( + dest_data, i, MfClassicKeyB, MfClassicActionDataWrite); + + if(key_a_found && key_a_write_allowed) { + FURI_LOG_I(TAG, "Writing block %d with key A", i); + uint64_t key = nfc_util_bytes2num(sec_tr->key_a, 6); + if(!mf_classic_write_block(tx_rx, &src_data->block[i], i, MfClassicKeyA, key)) { + FURI_LOG_E(TAG, "Failed to write block %d", i); + write_success = false; + break; + } + } else if(key_b_found && key_b_write_allowed) { + FURI_LOG_I(TAG, "Writing block %d with key A", i); + uint64_t key = nfc_util_bytes2num(sec_tr->key_b, 6); + if(!mf_classic_write_block(tx_rx, &src_data->block[i], i, MfClassicKeyB, key)) { + FURI_LOG_E(TAG, "Failed to write block %d", i); + write_success = false; + break; + } + } else { + FURI_LOG_E(TAG, "Failed to find key with write access"); + write_success = false; + break; + } + } else { + FURI_LOG_D(TAG, "Blocks %d are equal", i); + } + } + + return write_success; +} diff --git a/lib/nfc/protocols/mifare_classic.h b/lib/nfc/protocols/mifare_classic.h index ead846e42..d5467b100 100644 --- a/lib/nfc/protocols/mifare_classic.h +++ b/lib/nfc/protocols/mifare_classic.h @@ -27,6 +27,20 @@ typedef enum { MfClassicKeyB, } MfClassicKey; +typedef enum { + MfClassicActionDataRead, + MfClassicActionDataWrite, + MfClassicActionDataInc, + MfClassicActionDataDec, + + MfClassicActionKeyARead, + MfClassicActionKeyAWrite, + MfClassicActionKeyBRead, + MfClassicActionKeyBWrite, + MfClassicActionACRead, + MfClassicActionACWrite, +} MfClassicAction; + typedef struct { uint8_t value[MF_CLASSIC_BLOCK_SIZE]; } MfClassicBlock; @@ -90,6 +104,18 @@ bool mf_classic_is_sector_trailer(uint8_t block); uint8_t mf_classic_get_sector_by_block(uint8_t block); +bool mf_classic_is_allowed_access_sector_trailer( + MfClassicData* data, + uint8_t block_num, + MfClassicKey key, + MfClassicAction action); + +bool mf_classic_is_allowed_access_data_block( + MfClassicData* data, + uint8_t block_num, + MfClassicKey key, + MfClassicAction action); + bool mf_classic_is_key_found(MfClassicData* data, uint8_t sector_num, MfClassicKey key_type); void mf_classic_set_key_found( @@ -104,6 +130,10 @@ bool mf_classic_is_block_read(MfClassicData* data, uint8_t block_num); void mf_classic_set_block_read(MfClassicData* data, uint8_t block_num, MfClassicBlock* block_data); +bool mf_classic_is_sector_data_read(MfClassicData* data, uint8_t sector_num); + +void mf_classic_set_sector_data_not_read(MfClassicData* data); + bool mf_classic_is_sector_read(MfClassicData* data, uint8_t sector_num); bool mf_classic_is_card_read(MfClassicData* data); @@ -145,3 +175,16 @@ uint8_t mf_classic_read_card( uint8_t mf_classic_update_card(FuriHalNfcTxRxContext* tx_rx, MfClassicData* data); bool mf_classic_emulator(MfClassicEmulator* emulator, FuriHalNfcTxRxContext* tx_rx); + +bool mf_classic_write_block( + FuriHalNfcTxRxContext* tx_rx, + MfClassicBlock* src_block, + uint8_t block_num, + MfClassicKey key_type, + uint64_t key); + +bool mf_classic_write_sector( + FuriHalNfcTxRxContext* tx_rx, + MfClassicData* dest_data, + MfClassicData* src_data, + uint8_t sec_num); From d5f791b1fa8ee34428b92f6c0c2ddac9d6fcbe0a Mon Sep 17 00:00:00 2001 From: Georgii Surkov <37121527+gsurkov@users.noreply.github.com> Date: Fri, 28 Oct 2022 19:43:54 +0300 Subject: [PATCH 10/49] [FL-2911] IR Universal Audio Remote (#1942) * Add Audio universal remote * Add signal library for Audio Universal Remote * Update UniversalRemotes.md * Added IR profile for Samsung K450 soundbar (#1892) * Add symbols to API file * Rearrange Audio remote buttons * Add new icons, remove old ones * Remove old signals, add new ones * Add universal audio remote to CLI, refactor code * Improve help text * Correct formatting * Update UniversalRemotes.md * Furi: restore correct api_symbols.csv version Co-authored-by: Alexei Humeniy Co-authored-by: Aleksandr Kutuzov --- applications/main/infrared/infrared_cli.c | 211 ++++++--------- .../infrared/scenes/infrared_scene_config.h | 1 + .../scenes/infrared_scene_universal.c | 8 +- .../scenes/infrared_scene_universal_audio.c | 133 ++++++++++ assets/icons/Infrared/Pause_25x27.png | Bin 0 -> 3634 bytes assets/icons/Infrared/Pause_hvr_25x27.png | Bin 0 -> 3623 bytes assets/icons/Infrared/Play_25x27.png | Bin 0 -> 3653 bytes assets/icons/Infrared/Play_hvr_25x27.png | Bin 0 -> 3643 bytes assets/icons/Infrared/TrackNext_25x27.png | Bin 0 -> 3651 bytes assets/icons/Infrared/TrackNext_hvr_25x27.png | Bin 0 -> 3639 bytes assets/icons/Infrared/TrackPrev_25x27.png | Bin 0 -> 3657 bytes assets/icons/Infrared/TrackPrev_hvr_25x27.png | Bin 0 -> 3644 bytes assets/resources/infrared/assets/audio.ir | 244 ++++++++++++++++++ documentation/UniversalRemotes.md | 16 +- 14 files changed, 486 insertions(+), 127 deletions(-) create mode 100644 applications/main/infrared/scenes/infrared_scene_universal_audio.c create mode 100644 assets/icons/Infrared/Pause_25x27.png create mode 100644 assets/icons/Infrared/Pause_hvr_25x27.png create mode 100644 assets/icons/Infrared/Play_25x27.png create mode 100644 assets/icons/Infrared/Play_hvr_25x27.png create mode 100644 assets/icons/Infrared/TrackNext_25x27.png create mode 100644 assets/icons/Infrared/TrackNext_hvr_25x27.png create mode 100644 assets/icons/Infrared/TrackPrev_25x27.png create mode 100644 assets/icons/Infrared/TrackPrev_hvr_25x27.png create mode 100644 assets/resources/infrared/assets/audio.ir diff --git a/applications/main/infrared/infrared_cli.c b/applications/main/infrared/infrared_cli.c index 5a04f7495..8f35a8fd1 100644 --- a/applications/main/infrared/infrared_cli.c +++ b/applications/main/infrared/infrared_cli.c @@ -5,25 +5,21 @@ #include #include #include +#include #include "infrared_signal.h" #include "infrared_brute_force.h" -#include - #define INFRARED_CLI_BUF_SIZE 10 +#define INFRARED_ASSETS_FOLDER "infrared/assets" +#define INFRARED_BRUTE_FORCE_DUMMY_INDEX 0 DICT_DEF2(dict_signals, FuriString*, FURI_STRING_OPLIST, int, M_DEFAULT_OPLIST) -enum RemoteTypes { TV = 0, AC = 1 }; - 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_list_remote_signals(enum RemoteTypes remote_type); -static void - infrared_cli_brute_force_signals(Cli* cli, enum RemoteTypes remote_type, FuriString* signal); static const struct { const char* cmd; @@ -87,8 +83,10 @@ static void infrared_cli_print_usage(void) { INFRARED_MIN_FREQUENCY, INFRARED_MAX_FREQUENCY); printf("\tir decode []\r\n"); - printf("\tir universal \r\n"); - printf("\tir universal list \r\n"); + printf("\tir universal \r\n"); + printf("\tir universal list \r\n"); + // TODO: Do not hardcode universal remote names + printf("\tAvailable universal remotes: tv audio ac\r\n"); } static void infrared_cli_start_ir_rx(Cli* cli, FuriString* args) { @@ -356,89 +354,31 @@ static void infrared_cli_process_decode(Cli* cli, FuriString* args) { furi_record_close(RECORD_STORAGE); } -static void infrared_cli_process_universal(Cli* cli, FuriString* args) { - enum RemoteTypes Remote; - - FuriString* command; - FuriString* remote; - FuriString* signal; - command = furi_string_alloc(); - remote = furi_string_alloc(); - signal = furi_string_alloc(); - - do { - if(!args_read_string_and_trim(args, command)) { - infrared_cli_print_usage(); - break; - } - - if(furi_string_cmp_str(command, "list") == 0) { - args_read_string_and_trim(args, remote); - if(furi_string_cmp_str(remote, "tv") == 0) { - Remote = TV; - } else if(furi_string_cmp_str(remote, "ac") == 0) { - Remote = AC; - } else { - printf("Invalid remote type.\r\n"); - break; - } - infrared_cli_list_remote_signals(Remote); - break; - } - - if(furi_string_cmp_str(command, "tv") == 0) { - Remote = TV; - } else if(furi_string_cmp_str(command, "ac") == 0) { - Remote = AC; - } else { - printf("Invalid remote type.\r\n"); - break; - } - - args_read_string_and_trim(args, signal); - if(furi_string_empty(signal)) { - printf("Must supply a valid signal for type of remote selected.\r\n"); - break; - } - - infrared_cli_brute_force_signals(cli, Remote, signal); - break; - - } while(false); - - furi_string_free(command); - furi_string_free(remote); - furi_string_free(signal); -} - -static void infrared_cli_list_remote_signals(enum RemoteTypes remote_type) { - Storage* storage = furi_record_open(RECORD_STORAGE); - FlipperFormat* ff = flipper_format_buffered_file_alloc(storage); - dict_signals_t signals_dict; - FuriString* key; - const char* remote_file = NULL; - bool success = false; - int max = 1; - - switch(remote_type) { - case TV: - remote_file = EXT_PATH("infrared/assets/tv.ir"); - break; - case AC: - remote_file = EXT_PATH("infrared/assets/ac.ir"); - break; - default: - break; +static void infrared_cli_list_remote_signals(FuriString* remote_name) { + if(furi_string_empty(remote_name)) { + printf("Missing remote name.\r\n"); + return; } - dict_signals_init(signals_dict); - key = furi_string_alloc(); + Storage* storage = furi_record_open(RECORD_STORAGE); + FlipperFormat* ff = flipper_format_buffered_file_alloc(storage); + FuriString* remote_path = furi_string_alloc_printf( + "%s/%s.ir", EXT_PATH(INFRARED_ASSETS_FOLDER), furi_string_get_cstr(remote_name)); + + do { + if(!flipper_format_buffered_file_open_existing(ff, furi_string_get_cstr(remote_path))) { + printf("Invalid remote name.\r\n"); + break; + } + + dict_signals_t signals_dict; + dict_signals_init(signals_dict); + + FuriString* key = furi_string_alloc(); + FuriString* signal_name = furi_string_alloc(); - success = flipper_format_buffered_file_open_existing(ff, remote_file); - if(success) { - FuriString* signal_name; - signal_name = furi_string_alloc(); printf("Valid signals:\r\n"); + int max = 1; while(flipper_format_read_string(ff, "name", signal_name)) { furi_string_set_str(key, furi_string_get_cstr(signal_name)); int* v = dict_signals_get(signals_dict, key); @@ -449,57 +389,57 @@ static void infrared_cli_list_remote_signals(enum RemoteTypes remote_type) { dict_signals_set_at(signals_dict, key, 1); } } + dict_signals_it_t it; for(dict_signals_it(it, signals_dict); !dict_signals_end_p(it); dict_signals_next(it)) { const struct dict_signals_pair_s* pair = dict_signals_cref(it); printf("\t%s\r\n", furi_string_get_cstr(pair->key)); } - furi_string_free(signal_name); - } - furi_string_free(key); - dict_signals_clear(signals_dict); + furi_string_free(key); + furi_string_free(signal_name); + dict_signals_clear(signals_dict); + + } while(false); + flipper_format_free(ff); + furi_string_free(remote_path); furi_record_close(RECORD_STORAGE); } static void - infrared_cli_brute_force_signals(Cli* cli, enum RemoteTypes remote_type, FuriString* signal) { + infrared_cli_brute_force_signals(Cli* cli, FuriString* remote_name, FuriString* signal_name) { InfraredBruteForce* brute_force = infrared_brute_force_alloc(); - const char* remote_file = NULL; - uint32_t i = 0; - bool success = false; + FuriString* remote_path = furi_string_alloc_printf( + "%s/%s.ir", EXT_PATH(INFRARED_ASSETS_FOLDER), furi_string_get_cstr(remote_name)); - switch(remote_type) { - case TV: - remote_file = EXT_PATH("infrared/assets/tv.ir"); - break; - case AC: - remote_file = EXT_PATH("infrared/assets/ac.ir"); - break; - default: - break; - } + infrared_brute_force_set_db_filename(brute_force, furi_string_get_cstr(remote_path)); + infrared_brute_force_add_record( + brute_force, INFRARED_BRUTE_FORCE_DUMMY_INDEX, furi_string_get_cstr(signal_name)); - infrared_brute_force_set_db_filename(brute_force, remote_file); - infrared_brute_force_add_record(brute_force, i++, furi_string_get_cstr(signal)); - - success = infrared_brute_force_calculate_messages(brute_force); - if(success) { - uint32_t record_count; - uint32_t index = 0; - int records_sent = 0; - bool running = false; - - running = infrared_brute_force_start(brute_force, index, &record_count); - if(record_count <= 0) { - printf("Invalid signal.\n"); - infrared_brute_force_reset(brute_force); - return; + do { + if(furi_string_empty(signal_name)) { + printf("Missing signal name.\r\n"); + break; + } + if(!infrared_brute_force_calculate_messages(brute_force)) { + printf("Invalid remote name.\r\n"); + break; } - printf("Sending %ld codes to the tv.\r\n", record_count); + uint32_t record_count; + bool running = infrared_brute_force_start( + brute_force, INFRARED_BRUTE_FORCE_DUMMY_INDEX, &record_count); + + if(record_count <= 0) { + printf("Invalid signal name.\r\n"); + break; + } + + printf("Sending %ld signal(s)...\r\n", record_count); printf("Press Ctrl-C to stop.\r\n"); + + int records_sent = 0; while(running) { running = infrared_brute_force_send_next(brute_force); @@ -510,14 +450,35 @@ static void } infrared_brute_force_stop(brute_force); - } else { - printf("Invalid signal.\r\n"); - } + } while(false); + furi_string_free(remote_path); infrared_brute_force_reset(brute_force); infrared_brute_force_free(brute_force); } +static void infrared_cli_process_universal(Cli* cli, FuriString* args) { + FuriString* arg1 = furi_string_alloc(); + FuriString* arg2 = furi_string_alloc(); + + do { + if(!args_read_string_and_trim(args, arg1)) break; + if(!args_read_string_and_trim(args, arg2)) break; + } while(false); + + if(furi_string_empty(arg1)) { + printf("Wrong arguments.\r\n"); + infrared_cli_print_usage(); + } else if(furi_string_equal_str(arg1, "list")) { + infrared_cli_list_remote_signals(arg2); + } else { + infrared_cli_brute_force_signals(cli, arg1, arg2); + } + + furi_string_free(arg1); + furi_string_free(arg2); +} + static void infrared_cli_start_ir(Cli* cli, FuriString* args, void* context) { UNUSED(context); if(furi_hal_infrared_is_busy()) { diff --git a/applications/main/infrared/scenes/infrared_scene_config.h b/applications/main/infrared/scenes/infrared_scene_config.h index 22125fb79..111fd2d31 100644 --- a/applications/main/infrared/scenes/infrared_scene_config.h +++ b/applications/main/infrared/scenes/infrared_scene_config.h @@ -16,6 +16,7 @@ ADD_SCENE(infrared, remote_list, RemoteList) ADD_SCENE(infrared, universal, Universal) ADD_SCENE(infrared, universal_tv, UniversalTV) ADD_SCENE(infrared, universal_ac, UniversalAC) +ADD_SCENE(infrared, universal_audio, UniversalAudio) ADD_SCENE(infrared, debug, Debug) ADD_SCENE(infrared, error_databases, ErrorDatabases) ADD_SCENE(infrared, rpc, Rpc) diff --git a/applications/main/infrared/scenes/infrared_scene_universal.c b/applications/main/infrared/scenes/infrared_scene_universal.c index 2bd7082c4..914360d78 100644 --- a/applications/main/infrared/scenes/infrared_scene_universal.c +++ b/applications/main/infrared/scenes/infrared_scene_universal.c @@ -21,6 +21,12 @@ void infrared_scene_universal_on_enter(void* context) { SubmenuIndexUniversalTV, infrared_scene_universal_submenu_callback, context); + submenu_add_item( + submenu, + "Audio Players", + SubmenuIndexUniversalAudio, + infrared_scene_universal_submenu_callback, + context); submenu_add_item( submenu, "Air Conditioners", @@ -45,7 +51,7 @@ bool infrared_scene_universal_on_event(void* context, SceneManagerEvent event) { scene_manager_next_scene(scene_manager, InfraredSceneUniversalAC); consumed = true; } else if(event.event == SubmenuIndexUniversalAudio) { - //TODO Implement Audio universal remote + scene_manager_next_scene(scene_manager, InfraredSceneUniversalAudio); consumed = true; } } diff --git a/applications/main/infrared/scenes/infrared_scene_universal_audio.c b/applications/main/infrared/scenes/infrared_scene_universal_audio.c new file mode 100644 index 000000000..00c86fff4 --- /dev/null +++ b/applications/main/infrared/scenes/infrared_scene_universal_audio.c @@ -0,0 +1,133 @@ +#include "../infrared_i.h" + +#include "common/infrared_scene_universal_common.h" + +void infrared_scene_universal_audio_on_enter(void* context) { + infrared_scene_universal_common_on_enter(context); + + Infrared* infrared = context; + ButtonPanel* button_panel = infrared->button_panel; + InfraredBruteForce* brute_force = infrared->brute_force; + + infrared_brute_force_set_db_filename(brute_force, EXT_PATH("infrared/assets/audio.ir")); + + button_panel_reserve(button_panel, 2, 4); + uint32_t i = 0; + button_panel_add_item( + button_panel, + i, + 0, + 0, + 3, + 11, + &I_Power_25x27, + &I_Power_hvr_25x27, + infrared_scene_universal_common_item_callback, + context); + infrared_brute_force_add_record(brute_force, i++, "Power"); + button_panel_add_item( + button_panel, + i, + 1, + 0, + 36, + 11, + &I_Mute_25x27, + &I_Mute_hvr_25x27, + infrared_scene_universal_common_item_callback, + context); + infrared_brute_force_add_record(brute_force, i++, "Mute"); + button_panel_add_item( + button_panel, + i, + 0, + 1, + 3, + 41, + &I_Play_25x27, + &I_Play_hvr_25x27, + infrared_scene_universal_common_item_callback, + context); + infrared_brute_force_add_record(brute_force, i++, "Play"); + button_panel_add_item( + button_panel, + i, + 1, + 1, + 36, + 41, + &I_Pause_25x27, + &I_Pause_hvr_25x27, + infrared_scene_universal_common_item_callback, + context); + infrared_brute_force_add_record(brute_force, i++, "Pause"); + button_panel_add_item( + button_panel, + i, + 0, + 2, + 3, + 71, + &I_TrackPrev_25x27, + &I_TrackPrev_hvr_25x27, + infrared_scene_universal_common_item_callback, + context); + infrared_brute_force_add_record(brute_force, i++, "Prev"); + button_panel_add_item( + button_panel, + i, + 1, + 2, + 36, + 71, + &I_TrackNext_25x27, + &I_TrackNext_hvr_25x27, + infrared_scene_universal_common_item_callback, + context); + infrared_brute_force_add_record(brute_force, i++, "Next"); + button_panel_add_item( + button_panel, + i, + 0, + 3, + 3, + 101, + &I_Vol_down_25x27, + &I_Vol_down_hvr_25x27, + infrared_scene_universal_common_item_callback, + context); + infrared_brute_force_add_record(brute_force, i++, "Vol_dn"); + button_panel_add_item( + button_panel, + i, + 1, + 3, + 36, + 101, + &I_Vol_up_25x27, + &I_Vol_up_hvr_25x27, + infrared_scene_universal_common_item_callback, + context); + infrared_brute_force_add_record(brute_force, i++, "Vol_up"); + + button_panel_add_label(button_panel, 1, 8, FontPrimary, "Mus. remote"); + + view_set_orientation(view_stack_get_view(infrared->view_stack), ViewOrientationVertical); + view_dispatcher_switch_to_view(infrared->view_dispatcher, InfraredViewStack); + + infrared_show_loading_popup(infrared, true); + bool success = infrared_brute_force_calculate_messages(brute_force); + infrared_show_loading_popup(infrared, false); + + if(!success) { + scene_manager_next_scene(infrared->scene_manager, InfraredSceneErrorDatabases); + } +} + +bool infrared_scene_universal_audio_on_event(void* context, SceneManagerEvent event) { + return infrared_scene_universal_common_on_event(context, event); +} + +void infrared_scene_universal_audio_on_exit(void* context) { + infrared_scene_universal_common_on_exit(context); +} diff --git a/assets/icons/Infrared/Pause_25x27.png b/assets/icons/Infrared/Pause_25x27.png new file mode 100644 index 0000000000000000000000000000000000000000..a371ba81718e864efe908e61f34bc842079cc20d GIT binary patch literal 3634 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 literal 0 HcmV?d00001 diff --git a/assets/icons/Infrared/Pause_hvr_25x27.png b/assets/icons/Infrared/Pause_hvr_25x27.png new file mode 100644 index 0000000000000000000000000000000000000000..472d583db0ff9adc5ef810675c8b915c6e5a899c GIT binary patch literal 3623 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 HpN{x9W3^Ap literal 0 HcmV?d00001 diff --git a/assets/icons/Infrared/Play_hvr_25x27.png b/assets/icons/Infrared/Play_hvr_25x27.png new file mode 100644 index 0000000000000000000000000000000000000000..6708dcdbf2c7ac0c656bbb1c2441fb0f4d6fc9c5 GIT binary patch literal 3643 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 SxG~3f0odCdwl1|i74;vAA8iN# literal 0 HcmV?d00001 diff --git a/assets/icons/Infrared/TrackNext_hvr_25x27.png b/assets/icons/Infrared/TrackNext_hvr_25x27.png new file mode 100644 index 0000000000000000000000000000000000000000..a4de4fc3cbe582349b8c79bbb7ec6e931e8bc792 GIT binary patch literal 3639 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 W@<`O{ko&lf02|BW7A0mL7ybo9V{1nM literal 0 HcmV?d00001 diff --git a/assets/icons/Infrared/TrackPrev_hvr_25x27.png b/assets/icons/Infrared/TrackPrev_hvr_25x27.png new file mode 100644 index 0000000000000000000000000000000000000000..838055341e568fed60b3e28a280fe839d2c9db9e GIT binary patch literal 3644 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 in order to keep the library organised. + +When done, open a pull request containing the changed file. + ## Air Conditioners ### Recording signals Air conditioners differ from most other infrared-controlled devices because their state is tracked by the remote. @@ -31,7 +45,7 @@ Finally, record the `Off` signal: 4. Save the resulting signal under the name `Off`. The resulting remote file should now contain 6 signals. Any of them can be omitted, but that will mean that this functionality will not be used. -Test the file against the actual device. Every signal must do what it's supposed to. +Test the file against the actual device. Make sure that every signal does what it's supposed to. If everything checks out, append these signals **to the end** of the [A/C universal remote file](/assets/resources/infrared/assets/ac.ir). From 85d341104f32b509c70ed8808655697d25963df3 Mon Sep 17 00:00:00 2001 From: Vladimir <74153654+touchscan@users.noreply.github.com> Date: Fri, 28 Oct 2022 19:50:07 +0300 Subject: [PATCH 11/49] Update ac.ir (#1945) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added conditioner Saturn CS-TL09CHR ir signals (Dh, Cool_hi, Cool_lo, Heat_hi, Heat_lo, Off) Co-authored-by: あく --- assets/resources/infrared/assets/ac.ir | 37 ++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/assets/resources/infrared/assets/ac.ir b/assets/resources/infrared/assets/ac.ir index 97c384591..7ef953059 100644 --- a/assets/resources/infrared/assets/ac.ir +++ b/assets/resources/infrared/assets/ac.ir @@ -111,4 +111,41 @@ type: raw frequency: 38000 duty_cycle: 0.330000 data: 9106 4398 731 499 706 500 705 502 702 504 701 505 701 505 701 1606 701 505 701 1607 701 505 701 506 700 1607 700 506 700 506 700 505 700 505 701 506 700 506 700 506 699 506 700 506 700 1607 700 506 700 506 700 506 700 505 701 506 700 506 700 1608 699 506 700 1608 699 506 700 506 700 1608 700 506 700 19941 701 1606 700 505 701 505 701 506 700 505 700 506 700 506 700 506 700 506 700 506 700 506 700 506 700 506 701 506 700 506 700 506 700 506 700 506 700 506 700 506 700 506 700 506 700 506 700 506 700 506 700 506 699 506 700 506 700 1608 700 1607 700 506 700 506 700 +# +# Model: Saturn CS-TL09CHR +name: Dh +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 3014 1708 488 1059 464 1085 461 362 461 363 460 387 436 1085 462 362 461 402 436 1084 463 1083 463 364 459 1083 463 363 460 386 437 1082 518 1044 464 386 437 1108 439 1108 438 386 464 360 463 1082 464 360 490 352 459 1084 462 362 460 363 460 363 460 364 459 364 459 364 459 380 459 364 459 364 459 364 460 364 459 364 459 364 459 364 459 380 459 364 459 365 458 1088 459 364 459 365 458 1088 458 365 458 380 459 365 458 1088 459 365 458 364 459 365 459 365 458 365 458 380 458 1088 459 1088 459 1088 458 365 458 365 458 365 458 365 458 381 458 365 458 365 458 365 458 365 458 365 459 365 458 365 458 381 458 366 457 366 457 366 457 366 457 366 458 365 458 366 458 381 457 366 457 366 457 366 457 366 457 366 457 366 457 366 458 382 457 366 457 366 457 367 456 367 457 367 456 367 456 390 433 406 433 367 456 367 456 390 433 391 433 390 433 390 433 390 433 406 433 391 432 1114 432 391 432 391 432 391 432 391 432 1114 433 396 433 +# +name: Cool_hi +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 3011 1710 462 1084 462 1085 486 356 467 356 444 364 484 1060 462 364 483 357 458 1085 461 1085 461 388 435 1085 461 388 435 388 435 1084 462 1099 463 387 436 1084 462 1085 461 388 461 362 461 1084 462 362 461 378 460 1087 459 365 458 365 458 366 457 366 457 366 457 366 457 382 457 366 457 366 457 366 457 367 456 367 456 367 456 367 456 382 457 367 456 367 456 1090 457 367 456 367 456 1090 457 367 456 382 457 1090 456 1090 457 367 456 367 456 367 456 367 456 367 456 382 457 1091 456 1091 456 1091 456 1090 456 367 456 367 456 367 457 382 457 367 456 367 456 367 456 367 456 368 456 367 456 368 455 383 456 367 456 367 456 368 455 368 455 368 455 368 456 368 455 383 456 368 455 368 455 368 455 368 455 368 455 368 455 368 455 383 456 368 455 368 455 368 455 368 455 368 455 368 455 368 455 384 455 368 455 368 455 368 455 368 455 368 455 368 455 368 455 383 456 1091 456 1091 456 368 456 1091 455 368 455 368 455 1091 456 373 456 +# +name: Cool_lo +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 3012 1709 462 1083 463 1084 463 361 462 362 484 355 469 1059 463 387 436 402 437 1083 464 1083 464 362 462 1080 520 354 469 354 469 1026 521 1068 518 354 390 1108 439 1108 438 386 463 360 464 1081 465 359 464 375 463 1083 463 361 462 362 461 363 460 363 460 364 459 364 459 380 459 364 459 364 459 365 458 365 458 365 458 365 458 365 458 380 459 365 458 365 458 1089 458 365 459 365 458 1089 458 366 457 382 456 1090 457 1113 433 390 433 390 433 390 433 390 433 390 433 405 434 390 433 390 433 390 433 1113 434 390 433 390 433 390 433 405 434 390 433 390 433 390 434 390 433 390 433 390 433 390 433 405 434 390 433 390 433 390 433 390 433 390 433 390 433 390 434 405 433 390 433 390 433 390 433 390 433 390 433 390 433 390 433 406 433 390 433 390 433 390 433 390 433 390 433 391 432 391 432 406 433 390 433 391 432 391 432 391 433 390 433 390 433 391 432 406 433 391 432 391 432 1114 433 391 432 391 432 391 432 1114 433 396 433 +# +name: Heat_hi +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 3011 1712 461 1087 459 1088 459 364 459 389 434 366 458 1086 461 388 435 404 435 1086 460 1085 461 388 435 1085 461 365 458 388 435 1084 462 1098 464 387 436 1084 462 1084 462 388 461 362 461 1084 462 362 461 378 460 1086 460 364 459 365 458 365 458 365 458 366 457 365 458 381 458 366 457 366 458 366 457 366 457 366 457 366 457 365 458 381 458 366 458 366 457 1089 458 366 457 366 457 1089 458 366 457 381 458 1089 458 366 457 366 457 366 457 366 458 366 457 366 457 381 458 366 457 366 457 366 457 366 457 366 457 366 457 366 457 381 458 366 457 366 457 366 458 366 457 366 457 366 457 366 457 382 457 366 457 366 457 366 457 366 457 366 457 366 457 366 457 382 457 366 457 366 457 367 456 367 456 367 457 366 457 366 457 382 457 367 457 366 457 367 457 366 457 367 457 366 457 366 457 382 457 367 456 367 456 367 456 367 456 367 456 367 456 367 456 382 457 367 456 1090 457 367 456 1091 456 1090 457 1090 456 367 456 373 456 +# +name: Heat_lo +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 3045 1677 493 1023 524 1024 522 354 469 354 469 354 469 1026 576 353 470 350 487 1001 492 1054 492 354 469 1054 492 355 468 354 469 1054 492 1070 491 354 468 1056 438 1109 438 386 437 385 438 1107 439 385 438 400 464 1081 465 359 464 360 463 361 462 362 461 388 435 388 435 404 434 389 434 389 434 389 434 389 434 389 434 389 434 389 434 405 434 389 434 389 434 1112 434 389 434 389 434 1113 434 389 434 405 434 1112 435 389 434 366 458 365 458 365 458 365 458 365 458 381 458 365 458 366 457 365 458 1089 457 366 457 366 457 366 457 382 456 367 433 390 433 391 432 391 432 391 432 391 432 391 432 406 433 391 432 391 432 390 433 391 432 391 432 391 432 391 432 406 433 391 432 391 433 391 432 391 432 391 432 391 432 391 432 407 432 391 432 391 432 391 432 392 431 391 433 391 432 391 432 430 409 393 430 392 431 392 431 392 432 391 457 367 432 392 431 408 431 392 431 1138 433 391 408 392 431 392 456 367 456 1113 408 421 433 +# +name: Off +type: raw +frequency: 38000 +duty_cycle: 0.330000 +data: 3013 1709 463 1085 462 1084 463 387 461 355 469 355 444 1084 463 387 461 378 436 1084 462 1084 487 355 469 1059 463 387 460 355 444 1083 463 1098 464 386 437 1083 463 1083 489 361 463 360 463 1082 464 361 462 376 462 1085 461 363 460 364 459 364 459 365 458 365 459 364 459 380 459 365 458 365 458 365 458 365 458 365 458 365 458 365 459 380 458 365 459 365 458 365 459 365 458 365 458 1089 458 365 458 380 459 1088 459 365 458 365 458 365 458 365 459 365 458 365 458 381 458 365 458 365 458 365 458 1089 458 365 458 365 458 365 458 381 458 365 458 365 458 365 458 365 458 365 458 365 458 365 458 381 457 366 457 366 457 366 457 366 457 366 458 365 458 366 457 381 458 366 458 366 457 366 457 366 457 366 457 366 457 366 457 381 458 366 457 366 457 366 457 366 457 366 457 366 457 366 457 382 457 366 457 366 457 366 457 366 457 366 457 367 457 366 457 382 457 367 456 1090 457 1090 456 1090 457 1090 457 1090 456 367 457 372 457 From 104a1998a5e2f77cea99c31311541864fb98d4c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=82=E3=81=8F?= Date: Tue, 1 Nov 2022 19:27:25 +0900 Subject: [PATCH 12/49] Furi: raise bkpt only if debug session initiated, add debug support for release builds (#1957) * Fix hard crash on some custom firmwares in RELEASE mode * Furi: anya wa erai, anya wa eleganto, raise bkpt only if debug session initiated, add debug support for release builds Co-authored-by: DerSkythe --- furi/core/check.c | 59 ++++++++++++++++++++++++++++++++--------------- 1 file changed, 41 insertions(+), 18 deletions(-) diff --git a/furi/core/check.c b/furi/core/check.c index 00c20575a..1c2a005f3 100644 --- a/furi/core/check.c +++ b/furi/core/check.c @@ -1,6 +1,7 @@ #include "check.h" #include "common_defines.h" +#include #include #include #include @@ -24,16 +25,30 @@ PLACE_IN_SECTION("MB_MEM2") uint32_t __furi_check_registers[12] = {0}; : \ : "memory"); -// Restore registers and halt MCU -#define RESTORE_REGISTERS_AND_HALT_MCU() \ - asm volatile("ldr r12, =__furi_check_registers \n" \ - "ldm r12, {r0-r11} \n" \ +/** Restore registers and halt MCU + * + * - Always use it with GET_MESSAGE_AND_STORE_REGISTERS + * - If debugger is(was) connected this routine will raise bkpt + * - If debugger is not connected then endless loop + * + */ +#define RESTORE_REGISTERS_AND_HALT_MCU(debug) \ + register const bool r0 asm("r0") = debug; \ + asm volatile("cbnz r0, with_debugger%= \n" \ + "ldr r12, =__furi_check_registers\n" \ + "ldm r12, {r0-r11} \n" \ "loop%=: \n" \ - "bkpt 0x00 \n" \ "wfi \n" \ - "b loop%= \n" \ - : \ + "b loop%= \n" \ + "with_debugger%=: \n" \ + "ldr r12, =__furi_check_registers\n" \ + "ldm r12, {r0-r11} \n" \ + "debug_loop%=: \n" \ + "bkpt 0x00 \n" \ + "wfi \n" \ + "b debug_loop%= \n" \ : \ + : "r"(r0) \ : "memory"); extern size_t xPortGetTotalHeapSize(void); @@ -96,16 +111,19 @@ FURI_NORETURN void __furi_crash() { } __furi_print_heap_info(); -#ifdef FURI_DEBUG - furi_hal_console_puts("\r\nSystem halted. Connect debugger for more info\r\n"); - furi_hal_console_puts("\033[0m\r\n"); - RESTORE_REGISTERS_AND_HALT_MCU(); -#else - furi_hal_rtc_set_fault_data((uint32_t)__furi_check_message); - furi_hal_console_puts("\r\nRebooting system.\r\n"); - furi_hal_console_puts("\033[0m\r\n"); - furi_hal_power_reset(); -#endif + // Check if debug enabled by DAP + // https://developer.arm.com/documentation/ddi0403/d/Debug-Architecture/ARMv7-M-Debug/Debug-register-support-in-the-SCS/Debug-Halting-Control-and-Status-Register--DHCSR?lang=en + bool debug = CoreDebug->DHCSR & CoreDebug_DHCSR_C_DEBUGEN_Msk; + if(debug) { + furi_hal_console_puts("\r\nSystem halted. Connect debugger for more info\r\n"); + furi_hal_console_puts("\033[0m\r\n"); + RESTORE_REGISTERS_AND_HALT_MCU(debug); + } else { + furi_hal_rtc_set_fault_data((uint32_t)__furi_check_message); + furi_hal_console_puts("\r\nRebooting system.\r\n"); + furi_hal_console_puts("\033[0m\r\n"); + furi_hal_power_reset(); + } __builtin_unreachable(); } @@ -124,6 +142,11 @@ FURI_NORETURN void __furi_halt() { furi_hal_console_puts(__furi_check_message); furi_hal_console_puts("\r\nSystem halted. Bye-bye!\r\n"); furi_hal_console_puts("\033[0m\r\n"); - RESTORE_REGISTERS_AND_HALT_MCU(); + + // Check if debug enabled by DAP + // https://developer.arm.com/documentation/ddi0403/d/Debug-Architecture/ARMv7-M-Debug/Debug-register-support-in-the-SCS/Debug-Halting-Control-and-Status-Register--DHCSR?lang=en + bool debug = CoreDebug->DHCSR & CoreDebug_DHCSR_C_DEBUGEN_Msk; + RESTORE_REGISTERS_AND_HALT_MCU(debug); + __builtin_unreachable(); } From abfa804ae0bf8c126b5dd78a92cc59c9cec14103 Mon Sep 17 00:00:00 2001 From: "Vasyl \"vk\" Kaigorodov" Date: Wed, 2 Nov 2022 15:36:17 +0100 Subject: [PATCH 13/49] Gui: refactor buttons remapping (#1949) * Gui: refactor buttons remapping Instead of calling 3 separate functions with a ton of switch/case statements, use a 2-dimensional lookup table to remap buttons based on the orientation. * Gui: cleanup input mapping and fix incorrect asserts * SnakeGame: handle input special case Co-authored-by: Aleksandr Kutuzov --- applications/plugins/snake_game/snake_game.c | 2 + applications/services/gui/view_port.c | 110 +++++++----------- applications/services/gui/view_port.h | 1 + applications/services/input/input.c | 3 +- applications/services/input/input.h | 1 + .../targets/f7/furi_hal/furi_hal_resources.h | 1 + 6 files changed, 51 insertions(+), 67 deletions(-) diff --git a/applications/plugins/snake_game/snake_game.c b/applications/plugins/snake_game/snake_game.c index 283d017ed..f9309c280 100644 --- a/applications/plugins/snake_game/snake_game.c +++ b/applications/plugins/snake_game/snake_game.c @@ -380,6 +380,8 @@ int32_t snake_game_app(void* p) { case InputKeyBack: processing = false; break; + default: + break; } } } else if(event.type == EventTypeTick) { diff --git a/applications/services/gui/view_port.c b/applications/services/gui/view_port.c index baa8f7bd2..ffd01450b 100644 --- a/applications/services/gui/view_port.c +++ b/applications/services/gui/view_port.c @@ -7,61 +7,51 @@ // TODO add mutex to view_port ops -static void view_port_remap_buttons_vertical(InputEvent* event) { - switch(event->key) { - case InputKeyUp: - event->key = InputKeyRight; - break; - case InputKeyDown: - event->key = InputKeyLeft; - break; - case InputKeyRight: - event->key = InputKeyDown; - break; - case InputKeyLeft: - event->key = InputKeyUp; - break; - default: - break; - } -} +_Static_assert(ViewPortOrientationMAX == 4, "Incorrect ViewPortOrientation count"); +_Static_assert( + (ViewPortOrientationHorizontal == 0 && ViewPortOrientationHorizontalFlip == 1 && + ViewPortOrientationVertical == 2 && ViewPortOrientationVerticalFlip == 3), + "Incorrect ViewPortOrientation order"); +_Static_assert(InputKeyMAX == 6, "Incorrect InputKey count"); +_Static_assert( + (InputKeyUp == 0 && InputKeyDown == 1 && InputKeyRight == 2 && InputKeyLeft == 3 && + InputKeyOk == 4 && InputKeyBack == 5), + "Incorrect InputKey order"); -static void view_port_remap_buttons_vertical_flip(InputEvent* event) { - switch(event->key) { - case InputKeyUp: - event->key = InputKeyLeft; - break; - case InputKeyDown: - event->key = InputKeyRight; - break; - case InputKeyRight: - event->key = InputKeyUp; - break; - case InputKeyLeft: - event->key = InputKeyDown; - break; - default: - break; - } -} +/** InputKey directional keys mappings for different screen orientations +* +*/ +static const InputKey view_port_input_mapping[ViewPortOrientationMAX][InputKeyMAX] = { + {InputKeyUp, + InputKeyDown, + InputKeyRight, + InputKeyLeft, + InputKeyOk, + InputKeyBack}, //ViewPortOrientationHorizontal + {InputKeyDown, + InputKeyUp, + InputKeyLeft, + InputKeyRight, + InputKeyOk, + InputKeyBack}, //ViewPortOrientationHorizontalFlip + {InputKeyRight, + InputKeyLeft, + InputKeyDown, + InputKeyUp, + InputKeyOk, + InputKeyBack}, //ViewPortOrientationVertical + {InputKeyLeft, + InputKeyRight, + InputKeyUp, + InputKeyDown, + InputKeyOk, + InputKeyBack}, //ViewPortOrientationVerticalFlip +}; -static void view_port_remap_buttons_horizontal_flip(InputEvent* event) { - switch(event->key) { - case InputKeyUp: - event->key = InputKeyDown; - break; - case InputKeyDown: - event->key = InputKeyUp; - break; - case InputKeyRight: - event->key = InputKeyLeft; - break; - case InputKeyLeft: - event->key = InputKeyRight; - break; - default: - break; - } +// Remaps directional pad buttons on Flipper based on ViewPort orientation +static void view_port_map_input(InputEvent* event, ViewPortOrientation orientation) { + furi_assert(orientation < ViewPortOrientationMAX && event->key < InputKeyMAX); + event->key = view_port_input_mapping[orientation][event->key]; } static void view_port_setup_canvas_orientation(const ViewPort* view_port, Canvas* canvas) { @@ -170,19 +160,7 @@ void view_port_input(ViewPort* view_port, InputEvent* event) { if(view_port->input_callback) { ViewPortOrientation orientation = view_port_get_orientation(view_port); - switch(orientation) { - case ViewPortOrientationHorizontalFlip: - view_port_remap_buttons_horizontal_flip(event); - break; - case ViewPortOrientationVertical: - view_port_remap_buttons_vertical(event); - break; - case ViewPortOrientationVerticalFlip: - view_port_remap_buttons_vertical_flip(event); - break; - default: - break; - } + view_port_map_input(event, orientation); view_port->input_callback(event, view_port->input_callback_context); } } diff --git a/applications/services/gui/view_port.h b/applications/services/gui/view_port.h index 169681ac0..703e99248 100644 --- a/applications/services/gui/view_port.h +++ b/applications/services/gui/view_port.h @@ -19,6 +19,7 @@ typedef enum { ViewPortOrientationHorizontalFlip, ViewPortOrientationVertical, ViewPortOrientationVerticalFlip, + ViewPortOrientationMAX, /**< Special value, don't use it */ } ViewPortOrientation; /** ViewPort Draw callback diff --git a/applications/services/input/input.c b/applications/services/input/input.c index 7b8433aef..d1aef9e8a 100644 --- a/applications/services/input/input.c +++ b/applications/services/input/input.c @@ -60,8 +60,9 @@ const char* input_get_type_name(InputType type) { return "Long"; case InputTypeRepeat: return "Repeat"; + default: + return "Unknown"; } - return "Unknown"; } int32_t input_srv(void* p) { diff --git a/applications/services/input/input.h b/applications/services/input/input.h index bd0ba3902..172b16361 100644 --- a/applications/services/input/input.h +++ b/applications/services/input/input.h @@ -22,6 +22,7 @@ typedef enum { InputTypeShort, /**< Short event, emitted after InputTypeRelease done withing INPUT_LONG_PRESS interval */ InputTypeLong, /**< Long event, emmited after INPUT_LONG_PRESS interval, asynchronouse to InputTypeRelease */ InputTypeRepeat, /**< Repeat event, emmited with INPUT_REPEATE_PRESS period after InputTypeLong event */ + InputTypeMAX, /**< Special value for exceptional */ } InputType; /** Input Event, dispatches with FuriPubSub */ diff --git a/firmware/targets/f7/furi_hal/furi_hal_resources.h b/firmware/targets/f7/furi_hal/furi_hal_resources.h index d16c567e6..8d53ec48a 100644 --- a/firmware/targets/f7/furi_hal/furi_hal_resources.h +++ b/firmware/targets/f7/furi_hal/furi_hal_resources.h @@ -20,6 +20,7 @@ typedef enum { InputKeyLeft, InputKeyOk, InputKeyBack, + InputKeyMAX, /**< Special value */ } InputKey; /* Light */ From ebc2b66372091d811278bcb6180bfa16b62cd46f Mon Sep 17 00:00:00 2001 From: hedger Date: Wed, 2 Nov 2022 19:15:40 +0400 Subject: [PATCH 14/49] fbt fixes for mfbt pt2 (#1951) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fbt: split sdk management code * scripts: fixed import handling * fbt: sdk: reformatted paths * scrips: dist: bundling libs as a build artifact * fbt: sdk: better path management * typo fix * fbt: sdk: minor path handling fixes * toolchain: fixed windows toolchain download * fbt: minor refactorin * fbt: moved sdk management code to extapps.scons * fbt: fixed sdk symbols header path; disabled -fstack-usage * fbt: changed pathing for .py scripts * fbt: changed SDK_HEADERS pathing; added libusb to SDK; added icon_i.h to SDK; added hw target to SDK meta * fbt: added libusb headers to SDK * picopass: include cleanup; api: added subghz/registry.h; api: added mbedtls to exported headers * picopass: fixed formatting * fbt: fixed COPRO_ASSETS_SCRIPT * sdk: added basic infrared apis * toolchain: added ufbt to list of legal fbtenv callers; updated error messages * fbt: changed manifest collection & icon processing code * fbt: simpler srcdir lookup * toolchain: path management fixes; fbt: fixes for fap private libs paths * scripts: toolchain: reworked download on Windows * toolchain: v17 * scripts: added colorlog for logging * Github: fix unit tests Co-authored-by: あく --- .github/workflows/unit_tests.yml | 8 +- SConstruct | 10 +- applications/plugins/picopass/125_10px.png | Bin 0 -> 308 bytes applications/plugins/picopass/application.fam | 2 +- .../plugins/picopass/picopass_worker_i.h | 2 - applications/plugins/picopass/rfal_picopass.c | 7 +- applications/services/gui/application.fam | 1 + firmware.scons | 66 ++++------ firmware/SConscript | 7 +- firmware/targets/f7/api_symbols.csv | 124 +++++++++++++++++- lib/SConscript | 12 +- lib/STM32CubeWB.scons | 2 +- lib/flipper_application/SConscript | 2 +- lib/flipper_format/SConscript | 4 +- lib/infrared/SConscript | 5 + lib/lfrfid/SConscript | 12 +- lib/libusb_stm32.scons | 4 + lib/mbedtls.scons | 4 + lib/misc.scons | 2 +- lib/print/SConscript | 2 +- lib/subghz/SConscript | 25 ++-- lib/subghz/registry.h | 8 ++ lib/toolbox/SConscript | 34 ++--- scripts/fbt/util.py | 15 ++- scripts/fbt_tools/crosscc.py | 15 +++ scripts/fbt_tools/fbt_apps.py | 38 +++--- scripts/fbt_tools/fbt_assets.py | 20 ++- scripts/fbt_tools/fbt_dist.py | 9 +- scripts/fbt_tools/fbt_extapps.py | 24 +++- scripts/fbt_tools/fbt_sdk.py | 46 ++++--- scripts/fbt_tools/fbt_version.py | 5 +- scripts/fbt_tools/fwbin.py | 3 +- scripts/flipper/app.py | 16 ++- scripts/sconsdist.py | 6 +- scripts/testing/await_flipper.py | 0 scripts/testing/units.py | 0 scripts/toolchain/fbtenv.cmd | 13 +- scripts/toolchain/fbtenv.sh | 24 ++-- .../toolchain/windows-toolchain-download.ps1 | 38 ++++-- site_scons/cc.scons | 5 +- site_scons/environ.scons | 36 ++--- site_scons/extapps.scons | 38 +++++- 42 files changed, 459 insertions(+), 235 deletions(-) create mode 100644 applications/plugins/picopass/125_10px.png mode change 100644 => 100755 scripts/testing/await_flipper.py mode change 100644 => 100755 scripts/testing/units.py diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index b5bf10004..a7671f0f9 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -31,22 +31,26 @@ jobs: id: connect if: steps.compile.outcome == 'success' run: | - python3 ./scripts/testing/await_flipper.py ${{steps.device.outputs.flipper}} + . scripts/toolchain/fbtenv.sh + ./scripts/testing/await_flipper.py ${{steps.device.outputs.flipper}} - name: 'Format flipper SD card' id: format if: steps.connect.outcome == 'success' run: | + . scripts/toolchain/fbtenv.sh ./scripts/storage.py -p ${{steps.device.outputs.flipper}} format_ext - name: 'Copy assets and unit tests data to flipper' id: copy if: steps.format.outcome == 'success' run: | + . scripts/toolchain/fbtenv.sh ./scripts/storage.py -p ${{steps.device.outputs.flipper}} send assets/resources /ext ./scripts/storage.py -p ${{steps.device.outputs.flipper}} send assets/unit_tests /ext/unit_tests - name: 'Run units and validate results' if: steps.copy.outcome == 'success' run: | - python3 ./scripts/testing/units.py ${{steps.device.outputs.flipper}} + . scripts/toolchain/fbtenv.sh + ./scripts/testing/units.py ${{steps.device.outputs.flipper}} diff --git a/SConstruct b/SConstruct index 448df9715..13a698c81 100644 --- a/SConstruct +++ b/SConstruct @@ -33,10 +33,6 @@ coreenv = SConscript( ) SConscript("site_scons/cc.scons", exports={"ENV": coreenv}) -# Store root dir in environment for certain tools -coreenv["ROOT_DIR"] = Dir(".") - - # Create a separate "dist" environment and add construction envs to it distenv = coreenv.Clone( tools=[ @@ -233,13 +229,13 @@ distenv.PhonyTarget( # Linter distenv.PhonyTarget( "lint", - "${PYTHON3} scripts/lint.py check ${LINT_SOURCES}", + "${PYTHON3} ${FBT_SCRIPT_DIR}/lint.py check ${LINT_SOURCES}", LINT_SOURCES=firmware_env["LINT_SOURCES"], ) distenv.PhonyTarget( "format", - "${PYTHON3} scripts/lint.py format ${LINT_SOURCES}", + "${PYTHON3} ${FBT_SCRIPT_DIR}/lint.py format ${LINT_SOURCES}", LINT_SOURCES=firmware_env["LINT_SOURCES"], ) @@ -280,7 +276,7 @@ distenv.PhonyTarget( ) # Start Flipper CLI via PySerial's miniterm -distenv.PhonyTarget("cli", "${PYTHON3} scripts/serial_cli.py") +distenv.PhonyTarget("cli", "${PYTHON3} ${FBT_SCRIPT_DIR}/serial_cli.py") # Find blackmagic probe diff --git a/applications/plugins/picopass/125_10px.png b/applications/plugins/picopass/125_10px.png new file mode 100644 index 0000000000000000000000000000000000000000..ce01284a2c1f3eb413f581b84f1fb3f3a2a7223b GIT binary patch literal 308 zcmeAS@N?(olHy`uVBq!ia0vp^AT}2xkYHHq`AGmsv7|ftIx;Y9?C1WI$O_~uBzpw; zGB8xBF)%c=FfjZA3N^f7U???UV0e|lz+g3lfkC`r&aOZkpafHrx4R1i<>&pI=m5)bWZjP>yH&963)5S4_<9hOs!iI #include -#include -#include #include #include diff --git a/applications/plugins/picopass/rfal_picopass.c b/applications/plugins/picopass/rfal_picopass.c index 50cd4e95d..ac66cb92d 100644 --- a/applications/plugins/picopass/rfal_picopass.c +++ b/applications/plugins/picopass/rfal_picopass.c @@ -1,5 +1,4 @@ #include "rfal_picopass.h" -#include "utils.h" #define RFAL_PICOPASS_TXRX_FLAGS \ (FURI_HAL_NFC_LL_TXRX_FLAGS_CRC_TX_MANUAL | FURI_HAL_NFC_LL_TXRX_FLAGS_AGC_ON | \ @@ -97,7 +96,7 @@ FuriHalNfcReturn rfalPicoPassPollerSelect(uint8_t* csn, rfalPicoPassSelectRes* s rfalPicoPassSelectReq selReq; selReq.CMD = RFAL_PICOPASS_CMD_SELECT; - ST_MEMCPY(selReq.CSN, csn, RFAL_PICOPASS_UID_LEN); + memcpy(selReq.CSN, csn, RFAL_PICOPASS_UID_LEN); uint16_t recvLen = 0; uint32_t flags = RFAL_PICOPASS_TXRX_FLAGS; uint32_t fwt = furi_hal_nfc_ll_ms2fc(20); @@ -146,8 +145,8 @@ FuriHalNfcReturn rfalPicoPassPollerCheck(uint8_t* mac, rfalPicoPassCheckRes* chk FuriHalNfcReturn ret; rfalPicoPassCheckReq chkReq; chkReq.CMD = RFAL_PICOPASS_CMD_CHECK; - ST_MEMCPY(chkReq.mac, mac, 4); - ST_MEMSET(chkReq.null, 0, 4); + memcpy(chkReq.mac, mac, 4); + memset(chkReq.null, 0, 4); uint16_t recvLen = 0; uint32_t flags = RFAL_PICOPASS_TXRX_FLAGS; uint32_t fwt = furi_hal_nfc_ll_ms2fc(20); diff --git a/applications/services/gui/application.fam b/applications/services/gui/application.fam index 7fad7b4ea..869d964dd 100644 --- a/applications/services/gui/application.fam +++ b/applications/services/gui/application.fam @@ -12,6 +12,7 @@ App( order=70, sdk_headers=[ "gui.h", + "icon_i.h", "elements.h", "view_dispatcher.h", "view_stack.h", diff --git a/firmware.scons b/firmware.scons index 63a1aa3f7..a0c1ab339 100644 --- a/firmware.scons +++ b/firmware.scons @@ -1,6 +1,7 @@ Import("ENV", "fw_build_meta") from SCons.Errors import UserError +from SCons.Node import FS import itertools from fbt_extra.util import ( @@ -14,7 +15,6 @@ env = ENV.Clone( ("compilation_db", {"COMPILATIONDB_COMSTR": "\tCDB\t${TARGET}"}), "fwbin", "fbt_apps", - "fbt_sdk", ], COMPILATIONDB_USE_ABSPATH=False, BUILD_DIR=fw_build_meta["build_dir"], @@ -112,7 +112,9 @@ lib_targets = env.BuildModules( # Now, env is fully set up with everything to build apps -fwenv = env.Clone() +fwenv = env.Clone(FW_ARTIFACTS=[]) + +fw_artifacts = fwenv["FW_ARTIFACTS"] # Set up additional app-specific build flags SConscript("site_scons/firmwareopts.scons", exports={"ENV": fwenv}) @@ -130,7 +132,14 @@ if extra_int_apps := GetOption("extra_int_apps"): if fwenv["FAP_EXAMPLES"]: fwenv.Append(APPDIRS=[("applications/examples", False)]) -fwenv.LoadApplicationManifests() +for app_dir, _ in env["APPDIRS"]: + app_dir_node = env.Dir("#").Dir(app_dir) + + for entry in app_dir_node.glob("*"): + if isinstance(entry, FS.Dir) and not str(entry).startswith("."): + fwenv.LoadAppManifest(entry) + + fwenv.PrepareApplicationsBuild() # Build external apps @@ -138,6 +147,7 @@ if env["IS_BASE_FIRMWARE"]: extapps = fwenv["FW_EXTAPPS"] = SConscript( "site_scons/extapps.scons", exports={"ENV": fwenv} ) + fw_artifacts.append(extapps["sdk_tree"]) # Add preprocessor definitions for current set of apps @@ -220,7 +230,10 @@ Depends(fwelf, lib_targets) AddPostAction(fwelf, fwenv["APPBUILD_DUMP"]) AddPostAction( fwelf, - Action('${PYTHON3} "${ROOT_DIR}/scripts/fwsize.py" elf ${TARGET}', "Firmware size"), + Action( + '${PYTHON3} "${BIN_SIZE_SCRIPT}" elf ${TARGET}', + "Firmware size", + ), ) # Produce extra firmware files @@ -228,7 +241,7 @@ fwhex = fwenv["FW_HEX"] = fwenv.HEXBuilder("${FIRMWARE_BUILD_CFG}") fwbin = fwenv["FW_BIN"] = fwenv.BINBuilder("${FIRMWARE_BUILD_CFG}") AddPostAction( fwbin, - Action('@${PYTHON3} "${ROOT_DIR}/scripts/fwsize.py" bin ${TARGET}'), + Action('@${PYTHON3} "${BIN_SIZE_SCRIPT}" bin ${TARGET}'), ) fwdfu = fwenv["FW_DFU"] = fwenv.DFUBuilder("${FIRMWARE_BUILD_CFG}") @@ -238,12 +251,14 @@ fwdump = fwenv.ObjDump("${FIRMWARE_BUILD_CFG}") Alias(fwenv["FIRMWARE_BUILD_CFG"] + "_list", fwdump) -fw_artifacts = fwenv["FW_ARTIFACTS"] = [ - fwhex, - fwbin, - fwdfu, - fwenv["FW_VERSION_JSON"], -] +fw_artifacts.extend( + [ + fwhex, + fwbin, + fwdfu, + fwenv["FW_VERSION_JSON"], + ] +) fwcdb = fwenv.CompilationDatabase() @@ -272,34 +287,5 @@ if should_gen_cdb_and_link_dir(fwenv, BUILD_TARGETS): Alias(fwenv["FIRMWARE_BUILD_CFG"] + "_all", fw_artifacts) -if fwenv["IS_BASE_FIRMWARE"]: - sdk_source = fwenv.SDKPrebuilder( - "sdk_origin", - # Deps on root SDK headers and generated files - (fwenv["SDK_HEADERS"], fwenv["FW_ASSETS_HEADERS"]), - ) - # Extra deps on headers included in deeper levels - Depends(sdk_source, fwenv.ProcessSdkDepends("sdk_origin.d")) - - fwenv["SDK_DIR"] = fwenv.Dir("sdk") - sdk_tree = fwenv.SDKTree(fwenv["SDK_DIR"], "sdk_origin") - fw_artifacts.append(sdk_tree) - # AlwaysBuild(sdk_tree) - Alias("sdk_tree", sdk_tree) - - sdk_apicheck = fwenv.SDKSymUpdater(fwenv["SDK_DEFINITION"], "sdk_origin") - Precious(sdk_apicheck) - NoClean(sdk_apicheck) - AlwaysBuild(sdk_apicheck) - Alias("sdk_check", sdk_apicheck) - - sdk_apisyms = fwenv.SDKSymGenerator( - "assets/compiled/symbols.h", fwenv["SDK_DEFINITION"] - ) - Alias("api_syms", sdk_apisyms) - - if fwenv["FORCE"]: - fwenv.AlwaysBuild(sdk_source, sdk_tree, sdk_apicheck, sdk_apisyms) - Return("fwenv") diff --git a/firmware/SConscript b/firmware/SConscript index 2285a6f2c..19dde2e4c 100644 --- a/firmware/SConscript +++ b/firmware/SConscript @@ -2,11 +2,10 @@ Import("env") env.Append( LINT_SOURCES=["firmware"], - # SDK_HEADERS=[env.File("#/firmware/targets/furi_hal_include/furi_hal.h")], SDK_HEADERS=[ - *env.GlobRecursive("*.h", "#/firmware/targets/furi_hal_include", "*_i.h"), - *env.GlobRecursive("*.h", "#/firmware/targets/f${TARGET_HW}/furi_hal", "*_i.h"), - File("#/firmware/targets/f7/platform_specific/intrinsic_export.h"), + *env.GlobRecursive("*.h", "targets/furi_hal_include", "*_i.h"), + *env.GlobRecursive("*.h", "targets/f${TARGET_HW}/furi_hal", "*_i.h"), + File("targets/f7/platform_specific/intrinsic_export.h"), ], ) diff --git a/firmware/targets/f7/api_symbols.csv b/firmware/targets/f7/api_symbols.csv index 00ba30c24..0d9a5b561 100644 --- a/firmware/targets/f7/api_symbols.csv +++ b/firmware/targets/f7/api_symbols.csv @@ -1,5 +1,5 @@ entry,status,name,type,params -Version,+,7.0,, +Version,+,7.2,, Header,+,applications/services/bt/bt_service/bt.h,, Header,+,applications/services/cli/cli.h,, Header,+,applications/services/cli/cli_vcp.h,, @@ -7,6 +7,7 @@ Header,+,applications/services/dialogs/dialogs.h,, Header,+,applications/services/dolphin/dolphin.h,, Header,+,applications/services/gui/elements.h,, Header,+,applications/services/gui/gui.h,, +Header,+,applications/services/gui/icon_i.h,, Header,+,applications/services/gui/modules/button_menu.h,, Header,+,applications/services/gui/modules/button_panel.h,, Header,+,applications/services/gui/modules/byte_input.h,, @@ -110,12 +111,42 @@ Header,+,lib/STM32CubeWB/Drivers/STM32WBxx_HAL_Driver/Inc/stm32wbxx_ll_wwdg.h,, Header,+,lib/flipper_application/flipper_application.h,, Header,+,lib/flipper_format/flipper_format.h,, Header,+,lib/flipper_format/flipper_format_i.h,, +Header,+,lib/infrared/encoder_decoder/infrared.h,, +Header,+,lib/infrared/worker/infrared_transmit.h,, +Header,+,lib/infrared/worker/infrared_worker.h,, Header,+,lib/lfrfid/lfrfid_dict_file.h,, Header,+,lib/lfrfid/lfrfid_raw_file.h,, Header,+,lib/lfrfid/lfrfid_raw_worker.h,, Header,+,lib/lfrfid/lfrfid_worker.h,, Header,+,lib/lfrfid/protocols/lfrfid_protocols.h,, Header,+,lib/lfrfid/tools/bit_lib.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,, +Header,+,lib/libusb_stm32/inc/hid_usage_device.h,, +Header,+,lib/libusb_stm32/inc/hid_usage_game.h,, +Header,+,lib/libusb_stm32/inc/hid_usage_keyboard.h,, +Header,+,lib/libusb_stm32/inc/hid_usage_led.h,, +Header,+,lib/libusb_stm32/inc/hid_usage_ordinal.h,, +Header,+,lib/libusb_stm32/inc/hid_usage_power.h,, +Header,+,lib/libusb_stm32/inc/hid_usage_simulation.h,, +Header,+,lib/libusb_stm32/inc/hid_usage_sport.h,, +Header,+,lib/libusb_stm32/inc/hid_usage_telephony.h,, +Header,+,lib/libusb_stm32/inc/hid_usage_vr.h,, +Header,+,lib/libusb_stm32/inc/usb.h,, +Header,+,lib/libusb_stm32/inc/usb_cdc.h,, +Header,+,lib/libusb_stm32/inc/usb_cdca.h,, +Header,+,lib/libusb_stm32/inc/usb_cdce.h,, +Header,+,lib/libusb_stm32/inc/usb_cdci.h,, +Header,+,lib/libusb_stm32/inc/usb_cdcp.h,, +Header,+,lib/libusb_stm32/inc/usb_cdcw.h,, +Header,+,lib/libusb_stm32/inc/usb_dfu.h,, +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/des.h,, +Header,+,lib/mbedtls/include/mbedtls/sha1.h,, Header,+,lib/micro-ecc/uECC.h,, Header,+,lib/one_wire/ibutton/ibutton_worker.h,, Header,+,lib/one_wire/maxim_crc.h,, @@ -132,6 +163,7 @@ Header,+,lib/subghz/blocks/math.h,, Header,+,lib/subghz/environment.h,, Header,+,lib/subghz/protocols/raw.h,, Header,+,lib/subghz/receiver.h,, +Header,+,lib/subghz/registry.h,, Header,+,lib/subghz/subghz_setting.h,, Header,+,lib/subghz/subghz_tx_rx_worker.h,, Header,+,lib/subghz/subghz_worker.h,, @@ -407,6 +439,7 @@ Function,-,_system_r,int,"_reent*, const char*" Function,-,_tempnam_r,char*,"_reent*, const char*, const char*" Function,-,_tmpfile_r,FILE*,_reent* Function,-,_tmpnam_r,char*,"_reent*, char*" +Function,-,_tzset_r,void,_reent* Function,-,_ungetc_r,int,"_reent*, int, FILE*" Function,-,_unsetenv_r,int,"_reent*, const char*" Function,-,_vasiprintf_r,int,"_reent*, char**, const char*, __gnuc_va_list" @@ -454,6 +487,8 @@ Function,+,args_read_hex_bytes,_Bool,"FuriString*, uint8_t*, size_t" Function,+,args_read_int_and_trim,_Bool,"FuriString*, int*" Function,+,args_read_probably_quoted_string_and_trim,_Bool,"FuriString*, FuriString*" Function,+,args_read_string_and_trim,_Bool,"FuriString*, FuriString*" +Function,-,asctime,char*,const tm* +Function,-,asctime_r,char*,"const tm*, char*" Function,-,asin,double,double Function,-,asinf,float,float Function,-,asinh,double,double @@ -621,6 +656,7 @@ 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_write,void,"Cli*, const uint8_t*, size_t" +Function,-,clock,clock_t, Function,-,copysign,double,"double, double" Function,-,copysignf,float,"float, float" Function,-,copysignl,long double,"long double, long double" @@ -633,6 +669,8 @@ Function,-,cosl,long double,long double Function,+,crc32_calc_buffer,uint32_t,"uint32_t, const void*, size_t" Function,+,crc32_calc_file,uint32_t,"File*, const FileCrcProgressCb, void*" Function,-,ctermid,char*,char* +Function,-,ctime,char*,const time_t* +Function,-,ctime_r,char*,"const time_t*, char*" Function,-,cuserid,char*,char* Function,+,delete_mutex,_Bool,ValueMutex* Function,+,dialog_ex_alloc,DialogEx*, @@ -659,6 +697,7 @@ Function,+,dialog_message_set_icon,void,"DialogMessage*, const Icon*, uint8_t, u Function,+,dialog_message_set_text,void,"DialogMessage*, const char*, uint8_t, uint8_t, Align, Align" Function,+,dialog_message_show,DialogMessageButton,"DialogsApp*, const DialogMessage*" Function,+,dialog_message_show_storage_error,void,"DialogsApp*, const char*" +Function,-,difftime,double,"time_t, time_t" Function,-,digital_signal_alloc,DigitalSignal*,uint32_t Function,-,digital_signal_append,_Bool,"DigitalSignal*, DigitalSignal*" Function,-,digital_signal_free,void,DigitalSignal* @@ -1475,6 +1514,8 @@ Function,-,getenv,char*,const char* Function,-,gets,char*,char* Function,-,getsubopt,int,"char**, char**, char**" Function,-,getw,int,FILE* +Function,-,gmtime,tm*,const time_t* +Function,-,gmtime_r,tm*,"const time_t*, tm*" Function,+,gui_add_framebuffer_callback,void,"Gui*, GuiCanvasCommitCallback, void*" Function,+,gui_add_view_port,void,"Gui*, ViewPort*, GuiLayer" Function,+,gui_get_framebuffer_size,size_t,Gui* @@ -1535,6 +1576,42 @@ Function,-,ilogbl,int,long double Function,-,index,char*,"const char*, int" Function,-,infinity,double, Function,-,infinityf,float, +Function,+,infrared_alloc_decoder,InfraredDecoderHandler*, +Function,+,infrared_alloc_encoder,InfraredEncoderHandler*, +Function,+,infrared_check_decoder_ready,const InfraredMessage*,InfraredDecoderHandler* +Function,+,infrared_decode,const InfraredMessage*,"InfraredDecoderHandler*, _Bool, uint32_t" +Function,+,infrared_encode,InfraredStatus,"InfraredEncoderHandler*, uint32_t*, _Bool*" +Function,+,infrared_free_decoder,void,InfraredDecoderHandler* +Function,+,infrared_free_encoder,void,InfraredEncoderHandler* +Function,+,infrared_get_protocol_address_length,uint8_t,InfraredProtocol +Function,+,infrared_get_protocol_by_name,InfraredProtocol,const char* +Function,+,infrared_get_protocol_command_length,uint8_t,InfraredProtocol +Function,+,infrared_get_protocol_duty_cycle,float,InfraredProtocol +Function,+,infrared_get_protocol_frequency,uint32_t,InfraredProtocol +Function,+,infrared_get_protocol_name,const char*,InfraredProtocol +Function,+,infrared_is_protocol_valid,_Bool,InfraredProtocol +Function,+,infrared_reset_decoder,void,InfraredDecoderHandler* +Function,+,infrared_reset_encoder,void,"InfraredEncoderHandler*, const InfraredMessage*" +Function,+,infrared_send,void,"const InfraredMessage*, int" +Function,+,infrared_send_raw,void,"const uint32_t[], uint32_t, _Bool" +Function,+,infrared_send_raw_ext,void,"const uint32_t[], uint32_t, _Bool, uint32_t, float" +Function,+,infrared_worker_alloc,InfraredWorker*, +Function,+,infrared_worker_free,void,InfraredWorker* +Function,+,infrared_worker_get_decoded_signal,const InfraredMessage*,const InfraredWorkerSignal* +Function,+,infrared_worker_get_raw_signal,void,"const InfraredWorkerSignal*, const uint32_t**, size_t*" +Function,+,infrared_worker_rx_enable_blink_on_receiving,void,"InfraredWorker*, _Bool" +Function,+,infrared_worker_rx_enable_signal_decoding,void,"InfraredWorker*, _Bool" +Function,+,infrared_worker_rx_set_received_signal_callback,void,"InfraredWorker*, InfraredWorkerReceivedSignalCallback, void*" +Function,+,infrared_worker_rx_start,void,InfraredWorker* +Function,+,infrared_worker_rx_stop,void,InfraredWorker* +Function,+,infrared_worker_set_decoded_signal,void,"InfraredWorker*, const InfraredMessage*" +Function,+,infrared_worker_set_raw_signal,void,"InfraredWorker*, const uint32_t*, size_t" +Function,+,infrared_worker_signal_is_decoded,_Bool,const InfraredWorkerSignal* +Function,+,infrared_worker_tx_get_signal_steady_callback,InfraredWorkerGetSignalResponse,"void*, InfraredWorker*" +Function,+,infrared_worker_tx_set_get_signal_callback,void,"InfraredWorker*, InfraredWorkerGetSignalCallback, void*" +Function,+,infrared_worker_tx_set_signal_sent_callback,void,"InfraredWorker*, InfraredWorkerMessageSentCallback, void*" +Function,+,infrared_worker_tx_start,void,InfraredWorker* +Function,+,infrared_worker_tx_stop,void,InfraredWorker* Function,+,init_mutex,_Bool,"ValueMutex*, void*, size_t" Function,-,initstate,char*,"unsigned, char*, size_t" Function,+,input_get_key_name,const char*,InputKey @@ -1634,6 +1711,8 @@ Function,+,loader_update_menu,void, Function,+,loading_alloc,Loading*, Function,+,loading_free,void,Loading* Function,+,loading_get_view,View*,Loading* +Function,-,localtime,tm*,const time_t* +Function,-,localtime_r,tm*,"const time_t*, tm*" Function,-,log,double,double Function,-,log10,double,double Function,-,log10f,float,float @@ -1662,6 +1741,36 @@ 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_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* +Function,-,mbedtls_des3_init,void,mbedtls_des3_context* +Function,-,mbedtls_des3_set2key_dec,int,"mbedtls_des3_context*, const unsigned char[8 * 2]" +Function,-,mbedtls_des3_set2key_enc,int,"mbedtls_des3_context*, const unsigned char[8 * 2]" +Function,-,mbedtls_des3_set3key_dec,int,"mbedtls_des3_context*, const unsigned char[8 * 3]" +Function,-,mbedtls_des3_set3key_enc,int,"mbedtls_des3_context*, const unsigned char[8 * 3]" +Function,-,mbedtls_des_crypt_cbc,int,"mbedtls_des_context*, int, size_t, unsigned char[8], const unsigned char*, unsigned char*" +Function,-,mbedtls_des_crypt_ecb,int,"mbedtls_des_context*, const unsigned char[8], unsigned char[8]" +Function,-,mbedtls_des_free,void,mbedtls_des_context* +Function,-,mbedtls_des_init,void,mbedtls_des_context* +Function,-,mbedtls_des_key_check_key_parity,int,const unsigned char[8] +Function,-,mbedtls_des_key_check_weak,int,const unsigned char[8] +Function,-,mbedtls_des_key_set_parity,void,unsigned char[8] +Function,-,mbedtls_des_self_test,int,int +Function,-,mbedtls_des_setkey,void,"uint32_t[32], const unsigned char[8]" +Function,-,mbedtls_des_setkey_dec,int,"mbedtls_des_context*, const unsigned char[8]" +Function,-,mbedtls_des_setkey_enc,int,"mbedtls_des_context*, const unsigned char[8]" +Function,-,mbedtls_internal_sha1_process,int,"mbedtls_sha1_context*, const unsigned char[64]" +Function,-,mbedtls_platform_gmtime_r,tm*,"const mbedtls_time_t*, tm*" +Function,-,mbedtls_platform_zeroize,void,"void*, size_t" +Function,-,mbedtls_sha1,int,"const unsigned char*, size_t, unsigned char[20]" +Function,-,mbedtls_sha1_clone,void,"mbedtls_sha1_context*, const mbedtls_sha1_context*" +Function,-,mbedtls_sha1_finish,int,"mbedtls_sha1_context*, unsigned char[20]" +Function,-,mbedtls_sha1_free,void,mbedtls_sha1_context* +Function,-,mbedtls_sha1_init,void,mbedtls_sha1_context* +Function,-,mbedtls_sha1_self_test,int,int +Function,-,mbedtls_sha1_starts,int,mbedtls_sha1_context* +Function,-,mbedtls_sha1_update,int,"mbedtls_sha1_context*, const unsigned char*, size_t" Function,-,mblen,int,"const char*, size_t" Function,-,mbstowcs,size_t,"wchar_t*, const char*, size_t" Function,-,mbtowc,int,"wchar_t*, const char*, size_t" @@ -1702,6 +1811,7 @@ Function,-,mkostemps,int,"char*, int, int" Function,-,mkstemp,int,char* Function,-,mkstemps,int,"char*, int" Function,-,mktemp,char*,char* +Function,-,mktime,time_t,tm* Function,-,modf,double,"double, double*" Function,-,modff,float,"float, float*" Function,-,modfl,long double,"long double, long double*" @@ -2210,6 +2320,8 @@ Function,+,stream_write_vaformat,size_t,"Stream*, const char*, va_list" Function,-,strerror,char*,int Function,-,strerror_l,char*,"int, locale_t" Function,-,strerror_r,char*,"int, char*, size_t" +Function,-,strftime,size_t,"char*, size_t, const char*, const tm*" +Function,-,strftime_l,size_t,"char*, size_t, const char*, const tm*, locale_t" Function,+,string_stream_alloc,Stream*, Function,-,strlcat,size_t,"char*, const char*, size_t" Function,+,strlcpy,size_t,"char*, const char*, size_t" @@ -2224,6 +2336,8 @@ Function,-,strndup,char*,"const char*, size_t" Function,-,strnlen,size_t,"const char*, size_t" Function,-,strnstr,char*,"const char*, const char*, size_t" Function,-,strpbrk,char*,"const char*, const char*" +Function,-,strptime,char*,"const char*, const char*, tm*" +Function,-,strptime_l,char*,"const char*, const char*, tm*, locale_t" Function,+,strrchr,char*,"const char*, int" Function,-,strsep,char*,"char**, const char*" Function,-,strsignal,char*,int @@ -2313,6 +2427,9 @@ Function,+,subghz_protocol_raw_get_sample_write,size_t,SubGhzProtocolDecoderRAW* Function,+,subghz_protocol_raw_save_to_file_init,_Bool,"SubGhzProtocolDecoderRAW*, const char*, SubGhzRadioPreset*" Function,+,subghz_protocol_raw_save_to_file_pause,void,"SubGhzProtocolDecoderRAW*, _Bool" Function,+,subghz_protocol_raw_save_to_file_stop,void,SubGhzProtocolDecoderRAW* +Function,+,subghz_protocol_registry_count,size_t,const SubGhzProtocolRegistry* +Function,+,subghz_protocol_registry_get_by_index,const SubGhzProtocol*,"const SubGhzProtocolRegistry*, size_t" +Function,+,subghz_protocol_registry_get_by_name,const SubGhzProtocol*,"const SubGhzProtocolRegistry*, const char*" Function,+,subghz_receiver_alloc_init,SubGhzReceiver*,SubGhzEnvironment* Function,+,subghz_receiver_decode,void,"SubGhzReceiver*, _Bool, uint32_t" Function,+,subghz_receiver_free,void,SubGhzReceiver* @@ -2411,6 +2528,7 @@ Function,+,text_input_set_validator,void,"TextInput*, TextInputValidatorCallback Function,-,tgamma,double,double Function,-,tgammaf,float,float Function,-,tgammal,long double,long double +Function,-,time,time_t,time_t* Function,+,timerCalculateTimer,uint32_t,uint16_t Function,-,timerDelay,void,uint16_t Function,+,timerIsExpired,_Bool,uint32_t @@ -2429,6 +2547,7 @@ Function,-,toupper_l,int,"int, locale_t" Function,-,trunc,double,double Function,-,truncf,float,float Function,-,truncl,long double,long double +Function,-,tzset,void, Function,-,uECC_compress,void,"const uint8_t*, uint8_t*, uECC_Curve" Function,+,uECC_compute_public_key,int,"const uint8_t*, uint8_t*, uECC_Curve" Function,-,uECC_curve_private_key_size,int,uECC_Curve @@ -2666,10 +2785,13 @@ Variable,-,MSIRangeTable,const uint32_t[16], Variable,-,SmpsPrescalerTable,const uint32_t[4][6], Variable,+,SystemCoreClock,uint32_t, Variable,+,_ctype_,const char[], +Variable,-,_daylight,int, Variable,+,_global_impure_ptr,_reent*, Variable,+,_impure_ptr,_reent*, Variable,-,_sys_errlist,const char*[], Variable,-,_sys_nerr,int, +Variable,-,_timezone,long, +Variable,-,_tzname,char*[2], Variable,+,cli_vcp,CliSession, Variable,+,furi_hal_i2c_bus_external,FuriHalI2cBus, Variable,+,furi_hal_i2c_bus_power,FuriHalI2cBus, diff --git a/lib/SConscript b/lib/SConscript index 2822684bc..60ffabfa9 100644 --- a/lib/SConscript +++ b/lib/SConscript @@ -17,12 +17,12 @@ env.Append( "lib/print", ], SDK_HEADERS=[ - File("#/lib/one_wire/one_wire_host_timing.h"), - File("#/lib/one_wire/one_wire_host.h"), - File("#/lib/one_wire/one_wire_slave.h"), - File("#/lib/one_wire/one_wire_device.h"), - File("#/lib/one_wire/ibutton/ibutton_worker.h"), - File("#/lib/one_wire/maxim_crc.h"), + File("one_wire/one_wire_host_timing.h"), + File("one_wire/one_wire_host.h"), + File("one_wire/one_wire_slave.h"), + File("one_wire/one_wire_device.h"), + File("one_wire/ibutton/ibutton_worker.h"), + File("one_wire/maxim_crc.h"), ], ) diff --git a/lib/STM32CubeWB.scons b/lib/STM32CubeWB.scons index e8350ea99..b0e55f82e 100644 --- a/lib/STM32CubeWB.scons +++ b/lib/STM32CubeWB.scons @@ -15,7 +15,7 @@ env.Append( ], SDK_HEADERS=env.GlobRecursive( "*_ll_*.h", - "#/lib/STM32CubeWB/Drivers/STM32WBxx_HAL_Driver/Inc/", + Dir("STM32CubeWB/Drivers/STM32WBxx_HAL_Driver/Inc/"), exclude="*usb.h", ), ) diff --git a/lib/flipper_application/SConscript b/lib/flipper_application/SConscript index 3a5e7f4db..9fbbf95d1 100644 --- a/lib/flipper_application/SConscript +++ b/lib/flipper_application/SConscript @@ -5,7 +5,7 @@ env.Append( "#/lib/flipper_application", ], SDK_HEADERS=[ - File("#/lib/flipper_application/flipper_application.h"), + File("flipper_application.h"), ], ) diff --git a/lib/flipper_format/SConscript b/lib/flipper_format/SConscript index 5e185678b..353da8035 100644 --- a/lib/flipper_format/SConscript +++ b/lib/flipper_format/SConscript @@ -5,8 +5,8 @@ env.Append( "#/lib/flipper_format", ], SDK_HEADERS=[ - File("#/lib/flipper_format/flipper_format.h"), - File("#/lib/flipper_format/flipper_format_i.h"), + File("flipper_format.h"), + File("flipper_format_i.h"), ], ) diff --git a/lib/infrared/SConscript b/lib/infrared/SConscript index 35db75f87..9a1543f00 100644 --- a/lib/infrared/SConscript +++ b/lib/infrared/SConscript @@ -5,6 +5,11 @@ env.Append( "#/lib/infrared/encoder_decoder", "#/lib/infrared/worker", ], + SDK_HEADERS=[ + File("encoder_decoder/infrared.h"), + File("worker/infrared_worker.h"), + File("worker/infrared_transmit.h"), + ], ) diff --git a/lib/lfrfid/SConscript b/lib/lfrfid/SConscript index 6177a9a50..69ea9d3c1 100644 --- a/lib/lfrfid/SConscript +++ b/lib/lfrfid/SConscript @@ -8,12 +8,12 @@ env.Append( "#/lib/lfrfid", ], SDK_HEADERS=[ - File("#/lib/lfrfid/lfrfid_worker.h"), - File("#/lib/lfrfid/lfrfid_raw_worker.h"), - File("#/lib/lfrfid/lfrfid_raw_file.h"), - File("#/lib/lfrfid/lfrfid_dict_file.h"), - File("#/lib/lfrfid/tools/bit_lib.h"), - File("#/lib/lfrfid/protocols/lfrfid_protocols.h"), + File("lfrfid_worker.h"), + File("lfrfid_raw_worker.h"), + File("lfrfid_raw_file.h"), + File("lfrfid_dict_file.h"), + File("tools/bit_lib.h"), + File("protocols/lfrfid_protocols.h"), ], ) diff --git a/lib/libusb_stm32.scons b/lib/libusb_stm32.scons index cb867fdbc..4838b7c50 100644 --- a/lib/libusb_stm32.scons +++ b/lib/libusb_stm32.scons @@ -7,6 +7,10 @@ env.Append( CPPDEFINES=[ ("USB_PMASIZE", "0x400"), ], + SDK_HEADERS=env.GlobRecursive( + "*.h", + Dir("libusb_stm32/inc"), + ), ) diff --git a/lib/mbedtls.scons b/lib/mbedtls.scons index b57221a49..79a4a2520 100644 --- a/lib/mbedtls.scons +++ b/lib/mbedtls.scons @@ -5,6 +5,10 @@ env.Append( "#/lib/mbedtls", "#/lib/mbedtls/include", ], + SDK_HEADERS=[ + File("mbedtls/include/mbedtls/des.h"), + File("mbedtls/include/mbedtls/sha1.h"), + ], ) diff --git a/lib/misc.scons b/lib/misc.scons index b7d8554b5..91ad276a0 100644 --- a/lib/misc.scons +++ b/lib/misc.scons @@ -13,7 +13,7 @@ env.Append( "PB_ENABLE_MALLOC", ], SDK_HEADERS=[ - File("#/lib/micro-ecc/uECC.h"), + File("micro-ecc/uECC.h"), ], ) diff --git a/lib/print/SConscript b/lib/print/SConscript index d4a55ab84..f34c8152f 100644 --- a/lib/print/SConscript +++ b/lib/print/SConscript @@ -98,7 +98,7 @@ for wrapped_fn in wrapped_fn_list: env.Append( SDK_HEADERS=[ - File("#/lib/print/wrappers.h"), + File("wrappers.h"), ], ) diff --git a/lib/subghz/SConscript b/lib/subghz/SConscript index e25d122c8..6d9c0cd06 100644 --- a/lib/subghz/SConscript +++ b/lib/subghz/SConscript @@ -5,18 +5,19 @@ env.Append( "#/lib/subghz", ], SDK_HEADERS=[ - File("#/lib/subghz/environment.h"), - File("#/lib/subghz/receiver.h"), - File("#/lib/subghz/subghz_worker.h"), - File("#/lib/subghz/subghz_tx_rx_worker.h"), - File("#/lib/subghz/transmitter.h"), - File("#/lib/subghz/protocols/raw.h"), - File("#/lib/subghz/blocks/const.h"), - File("#/lib/subghz/blocks/decoder.h"), - File("#/lib/subghz/blocks/encoder.h"), - File("#/lib/subghz/blocks/generic.h"), - File("#/lib/subghz/blocks/math.h"), - File("#/lib/subghz/subghz_setting.h"), + File("environment.h"), + File("receiver.h"), + File("registry.h"), + File("subghz_worker.h"), + File("subghz_tx_rx_worker.h"), + File("transmitter.h"), + File("protocols/raw.h"), + File("blocks/const.h"), + File("blocks/decoder.h"), + File("blocks/encoder.h"), + File("blocks/generic.h"), + File("blocks/math.h"), + File("subghz_setting.h"), ], ) diff --git a/lib/subghz/registry.h b/lib/subghz/registry.h index 062cdf68f..91027807e 100644 --- a/lib/subghz/registry.h +++ b/lib/subghz/registry.h @@ -2,6 +2,10 @@ #include "types.h" +#ifdef __cplusplus +extern "C" { +#endif + typedef struct SubGhzEnvironment SubGhzEnvironment; typedef struct SubGhzProtocolRegistry SubGhzProtocolRegistry; @@ -37,3 +41,7 @@ const SubGhzProtocol* subghz_protocol_registry_get_by_index( * @return Number of protocols */ size_t subghz_protocol_registry_count(const SubGhzProtocolRegistry* protocol_registry); + +#ifdef __cplusplus +} +#endif diff --git a/lib/toolbox/SConscript b/lib/toolbox/SConscript index d631431ee..015a8ed18 100644 --- a/lib/toolbox/SConscript +++ b/lib/toolbox/SConscript @@ -8,23 +8,23 @@ env.Append( "#/lib/toolbox", ], SDK_HEADERS=[ - File("#/lib/toolbox/manchester_decoder.h"), - File("#/lib/toolbox/manchester_encoder.h"), - File("#/lib/toolbox/path.h"), - File("#/lib/toolbox/random_name.h"), - File("#/lib/toolbox/hmac_sha256.h"), - File("#/lib/toolbox/crc32_calc.h"), - File("#/lib/toolbox/dir_walk.h"), - File("#/lib/toolbox/md5.h"), - File("#/lib/toolbox/args.h"), - File("#/lib/toolbox/saved_struct.h"), - File("#/lib/toolbox/version.h"), - File("#/lib/toolbox/tar/tar_archive.h"), - File("#/lib/toolbox/stream/stream.h"), - File("#/lib/toolbox/stream/file_stream.h"), - File("#/lib/toolbox/stream/string_stream.h"), - File("#/lib/toolbox/stream/buffered_file_stream.h"), - File("#/lib/toolbox/protocols/protocol_dict.h"), + File("manchester_decoder.h"), + File("manchester_encoder.h"), + File("path.h"), + File("random_name.h"), + File("hmac_sha256.h"), + File("crc32_calc.h"), + File("dir_walk.h"), + File("md5.h"), + File("args.h"), + File("saved_struct.h"), + File("version.h"), + File("tar/tar_archive.h"), + File("stream/stream.h"), + File("stream/file_stream.h"), + File("stream/string_stream.h"), + File("stream/buffered_file_stream.h"), + File("protocols/protocol_dict.h"), ], ) diff --git a/scripts/fbt/util.py b/scripts/fbt/util.py index baa4ddfee..f5404458e 100644 --- a/scripts/fbt/util.py +++ b/scripts/fbt/util.py @@ -1,11 +1,11 @@ import SCons from SCons.Subst import quote_spaces from SCons.Errors import StopError +from SCons.Node.FS import _my_normcase import re import os -import random -import string + WINPATHSEP_RE = re.compile(r"\\([^\"'\\]|$)") @@ -41,3 +41,14 @@ def link_dir(target_path, source_path, is_windows): def single_quote(arg_list): return " ".join(f"'{arg}'" if " " in arg else str(arg) for arg in arg_list) + + +def extract_abs_dir_path(node): + if isinstance(node, SCons.Node.FS.EntryProxy): + node = node.get() + + for repo_dir in node.get_all_rdirs(): + if os.path.exists(repo_dir.abspath): + return repo_dir.abspath + + raise StopError(f"Can't find absolute path for {node.name}") diff --git a/scripts/fbt_tools/crosscc.py b/scripts/fbt_tools/crosscc.py index aacda58c6..dd5cd5319 100644 --- a/scripts/fbt_tools/crosscc.py +++ b/scripts/fbt_tools/crosscc.py @@ -37,6 +37,21 @@ def _get_tool_version(env, tool): def generate(env, **kw): + if not env.get("VERBOSE", False): + env.SetDefault( + CCCOMSTR="\tCC\t${SOURCE}", + CXXCOMSTR="\tCPP\t${SOURCE}", + ASCOMSTR="\tASM\t${SOURCE}", + ARCOMSTR="\tAR\t${TARGET}", + RANLIBCOMSTR="\tRANLIB\t${TARGET}", + LINKCOMSTR="\tLINK\t${TARGET}", + INSTALLSTR="\tINSTALL\t${TARGET}", + APPSCOMSTR="\tAPPS\t${TARGET}", + VERSIONCOMSTR="\tVERSION\t${TARGET}", + STRIPCOMSTR="\tSTRIP\t${TARGET}", + OBJDUMPCOMSTR="\tOBJDUMP\t${TARGET}", + ) + for orig_tool in (asm, gcc, gxx, ar, gnulink, strip, gdb, objdump): orig_tool.generate(env) env.SetDefault( diff --git a/scripts/fbt_tools/fbt_apps.py b/scripts/fbt_tools/fbt_apps.py index ef5e9b9d9..55e282017 100644 --- a/scripts/fbt_tools/fbt_apps.py +++ b/scripts/fbt_tools/fbt_apps.py @@ -2,7 +2,7 @@ from SCons.Builder import Builder from SCons.Action import Action from SCons.Warnings import warn, WarningOnByDefault import SCons -import os.path +from ansi.color import fg from fbt.appmanifest import ( FlipperAppType, @@ -16,21 +16,20 @@ from fbt.appmanifest import ( # AppBuildset env["APPBUILD"] - contains subset of apps, filtered for current config -def LoadApplicationManifests(env): - appmgr = env["APPMGR"] = AppManager() - for app_dir, _ in env["APPDIRS"]: - app_dir_node = env.Dir("#").Dir(app_dir) +def LoadAppManifest(env, entry): + try: + APP_MANIFEST_NAME = "application.fam" + manifest_glob = entry.glob(APP_MANIFEST_NAME) + if len(manifest_glob) == 0: + raise FlipperManifestException( + f"Folder {entry}: manifest {APP_MANIFEST_NAME} is missing" + ) - for entry in app_dir_node.glob("*", ondisk=True, source=True): - if isinstance(entry, SCons.Node.FS.Dir) and not str(entry).startswith("."): - try: - app_manifest_file_path = os.path.join( - entry.abspath, "application.fam" - ) - appmgr.load_manifest(app_manifest_file_path, entry) - env.Append(PY_LINT_SOURCES=[app_manifest_file_path]) - except FlipperManifestException as e: - warn(WarningOnByDefault, str(e)) + app_manifest_file_path = manifest_glob[0].rfile().abspath + env["APPMGR"].load_manifest(app_manifest_file_path, entry) + env.Append(PY_LINT_SOURCES=[app_manifest_file_path]) + except FlipperManifestException as e: + warn(WarningOnByDefault, str(e)) def PrepareApplicationsBuild(env): @@ -46,12 +45,12 @@ def PrepareApplicationsBuild(env): def DumpApplicationConfig(target, source, env): print(f"Loaded {len(env['APPMGR'].known_apps)} app definitions.") - print("Firmware modules configuration:") + print(fg.boldgreen("Firmware modules configuration:")) for apptype in FlipperAppType: app_sublist = env["APPBUILD"].get_apps_of_type(apptype) if app_sublist: print( - f"{apptype.value}:\n\t", + fg.green(f"{apptype.value}:\n\t"), ", ".join(app.appid for app in app_sublist), ) @@ -65,8 +64,11 @@ def build_apps_c(target, source, env): def generate(env): - env.AddMethod(LoadApplicationManifests) + env.AddMethod(LoadAppManifest) env.AddMethod(PrepareApplicationsBuild) + env.SetDefault( + APPMGR=AppManager(), + ) env.Append( BUILDERS={ diff --git a/scripts/fbt_tools/fbt_assets.py b/scripts/fbt_tools/fbt_assets.py index 4fa5353d0..521c37e90 100644 --- a/scripts/fbt_tools/fbt_assets.py +++ b/scripts/fbt_tools/fbt_assets.py @@ -1,11 +1,10 @@ -import SCons - from SCons.Builder import Builder from SCons.Action import Action -from SCons.Node.FS import File +from SCons.Errors import SConsEnvironmentError import os import subprocess +from ansi.color import fg def icons_emitter(target, source, env): @@ -13,7 +12,6 @@ def icons_emitter(target, source, env): target[0].File(env.subst("${ICON_FILE_NAME}.c")), target[0].File(env.subst("${ICON_FILE_NAME}.h")), ] - source = env.GlobRecursive("*.*", env["ICON_SRC_DIR"]) return target, source @@ -86,7 +84,7 @@ def proto_ver_generator(target, source, env): ) except (subprocess.CalledProcessError, EnvironmentError) as e: # Not great, not terrible - print("Git: fetch failed") + print(fg.boldred("Git: fetch failed")) try: git_describe = _invoke_git( @@ -94,10 +92,8 @@ def proto_ver_generator(target, source, env): source_dir=src_dir, ) except (subprocess.CalledProcessError, EnvironmentError) as e: - print("Git: describe failed") - Exit("git error") + raise SConsEnvironmentError("Git: describe failed") - # print("describe=", git_describe) git_major, git_minor = git_describe.split(".") version_file_data = ( "#pragma once", @@ -116,7 +112,7 @@ def CompileIcons(env, target_dir, source_dir, *, icon_bundle_name="assets_icons" icons = env.IconBuilder( target_dir, - ICON_SRC_DIR=source_dir, + source_dir, ICON_FILE_NAME=icon_bundle_name, ) env.Depends(icons, icons_src) @@ -125,8 +121,8 @@ def CompileIcons(env, target_dir, source_dir, *, icon_bundle_name="assets_icons" def generate(env): env.SetDefault( - ASSETS_COMPILER="${ROOT_DIR.abspath}/scripts/assets.py", - NANOPB_COMPILER="${ROOT_DIR.abspath}/lib/nanopb/generator/nanopb_generator.py", + ASSETS_COMPILER="${FBT_SCRIPT_DIR}/assets.py", + NANOPB_COMPILER="${ROOT_DIR}/lib/nanopb/generator/nanopb_generator.py", ) env.AddMethod(CompileIcons) @@ -143,7 +139,7 @@ def generate(env): BUILDERS={ "IconBuilder": Builder( action=Action( - '${PYTHON3} "${ASSETS_COMPILER}" icons ${ICON_SRC_DIR} ${TARGET.dir} --filename ${ICON_FILE_NAME}', + '${PYTHON3} "${ASSETS_COMPILER}" icons ${ABSPATHGETTERFUNC(SOURCE)} ${TARGET.dir} --filename ${ICON_FILE_NAME}', "${ICONSCOMSTR}", ), emitter=icons_emitter, diff --git a/scripts/fbt_tools/fbt_dist.py b/scripts/fbt_tools/fbt_dist.py index 853013e9f..fb59e5b95 100644 --- a/scripts/fbt_tools/fbt_dist.py +++ b/scripts/fbt_tools/fbt_dist.py @@ -103,7 +103,7 @@ def DistCommand(env, name, source, **kw): command = env.Command( target, source, - '@${PYTHON3} "${ROOT_DIR.abspath}/scripts/sconsdist.py" copy -p ${DIST_PROJECTS} -s "${DIST_SUFFIX}" ${DIST_EXTRA}', + '@${PYTHON3} "${DIST_SCRIPT}" copy -p ${DIST_PROJECTS} -s "${DIST_SUFFIX}" ${DIST_EXTRA}', **kw, ) env.Pseudo(target) @@ -121,6 +121,9 @@ def generate(env): env.SetDefault( COPRO_MCU_FAMILY="STM32WB5x", + SELFUPDATE_SCRIPT="${FBT_SCRIPT_DIR}/selfupdate.py", + DIST_SCRIPT="${FBT_SCRIPT_DIR}/sconsdist.py", + COPRO_ASSETS_SCRIPT="${FBT_SCRIPT_DIR}/assets.py", ) env.Append( @@ -128,7 +131,7 @@ def generate(env): "UsbInstall": Builder( action=[ Action( - '${PYTHON3} "${ROOT_DIR.abspath}/scripts/selfupdate.py" dist/${DIST_DIR}/f${TARGET_HW}-update-${DIST_SUFFIX}/update.fuf' + '${PYTHON3} "${SELFUPDATE_SCRIPT}" dist/${DIST_DIR}/f${TARGET_HW}-update-${DIST_SUFFIX}/update.fuf' ), Touch("${TARGET}"), ] @@ -136,7 +139,7 @@ def generate(env): "CoproBuilder": Builder( action=Action( [ - '${PYTHON3} "${ROOT_DIR.abspath}/scripts/assets.py" ' + '${PYTHON3} "${COPRO_ASSETS_SCRIPT}" ' "copro ${COPRO_CUBE_DIR} " "${TARGET} ${COPRO_MCU_FAMILY} " "--cube_ver=${COPRO_CUBE_VERSION} " diff --git a/scripts/fbt_tools/fbt_extapps.py b/scripts/fbt_tools/fbt_extapps.py index 38c943cc5..fb4dc2f16 100644 --- a/scripts/fbt_tools/fbt_extapps.py +++ b/scripts/fbt_tools/fbt_extapps.py @@ -1,15 +1,18 @@ -import shutil from SCons.Builder import Builder from SCons.Action import Action from SCons.Errors import UserError import SCons.Warnings -import os -import pathlib from fbt.elfmanifest import assemble_manifest_data from fbt.appmanifest import FlipperApplication, FlipperManifestException from fbt.sdk.cache import SdkCache +from fbt.util import extract_abs_dir_path + +import os +import pathlib import itertools +import shutil + from ansi.color import fg @@ -62,7 +65,7 @@ def BuildAppElf(env, app): lib_src_root_path = os.path.join(app_work_dir, "lib", lib_def.name) app_env.AppendUnique( CPPPATH=list( - app_env.Dir(lib_src_root_path).Dir(incpath).srcnode() + app_env.Dir(lib_src_root_path).Dir(incpath).srcnode().rfile().abspath for incpath in lib_def.fap_include_paths ), ) @@ -82,7 +85,12 @@ def BuildAppElf(env, app): *lib_def.cflags, ], CPPDEFINES=lib_def.cdefines, - CPPPATH=list(map(app._appdir.Dir, lib_def.cincludes)), + CPPPATH=list( + map( + lambda cpath: extract_abs_dir_path(app._appdir.Dir(cpath)), + lib_def.cincludes, + ) + ), ) lib = private_lib_env.StaticLibrary( @@ -157,7 +165,6 @@ def prepare_app_metadata(target, source, env): app = env["APP"] meta_file_name = source[0].path + ".meta" with open(meta_file_name, "wb") as f: - # f.write(f"hello this is {app}") f.write( assemble_manifest_data( app_manifest=app, @@ -236,7 +243,10 @@ def fap_dist_action(target, source, env): def generate(env, **kw): - env.SetDefault(EXT_APPS_WORK_DIR=kw.get("EXT_APPS_WORK_DIR")) + env.SetDefault( + EXT_APPS_WORK_DIR=kw.get("EXT_APPS_WORK_DIR"), + APP_RUN_SCRIPT="${FBT_SCRIPT_DIR}/runfap.py", + ) if not env["VERBOSE"]: env.SetDefault( diff --git a/scripts/fbt_tools/fbt_sdk.py b/scripts/fbt_tools/fbt_sdk.py index f1f55bdb8..c46346b65 100644 --- a/scripts/fbt_tools/fbt_sdk.py +++ b/scripts/fbt_tools/fbt_sdk.py @@ -46,7 +46,9 @@ def prebuild_sdk_emitter(target, source, env): def prebuild_sdk_create_origin_file(target, source, env): mega_file = env.subst("${TARGET}.c", target=target[0]) with open(mega_file, "wt") as sdk_c: - sdk_c.write("\n".join(f"#include <{h.path}>" for h in env["SDK_HEADERS"])) + sdk_c.write( + "\n".join(f"#include <{h.srcnode().path}>" for h in env["SDK_HEADERS"]) + ) class SdkMeta: @@ -62,18 +64,25 @@ class SdkMeta: "cc_args": self._wrap_scons_vars("$CCFLAGS $_CCCOMCOM"), "cpp_args": self._wrap_scons_vars("$CXXFLAGS $CCFLAGS $_CCCOMCOM"), "linker_args": self._wrap_scons_vars("$LINKFLAGS"), - "linker_script": self.env.subst("${LINKER_SCRIPT_PATH}"), + "linker_libs": self.env.subst("${LIBS}"), + "app_ep_subst": self.env.subst("${APP_ENTRY}"), + "sdk_path_subst": self.env.subst("${SDK_DIR_SUBST}"), + "hardware": self.env.subst("${TARGET_HW}"), } with open(json_manifest_path, "wt") as f: json.dump(meta_contents, f, indent=4) def _wrap_scons_vars(self, vars: str): - expanded_vars = self.env.subst(vars, target=Entry("dummy")) + expanded_vars = self.env.subst( + vars, + target=Entry("dummy"), + ) return expanded_vars.replace("\\", "/") class SdkTreeBuilder: SDK_DIR_SUBST = "SDK_ROOT_DIR" + SDK_APP_EP_SUBST = "SDK_APP_EP_SUBST" def __init__(self, env, target, source) -> None: self.env = env @@ -87,6 +96,11 @@ class SdkTreeBuilder: self.sdk_root_dir = target[0].Dir(".") self.sdk_deploy_dir = self.sdk_root_dir.Dir(self.target_sdk_dir_name) + self.sdk_env = self.env.Clone( + APP_ENTRY=self.SDK_APP_EP_SUBST, + SDK_DIR_SUBST=self.SDK_DIR_SUBST, + ) + def _parse_sdk_depends(self): deps_file = self.source[0] with open(deps_file.path, "rt") as deps_f: @@ -95,38 +109,36 @@ class SdkTreeBuilder: self.header_depends = list( filter(lambda fname: fname.endswith(".h"), depends.split()), ) - self.header_depends.append(self.env.subst("${LINKER_SCRIPT_PATH}")) - self.header_depends.append(self.env.subst("${SDK_DEFINITION}")) + self.header_depends.append(self.sdk_env.subst("${LINKER_SCRIPT_PATH}")) + self.header_depends.append(self.sdk_env.subst("${SDK_DEFINITION}")) self.header_dirs = sorted( set(map(os.path.normpath, map(os.path.dirname, self.header_depends))) ) def _generate_sdk_meta(self): - filtered_paths = [self.target_sdk_dir_name] + filtered_paths = ["."] full_fw_paths = list( map( os.path.normpath, - (self.env.Dir(inc_dir).relpath for inc_dir in self.env["CPPPATH"]), + ( + self.sdk_env.Dir(inc_dir).relpath + for inc_dir in self.sdk_env["CPPPATH"] + ), ) ) sdk_dirs = ", ".join(f"'{dir}'" for dir in self.header_dirs) filtered_paths.extend( - map( - self.build_sdk_file_path, - filter(lambda path: path in sdk_dirs, full_fw_paths), - ) + filter(lambda path: path in sdk_dirs, full_fw_paths), ) + filtered_paths = list(map(self.build_sdk_file_path, filtered_paths)) - sdk_env = self.env.Clone() - sdk_env.Replace( + self.sdk_env.Replace( CPPPATH=filtered_paths, - LINKER_SCRIPT=self.env.subst("${APP_LINKER_SCRIPT}"), ORIG_LINKER_SCRIPT_PATH=self.env["LINKER_SCRIPT_PATH"], LINKER_SCRIPT_PATH=self.build_sdk_file_path("${ORIG_LINKER_SCRIPT_PATH}"), ) - - meta = SdkMeta(sdk_env, self) + meta = SdkMeta(self.sdk_env, self) meta.save_to(self.target[0].path) def build_sdk_file_path(self, orig_path: str) -> str: @@ -211,7 +223,7 @@ def validate_sdk_cache(source, target, env): current_sdk = SdkCollector() current_sdk.process_source_file_for_sdk(source[0].path) for h in env["SDK_HEADERS"]: - current_sdk.add_header_to_sdk(pathlib.Path(h.path).as_posix()) + current_sdk.add_header_to_sdk(pathlib.Path(h.srcnode().path).as_posix()) sdk_cache = SdkCache(target[0].path) sdk_cache.validate_api(current_sdk.get_api()) diff --git a/scripts/fbt_tools/fbt_version.py b/scripts/fbt_tools/fbt_version.py index 909eea4f3..87497ca5f 100644 --- a/scripts/fbt_tools/fbt_version.py +++ b/scripts/fbt_tools/fbt_version.py @@ -12,11 +12,14 @@ def version_emitter(target, source, env): def generate(env): + env.SetDefault( + VERSION_SCRIPT="${FBT_SCRIPT_DIR}/version.py", + ) env.Append( BUILDERS={ "VersionBuilder": Builder( action=Action( - '${PYTHON3} "${ROOT_DIR.abspath}/scripts/version.py" generate -t ${TARGET_HW} -o ${TARGET.dir.posix} --dir "${ROOT_DIR}"', + '${PYTHON3} "${VERSION_SCRIPT}" generate -t ${TARGET_HW} -o ${TARGET.dir.posix} --dir "${ROOT_DIR}"', "${VERSIONCOMSTR}", ), emitter=version_emitter, diff --git a/scripts/fbt_tools/fwbin.py b/scripts/fbt_tools/fwbin.py index 67e0d6450..f510c2a60 100644 --- a/scripts/fbt_tools/fwbin.py +++ b/scripts/fbt_tools/fwbin.py @@ -8,7 +8,8 @@ __NM_ARM_BIN = "arm-none-eabi-nm" def generate(env): env.SetDefault( - BIN2DFU="${ROOT_DIR.abspath}/scripts/bin2dfu.py", + BIN2DFU="${FBT_SCRIPT_DIR}/bin2dfu.py", + BIN_SIZE_SCRIPT="${FBT_SCRIPT_DIR}/fwsize.py", OBJCOPY=__OBJCOPY_ARM_BIN, # FIXME NM=__NM_ARM_BIN, # FIXME ) diff --git a/scripts/flipper/app.py b/scripts/flipper/app.py index 958356021..30630a5f9 100644 --- a/scripts/flipper/app.py +++ b/scripts/flipper/app.py @@ -1,6 +1,7 @@ import logging import argparse import sys +import colorlog class App: @@ -10,7 +11,7 @@ class App: self.parser = argparse.ArgumentParser() self.parser.add_argument("-d", "--debug", action="store_true", help="Debug") # Logging - self.logger = logging.getLogger() + self.logger = colorlog.getLogger() # Application specific initialization self.init() @@ -21,10 +22,17 @@ class App: self.log_level = logging.DEBUG if self.args.debug else logging.INFO self.logger.setLevel(self.log_level) if not self.logger.hasHandlers(): - self.handler = logging.StreamHandler(sys.stdout) + self.handler = colorlog.StreamHandler(sys.stdout) self.handler.setLevel(self.log_level) - self.formatter = logging.Formatter( - "%(asctime)s [%(levelname)s] %(message)s" + self.formatter = colorlog.ColoredFormatter( + "%(log_color)s%(asctime)s [%(levelname)s] %(message)s", + log_colors={ + "DEBUG": "cyan", + # "INFO": "white", + "WARNING": "yellow", + "ERROR": "red", + "CRITICAL": "red,bg_white", + }, ) self.handler.setFormatter(self.formatter) self.logger.addHandler(self.handler) diff --git a/scripts/sconsdist.py b/scripts/sconsdist.py index 7636c87bb..b8f1d72b2 100644 --- a/scripts/sconsdist.py +++ b/scripts/sconsdist.py @@ -131,7 +131,9 @@ class Main(App): self.copy_single_project(project) self.logger.info( - fg.green(f"Firmware binaries can be found at:\n\t{self.output_dir_path}") + fg.boldgreen( + f"Firmware binaries can be found at:\n\t{self.output_dir_path}" + ) ) if self.args.version: @@ -167,7 +169,7 @@ class Main(App): if (bundle_result := UpdateMain(no_exit=True)(bundle_args)) == 0: self.logger.info( - fg.green( + fg.boldgreen( f"Use this directory to self-update your Flipper:\n\t{bundle_dir}" ) ) diff --git a/scripts/testing/await_flipper.py b/scripts/testing/await_flipper.py old mode 100644 new mode 100755 diff --git a/scripts/testing/units.py b/scripts/testing/units.py old mode 100644 new mode 100755 diff --git a/scripts/toolchain/fbtenv.cmd b/scripts/toolchain/fbtenv.cmd index a11a3ccd5..6e87bf95a 100644 --- a/scripts/toolchain/fbtenv.cmd +++ b/scripts/toolchain/fbtenv.cmd @@ -13,19 +13,22 @@ if not [%FBT_NOENV%] == [] ( exit /b 0 ) -set "FLIPPER_TOOLCHAIN_VERSION=16" -set "FBT_TOOLCHAIN_ROOT=%FBT_ROOT%\toolchain\x86_64-windows" +set "FLIPPER_TOOLCHAIN_VERSION=17" +if [%FBT_TOOLCHAIN_ROOT%] == [] ( + set "FBT_TOOLCHAIN_ROOT=%FBT_ROOT%\toolchain\x86_64-windows" +) if not exist "%FBT_TOOLCHAIN_ROOT%" ( - powershell -ExecutionPolicy Bypass -File "%FBT_ROOT%\scripts\toolchain\windows-toolchain-download.ps1" "%flipper_toolchain_version%" + powershell -ExecutionPolicy Bypass -File "%FBT_ROOT%\scripts\toolchain\windows-toolchain-download.ps1" "%flipper_toolchain_version%" "%FBT_TOOLCHAIN_ROOT%" ) if not exist "%FBT_TOOLCHAIN_ROOT%\VERSION" ( - powershell -ExecutionPolicy Bypass -File "%FBT_ROOT%\scripts\toolchain\windows-toolchain-download.ps1" "%flipper_toolchain_version%" + powershell -ExecutionPolicy Bypass -File "%FBT_ROOT%\scripts\toolchain\windows-toolchain-download.ps1" "%flipper_toolchain_version%" "%FBT_TOOLCHAIN_ROOT%" ) + set /p REAL_TOOLCHAIN_VERSION=<"%FBT_TOOLCHAIN_ROOT%\VERSION" if not "%REAL_TOOLCHAIN_VERSION%" == "%FLIPPER_TOOLCHAIN_VERSION%" ( - powershell -ExecutionPolicy Bypass -File "%FBT_ROOT%\scripts\toolchain\windows-toolchain-download.ps1" "%flipper_toolchain_version%" + powershell -ExecutionPolicy Bypass -File "%FBT_ROOT%\scripts\toolchain\windows-toolchain-download.ps1" "%flipper_toolchain_version%" "%FBT_TOOLCHAIN_ROOT%" ) diff --git a/scripts/toolchain/fbtenv.sh b/scripts/toolchain/fbtenv.sh index 15f29e4dc..d3fdb8cea 100755 --- a/scripts/toolchain/fbtenv.sh +++ b/scripts/toolchain/fbtenv.sh @@ -5,7 +5,7 @@ # public variables DEFAULT_SCRIPT_PATH="$(pwd -P)"; SCRIPT_PATH="${SCRIPT_PATH:-$DEFAULT_SCRIPT_PATH}"; -FBT_TOOLCHAIN_VERSION="${FBT_TOOLCHAIN_VERSION:-"16"}"; +FBT_TOOLCHAIN_VERSION="${FBT_TOOLCHAIN_VERSION:-"17"}"; FBT_TOOLCHAIN_PATH="${FBT_TOOLCHAIN_PATH:-$SCRIPT_PATH}"; fbtenv_show_usage() @@ -62,7 +62,7 @@ fbtenv_check_sourced() fbtenv_show_usage; return 1; fi - case ${0##*/} in dash|-dash|bash|-bash|ksh|-ksh|sh|-sh|*.sh|fbt) + case ${0##*/} in dash|-dash|bash|-bash|ksh|-ksh|sh|-sh|*.sh|fbt|ufbt) return 0;; esac fbtenv_show_usage; @@ -76,8 +76,8 @@ fbtenv_chck_many_source() return 0; fi fi - echo "Warning! FBT environment script sourced more than once!"; - echo "This may signal that you are making mistakes, please open a new shell!"; + echo "Warning! FBT environment script was sourced more than once!"; + echo "You might be doing things wrong, please open a new shell!"; return 1; } @@ -93,8 +93,8 @@ fbtenv_set_shell_prompt() fbtenv_check_script_path() { - if [ ! -x "$SCRIPT_PATH/fbt" ]; then - echo "Please source this script being into flipperzero-firmware root directory, or specify 'SCRIPT_PATH' manually"; + if [ ! -x "$SCRIPT_PATH/fbt" ] && [ ! -x "$SCRIPT_PATH/ufbt" ] ; then + echo "Please source this script from [u]fbt root directory, or specify 'SCRIPT_PATH' variable manually"; echo "Example:"; printf "\tSCRIPT_PATH=lang/c/flipperzero-firmware source lang/c/flipperzero-firmware/scripts/fbtenv.sh\n"; echo "If current directory is right, type 'unset SCRIPT_PATH' and try again" @@ -108,7 +108,7 @@ fbtenv_get_kernel_type() SYS_TYPE="$(uname -s)"; ARCH_TYPE="$(uname -m)"; if [ "$ARCH_TYPE" != "x86_64" ] && [ "$SYS_TYPE" != "Darwin" ]; then - echo "Now we provide toolchain only for x86_64 arhitecture, sorry.."; + echo "We only provide toolchain for x86_64 CPUs, sorry.."; return 1; fi if [ "$SYS_TYPE" = "Darwin" ]; then @@ -119,10 +119,10 @@ fbtenv_get_kernel_type() TOOLCHAIN_ARCH_DIR="$FBT_TOOLCHAIN_PATH/toolchain/x86_64-linux"; TOOLCHAIN_URL="https://update.flipperzero.one/builds/toolchain/gcc-arm-none-eabi-10.3-x86_64-linux-flipper-$FBT_TOOLCHAIN_VERSION.tar.gz"; elif echo "$SYS_TYPE" | grep -q "MINGW"; then - echo "In MinGW shell use \"fbt.cmd\" instead of \"fbt\""; + echo "In MinGW shell use \"[u]fbt.cmd\" instead of \"[u]fbt\""; return 1; else - echo "Your system is not recognized. Sorry.. Please report us your configuration."; + echo "Your system configuration is not supported. Sorry.. Please report us your configuration."; return 1; fi return 0; @@ -142,7 +142,7 @@ fbtenv_check_rosetta() fbtenv_check_tar() { - printf "Checking tar.."; + printf "Checking for tar.."; if ! tar --version > /dev/null 2>&1; then echo "no"; return 1; @@ -153,7 +153,7 @@ fbtenv_check_tar() fbtenv_check_downloaded_toolchain() { - printf "Checking downloaded toolchain tgz.."; + printf "Checking if downloaded toolchain tgz exists.."; if [ ! -f "$FBT_TOOLCHAIN_PATH/toolchain/$TOOLCHAIN_TAR" ]; then echo "no"; return 1; @@ -204,7 +204,7 @@ fbtenv_unpack_toolchain() fbtenv_clearing() { - printf "Clearing.."; + printf "Cleaning up.."; if [ -n "${FBT_TOOLCHAIN_PATH:-""}" ]; then rm -rf "${FBT_TOOLCHAIN_PATH:?}/toolchain/"*.tar.gz; rm -rf "${FBT_TOOLCHAIN_PATH:?}/toolchain/"*.part; diff --git a/scripts/toolchain/windows-toolchain-download.ps1 b/scripts/toolchain/windows-toolchain-download.ps1 index aaed89856..c96bb119c 100644 --- a/scripts/toolchain/windows-toolchain-download.ps1 +++ b/scripts/toolchain/windows-toolchain-download.ps1 @@ -1,34 +1,46 @@ Set-StrictMode -Version 2.0 $ErrorActionPreference = "Stop" [Net.ServicePointManager]::SecurityProtocol = "tls12, tls11, tls" -$repo_root = (Get-Item "$PSScriptRoot\..\..").FullName +# TODO: fix +$download_dir = (Get-Item "$PSScriptRoot\..\..").FullName $toolchain_version = $args[0] -$toolchain_url = "https://update.flipperzero.one/builds/toolchain/gcc-arm-none-eabi-10.3-x86_64-windows-flipper-$toolchain_version.zip" -$toolchain_zip = "gcc-arm-none-eabi-10.3-x86_64-windows-flipper-$toolchain_version.zip" -$toolchain_dir = "gcc-arm-none-eabi-10.3-x86_64-windows-flipper" +$toolchain_target_path = $args[1] -if (Test-Path -LiteralPath "$repo_root\toolchain\x86_64-windows") { +$toolchain_url = "https://update.flipperzero.one/builds/toolchain/gcc-arm-none-eabi-10.3-x86_64-windows-flipper-$toolchain_version.zip" +$toolchain_dist_folder = "gcc-arm-none-eabi-10.3-x86_64-windows-flipper" +$toolchain_zip = "$toolchain_dist_folder-$toolchain_version.zip" + +$toolchain_zip_temp_path = "$download_dir\$toolchain_zip" +$toolchain_dist_temp_path = "$download_dir\$toolchain_dist_folder" + +if (Test-Path -LiteralPath "$toolchain_target_path") { Write-Host -NoNewline "Removing old Windows toolchain.." - Remove-Item -LiteralPath "$repo_root\toolchain\x86_64-windows" -Force -Recurse + Remove-Item -LiteralPath "$toolchain_target_path" -Force -Recurse Write-Host "done!" } -if (!(Test-Path -Path "$repo_root\$toolchain_zip" -PathType Leaf)) { +if (!(Test-Path -Path "$toolchain_zip_temp_path" -PathType Leaf)) { Write-Host -NoNewline "Downloading Windows toolchain.." $wc = New-Object net.webclient - $wc.Downloadfile("$toolchain_url", "$repo_root\$toolchain_zip") + $wc.Downloadfile("$toolchain_url", "$toolchain_zip_temp_path") Write-Host "done!" } -if (!(Test-Path -LiteralPath "$repo_root\toolchain")) { - New-Item "$repo_root\toolchain" -ItemType Directory +if (!(Test-Path -LiteralPath "$toolchain_target_path\..")) { + New-Item "$toolchain_target_path\.." -ItemType Directory -Force } Write-Host -NoNewline "Extracting Windows toolchain.." +# This is faster than Expand-Archive Add-Type -Assembly "System.IO.Compression.Filesystem" -[System.IO.Compression.ZipFile]::ExtractToDirectory("$repo_root\$toolchain_zip", "$repo_root\") -Move-Item -Path "$repo_root\$toolchain_dir" -Destination "$repo_root\toolchain\x86_64-windows" +[System.IO.Compression.ZipFile]::ExtractToDirectory("$toolchain_zip_temp_path", "$download_dir") +# Expand-Archive -LiteralPath "$toolchain_zip_temp_path" -DestinationPath "$download_dir" + +Write-Host -NoNewline "moving.." +Move-Item -LiteralPath "$toolchain_dist_temp_path" -Destination "$toolchain_target_path" Write-Host "done!" Write-Host -NoNewline "Cleaning up temporary files.." -Remove-Item -LiteralPath "$repo_root\$toolchain_zip" -Force +Remove-Item -LiteralPath "$toolchain_zip_temp_path" -Force Write-Host "done!" + +# dasdasd \ No newline at end of file diff --git a/site_scons/cc.scons b/site_scons/cc.scons index c923b3872..1eb6a3376 100644 --- a/site_scons/cc.scons +++ b/site_scons/cc.scons @@ -30,10 +30,9 @@ ENV.AppendUnique( "-ffunction-sections", "-fsingle-precision-constant", "-fno-math-errno", - "-fstack-usage", + # Generates .su files with stack usage information + # "-fstack-usage", "-g", - # "-Wno-stringop-overread", - # "-Wno-stringop-overflow", ], CPPDEFINES=[ "_GNU_SOURCE", diff --git a/site_scons/environ.scons b/site_scons/environ.scons index 94705dada..96424caad 100644 --- a/site_scons/environ.scons +++ b/site_scons/environ.scons @@ -1,5 +1,10 @@ from SCons.Platform import TempFileMunge -from fbt.util import tempfile_arg_esc_func, single_quote, wrap_tempfile +from fbt.util import ( + tempfile_arg_esc_func, + single_quote, + wrap_tempfile, + extract_abs_dir_path, +) import os import multiprocessing @@ -52,6 +57,12 @@ coreenv = VAR_ENV.Clone( MAXLINELENGTH=2048, PROGSUFFIX=".elf", ENV=forward_os_env, + SINGLEQUOTEFUNC=single_quote, + ABSPATHGETTERFUNC=extract_abs_dir_path, + # Setting up temp file parameters - to overcome command line length limits + TEMPFILEARGESCFUNC=tempfile_arg_esc_func, + FBT_SCRIPT_DIR=Dir("#/scripts"), + ROOT_DIR=Dir("#"), ) # If DIST_SUFFIX is set in environment, is has precedence (set by CI) @@ -60,24 +71,6 @@ if os_suffix := os.environ.get("DIST_SUFFIX", None): DIST_SUFFIX=os_suffix, ) -# print(coreenv.Dump()) -if not coreenv["VERBOSE"]: - coreenv.SetDefault( - CCCOMSTR="\tCC\t${SOURCE}", - CXXCOMSTR="\tCPP\t${SOURCE}", - ASCOMSTR="\tASM\t${SOURCE}", - ARCOMSTR="\tAR\t${TARGET}", - RANLIBCOMSTR="\tRANLIB\t${TARGET}", - LINKCOMSTR="\tLINK\t${TARGET}", - INSTALLSTR="\tINSTALL\t${TARGET}", - APPSCOMSTR="\tAPPS\t${TARGET}", - VERSIONCOMSTR="\tVERSION\t${TARGET}", - STRIPCOMSTR="\tSTRIP\t${TARGET}", - OBJDUMPCOMSTR="\tOBJDUMP\t${TARGET}", - # GDBCOMSTR="\tGDB\t${SOURCE}", - # GDBPYCOMSTR="\tGDB-PY\t${SOURCE}", - ) - # Default value for commandline options SetOption("num_jobs", multiprocessing.cpu_count()) @@ -90,12 +83,7 @@ SetOption("max_drift", 1) # Random task queue - to discover isses with build logic faster # SetOption("random", 1) - -# Setting up temp file parameters - to overcome command line length limits -coreenv["TEMPFILEARGESCFUNC"] = tempfile_arg_esc_func wrap_tempfile(coreenv, "LINKCOM") wrap_tempfile(coreenv, "ARCOM") -coreenv["SINGLEQUOTEFUNC"] = single_quote - Return("coreenv") diff --git a/site_scons/extapps.scons b/site_scons/extapps.scons index 90d228e58..bb1d65ffc 100644 --- a/site_scons/extapps.scons +++ b/site_scons/extapps.scons @@ -3,10 +3,9 @@ from SCons.Errors import UserError Import("ENV") - from fbt.appmanifest import FlipperAppType -appenv = ENV.Clone( +appenv = ENV["APPENV"] = ENV.Clone( tools=[ ( "fbt_extapps", @@ -17,6 +16,7 @@ appenv = ENV.Clone( }, ), "fbt_assets", + "fbt_sdk", ] ) @@ -66,6 +66,7 @@ extapps = appenv["_extapps"] = { "validators": {}, "dist": {}, "resources_dist": None, + "sdk_tree": None, } @@ -115,10 +116,41 @@ if appsrc := appenv.subst("$APPSRC"): app_manifest, fap_file, app_validator = appenv.GetExtAppFromPath(appsrc) appenv.PhonyTarget( "launch_app", - '${PYTHON3} scripts/runfap.py ${SOURCE} --fap_dst_dir "/ext/apps/${FAP_CATEGORY}"', + '${PYTHON3} "${APP_RUN_SCRIPT}" ${SOURCE} --fap_dst_dir "/ext/apps/${FAP_CATEGORY}"', source=fap_file, FAP_CATEGORY=app_manifest.fap_category, ) appenv.Alias("launch_app", app_validator) +# SDK management + +sdk_origin_path = "${BUILD_DIR}/sdk_origin" +sdk_source = appenv.SDKPrebuilder( + sdk_origin_path, + # Deps on root SDK headers and generated files + (appenv["SDK_HEADERS"], appenv["FW_ASSETS_HEADERS"]), +) +# Extra deps on headers included in deeper levels +Depends(sdk_source, appenv.ProcessSdkDepends(f"{sdk_origin_path}.d")) + +appenv["SDK_DIR"] = appenv.Dir("${BUILD_DIR}/sdk") +sdk_tree = extapps["sdk_tree"] = appenv.SDKTree(appenv["SDK_DIR"], sdk_origin_path) +# AlwaysBuild(sdk_tree) +Alias("sdk_tree", sdk_tree) + +sdk_apicheck = appenv.SDKSymUpdater(appenv["SDK_DEFINITION"], sdk_origin_path) +Precious(sdk_apicheck) +NoClean(sdk_apicheck) +AlwaysBuild(sdk_apicheck) +Alias("sdk_check", sdk_apicheck) + +sdk_apisyms = appenv.SDKSymGenerator( + "${BUILD_DIR}/assets/compiled/symbols.h", appenv["SDK_DEFINITION"] +) +Alias("api_syms", sdk_apisyms) + +if appenv["FORCE"]: + appenv.AlwaysBuild(sdk_source, sdk_tree, sdk_apicheck, sdk_apisyms) + + Return("extapps") From a09d0a8bd42d727fe4b686b761cb8012dc99703a Mon Sep 17 00:00:00 2001 From: Konstantin Volkov <72250702+doomwastaken@users.noreply.github.com> Date: Wed, 2 Nov 2022 18:21:43 +0300 Subject: [PATCH 15/49] fixed job name, renamed compile step id (#1952) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: あく --- .github/workflows/unit_tests.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index a7671f0f9..1ca4a9c03 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -8,7 +8,7 @@ env: DEFAULT_TARGET: f7 jobs: - main: + run_units_on_test_bench: runs-on: [self-hosted, FlipperZeroTest] steps: - name: Checkout code @@ -22,14 +22,14 @@ jobs: run: | echo "flipper=/dev/ttyACM0" >> $GITHUB_OUTPUT - - name: 'Compile unit tests firmware' - id: compile + - name: 'Flash unit tests firmware' + id: flashing run: | FBT_TOOLCHAIN_PATH=/opt ./fbt flash OPENOCD_ADAPTER_SERIAL=2A0906016415303030303032 FIRMWARE_APP_SET=unit_tests FORCE=1 - name: 'Wait for flipper to finish updating' id: connect - if: steps.compile.outcome == 'success' + if: steps.flashing.outcome == 'success' run: | . scripts/toolchain/fbtenv.sh ./scripts/testing/await_flipper.py ${{steps.device.outputs.flipper}} From c417d467f72b63b25f7dce0eee6b3554fd2d8d0d Mon Sep 17 00:00:00 2001 From: Georgii Surkov <37121527+gsurkov@users.noreply.github.com> Date: Wed, 2 Nov 2022 19:13:06 +0300 Subject: [PATCH 16/49] Handle storage full error (#1958) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: あく --- applications/services/rpc/rpc_storage.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/applications/services/rpc/rpc_storage.c b/applications/services/rpc/rpc_storage.c index 1b545b414..e28998e13 100644 --- a/applications/services/rpc/rpc_storage.c +++ b/applications/services/rpc/rpc_storage.c @@ -405,6 +405,10 @@ static void rpc_system_storage_write_process(const PB_Main* request, void* conte if(!fs_operation_success) { send_response = true; command_status = rpc_system_storage_get_file_error(file); + if(command_status == PB_CommandStatus_OK) { + // Report errors not handled by underlying APIs + command_status = PB_CommandStatus_ERROR_STORAGE_INTERNAL; + } } if(send_response) { From 0652830c51cee5fced1d4651c538d24160599dfa Mon Sep 17 00:00:00 2001 From: Skorpionm <85568270+Skorpionm@users.noreply.github.com> Date: Wed, 2 Nov 2022 20:24:07 +0400 Subject: [PATCH 17/49] [FL-2940] WS: add protocol Ambient_Weather (#1960) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * WS: add protocol Ambient_Weather * WS: fix link * WS: removing unused code Co-authored-by: あく --- .../helpers/weather_station_types.h | 2 +- .../protocols/acurite_592txr.c | 2 +- .../protocols/ambient_weather.c | 278 ++++++++++++++++++ .../protocols/ambient_weather.h | 79 +++++ .../weather_station/protocols/gt_wt_03.c | 2 +- .../protocols/lacrosse_tx141thbv2.c | 2 +- .../weather_station/protocols/nexus_th.c | 2 +- .../protocols/protocol_items.c | 1 + .../protocols/protocol_items.h | 1 + 9 files changed, 364 insertions(+), 5 deletions(-) create mode 100644 applications/plugins/weather_station/protocols/ambient_weather.c create mode 100644 applications/plugins/weather_station/protocols/ambient_weather.h diff --git a/applications/plugins/weather_station/helpers/weather_station_types.h b/applications/plugins/weather_station/helpers/weather_station_types.h index 2976cbce8..5a66dd0ce 100644 --- a/applications/plugins/weather_station/helpers/weather_station_types.h +++ b/applications/plugins/weather_station/helpers/weather_station_types.h @@ -3,7 +3,7 @@ #include #include -#define WS_VERSION_APP "0.3.1" +#define WS_VERSION_APP "0.4" #define WS_DEVELOPED "SkorP" #define WS_GITHUB "https://github.com/flipperdevices/flipperzero-firmware" diff --git a/applications/plugins/weather_station/protocols/acurite_592txr.c b/applications/plugins/weather_station/protocols/acurite_592txr.c index db05af095..4d7f59544 100644 --- a/applications/plugins/weather_station/protocols/acurite_592txr.c +++ b/applications/plugins/weather_station/protocols/acurite_592txr.c @@ -4,7 +4,7 @@ /* * Help - * https://github.com/merbanan/rtl_433/blob/5bef4e43133ac4c0e2d18d36f87c52b4f9458453/src/devices/acurite.c + * https://github.com/merbanan/rtl_433/blob/master/src/devices/acurite.c * * Acurite 592TXR Temperature Humidity sensor decoder * Message Type 0x04, 7 bytes diff --git a/applications/plugins/weather_station/protocols/ambient_weather.c b/applications/plugins/weather_station/protocols/ambient_weather.c new file mode 100644 index 000000000..07f5330fc --- /dev/null +++ b/applications/plugins/weather_station/protocols/ambient_weather.c @@ -0,0 +1,278 @@ +#include "ambient_weather.h" +#include + +#define TAG "WSProtocolAmbient_Weather" + +/* + * Help + * https://github.com/merbanan/rtl_433/blob/master/src/devices/ambient_weather.c + * + * Decode Ambient Weather F007TH, F012TH, TF 30.3208.02, SwitchDoc F016TH. + * Devices supported: + * - Ambient Weather F007TH Thermo-Hygrometer. + * - Ambient Weather F012TH Indoor/Display Thermo-Hygrometer. + * - TFA senders 30.3208.02 from the TFA "Klima-Monitor" 30.3054, + * - SwitchDoc Labs F016TH. + * This decoder handles the 433mhz/868mhz thermo-hygrometers. + * The 915mhz (WH*) family of devices use different modulation/encoding. + * Byte 0 Byte 1 Byte 2 Byte 3 Byte 4 Byte 5 + * xxxxMMMM IIIIIIII BCCCTTTT TTTTTTTT HHHHHHHH MMMMMMMM + * - x: Unknown 0x04 on F007TH/F012TH + * - M: Model Number?, 0x05 on F007TH/F012TH/SwitchDocLabs F016TH + * - I: ID byte (8 bits), volatie, changes at power up, + * - B: Battery Low + * - C: Channel (3 bits 1-8) - F007TH set by Dip switch, F012TH soft setting + * - T: Temperature 12 bits - Fahrenheit * 10 + 400 + * - H: Humidity (8 bits) + * - M: Message integrity check LFSR Digest-8, gen 0x98, key 0x3e, init 0x64 + * + * three repeats without gap + * full preamble is 0x00145 (the last bits might not be fixed, e.g. 0x00146) + * and on decoding also 0xffd45 + */ + +#define AMBIENT_WEATHER_PACKET_HEADER_1 0xFFD440000000000 //0xffd45 .. 0xffd46 +#define AMBIENT_WEATHER_PACKET_HEADER_2 0x001440000000000 //0x00145 .. 0x00146 +#define AMBIENT_WEATHER_PACKET_HEADER_MASK 0xFFFFC0000000000 + +static const SubGhzBlockConst ws_protocol_ambient_weather_const = { + .te_short = 500, + .te_long = 1000, + .te_delta = 120, + .min_count_bit_for_found = 48, +}; + +struct WSProtocolDecoderAmbient_Weather { + SubGhzProtocolDecoderBase base; + + SubGhzBlockDecoder decoder; + WSBlockGeneric generic; + ManchesterState manchester_saved_state; + uint16_t header_count; +}; + +struct WSProtocolEncoderAmbient_Weather { + SubGhzProtocolEncoderBase base; + + SubGhzProtocolBlockEncoder encoder; + WSBlockGeneric generic; +}; + +const SubGhzProtocolDecoder ws_protocol_ambient_weather_decoder = { + .alloc = ws_protocol_decoder_ambient_weather_alloc, + .free = ws_protocol_decoder_ambient_weather_free, + + .feed = ws_protocol_decoder_ambient_weather_feed, + .reset = ws_protocol_decoder_ambient_weather_reset, + + .get_hash_data = ws_protocol_decoder_ambient_weather_get_hash_data, + .serialize = ws_protocol_decoder_ambient_weather_serialize, + .deserialize = ws_protocol_decoder_ambient_weather_deserialize, + .get_string = ws_protocol_decoder_ambient_weather_get_string, +}; + +const SubGhzProtocolEncoder ws_protocol_ambient_weather_encoder = { + .alloc = NULL, + .free = NULL, + + .deserialize = NULL, + .stop = NULL, + .yield = NULL, +}; + +const SubGhzProtocol ws_protocol_ambient_weather = { + .name = WS_PROTOCOL_AMBIENT_WEATHER_NAME, + .type = SubGhzProtocolWeatherStation, + .flag = SubGhzProtocolFlag_433 | SubGhzProtocolFlag_315 | SubGhzProtocolFlag_868 | + SubGhzProtocolFlag_AM | SubGhzProtocolFlag_Decodable, + + .decoder = &ws_protocol_ambient_weather_decoder, + .encoder = &ws_protocol_ambient_weather_encoder, +}; + +void* ws_protocol_decoder_ambient_weather_alloc(SubGhzEnvironment* environment) { + UNUSED(environment); + WSProtocolDecoderAmbient_Weather* instance = malloc(sizeof(WSProtocolDecoderAmbient_Weather)); + instance->base.protocol = &ws_protocol_ambient_weather; + instance->generic.protocol_name = instance->base.protocol->name; + return instance; +} + +void ws_protocol_decoder_ambient_weather_free(void* context) { + furi_assert(context); + WSProtocolDecoderAmbient_Weather* instance = context; + free(instance); +} + +void ws_protocol_decoder_ambient_weather_reset(void* context) { + furi_assert(context); + WSProtocolDecoderAmbient_Weather* instance = context; + manchester_advance( + instance->manchester_saved_state, + ManchesterEventReset, + &instance->manchester_saved_state, + NULL); +} + +static bool ws_protocol_ambient_weather_check_crc(WSProtocolDecoderAmbient_Weather* instance) { + uint8_t msg[] = { + instance->decoder.decode_data >> 40, + instance->decoder.decode_data >> 32, + instance->decoder.decode_data >> 24, + instance->decoder.decode_data >> 16, + instance->decoder.decode_data >> 8}; + + uint8_t crc = subghz_protocol_blocks_lfsr_digest8(msg, 5, 0x98, 0x3e) ^ 0x64; + return (crc == (uint8_t)(instance->decoder.decode_data & 0xFF)); +} + +/** + * Analysis of received data + * @param instance Pointer to a WSBlockGeneric* instance + */ +static void ws_protocol_ambient_weather_remote_controller(WSBlockGeneric* instance) { + instance->id = (instance->data >> 32) & 0xFF; + instance->battery_low = (instance->data >> 31) & 1; + instance->channel = ((instance->data >> 28) & 0x07) + 1; + instance->temp = ws_block_generic_fahrenheit_to_celsius( + ((float)((instance->data >> 16) & 0x0FFF) - 400.0f) / 10.0f); + instance->humidity = (instance->data >> 8) & 0xFF; + instance->btn = WS_NO_BTN; + + // ToDo maybe it won't be needed + /* + Sanity checks to reduce false positives and other bad data + Packets with Bad data often pass the MIC check. + - humidity > 100 (such as 255) and + - temperatures > 140 F (such as 369.5 F and 348.8 F + Specs in the F007TH and F012TH manuals state the range is: + - Temperature: -40 to 140 F + - Humidity: 10 to 99% + @todo - sanity check b[0] "model number" + - 0x45 - F007TH and F012TH + - 0x?5 - SwitchDocLabs F016TH temperature sensor (based on comment b[0] & 0x0f == 5) + - ? - TFA 30.3208.02 + if (instance->humidity < 0 || instance->humidity > 100) { + ERROR; + } + + if (instance->temp < -40.0 || instance->temp > 140.0) { + ERROR; + } + */ +} + +void ws_protocol_decoder_ambient_weather_feed(void* context, bool level, uint32_t duration) { + furi_assert(context); + WSProtocolDecoderAmbient_Weather* instance = context; + + ManchesterEvent event = ManchesterEventReset; + if(!level) { + if(DURATION_DIFF(duration, ws_protocol_ambient_weather_const.te_short) < + ws_protocol_ambient_weather_const.te_delta) { + event = ManchesterEventShortLow; + } else if( + DURATION_DIFF(duration, ws_protocol_ambient_weather_const.te_long) < + ws_protocol_ambient_weather_const.te_delta * 2) { + event = ManchesterEventLongLow; + } + } else { + if(DURATION_DIFF(duration, ws_protocol_ambient_weather_const.te_short) < + ws_protocol_ambient_weather_const.te_delta) { + event = ManchesterEventShortHigh; + } else if( + DURATION_DIFF(duration, ws_protocol_ambient_weather_const.te_long) < + ws_protocol_ambient_weather_const.te_delta * 2) { + event = ManchesterEventLongHigh; + } + } + if(event != ManchesterEventReset) { + bool data; + bool data_ok = manchester_advance( + instance->manchester_saved_state, event, &instance->manchester_saved_state, &data); + + if(data_ok) { + instance->decoder.decode_data = (instance->decoder.decode_data << 1) | !data; + } + + if(((instance->decoder.decode_data & AMBIENT_WEATHER_PACKET_HEADER_MASK) == + AMBIENT_WEATHER_PACKET_HEADER_1) || + ((instance->decoder.decode_data & AMBIENT_WEATHER_PACKET_HEADER_MASK) == + AMBIENT_WEATHER_PACKET_HEADER_2)) { + if(ws_protocol_ambient_weather_check_crc(instance)) { + instance->decoder.decode_data = instance->decoder.decode_data; + instance->generic.data = instance->decoder.decode_data; + instance->generic.data_count_bit = + ws_protocol_ambient_weather_const.min_count_bit_for_found; + ws_protocol_ambient_weather_remote_controller(&instance->generic); + if(instance->base.callback) + instance->base.callback(&instance->base, instance->base.context); + instance->decoder.decode_data = 0; + instance->decoder.decode_count_bit = 0; + } + } + } else { + instance->decoder.decode_data = 0; + instance->decoder.decode_count_bit = 0; + manchester_advance( + instance->manchester_saved_state, + ManchesterEventReset, + &instance->manchester_saved_state, + NULL); + } +} + +uint8_t ws_protocol_decoder_ambient_weather_get_hash_data(void* context) { + furi_assert(context); + WSProtocolDecoderAmbient_Weather* instance = context; + return subghz_protocol_blocks_get_hash_data( + &instance->decoder, (instance->decoder.decode_count_bit / 8) + 1); +} + +bool ws_protocol_decoder_ambient_weather_serialize( + void* context, + FlipperFormat* flipper_format, + SubGhzRadioPreset* preset) { + furi_assert(context); + WSProtocolDecoderAmbient_Weather* instance = context; + return ws_block_generic_serialize(&instance->generic, flipper_format, preset); +} + +bool ws_protocol_decoder_ambient_weather_deserialize(void* context, FlipperFormat* flipper_format) { + furi_assert(context); + WSProtocolDecoderAmbient_Weather* instance = context; + bool ret = false; + do { + if(!ws_block_generic_deserialize(&instance->generic, flipper_format)) { + break; + } + if(instance->generic.data_count_bit != + ws_protocol_ambient_weather_const.min_count_bit_for_found) { + FURI_LOG_E(TAG, "Wrong number of bits in key"); + break; + } + ret = true; + } while(false); + return ret; +} + +void ws_protocol_decoder_ambient_weather_get_string(void* context, FuriString* output) { + furi_assert(context); + WSProtocolDecoderAmbient_Weather* instance = context; + furi_string_printf( + output, + "%s %dbit\r\n" + "Key:0x%lX%08lX\r\n" + "Sn:0x%lX Ch:%d Bat:%d\r\n" + "Temp:%d.%d C Hum:%d%%", + instance->generic.protocol_name, + instance->generic.data_count_bit, + (uint32_t)(instance->generic.data >> 32), + (uint32_t)(instance->generic.data), + instance->generic.id, + instance->generic.channel, + instance->generic.battery_low, + (int16_t)instance->generic.temp, + abs(((int16_t)(instance->generic.temp * 10) - (((int16_t)instance->generic.temp) * 10))), + instance->generic.humidity); +} diff --git a/applications/plugins/weather_station/protocols/ambient_weather.h b/applications/plugins/weather_station/protocols/ambient_weather.h new file mode 100644 index 000000000..04cc5819c --- /dev/null +++ b/applications/plugins/weather_station/protocols/ambient_weather.h @@ -0,0 +1,79 @@ +#pragma once + +#include + +#include +#include +#include +#include "ws_generic.h" +#include + +#define WS_PROTOCOL_AMBIENT_WEATHER_NAME "Ambient_Weather" + +typedef struct WSProtocolDecoderAmbient_Weather WSProtocolDecoderAmbient_Weather; +typedef struct WSProtocolEncoderAmbient_Weather WSProtocolEncoderAmbient_Weather; + +extern const SubGhzProtocolDecoder ws_protocol_ambient_weather_decoder; +extern const SubGhzProtocolEncoder ws_protocol_ambient_weather_encoder; +extern const SubGhzProtocol ws_protocol_ambient_weather; + +/** + * Allocate WSProtocolDecoderAmbient_Weather. + * @param environment Pointer to a SubGhzEnvironment instance + * @return WSProtocolDecoderAmbient_Weather* pointer to a WSProtocolDecoderAmbient_Weather instance + */ +void* ws_protocol_decoder_ambient_weather_alloc(SubGhzEnvironment* environment); + +/** + * Free WSProtocolDecoderAmbient_Weather. + * @param context Pointer to a WSProtocolDecoderAmbient_Weather instance + */ +void ws_protocol_decoder_ambient_weather_free(void* context); + +/** + * Reset decoder WSProtocolDecoderAmbient_Weather. + * @param context Pointer to a WSProtocolDecoderAmbient_Weather instance + */ +void ws_protocol_decoder_ambient_weather_reset(void* context); + +/** + * Parse a raw sequence of levels and durations received from the air. + * @param context Pointer to a WSProtocolDecoderAmbient_Weather instance + * @param level Signal level true-high false-low + * @param duration Duration of this level in, us + */ +void ws_protocol_decoder_ambient_weather_feed(void* context, bool level, uint32_t duration); + +/** + * Getting the hash sum of the last randomly received parcel. + * @param context Pointer to a WSProtocolDecoderAmbient_Weather instance + * @return hash Hash sum + */ +uint8_t ws_protocol_decoder_ambient_weather_get_hash_data(void* context); + +/** + * Serialize data WSProtocolDecoderAmbient_Weather. + * @param context Pointer to a WSProtocolDecoderAmbient_Weather instance + * @param flipper_format Pointer to a FlipperFormat instance + * @param preset The modulation on which the signal was received, SubGhzRadioPreset + * @return true On success + */ +bool ws_protocol_decoder_ambient_weather_serialize( + void* context, + FlipperFormat* flipper_format, + SubGhzRadioPreset* preset); + +/** + * Deserialize data WSProtocolDecoderAmbient_Weather. + * @param context Pointer to a WSProtocolDecoderAmbient_Weather instance + * @param flipper_format Pointer to a FlipperFormat instance + * @return true On success + */ +bool ws_protocol_decoder_ambient_weather_deserialize(void* context, FlipperFormat* flipper_format); + +/** + * Getting a textual representation of the received data. + * @param context Pointer to a WSProtocolDecoderAmbient_Weather instance + * @param output Resulting text + */ +void ws_protocol_decoder_ambient_weather_get_string(void* context, FuriString* output); diff --git a/applications/plugins/weather_station/protocols/gt_wt_03.c b/applications/plugins/weather_station/protocols/gt_wt_03.c index 1492374c9..04bca9ac1 100644 --- a/applications/plugins/weather_station/protocols/gt_wt_03.c +++ b/applications/plugins/weather_station/protocols/gt_wt_03.c @@ -4,7 +4,7 @@ /* * Help - * https://github.com/merbanan/rtl_433/blob/5f0ff6db624270a4598958ab9dd79bb385ced3ef/src/devices/gt_wt_03.c + * https://github.com/merbanan/rtl_433/blob/master/src/devices/gt_wt_03.c * * * Globaltronics GT-WT-03 sensor on 433.92MHz. diff --git a/applications/plugins/weather_station/protocols/lacrosse_tx141thbv2.c b/applications/plugins/weather_station/protocols/lacrosse_tx141thbv2.c index 828d49be7..d4b89be87 100644 --- a/applications/plugins/weather_station/protocols/lacrosse_tx141thbv2.c +++ b/applications/plugins/weather_station/protocols/lacrosse_tx141thbv2.c @@ -4,7 +4,7 @@ /* * Help - * https://github.com/merbanan/rtl_433/blob/7e83cfd27d14247b6c3c81732bfe4a4f9a974d30/src/devices/lacrosse_tx141x.c + * https://github.com/merbanan/rtl_433/blob/master/src/devices/lacrosse_tx141x.c * * iiii iiii | bkcc tttt | tttt tttt | hhhh hhhh | cccc cccc | u * - i: identification; changes on battery switch diff --git a/applications/plugins/weather_station/protocols/nexus_th.c b/applications/plugins/weather_station/protocols/nexus_th.c index a2ab0f412..c3d823eda 100644 --- a/applications/plugins/weather_station/protocols/nexus_th.c +++ b/applications/plugins/weather_station/protocols/nexus_th.c @@ -4,7 +4,7 @@ /* * Help - * https://github.com/merbanan/rtl_433/blob/ef2d37cf51e3264d11cde9149ef87de2f0a4d37a/src/devices/nexus.c + * https://github.com/merbanan/rtl_433/blob/master/src/devices/nexus.c * * Nexus sensor protocol with ID, temperature and optional humidity * also FreeTec (Pearl) NC-7345 sensors for FreeTec Weatherstation NC-7344, diff --git a/applications/plugins/weather_station/protocols/protocol_items.c b/applications/plugins/weather_station/protocols/protocol_items.c index 3ec9e995a..d7f6458ab 100644 --- a/applications/plugins/weather_station/protocols/protocol_items.c +++ b/applications/plugins/weather_station/protocols/protocol_items.c @@ -9,6 +9,7 @@ const SubGhzProtocol* weather_station_protocol_registry_items[] = { &ws_protocol_lacrosse_tx141thbv2, &ws_protocol_oregon2, &ws_protocol_acurite_592txr, + &ws_protocol_ambient_weather, }; const SubGhzProtocolRegistry weather_station_protocol_registry = { diff --git a/applications/plugins/weather_station/protocols/protocol_items.h b/applications/plugins/weather_station/protocols/protocol_items.h index 8f3eb53d7..76c085ab4 100644 --- a/applications/plugins/weather_station/protocols/protocol_items.h +++ b/applications/plugins/weather_station/protocols/protocol_items.h @@ -9,5 +9,6 @@ #include "lacrosse_tx141thbv2.h" #include "oregon2.h" #include "acurite_592txr.h" +#include "ambient_weather.h" extern const SubGhzProtocolRegistry weather_station_protocol_registry; From 95182b266cfd6fc1d81ec1ff4019bfcca930e1cb Mon Sep 17 00:00:00 2001 From: Nikolay Minaylov Date: Thu, 3 Nov 2022 07:42:54 +0300 Subject: [PATCH 18/49] BadUSB scrolllock typo fix (#1968) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: あく --- applications/main/bad_usb/bad_usb_script.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/applications/main/bad_usb/bad_usb_script.c b/applications/main/bad_usb/bad_usb_script.c index 8ff38ef66..b90218f8d 100644 --- a/applications/main/bad_usb/bad_usb_script.c +++ b/applications/main/bad_usb/bad_usb_script.c @@ -82,7 +82,7 @@ static const DuckyKey ducky_keys[] = { {"PAGEUP", HID_KEYBOARD_PAGE_UP}, {"PAGEDOWN", HID_KEYBOARD_PAGE_DOWN}, {"PRINTSCREEN", HID_KEYBOARD_PRINT_SCREEN}, - {"SCROLLOCK", HID_KEYBOARD_SCROLL_LOCK}, + {"SCROLLLOCK", HID_KEYBOARD_SCROLL_LOCK}, {"SPACE", HID_KEYBOARD_SPACEBAR}, {"TAB", HID_KEYBOARD_TAB}, {"MENU", HID_KEYBOARD_APPLICATION}, From eee90c6c406fef114be9b2250936c5e36f0a46cd Mon Sep 17 00:00:00 2001 From: head47 <63517545+head47@users.noreply.github.com> Date: Thu, 3 Nov 2022 08:21:44 +0300 Subject: [PATCH 19/49] Run Bad USB immediately after connection (#1955) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: あく --- applications/main/bad_usb/bad_usb_script.c | 31 ++++++++++++++++++- applications/main/bad_usb/bad_usb_script.h | 1 + .../main/bad_usb/views/bad_usb_view.c | 10 +++++- 3 files changed, 40 insertions(+), 2 deletions(-) diff --git a/applications/main/bad_usb/bad_usb_script.c b/applications/main/bad_usb/bad_usb_script.c index b90218f8d..33b3f5030 100644 --- a/applications/main/bad_usb/bad_usb_script.c +++ b/applications/main/bad_usb/bad_usb_script.c @@ -524,12 +524,16 @@ static int32_t bad_usb_worker(void* context) { } else if(worker_state == BadUsbStateNotConnected) { // State: USB not connected uint32_t flags = furi_thread_flags_wait( - WorkerEvtEnd | WorkerEvtConnect, FuriFlagWaitAny, FuriWaitForever); + WorkerEvtEnd | WorkerEvtConnect | WorkerEvtToggle, + FuriFlagWaitAny, + FuriWaitForever); furi_check((flags & FuriFlagError) == 0); if(flags & WorkerEvtEnd) { break; } else if(flags & WorkerEvtConnect) { worker_state = BadUsbStateIdle; // Ready to run + } else if(flags & WorkerEvtToggle) { + worker_state = BadUsbStateWillRun; // Will run when USB is connected } bad_usb->st.state = worker_state; @@ -556,6 +560,31 @@ static int32_t bad_usb_worker(void* context) { } bad_usb->st.state = worker_state; + } else if(worker_state == BadUsbStateWillRun) { // State: start on connection + uint32_t flags = furi_thread_flags_wait( + WorkerEvtEnd | WorkerEvtConnect | WorkerEvtToggle, + FuriFlagWaitAny, + FuriWaitForever); + furi_check((flags & FuriFlagError) == 0); + if(flags & WorkerEvtEnd) { + break; + } else if(flags & WorkerEvtConnect) { // Start executing script + DOLPHIN_DEED(DolphinDeedBadUsbPlayScript); + delay_val = 0; + bad_usb->buf_len = 0; + bad_usb->st.line_cur = 0; + bad_usb->defdelay = 0; + bad_usb->repeat_cnt = 0; + bad_usb->file_end = false; + storage_file_seek(script_file, 0, true); + // extra time for PC to recognize Flipper as keyboard + furi_thread_flags_wait(0, FuriFlagWaitAny, 1500); + worker_state = BadUsbStateRunning; + } else if(flags & WorkerEvtToggle) { // Cancel scheduled execution + worker_state = BadUsbStateNotConnected; + } + bad_usb->st.state = worker_state; + } else if(worker_state == BadUsbStateRunning) { // State: running uint16_t delay_cur = (delay_val > 1000) ? (1000) : (delay_val); uint32_t flags = furi_thread_flags_wait( diff --git a/applications/main/bad_usb/bad_usb_script.h b/applications/main/bad_usb/bad_usb_script.h index f24372fab..188142db8 100644 --- a/applications/main/bad_usb/bad_usb_script.h +++ b/applications/main/bad_usb/bad_usb_script.h @@ -12,6 +12,7 @@ typedef enum { BadUsbStateInit, BadUsbStateNotConnected, BadUsbStateIdle, + BadUsbStateWillRun, BadUsbStateRunning, BadUsbStateDelay, BadUsbStateDone, diff --git a/applications/main/bad_usb/views/bad_usb_view.c b/applications/main/bad_usb/views/bad_usb_view.c index e5c5d92a3..b3eb9bb56 100644 --- a/applications/main/bad_usb/views/bad_usb_view.c +++ b/applications/main/bad_usb/views/bad_usb_view.c @@ -29,10 +29,13 @@ static void bad_usb_draw_callback(Canvas* canvas, void* _model) { canvas_draw_icon(canvas, 22, 20, &I_UsbTree_48x22); - if((model->state.state == BadUsbStateIdle) || (model->state.state == BadUsbStateDone)) { + if((model->state.state == BadUsbStateIdle) || (model->state.state == BadUsbStateDone) || + (model->state.state == BadUsbStateNotConnected)) { elements_button_center(canvas, "Run"); } else if((model->state.state == BadUsbStateRunning) || (model->state.state == BadUsbStateDelay)) { elements_button_center(canvas, "Stop"); + } else if(model->state.state == BadUsbStateWillRun) { + elements_button_center(canvas, "Cancel"); } if(model->state.state == BadUsbStateNotConnected) { @@ -40,6 +43,11 @@ static void bad_usb_draw_callback(Canvas* canvas, void* _model) { canvas_set_font(canvas, FontPrimary); canvas_draw_str_aligned(canvas, 127, 27, AlignRight, AlignBottom, "Connect"); canvas_draw_str_aligned(canvas, 127, 39, AlignRight, AlignBottom, "to USB"); + } else if(model->state.state == BadUsbStateWillRun) { + canvas_draw_icon(canvas, 4, 22, &I_Clock_18x18); + canvas_set_font(canvas, FontPrimary); + canvas_draw_str_aligned(canvas, 127, 27, AlignRight, AlignBottom, "Will run"); + canvas_draw_str_aligned(canvas, 127, 39, AlignRight, AlignBottom, "on connect"); } else if(model->state.state == BadUsbStateFileError) { canvas_draw_icon(canvas, 4, 22, &I_Error_18x18); canvas_set_font(canvas, FontPrimary); From 60d125e72a832d300a487b3668187f7a0fe6e996 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20D=C3=ADaz?= Date: Thu, 3 Nov 2022 08:57:56 +0100 Subject: [PATCH 20/49] subghz: add analyzer frequency logs (#1914) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * subghz: add analyzer frequency logs * SubGhz: switch to change on short press * SubGhz: use full RSSI bar for history view Co-authored-by: あく --- ...subghz_frequency_analyzer_log_item_array.c | 27 ++ ...subghz_frequency_analyzer_log_item_array.h | 73 +++++ .../subghz/views/subghz_frequency_analyzer.c | 280 ++++++++++++++++-- 3 files changed, 355 insertions(+), 25 deletions(-) create mode 100644 applications/main/subghz/helpers/subghz_frequency_analyzer_log_item_array.c create mode 100644 applications/main/subghz/helpers/subghz_frequency_analyzer_log_item_array.h diff --git a/applications/main/subghz/helpers/subghz_frequency_analyzer_log_item_array.c b/applications/main/subghz/helpers/subghz_frequency_analyzer_log_item_array.c new file mode 100644 index 000000000..9b33e92d1 --- /dev/null +++ b/applications/main/subghz/helpers/subghz_frequency_analyzer_log_item_array.c @@ -0,0 +1,27 @@ +#include "subghz_frequency_analyzer_log_item_array.h" + +const char* + subghz_frequency_analyzer_log_get_order_name(SubGhzFrequencyAnalyzerLogOrderBy order_by) { + if(order_by == SubGhzFrequencyAnalyzerLogOrderBySeqAsc) { + return "Seq. A"; + } + if(order_by == SubGhzFrequencyAnalyzerLogOrderByCountDesc) { + return "Count D"; + } + if(order_by == SubGhzFrequencyAnalyzerLogOrderByCountAsc) { + return "Count A"; + } + if(order_by == SubGhzFrequencyAnalyzerLogOrderByRSSIDesc) { + return "RSSI D"; + } + if(order_by == SubGhzFrequencyAnalyzerLogOrderByRSSIAsc) { + return "RSSI A"; + } + if(order_by == SubGhzFrequencyAnalyzerLogOrderByFrequencyDesc) { + return "Freq. D"; + } + if(order_by == SubGhzFrequencyAnalyzerLogOrderByFrequencyAsc) { + return "Freq. A"; + } + return "Seq. D"; +} diff --git a/applications/main/subghz/helpers/subghz_frequency_analyzer_log_item_array.h b/applications/main/subghz/helpers/subghz_frequency_analyzer_log_item_array.h new file mode 100644 index 000000000..eaf53b663 --- /dev/null +++ b/applications/main/subghz/helpers/subghz_frequency_analyzer_log_item_array.h @@ -0,0 +1,73 @@ +#pragma once + +#include +#include +#include +#include + +typedef enum { + SubGhzFrequencyAnalyzerLogOrderBySeqDesc, + SubGhzFrequencyAnalyzerLogOrderBySeqAsc, + SubGhzFrequencyAnalyzerLogOrderByCountDesc, + SubGhzFrequencyAnalyzerLogOrderByCountAsc, + SubGhzFrequencyAnalyzerLogOrderByRSSIDesc, + SubGhzFrequencyAnalyzerLogOrderByRSSIAsc, + SubGhzFrequencyAnalyzerLogOrderByFrequencyDesc, + SubGhzFrequencyAnalyzerLogOrderByFrequencyAsc, +} SubGhzFrequencyAnalyzerLogOrderBy; + +const char* + subghz_frequency_analyzer_log_get_order_name(SubGhzFrequencyAnalyzerLogOrderBy order_by); + +TUPLE_DEF2( + SubGhzFrequencyAnalyzerLogItem, + (seq, uint8_t), + (frequency, uint32_t), + (count, uint8_t), + (rssi_max, uint8_t)) +/* Register globaly the oplist */ +#define M_OPL_SubGhzFrequencyAnalyzerLogItem_t() \ + TUPLE_OPLIST(SubGhzFrequencyAnalyzerLogItem, M_POD_OPLIST, M_DEFAULT_OPLIST, M_DEFAULT_OPLIST) + +/* Define the array, register the oplist and define further algorithms on it */ +ARRAY_DEF(SubGhzFrequencyAnalyzerLogItemArray, SubGhzFrequencyAnalyzerLogItem_t) +#define M_OPL_SubGhzFrequencyAnalyzerLogItemArray_t() \ + ARRAY_OPLIST(SubGhzFrequencyAnalyzerLogItemArray, M_OPL_SubGhzFrequencyAnalyzerLogItem_t()) +ALGO_DEF(SubGhzFrequencyAnalyzerLogItemArray, SubGhzFrequencyAnalyzerLogItemArray_t) + +FUNC_OBJ_INS_DEF( + SubGhzFrequencyAnalyzerLogItemArray_compare_by /* name of the instance */, + SubGhzFrequencyAnalyzerLogItemArray_cmp_obj /* name of the interface */, + (a, + b) /* name of the input parameters of the function like object. The type are inherited from the interface. */ + , + { + /* code of the function object */ + if(self->order_by == SubGhzFrequencyAnalyzerLogOrderByFrequencyAsc) { + return a->frequency < b->frequency ? -1 : a->frequency > b->frequency; + } + if(self->order_by == SubGhzFrequencyAnalyzerLogOrderByFrequencyDesc) { + return a->frequency > b->frequency ? -1 : a->frequency < b->frequency; + } + if(self->order_by == SubGhzFrequencyAnalyzerLogOrderByRSSIAsc) { + return a->rssi_max < b->rssi_max ? -1 : a->rssi_max > b->rssi_max; + } + if(self->order_by == SubGhzFrequencyAnalyzerLogOrderByRSSIDesc) { + return a->rssi_max > b->rssi_max ? -1 : a->rssi_max < b->rssi_max; + } + if(self->order_by == SubGhzFrequencyAnalyzerLogOrderByCountAsc) { + return a->count < b->count ? -1 : a->count > b->count; + } + if(self->order_by == SubGhzFrequencyAnalyzerLogOrderByCountDesc) { + return a->count > b->count ? -1 : a->count < b->count; + } + if(self->order_by == SubGhzFrequencyAnalyzerLogOrderBySeqAsc) { + return a->seq < b->seq ? -1 : a->seq > b->seq; + } + + return a->seq > b->seq ? -1 : a->seq < b->seq; + }, + /* Additional fields stored in the function object */ + (order_by, SubGhzFrequencyAnalyzerLogOrderBy)) +#define M_OPL_SubGhzFrequencyAnalyzerLogItemArray_compare_by_t() \ + FUNC_OBJ_INS_OPLIST(SubGhzFrequencyAnalyzerLogItemArray_compare_by, M_DEFAULT_OPLIST) diff --git a/applications/main/subghz/views/subghz_frequency_analyzer.c b/applications/main/subghz/views/subghz_frequency_analyzer.c index c169f3611..e980bd970 100644 --- a/applications/main/subghz/views/subghz_frequency_analyzer.c +++ b/applications/main/subghz/views/subghz_frequency_analyzer.c @@ -5,30 +5,53 @@ #include #include #include +#include #include #include "../helpers/subghz_frequency_analyzer_worker.h" +#include "../helpers/subghz_frequency_analyzer_log_item_array.h" #include +#define LOG_FREQUENCY_MAX_ITEMS 60 // uint8_t (limited by 'seq' of SubGhzFrequencyAnalyzerLogItem) +#define RSSI_OFFSET 74 +#define RSSI_MAX 53 // 127 - RSSI_OFFSET + +#define SNPRINTF_FREQUENCY(buff, freq) \ + snprintf(buff, sizeof(buff), "%03ld.%03ld", freq / 1000000 % 1000, freq / 1000 % 1000); + typedef enum { SubGhzFrequencyAnalyzerStatusIDLE, } SubGhzFrequencyAnalyzerStatus; +typedef enum { + SubGhzFrequencyAnalyzerFragmentBottomTypeMain, + SubGhzFrequencyAnalyzerFragmentBottomTypeLog, +} SubGhzFrequencyAnalyzerFragmentBottomType; + struct SubGhzFrequencyAnalyzer { View* view; SubGhzFrequencyAnalyzerWorker* worker; SubGhzFrequencyAnalyzerCallback callback; void* context; bool locked; + uint32_t last_frequency; }; typedef struct { uint32_t frequency; - float rssi; + uint8_t rssi; uint32_t history_frequency[3]; bool signal; + SubGhzFrequencyAnalyzerLogItemArray_t log_frequency; + SubGhzFrequencyAnalyzerFragmentBottomType fragment_bottom_type; + SubGhzFrequencyAnalyzerLogOrderBy log_frequency_order_by; + uint8_t log_frequency_scroll_offset; } SubGhzFrequencyAnalyzerModel; +static inline uint8_t rssi_sanitize(float rssi) { + return (rssi * -1.0f) - RSSI_OFFSET; +} + void subghz_frequency_analyzer_set_callback( SubGhzFrequencyAnalyzer* subghz_frequency_analyzer, SubGhzFrequencyAnalyzerCallback callback, @@ -39,13 +62,11 @@ void subghz_frequency_analyzer_set_callback( subghz_frequency_analyzer->context = context; } -void subghz_frequency_analyzer_draw_rssi(Canvas* canvas, float rssi) { - uint8_t x = 20; - uint8_t y = 64; +void subghz_frequency_analyzer_draw_rssi(Canvas* canvas, uint8_t rssi, uint8_t x, uint8_t y) { uint8_t column_number = 0; if(rssi) { - rssi = (rssi + 90) / 3; - for(size_t i = 1; i < (uint8_t)rssi; i++) { + rssi = rssi / 3; + for(uint8_t i = 1; i < rssi; i++) { if(i > 20) break; if(i % 4) { column_number++; @@ -55,6 +76,54 @@ void subghz_frequency_analyzer_draw_rssi(Canvas* canvas, float rssi) { } } +static void subghz_frequency_analyzer_log_frequency_draw( + Canvas* canvas, + SubGhzFrequencyAnalyzerModel* model) { + char buffer[64]; + const uint8_t offset_x = 0; + const uint8_t offset_y = 43; + canvas_set_font(canvas, FontKeyboard); + + const size_t items_count = SubGhzFrequencyAnalyzerLogItemArray_size(model->log_frequency); + if(items_count == 0) { + canvas_draw_rframe(canvas, offset_x + 27u, offset_y - 3u, 73u, 16u, 5u); + canvas_draw_str_aligned( + canvas, offset_x + 64u, offset_y + 8u, AlignCenter, AlignBottom, "No records"); + return; + } else if(items_count > 3) { + elements_scrollbar_pos( + canvas, + offset_x + 127, + offset_y - 8, + 29, + model->log_frequency_scroll_offset, + items_count - 2); + } + + SubGhzFrequencyAnalyzerLogItem_t* log_frequency_item; + for(uint8_t i = 0; i < 3; ++i) { + const uint8_t item_pos = model->log_frequency_scroll_offset + i; + if(item_pos >= items_count) { + break; + } + log_frequency_item = + SubGhzFrequencyAnalyzerLogItemArray_get(model->log_frequency, item_pos); + // Frequency + SNPRINTF_FREQUENCY(buffer, (*log_frequency_item)->frequency) + canvas_draw_str(canvas, offset_x, offset_y + i * 10, buffer); + + // Count + snprintf(buffer, sizeof(buffer), "%3d", (*log_frequency_item)->count); + canvas_draw_str(canvas, offset_x + 48, offset_y + i * 10, buffer); + + // Max RSSI + subghz_frequency_analyzer_draw_rssi( + canvas, (*log_frequency_item)->rssi_max, offset_x + 69, (offset_y + i * 10)); + } + + canvas_set_font(canvas, FontSecondary); +} + static void subghz_frequency_analyzer_history_frequency_draw( Canvas* canvas, SubGhzFrequencyAnalyzerModel* model) { @@ -65,12 +134,7 @@ static void subghz_frequency_analyzer_history_frequency_draw( canvas_set_font(canvas, FontKeyboard); for(uint8_t i = 0; i < 3; i++) { if(model->history_frequency[i]) { - snprintf( - buffer, - sizeof(buffer), - "%03ld.%03ld", - model->history_frequency[i] / 1000000 % 1000, - model->history_frequency[i] / 1000 % 1000); + SNPRINTF_FREQUENCY(buffer, model->history_frequency[i]) canvas_draw_str(canvas, x, y + i * 10, buffer); } else { canvas_draw_str(canvas, x, y + i * 10, "---.---"); @@ -81,18 +145,34 @@ static void subghz_frequency_analyzer_history_frequency_draw( } void subghz_frequency_analyzer_draw(Canvas* canvas, SubGhzFrequencyAnalyzerModel* model) { + furi_assert(canvas); + furi_assert(model); char buffer[64]; canvas_set_color(canvas, ColorBlack); canvas_set_font(canvas, FontSecondary); - canvas_draw_str(canvas, 20, 8, "Frequency Analyzer"); - canvas_draw_str(canvas, 0, 64, "RSSI"); - subghz_frequency_analyzer_draw_rssi(canvas, model->rssi); + if(model->fragment_bottom_type == SubGhzFrequencyAnalyzerFragmentBottomTypeLog) { + const size_t items_count = SubGhzFrequencyAnalyzerLogItemArray_size(model->log_frequency); + const char* log_order_by_name = + subghz_frequency_analyzer_log_get_order_name(model->log_frequency_order_by); + if(items_count < LOG_FREQUENCY_MAX_ITEMS) { + snprintf(buffer, sizeof(buffer), "Frequency Analyzer [%s]", log_order_by_name); + canvas_draw_str_aligned(canvas, 64, 8, AlignCenter, AlignBottom, buffer); + } else { + snprintf(buffer, sizeof(buffer), "The log is full! [%s]", log_order_by_name); + canvas_draw_str(canvas, 2, 8, buffer); + } + subghz_frequency_analyzer_log_frequency_draw(canvas, model); + } else { + canvas_draw_str(canvas, 20, 8, "Frequency Analyzer"); + canvas_draw_str(canvas, 0, 64, "RSSI"); + subghz_frequency_analyzer_draw_rssi(canvas, model->rssi, 20u, 64u); - subghz_frequency_analyzer_history_frequency_draw(canvas, model); + subghz_frequency_analyzer_history_frequency_draw(canvas, model); + } - //Frequency + // Frequency canvas_set_font(canvas, FontBigNumbers); snprintf( buffer, @@ -103,23 +183,151 @@ void subghz_frequency_analyzer_draw(Canvas* canvas, SubGhzFrequencyAnalyzerModel if(model->signal) { canvas_draw_box(canvas, 4, 12, 121, 22); canvas_set_color(canvas, ColorWhite); - } else { } - canvas_draw_str(canvas, 8, 30, buffer); canvas_draw_icon(canvas, 96, 19, &I_MHz_25x11); } +static void subghz_frequency_analyzer_log_frequency_sort(SubGhzFrequencyAnalyzerModel* model) { + furi_assert(model); + M_LET((cmp, model->log_frequency_order_by), SubGhzFrequencyAnalyzerLogItemArray_compare_by_t) + SubGhzFrequencyAnalyzerLogItemArray_sort_fo( + model->log_frequency, SubGhzFrequencyAnalyzerLogItemArray_compare_by_as_interface(cmp)); +} + bool subghz_frequency_analyzer_input(InputEvent* event, void* context) { furi_assert(context); + SubGhzFrequencyAnalyzer* instance = context; if(event->key == InputKeyBack) { return false; } + if((event->type == InputTypeShort) && + ((event->key == InputKeyLeft) || (event->key == InputKeyRight))) { + with_view_model( + instance->view, + SubGhzFrequencyAnalyzerModel * model, + { + if(event->key == InputKeyLeft) { + if(model->fragment_bottom_type == 0) { + model->fragment_bottom_type = SubGhzFrequencyAnalyzerFragmentBottomTypeLog; + } else { + --model->fragment_bottom_type; + } + } else if(event->key == InputKeyRight) { + if(model->fragment_bottom_type == + SubGhzFrequencyAnalyzerFragmentBottomTypeLog) { + model->fragment_bottom_type = 0; + } else { + ++model->fragment_bottom_type; + } + } + }, + true); + } else if((event->type == InputTypeShort) && (event->key == InputKeyOk)) { + with_view_model( + instance->view, + SubGhzFrequencyAnalyzerModel * model, + { + if(model->fragment_bottom_type == SubGhzFrequencyAnalyzerFragmentBottomTypeLog) { + ++model->log_frequency_order_by; + if(model->log_frequency_order_by > + SubGhzFrequencyAnalyzerLogOrderByFrequencyAsc) { + model->log_frequency_order_by = 0; + } + subghz_frequency_analyzer_log_frequency_sort(model); + } + }, + true); + } else if((event->type == InputTypeShort) || (event->type == InputTypeRepeat)) { + with_view_model( + instance->view, + SubGhzFrequencyAnalyzerModel * model, + { + if(model->fragment_bottom_type == SubGhzFrequencyAnalyzerFragmentBottomTypeLog) { + if(event->key == InputKeyUp) { + if(model->log_frequency_scroll_offset > 0) { + --model->log_frequency_scroll_offset; + } + } else if(event->key == InputKeyDown) { + const size_t items_count = + SubGhzFrequencyAnalyzerLogItemArray_size(model->log_frequency); + if((model->log_frequency_scroll_offset + 3u) < items_count) { + ++model->log_frequency_scroll_offset; + } + } + } + }, + true); + } + return true; } +static void subghz_frequency_analyzer_log_frequency_search_it( + SubGhzFrequencyAnalyzerLogItemArray_it_t* itref, + SubGhzFrequencyAnalyzerLogItemArray_t* log_frequency, + uint32_t frequency) { + furi_assert(log_frequency); + + SubGhzFrequencyAnalyzerLogItemArray_it(*itref, *log_frequency); + SubGhzFrequencyAnalyzerLogItem_t* item; + while(!SubGhzFrequencyAnalyzerLogItemArray_end_p(*itref)) { + item = SubGhzFrequencyAnalyzerLogItemArray_ref(*itref); + if((*item)->frequency == frequency) { + break; + } + SubGhzFrequencyAnalyzerLogItemArray_next(*itref); + } +} + +static bool subghz_frequency_analyzer_log_frequency_insert(SubGhzFrequencyAnalyzerModel* model) { + furi_assert(model); + const size_t items_count = SubGhzFrequencyAnalyzerLogItemArray_size(model->log_frequency); + if(items_count < LOG_FREQUENCY_MAX_ITEMS) { + SubGhzFrequencyAnalyzerLogItem_t* item = + SubGhzFrequencyAnalyzerLogItemArray_push_new(model->log_frequency); + if(item == NULL) { + return false; + } + (*item)->frequency = model->frequency; + (*item)->count = 1u; + (*item)->rssi_max = model->rssi; + (*item)->seq = items_count; + return true; + } + return false; +} + +static void subghz_frequency_analyzer_log_frequency_update( + SubGhzFrequencyAnalyzerModel* model, + bool need_insert) { + furi_assert(model); + if(!model->frequency) { + return; + } + + SubGhzFrequencyAnalyzerLogItemArray_it_t it; + subghz_frequency_analyzer_log_frequency_search_it( + &it, &model->log_frequency, model->frequency); + if(!SubGhzFrequencyAnalyzerLogItemArray_end_p(it)) { + SubGhzFrequencyAnalyzerLogItem_t* item = SubGhzFrequencyAnalyzerLogItemArray_ref(it); + if((*item)->rssi_max < model->rssi) { + (*item)->rssi_max = model->rssi; + } + + if(need_insert && (*item)->count < UINT8_MAX) { + ++(*item)->count; + subghz_frequency_analyzer_log_frequency_sort(model); + } + } else if(need_insert) { + if(subghz_frequency_analyzer_log_frequency_insert(model)) { + subghz_frequency_analyzer_log_frequency_sort(model); + } + } +} + void subghz_frequency_analyzer_pair_callback( void* context, uint32_t frequency, @@ -130,6 +338,7 @@ void subghz_frequency_analyzer_pair_callback( if(instance->callback) { instance->callback(SubGhzCustomEventSceneAnalyzerUnlock, instance->context); } + instance->last_frequency = 0; //update history with_view_model( instance->view, @@ -151,9 +360,14 @@ void subghz_frequency_analyzer_pair_callback( instance->view, SubGhzFrequencyAnalyzerModel * model, { - model->rssi = rssi; + model->rssi = rssi_sanitize(rssi); model->frequency = frequency; model->signal = signal; + if(frequency) { + subghz_frequency_analyzer_log_frequency_update( + model, frequency != instance->last_frequency); + instance->last_frequency = frequency; + } }, true); } @@ -176,11 +390,14 @@ void subghz_frequency_analyzer_enter(void* context) { instance->view, SubGhzFrequencyAnalyzerModel * model, { - model->rssi = 0; + model->rssi = 0u; model->frequency = 0; - model->history_frequency[2] = 0; - model->history_frequency[1] = 0; - model->history_frequency[0] = 0; + model->fragment_bottom_type = SubGhzFrequencyAnalyzerFragmentBottomTypeMain; + model->log_frequency_order_by = SubGhzFrequencyAnalyzerLogOrderBySeqDesc; + model->log_frequency_scroll_offset = 0u; + model->history_frequency[0] = model->history_frequency[1] = + model->history_frequency[2] = 0u; + SubGhzFrequencyAnalyzerLogItemArray_init(model->log_frequency); }, true); } @@ -196,13 +413,26 @@ void subghz_frequency_analyzer_exit(void* context) { subghz_frequency_analyzer_worker_free(instance->worker); with_view_model( - instance->view, SubGhzFrequencyAnalyzerModel * model, { model->rssi = 0; }, true); + instance->view, + SubGhzFrequencyAnalyzerModel * model, + { + model->rssi = 0u; + model->frequency = 0; + model->fragment_bottom_type = SubGhzFrequencyAnalyzerFragmentBottomTypeMain; + model->log_frequency_order_by = SubGhzFrequencyAnalyzerLogOrderBySeqDesc; + model->log_frequency_scroll_offset = 0u; + model->history_frequency[0] = model->history_frequency[1] = + model->history_frequency[2] = 0u; + SubGhzFrequencyAnalyzerLogItemArray_clear(model->log_frequency); + }, + true); } SubGhzFrequencyAnalyzer* subghz_frequency_analyzer_alloc() { SubGhzFrequencyAnalyzer* instance = malloc(sizeof(SubGhzFrequencyAnalyzer)); // View allocation and configuration + instance->last_frequency = 0; instance->view = view_alloc(); view_allocate_model( instance->view, ViewModelTypeLocking, sizeof(SubGhzFrequencyAnalyzerModel)); From e3ea5bca7652d2e1b7161c833fb30cdfa93dbf34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=82=E3=81=8F?= Date: Fri, 4 Nov 2022 13:44:28 +0900 Subject: [PATCH 21/49] Dolphin: add L1_Mods_128x64 animation (#1973) --- .../dolphin/external/L1_Mods_128x64/frame_0.png | Bin 0 -> 4344 bytes .../dolphin/external/L1_Mods_128x64/frame_1.png | Bin 0 -> 4351 bytes .../dolphin/external/L1_Mods_128x64/frame_10.png | Bin 0 -> 4370 bytes .../dolphin/external/L1_Mods_128x64/frame_11.png | Bin 0 -> 4342 bytes .../dolphin/external/L1_Mods_128x64/frame_12.png | Bin 0 -> 4327 bytes .../dolphin/external/L1_Mods_128x64/frame_13.png | Bin 0 -> 4363 bytes .../dolphin/external/L1_Mods_128x64/frame_14.png | Bin 0 -> 4306 bytes .../dolphin/external/L1_Mods_128x64/frame_15.png | Bin 0 -> 4325 bytes .../dolphin/external/L1_Mods_128x64/frame_16.png | Bin 0 -> 4346 bytes .../dolphin/external/L1_Mods_128x64/frame_17.png | Bin 0 -> 4338 bytes .../dolphin/external/L1_Mods_128x64/frame_18.png | Bin 0 -> 4346 bytes .../dolphin/external/L1_Mods_128x64/frame_19.png | Bin 0 -> 4339 bytes .../dolphin/external/L1_Mods_128x64/frame_2.png | Bin 0 -> 4344 bytes .../dolphin/external/L1_Mods_128x64/frame_20.png | Bin 0 -> 4314 bytes .../dolphin/external/L1_Mods_128x64/frame_21.png | Bin 0 -> 4357 bytes .../dolphin/external/L1_Mods_128x64/frame_22.png | Bin 0 -> 4320 bytes .../dolphin/external/L1_Mods_128x64/frame_23.png | Bin 0 -> 4332 bytes .../dolphin/external/L1_Mods_128x64/frame_24.png | Bin 0 -> 4284 bytes .../dolphin/external/L1_Mods_128x64/frame_25.png | Bin 0 -> 4149 bytes .../dolphin/external/L1_Mods_128x64/frame_26.png | Bin 0 -> 4260 bytes .../dolphin/external/L1_Mods_128x64/frame_27.png | Bin 0 -> 4376 bytes .../dolphin/external/L1_Mods_128x64/frame_28.png | Bin 0 -> 4393 bytes .../dolphin/external/L1_Mods_128x64/frame_29.png | Bin 0 -> 4380 bytes .../dolphin/external/L1_Mods_128x64/frame_3.png | Bin 0 -> 4341 bytes .../dolphin/external/L1_Mods_128x64/frame_30.png | Bin 0 -> 4390 bytes .../dolphin/external/L1_Mods_128x64/frame_31.png | Bin 0 -> 4383 bytes .../dolphin/external/L1_Mods_128x64/frame_32.png | Bin 0 -> 4402 bytes .../dolphin/external/L1_Mods_128x64/frame_33.png | Bin 0 -> 4340 bytes .../dolphin/external/L1_Mods_128x64/frame_34.png | Bin 0 -> 4253 bytes .../dolphin/external/L1_Mods_128x64/frame_35.png | Bin 0 -> 4342 bytes .../dolphin/external/L1_Mods_128x64/frame_36.png | Bin 0 -> 4315 bytes .../dolphin/external/L1_Mods_128x64/frame_37.png | Bin 0 -> 4267 bytes .../dolphin/external/L1_Mods_128x64/frame_38.png | Bin 0 -> 4301 bytes .../dolphin/external/L1_Mods_128x64/frame_39.png | Bin 0 -> 4326 bytes .../dolphin/external/L1_Mods_128x64/frame_4.png | Bin 0 -> 4327 bytes .../dolphin/external/L1_Mods_128x64/frame_40.png | Bin 0 -> 4313 bytes .../dolphin/external/L1_Mods_128x64/frame_5.png | Bin 0 -> 4357 bytes .../dolphin/external/L1_Mods_128x64/frame_6.png | Bin 0 -> 4334 bytes .../dolphin/external/L1_Mods_128x64/frame_7.png | Bin 0 -> 4331 bytes .../dolphin/external/L1_Mods_128x64/frame_8.png | Bin 0 -> 4352 bytes .../dolphin/external/L1_Mods_128x64/frame_9.png | Bin 0 -> 4360 bytes assets/dolphin/external/L1_Mods_128x64/meta.txt | 14 ++++++++++++++ assets/dolphin/external/manifest.txt | 9 ++++++++- 43 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 assets/dolphin/external/L1_Mods_128x64/frame_0.png create mode 100644 assets/dolphin/external/L1_Mods_128x64/frame_1.png create mode 100644 assets/dolphin/external/L1_Mods_128x64/frame_10.png create mode 100644 assets/dolphin/external/L1_Mods_128x64/frame_11.png create mode 100644 assets/dolphin/external/L1_Mods_128x64/frame_12.png create mode 100644 assets/dolphin/external/L1_Mods_128x64/frame_13.png create mode 100644 assets/dolphin/external/L1_Mods_128x64/frame_14.png create mode 100644 assets/dolphin/external/L1_Mods_128x64/frame_15.png create mode 100644 assets/dolphin/external/L1_Mods_128x64/frame_16.png create mode 100644 assets/dolphin/external/L1_Mods_128x64/frame_17.png create mode 100644 assets/dolphin/external/L1_Mods_128x64/frame_18.png create mode 100644 assets/dolphin/external/L1_Mods_128x64/frame_19.png create mode 100644 assets/dolphin/external/L1_Mods_128x64/frame_2.png create mode 100644 assets/dolphin/external/L1_Mods_128x64/frame_20.png create mode 100644 assets/dolphin/external/L1_Mods_128x64/frame_21.png create mode 100644 assets/dolphin/external/L1_Mods_128x64/frame_22.png create mode 100644 assets/dolphin/external/L1_Mods_128x64/frame_23.png create mode 100644 assets/dolphin/external/L1_Mods_128x64/frame_24.png create mode 100644 assets/dolphin/external/L1_Mods_128x64/frame_25.png create mode 100644 assets/dolphin/external/L1_Mods_128x64/frame_26.png create mode 100644 assets/dolphin/external/L1_Mods_128x64/frame_27.png create mode 100644 assets/dolphin/external/L1_Mods_128x64/frame_28.png create mode 100644 assets/dolphin/external/L1_Mods_128x64/frame_29.png create mode 100644 assets/dolphin/external/L1_Mods_128x64/frame_3.png create mode 100644 assets/dolphin/external/L1_Mods_128x64/frame_30.png create mode 100644 assets/dolphin/external/L1_Mods_128x64/frame_31.png create mode 100644 assets/dolphin/external/L1_Mods_128x64/frame_32.png create mode 100644 assets/dolphin/external/L1_Mods_128x64/frame_33.png create mode 100644 assets/dolphin/external/L1_Mods_128x64/frame_34.png create mode 100644 assets/dolphin/external/L1_Mods_128x64/frame_35.png create mode 100644 assets/dolphin/external/L1_Mods_128x64/frame_36.png create mode 100644 assets/dolphin/external/L1_Mods_128x64/frame_37.png create mode 100644 assets/dolphin/external/L1_Mods_128x64/frame_38.png create mode 100644 assets/dolphin/external/L1_Mods_128x64/frame_39.png create mode 100644 assets/dolphin/external/L1_Mods_128x64/frame_4.png create mode 100644 assets/dolphin/external/L1_Mods_128x64/frame_40.png create mode 100644 assets/dolphin/external/L1_Mods_128x64/frame_5.png create mode 100644 assets/dolphin/external/L1_Mods_128x64/frame_6.png create mode 100644 assets/dolphin/external/L1_Mods_128x64/frame_7.png create mode 100644 assets/dolphin/external/L1_Mods_128x64/frame_8.png create mode 100644 assets/dolphin/external/L1_Mods_128x64/frame_9.png create mode 100644 assets/dolphin/external/L1_Mods_128x64/meta.txt diff --git a/assets/dolphin/external/L1_Mods_128x64/frame_0.png b/assets/dolphin/external/L1_Mods_128x64/frame_0.png new file mode 100644 index 0000000000000000000000000000000000000000..220908495ad987548598b3c9d253b5c0898a5e76 GIT binary patch literal 4344 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 literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_Mods_128x64/frame_10.png b/assets/dolphin/external/L1_Mods_128x64/frame_10.png new file mode 100644 index 0000000000000000000000000000000000000000..e90ad5e90d9d22f6dff7d583ff77c79483616922 GIT binary patch literal 4370 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 literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_Mods_128x64/frame_11.png b/assets/dolphin/external/L1_Mods_128x64/frame_11.png new file mode 100644 index 0000000000000000000000000000000000000000..031c0ad81a1ce3f4f77b62f74dc865fabc2103ce GIT binary patch literal 4342 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 Y^oL+X!q1V(-@guPi=*awrf0(c2Rp-`h5!Hn literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_Mods_128x64/frame_12.png b/assets/dolphin/external/L1_Mods_128x64/frame_12.png new file mode 100644 index 0000000000000000000000000000000000000000..856e068fd002a75f04ef4fd9a20649584763ee01 GIT binary patch literal 4327 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 GasLPY-HN^d literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_Mods_128x64/frame_13.png b/assets/dolphin/external/L1_Mods_128x64/frame_13.png new file mode 100644 index 0000000000000000000000000000000000000000..a0366b2cfb4478ee1e6a3261351ad2608a960caa GIT binary patch literal 4363 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 literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_Mods_128x64/frame_14.png b/assets/dolphin/external/L1_Mods_128x64/frame_14.png new file mode 100644 index 0000000000000000000000000000000000000000..24fd557abd9732bc5c44221f0d2de36aac1276c5 GIT binary patch literal 4306 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 literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_Mods_128x64/frame_15.png b/assets/dolphin/external/L1_Mods_128x64/frame_15.png new file mode 100644 index 0000000000000000000000000000000000000000..3bf1d3ed2b4aab9c4801cdb145d64886751a96ea GIT binary patch literal 4325 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 literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_Mods_128x64/frame_16.png b/assets/dolphin/external/L1_Mods_128x64/frame_16.png new file mode 100644 index 0000000000000000000000000000000000000000..f0b44898fb76099f26cfd61c57c2c252e843faba GIT binary patch literal 4346 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 literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_Mods_128x64/frame_17.png b/assets/dolphin/external/L1_Mods_128x64/frame_17.png new file mode 100644 index 0000000000000000000000000000000000000000..c98c70c91261ccdfda7bb489908991c192199121 GIT binary patch literal 4338 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 TR|WGY1^}2Eoi;4jzZmsD!#|dj literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_Mods_128x64/frame_18.png b/assets/dolphin/external/L1_Mods_128x64/frame_18.png new file mode 100644 index 0000000000000000000000000000000000000000..4f7b7ae82eb0a11725af52a2ece0ab0f85703825 GIT binary patch literal 4346 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 literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_Mods_128x64/frame_19.png b/assets/dolphin/external/L1_Mods_128x64/frame_19.png new file mode 100644 index 0000000000000000000000000000000000000000..b3ad6700cec06ed37af344e2373eb2bbd808edd4 GIT binary patch literal 4339 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 literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_Mods_128x64/frame_20.png b/assets/dolphin/external/L1_Mods_128x64/frame_20.png new file mode 100644 index 0000000000000000000000000000000000000000..ea2eae4d7cb6d87b5f75327cc668f38c0ed0d383 GIT binary patch literal 4314 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 DJeHGU literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_Mods_128x64/frame_21.png b/assets/dolphin/external/L1_Mods_128x64/frame_21.png new file mode 100644 index 0000000000000000000000000000000000000000..900cc7d12099f0141c1e0c27cd29025fb1645f3b GIT binary patch literal 4357 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 JY2tnHe*iH9mO=mk literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_Mods_128x64/frame_23.png b/assets/dolphin/external/L1_Mods_128x64/frame_23.png new file mode 100644 index 0000000000000000000000000000000000000000..4f82f63bc9e11628f6e221ffab62fd6a0059d2f0 GIT binary patch literal 4332 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 Un~;#LUq1j-!_x+pdhVD05Bny3KmY&$ literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_Mods_128x64/frame_25.png b/assets/dolphin/external/L1_Mods_128x64/frame_25.png new file mode 100644 index 0000000000000000000000000000000000000000..768030b3c5614c681e3e2af785c662695bc9296f GIT binary patch literal 4149 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 literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_Mods_128x64/frame_26.png b/assets/dolphin/external/L1_Mods_128x64/frame_26.png new file mode 100644 index 0000000000000000000000000000000000000000..12f22abdb996b1a1e301e114c3d1f60c641e2ee5 GIT binary patch literal 4260 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 literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_Mods_128x64/frame_3.png b/assets/dolphin/external/L1_Mods_128x64/frame_3.png new file mode 100644 index 0000000000000000000000000000000000000000..1b0e77426c54323d46644d443ce364301dca1962 GIT binary patch literal 4341 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 bLx2O|;^IXc4Q(KPe>p5ntxd{|+%EnP;{}-{ literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_Mods_128x64/frame_30.png b/assets/dolphin/external/L1_Mods_128x64/frame_30.png new file mode 100644 index 0000000000000000000000000000000000000000..13caae7ca2e39adf8dd0e33d7a94b1fc326b9090 GIT binary patch literal 4390 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 T<^(JL{>NCEA2Ta9@i_NCw3V}x literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_Mods_128x64/frame_32.png b/assets/dolphin/external/L1_Mods_128x64/frame_32.png new file mode 100644 index 0000000000000000000000000000000000000000..acf000827ffb7177e5b7dbdc7935afc06a35d147 GIT binary patch literal 4402 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 literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_Mods_128x64/frame_33.png b/assets/dolphin/external/L1_Mods_128x64/frame_33.png new file mode 100644 index 0000000000000000000000000000000000000000..b6c6fbb19168b4e5b01f36c2a17bbe00336a4a7d GIT binary patch literal 4340 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 b7eWHHnZlOuxnixqe;n2pcIM@#p6C7teSDMt literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_Mods_128x64/frame_34.png b/assets/dolphin/external/L1_Mods_128x64/frame_34.png new file mode 100644 index 0000000000000000000000000000000000000000..7d2dcda5d98ac939ae8263cb7db583cef6574fb7 GIT binary patch literal 4253 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 z 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
`cx7 zHpiOjL42rGG71I@4h~igR#PQWykQ6(9UT}P2}2^GEDdM?ok+#dpu_-$-wdX>04xPh zrs7FN@GnM;2Pu%M2Vpt;R}=)Ywe>&5#DKqZ#mX9th9Sccs&E*A@GG=GwF9Vjxc_(K zU$p}q=wuws4i`WQq+nTF;-&B#%u3&XH}p%9 z!i^CKGgEC-Q-rxW0%5KOH_|dO*ZhsOBnD71L@e&NZ9L2N-&nQ(iba`Fa2P6y;y@z# z|1Jbu9}<-m;6oyVk!q?M;KSA!ES~sFa^%-8{S~w+j)D)ud74v51n{3}M&bX(1x*cY zO-(Hf63Pk{2UW-5)S=p1+E}QLmzSrOr<$e)0-*-^8}IqQrVqw)3ihj3{-bt&i&zEu zYxqwyuqOYs6OPF0AquNmB%`IcSdGSSYvo|}^XE_25Cj4V3JMPC+wTAXdx@o~kppdD zJ`F^94dypnS=wO^<-g78<-ry%Sj{S4_sg7K96YBN1MTP#PH(-_g4B44Z9KFq~^=mqGNP{EgxF<0W`#-`{uwB z@N;dc_cQ%VfS^&IQ0=#_a+KW#Gj78lO4L@>mq`MRK>QqI&geA%ZDLi}SuRG>?ct{r z5)9Q7E^^KMqw3KVpq!WIxRhqCDu?hZiF`^R6VC>q4{jZQhKH7q@@8F>21?oU&kVv# zxIm+%+uq}btHHt}0tGvTY;c4AOTB!aeUXQEEn0le0Tt_4ng>*h>?`QH0^mv~J#-oc z%tv;DwR0eE33u~&UQH|l;hOc%Y(7D0o`t%+3hE#w9 Z;93Nk^X$3K`tbw+OS6-vrN-`;{|~Y=oI3yj literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_Mods_128x64/frame_36.png b/assets/dolphin/external/L1_Mods_128x64/frame_36.png new file mode 100644 index 0000000000000000000000000000000000000000..b018a94c19c4cdd239a3043b1ffe49e93cb0f471 GIT binary patch literal 4315 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 Bcas1B literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_Mods_128x64/frame_38.png b/assets/dolphin/external/L1_Mods_128x64/frame_38.png new file mode 100644 index 0000000000000000000000000000000000000000..ed38122f52a73a977c2ed044d22e8c113ae6c3f9 GIT binary patch literal 4301 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 literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_Mods_128x64/frame_39.png b/assets/dolphin/external/L1_Mods_128x64/frame_39.png new file mode 100644 index 0000000000000000000000000000000000000000..38610bb4b4a9aa68f97442f901f3318941a2305a GIT binary patch literal 4326 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 Lomr`g$L0S4vAUb6 literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_Mods_128x64/frame_4.png b/assets/dolphin/external/L1_Mods_128x64/frame_4.png new file mode 100644 index 0000000000000000000000000000000000000000..45e47de12adccf1b4deaa8bf4be3207357078bf2 GIT binary patch literal 4327 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 literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_Mods_128x64/frame_5.png b/assets/dolphin/external/L1_Mods_128x64/frame_5.png new file mode 100644 index 0000000000000000000000000000000000000000..7c293b4859a18b634ef661c0b206a41cc03ab31d GIT binary patch literal 4357 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 T11a*?C&S+Qh*g=zsSE!Hfg_nh literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_Mods_128x64/frame_7.png b/assets/dolphin/external/L1_Mods_128x64/frame_7.png new file mode 100644 index 0000000000000000000000000000000000000000..5c840d6f642fdb36967d4646a3d3695ec2a12da5 GIT binary patch literal 4331 zcmbVOc{r49+rJ0ZShEYoG@_C*X2v=-_GQSHT@qsq2D3EAGLj{cl2D3XUasrBe!p|O&hxmhOSaZ#f_yT3000P@V~y=N z-)PRG59a2)C&0;80KjKNG%~U^H!^}yX%rt~AOQfv`f}_;U9PW5>JF_Db~_nX!n4K*uib82XcZvJJMOxh9%R0z1qD5&qKh z(s##e0GQ`yghkRH=xu`TQvfc2+O2v(v~lEDY?ZB&GXO~ihM}U9{Gb#&ph?xCNgQa} z13a*Q?9B(<0DwS7gdr5TCkV_eo9pfXKC!M3NCBS;_Oif05(r=(GCRPf{~YjijImSU zs(%UOvGt*=tvlxWJSHZ3xT%ex2f=j# zEH|Lf3Gf?QcSrT}=AGnw7rvgz(B9OS%gwiKI<{{wA3y)BqE~*6+E}9QFVqNSI5BdS zv%1|MJ`bG9)Mu8~7shU#!I4AC?5<9>@E9Vc@6w|=_oNlRdT3sv{X|0C2r-Jz4(<2x!YF8UujpuX|42y&+}!1`Ghkc`?cl40Z{;5NFp5Wxv>0 zT`$Fj+i9#XaG?H}u;I?}=)HXTd(sW09qQ4ac%D4qmdO*z91$*mfzTG592PwlGfkD+ z>A5HO<(}$zULJ!cuo!MXIzhzi>L+MBSc-X-2a3b0UyCk+OeKh)fWB^`6>+f){Zrks z3X3qBxK7e3{hulQO%UYe&CAthh#N7FPH=zV3f6yiS6=4wo4lY-y83SS==6esPMLB! zO!U)&;?Cn=gXzGz%UAKyod_YHE4{c=S1y`(UUHF9YLvuJxM(;fYvA{}XoX0~*5~5p zzz4Vzqs3;xr!NS<3_PEbV&-D$Vsuw=Mxk7^3hc}qdjawCAoTiOn{vf)(e;KfFZ9C{ z7gM&<_x*DFa}`e?6cY2ejy)jMd~MvCZ@*#+(wyHs0Nam+713ehcO^Tz7@~U!#MCx17 zG<2F%T4dVfJqO1iN4;*wJ${E0$K55TozC4cb$o0)#^U`TS<=ng=lGnZS=MeBYL{%= zal)gp)?@4ZPrTrx_&C$Ncx<5qzT%B^(tgP|NWy}PVGqrTds-#nrj?i5qxBifkR2Pc zA@H-7_C`VXeAqszJ~>~`w1I@)JRF|a>CLui-`_6Seip_J(}O+i@h|l$jV#sb3-7D# z^O~j2O3qIA%>`3)^m9~lR?sB$S}<>wX4Q$RxT^l(j&Hi}@zabc-j%m2LQ{jCW-FxS z!WF4+b3JpN@>*)@V9gZiK!i4azBL8^HQ*+(vUC2z$X$^ZzpB&*zrs%Q!h@Ac-!J-( zX@p7X`miZ`z1ZrX2?GJga9%`Ye^Eox>+#ydZ8rikeX2Uk-_u)Ln^XqapV-YaS9Q9x z19EY-z197jNpZJLvR_*yY9=`1ptUt`~N(sg1utvG#ToA;cWR+%RCI0xFw zI%f%Fp|e_^-Yv~4Z3xLC4T+D6Cm-%Qykbx8L(FW>@Xch-?3~qE#jPr?l2$wRweA(& z*K?G9G=fGCZVd^fR!k|AYJD!yDyz7^h* zHX-x9s_V)7mBOs&Sp%iLqz{9wnAhh@Hmp>$WSiYzVde~$=hs|D&$N!^6lWE`TuPaQ zpAI?gdU~Tq&gOBpQf`kLsj0ugZMdf8tS4F*L-^S~#jX2Bm+Myet&6pHYN11zT7}4- zNM_`&&D@QnKU=nvf7FdI79|&~cX)zcfF?jSF)}faxdXW6d6+!6cWLZM+VOlRTtHXC zP=Hs^Ri1X8QZKbAyN*W<_^M=fO1uTG-mVfr4jJU#8ky zazl@~YPhCEXvf_Y&3L3Y5eqT2h3~PMC?;F&6I;BH6=!2oTaZ+6;$cOl#j$H&a-7mu zwFi%$Gy3TlZ(|z#2E}V*cZ88DeeIdTl9iKj&u4Z=@0nhh&W-FF*@bq3#PsqbAJr-j zRoM6(IdWZaobiS9R`&YEw`=1H>p@HX5`l4+orUFgWwt_+mhihr!rb<_g;io}uqQvD zhJ3catt;@KmyD=Md0<=l;PI!*FxQ1)Xr0M(NrgmvsZdpfyIqxc7;(PqT#H}Jqx3D? zQ5QSMMSsS?;#IrYuG_bbj+}Q^Ph;I^ZXuT~pL~Xwz@>RR&iQ9_yE#QEOQm_RsqDLD zKOUG?ei?PP_UMjSVUYUzK3i10ot?%`7fqAdYp!+oEo%G?y5Ggm3zo_*Bu&VAkSspa z^TYjDuC|r8y=~*xz-i37wLWF{FXn3&u7>ACxwklNm6lZ0_A2%D)D4E7T@s4jCvo)i z+>M#)Cr84Z18Pjpt&qFEpC3$?@sh2o`w}@=X%=epIyd?3?X$a9P3ETk>6E~M`*n4X zcu97eNpe^;bs{U8(w8-x8Ty2JQd#Jw!NiM$Lw6$1j@f?AZ+|uLs%?lFRj~q|#xjm{ zq3eR#utHKECckFx>y^TseW&-C)mHq9oI{M^gBvxHopbYY4q+D7$Io9MEA}qV&9T-z_UV)|!_-PbRz1S5udvYhL||6~{|4Wf-$a>edMM zz8htlu+}o`e5{PVY&CtfbMAfAPyhPvppQR3s=D7|)Xcq^k*y#r4eqT}tXlaIvN|gj z%JZWEPA(JA1QuREY|Th(&-mtI;doFXlr!ic!$g8r+NPM#E!Z$25X5n zSHilFZd7QLuWn~x7Vd18w)dZ#tDLJEYOIlG9@^C3wq5G{iEk`^XS1d|8L_O2i9p9e!bpMSAaoc8`j=ibXZ%|XheG~>&;u~gznyZivV|B? zXatCsDiVfQQ&WRz>8K*SG&OKqI$r835TqIs30KpEBTz6T60L^d+>n1RD90ep+XroD zZ2FHm&I|+fqtmHqI6O2oR5cW(N}>6}5jr|La5W?xiG*JZDRM6#kGo>HF`7egiq)plxWJ0|XftXiUKelL%zGxiJRH zSyA;SdZV?q)o~~kLKCKr)KG`16Hq=d9W|6D45@+CL7}v@5O{+2A3gtxucm=CHPkjR z&_JpgArL0U+Q!BRQ&R-O6s2aMWn`-Phigs_qT|ST!XMj2j_rTAI{%f6Hlh)5bPCOZ zLJ9m+2)2F{Iwi=DLWLkvsv3|(RyaJ7{9AJP_b&Y%v@wB33?X=%(kLXzUui}Y|APyf z8rqtgS~w((6Dk3wjw7hUw6(PHFdZKsZ!K?>rUn9mg8swz{{N;A&T$IIiR4!h0TZ46U%!@$zQiCjPhF#@-AQ$PugF&BMT6x#xV;Vu&_h*1J;}{hRTxq?JURmQ;4tfAl0r9mopfg)D=t zLA+kY=0^U8>FQvGhfs5-5APnx8`Zn%UZtvedo1H}LrS>2+uzp$4K)AX4wlPtE^XbZ{xV{dHuw$po%R{dcl#{-mG_hBy zyVQ3jVA2-^94RJH*ZXe?+X22HB@K1)sr;jxBCqotxlGR{&{N0bt=yv%E#1VEtb?V> zG{@9u-kV(;Em<30VRyPabyqEaMU{Vj1X2lQt-0si`q0vTe2AyF@Of&ilAsjX4c;f8 z^=!~8N%JIqNH=$Y*jCmtsLGOoAbV3P-d7E7+5q6G3`TEVSyaF(FTS4W*1R7|hblSVm}s@+4cfWJxHgA(fD9 zl|r^C5{hI=2npHW>3N=>_xg_}TtJHo)s4=V!!puZ$_NCxOH*jEAq5_o{DgO&$)jh+CW&QVx3 z-o|G@KF0{6&fi$Z!*;7gTJo~%1c2Z~T(YQSyWyv!jnAHgw5PG+{UBxaJ&x^p)G z$gOri-O@Eoo}3u_G~qKDIQQd4!B)UL6=I;YFuYZJnU^0Bv+bH2I6Xez$46`CDGzE0 zC^-QPxBz~`s~#GIg8A-3ufta3S-KlW`}1ztx9BVE6WXkW^9sF~=!M4UH>PUR# zGIw=rFl-(;bImBLw6QRH^9-I6T!u}YY~wdUP*x_Dx0++mZ6+S-9@V^mQce2O0`AXh zdx%Xl>oGRPOsO?}RATBJazQa;Q!q6|7~ov6YqYK0`KT?hG4dM^Z@dVuCm1k_T!dbh z+m!-{`D!9rD*%w%u$p9aga_!zEcy%pwO{t0yqzXv{SpKKX8BQv%Z+#Lek#Rj6v=t2 zP}?ZOi{D{pwEIA#zL?35G4?*88+$X1Wt|#P{rr#0`Q-A&uYD4$c#6;!o*a=l88uCl z+2Of2@7dnk3xfQ{Eg(s}5-LvIE3qHa36jZ5{dautk@K|K2dl6u>&LM2RMwJFpKbw3$r z!BPFDv|lMt<U1)>_2U@m z0h z>+V+M{9t>ScU_=0eBGsf$nY=G=p+2Xxbval@cTLNz~vs#?s?~w9YzE?%BQk`*!mk)~gy6EPD5pOT;Y;=ZE&6O9bEUx!YT!;QXXSyQ~u%f=#mT zayVUBe|q!$8bSEMg; z0`lfSB2s5t_L+vtw3o8NaNuOkVC%Xx}HH z(07!1G@Q;1Y7Y*i)lL_E(tKXhRPubv)mPs41|xS-OXk+` zVW)ymxt&_C+i&+UM>VexPHq`&Ix$k$cGeSRfF`bWPVpJMG~m4+cKu@gt$N5Xx?VZ5 zFET4~=SJT8(Y3bCq@N9+Sl{<7*a~>^JmneZsf&_}ddL^Rr^uhhe{-jnK!U)N9kAU7 z(k8nFh20eCsnkZ9@A9hzjUivPYu(bXK)KTH@@w#RRqOqI;)xf!K*M5@)LX08Q|*F(gg`|?pkB<)eSJH!hIsSpz zK*CIRHM{ywbyVoZpXLjh5=_Yhg0p{)HT5^4|NPA0oeaFDku!G9DT7HU8y_loEHg3H z*_Icg@22IJ9IhLCRU-3&;dnII#2&WSZoHUcqagV`COg*7yuKizz~O#X^)dY`6S*$w z%eo(qMwqVoU9htVdZ{63hdshdk-hR*dC|tjtnVYI>;0McP`$LAw46e$Fe#(r$UAt| z!74kSBS%t&$5<2OSMsSBU#*NOuQC<~r2}KFy9+C@W%eR_tYNp0gr3-YBD5M)hjD+a zG3>MTbya!S`90xv$>sLd^rER%kkL*v{o z`&n*LJu&KPd%8D#iA5e5_;{@9)$BB8x@elhS#fK)cdX9excBvidErv|g@keW)8u0x znK#1xml8WFI$m|~Y2meIPqbHZ2EX6XE?f@FJ?GKpvRQh!s{Xy|Kwra$kh6;-(F)Q> zKhCAi)IK^A>KagI9X`MC$t3#()2Q$H7b7w6^L z8lvB34Y5|NH?jge96aiLO}p|8WtiWrt4^=9E`FR$@|drs{eY}^4XRWfFGZK3EhA~0 zpD_1MP^XD2ZL_ZWWy~Ko(?`4K-ke+WZ|r5f`}t1Y<2tKu?&XYp6-D*KzG{`4rJupe zvoaw<->io`!jeMoZPX8Je%-ky`t@AJ)_r0ldE}>Oqi5H1H_|$vsrFaCkwrBYooMtFk-HHu4cHoPBH7?hS8S&V)q)Mx%jo+ios;~)&^`P zW^tXZry|ZhRmw8WaZE?54>JT$Cj!RaR4*dfij4OqVu^Tf)|pnKJ^=8AlALf%oQ*Y# zKqaf=f61tal4)Et0O%Wr((r@;A`|RI^d(Wykhv!f5HQIb4RO`7f!okbiGCzZ7@g=C zX6r-<3n1uuLktbT`k^SU0y2?_2ZxdaDGXF78uFK36qo+RhC#r8i7*4ukiVV6+1P_k zsdOS(M;!?zz~OMPj-EQgOIr)Cqvxfm21de>NElojhR}c_ktjHVdxQVEAY6lVZyyxa z%;Fz&+!-3;$7Iq_Fjz=PhII+gAVL+I)0!Qe<35((u>Kp89w6CVntFqD65Fe5Su zbP|n8qEf)WG~&IeK}+IiAkZCqH|0c?LYp_r}4TeyM!^q@cq5UPzU}A~? z>&AbSW;n5EL>QLHpa#(i+%54@{tf1)@4p-RCCK##Wk=^8ASn1iGb$m7Or$Wa%+L_- zin=$+8>OqOiPz9TXhSuTTAENzqJ|Gt53ZpNMQS1SG&FQ|5Co#`Z$1AB57$CknCKd7 zYa!vL2!y$ru9+Fa!UBP?(1083m|AH6##&JrOgx1^{B4`WwfzrP>%U@ArgS2nNu@hc zse!)>!QPL`q%!=dG%!*_T?>5B22UVSesK={+NHmPHY3tW!9;HhI+YCmE6pgdkst%X2nK>opd|9{g5<2nWVRV)9ec7C(C z1^J8ow;8yTf7^*j;r0-n+bl7{AzIu<+ih>{WWK$<%_XGxK z(1Dq(1j=*p?wH53o5L(@Dm$Tz7UPtD3+vqbR?1+5(a%Yn`?(Tm3ew}wOnlT^puyid^7Y-Rm01R`(X zX#6xoVXd>3mPod+JgC7yB2tP^!+e0Ru@!C{o-H6Ep2N5AXaw5B&|&){kJ!-V)&L%G zk4fW! zz}++7(I~n93@I)nSw+}H)dhonN#@xBBFJbS$DeBLYtJ&19O|i-;q%-t`S2E>pYK~< z;5_%=7nc z;UBzF3OifkoB(eVKTV+aU`?m>wuWCy@4f3R@xsHxtx7eovl}z_R(dlbcAyQs;WxST z9inCq9S+H9#78|L&M`gvVkovNwvq2T!JtDx70}>hB+D6eh?x*o z+fgc!@97>2^#F?1jqbr~4;OB>cnx>Hf!|6x@FHaH>4|&d>UZ5e!q6HOUmozNg^*S} zp4xpSPL^)-zwi6d5_SA}>|kH^^2LrJnqy(d%Ws3t@hbw)RVqn7CF?Lb>g$>}8+HI_ mDw8E^Om?~$DK}!r<^cqtf;vsOIV+M`##Fn*hVGB7#hsd7|TeaQc9vE$&#(45T!_# zDauY131v^RCuILk=bX;(`^UMye|+ETeV_Mv?&ouV?&Z1f>v}HP+ggeU$qE4gAYz3# zBk=wayw3;%=8dC}I2!;EGNqcD+FO~LLYXXvH`Sj403khT4#C8PB`JgcCCYYZlkVB# zB>zYNrkD?@7jw-LyAF`fi-{Y(j*1p?mXO?$ig&6#+bM)kiyq2QDf2PD9DO9l>6rGk zd|&R<)1fm{Q>!0VE5=gC)>=7j+aq>}D_{GPi=*8ngJ=7fZ^iwxI5j~Dig0~~w&aVb6aa#*663cUc>oc~CTh(9RGSYl-Mg~|47CPlAUmoD z^4ytGWq1(4ZAxrOI@E!YC`)`R8~lBCU`S!;m4C4bNqWDSx8MjYH7P}LCw>fU-6{+K zvdir+H?$4o$G(gVe(@gjpZW1RYr}7r0W(mT>))uj!p9GY*|yE}xQ~u@fthunqQGju zyrV#mGjO7R*$vYxm~lktZRm0gM|;g^Z+fPEz22VPLff`YDS2jAB8OuPHZg;Yftc_` z-tIh_guI;m}Pn-O?;nN_#7*S$J5MUHO55FS2 z?HVBFqk-lu0YF0aa-5MW2xv*l83urgue*-jzaee?1_A(P8IcEyjJFEEl*s3broP-$ z!IkDCZ80+v-pADwGubi{v0Er}*G*#?M=q|L|7j6eHe<)_L9vpTC~c9k0r6vz6HMtX z9=p z`Vr5va@!Am4P*nSFJC1`w4+45uY4pOyK>Ro;}TI;xlW2aO2j(HVaa=lT0xR>+;q|m zWFKEl#Lh{`@$+J@{LjY6TN15_ruUU56-&g+AufXF&ZA!a0ZX`VSE3XuzFZsPi7SpL zTI4HFE9_NBS33TO=uYm#;83QF;4t8G?(_+C6b$05g{LyVegtvnG=Yxq0@ zc^r`sO}bVZzdhdW`f5YXNT~$wb@Y(6VAeiD&JFQ{Sc>5&|J^%iljMvChgqGbfxBOET~?bHaot<;htW!jVj&+dGCo9aVdI)NBxmkWM6x7R%BZBmEd*RA7Y zo?Yd>es+Z{^7ulOMaBhuwj;UpjZCb9)EhKqj%d=wat2SR`Q5VdJoE;2n8_GcLH$xF9+PGj@CCYka2k`RS27sp#IE+ML%TRr_0R_}%s{Z!dYrZf>qu>&x%XZj%D?4o;LLGeaQ={Q)pcw_ZGzV2 z;%_hKk|LafOKEy`zaXWcHYkPGFEJz$x4&cmf&;w=HMuq^Gga)9OTcen8+E_sL89%s~LCkk@Lw6NSnt>-#ZlFB%TzMB<(=Uy_`$G z6WWwGDmz`?@$}t7cFK#CzJiam_n(^eU!Q)sYNMVa*Wgy8KV$r3c8NH2vUxZyHzoJg zeEb;Vc+hdz?VhA6r*|P~^}V%62P&IRdEgB6DJ!kxV1qXXe0M_cT&%iR1?$(Z zQVj13PY&O@mcFXH(zG7;vwD#8U24u&zytIWGzzMWl#P4>_5;iFC-dLkiWP_zc(DZ` zY#?bOEGXhC&q`o$rN7H9lQDfhYPZ`Z8zE_uN90zJ&C1q$yLMc?&<5!j3uoM0wjOVO z28kZ9^p7r+r^+NtFQ1m!e*pbJ{oCOZ2lh$!l3G{(i0i6P<<+^kyerRjdwrj=f_(CQ z{R#f$$&|8)vIk|6As2s|&n1bockUCMx-?STU8{d-IDl`=|28IdKC#C_F}qq~lVl;JD! zN8V%ly*IusD{eb06;>HvWM5YFq`NG{b#4GwZT><^F~&hUSRLg?DEA7X&UTz`I??p_ z=DPh5k>K>*m(%zCD&btm-Mgl$XI(TB^KLXW(F=bZc}|ujC3-o{_$GB8bv|=II?+9! znSa0VXOTtOmmwEh_s*~d4y~u>^TE=_sR{f<&ICPw$+h~?!Af7_&bJq4MGEBRVn^lN zX$L>EGednBuC|o4G`4`TB<$4D=4bi6-!nC{7emv|xHUPi7d$Mj`l#H~RsAXW)V%1q zJ(9YgXKqYZJXH;G@vAgHy+H4nKKm(7)>E#$`b+qyGRt7Q*XePm?w;DZXg)LH%VzlZ zJ*uvLEJ!10#_q-An4>8XjGmOK+rdwnM-GU-G9G>TNB_OBQ^WROGh1u=YFhe}&y+4e zCh#294qSC$K0KS2p`Tef^Yu!0+MeTkEUQX4!)H*#}l&#_oasU&tq|JvlYxAuqDr4rP4zM`i1(I z;mq|x{G+3c3CdE_l#5;=`-jbhZu`u;Gb_H_&VUa;Kd8Ii;Z)ANnUpJ~D}UNurc}Q0 zGiY&2I#_7hy3Z{%F67Z#RnPjjtt;EVohjKUrf_KkKRvh}Z8fgjXG8@rrf=NXe0OoM z!?M}3Q_(^(HZDzGUQR@|^1D}S!eH*>+?{QW;m(_!iGYV={p&-khuVm1-Lu=4W7?_< zIeGJ}nTL6gaCM4x@)fNd{keN<1+BfOXUb;E`|B#@lU3G?HtgrySIBj_Z|#;0#==zRJuNF=0!COO7+r*xnOOOHcV5>2`WC6 zMR5qVbtH%Sk#)Rah6Yf*5FC$yMq!hnAvAw_04_uy_9rinH~%F@z@UFZ*nax3znmi4 z*h5VjEDBUh9StWVkw~bPjylRy6HC(4@zhX*qLFAc0;!2WVc=*q4vFHu(7zuT&mhao z8%Hp+_}d(Br4Kv7W;1aJL~wAhdN4+v!SX?%baZqONHhYChVu~c01ll^3W3uD6n`_A zQ3A*;Dw9oR(4oH=NuG>AwmyvK>|asPm^L>55Yq$x&J{0fh!7GJfl@~zXtZCU{fQ1> z6Da@h#=oKi963x1fe0C$nyqg$KqWe=p=tL204&Mp|h>b z^kKXmbuX$HPFq`pgu$RR;TmYH23&)J@rLUlF`95R7OjK9XltR!6z$(U|E7<`qAg6c zjd^gSDGFt7rfp`1vamp*EHFr8EmI54-?~=x05*wEru?=|<=IAJEOd-9W;}}jqKh+S zQAlhC%aOtG|6K_7Cm3u-zzGHuipHp8p(-{cGL`;Ivj5j9{S~wsg+&dbcv-L*H0Ymc z#!>&p1x>8BrluAN4d;bQfoqT`8gOkbZ8BWP+uKXa3!{lep)jz&^}YVr^dWdoA%4}$ zf7H%z5w9SBP5)^I-r}EjqR@Fg#NsuJR%#54*J#4_){f?zo147p#>U3_`g-z1{^J0^ zS8Zix>=@EB=^9C&L_%WvH#YD5Q9$E+DV96M?sylfYm>VmOXh03`kndEzhLMtR@m>h zIUHFv=eXZ~rK51Icr#pG1&;yRe(Hks%FG@-SrW)7ALAP8)3iRsfl9W6qZ`8VF20wI zep&r)V`~ILMc5gbF3o&1R;C1=jQ|h!TGqH=iwHSB0MnCb6m}^RvLyBr8==^7=fx`H zN1Lv2MEEhV%JHMZ&3?vs30LvO%GUrsiUEARaTMhM3^aB(CkqW+QYgF9@^p<4pw+OV zqH1Fe$k$rkAZDr-z?FJU!2VpM70CF&>!%79rq4K)W!$V8CRH@Y$#S?e)_82d1NqdiHUn>rsfdkiu_Z0c4`x8A#Uh zF+ovbrBFFpAe;zk<(Em&#m1vDh;k**w?xwRTV!LQu!uN6=&QoD{@!ow2}zaqJ9)Yu zQ;cqnyZ3{CY+H?|K4?9_KF56nLYo}`yi87LKm#t_V(C+}%8N~gaA|ru!S9Q)Pb*fs ztJJ#pedmvqUtB0W+&2(Avs9sdDW?!}OSbjWAWWdufZOK1TElminxSopMX^27;tN6C zCuWMnLF!Ti9uLX42u7)#fRy2GBT+A9LiBw91@FU;ZH++;%Opmfc;pU zKj*sT!IwS2TCZmHA)wI~j@cDqUXK&TRLV%$6n|0h)opzgCju0)dM?-Wmzm;0{97*1 z^Otyj5;X5#i|ovB;`+iYD-=l>EHu? xWaGgK;|bf9egInHev@(@tbxtEvt<^}1)gLI8ik1mLV0x!See_J6`FXQ{Xc}7sS*GH literal 0 HcmV?d00001 diff --git a/assets/dolphin/external/L1_Mods_128x64/meta.txt b/assets/dolphin/external/L1_Mods_128x64/meta.txt new file mode 100644 index 000000000..0225c7e55 --- /dev/null +++ b/assets/dolphin/external/L1_Mods_128x64/meta.txt @@ -0,0 +1,14 @@ +Filetype: Flipper Animation +Version: 1 + +Width: 128 +Height: 64 +Passive frames: 23 +Active frames: 18 +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 39 40 +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 6bf6957c3..a6c7ca694 100644 --- a/assets/dolphin/external/manifest.txt +++ b/assets/dolphin/external/manifest.txt @@ -85,12 +85,19 @@ 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: 5 + Name: L1_Painting_128x64 Min butthurt: 0 Max butthurt: 7 Min level: 1 Max level: 3 -Weight: 6 +Weight: 4 Name: L3_Hijack_radio_128x64 Min butthurt: 0 From d68ac50efdbc99039cc8a25963fc0ab5c7ee556e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=82=E3=81=8F?= Date: Fri, 4 Nov 2022 14:26:04 +0900 Subject: [PATCH 22/49] Storage: tree timestamps (#1971) * Storage: tree timestamps * Rpc: add storage timestamp * Storage: correct timestamp owner * Storage: update timestamp at sd mount Co-authored-by: SG --- applications/services/rpc/rpc_storage.c | 38 +++++++++++++++++++ applications/services/storage/storage.c | 1 + applications/services/storage/storage.h | 10 +++++ applications/services/storage/storage_cli.c | 22 +++++++++++ .../services/storage/storage_external_api.c | 10 +++++ applications/services/storage/storage_glue.c | 8 ++++ applications/services/storage/storage_glue.h | 3 ++ applications/services/storage/storage_i.h | 1 + .../services/storage/storage_message.h | 7 ++++ .../services/storage/storage_processing.c | 29 ++++++++++++++ .../services/storage/storages/storage_ext.c | 1 + assets/protobuf | 2 +- firmware/targets/f7/api_symbols.csv | 4 +- firmware/targets/f7/furi_hal/furi_hal_rtc.c | 6 +++ .../targets/furi_hal_include/furi_hal_rtc.h | 2 + 15 files changed, 142 insertions(+), 2 deletions(-) diff --git a/applications/services/rpc/rpc_storage.c b/applications/services/rpc/rpc_storage.c index e28998e13..16e343fce 100644 --- a/applications/services/rpc/rpc_storage.c +++ b/applications/services/rpc/rpc_storage.c @@ -138,6 +138,41 @@ static void rpc_system_storage_info_process(const PB_Main* request, void* contex furi_record_close(RECORD_STORAGE); } +static void rpc_system_storage_timestamp_process(const PB_Main* request, void* context) { + furi_assert(request); + furi_assert(context); + furi_assert(request->which_content == PB_Main_storage_timestamp_request_tag); + + FURI_LOG_D(TAG, "Timestamp"); + + RpcStorageSystem* rpc_storage = context; + RpcSession* session = rpc_storage->session; + furi_assert(session); + + rpc_system_storage_reset_state(rpc_storage, session, true); + + PB_Main* response = malloc(sizeof(PB_Main)); + response->command_id = request->command_id; + + Storage* fs_api = furi_record_open(RECORD_STORAGE); + + const char* path = request->content.storage_timestamp_request.path; + uint32_t timestamp = 0; + FS_Error error = storage_common_timestamp(fs_api, path, ×tamp); + + response->command_status = rpc_system_storage_get_error(error); + response->which_content = PB_Main_empty_tag; + + if(error == FSE_OK) { + response->which_content = PB_Main_storage_timestamp_response_tag; + response->content.storage_timestamp_response.timestamp = timestamp; + } + + rpc_send_and_release(session, response); + free(response); + furi_record_close(RECORD_STORAGE); +} + static void rpc_system_storage_stat_process(const PB_Main* request, void* context) { furi_assert(request); furi_assert(context); @@ -672,6 +707,9 @@ void* rpc_system_storage_alloc(RpcSession* session) { rpc_handler.message_handler = rpc_system_storage_info_process; rpc_add_handler(session, PB_Main_storage_info_request_tag, &rpc_handler); + rpc_handler.message_handler = rpc_system_storage_timestamp_process; + rpc_add_handler(session, PB_Main_storage_timestamp_request_tag, &rpc_handler); + rpc_handler.message_handler = rpc_system_storage_stat_process; rpc_add_handler(session, PB_Main_storage_stat_request_tag, &rpc_handler); diff --git a/applications/services/storage/storage.c b/applications/services/storage/storage.c index 700408c9d..1816bf921 100644 --- a/applications/services/storage/storage.c +++ b/applications/services/storage/storage.c @@ -39,6 +39,7 @@ Storage* storage_app_alloc() { for(uint8_t i = 0; i < STORAGE_COUNT; i++) { storage_data_init(&app->storage[i]); + storage_data_timestamp(&app->storage[i]); } #ifndef FURI_RAM_EXEC diff --git a/applications/services/storage/storage.h b/applications/services/storage/storage.h index 968b69048..9c133e9be 100644 --- a/applications/services/storage/storage.h +++ b/applications/services/storage/storage.h @@ -177,6 +177,16 @@ bool storage_dir_rewind(File* file); /******************* Common Functions *******************/ +/** Retrieves unix timestamp of last access + * + * @param storage The storage instance + * @param path path to file/directory + * @param timestamp the timestamp pointer + * + * @return FS_Error operation result + */ +FS_Error storage_common_timestamp(Storage* storage, const char* path, uint32_t* timestamp); + /** Retrieves information about a file/directory * @param app pointer to the api * @param path path to file/directory diff --git a/applications/services/storage/storage_cli.c b/applications/services/storage/storage_cli.c index 880fb9700..0efcb5e2c 100644 --- a/applications/services/storage/storage_cli.c +++ b/applications/services/storage/storage_cli.c @@ -32,6 +32,7 @@ static void storage_cli_print_usage() { printf("\tmkdir\t - creates a new directory\r\n"); printf("\tmd5\t - md5 hash of the file\r\n"); printf("\tstat\t - info about file or dir\r\n"); + printf("\ttimestamp\t - last modification timestamp\r\n"); }; static void storage_cli_print_error(FS_Error error) { @@ -386,6 +387,22 @@ static void storage_cli_stat(Cli* cli, FuriString* path) { furi_record_close(RECORD_STORAGE); } +static void storage_cli_timestamp(Cli* cli, FuriString* path) { + UNUSED(cli); + Storage* api = furi_record_open(RECORD_STORAGE); + + uint32_t timestamp = 0; + FS_Error error = storage_common_timestamp(api, furi_string_get_cstr(path), ×tamp); + + if(error != FSE_OK) { + printf("Invalid arguments\r\n"); + } else { + printf("Timestamp %lu\r\n", timestamp); + } + + furi_record_close(RECORD_STORAGE); +} + static void storage_cli_copy(Cli* cli, FuriString* old_path, FuriString* args) { UNUSED(cli); Storage* api = furi_record_open(RECORD_STORAGE); @@ -578,6 +595,11 @@ void storage_cli(Cli* cli, FuriString* args, void* context) { break; } + if(furi_string_cmp_str(cmd, "timestamp") == 0) { + storage_cli_timestamp(cli, path); + break; + } + storage_cli_print_usage(); } while(false); diff --git a/applications/services/storage/storage_external_api.c b/applications/services/storage/storage_external_api.c index c0c730fb7..6854ef7f3 100644 --- a/applications/services/storage/storage_external_api.c +++ b/applications/services/storage/storage_external_api.c @@ -354,6 +354,16 @@ bool storage_dir_rewind(File* file) { /****************** COMMON ******************/ +FS_Error storage_common_timestamp(Storage* storage, const char* path, uint32_t* timestamp) { + S_API_PROLOGUE; + + SAData data = {.ctimestamp = {.path = path, .timestamp = timestamp}}; + + S_API_MESSAGE(StorageCommandCommonTimestamp); + S_API_EPILOGUE; + return S_RETURN_ERROR; +} + FS_Error storage_common_stat(Storage* storage, const char* path, FileInfo* fileinfo) { S_API_PROLOGUE; diff --git a/applications/services/storage/storage_glue.c b/applications/services/storage/storage_glue.c index c5682f67b..c6ff08bdc 100644 --- a/applications/services/storage/storage_glue.c +++ b/applications/services/storage/storage_glue.c @@ -82,6 +82,14 @@ const char* storage_data_status_text(StorageData* storage) { return result; } +void storage_data_timestamp(StorageData* storage) { + storage->timestamp = furi_hal_rtc_get_timestamp(); +} + +uint32_t storage_data_get_timestamp(StorageData* storage) { + return storage->timestamp; +} + /****************** storage glue ******************/ bool storage_has_file(const File* file, StorageData* storage_data) { diff --git a/applications/services/storage/storage_glue.h b/applications/services/storage/storage_glue.h index 53fa0de19..6fdc70099 100644 --- a/applications/services/storage/storage_glue.h +++ b/applications/services/storage/storage_glue.h @@ -42,6 +42,8 @@ bool storage_data_lock(StorageData* storage); bool storage_data_unlock(StorageData* storage); StorageStatus storage_data_status(StorageData* storage); const char* storage_data_status_text(StorageData* storage); +void storage_data_timestamp(StorageData* storage); +uint32_t storage_data_get_timestamp(StorageData* storage); LIST_DEF( StorageFileList, @@ -58,6 +60,7 @@ struct StorageData { FuriMutex* mutex; StorageStatus status; StorageFileList_t files; + uint32_t timestamp; }; bool storage_has_file(const File* file, StorageData* storage_data); diff --git a/applications/services/storage/storage_i.h b/applications/services/storage/storage_i.h index 5c836ccd2..406fc921e 100644 --- a/applications/services/storage/storage_i.h +++ b/applications/services/storage/storage_i.h @@ -1,5 +1,6 @@ #pragma once #include +#include #include #include "storage_glue.h" #include "storage_sd_api.h" diff --git a/applications/services/storage/storage_message.h b/applications/services/storage/storage_message.h index 78cd1e03c..987268017 100644 --- a/applications/services/storage/storage_message.h +++ b/applications/services/storage/storage_message.h @@ -42,6 +42,11 @@ typedef struct { uint16_t name_length; } SADataDRead; +typedef struct { + const char* path; + uint32_t* timestamp; +} SADataCTimestamp; + typedef struct { const char* path; FileInfo* fileinfo; @@ -78,6 +83,7 @@ typedef union { SADataDOpen dopen; SADataDRead dread; + SADataCTimestamp ctimestamp; SADataCStat cstat; SADataCFSInfo cfsinfo; @@ -112,6 +118,7 @@ typedef enum { StorageCommandDirClose, StorageCommandDirRead, StorageCommandDirRewind, + StorageCommandCommonTimestamp, StorageCommandCommonStat, StorageCommandCommonRemove, StorageCommandCommonMkDir, diff --git a/applications/services/storage/storage_processing.c b/applications/services/storage/storage_processing.c index 8643e974e..795a5d11c 100644 --- a/applications/services/storage/storage_processing.c +++ b/applications/services/storage/storage_processing.c @@ -114,6 +114,9 @@ bool storage_process_file_open( if(storage_path_already_open(real_path, storage->files)) { file->error_id = FSE_ALREADY_OPEN; } else { + if(access_mode & FSAM_WRITE) { + storage_data_timestamp(storage); + } storage_push_storage_file(file, real_path, type, storage); FS_CALL(storage, file.open(storage, file, remove_vfs(path), access_mode, open_mode)); } @@ -166,6 +169,7 @@ static uint16_t storage_process_file_write( if(storage == NULL) { file->error_id = FSE_INVALID_PARAMETER; } else { + storage_data_timestamp(storage); FS_CALL(storage, file.write(storage, file, buff, bytes_to_write)); } @@ -209,6 +213,7 @@ static bool storage_process_file_truncate(Storage* app, File* file) { if(storage == NULL) { file->error_id = FSE_INVALID_PARAMETER; } else { + storage_data_timestamp(storage); FS_CALL(storage, file.truncate(storage, file)); } @@ -222,6 +227,7 @@ static bool storage_process_file_sync(Storage* app, File* file) { if(storage == NULL) { file->error_id = FSE_INVALID_PARAMETER; } else { + storage_data_timestamp(storage); FS_CALL(storage, file.sync(storage, file)); } @@ -332,6 +338,21 @@ bool storage_process_dir_rewind(Storage* app, File* file) { /******************* Common FS Functions *******************/ +static FS_Error + storage_process_common_timestamp(Storage* app, const char* path, uint32_t* timestamp) { + FS_Error ret = FSE_OK; + StorageType type = storage_get_type_by_path(app, path); + + if(storage_type_is_not_valid(type)) { + ret = FSE_INVALID_NAME; + } else { + StorageData* storage = storage_get_storage_by_type(app, type); + *timestamp = storage_data_get_timestamp(storage); + } + + return ret; +} + static FS_Error storage_process_common_stat(Storage* app, const char* path, FileInfo* fileinfo) { FS_Error ret = FSE_OK; StorageType type = storage_get_type_by_path(app, path); @@ -366,6 +387,7 @@ static FS_Error storage_process_common_remove(Storage* app, const char* path) { break; } + storage_data_timestamp(storage); FS_CALL(storage, common.remove(storage, remove_vfs(path))); } while(false); @@ -382,6 +404,7 @@ static FS_Error storage_process_common_mkdir(Storage* app, const char* path) { ret = FSE_INVALID_NAME; } else { StorageData* storage = storage_get_storage_by_type(app, type); + storage_data_timestamp(storage); FS_CALL(storage, common.mkdir(storage, remove_vfs(path))); } @@ -417,6 +440,7 @@ static FS_Error storage_process_sd_format(Storage* app) { ret = FSE_NOT_READY; } else { ret = sd_format_card(&app->storage[ST_EXT]); + storage_data_timestamp(&app->storage[ST_EXT]); } return ret; @@ -429,6 +453,7 @@ static FS_Error storage_process_sd_unmount(Storage* app) { ret = FSE_NOT_READY; } else { sd_unmount_card(&app->storage[ST_EXT]); + storage_data_timestamp(&app->storage[ST_EXT]); } return ret; @@ -541,6 +566,10 @@ void storage_process_message_internal(Storage* app, StorageMessage* message) { message->return_data->bool_value = storage_process_dir_rewind(app, message->data->file.file); break; + case StorageCommandCommonTimestamp: + message->return_data->error_value = storage_process_common_timestamp( + app, message->data->ctimestamp.path, message->data->ctimestamp.timestamp); + break; case StorageCommandCommonStat: message->return_data->error_value = storage_process_common_stat( app, message->data->cstat.path, message->data->cstat.fileinfo); diff --git a/applications/services/storage/storages/storage_ext.c b/applications/services/storage/storages/storage_ext.c index 7341a6ec8..0c81a0006 100644 --- a/applications/services/storage/storages/storage_ext.c +++ b/applications/services/storage/storages/storage_ext.c @@ -90,6 +90,7 @@ static bool sd_mount_card(StorageData* storage, bool notify) { } } + storage_data_timestamp(storage); storage_data_unlock(storage); return result; diff --git a/assets/protobuf b/assets/protobuf index 6727eaf28..e5af96e08 160000 --- a/assets/protobuf +++ b/assets/protobuf @@ -1 +1 @@ -Subproject commit 6727eaf287db077dcd28719cd764f5804712223e +Subproject commit e5af96e08fea8351898f7b8c6d1e34ce5fd6cdef diff --git a/firmware/targets/f7/api_symbols.csv b/firmware/targets/f7/api_symbols.csv index 0d9a5b561..65e6f4857 100644 --- a/firmware/targets/f7/api_symbols.csv +++ b/firmware/targets/f7/api_symbols.csv @@ -1,5 +1,5 @@ entry,status,name,type,params -Version,+,7.2,, +Version,+,7.3,, Header,+,applications/services/bt/bt_service/bt.h,, Header,+,applications/services/cli/cli.h,, Header,+,applications/services/cli/cli_vcp.h,, @@ -1251,6 +1251,7 @@ Function,+,furi_hal_rtc_get_fault_data,uint32_t, Function,+,furi_hal_rtc_get_log_level,uint8_t, Function,+,furi_hal_rtc_get_pin_fails,uint32_t, Function,+,furi_hal_rtc_get_register,uint32_t,FuriHalRtcRegister +Function,+,furi_hal_rtc_get_timestamp,uint32_t, Function,-,furi_hal_rtc_init,void, Function,-,furi_hal_rtc_init_early,void, Function,+,furi_hal_rtc_is_flag_set,_Bool,FuriHalRtcFlag @@ -2235,6 +2236,7 @@ Function,+,storage_common_mkdir,FS_Error,"Storage*, const char*" Function,+,storage_common_remove,FS_Error,"Storage*, const char*" Function,+,storage_common_rename,FS_Error,"Storage*, const char*, const char*" Function,+,storage_common_stat,FS_Error,"Storage*, const char*, FileInfo*" +Function,+,storage_common_timestamp,FS_Error,"Storage*, const char*, uint32_t*" Function,+,storage_dir_close,_Bool,File* Function,+,storage_dir_open,_Bool,"File*, const char*" Function,+,storage_dir_read,_Bool,"File*, FileInfo*, char*, uint16_t" diff --git a/firmware/targets/f7/furi_hal/furi_hal_rtc.c b/firmware/targets/f7/furi_hal/furi_hal_rtc.c index 24dad38fa..14ece9466 100644 --- a/firmware/targets/f7/furi_hal/furi_hal_rtc.c +++ b/firmware/targets/f7/furi_hal/furi_hal_rtc.c @@ -318,6 +318,12 @@ uint32_t furi_hal_rtc_get_pin_fails() { return furi_hal_rtc_get_register(FuriHalRtcRegisterPinFails); } +uint32_t furi_hal_rtc_get_timestamp() { + FuriHalRtcDateTime datetime = {0}; + furi_hal_rtc_get_datetime(&datetime); + return furi_hal_rtc_datetime_to_timestamp(&datetime); +} + uint32_t furi_hal_rtc_datetime_to_timestamp(FuriHalRtcDateTime* datetime) { uint32_t timestamp = 0; uint8_t years = 0; diff --git a/firmware/targets/furi_hal_include/furi_hal_rtc.h b/firmware/targets/furi_hal_include/furi_hal_rtc.h index bdae3b931..361225fb2 100644 --- a/firmware/targets/furi_hal_include/furi_hal_rtc.h +++ b/firmware/targets/furi_hal_include/furi_hal_rtc.h @@ -93,6 +93,8 @@ void furi_hal_rtc_set_pin_fails(uint32_t value); uint32_t furi_hal_rtc_get_pin_fails(); +uint32_t furi_hal_rtc_get_timestamp(); + uint32_t furi_hal_rtc_datetime_to_timestamp(FuriHalRtcDateTime* datetime); #ifdef __cplusplus From 3bd74b7f0108690f451b8a1e83fc9f8d121aca61 Mon Sep 17 00:00:00 2001 From: Sergey Monchenko <14358100+msvsergey@users.noreply.github.com> Date: Fri, 4 Nov 2022 09:08:51 +0300 Subject: [PATCH 23/49] SubGhz: fix incorrect response in rpc mode. Code cleanup. (#1964) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Code cleanup * SubGhz: correct logic in RPC * SubGhz: do not blink on app rpc fail Co-authored-by: あく --- .../scenes/subghz_scene_receiver_info.c | 4 +--- .../main/subghz/scenes/subghz_scene_rpc.c | 3 +-- .../dap_link/gui/scenes/dap_scene_config.c | 4 ++-- applications/services/cli/cli_command_gpio.c | 4 ++-- .../desktop/views/desktop_view_locked.c | 2 +- applications/services/gui/view_dispatcher.c | 3 +-- .../services/notification/notification_app.c | 2 +- .../power_settings_app/views/battery_info.c | 21 ++++++++++++++----- 8 files changed, 25 insertions(+), 18 deletions(-) diff --git a/applications/main/subghz/scenes/subghz_scene_receiver_info.c b/applications/main/subghz/scenes/subghz_scene_receiver_info.c index bc6c8f112..c0f901578 100644 --- a/applications/main/subghz/scenes/subghz_scene_receiver_info.c +++ b/applications/main/subghz/scenes/subghz_scene_receiver_info.c @@ -155,9 +155,7 @@ bool subghz_scene_receiver_info_on_event(void* context, SceneManagerEvent event) } else if(event.event == SubGhzCustomEventSceneReceiverInfoSave) { //CC1101 Stop RX -> Save subghz->state_notifications = SubGhzNotificationStateIDLE; - if(subghz->txrx->hopper_state != SubGhzHopperStateOFF) { - subghz->txrx->hopper_state = SubGhzHopperStateOFF; - } + subghz->txrx->hopper_state = SubGhzHopperStateOFF; if(subghz->txrx->txrx_state == SubGhzTxRxStateRx) { subghz_rx_end(subghz); subghz_sleep(subghz); diff --git a/applications/main/subghz/scenes/subghz_scene_rpc.c b/applications/main/subghz/scenes/subghz_scene_rpc.c index 1f06f4f9e..79295aaa6 100644 --- a/applications/main/subghz/scenes/subghz_scene_rpc.c +++ b/applications/main/subghz/scenes/subghz_scene_rpc.c @@ -40,9 +40,8 @@ bool subghz_scene_rpc_on_event(void* context, SceneManagerEvent event) { bool result = false; if((subghz->txrx->txrx_state == SubGhzTxRxStateSleep) && (state == SubGhzRpcStateLoaded)) { - subghz_blink_start(subghz); result = subghz_tx_start(subghz, subghz->txrx->fff_data); - result = true; + if(result) subghz_blink_start(subghz); } rpc_system_app_confirm(subghz->rpc_ctx, RpcAppEventButtonPress, result); } else if(event.event == SubGhzCustomEventSceneRpcButtonRelease) { diff --git a/applications/plugins/dap_link/gui/scenes/dap_scene_config.c b/applications/plugins/dap_link/gui/scenes/dap_scene_config.c index 56b06411c..48d5fedcd 100644 --- a/applications/plugins/dap_link/gui/scenes/dap_scene_config.c +++ b/applications/plugins/dap_link/gui/scenes/dap_scene_config.c @@ -72,8 +72,8 @@ void dap_scene_config_on_enter(void* context) { variable_item_set_current_value_index(item, config->uart_swap); variable_item_set_current_value_text(item, uart_swap[config->uart_swap]); - item = variable_item_list_add(var_item_list, "Help and Pinout", 0, NULL, NULL); - item = variable_item_list_add(var_item_list, "About", 0, NULL, NULL); + variable_item_list_add(var_item_list, "Help and Pinout", 0, NULL, NULL); + variable_item_list_add(var_item_list, "About", 0, NULL, NULL); variable_item_list_set_selected_item( var_item_list, scene_manager_get_scene_state(app->scene_manager, DapSceneConfig)); diff --git a/applications/services/cli/cli_command_gpio.c b/applications/services/cli/cli_command_gpio.c index 54671eda4..d072ce00c 100644 --- a/applications/services/cli/cli_command_gpio.c +++ b/applications/services/cli/cli_command_gpio.c @@ -40,7 +40,7 @@ static bool pin_name_to_int(FuriString* pin_name, size_t* result) { bool debug = furi_hal_rtc_is_flag_set(FuriHalRtcFlagDebug); for(size_t i = 0; i < COUNT_OF(cli_command_gpio_pins); i++) { if(!furi_string_cmp(pin_name, cli_command_gpio_pins[i].name)) { - if(!cli_command_gpio_pins[i].debug || (cli_command_gpio_pins[i].debug && debug)) { + if(!cli_command_gpio_pins[i].debug || debug) { *result = i; found = true; break; @@ -55,7 +55,7 @@ static void gpio_print_pins(void) { printf("Wrong pin name. Available pins: "); bool debug = furi_hal_rtc_is_flag_set(FuriHalRtcFlagDebug); for(size_t i = 0; i < COUNT_OF(cli_command_gpio_pins); i++) { - if(!cli_command_gpio_pins[i].debug || (cli_command_gpio_pins[i].debug && debug)) { + if(!cli_command_gpio_pins[i].debug || debug) { printf("%s ", cli_command_gpio_pins[i].name); } } diff --git a/applications/services/desktop/views/desktop_view_locked.c b/applications/services/desktop/views/desktop_view_locked.c index d18ed6c98..0bf757036 100644 --- a/applications/services/desktop/views/desktop_view_locked.c +++ b/applications/services/desktop/views/desktop_view_locked.c @@ -160,7 +160,7 @@ static bool desktop_view_locked_input(InputEvent* event, void* context) { view_commit_model(locked_view->view, is_changed); if(view_state == DesktopViewLockedStateUnlocked) { - return view_state != DesktopViewLockedStateUnlocked; + return false; } else if(view_state == DesktopViewLockedStateLocked && pin_locked) { locked_view->callback(DesktopLockedEventShowPinInput, locked_view->context); } else if( diff --git a/applications/services/gui/view_dispatcher.c b/applications/services/gui/view_dispatcher.c index 1736558cb..8de452d20 100644 --- a/applications/services/gui/view_dispatcher.c +++ b/applications/services/gui/view_dispatcher.c @@ -304,8 +304,7 @@ void view_dispatcher_handle_custom_event(ViewDispatcher* view_dispatcher, uint32 } // If custom event is not consumed in View, call callback if(!is_consumed && view_dispatcher->custom_event_callback) { - is_consumed = - view_dispatcher->custom_event_callback(view_dispatcher->event_context, event); + view_dispatcher->custom_event_callback(view_dispatcher->event_context, event); } } diff --git a/applications/services/notification/notification_app.c b/applications/services/notification/notification_app.c index 640bd7d71..6091f0aa7 100644 --- a/applications/services/notification/notification_app.c +++ b/applications/services/notification/notification_app.c @@ -22,7 +22,7 @@ static const uint8_t reset_blink_mask = 1 << 6; void notification_vibro_on(); void notification_vibro_off(); -void notification_sound_on(float pwm, float freq); +void notification_sound_on(float freq, float volume); void notification_sound_off(); uint8_t notification_settings_get_display_brightness(NotificationApp* app, uint8_t value); diff --git a/applications/settings/power_settings_app/views/battery_info.c b/applications/settings/power_settings_app/views/battery_info.c index e1b7adb4b..bbb0acb9a 100644 --- a/applications/settings/power_settings_app/views/battery_info.c +++ b/applications/settings/power_settings_app/views/battery_info.c @@ -3,6 +3,9 @@ #include #include +#define LOW_CHARGE_THRESHOLD 10 +#define HIGH_DRAIN_CURRENT_THRESHOLD 100 + struct BatteryInfo { View* view; }; @@ -28,9 +31,9 @@ static void draw_battery(Canvas* canvas, BatteryInfoModel* data, int x, int y) { canvas_draw_icon(canvas, x, y, &I_BatteryBody_52x28); if(charge_current > 0) { canvas_draw_icon(canvas, x + 16, y + 7, &I_FaceCharging_29x14); - } else if(drain_current > 100) { + } else if(drain_current > HIGH_DRAIN_CURRENT_THRESHOLD) { canvas_draw_icon(canvas, x + 16, y + 7, &I_FaceConfused_29x14); - } else if(data->charge < 10) { + } else if(data->charge < LOW_CHARGE_THRESHOLD) { canvas_draw_icon(canvas, x + 16, y + 7, &I_FaceNopower_29x14); } else { canvas_draw_icon(canvas, x + 16, y + 7, &I_FaceNormal_29x14); @@ -51,11 +54,19 @@ static void draw_battery(Canvas* canvas, BatteryInfoModel* data, int x, int y) { (uint32_t)(data->vbus_voltage * 10) % 10, charge_current); } else if(drain_current > 0) { - snprintf(emote, sizeof(emote), "%s", drain_current > 100 ? "Oh no!" : "Om-nom-nom!"); + snprintf( + emote, + sizeof(emote), + "%s", + drain_current > HIGH_DRAIN_CURRENT_THRESHOLD ? "Oh no!" : "Om-nom-nom!"); snprintf(header, sizeof(header), "%s", "Consumption is"); snprintf( - value, sizeof(value), "%ld %s", drain_current, drain_current > 100 ? "mA!" : "mA"); - } else if(charge_current != 0 || drain_current != 0) { + value, + sizeof(value), + "%ld %s", + drain_current, + drain_current > HIGH_DRAIN_CURRENT_THRESHOLD ? "mA!" : "mA"); + } else if(drain_current != 0) { snprintf(header, 20, "..."); } else { snprintf(header, sizeof(header), "Charged!"); From bf8fd71c00be90015811b9e6b13b88d0e7b1d5ee Mon Sep 17 00:00:00 2001 From: gornekich Date: Fri, 4 Nov 2022 11:01:44 +0400 Subject: [PATCH 24/49] NFC magic cards support (#1966) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * nfc magic: introduce nfc app to work with magic cards * nfc: add nfc device functions to API * nfc magic: add bacis scenes * nfc magic: add wrong card and write confirm scenes * nfc magic: introduce magic lib * nfc magic: write magic lib * nfc magic: add write commands to magic lib * nfc magic: work on worker * furi_hal_nfc: add bits data exchage method to API * nfc magic: rework with new API * nfc magic: add check and wipe scenes * nfc magic: add icons, gui fixes * nfc: format python src Co-authored-by: あく --- .../plugins/nfc_magic/application.fam | 20 ++ .../nfc_magic/assets/DolphinCommon_56x48.png | Bin 0 -> 1416 bytes .../nfc_magic/assets/DolphinNice_96x59.png | Bin 0 -> 2459 bytes .../plugins/nfc_magic/assets/Loading_24.png | Bin 0 -> 3649 bytes .../nfc_magic/assets/NFC_manual_60x50.png | Bin 0 -> 3804 bytes .../plugins/nfc_magic/lib/magic/magic.c | 214 ++++++++++++++++++ .../plugins/nfc_magic/lib/magic/magic.h | 15 ++ applications/plugins/nfc_magic/nfc_magic.c | 169 ++++++++++++++ applications/plugins/nfc_magic/nfc_magic.h | 3 + applications/plugins/nfc_magic/nfc_magic_i.h | 77 +++++++ .../plugins/nfc_magic/nfc_magic_worker.c | 174 ++++++++++++++ .../plugins/nfc_magic/nfc_magic_worker.h | 38 ++++ .../plugins/nfc_magic/nfc_magic_worker_i.h | 24 ++ .../nfc_magic/scenes/nfc_magic_scene.c | 30 +++ .../nfc_magic/scenes/nfc_magic_scene.h | 29 +++ .../nfc_magic/scenes/nfc_magic_scene_check.c | 87 +++++++ .../nfc_magic/scenes/nfc_magic_scene_config.h | 12 + .../scenes/nfc_magic_scene_file_select.c | 34 +++ .../scenes/nfc_magic_scene_magic_info.c | 45 ++++ .../scenes/nfc_magic_scene_not_magic.c | 44 ++++ .../nfc_magic/scenes/nfc_magic_scene_start.c | 61 +++++ .../scenes/nfc_magic_scene_success.c | 42 ++++ .../nfc_magic/scenes/nfc_magic_scene_wipe.c | 90 ++++++++ .../scenes/nfc_magic_scene_wipe_fail.c | 41 ++++ .../nfc_magic/scenes/nfc_magic_scene_write.c | 90 ++++++++ .../scenes/nfc_magic_scene_write_confirm.c | 64 ++++++ .../scenes/nfc_magic_scene_write_fail.c | 58 +++++ .../scenes/nfc_magic_scene_wrong_card.c | 53 +++++ firmware/targets/f7/api_symbols.csv | 121 ++++++++++ firmware/targets/f7/furi_hal/furi_hal_nfc.c | 11 + .../targets/furi_hal_include/furi_hal_nfc.h | 10 + lib/ST25RFAL002/include/rfal_rf.h | 9 + .../source/st25r3916/rfal_rfst25r3916.c | 17 ++ lib/nfc/SConscript | 3 + lib/nfc/nfc_device.h | 8 + 35 files changed, 1693 insertions(+) create mode 100644 applications/plugins/nfc_magic/application.fam create mode 100644 applications/plugins/nfc_magic/assets/DolphinCommon_56x48.png create mode 100644 applications/plugins/nfc_magic/assets/DolphinNice_96x59.png create mode 100644 applications/plugins/nfc_magic/assets/Loading_24.png create mode 100644 applications/plugins/nfc_magic/assets/NFC_manual_60x50.png create mode 100644 applications/plugins/nfc_magic/lib/magic/magic.c create mode 100644 applications/plugins/nfc_magic/lib/magic/magic.h create mode 100644 applications/plugins/nfc_magic/nfc_magic.c create mode 100644 applications/plugins/nfc_magic/nfc_magic.h create mode 100644 applications/plugins/nfc_magic/nfc_magic_i.h create mode 100644 applications/plugins/nfc_magic/nfc_magic_worker.c create mode 100644 applications/plugins/nfc_magic/nfc_magic_worker.h create mode 100644 applications/plugins/nfc_magic/nfc_magic_worker_i.h create mode 100644 applications/plugins/nfc_magic/scenes/nfc_magic_scene.c create mode 100644 applications/plugins/nfc_magic/scenes/nfc_magic_scene.h create mode 100644 applications/plugins/nfc_magic/scenes/nfc_magic_scene_check.c create mode 100644 applications/plugins/nfc_magic/scenes/nfc_magic_scene_config.h create mode 100644 applications/plugins/nfc_magic/scenes/nfc_magic_scene_file_select.c create mode 100644 applications/plugins/nfc_magic/scenes/nfc_magic_scene_magic_info.c create mode 100644 applications/plugins/nfc_magic/scenes/nfc_magic_scene_not_magic.c create mode 100644 applications/plugins/nfc_magic/scenes/nfc_magic_scene_start.c create mode 100644 applications/plugins/nfc_magic/scenes/nfc_magic_scene_success.c create mode 100644 applications/plugins/nfc_magic/scenes/nfc_magic_scene_wipe.c create mode 100644 applications/plugins/nfc_magic/scenes/nfc_magic_scene_wipe_fail.c create mode 100644 applications/plugins/nfc_magic/scenes/nfc_magic_scene_write.c create mode 100644 applications/plugins/nfc_magic/scenes/nfc_magic_scene_write_confirm.c create mode 100644 applications/plugins/nfc_magic/scenes/nfc_magic_scene_write_fail.c create mode 100644 applications/plugins/nfc_magic/scenes/nfc_magic_scene_wrong_card.c diff --git a/applications/plugins/nfc_magic/application.fam b/applications/plugins/nfc_magic/application.fam new file mode 100644 index 000000000..f09d65c90 --- /dev/null +++ b/applications/plugins/nfc_magic/application.fam @@ -0,0 +1,20 @@ +App( + appid="nfc_magic", + name="Nfc Magic", + apptype=FlipperAppType.EXTERNAL, + entry_point="nfc_magic_app", + requires=[ + "storage", + "gui", + ], + stack_size=4 * 1024, + order=30, + fap_icon="../../../assets/icons/Archive/125_10px.png", + fap_category="Tools", + fap_private_libs=[ + Lib( + name="magic", + ), + ], + fap_icon_assets="assets", +) diff --git a/applications/plugins/nfc_magic/assets/DolphinCommon_56x48.png b/applications/plugins/nfc_magic/assets/DolphinCommon_56x48.png new file mode 100644 index 0000000000000000000000000000000000000000..089aaed83507431993a76ca25d32fdd9664c1c84 GIT binary patch literal 1416 zcmaJ>eNYr-7(dh;KXS5&nWVIBjS_NizYg|x=Pr^vz*7zxJO|P-dw2IeZq?gec9-rD zoPZchQ_6}yP{Slc4I!!28K==nodOJ_nsCY-(wOq2uZbLx!rlYU{KIi)_Wj!D_j`WN z^FGgREXdEDF)ewT&1Re7Tj(uBvlG44lnH3;I%IzsO|z`*Vr!`uv?9QOwgs{#Ld+Ki zC9n_zxxBOkx@@+IwMwAaD)#3Ik`}gun2kLe))Crfb7e+#AgzHGCc+X$b>qJuIf`S7 z?8b}I{ghw#z>uiaLknQh@LJUrqHcVYS3v97F^OZN zCe|7^J|?QzUx0Zu17e(=CM1fYFpjtLk|a4~$g}e?hGH0!VoBOT&<=s(1ct%J9~?O} z$)jW_dkX9yTX~%W*i_IM%0{ z7EmP^_pKn`<5>E(SixgJU};7`)7Hidp&+DLnizsebUk}_-GfgbN^il9b`v)f+ z{o5Zry)d<7`fHQ^uw_;+x>mcPw0&8iW69x{k92O{Q}`yFdH=5d$pbf49w1&NS)G+vhr6y}5TMsofQirRDUmKilk5=(KGouJ{H9hW=$X zgi;)vI!jl!_4H3jD(?Jz=8By|i47I&tKA1y9{nfp;_|FxKBDNWp{hN9hJ1nU?z%J6 z?>UxyzWvO}Pgc~rCZ#5%Eq+_hNS~bBdiGlT&f%%e`hHjSySR2=JuK2^+%;$R3#Wz~ z=e_mfqW23bPa0fhe)HdE5+GelU&!jS3ckUZOQ)CC5?mo zo=tzG_4|RuvPUO|mhCwA>y)1c%SWC%a4?a-x|J*?ch~+n=R7o@>p6J2dE=$stKZmK z-xoTRwET2^Wu)&1U7!Ebw!!D?x`xwQX3pMnrRwCT?`4GHt4&?|cIiI{_^XYp-np>6 xE^lPSXzOYCC4X`6tl@OB1M5_S7jml-Y~(TPp{aTIejNKZ`m*!Atyxdk{0EAy49frj literal 0 HcmV?d00001 diff --git a/applications/plugins/nfc_magic/assets/DolphinNice_96x59.png b/applications/plugins/nfc_magic/assets/DolphinNice_96x59.png new file mode 100644 index 0000000000000000000000000000000000000000..a299d3630239b4486e249cc501872bed5996df3b GIT binary patch literal 2459 zcmbVO3s4i+8V(M(gEFORwSrA`4O0uPn|M|5y* zB*aMDxC&7(gP9JN;POOi-9khrC>Z9YJs2U!LnVcQEEC0fDtKo&ILlzb30%M}3J^;~ zv7RzcsilOs4Mq@tD*&R;!LMSk2A~{(`HK9|hQBqEX)3sQr9Je6SZU*F-^fD-p+~Hs; zHLkO%v?>ZoxEv+F#whudr%615FkA0DYR0tMEo}3OOY#xecLWe>xV?u5KtSmC^ z7)Fmj6gjfKstiEV-*Cxbbb+&rRWuI_rBJ)ybs_f1Rn&f2>q3pYwI^|J(hdn{j{0EZIm_F zpIyIWLsRUgOItR-dUbVd|6Zo=_BU_Tj4|{{jxO#=JH4o8er(5{!nZD_j4}MH&zh~9 zVLC~y(0-D6GO0ghZD8BYzP?o{>22~lT6^d@X{SwQ8vrNY-PPIMajIwC)`s14Ep72@ zeq7YOzM`?U{+W)ocXBr`eSOcpk?Rxc=ou5&)fWW|pD};-Z0mvk9}=&`Rb&y<77W~a z(>6YM;6Y5aIU~JKZ}mQZynKHiSTQ#Bczn@&jTiN^?vPJ(jhm7cXLx0oum5P$`TceG zU+wR;OO^)8CVlnM)5p$CO&e94KJt>HccCaHGusmW_b`T6m| z-R6V6Db1pErTot?^d22ojm+2>_)FbD`_+WbDGMx9f@hO27maS2`csiV(D&Fs`PS2& zvrq18du_&zXID(!KIxsU$)iuTYuZ?zmYiP&n&i@Be{IdbS-jA2c0QAlu5NXQv_0K< z3Hvs4eeu6B7yD&CNT~gIkMV&UkRU=V!iQ(+_(O&u^ah$+s{_yn(yBYeD40HeU{xGsIT6W Zfq!wOp!QR4Q+~K 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 LTALIbyM_M?u*zuP literal 0 HcmV?d00001 diff --git a/applications/plugins/nfc_magic/assets/NFC_manual_60x50.png b/applications/plugins/nfc_magic/assets/NFC_manual_60x50.png new file mode 100644 index 0000000000000000000000000000000000000000..787c0bcfe01755f4dcadcdce004a1a0fcfb06f41 GIT binary patch literal 3804 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 IrsqTc1+$%)k^lez literal 0 HcmV?d00001 diff --git a/applications/plugins/nfc_magic/lib/magic/magic.c b/applications/plugins/nfc_magic/lib/magic/magic.c new file mode 100644 index 000000000..3cfca748b --- /dev/null +++ b/applications/plugins/nfc_magic/lib/magic/magic.c @@ -0,0 +1,214 @@ +#include "magic.h" + +#include + +#define TAG "Magic" + +#define MAGIC_CMD_WUPA (0x40) +#define MAGIC_CMD_WIPE (0x41) +#define MAGIC_CMD_READ (0x43) +#define MAGIC_CMD_WRITE (0x43) + +#define MAGIC_MIFARE_READ_CMD (0x30) +#define MAGIC_MIFARE_WRITE_CMD (0xA0) + +#define MAGIC_ACK (0x0A) + +#define MAGIC_BUFFER_SIZE (32) + +bool magic_wupa() { + bool magic_activated = false; + uint8_t tx_data[MAGIC_BUFFER_SIZE] = {}; + uint8_t rx_data[MAGIC_BUFFER_SIZE] = {}; + uint16_t rx_len = 0; + FuriHalNfcReturn ret = 0; + + do { + // Setup nfc poller + furi_hal_nfc_exit_sleep(); + furi_hal_nfc_ll_txrx_on(); + furi_hal_nfc_ll_poll(); + ret = furi_hal_nfc_ll_set_mode( + FuriHalNfcModePollNfca, FuriHalNfcBitrate106, FuriHalNfcBitrate106); + if(ret != FuriHalNfcReturnOk) break; + + furi_hal_nfc_ll_set_fdt_listen(FURI_HAL_NFC_LL_FDT_LISTEN_NFCA_POLLER); + furi_hal_nfc_ll_set_fdt_poll(FURI_HAL_NFC_LL_FDT_POLL_NFCA_POLLER); + furi_hal_nfc_ll_set_error_handling(FuriHalNfcErrorHandlingNfc); + furi_hal_nfc_ll_set_guard_time(FURI_HAL_NFC_LL_GT_NFCA); + + // Start communication + tx_data[0] = MAGIC_CMD_WUPA; + ret = furi_hal_nfc_ll_txrx_bits( + tx_data, + 7, + rx_data, + sizeof(rx_data), + &rx_len, + FURI_HAL_NFC_LL_TXRX_FLAGS_CRC_TX_MANUAL | FURI_HAL_NFC_LL_TXRX_FLAGS_AGC_ON | + FURI_HAL_NFC_LL_TXRX_FLAGS_CRC_RX_KEEP, + furi_hal_nfc_ll_ms2fc(20)); + if(ret != FuriHalNfcReturnIncompleteByte) break; + if(rx_len != 4) break; + if(rx_data[0] != MAGIC_ACK) break; + magic_activated = true; + } while(false); + + if(!magic_activated) { + furi_hal_nfc_ll_txrx_off(); + furi_hal_nfc_start_sleep(); + } + + return magic_activated; +} + +bool magic_data_access_cmd() { + bool write_cmd_success = false; + uint8_t tx_data[MAGIC_BUFFER_SIZE] = {}; + uint8_t rx_data[MAGIC_BUFFER_SIZE] = {}; + uint16_t rx_len = 0; + FuriHalNfcReturn ret = 0; + + do { + tx_data[0] = MAGIC_CMD_WRITE; + ret = furi_hal_nfc_ll_txrx_bits( + tx_data, + 8, + rx_data, + sizeof(rx_data), + &rx_len, + FURI_HAL_NFC_LL_TXRX_FLAGS_CRC_TX_MANUAL | FURI_HAL_NFC_LL_TXRX_FLAGS_AGC_ON | + FURI_HAL_NFC_LL_TXRX_FLAGS_CRC_RX_KEEP, + furi_hal_nfc_ll_ms2fc(20)); + if(ret != FuriHalNfcReturnIncompleteByte) break; + if(rx_len != 4) break; + if(rx_data[0] != MAGIC_ACK) break; + + write_cmd_success = true; + } while(false); + + if(!write_cmd_success) { + furi_hal_nfc_ll_txrx_off(); + furi_hal_nfc_start_sleep(); + } + + return write_cmd_success; +} + +bool magic_read_block(uint8_t block_num, MfClassicBlock* data) { + furi_assert(data); + + bool read_success = false; + + uint8_t tx_data[MAGIC_BUFFER_SIZE] = {}; + uint8_t rx_data[MAGIC_BUFFER_SIZE] = {}; + uint16_t rx_len = 0; + FuriHalNfcReturn ret = 0; + + do { + tx_data[0] = MAGIC_MIFARE_READ_CMD; + tx_data[1] = block_num; + ret = furi_hal_nfc_ll_txrx_bits( + tx_data, + 2 * 8, + rx_data, + sizeof(rx_data), + &rx_len, + FURI_HAL_NFC_LL_TXRX_FLAGS_AGC_ON, + furi_hal_nfc_ll_ms2fc(20)); + + if(ret != FuriHalNfcReturnOk) break; + if(rx_len != 16 * 8) break; + memcpy(data->value, rx_data, sizeof(data->value)); + read_success = true; + } while(false); + + if(!read_success) { + furi_hal_nfc_ll_txrx_off(); + furi_hal_nfc_start_sleep(); + } + + return read_success; +} + +bool magic_write_blk(uint8_t block_num, MfClassicBlock* data) { + furi_assert(data); + + bool write_success = false; + uint8_t tx_data[MAGIC_BUFFER_SIZE] = {}; + uint8_t rx_data[MAGIC_BUFFER_SIZE] = {}; + uint16_t rx_len = 0; + FuriHalNfcReturn ret = 0; + + do { + tx_data[0] = MAGIC_MIFARE_WRITE_CMD; + tx_data[1] = block_num; + ret = furi_hal_nfc_ll_txrx_bits( + tx_data, + 2 * 8, + rx_data, + sizeof(rx_data), + &rx_len, + FURI_HAL_NFC_LL_TXRX_FLAGS_AGC_ON | FURI_HAL_NFC_LL_TXRX_FLAGS_CRC_RX_KEEP, + furi_hal_nfc_ll_ms2fc(20)); + if(ret != FuriHalNfcReturnIncompleteByte) break; + if(rx_len != 4) break; + if(rx_data[0] != MAGIC_ACK) break; + + memcpy(tx_data, data->value, sizeof(data->value)); + ret = furi_hal_nfc_ll_txrx_bits( + tx_data, + 16 * 8, + rx_data, + sizeof(rx_data), + &rx_len, + FURI_HAL_NFC_LL_TXRX_FLAGS_AGC_ON | FURI_HAL_NFC_LL_TXRX_FLAGS_CRC_RX_KEEP, + furi_hal_nfc_ll_ms2fc(20)); + if(ret != FuriHalNfcReturnIncompleteByte) break; + if(rx_len != 4) break; + if(rx_data[0] != MAGIC_ACK) break; + + write_success = true; + } while(false); + + if(!write_success) { + furi_hal_nfc_ll_txrx_off(); + furi_hal_nfc_start_sleep(); + } + + return write_success; +} + +bool magic_wipe() { + bool wipe_success = false; + uint8_t tx_data[MAGIC_BUFFER_SIZE] = {}; + uint8_t rx_data[MAGIC_BUFFER_SIZE] = {}; + uint16_t rx_len = 0; + FuriHalNfcReturn ret = 0; + + do { + tx_data[0] = MAGIC_CMD_WIPE; + ret = furi_hal_nfc_ll_txrx_bits( + tx_data, + 8, + rx_data, + sizeof(rx_data), + &rx_len, + FURI_HAL_NFC_LL_TXRX_FLAGS_CRC_TX_MANUAL | FURI_HAL_NFC_LL_TXRX_FLAGS_AGC_ON | + FURI_HAL_NFC_LL_TXRX_FLAGS_CRC_RX_KEEP, + furi_hal_nfc_ll_ms2fc(2000)); + + if(ret != FuriHalNfcReturnIncompleteByte) break; + if(rx_len != 4) break; + if(rx_data[0] != MAGIC_ACK) break; + + wipe_success = true; + } while(false); + + return wipe_success; +} + +void magic_deactivate() { + furi_hal_nfc_ll_txrx_off(); + furi_hal_nfc_start_sleep(); +} diff --git a/applications/plugins/nfc_magic/lib/magic/magic.h b/applications/plugins/nfc_magic/lib/magic/magic.h new file mode 100644 index 000000000..64c60a0a7 --- /dev/null +++ b/applications/plugins/nfc_magic/lib/magic/magic.h @@ -0,0 +1,15 @@ +#pragma once + +#include + +bool magic_wupa(); + +bool magic_read_block(uint8_t block_num, MfClassicBlock* data); + +bool magic_data_access_cmd(); + +bool magic_write_blk(uint8_t block_num, MfClassicBlock* data); + +bool magic_wipe(); + +void magic_deactivate(); diff --git a/applications/plugins/nfc_magic/nfc_magic.c b/applications/plugins/nfc_magic/nfc_magic.c new file mode 100644 index 000000000..38eecba6a --- /dev/null +++ b/applications/plugins/nfc_magic/nfc_magic.c @@ -0,0 +1,169 @@ +#include "nfc_magic_i.h" + +bool nfc_magic_custom_event_callback(void* context, uint32_t event) { + furi_assert(context); + NfcMagic* nfc_magic = context; + return scene_manager_handle_custom_event(nfc_magic->scene_manager, event); +} + +bool nfc_magic_back_event_callback(void* context) { + furi_assert(context); + NfcMagic* nfc_magic = context; + return scene_manager_handle_back_event(nfc_magic->scene_manager); +} + +void nfc_magic_tick_event_callback(void* context) { + furi_assert(context); + NfcMagic* nfc_magic = context; + scene_manager_handle_tick_event(nfc_magic->scene_manager); +} + +void nfc_magic_show_loading_popup(void* context, bool show) { + NfcMagic* nfc_magic = context; + TaskHandle_t timer_task = xTaskGetHandle(configTIMER_SERVICE_TASK_NAME); + + if(show) { + // Raise timer priority so that animations can play + vTaskPrioritySet(timer_task, configMAX_PRIORITIES - 1); + view_dispatcher_switch_to_view(nfc_magic->view_dispatcher, NfcMagicViewLoading); + } else { + // Restore default timer priority + vTaskPrioritySet(timer_task, configTIMER_TASK_PRIORITY); + } +} + +NfcMagic* nfc_magic_alloc() { + NfcMagic* nfc_magic = malloc(sizeof(NfcMagic)); + + nfc_magic->worker = nfc_magic_worker_alloc(); + nfc_magic->view_dispatcher = view_dispatcher_alloc(); + nfc_magic->scene_manager = scene_manager_alloc(&nfc_magic_scene_handlers, nfc_magic); + view_dispatcher_enable_queue(nfc_magic->view_dispatcher); + view_dispatcher_set_event_callback_context(nfc_magic->view_dispatcher, nfc_magic); + view_dispatcher_set_custom_event_callback( + nfc_magic->view_dispatcher, nfc_magic_custom_event_callback); + view_dispatcher_set_navigation_event_callback( + nfc_magic->view_dispatcher, nfc_magic_back_event_callback); + view_dispatcher_set_tick_event_callback( + nfc_magic->view_dispatcher, nfc_magic_tick_event_callback, 100); + + // Nfc device + nfc_magic->nfc_dev = nfc_device_alloc(); + + // Open GUI record + nfc_magic->gui = furi_record_open(RECORD_GUI); + view_dispatcher_attach_to_gui( + nfc_magic->view_dispatcher, nfc_magic->gui, ViewDispatcherTypeFullscreen); + + // Open Notification record + nfc_magic->notifications = furi_record_open(RECORD_NOTIFICATION); + + // Submenu + nfc_magic->submenu = submenu_alloc(); + view_dispatcher_add_view( + nfc_magic->view_dispatcher, NfcMagicViewMenu, submenu_get_view(nfc_magic->submenu)); + + // Popup + nfc_magic->popup = popup_alloc(); + view_dispatcher_add_view( + nfc_magic->view_dispatcher, NfcMagicViewPopup, popup_get_view(nfc_magic->popup)); + + // Loading + nfc_magic->loading = loading_alloc(); + view_dispatcher_add_view( + nfc_magic->view_dispatcher, NfcMagicViewLoading, loading_get_view(nfc_magic->loading)); + + // Text Input + nfc_magic->text_input = text_input_alloc(); + view_dispatcher_add_view( + nfc_magic->view_dispatcher, + NfcMagicViewTextInput, + text_input_get_view(nfc_magic->text_input)); + + // Custom Widget + nfc_magic->widget = widget_alloc(); + view_dispatcher_add_view( + nfc_magic->view_dispatcher, NfcMagicViewWidget, widget_get_view(nfc_magic->widget)); + + return nfc_magic; +} + +void nfc_magic_free(NfcMagic* nfc_magic) { + furi_assert(nfc_magic); + + // Nfc device + nfc_device_free(nfc_magic->nfc_dev); + + // Submenu + view_dispatcher_remove_view(nfc_magic->view_dispatcher, NfcMagicViewMenu); + submenu_free(nfc_magic->submenu); + + // Popup + view_dispatcher_remove_view(nfc_magic->view_dispatcher, NfcMagicViewPopup); + popup_free(nfc_magic->popup); + + // Loading + view_dispatcher_remove_view(nfc_magic->view_dispatcher, NfcMagicViewLoading); + loading_free(nfc_magic->loading); + + // TextInput + view_dispatcher_remove_view(nfc_magic->view_dispatcher, NfcMagicViewTextInput); + text_input_free(nfc_magic->text_input); + + // Custom Widget + view_dispatcher_remove_view(nfc_magic->view_dispatcher, NfcMagicViewWidget); + widget_free(nfc_magic->widget); + + // Worker + nfc_magic_worker_stop(nfc_magic->worker); + nfc_magic_worker_free(nfc_magic->worker); + + // View Dispatcher + view_dispatcher_free(nfc_magic->view_dispatcher); + + // Scene Manager + scene_manager_free(nfc_magic->scene_manager); + + // GUI + furi_record_close(RECORD_GUI); + nfc_magic->gui = NULL; + + // Notifications + furi_record_close(RECORD_NOTIFICATION); + nfc_magic->notifications = NULL; + + free(nfc_magic); +} + +static const NotificationSequence nfc_magic_sequence_blink_start_blue = { + &message_blink_start_10, + &message_blink_set_color_blue, + &message_do_not_reset, + NULL, +}; + +static const NotificationSequence nfc_magic_sequence_blink_stop = { + &message_blink_stop, + NULL, +}; + +void nfc_magic_blink_start(NfcMagic* nfc_magic) { + notification_message(nfc_magic->notifications, &nfc_magic_sequence_blink_start_blue); +} + +void nfc_magic_blink_stop(NfcMagic* nfc_magic) { + notification_message(nfc_magic->notifications, &nfc_magic_sequence_blink_stop); +} + +int32_t nfc_magic_app(void* p) { + UNUSED(p); + NfcMagic* nfc_magic = nfc_magic_alloc(); + + scene_manager_next_scene(nfc_magic->scene_manager, NfcMagicSceneStart); + + view_dispatcher_run(nfc_magic->view_dispatcher); + + nfc_magic_free(nfc_magic); + + return 0; +} diff --git a/applications/plugins/nfc_magic/nfc_magic.h b/applications/plugins/nfc_magic/nfc_magic.h new file mode 100644 index 000000000..1abf1371e --- /dev/null +++ b/applications/plugins/nfc_magic/nfc_magic.h @@ -0,0 +1,3 @@ +#pragma once + +typedef struct NfcMagic NfcMagic; diff --git a/applications/plugins/nfc_magic/nfc_magic_i.h b/applications/plugins/nfc_magic/nfc_magic_i.h new file mode 100644 index 000000000..01b300826 --- /dev/null +++ b/applications/plugins/nfc_magic/nfc_magic_i.h @@ -0,0 +1,77 @@ +#pragma once + +#include "nfc_magic.h" +#include "nfc_magic_worker.h" + +#include "lib/magic/magic.h" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include + +#include "scenes/nfc_magic_scene.h" + +#include +#include + +#include +#include "nfc_magic_icons.h" + +enum NfcMagicCustomEvent { + // Reserve first 100 events for button types and indexes, starting from 0 + NfcMagicCustomEventReserved = 100, + + NfcMagicCustomEventViewExit, + NfcMagicCustomEventWorkerExit, + NfcMagicCustomEventByteInputDone, + NfcMagicCustomEventTextInputDone, +}; + +struct NfcMagic { + NfcMagicWorker* worker; + ViewDispatcher* view_dispatcher; + Gui* gui; + NotificationApp* notifications; + SceneManager* scene_manager; + // NfcMagicDevice* dev; + NfcDevice* nfc_dev; + + FuriString* text_box_store; + + // Common Views + Submenu* submenu; + Popup* popup; + Loading* loading; + TextInput* text_input; + Widget* widget; +}; + +typedef enum { + NfcMagicViewMenu, + NfcMagicViewPopup, + NfcMagicViewLoading, + NfcMagicViewTextInput, + NfcMagicViewWidget, +} NfcMagicView; + +NfcMagic* nfc_magic_alloc(); + +void nfc_magic_text_store_set(NfcMagic* nfc_magic, const char* text, ...); + +void nfc_magic_text_store_clear(NfcMagic* nfc_magic); + +void nfc_magic_blink_start(NfcMagic* nfc_magic); + +void nfc_magic_blink_stop(NfcMagic* nfc_magic); + +void nfc_magic_show_loading_popup(void* context, bool show); diff --git a/applications/plugins/nfc_magic/nfc_magic_worker.c b/applications/plugins/nfc_magic/nfc_magic_worker.c new file mode 100644 index 000000000..0623211e2 --- /dev/null +++ b/applications/plugins/nfc_magic/nfc_magic_worker.c @@ -0,0 +1,174 @@ +#include "nfc_magic_worker_i.h" + +#include "lib/magic/magic.h" + +#define TAG "NfcMagicWorker" + +static void + nfc_magic_worker_change_state(NfcMagicWorker* nfc_magic_worker, NfcMagicWorkerState state) { + furi_assert(nfc_magic_worker); + + nfc_magic_worker->state = state; +} + +NfcMagicWorker* nfc_magic_worker_alloc() { + NfcMagicWorker* nfc_magic_worker = malloc(sizeof(NfcMagicWorker)); + + // Worker thread attributes + nfc_magic_worker->thread = furi_thread_alloc(); + furi_thread_set_name(nfc_magic_worker->thread, "NfcMagicWorker"); + furi_thread_set_stack_size(nfc_magic_worker->thread, 8192); + furi_thread_set_callback(nfc_magic_worker->thread, nfc_magic_worker_task); + furi_thread_set_context(nfc_magic_worker->thread, nfc_magic_worker); + + nfc_magic_worker->callback = NULL; + nfc_magic_worker->context = NULL; + + nfc_magic_worker_change_state(nfc_magic_worker, NfcMagicWorkerStateReady); + + return nfc_magic_worker; +} + +void nfc_magic_worker_free(NfcMagicWorker* nfc_magic_worker) { + furi_assert(nfc_magic_worker); + + furi_thread_free(nfc_magic_worker->thread); + free(nfc_magic_worker); +} + +void nfc_magic_worker_stop(NfcMagicWorker* nfc_magic_worker) { + furi_assert(nfc_magic_worker); + + nfc_magic_worker_change_state(nfc_magic_worker, NfcMagicWorkerStateStop); + furi_thread_join(nfc_magic_worker->thread); +} + +void nfc_magic_worker_start( + NfcMagicWorker* nfc_magic_worker, + NfcMagicWorkerState state, + NfcDeviceData* dev_data, + NfcMagicWorkerCallback callback, + void* context) { + furi_assert(nfc_magic_worker); + furi_assert(dev_data); + + nfc_magic_worker->callback = callback; + nfc_magic_worker->context = context; + nfc_magic_worker->dev_data = dev_data; + nfc_magic_worker_change_state(nfc_magic_worker, state); + furi_thread_start(nfc_magic_worker->thread); +} + +int32_t nfc_magic_worker_task(void* context) { + NfcMagicWorker* nfc_magic_worker = context; + + if(nfc_magic_worker->state == NfcMagicWorkerStateCheck) { + nfc_magic_worker_check(nfc_magic_worker); + } else if(nfc_magic_worker->state == NfcMagicWorkerStateWrite) { + nfc_magic_worker_write(nfc_magic_worker); + } else if(nfc_magic_worker->state == NfcMagicWorkerStateWipe) { + nfc_magic_worker_wipe(nfc_magic_worker); + } + + nfc_magic_worker_change_state(nfc_magic_worker, NfcMagicWorkerStateReady); + + return 0; +} + +void nfc_magic_worker_write(NfcMagicWorker* nfc_magic_worker) { + bool card_found_notified = false; + FuriHalNfcDevData nfc_data = {}; + MfClassicData* src_data = &nfc_magic_worker->dev_data->mf_classic_data; + + while(nfc_magic_worker->state == NfcMagicWorkerStateWrite) { + if(furi_hal_nfc_detect(&nfc_data, 200)) { + if(!card_found_notified) { + nfc_magic_worker->callback( + NfcMagicWorkerEventCardDetected, nfc_magic_worker->context); + card_found_notified = true; + } + furi_hal_nfc_sleep(); + + if(!magic_wupa()) { + FURI_LOG_E(TAG, "Not Magic card"); + nfc_magic_worker->callback( + NfcMagicWorkerEventWrongCard, nfc_magic_worker->context); + break; + } + if(!magic_data_access_cmd()) { + FURI_LOG_E(TAG, "Not Magic card"); + nfc_magic_worker->callback( + NfcMagicWorkerEventWrongCard, nfc_magic_worker->context); + break; + } + for(size_t i = 0; i < 64; i++) { + FURI_LOG_D(TAG, "Writing block %d", i); + if(!magic_write_blk(i, &src_data->block[i])) { + FURI_LOG_E(TAG, "Failed to write %d block", i); + nfc_magic_worker->callback(NfcMagicWorkerEventFail, nfc_magic_worker->context); + break; + } + } + nfc_magic_worker->callback(NfcMagicWorkerEventSuccess, nfc_magic_worker->context); + break; + } else { + if(card_found_notified) { + nfc_magic_worker->callback( + NfcMagicWorkerEventNoCardDetected, nfc_magic_worker->context); + card_found_notified = false; + } + } + furi_delay_ms(300); + } + magic_deactivate(); +} + +void nfc_magic_worker_check(NfcMagicWorker* nfc_magic_worker) { + bool card_found_notified = false; + + while(nfc_magic_worker->state == NfcMagicWorkerStateCheck) { + if(magic_wupa()) { + if(!card_found_notified) { + nfc_magic_worker->callback( + NfcMagicWorkerEventCardDetected, nfc_magic_worker->context); + card_found_notified = true; + } + + nfc_magic_worker->callback(NfcMagicWorkerEventSuccess, nfc_magic_worker->context); + break; + } else { + if(card_found_notified) { + nfc_magic_worker->callback( + NfcMagicWorkerEventNoCardDetected, nfc_magic_worker->context); + card_found_notified = false; + } + } + furi_delay_ms(300); + } + magic_deactivate(); +} + +void nfc_magic_worker_wipe(NfcMagicWorker* nfc_magic_worker) { + MfClassicBlock block; + memset(&block, 0, sizeof(MfClassicBlock)); + block.value[0] = 0x01; + block.value[1] = 0x02; + block.value[2] = 0x03; + block.value[3] = 0x04; + block.value[4] = 0x04; + block.value[5] = 0x08; + block.value[6] = 0x04; + + while(nfc_magic_worker->state == NfcMagicWorkerStateWipe) { + magic_deactivate(); + furi_delay_ms(300); + if(!magic_wupa()) continue; + if(!magic_wipe()) continue; + if(!magic_data_access_cmd()) continue; + if(!magic_write_blk(0, &block)) continue; + nfc_magic_worker->callback(NfcMagicWorkerEventSuccess, nfc_magic_worker->context); + magic_deactivate(); + break; + } + magic_deactivate(); +} diff --git a/applications/plugins/nfc_magic/nfc_magic_worker.h b/applications/plugins/nfc_magic/nfc_magic_worker.h new file mode 100644 index 000000000..9d29bb3a8 --- /dev/null +++ b/applications/plugins/nfc_magic/nfc_magic_worker.h @@ -0,0 +1,38 @@ +#pragma once + +#include + +typedef struct NfcMagicWorker NfcMagicWorker; + +typedef enum { + NfcMagicWorkerStateReady, + + NfcMagicWorkerStateCheck, + NfcMagicWorkerStateWrite, + NfcMagicWorkerStateWipe, + + NfcMagicWorkerStateStop, +} NfcMagicWorkerState; + +typedef enum { + NfcMagicWorkerEventSuccess, + NfcMagicWorkerEventFail, + NfcMagicWorkerEventCardDetected, + NfcMagicWorkerEventNoCardDetected, + NfcMagicWorkerEventWrongCard, +} NfcMagicWorkerEvent; + +typedef bool (*NfcMagicWorkerCallback)(NfcMagicWorkerEvent event, void* context); + +NfcMagicWorker* nfc_magic_worker_alloc(); + +void nfc_magic_worker_free(NfcMagicWorker* nfc_magic_worker); + +void nfc_magic_worker_stop(NfcMagicWorker* nfc_magic_worker); + +void nfc_magic_worker_start( + NfcMagicWorker* nfc_magic_worker, + NfcMagicWorkerState state, + NfcDeviceData* dev_data, + NfcMagicWorkerCallback callback, + void* context); diff --git a/applications/plugins/nfc_magic/nfc_magic_worker_i.h b/applications/plugins/nfc_magic/nfc_magic_worker_i.h new file mode 100644 index 000000000..0cde2e712 --- /dev/null +++ b/applications/plugins/nfc_magic/nfc_magic_worker_i.h @@ -0,0 +1,24 @@ +#pragma once + +#include + +#include "nfc_magic_worker.h" + +struct NfcMagicWorker { + FuriThread* thread; + + NfcDeviceData* dev_data; + + NfcMagicWorkerCallback callback; + void* context; + + NfcMagicWorkerState state; +}; + +int32_t nfc_magic_worker_task(void* context); + +void nfc_magic_worker_check(NfcMagicWorker* nfc_magic_worker); + +void nfc_magic_worker_write(NfcMagicWorker* nfc_magic_worker); + +void nfc_magic_worker_wipe(NfcMagicWorker* nfc_magic_worker); diff --git a/applications/plugins/nfc_magic/scenes/nfc_magic_scene.c b/applications/plugins/nfc_magic/scenes/nfc_magic_scene.c new file mode 100644 index 000000000..520ef2a9d --- /dev/null +++ b/applications/plugins/nfc_magic/scenes/nfc_magic_scene.c @@ -0,0 +1,30 @@ +#include "nfc_magic_scene.h" + +// Generate scene on_enter handlers array +#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_enter, +void (*const nfc_magic_on_enter_handlers[])(void*) = { +#include "nfc_magic_scene_config.h" +}; +#undef ADD_SCENE + +// Generate scene on_event handlers array +#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_event, +bool (*const nfc_magic_on_event_handlers[])(void* context, SceneManagerEvent event) = { +#include "nfc_magic_scene_config.h" +}; +#undef ADD_SCENE + +// Generate scene on_exit handlers array +#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_exit, +void (*const nfc_magic_on_exit_handlers[])(void* context) = { +#include "nfc_magic_scene_config.h" +}; +#undef ADD_SCENE + +// Initialize scene handlers configuration structure +const SceneManagerHandlers nfc_magic_scene_handlers = { + .on_enter_handlers = nfc_magic_on_enter_handlers, + .on_event_handlers = nfc_magic_on_event_handlers, + .on_exit_handlers = nfc_magic_on_exit_handlers, + .scene_num = NfcMagicSceneNum, +}; diff --git a/applications/plugins/nfc_magic/scenes/nfc_magic_scene.h b/applications/plugins/nfc_magic/scenes/nfc_magic_scene.h new file mode 100644 index 000000000..f1e9f715d --- /dev/null +++ b/applications/plugins/nfc_magic/scenes/nfc_magic_scene.h @@ -0,0 +1,29 @@ +#pragma once + +#include + +// Generate scene id and total number +#define ADD_SCENE(prefix, name, id) NfcMagicScene##id, +typedef enum { +#include "nfc_magic_scene_config.h" + NfcMagicSceneNum, +} NfcMagicScene; +#undef ADD_SCENE + +extern const SceneManagerHandlers nfc_magic_scene_handlers; + +// Generate scene on_enter handlers declaration +#define ADD_SCENE(prefix, name, id) void prefix##_scene_##name##_on_enter(void*); +#include "nfc_magic_scene_config.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 "nfc_magic_scene_config.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 "nfc_magic_scene_config.h" +#undef ADD_SCENE diff --git a/applications/plugins/nfc_magic/scenes/nfc_magic_scene_check.c b/applications/plugins/nfc_magic/scenes/nfc_magic_scene_check.c new file mode 100644 index 000000000..d51797242 --- /dev/null +++ b/applications/plugins/nfc_magic/scenes/nfc_magic_scene_check.c @@ -0,0 +1,87 @@ +#include "../nfc_magic_i.h" + +enum { + NfcMagicSceneCheckStateCardSearch, + NfcMagicSceneCheckStateCardFound, +}; + +bool nfc_magic_check_worker_callback(NfcMagicWorkerEvent event, void* context) { + furi_assert(context); + + NfcMagic* nfc_magic = context; + view_dispatcher_send_custom_event(nfc_magic->view_dispatcher, event); + + return true; +} + +static void nfc_magic_scene_check_setup_view(NfcMagic* nfc_magic) { + Popup* popup = nfc_magic->popup; + popup_reset(popup); + uint32_t state = scene_manager_get_scene_state(nfc_magic->scene_manager, NfcMagicSceneCheck); + + if(state == NfcMagicSceneCheckStateCardSearch) { + popup_set_icon(nfc_magic->popup, 0, 8, &I_NFC_manual_60x50); + popup_set_text( + nfc_magic->popup, "Apply card to\nthe back", 128, 32, AlignRight, AlignCenter); + } else { + popup_set_icon(popup, 12, 23, &I_Loading_24); + popup_set_header(popup, "Checking\nDon't move...", 52, 32, AlignLeft, AlignCenter); + } + + view_dispatcher_switch_to_view(nfc_magic->view_dispatcher, NfcMagicViewPopup); +} + +void nfc_magic_scene_check_on_enter(void* context) { + NfcMagic* nfc_magic = context; + + scene_manager_set_scene_state( + nfc_magic->scene_manager, NfcMagicSceneCheck, NfcMagicSceneCheckStateCardSearch); + nfc_magic_scene_check_setup_view(nfc_magic); + + // Setup and start worker + nfc_magic_worker_start( + nfc_magic->worker, + NfcMagicWorkerStateCheck, + &nfc_magic->nfc_dev->dev_data, + nfc_magic_check_worker_callback, + nfc_magic); + nfc_magic_blink_start(nfc_magic); +} + +bool nfc_magic_scene_check_on_event(void* context, SceneManagerEvent event) { + NfcMagic* nfc_magic = context; + bool consumed = false; + + if(event.type == SceneManagerEventTypeCustom) { + if(event.event == NfcMagicWorkerEventSuccess) { + scene_manager_next_scene(nfc_magic->scene_manager, NfcMagicSceneMagicInfo); + consumed = true; + } else if(event.event == NfcMagicWorkerEventWrongCard) { + scene_manager_next_scene(nfc_magic->scene_manager, NfcMagicSceneNotMagic); + consumed = true; + } else if(event.event == NfcMagicWorkerEventCardDetected) { + scene_manager_set_scene_state( + nfc_magic->scene_manager, NfcMagicSceneCheck, NfcMagicSceneCheckStateCardFound); + nfc_magic_scene_check_setup_view(nfc_magic); + consumed = true; + } else if(event.event == NfcMagicWorkerEventNoCardDetected) { + scene_manager_set_scene_state( + nfc_magic->scene_manager, NfcMagicSceneCheck, NfcMagicSceneCheckStateCardSearch); + nfc_magic_scene_check_setup_view(nfc_magic); + consumed = true; + } + } + return consumed; +} + +void nfc_magic_scene_check_on_exit(void* context) { + NfcMagic* nfc_magic = context; + + nfc_magic_worker_stop(nfc_magic->worker); + scene_manager_set_scene_state( + nfc_magic->scene_manager, NfcMagicSceneCheck, NfcMagicSceneCheckStateCardSearch); + // Clear view + popup_reset(nfc_magic->popup); + + nfc_magic_blink_stop(nfc_magic); +} diff --git a/applications/plugins/nfc_magic/scenes/nfc_magic_scene_config.h b/applications/plugins/nfc_magic/scenes/nfc_magic_scene_config.h new file mode 100644 index 000000000..557e26914 --- /dev/null +++ b/applications/plugins/nfc_magic/scenes/nfc_magic_scene_config.h @@ -0,0 +1,12 @@ +ADD_SCENE(nfc_magic, start, Start) +ADD_SCENE(nfc_magic, file_select, FileSelect) +ADD_SCENE(nfc_magic, write_confirm, WriteConfirm) +ADD_SCENE(nfc_magic, wrong_card, WrongCard) +ADD_SCENE(nfc_magic, write, Write) +ADD_SCENE(nfc_magic, write_fail, WriteFail) +ADD_SCENE(nfc_magic, success, Success) +ADD_SCENE(nfc_magic, check, Check) +ADD_SCENE(nfc_magic, not_magic, NotMagic) +ADD_SCENE(nfc_magic, magic_info, MagicInfo) +ADD_SCENE(nfc_magic, wipe, Wipe) +ADD_SCENE(nfc_magic, wipe_fail, WipeFail) diff --git a/applications/plugins/nfc_magic/scenes/nfc_magic_scene_file_select.c b/applications/plugins/nfc_magic/scenes/nfc_magic_scene_file_select.c new file mode 100644 index 000000000..a19237ed4 --- /dev/null +++ b/applications/plugins/nfc_magic/scenes/nfc_magic_scene_file_select.c @@ -0,0 +1,34 @@ +#include "../nfc_magic_i.h" + +static bool nfc_magic_scene_file_select_is_file_suitable(NfcDevice* nfc_dev) { + return (nfc_dev->format == NfcDeviceSaveFormatMifareClassic) && + (nfc_dev->dev_data.mf_classic_data.type == MfClassicType1k) && + (nfc_dev->dev_data.nfc_data.uid_len == 4); +} + +void nfc_magic_scene_file_select_on_enter(void* context) { + NfcMagic* nfc_magic = context; + // Process file_select return + nfc_device_set_loading_callback(nfc_magic->nfc_dev, nfc_magic_show_loading_popup, nfc_magic); + + if(nfc_file_select(nfc_magic->nfc_dev)) { + if(nfc_magic_scene_file_select_is_file_suitable(nfc_magic->nfc_dev)) { + scene_manager_next_scene(nfc_magic->scene_manager, NfcMagicSceneWriteConfirm); + } else { + scene_manager_next_scene(nfc_magic->scene_manager, NfcMagicSceneWrongCard); + } + } else { + scene_manager_previous_scene(nfc_magic->scene_manager); + } +} + +bool nfc_magic_scene_file_select_on_event(void* context, SceneManagerEvent event) { + UNUSED(context); + UNUSED(event); + return false; +} + +void nfc_magic_scene_file_select_on_exit(void* context) { + NfcMagic* nfc_magic = context; + nfc_device_set_loading_callback(nfc_magic->nfc_dev, NULL, nfc_magic); +} diff --git a/applications/plugins/nfc_magic/scenes/nfc_magic_scene_magic_info.c b/applications/plugins/nfc_magic/scenes/nfc_magic_scene_magic_info.c new file mode 100644 index 000000000..e9b226b3a --- /dev/null +++ b/applications/plugins/nfc_magic/scenes/nfc_magic_scene_magic_info.c @@ -0,0 +1,45 @@ +#include "../nfc_magic_i.h" + +void nfc_magic_scene_magic_info_widget_callback( + GuiButtonType result, + InputType type, + void* context) { + NfcMagic* nfc_magic = context; + if(type == InputTypeShort) { + view_dispatcher_send_custom_event(nfc_magic->view_dispatcher, result); + } +} + +void nfc_magic_scene_magic_info_on_enter(void* context) { + NfcMagic* nfc_magic = context; + Widget* widget = nfc_magic->widget; + + notification_message(nfc_magic->notifications, &sequence_success); + + widget_add_icon_element(widget, 73, 17, &I_DolphinCommon_56x48); + widget_add_string_element( + widget, 3, 4, AlignLeft, AlignTop, FontPrimary, "Magic card detected"); + widget_add_button_element( + widget, GuiButtonTypeLeft, "Retry", nfc_magic_scene_magic_info_widget_callback, nfc_magic); + + // Setup and start worker + view_dispatcher_switch_to_view(nfc_magic->view_dispatcher, NfcMagicViewWidget); +} + +bool nfc_magic_scene_magic_info_on_event(void* context, SceneManagerEvent event) { + NfcMagic* nfc_magic = context; + bool consumed = false; + + if(event.type == SceneManagerEventTypeCustom) { + if(event.event == GuiButtonTypeLeft) { + consumed = scene_manager_previous_scene(nfc_magic->scene_manager); + } + } + return consumed; +} + +void nfc_magic_scene_magic_info_on_exit(void* context) { + NfcMagic* nfc_magic = context; + + widget_reset(nfc_magic->widget); +} diff --git a/applications/plugins/nfc_magic/scenes/nfc_magic_scene_not_magic.c b/applications/plugins/nfc_magic/scenes/nfc_magic_scene_not_magic.c new file mode 100644 index 000000000..b87f7f383 --- /dev/null +++ b/applications/plugins/nfc_magic/scenes/nfc_magic_scene_not_magic.c @@ -0,0 +1,44 @@ +#include "../nfc_magic_i.h" + +void nfc_magic_scene_not_magic_widget_callback(GuiButtonType result, InputType type, void* context) { + NfcMagic* nfc_magic = context; + if(type == InputTypeShort) { + view_dispatcher_send_custom_event(nfc_magic->view_dispatcher, result); + } +} + +void nfc_magic_scene_not_magic_on_enter(void* context) { + NfcMagic* nfc_magic = context; + Widget* widget = nfc_magic->widget; + + notification_message(nfc_magic->notifications, &sequence_error); + + // widget_add_icon_element(widget, 73, 17, &I_DolphinCommon_56x48); + widget_add_string_element( + widget, 3, 4, AlignLeft, AlignTop, FontPrimary, "This is wrong card"); + widget_add_string_multiline_element( + widget, 4, 17, AlignLeft, AlignTop, FontSecondary, "Not a magic\ncard"); + widget_add_button_element( + widget, GuiButtonTypeLeft, "Retry", nfc_magic_scene_not_magic_widget_callback, nfc_magic); + + // Setup and start worker + view_dispatcher_switch_to_view(nfc_magic->view_dispatcher, NfcMagicViewWidget); +} + +bool nfc_magic_scene_not_magic_on_event(void* context, SceneManagerEvent event) { + NfcMagic* nfc_magic = context; + bool consumed = false; + + if(event.type == SceneManagerEventTypeCustom) { + if(event.event == GuiButtonTypeLeft) { + consumed = scene_manager_previous_scene(nfc_magic->scene_manager); + } + } + return consumed; +} + +void nfc_magic_scene_not_magic_on_exit(void* context) { + NfcMagic* nfc_magic = context; + + widget_reset(nfc_magic->widget); +} diff --git a/applications/plugins/nfc_magic/scenes/nfc_magic_scene_start.c b/applications/plugins/nfc_magic/scenes/nfc_magic_scene_start.c new file mode 100644 index 000000000..f2984443f --- /dev/null +++ b/applications/plugins/nfc_magic/scenes/nfc_magic_scene_start.c @@ -0,0 +1,61 @@ +#include "../nfc_magic_i.h" +enum SubmenuIndex { + SubmenuIndexCheck, + SubmenuIndexWriteGen1A, + SubmenuIndexWipe, +}; + +void nfc_magic_scene_start_submenu_callback(void* context, uint32_t index) { + NfcMagic* nfc_magic = context; + view_dispatcher_send_custom_event(nfc_magic->view_dispatcher, index); +} + +void nfc_magic_scene_start_on_enter(void* context) { + NfcMagic* nfc_magic = context; + + Submenu* submenu = nfc_magic->submenu; + submenu_add_item( + submenu, + "Check Magic Tag", + SubmenuIndexCheck, + nfc_magic_scene_start_submenu_callback, + nfc_magic); + submenu_add_item( + submenu, + "Write Gen1A", + SubmenuIndexWriteGen1A, + nfc_magic_scene_start_submenu_callback, + nfc_magic); + submenu_add_item( + submenu, "Wipe", SubmenuIndexWipe, nfc_magic_scene_start_submenu_callback, nfc_magic); + + submenu_set_selected_item( + submenu, scene_manager_get_scene_state(nfc_magic->scene_manager, NfcMagicSceneStart)); + view_dispatcher_switch_to_view(nfc_magic->view_dispatcher, NfcMagicViewMenu); +} + +bool nfc_magic_scene_start_on_event(void* context, SceneManagerEvent event) { + NfcMagic* nfc_magic = context; + bool consumed = false; + + if(event.type == SceneManagerEventTypeCustom) { + if(event.event == SubmenuIndexCheck) { + scene_manager_next_scene(nfc_magic->scene_manager, NfcMagicSceneCheck); + consumed = true; + } else if(event.event == SubmenuIndexWriteGen1A) { + scene_manager_next_scene(nfc_magic->scene_manager, NfcMagicSceneFileSelect); + consumed = true; + } else if(event.event == SubmenuIndexWipe) { + scene_manager_next_scene(nfc_magic->scene_manager, NfcMagicSceneWipe); + consumed = true; + } + scene_manager_set_scene_state(nfc_magic->scene_manager, NfcMagicSceneStart, event.event); + } + + return consumed; +} + +void nfc_magic_scene_start_on_exit(void* context) { + NfcMagic* nfc_magic = context; + submenu_reset(nfc_magic->submenu); +} diff --git a/applications/plugins/nfc_magic/scenes/nfc_magic_scene_success.c b/applications/plugins/nfc_magic/scenes/nfc_magic_scene_success.c new file mode 100644 index 000000000..37441e80e --- /dev/null +++ b/applications/plugins/nfc_magic/scenes/nfc_magic_scene_success.c @@ -0,0 +1,42 @@ +#include "../nfc_magic_i.h" + +void nfc_magic_scene_success_popup_callback(void* context) { + NfcMagic* nfc_magic = context; + view_dispatcher_send_custom_event(nfc_magic->view_dispatcher, NfcMagicCustomEventViewExit); +} + +void nfc_magic_scene_success_on_enter(void* context) { + NfcMagic* nfc_magic = context; + + notification_message(nfc_magic->notifications, &sequence_success); + + Popup* popup = nfc_magic->popup; + popup_set_icon(popup, 32, 5, &I_DolphinNice_96x59); + popup_set_header(popup, "Success!", 10, 20, AlignLeft, AlignBottom); + popup_set_timeout(popup, 1500); + popup_set_context(popup, nfc_magic); + popup_set_callback(popup, nfc_magic_scene_success_popup_callback); + popup_enable_timeout(popup); + + view_dispatcher_switch_to_view(nfc_magic->view_dispatcher, NfcMagicViewPopup); +} + +bool nfc_magic_scene_success_on_event(void* context, SceneManagerEvent event) { + NfcMagic* nfc_magic = context; + bool consumed = false; + + if(event.type == SceneManagerEventTypeCustom) { + if(event.event == NfcMagicCustomEventViewExit) { + consumed = scene_manager_search_and_switch_to_previous_scene( + nfc_magic->scene_manager, NfcMagicSceneStart); + } + } + return consumed; +} + +void nfc_magic_scene_success_on_exit(void* context) { + NfcMagic* nfc_magic = context; + + // Clear view + popup_reset(nfc_magic->popup); +} diff --git a/applications/plugins/nfc_magic/scenes/nfc_magic_scene_wipe.c b/applications/plugins/nfc_magic/scenes/nfc_magic_scene_wipe.c new file mode 100644 index 000000000..1ca194286 --- /dev/null +++ b/applications/plugins/nfc_magic/scenes/nfc_magic_scene_wipe.c @@ -0,0 +1,90 @@ +#include "../nfc_magic_i.h" + +enum { + NfcMagicSceneWipeStateCardSearch, + NfcMagicSceneWipeStateCardFound, +}; + +bool nfc_magic_wipe_worker_callback(NfcMagicWorkerEvent event, void* context) { + furi_assert(context); + + NfcMagic* nfc_magic = context; + view_dispatcher_send_custom_event(nfc_magic->view_dispatcher, event); + + return true; +} + +static void nfc_magic_scene_wipe_setup_view(NfcMagic* nfc_magic) { + Popup* popup = nfc_magic->popup; + popup_reset(popup); + uint32_t state = scene_manager_get_scene_state(nfc_magic->scene_manager, NfcMagicSceneWipe); + + if(state == NfcMagicSceneWipeStateCardSearch) { + popup_set_icon(nfc_magic->popup, 0, 8, &I_NFC_manual_60x50); + popup_set_text( + nfc_magic->popup, "Apply card to\nthe back", 128, 32, AlignRight, AlignCenter); + } else { + popup_set_icon(popup, 12, 23, &I_Loading_24); + popup_set_header(popup, "Wiping\nDon't move...", 52, 32, AlignLeft, AlignCenter); + } + + view_dispatcher_switch_to_view(nfc_magic->view_dispatcher, NfcMagicViewPopup); +} + +void nfc_magic_scene_wipe_on_enter(void* context) { + NfcMagic* nfc_magic = context; + + scene_manager_set_scene_state( + nfc_magic->scene_manager, NfcMagicSceneWipe, NfcMagicSceneWipeStateCardSearch); + nfc_magic_scene_wipe_setup_view(nfc_magic); + + // Setup and start worker + nfc_magic_worker_start( + nfc_magic->worker, + NfcMagicWorkerStateWipe, + &nfc_magic->nfc_dev->dev_data, + nfc_magic_wipe_worker_callback, + nfc_magic); + nfc_magic_blink_start(nfc_magic); +} + +bool nfc_magic_scene_wipe_on_event(void* context, SceneManagerEvent event) { + NfcMagic* nfc_magic = context; + bool consumed = false; + + if(event.type == SceneManagerEventTypeCustom) { + if(event.event == NfcMagicWorkerEventSuccess) { + scene_manager_next_scene(nfc_magic->scene_manager, NfcMagicSceneSuccess); + consumed = true; + } else if(event.event == NfcMagicWorkerEventFail) { + scene_manager_next_scene(nfc_magic->scene_manager, NfcMagicSceneWipeFail); + consumed = true; + } else if(event.event == NfcMagicWorkerEventWrongCard) { + scene_manager_next_scene(nfc_magic->scene_manager, NfcMagicSceneNotMagic); + consumed = true; + } else if(event.event == NfcMagicWorkerEventCardDetected) { + scene_manager_set_scene_state( + nfc_magic->scene_manager, NfcMagicSceneWipe, NfcMagicSceneWipeStateCardFound); + nfc_magic_scene_wipe_setup_view(nfc_magic); + consumed = true; + } else if(event.event == NfcMagicWorkerEventNoCardDetected) { + scene_manager_set_scene_state( + nfc_magic->scene_manager, NfcMagicSceneWipe, NfcMagicSceneWipeStateCardSearch); + nfc_magic_scene_wipe_setup_view(nfc_magic); + consumed = true; + } + } + return consumed; +} + +void nfc_magic_scene_wipe_on_exit(void* context) { + NfcMagic* nfc_magic = context; + + nfc_magic_worker_stop(nfc_magic->worker); + scene_manager_set_scene_state( + nfc_magic->scene_manager, NfcMagicSceneWipe, NfcMagicSceneWipeStateCardSearch); + // Clear view + popup_reset(nfc_magic->popup); + + nfc_magic_blink_stop(nfc_magic); +} diff --git a/applications/plugins/nfc_magic/scenes/nfc_magic_scene_wipe_fail.c b/applications/plugins/nfc_magic/scenes/nfc_magic_scene_wipe_fail.c new file mode 100644 index 000000000..828b65e6c --- /dev/null +++ b/applications/plugins/nfc_magic/scenes/nfc_magic_scene_wipe_fail.c @@ -0,0 +1,41 @@ +#include "../nfc_magic_i.h" + +void nfc_magic_scene_wipe_fail_widget_callback(GuiButtonType result, InputType type, void* context) { + NfcMagic* nfc_magic = context; + if(type == InputTypeShort) { + view_dispatcher_send_custom_event(nfc_magic->view_dispatcher, result); + } +} + +void nfc_magic_scene_wipe_fail_on_enter(void* context) { + NfcMagic* nfc_magic = context; + Widget* widget = nfc_magic->widget; + + notification_message(nfc_magic->notifications, &sequence_error); + + widget_add_icon_element(widget, 73, 17, &I_DolphinCommon_56x48); + widget_add_string_element(widget, 3, 4, AlignLeft, AlignTop, FontPrimary, "Wipe failed"); + widget_add_button_element( + widget, GuiButtonTypeLeft, "Retry", nfc_magic_scene_wipe_fail_widget_callback, nfc_magic); + + // Setup and start worker + view_dispatcher_switch_to_view(nfc_magic->view_dispatcher, NfcMagicViewWidget); +} + +bool nfc_magic_scene_wipe_fail_on_event(void* context, SceneManagerEvent event) { + NfcMagic* nfc_magic = context; + bool consumed = false; + + if(event.type == SceneManagerEventTypeCustom) { + if(event.event == GuiButtonTypeLeft) { + consumed = scene_manager_previous_scene(nfc_magic->scene_manager); + } + } + return consumed; +} + +void nfc_magic_scene_wipe_fail_on_exit(void* context) { + NfcMagic* nfc_magic = context; + + widget_reset(nfc_magic->widget); +} diff --git a/applications/plugins/nfc_magic/scenes/nfc_magic_scene_write.c b/applications/plugins/nfc_magic/scenes/nfc_magic_scene_write.c new file mode 100644 index 000000000..c3e6f962a --- /dev/null +++ b/applications/plugins/nfc_magic/scenes/nfc_magic_scene_write.c @@ -0,0 +1,90 @@ +#include "../nfc_magic_i.h" + +enum { + NfcMagicSceneWriteStateCardSearch, + NfcMagicSceneWriteStateCardFound, +}; + +bool nfc_magic_write_worker_callback(NfcMagicWorkerEvent event, void* context) { + furi_assert(context); + + NfcMagic* nfc_magic = context; + view_dispatcher_send_custom_event(nfc_magic->view_dispatcher, event); + + return true; +} + +static void nfc_magic_scene_write_setup_view(NfcMagic* nfc_magic) { + Popup* popup = nfc_magic->popup; + popup_reset(popup); + uint32_t state = scene_manager_get_scene_state(nfc_magic->scene_manager, NfcMagicSceneWrite); + + if(state == NfcMagicSceneWriteStateCardSearch) { + popup_set_text( + nfc_magic->popup, "Apply card to\nthe back", 128, 32, AlignRight, AlignCenter); + popup_set_icon(nfc_magic->popup, 0, 8, &I_NFC_manual_60x50); + } else { + popup_set_icon(popup, 12, 23, &I_Loading_24); + popup_set_header(popup, "Writing\nDon't move...", 52, 32, AlignLeft, AlignCenter); + } + + view_dispatcher_switch_to_view(nfc_magic->view_dispatcher, NfcMagicViewPopup); +} + +void nfc_magic_scene_write_on_enter(void* context) { + NfcMagic* nfc_magic = context; + + scene_manager_set_scene_state( + nfc_magic->scene_manager, NfcMagicSceneWrite, NfcMagicSceneWriteStateCardSearch); + nfc_magic_scene_write_setup_view(nfc_magic); + + // Setup and start worker + nfc_magic_worker_start( + nfc_magic->worker, + NfcMagicWorkerStateWrite, + &nfc_magic->nfc_dev->dev_data, + nfc_magic_write_worker_callback, + nfc_magic); + nfc_magic_blink_start(nfc_magic); +} + +bool nfc_magic_scene_write_on_event(void* context, SceneManagerEvent event) { + NfcMagic* nfc_magic = context; + bool consumed = false; + + if(event.type == SceneManagerEventTypeCustom) { + if(event.event == NfcMagicWorkerEventSuccess) { + scene_manager_next_scene(nfc_magic->scene_manager, NfcMagicSceneSuccess); + consumed = true; + } else if(event.event == NfcMagicWorkerEventFail) { + scene_manager_next_scene(nfc_magic->scene_manager, NfcMagicSceneWriteFail); + consumed = true; + } else if(event.event == NfcMagicWorkerEventWrongCard) { + scene_manager_next_scene(nfc_magic->scene_manager, NfcMagicSceneNotMagic); + consumed = true; + } else if(event.event == NfcMagicWorkerEventCardDetected) { + scene_manager_set_scene_state( + nfc_magic->scene_manager, NfcMagicSceneWrite, NfcMagicSceneWriteStateCardFound); + nfc_magic_scene_write_setup_view(nfc_magic); + consumed = true; + } else if(event.event == NfcMagicWorkerEventNoCardDetected) { + scene_manager_set_scene_state( + nfc_magic->scene_manager, NfcMagicSceneWrite, NfcMagicSceneWriteStateCardSearch); + nfc_magic_scene_write_setup_view(nfc_magic); + consumed = true; + } + } + return consumed; +} + +void nfc_magic_scene_write_on_exit(void* context) { + NfcMagic* nfc_magic = context; + + nfc_magic_worker_stop(nfc_magic->worker); + scene_manager_set_scene_state( + nfc_magic->scene_manager, NfcMagicSceneWrite, NfcMagicSceneWriteStateCardSearch); + // Clear view + popup_reset(nfc_magic->popup); + + nfc_magic_blink_stop(nfc_magic); +} diff --git a/applications/plugins/nfc_magic/scenes/nfc_magic_scene_write_confirm.c b/applications/plugins/nfc_magic/scenes/nfc_magic_scene_write_confirm.c new file mode 100644 index 000000000..d31c1c194 --- /dev/null +++ b/applications/plugins/nfc_magic/scenes/nfc_magic_scene_write_confirm.c @@ -0,0 +1,64 @@ +#include "../nfc_magic_i.h" + +void nfc_magic_scene_write_confirm_widget_callback( + GuiButtonType result, + InputType type, + void* context) { + NfcMagic* nfc_magic = context; + if(type == InputTypeShort) { + view_dispatcher_send_custom_event(nfc_magic->view_dispatcher, result); + } +} + +void nfc_magic_scene_write_confirm_on_enter(void* context) { + NfcMagic* nfc_magic = context; + Widget* widget = nfc_magic->widget; + + widget_add_string_element(widget, 3, 0, AlignLeft, AlignTop, FontPrimary, "Risky operation"); + widget_add_text_box_element( + widget, + 0, + 13, + 128, + 54, + AlignLeft, + AlignTop, + "Writing to this card will change manufacturer block. On some cards it may not be rewritten", + false); + widget_add_button_element( + widget, + GuiButtonTypeCenter, + "Continue", + nfc_magic_scene_write_confirm_widget_callback, + nfc_magic); + widget_add_button_element( + widget, + GuiButtonTypeLeft, + "Back", + nfc_magic_scene_write_confirm_widget_callback, + nfc_magic); + + // Setup and start worker + view_dispatcher_switch_to_view(nfc_magic->view_dispatcher, NfcMagicViewWidget); +} + +bool nfc_magic_scene_write_confirm_on_event(void* context, SceneManagerEvent event) { + NfcMagic* nfc_magic = context; + bool consumed = false; + + if(event.type == SceneManagerEventTypeCustom) { + if(event.event == GuiButtonTypeLeft) { + consumed = scene_manager_previous_scene(nfc_magic->scene_manager); + } else if(event.event == GuiButtonTypeCenter) { + scene_manager_next_scene(nfc_magic->scene_manager, NfcMagicSceneWrite); + consumed = true; + } + } + return consumed; +} + +void nfc_magic_scene_write_confirm_on_exit(void* context) { + NfcMagic* nfc_magic = context; + + widget_reset(nfc_magic->widget); +} diff --git a/applications/plugins/nfc_magic/scenes/nfc_magic_scene_write_fail.c b/applications/plugins/nfc_magic/scenes/nfc_magic_scene_write_fail.c new file mode 100644 index 000000000..8a465bf61 --- /dev/null +++ b/applications/plugins/nfc_magic/scenes/nfc_magic_scene_write_fail.c @@ -0,0 +1,58 @@ +#include "../nfc_magic_i.h" + +void nfc_magic_scene_write_fail_widget_callback( + GuiButtonType result, + InputType type, + void* context) { + NfcMagic* nfc_magic = context; + if(type == InputTypeShort) { + view_dispatcher_send_custom_event(nfc_magic->view_dispatcher, result); + } +} + +void nfc_magic_scene_write_fail_on_enter(void* context) { + NfcMagic* nfc_magic = context; + Widget* widget = nfc_magic->widget; + + notification_message(nfc_magic->notifications, &sequence_error); + + widget_add_icon_element(widget, 72, 17, &I_DolphinCommon_56x48); + widget_add_string_element( + widget, 7, 4, AlignLeft, AlignTop, FontPrimary, "Writing gone wrong!"); + widget_add_string_multiline_element( + widget, + 7, + 17, + AlignLeft, + AlignTop, + FontSecondary, + "Not all sectors\nwere written\ncorrectly."); + + widget_add_button_element( + widget, GuiButtonTypeLeft, "Finish", nfc_magic_scene_write_fail_widget_callback, nfc_magic); + + // Setup and start worker + view_dispatcher_switch_to_view(nfc_magic->view_dispatcher, NfcMagicViewWidget); +} + +bool nfc_magic_scene_write_fail_on_event(void* context, SceneManagerEvent event) { + NfcMagic* nfc_magic = context; + bool consumed = false; + + if(event.type == SceneManagerEventTypeCustom) { + if(event.event == GuiButtonTypeLeft) { + consumed = scene_manager_search_and_switch_to_previous_scene( + nfc_magic->scene_manager, NfcMagicSceneStart); + } + } else if(event.type == SceneManagerEventTypeBack) { + consumed = scene_manager_search_and_switch_to_previous_scene( + nfc_magic->scene_manager, NfcMagicSceneStart); + } + return consumed; +} + +void nfc_magic_scene_write_fail_on_exit(void* context) { + NfcMagic* nfc_magic = context; + + widget_reset(nfc_magic->widget); +} diff --git a/applications/plugins/nfc_magic/scenes/nfc_magic_scene_wrong_card.c b/applications/plugins/nfc_magic/scenes/nfc_magic_scene_wrong_card.c new file mode 100644 index 000000000..69bf9eb50 --- /dev/null +++ b/applications/plugins/nfc_magic/scenes/nfc_magic_scene_wrong_card.c @@ -0,0 +1,53 @@ +#include "../nfc_magic_i.h" + +void nfc_magic_scene_wrong_card_widget_callback( + GuiButtonType result, + InputType type, + void* context) { + NfcMagic* nfc_magic = context; + if(type == InputTypeShort) { + view_dispatcher_send_custom_event(nfc_magic->view_dispatcher, result); + } +} + +void nfc_magic_scene_wrong_card_on_enter(void* context) { + NfcMagic* nfc_magic = context; + Widget* widget = nfc_magic->widget; + + notification_message(nfc_magic->notifications, &sequence_error); + + widget_add_icon_element(widget, 73, 17, &I_DolphinCommon_56x48); + widget_add_string_element( + widget, 1, 4, AlignLeft, AlignTop, FontPrimary, "This is wrong card"); + widget_add_string_multiline_element( + widget, + 1, + 17, + AlignLeft, + AlignTop, + FontSecondary, + "Writing is supported\nonly for 4 bytes UID\nMifare CLassic 1k"); + widget_add_button_element( + widget, GuiButtonTypeLeft, "Retry", nfc_magic_scene_wrong_card_widget_callback, nfc_magic); + + // Setup and start worker + view_dispatcher_switch_to_view(nfc_magic->view_dispatcher, NfcMagicViewWidget); +} + +bool nfc_magic_scene_wrong_card_on_event(void* context, SceneManagerEvent event) { + NfcMagic* nfc_magic = context; + bool consumed = false; + + if(event.type == SceneManagerEventTypeCustom) { + if(event.event == GuiButtonTypeLeft) { + consumed = scene_manager_previous_scene(nfc_magic->scene_manager); + } + } + return consumed; +} + +void nfc_magic_scene_wrong_card_on_exit(void* context) { + NfcMagic* nfc_magic = context; + + widget_reset(nfc_magic->widget); +} diff --git a/firmware/targets/f7/api_symbols.csv b/firmware/targets/f7/api_symbols.csv index 65e6f4857..0c10258f7 100644 --- a/firmware/targets/f7/api_symbols.csv +++ b/firmware/targets/f7/api_symbols.csv @@ -148,6 +148,7 @@ Header,+,lib/libusb_stm32/inc/usbd_core.h,, Header,+,lib/mbedtls/include/mbedtls/des.h,, Header,+,lib/mbedtls/include/mbedtls/sha1.h,, Header,+,lib/micro-ecc/uECC.h,, +Header,+,lib/nfc/nfc_device.h,, Header,+,lib/one_wire/ibutton/ibutton_worker.h,, Header,+,lib/one_wire/maxim_crc.h,, Header,+,lib/one_wire/one_wire_device.h,, @@ -668,6 +669,14 @@ Function,-,coshl,long double,long double Function,-,cosl,long double,long double Function,+,crc32_calc_buffer,uint32_t,"uint32_t, const void*, size_t" Function,+,crc32_calc_file,uint32_t,"File*, const FileCrcProgressCb, void*" +Function,-,crypto1_bit,uint8_t,"Crypto1*, uint8_t, int" +Function,-,crypto1_byte,uint8_t,"Crypto1*, uint8_t, int" +Function,-,crypto1_decrypt,void,"Crypto1*, uint8_t*, uint16_t, uint8_t*" +Function,-,crypto1_encrypt,void,"Crypto1*, uint8_t*, uint8_t*, uint16_t, uint8_t*, uint8_t*" +Function,-,crypto1_filter,uint32_t,uint32_t +Function,-,crypto1_init,void,"Crypto1*, uint64_t" +Function,-,crypto1_reset,void,Crypto1* +Function,-,crypto1_word,uint32_t,"Crypto1*, uint32_t, int" Function,-,ctermid,char*,char* Function,-,ctime,char*,const time_t* Function,-,ctime_r,char*,"const time_t*, char*" @@ -750,6 +759,8 @@ Function,+,elements_text_box,void,"Canvas*, uint8_t, uint8_t, uint8_t, uint8_t, Function,+,empty_screen_alloc,EmptyScreen*, Function,+,empty_screen_free,void,EmptyScreen* Function,+,empty_screen_get_view,View*,EmptyScreen* +Function,-,emv_card_emulation,_Bool,FuriHalNfcTxRxContext* +Function,-,emv_read_bank_card,_Bool,"FuriHalNfcTxRxContext*, EmvApplication*" Function,-,erand48,double,unsigned short[3] Function,-,erf,double,double Function,-,erfc,double,double @@ -1161,6 +1172,7 @@ Function,+,furi_hal_nfc_ll_set_fdt_poll,void,uint32_t Function,+,furi_hal_nfc_ll_set_guard_time,void,uint32_t Function,+,furi_hal_nfc_ll_set_mode,FuriHalNfcReturn,"FuriHalNfcMode, FuriHalNfcBitrate, FuriHalNfcBitrate" Function,+,furi_hal_nfc_ll_txrx,FuriHalNfcReturn,"uint8_t*, uint16_t, uint8_t*, uint16_t, uint16_t*, uint32_t, uint32_t" +Function,+,furi_hal_nfc_ll_txrx_bits,FuriHalNfcReturn,"uint8_t*, uint16_t, uint8_t*, uint16_t, uint16_t*, uint32_t, uint32_t" Function,+,furi_hal_nfc_ll_txrx_off,void, Function,+,furi_hal_nfc_ll_txrx_on,void, Function,+,furi_hal_nfc_sleep,void, @@ -1806,6 +1818,100 @@ Function,+,menu_free,void,Menu* Function,+,menu_get_view,View*,Menu* Function,+,menu_reset,void,Menu* Function,+,menu_set_selected_item,void,"Menu*, uint32_t" +Function,-,mf_classic_auth_attempt,_Bool,"FuriHalNfcTxRxContext*, MfClassicAuthContext*, uint64_t" +Function,-,mf_classic_auth_init_context,void,"MfClassicAuthContext*, uint8_t" +Function,-,mf_classic_authenticate,_Bool,"FuriHalNfcTxRxContext*, uint8_t, uint64_t, MfClassicKey" +Function,-,mf_classic_check_card_type,_Bool,"uint8_t, uint8_t, uint8_t" +Function,-,mf_classic_dict_add_key,_Bool,"MfClassicDict*, uint8_t*" +Function,-,mf_classic_dict_add_key_str,_Bool,"MfClassicDict*, FuriString*" +Function,-,mf_classic_dict_alloc,MfClassicDict*,MfClassicDictType +Function,-,mf_classic_dict_check_presence,_Bool,MfClassicDictType +Function,-,mf_classic_dict_delete_index,_Bool,"MfClassicDict*, uint32_t" +Function,-,mf_classic_dict_find_index,_Bool,"MfClassicDict*, uint8_t*, uint32_t*" +Function,-,mf_classic_dict_find_index_str,_Bool,"MfClassicDict*, FuriString*, uint32_t*" +Function,-,mf_classic_dict_free,void,MfClassicDict* +Function,-,mf_classic_dict_get_key_at_index,_Bool,"MfClassicDict*, uint64_t*, uint32_t" +Function,-,mf_classic_dict_get_key_at_index_str,_Bool,"MfClassicDict*, FuriString*, uint32_t" +Function,-,mf_classic_dict_get_next_key,_Bool,"MfClassicDict*, uint64_t*" +Function,-,mf_classic_dict_get_next_key_str,_Bool,"MfClassicDict*, FuriString*" +Function,-,mf_classic_dict_get_total_keys,uint32_t,MfClassicDict* +Function,-,mf_classic_dict_is_key_present,_Bool,"MfClassicDict*, uint8_t*" +Function,-,mf_classic_dict_is_key_present_str,_Bool,"MfClassicDict*, FuriString*" +Function,-,mf_classic_dict_rewind,_Bool,MfClassicDict* +Function,-,mf_classic_emulator,_Bool,"MfClassicEmulator*, FuriHalNfcTxRxContext*" +Function,-,mf_classic_get_classic_type,MfClassicType,"int8_t, uint8_t, uint8_t" +Function,-,mf_classic_get_read_sectors_and_keys,void,"MfClassicData*, uint8_t*, uint8_t*" +Function,-,mf_classic_get_sector_by_block,uint8_t,uint8_t +Function,-,mf_classic_get_sector_trailer_block_num_by_sector,uint8_t,uint8_t +Function,-,mf_classic_get_sector_trailer_by_sector,MfClassicSectorTrailer*,"MfClassicData*, uint8_t" +Function,-,mf_classic_get_total_sectors_num,uint8_t,MfClassicType +Function,-,mf_classic_get_type_str,const char*,MfClassicType +Function,-,mf_classic_is_allowed_access_data_block,_Bool,"MfClassicData*, uint8_t, MfClassicKey, MfClassicAction" +Function,-,mf_classic_is_allowed_access_sector_trailer,_Bool,"MfClassicData*, uint8_t, MfClassicKey, MfClassicAction" +Function,-,mf_classic_is_block_read,_Bool,"MfClassicData*, uint8_t" +Function,-,mf_classic_is_card_read,_Bool,MfClassicData* +Function,-,mf_classic_is_key_found,_Bool,"MfClassicData*, uint8_t, MfClassicKey" +Function,-,mf_classic_is_sector_data_read,_Bool,"MfClassicData*, uint8_t" +Function,-,mf_classic_is_sector_read,_Bool,"MfClassicData*, uint8_t" +Function,-,mf_classic_is_sector_trailer,_Bool,uint8_t +Function,-,mf_classic_read_card,uint8_t,"FuriHalNfcTxRxContext*, MfClassicReader*, MfClassicData*" +Function,-,mf_classic_read_sector,void,"FuriHalNfcTxRxContext*, MfClassicData*, uint8_t" +Function,-,mf_classic_reader_add_sector,void,"MfClassicReader*, uint8_t, uint64_t, uint64_t" +Function,-,mf_classic_set_block_read,void,"MfClassicData*, uint8_t, MfClassicBlock*" +Function,-,mf_classic_set_key_found,void,"MfClassicData*, uint8_t, MfClassicKey, uint64_t" +Function,-,mf_classic_set_key_not_found,void,"MfClassicData*, uint8_t, MfClassicKey" +Function,-,mf_classic_set_sector_data_not_read,void,MfClassicData* +Function,-,mf_classic_update_card,uint8_t,"FuriHalNfcTxRxContext*, MfClassicData*" +Function,-,mf_classic_write_block,_Bool,"FuriHalNfcTxRxContext*, MfClassicBlock*, uint8_t, MfClassicKey, uint64_t" +Function,-,mf_classic_write_sector,_Bool,"FuriHalNfcTxRxContext*, MfClassicData*, MfClassicData*, uint8_t" +Function,-,mf_df_cat_application,void,"MifareDesfireApplication*, FuriString*" +Function,-,mf_df_cat_application_info,void,"MifareDesfireApplication*, FuriString*" +Function,-,mf_df_cat_card_info,void,"MifareDesfireData*, FuriString*" +Function,-,mf_df_cat_data,void,"MifareDesfireData*, FuriString*" +Function,-,mf_df_cat_file,void,"MifareDesfireFile*, FuriString*" +Function,-,mf_df_cat_free_mem,void,"MifareDesfireFreeMemory*, FuriString*" +Function,-,mf_df_cat_key_settings,void,"MifareDesfireKeySettings*, FuriString*" +Function,-,mf_df_cat_version,void,"MifareDesfireVersion*, FuriString*" +Function,-,mf_df_check_card_type,_Bool,"uint8_t, uint8_t, uint8_t" +Function,-,mf_df_clear,void,MifareDesfireData* +Function,-,mf_df_parse_get_application_ids_response,_Bool,"uint8_t*, uint16_t, MifareDesfireApplication**" +Function,-,mf_df_parse_get_file_ids_response,_Bool,"uint8_t*, uint16_t, MifareDesfireFile**" +Function,-,mf_df_parse_get_file_settings_response,_Bool,"uint8_t*, uint16_t, MifareDesfireFile*" +Function,-,mf_df_parse_get_free_memory_response,_Bool,"uint8_t*, uint16_t, MifareDesfireFreeMemory*" +Function,-,mf_df_parse_get_key_settings_response,_Bool,"uint8_t*, uint16_t, MifareDesfireKeySettings*" +Function,-,mf_df_parse_get_key_version_response,_Bool,"uint8_t*, uint16_t, MifareDesfireKeyVersion*" +Function,-,mf_df_parse_get_version_response,_Bool,"uint8_t*, uint16_t, MifareDesfireVersion*" +Function,-,mf_df_parse_read_data_response,_Bool,"uint8_t*, uint16_t, MifareDesfireFile*" +Function,-,mf_df_parse_select_application_response,_Bool,"uint8_t*, uint16_t" +Function,-,mf_df_prepare_get_application_ids,uint16_t,uint8_t* +Function,-,mf_df_prepare_get_file_ids,uint16_t,uint8_t* +Function,-,mf_df_prepare_get_file_settings,uint16_t,"uint8_t*, uint8_t" +Function,-,mf_df_prepare_get_free_memory,uint16_t,uint8_t* +Function,-,mf_df_prepare_get_key_settings,uint16_t,uint8_t* +Function,-,mf_df_prepare_get_key_version,uint16_t,"uint8_t*, uint8_t" +Function,-,mf_df_prepare_get_value,uint16_t,"uint8_t*, uint8_t" +Function,-,mf_df_prepare_get_version,uint16_t,uint8_t* +Function,-,mf_df_prepare_read_data,uint16_t,"uint8_t*, uint8_t, uint32_t, uint32_t" +Function,-,mf_df_prepare_read_records,uint16_t,"uint8_t*, uint8_t, uint32_t, uint32_t" +Function,-,mf_df_prepare_select_application,uint16_t,"uint8_t*, uint8_t[3]" +Function,-,mf_df_read_card,_Bool,"FuriHalNfcTxRxContext*, MifareDesfireData*" +Function,-,mf_ul_check_card_type,_Bool,"uint8_t, uint8_t, uint8_t" +Function,-,mf_ul_prepare_emulation,void,"MfUltralightEmulator*, MfUltralightData*" +Function,-,mf_ul_prepare_emulation_response,_Bool,"uint8_t*, uint16_t, uint8_t*, uint16_t*, uint32_t*, void*" +Function,-,mf_ul_pwdgen_amiibo,uint32_t,FuriHalNfcDevData* +Function,-,mf_ul_pwdgen_xiaomi,uint32_t,FuriHalNfcDevData* +Function,-,mf_ul_read_card,_Bool,"FuriHalNfcTxRxContext*, MfUltralightReader*, MfUltralightData*" +Function,-,mf_ul_reset,void,MfUltralightData* +Function,-,mf_ul_reset_emulation,void,"MfUltralightEmulator*, _Bool" +Function,-,mf_ultralight_authenticate,_Bool,"FuriHalNfcTxRxContext*, uint32_t, uint16_t*" +Function,-,mf_ultralight_fast_read_pages,_Bool,"FuriHalNfcTxRxContext*, MfUltralightReader*, MfUltralightData*" +Function,-,mf_ultralight_get_config_pages,MfUltralightConfigPages*,MfUltralightData* +Function,-,mf_ultralight_read_counters,_Bool,"FuriHalNfcTxRxContext*, MfUltralightData*" +Function,-,mf_ultralight_read_pages,_Bool,"FuriHalNfcTxRxContext*, MfUltralightReader*, MfUltralightData*" +Function,-,mf_ultralight_read_pages_direct,_Bool,"FuriHalNfcTxRxContext*, uint8_t, uint8_t*" +Function,-,mf_ultralight_read_signature,_Bool,"FuriHalNfcTxRxContext*, MfUltralightData*" +Function,-,mf_ultralight_read_tearing_flags,_Bool,"FuriHalNfcTxRxContext*, MfUltralightData*" +Function,-,mf_ultralight_read_version,_Bool,"FuriHalNfcTxRxContext*, MfUltralightReader*, MfUltralightData*" Function,-,mkdtemp,char*,char* Function,-,mkostemp,int,"char*, int" Function,-,mkostemps,int,"char*, int, int" @@ -1829,6 +1935,19 @@ Function,-,nextafterl,long double,"long double, long double" Function,-,nexttoward,double,"double, long double" Function,-,nexttowardf,float,"float, long double" Function,-,nexttowardl,long double,"long double, long double" +Function,+,nfc_device_alloc,NfcDevice*, +Function,+,nfc_device_clear,void,NfcDevice* +Function,+,nfc_device_data_clear,void,NfcDeviceData* +Function,+,nfc_device_delete,_Bool,"NfcDevice*, _Bool" +Function,+,nfc_device_free,void,NfcDevice* +Function,+,nfc_device_load,_Bool,"NfcDevice*, const char*, _Bool" +Function,+,nfc_device_load_key_cache,_Bool,NfcDevice* +Function,+,nfc_device_restore,_Bool,"NfcDevice*, _Bool" +Function,+,nfc_device_save,_Bool,"NfcDevice*, const char*" +Function,+,nfc_device_save_shadow,_Bool,"NfcDevice*, const char*" +Function,+,nfc_device_set_loading_callback,void,"NfcDevice*, NfcLoadingCallback, void*" +Function,+,nfc_device_set_name,void,"NfcDevice*, const char*" +Function,+,nfc_file_select,_Bool,NfcDevice* Function,-,nfca_append_crc16,void,"uint8_t*, uint16_t" Function,-,nfca_emulation_handler,_Bool,"uint8_t*, uint16_t, uint8_t*, uint16_t*" Function,-,nfca_get_crc16,uint16_t,"uint8_t*, uint16_t" @@ -1913,6 +2032,7 @@ Function,+,power_reboot,void,PowerBootMode Function,+,powf,float,"float, float" Function,-,powl,long double,"long double, long double" Function,-,printf,int,"const char*, ..." +Function,-,prng_successor,uint32_t,"uint32_t, uint32_t" Function,+,protocol_dict_alloc,ProtocolDict*,"const ProtocolBase**, 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" @@ -2129,6 +2249,7 @@ Function,-,rfalT1TPollerRall,ReturnCode,"const uint8_t*, uint8_t*, uint16_t, uin Function,-,rfalT1TPollerRid,ReturnCode,rfalT1TRidRes* Function,-,rfalT1TPollerWrite,ReturnCode,"const uint8_t*, uint8_t, uint8_t" Function,-,rfalTransceiveBitsBlockingTx,ReturnCode,"uint8_t*, uint16_t, uint8_t*, uint16_t, uint16_t*, uint32_t, uint32_t" +Function,-,rfalTransceiveBitsBlockingTxRx,ReturnCode,"uint8_t*, uint16_t, uint8_t*, uint16_t, uint16_t*, uint32_t, uint32_t" Function,-,rfalTransceiveBlockingRx,ReturnCode, Function,-,rfalTransceiveBlockingTx,ReturnCode,"uint8_t*, uint16_t, uint8_t*, uint16_t, uint16_t*, uint32_t, uint32_t" Function,-,rfalTransceiveBlockingTxRx,ReturnCode,"uint8_t*, uint16_t, uint8_t*, uint16_t, uint16_t*, uint32_t, uint32_t" diff --git a/firmware/targets/f7/furi_hal/furi_hal_nfc.c b/firmware/targets/f7/furi_hal/furi_hal_nfc.c index 3ebf4f82b..2d27313ae 100644 --- a/firmware/targets/f7/furi_hal/furi_hal_nfc.c +++ b/firmware/targets/f7/furi_hal/furi_hal_nfc.c @@ -786,6 +786,17 @@ FuriHalNfcReturn furi_hal_nfc_ll_txrx( return rfalTransceiveBlockingTxRx(txBuf, txBufLen, rxBuf, rxBufLen, actLen, flags, fwt); } +FuriHalNfcReturn furi_hal_nfc_ll_txrx_bits( + uint8_t* txBuf, + uint16_t txBufLen, + uint8_t* rxBuf, + uint16_t rxBufLen, + uint16_t* actLen, + uint32_t flags, + uint32_t fwt) { + return rfalTransceiveBitsBlockingTxRx(txBuf, txBufLen, rxBuf, rxBufLen, actLen, flags, fwt); +} + void furi_hal_nfc_ll_poll() { rfalWorker(); } \ No newline at end of file diff --git a/firmware/targets/furi_hal_include/furi_hal_nfc.h b/firmware/targets/furi_hal_include/furi_hal_nfc.h index 90d968fea..d3f6de602 100644 --- a/firmware/targets/furi_hal_include/furi_hal_nfc.h +++ b/firmware/targets/furi_hal_include/furi_hal_nfc.h @@ -398,6 +398,7 @@ void furi_hal_nfc_ll_txrx_on(); void furi_hal_nfc_ll_txrx_off(); +// TODO rework all pollers with furi_hal_nfc_ll_txrx_bits FuriHalNfcReturn furi_hal_nfc_ll_txrx( uint8_t* txBuf, uint16_t txBufLen, @@ -407,6 +408,15 @@ FuriHalNfcReturn furi_hal_nfc_ll_txrx( uint32_t flags, uint32_t fwt); +FuriHalNfcReturn furi_hal_nfc_ll_txrx_bits( + uint8_t* txBuf, + uint16_t txBufLen, + uint8_t* rxBuf, + uint16_t rxBufLen, + uint16_t* actLen, + uint32_t flags, + uint32_t fwt); + void furi_hal_nfc_ll_poll(); #ifdef __cplusplus diff --git a/lib/ST25RFAL002/include/rfal_rf.h b/lib/ST25RFAL002/include/rfal_rf.h index e1b864830..35e9a4454 100644 --- a/lib/ST25RFAL002/include/rfal_rf.h +++ b/lib/ST25RFAL002/include/rfal_rf.h @@ -1496,6 +1496,15 @@ ReturnCode rfalTransceiveBlockingTxRx( uint32_t flags, uint32_t fwt); +ReturnCode rfalTransceiveBitsBlockingTxRx( + uint8_t* txBuf, + uint16_t txBufLen, + uint8_t* rxBuf, + uint16_t rxBufLen, + uint16_t* actLen, + uint32_t flags, + uint32_t fwt); + ReturnCode rfalTransceiveBitsBlockingTx( uint8_t* txBuf, uint16_t txBufLen, diff --git a/lib/ST25RFAL002/source/st25r3916/rfal_rfst25r3916.c b/lib/ST25RFAL002/source/st25r3916/rfal_rfst25r3916.c index 9ad35bcb6..0bad67a6d 100644 --- a/lib/ST25RFAL002/source/st25r3916/rfal_rfst25r3916.c +++ b/lib/ST25RFAL002/source/st25r3916/rfal_rfst25r3916.c @@ -1607,6 +1607,23 @@ ReturnCode rfalTransceiveBlockingTxRx( return ret; } +ReturnCode rfalTransceiveBitsBlockingTxRx( + uint8_t* txBuf, + uint16_t txBufLen, + uint8_t* rxBuf, + uint16_t rxBufLen, + uint16_t* actLen, + uint32_t flags, + uint32_t fwt) { + ReturnCode ret; + + EXIT_ON_ERR( + ret, rfalTransceiveBitsBlockingTx(txBuf, txBufLen, rxBuf, rxBufLen, actLen, flags, fwt)); + ret = rfalTransceiveBlockingRx(); + + return ret; +} + /*******************************************************************************/ static ReturnCode rfalRunTransceiveWorker(void) { if(gRFAL.state == RFAL_STATE_TXRX) { diff --git a/lib/nfc/SConscript b/lib/nfc/SConscript index 657f3a9e5..c6b70a677 100644 --- a/lib/nfc/SConscript +++ b/lib/nfc/SConscript @@ -4,6 +4,9 @@ env.Append( CPPPATH=[ "#/lib/nfc", ], + SDK_HEADERS=[ + File("#/lib/nfc/nfc_device.h"), + ], ) libenv = env.Clone(FW_LIB_NAME="nfc") diff --git a/lib/nfc/nfc_device.h b/lib/nfc/nfc_device.h index 6cac72c6b..c8e8517ae 100644 --- a/lib/nfc/nfc_device.h +++ b/lib/nfc/nfc_device.h @@ -12,6 +12,10 @@ #include #include +#ifdef __cplusplus +extern "C" { +#endif + #define NFC_DEV_NAME_MAX_LEN 22 #define NFC_READER_DATA_MAX_SIZE 64 #define NFC_DICT_KEY_BATCH_SIZE 50 @@ -101,3 +105,7 @@ bool nfc_device_delete(NfcDevice* dev, bool use_load_path); bool nfc_device_restore(NfcDevice* dev, bool use_load_path); void nfc_device_set_loading_callback(NfcDevice* dev, NfcLoadingCallback callback, void* context); + +#ifdef __cplusplus +} +#endif From 04e50c9f893db9ea1fd4517a0a2c81c5efcd2be8 Mon Sep 17 00:00:00 2001 From: hedger Date: Sat, 5 Nov 2022 15:47:59 +0400 Subject: [PATCH 25/49] fbt: fixes for ufbt pt3 (#1970) * fbt: replaced debug dir paths with FBT_DEBUG_DIR * scripts: updated requirements.txt * fbt: fixed wrong import * fbt: removed delayed import for file2image * fbt: added UPDATE_BUNDLE_DIR internal var * fbt: cleaner internal management of extapps * applications: added fap_libs for core apps to link with resources when building with --extra-ext-apps * fbt: removed deprecation stub for faps * fbt: added quotation for icons build cmd * fbt: reworked BUILD_DIR & fap work dir handling; fap debug: using debug elf path from fbt * fbt: explicit LIB_DIST_DIR --- SConstruct | 30 ++++--- applications/main/bad_usb/application.fam | 1 + applications/main/gpio/application.fam | 1 + applications/main/ibutton/application.fam | 1 + applications/main/infrared/application.fam | 1 + applications/main/lfrfid/application.fam | 1 + applications/main/u2f/application.fam | 1 + debug/flipperapps.py | 46 ++++++++--- fbt_options.py | 4 +- firmware.scons | 13 ++-- scripts/fbt/elfmanifest.py | 3 +- scripts/fbt/sdk/cache.py | 3 + scripts/fbt_tools/fbt_assets.py | 2 +- scripts/fbt_tools/fbt_debugopts.py | 16 ++-- scripts/fbt_tools/fbt_dist.py | 4 +- scripts/fbt_tools/fbt_extapps.py | 91 +++++++++++++--------- scripts/requirements.txt | 10 ++- site_scons/commandline.scons | 2 +- site_scons/environ.scons | 2 +- site_scons/extapps.scons | 73 +++++++---------- 20 files changed, 179 insertions(+), 126 deletions(-) diff --git a/SConstruct b/SConstruct index 13a698c81..67eac3825 100644 --- a/SConstruct +++ b/SConstruct @@ -43,6 +43,7 @@ distenv = coreenv.Clone( "jflash", ], ENV=os.environ, + UPDATE_BUNDLE_DIR="dist/${DIST_DIR}/f${TARGET_HW}-update-${DIST_SUFFIX}", ) firmware_env = distenv.AddFwProject( @@ -140,21 +141,28 @@ distenv.Default(basic_dist) dist_dir = distenv.GetProjetDirName() fap_dist = [ distenv.Install( - f"#/dist/{dist_dir}/apps/debug_elf", - firmware_env["FW_EXTAPPS"]["debug"].values(), + distenv.Dir(f"#/dist/{dist_dir}/apps/debug_elf"), + list( + app_artifact.debug + for app_artifact in firmware_env["FW_EXTAPPS"].applications.values() + ), ), - *( - distenv.Install(f"#/dist/{dist_dir}/apps/{dist_entry[0]}", dist_entry[1]) - for dist_entry in firmware_env["FW_EXTAPPS"]["dist"].values() + distenv.Install( + f"#/dist/{dist_dir}/apps", + "#/assets/resources/apps", ), ] -Depends(fap_dist, firmware_env["FW_EXTAPPS"]["validators"].values()) +Depends( + fap_dist, + list( + app_artifact.validator + for app_artifact in firmware_env["FW_EXTAPPS"].applications.values() + ), +) Alias("fap_dist", fap_dist) # distenv.Default(fap_dist) -distenv.Depends( - firmware_env["FW_RESOURCES"], firmware_env["FW_EXTAPPS"]["resources_dist"] -) +distenv.Depends(firmware_env["FW_RESOURCES"], firmware_env["FW_EXTAPPS"].resources_dist) # Target for bundling core2 package for qFlipper @@ -192,6 +200,7 @@ firmware_debug = distenv.PhonyTarget( source=firmware_env["FW_ELF"], GDBOPTS="${GDBOPTS_BASE}", GDBREMOTE="${OPENOCD_GDB_PIPE}", + FBT_FAP_DEBUG_ELF_ROOT=firmware_env.subst("$FBT_FAP_DEBUG_ELF_ROOT"), ) distenv.Depends(firmware_debug, firmware_flash) @@ -201,6 +210,7 @@ distenv.PhonyTarget( source=firmware_env["FW_ELF"], GDBOPTS="${GDBOPTS_BASE} ${GDBOPTS_BLACKMAGIC}", GDBREMOTE="${BLACKMAGIC_ADDR}", + FBT_FAP_DEBUG_ELF_ROOT=firmware_env.subst("$FBT_FAP_DEBUG_ELF_ROOT"), ) # Debug alien elf @@ -209,7 +219,7 @@ distenv.PhonyTarget( "${GDBPYCOM}", GDBOPTS="${GDBOPTS_BASE}", GDBREMOTE="${OPENOCD_GDB_PIPE}", - GDBPYOPTS='-ex "source debug/PyCortexMDebug/PyCortexMDebug.py" ', + GDBPYOPTS='-ex "source ${FBT_DEBUG_DIR}/PyCortexMDebug/PyCortexMDebug.py" ', ) distenv.PhonyTarget( diff --git a/applications/main/bad_usb/application.fam b/applications/main/bad_usb/application.fam index 4da34f0de..2442dd3aa 100644 --- a/applications/main/bad_usb/application.fam +++ b/applications/main/bad_usb/application.fam @@ -11,4 +11,5 @@ App( stack_size=2 * 1024, icon="A_BadUsb_14", order=70, + fap_libs=["assets"], ) diff --git a/applications/main/gpio/application.fam b/applications/main/gpio/application.fam index 64f8db5b0..efeb8b6fe 100644 --- a/applications/main/gpio/application.fam +++ b/applications/main/gpio/application.fam @@ -8,4 +8,5 @@ App( stack_size=1 * 1024, icon="A_GPIO_14", order=50, + fap_libs=["assets"], ) diff --git a/applications/main/ibutton/application.fam b/applications/main/ibutton/application.fam index 0bc6f8a9b..77bb9a33c 100644 --- a/applications/main/ibutton/application.fam +++ b/applications/main/ibutton/application.fam @@ -12,6 +12,7 @@ App( icon="A_iButton_14", stack_size=2 * 1024, order=60, + fap_libs=["assets"], ) App( diff --git a/applications/main/infrared/application.fam b/applications/main/infrared/application.fam index 6f76ed429..9c5eaf392 100644 --- a/applications/main/infrared/application.fam +++ b/applications/main/infrared/application.fam @@ -12,6 +12,7 @@ App( icon="A_Infrared_14", stack_size=3 * 1024, order=40, + fap_libs=["assets"], ) App( diff --git a/applications/main/lfrfid/application.fam b/applications/main/lfrfid/application.fam index 4a1498181..150a6f3db 100644 --- a/applications/main/lfrfid/application.fam +++ b/applications/main/lfrfid/application.fam @@ -14,6 +14,7 @@ App( icon="A_125khz_14", stack_size=2 * 1024, order=20, + fap_libs=["assets"], ) App( diff --git a/applications/main/u2f/application.fam b/applications/main/u2f/application.fam index 6b32e0225..82010ffb4 100644 --- a/applications/main/u2f/application.fam +++ b/applications/main/u2f/application.fam @@ -11,4 +11,5 @@ App( stack_size=2 * 1024, icon="A_U2F_14", order=80, + fap_libs=["assets"], ) diff --git a/debug/flipperapps.py b/debug/flipperapps.py index 8e1aa2daf..e815e40b1 100644 --- a/debug/flipperapps.py +++ b/debug/flipperapps.py @@ -1,5 +1,5 @@ from dataclasses import dataclass -from typing import Tuple, Dict +from typing import Optional, Tuple, Dict, ClassVar import struct import posixpath import os @@ -22,14 +22,18 @@ class AppState: debug_link_elf: str = "" debug_link_crc: int = 0 + DEBUG_ELF_ROOT: ClassVar[Optional[str]] = None + def __post_init__(self): if self.other_sections is None: self.other_sections = {} - def get_original_elf_path(self, elf_path="build/latest/.extapps") -> str: + def get_original_elf_path(self) -> str: + if self.DEBUG_ELF_ROOT is None: + raise ValueError("DEBUG_ELF_ROOT not set; call fap-set-debug-elf-root") return ( - posixpath.join(elf_path, self.debug_link_elf) - if elf_path + posixpath.join(self.DEBUG_ELF_ROOT, self.debug_link_elf) + if self.DEBUG_ELF_ROOT else self.debug_link_elf ) @@ -84,7 +88,9 @@ class AppState: if debug_link_size := int(app_state["debug_link_info"]["debug_link_size"]): debug_link_data = ( gdb.selected_inferior() - .read_memory(int(app_state["debug_link_info"]["debug_link"]), debug_link_size) + .read_memory( + int(app_state["debug_link_info"]["debug_link"]), debug_link_size + ) .tobytes() ) state.debug_link_elf, state.debug_link_crc = AppState.parse_debug_link_data( @@ -103,6 +109,29 @@ class AppState: return state +class SetFapDebugElfRoot(gdb.Command): + """Set path to original ELF files for debug info""" + + def __init__(self): + super().__init__( + "fap-set-debug-elf-root", gdb.COMMAND_FILES, gdb.COMPLETE_FILENAME + ) + self.dont_repeat() + + def invoke(self, arg, from_tty): + AppState.DEBUG_ELF_ROOT = arg + try: + global helper + print(f"Set '{arg}' as debug info lookup path for Flipper external apps") + helper.attach_fw() + gdb.events.stop.connect(helper.handle_stop) + except gdb.error as e: + print(f"Support for Flipper external apps debug is not available: {e}") + + +SetFapDebugElfRoot() + + class FlipperAppDebugHelper: def __init__(self): self.app_ptr = None @@ -149,9 +178,4 @@ class FlipperAppDebugHelper: helper = FlipperAppDebugHelper() -try: - helper.attach_fw() - print("Support for Flipper external apps debug is enabled") - gdb.events.stop.connect(helper.handle_stop) -except gdb.error as e: - print(f"Support for Flipper external apps debug is not available: {e}") +print("Support for Flipper external apps debug is loaded") diff --git a/fbt_options.py b/fbt_options.py index 6ef9759e3..11124b936 100644 --- a/fbt_options.py +++ b/fbt_options.py @@ -49,12 +49,12 @@ OPENOCD_OPTS = [ "-c", "transport select hla_swd", "-f", - "debug/stm32wbx.cfg", + "${FBT_DEBUG_DIR}/stm32wbx.cfg", "-c", "stm32wbx.cpu configure -rtos auto", ] -SVD_FILE = "debug/STM32WB55_CM4.svd" +SVD_FILE = "${FBT_DEBUG_DIR}/STM32WB55_CM4.svd" # Look for blackmagic probe on serial ports and local network BLACKMAGIC = "auto" diff --git a/firmware.scons b/firmware.scons index a0c1ab339..6feb73ac3 100644 --- a/firmware.scons +++ b/firmware.scons @@ -20,8 +20,7 @@ env = ENV.Clone( BUILD_DIR=fw_build_meta["build_dir"], IS_BASE_FIRMWARE=fw_build_meta["type"] == "firmware", FW_FLAVOR=fw_build_meta["flavor"], - PLUGIN_ELF_DIR="${BUILD_DIR}", - LIB_DIST_DIR="${BUILD_DIR}/lib", + LIB_DIST_DIR=fw_build_meta["build_dir"].Dir("lib"), LINT_SOURCES=[ "applications", ], @@ -142,12 +141,14 @@ for app_dir, _ in env["APPDIRS"]: fwenv.PrepareApplicationsBuild() -# Build external apps +# Build external apps + configure SDK if env["IS_BASE_FIRMWARE"]: - extapps = fwenv["FW_EXTAPPS"] = SConscript( - "site_scons/extapps.scons", exports={"ENV": fwenv} + fwenv.SetDefault(FBT_FAP_DEBUG_ELF_ROOT="${BUILD_DIR}/.extapps") + fwenv["FW_EXTAPPS"] = SConscript( + "site_scons/extapps.scons", + exports={"ENV": fwenv}, ) - fw_artifacts.append(extapps["sdk_tree"]) + fw_artifacts.append(fwenv["FW_EXTAPPS"].sdk_tree) # Add preprocessor definitions for current set of apps diff --git a/scripts/fbt/elfmanifest.py b/scripts/fbt/elfmanifest.py index 313a64c09..17bceddf4 100644 --- a/scripts/fbt/elfmanifest.py +++ b/scripts/fbt/elfmanifest.py @@ -5,6 +5,7 @@ import struct from dataclasses import dataclass, field from .appmanifest import FlipperApplication +from flipper.assets.icon import file2image _MANIFEST_MAGIC = 0x52474448 @@ -53,8 +54,6 @@ def assemble_manifest_data( ): image_data = b"" if app_manifest.fap_icon: - from flipper.assets.icon import file2image - image = file2image(os.path.join(app_manifest._apppath, app_manifest.fap_icon)) if (image.width, image.height) != (10, 10): raise ValueError( diff --git a/scripts/fbt/sdk/cache.py b/scripts/fbt/sdk/cache.py index 62d42798c..756c37827 100644 --- a/scripts/fbt/sdk/cache.py +++ b/scripts/fbt/sdk/cache.py @@ -89,6 +89,9 @@ class SdkCache: syms.update(map(lambda e: e.name, self.get_variables())) return syms + def get_disabled_names(self): + return set(map(lambda e: e.name, self.disabled_entries)) + def get_functions(self): return self._filter_enabled(self.sdk.functions) diff --git a/scripts/fbt_tools/fbt_assets.py b/scripts/fbt_tools/fbt_assets.py index 521c37e90..e17487358 100644 --- a/scripts/fbt_tools/fbt_assets.py +++ b/scripts/fbt_tools/fbt_assets.py @@ -139,7 +139,7 @@ def generate(env): BUILDERS={ "IconBuilder": Builder( action=Action( - '${PYTHON3} "${ASSETS_COMPILER}" icons ${ABSPATHGETTERFUNC(SOURCE)} ${TARGET.dir} --filename ${ICON_FILE_NAME}', + '${PYTHON3} "${ASSETS_COMPILER}" icons "${ABSPATHGETTERFUNC(SOURCE)}" "${TARGET.dir}" --filename ${ICON_FILE_NAME}', "${ICONSCOMSTR}", ), emitter=icons_emitter, diff --git a/scripts/fbt_tools/fbt_debugopts.py b/scripts/fbt_tools/fbt_debugopts.py index 0c0ce3d32..c3be5ca47 100644 --- a/scripts/fbt_tools/fbt_debugopts.py +++ b/scripts/fbt_tools/fbt_debugopts.py @@ -1,7 +1,6 @@ from re import search from SCons.Errors import UserError -from fbt_options import OPENOCD_OPTS def _get_device_serials(search_str="STLink"): @@ -20,6 +19,9 @@ def GetDevices(env): def generate(env, **kw): env.AddMethod(GetDevices) + env.SetDefault( + FBT_DEBUG_DIR="${ROOT_DIR}/debug", + ) if (adapter_serial := env.subst("$OPENOCD_ADAPTER_SERIAL")) != "auto": env.Append( @@ -36,7 +38,7 @@ def generate(env, **kw): env.SetDefault( OPENOCD_GDB_PIPE=[ - "|openocd -c 'gdb_port pipe; log_output debug/openocd.log' ${[SINGLEQUOTEFUNC(OPENOCD_OPTS)]}" + "|openocd -c 'gdb_port pipe; log_output ${FBT_DEBUG_DIR}/openocd.log' ${[SINGLEQUOTEFUNC(OPENOCD_OPTS)]}" ], GDBOPTS_BASE=[ "-ex", @@ -58,17 +60,19 @@ def generate(env, **kw): ], GDBPYOPTS=[ "-ex", - "source debug/FreeRTOS/FreeRTOS.py", + "source ${FBT_DEBUG_DIR}/FreeRTOS/FreeRTOS.py", "-ex", - "source debug/flipperapps.py", + "source ${FBT_DEBUG_DIR}/flipperapps.py", "-ex", - "source debug/PyCortexMDebug/PyCortexMDebug.py", + "fap-set-debug-elf-root ${FBT_FAP_DEBUG_ELF_ROOT}", + "-ex", + "source ${FBT_DEBUG_DIR}/PyCortexMDebug/PyCortexMDebug.py", "-ex", "svd_load ${SVD_FILE}", "-ex", "compare-sections", ], - JFLASHPROJECT="${ROOT_DIR.abspath}/debug/fw.jflash", + JFLASHPROJECT="${FBT_DEBUG_DIR}/fw.jflash", ) diff --git a/scripts/fbt_tools/fbt_dist.py b/scripts/fbt_tools/fbt_dist.py index fb59e5b95..f0b443486 100644 --- a/scripts/fbt_tools/fbt_dist.py +++ b/scripts/fbt_tools/fbt_dist.py @@ -22,7 +22,7 @@ def GetProjetDirName(env, project=None): def create_fw_build_targets(env, configuration_name): flavor = GetProjetDirName(env, configuration_name) - build_dir = env.Dir("build").Dir(flavor).abspath + build_dir = env.Dir("build").Dir(flavor) return env.SConscript( "firmware.scons", variant_dir=build_dir, @@ -131,7 +131,7 @@ def generate(env): "UsbInstall": Builder( action=[ Action( - '${PYTHON3} "${SELFUPDATE_SCRIPT}" dist/${DIST_DIR}/f${TARGET_HW}-update-${DIST_SUFFIX}/update.fuf' + '${PYTHON3} "${SELFUPDATE_SCRIPT}" ${UPDATE_BUNDLE_DIR}/update.fuf' ), Touch("${TARGET}"), ] diff --git a/scripts/fbt_tools/fbt_extapps.py b/scripts/fbt_tools/fbt_extapps.py index fb4dc2f16..f1906191b 100644 --- a/scripts/fbt_tools/fbt_extapps.py +++ b/scripts/fbt_tools/fbt_extapps.py @@ -1,6 +1,9 @@ +from dataclasses import dataclass, field +from typing import Optional from SCons.Builder import Builder from SCons.Action import Action from SCons.Errors import UserError +from SCons.Node import NodeList import SCons.Warnings from fbt.elfmanifest import assemble_manifest_data @@ -16,6 +19,15 @@ import shutil from ansi.color import fg +@dataclass +class FlipperExternalAppInfo: + app: FlipperApplication + compact: NodeList = field(default_factory=NodeList) + debug: NodeList = field(default_factory=NodeList) + validator: NodeList = field(default_factory=NodeList) + installer: NodeList = field(default_factory=NodeList) + + def BuildAppElf(env, app): ext_apps_work_dir = env.subst("$EXT_APPS_WORK_DIR") app_work_dir = os.path.join(ext_apps_work_dir, app.appid) @@ -26,15 +38,7 @@ def BuildAppElf(env, app): app_alias = f"fap_{app.appid}" - # Deprecation stub - legacy_app_taget_name = f"{app_env['FIRMWARE_BUILD_CFG']}_{app.appid}" - - def legacy_app_build_stub(**kw): - raise UserError( - f"Target name '{legacy_app_taget_name}' is deprecated, use '{app_alias}' instead" - ) - - app_env.PhonyTarget(legacy_app_taget_name, Action(legacy_app_build_stub, None)) + app_artifacts = FlipperExternalAppInfo(app) externally_built_files = [] if app.fap_extbuild: @@ -115,20 +119,22 @@ def BuildAppElf(env, app): CPPPATH=env.Dir(app_work_dir), ) - app_elf_raw = app_env.Program( + app_artifacts.debug = app_env.Program( os.path.join(ext_apps_work_dir, f"{app.appid}_d"), app_sources, APP_ENTRY=app.entry_point, ) - app_env.Clean(app_elf_raw, [*externally_built_files, app_env.Dir(app_work_dir)]) + app_env.Clean( + app_artifacts.debug, [*externally_built_files, app_env.Dir(app_work_dir)] + ) - app_elf_dump = app_env.ObjDump(app_elf_raw) + app_elf_dump = app_env.ObjDump(app_artifacts.debug) app_env.Alias(f"{app_alias}_list", app_elf_dump) - app_elf_augmented = app_env.EmbedAppMetadata( + app_artifacts.compact = app_env.EmbedAppMetadata( os.path.join(ext_apps_work_dir, app.appid), - app_elf_raw, + app_artifacts.debug, APP=app, ) @@ -139,19 +145,21 @@ def BuildAppElf(env, app): } app_env.Depends( - app_elf_augmented, + app_artifacts.compact, [app_env["SDK_DEFINITION"], app_env.Value(manifest_vals)], ) if app.fap_icon: app_env.Depends( - app_elf_augmented, + app_artifacts.compact, app_env.File(f"{app._apppath}/{app.fap_icon}"), ) - app_elf_import_validator = app_env.ValidateAppImports(app_elf_augmented) - app_env.AlwaysBuild(app_elf_import_validator) - app_env.Alias(app_alias, app_elf_import_validator) - return (app_elf_augmented, app_elf_raw, app_elf_import_validator) + app_artifacts.validator = app_env.ValidateAppImports(app_artifacts.compact) + app_env.AlwaysBuild(app_artifacts.validator) + app_env.Alias(app_alias, app_artifacts.validator) + + env["EXT_APPS"][app.appid] = app_artifacts + return app_artifacts def prepare_app_metadata(target, source, env): @@ -182,11 +190,17 @@ def validate_app_imports(target, source, env): app_syms.add(line.split()[0]) unresolved_syms = app_syms - sdk_cache.get_valid_names() if unresolved_syms: - SCons.Warnings.warn( - SCons.Warnings.LinkWarning, - fg.brightyellow(f"{source[0].path}: app won't run. Unresolved symbols: ") - + fg.brightmagenta(f"{unresolved_syms}"), - ) + warning_msg = fg.brightyellow( + f"{source[0].path}: app won't run. Unresolved symbols: " + ) + fg.brightmagenta(f"{unresolved_syms}") + disabled_api_syms = unresolved_syms.intersection(sdk_cache.get_disabled_names()) + if disabled_api_syms: + warning_msg += ( + fg.brightyellow(" (in API, but disabled: ") + + fg.brightmagenta(f"{disabled_api_syms}") + + fg.brightyellow(")") + ) + SCons.Warnings.warn(SCons.Warnings.LinkWarning, warning_msg), def GetExtAppFromPath(env, app_dir): @@ -208,26 +222,26 @@ def GetExtAppFromPath(env, app_dir): if not app: raise UserError(f"Failed to resolve application for given APPSRC={app_dir}") - app_elf = env["_extapps"]["compact"].get(app.appid, None) - if not app_elf: + app_artifacts = env["EXT_APPS"].get(app.appid, None) + if not app_artifacts: raise UserError( f"Application {app.appid} is not configured for building as external" ) - app_validator = env["_extapps"]["validators"].get(app.appid, None) - - return (app, app_elf[0], app_validator[0]) + return app_artifacts def fap_dist_emitter(target, source, env): target_dir = target[0] target = [] - for dist_entry in env["_extapps"]["dist"].values(): - target.append(target_dir.Dir(dist_entry[0]).File(dist_entry[1][0].name)) - - for compact_entry in env["_extapps"]["compact"].values(): - source.extend(compact_entry) + for _, app_artifacts in env["EXT_APPS"].items(): + source.extend(app_artifacts.compact) + target.append( + target_dir.Dir(app_artifacts.app.fap_category).File( + app_artifacts.compact[0].name + ) + ) return (target, source) @@ -244,10 +258,9 @@ def fap_dist_action(target, source, env): def generate(env, **kw): env.SetDefault( - EXT_APPS_WORK_DIR=kw.get("EXT_APPS_WORK_DIR"), + EXT_APPS_WORK_DIR="${FBT_FAP_DEBUG_ELF_ROOT}", APP_RUN_SCRIPT="${FBT_SCRIPT_DIR}/runfap.py", ) - if not env["VERBOSE"]: env.SetDefault( FAPDISTCOMSTR="\tFAPDIST\t${TARGET}", @@ -256,6 +269,10 @@ def generate(env, **kw): APPCHECK_COMSTR="\tAPPCHK\t${SOURCE}", ) + env.SetDefault( + EXT_APPS={}, # appid -> FlipperExternalAppInfo + ) + env.AddMethod(BuildAppElf) env.AddMethod(GetExtAppFromPath) env.Append( diff --git a/scripts/requirements.txt b/scripts/requirements.txt index 35cac7742..5b6fac5f7 100644 --- a/scripts/requirements.txt +++ b/scripts/requirements.txt @@ -1,7 +1,9 @@ -pyserial==3.5 +ansi==0.3.6 +black==22.6.0 +colorlog==6.7.0 heatshrink2==0.11.0 Pillow==9.1.1 -grpcio==1.47.0 -grpcio-tools==1.47.0 -protobuf==3.20.2 +protobuf==3.20.1 +pyserial==3.5 python3-protobuf==2.5.0 +SCons==4.4.0 diff --git a/site_scons/commandline.scons b/site_scons/commandline.scons index cadb417f8..044de6b30 100644 --- a/site_scons/commandline.scons +++ b/site_scons/commandline.scons @@ -147,7 +147,7 @@ vars.AddVariables( PathVariable( "SVD_FILE", help="Path to SVD file", - validator=PathVariable.PathIsFile, + validator=PathVariable.PathAccept, default="", ), PathVariable( diff --git a/site_scons/environ.scons b/site_scons/environ.scons index 96424caad..acdc83e2a 100644 --- a/site_scons/environ.scons +++ b/site_scons/environ.scons @@ -61,8 +61,8 @@ coreenv = VAR_ENV.Clone( ABSPATHGETTERFUNC=extract_abs_dir_path, # Setting up temp file parameters - to overcome command line length limits TEMPFILEARGESCFUNC=tempfile_arg_esc_func, - FBT_SCRIPT_DIR=Dir("#/scripts"), ROOT_DIR=Dir("#"), + FBT_SCRIPT_DIR="${ROOT_DIR}/scripts", ) # If DIST_SUFFIX is set in environment, is has precedence (set by CI) diff --git a/site_scons/extapps.scons b/site_scons/extapps.scons index bb1d65ffc..3d2a98788 100644 --- a/site_scons/extapps.scons +++ b/site_scons/extapps.scons @@ -1,4 +1,6 @@ +from dataclasses import dataclass, field from SCons.Errors import UserError +from SCons.Node import NodeList Import("ENV") @@ -7,14 +9,7 @@ from fbt.appmanifest import FlipperAppType appenv = ENV["APPENV"] = ENV.Clone( tools=[ - ( - "fbt_extapps", - { - "EXT_APPS_WORK_DIR": ENV.subst( - "${BUILD_DIR}/.extapps", - ) - }, - ), + "fbt_extapps", "fbt_assets", "fbt_sdk", ] @@ -60,22 +55,11 @@ appenv.AppendUnique( ) -extapps = appenv["_extapps"] = { - "compact": {}, - "debug": {}, - "validators": {}, - "dist": {}, - "resources_dist": None, - "sdk_tree": None, -} - - -def build_app_as_external(env, appdef): - compact_elf, debug_elf, validator = env.BuildAppElf(appdef) - extapps["compact"][appdef.appid] = compact_elf - extapps["debug"][appdef.appid] = debug_elf - extapps["validators"][appdef.appid] = validator - extapps["dist"][appdef.appid] = (appdef.fap_category, compact_elf) +@dataclass +class FlipperExtAppBuildArtifacts: + applications: dict = field(default_factory=dict) + resources_dist: NodeList = field(default_factory=NodeList) + sdk_tree: NodeList = field(default_factory=NodeList) apps_to_build_as_faps = [ @@ -85,38 +69,39 @@ apps_to_build_as_faps = [ if appenv["DEBUG_TOOLS"]: apps_to_build_as_faps.append(FlipperAppType.DEBUG) -for apptype in apps_to_build_as_faps: - for app in appenv["APPBUILD"].get_apps_of_type(apptype, True): - build_app_as_external(appenv, app) +known_extapps = [ + app + for apptype in apps_to_build_as_faps + for app in appenv["APPBUILD"].get_apps_of_type(apptype, True) +] # Ugly access to global option if extra_app_list := GetOption("extra_ext_apps"): - for extra_app in extra_app_list.split(","): - build_app_as_external(appenv, appenv["APPMGR"].get(extra_app)) + known_extapps.extend(map(appenv["APPMGR"].get, extra_app_list.split(","))) + +for app in known_extapps: + appenv.BuildAppElf(app) if appenv["FORCE"]: - appenv.AlwaysBuild(extapps["compact"].values()) + appenv.AlwaysBuild( + list(app_artifact.compact for app_artifact in appenv["EXT_APPS"].values()) + ) -# Deprecation stub -def legacy_app_build_stub(**kw): - raise UserError(f"Target name 'firmware_extapps' is deprecated, use 'faps' instead") +Alias( + "faps", list(app_artifact.validator for app_artifact in appenv["EXT_APPS"].values()) +) - -appenv.PhonyTarget("firmware_extapps", appenv.Action(legacy_app_build_stub, None)) - - -Alias("faps", extapps["compact"].values()) -Alias("faps", extapps["validators"].values()) - -extapps["resources_dist"] = appenv.FapDist(appenv.Dir("#/assets/resources/apps"), []) +extapps = FlipperExtAppBuildArtifacts() +extapps.applications = appenv["EXT_APPS"] +extapps.resources_dist = appenv.FapDist(appenv.Dir("#/assets/resources/apps"), []) if appsrc := appenv.subst("$APPSRC"): app_manifest, fap_file, app_validator = appenv.GetExtAppFromPath(appsrc) appenv.PhonyTarget( "launch_app", - '${PYTHON3} "${APP_RUN_SCRIPT}" ${SOURCE} --fap_dst_dir "/ext/apps/${FAP_CATEGORY}"', + '${PYTHON3} "${APP_RUN_SCRIPT}" "${SOURCE}" --fap_dst_dir "/ext/apps/${FAP_CATEGORY}"', source=fap_file, FAP_CATEGORY=app_manifest.fap_category, ) @@ -131,12 +116,14 @@ sdk_source = appenv.SDKPrebuilder( (appenv["SDK_HEADERS"], appenv["FW_ASSETS_HEADERS"]), ) # Extra deps on headers included in deeper levels +# Available on second and subsequent builds Depends(sdk_source, appenv.ProcessSdkDepends(f"{sdk_origin_path}.d")) appenv["SDK_DIR"] = appenv.Dir("${BUILD_DIR}/sdk") -sdk_tree = extapps["sdk_tree"] = appenv.SDKTree(appenv["SDK_DIR"], sdk_origin_path) +sdk_tree = appenv.SDKTree(appenv["SDK_DIR"], sdk_origin_path) # AlwaysBuild(sdk_tree) Alias("sdk_tree", sdk_tree) +extapps.sdk_tree = sdk_tree sdk_apicheck = appenv.SDKSymUpdater(appenv["SDK_DEFINITION"], sdk_origin_path) Precious(sdk_apicheck) From e8913f2e33d8fbbbee0e3d0ebcf77f1e65bc6ece Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=82=E3=81=8F?= Date: Sun, 6 Nov 2022 00:07:24 +0900 Subject: [PATCH 26/49] Code cleanup: srand, PVS warnings (#1974) * Remove srand invocation * PVS High priority fixes * PVS High errors part 2 * Furi: heap tracing inheritance * Furi add __builtin_unreachable to furi_thread_catch --- applications/main/bad_usb/bad_usb_script.c | 6 +--- applications/main/fap_loader/fap_loader_app.c | 2 +- applications/main/gpio/usb_uart_bridge.c | 4 +-- applications/main/lfrfid/lfrfid.c | 2 +- .../nfc_scene_mf_classic_read_success.c | 2 +- .../nfc_scene_mf_ultralight_read_success.c | 2 +- applications/main/subghz/subghz_i.c | 6 ---- applications/main/u2f/u2f_hid.c | 2 +- applications/plugins/snake_game/snake_game.c | 1 - .../protocols/ambient_weather.c | 1 - applications/services/cli/cli_command_gpio.c | 33 +++++++++++-------- applications/services/cli/cli_vcp.c | 4 +-- applications/services/crypto/crypto_cli.c | 4 +-- applications/services/gui/gui.c | 2 +- .../gui/modules/file_browser_worker.c | 2 +- .../widget_elements/widget_element_button.c | 2 +- applications/services/gui/view_dispatcher.c | 4 +-- applications/services/loader/loader.c | 4 +-- applications/services/storage/storage_cli.c | 4 +-- .../services/storage/storage_external_api.c | 4 +-- firmware/targets/f7/ble_glue/gap.c | 3 +- .../targets/f7/furi_hal/furi_hal_crypto.c | 2 +- furi/core/check.h | 4 +-- furi/core/event_flag.c | 6 ++-- furi/core/memmgr_heap.c | 6 ++-- furi/core/mutex.c | 6 ++-- furi/core/semaphore.c | 6 ++-- furi/core/stream_buffer.c | 4 +-- furi/core/string.c | 4 +-- furi/core/thread.c | 7 ++++ lib/flipper_format/flipper_format_stream.c | 2 +- .../common/infrared_common_decoder.c | 6 ++-- lib/lfrfid/lfrfid_worker.c | 3 +- lib/print/printf_tiny.c | 2 +- lib/subghz/protocols/power_smart.c | 1 - lib/subghz/subghz_keystore.c | 2 +- lib/toolbox/random_name.c | 6 ---- 37 files changed, 76 insertions(+), 85 deletions(-) diff --git a/applications/main/bad_usb/bad_usb_script.c b/applications/main/bad_usb/bad_usb_script.c index 33b3f5030..ae6181149 100644 --- a/applications/main/bad_usb/bad_usb_script.c +++ b/applications/main/bad_usb/bad_usb_script.c @@ -338,10 +338,6 @@ static int32_t furi_hal_hid_kb_release(key); return (0); } - if(error != NULL) { - strncpy(error, "Unknown error", error_len); - } - return SCRIPT_STATE_ERROR; } static bool ducky_set_usb_id(BadUsbScript* bad_usb, const char* line) { @@ -656,7 +652,7 @@ static int32_t bad_usb_worker(void* context) { BadUsbScript* bad_usb_script_open(FuriString* file_path) { furi_assert(file_path); - BadUsbScript* bad_usb = malloc(sizeof(BadUsbScript)); + BadUsbScript* bad_usb = malloc(sizeof(BadUsbScript)); //-V773 bad_usb->file_path = furi_string_alloc(); furi_string_set(bad_usb->file_path, file_path); diff --git a/applications/main/fap_loader/fap_loader_app.c b/applications/main/fap_loader/fap_loader_app.c index 9b4c92335..10cec086a 100644 --- a/applications/main/fap_loader/fap_loader_app.c +++ b/applications/main/fap_loader/fap_loader_app.c @@ -155,7 +155,7 @@ static bool fap_loader_select_app(FapLoader* loader) { } static FapLoader* fap_loader_alloc(const char* path) { - FapLoader* loader = malloc(sizeof(FapLoader)); + FapLoader* loader = malloc(sizeof(FapLoader)); //-V773 loader->fap_path = furi_string_alloc_set(path); loader->storage = furi_record_open(RECORD_STORAGE); loader->dialogs = furi_record_open(RECORD_DIALOGS); diff --git a/applications/main/gpio/usb_uart_bridge.c b/applications/main/gpio/usb_uart_bridge.c index a5caceafa..a1ab40329 100644 --- a/applications/main/gpio/usb_uart_bridge.c +++ b/applications/main/gpio/usb_uart_bridge.c @@ -184,7 +184,7 @@ static int32_t usb_uart_worker(void* context) { while(1) { uint32_t events = furi_thread_flags_wait(WORKER_ALL_RX_EVENTS, FuriFlagWaitAny, FuriWaitForever); - furi_check((events & FuriFlagError) == 0); + furi_check(!(events & FuriFlagError)); if(events & WorkerEvtStop) break; if(events & WorkerEvtRxDone) { size_t len = furi_stream_buffer_receive( @@ -288,7 +288,7 @@ static int32_t usb_uart_tx_thread(void* context) { while(1) { uint32_t events = furi_thread_flags_wait(WORKER_ALL_TX_EVENTS, FuriFlagWaitAny, FuriWaitForever); - furi_check((events & FuriFlagError) == 0); + furi_check(!(events & FuriFlagError)); if(events & WorkerEvtTxStop) break; if(events & WorkerEvtCdcRx) { furi_check(furi_mutex_acquire(usb_uart->usb_mutex, FuriWaitForever) == FuriStatusOk); diff --git a/applications/main/lfrfid/lfrfid.c b/applications/main/lfrfid/lfrfid.c index 513227306..d391c5e89 100644 --- a/applications/main/lfrfid/lfrfid.c +++ b/applications/main/lfrfid/lfrfid.c @@ -32,7 +32,7 @@ static void rpc_command_callback(RpcAppSystemEvent rpc_event, void* context) { } static LfRfid* lfrfid_alloc() { - LfRfid* lfrfid = malloc(sizeof(LfRfid)); + LfRfid* lfrfid = malloc(sizeof(LfRfid)); //-V773 lfrfid->storage = furi_record_open(RECORD_STORAGE); lfrfid->dialogs = furi_record_open(RECORD_DIALOGS); diff --git a/applications/main/nfc/scenes/nfc_scene_mf_classic_read_success.c b/applications/main/nfc/scenes/nfc_scene_mf_classic_read_success.c index ae31e92cc..444c9a540 100644 --- a/applications/main/nfc/scenes/nfc_scene_mf_classic_read_success.c +++ b/applications/main/nfc/scenes/nfc_scene_mf_classic_read_success.c @@ -24,7 +24,7 @@ void nfc_scene_mf_classic_read_success_on_enter(void* context) { widget_add_button_element( widget, GuiButtonTypeRight, "More", nfc_scene_mf_classic_read_success_widget_callback, nfc); - FuriString* temp_str; + FuriString* temp_str = NULL; if(furi_string_size(nfc->dev->dev_data.parsed_data)) { temp_str = furi_string_alloc_set(nfc->dev->dev_data.parsed_data); } else { diff --git a/applications/main/nfc/scenes/nfc_scene_mf_ultralight_read_success.c b/applications/main/nfc/scenes/nfc_scene_mf_ultralight_read_success.c index 63bffbf36..cb5ccd6e8 100644 --- a/applications/main/nfc/scenes/nfc_scene_mf_ultralight_read_success.c +++ b/applications/main/nfc/scenes/nfc_scene_mf_ultralight_read_success.c @@ -31,7 +31,7 @@ void nfc_scene_mf_ultralight_read_success_on_enter(void* context) { nfc_scene_mf_ultralight_read_success_widget_callback, nfc); - FuriString* temp_str; + FuriString* temp_str = NULL; if(furi_string_size(nfc->dev->dev_data.parsed_data)) { temp_str = furi_string_alloc_set(nfc->dev->dev_data.parsed_data); } else { diff --git a/applications/main/subghz/subghz_i.c b/applications/main/subghz/subghz_i.c index beefd8024..a887b6543 100644 --- a/applications/main/subghz/subghz_i.c +++ b/applications/main/subghz/subghz_i.c @@ -513,12 +513,6 @@ bool subghz_path_is_file(FuriString* path) { } uint32_t subghz_random_serial(void) { - static bool rand_generator_inited = false; - - if(!rand_generator_inited) { - srand(DWT->CYCCNT); - rand_generator_inited = true; - } return (uint32_t)rand(); } diff --git a/applications/main/u2f/u2f_hid.c b/applications/main/u2f/u2f_hid.c index 4922d6a5a..b6e86384d 100644 --- a/applications/main/u2f/u2f_hid.c +++ b/applications/main/u2f/u2f_hid.c @@ -203,7 +203,7 @@ static int32_t u2f_hid_worker(void* context) { WorkerEvtStop | WorkerEvtConnect | WorkerEvtDisconnect | WorkerEvtRequest, FuriFlagWaitAny, FuriWaitForever); - furi_check((flags & FuriFlagError) == 0); + furi_check(!(flags & FuriFlagError)); if(flags & WorkerEvtStop) break; if(flags & WorkerEvtConnect) { u2f_set_state(u2f_hid->u2f_instance, 1); diff --git a/applications/plugins/snake_game/snake_game.c b/applications/plugins/snake_game/snake_game.c index f9309c280..ef4ae2ee8 100644 --- a/applications/plugins/snake_game/snake_game.c +++ b/applications/plugins/snake_game/snake_game.c @@ -318,7 +318,6 @@ static void int32_t snake_game_app(void* p) { UNUSED(p); - srand(DWT->CYCCNT); FuriMessageQueue* event_queue = furi_message_queue_alloc(8, sizeof(SnakeEvent)); diff --git a/applications/plugins/weather_station/protocols/ambient_weather.c b/applications/plugins/weather_station/protocols/ambient_weather.c index 07f5330fc..5fede684e 100644 --- a/applications/plugins/weather_station/protocols/ambient_weather.c +++ b/applications/plugins/weather_station/protocols/ambient_weather.c @@ -200,7 +200,6 @@ void ws_protocol_decoder_ambient_weather_feed(void* context, bool level, uint32_ ((instance->decoder.decode_data & AMBIENT_WEATHER_PACKET_HEADER_MASK) == AMBIENT_WEATHER_PACKET_HEADER_2)) { if(ws_protocol_ambient_weather_check_crc(instance)) { - instance->decoder.decode_data = instance->decoder.decode_data; instance->generic.data = instance->decoder.decode_data; instance->generic.data_count_bit = ws_protocol_ambient_weather_const.min_count_bit_for_found; diff --git a/applications/services/cli/cli_command_gpio.c b/applications/services/cli/cli_command_gpio.c index d072ce00c..3e7301cdc 100644 --- a/applications/services/cli/cli_command_gpio.c +++ b/applications/services/cli/cli_command_gpio.c @@ -61,15 +61,20 @@ static void gpio_print_pins(void) { } } -typedef enum { OK, ERR_CMD_SYNTAX, ERR_PIN, ERR_VALUE } GpioParseError; +typedef enum { + GpioParseReturnOk, + GpioParseReturnCmdSyntaxError, + GpioParseReturnPinError, + GpioParseReturnValueError +} GpioParseReturn; -static GpioParseError gpio_command_parse(FuriString* args, size_t* pin_num, uint8_t* value) { +static GpioParseReturn gpio_command_parse(FuriString* args, size_t* pin_num, uint8_t* value) { FuriString* pin_name; pin_name = furi_string_alloc(); size_t ws = furi_string_search_char(args, ' '); if(ws == FURI_STRING_FAILURE) { - return ERR_CMD_SYNTAX; + return GpioParseReturnCmdSyntaxError; } furi_string_set_n(pin_name, args, 0, ws); @@ -78,7 +83,7 @@ static GpioParseError gpio_command_parse(FuriString* args, size_t* pin_num, uint if(!pin_name_to_int(pin_name, pin_num)) { furi_string_free(pin_name); - return ERR_PIN; + return GpioParseReturnPinError; } furi_string_free(pin_name); @@ -88,10 +93,10 @@ static GpioParseError gpio_command_parse(FuriString* args, size_t* pin_num, uint } else if(!furi_string_cmp(args, "1")) { *value = 1; } else { - return ERR_VALUE; + return GpioParseReturnValueError; } - return OK; + return GpioParseReturnOk; } void cli_command_gpio_mode(Cli* cli, FuriString* args, void* context) { @@ -101,15 +106,15 @@ void cli_command_gpio_mode(Cli* cli, FuriString* args, void* context) { size_t num = 0; uint8_t value = 255; - GpioParseError err = gpio_command_parse(args, &num, &value); + GpioParseReturn err = gpio_command_parse(args, &num, &value); - if(ERR_CMD_SYNTAX == err) { + if(err == GpioParseReturnCmdSyntaxError) { cli_print_usage("gpio mode", " <0|1>", furi_string_get_cstr(args)); return; - } else if(ERR_PIN == err) { + } else if(err == GpioParseReturnPinError) { gpio_print_pins(); return; - } else if(ERR_VALUE == err) { + } else if(err == GpioParseReturnValueError) { printf("Value is invalid. Enter 1 for input or 0 for output"); return; } @@ -161,15 +166,15 @@ void cli_command_gpio_set(Cli* cli, FuriString* args, void* context) { size_t num = 0; uint8_t value = 0; - GpioParseError err = gpio_command_parse(args, &num, &value); + GpioParseReturn err = gpio_command_parse(args, &num, &value); - if(ERR_CMD_SYNTAX == err) { + if(err == GpioParseReturnCmdSyntaxError) { cli_print_usage("gpio set", " <0|1>", furi_string_get_cstr(args)); return; - } else if(ERR_PIN == err) { + } else if(err == GpioParseReturnPinError) { gpio_print_pins(); return; - } else if(ERR_VALUE == err) { + } else if(err == GpioParseReturnValueError) { printf("Value is invalid. Enter 1 for high or 0 for low"); return; } diff --git a/applications/services/cli/cli_vcp.c b/applications/services/cli/cli_vcp.c index 1e27e185b..94b82950d 100644 --- a/applications/services/cli/cli_vcp.c +++ b/applications/services/cli/cli_vcp.c @@ -103,7 +103,7 @@ static int32_t vcp_worker(void* context) { while(1) { uint32_t flags = furi_thread_flags_wait(VCP_THREAD_FLAG_ALL, FuriFlagWaitAny, FuriWaitForever); - furi_assert((flags & FuriFlagError) == 0); + furi_assert(!(flags & FuriFlagError)); // VCP session opened if(flags & VcpEvtConnect) { @@ -303,7 +303,7 @@ static void vcp_on_cdc_control_line(void* context, uint8_t state) { 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) == 0); + furi_check(!(ret & FuriFlagError)); } static void vcp_on_cdc_tx_complete(void* context) { diff --git a/applications/services/crypto/crypto_cli.c b/applications/services/crypto/crypto_cli.c index a64a3ad0b..1b26ba9fb 100644 --- a/applications/services/crypto/crypto_cli.c +++ b/applications/services/crypto/crypto_cli.c @@ -167,7 +167,7 @@ void crypto_cli_decrypt(Cli* cli, FuriString* args) { void crypto_cli_has_key(Cli* cli, FuriString* args) { UNUSED(cli); int key_slot = 0; - uint8_t iv[16]; + uint8_t iv[16] = {0}; do { if(!args_read_int_and_trim(args, &key_slot) || !(key_slot > 0 && key_slot <= 100)) { @@ -249,7 +249,7 @@ void crypto_cli_store_key(Cli* cli, FuriString* args) { } if(key_slot > 0) { - uint8_t iv[16]; + uint8_t iv[16] = {0}; if(key_slot > 1) { if(!furi_hal_crypto_store_load_key(key_slot - 1, iv)) { printf( diff --git a/applications/services/gui/gui.c b/applications/services/gui/gui.c index 2d06d70c7..9f6ebcd76 100644 --- a/applications/services/gui/gui.c +++ b/applications/services/gui/gui.c @@ -436,7 +436,7 @@ void gui_add_framebuffer_callback(Gui* gui, GuiCanvasCommitCallback callback, vo const CanvasCallbackPair p = {callback, context}; gui_lock(gui); - furi_assert(CanvasCallbackPairArray_count(gui->canvas_callback_pair, p) == 0); + furi_assert(!CanvasCallbackPairArray_count(gui->canvas_callback_pair, p)); CanvasCallbackPairArray_push_back(gui->canvas_callback_pair, p); gui_unlock(gui); diff --git a/applications/services/gui/modules/file_browser_worker.c b/applications/services/gui/modules/file_browser_worker.c index fdaf8273f..b9b2b2d8f 100644 --- a/applications/services/gui/modules/file_browser_worker.c +++ b/applications/services/gui/modules/file_browser_worker.c @@ -359,7 +359,7 @@ static int32_t browser_worker(void* context) { BrowserWorker* file_browser_worker_alloc(FuriString* path, const char* filter_ext, bool skip_assets) { - BrowserWorker* browser = malloc(sizeof(BrowserWorker)); + BrowserWorker* browser = malloc(sizeof(BrowserWorker)); //-V773 idx_last_array_init(browser->idx_last); diff --git a/applications/services/gui/modules/widget_elements/widget_element_button.c b/applications/services/gui/modules/widget_elements/widget_element_button.c index be33b1897..e3267058e 100644 --- a/applications/services/gui/modules/widget_elements/widget_element_button.c +++ b/applications/services/gui/modules/widget_elements/widget_element_button.c @@ -60,7 +60,7 @@ WidgetElement* widget_element_button_create( ButtonCallback callback, void* context) { // Allocate and init model - GuiButtonModel* model = malloc(sizeof(GuiButtonModel)); + GuiButtonModel* model = malloc(sizeof(GuiButtonModel)); //-V773 model->button_type = button_type; model->callback = callback; model->context = context; diff --git a/applications/services/gui/view_dispatcher.c b/applications/services/gui/view_dispatcher.c index 8de452d20..4034cc0b4 100644 --- a/applications/services/gui/view_dispatcher.c +++ b/applications/services/gui/view_dispatcher.c @@ -23,7 +23,7 @@ void view_dispatcher_free(ViewDispatcher* view_dispatcher) { gui_remove_view_port(view_dispatcher->gui, view_dispatcher->view_port); } // Crash if not all views were freed - furi_assert(ViewDict_size(view_dispatcher->views) == 0); + furi_assert(!ViewDict_size(view_dispatcher->views)); ViewDict_clear(view_dispatcher->views); // Free ViewPort @@ -157,7 +157,7 @@ void view_dispatcher_remove_view(ViewDispatcher* view_dispatcher, uint32_t view_ view_dispatcher->ongoing_input_view = NULL; } // Remove view - ViewDict_erase(view_dispatcher->views, view_id); + furi_check(ViewDict_erase(view_dispatcher->views, view_id)); view_set_update_callback(view, NULL); view_set_update_callback_context(view, NULL); diff --git a/applications/services/loader/loader.c b/applications/services/loader/loader.c index bc456536c..62dbad95f 100644 --- a/applications/services/loader/loader.c +++ b/applications/services/loader/loader.c @@ -269,7 +269,7 @@ static void loader_thread_state_callback(FuriThreadState thread_state, void* con event.type = LoaderEventTypeApplicationStarted; furi_pubsub_publish(loader_instance->pubsub, &event); - if(!loader_instance->application->flags & FlipperApplicationFlagInsomniaSafe) { + if(!(loader_instance->application->flags & FlipperApplicationFlagInsomniaSafe)) { furi_hal_power_insomnia_enter(); } } else if(thread_state == FuriThreadStateStopped) { @@ -284,7 +284,7 @@ static void loader_thread_state_callback(FuriThreadState thread_state, void* con loader_instance->application_arguments = NULL; } - if(!loader_instance->application->flags & FlipperApplicationFlagInsomniaSafe) { + if(!(loader_instance->application->flags & FlipperApplicationFlagInsomniaSafe)) { furi_hal_power_insomnia_exit(); } loader_unlock(instance); diff --git a/applications/services/storage/storage_cli.c b/applications/services/storage/storage_cli.c index 0efcb5e2c..c83f16499 100644 --- a/applications/services/storage/storage_cli.c +++ b/applications/services/storage/storage_cli.c @@ -275,7 +275,7 @@ static void storage_cli_read_chunks(Cli* cli, FuriString* path, FuriString* args uint32_t buffer_size; int parsed_count = sscanf(furi_string_get_cstr(args), "%lu", &buffer_size); - if(parsed_count == EOF || parsed_count != 1) { + if(parsed_count != 1) { storage_cli_print_usage(); } else if(storage_file_open(file, furi_string_get_cstr(path), FSAM_READ, FSOM_OPEN_EXISTING)) { uint64_t file_size = storage_file_size(file); @@ -315,7 +315,7 @@ static void storage_cli_write_chunk(Cli* cli, FuriString* path, FuriString* args uint32_t buffer_size; int parsed_count = sscanf(furi_string_get_cstr(args), "%lu", &buffer_size); - if(parsed_count == EOF || parsed_count != 1) { + if(parsed_count != 1) { storage_cli_print_usage(); } else { if(storage_file_open(file, furi_string_get_cstr(path), FSAM_WRITE, FSOM_OPEN_APPEND)) { diff --git a/applications/services/storage/storage_external_api.c b/applications/services/storage/storage_external_api.c index 6854ef7f3..2c3a7bfc9 100644 --- a/applications/services/storage/storage_external_api.c +++ b/applications/services/storage/storage_external_api.c @@ -545,8 +545,8 @@ static FS_Error FS_Error storage_common_merge(Storage* storage, const char* old_path, const char* new_path) { FS_Error error; - const char* new_path_tmp; - FuriString* new_path_next; + const char* new_path_tmp = NULL; + FuriString* new_path_next = NULL; new_path_next = furi_string_alloc(); FileInfo fileinfo; diff --git a/firmware/targets/f7/ble_glue/gap.c b/firmware/targets/f7/ble_glue/gap.c index aa8cd2c90..cf27df3a3 100644 --- a/firmware/targets/f7/ble_glue/gap.c +++ b/firmware/targets/f7/ble_glue/gap.c @@ -179,7 +179,7 @@ SVCCTL_UserEvtFlowStatus_t SVCCTL_App_Notification(void* pckt) { case EVT_BLUE_GAP_PASS_KEY_REQUEST: { // Generate random PIN code - uint32_t pin = rand() % 999999; + uint32_t pin = rand() % 999999; //-V1064 aci_gap_pass_key_resp(gap->service.connection_handle, pin); if(furi_hal_rtc_is_flag_set(FuriHalRtcFlagLock)) { FURI_LOG_I(TAG, "Pass key request event. Pin: ******"); @@ -478,7 +478,6 @@ bool gap_init(GapConfig* config, GapEventCallback on_event_cb, void* context) { gap = malloc(sizeof(Gap)); gap->config = config; - srand(DWT->CYCCNT); // Create advertising timer gap->advertise_timer = furi_timer_alloc(gap_advetise_timer_callback, FuriTimerTypeOnce, NULL); // Initialization of GATT & GAP layer diff --git a/firmware/targets/f7/furi_hal/furi_hal_crypto.c b/firmware/targets/f7/furi_hal/furi_hal_crypto.c index dbd8c58c2..e0ed3ab9b 100644 --- a/firmware/targets/f7/furi_hal/furi_hal_crypto.c +++ b/firmware/targets/f7/furi_hal/furi_hal_crypto.c @@ -91,7 +91,7 @@ bool furi_hal_crypto_verify_key(uint8_t key_slot) { uint8_t keys_nb = 0; uint8_t valid_keys_nb = 0; uint8_t last_valid_slot = ENCLAVE_FACTORY_KEY_SLOTS; - uint8_t empty_iv[16]; + uint8_t empty_iv[16] = {0}; furi_hal_crypto_verify_enclave(&keys_nb, &valid_keys_nb); if(key_slot <= ENCLAVE_FACTORY_KEY_SLOTS) { // It's a factory key if(key_slot > keys_nb) return false; diff --git a/furi/core/check.h b/furi/core/check.h index 78efc1451..192c5260e 100644 --- a/furi/core/check.h +++ b/furi/core/check.h @@ -46,7 +46,7 @@ FURI_NORETURN void __furi_halt(); /** Check condition and crash if check failed */ #define furi_check(__e) \ do { \ - if((__e) == 0) { \ + if(!(__e)) { \ furi_crash("furi_check failed\r\n"); \ } \ } while(0) @@ -55,7 +55,7 @@ FURI_NORETURN void __furi_halt(); #ifdef FURI_DEBUG #define furi_assert(__e) \ do { \ - if((__e) == 0) { \ + if(!(__e)) { \ furi_crash("furi_assert failed\r\n"); \ } \ } while(0) diff --git a/furi/core/event_flag.c b/furi/core/event_flag.c index 5d2a49910..07dd30a16 100644 --- a/furi/core/event_flag.c +++ b/furi/core/event_flag.c @@ -25,7 +25,7 @@ uint32_t furi_event_flag_set(FuriEventFlag* instance, uint32_t flags) { uint32_t rflags; BaseType_t yield; - if(FURI_IS_IRQ_MODE() != 0U) { + if(FURI_IS_IRQ_MODE()) { yield = pdFALSE; if(xEventGroupSetBitsFromISR(hEventGroup, (EventBits_t)flags, &yield) == pdFAIL) { rflags = (uint32_t)FuriStatusErrorResource; @@ -48,7 +48,7 @@ uint32_t furi_event_flag_clear(FuriEventFlag* instance, uint32_t flags) { EventGroupHandle_t hEventGroup = (EventGroupHandle_t)instance; uint32_t rflags; - if(FURI_IS_IRQ_MODE() != 0U) { + if(FURI_IS_IRQ_MODE()) { rflags = xEventGroupGetBitsFromISR(hEventGroup); if(xEventGroupClearBitsFromISR(hEventGroup, (EventBits_t)flags) == pdFAIL) { @@ -73,7 +73,7 @@ uint32_t furi_event_flag_get(FuriEventFlag* instance) { EventGroupHandle_t hEventGroup = (EventGroupHandle_t)instance; uint32_t rflags; - if(FURI_IS_IRQ_MODE() != 0U) { + if(FURI_IS_IRQ_MODE()) { rflags = xEventGroupGetBitsFromISR(hEventGroup); } else { rflags = xEventGroupGetBits(hEventGroup); diff --git a/furi/core/memmgr_heap.c b/furi/core/memmgr_heap.c index 32b2875cc..ac51b4a20 100644 --- a/furi/core/memmgr_heap.c +++ b/furi/core/memmgr_heap.c @@ -150,8 +150,7 @@ void memmgr_heap_disable_thread_trace(FuriThreadId thread_id) { vTaskSuspendAll(); { memmgr_heap_thread_trace_depth++; - furi_check(MemmgrHeapThreadDict_get(memmgr_heap_thread_dict, (uint32_t)thread_id) != NULL); - MemmgrHeapThreadDict_erase(memmgr_heap_thread_dict, (uint32_t)thread_id); + furi_check(MemmgrHeapThreadDict_erase(memmgr_heap_thread_dict, (uint32_t)thread_id)); memmgr_heap_thread_trace_depth--; } (void)xTaskResumeAll(); @@ -212,7 +211,8 @@ static inline void traceFREE(void* pointer, size_t size) { MemmgrHeapAllocDict_t* alloc_dict = MemmgrHeapThreadDict_get(memmgr_heap_thread_dict, (uint32_t)thread_id); if(alloc_dict) { - MemmgrHeapAllocDict_erase(*alloc_dict, (uint32_t)pointer); + // In some cases thread may want to release memory that was not allocated by it + (void)MemmgrHeapAllocDict_erase(*alloc_dict, (uint32_t)pointer); } memmgr_heap_thread_trace_depth--; } diff --git a/furi/core/mutex.c b/furi/core/mutex.c index 78ea05196..ab66b0f18 100644 --- a/furi/core/mutex.c +++ b/furi/core/mutex.c @@ -45,7 +45,7 @@ FuriStatus furi_mutex_acquire(FuriMutex* instance, uint32_t timeout) { stat = FuriStatusOk; - if(FURI_IS_IRQ_MODE() != 0U) { + if(FURI_IS_IRQ_MODE()) { stat = FuriStatusErrorISR; } else if(hMutex == NULL) { stat = FuriStatusErrorParameter; @@ -85,7 +85,7 @@ FuriStatus furi_mutex_release(FuriMutex* instance) { stat = FuriStatusOk; - if(FURI_IS_IRQ_MODE() != 0U) { + if(FURI_IS_IRQ_MODE()) { stat = FuriStatusErrorISR; } else if(hMutex == NULL) { stat = FuriStatusErrorParameter; @@ -111,7 +111,7 @@ FuriThreadId furi_mutex_get_owner(FuriMutex* instance) { hMutex = (SemaphoreHandle_t)((uint32_t)instance & ~1U); - if((FURI_IS_IRQ_MODE() != 0U) || (hMutex == NULL)) { + if((FURI_IS_IRQ_MODE()) || (hMutex == NULL)) { owner = 0; } else { owner = (FuriThreadId)xSemaphoreGetMutexHolder(hMutex); diff --git a/furi/core/semaphore.c b/furi/core/semaphore.c index a204cbe6e..8c99bfc54 100644 --- a/furi/core/semaphore.c +++ b/furi/core/semaphore.c @@ -45,7 +45,7 @@ FuriStatus furi_semaphore_acquire(FuriSemaphore* instance, uint32_t timeout) { stat = FuriStatusOk; - if(FURI_IS_IRQ_MODE() != 0U) { + if(FURI_IS_IRQ_MODE()) { if(timeout != 0U) { stat = FuriStatusErrorParameter; } else { @@ -80,7 +80,7 @@ FuriStatus furi_semaphore_release(FuriSemaphore* instance) { stat = FuriStatusOk; - if(FURI_IS_IRQ_MODE() != 0U) { + if(FURI_IS_IRQ_MODE()) { yield = pdFALSE; if(xSemaphoreGiveFromISR(hSemaphore, &yield) != pdTRUE) { @@ -104,7 +104,7 @@ uint32_t furi_semaphore_get_count(FuriSemaphore* instance) { SemaphoreHandle_t hSemaphore = (SemaphoreHandle_t)instance; uint32_t count; - if(FURI_IS_IRQ_MODE() != 0U) { + if(FURI_IS_IRQ_MODE()) { count = (uint32_t)uxSemaphoreGetCountFromISR(hSemaphore); } else { count = (uint32_t)uxSemaphoreGetCount(hSemaphore); diff --git a/furi/core/stream_buffer.c b/furi/core/stream_buffer.c index b9d0629fe..2df84fa5b 100644 --- a/furi/core/stream_buffer.c +++ b/furi/core/stream_buffer.c @@ -23,7 +23,7 @@ size_t furi_stream_buffer_send( uint32_t timeout) { size_t ret; - if(FURI_IS_IRQ_MODE() != 0U) { + if(FURI_IS_IRQ_MODE()) { BaseType_t yield; ret = xStreamBufferSendFromISR(stream_buffer, data, length, &yield); portYIELD_FROM_ISR(yield); @@ -41,7 +41,7 @@ size_t furi_stream_buffer_receive( uint32_t timeout) { size_t ret; - if(FURI_IS_IRQ_MODE() != 0U) { + if(FURI_IS_IRQ_MODE()) { BaseType_t yield; ret = xStreamBufferReceiveFromISR(stream_buffer, data, length, &yield); portYIELD_FROM_ISR(yield); diff --git a/furi/core/string.c b/furi/core/string.c index 099f70c11..901b1f625 100644 --- a/furi/core/string.c +++ b/furi/core/string.c @@ -29,13 +29,13 @@ FuriString* furi_string_alloc() { } FuriString* furi_string_alloc_set(const FuriString* s) { - FuriString* string = malloc(sizeof(FuriString)); + FuriString* string = malloc(sizeof(FuriString)); //-V773 string_init_set(string->string, s->string); return string; } FuriString* furi_string_alloc_set_str(const char cstr[]) { - FuriString* string = malloc(sizeof(FuriString)); + FuriString* string = malloc(sizeof(FuriString)); //-V773 string_init_set(string->string, cstr); return string; } diff --git a/furi/core/thread.c b/furi/core/thread.c index 508146f63..157e022e9 100644 --- a/furi/core/thread.c +++ b/furi/core/thread.c @@ -50,6 +50,7 @@ static int32_t __furi_thread_stdout_flush(FuriThread* thread); __attribute__((__noreturn__)) void furi_thread_catch() { asm volatile("nop"); // extra magic furi_crash("You are doing it wrong"); + __builtin_unreachable(); } static void furi_thread_set_state(FuriThread* thread, FuriThreadState state) { @@ -112,6 +113,12 @@ FuriThread* furi_thread_alloc() { FuriThread* thread = malloc(sizeof(FuriThread)); thread->output.buffer = furi_string_alloc(); thread->is_service = false; + + if(furi_thread_get_current_id()) { + FuriThread* parent = pvTaskGetThreadLocalStoragePointer(NULL, 0); + if(parent) thread->heap_trace_enabled = parent->heap_trace_enabled; + } + return thread; } diff --git a/lib/flipper_format/flipper_format_stream.c b/lib/flipper_format/flipper_format_stream.c index 41934a3b1..9cce95d47 100644 --- a/lib/flipper_format/flipper_format_stream.c +++ b/lib/flipper_format/flipper_format_stream.c @@ -313,7 +313,7 @@ bool flipper_format_stream_write_value_line(Stream* stream, FlipperStreamWriteDa furi_crash("Unknown FF type"); } - if((size_t)(i + 1) < write_data->data_size) { + if(((size_t)i + 1) < write_data->data_size) { furi_string_cat(value, " "); } diff --git a/lib/infrared/encoder_decoder/common/infrared_common_decoder.c b/lib/infrared/encoder_decoder/common/infrared_common_decoder.c index bff4c73db..7f1c3a4fd 100644 --- a/lib/infrared/encoder_decoder/common/infrared_common_decoder.c +++ b/lib/infrared/encoder_decoder/common/infrared_common_decoder.c @@ -85,8 +85,8 @@ static InfraredStatus infrared_common_decode_bits(InfraredCommonDecoder* decoder if(timings->min_split_time && !level) { if(timing > timings->min_split_time) { /* long low timing - check if we're ready for any of protocol modification */ - for(size_t i = 0; decoder->protocol->databit_len[i] && - (i < COUNT_OF(decoder->protocol->databit_len)); + for(size_t i = 0; i < COUNT_OF(decoder->protocol->databit_len) && + decoder->protocol->databit_len[i]; ++i) { if(decoder->protocol->databit_len[i] == decoder->databit_cnt) { return InfraredStatusReady; @@ -199,7 +199,7 @@ InfraredMessage* infrared_common_decoder_check_ready(InfraredCommonDecoder* deco bool found_length = false; for(size_t i = 0; - decoder->protocol->databit_len[i] && (i < COUNT_OF(decoder->protocol->databit_len)); + i < COUNT_OF(decoder->protocol->databit_len) && decoder->protocol->databit_len[i]; ++i) { if(decoder->protocol->databit_len[i] == decoder->databit_cnt) { found_length = true; diff --git a/lib/lfrfid/lfrfid_worker.c b/lib/lfrfid/lfrfid_worker.c index 8b4f8b6a9..f3e37fa3c 100644 --- a/lib/lfrfid/lfrfid_worker.c +++ b/lib/lfrfid/lfrfid_worker.c @@ -140,9 +140,8 @@ size_t lfrfid_worker_dict_get_data_size(LFRFIDWorker* worker, LFRFIDProtocol pro static int32_t lfrfid_worker_thread(void* thread_context) { LFRFIDWorker* worker = thread_context; - bool running = true; - while(running) { + while(true) { uint32_t flags = furi_thread_flags_wait(LFRFIDEventAll, FuriFlagWaitAny, FuriWaitForever); if(flags != FuriFlagErrorTimeout) { // stop thread diff --git a/lib/print/printf_tiny.c b/lib/print/printf_tiny.c index 0db11922d..6e47f6528 100644 --- a/lib/print/printf_tiny.c +++ b/lib/print/printf_tiny.c @@ -541,7 +541,7 @@ static size_t _etoa( exp2 = (int)(expval * 3.321928094887362 + 0.5); const double z = expval * 2.302585092994046 - exp2 * 0.6931471805599453; const double z2 = z * z; - conv.U = (uint64_t)(exp2 + 1023) << 52U; + conv.U = ((uint64_t)exp2 + 1023) << 52U; // compute exp(z) using continued fractions, see https://en.wikipedia.org/wiki/Exponential_function#Continued_fractions_for_ex conv.F *= 1 + 2 * z / (2 - z + (z2 / (6 + (z2 / (10 + z2 / 14))))); // correct for rounding errors diff --git a/lib/subghz/protocols/power_smart.c b/lib/subghz/protocols/power_smart.c index 63ca78711..1e8d10e95 100644 --- a/lib/subghz/protocols/power_smart.c +++ b/lib/subghz/protocols/power_smart.c @@ -312,7 +312,6 @@ void subghz_protocol_decoder_power_smart_feed( if((instance->decoder.decode_data & POWER_SMART_PACKET_HEADER_MASK) == POWER_SMART_PACKET_HEADER) { if(subghz_protocol_power_smart_chek_valid(instance->decoder.decode_data)) { - instance->decoder.decode_data = instance->decoder.decode_data; instance->generic.data = instance->decoder.decode_data; instance->generic.data_count_bit = subghz_protocol_power_smart_const.min_count_bit_for_found; diff --git a/lib/subghz/subghz_keystore.c b/lib/subghz/subghz_keystore.c index 1b4b3b716..e06bd9796 100644 --- a/lib/subghz/subghz_keystore.c +++ b/lib/subghz/subghz_keystore.c @@ -464,7 +464,7 @@ bool subghz_keystore_raw_encrypted_save( } stream_write_cstring(output_stream, encrypted_line); - } while(ret > 0 && result); + } while(result); flipper_format_free(output_flipper_format); diff --git a/lib/toolbox/random_name.c b/lib/toolbox/random_name.c index 5a5374398..64e712c7c 100644 --- a/lib/toolbox/random_name.c +++ b/lib/toolbox/random_name.c @@ -5,12 +5,6 @@ #include void set_random_name(char* name, uint8_t max_name_size) { - static bool rand_generator_inited = false; - - if(!rand_generator_inited) { - srand(DWT->CYCCNT); - rand_generator_inited = true; - } const char* prefix[] = { "ancient", "hollow", "strange", "disappeared", "unknown", "unthinkable", "unnamable", "nameless", "my", "concealed", From 0a86ad43ca2ccf00f3f71e288bb7552b48e14dce Mon Sep 17 00:00:00 2001 From: hedger Date: Sun, 6 Nov 2022 11:39:50 +0400 Subject: [PATCH 27/49] fbt: fix for launch_app (#1978) --- site_scons/extapps.scons | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/site_scons/extapps.scons b/site_scons/extapps.scons index 3d2a98788..670d71fd0 100644 --- a/site_scons/extapps.scons +++ b/site_scons/extapps.scons @@ -98,14 +98,14 @@ extapps.applications = appenv["EXT_APPS"] extapps.resources_dist = appenv.FapDist(appenv.Dir("#/assets/resources/apps"), []) if appsrc := appenv.subst("$APPSRC"): - app_manifest, fap_file, app_validator = appenv.GetExtAppFromPath(appsrc) + app_artifacts = appenv.GetExtAppFromPath(appsrc) appenv.PhonyTarget( "launch_app", '${PYTHON3} "${APP_RUN_SCRIPT}" "${SOURCE}" --fap_dst_dir "/ext/apps/${FAP_CATEGORY}"', - source=fap_file, - FAP_CATEGORY=app_manifest.fap_category, + source=app_artifacts.compact, + FAP_CATEGORY=app_artifacts.app.fap_category, ) - appenv.Alias("launch_app", app_validator) + appenv.Alias("launch_app", app_artifacts.validator) # SDK management From 65005e71d2524e2c82d9bb6631f655b3c442d2e5 Mon Sep 17 00:00:00 2001 From: Skorpionm <85568270+Skorpionm@users.noreply.github.com> Date: Sun, 6 Nov 2022 21:30:02 +0400 Subject: [PATCH 28/49] WS: fix show negative temperature (#1980) --- .../plugins/weather_station/protocols/acurite_592txr.c | 5 ++--- .../plugins/weather_station/protocols/acurite_606tx.c | 5 ++--- .../plugins/weather_station/protocols/ambient_weather.c | 5 ++--- applications/plugins/weather_station/protocols/gt_wt_03.c | 5 ++--- applications/plugins/weather_station/protocols/infactory.c | 5 ++--- .../weather_station/protocols/lacrosse_tx141thbv2.c | 5 ++--- applications/plugins/weather_station/protocols/nexus_th.c | 5 ++--- .../plugins/weather_station/protocols/thermopro_tx4.c | 5 ++--- .../weather_station/views/weather_station_receiver_info.c | 7 +------ 9 files changed, 17 insertions(+), 30 deletions(-) diff --git a/applications/plugins/weather_station/protocols/acurite_592txr.c b/applications/plugins/weather_station/protocols/acurite_592txr.c index 4d7f59544..5384a3c91 100644 --- a/applications/plugins/weather_station/protocols/acurite_592txr.c +++ b/applications/plugins/weather_station/protocols/acurite_592txr.c @@ -293,7 +293,7 @@ void ws_protocol_decoder_acurite_592txr_get_string(void* context, FuriString* ou "%s %dbit\r\n" "Key:0x%lX%08lX\r\n" "Sn:0x%lX Ch:%d Bat:%d\r\n" - "Temp:%d.%d C Hum:%d%%", + "Temp:%3.1f C Hum:%d%%", instance->generic.protocol_name, instance->generic.data_count_bit, (uint32_t)(instance->generic.data >> 32), @@ -301,7 +301,6 @@ void ws_protocol_decoder_acurite_592txr_get_string(void* context, FuriString* ou instance->generic.id, instance->generic.channel, instance->generic.battery_low, - (int16_t)instance->generic.temp, - abs(((int16_t)(instance->generic.temp * 10) - (((int16_t)instance->generic.temp) * 10))), + (double)instance->generic.temp, instance->generic.humidity); } diff --git a/applications/plugins/weather_station/protocols/acurite_606tx.c b/applications/plugins/weather_station/protocols/acurite_606tx.c index 3c9144057..4cb5d18b8 100644 --- a/applications/plugins/weather_station/protocols/acurite_606tx.c +++ b/applications/plugins/weather_station/protocols/acurite_606tx.c @@ -234,7 +234,7 @@ void ws_protocol_decoder_acurite_606tx_get_string(void* context, FuriString* out "%s %dbit\r\n" "Key:0x%lX%08lX\r\n" "Sn:0x%lX Ch:%d Bat:%d\r\n" - "Temp:%d.%d C Hum:%d%%", + "Temp:%3.1f C Hum:%d%%", instance->generic.protocol_name, instance->generic.data_count_bit, (uint32_t)(instance->generic.data >> 32), @@ -242,7 +242,6 @@ void ws_protocol_decoder_acurite_606tx_get_string(void* context, FuriString* out instance->generic.id, instance->generic.channel, instance->generic.battery_low, - (int16_t)instance->generic.temp, - abs(((int16_t)(instance->generic.temp * 10) - (((int16_t)instance->generic.temp) * 10))), + (double)instance->generic.temp, instance->generic.humidity); } diff --git a/applications/plugins/weather_station/protocols/ambient_weather.c b/applications/plugins/weather_station/protocols/ambient_weather.c index 5fede684e..5ae22b790 100644 --- a/applications/plugins/weather_station/protocols/ambient_weather.c +++ b/applications/plugins/weather_station/protocols/ambient_weather.c @@ -263,7 +263,7 @@ void ws_protocol_decoder_ambient_weather_get_string(void* context, FuriString* o "%s %dbit\r\n" "Key:0x%lX%08lX\r\n" "Sn:0x%lX Ch:%d Bat:%d\r\n" - "Temp:%d.%d C Hum:%d%%", + "Temp:%3.1f C Hum:%d%%", instance->generic.protocol_name, instance->generic.data_count_bit, (uint32_t)(instance->generic.data >> 32), @@ -271,7 +271,6 @@ void ws_protocol_decoder_ambient_weather_get_string(void* context, FuriString* o instance->generic.id, instance->generic.channel, instance->generic.battery_low, - (int16_t)instance->generic.temp, - abs(((int16_t)(instance->generic.temp * 10) - (((int16_t)instance->generic.temp) * 10))), + (double)instance->generic.temp, instance->generic.humidity); } diff --git a/applications/plugins/weather_station/protocols/gt_wt_03.c b/applications/plugins/weather_station/protocols/gt_wt_03.c index 04bca9ac1..7831cf069 100644 --- a/applications/plugins/weather_station/protocols/gt_wt_03.c +++ b/applications/plugins/weather_station/protocols/gt_wt_03.c @@ -327,7 +327,7 @@ void ws_protocol_decoder_gt_wt_03_get_string(void* context, FuriString* output) "%s %dbit\r\n" "Key:0x%lX%08lX\r\n" "Sn:0x%lX Ch:%d Bat:%d\r\n" - "Temp:%d.%d C Hum:%d%%", + "Temp:%3.1f C Hum:%d%%", instance->generic.protocol_name, instance->generic.data_count_bit, (uint32_t)(instance->generic.data >> 32), @@ -335,7 +335,6 @@ void ws_protocol_decoder_gt_wt_03_get_string(void* context, FuriString* output) instance->generic.id, instance->generic.channel, instance->generic.battery_low, - (int16_t)instance->generic.temp, - abs(((int16_t)(instance->generic.temp * 10) - (((int16_t)instance->generic.temp) * 10))), + (double)instance->generic.temp, instance->generic.humidity); } diff --git a/applications/plugins/weather_station/protocols/infactory.c b/applications/plugins/weather_station/protocols/infactory.c index b08a4e9db..2d444d981 100644 --- a/applications/plugins/weather_station/protocols/infactory.c +++ b/applications/plugins/weather_station/protocols/infactory.c @@ -283,7 +283,7 @@ void ws_protocol_decoder_infactory_get_string(void* context, FuriString* output) "%s %dbit\r\n" "Key:0x%lX%08lX\r\n" "Sn:0x%lX Ch:%d Bat:%d\r\n" - "Temp:%d.%d C Hum:%d%%", + "Temp:%3.1f C Hum:%d%%", instance->generic.protocol_name, instance->generic.data_count_bit, (uint32_t)(instance->generic.data >> 32), @@ -291,7 +291,6 @@ void ws_protocol_decoder_infactory_get_string(void* context, FuriString* output) instance->generic.id, instance->generic.channel, instance->generic.battery_low, - (int16_t)instance->generic.temp, - abs(((int16_t)(instance->generic.temp * 10) - (((int16_t)instance->generic.temp) * 10))), + (double)instance->generic.temp, instance->generic.humidity); } diff --git a/applications/plugins/weather_station/protocols/lacrosse_tx141thbv2.c b/applications/plugins/weather_station/protocols/lacrosse_tx141thbv2.c index d4b89be87..e4b612250 100644 --- a/applications/plugins/weather_station/protocols/lacrosse_tx141thbv2.c +++ b/applications/plugins/weather_station/protocols/lacrosse_tx141thbv2.c @@ -284,7 +284,7 @@ void ws_protocol_decoder_lacrosse_tx141thbv2_get_string(void* context, FuriStrin "%s %dbit\r\n" "Key:0x%lX%08lX\r\n" "Sn:0x%lX Ch:%d Bat:%d\r\n" - "Temp:%d.%d C Hum:%d%%", + "Temp:%3.1f C Hum:%d%%", instance->generic.protocol_name, instance->generic.data_count_bit, (uint32_t)(instance->generic.data >> 32), @@ -292,7 +292,6 @@ void ws_protocol_decoder_lacrosse_tx141thbv2_get_string(void* context, FuriStrin instance->generic.id, instance->generic.channel, instance->generic.battery_low, - (int16_t)instance->generic.temp, - abs(((int16_t)(instance->generic.temp * 10) - (((int16_t)instance->generic.temp) * 10))), + (double)instance->generic.temp, instance->generic.humidity); } diff --git a/applications/plugins/weather_station/protocols/nexus_th.c b/applications/plugins/weather_station/protocols/nexus_th.c index c3d823eda..7d4a77aea 100644 --- a/applications/plugins/weather_station/protocols/nexus_th.c +++ b/applications/plugins/weather_station/protocols/nexus_th.c @@ -247,7 +247,7 @@ void ws_protocol_decoder_nexus_th_get_string(void* context, FuriString* output) "%s %dbit\r\n" "Key:0x%lX%08lX\r\n" "Sn:0x%lX Ch:%d Bat:%d\r\n" - "Temp:%d.%d C Hum:%d%%", + "Temp:%3.1f C Hum:%d%%", instance->generic.protocol_name, instance->generic.data_count_bit, (uint32_t)(instance->generic.data >> 32), @@ -255,7 +255,6 @@ void ws_protocol_decoder_nexus_th_get_string(void* context, FuriString* output) instance->generic.id, instance->generic.channel, instance->generic.battery_low, - (int16_t)instance->generic.temp, - abs(((int16_t)(instance->generic.temp * 10) - (((int16_t)instance->generic.temp) * 10))), + (double)instance->generic.temp, instance->generic.humidity); } diff --git a/applications/plugins/weather_station/protocols/thermopro_tx4.c b/applications/plugins/weather_station/protocols/thermopro_tx4.c index 9a2eacb2f..0882bc33d 100644 --- a/applications/plugins/weather_station/protocols/thermopro_tx4.c +++ b/applications/plugins/weather_station/protocols/thermopro_tx4.c @@ -246,7 +246,7 @@ void ws_protocol_decoder_thermopro_tx4_get_string(void* context, FuriString* out "%s %dbit\r\n" "Key:0x%lX%08lX\r\n" "Sn:0x%lX Ch:%d Bat:%d\r\n" - "Temp:%d.%d C Hum:%d%%", + "Temp:%3.1f C Hum:%d%%", instance->generic.protocol_name, instance->generic.data_count_bit, (uint32_t)(instance->generic.data >> 32), @@ -254,7 +254,6 @@ void ws_protocol_decoder_thermopro_tx4_get_string(void* context, FuriString* out instance->generic.id, instance->generic.channel, instance->generic.battery_low, - (int16_t)instance->generic.temp, - abs(((int16_t)(instance->generic.temp * 10) - (((int16_t)instance->generic.temp) * 10))), + (double)instance->generic.temp, instance->generic.humidity); } diff --git a/applications/plugins/weather_station/views/weather_station_receiver_info.c b/applications/plugins/weather_station/views/weather_station_receiver_info.c index 34ec122d1..49b447f10 100644 --- a/applications/plugins/weather_station/views/weather_station_receiver_info.c +++ b/applications/plugins/weather_station/views/weather_station_receiver_info.c @@ -75,12 +75,7 @@ void ws_view_receiver_info_draw(Canvas* canvas, WSReceiverInfoModel* model) { if(model->generic->temp != WS_NO_TEMPERATURE) { canvas_draw_icon(canvas, 18, 42, &I_Therm_7x16); - snprintf( - buffer, - sizeof(buffer), - "%3.2d.%d C", - (int16_t)model->generic->temp, - abs(((int16_t)(model->generic->temp * 10) - (((int16_t)model->generic->temp) * 10)))); + snprintf(buffer, sizeof(buffer), "%3.1f C", (double)model->generic->temp); canvas_draw_str_aligned(canvas, 63, 46, AlignRight, AlignTop, buffer); canvas_draw_circle(canvas, 55, 45, 1); } From aa2ecbe80f77d6b7132cd4fdb83eb49fba297b31 Mon Sep 17 00:00:00 2001 From: Samuel Stauffer Date: Mon, 7 Nov 2022 06:38:04 -0800 Subject: [PATCH 29/49] infrared: add Kaseikyo IR protocol (#1965) * infrared: add Kaseikyo IR protocol Add Kaseikyo IR protocol support. This protocol is also called the Panasonic protocol and is used by a number of manufacturers including Denon. The protocol includes a vendor field and a number of fields that are vendor specific. To support the format of address+command used by flipper the vendor+genre1+genre2+id fields are encoded into the address while the data is used for the command. There are older versions of the protocol that used a reverse bit order that are not supported. Protocol information: - https://github.com/Arduino-IRremote/Arduino-IRremote/blob/master/src/ir_Kaseikyo.hpp - http://www.hifi-remote.com/johnsfine/DecodeIR.html#Kaseikyo - https://www.denon.com/-/media/files/documentmaster/denonna/avr-x3700h_avc-x3700h_ir_code_v01_04062020.doc * Format and add unit test to Kaseikyo IR codec. Co-authored-by: Georgii Surkov <37121527+gsurkov@users.noreply.github.com> --- .../debug/unit_tests/infrared/infrared_test.c | 12 ++ .../unit_tests/infrared/test_kaseikyo.irtest | 105 ++++++++++++++++++ .../common/infrared_common_protocol_defs.c | 23 ++++ lib/infrared/encoder_decoder/infrared.c | 14 +++ lib/infrared/encoder_decoder/infrared.h | 1 + .../infrared_protocol_defs_i.h | 51 +++++++++ .../kaseikyo/infrared_decoder_kaseikyo.c | 54 +++++++++ .../kaseikyo/infrared_encoder_kaseikyo.c | 45 ++++++++ .../kaseikyo/infrared_kaseikyo_spec.c | 17 +++ 9 files changed, 322 insertions(+) create mode 100644 assets/unit_tests/infrared/test_kaseikyo.irtest create mode 100644 lib/infrared/encoder_decoder/kaseikyo/infrared_decoder_kaseikyo.c create mode 100644 lib/infrared/encoder_decoder/kaseikyo/infrared_encoder_kaseikyo.c create mode 100644 lib/infrared/encoder_decoder/kaseikyo/infrared_kaseikyo_spec.c diff --git a/applications/debug/unit_tests/infrared/infrared_test.c b/applications/debug/unit_tests/infrared/infrared_test.c index 8879c8fc8..2bcb95da8 100644 --- a/applications/debug/unit_tests/infrared/infrared_test.c +++ b/applications/debug/unit_tests/infrared/infrared_test.c @@ -424,6 +424,7 @@ MU_TEST(infrared_test_decoder_mixed) { infrared_test_run_decoder(InfraredProtocolRC5, 5); infrared_test_run_decoder(InfraredProtocolSamsung32, 1); infrared_test_run_decoder(InfraredProtocolSIRC, 3); + infrared_test_run_decoder(InfraredProtocolKaseikyo, 1); } MU_TEST(infrared_test_decoder_nec) { @@ -489,6 +490,15 @@ MU_TEST(infrared_test_encoder_rc6) { infrared_test_run_encoder(InfraredProtocolRC6, 1); } +MU_TEST(infrared_test_decoder_kaseikyo) { + infrared_test_run_decoder(InfraredProtocolKaseikyo, 1); + infrared_test_run_decoder(InfraredProtocolKaseikyo, 2); + infrared_test_run_decoder(InfraredProtocolKaseikyo, 3); + infrared_test_run_decoder(InfraredProtocolKaseikyo, 4); + infrared_test_run_decoder(InfraredProtocolKaseikyo, 5); + infrared_test_run_decoder(InfraredProtocolKaseikyo, 6); +} + MU_TEST(infrared_test_encoder_decoder_all) { infrared_test_run_encoder_decoder(InfraredProtocolNEC, 1); infrared_test_run_encoder_decoder(InfraredProtocolNECext, 1); @@ -498,6 +508,7 @@ MU_TEST(infrared_test_encoder_decoder_all) { infrared_test_run_encoder_decoder(InfraredProtocolRC6, 1); infrared_test_run_encoder_decoder(InfraredProtocolRC5, 1); infrared_test_run_encoder_decoder(InfraredProtocolSIRC, 1); + infrared_test_run_encoder_decoder(InfraredProtocolKaseikyo, 1); } MU_TEST_SUITE(infrared_test) { @@ -515,6 +526,7 @@ MU_TEST_SUITE(infrared_test) { MU_RUN_TEST(infrared_test_decoder_nec); MU_RUN_TEST(infrared_test_decoder_samsung32); MU_RUN_TEST(infrared_test_decoder_necext1); + MU_RUN_TEST(infrared_test_decoder_kaseikyo); MU_RUN_TEST(infrared_test_decoder_mixed); MU_RUN_TEST(infrared_test_encoder_decoder_all); } diff --git a/assets/unit_tests/infrared/test_kaseikyo.irtest b/assets/unit_tests/infrared/test_kaseikyo.irtest new file mode 100644 index 000000000..d0142fecd --- /dev/null +++ b/assets/unit_tests/infrared/test_kaseikyo.irtest @@ -0,0 +1,105 @@ +Filetype: IR tests file +Version: 1 +# +name: decoder_input1 +type: raw +data: 1000000 3363 1685 407 436 411 432 415 1240 434 410 437 1245 439 404 433 1249 435 408 439 431 406 1249 435 435 412 405 442 1241 433 1249 435 408 439 405 442 428 409 434 413 430 407 411 436 433 414 429 408 1248 436 407 440 1243 441 428 409 434 413 431 406 1249 435 1248 436 406 441 1242 442 1240 434 409 438 431 416 428 409 408 439 430 407 411 436 407 440 429 408 436 411 432 415 402 435 1247 437 1245 439 1243 441 1238 436 +# +name: decoder_expected1 +type: parsed_array +count: 1 +# +protocol: Kaseikyo +address: 41 54 32 00 +command: 1B 00 00 00 +repeat: false +# +name: decoder_input2 +type: raw +data: 1000000 3365 1683 409 434 413 431 406 1276 408 435 412 1270 414 429 408 1248 436 434 413 430 407 1275 409 434 413 431 406 1276 408 1248 436 433 414 430 407 437 410 433 414 429 408 436 411 432 415 428 409 1246 438 432 415 1267 407 437 410 433 414 429 408 436 411 432 415 1266 408 1250 434 1248 436 432 415 429 408 435 412 432 415 428 409 434 413 430 407 437 410 433 414 429 408 436 411 432 415 428 409 435 412 1240 434 +# +name: decoder_expected2 +type: parsed_array +count: 1 +# +protocol: Kaseikyo +address: 41 54 32 00 +command: 1C 00 00 00 +repeat: false +# +name: decoder_input3 +type: raw +data: 1000000 3361 1661 442 427 410 434 413 1243 441 428 409 1247 437 432 415 1241 433 410 437 407 440 1242 432 437 410 407 440 1242 442 1241 433 436 411 407 440 430 407 436 411 406 441 402 435 435 412 431 416 1240 434 410 437 1245 439 404 433 411 436 407 440 403 434 436 411 432 415 429 408 1249 435 1247 437 1245 439 430 407 1250 434 434 413 404 433 438 409 434 413 1243 441 1241 433 410 437 1245 439 430 407 1250 434 432 415 +# +name: decoder_expected3 +type: parsed_array +count: 1 +# +protocol: Kaseikyo +address: 41 54 32 00 +command: 70 01 00 00 +repeat: false +# +name: decoder_input4 +type: raw +data: 1000000 3365 1656 436 406 441 402 435 1248 436 406 441 1242 432 410 437 1246 438 404 433 410 437 1246 438 404 433 437 410 1245 491 1190 442 401 436 435 412 431 416 427 410 433 414 429 408 435 412 431 416 1240 434 435 412 1244 440 1241 433 436 411 433 414 402 435 409 438 405 442 402 435 1247 437 1244 440 1241 433 437 410 1245 439 430 407 410 437 406 441 402 435 409 438 1243 441 402 435 1247 437 406 441 1240 434 433 414 +# +name: decoder_expected4 +type: parsed_array +count: 1 +# +protocol: Kaseikyo +address: 43 54 32 00 +command: 70 01 00 00 +repeat: false +# +name: decoder_input5 +type: raw +data: 1000000 3357 1665 438 431 416 428 409 1247 437 432 415 1241 433 436 411 1245 439 430 407 436 411 1245 439 430 407 437 410 1246 438 1243 441 428 409 436 411 432 415 428 409 435 412 431 416 427 410 434 413 1243 441 427 410 1247 437 1245 439 430 407 437 410 1246 438 1244 440 429 408 1250 434 1248 488 355 440 429 408 436 411 432 415 428 408 435 412 431 416 428 409 1247 437 432 415 428 409 1248 436 1246 490 1191 441 1240 434 +# +name: decoder_expected5 +type: parsed_array +count: 1 +# +protocol: Kaseikyo +address: 43 54 32 00 +command: 1B 00 00 00 +repeat: false +# +name: decoder_input6 +type: raw +data: 1000000 3358 1664 439 430 407 437 410 1245 439 430 407 1250 434 434 413 1243 441 428 409 435 412 1244 440 428 409 435 412 1244 440 1242 432 437 410 434 413 430 407 436 411 432 415 428 409 435 412 431 416 1240 434 435 412 1244 440 1242 442 427 410 434 413 1243 441 427 409 1247 437 433 414 429 408 436 411 432 415 428 409 435 412 431 416 427 410 434 413 1243 441 1240 486 357 438 432 415 1240 434 436 411 432 415 425 412 +# +name: decoder_expected6 +type: parsed_array +count: 1 +# +protocol: Kaseikyo +address: 43 54 32 00 +command: 05 00 00 00 +repeat: false +# +name: encoder_decoder_input1 +type: parsed_array +count: 4 +# +protocol: Kaseikyo +address: 41 54 32 00 +command: 1B 00 00 00 +repeat: false +# +protocol: Kaseikyo +address: 41 54 32 00 +command: 70 01 00 00 +repeat: false +# +protocol: Kaseikyo +address: 43 54 32 00 +command: 05 00 00 00 +repeat: false +# +protocol: Kaseikyo +address: 43 54 32 00 +command: 1B 00 00 00 +repeat: false +# diff --git a/lib/infrared/encoder_decoder/common/infrared_common_protocol_defs.c b/lib/infrared/encoder_decoder/common/infrared_common_protocol_defs.c index e8f7664a7..3dd26e9d8 100644 --- a/lib/infrared/encoder_decoder/common/infrared_common_protocol_defs.c +++ b/lib/infrared/encoder_decoder/common/infrared_common_protocol_defs.c @@ -115,3 +115,26 @@ const InfraredCommonProtocolSpec protocol_sirc = { .decode_repeat = NULL, .encode_repeat = infrared_encoder_sirc_encode_repeat, }; + +const InfraredCommonProtocolSpec protocol_kaseikyo = { + .timings = + { + .preamble_mark = INFRARED_KASEIKYO_PREAMBLE_MARK, + .preamble_space = INFRARED_KASEIKYO_PREAMBLE_SPACE, + .bit1_mark = INFRARED_KASEIKYO_BIT1_MARK, + .bit1_space = INFRARED_KASEIKYO_BIT1_SPACE, + .bit0_mark = INFRARED_KASEIKYO_BIT0_MARK, + .bit0_space = INFRARED_KASEIKYO_BIT0_SPACE, + .preamble_tolerance = INFRARED_KASEIKYO_PREAMBLE_TOLERANCE, + .bit_tolerance = INFRARED_KASEIKYO_BIT_TOLERANCE, + .silence_time = INFRARED_KASEIKYO_SILENCE, + .min_split_time = INFRARED_KASEIKYO_MIN_SPLIT_TIME, + }, + .databit_len[0] = 48, + .no_stop_bit = false, + .decode = infrared_common_decode_pdwm, + .encode = infrared_common_encode_pdwm, + .interpret = infrared_decoder_kaseikyo_interpret, + .decode_repeat = NULL, + .encode_repeat = NULL, +}; diff --git a/lib/infrared/encoder_decoder/infrared.c b/lib/infrared/encoder_decoder/infrared.c index 71bd6bb6d..2c5ef0fff 100644 --- a/lib/infrared/encoder_decoder/infrared.c +++ b/lib/infrared/encoder_decoder/infrared.c @@ -110,6 +110,20 @@ static const InfraredEncoderDecoder infrared_encoder_decoder[] = { .free = infrared_encoder_sirc_free}, .get_protocol_spec = infrared_sirc_get_spec, }, + { + .decoder = + {.alloc = infrared_decoder_kaseikyo_alloc, + .decode = infrared_decoder_kaseikyo_decode, + .reset = infrared_decoder_kaseikyo_reset, + .check_ready = infrared_decoder_kaseikyo_check_ready, + .free = infrared_decoder_kaseikyo_free}, + .encoder = + {.alloc = infrared_encoder_kaseikyo_alloc, + .encode = infrared_encoder_kaseikyo_encode, + .reset = infrared_encoder_kaseikyo_reset, + .free = infrared_encoder_kaseikyo_free}, + .get_protocol_spec = infrared_kaseikyo_get_spec, + }, }; static int infrared_find_index_by_protocol(InfraredProtocol protocol); diff --git a/lib/infrared/encoder_decoder/infrared.h b/lib/infrared/encoder_decoder/infrared.h index 945913000..086950f1e 100644 --- a/lib/infrared/encoder_decoder/infrared.h +++ b/lib/infrared/encoder_decoder/infrared.h @@ -31,6 +31,7 @@ typedef enum { InfraredProtocolSIRC, InfraredProtocolSIRC15, InfraredProtocolSIRC20, + InfraredProtocolKaseikyo, InfraredProtocolMAX, } InfraredProtocol; diff --git a/lib/infrared/encoder_decoder/infrared_protocol_defs_i.h b/lib/infrared/encoder_decoder/infrared_protocol_defs_i.h index 575961f13..6146f7b4e 100644 --- a/lib/infrared/encoder_decoder/infrared_protocol_defs_i.h +++ b/lib/infrared/encoder_decoder/infrared_protocol_defs_i.h @@ -267,3 +267,54 @@ InfraredStatus infrared_encoder_sirc_encode_repeat( bool* level); extern const InfraredCommonProtocolSpec protocol_sirc; + +/*************************************************************************************************** +* Kaseikyo protocol description +* https://github.com/Arduino-IRremote/Arduino-IRremote/blob/master/src/ir_Kaseikyo.hpp +**************************************************************************************************** +* Preamble Preamble Pulse Distance/Width Pause Preamble Preamble +* mark space Modulation up to period repeat repeat +* mark space +* +* 3360 1665 48 bit ...130000 3456 1728 +* __________ _ _ _ _ _ _ _ _ _ _ _ _ _ ___________ +* ____ __________ _ _ _ __ __ __ _ _ __ __ _ _ ________________ ___________ +* +***************************************************************************************************/ + +#define INFRARED_KASEIKYO_UNIT 432 +#define INFRARED_KASEIKYO_PREAMBLE_MARK (8 * INFRARED_KASEIKYO_UNIT) +#define INFRARED_KASEIKYO_PREAMBLE_SPACE (4 * INFRARED_KASEIKYO_UNIT) +#define INFRARED_KASEIKYO_BIT1_MARK INFRARED_KASEIKYO_UNIT +#define INFRARED_KASEIKYO_BIT1_SPACE (3 * INFRARED_KASEIKYO_UNIT) +#define INFRARED_KASEIKYO_BIT0_MARK INFRARED_KASEIKYO_UNIT +#define INFRARED_KASEIKYO_BIT0_SPACE INFRARED_KASEIKYO_UNIT +#define INFRARED_KASEIKYO_REPEAT_PERIOD 130000 +#define INFRARED_KASEIKYO_SILENCE INFRARED_KASEIKYO_REPEAT_PERIOD +#define INFRARED_KASEIKYO_MIN_SPLIT_TIME INFRARED_KASEIKYO_REPEAT_PAUSE_MIN +#define INFRARED_KASEIKYO_REPEAT_PAUSE_MIN 4000 +#define INFRARED_KASEIKYO_REPEAT_PAUSE_MAX 150000 +#define INFRARED_KASEIKYO_REPEAT_MARK INFRARED_KASEIKYO_PREAMBLE_MARK +#define INFRARED_KASEIKYO_REPEAT_SPACE (INFRARED_KASEIKYO_REPEAT_PERIOD - 56000) +#define INFRARED_KASEIKYO_PREAMBLE_TOLERANCE 200 // us +#define INFRARED_KASEIKYO_BIT_TOLERANCE 120 // us + +void* infrared_decoder_kaseikyo_alloc(void); +void infrared_decoder_kaseikyo_reset(void* decoder); +void infrared_decoder_kaseikyo_free(void* decoder); +InfraredMessage* infrared_decoder_kaseikyo_check_ready(void* decoder); +InfraredMessage* infrared_decoder_kaseikyo_decode(void* decoder, bool level, uint32_t duration); +void* infrared_encoder_kaseikyo_alloc(void); +InfraredStatus + infrared_encoder_kaseikyo_encode(void* encoder_ptr, uint32_t* duration, bool* level); +void infrared_encoder_kaseikyo_reset(void* encoder_ptr, const InfraredMessage* message); +void infrared_encoder_kaseikyo_free(void* encoder_ptr); +bool infrared_decoder_kaseikyo_interpret(InfraredCommonDecoder* decoder); +InfraredStatus infrared_decoder_kaseikyo_decode_repeat(InfraredCommonDecoder* decoder); +InfraredStatus infrared_encoder_kaseikyo_encode_repeat( + InfraredCommonEncoder* encoder, + uint32_t* duration, + bool* level); +const InfraredProtocolSpecification* infrared_kaseikyo_get_spec(InfraredProtocol protocol); + +extern const InfraredCommonProtocolSpec protocol_kaseikyo; diff --git a/lib/infrared/encoder_decoder/kaseikyo/infrared_decoder_kaseikyo.c b/lib/infrared/encoder_decoder/kaseikyo/infrared_decoder_kaseikyo.c new file mode 100644 index 000000000..b8db81d7e --- /dev/null +++ b/lib/infrared/encoder_decoder/kaseikyo/infrared_decoder_kaseikyo.c @@ -0,0 +1,54 @@ +#include "infrared.h" +#include "infrared_protocol_defs_i.h" +#include +#include +#include +#include "../infrared_i.h" + +InfraredMessage* infrared_decoder_kaseikyo_check_ready(void* ctx) { + return infrared_common_decoder_check_ready(ctx); +} + +bool infrared_decoder_kaseikyo_interpret(InfraredCommonDecoder* decoder) { + furi_assert(decoder); + + bool result = false; + uint16_t vendor_id = ((uint16_t)(decoder->data[1]) << 8) | (uint16_t)decoder->data[0]; + uint8_t vendor_parity = decoder->data[2] & 0x0f; + uint8_t genre1 = decoder->data[2] >> 4; + uint8_t genre2 = decoder->data[3] & 0x0f; + uint16_t data = (uint16_t)(decoder->data[3] >> 4) | ((uint16_t)(decoder->data[4] & 0x3f) << 4); + uint8_t id = decoder->data[4] >> 6; + uint8_t parity = decoder->data[5]; + + uint8_t vendor_parity_check = decoder->data[0] ^ decoder->data[1]; + vendor_parity_check = (vendor_parity_check & 0xf) ^ (vendor_parity_check >> 4); + uint8_t parity_check = decoder->data[2] ^ decoder->data[3] ^ decoder->data[4]; + + if(vendor_parity == vendor_parity_check && parity == parity_check) { + decoder->message.command = (uint32_t)data; + decoder->message.address = ((uint32_t)id << 24) | ((uint32_t)vendor_id << 8) | + ((uint32_t)genre1 << 4) | (uint32_t)genre2; + decoder->message.protocol = InfraredProtocolKaseikyo; + decoder->message.repeat = false; + result = true; + } + + return result; +} + +void* infrared_decoder_kaseikyo_alloc(void) { + return infrared_common_decoder_alloc(&protocol_kaseikyo); +} + +InfraredMessage* infrared_decoder_kaseikyo_decode(void* decoder, bool level, uint32_t duration) { + return infrared_common_decode(decoder, level, duration); +} + +void infrared_decoder_kaseikyo_free(void* decoder) { + infrared_common_decoder_free(decoder); +} + +void infrared_decoder_kaseikyo_reset(void* decoder) { + infrared_common_decoder_reset(decoder); +} diff --git a/lib/infrared/encoder_decoder/kaseikyo/infrared_encoder_kaseikyo.c b/lib/infrared/encoder_decoder/kaseikyo/infrared_encoder_kaseikyo.c new file mode 100644 index 000000000..5814c7255 --- /dev/null +++ b/lib/infrared/encoder_decoder/kaseikyo/infrared_encoder_kaseikyo.c @@ -0,0 +1,45 @@ +#include +#include "common/infrared_common_i.h" +#include +#include "../infrared_i.h" +#include "infrared_protocol_defs_i.h" +#include + +void infrared_encoder_kaseikyo_reset(void* encoder_ptr, const InfraredMessage* message) { + furi_assert(encoder_ptr); + + InfraredCommonEncoder* encoder = encoder_ptr; + infrared_common_encoder_reset(encoder); + + uint32_t address = message->address; + uint16_t command = message->command; + + uint8_t id = (address >> 24) & 3; + uint16_t vendor_id = (address >> 8) & 0xffff; + uint8_t genre1 = (address >> 4) & 0xf; + uint8_t genre2 = address & 0xf; + + encoder->data[0] = (uint8_t)(vendor_id & 0xff); + encoder->data[1] = (uint8_t)(vendor_id >> 8); + uint8_t vendor_parity = encoder->data[0] ^ encoder->data[1]; + vendor_parity = (vendor_parity & 0xf) ^ (vendor_parity >> 4); + encoder->data[2] = (vendor_parity & 0xf) | (genre1 << 4); + encoder->data[3] = (genre2 & 0xf) | ((uint8_t)(command & 0xf) << 4); + encoder->data[4] = (id << 6) | (uint8_t)(command >> 4); + encoder->data[5] = encoder->data[2] ^ encoder->data[3] ^ encoder->data[4]; + + encoder->bits_to_encode = encoder->protocol->databit_len[0]; +} + +void* infrared_encoder_kaseikyo_alloc(void) { + return infrared_common_encoder_alloc(&protocol_kaseikyo); +} + +void infrared_encoder_kaseikyo_free(void* encoder_ptr) { + infrared_common_encoder_free(encoder_ptr); +} + +InfraredStatus + infrared_encoder_kaseikyo_encode(void* encoder_ptr, uint32_t* duration, bool* level) { + return infrared_common_encode(encoder_ptr, duration, level); +} diff --git a/lib/infrared/encoder_decoder/kaseikyo/infrared_kaseikyo_spec.c b/lib/infrared/encoder_decoder/kaseikyo/infrared_kaseikyo_spec.c new file mode 100644 index 000000000..87c86c7b3 --- /dev/null +++ b/lib/infrared/encoder_decoder/kaseikyo/infrared_kaseikyo_spec.c @@ -0,0 +1,17 @@ +#include "../infrared_i.h" +#include "infrared_protocol_defs_i.h" + +static const InfraredProtocolSpecification infrared_kaseikyo_protocol_specification = { + .name = "Kaseikyo", + .address_length = 26, + .command_length = 10, + .frequency = INFRARED_COMMON_CARRIER_FREQUENCY, + .duty_cycle = INFRARED_COMMON_DUTY_CYCLE, +}; + +const InfraredProtocolSpecification* infrared_kaseikyo_get_spec(InfraredProtocol protocol) { + if(protocol == InfraredProtocolKaseikyo) + return &infrared_kaseikyo_protocol_specification; + else + return NULL; +} From 2d6c2886ae429b634736fe9da6f48caef155d2e3 Mon Sep 17 00:00:00 2001 From: hedger Date: Mon, 7 Nov 2022 18:54:41 +0400 Subject: [PATCH 30/49] fbt: compile_db fixes (#1981) * fbt: forked compilation_db tool * fbt: fixes for static analysis * pvs-studio: ignoring more generic warnings * fbt: util: added extract_abs_dir * vscode: added fap-set-debug-elf-root for debug configurations --- .github/workflows/pvs_studio.yml | 2 +- .gitignore | 2 + .pvsconfig | 1 + .vscode/example/launch.json | 4 + SConstruct | 8 +- firmware.scons | 5 +- scripts/fbt/util.py | 12 +- scripts/fbt_tools/compilation_db.py | 278 ++++++++++++++++++++++++++++ scripts/fbt_tools/fbt_extapps.py | 3 +- 9 files changed, 307 insertions(+), 8 deletions(-) create mode 100644 scripts/fbt_tools/compilation_db.py diff --git a/.github/workflows/pvs_studio.yml b/.github/workflows/pvs_studio.yml index f28fad20d..9de493a44 100644 --- a/.github/workflows/pvs_studio.yml +++ b/.github/workflows/pvs_studio.yml @@ -57,7 +57,7 @@ jobs: - name: 'Generate compile_comands.json' run: | - FBT_TOOLCHAIN_PATH=/runner/_work ./fbt COMPACT=1 version_json proto_ver icons firmware_cdb dolphin_internal dolphin_blocking + FBT_TOOLCHAIN_PATH=/runner/_work ./fbt COMPACT=1 version_json proto_ver icons firmware_cdb dolphin_internal dolphin_blocking _fap_icons - name: 'Static code analysis' run: | diff --git a/.gitignore b/.gitignore index 38a31bf01..61c594ed1 100644 --- a/.gitignore +++ b/.gitignore @@ -54,3 +54,5 @@ openocd.log # PVS Studio temporary files .PVS-Studio/ PVS-Studio.log + +.gdbinit diff --git a/.pvsconfig b/.pvsconfig index d17eaa5a0..5f1ffb7cb 100644 --- a/.pvsconfig +++ b/.pvsconfig @@ -5,6 +5,7 @@ //-V:BPTREE_DEF2:779,1086,557,773,512 //-V:DICT_DEF2:779,524,776,760,1044,1001,729,590,568,747,685 //-V:ALGO_DEF:1048,747,1044 +//-V:TUPLE_DEF2:524,590,1001,760 # Non-severe malloc/null pointer deref warnings //-V::522:2,3 diff --git a/.vscode/example/launch.json b/.vscode/example/launch.json index c8b0c601d..5c46d3979 100644 --- a/.vscode/example/launch.json +++ b/.vscode/example/launch.json @@ -38,6 +38,7 @@ "postAttachCommands": [ // "compare-sections", "source debug/flipperapps.py", + "fap-set-debug-elf-root build/latest/.extapps", // "source debug/FreeRTOS/FreeRTOS.py", // "svd_load debug/STM32WB55_CM4.svd" ] @@ -59,6 +60,7 @@ "set confirm off", "set mem inaccessible-by-default off", "source debug/flipperapps.py", + "fap-set-debug-elf-root build/latest/.extapps", // "compare-sections", ] // "showDevDebugOutput": "raw", @@ -76,6 +78,7 @@ "rtos": "FreeRTOS", "postAttachCommands": [ "source debug/flipperapps.py", + "fap-set-debug-elf-root build/latest/.extapps", ] // "showDevDebugOutput": "raw", }, @@ -95,6 +98,7 @@ ], "postAttachCommands": [ "source debug/flipperapps.py", + "fap-set-debug-elf-root build/latest/.extapps", ], // "showDevDebugOutput": "raw", }, diff --git a/SConstruct b/SConstruct index 67eac3825..3ee89f646 100644 --- a/SConstruct +++ b/SConstruct @@ -200,7 +200,9 @@ firmware_debug = distenv.PhonyTarget( source=firmware_env["FW_ELF"], GDBOPTS="${GDBOPTS_BASE}", GDBREMOTE="${OPENOCD_GDB_PIPE}", - FBT_FAP_DEBUG_ELF_ROOT=firmware_env.subst("$FBT_FAP_DEBUG_ELF_ROOT"), + FBT_FAP_DEBUG_ELF_ROOT=firmware_env.subst("$FBT_FAP_DEBUG_ELF_ROOT").replace( + "\\", "/" + ), ) distenv.Depends(firmware_debug, firmware_flash) @@ -210,7 +212,9 @@ distenv.PhonyTarget( source=firmware_env["FW_ELF"], GDBOPTS="${GDBOPTS_BASE} ${GDBOPTS_BLACKMAGIC}", GDBREMOTE="${BLACKMAGIC_ADDR}", - FBT_FAP_DEBUG_ELF_ROOT=firmware_env.subst("$FBT_FAP_DEBUG_ELF_ROOT"), + FBT_FAP_DEBUG_ELF_ROOT=firmware_env.subst("$FBT_FAP_DEBUG_ELF_ROOT").replace( + "\\", "/" + ), ) # Debug alien elf diff --git a/firmware.scons b/firmware.scons index 6feb73ac3..6a9237477 100644 --- a/firmware.scons +++ b/firmware.scons @@ -264,7 +264,10 @@ fw_artifacts.extend( fwcdb = fwenv.CompilationDatabase() # without filtering, both updater & firmware commands would be generated in same file -fwenv.Replace(COMPILATIONDB_PATH_FILTER=fwenv.subst("*${FW_FLAVOR}*")) +fwenv.Replace( + COMPILATIONDB_PATH_FILTER=fwenv.subst("*${FW_FLAVOR}*"), + COMPILATIONDB_SRCPATH_FILTER="*.c*", +) AlwaysBuild(fwcdb) Precious(fwcdb) NoClean(fwcdb) diff --git a/scripts/fbt/util.py b/scripts/fbt/util.py index f5404458e..b8e9c5928 100644 --- a/scripts/fbt/util.py +++ b/scripts/fbt/util.py @@ -43,12 +43,18 @@ def single_quote(arg_list): return " ".join(f"'{arg}'" if " " in arg else str(arg) for arg in arg_list) -def extract_abs_dir_path(node): +def extract_abs_dir(node): if isinstance(node, SCons.Node.FS.EntryProxy): node = node.get() for repo_dir in node.get_all_rdirs(): if os.path.exists(repo_dir.abspath): - return repo_dir.abspath + return repo_dir - raise StopError(f"Can't find absolute path for {node.name}") + +def extract_abs_dir_path(node): + abs_dir_node = extract_abs_dir(node) + if abs_dir_node is None: + raise StopError(f"Can't find absolute path for {node.name}") + + return abs_dir_node.abspath diff --git a/scripts/fbt_tools/compilation_db.py b/scripts/fbt_tools/compilation_db.py new file mode 100644 index 000000000..17ff6aaa3 --- /dev/null +++ b/scripts/fbt_tools/compilation_db.py @@ -0,0 +1,278 @@ +""" +Implements the ability for SCons to emit a compilation database for the MongoDB project. See +http://clang.llvm.org/docs/JSONCompilationDatabase.html for details on what a compilation +database is, and why you might want one. The only user visible entry point here is +'env.CompilationDatabase'. This method takes an optional 'target' to name the file that +should hold the compilation database, otherwise, the file defaults to compile_commands.json, +which is the name that most clang tools search for by default. +""" + +# Copyright 2020 MongoDB Inc. +# +# 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 the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 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. +# + +import json +import itertools +import fnmatch +import SCons + +from SCons.Tool.cxx import CXXSuffixes +from SCons.Tool.cc import CSuffixes +from SCons.Tool.asm import ASSuffixes, ASPPSuffixes + +# TODO: Is there a better way to do this than this global? Right now this exists so that the +# emitter we add can record all of the things it emits, so that the scanner for the top level +# compilation database can access the complete list, and also so that the writer has easy +# access to write all of the files. But it seems clunky. How can the emitter and the scanner +# communicate more gracefully? +__COMPILATION_DB_ENTRIES = [] + + +# We make no effort to avoid rebuilding the entries. Someday, perhaps we could and even +# integrate with the cache, but there doesn't seem to be much call for it. +class __CompilationDbNode(SCons.Node.Python.Value): + def __init__(self, value): + SCons.Node.Python.Value.__init__(self, value) + self.Decider(changed_since_last_build_node) + + +def changed_since_last_build_node(child, target, prev_ni, node): + """Dummy decider to force always building""" + return True + + +def make_emit_compilation_DB_entry(comstr): + """ + Effectively this creates a lambda function to capture: + * command line + * source + * target + :param comstr: unevaluated command line + :return: an emitter which has captured the above + """ + user_action = SCons.Action.Action(comstr) + + def emit_compilation_db_entry(target, source, env): + """ + This emitter will be added to each c/c++ object build to capture the info needed + for clang tools + :param target: target node(s) + :param source: source node(s) + :param env: Environment for use building this node + :return: target(s), source(s) + """ + + dbtarget = __CompilationDbNode(source) + + entry = env.__COMPILATIONDB_Entry( + target=dbtarget, + source=[], + __COMPILATIONDB_UOUTPUT=target, + __COMPILATIONDB_USOURCE=source, + __COMPILATIONDB_UACTION=user_action, + __COMPILATIONDB_ENV=env, + ) + + # TODO: Technically, these next two lines should not be required: it should be fine to + # cache the entries. However, they don't seem to update properly. Since they are quick + # to re-generate disable caching and sidestep this problem. + env.AlwaysBuild(entry) + env.NoCache(entry) + + __COMPILATION_DB_ENTRIES.append(dbtarget) + + return target, source + + return emit_compilation_db_entry + + +def compilation_db_entry_action(target, source, env, **kw): + """ + Create a dictionary with evaluated command line, target, source + and store that info as an attribute on the target + (Which has been stored in __COMPILATION_DB_ENTRIES array + :param target: target node(s) + :param source: source node(s) + :param env: Environment for use building this node + :param kw: + :return: None + """ + + command = env["__COMPILATIONDB_UACTION"].strfunction( + target=env["__COMPILATIONDB_UOUTPUT"], + source=env["__COMPILATIONDB_USOURCE"], + env=env["__COMPILATIONDB_ENV"], + ) + + entry = { + "directory": env.Dir("#").abspath, + "command": command, + "file": env["__COMPILATIONDB_USOURCE"][0], + "output": env["__COMPILATIONDB_UOUTPUT"][0], + } + + target[0].write(entry) + + +def write_compilation_db(target, source, env): + entries = [] + + use_abspath = env["COMPILATIONDB_USE_ABSPATH"] in [True, 1, "True", "true"] + use_path_filter = env.subst("$COMPILATIONDB_PATH_FILTER") + use_srcpath_filter = env.subst("$COMPILATIONDB_SRCPATH_FILTER") + + for s in __COMPILATION_DB_ENTRIES: + entry = s.read() + source_file = entry["file"] + output_file = entry["output"] + + if source_file.rfile().srcnode().exists(): + source_file = source_file.rfile().srcnode() + + if use_abspath: + source_file = source_file.abspath + output_file = output_file.abspath + else: + source_file = source_file.path + output_file = output_file.path + + # print("output_file, path_filter", output_file, use_path_filter) + if use_path_filter and not fnmatch.fnmatch(output_file, use_path_filter): + continue + + if use_srcpath_filter and not fnmatch.fnmatch(source_file, use_srcpath_filter): + continue + + path_entry = { + "directory": entry["directory"], + "command": entry["command"], + "file": source_file, + "output": output_file, + } + + entries.append(path_entry) + + with open(target[0].path, "w") as output_file: + json.dump( + entries, output_file, sort_keys=True, indent=4, separators=(",", ": ") + ) + + +def scan_compilation_db(node, env, path): + return __COMPILATION_DB_ENTRIES + + +def compilation_db_emitter(target, source, env): + """fix up the source/targets""" + + # Someone called env.CompilationDatabase('my_targetname.json') + if not target and len(source) == 1: + target = source + + # Default target name is compilation_db.json + if not target: + target = [ + "compile_commands.json", + ] + + # No source should have been passed. Drop it. + if source: + source = [] + + return target, source + + +def generate(env, **kwargs): + static_obj, shared_obj = SCons.Tool.createObjBuilders(env) + + env.SetDefault( + COMPILATIONDB_COMSTR=kwargs.get( + "COMPILATIONDB_COMSTR", "Building compilation database $TARGET" + ), + COMPILATIONDB_USE_ABSPATH=False, + COMPILATIONDB_PATH_FILTER="", + COMPILATIONDB_SRCPATH_FILTER="", + ) + + components_by_suffix = itertools.chain( + itertools.product( + CSuffixes, + [ + (static_obj, SCons.Defaults.StaticObjectEmitter, "$CCCOM"), + (shared_obj, SCons.Defaults.SharedObjectEmitter, "$SHCCCOM"), + ], + ), + itertools.product( + CXXSuffixes, + [ + (static_obj, SCons.Defaults.StaticObjectEmitter, "$CXXCOM"), + (shared_obj, SCons.Defaults.SharedObjectEmitter, "$SHCXXCOM"), + ], + ), + itertools.product( + ASSuffixes, + [(static_obj, SCons.Defaults.StaticObjectEmitter, "$ASCOM")], + [(shared_obj, SCons.Defaults.SharedObjectEmitter, "$ASCOM")], + ), + itertools.product( + ASPPSuffixes, + [(static_obj, SCons.Defaults.StaticObjectEmitter, "$ASPPCOM")], + [(shared_obj, SCons.Defaults.SharedObjectEmitter, "$ASPPCOM")], + ), + ) + + for entry in components_by_suffix: + suffix = entry[0] + builder, base_emitter, command = entry[1] + + # Assumes a dictionary emitter + emitter = builder.emitter.get(suffix, False) + if emitter: + # We may not have tools installed which initialize all or any of + # cxx, cc, or assembly. If not skip resetting the respective emitter. + builder.emitter[suffix] = SCons.Builder.ListEmitter( + [ + emitter, + make_emit_compilation_DB_entry(command), + ] + ) + + env.Append( + BUILDERS={ + "__COMPILATIONDB_Entry": SCons.Builder.Builder( + action=SCons.Action.Action(compilation_db_entry_action, None), + ), + "CompilationDatabase": SCons.Builder.Builder( + action=SCons.Action.Action( + write_compilation_db, "$COMPILATIONDB_COMSTR" + ), + target_scanner=SCons.Scanner.Scanner( + function=scan_compilation_db, node_class=None + ), + emitter=compilation_db_emitter, + suffix="json", + ), + } + ) + + +def exists(env): + return True diff --git a/scripts/fbt_tools/fbt_extapps.py b/scripts/fbt_tools/fbt_extapps.py index f1906191b..a4116e513 100644 --- a/scripts/fbt_tools/fbt_extapps.py +++ b/scripts/fbt_tools/fbt_extapps.py @@ -57,11 +57,12 @@ def BuildAppElf(env, app): ) if app.fap_icon_assets: - app_env.CompileIcons( + fap_icons = app_env.CompileIcons( app_env.Dir(app_work_dir), app._appdir.Dir(app.fap_icon_assets), icon_bundle_name=f"{app.appid}_icons", ) + app_env.Alias("_fap_icons", fap_icons) private_libs = [] From 4d11213494be5bb0614f6cae50b7c57c42314a31 Mon Sep 17 00:00:00 2001 From: Sergey Gavrilov Date: Tue, 8 Nov 2022 02:15:58 +1000 Subject: [PATCH 31/49] DAP-Link: show error if usb is locked (#1982) --- applications/plugins/dap_link/dap_link.c | 20 ++++++++++++++++++ .../dap_link/icons/ActiveConnection_50x64.png | Bin 0 -> 3842 bytes 2 files changed, 20 insertions(+) create mode 100644 applications/plugins/dap_link/icons/ActiveConnection_50x64.png diff --git a/applications/plugins/dap_link/dap_link.c b/applications/plugins/dap_link/dap_link.c index 58d032b91..443d77c5e 100644 --- a/applications/plugins/dap_link/dap_link.c +++ b/applications/plugins/dap_link/dap_link.c @@ -13,6 +13,8 @@ #include "dap_config.h" #include "gui/dap_gui.h" #include "usb/dap_v2_usb.h" +#include +#include "dap_link_icons.h" /***************************************************************************/ /****************************** DAP COMMON *********************************/ @@ -495,6 +497,24 @@ DapConfig* dap_app_get_config(DapApp* app) { int32_t dap_link_app(void* p) { UNUSED(p); + if(furi_hal_usb_is_locked()) { + DialogsApp* dialogs = furi_record_open(RECORD_DIALOGS); + DialogMessage* message = dialog_message_alloc(); + dialog_message_set_header(message, "Connection\nis active!", 3, 2, AlignLeft, AlignTop); + dialog_message_set_text( + message, + "Disconnect from\nPC or phone to\nuse this function.", + 3, + 30, + AlignLeft, + AlignTop); + dialog_message_set_icon(message, &I_ActiveConnection_50x64, 78, 0); + dialog_message_show(dialogs, message); + dialog_message_free(message); + furi_record_close(RECORD_DIALOGS); + return -1; + } + // alloc app DapApp* app = dap_app_alloc(); app_handle = app; diff --git a/applications/plugins/dap_link/icons/ActiveConnection_50x64.png b/applications/plugins/dap_link/icons/ActiveConnection_50x64.png new file mode 100644 index 0000000000000000000000000000000000000000..1d7686dddf8a33b724c7528ed36435514b7518b2 GIT binary patch literal 3842 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 Date: Tue, 8 Nov 2022 19:56:49 +0300 Subject: [PATCH 32/49] Update toolchain to version 19. Update codeowners. Fix amap analyze. (#1986) * Up toolchain to 19 * Fix amap_analyse.yml * Github: update codeowners Co-authored-by: Aleksandr Kutuzov --- .github/CODEOWNERS | 9 +++++++-- .github/workflows/amap_analyse.yml | 2 +- scripts/toolchain/fbtenv.cmd | 2 +- scripts/toolchain/fbtenv.sh | 2 +- 4 files changed, 10 insertions(+), 5 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index c9b8ff3f5..6b77482c6 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -18,7 +18,7 @@ /applications/main/gpio/ @skotopes @DrZlo13 @hedger @nminaylov /applications/main/ibutton/ @skotopes @DrZlo13 @hedger @gsurkov /applications/main/infrared/ @skotopes @DrZlo13 @hedger @gsurkov -/applications/main/nfc/ @skotopes @DrZlo13 @hedger @gornekich +/applications/main/nfc/ @skotopes @DrZlo13 @hedger @gornekich @Astrrra /applications/main/subghz/ @skotopes @DrZlo13 @hedger @Skorpionm /applications/main/u2f/ @skotopes @DrZlo13 @hedger @nminaylov @@ -40,6 +40,8 @@ /applications/system/storage_move_to_sd/ @skotopes @DrZlo13 @hedger @nminaylov +/applications/debug/unit_tests/ @skotopes @DrZlo13 @hedger @nminaylov @gornekich @Astrrra @gsurkov @Skorpionm + # Documentation /documentation/ @skotopes @DrZlo13 @hedger @drunkbatya /scripts/toolchain/ @skotopes @DrZlo13 @hedger @drunkbatya @@ -54,6 +56,9 @@ /lib/mbedtls/ @skotopes @DrZlo13 @hedger @nminaylov /lib/micro-ecc/ @skotopes @DrZlo13 @hedger @nminaylov /lib/nanopb/ @skotopes @DrZlo13 @hedger @nminaylov -/lib/nfc/ @skotopes @DrZlo13 @hedger @gornekich +/lib/nfc/ @skotopes @DrZlo13 @hedger @gornekich @Astrrra /lib/one_wire/ @skotopes @DrZlo13 @hedger @gsurkov /lib/subghz/ @skotopes @DrZlo13 @hedger @Skorpionm + +# CI/CD +/.github/workflows/ @skotopes @DrZlo13 @hedger @drunkbatya diff --git a/.github/workflows/amap_analyse.yml b/.github/workflows/amap_analyse.yml index a50c5436f..cfb1eab14 100644 --- a/.github/workflows/amap_analyse.yml +++ b/.github/workflows/amap_analyse.yml @@ -91,7 +91,7 @@ jobs: export RODATA_SIZE="$(get_size ".rodata")" export DATA_SIZE="$(get_size ".data")" export FREE_FLASH_SIZE="$(get_size ".free_flash")" - python3 -m pip install mariadb + python3 -m pip install mariadb==1.1.4 python3 scripts/amap_mariadb_insert.py \ ${{ secrets.AMAP_MARIADB_USER }} \ ${{ secrets.AMAP_MARIADB_PASSWORD }} \ diff --git a/scripts/toolchain/fbtenv.cmd b/scripts/toolchain/fbtenv.cmd index 6e87bf95a..44a2551f7 100644 --- a/scripts/toolchain/fbtenv.cmd +++ b/scripts/toolchain/fbtenv.cmd @@ -13,7 +13,7 @@ if not [%FBT_NOENV%] == [] ( exit /b 0 ) -set "FLIPPER_TOOLCHAIN_VERSION=17" +set "FLIPPER_TOOLCHAIN_VERSION=19" if [%FBT_TOOLCHAIN_ROOT%] == [] ( set "FBT_TOOLCHAIN_ROOT=%FBT_ROOT%\toolchain\x86_64-windows" diff --git a/scripts/toolchain/fbtenv.sh b/scripts/toolchain/fbtenv.sh index d3fdb8cea..852e00394 100755 --- a/scripts/toolchain/fbtenv.sh +++ b/scripts/toolchain/fbtenv.sh @@ -5,7 +5,7 @@ # public variables DEFAULT_SCRIPT_PATH="$(pwd -P)"; SCRIPT_PATH="${SCRIPT_PATH:-$DEFAULT_SCRIPT_PATH}"; -FBT_TOOLCHAIN_VERSION="${FBT_TOOLCHAIN_VERSION:-"17"}"; +FBT_TOOLCHAIN_VERSION="${FBT_TOOLCHAIN_VERSION:-"19"}"; FBT_TOOLCHAIN_PATH="${FBT_TOOLCHAIN_PATH:-$SCRIPT_PATH}"; fbtenv_show_usage() From 328d049b6a56ed3ff9e92ad47a3778381b8db64e Mon Sep 17 00:00:00 2001 From: Samuel Stauffer Date: Tue, 8 Nov 2022 09:07:55 -0800 Subject: [PATCH 33/49] Add Acurite 609TXC protocol to weather station (#1987) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: あく --- .../protocols/acurite_609txc.c | 247 ++++++++++++++++++ .../protocols/acurite_609txc.h | 79 ++++++ .../protocols/protocol_items.c | 1 + .../protocols/protocol_items.h | 1 + 4 files changed, 328 insertions(+) create mode 100644 applications/plugins/weather_station/protocols/acurite_609txc.c create mode 100644 applications/plugins/weather_station/protocols/acurite_609txc.h diff --git a/applications/plugins/weather_station/protocols/acurite_609txc.c b/applications/plugins/weather_station/protocols/acurite_609txc.c new file mode 100644 index 000000000..aeb0785eb --- /dev/null +++ b/applications/plugins/weather_station/protocols/acurite_609txc.c @@ -0,0 +1,247 @@ +#include "acurite_609txc.h" + +#define TAG "WSProtocolAcurite_609TXC" + +/* + * Help + * https://github.com/merbanan/rtl_433/blob/5bef4e43133ac4c0e2d18d36f87c52b4f9458453/src/devices/acurite.c#L216 + * + * 0000 1111 | 0011 0000 | 0101 1100 | 0000 0000 | 1110 0111 + * iiii iiii | buuu tttt | tttt tttt | hhhh hhhh | cccc cccc + * - i: identification; changes on battery switch + * - c: checksum (sum of previous by bytes) + * - u: unknown + * - b: battery low; flag to indicate low battery voltage + * - t: temperature; in °C * 10, 12 bit with complement + * - h: humidity + * + */ + +static const SubGhzBlockConst ws_protocol_acurite_609txc_const = { + .te_short = 500, + .te_long = 1000, + .te_delta = 150, + .min_count_bit_for_found = 40, +}; + +struct WSProtocolDecoderAcurite_609TXC { + SubGhzProtocolDecoderBase base; + + SubGhzBlockDecoder decoder; + WSBlockGeneric generic; +}; + +struct WSProtocolEncoderAcurite_609TXC { + SubGhzProtocolEncoderBase base; + + SubGhzProtocolBlockEncoder encoder; + WSBlockGeneric generic; +}; + +typedef enum { + Acurite_609TXCDecoderStepReset = 0, + Acurite_609TXCDecoderStepSaveDuration, + Acurite_609TXCDecoderStepCheckDuration, +} Acurite_609TXCDecoderStep; + +const SubGhzProtocolDecoder ws_protocol_acurite_609txc_decoder = { + .alloc = ws_protocol_decoder_acurite_609txc_alloc, + .free = ws_protocol_decoder_acurite_609txc_free, + + .feed = ws_protocol_decoder_acurite_609txc_feed, + .reset = ws_protocol_decoder_acurite_609txc_reset, + + .get_hash_data = ws_protocol_decoder_acurite_609txc_get_hash_data, + .serialize = ws_protocol_decoder_acurite_609txc_serialize, + .deserialize = ws_protocol_decoder_acurite_609txc_deserialize, + .get_string = ws_protocol_decoder_acurite_609txc_get_string, +}; + +const SubGhzProtocolEncoder ws_protocol_acurite_609txc_encoder = { + .alloc = NULL, + .free = NULL, + + .deserialize = NULL, + .stop = NULL, + .yield = NULL, +}; + +const SubGhzProtocol ws_protocol_acurite_609txc = { + .name = WS_PROTOCOL_ACURITE_609TXC_NAME, + .type = SubGhzProtocolWeatherStation, + .flag = SubGhzProtocolFlag_433 | SubGhzProtocolFlag_315 | SubGhzProtocolFlag_868 | + SubGhzProtocolFlag_AM | SubGhzProtocolFlag_Decodable, + + .decoder = &ws_protocol_acurite_609txc_decoder, + .encoder = &ws_protocol_acurite_609txc_encoder, +}; + +void* ws_protocol_decoder_acurite_609txc_alloc(SubGhzEnvironment* environment) { + UNUSED(environment); + WSProtocolDecoderAcurite_609TXC* instance = malloc(sizeof(WSProtocolDecoderAcurite_609TXC)); + instance->base.protocol = &ws_protocol_acurite_609txc; + instance->generic.protocol_name = instance->base.protocol->name; + return instance; +} + +void ws_protocol_decoder_acurite_609txc_free(void* context) { + furi_assert(context); + WSProtocolDecoderAcurite_609TXC* instance = context; + free(instance); +} + +void ws_protocol_decoder_acurite_609txc_reset(void* context) { + furi_assert(context); + WSProtocolDecoderAcurite_609TXC* instance = context; + instance->decoder.parser_step = Acurite_609TXCDecoderStepReset; +} + +static bool ws_protocol_acurite_609txc_check(WSProtocolDecoderAcurite_609TXC* instance) { + if(!instance->decoder.decode_data) return false; + uint8_t crc = (uint8_t)(instance->decoder.decode_data >> 32) + + (uint8_t)(instance->decoder.decode_data >> 24) + + (uint8_t)(instance->decoder.decode_data >> 16) + + (uint8_t)(instance->decoder.decode_data >> 8); + return (crc == (instance->decoder.decode_data & 0xFF)); +} + +/** + * Analysis of received data + * @param instance Pointer to a WSBlockGeneric* instance + */ +static void ws_protocol_acurite_609txc_remote_controller(WSBlockGeneric* instance) { + instance->id = (instance->data >> 32) & 0xFF; + instance->battery_low = (instance->data >> 31) & 1; + + instance->channel = WS_NO_CHANNEL; + + // Temperature in Celsius is encoded as a 12 bit integer value + // multiplied by 10 using the 4th - 6th nybbles (bytes 1 & 2) + // negative values are recovered by sign extend from int16_t. + int16_t temp_raw = + (int16_t)(((instance->data >> 12) & 0xf000) | ((instance->data >> 16) << 4)); + instance->temp = (temp_raw >> 4) * 0.1f; + instance->humidity = (instance->data >> 8) & 0xff; + instance->btn = WS_NO_BTN; +} + +void ws_protocol_decoder_acurite_609txc_feed(void* context, bool level, uint32_t duration) { + furi_assert(context); + WSProtocolDecoderAcurite_609TXC* instance = context; + + switch(instance->decoder.parser_step) { + case Acurite_609TXCDecoderStepReset: + if((!level) && (DURATION_DIFF(duration, ws_protocol_acurite_609txc_const.te_short * 17) < + ws_protocol_acurite_609txc_const.te_delta * 8)) { + //Found syncPrefix + instance->decoder.parser_step = Acurite_609TXCDecoderStepSaveDuration; + instance->decoder.decode_data = 0; + instance->decoder.decode_count_bit = 0; + } + break; + + case Acurite_609TXCDecoderStepSaveDuration: + if(level) { + instance->decoder.te_last = duration; + instance->decoder.parser_step = Acurite_609TXCDecoderStepCheckDuration; + } else { + instance->decoder.parser_step = Acurite_609TXCDecoderStepReset; + } + break; + + case Acurite_609TXCDecoderStepCheckDuration: + if(!level) { + if(DURATION_DIFF(instance->decoder.te_last, ws_protocol_acurite_609txc_const.te_short) < + ws_protocol_acurite_609txc_const.te_delta) { + if((DURATION_DIFF(duration, ws_protocol_acurite_609txc_const.te_short) < + ws_protocol_acurite_609txc_const.te_delta) || + (duration > ws_protocol_acurite_609txc_const.te_long * 3)) { + //Found syncPostfix + instance->decoder.parser_step = Acurite_609TXCDecoderStepReset; + if((instance->decoder.decode_count_bit == + ws_protocol_acurite_609txc_const.min_count_bit_for_found) && + ws_protocol_acurite_609txc_check(instance)) { + instance->generic.data = instance->decoder.decode_data; + instance->generic.data_count_bit = instance->decoder.decode_count_bit; + ws_protocol_acurite_609txc_remote_controller(&instance->generic); + if(instance->base.callback) + instance->base.callback(&instance->base, instance->base.context); + } + instance->decoder.decode_data = 0; + instance->decoder.decode_count_bit = 0; + } else if( + DURATION_DIFF(duration, ws_protocol_acurite_609txc_const.te_long) < + ws_protocol_acurite_609txc_const.te_delta * 2) { + subghz_protocol_blocks_add_bit(&instance->decoder, 0); + instance->decoder.parser_step = Acurite_609TXCDecoderStepSaveDuration; + } else if( + DURATION_DIFF(duration, ws_protocol_acurite_609txc_const.te_long * 2) < + ws_protocol_acurite_609txc_const.te_delta * 4) { + subghz_protocol_blocks_add_bit(&instance->decoder, 1); + instance->decoder.parser_step = Acurite_609TXCDecoderStepSaveDuration; + } else { + instance->decoder.parser_step = Acurite_609TXCDecoderStepReset; + } + } else { + instance->decoder.parser_step = Acurite_609TXCDecoderStepReset; + } + } else { + instance->decoder.parser_step = Acurite_609TXCDecoderStepReset; + } + break; + } +} + +uint8_t ws_protocol_decoder_acurite_609txc_get_hash_data(void* context) { + furi_assert(context); + WSProtocolDecoderAcurite_609TXC* instance = context; + return subghz_protocol_blocks_get_hash_data( + &instance->decoder, (instance->decoder.decode_count_bit / 8) + 1); +} + +bool ws_protocol_decoder_acurite_609txc_serialize( + void* context, + FlipperFormat* flipper_format, + SubGhzRadioPreset* preset) { + furi_assert(context); + WSProtocolDecoderAcurite_609TXC* instance = context; + return ws_block_generic_serialize(&instance->generic, flipper_format, preset); +} + +bool ws_protocol_decoder_acurite_609txc_deserialize(void* context, FlipperFormat* flipper_format) { + furi_assert(context); + WSProtocolDecoderAcurite_609TXC* instance = context; + bool ret = false; + do { + if(!ws_block_generic_deserialize(&instance->generic, flipper_format)) { + break; + } + if(instance->generic.data_count_bit != + ws_protocol_acurite_609txc_const.min_count_bit_for_found) { + FURI_LOG_E(TAG, "Wrong number of bits in key"); + break; + } + ret = true; + } while(false); + return ret; +} + +void ws_protocol_decoder_acurite_609txc_get_string(void* context, FuriString* output) { + furi_assert(context); + WSProtocolDecoderAcurite_609TXC* instance = context; + furi_string_printf( + output, + "%s %dbit\r\n" + "Key:0x%lX%08lX\r\n" + "Sn:0x%lX Ch:%d Bat:%d\r\n" + "Temp:%3.1f C Hum:%d%%", + instance->generic.protocol_name, + instance->generic.data_count_bit, + (uint32_t)(instance->generic.data >> 40), + (uint32_t)(instance->generic.data), + instance->generic.id, + instance->generic.channel, + instance->generic.battery_low, + (double)instance->generic.temp, + instance->generic.humidity); +} diff --git a/applications/plugins/weather_station/protocols/acurite_609txc.h b/applications/plugins/weather_station/protocols/acurite_609txc.h new file mode 100644 index 000000000..f87c20e9b --- /dev/null +++ b/applications/plugins/weather_station/protocols/acurite_609txc.h @@ -0,0 +1,79 @@ +#pragma once + +#include + +#include +#include +#include +#include "ws_generic.h" +#include + +#define WS_PROTOCOL_ACURITE_609TXC_NAME "Acurite-609TXC" + +typedef struct WSProtocolDecoderAcurite_609TXC WSProtocolDecoderAcurite_609TXC; +typedef struct WSProtocolEncoderAcurite_609TXC WSProtocolEncoderAcurite_609TXC; + +extern const SubGhzProtocolDecoder ws_protocol_acurite_609txc_decoder; +extern const SubGhzProtocolEncoder ws_protocol_acurite_609txc_encoder; +extern const SubGhzProtocol ws_protocol_acurite_609txc; + +/** + * Allocate WSProtocolDecoderAcurite_609TXC. + * @param environment Pointer to a SubGhzEnvironment instance + * @return WSProtocolDecoderAcurite_609TXC* pointer to a WSProtocolDecoderAcurite_609TXC instance + */ +void* ws_protocol_decoder_acurite_609txc_alloc(SubGhzEnvironment* environment); + +/** + * Free WSProtocolDecoderAcurite_609TXC. + * @param context Pointer to a WSProtocolDecoderAcurite_609TXC instance + */ +void ws_protocol_decoder_acurite_609txc_free(void* context); + +/** + * Reset decoder WSProtocolDecoderAcurite_609TXC. + * @param context Pointer to a WSProtocolDecoderAcurite_609TXC instance + */ +void ws_protocol_decoder_acurite_609txc_reset(void* context); + +/** + * Parse a raw sequence of levels and durations received from the air. + * @param context Pointer to a WSProtocolDecoderAcurite_609TXC instance + * @param level Signal level true-high false-low + * @param duration Duration of this level in, us + */ +void ws_protocol_decoder_acurite_609txc_feed(void* context, bool level, uint32_t duration); + +/** + * Getting the hash sum of the last randomly received parcel. + * @param context Pointer to a WSProtocolDecoderAcurite_609TXC instance + * @return hash Hash sum + */ +uint8_t ws_protocol_decoder_acurite_609txc_get_hash_data(void* context); + +/** + * Serialize data WSProtocolDecoderAcurite_609TXC. + * @param context Pointer to a WSProtocolDecoderAcurite_609TXC instance + * @param flipper_format Pointer to a FlipperFormat instance + * @param preset The modulation on which the signal was received, SubGhzRadioPreset + * @return true On success + */ +bool ws_protocol_decoder_acurite_609txc_serialize( + void* context, + FlipperFormat* flipper_format, + SubGhzRadioPreset* preset); + +/** + * Deserialize data WSProtocolDecoderAcurite_609TXC. + * @param context Pointer to a WSProtocolDecoderAcurite_609TXC instance + * @param flipper_format Pointer to a FlipperFormat instance + * @return true On success + */ +bool ws_protocol_decoder_acurite_609txc_deserialize(void* context, FlipperFormat* flipper_format); + +/** + * Getting a textual representation of the received data. + * @param context Pointer to a WSProtocolDecoderAcurite_609TXC instance + * @param output Resulting text + */ +void ws_protocol_decoder_acurite_609txc_get_string(void* context, FuriString* output); diff --git a/applications/plugins/weather_station/protocols/protocol_items.c b/applications/plugins/weather_station/protocols/protocol_items.c index d7f6458ab..d2b20e51a 100644 --- a/applications/plugins/weather_station/protocols/protocol_items.c +++ b/applications/plugins/weather_station/protocols/protocol_items.c @@ -6,6 +6,7 @@ const SubGhzProtocol* weather_station_protocol_registry_items[] = { &ws_protocol_nexus_th, &ws_protocol_gt_wt_03, &ws_protocol_acurite_606tx, + &ws_protocol_acurite_609txc, &ws_protocol_lacrosse_tx141thbv2, &ws_protocol_oregon2, &ws_protocol_acurite_592txr, diff --git a/applications/plugins/weather_station/protocols/protocol_items.h b/applications/plugins/weather_station/protocols/protocol_items.h index 76c085ab4..45b297e10 100644 --- a/applications/plugins/weather_station/protocols/protocol_items.h +++ b/applications/plugins/weather_station/protocols/protocol_items.h @@ -6,6 +6,7 @@ #include "nexus_th.h" #include "gt_wt_03.h" #include "acurite_606tx.h" +#include "acurite_609txc.h" #include "lacrosse_tx141thbv2.h" #include "oregon2.h" #include "acurite_592txr.h" From 9f0aef330efc48028d74009f00e81ec16d4656d2 Mon Sep 17 00:00:00 2001 From: Georgii Surkov <37121527+gsurkov@users.noreply.github.com> Date: Tue, 8 Nov 2022 20:38:28 +0300 Subject: [PATCH 34/49] [FL-2956] Initial unit test docs (#1984) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: あく --- documentation/UnitTests.md | 49 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 documentation/UnitTests.md diff --git a/documentation/UnitTests.md b/documentation/UnitTests.md new file mode 100644 index 000000000..3f56a9a90 --- /dev/null +++ b/documentation/UnitTests.md @@ -0,0 +1,49 @@ +# Unit tests +## Intro +Unit tests are special pieces of code that apply known inputs to the feature code and check the results to see if they were correct. +They are crucial for writing robust, bug-free code. + +Flipper Zero firmware includes a separate application called [unit_tests](/applications/debug/unit_tests). +It is run directly on the Flipper Zero in order to employ its hardware features and to rule out any platform-related differences. + +When contributing code to the Flipper Zero firmware, it is highly desirable to supply unit tests along with the proposed features. +Running existing unit tests is useful to ensure that the new code doesn't introduce any regressions. + +## Running unit tests +In order to run the unit tests, follow these steps: +1. Compile the firmware with the tests enabled: `./fbt FIRMWARE_APP_SET=unit_tests`. +2. Flash the firmware using your preferred method. +3. Copy the [assets/unit_tests](assets/unit_tests) folder to the root your Flipper Zero's SD card. +4. Launch the CLI session and run the `unit_tests` command. + +**NOTE:** To run a particular test (and skip all others), specify its name as the command argument. +See [test_index.c](applications/debug/unit_tests/test_index.c) for the complete list of test names. + +## Adding unit tests +### General +#### Entry point +The common entry point for all tests it the [unit_tests](applications/debug/unit_tests) application. Test-specific code is placed into an arbitrarily named subdirectory and is then called from the [test_index.c](applications/debug/unit_tests/test_index.c) source file. +#### Test assets +Some unit tests require external data in order to function. These files (commonly called assets) reside in the [assets/unit_tests](/assets/unit_tests) directory in their respective subdirectories. Asset files can be of any type (plain text, FlipperFormat(FFF), binary etc). +### Application-specific +#### Infrared +Each infrared protocol has a corresponding set of unit tests, so it makes sense to implement one when adding support for a new protocol. +In order to add unit tests for your protocol, follow these steps: +1. Create a file named `test_.irtest` in the [assets](assets/unit_tests/infrared) directory. +2. Fill it with the test data (more on it below). +3. Add the test code to [infrared_test.c](applications/debug/unit_tests/infrared/infrared_test.c). +4. Update the [assets](assets/unit_tests/infrared) on your Flipper Zero and run the tests to see if they pass. + +##### Test data format +Each unit test has 3 sections: +1. `decoder` - takes in raw signal and outputs decoded messages. +2. `encoder` - takes in decoded messages and outputs raw signal. +3. `encoder_decoder` - takes in decoded messages, turns them into raw signal and then decodes again. + +Infrared test asset files have an `.irtest` extension and are regular `.ir` files with a few additions. +Decoder input data has signal names `decoder_input_N`, where N is a test sequence number. Expected data goes under the name `decoder_expected_N`. When testing the encoder these two are switched. + +Decoded data is represented in arrays (since a single raw signal may decode to several messages). If there is only one signal, then it has to be an array of size 1. Use the existing files as syntax examples. + +##### Getting raw signals +Recording raw IR signals is possible using Flipper Zero. Launch the CLI session, run `ir rx raw`, then point the remote towards Flipper's receiver and send the signals. The raw signal data will be printed to the console in a convenient format. From c89e5e11a43589649278050b94fd7c2ef5f3b824 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=82=E3=81=8F?= Date: Thu, 10 Nov 2022 02:33:09 +0900 Subject: [PATCH 35/49] Furi: show thread allocation balance for child threads (#1992) --- applications/services/loader/loader.c | 6 +----- furi/core/thread.c | 12 ++++++++++-- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/applications/services/loader/loader.c b/applications/services/loader/loader.c index 62dbad95f..712576e14 100644 --- a/applications/services/loader/loader.c +++ b/applications/services/loader/loader.c @@ -273,11 +273,7 @@ static void loader_thread_state_callback(FuriThreadState thread_state, void* con furi_hal_power_insomnia_enter(); } } else if(thread_state == FuriThreadStateStopped) { - FURI_LOG_I( - TAG, - "Application thread stopped. Free heap: %d. Thread allocation balance: %d.", - memmgr_get_free_heap(), - furi_thread_get_heap_size(instance->application_thread)); + FURI_LOG_I(TAG, "Application stopped. Free heap: %d", memmgr_get_free_heap()); if(loader_instance->application_arguments) { free(loader_instance->application_arguments); diff --git a/furi/core/thread.c b/furi/core/thread.c index 157e022e9..8320a47e8 100644 --- a/furi/core/thread.c +++ b/furi/core/thread.c @@ -12,6 +12,8 @@ #include #include +#define TAG "FuriThread" + #define THREAD_NOTIFY_INDEX 1 // Index 0 is used for stream buffers typedef struct FuriThreadStdout FuriThreadStdout; @@ -82,6 +84,12 @@ static void furi_thread_body(void* context) { if(thread->heap_trace_enabled == true) { furi_delay_ms(33); thread->heap_size = memmgr_heap_get_thread_memory((FuriThreadId)task_handle); + furi_log_print_format( + thread->heap_size ? FuriLogLevelError : FuriLogLevelInfo, + TAG, + "%s allocation balance: %d", + thread->name ? thread->name : "Thread", + thread->heap_size); memmgr_heap_disable_thread_trace((FuriThreadId)task_handle); } @@ -89,8 +97,8 @@ static void furi_thread_body(void* context) { if(thread->is_service) { FURI_LOG_E( - "Service", - "%s thread exited. Thread memory cannot be reclaimed.", + TAG, + "%s service thread exited. Thread memory cannot be reclaimed.", thread->name ? thread->name : ""); } From 3985b456c39c644314282ad426cb757c770236b8 Mon Sep 17 00:00:00 2001 From: gornekich Date: Wed, 9 Nov 2022 22:12:55 +0400 Subject: [PATCH 36/49] NFC: fix crash on MFC read (#1993) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * nfc: fix nfc_worker_stop logic * nfc: fix stop order Co-authored-by: あく --- lib/nfc/nfc_worker.c | 10 +++++----- lib/nfc/nfc_worker.h | 1 - 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/lib/nfc/nfc_worker.c b/lib/nfc/nfc_worker.c index e1e379a06..5ce543636 100644 --- a/lib/nfc/nfc_worker.c +++ b/lib/nfc/nfc_worker.c @@ -70,12 +70,12 @@ void nfc_worker_start( void nfc_worker_stop(NfcWorker* nfc_worker) { furi_assert(nfc_worker); - if(nfc_worker->state == NfcWorkerStateBroken || nfc_worker->state == NfcWorkerStateReady) { - return; + furi_assert(nfc_worker->thread); + if(furi_thread_get_state(nfc_worker->thread) != FuriThreadStateStopped) { + furi_hal_nfc_stop(); + nfc_worker_change_state(nfc_worker, NfcWorkerStateStop); + furi_thread_join(nfc_worker->thread); } - furi_hal_nfc_stop(); - nfc_worker_change_state(nfc_worker, NfcWorkerStateStop); - furi_thread_join(nfc_worker->thread); } void nfc_worker_change_state(NfcWorker* nfc_worker, NfcWorkerState state) { diff --git a/lib/nfc/nfc_worker.h b/lib/nfc/nfc_worker.h index ce3a18241..ddee34c95 100644 --- a/lib/nfc/nfc_worker.h +++ b/lib/nfc/nfc_worker.h @@ -7,7 +7,6 @@ typedef struct NfcWorker NfcWorker; typedef enum { // Init states NfcWorkerStateNone, - NfcWorkerStateBroken, NfcWorkerStateReady, // Main worker states NfcWorkerStateRead, From a959fa32bc65620460d2bcfd593a12ddcf55def3 Mon Sep 17 00:00:00 2001 From: hedger Date: Thu, 10 Nov 2022 15:55:11 +0400 Subject: [PATCH 37/49] fbt: 'target' field for apps; lib debugging support (#1995) * fbt: added 'target' field to application manifest * fbt: earlier pagination setup for gdb * fbt: added LIB_DEBUG flag * fbt: sdk: added SDK_MAP_FILE_SUBST --- SConstruct | 9 +++------ documentation/AppManifests.md | 1 + firmware.scons | 4 ++-- scripts/fbt/appmanifest.py | 20 +++++++++++++++++--- scripts/fbt/util.py | 7 ++++++- scripts/fbt_tools/fbt_apps.py | 13 +++++++------ scripts/fbt_tools/fbt_debugopts.py | 4 ++-- scripts/fbt_tools/fbt_sdk.py | 22 ++++++++++++++-------- site_scons/commandline.scons | 5 +++++ site_scons/extapps.scons | 10 +++++++++- 10 files changed, 66 insertions(+), 29 deletions(-) diff --git a/SConstruct b/SConstruct index 3ee89f646..34ff80bc9 100644 --- a/SConstruct +++ b/SConstruct @@ -7,6 +7,7 @@ # construction of certain targets behind command-line options. import os +from fbt.util import path_as_posix DefaultEnvironment(tools=[]) @@ -200,9 +201,7 @@ firmware_debug = distenv.PhonyTarget( source=firmware_env["FW_ELF"], GDBOPTS="${GDBOPTS_BASE}", GDBREMOTE="${OPENOCD_GDB_PIPE}", - FBT_FAP_DEBUG_ELF_ROOT=firmware_env.subst("$FBT_FAP_DEBUG_ELF_ROOT").replace( - "\\", "/" - ), + FBT_FAP_DEBUG_ELF_ROOT=path_as_posix(firmware_env.subst("$FBT_FAP_DEBUG_ELF_ROOT")), ) distenv.Depends(firmware_debug, firmware_flash) @@ -212,9 +211,7 @@ distenv.PhonyTarget( source=firmware_env["FW_ELF"], GDBOPTS="${GDBOPTS_BASE} ${GDBOPTS_BLACKMAGIC}", GDBREMOTE="${BLACKMAGIC_ADDR}", - FBT_FAP_DEBUG_ELF_ROOT=firmware_env.subst("$FBT_FAP_DEBUG_ELF_ROOT").replace( - "\\", "/" - ), + FBT_FAP_DEBUG_ELF_ROOT=path_as_posix(firmware_env.subst("$FBT_FAP_DEBUG_ELF_ROOT")), ) # Debug alien elf diff --git a/documentation/AppManifests.md b/documentation/AppManifests.md index f4814ee5d..d70a12f9c 100644 --- a/documentation/AppManifests.md +++ b/documentation/AppManifests.md @@ -40,6 +40,7 @@ Only 2 parameters are mandatory: ***appid*** and ***apptype***, others are optio * **icon**: Animated icon name from built-in assets to be used when building app as a part of firmware. * **order**: Order of an application within its group when sorting entries in it. The lower the order is, the closer to the start of the list the item is placed. *Used for ordering startup hooks and menu entries.* * **sdk_headers**: List of C header files from this app's code to include in API definitions for external applications. +* **targets**: list of strings, target names, which this application is compatible with. If not specified, application is built for all targets. Default value is `["all"]`. #### Parameters for external applications diff --git a/firmware.scons b/firmware.scons index 6a9237477..da5caba53 100644 --- a/firmware.scons +++ b/firmware.scons @@ -40,11 +40,11 @@ env = ENV.Clone( FW_LIB_OPTS={ "Default": { "CCFLAGS": [ - "-Os", + "-Og" if ENV["LIB_DEBUG"] else "-Os", ], "CPPDEFINES": [ "NDEBUG", - "FURI_NDEBUG", + "FURI_DEBUG" if ENV["LIB_DEBUG"] else "FURI_NDEBUG", ], # You can add other entries named after libraries # If they are present, they have precedence over Default diff --git a/scripts/fbt/appmanifest.py b/scripts/fbt/appmanifest.py index de7c6b682..908a64a6f 100644 --- a/scripts/fbt/appmanifest.py +++ b/scripts/fbt/appmanifest.py @@ -52,6 +52,8 @@ class FlipperApplication: icon: Optional[str] = None order: int = 0 sdk_headers: List[str] = field(default_factory=list) + targets: List[str] = field(default_factory=lambda: ["all"]) + # .fap-specific sources: List[str] = field(default_factory=lambda: ["*.c*"]) fap_version: Tuple[int] = field(default_factory=lambda: (0, 1)) @@ -135,8 +137,8 @@ class AppManager: raise FlipperManifestException(f"Duplicate app declaration: {app.appid}") self.known_apps[app.appid] = app - def filter_apps(self, applist: List[str]): - return AppBuildset(self, applist) + def filter_apps(self, applist: List[str], hw_target: str): + return AppBuildset(self, applist, hw_target) class AppBuilderException(Exception): @@ -155,11 +157,13 @@ class AppBuildset: FlipperAppType.STARTUP, ) - def __init__(self, appmgr: AppManager, appnames: List[str]): + def __init__(self, appmgr: AppManager, appnames: List[str], hw_target: str): self.appmgr = appmgr self.appnames = set(appnames) + self.hw_target = hw_target self._orig_appnames = appnames self._process_deps() + self._filter_by_target() self._check_conflicts() self._check_unsatisfied() # unneeded? self.apps = sorted( @@ -170,6 +174,16 @@ class AppBuildset: def _is_missing_dep(self, dep_name: str): return dep_name not in self.appnames + def _filter_by_target(self): + for appname in self.appnames.copy(): + app = self.appmgr.get(appname) + # if app.apptype not in self.BUILTIN_APP_TYPES: + if not any(map(lambda t: t in app.targets, ["all", self.hw_target])): + print( + f"Removing {appname} due to target mismatch (building for {self.hw_target}, app supports {app.targets}" + ) + self.appnames.remove(appname) + def _process_deps(self): while True: provided = [] diff --git a/scripts/fbt/util.py b/scripts/fbt/util.py index b8e9c5928..ee7562058 100644 --- a/scripts/fbt/util.py +++ b/scripts/fbt/util.py @@ -1,7 +1,6 @@ import SCons from SCons.Subst import quote_spaces from SCons.Errors import StopError -from SCons.Node.FS import _my_normcase import re import os @@ -58,3 +57,9 @@ def extract_abs_dir_path(node): raise StopError(f"Can't find absolute path for {node.name}") return abs_dir_node.abspath + + +def path_as_posix(path): + if SCons.Platform.platform_default() == "win32": + return path.replace(os.path.sep, os.path.altsep) + return path diff --git a/scripts/fbt_tools/fbt_apps.py b/scripts/fbt_tools/fbt_apps.py index 55e282017..96528f4e5 100644 --- a/scripts/fbt_tools/fbt_apps.py +++ b/scripts/fbt_tools/fbt_apps.py @@ -1,7 +1,6 @@ from SCons.Builder import Builder from SCons.Action import Action from SCons.Warnings import warn, WarningOnByDefault -import SCons from ansi.color import fg from fbt.appmanifest import ( @@ -33,14 +32,12 @@ def LoadAppManifest(env, entry): def PrepareApplicationsBuild(env): - appbuild = env["APPBUILD"] = env["APPMGR"].filter_apps(env["APPS"]) + appbuild = env["APPBUILD"] = env["APPMGR"].filter_apps( + env["APPS"], env.subst("f${TARGET_HW}") + ) env.Append( SDK_HEADERS=appbuild.get_sdk_headers(), ) - env["APPBUILD_DUMP"] = env.Action( - DumpApplicationConfig, - "\tINFO\t", - ) def DumpApplicationConfig(target, source, env): @@ -68,6 +65,10 @@ def generate(env): env.AddMethod(PrepareApplicationsBuild) env.SetDefault( APPMGR=AppManager(), + APPBUILD_DUMP=env.Action( + DumpApplicationConfig, + "\tINFO\t", + ), ) env.Append( diff --git a/scripts/fbt_tools/fbt_debugopts.py b/scripts/fbt_tools/fbt_debugopts.py index c3be5ca47..9ff05cb73 100644 --- a/scripts/fbt_tools/fbt_debugopts.py +++ b/scripts/fbt_tools/fbt_debugopts.py @@ -41,12 +41,12 @@ def generate(env, **kw): "|openocd -c 'gdb_port pipe; log_output ${FBT_DEBUG_DIR}/openocd.log' ${[SINGLEQUOTEFUNC(OPENOCD_OPTS)]}" ], GDBOPTS_BASE=[ + "-ex", + "set pagination off", "-ex", "target extended-remote ${GDBREMOTE}", "-ex", "set confirm off", - "-ex", - "set pagination off", ], GDBOPTS_BLACKMAGIC=[ "-ex", diff --git a/scripts/fbt_tools/fbt_sdk.py b/scripts/fbt_tools/fbt_sdk.py index c46346b65..3a37eacc9 100644 --- a/scripts/fbt_tools/fbt_sdk.py +++ b/scripts/fbt_tools/fbt_sdk.py @@ -14,6 +14,7 @@ import json from fbt.sdk.collector import SdkCollector from fbt.sdk.cache import SdkCache +from fbt.util import path_as_posix def ProcessSdkDepends(env, filename): @@ -52,6 +53,8 @@ def prebuild_sdk_create_origin_file(target, source, env): class SdkMeta: + MAP_FILE_SUBST = "SDK_MAP_FILE_SUBST" + def __init__(self, env, tree_builder: "SdkTreeBuilder"): self.env = env self.treebuilder = tree_builder @@ -67,6 +70,7 @@ class SdkMeta: "linker_libs": self.env.subst("${LIBS}"), "app_ep_subst": self.env.subst("${APP_ENTRY}"), "sdk_path_subst": self.env.subst("${SDK_DIR_SUBST}"), + "map_file_subst": self.MAP_FILE_SUBST, "hardware": self.env.subst("${TARGET_HW}"), } with open(json_manifest_path, "wt") as f: @@ -75,9 +79,9 @@ class SdkMeta: def _wrap_scons_vars(self, vars: str): expanded_vars = self.env.subst( vars, - target=Entry("dummy"), + target=Entry(self.MAP_FILE_SUBST), ) - return expanded_vars.replace("\\", "/") + return path_as_posix(expanded_vars) class SdkTreeBuilder: @@ -142,13 +146,15 @@ class SdkTreeBuilder: meta.save_to(self.target[0].path) def build_sdk_file_path(self, orig_path: str) -> str: - return posixpath.normpath( - posixpath.join( - self.SDK_DIR_SUBST, - self.target_sdk_dir_name, - orig_path, + return path_as_posix( + posixpath.normpath( + posixpath.join( + self.SDK_DIR_SUBST, + self.target_sdk_dir_name, + orig_path, + ) ) - ).replace("\\", "/") + ) def emitter(self, target, source, env): target_folder = target[0] diff --git a/site_scons/commandline.scons b/site_scons/commandline.scons index 044de6b30..6087c1c7a 100644 --- a/site_scons/commandline.scons +++ b/site_scons/commandline.scons @@ -63,6 +63,11 @@ vars.AddVariables( help="Enable debug build", default=True, ), + BoolVariable( + "LIB_DEBUG", + help="Enable debug build for libraries", + default=False, + ), BoolVariable( "COMPACT", help="Optimize for size", diff --git a/site_scons/extapps.scons b/site_scons/extapps.scons index 670d71fd0..b8f210563 100644 --- a/site_scons/extapps.scons +++ b/site_scons/extapps.scons @@ -1,6 +1,6 @@ from dataclasses import dataclass, field -from SCons.Errors import UserError from SCons.Node import NodeList +from SCons.Warnings import warn, WarningOnByDefault Import("ENV") @@ -80,6 +80,14 @@ if extra_app_list := GetOption("extra_ext_apps"): known_extapps.extend(map(appenv["APPMGR"].get, extra_app_list.split(","))) for app in known_extapps: + if not any(map(lambda t: t in app.targets, ["all", appenv.subst("f${TARGET_HW}")])): + warn( + WarningOnByDefault, + f"Can't build '{app.name}' (id '{app.appid}'): target mismatch" + f" (building for {appenv.subst('f${TARGET_HW}')}, app supports {app.targets}", + ) + continue + appenv.BuildAppElf(app) From f94e8f4ac8978118489f1086fa2c979995f71964 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=82=E3=81=8F?= Date: Thu, 10 Nov 2022 22:56:08 +0900 Subject: [PATCH 38/49] Rpc: increase stack size, fix stack overflow (#1997) --- applications/services/rpc/rpc.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/applications/services/rpc/rpc.c b/applications/services/rpc/rpc.c index 73eaadfb1..f1e0cbd66 100644 --- a/applications/services/rpc/rpc.c +++ b/applications/services/rpc/rpc.c @@ -372,7 +372,7 @@ RpcSession* rpc_session_open(Rpc* rpc) { session->thread = furi_thread_alloc(); furi_thread_set_name(session->thread, "RpcSessionWorker"); - furi_thread_set_stack_size(session->thread, 2048); + furi_thread_set_stack_size(session->thread, 3072); furi_thread_set_context(session->thread, session); furi_thread_set_callback(session->thread, rpc_session_worker); From a66e8d9ac98d1845714f28d60e3c63f2584ef0b9 Mon Sep 17 00:00:00 2001 From: Rom1 Date: Thu, 10 Nov 2022 16:21:28 +0100 Subject: [PATCH 39/49] corr: bad path for furi core (#1975) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * corr: bad path for furi core * Documentation: exclude submodules * Documentation: wider folder list Co-authored-by: Sergey Gavrilov Co-authored-by: あく --- documentation/.gitignore | 2 ++ documentation/Doxyfile | 22 +++++++++++++++------- 2 files changed, 17 insertions(+), 7 deletions(-) create mode 100644 documentation/.gitignore diff --git a/documentation/.gitignore b/documentation/.gitignore new file mode 100644 index 000000000..c18ff03bb --- /dev/null +++ b/documentation/.gitignore @@ -0,0 +1,2 @@ +/html +/latex \ No newline at end of file diff --git a/documentation/Doxyfile b/documentation/Doxyfile index 6d6bb8aa8..1824e5a52 100644 --- a/documentation/Doxyfile +++ b/documentation/Doxyfile @@ -872,12 +872,9 @@ WARN_LOGFILE = # Note: If this tag is empty the current directory is searched. INPUT = applications \ - core \ - lib/infrared \ - lib/subghz \ - lib/toolbox \ - lib/onewire \ - firmware/targets/furi_hal_include + lib \ + firmware \ + furi # This tag can be used to specify the character encoding of the source files # that doxygen parses. Internally doxygen uses the UTF-8 encoding. Doxygen uses @@ -930,7 +927,18 @@ RECURSIVE = YES # Note that relative paths are relative to the directory from which doxygen is # run. -EXCLUDE = +EXCLUDE = \ + lib/mlib \ + lib/STM32CubeWB \ + lib/littlefs \ + lib/nanopb \ + assets/protobuf \ + lib/libusb_stm32 \ + lib/FreeRTOS-Kernel \ + lib/microtar \ + lib/mbedtls \ + lib/cxxheaderparser \ + applications/plugins/dap_link/lib/free-dap # The EXCLUDE_SYMLINKS tag can be used to select whether or not files or # directories that are symbolic links (a Unix file system feature) are excluded From 820afd2aec377629c3e514ceb57ecdd0f9163b6c Mon Sep 17 00:00:00 2001 From: Astra <93453568+Astrrra@users.noreply.github.com> Date: Thu, 10 Nov 2022 18:20:35 +0200 Subject: [PATCH 40/49] NFC Unit tests part 1.1 (#1927) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Mifare Classic 1/4K, 4/7b uid, NFC-A: NFC-A is not complete yet, as there are no 4b uid tests. Also, Mifare Classic tests don't cover the key cache yet. * NFC unit tests require access to the NFC app * Made nfc_device_save accept full path as an argument * Move from cstrs to furi strings and fix logic * nfc tests: fix memory leak * nfc: add mf_classic_get_total_blocks() to API * nfc tests: simplify nfc tests * nfc: fix memory leak in shadow file saving * nfc: fix set uid scene * nfc: fix saving files * nfc: fix preload nfc file path * nfc: remove comments Co-authored-by: Sergey Gavrilov Co-authored-by: gornekich Co-authored-by: あく --- applications/debug/unit_tests/nfc/nfc_test.c | 197 ++++++++++++++++++ .../main/nfc/helpers/nfc_generators.c | 7 +- .../main/nfc/helpers/nfc_generators.h | 2 + applications/main/nfc/nfc.c | 11 +- applications/main/nfc/nfc_i.h | 2 + .../main/nfc/scenes/nfc_scene_file_select.c | 3 + .../nfc/scenes/nfc_scene_mf_classic_emulate.c | 5 +- .../nfc/scenes/nfc_scene_mf_classic_update.c | 2 +- .../scenes/nfc_scene_mf_ultralight_emulate.c | 5 +- .../main/nfc/scenes/nfc_scene_save_name.c | 2 +- .../main/nfc/scenes/nfc_scene_set_uid.c | 3 +- fbt_options.py | 1 + firmware/targets/f7/api_symbols.csv | 3 +- lib/nfc/nfc_device.c | 44 ++-- lib/nfc/protocols/mifare_classic.c | 2 +- lib/nfc/protocols/mifare_classic.h | 2 + 16 files changed, 256 insertions(+), 35 deletions(-) diff --git a/applications/debug/unit_tests/nfc/nfc_test.c b/applications/debug/unit_tests/nfc/nfc_test.c index 8009f6a17..454c11c0f 100644 --- a/applications/debug/unit_tests/nfc/nfc_test.c +++ b/applications/debug/unit_tests/nfc/nfc_test.c @@ -5,6 +5,8 @@ #include #include #include +#include +#include #include #include @@ -17,6 +19,7 @@ #define NFC_TEST_SIGNAL_SHORT_FILE "nfc_nfca_signal_short.nfc" #define NFC_TEST_SIGNAL_LONG_FILE "nfc_nfca_signal_long.nfc" #define NFC_TEST_DICT_PATH EXT_PATH("unit_tests/mf_classic_dict.nfc") +#define NFC_TEST_NFC_DEV_PATH EXT_PATH("unit_tests/nfc/nfc_dev_test.nfc") static const char* nfc_test_file_type = "Flipper NFC test"; static const uint32_t nfc_test_file_version = 1; @@ -287,9 +290,203 @@ MU_TEST(mf_classic_dict_load_test) { furi_record_close(RECORD_STORAGE); } +MU_TEST(nfca_file_test) { + NfcDevice* nfc = nfc_device_alloc(); + mu_assert(nfc != NULL, "nfc_device_data != NULL assert failed\r\n"); + nfc->format = NfcDeviceSaveFormatUid; + + // Fill the UID, sak, ATQA and type + uint8_t uid[7] = {0x04, 0x01, 0x23, 0x45, 0x67, 0x89, 0x00}; + memcpy(nfc->dev_data.nfc_data.uid, uid, 7); + nfc->dev_data.nfc_data.uid_len = 7; + + nfc->dev_data.nfc_data.sak = 0x08; + nfc->dev_data.nfc_data.atqa[0] = 0x00; + nfc->dev_data.nfc_data.atqa[1] = 0x04; + nfc->dev_data.nfc_data.type = FuriHalNfcTypeA; + + // Save the NFC device data to the file + mu_assert( + nfc_device_save(nfc, NFC_TEST_NFC_DEV_PATH), "nfc_device_save == true assert failed\r\n"); + nfc_device_free(nfc); + + // Load the NFC device data from the file + NfcDevice* nfc_validate = nfc_device_alloc(); + mu_assert( + nfc_device_load(nfc_validate, NFC_TEST_NFC_DEV_PATH, true), + "nfc_device_load == true assert failed\r\n"); + + // Check the UID, sak, ATQA and type + mu_assert(memcmp(nfc_validate->dev_data.nfc_data.uid, uid, 7) == 0, "uid assert failed\r\n"); + mu_assert(nfc_validate->dev_data.nfc_data.sak == 0x08, "sak == 0x08 assert failed\r\n"); + mu_assert( + nfc_validate->dev_data.nfc_data.atqa[0] == 0x00, "atqa[0] == 0x00 assert failed\r\n"); + mu_assert( + nfc_validate->dev_data.nfc_data.atqa[1] == 0x04, "atqa[1] == 0x04 assert failed\r\n"); + mu_assert( + nfc_validate->dev_data.nfc_data.type == FuriHalNfcTypeA, + "type == FuriHalNfcTypeA assert failed\r\n"); + nfc_device_free(nfc_validate); +} + +static void mf_classic_generator_test(uint8_t uid_len, MfClassicType type) { + NfcDevice* nfc_dev = nfc_device_alloc(); + mu_assert(nfc_dev != NULL, "nfc_device_data != NULL assert failed\r\n"); + nfc_dev->format = NfcDeviceSaveFormatMifareClassic; + + // Create a test file + nfc_generate_mf_classic(&nfc_dev->dev_data, uid_len, type); + + // Get the uid from generated MFC + uint8_t uid[7] = {0}; + memcpy(uid, nfc_dev->dev_data.nfc_data.uid, uid_len); + uint8_t sak = nfc_dev->dev_data.nfc_data.sak; + uint8_t atqa[2] = {}; + memcpy(atqa, nfc_dev->dev_data.nfc_data.atqa, 2); + + MfClassicData* mf_data = &nfc_dev->dev_data.mf_classic_data; + // Check the manufacturer block (should be uid[uid_len] + 0xFF[rest]) + uint8_t manufacturer_block[16] = {0}; + memcpy(manufacturer_block, nfc_dev->dev_data.mf_classic_data.block[0].value, 16); + mu_assert( + memcmp(manufacturer_block, uid, uid_len) == 0, + "manufacturer_block uid doesn't match the file\r\n"); + for(uint8_t i = uid_len; i < 16; i++) { + mu_assert( + manufacturer_block[i] == 0xFF, "manufacturer_block[i] == 0xFF assert failed\r\n"); + } + + // Reference sector trailers (should be 0xFF[6] + 0xFF + 0x07 + 0x80 + 0x69 + 0xFF[6]) + uint8_t sector_trailer[16] = { + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0x07, + 0x80, + 0x69, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF}; + // Reference block data + uint8_t block_data[16] = {}; + memset(block_data, 0xff, sizeof(block_data)); + uint16_t total_blocks = mf_classic_get_total_block_num(type); + for(size_t i = 1; i < total_blocks; i++) { + if(mf_classic_is_sector_trailer(i)) { + mu_assert( + memcmp(mf_data->block[i].value, sector_trailer, 16) == 0, + "Failed sector trailer compare"); + } else { + mu_assert(memcmp(mf_data->block[i].value, block_data, 16) == 0, "Failed data compare"); + } + } + // Save the NFC device data to the file + mu_assert( + nfc_device_save(nfc_dev, NFC_TEST_NFC_DEV_PATH), + "nfc_device_save == true assert failed\r\n"); + // Verify that key cache is saved + FuriString* key_cache_name = furi_string_alloc(); + furi_string_set_str(key_cache_name, "/ext/nfc/cache/"); + for(size_t i = 0; i < uid_len; i++) { + furi_string_cat_printf(key_cache_name, "%02X", uid[i]); + } + furi_string_cat_printf(key_cache_name, ".keys"); + mu_assert( + storage_common_stat(nfc_dev->storage, furi_string_get_cstr(key_cache_name), NULL) == + FSE_OK, + "Key cache file save failed"); + nfc_device_free(nfc_dev); + + // Load the NFC device data from the file + NfcDevice* nfc_validate = nfc_device_alloc(); + mu_assert(nfc_validate, "Nfc device alloc assert"); + mu_assert( + nfc_device_load(nfc_validate, NFC_TEST_NFC_DEV_PATH, false), + "nfc_device_load == true assert failed\r\n"); + + // Check the UID, sak, ATQA and type + mu_assert( + memcmp(nfc_validate->dev_data.nfc_data.uid, uid, uid_len) == 0, + "uid compare assert failed\r\n"); + mu_assert(nfc_validate->dev_data.nfc_data.sak == sak, "sak compare assert failed\r\n"); + mu_assert( + memcmp(nfc_validate->dev_data.nfc_data.atqa, atqa, 2) == 0, + "atqa compare assert failed\r\n"); + mu_assert( + nfc_validate->dev_data.nfc_data.type == FuriHalNfcTypeA, + "type == FuriHalNfcTypeA assert failed\r\n"); + + // Check the manufacturer block + mu_assert( + memcmp(nfc_validate->dev_data.mf_classic_data.block[0].value, manufacturer_block, 16) == 0, + "manufacturer_block assert failed\r\n"); + // Check other blocks + for(size_t i = 1; i < total_blocks; i++) { + if(mf_classic_is_sector_trailer(i)) { + mu_assert( + memcmp(mf_data->block[i].value, sector_trailer, 16) == 0, + "Failed sector trailer compare"); + } else { + mu_assert(memcmp(mf_data->block[i].value, block_data, 16) == 0, "Failed data compare"); + } + } + nfc_device_free(nfc_validate); + + // Check saved key cache + NfcDevice* nfc_keys = nfc_device_alloc(); + mu_assert(nfc_validate, "Nfc device alloc assert"); + nfc_keys->dev_data.nfc_data.uid_len = uid_len; + memcpy(nfc_keys->dev_data.nfc_data.uid, uid, uid_len); + mu_assert(nfc_device_load_key_cache(nfc_keys), "Failed to load key cache"); + uint8_t total_sec = mf_classic_get_total_sectors_num(type); + uint8_t default_key[6] = {}; + memset(default_key, 0xff, 6); + for(size_t i = 0; i < total_sec; i++) { + MfClassicSectorTrailer* sec_tr = + mf_classic_get_sector_trailer_by_sector(&nfc_keys->dev_data.mf_classic_data, i); + mu_assert(memcmp(sec_tr->key_a, default_key, 6) == 0, "Failed key compare"); + mu_assert(memcmp(sec_tr->key_b, default_key, 6) == 0, "Failed key compare"); + } + + // Delete key cache file + mu_assert( + storage_common_remove(nfc_keys->storage, furi_string_get_cstr(key_cache_name)) == FSE_OK, + "Failed to remove key cache file"); + furi_string_free(key_cache_name); + nfc_device_free(nfc_keys); +} + +MU_TEST(mf_classic_1k_4b_file_test) { + mf_classic_generator_test(4, MfClassicType1k); +} + +MU_TEST(mf_classic_4k_4b_file_test) { + mf_classic_generator_test(4, MfClassicType4k); +} + +MU_TEST(mf_classic_1k_7b_file_test) { + mf_classic_generator_test(7, MfClassicType1k); +} + +MU_TEST(mf_classic_4k_7b_file_test) { + mf_classic_generator_test(7, MfClassicType4k); +} + MU_TEST_SUITE(nfc) { nfc_test_alloc(); + MU_RUN_TEST(nfca_file_test); + MU_RUN_TEST(mf_classic_1k_4b_file_test); + MU_RUN_TEST(mf_classic_4k_4b_file_test); + MU_RUN_TEST(mf_classic_1k_7b_file_test); + MU_RUN_TEST(mf_classic_4k_7b_file_test); MU_RUN_TEST(nfc_digital_signal_test); MU_RUN_TEST(mf_classic_dict_test); MU_RUN_TEST(mf_classic_dict_load_test); diff --git a/applications/main/nfc/helpers/nfc_generators.c b/applications/main/nfc/helpers/nfc_generators.c index 11083b9f0..5f0527c6a 100644 --- a/applications/main/nfc/helpers/nfc_generators.c +++ b/applications/main/nfc/helpers/nfc_generators.c @@ -314,7 +314,7 @@ static void nfc_generate_ntag_i2c_plus_2k(NfcDeviceData* data) { mful->version.storage_size = 0x15; } -static void nfc_generate_mf_classic(NfcDeviceData* data, uint8_t uid_len, MfClassicType type) { +void nfc_generate_mf_classic(NfcDeviceData* data, uint8_t uid_len, MfClassicType type) { nfc_generate_common_start(data); nfc_generate_mf_classic_common(data, uid_len, type); @@ -337,6 +337,9 @@ static void nfc_generate_mf_classic(NfcDeviceData* data, uint8_t uid_len, MfClas } mf_classic_set_block_read(mfc, i, &mfc->block[i]); } + // Set SAK to 18 + data->nfc_data.sak = 0x18; + } else if(type == MfClassicType1k) { // Set every block to 0xFF for(uint16_t i = 1; i < MF_CLASSIC_1K_TOTAL_SECTORS_NUM * 4; i += 1) { @@ -347,6 +350,8 @@ static void nfc_generate_mf_classic(NfcDeviceData* data, uint8_t uid_len, MfClas } mf_classic_set_block_read(mfc, i, &mfc->block[i]); } + // Set SAK to 08 + data->nfc_data.sak = 0x08; } mfc->type = type; diff --git a/applications/main/nfc/helpers/nfc_generators.h b/applications/main/nfc/helpers/nfc_generators.h index 10a05591b..362a19b1e 100644 --- a/applications/main/nfc/helpers/nfc_generators.h +++ b/applications/main/nfc/helpers/nfc_generators.h @@ -11,3 +11,5 @@ struct NfcGenerator { }; extern const NfcGenerator* const nfc_generators[]; + +void nfc_generate_mf_classic(NfcDeviceData* data, uint8_t uid_len, MfClassicType type); diff --git a/applications/main/nfc/nfc.c b/applications/main/nfc/nfc.c index 55c68a450..0dd071bc4 100644 --- a/applications/main/nfc/nfc.c +++ b/applications/main/nfc/nfc.c @@ -116,7 +116,9 @@ void nfc_free(Nfc* nfc) { // Stop worker nfc_worker_stop(nfc->worker); // Save data in shadow file - nfc_device_save_shadow(nfc->dev, nfc->dev->dev_name); + if(furi_string_size(nfc->dev->load_path)) { + nfc_device_save_shadow(nfc->dev, furi_string_get_cstr(nfc->dev->load_path)); + } } if(nfc->rpc_ctx) { rpc_system_app_send_exited(nfc->rpc_ctx); @@ -218,6 +220,13 @@ void nfc_blink_stop(Nfc* nfc) { notification_message(nfc->notifications, &sequence_blink_stop); } +bool nfc_save_file(Nfc* nfc) { + furi_string_printf( + nfc->dev->load_path, "%s/%s%s", NFC_APP_FOLDER, nfc->dev->dev_name, NFC_APP_EXTENSION); + bool file_saved = nfc_device_save(nfc->dev, furi_string_get_cstr(nfc->dev->load_path)); + return file_saved; +} + void nfc_show_loading_popup(void* context, bool show) { Nfc* nfc = context; TaskHandle_t timer_task = xTaskGetHandle(configTIMER_SERVICE_TASK_NAME); diff --git a/applications/main/nfc/nfc_i.h b/applications/main/nfc/nfc_i.h index e9b36a3e9..57eefbf67 100644 --- a/applications/main/nfc/nfc_i.h +++ b/applications/main/nfc/nfc_i.h @@ -114,4 +114,6 @@ void nfc_blink_detect_start(Nfc* nfc); void nfc_blink_stop(Nfc* nfc); +bool nfc_save_file(Nfc* nfc); + void nfc_show_loading_popup(void* context, bool show); diff --git a/applications/main/nfc/scenes/nfc_scene_file_select.c b/applications/main/nfc/scenes/nfc_scene_file_select.c index 693fdec20..374a933d1 100644 --- a/applications/main/nfc/scenes/nfc_scene_file_select.c +++ b/applications/main/nfc/scenes/nfc_scene_file_select.c @@ -5,6 +5,9 @@ void nfc_scene_file_select_on_enter(void* context) { Nfc* nfc = context; // Process file_select return nfc_device_set_loading_callback(nfc->dev, nfc_show_loading_popup, nfc); + if(!furi_string_size(nfc->dev->load_path)) { + furi_string_set_str(nfc->dev->load_path, NFC_APP_FOLDER); + } if(nfc_file_select(nfc->dev)) { scene_manager_set_scene_state(nfc->scene_manager, NfcSceneSavedMenu, 0); scene_manager_next_scene(nfc->scene_manager, NfcSceneSavedMenu); diff --git a/applications/main/nfc/scenes/nfc_scene_mf_classic_emulate.c b/applications/main/nfc/scenes/nfc_scene_mf_classic_emulate.c index e514fa728..68eda7040 100644 --- a/applications/main/nfc/scenes/nfc_scene_mf_classic_emulate.c +++ b/applications/main/nfc/scenes/nfc_scene_mf_classic_emulate.c @@ -48,7 +48,10 @@ bool nfc_scene_mf_classic_emulate_on_event(void* context, SceneManagerEvent even NFC_MF_CLASSIC_DATA_CHANGED) { scene_manager_set_scene_state( nfc->scene_manager, NfcSceneMfClassicEmulate, NFC_MF_CLASSIC_DATA_NOT_CHANGED); - nfc_device_save_shadow(nfc->dev, nfc->dev->dev_name); + // Save shadow file + if(furi_string_size(nfc->dev->load_path)) { + nfc_device_save_shadow(nfc->dev, furi_string_get_cstr(nfc->dev->load_path)); + } } consumed = false; } diff --git a/applications/main/nfc/scenes/nfc_scene_mf_classic_update.c b/applications/main/nfc/scenes/nfc_scene_mf_classic_update.c index dd3a6f7d5..aacf77f77 100644 --- a/applications/main/nfc/scenes/nfc_scene_mf_classic_update.c +++ b/applications/main/nfc/scenes/nfc_scene_mf_classic_update.c @@ -57,7 +57,7 @@ bool nfc_scene_mf_classic_update_on_event(void* context, SceneManagerEvent event if(event.type == SceneManagerEventTypeCustom) { if(event.event == NfcWorkerEventSuccess) { nfc_worker_stop(nfc->worker); - if(nfc_device_save_shadow(nfc->dev, nfc->dev->dev_name)) { + if(nfc_device_save_shadow(nfc->dev, furi_string_get_cstr(nfc->dev->load_path))) { scene_manager_next_scene(nfc->scene_manager, NfcSceneMfClassicUpdateSuccess); } else { scene_manager_next_scene(nfc->scene_manager, NfcSceneMfClassicWrongCard); diff --git a/applications/main/nfc/scenes/nfc_scene_mf_ultralight_emulate.c b/applications/main/nfc/scenes/nfc_scene_mf_ultralight_emulate.c index e84fb3927..adcadaaf2 100644 --- a/applications/main/nfc/scenes/nfc_scene_mf_ultralight_emulate.c +++ b/applications/main/nfc/scenes/nfc_scene_mf_ultralight_emulate.c @@ -48,7 +48,10 @@ bool nfc_scene_mf_ultralight_emulate_on_event(void* context, SceneManagerEvent e NFC_MF_UL_DATA_CHANGED) { scene_manager_set_scene_state( nfc->scene_manager, NfcSceneMfUltralightEmulate, NFC_MF_UL_DATA_NOT_CHANGED); - nfc_device_save_shadow(nfc->dev, nfc->dev->dev_name); + // Save shadow file + if(furi_string_size(nfc->dev->load_path)) { + nfc_device_save_shadow(nfc->dev, furi_string_get_cstr(nfc->dev->load_path)); + } } consumed = false; } diff --git a/applications/main/nfc/scenes/nfc_scene_save_name.c b/applications/main/nfc/scenes/nfc_scene_save_name.c index 791f8d99b..ca4b1a350 100644 --- a/applications/main/nfc/scenes/nfc_scene_save_name.c +++ b/applications/main/nfc/scenes/nfc_scene_save_name.c @@ -62,7 +62,7 @@ bool nfc_scene_save_name_on_event(void* context, SceneManagerEvent event) { nfc->dev->dev_data.nfc_data = nfc->dev_edit_data; } strlcpy(nfc->dev->dev_name, nfc->text_store, strlen(nfc->text_store) + 1); - if(nfc_device_save(nfc->dev, nfc->text_store)) { + if(nfc_save_file(nfc)) { scene_manager_next_scene(nfc->scene_manager, NfcSceneSaveSuccess); if(!scene_manager_has_previous_scene(nfc->scene_manager, NfcSceneSavedMenu)) { // Nothing, do not count editing as saving diff --git a/applications/main/nfc/scenes/nfc_scene_set_uid.c b/applications/main/nfc/scenes/nfc_scene_set_uid.c index 1d3fb5eb9..5f0f52f6e 100644 --- a/applications/main/nfc/scenes/nfc_scene_set_uid.c +++ b/applications/main/nfc/scenes/nfc_scene_set_uid.c @@ -31,7 +31,7 @@ bool nfc_scene_set_uid_on_event(void* context, SceneManagerEvent event) { if(event.event == NfcCustomEventByteInputDone) { if(scene_manager_has_previous_scene(nfc->scene_manager, NfcSceneSavedMenu)) { nfc->dev->dev_data.nfc_data = nfc->dev_edit_data; - if(nfc_device_save(nfc->dev, nfc->dev->dev_name)) { + if(nfc_save_file(nfc)) { scene_manager_next_scene(nfc->scene_manager, NfcSceneSaveSuccess); consumed = true; } @@ -41,6 +41,7 @@ bool nfc_scene_set_uid_on_event(void* context, SceneManagerEvent event) { } } } + return consumed; } diff --git a/fbt_options.py b/fbt_options.py index 11124b936..a00f7c1b6 100644 --- a/fbt_options.py +++ b/fbt_options.py @@ -81,6 +81,7 @@ FIRMWARE_APPS = { "basic_services", "updater_app", "unit_tests", + "nfc", ], } diff --git a/firmware/targets/f7/api_symbols.csv b/firmware/targets/f7/api_symbols.csv index 0c10258f7..d6e522a2b 100644 --- a/firmware/targets/f7/api_symbols.csv +++ b/firmware/targets/f7/api_symbols.csv @@ -1,5 +1,5 @@ entry,status,name,type,params -Version,+,7.3,, +Version,+,7.4,, Header,+,applications/services/bt/bt_service/bt.h,, Header,+,applications/services/cli/cli.h,, Header,+,applications/services/cli/cli_vcp.h,, @@ -1844,6 +1844,7 @@ Function,-,mf_classic_get_read_sectors_and_keys,void,"MfClassicData*, uint8_t*, Function,-,mf_classic_get_sector_by_block,uint8_t,uint8_t Function,-,mf_classic_get_sector_trailer_block_num_by_sector,uint8_t,uint8_t Function,-,mf_classic_get_sector_trailer_by_sector,MfClassicSectorTrailer*,"MfClassicData*, uint8_t" +Function,-,mf_classic_get_total_block_num,uint16_t,MfClassicType Function,-,mf_classic_get_total_sectors_num,uint8_t,MfClassicType Function,-,mf_classic_get_type_str,const char*,MfClassicType Function,-,mf_classic_is_allowed_access_data_block,_Bool,"MfClassicData*, uint8_t, MfClassicKey, MfClassicAction" diff --git a/lib/nfc/nfc_device.c b/lib/nfc/nfc_device.c index a5e3fc14f..9f17aed3a 100644 --- a/lib/nfc/nfc_device.c +++ b/lib/nfc/nfc_device.c @@ -1006,12 +1006,7 @@ static void nfc_device_get_shadow_path(FuriString* orig_path, FuriString* shadow furi_string_cat_printf(shadow_path, "%s", NFC_APP_SHADOW_EXTENSION); } -static bool nfc_device_save_file( - NfcDevice* dev, - const char* dev_name, - const char* folder, - const char* extension, - bool use_load_path) { +bool nfc_device_save(NfcDevice* dev, const char* dev_name) { furi_assert(dev); bool saved = false; @@ -1021,19 +1016,10 @@ static bool nfc_device_save_file( temp_str = furi_string_alloc(); do { - if(use_load_path && !furi_string_empty(dev->load_path)) { - // Get directory name - path_extract_dirname(furi_string_get_cstr(dev->load_path), temp_str); - // Create nfc directory if necessary - if(!storage_simply_mkdir(dev->storage, furi_string_get_cstr(temp_str))) break; - // Make path to file to save - furi_string_cat_printf(temp_str, "/%s%s", dev_name, extension); - } else { - // Create nfc directory if necessary - if(!storage_simply_mkdir(dev->storage, NFC_APP_FOLDER)) break; - // First remove nfc device file if it was saved - furi_string_printf(temp_str, "%s/%s%s", folder, dev_name, extension); - } + // Create nfc directory if necessary + if(!storage_simply_mkdir(dev->storage, NFC_APP_FOLDER)) break; + // First remove nfc device file if it was saved + furi_string_printf(temp_str, "%s", dev_name); // Open file if(!flipper_format_file_open_always(file, furi_string_get_cstr(temp_str))) break; // Write header @@ -1072,13 +1058,19 @@ static bool nfc_device_save_file( return saved; } -bool nfc_device_save(NfcDevice* dev, const char* dev_name) { - return nfc_device_save_file(dev, dev_name, NFC_APP_FOLDER, NFC_APP_EXTENSION, true); -} - -bool nfc_device_save_shadow(NfcDevice* dev, const char* dev_name) { +bool nfc_device_save_shadow(NfcDevice* dev, const char* path) { dev->shadow_file_exist = true; - return nfc_device_save_file(dev, dev_name, NFC_APP_FOLDER, NFC_APP_SHADOW_EXTENSION, true); + // Replace extension from .nfc to .shd if necessary + FuriString* orig_path = furi_string_alloc(); + furi_string_set_str(orig_path, path); + FuriString* shadow_path = furi_string_alloc(); + nfc_device_get_shadow_path(orig_path, shadow_path); + + bool file_saved = nfc_device_save(dev, furi_string_get_cstr(shadow_path)); + furi_string_free(orig_path); + furi_string_free(shadow_path); + + return file_saved; } static bool nfc_device_load_data(NfcDevice* dev, FuriString* path, bool show_dialog) { @@ -1195,7 +1187,7 @@ bool nfc_file_select(NfcDevice* dev) { }; bool res = - dialog_file_browser_show(dev->dialogs, dev->load_path, nfc_app_folder, &browser_options); + dialog_file_browser_show(dev->dialogs, dev->load_path, dev->load_path, &browser_options); furi_string_free(nfc_app_folder); if(res) { diff --git a/lib/nfc/protocols/mifare_classic.c b/lib/nfc/protocols/mifare_classic.c index 7b0e17975..b7a52bc01 100644 --- a/lib/nfc/protocols/mifare_classic.c +++ b/lib/nfc/protocols/mifare_classic.c @@ -82,7 +82,7 @@ uint8_t mf_classic_get_total_sectors_num(MfClassicType type) { } } -static uint16_t mf_classic_get_total_block_num(MfClassicType type) { +uint16_t mf_classic_get_total_block_num(MfClassicType type) { if(type == MfClassicType1k) { return 64; } else if(type == MfClassicType4k) { diff --git a/lib/nfc/protocols/mifare_classic.h b/lib/nfc/protocols/mifare_classic.h index d5467b100..9a0bb5790 100644 --- a/lib/nfc/protocols/mifare_classic.h +++ b/lib/nfc/protocols/mifare_classic.h @@ -98,6 +98,8 @@ MfClassicType mf_classic_get_classic_type(int8_t ATQA0, uint8_t ATQA1, uint8_t S uint8_t mf_classic_get_total_sectors_num(MfClassicType type); +uint16_t mf_classic_get_total_block_num(MfClassicType type); + uint8_t mf_classic_get_sector_trailer_block_num_by_sector(uint8_t sector); bool mf_classic_is_sector_trailer(uint8_t block); From e7c4b40dbe14f71275639ac9ba9a712c17a5e566 Mon Sep 17 00:00:00 2001 From: Astra <93453568+Astrrra@users.noreply.github.com> Date: Thu, 10 Nov 2022 18:29:57 +0200 Subject: [PATCH 41/49] Force card types in extra actions (#1961) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Mifare Classic forced read * Add all the needed card types * nfc: remove unused scene * nfc: remove unused worker state * nfc: fix read card type scene state usage * nfc: move NfcReadMode to NfcDevData struct * nfc: fix bank card reading and scene navigation * nfc magic: fix magic deactifate function Co-authored-by: gornekich Co-authored-by: あく --- .../main/nfc/scenes/nfc_scene_config.h | 1 + .../main/nfc/scenes/nfc_scene_exit_confirm.c | 9 +- .../main/nfc/scenes/nfc_scene_extra_actions.c | 13 +++ .../nfc/scenes/nfc_scene_read_card_type.c | 97 +++++++++++++++++++ .../main/nfc/scenes/nfc_scene_start.c | 2 + .../plugins/nfc_magic/lib/magic/magic.c | 2 +- .../plugins/nfc_magic/nfc_magic_worker.c | 1 - lib/nfc/nfc_device.h | 10 ++ lib/nfc/nfc_worker.c | 81 +++++++++++++++- lib/nfc/nfc_worker_i.h | 2 + 10 files changed, 213 insertions(+), 5 deletions(-) create mode 100644 applications/main/nfc/scenes/nfc_scene_read_card_type.c diff --git a/applications/main/nfc/scenes/nfc_scene_config.h b/applications/main/nfc/scenes/nfc_scene_config.h index 9b922add2..49c8412c5 100644 --- a/applications/main/nfc/scenes/nfc_scene_config.h +++ b/applications/main/nfc/scenes/nfc_scene_config.h @@ -60,3 +60,4 @@ ADD_SCENE(nfc, detect_reader, DetectReader) ADD_SCENE(nfc, mfkey_nonces_info, MfkeyNoncesInfo) ADD_SCENE(nfc, mfkey_complete, MfkeyComplete) ADD_SCENE(nfc, nfc_data_info, NfcDataInfo) +ADD_SCENE(nfc, read_card_type, ReadCardType) diff --git a/applications/main/nfc/scenes/nfc_scene_exit_confirm.c b/applications/main/nfc/scenes/nfc_scene_exit_confirm.c index b22dd42a1..3ce4f6de8 100644 --- a/applications/main/nfc/scenes/nfc_scene_exit_confirm.c +++ b/applications/main/nfc/scenes/nfc_scene_exit_confirm.c @@ -29,8 +29,13 @@ bool nfc_scene_exit_confirm_on_event(void* context, SceneManagerEvent event) { if(event.event == DialogExResultRight) { consumed = scene_manager_previous_scene(nfc->scene_manager); } else if(event.event == DialogExResultLeft) { - consumed = scene_manager_search_and_switch_to_previous_scene( - nfc->scene_manager, NfcSceneStart); + if(scene_manager_has_previous_scene(nfc->scene_manager, NfcSceneReadCardType)) { + consumed = scene_manager_search_and_switch_to_previous_scene( + nfc->scene_manager, NfcSceneReadCardType); + } else { + consumed = scene_manager_search_and_switch_to_previous_scene( + nfc->scene_manager, NfcSceneStart); + } } } else if(event.type == SceneManagerEventTypeBack) { consumed = true; diff --git a/applications/main/nfc/scenes/nfc_scene_extra_actions.c b/applications/main/nfc/scenes/nfc_scene_extra_actions.c index e888e9d35..fc6021d73 100644 --- a/applications/main/nfc/scenes/nfc_scene_extra_actions.c +++ b/applications/main/nfc/scenes/nfc_scene_extra_actions.c @@ -1,6 +1,7 @@ #include "../nfc_i.h" enum SubmenuIndex { + SubmenuIndexReadCardType, SubmenuIndexMfClassicKeys, SubmenuIndexMfUltralightUnlock, }; @@ -15,6 +16,12 @@ void nfc_scene_extra_actions_on_enter(void* context) { Nfc* nfc = context; Submenu* submenu = nfc->submenu; + submenu_add_item( + submenu, + "Read Specific Card Type", + SubmenuIndexReadCardType, + nfc_scene_extra_actions_submenu_callback, + nfc); submenu_add_item( submenu, "Mifare Classic Keys", @@ -44,9 +51,15 @@ bool nfc_scene_extra_actions_on_event(void* context, SceneManagerEvent event) { consumed = true; } else if(event.event == SubmenuIndexMfUltralightUnlock) { scene_manager_next_scene(nfc->scene_manager, NfcSceneMfUltralightUnlockMenu); + consumed = true; + } else if(event.event == SubmenuIndexReadCardType) { + scene_manager_set_scene_state(nfc->scene_manager, NfcSceneReadCardType, 0); + scene_manager_next_scene(nfc->scene_manager, NfcSceneReadCardType); + consumed = true; } scene_manager_set_scene_state(nfc->scene_manager, NfcSceneExtraActions, event.event); } + return consumed; } diff --git a/applications/main/nfc/scenes/nfc_scene_read_card_type.c b/applications/main/nfc/scenes/nfc_scene_read_card_type.c new file mode 100644 index 000000000..94262aa1e --- /dev/null +++ b/applications/main/nfc/scenes/nfc_scene_read_card_type.c @@ -0,0 +1,97 @@ +#include "../nfc_i.h" +#include "nfc_worker_i.h" + +enum SubmenuIndex { + SubmenuIndexReadMifareClassic, + SubmenuIndexReadMifareDesfire, + SubmenuIndexReadMfUltralight, + SubmenuIndexReadEMV, + SubmenuIndexReadNFCA, +}; + +void nfc_scene_read_card_type_submenu_callback(void* context, uint32_t index) { + Nfc* nfc = context; + + view_dispatcher_send_custom_event(nfc->view_dispatcher, index); +} + +void nfc_scene_read_card_type_on_enter(void* context) { + Nfc* nfc = context; + Submenu* submenu = nfc->submenu; + + submenu_add_item( + submenu, + "Read Mifare Classic", + SubmenuIndexReadMifareClassic, + nfc_scene_read_card_type_submenu_callback, + nfc); + submenu_add_item( + submenu, + "Read Mifare DESFire", + SubmenuIndexReadMifareDesfire, + nfc_scene_read_card_type_submenu_callback, + nfc); + submenu_add_item( + submenu, + "Read NTAG/Ultralight", + SubmenuIndexReadMfUltralight, + nfc_scene_read_card_type_submenu_callback, + nfc); + submenu_add_item( + submenu, + "Read EMV card", + SubmenuIndexReadEMV, + nfc_scene_read_card_type_submenu_callback, + nfc); + submenu_add_item( + submenu, + "Read NFC-A data", + SubmenuIndexReadNFCA, + nfc_scene_read_card_type_submenu_callback, + nfc); + uint32_t state = scene_manager_get_scene_state(nfc->scene_manager, NfcSceneReadCardType); + submenu_set_selected_item(submenu, state); + + view_dispatcher_switch_to_view(nfc->view_dispatcher, NfcViewMenu); +} + +bool nfc_scene_read_card_type_on_event(void* context, SceneManagerEvent event) { + Nfc* nfc = context; + bool consumed = false; + + if(event.type == SceneManagerEventTypeCustom) { + if(event.event == SubmenuIndexReadMifareClassic) { + nfc->dev->dev_data.read_mode = NfcReadModeMfClassic; + scene_manager_next_scene(nfc->scene_manager, NfcSceneRead); + consumed = true; + } + if(event.event == SubmenuIndexReadMifareDesfire) { + nfc->dev->dev_data.read_mode = NfcReadModeMfDesfire; + scene_manager_next_scene(nfc->scene_manager, NfcSceneRead); + consumed = true; + } + if(event.event == SubmenuIndexReadMfUltralight) { + nfc->dev->dev_data.read_mode = NfcReadModeMfUltralight; + scene_manager_next_scene(nfc->scene_manager, NfcSceneRead); + consumed = true; + } + if(event.event == SubmenuIndexReadEMV) { + nfc->dev->dev_data.read_mode = NfcReadModeEMV; + scene_manager_next_scene(nfc->scene_manager, NfcSceneRead); + consumed = true; + } + if(event.event == SubmenuIndexReadNFCA) { + nfc->dev->dev_data.read_mode = NfcReadModeNFCA; + scene_manager_next_scene(nfc->scene_manager, NfcSceneRead); + consumed = true; + } + scene_manager_set_scene_state(nfc->scene_manager, NfcSceneReadCardType, event.event); + } + return consumed; +} + +void nfc_scene_read_card_type_on_exit(void* context) { + Nfc* nfc = context; + + submenu_reset(nfc->submenu); +} diff --git a/applications/main/nfc/scenes/nfc_scene_start.c b/applications/main/nfc/scenes/nfc_scene_start.c index 028f85ae0..f8b9f52c6 100644 --- a/applications/main/nfc/scenes/nfc_scene_start.c +++ b/applications/main/nfc/scenes/nfc_scene_start.c @@ -1,4 +1,5 @@ #include "../nfc_i.h" +#include "nfc_worker_i.h" #include enum SubmenuIndex { @@ -47,6 +48,7 @@ bool nfc_scene_start_on_event(void* context, SceneManagerEvent event) { if(event.type == SceneManagerEventTypeCustom) { if(event.event == SubmenuIndexRead) { + nfc->dev->dev_data.read_mode = NfcReadModeAuto; scene_manager_next_scene(nfc->scene_manager, NfcSceneRead); DOLPHIN_DEED(DolphinDeedNfcRead); consumed = true; diff --git a/applications/plugins/nfc_magic/lib/magic/magic.c b/applications/plugins/nfc_magic/lib/magic/magic.c index 3cfca748b..a922bc7a8 100644 --- a/applications/plugins/nfc_magic/lib/magic/magic.c +++ b/applications/plugins/nfc_magic/lib/magic/magic.c @@ -210,5 +210,5 @@ bool magic_wipe() { void magic_deactivate() { furi_hal_nfc_ll_txrx_off(); - furi_hal_nfc_start_sleep(); + furi_hal_nfc_sleep(); } diff --git a/applications/plugins/nfc_magic/nfc_magic_worker.c b/applications/plugins/nfc_magic/nfc_magic_worker.c index 0623211e2..0e1f6cea4 100644 --- a/applications/plugins/nfc_magic/nfc_magic_worker.c +++ b/applications/plugins/nfc_magic/nfc_magic_worker.c @@ -167,7 +167,6 @@ void nfc_magic_worker_wipe(NfcMagicWorker* nfc_magic_worker) { if(!magic_data_access_cmd()) continue; if(!magic_write_blk(0, &block)) continue; nfc_magic_worker->callback(NfcMagicWorkerEventSuccess, nfc_magic_worker->context); - magic_deactivate(); break; } magic_deactivate(); diff --git a/lib/nfc/nfc_device.h b/lib/nfc/nfc_device.h index c8e8517ae..3d302c18b 100644 --- a/lib/nfc/nfc_device.h +++ b/lib/nfc/nfc_device.h @@ -51,9 +51,19 @@ typedef struct { MfClassicDict* dict; } NfcMfClassicDictAttackData; +typedef enum { + NfcReadModeAuto, + NfcReadModeMfClassic, + NfcReadModeMfUltralight, + NfcReadModeMfDesfire, + NfcReadModeEMV, + NfcReadModeNFCA, +} NfcReadMode; + typedef struct { FuriHalNfcDevData nfc_data; NfcProtocol protocol; + NfcReadMode read_mode; union { NfcReaderRequestData reader_data; NfcMfClassicDictAttackData mf_classic_dict_attack_data; diff --git a/lib/nfc/nfc_worker.c b/lib/nfc/nfc_worker.c index 5ce543636..5ad37c6e3 100644 --- a/lib/nfc/nfc_worker.c +++ b/lib/nfc/nfc_worker.c @@ -90,7 +90,11 @@ int32_t nfc_worker_task(void* context) { furi_hal_nfc_exit_sleep(); if(nfc_worker->state == NfcWorkerStateRead) { - nfc_worker_read(nfc_worker); + if(nfc_worker->dev_data->read_mode == NfcReadModeAuto) { + nfc_worker_read(nfc_worker); + } else { + nfc_worker_read_type(nfc_worker); + } } else if(nfc_worker->state == NfcWorkerStateUidEmulate) { nfc_worker_emulate_uid(nfc_worker); } else if(nfc_worker->state == NfcWorkerStateEmulateApdu) { @@ -394,6 +398,81 @@ void nfc_worker_read(NfcWorker* nfc_worker) { } } +void nfc_worker_read_type(NfcWorker* nfc_worker) { + furi_assert(nfc_worker); + furi_assert(nfc_worker->callback); + + NfcReadMode read_mode = nfc_worker->dev_data->read_mode; + nfc_device_data_clear(nfc_worker->dev_data); + NfcDeviceData* dev_data = nfc_worker->dev_data; + FuriHalNfcDevData* nfc_data = &nfc_worker->dev_data->nfc_data; + FuriHalNfcTxRxContext tx_rx = {}; + NfcWorkerEvent event = 0; + bool card_not_detected_notified = false; + + while(nfc_worker->state == NfcWorkerStateRead) { + if(furi_hal_nfc_detect(nfc_data, 300)) { + FURI_LOG_D(TAG, "Card detected"); + furi_hal_nfc_sleep(); + // Process first found device + nfc_worker->callback(NfcWorkerEventCardDetected, nfc_worker->context); + card_not_detected_notified = false; + if(nfc_data->type == FuriHalNfcTypeA) { + if(read_mode == NfcReadModeMfClassic) { + nfc_worker->dev_data->protocol = NfcDeviceProtocolMifareClassic; + nfc_worker->dev_data->mf_classic_data.type = mf_classic_get_classic_type( + nfc_data->atqa[0], nfc_data->atqa[1], nfc_data->sak); + if(nfc_worker_read_mf_classic(nfc_worker, &tx_rx)) { + FURI_LOG_D(TAG, "Card read"); + dev_data->protocol = NfcDeviceProtocolMifareClassic; + event = NfcWorkerEventReadMfClassicDone; + break; + } else { + FURI_LOG_D(TAG, "Card read failed"); + dev_data->protocol = NfcDeviceProtocolMifareClassic; + event = NfcWorkerEventReadMfClassicDictAttackRequired; + break; + } + } else if(read_mode == NfcReadModeMfUltralight) { + FURI_LOG_I(TAG, "Mifare Ultralight / NTAG"); + nfc_worker->dev_data->protocol = NfcDeviceProtocolMifareUl; + if(nfc_worker_read_mf_ultralight(nfc_worker, &tx_rx)) { + event = NfcWorkerEventReadMfUltralight; + break; + } + } else if(read_mode == NfcReadModeMfDesfire) { + nfc_worker->dev_data->protocol = NfcDeviceProtocolMifareDesfire; + if(nfc_worker_read_mf_desfire(nfc_worker, &tx_rx)) { + event = NfcWorkerEventReadMfDesfire; + break; + } + } else if(read_mode == NfcReadModeEMV) { + nfc_worker->dev_data->protocol = NfcDeviceProtocolEMV; + if(nfc_worker_read_bank_card(nfc_worker, &tx_rx)) { + event = NfcWorkerEventReadBankCard; + break; + } + } else if(read_mode == NfcReadModeNFCA) { + nfc_worker->dev_data->protocol = NfcDeviceProtocolUnknown; + event = NfcWorkerEventReadUidNfcA; + break; + } + } else { + if(!card_not_detected_notified) { + nfc_worker->callback(NfcWorkerEventNoCardDetected, nfc_worker->context); + card_not_detected_notified = true; + } + } + } + furi_hal_nfc_sleep(); + furi_delay_ms(100); + } + // Notify caller and exit + if(event > NfcWorkerEventReserved) { + nfc_worker->callback(event, nfc_worker->context); + } +} + void nfc_worker_emulate_uid(NfcWorker* nfc_worker) { FuriHalNfcTxRxContext tx_rx = {}; FuriHalNfcDevData* data = &nfc_worker->dev_data->nfc_data; diff --git a/lib/nfc/nfc_worker_i.h b/lib/nfc/nfc_worker_i.h index b9f69e620..9733426ab 100644 --- a/lib/nfc/nfc_worker_i.h +++ b/lib/nfc/nfc_worker_i.h @@ -35,6 +35,8 @@ int32_t nfc_worker_task(void* context); void nfc_worker_read(NfcWorker* nfc_worker); +void nfc_worker_read_type(NfcWorker* nfc_worker); + void nfc_worker_emulate_uid(NfcWorker* nfc_worker); void nfc_worker_emulate_mf_ultralight(NfcWorker* nfc_worker); From aec36e7041a24af68d04d57b024726d909d29c82 Mon Sep 17 00:00:00 2001 From: lauaall <48836917+skelq@users.noreply.github.com> Date: Thu, 10 Nov 2022 18:48:58 +0200 Subject: [PATCH 42/49] Fixed typos (#1999) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sergey Gavrilov Co-authored-by: あく --- documentation/KeyCombo.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/documentation/KeyCombo.md b/documentation/KeyCombo.md index 359fd5b9b..e6f55dc52 100644 --- a/documentation/KeyCombo.md +++ b/documentation/KeyCombo.md @@ -1,7 +1,7 @@ # Key Combos -There are times when your flipper feels blue and don't respond to your commands. -In that case you may find this guide useful. +There are times when your flipper feels blue and doesn't respond to your commands. +In that case, you may find this guide useful. ## Basic Combos @@ -9,7 +9,7 @@ In that case you may find this guide useful. ### Hardware Reset -- Press `LEFT` and `BACK` and hold for couple seconds +- Press `LEFT` and `BACK` and hold for a couple of seconds - Release `LEFT` and `BACK` This combo performs hardware reset by pulling MCU reset line down. @@ -29,7 +29,7 @@ There is 1 case when it's not working: - If you have not disconnected USB, then disconnect USB and repeat previous step - Release `BACK` key -This combo performs reset by switching SYS power line off and then on. +This combo performs a reset by switching SYS power line off and then on. Main components involved: Keys -> DD6(bq25896, charger) There is 1 case when it's not working: @@ -60,13 +60,13 @@ There is 1 case when it's not working: ### Hardware Reset + Software DFU -- Press `LEFT` and `BACK` and hold for couple seconds +- Press `LEFT` and `BACK` and hold for a couple of seconds - Release `BACK` - Device will enter DFU with indication (Blue LED + DFU Screen) - Release `LEFT` This combo performs hardware reset by pulling MCU reset line down. -Then `LEFT` key indicates to boot-loader that DFU mode requested. +Then `LEFT` key indicates to boot-loader that DFU mode is requested. There are 2 cases when it's not working: @@ -76,7 +76,7 @@ There are 2 cases when it's not working: ### Hardware Reset + Hardware DFU -- Press `LEFT` and `BACK` and `OK` and hold for couple seconds +- Press `LEFT` and `BACK` and `OK` and hold for a couple of seconds - Release `BACK` and `LEFT` - Device will enter DFU without indication @@ -127,8 +127,8 @@ There are 2 cases when it's not working: If none of the described methods were useful: -- Ensure battery charged -- Disconnect battery and connect again (Requires disassembly) -- Try to Flash device with ST-Link or other programmer that support SWD +- Ensure the battery charged +- Disconnect the battery and connect again (Requires disassembly) +- Try to Flash device with ST-Link or other programmer that supports SWD -If you still here and your device is not working: it's not software issue. +If you still here and your device is not working: it's not a software issue. From 721ab717d784859f7076175d22991b6d950c361e Mon Sep 17 00:00:00 2001 From: Skorpionm <85568270+Skorpionm@users.noreply.github.com> Date: Thu, 10 Nov 2022 21:14:44 +0400 Subject: [PATCH 43/49] [FL-2961] SubGhz: properly handle storage loss (#1990) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: あく --- .../subghz/scenes/subghz_scene_more_raw.c | 38 +++++++++++++------ .../subghz/scenes/subghz_scene_read_raw.c | 38 ++++++++++++++----- applications/main/subghz/subghz_i.c | 17 +++++++++ applications/main/subghz/subghz_i.h | 1 + 4 files changed, 74 insertions(+), 20 deletions(-) diff --git a/applications/main/subghz/scenes/subghz_scene_more_raw.c b/applications/main/subghz/scenes/subghz_scene_more_raw.c index d75ab13c7..864be1ed1 100644 --- a/applications/main/subghz/scenes/subghz_scene_more_raw.c +++ b/applications/main/subghz/scenes/subghz_scene_more_raw.c @@ -38,18 +38,34 @@ bool subghz_scene_more_raw_on_event(void* context, SceneManagerEvent event) { if(event.type == SceneManagerEventTypeCustom) { if(event.event == SubmenuIndexDelete) { - scene_manager_set_scene_state( - subghz->scene_manager, SubGhzSceneReadRAW, SubGhzCustomEventManagerNoSet); - scene_manager_set_scene_state( - subghz->scene_manager, SubGhzSceneMoreRAW, SubmenuIndexDelete); - scene_manager_next_scene(subghz->scene_manager, SubGhzSceneDeleteRAW); - return true; + if(subghz_file_available(subghz)) { + scene_manager_set_scene_state( + subghz->scene_manager, SubGhzSceneReadRAW, SubGhzCustomEventManagerNoSet); + scene_manager_set_scene_state( + subghz->scene_manager, SubGhzSceneMoreRAW, SubmenuIndexDelete); + scene_manager_next_scene(subghz->scene_manager, SubGhzSceneDeleteRAW); + return true; + } else { + if(!scene_manager_search_and_switch_to_previous_scene( + subghz->scene_manager, SubGhzSceneStart)) { + scene_manager_stop(subghz->scene_manager); + view_dispatcher_stop(subghz->view_dispatcher); + } + } } else if(event.event == SubmenuIndexEdit) { - furi_string_reset(subghz->file_path_tmp); - scene_manager_set_scene_state( - subghz->scene_manager, SubGhzSceneMoreRAW, SubmenuIndexEdit); - scene_manager_next_scene(subghz->scene_manager, SubGhzSceneSaveName); - return true; + if(subghz_file_available(subghz)) { + furi_string_reset(subghz->file_path_tmp); + scene_manager_set_scene_state( + subghz->scene_manager, SubGhzSceneMoreRAW, SubmenuIndexEdit); + scene_manager_next_scene(subghz->scene_manager, SubGhzSceneSaveName); + return true; + } else { + if(!scene_manager_search_and_switch_to_previous_scene( + subghz->scene_manager, SubGhzSceneStart)) { + scene_manager_stop(subghz->scene_manager); + view_dispatcher_stop(subghz->view_dispatcher); + } + } } } return false; diff --git a/applications/main/subghz/scenes/subghz_scene_read_raw.c b/applications/main/subghz/scenes/subghz_scene_read_raw.c index ba9ce803b..2e5ba0966 100644 --- a/applications/main/subghz/scenes/subghz_scene_read_raw.c +++ b/applications/main/subghz/scenes/subghz_scene_read_raw.c @@ -198,20 +198,28 @@ bool subghz_scene_read_raw_on_event(void* context, SceneManagerEvent event) { break; case SubGhzCustomEventViewReadRAWMore: - if(subghz_scene_read_raw_update_filename(subghz)) { - scene_manager_set_scene_state( - subghz->scene_manager, SubGhzSceneReadRAW, SubGhzCustomEventManagerSet); - subghz->txrx->rx_key_state = SubGhzRxKeyStateRAWLoad; - scene_manager_next_scene(subghz->scene_manager, SubGhzSceneMoreRAW); - consumed = true; + if(subghz_file_available(subghz)) { + if(subghz_scene_read_raw_update_filename(subghz)) { + scene_manager_set_scene_state( + subghz->scene_manager, SubGhzSceneReadRAW, SubGhzCustomEventManagerSet); + subghz->txrx->rx_key_state = SubGhzRxKeyStateRAWLoad; + scene_manager_next_scene(subghz->scene_manager, SubGhzSceneMoreRAW); + consumed = true; + } else { + furi_crash("SubGhz: RAW file name update error."); + } } else { - furi_crash("SubGhz: RAW file name update error."); + if(!scene_manager_search_and_switch_to_previous_scene( + subghz->scene_manager, SubGhzSceneStart)) { + scene_manager_stop(subghz->scene_manager); + view_dispatcher_stop(subghz->view_dispatcher); + } } break; case SubGhzCustomEventViewReadRAWSendStart: - if(subghz_scene_read_raw_update_filename(subghz)) { + if(subghz_file_available(subghz) && subghz_scene_read_raw_update_filename(subghz)) { //start send subghz->state_notifications = SubGhzNotificationStateIDLE; if(subghz->txrx->txrx_state == SubGhzTxRxStateRx) { @@ -238,6 +246,12 @@ bool subghz_scene_read_raw_on_event(void* context, SceneManagerEvent event) { subghz->state_notifications = SubGhzNotificationStateTx; } } + } else { + if(!scene_manager_search_and_switch_to_previous_scene( + subghz->scene_manager, SubGhzSceneStart)) { + scene_manager_stop(subghz->scene_manager); + view_dispatcher_stop(subghz->view_dispatcher); + } } consumed = true; break; @@ -314,11 +328,17 @@ bool subghz_scene_read_raw_on_event(void* context, SceneManagerEvent event) { break; case SubGhzCustomEventViewReadRAWSave: - if(subghz_scene_read_raw_update_filename(subghz)) { + if(subghz_file_available(subghz) && subghz_scene_read_raw_update_filename(subghz)) { scene_manager_set_scene_state( subghz->scene_manager, SubGhzSceneReadRAW, SubGhzCustomEventManagerSetRAW); subghz->txrx->rx_key_state = SubGhzRxKeyStateBack; scene_manager_next_scene(subghz->scene_manager, SubGhzSceneSaveName); + } else { + if(!scene_manager_search_and_switch_to_previous_scene( + subghz->scene_manager, SubGhzSceneStart)) { + scene_manager_stop(subghz->scene_manager); + view_dispatcher_stop(subghz->view_dispatcher); + } } consumed = true; break; diff --git a/applications/main/subghz/subghz_i.c b/applications/main/subghz/subghz_i.c index a887b6543..d9070ba8c 100644 --- a/applications/main/subghz/subghz_i.c +++ b/applications/main/subghz/subghz_i.c @@ -490,6 +490,23 @@ bool subghz_rename_file(SubGhz* subghz) { return ret; } +bool subghz_file_available(SubGhz* subghz) { + furi_assert(subghz); + bool ret = true; + Storage* storage = furi_record_open(RECORD_STORAGE); + + FS_Error fs_result = + storage_common_stat(storage, furi_string_get_cstr(subghz->file_path), NULL); + + if(fs_result != FSE_OK) { + dialog_message_show_storage_error(subghz->dialogs, "File not available\n file/directory"); + ret = false; + } + + furi_record_close(RECORD_STORAGE); + return ret; +} + bool subghz_delete_file(SubGhz* subghz) { furi_assert(subghz); diff --git a/applications/main/subghz/subghz_i.h b/applications/main/subghz/subghz_i.h index 46768cf02..0a40196bb 100644 --- a/applications/main/subghz/subghz_i.h +++ b/applications/main/subghz/subghz_i.h @@ -124,6 +124,7 @@ bool subghz_save_protocol_to_file( const char* dev_file_name); bool subghz_load_protocol_from_file(SubGhz* subghz); bool subghz_rename_file(SubGhz* subghz); +bool subghz_file_available(SubGhz* subghz); bool subghz_delete_file(SubGhz* subghz); void subghz_file_name_clear(SubGhz* subghz); bool subghz_path_is_file(FuriString* path); From 90cefe7c7198d09c2bcaf43eff0e8ded54c761c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=82=E3=81=8F?= Date: Sat, 12 Nov 2022 17:46:04 +0900 Subject: [PATCH 44/49] [FL-2975] Bug fixes and improvements: Furi, Input, Cli (#2004) * Furi: configurable heap allocation tracking * Furi: relax restriction in thread heap setter asserts, apply heap tracking setting on app start instead of thread allocation * Furi: hide dangerous heap tracking levels in release build * Input: fix non-working debounce --- applications/services/cli/cli_commands.c | 96 +++++++++++++++---- .../desktop/scenes/desktop_scene_main.c | 7 ++ applications/services/input/input.c | 20 +++- applications/services/loader/loader.c | 9 +- .../settings/system/system_settings.c | 37 +++++++ firmware/targets/f7/api_symbols.csv | 4 +- .../targets/f7/furi_hal/furi_hal_resources.h | 2 +- firmware/targets/f7/furi_hal/furi_hal_rtc.c | 16 +++- .../targets/furi_hal_include/furi_hal_rtc.h | 11 +++ furi/core/thread.c | 9 +- 10 files changed, 186 insertions(+), 25 deletions(-) diff --git a/applications/services/cli/cli_commands.c b/applications/services/cli/cli_commands.c index 239434b7a..64ff8ef71 100644 --- a/applications/services/cli/cli_commands.c +++ b/applications/services/cli/cli_commands.c @@ -7,6 +7,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" @@ -192,6 +193,83 @@ void cli_command_log(Cli* cli, FuriString* args, void* context) { furi_stream_buffer_free(ring); } +void cli_command_sysctl_debug(Cli* cli, FuriString* args, void* context) { + UNUSED(cli); + UNUSED(context); + if(!furi_string_cmp(args, "0")) { + furi_hal_rtc_reset_flag(FuriHalRtcFlagDebug); + loader_update_menu(); + printf("Debug disabled."); + } else if(!furi_string_cmp(args, "1")) { + furi_hal_rtc_set_flag(FuriHalRtcFlagDebug); + loader_update_menu(); + printf("Debug enabled."); + } else { + cli_print_usage("sysctl debug", "<1|0>", furi_string_get_cstr(args)); + } +} + +void cli_command_sysctl_heap_track(Cli* cli, FuriString* args, void* context) { + UNUSED(cli); + UNUSED(context); + if(!furi_string_cmp(args, "none")) { + furi_hal_rtc_set_heap_track_mode(FuriHalRtcHeapTrackModeNone); + printf("Heap tracking disabled"); + } else if(!furi_string_cmp(args, "main")) { + furi_hal_rtc_set_heap_track_mode(FuriHalRtcHeapTrackModeMain); + printf("Heap tracking enabled for application main thread"); +#if FURI_DEBUG + } else if(!furi_string_cmp(args, "tree")) { + furi_hal_rtc_set_heap_track_mode(FuriHalRtcHeapTrackModeTree); + printf("Heap tracking enabled for application main and child threads"); + } else if(!furi_string_cmp(args, "all")) { + furi_hal_rtc_set_heap_track_mode(FuriHalRtcHeapTrackModeAll); + printf("Heap tracking enabled for all threads"); +#endif + } else { + cli_print_usage("sysctl heap_track", "", furi_string_get_cstr(args)); + } +} + +void cli_command_sysctl_print_usage() { + printf("Usage:\r\n"); + printf("sysctl \r\n"); + printf("Cmd list:\r\n"); + + printf("\tdebug <0|1>\t - Enable or disable system debug\r\n"); +#if FURI_DEBUG + printf("\theap_track \t - Set heap allocation tracking mode\r\n"); +#else + printf("\theap_track \t - Set heap allocation tracking mode\r\n"); +#endif +} + +void cli_command_sysctl(Cli* cli, FuriString* args, void* context) { + FuriString* cmd; + cmd = furi_string_alloc(); + + do { + if(!args_read_string_and_trim(args, cmd)) { + cli_command_sysctl_print_usage(); + break; + } + + if(furi_string_cmp_str(cmd, "debug") == 0) { + cli_command_sysctl_debug(cli, args, context); + break; + } + + if(furi_string_cmp_str(cmd, "heap_track") == 0) { + cli_command_sysctl_heap_track(cli, args, context); + break; + } + + cli_command_sysctl_print_usage(); + } while(false); + + furi_string_free(cmd); +} + void cli_command_vibro(Cli* cli, FuriString* args, void* context) { UNUSED(cli); UNUSED(context); @@ -208,22 +286,6 @@ void cli_command_vibro(Cli* cli, FuriString* args, void* context) { } } -void cli_command_debug(Cli* cli, FuriString* args, void* context) { - UNUSED(cli); - UNUSED(context); - if(!furi_string_cmp(args, "0")) { - furi_hal_rtc_reset_flag(FuriHalRtcFlagDebug); - loader_update_menu(); - printf("Debug disabled."); - } else if(!furi_string_cmp(args, "1")) { - furi_hal_rtc_set_flag(FuriHalRtcFlagDebug); - loader_update_menu(); - printf("Debug enabled."); - } else { - cli_print_usage("debug", "<1|0>", furi_string_get_cstr(args)); - } -} - void cli_command_led(Cli* cli, FuriString* args, void* context) { UNUSED(cli); UNUSED(context); @@ -356,7 +418,7 @@ void cli_commands_init(Cli* cli) { cli_add_command(cli, "date", CliCommandFlagParallelSafe, cli_command_date, NULL); cli_add_command(cli, "log", CliCommandFlagParallelSafe, cli_command_log, NULL); - cli_add_command(cli, "debug", CliCommandFlagDefault, cli_command_debug, NULL); + cli_add_command(cli, "sysctl", CliCommandFlagDefault, cli_command_sysctl, NULL); cli_add_command(cli, "ps", CliCommandFlagParallelSafe, cli_command_ps, NULL); cli_add_command(cli, "free", CliCommandFlagParallelSafe, cli_command_free, NULL); cli_add_command(cli, "free_blocks", CliCommandFlagParallelSafe, cli_command_free_blocks, NULL); diff --git a/applications/services/desktop/scenes/desktop_scene_main.c b/applications/services/desktop/scenes/desktop_scene_main.c index 654de44d5..4f01ad5be 100644 --- a/applications/services/desktop/scenes/desktop_scene_main.c +++ b/applications/services/desktop/scenes/desktop_scene_main.c @@ -45,6 +45,13 @@ static void desktop_switch_to_app(Desktop* desktop, const FlipperApplication* fl return; } + FuriHalRtcHeapTrackMode mode = furi_hal_rtc_get_heap_track_mode(); + if(mode > FuriHalRtcHeapTrackModeNone) { + furi_thread_enable_heap_trace(desktop->scene_thread); + } else { + furi_thread_disable_heap_trace(desktop->scene_thread); + } + furi_thread_set_name(desktop->scene_thread, flipper_app->name); furi_thread_set_stack_size(desktop->scene_thread, flipper_app->stack_size); furi_thread_set_callback(desktop->scene_thread, flipper_app->app); diff --git a/applications/services/input/input.c b/applications/services/input/input.c index d1aef9e8a..1d02df1e5 100644 --- a/applications/services/input/input.c +++ b/applications/services/input/input.c @@ -1,5 +1,7 @@ #include "input_i.h" +// #define INPUT_DEBUG + #define GPIO_Read(input_pin) (furi_hal_gpio_read(input_pin.pin->gpio) ^ (input_pin.pin->inverted)) static Input* input = NULL; @@ -72,6 +74,10 @@ int32_t input_srv(void* p) { input->event_pubsub = furi_pubsub_alloc(); furi_record_create(RECORD_INPUT_EVENTS, input->event_pubsub); +#if INPUT_DEBUG + furi_hal_gpio_init_simple(&gpio_ext_pa4, GpioModeOutputPushPull); +#endif + #ifdef SRV_CLI input->cli = furi_record_open(RECORD_CLI); if(input->cli) { @@ -95,10 +101,16 @@ int32_t input_srv(void* p) { bool is_changing = false; for(size_t i = 0; i < input_pins_count; i++) { bool state = GPIO_Read(input->pin_states[i]); + if(state) { + if(input->pin_states[i].debounce < INPUT_DEBOUNCE_TICKS) + input->pin_states[i].debounce += 1; + } else { + if(input->pin_states[i].debounce > 0) input->pin_states[i].debounce -= 1; + } + if(input->pin_states[i].debounce > 0 && input->pin_states[i].debounce < INPUT_DEBOUNCE_TICKS) { is_changing = true; - input->pin_states[i].debounce += (state ? 1 : -1); } else if(input->pin_states[i].state != state) { input->pin_states[i].state = state; @@ -129,8 +141,14 @@ int32_t input_srv(void* p) { } if(is_changing) { +#if INPUT_DEBUG + furi_hal_gpio_write(&gpio_ext_pa4, 1); +#endif furi_delay_tick(1); } else { +#if INPUT_DEBUG + furi_hal_gpio_write(&gpio_ext_pa4, 0); +#endif furi_thread_flags_wait(INPUT_THREAD_FLAG_ISR, FuriFlagWaitAny, FuriWaitForever); } } diff --git a/applications/services/loader/loader.c b/applications/services/loader/loader.c index 712576e14..931719723 100644 --- a/applications/services/loader/loader.c +++ b/applications/services/loader/loader.c @@ -21,6 +21,13 @@ static bool FURI_LOG_I(TAG, "Starting: %s", loader_instance->application->name); + FuriHalRtcHeapTrackMode mode = furi_hal_rtc_get_heap_track_mode(); + if(mode > FuriHalRtcHeapTrackModeNone) { + furi_thread_enable_heap_trace(loader_instance->application_thread); + } else { + furi_thread_disable_heap_trace(loader_instance->application_thread); + } + furi_thread_set_name(loader_instance->application_thread, loader_instance->application->name); furi_thread_set_stack_size( loader_instance->application_thread, loader_instance->application->stack_size); @@ -306,7 +313,7 @@ static Loader* loader_alloc() { Loader* instance = malloc(sizeof(Loader)); instance->application_thread = furi_thread_alloc(); - furi_thread_enable_heap_trace(instance->application_thread); + furi_thread_set_state_context(instance->application_thread, instance); furi_thread_set_state_callback(instance->application_thread, loader_thread_state_callback); diff --git a/applications/settings/system/system_settings.c b/applications/settings/system/system_settings.c index 7661413d7..dfce11a22 100644 --- a/applications/settings/system/system_settings.c +++ b/applications/settings/system/system_settings.c @@ -45,6 +45,31 @@ static void debug_changed(VariableItem* item) { loader_update_menu(); } +const char* const heap_trace_mode_text[] = { + "None", + "Main", +#if FURI_DEBUG + "Tree", + "All", +#endif +}; + +const uint32_t heap_trace_mode_value[] = { + FuriHalRtcHeapTrackModeNone, + FuriHalRtcHeapTrackModeMain, +#if FURI_DEBUG + FuriHalRtcHeapTrackModeTree, + FuriHalRtcHeapTrackModeAll, +#endif +}; + +static void heap_trace_mode_changed(VariableItem* item) { + // SystemSettings* app = variable_item_get_context(item); + uint8_t index = variable_item_get_current_value_index(item); + variable_item_set_current_value_text(item, heap_trace_mode_text[index]); + furi_hal_rtc_set_heap_track_mode(heap_trace_mode_value[index]); +} + static uint32_t system_settings_exit(void* context) { UNUSED(context); return VIEW_NONE; @@ -79,6 +104,18 @@ SystemSettings* system_settings_alloc() { variable_item_set_current_value_index(item, value_index); variable_item_set_current_value_text(item, debug_text[value_index]); + item = variable_item_list_add( + app->var_item_list, + "Heap Trace", + COUNT_OF(heap_trace_mode_text), + heap_trace_mode_changed, + app); + value_index = value_index_uint32( + furi_hal_rtc_get_heap_track_mode(), heap_trace_mode_value, COUNT_OF(heap_trace_mode_text)); + furi_hal_rtc_set_heap_track_mode(heap_trace_mode_value[value_index]); + variable_item_set_current_value_index(item, value_index); + variable_item_set_current_value_text(item, heap_trace_mode_text[value_index]); + view_set_previous_callback( variable_item_list_get_view(app->var_item_list), system_settings_exit); view_dispatcher_add_view( diff --git a/firmware/targets/f7/api_symbols.csv b/firmware/targets/f7/api_symbols.csv index d6e522a2b..1ad5efa72 100644 --- a/firmware/targets/f7/api_symbols.csv +++ b/firmware/targets/f7/api_symbols.csv @@ -1,5 +1,5 @@ entry,status,name,type,params -Version,+,7.4,, +Version,+,7.5,, Header,+,applications/services/bt/bt_service/bt.h,, Header,+,applications/services/cli/cli.h,, Header,+,applications/services/cli/cli_vcp.h,, @@ -1260,6 +1260,7 @@ Function,+,furi_hal_rtc_deinit_early,void, Function,+,furi_hal_rtc_get_boot_mode,FuriHalRtcBootMode, Function,+,furi_hal_rtc_get_datetime,void,FuriHalRtcDateTime* Function,+,furi_hal_rtc_get_fault_data,uint32_t, +Function,+,furi_hal_rtc_get_heap_track_mode,FuriHalRtcHeapTrackMode, Function,+,furi_hal_rtc_get_log_level,uint8_t, Function,+,furi_hal_rtc_get_pin_fails,uint32_t, Function,+,furi_hal_rtc_get_register,uint32_t,FuriHalRtcRegister @@ -1272,6 +1273,7 @@ Function,+,furi_hal_rtc_set_boot_mode,void,FuriHalRtcBootMode Function,+,furi_hal_rtc_set_datetime,void,FuriHalRtcDateTime* Function,+,furi_hal_rtc_set_fault_data,void,uint32_t Function,+,furi_hal_rtc_set_flag,void,FuriHalRtcFlag +Function,+,furi_hal_rtc_set_heap_track_mode,void,FuriHalRtcHeapTrackMode Function,+,furi_hal_rtc_set_log_level,void,uint8_t Function,+,furi_hal_rtc_set_pin_fails,void,uint32_t Function,+,furi_hal_rtc_set_register,void,"FuriHalRtcRegister, uint32_t" diff --git a/firmware/targets/f7/furi_hal/furi_hal_resources.h b/firmware/targets/f7/furi_hal/furi_hal_resources.h index 8d53ec48a..64e5e19f9 100644 --- a/firmware/targets/f7/furi_hal/furi_hal_resources.h +++ b/firmware/targets/f7/furi_hal/furi_hal_resources.h @@ -10,7 +10,7 @@ extern "C" { #endif /* Input Related Constants */ -#define INPUT_DEBOUNCE_TICKS 30 +#define INPUT_DEBOUNCE_TICKS 4 /* Input Keys */ typedef enum { diff --git a/firmware/targets/f7/furi_hal/furi_hal_rtc.c b/firmware/targets/f7/furi_hal/furi_hal_rtc.c index 14ece9466..c38cbfec5 100644 --- a/firmware/targets/f7/furi_hal/furi_hal_rtc.c +++ b/firmware/targets/f7/furi_hal/furi_hal_rtc.c @@ -30,7 +30,8 @@ typedef struct { uint8_t log_reserved : 4; uint8_t flags; uint8_t boot_mode : 4; - uint16_t reserved : 12; + uint8_t heap_track_mode : 2; + uint16_t reserved : 10; } DeveloperReg; _Static_assert(sizeof(DeveloperReg) == 4, "DeveloperReg size mismatch"); @@ -224,6 +225,19 @@ FuriHalRtcBootMode furi_hal_rtc_get_boot_mode() { return (FuriHalRtcBootMode)data->boot_mode; } +void furi_hal_rtc_set_heap_track_mode(FuriHalRtcHeapTrackMode mode) { + uint32_t data_reg = furi_hal_rtc_get_register(FuriHalRtcRegisterSystem); + DeveloperReg* data = (DeveloperReg*)&data_reg; + data->heap_track_mode = mode; + furi_hal_rtc_set_register(FuriHalRtcRegisterSystem, data_reg); +} + +FuriHalRtcHeapTrackMode furi_hal_rtc_get_heap_track_mode() { + uint32_t data_reg = furi_hal_rtc_get_register(FuriHalRtcRegisterSystem); + DeveloperReg* data = (DeveloperReg*)&data_reg; + return (FuriHalRtcHeapTrackMode)data->heap_track_mode; +} + void furi_hal_rtc_set_datetime(FuriHalRtcDateTime* datetime) { furi_assert(datetime); diff --git a/firmware/targets/furi_hal_include/furi_hal_rtc.h b/firmware/targets/furi_hal_include/furi_hal_rtc.h index 361225fb2..5ce122271 100644 --- a/firmware/targets/furi_hal_include/furi_hal_rtc.h +++ b/firmware/targets/furi_hal_include/furi_hal_rtc.h @@ -39,6 +39,13 @@ typedef enum { FuriHalRtcBootModePostUpdate, /**< Boot to Update, post update */ } FuriHalRtcBootMode; +typedef enum { + FuriHalRtcHeapTrackModeNone = 0, /**< Disable allocation tracking */ + FuriHalRtcHeapTrackModeMain, /**< Enable allocation tracking for main application thread */ + FuriHalRtcHeapTrackModeTree, /**< Enable allocation tracking for main and children application threads */ + FuriHalRtcHeapTrackModeAll, /**< Enable allocation tracking for all threads */ +} FuriHalRtcHeapTrackMode; + typedef enum { FuriHalRtcRegisterHeader, /**< RTC structure header */ FuriHalRtcRegisterSystem, /**< Various system bits */ @@ -79,6 +86,10 @@ void furi_hal_rtc_set_boot_mode(FuriHalRtcBootMode mode); FuriHalRtcBootMode furi_hal_rtc_get_boot_mode(); +void furi_hal_rtc_set_heap_track_mode(FuriHalRtcHeapTrackMode mode); + +FuriHalRtcHeapTrackMode furi_hal_rtc_get_heap_track_mode(); + void furi_hal_rtc_set_datetime(FuriHalRtcDateTime* datetime); void furi_hal_rtc_get_datetime(FuriHalRtcDateTime* datetime); diff --git a/furi/core/thread.c b/furi/core/thread.c index 8320a47e8..201b47fe7 100644 --- a/furi/core/thread.c +++ b/furi/core/thread.c @@ -122,9 +122,14 @@ FuriThread* furi_thread_alloc() { thread->output.buffer = furi_string_alloc(); thread->is_service = false; - if(furi_thread_get_current_id()) { + FuriHalRtcHeapTrackMode mode = furi_hal_rtc_get_heap_track_mode(); + if(mode == FuriHalRtcHeapTrackModeAll) { + thread->heap_trace_enabled = true; + } else if(mode == FuriHalRtcHeapTrackModeTree && furi_thread_get_current_id()) { FuriThread* parent = pvTaskGetThreadLocalStoragePointer(NULL, 0); if(parent) thread->heap_trace_enabled = parent->heap_trace_enabled; + } else { + thread->heap_trace_enabled = false; } return thread; @@ -243,14 +248,12 @@ FuriThreadId furi_thread_get_id(FuriThread* thread) { void furi_thread_enable_heap_trace(FuriThread* thread) { furi_assert(thread); furi_assert(thread->state == FuriThreadStateStopped); - furi_assert(thread->heap_trace_enabled == false); thread->heap_trace_enabled = true; } void furi_thread_disable_heap_trace(FuriThread* thread) { furi_assert(thread); furi_assert(thread->state == FuriThreadStateStopped); - furi_assert(thread->heap_trace_enabled == true); thread->heap_trace_enabled = false; } From 3c7a4eeaed1f8c636771adb6974a47e3934b9149 Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Sat, 12 Nov 2022 12:45:19 +0300 Subject: [PATCH 45/49] iButton: Fix header "Saved!" message stays on other screens (#2003) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * iButton: Fix header "Saved!" message stays on other screens * SubGhz,iButton: proper popup reset Co-authored-by: あく --- .../ibutton/scenes/ibutton_scene_delete_success.c | 7 +------ .../main/ibutton/scenes/ibutton_scene_save_success.c | 7 +------ .../ibutton/scenes/ibutton_scene_write_success.c | 7 +------ .../main/subghz/scenes/subghz_scene_delete_success.c | 11 ++--------- .../main/subghz/scenes/subghz_scene_save_success.c | 11 ++--------- .../main/subghz/scenes/subghz_scene_show_error_sub.c | 12 +++--------- .../main/subghz/scenes/subghz_scene_show_only_rx.c | 11 ++--------- 7 files changed, 12 insertions(+), 54 deletions(-) diff --git a/applications/main/ibutton/scenes/ibutton_scene_delete_success.c b/applications/main/ibutton/scenes/ibutton_scene_delete_success.c index 8b7d9dfc0..9ff165e4a 100644 --- a/applications/main/ibutton/scenes/ibutton_scene_delete_success.c +++ b/applications/main/ibutton/scenes/ibutton_scene_delete_success.c @@ -39,10 +39,5 @@ void ibutton_scene_delete_success_on_exit(void* context) { iButton* ibutton = context; Popup* popup = ibutton->popup; - popup_set_text(popup, NULL, 0, 0, AlignCenter, AlignTop); - popup_set_icon(popup, 0, 0, NULL); - - popup_disable_timeout(popup); - popup_set_context(popup, NULL); - popup_set_callback(popup, NULL); + popup_reset(popup); } diff --git a/applications/main/ibutton/scenes/ibutton_scene_save_success.c b/applications/main/ibutton/scenes/ibutton_scene_save_success.c index e0b9b3c47..8b16d2929 100644 --- a/applications/main/ibutton/scenes/ibutton_scene_save_success.c +++ b/applications/main/ibutton/scenes/ibutton_scene_save_success.c @@ -39,10 +39,5 @@ void ibutton_scene_save_success_on_exit(void* context) { iButton* ibutton = context; Popup* popup = ibutton->popup; - popup_set_text(popup, NULL, 0, 0, AlignCenter, AlignTop); - popup_set_icon(popup, 0, 0, NULL); - - popup_disable_timeout(popup); - popup_set_context(popup, NULL); - popup_set_callback(popup, NULL); + popup_reset(popup); } diff --git a/applications/main/ibutton/scenes/ibutton_scene_write_success.c b/applications/main/ibutton/scenes/ibutton_scene_write_success.c index 3acb1dea0..17cd53d08 100644 --- a/applications/main/ibutton/scenes/ibutton_scene_write_success.c +++ b/applications/main/ibutton/scenes/ibutton_scene_write_success.c @@ -43,10 +43,5 @@ void ibutton_scene_write_success_on_exit(void* context) { iButton* ibutton = context; Popup* popup = ibutton->popup; - popup_set_text(popup, NULL, 0, 0, AlignCenter, AlignTop); - popup_set_icon(popup, 0, 0, NULL); - - popup_disable_timeout(popup); - popup_set_context(popup, NULL); - popup_set_callback(popup, NULL); + popup_reset(popup); } diff --git a/applications/main/subghz/scenes/subghz_scene_delete_success.c b/applications/main/subghz/scenes/subghz_scene_delete_success.c index d6e1f8dd4..4f98b6a39 100644 --- a/applications/main/subghz/scenes/subghz_scene_delete_success.c +++ b/applications/main/subghz/scenes/subghz_scene_delete_success.c @@ -44,14 +44,7 @@ bool subghz_scene_delete_success_on_event(void* context, SceneManagerEvent event void subghz_scene_delete_success_on_exit(void* context) { SubGhz* subghz = context; - - // Clear view Popup* popup = subghz->popup; - popup_set_header(popup, NULL, 0, 0, AlignCenter, AlignBottom); - popup_set_text(popup, NULL, 0, 0, AlignCenter, AlignTop); - popup_set_icon(popup, 0, 0, NULL); - popup_set_callback(popup, NULL); - popup_set_context(popup, NULL); - popup_set_timeout(popup, 0); - popup_disable_timeout(popup); + + popup_reset(popup); } diff --git a/applications/main/subghz/scenes/subghz_scene_save_success.c b/applications/main/subghz/scenes/subghz_scene_save_success.c index d32c9271a..2977975f7 100644 --- a/applications/main/subghz/scenes/subghz_scene_save_success.c +++ b/applications/main/subghz/scenes/subghz_scene_save_success.c @@ -44,14 +44,7 @@ bool subghz_scene_save_success_on_event(void* context, SceneManagerEvent event) void subghz_scene_save_success_on_exit(void* context) { SubGhz* subghz = context; - - // Clear view Popup* popup = subghz->popup; - popup_set_header(popup, NULL, 0, 0, AlignCenter, AlignBottom); - popup_set_text(popup, NULL, 0, 0, AlignCenter, AlignTop); - popup_set_icon(popup, 0, 0, NULL); - popup_set_callback(popup, NULL); - popup_set_context(popup, NULL); - popup_set_timeout(popup, 0); - popup_disable_timeout(popup); + + popup_reset(popup); } diff --git a/applications/main/subghz/scenes/subghz_scene_show_error_sub.c b/applications/main/subghz/scenes/subghz_scene_show_error_sub.c index 2720b2b94..113e7ae74 100644 --- a/applications/main/subghz/scenes/subghz_scene_show_error_sub.c +++ b/applications/main/subghz/scenes/subghz_scene_show_error_sub.c @@ -36,16 +36,10 @@ bool subghz_scene_show_error_sub_on_event(void* context, SceneManagerEvent event void subghz_scene_show_error_sub_on_exit(void* context) { SubGhz* subghz = context; - - // Clear view Popup* popup = subghz->popup; - popup_set_header(popup, NULL, 0, 0, AlignCenter, AlignBottom); - popup_set_text(popup, NULL, 0, 0, AlignCenter, AlignTop); - popup_set_icon(popup, 0, 0, NULL); - popup_set_callback(popup, NULL); - popup_set_context(popup, NULL); - popup_set_timeout(popup, 0); - popup_disable_timeout(popup); + + popup_reset(popup); + furi_string_reset(subghz->error_str); notification_message(subghz->notifications, &sequence_reset_rgb); diff --git a/applications/main/subghz/scenes/subghz_scene_show_only_rx.c b/applications/main/subghz/scenes/subghz_scene_show_only_rx.c index 3bc08e5b4..1907c4192 100644 --- a/applications/main/subghz/scenes/subghz_scene_show_only_rx.c +++ b/applications/main/subghz/scenes/subghz_scene_show_only_rx.c @@ -43,14 +43,7 @@ bool subghz_scene_show_only_rx_on_event(void* context, SceneManagerEvent event) void subghz_scene_show_only_rx_on_exit(void* context) { SubGhz* subghz = context; - - // Clear view Popup* popup = subghz->popup; - popup_set_header(popup, NULL, 0, 0, AlignCenter, AlignBottom); - popup_set_text(popup, NULL, 0, 0, AlignCenter, AlignTop); - popup_set_icon(popup, 0, 0, NULL); - popup_set_callback(popup, NULL); - popup_set_context(popup, NULL); - popup_set_timeout(popup, 0); - popup_disable_timeout(popup); + + popup_reset(popup); } From f9730bcafeafec8afbe82fa676133b413a6bbae0 Mon Sep 17 00:00:00 2001 From: hedger Date: Sat, 12 Nov 2022 14:03:22 +0400 Subject: [PATCH 46/49] fbt: lint fixes (#2008) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * lint: exclude "lib" subfolder from naming checks; fbt: moved LINT_SOURCES from literal strings to Dir() nodes * lint: also exclude hidden directories Co-authored-by: あく --- SConstruct | 4 ++-- firmware.scons | 2 +- firmware/SConscript | 2 +- furi/SConscript | 3 +-- lib/SConscript | 26 +++++++++++++------------- lib/lfrfid/SConscript | 2 +- lib/nfc/SConscript | 2 +- scripts/lint.py | 16 +++++++++++++--- 8 files changed, 33 insertions(+), 24 deletions(-) diff --git a/SConstruct b/SConstruct index 34ff80bc9..474175c14 100644 --- a/SConstruct +++ b/SConstruct @@ -241,13 +241,13 @@ distenv.PhonyTarget( distenv.PhonyTarget( "lint", "${PYTHON3} ${FBT_SCRIPT_DIR}/lint.py check ${LINT_SOURCES}", - LINT_SOURCES=firmware_env["LINT_SOURCES"], + LINT_SOURCES=[n.srcnode() for n in firmware_env["LINT_SOURCES"]], ) distenv.PhonyTarget( "format", "${PYTHON3} ${FBT_SCRIPT_DIR}/lint.py format ${LINT_SOURCES}", - LINT_SOURCES=firmware_env["LINT_SOURCES"], + LINT_SOURCES=[n.srcnode() for n in firmware_env["LINT_SOURCES"]], ) # PY_LINT_SOURCES contains recursively-built modules' SConscript files + application manifests diff --git a/firmware.scons b/firmware.scons index da5caba53..d674bf160 100644 --- a/firmware.scons +++ b/firmware.scons @@ -22,7 +22,7 @@ env = ENV.Clone( FW_FLAVOR=fw_build_meta["flavor"], LIB_DIST_DIR=fw_build_meta["build_dir"].Dir("lib"), LINT_SOURCES=[ - "applications", + Dir("applications"), ], LIBPATH=[ "${LIB_DIST_DIR}", diff --git a/firmware/SConscript b/firmware/SConscript index 19dde2e4c..a16f14e65 100644 --- a/firmware/SConscript +++ b/firmware/SConscript @@ -1,7 +1,7 @@ Import("env") env.Append( - LINT_SOURCES=["firmware"], + LINT_SOURCES=[Dir(".")], SDK_HEADERS=[ *env.GlobRecursive("*.h", "targets/furi_hal_include", "*_i.h"), *env.GlobRecursive("*.h", "targets/f${TARGET_HW}/furi_hal", "*_i.h"), diff --git a/furi/SConscript b/furi/SConscript index f95ef13f9..8f8caeb87 100644 --- a/furi/SConscript +++ b/furi/SConscript @@ -2,8 +2,7 @@ Import("env") env.Append( LINT_SOURCES=[ - "furi", - "furi/core", + Dir("."), ] ) diff --git a/lib/SConscript b/lib/SConscript index 60ffabfa9..abede5f33 100644 --- a/lib/SConscript +++ b/lib/SConscript @@ -2,19 +2,19 @@ Import("env") env.Append( LINT_SOURCES=[ - "lib/app-scened-template", - "lib/digital_signal", - "lib/drivers", - "lib/flipper_format", - "lib/infrared", - "lib/nfc", - "lib/one_wire", - "lib/ST25RFAL002", - "lib/subghz", - "lib/toolbox", - "lib/u8g2", - "lib/update_util", - "lib/print", + Dir("app-scened-template"), + Dir("digital_signal"), + Dir("drivers"), + Dir("flipper_format"), + Dir("infrared"), + Dir("nfc"), + Dir("one_wire"), + Dir("ST25RFAL002"), + Dir("subghz"), + Dir("toolbox"), + Dir("u8g2"), + Dir("update_util"), + Dir("print"), ], SDK_HEADERS=[ File("one_wire/one_wire_host_timing.h"), diff --git a/lib/lfrfid/SConscript b/lib/lfrfid/SConscript index 69ea9d3c1..f9431ca75 100644 --- a/lib/lfrfid/SConscript +++ b/lib/lfrfid/SConscript @@ -2,7 +2,7 @@ Import("env") env.Append( LINT_SOURCES=[ - "lib/lfrfid", + Dir("."), ], CPPPATH=[ "#/lib/lfrfid", diff --git a/lib/nfc/SConscript b/lib/nfc/SConscript index c6b70a677..b086298de 100644 --- a/lib/nfc/SConscript +++ b/lib/nfc/SConscript @@ -5,7 +5,7 @@ env.Append( "#/lib/nfc", ], SDK_HEADERS=[ - File("#/lib/nfc/nfc_device.h"), + File("nfc_device.h"), ], ) diff --git a/scripts/lint.py b/scripts/lint.py index c178c8763..58f2d69f5 100755 --- a/scripts/lint.py +++ b/scripts/lint.py @@ -35,11 +35,23 @@ class Main(App): ) self.parser_format.set_defaults(func=self.format) + @staticmethod + def _filter_lint_directories(dirnames: list[str]): + # Skipping 3rd-party code - usually resides in subfolder "lib" + if "lib" in dirnames: + dirnames.remove("lib") + # Skipping hidden folders + for dirname in dirnames.copy(): + if dirname.startswith("."): + dirnames.remove(dirname) + def _check_folders(self, folders: list): show_message = False pattern = re.compile(SOURCE_CODE_DIR_PATTERN) for folder in folders: for dirpath, dirnames, filenames in os.walk(folder): + self._filter_lint_directories(dirnames) + for dirname in dirnames: if not pattern.match(dirname): to_fix = os.path.join(dirpath, dirname) @@ -54,9 +66,7 @@ class Main(App): output = [] for folder in folders: for dirpath, dirnames, filenames in os.walk(folder): - # Skipping 3rd-party code - usually resides in subfolder "lib" - if "lib" in dirnames: - dirnames.remove("lib") + self._filter_lint_directories(dirnames) for filename in filenames: ext = os.path.splitext(filename.lower())[1] From 73441af9c65228030652230ff65ea850b8969485 Mon Sep 17 00:00:00 2001 From: Nikolay Minaylov Date: Sat, 12 Nov 2022 14:55:42 +0300 Subject: [PATCH 47/49] BadUSB and Archive fixes (#2005) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * BsdUsb: fix empty lines handling * Archive: folders and unknown files rename fix Co-authored-by: あく --- .../main/archive/scenes/archive_scene_browser.c | 2 +- .../main/archive/scenes/archive_scene_rename.c | 4 +++- .../main/archive/views/archive_browser_view.c | 1 - applications/main/bad_usb/bad_usb_script.c | 14 ++++++-------- lib/toolbox/path.c | 2 +- 5 files changed, 11 insertions(+), 12 deletions(-) diff --git a/applications/main/archive/scenes/archive_scene_browser.c b/applications/main/archive/scenes/archive_scene_browser.c index 9dc671617..04f4dcc3a 100644 --- a/applications/main/archive/scenes/archive_scene_browser.c +++ b/applications/main/archive/scenes/archive_scene_browser.c @@ -133,7 +133,7 @@ bool archive_scene_browser_on_event(void* context, SceneManagerEvent event) { case ArchiveBrowserEventFileMenuRename: if(favorites) { browser->callback(ArchiveBrowserEventEnterFavMove, browser->context); - } else if((archive_is_known_app(selected->type)) && (selected->is_app == false)) { + } else if(selected->is_app == false) { archive_show_file_menu(browser, false); scene_manager_set_scene_state( archive->scene_manager, ArchiveAppSceneBrowser, SCENE_STATE_NEED_REFRESH); diff --git a/applications/main/archive/scenes/archive_scene_rename.c b/applications/main/archive/scenes/archive_scene_rename.c index 1451428bd..37f860a9a 100644 --- a/applications/main/archive/scenes/archive_scene_rename.c +++ b/applications/main/archive/scenes/archive_scene_rename.c @@ -57,9 +57,11 @@ bool archive_scene_rename_on_event(void* context, SceneManagerEvent event) { ArchiveFile_t* file = archive_get_current_file(archive->browser); FuriString* path_dst; + path_dst = furi_string_alloc(); path_extract_dirname(path_src, path_dst); - furi_string_cat_printf(path_dst, "/%s%s", archive->text_store, known_ext[file->type]); + furi_string_cat_printf( + path_dst, "/%s%s", archive->text_store, archive->file_extension); storage_common_rename(fs_api, path_src, furi_string_get_cstr(path_dst)); furi_record_close(RECORD_STORAGE); diff --git a/applications/main/archive/views/archive_browser_view.c b/applications/main/archive/views/archive_browser_view.c index a2e219b95..65be42135 100644 --- a/applications/main/archive/views/archive_browser_view.c +++ b/applications/main/archive/views/archive_browser_view.c @@ -65,7 +65,6 @@ static void render_item_menu(Canvas* canvas, ArchiveBrowserViewModel* model) { if(!archive_is_known_app(selected->type)) { furi_string_set(menu[0], "---"); furi_string_set(menu[1], "---"); - furi_string_set(menu[2], "---"); } else { if(model->tab_idx == ArchiveTabFavorites) { furi_string_set(menu[2], "Move"); diff --git a/applications/main/bad_usb/bad_usb_script.c b/applications/main/bad_usb/bad_usb_script.c index ae6181149..295cc1c3e 100644 --- a/applications/main/bad_usb/bad_usb_script.c +++ b/applications/main/bad_usb/bad_usb_script.c @@ -237,12 +237,8 @@ static int32_t const char* line_tmp = furi_string_get_cstr(line); bool state = false; - for(uint32_t i = 0; i < line_len; i++) { - if((line_tmp[i] != ' ') && (line_tmp[i] != '\t') && (line_tmp[i] != '\n')) { - line_tmp = &line_tmp[i]; - break; // Skip spaces and tabs - } - if(i == line_len - 1) return SCRIPT_STATE_NEXT_LINE; // Skip empty lines + if(line_len == 0) { + return SCRIPT_STATE_NEXT_LINE; // Skip empty lines } FURI_LOG_D(WORKER_TAG, "line:%s", line_tmp); @@ -450,10 +446,12 @@ static int32_t ducky_script_execute_next(BadUsbScript* bad_usb, File* script_fil bad_usb->st.line_cur++; bad_usb->buf_len = bad_usb->buf_len + bad_usb->buf_start - (i + 1); bad_usb->buf_start = i + 1; + furi_string_trim(bad_usb->line); delay_val = ducky_parse_line( bad_usb, bad_usb->line, bad_usb->st.error, sizeof(bad_usb->st.error)); - - if(delay_val < 0) { + if(delay_val == SCRIPT_STATE_NEXT_LINE) { // Empty line + return 0; + } else if(delay_val < 0) { bad_usb->st.error_line = bad_usb->st.line_cur; FURI_LOG_E(WORKER_TAG, "Unknown command at line %u", bad_usb->st.line_cur); return SCRIPT_STATE_ERROR; diff --git a/lib/toolbox/path.c b/lib/toolbox/path.c index 53e9fc092..ce65aca4f 100644 --- a/lib/toolbox/path.c +++ b/lib/toolbox/path.c @@ -38,7 +38,7 @@ void path_extract_extension(FuriString* path, char* ext, size_t ext_len_max) { size_t dot = furi_string_search_rchar(path, '.'); size_t filename_start = furi_string_search_rchar(path, '/'); - if((dot > 0) && (filename_start < dot)) { + if((dot != FURI_STRING_FAILURE) && (filename_start < dot)) { strlcpy(ext, &(furi_string_get_cstr(path))[dot], ext_len_max); } } From b56fed477a2cbad5f8cfbce8af63adcb7da8bd39 Mon Sep 17 00:00:00 2001 From: hedger Date: Sat, 12 Nov 2022 21:22:40 +0400 Subject: [PATCH 48/49] Path handling fixes in toolchain download #2010 --- scripts/toolchain/fbtenv.cmd | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/scripts/toolchain/fbtenv.cmd b/scripts/toolchain/fbtenv.cmd index 44a2551f7..2305e891f 100644 --- a/scripts/toolchain/fbtenv.cmd +++ b/scripts/toolchain/fbtenv.cmd @@ -5,7 +5,7 @@ if not [%FBT_ROOT%] == [] ( ) set "FBT_ROOT=%~dp0\..\..\" -pushd %FBT_ROOT% +pushd "%FBT_ROOT%" set "FBT_ROOT=%cd%" popd @@ -15,23 +15,25 @@ if not [%FBT_NOENV%] == [] ( set "FLIPPER_TOOLCHAIN_VERSION=19" -if [%FBT_TOOLCHAIN_ROOT%] == [] ( +if ["%FBT_TOOLCHAIN_ROOT%"] == [""] ( set "FBT_TOOLCHAIN_ROOT=%FBT_ROOT%\toolchain\x86_64-windows" ) +set "FBT_TOOLCHAIN_VERSION_FILE=%FBT_TOOLCHAIN_ROOT%\VERSION" + if not exist "%FBT_TOOLCHAIN_ROOT%" ( - powershell -ExecutionPolicy Bypass -File "%FBT_ROOT%\scripts\toolchain\windows-toolchain-download.ps1" "%flipper_toolchain_version%" "%FBT_TOOLCHAIN_ROOT%" -) -if not exist "%FBT_TOOLCHAIN_ROOT%\VERSION" ( - powershell -ExecutionPolicy Bypass -File "%FBT_ROOT%\scripts\toolchain\windows-toolchain-download.ps1" "%flipper_toolchain_version%" "%FBT_TOOLCHAIN_ROOT%" + powershell -ExecutionPolicy Bypass -File "%FBT_ROOT%\scripts\toolchain\windows-toolchain-download.ps1" %flipper_toolchain_version% "%FBT_TOOLCHAIN_ROOT%" ) -set /p REAL_TOOLCHAIN_VERSION=<"%FBT_TOOLCHAIN_ROOT%\VERSION" +if not exist "%FBT_TOOLCHAIN_VERSION_FILE%" ( + powershell -ExecutionPolicy Bypass -File "%FBT_ROOT%\scripts\toolchain\windows-toolchain-download.ps1" %flipper_toolchain_version% "%FBT_TOOLCHAIN_ROOT%" +) + +set /p REAL_TOOLCHAIN_VERSION=<"%FBT_TOOLCHAIN_VERSION_FILE%" if not "%REAL_TOOLCHAIN_VERSION%" == "%FLIPPER_TOOLCHAIN_VERSION%" ( - powershell -ExecutionPolicy Bypass -File "%FBT_ROOT%\scripts\toolchain\windows-toolchain-download.ps1" "%flipper_toolchain_version%" "%FBT_TOOLCHAIN_ROOT%" + powershell -ExecutionPolicy Bypass -File "%FBT_ROOT%\scripts\toolchain\windows-toolchain-download.ps1" %flipper_toolchain_version% "%FBT_TOOLCHAIN_ROOT%" ) - set "HOME=%USERPROFILE%" set "PYTHONHOME=%FBT_TOOLCHAIN_ROOT%\python" set "PYTHONPATH=" From 41de5f3c5221aa8bc485aab7bb472a7152003f54 Mon Sep 17 00:00:00 2001 From: hedger Date: Sat, 12 Nov 2022 22:28:29 +0400 Subject: [PATCH 49/49] fbt: more fixes for windows environment #2011 --- scripts/toolchain/fbtenv.cmd | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/toolchain/fbtenv.cmd b/scripts/toolchain/fbtenv.cmd index 2305e891f..9fbd8fd9b 100644 --- a/scripts/toolchain/fbtenv.cmd +++ b/scripts/toolchain/fbtenv.cmd @@ -1,6 +1,6 @@ @echo off -if not [%FBT_ROOT%] == [] ( +if not ["%FBT_ROOT%"] == [""] ( goto already_set ) @@ -9,7 +9,7 @@ pushd "%FBT_ROOT%" set "FBT_ROOT=%cd%" popd -if not [%FBT_NOENV%] == [] ( +if not ["%FBT_NOENV%"] == [""] ( exit /b 0 )