Merge branch 'release-candidate' into release
This commit is contained in:
		
						commit
						94bb1ad354
					
				
							
								
								
									
										4
									
								
								.github/workflows/build.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/build.yml
									
									
									
									
										vendored
									
									
								
							| @ -15,7 +15,7 @@ env: | ||||
| 
 | ||||
| jobs: | ||||
|   main: | ||||
|     runs-on: [self-hosted,Office] | ||||
|     runs-on: [self-hosted,FlipperZero] | ||||
|     steps: | ||||
|       - name: 'Cleanup workspace' | ||||
|         uses: AutoModality/action-clean@v1 | ||||
| @ -166,7 +166,7 @@ jobs: | ||||
| 
 | ||||
|   compact: | ||||
|     if: ${{ !startsWith(github.ref, 'refs/tags') }} | ||||
|     runs-on: [self-hosted,koteeq] | ||||
|     runs-on: [self-hosted,FlipperZero] | ||||
|     steps: | ||||
|       - name: 'Cleanup workspace' | ||||
|         uses: AutoModality/action-clean@v1 | ||||
|  | ||||
							
								
								
									
										2
									
								
								.github/workflows/lint_c.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/lint_c.yml
									
									
									
									
										vendored
									
									
								
							| @ -14,7 +14,7 @@ env: | ||||
| 
 | ||||
| jobs: | ||||
|   lint_c_cpp: | ||||
|     runs-on: [self-hosted,Office] | ||||
|     runs-on: [self-hosted,FlipperZero] | ||||
|     steps: | ||||
|       - name: 'Cleanup workspace' | ||||
|         uses: AutoModality/action-clean@v1 | ||||
|  | ||||
							
								
								
									
										2
									
								
								.github/workflows/reindex.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/reindex.yml
									
									
									
									
										vendored
									
									
								
							| @ -7,7 +7,7 @@ on: | ||||
| jobs: | ||||
|   reindex: | ||||
|     name: 'Reindex updates' | ||||
|     runs-on: [self-hosted,Office] | ||||
|     runs-on: [self-hosted,FlipperZero] | ||||
|     steps: | ||||
|       - name: Trigger reindex | ||||
|         uses: wei/curl@master | ||||
|  | ||||
| @ -7,6 +7,13 @@ | ||||
| Welcome to [Flipper Zero](https://flipperzero.one/)'s Firmware repo! | ||||
| Our goal is to create nice and clean code with good documentation, to make it a pleasure for everyone to work with. | ||||
| 
 | ||||
| # Clone the Repository | ||||
| 
 | ||||
| You should clone with  | ||||
| ```shell | ||||
| $ git clone --recursive https://github.com/flipperdevices/flipperzero-firmware.git | ||||
| ``` | ||||
| 
 | ||||
| # Update firmware | ||||
| 
 | ||||
| [Get Latest Firmware from Update Server](https://update.flipperzero.one/) | ||||
| @ -74,6 +81,8 @@ Check `dist/` for build outputs. | ||||
| 
 | ||||
| Use **`flipper-z-{target}-full-{suffix}.dfu`** to flash your device. | ||||
| 
 | ||||
| If compilation fails, make sure all submodules are all initialized. Either clone with `--recursive` or use `git submodule update --init --recursive`. | ||||
| 
 | ||||
| # Build on Linux/macOS | ||||
| 
 | ||||
| ## macOS Prerequisites | ||||
|  | ||||
| @ -44,6 +44,7 @@ extern int32_t vibro_test_app(void* p); | ||||
| extern int32_t bt_hid_app(void* p); | ||||
| extern int32_t battery_test_app(void* p); | ||||
| extern int32_t text_box_test_app(void* p); | ||||
| extern int32_t file_browser_app(void* p); | ||||
| 
 | ||||
| // Plugins
 | ||||
| extern int32_t music_player_app(void* p); | ||||
| @ -419,14 +420,6 @@ const FlipperApplication FLIPPER_DEBUG_APPS[] = { | ||||
|      .flags = FlipperApplicationFlagDefault}, | ||||
| #endif | ||||
| 
 | ||||
| #ifdef APP_SCENED | ||||
|     {.app = scened_app, | ||||
|      .name = "Templated Scene", | ||||
|      .stack_size = 1024, | ||||
|      .icon = NULL, | ||||
|      .flags = FlipperApplicationFlagDefault}, | ||||
| #endif | ||||
| 
 | ||||
| #ifdef APP_LF_RFID | ||||
|     {.app = lfrfid_debug_app, | ||||
|      .name = "LF-RFID Debug", | ||||
| @ -459,6 +452,14 @@ const FlipperApplication FLIPPER_DEBUG_APPS[] = { | ||||
|      .flags = FlipperApplicationFlagDefault}, | ||||
| #endif | ||||
| 
 | ||||
| #ifdef APP_FILE_BROWSER_TEST | ||||
|     {.app = file_browser_app, | ||||
|      .name = "File Browser test", | ||||
|      .stack_size = 2048, | ||||
|      .icon = &A_BadUsb_14, | ||||
|      .flags = FlipperApplicationFlagDefault}, | ||||
| #endif | ||||
| 
 | ||||
| #ifdef APP_BATTERY_TEST | ||||
|     {.app = battery_test_app, | ||||
|      .name = "Battery Test", | ||||
|  | ||||
| @ -62,6 +62,7 @@ APP_USB_MOUSE = 1 | ||||
| APP_BAD_USB = 1 | ||||
| APP_U2F = 1 | ||||
| APP_UART_ECHO = 1 | ||||
| APP_FILE_BROWSER_TEST = 1 | ||||
| endif | ||||
| 
 | ||||
| 
 | ||||
| @ -207,6 +208,11 @@ CFLAGS		+= -DAPP_KEYPAD_TEST | ||||
| SRV_GUI		= 1 | ||||
| endif | ||||
| 
 | ||||
| APP_FILE_BROWSER_TEST ?= 0 | ||||
| ifeq ($(APP_FILE_BROWSER_TEST), 1) | ||||
| CFLAGS		+= -DAPP_FILE_BROWSER_TEST | ||||
| SRV_GUI = 1 | ||||
| endif | ||||
| 
 | ||||
| APP_ACCESSOR ?= 0 | ||||
| ifeq ($(APP_ACCESSOR), 1) | ||||
|  | ||||
| @ -1,4 +1,5 @@ | ||||
| #include "bad_usb_app_i.h" | ||||
| #include "m-string.h" | ||||
| #include <furi.h> | ||||
| #include <furi_hal.h> | ||||
| #include <storage/storage.h> | ||||
| @ -22,33 +23,13 @@ static void bad_usb_app_tick_event_callback(void* context) { | ||||
|     scene_manager_handle_tick_event(app->scene_manager); | ||||
| } | ||||
| 
 | ||||
| static bool bad_usb_check_assets() { | ||||
|     Storage* fs_api = furi_record_open("storage"); | ||||
| 
 | ||||
|     File* dir = storage_file_alloc(fs_api); | ||||
|     bool ret = false; | ||||
| 
 | ||||
|     if(storage_dir_open(dir, BAD_USB_APP_PATH_FOLDER)) { | ||||
|         ret = true; | ||||
|     } | ||||
| 
 | ||||
|     storage_dir_close(dir); | ||||
|     storage_file_free(dir); | ||||
| 
 | ||||
|     furi_record_close("storage"); | ||||
| 
 | ||||
|     return ret; | ||||
| } | ||||
| 
 | ||||
| BadUsbApp* bad_usb_app_alloc(char* arg) { | ||||
|     BadUsbApp* app = malloc(sizeof(BadUsbApp)); | ||||
| 
 | ||||
|     string_init(app->file_path); | ||||
| 
 | ||||
|     if(arg != NULL) { | ||||
|         string_t filename; | ||||
|         string_init(filename); | ||||
|         path_extract_filename_no_ext(arg, filename); | ||||
|         strncpy(app->file_name, string_get_cstr(filename), BAD_USB_FILE_NAME_LEN); | ||||
|         string_clear(filename); | ||||
|         string_set_str(app->file_path, arg); | ||||
|     } | ||||
| 
 | ||||
|     app->gui = furi_record_open("gui"); | ||||
| @ -83,13 +64,11 @@ BadUsbApp* bad_usb_app_alloc(char* arg) { | ||||
|         app->error = BadUsbAppErrorCloseRpc; | ||||
|         scene_manager_next_scene(app->scene_manager, BadUsbSceneError); | ||||
|     } else { | ||||
|         if(*app->file_name != '\0') { | ||||
|         if(!string_empty_p(app->file_path)) { | ||||
|             scene_manager_next_scene(app->scene_manager, BadUsbSceneWork); | ||||
|         } else if(bad_usb_check_assets()) { | ||||
|             scene_manager_next_scene(app->scene_manager, BadUsbSceneFileSelect); | ||||
|         } else { | ||||
|             app->error = BadUsbAppErrorNoFiles; | ||||
|             scene_manager_next_scene(app->scene_manager, BadUsbSceneError); | ||||
|             string_set_str(app->file_path, BAD_USB_APP_PATH_FOLDER); | ||||
|             scene_manager_next_scene(app->scene_manager, BadUsbSceneFileSelect); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| @ -117,6 +96,8 @@ void bad_usb_app_free(BadUsbApp* app) { | ||||
|     furi_record_close("notification"); | ||||
|     furi_record_close("dialogs"); | ||||
| 
 | ||||
|     string_clear(app->file_path); | ||||
| 
 | ||||
|     free(app); | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -16,7 +16,6 @@ | ||||
| 
 | ||||
| #define BAD_USB_APP_PATH_FOLDER "/any/badusb" | ||||
| #define BAD_USB_APP_EXTENSION ".txt" | ||||
| #define BAD_USB_FILE_NAME_LEN 40 | ||||
| 
 | ||||
| typedef enum { | ||||
|     BadUsbAppErrorNoFiles, | ||||
| @ -32,7 +31,7 @@ struct BadUsbApp { | ||||
|     Widget* widget; | ||||
| 
 | ||||
|     BadUsbAppError error; | ||||
|     char file_name[BAD_USB_FILE_NAME_LEN + 1]; | ||||
|     string_t file_path; | ||||
|     BadUsb* bad_usb_view; | ||||
|     BadUsbScript* bad_usb_script; | ||||
| }; | ||||
|  | ||||
| @ -5,14 +5,16 @@ | ||||
| static bool bad_usb_file_select(BadUsbApp* bad_usb) { | ||||
|     furi_assert(bad_usb); | ||||
| 
 | ||||
|     // Input events and views are managed by file_select
 | ||||
|     bool res = dialog_file_select_show( | ||||
|     // Input events and views are managed by file_browser
 | ||||
|     bool res = dialog_file_browser_show( | ||||
|         bad_usb->dialogs, | ||||
|         BAD_USB_APP_PATH_FOLDER, | ||||
|         bad_usb->file_path, | ||||
|         bad_usb->file_path, | ||||
|         BAD_USB_APP_EXTENSION, | ||||
|         bad_usb->file_name, | ||||
|         sizeof(bad_usb->file_name), | ||||
|         NULL); | ||||
|         true, | ||||
|         &I_badusb_10px, | ||||
|         true); | ||||
| 
 | ||||
|     return res; | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -2,6 +2,8 @@ | ||||
| #include "../bad_usb_app_i.h" | ||||
| #include "../views/bad_usb_view.h" | ||||
| #include "furi_hal.h" | ||||
| #include "m-string.h" | ||||
| #include "toolbox/path.h" | ||||
| 
 | ||||
| void bad_usb_scene_work_ok_callback(InputType type, void* context) { | ||||
|     furi_assert(context); | ||||
| @ -28,10 +30,9 @@ void bad_usb_scene_work_on_enter(void* context) { | ||||
|     string_t file_name; | ||||
|     string_init(file_name); | ||||
| 
 | ||||
|     bad_usb_set_file_name(app->bad_usb_view, app->file_name); | ||||
|     string_printf( | ||||
|         file_name, "%s/%s%s", BAD_USB_APP_PATH_FOLDER, app->file_name, BAD_USB_APP_EXTENSION); | ||||
|     app->bad_usb_script = bad_usb_script_open(file_name); | ||||
|     path_extract_filename(app->file_path, file_name, true); | ||||
|     bad_usb_set_file_name(app->bad_usb_view, string_get_cstr(file_name)); | ||||
|     app->bad_usb_script = bad_usb_script_open(app->file_path); | ||||
| 
 | ||||
|     string_clear(file_name); | ||||
| 
 | ||||
|  | ||||
| @ -2,6 +2,8 @@ | ||||
| #include "../bad_usb_script.h" | ||||
| #include <gui/elements.h> | ||||
| 
 | ||||
| #define MAX_NAME_LEN 64 | ||||
| 
 | ||||
| struct BadUsb { | ||||
|     View* view; | ||||
|     BadUsbOkCallback callback; | ||||
| @ -9,7 +11,7 @@ struct BadUsb { | ||||
| }; | ||||
| 
 | ||||
| typedef struct { | ||||
|     char* file_name; | ||||
|     char file_name[MAX_NAME_LEN]; | ||||
|     BadUsbState state; | ||||
|     uint8_t anim_frame; | ||||
| } BadUsbModel; | ||||
| @ -149,11 +151,11 @@ void bad_usb_set_ok_callback(BadUsb* bad_usb, BadUsbOkCallback callback, void* c | ||||
|         }); | ||||
| } | ||||
| 
 | ||||
| void bad_usb_set_file_name(BadUsb* bad_usb, char* name) { | ||||
| void bad_usb_set_file_name(BadUsb* bad_usb, const char* name) { | ||||
|     furi_assert(name); | ||||
|     with_view_model( | ||||
|         bad_usb->view, (BadUsbModel * model) { | ||||
|             model->file_name = name; | ||||
|             strncpy(model->file_name, name, MAX_NAME_LEN); | ||||
|             return true; | ||||
|         }); | ||||
| } | ||||
|  | ||||
| @ -14,6 +14,6 @@ View* bad_usb_get_view(BadUsb* bad_usb); | ||||
| 
 | ||||
| void bad_usb_set_ok_callback(BadUsb* bad_usb, BadUsbOkCallback callback, void* context); | ||||
| 
 | ||||
| void bad_usb_set_file_name(BadUsb* bad_usb, char* name); | ||||
| void bad_usb_set_file_name(BadUsb* bad_usb, const char* name); | ||||
| 
 | ||||
| void bad_usb_set_state(BadUsb* bad_usb, BadUsbState* st); | ||||
|  | ||||
							
								
								
									
										31
									
								
								applications/bt/bt_service/bt.c
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							
							
						
						
									
										31
									
								
								applications/bt/bt_service/bt.c
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							| @ -80,7 +80,7 @@ static bool bt_pin_code_verify_event_handler(Bt* bt, uint32_t pin) { | ||||
|     string_init_printf(pin_str, "Verify code\n%06d", pin); | ||||
|     dialog_message_set_text( | ||||
|         bt->dialog_message, string_get_cstr(pin_str), 64, 4, AlignCenter, AlignTop); | ||||
|     dialog_message_set_buttons(bt->dialog_message, "Cancel", "Ok", NULL); | ||||
|     dialog_message_set_buttons(bt->dialog_message, "Cancel", "OK", NULL); | ||||
|     DialogMessageButton button = dialog_message_show(bt->dialogs, bt->dialog_message); | ||||
|     string_clear(pin_str); | ||||
|     return button == DialogMessageButtonCenter; | ||||
| @ -91,11 +91,16 @@ static void bt_battery_level_changed_callback(const void* _event, void* context) | ||||
|     furi_assert(context); | ||||
| 
 | ||||
|     Bt* bt = context; | ||||
|     BtMessage message = {}; | ||||
|     const PowerEvent* event = _event; | ||||
|     if(event->type == PowerEventTypeBatteryLevelChanged) { | ||||
|         BtMessage message = { | ||||
|             .type = BtMessageTypeUpdateBatteryLevel, | ||||
|             .data.battery_level = event->data.battery_level}; | ||||
|         message.type = BtMessageTypeUpdateBatteryLevel; | ||||
|         message.data.battery_level = event->data.battery_level; | ||||
|         furi_check(osMessageQueuePut(bt->message_queue, &message, 0, osWaitForever) == osOK); | ||||
|     } else if( | ||||
|         event->type == PowerEventTypeStartCharging || event->type == PowerEventTypeFullyCharged || | ||||
|         event->type == PowerEventTypeStopCharging) { | ||||
|         message.type = BtMessageTypeUpdatePowerState; | ||||
|         furi_check(osMessageQueuePut(bt->message_queue, &message, 0, osWaitForever) == osOK); | ||||
|     } | ||||
| } | ||||
| @ -167,7 +172,11 @@ static void bt_rpc_send_bytes_callback(void* context, uint8_t* bytes, size_t byt | ||||
|     furi_assert(context); | ||||
|     Bt* bt = context; | ||||
| 
 | ||||
|     osEventFlagsClear(bt->rpc_event, BT_RPC_EVENT_ALL); | ||||
|     if(osEventFlagsGet(bt->rpc_event) & BT_RPC_EVENT_DISCONNECTED) { | ||||
|         // Early stop from sending if we're already disconnected
 | ||||
|         return; | ||||
|     } | ||||
|     osEventFlagsClear(bt->rpc_event, BT_RPC_EVENT_ALL & (~BT_RPC_EVENT_DISCONNECTED)); | ||||
|     size_t bytes_sent = 0; | ||||
|     while(bytes_sent < bytes_len) { | ||||
|         size_t bytes_remain = bytes_len - bytes_sent; | ||||
| @ -178,10 +187,14 @@ static void bt_rpc_send_bytes_callback(void* context, uint8_t* bytes, size_t byt | ||||
|             furi_hal_bt_serial_tx(&bytes[bytes_sent], bytes_remain); | ||||
|             bytes_sent += bytes_remain; | ||||
|         } | ||||
|         uint32_t event_flag = | ||||
|             osEventFlagsWait(bt->rpc_event, BT_RPC_EVENT_ALL, osFlagsWaitAny, osWaitForever); | ||||
|         // We want BT_RPC_EVENT_DISCONNECTED to stick, so don't clear
 | ||||
|         uint32_t event_flag = osEventFlagsWait( | ||||
|             bt->rpc_event, BT_RPC_EVENT_ALL, osFlagsWaitAny | osFlagsNoClear, osWaitForever); | ||||
|         if(event_flag & BT_RPC_EVENT_DISCONNECTED) { | ||||
|             break; | ||||
|         } else { | ||||
|             // If we didn't get BT_RPC_EVENT_DISCONNECTED, then clear everything else
 | ||||
|             osEventFlagsClear(bt->rpc_event, BT_RPC_EVENT_ALL & (~BT_RPC_EVENT_DISCONNECTED)); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -197,6 +210,8 @@ static bool bt_on_gap_event_callback(GapEvent event, void* context) { | ||||
|         bt->status = BtStatusConnected; | ||||
|         BtMessage message = {.type = BtMessageTypeUpdateStatus}; | ||||
|         furi_check(osMessageQueuePut(bt->message_queue, &message, 0, osWaitForever) == osOK); | ||||
|         // Clear BT_RPC_EVENT_DISCONNECTED because it might be set from previous session
 | ||||
|         osEventFlagsClear(bt->rpc_event, BT_RPC_EVENT_DISCONNECTED); | ||||
|         if(bt->profile == BtProfileSerial) { | ||||
|             // Open RPC session
 | ||||
|             bt->rpc_session = rpc_session_open(bt->rpc); | ||||
| @ -368,6 +383,8 @@ int32_t bt_srv() { | ||||
|         } else if(message.type == BtMessageTypeUpdateBatteryLevel) { | ||||
|             // Update battery level
 | ||||
|             furi_hal_bt_update_battery_level(message.data.battery_level); | ||||
|         } else if(message.type == BtMessageTypeUpdatePowerState) { | ||||
|             furi_hal_bt_update_power_state(); | ||||
|         } else if(message.type == BtMessageTypePinCodeShow) { | ||||
|             // Display PIN code
 | ||||
|             bt_pin_code_show(bt, message.data.pin_code); | ||||
|  | ||||
| @ -21,6 +21,7 @@ | ||||
| typedef enum { | ||||
|     BtMessageTypeUpdateStatus, | ||||
|     BtMessageTypeUpdateBatteryLevel, | ||||
|     BtMessageTypeUpdatePowerState, | ||||
|     BtMessageTypePinCodeShow, | ||||
|     BtMessageTypeKeysStorageUpdated, | ||||
|     BtMessageTypeSetProfile, | ||||
|  | ||||
| @ -0,0 +1,99 @@ | ||||
| #include "assets_icons.h" | ||||
| #include "file_browser_app_i.h" | ||||
| #include "gui/modules/file_browser.h" | ||||
| #include "m-string.h" | ||||
| #include <furi.h> | ||||
| #include <furi_hal.h> | ||||
| #include <storage/storage.h> | ||||
| #include <lib/toolbox/path.h> | ||||
| 
 | ||||
| static bool file_browser_app_custom_event_callback(void* context, uint32_t event) { | ||||
|     furi_assert(context); | ||||
|     FileBrowserApp* app = context; | ||||
|     return scene_manager_handle_custom_event(app->scene_manager, event); | ||||
| } | ||||
| 
 | ||||
| static bool file_browser_app_back_event_callback(void* context) { | ||||
|     furi_assert(context); | ||||
|     FileBrowserApp* app = context; | ||||
|     return scene_manager_handle_back_event(app->scene_manager); | ||||
| } | ||||
| 
 | ||||
| static void file_browser_app_tick_event_callback(void* context) { | ||||
|     furi_assert(context); | ||||
|     FileBrowserApp* app = context; | ||||
|     scene_manager_handle_tick_event(app->scene_manager); | ||||
| } | ||||
| 
 | ||||
| FileBrowserApp* file_browser_app_alloc(char* arg) { | ||||
|     UNUSED(arg); | ||||
|     FileBrowserApp* app = malloc(sizeof(FileBrowserApp)); | ||||
| 
 | ||||
|     app->gui = furi_record_open("gui"); | ||||
|     app->dialogs = furi_record_open("dialogs"); | ||||
| 
 | ||||
|     app->view_dispatcher = view_dispatcher_alloc(); | ||||
|     view_dispatcher_enable_queue(app->view_dispatcher); | ||||
| 
 | ||||
|     app->scene_manager = scene_manager_alloc(&file_browser_scene_handlers, app); | ||||
| 
 | ||||
|     view_dispatcher_set_event_callback_context(app->view_dispatcher, app); | ||||
|     view_dispatcher_set_tick_event_callback( | ||||
|         app->view_dispatcher, file_browser_app_tick_event_callback, 500); | ||||
|     view_dispatcher_set_custom_event_callback( | ||||
|         app->view_dispatcher, file_browser_app_custom_event_callback); | ||||
|     view_dispatcher_set_navigation_event_callback( | ||||
|         app->view_dispatcher, file_browser_app_back_event_callback); | ||||
| 
 | ||||
|     app->widget = widget_alloc(); | ||||
| 
 | ||||
|     string_init(app->file_path); | ||||
|     app->file_browser = file_browser_alloc(app->file_path); | ||||
|     file_browser_configure(app->file_browser, "*", true, &I_badusb_10px, true); | ||||
| 
 | ||||
|     view_dispatcher_add_view( | ||||
|         app->view_dispatcher, FileBrowserAppViewStart, widget_get_view(app->widget)); | ||||
|     view_dispatcher_add_view( | ||||
|         app->view_dispatcher, FileBrowserAppViewResult, widget_get_view(app->widget)); | ||||
|     view_dispatcher_add_view( | ||||
|         app->view_dispatcher, FileBrowserAppViewBrowser, file_browser_get_view(app->file_browser)); | ||||
| 
 | ||||
|     view_dispatcher_attach_to_gui(app->view_dispatcher, app->gui, ViewDispatcherTypeFullscreen); | ||||
| 
 | ||||
|     scene_manager_next_scene(app->scene_manager, FileBrowserSceneStart); | ||||
| 
 | ||||
|     return app; | ||||
| } | ||||
| 
 | ||||
| void file_browser_app_free(FileBrowserApp* app) { | ||||
|     furi_assert(app); | ||||
| 
 | ||||
|     // Views
 | ||||
|     view_dispatcher_remove_view(app->view_dispatcher, FileBrowserAppViewStart); | ||||
|     view_dispatcher_remove_view(app->view_dispatcher, FileBrowserAppViewResult); | ||||
|     view_dispatcher_remove_view(app->view_dispatcher, FileBrowserAppViewBrowser); | ||||
|     widget_free(app->widget); | ||||
|     file_browser_free(app->file_browser); | ||||
| 
 | ||||
|     // View dispatcher
 | ||||
|     view_dispatcher_free(app->view_dispatcher); | ||||
|     scene_manager_free(app->scene_manager); | ||||
| 
 | ||||
|     // Close records
 | ||||
|     furi_record_close("gui"); | ||||
|     furi_record_close("notification"); | ||||
|     furi_record_close("dialogs"); | ||||
| 
 | ||||
|     string_clear(app->file_path); | ||||
| 
 | ||||
|     free(app); | ||||
| } | ||||
| 
 | ||||
| int32_t file_browser_app(void* p) { | ||||
|     FileBrowserApp* file_browser_app = file_browser_app_alloc((char*)p); | ||||
| 
 | ||||
|     view_dispatcher_run(file_browser_app->view_dispatcher); | ||||
| 
 | ||||
|     file_browser_app_free(file_browser_app); | ||||
|     return 0; | ||||
| } | ||||
| @ -0,0 +1,32 @@ | ||||
| #pragma once | ||||
| 
 | ||||
| #include "scenes/file_browser_scene.h" | ||||
| 
 | ||||
| #include <gui/gui.h> | ||||
| #include <gui/view_dispatcher.h> | ||||
| #include <gui/scene_manager.h> | ||||
| #include <gui/modules/submenu.h> | ||||
| #include <gui/modules/file_browser.h> | ||||
| #include <dialogs/dialogs.h> | ||||
| #include <notification/notification_messages.h> | ||||
| #include <gui/modules/variable_item_list.h> | ||||
| #include <gui/modules/widget.h> | ||||
| 
 | ||||
| typedef struct FileBrowserApp FileBrowserApp; | ||||
| 
 | ||||
| struct FileBrowserApp { | ||||
|     Gui* gui; | ||||
|     ViewDispatcher* view_dispatcher; | ||||
|     SceneManager* scene_manager; | ||||
|     DialogsApp* dialogs; | ||||
|     Widget* widget; | ||||
|     FileBrowser* file_browser; | ||||
| 
 | ||||
|     string_t file_path; | ||||
| }; | ||||
| 
 | ||||
| typedef enum { | ||||
|     FileBrowserAppViewStart, | ||||
|     FileBrowserAppViewBrowser, | ||||
|     FileBrowserAppViewResult, | ||||
| } FileBrowserAppView; | ||||
| @ -0,0 +1,30 @@ | ||||
| #include "file_browser_scene.h" | ||||
| 
 | ||||
| // Generate scene on_enter handlers array
 | ||||
| #define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_enter, | ||||
| void (*const file_browser_scene_on_enter_handlers[])(void*) = { | ||||
| #include "file_browser_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 file_browser_scene_on_event_handlers[])(void* context, SceneManagerEvent event) = { | ||||
| #include "file_browser_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 file_browser_scene_on_exit_handlers[])(void* context) = { | ||||
| #include "file_browser_scene_config.h" | ||||
| }; | ||||
| #undef ADD_SCENE | ||||
| 
 | ||||
| // Initialize scene handlers configuration structure
 | ||||
| const SceneManagerHandlers file_browser_scene_handlers = { | ||||
|     .on_enter_handlers = file_browser_scene_on_enter_handlers, | ||||
|     .on_event_handlers = file_browser_scene_on_event_handlers, | ||||
|     .on_exit_handlers = file_browser_scene_on_exit_handlers, | ||||
|     .scene_num = FileBrowserSceneNum, | ||||
| }; | ||||
| @ -0,0 +1,29 @@ | ||||
| #pragma once | ||||
| 
 | ||||
| #include <gui/scene_manager.h> | ||||
| 
 | ||||
| // Generate scene id and total number
 | ||||
| #define ADD_SCENE(prefix, name, id) FileBrowserScene##id, | ||||
| typedef enum { | ||||
| #include "file_browser_scene_config.h" | ||||
|     FileBrowserSceneNum, | ||||
| } FileBrowserScene; | ||||
| #undef ADD_SCENE | ||||
| 
 | ||||
| extern const SceneManagerHandlers file_browser_scene_handlers; | ||||
| 
 | ||||
| // Generate scene on_enter handlers declaration
 | ||||
| #define ADD_SCENE(prefix, name, id) void prefix##_scene_##name##_on_enter(void*); | ||||
| #include "file_browser_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 "file_browser_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 "file_browser_scene_config.h" | ||||
| #undef ADD_SCENE | ||||
| @ -0,0 +1,43 @@ | ||||
| #include "../file_browser_app_i.h" | ||||
| #include "furi/check.h" | ||||
| #include "furi/log.h" | ||||
| #include "furi_hal.h" | ||||
| #include "m-string.h" | ||||
| 
 | ||||
| #define DEFAULT_PATH "/" | ||||
| #define EXTENSION "*" | ||||
| 
 | ||||
| bool file_browser_scene_browser_on_event(void* context, SceneManagerEvent event) { | ||||
|     UNUSED(context); | ||||
|     FileBrowserApp* app = context; | ||||
|     bool consumed = false; | ||||
| 
 | ||||
|     if(event.type == SceneManagerEventTypeCustom) { | ||||
|         scene_manager_next_scene(app->scene_manager, FileBrowserSceneResult); | ||||
|         consumed = true; | ||||
|     } else if(event.type == SceneManagerEventTypeTick) { | ||||
|     } | ||||
|     return consumed; | ||||
| } | ||||
| 
 | ||||
| static void file_browser_callback(void* context) { | ||||
|     FileBrowserApp* app = context; | ||||
|     furi_assert(app); | ||||
|     view_dispatcher_send_custom_event(app->view_dispatcher, SceneManagerEventTypeCustom); | ||||
| } | ||||
| 
 | ||||
| void file_browser_scene_browser_on_enter(void* context) { | ||||
|     FileBrowserApp* app = context; | ||||
| 
 | ||||
|     file_browser_set_callback(app->file_browser, file_browser_callback, app); | ||||
| 
 | ||||
|     file_browser_start(app->file_browser, app->file_path); | ||||
| 
 | ||||
|     view_dispatcher_switch_to_view(app->view_dispatcher, FileBrowserAppViewBrowser); | ||||
| } | ||||
| 
 | ||||
| void file_browser_scene_browser_on_exit(void* context) { | ||||
|     FileBrowserApp* app = context; | ||||
| 
 | ||||
|     file_browser_stop(app->file_browser); | ||||
| } | ||||
| @ -0,0 +1,3 @@ | ||||
| ADD_SCENE(file_browser, start, Start) | ||||
| ADD_SCENE(file_browser, browser, Browser) | ||||
| ADD_SCENE(file_browser, result, Result) | ||||
| @ -0,0 +1,36 @@ | ||||
| #include "../file_browser_app_i.h" | ||||
| #include "furi_hal.h" | ||||
| #include "m-string.h" | ||||
| 
 | ||||
| void file_browser_scene_result_ok_callback(InputType type, void* context) { | ||||
|     furi_assert(context); | ||||
|     FileBrowserApp* app = context; | ||||
|     view_dispatcher_send_custom_event(app->view_dispatcher, type); | ||||
| } | ||||
| 
 | ||||
| bool file_browser_scene_result_on_event(void* context, SceneManagerEvent event) { | ||||
|     UNUSED(context); | ||||
|     //FileBrowserApp* app = context;
 | ||||
|     bool consumed = false; | ||||
| 
 | ||||
|     if(event.type == SceneManagerEventTypeCustom) { | ||||
|         consumed = true; | ||||
|     } else if(event.type == SceneManagerEventTypeTick) { | ||||
|     } | ||||
|     return consumed; | ||||
| } | ||||
| 
 | ||||
| void file_browser_scene_result_on_enter(void* context) { | ||||
|     FileBrowserApp* app = context; | ||||
| 
 | ||||
|     widget_add_string_multiline_element( | ||||
|         app->widget, 64, 10, AlignCenter, AlignTop, FontSecondary, string_get_cstr(app->file_path)); | ||||
| 
 | ||||
|     view_dispatcher_switch_to_view(app->view_dispatcher, FileBrowserAppViewResult); | ||||
| } | ||||
| 
 | ||||
| void file_browser_scene_result_on_exit(void* context) { | ||||
|     UNUSED(context); | ||||
|     FileBrowserApp* app = context; | ||||
|     widget_reset(app->widget); | ||||
| } | ||||
| @ -0,0 +1,44 @@ | ||||
| #include "../file_browser_app_i.h" | ||||
| #include "furi_hal.h" | ||||
| #include "gui/modules/widget_elements/widget_element_i.h" | ||||
| 
 | ||||
| static void | ||||
|     file_browser_scene_start_ok_callback(GuiButtonType result, InputType type, void* context) { | ||||
|     UNUSED(result); | ||||
|     furi_assert(context); | ||||
|     FileBrowserApp* app = context; | ||||
|     if(type == InputTypeShort) { | ||||
|         view_dispatcher_send_custom_event(app->view_dispatcher, type); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| bool file_browser_scene_start_on_event(void* context, SceneManagerEvent event) { | ||||
|     FileBrowserApp* app = context; | ||||
|     bool consumed = false; | ||||
| 
 | ||||
|     if(event.type == SceneManagerEventTypeCustom) { | ||||
|         string_set_str(app->file_path, "/any/badusb/demo_windows.txt"); | ||||
|         scene_manager_next_scene(app->scene_manager, FileBrowserSceneBrowser); | ||||
|         consumed = true; | ||||
|     } else if(event.type == SceneManagerEventTypeTick) { | ||||
|     } | ||||
|     return consumed; | ||||
| } | ||||
| 
 | ||||
| void file_browser_scene_start_on_enter(void* context) { | ||||
|     FileBrowserApp* app = context; | ||||
| 
 | ||||
|     widget_add_string_multiline_element( | ||||
|         app->widget, 64, 20, AlignCenter, AlignTop, FontSecondary, "Press OK to start"); | ||||
| 
 | ||||
|     widget_add_button_element( | ||||
|         app->widget, GuiButtonTypeCenter, "Ok", file_browser_scene_start_ok_callback, app); | ||||
| 
 | ||||
|     view_dispatcher_switch_to_view(app->view_dispatcher, FileBrowserAppViewStart); | ||||
| } | ||||
| 
 | ||||
| void file_browser_scene_start_on_exit(void* context) { | ||||
|     UNUSED(context); | ||||
|     FileBrowserApp* app = context; | ||||
|     widget_reset(app->widget); | ||||
| } | ||||
| @ -100,11 +100,12 @@ void desktop_pin_lock_init(DesktopSettings* settings) { | ||||
|     } else { | ||||
|         furi_hal_rtc_set_pin_fails(0); | ||||
|         furi_hal_rtc_reset_flag(FuriHalRtcFlagLock); | ||||
|         furi_hal_usb_enable(); | ||||
|     } | ||||
| 
 | ||||
|     if(desktop_pin_lock_is_locked()) { | ||||
|         furi_hal_usb_disable(); | ||||
|         Cli* cli = furi_record_open("cli"); | ||||
|         cli_session_close(cli); | ||||
|         furi_record_close("cli"); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -125,7 +125,9 @@ static void desktop_view_locked_draw(Canvas* canvas, void* model) { | ||||
|         canvas_set_font(canvas, FontSecondary); | ||||
|         elements_bold_rounded_frame(canvas, 14, 2 + STATUS_BAR_Y_SHIFT, 99, 48); | ||||
|         elements_multiline_text(canvas, 65, 20 + STATUS_BAR_Y_SHIFT, "To unlock\npress:"); | ||||
|         canvas_draw_icon(canvas, 65, 36 + STATUS_BAR_Y_SHIFT, &I_Back3_45x8); | ||||
|         canvas_draw_icon(canvas, 65, 36 + STATUS_BAR_Y_SHIFT, &I_Pin_back_arrow_10x8); | ||||
|         canvas_draw_icon(canvas, 80, 36 + STATUS_BAR_Y_SHIFT, &I_Pin_back_arrow_10x8); | ||||
|         canvas_draw_icon(canvas, 95, 36 + STATUS_BAR_Y_SHIFT, &I_Pin_back_arrow_10x8); | ||||
|         canvas_draw_icon(canvas, 16, 7 + STATUS_BAR_Y_SHIFT, &I_WarningDolphin_45x42); | ||||
|         canvas_draw_dot(canvas, 17, 61); | ||||
|     } else if(view_state == DesktopViewLockedStateUnlockedHintShown) { | ||||
|  | ||||
| @ -1,6 +1,7 @@ | ||||
| #include "dialogs/dialogs_message.h" | ||||
| #include "dialogs_i.h" | ||||
| #include "dialogs_api_lock.h" | ||||
| #include "dialogs_module_file_select.h" | ||||
| #include "dialogs_module_file_browser.h" | ||||
| #include "dialogs_module_message.h" | ||||
| 
 | ||||
| static DialogsApp* dialogs_app_alloc() { | ||||
| @ -13,9 +14,9 @@ static DialogsApp* dialogs_app_alloc() { | ||||
| static void dialogs_app_process_message(DialogsApp* app, DialogsAppMessage* message) { | ||||
|     UNUSED(app); | ||||
|     switch(message->command) { | ||||
|     case DialogsAppCommandFileOpen: | ||||
|     case DialogsAppCommandFileBrowser: | ||||
|         message->return_data->bool_value = | ||||
|             dialogs_app_process_module_file_select(&message->data->file_select); | ||||
|             dialogs_app_process_module_file_browser(&message->data->file_browser); | ||||
|         break; | ||||
|     case DialogsAppCommandDialog: | ||||
|         message->return_data->dialog_value = | ||||
|  | ||||
| @ -1,6 +1,7 @@ | ||||
| #pragma once | ||||
| #include <furi.h> | ||||
| #include <gui/canvas.h> | ||||
| #include "m-string.h" | ||||
| 
 | ||||
| #ifdef __cplusplus | ||||
| extern "C" { | ||||
| @ -10,25 +11,27 @@ extern "C" { | ||||
| 
 | ||||
| typedef struct DialogsApp DialogsApp; | ||||
| 
 | ||||
| /****************** FILE SELECT ******************/ | ||||
| /****************** FILE BROWSER ******************/ | ||||
| 
 | ||||
| /**
 | ||||
|  * Shows and processes the file selection dialog | ||||
|  * Shows and processes the file browser dialog | ||||
|  * @param context api pointer | ||||
|  * @param path path to directory | ||||
|  * @param result_path selected file path string pointer | ||||
|  * @param path preselected file path string pointer | ||||
|  * @param extension file extension to be offered for selection | ||||
|  * @param selected_filename buffer where the selected filename will be saved | ||||
|  * @param selected_filename_size and the size of this buffer | ||||
|  * @param preselected_filename filename to be preselected | ||||
|  * @param skip_assets true - do not show assets folders | ||||
|  * @param icon file icon pointer, NULL for default icon | ||||
|  * @param hide_ext true - hide extensions for files | ||||
|  * @return bool whether a file was selected | ||||
|  */ | ||||
| bool dialog_file_select_show( | ||||
| bool dialog_file_browser_show( | ||||
|     DialogsApp* context, | ||||
|     const char* path, | ||||
|     string_ptr result_path, | ||||
|     string_ptr path, | ||||
|     const char* extension, | ||||
|     char* result, | ||||
|     uint8_t result_size, | ||||
|     const char* preselected_filename); | ||||
|     bool skip_assets, | ||||
|     const Icon* icon, | ||||
|     bool hide_ext); | ||||
| 
 | ||||
| /****************** MESSAGE ******************/ | ||||
| 
 | ||||
|  | ||||
| @ -1,31 +1,36 @@ | ||||
| #include "dialogs/dialogs_message.h" | ||||
| #include "dialogs_i.h" | ||||
| #include "dialogs_api_lock.h" | ||||
| #include "m-string.h" | ||||
| 
 | ||||
| /****************** File select ******************/ | ||||
| /****************** File browser ******************/ | ||||
| 
 | ||||
| bool dialog_file_select_show( | ||||
| bool dialog_file_browser_show( | ||||
|     DialogsApp* context, | ||||
|     const char* path, | ||||
|     string_ptr result_path, | ||||
|     string_ptr path, | ||||
|     const char* extension, | ||||
|     char* result, | ||||
|     uint8_t result_size, | ||||
|     const char* preselected_filename) { | ||||
|     bool skip_assets, | ||||
|     const Icon* icon, | ||||
|     bool hide_ext) { | ||||
|     FuriApiLock lock = API_LOCK_INIT_LOCKED(); | ||||
|     furi_check(lock != NULL); | ||||
| 
 | ||||
|     DialogsAppData data = { | ||||
|         .file_select = { | ||||
|             .path = path, | ||||
|         .file_browser = { | ||||
|             .extension = extension, | ||||
|             .result = result, | ||||
|             .result_size = result_size, | ||||
|             .preselected_filename = preselected_filename, | ||||
|             .result_path = result_path, | ||||
|             .file_icon = icon, | ||||
|             .hide_ext = hide_ext, | ||||
|             .skip_assets = skip_assets, | ||||
|             .preselected_filename = path, | ||||
| 
 | ||||
|         }}; | ||||
| 
 | ||||
|     DialogsAppReturn return_data; | ||||
|     DialogsAppMessage message = { | ||||
|         .lock = lock, | ||||
|         .command = DialogsAppCommandFileOpen, | ||||
|         .command = DialogsAppCommandFileBrowser, | ||||
|         .data = &data, | ||||
|         .return_data = &return_data, | ||||
|     }; | ||||
|  | ||||
| @ -2,25 +2,27 @@ | ||||
| #include <furi.h> | ||||
| #include "dialogs_i.h" | ||||
| #include "dialogs_api_lock.h" | ||||
| #include "m-string.h" | ||||
| 
 | ||||
| #ifdef __cplusplus | ||||
| extern "C" { | ||||
| #endif | ||||
| 
 | ||||
| typedef struct { | ||||
|     const char* path; | ||||
|     const char* extension; | ||||
|     char* result; | ||||
|     uint8_t result_size; | ||||
|     const char* preselected_filename; | ||||
| } DialogsAppMessageDataFileSelect; | ||||
|     bool skip_assets; | ||||
|     bool hide_ext; | ||||
|     const Icon* file_icon; | ||||
|     string_ptr result_path; | ||||
|     string_ptr preselected_filename; | ||||
| } DialogsAppMessageDataFileBrowser; | ||||
| 
 | ||||
| typedef struct { | ||||
|     const DialogMessage* message; | ||||
| } DialogsAppMessageDataDialog; | ||||
| 
 | ||||
| typedef union { | ||||
|     DialogsAppMessageDataFileSelect file_select; | ||||
|     DialogsAppMessageDataFileBrowser file_browser; | ||||
|     DialogsAppMessageDataDialog dialog; | ||||
| } DialogsAppData; | ||||
| 
 | ||||
| @ -30,7 +32,7 @@ typedef union { | ||||
| } DialogsAppReturn; | ||||
| 
 | ||||
| typedef enum { | ||||
|     DialogsAppCommandFileOpen, | ||||
|     DialogsAppCommandFileBrowser, | ||||
|     DialogsAppCommandDialog, | ||||
| } DialogsAppCommand; | ||||
| 
 | ||||
|  | ||||
							
								
								
									
										59
									
								
								applications/dialogs/dialogs_module_file_browser.c
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								applications/dialogs/dialogs_module_file_browser.c
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,59 @@ | ||||
| #include "dialogs_i.h" | ||||
| #include "dialogs_api_lock.h" | ||||
| #include "gui/modules/file_browser.h" | ||||
| 
 | ||||
| typedef struct { | ||||
|     FuriApiLock lock; | ||||
|     bool result; | ||||
| } DialogsAppFileBrowserContext; | ||||
| 
 | ||||
| static void dialogs_app_file_browser_back_callback(void* context) { | ||||
|     furi_assert(context); | ||||
|     DialogsAppFileBrowserContext* file_browser_context = context; | ||||
|     file_browser_context->result = false; | ||||
|     API_LOCK_UNLOCK(file_browser_context->lock); | ||||
| } | ||||
| 
 | ||||
| static void dialogs_app_file_browser_callback(void* context) { | ||||
|     furi_assert(context); | ||||
|     DialogsAppFileBrowserContext* file_browser_context = context; | ||||
|     file_browser_context->result = true; | ||||
|     API_LOCK_UNLOCK(file_browser_context->lock); | ||||
| } | ||||
| 
 | ||||
| bool dialogs_app_process_module_file_browser(const DialogsAppMessageDataFileBrowser* data) { | ||||
|     bool ret = false; | ||||
|     Gui* gui = furi_record_open("gui"); | ||||
| 
 | ||||
|     DialogsAppFileBrowserContext* file_browser_context = | ||||
|         malloc(sizeof(DialogsAppFileBrowserContext)); | ||||
|     file_browser_context->lock = API_LOCK_INIT_LOCKED(); | ||||
| 
 | ||||
|     ViewHolder* view_holder = view_holder_alloc(); | ||||
|     view_holder_attach_to_gui(view_holder, gui); | ||||
|     view_holder_set_back_callback( | ||||
|         view_holder, dialogs_app_file_browser_back_callback, file_browser_context); | ||||
| 
 | ||||
|     FileBrowser* file_browser = file_browser_alloc(data->result_path); | ||||
|     file_browser_set_callback( | ||||
|         file_browser, dialogs_app_file_browser_callback, file_browser_context); | ||||
|     file_browser_configure( | ||||
|         file_browser, data->extension, data->skip_assets, data->file_icon, data->hide_ext); | ||||
|     file_browser_start(file_browser, data->preselected_filename); | ||||
| 
 | ||||
|     view_holder_set_view(view_holder, file_browser_get_view(file_browser)); | ||||
|     view_holder_start(view_holder); | ||||
|     API_LOCK_WAIT_UNTIL_UNLOCK(file_browser_context->lock); | ||||
| 
 | ||||
|     ret = file_browser_context->result; | ||||
| 
 | ||||
|     view_holder_stop(view_holder); | ||||
|     view_holder_free(view_holder); | ||||
|     file_browser_stop(file_browser); | ||||
|     file_browser_free(file_browser); | ||||
|     API_LOCK_FREE(file_browser_context->lock); | ||||
|     free(file_browser_context); | ||||
|     furi_record_close("gui"); | ||||
| 
 | ||||
|     return ret; | ||||
| } | ||||
| @ -5,7 +5,7 @@ | ||||
| extern "C" { | ||||
| #endif | ||||
| 
 | ||||
| bool dialogs_app_process_module_file_select(const DialogsAppMessageDataFileSelect* data); | ||||
| bool dialogs_app_process_module_file_browser(const DialogsAppMessageDataFileBrowser* data); | ||||
| 
 | ||||
| #ifdef __cplusplus | ||||
| } | ||||
| @ -1,59 +0,0 @@ | ||||
| #include "dialogs_i.h" | ||||
| #include "dialogs_api_lock.h" | ||||
| #include <gui/modules/file_select.h> | ||||
| 
 | ||||
| typedef struct { | ||||
|     FuriApiLock lock; | ||||
|     bool result; | ||||
| } DialogsAppFileSelectContext; | ||||
| 
 | ||||
| static void dialogs_app_file_select_back_callback(void* context) { | ||||
|     furi_assert(context); | ||||
|     DialogsAppFileSelectContext* file_select_context = context; | ||||
|     file_select_context->result = false; | ||||
|     API_LOCK_UNLOCK(file_select_context->lock); | ||||
| } | ||||
| 
 | ||||
| static void dialogs_app_file_select_callback(bool result, void* context) { | ||||
|     furi_assert(context); | ||||
|     DialogsAppFileSelectContext* file_select_context = context; | ||||
|     file_select_context->result = result; | ||||
|     API_LOCK_UNLOCK(file_select_context->lock); | ||||
| } | ||||
| 
 | ||||
| bool dialogs_app_process_module_file_select(const DialogsAppMessageDataFileSelect* data) { | ||||
|     bool ret = false; | ||||
|     Gui* gui = furi_record_open("gui"); | ||||
| 
 | ||||
|     DialogsAppFileSelectContext* file_select_context = malloc(sizeof(DialogsAppFileSelectContext)); | ||||
|     file_select_context->lock = API_LOCK_INIT_LOCKED(); | ||||
| 
 | ||||
|     ViewHolder* view_holder = view_holder_alloc(); | ||||
|     view_holder_attach_to_gui(view_holder, gui); | ||||
|     view_holder_set_back_callback( | ||||
|         view_holder, dialogs_app_file_select_back_callback, file_select_context); | ||||
| 
 | ||||
|     FileSelect* file_select = file_select_alloc(); | ||||
|     file_select_set_callback(file_select, dialogs_app_file_select_callback, file_select_context); | ||||
|     file_select_set_filter(file_select, data->path, data->extension); | ||||
|     file_select_set_result_buffer(file_select, data->result, data->result_size); | ||||
|     file_select_init(file_select); | ||||
|     if(data->preselected_filename != NULL) { | ||||
|         file_select_set_selected_file(file_select, data->preselected_filename); | ||||
|     } | ||||
| 
 | ||||
|     view_holder_set_view(view_holder, file_select_get_view(file_select)); | ||||
|     view_holder_start(view_holder); | ||||
|     API_LOCK_WAIT_UNTIL_UNLOCK(file_select_context->lock); | ||||
| 
 | ||||
|     ret = file_select_context->result; | ||||
| 
 | ||||
|     view_holder_stop(view_holder); | ||||
|     view_holder_free(view_holder); | ||||
|     file_select_free(file_select); | ||||
|     API_LOCK_FREE(file_select_context->lock); | ||||
|     free(file_select_context); | ||||
|     furi_record_close("gui"); | ||||
| 
 | ||||
|     return ret; | ||||
| } | ||||
| @ -72,19 +72,8 @@ bool dolphin_state_load(DolphinState* dolphin_state) { | ||||
| 
 | ||||
| uint64_t dolphin_state_timestamp() { | ||||
|     FuriHalRtcDateTime datetime; | ||||
|     struct tm current; | ||||
| 
 | ||||
|     furi_hal_rtc_get_datetime(&datetime); | ||||
| 
 | ||||
|     current.tm_year = datetime.year - 1900; | ||||
|     current.tm_mday = datetime.day; | ||||
|     current.tm_mon = datetime.month - 1; | ||||
| 
 | ||||
|     current.tm_hour = datetime.hour; | ||||
|     current.tm_min = datetime.minute; | ||||
|     current.tm_sec = datetime.second; | ||||
| 
 | ||||
|     return mktime(¤t); | ||||
|     return furi_hal_rtc_datetime_to_timestamp(&datetime); | ||||
| } | ||||
| 
 | ||||
| bool dolphin_state_is_levelup(uint32_t icounter) { | ||||
|  | ||||
| @ -3,7 +3,7 @@ | ||||
| typedef enum { | ||||
|     GpioStartEventOtgOff = 0, | ||||
|     GpioStartEventOtgOn, | ||||
|     GpioStartEventManualConrol, | ||||
|     GpioStartEventManualControl, | ||||
|     GpioStartEventUsbUart, | ||||
| 
 | ||||
|     GpioCustomEventErrorBack, | ||||
|  | ||||
| @ -15,15 +15,15 @@ enum GpioOtg { | ||||
| }; | ||||
| 
 | ||||
| const char* const gpio_otg_text[GpioOtgSettingsNum] = { | ||||
|     "Off", | ||||
|     "On", | ||||
|     "OFF", | ||||
|     "ON", | ||||
| }; | ||||
| 
 | ||||
| static void gpio_scene_start_var_list_enter_callback(void* context, uint32_t index) { | ||||
|     furi_assert(context); | ||||
|     GpioApp* app = context; | ||||
|     if(index == GpioItemTest) { | ||||
|         view_dispatcher_send_custom_event(app->view_dispatcher, GpioStartEventManualConrol); | ||||
|         view_dispatcher_send_custom_event(app->view_dispatcher, GpioStartEventManualControl); | ||||
|     } else if(index == GpioItemUsbUart) { | ||||
|         view_dispatcher_send_custom_event(app->view_dispatcher, GpioStartEventUsbUart); | ||||
|     } | ||||
| @ -82,7 +82,7 @@ bool gpio_scene_start_on_event(void* context, SceneManagerEvent event) { | ||||
|             furi_hal_power_enable_otg(); | ||||
|         } else if(event.event == GpioStartEventOtgOff) { | ||||
|             furi_hal_power_disable_otg(); | ||||
|         } else if(event.event == GpioStartEventManualConrol) { | ||||
|         } else if(event.event == GpioStartEventManualControl) { | ||||
|             scene_manager_set_scene_state(app->scene_manager, GpioSceneStart, GpioItemTest); | ||||
|             scene_manager_next_scene(app->scene_manager, GpioSceneTest); | ||||
|         } else if(event.event == GpioStartEventUsbUart) { | ||||
|  | ||||
| @ -45,8 +45,8 @@ static void dialog_ex_view_draw_callback(Canvas* canvas, void* _model) { | ||||
|     } | ||||
| 
 | ||||
|     // Draw header
 | ||||
|     if(model->header.text != NULL) { | ||||
|     canvas_set_font(canvas, FontPrimary); | ||||
|     if(model->header.text != NULL) { | ||||
|         elements_multiline_text_aligned( | ||||
|             canvas, | ||||
|             model->header.x, | ||||
| @ -57,8 +57,8 @@ static void dialog_ex_view_draw_callback(Canvas* canvas, void* _model) { | ||||
|     } | ||||
| 
 | ||||
|     // Draw text
 | ||||
|     if(model->text.text != NULL) { | ||||
|     canvas_set_font(canvas, FontSecondary); | ||||
|     if(model->text.text != NULL) { | ||||
|         elements_multiline_text_aligned( | ||||
|             canvas, | ||||
|             model->text.x, | ||||
|  | ||||
							
								
								
									
										534
									
								
								applications/gui/modules/file_browser.c
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										534
									
								
								applications/gui/modules/file_browser.c
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,534 @@ | ||||
| #include "file_browser.h" | ||||
| #include "assets_icons.h" | ||||
| #include "cmsis_os2.h" | ||||
| #include "file_browser_worker.h" | ||||
| #include "furi/check.h" | ||||
| #include "furi/common_defines.h" | ||||
| #include "furi/log.h" | ||||
| #include "furi_hal_resources.h" | ||||
| #include "m-string.h" | ||||
| #include <m-array.h> | ||||
| #include <gui/elements.h> | ||||
| #include <furi.h> | ||||
| #include "toolbox/path.h" | ||||
| 
 | ||||
| #define LIST_ITEMS 5u | ||||
| #define MAX_LEN_PX 110 | ||||
| #define FRAME_HEIGHT 12 | ||||
| #define Y_OFFSET 3 | ||||
| 
 | ||||
| #define ITEM_LIST_LEN_MAX 100 | ||||
| 
 | ||||
| typedef enum { | ||||
|     BrowserItemTypeLoading, | ||||
|     BrowserItemTypeBack, | ||||
|     BrowserItemTypeFolder, | ||||
|     BrowserItemTypeFile, | ||||
| } BrowserItemType; | ||||
| 
 | ||||
| typedef struct { | ||||
|     string_t path; | ||||
|     BrowserItemType type; | ||||
| } BrowserItem_t; | ||||
| 
 | ||||
| static void BrowserItem_t_init(BrowserItem_t* obj) { | ||||
|     obj->type = BrowserItemTypeLoading; | ||||
|     string_init(obj->path); | ||||
| } | ||||
| 
 | ||||
| static void BrowserItem_t_init_set(BrowserItem_t* obj, const BrowserItem_t* src) { | ||||
|     obj->type = src->type; | ||||
|     string_init_set(obj->path, src->path); | ||||
| } | ||||
| 
 | ||||
| static void BrowserItem_t_set(BrowserItem_t* obj, const BrowserItem_t* src) { | ||||
|     obj->type = src->type; | ||||
|     string_set(obj->path, src->path); | ||||
| } | ||||
| 
 | ||||
| static void BrowserItem_t_clear(BrowserItem_t* obj) { | ||||
|     string_clear(obj->path); | ||||
| } | ||||
| 
 | ||||
| ARRAY_DEF( | ||||
|     items_array, | ||||
|     BrowserItem_t, | ||||
|     (INIT(API_2(BrowserItem_t_init)), | ||||
|      SET(API_6(BrowserItem_t_set)), | ||||
|      INIT_SET(API_6(BrowserItem_t_init_set)), | ||||
|      CLEAR(API_2(BrowserItem_t_clear)))) | ||||
| 
 | ||||
| struct FileBrowser { | ||||
|     View* view; | ||||
|     BrowserWorker* worker; | ||||
|     const char* ext_filter; | ||||
|     bool skip_assets; | ||||
| 
 | ||||
|     FileBrowserCallback callback; | ||||
|     void* context; | ||||
| 
 | ||||
|     string_ptr result_path; | ||||
| }; | ||||
| 
 | ||||
| typedef struct { | ||||
|     items_array_t items; | ||||
| 
 | ||||
|     bool is_root; | ||||
|     bool folder_loading; | ||||
|     bool list_loading; | ||||
|     uint32_t item_cnt; | ||||
|     int32_t item_idx; | ||||
|     int32_t array_offset; | ||||
|     int32_t list_offset; | ||||
| 
 | ||||
|     const Icon* file_icon; | ||||
|     bool hide_ext; | ||||
| } FileBrowserModel; | ||||
| 
 | ||||
| static const Icon* BrowserItemIcons[] = { | ||||
|     [BrowserItemTypeLoading] = &I_loading_10px, | ||||
|     [BrowserItemTypeBack] = &I_back_10px, | ||||
|     [BrowserItemTypeFolder] = &I_dir_10px, | ||||
|     [BrowserItemTypeFile] = &I_unknown_10px, | ||||
| }; | ||||
| 
 | ||||
| static void file_browser_view_draw_callback(Canvas* canvas, void* _model); | ||||
| static bool file_browser_view_input_callback(InputEvent* event, void* context); | ||||
| 
 | ||||
| static void | ||||
|     browser_folder_open_cb(void* context, uint32_t item_cnt, int32_t file_idx, bool is_root); | ||||
| static void browser_list_load_cb(void* context, uint32_t list_load_offset); | ||||
| static void browser_list_item_cb(void* context, string_t item_path, bool is_folder, bool is_last); | ||||
| static void browser_long_load_cb(void* context); | ||||
| 
 | ||||
| FileBrowser* file_browser_alloc(string_ptr result_path) { | ||||
|     furi_assert(result_path); | ||||
|     FileBrowser* browser = malloc(sizeof(FileBrowser)); | ||||
|     browser->view = view_alloc(); | ||||
|     view_allocate_model(browser->view, ViewModelTypeLocking, sizeof(FileBrowserModel)); | ||||
|     view_set_context(browser->view, browser); | ||||
|     view_set_draw_callback(browser->view, file_browser_view_draw_callback); | ||||
|     view_set_input_callback(browser->view, file_browser_view_input_callback); | ||||
| 
 | ||||
|     browser->result_path = result_path; | ||||
| 
 | ||||
|     with_view_model( | ||||
|         browser->view, (FileBrowserModel * model) { | ||||
|             items_array_init(model->items); | ||||
|             return false; | ||||
|         }); | ||||
| 
 | ||||
|     return browser; | ||||
| } | ||||
| 
 | ||||
| void file_browser_free(FileBrowser* browser) { | ||||
|     furi_assert(browser); | ||||
| 
 | ||||
|     with_view_model( | ||||
|         browser->view, (FileBrowserModel * model) { | ||||
|             items_array_clear(model->items); | ||||
|             return false; | ||||
|         }); | ||||
| 
 | ||||
|     view_free(browser->view); | ||||
|     free(browser); | ||||
| } | ||||
| 
 | ||||
| View* file_browser_get_view(FileBrowser* browser) { | ||||
|     furi_assert(browser); | ||||
|     return browser->view; | ||||
| } | ||||
| 
 | ||||
| void file_browser_configure( | ||||
|     FileBrowser* browser, | ||||
|     const char* extension, | ||||
|     bool skip_assets, | ||||
|     const Icon* file_icon, | ||||
|     bool hide_ext) { | ||||
|     furi_assert(browser); | ||||
| 
 | ||||
|     browser->ext_filter = extension; | ||||
|     browser->skip_assets = skip_assets; | ||||
| 
 | ||||
|     with_view_model( | ||||
|         browser->view, (FileBrowserModel * model) { | ||||
|             model->file_icon = file_icon; | ||||
|             model->hide_ext = hide_ext; | ||||
|             return false; | ||||
|         }); | ||||
| } | ||||
| 
 | ||||
| void file_browser_start(FileBrowser* browser, string_t path) { | ||||
|     furi_assert(browser); | ||||
|     browser->worker = file_browser_worker_alloc(path, browser->ext_filter, browser->skip_assets); | ||||
|     file_browser_worker_set_callback_context(browser->worker, browser); | ||||
|     file_browser_worker_set_folder_callback(browser->worker, browser_folder_open_cb); | ||||
|     file_browser_worker_set_list_callback(browser->worker, browser_list_load_cb); | ||||
|     file_browser_worker_set_item_callback(browser->worker, browser_list_item_cb); | ||||
|     file_browser_worker_set_long_load_callback(browser->worker, browser_long_load_cb); | ||||
| } | ||||
| 
 | ||||
| void file_browser_stop(FileBrowser* browser) { | ||||
|     furi_assert(browser); | ||||
|     file_browser_worker_free(browser->worker); | ||||
|     with_view_model( | ||||
|         browser->view, (FileBrowserModel * model) { | ||||
|             items_array_reset(model->items); | ||||
|             model->item_cnt = 0; | ||||
|             model->item_idx = 0; | ||||
|             model->array_offset = 0; | ||||
|             model->list_offset = 0; | ||||
|             return false; | ||||
|         }); | ||||
| } | ||||
| 
 | ||||
| void file_browser_set_callback(FileBrowser* browser, FileBrowserCallback callback, void* context) { | ||||
|     browser->context = context; | ||||
|     browser->callback = callback; | ||||
| } | ||||
| 
 | ||||
| static bool browser_is_item_in_array(FileBrowserModel* model, uint32_t idx) { | ||||
|     size_t array_size = items_array_size(model->items); | ||||
| 
 | ||||
|     if((idx >= (uint32_t)model->array_offset + array_size) || | ||||
|        (idx < (uint32_t)model->array_offset)) { | ||||
|         return false; | ||||
|     } | ||||
|     return true; | ||||
| } | ||||
| 
 | ||||
| static bool browser_is_list_load_required(FileBrowserModel* model) { | ||||
|     size_t array_size = items_array_size(model->items); | ||||
|     uint32_t item_cnt = (model->is_root) ? model->item_cnt : model->item_cnt - 1; | ||||
| 
 | ||||
|     if((model->list_loading) || (array_size >= item_cnt)) { | ||||
|         return false; | ||||
|     } | ||||
| 
 | ||||
|     if((model->array_offset > 0) && | ||||
|        (model->item_idx < (model->array_offset + ITEM_LIST_LEN_MAX / 4))) { | ||||
|         return true; | ||||
|     } | ||||
| 
 | ||||
|     if(((model->array_offset + array_size) < item_cnt) && | ||||
|        (model->item_idx > (int32_t)(model->array_offset + array_size - ITEM_LIST_LEN_MAX / 4))) { | ||||
|         return true; | ||||
|     } | ||||
| 
 | ||||
|     return false; | ||||
| } | ||||
| 
 | ||||
| static void browser_update_offset(FileBrowser* browser) { | ||||
|     furi_assert(browser); | ||||
| 
 | ||||
|     with_view_model( | ||||
|         browser->view, (FileBrowserModel * model) { | ||||
|             uint16_t bounds = model->item_cnt > (LIST_ITEMS - 1) ? 2 : model->item_cnt; | ||||
| 
 | ||||
|             if((model->item_cnt > (LIST_ITEMS - 1)) && | ||||
|                (model->item_idx >= ((int32_t)model->item_cnt - 1))) { | ||||
|                 model->list_offset = model->item_idx - (LIST_ITEMS - 1); | ||||
|             } else if(model->list_offset < model->item_idx - bounds) { | ||||
|                 model->list_offset = CLAMP( | ||||
|                     model->item_idx - (int32_t)(LIST_ITEMS - 2), | ||||
|                     (int32_t)model->item_cnt - bounds, | ||||
|                     0); | ||||
|             } else if(model->list_offset > model->item_idx - bounds) { | ||||
|                 model->list_offset = | ||||
|                     CLAMP(model->item_idx - 1, (int32_t)model->item_cnt - bounds, 0); | ||||
|             } | ||||
| 
 | ||||
|             return false; | ||||
|         }); | ||||
| } | ||||
| 
 | ||||
| static void | ||||
|     browser_folder_open_cb(void* context, uint32_t item_cnt, int32_t file_idx, bool is_root) { | ||||
|     furi_assert(context); | ||||
|     FileBrowser* browser = (FileBrowser*)context; | ||||
| 
 | ||||
|     int32_t load_offset = 0; | ||||
| 
 | ||||
|     with_view_model( | ||||
|         browser->view, (FileBrowserModel * model) { | ||||
|             items_array_reset(model->items); | ||||
|             if(is_root) { | ||||
|                 model->item_cnt = item_cnt; | ||||
|                 model->item_idx = (file_idx > 0) ? file_idx : 0; | ||||
|                 load_offset = | ||||
|                     CLAMP(model->item_idx - ITEM_LIST_LEN_MAX / 2, (int32_t)model->item_cnt, 0); | ||||
|             } else { | ||||
|                 model->item_cnt = item_cnt + 1; | ||||
|                 model->item_idx = file_idx + 1; | ||||
|                 load_offset = CLAMP( | ||||
|                     model->item_idx - ITEM_LIST_LEN_MAX / 2 - 1, (int32_t)model->item_cnt - 1, 0); | ||||
|             } | ||||
|             model->array_offset = 0; | ||||
|             model->list_offset = 0; | ||||
|             model->is_root = is_root; | ||||
|             model->list_loading = true; | ||||
|             model->folder_loading = false; | ||||
|             return true; | ||||
|         }); | ||||
|     browser_update_offset(browser); | ||||
| 
 | ||||
|     file_browser_worker_load(browser->worker, load_offset, ITEM_LIST_LEN_MAX); | ||||
| } | ||||
| 
 | ||||
| static void browser_list_load_cb(void* context, uint32_t list_load_offset) { | ||||
|     furi_assert(context); | ||||
|     FileBrowser* browser = (FileBrowser*)context; | ||||
| 
 | ||||
|     BrowserItem_t back_item; | ||||
|     BrowserItem_t_init(&back_item); | ||||
|     back_item.type = BrowserItemTypeBack; | ||||
| 
 | ||||
|     with_view_model( | ||||
|         browser->view, (FileBrowserModel * model) { | ||||
|             items_array_reset(model->items); | ||||
|             model->array_offset = list_load_offset; | ||||
|             if(!model->is_root) { | ||||
|                 if(list_load_offset == 0) { | ||||
|                     items_array_push_back(model->items, back_item); | ||||
|                 } else { | ||||
|                     model->array_offset += 1; | ||||
|                 } | ||||
|             } | ||||
|             return false; | ||||
|         }); | ||||
| 
 | ||||
|     BrowserItem_t_clear(&back_item); | ||||
| } | ||||
| 
 | ||||
| static void browser_list_item_cb(void* context, string_t item_path, bool is_folder, bool is_last) { | ||||
|     furi_assert(context); | ||||
|     FileBrowser* browser = (FileBrowser*)context; | ||||
| 
 | ||||
|     BrowserItem_t item; | ||||
| 
 | ||||
|     if(!is_last) { | ||||
|         BrowserItem_t_init(&item); | ||||
|         string_set(item.path, item_path); | ||||
|         item.type = (is_folder) ? BrowserItemTypeFolder : BrowserItemTypeFile; | ||||
| 
 | ||||
|         with_view_model( | ||||
|             browser->view, (FileBrowserModel * model) { | ||||
|                 items_array_push_back(model->items, item); | ||||
|                 return false; | ||||
|             }); | ||||
|         BrowserItem_t_clear(&item); | ||||
|     } else { | ||||
|         with_view_model( | ||||
|             browser->view, (FileBrowserModel * model) { | ||||
|                 model->list_loading = false; | ||||
|                 return true; | ||||
|             }); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| static void browser_long_load_cb(void* context) { | ||||
|     furi_assert(context); | ||||
|     FileBrowser* browser = (FileBrowser*)context; | ||||
| 
 | ||||
|     with_view_model( | ||||
|         browser->view, (FileBrowserModel * model) { | ||||
|             model->folder_loading = true; | ||||
|             return true; | ||||
|         }); | ||||
| } | ||||
| 
 | ||||
| static void browser_draw_frame(Canvas* canvas, uint16_t idx, bool scrollbar) { | ||||
|     canvas_set_color(canvas, ColorBlack); | ||||
|     canvas_draw_box( | ||||
|         canvas, 0, Y_OFFSET + idx * FRAME_HEIGHT, (scrollbar ? 122 : 127), FRAME_HEIGHT); | ||||
| 
 | ||||
|     canvas_set_color(canvas, ColorWhite); | ||||
|     canvas_draw_dot(canvas, 0, Y_OFFSET + idx * FRAME_HEIGHT); | ||||
|     canvas_draw_dot(canvas, 1, Y_OFFSET + idx * FRAME_HEIGHT); | ||||
|     canvas_draw_dot(canvas, 0, (Y_OFFSET + idx * FRAME_HEIGHT) + 1); | ||||
| 
 | ||||
|     canvas_draw_dot(canvas, 0, (Y_OFFSET + idx * FRAME_HEIGHT) + (FRAME_HEIGHT - 1)); | ||||
|     canvas_draw_dot(canvas, scrollbar ? 121 : 126, Y_OFFSET + idx * FRAME_HEIGHT); | ||||
|     canvas_draw_dot( | ||||
|         canvas, scrollbar ? 121 : 126, (Y_OFFSET + idx * FRAME_HEIGHT) + (FRAME_HEIGHT - 1)); | ||||
| } | ||||
| 
 | ||||
| static void browser_draw_loading(Canvas* canvas, FileBrowserModel* model) { | ||||
|     uint8_t width = 49; | ||||
|     uint8_t height = 47; | ||||
|     uint8_t x = 128 / 2 - width / 2; | ||||
|     uint8_t y = 64 / 2 - height / 2; | ||||
| 
 | ||||
|     UNUSED(model); | ||||
| 
 | ||||
|     elements_bold_rounded_frame(canvas, x, y, width, height); | ||||
| 
 | ||||
|     canvas_set_font(canvas, FontSecondary); | ||||
|     elements_multiline_text(canvas, x + 7, y + 13, "Loading..."); | ||||
| 
 | ||||
|     canvas_draw_icon(canvas, x + 13, y + 19, &A_Loading_24); | ||||
| } | ||||
| 
 | ||||
| static void browser_draw_list(Canvas* canvas, FileBrowserModel* model) { | ||||
|     uint32_t array_size = items_array_size(model->items); | ||||
|     bool show_scrollbar = model->item_cnt > LIST_ITEMS; | ||||
| 
 | ||||
|     string_t filename; | ||||
|     string_init(filename); | ||||
| 
 | ||||
|     for(uint32_t i = 0; i < MIN(model->item_cnt, LIST_ITEMS); i++) { | ||||
|         int32_t idx = CLAMP((uint32_t)(i + model->list_offset), model->item_cnt, 0u); | ||||
| 
 | ||||
|         BrowserItemType item_type = BrowserItemTypeLoading; | ||||
| 
 | ||||
|         if(browser_is_item_in_array(model, idx)) { | ||||
|             BrowserItem_t* item = items_array_get( | ||||
|                 model->items, CLAMP(idx - model->array_offset, (int32_t)(array_size - 1), 0)); | ||||
|             item_type = item->type; | ||||
|             path_extract_filename( | ||||
|                 item->path, filename, (model->hide_ext) && (item_type == BrowserItemTypeFile)); | ||||
|         } else { | ||||
|             string_set_str(filename, "---"); | ||||
|         } | ||||
| 
 | ||||
|         if(item_type == BrowserItemTypeBack) { | ||||
|             string_set_str(filename, ". ."); | ||||
|         } | ||||
| 
 | ||||
|         elements_string_fit_width( | ||||
|             canvas, filename, (show_scrollbar ? MAX_LEN_PX - 6 : MAX_LEN_PX)); | ||||
| 
 | ||||
|         if(model->item_idx == idx) { | ||||
|             browser_draw_frame(canvas, i, show_scrollbar); | ||||
|         } else { | ||||
|             canvas_set_color(canvas, ColorBlack); | ||||
|         } | ||||
| 
 | ||||
|         if((item_type == BrowserItemTypeFile) && (model->file_icon)) { | ||||
|             canvas_draw_icon(canvas, 2, Y_OFFSET + 1 + i * FRAME_HEIGHT, model->file_icon); | ||||
|         } else if(BrowserItemIcons[item_type] != NULL) { | ||||
|             canvas_draw_icon( | ||||
|                 canvas, 2, Y_OFFSET + 1 + i * FRAME_HEIGHT, BrowserItemIcons[item_type]); | ||||
|         } | ||||
|         canvas_draw_str(canvas, 15, Y_OFFSET + 9 + i * FRAME_HEIGHT, string_get_cstr(filename)); | ||||
|     } | ||||
| 
 | ||||
|     if(show_scrollbar) { | ||||
|         elements_scrollbar_pos( | ||||
|             canvas, | ||||
|             126, | ||||
|             Y_OFFSET, | ||||
|             canvas_height(canvas) - Y_OFFSET, | ||||
|             model->item_idx, | ||||
|             model->item_cnt); | ||||
|     } | ||||
| 
 | ||||
|     string_clear(filename); | ||||
| } | ||||
| 
 | ||||
| static void file_browser_view_draw_callback(Canvas* canvas, void* _model) { | ||||
|     FileBrowserModel* model = _model; | ||||
| 
 | ||||
|     if(model->folder_loading) { | ||||
|         browser_draw_loading(canvas, model); | ||||
|     } else { | ||||
|         browser_draw_list(canvas, model); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| static bool file_browser_view_input_callback(InputEvent* event, void* context) { | ||||
|     FileBrowser* browser = context; | ||||
|     furi_assert(browser); | ||||
|     bool consumed = false; | ||||
|     bool is_loading = false; | ||||
| 
 | ||||
|     with_view_model( | ||||
|         browser->view, (FileBrowserModel * model) { | ||||
|             is_loading = model->folder_loading; | ||||
|             return false; | ||||
|         }); | ||||
| 
 | ||||
|     if(is_loading) { | ||||
|         return false; | ||||
|     } else if(event->key == InputKeyUp || event->key == InputKeyDown) { | ||||
|         if(event->type == InputTypeShort || event->type == InputTypeRepeat) { | ||||
|             with_view_model( | ||||
|                 browser->view, (FileBrowserModel * model) { | ||||
|                     if(event->key == InputKeyUp) { | ||||
|                         model->item_idx = | ||||
|                             ((model->item_idx - 1) + model->item_cnt) % model->item_cnt; | ||||
|                         if(browser_is_list_load_required(model)) { | ||||
|                             model->list_loading = true; | ||||
|                             int32_t load_offset = CLAMP( | ||||
|                                 model->item_idx - ITEM_LIST_LEN_MAX / 4 * 3, | ||||
|                                 (int32_t)model->item_cnt - ITEM_LIST_LEN_MAX, | ||||
|                                 0); | ||||
|                             file_browser_worker_load( | ||||
|                                 browser->worker, load_offset, ITEM_LIST_LEN_MAX); | ||||
|                         } | ||||
|                     } else if(event->key == InputKeyDown) { | ||||
|                         model->item_idx = (model->item_idx + 1) % model->item_cnt; | ||||
|                         if(browser_is_list_load_required(model)) { | ||||
|                             model->list_loading = true; | ||||
|                             int32_t load_offset = CLAMP( | ||||
|                                 model->item_idx - ITEM_LIST_LEN_MAX / 4 * 1, | ||||
|                                 (int32_t)model->item_cnt - ITEM_LIST_LEN_MAX, | ||||
|                                 0); | ||||
|                             file_browser_worker_load( | ||||
|                                 browser->worker, load_offset, ITEM_LIST_LEN_MAX); | ||||
|                         } | ||||
|                     } | ||||
|                     return true; | ||||
|                 }); | ||||
|             browser_update_offset(browser); | ||||
|             consumed = true; | ||||
|         } | ||||
|     } else if(event->key == InputKeyOk) { | ||||
|         if(event->type == InputTypeShort) { | ||||
|             BrowserItem_t* selected_item = NULL; | ||||
|             int32_t select_index = 0; | ||||
|             with_view_model( | ||||
|                 browser->view, (FileBrowserModel * model) { | ||||
|                     if(browser_is_item_in_array(model, model->item_idx)) { | ||||
|                         selected_item = | ||||
|                             items_array_get(model->items, model->item_idx - model->array_offset); | ||||
|                         select_index = model->item_idx; | ||||
|                         if((!model->is_root) && (select_index > 0)) { | ||||
|                             select_index -= 1; | ||||
|                         } | ||||
|                     } | ||||
|                     return false; | ||||
|                 }); | ||||
| 
 | ||||
|             if(selected_item) { | ||||
|                 if(selected_item->type == BrowserItemTypeBack) { | ||||
|                     file_browser_worker_folder_exit(browser->worker); | ||||
|                 } else if(selected_item->type == BrowserItemTypeFolder) { | ||||
|                     file_browser_worker_folder_enter( | ||||
|                         browser->worker, selected_item->path, select_index); | ||||
|                 } else if(selected_item->type == BrowserItemTypeFile) { | ||||
|                     string_set(browser->result_path, selected_item->path); | ||||
|                     if(browser->callback) { | ||||
|                         browser->callback(browser->context); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|             consumed = true; | ||||
|         } | ||||
|     } else if(event->key == InputKeyLeft) { | ||||
|         if(event->type == InputTypeShort) { | ||||
|             bool is_root = false; | ||||
|             with_view_model( | ||||
|                 browser->view, (FileBrowserModel * model) { | ||||
|                     is_root = model->is_root; | ||||
|                     return false; | ||||
|                 }); | ||||
|             if(!is_root) { | ||||
|                 file_browser_worker_folder_exit(browser->worker); | ||||
|             } | ||||
|             consumed = true; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     return consumed; | ||||
| } | ||||
							
								
								
									
										39
									
								
								applications/gui/modules/file_browser.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								applications/gui/modules/file_browser.h
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,39 @@ | ||||
| /**
 | ||||
|  * @file file_browser.h | ||||
|  * GUI: FileBrowser view module API | ||||
|  */ | ||||
| 
 | ||||
| #pragma once | ||||
| 
 | ||||
| #include "m-string.h" | ||||
| #include <gui/view.h> | ||||
| 
 | ||||
| #ifdef __cplusplus | ||||
| extern "C" { | ||||
| #endif | ||||
| 
 | ||||
| typedef struct FileBrowser FileBrowser; | ||||
| typedef void (*FileBrowserCallback)(void* context); | ||||
| 
 | ||||
| FileBrowser* file_browser_alloc(string_ptr result_path); | ||||
| 
 | ||||
| void file_browser_free(FileBrowser* browser); | ||||
| 
 | ||||
| View* file_browser_get_view(FileBrowser* browser); | ||||
| 
 | ||||
| void file_browser_configure( | ||||
|     FileBrowser* browser, | ||||
|     const char* extension, | ||||
|     bool skip_assets, | ||||
|     const Icon* file_icon, | ||||
|     bool hide_ext); | ||||
| 
 | ||||
| void file_browser_start(FileBrowser* browser, string_t path); | ||||
| 
 | ||||
| void file_browser_stop(FileBrowser* browser); | ||||
| 
 | ||||
| void file_browser_set_callback(FileBrowser* browser, FileBrowserCallback callback, void* context); | ||||
| 
 | ||||
| #ifdef __cplusplus | ||||
| } | ||||
| #endif | ||||
							
								
								
									
										408
									
								
								applications/gui/modules/file_browser_worker.c
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										408
									
								
								applications/gui/modules/file_browser_worker.c
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,408 @@ | ||||
| #include "file_browser_worker.h" | ||||
| #include "furi/check.h" | ||||
| #include "furi/common_defines.h" | ||||
| #include "m-string.h" | ||||
| #include "storage/filesystem_api_defines.h" | ||||
| #include <m-array.h> | ||||
| #include <stdbool.h> | ||||
| #include <storage/storage.h> | ||||
| #include <furi.h> | ||||
| #include <stddef.h> | ||||
| #include "toolbox/path.h" | ||||
| 
 | ||||
| #define TAG "BrowserWorker" | ||||
| 
 | ||||
| #define ASSETS_DIR "assets" | ||||
| #define BROWSER_ROOT "/any" | ||||
| #define FILE_NAME_LEN_MAX 256 | ||||
| #define LONG_LOAD_THRESHOLD 100 | ||||
| 
 | ||||
| typedef enum { | ||||
|     WorkerEvtStop = (1 << 0), | ||||
|     WorkerEvtLoad = (1 << 1), | ||||
|     WorkerEvtFolderEnter = (1 << 2), | ||||
|     WorkerEvtFolderExit = (1 << 3), | ||||
| } WorkerEvtFlags; | ||||
| 
 | ||||
| #define WORKER_FLAGS_ALL \ | ||||
|     (WorkerEvtStop | WorkerEvtLoad | WorkerEvtFolderEnter | WorkerEvtFolderExit) | ||||
| 
 | ||||
| ARRAY_DEF(idx_last_array, int32_t) | ||||
| 
 | ||||
| struct BrowserWorker { | ||||
|     FuriThread* thread; | ||||
| 
 | ||||
|     string_t filter_extension; | ||||
|     string_t path_next; | ||||
|     int32_t item_sel_idx; | ||||
|     uint32_t load_offset; | ||||
|     uint32_t load_count; | ||||
|     bool skip_assets; | ||||
|     idx_last_array_t idx_last; | ||||
| 
 | ||||
|     void* cb_ctx; | ||||
|     BrowserWorkerFolderOpenCallback folder_cb; | ||||
|     BrowserWorkerListLoadCallback list_load_cb; | ||||
|     BrowserWorkerListItemCallback list_item_cb; | ||||
|     BrowserWorkerLongLoadCallback long_load_cb; | ||||
| }; | ||||
| 
 | ||||
| static bool browser_path_is_file(string_t path) { | ||||
|     bool state = false; | ||||
|     FileInfo file_info; | ||||
|     Storage* storage = furi_record_open("storage"); | ||||
|     if(storage_common_stat(storage, string_get_cstr(path), &file_info) == FSE_OK) { | ||||
|         if((file_info.flags & FSF_DIRECTORY) == 0) { | ||||
|             state = true; | ||||
|         } | ||||
|     } | ||||
|     furi_record_close("storage"); | ||||
|     return state; | ||||
| } | ||||
| 
 | ||||
| static bool browser_path_trim(string_t path) { | ||||
|     bool is_root = false; | ||||
|     size_t filename_start = string_search_rchar(path, '/'); | ||||
|     string_left(path, filename_start); | ||||
|     if((string_empty_p(path)) || (filename_start == STRING_FAILURE)) { | ||||
|         string_set_str(path, BROWSER_ROOT); | ||||
|         is_root = true; | ||||
|     } | ||||
|     return is_root; | ||||
| } | ||||
| 
 | ||||
| static bool browser_filter_by_name(BrowserWorker* browser, string_t name, bool is_folder) { | ||||
|     if(is_folder) { | ||||
|         // Skip assets folders (if enabled)
 | ||||
|         if(browser->skip_assets) { | ||||
|             return ((string_cmp_str(name, ASSETS_DIR) == 0) ? (false) : (true)); | ||||
|         } else { | ||||
|             return true; | ||||
|         } | ||||
|     } else { | ||||
|         // Filter files by extension
 | ||||
|         if((string_empty_p(browser->filter_extension)) || | ||||
|            (string_cmp_str(browser->filter_extension, "*") == 0)) { | ||||
|             return true; | ||||
|         } | ||||
|         if(string_end_with_string_p(name, browser->filter_extension)) { | ||||
|             return true; | ||||
|         } | ||||
|     } | ||||
|     return false; | ||||
| } | ||||
| 
 | ||||
| static bool browser_folder_check_and_switch(string_t path) { | ||||
|     FileInfo file_info; | ||||
|     Storage* storage = furi_record_open("storage"); | ||||
|     bool is_root = false; | ||||
|     while(1) { | ||||
|         // Check if folder is existing and navigate back if not
 | ||||
|         if(storage_common_stat(storage, string_get_cstr(path), &file_info) == FSE_OK) { | ||||
|             if(file_info.flags & FSF_DIRECTORY) { | ||||
|                 break; | ||||
|             } | ||||
|         } | ||||
|         if(is_root) { | ||||
|             break; | ||||
|         } | ||||
|         is_root = browser_path_trim(path); | ||||
|     } | ||||
|     furi_record_close("storage"); | ||||
|     return is_root; | ||||
| } | ||||
| 
 | ||||
| static bool browser_folder_init( | ||||
|     BrowserWorker* browser, | ||||
|     string_t path, | ||||
|     string_t filename, | ||||
|     uint32_t* item_cnt, | ||||
|     int32_t* file_idx) { | ||||
|     bool state = false; | ||||
|     FileInfo file_info; | ||||
|     uint32_t total_files_cnt = 0; | ||||
| 
 | ||||
|     Storage* storage = furi_record_open("storage"); | ||||
|     File* directory = storage_file_alloc(storage); | ||||
| 
 | ||||
|     char name_temp[FILE_NAME_LEN_MAX]; | ||||
|     string_t name_str; | ||||
|     string_init(name_str); | ||||
| 
 | ||||
|     *item_cnt = 0; | ||||
|     *file_idx = -1; | ||||
| 
 | ||||
|     if(storage_dir_open(directory, string_get_cstr(path))) { | ||||
|         state = true; | ||||
|         while(1) { | ||||
|             if(!storage_dir_read(directory, &file_info, name_temp, FILE_NAME_LEN_MAX)) { | ||||
|                 break; | ||||
|             } | ||||
|             if((storage_file_get_error(directory) == FSE_OK) && (name_temp[0] != '\0')) { | ||||
|                 total_files_cnt++; | ||||
|                 string_set_str(name_str, name_temp); | ||||
|                 if(browser_filter_by_name(browser, name_str, (file_info.flags & FSF_DIRECTORY))) { | ||||
|                     if(!string_empty_p(filename)) { | ||||
|                         if(string_cmp(name_str, filename) == 0) { | ||||
|                             *file_idx = *item_cnt; | ||||
|                         } | ||||
|                     } | ||||
|                     (*item_cnt)++; | ||||
|                 } | ||||
|                 if(total_files_cnt == LONG_LOAD_THRESHOLD) { | ||||
|                     // There are too many files in folder and counting them will take some time - send callback to app
 | ||||
|                     if(browser->long_load_cb) { | ||||
|                         browser->long_load_cb(browser->cb_ctx); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     string_clear(name_str); | ||||
| 
 | ||||
|     storage_dir_close(directory); | ||||
|     storage_file_free(directory); | ||||
| 
 | ||||
|     furi_record_close("storage"); | ||||
| 
 | ||||
|     return state; | ||||
| } | ||||
| 
 | ||||
| static bool | ||||
|     browser_folder_load(BrowserWorker* browser, string_t path, uint32_t offset, uint32_t count) { | ||||
|     FileInfo file_info; | ||||
| 
 | ||||
|     Storage* storage = furi_record_open("storage"); | ||||
|     File* directory = storage_file_alloc(storage); | ||||
| 
 | ||||
|     char name_temp[FILE_NAME_LEN_MAX]; | ||||
|     string_t name_str; | ||||
|     string_init(name_str); | ||||
| 
 | ||||
|     uint32_t items_cnt = 0; | ||||
| 
 | ||||
|     do { | ||||
|         if(!storage_dir_open(directory, string_get_cstr(path))) { | ||||
|             break; | ||||
|         } | ||||
| 
 | ||||
|         items_cnt = 0; | ||||
|         while(items_cnt < offset) { | ||||
|             if(!storage_dir_read(directory, &file_info, name_temp, FILE_NAME_LEN_MAX)) { | ||||
|                 break; | ||||
|             } | ||||
|             if(storage_file_get_error(directory) == FSE_OK) { | ||||
|                 string_set_str(name_str, name_temp); | ||||
|                 if(browser_filter_by_name(browser, name_str, (file_info.flags & FSF_DIRECTORY))) { | ||||
|                     items_cnt++; | ||||
|                 } | ||||
|             } else { | ||||
|                 break; | ||||
|             } | ||||
|         } | ||||
|         if(items_cnt != offset) { | ||||
|             break; | ||||
|         } | ||||
| 
 | ||||
|         if(browser->list_load_cb) { | ||||
|             browser->list_load_cb(browser->cb_ctx, offset); | ||||
|         } | ||||
| 
 | ||||
|         items_cnt = 0; | ||||
|         while(items_cnt < count) { | ||||
|             if(!storage_dir_read(directory, &file_info, name_temp, FILE_NAME_LEN_MAX)) { | ||||
|                 break; | ||||
|             } | ||||
|             if(storage_file_get_error(directory) == FSE_OK) { | ||||
|                 string_set_str(name_str, name_temp); | ||||
|                 if(browser_filter_by_name(browser, name_str, (file_info.flags & FSF_DIRECTORY))) { | ||||
|                     string_printf(name_str, "%s/%s", string_get_cstr(path), name_temp); | ||||
|                     if(browser->list_item_cb) { | ||||
|                         browser->list_item_cb( | ||||
|                             browser->cb_ctx, name_str, (file_info.flags & FSF_DIRECTORY), false); | ||||
|                     } | ||||
|                     items_cnt++; | ||||
|                 } | ||||
|             } else { | ||||
|                 break; | ||||
|             } | ||||
|         } | ||||
|         if(browser->list_item_cb) { | ||||
|             browser->list_item_cb(browser->cb_ctx, NULL, false, true); | ||||
|         } | ||||
|     } while(0); | ||||
| 
 | ||||
|     string_clear(name_str); | ||||
| 
 | ||||
|     storage_dir_close(directory); | ||||
|     storage_file_free(directory); | ||||
| 
 | ||||
|     furi_record_close("storage"); | ||||
| 
 | ||||
|     return (items_cnt == count); | ||||
| } | ||||
| 
 | ||||
| static int32_t browser_worker(void* context) { | ||||
|     BrowserWorker* browser = (BrowserWorker*)context; | ||||
|     furi_assert(browser); | ||||
|     FURI_LOG_D(TAG, "Start"); | ||||
| 
 | ||||
|     uint32_t items_cnt = 0; | ||||
|     string_t path; | ||||
|     string_init_set_str(path, BROWSER_ROOT); | ||||
|     browser->item_sel_idx = -1; | ||||
| 
 | ||||
|     // If start path is a path to the file - try finding index of this file in a folder
 | ||||
|     string_t filename; | ||||
|     string_init(filename); | ||||
|     if(browser_path_is_file(browser->path_next)) { | ||||
|         path_extract_filename(browser->path_next, filename, false); | ||||
|     } | ||||
| 
 | ||||
|     osThreadFlagsSet(furi_thread_get_thread_id(browser->thread), WorkerEvtFolderEnter); | ||||
| 
 | ||||
|     while(1) { | ||||
|         uint32_t flags = osThreadFlagsWait(WORKER_FLAGS_ALL, osFlagsWaitAny, osWaitForever); | ||||
|         furi_assert((flags & osFlagsError) == 0); | ||||
| 
 | ||||
|         if(flags & WorkerEvtFolderEnter) { | ||||
|             string_set(path, browser->path_next); | ||||
|             bool is_root = browser_folder_check_and_switch(path); | ||||
| 
 | ||||
|             // Push previous selected item index to history array
 | ||||
|             idx_last_array_push_back(browser->idx_last, browser->item_sel_idx); | ||||
| 
 | ||||
|             int32_t file_idx = 0; | ||||
|             browser_folder_init(browser, path, filename, &items_cnt, &file_idx); | ||||
|             FURI_LOG_D( | ||||
|                 TAG, | ||||
|                 "Enter folder: %s items: %u idx: %d", | ||||
|                 string_get_cstr(path), | ||||
|                 items_cnt, | ||||
|                 file_idx); | ||||
|             if(browser->folder_cb) { | ||||
|                 browser->folder_cb(browser->cb_ctx, items_cnt, file_idx, is_root); | ||||
|             } | ||||
|             string_reset(filename); | ||||
|         } | ||||
| 
 | ||||
|         if(flags & WorkerEvtFolderExit) { | ||||
|             browser_path_trim(path); | ||||
|             bool is_root = browser_folder_check_and_switch(path); | ||||
| 
 | ||||
|             int32_t file_idx = 0; | ||||
|             browser_folder_init(browser, path, filename, &items_cnt, &file_idx); | ||||
|             if(idx_last_array_size(browser->idx_last) > 0) { | ||||
|                 // Pop previous selected item index from history array
 | ||||
|                 idx_last_array_pop_back(&file_idx, browser->idx_last); | ||||
|             } | ||||
|             FURI_LOG_D( | ||||
|                 TAG, "Exit to: %s items: %u idx: %d", string_get_cstr(path), items_cnt, file_idx); | ||||
|             if(browser->folder_cb) { | ||||
|                 browser->folder_cb(browser->cb_ctx, items_cnt, file_idx, is_root); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         if(flags & WorkerEvtLoad) { | ||||
|             FURI_LOG_D(TAG, "Load offset: %u cnt: %u", browser->load_offset, browser->load_count); | ||||
|             browser_folder_load(browser, path, browser->load_offset, browser->load_count); | ||||
|         } | ||||
| 
 | ||||
|         if(flags & WorkerEvtStop) { | ||||
|             break; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     string_clear(filename); | ||||
|     string_clear(path); | ||||
| 
 | ||||
|     FURI_LOG_D(TAG, "End"); | ||||
|     return 0; | ||||
| } | ||||
| 
 | ||||
| BrowserWorker* file_browser_worker_alloc(string_t path, const char* filter_ext, bool skip_assets) { | ||||
|     BrowserWorker* browser = malloc(sizeof(BrowserWorker)); | ||||
| 
 | ||||
|     idx_last_array_init(browser->idx_last); | ||||
| 
 | ||||
|     string_init_set_str(browser->filter_extension, filter_ext); | ||||
|     browser->skip_assets = skip_assets; | ||||
|     string_init_set(browser->path_next, path); | ||||
| 
 | ||||
|     browser->thread = furi_thread_alloc(); | ||||
|     furi_thread_set_name(browser->thread, "BrowserWorker"); | ||||
|     furi_thread_set_stack_size(browser->thread, 2048); | ||||
|     furi_thread_set_context(browser->thread, browser); | ||||
|     furi_thread_set_callback(browser->thread, browser_worker); | ||||
|     furi_thread_start(browser->thread); | ||||
| 
 | ||||
|     return browser; | ||||
| } | ||||
| 
 | ||||
| void file_browser_worker_free(BrowserWorker* browser) { | ||||
|     furi_assert(browser); | ||||
| 
 | ||||
|     osThreadFlagsSet(furi_thread_get_thread_id(browser->thread), WorkerEvtStop); | ||||
|     furi_thread_join(browser->thread); | ||||
|     furi_thread_free(browser->thread); | ||||
| 
 | ||||
|     string_clear(browser->filter_extension); | ||||
|     string_clear(browser->path_next); | ||||
| 
 | ||||
|     idx_last_array_clear(browser->idx_last); | ||||
| 
 | ||||
|     free(browser); | ||||
| } | ||||
| 
 | ||||
| void file_browser_worker_set_callback_context(BrowserWorker* browser, void* context) { | ||||
|     furi_assert(browser); | ||||
|     browser->cb_ctx = context; | ||||
| } | ||||
| 
 | ||||
| void file_browser_worker_set_folder_callback( | ||||
|     BrowserWorker* browser, | ||||
|     BrowserWorkerFolderOpenCallback cb) { | ||||
|     furi_assert(browser); | ||||
|     browser->folder_cb = cb; | ||||
| } | ||||
| 
 | ||||
| void file_browser_worker_set_list_callback( | ||||
|     BrowserWorker* browser, | ||||
|     BrowserWorkerListLoadCallback cb) { | ||||
|     furi_assert(browser); | ||||
|     browser->list_load_cb = cb; | ||||
| } | ||||
| 
 | ||||
| void file_browser_worker_set_item_callback( | ||||
|     BrowserWorker* browser, | ||||
|     BrowserWorkerListItemCallback cb) { | ||||
|     furi_assert(browser); | ||||
|     browser->list_item_cb = cb; | ||||
| } | ||||
| 
 | ||||
| void file_browser_worker_set_long_load_callback( | ||||
|     BrowserWorker* browser, | ||||
|     BrowserWorkerLongLoadCallback cb) { | ||||
|     furi_assert(browser); | ||||
|     browser->long_load_cb = cb; | ||||
| } | ||||
| 
 | ||||
| void file_browser_worker_folder_enter(BrowserWorker* browser, string_t path, int32_t item_idx) { | ||||
|     furi_assert(browser); | ||||
|     string_set(browser->path_next, path); | ||||
|     browser->item_sel_idx = item_idx; | ||||
|     osThreadFlagsSet(furi_thread_get_thread_id(browser->thread), WorkerEvtFolderEnter); | ||||
| } | ||||
| 
 | ||||
| void file_browser_worker_folder_exit(BrowserWorker* browser) { | ||||
|     furi_assert(browser); | ||||
|     osThreadFlagsSet(furi_thread_get_thread_id(browser->thread), WorkerEvtFolderExit); | ||||
| } | ||||
| 
 | ||||
| void file_browser_worker_load(BrowserWorker* browser, uint32_t offset, uint32_t count) { | ||||
|     furi_assert(browser); | ||||
|     browser->load_offset = offset; | ||||
|     browser->load_count = count; | ||||
|     osThreadFlagsSet(furi_thread_get_thread_id(browser->thread), WorkerEvtLoad); | ||||
| } | ||||
							
								
								
									
										55
									
								
								applications/gui/modules/file_browser_worker.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								applications/gui/modules/file_browser_worker.h
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,55 @@ | ||||
| #pragma once | ||||
| 
 | ||||
| #include "m-string.h" | ||||
| #include <gui/view.h> | ||||
| #include <stdint.h> | ||||
| 
 | ||||
| #ifdef __cplusplus | ||||
| extern "C" { | ||||
| #endif | ||||
| 
 | ||||
| typedef struct BrowserWorker BrowserWorker; | ||||
| typedef void (*BrowserWorkerFolderOpenCallback)( | ||||
|     void* context, | ||||
|     uint32_t item_cnt, | ||||
|     int32_t file_idx, | ||||
|     bool is_root); | ||||
| typedef void (*BrowserWorkerListLoadCallback)(void* context, uint32_t list_load_offset); | ||||
| typedef void (*BrowserWorkerListItemCallback)( | ||||
|     void* context, | ||||
|     string_t item_path, | ||||
|     bool is_folder, | ||||
|     bool is_last); | ||||
| typedef void (*BrowserWorkerLongLoadCallback)(void* context); | ||||
| 
 | ||||
| BrowserWorker* file_browser_worker_alloc(string_t path, const char* filter_ext, bool skip_assets); | ||||
| 
 | ||||
| void file_browser_worker_free(BrowserWorker* browser); | ||||
| 
 | ||||
| void file_browser_worker_set_callback_context(BrowserWorker* browser, void* context); | ||||
| 
 | ||||
| void file_browser_worker_set_folder_callback( | ||||
|     BrowserWorker* browser, | ||||
|     BrowserWorkerFolderOpenCallback cb); | ||||
| 
 | ||||
| void file_browser_worker_set_list_callback( | ||||
|     BrowserWorker* browser, | ||||
|     BrowserWorkerListLoadCallback cb); | ||||
| 
 | ||||
| void file_browser_worker_set_item_callback( | ||||
|     BrowserWorker* browser, | ||||
|     BrowserWorkerListItemCallback cb); | ||||
| 
 | ||||
| void file_browser_worker_set_long_load_callback( | ||||
|     BrowserWorker* browser, | ||||
|     BrowserWorkerLongLoadCallback cb); | ||||
| 
 | ||||
| void file_browser_worker_folder_enter(BrowserWorker* browser, string_t path, int32_t item_idx); | ||||
| 
 | ||||
| void file_browser_worker_folder_exit(BrowserWorker* browser); | ||||
| 
 | ||||
| void file_browser_worker_load(BrowserWorker* browser, uint32_t offset, uint32_t count); | ||||
| 
 | ||||
| #ifdef __cplusplus | ||||
| } | ||||
| #endif | ||||
| @ -1,475 +0,0 @@ | ||||
| #include "file_select.h" | ||||
| #include <gui/elements.h> | ||||
| #include <m-string.h> | ||||
| #include <storage/storage.h> | ||||
| 
 | ||||
| #define FILENAME_COUNT 4 | ||||
| 
 | ||||
| struct FileSelect { | ||||
|     // public
 | ||||
|     View* view; | ||||
|     Storage* fs_api; | ||||
|     const char* path; | ||||
|     const char* extension; | ||||
| 
 | ||||
|     bool init_completed; | ||||
| 
 | ||||
|     FileSelectCallback callback; | ||||
|     void* context; | ||||
| 
 | ||||
|     char* buffer; | ||||
|     uint8_t buffer_size; | ||||
| }; | ||||
| 
 | ||||
| typedef struct { | ||||
|     string_t filename[FILENAME_COUNT]; | ||||
|     uint8_t position; | ||||
| 
 | ||||
|     uint16_t first_file_index; | ||||
|     uint16_t file_count; | ||||
| 
 | ||||
| } FileSelectModel; | ||||
| 
 | ||||
| bool file_select_fill_strings(FileSelect* file_select); | ||||
| bool file_select_fill_count(FileSelect* file_select); | ||||
| static bool file_select_init_inner(FileSelect* file_select); | ||||
| 
 | ||||
| static void file_select_draw_callback(Canvas* canvas, void* _model) { | ||||
|     FileSelectModel* model = _model; | ||||
| 
 | ||||
|     string_t string_buff; | ||||
|     const uint8_t item_height = 16; | ||||
|     const uint8_t item_width = 123; | ||||
|     const uint8_t text_max_width = 115; | ||||
| 
 | ||||
|     canvas_clear(canvas); | ||||
|     canvas_set_font(canvas, FontSecondary); | ||||
| 
 | ||||
|     if(model->file_count) { | ||||
|         for(uint8_t i = 0; i < MIN(FILENAME_COUNT, model->file_count); i++) { | ||||
|             if(i == model->position) { | ||||
|                 canvas_set_color(canvas, ColorBlack); | ||||
|                 canvas_draw_box(canvas, 0, (i * item_height) + 1, item_width, item_height - 2); | ||||
| 
 | ||||
|                 canvas_set_color(canvas, ColorWhite); | ||||
|                 canvas_draw_dot(canvas, 0, (i * item_height) + 1); | ||||
|                 canvas_draw_dot(canvas, 0, (i * item_height) + item_height - 2); | ||||
|                 canvas_draw_dot(canvas, item_width - 1, (i * item_height) + 1); | ||||
|                 canvas_draw_dot(canvas, item_width - 1, (i * item_height) + item_height - 2); | ||||
|             } else { | ||||
|                 canvas_set_color(canvas, ColorBlack); | ||||
|             } | ||||
| 
 | ||||
|             string_init_set(string_buff, model->filename[i]); | ||||
|             elements_string_fit_width(canvas, string_buff, text_max_width); | ||||
|             canvas_draw_str( | ||||
|                 canvas, 6, (i * item_height) + item_height - 4, string_get_cstr(string_buff)); | ||||
| 
 | ||||
|             string_clear(string_buff); | ||||
|         } | ||||
|     } else { | ||||
|         canvas_draw_str(canvas, 6, item_height, "Empty folder"); | ||||
|     } | ||||
|     elements_scrollbar(canvas, model->first_file_index + model->position, model->file_count); | ||||
| } | ||||
| 
 | ||||
| static bool file_select_input_callback(InputEvent* event, void* context) { | ||||
|     FileSelect* file_select = (FileSelect*)context; | ||||
|     bool consumed = false; | ||||
| 
 | ||||
|     if((event->type == InputTypeShort) | (event->type == InputTypeRepeat)) { | ||||
|         if(!file_select->init_completed) { | ||||
|             if(!file_select_init_inner(file_select)) { | ||||
|                 file_select->callback(false, file_select->context); | ||||
|             } | ||||
|         } else if(event->key == InputKeyUp) { | ||||
|             with_view_model( | ||||
|                 file_select->view, (FileSelectModel * model) { | ||||
|                     if(model->position == 0) { | ||||
|                         if(model->first_file_index == 0) { | ||||
|                             // wrap
 | ||||
|                             int16_t max_first_file_index = model->file_count - FILENAME_COUNT; | ||||
|                             model->position = MIN(FILENAME_COUNT - 1, model->file_count - 1); | ||||
|                             model->first_file_index = | ||||
|                                 max_first_file_index < 0 ? 0 : max_first_file_index; | ||||
|                         } else { | ||||
|                             model->first_file_index--; | ||||
|                         } | ||||
|                     } else if(model->position == 1) { | ||||
|                         if(model->first_file_index == 0) { | ||||
|                             model->position--; | ||||
|                         } else { | ||||
|                             model->first_file_index--; | ||||
|                         } | ||||
|                     } else { | ||||
|                         model->position--; | ||||
|                     } | ||||
|                     return true; | ||||
|                 }); | ||||
|             consumed = true; | ||||
|             if(!file_select_fill_strings(file_select)) { | ||||
|                 file_select->callback(false, file_select->context); | ||||
|             } | ||||
|         } else if(event->key == InputKeyDown) { | ||||
|             with_view_model( | ||||
|                 file_select->view, (FileSelectModel * model) { | ||||
|                     uint16_t max_first_file_index = model->file_count > FILENAME_COUNT ? | ||||
|                                                         model->file_count - FILENAME_COUNT : | ||||
|                                                         0; | ||||
| 
 | ||||
|                     if(model->position >= MIN(FILENAME_COUNT - 1, model->file_count - 1)) { | ||||
|                         if(model->first_file_index >= max_first_file_index) { | ||||
|                             // wrap
 | ||||
|                             model->position = 0; | ||||
|                             model->first_file_index = 0; | ||||
|                         } else { | ||||
|                             model->first_file_index++; | ||||
|                         } | ||||
|                     } else if(model->position >= (FILENAME_COUNT - 2)) { | ||||
|                         if(model->first_file_index >= max_first_file_index) { | ||||
|                             model->position++; | ||||
|                         } else { | ||||
|                             model->first_file_index++; | ||||
|                         } | ||||
|                     } else { | ||||
|                         model->position++; | ||||
|                     } | ||||
|                     return true; | ||||
|                 }); | ||||
|             consumed = true; | ||||
|             if(!file_select_fill_strings(file_select)) { | ||||
|                 file_select->callback(false, file_select->context); | ||||
|             } | ||||
|         } else if(event->key == InputKeyOk) { | ||||
|             if(file_select->callback != NULL) { | ||||
|                 size_t files = 0; | ||||
|                 if(file_select->buffer) { | ||||
|                     with_view_model( | ||||
|                         file_select->view, (FileSelectModel * model) { | ||||
|                             files = model->file_count; | ||||
|                             strlcpy( | ||||
|                                 file_select->buffer, | ||||
|                                 string_get_cstr(model->filename[model->position]), | ||||
|                                 file_select->buffer_size); | ||||
| 
 | ||||
|                             return false; | ||||
|                         }); | ||||
|                 }; | ||||
|                 if(files > 0) { | ||||
|                     file_select->callback(true, file_select->context); | ||||
|                 } | ||||
|             } | ||||
|             consumed = true; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     return consumed; | ||||
| } | ||||
| 
 | ||||
| static bool file_select_init_inner(FileSelect* file_select) { | ||||
|     bool result = false; | ||||
|     if(file_select->path && file_select->extension && file_select->fs_api) { | ||||
|         if(file_select_fill_count(file_select)) { | ||||
|             if(file_select_fill_strings(file_select)) { | ||||
|                 file_select->init_completed = true; | ||||
|                 result = true; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     return result; | ||||
| } | ||||
| 
 | ||||
| FileSelect* file_select_alloc() { | ||||
|     FileSelect* file_select = malloc(sizeof(FileSelect)); | ||||
|     file_select->view = view_alloc(); | ||||
|     file_select->fs_api = furi_record_open("storage"); | ||||
| 
 | ||||
|     view_set_context(file_select->view, file_select); | ||||
|     view_allocate_model(file_select->view, ViewModelTypeLockFree, sizeof(FileSelectModel)); | ||||
|     view_set_draw_callback(file_select->view, file_select_draw_callback); | ||||
|     view_set_input_callback(file_select->view, file_select_input_callback); | ||||
| 
 | ||||
|     with_view_model( | ||||
|         file_select->view, (FileSelectModel * model) { | ||||
|             for(uint8_t i = 0; i < FILENAME_COUNT; i++) { | ||||
|                 string_init(model->filename[i]); | ||||
|             } | ||||
| 
 | ||||
|             model->first_file_index = 0; | ||||
|             model->file_count = 0; | ||||
|             return false; | ||||
|         }); | ||||
| 
 | ||||
|     return file_select; | ||||
| } | ||||
| 
 | ||||
| void file_select_free(FileSelect* file_select) { | ||||
|     furi_assert(file_select); | ||||
|     with_view_model( | ||||
|         file_select->view, (FileSelectModel * model) { | ||||
|             for(uint8_t i = 0; i < FILENAME_COUNT; i++) { | ||||
|                 string_clear(model->filename[i]); | ||||
|             } | ||||
|             return false; | ||||
|         }); | ||||
|     view_free(file_select->view); | ||||
|     free(file_select); | ||||
|     furi_record_close("storage"); | ||||
| } | ||||
| 
 | ||||
| View* file_select_get_view(FileSelect* file_select) { | ||||
|     furi_assert(file_select); | ||||
|     return file_select->view; | ||||
| } | ||||
| 
 | ||||
| void file_select_set_callback(FileSelect* file_select, FileSelectCallback callback, void* context) { | ||||
|     file_select->context = context; | ||||
|     file_select->callback = callback; | ||||
| } | ||||
| 
 | ||||
| void file_select_set_filter(FileSelect* file_select, const char* path, const char* extension) { | ||||
|     furi_assert(file_select); | ||||
|     file_select->path = path; | ||||
|     file_select->extension = extension; | ||||
| } | ||||
| 
 | ||||
| void file_select_set_result_buffer(FileSelect* file_select, char* buffer, uint8_t buffer_size) { | ||||
|     file_select->buffer = buffer; | ||||
|     file_select->buffer_size = buffer_size; | ||||
| 
 | ||||
|     if(file_select->buffer) { | ||||
|         strlcpy(file_select->buffer, "", file_select->buffer_size); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| bool file_select_init(FileSelect* file_select) { | ||||
|     if(!file_select_init_inner(file_select)) { | ||||
|         file_select->callback(false, file_select->context); | ||||
|         return false; | ||||
|     } else { | ||||
|         return true; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| static bool filter_file(FileSelect* file_select, FileInfo* file_info, char* name) { | ||||
|     bool result = false; | ||||
| 
 | ||||
|     if(!(file_info->flags & FSF_DIRECTORY)) { | ||||
|         if(strcmp(file_select->extension, "*") == 0) { | ||||
|             result = true; | ||||
|         } else if(strstr(name, file_select->extension) != NULL) { | ||||
|             result = true; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     return result; | ||||
| } | ||||
| 
 | ||||
| bool file_select_fill_strings(FileSelect* file_select) { | ||||
|     furi_assert(file_select); | ||||
|     furi_assert(file_select->fs_api); | ||||
|     furi_assert(file_select->path); | ||||
|     furi_assert(file_select->extension); | ||||
| 
 | ||||
|     FileInfo file_info; | ||||
|     File* directory = storage_file_alloc(file_select->fs_api); | ||||
| 
 | ||||
|     uint8_t string_counter = 0; | ||||
|     uint16_t file_counter = 0; | ||||
|     const uint8_t name_length = 100; | ||||
|     char* name = malloc(name_length); | ||||
|     uint16_t first_file_index = 0; | ||||
| 
 | ||||
|     with_view_model( | ||||
|         file_select->view, (FileSelectModel * model) { | ||||
|             first_file_index = model->first_file_index; | ||||
|             return false; | ||||
|         }); | ||||
| 
 | ||||
|     if(!storage_dir_open(directory, file_select->path)) { | ||||
|         storage_dir_close(directory); | ||||
|         storage_file_free(directory); | ||||
|         free(name); | ||||
|         return true; | ||||
|     } | ||||
| 
 | ||||
|     while(1) { | ||||
|         if(!storage_dir_read(directory, &file_info, name, name_length)) { | ||||
|             break; | ||||
|         } | ||||
| 
 | ||||
|         if(storage_file_get_error(directory) == FSE_OK) { | ||||
|             if(filter_file(file_select, &file_info, name)) { | ||||
|                 if(file_counter >= first_file_index) { | ||||
|                     with_view_model( | ||||
|                         file_select->view, (FileSelectModel * model) { | ||||
|                             string_set_str(model->filename[string_counter], name); | ||||
| 
 | ||||
|                             if(strcmp(file_select->extension, "*") != 0) { | ||||
|                                 string_replace_all_str( | ||||
|                                     model->filename[string_counter], file_select->extension, ""); | ||||
|                             } | ||||
| 
 | ||||
|                             return true; | ||||
|                         }); | ||||
|                     string_counter++; | ||||
| 
 | ||||
|                     if(string_counter >= FILENAME_COUNT) { | ||||
|                         break; | ||||
|                     } | ||||
|                 } | ||||
|                 file_counter++; | ||||
|             } | ||||
|         } else { | ||||
|             storage_dir_close(directory); | ||||
|             storage_file_free(directory); | ||||
|             free(name); | ||||
|             return false; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     storage_dir_close(directory); | ||||
|     storage_file_free(directory); | ||||
|     free(name); | ||||
|     return true; | ||||
| } | ||||
| 
 | ||||
| bool file_select_fill_count(FileSelect* file_select) { | ||||
|     furi_assert(file_select); | ||||
|     furi_assert(file_select->fs_api); | ||||
|     furi_assert(file_select->path); | ||||
|     furi_assert(file_select->extension); | ||||
| 
 | ||||
|     FileInfo file_info; | ||||
|     File* directory = storage_file_alloc(file_select->fs_api); | ||||
| 
 | ||||
|     uint16_t file_counter = 0; | ||||
|     const uint8_t name_length = 100; | ||||
|     char* name = malloc(name_length); | ||||
| 
 | ||||
|     if(!storage_dir_open(directory, file_select->path)) { | ||||
|         storage_dir_close(directory); | ||||
|         storage_file_free(directory); | ||||
|         free(name); | ||||
|         return true; | ||||
|     } | ||||
| 
 | ||||
|     while(1) { | ||||
|         if(!storage_dir_read(directory, &file_info, name, name_length)) { | ||||
|             break; | ||||
|         } | ||||
| 
 | ||||
|         if(storage_file_get_error(directory) == FSE_OK) { | ||||
|             if(filter_file(file_select, &file_info, name)) { | ||||
|                 file_counter++; | ||||
|             } | ||||
|         } else { | ||||
|             storage_dir_close(directory); | ||||
|             storage_file_free(directory); | ||||
|             free(name); | ||||
|             return false; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     with_view_model( | ||||
|         file_select->view, (FileSelectModel * model) { | ||||
|             model->file_count = file_counter; | ||||
|             return false; | ||||
|         }); | ||||
| 
 | ||||
|     storage_dir_close(directory); | ||||
|     storage_file_free(directory); | ||||
|     free(name); | ||||
|     return true; | ||||
| } | ||||
| 
 | ||||
| void file_select_set_selected_file_internal(FileSelect* file_select, const char* filename) { | ||||
|     furi_assert(file_select); | ||||
|     furi_assert(filename); | ||||
|     furi_assert(file_select->fs_api); | ||||
|     furi_assert(file_select->path); | ||||
|     furi_assert(file_select->extension); | ||||
| 
 | ||||
|     if(strlen(filename) == 0) return; | ||||
| 
 | ||||
|     FileInfo file_info; | ||||
|     File* directory = storage_file_alloc(file_select->fs_api); | ||||
| 
 | ||||
|     const uint8_t name_length = 100; | ||||
|     char* name = malloc(name_length); | ||||
|     uint16_t file_position = 0; | ||||
|     bool file_found = false; | ||||
| 
 | ||||
|     string_t filename_str; | ||||
|     string_init_set_str(filename_str, filename); | ||||
|     if(strcmp(file_select->extension, "*") != 0) { | ||||
|         string_cat_str(filename_str, file_select->extension); | ||||
|     } | ||||
| 
 | ||||
|     if(!storage_dir_open(directory, file_select->path)) { | ||||
|         string_clear(filename_str); | ||||
|         storage_dir_close(directory); | ||||
|         storage_file_free(directory); | ||||
|         free(name); | ||||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     while(1) { | ||||
|         if(!storage_dir_read(directory, &file_info, name, name_length)) { | ||||
|             break; | ||||
|         } | ||||
| 
 | ||||
|         if(storage_file_get_error(directory) == FSE_OK) { | ||||
|             if(filter_file(file_select, &file_info, name)) { | ||||
|                 if(strcmp(string_get_cstr(filename_str), name) == 0) { | ||||
|                     file_found = true; | ||||
|                     break; | ||||
|                 } | ||||
| 
 | ||||
|                 file_position++; | ||||
|             } | ||||
|         } else { | ||||
|             string_clear(filename_str); | ||||
|             storage_dir_close(directory); | ||||
|             storage_file_free(directory); | ||||
|             free(name); | ||||
|             return; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     if(file_found) { | ||||
|         with_view_model( | ||||
|             file_select->view, (FileSelectModel * model) { | ||||
|                 uint16_t max_first_file_index = | ||||
|                     model->file_count > FILENAME_COUNT ? model->file_count - FILENAME_COUNT : 0; | ||||
| 
 | ||||
|                 model->first_file_index = file_position; | ||||
| 
 | ||||
|                 if(model->first_file_index > 0) { | ||||
|                     model->first_file_index -= 1; | ||||
|                 } | ||||
| 
 | ||||
|                 if(model->first_file_index >= max_first_file_index) { | ||||
|                     model->first_file_index = max_first_file_index; | ||||
|                 } | ||||
| 
 | ||||
|                 model->position = file_position - model->first_file_index; | ||||
| 
 | ||||
|                 return true; | ||||
|             }); | ||||
|     } | ||||
| 
 | ||||
|     string_clear(filename_str); | ||||
|     storage_dir_close(directory); | ||||
|     storage_file_free(directory); | ||||
|     free(name); | ||||
| } | ||||
| 
 | ||||
| void file_select_set_selected_file(FileSelect* file_select, const char* filename) { | ||||
|     file_select_set_selected_file_internal(file_select, filename); | ||||
| 
 | ||||
|     if(!file_select_fill_strings(file_select)) { | ||||
|         file_select->callback(false, file_select->context); | ||||
|     } | ||||
| } | ||||
| @ -1,31 +0,0 @@ | ||||
| /**
 | ||||
|  * @file file_select.h | ||||
|  * GUI: FileSelect view module API | ||||
|  */ | ||||
| 
 | ||||
| #pragma once | ||||
| 
 | ||||
| #include <gui/view.h> | ||||
| 
 | ||||
| #ifdef __cplusplus | ||||
| extern "C" { | ||||
| #endif | ||||
| 
 | ||||
| typedef struct FileSelect FileSelect; | ||||
| 
 | ||||
| typedef void (*FileSelectCallback)(bool result, void* context); | ||||
| 
 | ||||
| FileSelect* file_select_alloc(); | ||||
| 
 | ||||
| void file_select_free(FileSelect* file_select); | ||||
| View* file_select_get_view(FileSelect* file_select); | ||||
| 
 | ||||
| void file_select_set_callback(FileSelect* file_select, FileSelectCallback callback, void* context); | ||||
| void file_select_set_filter(FileSelect* file_select, const char* path, const char* extension); | ||||
| void file_select_set_result_buffer(FileSelect* file_select, char* buffer, uint8_t buffer_size); | ||||
| bool file_select_init(FileSelect* file_select); | ||||
| void file_select_set_selected_file(FileSelect* file_select, const char* filename); | ||||
| 
 | ||||
| #ifdef __cplusplus | ||||
| } | ||||
| #endif | ||||
| @ -3,7 +3,7 @@ | ||||
| #include "applications/storage/storage.h" | ||||
| 
 | ||||
| struct ValidatorIsFile { | ||||
|     const char* app_path_folder; | ||||
|     char* app_path_folder; | ||||
|     const char* app_extension; | ||||
|     char* current_name; | ||||
| }; | ||||
| @ -40,7 +40,7 @@ ValidatorIsFile* validator_is_file_alloc_init( | ||||
|     const char* current_name) { | ||||
|     ValidatorIsFile* instance = malloc(sizeof(ValidatorIsFile)); | ||||
| 
 | ||||
|     instance->app_path_folder = app_path_folder; | ||||
|     instance->app_path_folder = strdup(app_path_folder); | ||||
|     instance->app_extension = app_extension; | ||||
|     instance->current_name = strdup(current_name); | ||||
| 
 | ||||
| @ -49,6 +49,7 @@ ValidatorIsFile* validator_is_file_alloc_init( | ||||
| 
 | ||||
| void validator_is_file_free(ValidatorIsFile* instance) { | ||||
|     furi_assert(instance); | ||||
|     free(instance->app_path_folder); | ||||
|     free(instance->current_name); | ||||
|     free(instance); | ||||
| } | ||||
|  | ||||
| @ -1,7 +1,8 @@ | ||||
| #include "ibutton.h" | ||||
| #include "assets_icons.h" | ||||
| #include "ibutton_i.h" | ||||
| #include "ibutton/scenes/ibutton_scene.h" | ||||
| 
 | ||||
| #include "m-string.h" | ||||
| #include <toolbox/path.h> | ||||
| #include <flipper_format/flipper_format.h> | ||||
| 
 | ||||
| @ -85,6 +86,8 @@ void ibutton_tick_event_callback(void* context) { | ||||
| iButton* ibutton_alloc() { | ||||
|     iButton* ibutton = malloc(sizeof(iButton)); | ||||
| 
 | ||||
|     string_init(ibutton->file_path); | ||||
| 
 | ||||
|     ibutton->scene_manager = scene_manager_alloc(&ibutton_scene_handlers, ibutton); | ||||
| 
 | ||||
|     ibutton->view_dispatcher = view_dispatcher_alloc(); | ||||
| @ -176,46 +179,25 @@ void ibutton_free(iButton* ibutton) { | ||||
|     ibutton_worker_free(ibutton->key_worker); | ||||
|     ibutton_key_free(ibutton->key); | ||||
| 
 | ||||
|     string_clear(ibutton->file_path); | ||||
| 
 | ||||
|     free(ibutton); | ||||
| } | ||||
| 
 | ||||
| bool ibutton_file_select(iButton* ibutton) { | ||||
|     bool success = dialog_file_select_show( | ||||
|     bool success = dialog_file_browser_show( | ||||
|         ibutton->dialogs, | ||||
|         IBUTTON_APP_FOLDER, | ||||
|         ibutton->file_path, | ||||
|         ibutton->file_path, | ||||
|         IBUTTON_APP_EXTENSION, | ||||
|         ibutton->file_name, | ||||
|         IBUTTON_FILE_NAME_SIZE, | ||||
|         ibutton_key_get_name_p(ibutton->key)); | ||||
|         true, | ||||
|         &I_ibutt_10px, | ||||
|         true); | ||||
| 
 | ||||
|     if(success) { | ||||
|         string_t key_str; | ||||
|         string_init_printf( | ||||
|             key_str, "%s/%s%s", IBUTTON_APP_FOLDER, ibutton->file_name, IBUTTON_APP_EXTENSION); | ||||
|         success = ibutton_load_key_data(ibutton, key_str); | ||||
| 
 | ||||
|         if(success) { | ||||
|             ibutton_key_set_name(ibutton->key, ibutton->file_name); | ||||
|         success = ibutton_load_key_data(ibutton, ibutton->file_path); | ||||
|     } | ||||
| 
 | ||||
|         string_clear(key_str); | ||||
|     } | ||||
| 
 | ||||
|     return success; | ||||
| } | ||||
| 
 | ||||
| bool ibutton_load_key(iButton* ibutton, const char* key_name) { | ||||
|     string_t key_path; | ||||
|     string_init_set_str(key_path, key_name); | ||||
| 
 | ||||
|     const bool success = ibutton_load_key_data(ibutton, key_path); | ||||
| 
 | ||||
|     if(success) { | ||||
|         path_extract_filename_no_ext(key_name, key_path); | ||||
|         ibutton_key_set_name(ibutton->key, string_get_cstr(key_path)); | ||||
|     } | ||||
| 
 | ||||
|     string_clear(key_path); | ||||
|     return success; | ||||
| } | ||||
| 
 | ||||
| @ -226,27 +208,23 @@ bool ibutton_save_key(iButton* ibutton, const char* key_name) { | ||||
|     FlipperFormat* file = flipper_format_file_alloc(ibutton->storage); | ||||
|     iButtonKey* key = ibutton->key; | ||||
| 
 | ||||
|     string_t key_file_name; | ||||
|     bool result = false; | ||||
|     string_init(key_file_name); | ||||
| 
 | ||||
|     do { | ||||
|         // First remove key if it was saved (we rename the key)
 | ||||
|         if(!ibutton_delete_key(ibutton)) break; | ||||
|         // Check if we has old key
 | ||||
|         if(string_end_with_str_p(ibutton->file_path, IBUTTON_APP_EXTENSION)) { | ||||
|             // First remove old key
 | ||||
|             ibutton_delete_key(ibutton); | ||||
| 
 | ||||
|         // Save the key
 | ||||
|         ibutton_key_set_name(key, key_name); | ||||
|             // Remove old key name from path
 | ||||
|             size_t filename_start = string_search_rchar(ibutton->file_path, '/'); | ||||
|             string_left(ibutton->file_path, filename_start); | ||||
|         } | ||||
| 
 | ||||
|         // Set full file name, for new key
 | ||||
|         string_printf( | ||||
|             key_file_name, | ||||
|             "%s/%s%s", | ||||
|             IBUTTON_APP_FOLDER, | ||||
|             ibutton_key_get_name_p(key), | ||||
|             IBUTTON_APP_EXTENSION); | ||||
|         string_cat_printf(ibutton->file_path, "/%s%s", key_name, IBUTTON_APP_EXTENSION); | ||||
| 
 | ||||
|         // Open file for write
 | ||||
|         if(!flipper_format_file_open_always(file, string_get_cstr(key_file_name))) break; | ||||
|         if(!flipper_format_file_open_always(file, string_get_cstr(ibutton->file_path))) break; | ||||
| 
 | ||||
|         // Write header
 | ||||
|         if(!flipper_format_write_header_cstr(file, IBUTTON_APP_FILE_TYPE, 1)) break; | ||||
| @ -271,8 +249,6 @@ bool ibutton_save_key(iButton* ibutton, const char* key_name) { | ||||
| 
 | ||||
|     flipper_format_free(file); | ||||
| 
 | ||||
|     string_clear(key_file_name); | ||||
| 
 | ||||
|     if(!result) { | ||||
|         dialog_message_show_storage_error(ibutton->dialogs, "Cannot save\nkey file"); | ||||
|     } | ||||
| @ -281,17 +257,8 @@ bool ibutton_save_key(iButton* ibutton, const char* key_name) { | ||||
| } | ||||
| 
 | ||||
| bool ibutton_delete_key(iButton* ibutton) { | ||||
|     string_t file_name; | ||||
|     bool result = false; | ||||
| 
 | ||||
|     string_init_printf( | ||||
|         file_name, | ||||
|         "%s/%s%s", | ||||
|         IBUTTON_APP_FOLDER, | ||||
|         ibutton_key_get_name_p(ibutton->key), | ||||
|         IBUTTON_APP_EXTENSION); | ||||
|     result = storage_simply_remove(ibutton->storage, string_get_cstr(file_name)); | ||||
|     string_clear(file_name); | ||||
|     result = storage_simply_remove(ibutton->storage, string_get_cstr(ibutton->file_path)); | ||||
| 
 | ||||
|     return result; | ||||
| } | ||||
| @ -335,8 +302,17 @@ int32_t ibutton_app(void* p) { | ||||
| 
 | ||||
|     ibutton_make_app_folder(ibutton); | ||||
| 
 | ||||
|     if(p && ibutton_load_key(ibutton, (const char*)p)) { | ||||
|     bool key_loaded = false; | ||||
| 
 | ||||
|     if(p) { | ||||
|         string_set_str(ibutton->file_path, (const char*)p); | ||||
|         if(ibutton_load_key_data(ibutton, ibutton->file_path)) { | ||||
|             key_loaded = true; | ||||
|             // TODO: Display an error if the key from p could not be loaded
 | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     if(key_loaded) { | ||||
|         scene_manager_next_scene(ibutton->scene_manager, iButtonSceneEmulate); | ||||
|     } else { | ||||
|         scene_manager_next_scene(ibutton->scene_manager, iButtonSceneStart); | ||||
|  | ||||
| @ -41,7 +41,7 @@ struct iButton { | ||||
|     iButtonWorker* key_worker; | ||||
|     iButtonKey* key; | ||||
| 
 | ||||
|     char file_name[IBUTTON_FILE_NAME_SIZE]; | ||||
|     string_t file_path; | ||||
|     char text_store[IBUTTON_TEXT_STORE_SIZE + 1]; | ||||
| 
 | ||||
|     Submenu* submenu; | ||||
| @ -74,7 +74,6 @@ typedef enum { | ||||
| } iButtonNotificationMessage; | ||||
| 
 | ||||
| bool ibutton_file_select(iButton* ibutton); | ||||
| bool ibutton_load_key(iButton* ibutton, const char* key_name); | ||||
| bool ibutton_save_key(iButton* ibutton, const char* key_name); | ||||
| bool ibutton_delete_key(iButton* ibutton); | ||||
| void ibutton_text_store_set(iButton* ibutton, const char* text, ...); | ||||
|  | ||||
| @ -1,4 +1,5 @@ | ||||
| #include "../ibutton_i.h" | ||||
| #include "m-string.h" | ||||
| 
 | ||||
| enum SubmenuIndex { | ||||
|     SubmenuIndexCyfral, | ||||
| @ -44,7 +45,7 @@ bool ibutton_scene_add_type_on_event(void* context, SceneManagerEvent event) { | ||||
|             furi_crash("Unknown key type"); | ||||
|         } | ||||
| 
 | ||||
|         ibutton_key_set_name(key, ""); | ||||
|         string_set_str(ibutton->file_path, IBUTTON_APP_FOLDER); | ||||
|         ibutton_key_clear_data(key); | ||||
|         scene_manager_next_scene(ibutton->scene_manager, iButtonSceneAddValue); | ||||
|     } | ||||
|  | ||||
| @ -1,4 +1,5 @@ | ||||
| #include "../ibutton_i.h" | ||||
| #include <toolbox/path.h> | ||||
| 
 | ||||
| static void ibutton_scene_delete_confirm_widget_callback( | ||||
|     GuiButtonType result, | ||||
| @ -16,7 +17,11 @@ void ibutton_scene_delete_confirm_on_enter(void* context) { | ||||
|     iButtonKey* key = ibutton->key; | ||||
|     const uint8_t* key_data = ibutton_key_get_data_p(key); | ||||
| 
 | ||||
|     ibutton_text_store_set(ibutton, "\e#Delete %s?\e#", ibutton_key_get_name_p(key)); | ||||
|     string_t key_name; | ||||
|     string_init(key_name); | ||||
|     path_extract_filename(ibutton->file_path, key_name, true); | ||||
| 
 | ||||
|     ibutton_text_store_set(ibutton, "\e#Delete %s?\e#", string_get_cstr(key_name)); | ||||
|     widget_add_text_box_element( | ||||
|         widget, 0, 0, 128, 27, AlignCenter, AlignCenter, ibutton->text_store, false); | ||||
|     widget_add_button_element( | ||||
| @ -62,6 +67,8 @@ void ibutton_scene_delete_confirm_on_enter(void* context) { | ||||
|         widget, 64, 33, AlignCenter, AlignBottom, FontSecondary, ibutton->text_store); | ||||
| 
 | ||||
|     view_dispatcher_switch_to_view(ibutton->view_dispatcher, iButtonViewWidget); | ||||
| 
 | ||||
|     string_clear(key_name); | ||||
| } | ||||
| 
 | ||||
| bool ibutton_scene_delete_confirm_on_event(void* context, SceneManagerEvent event) { | ||||
|  | ||||
| @ -1,5 +1,6 @@ | ||||
| #include "../ibutton_i.h" | ||||
| #include <dolphin/dolphin.h> | ||||
| #include <toolbox/path.h> | ||||
| 
 | ||||
| static void ibutton_scene_emulate_callback(void* context, bool emulated) { | ||||
|     iButton* ibutton = context; | ||||
| @ -15,14 +16,19 @@ void ibutton_scene_emulate_on_enter(void* context) { | ||||
|     iButtonKey* key = ibutton->key; | ||||
| 
 | ||||
|     const uint8_t* key_data = ibutton_key_get_data_p(key); | ||||
|     const char* key_name = ibutton_key_get_name_p(key); | ||||
| 
 | ||||
|     string_t key_name; | ||||
|     string_init(key_name); | ||||
|     if(string_end_with_str_p(ibutton->file_path, IBUTTON_APP_EXTENSION)) { | ||||
|         path_extract_filename(ibutton->file_path, key_name, true); | ||||
|     } | ||||
| 
 | ||||
|     uint8_t line_count = 2; | ||||
|     DOLPHIN_DEED(DolphinDeedIbuttonEmulate); | ||||
| 
 | ||||
|     // check that stored key has name
 | ||||
|     if(strcmp(key_name, "") != 0) { | ||||
|         ibutton_text_store_set(ibutton, "emulating\n%s", key_name); | ||||
|     if(!string_empty_p(key_name)) { | ||||
|         ibutton_text_store_set(ibutton, "emulating\n%s", string_get_cstr(key_name)); | ||||
|         line_count = 2; | ||||
|     } else { | ||||
|         // if not, show key data
 | ||||
| @ -77,6 +83,8 @@ void ibutton_scene_emulate_on_enter(void* context) { | ||||
|     ibutton_worker_emulate_set_callback( | ||||
|         ibutton->key_worker, ibutton_scene_emulate_callback, ibutton); | ||||
|     ibutton_worker_emulate_start(ibutton->key_worker, key); | ||||
| 
 | ||||
|     string_clear(key_name); | ||||
| } | ||||
| 
 | ||||
| bool ibutton_scene_emulate_on_event(void* context, SceneManagerEvent event) { | ||||
|  | ||||
| @ -1,4 +1,5 @@ | ||||
| #include "../ibutton_i.h" | ||||
| #include <toolbox/path.h> | ||||
| 
 | ||||
| void ibutton_scene_info_on_enter(void* context) { | ||||
|     iButton* ibutton = context; | ||||
| @ -7,7 +8,11 @@ void ibutton_scene_info_on_enter(void* context) { | ||||
| 
 | ||||
|     const uint8_t* key_data = ibutton_key_get_data_p(key); | ||||
| 
 | ||||
|     ibutton_text_store_set(ibutton, "%s", ibutton_key_get_name_p(key)); | ||||
|     string_t key_name; | ||||
|     string_init(key_name); | ||||
|     path_extract_filename(ibutton->file_path, key_name, true); | ||||
| 
 | ||||
|     ibutton_text_store_set(ibutton, "%s", string_get_cstr(key_name)); | ||||
|     widget_add_text_box_element( | ||||
|         widget, 0, 0, 128, 28, AlignCenter, AlignCenter, ibutton->text_store, false); | ||||
| 
 | ||||
| @ -46,6 +51,8 @@ void ibutton_scene_info_on_enter(void* context) { | ||||
|         widget, 64, 35, AlignCenter, AlignBottom, FontPrimary, ibutton->text_store); | ||||
| 
 | ||||
|     view_dispatcher_switch_to_view(ibutton->view_dispatcher, iButtonViewWidget); | ||||
| 
 | ||||
|     string_clear(key_name); | ||||
| } | ||||
| 
 | ||||
| bool ibutton_scene_info_on_event(void* context, SceneManagerEvent event) { | ||||
|  | ||||
| @ -18,7 +18,7 @@ void ibutton_scene_read_on_enter(void* context) { | ||||
|     popup_set_icon(popup, 0, 5, &I_DolphinWait_61x59); | ||||
| 
 | ||||
|     view_dispatcher_switch_to_view(ibutton->view_dispatcher, iButtonViewPopup); | ||||
|     ibutton_key_set_name(key, ""); | ||||
|     string_set_str(ibutton->file_path, IBUTTON_APP_FOLDER); | ||||
| 
 | ||||
|     ibutton_worker_read_set_callback(worker, ibutton_scene_read_callback, ibutton); | ||||
|     ibutton_worker_read_start(worker, key); | ||||
|  | ||||
| @ -61,11 +61,7 @@ void ibutton_scene_read_crc_error_on_exit(void* context) { | ||||
| 
 | ||||
|     ibutton_text_store_clear(ibutton); | ||||
| 
 | ||||
|     dialog_ex_set_header(dialog_ex, NULL, 0, 0, AlignCenter, AlignCenter); | ||||
|     dialog_ex_set_text(dialog_ex, NULL, 0, 0, AlignCenter, AlignTop); | ||||
|     dialog_ex_set_left_button_text(dialog_ex, NULL); | ||||
|     dialog_ex_set_result_callback(dialog_ex, NULL); | ||||
|     dialog_ex_set_context(dialog_ex, NULL); | ||||
|     dialog_ex_reset(dialog_ex); | ||||
| 
 | ||||
|     ibutton_notification_message(ibutton, iButtonNotificationMessageRedOff); | ||||
| } | ||||
|  | ||||
| @ -62,11 +62,7 @@ void ibutton_scene_read_not_key_error_on_exit(void* context) { | ||||
| 
 | ||||
|     ibutton_text_store_clear(ibutton); | ||||
| 
 | ||||
|     dialog_ex_set_header(dialog_ex, NULL, 0, 0, AlignCenter, AlignCenter); | ||||
|     dialog_ex_set_text(dialog_ex, NULL, 0, 0, AlignCenter, AlignTop); | ||||
|     dialog_ex_set_left_button_text(dialog_ex, NULL); | ||||
|     dialog_ex_set_result_callback(dialog_ex, NULL); | ||||
|     dialog_ex_set_context(dialog_ex, NULL); | ||||
|     dialog_ex_reset(dialog_ex); | ||||
| 
 | ||||
|     ibutton_notification_message(ibutton, iButtonNotificationMessageRedOff); | ||||
| } | ||||
|  | ||||
| @ -76,12 +76,7 @@ void ibutton_scene_read_success_on_exit(void* context) { | ||||
| 
 | ||||
|     ibutton_text_store_clear(ibutton); | ||||
| 
 | ||||
|     dialog_ex_set_text(dialog_ex, NULL, 0, 0, AlignCenter, AlignTop); | ||||
|     dialog_ex_set_left_button_text(dialog_ex, NULL); | ||||
|     dialog_ex_set_right_button_text(dialog_ex, NULL); | ||||
|     dialog_ex_set_result_callback(dialog_ex, NULL); | ||||
|     dialog_ex_set_context(dialog_ex, NULL); | ||||
|     dialog_ex_set_icon(dialog_ex, 0, 0, NULL); | ||||
|     dialog_ex_reset(dialog_ex); | ||||
| 
 | ||||
|     ibutton_notification_message(ibutton, iButtonNotificationMessageGreenOff); | ||||
| } | ||||
|  | ||||
| @ -1,5 +1,7 @@ | ||||
| #include "../ibutton_i.h" | ||||
| #include "m-string.h" | ||||
| #include <lib/toolbox/random_name.h> | ||||
| #include <toolbox/path.h> | ||||
| 
 | ||||
| static void ibutton_scene_save_name_text_input_callback(void* context) { | ||||
|     iButton* ibutton = context; | ||||
| @ -10,13 +12,17 @@ void ibutton_scene_save_name_on_enter(void* context) { | ||||
|     iButton* ibutton = context; | ||||
|     TextInput* text_input = ibutton->text_input; | ||||
| 
 | ||||
|     const char* key_name = ibutton_key_get_name_p(ibutton->key); | ||||
|     const bool key_name_is_empty = !strcmp(key_name, ""); | ||||
|     string_t key_name; | ||||
|     string_init(key_name); | ||||
|     if(string_end_with_str_p(ibutton->file_path, IBUTTON_APP_EXTENSION)) { | ||||
|         path_extract_filename(ibutton->file_path, key_name, true); | ||||
|     } | ||||
| 
 | ||||
|     const bool key_name_is_empty = string_empty_p(key_name); | ||||
|     if(key_name_is_empty) { | ||||
|         set_random_name(ibutton->text_store, IBUTTON_TEXT_STORE_SIZE); | ||||
|     } else { | ||||
|         ibutton_text_store_set(ibutton, "%s", key_name); | ||||
|         ibutton_text_store_set(ibutton, "%s", string_get_cstr(key_name)); | ||||
|     } | ||||
| 
 | ||||
|     text_input_set_header_text(text_input, "Name the key"); | ||||
| @ -28,11 +34,19 @@ void ibutton_scene_save_name_on_enter(void* context) { | ||||
|         IBUTTON_KEY_NAME_SIZE, | ||||
|         key_name_is_empty); | ||||
| 
 | ||||
|     ValidatorIsFile* validator_is_file = | ||||
|         validator_is_file_alloc_init(IBUTTON_APP_FOLDER, IBUTTON_APP_EXTENSION, key_name); | ||||
|     string_t folder_path; | ||||
|     string_init(folder_path); | ||||
| 
 | ||||
|     path_extract_dirname(string_get_cstr(ibutton->file_path), folder_path); | ||||
| 
 | ||||
|     ValidatorIsFile* validator_is_file = validator_is_file_alloc_init( | ||||
|         string_get_cstr(folder_path), IBUTTON_APP_EXTENSION, string_get_cstr(key_name)); | ||||
|     text_input_set_validator(text_input, validator_is_file_callback, validator_is_file); | ||||
| 
 | ||||
|     view_dispatcher_switch_to_view(ibutton->view_dispatcher, iButtonViewTextInput); | ||||
| 
 | ||||
|     string_clear(key_name); | ||||
|     string_clear(folder_path); | ||||
| } | ||||
| 
 | ||||
| bool ibutton_scene_save_name_on_event(void* context, SceneManagerEvent event) { | ||||
|  | ||||
| @ -36,6 +36,7 @@ bool ibutton_scene_start_on_event(void* context, SceneManagerEvent event) { | ||||
|         if(event.event == SubmenuIndexRead) { | ||||
|             scene_manager_next_scene(ibutton->scene_manager, iButtonSceneRead); | ||||
|         } else if(event.event == SubmenuIndexSaved) { | ||||
|             string_set_str(ibutton->file_path, IBUTTON_APP_FOLDER); | ||||
|             scene_manager_next_scene(ibutton->scene_manager, iButtonSceneSelectKey); | ||||
|         } else if(event.event == SubmenuIndexAdd) { | ||||
|             scene_manager_next_scene(ibutton->scene_manager, iButtonSceneAddType); | ||||
|  | ||||
| @ -1,4 +1,6 @@ | ||||
| #include "../ibutton_i.h" | ||||
| #include "m-string.h" | ||||
| #include "toolbox/path.h" | ||||
| 
 | ||||
| typedef enum { | ||||
|     iButtonSceneWriteStateDefault, | ||||
| @ -17,13 +19,18 @@ void ibutton_scene_write_on_enter(void* context) { | ||||
|     iButtonWorker* worker = ibutton->key_worker; | ||||
| 
 | ||||
|     const uint8_t* key_data = ibutton_key_get_data_p(key); | ||||
|     const char* key_name = ibutton_key_get_name_p(key); | ||||
| 
 | ||||
|     string_t key_name; | ||||
|     string_init(key_name); | ||||
|     if(string_end_with_str_p(ibutton->file_path, IBUTTON_APP_EXTENSION)) { | ||||
|         path_extract_filename(ibutton->file_path, key_name, true); | ||||
|     } | ||||
| 
 | ||||
|     uint8_t line_count = 2; | ||||
| 
 | ||||
|     // check that stored key has name
 | ||||
|     if(strcmp(key_name, "") != 0) { | ||||
|         ibutton_text_store_set(ibutton, "writing\n%s", key_name); | ||||
|     if(!string_empty_p(key_name)) { | ||||
|         ibutton_text_store_set(ibutton, "writing\n%s", string_get_cstr(key_name)); | ||||
|         line_count = 2; | ||||
|     } else { | ||||
|         // if not, show key data
 | ||||
| @ -79,6 +86,8 @@ void ibutton_scene_write_on_enter(void* context) { | ||||
| 
 | ||||
|     ibutton_worker_write_set_callback(worker, ibutton_scene_write_callback, ibutton); | ||||
|     ibutton_worker_write_start(worker, key); | ||||
| 
 | ||||
|     string_clear(key_name); | ||||
| } | ||||
| 
 | ||||
| bool ibutton_scene_write_on_event(void* context, SceneManagerEvent event) { | ||||
|  | ||||
| @ -1,6 +1,5 @@ | ||||
| #include <furi_hal_delay.h> | ||||
| #include <infrared.h> | ||||
| #include <app_template.h> | ||||
| #include <cli/cli.h> | ||||
| #include <cmsis_os2.h> | ||||
| #include <infrared_worker.h> | ||||
| @ -86,7 +85,7 @@ static void infrared_cli_print_usage(void) { | ||||
|     } | ||||
|     printf("\r\n"); | ||||
|     printf("\tRaw format:\r\n"); | ||||
|     printf("\tir_tx RAW F:<frequency> DC:<duty_cycle> <sample0> <sample1>...\r\n"); | ||||
|     printf("\tir tx RAW F:<frequency> DC:<duty_cycle> <sample0> <sample1>...\r\n"); | ||||
|     printf( | ||||
|         "\tFrequency (%d - %d), Duty cycle (0 - 100), max 512 samples\r\n", | ||||
|         INFRARED_MIN_FREQUENCY, | ||||
| @ -178,7 +177,7 @@ static void infrared_cli_start_ir(Cli* cli, string_t args, void* context) { | ||||
|                 break; | ||||
|             } | ||||
|             if(string_get_cstr(args)[size] == ' ') { | ||||
|                 string_right(args, size); | ||||
|                 string_right(args, size + 1); | ||||
|                 break; | ||||
|             } | ||||
|         } | ||||
|  | ||||
| @ -1,4 +1,5 @@ | ||||
| #include "infrared_app.h" | ||||
| #include "m-string.h" | ||||
| #include <infrared_worker.h> | ||||
| #include <furi.h> | ||||
| #include <gui/gui.h> | ||||
| @ -12,20 +13,18 @@ int32_t InfraredApp::run(void* args) { | ||||
|     bool exit = false; | ||||
| 
 | ||||
|     if(args) { | ||||
|         std::string path = static_cast<const char*>(args); | ||||
|         std::string remote_name(path, path.find_last_of('/') + 1, path.size()); | ||||
|         auto last_dot = remote_name.find_last_of('.'); | ||||
|         if(last_dot != std::string::npos) { | ||||
|             remote_name.erase(last_dot); | ||||
|             path.erase(path.find_last_of('/')); | ||||
|             bool result = remote_manager.load(path, remote_name); | ||||
|         string_t path; | ||||
|         string_init_set_str(path, (char*)args); | ||||
|         if(string_end_with_str_p(path, InfraredApp::infrared_extension)) { | ||||
|             bool result = remote_manager.load(path); | ||||
|             if(result) { | ||||
|                 current_scene = InfraredApp::Scene::Remote; | ||||
|             } else { | ||||
|                 printf("Failed to load remote \'%s\'\r\n", remote_name.c_str()); | ||||
|                 printf("Failed to load remote \'%s\'\r\n", string_get_cstr(path)); | ||||
|                 return -1; | ||||
|             } | ||||
|         } | ||||
|         string_clear(path); | ||||
|     } | ||||
| 
 | ||||
|     scenes[current_scene]->on_enter(this); | ||||
| @ -51,6 +50,7 @@ int32_t InfraredApp::run(void* args) { | ||||
| 
 | ||||
| InfraredApp::InfraredApp() { | ||||
|     furi_check(InfraredAppRemoteManager::max_button_name_length < get_text_store_size()); | ||||
|     string_init_set_str(file_path, InfraredApp::infrared_directory); | ||||
|     notification = static_cast<NotificationApp*>(furi_record_open("notification")); | ||||
|     dialogs = static_cast<DialogsApp*>(furi_record_open("dialogs")); | ||||
|     infrared_worker = infrared_worker_alloc(); | ||||
| @ -60,6 +60,7 @@ InfraredApp::~InfraredApp() { | ||||
|     infrared_worker_free(infrared_worker); | ||||
|     furi_record_close("notification"); | ||||
|     furi_record_close("dialogs"); | ||||
|     string_clear(file_path); | ||||
|     for(auto& [key, scene] : scenes) delete scene; | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -251,6 +251,8 @@ public: | ||||
|     /** Main class destructor, deinitializes all critical objects */ | ||||
|     ~InfraredApp(); | ||||
| 
 | ||||
|     string_t file_path; | ||||
| 
 | ||||
|     /** Path to Infrared directory */ | ||||
|     static constexpr const char* infrared_directory = "/any/infrared"; | ||||
|     /** Infrared files extension (remote files and universal databases) */ | ||||
|  | ||||
| @ -1,3 +1,5 @@ | ||||
| #include "m-string.h" | ||||
| #include "storage/filesystem_api_defines.h" | ||||
| #include <flipper_format/flipper_format.h> | ||||
| #include "infrared_app_remote_manager.h" | ||||
| #include "infrared/helpers/infrared_parser.h" | ||||
| @ -11,44 +13,58 @@ | ||||
| #include <gui/modules/button_menu.h> | ||||
| #include <storage/storage.h> | ||||
| #include "infrared_app.h" | ||||
| #include <toolbox/path.h> | ||||
| 
 | ||||
| static const char* default_remote_name = "remote"; | ||||
| 
 | ||||
| std::string InfraredAppRemoteManager::make_full_name( | ||||
|     const std::string& path, | ||||
|     const std::string& remote_name) const { | ||||
|     return std::string("") + path + "/" + remote_name + InfraredApp::infrared_extension; | ||||
| } | ||||
| 
 | ||||
| std::string InfraredAppRemoteManager::find_vacant_remote_name(const std::string& name) { | ||||
|     std::string result_name; | ||||
| void InfraredAppRemoteManager::find_vacant_remote_name(string_t name, string_t path) { | ||||
|     Storage* storage = static_cast<Storage*>(furi_record_open("storage")); | ||||
| 
 | ||||
|     FS_Error error = storage_common_stat( | ||||
|         storage, make_full_name(InfraredApp::infrared_directory, name).c_str(), NULL); | ||||
|     string_t base_path; | ||||
|     string_init_set(base_path, path); | ||||
| 
 | ||||
|     if(error == FSE_NOT_EXIST) { | ||||
|         result_name = name; | ||||
|     } else if(error != FSE_OK) { | ||||
|         result_name = std::string(); | ||||
|     } else { | ||||
|     if(string_end_with_str_p(base_path, InfraredApp::infrared_extension)) { | ||||
|         size_t filename_start = string_search_rchar(base_path, '/'); | ||||
|         string_left(base_path, filename_start); | ||||
|     } | ||||
| 
 | ||||
|     string_printf( | ||||
|         base_path, | ||||
|         "%s/%s%s", | ||||
|         string_get_cstr(path), | ||||
|         string_get_cstr(name), | ||||
|         InfraredApp::infrared_extension); | ||||
| 
 | ||||
|     FS_Error error = storage_common_stat(storage, string_get_cstr(base_path), NULL); | ||||
| 
 | ||||
|     if(error == FSE_OK) { | ||||
|         /* if suggested name is occupied, try another one (name2, name3, etc) */ | ||||
|         size_t dot = string_search_rchar(base_path, '.'); | ||||
|         string_left(base_path, dot); | ||||
| 
 | ||||
|         string_t path_temp; | ||||
|         string_init(path_temp); | ||||
| 
 | ||||
|         uint32_t i = 1; | ||||
|         std::string new_name; | ||||
|         do { | ||||
|             new_name = make_full_name(InfraredApp::infrared_directory, name + std::to_string(++i)); | ||||
|             error = storage_common_stat(storage, new_name.c_str(), NULL); | ||||
|             string_printf( | ||||
|                 path_temp, | ||||
|                 "%s%u%s", | ||||
|                 string_get_cstr(base_path), | ||||
|                 ++i, | ||||
|                 InfraredApp::infrared_extension); | ||||
|             error = storage_common_stat(storage, string_get_cstr(path_temp), NULL); | ||||
|         } while(error == FSE_OK); | ||||
| 
 | ||||
|         string_clear(path_temp); | ||||
| 
 | ||||
|         if(error == FSE_NOT_EXIST) { | ||||
|             result_name = name + std::to_string(i); | ||||
|         } else { | ||||
|             result_name = std::string(); | ||||
|             string_cat_printf(name, "%u", i); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     string_clear(base_path); | ||||
|     furi_record_close("storage"); | ||||
|     return result_name; | ||||
| } | ||||
| 
 | ||||
| bool InfraredAppRemoteManager::add_button(const char* button_name, const InfraredAppSignal& signal) { | ||||
| @ -61,12 +77,23 @@ bool InfraredAppRemoteManager::add_remote_with_button( | ||||
|     const InfraredAppSignal& signal) { | ||||
|     furi_check(button_name != nullptr); | ||||
| 
 | ||||
|     auto new_name = find_vacant_remote_name(default_remote_name); | ||||
|     if(new_name.empty()) { | ||||
|         return false; | ||||
|     } | ||||
|     string_t new_name; | ||||
|     string_init_set_str(new_name, default_remote_name); | ||||
| 
 | ||||
|     string_t new_path; | ||||
|     string_init_set_str(new_path, InfraredApp::infrared_directory); | ||||
| 
 | ||||
|     find_vacant_remote_name(new_name, new_path); | ||||
| 
 | ||||
|     string_cat_printf( | ||||
|         new_path, "/%s%s", string_get_cstr(new_name), InfraredApp::infrared_extension); | ||||
| 
 | ||||
|     remote = std::make_unique<InfraredAppRemote>(new_path); | ||||
|     remote->name = std::string(string_get_cstr(new_name)); | ||||
| 
 | ||||
|     string_clear(new_path); | ||||
|     string_clear(new_name); | ||||
| 
 | ||||
|     remote = std::make_unique<InfraredAppRemote>(InfraredApp::infrared_directory, new_name); | ||||
|     return add_button(button_name, signal); | ||||
| } | ||||
| 
 | ||||
| @ -93,8 +120,7 @@ const InfraredAppSignal& InfraredAppRemoteManager::get_button_data(size_t index) | ||||
| bool InfraredAppRemoteManager::delete_remote() { | ||||
|     Storage* storage = static_cast<Storage*>(furi_record_open("storage")); | ||||
| 
 | ||||
|     FS_Error error = | ||||
|         storage_common_remove(storage, make_full_name(remote->path, remote->name).c_str()); | ||||
|     FS_Error error = storage_common_remove(storage, string_get_cstr(remote->path)); | ||||
|     reset_remote(); | ||||
| 
 | ||||
|     furi_record_close("storage"); | ||||
| @ -128,22 +154,33 @@ std::string InfraredAppRemoteManager::get_remote_name() { | ||||
| bool InfraredAppRemoteManager::rename_remote(const char* str) { | ||||
|     furi_check(str != nullptr); | ||||
|     furi_check(remote.get() != nullptr); | ||||
|     furi_check(!string_empty_p(remote->path)); | ||||
| 
 | ||||
|     if(!remote->name.compare(str)) { | ||||
|         return true; | ||||
|     } | ||||
| 
 | ||||
|     auto new_name = find_vacant_remote_name(str); | ||||
|     if(new_name.empty()) { | ||||
|         return false; | ||||
|     string_t new_name; | ||||
|     string_init_set_str(new_name, str); | ||||
|     find_vacant_remote_name(new_name, remote->path); | ||||
| 
 | ||||
|     string_t new_path; | ||||
|     string_init_set(new_path, remote->path); | ||||
|     if(string_end_with_str_p(new_path, InfraredApp::infrared_extension)) { | ||||
|         size_t filename_start = string_search_rchar(new_path, '/'); | ||||
|         string_left(new_path, filename_start); | ||||
|     } | ||||
|     string_cat_printf( | ||||
|         new_path, "/%s%s", string_get_cstr(new_name), InfraredApp::infrared_extension); | ||||
| 
 | ||||
|     Storage* storage = static_cast<Storage*>(furi_record_open("storage")); | ||||
| 
 | ||||
|     std::string old_filename = make_full_name(remote->path, remote->name); | ||||
|     std::string new_filename = make_full_name(remote->path, new_name); | ||||
|     FS_Error error = storage_common_rename(storage, old_filename.c_str(), new_filename.c_str()); | ||||
|     remote->name = new_name; | ||||
|     FS_Error error = | ||||
|         storage_common_rename(storage, string_get_cstr(remote->path), string_get_cstr(new_path)); | ||||
|     remote->name = std::string(string_get_cstr(new_name)); | ||||
| 
 | ||||
|     string_clear(new_name); | ||||
|     string_clear(new_path); | ||||
| 
 | ||||
|     furi_record_close("storage"); | ||||
|     return (error == FSE_OK || error == FSE_EXIST); | ||||
| @ -171,10 +208,8 @@ bool InfraredAppRemoteManager::store(void) { | ||||
| 
 | ||||
|     FlipperFormat* ff = flipper_format_file_alloc(storage); | ||||
| 
 | ||||
|     FURI_LOG_I( | ||||
|         "RemoteManager", "store file: \'%s\'", make_full_name(remote->path, remote->name).c_str()); | ||||
|     result = | ||||
|         flipper_format_file_open_always(ff, make_full_name(remote->path, remote->name).c_str()); | ||||
|     FURI_LOG_I("RemoteManager", "store file: \'%s\'", string_get_cstr(remote->path)); | ||||
|     result = flipper_format_file_open_always(ff, string_get_cstr(remote->path)); | ||||
|     if(result) { | ||||
|         result = flipper_format_write_header_cstr(ff, "IR signals file", 1); | ||||
|     } | ||||
| @ -192,13 +227,13 @@ bool InfraredAppRemoteManager::store(void) { | ||||
|     return result; | ||||
| } | ||||
| 
 | ||||
| bool InfraredAppRemoteManager::load(const std::string& path, const std::string& remote_name) { | ||||
| bool InfraredAppRemoteManager::load(string_t path) { | ||||
|     bool result = false; | ||||
|     Storage* storage = static_cast<Storage*>(furi_record_open("storage")); | ||||
|     FlipperFormat* ff = flipper_format_file_alloc(storage); | ||||
| 
 | ||||
|     FURI_LOG_I("RemoteManager", "load file: \'%s\'", make_full_name(path, remote_name).c_str()); | ||||
|     result = flipper_format_file_open_existing(ff, make_full_name(path, remote_name).c_str()); | ||||
|     FURI_LOG_I("RemoteManager", "load file: \'%s\'", string_get_cstr(path)); | ||||
|     result = flipper_format_file_open_existing(ff, string_get_cstr(path)); | ||||
|     if(result) { | ||||
|         string_t header; | ||||
|         string_init(header); | ||||
| @ -210,7 +245,14 @@ bool InfraredAppRemoteManager::load(const std::string& path, const std::string& | ||||
|         string_clear(header); | ||||
|     } | ||||
|     if(result) { | ||||
|         remote = std::make_unique<InfraredAppRemote>(path, remote_name); | ||||
|         string_t new_name; | ||||
|         string_init(new_name); | ||||
| 
 | ||||
|         remote = std::make_unique<InfraredAppRemote>(path); | ||||
|         path_extract_filename(path, new_name, true); | ||||
|         remote->name = std::string(string_get_cstr(new_name)); | ||||
| 
 | ||||
|         string_clear(new_name); | ||||
|         InfraredAppSignal signal; | ||||
|         std::string signal_name; | ||||
|         while(infrared_parser_read_signal(ff, signal, signal_name)) { | ||||
|  | ||||
| @ -8,6 +8,7 @@ | ||||
| 
 | ||||
| #include "infrared_app_signal.h" | ||||
| 
 | ||||
| #include "m-string.h" | ||||
| #include <infrared_worker.h> | ||||
| #include <infrared.h> | ||||
| 
 | ||||
| @ -60,17 +61,19 @@ class InfraredAppRemote { | ||||
|     /** Name of remote */ | ||||
|     std::string name; | ||||
|     /** Path to remote file */ | ||||
|     std::string path; | ||||
|     string_t path; | ||||
| 
 | ||||
| public: | ||||
|     /** Initialize new remote
 | ||||
|      *  | ||||
|      * @param path - remote file path | ||||
|      * @param name - new remote name | ||||
|      */ | ||||
|     InfraredAppRemote(const std::string& path, const std::string& name) | ||||
|         : name(name) | ||||
|         , path(path) { | ||||
|     InfraredAppRemote(string_t file_path) { | ||||
|         string_init_set(path, file_path); | ||||
|     } | ||||
| 
 | ||||
|     ~InfraredAppRemote() { | ||||
|         string_clear(path); | ||||
|     } | ||||
| }; | ||||
| 
 | ||||
| @ -78,12 +81,6 @@ public: | ||||
| class InfraredAppRemoteManager { | ||||
|     /** Remote instance. There can be 1 remote loaded at a time. */ | ||||
|     std::unique_ptr<InfraredAppRemote> remote; | ||||
|     /** Make full name from remote name
 | ||||
|      * | ||||
|      * @param remote_name name of remote | ||||
|      * @retval full name of remote on disk | ||||
|      */ | ||||
|     std::string make_full_name(const std::string& path, const std::string& remote_name) const; | ||||
| 
 | ||||
| public: | ||||
|     /** Restriction to button name length. Buttons larger are ignored. */ | ||||
| @ -125,9 +122,9 @@ public: | ||||
|      * incremented digit(2,3,4,etc) added to name and check repeated. | ||||
|      * | ||||
|      * @param name - suggested remote name | ||||
|      * @retval garanteed free remote name, prefixed with suggested | ||||
|      * @param path - remote file path | ||||
|      */ | ||||
|     std::string find_vacant_remote_name(const std::string& name); | ||||
|     void find_vacant_remote_name(string_t name, string_t path); | ||||
| 
 | ||||
|     /** Get button list
 | ||||
|      * | ||||
| @ -185,8 +182,8 @@ public: | ||||
| 
 | ||||
|     /** Load data from disk into current remote
 | ||||
|      * | ||||
|      * @param name - name of remote to load | ||||
|      * @param path - path to remote file | ||||
|      * @retval true if success, false otherwise | ||||
|      */ | ||||
|     bool load(const std::string& path, const std::string& name); | ||||
|     bool load(string_t path); | ||||
| }; | ||||
|  | ||||
| @ -1,4 +1,6 @@ | ||||
| #include "../infrared_app.h" | ||||
| #include "m-string.h" | ||||
| #include "toolbox/path.h" | ||||
| 
 | ||||
| void InfraredAppSceneEditRename::on_enter(InfraredApp* app) { | ||||
|     InfraredAppViewManager* view_manager = app->get_view_manager(); | ||||
| @ -21,9 +23,18 @@ void InfraredAppSceneEditRename::on_enter(InfraredApp* app) { | ||||
|         enter_name_length = InfraredAppRemoteManager::max_remote_name_length; | ||||
|         text_input_set_header_text(text_input, "Name the remote"); | ||||
| 
 | ||||
|         string_t folder_path; | ||||
|         string_init(folder_path); | ||||
| 
 | ||||
|         if(string_end_with_str_p(app->file_path, InfraredApp::infrared_extension)) { | ||||
|             path_extract_dirname(string_get_cstr(app->file_path), folder_path); | ||||
|         } | ||||
| 
 | ||||
|         ValidatorIsFile* validator_is_file = validator_is_file_alloc_init( | ||||
|             app->infrared_directory, app->infrared_extension, remote_name.c_str()); | ||||
|             string_get_cstr(folder_path), app->infrared_extension, remote_name.c_str()); | ||||
|         text_input_set_validator(text_input, validator_is_file_callback, validator_is_file); | ||||
| 
 | ||||
|         string_clear(folder_path); | ||||
|     } | ||||
| 
 | ||||
|     text_input_set_result_callback( | ||||
|  | ||||
| @ -1,4 +1,5 @@ | ||||
| #include "../infrared_app.h" | ||||
| #include "assets_icons.h" | ||||
| #include "infrared/infrared_app_event.h" | ||||
| #include <text_store.h> | ||||
| 
 | ||||
| @ -8,11 +9,6 @@ void InfraredAppSceneRemoteList::on_enter(InfraredApp* app) { | ||||
|     bool result = false; | ||||
|     bool file_select_result; | ||||
|     auto remote_manager = app->get_remote_manager(); | ||||
|     auto last_selected_remote = remote_manager->get_remote_name(); | ||||
|     const char* last_selected_remote_name = | ||||
|         last_selected_remote.size() ? last_selected_remote.c_str() : nullptr; | ||||
|     auto filename_ts = | ||||
|         std::make_unique<TextStore>(InfraredAppRemoteManager::max_remote_name_length); | ||||
|     DialogsApp* dialogs = app->get_dialogs(); | ||||
| 
 | ||||
|     InfraredAppViewManager* view_manager = app->get_view_manager(); | ||||
| @ -20,16 +16,17 @@ void InfraredAppSceneRemoteList::on_enter(InfraredApp* app) { | ||||
|     button_menu_reset(button_menu); | ||||
|     view_manager->switch_to(InfraredAppViewManager::ViewId::ButtonMenu); | ||||
| 
 | ||||
|     file_select_result = dialog_file_select_show( | ||||
|     file_select_result = dialog_file_browser_show( | ||||
|         dialogs, | ||||
|         InfraredApp::infrared_directory, | ||||
|         app->file_path, | ||||
|         app->file_path, | ||||
|         InfraredApp::infrared_extension, | ||||
|         filename_ts->text, | ||||
|         filename_ts->text_size, | ||||
|         last_selected_remote_name); | ||||
|         true, | ||||
|         &I_ir_10px, | ||||
|         true); | ||||
| 
 | ||||
|     if(file_select_result) { | ||||
|         if(remote_manager->load(InfraredApp::infrared_directory, std::string(filename_ts->text))) { | ||||
|         if(remote_manager->load(app->file_path)) { | ||||
|             app->switch_to_next_scene(InfraredApp::Scene::Remote); | ||||
|             result = true; | ||||
|         } | ||||
|  | ||||
| @ -26,6 +26,8 @@ void InfraredAppSceneStart::on_enter(InfraredApp* app) { | ||||
|         submenu, "Learn New Remote", SubmenuIndexLearnNewRemote, submenu_callback, app); | ||||
|     submenu_add_item(submenu, "Saved Remotes", SubmenuIndexSavedRemotes, submenu_callback, app); | ||||
|     submenu_set_selected_item(submenu, submenu_item_selected); | ||||
| 
 | ||||
|     string_set_str(app->file_path, InfraredApp::infrared_directory); | ||||
|     submenu_item_selected = 0; | ||||
| 
 | ||||
|     view_manager->switch_to(InfraredAppViewManager::ViewId::Submenu); | ||||
|  | ||||
| @ -59,7 +59,7 @@ static void infrared_progress_view_draw_callback(Canvas* canvas, void* _model) { | ||||
|     elements_multiline_text_aligned( | ||||
|         canvas, x + 33, y + 37, AlignCenter, AlignCenter, percents_string); | ||||
| 
 | ||||
|     canvas_draw_icon(canvas, x + 11, y + height - 15, &I_Back_15x10); | ||||
|     canvas_draw_icon(canvas, x + 14, y + height - 14, &I_Pin_back_arrow_10x8); | ||||
|     canvas_draw_str(canvas, x + 30, y + height - 6, "= stop"); | ||||
| } | ||||
| 
 | ||||
|  | ||||
							
								
								
									
										107
									
								
								applications/lfrfid/helpers/decoder_ioprox.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										107
									
								
								applications/lfrfid/helpers/decoder_ioprox.cpp
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,107 @@ | ||||
| #include "decoder_ioprox.h" | ||||
| #include <furi_hal.h> | ||||
| #include <cli/cli.h> | ||||
| #include <utility> | ||||
| 
 | ||||
| constexpr uint32_t clocks_in_us = 64; | ||||
| 
 | ||||
| constexpr uint32_t jitter_time_us = 20; | ||||
| constexpr uint32_t min_time_us = 64; | ||||
| constexpr uint32_t max_time_us = 80; | ||||
| constexpr uint32_t baud_time_us = 500; | ||||
| 
 | ||||
| constexpr uint32_t min_time = (min_time_us - jitter_time_us) * clocks_in_us; | ||||
| constexpr uint32_t mid_time = ((max_time_us - min_time_us) / 2 + min_time_us) * clocks_in_us; | ||||
| constexpr uint32_t max_time = (max_time_us + jitter_time_us) * clocks_in_us; | ||||
| constexpr uint32_t baud_time = baud_time_us * clocks_in_us; | ||||
| 
 | ||||
| bool DecoderIoProx::read(uint8_t* data, uint8_t data_size) { | ||||
|     bool result = false; | ||||
|     furi_assert(data_size >= 4); | ||||
| 
 | ||||
|     if(ready) { | ||||
|         result = true; | ||||
|         ioprox.decode(raw_data, sizeof(raw_data), data, data_size); | ||||
|         ready = false; | ||||
|     } | ||||
| 
 | ||||
|     return result; | ||||
| } | ||||
| 
 | ||||
| void DecoderIoProx::process_front(bool is_rising_edge, uint32_t time) { | ||||
|     if(ready) { | ||||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     // Always track the time that's gone by.
 | ||||
|     current_period_duration += time; | ||||
|     demodulation_sample_duration += time; | ||||
| 
 | ||||
|     // If a baud time has elapsed, we're at a sample point.
 | ||||
|     if(demodulation_sample_duration >= baud_time) { | ||||
|         // Start a new baud period...
 | ||||
|         demodulation_sample_duration = 0; | ||||
|         demodulated_value_invalid = false; | ||||
| 
 | ||||
|         // ... and if we didn't have any baud errors, capture a sample.
 | ||||
|         if(!demodulated_value_invalid) { | ||||
|             store_data(current_demodulated_value); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     //
 | ||||
|     // FSK demodulator.
 | ||||
|     //
 | ||||
| 
 | ||||
|     // If this isn't a rising edge, this isn't a pulse of interest.
 | ||||
|     // We're done.
 | ||||
|     if(!is_rising_edge) { | ||||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     bool is_valid_low = (current_period_duration > min_time) && | ||||
|                         (current_period_duration <= mid_time); | ||||
|     bool is_valid_high = (current_period_duration > mid_time) && | ||||
|                          (current_period_duration < max_time); | ||||
| 
 | ||||
|     // If this is between the minimum and our threshold, this is a logical 0.
 | ||||
|     if(is_valid_low) { | ||||
|         current_demodulated_value = false; | ||||
|     } | ||||
|     // Otherwise, if between our threshold and the max time, it's a logical 1.
 | ||||
|     else if(is_valid_high) { | ||||
|         current_demodulated_value = true; | ||||
|     } | ||||
|     // Otherwise, invalidate this sample.
 | ||||
|     else { | ||||
|         demodulated_value_invalid = true; | ||||
|     } | ||||
| 
 | ||||
|     // We're starting a new period; track that.
 | ||||
|     current_period_duration = 0; | ||||
| } | ||||
| 
 | ||||
| DecoderIoProx::DecoderIoProx() { | ||||
|     reset_state(); | ||||
| } | ||||
| 
 | ||||
| void DecoderIoProx::store_data(bool data) { | ||||
|     for(int i = 0; i < 7; ++i) { | ||||
|         raw_data[i] = (raw_data[i] << 1) | ((raw_data[i + 1] >> 7) & 1); | ||||
|     } | ||||
|     raw_data[7] = (raw_data[7] << 1) | data; | ||||
| 
 | ||||
|     if(ioprox.can_be_decoded(raw_data, sizeof(raw_data))) { | ||||
|         ready = true; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| void DecoderIoProx::reset_state() { | ||||
|     current_demodulated_value = false; | ||||
|     demodulated_value_invalid = false; | ||||
| 
 | ||||
|     current_period_duration = 0; | ||||
|     demodulation_sample_duration = 0; | ||||
| 
 | ||||
|     ready = false; | ||||
| } | ||||
							
								
								
									
										26
									
								
								applications/lfrfid/helpers/decoder_ioprox.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								applications/lfrfid/helpers/decoder_ioprox.h
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,26 @@ | ||||
| #pragma once | ||||
| #include <stdint.h> | ||||
| #include <atomic> | ||||
| #include "protocols/protocol_ioprox.h" | ||||
| 
 | ||||
| class DecoderIoProx { | ||||
| public: | ||||
|     bool read(uint8_t* data, uint8_t data_size); | ||||
|     void process_front(bool polarity, uint32_t time); | ||||
|     DecoderIoProx(); | ||||
| 
 | ||||
| private: | ||||
|     uint32_t current_period_duration = 0; | ||||
|     uint32_t demodulation_sample_duration = 0; | ||||
| 
 | ||||
|     bool current_demodulated_value = false; | ||||
|     bool demodulated_value_invalid = false; | ||||
| 
 | ||||
|     uint8_t raw_data[8] = {0}; | ||||
|     void store_data(bool data); | ||||
| 
 | ||||
|     std::atomic<bool> ready; | ||||
| 
 | ||||
|     void reset_state(); | ||||
|     ProtocolIoProx ioprox; | ||||
| }; | ||||
							
								
								
									
										32
									
								
								applications/lfrfid/helpers/encoder_ioprox.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								applications/lfrfid/helpers/encoder_ioprox.cpp
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,32 @@ | ||||
| #include "encoder_ioprox.h" | ||||
| #include "protocols/protocol_ioprox.h" | ||||
| #include <furi.h> | ||||
| 
 | ||||
| void EncoderIoProx::init(const uint8_t* data, const uint8_t data_size) { | ||||
|     ProtocolIoProx ioprox; | ||||
|     ioprox.encode(data, data_size, card_data, sizeof(card_data)); | ||||
|     card_data_index = 0; | ||||
| } | ||||
| 
 | ||||
| void EncoderIoProx::get_next(bool* polarity, uint16_t* period, uint16_t* pulse) { | ||||
|     uint8_t bit = (card_data[card_data_index / 8] >> (7 - (card_data_index % 8))) & 1; | ||||
| 
 | ||||
|     bool advance = fsk->next(bit, period); | ||||
|     if(advance) { | ||||
|         card_data_index++; | ||||
|         if(card_data_index >= (8 * card_data_max)) { | ||||
|             card_data_index = 0; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     *polarity = true; | ||||
|     *pulse = *period / 2; | ||||
| } | ||||
| 
 | ||||
| EncoderIoProx::EncoderIoProx() { | ||||
|     fsk = new OscFSK(8, 10, 64); | ||||
| } | ||||
| 
 | ||||
| EncoderIoProx::~EncoderIoProx() { | ||||
|     delete fsk; | ||||
| } | ||||
							
								
								
									
										25
									
								
								applications/lfrfid/helpers/encoder_ioprox.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								applications/lfrfid/helpers/encoder_ioprox.h
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,25 @@ | ||||
| #pragma once | ||||
| #include "encoder_generic.h" | ||||
| #include "osc_fsk.h" | ||||
| 
 | ||||
| class EncoderIoProx : public EncoderGeneric { | ||||
| public: | ||||
|     /**
 | ||||
|      * @brief init data to emulate | ||||
|      *  | ||||
|      * @param data 1 byte FC, 1 byte Version, 2 bytes code | ||||
|      * @param data_size must be 4 | ||||
|      */ | ||||
|     void init(const uint8_t* data, const uint8_t data_size) final; | ||||
|     void get_next(bool* polarity, uint16_t* period, uint16_t* pulse) final; | ||||
|     EncoderIoProx(); | ||||
|     ~EncoderIoProx(); | ||||
| 
 | ||||
| private: | ||||
|     static const uint8_t card_data_max = 8; | ||||
| 
 | ||||
|     uint8_t card_data[card_data_max]; | ||||
|     uint8_t card_data_index; | ||||
| 
 | ||||
|     OscFSK* fsk; | ||||
| }; | ||||
| @ -12,6 +12,9 @@ const char* lfrfid_key_get_type_string(LfrfidKeyType type) { | ||||
|     case LfrfidKeyType::KeyI40134: | ||||
|         return "I40134"; | ||||
|         break; | ||||
|     case LfrfidKeyType::KeyIoProxXSF: | ||||
|         return "IoProxXSF"; | ||||
|         break; | ||||
|     } | ||||
| 
 | ||||
|     return "Unknown"; | ||||
| @ -28,6 +31,8 @@ const char* lfrfid_key_get_manufacturer_string(LfrfidKeyType type) { | ||||
|     case LfrfidKeyType::KeyI40134: | ||||
|         return "Indala"; | ||||
|         break; | ||||
|     case LfrfidKeyType::KeyIoProxXSF: | ||||
|         return "Kantech"; | ||||
|     } | ||||
| 
 | ||||
|     return "Unknown"; | ||||
| @ -42,6 +47,8 @@ bool lfrfid_key_get_string_type(const char* string, LfrfidKeyType* type) { | ||||
|         *type = LfrfidKeyType::KeyH10301; | ||||
|     } else if(strcmp("I40134", string) == 0) { | ||||
|         *type = LfrfidKeyType::KeyI40134; | ||||
|     } else if(strcmp("IoProxXSF", string) == 0) { | ||||
|         *type = LfrfidKeyType::KeyIoProxXSF; | ||||
|     } else { | ||||
|         result = false; | ||||
|     } | ||||
| @ -60,6 +67,9 @@ uint8_t lfrfid_key_get_type_data_count(LfrfidKeyType type) { | ||||
|     case LfrfidKeyType::KeyI40134: | ||||
|         return 3; | ||||
|         break; | ||||
|     case LfrfidKeyType::KeyIoProxXSF: | ||||
|         return 4; | ||||
|         break; | ||||
|     } | ||||
| 
 | ||||
|     return 0; | ||||
|  | ||||
| @ -8,6 +8,7 @@ enum class LfrfidKeyType : uint8_t { | ||||
|     KeyEM4100, | ||||
|     KeyH10301, | ||||
|     KeyI40134, | ||||
|     KeyIoProxXSF, | ||||
| }; | ||||
| 
 | ||||
| const char* lfrfid_key_get_type_string(LfrfidKeyType type); | ||||
|  | ||||
							
								
								
									
										193
									
								
								applications/lfrfid/helpers/protocols/protocol_ioprox.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										193
									
								
								applications/lfrfid/helpers/protocols/protocol_ioprox.cpp
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,193 @@ | ||||
| #include "protocol_ioprox.h" | ||||
| #include <furi.h> | ||||
| #include <cli/cli.h> | ||||
| 
 | ||||
| /**
 | ||||
|  * Writes a bit into the output buffer. | ||||
|  */ | ||||
| static void write_bit(bool bit, uint8_t position, uint8_t* data) { | ||||
|     if(bit) { | ||||
|         data[position / 8] |= 1UL << (7 - (position % 8)); | ||||
|     } else { | ||||
|         data[position / 8] &= ~(1UL << (7 - (position % 8))); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| /**
 | ||||
|  * Writes up to eight contiguous bits into the output buffer. | ||||
|  */ | ||||
| static void write_bits(uint8_t byte, uint8_t position, uint8_t* data, uint8_t length) { | ||||
|     furi_check(length <= 8); | ||||
|     furi_check(length > 0); | ||||
| 
 | ||||
|     for(uint8_t i = 0; i < length; ++i) { | ||||
|         uint8_t shift = 7 - i; | ||||
|         write_bit((byte >> shift) & 1, position + i, data); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| uint8_t ProtocolIoProx::get_encoded_data_size() { | ||||
|     return 8; | ||||
| } | ||||
| 
 | ||||
| uint8_t ProtocolIoProx::get_decoded_data_size() { | ||||
|     return 4; | ||||
| } | ||||
| 
 | ||||
| void ProtocolIoProx::encode( | ||||
|     const uint8_t* decoded_data, | ||||
|     const uint8_t decoded_data_size, | ||||
|     uint8_t* encoded_data, | ||||
|     const uint8_t encoded_data_size) { | ||||
|     furi_check(decoded_data_size >= get_decoded_data_size()); | ||||
|     furi_check(encoded_data_size >= get_encoded_data_size()); | ||||
| 
 | ||||
|     // Packet to transmit:
 | ||||
|     //
 | ||||
|     // 0           10          20          30          40          50          60
 | ||||
|     // v           v           v           v           v           v           v
 | ||||
|     // 01234567 8 90123456 7 89012345 6 78901234 5 67890123 4 56789012 3 45678901 23
 | ||||
|     // -----------------------------------------------------------------------------
 | ||||
|     // 00000000 0 11110000 1 facility 1 version_ 1 code-one 1 code-two 1 checksum 11
 | ||||
| 
 | ||||
|     // Preamble.
 | ||||
|     write_bits(0b00000000, 0, encoded_data, 8); | ||||
|     write_bit(0, 8, encoded_data); | ||||
| 
 | ||||
|     write_bits(0b11110000, 9, encoded_data, 8); | ||||
|     write_bit(1, 17, encoded_data); | ||||
| 
 | ||||
|     // Facility code.
 | ||||
|     write_bits(decoded_data[0], 18, encoded_data, 8); | ||||
|     write_bit(1, 26, encoded_data); | ||||
| 
 | ||||
|     // Version
 | ||||
|     write_bits(decoded_data[1], 27, encoded_data, 8); | ||||
|     write_bit(1, 35, encoded_data); | ||||
| 
 | ||||
|     // Code one
 | ||||
|     write_bits(decoded_data[2], 36, encoded_data, 8); | ||||
|     write_bit(1, 44, encoded_data); | ||||
| 
 | ||||
|     // Code two
 | ||||
|     write_bits(decoded_data[3], 45, encoded_data, 8); | ||||
|     write_bit(1, 53, encoded_data); | ||||
| 
 | ||||
|     // Checksum
 | ||||
|     write_bits(compute_checksum(encoded_data, 8), 54, encoded_data, 8); | ||||
|     write_bit(1, 62, encoded_data); | ||||
|     write_bit(1, 63, encoded_data); | ||||
| } | ||||
| 
 | ||||
| void ProtocolIoProx::decode( | ||||
|     const uint8_t* encoded_data, | ||||
|     const uint8_t encoded_data_size, | ||||
|     uint8_t* decoded_data, | ||||
|     const uint8_t decoded_data_size) { | ||||
|     furi_check(decoded_data_size >= get_decoded_data_size()); | ||||
|     furi_check(encoded_data_size >= get_encoded_data_size()); | ||||
| 
 | ||||
|     // Packet structure:
 | ||||
|     // (Note: the second word seems fixed; but this may not be a guarantee;
 | ||||
|     //  it currently has no meaning.)
 | ||||
|     //
 | ||||
|     //0        1        2        3        4        5        6        7
 | ||||
|     //v        v        v        v        v        v        v        v
 | ||||
|     //01234567 89ABCDEF 01234567 89ABCDEF 01234567 89ABCDEF 01234567 89ABCDEF
 | ||||
|     //-----------------------------------------------------------------------
 | ||||
|     //00000000 01111000 01FFFFFF FF1VVVVV VVV1CCCC CCCC1CCC CCCCC1XX XXXXXX11
 | ||||
|     //
 | ||||
|     // F = facility code
 | ||||
|     // V = version
 | ||||
|     // C = code
 | ||||
|     // X = checksum
 | ||||
| 
 | ||||
|     // Facility code
 | ||||
|     decoded_data[0] = (encoded_data[2] << 2) | (encoded_data[3] >> 6); | ||||
| 
 | ||||
|     // Version code.
 | ||||
|     decoded_data[1] = (encoded_data[3] << 3) | (encoded_data[4] >> 5); | ||||
| 
 | ||||
|     // Code bytes.
 | ||||
|     decoded_data[2] = (encoded_data[4] << 4) | (encoded_data[5] >> 4); | ||||
|     decoded_data[3] = (encoded_data[5] << 5) | (encoded_data[6] >> 3); | ||||
| } | ||||
| 
 | ||||
| bool ProtocolIoProx::can_be_decoded(const uint8_t* encoded_data, const uint8_t encoded_data_size) { | ||||
|     furi_check(encoded_data_size >= get_encoded_data_size()); | ||||
| 
 | ||||
|     // Packet framing
 | ||||
|     //
 | ||||
|     //0        1        2        3        4        5        6        7
 | ||||
|     //v        v        v        v        v        v        v        v
 | ||||
|     //01234567 89ABCDEF 01234567 89ABCDEF 01234567 89ABCDEF 01234567 89ABCDEF
 | ||||
|     //-----------------------------------------------------------------------
 | ||||
|     //00000000 01______ _1______ __1_____ ___1____ ____1___ _____1XX XXXXXX11
 | ||||
|     //
 | ||||
|     // _ = variable data
 | ||||
|     // 0 = preamble 0
 | ||||
|     // 1 = framing 1
 | ||||
|     // X = checksum
 | ||||
| 
 | ||||
|     // Validate the packet preamble is there...
 | ||||
|     if(encoded_data[0] != 0b00000000) { | ||||
|         return false; | ||||
|     } | ||||
|     if((encoded_data[1] >> 6) != 0b01) { | ||||
|         return false; | ||||
|     } | ||||
| 
 | ||||
|     // ... check for known ones...
 | ||||
|     if((encoded_data[2] & 0b01000000) == 0) { | ||||
|         return false; | ||||
|     } | ||||
|     if((encoded_data[3] & 0b00100000) == 0) { | ||||
|         return false; | ||||
|     } | ||||
|     if((encoded_data[4] & 0b00010000) == 0) { | ||||
|         return false; | ||||
|     } | ||||
|     if((encoded_data[5] & 0b00001000) == 0) { | ||||
|         return false; | ||||
|     } | ||||
|     if((encoded_data[6] & 0b00000100) == 0) { | ||||
|         return false; | ||||
|     } | ||||
|     if((encoded_data[7] & 0b00000011) == 0) { | ||||
|         return false; | ||||
|     } | ||||
| 
 | ||||
|     // ... and validate our checksums.
 | ||||
|     uint8_t checksum = compute_checksum(encoded_data, 8); | ||||
|     uint8_t checkval = (encoded_data[6] << 6) | (encoded_data[7] >> 2); | ||||
| 
 | ||||
|     if(checksum != checkval) { | ||||
|         return false; | ||||
|     } | ||||
| 
 | ||||
|     return true; | ||||
| } | ||||
| 
 | ||||
| uint8_t ProtocolIoProx::compute_checksum(const uint8_t* data, const uint8_t data_size) { | ||||
|     furi_check(data_size == get_encoded_data_size()); | ||||
| 
 | ||||
|     // Packet structure:
 | ||||
|     //
 | ||||
|     //0        1        2         3         4         5         6         7
 | ||||
|     //v        v        v         v         v         v         v         v
 | ||||
|     //01234567 8 9ABCDEF0 1 23456789 A BCDEF012 3 456789AB C DEF01234 5 6789ABCD EF
 | ||||
|     //00000000 0 VVVVVVVV 1 WWWWWWWW 1 XXXXXXXX 1 YYYYYYYY 1 ZZZZZZZZ 1 CHECKSUM 11
 | ||||
|     //
 | ||||
|     // algorithm as observed by the proxmark3 folks
 | ||||
|     // CHECKSUM == 0xFF - (V + W + X + Y + Z)
 | ||||
| 
 | ||||
|     uint8_t checksum = 0; | ||||
| 
 | ||||
|     checksum += (data[1] << 1) | (data[2] >> 7); // VVVVVVVVV
 | ||||
|     checksum += (data[2] << 2) | (data[3] >> 6); // WWWWWWWWW
 | ||||
|     checksum += (data[3] << 3) | (data[4] >> 5); // XXXXXXXXX
 | ||||
|     checksum += (data[4] << 4) | (data[5] >> 4); // YYYYYYYYY
 | ||||
|     checksum += (data[5] << 5) | (data[6] >> 3); // ZZZZZZZZZ
 | ||||
| 
 | ||||
|     return 0xFF - checksum; | ||||
| } | ||||
							
								
								
									
										26
									
								
								applications/lfrfid/helpers/protocols/protocol_ioprox.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								applications/lfrfid/helpers/protocols/protocol_ioprox.h
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,26 @@ | ||||
| #pragma once | ||||
| #include "protocol_generic.h" | ||||
| 
 | ||||
| class ProtocolIoProx : public ProtocolGeneric { | ||||
| public: | ||||
|     uint8_t get_encoded_data_size() final; | ||||
|     uint8_t get_decoded_data_size() final; | ||||
| 
 | ||||
|     void encode( | ||||
|         const uint8_t* decoded_data, | ||||
|         const uint8_t decoded_data_size, | ||||
|         uint8_t* encoded_data, | ||||
|         const uint8_t encoded_data_size) final; | ||||
| 
 | ||||
|     void decode( | ||||
|         const uint8_t* encoded_data, | ||||
|         const uint8_t encoded_data_size, | ||||
|         uint8_t* decoded_data, | ||||
|         const uint8_t decoded_data_size) final; | ||||
| 
 | ||||
|     bool can_be_decoded(const uint8_t* encoded_data, const uint8_t encoded_data_size) final; | ||||
| 
 | ||||
| private: | ||||
|     /**  Computes the IoProx checksum of the provided (decoded) data. */ | ||||
|     uint8_t compute_checksum(const uint8_t* data, const uint8_t data_size); | ||||
| }; | ||||
| @ -25,10 +25,12 @@ void RfidReader::decode(bool polarity) { | ||||
|     case Type::Normal: | ||||
|         decoder_em.process_front(polarity, period); | ||||
|         decoder_hid26.process_front(polarity, period); | ||||
|         decoder_ioprox.process_front(polarity, period); | ||||
|         break; | ||||
|     case Type::Indala: | ||||
|         decoder_em.process_front(polarity, period); | ||||
|         decoder_hid26.process_front(polarity, period); | ||||
|         decoder_ioprox.process_front(polarity, period); | ||||
|         decoder_indala.process_front(polarity, period); | ||||
|         break; | ||||
|     } | ||||
| @ -110,6 +112,11 @@ bool RfidReader::read(LfrfidKeyType* _type, uint8_t* data, uint8_t data_size, bo | ||||
|         something_read = true; | ||||
|     } | ||||
| 
 | ||||
|     if(decoder_ioprox.read(data, data_size)) { | ||||
|         *_type = LfrfidKeyType::KeyIoProxXSF; | ||||
|         something_read = true; | ||||
|     } | ||||
| 
 | ||||
|     if(decoder_indala.read(data, data_size)) { | ||||
|         *_type = LfrfidKeyType::KeyI40134; | ||||
|         something_read = true; | ||||
|  | ||||
| @ -4,6 +4,7 @@ | ||||
| #include "decoder_emmarin.h" | ||||
| #include "decoder_hid26.h" | ||||
| #include "decoder_indala.h" | ||||
| #include "decoder_ioprox.h" | ||||
| #include "key_info.h" | ||||
| 
 | ||||
| //#define RFID_GPIO_DEBUG 1
 | ||||
| @ -34,6 +35,7 @@ private: | ||||
|     DecoderEMMarin decoder_em; | ||||
|     DecoderHID26 decoder_hid26; | ||||
|     DecoderIndala decoder_indala; | ||||
|     DecoderIoProx decoder_ioprox; | ||||
| 
 | ||||
|     uint32_t last_dwt_value; | ||||
| 
 | ||||
|  | ||||
| @ -5,6 +5,7 @@ | ||||
| #include "encoder_emmarin.h" | ||||
| #include "encoder_hid_h10301.h" | ||||
| #include "encoder_indala_40134.h" | ||||
| #include "encoder_ioprox.h" | ||||
| #include "pulse_joiner.h" | ||||
| #include <map> | ||||
| 
 | ||||
| @ -22,6 +23,7 @@ private: | ||||
|         {LfrfidKeyType::KeyEM4100, new EncoderEM()}, | ||||
|         {LfrfidKeyType::KeyH10301, new EncoderHID_H10301()}, | ||||
|         {LfrfidKeyType::KeyI40134, new EncoderIndala_40134()}, | ||||
|         {LfrfidKeyType::KeyIoProxXSF, new EncoderIoProx()}, | ||||
|     }; | ||||
| 
 | ||||
|     PulseJoiner pulse_joiner; | ||||
|  | ||||
| @ -84,6 +84,11 @@ void RfidWorker::sq_write() { | ||||
|             writer.write_indala(key.get_data()); | ||||
|             writer.stop(); | ||||
|             break; | ||||
|         case LfrfidKeyType::KeyIoProxXSF: | ||||
|             writer.start(); | ||||
|             writer.write_ioprox(key.get_data()); | ||||
|             writer.stop(); | ||||
|             break; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -92,6 +97,7 @@ void RfidWorker::sq_write_start_validate() { | ||||
|     switch(key.get_type()) { | ||||
|     case LfrfidKeyType::KeyEM4100: | ||||
|     case LfrfidKeyType::KeyH10301: | ||||
|     case LfrfidKeyType::KeyIoProxXSF: | ||||
|         reader.start_forced(RfidReader::Type::Normal); | ||||
|         break; | ||||
|     case LfrfidKeyType::KeyI40134: | ||||
|  | ||||
| @ -1,4 +1,5 @@ | ||||
| #include "rfid_writer.h" | ||||
| #include "protocols/protocol_ioprox.h" | ||||
| #include <furi_hal.h> | ||||
| #include "protocols/protocol_emmarin.h" | ||||
| #include "protocols/protocol_hid_h10301.h" | ||||
| @ -143,6 +144,28 @@ void RfidWriter::write_hid(const uint8_t hid_data[3]) { | ||||
|     FURI_CRITICAL_EXIT(); | ||||
| } | ||||
| 
 | ||||
| /** Endian fixup. Translates an ioprox block into a t5577 block */ | ||||
| static uint32_t ioprox_encode_block(const uint8_t block_data[4]) { | ||||
|     uint8_t raw_card_data[] = {block_data[3], block_data[2], block_data[1], block_data[0]}; | ||||
|     return *reinterpret_cast<uint32_t*>(&raw_card_data); | ||||
| } | ||||
| 
 | ||||
| void RfidWriter::write_ioprox(const uint8_t ioprox_data[4]) { | ||||
|     ProtocolIoProx ioprox_card; | ||||
| 
 | ||||
|     uint8_t encoded_data[8]; | ||||
|     ioprox_card.encode(ioprox_data, 4, encoded_data, sizeof(encoded_data)); | ||||
| 
 | ||||
|     const uint32_t ioprox_config_block_data = 0b00000000000101000111000001000000; | ||||
| 
 | ||||
|     FURI_CRITICAL_ENTER(); | ||||
|     write_block(0, 0, false, ioprox_config_block_data); | ||||
|     write_block(0, 1, false, ioprox_encode_block(&encoded_data[0])); | ||||
|     write_block(0, 2, false, ioprox_encode_block(&encoded_data[4])); | ||||
|     write_reset(); | ||||
|     FURI_CRITICAL_EXIT(); | ||||
| } | ||||
| 
 | ||||
| void RfidWriter::write_indala(const uint8_t indala_data[3]) { | ||||
|     ProtocolIndala40134 indala_card; | ||||
|     uint32_t card_data[2]; | ||||
|  | ||||
| @ -9,6 +9,7 @@ public: | ||||
|     void stop(); | ||||
|     void write_em(const uint8_t em_data[5]); | ||||
|     void write_hid(const uint8_t hid_data[3]); | ||||
|     void write_ioprox(const uint8_t ioprox_data[4]); | ||||
|     void write_indala(const uint8_t indala_data[3]); | ||||
| 
 | ||||
| private: | ||||
|  | ||||
| @ -1,4 +1,7 @@ | ||||
| #include "lfrfid_app.h" | ||||
| #include "assets_icons.h" | ||||
| #include "furi/common_defines.h" | ||||
| #include "m-string.h" | ||||
| #include "scene/lfrfid_app_scene_start.h" | ||||
| #include "scene/lfrfid_app_scene_read.h" | ||||
| #include "scene/lfrfid_app_scene_read_success.h" | ||||
| @ -31,9 +34,11 @@ LfRfidApp::LfRfidApp() | ||||
|     , storage{"storage"} | ||||
|     , dialogs{"dialogs"} | ||||
|     , text_store(40) { | ||||
|     string_init_set_str(file_path, app_folder); | ||||
| } | ||||
| 
 | ||||
| LfRfidApp::~LfRfidApp() { | ||||
|     string_clear(file_path); | ||||
| } | ||||
| 
 | ||||
| void LfRfidApp::run(void* _args) { | ||||
| @ -42,7 +47,8 @@ void LfRfidApp::run(void* _args) { | ||||
|     make_app_folder(); | ||||
| 
 | ||||
|     if(strlen(args)) { | ||||
|         load_key_data(args, &worker.key); | ||||
|         string_set_str(file_path, args); | ||||
|         load_key_data(file_path, &worker.key); | ||||
|         scene_controller.add_scene(SceneType::Emulate, new LfRfidAppSceneEmulate()); | ||||
|         scene_controller.process(100, SceneType::Emulate); | ||||
|     } else { | ||||
| @ -69,65 +75,49 @@ void LfRfidApp::run(void* _args) { | ||||
| } | ||||
| 
 | ||||
| bool LfRfidApp::save_key(RfidKey* key) { | ||||
|     string_t file_name; | ||||
|     bool result = false; | ||||
| 
 | ||||
|     make_app_folder(); | ||||
| 
 | ||||
|     string_init_printf(file_name, "%s/%s%s", app_folder, key->get_name(), app_extension); | ||||
|     result = save_key_data(string_get_cstr(file_name), key); | ||||
|     string_clear(file_name); | ||||
|     if(string_end_with_str_p(file_path, app_extension)) { | ||||
|         size_t filename_start = string_search_rchar(file_path, '/'); | ||||
|         string_left(file_path, filename_start); | ||||
|     } | ||||
| 
 | ||||
|     string_cat_printf(file_path, "/%s%s", key->get_name(), app_extension); | ||||
| 
 | ||||
|     result = save_key_data(file_path, key); | ||||
|     return result; | ||||
| } | ||||
| 
 | ||||
| bool LfRfidApp::load_key_from_file_select(bool need_restore) { | ||||
|     TextStore* filename_ts = new TextStore(64); | ||||
|     bool result = false; | ||||
| 
 | ||||
|     if(need_restore) { | ||||
|         result = dialog_file_select_show( | ||||
|             dialogs, | ||||
|             app_folder, | ||||
|             app_extension, | ||||
|             filename_ts->text, | ||||
|             filename_ts->text_size, | ||||
|             worker.key.get_name()); | ||||
|     } else { | ||||
|         result = dialog_file_select_show( | ||||
|             dialogs, app_folder, app_extension, filename_ts->text, filename_ts->text_size, NULL); | ||||
|     if(!need_restore) { | ||||
|         string_set_str(file_path, app_folder); | ||||
|     } | ||||
| 
 | ||||
|     bool result = dialog_file_browser_show( | ||||
|         dialogs, file_path, file_path, app_extension, true, &I_125_10px, true); | ||||
| 
 | ||||
|     if(result) { | ||||
|         string_t key_str; | ||||
|         string_init_printf(key_str, "%s/%s%s", app_folder, filename_ts->text, app_extension); | ||||
|         result = load_key_data(string_get_cstr(key_str), &worker.key); | ||||
|         string_clear(key_str); | ||||
|         result = load_key_data(file_path, &worker.key); | ||||
|     } | ||||
| 
 | ||||
|     delete filename_ts; | ||||
|     return result; | ||||
| } | ||||
| 
 | ||||
| bool LfRfidApp::delete_key(RfidKey* key) { | ||||
|     string_t file_name; | ||||
|     bool result = false; | ||||
| 
 | ||||
|     string_init_printf(file_name, "%s/%s%s", app_folder, key->get_name(), app_extension); | ||||
|     result = storage_simply_remove(storage, string_get_cstr(file_name)); | ||||
|     string_clear(file_name); | ||||
| 
 | ||||
|     return result; | ||||
|     UNUSED(key); | ||||
|     return storage_simply_remove(storage, string_get_cstr(file_path)); | ||||
| } | ||||
| 
 | ||||
| bool LfRfidApp::load_key_data(const char* path, RfidKey* key) { | ||||
| bool LfRfidApp::load_key_data(string_t path, RfidKey* key) { | ||||
|     FlipperFormat* file = flipper_format_file_alloc(storage); | ||||
|     bool result = false; | ||||
|     string_t str_result; | ||||
|     string_init(str_result); | ||||
| 
 | ||||
|     do { | ||||
|         if(!flipper_format_file_open_existing(file, path)) break; | ||||
|         if(!flipper_format_file_open_existing(file, string_get_cstr(path))) break; | ||||
| 
 | ||||
|         // header
 | ||||
|         uint32_t version; | ||||
| @ -149,7 +139,7 @@ bool LfRfidApp::load_key_data(const char* path, RfidKey* key) { | ||||
|             break; | ||||
|         loaded_key.set_data(key_data, loaded_key.get_type_data_count()); | ||||
| 
 | ||||
|         path_extract_filename_no_ext(path, str_result); | ||||
|         path_extract_filename(path, str_result, true); | ||||
|         loaded_key.set_name(string_get_cstr(str_result)); | ||||
| 
 | ||||
|         *key = loaded_key; | ||||
| @ -166,12 +156,12 @@ bool LfRfidApp::load_key_data(const char* path, RfidKey* key) { | ||||
|     return result; | ||||
| } | ||||
| 
 | ||||
| bool LfRfidApp::save_key_data(const char* path, RfidKey* key) { | ||||
| bool LfRfidApp::save_key_data(string_t path, RfidKey* key) { | ||||
|     FlipperFormat* file = flipper_format_file_alloc(storage); | ||||
|     bool result = false; | ||||
| 
 | ||||
|     do { | ||||
|         if(!flipper_format_file_open_always(file, path)) break; | ||||
|         if(!flipper_format_file_open_always(file, string_get_cstr(path))) break; | ||||
|         if(!flipper_format_write_header_cstr(file, app_filetype, 1)) break; | ||||
|         if(!flipper_format_write_comment_cstr(file, "Key type can be EM4100, H10301 or I40134")) | ||||
|             break; | ||||
|  | ||||
| @ -1,4 +1,5 @@ | ||||
| #pragma once | ||||
| #include "m-string.h" | ||||
| #include <furi.h> | ||||
| #include <furi_hal.h> | ||||
| 
 | ||||
| @ -76,6 +77,8 @@ public: | ||||
| 
 | ||||
|     TextStore text_store; | ||||
| 
 | ||||
|     string_t file_path; | ||||
| 
 | ||||
|     void run(void* args); | ||||
| 
 | ||||
|     static const char* app_folder; | ||||
| @ -86,8 +89,8 @@ public: | ||||
|     bool load_key_from_file_select(bool need_restore); | ||||
|     bool delete_key(RfidKey* key); | ||||
| 
 | ||||
|     bool load_key_data(const char* path, RfidKey* key); | ||||
|     bool save_key_data(const char* path, RfidKey* key); | ||||
|     bool load_key_data(string_t path, RfidKey* key); | ||||
|     bool save_key_data(string_t path, RfidKey* key); | ||||
| 
 | ||||
|     void make_app_folder(); | ||||
| }; | ||||
|  | ||||
| @ -28,6 +28,7 @@ void lfrfid_cli_print_usage() { | ||||
|     printf("\tEM4100, EM-Marin (5 bytes key_data)\r\n"); | ||||
|     printf("\tH10301, HID26 (3 bytes key_data)\r\n"); | ||||
|     printf("\tI40134, Indala (3 bytes key_data)\r\n"); | ||||
|     printf("\tIoProxXSF, IoProx (4 bytes key_data)\r\n"); | ||||
|     printf("\t<key_data> are hex-formatted\r\n"); | ||||
| }; | ||||
| 
 | ||||
| @ -43,6 +44,9 @@ static bool lfrfid_cli_get_key_type(string_t data, LfrfidKeyType* type) { | ||||
|     } else if(string_cmp_str(data, "I40134") == 0 || string_cmp_str(data, "Indala") == 0) { | ||||
|         result = true; | ||||
|         *type = LfrfidKeyType::KeyI40134; | ||||
|     } else if(string_cmp_str(data, "IoProxXSF") == 0 || string_cmp_str(data, "IoProx") == 0) { | ||||
|         result = true; | ||||
|         *type = LfrfidKeyType::KeyIoProxXSF; | ||||
|     } | ||||
| 
 | ||||
|     return result; | ||||
|  | ||||
| @ -50,6 +50,14 @@ void LfRfidAppSceneDeleteConfirm::on_enter(LfRfidApp* app, bool /* need_restore | ||||
|         string_printf( | ||||
|             string_decrypted, "FC: %u    ID: %u", data[0], (uint16_t)((data[1] << 8) | (data[2]))); | ||||
|         break; | ||||
|     case LfrfidKeyType::KeyIoProxXSF: | ||||
|         string_printf( | ||||
|             string_decrypted, | ||||
|             "FC: %u   VC: %u   ID: %u", | ||||
|             data[0], | ||||
|             data[1], | ||||
|             (uint16_t)((data[2] << 8) | (data[3]))); | ||||
|         break; | ||||
|     } | ||||
|     line_3->set_text( | ||||
|         string_get_cstr(string_decrypted), 64, 39, 0, AlignCenter, AlignBottom, FontSecondary); | ||||
|  | ||||
| @ -7,6 +7,7 @@ void LfRfidAppSceneReadSuccess::on_enter(LfRfidApp* app, bool /* need_restore */ | ||||
|     string_init(string[0]); | ||||
|     string_init(string[1]); | ||||
|     string_init(string[2]); | ||||
|     string_init(string[3]); | ||||
| 
 | ||||
|     auto container = app->view_controller.get<ContainerVM>(); | ||||
| 
 | ||||
| @ -25,11 +26,13 @@ void LfRfidAppSceneReadSuccess::on_enter(LfRfidApp* app, bool /* need_restore */ | ||||
|     header->set_text(app->worker.key.get_type_text(), 89, 3, 0, AlignCenter); | ||||
| 
 | ||||
|     auto line_1_text = container->add<StringElement>(); | ||||
|     auto line_2_text = container->add<StringElement>(); | ||||
|     auto line_2l_text = container->add<StringElement>(); | ||||
|     auto line_2r_text = container->add<StringElement>(); | ||||
|     auto line_3_text = container->add<StringElement>(); | ||||
| 
 | ||||
|     auto line_1_value = container->add<StringElement>(); | ||||
|     auto line_2_value = container->add<StringElement>(); | ||||
|     auto line_2l_value = container->add<StringElement>(); | ||||
|     auto line_2r_value = container->add<StringElement>(); | ||||
|     auto line_3_value = container->add<StringElement>(); | ||||
| 
 | ||||
|     const uint8_t* data = app->worker.key.get_data(); | ||||
| @ -37,7 +40,7 @@ void LfRfidAppSceneReadSuccess::on_enter(LfRfidApp* app, bool /* need_restore */ | ||||
|     switch(app->worker.key.get_type()) { | ||||
|     case LfrfidKeyType::KeyEM4100: | ||||
|         line_1_text->set_text("HEX:", 65, 23, 0, AlignRight, AlignBottom, FontSecondary); | ||||
|         line_2_text->set_text("Mod:", 65, 35, 0, AlignRight, AlignBottom, FontSecondary); | ||||
|         line_2l_text->set_text("Mod:", 65, 35, 0, AlignRight, AlignBottom, FontSecondary); | ||||
|         line_3_text->set_text("ID:", 65, 47, 0, AlignRight, AlignBottom, FontSecondary); | ||||
| 
 | ||||
|         for(uint8_t i = 0; i < app->worker.key.get_type_data_count(); i++) { | ||||
| @ -49,7 +52,7 @@ void LfRfidAppSceneReadSuccess::on_enter(LfRfidApp* app, bool /* need_restore */ | ||||
| 
 | ||||
|         line_1_value->set_text( | ||||
|             string_get_cstr(string[0]), 68, 23, 0, AlignLeft, AlignBottom, FontSecondary); | ||||
|         line_2_value->set_text( | ||||
|         line_2l_value->set_text( | ||||
|             string_get_cstr(string[1]), 68, 35, 0, AlignLeft, AlignBottom, FontSecondary); | ||||
|         line_3_value->set_text( | ||||
|             string_get_cstr(string[2]), 68, 47, 0, AlignLeft, AlignBottom, FontSecondary); | ||||
| @ -57,7 +60,7 @@ void LfRfidAppSceneReadSuccess::on_enter(LfRfidApp* app, bool /* need_restore */ | ||||
|     case LfrfidKeyType::KeyH10301: | ||||
|     case LfrfidKeyType::KeyI40134: | ||||
|         line_1_text->set_text("HEX:", 65, 23, 0, AlignRight, AlignBottom, FontSecondary); | ||||
|         line_2_text->set_text("FC:", 65, 35, 0, AlignRight, AlignBottom, FontSecondary); | ||||
|         line_2l_text->set_text("FC:", 65, 35, 0, AlignRight, AlignBottom, FontSecondary); | ||||
|         line_3_text->set_text("Card:", 65, 47, 0, AlignRight, AlignBottom, FontSecondary); | ||||
| 
 | ||||
|         for(uint8_t i = 0; i < app->worker.key.get_type_data_count(); i++) { | ||||
| @ -69,11 +72,36 @@ void LfRfidAppSceneReadSuccess::on_enter(LfRfidApp* app, bool /* need_restore */ | ||||
| 
 | ||||
|         line_1_value->set_text( | ||||
|             string_get_cstr(string[0]), 68, 23, 0, AlignLeft, AlignBottom, FontSecondary); | ||||
|         line_2_value->set_text( | ||||
|         line_2l_value->set_text( | ||||
|             string_get_cstr(string[1]), 68, 35, 0, AlignLeft, AlignBottom, FontSecondary); | ||||
|         line_3_value->set_text( | ||||
|             string_get_cstr(string[2]), 68, 47, 0, AlignLeft, AlignBottom, FontSecondary); | ||||
|         break; | ||||
| 
 | ||||
|     case LfrfidKeyType::KeyIoProxXSF: | ||||
|         line_1_text->set_text("HEX:", 65, 23, 0, AlignRight, AlignBottom, FontSecondary); | ||||
|         line_2l_text->set_text("FC:", 65, 35, 0, AlignRight, AlignBottom, FontSecondary); | ||||
|         line_2r_text->set_text("VС:", 95, 35, 0, AlignRight, AlignBottom, FontSecondary); | ||||
|         line_3_text->set_text("Card:", 65, 47, 0, AlignRight, AlignBottom, FontSecondary); | ||||
| 
 | ||||
|         for(uint8_t i = 0; i < app->worker.key.get_type_data_count(); i++) { | ||||
|             string_cat_printf(string[0], "%02X", data[i]); | ||||
|         } | ||||
| 
 | ||||
|         string_printf(string[1], "%u", data[0]); | ||||
|         string_printf(string[2], "%u", (uint16_t)((data[2] << 8) | (data[3]))); | ||||
|         string_printf(string[3], "%u", data[1]); | ||||
| 
 | ||||
|         line_1_value->set_text( | ||||
|             string_get_cstr(string[0]), 68, 23, 0, AlignLeft, AlignBottom, FontSecondary); | ||||
|         line_2l_value->set_text( | ||||
|             string_get_cstr(string[1]), 68, 35, 0, AlignLeft, AlignBottom, FontSecondary); | ||||
|         line_2r_value->set_text( | ||||
|             string_get_cstr(string[3]), 98, 35, 0, AlignLeft, AlignBottom, FontSecondary); | ||||
|         line_3_value->set_text( | ||||
|             string_get_cstr(string[2]), 68, 47, 0, AlignLeft, AlignBottom, FontSecondary); | ||||
| 
 | ||||
|         break; | ||||
|     } | ||||
| 
 | ||||
|     app->view_controller.switch_to<ContainerVM>(); | ||||
|  | ||||
| @ -1,23 +1,10 @@ | ||||
| #include "lfrfid_app_scene_save_data.h" | ||||
| #include <dolphin/dolphin.h> | ||||
| 
 | ||||
| static void print_buffer(const uint8_t* buffer) { | ||||
|     for(uint8_t i = 0; i < LFRFID_KEY_SIZE; i++) { | ||||
|         printf("%02X", buffer[i]); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| void LfRfidAppSceneSaveData::on_enter(LfRfidApp* app, bool need_restore) { | ||||
|     auto byte_input = app->view_controller.get<ByteInputVM>(); | ||||
|     RfidKey& key = app->worker.key; | ||||
| 
 | ||||
|     printf("k: "); | ||||
|     print_buffer(key.get_data()); | ||||
|     printf(" o: "); | ||||
|     print_buffer(old_key_data); | ||||
|     printf(" n: "); | ||||
|     print_buffer(new_key_data); | ||||
|     printf("\r\n"); | ||||
|     if(need_restore) printf("restored\r\n"); | ||||
| 
 | ||||
|     if(need_restore) { | ||||
|  | ||||
| @ -1,11 +1,14 @@ | ||||
| #include "lfrfid_app_scene_save_name.h" | ||||
| #include "m-string.h" | ||||
| #include <lib/toolbox/random_name.h> | ||||
| #include <lib/toolbox/path.h> | ||||
| 
 | ||||
| void LfRfidAppSceneSaveName::on_enter(LfRfidApp* app, bool /* need_restore */) { | ||||
|     const char* key_name = app->worker.key.get_name(); | ||||
| 
 | ||||
|     bool key_name_empty = !strcmp(key_name, ""); | ||||
|     if(key_name_empty) { | ||||
|         string_set_str(app->file_path, app->app_folder); | ||||
|         set_random_name(app->text_store.text, app->text_store.text_size); | ||||
|     } else { | ||||
|         app->text_store.set("%s", key_name); | ||||
| @ -21,10 +24,17 @@ void LfRfidAppSceneSaveName::on_enter(LfRfidApp* app, bool /* need_restore */) { | ||||
|         app->worker.key.get_name_length(), | ||||
|         key_name_empty); | ||||
| 
 | ||||
|     string_t folder_path; | ||||
|     string_init(folder_path); | ||||
| 
 | ||||
|     path_extract_dirname(string_get_cstr(app->file_path), folder_path); | ||||
| 
 | ||||
|     ValidatorIsFile* validator_is_file = | ||||
|         validator_is_file_alloc_init(app->app_folder, app->app_extension, key_name); | ||||
|         validator_is_file_alloc_init(string_get_cstr(folder_path), app->app_extension, key_name); | ||||
|     text_input->set_validator(validator_is_file_callback, validator_is_file); | ||||
| 
 | ||||
|     string_clear(folder_path); | ||||
| 
 | ||||
|     app->view_controller.switch_to<TextInputVM>(); | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -10,6 +10,6 @@ public: | ||||
| private: | ||||
|     static void submenu_callback(void* context, uint32_t index); | ||||
|     uint32_t submenu_item_selected = 0; | ||||
|     static const uint8_t keys_count = static_cast<uint8_t>(LfrfidKeyType::KeyI40134); | ||||
|     static const uint8_t keys_count = static_cast<uint8_t>(LfrfidKeyType::KeyIoProxXSF); | ||||
|     string_t submenu_name[keys_count + 1]; | ||||
| }; | ||||
|  | ||||
| @ -43,6 +43,14 @@ void LfRfidAppSceneSavedInfo::on_enter(LfRfidApp* app, bool /* need_restore */) | ||||
|         string_printf( | ||||
|             string_decrypted, "FC: %u    ID: %u", data[0], (uint16_t)((data[1] << 8) | (data[2]))); | ||||
|         break; | ||||
|     case LfrfidKeyType::KeyIoProxXSF: | ||||
|         string_printf( | ||||
|             string_decrypted, | ||||
|             "FC: %u   VC: %u   ID: %u", | ||||
|             data[0], | ||||
|             data[1], | ||||
|             (uint16_t)((data[2] << 8) | (data[3]))); | ||||
|         break; | ||||
|     } | ||||
|     line_3->set_text( | ||||
|         string_get_cstr(string_decrypted), 64, 39, 0, AlignCenter, AlignBottom, FontSecondary); | ||||
|  | ||||
| @ -1,3 +1,5 @@ | ||||
| #include "assets_icons.h" | ||||
| #include "m-string.h" | ||||
| #include <furi.h> | ||||
| #include <furi_hal.h> | ||||
| 
 | ||||
| @ -298,23 +300,23 @@ int32_t music_player_app(void* p) { | ||||
|         if(p) { | ||||
|             string_cat_str(file_path, p); | ||||
|         } else { | ||||
|             char file_name[256] = {0}; | ||||
|             string_set_str(file_path, MUSIC_PLAYER_APP_PATH_FOLDER); | ||||
| 
 | ||||
|             DialogsApp* dialogs = furi_record_open("dialogs"); | ||||
|             bool res = dialog_file_select_show( | ||||
|             bool res = dialog_file_browser_show( | ||||
|                 dialogs, | ||||
|                 MUSIC_PLAYER_APP_PATH_FOLDER, | ||||
|                 file_path, | ||||
|                 file_path, | ||||
|                 MUSIC_PLAYER_APP_EXTENSION, | ||||
|                 file_name, | ||||
|                 255, | ||||
|                 NULL); | ||||
|                 true, | ||||
|                 &I_music_10px, | ||||
|                 false); | ||||
| 
 | ||||
|             furi_record_close("dialogs"); | ||||
|             if(!res) { | ||||
|                 FURI_LOG_E(TAG, "No file selected"); | ||||
|                 break; | ||||
|             } | ||||
|             string_cat_str(file_path, MUSIC_PLAYER_APP_PATH_FOLDER); | ||||
|             string_cat_str(file_path, "/"); | ||||
|             string_cat_str(file_path, file_name); | ||||
|         } | ||||
| 
 | ||||
|         if(!music_player_worker_load(music_player->worker, string_get_cstr(file_path))) { | ||||
|  | ||||
| @ -59,9 +59,10 @@ static int32_t music_player_worker_thread_callback(void* context) { | ||||
|             float frequency = NOTE_C4 * powf(TWO_POW_TWELTH_ROOT, note_from_a4); | ||||
|             float duration = | ||||
|                 60.0 * osKernelGetTickFreq() * 4 / instance->bpm / note_block->duration; | ||||
|             while(note_block->dots > 0) { | ||||
|             uint32_t dots = note_block->dots; | ||||
|             while(dots > 0) { | ||||
|                 duration += duration / 2; | ||||
|                 note_block->dots--; | ||||
|                 dots--; | ||||
|             } | ||||
|             uint32_t next_tick = furi_hal_get_tick() + duration; | ||||
|             float volume = instance->volume; | ||||
|  | ||||
| @ -173,6 +173,8 @@ int32_t nfc_app(void* p) { | ||||
|         if(nfc_device_load(nfc->dev, p)) { | ||||
|             if(nfc->dev->format == NfcDeviceSaveFormatMifareUl) { | ||||
|                 scene_manager_next_scene(nfc->scene_manager, NfcSceneEmulateMifareUl); | ||||
|             } else if(nfc->dev->format == NfcDeviceSaveFormatMifareClassic) { | ||||
|                 scene_manager_next_scene(nfc->scene_manager, NfcSceneEmulateMifareClassic); | ||||
|             } else { | ||||
|                 scene_manager_next_scene(nfc->scene_manager, NfcSceneEmulateUid); | ||||
|             } | ||||
|  | ||||
| @ -1,4 +1,6 @@ | ||||
| #include "nfc_device.h" | ||||
| #include "assets_icons.h" | ||||
| #include "m-string.h" | ||||
| #include "nfc_types.h" | ||||
| 
 | ||||
| #include <toolbox/path.h> | ||||
| @ -7,10 +9,14 @@ | ||||
| static const char* nfc_file_header = "Flipper NFC device"; | ||||
| static const uint32_t nfc_file_version = 2; | ||||
| 
 | ||||
| // Protocols format versions
 | ||||
| static const uint32_t nfc_mifare_classic_data_format_version = 1; | ||||
| 
 | ||||
| NfcDevice* nfc_device_alloc() { | ||||
|     NfcDevice* nfc_dev = malloc(sizeof(NfcDevice)); | ||||
|     nfc_dev->storage = furi_record_open("storage"); | ||||
|     nfc_dev->dialogs = furi_record_open("dialogs"); | ||||
|     string_init(nfc_dev->load_path); | ||||
|     return nfc_dev; | ||||
| } | ||||
| 
 | ||||
| @ -19,6 +25,7 @@ void nfc_device_free(NfcDevice* nfc_dev) { | ||||
|     nfc_device_clear(nfc_dev); | ||||
|     furi_record_close("storage"); | ||||
|     furi_record_close("dialogs"); | ||||
|     string_clear(nfc_dev->load_path); | ||||
|     free(nfc_dev); | ||||
| } | ||||
| 
 | ||||
| @ -192,6 +199,10 @@ static bool nfc_device_save_mifare_df_key_settings( | ||||
|         string_printf(key, "%s Key Changeable", prefix); | ||||
|         if(!flipper_format_write_bool(file, string_get_cstr(key), &ks->master_key_changeable, 1)) | ||||
|             break; | ||||
|         if(ks->flags) { | ||||
|             string_printf(key, "%s Flags", prefix); | ||||
|             if(!flipper_format_write_hex(file, string_get_cstr(key), &ks->flags, 1)) break; | ||||
|         } | ||||
|         string_printf(key, "%s Max Keys", prefix); | ||||
|         if(!flipper_format_write_hex(file, string_get_cstr(key), &ks->max_keys, 1)) break; | ||||
|         for(MifareDesfireKeyVersion* kv = ks->key_version_head; kv; kv = kv->next) { | ||||
| @ -227,8 +238,14 @@ bool nfc_device_load_mifare_df_key_settings( | ||||
|         string_printf(key, "%s Key Changeable", prefix); | ||||
|         if(!flipper_format_read_bool(file, string_get_cstr(key), &ks->master_key_changeable, 1)) | ||||
|             break; | ||||
|         string_printf(key, "%s Flags", prefix); | ||||
|         if(flipper_format_key_exist(file, string_get_cstr(key))) { | ||||
|             if(!flipper_format_read_hex(file, string_get_cstr(key), &ks->flags, 1)) break; | ||||
|         } | ||||
|         string_printf(key, "%s Max Keys", prefix); | ||||
|         if(!flipper_format_read_hex(file, string_get_cstr(key), &ks->max_keys, 1)) break; | ||||
|         ks->flags |= ks->max_keys >> 4; | ||||
|         ks->max_keys &= 0xF; | ||||
|         MifareDesfireKeyVersion** kv_head = &ks->key_version_head; | ||||
|         for(int key_id = 0; key_id < ks->max_keys; key_id++) { | ||||
|             string_printf(key, "%s Key %d Version", prefix, key_id); | ||||
| @ -624,6 +641,7 @@ static bool nfc_device_save_mifare_classic_data(FlipperFormat* file, NfcDevice* | ||||
|     // Save Mifare Classic specific data
 | ||||
|     do { | ||||
|         if(!flipper_format_write_comment_cstr(file, "Mifare Classic specific data")) break; | ||||
| 
 | ||||
|         if(data->type == MfClassicType1k) { | ||||
|             if(!flipper_format_write_string_cstr(file, "Mifare Classic type", "1K")) break; | ||||
|             blocks = 64; | ||||
| @ -631,8 +649,17 @@ static bool nfc_device_save_mifare_classic_data(FlipperFormat* file, NfcDevice* | ||||
|             if(!flipper_format_write_string_cstr(file, "Mifare Classic type", "4K")) break; | ||||
|             blocks = 256; | ||||
|         } | ||||
|         if(!flipper_format_write_comment_cstr(file, "Mifare Classic blocks")) break; | ||||
|         if(!flipper_format_write_uint32( | ||||
|                file, "Data format version", &nfc_mifare_classic_data_format_version, 1)) | ||||
|             break; | ||||
| 
 | ||||
|         if(!flipper_format_write_comment_cstr( | ||||
|                file, "Key map is the bit mask indicating valid key in each sector")) | ||||
|             break; | ||||
|         if(!flipper_format_write_hex_uint64(file, "Key A map", &data->key_a_mask, 1)) break; | ||||
|         if(!flipper_format_write_hex_uint64(file, "Key B map", &data->key_b_mask, 1)) break; | ||||
| 
 | ||||
|         if(!flipper_format_write_comment_cstr(file, "Mifare Classic blocks")) break; | ||||
|         bool block_saved = true; | ||||
|         for(size_t i = 0; i < blocks; i++) { | ||||
|             string_printf(temp_str, "Block %d", i); | ||||
| @ -654,6 +681,7 @@ static bool nfc_device_load_mifare_classic_data(FlipperFormat* file, NfcDevice* | ||||
|     bool parsed = false; | ||||
|     MfClassicData* data = &dev->dev_data.mf_classic_data; | ||||
|     string_t temp_str; | ||||
|     uint32_t data_format_version = 0; | ||||
|     string_init(temp_str); | ||||
|     uint16_t data_blocks = 0; | ||||
| 
 | ||||
| @ -669,6 +697,19 @@ static bool nfc_device_load_mifare_classic_data(FlipperFormat* file, NfcDevice* | ||||
|         } else { | ||||
|             break; | ||||
|         } | ||||
| 
 | ||||
|         // Read Mifare Classic format version
 | ||||
|         if(!flipper_format_read_uint32(file, "Data format version", &data_format_version, 1)) { | ||||
|             // Load unread sectors with zero keys access for backward compatability
 | ||||
|             if(!flipper_format_rewind(file)) break; | ||||
|             data->key_a_mask = 0xffffffffffffffff; | ||||
|             data->key_b_mask = 0xffffffffffffffff; | ||||
|         } else { | ||||
|             if(data_format_version != nfc_mifare_classic_data_format_version) break; | ||||
|             if(!flipper_format_read_hex_uint64(file, "Key A map", &data->key_a_mask, 1)) break; | ||||
|             if(!flipper_format_read_hex_uint64(file, "Key B map", &data->key_b_mask, 1)) break; | ||||
|         } | ||||
| 
 | ||||
|         // Read Mifare Classic blocks
 | ||||
|         bool block_read = true; | ||||
|         for(size_t i = 0; i < data_blocks; i++) { | ||||
| @ -693,11 +734,24 @@ void nfc_device_set_name(NfcDevice* dev, const char* name) { | ||||
|     strlcpy(dev->dev_name, name, NFC_DEV_NAME_MAX_LEN); | ||||
| } | ||||
| 
 | ||||
| static void nfc_device_get_path_without_ext(string_t orig_path, string_t shadow_path) { | ||||
|     // TODO: this won't work if there is ".nfc" anywhere in the path other than
 | ||||
|     // at the end
 | ||||
|     size_t ext_start = string_search_str(orig_path, NFC_APP_EXTENSION); | ||||
|     string_set_n(shadow_path, orig_path, 0, ext_start); | ||||
| } | ||||
| 
 | ||||
| static void nfc_device_get_shadow_path(string_t orig_path, string_t shadow_path) { | ||||
|     nfc_device_get_path_without_ext(orig_path, shadow_path); | ||||
|     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) { | ||||
|     const char* extension, | ||||
|     bool use_load_path) { | ||||
|     furi_assert(dev); | ||||
| 
 | ||||
|     bool saved = false; | ||||
| @ -707,10 +761,19 @@ static bool nfc_device_save_file( | ||||
|     string_init(temp_str); | ||||
| 
 | ||||
|     do { | ||||
|         if(use_load_path && !string_empty_p(dev->load_path)) { | ||||
|             // Get directory name
 | ||||
|             path_extract_dirname(string_get_cstr(dev->load_path), temp_str); | ||||
|             // Create nfc directory if necessary
 | ||||
|             if(!storage_simply_mkdir(dev->storage, string_get_cstr(temp_str))) break; | ||||
|             // Make path to file to save
 | ||||
|             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
 | ||||
|             string_printf(temp_str, "%s/%s%s", folder, dev_name, extension); | ||||
|         } | ||||
|         // Open file
 | ||||
|         if(!flipper_format_file_open_always(file, string_get_cstr(temp_str))) break; | ||||
|         // Write header
 | ||||
| @ -749,12 +812,12 @@ static bool nfc_device_save_file( | ||||
| } | ||||
| 
 | ||||
| bool nfc_device_save(NfcDevice* dev, const char* dev_name) { | ||||
|     return nfc_device_save_file(dev, dev_name, NFC_APP_FOLDER, NFC_APP_EXTENSION); | ||||
|     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) { | ||||
|     dev->shadow_file_exist = true; | ||||
|     return nfc_device_save_file(dev, dev_name, NFC_APP_FOLDER, NFC_APP_SHADOW_EXTENSION); | ||||
|     return nfc_device_save_file(dev, dev_name, NFC_APP_FOLDER, NFC_APP_SHADOW_EXTENSION, true); | ||||
| } | ||||
| 
 | ||||
| static bool nfc_device_load_data(NfcDevice* dev, string_t path) { | ||||
| @ -768,9 +831,7 @@ static bool nfc_device_load_data(NfcDevice* dev, string_t path) { | ||||
| 
 | ||||
|     do { | ||||
|         // Check existance of shadow file
 | ||||
|         size_t ext_start = string_search_str(path, NFC_APP_EXTENSION); | ||||
|         string_set_n(temp_str, path, 0, ext_start); | ||||
|         string_cat_printf(temp_str, "%s", NFC_APP_SHADOW_EXTENSION); | ||||
|         nfc_device_get_shadow_path(path, temp_str); | ||||
|         dev->shadow_file_exist = | ||||
|             storage_common_stat(dev->storage, string_get_cstr(temp_str), NULL) == FSE_OK; | ||||
|         // Open shadow file if it exists. If not - open original
 | ||||
| @ -827,15 +888,16 @@ bool nfc_device_load(NfcDevice* dev, const char* file_path) { | ||||
|     furi_assert(file_path); | ||||
| 
 | ||||
|     // Load device data
 | ||||
|     string_t path; | ||||
|     string_init_set_str(path, file_path); | ||||
|     bool dev_load = nfc_device_load_data(dev, path); | ||||
|     string_set_str(dev->load_path, file_path); | ||||
|     bool dev_load = nfc_device_load_data(dev, dev->load_path); | ||||
|     if(dev_load) { | ||||
|         // Set device name
 | ||||
|         path_extract_filename_no_ext(file_path, path); | ||||
|         nfc_device_set_name(dev, string_get_cstr(path)); | ||||
|         string_t filename; | ||||
|         string_init(filename); | ||||
|         path_extract_filename_no_ext(file_path, filename); | ||||
|         nfc_device_set_name(dev, string_get_cstr(filename)); | ||||
|         string_clear(filename); | ||||
|     } | ||||
|     string_clear(path); | ||||
| 
 | ||||
|     return dev_load; | ||||
| } | ||||
| @ -843,23 +905,22 @@ bool nfc_device_load(NfcDevice* dev, const char* file_path) { | ||||
| bool nfc_file_select(NfcDevice* dev) { | ||||
|     furi_assert(dev); | ||||
| 
 | ||||
|     // Input events and views are managed by file_select
 | ||||
|     bool res = dialog_file_select_show( | ||||
|         dev->dialogs, | ||||
|         NFC_APP_FOLDER, | ||||
|         NFC_APP_EXTENSION, | ||||
|         dev->file_name, | ||||
|         sizeof(dev->file_name), | ||||
|         dev->dev_name); | ||||
|     // Input events and views are managed by file_browser
 | ||||
|     string_t nfc_app_folder; | ||||
|     string_init_set_str(nfc_app_folder, NFC_APP_FOLDER); | ||||
|     bool res = dialog_file_browser_show( | ||||
|         dev->dialogs, dev->load_path, nfc_app_folder, NFC_APP_EXTENSION, true, &I_Nfc_10px, true); | ||||
|     string_clear(nfc_app_folder); | ||||
|     if(res) { | ||||
|         string_t dev_str; | ||||
|         // Get key file path
 | ||||
|         string_init_printf(dev_str, "%s/%s%s", NFC_APP_FOLDER, dev->file_name, NFC_APP_EXTENSION); | ||||
|         res = nfc_device_load_data(dev, dev_str); | ||||
|         string_t filename; | ||||
|         string_init(filename); | ||||
|         path_extract_filename(dev->load_path, filename, true); | ||||
|         strncpy(dev->dev_name, string_get_cstr(filename), NFC_DEV_NAME_MAX_LEN); | ||||
|         res = nfc_device_load_data(dev, dev->load_path); | ||||
|         if(res) { | ||||
|             nfc_device_set_name(dev, dev->file_name); | ||||
|             nfc_device_set_name(dev, dev->dev_name); | ||||
|         } | ||||
|         string_clear(dev_str); | ||||
|         string_clear(filename); | ||||
|     } | ||||
| 
 | ||||
|     return res; | ||||
| @ -877,9 +938,10 @@ void nfc_device_clear(NfcDevice* dev) { | ||||
|     nfc_device_data_clear(&dev->dev_data); | ||||
|     memset(&dev->dev_data, 0, sizeof(dev->dev_data)); | ||||
|     dev->format = NfcDeviceSaveFormatUid; | ||||
|     string_reset(dev->load_path); | ||||
| } | ||||
| 
 | ||||
| bool nfc_device_delete(NfcDevice* dev) { | ||||
| bool nfc_device_delete(NfcDevice* dev, bool use_load_path) { | ||||
|     furi_assert(dev); | ||||
| 
 | ||||
|     bool deleted = false; | ||||
| @ -888,12 +950,20 @@ bool nfc_device_delete(NfcDevice* dev) { | ||||
| 
 | ||||
|     do { | ||||
|         // Delete original file
 | ||||
|         string_init_printf(file_path, "%s/%s%s", NFC_APP_FOLDER, dev->dev_name, NFC_APP_EXTENSION); | ||||
|         if(use_load_path && !string_empty_p(dev->load_path)) { | ||||
|             string_set(file_path, dev->load_path); | ||||
|         } else { | ||||
|             string_printf(file_path, "%s/%s%s", NFC_APP_FOLDER, dev->dev_name, NFC_APP_EXTENSION); | ||||
|         } | ||||
|         if(!storage_simply_remove(dev->storage, string_get_cstr(file_path))) break; | ||||
|         // Delete shadow file if it exists
 | ||||
|         if(dev->shadow_file_exist) { | ||||
|             if(use_load_path && !string_empty_p(dev->load_path)) { | ||||
|                 nfc_device_get_shadow_path(dev->load_path, file_path); | ||||
|             } else { | ||||
|                 string_printf( | ||||
|                     file_path, "%s/%s%s", NFC_APP_FOLDER, dev->dev_name, NFC_APP_SHADOW_EXTENSION); | ||||
|             } | ||||
|             if(!storage_simply_remove(dev->storage, string_get_cstr(file_path))) break; | ||||
|         } | ||||
|         deleted = true; | ||||
| @ -907,19 +977,29 @@ bool nfc_device_delete(NfcDevice* dev) { | ||||
|     return deleted; | ||||
| } | ||||
| 
 | ||||
| bool nfc_device_restore(NfcDevice* dev) { | ||||
| bool nfc_device_restore(NfcDevice* dev, bool use_load_path) { | ||||
|     furi_assert(dev); | ||||
|     furi_assert(dev->shadow_file_exist); | ||||
| 
 | ||||
|     bool restored = false; | ||||
|     string_t path; | ||||
| 
 | ||||
|     string_init(path); | ||||
| 
 | ||||
|     do { | ||||
|         string_init_printf( | ||||
|         if(use_load_path && !string_empty_p(dev->load_path)) { | ||||
|             nfc_device_get_shadow_path(dev->load_path, path); | ||||
|         } else { | ||||
|             string_printf( | ||||
|                 path, "%s/%s%s", NFC_APP_FOLDER, dev->dev_name, NFC_APP_SHADOW_EXTENSION); | ||||
|         } | ||||
|         if(!storage_simply_remove(dev->storage, string_get_cstr(path))) break; | ||||
|         dev->shadow_file_exist = false; | ||||
|         if(use_load_path && !string_empty_p(dev->load_path)) { | ||||
|             string_set(path, dev->load_path); | ||||
|         } else { | ||||
|             string_printf(path, "%s/%s%s", NFC_APP_FOLDER, dev->dev_name, NFC_APP_EXTENSION); | ||||
|         } | ||||
|         if(!nfc_device_load_data(dev, path)) break; | ||||
|         restored = true; | ||||
|     } while(0); | ||||
|  | ||||
| @ -12,7 +12,6 @@ | ||||
| #include <lib/nfc_protocols/mifare_desfire.h> | ||||
| 
 | ||||
| #define NFC_DEV_NAME_MAX_LEN 22 | ||||
| #define NFC_FILE_NAME_MAX_LEN 120 | ||||
| #define NFC_READER_DATA_MAX_SIZE 64 | ||||
| 
 | ||||
| #define NFC_APP_FOLDER "/any/nfc" | ||||
| @ -57,7 +56,7 @@ typedef struct { | ||||
|     DialogsApp* dialogs; | ||||
|     NfcDeviceData dev_data; | ||||
|     char dev_name[NFC_DEV_NAME_MAX_LEN + 1]; | ||||
|     char file_name[NFC_FILE_NAME_MAX_LEN]; | ||||
|     string_t load_path; | ||||
|     NfcDeviceSaveFormat format; | ||||
|     bool shadow_file_exist; | ||||
| } NfcDevice; | ||||
| @ -80,6 +79,6 @@ void nfc_device_data_clear(NfcDeviceData* dev); | ||||
| 
 | ||||
| void nfc_device_clear(NfcDevice* dev); | ||||
| 
 | ||||
| bool nfc_device_delete(NfcDevice* dev); | ||||
| bool nfc_device_delete(NfcDevice* dev, bool use_load_path); | ||||
| 
 | ||||
| bool nfc_device_restore(NfcDevice* dev); | ||||
| bool nfc_device_restore(NfcDevice* dev, bool use_load_path); | ||||
|  | ||||
| @ -35,6 +35,14 @@ const char* nfc_mf_ul_type(MfUltralightType type, bool full_name) { | ||||
|         return "NTAG215"; | ||||
|     } else if(type == MfUltralightTypeNTAG216) { | ||||
|         return "NTAG216"; | ||||
|     } else if(type == MfUltralightTypeNTAGI2C1K) { | ||||
|         return "NTAG I2C 1K"; | ||||
|     } else if(type == MfUltralightTypeNTAGI2C2K) { | ||||
|         return "NTAG I2C 2K"; | ||||
|     } else if(type == MfUltralightTypeNTAGI2CPlus1K) { | ||||
|         return "NTAG I2C Plus 1K"; | ||||
|     } else if(type == MfUltralightTypeNTAGI2CPlus2K) { | ||||
|         return "NTAG I2C Plus 2K"; | ||||
|     } else if(type == MfUltralightTypeUL11 && full_name) { | ||||
|         return "Mifare Ultralight 11"; | ||||
|     } else if(type == MfUltralightTypeUL21 && full_name) { | ||||
|  | ||||
| @ -7,6 +7,7 @@ | ||||
| #include <lib/nfc_protocols/mifare_ultralight.h> | ||||
| #include <lib/nfc_protocols/mifare_classic.h> | ||||
| #include <lib/nfc_protocols/mifare_desfire.h> | ||||
| #include <lib/nfc_protocols/nfca.h> | ||||
| 
 | ||||
| #include "helpers/nfc_mf_classic_dict.h" | ||||
| 
 | ||||
| @ -104,6 +105,8 @@ int32_t nfc_worker_task(void* context) { | ||||
|         nfc_worker_emulate_mifare_ul(nfc_worker); | ||||
|     } else if(nfc_worker->state == NfcWorkerStateReadMifareClassic) { | ||||
|         nfc_worker_mifare_classic_dict_attack(nfc_worker); | ||||
|     } else if(nfc_worker->state == NfcWorkerStateEmulateMifareClassic) { | ||||
|         nfc_worker_emulate_mifare_classic(nfc_worker); | ||||
|     } else if(nfc_worker->state == NfcWorkerStateReadMifareDesfire) { | ||||
|         nfc_worker_read_mifare_desfire(nfc_worker); | ||||
|     } | ||||
| @ -312,6 +315,11 @@ void nfc_worker_emulate_mifare_ul(NfcWorker* nfc_worker) { | ||||
|     MfUltralightEmulator emulator = {}; | ||||
|     mf_ul_prepare_emulation(&emulator, &nfc_worker->dev_data->mf_ul_data); | ||||
|     while(nfc_worker->state == NfcWorkerStateEmulateMifareUltralight) { | ||||
|         emulator.auth_success = false; | ||||
|         if(emulator.data.type >= MfUltralightTypeNTAGI2C1K) { | ||||
|             // Sector index needs to be reset
 | ||||
|             emulator.curr_sector = 0; | ||||
|         } | ||||
|         furi_hal_nfc_emulate_nfca( | ||||
|             nfc_data->uid, | ||||
|             nfc_data->uid_len, | ||||
| @ -474,6 +482,34 @@ void nfc_worker_mifare_classic_dict_attack(NfcWorker* nfc_worker) { | ||||
|     stream_free(nfc_worker->dict_stream); | ||||
| } | ||||
| 
 | ||||
| void nfc_worker_emulate_mifare_classic(NfcWorker* nfc_worker) { | ||||
|     FuriHalNfcTxRxContext tx_rx; | ||||
|     FuriHalNfcDevData* nfc_data = &nfc_worker->dev_data->nfc_data; | ||||
|     MfClassicEmulator emulator = { | ||||
|         .cuid = nfc_util_bytes2num(&nfc_data->uid[nfc_data->uid_len - 4], 4), | ||||
|         .data = nfc_worker->dev_data->mf_classic_data, | ||||
|         .data_changed = false, | ||||
|     }; | ||||
|     NfcaSignal* nfca_signal = nfca_signal_alloc(); | ||||
|     tx_rx.nfca_signal = nfca_signal; | ||||
| 
 | ||||
|     while(nfc_worker->state == NfcWorkerStateEmulateMifareClassic) { | ||||
|         if(furi_hal_nfc_listen( | ||||
|                nfc_data->uid, nfc_data->uid_len, nfc_data->atqa, nfc_data->sak, true, 300)) { | ||||
|             mf_classic_emulator(&emulator, &tx_rx); | ||||
|         } | ||||
|     } | ||||
|     if(emulator.data_changed) { | ||||
|         nfc_worker->dev_data->mf_classic_data = emulator.data; | ||||
|         if(nfc_worker->callback) { | ||||
|             nfc_worker->callback(NfcWorkerEventSuccess, nfc_worker->context); | ||||
|         } | ||||
|         emulator.data_changed = false; | ||||
|     } | ||||
| 
 | ||||
|     nfca_signal_free(nfca_signal); | ||||
| } | ||||
| 
 | ||||
| void nfc_worker_read_mifare_desfire(NfcWorker* nfc_worker) { | ||||
|     ReturnCode err; | ||||
|     uint8_t tx_buff[64] = {}; | ||||
|  | ||||
| @ -19,6 +19,7 @@ typedef enum { | ||||
|     NfcWorkerStateReadMifareUltralight, | ||||
|     NfcWorkerStateEmulateMifareUltralight, | ||||
|     NfcWorkerStateReadMifareClassic, | ||||
|     NfcWorkerStateEmulateMifareClassic, | ||||
|     NfcWorkerStateReadMifareDesfire, | ||||
|     // Transition
 | ||||
|     NfcWorkerStateStop, | ||||
|  | ||||
| @ -34,4 +34,6 @@ ADD_SCENE(nfc, restore_original, RestoreOriginal) | ||||
| ADD_SCENE(nfc, debug, Debug) | ||||
| ADD_SCENE(nfc, field, Field) | ||||
| ADD_SCENE(nfc, read_mifare_classic, ReadMifareClassic) | ||||
| ADD_SCENE(nfc, emulate_mifare_classic, EmulateMifareClassic) | ||||
| ADD_SCENE(nfc, mifare_classic_menu, MifareClassicMenu) | ||||
| ADD_SCENE(nfc, dict_not_found, DictNotFound) | ||||
|  | ||||
| @ -73,7 +73,7 @@ bool nfc_scene_delete_on_event(void* context, SceneManagerEvent event) { | ||||
|         if(event.event == GuiButtonTypeLeft) { | ||||
|             return scene_manager_previous_scene(nfc->scene_manager); | ||||
|         } else if(event.event == GuiButtonTypeRight) { | ||||
|             if(nfc_device_delete(nfc->dev)) { | ||||
|             if(nfc_device_delete(nfc->dev, true)) { | ||||
|                 scene_manager_next_scene(nfc->scene_manager, NfcSceneDeleteSuccess); | ||||
|             } else { | ||||
|                 scene_manager_search_and_switch_to_previous_scene( | ||||
|  | ||||
							
								
								
									
										64
									
								
								applications/nfc/scenes/nfc_scene_emulate_mifare_classic.c
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								applications/nfc/scenes/nfc_scene_emulate_mifare_classic.c
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,64 @@ | ||||
| #include "../nfc_i.h" | ||||
| #include <dolphin/dolphin.h> | ||||
| 
 | ||||
| #define NFC_MF_CLASSIC_DATA_NOT_CHANGED (0UL) | ||||
| #define NFC_MF_CLASSIC_DATA_CHANGED (1UL) | ||||
| 
 | ||||
| void nfc_emulate_mifare_classic_worker_callback(NfcWorkerEvent event, void* context) { | ||||
|     UNUSED(event); | ||||
|     Nfc* nfc = context; | ||||
| 
 | ||||
|     scene_manager_set_scene_state( | ||||
|         nfc->scene_manager, NfcSceneEmulateMifareClassic, NFC_MF_CLASSIC_DATA_CHANGED); | ||||
| } | ||||
| 
 | ||||
| void nfc_scene_emulate_mifare_classic_on_enter(void* context) { | ||||
|     Nfc* nfc = context; | ||||
|     DOLPHIN_DEED(DolphinDeedNfcEmulate); | ||||
| 
 | ||||
|     // Setup view
 | ||||
|     Popup* popup = nfc->popup; | ||||
|     if(strcmp(nfc->dev->dev_name, "")) { | ||||
|         nfc_text_store_set(nfc, "%s", nfc->dev->dev_name); | ||||
|     } | ||||
|     popup_set_icon(popup, 0, 3, &I_RFIDDolphinSend_97x61); | ||||
|     popup_set_header(popup, "Emulating\nMf Classic", 56, 31, AlignLeft, AlignTop); | ||||
| 
 | ||||
|     // Setup and start worker
 | ||||
|     view_dispatcher_switch_to_view(nfc->view_dispatcher, NfcViewPopup); | ||||
|     nfc_worker_start( | ||||
|         nfc->worker, | ||||
|         NfcWorkerStateEmulateMifareClassic, | ||||
|         &nfc->dev->dev_data, | ||||
|         nfc_emulate_mifare_classic_worker_callback, | ||||
|         nfc); | ||||
| } | ||||
| 
 | ||||
| bool nfc_scene_emulate_mifare_classic_on_event(void* context, SceneManagerEvent event) { | ||||
|     Nfc* nfc = context; | ||||
|     bool consumed = false; | ||||
| 
 | ||||
|     if(event.type == SceneManagerEventTypeTick) { | ||||
|         notification_message(nfc->notifications, &sequence_blink_blue_10); | ||||
|         consumed = true; | ||||
|     } else if(event.type == SceneManagerEventTypeBack) { | ||||
|         // Stop worker
 | ||||
|         nfc_worker_stop(nfc->worker); | ||||
|         // Check if data changed and save in shadow file
 | ||||
|         if(scene_manager_get_scene_state(nfc->scene_manager, NfcSceneEmulateMifareClassic) == | ||||
|            NFC_MF_CLASSIC_DATA_CHANGED) { | ||||
|             scene_manager_set_scene_state( | ||||
|                 nfc->scene_manager, NfcSceneEmulateMifareClassic, NFC_MF_CLASSIC_DATA_NOT_CHANGED); | ||||
|             nfc_device_save_shadow(nfc->dev, nfc->dev->dev_name); | ||||
|         } | ||||
|         consumed = false; | ||||
|     } | ||||
|     return consumed; | ||||
| } | ||||
| 
 | ||||
| void nfc_scene_emulate_mifare_classic_on_exit(void* context) { | ||||
|     Nfc* nfc = context; | ||||
| 
 | ||||
|     // Clear view
 | ||||
|     popup_reset(nfc->popup); | ||||
| } | ||||
							
								
								
									
										64
									
								
								applications/nfc/scenes/nfc_scene_mifare_classic_menu.c
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								applications/nfc/scenes/nfc_scene_mifare_classic_menu.c
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,64 @@ | ||||
| #include "../nfc_i.h" | ||||
| 
 | ||||
| enum SubmenuIndex { | ||||
|     SubmenuIndexSave, | ||||
|     SubmenuIndexEmulate, | ||||
| }; | ||||
| 
 | ||||
| void nfc_scene_mifare_classic_menu_submenu_callback(void* context, uint32_t index) { | ||||
|     Nfc* nfc = context; | ||||
| 
 | ||||
|     view_dispatcher_send_custom_event(nfc->view_dispatcher, index); | ||||
| } | ||||
| 
 | ||||
| void nfc_scene_mifare_classic_menu_on_enter(void* context) { | ||||
|     Nfc* nfc = context; | ||||
|     Submenu* submenu = nfc->submenu; | ||||
| 
 | ||||
|     submenu_add_item( | ||||
|         submenu, "Save", SubmenuIndexSave, nfc_scene_mifare_classic_menu_submenu_callback, nfc); | ||||
|     submenu_add_item( | ||||
|         submenu, | ||||
|         "Emulate", | ||||
|         SubmenuIndexEmulate, | ||||
|         nfc_scene_mifare_classic_menu_submenu_callback, | ||||
|         nfc); | ||||
|     submenu_set_selected_item( | ||||
|         nfc->submenu, scene_manager_get_scene_state(nfc->scene_manager, NfcSceneMifareUlMenu)); | ||||
| 
 | ||||
|     view_dispatcher_switch_to_view(nfc->view_dispatcher, NfcViewMenu); | ||||
| } | ||||
| 
 | ||||
| bool nfc_scene_mifare_classic_menu_on_event(void* context, SceneManagerEvent event) { | ||||
|     Nfc* nfc = context; | ||||
|     bool consumed = false; | ||||
| 
 | ||||
|     if(event.type == SceneManagerEventTypeCustom) { | ||||
|         if(event.event == SubmenuIndexSave) { | ||||
|             scene_manager_set_scene_state( | ||||
|                 nfc->scene_manager, NfcSceneMifareUlMenu, SubmenuIndexSave); | ||||
|             nfc->dev->format = NfcDeviceSaveFormatMifareClassic; | ||||
|             // Clear device name
 | ||||
|             nfc_device_set_name(nfc->dev, ""); | ||||
|             scene_manager_next_scene(nfc->scene_manager, NfcSceneSaveName); | ||||
|             consumed = true; | ||||
|         } else if(event.event == SubmenuIndexEmulate) { | ||||
|             scene_manager_set_scene_state( | ||||
|                 nfc->scene_manager, NfcSceneMifareUlMenu, SubmenuIndexEmulate); | ||||
|             scene_manager_next_scene(nfc->scene_manager, NfcSceneEmulateMifareClassic); | ||||
|             consumed = true; | ||||
|         } | ||||
|     } else if(event.type == SceneManagerEventTypeBack) { | ||||
|         consumed = | ||||
|             scene_manager_search_and_switch_to_previous_scene(nfc->scene_manager, NfcSceneStart); | ||||
|     } | ||||
| 
 | ||||
|     return consumed; | ||||
| } | ||||
| 
 | ||||
| void nfc_scene_mifare_classic_menu_on_exit(void* context) { | ||||
|     Nfc* nfc = context; | ||||
| 
 | ||||
|     // Clear view
 | ||||
|     submenu_reset(nfc->submenu); | ||||
| } | ||||
| @ -47,7 +47,7 @@ bool nfc_scene_read_mifare_classic_on_event(void* context, SceneManagerEvent eve | ||||
|         consumed = true; | ||||
|     } else if(event.type == SceneManagerEventTypeCustom) { | ||||
|         if(event.event == NfcCustomEventDictAttackDone) { | ||||
|             scene_manager_next_scene(nfc->scene_manager, NfcSceneSaveName); | ||||
|             scene_manager_next_scene(nfc->scene_manager, NfcSceneMifareClassicMenu); | ||||
|             consumed = true; | ||||
|         } else if(event.event == NfcWorkerEventDetectedClassic1k) { | ||||
|             dict_attack_card_detected(nfc->dict_attack, MfClassicType1k); | ||||
| @ -71,7 +71,6 @@ bool nfc_scene_read_mifare_classic_on_event(void* context, SceneManagerEvent eve | ||||
|             scene_manager_set_scene_state( | ||||
|                 nfc->scene_manager, NfcSceneReadMifareClassic, NfcSceneReadMifareClassicStateDone); | ||||
|             notification_message(nfc->notifications, &sequence_success); | ||||
|             nfc->dev->format = NfcDeviceSaveFormatMifareClassic; | ||||
|             dict_attack_set_result(nfc->dict_attack, true); | ||||
|             consumed = true; | ||||
|         } else if(event.event == NfcWorkerEventFail) { | ||||
|  | ||||
Some files were not shown because too many files have changed in this diff Show More
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Aleksandr Kutuzov
						Aleksandr Kutuzov