├── flipper_share ├── images │ └── .gitkeep ├── flipper_share.png ├── screenshots │ ├── 1.png │ ├── 2.png │ ├── 3.png │ ├── 4.png │ ├── 5.png │ ├── 6.png │ └── 7.png ├── .gitignore ├── scenes │ ├── flipper_share_scene_config.h │ ├── flipper_share_scene_menu.c │ ├── flipper_share_scene.h │ ├── flipper_share_scene.c │ ├── flipper_share_scene_file_browser.c │ ├── flipper_share_scene_show_file.c │ ├── flipper_share_scene_send.c │ └── flipper_share_scene_receive.c ├── CHANGELOG.md ├── application.fam ├── manifest.yml ├── flipper_share_app.h ├── .github │ └── workflows │ │ └── build.yml ├── subghz_share.h ├── README_catalog.md ├── README.md ├── subghz_share.c ├── flipper_share.h └── flipper_share_app.c ├── hid_app ├── docs │ └── changelog.md ├── README.md ├── hid_ble_10px.png ├── hid_usb_10px.png ├── assets │ ├── Volup_8x6.png │ ├── Arr_dwn_7x9.png │ ├── Arr_up_7x9.png │ ├── Ok_btn_9x9.png │ ├── Space_60x18.png │ ├── Space_65x18.png │ ├── Voldwn_6x6.png │ ├── ButtonDown_7x4.png │ ├── ButtonF10_5x8.png │ ├── ButtonF11_5x8.png │ ├── ButtonF12_5x8.png │ ├── ButtonF1_5x8.png │ ├── ButtonF2_5x8.png │ ├── ButtonF3_5x8.png │ ├── ButtonF4_5x8.png │ ├── ButtonF5_5x8.png │ ├── ButtonF6_5x8.png │ ├── ButtonF7_5x8.png │ ├── ButtonF8_5x8.png │ ├── ButtonF9_5x8.png │ ├── ButtonLeft_4x7.png │ ├── ButtonUp_7x4.png │ ├── Button_18x18.png │ ├── Circles_47x47.png │ ├── Like_def_11x9.png │ ├── ButtonRight_4x7.png │ ├── Pin_arrow_up_7x9.png │ ├── Ble_connected_15x15.png │ ├── Left_mouse_icon_9x9.png │ ├── Like_pressed_17x17.png │ ├── Pin_arrow_down_7x9.png │ ├── Pin_arrow_left_9x7.png │ ├── Pin_arrow_right_9x7.png │ ├── Pin_back_arrow_10x8.png │ ├── Ok_btn_pressed_13x13.png │ ├── Pressed_Button_13x13.png │ ├── Right_mouse_icon_9x9.png │ └── Ble_disconnected_15x15.png ├── .catalog │ └── screenshots │ │ ├── 1.png │ │ ├── 2.png │ │ ├── 3.png │ │ ├── 4.png │ │ └── 5.png ├── views.h ├── views │ ├── hid_media.h │ ├── hid_slack.h │ ├── hid_tiktok.h │ ├── hid_keyboard.h │ ├── hid_mouse.h │ ├── hid_mouse_clicker.h │ ├── hid_keynote.h │ ├── hid_mouse_jiggler.h │ ├── hid_mouse_jiggler.c │ ├── hid_mouse_clicker.c │ ├── hid_media.c │ ├── hid_mouse.c │ ├── hid_tiktok.c │ ├── hid_slack.c │ ├── hid_keynote.c │ └── hid_keyboard.c ├── application.fam ├── hid.h └── hid.c ├── .gitignore └── README.md /flipper_share/images/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /hid_app/docs/changelog.md: -------------------------------------------------------------------------------- 1 | v0.2: 2 | released 3 | 4 | v0.1: 5 | initital release 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /**/dist/* 2 | .vscode 3 | .clang-format 4 | .editorconfig 5 | 6 | .DS_Store 7 | -------------------------------------------------------------------------------- /hid_app/README.md: -------------------------------------------------------------------------------- 1 | Fork of officail hid_app (Remote), that have support of Slack voice calls control. 2 | -------------------------------------------------------------------------------- /hid_app/hid_ble_10px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lomalkin/flipper-zero-apps/HEAD/hid_app/hid_ble_10px.png -------------------------------------------------------------------------------- /hid_app/hid_usb_10px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lomalkin/flipper-zero-apps/HEAD/hid_app/hid_usb_10px.png -------------------------------------------------------------------------------- /hid_app/assets/Volup_8x6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lomalkin/flipper-zero-apps/HEAD/hid_app/assets/Volup_8x6.png -------------------------------------------------------------------------------- /hid_app/assets/Arr_dwn_7x9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lomalkin/flipper-zero-apps/HEAD/hid_app/assets/Arr_dwn_7x9.png -------------------------------------------------------------------------------- /hid_app/assets/Arr_up_7x9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lomalkin/flipper-zero-apps/HEAD/hid_app/assets/Arr_up_7x9.png -------------------------------------------------------------------------------- /hid_app/assets/Ok_btn_9x9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lomalkin/flipper-zero-apps/HEAD/hid_app/assets/Ok_btn_9x9.png -------------------------------------------------------------------------------- /hid_app/assets/Space_60x18.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lomalkin/flipper-zero-apps/HEAD/hid_app/assets/Space_60x18.png -------------------------------------------------------------------------------- /hid_app/assets/Space_65x18.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lomalkin/flipper-zero-apps/HEAD/hid_app/assets/Space_65x18.png -------------------------------------------------------------------------------- /hid_app/assets/Voldwn_6x6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lomalkin/flipper-zero-apps/HEAD/hid_app/assets/Voldwn_6x6.png -------------------------------------------------------------------------------- /flipper_share/flipper_share.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lomalkin/flipper-zero-apps/HEAD/flipper_share/flipper_share.png -------------------------------------------------------------------------------- /flipper_share/screenshots/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lomalkin/flipper-zero-apps/HEAD/flipper_share/screenshots/1.png -------------------------------------------------------------------------------- /flipper_share/screenshots/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lomalkin/flipper-zero-apps/HEAD/flipper_share/screenshots/2.png -------------------------------------------------------------------------------- /flipper_share/screenshots/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lomalkin/flipper-zero-apps/HEAD/flipper_share/screenshots/3.png -------------------------------------------------------------------------------- /flipper_share/screenshots/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lomalkin/flipper-zero-apps/HEAD/flipper_share/screenshots/4.png -------------------------------------------------------------------------------- /flipper_share/screenshots/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lomalkin/flipper-zero-apps/HEAD/flipper_share/screenshots/5.png -------------------------------------------------------------------------------- /flipper_share/screenshots/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lomalkin/flipper-zero-apps/HEAD/flipper_share/screenshots/6.png -------------------------------------------------------------------------------- /flipper_share/screenshots/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lomalkin/flipper-zero-apps/HEAD/flipper_share/screenshots/7.png -------------------------------------------------------------------------------- /hid_app/assets/ButtonDown_7x4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lomalkin/flipper-zero-apps/HEAD/hid_app/assets/ButtonDown_7x4.png -------------------------------------------------------------------------------- /hid_app/assets/ButtonF10_5x8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lomalkin/flipper-zero-apps/HEAD/hid_app/assets/ButtonF10_5x8.png -------------------------------------------------------------------------------- /hid_app/assets/ButtonF11_5x8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lomalkin/flipper-zero-apps/HEAD/hid_app/assets/ButtonF11_5x8.png -------------------------------------------------------------------------------- /hid_app/assets/ButtonF12_5x8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lomalkin/flipper-zero-apps/HEAD/hid_app/assets/ButtonF12_5x8.png -------------------------------------------------------------------------------- /hid_app/assets/ButtonF1_5x8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lomalkin/flipper-zero-apps/HEAD/hid_app/assets/ButtonF1_5x8.png -------------------------------------------------------------------------------- /hid_app/assets/ButtonF2_5x8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lomalkin/flipper-zero-apps/HEAD/hid_app/assets/ButtonF2_5x8.png -------------------------------------------------------------------------------- /hid_app/assets/ButtonF3_5x8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lomalkin/flipper-zero-apps/HEAD/hid_app/assets/ButtonF3_5x8.png -------------------------------------------------------------------------------- /hid_app/assets/ButtonF4_5x8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lomalkin/flipper-zero-apps/HEAD/hid_app/assets/ButtonF4_5x8.png -------------------------------------------------------------------------------- /hid_app/assets/ButtonF5_5x8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lomalkin/flipper-zero-apps/HEAD/hid_app/assets/ButtonF5_5x8.png -------------------------------------------------------------------------------- /hid_app/assets/ButtonF6_5x8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lomalkin/flipper-zero-apps/HEAD/hid_app/assets/ButtonF6_5x8.png -------------------------------------------------------------------------------- /hid_app/assets/ButtonF7_5x8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lomalkin/flipper-zero-apps/HEAD/hid_app/assets/ButtonF7_5x8.png -------------------------------------------------------------------------------- /hid_app/assets/ButtonF8_5x8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lomalkin/flipper-zero-apps/HEAD/hid_app/assets/ButtonF8_5x8.png -------------------------------------------------------------------------------- /hid_app/assets/ButtonF9_5x8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lomalkin/flipper-zero-apps/HEAD/hid_app/assets/ButtonF9_5x8.png -------------------------------------------------------------------------------- /hid_app/assets/ButtonLeft_4x7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lomalkin/flipper-zero-apps/HEAD/hid_app/assets/ButtonLeft_4x7.png -------------------------------------------------------------------------------- /hid_app/assets/ButtonUp_7x4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lomalkin/flipper-zero-apps/HEAD/hid_app/assets/ButtonUp_7x4.png -------------------------------------------------------------------------------- /hid_app/assets/Button_18x18.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lomalkin/flipper-zero-apps/HEAD/hid_app/assets/Button_18x18.png -------------------------------------------------------------------------------- /hid_app/assets/Circles_47x47.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lomalkin/flipper-zero-apps/HEAD/hid_app/assets/Circles_47x47.png -------------------------------------------------------------------------------- /hid_app/assets/Like_def_11x9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lomalkin/flipper-zero-apps/HEAD/hid_app/assets/Like_def_11x9.png -------------------------------------------------------------------------------- /hid_app/.catalog/screenshots/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lomalkin/flipper-zero-apps/HEAD/hid_app/.catalog/screenshots/1.png -------------------------------------------------------------------------------- /hid_app/.catalog/screenshots/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lomalkin/flipper-zero-apps/HEAD/hid_app/.catalog/screenshots/2.png -------------------------------------------------------------------------------- /hid_app/.catalog/screenshots/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lomalkin/flipper-zero-apps/HEAD/hid_app/.catalog/screenshots/3.png -------------------------------------------------------------------------------- /hid_app/.catalog/screenshots/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lomalkin/flipper-zero-apps/HEAD/hid_app/.catalog/screenshots/4.png -------------------------------------------------------------------------------- /hid_app/.catalog/screenshots/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lomalkin/flipper-zero-apps/HEAD/hid_app/.catalog/screenshots/5.png -------------------------------------------------------------------------------- /hid_app/assets/ButtonRight_4x7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lomalkin/flipper-zero-apps/HEAD/hid_app/assets/ButtonRight_4x7.png -------------------------------------------------------------------------------- /hid_app/assets/Pin_arrow_up_7x9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lomalkin/flipper-zero-apps/HEAD/hid_app/assets/Pin_arrow_up_7x9.png -------------------------------------------------------------------------------- /hid_app/assets/Ble_connected_15x15.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lomalkin/flipper-zero-apps/HEAD/hid_app/assets/Ble_connected_15x15.png -------------------------------------------------------------------------------- /hid_app/assets/Left_mouse_icon_9x9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lomalkin/flipper-zero-apps/HEAD/hid_app/assets/Left_mouse_icon_9x9.png -------------------------------------------------------------------------------- /hid_app/assets/Like_pressed_17x17.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lomalkin/flipper-zero-apps/HEAD/hid_app/assets/Like_pressed_17x17.png -------------------------------------------------------------------------------- /hid_app/assets/Pin_arrow_down_7x9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lomalkin/flipper-zero-apps/HEAD/hid_app/assets/Pin_arrow_down_7x9.png -------------------------------------------------------------------------------- /hid_app/assets/Pin_arrow_left_9x7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lomalkin/flipper-zero-apps/HEAD/hid_app/assets/Pin_arrow_left_9x7.png -------------------------------------------------------------------------------- /hid_app/assets/Pin_arrow_right_9x7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lomalkin/flipper-zero-apps/HEAD/hid_app/assets/Pin_arrow_right_9x7.png -------------------------------------------------------------------------------- /hid_app/assets/Pin_back_arrow_10x8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lomalkin/flipper-zero-apps/HEAD/hid_app/assets/Pin_back_arrow_10x8.png -------------------------------------------------------------------------------- /hid_app/assets/Ok_btn_pressed_13x13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lomalkin/flipper-zero-apps/HEAD/hid_app/assets/Ok_btn_pressed_13x13.png -------------------------------------------------------------------------------- /hid_app/assets/Pressed_Button_13x13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lomalkin/flipper-zero-apps/HEAD/hid_app/assets/Pressed_Button_13x13.png -------------------------------------------------------------------------------- /hid_app/assets/Right_mouse_icon_9x9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lomalkin/flipper-zero-apps/HEAD/hid_app/assets/Right_mouse_icon_9x9.png -------------------------------------------------------------------------------- /hid_app/assets/Ble_disconnected_15x15.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lomalkin/flipper-zero-apps/HEAD/hid_app/assets/Ble_disconnected_15x15.png -------------------------------------------------------------------------------- /flipper_share/.gitignore: -------------------------------------------------------------------------------- 1 | dist/* 2 | .vscode 3 | .clang-format 4 | .clangd 5 | .editorconfig 6 | .env 7 | .ufbt 8 | 9 | 10 | # OS crap 11 | .DS_Store 12 | 13 | .vscode/ 14 | 15 | _old 16 | -------------------------------------------------------------------------------- /flipper_share/scenes/flipper_share_scene_config.h: -------------------------------------------------------------------------------- 1 | ADD_SCENE(flipper_share, menu, Menu) 2 | ADD_SCENE(flipper_share, file_browser, FileBrowser) 3 | ADD_SCENE(flipper_share, show_file, ShowFile) 4 | ADD_SCENE(flipper_share, send, Send) 5 | ADD_SCENE(flipper_share, receive, Receive) 6 | -------------------------------------------------------------------------------- /hid_app/views.h: -------------------------------------------------------------------------------- 1 | typedef enum { 2 | HidViewSubmenu, 3 | HidViewKeynote, 4 | HidViewKeyboard, 5 | HidViewMedia, 6 | HidViewMouse, 7 | HidViewMouseClicker, 8 | HidViewMouseJiggler, 9 | BtHidViewTikTok, 10 | HidViewSlack, 11 | HidViewExitConfirm, 12 | } HidView; -------------------------------------------------------------------------------- /hid_app/views/hid_media.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | typedef struct HidMedia HidMedia; 6 | 7 | HidMedia* hid_media_alloc(); 8 | 9 | void hid_media_free(HidMedia* hid_media); 10 | 11 | View* hid_media_get_view(HidMedia* hid_media); 12 | 13 | void hid_media_set_connected_status(HidMedia* hid_media, bool connected); 14 | -------------------------------------------------------------------------------- /hid_app/views/hid_slack.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | typedef struct Hid Hid; 6 | typedef struct HidSlack HidSlack; 7 | 8 | HidSlack* hid_slack_alloc(Hid* bt_hid); 9 | 10 | void hid_slack_free(HidSlack* hid_slack); 11 | 12 | View* hid_slack_get_view(HidSlack* hid_slack); 13 | 14 | void hid_slack_set_connected_status(HidSlack* hid_slack, bool connected); 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flipper Zero applications by lomalkin 2 | 3 | --- 4 | 5 | ## Flipper Share - a file sharing app via Sub-GHz 6 | 7 | See [README.md](flipper_share/README.md) in `flipper_share` folder for details. 8 | 9 | 10 | ## ble_hid_slack (deprecated) 11 | 12 | This is a fork of official `ble_hid` (Remote) application, with added support of Slack voice calls control via Flipper. 13 | -------------------------------------------------------------------------------- /hid_app/views/hid_tiktok.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | typedef struct Hid Hid; 6 | typedef struct HidTikTok HidTikTok; 7 | 8 | HidTikTok* hid_tiktok_alloc(Hid* bt_hid); 9 | 10 | void hid_tiktok_free(HidTikTok* hid_tiktok); 11 | 12 | View* hid_tiktok_get_view(HidTikTok* hid_tiktok); 13 | 14 | void hid_tiktok_set_connected_status(HidTikTok* hid_tiktok, bool connected); 15 | -------------------------------------------------------------------------------- /hid_app/views/hid_keyboard.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | typedef struct Hid Hid; 6 | typedef struct HidKeyboard HidKeyboard; 7 | 8 | HidKeyboard* hid_keyboard_alloc(Hid* bt_hid); 9 | 10 | void hid_keyboard_free(HidKeyboard* hid_keyboard); 11 | 12 | View* hid_keyboard_get_view(HidKeyboard* hid_keyboard); 13 | 14 | void hid_keyboard_set_connected_status(HidKeyboard* hid_keyboard, bool connected); 15 | -------------------------------------------------------------------------------- /hid_app/views/hid_mouse.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #define MOUSE_MOVE_SHORT 5 6 | #define MOUSE_MOVE_LONG 20 7 | 8 | typedef struct Hid Hid; 9 | typedef struct HidMouse HidMouse; 10 | 11 | HidMouse* hid_mouse_alloc(Hid* bt_hid); 12 | 13 | void hid_mouse_free(HidMouse* hid_mouse); 14 | 15 | View* hid_mouse_get_view(HidMouse* hid_mouse); 16 | 17 | void hid_mouse_set_connected_status(HidMouse* hid_mouse, bool connected); 18 | -------------------------------------------------------------------------------- /hid_app/views/hid_mouse_clicker.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | typedef struct Hid Hid; 6 | typedef struct HidMouseClicker HidMouseClicker; 7 | 8 | HidMouseClicker* hid_mouse_clicker_alloc(Hid* bt_hid); 9 | 10 | void hid_mouse_clicker_free(HidMouseClicker* hid_mouse_clicker); 11 | 12 | View* hid_mouse_clicker_get_view(HidMouseClicker* hid_mouse_clicker); 13 | 14 | void hid_mouse_clicker_set_connected_status(HidMouseClicker* hid_mouse_clicker, bool connected); 15 | -------------------------------------------------------------------------------- /hid_app/views/hid_keynote.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | typedef struct Hid Hid; 6 | typedef struct HidKeynote HidKeynote; 7 | 8 | HidKeynote* hid_keynote_alloc(Hid* bt_hid); 9 | 10 | void hid_keynote_free(HidKeynote* hid_keynote); 11 | 12 | View* hid_keynote_get_view(HidKeynote* hid_keynote); 13 | 14 | void hid_keynote_set_connected_status(HidKeynote* hid_keynote, bool connected); 15 | 16 | void hid_keynote_set_orientation(HidKeynote* hid_keynote, bool vertical); 17 | -------------------------------------------------------------------------------- /hid_app/views/hid_mouse_jiggler.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #define MOUSE_MOVE_SHORT 5 6 | #define MOUSE_MOVE_LONG 20 7 | 8 | typedef struct Hid Hid; 9 | typedef struct HidMouseJiggler HidMouseJiggler; 10 | 11 | HidMouseJiggler* hid_mouse_jiggler_alloc(Hid* bt_hid); 12 | 13 | void hid_mouse_jiggler_free(HidMouseJiggler* hid_mouse_jiggler); 14 | 15 | View* hid_mouse_jiggler_get_view(HidMouseJiggler* hid_mouse_jiggler); 16 | 17 | void hid_mouse_jiggler_set_connected_status(HidMouseJiggler* hid_mouse_jiggler, bool connected); 18 | -------------------------------------------------------------------------------- /flipper_share/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | v1.0: 2 | - First public version of flipper_share app. 3 | 4 | v1.1: 5 | - GUI Improvements: file name and file size display during transfer. 6 | - Added advanced torrent-like style progress bar showing received file parts. 7 | 8 | v1.2: Upgrade is recommended for more reliable transfers. 9 | - Fixed a potential memory corruption when receiving series of files. 10 | - Fixed an integrity check issue after file reception. 11 | - Improve LED indications during transfer (requests added). 12 | - Code refactored a bit. 13 | - Readme: updated description of the protocol and example session. 14 | -------------------------------------------------------------------------------- /flipper_share/application.fam: -------------------------------------------------------------------------------- 1 | # For details & more options, see documentation/AppManifests.md in firmware repo 2 | 3 | App( 4 | appid="flipper_share", 5 | name="Flipper Share", 6 | apptype=FlipperAppType.EXTERNAL, 7 | entry_point="flipper_share_app", 8 | stack_size=2 * 1024, 9 | fap_category="Sub-GHz", 10 | # Optional values 11 | fap_version="1.2", 12 | fap_icon="flipper_share.png", # 10x10 1-bit PNG 13 | fap_description="Direct file transfer between flippers via Sub-GHz", 14 | fap_author="@lomalkin", 15 | fap_weburl="https://github.com/lomalkin/flipper-zero-apps/blob/-/flipper_share", 16 | # fap_icon_assets="images", # Image assets to compile for this application 17 | ) 18 | -------------------------------------------------------------------------------- /flipper_share/scenes/flipper_share_scene_menu.c: -------------------------------------------------------------------------------- 1 | #include "../flipper_share_app.h" 2 | 3 | void flipper_share_scene_menu_on_enter(void* context) { 4 | FlipperShareApp* app = context; 5 | 6 | // Menu is already set up in the main application file 7 | view_dispatcher_switch_to_view(app->view_dispatcher, FlipperShareViewIdMenu); 8 | } 9 | 10 | bool flipper_share_scene_menu_on_event(void* context, SceneManagerEvent event) { 11 | UNUSED(context); 12 | bool consumed = false; 13 | 14 | if(event.type == SceneManagerEventTypeCustom) { 15 | // Events from submenus are already handled in submenu_callback 16 | consumed = true; 17 | } 18 | 19 | return consumed; 20 | } 21 | 22 | void flipper_share_scene_menu_on_exit(void* context) { 23 | UNUSED(context); 24 | } 25 | -------------------------------------------------------------------------------- /flipper_share/manifest.yml: -------------------------------------------------------------------------------- 1 | # PR this file to: https://github.com/flipperdevices/flipper-application-catalog/ 2 | # applications/Sub-GHz/flipper_share/manifest.yml 3 | 4 | # Check: 5 | # python3 -m venv venv 6 | # pip install -r tools/requirements.txt 7 | # python3 tools/bundle.py --nolint applications/Sub-GHz/flipper_share/manifest.yml bundle.zip 8 | 9 | sourcecode: 10 | type: git 11 | location: 12 | origin: https://github.com/lomalkin/flipper-zero-apps.git 13 | commit_sha: 2926d96c0cd74e7b1712bc669c0c451b63d3d37c 14 | subdir: flipper_share 15 | description: "@README_catalog.md" 16 | changelog: "@CHANGELOG.md" 17 | screenshots: 18 | - screenshots/1.png 19 | - screenshots/2.png 20 | - screenshots/3.png 21 | - screenshots/4.png 22 | - screenshots/5.png 23 | - screenshots/6.png 24 | - screenshots/7.png 25 | -------------------------------------------------------------------------------- /hid_app/application.fam: -------------------------------------------------------------------------------- 1 | App( 2 | appid="hid_usb", 3 | name="Remote", 4 | apptype=FlipperAppType.EXTERNAL, 5 | entry_point="hid_usb_app", 6 | stack_size=1 * 1024, 7 | fap_description="Use Flipper as a HID remote control over USB", 8 | fap_category="USB", 9 | fap_icon="hid_usb_10px.png", 10 | fap_icon_assets="assets", 11 | fap_icon_assets_symbol="hid", 12 | ) 13 | 14 | 15 | App( 16 | appid="hid_ble_slack", 17 | name="Remote Slack", 18 | apptype=FlipperAppType.EXTERNAL, 19 | entry_point="hid_ble_app", 20 | stack_size=1 * 1024, 21 | fap_description="Use Flipper as a HID remote control over Bluetooth", 22 | fap_category="Bluetooth", 23 | fap_icon="hid_ble_10px.png", 24 | fap_icon_assets="assets", 25 | fap_icon_assets_symbol="hid", 26 | # Optional values 27 | fap_version=(0, 2), # (major, minor) 28 | # fap_author="J. Doe", 29 | fap_weburl="https://github.com/lomalkin/flipper-apps.git", 30 | ) 31 | -------------------------------------------------------------------------------- /flipper_share/scenes/flipper_share_scene.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | // Generate scene id and total number 6 | #define ADD_SCENE(prefix, name, id) FlipperShareScene##id, 7 | typedef enum { 8 | #include "flipper_share_scene_config.h" 9 | FlipperShareSceneNum, 10 | } FlipperShareScene; 11 | #undef ADD_SCENE 12 | 13 | extern const SceneManagerHandlers flipper_share_scene_handlers; 14 | 15 | // Generate scene on_enter handlers declaration 16 | #define ADD_SCENE(prefix, name, id) void prefix##_scene_##name##_on_enter(void*); 17 | #include "flipper_share_scene_config.h" 18 | #undef ADD_SCENE 19 | 20 | // Generate scene on_event handlers declaration 21 | #define ADD_SCENE(prefix, name, id) \ 22 | bool prefix##_scene_##name##_on_event(void* context, SceneManagerEvent event); 23 | #include "flipper_share_scene_config.h" 24 | #undef ADD_SCENE 25 | 26 | // Generate scene on_exit handlers declaration 27 | #define ADD_SCENE(prefix, name, id) void prefix##_scene_##name##_on_exit(void* context); 28 | #include "flipper_share_scene_config.h" 29 | #undef ADD_SCENE 30 | -------------------------------------------------------------------------------- /flipper_share/scenes/flipper_share_scene.c: -------------------------------------------------------------------------------- 1 | #include "flipper_share_scene.h" 2 | 3 | // Generate scene on_enter handlers array 4 | #define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_enter, 5 | void (*const flipper_share_on_enter_handlers[])(void*) = { 6 | #include "flipper_share_scene_config.h" 7 | }; 8 | #undef ADD_SCENE 9 | 10 | // Generate scene on_event handlers array 11 | #define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_event, 12 | bool (*const flipper_share_on_event_handlers[])(void* context, SceneManagerEvent event) = { 13 | #include "flipper_share_scene_config.h" 14 | }; 15 | #undef ADD_SCENE 16 | 17 | // Generate scene on_exit handlers array 18 | #define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_exit, 19 | void (*const flipper_share_on_exit_handlers[])(void* context) = { 20 | #include "flipper_share_scene_config.h" 21 | }; 22 | #undef ADD_SCENE 23 | 24 | // Initialize scene handlers configuration structure 25 | const SceneManagerHandlers flipper_share_scene_handlers = { 26 | .on_enter_handlers = flipper_share_on_enter_handlers, 27 | .on_event_handlers = flipper_share_on_event_handlers, 28 | .on_exit_handlers = flipper_share_on_exit_handlers, 29 | .scene_num = FlipperShareSceneNum, 30 | }; 31 | -------------------------------------------------------------------------------- /flipper_share/flipper_share_app.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | 19 | #include "scenes/flipper_share_scene.h" 20 | 21 | typedef struct FlipperShareApp FlipperShareApp; 22 | 23 | typedef enum { 24 | FlipperShareViewIdMenu, 25 | FlipperShareViewIdFileBrowser, 26 | FlipperShareViewIdShowFile, 27 | FlipperShareViewIdProgress, 28 | FlipperShareViewIdReceive, 29 | FlipperShareViewIdAbout, 30 | } FlipperShareViewId; 31 | 32 | struct FlipperShareApp { 33 | Gui* gui; 34 | SceneManager* scene_manager; 35 | ViewDispatcher* view_dispatcher; 36 | 37 | Submenu* submenu; 38 | FileBrowser* file_browser; 39 | DialogEx* dialog_show_file; 40 | DialogEx* dialog_receive; 41 | DialogEx* dialog_about; 42 | 43 | FuriString* result_path; 44 | char selected_file_path[256]; 45 | size_t selected_file_size; 46 | bool file_info_loaded; 47 | 48 | void* file_reading_state; 49 | FuriTimer* timer; 50 | }; 51 | 52 | void show_file_info_scene(FlipperShareApp* app); // Correct declaration 53 | -------------------------------------------------------------------------------- /flipper_share/.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: "FAP: Build for multiple SDK sources" 2 | # This will build your app for dev and release channels on GitHub. 3 | # It will also build your app every day to make sure it's up to date with the latest SDK changes. 4 | # See https://github.com/marketplace/actions/build-flipper-application-package-fap for more information 5 | 6 | on: 7 | push: 8 | ## put your main branch name under "branches" 9 | #branches: 10 | # - master 11 | - dev 12 | pull_request: 13 | schedule: 14 | # do a build every day 15 | - cron: "1 1 * * *" 16 | 17 | jobs: 18 | ufbt-build: 19 | runs-on: ubuntu-latest 20 | strategy: 21 | matrix: 22 | include: 23 | - name: dev channel 24 | sdk-channel: dev 25 | - name: release channel 26 | sdk-channel: release 27 | # You can add unofficial channels here. See ufbt action docs for more info. 28 | name: 'ufbt: Build for ${{ matrix.name }}' 29 | steps: 30 | - name: Checkout 31 | uses: actions/checkout@v4 32 | - name: Build with ufbt 33 | uses: flipperdevices/flipperzero-ufbt-action@v0.1 34 | id: build-app 35 | with: 36 | sdk-channel: ${{ matrix.sdk-channel }} 37 | - name: Upload app artifacts 38 | uses: actions/upload-artifact@v4 39 | with: 40 | # See ufbt action docs for other output variables 41 | name: ${{ github.event.repository.name }}-${{ steps.build-app.outputs.suffix }} 42 | path: ${{ steps.build-app.outputs.fap-artifacts }} 43 | -------------------------------------------------------------------------------- /flipper_share/scenes/flipper_share_scene_file_browser.c: -------------------------------------------------------------------------------- 1 | #include "../flipper_share_app.h" 2 | 3 | void flipper_share_scene_file_browser_on_enter(void* context) { 4 | FlipperShareApp* app = context; 5 | 6 | // Reset selected file path 7 | app->selected_file_path[0] = '\0'; 8 | 9 | // Start file browser 10 | FuriString* path = furi_string_alloc_set_str("/ext"); 11 | file_browser_start(app->file_browser, path); 12 | furi_string_free(path); 13 | 14 | // Show file browser view 15 | view_dispatcher_switch_to_view(app->view_dispatcher, FlipperShareViewIdFileBrowser); 16 | } 17 | 18 | bool flipper_share_scene_file_browser_on_event(void* context, SceneManagerEvent event) { 19 | if(!context) { 20 | return false; 21 | } 22 | 23 | FlipperShareApp* app = context; 24 | bool consumed = false; 25 | 26 | if(event.type == SceneManagerEventTypeCustom) { 27 | // Handle file selection event 28 | if(event.event == 1) { 29 | // After selecting a file, switch to show file information 30 | scene_manager_next_scene(app->scene_manager, FlipperShareSceneShowFile); 31 | consumed = true; 32 | } 33 | } else if(event.type == SceneManagerEventTypeBack) { 34 | // Handle Back button - return to main menu 35 | if(app->scene_manager) { 36 | scene_manager_previous_scene(app->scene_manager); 37 | } 38 | consumed = true; 39 | } 40 | 41 | return consumed; 42 | } 43 | 44 | void flipper_share_scene_file_browser_on_exit(void* context) { 45 | UNUSED(context); 46 | } 47 | -------------------------------------------------------------------------------- /flipper_share/subghz_share.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | // #include "../subghz_i.h" 3 | #include 4 | #include 5 | 6 | typedef struct SubGhzChatWorker SubGhzChatWorker; 7 | 8 | typedef enum { 9 | SubGhzChatEventNoEvent, 10 | SubGhzChatEventUserEntrance, 11 | SubGhzChatEventUserExit, 12 | SubGhzChatEventInputData, 13 | SubGhzChatEventRXData, 14 | SubGhzChatEventNewMessage, 15 | } SubGhzChatEventType; 16 | 17 | typedef struct { 18 | SubGhzChatEventType event; 19 | char c; 20 | } SubGhzChatEvent; 21 | 22 | // TODO: check actual usage 23 | 24 | SubGhzChatWorker* subghz_chat_worker_alloc(); 25 | void subghz_chat_worker_free(SubGhzChatWorker* instance); 26 | bool subghz_chat_worker_start( 27 | SubGhzChatWorker* instance, 28 | const SubGhzDevice* device, 29 | uint32_t frequency); 30 | void subghz_chat_worker_stop(SubGhzChatWorker* instance); 31 | bool subghz_chat_worker_is_running(SubGhzChatWorker* instance); 32 | SubGhzChatEvent subghz_chat_worker_get_event_chat(SubGhzChatWorker* instance); 33 | void subghz_chat_worker_put_event_chat(SubGhzChatWorker* instance, SubGhzChatEvent* event); 34 | size_t subghz_chat_worker_available(SubGhzChatWorker* instance); 35 | size_t subghz_chat_worker_read(SubGhzChatWorker* instance, uint8_t* data, size_t size); 36 | bool subghz_chat_worker_write(SubGhzChatWorker* instance, uint8_t* data, size_t size); 37 | 38 | const SubGhzDevice* subghz_cli_command_get_device(uint32_t* device_ind); 39 | 40 | void subghz_cli_radio_device_power_on(void); 41 | void subghz_cli_radio_device_power_off(void); 42 | 43 | uint8_t ss_subghz_init(void); 44 | uint8_t ss_subghz_deinit(void); 45 | uint8_t subghz_share_send(uint8_t* data, size_t size); 46 | -------------------------------------------------------------------------------- /flipper_share/README_catalog.md: -------------------------------------------------------------------------------- 1 | # Flipper Share - direct file transfer between flippers 2 | 3 | ## Overview 4 | 5 | Flipper Share is a wireless-enabled file sharing application for Flipper Zero. 6 | 7 | It allows to send any file over a Sub-GHz via internal transmitter directly from one Flipper Zero to another without any additional hardware, cables, smartphones, computers, internet connection and magic needed. 8 | 9 | Features: 10 | 11 | - Works from out of the box on any Flipper Zero, the simplest possible way to transfer files directly 12 | - Multiple receivers supported simultaneously and works just fine (broadcast) 13 | - Continuation of download / auto retries in case of packet loss is guaranteed at the protocol level 14 | - Integrity check with MD5 hash after file reception 15 | - File size tested is up to 1.6 MB transferred successfully, without corruption (update bundle) 16 | - Actual speed is around 800 bytes/sec, that allows to transfer average .fap file from one flipper to another in less than 1 minute 17 | - No pairing or explicit session establishment needed 18 | - No encryption, anyone nearby can receive the file, please don't send sensitive data 19 | - Fun torrent-like progress bar showing received parts of the file instead of boring usual percentage scale 20 | 21 | # Notes 22 | 23 | See the [README.md](README.md) for more details and Flipper Share protocol description. 24 | 25 | Source code of the latest version is [here](https://github.com/lomalkin/flipper-zero-apps/blob/-/flipper_share). Please feel free to open an issues and PRs if you have any ideas or found bugs. 26 | 27 | 28 | # Credits 29 | 30 | Special thanks to [@Skorpionm](https://github.com/Skorpionm/) for building a solid foundation with the Sub-GHz packet abstraction layer API — it made this app possible, convenient, and reliable. 31 | 32 | 33 | -------------------------------------------------------------------------------- /hid_app/hid.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | #include 17 | #include 18 | #include 19 | #include "views/hid_keynote.h" 20 | #include "views/hid_keyboard.h" 21 | #include "views/hid_media.h" 22 | #include "views/hid_mouse.h" 23 | #include "views/hid_mouse_clicker.h" 24 | #include "views/hid_mouse_jiggler.h" 25 | #include "views/hid_tiktok.h" 26 | #include "views/hid_slack.h" 27 | 28 | #define HID_BT_KEYS_STORAGE_NAME ".bt_hid.keys" 29 | 30 | typedef enum { 31 | HidTransportUsb, 32 | HidTransportBle, 33 | } HidTransport; 34 | 35 | typedef struct Hid Hid; 36 | 37 | struct Hid { 38 | Bt* bt; 39 | Gui* gui; 40 | NotificationApp* notifications; 41 | ViewDispatcher* view_dispatcher; 42 | Submenu* device_type_submenu; 43 | DialogEx* dialog; 44 | HidKeynote* hid_keynote; 45 | HidKeyboard* hid_keyboard; 46 | HidMedia* hid_media; 47 | HidMouse* hid_mouse; 48 | HidMouseClicker* hid_mouse_clicker; 49 | HidMouseJiggler* hid_mouse_jiggler; 50 | HidTikTok* hid_tiktok; 51 | HidSlack* hid_slack; 52 | 53 | HidTransport transport; 54 | uint32_t view_id; 55 | }; 56 | 57 | void hid_hal_keyboard_press(Hid* instance, uint16_t event); 58 | void hid_hal_keyboard_release(Hid* instance, uint16_t event); 59 | void hid_hal_keyboard_release_all(Hid* instance); 60 | 61 | void hid_hal_consumer_key_press(Hid* instance, uint16_t event); 62 | void hid_hal_consumer_key_release(Hid* instance, uint16_t event); 63 | void hid_hal_consumer_key_release_all(Hid* instance); 64 | 65 | void hid_hal_mouse_move(Hid* instance, int8_t dx, int8_t dy); 66 | void hid_hal_mouse_scroll(Hid* instance, int8_t delta); 67 | void hid_hal_mouse_press(Hid* instance, uint16_t event); 68 | void hid_hal_mouse_release(Hid* instance, uint16_t event); 69 | void hid_hal_mouse_release_all(Hid* instance); -------------------------------------------------------------------------------- /flipper_share/scenes/flipper_share_scene_show_file.c: -------------------------------------------------------------------------------- 1 | #include "../flipper_share_app.h" 2 | 3 | 4 | // Callback for handling button presses in the dialog 5 | static void dialog_ex_callback(DialogExResult result, void* context) { 6 | furi_assert(context); 7 | FlipperShareApp* app = context; 8 | 9 | if(result == DialogExResultLeft || result == DialogExResultRight) { 10 | view_dispatcher_send_custom_event(app->view_dispatcher, result); 11 | } 12 | } 13 | 14 | void flipper_share_scene_show_file_on_enter(void* context) { 15 | if(!context) { 16 | return; 17 | } 18 | 19 | FlipperShareApp* app = context; 20 | 21 | // Additional safety checks 22 | if(!app || !app->dialog_show_file || !app->view_dispatcher) { 23 | return; 24 | } 25 | 26 | // Use the selected file path from app->selected_file_path 27 | const char* file_path = app->selected_file_path[0] ? app->selected_file_path : "No file selected"; 28 | 29 | // Configure dialog with file information 30 | dialog_ex_set_header(app->dialog_show_file, "File Selected", 64, 10, AlignCenter, AlignCenter); 31 | dialog_ex_set_text(app->dialog_show_file, file_path, 64, 32, AlignCenter, AlignCenter); 32 | dialog_ex_set_left_button_text(app->dialog_show_file, "Back"); 33 | dialog_ex_set_right_button_text(app->dialog_show_file, "OK"); 34 | 35 | // Important: set up the callback for dialog buttons 36 | dialog_ex_set_context(app->dialog_show_file, app); 37 | dialog_ex_set_result_callback(app->dialog_show_file, dialog_ex_callback); 38 | 39 | view_dispatcher_switch_to_view(app->view_dispatcher, FlipperShareViewIdShowFile); 40 | } 41 | 42 | bool flipper_share_scene_show_file_on_event(void* context, SceneManagerEvent event) { 43 | if(!context) { 44 | return false; 45 | } 46 | 47 | FlipperShareApp* app = context; 48 | bool consumed = false; 49 | 50 | if(event.type == SceneManagerEventTypeCustom) { 51 | if(event.event == DialogExResultLeft) { 52 | // Back button pressed - return to file browser 53 | if(app->scene_manager) { 54 | scene_manager_previous_scene(app->scene_manager); 55 | } 56 | consumed = true; 57 | } else if(event.event == DialogExResultRight) { 58 | // OK button pressed - start reading file 59 | if(app->scene_manager) { 60 | scene_manager_next_scene(app->scene_manager, FlipperShareSceneSend); 61 | } 62 | consumed = true; 63 | } 64 | } else if(event.type == SceneManagerEventTypeBack) { 65 | // Handle back button (just in case) 66 | scene_manager_previous_scene(app->scene_manager); 67 | consumed = true; 68 | } 69 | 70 | return consumed; 71 | } 72 | 73 | void flipper_share_scene_show_file_on_exit(void* context) { 74 | UNUSED(context); 75 | } 76 | -------------------------------------------------------------------------------- /flipper_share/README.md: -------------------------------------------------------------------------------- 1 | # Flipper Share - direct file transfer between flippers 2 | 3 | ## Overview 4 | 5 | Flipper Share is a wireless-enabled file sharing application for Flipper Zero. 6 | 7 | It allows to send any file over a Sub-GHz via internal transmitter directly from one Flipper Zero to another without any additional hardware, cables, smartphones, computers, internet connection and magic needed. 8 | 9 | Features: 10 | 11 | - Works from out of the box on any Flipper Zero, the simplest possible way to transfer files directly 12 | - Multiple receivers supported simultaneously and works just fine (broadcast) 13 | - Continuation of download / auto retries in case of packet loss is guaranteed at the protocol level 14 | - Integrity check with MD5 hash after file reception 15 | - File size tested is up to 1.6 MB transferred successfully, without corruption (update bundle) 16 | - Actual speed is around 800 bytes/sec, that allows to transfer average `.fap` file from one flipper to another in less than 1 minute 17 | - No pairing or explicit session establishment needed 18 | - No encryption, anyone nearby can receive the file, please don't send sensitive data 19 | - Fun torrent-like progress bar showing received parts of the file instead of boring usual percentage scale 20 | 21 | Please feel free to open an issues and PRs if you have any ideas or found bugs. 22 | 23 | # Credits 24 | 25 | Special thanks to [@Skorpionm](https://github.com/Skorpionm/) for building a solid foundation with the Sub-GHz packet abstraction layer API — it made this app possible, convenient, and reliable. 26 | 27 | --- 28 | 29 | # Flipper Share protocol 30 | 31 | ## Packet structure 32 | 33 | Each packet is **60 bytes long** (due to Flipper CC1101 limitations). 34 | 35 | | Field | Size | Type | 36 | |---------------|----------|----------------------| 37 | | `version` | 1 byte | uint8_t | 38 | | `tx_id` | 1 byte | uint8_t | 39 | | `packet_type` | 1 byte | uint8_t | 40 | | `payload` | 56 bytes | variable (see below) | 41 | | `crc` | 1 byte | uint8_t | 42 | 43 | ## Payloads by `packet_type` 44 | 45 | ### `0x01` — Announce 46 | 47 | | Field | Size | Type | 48 | |--------------|----------|-----------------------| 49 | | `file_name` | 36 bytes | char[36], zero-padded | 50 | | `file_size` | 4 bytes | uint32_t | 51 | | `file_hash` | 16 bytes | char[16], MD5 | 52 | 53 | ### `0x02` — Request range 54 | 55 | | Field | Size | Type | 56 | |------------|----------|----------| 57 | | `start` | 4 bytes | uint32_t | 58 | | `end` | 4 bytes | uint32_t | 59 | | `padding` | 48 bytes | zero | 60 | 61 | ### `0x03` — Data 62 | 63 | | Field | Size | Type | 64 | |------------- |----------|----------| 65 | | `block_num` | 4 bytes | uint32_t | 66 | | `block_data` | 52 bytes | raw data | 67 | 68 | ## Example session 69 | 70 | **Sender:** 71 | - User selects a file to share. 72 | - The sender prepares the **announce** packet with file metadata and random **tx_id**. 73 | - The sender sends the **announce** packet periodically (every 3 sec) 74 | - When received a **request range** packet, the sender: 75 | - Checks if the **tx_id** matches the expected one. 76 | - Checks if the **start** and **end** are within the file size. 77 | - Sends **data** packets for each block in the requested range. 78 | - Each **data** packet contains a **block_num** and **block_data**. 79 | - The sender continues sending data packets until all requested blocks are sent. 80 | 81 | **Receiver:** 82 | - The receiver listens for **announce** packets. 83 | - When received an **announce** packet, the receiver displays the file information. 84 | - Receiver locks to first valid received announce's **tx_id**. 85 | - Saves **file_name**, **file_size**, and **file_hash** to internal state. 86 | - Allocates file **file_name** in output directory. 87 | - Creates map of received blocks (count calculated from **file_size** and actual block size). 88 | - The receiver sends a one-time **request range** packet with full file range to start a transfer. 89 | - On received a **data** packet, the receiver: 90 | - Checks if the **tx_id** matches the expected one. 91 | - Checks if the **block_num** is in range. 92 | - Saves the block data to the file. 93 | - If there are no other communications: 94 | - The receiver waits for some timeout and sending **request range** for the first missing blocks region. It can be optimized better in future without breaking compatibility with old versions. 95 | - If all blocks are received, the receiver: 96 | - Calculates MD5 hash of the received file. 97 | - Compares it with the announced **file_hash**. 98 | - Receiving if finished successfully or with errors (if hash mismatch). 99 | 100 | If you implement this protocol or similar in other applications or devices, I’d be happy to hear about it — please let me know! -------------------------------------------------------------------------------- /hid_app/views/hid_mouse_jiggler.c: -------------------------------------------------------------------------------- 1 | #include "hid_mouse_jiggler.h" 2 | #include 3 | #include "../hid.h" 4 | 5 | #include "hid_icons.h" 6 | 7 | #define TAG "HidMouseJiggler" 8 | 9 | struct HidMouseJiggler { 10 | View* view; 11 | Hid* hid; 12 | FuriTimer* timer; 13 | }; 14 | 15 | typedef struct { 16 | bool connected; 17 | bool running; 18 | uint8_t counter; 19 | HidTransport transport; 20 | } HidMouseJigglerModel; 21 | 22 | static void hid_mouse_jiggler_draw_callback(Canvas* canvas, void* context) { 23 | furi_assert(context); 24 | HidMouseJigglerModel* model = context; 25 | 26 | // Header 27 | if(model->transport == HidTransportBle) { 28 | if(model->connected) { 29 | canvas_draw_icon(canvas, 0, 0, &I_Ble_connected_15x15); 30 | } else { 31 | canvas_draw_icon(canvas, 0, 0, &I_Ble_disconnected_15x15); 32 | } 33 | } 34 | 35 | canvas_set_font(canvas, FontPrimary); 36 | elements_multiline_text_aligned(canvas, 17, 3, AlignLeft, AlignTop, "Mouse Jiggler"); 37 | 38 | canvas_set_font(canvas, FontPrimary); 39 | elements_multiline_text(canvas, AlignLeft, 35, "Press Start\nto jiggle"); 40 | canvas_set_font(canvas, FontSecondary); 41 | 42 | // Ok 43 | canvas_draw_icon(canvas, 63, 25, &I_Space_65x18); 44 | if(model->running) { 45 | elements_slightly_rounded_box(canvas, 66, 27, 60, 13); 46 | canvas_set_color(canvas, ColorWhite); 47 | } 48 | canvas_draw_icon(canvas, 74, 29, &I_Ok_btn_9x9); 49 | if(model->running) { 50 | elements_multiline_text_aligned(canvas, 91, 36, AlignLeft, AlignBottom, "Stop"); 51 | } else { 52 | elements_multiline_text_aligned(canvas, 91, 36, AlignLeft, AlignBottom, "Start"); 53 | } 54 | canvas_set_color(canvas, ColorBlack); 55 | 56 | // Back 57 | canvas_draw_icon(canvas, 74, 49, &I_Pin_back_arrow_10x8); 58 | elements_multiline_text_aligned(canvas, 91, 57, AlignLeft, AlignBottom, "Quit"); 59 | } 60 | 61 | static void hid_mouse_jiggler_timer_callback(void* context) { 62 | furi_assert(context); 63 | HidMouseJiggler* hid_mouse_jiggler = context; 64 | with_view_model( 65 | hid_mouse_jiggler->view, 66 | HidMouseJigglerModel * model, 67 | { 68 | if(model->running) { 69 | model->counter++; 70 | hid_hal_mouse_move( 71 | hid_mouse_jiggler->hid, 72 | (model->counter % 2 == 0) ? MOUSE_MOVE_SHORT : -MOUSE_MOVE_SHORT, 73 | 0); 74 | } 75 | }, 76 | false); 77 | } 78 | 79 | static void hid_mouse_jiggler_enter_callback(void* context) { 80 | furi_assert(context); 81 | HidMouseJiggler* hid_mouse_jiggler = context; 82 | 83 | furi_timer_start(hid_mouse_jiggler->timer, 500); 84 | } 85 | 86 | static void hid_mouse_jiggler_exit_callback(void* context) { 87 | furi_assert(context); 88 | HidMouseJiggler* hid_mouse_jiggler = context; 89 | furi_timer_stop(hid_mouse_jiggler->timer); 90 | } 91 | 92 | static bool hid_mouse_jiggler_input_callback(InputEvent* event, void* context) { 93 | furi_assert(context); 94 | HidMouseJiggler* hid_mouse_jiggler = context; 95 | 96 | bool consumed = false; 97 | 98 | if(event->type == InputTypeShort && event->key == InputKeyOk) { 99 | with_view_model( 100 | hid_mouse_jiggler->view, 101 | HidMouseJigglerModel * model, 102 | { model->running = !model->running; }, 103 | true); 104 | consumed = true; 105 | } 106 | 107 | return consumed; 108 | } 109 | 110 | HidMouseJiggler* hid_mouse_jiggler_alloc(Hid* hid) { 111 | HidMouseJiggler* hid_mouse_jiggler = malloc(sizeof(HidMouseJiggler)); 112 | 113 | hid_mouse_jiggler->view = view_alloc(); 114 | view_set_context(hid_mouse_jiggler->view, hid_mouse_jiggler); 115 | view_allocate_model( 116 | hid_mouse_jiggler->view, ViewModelTypeLocking, sizeof(HidMouseJigglerModel)); 117 | view_set_draw_callback(hid_mouse_jiggler->view, hid_mouse_jiggler_draw_callback); 118 | view_set_input_callback(hid_mouse_jiggler->view, hid_mouse_jiggler_input_callback); 119 | view_set_enter_callback(hid_mouse_jiggler->view, hid_mouse_jiggler_enter_callback); 120 | view_set_exit_callback(hid_mouse_jiggler->view, hid_mouse_jiggler_exit_callback); 121 | 122 | hid_mouse_jiggler->hid = hid; 123 | 124 | hid_mouse_jiggler->timer = furi_timer_alloc( 125 | hid_mouse_jiggler_timer_callback, FuriTimerTypePeriodic, hid_mouse_jiggler); 126 | 127 | with_view_model( 128 | hid_mouse_jiggler->view, 129 | HidMouseJigglerModel * model, 130 | { model->transport = hid->transport; }, 131 | true); 132 | 133 | return hid_mouse_jiggler; 134 | } 135 | 136 | void hid_mouse_jiggler_free(HidMouseJiggler* hid_mouse_jiggler) { 137 | furi_assert(hid_mouse_jiggler); 138 | 139 | furi_timer_stop(hid_mouse_jiggler->timer); 140 | furi_timer_free(hid_mouse_jiggler->timer); 141 | 142 | view_free(hid_mouse_jiggler->view); 143 | 144 | free(hid_mouse_jiggler); 145 | } 146 | 147 | View* hid_mouse_jiggler_get_view(HidMouseJiggler* hid_mouse_jiggler) { 148 | furi_assert(hid_mouse_jiggler); 149 | return hid_mouse_jiggler->view; 150 | } 151 | 152 | void hid_mouse_jiggler_set_connected_status(HidMouseJiggler* hid_mouse_jiggler, bool connected) { 153 | furi_assert(hid_mouse_jiggler); 154 | with_view_model( 155 | hid_mouse_jiggler->view, 156 | HidMouseJigglerModel * model, 157 | { model->connected = connected; }, 158 | true); 159 | } 160 | -------------------------------------------------------------------------------- /flipper_share/subghz_share.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include 6 | 7 | #include // otg 8 | #include // NotificationApp 9 | #include 10 | #include 11 | #include 12 | 13 | #include 14 | 15 | #include "subghz_share.h" 16 | #include "flipper_share.h" 17 | 18 | #define TAG "SubGhzShare" 19 | 20 | #define SUBGHZ_MESSAGE_LEN_MAX 60 21 | 22 | #define SUBGHZ_DEVICE_CC1101_INT_NAME "cc1101_int" 23 | #define RECORD_POWER "power" 24 | typedef struct Power Power; 25 | 26 | struct SubGhzChatWorker { 27 | FuriThread* thread; 28 | SubGhzTxRxWorker* subghz_txrx; 29 | 30 | volatile bool worker_running; 31 | volatile bool worker_stoping; 32 | FuriMessageQueue* event_queue; 33 | uint32_t last_time_rx_data; 34 | 35 | // PipeSide* pipe; 36 | }; 37 | 38 | SubGhzChatWorker* subghz_chat = NULL; 39 | 40 | void subghz_cli_radio_device_power_on(void) { 41 | Power* power = furi_record_open(RECORD_POWER); 42 | power_enable_otg(power, true); 43 | furi_record_close(RECORD_POWER); 44 | } 45 | 46 | void subghz_cli_radio_device_power_off(void) { 47 | Power* power = furi_record_open(RECORD_POWER); 48 | power_enable_otg(power, false); 49 | furi_record_close(RECORD_POWER); 50 | } 51 | 52 | uint8_t subghz_share_send(uint8_t* data, size_t size) { 53 | // FURI_LOG_I(TAG, "subghz_share_send: start"); 54 | furi_assert(subghz_chat); 55 | if(!subghz_chat) { 56 | FURI_LOG_E(TAG, "subghz_share_send: subghz_chat is NULL"); 57 | return 1; 58 | } 59 | if(!subghz_chat->subghz_txrx) { 60 | FURI_LOG_E(TAG, "subghz_share_send: subghz_chat or subghz_txrx is NULL"); 61 | return 2; 62 | } 63 | if(!subghz_tx_rx_worker_write(subghz_chat->subghz_txrx, data, size)) { 64 | FURI_LOG_W(TAG, "subghz_share_send: write failed"); 65 | return 3; 66 | } 67 | 68 | return 0; 69 | } 70 | 71 | uint8_t ss_subghz_deinit() { 72 | FURI_LOG_T(TAG, "ss_subghz_deinit: start"); 73 | 74 | if(!subghz_chat) { 75 | FURI_LOG_I(TAG, "ss_subghz_deinit: already deinitialized"); 76 | return 0; 77 | } 78 | 79 | /* stop worker if running */ 80 | if(subghz_chat->subghz_txrx && subghz_tx_rx_worker_is_running(subghz_chat->subghz_txrx)) { 81 | subghz_tx_rx_worker_stop(subghz_chat->subghz_txrx); 82 | } 83 | 84 | subghz_chat->worker_running = false; 85 | 86 | /* try to deinit devices (safe to call repeatedly) */ 87 | subghz_devices_deinit(); 88 | 89 | /* restore power mode */ 90 | furi_hal_power_suppress_charge_exit(); 91 | 92 | /* free queue if allocated */ 93 | if(subghz_chat->event_queue) { 94 | furi_message_queue_free(subghz_chat->event_queue); 95 | subghz_chat->event_queue = NULL; 96 | } 97 | 98 | /* free txrx worker if allocated */ 99 | if(subghz_chat->subghz_txrx) { 100 | subghz_tx_rx_worker_free(subghz_chat->subghz_txrx); 101 | subghz_chat->subghz_txrx = NULL; 102 | } 103 | 104 | /* free thread if allocated */ 105 | if(subghz_chat->thread) { 106 | // if thread is joinable, join if needed; otherwise free 107 | // furi_thread_join(subghz_chat->thread); // optional 108 | furi_thread_free(subghz_chat->thread); 109 | subghz_chat->thread = NULL; 110 | } 111 | 112 | free(subghz_chat); 113 | subghz_chat = NULL; 114 | 115 | FURI_LOG_T(TAG, "ss_subghz_deinit: done"); 116 | return 0; 117 | } 118 | 119 | static void ss_subghz_rx_callback(void* ctx) { 120 | uint8_t buffer[SUBGHZ_MESSAGE_LEN_MAX]; 121 | size_t len = subghz_tx_rx_worker_read(subghz_chat->subghz_txrx, buffer, sizeof(buffer)); 122 | if(len > 0) { 123 | // FURI_LOG_I(TAG, "ss_subghz_rx_callback: received %zu bytes", len); 124 | // FURI_LOG_I(TAG, "ss_subghz_rx_callback: received %zu bytes: %.*s", len, (int)len, buffer); 125 | // if (fs_receive_callback(buffer, len) != 0) { 126 | // FURI_LOG_E(TAG, "ss_subghz_rx_callback: failed to add packet to decoder"); 127 | // } 128 | fs_receive_callback(buffer, len); 129 | } 130 | furi_assert(ctx); 131 | } 132 | 133 | uint8_t ss_subghz_init() { 134 | FURI_LOG_I(TAG, "ss_subghz_init: start"); 135 | 136 | /* already initialized? */ 137 | if(subghz_chat) { 138 | FURI_LOG_W(TAG, "ss_subghz_init: already initialized"); 139 | return 0; 140 | } 141 | 142 | subghz_chat = malloc(sizeof(SubGhzChatWorker)); 143 | furi_assert(subghz_chat); 144 | 145 | uint32_t frequency = 433920000; // TODO: define / make configurable? 146 | 147 | // TODO: check if allowed in this region? 148 | 149 | // FURI_LOG_I(TAG, "ss_subghz_init: subghz_devices_init"); 150 | subghz_devices_init(); 151 | 152 | // FURI_LOG_I(TAG, "ss_subghz_init: subghz_devices_get_by_name"); 153 | const SubGhzDevice* device = subghz_devices_get_by_name(SUBGHZ_DEVICE_CC1101_INT_NAME); 154 | // FURI_LOG_I(TAG, "ss_subghz_init: check if the device is connected"); 155 | if(!subghz_devices_is_connect(device)) { 156 | subghz_cli_radio_device_power_off(); 157 | device = subghz_devices_get_by_name(SUBGHZ_DEVICE_CC1101_INT_NAME); 158 | } 159 | furi_assert(device); 160 | 161 | // FURI_LOG_I(TAG, "ss_subghz_init: furi_message_queue_alloc"); 162 | subghz_chat->event_queue = furi_message_queue_alloc(80, sizeof(SubGhzChatEvent)); 163 | 164 | // FURI_LOG_I(TAG, "ss_subghz_init: subghz_tx_rx_worker_alloc"); 165 | furi_assert(subghz_chat); 166 | subghz_chat->subghz_txrx = subghz_tx_rx_worker_alloc(); 167 | furi_assert(subghz_chat->subghz_txrx); 168 | 169 | // FURI_LOG_I(TAG, "ss_subghz_init: subghz_tx_rx_worker_start"); 170 | if(subghz_tx_rx_worker_start(subghz_chat->subghz_txrx, device, frequency)) { 171 | furi_message_queue_reset(subghz_chat->event_queue); 172 | subghz_tx_rx_worker_set_callback_have_read( 173 | subghz_chat->subghz_txrx, ss_subghz_rx_callback, subghz_chat); 174 | } 175 | 176 | furi_hal_power_suppress_charge_enter(); 177 | 178 | FURI_LOG_I(TAG, "ss_subghz_init: SubGhz init done"); 179 | return 0; 180 | } 181 | -------------------------------------------------------------------------------- /flipper_share/flipper_share.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | #define FS_PACKET_LENGTH 60 // Total max packet length 8 | #define FS_PAYLOAD_LENGTH (FS_PACKET_LENGTH - 4) // 4 for version, tx_id, packet_type, crc 9 | #define FS_FILENAME_LENGTH (FS_PAYLOAD_LENGTH - 4 - 16) // 4 for file size, 16 for MD5 hash 10 | #define FS_DATA_LENGTH (FS_PAYLOAD_LENGTH - 4) // 4 for block number 11 | 12 | #define FS_TIMEOUT_IDLE 1000 // ms 13 | #define FS_TIMEOUT_BETWEEN_PACKETS 120 // 70 ms // TODO: optimize? 14 | 15 | #define FS_SUBGHZ_RETRY_DELAY_MS 1000 // For Retry On error 16 | 17 | #define FS_PARTS_COUNT 100u // For progress bar in GUI 18 | 19 | #define FS_PARTS_BYTES ((uint32_t)((FS_PARTS_COUNT + 7u) / 8u)) 20 | 21 | // Packet structure: 22 | typedef struct { 23 | uint8_t version; 24 | uint8_t tx_id; 25 | uint8_t packet_type; 26 | uint8_t payload[FS_PAYLOAD_LENGTH]; 27 | uint8_t crc; 28 | } FS_packet_t; 29 | 30 | // Packet types: 31 | typedef enum { 32 | FS_PKT_ANNOUNCE = 1, 33 | FS_PKT_REQUEST = 2, 34 | FS_PKT_DATA = 3, 35 | } fs_pkt_type_t; 36 | 37 | // Payloads: 38 | typedef struct { 39 | uint8_t file_name[FS_FILENAME_LENGTH]; 40 | uint32_t file_size; 41 | uint8_t hash_md5[16]; 42 | } FS_pl_announce_t; 43 | 44 | typedef struct { 45 | uint32_t range_start; // Bytes, not blocks 46 | uint32_t range_end; // Bytes, not blocks 47 | } FS_pl_request_t; 48 | 49 | typedef struct { 50 | uint32_t block_number; // Block number, not byte offset 51 | uint8_t data[FS_DATA_LENGTH]; 52 | } FS_pl_data_t; 53 | 54 | // Modes: 55 | typedef enum { 56 | FS_MODE_NONE = 0, 57 | FS_MODE_SENDER, 58 | FS_MODE_RECEIVER 59 | } fs_mode_t; 60 | 61 | typedef enum { 62 | FS_ST_IDLE = 0, 63 | FS_ST_ANNOUNCING, // sender 64 | FS_ST_RECEIVING // receiver 65 | } fs_state_t; 66 | 67 | typedef struct { 68 | fs_mode_t mode; 69 | fs_state_t state; 70 | uint8_t tx_id; // random pseudo-session tx_id, must match on both sides 71 | uint32_t last_tick_ms; // for internal step 72 | uint32_t last_announce_ms; // 73 | uint32_t last_rx_ms; // LRU rx 74 | 75 | void (*cb_send_bytes)(const uint8_t* buf, size_t len); // Radio send bytes callback 76 | uint32_t (*cb_now_ms)(void); // Time callback, monotonic ms 77 | 78 | // Sender data: 79 | char s_file_path[256]; 80 | char s_file_name[FS_FILENAME_LENGTH]; 81 | uint32_t s_file_size; 82 | unsigned char s_md5[16]; 83 | 84 | Storage* storage; // Storage record for file operations 85 | File* file; // File handle for reading the file 86 | 87 | uint8_t s_is_blocks_requested; 88 | uint32_t s_block_needed_first; 89 | uint32_t s_block_needed_last; 90 | 91 | // Callback for reading a data block (FS_DATA_LENGTH bytes) 92 | // MUST fill out52 (zero-pad the incomplete last block). 93 | // Returns the actual number of "valid" bytes in this block (for the last one), 94 | // usually 52, and for the last one — the remainder. (The receiver will trim it based on file_size) 95 | uint32_t (*cb_read_block)(uint32_t block_number, uint8_t out52[FS_DATA_LENGTH]); // Reader callback from real storage 96 | 97 | // Receiver data: 98 | bool r_locked; // if tx_id of receiver matches to tx_id of sender 99 | uint8_t r_locked_tx_id; // TODO: no need for receiver? 100 | 101 | char r_file_path[256]; // Build actual path when announce handling and lock session to tx_id 102 | char r_file_name[FS_FILENAME_LENGTH]; 103 | uint32_t r_file_size; 104 | uint32_t r_blocks_needed; // blocks total needed, calculated from file_size during announce handling 105 | uint32_t r_blocks_received; // how many have been received 106 | unsigned char r_md5[16]; 107 | bool r_is_finished; 108 | bool r_is_success; 109 | 110 | // Callback for writing a received block data by number to real storage. 111 | // in52 is always 52 bytes, but must write min(52, remainder). 112 | void (*cb_write_block)(uint32_t block_number, const uint8_t in52[FS_DATA_LENGTH], uint32_t valid_len); 113 | } fs_ctx_t; 114 | 115 | extern fs_ctx_t g; // extern to be available in GUI 116 | 117 | // Public API: 118 | 119 | // Init structure for both: Receiver and Sender 120 | typedef struct { 121 | fs_mode_t mode; // SENDER / RECEIVER 122 | uint8_t tx_id; // pseudo-random session ID for SENDER, RECEIVER will lock to it on ANNOUNCE handling 123 | 124 | void (*send_bytes)(const uint8_t* buf, size_t len); // Radio send bytes callback 125 | uint32_t (*now_ms)(void); // Time callback, monotonic ms 126 | 127 | // sender-specific, (required if mode == SENDER) 128 | char s_file_path[256]; 129 | uint32_t (*s_read_block)(uint32_t block_number, uint8_t out52[FS_DATA_LENGTH]); 130 | 131 | // receiver-specific (required if mode == RECEIVER) 132 | void (*r_write_block)(uint32_t block_number, const uint8_t in52[FS_DATA_LENGTH], uint32_t valid_len); 133 | } fs_init_params_t; 134 | 135 | // API functions: 136 | bool fs_init_from_external_transmit(); 137 | bool fs_init_from_external_receive(); 138 | bool fs_init(const fs_init_params_t* p); 139 | void fs_deinit(void); 140 | void fs_idle(void); // to be called periodically from main loop (50ms?) 141 | 142 | // High-level sending (can be called from outside): 143 | void fs_send_announce(void); 144 | void fs_send_request(uint32_t range_start, uint32_t range_end); 145 | // void fs_send_data(uint32_t block_number); 146 | void fs_send_data(void); 147 | 148 | // External callback for receiving "raw" 60 bytes from radio: 149 | void fs_receive_callback(const uint8_t* buf, size_t size); 150 | 151 | // Parts for progress bar in GUI 152 | 153 | bool fs_parts_init(uint32_t block_count); 154 | void fs_parts_reset(void); 155 | void fs_parts_on_block_set(uint32_t block_index); 156 | int fs_parts_get(uint32_t part_index); 157 | void fs_parts_bitmap_copy(uint8_t* dst); 158 | 159 | uint32_t fs_parts_count(void); 160 | uint32_t fs_parts_block_count(void); 161 | bool fs_parts_is_ready(void); -------------------------------------------------------------------------------- /hid_app/views/hid_mouse_clicker.c: -------------------------------------------------------------------------------- 1 | #include "hid_mouse_clicker.h" 2 | #include 3 | #include "../hid.h" 4 | 5 | #include "hid_icons.h" 6 | 7 | #define TAG "HidMouseClicker" 8 | #define DEFAULT_CLICK_RATE 1 9 | #define MAXIMUM_CLICK_RATE 60 10 | 11 | struct HidMouseClicker { 12 | View* view; 13 | Hid* hid; 14 | FuriTimer* timer; 15 | }; 16 | 17 | typedef struct { 18 | bool connected; 19 | bool running; 20 | int rate; 21 | HidTransport transport; 22 | } HidMouseClickerModel; 23 | 24 | static void hid_mouse_clicker_start_or_restart_timer(void* context) { 25 | furi_assert(context); 26 | HidMouseClicker* hid_mouse_clicker = context; 27 | 28 | if(furi_timer_is_running(hid_mouse_clicker->timer)) { 29 | furi_timer_stop(hid_mouse_clicker->timer); 30 | } 31 | 32 | with_view_model( 33 | hid_mouse_clicker->view, 34 | HidMouseClickerModel * model, 35 | { 36 | furi_timer_start( 37 | hid_mouse_clicker->timer, furi_kernel_get_tick_frequency() / model->rate); 38 | }, 39 | true); 40 | } 41 | 42 | static void hid_mouse_clicker_draw_callback(Canvas* canvas, void* context) { 43 | furi_assert(context); 44 | HidMouseClickerModel* model = context; 45 | 46 | // Header 47 | if(model->transport == HidTransportBle) { 48 | if(model->connected) { 49 | canvas_draw_icon(canvas, 0, 0, &I_Ble_connected_15x15); 50 | } else { 51 | canvas_draw_icon(canvas, 0, 0, &I_Ble_disconnected_15x15); 52 | } 53 | } 54 | 55 | canvas_set_font(canvas, FontPrimary); 56 | elements_multiline_text_aligned(canvas, 17, 3, AlignLeft, AlignTop, "Mouse Clicker"); 57 | 58 | // Ok 59 | canvas_draw_icon(canvas, 63, 25, &I_Space_65x18); 60 | if(model->running) { 61 | canvas_set_font(canvas, FontPrimary); 62 | 63 | FuriString* rate_label = furi_string_alloc(); 64 | furi_string_printf(rate_label, "%d clicks/s\n\nUp / Down", model->rate); 65 | elements_multiline_text(canvas, AlignLeft, 35, furi_string_get_cstr(rate_label)); 66 | canvas_set_font(canvas, FontSecondary); 67 | furi_string_free(rate_label); 68 | 69 | elements_slightly_rounded_box(canvas, 66, 27, 60, 13); 70 | canvas_set_color(canvas, ColorWhite); 71 | } else { 72 | canvas_set_font(canvas, FontPrimary); 73 | elements_multiline_text(canvas, AlignLeft, 35, "Press Start\nto start\nclicking"); 74 | canvas_set_font(canvas, FontSecondary); 75 | } 76 | canvas_draw_icon(canvas, 74, 29, &I_Ok_btn_9x9); 77 | if(model->running) { 78 | elements_multiline_text_aligned(canvas, 91, 36, AlignLeft, AlignBottom, "Stop"); 79 | } else { 80 | elements_multiline_text_aligned(canvas, 91, 36, AlignLeft, AlignBottom, "Start"); 81 | } 82 | canvas_set_color(canvas, ColorBlack); 83 | 84 | // Back 85 | canvas_draw_icon(canvas, 74, 49, &I_Pin_back_arrow_10x8); 86 | elements_multiline_text_aligned(canvas, 91, 57, AlignLeft, AlignBottom, "Quit"); 87 | } 88 | 89 | static void hid_mouse_clicker_timer_callback(void* context) { 90 | furi_assert(context); 91 | HidMouseClicker* hid_mouse_clicker = context; 92 | with_view_model( 93 | hid_mouse_clicker->view, 94 | HidMouseClickerModel * model, 95 | { 96 | if(model->running) { 97 | hid_hal_mouse_press(hid_mouse_clicker->hid, HID_MOUSE_BTN_LEFT); 98 | hid_hal_mouse_release(hid_mouse_clicker->hid, HID_MOUSE_BTN_LEFT); 99 | } 100 | }, 101 | false); 102 | } 103 | 104 | static void hid_mouse_clicker_enter_callback(void* context) { 105 | hid_mouse_clicker_start_or_restart_timer(context); 106 | } 107 | 108 | static void hid_mouse_clicker_exit_callback(void* context) { 109 | furi_assert(context); 110 | HidMouseClicker* hid_mouse_clicker = context; 111 | furi_timer_stop(hid_mouse_clicker->timer); 112 | } 113 | 114 | static bool hid_mouse_clicker_input_callback(InputEvent* event, void* context) { 115 | furi_assert(context); 116 | HidMouseClicker* hid_mouse_clicker = context; 117 | 118 | bool consumed = false; 119 | bool rate_changed = false; 120 | 121 | if(event->type != InputTypeShort && event->type != InputTypeRepeat) { 122 | return false; 123 | } 124 | 125 | with_view_model( 126 | hid_mouse_clicker->view, 127 | HidMouseClickerModel * model, 128 | { 129 | switch(event->key) { 130 | case InputKeyOk: 131 | model->running = !model->running; 132 | consumed = true; 133 | break; 134 | case InputKeyUp: 135 | if(model->rate < MAXIMUM_CLICK_RATE) { 136 | model->rate++; 137 | } 138 | rate_changed = true; 139 | consumed = true; 140 | break; 141 | case InputKeyDown: 142 | if(model->rate > 1) { 143 | model->rate--; 144 | } 145 | rate_changed = true; 146 | consumed = true; 147 | break; 148 | default: 149 | consumed = true; 150 | break; 151 | } 152 | }, 153 | true); 154 | 155 | if(rate_changed) { 156 | hid_mouse_clicker_start_or_restart_timer(context); 157 | } 158 | 159 | return consumed; 160 | } 161 | 162 | HidMouseClicker* hid_mouse_clicker_alloc(Hid* hid) { 163 | HidMouseClicker* hid_mouse_clicker = malloc(sizeof(HidMouseClicker)); 164 | 165 | hid_mouse_clicker->view = view_alloc(); 166 | view_set_context(hid_mouse_clicker->view, hid_mouse_clicker); 167 | view_allocate_model( 168 | hid_mouse_clicker->view, ViewModelTypeLocking, sizeof(HidMouseClickerModel)); 169 | view_set_draw_callback(hid_mouse_clicker->view, hid_mouse_clicker_draw_callback); 170 | view_set_input_callback(hid_mouse_clicker->view, hid_mouse_clicker_input_callback); 171 | view_set_enter_callback(hid_mouse_clicker->view, hid_mouse_clicker_enter_callback); 172 | view_set_exit_callback(hid_mouse_clicker->view, hid_mouse_clicker_exit_callback); 173 | 174 | hid_mouse_clicker->hid = hid; 175 | 176 | hid_mouse_clicker->timer = furi_timer_alloc( 177 | hid_mouse_clicker_timer_callback, FuriTimerTypePeriodic, hid_mouse_clicker); 178 | 179 | with_view_model( 180 | hid_mouse_clicker->view, 181 | HidMouseClickerModel * model, 182 | { 183 | model->transport = hid->transport; 184 | model->rate = DEFAULT_CLICK_RATE; 185 | }, 186 | true); 187 | 188 | return hid_mouse_clicker; 189 | } 190 | 191 | void hid_mouse_clicker_free(HidMouseClicker* hid_mouse_clicker) { 192 | furi_assert(hid_mouse_clicker); 193 | 194 | furi_timer_stop(hid_mouse_clicker->timer); 195 | furi_timer_free(hid_mouse_clicker->timer); 196 | 197 | view_free(hid_mouse_clicker->view); 198 | 199 | free(hid_mouse_clicker); 200 | } 201 | 202 | View* hid_mouse_clicker_get_view(HidMouseClicker* hid_mouse_clicker) { 203 | furi_assert(hid_mouse_clicker); 204 | return hid_mouse_clicker->view; 205 | } 206 | 207 | void hid_mouse_clicker_set_connected_status(HidMouseClicker* hid_mouse_clicker, bool connected) { 208 | furi_assert(hid_mouse_clicker); 209 | with_view_model( 210 | hid_mouse_clicker->view, 211 | HidMouseClickerModel * model, 212 | { model->connected = connected; }, 213 | true); 214 | } 215 | -------------------------------------------------------------------------------- /flipper_share/scenes/flipper_share_scene_send.c: -------------------------------------------------------------------------------- 1 | #include "../flipper_share_app.h" 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | #include "subghz_share.h" 8 | #include "flipper_share.h" 9 | 10 | #define FS_IDLE_OPERATION 50 //ms 11 | 12 | #define TAG "FlipperShareSend" 13 | 14 | typedef struct { 15 | uint32_t counter; 16 | bool reading_complete; 17 | FuriThread* worker_thread; 18 | } FileReadingState; 19 | 20 | static void dialog_ex_callback(DialogExResult result, void* context); 21 | static void update_timer_callback(void* context); 22 | 23 | static FileReadingState* file_reading_state_alloc() { 24 | FileReadingState* state = malloc(sizeof(FileReadingState)); 25 | state->counter = 0; 26 | state->reading_complete = false; 27 | state->worker_thread = NULL; 28 | return state; 29 | } 30 | 31 | static void file_reading_state_free(FileReadingState* state) { 32 | if(state->worker_thread) { 33 | furi_thread_free(state->worker_thread); 34 | } 35 | free(state); 36 | } 37 | 38 | static int32_t file_read_worker_thread(void* context) { 39 | FlipperShareApp* app = context; 40 | FileReadingState* state = (FileReadingState*)app->file_reading_state; 41 | 42 | bool is_running = true; 43 | 44 | fs_init_from_external_transmit(app->selected_file_path); 45 | 46 | state->counter = g.s_file_size; 47 | 48 | while(is_running) { 49 | fs_idle(); 50 | 51 | furi_delay_ms(FS_IDLE_OPERATION); 52 | 53 | // Check if we should stop 54 | if(furi_thread_flags_get() & 0x1) { 55 | is_running = false; 56 | } 57 | } 58 | 59 | state->reading_complete = true; 60 | 61 | return 0; 62 | } 63 | 64 | void flipper_share_scene_send_on_enter(void* context) { 65 | FlipperShareApp* app = context; 66 | 67 | // Create state for the scene 68 | FileReadingState* state = file_reading_state_alloc(); 69 | app->file_reading_state = state; 70 | 71 | // Setup dialog to show progress 72 | dialog_ex_set_header(app->dialog_show_file, "Sending...", 64, 10, AlignCenter, AlignCenter); 73 | dialog_ex_set_text(app->dialog_show_file, "Starting...", 64, 32, AlignCenter, AlignCenter); 74 | dialog_ex_set_left_button_text(app->dialog_show_file, "Cancel"); 75 | dialog_ex_set_right_button_text(app->dialog_show_file, NULL); // Skip right button 76 | 77 | // Setup callback for dialog buttons 78 | dialog_ex_set_context(app->dialog_show_file, app); 79 | dialog_ex_set_result_callback(app->dialog_show_file, dialog_ex_callback); 80 | 81 | // Start thread for reading file 82 | state->worker_thread = 83 | furi_thread_alloc_ex("FileReadWorker", 2048, file_read_worker_thread, app); 84 | furi_thread_start(state->worker_thread); 85 | 86 | // Show dialog 87 | view_dispatcher_switch_to_view(app->view_dispatcher, FlipperShareViewIdShowFile); 88 | 89 | // Start timer for updating display 90 | app->timer = furi_timer_alloc(update_timer_callback, FuriTimerTypePeriodic, app); 91 | furi_timer_start(app->timer, 250); 92 | 93 | ss_subghz_init(); // TODO Move to thread? 94 | } 95 | 96 | // Callback for handling button presses in the dialog 97 | static void dialog_ex_callback(DialogExResult result, void* context) { 98 | furi_assert(context); 99 | FlipperShareApp* app = context; 100 | 101 | if(result == DialogExResultLeft) { 102 | view_dispatcher_send_custom_event(app->view_dispatcher, DialogExResultLeft); 103 | } 104 | } 105 | 106 | static void update_timer_callback(void* context) { 107 | furi_assert(context); 108 | FlipperShareApp* app = context; 109 | 110 | FileReadingState* state = (FileReadingState*)app->file_reading_state; 111 | 112 | char progress_text[255]; 113 | if(state) { 114 | if(state->reading_complete) { 115 | snprintf( 116 | progress_text, sizeof(progress_text), "Complete! %lu bytes read", state->counter); 117 | 118 | dialog_ex_set_right_button_text(app->dialog_show_file, "OK"); 119 | } else { 120 | // Print filename and size 121 | const char* prefix = ""; 122 | int pref_len = snprintf(progress_text, sizeof(progress_text), "%s", prefix); 123 | if(pref_len < 0) pref_len = 0; 124 | int avail = (int)sizeof(progress_text) - pref_len - 1; 125 | if(avail < 0) avail = 0; 126 | snprintf( 127 | progress_text + pref_len, 128 | (size_t)avail + 1, 129 | "%.*s, %lu KB", 130 | avail, 131 | g.s_file_name, 132 | g.s_file_size / 1024); 133 | 134 | // int len = strlen(progress_text); 135 | // if(len < (int)sizeof(progress_text) - 1) { 136 | // // int percent = (int)((state->counter * 100) / app->selected_file_size); 137 | // // snprintf(progress_text + len, sizeof(progress_text) - (size_t)len, ""); 138 | // } 139 | } 140 | // Update dialog text 141 | dialog_ex_set_text(app->dialog_show_file, progress_text, 64, 32, AlignCenter, AlignCenter); 142 | } 143 | // snprintf( 144 | // progress_text, 145 | // 255, 146 | // "%s\n %u bytes", 147 | // app->selected_file_path, 148 | // app->selected_file_size); 149 | 150 | //snprintf(progress_text, sizeof(progress_text), " %lu bytes", state->counter); 151 | 152 | // Update dialog text 153 | dialog_ex_set_text(app->dialog_show_file, progress_text, 64, 32, AlignCenter, AlignCenter); 154 | } 155 | 156 | bool flipper_share_scene_send_on_event(void* context, SceneManagerEvent event) { 157 | FlipperShareApp* app = context; 158 | bool consumed = false; 159 | 160 | if(event.type == SceneManagerEventTypeCustom) { 161 | if(event.event == DialogExResultLeft) { 162 | // Cancel button pressed - stop reading and return to file info 163 | FileReadingState* state = (FileReadingState*)app->file_reading_state; 164 | if(state && state->worker_thread) { 165 | furi_thread_flags_set(furi_thread_get_id(state->worker_thread), 0x1); 166 | furi_thread_join(state->worker_thread); 167 | } 168 | 169 | // Stop timer 170 | if(app->timer) { 171 | furi_timer_stop(app->timer); 172 | furi_timer_free(app->timer); 173 | app->timer = NULL; 174 | } 175 | 176 | scene_manager_previous_scene(app->scene_manager); 177 | consumed = true; 178 | } else if(event.event == DialogExResultRight) { 179 | // OK button pressed - return to file info 180 | // Only available when completed 181 | 182 | // Stop timer 183 | if(app->timer) { 184 | furi_timer_stop(app->timer); 185 | furi_timer_free(app->timer); 186 | app->timer = NULL; 187 | } 188 | 189 | scene_manager_previous_scene(app->scene_manager); 190 | consumed = true; 191 | } 192 | } else if(event.type == SceneManagerEventTypeBack) { 193 | // Back button - same as Cancel 194 | FileReadingState* state = (FileReadingState*)app->file_reading_state; 195 | if(state && state->worker_thread) { 196 | furi_thread_flags_set(furi_thread_get_id(state->worker_thread), 0x1); 197 | furi_thread_join(state->worker_thread); 198 | } 199 | 200 | // Stop timer 201 | if(app->timer) { 202 | furi_timer_stop(app->timer); 203 | furi_timer_free(app->timer); 204 | app->timer = NULL; 205 | } 206 | 207 | scene_manager_previous_scene(app->scene_manager); 208 | consumed = true; 209 | } 210 | 211 | return consumed; 212 | } 213 | 214 | void flipper_share_scene_send_on_exit(void* context) { 215 | FlipperShareApp* app = context; 216 | 217 | if(ss_subghz_deinit()) { 218 | FURI_LOG_W(TAG, "ss_subghz_deinit reported error on scene exit"); 219 | } 220 | 221 | // Free resources 222 | if(app->file_reading_state) { 223 | file_reading_state_free((FileReadingState*)app->file_reading_state); 224 | app->file_reading_state = NULL; 225 | } 226 | 227 | // Just in case, check that the timer is stopped 228 | if(app->timer) { 229 | furi_timer_stop(app->timer); 230 | furi_timer_free(app->timer); 231 | app->timer = NULL; 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /hid_app/views/hid_media.c: -------------------------------------------------------------------------------- 1 | #include "hid_media.h" 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include "../hid.h" 7 | 8 | #include "hid_icons.h" 9 | 10 | #define TAG "HidMedia" 11 | 12 | struct HidMedia { 13 | View* view; 14 | Hid* hid; 15 | }; 16 | 17 | typedef struct { 18 | bool left_pressed; 19 | bool up_pressed; 20 | bool right_pressed; 21 | bool down_pressed; 22 | bool ok_pressed; 23 | bool connected; 24 | HidTransport transport; 25 | } HidMediaModel; 26 | 27 | static void hid_media_draw_arrow(Canvas* canvas, uint8_t x, uint8_t y, CanvasDirection dir) { 28 | canvas_draw_triangle(canvas, x, y, 5, 3, dir); 29 | if(dir == CanvasDirectionBottomToTop) { 30 | canvas_draw_dot(canvas, x, y - 1); 31 | } else if(dir == CanvasDirectionTopToBottom) { 32 | canvas_draw_dot(canvas, x, y + 1); 33 | } else if(dir == CanvasDirectionRightToLeft) { 34 | canvas_draw_dot(canvas, x - 1, y); 35 | } else if(dir == CanvasDirectionLeftToRight) { 36 | canvas_draw_dot(canvas, x + 1, y); 37 | } 38 | } 39 | 40 | static void hid_media_draw_callback(Canvas* canvas, void* context) { 41 | furi_assert(context); 42 | HidMediaModel* model = context; 43 | 44 | // Header 45 | if(model->transport == HidTransportBle) { 46 | if(model->connected) { 47 | canvas_draw_icon(canvas, 0, 0, &I_Ble_connected_15x15); 48 | } else { 49 | canvas_draw_icon(canvas, 0, 0, &I_Ble_disconnected_15x15); 50 | } 51 | } 52 | 53 | canvas_set_font(canvas, FontPrimary); 54 | elements_multiline_text_aligned(canvas, 17, 3, AlignLeft, AlignTop, "Media"); 55 | canvas_set_font(canvas, FontSecondary); 56 | 57 | // Keypad circles 58 | canvas_draw_icon(canvas, 76, 8, &I_Circles_47x47); 59 | 60 | // Up 61 | if(model->up_pressed) { 62 | canvas_set_bitmap_mode(canvas, 1); 63 | canvas_draw_icon(canvas, 93, 9, &I_Pressed_Button_13x13); 64 | canvas_set_bitmap_mode(canvas, 0); 65 | canvas_set_color(canvas, ColorWhite); 66 | } 67 | canvas_draw_icon(canvas, 96, 12, &I_Volup_8x6); 68 | canvas_set_color(canvas, ColorBlack); 69 | 70 | // Down 71 | if(model->down_pressed) { 72 | canvas_set_bitmap_mode(canvas, 1); 73 | canvas_draw_icon(canvas, 93, 41, &I_Pressed_Button_13x13); 74 | canvas_set_bitmap_mode(canvas, 0); 75 | canvas_set_color(canvas, ColorWhite); 76 | } 77 | canvas_draw_icon(canvas, 96, 45, &I_Voldwn_6x6); 78 | canvas_set_color(canvas, ColorBlack); 79 | 80 | // Left 81 | if(model->left_pressed) { 82 | canvas_set_bitmap_mode(canvas, 1); 83 | canvas_draw_icon(canvas, 77, 25, &I_Pressed_Button_13x13); 84 | canvas_set_bitmap_mode(canvas, 0); 85 | canvas_set_color(canvas, ColorWhite); 86 | } 87 | hid_media_draw_arrow(canvas, 82, 31, CanvasDirectionRightToLeft); 88 | hid_media_draw_arrow(canvas, 86, 31, CanvasDirectionRightToLeft); 89 | canvas_set_color(canvas, ColorBlack); 90 | 91 | // Right 92 | if(model->right_pressed) { 93 | canvas_set_bitmap_mode(canvas, 1); 94 | canvas_draw_icon(canvas, 109, 25, &I_Pressed_Button_13x13); 95 | canvas_set_bitmap_mode(canvas, 0); 96 | canvas_set_color(canvas, ColorWhite); 97 | } 98 | hid_media_draw_arrow(canvas, 112, 31, CanvasDirectionLeftToRight); 99 | hid_media_draw_arrow(canvas, 116, 31, CanvasDirectionLeftToRight); 100 | canvas_set_color(canvas, ColorBlack); 101 | 102 | // Ok 103 | if(model->ok_pressed) { 104 | canvas_draw_icon(canvas, 93, 25, &I_Pressed_Button_13x13); 105 | canvas_set_color(canvas, ColorWhite); 106 | } 107 | hid_media_draw_arrow(canvas, 96, 31, CanvasDirectionLeftToRight); 108 | canvas_draw_line(canvas, 100, 29, 100, 33); 109 | canvas_draw_line(canvas, 102, 29, 102, 33); 110 | canvas_set_color(canvas, ColorBlack); 111 | 112 | // Exit 113 | canvas_draw_icon(canvas, 0, 54, &I_Pin_back_arrow_10x8); 114 | canvas_set_font(canvas, FontSecondary); 115 | elements_multiline_text_aligned(canvas, 13, 62, AlignLeft, AlignBottom, "Hold to exit"); 116 | } 117 | 118 | static void hid_media_process_press(HidMedia* hid_media, InputEvent* event) { 119 | with_view_model( 120 | hid_media->view, 121 | HidMediaModel * model, 122 | { 123 | if(event->key == InputKeyUp) { 124 | model->up_pressed = true; 125 | hid_hal_consumer_key_press(hid_media->hid, HID_CONSUMER_VOLUME_INCREMENT); 126 | } else if(event->key == InputKeyDown) { 127 | model->down_pressed = true; 128 | hid_hal_consumer_key_press(hid_media->hid, HID_CONSUMER_VOLUME_DECREMENT); 129 | } else if(event->key == InputKeyLeft) { 130 | model->left_pressed = true; 131 | hid_hal_consumer_key_press(hid_media->hid, HID_CONSUMER_SCAN_PREVIOUS_TRACK); 132 | } else if(event->key == InputKeyRight) { 133 | model->right_pressed = true; 134 | hid_hal_consumer_key_press(hid_media->hid, HID_CONSUMER_SCAN_NEXT_TRACK); 135 | } else if(event->key == InputKeyOk) { 136 | model->ok_pressed = true; 137 | hid_hal_consumer_key_press(hid_media->hid, HID_CONSUMER_PLAY_PAUSE); 138 | } 139 | }, 140 | true); 141 | } 142 | 143 | static void hid_media_process_release(HidMedia* hid_media, InputEvent* event) { 144 | with_view_model( 145 | hid_media->view, 146 | HidMediaModel * model, 147 | { 148 | if(event->key == InputKeyUp) { 149 | model->up_pressed = false; 150 | hid_hal_consumer_key_release(hid_media->hid, HID_CONSUMER_VOLUME_INCREMENT); 151 | } else if(event->key == InputKeyDown) { 152 | model->down_pressed = false; 153 | hid_hal_consumer_key_release(hid_media->hid, HID_CONSUMER_VOLUME_DECREMENT); 154 | } else if(event->key == InputKeyLeft) { 155 | model->left_pressed = false; 156 | hid_hal_consumer_key_release(hid_media->hid, HID_CONSUMER_SCAN_PREVIOUS_TRACK); 157 | } else if(event->key == InputKeyRight) { 158 | model->right_pressed = false; 159 | hid_hal_consumer_key_release(hid_media->hid, HID_CONSUMER_SCAN_NEXT_TRACK); 160 | } else if(event->key == InputKeyOk) { 161 | model->ok_pressed = false; 162 | hid_hal_consumer_key_release(hid_media->hid, HID_CONSUMER_PLAY_PAUSE); 163 | } 164 | }, 165 | true); 166 | } 167 | 168 | static bool hid_media_input_callback(InputEvent* event, void* context) { 169 | furi_assert(context); 170 | HidMedia* hid_media = context; 171 | bool consumed = false; 172 | 173 | if(event->type == InputTypePress) { 174 | hid_media_process_press(hid_media, event); 175 | consumed = true; 176 | } else if(event->type == InputTypeRelease) { 177 | hid_media_process_release(hid_media, event); 178 | consumed = true; 179 | } else if(event->type == InputTypeShort) { 180 | if(event->key == InputKeyBack) { 181 | hid_hal_consumer_key_release_all(hid_media->hid); 182 | } 183 | } 184 | 185 | return consumed; 186 | } 187 | 188 | HidMedia* hid_media_alloc(Hid* hid) { 189 | HidMedia* hid_media = malloc(sizeof(HidMedia)); 190 | hid_media->view = view_alloc(); 191 | hid_media->hid = hid; 192 | view_set_context(hid_media->view, hid_media); 193 | view_allocate_model(hid_media->view, ViewModelTypeLocking, sizeof(HidMediaModel)); 194 | view_set_draw_callback(hid_media->view, hid_media_draw_callback); 195 | view_set_input_callback(hid_media->view, hid_media_input_callback); 196 | 197 | with_view_model( 198 | hid_media->view, HidMediaModel * model, { model->transport = hid->transport; }, true); 199 | 200 | return hid_media; 201 | } 202 | 203 | void hid_media_free(HidMedia* hid_media) { 204 | furi_assert(hid_media); 205 | view_free(hid_media->view); 206 | free(hid_media); 207 | } 208 | 209 | View* hid_media_get_view(HidMedia* hid_media) { 210 | furi_assert(hid_media); 211 | return hid_media->view; 212 | } 213 | 214 | void hid_media_set_connected_status(HidMedia* hid_media, bool connected) { 215 | furi_assert(hid_media); 216 | with_view_model( 217 | hid_media->view, HidMediaModel * model, { model->connected = connected; }, true); 218 | } 219 | -------------------------------------------------------------------------------- /hid_app/views/hid_mouse.c: -------------------------------------------------------------------------------- 1 | #include "hid_mouse.h" 2 | #include 3 | #include "../hid.h" 4 | 5 | #include "hid_icons.h" 6 | 7 | #define TAG "HidMouse" 8 | 9 | struct HidMouse { 10 | View* view; 11 | Hid* hid; 12 | }; 13 | 14 | typedef struct { 15 | bool left_pressed; 16 | bool up_pressed; 17 | bool right_pressed; 18 | bool down_pressed; 19 | bool left_mouse_pressed; 20 | bool left_mouse_held; 21 | bool right_mouse_pressed; 22 | bool connected; 23 | HidTransport transport; 24 | } HidMouseModel; 25 | 26 | static void hid_mouse_draw_callback(Canvas* canvas, void* context) { 27 | furi_assert(context); 28 | HidMouseModel* model = context; 29 | 30 | // Header 31 | if(model->transport == HidTransportBle) { 32 | if(model->connected) { 33 | canvas_draw_icon(canvas, 0, 0, &I_Ble_connected_15x15); 34 | } else { 35 | canvas_draw_icon(canvas, 0, 0, &I_Ble_disconnected_15x15); 36 | } 37 | } 38 | 39 | canvas_set_font(canvas, FontPrimary); 40 | elements_multiline_text_aligned(canvas, 17, 3, AlignLeft, AlignTop, "Mouse"); 41 | canvas_set_font(canvas, FontSecondary); 42 | 43 | if(model->left_mouse_held == true) { 44 | elements_multiline_text_aligned(canvas, 0, 62, AlignLeft, AlignBottom, "Selecting..."); 45 | } else { 46 | canvas_draw_icon(canvas, 0, 54, &I_Pin_back_arrow_10x8); 47 | canvas_set_font(canvas, FontSecondary); 48 | elements_multiline_text_aligned(canvas, 13, 62, AlignLeft, AlignBottom, "Hold to exit"); 49 | } 50 | 51 | // Keypad circles 52 | canvas_draw_icon(canvas, 64, 8, &I_Circles_47x47); 53 | 54 | // Up 55 | if(model->up_pressed) { 56 | canvas_set_bitmap_mode(canvas, 1); 57 | canvas_draw_icon(canvas, 81, 9, &I_Pressed_Button_13x13); 58 | canvas_set_bitmap_mode(canvas, 0); 59 | canvas_set_color(canvas, ColorWhite); 60 | } 61 | canvas_draw_icon(canvas, 84, 10, &I_Pin_arrow_up_7x9); 62 | canvas_set_color(canvas, ColorBlack); 63 | 64 | // Down 65 | if(model->down_pressed) { 66 | canvas_set_bitmap_mode(canvas, 1); 67 | canvas_draw_icon(canvas, 81, 41, &I_Pressed_Button_13x13); 68 | canvas_set_bitmap_mode(canvas, 0); 69 | canvas_set_color(canvas, ColorWhite); 70 | } 71 | canvas_draw_icon(canvas, 84, 43, &I_Pin_arrow_down_7x9); 72 | canvas_set_color(canvas, ColorBlack); 73 | 74 | // Left 75 | if(model->left_pressed) { 76 | canvas_set_bitmap_mode(canvas, 1); 77 | canvas_draw_icon(canvas, 65, 25, &I_Pressed_Button_13x13); 78 | canvas_set_bitmap_mode(canvas, 0); 79 | canvas_set_color(canvas, ColorWhite); 80 | } 81 | canvas_draw_icon(canvas, 67, 28, &I_Pin_arrow_left_9x7); 82 | canvas_set_color(canvas, ColorBlack); 83 | 84 | // Right 85 | if(model->right_pressed) { 86 | canvas_set_bitmap_mode(canvas, 1); 87 | canvas_draw_icon(canvas, 97, 25, &I_Pressed_Button_13x13); 88 | canvas_set_bitmap_mode(canvas, 0); 89 | canvas_set_color(canvas, ColorWhite); 90 | } 91 | canvas_draw_icon(canvas, 99, 28, &I_Pin_arrow_right_9x7); 92 | canvas_set_color(canvas, ColorBlack); 93 | 94 | // Ok 95 | if(model->left_mouse_pressed) { 96 | canvas_draw_icon(canvas, 81, 25, &I_Ok_btn_pressed_13x13); 97 | } else { 98 | canvas_draw_icon(canvas, 83, 27, &I_Left_mouse_icon_9x9); 99 | } 100 | 101 | // Back 102 | if(model->right_mouse_pressed) { 103 | canvas_draw_icon(canvas, 108, 48, &I_Ok_btn_pressed_13x13); 104 | } else { 105 | canvas_draw_icon(canvas, 110, 50, &I_Right_mouse_icon_9x9); 106 | } 107 | } 108 | 109 | static void hid_mouse_process(HidMouse* hid_mouse, InputEvent* event) { 110 | with_view_model( 111 | hid_mouse->view, 112 | HidMouseModel * model, 113 | { 114 | if(event->key == InputKeyBack) { 115 | if(event->type == InputTypeShort) { 116 | hid_hal_mouse_press(hid_mouse->hid, HID_MOUSE_BTN_RIGHT); 117 | hid_hal_mouse_release(hid_mouse->hid, HID_MOUSE_BTN_RIGHT); 118 | } else if(event->type == InputTypePress) { 119 | model->right_mouse_pressed = true; 120 | } else if(event->type == InputTypeRelease) { 121 | model->right_mouse_pressed = false; 122 | } 123 | } else if(event->key == InputKeyOk) { 124 | if(event->type == InputTypeShort) { 125 | // Just release if it was being held before 126 | if(!model->left_mouse_held) 127 | hid_hal_mouse_press(hid_mouse->hid, HID_MOUSE_BTN_LEFT); 128 | hid_hal_mouse_release(hid_mouse->hid, HID_MOUSE_BTN_LEFT); 129 | model->left_mouse_held = false; 130 | } else if(event->type == InputTypeLong) { 131 | hid_hal_mouse_press(hid_mouse->hid, HID_MOUSE_BTN_LEFT); 132 | model->left_mouse_held = true; 133 | model->left_mouse_pressed = true; 134 | } else if(event->type == InputTypePress) { 135 | model->left_mouse_pressed = true; 136 | } else if(event->type == InputTypeRelease) { 137 | // Only release if it wasn't a long press 138 | if(!model->left_mouse_held) model->left_mouse_pressed = false; 139 | } 140 | } else if(event->key == InputKeyRight) { 141 | if(event->type == InputTypePress) { 142 | model->right_pressed = true; 143 | hid_hal_mouse_move(hid_mouse->hid, MOUSE_MOVE_SHORT, 0); 144 | } else if(event->type == InputTypeRepeat) { 145 | hid_hal_mouse_move(hid_mouse->hid, MOUSE_MOVE_LONG, 0); 146 | } else if(event->type == InputTypeRelease) { 147 | model->right_pressed = false; 148 | } 149 | } else if(event->key == InputKeyLeft) { 150 | if(event->type == InputTypePress) { 151 | model->left_pressed = true; 152 | hid_hal_mouse_move(hid_mouse->hid, -MOUSE_MOVE_SHORT, 0); 153 | } else if(event->type == InputTypeRepeat) { 154 | hid_hal_mouse_move(hid_mouse->hid, -MOUSE_MOVE_LONG, 0); 155 | } else if(event->type == InputTypeRelease) { 156 | model->left_pressed = false; 157 | } 158 | } else if(event->key == InputKeyDown) { 159 | if(event->type == InputTypePress) { 160 | model->down_pressed = true; 161 | hid_hal_mouse_move(hid_mouse->hid, 0, MOUSE_MOVE_SHORT); 162 | } else if(event->type == InputTypeRepeat) { 163 | hid_hal_mouse_move(hid_mouse->hid, 0, MOUSE_MOVE_LONG); 164 | } else if(event->type == InputTypeRelease) { 165 | model->down_pressed = false; 166 | } 167 | } else if(event->key == InputKeyUp) { 168 | if(event->type == InputTypePress) { 169 | model->up_pressed = true; 170 | hid_hal_mouse_move(hid_mouse->hid, 0, -MOUSE_MOVE_SHORT); 171 | } else if(event->type == InputTypeRepeat) { 172 | hid_hal_mouse_move(hid_mouse->hid, 0, -MOUSE_MOVE_LONG); 173 | } else if(event->type == InputTypeRelease) { 174 | model->up_pressed = false; 175 | } 176 | } 177 | }, 178 | true); 179 | } 180 | 181 | static bool hid_mouse_input_callback(InputEvent* event, void* context) { 182 | furi_assert(context); 183 | HidMouse* hid_mouse = context; 184 | bool consumed = false; 185 | 186 | if(event->type == InputTypeLong && event->key == InputKeyBack) { 187 | hid_hal_mouse_release_all(hid_mouse->hid); 188 | } else { 189 | hid_mouse_process(hid_mouse, event); 190 | consumed = true; 191 | } 192 | 193 | return consumed; 194 | } 195 | 196 | HidMouse* hid_mouse_alloc(Hid* hid) { 197 | HidMouse* hid_mouse = malloc(sizeof(HidMouse)); 198 | hid_mouse->view = view_alloc(); 199 | hid_mouse->hid = hid; 200 | view_set_context(hid_mouse->view, hid_mouse); 201 | view_allocate_model(hid_mouse->view, ViewModelTypeLocking, sizeof(HidMouseModel)); 202 | view_set_draw_callback(hid_mouse->view, hid_mouse_draw_callback); 203 | view_set_input_callback(hid_mouse->view, hid_mouse_input_callback); 204 | 205 | with_view_model( 206 | hid_mouse->view, HidMouseModel * model, { model->transport = hid->transport; }, true); 207 | 208 | return hid_mouse; 209 | } 210 | 211 | void hid_mouse_free(HidMouse* hid_mouse) { 212 | furi_assert(hid_mouse); 213 | view_free(hid_mouse->view); 214 | free(hid_mouse); 215 | } 216 | 217 | View* hid_mouse_get_view(HidMouse* hid_mouse) { 218 | furi_assert(hid_mouse); 219 | return hid_mouse->view; 220 | } 221 | 222 | void hid_mouse_set_connected_status(HidMouse* hid_mouse, bool connected) { 223 | furi_assert(hid_mouse); 224 | with_view_model( 225 | hid_mouse->view, HidMouseModel * model, { model->connected = connected; }, true); 226 | } 227 | -------------------------------------------------------------------------------- /hid_app/views/hid_tiktok.c: -------------------------------------------------------------------------------- 1 | #include "hid_tiktok.h" 2 | #include "../hid.h" 3 | #include 4 | 5 | #include "hid_icons.h" 6 | 7 | #define TAG "HidTikTok" 8 | 9 | struct HidTikTok { 10 | View* view; 11 | Hid* hid; 12 | }; 13 | 14 | typedef struct { 15 | bool left_pressed; 16 | bool up_pressed; 17 | bool right_pressed; 18 | bool down_pressed; 19 | bool ok_pressed; 20 | bool connected; 21 | bool is_cursor_set; 22 | HidTransport transport; 23 | } HidTikTokModel; 24 | 25 | static void hid_tiktok_draw_callback(Canvas* canvas, void* context) { 26 | furi_assert(context); 27 | HidTikTokModel* model = context; 28 | 29 | // Header 30 | if(model->transport == HidTransportBle) { 31 | if(model->connected) { 32 | canvas_draw_icon(canvas, 0, 0, &I_Ble_connected_15x15); 33 | } else { 34 | canvas_draw_icon(canvas, 0, 0, &I_Ble_disconnected_15x15); 35 | } 36 | } 37 | 38 | canvas_set_font(canvas, FontPrimary); 39 | elements_multiline_text_aligned(canvas, 17, 3, AlignLeft, AlignTop, "TikTok"); 40 | canvas_set_font(canvas, FontSecondary); 41 | 42 | // Keypad circles 43 | canvas_draw_icon(canvas, 76, 8, &I_Circles_47x47); 44 | 45 | // Up 46 | if(model->up_pressed) { 47 | canvas_set_bitmap_mode(canvas, 1); 48 | canvas_draw_icon(canvas, 93, 9, &I_Pressed_Button_13x13); 49 | canvas_set_bitmap_mode(canvas, 0); 50 | canvas_set_color(canvas, ColorWhite); 51 | } 52 | canvas_draw_icon(canvas, 96, 11, &I_Arr_up_7x9); 53 | canvas_set_color(canvas, ColorBlack); 54 | 55 | // Down 56 | if(model->down_pressed) { 57 | canvas_set_bitmap_mode(canvas, 1); 58 | canvas_draw_icon(canvas, 93, 41, &I_Pressed_Button_13x13); 59 | canvas_set_bitmap_mode(canvas, 0); 60 | canvas_set_color(canvas, ColorWhite); 61 | } 62 | canvas_draw_icon(canvas, 96, 44, &I_Arr_dwn_7x9); 63 | canvas_set_color(canvas, ColorBlack); 64 | 65 | // Left 66 | if(model->left_pressed) { 67 | canvas_set_bitmap_mode(canvas, 1); 68 | canvas_draw_icon(canvas, 77, 25, &I_Pressed_Button_13x13); 69 | canvas_set_bitmap_mode(canvas, 0); 70 | canvas_set_color(canvas, ColorWhite); 71 | } 72 | canvas_draw_icon(canvas, 81, 29, &I_Voldwn_6x6); 73 | canvas_set_color(canvas, ColorBlack); 74 | 75 | // Right 76 | if(model->right_pressed) { 77 | canvas_set_bitmap_mode(canvas, 1); 78 | canvas_draw_icon(canvas, 109, 25, &I_Pressed_Button_13x13); 79 | canvas_set_bitmap_mode(canvas, 0); 80 | canvas_set_color(canvas, ColorWhite); 81 | } 82 | canvas_draw_icon(canvas, 111, 29, &I_Volup_8x6); 83 | canvas_set_color(canvas, ColorBlack); 84 | 85 | // Ok 86 | if(model->ok_pressed) { 87 | canvas_draw_icon(canvas, 91, 23, &I_Like_pressed_17x17); 88 | } else { 89 | canvas_draw_icon(canvas, 94, 27, &I_Like_def_11x9); 90 | } 91 | // Exit 92 | canvas_draw_icon(canvas, 0, 54, &I_Pin_back_arrow_10x8); 93 | canvas_set_font(canvas, FontSecondary); 94 | elements_multiline_text_aligned(canvas, 13, 62, AlignLeft, AlignBottom, "Hold to exit"); 95 | } 96 | 97 | static void hid_tiktok_reset_cursor(HidTikTok* hid_tiktok) { 98 | // Set cursor to the phone's left up corner 99 | // Delays to guarantee one packet per connection interval 100 | for(size_t i = 0; i < 8; i++) { 101 | hid_hal_mouse_move(hid_tiktok->hid, -127, -127); 102 | furi_delay_ms(50); 103 | } 104 | // Move cursor from the corner 105 | hid_hal_mouse_move(hid_tiktok->hid, 20, 120); 106 | furi_delay_ms(50); 107 | } 108 | 109 | static void 110 | hid_tiktok_process_press(HidTikTok* hid_tiktok, HidTikTokModel* model, InputEvent* event) { 111 | if(event->key == InputKeyUp) { 112 | model->up_pressed = true; 113 | } else if(event->key == InputKeyDown) { 114 | model->down_pressed = true; 115 | } else if(event->key == InputKeyLeft) { 116 | model->left_pressed = true; 117 | hid_hal_consumer_key_press(hid_tiktok->hid, HID_CONSUMER_VOLUME_DECREMENT); 118 | } else if(event->key == InputKeyRight) { 119 | model->right_pressed = true; 120 | hid_hal_consumer_key_press(hid_tiktok->hid, HID_CONSUMER_VOLUME_INCREMENT); 121 | } else if(event->key == InputKeyOk) { 122 | model->ok_pressed = true; 123 | } 124 | } 125 | 126 | static void 127 | hid_tiktok_process_release(HidTikTok* hid_tiktok, HidTikTokModel* model, InputEvent* event) { 128 | if(event->key == InputKeyUp) { 129 | model->up_pressed = false; 130 | } else if(event->key == InputKeyDown) { 131 | model->down_pressed = false; 132 | } else if(event->key == InputKeyLeft) { 133 | model->left_pressed = false; 134 | hid_hal_consumer_key_release(hid_tiktok->hid, HID_CONSUMER_VOLUME_DECREMENT); 135 | } else if(event->key == InputKeyRight) { 136 | model->right_pressed = false; 137 | hid_hal_consumer_key_release(hid_tiktok->hid, HID_CONSUMER_VOLUME_INCREMENT); 138 | } else if(event->key == InputKeyOk) { 139 | model->ok_pressed = false; 140 | } 141 | } 142 | 143 | static bool hid_tiktok_input_callback(InputEvent* event, void* context) { 144 | furi_assert(context); 145 | HidTikTok* hid_tiktok = context; 146 | bool consumed = false; 147 | 148 | with_view_model( 149 | hid_tiktok->view, 150 | HidTikTokModel * model, 151 | { 152 | if(event->type == InputTypePress) { 153 | hid_tiktok_process_press(hid_tiktok, model, event); 154 | if(model->connected && !model->is_cursor_set) { 155 | hid_tiktok_reset_cursor(hid_tiktok); 156 | model->is_cursor_set = true; 157 | } 158 | consumed = true; 159 | } else if(event->type == InputTypeRelease) { 160 | hid_tiktok_process_release(hid_tiktok, model, event); 161 | consumed = true; 162 | } else if(event->type == InputTypeShort) { 163 | if(event->key == InputKeyOk) { 164 | hid_hal_mouse_press(hid_tiktok->hid, HID_MOUSE_BTN_LEFT); 165 | furi_delay_ms(50); 166 | hid_hal_mouse_release(hid_tiktok->hid, HID_MOUSE_BTN_LEFT); 167 | furi_delay_ms(50); 168 | hid_hal_mouse_press(hid_tiktok->hid, HID_MOUSE_BTN_LEFT); 169 | furi_delay_ms(50); 170 | hid_hal_mouse_release(hid_tiktok->hid, HID_MOUSE_BTN_LEFT); 171 | consumed = true; 172 | } else if(event->key == InputKeyUp) { 173 | // Emulate up swipe 174 | hid_hal_mouse_scroll(hid_tiktok->hid, -6); 175 | hid_hal_mouse_scroll(hid_tiktok->hid, -12); 176 | hid_hal_mouse_scroll(hid_tiktok->hid, -19); 177 | hid_hal_mouse_scroll(hid_tiktok->hid, -12); 178 | hid_hal_mouse_scroll(hid_tiktok->hid, -6); 179 | consumed = true; 180 | } else if(event->key == InputKeyDown) { 181 | // Emulate down swipe 182 | hid_hal_mouse_scroll(hid_tiktok->hid, 6); 183 | hid_hal_mouse_scroll(hid_tiktok->hid, 12); 184 | hid_hal_mouse_scroll(hid_tiktok->hid, 19); 185 | hid_hal_mouse_scroll(hid_tiktok->hid, 12); 186 | hid_hal_mouse_scroll(hid_tiktok->hid, 6); 187 | consumed = true; 188 | } else if(event->key == InputKeyBack) { 189 | hid_hal_consumer_key_release_all(hid_tiktok->hid); 190 | consumed = true; 191 | } 192 | } else if(event->type == InputTypeLong) { 193 | if(event->key == InputKeyBack) { 194 | hid_hal_consumer_key_release_all(hid_tiktok->hid); 195 | model->is_cursor_set = false; 196 | consumed = false; 197 | } 198 | } 199 | }, 200 | true); 201 | 202 | return consumed; 203 | } 204 | 205 | HidTikTok* hid_tiktok_alloc(Hid* bt_hid) { 206 | HidTikTok* hid_tiktok = malloc(sizeof(HidTikTok)); 207 | hid_tiktok->hid = bt_hid; 208 | hid_tiktok->view = view_alloc(); 209 | view_set_context(hid_tiktok->view, hid_tiktok); 210 | view_allocate_model(hid_tiktok->view, ViewModelTypeLocking, sizeof(HidTikTokModel)); 211 | view_set_draw_callback(hid_tiktok->view, hid_tiktok_draw_callback); 212 | view_set_input_callback(hid_tiktok->view, hid_tiktok_input_callback); 213 | 214 | with_view_model( 215 | hid_tiktok->view, HidTikTokModel * model, { model->transport = bt_hid->transport; }, true); 216 | 217 | return hid_tiktok; 218 | } 219 | 220 | void hid_tiktok_free(HidTikTok* hid_tiktok) { 221 | furi_assert(hid_tiktok); 222 | view_free(hid_tiktok->view); 223 | free(hid_tiktok); 224 | } 225 | 226 | View* hid_tiktok_get_view(HidTikTok* hid_tiktok) { 227 | furi_assert(hid_tiktok); 228 | return hid_tiktok->view; 229 | } 230 | 231 | void hid_tiktok_set_connected_status(HidTikTok* hid_tiktok, bool connected) { 232 | furi_assert(hid_tiktok); 233 | with_view_model( 234 | hid_tiktok->view, 235 | HidTikTokModel * model, 236 | { 237 | model->connected = connected; 238 | model->is_cursor_set = false; 239 | }, 240 | true); 241 | } 242 | -------------------------------------------------------------------------------- /hid_app/views/hid_slack.c: -------------------------------------------------------------------------------- 1 | #include "hid_slack.h" 2 | #include "../hid.h" 3 | #include 4 | 5 | #include "hid_icons.h" 6 | 7 | #define TAG "HidSlack" 8 | 9 | struct HidSlack { 10 | View* view; 11 | Hid* hid; 12 | }; 13 | 14 | typedef struct { 15 | bool left_pressed; 16 | bool up_pressed; 17 | bool right_pressed; 18 | bool down_pressed; 19 | bool ok_pressed; 20 | bool connected; 21 | bool is_cursor_set; 22 | HidTransport transport; 23 | } HidSlackModel; 24 | 25 | static void hid_slack_draw_callback(Canvas* canvas, void* context) { 26 | furi_assert(context); 27 | HidSlackModel* model = context; 28 | 29 | // Header 30 | if(model->transport == HidTransportBle) { 31 | if(model->connected) { 32 | canvas_draw_icon(canvas, 0, 0, &I_Ble_connected_15x15); 33 | } else { 34 | canvas_draw_icon(canvas, 0, 0, &I_Ble_disconnected_15x15); 35 | } 36 | } 37 | 38 | canvas_set_font(canvas, FontPrimary); 39 | elements_multiline_text_aligned(canvas, 17, 3, AlignLeft, AlignTop, "SlackMute"); 40 | canvas_set_font(canvas, FontSecondary); 41 | 42 | // Keypad circles 43 | canvas_draw_icon(canvas, 76, 8, &I_Circles_47x47); 44 | 45 | // Up 46 | // if(model->up_pressed) { 47 | // canvas_set_bitmap_mode(canvas, 1); 48 | // canvas_draw_icon(canvas, 93, 9, &I_Pressed_Button_13x13); 49 | // canvas_set_bitmap_mode(canvas, 0); 50 | // canvas_set_color(canvas, ColorWhite); 51 | // } 52 | // canvas_draw_icon(canvas, 96, 11, &I_Arr_up_7x9); 53 | // canvas_set_color(canvas, ColorBlack); 54 | 55 | // Down 56 | // if(model->down_pressed) { 57 | // canvas_set_bitmap_mode(canvas, 1); 58 | // canvas_draw_icon(canvas, 93, 41, &I_Pressed_Button_13x13); 59 | // canvas_set_bitmap_mode(canvas, 0); 60 | // canvas_set_color(canvas, ColorWhite); 61 | // } 62 | // canvas_draw_icon(canvas, 96, 44, &I_Arr_dwn_7x9); 63 | // canvas_set_color(canvas, ColorBlack); 64 | 65 | // Left 66 | if(model->left_pressed) { 67 | canvas_set_bitmap_mode(canvas, 1); 68 | canvas_draw_icon(canvas, 77, 25, &I_Pressed_Button_13x13); 69 | canvas_set_bitmap_mode(canvas, 0); 70 | canvas_set_color(canvas, ColorWhite); 71 | } 72 | canvas_draw_icon(canvas, 81, 29, &I_Voldwn_6x6); 73 | canvas_set_color(canvas, ColorBlack); 74 | 75 | // Right 76 | if(model->right_pressed) { 77 | canvas_set_bitmap_mode(canvas, 1); 78 | canvas_draw_icon(canvas, 109, 25, &I_Pressed_Button_13x13); 79 | canvas_set_bitmap_mode(canvas, 0); 80 | canvas_set_color(canvas, ColorWhite); 81 | } 82 | canvas_draw_icon(canvas, 111, 29, &I_Volup_8x6); 83 | canvas_set_color(canvas, ColorBlack); 84 | 85 | // Ok 86 | if(model->ok_pressed) { 87 | canvas_draw_icon(canvas, 91, 23, &I_Like_pressed_17x17); 88 | } else { 89 | canvas_draw_icon(canvas, 94, 27, &I_Like_def_11x9); 90 | } 91 | // Exit 92 | canvas_draw_icon(canvas, 0, 54, &I_Pin_back_arrow_10x8); 93 | canvas_set_font(canvas, FontSecondary); 94 | elements_multiline_text_aligned(canvas, 13, 62, AlignLeft, AlignBottom, "Hold to exit"); 95 | } 96 | 97 | static void hid_slack_process_press(HidSlack* hid_slack, HidSlackModel* model, InputEvent* event) { 98 | if(event->key == InputKeyUp) { 99 | model->up_pressed = true; 100 | 101 | //hid_hal_consumer_key_press(hid_slack->hid, HID_CONSUMER_VOLUME_MUTE); 102 | //hid_hal_consumer_key_press(hid_slack->hid, HID_CONSUMER_VOLUME_DECREMENT); 103 | } else if(event->key == InputKeyDown) { 104 | model->down_pressed = true; 105 | } else if(event->key == InputKeyLeft) { 106 | model->left_pressed = true; 107 | hid_hal_consumer_key_press(hid_slack->hid, HID_CONSUMER_VOLUME_DECREMENT); 108 | } else if(event->key == InputKeyRight) { 109 | model->right_pressed = true; 110 | hid_hal_consumer_key_press(hid_slack->hid, HID_CONSUMER_VOLUME_INCREMENT); 111 | } else if(event->key == InputKeyOk) { 112 | //hid_hal_consumer_key_press(hid_slack->hid, HID_KEYBOARD_L_SHIFT | HID_KEYBOARD_J); 113 | //hid_hal_consumer_key_press(hid_slack->hid, HID_KEYBOARD_L_SHIFT); 114 | //hid_hal_keyboard_press(hid_slack->hid, HID_KEYBOARD_J); 115 | //hid_hal_keyboard_press(hid_slack->hid, HID_KEYBOARD_L_SHIFT | HID_KEYBOARD_J); 116 | hid_hal_keyboard_press( 117 | hid_slack->hid, 118 | KEY_MOD_LEFT_SHIFT | KEY_MOD_LEFT_GUI | HID_KEYBOARD_SPACEBAR); // Cmd+Shift+Space 119 | model->ok_pressed = true; 120 | } 121 | } 122 | 123 | static void 124 | hid_slack_process_release(HidSlack* hid_slack, HidSlackModel* model, InputEvent* event) { 125 | if(event->key == InputKeyUp) { 126 | model->up_pressed = false; 127 | //hid_hal_consumer_key_release(hid_slack->hid, HID_CONSUMER_VOLUME_DECREMENT); 128 | } else if(event->key == InputKeyDown) { 129 | model->down_pressed = false; 130 | } else if(event->key == InputKeyLeft) { 131 | model->left_pressed = false; 132 | hid_hal_consumer_key_release(hid_slack->hid, HID_CONSUMER_VOLUME_DECREMENT); 133 | } else if(event->key == InputKeyRight) { 134 | model->right_pressed = false; 135 | hid_hal_consumer_key_release(hid_slack->hid, HID_CONSUMER_VOLUME_INCREMENT); 136 | } else if(event->key == InputKeyOk) { 137 | //hid_hal_consumer_key_release(hid_slack->hid, HID_KEYBOARD_L_SHIFT | HID_KEYBOARD_J); 138 | //hid_hal_keyboard_release(hid_slack->hid, HID_KEYBOARD_L_SHIFT | HID_KEYBOARD_J); 139 | // hid_hal_keyboard_release(hid_slack->hid, HID_KEYBOARD_J); 140 | // hid_hal_consumer_key_release(hid_slack->hid, HID_KEYBOARD_L_SHIFT); 141 | //hid_hal_keyboard_press(hid_slack->hid, HID_KEYBOARD_L_SHIFT | HID_KEYBOARD_J); 142 | hid_hal_keyboard_release( 143 | hid_slack->hid, 144 | KEY_MOD_LEFT_SHIFT | KEY_MOD_LEFT_GUI | HID_KEYBOARD_SPACEBAR); // Cmd+Shift+Space 145 | model->ok_pressed = false; 146 | } 147 | } 148 | 149 | static bool hid_slack_input_callback(InputEvent* event, void* context) { 150 | furi_assert(context); 151 | HidSlack* hid_slack = context; 152 | bool consumed = false; 153 | 154 | with_view_model( 155 | hid_slack->view, 156 | HidSlackModel * model, 157 | { 158 | if(event->type == InputTypePress) { 159 | hid_slack_process_press(hid_slack, model, event); 160 | if(model->connected && !model->is_cursor_set) { 161 | //hid_slack_reset_cursor(hid_slack); 162 | model->is_cursor_set = true; 163 | } 164 | consumed = true; 165 | } else if(event->type == InputTypeRelease) { 166 | hid_slack_process_release(hid_slack, model, event); 167 | consumed = true; 168 | } else if(event->type == InputTypeShort) { 169 | if(event->key == InputKeyOk) { 170 | // 171 | consumed = true; 172 | } else if(event->key == InputKeyUp) { 173 | // 174 | // Unmute 175 | //hid_hal_consumer_key_press(hid_slack->hid, HID_CONSUMER_VOLUME_MUTE); 176 | consumed = true; 177 | } else if(event->key == InputKeyDown) { 178 | // 179 | // Mute 180 | //hid_hal_consumer_key_release(hid_slack->hid, HID_CONSUMER_VOLUME_MUTE); 181 | consumed = true; 182 | } else if(event->key == InputKeyBack) { 183 | hid_hal_consumer_key_release_all(hid_slack->hid); 184 | consumed = true; 185 | } 186 | } else if(event->type == InputTypeLong) { 187 | if(event->key == InputKeyBack) { 188 | hid_hal_consumer_key_release_all(hid_slack->hid); 189 | model->is_cursor_set = false; 190 | consumed = false; 191 | } 192 | } 193 | }, 194 | true); 195 | 196 | return consumed; 197 | } 198 | 199 | HidSlack* hid_slack_alloc(Hid* bt_hid) { 200 | HidSlack* hid_slack = malloc(sizeof(HidSlack)); 201 | hid_slack->hid = bt_hid; 202 | hid_slack->view = view_alloc(); 203 | view_set_context(hid_slack->view, hid_slack); 204 | view_allocate_model(hid_slack->view, ViewModelTypeLocking, sizeof(HidSlackModel)); 205 | view_set_draw_callback(hid_slack->view, hid_slack_draw_callback); 206 | view_set_input_callback(hid_slack->view, hid_slack_input_callback); 207 | 208 | with_view_model( 209 | hid_slack->view, HidSlackModel * model, { model->transport = bt_hid->transport; }, true); 210 | 211 | return hid_slack; 212 | } 213 | 214 | void hid_slack_free(HidSlack* hid_slack) { 215 | furi_assert(hid_slack); 216 | view_free(hid_slack->view); 217 | free(hid_slack); 218 | } 219 | 220 | View* hid_slack_get_view(HidSlack* hid_slack) { 221 | furi_assert(hid_slack); 222 | return hid_slack->view; 223 | } 224 | 225 | void hid_slack_set_connected_status(HidSlack* hid_slack, bool connected) { 226 | furi_assert(hid_slack); 227 | with_view_model( 228 | hid_slack->view, 229 | HidSlackModel * model, 230 | { 231 | model->connected = connected; 232 | model->is_cursor_set = false; 233 | }, 234 | true); 235 | } 236 | -------------------------------------------------------------------------------- /flipper_share/flipper_share_app.c: -------------------------------------------------------------------------------- 1 | #include "flipper_share_app.h" 2 | #include 3 | #include 4 | 5 | // Callback when a file is selected in the file browser 6 | static void file_browser_select_callback(void* context) { 7 | if(!context) return; 8 | FlipperShareApp* app = context; 9 | 10 | // Get the selected file path from result_path 11 | const char* file_path = furi_string_get_cstr(app->result_path); 12 | if(file_path && file_path[0]) { 13 | strncpy(app->selected_file_path, file_path, sizeof(app->selected_file_path) - 1); 14 | app->selected_file_path[sizeof(app->selected_file_path) - 1] = '\0'; 15 | app->file_info_loaded = false; 16 | app->selected_file_size = 0; 17 | 18 | // Send file selection event 19 | if(app->view_dispatcher) { 20 | view_dispatcher_send_custom_event(app->view_dispatcher, 1); 21 | } 22 | } 23 | } 24 | 25 | // Callback for internal use, not used in the app 26 | _Bool file_browser_callback(FuriString* path, void* context, unsigned char** icon, FuriString* name) { 27 | UNUSED(icon); 28 | UNUSED(name); 29 | UNUSED(path); 30 | UNUSED(context); 31 | return false; 32 | } 33 | 34 | // Set the callback after creating the file_browser (in flipper_share_alloc): 35 | // file_browser_set_result_callback(app->file_browser, file_browser_callback, app); 36 | 37 | void show_file_info_scene(FlipperShareApp* app) { 38 | furi_assert(app); 39 | dialog_ex_set_header(app->dialog_show_file, "File Info", 64, 0, AlignCenter, AlignTop); 40 | dialog_ex_set_text(app->dialog_show_file, app->selected_file_path, 64, 32, AlignCenter, AlignCenter); 41 | view_dispatcher_switch_to_view(app->view_dispatcher, FlipperShareViewIdShowFile); 42 | } 43 | 44 | bool flipper_share_custom_event_callback(void* context, uint32_t event) { 45 | furi_assert(context); 46 | FlipperShareApp* app = context; 47 | return scene_manager_handle_custom_event(app->scene_manager, event); 48 | } 49 | 50 | static bool flipper_share_back_event_callback(void* context) { 51 | furi_assert(context); 52 | FlipperShareApp* app = context; 53 | return scene_manager_handle_back_event(app->scene_manager); 54 | } 55 | 56 | static void submenu_callback(void* context, uint32_t index) { 57 | furi_assert(context); 58 | FlipperShareApp* app = context; 59 | 60 | if(index == 0) { // Send - open file browser 61 | scene_manager_next_scene(app->scene_manager, FlipperShareSceneFileBrowser); 62 | } else if(index == 1) { // Receive 63 | scene_manager_next_scene(app->scene_manager, FlipperShareSceneReceive); 64 | } else if(index == 2) { // About 65 | dialog_ex_set_header(app->dialog_about, "About", 64, 0, AlignCenter, AlignTop); 66 | dialog_ex_set_text( 67 | app->dialog_about, 68 | "\nFlipper Share (flipper_share)\n" 69 | "A file sharing app via Sub-GHz\n" 70 | "Developed by @lomalkin\n" 71 | "github.com/lomalkin", 72 | 0, 73 | 0, 74 | AlignLeft, 75 | AlignTop); 76 | view_dispatcher_switch_to_view(app->view_dispatcher, FlipperShareViewIdAbout); 77 | } 78 | } 79 | 80 | // Return to main menu when pressing Back on About 81 | static uint32_t flipper_share_about_previous(void* context) { 82 | UNUSED(context); 83 | return FlipperShareViewIdMenu; 84 | } 85 | 86 | // TODO: check / cleanup 87 | 88 | // Function to read file information (size, etc.) - commented out for debugging 89 | /* 90 | static bool read_file_info(FlipperShareApp* app) { 91 | if(!app || !app->selected_file_path[0]) { 92 | return false; 93 | } 94 | 95 | Storage* storage = furi_record_open(RECORD_STORAGE); 96 | if(!storage) { 97 | return false; 98 | } 99 | 100 | FileInfo file_info; 101 | bool success = false; 102 | 103 | if(storage_common_stat(storage, app->selected_file_path, &file_info) == FSE_OK) { 104 | app->selected_file_size = file_info.size; 105 | app->file_info_loaded = true; 106 | success = true; 107 | } else { 108 | app->file_info_loaded = false; 109 | } 110 | 111 | furi_record_close(RECORD_STORAGE); 112 | return success; 113 | } 114 | */ 115 | 116 | // Example function for reading file content (commented out for now) 117 | // static bool read_file_content(FlipperShareApp* app, char* buffer, size_t buffer_size) { 118 | // Storage* storage = furi_record_open(RECORD_STORAGE); 119 | // File* file = storage_file_alloc(storage); 120 | // bool success = false; 121 | // 122 | // if(storage_file_open(file, app->selected_file_path, FSAM_READ, FSOM_OPEN_EXISTING)) { 123 | // size_t bytes_read = storage_file_read(file, buffer, buffer_size - 1); 124 | // size_t bytes_read = storage_file_read(file, buffer, buffer_size - 1); 125 | // buffer[bytes_read] = '\0'; // Null terminate 126 | // success = true; 127 | // storage_file_close(file); 128 | // } 129 | // 130 | // storage_file_free(file); 131 | // furi_record_close(RECORD_STORAGE); 132 | // return success; 133 | // } 134 | 135 | // Temporarily disabled callback to debug crash 136 | /* 137 | static void file_browser_callback(void* context) { 138 | if(!context) { 139 | return; 140 | } 141 | 142 | FlipperShareApp* app = context; 143 | 144 | // Set the selected file path 145 | strcpy(app->selected_file_path, "/ext/test_file.txt"); 146 | 147 | // Don't read file info for now - just set default values 148 | app->selected_file_size = 0; 149 | app->file_info_loaded = false; 150 | 151 | // Send custom event to handle file selection in the scene 152 | // This is safer than calling scene manager directly from callback 153 | if(app->view_dispatcher) { 154 | view_dispatcher_send_custom_event(app->view_dispatcher, 1); 155 | } 156 | } 157 | */ 158 | 159 | static FlipperShareApp* flipper_share_alloc() { 160 | FlipperShareApp* app = malloc(sizeof(FlipperShareApp)); 161 | app->gui = furi_record_open(RECORD_GUI); 162 | 163 | app->view_dispatcher = view_dispatcher_alloc(); 164 | 165 | app->scene_manager = scene_manager_alloc(&flipper_share_scene_handlers, app); 166 | view_dispatcher_set_event_callback_context(app->view_dispatcher, app); 167 | view_dispatcher_set_custom_event_callback( 168 | app->view_dispatcher, flipper_share_custom_event_callback); 169 | view_dispatcher_set_navigation_event_callback( 170 | app->view_dispatcher, flipper_share_back_event_callback); 171 | 172 | // Create submenu for main menu 173 | app->submenu = submenu_alloc(); 174 | submenu_add_item(app->submenu, "Send", 0, submenu_callback, app); 175 | submenu_add_item(app->submenu, "Receive", 1, submenu_callback, app); 176 | submenu_add_item(app->submenu, "About", 2, submenu_callback, app); 177 | view_dispatcher_add_view( 178 | app->view_dispatcher, FlipperShareViewIdMenu, submenu_get_view(app->submenu)); 179 | 180 | // Create file browser with result_path for selected file retrieval 181 | FuriString* result_path = furi_string_alloc(); 182 | // Allocate file browser once 183 | app->file_browser = file_browser_alloc(result_path); 184 | 185 | // Configure file browser 186 | file_browser_configure( 187 | app->file_browser, 188 | "*", // all extensions 189 | "/ext", // initial path - use /ext where files are located 190 | false, // do not skip assets 191 | false, // do not hide dot files 192 | NULL, // default file icon 193 | false // do not hide extensions 194 | ); 195 | 196 | // Set callback for file selection 197 | file_browser_set_callback(app->file_browser, file_browser_select_callback, app); 198 | 199 | // Store result_path for later use 200 | app->result_path = result_path; 201 | view_dispatcher_add_view( 202 | app->view_dispatcher, 203 | FlipperShareViewIdFileBrowser, 204 | file_browser_get_view(app->file_browser)); 205 | 206 | // Create dialog to show file path/info 207 | app->dialog_show_file = dialog_ex_alloc(); 208 | view_dispatcher_add_view( 209 | app->view_dispatcher, 210 | FlipperShareViewIdShowFile, 211 | dialog_ex_get_view(app->dialog_show_file)); 212 | 213 | // Create dialog for Receive 214 | app->dialog_receive = dialog_ex_alloc(); 215 | view_dispatcher_add_view( 216 | app->view_dispatcher, FlipperShareViewIdReceive, dialog_ex_get_view(app->dialog_receive)); 217 | 218 | app->selected_file_path[0] = '\0'; // Explicitly initialize with empty string 219 | app->selected_file_size = 0; 220 | app->file_info_loaded = false; 221 | 222 | // Initialize fields for file reading scene 223 | app->file_reading_state = NULL; 224 | app->timer = NULL; 225 | 226 | // Create dialog for About 227 | app->dialog_about = dialog_ex_alloc(); 228 | view_dispatcher_add_view( 229 | app->view_dispatcher, FlipperShareViewIdAbout, dialog_ex_get_view(app->dialog_about)); 230 | // Ensure Back from About returns to Menu 231 | view_set_previous_callback(dialog_ex_get_view(app->dialog_about), flipper_share_about_previous); 232 | 233 | return app; 234 | } 235 | 236 | static void flipper_share_free(FlipperShareApp* app) { 237 | furi_assert(app); 238 | 239 | view_dispatcher_remove_view(app->view_dispatcher, FlipperShareViewIdReceive); 240 | view_dispatcher_remove_view(app->view_dispatcher, FlipperShareViewIdShowFile); 241 | view_dispatcher_remove_view(app->view_dispatcher, FlipperShareViewIdFileBrowser); 242 | view_dispatcher_remove_view(app->view_dispatcher, FlipperShareViewIdMenu); 243 | // Also remove the About view to avoid leaving a dangling view pointer 244 | view_dispatcher_remove_view(app->view_dispatcher, FlipperShareViewIdAbout); 245 | 246 | dialog_ex_free(app->dialog_show_file); 247 | dialog_ex_free(app->dialog_receive); 248 | dialog_ex_free(app->dialog_about); 249 | 250 | file_browser_free(app->file_browser); 251 | if(app->result_path) { 252 | furi_string_free(app->result_path); 253 | } 254 | submenu_free(app->submenu); 255 | 256 | scene_manager_free(app->scene_manager); 257 | view_dispatcher_free(app->view_dispatcher); 258 | 259 | furi_record_close(RECORD_GUI); 260 | app->gui = NULL; 261 | 262 | free(app); 263 | } 264 | 265 | int32_t flipper_share_app(void* p) { 266 | UNUSED(p); 267 | FlipperShareApp* app = flipper_share_alloc(); 268 | 269 | view_dispatcher_attach_to_gui(app->view_dispatcher, app->gui, ViewDispatcherTypeFullscreen); 270 | 271 | scene_manager_next_scene(app->scene_manager, FlipperShareSceneMenu); 272 | 273 | view_dispatcher_run(app->view_dispatcher); 274 | 275 | flipper_share_free(app); 276 | 277 | return 0; 278 | } 279 | -------------------------------------------------------------------------------- /hid_app/views/hid_keynote.c: -------------------------------------------------------------------------------- 1 | #include "hid_keynote.h" 2 | #include 3 | #include "../hid.h" 4 | 5 | #include "hid_icons.h" 6 | 7 | #define TAG "HidKeynote" 8 | 9 | struct HidKeynote { 10 | View* view; 11 | Hid* hid; 12 | }; 13 | 14 | typedef struct { 15 | bool left_pressed; 16 | bool up_pressed; 17 | bool right_pressed; 18 | bool down_pressed; 19 | bool ok_pressed; 20 | bool back_pressed; 21 | bool connected; 22 | HidTransport transport; 23 | } HidKeynoteModel; 24 | 25 | static void hid_keynote_draw_arrow(Canvas* canvas, uint8_t x, uint8_t y, CanvasDirection dir) { 26 | canvas_draw_triangle(canvas, x, y, 5, 3, dir); 27 | if(dir == CanvasDirectionBottomToTop) { 28 | canvas_draw_line(canvas, x, y + 6, x, y - 1); 29 | } else if(dir == CanvasDirectionTopToBottom) { 30 | canvas_draw_line(canvas, x, y - 6, x, y + 1); 31 | } else if(dir == CanvasDirectionRightToLeft) { 32 | canvas_draw_line(canvas, x + 6, y, x - 1, y); 33 | } else if(dir == CanvasDirectionLeftToRight) { 34 | canvas_draw_line(canvas, x - 6, y, x + 1, y); 35 | } 36 | } 37 | 38 | static void hid_keynote_draw_callback(Canvas* canvas, void* context) { 39 | furi_assert(context); 40 | HidKeynoteModel* model = context; 41 | 42 | // Header 43 | if(model->transport == HidTransportBle) { 44 | if(model->connected) { 45 | canvas_draw_icon(canvas, 0, 0, &I_Ble_connected_15x15); 46 | } else { 47 | canvas_draw_icon(canvas, 0, 0, &I_Ble_disconnected_15x15); 48 | } 49 | } 50 | 51 | canvas_set_font(canvas, FontPrimary); 52 | elements_multiline_text_aligned(canvas, 17, 3, AlignLeft, AlignTop, "Keynote"); 53 | 54 | canvas_draw_icon(canvas, 68, 2, &I_Pin_back_arrow_10x8); 55 | canvas_set_font(canvas, FontSecondary); 56 | elements_multiline_text_aligned(canvas, 127, 3, AlignRight, AlignTop, "Hold to exit"); 57 | 58 | // Up 59 | canvas_draw_icon(canvas, 21, 24, &I_Button_18x18); 60 | if(model->up_pressed) { 61 | elements_slightly_rounded_box(canvas, 24, 26, 13, 13); 62 | canvas_set_color(canvas, ColorWhite); 63 | } 64 | hid_keynote_draw_arrow(canvas, 30, 30, CanvasDirectionBottomToTop); 65 | canvas_set_color(canvas, ColorBlack); 66 | 67 | // Down 68 | canvas_draw_icon(canvas, 21, 45, &I_Button_18x18); 69 | if(model->down_pressed) { 70 | elements_slightly_rounded_box(canvas, 24, 47, 13, 13); 71 | canvas_set_color(canvas, ColorWhite); 72 | } 73 | hid_keynote_draw_arrow(canvas, 30, 55, CanvasDirectionTopToBottom); 74 | canvas_set_color(canvas, ColorBlack); 75 | 76 | // Left 77 | canvas_draw_icon(canvas, 0, 45, &I_Button_18x18); 78 | if(model->left_pressed) { 79 | elements_slightly_rounded_box(canvas, 3, 47, 13, 13); 80 | canvas_set_color(canvas, ColorWhite); 81 | } 82 | hid_keynote_draw_arrow(canvas, 7, 53, CanvasDirectionRightToLeft); 83 | canvas_set_color(canvas, ColorBlack); 84 | 85 | // Right 86 | canvas_draw_icon(canvas, 42, 45, &I_Button_18x18); 87 | if(model->right_pressed) { 88 | elements_slightly_rounded_box(canvas, 45, 47, 13, 13); 89 | canvas_set_color(canvas, ColorWhite); 90 | } 91 | hid_keynote_draw_arrow(canvas, 53, 53, CanvasDirectionLeftToRight); 92 | canvas_set_color(canvas, ColorBlack); 93 | 94 | // Ok 95 | canvas_draw_icon(canvas, 63, 25, &I_Space_65x18); 96 | if(model->ok_pressed) { 97 | elements_slightly_rounded_box(canvas, 66, 27, 60, 13); 98 | canvas_set_color(canvas, ColorWhite); 99 | } 100 | canvas_draw_icon(canvas, 74, 29, &I_Ok_btn_9x9); 101 | elements_multiline_text_aligned(canvas, 91, 36, AlignLeft, AlignBottom, "Space"); 102 | canvas_set_color(canvas, ColorBlack); 103 | 104 | // Back 105 | canvas_draw_icon(canvas, 63, 45, &I_Space_65x18); 106 | if(model->back_pressed) { 107 | elements_slightly_rounded_box(canvas, 66, 47, 60, 13); 108 | canvas_set_color(canvas, ColorWhite); 109 | } 110 | canvas_draw_icon(canvas, 74, 49, &I_Pin_back_arrow_10x8); 111 | elements_multiline_text_aligned(canvas, 91, 57, AlignLeft, AlignBottom, "Back"); 112 | } 113 | 114 | static void hid_keynote_draw_vertical_callback(Canvas* canvas, void* context) { 115 | furi_assert(context); 116 | HidKeynoteModel* model = context; 117 | 118 | // Header 119 | if(model->transport == HidTransportBle) { 120 | if(model->connected) { 121 | canvas_draw_icon(canvas, 0, 0, &I_Ble_connected_15x15); 122 | } else { 123 | canvas_draw_icon(canvas, 0, 0, &I_Ble_disconnected_15x15); 124 | } 125 | canvas_set_font(canvas, FontPrimary); 126 | elements_multiline_text_aligned(canvas, 20, 3, AlignLeft, AlignTop, "Keynote"); 127 | } else { 128 | canvas_set_font(canvas, FontPrimary); 129 | elements_multiline_text_aligned(canvas, 12, 3, AlignLeft, AlignTop, "Keynote"); 130 | } 131 | 132 | canvas_draw_icon(canvas, 2, 18, &I_Pin_back_arrow_10x8); 133 | canvas_set_font(canvas, FontSecondary); 134 | elements_multiline_text_aligned(canvas, 15, 19, AlignLeft, AlignTop, "Hold to exit"); 135 | 136 | const uint8_t x_2 = 23; 137 | const uint8_t x_1 = 2; 138 | const uint8_t x_3 = 44; 139 | 140 | const uint8_t y_1 = 44; 141 | const uint8_t y_2 = 65; 142 | 143 | // Up 144 | canvas_draw_icon(canvas, x_2, y_1, &I_Button_18x18); 145 | if(model->up_pressed) { 146 | elements_slightly_rounded_box(canvas, x_2 + 3, y_1 + 2, 13, 13); 147 | canvas_set_color(canvas, ColorWhite); 148 | } 149 | hid_keynote_draw_arrow(canvas, x_2 + 9, y_1 + 6, CanvasDirectionBottomToTop); 150 | canvas_set_color(canvas, ColorBlack); 151 | 152 | // Down 153 | canvas_draw_icon(canvas, x_2, y_2, &I_Button_18x18); 154 | if(model->down_pressed) { 155 | elements_slightly_rounded_box(canvas, x_2 + 3, y_2 + 2, 13, 13); 156 | canvas_set_color(canvas, ColorWhite); 157 | } 158 | hid_keynote_draw_arrow(canvas, x_2 + 9, y_2 + 10, CanvasDirectionTopToBottom); 159 | canvas_set_color(canvas, ColorBlack); 160 | 161 | // Left 162 | canvas_draw_icon(canvas, x_1, y_2, &I_Button_18x18); 163 | if(model->left_pressed) { 164 | elements_slightly_rounded_box(canvas, x_1 + 3, y_2 + 2, 13, 13); 165 | canvas_set_color(canvas, ColorWhite); 166 | } 167 | hid_keynote_draw_arrow(canvas, x_1 + 7, y_2 + 8, CanvasDirectionRightToLeft); 168 | canvas_set_color(canvas, ColorBlack); 169 | 170 | // Right 171 | canvas_draw_icon(canvas, x_3, y_2, &I_Button_18x18); 172 | if(model->right_pressed) { 173 | elements_slightly_rounded_box(canvas, x_3 + 3, y_2 + 2, 13, 13); 174 | canvas_set_color(canvas, ColorWhite); 175 | } 176 | hid_keynote_draw_arrow(canvas, x_3 + 11, y_2 + 8, CanvasDirectionLeftToRight); 177 | canvas_set_color(canvas, ColorBlack); 178 | 179 | // Ok 180 | canvas_draw_icon(canvas, 2, 86, &I_Space_60x18); 181 | if(model->ok_pressed) { 182 | elements_slightly_rounded_box(canvas, 5, 88, 55, 13); 183 | canvas_set_color(canvas, ColorWhite); 184 | } 185 | canvas_draw_icon(canvas, 11, 90, &I_Ok_btn_9x9); 186 | elements_multiline_text_aligned(canvas, 26, 98, AlignLeft, AlignBottom, "Space"); 187 | canvas_set_color(canvas, ColorBlack); 188 | 189 | // Back 190 | canvas_draw_icon(canvas, 2, 107, &I_Space_60x18); 191 | if(model->back_pressed) { 192 | elements_slightly_rounded_box(canvas, 5, 109, 55, 13); 193 | canvas_set_color(canvas, ColorWhite); 194 | } 195 | canvas_draw_icon(canvas, 11, 111, &I_Pin_back_arrow_10x8); 196 | elements_multiline_text_aligned(canvas, 26, 119, AlignLeft, AlignBottom, "Back"); 197 | } 198 | 199 | static void hid_keynote_process(HidKeynote* hid_keynote, InputEvent* event) { 200 | with_view_model( 201 | hid_keynote->view, 202 | HidKeynoteModel * model, 203 | { 204 | if(event->type == InputTypePress) { 205 | if(event->key == InputKeyUp) { 206 | model->up_pressed = true; 207 | hid_hal_keyboard_press(hid_keynote->hid, HID_KEYBOARD_UP_ARROW); 208 | } else if(event->key == InputKeyDown) { 209 | model->down_pressed = true; 210 | hid_hal_keyboard_press(hid_keynote->hid, HID_KEYBOARD_DOWN_ARROW); 211 | } else if(event->key == InputKeyLeft) { 212 | model->left_pressed = true; 213 | hid_hal_keyboard_press(hid_keynote->hid, HID_KEYBOARD_LEFT_ARROW); 214 | } else if(event->key == InputKeyRight) { 215 | model->right_pressed = true; 216 | hid_hal_keyboard_press(hid_keynote->hid, HID_KEYBOARD_RIGHT_ARROW); 217 | } else if(event->key == InputKeyOk) { 218 | model->ok_pressed = true; 219 | hid_hal_keyboard_press(hid_keynote->hid, HID_KEYBOARD_SPACEBAR); 220 | } else if(event->key == InputKeyBack) { 221 | model->back_pressed = true; 222 | } 223 | } else if(event->type == InputTypeRelease) { 224 | if(event->key == InputKeyUp) { 225 | model->up_pressed = false; 226 | hid_hal_keyboard_release(hid_keynote->hid, HID_KEYBOARD_UP_ARROW); 227 | } else if(event->key == InputKeyDown) { 228 | model->down_pressed = false; 229 | hid_hal_keyboard_release(hid_keynote->hid, HID_KEYBOARD_DOWN_ARROW); 230 | } else if(event->key == InputKeyLeft) { 231 | model->left_pressed = false; 232 | hid_hal_keyboard_release(hid_keynote->hid, HID_KEYBOARD_LEFT_ARROW); 233 | } else if(event->key == InputKeyRight) { 234 | model->right_pressed = false; 235 | hid_hal_keyboard_release(hid_keynote->hid, HID_KEYBOARD_RIGHT_ARROW); 236 | } else if(event->key == InputKeyOk) { 237 | model->ok_pressed = false; 238 | hid_hal_keyboard_release(hid_keynote->hid, HID_KEYBOARD_SPACEBAR); 239 | } else if(event->key == InputKeyBack) { 240 | model->back_pressed = false; 241 | } 242 | } else if(event->type == InputTypeShort) { 243 | if(event->key == InputKeyBack) { 244 | hid_hal_keyboard_press(hid_keynote->hid, HID_KEYBOARD_DELETE); 245 | hid_hal_keyboard_release(hid_keynote->hid, HID_KEYBOARD_DELETE); 246 | hid_hal_consumer_key_press(hid_keynote->hid, HID_CONSUMER_AC_BACK); 247 | hid_hal_consumer_key_release(hid_keynote->hid, HID_CONSUMER_AC_BACK); 248 | } 249 | } 250 | }, 251 | true); 252 | } 253 | 254 | static bool hid_keynote_input_callback(InputEvent* event, void* context) { 255 | furi_assert(context); 256 | HidKeynote* hid_keynote = context; 257 | bool consumed = false; 258 | 259 | if(event->type == InputTypeLong && event->key == InputKeyBack) { 260 | hid_hal_keyboard_release_all(hid_keynote->hid); 261 | } else { 262 | hid_keynote_process(hid_keynote, event); 263 | consumed = true; 264 | } 265 | 266 | return consumed; 267 | } 268 | 269 | HidKeynote* hid_keynote_alloc(Hid* hid) { 270 | HidKeynote* hid_keynote = malloc(sizeof(HidKeynote)); 271 | hid_keynote->view = view_alloc(); 272 | hid_keynote->hid = hid; 273 | view_set_context(hid_keynote->view, hid_keynote); 274 | view_allocate_model(hid_keynote->view, ViewModelTypeLocking, sizeof(HidKeynoteModel)); 275 | view_set_draw_callback(hid_keynote->view, hid_keynote_draw_callback); 276 | view_set_input_callback(hid_keynote->view, hid_keynote_input_callback); 277 | 278 | with_view_model( 279 | hid_keynote->view, HidKeynoteModel * model, { model->transport = hid->transport; }, true); 280 | 281 | return hid_keynote; 282 | } 283 | 284 | void hid_keynote_free(HidKeynote* hid_keynote) { 285 | furi_assert(hid_keynote); 286 | view_free(hid_keynote->view); 287 | free(hid_keynote); 288 | } 289 | 290 | View* hid_keynote_get_view(HidKeynote* hid_keynote) { 291 | furi_assert(hid_keynote); 292 | return hid_keynote->view; 293 | } 294 | 295 | void hid_keynote_set_connected_status(HidKeynote* hid_keynote, bool connected) { 296 | furi_assert(hid_keynote); 297 | with_view_model( 298 | hid_keynote->view, HidKeynoteModel * model, { model->connected = connected; }, true); 299 | } 300 | 301 | void hid_keynote_set_orientation(HidKeynote* hid_keynote, bool vertical) { 302 | furi_assert(hid_keynote); 303 | 304 | if(vertical) { 305 | view_set_draw_callback(hid_keynote->view, hid_keynote_draw_vertical_callback); 306 | view_set_orientation(hid_keynote->view, ViewOrientationVerticalFlip); 307 | 308 | } else { 309 | view_set_draw_callback(hid_keynote->view, hid_keynote_draw_callback); 310 | view_set_orientation(hid_keynote->view, ViewOrientationHorizontal); 311 | } 312 | } 313 | -------------------------------------------------------------------------------- /flipper_share/scenes/flipper_share_scene_receive.c: -------------------------------------------------------------------------------- 1 | #include "../flipper_share_app.h" 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | #include "subghz_share.h" 8 | #include "flipper_share.h" 9 | #include "flipper_share_scene.h" 10 | 11 | #include 12 | #include 13 | #include 14 | 15 | #define FS_IDLE_OPERATION 50 //ms 16 | 17 | #define TAG "FlipperShareSend" 18 | 19 | typedef struct { 20 | uint32_t counter; 21 | bool reading_complete; 22 | FuriThread* worker_thread; 23 | } FileReadingState; 24 | 25 | static void update_timer_callback(void* context); 26 | static void dialog_ex_callback(DialogExResult result, void* context); 27 | 28 | static FileReadingState* file_state_alloc() { 29 | FileReadingState* state = malloc(sizeof(FileReadingState)); 30 | state->counter = 0; 31 | state->reading_complete = false; 32 | state->worker_thread = NULL; 33 | return state; 34 | } 35 | 36 | static void file_reading_state_free(FileReadingState* state) { 37 | if(state->worker_thread) { 38 | furi_thread_free(state->worker_thread); 39 | } 40 | free(state); 41 | } 42 | 43 | static int32_t file_read_worker_thread(void* context) { 44 | FlipperShareApp* app = context; 45 | FileReadingState* state = (FileReadingState*)app->file_reading_state; 46 | 47 | bool is_running = true; 48 | 49 | FURI_LOG_I( 50 | TAG, 51 | "file_read_worker_thread: file: %s, size: %zu bytes", 52 | app->selected_file_path, 53 | app->selected_file_size); 54 | 55 | fs_init_from_external_receive(); 56 | 57 | while(is_running) { 58 | fs_idle(); 59 | furi_delay_ms(FS_IDLE_OPERATION); 60 | 61 | // "r_file_path=%s", g.r_file_path); 62 | state->counter = (g.r_blocks_received * 100) / g.r_blocks_needed; 63 | 64 | if(g.r_is_finished) { 65 | state->reading_complete = true; 66 | } 67 | 68 | // Check if we should stop 69 | if(furi_thread_flags_get() & 0x1) { 70 | is_running = false; 71 | } 72 | } 73 | 74 | state->reading_complete = true; 75 | 76 | return 0; 77 | } 78 | 79 | // Graphical progress view (shown while locked and not finished) 80 | static View* progress_view = NULL; 81 | static bool progress_view_active = false; 82 | 83 | static void progress_view_draw_callback(Canvas* canvas, void* context) { 84 | // model holds percent (0-100) 85 | uint8_t* model = (uint8_t*)context; 86 | uint8_t percent = model ? *model : 0; 87 | 88 | FURI_LOG_I(TAG, "Progress view draw: percent=%u", (unsigned int)percent); 89 | 90 | canvas_clear(canvas); 91 | 92 | // Header 93 | canvas_set_font(canvas, FontPrimary); 94 | canvas_set_color(canvas, ColorBlack); 95 | elements_multiline_text_aligned(canvas, 64, 4, AlignCenter, AlignTop, "Receiving..."); 96 | 97 | // Filename (basename) g.r_file_name and g.r_file_size 98 | canvas_set_font(canvas, FontSecondary); 99 | char name_line[64]; 100 | snprintf(name_line, sizeof(name_line), "%.*s, %lu KB", 48, g.r_file_name, (unsigned long)(g.r_file_size / 1024)); 101 | elements_multiline_text_aligned(canvas, 64, 20, AlignCenter, AlignTop, name_line); 102 | 103 | // Show progress percent as text above bar 104 | snprintf(name_line, sizeof(name_line), "Progress: %u%%", (unsigned int)percent); 105 | elements_multiline_text_aligned(canvas, 64, 36, AlignCenter, AlignTop, name_line); 106 | 107 | // Progress bar frame and fill 108 | const int x = 13; 109 | const int y = 50; 110 | const int w = 101; // frame width 111 | const int h = 12; 112 | 113 | canvas_set_color(canvas, ColorBlack); 114 | canvas_draw_frame(canvas, x, y, w, h); 115 | 116 | uint8_t parts_bits[FS_PARTS_BYTES]; 117 | fs_parts_bitmap_copy(parts_bits); 118 | 119 | for (uint32_t i = 0; i < FS_PARTS_COUNT; ++i) { 120 | if ((parts_bits[i >> 3] >> (i & 7u)) & 1u) { // bit value by number 121 | canvas_draw_line(canvas, x + i + 1, y, x + i + 1, y + h - 1); 122 | } 123 | } 124 | 125 | // Percent text below 126 | char pct[16]; 127 | snprintf(pct, sizeof(pct), "%u%%", (unsigned int)percent); 128 | elements_multiline_text_aligned(canvas, 64, y + h + 8, AlignCenter, AlignTop, pct); 129 | } 130 | 131 | static bool progress_view_input_callback(InputEvent* event, void* context) { 132 | if(!context) return false; 133 | FlipperShareApp* app = context; 134 | 135 | FURI_LOG_I(TAG, "Progress view input: key=%d, type=%d", event->key, event->type); 136 | 137 | if(event->type == InputTypeShort || event->type == InputTypeLong) { 138 | if(event->key == InputKeyBack || event->key == InputKeyLeft) { 139 | FURI_LOG_I(TAG, "Back/Left button pressed in progress view, handling locally"); 140 | 141 | FileReadingState* state = (FileReadingState*)app->file_reading_state; 142 | if(state && state->worker_thread) { 143 | FURI_LOG_I(TAG, "Stopping worker thread from input handler"); 144 | furi_thread_flags_set(furi_thread_get_id(state->worker_thread), 0x1); 145 | furi_thread_join(state->worker_thread); 146 | } 147 | 148 | if(app->timer) { 149 | FURI_LOG_I(TAG, "Stopping timer from input handler"); 150 | furi_timer_stop(app->timer); 151 | furi_timer_free(app->timer); 152 | app->timer = NULL; 153 | } 154 | 155 | progress_view_active = false; 156 | 157 | FURI_LOG_I(TAG, "Switching to dialog view"); 158 | view_dispatcher_switch_to_view(app->view_dispatcher, FlipperShareViewIdShowFile); 159 | 160 | FURI_LOG_I(TAG, "Sending DialogExResultLeft event"); 161 | view_dispatcher_send_custom_event(app->view_dispatcher, DialogExResultLeft); 162 | 163 | return true; 164 | } 165 | } 166 | return false; 167 | } 168 | 169 | static void progress_view_init(FlipperShareApp* app) { 170 | if(progress_view) return; 171 | progress_view = view_alloc(); 172 | view_set_context(progress_view, app); 173 | view_allocate_model(progress_view, ViewModelTypeLocking, sizeof(uint8_t)); 174 | view_set_draw_callback(progress_view, progress_view_draw_callback); 175 | view_set_input_callback(progress_view, progress_view_input_callback); 176 | view_dispatcher_add_view(app->view_dispatcher, FlipperShareViewIdProgress, progress_view); 177 | } 178 | 179 | static void progress_view_deinit(FlipperShareApp* app) { 180 | if(!progress_view) return; 181 | view_dispatcher_remove_view(app->view_dispatcher, FlipperShareViewIdProgress); 182 | view_free(progress_view); 183 | progress_view = NULL; 184 | progress_view_active = false; 185 | } 186 | 187 | void flipper_share_scene_receive_on_enter(void* context) { 188 | FlipperShareApp* app = context; 189 | 190 | // Create state for the scene 191 | FileReadingState* state = file_state_alloc(); 192 | app->file_reading_state = state; 193 | 194 | // Setup dialog to show progress (use same UI as send scene so buttons appear) 195 | dialog_ex_set_header(app->dialog_show_file, "Receiving...", 64, 10, AlignCenter, AlignCenter); 196 | dialog_ex_set_text(app->dialog_show_file, "Waiting for announce...", 64, 32, AlignCenter, AlignCenter); 197 | dialog_ex_set_left_button_text(app->dialog_show_file, "Back"); 198 | dialog_ex_set_right_button_text(app->dialog_show_file, NULL); 199 | 200 | dialog_ex_set_context(app->dialog_show_file, app); 201 | dialog_ex_set_result_callback(app->dialog_show_file, dialog_ex_callback); 202 | 203 | // Start thread for reading file 204 | state->worker_thread = 205 | furi_thread_alloc_ex("FileReadWorker", 2048, file_read_worker_thread, app); 206 | furi_thread_start(state->worker_thread); 207 | 208 | view_dispatcher_switch_to_view(app->view_dispatcher, FlipperShareViewIdShowFile); 209 | 210 | // Start timer for updating display 211 | app->timer = furi_timer_alloc(update_timer_callback, FuriTimerTypePeriodic, app); 212 | furi_timer_start(app->timer, 250); 213 | 214 | ss_subghz_init(); // TODO Move to thread? 215 | } 216 | 217 | static void update_timer_callback(void* context) { 218 | furi_assert(context); 219 | FlipperShareApp* app = context; 220 | FileReadingState* state = (FileReadingState*)app->file_reading_state; 221 | char progress_text[256]; 222 | 223 | if(state) { 224 | // FURI_LOG_I( 225 | // TAG, 226 | // "Timer: counter=%u, complete=%d, locked=%d, finished=%d", 227 | // (unsigned int)state->counter, 228 | // state->reading_complete, 229 | // g.r_locked, 230 | // g.r_is_finished); 231 | 232 | if(state->reading_complete) { 233 | if (g.r_is_success) { 234 | dialog_ex_set_header(app->dialog_show_file, "Success!", 64, 10, AlignCenter, AlignCenter); 235 | } else { 236 | dialog_ex_set_header(app->dialog_show_file, "Hash failed", 64, 10, AlignCenter, AlignCenter); 237 | } 238 | snprintf(progress_text, sizeof(progress_text), "Saved to:\n%.*s", 64, g.r_file_path); 239 | // dialog_ex_set_right_button_text(app->dialog_show_file, "OK"); 240 | 241 | // If completed and still showing progress view, switch back to dialog 242 | if(progress_view_active) { 243 | FURI_LOG_I(TAG, "Transfer complete, switching to dialog"); 244 | view_dispatcher_switch_to_view(app->view_dispatcher, FlipperShareViewIdShowFile); 245 | progress_view_active = false; 246 | } 247 | } else { 248 | if(g.r_locked) { 249 | snprintf( 250 | progress_text, 251 | sizeof(progress_text), 252 | "%.*s, %lu KB\n%u%%", 253 | 64, 254 | g.r_file_name, 255 | (unsigned long)(g.r_file_size / 1024), 256 | (unsigned int)state->counter); 257 | 258 | // If locked and not finished, show graphical progress view instead of dialog 259 | if(!progress_view) { 260 | FURI_LOG_I(TAG, "Initializing progress view"); 261 | progress_view_init(app); 262 | } 263 | 264 | if(!progress_view_active) { 265 | FURI_LOG_I(TAG, "Switching to progress view"); 266 | view_dispatcher_switch_to_view(app->view_dispatcher, FlipperShareViewIdProgress); 267 | progress_view_active = true; 268 | } 269 | 270 | // Update progress view model 271 | with_view_model(progress_view, uint8_t* model, { 272 | *model = (uint8_t)state->counter; 273 | FURI_LOG_I(TAG, "Updating progress model: %u%%", (unsigned int)state->counter); 274 | }, true); 275 | } else { 276 | snprintf(progress_text, sizeof(progress_text), "Waiting for announce..."); 277 | 278 | // If we're no longer locked but the progress view is active, switch back to dialog 279 | if(progress_view_active) { 280 | FURI_LOG_I(TAG, "No longer locked, switching to dialog"); 281 | view_dispatcher_switch_to_view(app->view_dispatcher, FlipperShareViewIdShowFile); 282 | progress_view_active = false; 283 | } 284 | } 285 | } 286 | dialog_ex_set_text(app->dialog_show_file, progress_text, 64, 32, AlignCenter, AlignCenter); 287 | } 288 | } 289 | 290 | // Callback for DialogEx buttons 291 | static void dialog_ex_callback(DialogExResult result, void* context) { 292 | furi_assert(context); 293 | FlipperShareApp* app = context; 294 | 295 | if(result == DialogExResultLeft) { 296 | view_dispatcher_send_custom_event(app->view_dispatcher, DialogExResultLeft); 297 | } else if(result == DialogExResultRight) { 298 | view_dispatcher_send_custom_event(app->view_dispatcher, DialogExResultRight); 299 | } 300 | } 301 | 302 | bool flipper_share_scene_receive_on_event(void* context, SceneManagerEvent event) { 303 | FlipperShareApp* app = context; 304 | bool consumed = false; 305 | 306 | if(event.type == SceneManagerEventTypeCustom) { 307 | if(event.event == DialogExResultLeft) { 308 | // Back button pressed - stop reading and return to file info 309 | FileReadingState* state = (FileReadingState*)app->file_reading_state; 310 | if(state && state->worker_thread) { 311 | FURI_LOG_I(TAG, "Stopping worker thread"); 312 | furi_thread_flags_set(furi_thread_get_id(state->worker_thread), 0x1); 313 | furi_thread_join(state->worker_thread); 314 | } 315 | 316 | // Stop timer 317 | if(app->timer) { 318 | FURI_LOG_I(TAG, "Stopping timer"); 319 | furi_timer_stop(app->timer); 320 | furi_timer_free(app->timer); 321 | app->timer = NULL; 322 | } 323 | 324 | FURI_LOG_I(TAG, "Returning to previous scene"); 325 | scene_manager_previous_scene(app->scene_manager); 326 | consumed = true; 327 | } else if(event.event == DialogExResultRight) { 328 | FURI_LOG_I(TAG, "Receive scene: DialogExResultRight (OK) received"); 329 | // OK button pressed - return to file info 330 | // Only available when completed 331 | 332 | // Stop timer 333 | if(app->timer) { 334 | FURI_LOG_I(TAG, "Stopping timer"); 335 | furi_timer_stop(app->timer); 336 | furi_timer_free(app->timer); 337 | app->timer = NULL; 338 | } 339 | 340 | scene_manager_previous_scene(app->scene_manager); 341 | consumed = true; 342 | } 343 | } else if(event.type == SceneManagerEventTypeBack) { 344 | // Back button - same as Cancel 345 | FURI_LOG_I(TAG, "Receive scene: Back button event received"); 346 | FileReadingState* state = (FileReadingState*)app->file_reading_state; 347 | if(state && state->worker_thread) { 348 | FURI_LOG_I(TAG, "Stopping worker thread"); 349 | furi_thread_flags_set(furi_thread_get_id(state->worker_thread), 0x1); 350 | furi_thread_join(state->worker_thread); 351 | } 352 | 353 | // Stop timer 354 | if(app->timer) { 355 | furi_timer_stop(app->timer); 356 | furi_timer_free(app->timer); 357 | app->timer = NULL; 358 | } 359 | 360 | scene_manager_previous_scene(app->scene_manager); 361 | consumed = true; 362 | } 363 | 364 | return consumed; 365 | } 366 | 367 | void flipper_share_scene_receive_on_exit(void* context) { 368 | FlipperShareApp* app = context; 369 | // Ensure progress view is deinitialized if it was created 370 | progress_view_deinit(app); 371 | 372 | if(ss_subghz_deinit()) { 373 | FURI_LOG_W(TAG, "ss_subghz_deinit reported error on scene exit"); 374 | } 375 | 376 | // Clean up resources 377 | if(app->file_reading_state) { 378 | file_reading_state_free((FileReadingState*)app->file_reading_state); 379 | app->file_reading_state = NULL; 380 | } 381 | 382 | // Check if the timer is stopped 383 | if(app->timer) { 384 | furi_timer_stop(app->timer); 385 | furi_timer_free(app->timer); 386 | app->timer = NULL; 387 | } 388 | } 389 | -------------------------------------------------------------------------------- /hid_app/hid.c: -------------------------------------------------------------------------------- 1 | #include "hid.h" 2 | #include "views.h" 3 | #include 4 | #include 5 | 6 | #define TAG "HidApp" 7 | 8 | enum HidDebugSubmenuIndex { 9 | HidSubmenuIndexKeynote, 10 | HidSubmenuIndexKeynoteVertical, 11 | HidSubmenuIndexKeyboard, 12 | HidSubmenuIndexMedia, 13 | HidSubmenuIndexTikTok, 14 | HidSubmenuIndexSlack, 15 | HidSubmenuIndexMouse, 16 | HidSubmenuIndexMouseClicker, 17 | HidSubmenuIndexMouseJiggler, 18 | }; 19 | 20 | static void hid_submenu_callback(void* context, uint32_t index) { 21 | furi_assert(context); 22 | Hid* app = context; 23 | if(index == HidSubmenuIndexKeynote) { 24 | app->view_id = HidViewKeynote; 25 | hid_keynote_set_orientation(app->hid_keynote, false); 26 | view_dispatcher_switch_to_view(app->view_dispatcher, HidViewKeynote); 27 | } else if(index == HidSubmenuIndexKeynoteVertical) { 28 | app->view_id = HidViewKeynote; 29 | hid_keynote_set_orientation(app->hid_keynote, true); 30 | view_dispatcher_switch_to_view(app->view_dispatcher, HidViewKeynote); 31 | } else if(index == HidSubmenuIndexKeyboard) { 32 | app->view_id = HidViewKeyboard; 33 | view_dispatcher_switch_to_view(app->view_dispatcher, HidViewKeyboard); 34 | } else if(index == HidSubmenuIndexMedia) { 35 | app->view_id = HidViewMedia; 36 | view_dispatcher_switch_to_view(app->view_dispatcher, HidViewMedia); 37 | } else if(index == HidSubmenuIndexMouse) { 38 | app->view_id = HidViewMouse; 39 | view_dispatcher_switch_to_view(app->view_dispatcher, HidViewMouse); 40 | } else if(index == HidSubmenuIndexTikTok) { 41 | app->view_id = BtHidViewTikTok; 42 | view_dispatcher_switch_to_view(app->view_dispatcher, BtHidViewTikTok); 43 | } else if(index == HidSubmenuIndexSlack) { 44 | app->view_id = HidViewSlack; 45 | view_dispatcher_switch_to_view(app->view_dispatcher, HidViewSlack); 46 | } else if(index == HidSubmenuIndexMouseClicker) { 47 | app->view_id = HidViewMouseClicker; 48 | view_dispatcher_switch_to_view(app->view_dispatcher, HidViewMouseClicker); 49 | } else if(index == HidSubmenuIndexMouseJiggler) { 50 | app->view_id = HidViewMouseJiggler; 51 | view_dispatcher_switch_to_view(app->view_dispatcher, HidViewMouseJiggler); 52 | } 53 | } 54 | 55 | static void bt_hid_connection_status_changed_callback(BtStatus status, void* context) { 56 | furi_assert(context); 57 | Hid* hid = context; 58 | bool connected = (status == BtStatusConnected); 59 | if(hid->transport == HidTransportBle) { 60 | if(connected) { 61 | notification_internal_message(hid->notifications, &sequence_set_blue_255); 62 | } else { 63 | notification_internal_message(hid->notifications, &sequence_reset_blue); 64 | } 65 | } 66 | hid_keynote_set_connected_status(hid->hid_keynote, connected); 67 | hid_keyboard_set_connected_status(hid->hid_keyboard, connected); 68 | hid_media_set_connected_status(hid->hid_media, connected); 69 | hid_mouse_set_connected_status(hid->hid_mouse, connected); 70 | hid_mouse_clicker_set_connected_status(hid->hid_mouse_clicker, connected); 71 | hid_mouse_jiggler_set_connected_status(hid->hid_mouse_jiggler, connected); 72 | hid_tiktok_set_connected_status(hid->hid_tiktok, connected); 73 | hid_slack_set_connected_status(hid->hid_slack, connected); 74 | } 75 | 76 | static void hid_dialog_callback(DialogExResult result, void* context) { 77 | furi_assert(context); 78 | Hid* app = context; 79 | if(result == DialogExResultLeft) { 80 | view_dispatcher_stop(app->view_dispatcher); 81 | } else if(result == DialogExResultRight) { 82 | view_dispatcher_switch_to_view(app->view_dispatcher, app->view_id); // Show last view 83 | } else if(result == DialogExResultCenter) { 84 | view_dispatcher_switch_to_view(app->view_dispatcher, HidViewSubmenu); 85 | } 86 | } 87 | 88 | static uint32_t hid_exit_confirm_view(void* context) { 89 | UNUSED(context); 90 | return HidViewExitConfirm; 91 | } 92 | 93 | static uint32_t hid_exit(void* context) { 94 | UNUSED(context); 95 | return VIEW_NONE; 96 | } 97 | 98 | Hid* hid_alloc(HidTransport transport) { 99 | Hid* app = malloc(sizeof(Hid)); 100 | app->transport = transport; 101 | 102 | // Gui 103 | app->gui = furi_record_open(RECORD_GUI); 104 | 105 | // Bt 106 | app->bt = furi_record_open(RECORD_BT); 107 | 108 | // Notifications 109 | app->notifications = furi_record_open(RECORD_NOTIFICATION); 110 | 111 | // View dispatcher 112 | app->view_dispatcher = view_dispatcher_alloc(); 113 | view_dispatcher_enable_queue(app->view_dispatcher); 114 | view_dispatcher_attach_to_gui(app->view_dispatcher, app->gui, ViewDispatcherTypeFullscreen); 115 | // Device Type Submenu view 116 | app->device_type_submenu = submenu_alloc(); 117 | submenu_add_item( 118 | app->device_type_submenu, "Keynote", HidSubmenuIndexKeynote, hid_submenu_callback, app); 119 | submenu_add_item( 120 | app->device_type_submenu, 121 | "Keynote Vertical", 122 | HidSubmenuIndexKeynoteVertical, 123 | hid_submenu_callback, 124 | app); 125 | submenu_add_item( 126 | app->device_type_submenu, "Keyboard", HidSubmenuIndexKeyboard, hid_submenu_callback, app); 127 | submenu_add_item( 128 | app->device_type_submenu, "Media", HidSubmenuIndexMedia, hid_submenu_callback, app); 129 | submenu_add_item( 130 | app->device_type_submenu, "Mouse", HidSubmenuIndexMouse, hid_submenu_callback, app); 131 | if(app->transport == HidTransportBle) { 132 | submenu_add_item( 133 | app->device_type_submenu, 134 | "TikTok Controller", 135 | HidSubmenuIndexTikTok, 136 | hid_submenu_callback, 137 | app); 138 | submenu_add_item( 139 | app->device_type_submenu, 140 | "Slack Audio Controller", 141 | HidSubmenuIndexSlack, 142 | hid_submenu_callback, 143 | app); 144 | } 145 | submenu_add_item( 146 | app->device_type_submenu, 147 | "Mouse Clicker", 148 | HidSubmenuIndexMouseClicker, 149 | hid_submenu_callback, 150 | app); 151 | submenu_add_item( 152 | app->device_type_submenu, 153 | "Mouse Jiggler", 154 | HidSubmenuIndexMouseJiggler, 155 | hid_submenu_callback, 156 | app); 157 | view_set_previous_callback(submenu_get_view(app->device_type_submenu), hid_exit); 158 | view_dispatcher_add_view( 159 | app->view_dispatcher, HidViewSubmenu, submenu_get_view(app->device_type_submenu)); 160 | app->view_id = HidViewSubmenu; 161 | view_dispatcher_switch_to_view(app->view_dispatcher, app->view_id); 162 | return app; 163 | } 164 | 165 | Hid* hid_app_alloc_view(void* context) { 166 | furi_assert(context); 167 | Hid* app = context; 168 | // Dialog view 169 | app->dialog = dialog_ex_alloc(); 170 | dialog_ex_set_result_callback(app->dialog, hid_dialog_callback); 171 | dialog_ex_set_context(app->dialog, app); 172 | dialog_ex_set_left_button_text(app->dialog, "Exit"); 173 | dialog_ex_set_right_button_text(app->dialog, "Stay"); 174 | dialog_ex_set_center_button_text(app->dialog, "Menu"); 175 | dialog_ex_set_header(app->dialog, "Close Current App?", 16, 12, AlignLeft, AlignTop); 176 | view_dispatcher_add_view( 177 | app->view_dispatcher, HidViewExitConfirm, dialog_ex_get_view(app->dialog)); 178 | 179 | // Keynote view 180 | app->hid_keynote = hid_keynote_alloc(app); 181 | view_set_previous_callback(hid_keynote_get_view(app->hid_keynote), hid_exit_confirm_view); 182 | view_dispatcher_add_view( 183 | app->view_dispatcher, HidViewKeynote, hid_keynote_get_view(app->hid_keynote)); 184 | 185 | // Keyboard view 186 | app->hid_keyboard = hid_keyboard_alloc(app); 187 | view_set_previous_callback(hid_keyboard_get_view(app->hid_keyboard), hid_exit_confirm_view); 188 | view_dispatcher_add_view( 189 | app->view_dispatcher, HidViewKeyboard, hid_keyboard_get_view(app->hid_keyboard)); 190 | 191 | // Media view 192 | app->hid_media = hid_media_alloc(app); 193 | view_set_previous_callback(hid_media_get_view(app->hid_media), hid_exit_confirm_view); 194 | view_dispatcher_add_view( 195 | app->view_dispatcher, HidViewMedia, hid_media_get_view(app->hid_media)); 196 | 197 | // TikTok view 198 | app->hid_tiktok = hid_tiktok_alloc(app); 199 | view_set_previous_callback(hid_tiktok_get_view(app->hid_tiktok), hid_exit_confirm_view); 200 | view_dispatcher_add_view( 201 | app->view_dispatcher, BtHidViewTikTok, hid_tiktok_get_view(app->hid_tiktok)); 202 | 203 | // Slack view 204 | app->hid_slack = hid_slack_alloc(app); 205 | view_set_previous_callback(hid_slack_get_view(app->hid_slack), hid_exit_confirm_view); 206 | view_dispatcher_add_view( 207 | app->view_dispatcher, HidViewSlack, hid_slack_get_view(app->hid_slack)); 208 | 209 | // Mouse view 210 | app->hid_mouse = hid_mouse_alloc(app); 211 | view_set_previous_callback(hid_mouse_get_view(app->hid_mouse), hid_exit_confirm_view); 212 | view_dispatcher_add_view( 213 | app->view_dispatcher, HidViewMouse, hid_mouse_get_view(app->hid_mouse)); 214 | 215 | // Mouse clicker view 216 | app->hid_mouse_clicker = hid_mouse_clicker_alloc(app); 217 | view_set_previous_callback( 218 | hid_mouse_clicker_get_view(app->hid_mouse_clicker), hid_exit_confirm_view); 219 | view_dispatcher_add_view( 220 | app->view_dispatcher, 221 | HidViewMouseClicker, 222 | hid_mouse_clicker_get_view(app->hid_mouse_clicker)); 223 | 224 | // Mouse jiggler view 225 | app->hid_mouse_jiggler = hid_mouse_jiggler_alloc(app); 226 | view_set_previous_callback( 227 | hid_mouse_jiggler_get_view(app->hid_mouse_jiggler), hid_exit_confirm_view); 228 | view_dispatcher_add_view( 229 | app->view_dispatcher, 230 | HidViewMouseJiggler, 231 | hid_mouse_jiggler_get_view(app->hid_mouse_jiggler)); 232 | 233 | return app; 234 | } 235 | 236 | void hid_free(Hid* app) { 237 | furi_assert(app); 238 | 239 | // Reset notification 240 | if(app->transport == HidTransportBle) { 241 | notification_internal_message(app->notifications, &sequence_reset_blue); 242 | } 243 | 244 | // Free views 245 | view_dispatcher_remove_view(app->view_dispatcher, HidViewSubmenu); 246 | submenu_free(app->device_type_submenu); 247 | view_dispatcher_remove_view(app->view_dispatcher, HidViewExitConfirm); 248 | dialog_ex_free(app->dialog); 249 | view_dispatcher_remove_view(app->view_dispatcher, HidViewKeynote); 250 | hid_keynote_free(app->hid_keynote); 251 | view_dispatcher_remove_view(app->view_dispatcher, HidViewKeyboard); 252 | hid_keyboard_free(app->hid_keyboard); 253 | view_dispatcher_remove_view(app->view_dispatcher, HidViewMedia); 254 | hid_media_free(app->hid_media); 255 | view_dispatcher_remove_view(app->view_dispatcher, HidViewMouse); 256 | hid_mouse_free(app->hid_mouse); 257 | view_dispatcher_remove_view(app->view_dispatcher, HidViewMouseClicker); 258 | hid_mouse_clicker_free(app->hid_mouse_clicker); 259 | view_dispatcher_remove_view(app->view_dispatcher, HidViewMouseJiggler); 260 | hid_mouse_jiggler_free(app->hid_mouse_jiggler); 261 | view_dispatcher_remove_view(app->view_dispatcher, BtHidViewTikTok); 262 | hid_tiktok_free(app->hid_tiktok); 263 | view_dispatcher_remove_view(app->view_dispatcher, HidViewSlack); 264 | hid_slack_free(app->hid_slack); 265 | view_dispatcher_free(app->view_dispatcher); 266 | 267 | // Close records 268 | furi_record_close(RECORD_GUI); 269 | app->gui = NULL; 270 | furi_record_close(RECORD_NOTIFICATION); 271 | app->notifications = NULL; 272 | furi_record_close(RECORD_BT); 273 | app->bt = NULL; 274 | 275 | // Free rest 276 | free(app); 277 | } 278 | 279 | void hid_hal_keyboard_press(Hid* instance, uint16_t event) { 280 | furi_assert(instance); 281 | if(instance->transport == HidTransportBle) { 282 | furi_hal_bt_hid_kb_press(event); 283 | } else if(instance->transport == HidTransportUsb) { 284 | furi_hal_hid_kb_press(event); 285 | } else { 286 | furi_crash(NULL); 287 | } 288 | } 289 | 290 | void hid_hal_keyboard_release(Hid* instance, uint16_t event) { 291 | furi_assert(instance); 292 | if(instance->transport == HidTransportBle) { 293 | furi_hal_bt_hid_kb_release(event); 294 | } else if(instance->transport == HidTransportUsb) { 295 | furi_hal_hid_kb_release(event); 296 | } else { 297 | furi_crash(NULL); 298 | } 299 | } 300 | 301 | void hid_hal_keyboard_release_all(Hid* instance) { 302 | furi_assert(instance); 303 | if(instance->transport == HidTransportBle) { 304 | furi_hal_bt_hid_kb_release_all(); 305 | } else if(instance->transport == HidTransportUsb) { 306 | furi_hal_hid_kb_release_all(); 307 | } else { 308 | furi_crash(NULL); 309 | } 310 | } 311 | 312 | void hid_hal_consumer_key_press(Hid* instance, uint16_t event) { 313 | furi_assert(instance); 314 | if(instance->transport == HidTransportBle) { 315 | furi_hal_bt_hid_consumer_key_press(event); 316 | } else if(instance->transport == HidTransportUsb) { 317 | furi_hal_hid_consumer_key_press(event); 318 | } else { 319 | furi_crash(NULL); 320 | } 321 | } 322 | 323 | void hid_hal_consumer_key_release(Hid* instance, uint16_t event) { 324 | furi_assert(instance); 325 | if(instance->transport == HidTransportBle) { 326 | furi_hal_bt_hid_consumer_key_release(event); 327 | } else if(instance->transport == HidTransportUsb) { 328 | furi_hal_hid_consumer_key_release(event); 329 | } else { 330 | furi_crash(NULL); 331 | } 332 | } 333 | 334 | void hid_hal_consumer_key_release_all(Hid* instance) { 335 | furi_assert(instance); 336 | if(instance->transport == HidTransportBle) { 337 | furi_hal_bt_hid_consumer_key_release_all(); 338 | } else if(instance->transport == HidTransportUsb) { 339 | furi_hal_hid_kb_release_all(); 340 | } else { 341 | furi_crash(NULL); 342 | } 343 | } 344 | 345 | void hid_hal_mouse_move(Hid* instance, int8_t dx, int8_t dy) { 346 | furi_assert(instance); 347 | if(instance->transport == HidTransportBle) { 348 | furi_hal_bt_hid_mouse_move(dx, dy); 349 | } else if(instance->transport == HidTransportUsb) { 350 | furi_hal_hid_mouse_move(dx, dy); 351 | } else { 352 | furi_crash(NULL); 353 | } 354 | } 355 | 356 | void hid_hal_mouse_scroll(Hid* instance, int8_t delta) { 357 | furi_assert(instance); 358 | if(instance->transport == HidTransportBle) { 359 | furi_hal_bt_hid_mouse_scroll(delta); 360 | } else if(instance->transport == HidTransportUsb) { 361 | furi_hal_hid_mouse_scroll(delta); 362 | } else { 363 | furi_crash(NULL); 364 | } 365 | } 366 | 367 | void hid_hal_mouse_press(Hid* instance, uint16_t event) { 368 | furi_assert(instance); 369 | if(instance->transport == HidTransportBle) { 370 | furi_hal_bt_hid_mouse_press(event); 371 | } else if(instance->transport == HidTransportUsb) { 372 | furi_hal_hid_mouse_press(event); 373 | } else { 374 | furi_crash(NULL); 375 | } 376 | } 377 | 378 | void hid_hal_mouse_release(Hid* instance, uint16_t event) { 379 | furi_assert(instance); 380 | if(instance->transport == HidTransportBle) { 381 | furi_hal_bt_hid_mouse_release(event); 382 | } else if(instance->transport == HidTransportUsb) { 383 | furi_hal_hid_mouse_release(event); 384 | } else { 385 | furi_crash(NULL); 386 | } 387 | } 388 | 389 | void hid_hal_mouse_release_all(Hid* instance) { 390 | furi_assert(instance); 391 | if(instance->transport == HidTransportBle) { 392 | furi_hal_bt_hid_mouse_release_all(); 393 | } else if(instance->transport == HidTransportUsb) { 394 | furi_hal_hid_mouse_release(HID_MOUSE_BTN_LEFT); 395 | furi_hal_hid_mouse_release(HID_MOUSE_BTN_RIGHT); 396 | } else { 397 | furi_crash(NULL); 398 | } 399 | } 400 | 401 | int32_t hid_usb_app(void* p) { 402 | UNUSED(p); 403 | Hid* app = hid_alloc(HidTransportUsb); 404 | app = hid_app_alloc_view(app); 405 | FuriHalUsbInterface* usb_mode_prev = furi_hal_usb_get_config(); 406 | furi_hal_usb_unlock(); 407 | furi_check(furi_hal_usb_set_config(&usb_hid, NULL) == true); 408 | 409 | bt_hid_connection_status_changed_callback(BtStatusConnected, app); 410 | 411 | dolphin_deed(DolphinDeedPluginStart); 412 | 413 | view_dispatcher_run(app->view_dispatcher); 414 | 415 | furi_hal_usb_set_config(usb_mode_prev, NULL); 416 | 417 | hid_free(app); 418 | 419 | return 0; 420 | } 421 | 422 | int32_t hid_ble_app(void* p) { 423 | UNUSED(p); 424 | Hid* app = hid_alloc(HidTransportBle); 425 | app = hid_app_alloc_view(app); 426 | 427 | bt_disconnect(app->bt); 428 | 429 | // Wait 2nd core to update nvm storage 430 | furi_delay_ms(200); 431 | 432 | // Migrate data from old sd-card folder 433 | Storage* storage = furi_record_open(RECORD_STORAGE); 434 | 435 | storage_common_migrate( 436 | storage, 437 | EXT_PATH("apps/Tools/" HID_BT_KEYS_STORAGE_NAME), 438 | APP_DATA_PATH(HID_BT_KEYS_STORAGE_NAME)); 439 | 440 | bt_keys_storage_set_storage_path(app->bt, APP_DATA_PATH(HID_BT_KEYS_STORAGE_NAME)); 441 | 442 | furi_record_close(RECORD_STORAGE); 443 | 444 | if(!bt_set_profile(app->bt, BtProfileHidKeyboard)) { 445 | FURI_LOG_E(TAG, "Failed to switch to HID profile"); 446 | } 447 | 448 | furi_hal_bt_start_advertising(); 449 | bt_set_status_changed_callback(app->bt, bt_hid_connection_status_changed_callback, app); 450 | 451 | dolphin_deed(DolphinDeedPluginStart); 452 | 453 | view_dispatcher_run(app->view_dispatcher); 454 | 455 | bt_set_status_changed_callback(app->bt, NULL, NULL); 456 | 457 | bt_disconnect(app->bt); 458 | 459 | // Wait 2nd core to update nvm storage 460 | furi_delay_ms(200); 461 | 462 | bt_keys_storage_set_default_path(app->bt); 463 | 464 | if(!bt_set_profile(app->bt, BtProfileSerial)) { 465 | FURI_LOG_E(TAG, "Failed to switch to Serial profile"); 466 | } 467 | 468 | hid_free(app); 469 | 470 | return 0; 471 | } 472 | -------------------------------------------------------------------------------- /hid_app/views/hid_keyboard.c: -------------------------------------------------------------------------------- 1 | #include "hid_keyboard.h" 2 | #include 3 | #include 4 | #include 5 | #include "../hid.h" 6 | #include "hid_icons.h" 7 | 8 | #define TAG "HidKeyboard" 9 | 10 | struct HidKeyboard { 11 | View* view; 12 | Hid* hid; 13 | }; 14 | 15 | typedef struct { 16 | bool shift; 17 | bool alt; 18 | bool ctrl; 19 | bool gui; 20 | uint8_t x; 21 | uint8_t y; 22 | uint8_t last_key_code; 23 | uint16_t modifier_code; 24 | bool ok_pressed; 25 | bool back_pressed; 26 | bool connected; 27 | char key_string[5]; 28 | HidTransport transport; 29 | } HidKeyboardModel; 30 | 31 | typedef struct { 32 | uint8_t width; 33 | char* key; 34 | const Icon* icon; 35 | char* shift_key; 36 | uint8_t value; 37 | } HidKeyboardKey; 38 | 39 | typedef struct { 40 | int8_t x; 41 | int8_t y; 42 | } HidKeyboardPoint; 43 | // 4 BY 12 44 | #define MARGIN_TOP 0 45 | #define MARGIN_LEFT 4 46 | #define KEY_WIDTH 9 47 | #define KEY_HEIGHT 12 48 | #define KEY_PADDING 1 49 | #define ROW_COUNT 7 50 | #define COLUMN_COUNT 12 51 | 52 | // 0 width items are not drawn, but there value is used 53 | const HidKeyboardKey hid_keyboard_keyset[ROW_COUNT][COLUMN_COUNT] = { 54 | { 55 | {.width = 1, .icon = &I_ButtonF1_5x8, .value = HID_KEYBOARD_F1}, 56 | {.width = 1, .icon = &I_ButtonF2_5x8, .value = HID_KEYBOARD_F2}, 57 | {.width = 1, .icon = &I_ButtonF3_5x8, .value = HID_KEYBOARD_F3}, 58 | {.width = 1, .icon = &I_ButtonF4_5x8, .value = HID_KEYBOARD_F4}, 59 | {.width = 1, .icon = &I_ButtonF5_5x8, .value = HID_KEYBOARD_F5}, 60 | {.width = 1, .icon = &I_ButtonF6_5x8, .value = HID_KEYBOARD_F6}, 61 | {.width = 1, .icon = &I_ButtonF7_5x8, .value = HID_KEYBOARD_F7}, 62 | {.width = 1, .icon = &I_ButtonF8_5x8, .value = HID_KEYBOARD_F8}, 63 | {.width = 1, .icon = &I_ButtonF9_5x8, .value = HID_KEYBOARD_F9}, 64 | {.width = 1, .icon = &I_ButtonF10_5x8, .value = HID_KEYBOARD_F10}, 65 | {.width = 1, .icon = &I_ButtonF11_5x8, .value = HID_KEYBOARD_F11}, 66 | {.width = 1, .icon = &I_ButtonF12_5x8, .value = HID_KEYBOARD_F12}, 67 | }, 68 | { 69 | {.width = 1, .icon = NULL, .key = "1", .shift_key = "!", .value = HID_KEYBOARD_1}, 70 | {.width = 1, .icon = NULL, .key = "2", .shift_key = "@", .value = HID_KEYBOARD_2}, 71 | {.width = 1, .icon = NULL, .key = "3", .shift_key = "#", .value = HID_KEYBOARD_3}, 72 | {.width = 1, .icon = NULL, .key = "4", .shift_key = "$", .value = HID_KEYBOARD_4}, 73 | {.width = 1, .icon = NULL, .key = "5", .shift_key = "%", .value = HID_KEYBOARD_5}, 74 | {.width = 1, .icon = NULL, .key = "6", .shift_key = "^", .value = HID_KEYBOARD_6}, 75 | {.width = 1, .icon = NULL, .key = "7", .shift_key = "&", .value = HID_KEYBOARD_7}, 76 | {.width = 1, .icon = NULL, .key = "8", .shift_key = "*", .value = HID_KEYBOARD_8}, 77 | {.width = 1, .icon = NULL, .key = "9", .shift_key = "(", .value = HID_KEYBOARD_9}, 78 | {.width = 1, .icon = NULL, .key = "0", .shift_key = ")", .value = HID_KEYBOARD_0}, 79 | {.width = 2, .icon = &I_Pin_arrow_left_9x7, .value = HID_KEYBOARD_DELETE}, 80 | {.width = 0, .value = HID_KEYBOARD_DELETE}, 81 | }, 82 | { 83 | {.width = 1, .icon = NULL, .key = "q", .shift_key = "Q", .value = HID_KEYBOARD_Q}, 84 | {.width = 1, .icon = NULL, .key = "w", .shift_key = "W", .value = HID_KEYBOARD_W}, 85 | {.width = 1, .icon = NULL, .key = "e", .shift_key = "E", .value = HID_KEYBOARD_E}, 86 | {.width = 1, .icon = NULL, .key = "r", .shift_key = "R", .value = HID_KEYBOARD_R}, 87 | {.width = 1, .icon = NULL, .key = "t", .shift_key = "T", .value = HID_KEYBOARD_T}, 88 | {.width = 1, .icon = NULL, .key = "y", .shift_key = "Y", .value = HID_KEYBOARD_Y}, 89 | {.width = 1, .icon = NULL, .key = "u", .shift_key = "U", .value = HID_KEYBOARD_U}, 90 | {.width = 1, .icon = NULL, .key = "i", .shift_key = "I", .value = HID_KEYBOARD_I}, 91 | {.width = 1, .icon = NULL, .key = "o", .shift_key = "O", .value = HID_KEYBOARD_O}, 92 | {.width = 1, .icon = NULL, .key = "p", .shift_key = "P", .value = HID_KEYBOARD_P}, 93 | {.width = 1, .icon = NULL, .key = "[", .shift_key = "{", .value = HID_KEYBOARD_OPEN_BRACKET}, 94 | {.width = 1, 95 | .icon = NULL, 96 | .key = "]", 97 | .shift_key = "}", 98 | .value = HID_KEYBOARD_CLOSE_BRACKET}, 99 | }, 100 | { 101 | {.width = 1, .icon = NULL, .key = "a", .shift_key = "A", .value = HID_KEYBOARD_A}, 102 | {.width = 1, .icon = NULL, .key = "s", .shift_key = "S", .value = HID_KEYBOARD_S}, 103 | {.width = 1, .icon = NULL, .key = "d", .shift_key = "D", .value = HID_KEYBOARD_D}, 104 | {.width = 1, .icon = NULL, .key = "f", .shift_key = "F", .value = HID_KEYBOARD_F}, 105 | {.width = 1, .icon = NULL, .key = "g", .shift_key = "G", .value = HID_KEYBOARD_G}, 106 | {.width = 1, .icon = NULL, .key = "h", .shift_key = "H", .value = HID_KEYBOARD_H}, 107 | {.width = 1, .icon = NULL, .key = "j", .shift_key = "J", .value = HID_KEYBOARD_J}, 108 | {.width = 1, .icon = NULL, .key = "k", .shift_key = "K", .value = HID_KEYBOARD_K}, 109 | {.width = 1, .icon = NULL, .key = "l", .shift_key = "L", .value = HID_KEYBOARD_L}, 110 | {.width = 1, .icon = NULL, .key = ";", .shift_key = ":", .value = HID_KEYBOARD_SEMICOLON}, 111 | {.width = 2, .icon = &I_Pin_arrow_right_9x7, .value = HID_KEYBOARD_RETURN}, 112 | {.width = 0, .value = HID_KEYBOARD_RETURN}, 113 | }, 114 | { 115 | {.width = 1, .icon = NULL, .key = "z", .shift_key = "Z", .value = HID_KEYBOARD_Z}, 116 | {.width = 1, .icon = NULL, .key = "x", .shift_key = "X", .value = HID_KEYBOARD_X}, 117 | {.width = 1, .icon = NULL, .key = "c", .shift_key = "C", .value = HID_KEYBOARD_C}, 118 | {.width = 1, .icon = NULL, .key = "v", .shift_key = "V", .value = HID_KEYBOARD_V}, 119 | {.width = 1, .icon = NULL, .key = "b", .shift_key = "B", .value = HID_KEYBOARD_B}, 120 | {.width = 1, .icon = NULL, .key = "n", .shift_key = "N", .value = HID_KEYBOARD_N}, 121 | {.width = 1, .icon = NULL, .key = "m", .shift_key = "M", .value = HID_KEYBOARD_M}, 122 | {.width = 1, .icon = NULL, .key = "/", .shift_key = "?", .value = HID_KEYBOARD_SLASH}, 123 | {.width = 1, .icon = NULL, .key = "\\", .shift_key = "|", .value = HID_KEYBOARD_BACKSLASH}, 124 | {.width = 1, .icon = NULL, .key = "`", .shift_key = "~", .value = HID_KEYBOARD_GRAVE_ACCENT}, 125 | {.width = 1, .icon = &I_ButtonUp_7x4, .value = HID_KEYBOARD_UP_ARROW}, 126 | {.width = 1, .icon = NULL, .key = "-", .shift_key = "_", .value = HID_KEYBOARD_MINUS}, 127 | }, 128 | { 129 | {.width = 1, .icon = &I_Pin_arrow_up_7x9, .value = HID_KEYBOARD_L_SHIFT}, 130 | {.width = 1, .icon = NULL, .key = ",", .shift_key = "<", .value = HID_KEYBOARD_COMMA}, 131 | {.width = 1, .icon = NULL, .key = ".", .shift_key = ">", .value = HID_KEYBOARD_DOT}, 132 | {.width = 4, .icon = NULL, .key = " ", .value = HID_KEYBOARD_SPACEBAR}, 133 | {.width = 0, .value = HID_KEYBOARD_SPACEBAR}, 134 | {.width = 0, .value = HID_KEYBOARD_SPACEBAR}, 135 | {.width = 0, .value = HID_KEYBOARD_SPACEBAR}, 136 | {.width = 1, .icon = NULL, .key = "'", .shift_key = "\"", .value = HID_KEYBOARD_APOSTROPHE}, 137 | {.width = 1, .icon = NULL, .key = "=", .shift_key = "+", .value = HID_KEYBOARD_EQUAL_SIGN}, 138 | {.width = 1, .icon = &I_ButtonLeft_4x7, .value = HID_KEYBOARD_LEFT_ARROW}, 139 | {.width = 1, .icon = &I_ButtonDown_7x4, .value = HID_KEYBOARD_DOWN_ARROW}, 140 | {.width = 1, .icon = &I_ButtonRight_4x7, .value = HID_KEYBOARD_RIGHT_ARROW}, 141 | }, 142 | { 143 | {.width = 3, .icon = NULL, .key = "Ctrl", .value = HID_KEYBOARD_L_CTRL}, 144 | {.width = 0, .icon = NULL, .value = HID_KEYBOARD_L_CTRL}, 145 | {.width = 0, .icon = NULL, .value = HID_KEYBOARD_L_CTRL}, 146 | {.width = 3, .icon = NULL, .key = "Alt", .value = HID_KEYBOARD_L_ALT}, 147 | {.width = 0, .icon = NULL, .value = HID_KEYBOARD_L_ALT}, 148 | {.width = 0, .icon = NULL, .value = HID_KEYBOARD_L_ALT}, 149 | {.width = 3, .icon = NULL, .key = "Cmd", .value = HID_KEYBOARD_L_GUI}, 150 | {.width = 0, .icon = NULL, .value = HID_KEYBOARD_L_GUI}, 151 | {.width = 0, .icon = NULL, .value = HID_KEYBOARD_L_GUI}, 152 | {.width = 3, .icon = NULL, .key = "Tab", .value = HID_KEYBOARD_TAB}, 153 | {.width = 0, .icon = NULL, .value = HID_KEYBOARD_TAB}, 154 | {.width = 0, .icon = NULL, .value = HID_KEYBOARD_TAB}, 155 | }, 156 | }; 157 | 158 | static void hid_keyboard_to_upper(char* str) { 159 | while(*str) { 160 | *str = toupper((unsigned char)*str); 161 | str++; 162 | } 163 | } 164 | 165 | static void hid_keyboard_draw_key( 166 | Canvas* canvas, 167 | HidKeyboardModel* model, 168 | uint8_t x, 169 | uint8_t y, 170 | HidKeyboardKey key, 171 | bool selected) { 172 | if(!key.width) return; 173 | 174 | canvas_set_color(canvas, ColorBlack); 175 | uint8_t keyWidth = KEY_WIDTH * key.width + KEY_PADDING * (key.width - 1); 176 | if(selected) { 177 | // Draw a filled box 178 | elements_slightly_rounded_box( 179 | canvas, 180 | MARGIN_LEFT + x * (KEY_WIDTH + KEY_PADDING), 181 | MARGIN_TOP + y * (KEY_HEIGHT + KEY_PADDING), 182 | keyWidth, 183 | KEY_HEIGHT); 184 | canvas_set_color(canvas, ColorWhite); 185 | } else { 186 | // Draw a framed box 187 | elements_slightly_rounded_frame( 188 | canvas, 189 | MARGIN_LEFT + x * (KEY_WIDTH + KEY_PADDING), 190 | MARGIN_TOP + y * (KEY_HEIGHT + KEY_PADDING), 191 | keyWidth, 192 | KEY_HEIGHT); 193 | } 194 | if(key.icon != NULL) { 195 | // Draw the icon centered on the button 196 | canvas_draw_icon( 197 | canvas, 198 | MARGIN_LEFT + x * (KEY_WIDTH + KEY_PADDING) + keyWidth / 2 - key.icon->width / 2, 199 | MARGIN_TOP + y * (KEY_HEIGHT + KEY_PADDING) + KEY_HEIGHT / 2 - key.icon->height / 2, 200 | key.icon); 201 | } else { 202 | // If shift is toggled use the shift key when available 203 | strcpy(model->key_string, (model->shift && key.shift_key != 0) ? key.shift_key : key.key); 204 | // Upper case if ctrl or alt was toggled true 205 | if((model->ctrl && key.value == HID_KEYBOARD_L_CTRL) || 206 | (model->alt && key.value == HID_KEYBOARD_L_ALT) || 207 | (model->gui && key.value == HID_KEYBOARD_L_GUI)) { 208 | hid_keyboard_to_upper(model->key_string); 209 | } 210 | canvas_draw_str_aligned( 211 | canvas, 212 | MARGIN_LEFT + x * (KEY_WIDTH + KEY_PADDING) + keyWidth / 2 + 1, 213 | MARGIN_TOP + y * (KEY_HEIGHT + KEY_PADDING) + KEY_HEIGHT / 2, 214 | AlignCenter, 215 | AlignCenter, 216 | model->key_string); 217 | } 218 | } 219 | 220 | static void hid_keyboard_draw_callback(Canvas* canvas, void* context) { 221 | furi_assert(context); 222 | HidKeyboardModel* model = context; 223 | 224 | // Header 225 | if((!model->connected) && (model->transport == HidTransportBle)) { 226 | canvas_draw_icon(canvas, 0, 0, &I_Ble_disconnected_15x15); 227 | canvas_set_font(canvas, FontPrimary); 228 | elements_multiline_text_aligned(canvas, 17, 3, AlignLeft, AlignTop, "Keyboard"); 229 | 230 | canvas_draw_icon(canvas, 68, 3, &I_Pin_back_arrow_10x8); 231 | canvas_set_font(canvas, FontSecondary); 232 | elements_multiline_text_aligned(canvas, 127, 4, AlignRight, AlignTop, "Hold to exit"); 233 | 234 | elements_multiline_text_aligned( 235 | canvas, 4, 60, AlignLeft, AlignBottom, "Waiting for Connection..."); 236 | return; // Dont render the keyboard if we are not yet connected 237 | } 238 | 239 | canvas_set_font(canvas, FontKeyboard); 240 | // Start shifting the all keys up if on the next row (Scrolling) 241 | uint8_t initY = model->y == 0 ? 0 : 1; 242 | 243 | if(model->y > 5) { 244 | initY = model->y - 4; 245 | } 246 | 247 | for(uint8_t y = initY; y < ROW_COUNT; y++) { 248 | const HidKeyboardKey* keyboardKeyRow = hid_keyboard_keyset[y]; 249 | uint8_t x = 0; 250 | for(uint8_t i = 0; i < COLUMN_COUNT; i++) { 251 | HidKeyboardKey key = keyboardKeyRow[i]; 252 | // Select when the button is hovered 253 | // Select if the button is hovered within its width 254 | // Select if back is clicked and its the backspace key 255 | // Deselect when the button clicked or not hovered 256 | bool keySelected = (x <= model->x && model->x < (x + key.width)) && y == model->y; 257 | bool backSelected = model->back_pressed && key.value == HID_KEYBOARD_DELETE; 258 | hid_keyboard_draw_key( 259 | canvas, 260 | model, 261 | x, 262 | y - initY, 263 | key, 264 | (!model->ok_pressed && keySelected) || backSelected); 265 | x += key.width; 266 | } 267 | } 268 | } 269 | 270 | static uint8_t hid_keyboard_get_selected_key(HidKeyboardModel* model) { 271 | HidKeyboardKey key = hid_keyboard_keyset[model->y][model->x]; 272 | return key.value; 273 | } 274 | 275 | static void hid_keyboard_get_select_key(HidKeyboardModel* model, HidKeyboardPoint delta) { 276 | // Keep going until a valid spot is found, this allows for nulls and zero width keys in the map 277 | do { 278 | const int delta_sum = model->y + delta.y; 279 | model->y = delta_sum < 0 ? ROW_COUNT - 1 : delta_sum % ROW_COUNT; 280 | } while(delta.y != 0 && hid_keyboard_keyset[model->y][model->x].value == 0); 281 | 282 | do { 283 | const int delta_sum = model->x + delta.x; 284 | model->x = delta_sum < 0 ? COLUMN_COUNT - 1 : delta_sum % COLUMN_COUNT; 285 | } while(delta.x != 0 && hid_keyboard_keyset[model->y][model->x].width == 286 | 0); // Skip zero width keys, pretend they are one key 287 | } 288 | 289 | static void hid_keyboard_process(HidKeyboard* hid_keyboard, InputEvent* event) { 290 | with_view_model( 291 | hid_keyboard->view, 292 | HidKeyboardModel * model, 293 | { 294 | if(event->key == InputKeyOk) { 295 | if(event->type == InputTypePress) { 296 | model->ok_pressed = true; 297 | } else if(event->type == InputTypeLong || event->type == InputTypeShort) { 298 | model->last_key_code = hid_keyboard_get_selected_key(model); 299 | 300 | // Toggle the modifier key when clicked, and click the key 301 | if(model->last_key_code == HID_KEYBOARD_L_SHIFT) { 302 | model->shift = !model->shift; 303 | if(model->shift) 304 | model->modifier_code |= KEY_MOD_LEFT_SHIFT; 305 | else 306 | model->modifier_code &= ~KEY_MOD_LEFT_SHIFT; 307 | } else if(model->last_key_code == HID_KEYBOARD_L_ALT) { 308 | model->alt = !model->alt; 309 | if(model->alt) 310 | model->modifier_code |= KEY_MOD_LEFT_ALT; 311 | else 312 | model->modifier_code &= ~KEY_MOD_LEFT_ALT; 313 | } else if(model->last_key_code == HID_KEYBOARD_L_CTRL) { 314 | model->ctrl = !model->ctrl; 315 | if(model->ctrl) 316 | model->modifier_code |= KEY_MOD_LEFT_CTRL; 317 | else 318 | model->modifier_code &= ~KEY_MOD_LEFT_CTRL; 319 | } else if(model->last_key_code == HID_KEYBOARD_L_GUI) { 320 | model->gui = !model->gui; 321 | if(model->gui) 322 | model->modifier_code |= KEY_MOD_LEFT_GUI; 323 | else 324 | model->modifier_code &= ~KEY_MOD_LEFT_GUI; 325 | } 326 | hid_hal_keyboard_press( 327 | hid_keyboard->hid, model->modifier_code | model->last_key_code); 328 | } else if(event->type == InputTypeRelease) { 329 | // Release happens after short and long presses 330 | hid_hal_keyboard_release( 331 | hid_keyboard->hid, model->modifier_code | model->last_key_code); 332 | model->ok_pressed = false; 333 | } 334 | } else if(event->key == InputKeyBack) { 335 | // If back is pressed for a short time, backspace 336 | if(event->type == InputTypePress) { 337 | model->back_pressed = true; 338 | } else if(event->type == InputTypeShort) { 339 | hid_hal_keyboard_press(hid_keyboard->hid, HID_KEYBOARD_DELETE); 340 | hid_hal_keyboard_release(hid_keyboard->hid, HID_KEYBOARD_DELETE); 341 | } else if(event->type == InputTypeRelease) { 342 | model->back_pressed = false; 343 | } 344 | } else if(event->type == InputTypePress || event->type == InputTypeRepeat) { 345 | // Cycle the selected keys 346 | if(event->key == InputKeyUp) { 347 | hid_keyboard_get_select_key(model, (HidKeyboardPoint){.x = 0, .y = -1}); 348 | } else if(event->key == InputKeyDown) { 349 | hid_keyboard_get_select_key(model, (HidKeyboardPoint){.x = 0, .y = 1}); 350 | } else if(event->key == InputKeyLeft) { 351 | hid_keyboard_get_select_key(model, (HidKeyboardPoint){.x = -1, .y = 0}); 352 | } else if(event->key == InputKeyRight) { 353 | hid_keyboard_get_select_key(model, (HidKeyboardPoint){.x = 1, .y = 0}); 354 | } 355 | } 356 | }, 357 | true); 358 | } 359 | 360 | static bool hid_keyboard_input_callback(InputEvent* event, void* context) { 361 | furi_assert(context); 362 | HidKeyboard* hid_keyboard = context; 363 | bool consumed = false; 364 | 365 | if(event->type == InputTypeLong && event->key == InputKeyBack) { 366 | hid_hal_keyboard_release_all(hid_keyboard->hid); 367 | } else { 368 | hid_keyboard_process(hid_keyboard, event); 369 | consumed = true; 370 | } 371 | 372 | return consumed; 373 | } 374 | 375 | HidKeyboard* hid_keyboard_alloc(Hid* bt_hid) { 376 | HidKeyboard* hid_keyboard = malloc(sizeof(HidKeyboard)); 377 | hid_keyboard->view = view_alloc(); 378 | hid_keyboard->hid = bt_hid; 379 | view_set_context(hid_keyboard->view, hid_keyboard); 380 | view_allocate_model(hid_keyboard->view, ViewModelTypeLocking, sizeof(HidKeyboardModel)); 381 | view_set_draw_callback(hid_keyboard->view, hid_keyboard_draw_callback); 382 | view_set_input_callback(hid_keyboard->view, hid_keyboard_input_callback); 383 | 384 | with_view_model( 385 | hid_keyboard->view, 386 | HidKeyboardModel * model, 387 | { 388 | model->transport = bt_hid->transport; 389 | model->y = 1; 390 | }, 391 | true); 392 | 393 | return hid_keyboard; 394 | } 395 | 396 | void hid_keyboard_free(HidKeyboard* hid_keyboard) { 397 | furi_assert(hid_keyboard); 398 | view_free(hid_keyboard->view); 399 | free(hid_keyboard); 400 | } 401 | 402 | View* hid_keyboard_get_view(HidKeyboard* hid_keyboard) { 403 | furi_assert(hid_keyboard); 404 | return hid_keyboard->view; 405 | } 406 | 407 | void hid_keyboard_set_connected_status(HidKeyboard* hid_keyboard, bool connected) { 408 | furi_assert(hid_keyboard); 409 | with_view_model( 410 | hid_keyboard->view, HidKeyboardModel * model, { model->connected = connected; }, true); 411 | } 412 | --------------------------------------------------------------------------------