[FL-2882] BLE tiktok controller (#1859)
* bt hid: introduce tiktok controller * assets: add bt tiktok assets * bt hid: finish tiktok draw * bt hid: add input process to tiktok view * bt hid: add tiktok swipe emulation * bt hid: fix exit from tiktok controller * ble hid: add delay to emulate double tap * ble hid: change options order * bt hid: build as external application * ble hid: fix naming Co-authored-by: あく <alleteam@gmail.com>
| @ -5,6 +5,5 @@ App( | ||||
|     provides=[ | ||||
|         "music_player", | ||||
|         "snake_game", | ||||
|         "bt_hid", | ||||
|     ], | ||||
| ) | ||||
|  | ||||
| @ -1,15 +1,10 @@ | ||||
| App( | ||||
|     appid="bt_hid", | ||||
|     name="Bluetooth Remote", | ||||
|     apptype=FlipperAppType.PLUGIN, | ||||
|     apptype=FlipperAppType.EXTERNAL, | ||||
|     entry_point="bt_hid_app", | ||||
|     stack_size=1 * 1024, | ||||
|     cdefines=["APP_BLE_HID"], | ||||
|     requires=[ | ||||
|         "bt", | ||||
|         "gui", | ||||
|     ], | ||||
|     order=10, | ||||
|     fap_icon="bt_remote_10px.png", | ||||
|     fap_category="Tools", | ||||
|     fap_icon="bt_remote_10px.png", | ||||
|     fap_icon_assets="assets", | ||||
| ) | ||||
|  | ||||
							
								
								
									
										
											BIN
										
									
								
								applications/plugins/bt_hid_app/assets/Arr_dwn_7x9.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 3.5 KiB | 
							
								
								
									
										
											BIN
										
									
								
								applications/plugins/bt_hid_app/assets/Arr_up_7x9.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 3.5 KiB | 
							
								
								
									
										
											BIN
										
									
								
								applications/plugins/bt_hid_app/assets/Ble_connected_15x15.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 3.5 KiB | 
| After Width: | Height: | Size: 3.5 KiB | 
							
								
								
									
										
											BIN
										
									
								
								applications/plugins/bt_hid_app/assets/Button_18x18.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 3.5 KiB | 
							
								
								
									
										
											BIN
										
									
								
								applications/plugins/bt_hid_app/assets/Circles_47x47.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 3.6 KiB | 
							
								
								
									
										
											BIN
										
									
								
								applications/plugins/bt_hid_app/assets/Left_mouse_icon_9x9.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 3.5 KiB | 
							
								
								
									
										
											BIN
										
									
								
								applications/plugins/bt_hid_app/assets/Like_def_11x9.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 3.5 KiB | 
							
								
								
									
										
											BIN
										
									
								
								applications/plugins/bt_hid_app/assets/Like_pressed_17x17.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 3.6 KiB | 
							
								
								
									
										
											BIN
										
									
								
								applications/plugins/bt_hid_app/assets/Ok_btn_pressed_13x13.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 3.5 KiB | 
							
								
								
									
										
											BIN
										
									
								
								applications/plugins/bt_hid_app/assets/Pressed_Button_13x13.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 3.5 KiB | 
							
								
								
									
										
											BIN
										
									
								
								applications/plugins/bt_hid_app/assets/Right_mouse_icon_9x9.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 3.5 KiB | 
							
								
								
									
										
											BIN
										
									
								
								applications/plugins/bt_hid_app/assets/Space_65x18.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 3.5 KiB | 
							
								
								
									
										
											BIN
										
									
								
								applications/plugins/bt_hid_app/assets/Voldwn_6x6.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 3.5 KiB | 
							
								
								
									
										
											BIN
										
									
								
								applications/plugins/bt_hid_app/assets/Volup_8x6.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 3.5 KiB | 
| @ -9,6 +9,7 @@ enum BtDebugSubmenuIndex { | ||||
|     BtHidSubmenuIndexKeynote, | ||||
|     BtHidSubmenuIndexKeyboard, | ||||
|     BtHidSubmenuIndexMedia, | ||||
|     BtHidSubmenuIndexTikTok, | ||||
|     BtHidSubmenuIndexMouse, | ||||
| }; | ||||
| 
 | ||||
| @ -27,6 +28,9 @@ void bt_hid_submenu_callback(void* context, uint32_t index) { | ||||
|     } else if(index == BtHidSubmenuIndexMouse) { | ||||
|         app->view_id = BtHidViewMouse; | ||||
|         view_dispatcher_switch_to_view(app->view_dispatcher, BtHidViewMouse); | ||||
|     } else if(index == BtHidSubmenuIndexTikTok) { | ||||
|         app->view_id = BtHidViewTikTok; | ||||
|         view_dispatcher_switch_to_view(app->view_dispatcher, BtHidViewTikTok); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| @ -65,6 +69,7 @@ void bt_hid_connection_status_changed_callback(BtStatus status, void* context) { | ||||
|     bt_hid_keyboard_set_connected_status(bt_hid->bt_hid_keyboard, connected); | ||||
|     bt_hid_media_set_connected_status(bt_hid->bt_hid_media, connected); | ||||
|     bt_hid_mouse_set_connected_status(bt_hid->bt_hid_mouse, connected); | ||||
|     bt_hid_tiktok_set_connected_status(bt_hid->bt_hid_tiktok, connected); | ||||
| } | ||||
| 
 | ||||
| BtHid* bt_hid_app_alloc() { | ||||
| @ -91,6 +96,8 @@ BtHid* bt_hid_app_alloc() { | ||||
|     submenu_add_item( | ||||
|         app->submenu, "Keyboard", BtHidSubmenuIndexKeyboard, bt_hid_submenu_callback, app); | ||||
|     submenu_add_item(app->submenu, "Media", BtHidSubmenuIndexMedia, bt_hid_submenu_callback, app); | ||||
|     submenu_add_item( | ||||
|         app->submenu, "TikTok Controller", BtHidSubmenuIndexTikTok, bt_hid_submenu_callback, app); | ||||
|     submenu_add_item(app->submenu, "Mouse", BtHidSubmenuIndexMouse, bt_hid_submenu_callback, app); | ||||
|     view_set_previous_callback(submenu_get_view(app->submenu), bt_hid_exit); | ||||
|     view_dispatcher_add_view( | ||||
| @ -127,6 +134,13 @@ BtHid* bt_hid_app_alloc() { | ||||
|     view_dispatcher_add_view( | ||||
|         app->view_dispatcher, BtHidViewMedia, bt_hid_media_get_view(app->bt_hid_media)); | ||||
| 
 | ||||
|     // TikTok view
 | ||||
|     app->bt_hid_tiktok = bt_hid_tiktok_alloc(); | ||||
|     view_set_previous_callback( | ||||
|         bt_hid_tiktok_get_view(app->bt_hid_tiktok), bt_hid_exit_confirm_view); | ||||
|     view_dispatcher_add_view( | ||||
|         app->view_dispatcher, BtHidViewTikTok, bt_hid_tiktok_get_view(app->bt_hid_tiktok)); | ||||
| 
 | ||||
|     // Mouse view
 | ||||
|     app->bt_hid_mouse = bt_hid_mouse_alloc(); | ||||
|     view_set_previous_callback(bt_hid_mouse_get_view(app->bt_hid_mouse), bt_hid_exit_confirm_view); | ||||
| @ -159,6 +173,8 @@ void bt_hid_app_free(BtHid* app) { | ||||
|     bt_hid_media_free(app->bt_hid_media); | ||||
|     view_dispatcher_remove_view(app->view_dispatcher, BtHidViewMouse); | ||||
|     bt_hid_mouse_free(app->bt_hid_mouse); | ||||
|     view_dispatcher_remove_view(app->view_dispatcher, BtHidViewTikTok); | ||||
|     bt_hid_tiktok_free(app->bt_hid_tiktok); | ||||
|     view_dispatcher_free(app->view_dispatcher); | ||||
| 
 | ||||
|     // Close records
 | ||||
|  | ||||
| @ -13,6 +13,7 @@ | ||||
| #include "views/bt_hid_keyboard.h" | ||||
| #include "views/bt_hid_media.h" | ||||
| #include "views/bt_hid_mouse.h" | ||||
| #include "views/bt_hid_tiktok.h" | ||||
| 
 | ||||
| typedef struct { | ||||
|     Bt* bt; | ||||
| @ -25,6 +26,7 @@ typedef struct { | ||||
|     BtHidKeyboard* bt_hid_keyboard; | ||||
|     BtHidMedia* bt_hid_media; | ||||
|     BtHidMouse* bt_hid_mouse; | ||||
|     BtHidTikTok* bt_hid_tiktok; | ||||
|     uint32_t view_id; | ||||
| } BtHid; | ||||
| 
 | ||||
| @ -34,5 +36,6 @@ typedef enum { | ||||
|     BtHidViewKeyboard, | ||||
|     BtHidViewMedia, | ||||
|     BtHidViewMouse, | ||||
|     BtHidViewTikTok, | ||||
|     BtHidViewExitConfirm, | ||||
| } BtHidView; | ||||
|  | ||||
| @ -5,6 +5,8 @@ | ||||
| #include <gui/elements.h> | ||||
| #include <gui/icon_i.h> | ||||
| 
 | ||||
| #include "bt_hid_icons.h" | ||||
| 
 | ||||
| struct BtHidKeyboard { | ||||
|     View* view; | ||||
| }; | ||||
|  | ||||
| @ -4,6 +4,8 @@ | ||||
| #include <furi_hal_usb_hid.h> | ||||
| #include <gui/elements.h> | ||||
| 
 | ||||
| #include "bt_hid_icons.h" | ||||
| 
 | ||||
| struct BtHidKeynote { | ||||
|     View* view; | ||||
| }; | ||||
|  | ||||
| @ -4,6 +4,8 @@ | ||||
| #include <furi_hal_usb_hid.h> | ||||
| #include <gui/elements.h> | ||||
| 
 | ||||
| #include "bt_hid_icons.h" | ||||
| 
 | ||||
| struct BtHidMedia { | ||||
|     View* view; | ||||
| }; | ||||
|  | ||||
| @ -4,6 +4,8 @@ | ||||
| #include <furi_hal_usb_hid.h> | ||||
| #include <gui/elements.h> | ||||
| 
 | ||||
| #include "bt_hid_icons.h" | ||||
| 
 | ||||
| struct BtHidMouse { | ||||
|     View* view; | ||||
| }; | ||||
|  | ||||
							
								
								
									
										207
									
								
								applications/plugins/bt_hid_app/views/bt_hid_tiktok.c
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,207 @@ | ||||
| #include "bt_hid_tiktok.h" | ||||
| #include <furi.h> | ||||
| #include <furi_hal_bt_hid.h> | ||||
| #include <furi_hal_usb_hid.h> | ||||
| #include <gui/elements.h> | ||||
| 
 | ||||
| #include "bt_hid_icons.h" | ||||
| 
 | ||||
| struct BtHidTikTok { | ||||
|     View* view; | ||||
| }; | ||||
| 
 | ||||
| typedef struct { | ||||
|     bool left_pressed; | ||||
|     bool up_pressed; | ||||
|     bool right_pressed; | ||||
|     bool down_pressed; | ||||
|     bool ok_pressed; | ||||
|     bool connected; | ||||
| } BtHidTikTokModel; | ||||
| 
 | ||||
| static void bt_hid_tiktok_draw_callback(Canvas* canvas, void* context) { | ||||
|     furi_assert(context); | ||||
|     BtHidTikTokModel* model = context; | ||||
| 
 | ||||
|     // Header
 | ||||
|     if(model->connected) { | ||||
|         canvas_draw_icon(canvas, 0, 0, &I_Ble_connected_15x15); | ||||
|     } else { | ||||
|         canvas_draw_icon(canvas, 0, 0, &I_Ble_disconnected_15x15); | ||||
|     } | ||||
|     canvas_set_font(canvas, FontPrimary); | ||||
|     elements_multiline_text_aligned(canvas, 17, 3, AlignLeft, AlignTop, "TikTok"); | ||||
|     canvas_set_font(canvas, FontSecondary); | ||||
| 
 | ||||
|     // Keypad circles
 | ||||
|     canvas_draw_icon(canvas, 76, 8, &I_Circles_47x47); | ||||
| 
 | ||||
|     // Up
 | ||||
|     if(model->up_pressed) { | ||||
|         canvas_set_bitmap_mode(canvas, 1); | ||||
|         canvas_draw_icon(canvas, 93, 9, &I_Pressed_Button_13x13); | ||||
|         canvas_set_bitmap_mode(canvas, 0); | ||||
|         canvas_set_color(canvas, ColorWhite); | ||||
|     } | ||||
|     canvas_draw_icon(canvas, 96, 11, &I_Arr_up_7x9); | ||||
|     canvas_set_color(canvas, ColorBlack); | ||||
| 
 | ||||
|     // Down
 | ||||
|     if(model->down_pressed) { | ||||
|         canvas_set_bitmap_mode(canvas, 1); | ||||
|         canvas_draw_icon(canvas, 93, 41, &I_Pressed_Button_13x13); | ||||
|         canvas_set_bitmap_mode(canvas, 0); | ||||
|         canvas_set_color(canvas, ColorWhite); | ||||
|     } | ||||
|     canvas_draw_icon(canvas, 96, 44, &I_Arr_dwn_7x9); | ||||
|     canvas_set_color(canvas, ColorBlack); | ||||
| 
 | ||||
|     // Left
 | ||||
|     if(model->left_pressed) { | ||||
|         canvas_set_bitmap_mode(canvas, 1); | ||||
|         canvas_draw_icon(canvas, 77, 25, &I_Pressed_Button_13x13); | ||||
|         canvas_set_bitmap_mode(canvas, 0); | ||||
|         canvas_set_color(canvas, ColorWhite); | ||||
|     } | ||||
|     canvas_draw_icon(canvas, 81, 29, &I_Voldwn_6x6); | ||||
|     canvas_set_color(canvas, ColorBlack); | ||||
| 
 | ||||
|     // Right
 | ||||
|     if(model->right_pressed) { | ||||
|         canvas_set_bitmap_mode(canvas, 1); | ||||
|         canvas_draw_icon(canvas, 109, 25, &I_Pressed_Button_13x13); | ||||
|         canvas_set_bitmap_mode(canvas, 0); | ||||
|         canvas_set_color(canvas, ColorWhite); | ||||
|     } | ||||
|     canvas_draw_icon(canvas, 111, 29, &I_Volup_8x6); | ||||
|     canvas_set_color(canvas, ColorBlack); | ||||
| 
 | ||||
|     // Ok
 | ||||
|     if(model->ok_pressed) { | ||||
|         canvas_draw_icon(canvas, 91, 23, &I_Like_pressed_17x17); | ||||
|     } else { | ||||
|         canvas_draw_icon(canvas, 94, 27, &I_Like_def_11x9); | ||||
|     } | ||||
|     // Exit
 | ||||
|     canvas_draw_icon(canvas, 0, 54, &I_Pin_back_arrow_10x8); | ||||
|     canvas_set_font(canvas, FontSecondary); | ||||
|     elements_multiline_text_aligned(canvas, 13, 62, AlignLeft, AlignBottom, "Hold to exit"); | ||||
| } | ||||
| 
 | ||||
| static void bt_hid_tiktok_process_press(BtHidTikTok* bt_hid_tiktok, InputEvent* event) { | ||||
|     with_view_model( | ||||
|         bt_hid_tiktok->view, | ||||
|         BtHidTikTokModel * model, | ||||
|         { | ||||
|             if(event->key == InputKeyUp) { | ||||
|                 model->up_pressed = true; | ||||
|             } else if(event->key == InputKeyDown) { | ||||
|                 model->down_pressed = true; | ||||
|             } else if(event->key == InputKeyLeft) { | ||||
|                 model->left_pressed = true; | ||||
|                 furi_hal_bt_hid_consumer_key_press(HID_CONSUMER_VOLUME_DECREMENT); | ||||
|             } else if(event->key == InputKeyRight) { | ||||
|                 model->right_pressed = true; | ||||
|                 furi_hal_bt_hid_consumer_key_press(HID_CONSUMER_VOLUME_INCREMENT); | ||||
|             } else if(event->key == InputKeyOk) { | ||||
|                 model->ok_pressed = true; | ||||
|             } | ||||
|         }, | ||||
|         true); | ||||
| } | ||||
| 
 | ||||
| static void bt_hid_tiktok_process_release(BtHidTikTok* bt_hid_tiktok, InputEvent* event) { | ||||
|     with_view_model( | ||||
|         bt_hid_tiktok->view, | ||||
|         BtHidTikTokModel * model, | ||||
|         { | ||||
|             if(event->key == InputKeyUp) { | ||||
|                 model->up_pressed = false; | ||||
|             } else if(event->key == InputKeyDown) { | ||||
|                 model->down_pressed = false; | ||||
|             } else if(event->key == InputKeyLeft) { | ||||
|                 model->left_pressed = false; | ||||
|                 furi_hal_bt_hid_consumer_key_release(HID_CONSUMER_VOLUME_DECREMENT); | ||||
|             } else if(event->key == InputKeyRight) { | ||||
|                 model->right_pressed = false; | ||||
|                 furi_hal_bt_hid_consumer_key_release(HID_CONSUMER_VOLUME_INCREMENT); | ||||
|             } else if(event->key == InputKeyOk) { | ||||
|                 model->ok_pressed = false; | ||||
|             } | ||||
|         }, | ||||
|         true); | ||||
| } | ||||
| 
 | ||||
| static bool bt_hid_tiktok_input_callback(InputEvent* event, void* context) { | ||||
|     furi_assert(context); | ||||
|     BtHidTikTok* bt_hid_tiktok = context; | ||||
|     bool consumed = false; | ||||
| 
 | ||||
|     if(event->type == InputTypePress) { | ||||
|         bt_hid_tiktok_process_press(bt_hid_tiktok, event); | ||||
|         consumed = true; | ||||
|     } else if(event->type == InputTypeRelease) { | ||||
|         bt_hid_tiktok_process_release(bt_hid_tiktok, event); | ||||
|         consumed = true; | ||||
|     } else if(event->type == InputTypeShort) { | ||||
|         if(event->key == InputKeyOk) { | ||||
|             furi_hal_bt_hid_mouse_press(HID_MOUSE_BTN_LEFT); | ||||
|             furi_delay_ms(50); | ||||
|             furi_hal_bt_hid_mouse_release(HID_MOUSE_BTN_LEFT); | ||||
|             furi_delay_ms(50); | ||||
|             furi_hal_bt_hid_mouse_press(HID_MOUSE_BTN_LEFT); | ||||
|             furi_delay_ms(50); | ||||
|             furi_hal_bt_hid_mouse_release(HID_MOUSE_BTN_LEFT); | ||||
|             consumed = true; | ||||
|         } else if(event->key == InputKeyUp) { | ||||
|             // Emulate up swipe
 | ||||
|             furi_hal_bt_hid_mouse_scroll(-6); | ||||
|             furi_hal_bt_hid_mouse_scroll(-12); | ||||
|             furi_hal_bt_hid_mouse_scroll(-19); | ||||
|             furi_hal_bt_hid_mouse_scroll(-12); | ||||
|             furi_hal_bt_hid_mouse_scroll(-6); | ||||
|             consumed = true; | ||||
|         } else if(event->key == InputKeyDown) { | ||||
|             // Emulate down swipe
 | ||||
|             furi_hal_bt_hid_mouse_scroll(6); | ||||
|             furi_hal_bt_hid_mouse_scroll(12); | ||||
|             furi_hal_bt_hid_mouse_scroll(19); | ||||
|             furi_hal_bt_hid_mouse_scroll(12); | ||||
|             furi_hal_bt_hid_mouse_scroll(6); | ||||
|             consumed = true; | ||||
|         } else if(event->key == InputKeyBack) { | ||||
|             furi_hal_bt_hid_consumer_key_release_all(); | ||||
|             consumed = true; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     return consumed; | ||||
| } | ||||
| 
 | ||||
| BtHidTikTok* bt_hid_tiktok_alloc() { | ||||
|     BtHidTikTok* bt_hid_tiktok = malloc(sizeof(BtHidTikTok)); | ||||
|     bt_hid_tiktok->view = view_alloc(); | ||||
|     view_set_context(bt_hid_tiktok->view, bt_hid_tiktok); | ||||
|     view_allocate_model(bt_hid_tiktok->view, ViewModelTypeLocking, sizeof(BtHidTikTokModel)); | ||||
|     view_set_draw_callback(bt_hid_tiktok->view, bt_hid_tiktok_draw_callback); | ||||
|     view_set_input_callback(bt_hid_tiktok->view, bt_hid_tiktok_input_callback); | ||||
| 
 | ||||
|     return bt_hid_tiktok; | ||||
| } | ||||
| 
 | ||||
| void bt_hid_tiktok_free(BtHidTikTok* bt_hid_tiktok) { | ||||
|     furi_assert(bt_hid_tiktok); | ||||
|     view_free(bt_hid_tiktok->view); | ||||
|     free(bt_hid_tiktok); | ||||
| } | ||||
| 
 | ||||
| View* bt_hid_tiktok_get_view(BtHidTikTok* bt_hid_tiktok) { | ||||
|     furi_assert(bt_hid_tiktok); | ||||
|     return bt_hid_tiktok->view; | ||||
| } | ||||
| 
 | ||||
| void bt_hid_tiktok_set_connected_status(BtHidTikTok* bt_hid_tiktok, bool connected) { | ||||
|     furi_assert(bt_hid_tiktok); | ||||
|     with_view_model( | ||||
|         bt_hid_tiktok->view, BtHidTikTokModel * model, { model->connected = connected; }, true); | ||||
| } | ||||
							
								
								
									
										13
									
								
								applications/plugins/bt_hid_app/views/bt_hid_tiktok.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,13 @@ | ||||
| #pragma once | ||||
| 
 | ||||
| #include <gui/view.h> | ||||
| 
 | ||||
| typedef struct BtHidTikTok BtHidTikTok; | ||||
| 
 | ||||
| BtHidTikTok* bt_hid_tiktok_alloc(); | ||||
| 
 | ||||
| void bt_hid_tiktok_free(BtHidTikTok* bt_hid_tiktok); | ||||
| 
 | ||||
| View* bt_hid_tiktok_get_view(BtHidTikTok* bt_hid_tiktok); | ||||
| 
 | ||||
| void bt_hid_tiktok_set_connected_status(BtHidTikTok* bt_hid_tiktok, bool connected); | ||||
 gornekich
						gornekich