diff --git a/flashlight/application.fam b/flashlight/application.fam index 13eb0e0ef4f..268e3ef4abb 100644 --- a/flashlight/application.fam +++ b/flashlight/application.fam @@ -9,6 +9,7 @@ App( ], stack_size=2 * 1024, order=20, + fap_icon_assets="icons", fap_icon="flash10px.png", fap_category="GPIO", fap_author="@xMasterX", diff --git a/flashlight/flashlight.c b/flashlight/flashlight.c index 43d1e19ce36..79cfe7f60c9 100644 --- a/flashlight/flashlight.c +++ b/flashlight/flashlight.c @@ -7,6 +7,8 @@ #include #include +#include "flashlight_icons.h" + typedef enum { EventTypeTick, EventTypeKey, @@ -28,17 +30,19 @@ static void render_callback(Canvas* const canvas, void* ctx) { furi_mutex_acquire(plugin_state->mutex, FuriWaitForever); canvas_set_font(canvas, FontPrimary); - elements_multiline_text_aligned(canvas, 64, 2, AlignCenter, AlignTop, "Flashlight"); + elements_multiline_text_aligned(canvas, 64, 4, AlignCenter, AlignTop, "Flashlight"); canvas_set_font(canvas, FontSecondary); + canvas_draw_icon(canvas, 0, 17, &I_led_connections); + if(!plugin_state->is_on) { elements_multiline_text_aligned( - canvas, 64, 28, AlignCenter, AlignTop, "Press OK button turn on"); + canvas, 64, 44, AlignCenter, AlignTop, "Press OK button turn on"); } else { - elements_multiline_text_aligned(canvas, 64, 28, AlignCenter, AlignTop, "Light is on!"); + elements_multiline_text_aligned(canvas, 64, 38, AlignCenter, AlignTop, "Light is on!"); elements_multiline_text_aligned( - canvas, 64, 40, AlignCenter, AlignTop, "Press OK button to off"); + canvas, 64, 50, AlignCenter, AlignTop, "Press OK button to off"); } furi_mutex_release(plugin_state->mutex); diff --git a/flashlight/icons/led_connections.png b/flashlight/icons/led_connections.png new file mode 100644 index 00000000000..e3825cbdbc4 Binary files /dev/null and b/flashlight/icons/led_connections.png differ diff --git a/hex_viewer/.gitignore b/hex_viewer/.gitignore new file mode 100644 index 00000000000..c6127b38c1a --- /dev/null +++ b/hex_viewer/.gitignore @@ -0,0 +1,52 @@ +# Prerequisites +*.d + +# Object files +*.o +*.ko +*.obj +*.elf + +# Linker output +*.ilk +*.map +*.exp + +# Precompiled Headers +*.gch +*.pch + +# Libraries +*.lib +*.a +*.la +*.lo + +# Shared objects (inc. Windows DLLs) +*.dll +*.so +*.so.* +*.dylib + +# Executables +*.exe +*.out +*.app +*.i*86 +*.x86_64 +*.hex + +# Debug files +*.dSYM/ +*.su +*.idb +*.pdb + +# Kernel Module Compile Results +*.mod* +*.cmd +.tmp_versions/ +modules.order +Module.symvers +Mkfile.old +dkms.conf diff --git a/hex_viewer/.gitsubtree b/hex_viewer/.gitsubtree new file mode 100644 index 00000000000..4e0b7358ea7 --- /dev/null +++ b/hex_viewer/.gitsubtree @@ -0,0 +1,2 @@ +https://github.com/xMasterX/all-the-plugins dev base_pack/hex_viewer +https://github.com/QtRoS/flipper-zero-hex-viewer master / diff --git a/hex_viewer/LICENSE b/hex_viewer/LICENSE new file mode 100644 index 00000000000..69004dc62a6 --- /dev/null +++ b/hex_viewer/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Roman Shchekin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/hex_viewer/README.md b/hex_viewer/README.md new file mode 100644 index 00000000000..4f9d0363b02 --- /dev/null +++ b/hex_viewer/README.md @@ -0,0 +1,16 @@ +# #️⃣ Hex Viewer + +Hex Viewer application for Flipper Zero! +The app allows you to view various files as HEX + +**Some facts**: +- Written with pure C in a very simple and effective manner +- Tested on files up to 16Mb +- Very effective: calls `canvas_draw_str` 8 times during repaint and that's almost it +- Can also view text representation of bytes (makes it kinda poor man's text viewer) +- Has "Scroll to ..." feature which allows you to jump to any percent of file + +Feel free to send PRs! + +App URL: https://lab.flipper.net/apps/hex_viewer +Catalog's manifest: [flipper-application-catalog/applications/Tools/hex_viewer/manifest.yml](https://github.com/flipperdevices/flipper-application-catalog/blob/main/applications/Tools/hex_viewer/manifest.yml) diff --git a/hex_viewer/application.fam b/hex_viewer/application.fam new file mode 100644 index 00000000000..c67fe12c5cc --- /dev/null +++ b/hex_viewer/application.fam @@ -0,0 +1,17 @@ +App( + appid="hex_viewer", + name="HEX Viewer", + apptype=FlipperAppType.EXTERNAL, + entry_point="hex_viewer_app", + requires=[ + "gui", + "dialogs", + ], + stack_size=2 * 1024, + fap_icon="icons/hex_10px.bmp", + fap_icon_assets="icons", + fap_category="Tools", + fap_author="@QtRoS", + fap_version="2.0", + fap_description="App allows to view various files as HEX", +) diff --git a/hex_viewer/helpers/hex_viewer_custom_event.h b/hex_viewer/helpers/hex_viewer_custom_event.h new file mode 100644 index 00000000000..f6dde56e6d4 --- /dev/null +++ b/hex_viewer/helpers/hex_viewer_custom_event.h @@ -0,0 +1,62 @@ +#pragma once + +typedef enum { + HexViewerCustomEventStartscreenUp, + HexViewerCustomEventStartscreenDown, + HexViewerCustomEventStartscreenLeft, + HexViewerCustomEventStartscreenRight, + HexViewerCustomEventStartscreenOk, + HexViewerCustomEventStartscreenBack, + HexViewerCustomEventScene1Up, + HexViewerCustomEventScene1Down, + HexViewerCustomEventScene1Left, + HexViewerCustomEventScene1Right, + HexViewerCustomEventScene1Ok, + HexViewerCustomEventScene1Back, + HexViewerCustomEventScene2Up, + HexViewerCustomEventScene2Down, + HexViewerCustomEventScene2Left, + HexViewerCustomEventScene2Right, + HexViewerCustomEventScene2Ok, + HexViewerCustomEventScene2Back, +} HexViewerCustomEvent; + +enum HexViewerCustomEventType { + // Reserve first 100 events for button types and indexes, starting from 0 + HexViewerCustomEventMenuVoid, + HexViewerCustomEventMenuSelected, + HexViewerCustomEventMenuPercentEntered, +}; + +#pragma pack(push, 1) +typedef union { + uint32_t packed_value; + struct { + uint16_t type; + int16_t value; + } content; +} HexViewerCustomEventMenu; +#pragma pack(pop) + +static inline uint32_t hex_viewer_custom_menu_event_pack(uint16_t type, int16_t value) { + HexViewerCustomEventMenu event = {.content = {.type = type, .value = value}}; + return event.packed_value; +} +static inline void + hex_viewer_custom_menu_event_unpack(uint32_t packed_value, uint16_t* type, int16_t* value) { + HexViewerCustomEventMenu event = {.packed_value = packed_value}; + if(type) *type = event.content.type; + if(value) *value = event.content.value; +} + +static inline uint16_t hex_viewer_custom_menu_event_get_type(uint32_t packed_value) { + uint16_t type; + hex_viewer_custom_menu_event_unpack(packed_value, &type, NULL); + return type; +} + +static inline int16_t hex_viewer_custom_menu_event_get_value(uint32_t packed_value) { + int16_t value; + hex_viewer_custom_menu_event_unpack(packed_value, NULL, &value); + return value; +} \ No newline at end of file diff --git a/hex_viewer/helpers/hex_viewer_haptic.c b/hex_viewer/helpers/hex_viewer_haptic.c new file mode 100644 index 00000000000..b3d230468de --- /dev/null +++ b/hex_viewer/helpers/hex_viewer_haptic.c @@ -0,0 +1,35 @@ +#include "hex_viewer_haptic.h" +#include "../hex_viewer.h" + +void hex_viewer_play_happy_bump(void* context) { + HexViewer* app = context; + if(app->haptic != 1) { + return; + } + notification_message(app->notification, &sequence_set_vibro_on); + furi_thread_flags_wait(0, FuriFlagWaitAny, 20); + notification_message(app->notification, &sequence_reset_vibro); +} + +void hex_viewer_play_bad_bump(void* context) { + HexViewer* app = context; + if(app->haptic != 1) { + return; + } + notification_message(app->notification, &sequence_set_vibro_on); + furi_thread_flags_wait(0, FuriFlagWaitAny, 100); + notification_message(app->notification, &sequence_reset_vibro); +} + +void hex_viewer_play_long_bump(void* context) { + HexViewer* app = context; + if(app->haptic != 1) { + return; + } + for(int i = 0; i < 4; i++) { + notification_message(app->notification, &sequence_set_vibro_on); + furi_thread_flags_wait(0, FuriFlagWaitAny, 50); + notification_message(app->notification, &sequence_reset_vibro); + furi_thread_flags_wait(0, FuriFlagWaitAny, 100); + } +} diff --git a/hex_viewer/helpers/hex_viewer_haptic.h b/hex_viewer/helpers/hex_viewer_haptic.h new file mode 100644 index 00000000000..ade33bc7808 --- /dev/null +++ b/hex_viewer/helpers/hex_viewer_haptic.h @@ -0,0 +1,7 @@ +#include + +void hex_viewer_play_happy_bump(void* context); + +void hex_viewer_play_bad_bump(void* context); + +void hex_viewer_play_long_bump(void* context); diff --git a/hex_viewer/helpers/hex_viewer_led.c b/hex_viewer/helpers/hex_viewer_led.c new file mode 100644 index 00000000000..d52fa5622f0 --- /dev/null +++ b/hex_viewer/helpers/hex_viewer_led.c @@ -0,0 +1,39 @@ +#include "hex_viewer_led.h" +#include "../hex_viewer.h" + +void hex_viewer_led_set_rgb(void* context, int red, int green, int blue) { + HexViewer* app = context; + if(app->led != 1) { + return; + } + NotificationMessage notification_led_message_1; + notification_led_message_1.type = NotificationMessageTypeLedRed; + NotificationMessage notification_led_message_2; + notification_led_message_2.type = NotificationMessageTypeLedGreen; + NotificationMessage notification_led_message_3; + notification_led_message_3.type = NotificationMessageTypeLedBlue; + + notification_led_message_1.data.led.value = red; + notification_led_message_2.data.led.value = green; + notification_led_message_3.data.led.value = blue; + const NotificationSequence notification_sequence = { + ¬ification_led_message_1, + ¬ification_led_message_2, + ¬ification_led_message_3, + &message_do_not_reset, + NULL, + }; + notification_message(app->notification, ¬ification_sequence); + furi_thread_flags_wait( + 0, FuriFlagWaitAny, 10); //Delay, prevent removal from RAM before LED value set +} + +void hex_viewer_led_reset(void* context) { + HexViewer* app = context; + notification_message(app->notification, &sequence_reset_red); + notification_message(app->notification, &sequence_reset_green); + notification_message(app->notification, &sequence_reset_blue); + + furi_thread_flags_wait( + 0, FuriFlagWaitAny, 300); //Delay, prevent removal from RAM before LED value set +} diff --git a/hex_viewer/helpers/hex_viewer_led.h b/hex_viewer/helpers/hex_viewer_led.h new file mode 100644 index 00000000000..ba0e1cdad75 --- /dev/null +++ b/hex_viewer/helpers/hex_viewer_led.h @@ -0,0 +1,5 @@ + + +void hex_viewer_led_set_rgb(void* context, int red, int green, int blue); + +void hex_viewer_led_reset(void* context); diff --git a/hex_viewer/helpers/hex_viewer_speaker.c b/hex_viewer/helpers/hex_viewer_speaker.c new file mode 100644 index 00000000000..4ee3de8dc08 --- /dev/null +++ b/hex_viewer/helpers/hex_viewer_speaker.c @@ -0,0 +1,26 @@ +#include "hex_viewer_speaker.h" +#include "../hex_viewer.h" + +#define NOTE_INPUT 587.33f + +void hex_viewer_play_input_sound(void* context) { + HexViewer* app = context; + if(app->speaker != 1) { + return; + } + float volume = 1.0f; + if(furi_hal_speaker_is_mine() || furi_hal_speaker_acquire(30)) { + furi_hal_speaker_start(NOTE_INPUT, volume); + } +} + +void hex_viewer_stop_all_sound(void* context) { + HexViewer* app = context; + if(app->speaker != 1) { + return; + } + if(furi_hal_speaker_is_mine()) { + furi_hal_speaker_stop(); + furi_hal_speaker_release(); + } +} diff --git a/hex_viewer/helpers/hex_viewer_speaker.h b/hex_viewer/helpers/hex_viewer_speaker.h new file mode 100644 index 00000000000..747d791464b --- /dev/null +++ b/hex_viewer/helpers/hex_viewer_speaker.h @@ -0,0 +1,4 @@ +#define NOTE_INPUT 587.33f + +void hex_viewer_play_input_sound(void* context); +void hex_viewer_stop_all_sound(void* context); diff --git a/hex_viewer/helpers/hex_viewer_storage.c b/hex_viewer/helpers/hex_viewer_storage.c new file mode 100644 index 00000000000..0b795bc5fb0 --- /dev/null +++ b/hex_viewer/helpers/hex_viewer_storage.c @@ -0,0 +1,173 @@ +#include "hex_viewer_storage.h" + +static Storage* hex_viewer_open_storage() { + return furi_record_open(RECORD_STORAGE); +} + +static void hex_viewer_close_storage() { + furi_record_close(RECORD_STORAGE); +} + +static void hex_viewer_close_config_file(FlipperFormat* file) { + if(file == NULL) return; + flipper_format_file_close(file); + flipper_format_free(file); +} + +void hex_viewer_save_settings(void* context) { + HexViewer* app = context; + if(app->save_settings == 0) { + return; + } + + FURI_LOG_D(TAG, "Saving Settings"); + Storage* storage = hex_viewer_open_storage(); + FlipperFormat* fff_file = flipper_format_file_alloc(storage); + + // Overwrite wont work, so delete first + if(storage_file_exists(storage, HEX_VIEWER_SETTINGS_SAVE_PATH)) { + storage_simply_remove(storage, HEX_VIEWER_SETTINGS_SAVE_PATH); + } + + // Open File, create if not exists + if(!storage_common_stat(storage, HEX_VIEWER_SETTINGS_SAVE_PATH, NULL) == FSE_OK) { + FURI_LOG_D( + TAG, "Config file %s is not found. Will create new.", HEX_VIEWER_SETTINGS_SAVE_PATH); + if(storage_common_stat(storage, CONFIG_FILE_DIRECTORY_PATH, NULL) == FSE_NOT_EXIST) { + FURI_LOG_D( + TAG, "Directory %s doesn't exist. Will create new.", CONFIG_FILE_DIRECTORY_PATH); + if(!storage_simply_mkdir(storage, CONFIG_FILE_DIRECTORY_PATH)) { + FURI_LOG_E(TAG, "Error creating directory %s", CONFIG_FILE_DIRECTORY_PATH); + } + } + } + + if(!flipper_format_file_open_new(fff_file, HEX_VIEWER_SETTINGS_SAVE_PATH)) { + //totp_close_config_file(fff_file); + FURI_LOG_E(TAG, "Error creating new file %s", HEX_VIEWER_SETTINGS_SAVE_PATH); + hex_viewer_close_storage(); + return; + } + + // Store Settings + flipper_format_write_header_cstr( + fff_file, HEX_VIEWER_SETTINGS_HEADER, HEX_VIEWER_SETTINGS_FILE_VERSION); + flipper_format_write_uint32(fff_file, HEX_VIEWER_SETTINGS_KEY_HAPTIC, &app->haptic, 1); + flipper_format_write_uint32(fff_file, HEX_VIEWER_SETTINGS_KEY_SPEAKER, &app->speaker, 1); + flipper_format_write_uint32(fff_file, HEX_VIEWER_SETTINGS_KEY_LED, &app->led, 1); + flipper_format_write_uint32( + fff_file, HEX_VIEWER_SETTINGS_KEY_SAVE_SETTINGS, &app->save_settings, 1); + + if(!flipper_format_rewind(fff_file)) { + hex_viewer_close_config_file(fff_file); + FURI_LOG_E(TAG, "Rewind error"); + hex_viewer_close_storage(); + return; + } + + hex_viewer_close_config_file(fff_file); + hex_viewer_close_storage(); +} + +void hex_viewer_read_settings(void* context) { + HexViewer* app = context; + Storage* storage = hex_viewer_open_storage(); + FlipperFormat* fff_file = flipper_format_file_alloc(storage); + + if(storage_common_stat(storage, HEX_VIEWER_SETTINGS_SAVE_PATH, NULL) != FSE_OK) { + hex_viewer_close_config_file(fff_file); + hex_viewer_close_storage(); + return; + } + uint32_t file_version; + FuriString* temp_str = furi_string_alloc(); + + if(!flipper_format_file_open_existing(fff_file, HEX_VIEWER_SETTINGS_SAVE_PATH)) { + FURI_LOG_E(TAG, "Cannot open file %s", HEX_VIEWER_SETTINGS_SAVE_PATH); + hex_viewer_close_config_file(fff_file); + hex_viewer_close_storage(); + return; + } + + if(!flipper_format_read_header(fff_file, temp_str, &file_version)) { + FURI_LOG_E(TAG, "Missing Header Data"); + hex_viewer_close_config_file(fff_file); + hex_viewer_close_storage(); + furi_string_free(temp_str); + return; + } + furi_string_free(temp_str); + + if(file_version < HEX_VIEWER_SETTINGS_FILE_VERSION) { + FURI_LOG_I(TAG, "old config version, will be removed."); + hex_viewer_close_config_file(fff_file); + hex_viewer_close_storage(); + return; + } + + flipper_format_read_uint32(fff_file, HEX_VIEWER_SETTINGS_KEY_HAPTIC, &app->haptic, 1); + flipper_format_read_uint32(fff_file, HEX_VIEWER_SETTINGS_KEY_SPEAKER, &app->speaker, 1); + flipper_format_read_uint32(fff_file, HEX_VIEWER_SETTINGS_KEY_LED, &app->led, 1); + flipper_format_read_uint32( + fff_file, HEX_VIEWER_SETTINGS_KEY_SAVE_SETTINGS, &app->save_settings, 1); + + flipper_format_rewind(fff_file); + + hex_viewer_close_config_file(fff_file); + hex_viewer_close_storage(); +} + +bool hex_viewer_open_file(void* context, const char* file_path) { + HexViewer* hex_viewer = context; + furi_assert(hex_viewer); + furi_assert(file_path); + + // TODO Separate function? + if(hex_viewer->model->stream) { + buffered_file_stream_close(hex_viewer->model->stream); + stream_free(hex_viewer->model->stream); + hex_viewer->model->file_offset = 0; + } + + hex_viewer->model->stream = buffered_file_stream_alloc(hex_viewer->storage); + bool isOk = true; + + do { + if(!buffered_file_stream_open( + hex_viewer->model->stream, file_path, FSAM_READ, FSOM_OPEN_EXISTING)) { + FURI_LOG_E(TAG, "Unable to open stream: %s", file_path); + isOk = false; + break; + }; + + hex_viewer->model->file_size = stream_size(hex_viewer->model->stream); + } while(false); + + return isOk; +} + +bool hex_viewer_read_file(void* context) { + HexViewer* hex_viewer = context; + furi_assert(hex_viewer); + furi_assert(hex_viewer->model->stream); + furi_assert(hex_viewer->model->file_offset % HEX_VIEWER_BYTES_PER_LINE == 0); + + memset(hex_viewer->model->file_bytes, 0x0, HEX_VIEWER_BUF_SIZE); + bool isOk = true; + + do { + uint32_t offset = hex_viewer->model->file_offset; + if(!stream_seek(hex_viewer->model->stream, offset, true)) { + FURI_LOG_E(TAG, "Unable to seek stream"); + isOk = false; + break; + } + + hex_viewer->model->file_read_bytes = stream_read( + hex_viewer->model->stream, + (uint8_t*)hex_viewer->model->file_bytes, + HEX_VIEWER_BUF_SIZE); + } while(false); + + return isOk; +} \ No newline at end of file diff --git a/hex_viewer/helpers/hex_viewer_storage.h b/hex_viewer/helpers/hex_viewer_storage.h new file mode 100644 index 00000000000..cd0c5277bf2 --- /dev/null +++ b/hex_viewer/helpers/hex_viewer_storage.h @@ -0,0 +1,23 @@ +#pragma once + +#include +#include +#include +#include +#include "../hex_viewer.h" + +#define HEX_VIEWER_SETTINGS_FILE_VERSION 1 +#define CONFIG_FILE_DIRECTORY_PATH EXT_PATH("apps_data/hex_viewer") +#define HEX_VIEWER_SETTINGS_SAVE_PATH CONFIG_FILE_DIRECTORY_PATH "/hex_viewer.conf" +#define HEX_VIEWER_SETTINGS_SAVE_PATH_TMP HEX_VIEWER_SETTINGS_SAVE_PATH ".tmp" +#define HEX_VIEWER_SETTINGS_HEADER "HexViewer Config File" +#define HEX_VIEWER_SETTINGS_KEY_HAPTIC "Haptic" +#define HEX_VIEWER_SETTINGS_KEY_LED "Led" +#define HEX_VIEWER_SETTINGS_KEY_SPEAKER "Speaker" +#define HEX_VIEWER_SETTINGS_KEY_SAVE_SETTINGS "SaveSettings" + +void hex_viewer_save_settings(void* context); +void hex_viewer_read_settings(void* context); + +bool hex_viewer_open_file(void* context, const char* file_path); +bool hex_viewer_read_file(void* context); \ No newline at end of file diff --git a/hex_viewer/hex_viewer.c b/hex_viewer/hex_viewer.c new file mode 100644 index 00000000000..916ed9f7c30 --- /dev/null +++ b/hex_viewer/hex_viewer.c @@ -0,0 +1,149 @@ +#include "hex_viewer.h" + +bool hex_viewer_custom_event_callback(void* context, uint32_t event) { + furi_assert(context); + HexViewer* app = context; + return scene_manager_handle_custom_event(app->scene_manager, event); +} + +void hex_viewer_tick_event_callback(void* context) { + furi_assert(context); + HexViewer* app = context; + scene_manager_handle_tick_event(app->scene_manager); +} + +//leave app if back button pressed +bool hex_viewer_navigation_event_callback(void* context) { + furi_assert(context); + HexViewer* app = context; + return scene_manager_handle_back_event(app->scene_manager); +} + +HexViewer* hex_viewer_app_alloc() { + HexViewer* app = malloc(sizeof(HexViewer)); + + app->model = malloc(sizeof(HexViewerModel)); + memset(app->model, 0, sizeof(HexViewerModel)); + + app->gui = furi_record_open(RECORD_GUI); + app->storage = furi_record_open(RECORD_STORAGE); + app->notification = furi_record_open(RECORD_NOTIFICATION); + + //Turn backlight on, believe me this makes testing your app easier + notification_message(app->notification, &sequence_display_backlight_on); + + //Scene additions + app->view_dispatcher = view_dispatcher_alloc(); + view_dispatcher_enable_queue(app->view_dispatcher); + + app->scene_manager = scene_manager_alloc(&hex_viewer_scene_handlers, app); + view_dispatcher_set_event_callback_context(app->view_dispatcher, app); + view_dispatcher_set_navigation_event_callback( + app->view_dispatcher, hex_viewer_navigation_event_callback); + view_dispatcher_set_tick_event_callback( + app->view_dispatcher, hex_viewer_tick_event_callback, 100); + view_dispatcher_set_custom_event_callback( + app->view_dispatcher, hex_viewer_custom_event_callback); + + // Set defaults, in case no config loaded + app->haptic = 1; + app->speaker = 1; + app->led = 1; + app->save_settings = 1; + + // Used for File Browser + app->dialogs = furi_record_open(RECORD_DIALOGS); + app->file_path = furi_string_alloc(); + + // Load configs + hex_viewer_read_settings(app); + + app->submenu = submenu_alloc(); + view_dispatcher_add_view( + app->view_dispatcher, HexViewerViewIdMenu, submenu_get_view(app->submenu)); + + app->hex_viewer_startscreen = hex_viewer_startscreen_alloc(); + view_dispatcher_add_view( + app->view_dispatcher, + HexViewerViewIdStartscreen, + hex_viewer_startscreen_get_view(app->hex_viewer_startscreen)); + + app->text_input = text_input_alloc(); + view_dispatcher_add_view( + app->view_dispatcher, HexViewerViewIdScroll, text_input_get_view(app->text_input)); + + app->variable_item_list = variable_item_list_alloc(); + view_dispatcher_add_view( + app->view_dispatcher, + HexViewerViewIdSettings, + variable_item_list_get_view(app->variable_item_list)); + + //End Scene Additions + + return app; +} + +void hex_viewer_app_free(HexViewer* app) { + furi_assert(app); + + if(app->model->stream) { + buffered_file_stream_close(app->model->stream); + stream_free(app->model->stream); + } + + // Scene manager + scene_manager_free(app->scene_manager); + + // View Dispatcher + view_dispatcher_remove_view(app->view_dispatcher, HexViewerViewIdMenu); + submenu_free(app->submenu); + view_dispatcher_remove_view(app->view_dispatcher, HexViewerViewIdStartscreen); + hex_viewer_startscreen_free(app->hex_viewer_startscreen); + view_dispatcher_remove_view(app->view_dispatcher, HexViewerViewIdScroll); + text_input_free(app->text_input); + view_dispatcher_remove_view(app->view_dispatcher, HexViewerViewIdSettings); + variable_item_list_free(app->variable_item_list); + + view_dispatcher_free(app->view_dispatcher); + furi_record_close(RECORD_STORAGE); + furi_record_close(RECORD_GUI); + + app->storage = NULL; + app->gui = NULL; + app->notification = NULL; + + // Close File Browser + furi_record_close(RECORD_DIALOGS); + furi_string_free(app->file_path); + + free(app->model); + + //Remove whatever is left + free(app); +} + +int32_t hex_viewer_app(void* p) { + UNUSED(p); + HexViewer* app = hex_viewer_app_alloc(); + + view_dispatcher_attach_to_gui(app->view_dispatcher, app->gui, ViewDispatcherTypeFullscreen); + + if(p && strlen(p) && hex_viewer_open_file(app, (const char*)p)) { + hex_viewer_read_file(app); + scene_manager_next_scene(app->scene_manager, HexViewerSceneStartscreen); + } else { + scene_manager_next_scene(app->scene_manager, HexViewerSceneStartscreen); + scene_manager_next_scene(app->scene_manager, HexViewerSceneOpen); + } + + furi_hal_power_suppress_charge_enter(); + + view_dispatcher_run(app->view_dispatcher); + + hex_viewer_save_settings(app); + + furi_hal_power_suppress_charge_exit(); + hex_viewer_app_free(app); + + return 0; +} diff --git a/hex_viewer/hex_viewer.h b/hex_viewer/hex_viewer.h new file mode 100644 index 00000000000..c8b98926f1e --- /dev/null +++ b/hex_viewer/hex_viewer.h @@ -0,0 +1,92 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "scenes/hex_viewer_scene.h" +#include "views/hex_viewer_startscreen.h" +#include "helpers/hex_viewer_storage.h" + +#include +#include +#include +#include + +#define TAG "HexViewer" + +#define HEX_VIEWER_APP_PATH_FOLDER "/any" // TODO ANY_PATH +#define HEX_VIEWER_APP_EXTENSION "*" +#define HEX_VIEWER_PERCENT_INPUT 16 + +#define HEX_VIEWER_BYTES_PER_LINE 4u +#define HEX_VIEWER_LINES_ON_SCREEN 4u +#define HEX_VIEWER_BUF_SIZE (HEX_VIEWER_LINES_ON_SCREEN * HEX_VIEWER_BYTES_PER_LINE) + +typedef struct { + uint8_t file_bytes[HEX_VIEWER_LINES_ON_SCREEN][HEX_VIEWER_BYTES_PER_LINE]; + uint32_t file_offset; + uint32_t file_read_bytes; + uint32_t file_size; + + Stream* stream; +} HexViewerModel; + +typedef struct { + HexViewerModel* model; + + Gui* gui; + Storage* storage; + NotificationApp* notification; + ViewDispatcher* view_dispatcher; + Submenu* submenu; + TextInput* text_input; + SceneManager* scene_manager; + VariableItemList* variable_item_list; + HexViewerStartscreen* hex_viewer_startscreen; + DialogsApp* dialogs; // File Browser + FuriString* file_path; // File Browser + uint32_t haptic; + uint32_t speaker; + uint32_t led; + uint32_t save_settings; + char percent_buf[HEX_VIEWER_PERCENT_INPUT]; +} HexViewer; + +typedef enum { + HexViewerViewIdStartscreen, + HexViewerViewIdMenu, + HexViewerViewIdScroll, + HexViewerViewIdSettings, +} HexViewerViewId; + +typedef enum { + HexViewerHapticOff, + HexViewerHapticOn, +} HexViewerHapticState; + +typedef enum { + HexViewerSpeakerOff, + HexViewerSpeakerOn, +} HexViewerSpeakerState; + +typedef enum { + HexViewerLedOff, + HexViewerLedOn, +} HexViewerLedState; + +typedef enum { + HexViewerSettingsOff, + HexViewerSettingsOn, +} HexViewerSettingsStoreState; diff --git a/hex_viewer/icons/hex_10px.bmp b/hex_viewer/icons/hex_10px.bmp new file mode 100644 index 00000000000..54b78dc42b5 Binary files /dev/null and b/hex_viewer/icons/hex_10px.bmp differ diff --git a/hex_viewer/icons/hex_10px.png b/hex_viewer/icons/hex_10px.png new file mode 100644 index 00000000000..582e288c6dc Binary files /dev/null and b/hex_viewer/icons/hex_10px.png differ diff --git a/hex_viewer/img/1.png b/hex_viewer/img/1.png new file mode 100644 index 00000000000..a3df62f8d13 Binary files /dev/null and b/hex_viewer/img/1.png differ diff --git a/hex_viewer/img/2.png b/hex_viewer/img/2.png new file mode 100644 index 00000000000..91b9f3dd81e Binary files /dev/null and b/hex_viewer/img/2.png differ diff --git a/hex_viewer/scenes/hex_viewer_scene.c b/hex_viewer/scenes/hex_viewer_scene.c new file mode 100644 index 00000000000..385828b0d47 --- /dev/null +++ b/hex_viewer/scenes/hex_viewer_scene.c @@ -0,0 +1,30 @@ +#include "hex_viewer_scene.h" + +// Generate scene on_enter handlers array +#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_enter, +void (*const hex_viewer_on_enter_handlers[])(void*) = { +#include "hex_viewer_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 hex_viewer_on_event_handlers[])(void* context, SceneManagerEvent event) = { +#include "hex_viewer_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 hex_viewer_on_exit_handlers[])(void* context) = { +#include "hex_viewer_scene_config.h" +}; +#undef ADD_SCENE + +// Initialize scene handlers configuration structure +const SceneManagerHandlers hex_viewer_scene_handlers = { + .on_enter_handlers = hex_viewer_on_enter_handlers, + .on_event_handlers = hex_viewer_on_event_handlers, + .on_exit_handlers = hex_viewer_on_exit_handlers, + .scene_num = HexViewerSceneNum, +}; diff --git a/hex_viewer/scenes/hex_viewer_scene.h b/hex_viewer/scenes/hex_viewer_scene.h new file mode 100644 index 00000000000..e1f322fcaef --- /dev/null +++ b/hex_viewer/scenes/hex_viewer_scene.h @@ -0,0 +1,29 @@ +#pragma once + +#include + +// Generate scene id and total number +#define ADD_SCENE(prefix, name, id) HexViewerScene##id, +typedef enum { +#include "hex_viewer_scene_config.h" + HexViewerSceneNum, +} HexViewerScene; +#undef ADD_SCENE + +extern const SceneManagerHandlers hex_viewer_scene_handlers; + +// Generate scene on_enter handlers declaration +#define ADD_SCENE(prefix, name, id) void prefix##_scene_##name##_on_enter(void*); +#include "hex_viewer_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 "hex_viewer_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 "hex_viewer_scene_config.h" +#undef ADD_SCENE diff --git a/hex_viewer/scenes/hex_viewer_scene_config.h b/hex_viewer/scenes/hex_viewer_scene_config.h new file mode 100644 index 00000000000..7db382bc522 --- /dev/null +++ b/hex_viewer/scenes/hex_viewer_scene_config.h @@ -0,0 +1,6 @@ +ADD_SCENE(hex_viewer, startscreen, Startscreen) +ADD_SCENE(hex_viewer, menu, Menu) +ADD_SCENE(hex_viewer, scroll, Scroll) +ADD_SCENE(hex_viewer, info, Info) +ADD_SCENE(hex_viewer, open, Open) +ADD_SCENE(hex_viewer, settings, Settings) \ No newline at end of file diff --git a/hex_viewer/scenes/hex_viewer_scene_info.c b/hex_viewer/scenes/hex_viewer_scene_info.c new file mode 100644 index 00000000000..39d51b8c57f --- /dev/null +++ b/hex_viewer/scenes/hex_viewer_scene_info.c @@ -0,0 +1,42 @@ +#include "../hex_viewer.h" + +void hex_viewer_scene_info_on_enter(void* context) { + furi_assert(context); + HexViewer* app = context; + + FuriString* buffer; + buffer = furi_string_alloc(); + furi_string_printf( + buffer, + "File path: %s\nFile size: %lu (0x%lX)", + furi_string_get_cstr(app->file_path), + app->model->file_size, + app->model->file_size); + + DialogMessage* message = dialog_message_alloc(); + dialog_message_set_header(message, "Hex Viewer v2.0", 16, 2, AlignLeft, AlignTop); + dialog_message_set_icon(message, &I_hex_10px, 3, 2); + dialog_message_set_text(message, furi_string_get_cstr(buffer), 3, 16, AlignLeft, AlignTop); + dialog_message_set_buttons(message, NULL, NULL, "Back"); + dialog_message_show(app->dialogs, message); + + furi_string_free(buffer); + dialog_message_free(message); + + scene_manager_search_and_switch_to_previous_scene( + app->scene_manager, HexViewerViewIdStartscreen); +} + +bool hex_viewer_scene_info_on_event(void* context, SceneManagerEvent event) { + HexViewer* app = context; + UNUSED(app); + UNUSED(event); + bool consumed = true; + + return consumed; +} + +void hex_viewer_scene_info_on_exit(void* context) { + HexViewer* app = context; + UNUSED(app); +} diff --git a/hex_viewer/scenes/hex_viewer_scene_menu.c b/hex_viewer/scenes/hex_viewer_scene_menu.c new file mode 100644 index 00000000000..50e0b55ba91 --- /dev/null +++ b/hex_viewer/scenes/hex_viewer_scene_menu.c @@ -0,0 +1,83 @@ +#include "../hex_viewer.h" + +enum SubmenuIndex { + SubmenuIndexScroll = 10, + SubmenuIndexInfo, + SubmenuIndexOpen, + // SubmenuIndexSettings, +}; + +void hex_viewer_scene_menu_submenu_callback(void* context, uint32_t index) { + HexViewer* app = context; + view_dispatcher_send_custom_event(app->view_dispatcher, index); +} + +void hex_viewer_scene_menu_on_enter(void* context) { + HexViewer* app = context; + + submenu_set_header(app->submenu, "Select action"); + submenu_add_item( + app->submenu, + "Open file ...", + SubmenuIndexOpen, + hex_viewer_scene_menu_submenu_callback, + app); + submenu_add_item( + app->submenu, + "Scroll to ...", + SubmenuIndexScroll, + hex_viewer_scene_menu_submenu_callback, + app); + submenu_add_item( + app->submenu, + "Show info ...", + SubmenuIndexInfo, + hex_viewer_scene_menu_submenu_callback, + app); + // submenu_add_item(app->submenu, "Settings", SubmenuIndexSettings, hex_viewer_scene_menu_submenu_callback, app); + + submenu_set_selected_item( + app->submenu, scene_manager_get_scene_state(app->scene_manager, HexViewerSceneMenu)); + + view_dispatcher_switch_to_view(app->view_dispatcher, HexViewerViewIdMenu); +} + +bool hex_viewer_scene_menu_on_event(void* context, SceneManagerEvent event) { + HexViewer* app = context; + + if(event.type == SceneManagerEventTypeBack) { + //exit app + // scene_manager_stop(app->scene_manager); + // view_dispatcher_stop(app->view_dispatcher); + scene_manager_previous_scene(app->scene_manager); + return true; + } else if(event.type == SceneManagerEventTypeCustom) { + if(event.event == SubmenuIndexScroll) { + scene_manager_set_scene_state( + app->scene_manager, HexViewerSceneMenu, SubmenuIndexScroll); + scene_manager_next_scene(app->scene_manager, HexViewerSceneScroll); + return true; + } else if(event.event == SubmenuIndexInfo) { + scene_manager_set_scene_state( + app->scene_manager, HexViewerSceneMenu, SubmenuIndexInfo); + scene_manager_next_scene(app->scene_manager, HexViewerSceneInfo); + return true; + } else if(event.event == SubmenuIndexOpen) { + scene_manager_set_scene_state( + app->scene_manager, HexViewerSceneMenu, SubmenuIndexOpen); + scene_manager_next_scene(app->scene_manager, HexViewerSceneOpen); + // } else if (event.event == SubmenuIndexSettings) { + // scene_manager_set_scene_state( + // app->scene_manager, HexViewerSceneMenu, SubmenuIndexSettings); + // scene_manager_next_scene(app->scene_manager, HexViewerSceneSettings); + // return true; + } + } + + return false; +} + +void hex_viewer_scene_menu_on_exit(void* context) { + HexViewer* app = context; + submenu_reset(app->submenu); +} \ No newline at end of file diff --git a/hex_viewer/scenes/hex_viewer_scene_open.c b/hex_viewer/scenes/hex_viewer_scene_open.c new file mode 100644 index 00000000000..cd9e90b7f73 --- /dev/null +++ b/hex_viewer/scenes/hex_viewer_scene_open.c @@ -0,0 +1,42 @@ +#include "../hex_viewer.h" + +void hex_viewer_scene_open_on_enter(void* context) { + furi_assert(context); + HexViewer* app = context; + + FuriString* initial_path; + initial_path = furi_string_alloc(); + furi_string_set(initial_path, HEX_VIEWER_APP_PATH_FOLDER); + + DialogsFileBrowserOptions browser_options; + dialog_file_browser_set_basic_options(&browser_options, HEX_VIEWER_APP_EXTENSION, &I_hex_10px); + browser_options.hide_ext = false; + + bool success = + dialog_file_browser_show(app->dialogs, app->file_path, initial_path, &browser_options); + furi_string_free(initial_path); + + if(success) { + success = hex_viewer_open_file(app, furi_string_get_cstr(app->file_path)); + if(success) hex_viewer_read_file(app); + } + + if(success) { + scene_manager_search_and_switch_to_previous_scene( + app->scene_manager, HexViewerViewIdStartscreen); + } else { + scene_manager_previous_scene(app->scene_manager); + } +} + +bool hex_viewer_scene_open_on_event(void* context, SceneManagerEvent event) { + UNUSED(context); + UNUSED(event); + bool consumed = true; + + return consumed; +} + +void hex_viewer_scene_open_on_exit(void* context) { + UNUSED(context); +} diff --git a/hex_viewer/scenes/hex_viewer_scene_scroll.c b/hex_viewer/scenes/hex_viewer_scene_scroll.c new file mode 100644 index 00000000000..9613f53904f --- /dev/null +++ b/hex_viewer/scenes/hex_viewer_scene_scroll.c @@ -0,0 +1,61 @@ +#include "../hex_viewer.h" +#include "../helpers/hex_viewer_custom_event.h" + +void hex_viewer_scene_scroll_callback(void* context) { + HexViewer* app = (HexViewer*)context; + view_dispatcher_send_custom_event( + app->view_dispatcher, HexViewerCustomEventMenuPercentEntered); +} + +void hex_viewer_scene_scroll_on_enter(void* context) { + furi_assert(context); + HexViewer* app = context; + + TextInput* text_input = app->text_input; + + text_input_set_header_text(text_input, "Scroll to percentage (0..100)"); + text_input_set_result_callback( + text_input, + hex_viewer_scene_scroll_callback, + app, + app->percent_buf, + HEX_VIEWER_PERCENT_INPUT, + false); + + view_dispatcher_switch_to_view(app->view_dispatcher, HexViewerSceneScroll); +} + +bool hex_viewer_scene_scroll_on_event(void* context, SceneManagerEvent event) { + HexViewer* app = (HexViewer*)context; + bool consumed = false; + + if(event.type == SceneManagerEventTypeCustom) { + if(event.event == HexViewerCustomEventMenuPercentEntered) { + int ipercent = atoi(app->percent_buf); + ipercent = MIN(ipercent, 100); + ipercent = MAX(ipercent, 0); + float percent = ipercent / 100.0; + + uint32_t line_count = app->model->file_size / HEX_VIEWER_BYTES_PER_LINE; + if(app->model->file_size % HEX_VIEWER_BYTES_PER_LINE != 0) line_count += 1; + uint32_t scrollable_lines = line_count - HEX_VIEWER_LINES_ON_SCREEN; + uint32_t target_line = (uint32_t)(percent * scrollable_lines); + + uint32_t new_file_offset = target_line * HEX_VIEWER_BYTES_PER_LINE; + if(app->model->file_size > new_file_offset) { + app->model->file_offset = new_file_offset; + if(!hex_viewer_read_file(app)) new_file_offset = new_file_offset; // TODO Do smth + } + + scene_manager_search_and_switch_to_previous_scene( + app->scene_manager, HexViewerViewIdStartscreen); + + consumed = true; + } + } + return consumed; +} + +void hex_viewer_scene_scroll_on_exit(void* context) { + UNUSED(context); +} diff --git a/hex_viewer/scenes/hex_viewer_scene_settings.c b/hex_viewer/scenes/hex_viewer_scene_settings.c new file mode 100644 index 00000000000..70a8b3a4161 --- /dev/null +++ b/hex_viewer/scenes/hex_viewer_scene_settings.c @@ -0,0 +1,133 @@ +#include "../hex_viewer.h" +#include + +enum SettingsIndex { + SettingsIndexHaptic = 10, + SettingsIndexValue1, + SettingsIndexValue2, +}; + +const char* const haptic_text[2] = { + "OFF", + "ON", +}; +const uint32_t haptic_value[2] = { + HexViewerHapticOff, + HexViewerHapticOn, +}; + +const char* const speaker_text[2] = { + "OFF", + "ON", +}; +const uint32_t speaker_value[2] = { + HexViewerSpeakerOff, + HexViewerSpeakerOn, +}; + +const char* const led_text[2] = { + "OFF", + "ON", +}; +const uint32_t led_value[2] = { + HexViewerLedOff, + HexViewerLedOn, +}; + +const char* const settings_text[2] = { + "OFF", + "ON", +}; +const uint32_t settings_value[2] = { + HexViewerSettingsOff, + HexViewerSettingsOn, +}; + +static void hex_viewer_scene_settings_set_haptic(VariableItem* item) { + HexViewer* app = variable_item_get_context(item); + uint8_t index = variable_item_get_current_value_index(item); + + variable_item_set_current_value_text(item, haptic_text[index]); + app->haptic = haptic_value[index]; +} + +static void hex_viewer_scene_settings_set_speaker(VariableItem* item) { + HexViewer* app = variable_item_get_context(item); + uint8_t index = variable_item_get_current_value_index(item); + variable_item_set_current_value_text(item, speaker_text[index]); + app->speaker = speaker_value[index]; +} + +static void hex_viewer_scene_settings_set_led(VariableItem* item) { + HexViewer* app = variable_item_get_context(item); + uint8_t index = variable_item_get_current_value_index(item); + variable_item_set_current_value_text(item, led_text[index]); + app->led = led_value[index]; +} + +static void hex_viewer_scene_settings_set_save_settings(VariableItem* item) { + HexViewer* app = variable_item_get_context(item); + uint8_t index = variable_item_get_current_value_index(item); + variable_item_set_current_value_text(item, settings_text[index]); + app->save_settings = settings_value[index]; +} + +void hex_viewer_scene_settings_submenu_callback(void* context, uint32_t index) { + HexViewer* app = context; + view_dispatcher_send_custom_event(app->view_dispatcher, index); +} + +void hex_viewer_scene_settings_on_enter(void* context) { + HexViewer* app = context; + VariableItem* item; + uint8_t value_index; + + // Vibro on/off + item = variable_item_list_add( + app->variable_item_list, "Vibro/Haptic:", 2, hex_viewer_scene_settings_set_haptic, app); + value_index = value_index_uint32(app->haptic, haptic_value, 2); + variable_item_set_current_value_index(item, value_index); + variable_item_set_current_value_text(item, haptic_text[value_index]); + + // Sound on/off + item = variable_item_list_add( + app->variable_item_list, "Sound:", 2, hex_viewer_scene_settings_set_speaker, app); + value_index = value_index_uint32(app->speaker, speaker_value, 2); + variable_item_set_current_value_index(item, value_index); + variable_item_set_current_value_text(item, speaker_text[value_index]); + + // LED Effects on/off + item = variable_item_list_add( + app->variable_item_list, "LED FX:", 2, hex_viewer_scene_settings_set_led, app); + value_index = value_index_uint32(app->led, led_value, 2); + variable_item_set_current_value_index(item, value_index); + variable_item_set_current_value_text(item, led_text[value_index]); + + // Save Settings to File + item = variable_item_list_add( + app->variable_item_list, + "Save Settings", + 2, + hex_viewer_scene_settings_set_save_settings, + app); + value_index = value_index_uint32(app->save_settings, settings_value, 2); + variable_item_set_current_value_index(item, value_index); + variable_item_set_current_value_text(item, settings_text[value_index]); + + view_dispatcher_switch_to_view(app->view_dispatcher, HexViewerViewIdSettings); +} + +bool hex_viewer_scene_settings_on_event(void* context, SceneManagerEvent event) { + HexViewer* app = context; + UNUSED(app); + bool consumed = false; + if(event.type == SceneManagerEventTypeCustom) { + } + return consumed; +} + +void hex_viewer_scene_settings_on_exit(void* context) { + HexViewer* app = context; + variable_item_list_set_selected_item(app->variable_item_list, 0); + variable_item_list_reset(app->variable_item_list); +} \ No newline at end of file diff --git a/hex_viewer/scenes/hex_viewer_scene_startscreen.c b/hex_viewer/scenes/hex_viewer_scene_startscreen.c new file mode 100644 index 00000000000..6793655af4d --- /dev/null +++ b/hex_viewer/scenes/hex_viewer_scene_startscreen.c @@ -0,0 +1,65 @@ +#include "../hex_viewer.h" +#include "../helpers/hex_viewer_custom_event.h" +#include "../views/hex_viewer_startscreen.h" + +void hex_viewer_scene_startscreen_callback(HexViewerCustomEvent event, void* context) { + furi_assert(context); + HexViewer* app = context; + view_dispatcher_send_custom_event(app->view_dispatcher, event); +} + +void hex_viewer_scene_startscreen_on_enter(void* context) { + furi_assert(context); + HexViewer* app = context; + hex_viewer_startscreen_set_callback( + app->hex_viewer_startscreen, hex_viewer_scene_startscreen_callback, app); + view_dispatcher_switch_to_view(app->view_dispatcher, HexViewerViewIdStartscreen); +} + +bool hex_viewer_scene_startscreen_on_event(void* context, SceneManagerEvent event) { + HexViewer* app = context; + bool consumed = false; + + if(event.type == SceneManagerEventTypeCustom) { + switch(event.event) { + case HexViewerCustomEventStartscreenLeft: + //app->model->mode = !app->model->mode; + consumed = true; + break; + case HexViewerCustomEventStartscreenRight: + consumed = true; + break; + case HexViewerCustomEventStartscreenUp: + consumed = true; + break; + case HexViewerCustomEventStartscreenDown: + consumed = true; + break; + case HexViewerCustomEventStartscreenOk: + if(!app->model->file_size) + scene_manager_next_scene(app->scene_manager, HexViewerSceneOpen); + else + scene_manager_next_scene(app->scene_manager, HexViewerSceneMenu); + consumed = true; + break; + case HexViewerCustomEventStartscreenBack: // TODO Delete + notification_message(app->notification, &sequence_reset_red); + notification_message(app->notification, &sequence_reset_green); + notification_message(app->notification, &sequence_reset_blue); + if(!scene_manager_search_and_switch_to_previous_scene( + app->scene_manager, HexViewerSceneStartscreen)) { + scene_manager_stop(app->scene_manager); + view_dispatcher_stop(app->view_dispatcher); + } + consumed = true; + break; + } + } + + return consumed; +} + +void hex_viewer_scene_startscreen_on_exit(void* context) { + HexViewer* app = context; + UNUSED(app); +} \ No newline at end of file diff --git a/hex_viewer/views/hex_viewer_startscreen.c b/hex_viewer/views/hex_viewer_startscreen.c new file mode 100644 index 00000000000..515a0b13908 --- /dev/null +++ b/hex_viewer/views/hex_viewer_startscreen.c @@ -0,0 +1,248 @@ +#include "../hex_viewer.h" +#include +#include +#include +#include + +struct HexViewerStartscreen { + View* view; + HexViewerStartscreenCallback callback; + void* context; +}; + +typedef struct { + uint8_t file_bytes[HEX_VIEWER_LINES_ON_SCREEN][HEX_VIEWER_BYTES_PER_LINE]; + uint32_t file_offset; + uint32_t file_read_bytes; + uint32_t file_size; + bool mode; + uint32_t dbg; +} HexViewerStartscreenModel; + +void hex_viewer_startscreen_set_callback( + HexViewerStartscreen* instance, + HexViewerStartscreenCallback callback, + void* context) { + furi_assert(instance); + furi_assert(callback); + instance->callback = callback; + instance->context = context; +} + +void hex_viewer_startscreen_draw(Canvas* canvas, HexViewerStartscreenModel* model) { + canvas_clear(canvas); + + if(!model->file_size) { + canvas_set_color(canvas, ColorBlack); + canvas_set_font(canvas, FontPrimary); + canvas_draw_str_aligned(canvas, 64, 10, AlignCenter, AlignTop, "HexViewer v2.0"); + canvas_set_font(canvas, FontSecondary); + canvas_draw_str_aligned(canvas, 64, 22, AlignCenter, AlignTop, "Basic hex viewer"); + canvas_draw_str_aligned(canvas, 64, 32, AlignCenter, AlignTop, "for your Flipper"); + elements_button_center(canvas, "Open"); + } else { + canvas_set_color(canvas, ColorBlack); + + elements_button_left(canvas, model->mode ? "Addr" : "Text"); + //elements_button_right(canvas, "Info"); + elements_button_center(canvas, "Menu"); + + int ROW_HEIGHT = 12; + int TOP_OFFSET = 10; + int LEFT_OFFSET = 3; + + uint32_t line_count = model->file_size / HEX_VIEWER_BYTES_PER_LINE; + if(model->file_size % HEX_VIEWER_BYTES_PER_LINE != 0) line_count += 1; + uint32_t first_line_on_screen = model->file_offset / HEX_VIEWER_BYTES_PER_LINE; + if(line_count > HEX_VIEWER_LINES_ON_SCREEN) { + uint8_t width = canvas_width(canvas); + elements_scrollbar_pos( + canvas, + width, + 0, + ROW_HEIGHT * HEX_VIEWER_LINES_ON_SCREEN, + first_line_on_screen, + line_count - (HEX_VIEWER_LINES_ON_SCREEN - 1)); + } + + char temp_buf[32]; + uint32_t row_iters = model->file_read_bytes / HEX_VIEWER_BYTES_PER_LINE; + if(model->file_read_bytes % HEX_VIEWER_BYTES_PER_LINE != 0) row_iters += 1; + + // For the rest of drawing. + canvas_set_font(canvas, FontKeyboard); + + for(uint32_t i = 0; i < row_iters; ++i) { + uint32_t bytes_left_per_row = model->file_read_bytes - i * HEX_VIEWER_BYTES_PER_LINE; + bytes_left_per_row = MIN(bytes_left_per_row, HEX_VIEWER_BYTES_PER_LINE); + + if(model->mode) { + memcpy(temp_buf, model->file_bytes[i], bytes_left_per_row); + temp_buf[bytes_left_per_row] = '\0'; + for(uint32_t j = 0; j < bytes_left_per_row; ++j) + if(!isprint((int)temp_buf[j])) temp_buf[j] = '.'; + + //canvas_set_font(canvas, FontKeyboard); + canvas_draw_str(canvas, LEFT_OFFSET, TOP_OFFSET + i * ROW_HEIGHT, temp_buf); + } else { + uint32_t addr = model->file_offset + i * HEX_VIEWER_BYTES_PER_LINE; + snprintf(temp_buf, 32, "%04lX", addr); + + //canvas_set_font(canvas, FontKeyboard); + canvas_draw_str(canvas, LEFT_OFFSET, TOP_OFFSET + i * ROW_HEIGHT, temp_buf); + } + + char* p = temp_buf; + for(uint32_t j = 0; j < bytes_left_per_row; ++j) + p += snprintf(p, 32, "%02X ", model->file_bytes[i][j]); + + //canvas_set_font(canvas, FontKeyboard); + canvas_draw_str(canvas, LEFT_OFFSET + 41, TOP_OFFSET + i * ROW_HEIGHT, temp_buf); + } + + // Poor man's debug + // snprintf(temp_buf, 32, "D %02lX", model->dbg); + // elements_button_right(canvas, temp_buf); + } +} + +static void hex_viewer_startscreen_model_init(HexViewerStartscreenModel* const model) { + memset(model->file_bytes, 0, sizeof(model->file_bytes)); + model->file_offset = 0; + model->file_read_bytes = 0; + model->file_size = 0; + model->mode = false; + model->dbg = 0; +} + +static void + update_local_model_from_app(HexViewer* const app, HexViewerStartscreenModel* const model) { + memcpy(model->file_bytes, app->model->file_bytes, sizeof(model->file_bytes)); + model->file_offset = app->model->file_offset; + model->file_read_bytes = app->model->file_read_bytes; + model->file_size = app->model->file_size; + //model->mode = app->model->mode; +} + +bool hex_viewer_startscreen_input(InputEvent* event, void* context) { + furi_assert(context); + HexViewerStartscreen* instance = context; + HexViewer* app = instance->context; // TO so good, but works + + if(event->type == InputTypeRelease || event->type == InputTypeRepeat) { + switch(event->key) { + case InputKeyBack: + with_view_model( + instance->view, + HexViewerStartscreenModel * model, + { + instance->callback(HexViewerCustomEventStartscreenBack, instance->context); + update_local_model_from_app(instance->context, model); + }, + true); + break; + case InputKeyLeft: + with_view_model( + instance->view, + HexViewerStartscreenModel * model, + { model->mode = !model->mode; }, + true); + break; + case InputKeyRight: + with_view_model( + instance->view, HexViewerStartscreenModel * model, { model->dbg = 0; }, true); + break; + case InputKeyUp: + with_view_model( + instance->view, + HexViewerStartscreenModel * model, + { + if(app->model->file_offset > 0) { + app->model->file_offset -= HEX_VIEWER_BYTES_PER_LINE; + if(!hex_viewer_read_file(app)) break; // TODO Do smth + } + + update_local_model_from_app(instance->context, model); + }, + true); + break; + case InputKeyDown: + with_view_model( + instance->view, + HexViewerStartscreenModel * model, + { + uint32_t last_byte_on_screen = + app->model->file_offset + app->model->file_read_bytes; + if(app->model->file_size > last_byte_on_screen) { + app->model->file_offset += HEX_VIEWER_BYTES_PER_LINE; + if(!hex_viewer_read_file(app)) break; // TODO Do smth + } + + update_local_model_from_app(instance->context, model); + }, + true); + break; + case InputKeyOk: + with_view_model( + instance->view, + HexViewerStartscreenModel * model, + { + instance->callback(HexViewerCustomEventStartscreenOk, instance->context); + update_local_model_from_app(instance->context, model); + }, + true); + break; + case InputKeyMAX: + break; + } + } + + return true; +} + +void hex_viewer_startscreen_exit(void* context) { + furi_assert(context); +} + +void hex_viewer_startscreen_enter(void* context) { + furi_assert(context); + HexViewerStartscreen* instance = (HexViewerStartscreen*)context; + with_view_model( + instance->view, + HexViewerStartscreenModel * model, + { update_local_model_from_app(instance->context, model); }, + true); +} + +HexViewerStartscreen* hex_viewer_startscreen_alloc() { + HexViewerStartscreen* instance = malloc(sizeof(HexViewerStartscreen)); + instance->view = view_alloc(); + view_allocate_model(instance->view, ViewModelTypeLocking, sizeof(HexViewerStartscreenModel)); + view_set_context(instance->view, instance); + view_set_draw_callback(instance->view, (ViewDrawCallback)hex_viewer_startscreen_draw); + view_set_input_callback(instance->view, hex_viewer_startscreen_input); + view_set_enter_callback(instance->view, hex_viewer_startscreen_enter); + view_set_exit_callback(instance->view, hex_viewer_startscreen_exit); + + with_view_model( + instance->view, + HexViewerStartscreenModel * model, + { hex_viewer_startscreen_model_init(model); }, + true); + + return instance; +} + +void hex_viewer_startscreen_free(HexViewerStartscreen* instance) { + furi_assert(instance); + + with_view_model( + instance->view, HexViewerStartscreenModel * model, { UNUSED(model); }, true); + view_free(instance->view); + free(instance); +} + +View* hex_viewer_startscreen_get_view(HexViewerStartscreen* instance) { + furi_assert(instance); + return instance->view; +} diff --git a/hex_viewer/views/hex_viewer_startscreen.h b/hex_viewer/views/hex_viewer_startscreen.h new file mode 100644 index 00000000000..3bda8274428 --- /dev/null +++ b/hex_viewer/views/hex_viewer_startscreen.h @@ -0,0 +1,19 @@ +#pragma once + +#include +#include "../helpers/hex_viewer_custom_event.h" + +typedef struct HexViewerStartscreen HexViewerStartscreen; + +typedef void (*HexViewerStartscreenCallback)(HexViewerCustomEvent event, void* context); + +void hex_viewer_startscreen_set_callback( + HexViewerStartscreen* hex_viewer_startscreen, + HexViewerStartscreenCallback callback, + void* context); + +View* hex_viewer_startscreen_get_view(HexViewerStartscreen* hex_viewer_static); + +HexViewerStartscreen* hex_viewer_startscreen_alloc(); + +void hex_viewer_startscreen_free(HexViewerStartscreen* hex_viewer_static); \ No newline at end of file diff --git a/ir_remote/.gitsubtree b/ir_remote/.gitsubtree new file mode 100644 index 00000000000..e44a44b31fe --- /dev/null +++ b/ir_remote/.gitsubtree @@ -0,0 +1 @@ +https://github.com/xMasterX/all-the-plugins dev non_catalog_apps/ir_remote diff --git a/ir_remote/README.md b/ir_remote/README.md new file mode 100644 index 00000000000..f1a3161db6e --- /dev/null +++ b/ir_remote/README.md @@ -0,0 +1,63 @@ +# Alternative Infrared Remote for Flipperzero + +It is a plugin like [UniversalRF Remix](https://github.com/ESurge/flipperzero-firmware-unirfremix) but for infrared files. I do this plugin for convenience, because the main IR app need to navigate for different button abit troublesome (buttons like up,down,left,right,back). I found it useful for TV and TV box. + +It supports short press and long press input for different ir remote buttons. Tested on the [unleashed firmware version unlshd-023](https://github.com/DarkFlippers/unleashed-firmware/releases/tag/unlshd-023) + + +## How to install + +1. Update unleashed firmware to the version unlshd-023, then download the `ir_remote.fap` from [releases](https://github.com/Hong5489/ir_remote/tags) + +2. Put the `ir_remote.fap` file in your flipper's SD card, under `apps` folder + +## How to use + +1. Similar to UniRF app, put the path of the ir file and the ir button for each button on flipper (UP,DOWN,LEFT,RIGHT,BACK) + +The format With `HOLD` one is long press, without is short press + +Example of the configuration file: +``` +REMOTE: /ext/infrared/Philips_32PFL4208T.ir +UP: Up +DOWN: Down +LEFT: Left +RIGHT: Right +OK: +BACK: Back +UPHOLD: VOL+ +DOWNHOLD: VOL- +LEFTHOLD: Source +RIGHTHOLD: SmartTV +OKHOLD: POWER +``` + +Leave it empty for the button you don't need + +2. Save it as `.txt` file, then create a new folder in your SD card `ir_remote`, put it inside the folder + +3. Lastly, you can open the app, choose the configuration file, then you can try out the ir for each buttons + +4. Long press back button to exit the app + +## How to build + +You can clone this repo and put it inside the `applications_user` folder, then build it with the command: +``` +./fbt fap_ir_remote +``` +Or you can build and run it on your flipper with the command: +``` +./fbt launch_app APPSRC=applications_user/ir_remote +``` + +## Screenshots + +Choose config file to map + +![image](ir.png) + +Show all button name in the config file (If empty will show N/A). Upper part short press, Lower part long press + +![image2](ir2.png) \ No newline at end of file diff --git a/ir_remote/application.fam b/ir_remote/application.fam new file mode 100644 index 00000000000..c6c3de570bd --- /dev/null +++ b/ir_remote/application.fam @@ -0,0 +1,18 @@ +App( + appid="ir_remote", + name="IR Remote", + apptype=FlipperAppType.EXTERNAL, + entry_point="infrared_remote_app", + stack_size=3 * 1024, + requires=[ + "gui", + "dialogs", + ], + fap_category="Infrared", + fap_icon="ir_10px.png", + fap_icon_assets="images", + fap_author="@Hong5489 & @friebel & @d4ve10", + fap_weburl="https://github.com/Hong5489/ir_remote", + fap_version="1.0", + fap_description="Bind any IR remote button to each button on flipper d-pad, provides another way to use flipper as IR remote.", +) diff --git a/ir_remote/example.txt b/ir_remote/example.txt new file mode 100644 index 00000000000..ffd192b8d32 --- /dev/null +++ b/ir_remote/example.txt @@ -0,0 +1,12 @@ +REMOTE: /ext/infrared/Philips_32PFL4208T.ir +UP: Up +DOWN: Down +LEFT: Left +RIGHT: Right +OK: +BACK: Back +UPHOLD: VOL+ +DOWNHOLD: VOL- +LEFTHOLD: Source +RIGHTHOLD: SmartTV +OKHOLD: POWER \ No newline at end of file diff --git a/ir_remote/images/ButtonDown_7x4.png b/ir_remote/images/ButtonDown_7x4.png new file mode 100644 index 00000000000..2954bb6a67d Binary files /dev/null and b/ir_remote/images/ButtonDown_7x4.png differ diff --git a/ir_remote/images/ButtonLeft_4x7.png b/ir_remote/images/ButtonLeft_4x7.png new file mode 100644 index 00000000000..0b4655d4324 Binary files /dev/null and b/ir_remote/images/ButtonLeft_4x7.png differ diff --git a/ir_remote/images/ButtonRight_4x7.png b/ir_remote/images/ButtonRight_4x7.png new file mode 100644 index 00000000000..8e1c74c1c00 Binary files /dev/null and b/ir_remote/images/ButtonRight_4x7.png differ diff --git a/ir_remote/images/ButtonUp_7x4.png b/ir_remote/images/ButtonUp_7x4.png new file mode 100644 index 00000000000..1be79328b40 Binary files /dev/null and b/ir_remote/images/ButtonUp_7x4.png differ diff --git a/ir_remote/images/Ok_btn_9x9.png b/ir_remote/images/Ok_btn_9x9.png new file mode 100644 index 00000000000..9a1539da204 Binary files /dev/null and b/ir_remote/images/Ok_btn_9x9.png differ diff --git a/ir_remote/images/back_10px.png b/ir_remote/images/back_10px.png new file mode 100644 index 00000000000..f9c615a99e6 Binary files /dev/null and b/ir_remote/images/back_10px.png differ diff --git a/ir_remote/images/sub1_10px.png b/ir_remote/images/sub1_10px.png new file mode 100644 index 00000000000..5a25fdf4ef1 Binary files /dev/null and b/ir_remote/images/sub1_10px.png differ diff --git a/ir_remote/infrared_remote.c b/ir_remote/infrared_remote.c new file mode 100644 index 00000000000..033df2d4e41 --- /dev/null +++ b/ir_remote/infrared_remote.c @@ -0,0 +1,188 @@ +#include "infrared_remote.h" + +#include +#include +#include +#include +#include +#include +#include + +#define TAG "InfraredRemote" + +ARRAY_DEF(InfraredButtonArray, InfraredRemoteButton*, M_PTR_OPLIST); + +struct InfraredRemote { + InfraredButtonArray_t buttons; + FuriString* name; + FuriString* path; +}; + +static void infrared_remote_clear_buttons(InfraredRemote* remote) { + InfraredButtonArray_it_t it; + for(InfraredButtonArray_it(it, remote->buttons); !InfraredButtonArray_end_p(it); + InfraredButtonArray_next(it)) { + infrared_remote_button_free(*InfraredButtonArray_cref(it)); + } + InfraredButtonArray_reset(remote->buttons); +} + +InfraredRemote* infrared_remote_alloc(void) { + InfraredRemote* remote = malloc(sizeof(InfraredRemote)); + InfraredButtonArray_init(remote->buttons); + remote->name = furi_string_alloc(); + remote->path = furi_string_alloc(); + return remote; +} + +void infrared_remote_free(InfraredRemote* remote) { + infrared_remote_clear_buttons(remote); + InfraredButtonArray_clear(remote->buttons); + furi_string_free(remote->path); + furi_string_free(remote->name); + free(remote); +} + +void infrared_remote_reset(InfraredRemote* remote) { + infrared_remote_clear_buttons(remote); + furi_string_reset(remote->name); + furi_string_reset(remote->path); +} + +void infrared_remote_set_name(InfraredRemote* remote, const char* name) { + furi_string_set(remote->name, name); +} + +const char* infrared_remote_get_name(InfraredRemote* remote) { + return furi_string_get_cstr(remote->name); +} + +void infrared_remote_set_path(InfraredRemote* remote, const char* path) { + furi_string_set(remote->path, path); +} + +const char* infrared_remote_get_path(InfraredRemote* remote) { + return furi_string_get_cstr(remote->path); +} + +size_t infrared_remote_get_button_count(InfraredRemote* remote) { + return InfraredButtonArray_size(remote->buttons); +} + +InfraredRemoteButton* infrared_remote_get_button(InfraredRemote* remote, size_t index) { + furi_assert(index < InfraredButtonArray_size(remote->buttons)); + return *InfraredButtonArray_get(remote->buttons, index); +} + +bool infrared_remote_find_button_by_name(InfraredRemote* remote, const char* name, size_t* index) { + for(size_t i = 0; i < InfraredButtonArray_size(remote->buttons); i++) { + InfraredRemoteButton* button = *InfraredButtonArray_get(remote->buttons, i); + if(!strcmp(infrared_remote_button_get_name(button), name)) { + *index = i; + return true; + } + } + return false; +} + +bool infrared_remote_add_button(InfraredRemote* remote, const char* name, InfraredSignal* signal) { + InfraredRemoteButton* button = infrared_remote_button_alloc(); + infrared_remote_button_set_name(button, name); + infrared_remote_button_set_signal(button, signal); + InfraredButtonArray_push_back(remote->buttons, button); + return infrared_remote_store(remote); +} + +bool infrared_remote_rename_button(InfraredRemote* remote, const char* new_name, size_t index) { + furi_assert(index < InfraredButtonArray_size(remote->buttons)); + InfraredRemoteButton* button = *InfraredButtonArray_get(remote->buttons, index); + infrared_remote_button_set_name(button, new_name); + return infrared_remote_store(remote); +} + +bool infrared_remote_delete_button(InfraredRemote* remote, size_t index) { + furi_assert(index < InfraredButtonArray_size(remote->buttons)); + InfraredRemoteButton* button; + InfraredButtonArray_pop_at(&button, remote->buttons, index); + infrared_remote_button_free(button); + return infrared_remote_store(remote); +} + +bool infrared_remote_store(InfraredRemote* remote) { + Storage* storage = furi_record_open(RECORD_STORAGE); + FlipperFormat* ff = flipper_format_file_alloc(storage); + const char* path = furi_string_get_cstr(remote->path); + + FURI_LOG_I(TAG, "store file: \'%s\'", path); + + bool success = flipper_format_file_open_always(ff, path) && + flipper_format_write_header_cstr(ff, "IR signals file", 1); + if(success) { + InfraredButtonArray_it_t it; + for(InfraredButtonArray_it(it, remote->buttons); !InfraredButtonArray_end_p(it); + InfraredButtonArray_next(it)) { + InfraredRemoteButton* button = *InfraredButtonArray_cref(it); + success = infrared_signal_save( + infrared_remote_button_get_signal(button), + ff, + infrared_remote_button_get_name(button)); + if(!success) { + break; + } + } + } + + flipper_format_free(ff); + furi_record_close(RECORD_STORAGE); + return success; +} + +bool infrared_remote_load(InfraredRemote* remote, FuriString* path) { + Storage* storage = furi_record_open(RECORD_STORAGE); + FlipperFormat* ff = flipper_format_buffered_file_alloc(storage); + + FuriString* buf; + buf = furi_string_alloc(); + + FURI_LOG_I(TAG, "load file: \'%s\'", furi_string_get_cstr(path)); + bool success = flipper_format_buffered_file_open_existing(ff, furi_string_get_cstr(path)); + + if(success) { + uint32_t version; + success = flipper_format_read_header(ff, buf, &version) && + !furi_string_cmp(buf, "IR signals file") && (version == 1); + } + + if(success) { + path_extract_filename(path, buf, true); + infrared_remote_clear_buttons(remote); + infrared_remote_set_name(remote, furi_string_get_cstr(buf)); + infrared_remote_set_path(remote, furi_string_get_cstr(path)); + + for(bool can_read = true; can_read;) { + InfraredRemoteButton* button = infrared_remote_button_alloc(); + can_read = infrared_signal_read(infrared_remote_button_get_signal(button), ff, buf); + if(can_read) { + infrared_remote_button_set_name(button, furi_string_get_cstr(buf)); + InfraredButtonArray_push_back(remote->buttons, button); + } else { + infrared_remote_button_free(button); + } + } + } + + furi_string_free(buf); + flipper_format_free(ff); + furi_record_close(RECORD_STORAGE); + return success; +} + +bool infrared_remote_remove(InfraredRemote* remote) { + Storage* storage = furi_record_open(RECORD_STORAGE); + + FS_Error status = storage_common_remove(storage, furi_string_get_cstr(remote->path)); + infrared_remote_reset(remote); + + furi_record_close(RECORD_STORAGE); + return (status == FSE_OK || status == FSE_NOT_EXIST); +} diff --git a/ir_remote/infrared_remote.h b/ir_remote/infrared_remote.h new file mode 100644 index 00000000000..fe3dc851adb --- /dev/null +++ b/ir_remote/infrared_remote.h @@ -0,0 +1,31 @@ +#pragma once + +#include + +#include "infrared_remote_button.h" + +#define IR_REMOTE_PATH EXT_PATH("infrared/remote") + +typedef struct InfraredRemote InfraredRemote; + +InfraredRemote* infrared_remote_alloc(void); +void infrared_remote_free(InfraredRemote* remote); +void infrared_remote_reset(InfraredRemote* remote); + +void infrared_remote_set_name(InfraredRemote* remote, const char* name); +const char* infrared_remote_get_name(InfraredRemote* remote); + +void infrared_remote_set_path(InfraredRemote* remote, const char* path); +const char* infrared_remote_get_path(InfraredRemote* remote); + +size_t infrared_remote_get_button_count(InfraredRemote* remote); +InfraredRemoteButton* infrared_remote_get_button(InfraredRemote* remote, size_t index); +bool infrared_remote_find_button_by_name(InfraredRemote* remote, const char* name, size_t* index); + +bool infrared_remote_add_button(InfraredRemote* remote, const char* name, InfraredSignal* signal); +bool infrared_remote_rename_button(InfraredRemote* remote, const char* new_name, size_t index); +bool infrared_remote_delete_button(InfraredRemote* remote, size_t index); + +bool infrared_remote_store(InfraredRemote* remote); +bool infrared_remote_load(InfraredRemote* remote, FuriString* path); +bool infrared_remote_remove(InfraredRemote* remote); diff --git a/ir_remote/infrared_remote_app.c b/ir_remote/infrared_remote_app.c new file mode 100644 index 00000000000..aa918bc7d79 --- /dev/null +++ b/ir_remote/infrared_remote_app.c @@ -0,0 +1,610 @@ +#include +#include + +#include + +#include +#include +#include +#include + +#include +#include + +#include +#include + +#include "infrared_signal.h" +#include "infrared_remote.h" +#include "infrared_remote_button.h" +#define TAG "ir_remote" + +#include + +typedef struct { + int status; + ViewPort* view_port; + FuriString* up_button; + FuriString* down_button; + FuriString* left_button; + FuriString* right_button; + FuriString* ok_button; + FuriString* back_button; + FuriString* up_hold_button; + FuriString* down_hold_button; + FuriString* left_hold_button; + FuriString* right_hold_button; + FuriString* ok_hold_button; + InfraredWorker* infrared_worker; +} IRApp; + +// Screen is 128x64 px +static void app_draw_callback(Canvas* canvas, void* ctx) { + // Show config is incorrect when cannot read the remote file + // Showing button string in the screen, upper part is short press, lower part is long press + IRApp* app = ctx; + if(app->status) { + canvas_clear(canvas); + view_port_set_orientation(app->view_port, ViewPortOrientationHorizontal); + canvas_set_font(canvas, FontPrimary); + canvas_draw_str_aligned(canvas, 62, 5, AlignCenter, AlignTop, "Config is incorrect."); + canvas_set_font(canvas, FontSecondary); + canvas_draw_str_aligned(canvas, 62, 30, AlignCenter, AlignTop, "Please configure map."); + canvas_draw_str_aligned(canvas, 62, 60, AlignCenter, AlignBottom, "Press Back to Exit."); + } else { + canvas_clear(canvas); + view_port_set_orientation(app->view_port, ViewPortOrientationVertical); + canvas_draw_icon(canvas, 1, 5, &I_ButtonUp_7x4); + canvas_draw_icon(canvas, 1, 15, &I_ButtonDown_7x4); + canvas_draw_icon(canvas, 2, 23, &I_ButtonLeft_4x7); + canvas_draw_icon(canvas, 2, 33, &I_ButtonRight_4x7); + canvas_draw_icon(canvas, 0, 42, &I_Ok_btn_9x9); + canvas_draw_icon(canvas, 0, 53, &I_back_10px); + + //Labels + canvas_set_font(canvas, FontSecondary); + + canvas_draw_str_aligned( + canvas, 32, 8, AlignCenter, AlignCenter, furi_string_get_cstr(app->up_button)); + canvas_draw_str_aligned( + canvas, 32, 18, AlignCenter, AlignCenter, furi_string_get_cstr(app->down_button)); + canvas_draw_str_aligned( + canvas, 32, 28, AlignCenter, AlignCenter, furi_string_get_cstr(app->left_button)); + canvas_draw_str_aligned( + canvas, 32, 38, AlignCenter, AlignCenter, furi_string_get_cstr(app->right_button)); + canvas_draw_str_aligned( + canvas, 32, 48, AlignCenter, AlignCenter, furi_string_get_cstr(app->ok_button)); + canvas_draw_str_aligned( + canvas, 32, 58, AlignCenter, AlignCenter, furi_string_get_cstr(app->back_button)); + + canvas_draw_line(canvas, 0, 65, 64, 65); + + canvas_draw_icon(canvas, 1, 70, &I_ButtonUp_7x4); + canvas_draw_icon(canvas, 1, 80, &I_ButtonDown_7x4); + canvas_draw_icon(canvas, 2, 88, &I_ButtonLeft_4x7); + canvas_draw_icon(canvas, 2, 98, &I_ButtonRight_4x7); + canvas_draw_icon(canvas, 0, 107, &I_Ok_btn_9x9); + canvas_draw_icon(canvas, 0, 118, &I_back_10px); + + canvas_draw_str_aligned( + canvas, 32, 73, AlignCenter, AlignCenter, furi_string_get_cstr(app->up_hold_button)); + canvas_draw_str_aligned( + canvas, 32, 83, AlignCenter, AlignCenter, furi_string_get_cstr(app->down_hold_button)); + canvas_draw_str_aligned( + canvas, 32, 93, AlignCenter, AlignCenter, furi_string_get_cstr(app->left_hold_button)); + canvas_draw_str_aligned( + canvas, + 32, + 103, + AlignCenter, + AlignCenter, + furi_string_get_cstr(app->right_hold_button)); + canvas_draw_str_aligned( + canvas, 32, 113, AlignCenter, AlignCenter, furi_string_get_cstr(app->ok_hold_button)); + canvas_draw_str_aligned(canvas, 32, 123, AlignCenter, AlignCenter, "Exit App"); + } +} + +static void app_input_callback(InputEvent* input_event, void* ctx) { + furi_assert(ctx); + + FuriMessageQueue* event_queue = ctx; + furi_message_queue_put(event_queue, input_event, FuriWaitForever); +} + +int32_t infrared_remote_app(char* p) { + FuriMessageQueue* event_queue = furi_message_queue_alloc(8, sizeof(InputEvent)); + + // App button string + IRApp* app = malloc(sizeof(IRApp)); + app->up_button = furi_string_alloc(); + app->down_button = furi_string_alloc(); + app->left_button = furi_string_alloc(); + app->right_button = furi_string_alloc(); + app->ok_button = furi_string_alloc(); + app->back_button = furi_string_alloc(); + app->up_hold_button = furi_string_alloc(); + app->down_hold_button = furi_string_alloc(); + app->left_hold_button = furi_string_alloc(); + app->right_hold_button = furi_string_alloc(); + app->ok_hold_button = furi_string_alloc(); + app->view_port = view_port_alloc(); + app->infrared_worker = infrared_worker_alloc(); + + // Configure view port + view_port_draw_callback_set(app->view_port, app_draw_callback, app); + view_port_input_callback_set(app->view_port, app_input_callback, event_queue); + + // Register view port in GUI + Gui* gui = furi_record_open(RECORD_GUI); + gui_add_view_port(gui, app->view_port, GuiLayerFullscreen); + + InputEvent event; + + FuriString* map_file = furi_string_alloc(); + Storage* storage = furi_record_open(RECORD_STORAGE); + FlipperFormat* ff = flipper_format_file_alloc(storage); + if(!storage_file_exists(storage, IR_REMOTE_PATH)) { + storage_common_mkdir(storage, IR_REMOTE_PATH); //Make Folder If dir not exist + } + + bool res; + if(p && strlen(p)) { + furi_string_set(map_file, p); + res = true; + } else { + DialogsApp* dialogs = furi_record_open(RECORD_DIALOGS); + DialogsFileBrowserOptions browser_options; + dialog_file_browser_set_basic_options(&browser_options, ".txt", &I_sub1_10px); + browser_options.base_path = IR_REMOTE_PATH; + furi_string_set(map_file, IR_REMOTE_PATH); + res = dialog_file_browser_show(dialogs, map_file, map_file, &browser_options); + furi_record_close(RECORD_DIALOGS); + } + + // if user didn't choose anything, free everything and exit + if(!res) { + FURI_LOG_I(TAG, "exit"); + flipper_format_free(ff); + furi_record_close(RECORD_STORAGE); + + furi_string_free(app->up_button); + furi_string_free(app->down_button); + furi_string_free(app->left_button); + furi_string_free(app->right_button); + furi_string_free(app->ok_button); + furi_string_free(app->back_button); + furi_string_free(app->up_hold_button); + furi_string_free(app->down_hold_button); + furi_string_free(app->left_hold_button); + furi_string_free(app->right_hold_button); + furi_string_free(app->ok_hold_button); + + view_port_enabled_set(app->view_port, false); + gui_remove_view_port(gui, app->view_port); + view_port_free(app->view_port); + free(app); + furi_message_queue_free(event_queue); + + furi_record_close(RECORD_GUI); + return 255; + } + + InfraredRemote* remote = infrared_remote_alloc(); + FuriString* remote_path = furi_string_alloc(); + + InfraredSignal* up_signal = infrared_signal_alloc(); + InfraredSignal* down_signal = infrared_signal_alloc(); + InfraredSignal* left_signal = infrared_signal_alloc(); + InfraredSignal* right_signal = infrared_signal_alloc(); + InfraredSignal* ok_signal = infrared_signal_alloc(); + InfraredSignal* back_signal = infrared_signal_alloc(); + InfraredSignal* up_hold_signal = infrared_signal_alloc(); + InfraredSignal* down_hold_signal = infrared_signal_alloc(); + InfraredSignal* left_hold_signal = infrared_signal_alloc(); + InfraredSignal* right_hold_signal = infrared_signal_alloc(); + InfraredSignal* ok_hold_signal = infrared_signal_alloc(); + + InfraredSignal* active_signal = NULL; + bool is_transmitting = false; + + bool up_enabled = false; + bool down_enabled = false; + bool left_enabled = false; + bool right_enabled = false; + bool ok_enabled = false; + bool back_enabled = false; + bool up_hold_enabled = false; + bool down_hold_enabled = false; + bool left_hold_enabled = false; + bool right_hold_enabled = false; + bool ok_hold_enabled = false; + + if(!flipper_format_file_open_existing(ff, furi_string_get_cstr(map_file))) { + FURI_LOG_E(TAG, "Could not open MAP file %s", furi_string_get_cstr(map_file)); + app->status = 1; + } else { + //Filename Assignment/Check Start + + if(!flipper_format_read_string(ff, "REMOTE", remote_path)) { + FURI_LOG_E(TAG, "Could not read REMOTE string"); + app->status = 1; + } else { + if(!infrared_remote_load(remote, remote_path)) { + FURI_LOG_E(TAG, "Could not load ir file: %s", furi_string_get_cstr(remote_path)); + app->status = 1; + } else { + FURI_LOG_I(TAG, "Loaded REMOTE file: %s", furi_string_get_cstr(remote_path)); + } + } + + //assign variables to values within map file + //set missing filenames to N/A + //assign button signals + size_t index = 0; + if(!flipper_format_read_string(ff, "UP", app->up_button)) { + FURI_LOG_W(TAG, "Could not read UP string"); + furi_string_set(app->up_button, "N/A"); + } else { + if(!infrared_remote_find_button_by_name( + remote, furi_string_get_cstr(app->up_button), &index)) { + FURI_LOG_W(TAG, "Error"); + } else { + up_signal = + infrared_remote_button_get_signal(infrared_remote_get_button(remote, index)); + up_enabled = true; + } + } + + if(!flipper_format_read_string(ff, "DOWN", app->down_button)) { + FURI_LOG_W(TAG, "Could not read DOWN string"); + furi_string_set(app->down_button, "N/A"); + } else { + if(!infrared_remote_find_button_by_name( + remote, furi_string_get_cstr(app->down_button), &index)) { + FURI_LOG_W(TAG, "Error"); + } else { + down_signal = + infrared_remote_button_get_signal(infrared_remote_get_button(remote, index)); + down_enabled = true; + } + } + + if(!flipper_format_read_string(ff, "LEFT", app->left_button)) { + FURI_LOG_W(TAG, "Could not read LEFT string"); + furi_string_set(app->left_button, "N/A"); + } else { + if(!infrared_remote_find_button_by_name( + remote, furi_string_get_cstr(app->left_button), &index)) { + FURI_LOG_W(TAG, "Error"); + } else { + left_signal = + infrared_remote_button_get_signal(infrared_remote_get_button(remote, index)); + left_enabled = true; + } + } + + if(!flipper_format_read_string(ff, "RIGHT", app->right_button)) { + FURI_LOG_W(TAG, "Could not read RIGHT string"); + furi_string_set(app->right_button, "N/A"); + } else { + if(!infrared_remote_find_button_by_name( + remote, furi_string_get_cstr(app->right_button), &index)) { + FURI_LOG_W(TAG, "Error"); + } else { + right_signal = + infrared_remote_button_get_signal(infrared_remote_get_button(remote, index)); + right_enabled = true; + } + } + + if(!flipper_format_read_string(ff, "OK", app->ok_button)) { + FURI_LOG_W(TAG, "Could not read OK string"); + furi_string_set(app->ok_button, "N/A"); + } else { + if(!infrared_remote_find_button_by_name( + remote, furi_string_get_cstr(app->ok_button), &index)) { + FURI_LOG_W(TAG, "Error"); + } else { + ok_signal = + infrared_remote_button_get_signal(infrared_remote_get_button(remote, index)); + ok_enabled = true; + } + } + + if(!flipper_format_read_string(ff, "BACK", app->back_button)) { + FURI_LOG_W(TAG, "Could not read BACK string"); + furi_string_set(app->back_button, "N/A"); + } else { + if(!infrared_remote_find_button_by_name( + remote, furi_string_get_cstr(app->back_button), &index)) { + FURI_LOG_W(TAG, "Error"); + } else { + back_signal = + infrared_remote_button_get_signal(infrared_remote_get_button(remote, index)); + back_enabled = true; + } + } + + if(!flipper_format_read_string(ff, "UPHOLD", app->up_hold_button)) { + FURI_LOG_W(TAG, "Could not read UPHOLD string"); + furi_string_set(app->up_hold_button, "N/A"); + } else { + if(!infrared_remote_find_button_by_name( + remote, furi_string_get_cstr(app->up_hold_button), &index)) { + FURI_LOG_W(TAG, "Error"); + } else { + up_hold_signal = + infrared_remote_button_get_signal(infrared_remote_get_button(remote, index)); + up_hold_enabled = true; + } + } + + if(!flipper_format_read_string(ff, "DOWNHOLD", app->down_hold_button)) { + FURI_LOG_W(TAG, "Could not read DOWNHOLD string"); + furi_string_set(app->down_hold_button, "N/A"); + } else { + if(!infrared_remote_find_button_by_name( + remote, furi_string_get_cstr(app->down_hold_button), &index)) { + FURI_LOG_W(TAG, "Error"); + } else { + down_hold_signal = + infrared_remote_button_get_signal(infrared_remote_get_button(remote, index)); + down_hold_enabled = true; + } + } + + if(!flipper_format_read_string(ff, "LEFTHOLD", app->left_hold_button)) { + FURI_LOG_W(TAG, "Could not read LEFTHOLD string"); + furi_string_set(app->left_hold_button, "N/A"); + } else { + if(!infrared_remote_find_button_by_name( + remote, furi_string_get_cstr(app->left_hold_button), &index)) { + FURI_LOG_W(TAG, "Error"); + } else { + left_hold_signal = + infrared_remote_button_get_signal(infrared_remote_get_button(remote, index)); + left_hold_enabled = true; + } + } + + if(!flipper_format_read_string(ff, "RIGHTHOLD", app->right_hold_button)) { + FURI_LOG_W(TAG, "Could not read RIGHTHOLD string"); + furi_string_set(app->right_hold_button, "N/A"); + } else { + if(!infrared_remote_find_button_by_name( + remote, furi_string_get_cstr(app->right_hold_button), &index)) { + FURI_LOG_W(TAG, "Error"); + } else { + right_hold_signal = + infrared_remote_button_get_signal(infrared_remote_get_button(remote, index)); + right_hold_enabled = true; + } + } + + if(!flipper_format_read_string(ff, "OKHOLD", app->ok_hold_button)) { + FURI_LOG_W(TAG, "Could not read OKHOLD string"); + furi_string_set(app->ok_hold_button, "N/A"); + } else { + if(!infrared_remote_find_button_by_name( + remote, furi_string_get_cstr(app->ok_hold_button), &index)) { + FURI_LOG_W(TAG, "Error"); + } else { + ok_hold_signal = + infrared_remote_button_get_signal(infrared_remote_get_button(remote, index)); + ok_hold_enabled = true; + } + } + } + + furi_string_free(remote_path); + + flipper_format_free(ff); + furi_record_close(RECORD_STORAGE); + + bool otg_was_enabled = furi_hal_power_is_otg_enabled(); + InfraredSettings settings = {0}; + saved_struct_load( + INFRARED_SETTINGS_PATH, + &settings, + sizeof(InfraredSettings), + INFRARED_SETTINGS_MAGIC, + INFRARED_SETTINGS_VERSION); + if(settings.tx_pin < FuriHalInfraredTxPinMax) { + furi_hal_infrared_set_tx_output(settings.tx_pin); + if(settings.otg_enabled != otg_was_enabled) { + if(settings.otg_enabled) { + furi_hal_power_enable_otg(); + } else { + furi_hal_power_disable_otg(); + } + } + } else { + FuriHalInfraredTxPin tx_pin_detected = furi_hal_infrared_detect_tx_output(); + furi_hal_infrared_set_tx_output(tx_pin_detected); + if(tx_pin_detected != FuriHalInfraredTxPinInternal) { + furi_hal_power_enable_otg(); + } + } + + bool running = true; + NotificationApp* notification = furi_record_open(RECORD_NOTIFICATION); + + if(app->status) { + view_port_update(app->view_port); + while(running) { + if(furi_message_queue_get(event_queue, &event, 100) == FuriStatusOk) { + if(event.type == InputTypeShort) { + switch(event.key) { + case InputKeyBack: + running = false; + break; + default: + break; + } + } + } + } + } else { + view_port_update(app->view_port); + while(running) { + if(furi_message_queue_get(event_queue, &event, 100) == FuriStatusOk) { + // short press signal + if(event.type == InputTypeShort) { + switch(event.key) { + case InputKeyUp: + if(up_enabled) { + active_signal = up_signal; + FURI_LOG_I(TAG, "up"); + } + break; + case InputKeyDown: + if(down_enabled) { + active_signal = down_signal; + FURI_LOG_I(TAG, "down"); + } + break; + case InputKeyRight: + if(right_enabled) { + active_signal = right_signal; + FURI_LOG_I(TAG, "right"); + } + break; + case InputKeyLeft: + if(left_enabled) { + active_signal = left_signal; + FURI_LOG_I(TAG, "left"); + } + break; + case InputKeyOk: + if(ok_enabled) { + active_signal = ok_signal; + FURI_LOG_I(TAG, "ok"); + } + break; + case InputKeyBack: + if(back_enabled) { + active_signal = back_signal; + FURI_LOG_I(TAG, "back"); + } + break; + default: + running = false; + break; + } + // long press signal + } else if(event.type == InputTypeLong) { + switch(event.key) { + case InputKeyUp: + if(up_hold_enabled) { + active_signal = up_hold_signal; + FURI_LOG_I(TAG, "up!"); + } + break; + case InputKeyDown: + if(down_hold_enabled) { + active_signal = down_hold_signal; + FURI_LOG_I(TAG, "down!"); + } + break; + case InputKeyRight: + if(right_hold_enabled) { + active_signal = right_hold_signal; + FURI_LOG_I(TAG, "right!"); + } + break; + case InputKeyLeft: + if(left_hold_enabled) { + active_signal = left_hold_signal; + FURI_LOG_I(TAG, "left!"); + } + break; + case InputKeyOk: + if(ok_hold_enabled) { + active_signal = ok_hold_signal; + FURI_LOG_I(TAG, "ok!"); + } + break; + default: + running = false; + break; + } + } else if(event.type == InputTypeRelease && is_transmitting) { + notification_message(notification, &sequence_blink_stop); + infrared_worker_tx_stop(app->infrared_worker); + is_transmitting = false; + active_signal = NULL; + } + + if(active_signal != NULL && + (event.type == InputTypeShort || event.type == InputTypeLong)) { + if(is_transmitting) { + infrared_worker_tx_stop(app->infrared_worker); + } + + if(infrared_signal_is_raw(active_signal)) { + InfraredRawSignal* raw_signal = + infrared_signal_get_raw_signal(active_signal); + infrared_worker_set_raw_signal( + app->infrared_worker, + raw_signal->timings, + raw_signal->timings_size, + raw_signal->frequency, + raw_signal->duty_cycle); + } else { + InfraredMessage* message = infrared_signal_get_message(active_signal); + infrared_worker_set_decoded_signal(app->infrared_worker, message); + } + + infrared_worker_tx_set_get_signal_callback( + app->infrared_worker, infrared_worker_tx_get_signal_steady_callback, app); + + infrared_worker_tx_start(app->infrared_worker); + notification_message(notification, &sequence_blink_start_magenta); + is_transmitting = true; + } + } + view_port_update(app->view_port); + } + } + + furi_hal_infrared_set_tx_output(FuriHalInfraredTxPinInternal); + if(furi_hal_power_is_otg_enabled() != otg_was_enabled) { + if(otg_was_enabled) { + furi_hal_power_enable_otg(); + } else { + furi_hal_power_disable_otg(); + } + } + + // Free all things + furi_string_free(app->up_button); + furi_string_free(app->down_button); + furi_string_free(app->left_button); + furi_string_free(app->right_button); + furi_string_free(app->ok_button); + furi_string_free(app->back_button); + furi_string_free(app->up_hold_button); + furi_string_free(app->down_hold_button); + furi_string_free(app->left_hold_button); + furi_string_free(app->right_hold_button); + furi_string_free(app->ok_hold_button); + + if(is_transmitting) { + infrared_worker_tx_stop(app->infrared_worker); + notification_message(notification, &sequence_blink_stop); + } + infrared_worker_free(app->infrared_worker); + + infrared_remote_free(remote); + view_port_enabled_set(app->view_port, false); + gui_remove_view_port(gui, app->view_port); + view_port_free(app->view_port); + free(app); + furi_message_queue_free(event_queue); + + furi_record_close(RECORD_NOTIFICATION); + furi_record_close(RECORD_GUI); + + return 0; +} diff --git a/ir_remote/infrared_remote_button.c b/ir_remote/infrared_remote_button.c new file mode 100644 index 00000000000..510c9da12b9 --- /dev/null +++ b/ir_remote/infrared_remote_button.c @@ -0,0 +1,37 @@ +#include "infrared_remote_button.h" + +#include + +struct InfraredRemoteButton { + FuriString* name; + InfraredSignal* signal; +}; + +InfraredRemoteButton* infrared_remote_button_alloc(void) { + InfraredRemoteButton* button = malloc(sizeof(InfraredRemoteButton)); + button->name = furi_string_alloc(); + button->signal = infrared_signal_alloc(); + return button; +} + +void infrared_remote_button_free(InfraredRemoteButton* button) { + furi_string_free(button->name); + infrared_signal_free(button->signal); + free(button); +} + +void infrared_remote_button_set_name(InfraredRemoteButton* button, const char* name) { + furi_string_set(button->name, name); +} + +const char* infrared_remote_button_get_name(InfraredRemoteButton* button) { + return furi_string_get_cstr(button->name); +} + +void infrared_remote_button_set_signal(InfraredRemoteButton* button, InfraredSignal* signal) { + infrared_signal_set_signal(button->signal, signal); +} + +InfraredSignal* infrared_remote_button_get_signal(InfraredRemoteButton* button) { + return button->signal; +} diff --git a/ir_remote/infrared_remote_button.h b/ir_remote/infrared_remote_button.h new file mode 100644 index 00000000000..1b9b651884b --- /dev/null +++ b/ir_remote/infrared_remote_button.h @@ -0,0 +1,14 @@ +#pragma once + +#include "infrared_signal.h" + +typedef struct InfraredRemoteButton InfraredRemoteButton; + +InfraredRemoteButton* infrared_remote_button_alloc(void); +void infrared_remote_button_free(InfraredRemoteButton* button); + +void infrared_remote_button_set_name(InfraredRemoteButton* button, const char* name); +const char* infrared_remote_button_get_name(InfraredRemoteButton* button); + +void infrared_remote_button_set_signal(InfraredRemoteButton* button, InfraredSignal* signal); +InfraredSignal* infrared_remote_button_get_signal(InfraredRemoteButton* button); diff --git a/ir_remote/infrared_signal.c b/ir_remote/infrared_signal.c new file mode 100644 index 00000000000..3c0944ea7ba --- /dev/null +++ b/ir_remote/infrared_signal.c @@ -0,0 +1,300 @@ +#include "infrared_signal.h" + +#include +#include +#include +#include +#include + +#define TAG "InfraredSignal" + +struct InfraredSignal { + bool is_raw; + union { + InfraredMessage message; + InfraredRawSignal raw; + } payload; +}; + +static void infrared_signal_clear_timings(InfraredSignal* signal) { + if(signal->is_raw) { + free(signal->payload.raw.timings); + signal->payload.raw.timings_size = 0; + signal->payload.raw.timings = NULL; + } +} + +static bool infrared_signal_is_message_valid(InfraredMessage* message) { + if(!infrared_is_protocol_valid(message->protocol)) { + FURI_LOG_E(TAG, "Unknown protocol"); + return false; + } + + uint32_t address_length = infrared_get_protocol_address_length(message->protocol); + uint32_t address_mask = (1UL << address_length) - 1; + + if(message->address != (message->address & address_mask)) { + FURI_LOG_E( + TAG, + "Address is out of range (mask 0x%08lX): 0x%lX\r\n", + address_mask, + message->address); + return false; + } + + uint32_t command_length = infrared_get_protocol_command_length(message->protocol); + uint32_t command_mask = (1UL << command_length) - 1; + + if(message->command != (message->command & command_mask)) { + FURI_LOG_E( + TAG, + "Command is out of range (mask 0x%08lX): 0x%lX\r\n", + command_mask, + message->command); + return false; + } + + return true; +} + +static bool infrared_signal_is_raw_valid(InfraredRawSignal* raw) { + if((raw->frequency > INFRARED_MAX_FREQUENCY) || (raw->frequency < INFRARED_MIN_FREQUENCY)) { + FURI_LOG_E( + TAG, + "Frequency is out of range (%X - %X): %lX", + INFRARED_MIN_FREQUENCY, + INFRARED_MAX_FREQUENCY, + raw->frequency); + return false; + + } else if((raw->duty_cycle <= 0) || (raw->duty_cycle > 1)) { + FURI_LOG_E(TAG, "Duty cycle is out of range (0 - 1): %f", (double)raw->duty_cycle); + return false; + + } else if((raw->timings_size <= 0) || (raw->timings_size > MAX_TIMINGS_AMOUNT)) { + FURI_LOG_E( + TAG, + "Timings amount is out of range (0 - %X): %X", + MAX_TIMINGS_AMOUNT, + raw->timings_size); + return false; + } + + return true; +} + +static inline bool infrared_signal_save_message(InfraredMessage* message, FlipperFormat* ff) { + const char* protocol_name = infrared_get_protocol_name(message->protocol); + return flipper_format_write_string_cstr(ff, "type", "parsed") && + flipper_format_write_string_cstr(ff, "protocol", protocol_name) && + flipper_format_write_hex(ff, "address", (uint8_t*)&message->address, 4) && + flipper_format_write_hex(ff, "command", (uint8_t*)&message->command, 4); +} + +static inline bool infrared_signal_save_raw(InfraredRawSignal* raw, FlipperFormat* ff) { + furi_assert(raw->timings_size <= MAX_TIMINGS_AMOUNT); + return flipper_format_write_string_cstr(ff, "type", "raw") && + flipper_format_write_uint32(ff, "frequency", &raw->frequency, 1) && + flipper_format_write_float(ff, "duty_cycle", &raw->duty_cycle, 1) && + flipper_format_write_uint32(ff, "data", raw->timings, raw->timings_size); +} + +static inline bool infrared_signal_read_message(InfraredSignal* signal, FlipperFormat* ff) { + FuriString* buf; + buf = furi_string_alloc(); + bool success = false; + + do { + if(!flipper_format_read_string(ff, "protocol", buf)) break; + + InfraredMessage message; + message.protocol = infrared_get_protocol_by_name(furi_string_get_cstr(buf)); + + success = flipper_format_read_hex(ff, "address", (uint8_t*)&message.address, 4) && + flipper_format_read_hex(ff, "command", (uint8_t*)&message.command, 4) && + infrared_signal_is_message_valid(&message); + + if(!success) break; + + infrared_signal_set_message(signal, &message); + } while(0); + + furi_string_free(buf); + return success; +} + +static inline bool infrared_signal_read_raw(InfraredSignal* signal, FlipperFormat* ff) { + uint32_t timings_size, frequency; + float duty_cycle; + + bool success = flipper_format_read_uint32(ff, "frequency", &frequency, 1) && + flipper_format_read_float(ff, "duty_cycle", &duty_cycle, 1) && + flipper_format_get_value_count(ff, "data", &timings_size); + + if(!success || timings_size > MAX_TIMINGS_AMOUNT) { + return false; + } + + uint32_t* timings = malloc(sizeof(uint32_t) * timings_size); + success = flipper_format_read_uint32(ff, "data", timings, timings_size); + + if(success) { + infrared_signal_set_raw_signal(signal, timings, timings_size, frequency, duty_cycle); + } + + free(timings); + return success; +} + +static bool infrared_signal_read_body(InfraredSignal* signal, FlipperFormat* ff) { + FuriString* tmp = furi_string_alloc(); + + bool success = false; + + do { + if(!flipper_format_read_string(ff, "type", tmp)) break; + if(furi_string_equal(tmp, "raw")) { + success = infrared_signal_read_raw(signal, ff); + } else if(furi_string_equal(tmp, "parsed")) { + success = infrared_signal_read_message(signal, ff); + } else { + FURI_LOG_E(TAG, "Unknown signal type"); + } + } while(false); + + furi_string_free(tmp); + return success; +} + +InfraredSignal* infrared_signal_alloc(void) { + InfraredSignal* signal = malloc(sizeof(InfraredSignal)); + + signal->is_raw = false; + signal->payload.message.protocol = InfraredProtocolUnknown; + + return signal; +} + +void infrared_signal_free(InfraredSignal* signal) { + infrared_signal_clear_timings(signal); + free(signal); +} + +bool infrared_signal_is_raw(InfraredSignal* signal) { + return signal->is_raw; +} + +bool infrared_signal_is_valid(InfraredSignal* signal) { + return signal->is_raw ? infrared_signal_is_raw_valid(&signal->payload.raw) : + infrared_signal_is_message_valid(&signal->payload.message); +} + +void infrared_signal_set_signal(InfraredSignal* signal, const InfraredSignal* other) { + if(other->is_raw) { + const InfraredRawSignal* raw = &other->payload.raw; + infrared_signal_set_raw_signal( + signal, raw->timings, raw->timings_size, raw->frequency, raw->duty_cycle); + } else { + const InfraredMessage* message = &other->payload.message; + infrared_signal_set_message(signal, message); + } +} + +void infrared_signal_set_raw_signal( + InfraredSignal* signal, + const uint32_t* timings, + size_t timings_size, + uint32_t frequency, + float duty_cycle) { + infrared_signal_clear_timings(signal); + + signal->is_raw = true; + + signal->payload.raw.timings_size = timings_size; + signal->payload.raw.frequency = frequency; + signal->payload.raw.duty_cycle = duty_cycle; + + signal->payload.raw.timings = malloc(timings_size * sizeof(uint32_t)); + memcpy(signal->payload.raw.timings, timings, timings_size * sizeof(uint32_t)); +} + +InfraredRawSignal* infrared_signal_get_raw_signal(InfraredSignal* signal) { + furi_assert(signal->is_raw); + return &signal->payload.raw; +} + +void infrared_signal_set_message(InfraredSignal* signal, const InfraredMessage* message) { + infrared_signal_clear_timings(signal); + + signal->is_raw = false; + signal->payload.message = *message; +} + +InfraredMessage* infrared_signal_get_message(InfraredSignal* signal) { + furi_assert(!signal->is_raw); + return &signal->payload.message; +} + +bool infrared_signal_save(InfraredSignal* signal, FlipperFormat* ff, const char* name) { + if(!flipper_format_write_comment_cstr(ff, "") || + !flipper_format_write_string_cstr(ff, "name", name)) { + return false; + } else if(signal->is_raw) { + return infrared_signal_save_raw(&signal->payload.raw, ff); + } else { + return infrared_signal_save_message(&signal->payload.message, ff); + } +} + +bool infrared_signal_read(InfraredSignal* signal, FlipperFormat* ff, FuriString* name) { + FuriString* tmp = furi_string_alloc(); + + bool success = false; + + do { + if(!flipper_format_read_string(ff, "name", tmp)) break; + furi_string_set(name, tmp); + if(!infrared_signal_read_body(signal, ff)) break; + success = true; + } while(0); + + furi_string_free(tmp); + return success; +} + +bool infrared_signal_search_and_read( + InfraredSignal* signal, + FlipperFormat* ff, + const FuriString* name) { + bool success = false; + FuriString* tmp = furi_string_alloc(); + + do { + bool is_name_found = false; + while(flipper_format_read_string(ff, "name", tmp)) { + is_name_found = furi_string_equal(name, tmp); + if(is_name_found) break; + } + if(!is_name_found) break; + if(!infrared_signal_read_body(signal, ff)) break; + success = true; + } while(false); + + furi_string_free(tmp); + return success; +} + +void infrared_signal_transmit(InfraredSignal* signal) { + if(signal->is_raw) { + InfraredRawSignal* raw_signal = &signal->payload.raw; + infrared_send_raw_ext( + raw_signal->timings, + raw_signal->timings_size, + true, + raw_signal->frequency, + raw_signal->duty_cycle); + } else { + InfraredMessage* message = &signal->payload.message; + infrared_send(message, 2); + } +} diff --git a/ir_remote/infrared_signal.h b/ir_remote/infrared_signal.h new file mode 100644 index 00000000000..e0ef355d9e1 --- /dev/null +++ b/ir_remote/infrared_signal.h @@ -0,0 +1,45 @@ +#pragma once + +#include +#include +#include + +#include +#include + +typedef struct InfraredSignal InfraredSignal; + +typedef struct { + size_t timings_size; + uint32_t* timings; + uint32_t frequency; + float duty_cycle; +} InfraredRawSignal; + +InfraredSignal* infrared_signal_alloc(void); +void infrared_signal_free(InfraredSignal* signal); + +bool infrared_signal_is_raw(InfraredSignal* signal); +bool infrared_signal_is_valid(InfraredSignal* signal); + +void infrared_signal_set_signal(InfraredSignal* signal, const InfraredSignal* other); + +void infrared_signal_set_raw_signal( + InfraredSignal* signal, + const uint32_t* timings, + size_t timings_size, + uint32_t frequency, + float duty_cycle); +InfraredRawSignal* infrared_signal_get_raw_signal(InfraredSignal* signal); + +void infrared_signal_set_message(InfraredSignal* signal, const InfraredMessage* message); +InfraredMessage* infrared_signal_get_message(InfraredSignal* signal); + +bool infrared_signal_save(InfraredSignal* signal, FlipperFormat* ff, const char* name); +bool infrared_signal_read(InfraredSignal* signal, FlipperFormat* ff, FuriString* name); +bool infrared_signal_search_and_read( + InfraredSignal* signal, + FlipperFormat* ff, + const FuriString* name); + +void infrared_signal_transmit(InfraredSignal* signal); diff --git a/ir_remote/ir.png b/ir_remote/ir.png new file mode 100755 index 00000000000..71bb60fa34c Binary files /dev/null and b/ir_remote/ir.png differ diff --git a/ir_remote/ir2.png b/ir_remote/ir2.png new file mode 100644 index 00000000000..133b1c86676 Binary files /dev/null and b/ir_remote/ir2.png differ diff --git a/ir_remote/ir_10px.png b/ir_remote/ir_10px.png new file mode 100644 index 00000000000..22c986180a2 Binary files /dev/null and b/ir_remote/ir_10px.png differ diff --git a/magspoof/scenes/mag_scene_saved_menu.c b/magspoof/scenes/mag_scene_saved_menu.c index fcf9fc5a080..feea2439c94 100644 --- a/magspoof/scenes/mag_scene_saved_menu.c +++ b/magspoof/scenes/mag_scene_saved_menu.c @@ -17,22 +17,6 @@ void mag_scene_saved_menu_on_enter(void* context) { Mag* mag = context; Submenu* submenu = mag->submenu; - // messy code to quickly check which tracks are available for emulation/display - // there's likely a better spot to do this, but the MagDevice functions don't have access to the full mag struct... - bool is_empty_t1 = furi_string_empty(mag->mag_dev->dev_data.track[0].str); - bool is_empty_t2 = furi_string_empty(mag->mag_dev->dev_data.track[1].str); - bool is_empty_t3 = furi_string_empty(mag->mag_dev->dev_data.track[2].str); - - if(!is_empty_t1 && !is_empty_t2) { - mag->state.track = MagTrackStateOneAndTwo; - } else if(!is_empty_t1) { - mag->state.track = MagTrackStateOne; - } else if(!is_empty_t2) { - mag->state.track = MagTrackStateTwo; - } else if(!is_empty_t3) { - mag->state.track = MagTrackStateThree; - } // TODO: what happens if no track data present? - submenu_add_item( submenu, "Emulate", SubmenuIndexEmulate, mag_scene_saved_menu_submenu_callback, mag); //submenu_add_item( diff --git a/mass_storage/.catalog/CHANGELOG.md b/mass_storage/.catalog/CHANGELOG.md new file mode 100644 index 00000000000..4b7d6d3be1e --- /dev/null +++ b/mass_storage/.catalog/CHANGELOG.md @@ -0,0 +1,17 @@ +## v.1.3 + +Minimal changes for recent API updates + +## v.1.2 + + * Fix deadlock on disk eject + * Locked USB notification + +## v.1.1 + + * Faster image creation + * Speed and transfer size in UI + +## v.1.0 + +Initial release. diff --git a/mass_storage/.catalog/README.md b/mass_storage/.catalog/README.md new file mode 100644 index 00000000000..91133249638 --- /dev/null +++ b/mass_storage/.catalog/README.md @@ -0,0 +1,3 @@ +# USB Mass Storage emulator + +This application allows you to use your Flipper Zero as a (very slow) USB mass storage device. You can create a disk image of up to 64MB, and then format and mount it on your computer via USB. All the images are stored on the SD card, which allows you to have multiple images and switch between them. \ No newline at end of file diff --git a/mass_storage/.catalog/screenshots/1.png b/mass_storage/.catalog/screenshots/1.png new file mode 100644 index 00000000000..71be82a8c0c Binary files /dev/null and b/mass_storage/.catalog/screenshots/1.png differ diff --git a/mass_storage/.catalog/screenshots/2.png b/mass_storage/.catalog/screenshots/2.png new file mode 100644 index 00000000000..c8036651f4d Binary files /dev/null and b/mass_storage/.catalog/screenshots/2.png differ diff --git a/mass_storage/.gitsubtree b/mass_storage/.gitsubtree new file mode 100644 index 00000000000..e2e26719e1a --- /dev/null +++ b/mass_storage/.gitsubtree @@ -0,0 +1,2 @@ +https://github.com/xMasterX/all-the-plugins dev base_pack/mass_storage +https://github.com/flipperdevices/flipperzero-good-faps dev mass_storage diff --git a/mass_storage/application.fam b/mass_storage/application.fam new file mode 100644 index 00000000000..48af27d5997 --- /dev/null +++ b/mass_storage/application.fam @@ -0,0 +1,16 @@ +App( + appid="mass_storage", + name="Mass Storage", + apptype=FlipperAppType.EXTERNAL, + entry_point="mass_storage_app", + requires=[ + "gui", + "dialogs", + ], + stack_size=2 * 1024, + fap_description="Implements a mass storage device over USB for disk images", + fap_version="1.3", + fap_icon="assets/floppydisk_10px.png", + fap_icon_assets="assets", + fap_category="USB", +) diff --git a/mass_storage/assets/ActiveConnection_50x64.png b/mass_storage/assets/ActiveConnection_50x64.png new file mode 100644 index 00000000000..1d7686dddf8 Binary files /dev/null and b/mass_storage/assets/ActiveConnection_50x64.png differ diff --git a/mass_storage/assets/Drive_112x35.png b/mass_storage/assets/Drive_112x35.png new file mode 100644 index 00000000000..6f7b9c8342b Binary files /dev/null and b/mass_storage/assets/Drive_112x35.png differ diff --git a/mass_storage/assets/floppydisk_10px.png b/mass_storage/assets/floppydisk_10px.png new file mode 100644 index 00000000000..91af40ba742 Binary files /dev/null and b/mass_storage/assets/floppydisk_10px.png differ diff --git a/mass_storage/helpers/mass_storage_scsi.c b/mass_storage/helpers/mass_storage_scsi.c new file mode 100644 index 00000000000..c1efacf8e23 --- /dev/null +++ b/mass_storage/helpers/mass_storage_scsi.c @@ -0,0 +1,266 @@ +#include "mass_storage_scsi.h" + +#include + +#define TAG "MassStorageSCSI" + +#define SCSI_TEST_UNIT_READY (0x00) +#define SCSI_REQUEST_SENSE (0x03) +#define SCSI_INQUIRY (0x12) +#define SCSI_READ_FORMAT_CAPACITIES (0x23) +#define SCSI_READ_CAPACITY_10 (0x25) +#define SCSI_MODE_SENSE_6 (0x1A) +#define SCSI_READ_10 (0x28) +#define SCSI_PREVENT_MEDIUM_REMOVAL (0x1E) +#define SCSI_START_STOP_UNIT (0x1B) +#define SCSI_WRITE_10 (0x2A) + +bool scsi_cmd_start(SCSISession* scsi, uint8_t* cmd, uint8_t len) { + if(!len) { + scsi->sk = SCSI_SK_ILLEGAL_REQUEST; + scsi->asc = SCSI_ASC_INVALID_COMMAND_OPERATION_CODE; + return false; + } + FURI_LOG_T(TAG, "START %02X", cmd[0]); + scsi->cmd = cmd; + scsi->cmd_len = len; + scsi->rx_done = false; + scsi->tx_done = false; + switch(cmd[0]) { + case SCSI_WRITE_10: { + if(len < 10) return false; + scsi->write_10.lba = cmd[2] << 24 | cmd[3] << 16 | cmd[4] << 8 | cmd[5]; + scsi->write_10.count = cmd[7] << 8 | cmd[8]; + FURI_LOG_D(TAG, "SCSI_WRITE_10 %08lX %04X", scsi->write_10.lba, scsi->write_10.count); + return true; + }; break; + case SCSI_READ_10: { + if(len < 10) return false; + scsi->read_10.lba = cmd[2] << 24 | cmd[3] << 16 | cmd[4] << 8 | cmd[5]; + scsi->read_10.count = cmd[7] << 8 | cmd[8]; + FURI_LOG_D(TAG, "SCSI_READ_10 %08lX %04X", scsi->read_10.lba, scsi->read_10.count); + return true; + }; break; + } + return true; +} + +bool scsi_cmd_rx_data(SCSISession* scsi, uint8_t* data, uint32_t len) { + FURI_LOG_T(TAG, "RX %02X len %lu", scsi->cmd[0], len); + if(scsi->rx_done) return false; + switch(scsi->cmd[0]) { + case SCSI_WRITE_10: { + uint32_t block_size = SCSI_BLOCK_SIZE; + uint16_t blocks = len / block_size; + bool result = + scsi->fn.write(scsi->fn.ctx, scsi->write_10.lba, blocks, data, blocks * block_size); + scsi->write_10.lba += blocks; + scsi->write_10.count -= blocks; + if(!scsi->write_10.count) { + scsi->rx_done = true; + } + return result; + }; break; + default: { + FURI_LOG_W(TAG, "unexpected scsi rx data cmd=%02X", scsi->cmd[0]); + scsi->sk = SCSI_SK_ILLEGAL_REQUEST; + scsi->asc = SCSI_ASC_INVALID_COMMAND_OPERATION_CODE; + return false; + }; break; + } +} + +bool scsi_cmd_tx_data(SCSISession* scsi, uint8_t* data, uint32_t* len, uint32_t cap) { + FURI_LOG_T(TAG, "TX %02X cap %lu", scsi->cmd[0], cap); + if(scsi->tx_done) return false; + switch(scsi->cmd[0]) { + case SCSI_REQUEST_SENSE: { + FURI_LOG_D(TAG, "SCSI_REQUEST_SENSE"); + if(cap < 18) return false; + memset(data, 0, cap); + data[0] = 0x70; // fixed format sense data + data[1] = 0; // obsolete + data[2] = scsi->sk; // sense key + data[3] = 0; // information + data[4] = 0; // information + data[5] = 0; // information + data[6] = 0; // information + data[7] = 10; // additional sense length (len-8) + data[8] = 0; // command specific information + data[9] = 0; // command specific information + data[10] = 0; // command specific information + data[11] = 0; // command specific information + data[12] = scsi->asc; // additional sense code + data[13] = 0; // additional sense code qualifier + data[14] = 0; // field replaceable unit code + data[15] = 0; // sense key specific information + data[16] = 0; // sense key specific information + data[17] = 0; // sense key specific information + *len = 18; + scsi->sk = 0; + scsi->asc = 0; + scsi->tx_done = true; + return true; + }; break; + case SCSI_INQUIRY: { + FURI_LOG_D(TAG, "SCSI_INQUIRY"); + if(scsi->cmd_len < 5) return false; + + if(cap < 36) return false; + + bool evpd = scsi->cmd[1] & 1; + uint8_t page_code = scsi->cmd[2]; + if(evpd == 0) { + if(page_code != 0) return false; + + data[0] = 0x00; // device type: direct access block device + data[1] = 0x80; // removable: true + data[2] = 0x04; // version + data[3] = 0x02; // response data format + data[4] = 31; // additional length (len - 5) + data[5] = 0; // flags + data[6] = 0; // flags + data[7] = 0; // flags + memcpy(data + 8, "Flipper ", 8); // vendor id + memcpy(data + 16, "Mass Storage ", 16); // product id + memcpy(data + 32, "0001", 4); // product revision level + *len = 36; + scsi->tx_done = true; + return true; + } else { + if(page_code != 0x80) { + FURI_LOG_W(TAG, "Unsupported VPD code %02X", page_code); + return false; + } + data[0] = 0x00; + data[1] = 0x80; + data[2] = 0x00; + data[3] = 0x01; // Serial len + data[4] = '0'; + *len = 5; + scsi->tx_done = true; + return true; + } + }; break; + case SCSI_READ_FORMAT_CAPACITIES: { + FURI_LOG_D(TAG, "SCSI_READ_FORMAT_CAPACITIES"); + if(cap < 12) { + return false; + } + uint32_t n_blocks = scsi->fn.num_blocks(scsi->fn.ctx); + uint32_t block_size = SCSI_BLOCK_SIZE; + // Capacity List Header + data[0] = 0; + data[1] = 0; + data[2] = 0; + data[3] = 8; + + // Capacity Descriptor + data[4] = (n_blocks - 1) >> 24; + data[5] = (n_blocks - 1) >> 16; + data[6] = (n_blocks - 1) >> 8; + data[7] = (n_blocks - 1) & 0xFF; + data[8] = 0x02; // Formatted media + data[9] = block_size >> 16; + data[10] = block_size >> 8; + data[11] = block_size & 0xFF; + *len = 12; + scsi->tx_done = true; + return true; + }; break; + case SCSI_READ_CAPACITY_10: { + FURI_LOG_D(TAG, "SCSI_READ_CAPACITY_10"); + if(cap < 8) return false; + uint32_t n_blocks = scsi->fn.num_blocks(scsi->fn.ctx); + uint32_t block_size = SCSI_BLOCK_SIZE; + data[0] = (n_blocks - 1) >> 24; + data[1] = (n_blocks - 1) >> 16; + data[2] = (n_blocks - 1) >> 8; + data[3] = (n_blocks - 1) & 0xFF; + data[4] = block_size >> 24; + data[5] = block_size >> 16; + data[6] = block_size >> 8; + data[7] = block_size & 0xFF; + *len = 8; + scsi->tx_done = true; + return true; + }; break; + case SCSI_MODE_SENSE_6: { + FURI_LOG_D(TAG, "SCSI_MODE_SENSE_6 %lu", cap); + if(cap < 4) return false; + data[0] = 3; // mode data length (len - 1) + data[1] = 0; // medium type + data[2] = 0; // device-specific parameter + data[3] = 0; // block descriptor length + *len = 4; + scsi->tx_done = true; + return true; + }; break; + case SCSI_READ_10: { + uint32_t block_size = SCSI_BLOCK_SIZE; + bool result = + scsi->fn.read(scsi->fn.ctx, scsi->read_10.lba, scsi->read_10.count, data, len, cap); + *len -= *len % block_size; + uint16_t blocks = *len / block_size; + scsi->read_10.lba += blocks; + scsi->read_10.count -= blocks; + if(!scsi->read_10.count) { + scsi->tx_done = true; + } + return result; + }; break; + default: { + FURI_LOG_W(TAG, "unexpected scsi tx data cmd=%02X", scsi->cmd[0]); + scsi->sk = SCSI_SK_ILLEGAL_REQUEST; + scsi->asc = SCSI_ASC_INVALID_COMMAND_OPERATION_CODE; + return false; + }; break; + } +} + +bool scsi_cmd_end(SCSISession* scsi) { + FURI_LOG_T(TAG, "END %02X", scsi->cmd[0]); + uint8_t* cmd = scsi->cmd; + uint8_t len = scsi->cmd_len; + scsi->cmd = NULL; + scsi->cmd_len = 0; + switch(cmd[0]) { + case SCSI_WRITE_10: + return scsi->rx_done; + + case SCSI_REQUEST_SENSE: + case SCSI_INQUIRY: + case SCSI_READ_FORMAT_CAPACITIES: + case SCSI_READ_CAPACITY_10: + case SCSI_MODE_SENSE_6: + case SCSI_READ_10: + return scsi->tx_done; + + case SCSI_TEST_UNIT_READY: { + FURI_LOG_D(TAG, "SCSI_TEST_UNIT_READY"); + return true; + }; break; + case SCSI_PREVENT_MEDIUM_REMOVAL: { + if(len < 6) return false; + bool prevent = cmd[5]; + FURI_LOG_D(TAG, "SCSI_PREVENT_MEDIUM_REMOVAL prevent=%d", prevent); + return !prevent; + }; break; + case SCSI_START_STOP_UNIT: { + if(len < 6) return false; + bool eject = (cmd[4] & 2) != 0; + bool start = (cmd[4] & 1) != 0; + FURI_LOG_D(TAG, "SCSI_START_STOP_UNIT eject=%d start=%d", eject, start); + if(eject) { + scsi->fn.eject(scsi->fn.ctx); + } + return true; + }; break; + default: { + FURI_LOG_W(TAG, "unexpected scsi cmd=%02X", cmd[0]); + scsi->sk = SCSI_SK_ILLEGAL_REQUEST; + scsi->asc = SCSI_ASC_INVALID_COMMAND_OPERATION_CODE; + return false; + }; break; + } +} diff --git a/mass_storage/helpers/mass_storage_scsi.h b/mass_storage/helpers/mass_storage_scsi.h new file mode 100644 index 00000000000..a35d6aff322 --- /dev/null +++ b/mass_storage/helpers/mass_storage_scsi.h @@ -0,0 +1,56 @@ +#pragma once + +#include + +#define SCSI_BLOCK_SIZE (0x200UL) + +#define SCSI_SK_ILLEGAL_REQUEST (5) + +#define SCSI_ASC_INVALID_COMMAND_OPERATION_CODE (0x20) +#define SCSI_ASC_LBA_OOB (0x21) +#define SCSI_ASC_INVALID_FIELD_IN_CDB (0x24) + +typedef struct { + void* ctx; + bool (*read)( + void* ctx, + uint32_t lba, + uint16_t count, + uint8_t* out, + uint32_t* out_len, + uint32_t out_cap); + bool (*write)(void* ctx, uint32_t lba, uint16_t count, uint8_t* buf, uint32_t len); + uint32_t (*num_blocks)(void* ctx); + void (*eject)(void* ctx); +} SCSIDeviceFunc; + +typedef struct { + SCSIDeviceFunc fn; + + uint8_t* cmd; + uint8_t cmd_len; + bool rx_done; + bool tx_done; + + uint8_t sk; // sense key + uint8_t asc; // additional sense code + + // command-specific data + // valid from cmd_start to cmd_end + union { + struct { + uint16_t count; + uint32_t lba; + } read_10; // SCSI_READ_10 + + struct { + uint16_t count; + uint32_t lba; + } write_10; // SCSI_WRITE_10 + }; +} SCSISession; + +bool scsi_cmd_start(SCSISession* scsi, uint8_t* cmd, uint8_t len); +bool scsi_cmd_rx_data(SCSISession* scsi, uint8_t* data, uint32_t len); +bool scsi_cmd_tx_data(SCSISession* scsi, uint8_t* data, uint32_t* len, uint32_t cap); +bool scsi_cmd_end(SCSISession* scsi); \ No newline at end of file diff --git a/mass_storage/helpers/mass_storage_usb.c b/mass_storage/helpers/mass_storage_usb.c new file mode 100644 index 00000000000..f493203a6a3 --- /dev/null +++ b/mass_storage/helpers/mass_storage_usb.c @@ -0,0 +1,481 @@ +#include "mass_storage_usb.h" +#include + +#define TAG "MassStorageUsb" + +#define USB_MSC_RX_EP (0x01) +#define USB_MSC_TX_EP (0x82) + +#define USB_MSC_RX_EP_SIZE (64UL) +#define USB_MSC_TX_EP_SIZE (64UL) + +#define USB_MSC_BOT_GET_MAX_LUN (0xFE) +#define USB_MSC_BOT_RESET (0xFF) + +#define CBW_SIG (0x43425355) +#define CBW_FLAGS_DEVICE_TO_HOST (0x80) + +#define CSW_SIG (0x53425355) +#define CSW_STATUS_OK (0) +#define CSW_STATUS_NOK (1) +#define CSW_STATUS_PHASE_ERROR (2) + +// must be SCSI_BLOCK_SIZE aligned +// larger than 0x10000 exceeds size_t, storage_file_* ops fail +#define USB_MSC_BUF_MAX (0x10000UL - SCSI_BLOCK_SIZE) + +static usbd_respond usb_ep_config(usbd_device* dev, uint8_t cfg); +static usbd_respond usb_control(usbd_device* dev, usbd_ctlreq* req, usbd_rqc_callback* callback); + +typedef enum { + EventExit = 1 << 0, + EventReset = 1 << 1, + EventRxTx = 1 << 2, + + EventAll = EventExit | EventReset | EventRxTx, +} MassStorageEvent; + +typedef struct { + uint32_t sig; + uint32_t tag; + uint32_t len; + uint8_t flags; + uint8_t lun; + uint8_t cmd_len; + uint8_t cmd[16]; +} __attribute__((packed)) CBW; + +typedef struct { + uint32_t sig; + uint32_t tag; + uint32_t residue; + uint8_t status; +} __attribute__((packed)) CSW; + +struct MassStorageUsb { + FuriHalUsbInterface usb; + FuriHalUsbInterface* usb_prev; + + FuriThread* thread; + usbd_device* dev; + SCSIDeviceFunc fn; +}; + +static int32_t mass_thread_worker(void* context) { + MassStorageUsb* mass = context; + usbd_device* dev = mass->dev; + SCSISession scsi = { + .fn = mass->fn, + }; + CBW cbw = {0}; + CSW csw = {0}; + uint8_t* buf = NULL; + uint32_t buf_len = 0, buf_cap = 0, buf_sent = 0; + enum { + StateReadCBW, + StateReadData, + StateWriteData, + StateBuildCSW, + StateWriteCSW, + } state = StateReadCBW; + while(true) { + uint32_t flags = furi_thread_flags_wait(EventAll, FuriFlagWaitAny, FuriWaitForever); + if(flags & EventExit) { + FURI_LOG_D(TAG, "exit"); + break; + } + if(flags & EventReset) { + FURI_LOG_D(TAG, "reset"); + scsi.sk = 0; + scsi.asc = 0; + memset(&cbw, 0, sizeof(cbw)); + memset(&csw, 0, sizeof(csw)); + if(buf) { + free(buf); + buf = NULL; + } + buf_len = buf_cap = buf_sent = 0; + state = StateReadCBW; + } + if(flags & EventRxTx) do { + switch(state) { + case StateReadCBW: { + FURI_LOG_T(TAG, "StateReadCBW"); + int32_t len = usbd_ep_read(dev, USB_MSC_RX_EP, &cbw, sizeof(cbw)); + if(len <= 0) { + FURI_LOG_T(TAG, "cbw not ready"); + break; + } + if(len != sizeof(cbw) || cbw.sig != CBW_SIG) { + FURI_LOG_W(TAG, "bad cbw sig=%08lx", cbw.sig); + usbd_ep_stall(dev, USB_MSC_TX_EP); + usbd_ep_stall(dev, USB_MSC_RX_EP); + continue; + } + if(!scsi_cmd_start(&scsi, cbw.cmd, cbw.cmd_len)) { + FURI_LOG_W(TAG, "bad cmd"); + usbd_ep_stall(dev, USB_MSC_RX_EP); + csw.sig = CSW_SIG; + csw.tag = cbw.tag; + csw.status = CSW_STATUS_NOK; + state = StateWriteCSW; + continue; + } + if(cbw.flags & CBW_FLAGS_DEVICE_TO_HOST) { + buf_len = 0; + buf_sent = 0; + state = StateWriteData; + } else { + buf_len = 0; + state = StateReadData; + } + continue; + }; break; + case StateReadData: { + FURI_LOG_T(TAG, "StateReadData %lu/%lu", buf_len, cbw.len); + if(!cbw.len) { + state = StateBuildCSW; + continue; + } + uint32_t buf_clamp = MIN(cbw.len, USB_MSC_BUF_MAX); + if(buf_clamp > buf_cap) { + FURI_LOG_T(TAG, "growing buf %lu -> %lu", buf_cap, buf_clamp); + if(buf) { + free(buf); + } + buf_cap = buf_clamp; + buf = malloc(buf_cap); + } + if(buf_len < buf_clamp) { + int32_t len = + usbd_ep_read(dev, USB_MSC_RX_EP, buf + buf_len, buf_clamp - buf_len); + if(len < 0) { + FURI_LOG_T(TAG, "rx not ready %ld", len); + break; + } + FURI_LOG_T(TAG, "clamp %lu len %ld", buf_clamp, len); + buf_len += len; + } + if(buf_len == buf_clamp) { + if(!scsi_cmd_rx_data(&scsi, buf, buf_len)) { + FURI_LOG_W(TAG, "short rx"); + usbd_ep_stall(dev, USB_MSC_RX_EP); + csw.sig = CSW_SIG; + csw.tag = cbw.tag; + csw.status = CSW_STATUS_NOK; + csw.residue = cbw.len; + state = StateWriteCSW; + continue; + } + cbw.len -= buf_len; + buf_len = 0; + } + continue; + }; break; + case StateWriteData: { + FURI_LOG_T(TAG, "StateWriteData %lu", cbw.len); + if(!cbw.len) { + state = StateBuildCSW; + continue; + } + uint32_t buf_clamp = MIN(cbw.len, USB_MSC_BUF_MAX); + if(buf_clamp > buf_cap) { + FURI_LOG_T(TAG, "growing buf %lu -> %lu", buf_cap, buf_clamp); + if(buf) { + free(buf); + } + buf_cap = buf_clamp; + buf = malloc(buf_cap); + } + if(!buf_len && !scsi_cmd_tx_data(&scsi, buf, &buf_len, buf_clamp)) { + FURI_LOG_W(TAG, "short tx"); + // usbd_ep_stall(dev, USB_MSC_TX_EP); + state = StateBuildCSW; + continue; + } + int32_t len = usbd_ep_write( + dev, + USB_MSC_TX_EP, + buf + buf_sent, + MIN(USB_MSC_TX_EP_SIZE, buf_len - buf_sent)); + if(len < 0) { + FURI_LOG_T(TAG, "tx not ready %ld", len); + break; + } + buf_sent += len; + if(buf_sent == buf_len) { + cbw.len -= buf_len; + buf_len = 0; + buf_sent = 0; + } + continue; + }; break; + case StateBuildCSW: { + FURI_LOG_T(TAG, "StateBuildCSW"); + csw.sig = CSW_SIG; + csw.tag = cbw.tag; + if(scsi_cmd_end(&scsi)) { + csw.status = CSW_STATUS_OK; + } else { + csw.status = CSW_STATUS_NOK; + } + csw.residue = cbw.len; + state = StateWriteCSW; + continue; + }; break; + case StateWriteCSW: { + FURI_LOG_T(TAG, "StateWriteCSW"); + if(csw.status) { + FURI_LOG_W( + TAG, + "csw sig=%08lx tag=%08lx residue=%08lx status=%02x", + csw.sig, + csw.tag, + csw.residue, + csw.status); + } + int32_t len = usbd_ep_write(dev, USB_MSC_TX_EP, &csw, sizeof(csw)); + if(len < 0) { + FURI_LOG_T(TAG, "csw not ready"); + break; + } + if(len != sizeof(csw)) { + FURI_LOG_W(TAG, "bad csw write %ld", len); + usbd_ep_stall(dev, USB_MSC_TX_EP); + break; + } + memset(&cbw, 0, sizeof(cbw)); + memset(&csw, 0, sizeof(csw)); + state = StateReadCBW; + continue; + }; break; + } + break; + } while(true); + } + if(buf) { + free(buf); + } + return 0; +} + +// needed in usb_deinit, usb_suspend, usb_rxtx_ep_callback, usb_control, +// where if_ctx isn't passed +static MassStorageUsb* mass_cur = NULL; + +static void usb_init(usbd_device* dev, FuriHalUsbInterface* intf, void* ctx) { + UNUSED(intf); + MassStorageUsb* mass = ctx; + mass_cur = mass; + mass->dev = dev; + + usbd_reg_config(dev, usb_ep_config); + usbd_reg_control(dev, usb_control); + usbd_connect(dev, true); + + mass->thread = furi_thread_alloc(); + furi_thread_set_name(mass->thread, "MassStorageUsb"); + furi_thread_set_stack_size(mass->thread, 1024); + furi_thread_set_context(mass->thread, ctx); + furi_thread_set_callback(mass->thread, mass_thread_worker); + furi_thread_start(mass->thread); +} + +static void usb_deinit(usbd_device* dev) { + usbd_reg_config(dev, NULL); + usbd_reg_control(dev, NULL); + + MassStorageUsb* mass = mass_cur; + if(!mass || mass->dev != dev) { + FURI_LOG_E(TAG, "deinit mass_cur leak"); + return; + } + mass_cur = NULL; + + furi_assert(mass->thread); + furi_thread_flags_set(furi_thread_get_id(mass->thread), EventExit); + furi_thread_join(mass->thread); + furi_thread_free(mass->thread); + mass->thread = NULL; + + free(mass->usb.str_prod_descr); + mass->usb.str_prod_descr = NULL; + free(mass->usb.str_serial_descr); + mass->usb.str_serial_descr = NULL; + free(mass); +} + +static void usb_wakeup(usbd_device* dev) { + UNUSED(dev); +} + +static void usb_suspend(usbd_device* dev) { + MassStorageUsb* mass = mass_cur; + if(!mass || mass->dev != dev) return; + furi_thread_flags_set(furi_thread_get_id(mass->thread), EventReset); +} + +static void usb_rxtx_ep_callback(usbd_device* dev, uint8_t event, uint8_t ep) { + UNUSED(ep); + UNUSED(event); + MassStorageUsb* mass = mass_cur; + if(!mass || mass->dev != dev) return; + furi_thread_flags_set(furi_thread_get_id(mass->thread), EventRxTx); +} + +static usbd_respond usb_ep_config(usbd_device* dev, uint8_t cfg) { + switch(cfg) { + case 0: // deconfig + usbd_ep_deconfig(dev, USB_MSC_RX_EP); + usbd_ep_deconfig(dev, USB_MSC_TX_EP); + usbd_reg_endpoint(dev, USB_MSC_RX_EP, NULL); + usbd_reg_endpoint(dev, USB_MSC_TX_EP, NULL); + return usbd_ack; + case 1: // config + usbd_ep_config( + dev, USB_MSC_RX_EP, USB_EPTYPE_BULK /* | USB_EPTYPE_DBLBUF*/, USB_MSC_RX_EP_SIZE); + usbd_ep_config( + dev, USB_MSC_TX_EP, USB_EPTYPE_BULK /* | USB_EPTYPE_DBLBUF*/, USB_MSC_TX_EP_SIZE); + usbd_reg_endpoint(dev, USB_MSC_RX_EP, usb_rxtx_ep_callback); + usbd_reg_endpoint(dev, USB_MSC_TX_EP, usb_rxtx_ep_callback); + return usbd_ack; + } + return usbd_fail; +} + +static usbd_respond usb_control(usbd_device* dev, usbd_ctlreq* req, usbd_rqc_callback* callback) { + UNUSED(callback); + if(((USB_REQ_RECIPIENT | USB_REQ_TYPE) & req->bmRequestType) != + (USB_REQ_INTERFACE | USB_REQ_CLASS)) { + return usbd_fail; + } + switch(req->bRequest) { + case USB_MSC_BOT_GET_MAX_LUN: { + static uint8_t max_lun = 0; + dev->status.data_ptr = &max_lun; + dev->status.data_count = 1; + return usbd_ack; + }; break; + case USB_MSC_BOT_RESET: { + MassStorageUsb* mass = mass_cur; + if(!mass || mass->dev != dev) return usbd_fail; + furi_thread_flags_set(furi_thread_get_id(mass->thread), EventReset); + return usbd_ack; + }; break; + } + return usbd_fail; +} + +static const struct usb_string_descriptor dev_manuf_desc = USB_STRING_DESC("Flipper Devices Inc."); + +struct MassStorageDescriptor { + struct usb_config_descriptor config; + struct usb_interface_descriptor intf; + struct usb_endpoint_descriptor ep_rx; + struct usb_endpoint_descriptor ep_tx; +} __attribute__((packed)); + +static const struct usb_device_descriptor usb_mass_dev_descr = { + .bLength = sizeof(struct usb_device_descriptor), + .bDescriptorType = USB_DTYPE_DEVICE, + .bcdUSB = VERSION_BCD(2, 0, 0), + .bDeviceClass = USB_CLASS_PER_INTERFACE, + .bDeviceSubClass = USB_SUBCLASS_NONE, + .bDeviceProtocol = USB_PROTO_NONE, + .bMaxPacketSize0 = 8, // USB_EP0_SIZE + .idVendor = 0x0483, + .idProduct = 0x5720, + .bcdDevice = VERSION_BCD(1, 0, 0), + .iManufacturer = 1, // UsbDevManuf + .iProduct = 2, // UsbDevProduct + .iSerialNumber = 3, // UsbDevSerial + .bNumConfigurations = 1, +}; + +static const struct MassStorageDescriptor usb_mass_cfg_descr = { + .config = + { + .bLength = sizeof(struct usb_config_descriptor), + .bDescriptorType = USB_DTYPE_CONFIGURATION, + .wTotalLength = sizeof(struct MassStorageDescriptor), + .bNumInterfaces = 1, + .bConfigurationValue = 1, + .iConfiguration = NO_DESCRIPTOR, + .bmAttributes = USB_CFG_ATTR_RESERVED | USB_CFG_ATTR_SELFPOWERED, + .bMaxPower = USB_CFG_POWER_MA(100), + }, + .intf = + { + .bLength = sizeof(struct usb_interface_descriptor), + .bDescriptorType = USB_DTYPE_INTERFACE, + .bInterfaceNumber = 0, + .bAlternateSetting = 0, + .bNumEndpoints = 2, + .bInterfaceClass = USB_CLASS_MASS_STORAGE, + .bInterfaceSubClass = 0x06, // scsi transparent + .bInterfaceProtocol = 0x50, // bulk only + .iInterface = NO_DESCRIPTOR, + }, + .ep_rx = + { + .bLength = sizeof(struct usb_endpoint_descriptor), + .bDescriptorType = USB_DTYPE_ENDPOINT, + .bEndpointAddress = USB_MSC_RX_EP, + .bmAttributes = USB_EPTYPE_BULK, + .wMaxPacketSize = USB_MSC_RX_EP_SIZE, + .bInterval = 0, + }, + .ep_tx = + { + .bLength = sizeof(struct usb_endpoint_descriptor), + .bDescriptorType = USB_DTYPE_ENDPOINT, + .bEndpointAddress = USB_MSC_TX_EP, + .bmAttributes = USB_EPTYPE_BULK, + .wMaxPacketSize = USB_MSC_TX_EP_SIZE, + .bInterval = 0, + }, +}; + +MassStorageUsb* mass_storage_usb_start(const char* filename, SCSIDeviceFunc fn) { + MassStorageUsb* mass = malloc(sizeof(MassStorageUsb)); + mass->usb_prev = furi_hal_usb_get_config(); + mass->usb.init = usb_init; + mass->usb.deinit = usb_deinit; + mass->usb.wakeup = usb_wakeup; + mass->usb.suspend = usb_suspend; + mass->usb.dev_descr = (struct usb_device_descriptor*)&usb_mass_dev_descr; + mass->usb.str_manuf_descr = (void*)&dev_manuf_desc; + mass->usb.str_prod_descr = NULL; + mass->usb.str_serial_descr = NULL; + mass->usb.cfg_descr = (void*)&usb_mass_cfg_descr; + + const char* name = furi_hal_version_get_device_name_ptr(); + if(!name) name = "Flipper Zero"; + size_t len = strlen(name); + struct usb_string_descriptor* str_prod_descr = malloc(len * 2 + 2); + str_prod_descr->bLength = len * 2 + 2; + str_prod_descr->bDescriptorType = USB_DTYPE_STRING; + for(uint8_t i = 0; i < len; i++) str_prod_descr->wString[i] = name[i]; + mass->usb.str_prod_descr = str_prod_descr; + + len = strlen(filename); + struct usb_string_descriptor* str_serial_descr = malloc(len * 2 + 2); + str_serial_descr->bLength = len * 2 + 2; + str_serial_descr->bDescriptorType = USB_DTYPE_STRING; + for(uint8_t i = 0; i < len; i++) str_serial_descr->wString[i] = filename[i]; + mass->usb.str_serial_descr = str_serial_descr; + + mass->fn = fn; + if(!furi_hal_usb_set_config(&mass->usb, mass)) { + FURI_LOG_E(TAG, "USB locked, cannot start Mass Storage"); + free(mass->usb.str_prod_descr); + free(mass->usb.str_serial_descr); + free(mass); + return NULL; + } + return mass; +} + +void mass_storage_usb_stop(MassStorageUsb* mass) { + furi_hal_usb_set_config(mass->usb_prev, NULL); +} diff --git a/mass_storage/helpers/mass_storage_usb.h b/mass_storage/helpers/mass_storage_usb.h new file mode 100644 index 00000000000..0f370f98ed6 --- /dev/null +++ b/mass_storage/helpers/mass_storage_usb.h @@ -0,0 +1,9 @@ +#pragma once + +#include +#include "mass_storage_scsi.h" + +typedef struct MassStorageUsb MassStorageUsb; + +MassStorageUsb* mass_storage_usb_start(const char* filename, SCSIDeviceFunc fn); +void mass_storage_usb_stop(MassStorageUsb* mass); diff --git a/mass_storage/mass_storage_app.c b/mass_storage/mass_storage_app.c new file mode 100644 index 00000000000..e84319fa8a9 --- /dev/null +++ b/mass_storage/mass_storage_app.c @@ -0,0 +1,157 @@ +#include "mass_storage_app_i.h" +#include +#include +#include + +static bool mass_storage_app_custom_event_callback(void* context, uint32_t event) { + furi_assert(context); + MassStorageApp* app = context; + return scene_manager_handle_custom_event(app->scene_manager, event); +} + +static bool mass_storage_app_back_event_callback(void* context) { + furi_assert(context); + MassStorageApp* app = context; + return scene_manager_handle_back_event(app->scene_manager); +} + +static void mass_storage_app_tick_event_callback(void* context) { + furi_assert(context); + MassStorageApp* app = context; + scene_manager_handle_tick_event(app->scene_manager); +} + +void mass_storage_app_show_loading_popup(MassStorageApp* app, bool show) { + if(show) { + // Raise timer priority so that animations can play + furi_timer_set_thread_priority(FuriTimerThreadPriorityElevated); + view_dispatcher_switch_to_view(app->view_dispatcher, MassStorageAppViewLoading); + } else { + // Restore default timer priority + furi_timer_set_thread_priority(FuriTimerThreadPriorityNormal); + } +} + +MassStorageApp* mass_storage_app_alloc(char* arg) { + MassStorageApp* app = malloc(sizeof(MassStorageApp)); + app->file_path = furi_string_alloc(); + + if(arg != NULL) { + furi_string_set_str(app->file_path, arg); + } else { + furi_string_set_str(app->file_path, MASS_STORAGE_APP_PATH_FOLDER); + } + + app->gui = furi_record_open(RECORD_GUI); + app->fs_api = furi_record_open(RECORD_STORAGE); + app->dialogs = furi_record_open(RECORD_DIALOGS); + + app->create_image_size = (uint8_t)-1; + SDInfo sd_info; + if(storage_sd_info(app->fs_api, &sd_info) == FSE_OK) { + switch(sd_info.fs_type) { + case FST_FAT12: + app->create_image_max = 16LL * 1024 * 1024; + break; + case FST_FAT16: + app->create_image_max = 2LL * 1024 * 1024 * 1024; + break; + case FST_FAT32: + app->create_image_max = 4LL * 1024 * 1024 * 1024; + break; + default: + app->create_image_max = 0; + break; + } + } + + app->view_dispatcher = view_dispatcher_alloc(); + view_dispatcher_enable_queue(app->view_dispatcher); + + app->scene_manager = scene_manager_alloc(&mass_storage_scene_handlers, app); + + view_dispatcher_set_event_callback_context(app->view_dispatcher, app); + view_dispatcher_set_tick_event_callback( + app->view_dispatcher, mass_storage_app_tick_event_callback, 500); + view_dispatcher_set_custom_event_callback( + app->view_dispatcher, mass_storage_app_custom_event_callback); + view_dispatcher_set_navigation_event_callback( + app->view_dispatcher, mass_storage_app_back_event_callback); + + app->mass_storage_view = mass_storage_alloc(); + view_dispatcher_add_view( + app->view_dispatcher, + MassStorageAppViewWork, + mass_storage_get_view(app->mass_storage_view)); + + app->text_input = text_input_alloc(); + view_dispatcher_add_view( + app->view_dispatcher, MassStorageAppViewTextInput, text_input_get_view(app->text_input)); + + app->loading = loading_alloc(); + view_dispatcher_add_view( + app->view_dispatcher, MassStorageAppViewLoading, loading_get_view(app->loading)); + + app->variable_item_list = variable_item_list_alloc(); + view_dispatcher_add_view( + app->view_dispatcher, + MassStorageAppViewStart, + variable_item_list_get_view(app->variable_item_list)); + + app->widget = widget_alloc(); + view_dispatcher_add_view( + app->view_dispatcher, MassStorageAppViewWidget, widget_get_view(app->widget)); + + app->popup = popup_alloc(); + view_dispatcher_add_view( + app->view_dispatcher, MassStorageAppViewPopup, popup_get_view(app->popup)); + + view_dispatcher_attach_to_gui(app->view_dispatcher, app->gui, ViewDispatcherTypeFullscreen); + + if(storage_file_exists(app->fs_api, furi_string_get_cstr(app->file_path))) { + scene_manager_next_scene(app->scene_manager, MassStorageSceneWork); + } else { + scene_manager_next_scene(app->scene_manager, MassStorageSceneStart); + } + + return app; +} + +void mass_storage_app_free(MassStorageApp* app) { + furi_assert(app); + + // Views + view_dispatcher_remove_view(app->view_dispatcher, MassStorageAppViewWork); + view_dispatcher_remove_view(app->view_dispatcher, MassStorageAppViewTextInput); + view_dispatcher_remove_view(app->view_dispatcher, MassStorageAppViewStart); + view_dispatcher_remove_view(app->view_dispatcher, MassStorageAppViewLoading); + view_dispatcher_remove_view(app->view_dispatcher, MassStorageAppViewWidget); + view_dispatcher_remove_view(app->view_dispatcher, MassStorageAppViewPopup); + + mass_storage_free(app->mass_storage_view); + text_input_free(app->text_input); + variable_item_list_free(app->variable_item_list); + loading_free(app->loading); + widget_free(app->widget); + popup_free(app->popup); + + // View dispatcher + view_dispatcher_free(app->view_dispatcher); + scene_manager_free(app->scene_manager); + + furi_string_free(app->file_path); + + // Close records + furi_record_close(RECORD_GUI); + furi_record_close(RECORD_STORAGE); + furi_record_close(RECORD_DIALOGS); + + free(app); +} + +int32_t mass_storage_app(void* p) { + MassStorageApp* mass_storage_app = mass_storage_app_alloc((char*)p); + view_dispatcher_run(mass_storage_app->view_dispatcher); + mass_storage_app_free(mass_storage_app); + return 0; +} diff --git a/mass_storage/mass_storage_app.h b/mass_storage/mass_storage_app.h new file mode 100644 index 00000000000..820ad2b6c77 --- /dev/null +++ b/mass_storage/mass_storage_app.h @@ -0,0 +1,11 @@ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct MassStorageApp MassStorageApp; + +#ifdef __cplusplus +} +#endif diff --git a/mass_storage/mass_storage_app_i.h b/mass_storage/mass_storage_app_i.h new file mode 100644 index 00000000000..9fa6945adad --- /dev/null +++ b/mass_storage/mass_storage_app_i.h @@ -0,0 +1,68 @@ +#pragma once + +#include "mass_storage_app.h" +#include "scenes/mass_storage_scene.h" +#include "helpers/mass_storage_usb.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "views/mass_storage_view.h" +#include + +#define MASS_STORAGE_APP_PATH_FOLDER STORAGE_APP_DATA_PATH_PREFIX +#define MASS_STORAGE_APP_EXTENSION ".img" +#define MASS_STORAGE_FILE_NAME_LEN 40 + +struct MassStorageApp { + Gui* gui; + Storage* fs_api; + ViewDispatcher* view_dispatcher; + SceneManager* scene_manager; + Widget* widget; + Popup* popup; + DialogsApp* dialogs; + TextInput* text_input; + VariableItemList* variable_item_list; + Loading* loading; + + FuriString* file_path; + File* file; + MassStorage* mass_storage_view; + + FuriMutex* usb_mutex; + MassStorageUsb* usb; + + uint64_t create_image_max; + uint8_t create_image_size; + char create_image_name[MASS_STORAGE_FILE_NAME_LEN]; + + uint32_t bytes_read, bytes_written; +}; + +typedef enum { + MassStorageAppViewStart, + MassStorageAppViewTextInput, + MassStorageAppViewWork, + MassStorageAppViewLoading, + MassStorageAppViewWidget, + MassStorageAppViewPopup, +} MassStorageAppView; + +enum MassStorageCustomEvent { + // Reserve first 100 events for button types and indexes, starting from 0 + MassStorageCustomEventReserved = 100, + + MassStorageCustomEventEject, +}; + +void mass_storage_app_show_loading_popup(MassStorageApp* app, bool show); diff --git a/mass_storage/scenes/mass_storage_scene.c b/mass_storage/scenes/mass_storage_scene.c new file mode 100644 index 00000000000..bab24ca4004 --- /dev/null +++ b/mass_storage/scenes/mass_storage_scene.c @@ -0,0 +1,30 @@ +#include "mass_storage_scene.h" + +// Generate scene on_enter handlers array +#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_enter, +void (*const mass_storage_scene_on_enter_handlers[])(void*) = { +#include "mass_storage_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 mass_storage_scene_on_event_handlers[])(void* context, SceneManagerEvent event) = { +#include "mass_storage_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 mass_storage_scene_on_exit_handlers[])(void* context) = { +#include "mass_storage_scene_config.h" +}; +#undef ADD_SCENE + +// Initialize scene handlers configuration structure +const SceneManagerHandlers mass_storage_scene_handlers = { + .on_enter_handlers = mass_storage_scene_on_enter_handlers, + .on_event_handlers = mass_storage_scene_on_event_handlers, + .on_exit_handlers = mass_storage_scene_on_exit_handlers, + .scene_num = MassStorageSceneNum, +}; diff --git a/mass_storage/scenes/mass_storage_scene.h b/mass_storage/scenes/mass_storage_scene.h new file mode 100644 index 00000000000..d43bb4c978e --- /dev/null +++ b/mass_storage/scenes/mass_storage_scene.h @@ -0,0 +1,29 @@ +#pragma once + +#include + +// Generate scene id and total number +#define ADD_SCENE(prefix, name, id) MassStorageScene##id, +typedef enum { +#include "mass_storage_scene_config.h" + MassStorageSceneNum, +} MassStorageScene; +#undef ADD_SCENE + +extern const SceneManagerHandlers mass_storage_scene_handlers; + +// Generate scene on_enter handlers declaration +#define ADD_SCENE(prefix, name, id) void prefix##_scene_##name##_on_enter(void*); +#include "mass_storage_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 "mass_storage_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 "mass_storage_scene_config.h" +#undef ADD_SCENE diff --git a/mass_storage/scenes/mass_storage_scene_config.h b/mass_storage/scenes/mass_storage_scene_config.h new file mode 100644 index 00000000000..5017a4e07bc --- /dev/null +++ b/mass_storage/scenes/mass_storage_scene_config.h @@ -0,0 +1,6 @@ +ADD_SCENE(mass_storage, start, Start) +ADD_SCENE(mass_storage, file_select, FileSelect) +ADD_SCENE(mass_storage, work, Work) +ADD_SCENE(mass_storage, create_image, CreateImage) +ADD_SCENE(mass_storage, create_image_name, CreateImageName) +ADD_SCENE(mass_storage, usb_locked, UsbLocked) diff --git a/mass_storage/scenes/mass_storage_scene_create_image.c b/mass_storage/scenes/mass_storage_scene_create_image.c new file mode 100644 index 00000000000..c1b6ca46afa --- /dev/null +++ b/mass_storage/scenes/mass_storage_scene_create_image.c @@ -0,0 +1,193 @@ +#include "../mass_storage_app_i.h" +#include + +enum VarItemListIndex { + VarItemListIndexImageSize, + VarItemListIndexImageName, + VarItemListIndexCreateImage, +}; + +void mass_storage_scene_create_image_variable_item_list_callback(void* context, uint32_t index) { + MassStorageApp* app = context; + view_dispatcher_send_custom_event(app->view_dispatcher, index); +} + +static const struct { + char* name; + uint64_t value; +} image_sizes[] = { + {"1MB", 1LL * 1024 * 1024}, + {"2MB", 2LL * 1024 * 1024}, + {"4MB", 4LL * 1024 * 1024}, + {"8MB", 8LL * 1024 * 1024}, + {"16MB", 16LL * 1024 * 1024}, + {"32MB", 32LL * 1024 * 1024}, + {"64MB", 64LL * 1024 * 1024}, + {"128MB", 128LL * 1024 * 1024}, + {"256MB", 256LL * 1024 * 1024}, + {"512MB", 512LL * 1024 * 1024}, + {"1GB", 1LL * 1024 * 1024 * 1024}, + {"2GB", 2LL * 1024 * 1024 * 1024}, + {"4GB", 4LL * 1024 * 1024 * 1024}, + {"8GB", 8LL * 1024 * 1024 * 1024}, + {"16GB", 16LL * 1024 * 1024 * 1024}, + {"32GB", 32LL * 1024 * 1024 * 1024}, + {"64GB", 64LL * 1024 * 1024 * 1024}, + {"128GB", 128LL * 1024 * 1024 * 1024}, + {"256GB", 256LL * 1024 * 1024 * 1024}, + {"512GB", 512LL * 1024 * 1024 * 1024}, +}; +static void mass_storage_scene_create_image_image_size_changed(VariableItem* item) { + MassStorageApp* app = variable_item_get_context(item); + app->create_image_size = variable_item_get_current_value_index(item); + variable_item_set_current_value_text(item, image_sizes[app->create_image_size].name); +} + +void mass_storage_scene_create_image_on_enter(void* context) { + MassStorageApp* app = context; + VariableItemList* variable_item_list = app->variable_item_list; + VariableItem* item; + + uint8_t size_count = COUNT_OF(image_sizes); + if(app->create_image_max) { + for(size_t i = 1; i < size_count; i++) { + if(image_sizes[i].value > app->create_image_max) { + size_count = i; + break; + } + } + } + if(app->create_image_size == (uint8_t)-1) { + app->create_image_size = CLAMP(7, size_count - 2, 0); // 7 = 128MB + } + item = variable_item_list_add( + variable_item_list, + "Image Size", + size_count, + mass_storage_scene_create_image_image_size_changed, + app); + variable_item_set_current_value_index(item, app->create_image_size); + variable_item_set_current_value_text(item, image_sizes[app->create_image_size].name); + + item = variable_item_list_add(variable_item_list, "Image Name", 0, NULL, app); + variable_item_set_current_value_text(item, app->create_image_name); + + variable_item_list_add(variable_item_list, "Create Image", 0, NULL, app); + + variable_item_list_set_enter_callback( + variable_item_list, mass_storage_scene_create_image_variable_item_list_callback, app); + + variable_item_list_set_header(variable_item_list, "Create Disk Image"); + + variable_item_list_set_selected_item( + variable_item_list, + scene_manager_get_scene_state(app->scene_manager, MassStorageSceneCreateImage)); + + view_dispatcher_switch_to_view(app->view_dispatcher, MassStorageAppViewStart); +} + +static void popup_callback_ok(void* context) { + MassStorageApp* app = context; + scene_manager_set_scene_state( + app->scene_manager, MassStorageSceneStart, MassStorageSceneFileSelect); + scene_manager_previous_scene(app->scene_manager); + scene_manager_next_scene(app->scene_manager, MassStorageSceneFileSelect); +} + +static void popup_callback_error(void* context) { + MassStorageApp* app = context; + view_dispatcher_switch_to_view(app->view_dispatcher, MassStorageAppViewStart); +} + +bool mass_storage_scene_create_image_on_event(void* context, SceneManagerEvent event) { + MassStorageApp* app = context; + bool consumed = false; + + if(event.type == SceneManagerEventTypeCustom) { + scene_manager_set_scene_state( + app->scene_manager, MassStorageSceneCreateImage, event.event); + consumed = true; + switch(event.event) { + case VarItemListIndexImageName: + scene_manager_next_scene(app->scene_manager, MassStorageSceneCreateImageName); + break; + case VarItemListIndexCreateImage: { + mass_storage_app_show_loading_popup(app, true); + const char* name = strnlen(app->create_image_name, sizeof(app->create_image_name)) ? + app->create_image_name : + image_sizes[app->create_image_size].name; + furi_string_printf( + app->file_path, + "%s/%s%s", + MASS_STORAGE_APP_PATH_FOLDER, + name, + MASS_STORAGE_APP_EXTENSION); + + app->file = storage_file_alloc(app->fs_api); + const char* error = NULL; + bool success = false; + + do { + if(!storage_file_open( + app->file, + furi_string_get_cstr(app->file_path), + FSAM_READ | FSAM_WRITE, + FSOM_CREATE_NEW)) + break; + + uint64_t size = image_sizes[app->create_image_size].value; + if(size == app->create_image_max) size--; + if(!storage_file_expand(app->file, size)) break; + + // Format as exFAT + error = "Image formatting failed"; + if(storage_virtual_init(app->fs_api, app->file) != FSE_OK) { + if(storage_virtual_quit(app->fs_api) != FSE_OK) break; + if(storage_virtual_init(app->fs_api, app->file) != FSE_OK) break; + } + if(storage_virtual_format(app->fs_api) == FSE_OK) { + success = true; + error = NULL; + } + storage_virtual_quit(app->fs_api); + } while(false); + + if(!success) { + error = storage_file_get_error_desc(app->file); + FS_Error error = storage_file_get_error(app->file); + storage_file_close(app->file); + if(error != FSE_EXIST) { + storage_common_remove(app->fs_api, furi_string_get_cstr(app->file_path)); + } + } + storage_file_free(app->file); + mass_storage_app_show_loading_popup(app, false); + + if(error) { + popup_set_header( + app->popup, "Error Creating Image!", 64, 26, AlignCenter, AlignCenter); + popup_set_text(app->popup, error, 64, 40, AlignCenter, AlignCenter); + popup_set_callback(app->popup, popup_callback_error); + } else { + popup_set_header(app->popup, "Image Created!", 64, 32, AlignCenter, AlignCenter); + popup_set_text(app->popup, "", 0, 0, AlignLeft, AlignBottom); + popup_set_callback(app->popup, popup_callback_ok); + } + popup_set_context(app->popup, app); + popup_set_timeout(app->popup, 0); + popup_disable_timeout(app->popup); + view_dispatcher_switch_to_view(app->view_dispatcher, MassStorageAppViewPopup); + break; + } + default: + break; + } + } + + return consumed; +} + +void mass_storage_scene_create_image_on_exit(void* context) { + MassStorageApp* app = context; + variable_item_list_reset(app->variable_item_list); +} diff --git a/mass_storage/scenes/mass_storage_scene_create_image_name.c b/mass_storage/scenes/mass_storage_scene_create_image_name.c new file mode 100644 index 00000000000..38efa7cb68f --- /dev/null +++ b/mass_storage/scenes/mass_storage_scene_create_image_name.c @@ -0,0 +1,52 @@ +#include "../mass_storage_app_i.h" + +enum TextInputIndex { + TextInputResultOk, +}; + +static void mass_storage_scene_create_image_name_text_input_callback(void* context) { + MassStorageApp* app = context; + view_dispatcher_send_custom_event(app->view_dispatcher, TextInputResultOk); +} + +void mass_storage_scene_create_image_name_on_enter(void* context) { + MassStorageApp* app = context; + TextInput* text_input = app->text_input; + + text_input_set_header_text(text_input, "Image name, empty = default"); + + text_input_set_minimum_length(text_input, 0); + + text_input_set_result_callback( + text_input, + mass_storage_scene_create_image_name_text_input_callback, + app, + app->create_image_name, + sizeof(app->create_image_name), + false); + + view_dispatcher_switch_to_view(app->view_dispatcher, MassStorageAppViewTextInput); +} + +bool mass_storage_scene_create_image_name_on_event(void* context, SceneManagerEvent event) { + MassStorageApp* app = context; + bool consumed = false; + + if(event.type == SceneManagerEventTypeCustom) { + consumed = true; + switch(event.event) { + case TextInputResultOk: + scene_manager_previous_scene(app->scene_manager); + break; + default: + break; + } + } + + return consumed; +} + +void mass_storage_scene_create_image_name_on_exit(void* context) { + MassStorageApp* app = context; + text_input_reset(app->text_input); +} diff --git a/mass_storage/scenes/mass_storage_scene_file_select.c b/mass_storage/scenes/mass_storage_scene_file_select.c new file mode 100644 index 00000000000..58bc01604d1 --- /dev/null +++ b/mass_storage/scenes/mass_storage_scene_file_select.c @@ -0,0 +1,38 @@ +#include "../mass_storage_app_i.h" +#include "furi_hal_power.h" + +static bool mass_storage_file_select(MassStorageApp* mass_storage) { + furi_assert(mass_storage); + + DialogsFileBrowserOptions browser_options; + dialog_file_browser_set_basic_options( + &browser_options, MASS_STORAGE_APP_EXTENSION "|.iso", &I_floppydisk_10px); + browser_options.base_path = MASS_STORAGE_APP_PATH_FOLDER; + browser_options.hide_ext = false; + + // Input events and views are managed by file_select + bool res = dialog_file_browser_show( + mass_storage->dialogs, mass_storage->file_path, mass_storage->file_path, &browser_options); + return res; +} + +void mass_storage_scene_file_select_on_enter(void* context) { + MassStorageApp* mass_storage = context; + + if(mass_storage_file_select(mass_storage)) { + scene_manager_next_scene(mass_storage->scene_manager, MassStorageSceneWork); + } else { + scene_manager_previous_scene(mass_storage->scene_manager); + } +} + +bool mass_storage_scene_file_select_on_event(void* context, SceneManagerEvent event) { + UNUSED(context); + UNUSED(event); + // MassStorageApp* mass_storage = context; + return false; +} + +void mass_storage_scene_file_select_on_exit(void* context) { + UNUSED(context); +} diff --git a/mass_storage/scenes/mass_storage_scene_start.c b/mass_storage/scenes/mass_storage_scene_start.c new file mode 100644 index 00000000000..5ae40d84c08 --- /dev/null +++ b/mass_storage/scenes/mass_storage_scene_start.c @@ -0,0 +1,63 @@ +#include "../mass_storage_app_i.h" + +enum VarItemListIndex { + VarItemListIndexSelectDiskImage, + VarItemListIndexCreateDiskImage, +}; + +static void mass_storage_scene_start_variable_item_list_callback(void* context, uint32_t index) { + MassStorageApp* app = context; + view_dispatcher_send_custom_event(app->view_dispatcher, index); +} + +void mass_storage_scene_start_on_enter(void* context) { + MassStorageApp* app = context; + VariableItemList* variable_item_list = app->variable_item_list; + + variable_item_list_add(variable_item_list, "Select Disk Image", 0, NULL, app); + + variable_item_list_add(variable_item_list, "Create Disk Image", 0, NULL, app); + + variable_item_list_set_enter_callback( + variable_item_list, mass_storage_scene_start_variable_item_list_callback, app); + + variable_item_list_set_header(variable_item_list, "USB Mass Storage"); + + variable_item_list_set_selected_item( + variable_item_list, + scene_manager_get_scene_state(app->scene_manager, MassStorageSceneStart)); + + view_dispatcher_switch_to_view(app->view_dispatcher, MassStorageAppViewStart); +} + +bool mass_storage_scene_start_on_event(void* context, SceneManagerEvent event) { + UNUSED(context); + UNUSED(event); + MassStorageApp* app = context; + + bool consumed = false; + + if(event.type == SceneManagerEventTypeCustom) { + scene_manager_set_scene_state(app->scene_manager, MassStorageSceneStart, event.event); + consumed = true; + switch(event.event) { + case VarItemListIndexSelectDiskImage: + scene_manager_next_scene(app->scene_manager, MassStorageSceneFileSelect); + break; + case VarItemListIndexCreateDiskImage: + scene_manager_set_scene_state(app->scene_manager, MassStorageSceneCreateImage, 0); + scene_manager_next_scene(app->scene_manager, MassStorageSceneCreateImage); + break; + default: + break; + } + } + + return consumed; +} + +void mass_storage_scene_start_on_exit(void* context) { + UNUSED(context); + MassStorageApp* app = context; + variable_item_list_reset(app->variable_item_list); +} diff --git a/mass_storage/scenes/mass_storage_scene_usb_locked.c b/mass_storage/scenes/mass_storage_scene_usb_locked.c new file mode 100644 index 00000000000..ad144ec0a51 --- /dev/null +++ b/mass_storage/scenes/mass_storage_scene_usb_locked.c @@ -0,0 +1,40 @@ +#include "../mass_storage_app_i.h" + +void mass_storage_scene_usb_locked_on_enter(void* context) { + MassStorageApp* app = context; + + widget_add_icon_element(app->widget, 78, 0, &I_ActiveConnection_50x64); + widget_add_string_multiline_element( + app->widget, 3, 2, AlignLeft, AlignTop, FontPrimary, "Connection\nis active!"); + widget_add_string_multiline_element( + app->widget, + 3, + 30, + AlignLeft, + AlignTop, + FontSecondary, + "Disconnect from\nPC or phone to\nuse this function."); + + view_dispatcher_switch_to_view(app->view_dispatcher, MassStorageAppViewWidget); +} + +bool mass_storage_scene_usb_locked_on_event(void* context, SceneManagerEvent event) { + MassStorageApp* app = context; + bool consumed = false; + + if(event.type == SceneManagerEventTypeBack) { + consumed = scene_manager_search_and_switch_to_previous_scene( + app->scene_manager, MassStorageSceneFileSelect); + if(!consumed) { + consumed = scene_manager_search_and_switch_to_previous_scene( + app->scene_manager, MassStorageSceneStart); + } + } + + return consumed; +} + +void mass_storage_scene_usb_locked_on_exit(void* context) { + MassStorageApp* app = context; + widget_reset(app->widget); +} diff --git a/mass_storage/scenes/mass_storage_scene_work.c b/mass_storage/scenes/mass_storage_scene_work.c new file mode 100644 index 00000000000..1ae27852ce2 --- /dev/null +++ b/mass_storage/scenes/mass_storage_scene_work.c @@ -0,0 +1,138 @@ +#include "../mass_storage_app_i.h" +#include "../views/mass_storage_view.h" +#include "../helpers/mass_storage_usb.h" +#include + +#define TAG "MassStorageSceneWork" + +static bool file_read( + void* ctx, + uint32_t lba, + uint16_t count, + uint8_t* out, + uint32_t* out_len, + uint32_t out_cap) { + MassStorageApp* app = ctx; + FURI_LOG_T(TAG, "file_read lba=%08lX count=%04X out_cap=%08lX", lba, count, out_cap); + if(!storage_file_seek(app->file, lba * SCSI_BLOCK_SIZE, true)) { + FURI_LOG_W(TAG, "seek failed"); + return false; + } + uint16_t clamp = MIN(out_cap, count * SCSI_BLOCK_SIZE); + *out_len = storage_file_read(app->file, out, clamp); + FURI_LOG_T(TAG, "%lu/%lu", *out_len, count * SCSI_BLOCK_SIZE); + app->bytes_read += *out_len; + return *out_len == clamp; +} + +static bool file_write(void* ctx, uint32_t lba, uint16_t count, uint8_t* buf, uint32_t len) { + MassStorageApp* app = ctx; + FURI_LOG_T(TAG, "file_write lba=%08lX count=%04X len=%08lX", lba, count, len); + if(len != count * SCSI_BLOCK_SIZE) { + FURI_LOG_W(TAG, "bad write params count=%u len=%lu", count, len); + return false; + } + if(!storage_file_seek(app->file, lba * SCSI_BLOCK_SIZE, true)) { + FURI_LOG_W(TAG, "seek failed"); + return false; + } + app->bytes_written += len; + return storage_file_write(app->file, buf, len) == len; +} + +static uint32_t file_num_blocks(void* ctx) { + MassStorageApp* app = ctx; + return storage_file_size(app->file) / SCSI_BLOCK_SIZE; +} + +static void file_eject(void* ctx) { + MassStorageApp* app = ctx; + FURI_LOG_D(TAG, "EJECT"); + view_dispatcher_send_custom_event(app->view_dispatcher, MassStorageCustomEventEject); +} + +bool mass_storage_scene_work_on_event(void* context, SceneManagerEvent event) { + MassStorageApp* app = context; + bool consumed = false; + if(event.type == SceneManagerEventTypeCustom) { + if(event.event == MassStorageCustomEventEject) { + consumed = scene_manager_search_and_switch_to_previous_scene( + app->scene_manager, MassStorageSceneFileSelect); + if(!consumed) { + consumed = scene_manager_search_and_switch_to_previous_scene( + app->scene_manager, MassStorageSceneStart); + } + } + } else if(event.type == SceneManagerEventTypeTick) { + mass_storage_set_stats(app->mass_storage_view, app->bytes_read, app->bytes_written); + } else if(event.type == SceneManagerEventTypeBack) { + consumed = scene_manager_search_and_switch_to_previous_scene( + app->scene_manager, MassStorageSceneFileSelect); + if(!consumed) { + consumed = scene_manager_search_and_switch_to_previous_scene( + app->scene_manager, MassStorageSceneStart); + } + } + return consumed; +} + +void mass_storage_scene_work_on_enter(void* context) { + MassStorageApp* app = context; + app->bytes_read = app->bytes_written = 0; + + if(!storage_file_exists(app->fs_api, furi_string_get_cstr(app->file_path))) { + scene_manager_search_and_switch_to_previous_scene( + app->scene_manager, MassStorageSceneStart); + return; + } + + mass_storage_app_show_loading_popup(app, true); + + app->usb_mutex = furi_mutex_alloc(FuriMutexTypeNormal); + + FuriString* file_name = furi_string_alloc(); + path_extract_filename(app->file_path, file_name, true); + + mass_storage_set_file_name(app->mass_storage_view, file_name); + app->file = storage_file_alloc(app->fs_api); + furi_assert(storage_file_open( + app->file, + furi_string_get_cstr(app->file_path), + FSAM_READ | FSAM_WRITE, + FSOM_OPEN_EXISTING)); + + SCSIDeviceFunc fn = { + .ctx = app, + .read = file_read, + .write = file_write, + .num_blocks = file_num_blocks, + .eject = file_eject, + }; + + furi_hal_usb_unlock(); + app->usb = mass_storage_usb_start(furi_string_get_cstr(file_name), fn); + + furi_string_free(file_name); + + mass_storage_app_show_loading_popup(app, false); + view_dispatcher_switch_to_view(app->view_dispatcher, MassStorageAppViewWork); +} + +void mass_storage_scene_work_on_exit(void* context) { + MassStorageApp* app = context; + mass_storage_app_show_loading_popup(app, true); + + if(app->usb_mutex) { + furi_mutex_free(app->usb_mutex); + app->usb_mutex = NULL; + } + if(app->usb) { + mass_storage_usb_stop(app->usb); + app->usb = NULL; + } + if(app->file) { + storage_file_free(app->file); + app->file = NULL; + } + mass_storage_app_show_loading_popup(app, false); +} diff --git a/mass_storage/views/mass_storage_view.c b/mass_storage/views/mass_storage_view.c new file mode 100644 index 00000000000..25eeb5c19e3 --- /dev/null +++ b/mass_storage/views/mass_storage_view.c @@ -0,0 +1,122 @@ +#include "mass_storage_view.h" +#include "../mass_storage_app_i.h" +#include + +struct MassStorage { + View* view; +}; + +typedef struct { + FuriString *file_name, *status_string; + uint32_t read_speed, write_speed; + uint32_t bytes_read, bytes_written; + uint32_t update_time; +} MassStorageModel; + +static void append_suffixed_byte_count(FuriString* string, uint32_t count) { + if(count < 1024) { + furi_string_cat_printf(string, "%luB", count); + } else if(count < 1024 * 1024) { + furi_string_cat_printf(string, "%luK", count / 1024); + } else if(count < 1024 * 1024 * 1024) { + furi_string_cat_printf(string, "%.1fM", (double)count / (1024 * 1024)); + } else { + furi_string_cat_printf(string, "%.1fG", (double)count / (1024 * 1024 * 1024)); + } +} + +static void mass_storage_draw_callback(Canvas* canvas, void* _model) { + MassStorageModel* model = _model; + + canvas_draw_icon(canvas, 8, 14, &I_Drive_112x35); + + canvas_set_font(canvas, FontPrimary); + canvas_draw_str_aligned( + canvas, canvas_width(canvas) / 2, 0, AlignCenter, AlignTop, "USB Mass Storage"); + + canvas_set_font(canvas, FontBatteryPercent); + elements_string_fit_width(canvas, model->file_name, 89 - 2); + canvas_draw_str_aligned( + canvas, 92, 24, AlignRight, AlignBottom, furi_string_get_cstr(model->file_name)); + + furi_string_set_str(model->status_string, "R:"); + append_suffixed_byte_count(model->status_string, model->bytes_read); + if(model->read_speed) { + furi_string_cat_str(model->status_string, "/"); + append_suffixed_byte_count(model->status_string, model->read_speed); + furi_string_cat_str(model->status_string, "s"); + } + canvas_draw_str(canvas, 14, 34, furi_string_get_cstr(model->status_string)); + + furi_string_set_str(model->status_string, "W:"); + append_suffixed_byte_count(model->status_string, model->bytes_written); + if(model->write_speed) { + furi_string_cat_str(model->status_string, "/"); + append_suffixed_byte_count(model->status_string, model->write_speed); + furi_string_cat_str(model->status_string, "s"); + } + canvas_draw_str(canvas, 14, 43, furi_string_get_cstr(model->status_string)); +} + +MassStorage* mass_storage_alloc() { + MassStorage* mass_storage = malloc(sizeof(MassStorage)); + + mass_storage->view = view_alloc(); + view_allocate_model(mass_storage->view, ViewModelTypeLocking, sizeof(MassStorageModel)); + with_view_model( + mass_storage->view, + MassStorageModel * model, + { + model->file_name = furi_string_alloc(); + model->status_string = furi_string_alloc(); + }, + false); + view_set_context(mass_storage->view, mass_storage); + view_set_draw_callback(mass_storage->view, mass_storage_draw_callback); + + return mass_storage; +} + +void mass_storage_free(MassStorage* mass_storage) { + furi_assert(mass_storage); + with_view_model( + mass_storage->view, + MassStorageModel * model, + { + furi_string_free(model->file_name); + furi_string_free(model->status_string); + }, + false); + view_free(mass_storage->view); + free(mass_storage); +} + +View* mass_storage_get_view(MassStorage* mass_storage) { + furi_assert(mass_storage); + return mass_storage->view; +} + +void mass_storage_set_file_name(MassStorage* mass_storage, FuriString* name) { + furi_assert(name); + with_view_model( + mass_storage->view, + MassStorageModel * model, + { furi_string_set(model->file_name, name); }, + true); +} + +void mass_storage_set_stats(MassStorage* mass_storage, uint32_t read, uint32_t written) { + with_view_model( + mass_storage->view, + MassStorageModel * model, + { + uint32_t now = furi_get_tick(); + model->read_speed = (read - model->bytes_read) * 1000 / (now - model->update_time); + model->write_speed = + (written - model->bytes_written) * 1000 / (now - model->update_time); + model->bytes_read = read; + model->bytes_written = written; + model->update_time = now; + }, + true); +} diff --git a/mass_storage/views/mass_storage_view.h b/mass_storage/views/mass_storage_view.h new file mode 100644 index 00000000000..2edbf2a62c7 --- /dev/null +++ b/mass_storage/views/mass_storage_view.h @@ -0,0 +1,15 @@ +#pragma once + +#include + +typedef struct MassStorage MassStorage; + +MassStorage* mass_storage_alloc(); + +void mass_storage_free(MassStorage* mass_storage); + +View* mass_storage_get_view(MassStorage* mass_storage); + +void mass_storage_set_file_name(MassStorage* mass_storage, FuriString* name); + +void mass_storage_set_stats(MassStorage* mass_storage, uint32_t read, uint32_t written); diff --git a/nightstand_clock/.gitattributes b/nightstand_clock/.gitattributes new file mode 100644 index 00000000000..dfe0770424b --- /dev/null +++ b/nightstand_clock/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/nightstand_clock/.gitsubtree b/nightstand_clock/.gitsubtree new file mode 100644 index 00000000000..d6fe361f029 --- /dev/null +++ b/nightstand_clock/.gitsubtree @@ -0,0 +1,2 @@ +https://github.com/xMasterX/all-the-plugins dev non_catalog_apps/FlipperNightStand_clock +https://github.com/nymda/FlipperNightStand main / diff --git a/nightstand_clock/README.md b/nightstand_clock/README.md new file mode 100644 index 00000000000..bba494eb9ed --- /dev/null +++ b/nightstand_clock/README.md @@ -0,0 +1,9 @@ +# FlipperNightStand + +Fork of the standard clock app, for use as a bedside clock at night + +-Date and AM/PM have their places swapped +-Backlight stays on constantly +-Control brightness with up/down +-Notification LED is turned off by default +-At 0 brightness, press down to toggle the LED between off and dim red diff --git a/nightstand_clock/application.fam b/nightstand_clock/application.fam new file mode 100644 index 00000000000..380c34b164d --- /dev/null +++ b/nightstand_clock/application.fam @@ -0,0 +1,14 @@ +App( + appid="nightstand", + name="Nightstand Clock", + apptype=FlipperAppType.EXTERNAL, + entry_point="clock_app", + requires=["gui"], + stack_size=2 * 1024, + fap_icon="clock.png", + fap_category="Tools", + fap_author="@nymda & @Willy-JL", + fap_weburl="https://github.com/nymda/FlipperNightStand", + fap_version="1.0", + fap_description="Clock with screen brightness controls", +) diff --git a/nightstand_clock/clock.png b/nightstand_clock/clock.png new file mode 100644 index 00000000000..8a5406e6ce5 Binary files /dev/null and b/nightstand_clock/clock.png differ diff --git a/nightstand_clock/clock_app.c b/nightstand_clock/clock_app.c new file mode 100644 index 00000000000..e8d00906bba --- /dev/null +++ b/nightstand_clock/clock_app.c @@ -0,0 +1,366 @@ +#include +#include + +#include +#include + +#include +#include + +#include "clock_app.h" + +/* + This is a modified version of the default clock app intended for use overnight + Up / Down controls the displays brightness. Down at brightness 0 turns the notification LED on and off. +*/ + +int brightness = 5; +bool led = false; +NotificationApp* notif = 0; + +int dspBrightnessBarFrames = 0; +const int dspBrightnessBarDisplayFrames = 8; + +const NotificationMessage message_red_dim = { + .type = NotificationMessageTypeLedRed, + .data.led.value = 0xFF / 16, +}; + +const NotificationMessage message_red_off = { + .type = NotificationMessageTypeLedRed, + .data.led.value = 0x00, +}; + +static const NotificationSequence led_on = { + &message_red_dim, + &message_do_not_reset, + NULL, +}; + +static const NotificationSequence led_off = { + &message_red_off, + &message_do_not_reset, + NULL, +}; + +static const NotificationSequence led_reset = { + &message_red_0, + NULL, +}; + +void set_backlight_brightness(float brightness) { + notif->settings.display_brightness = brightness; + notification_message(notif, &sequence_display_backlight_on); +} + +void handle_up() { + dspBrightnessBarFrames = dspBrightnessBarDisplayFrames; + if(brightness < 100) { + led = false; + notification_message(notif, &led_off); + brightness += 5; + } + set_backlight_brightness((float)(brightness / 100.f)); +} + +void handle_down() { + dspBrightnessBarFrames = dspBrightnessBarDisplayFrames; + if(brightness > 0) { + brightness -= 5; + if(brightness == 0) { //trigger only on the first brightness 5 -> 0 transition + led = true; + notification_message(notif, &led_on); + } + } else if(brightness == 0) { //trigger on every down press afterwards + led = !led; + if(led) { + notification_message(notif, &led_on); + } else { + notification_message(notif, &led_off); + } + } + set_backlight_brightness((float)(brightness / 100.f)); +} + +static void clock_input_callback(InputEvent* input_event, FuriMessageQueue* event_queue) { + furi_assert(event_queue); + PluginEvent event = {.type = EventTypeKey, .input = *input_event}; + furi_message_queue_put(event_queue, &event, FuriWaitForever); +} + +//do you are have stupid? +void elements_progress_bar_vertical( + Canvas* canvas, + uint8_t x, + uint8_t y, + uint8_t height, + float progress) { + furi_assert(canvas); + furi_assert((progress >= 0) && (progress <= 1.0)); + uint8_t width = 9; + + uint8_t progress_length = roundf((1.f - progress) * (height - 2)); + + canvas_set_color(canvas, ColorBlack); + canvas_draw_box(canvas, x + 1, y + 1, width - 2, height - 2); + + canvas_set_color(canvas, ColorWhite); + canvas_draw_box(canvas, x + 1, y + 1, width - 2, progress_length); + + canvas_set_color(canvas, ColorBlack); + canvas_draw_rframe(canvas, x, y, width, height, 3); +} + +static void clock_render_callback(Canvas* const canvas, void* ctx) { + //canvas_clear(canvas); + //canvas_set_color(canvas, ColorBlack); + + //avoids a bug with the brightness being reverted after the backlight-off period + set_backlight_brightness((float)(brightness / 100.f)); + + if(dspBrightnessBarFrames > 0) { + elements_progress_bar_vertical(canvas, 119, 1, 62, (float)(brightness / 100.f)); + dspBrightnessBarFrames--; + } + + ClockState* state = ctx; + if(furi_mutex_acquire(state->mutex, 200) != FuriStatusOk) { + //FURI_LOG_D(TAG, "Can't obtain mutex, requeue render"); + PluginEvent event = {.type = EventTypeTick}; + furi_message_queue_put(state->event_queue, &event, 0); + return; + } + + DateTime curr_dt; + furi_hal_rtc_get_datetime(&curr_dt); + uint32_t curr_ts = datetime_datetime_to_timestamp(&curr_dt); + + char time_string[TIME_LEN]; + char date_string[DATE_LEN]; + char meridian_string[MERIDIAN_LEN]; + char timer_string[20]; + + if(state->time_format == LocaleTimeFormat24h) { + snprintf( + time_string, TIME_LEN, CLOCK_TIME_FORMAT, curr_dt.hour, curr_dt.minute, curr_dt.second); + } else { + bool pm = curr_dt.hour > 12; + bool pm12 = curr_dt.hour >= 12; + snprintf( + time_string, + TIME_LEN, + CLOCK_TIME_FORMAT, + pm ? curr_dt.hour - 12 : curr_dt.hour, + curr_dt.minute, + curr_dt.second); + + snprintf( + meridian_string, + MERIDIAN_LEN, + MERIDIAN_FORMAT, + pm12 ? MERIDIAN_STRING_PM : MERIDIAN_STRING_AM); + } + + if(state->date_format == LocaleDateFormatYMD) { + snprintf( + date_string, DATE_LEN, CLOCK_ISO_DATE_FORMAT, curr_dt.year, curr_dt.month, curr_dt.day); + } else if(state->date_format == LocaleDateFormatMDY) { + snprintf( + date_string, DATE_LEN, CLOCK_RFC_DATE_FORMAT, curr_dt.month, curr_dt.day, curr_dt.year); + } else { + snprintf( + date_string, DATE_LEN, CLOCK_RFC_DATE_FORMAT, curr_dt.day, curr_dt.month, curr_dt.year); + } + + bool timer_running = state->timer_running; + uint32_t timer_start_timestamp = state->timer_start_timestamp; + uint32_t timer_stopped_seconds = state->timer_stopped_seconds; + + furi_mutex_release(state->mutex); + + canvas_set_font(canvas, FontBigNumbers); + + if(timer_start_timestamp != 0) { + int32_t elapsed_secs = timer_running ? (curr_ts - timer_start_timestamp) : + timer_stopped_seconds; + snprintf(timer_string, 20, "%.2ld:%.2ld", elapsed_secs / 60, elapsed_secs % 60); + canvas_draw_str_aligned(canvas, 64, 8, AlignCenter, AlignCenter, time_string); // DRAW TIME + canvas_draw_str_aligned(canvas, 64, 32, AlignCenter, AlignTop, timer_string); // DRAW TIMER + canvas_set_font(canvas, FontSecondary); + canvas_draw_str_aligned(canvas, 64, 20, AlignCenter, AlignTop, date_string); // DRAW DATE + elements_button_left(canvas, "Reset"); + } else { + canvas_draw_str_aligned(canvas, 64, 32, AlignCenter, AlignCenter, time_string); + canvas_set_font(canvas, FontSecondary); + canvas_draw_str_aligned(canvas, 65, 17, AlignCenter, AlignCenter, date_string); + + if(state->time_format == LocaleTimeFormat12h) + canvas_draw_str_aligned(canvas, 64, 47, AlignCenter, AlignCenter, meridian_string); + } + if(timer_running) { + elements_button_center(canvas, "Stop"); + } else if(timer_start_timestamp != 0 && !timer_running) { + elements_button_center(canvas, "Start"); + } +} + +static void clock_state_init(ClockState* const state) { + state->time_format = locale_get_time_format(); + + state->date_format = locale_get_date_format(); + + //FURI_LOG_D(TAG, "Time format: %s", state->settings.time_format == H12 ? "12h" : "24h"); + //FURI_LOG_D(TAG, "Date format: %s", state->settings.date_format == Iso ? "ISO 8601" : "RFC 5322"); + //furi_hal_rtc_get_datetime(&state->datetime); +} + +// Runs every 1000ms by default +static void clock_tick(void* ctx) { + furi_assert(ctx); + FuriMessageQueue* event_queue = ctx; + PluginEvent event = {.type = EventTypeTick}; + // It's OK to loose this event if system overloaded + furi_message_queue_put(event_queue, &event, 0); +} + +void timer_start_stop(ClockState* plugin_state) { + // START/STOP TIMER + uint32_t curr_ts = furi_hal_rtc_get_timestamp(); + + if(plugin_state->timer_running) { + // Update stopped seconds + plugin_state->timer_stopped_seconds = curr_ts - plugin_state->timer_start_timestamp; + } else { + if(plugin_state->timer_start_timestamp == 0) { + // Set starting timestamp if this is first time + plugin_state->timer_start_timestamp = curr_ts; + } else { + // Timer was already running, need to slightly readjust so we don't + // count the intervening time + plugin_state->timer_start_timestamp = curr_ts - plugin_state->timer_stopped_seconds; + } + } + plugin_state->timer_running = !plugin_state->timer_running; +} + +void timer_reset_seconds(ClockState* plugin_state) { + if(plugin_state->timer_start_timestamp != 0) { + // Reset seconds + plugin_state->timer_running = false; + plugin_state->timer_start_timestamp = 0; + plugin_state->timer_stopped_seconds = 0; + } +} + +int32_t clock_app(void* p) { + UNUSED(p); + ClockState* plugin_state = malloc(sizeof(ClockState)); + + plugin_state->event_queue = furi_message_queue_alloc(8, sizeof(PluginEvent)); + if(plugin_state->event_queue == NULL) { + FURI_LOG_E(TAG, "Cannot create event queue"); + free(plugin_state); + return 255; + } + //FURI_LOG_D(TAG, "Event queue created"); + + plugin_state->mutex = furi_mutex_alloc(FuriMutexTypeNormal); + if(plugin_state->mutex == NULL) { + FURI_LOG_E(TAG, "Cannot create mutex"); + furi_message_queue_free(plugin_state->event_queue); + free(plugin_state); + return 255; + } + //FURI_LOG_D(TAG, "Mutex created"); + + clock_state_init(plugin_state); + + notif = furi_record_open(RECORD_NOTIFICATION); + float tmpBrightness = notif->settings.display_brightness; + brightness = tmpBrightness * 100; // Keep current brightness by default + + notification_message(notif, &sequence_display_backlight_enforce_on); + notification_message(notif, &led_off); + + // Set system callbacks + ViewPort* view_port = view_port_alloc(); + view_port_draw_callback_set(view_port, clock_render_callback, plugin_state); + view_port_input_callback_set(view_port, clock_input_callback, plugin_state->event_queue); + + FuriTimer* timer = + furi_timer_alloc(clock_tick, FuriTimerTypePeriodic, plugin_state->event_queue); + + if(timer == NULL) { + FURI_LOG_E(TAG, "Cannot create timer"); + furi_mutex_free(plugin_state->mutex); + furi_message_queue_free(plugin_state->event_queue); + free(plugin_state); + return 255; + } + //FURI_LOG_D(TAG, "Timer created"); + + // Open GUI and register view_port + Gui* gui = furi_record_open(RECORD_GUI); + gui_add_view_port(gui, view_port, GuiLayerFullscreen); + + furi_timer_start(timer, furi_kernel_get_tick_frequency()); + //FURI_LOG_D(TAG, "Timer started"); + + // Main loop + PluginEvent event; + for(bool processing = true; processing;) { + FuriStatus event_status = furi_message_queue_get(plugin_state->event_queue, &event, 100); + + if(event_status != FuriStatusOk) continue; + + if(furi_mutex_acquire(plugin_state->mutex, FuriWaitForever) != FuriStatusOk) continue; + // press events + if(event.type == EventTypeKey) { + if(event.input.type == InputTypeShort) { + switch(event.input.key) { + case InputKeyLeft: + // Reset seconds + timer_reset_seconds(plugin_state); + break; + case InputKeyOk: + // Toggle timer + timer_start_stop(plugin_state); + break; + case InputKeyBack: + // Exit the plugin + processing = false; + break; + case InputKeyUp: + handle_up(); + break; + case InputKeyDown: + handle_down(); + break; + default: + break; + } + } + } /*else if(event.type == EventTypeTick) { + furi_hal_rtc_get_datetime(&plugin_state->datetime); + }*/ + + furi_mutex_release(plugin_state->mutex); + view_port_update(view_port); + } + + furi_timer_free(timer); + view_port_enabled_set(view_port, false); + gui_remove_view_port(gui, view_port); + furi_record_close(RECORD_GUI); + furi_record_close(RECORD_NOTIFICATION); + view_port_free(view_port); + furi_message_queue_free(plugin_state->event_queue); + furi_mutex_free(plugin_state->mutex); + free(plugin_state); + + set_backlight_brightness(tmpBrightness); + notification_message(notif, &sequence_display_backlight_enforce_auto); + notification_message(notif, &led_reset); + + return 0; +} \ No newline at end of file diff --git a/nightstand_clock/clock_app.h b/nightstand_clock/clock_app.h new file mode 100644 index 00000000000..74648ba330e --- /dev/null +++ b/nightstand_clock/clock_app.h @@ -0,0 +1,39 @@ +#pragma once + +#include +#include + +#define TAG "Clock" + +#define CLOCK_ISO_DATE_FORMAT "%.4d-%.2d-%.2d" +#define CLOCK_RFC_DATE_FORMAT "%.2d-%.2d-%.4d" +#define CLOCK_TIME_FORMAT "%.2d:%.2d:%.2d" + +#define MERIDIAN_FORMAT "%s" +#define MERIDIAN_STRING_AM "AM" +#define MERIDIAN_STRING_PM "PM" + +#define TIME_LEN 12 +#define DATE_LEN 14 +#define MERIDIAN_LEN 3 + +typedef enum { + EventTypeTick, + EventTypeKey, +} EventType; + +typedef struct { + EventType type; + InputEvent input; +} PluginEvent; + +typedef struct { + LocaleDateFormat date_format; + LocaleTimeFormat time_format; + DateTime datetime; + FuriMutex* mutex; + FuriMessageQueue* event_queue; + uint32_t timer_start_timestamp; + uint32_t timer_stopped_seconds; + bool timer_running; +} ClockState; diff --git a/subghz_playlist/.gitsubtree b/subghz_playlist/.gitsubtree new file mode 100644 index 00000000000..7efa9c1aa7a --- /dev/null +++ b/subghz_playlist/.gitsubtree @@ -0,0 +1 @@ +https://github.com/xMasterX/all-the-plugins dev base_pack/playlist diff --git a/subghz_playlist/application.fam b/subghz_playlist/application.fam new file mode 100644 index 00000000000..4f112ff968a --- /dev/null +++ b/subghz_playlist/application.fam @@ -0,0 +1,15 @@ +App( + appid="subghz_playlist", + name="Sub-GHz Playlist", + apptype=FlipperAppType.EXTERNAL, + entry_point="playlist_app", + requires=["storage", "gui", "dialogs", "subghz"], + stack_size=2 * 1024, + order=14, + fap_icon="subplaylist_10px.png", + fap_category="Sub-GHz", + fap_icon_assets="images", + fap_author="@darmiel", + fap_version="1.0", + fap_description="App works with list of sub-ghz files from .txt file that contains paths to target files.", +) diff --git a/subghz_playlist/canvas_helper.c b/subghz_playlist/canvas_helper.c new file mode 100644 index 00000000000..ecb2eed8ba2 --- /dev/null +++ b/subghz_playlist/canvas_helper.c @@ -0,0 +1,81 @@ +#include + +#define WIDTH 128 +#define HEIGHT 64 + +void draw_centered_boxed_str(Canvas* canvas, int x, int y, int height, int pad, const char* text) { + // get width of text + int w = canvas_string_width(canvas, text); + canvas_draw_rframe(canvas, x, y, w + pad, height, 2); + canvas_draw_str_aligned(canvas, x + pad / 2, y + height / 2, AlignLeft, AlignCenter, text); +} + +void draw_corner_aligned(Canvas* canvas, int width, int height, Align horizontal, Align vertical) { + canvas_set_color(canvas, ColorBlack); + switch(horizontal) { + case AlignLeft: + switch(vertical) { + case AlignTop: + canvas_draw_rbox(canvas, 0, 0, width, height, 3); + canvas_draw_box(canvas, 0, 0, width, 3); + canvas_draw_box(canvas, 0, 0, 3, height); + break; + case AlignCenter: + canvas_draw_rbox(canvas, 0, HEIGHT - height / 2, width, height, 3); + canvas_draw_box(canvas, 0, HEIGHT - height / 2, 3, height); + break; + case AlignBottom: + canvas_draw_rbox(canvas, 0, HEIGHT - height, width, height, 3); + canvas_draw_box(canvas, 0, HEIGHT - height, 3, height); + canvas_draw_box(canvas, 0, HEIGHT - 3, width, 3); + break; + default: + break; + } + break; + case AlignRight: + switch(vertical) { + case AlignTop: + canvas_draw_rbox(canvas, WIDTH - width, 0, width, height, 3); + canvas_draw_box(canvas, WIDTH - width, 0, width, 3); // bottom corner + canvas_draw_box(canvas, WIDTH - 3, 0, 3, height); // right corner + break; + case AlignCenter: + canvas_draw_rbox(canvas, WIDTH - width, HEIGHT / 2 - height / 2, width, height, 3); + canvas_draw_box(canvas, WIDTH - 3, HEIGHT / 2 - height / 2, 3, height); // right corner + break; + case AlignBottom: + canvas_draw_rbox(canvas, WIDTH - width, HEIGHT - height, width, height, 3); + canvas_draw_box(canvas, WIDTH - 3, HEIGHT - height, 3, height); // right corner + canvas_draw_box(canvas, WIDTH - width, HEIGHT - 3, width, 3); // bottom corner + break; + default: + break; + } + break; + case AlignCenter: + switch(vertical) { + case AlignTop: + canvas_draw_rbox(canvas, WIDTH / 2 - width / 2, 0, width, height, 3); + canvas_draw_box(canvas, WIDTH / 2 - width / 2, 0, width, 3); // bottom corner + canvas_draw_box(canvas, WIDTH / 2 - 3, 0, 3, height); // right corner + break; + case AlignCenter: + canvas_draw_rbox( + canvas, WIDTH / 2 - width / 2, HEIGHT / 2 - height / 2, width, height, 3); + canvas_draw_box( + canvas, WIDTH / 2 - 3, HEIGHT / 2 - height / 2, 3, height); // right corner + break; + case AlignBottom: + canvas_draw_rbox(canvas, WIDTH / 2 - width / 2, HEIGHT - height, width, height, 3); + canvas_draw_box(canvas, WIDTH / 2 - 3, HEIGHT - height, 3, height); // right corner + canvas_draw_box(canvas, WIDTH / 2 - width / 2, HEIGHT - 3, width, 3); // bottom corner + break; + default: + break; + } + break; + default: + break; + } +} \ No newline at end of file diff --git a/subghz_playlist/canvas_helper.h b/subghz_playlist/canvas_helper.h new file mode 100644 index 00000000000..cf73bdb32bb --- /dev/null +++ b/subghz_playlist/canvas_helper.h @@ -0,0 +1,5 @@ +#include + +void draw_centered_boxed_str(Canvas* canvas, int x, int y, int height, int pad, const char* text); + +void draw_corner_aligned(Canvas* canvas, int width, int height, Align horizontal, Align vertical); \ No newline at end of file diff --git a/subghz_playlist/helpers/radio_device_loader.c b/subghz_playlist/helpers/radio_device_loader.c new file mode 100644 index 00000000000..d2cffde5830 --- /dev/null +++ b/subghz_playlist/helpers/radio_device_loader.c @@ -0,0 +1,64 @@ +#include "radio_device_loader.h" + +#include +#include + +static void radio_device_loader_power_on() { + uint8_t attempts = 0; + while(!furi_hal_power_is_otg_enabled() && attempts++ < 5) { + furi_hal_power_enable_otg(); + //CC1101 power-up time + furi_delay_ms(10); + } +} + +static void radio_device_loader_power_off() { + if(furi_hal_power_is_otg_enabled()) furi_hal_power_disable_otg(); +} + +bool radio_device_loader_is_connect_external(const char* name) { + bool is_connect = false; + bool is_otg_enabled = furi_hal_power_is_otg_enabled(); + + if(!is_otg_enabled) { + radio_device_loader_power_on(); + } + + const SubGhzDevice* device = subghz_devices_get_by_name(name); + if(device) { + is_connect = subghz_devices_is_connect(device); + } + + if(!is_otg_enabled) { + radio_device_loader_power_off(); + } + return is_connect; +} + +const SubGhzDevice* radio_device_loader_set( + const SubGhzDevice* current_radio_device, + SubGhzRadioDeviceType radio_device_type) { + const SubGhzDevice* radio_device; + + if(radio_device_type == SubGhzRadioDeviceTypeExternalCC1101 && + radio_device_loader_is_connect_external(SUBGHZ_DEVICE_CC1101_EXT_NAME)) { + radio_device_loader_power_on(); + radio_device = subghz_devices_get_by_name(SUBGHZ_DEVICE_CC1101_EXT_NAME); + subghz_devices_begin(radio_device); + } else if(current_radio_device == NULL) { + radio_device = subghz_devices_get_by_name(SUBGHZ_DEVICE_CC1101_INT_NAME); + } else { + radio_device_loader_end(current_radio_device); + radio_device = subghz_devices_get_by_name(SUBGHZ_DEVICE_CC1101_INT_NAME); + } + + return radio_device; +} + +void radio_device_loader_end(const SubGhzDevice* radio_device) { + furi_assert(radio_device); + radio_device_loader_power_off(); + if(radio_device != subghz_devices_get_by_name(SUBGHZ_DEVICE_CC1101_INT_NAME)) { + subghz_devices_end(radio_device); + } +} \ No newline at end of file diff --git a/subghz_playlist/helpers/radio_device_loader.h b/subghz_playlist/helpers/radio_device_loader.h new file mode 100644 index 00000000000..bee4e2c362b --- /dev/null +++ b/subghz_playlist/helpers/radio_device_loader.h @@ -0,0 +1,15 @@ +#pragma once + +#include + +/** SubGhzRadioDeviceType */ +typedef enum { + SubGhzRadioDeviceTypeInternal, + SubGhzRadioDeviceTypeExternalCC1101, +} SubGhzRadioDeviceType; + +const SubGhzDevice* radio_device_loader_set( + const SubGhzDevice* current_radio_device, + SubGhzRadioDeviceType radio_device_type); + +void radio_device_loader_end(const SubGhzDevice* radio_device); \ No newline at end of file diff --git a/subghz_playlist/images/ButtonRight_4x7.png b/subghz_playlist/images/ButtonRight_4x7.png new file mode 100644 index 00000000000..8e1c74c1c00 Binary files /dev/null and b/subghz_playlist/images/ButtonRight_4x7.png differ diff --git a/subghz_playlist/images/sub1_10px.png b/subghz_playlist/images/sub1_10px.png new file mode 100644 index 00000000000..5a25fdf4ef1 Binary files /dev/null and b/subghz_playlist/images/sub1_10px.png differ diff --git a/subghz_playlist/playlist.c b/subghz_playlist/playlist.c new file mode 100644 index 00000000000..e2fb7051fe6 --- /dev/null +++ b/subghz_playlist/playlist.c @@ -0,0 +1,843 @@ +#include + +#include +#include +#include +#include + +#include +#include + +#include +#include + +#include "helpers/radio_device_loader.h" + +#include "flipper_format_stream.h" +#include "flipper_format_stream_i.h" + +#include +#include + +#include "playlist_file.h" +#include "canvas_helper.h" + +#define PLAYLIST_EXT ".txt" +#define TAG "Playlist" + +#define STATE_NONE 0 +#define STATE_OVERVIEW 1 +#define STATE_SENDING 2 + +#define WIDTH 128 +#define HEIGHT 64 + +typedef struct { + int current_count; // number of processed files + int total_count; // number of items in the playlist + + int playlist_repetitions; // number of times to repeat the whole playlist + int current_playlist_repetition; // current playlist repetition + + // last 3 files + FuriString* prev_0_path; // current file + FuriString* prev_1_path; // previous file + FuriString* prev_2_path; // previous previous file + FuriString* prev_3_path; // you get the idea + + int state; // current state + + ViewPort* view_port; +} DisplayMeta; + +typedef struct { + FuriThread* thread; + Storage* storage; + FlipperFormat* format; + + DisplayMeta* meta; + + FuriString* file_path; // path to the playlist file + const SubGhzDevice* radio_device; + + bool ctl_request_exit; // can be set to true if the worker should exit + bool ctl_pause; // can be set to true if the worker should pause + bool ctl_request_skip; // can be set to true if the worker should skip the current file + bool ctl_request_prev; // can be set to true if the worker should go to the previous file + + bool is_running; // indicates if the worker is running +} PlaylistWorker; + +typedef struct { + FuriMutex* mutex; + FuriMessageQueue* input_queue; + ViewPort* view_port; + Gui* gui; + + DisplayMeta* meta; + PlaylistWorker* worker; + + FuriString* file_path; // Path to the playlist file +} Playlist; + +//////////////////////////////////////////////////////////////////////////////// + +void meta_set_state(DisplayMeta* meta, int state) { + meta->state = state; + view_port_update(meta->view_port); +} + +static FuriHalSubGhzPreset str_to_preset(FuriString* preset) { + if(furi_string_cmp_str(preset, "FuriHalSubGhzPresetOok270Async") == 0) { + return FuriHalSubGhzPresetOok270Async; + } + if(furi_string_cmp_str(preset, "FuriHalSubGhzPresetOok650Async") == 0) { + return FuriHalSubGhzPresetOok650Async; + } + if(furi_string_cmp_str(preset, "FuriHalSubGhzPreset2FSKDev238Async") == 0) { + return FuriHalSubGhzPreset2FSKDev238Async; + } + if(furi_string_cmp_str(preset, "FuriHalSubGhzPreset2FSKDev476Async") == 0) { + return FuriHalSubGhzPreset2FSKDev476Async; + } + if(furi_string_cmp_str(preset, "FuriHalSubGhzPresetMSK99_97KbAsync") == 0) { + return FuriHalSubGhzPresetMSK99_97KbAsync; + } + if(furi_string_cmp_str(preset, "FuriHalSubGhzPresetMSK99_97KbAsync") == 0) { + return FuriHalSubGhzPresetMSK99_97KbAsync; + } + return FuriHalSubGhzPresetCustom; +} + +// -4: missing protocol +// -3: missing preset +// -2: transmit error +// -1: error +// 0: ok +// 1: resend +// 2: exited +static int playlist_worker_process( + PlaylistWorker* worker, + FlipperFormat* fff_file, + FlipperFormat* fff_data, + const char* path, + FuriString* preset, + FuriString* protocol) { + // actual sending of .sub file + + if(!flipper_format_file_open_existing(fff_file, path)) { + FURI_LOG_E(TAG, " (TX) Failed to open %s", path); + return -1; + } + + // read frequency or default to 433.92MHz + uint32_t frequency = 0; + if(!flipper_format_read_uint32(fff_file, "Frequency", &frequency, 1)) { + FURI_LOG_W(TAG, " (TX) Missing Frequency, defaulting to 433.92MHz"); + frequency = 433920000; + } + if(!subghz_devices_is_frequency_valid(worker->radio_device, frequency)) { + FURI_LOG_E( + TAG, " (TX) The SubGhz device used does not support the frequency %lu", frequency); + return -2; + } + + // check if preset is present + if(!flipper_format_read_string(fff_file, "Preset", preset)) { + FURI_LOG_E(TAG, " (TX) Missing Preset"); + return -3; + } + + // check if protocol is present + if(!flipper_format_read_string(fff_file, "Protocol", protocol)) { + FURI_LOG_E(TAG, " (TX) Missing Protocol"); + return -4; + } + + if(!furi_string_cmp_str(protocol, "RAW")) { + subghz_protocol_raw_gen_fff_data( + fff_data, path, subghz_devices_get_name(worker->radio_device)); + } else { + stream_copy_full( + flipper_format_get_raw_stream(fff_file), flipper_format_get_raw_stream(fff_data)); + } + flipper_format_file_close(fff_file); + flipper_format_free(fff_file); + + // (try to) send file + SubGhzEnvironment* environment = subghz_environment_alloc(); + subghz_environment_set_protocol_registry(environment, (void*)&subghz_protocol_registry); + SubGhzTransmitter* transmitter = + subghz_transmitter_alloc_init(environment, furi_string_get_cstr(protocol)); + + subghz_transmitter_deserialize(transmitter, fff_data); + + subghz_devices_load_preset(worker->radio_device, str_to_preset(preset), NULL); + // there is no check for a custom preset + frequency = subghz_devices_set_frequency(worker->radio_device, frequency); + + // Set device to TX and check frequency is alowed to TX + if(!subghz_devices_set_tx(worker->radio_device)) { + FURI_LOG_E( + TAG, + " (TX) The SubGhz device used does not support the frequency for transmitеing, %lu", + frequency); + return -5; + } + FURI_LOG_D(TAG, " (TX) Start sending ..."); + int status = 0; + + subghz_devices_start_async_tx(worker->radio_device, subghz_transmitter_yield, transmitter); + while(!subghz_devices_is_async_complete_tx(worker->radio_device)) { + if(worker->ctl_request_exit) { + FURI_LOG_D(TAG, " (TX) Requested to exit. Cancelling sending..."); + status = 2; + break; + } + if(worker->ctl_pause) { + FURI_LOG_D(TAG, " (TX) Requested to pause. Cancelling and resending..."); + status = 1; + break; + } + if(worker->ctl_request_skip) { + worker->ctl_request_skip = false; + FURI_LOG_D(TAG, " (TX) Requested to skip. Cancelling and resending..."); + status = 0; + break; + } + if(worker->ctl_request_prev) { + worker->ctl_request_prev = false; + FURI_LOG_D(TAG, " (TX) Requested to prev. Cancelling and resending..."); + status = 3; + break; + } + furi_delay_ms(50); + } + + FURI_LOG_D(TAG, " (TX) Done sending."); + + subghz_devices_stop_async_tx(worker->radio_device); + subghz_devices_idle(worker->radio_device); + + subghz_transmitter_free(transmitter); + subghz_environment_free(environment); + + return status; +} + +// true - the worker can continue +// false - the worker should exit +static bool playlist_worker_wait_pause(PlaylistWorker* worker) { + // wait if paused + while(worker->ctl_pause && !worker->ctl_request_exit) { + furi_delay_ms(50); + } + // exit loop if requested to stop + if(worker->ctl_request_exit) { + FURI_LOG_D(TAG, "Requested to exit. Exiting loop..."); + return false; + } + return true; +} + +void updatePlayListView(PlaylistWorker* worker, const char* str) { + furi_string_reset(worker->meta->prev_3_path); + furi_string_set(worker->meta->prev_3_path, furi_string_get_cstr(worker->meta->prev_2_path)); + + furi_string_reset(worker->meta->prev_2_path); + furi_string_set(worker->meta->prev_2_path, furi_string_get_cstr(worker->meta->prev_1_path)); + + furi_string_reset(worker->meta->prev_1_path); + furi_string_set(worker->meta->prev_1_path, furi_string_get_cstr(worker->meta->prev_0_path)); + + furi_string_reset(worker->meta->prev_0_path); + furi_string_set(worker->meta->prev_0_path, str); + + view_port_update(worker->meta->view_port); +} + +static bool playlist_worker_play_playlist_once( + PlaylistWorker* worker, + Storage* storage, + FlipperFormat* fff_head, + FlipperFormat* fff_data, + FuriString* data, + FuriString* preset, + FuriString* protocol) { + // + if(!flipper_format_rewind(fff_head)) { + FURI_LOG_E(TAG, "Failed to rewind file"); + return false; + } + + while(flipper_format_read_string(fff_head, "sub", data)) { + if(!playlist_worker_wait_pause(worker)) { + break; + } + + // update state to sending + meta_set_state(worker->meta, STATE_SENDING); + + ++worker->meta->current_count; + const char* str = furi_string_get_cstr(data); + + // it's not fancy, but it works for now :) + updatePlayListView(worker, str); + + for(int i = 0; i < 1; i++) { + if(!playlist_worker_wait_pause(worker)) { + break; + } + + view_port_update(worker->meta->view_port); + + FURI_LOG_D(TAG, "(worker) Sending %s", str); + + FlipperFormat* fff_file = flipper_format_file_alloc(storage); + + int status = + playlist_worker_process(worker, fff_file, fff_data, str, preset, protocol); + + // if there was an error, fff_file is not already freed + if(status < 0) { + flipper_format_file_close(fff_file); + flipper_format_free(fff_file); + } + + // re-send file is paused mid-send + if(status == 1) { + i -= 1; + // errored, skip to next file + } else if(status < 0) { + break; + // exited, exit loop + } else if(status == 2) { + return false; + } else if(status == 3) { + //aqui rebobinamos y avanzamos de nuevo el fichero n-1 veces + //decrementamos el contador de ficheros enviados + worker->meta->current_count--; + if(worker->meta->current_count > 0) { + worker->meta->current_count--; + } + //rebobinamos el fichero + if(!flipper_format_rewind(fff_head)) { + FURI_LOG_E(TAG, "Failed to rewind file"); + return false; + } + //avanzamos el fichero n-1 veces + for(int j = 0; j < worker->meta->current_count; j++) { + flipper_format_read_string(fff_head, "sub", data); + } + break; + } + } + } // end of loop + return true; +} + +static int32_t playlist_worker_thread(void* ctx) { + Storage* storage = furi_record_open(RECORD_STORAGE); + FlipperFormat* fff_head = flipper_format_file_alloc(storage); + + PlaylistWorker* worker = ctx; + if(!flipper_format_file_open_existing(fff_head, furi_string_get_cstr(worker->file_path))) { + FURI_LOG_E(TAG, "Failed to open %s", furi_string_get_cstr(worker->file_path)); + worker->is_running = false; + + furi_record_close(RECORD_STORAGE); + flipper_format_free(fff_head); + return 0; + } + + playlist_worker_wait_pause(worker); + FlipperFormat* fff_data = flipper_format_string_alloc(); + + FuriString* data; + FuriString* preset; + FuriString* protocol; + data = furi_string_alloc(); + preset = furi_string_alloc(); + protocol = furi_string_alloc(); + + for(int i = 0; i < MAX(1, worker->meta->playlist_repetitions); i++) { + // infinite repetitions if playlist_repetitions is 0 + if(worker->meta->playlist_repetitions <= 0) { + --i; + } + ++worker->meta->current_playlist_repetition; + // send playlist + worker->meta->current_count = 0; + if(worker->ctl_request_exit) { + break; + } + + FURI_LOG_D( + TAG, + "Sending playlist (i %d rep %d b %d)", + i, + worker->meta->current_playlist_repetition, + worker->meta->playlist_repetitions); + + if(!playlist_worker_play_playlist_once( + worker, storage, fff_head, fff_data, data, preset, protocol)) { + break; + } + } + + furi_record_close(RECORD_STORAGE); + flipper_format_free(fff_head); + + furi_string_free(data); + furi_string_free(preset); + furi_string_free(protocol); + + flipper_format_free(fff_data); + + FURI_LOG_D(TAG, "Done reading. Read %d data lines.", worker->meta->current_count); + worker->is_running = false; + + // update state to overview + meta_set_state(worker->meta, STATE_OVERVIEW); + + return 0; +} + +//////////////////////////////////////////////////////////////////////////////// + +void playlist_meta_reset(DisplayMeta* instance) { + instance->current_count = 0; + instance->current_playlist_repetition = 0; + + furi_string_reset(instance->prev_0_path); + furi_string_reset(instance->prev_1_path); + furi_string_reset(instance->prev_2_path); + furi_string_reset(instance->prev_3_path); +} + +DisplayMeta* playlist_meta_alloc() { + DisplayMeta* instance = malloc(sizeof(DisplayMeta)); + instance->prev_0_path = furi_string_alloc(); + instance->prev_1_path = furi_string_alloc(); + instance->prev_2_path = furi_string_alloc(); + instance->prev_3_path = furi_string_alloc(); + playlist_meta_reset(instance); + instance->state = STATE_NONE; + instance->playlist_repetitions = 1; + return instance; +} + +void playlist_meta_free(DisplayMeta* instance) { + furi_string_free(instance->prev_0_path); + furi_string_free(instance->prev_1_path); + furi_string_free(instance->prev_2_path); + furi_string_free(instance->prev_3_path); + free(instance); +} + +//////////////////////////////////////////////////////////////////////////////// + +PlaylistWorker* playlist_worker_alloc(DisplayMeta* meta) { + PlaylistWorker* instance = malloc(sizeof(PlaylistWorker)); + + instance->thread = furi_thread_alloc(); + furi_thread_set_name(instance->thread, "PlaylistWorker"); + furi_thread_set_stack_size(instance->thread, 2048); + furi_thread_set_context(instance->thread, instance); + furi_thread_set_callback(instance->thread, playlist_worker_thread); + + instance->meta = meta; + instance->ctl_pause = true; // require the user to manually start the worker + + instance->file_path = furi_string_alloc(); + + subghz_devices_init(); + + instance->radio_device = + radio_device_loader_set(instance->radio_device, SubGhzRadioDeviceTypeExternalCC1101); + + subghz_devices_reset(instance->radio_device); + subghz_devices_idle(instance->radio_device); + + return instance; +} + +void playlist_worker_free(PlaylistWorker* instance) { + furi_assert(instance); + furi_thread_free(instance->thread); + furi_string_free(instance->file_path); + + subghz_devices_sleep(instance->radio_device); + radio_device_loader_end(instance->radio_device); + + subghz_devices_deinit(); + + free(instance); +} + +void playlist_worker_stop(PlaylistWorker* worker) { + furi_assert(worker); + furi_assert(worker->is_running); + + worker->ctl_request_exit = true; + furi_thread_join(worker->thread); +} + +bool playlist_worker_running(PlaylistWorker* worker) { + furi_assert(worker); + return worker->is_running; +} + +void playlist_worker_start(PlaylistWorker* instance, const char* file_path) { + furi_assert(instance); + furi_assert(!instance->is_running); + + furi_string_set(instance->file_path, file_path); + instance->is_running = true; + + // reset meta (current/total) + playlist_meta_reset(instance->meta); + + FURI_LOG_D(TAG, "Starting thread..."); + furi_thread_start(instance->thread); +} + +//////////////////////////////////////////////////////////////////////////////// + +static void render_callback(Canvas* canvas, void* ctx) { + Playlist* app = ctx; + furi_check(furi_mutex_acquire(app->mutex, FuriWaitForever) == FuriStatusOk); + + canvas_clear(canvas); + canvas_set_color(canvas, ColorBlack); + canvas_set_font(canvas, FontSecondary); + + FuriString* temp_str; + temp_str = furi_string_alloc(); + + switch(app->meta->state) { + case STATE_NONE: + canvas_set_font(canvas, FontPrimary); + canvas_draw_str_aligned( + canvas, WIDTH / 2, HEIGHT / 2, AlignCenter, AlignCenter, "No playlist loaded"); + break; + + case STATE_OVERVIEW: + // draw file name + { + path_extract_filename(app->file_path, temp_str, true); + + canvas_set_font(canvas, FontPrimary); + draw_centered_boxed_str(canvas, 1, 1, 15, 6, furi_string_get_cstr(temp_str)); + } + + canvas_set_font(canvas, FontSecondary); + + // draw loaded count + { + furi_string_printf(temp_str, "%d Items in playlist", app->meta->total_count); + canvas_draw_str_aligned( + canvas, 1, 19, AlignLeft, AlignTop, furi_string_get_cstr(temp_str)); + + if(app->meta->playlist_repetitions <= 0) { + furi_string_set(temp_str, "Repeat: inf"); + } else if(app->meta->playlist_repetitions == 1) { + furi_string_set(temp_str, "Repeat: no"); + } else { + furi_string_printf(temp_str, "Repeat: %dx", app->meta->playlist_repetitions); + } + canvas_draw_str_aligned( + canvas, 1, 29, AlignLeft, AlignTop, furi_string_get_cstr(temp_str)); + } + + // draw buttons + draw_corner_aligned(canvas, 40, 15, AlignCenter, AlignBottom); + + canvas_set_color(canvas, ColorWhite); + canvas_draw_str_aligned(canvas, WIDTH / 2 - 7, HEIGHT - 11, AlignLeft, AlignTop, "Start"); + canvas_draw_disc(canvas, WIDTH / 2 - 14, HEIGHT - 8, 3); + + // + canvas_set_color(canvas, ColorBlack); + draw_corner_aligned(canvas, 20, 15, AlignLeft, AlignBottom); + + canvas_set_color(canvas, ColorWhite); + canvas_draw_str_aligned(canvas, 4, HEIGHT - 11, AlignLeft, AlignTop, "R-"); + + // + canvas_set_color(canvas, ColorBlack); + draw_corner_aligned(canvas, 20, 15, AlignRight, AlignBottom); + canvas_set_color(canvas, ColorWhite); + canvas_draw_str_aligned(canvas, WIDTH - 4, HEIGHT - 11, AlignRight, AlignTop, "R+"); + + canvas_set_color(canvas, ColorBlack); + + break; + case STATE_SENDING: + canvas_set_color(canvas, ColorBlack); + if(app->worker->ctl_pause) { + canvas_draw_icon(canvas, 2, HEIGHT - 8, &I_ButtonRight_4x7); + } else { + canvas_draw_box(canvas, 2, HEIGHT - 8, 2, 7); + canvas_draw_box(canvas, 5, HEIGHT - 8, 2, 7); + } + + // draw progress text + { + canvas_set_font(canvas, FontSecondary); + furi_string_printf( + temp_str, "[%d/%d]", app->meta->current_count, app->meta->total_count); + canvas_draw_str_aligned( + canvas, 11, HEIGHT - 8, AlignLeft, AlignTop, furi_string_get_cstr(temp_str)); + + int h = canvas_string_width(canvas, furi_string_get_cstr(temp_str)); + int xs = 11 + h + 2; + int w = WIDTH - xs - 1; + canvas_draw_box(canvas, xs, HEIGHT - 5, w, 1); + + float progress = (float)app->meta->current_count / (float)app->meta->total_count; + int wp = (int)(progress * w); + canvas_draw_box(canvas, xs + wp - 1, HEIGHT - 7, 2, 5); + } + + { + if(app->meta->playlist_repetitions <= 0) { + furi_string_printf(temp_str, "[%d/Inf]", app->meta->current_playlist_repetition); + } else { + furi_string_printf( + temp_str, + "[%d/%d]", + app->meta->current_playlist_repetition, + app->meta->playlist_repetitions); + } + canvas_set_color(canvas, ColorBlack); + int w = canvas_string_width(canvas, furi_string_get_cstr(temp_str)); + draw_corner_aligned(canvas, w + 6, 13, AlignRight, AlignTop); + canvas_set_color(canvas, ColorWhite); + canvas_draw_str_aligned( + canvas, WIDTH - 3, 3, AlignRight, AlignTop, furi_string_get_cstr(temp_str)); + } + + // draw last and current file + { + canvas_set_color(canvas, ColorBlack); + canvas_set_font(canvas, FontSecondary); + + // current + if(!furi_string_empty(app->meta->prev_0_path)) { + path_extract_filename(app->meta->prev_0_path, temp_str, true); + int w = canvas_string_width(canvas, furi_string_get_cstr(temp_str)); + canvas_set_color(canvas, ColorBlack); + canvas_draw_rbox(canvas, 1, 1, w + 4, 12, 2); + canvas_set_color(canvas, ColorWhite); + canvas_draw_str_aligned( + canvas, 3, 3, AlignLeft, AlignTop, furi_string_get_cstr(temp_str)); + } + + // last 3 + canvas_set_color(canvas, ColorBlack); + + if(!furi_string_empty(app->meta->prev_1_path)) { + path_extract_filename(app->meta->prev_1_path, temp_str, true); + canvas_draw_str_aligned( + canvas, 3, 15, AlignLeft, AlignTop, furi_string_get_cstr(temp_str)); + } + + if(!furi_string_empty(app->meta->prev_2_path)) { + path_extract_filename(app->meta->prev_2_path, temp_str, true); + canvas_draw_str_aligned( + canvas, 3, 26, AlignLeft, AlignTop, furi_string_get_cstr(temp_str)); + } + + if(!furi_string_empty(app->meta->prev_3_path)) { + path_extract_filename(app->meta->prev_3_path, temp_str, true); + canvas_draw_str_aligned( + canvas, 3, 37, AlignLeft, AlignTop, furi_string_get_cstr(temp_str)); + } + } + break; + default: + break; + } + + furi_string_free(temp_str); + furi_mutex_release(app->mutex); +} + +static void input_callback(InputEvent* event, void* ctx) { + Playlist* app = ctx; + furi_message_queue_put(app->input_queue, event, 0); +} + +//////////////////////////////////////////////////////////////////////////////// + +Playlist* playlist_alloc(DisplayMeta* meta) { + Playlist* app = malloc(sizeof(Playlist)); + app->file_path = furi_string_alloc(); + furi_string_set(app->file_path, PLAYLIST_FOLDER); + + app->meta = meta; + app->worker = NULL; + + app->mutex = furi_mutex_alloc(FuriMutexTypeNormal); + app->input_queue = furi_message_queue_alloc(32, sizeof(InputEvent)); + + // view port + app->view_port = view_port_alloc(); + view_port_draw_callback_set(app->view_port, render_callback, app); + view_port_input_callback_set(app->view_port, input_callback, app); + + // gui + app->gui = furi_record_open(RECORD_GUI); + gui_add_view_port(app->gui, app->view_port, GuiLayerFullscreen); + + return app; +} + +void playlist_start_worker(Playlist* app, DisplayMeta* meta) { + app->worker = playlist_worker_alloc(meta); + + // count playlist items + Storage* storage = furi_record_open(RECORD_STORAGE); + app->meta->total_count = + playlist_count_playlist_items(storage, furi_string_get_cstr(app->file_path)); + furi_record_close(RECORD_STORAGE); + + // start thread + playlist_worker_start(app->worker, furi_string_get_cstr(app->file_path)); +} + +void playlist_free(Playlist* app) { + furi_string_free(app->file_path); + + gui_remove_view_port(app->gui, app->view_port); + furi_record_close(RECORD_GUI); + view_port_free(app->view_port); + + furi_message_queue_free(app->input_queue); + furi_mutex_free(app->mutex); + + playlist_meta_free(app->meta); + + free(app); +} + +int32_t playlist_app(char* p) { + // create playlist folder + { + Storage* storage = furi_record_open(RECORD_STORAGE); + if(!storage_simply_mkdir(storage, PLAYLIST_FOLDER)) { + FURI_LOG_E(TAG, "Could not create folder %s", PLAYLIST_FOLDER); + } + furi_record_close(RECORD_STORAGE); + } + + // create app + DisplayMeta* meta = playlist_meta_alloc(); + Playlist* app = playlist_alloc(meta); + meta->view_port = app->view_port; + + furi_hal_power_suppress_charge_enter(); + + // select playlist file + if(p && strlen(p)) { + furi_string_set(app->file_path, p); + } else { + DialogsApp* dialogs = furi_record_open(RECORD_DIALOGS); + DialogsFileBrowserOptions browser_options; + dialog_file_browser_set_basic_options(&browser_options, PLAYLIST_EXT, &I_sub1_10px); + browser_options.base_path = PLAYLIST_FOLDER; + + const bool res = + dialog_file_browser_show(dialogs, app->file_path, app->file_path, &browser_options); + furi_record_close(RECORD_DIALOGS); + // check if a file was selected + if(!res) { + FURI_LOG_E(TAG, "No file selected"); + goto exit_cleanup; + } + } + + //////////////////////////////////////////////////////////////////////////////// + + playlist_start_worker(app, meta); + meta_set_state(app->meta, STATE_OVERVIEW); + + bool exit_loop = false; + InputEvent input; + while(1) { // close application if no file was selected + furi_check( + furi_message_queue_get(app->input_queue, &input, FuriWaitForever) == FuriStatusOk); + + switch(input.key) { + case InputKeyLeft: + if(app->meta->state == STATE_OVERVIEW) { + if(input.type == InputTypeShort && app->meta->playlist_repetitions > 0) { + --app->meta->playlist_repetitions; + } + } else if(app->meta->state == STATE_SENDING) { + if(input.type == InputTypeShort) { + app->worker->ctl_request_prev = true; + } + } + break; + + case InputKeyRight: + if(app->meta->state == STATE_OVERVIEW) { + if(input.type == InputTypeShort) { + ++app->meta->playlist_repetitions; + } + } else if(app->meta->state == STATE_SENDING) { + if(input.type == InputTypeShort) { + app->worker->ctl_request_skip = true; + } + } + break; + + case InputKeyOk: + if(input.type == InputTypeShort) { + // toggle pause state + if(!app->worker->is_running) { + app->worker->ctl_pause = false; + app->worker->ctl_request_exit = false; + playlist_worker_start(app->worker, furi_string_get_cstr(app->file_path)); + } else { + app->worker->ctl_pause = !app->worker->ctl_pause; + } + } + break; + case InputKeyBack: + FURI_LOG_D(TAG, "Pressed Back button. Application will exit"); + exit_loop = true; + break; + default: + break; + } + + furi_mutex_release(app->mutex); + + // exit application + if(exit_loop == true) { + break; + } + + view_port_update(app->view_port); + } + +exit_cleanup: + + furi_hal_power_suppress_charge_exit(); + + if(app->worker != NULL) { + if(playlist_worker_running(app->worker)) { + FURI_LOG_D(TAG, "Thread is still running. Requesting thread to finish ..."); + playlist_worker_stop(app->worker); + } + FURI_LOG_D(TAG, "Freeing Worker ..."); + playlist_worker_free(app->worker); + } + + FURI_LOG_D(TAG, "Freeing Playlist ..."); + playlist_free(app); + return 0; +} diff --git a/subghz_playlist/playlist_file.c b/subghz_playlist/playlist_file.c new file mode 100644 index 00000000000..e3540648ec5 --- /dev/null +++ b/subghz_playlist/playlist_file.c @@ -0,0 +1,21 @@ +#include + +#include +#include + +int playlist_count_playlist_items(Storage* storage, const char* file_path) { + FlipperFormat* format = flipper_format_file_alloc(storage); + if(!flipper_format_file_open_existing(format, file_path)) { + return -1; + } + int count = 0; + FuriString* data; + data = furi_string_alloc(); + while(flipper_format_read_string(format, "sub", data)) { + ++count; + } + flipper_format_file_close(format); + flipper_format_free(format); + furi_string_free(data); + return count; +} diff --git a/subghz_playlist/playlist_file.h b/subghz_playlist/playlist_file.h new file mode 100644 index 00000000000..6674040b4a9 --- /dev/null +++ b/subghz_playlist/playlist_file.h @@ -0,0 +1,9 @@ +#pragma once + +#include + +#include + +#define PLAYLIST_FOLDER EXT_PATH("subghz/playlist") + +int playlist_count_playlist_items(Storage* storage, const char* file_path); diff --git a/subghz_playlist/subplaylist_10px.png b/subghz_playlist/subplaylist_10px.png new file mode 100644 index 00000000000..3d3f1d27f64 Binary files /dev/null and b/subghz_playlist/subplaylist_10px.png differ diff --git a/subghz_remote/.gitignore b/subghz_remote/.gitignore new file mode 100644 index 00000000000..e2a15a10a84 --- /dev/null +++ b/subghz_remote/.gitignore @@ -0,0 +1,4 @@ +dist/* +.vscode +.clang-format +.editorconfig \ No newline at end of file diff --git a/subghz_remote/.gitsubtree b/subghz_remote/.gitsubtree new file mode 100644 index 00000000000..887425ae6a9 --- /dev/null +++ b/subghz_remote/.gitsubtree @@ -0,0 +1 @@ +https://github.com/DarkFlippers/SubGHz_Remote ufw_main_app / diff --git a/subghz_remote/LICENSE b/subghz_remote/LICENSE new file mode 100644 index 00000000000..6f55e3b0de2 --- /dev/null +++ b/subghz_remote/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 DarkFlippers @gid9798 @xMasterX + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/subghz_remote/application.fam b/subghz_remote/application.fam new file mode 100644 index 00000000000..63eff8c190f --- /dev/null +++ b/subghz_remote/application.fam @@ -0,0 +1,41 @@ +# App( +# appid="subghz_remote_ofw", +# name="Sub-GHz Remote", +# apptype=FlipperAppType.EXTERNAL, +# entry_point="subghz_remote_app", +# requires=[ +# "gui", +# "dialogs", +# ], +# stack_size=2 * 1024, +# targets=["f7"], +# fap_icon="icon.png", +# fap_author="gid9798 xMasterX", +# fap_description="SubGhz Remote, uses up to 5 .sub files", +# fap_category="Sub-GHz", +# fap_icon_assets="icons", +# fap_icon_assets_symbol="subghz_remote", +# fap_version="1.2", +# fap_weburl="https://github.com/DarkFlippers/SubGHz_Remote", +# ) + +App( + appid="subghz_remote", + name="Sub-GHz Remote", + apptype=FlipperAppType.EXTERNAL, + entry_point="subghz_remote_app", + requires=[ + "gui", + "dialogs", + ], + stack_size=2 * 1024, + targets=["f7"], + fap_icon="icon.png", + fap_author="gid9798 xMasterX", + fap_description="SubGhz Remote, uses up to 5 .sub files", + fap_category="Sub-GHz", + fap_icon_assets="icons", + fap_icon_assets_symbol="subghz_remote", + fap_version="1.2", + fap_weburl="https://github.com/DarkFlippers/SubGHz_Remote", +) diff --git a/subghz_remote/catalog/docs/Changelog.md b/subghz_remote/catalog/docs/Changelog.md new file mode 100644 index 00000000000..31ed245e8d7 --- /dev/null +++ b/subghz_remote/catalog/docs/Changelog.md @@ -0,0 +1,25 @@ +## v1.2 +- **Official FirmWare Support** +- Add warning screen on CustomFW + - The .sub file format may differ from the official one and may be broken + +## v1.1 +- **Was combined with a configuration plugin** + - Editing/Creating map file +- Support for starting arguments + +## v1.0 + +**Initial implementation:** +- Transmission +- GUI +- All .sub files for which transfer is available are supported +- Signal types: + - Static + - Dynamic + - RAW + - BinRAW + +*Custom modulations are not supported yet* + +**Map File Format** - FlipperFormat .txt file \ No newline at end of file diff --git a/subghz_remote/catalog/docs/Readme.md b/subghz_remote/catalog/docs/Readme.md new file mode 100644 index 00000000000..1da170e19b0 --- /dev/null +++ b/subghz_remote/catalog/docs/Readme.md @@ -0,0 +1,24 @@ +With this application, you can combine up to 5 .sub files into one remote, and use flipper as a remote with multiple buttons. +## What is "Map" Files? +"Map" is short for mapping +A Map Files is a .txt files that the application uses to store information about remotes +# How to use +## First screen +After launching the application, you will see the MAP file selection screen (file browser). +- Select map file or press "back" to go Main menu +## Main menu +- Open map file - switching to remote + - Select map file + - On remote screen, use the navigation buttons(D-pad) to send a signal +- Edit Map File - map file editor + - Select map file + - Up/Down - slot nafigation + - Ok - edit menu + - Left - preview/save +- New Map File - Creating a new map file + - Enter a name + - The rest is similar to map file editor +# About map file +Map file - FlipperFormat .txt file. + +Stores custom names, and paths to used .sub files. \ No newline at end of file diff --git a/subghz_remote/catalog/screenshots/Editor_main.png b/subghz_remote/catalog/screenshots/Editor_main.png new file mode 100644 index 00000000000..9498c945144 Binary files /dev/null and b/subghz_remote/catalog/screenshots/Editor_main.png differ diff --git a/subghz_remote/catalog/screenshots/Editor_submenu.png b/subghz_remote/catalog/screenshots/Editor_submenu.png new file mode 100644 index 00000000000..f5f7cd8495a Binary files /dev/null and b/subghz_remote/catalog/screenshots/Editor_submenu.png differ diff --git a/subghz_remote/catalog/screenshots/Remote_idle.png b/subghz_remote/catalog/screenshots/Remote_idle.png new file mode 100644 index 00000000000..eb91fff5815 Binary files /dev/null and b/subghz_remote/catalog/screenshots/Remote_idle.png differ diff --git a/subghz_remote/catalog/screenshots/Remote_send.png b/subghz_remote/catalog/screenshots/Remote_send.png new file mode 100644 index 00000000000..000cff3b33a Binary files /dev/null and b/subghz_remote/catalog/screenshots/Remote_send.png differ diff --git a/subghz_remote/helpers/subrem_custom_event.h b/subghz_remote/helpers/subrem_custom_event.h new file mode 100644 index 00000000000..810df6a89fa --- /dev/null +++ b/subghz_remote/helpers/subrem_custom_event.h @@ -0,0 +1,58 @@ +#pragma once + +typedef enum { + SubRemEditMenuStateUP = 0, + SubRemEditMenuStateDOWN, + SubRemEditMenuStateLEFT, + SubRemEditMenuStateRIGHT, + SubRemEditMenuStateOK, +} SubRemEditMenuState; + +typedef enum { + // StartSubmenuIndex + SubmenuIndexSubRemOpenMapFile = 0, + SubmenuIndexSubRemEditMapFile, + SubmenuIndexSubRemNewMapFile, +#if FURI_DEBUG + SubmenuIndexSubRemRemoteView, +#endif + // SubmenuIndexSubRemAbout, + + // EditSubmenuIndex + EditSubmenuIndexEditLabel, + EditSubmenuIndexEditFile, + + // SubRemCustomEvent + SubRemCustomEventViewRemoteStartUP = 100, + SubRemCustomEventViewRemoteStartDOWN, + SubRemCustomEventViewRemoteStartLEFT, + SubRemCustomEventViewRemoteStartRIGHT, + SubRemCustomEventViewRemoteStartOK, + SubRemCustomEventViewRemoteBack, + SubRemCustomEventViewRemoteStop, + SubRemCustomEventViewRemoteForcedStop, + + SubRemCustomEventViewEditMenuBack, + SubRemCustomEventViewEditMenuUP, + SubRemCustomEventViewEditMenuDOWN, + SubRemCustomEventViewEditMenuEdit, + SubRemCustomEventViewEditMenuSave, + + SubRemCustomEventSceneEditsubmenu, + SubRemCustomEventSceneEditLabelInputDone, + SubRemCustomEventSceneEditLabelWidgetAcces, + SubRemCustomEventSceneEditLabelWidgetBack, + + SubRemCustomEventSceneEditOpenSubErrorPopup, + + SubRemCustomEventSceneEditPreviewSaved, + + SubRemCustomEventSceneNewName, + +#ifdef FW_ORIGIN_Official + SubRemCustomEventSceneFwWarningExit, + SubRemCustomEventSceneFwWarningNext, + SubRemCustomEventSceneFwWarningContinue, +#endif + +} SubRemCustomEvent; \ No newline at end of file diff --git a/subghz_remote/helpers/subrem_presets.c b/subghz_remote/helpers/subrem_presets.c new file mode 100644 index 00000000000..2c759fe1260 --- /dev/null +++ b/subghz_remote/helpers/subrem_presets.c @@ -0,0 +1,192 @@ +#include "subrem_presets.h" + +#define TAG "SubRemPresets" + +SubRemSubFilePreset* subrem_sub_file_preset_alloc(void) { + SubRemSubFilePreset* sub_preset = malloc(sizeof(SubRemSubFilePreset)); + + sub_preset->fff_data = flipper_format_string_alloc(); + sub_preset->file_path = furi_string_alloc(); + sub_preset->protocaol_name = furi_string_alloc(); + sub_preset->label = furi_string_alloc(); + + sub_preset->freq_preset.name = furi_string_alloc(); + + sub_preset->type = SubGhzProtocolTypeUnknown; + sub_preset->load_state = SubRemLoadSubStateNotSet; + + return sub_preset; +} + +void subrem_sub_file_preset_free(SubRemSubFilePreset* sub_preset) { + furi_assert(sub_preset); + + furi_string_free(sub_preset->label); + furi_string_free(sub_preset->protocaol_name); + furi_string_free(sub_preset->file_path); + flipper_format_free(sub_preset->fff_data); + + furi_string_free(sub_preset->freq_preset.name); + + free(sub_preset); +} + +void subrem_sub_file_preset_reset(SubRemSubFilePreset* sub_preset) { + furi_assert(sub_preset); + + furi_string_set_str(sub_preset->label, ""); + furi_string_reset(sub_preset->protocaol_name); + furi_string_reset(sub_preset->file_path); + + Stream* fff_data_stream = flipper_format_get_raw_stream(sub_preset->fff_data); + stream_clean(fff_data_stream); + + sub_preset->type = SubGhzProtocolTypeUnknown; + sub_preset->load_state = SubRemLoadSubStateNotSet; +} + +SubRemLoadSubState subrem_sub_preset_load( + SubRemSubFilePreset* sub_preset, + SubGhzTxRx* txrx, + FlipperFormat* fff_data_file) { + furi_assert(sub_preset); + furi_assert(txrx); + furi_assert(fff_data_file); + + Stream* fff_data_stream = flipper_format_get_raw_stream(sub_preset->fff_data); + + SubRemLoadSubState ret; + FuriString* temp_str = furi_string_alloc(); + uint32_t temp_data32; + uint32_t repeat = 200; + + ret = SubRemLoadSubStateError; + + do { + stream_clean(fff_data_stream); + if(!flipper_format_read_header(fff_data_file, temp_str, &temp_data32)) { + FURI_LOG_E(TAG, "Missing or incorrect header"); + break; + } + + if(((!strcmp(furi_string_get_cstr(temp_str), SUBGHZ_KEY_FILE_TYPE)) || + (!strcmp(furi_string_get_cstr(temp_str), SUBGHZ_RAW_FILE_TYPE))) && + temp_data32 == SUBGHZ_KEY_FILE_VERSION) { + } else { + FURI_LOG_E(TAG, "Type or version mismatch"); + break; + } + + SubGhzSetting* setting = subghz_txrx_get_setting(txrx); + + //Load frequency or using default from settings + ret = SubRemLoadSubStateErrorFreq; + if(!flipper_format_read_uint32(fff_data_file, "Frequency", &temp_data32, 1)) { + FURI_LOG_W(TAG, "Cannot read frequency. Set default frequency"); + sub_preset->freq_preset.frequency = subghz_setting_get_default_frequency(setting); + } else if(!subghz_txrx_radio_device_is_frequency_valid(txrx, temp_data32)) { + FURI_LOG_E(TAG, "Frequency not supported on chosen radio module"); + break; + } + sub_preset->freq_preset.frequency = temp_data32; + + //Load preset + ret = SubRemLoadSubStateErrorMod; + if(!flipper_format_read_string(fff_data_file, "Preset", temp_str)) { + FURI_LOG_E(TAG, "Missing Preset"); + break; + } + + furi_string_set_str( + temp_str, subghz_txrx_get_preset_name(txrx, furi_string_get_cstr(temp_str))); + if(!strcmp(furi_string_get_cstr(temp_str), "")) { + break; + } + + if(!strcmp(furi_string_get_cstr(temp_str), "CUSTOM")) { + //TODO Does this work properly? + //delete preset if it already exists + subghz_setting_delete_custom_preset(setting, furi_string_get_cstr(temp_str)); + //load custom preset from file + if(!subghz_setting_load_custom_preset( + setting, furi_string_get_cstr(temp_str), fff_data_file)) { + FURI_LOG_E(TAG, "Missing Custom preset"); + break; + } + // FURI_LOG_E(TAG, "CUSTOM preset is not supported"); + // break; + // TODO Custom preset loading logic if need + // sub_preset->freq_preset.preset_index = + // subghz_setting_get_inx_preset_by_name(setting, furi_string_get_cstr(temp_str)); + } + + furi_string_set(sub_preset->freq_preset.name, temp_str); + + // Load protocol + ret = SubRemLoadSubStateErrorProtocol; + if(!flipper_format_read_string(fff_data_file, "Protocol", temp_str)) { + FURI_LOG_E(TAG, "Missing Protocol"); + break; + } + + FlipperFormat* fff_data = sub_preset->fff_data; + if(!strcmp(furi_string_get_cstr(temp_str), "RAW")) { + //if RAW + subghz_protocol_raw_gen_fff_data( + fff_data, + furi_string_get_cstr(sub_preset->file_path), + subghz_txrx_radio_device_get_name(txrx)); + } else { + stream_copy_full( + flipper_format_get_raw_stream(fff_data_file), + flipper_format_get_raw_stream(fff_data)); + } + + if(subghz_txrx_load_decoder_by_name_protocol(txrx, furi_string_get_cstr(temp_str))) { + SubGhzProtocolStatus status = + subghz_protocol_decoder_base_deserialize(subghz_txrx_get_decoder(txrx), fff_data); + if(status != SubGhzProtocolStatusOk) { + break; + } + } else { + FURI_LOG_E(TAG, "Protocol not found"); + break; + } + + const SubGhzProtocol* protocol = subghz_txrx_get_decoder(txrx)->protocol; + + if(protocol->flag & SubGhzProtocolFlag_Send) { + if((protocol->type == SubGhzProtocolTypeStatic) || + (protocol->type == SubGhzProtocolTypeDynamic) || +#ifndef FW_ORIGIN_Official + (protocol->type == SubGhzProtocolTypeBinRAW) || +#endif + (protocol->type == SubGhzProtocolTypeRAW)) { + sub_preset->type = protocol->type; + } else { + FURI_LOG_E(TAG, "Unsuported Protocol"); + break; + } + + furi_string_set(sub_preset->protocaol_name, temp_str); + } else { + FURI_LOG_E(TAG, "Protocol does not support transmission"); + break; + } + + if(!flipper_format_insert_or_update_uint32(fff_data, "Repeat", &repeat, 1)) { + FURI_LOG_E(TAG, "Unable Repeat"); + break; + } + + ret = SubRemLoadSubStateOK; + +#if FURI_DEBUG + FURI_LOG_I(TAG, "%-16s - protocol Loaded", furi_string_get_cstr(sub_preset->label)); +#endif + } while(false); + + furi_string_free(temp_str); + sub_preset->load_state = ret; + return ret; +} diff --git a/subghz_remote/helpers/subrem_presets.h b/subghz_remote/helpers/subrem_presets.h new file mode 100644 index 00000000000..b9f9e7cbbb6 --- /dev/null +++ b/subghz_remote/helpers/subrem_presets.h @@ -0,0 +1,39 @@ +#pragma once + +#include "subrem_types.h" +#include "txrx/subghz_txrx.h" + +#include +#include + +typedef struct { + FuriString* name; + uint32_t frequency; + // size_t preset_index; // Need for custom preset +} FreqPreset; + +// Sub File preset +typedef struct { + FlipperFormat* fff_data; + FreqPreset freq_preset; + FuriString* file_path; + FuriString* protocaol_name; + FuriString* label; + SubGhzProtocolType type; + SubRemLoadSubState load_state; +} SubRemSubFilePreset; + +typedef struct { + SubRemSubFilePreset* subs_preset[SubRemSubKeyNameMaxCount]; +} SubRemMapPreset; + +SubRemSubFilePreset* subrem_sub_file_preset_alloc(void); + +void subrem_sub_file_preset_free(SubRemSubFilePreset* sub_preset); + +void subrem_sub_file_preset_reset(SubRemSubFilePreset* sub_preset); + +SubRemLoadSubState subrem_sub_preset_load( + SubRemSubFilePreset* sub_preset, + SubGhzTxRx* txrx, + FlipperFormat* fff_data_file); diff --git a/subghz_remote/helpers/subrem_types.h b/subghz_remote/helpers/subrem_types.h new file mode 100644 index 00000000000..b43f8499d84 --- /dev/null +++ b/subghz_remote/helpers/subrem_types.h @@ -0,0 +1,48 @@ +#pragma once + +#include +#include + +#define SUBREM_APP_APP_FILE_VERSION 1 +#define SUBREM_APP_APP_FILE_TYPE "Flipper SubRem Map file" +#define SUBREM_APP_EXTENSION ".txt" + +typedef enum { + SubRemSubKeyNameUp = (0U), + SubRemSubKeyNameDown, + SubRemSubKeyNameLeft, + SubRemSubKeyNameRight, + SubRemSubKeyNameOk, + SubRemSubKeyNameMaxCount, +} SubRemSubKeyName; + +typedef enum { + SubRemViewIDSubmenu, + SubRemViewIDWidget, + SubRemViewIDPopup, + SubRemViewIDTextInput, + SubRemViewIDRemote, + SubRemViewIDEditMenu, +} SubRemViewID; + +typedef enum { + SubRemLoadSubStateNotSet = 0, + SubRemLoadSubStatePreloaded, + SubRemLoadSubStateError, + SubRemLoadSubStateErrorIncorectPath, + SubRemLoadSubStateErrorNoFile, + SubRemLoadSubStateErrorFreq, + SubRemLoadSubStateErrorMod, + SubRemLoadSubStateErrorProtocol, + SubRemLoadSubStateOK, +} SubRemLoadSubState; + +typedef enum { + SubRemLoadMapStateBack = 0, + SubRemLoadMapStateError, + SubRemLoadMapStateErrorOpenError, + SubRemLoadMapStateErrorStorage, + SubRemLoadMapStateErrorBrokenFile, + SubRemLoadMapStateNotAllOK, + SubRemLoadMapStateOK, +} SubRemLoadMapState; \ No newline at end of file diff --git a/subghz_remote/helpers/txrx/Readme.md b/subghz_remote/helpers/txrx/Readme.md new file mode 100644 index 00000000000..9181601980a --- /dev/null +++ b/subghz_remote/helpers/txrx/Readme.md @@ -0,0 +1,4 @@ +This is part of the official `SubGhz` app from [flipperzero-firmware](https://github.com/flipperdevices/flipperzero-firmware/tree/3217f286f03da119398586daf94c0723d28b872a/applications/main/subghz) + +With changes from [unleashed-firmware +](https://github.com/DarkFlippers/unleashed-firmware/tree/3eac6ccd48a3851cf5d63bf7899b387a293e5319/applications/main/subghz) \ No newline at end of file diff --git a/subghz_remote/helpers/txrx/subghz_txrx.c b/subghz_remote/helpers/txrx/subghz_txrx.c new file mode 100644 index 00000000000..a9b38284713 --- /dev/null +++ b/subghz_remote/helpers/txrx/subghz_txrx.c @@ -0,0 +1,672 @@ +#include "subghz_txrx_i.h" + +#include +#include +#include + +#ifndef FW_ORIGIN_Official +#include +#endif + +#define TAG "SubGhz" + +static void subghz_txrx_radio_device_power_on(SubGhzTxRx* instance) { + UNUSED(instance); + uint8_t attempts = 0; + while(!furi_hal_power_is_otg_enabled() && attempts++ < 5) { + furi_hal_power_enable_otg(); + //CC1101 power-up time + furi_delay_ms(10); + } +} + +static void subghz_txrx_radio_device_power_off(SubGhzTxRx* instance) { + UNUSED(instance); + if(furi_hal_power_is_otg_enabled()) furi_hal_power_disable_otg(); +} + +SubGhzTxRx* subghz_txrx_alloc(void) { + SubGhzTxRx* instance = malloc(sizeof(SubGhzTxRx)); + instance->setting = subghz_setting_alloc(); + subghz_setting_load(instance->setting, EXT_PATH("subghz/assets/setting_user")); + + instance->preset = malloc(sizeof(SubGhzRadioPreset)); + instance->preset->name = furi_string_alloc(); + subghz_txrx_set_preset( + instance, "AM650", subghz_setting_get_default_frequency(instance->setting), NULL, 0); + + instance->txrx_state = SubGhzTxRxStateSleep; + + subghz_txrx_hopper_set_state(instance, SubGhzHopperStateOFF); + subghz_txrx_speaker_set_state(instance, SubGhzSpeakerStateDisable); + subghz_txrx_set_debug_pin_state(instance, false); + + instance->worker = subghz_worker_alloc(); + instance->fff_data = flipper_format_string_alloc(); + + instance->environment = subghz_environment_alloc(); + instance->is_database_loaded = + subghz_environment_load_keystore(instance->environment, SUBGHZ_KEYSTORE_DIR_NAME); + subghz_environment_load_keystore(instance->environment, SUBGHZ_KEYSTORE_DIR_USER_NAME); + subghz_environment_set_alutech_at_4n_rainbow_table_file_name( + instance->environment, SUBGHZ_ALUTECH_AT_4N_DIR_NAME); + subghz_environment_set_nice_flor_s_rainbow_table_file_name( + instance->environment, SUBGHZ_NICE_FLOR_S_DIR_NAME); + subghz_environment_set_protocol_registry( + instance->environment, (void*)&subghz_protocol_registry); + instance->receiver = subghz_receiver_alloc_init(instance->environment); + + subghz_worker_set_overrun_callback( + instance->worker, (SubGhzWorkerOverrunCallback)subghz_receiver_reset); + subghz_worker_set_pair_callback( + instance->worker, (SubGhzWorkerPairCallback)subghz_receiver_decode); + subghz_worker_set_context(instance->worker, instance->receiver); + + //set default device Internal + subghz_devices_init(); + instance->radio_device_type = SubGhzRadioDeviceTypeInternal; + instance->radio_device_type = + subghz_txrx_radio_device_set(instance, SubGhzRadioDeviceTypeExternalCC1101); + + return instance; +} + +void subghz_txrx_free(SubGhzTxRx* instance) { + furi_assert(instance); + + if(instance->radio_device_type != SubGhzRadioDeviceTypeInternal) { + subghz_txrx_radio_device_power_off(instance); + subghz_devices_end(instance->radio_device); + } + + subghz_devices_deinit(); + + subghz_worker_free(instance->worker); + subghz_receiver_free(instance->receiver); + subghz_environment_free(instance->environment); + flipper_format_free(instance->fff_data); + furi_string_free(instance->preset->name); + subghz_setting_free(instance->setting); + + free(instance->preset); + free(instance); +} + +bool subghz_txrx_is_database_loaded(SubGhzTxRx* instance) { + furi_assert(instance); + return instance->is_database_loaded; +} + +void subghz_txrx_set_preset( + SubGhzTxRx* instance, + const char* preset_name, + uint32_t frequency, + uint8_t* preset_data, + size_t preset_data_size) { + furi_assert(instance); + furi_string_set(instance->preset->name, preset_name); + SubGhzRadioPreset* preset = instance->preset; + preset->frequency = frequency; + preset->data = preset_data; + preset->data_size = preset_data_size; +} + +const char* subghz_txrx_get_preset_name(SubGhzTxRx* instance, const char* preset) { + UNUSED(instance); + const char* preset_name = ""; + if(!strcmp(preset, "FuriHalSubGhzPresetOok270Async")) { + preset_name = "AM270"; + } else if(!strcmp(preset, "FuriHalSubGhzPresetOok650Async")) { + preset_name = "AM650"; + } else if(!strcmp(preset, "FuriHalSubGhzPreset2FSKDev238Async")) { + preset_name = "FM238"; + } else if(!strcmp(preset, "FuriHalSubGhzPreset2FSKDev476Async")) { + preset_name = "FM476"; + } else if(!strcmp(preset, "FuriHalSubGhzPresetCustom")) { + preset_name = "CUSTOM"; + } else { + FURI_LOG_E(TAG, "Unknown preset"); + } + return preset_name; +} + +SubGhzRadioPreset subghz_txrx_get_preset(SubGhzTxRx* instance) { + furi_assert(instance); + return *instance->preset; +} + +void subghz_txrx_get_frequency_and_modulation( + SubGhzTxRx* instance, + FuriString* frequency, + FuriString* modulation, + bool long_name) { + furi_assert(instance); + SubGhzRadioPreset* preset = instance->preset; + if(frequency != NULL) { + furi_string_printf( + frequency, + "%03ld.%02ld", + preset->frequency / 1000000 % 1000, + preset->frequency / 10000 % 100); + } + if(modulation != NULL) { + if(long_name) { + furi_string_printf(modulation, "%s", furi_string_get_cstr(preset->name)); + } else { + furi_string_printf(modulation, "%.2s", furi_string_get_cstr(preset->name)); + } + } +} + +static void subghz_txrx_begin(SubGhzTxRx* instance, uint8_t* preset_data) { + furi_assert(instance); + subghz_devices_reset(instance->radio_device); + subghz_devices_idle(instance->radio_device); + subghz_devices_load_preset(instance->radio_device, FuriHalSubGhzPresetCustom, preset_data); + instance->txrx_state = SubGhzTxRxStateIDLE; +} + +static uint32_t subghz_txrx_rx(SubGhzTxRx* instance, uint32_t frequency) { + furi_assert(instance); + furi_assert( + instance->txrx_state != SubGhzTxRxStateRx && instance->txrx_state != SubGhzTxRxStateSleep); + + subghz_devices_idle(instance->radio_device); + + uint32_t value = subghz_devices_set_frequency(instance->radio_device, frequency); + subghz_devices_flush_rx(instance->radio_device); + subghz_txrx_speaker_on(instance); + + subghz_devices_start_async_rx( + instance->radio_device, subghz_worker_rx_callback, instance->worker); + subghz_worker_start(instance->worker); + instance->txrx_state = SubGhzTxRxStateRx; + return value; +} + +static void subghz_txrx_idle(SubGhzTxRx* instance) { + furi_assert(instance); + furi_assert(instance->txrx_state != SubGhzTxRxStateSleep); + subghz_devices_idle(instance->radio_device); + subghz_txrx_speaker_off(instance); + instance->txrx_state = SubGhzTxRxStateIDLE; +} + +static void subghz_txrx_rx_end(SubGhzTxRx* instance) { + furi_assert(instance); + furi_assert(instance->txrx_state == SubGhzTxRxStateRx); + + if(subghz_worker_is_running(instance->worker)) { + subghz_worker_stop(instance->worker); + subghz_devices_stop_async_rx(instance->radio_device); + } + subghz_devices_idle(instance->radio_device); + subghz_txrx_speaker_off(instance); + instance->txrx_state = SubGhzTxRxStateIDLE; +} + +void subghz_txrx_sleep(SubGhzTxRx* instance) { + furi_assert(instance); + subghz_devices_sleep(instance->radio_device); + instance->txrx_state = SubGhzTxRxStateSleep; +} + +static bool subghz_txrx_tx(SubGhzTxRx* instance, uint32_t frequency) { + furi_assert(instance); + furi_assert(instance->txrx_state != SubGhzTxRxStateSleep); + + subghz_devices_idle(instance->radio_device); + subghz_devices_set_frequency(instance->radio_device, frequency); + + bool ret = subghz_devices_set_tx(instance->radio_device); + if(ret) { + subghz_txrx_speaker_on(instance); + instance->txrx_state = SubGhzTxRxStateTx; + } + + return ret; +} + +SubGhzTxRxStartTxState subghz_txrx_tx_start(SubGhzTxRx* instance, FlipperFormat* flipper_format) { + furi_assert(instance); + furi_assert(flipper_format); + + subghz_txrx_stop(instance); + + SubGhzTxRxStartTxState ret = SubGhzTxRxStartTxStateErrorParserOthers; + FuriString* temp_str = furi_string_alloc(); + uint32_t repeat = 200; + do { + if(!flipper_format_rewind(flipper_format)) { + FURI_LOG_E(TAG, "Rewind error"); + break; + } + if(!flipper_format_read_string(flipper_format, "Protocol", temp_str)) { + FURI_LOG_E(TAG, "Missing Protocol"); + break; + } + if(!flipper_format_insert_or_update_uint32(flipper_format, "Repeat", &repeat, 1)) { + FURI_LOG_E(TAG, "Unable Repeat"); + break; + } + ret = SubGhzTxRxStartTxStateOk; + + SubGhzRadioPreset* preset = instance->preset; + instance->transmitter = + subghz_transmitter_alloc_init(instance->environment, furi_string_get_cstr(temp_str)); + + if(instance->transmitter) { + if(subghz_transmitter_deserialize(instance->transmitter, flipper_format) == + SubGhzProtocolStatusOk) { + if(strcmp(furi_string_get_cstr(preset->name), "") != 0) { + subghz_txrx_begin( + instance, + subghz_setting_get_preset_data_by_name( + instance->setting, furi_string_get_cstr(preset->name))); + if(preset->frequency) { + if(!subghz_txrx_tx(instance, preset->frequency)) { + FURI_LOG_E(TAG, "Only Rx"); + ret = SubGhzTxRxStartTxStateErrorOnlyRx; + } + } else { + ret = SubGhzTxRxStartTxStateErrorParserOthers; + } + + } else { + FURI_LOG_E( + TAG, "Unknown name preset \" %s \"", furi_string_get_cstr(preset->name)); + ret = SubGhzTxRxStartTxStateErrorParserOthers; + } + + if(ret == SubGhzTxRxStartTxStateOk) { + //Start TX + subghz_devices_start_async_tx( + instance->radio_device, subghz_transmitter_yield, instance->transmitter); + } + } else { + ret = SubGhzTxRxStartTxStateErrorParserOthers; + } + } else { + ret = SubGhzTxRxStartTxStateErrorParserOthers; + } + if(ret != SubGhzTxRxStartTxStateOk) { + subghz_transmitter_free(instance->transmitter); + if(instance->txrx_state != SubGhzTxRxStateIDLE) { + subghz_txrx_idle(instance); + } + } + + } while(false); + furi_string_free(temp_str); + return ret; +} + +void subghz_txrx_rx_start(SubGhzTxRx* instance) { + furi_assert(instance); + subghz_txrx_stop(instance); + subghz_txrx_begin( + instance, + subghz_setting_get_preset_data_by_name( + subghz_txrx_get_setting(instance), furi_string_get_cstr(instance->preset->name))); + subghz_txrx_rx(instance, instance->preset->frequency); +} + +void subghz_txrx_set_need_save_callback( + SubGhzTxRx* instance, + SubGhzTxRxNeedSaveCallback callback, + void* context) { + furi_assert(instance); + instance->need_save_callback = callback; + instance->need_save_context = context; +} + +static void subghz_txrx_tx_stop(SubGhzTxRx* instance) { + furi_assert(instance); + furi_assert(instance->txrx_state == SubGhzTxRxStateTx); + //Stop TX + subghz_devices_stop_async_tx(instance->radio_device); + subghz_transmitter_stop(instance->transmitter); + subghz_transmitter_free(instance->transmitter); + + //if protocol dynamic then we save the last upload + if(instance->decoder_result->protocol->type == SubGhzProtocolTypeDynamic) { + if(instance->need_save_callback) { + instance->need_save_callback(instance->need_save_context); + } + } + subghz_txrx_idle(instance); + subghz_txrx_speaker_off(instance); + //Todo: Show message +} + +FlipperFormat* subghz_txrx_get_fff_data(SubGhzTxRx* instance) { + furi_assert(instance); + return instance->fff_data; +} + +SubGhzSetting* subghz_txrx_get_setting(SubGhzTxRx* instance) { + furi_assert(instance); + return instance->setting; +} + +void subghz_txrx_stop(SubGhzTxRx* instance) { + furi_assert(instance); + + switch(instance->txrx_state) { + case SubGhzTxRxStateTx: + subghz_txrx_tx_stop(instance); + subghz_txrx_speaker_unmute(instance); + break; + case SubGhzTxRxStateRx: + subghz_txrx_rx_end(instance); + subghz_txrx_speaker_mute(instance); + break; + + default: + break; + } +} + +void subghz_txrx_hopper_update(SubGhzTxRx* instance) { + furi_assert(instance); + + switch(instance->hopper_state) { + case SubGhzHopperStateOFF: + case SubGhzHopperStatePause: + return; + case SubGhzHopperStateRSSITimeOut: + if(instance->hopper_timeout != 0) { + instance->hopper_timeout--; + return; + } + break; + default: + break; + } + float rssi = -127.0f; + if(instance->hopper_state != SubGhzHopperStateRSSITimeOut) { + // See RSSI Calculation timings in CC1101 17.3 RSSI + rssi = subghz_devices_get_rssi(instance->radio_device); + + // Stay if RSSI is high enough + if(rssi > -90.0f) { + instance->hopper_timeout = 10; + instance->hopper_state = SubGhzHopperStateRSSITimeOut; + return; + } + } else { + instance->hopper_state = SubGhzHopperStateRunning; + } + // Select next frequency + if(instance->hopper_idx_frequency < + subghz_setting_get_hopper_frequency_count(instance->setting) - 1) { + instance->hopper_idx_frequency++; + } else { + instance->hopper_idx_frequency = 0; + } + + if(instance->txrx_state == SubGhzTxRxStateRx) { + subghz_txrx_rx_end(instance); + }; + if(instance->txrx_state == SubGhzTxRxStateIDLE) { + subghz_receiver_reset(instance->receiver); + instance->preset->frequency = + subghz_setting_get_hopper_frequency(instance->setting, instance->hopper_idx_frequency); + subghz_txrx_rx(instance, instance->preset->frequency); + } +} + +SubGhzHopperState subghz_txrx_hopper_get_state(SubGhzTxRx* instance) { + furi_assert(instance); + return instance->hopper_state; +} + +void subghz_txrx_hopper_set_state(SubGhzTxRx* instance, SubGhzHopperState state) { + furi_assert(instance); + instance->hopper_state = state; +} + +void subghz_txrx_hopper_unpause(SubGhzTxRx* instance) { + furi_assert(instance); + if(instance->hopper_state == SubGhzHopperStatePause) { + instance->hopper_state = SubGhzHopperStateRunning; + } +} + +void subghz_txrx_hopper_pause(SubGhzTxRx* instance) { + furi_assert(instance); + if(instance->hopper_state == SubGhzHopperStateRunning) { + instance->hopper_state = SubGhzHopperStatePause; + } +} + +void subghz_txrx_speaker_on(SubGhzTxRx* instance) { + furi_assert(instance); + if(instance->debug_pin_state) { + subghz_devices_set_async_mirror_pin(instance->radio_device, &gpio_ibutton); + } + + if(instance->speaker_state == SubGhzSpeakerStateEnable) { + if(furi_hal_speaker_acquire(30)) { + if(!instance->debug_pin_state) { + subghz_devices_set_async_mirror_pin(instance->radio_device, &gpio_speaker); + } + } else { + instance->speaker_state = SubGhzSpeakerStateDisable; + } + } +} + +void subghz_txrx_speaker_off(SubGhzTxRx* instance) { + furi_assert(instance); + if(instance->debug_pin_state) { + subghz_devices_set_async_mirror_pin(instance->radio_device, NULL); + } + if(instance->speaker_state != SubGhzSpeakerStateDisable) { + if(furi_hal_speaker_is_mine()) { + if(!instance->debug_pin_state) { + subghz_devices_set_async_mirror_pin(instance->radio_device, NULL); + } + furi_hal_speaker_release(); + if(instance->speaker_state == SubGhzSpeakerStateShutdown) + instance->speaker_state = SubGhzSpeakerStateDisable; + } + } +} + +void subghz_txrx_speaker_mute(SubGhzTxRx* instance) { + furi_assert(instance); + if(instance->debug_pin_state) { + subghz_devices_set_async_mirror_pin(instance->radio_device, NULL); + } + if(instance->speaker_state == SubGhzSpeakerStateEnable) { + if(furi_hal_speaker_is_mine()) { + if(!instance->debug_pin_state) { + subghz_devices_set_async_mirror_pin(instance->radio_device, NULL); + } + } + } +} + +void subghz_txrx_speaker_unmute(SubGhzTxRx* instance) { + furi_assert(instance); + if(instance->debug_pin_state) { + subghz_devices_set_async_mirror_pin(instance->radio_device, &gpio_ibutton); + } + if(instance->speaker_state == SubGhzSpeakerStateEnable) { + if(furi_hal_speaker_is_mine()) { + if(!instance->debug_pin_state) { + subghz_devices_set_async_mirror_pin(instance->radio_device, &gpio_speaker); + } + } + } +} + +void subghz_txrx_speaker_set_state(SubGhzTxRx* instance, SubGhzSpeakerState state) { + furi_assert(instance); + instance->speaker_state = state; +} + +SubGhzSpeakerState subghz_txrx_speaker_get_state(SubGhzTxRx* instance) { + furi_assert(instance); + return instance->speaker_state; +} + +bool subghz_txrx_load_decoder_by_name_protocol(SubGhzTxRx* instance, const char* name_protocol) { + furi_assert(instance); + furi_assert(name_protocol); + bool res = false; + instance->decoder_result = + subghz_receiver_search_decoder_base_by_name(instance->receiver, name_protocol); + if(instance->decoder_result) { + res = true; + } + return res; +} + +SubGhzProtocolDecoderBase* subghz_txrx_get_decoder(SubGhzTxRx* instance) { + furi_assert(instance); + return instance->decoder_result; +} + +bool subghz_txrx_protocol_is_serializable(SubGhzTxRx* instance) { + furi_assert(instance); + return ( + (instance->decoder_result->protocol->flag & SubGhzProtocolFlag_Save) == + SubGhzProtocolFlag_Save); +} + +bool subghz_txrx_protocol_is_transmittable(SubGhzTxRx* instance, bool check_type) { + furi_assert(instance); + const SubGhzProtocol* protocol = instance->decoder_result->protocol; + if(check_type) { + return ( + ((protocol->flag & SubGhzProtocolFlag_Send) == SubGhzProtocolFlag_Send) && + protocol->encoder->deserialize && protocol->type == SubGhzProtocolTypeStatic); + } + return ( + ((protocol->flag & SubGhzProtocolFlag_Send) == SubGhzProtocolFlag_Send) && + protocol->encoder->deserialize); +} + +void subghz_txrx_receiver_set_filter(SubGhzTxRx* instance, SubGhzProtocolFlag filter) { + furi_assert(instance); + subghz_receiver_set_filter(instance->receiver, filter); +} + +void subghz_txrx_set_rx_calback( + SubGhzTxRx* instance, + SubGhzReceiverCallback callback, + void* context) { + subghz_receiver_set_rx_callback(instance->receiver, callback, context); +} + +void subghz_txrx_set_raw_file_encoder_worker_callback_end( + SubGhzTxRx* instance, + SubGhzProtocolEncoderRAWCallbackEnd callback, + void* context) { + subghz_protocol_raw_file_encoder_worker_set_callback_end( + (SubGhzProtocolEncoderRAW*)subghz_transmitter_get_protocol_instance(instance->transmitter), + callback, + context); +} + +bool subghz_txrx_radio_device_is_external_connected(SubGhzTxRx* instance, const char* name) { + furi_assert(instance); + + bool is_connect = false; + bool is_otg_enabled = furi_hal_power_is_otg_enabled(); + + if(!is_otg_enabled) { + subghz_txrx_radio_device_power_on(instance); + } + + const SubGhzDevice* device = subghz_devices_get_by_name(name); + if(device) { + is_connect = subghz_devices_is_connect(device); + } + + if(!is_otg_enabled) { + subghz_txrx_radio_device_power_off(instance); + } + return is_connect; +} + +SubGhzRadioDeviceType + subghz_txrx_radio_device_set(SubGhzTxRx* instance, SubGhzRadioDeviceType radio_device_type) { + furi_assert(instance); + + if(radio_device_type == SubGhzRadioDeviceTypeExternalCC1101 && + subghz_txrx_radio_device_is_external_connected(instance, SUBGHZ_DEVICE_CC1101_EXT_NAME)) { + subghz_txrx_radio_device_power_on(instance); + instance->radio_device = subghz_devices_get_by_name(SUBGHZ_DEVICE_CC1101_EXT_NAME); + subghz_devices_begin(instance->radio_device); + instance->radio_device_type = SubGhzRadioDeviceTypeExternalCC1101; + } else { + subghz_txrx_radio_device_power_off(instance); + if(instance->radio_device_type != SubGhzRadioDeviceTypeInternal) { + subghz_devices_end(instance->radio_device); + } + instance->radio_device = subghz_devices_get_by_name(SUBGHZ_DEVICE_CC1101_INT_NAME); + instance->radio_device_type = SubGhzRadioDeviceTypeInternal; + } + + return instance->radio_device_type; +} + +SubGhzRadioDeviceType subghz_txrx_radio_device_get(SubGhzTxRx* instance) { + furi_assert(instance); + return instance->radio_device_type; +} + +float subghz_txrx_radio_device_get_rssi(SubGhzTxRx* instance) { + furi_assert(instance); + return subghz_devices_get_rssi(instance->radio_device); +} + +const char* subghz_txrx_radio_device_get_name(SubGhzTxRx* instance) { + furi_assert(instance); + return subghz_devices_get_name(instance->radio_device); +} + +bool subghz_txrx_radio_device_is_frequency_valid(SubGhzTxRx* instance, uint32_t frequency) { + furi_assert(instance); + return subghz_devices_is_frequency_valid(instance->radio_device, frequency); +} + +bool subghz_txrx_radio_device_is_tx_allowed(SubGhzTxRx* instance, uint32_t frequency) { + furi_assert(instance); + furi_assert(instance->txrx_state != SubGhzTxRxStateSleep); + + subghz_devices_idle(instance->radio_device); + subghz_devices_set_frequency(instance->radio_device, frequency); + + bool ret = subghz_devices_set_tx(instance->radio_device); + subghz_devices_idle(instance->radio_device); + + return ret; +} + +void subghz_txrx_set_debug_pin_state(SubGhzTxRx* instance, bool state) { + furi_assert(instance); + instance->debug_pin_state = state; +} + +bool subghz_txrx_get_debug_pin_state(SubGhzTxRx* instance) { + furi_assert(instance); + return instance->debug_pin_state; +} + +#ifndef FW_ORIGIN_Official +void subghz_txrx_reset_dynamic_and_custom_btns(SubGhzTxRx* instance) { + furi_assert(instance); + subghz_environment_reset_keeloq(instance->environment); + + subghz_custom_btns_reset(); +} +#endif + +SubGhzReceiver* subghz_txrx_get_receiver(SubGhzTxRx* instance) { + furi_assert(instance); + return instance->receiver; +} \ No newline at end of file diff --git a/subghz_remote/helpers/txrx/subghz_txrx.h b/subghz_remote/helpers/txrx/subghz_txrx.h new file mode 100644 index 00000000000..8a96c54c3a9 --- /dev/null +++ b/subghz_remote/helpers/txrx/subghz_txrx.h @@ -0,0 +1,375 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +typedef struct SubGhzTxRx SubGhzTxRx; + +typedef void (*SubGhzTxRxNeedSaveCallback)(void* context); + +typedef enum { + SubGhzTxRxStartTxStateOk, + SubGhzTxRxStartTxStateErrorOnlyRx, + SubGhzTxRxStartTxStateErrorParserOthers, +} SubGhzTxRxStartTxState; + +// Type from subghz_types.h need for txrx working +/** SubGhzTxRx state */ +typedef enum { + SubGhzTxRxStateIDLE, + SubGhzTxRxStateRx, + SubGhzTxRxStateTx, + SubGhzTxRxStateSleep, +} SubGhzTxRxState; + +/** SubGhzHopperState state */ +typedef enum { + SubGhzHopperStateOFF, + SubGhzHopperStateRunning, + SubGhzHopperStatePause, + SubGhzHopperStateRSSITimeOut, +} SubGhzHopperState; + +/** SubGhzSpeakerState state */ +typedef enum { + SubGhzSpeakerStateDisable, + SubGhzSpeakerStateShutdown, + SubGhzSpeakerStateEnable, +} SubGhzSpeakerState; + +/** SubGhzRadioDeviceType */ +typedef enum { + SubGhzRadioDeviceTypeAuto, + SubGhzRadioDeviceTypeInternal, + SubGhzRadioDeviceTypeExternalCC1101, +} SubGhzRadioDeviceType; + +/** + * Allocate SubGhzTxRx + * + * @return SubGhzTxRx* pointer to SubGhzTxRx + */ +SubGhzTxRx* subghz_txrx_alloc(void); + +/** + * Free SubGhzTxRx + * + * @param instance Pointer to a SubGhzTxRx + */ +void subghz_txrx_free(SubGhzTxRx* instance); + +/** + * Check if the database is loaded + * + * @param instance Pointer to a SubGhzTxRx + * @return bool True if the database is loaded + */ +bool subghz_txrx_is_database_loaded(SubGhzTxRx* instance); + +/** + * Set preset + * + * @param instance Pointer to a SubGhzTxRx + * @param preset_name Name of preset + * @param frequency Frequency in Hz + * @param preset_data Data of preset + * @param preset_data_size Size of preset data + */ +void subghz_txrx_set_preset( + SubGhzTxRx* instance, + const char* preset_name, + uint32_t frequency, + uint8_t* preset_data, + size_t preset_data_size); + +/** + * Get name of preset + * + * @param instance Pointer to a SubGhzTxRx + * @param preset String of preset + * @return const char* Name of preset + */ +const char* subghz_txrx_get_preset_name(SubGhzTxRx* instance, const char* preset); + +/** + * Get of preset + * + * @param instance Pointer to a SubGhzTxRx + * @return SubGhzRadioPreset Preset + */ +SubGhzRadioPreset subghz_txrx_get_preset(SubGhzTxRx* instance); + +/** + * Get string frequency and modulation + * + * @param instance Pointer to a SubGhzTxRx + * @param frequency Pointer to a string frequency + * @param modulation Pointer to a string modulation + */ +void subghz_txrx_get_frequency_and_modulation( + SubGhzTxRx* instance, + FuriString* frequency, + FuriString* modulation, + bool long_name); + +/** + * Start TX CC1101 + * + * @param instance Pointer to a SubGhzTxRx + * @param flipper_format Pointer to a FlipperFormat + * @return SubGhzTxRxStartTxState + */ +SubGhzTxRxStartTxState subghz_txrx_tx_start(SubGhzTxRx* instance, FlipperFormat* flipper_format); + +/** + * Start RX CC1101 + * + * @param instance Pointer to a SubGhzTxRx + */ +void subghz_txrx_rx_start(SubGhzTxRx* instance); + +/** + * Stop TX/RX CC1101 + * + * @param instance Pointer to a SubGhzTxRx + */ +void subghz_txrx_stop(SubGhzTxRx* instance); + +/** + * Set sleep mode CC1101 + * + * @param instance Pointer to a SubGhzTxRx + */ +void subghz_txrx_sleep(SubGhzTxRx* instance); + +/** + * Update frequency CC1101 in automatic mode (hopper) + * + * @param instance Pointer to a SubGhzTxRx + */ +void subghz_txrx_hopper_update(SubGhzTxRx* instance); + +/** + * Get state hopper + * + * @param instance Pointer to a SubGhzTxRx + * @return SubGhzHopperState + */ +SubGhzHopperState subghz_txrx_hopper_get_state(SubGhzTxRx* instance); + +/** + * Set state hopper + * + * @param instance Pointer to a SubGhzTxRx + * @param state State hopper + */ +void subghz_txrx_hopper_set_state(SubGhzTxRx* instance, SubGhzHopperState state); + +/** + * Unpause hopper + * + * @param instance Pointer to a SubGhzTxRx + */ +void subghz_txrx_hopper_unpause(SubGhzTxRx* instance); + +/** + * Set pause hopper + * + * @param instance Pointer to a SubGhzTxRx + */ +void subghz_txrx_hopper_pause(SubGhzTxRx* instance); + +/** + * Speaker on + * + * @param instance Pointer to a SubGhzTxRx + */ +void subghz_txrx_speaker_on(SubGhzTxRx* instance); + +/** + * Speaker off + * + * @param instance Pointer to a SubGhzTxRx + */ +void subghz_txrx_speaker_off(SubGhzTxRx* instance); + +/** + * Speaker mute + * + * @param instance Pointer to a SubGhzTxRx + */ +void subghz_txrx_speaker_mute(SubGhzTxRx* instance); + +/** + * Speaker unmute + * + * @param instance Pointer to a SubGhzTxRx + */ +void subghz_txrx_speaker_unmute(SubGhzTxRx* instance); + +/** + * Set state speaker + * + * @param instance Pointer to a SubGhzTxRx + * @param state State speaker + */ +void subghz_txrx_speaker_set_state(SubGhzTxRx* instance, SubGhzSpeakerState state); + +/** + * Get state speaker + * + * @param instance Pointer to a SubGhzTxRx + * @return SubGhzSpeakerState + */ +SubGhzSpeakerState subghz_txrx_speaker_get_state(SubGhzTxRx* instance); + +/** + * load decoder by name protocol + * + * @param instance Pointer to a SubGhzTxRx + * @param name_protocol Name protocol + * @return bool True if the decoder is loaded + */ +bool subghz_txrx_load_decoder_by_name_protocol(SubGhzTxRx* instance, const char* name_protocol); + +/** + * Get decoder + * + * @param instance Pointer to a SubGhzTxRx + * @return SubGhzProtocolDecoderBase* Pointer to a SubGhzProtocolDecoderBase + */ +SubGhzProtocolDecoderBase* subghz_txrx_get_decoder(SubGhzTxRx* instance); + +/** + * Set callback for save data + * + * @param instance Pointer to a SubGhzTxRx + * @param callback Callback for save data + * @param context Context for callback + */ +void subghz_txrx_set_need_save_callback( + SubGhzTxRx* instance, + SubGhzTxRxNeedSaveCallback callback, + void* context); + +/** + * Get pointer to a load data key + * + * @param instance Pointer to a SubGhzTxRx + * @return FlipperFormat* + */ +FlipperFormat* subghz_txrx_get_fff_data(SubGhzTxRx* instance); + +/** + * Get pointer to a SugGhzSetting + * + * @param instance Pointer to a SubGhzTxRx + * @return SubGhzSetting* + */ +SubGhzSetting* subghz_txrx_get_setting(SubGhzTxRx* instance); + +/** + * Is it possible to save this protocol + * + * @param instance Pointer to a SubGhzTxRx + * @return bool True if it is possible to save this protocol + */ +bool subghz_txrx_protocol_is_serializable(SubGhzTxRx* instance); + +/** + * Is it possible to send this protocol + * + * @param instance Pointer to a SubGhzTxRx + * @return bool True if it is possible to send this protocol + */ +bool subghz_txrx_protocol_is_transmittable(SubGhzTxRx* instance, bool check_type); + +/** + * Set filter, what types of decoder to use + * + * @param instance Pointer to a SubGhzTxRx + * @param filter Filter + */ +void subghz_txrx_receiver_set_filter(SubGhzTxRx* instance, SubGhzProtocolFlag filter); + +/** + * Set callback for receive data + * + * @param instance Pointer to a SubGhzTxRx + * @param callback Callback for receive data + * @param context Context for callback + */ +void subghz_txrx_set_rx_calback( + SubGhzTxRx* instance, + SubGhzReceiverCallback callback, + void* context); + +/** + * Set callback for Raw decoder, end of data transfer + * + * @param instance Pointer to a SubGhzTxRx + * @param callback Callback for Raw decoder, end of data transfer + * @param context Context for callback + */ +void subghz_txrx_set_raw_file_encoder_worker_callback_end( + SubGhzTxRx* instance, + SubGhzProtocolEncoderRAWCallbackEnd callback, + void* context); + +/* Checking if an external radio device is connected +* +* @param instance Pointer to a SubGhzTxRx +* @param name Name of external radio device +* @return bool True if is connected to the external radio device +*/ +bool subghz_txrx_radio_device_is_external_connected(SubGhzTxRx* instance, const char* name); + +/* Set the selected radio device to use +* +* @param instance Pointer to a SubGhzTxRx +* @param radio_device_type Radio device type +* @return SubGhzRadioDeviceType Type of installed radio device +*/ +SubGhzRadioDeviceType + subghz_txrx_radio_device_set(SubGhzTxRx* instance, SubGhzRadioDeviceType radio_device_type); + +/* Get the selected radio device to use +* +* @param instance Pointer to a SubGhzTxRx +* @return SubGhzRadioDeviceType Type of installed radio device +*/ +SubGhzRadioDeviceType subghz_txrx_radio_device_get(SubGhzTxRx* instance); + +/* Get RSSI the selected radio device to use +* +* @param instance Pointer to a SubGhzTxRx +* @return float RSSI +*/ +float subghz_txrx_radio_device_get_rssi(SubGhzTxRx* instance); + +/* Get name the selected radio device to use +* +* @param instance Pointer to a SubGhzTxRx +* @return const char* Name of installed radio device +*/ +const char* subghz_txrx_radio_device_get_name(SubGhzTxRx* instance); + +/* Get get intelligence whether frequency the selected radio device to use +* +* @param instance Pointer to a SubGhzTxRx +* @return bool True if the frequency is valid +*/ +bool subghz_txrx_radio_device_is_frequency_valid(SubGhzTxRx* instance, uint32_t frequency); + +bool subghz_txrx_radio_device_is_tx_allowed(SubGhzTxRx* instance, uint32_t frequency); + +void subghz_txrx_set_debug_pin_state(SubGhzTxRx* instance, bool state); +bool subghz_txrx_get_debug_pin_state(SubGhzTxRx* instance); +#ifndef FW_ORIGIN_Official +void subghz_txrx_reset_dynamic_and_custom_btns(SubGhzTxRx* instance); +#endif +SubGhzReceiver* subghz_txrx_get_receiver(SubGhzTxRx* instance); // TODO use only in DecodeRaw diff --git a/subghz_remote/helpers/txrx/subghz_txrx_i.h b/subghz_remote/helpers/txrx/subghz_txrx_i.h new file mode 100644 index 00000000000..f058c228238 --- /dev/null +++ b/subghz_remote/helpers/txrx/subghz_txrx_i.h @@ -0,0 +1,31 @@ +#pragma once + +#include "subghz_txrx.h" + +struct SubGhzTxRx { + SubGhzWorker* worker; + + SubGhzEnvironment* environment; + SubGhzReceiver* receiver; + SubGhzTransmitter* transmitter; + SubGhzProtocolDecoderBase* decoder_result; + FlipperFormat* fff_data; + + SubGhzRadioPreset* preset; + SubGhzSetting* setting; + + uint8_t hopper_timeout; + uint8_t hopper_idx_frequency; + bool is_database_loaded; + SubGhzHopperState hopper_state; + + SubGhzTxRxState txrx_state; + SubGhzSpeakerState speaker_state; + const SubGhzDevice* radio_device; + SubGhzRadioDeviceType radio_device_type; + + SubGhzTxRxNeedSaveCallback need_save_callback; + void* need_save_context; + + bool debug_pin_state; +}; diff --git a/subghz_remote/icon.png b/subghz_remote/icon.png new file mode 100644 index 00000000000..c6b410f4c59 Binary files /dev/null and b/subghz_remote/icon.png differ diff --git a/subghz_remote/icons/remote_scene/Dpad/down.png b/subghz_remote/icons/remote_scene/Dpad/down.png new file mode 100644 index 00000000000..198e22ea285 Binary files /dev/null and b/subghz_remote/icons/remote_scene/Dpad/down.png differ diff --git a/subghz_remote/icons/remote_scene/Dpad/down_hover.png b/subghz_remote/icons/remote_scene/Dpad/down_hover.png new file mode 100644 index 00000000000..39ac087e769 Binary files /dev/null and b/subghz_remote/icons/remote_scene/Dpad/down_hover.png differ diff --git a/subghz_remote/icons/remote_scene/Dpad/left.png b/subghz_remote/icons/remote_scene/Dpad/left.png new file mode 100644 index 00000000000..38b83c79bb7 Binary files /dev/null and b/subghz_remote/icons/remote_scene/Dpad/left.png differ diff --git a/subghz_remote/icons/remote_scene/Dpad/left_hover.png b/subghz_remote/icons/remote_scene/Dpad/left_hover.png new file mode 100644 index 00000000000..45f58b8b674 Binary files /dev/null and b/subghz_remote/icons/remote_scene/Dpad/left_hover.png differ diff --git a/subghz_remote/icons/remote_scene/Dpad/ok.png b/subghz_remote/icons/remote_scene/Dpad/ok.png new file mode 100644 index 00000000000..dfd3d2fd112 Binary files /dev/null and b/subghz_remote/icons/remote_scene/Dpad/ok.png differ diff --git a/subghz_remote/icons/remote_scene/Dpad/ok_hover.png b/subghz_remote/icons/remote_scene/Dpad/ok_hover.png new file mode 100644 index 00000000000..9107c1a79aa Binary files /dev/null and b/subghz_remote/icons/remote_scene/Dpad/ok_hover.png differ diff --git a/subghz_remote/icons/remote_scene/Dpad/right.png b/subghz_remote/icons/remote_scene/Dpad/right.png new file mode 100644 index 00000000000..ca6748f9ae7 Binary files /dev/null and b/subghz_remote/icons/remote_scene/Dpad/right.png differ diff --git a/subghz_remote/icons/remote_scene/Dpad/right_hover.png b/subghz_remote/icons/remote_scene/Dpad/right_hover.png new file mode 100644 index 00000000000..2d53ce701c9 Binary files /dev/null and b/subghz_remote/icons/remote_scene/Dpad/right_hover.png differ diff --git a/subghz_remote/icons/remote_scene/Dpad/up.png b/subghz_remote/icons/remote_scene/Dpad/up.png new file mode 100644 index 00000000000..cd032ed1226 Binary files /dev/null and b/subghz_remote/icons/remote_scene/Dpad/up.png differ diff --git a/subghz_remote/icons/remote_scene/Dpad/up_hover.png b/subghz_remote/icons/remote_scene/Dpad/up_hover.png new file mode 100644 index 00000000000..8bc334750d1 Binary files /dev/null and b/subghz_remote/icons/remote_scene/Dpad/up_hover.png differ diff --git a/subghz_remote/icons/remote_scene/list/ButtonDown_7x4.png b/subghz_remote/icons/remote_scene/list/ButtonDown_7x4.png new file mode 100644 index 00000000000..2954bb6a67d Binary files /dev/null and b/subghz_remote/icons/remote_scene/list/ButtonDown_7x4.png differ diff --git a/subghz_remote/icons/remote_scene/list/ButtonLeft_4x7.png b/subghz_remote/icons/remote_scene/list/ButtonLeft_4x7.png new file mode 100644 index 00000000000..0b4655d4324 Binary files /dev/null and b/subghz_remote/icons/remote_scene/list/ButtonLeft_4x7.png differ diff --git a/subghz_remote/icons/remote_scene/list/ButtonRight_4x7.png b/subghz_remote/icons/remote_scene/list/ButtonRight_4x7.png new file mode 100644 index 00000000000..8e1c74c1c00 Binary files /dev/null and b/subghz_remote/icons/remote_scene/list/ButtonRight_4x7.png differ diff --git a/subghz_remote/icons/remote_scene/list/ButtonUp_7x4.png b/subghz_remote/icons/remote_scene/list/ButtonUp_7x4.png new file mode 100644 index 00000000000..1be79328b40 Binary files /dev/null and b/subghz_remote/icons/remote_scene/list/ButtonUp_7x4.png differ diff --git a/subghz_remote/icons/remote_scene/list/Ok_btn_9x9.png b/subghz_remote/icons/remote_scene/list/Ok_btn_9x9.png new file mode 100644 index 00000000000..9a1539da204 Binary files /dev/null and b/subghz_remote/icons/remote_scene/list/Ok_btn_9x9.png differ diff --git a/subghz_remote/icons/remote_scene/statusbar/External_antenna_20x12.png b/subghz_remote/icons/remote_scene/statusbar/External_antenna_20x12.png new file mode 100644 index 00000000000..940087071a1 Binary files /dev/null and b/subghz_remote/icons/remote_scene/statusbar/External_antenna_20x12.png differ diff --git a/subghz_remote/icons/remote_scene/statusbar/Internal_antenna_20x12.png b/subghz_remote/icons/remote_scene/statusbar/Internal_antenna_20x12.png new file mode 100644 index 00000000000..a8a5be09fb8 Binary files /dev/null and b/subghz_remote/icons/remote_scene/statusbar/Internal_antenna_20x12.png differ diff --git a/subghz_remote/icons/remote_scene/statusbar/Pin_arrow_up_7x9.png b/subghz_remote/icons/remote_scene/statusbar/Pin_arrow_up_7x9.png new file mode 100644 index 00000000000..a91a6fd5e99 Binary files /dev/null and b/subghz_remote/icons/remote_scene/statusbar/Pin_arrow_up_7x9.png differ diff --git a/subghz_remote/icons/remote_scene/statusbar/Status_cube_14x14.png b/subghz_remote/icons/remote_scene/statusbar/Status_cube_14x14.png new file mode 100644 index 00000000000..f1e72cbbd00 Binary files /dev/null and b/subghz_remote/icons/remote_scene/statusbar/Status_cube_14x14.png differ diff --git a/subghz_remote/icons/remote_scene/statusbar/status_bar.png b/subghz_remote/icons/remote_scene/statusbar/status_bar.png new file mode 100644 index 00000000000..8136fe32e5f Binary files /dev/null and b/subghz_remote/icons/remote_scene/statusbar/status_bar.png differ diff --git a/subghz_remote/icons/sub1_10px.png b/subghz_remote/icons/sub1_10px.png new file mode 100644 index 00000000000..5a25fdf4ef1 Binary files /dev/null and b/subghz_remote/icons/sub1_10px.png differ diff --git a/subghz_remote/icons/subrem_10px.png b/subghz_remote/icons/subrem_10px.png new file mode 100644 index 00000000000..c6b410f4c59 Binary files /dev/null and b/subghz_remote/icons/subrem_10px.png differ diff --git a/subghz_remote/scenes/subrem_scene.c b/subghz_remote/scenes/subrem_scene.c new file mode 100644 index 00000000000..c45285b9675 --- /dev/null +++ b/subghz_remote/scenes/subrem_scene.c @@ -0,0 +1,30 @@ +#include "../subghz_remote_app_i.h" + +// Generate scene on_enter handlers array +#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_enter, +void (*const subrem_scene_on_enter_handlers[])(void*) = { +#include "subrem_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 subrem_scene_on_event_handlers[])(void* context, SceneManagerEvent event) = { +#include "subrem_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 subrem_scene_on_exit_handlers[])(void* context) = { +#include "subrem_scene_config.h" +}; +#undef ADD_SCENE + +// Initialize scene handlers configuration structure +const SceneManagerHandlers subrem_scene_handlers = { + .on_enter_handlers = subrem_scene_on_enter_handlers, + .on_event_handlers = subrem_scene_on_event_handlers, + .on_exit_handlers = subrem_scene_on_exit_handlers, + .scene_num = SubRemSceneNum, +}; diff --git a/subghz_remote/scenes/subrem_scene.h b/subghz_remote/scenes/subrem_scene.h new file mode 100644 index 00000000000..5c01f8ca516 --- /dev/null +++ b/subghz_remote/scenes/subrem_scene.h @@ -0,0 +1,29 @@ +#pragma once + +#include + +// Generate scene id and total number +#define ADD_SCENE(prefix, name, id) SubRemScene##id, +typedef enum { +#include "subrem_scene_config.h" + SubRemSceneNum, +} SubRemScene; +#undef ADD_SCENE + +extern const SceneManagerHandlers subrem_scene_handlers; + +// Generate scene on_enter handlers declaration +#define ADD_SCENE(prefix, name, id) void prefix##_scene_##name##_on_enter(void*); +#include "subrem_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 "subrem_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 "subrem_scene_config.h" +#undef ADD_SCENE diff --git a/subghz_remote/scenes/subrem_scene_config.h b/subghz_remote/scenes/subrem_scene_config.h new file mode 100644 index 00000000000..56fe641a68b --- /dev/null +++ b/subghz_remote/scenes/subrem_scene_config.h @@ -0,0 +1,12 @@ +ADD_SCENE(subrem, start, Start) +ADD_SCENE(subrem, open_map_file, OpenMapFile) +ADD_SCENE(subrem, remote, Remote) +ADD_SCENE(subrem, edit_menu, EditMenu) +ADD_SCENE(subrem, edit_submenu, EditSubMenu) +ADD_SCENE(subrem, edit_label, EditLabel) +ADD_SCENE(subrem, open_sub_file, OpenSubFile) +ADD_SCENE(subrem, edit_preview, EditPreview) +ADD_SCENE(subrem, enter_new_name, EnterNewName) +#ifdef FW_ORIGIN_Official +ADD_SCENE(subrem, fw_warning, FwWarning) +#endif \ No newline at end of file diff --git a/subghz_remote/scenes/subrem_scene_edit_label.c b/subghz_remote/scenes/subrem_scene_edit_label.c new file mode 100644 index 00000000000..25c10bb46a4 --- /dev/null +++ b/subghz_remote/scenes/subrem_scene_edit_label.c @@ -0,0 +1,133 @@ +#include "../subghz_remote_app_i.h" + +#include + +typedef enum { + SubRemSceneEditLabelStateTextInput, + SubRemSceneEditLabelStateWidget, +} SubRemSceneEditLabelState; + +void subrem_scene_edit_label_text_input_callback(void* context) { + furi_assert(context); + SubGhzRemoteApp* app = context; + view_dispatcher_send_custom_event( + app->view_dispatcher, SubRemCustomEventSceneEditLabelInputDone); +} + +void subrem_scene_edit_label_widget_callback(GuiButtonType result, InputType type, void* context) { + furi_assert(context); + SubGhzRemoteApp* app = context; + if((result == GuiButtonTypeCenter) && (type == InputTypeShort)) { + view_dispatcher_send_custom_event( + app->view_dispatcher, SubRemCustomEventSceneEditLabelWidgetAcces); + } else if((result == GuiButtonTypeLeft) && (type == InputTypeShort)) { + view_dispatcher_send_custom_event( + app->view_dispatcher, SubRemCustomEventSceneEditLabelWidgetBack); + } +} + +void subrem_scene_edit_label_on_enter(void* context) { + SubGhzRemoteApp* app = context; + + SubRemSubFilePreset* sub_preset = app->map_preset->subs_preset[app->chosen_sub]; + + FuriString* temp_str = furi_string_alloc(); + + if(furi_string_empty(sub_preset->label)) { + if(furi_string_empty(sub_preset->file_path)) { + path_extract_filename(sub_preset->file_path, temp_str, true); + strcpy(app->file_name_tmp, furi_string_get_cstr(temp_str)); + } else { + strcpy(app->file_name_tmp, ""); + } + } else { + strcpy(app->file_name_tmp, furi_string_get_cstr(sub_preset->label)); + } + + TextInput* text_input = app->text_input; + text_input_set_header_text(text_input, "Label name"); + text_input_set_result_callback( + text_input, + subrem_scene_edit_label_text_input_callback, + app, + app->file_name_tmp, + 25, + false); +#ifndef FW_ORIGIN_Official + text_input_set_minimum_length(app->text_input, 0); +#endif + widget_add_string_element( + app->widget, 63, 12, AlignCenter, AlignCenter, FontPrimary, "Empty Label Name"); + widget_add_string_element( + app->widget, 63, 32, AlignCenter, AlignCenter, FontSecondary, "Continue?"); + + widget_add_button_element( + app->widget, GuiButtonTypeCenter, "Ok", subrem_scene_edit_label_widget_callback, app); + widget_add_button_element( + app->widget, GuiButtonTypeLeft, "Back", subrem_scene_edit_label_widget_callback, app); + + scene_manager_set_scene_state( + app->scene_manager, SubRemSceneEditLabel, SubRemSceneEditLabelStateTextInput); + view_dispatcher_switch_to_view(app->view_dispatcher, SubRemViewIDTextInput); + + furi_string_free(temp_str); +} + +bool subrem_scene_edit_label_on_event(void* context, SceneManagerEvent event) { + SubGhzRemoteApp* app = context; + + FuriString* label = app->map_preset->subs_preset[app->chosen_sub]->label; + + if(event.type == SceneManagerEventTypeBack) { + if(scene_manager_get_scene_state(app->scene_manager, SubRemSceneEditLabel) == + SubRemSceneEditLabelStateWidget) { + scene_manager_set_scene_state( + app->scene_manager, SubRemSceneEditLabel, SubRemSceneEditLabelStateTextInput); + view_dispatcher_switch_to_view(app->view_dispatcher, SubRemViewIDTextInput); + return true; + } else if( + scene_manager_get_scene_state(app->scene_manager, SubRemSceneEditLabel) == + SubRemSceneEditLabelStateTextInput) { + scene_manager_previous_scene(app->scene_manager); + return true; + } + + scene_manager_previous_scene(app->scene_manager); + return true; + } else if(event.type == SceneManagerEventTypeCustom) { + if(event.event == SubRemCustomEventSceneEditLabelInputDone) { + if(strcmp(app->file_name_tmp, "") == 0) { + scene_manager_set_scene_state( + app->scene_manager, SubRemSceneEditLabel, SubRemSceneEditLabelStateWidget); + view_dispatcher_switch_to_view(app->view_dispatcher, SubRemViewIDWidget); + + } else { + furi_string_set(label, app->file_name_tmp); + app->map_not_saved = true; + scene_manager_previous_scene(app->scene_manager); + } + return true; + } else if(event.event == SubRemCustomEventSceneEditLabelWidgetAcces) { + furi_string_set(label, app->file_name_tmp); + app->map_not_saved = true; + scene_manager_previous_scene(app->scene_manager); + + return true; + } else if(event.event == SubRemCustomEventSceneEditLabelWidgetBack) { + scene_manager_set_scene_state( + app->scene_manager, SubRemSceneEditLabel, SubRemSceneEditLabelStateTextInput); + view_dispatcher_switch_to_view(app->view_dispatcher, SubRemViewIDTextInput); + + return true; + } + } + return false; +} + +void subrem_scene_edit_label_on_exit(void* context) { + SubGhzRemoteApp* app = context; + + // Clear view + text_input_reset(app->text_input); + widget_reset(app->widget); +} diff --git a/subghz_remote/scenes/subrem_scene_edit_menu.c b/subghz_remote/scenes/subrem_scene_edit_menu.c new file mode 100644 index 00000000000..bc54311c56b --- /dev/null +++ b/subghz_remote/scenes/subrem_scene_edit_menu.c @@ -0,0 +1,123 @@ +#include "../subghz_remote_app_i.h" + +void subrem_scene_edit_menu_callback(SubRemCustomEvent event, void* context) { + furi_assert(context); + SubGhzRemoteApp* app = context; + view_dispatcher_send_custom_event(app->view_dispatcher, event); +} + +void subrem_scene_edit_menu_widget_callback(GuiButtonType result, InputType type, void* context) { + furi_assert(context); + SubGhzRemoteApp* app = context; + if((result == GuiButtonTypeRight) && (type == InputTypeShort)) { + app->map_not_saved = false; + view_dispatcher_send_custom_event(app->view_dispatcher, SubRemCustomEventViewEditMenuBack); + } else if((result == GuiButtonTypeLeft) && (type == InputTypeShort)) { + view_dispatcher_switch_to_view(app->view_dispatcher, SubRemViewIDEditMenu); + } +} + +static uint8_t subrem_scene_edit_menu_state_to_index(SubRemEditMenuState event_id) { + uint8_t ret = 0; + + if(event_id == SubRemEditMenuStateUP) { + ret = SubRemSubKeyNameUp; + } else if(event_id == SubRemEditMenuStateDOWN) { + ret = SubRemSubKeyNameDown; + } else if(event_id == SubRemEditMenuStateLEFT) { + ret = SubRemSubKeyNameLeft; + } else if(event_id == SubRemEditMenuStateRIGHT) { + ret = SubRemSubKeyNameRight; + } else if(event_id == SubRemEditMenuStateOK) { + ret = SubRemSubKeyNameOk; + } + + return ret; +} + +static void subrem_scene_edit_menu_update_data(SubGhzRemoteApp* app) { + furi_assert(app); + uint8_t index = subrem_scene_edit_menu_state_to_index( + scene_manager_get_scene_state(app->scene_manager, SubRemSceneEditMenu)); + + subrem_view_edit_menu_add_data_to_show( + app->subrem_edit_menu, + index, + app->map_preset->subs_preset[index]->label, + app->map_preset->subs_preset[index]->file_path, + app->map_preset->subs_preset[index]->load_state); +} + +void subrem_scene_edit_menu_on_enter(void* context) { + SubGhzRemoteApp* app = context; + + subrem_view_edit_menu_set_callback( + app->subrem_edit_menu, subrem_scene_edit_menu_callback, app); + + subrem_scene_edit_menu_update_data(app); + + view_dispatcher_switch_to_view(app->view_dispatcher, SubRemViewIDEditMenu); + + Widget* widget = app->widget; + + widget_add_string_element( + widget, 63, 12, AlignCenter, AlignBottom, FontPrimary, "Changes are not saved"); + widget_add_string_element( + widget, 63, 32, AlignCenter, AlignBottom, FontPrimary, "do you want to exit?"); + + widget_add_button_element( + widget, GuiButtonTypeRight, "Yes", subrem_scene_edit_menu_widget_callback, app); + widget_add_button_element( + widget, GuiButtonTypeLeft, "No", subrem_scene_edit_menu_widget_callback, app); +} + +bool subrem_scene_edit_menu_on_event(void* context, SceneManagerEvent event) { + SubGhzRemoteApp* app = context; + + if(event.type == SceneManagerEventTypeBack) { + // Catch widget backEvent + return true; + } + + if(event.type == SceneManagerEventTypeCustom) { + if(event.event == SubRemCustomEventViewEditMenuBack) { + if(app->map_not_saved) { + view_dispatcher_switch_to_view(app->view_dispatcher, SubRemViewIDWidget); + } else if(!scene_manager_search_and_switch_to_previous_scene( + app->scene_manager, SubRemSceneStart)) { + scene_manager_stop(app->scene_manager); + view_dispatcher_stop(app->view_dispatcher); + } + + return true; + } else if( + event.event == SubRemCustomEventViewEditMenuUP || + event.event == SubRemCustomEventViewEditMenuDOWN) { + scene_manager_set_scene_state( + app->scene_manager, + SubRemSceneEditMenu, + subrem_view_edit_menu_get_index(app->subrem_edit_menu)); + subrem_scene_edit_menu_update_data(app); + + return true; + } else if(event.event == SubRemCustomEventViewEditMenuEdit) { + app->chosen_sub = subrem_view_edit_menu_get_index(app->subrem_edit_menu); + scene_manager_set_scene_state( + app->scene_manager, SubRemSceneEditSubMenu, EditSubmenuIndexEditLabel); + scene_manager_next_scene(app->scene_manager, SubRemSceneEditSubMenu); + + return true; + } else if(event.event == SubRemCustomEventViewEditMenuSave) { + scene_manager_next_scene(app->scene_manager, SubRemSceneEditPreview); + + return true; + } + } + + return false; +} + +void subrem_scene_edit_menu_on_exit(void* context) { + SubGhzRemoteApp* app = context; + widget_reset(app->widget); +} diff --git a/subghz_remote/scenes/subrem_scene_edit_preview.c b/subghz_remote/scenes/subrem_scene_edit_preview.c new file mode 100644 index 00000000000..9f581289044 --- /dev/null +++ b/subghz_remote/scenes/subrem_scene_edit_preview.c @@ -0,0 +1,74 @@ +#include "../subghz_remote_app_i.h" +#include "../views/remote.h" + +#define TAG "SubRemScenRemote" + +void subghz_scene_edit_preview_save_popup_callback(void* context) { + SubGhzRemoteApp* app = context; + view_dispatcher_send_custom_event( + app->view_dispatcher, SubRemCustomEventSceneEditPreviewSaved); +} + +void subrem_scene_edit_preview_callback(SubRemCustomEvent event, void* context) { + furi_assert(context); + SubGhzRemoteApp* app = context; + view_dispatcher_send_custom_event(app->view_dispatcher, event); +} + +void subrem_scene_edit_preview_on_enter(void* context) { + SubGhzRemoteApp* app = context; + + // Setup view + Popup* popup = app->popup; + popup_set_icon(popup, 36, 5, &I_DolphinDone_80x58); + popup_set_header(popup, "Saved!", 13, 22, AlignLeft, AlignBottom); + popup_set_timeout(popup, 1500); + popup_set_context(popup, app); + popup_set_callback(popup, subghz_scene_edit_preview_save_popup_callback); + popup_enable_timeout(popup); + + subrem_view_remote_update_data_labels(app->subrem_remote_view, app->map_preset->subs_preset); + subrem_view_remote_set_state(app->subrem_remote_view, SubRemViewRemoteStateOFF, 0); + + subrem_view_remote_set_callback( + app->subrem_remote_view, subrem_scene_edit_preview_callback, app); + + view_dispatcher_switch_to_view(app->view_dispatcher, SubRemViewIDRemote); +} + +bool subrem_scene_edit_preview_on_event(void* context, SceneManagerEvent event) { + SubGhzRemoteApp* app = context; + + if(event.type == SceneManagerEventTypeBack || + (event.type == SceneManagerEventTypeCustom && + (event.event == SubRemCustomEventViewRemoteStartLEFT || + event.event == SubRemCustomEventViewRemoteForcedStop))) { + scene_manager_previous_scene(app->scene_manager); + return true; + } else if( + event.type == SceneManagerEventTypeCustom && + (event.event == SubRemCustomEventViewRemoteStartRIGHT || + event.event == SubRemCustomEventViewRemoteStartOK)) { + if(subrem_save_map_to_file(app)) { + view_dispatcher_switch_to_view(app->view_dispatcher, SubRemViewIDPopup); + app->map_not_saved = false; + return true; + } + // TODO error screen + return true; + } else if( + event.type == SceneManagerEventTypeCustom && + event.event == SubRemCustomEventSceneEditPreviewSaved) { + scene_manager_search_and_switch_to_previous_scene(app->scene_manager, SubRemSceneEditMenu); + } + // } else if(event.type == SceneManagerEventTypeTick) { + // } + return false; +} + +void subrem_scene_edit_preview_on_exit(void* context) { + SubGhzRemoteApp* app = context; + + subrem_view_remote_set_state(app->subrem_remote_view, SubRemViewRemoteStateIdle, 0); + popup_reset(app->popup); +} diff --git a/subghz_remote/scenes/subrem_scene_edit_submenu.c b/subghz_remote/scenes/subrem_scene_edit_submenu.c new file mode 100644 index 00000000000..447beb96d91 --- /dev/null +++ b/subghz_remote/scenes/subrem_scene_edit_submenu.c @@ -0,0 +1,54 @@ +#include "../subghz_remote_app_i.h" +#include "../helpers/subrem_custom_event.h" + +void subrem_scene_edit_submenu_text_input_callback(void* context) { + furi_assert(context); + SubGhzRemoteApp* app = context; + view_dispatcher_send_custom_event(app->view_dispatcher, SubRemCustomEventSceneEditsubmenu); +} + +void subrem_scene_edit_submenu_callback(void* context, uint32_t index) { + furi_assert(context); + SubGhzRemoteApp* app = context; + + view_dispatcher_send_custom_event(app->view_dispatcher, index); +} + +void subrem_scene_edit_submenu_on_enter(void* context) { + furi_assert(context); + + SubGhzRemoteApp* app = context; + Submenu* submenu = app->submenu; + submenu_add_item( + submenu, "Edit Label", EditSubmenuIndexEditLabel, subrem_scene_edit_submenu_callback, app); + submenu_add_item( + submenu, "Edit File", EditSubmenuIndexEditFile, subrem_scene_edit_submenu_callback, app); + + view_dispatcher_switch_to_view(app->view_dispatcher, SubRemViewIDSubmenu); +} + +bool subrem_scene_edit_submenu_on_event(void* context, SceneManagerEvent event) { + furi_assert(context); + + SubGhzRemoteApp* app = context; + bool consumed = false; + + if(event.type == SceneManagerEventTypeCustom) { + if(event.event == EditSubmenuIndexEditLabel) { + scene_manager_next_scene(app->scene_manager, SubRemSceneEditLabel); + consumed = true; + } else if(event.event == EditSubmenuIndexEditFile) { + scene_manager_next_scene(app->scene_manager, SubRemSceneOpenSubFile); + consumed = true; + } + } + + return consumed; +} + +void subrem_scene_edit_submenu_on_exit(void* context) { + furi_assert(context); + + SubGhzRemoteApp* app = context; + submenu_reset(app->submenu); +} diff --git a/subghz_remote/scenes/subrem_scene_enter_new_name.c b/subghz_remote/scenes/subrem_scene_enter_new_name.c new file mode 100644 index 00000000000..b829723a3aa --- /dev/null +++ b/subghz_remote/scenes/subrem_scene_enter_new_name.c @@ -0,0 +1,70 @@ +#include "../subghz_remote_app_i.h" +#include "../helpers/subrem_custom_event.h" + +#include + +void subrem_scene_enter_new_name_text_input_callback(void* context) { + furi_assert(context); + SubGhzRemoteApp* app = context; + view_dispatcher_send_custom_event(app->view_dispatcher, SubRemCustomEventSceneNewName); +} + +void subrem_scene_enter_new_name_on_enter(void* context) { + SubGhzRemoteApp* app = context; + + // Setup view + TextInput* text_input = app->text_input; + + //strncpy(app->file_name_tmp, "subrem_", SUBREM_MAX_LEN_NAME); + text_input_set_header_text(text_input, "Map file Name"); + text_input_set_result_callback( + text_input, + subrem_scene_enter_new_name_text_input_callback, + app, + app->file_name_tmp, + 25, + false); + + ValidatorIsFile* validator_is_file = validator_is_file_alloc_init( + furi_string_get_cstr(app->file_path), SUBREM_APP_EXTENSION, ""); + text_input_set_validator(text_input, validator_is_file_callback, validator_is_file); + + view_dispatcher_switch_to_view(app->view_dispatcher, SubRemViewIDTextInput); +} + +bool subrem_scene_enter_new_name_on_event(void* context, SceneManagerEvent event) { + furi_assert(context); + + SubGhzRemoteApp* app = context; + bool consumed = false; + + if(event.type == SceneManagerEventTypeCustom) { + if(event.event == SubRemCustomEventSceneNewName) { + if(strcmp(app->file_name_tmp, "") != 0) { + furi_string_set(app->file_path, SUBREM_APP_FOLDER); + furi_string_cat_printf( + app->file_path, "/%s%s", app->file_name_tmp, SUBREM_APP_EXTENSION); + + subrem_map_preset_reset(app->map_preset); + scene_manager_next_scene(app->scene_manager, SubRemSceneEditMenu); + } else { //error + } + consumed = true; + } + } + + return consumed; +} + +void subrem_scene_enter_new_name_on_exit(void* context) { + furi_assert(context); + + SubGhzRemoteApp* app = context; + submenu_reset(app->submenu); + + // Clear validator & view + void* validator_context = text_input_get_validator_callback_context(app->text_input); + text_input_set_validator(app->text_input, NULL, NULL); + validator_is_file_free(validator_context); + text_input_reset(app->text_input); +} diff --git a/subghz_remote/scenes/subrem_scene_fw_warning.c b/subghz_remote/scenes/subrem_scene_fw_warning.c new file mode 100644 index 00000000000..de473b72c95 --- /dev/null +++ b/subghz_remote/scenes/subrem_scene_fw_warning.c @@ -0,0 +1,129 @@ +#include "../subghz_remote_app_i.h" +#include "../helpers/subrem_custom_event.h" +#ifdef FW_ORIGIN_Official +typedef enum { + SceneFwWarningStateAttention, + SceneFwWarningStateAccept, +} SceneFwWarningState; + +static void subrem_scene_fw_warning_widget_render(SubGhzRemoteApp* app, SceneFwWarningState state); + +static void + subrem_scene_fw_warning_widget_callback(GuiButtonType result, InputType type, void* context) { + furi_assert(context); + SubGhzRemoteApp* app = context; + + if(type == InputTypeShort) { + SubRemCustomEvent event = SubRemCustomEventSceneFwWarningExit; + + switch(scene_manager_get_scene_state(app->scene_manager, SubRemSceneFwWarning)) { + case SceneFwWarningStateAttention: + if(result == GuiButtonTypeRight) { + event = SubRemCustomEventSceneFwWarningNext; + } + break; + + case SceneFwWarningStateAccept: + if(result == GuiButtonTypeRight) { + event = SubRemCustomEventSceneFwWarningContinue; + } + + break; + } + + view_dispatcher_send_custom_event(app->view_dispatcher, event); + } +} + +static void + subrem_scene_fw_warning_widget_render(SubGhzRemoteApp* app, SceneFwWarningState state) { + furi_assert(app); + Widget* widget = app->widget; + + widget_reset(widget); + + switch(state) { + case SceneFwWarningStateAttention: + widget_add_button_element( + widget, GuiButtonTypeLeft, "Exit", subrem_scene_fw_warning_widget_callback, app); + widget_add_button_element( + widget, GuiButtonTypeRight, "Continue", subrem_scene_fw_warning_widget_callback, app); + widget_add_string_element( + widget, 64, 12, AlignCenter, AlignBottom, FontPrimary, "Not official FW"); + widget_add_string_multiline_element( + widget, + 64, + 32, + AlignCenter, + AlignCenter, + FontSecondary, + "You are using custom firmware\nPlease download a compatible\nversion of the application"); + break; + + case SceneFwWarningStateAccept: + widget_add_button_element( + widget, GuiButtonTypeLeft, "Exit", subrem_scene_fw_warning_widget_callback, app); + widget_add_button_element( + widget, GuiButtonTypeRight, "Accept", subrem_scene_fw_warning_widget_callback, app); + widget_add_string_element( + widget, 64, 12, AlignCenter, AlignBottom, FontPrimary, "Not official FW"); + widget_add_string_multiline_element( + widget, + 64, + 32, + AlignCenter, + AlignCenter, + FontSecondary, + "Yes, I understand that\nthe application can\nbreak my subghz key file"); + break; + } +} + +void subrem_scene_fw_warning_on_enter(void* context) { + furi_assert(context); + + SubGhzRemoteApp* app = context; + + scene_manager_set_scene_state( + app->scene_manager, SubRemSceneFwWarning, SceneFwWarningStateAttention); + + subrem_scene_fw_warning_widget_render(app, SceneFwWarningStateAttention); + + view_dispatcher_switch_to_view(app->view_dispatcher, SubRemViewIDWidget); +} + +bool subrem_scene_fw_warning_on_event(void* context, SceneManagerEvent event) { + furi_assert(context); + + SubGhzRemoteApp* app = context; + bool consumed = false; + + if(event.type == SceneManagerEventTypeBack) { + consumed = true; + } else if(event.type == SceneManagerEventTypeCustom) { + if(event.event == SubRemCustomEventSceneFwWarningExit) { + scene_manager_stop(app->scene_manager); + view_dispatcher_stop(app->view_dispatcher); + consumed = true; + } else if(event.event == SubRemCustomEventSceneFwWarningNext) { + scene_manager_set_scene_state( + app->scene_manager, SubRemSceneFwWarning, SceneFwWarningStateAccept); + subrem_scene_fw_warning_widget_render(app, SceneFwWarningStateAccept); + view_dispatcher_switch_to_view(app->view_dispatcher, SubRemViewIDWidget); + consumed = true; + } else if(event.event == SubRemCustomEventSceneFwWarningContinue) { + scene_manager_previous_scene(app->scene_manager); + consumed = true; + } + } + + return consumed; +} + +void subrem_scene_fw_warning_on_exit(void* context) { + furi_assert(context); + + SubGhzRemoteApp* app = context; + widget_reset(app->widget); +} +#endif \ No newline at end of file diff --git a/subghz_remote/scenes/subrem_scene_open_map_file.c b/subghz_remote/scenes/subrem_scene_open_map_file.c new file mode 100644 index 00000000000..b91a3512954 --- /dev/null +++ b/subghz_remote/scenes/subrem_scene_open_map_file.c @@ -0,0 +1,29 @@ +#include "../subghz_remote_app_i.h" + +void subrem_scene_open_map_file_on_enter(void* context) { + furi_assert(context); + SubGhzRemoteApp* app = context; + + SubRemLoadMapState load_state = subrem_load_from_file(app); + uint32_t start_scene_state = + scene_manager_get_scene_state(app->scene_manager, SubRemSceneStart); + + if(load_state == SubRemLoadMapStateBack) { + scene_manager_previous_scene(app->scene_manager); + } else if(start_scene_state == SubmenuIndexSubRemEditMapFile) { + scene_manager_set_scene_state(app->scene_manager, SubRemSceneEditMenu, SubRemSubKeyNameUp); + scene_manager_next_scene(app->scene_manager, SubRemSceneEditMenu); + } else if(start_scene_state == SubmenuIndexSubRemOpenMapFile) { + scene_manager_next_scene(app->scene_manager, SubRemSceneRemote); + } +} + +bool subrem_scene_open_map_file_on_event(void* context, SceneManagerEvent event) { + UNUSED(context); + UNUSED(event); + return false; +} + +void subrem_scene_open_map_file_on_exit(void* context) { + UNUSED(context); +} diff --git a/subghz_remote/scenes/subrem_scene_open_sub_file.c b/subghz_remote/scenes/subrem_scene_open_sub_file.c new file mode 100644 index 00000000000..d34f01b99c5 --- /dev/null +++ b/subghz_remote/scenes/subrem_scene_open_sub_file.c @@ -0,0 +1,119 @@ +#include "../subghz_remote_app_i.h" + +void subrem_scene_open_sub_file_error_popup_callback(void* context) { + SubGhzRemoteApp* app = context; + view_dispatcher_send_custom_event( + app->view_dispatcher, SubRemCustomEventSceneEditOpenSubErrorPopup); +} + +SubRemLoadSubState subrem_scene_open_sub_file_dialog(SubGhzRemoteApp* app) { + furi_assert(app); + + SubRemSubFilePreset* sub = app->map_preset->subs_preset[app->chosen_sub]; + + FuriString* temp_file_path = furi_string_alloc(); + + if(furi_string_empty(sub->file_path)) { + furi_string_set(temp_file_path, SUBGHZ_RAW_FOLDER); + } else { + furi_string_set(temp_file_path, sub->file_path); + } + + SubRemLoadSubState ret = SubRemLoadSubStateNotSet; + + DialogsFileBrowserOptions browser_options; + + dialog_file_browser_set_basic_options( + &browser_options, SUBGHZ_APP_FILENAME_EXTENSION, &I_sub1_10px); + browser_options.base_path = SUBGHZ_RAW_FOLDER; + + // Input events and views are managed by file_select + if(!dialog_file_browser_show(app->dialogs, temp_file_path, temp_file_path, &browser_options)) { + } else { + // Check sub file + SubRemSubFilePreset* sub_candidate = subrem_sub_file_preset_alloc(); + furi_string_set(sub_candidate->label, sub->label); + furi_string_set(sub_candidate->file_path, temp_file_path); + + Storage* storage = furi_record_open(RECORD_STORAGE); + FlipperFormat* fff_file = flipper_format_file_alloc(storage); + + if(flipper_format_file_open_existing( + fff_file, furi_string_get_cstr(sub_candidate->file_path))) { + ret = subrem_sub_preset_load(sub_candidate, app->txrx, fff_file); + } + + flipper_format_file_close(fff_file); + flipper_format_free(fff_file); + furi_record_close(RECORD_STORAGE); + + if(ret == SubRemLoadSubStateOK) { + subrem_sub_file_preset_free(app->map_preset->subs_preset[app->chosen_sub]); + app->map_preset->subs_preset[app->chosen_sub] = sub_candidate; + app->map_not_saved = true; + } else { + subrem_sub_file_preset_free(sub_candidate); + } + } + + furi_string_free(temp_file_path); + + return ret; +} + +void subrem_scene_open_sub_file_on_enter(void* context) { + furi_assert(context); + SubGhzRemoteApp* app = context; + + SubRemLoadSubState load_state = subrem_scene_open_sub_file_dialog(app); + + Popup* popup = app->popup; + // popup_set_icon(); + popup_set_header(popup, "ERROR", 63, 16, AlignCenter, AlignBottom); + popup_set_timeout(popup, 1500); + popup_set_context(popup, app); + popup_set_callback(popup, subrem_scene_open_sub_file_error_popup_callback); + popup_enable_timeout(popup); + + if(load_state == SubRemLoadSubStateOK) { + scene_manager_previous_scene(app->scene_manager); + } else if(load_state == SubRemLoadSubStateNotSet) { + scene_manager_previous_scene(app->scene_manager); + } else { + switch(load_state) { + case SubRemLoadSubStateErrorFreq: + + popup_set_text(popup, "Bad frequency", 63, 30, AlignCenter, AlignBottom); + break; + case SubRemLoadSubStateErrorMod: + + popup_set_text(popup, "Bad modulation", 63, 30, AlignCenter, AlignBottom); + break; + case SubRemLoadSubStateErrorProtocol: + + popup_set_text(popup, "Unsupported protocol", 63, 30, AlignCenter, AlignBottom); + break; + + default: + break; + } + view_dispatcher_switch_to_view(app->view_dispatcher, SubRemViewIDPopup); + } +} + +bool subrem_scene_open_sub_file_on_event(void* context, SceneManagerEvent event) { + SubGhzRemoteApp* app = context; + + if(event.type == SceneManagerEventTypeCustom && + event.event == SubRemCustomEventSceneEditOpenSubErrorPopup) { + scene_manager_previous_scene(app->scene_manager); + return true; + } + return false; +} + +void subrem_scene_open_sub_file_on_exit(void* context) { + SubGhzRemoteApp* app = context; + + popup_reset(app->popup); +} diff --git a/subghz_remote/scenes/subrem_scene_remote.c b/subghz_remote/scenes/subrem_scene_remote.c new file mode 100644 index 00000000000..efe289871a8 --- /dev/null +++ b/subghz_remote/scenes/subrem_scene_remote.c @@ -0,0 +1,118 @@ +#include "../subghz_remote_app_i.h" +#include "../views/remote.h" + +#include + +#define TAG "SubRemScenRemote" + +void subrem_scene_remote_callback(SubRemCustomEvent event, void* context) { + furi_assert(context); + SubGhzRemoteApp* app = context; + view_dispatcher_send_custom_event(app->view_dispatcher, event); +} + +void subrem_scene_remote_raw_callback_end_tx(void* context) { + furi_assert(context); + SubGhzRemoteApp* app = context; + view_dispatcher_send_custom_event(app->view_dispatcher, SubRemCustomEventViewRemoteForcedStop); +} + +static uint8_t subrem_scene_remote_event_to_index(SubRemCustomEvent event_id) { + uint8_t ret = 0; + + if(event_id == SubRemCustomEventViewRemoteStartUP) { + ret = SubRemSubKeyNameUp; + } else if(event_id == SubRemCustomEventViewRemoteStartDOWN) { + ret = SubRemSubKeyNameDown; + } else if(event_id == SubRemCustomEventViewRemoteStartLEFT) { + ret = SubRemSubKeyNameLeft; + } else if(event_id == SubRemCustomEventViewRemoteStartRIGHT) { + ret = SubRemSubKeyNameRight; + } else if(event_id == SubRemCustomEventViewRemoteStartOK) { + ret = SubRemSubKeyNameOk; + } + + return ret; +} + +void subrem_scene_remote_on_enter(void* context) { + SubGhzRemoteApp* app = context; + + subrem_view_remote_update_data_labels(app->subrem_remote_view, app->map_preset->subs_preset); + subrem_view_remote_set_radio( + app->subrem_remote_view, + subghz_txrx_radio_device_get(app->txrx) != SubGhzRadioDeviceTypeInternal); + + subrem_view_remote_set_callback(app->subrem_remote_view, subrem_scene_remote_callback, app); + + view_dispatcher_switch_to_view(app->view_dispatcher, SubRemViewIDRemote); +} + +bool subrem_scene_remote_on_event(void* context, SceneManagerEvent event) { + SubGhzRemoteApp* app = context; + if(event.type == SceneManagerEventTypeCustom) { + if(event.event == SubRemCustomEventViewRemoteBack) { + if(!scene_manager_previous_scene(app->scene_manager)) { + scene_manager_stop(app->scene_manager); + view_dispatcher_stop(app->view_dispatcher); + } + return true; + } else if( + event.event == SubRemCustomEventViewRemoteStartUP || + event.event == SubRemCustomEventViewRemoteStartDOWN || + event.event == SubRemCustomEventViewRemoteStartLEFT || + event.event == SubRemCustomEventViewRemoteStartRIGHT || + event.event == SubRemCustomEventViewRemoteStartOK) { + // Start sending sub + subrem_tx_stop_sub(app, true); + + uint8_t chosen_sub = subrem_scene_remote_event_to_index(event.event); + app->chosen_sub = chosen_sub; + + subrem_view_remote_set_state( + app->subrem_remote_view, SubRemViewRemoteStateLoading, chosen_sub); + + if(subrem_tx_start_sub(app, app->map_preset->subs_preset[chosen_sub])) { + if(app->map_preset->subs_preset[chosen_sub]->type == SubGhzProtocolTypeRAW) { + subghz_txrx_set_raw_file_encoder_worker_callback_end( + app->txrx, subrem_scene_remote_raw_callback_end_tx, app); + } + subrem_view_remote_set_state( + app->subrem_remote_view, SubRemViewRemoteStateSending, chosen_sub); + notification_message(app->notifications, &sequence_blink_start_magenta); + } else { + subrem_view_remote_set_state( + app->subrem_remote_view, SubRemViewRemoteStateIdle, 0); + notification_message(app->notifications, &sequence_blink_red_100); + } + return true; + } else if(event.event == SubRemCustomEventViewRemoteForcedStop) { + subrem_tx_stop_sub(app, true); + subrem_view_remote_set_state(app->subrem_remote_view, SubRemViewRemoteStateIdle, 0); + + notification_message(app->notifications, &sequence_blink_stop); + return true; + } else if(event.event == SubRemCustomEventViewRemoteStop) { + if(subrem_tx_stop_sub(app, false)) { + subrem_view_remote_set_state( + app->subrem_remote_view, SubRemViewRemoteStateIdle, 0); + + notification_message(app->notifications, &sequence_blink_stop); + } + return true; + } + } + // } else if(event.type == SceneManagerEventTypeTick) { + // } + return false; +} + +void subrem_scene_remote_on_exit(void* context) { + SubGhzRemoteApp* app = context; + + subrem_tx_stop_sub(app, true); + + subrem_view_remote_set_state(app->subrem_remote_view, SubRemViewRemoteStateIdle, 0); + + notification_message(app->notifications, &sequence_blink_stop); +} diff --git a/subghz_remote/scenes/subrem_scene_start.c b/subghz_remote/scenes/subrem_scene_start.c new file mode 100644 index 00000000000..0f3399b7c42 --- /dev/null +++ b/subghz_remote/scenes/subrem_scene_start.c @@ -0,0 +1,100 @@ +#include "../subghz_remote_app_i.h" +#include "../helpers/subrem_custom_event.h" + +void subrem_scene_start_submenu_callback(void* context, uint32_t index) { + furi_assert(context); + SubGhzRemoteApp* app = context; + + view_dispatcher_send_custom_event(app->view_dispatcher, index); +} + +void subrem_scene_start_on_enter(void* context) { + furi_assert(context); + + SubGhzRemoteApp* app = context; + Submenu* submenu = app->submenu; + submenu_add_item( + submenu, + "Open Map File", + SubmenuIndexSubRemOpenMapFile, + subrem_scene_start_submenu_callback, + app); +#if FURI_DEBUG + submenu_add_item( + submenu, + "Remote_Debug", + SubmenuIndexSubRemRemoteView, + subrem_scene_start_submenu_callback, + app); +#endif + submenu_add_item( + submenu, + "Edit Map File", + SubmenuIndexSubRemEditMapFile, + subrem_scene_start_submenu_callback, + app); + submenu_add_item( + submenu, + "New Map File", + SubmenuIndexSubRemNewMapFile, + subrem_scene_start_submenu_callback, + app); + // submenu_add_item( + // submenu, + // "About", + // SubmenuIndexSubGhzRemoteAbout, + // subrem_scene_start_submenu_callback, + // app); + + submenu_set_selected_item( + submenu, scene_manager_get_scene_state(app->scene_manager, SubRemSceneStart)); + + view_dispatcher_switch_to_view(app->view_dispatcher, SubRemViewIDSubmenu); +} + +bool subrem_scene_start_on_event(void* context, SceneManagerEvent event) { + furi_assert(context); + + SubGhzRemoteApp* app = context; + bool consumed = false; + + if(event.type == SceneManagerEventTypeCustom) { + if(event.event == SubmenuIndexSubRemOpenMapFile) { + scene_manager_set_scene_state( + app->scene_manager, SubRemSceneStart, SubmenuIndexSubRemOpenMapFile); + + scene_manager_next_scene(app->scene_manager, SubRemSceneOpenMapFile); + consumed = true; + } +#if FURI_DEBUG + else if(event.event == SubmenuIndexSubRemRemoteView) { + scene_manager_next_scene(app->scene_manager, SubRemSceneRemote); + consumed = true; + } +#endif + else if(event.event == SubmenuIndexSubRemEditMapFile) { + scene_manager_set_scene_state( + app->scene_manager, SubRemSceneStart, SubmenuIndexSubRemEditMapFile); + scene_manager_next_scene(app->scene_manager, SubRemSceneOpenMapFile); + consumed = true; + } else if(event.event == SubmenuIndexSubRemNewMapFile) { + scene_manager_set_scene_state( + app->scene_manager, SubRemSceneStart, SubmenuIndexSubRemNewMapFile); + scene_manager_next_scene(app->scene_manager, SubRemSceneEnterNewName); + consumed = true; + } + // } else if(event.event == SubmenuIndexSubRemAbout) { + // scene_manager_next_scene(app->scene_manager, SubRemSceneAbout); + // consumed = true; + // } + } + + return consumed; +} + +void subrem_scene_start_on_exit(void* context) { + furi_assert(context); + + SubGhzRemoteApp* app = context; + submenu_reset(app->submenu); +} diff --git a/subghz_remote/subghz_remote_app.c b/subghz_remote/subghz_remote_app.c new file mode 100644 index 00000000000..473e3c01286 --- /dev/null +++ b/subghz_remote/subghz_remote_app.c @@ -0,0 +1,218 @@ +#include "subghz_remote_app_i.h" +#include + +static bool subghz_remote_app_custom_event_callback(void* context, uint32_t event) { + furi_assert(context); + SubGhzRemoteApp* app = context; + return scene_manager_handle_custom_event(app->scene_manager, event); +} + +static bool subghz_remote_app_back_event_callback(void* context) { + furi_assert(context); + SubGhzRemoteApp* app = context; + return scene_manager_handle_back_event(app->scene_manager); +} + +static void subghz_remote_app_tick_event_callback(void* context) { + furi_assert(context); + SubGhzRemoteApp* app = context; + scene_manager_handle_tick_event(app->scene_manager); +} + +static void subghz_remote_make_app_folder(SubGhzRemoteApp* app) { + furi_assert(app); + + Storage* storage = furi_record_open(RECORD_STORAGE); + + // Migrate old users data + storage_common_migrate(storage, EXT_PATH("unirf"), SUBREM_APP_FOLDER); + + if(!storage_simply_mkdir(storage, SUBREM_APP_FOLDER)) { + // FURI_LOG_E(TAG, "Could not create folder %s", SUBREM_APP_FOLDER); + dialog_message_show_storage_error(app->dialogs, "Cannot create\napp folder"); + } + furi_record_close(RECORD_STORAGE); +} + +SubGhzRemoteApp* subghz_remote_app_alloc() { + SubGhzRemoteApp* app = malloc(sizeof(SubGhzRemoteApp)); + + furi_hal_power_suppress_charge_enter(); + + app->file_path = furi_string_alloc(); + furi_string_set(app->file_path, SUBREM_APP_FOLDER); + + // GUI + app->gui = furi_record_open(RECORD_GUI); + + // View Dispatcher + app->view_dispatcher = view_dispatcher_alloc(); + + app->scene_manager = scene_manager_alloc(&subrem_scene_handlers, app); + view_dispatcher_enable_queue(app->view_dispatcher); + + view_dispatcher_set_event_callback_context(app->view_dispatcher, app); + view_dispatcher_set_custom_event_callback( + app->view_dispatcher, subghz_remote_app_custom_event_callback); + view_dispatcher_set_navigation_event_callback( + app->view_dispatcher, subghz_remote_app_back_event_callback); + view_dispatcher_set_tick_event_callback( + app->view_dispatcher, subghz_remote_app_tick_event_callback, 100); + + view_dispatcher_attach_to_gui(app->view_dispatcher, app->gui, ViewDispatcherTypeFullscreen); + + // Open Notification record + app->notifications = furi_record_open(RECORD_NOTIFICATION); + + // SubMenu + app->submenu = submenu_alloc(); + view_dispatcher_add_view( + app->view_dispatcher, SubRemViewIDSubmenu, submenu_get_view(app->submenu)); + + // Dialog + app->dialogs = furi_record_open(RECORD_DIALOGS); + + // TextInput + app->text_input = text_input_alloc(); + view_dispatcher_add_view( + app->view_dispatcher, SubRemViewIDTextInput, text_input_get_view(app->text_input)); + + // Widget + app->widget = widget_alloc(); + view_dispatcher_add_view( + app->view_dispatcher, SubRemViewIDWidget, widget_get_view(app->widget)); + + // Popup + app->popup = popup_alloc(); + view_dispatcher_add_view(app->view_dispatcher, SubRemViewIDPopup, popup_get_view(app->popup)); + + // Remote view + app->subrem_remote_view = subrem_view_remote_alloc(); + view_dispatcher_add_view( + app->view_dispatcher, + SubRemViewIDRemote, + subrem_view_remote_get_view(app->subrem_remote_view)); + + // Edit Menu view + app->subrem_edit_menu = subrem_view_edit_menu_alloc(); + view_dispatcher_add_view( + app->view_dispatcher, + SubRemViewIDEditMenu, + subrem_view_edit_menu_get_view(app->subrem_edit_menu)); + + app->map_preset = malloc(sizeof(SubRemMapPreset)); + for(uint8_t i = 0; i < SubRemSubKeyNameMaxCount; i++) { + app->map_preset->subs_preset[i] = subrem_sub_file_preset_alloc(); + } + + app->txrx = subghz_txrx_alloc(); + + subghz_txrx_set_need_save_callback(app->txrx, subrem_save_active_sub, app); + + app->map_not_saved = false; + + return app; +} + +void subghz_remote_app_free(SubGhzRemoteApp* app) { + furi_assert(app); + + furi_hal_power_suppress_charge_exit(); + + // Submenu + view_dispatcher_remove_view(app->view_dispatcher, SubRemViewIDSubmenu); + submenu_free(app->submenu); + + // Dialog + furi_record_close(RECORD_DIALOGS); + + // TextInput + view_dispatcher_remove_view(app->view_dispatcher, SubRemViewIDTextInput); + text_input_free(app->text_input); + + // Widget + view_dispatcher_remove_view(app->view_dispatcher, SubRemViewIDWidget); + widget_free(app->widget); + + // Popup + view_dispatcher_remove_view(app->view_dispatcher, SubRemViewIDPopup); + popup_free(app->popup); + + // Remote view + view_dispatcher_remove_view(app->view_dispatcher, SubRemViewIDRemote); + subrem_view_remote_free(app->subrem_remote_view); + + // Edit view + view_dispatcher_remove_view(app->view_dispatcher, SubRemViewIDEditMenu); + subrem_view_edit_menu_free(app->subrem_edit_menu); + + scene_manager_free(app->scene_manager); + view_dispatcher_free(app->view_dispatcher); + + subghz_txrx_free(app->txrx); + + for(uint8_t i = 0; i < SubRemSubKeyNameMaxCount; i++) { + subrem_sub_file_preset_free(app->map_preset->subs_preset[i]); + } + free(app->map_preset); + + // Notifications + furi_record_close(RECORD_NOTIFICATION); + app->notifications = NULL; + + // Close records + furi_record_close(RECORD_GUI); + + // Path strings + furi_string_free(app->file_path); + + free(app); +} + +int32_t subghz_remote_app(void* arg) { + SubGhzRemoteApp* subghz_remote_app = subghz_remote_app_alloc(); + + subghz_remote_make_app_folder(subghz_remote_app); + + bool map_loaded = false; +#ifdef FW_ORIGIN_Official + const bool fw_ofw = strcmp(version_get_firmware_origin(version_get()), "Official") == 0; +#endif + if((arg != NULL) && (strlen(arg) != 0)) { + furi_string_set(subghz_remote_app->file_path, (const char*)arg); + SubRemLoadMapState load_state = subrem_map_file_load( + subghz_remote_app, furi_string_get_cstr(subghz_remote_app->file_path)); + + if(load_state == SubRemLoadMapStateOK || load_state == SubRemLoadMapStateNotAllOK) { + map_loaded = true; + } else { + // TODO Replace + dialog_message_show_storage_error(subghz_remote_app->dialogs, "Cannot load\nmap file"); + } + } + + if(map_loaded) { + scene_manager_next_scene(subghz_remote_app->scene_manager, SubRemSceneRemote); + } else { + furi_string_set(subghz_remote_app->file_path, SUBREM_APP_FOLDER); + scene_manager_next_scene(subghz_remote_app->scene_manager, SubRemSceneStart); +#ifdef FW_ORIGIN_Official + if(fw_ofw) { + scene_manager_next_scene(subghz_remote_app->scene_manager, SubRemSceneOpenMapFile); + } + } + + if(!fw_ofw) { + scene_manager_next_scene(subghz_remote_app->scene_manager, SubRemSceneFwWarning); + } +#else + scene_manager_next_scene(subghz_remote_app->scene_manager, SubRemSceneOpenMapFile); + } +#endif + + view_dispatcher_run(subghz_remote_app->view_dispatcher); + + subghz_remote_app_free(subghz_remote_app); + + return 0; +} diff --git a/subghz_remote/subghz_remote_app_i.c b/subghz_remote/subghz_remote_app_i.c new file mode 100644 index 00000000000..45321441d4f --- /dev/null +++ b/subghz_remote/subghz_remote_app_i.c @@ -0,0 +1,317 @@ +#include "subghz_remote_app_i.h" +#include +#include + +#include "helpers/txrx/subghz_txrx.h" +#ifndef FW_ORIGIN_Official +#include +#endif + +#define TAG "SubGhzRemote" + +static const char* map_file_labels[SubRemSubKeyNameMaxCount][2] = { + [SubRemSubKeyNameUp] = {"UP", "ULABEL"}, + [SubRemSubKeyNameDown] = {"DOWN", "DLABEL"}, + [SubRemSubKeyNameLeft] = {"LEFT", "LLABEL"}, + [SubRemSubKeyNameRight] = {"RIGHT", "RLABEL"}, + [SubRemSubKeyNameOk] = {"OK", "OKLABEL"}, +}; + +void subrem_map_preset_reset(SubRemMapPreset* map_preset) { + furi_assert(map_preset); + + for(uint8_t i = 0; i < SubRemSubKeyNameMaxCount; i++) { + subrem_sub_file_preset_reset(map_preset->subs_preset[i]); + } +} + +static SubRemLoadMapState subrem_map_preset_check( + SubRemMapPreset* map_preset, + SubGhzTxRx* txrx, + FlipperFormat* fff_data_file) { + furi_assert(map_preset); + furi_assert(txrx); + + bool all_loaded = true; + SubRemLoadMapState ret = SubRemLoadMapStateErrorBrokenFile; + + SubRemLoadSubState sub_loadig_state; + SubRemSubFilePreset* sub_preset; + + for(uint8_t i = 0; i < SubRemSubKeyNameMaxCount; i++) { + sub_preset = map_preset->subs_preset[i]; + + sub_loadig_state = SubRemLoadSubStateErrorNoFile; + + if(furi_string_empty(sub_preset->file_path)) { + // FURI_LOG_I(TAG, "Empty file path"); + } else if(!flipper_format_file_open_existing( + fff_data_file, furi_string_get_cstr(sub_preset->file_path))) { + sub_preset->load_state = SubRemLoadSubStateErrorNoFile; + FURI_LOG_W(TAG, "Error open file %s", furi_string_get_cstr(sub_preset->file_path)); + } else { + sub_loadig_state = subrem_sub_preset_load(sub_preset, txrx, fff_data_file); + } + + if(sub_loadig_state != SubRemLoadSubStateOK) { + all_loaded = false; + } else { + ret = SubRemLoadMapStateNotAllOK; + } + + if(ret != SubRemLoadMapStateErrorBrokenFile && all_loaded) { + ret = SubRemLoadMapStateOK; + } + + flipper_format_file_close(fff_data_file); + } + + return ret; +} + +static bool subrem_map_preset_load(SubRemMapPreset* map_preset, FlipperFormat* fff_data_file) { + furi_assert(map_preset); + bool ret = false; + SubRemSubFilePreset* sub_preset; + for(uint8_t i = 0; i < SubRemSubKeyNameMaxCount; i++) { + sub_preset = map_preset->subs_preset[i]; + if(!flipper_format_read_string( + fff_data_file, map_file_labels[i][0], sub_preset->file_path)) { +#if FURI_DEBUG + FURI_LOG_W(TAG, "No file patch for %s", map_file_labels[i][0]); +#endif + sub_preset->type = SubGhzProtocolTypeUnknown; + } else if(!path_contains_only_ascii(furi_string_get_cstr(sub_preset->file_path))) { + FURI_LOG_E(TAG, "Incorrect characters in [%s] file path", map_file_labels[i][0]); + sub_preset->type = SubGhzProtocolTypeUnknown; + } else if(!flipper_format_rewind(fff_data_file)) { + // Rewind error + } else if(!flipper_format_read_string( + fff_data_file, map_file_labels[i][1], sub_preset->label)) { +#if FURI_DEBUG + FURI_LOG_W(TAG, "No Label for %s", map_file_labels[i][0]); +#endif + ret = true; + } else { + ret = true; + } + if(ret) { + // Preload seccesful + FURI_LOG_I( + TAG, + "%-5s: %s %s", + map_file_labels[i][0], + furi_string_get_cstr(sub_preset->label), + furi_string_get_cstr(sub_preset->file_path)); + sub_preset->load_state = SubRemLoadSubStatePreloaded; + } + + flipper_format_rewind(fff_data_file); + } + return ret; +} + +SubRemLoadMapState subrem_map_file_load(SubGhzRemoteApp* app, const char* file_path) { + furi_assert(app); + furi_assert(file_path); +#if FURI_DEBUG + FURI_LOG_I(TAG, "Load Map File Start"); +#endif + Storage* storage = furi_record_open(RECORD_STORAGE); + FlipperFormat* fff_data_file = flipper_format_file_alloc(storage); + SubRemLoadMapState ret = SubRemLoadMapStateErrorOpenError; +#if FURI_DEBUG + FURI_LOG_I(TAG, "Open Map File.."); +#endif + subrem_map_preset_reset(app->map_preset); + + if(!flipper_format_file_open_existing(fff_data_file, file_path)) { + FURI_LOG_E(TAG, "Could not open MAP file %s", file_path); + ret = SubRemLoadMapStateErrorOpenError; + } else { + if(!subrem_map_preset_load(app->map_preset, fff_data_file)) { + FURI_LOG_E(TAG, "Could no Sub file path in MAP file"); + // ret = // error for popup + } else if(!flipper_format_file_close(fff_data_file)) { + ret = SubRemLoadMapStateErrorOpenError; + } else { + ret = subrem_map_preset_check(app->map_preset, app->txrx, fff_data_file); + } + } + + if(ret == SubRemLoadMapStateOK) { + FURI_LOG_I(TAG, "Load Map File Seccesful"); + } else if(ret == SubRemLoadMapStateNotAllOK) { + FURI_LOG_I(TAG, "Load Map File Seccesful [Not all files]"); + } else { + FURI_LOG_E(TAG, "Broken Map File"); + } + + flipper_format_file_close(fff_data_file); + flipper_format_free(fff_data_file); + + furi_record_close(RECORD_STORAGE); + return ret; +} + +bool subrem_save_protocol_to_file(FlipperFormat* flipper_format, const char* sub_file_name) { + furi_assert(flipper_format); + furi_assert(sub_file_name); + + Storage* storage = furi_record_open(RECORD_STORAGE); + Stream* flipper_format_stream = flipper_format_get_raw_stream(flipper_format); + + bool saved = false; + uint32_t repeat = 200; + FuriString* file_dir = furi_string_alloc(); + + path_extract_dirname(sub_file_name, file_dir); + do { + // removing additional fields + flipper_format_delete_key(flipper_format, "Repeat"); + // flipper_format_delete_key(flipper_format, "Manufacture"); + + if(!storage_simply_remove(storage, sub_file_name)) { + break; + } + + //ToDo check Write + stream_seek(flipper_format_stream, 0, StreamOffsetFromStart); + stream_save_to_file(flipper_format_stream, storage, sub_file_name, FSOM_CREATE_ALWAYS); + + if(!flipper_format_insert_or_update_uint32(flipper_format, "Repeat", &repeat, 1)) { + FURI_LOG_E(TAG, "Unable Repeat"); + break; + } + + saved = true; + } while(0); + + furi_string_free(file_dir); + furi_record_close(RECORD_STORAGE); + return saved; +} + +void subrem_save_active_sub(void* context) { + furi_assert(context); + SubGhzRemoteApp* app = context; + + SubRemSubFilePreset* sub_preset = app->map_preset->subs_preset[app->chosen_sub]; + subrem_save_protocol_to_file( + sub_preset->fff_data, furi_string_get_cstr(sub_preset->file_path)); +} + +bool subrem_tx_start_sub(SubGhzRemoteApp* app, SubRemSubFilePreset* sub_preset) { + furi_assert(app); + furi_assert(sub_preset); + bool ret = false; + + subrem_tx_stop_sub(app, true); + + if(sub_preset->type == SubGhzProtocolTypeUnknown) { + ret = false; + } else { + FURI_LOG_I(TAG, "Send %s", furi_string_get_cstr(sub_preset->label)); + + subghz_txrx_load_decoder_by_name_protocol( + app->txrx, furi_string_get_cstr(sub_preset->protocaol_name)); + + subghz_txrx_set_preset( + app->txrx, + furi_string_get_cstr(sub_preset->freq_preset.name), + sub_preset->freq_preset.frequency, + NULL, + 0); +#ifndef FW_ORIGIN_Official + subghz_custom_btns_reset(); +#endif + if(subghz_txrx_tx_start(app->txrx, sub_preset->fff_data) == SubGhzTxRxStartTxStateOk) { + ret = true; + } + } + + return ret; +} + +bool subrem_tx_stop_sub(SubGhzRemoteApp* app, bool forced) { + furi_assert(app); + SubRemSubFilePreset* sub_preset = app->map_preset->subs_preset[app->chosen_sub]; + + if(forced || (sub_preset->type != SubGhzProtocolTypeRAW)) { + subghz_txrx_stop(app->txrx); +#ifndef FW_ORIGIN_Official + if(sub_preset->type == SubGhzProtocolTypeDynamic) { + subghz_txrx_reset_dynamic_and_custom_btns(app->txrx); + } + subghz_custom_btns_reset(); +#endif + return true; + } + + return false; +} + +SubRemLoadMapState subrem_load_from_file(SubGhzRemoteApp* app) { + furi_assert(app); + + FuriString* file_path = furi_string_alloc(); + SubRemLoadMapState ret = SubRemLoadMapStateBack; + + DialogsFileBrowserOptions browser_options; + dialog_file_browser_set_basic_options(&browser_options, SUBREM_APP_EXTENSION, &I_subrem_10px); + browser_options.base_path = SUBREM_APP_FOLDER; + + // Input events and views are managed by file_select + if(!dialog_file_browser_show(app->dialogs, app->file_path, app->file_path, &browser_options)) { + } else { + ret = subrem_map_file_load(app, furi_string_get_cstr(app->file_path)); + } + + furi_string_free(file_path); + + return ret; +} + +bool subrem_save_map_to_file(SubGhzRemoteApp* app) { + furi_assert(app); + + const char* file_name = furi_string_get_cstr(app->file_path); + bool saved = false; + FlipperFormat* fff_data = flipper_format_string_alloc(); + + SubRemSubFilePreset* sub_preset; + + flipper_format_write_header_cstr( + fff_data, SUBREM_APP_APP_FILE_TYPE, SUBREM_APP_APP_FILE_VERSION); + for(uint8_t i = 0; i < SubRemSubKeyNameMaxCount; i++) { + sub_preset = app->map_preset->subs_preset[i]; + if(!furi_string_empty(sub_preset->file_path)) { + flipper_format_write_string(fff_data, map_file_labels[i][0], sub_preset->file_path); + } + } + for(uint8_t i = 0; i < SubRemSubKeyNameMaxCount; i++) { + sub_preset = app->map_preset->subs_preset[i]; + if(!furi_string_empty(sub_preset->file_path)) { + flipper_format_write_string(fff_data, map_file_labels[i][1], sub_preset->label); + } + } + + Storage* storage = furi_record_open(RECORD_STORAGE); + Stream* flipper_format_stream = flipper_format_get_raw_stream(fff_data); + + do { + if(!storage_simply_remove(storage, file_name)) { + break; + } + //ToDo check Write + stream_seek(flipper_format_stream, 0, StreamOffsetFromStart); + stream_save_to_file(flipper_format_stream, storage, file_name, FSOM_CREATE_ALWAYS); + + saved = true; + } while(0); + + furi_record_close(RECORD_STORAGE); + flipper_format_free(fff_data); + + return saved; +} \ No newline at end of file diff --git a/subghz_remote/subghz_remote_app_i.h b/subghz_remote/subghz_remote_app_i.h new file mode 100644 index 00000000000..af8f7d3a30a --- /dev/null +++ b/subghz_remote/subghz_remote_app_i.h @@ -0,0 +1,71 @@ +#pragma once + +#include "helpers/subrem_types.h" +#include "helpers/subrem_presets.h" +#include "scenes/subrem_scene.h" + +#include "helpers/txrx/subghz_txrx.h" + +// Fix importing from firmware for folder path +#if __has_include("subghz_remote_icons.h") +#include "subghz_remote_icons.h" +#endif + +#include "views/remote.h" +#include "views/edit_menu.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#define SUBREM_APP_FOLDER EXT_PATH("subghz/remote") +#define SUBREM_MAX_LEN_NAME 64 + +typedef struct { + Gui* gui; + ViewDispatcher* view_dispatcher; + SceneManager* scene_manager; + NotificationApp* notifications; + DialogsApp* dialogs; + Widget* widget; + Popup* popup; + TextInput* text_input; + Submenu* submenu; + + FuriString* file_path; + char file_name_tmp[SUBREM_MAX_LEN_NAME]; + + SubRemViewRemote* subrem_remote_view; + SubRemViewEditMenu* subrem_edit_menu; + + SubRemMapPreset* map_preset; + + SubGhzTxRx* txrx; + + bool map_not_saved; + + uint8_t chosen_sub; +} SubGhzRemoteApp; + +SubRemLoadMapState subrem_load_from_file(SubGhzRemoteApp* app); + +bool subrem_tx_start_sub(SubGhzRemoteApp* app, SubRemSubFilePreset* sub_preset); + +bool subrem_tx_stop_sub(SubGhzRemoteApp* app, bool forced); + +SubRemLoadMapState subrem_map_file_load(SubGhzRemoteApp* app, const char* file_path); + +void subrem_map_preset_reset(SubRemMapPreset* map_preset); + +bool subrem_save_map_to_file(SubGhzRemoteApp* app); + +void subrem_save_active_sub(void* context); \ No newline at end of file diff --git a/subghz_remote/views/edit_menu.c b/subghz_remote/views/edit_menu.c new file mode 100644 index 00000000000..91d826f033e --- /dev/null +++ b/subghz_remote/views/edit_menu.c @@ -0,0 +1,290 @@ +#include "edit_menu.h" +#include "../subghz_remote_app_i.h" + +#include +#include + +#define subrem_view_edit_menu_MAX_LABEL_LENGTH 12 + +#define FRAME_HEIGHT 12 + +struct SubRemViewEditMenu { + View* view; + SubRemViewEditMenuCallback callback; + void* context; +}; + +typedef struct { + FuriString* label; + FuriString* file_path; + SubRemLoadSubState sub_state; + + uint8_t chosen; +} SubRemViewEditMenuModel; + +void subrem_view_edit_menu_set_callback( + SubRemViewEditMenu* subrem_view_edit_menu, + SubRemViewEditMenuCallback callback, + void* context) { + furi_assert(subrem_view_edit_menu); + + subrem_view_edit_menu->callback = callback; + subrem_view_edit_menu->context = context; +} + +void subrem_view_edit_menu_add_data_to_show( + SubRemViewEditMenu* subrem_view_edit_remote, + uint8_t index, + FuriString* label, + FuriString* path, + SubRemLoadSubState state) { + furi_assert(subrem_view_edit_remote); + + with_view_model( + subrem_view_edit_remote->view, + SubRemViewEditMenuModel * model, + { + model->chosen = index; + if(!furi_string_empty(label)) { + furi_string_set(model->label, label); + } else { + furi_string_set(model->label, "Empty label"); + } + furi_string_set(model->file_path, path); + model->sub_state = state; + }, + true); +} + +uint8_t subrem_view_edit_menu_get_index(SubRemViewEditMenu* subrem_view_edit_remote) { + furi_assert(subrem_view_edit_remote); + uint8_t index; + + with_view_model( + subrem_view_edit_remote->view, + SubRemViewEditMenuModel * model, + { index = model->chosen; }, + true); + return index; +} + +void subrem_view_edit_menu_draw(Canvas* canvas, SubRemViewEditMenuModel* model) { + canvas_clear(canvas); + canvas_set_color(canvas, ColorBlack); + + canvas_clear(canvas); + + // Draw bottom btn + canvas_set_font(canvas, FontSecondary); + elements_button_left(canvas, "Back"); + elements_button_center(canvas, "Edit"); + elements_button_right(canvas, "Save"); + + // Draw top frame + canvas_draw_line(canvas, 1, 0, 125, 0); + canvas_draw_box(canvas, 0, 1, 127, FRAME_HEIGHT - 2); + canvas_draw_line(canvas, 1, FRAME_HEIGHT - 1, 125, FRAME_HEIGHT - 1); + + canvas_set_color(canvas, ColorWhite); + + // Draw btn name + canvas_set_font(canvas, FontPrimary); + switch(model->chosen) { + case SubRemSubKeyNameUp: + canvas_draw_str(canvas, 3, FRAME_HEIGHT - 2, "UP"); + break; + + case SubRemSubKeyNameDown: + canvas_draw_str(canvas, 3, FRAME_HEIGHT - 2, "DOWN"); + break; + + case SubRemSubKeyNameLeft: + canvas_draw_str(canvas, 3, FRAME_HEIGHT - 2, "LEFT"); + break; + + case SubRemSubKeyNameRight: + canvas_draw_str(canvas, 3, FRAME_HEIGHT - 2, "RIGHT"); + break; + + case SubRemSubKeyNameOk: + canvas_draw_str(canvas, 3, FRAME_HEIGHT - 2, "OK"); + break; + + default: + break; + } + + // Draw Label + canvas_set_font(canvas, FontSecondary); + elements_text_box( + canvas, + 38, + 2, + 127 - 38, + FRAME_HEIGHT, + AlignCenter, + AlignBottom, + furi_string_empty(model->label) ? "Empty label" : furi_string_get_cstr(model->label), + true); + + // Draw arrow + canvas_set_color(canvas, ColorBlack); + if(model->chosen != 0) { + canvas_draw_icon(canvas, 119, 13, &I_Pin_arrow_up_7x9); + } + if(model->chosen != 4) { + canvas_draw_icon_ex(canvas, 119, 42, &I_Pin_arrow_up_7x9, IconRotation180); + } + + // Draw file_path + if(model->sub_state == SubRemLoadSubStateOK) { + canvas_set_font(canvas, FontSecondary); + elements_text_box( + canvas, + 1, + FRAME_HEIGHT + 1, + 118, + (63 - FRAME_HEIGHT * 2), + AlignLeft, + AlignTop, + furi_string_get_cstr(model->file_path), + false); + } else if(furi_string_empty(model->file_path)) { + canvas_set_font(canvas, FontPrimary); + canvas_draw_str(canvas, 1, FRAME_HEIGHT * 2 - 2, "Button not set"); + } else { + canvas_set_font(canvas, FontPrimary); + canvas_draw_str(canvas, 1, FRAME_HEIGHT * 2 - 2, "ERR:"); + canvas_set_font(canvas, FontSecondary); + switch(model->sub_state) { + case SubRemLoadSubStateErrorNoFile: + canvas_draw_str(canvas, 26, FRAME_HEIGHT * 2 - 2, "File not found"); + break; + case SubRemLoadSubStateErrorFreq: + canvas_draw_str(canvas, 26, FRAME_HEIGHT * 2 - 2, "Bad frequency"); + break; + case SubRemLoadSubStateErrorMod: + canvas_draw_str(canvas, 26, FRAME_HEIGHT * 2 - 2, "Bad modulation"); + break; + case SubRemLoadSubStateErrorProtocol: + canvas_draw_str(canvas, 26, FRAME_HEIGHT * 2 - 2, "Unsupported protocol"); + break; + + default: + break; + } + elements_text_box( + canvas, + 1, + FRAME_HEIGHT * 2, + 118, + 30, + AlignLeft, + AlignTop, + furi_string_get_cstr(model->file_path), + false); + } +} + +bool subrem_view_edit_menu_input(InputEvent* event, void* context) { + furi_assert(context); + SubRemViewEditMenu* subrem_view_edit_menu = context; + + if((event->key == InputKeyBack || event->key == InputKeyLeft) && + event->type == InputTypeShort) { + subrem_view_edit_menu->callback( + SubRemCustomEventViewEditMenuBack, subrem_view_edit_menu->context); + return true; + } else if(event->key == InputKeyUp && event->type == InputTypeShort) { + with_view_model( + subrem_view_edit_menu->view, + SubRemViewEditMenuModel * model, + { + if(model->chosen > 0) { + model->chosen -= 1; + }; + }, + true); + subrem_view_edit_menu->callback( + SubRemCustomEventViewEditMenuUP, subrem_view_edit_menu->context); + return true; + } else if(event->key == InputKeyDown && event->type == InputTypeShort) { + with_view_model( + subrem_view_edit_menu->view, + SubRemViewEditMenuModel * model, + { + if(model->chosen < 4) { + model->chosen += 1; + }; + }, + true); + subrem_view_edit_menu->callback( + SubRemCustomEventViewEditMenuDOWN, subrem_view_edit_menu->context); + return true; + } else if(event->key == InputKeyOk && event->type == InputTypeShort) { + subrem_view_edit_menu->callback( + SubRemCustomEventViewEditMenuEdit, subrem_view_edit_menu->context); + return true; + } else if(event->key == InputKeyRight && event->type == InputTypeShort) { + subrem_view_edit_menu->callback( + SubRemCustomEventViewEditMenuSave, subrem_view_edit_menu->context); + return true; + } + + return true; +} + +void subrem_view_edit_menu_enter(void* context) { + furi_assert(context); +} + +void subrem_view_edit_menu_exit(void* context) { + furi_assert(context); +} + +SubRemViewEditMenu* subrem_view_edit_menu_alloc(void) { + SubRemViewEditMenu* subrem_view_edit_menu = malloc(sizeof(SubRemViewEditMenu)); + + // View allocation and configuration + subrem_view_edit_menu->view = view_alloc(); + view_allocate_model( + subrem_view_edit_menu->view, ViewModelTypeLocking, sizeof(SubRemViewEditMenuModel)); + view_set_context(subrem_view_edit_menu->view, subrem_view_edit_menu); + view_set_draw_callback( + subrem_view_edit_menu->view, (ViewDrawCallback)subrem_view_edit_menu_draw); + view_set_input_callback(subrem_view_edit_menu->view, subrem_view_edit_menu_input); + view_set_enter_callback(subrem_view_edit_menu->view, subrem_view_edit_menu_enter); + view_set_exit_callback(subrem_view_edit_menu->view, subrem_view_edit_menu_exit); + + with_view_model( + subrem_view_edit_menu->view, + SubRemViewEditMenuModel * model, + { + model->label = furi_string_alloc(); // furi_string_alloc_set_str("LABEL"); + model->file_path = furi_string_alloc(); // furi_string_alloc_set_str("FILE_PATH"); + + model->chosen = 0; + }, + true); + return subrem_view_edit_menu; +} + +void subrem_view_edit_menu_free(SubRemViewEditMenu* subghz_edit_menu) { + furi_assert(subghz_edit_menu); + + with_view_model( + subghz_edit_menu->view, + SubRemViewEditMenuModel * model, + { + furi_string_free(model->label); + furi_string_free(model->file_path); + }, + true); + view_free(subghz_edit_menu->view); + free(subghz_edit_menu); +} + +View* subrem_view_edit_menu_get_view(SubRemViewEditMenu* subrem_view_edit_menu) { + furi_assert(subrem_view_edit_menu); + return subrem_view_edit_menu->view; +} \ No newline at end of file diff --git a/subghz_remote/views/edit_menu.h b/subghz_remote/views/edit_menu.h new file mode 100644 index 00000000000..12d33e8ba14 --- /dev/null +++ b/subghz_remote/views/edit_menu.h @@ -0,0 +1,29 @@ +#pragma once + +#include +#include "../helpers/subrem_custom_event.h" +#include "../helpers/subrem_presets.h" + +typedef struct SubRemViewEditMenu SubRemViewEditMenu; + +typedef void (*SubRemViewEditMenuCallback)(SubRemCustomEvent event, void* context); + +void subrem_view_edit_menu_set_callback( + SubRemViewEditMenu* subrem_view_edit_menu, + SubRemViewEditMenuCallback callback, + void* context); + +SubRemViewEditMenu* subrem_view_edit_menu_alloc(void); + +void subrem_view_edit_menu_free(SubRemViewEditMenu* subrem_view_edit_menu); + +View* subrem_view_edit_menu_get_view(SubRemViewEditMenu* subrem_view_edit_menu); + +void subrem_view_edit_menu_add_data_to_show( + SubRemViewEditMenu* subrem_view_edit_remote, + uint8_t index, + FuriString* label, + FuriString* path, + SubRemLoadSubState state); + +uint8_t subrem_view_edit_menu_get_index(SubRemViewEditMenu* subrem_view_edit_remote); \ No newline at end of file diff --git a/subghz_remote/views/remote.c b/subghz_remote/views/remote.c new file mode 100644 index 00000000000..ca6df70a379 --- /dev/null +++ b/subghz_remote/views/remote.c @@ -0,0 +1,331 @@ +#include "remote.h" +#include "../subghz_remote_app_i.h" + +#include +#include + +#include + +#define SUBREM_VIEW_REMOTE_MAX_LABEL_LENGTH 30 +#define SUBREM_VIEW_REMOTE_LEFT_OFFSET 10 +#define SUBREM_VIEW_REMOTE_RIGHT_OFFSET 0 + +struct SubRemViewRemote { + View* view; + SubRemViewRemoteCallback callback; + void* context; +}; + +typedef struct { + char* labels[SubRemSubKeyNameMaxCount]; + + SubRemViewRemoteState state; + + uint8_t pressed_btn; + bool is_external; +} SubRemViewRemoteModel; + +void subrem_view_remote_set_callback( + SubRemViewRemote* subrem_view_remote, + SubRemViewRemoteCallback callback, + void* context) { + furi_assert(subrem_view_remote); + + subrem_view_remote->callback = callback; + subrem_view_remote->context = context; +} + +void subrem_view_remote_update_data_labels( + SubRemViewRemote* subrem_view_remote, + SubRemSubFilePreset** subs_presets) { + furi_assert(subrem_view_remote); + furi_assert(subs_presets); + + FuriString* labels[SubRemSubKeyNameMaxCount]; + SubRemSubFilePreset* sub_preset; + + for(uint8_t i = 0; i < SubRemSubKeyNameMaxCount; i++) { + sub_preset = subs_presets[i]; + switch(sub_preset->load_state) { + case SubRemLoadSubStateOK: + if(!furi_string_empty(sub_preset->label)) { + labels[i] = furi_string_alloc_set(sub_preset->label); + } else if(!furi_string_empty(sub_preset->file_path)) { + labels[i] = furi_string_alloc(); + path_extract_filename(sub_preset->file_path, labels[i], true); + } else { + labels[i] = furi_string_alloc_set("Empty Label"); + } + break; + + case SubRemLoadSubStateErrorNoFile: + labels[i] = furi_string_alloc_set("[X] Can't open file"); + break; + + case SubRemLoadSubStateErrorFreq: + case SubRemLoadSubStateErrorMod: + case SubRemLoadSubStateErrorProtocol: + labels[i] = furi_string_alloc_set("[X] Error in .sub file"); + break; + + default: + labels[i] = furi_string_alloc_set(""); + break; + } + } + + with_view_model( + subrem_view_remote->view, + SubRemViewRemoteModel * model, + { + for(uint8_t i = 0; i < SubRemSubKeyNameMaxCount; i++) { + strncpy( + model->labels[i], + furi_string_get_cstr(labels[i]), + SUBREM_VIEW_REMOTE_MAX_LABEL_LENGTH); + } + }, + true); + + for(uint8_t i = 0; i < SubRemSubKeyNameMaxCount; i++) { + furi_string_free(labels[i]); + } +} + +void subrem_view_remote_set_state( + SubRemViewRemote* subrem_view_remote, + SubRemViewRemoteState state, + uint8_t presed_btn) { + furi_assert(subrem_view_remote); + with_view_model( + subrem_view_remote->view, + SubRemViewRemoteModel * model, + { + model->state = state; + model->pressed_btn = presed_btn; + }, + true); +} + +void subrem_view_remote_set_radio(SubRemViewRemote* subrem_view_remote, bool external) { + furi_assert(subrem_view_remote); + with_view_model( + subrem_view_remote->view, + SubRemViewRemoteModel * model, + { model->is_external = external; }, + true); +} + +void subrem_view_remote_draw(Canvas* canvas, SubRemViewRemoteModel* model) { + canvas_clear(canvas); + canvas_set_color(canvas, ColorBlack); + + // Statusbar + canvas_draw_icon(canvas, 0, 0, &I_status_bar); + if(model->state == SubRemViewRemoteStateOFF) { + canvas_invert_color(canvas); + canvas_draw_rbox(canvas, 12, 0, 52 - 12, 13, 2); + canvas_invert_color(canvas); + canvas_draw_rframe(canvas, 12, 0, 52 - 12, 13, 2); + canvas_draw_str_aligned(canvas, 32, 3, AlignCenter, AlignTop, "Preview"); + } else { + canvas_draw_icon( + canvas, + 0, + 2, + (model->is_external) ? &I_External_antenna_20x12 : &I_Internal_antenna_20x12); + canvas_draw_icon(canvas, 50, 0, &I_Status_cube_14x14); + if(model->state == SubRemViewRemoteStateSending) { + canvas_draw_icon_ex(canvas, 52, 3, &I_Pin_arrow_up_7x9, IconRotation90); + } + } + + //Icons for Labels + const uint8_t list_y = 14; + canvas_draw_icon(canvas, 1, list_y + 5, &I_ButtonUp_7x4); + canvas_draw_icon(canvas, 1, list_y + 15, &I_ButtonDown_7x4); + canvas_draw_icon(canvas, 2, list_y + 23, &I_ButtonLeft_4x7); + canvas_draw_icon(canvas, 2, list_y + 33, &I_ButtonRight_4x7); + canvas_draw_icon(canvas, 0, list_y + 42, &I_Ok_btn_9x9); + + //Labels + canvas_set_font(canvas, FontSecondary); + uint8_t y = list_y; + for(uint8_t i = 0; i < SubRemSubKeyNameMaxCount; i++) { + elements_text_box( + canvas, + SUBREM_VIEW_REMOTE_LEFT_OFFSET, + y + 2, + 64 - SUBREM_VIEW_REMOTE_LEFT_OFFSET - SUBREM_VIEW_REMOTE_RIGHT_OFFSET, + 12, + AlignLeft, + AlignBottom, + model->labels[i], + false); + y += 10; + } + + if(model->state != SubRemViewRemoteStateOFF) { + // D-pad 59x62 + const uint8_t d_pad_x = 3; + const uint8_t d_pad_y = 66; + + canvas_draw_icon(canvas, d_pad_x + 1 * (19 + 1), d_pad_y + 0 * (20 + 1), &I_up); + + canvas_draw_icon(canvas, d_pad_x + 0 * (19 + 1), d_pad_y + 1 * (20 + 1), &I_left); + canvas_draw_icon(canvas, d_pad_x + 1 * (19 + 1), d_pad_y + 1 * (20 + 1), &I_ok); + canvas_draw_icon(canvas, d_pad_x + 2 * (19 + 1), d_pad_y + 1 * (20 + 1), &I_right); + + canvas_draw_icon(canvas, d_pad_x + 1 * (19 + 1), d_pad_y + 2 * (20 + 1), &I_down); + if(model->state == SubRemViewRemoteStateSending) { + switch(model->pressed_btn) { + case SubRemSubKeyNameUp: + canvas_draw_icon( + canvas, d_pad_x + 1 * (19 + 1), d_pad_y + 0 * (20 + 1), &I_up_hover); + break; + case SubRemSubKeyNameDown: + canvas_draw_icon( + canvas, d_pad_x + 1 * (19 + 1), d_pad_y + 2 * (20 + 1), &I_down_hover); + break; + case SubRemSubKeyNameLeft: + canvas_draw_icon( + canvas, d_pad_x + 0 * (19 + 1), d_pad_y + 1 * (20 + 1), &I_left_hover); + break; + case SubRemSubKeyNameRight: + canvas_draw_icon( + canvas, d_pad_x + 2 * (19 + 1), d_pad_y + 1 * (20 + 1), &I_right_hover); + break; + case SubRemSubKeyNameOk: + canvas_draw_icon( + canvas, d_pad_x + 1 * (19 + 1), d_pad_y + 1 * (20 + 1), &I_ok_hover); + break; + default: + break; + } + } + } else { + canvas_draw_icon(canvas, 2, 128 - 11, &I_ButtonLeft_4x7); + canvas_draw_str_aligned(canvas, 8, 128 - 4, AlignLeft, AlignBottom, "Back"); + + canvas_draw_icon(canvas, 58, 128 - 11, &I_ButtonRight_4x7); + canvas_draw_str_aligned(canvas, 56, 128 - 4, AlignRight, AlignBottom, "Save"); + } +} + +bool subrem_view_remote_input(InputEvent* event, void* context) { + furi_assert(context); + SubRemViewRemote* subrem_view_remote = context; + + if(event->key == InputKeyBack && event->type == InputTypePress) { + bool is_stopping = false; + with_view_model( + subrem_view_remote->view, + SubRemViewRemoteModel * model, + { + if(model->state == SubRemViewRemoteStateSending) { + is_stopping = true; + model->pressed_btn = 0; + } + }, + true); + + //Cant send exit the app inside that with_model,locks the model and the app will hang and not unload! + if(is_stopping) + subrem_view_remote->callback( + SubRemCustomEventViewRemoteForcedStop, subrem_view_remote->context); + else + subrem_view_remote->callback( + SubRemCustomEventViewRemoteBack, subrem_view_remote->context); + + return true; + } + // BACK button processing end + + if(event->key == InputKeyUp && event->type == InputTypePress) { + subrem_view_remote->callback( + SubRemCustomEventViewRemoteStartUP, subrem_view_remote->context); + return true; + } else if(event->key == InputKeyDown && event->type == InputTypePress) { + subrem_view_remote->callback( + SubRemCustomEventViewRemoteStartDOWN, subrem_view_remote->context); + return true; + } else if(event->key == InputKeyLeft && event->type == InputTypePress) { + subrem_view_remote->callback( + SubRemCustomEventViewRemoteStartLEFT, subrem_view_remote->context); + return true; + } else if(event->key == InputKeyRight && event->type == InputTypePress) { + subrem_view_remote->callback( + SubRemCustomEventViewRemoteStartRIGHT, subrem_view_remote->context); + return true; + } else if(event->key == InputKeyOk && event->type == InputTypePress) { + subrem_view_remote->callback( + SubRemCustomEventViewRemoteStartOK, subrem_view_remote->context); + return true; + } else if(event->type == InputTypeRelease) { + subrem_view_remote->callback(SubRemCustomEventViewRemoteStop, subrem_view_remote->context); + return true; + } + + return true; +} + +void subrem_view_remote_enter(void* context) { + furi_assert(context); +} + +void subrem_view_remote_exit(void* context) { + furi_assert(context); +} + +SubRemViewRemote* subrem_view_remote_alloc(void) { + SubRemViewRemote* subrem_view_remote = malloc(sizeof(SubRemViewRemote)); + + // View allocation and configuration + subrem_view_remote->view = view_alloc(); + view_allocate_model( + subrem_view_remote->view, ViewModelTypeLocking, sizeof(SubRemViewRemoteModel)); + view_set_context(subrem_view_remote->view, subrem_view_remote); + view_set_orientation(subrem_view_remote->view, ViewOrientationVertical); + view_set_draw_callback(subrem_view_remote->view, (ViewDrawCallback)subrem_view_remote_draw); + view_set_input_callback(subrem_view_remote->view, subrem_view_remote_input); + view_set_enter_callback(subrem_view_remote->view, subrem_view_remote_enter); + view_set_exit_callback(subrem_view_remote->view, subrem_view_remote_exit); + + with_view_model( + subrem_view_remote->view, + SubRemViewRemoteModel * model, + { + model->state = SubRemViewRemoteStateIdle; + + for(uint8_t i = 0; i < SubRemSubKeyNameMaxCount; i++) { + model->labels[i] = malloc(sizeof(char) * SUBREM_VIEW_REMOTE_MAX_LABEL_LENGTH + 1); + strcpy(model->labels[i], ""); + } + + model->pressed_btn = 0; + model->is_external = false; + }, + true); + return subrem_view_remote; +} + +void subrem_view_remote_free(SubRemViewRemote* subghz_remote) { + furi_assert(subghz_remote); + + with_view_model( + subghz_remote->view, + SubRemViewRemoteModel * model, + { + for(uint8_t i = 0; i < SubRemSubKeyNameMaxCount; i++) { + free(model->labels[i]); + } + }, + true); + view_free(subghz_remote->view); + free(subghz_remote); +} + +View* subrem_view_remote_get_view(SubRemViewRemote* subrem_view_remote) { + furi_assert(subrem_view_remote); + return subrem_view_remote->view; +} \ No newline at end of file diff --git a/subghz_remote/views/remote.h b/subghz_remote/views/remote.h new file mode 100644 index 00000000000..505140298dc --- /dev/null +++ b/subghz_remote/views/remote.h @@ -0,0 +1,38 @@ +#pragma once + +#include +#include "../helpers/subrem_custom_event.h" +#include "../helpers/subrem_presets.h" + +typedef enum { + SubRemViewRemoteStateIdle, + SubRemViewRemoteStateLoading, + SubRemViewRemoteStateSending, + SubRemViewRemoteStateOFF, +} SubRemViewRemoteState; + +typedef struct SubRemViewRemote SubRemViewRemote; + +typedef void (*SubRemViewRemoteCallback)(SubRemCustomEvent event, void* context); + +void subrem_view_remote_set_callback( + SubRemViewRemote* subrem_view_remote, + SubRemViewRemoteCallback callback, + void* context); + +SubRemViewRemote* subrem_view_remote_alloc(void); + +void subrem_view_remote_free(SubRemViewRemote* subrem_view_remote); + +View* subrem_view_remote_get_view(SubRemViewRemote* subrem_view_remote); + +void subrem_view_remote_update_data_labels( + SubRemViewRemote* subrem_view_remote, + SubRemSubFilePreset** subs_presets); + +void subrem_view_remote_set_state( + SubRemViewRemote* subrem_view_remote, + SubRemViewRemoteState state, + uint8_t presed_btn); + +void subrem_view_remote_set_radio(SubRemViewRemote* subrem_view_remote, bool external); \ No newline at end of file diff --git a/text_viewer/.gitsubtree b/text_viewer/.gitsubtree new file mode 100644 index 00000000000..109b9d7a607 --- /dev/null +++ b/text_viewer/.gitsubtree @@ -0,0 +1 @@ +https://github.com/xMasterX/all-the-plugins dev base_pack/text_viewer diff --git a/text_viewer/application.fam b/text_viewer/application.fam new file mode 100644 index 00000000000..c186f4a3dec --- /dev/null +++ b/text_viewer/application.fam @@ -0,0 +1,18 @@ +App( + appid="text_viewer", + name="Text Viewer", + apptype=FlipperAppType.EXTERNAL, + entry_point="text_viewer", + requires=[ + "gui", + "dialogs", + ], + stack_size=10 * 1024, + order=20, + fap_icon="icons/text_10px.png", + fap_category="Tools", + fap_icon_assets="icons", + fap_author="@Willy-JL", # Original by @kowalski7cc & @kyhwana, new has code borrowed from archive > show + fap_version="1.5", + fap_description="Text viewer application", +) diff --git a/text_viewer/icons/text_10px.png b/text_viewer/icons/text_10px.png new file mode 100644 index 00000000000..8e8a6183dd5 Binary files /dev/null and b/text_viewer/icons/text_10px.png differ diff --git a/text_viewer/img/1.png b/text_viewer/img/1.png new file mode 100644 index 00000000000..ce37e84c0c2 Binary files /dev/null and b/text_viewer/img/1.png differ diff --git a/text_viewer/img/2.png b/text_viewer/img/2.png new file mode 100644 index 00000000000..00711f0527b Binary files /dev/null and b/text_viewer/img/2.png differ diff --git a/text_viewer/scenes/text_viewer_scene.c b/text_viewer/scenes/text_viewer_scene.c new file mode 100644 index 00000000000..a507da4afe4 --- /dev/null +++ b/text_viewer/scenes/text_viewer_scene.c @@ -0,0 +1,30 @@ +#include "text_viewer_scene.h" + +// Generate scene on_enter handlers array +#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_enter, +void (*const text_viewer_on_enter_handlers[])(void*) = { +#include "text_viewer_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 text_viewer_on_event_handlers[])(void* context, SceneManagerEvent event) = { +#include "text_viewer_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 text_viewer_on_exit_handlers[])(void* context) = { +#include "text_viewer_scene_config.h" +}; +#undef ADD_SCENE + +// Initialize scene handlers configuration structure +const SceneManagerHandlers text_viewer_scene_handlers = { + .on_enter_handlers = text_viewer_on_enter_handlers, + .on_event_handlers = text_viewer_on_event_handlers, + .on_exit_handlers = text_viewer_on_exit_handlers, + .scene_num = TextViewerSceneNum, +}; diff --git a/text_viewer/scenes/text_viewer_scene.h b/text_viewer/scenes/text_viewer_scene.h new file mode 100644 index 00000000000..a8770db3b95 --- /dev/null +++ b/text_viewer/scenes/text_viewer_scene.h @@ -0,0 +1,29 @@ +#pragma once + +#include + +// Generate scene id and total number +#define ADD_SCENE(prefix, name, id) TextViewerScene##id, +typedef enum { +#include "text_viewer_scene_config.h" + TextViewerSceneNum, +} TextViewerScene; +#undef ADD_SCENE + +extern const SceneManagerHandlers text_viewer_scene_handlers; + +// Generate scene on_enter handlers declaration +#define ADD_SCENE(prefix, name, id) void prefix##_scene_##name##_on_enter(void*); +#include "text_viewer_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 "text_viewer_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 "text_viewer_scene_config.h" +#undef ADD_SCENE diff --git a/text_viewer/scenes/text_viewer_scene_config.h b/text_viewer/scenes/text_viewer_scene_config.h new file mode 100644 index 00000000000..86c69441e9c --- /dev/null +++ b/text_viewer/scenes/text_viewer_scene_config.h @@ -0,0 +1 @@ +ADD_SCENE(text_viewer, show, Show) diff --git a/text_viewer/scenes/text_viewer_scene_show.c b/text_viewer/scenes/text_viewer_scene_show.c new file mode 100644 index 00000000000..3d5aa077a8f --- /dev/null +++ b/text_viewer/scenes/text_viewer_scene_show.c @@ -0,0 +1,132 @@ +#include "../text_viewer.h" + +#define SHOW_MAX_FILE_SIZE 8000 + +void text_viewer_scene_show_widget_callback(GuiButtonType result, InputType type, void* context) { + furi_assert(context); + TextViewer* app = (TextViewer*)context; + if(type == InputTypeShort) { + view_dispatcher_send_custom_event(app->view_dispatcher, result); + } +} + +static bool text_show_read_lines(File* file, FuriString* str_result) { + //furi_string_reset(str_result); + uint8_t buffer[SHOW_MAX_FILE_SIZE]; + + uint16_t read_count = storage_file_read(file, buffer, SHOW_MAX_FILE_SIZE); + if(storage_file_get_error(file) != FSE_OK) { + return false; + } + + for(uint16_t i = 0; i < read_count; i++) { + furi_string_push_back(str_result, buffer[i]); + } + + return true; +} + +void text_viewer_scene_show_on_enter(void* context) { + furi_assert(context); + TextViewer* app = context; + + FuriString* buffer; + buffer = furi_string_alloc(); + + Storage* storage = furi_record_open(RECORD_STORAGE); + File* file = storage_file_alloc(storage); + + FileInfo fileinfo; + FS_Error error = storage_common_stat(storage, furi_string_get_cstr(app->path), &fileinfo); + if(error == FSE_OK) { + if((fileinfo.size < SHOW_MAX_FILE_SIZE) && (fileinfo.size > 2)) { + bool ok = storage_file_open( + file, furi_string_get_cstr(app->path), FSAM_READ, FSOM_OPEN_EXISTING); + if(ok) { + if(!text_show_read_lines(file, buffer)) { + goto text_file_read_err; + } + if(!furi_string_size(buffer)) { + goto text_file_read_err; + } + + storage_file_seek(file, 0, true); + + widget_add_text_scroll_element( + app->widget, 0, 0, 128, 64, furi_string_get_cstr(buffer)); + + } else { + text_file_read_err: + widget_add_text_box_element( + app->widget, + 0, + 0, + 128, + 64, + AlignLeft, + AlignCenter, + "\e#Error:\nStorage file open error\e#", + false); + } + storage_file_close(file); + } else if(fileinfo.size < 2) { + widget_add_text_box_element( + app->widget, + 0, + 0, + 128, + 64, + AlignLeft, + AlignCenter, + "\e#Error:\nFile is too small\e#", + false); + } else { + widget_add_text_box_element( + app->widget, + 0, + 0, + 128, + 64, + AlignLeft, + AlignCenter, + "\e#Error:\nFile is too large to show\e#", + false); + } + } else { + widget_add_text_box_element( + app->widget, + 0, + 0, + 128, + 64, + AlignLeft, + AlignCenter, + "\e#Error:\nFile system error\e#", + false); + } + + furi_string_free(buffer); + + storage_file_free(file); + furi_record_close(RECORD_STORAGE); + + view_dispatcher_switch_to_view(app->view_dispatcher, TextViewerViewWidget); +} + +bool text_viewer_scene_show_on_event(void* context, SceneManagerEvent event) { + furi_assert(context); + TextViewer* app = (TextViewer*)context; + + if(event.type == SceneManagerEventTypeCustom) { + scene_manager_previous_scene(app->scene_manager); + return true; + } + return false; +} + +void text_viewer_scene_show_on_exit(void* context) { + furi_assert(context); + TextViewer* app = (TextViewer*)context; + + widget_reset(app->widget); +} diff --git a/text_viewer/text_viewer.c b/text_viewer/text_viewer.c new file mode 100644 index 00000000000..782b44c6fcf --- /dev/null +++ b/text_viewer/text_viewer.c @@ -0,0 +1,84 @@ +#include "text_viewer.h" + +static bool text_viewer_custom_event_callback(void* context, uint32_t event) { + furi_assert(context); + TextViewer* app = context; + return scene_manager_handle_custom_event(app->scene_manager, event); +} + +static bool text_viewer_back_event_callback(void* context) { + furi_assert(context); + TextViewer* app = context; + return scene_manager_handle_back_event(app->scene_manager); +} + +TextViewer* text_viewer_alloc() { + TextViewer* app = malloc(sizeof(TextViewer)); + app->gui = furi_record_open(RECORD_GUI); + + app->view_dispatcher = view_dispatcher_alloc(); + app->scene_manager = scene_manager_alloc(&text_viewer_scene_handlers, app); + view_dispatcher_enable_queue(app->view_dispatcher); + view_dispatcher_set_event_callback_context(app->view_dispatcher, app); + + view_dispatcher_set_custom_event_callback( + app->view_dispatcher, text_viewer_custom_event_callback); + view_dispatcher_set_navigation_event_callback( + app->view_dispatcher, text_viewer_back_event_callback); + + view_dispatcher_attach_to_gui(app->view_dispatcher, app->gui, ViewDispatcherTypeFullscreen); + + app->widget = widget_alloc(); + view_dispatcher_add_view( + app->view_dispatcher, TextViewerViewWidget, widget_get_view(app->widget)); + + app->path = furi_string_alloc(); + + return app; +} + +void text_viewer_free(TextViewer* app) { + furi_assert(app); + + view_dispatcher_remove_view(app->view_dispatcher, TextViewerViewWidget); + widget_free(app->widget); + + view_dispatcher_free(app->view_dispatcher); + scene_manager_free(app->scene_manager); + + furi_string_free(app->path); + + furi_record_close(RECORD_GUI); + free(app); +} + +extern int32_t text_viewer(void* p) { + TextViewer* app = text_viewer_alloc(); + + do { + if(p && strlen(p)) { + furi_string_set(app->path, (const char*)p); + } else { + furi_string_set(app->path, TEXT_VIEWER_PATH); + + DialogsFileBrowserOptions browser_options; + dialog_file_browser_set_basic_options( + &browser_options, TEXT_VIEWER_EXTENSION, &I_text_10px); + browser_options.hide_ext = false; + + DialogsApp* dialogs = furi_record_open(RECORD_DIALOGS); + bool res = dialog_file_browser_show(dialogs, app->path, app->path, &browser_options); + + furi_record_close(RECORD_DIALOGS); + if(!res) { + break; + } + } + + scene_manager_next_scene(app->scene_manager, TextViewerSceneShow); + view_dispatcher_run(app->view_dispatcher); + } while(false); + + text_viewer_free(app); + return 0; +} diff --git a/text_viewer/text_viewer.h b/text_viewer/text_viewer.h new file mode 100644 index 00000000000..f0427ca2834 --- /dev/null +++ b/text_viewer/text_viewer.h @@ -0,0 +1,26 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include "text_viewer_icons.h" +#include "scenes/text_viewer_scene.h" + +#define TEXT_VIEWER_PATH STORAGE_EXT_PATH_PREFIX +#define TEXT_VIEWER_EXTENSION "*" + +typedef struct { + Gui* gui; + SceneManager* scene_manager; + ViewDispatcher* view_dispatcher; + Widget* widget; + + FuriString* path; +} TextViewer; + +typedef enum { + TextViewerViewWidget, +} TextViewerView; diff --git a/uhf_rfid/application.fam b/uhf_rfid/application.fam index dc9eb2bf6d1..9d75b7036a3 100644 --- a/uhf_rfid/application.fam +++ b/uhf_rfid/application.fam @@ -1,6 +1,6 @@ App( appid="uhf_rfid", - name="[YRM100] UHF RFID", + name="YRM100 UHF RFID", apptype=FlipperAppType.EXTERNAL, targets=["f7"], entry_point="uhf_app_main", diff --git a/xremote/xremote_app.c b/xremote/xremote_app.c index 84604efa7c2..fd2e81de1bd 100644 --- a/xremote/xremote_app.c +++ b/xremote/xremote_app.c @@ -523,7 +523,7 @@ void xremote_app_submenu_alloc(XRemoteApp* app, uint32_t index, ViewNavigationCa View* view = submenu_get_view(app->submenu); view_set_previous_callback(view, prev_cb); -#if defined(FW_ORIGIN_Unleashed) || defined(FW_ORIGIN_RM) +#if defined(FW_ORIGIN_Unleashed) || defined(FW_ORIGIN_RM) || defined(FW_ORIGIN_Momentum) submenu_set_orientation(app->submenu, settings->orientation); #else view_set_orientation(view, settings->orientation);