Просмотр исходного кода

[FL-2119] BT HID App (#888)

* view_dispatcher: add default back processing for Long events
* assets: add ble connected and disconnected assets
* bt keyboard: introduce new application
* bt keyboard: add logic to keyboard mode
* bt: remove debug ble hid application
* bt hid: introduce media controller
* gui canvas: rename CanvasFontDirection -> CanvasDirection
* gui canvas: add arrow element
* assets: update finilized assets
* bt hid: finalise keynote GUI
* bt hid: finalise media player GUI
* bt: add media key buttons support
* bt: add exit confirm view
* bt: change Clicker -> Remote
* bt: support f6 target
* bt: hopefully final bt hid design
* bt hid: add blue led notification when device is connected
* bt: leave only bt clicker for now
* bt: add display notification on pin code show event
gornekich 4 лет назад
Родитель
Сommit
f0d4584b40
36 измененных файлов с 964 добавлено и 260 удалено
  1. 4 5
      applications/applications.c
  2. 4 4
      applications/applications.mk
  3. 169 0
      applications/bt/bt_hid_app/bt_hid.c
  4. 34 0
      applications/bt/bt_hid_app/bt_hid.h
  5. 192 0
      applications/bt/bt_hid_app/views/bt_hid_keynote.c
  6. 13 0
      applications/bt/bt_hid_app/views/bt_hid_keynote.h
  7. 193 0
      applications/bt/bt_hid_app/views/bt_hid_media.c
  8. 13 0
      applications/bt/bt_hid_app/views/bt_hid_media.h
  9. 9 0
      applications/bt/bt_service/bt.c
  10. 17 2
      applications/bt/bt_service/bt.h
  11. 7 0
      applications/bt/bt_service/bt_api.c
  12. 4 6
      applications/bt/bt_service/bt_i.h
  13. 0 138
      applications/debug_tools/ble_keyboard/ble_keyboard.c
  14. 29 2
      applications/gui/canvas.c
  15. 23 6
      applications/gui/canvas.h
  16. 1 1
      applications/gui/view_dispatcher.c
  17. 0 0
      assets/compiled/assets_icons.c
  18. 84 75
      assets/compiled/assets_icons.h
  19. BIN
      assets/icons/BLE/BLE_HID/Ble_connected_38x34.png
  20. BIN
      assets/icons/BLE/BLE_HID/Ble_disconnected_24x34.png
  21. BIN
      assets/icons/BLE/BLE_HID/Button_18x18.png
  22. BIN
      assets/icons/BLE/BLE_HID/Circles_47x47.png
  23. BIN
      assets/icons/BLE/BLE_HID/Ok_btn_9x9.png
  24. BIN
      assets/icons/BLE/BLE_HID/Pressed_Button_13x13.png
  25. BIN
      assets/icons/BLE/BLE_HID/Space_65x18.png
  26. BIN
      assets/icons/BLE/BLE_HID/Voldwn_6x6.png
  27. BIN
      assets/icons/BLE/BLE_HID/Volup_8x6.png
  28. 6 2
      firmware/targets/f6/ble-glue/gap.c
  29. 2 2
      firmware/targets/f6/ble-glue/hid_service.h
  30. 57 2
      firmware/targets/f6/furi-hal/furi-hal-bt-hid.c
  31. 3 3
      firmware/targets/f6/furi-hal/furi-hal-bt.c
  32. 6 2
      firmware/targets/f7/ble-glue/gap.c
  33. 2 2
      firmware/targets/f7/ble-glue/hid_service.h
  34. 57 2
      firmware/targets/f7/furi-hal/furi-hal-bt-hid.c
  35. 3 3
      firmware/targets/f7/furi-hal/furi-hal-bt.c
  36. 32 3
      firmware/targets/furi-hal-include/furi-hal-bt-hid.h

+ 4 - 5
applications/applications.c

@@ -40,7 +40,7 @@ extern int32_t subghz_app(void* p);
 extern int32_t usb_mouse_app(void* p);
 extern int32_t usb_mouse_app(void* p);
 extern int32_t usb_test_app(void* p);
 extern int32_t usb_test_app(void* p);
 extern int32_t vibro_test_app(void* p);
 extern int32_t vibro_test_app(void* p);
-extern int32_t ble_keyboard_app(void* p);
+extern int32_t bt_hid_app(void* p);
 
 
 // Plugins
 // Plugins
 extern int32_t music_player_app(void* p);
 extern int32_t music_player_app(void* p);
@@ -206,6 +206,9 @@ const size_t FLIPPER_ON_SYSTEM_START_COUNT =
 
 
 // Plugin menu
 // Plugin menu
 const FlipperApplication FLIPPER_PLUGINS[] = {
 const FlipperApplication FLIPPER_PLUGINS[] = {
+#ifdef APP_BLE_HID
+    {.app = bt_hid_app, .name = "Bluetooth remote", .stack_size = 1024, .icon = NULL},
+#endif
 
 
 #ifdef APP_MUSIC_PLAYER
 #ifdef APP_MUSIC_PLAYER
     {.app = music_player_app, .name = "Music Player", .stack_size = 1024, .icon = &A_Plugins_14},
     {.app = music_player_app, .name = "Music Player", .stack_size = 1024, .icon = &A_Plugins_14},
@@ -220,10 +223,6 @@ const size_t FLIPPER_PLUGINS_COUNT = sizeof(FLIPPER_PLUGINS) / sizeof(FlipperApp
 
 
 // Plugin menu
 // Plugin menu
 const FlipperApplication FLIPPER_DEBUG_APPS[] = {
 const FlipperApplication FLIPPER_DEBUG_APPS[] = {
-#ifdef APP_BLE_KEYBOARD
-    {.app = ble_keyboard_app, .name = "BLE keyboard demo", .stack_size = 1024, .icon = NULL},
-#endif
-
 #ifdef APP_BLINK
 #ifdef APP_BLINK
     {.app = blink_test_app, .name = "Blink Test", .stack_size = 1024, .icon = NULL},
     {.app = blink_test_app, .name = "Blink Test", .stack_size = 1024, .icon = NULL},
 #endif
 #endif

+ 4 - 4
applications/applications.mk

@@ -47,7 +47,7 @@ APP_SD_TEST	= 1
 APP_VIBRO_TEST = 1
 APP_VIBRO_TEST = 1
 APP_USB_TEST = 1
 APP_USB_TEST = 1
 APP_DISPLAY_TEST = 1
 APP_DISPLAY_TEST = 1
-APP_BLE_KEYBOARD = 1
+APP_BLE_HID = 1
 APP_USB_MOUSE = 1
 APP_USB_MOUSE = 1
 APP_BAD_USB = 1
 APP_BAD_USB = 1
 APP_UART_ECHO = 1
 APP_UART_ECHO = 1
@@ -167,9 +167,9 @@ CFLAGS		+= -DAPP_BAD_USB
 SRV_GUI = 1
 SRV_GUI = 1
 endif 
 endif 
 
 
-APP_BLE_KEYBOARD ?=0
-ifeq ($(APP_BLE_KEYBOARD), 1)
-CFLAGS		+= -DAPP_BLE_KEYBOARD
+APP_BLE_HID ?=0
+ifeq ($(APP_BLE_HID), 1)
+CFLAGS		+= -DAPP_BLE_HID
 SRV_GUI = 1
 SRV_GUI = 1
 endif
 endif
 
 

+ 169 - 0
applications/bt/bt_hid_app/bt_hid.c

@@ -0,0 +1,169 @@
+#include "bt_hid.h"
+#include <furi-hal-bt.h>
+#include <applications/notification/notification-messages.h>
+
+#define TAG "BtHidApp"
+
+enum BtDebugSubmenuIndex {
+    BtHidSubmenuIndexKeynote,
+    BtHidSubmenuIndexMedia,
+};
+
+void bt_hid_submenu_callback(void* context, uint32_t index) {
+    furi_assert(context);
+    BtHid* app = context;
+    if(index == BtHidSubmenuIndexKeynote) {
+        app->view_id = BtHidViewKeynote;
+        view_dispatcher_switch_to_view(app->view_dispatcher, BtHidViewKeynote);
+    } else if(index == BtHidSubmenuIndexMedia) {
+        app->view_id = BtHidViewMedia;
+        view_dispatcher_switch_to_view(app->view_dispatcher, BtHidViewMedia);
+    }
+}
+
+void bt_hid_dialog_callback(DialogExResult result, void* context) {
+    furi_assert(context);
+    BtHid* app = context;
+    if(result == DialogExResultLeft) {
+        // TODO switch to Submenu after Media is done
+        view_dispatcher_stop(app->view_dispatcher);
+    } else if(result == DialogExResultRight) {
+        view_dispatcher_switch_to_view(app->view_dispatcher, app->view_id);
+    }
+}
+
+uint32_t bt_hid_exit_confirm_view(void* context) {
+    return BtHidViewExitConfirm;
+}
+
+uint32_t bt_hid_exit(void* context) {
+    return VIEW_NONE;
+}
+
+void bt_hid_connection_status_changed_callback(BtStatus status, void* context) {
+    furi_assert(context);
+    BtHid* bt_hid = context;
+    bool connected = (status == BtStatusConnected);
+    if(connected) {
+        notification_internal_message(bt_hid->notifications, &sequence_set_blue_255);
+    } else {
+        notification_internal_message(bt_hid->notifications, &sequence_reset_blue);
+    }
+    bt_hid_keynote_set_connected_status(bt_hid->bt_hid_keynote, connected);
+    bt_hid_media_set_connected_status(bt_hid->bt_hid_media, connected);
+}
+
+BtHid* bt_hid_app_alloc() {
+    BtHid* app = furi_alloc(sizeof(BtHid));
+
+    // Load Bluetooth settings
+    bt_settings_load(&app->bt_settings);
+
+    // Gui
+    app->gui = furi_record_open("gui");
+
+    // Bt
+    app->bt = furi_record_open("bt");
+
+    // Notifications
+    app->notifications = furi_record_open("notification");
+
+    // View dispatcher
+    app->view_dispatcher = view_dispatcher_alloc();
+    view_dispatcher_enable_queue(app->view_dispatcher);
+    view_dispatcher_attach_to_gui(app->view_dispatcher, app->gui, ViewDispatcherTypeFullscreen);
+
+    // Submenu view
+    app->submenu = submenu_alloc();
+    submenu_add_item(
+        app->submenu, "Keynote", BtHidSubmenuIndexKeynote, bt_hid_submenu_callback, app);
+    submenu_add_item(
+        app->submenu, "Media player", BtHidSubmenuIndexMedia, bt_hid_submenu_callback, app);
+    view_set_previous_callback(submenu_get_view(app->submenu), bt_hid_exit);
+    view_dispatcher_add_view(
+        app->view_dispatcher, BtHidViewSubmenu, submenu_get_view(app->submenu));
+
+    // Dialog view
+    app->dialog = dialog_ex_alloc();
+    dialog_ex_set_result_callback(app->dialog, bt_hid_dialog_callback);
+    dialog_ex_set_context(app->dialog, app);
+    dialog_ex_set_left_button_text(app->dialog, "Exit");
+    dialog_ex_set_right_button_text(app->dialog, "Stay");
+    dialog_ex_set_header(app->dialog, "Close current app?", 16, 12, AlignLeft, AlignTop);
+    view_dispatcher_add_view(
+        app->view_dispatcher, BtHidViewExitConfirm, dialog_ex_get_view(app->dialog));
+
+    // Keynote view
+    app->bt_hid_keynote = bt_hid_keynote_alloc();
+    view_set_previous_callback(
+        bt_hid_keynote_get_view(app->bt_hid_keynote), bt_hid_exit_confirm_view);
+    view_dispatcher_add_view(
+        app->view_dispatcher, BtHidViewKeynote, bt_hid_keynote_get_view(app->bt_hid_keynote));
+
+    // Media view
+    app->bt_hid_media = bt_hid_media_alloc();
+    view_set_previous_callback(bt_hid_media_get_view(app->bt_hid_media), bt_hid_exit_confirm_view);
+    view_dispatcher_add_view(
+        app->view_dispatcher, BtHidViewMedia, bt_hid_media_get_view(app->bt_hid_media));
+
+    // TODO switch to menu after Media is done
+    view_dispatcher_switch_to_view(app->view_dispatcher, BtHidViewKeynote);
+
+    return app;
+}
+
+void bt_hid_app_free(BtHid* app) {
+    furi_assert(app);
+
+    // Reset notification
+    notification_internal_message(app->notifications, &sequence_reset_blue);
+
+    // Free views
+    view_dispatcher_remove_view(app->view_dispatcher, BtHidViewSubmenu);
+    submenu_free(app->submenu);
+    view_dispatcher_remove_view(app->view_dispatcher, BtHidViewExitConfirm);
+    dialog_ex_free(app->dialog);
+    view_dispatcher_remove_view(app->view_dispatcher, BtHidViewKeynote);
+    bt_hid_keynote_free(app->bt_hid_keynote);
+    view_dispatcher_remove_view(app->view_dispatcher, BtHidViewMedia);
+    bt_hid_media_free(app->bt_hid_media);
+    view_dispatcher_free(app->view_dispatcher);
+
+    // Close records
+    furi_record_close("gui");
+    app->gui = NULL;
+    furi_record_close("notification");
+    app->notifications = NULL;
+    furi_record_close("bt");
+    app->bt = NULL;
+
+    // Free rest
+    free(app);
+}
+
+int32_t bt_hid_app(void* p) {
+    // Switch profile to Hid
+    BtHid* app = bt_hid_app_alloc();
+    bt_set_status_changed_callback(app->bt, bt_hid_connection_status_changed_callback, app);
+    // Change profile
+    if(!bt_set_profile(app->bt, BtProfileHidKeyboard)) {
+        FURI_LOG_E(TAG, "Failed to switch profile");
+        bt_hid_app_free(app);
+        return -1;
+    }
+    furi_hal_bt_start_advertising();
+
+    view_dispatcher_run(app->view_dispatcher);
+
+    bt_set_status_changed_callback(app->bt, NULL, NULL);
+    // Stop advertising if bt was off
+    if(app->bt_settings.enabled) {
+        furi_hal_bt_stop_advertising();
+    }
+    // Change back profile to Serial
+    bt_set_profile(app->bt, BtProfileSerial);
+
+    bt_hid_app_free(app);
+
+    return 0;
+}

+ 34 - 0
applications/bt/bt_hid_app/bt_hid.h

@@ -0,0 +1,34 @@
+#pragma once
+
+#include <furi.h>
+#include <bt/bt_service/bt.h>
+#include <gui/gui.h>
+#include <gui/view.h>
+#include <gui/view_dispatcher.h>
+#include <applications/notification/notification.h>
+#include <applications/bt/bt_settings.h>
+
+#include <gui/modules/submenu.h>
+#include <gui/modules/dialog_ex.h>
+#include "views/bt_hid_keynote.h"
+#include "views/bt_hid_media.h"
+
+typedef struct {
+    BtSettings bt_settings;
+    Bt* bt;
+    Gui* gui;
+    NotificationApp* notifications;
+    ViewDispatcher* view_dispatcher;
+    Submenu* submenu;
+    DialogEx* dialog;
+    BtHidKeynote* bt_hid_keynote;
+    BtHidMedia* bt_hid_media;
+    uint32_t view_id;
+} BtHid;
+
+typedef enum {
+    BtHidViewSubmenu,
+    BtHidViewKeynote,
+    BtHidViewMedia,
+    BtHidViewExitConfirm,
+} BtHidView;

+ 192 - 0
applications/bt/bt_hid_app/views/bt_hid_keynote.c

@@ -0,0 +1,192 @@
+#include "bt_hid_keynote.h"
+#include <furi.h>
+#include <furi-hal-bt-hid.h>
+#include <furi-hal-usb-hid.h>
+#include <gui/elements.h>
+
+struct BtHidKeynote {
+    View* view;
+};
+
+typedef struct {
+    bool left_pressed;
+    bool up_pressed;
+    bool right_pressed;
+    bool down_pressed;
+    bool ok_pressed;
+    bool connected;
+} BtHidKeynoteModel;
+
+static void bt_hid_keynote_draw_arrow(Canvas* canvas, uint8_t x, uint8_t y, CanvasDirection dir) {
+    canvas_draw_triangle(canvas, x, y, 5, 3, dir);
+    if(dir == CanvasDirectionBottomToTop) {
+        canvas_draw_line(canvas, x, y + 6, x, y - 1);
+    } else if(dir == CanvasDirectionTopToBottom) {
+        canvas_draw_line(canvas, x, y - 6, x, y + 1);
+    } else if(dir == CanvasDirectionRightToLeft) {
+        canvas_draw_line(canvas, x + 6, y, x - 1, y);
+    } else if(dir == CanvasDirectionLeftToRight) {
+        canvas_draw_line(canvas, x - 6, y, x + 1, y);
+    }
+}
+
+static void bt_hid_keynote_draw_callback(Canvas* canvas, void* context) {
+    furi_assert(context);
+    BtHidKeynoteModel* model = context;
+
+    // Header
+    canvas_set_font(canvas, FontPrimary);
+    elements_multiline_text_aligned(canvas, 9, 3, AlignLeft, AlignTop, "Keynote");
+    canvas_set_font(canvas, FontSecondary);
+
+    // Connected status
+    if(model->connected) {
+        canvas_draw_icon(canvas, 18, 18, &I_Ble_connected_38x34);
+        elements_multiline_text_aligned(canvas, 9, 60, AlignLeft, AlignBottom, "Connected");
+    } else {
+        canvas_draw_icon(canvas, 18, 18, &I_Ble_disconnected_24x34);
+        elements_multiline_text_aligned(canvas, 3, 60, AlignLeft, AlignBottom, "Disconnected");
+    }
+
+    // Up
+    canvas_draw_icon(canvas, 86, 4, &I_Button_18x18);
+    if(model->up_pressed) {
+        elements_slightly_rounded_box(canvas, 89, 6, 13, 13);
+        canvas_set_color(canvas, ColorWhite);
+    }
+    bt_hid_keynote_draw_arrow(canvas, 95, 10, CanvasDirectionBottomToTop);
+    canvas_set_color(canvas, ColorBlack);
+
+    // Down
+    canvas_draw_icon(canvas, 86, 25, &I_Button_18x18);
+    if(model->down_pressed) {
+        elements_slightly_rounded_box(canvas, 89, 27, 13, 13);
+        canvas_set_color(canvas, ColorWhite);
+    }
+    bt_hid_keynote_draw_arrow(canvas, 95, 35, CanvasDirectionTopToBottom);
+    canvas_set_color(canvas, ColorBlack);
+
+    // Left
+    canvas_draw_icon(canvas, 65, 25, &I_Button_18x18);
+    if(model->left_pressed) {
+        elements_slightly_rounded_box(canvas, 68, 27, 13, 13);
+        canvas_set_color(canvas, ColorWhite);
+    }
+    bt_hid_keynote_draw_arrow(canvas, 72, 33, CanvasDirectionRightToLeft);
+    canvas_set_color(canvas, ColorBlack);
+
+    // Right
+    canvas_draw_icon(canvas, 107, 25, &I_Button_18x18);
+    if(model->right_pressed) {
+        elements_slightly_rounded_box(canvas, 110, 27, 13, 13);
+        canvas_set_color(canvas, ColorWhite);
+    }
+    bt_hid_keynote_draw_arrow(canvas, 118, 33, CanvasDirectionLeftToRight);
+    canvas_set_color(canvas, ColorBlack);
+
+    // Ok
+    canvas_draw_icon(canvas, 63, 45, &I_Space_65x18);
+    if(model->ok_pressed) {
+        elements_slightly_rounded_box(canvas, 66, 47, 60, 13);
+        canvas_set_color(canvas, ColorWhite);
+    }
+    canvas_draw_icon(canvas, 74, 49, &I_Ok_btn_9x9);
+    elements_multiline_text_aligned(canvas, 91, 56, AlignLeft, AlignBottom, "Space");
+}
+
+static void bt_hid_keynote_process_press(BtHidKeynote* bt_hid_keynote, InputEvent* event) {
+    with_view_model(
+        bt_hid_keynote->view, (BtHidKeynoteModel * model) {
+            if(event->key == InputKeyUp) {
+                model->up_pressed = true;
+                furi_hal_bt_hid_kb_press(KEY_UP_ARROW);
+            } else if(event->key == InputKeyDown) {
+                model->down_pressed = true;
+                furi_hal_bt_hid_kb_press(KEY_DOWN_ARROW);
+            } else if(event->key == InputKeyLeft) {
+                model->left_pressed = true;
+                furi_hal_bt_hid_kb_press(KEY_LEFT_ARROW);
+            } else if(event->key == InputKeyRight) {
+                model->right_pressed = true;
+                furi_hal_bt_hid_kb_press(KEY_RIGHT_ARROW);
+            } else if(event->key == InputKeyOk) {
+                model->ok_pressed = true;
+                furi_hal_bt_hid_kb_press(KEY_SPACE);
+            }
+            return true;
+        });
+}
+
+static void bt_hid_keynote_process_release(BtHidKeynote* bt_hid_keynote, InputEvent* event) {
+    with_view_model(
+        bt_hid_keynote->view, (BtHidKeynoteModel * model) {
+            if(event->key == InputKeyUp) {
+                model->up_pressed = false;
+                furi_hal_bt_hid_kb_release(KEY_UP_ARROW);
+            } else if(event->key == InputKeyDown) {
+                model->down_pressed = false;
+                furi_hal_bt_hid_kb_release(KEY_DOWN_ARROW);
+            } else if(event->key == InputKeyLeft) {
+                model->left_pressed = false;
+                furi_hal_bt_hid_kb_release(KEY_LEFT_ARROW);
+            } else if(event->key == InputKeyRight) {
+                model->right_pressed = false;
+                furi_hal_bt_hid_kb_release(KEY_RIGHT_ARROW);
+            } else if(event->key == InputKeyOk) {
+                model->ok_pressed = false;
+                furi_hal_bt_hid_kb_release(KEY_SPACE);
+            }
+            return true;
+        });
+}
+
+static bool bt_hid_keynote_input_callback(InputEvent* event, void* context) {
+    furi_assert(context);
+    BtHidKeynote* bt_hid_keynote = context;
+    bool consumed = false;
+
+    if(event->type == InputTypePress) {
+        bt_hid_keynote_process_press(bt_hid_keynote, event);
+        consumed = true;
+    } else if(event->type == InputTypeRelease) {
+        bt_hid_keynote_process_release(bt_hid_keynote, event);
+        consumed = true;
+    } else if(event->type == InputTypeShort) {
+        if(event->key == InputKeyBack) {
+            furi_hal_hid_kb_release_all();
+        }
+    }
+
+    return consumed;
+}
+
+BtHidKeynote* bt_hid_keynote_alloc() {
+    BtHidKeynote* bt_hid_keynote = furi_alloc(sizeof(BtHidKeynote));
+    bt_hid_keynote->view = view_alloc();
+    view_set_context(bt_hid_keynote->view, bt_hid_keynote);
+    view_allocate_model(bt_hid_keynote->view, ViewModelTypeLocking, sizeof(BtHidKeynoteModel));
+    view_set_draw_callback(bt_hid_keynote->view, bt_hid_keynote_draw_callback);
+    view_set_input_callback(bt_hid_keynote->view, bt_hid_keynote_input_callback);
+
+    return bt_hid_keynote;
+}
+
+void bt_hid_keynote_free(BtHidKeynote* bt_hid_keynote) {
+    furi_assert(bt_hid_keynote);
+    view_free(bt_hid_keynote->view);
+    free(bt_hid_keynote);
+}
+
+View* bt_hid_keynote_get_view(BtHidKeynote* bt_hid_keynote) {
+    furi_assert(bt_hid_keynote);
+    return bt_hid_keynote->view;
+}
+
+void bt_hid_keynote_set_connected_status(BtHidKeynote* bt_hid_keynote, bool connected) {
+    furi_assert(bt_hid_keynote);
+    with_view_model(
+        bt_hid_keynote->view, (BtHidKeynoteModel * model) {
+            model->connected = connected;
+            return true;
+        });
+}

+ 13 - 0
applications/bt/bt_hid_app/views/bt_hid_keynote.h

@@ -0,0 +1,13 @@
+#pragma once
+
+#include <gui/view.h>
+
+typedef struct BtHidKeynote BtHidKeynote;
+
+BtHidKeynote* bt_hid_keynote_alloc();
+
+void bt_hid_keynote_free(BtHidKeynote* bt_hid_keynote);
+
+View* bt_hid_keynote_get_view(BtHidKeynote* bt_hid_keynote);
+
+void bt_hid_keynote_set_connected_status(BtHidKeynote* bt_hid_keynote, bool connected);

+ 193 - 0
applications/bt/bt_hid_app/views/bt_hid_media.c

@@ -0,0 +1,193 @@
+#include "bt_hid_media.h"
+#include <furi.h>
+#include <furi-hal-bt-hid.h>
+#include <furi-hal-usb-hid.h>
+#include <gui/elements.h>
+
+struct BtHidMedia {
+    View* view;
+};
+
+typedef struct {
+    bool left_pressed;
+    bool up_pressed;
+    bool right_pressed;
+    bool down_pressed;
+    bool ok_pressed;
+    bool connected;
+} BtHidMediaModel;
+
+static void bt_hid_media_draw_arrow(Canvas* canvas, uint8_t x, uint8_t y, CanvasDirection dir) {
+    canvas_draw_triangle(canvas, x, y, 5, 3, dir);
+    if(dir == CanvasDirectionBottomToTop) {
+        canvas_draw_dot(canvas, x, y - 1);
+    } else if(dir == CanvasDirectionTopToBottom) {
+        canvas_draw_dot(canvas, x, y + 1);
+    } else if(dir == CanvasDirectionRightToLeft) {
+        canvas_draw_dot(canvas, x - 1, y);
+    } else if(dir == CanvasDirectionLeftToRight) {
+        canvas_draw_dot(canvas, x + 1, y);
+    }
+}
+
+static void bt_hid_media_draw_callback(Canvas* canvas, void* context) {
+    furi_assert(context);
+    BtHidMediaModel* model = context;
+
+    // Header
+    canvas_set_font(canvas, FontPrimary);
+    elements_multiline_text_aligned(canvas, 9, 3, AlignLeft, AlignTop, "Media player");
+    canvas_set_font(canvas, FontSecondary);
+
+    // Connected status
+    if(model->connected) {
+        canvas_draw_icon(canvas, 23, 17, &I_Ble_connected_38x34);
+        elements_multiline_text_aligned(canvas, 35, 61, AlignCenter, AlignBottom, "Connected");
+    } else {
+        canvas_draw_icon(canvas, 23, 17, &I_Ble_disconnected_24x34);
+        elements_multiline_text_aligned(canvas, 35, 61, AlignCenter, AlignBottom, "Disconnected");
+    }
+
+    // Keypad circles
+    canvas_draw_icon(canvas, 76, 8, &I_Circles_47x47);
+
+    // Up
+    if(model->up_pressed) {
+        canvas_draw_icon(canvas, 93, 9, &I_Pressed_Button_13x13);
+        canvas_set_color(canvas, ColorWhite);
+    }
+    canvas_draw_icon(canvas, 96, 12, &I_Volup_8x6);
+    canvas_set_color(canvas, ColorBlack);
+
+    // Down
+    if(model->down_pressed) {
+        canvas_draw_icon(canvas, 93, 41, &I_Pressed_Button_13x13);
+        canvas_set_color(canvas, ColorWhite);
+    }
+    canvas_draw_icon(canvas, 96, 45, &I_Voldwn_6x6);
+    canvas_set_color(canvas, ColorBlack);
+
+    // Left
+    if(model->left_pressed) {
+        canvas_draw_icon(canvas, 77, 25, &I_Pressed_Button_13x13);
+        canvas_set_color(canvas, ColorWhite);
+    }
+    bt_hid_media_draw_arrow(canvas, 82, 31, CanvasDirectionRightToLeft);
+    bt_hid_media_draw_arrow(canvas, 86, 31, CanvasDirectionRightToLeft);
+    canvas_set_color(canvas, ColorBlack);
+
+    // Right
+    if(model->right_pressed) {
+        canvas_draw_icon(canvas, 109, 25, &I_Pressed_Button_13x13);
+        canvas_set_color(canvas, ColorWhite);
+    }
+    bt_hid_media_draw_arrow(canvas, 112, 31, CanvasDirectionLeftToRight);
+    bt_hid_media_draw_arrow(canvas, 116, 31, CanvasDirectionLeftToRight);
+    canvas_set_color(canvas, ColorBlack);
+
+    // Ok
+    if(model->ok_pressed) {
+        canvas_draw_icon(canvas, 93, 25, &I_Pressed_Button_13x13);
+        canvas_set_color(canvas, ColorWhite);
+    }
+    bt_hid_media_draw_arrow(canvas, 96, 31, CanvasDirectionLeftToRight);
+    canvas_draw_line(canvas, 100, 29, 100, 33);
+    canvas_draw_line(canvas, 102, 29, 102, 33);
+}
+
+static void bt_hid_media_process_press(BtHidMedia* bt_hid_media, InputEvent* event) {
+    with_view_model(
+        bt_hid_media->view, (BtHidMediaModel * model) {
+            if(event->key == InputKeyUp) {
+                model->up_pressed = true;
+                furi_hal_bt_hid_media_press(FuriHalBtHidMediaVolumeUp);
+            } else if(event->key == InputKeyDown) {
+                model->down_pressed = true;
+                furi_hal_bt_hid_media_press(FuriHalBtHidMediaVolumeDown);
+            } else if(event->key == InputKeyLeft) {
+                model->left_pressed = true;
+                furi_hal_bt_hid_media_press(FuriHalBtHidMediaScanPrevious);
+            } else if(event->key == InputKeyRight) {
+                model->right_pressed = true;
+                furi_hal_bt_hid_media_press(FuriHalBtHidMediaScanNext);
+            } else if(event->key == InputKeyOk) {
+                model->ok_pressed = true;
+                furi_hal_bt_hid_media_press(FuriHalBtHidMediaPlayPause);
+            }
+            return true;
+        });
+}
+
+static void bt_hid_media_process_release(BtHidMedia* bt_hid_media, InputEvent* event) {
+    with_view_model(
+        bt_hid_media->view, (BtHidMediaModel * model) {
+            if(event->key == InputKeyUp) {
+                model->up_pressed = false;
+                furi_hal_bt_hid_media_release(FuriHalBtHidMediaVolumeUp);
+            } else if(event->key == InputKeyDown) {
+                model->down_pressed = false;
+                furi_hal_bt_hid_media_release(FuriHalBtHidMediaVolumeDown);
+            } else if(event->key == InputKeyLeft) {
+                model->left_pressed = false;
+                furi_hal_bt_hid_media_release(FuriHalBtHidMediaScanPrevious);
+            } else if(event->key == InputKeyRight) {
+                model->right_pressed = false;
+                furi_hal_bt_hid_media_release(FuriHalBtHidMediaScanNext);
+            } else if(event->key == InputKeyOk) {
+                model->ok_pressed = false;
+                furi_hal_bt_hid_media_release(FuriHalBtHidMediaPlayPause);
+            }
+            return true;
+        });
+}
+
+static bool bt_hid_media_input_callback(InputEvent* event, void* context) {
+    furi_assert(context);
+    BtHidMedia* bt_hid_media = context;
+    bool consumed = false;
+
+    if(event->type == InputTypePress) {
+        bt_hid_media_process_press(bt_hid_media, event);
+        consumed = true;
+    } else if(event->type == InputTypeRelease) {
+        bt_hid_media_process_release(bt_hid_media, event);
+        consumed = true;
+    } else if(event->type == InputTypeShort) {
+        if(event->key == InputKeyBack) {
+            furi_hal_bt_hid_media_release_all();
+        }
+    }
+
+    return consumed;
+}
+
+BtHidMedia* bt_hid_media_alloc() {
+    BtHidMedia* bt_hid_media = furi_alloc(sizeof(BtHidMedia));
+    bt_hid_media->view = view_alloc();
+    view_set_context(bt_hid_media->view, bt_hid_media);
+    view_allocate_model(bt_hid_media->view, ViewModelTypeLocking, sizeof(BtHidMediaModel));
+    view_set_draw_callback(bt_hid_media->view, bt_hid_media_draw_callback);
+    view_set_input_callback(bt_hid_media->view, bt_hid_media_input_callback);
+
+    return bt_hid_media;
+}
+
+void bt_hid_media_free(BtHidMedia* bt_hid_media) {
+    furi_assert(bt_hid_media);
+    view_free(bt_hid_media->view);
+    free(bt_hid_media);
+}
+
+View* bt_hid_media_get_view(BtHidMedia* bt_hid_media) {
+    furi_assert(bt_hid_media);
+    return bt_hid_media->view;
+}
+
+void bt_hid_media_set_connected_status(BtHidMedia* bt_hid_media, bool connected) {
+    furi_assert(bt_hid_media);
+    with_view_model(
+        bt_hid_media->view, (BtHidMediaModel * model) {
+            model->connected = connected;
+            return true;
+        });
+}

+ 13 - 0
applications/bt/bt_hid_app/views/bt_hid_media.h

@@ -0,0 +1,13 @@
+#pragma once
+
+#include <gui/view.h>
+
+typedef struct BtHidMedia BtHidMedia;
+
+BtHidMedia* bt_hid_media_alloc();
+
+void bt_hid_media_free(BtHidMedia* bt_hid_media);
+
+View* bt_hid_media_get_view(BtHidMedia* bt_hid_media);
+
+void bt_hid_media_set_connected_status(BtHidMedia* bt_hid_media, bool connected);

+ 9 - 0
applications/bt/bt_service/bt.c

@@ -2,6 +2,8 @@
 #include "battery_service.h"
 #include "battery_service.h"
 #include "bt_keys_storage.h"
 #include "bt_keys_storage.h"
 
 
+#include <applications/notification/notification-messages.h>
+
 #define TAG "BtSrv"
 #define TAG "BtSrv"
 
 
 #define BT_RPC_EVENT_BUFF_SENT (1UL << 0)
 #define BT_RPC_EVENT_BUFF_SENT (1UL << 0)
@@ -29,6 +31,7 @@ static ViewPort* bt_statusbar_view_port_alloc(Bt* bt) {
 
 
 static void bt_pin_code_show_event_handler(Bt* bt, uint32_t pin) {
 static void bt_pin_code_show_event_handler(Bt* bt, uint32_t pin) {
     furi_assert(bt);
     furi_assert(bt);
+    notification_message(bt->notification, &sequence_display_on);
     string_t pin_str;
     string_t pin_str;
     dialog_message_set_icon(bt->dialog_message, &I_BLE_Pairing_128x64, 0, 0);
     dialog_message_set_icon(bt->dialog_message, &I_BLE_Pairing_128x64, 0, 0);
     string_init_printf(pin_str, "Pairing code\n%06d", pin);
     string_init_printf(pin_str, "Pairing code\n%06d", pin);
@@ -41,6 +44,7 @@ static void bt_pin_code_show_event_handler(Bt* bt, uint32_t pin) {
 
 
 static bool bt_pin_code_verify_event_handler(Bt* bt, uint32_t pin) {
 static bool bt_pin_code_verify_event_handler(Bt* bt, uint32_t pin) {
     furi_assert(bt);
     furi_assert(bt);
+    notification_message(bt->notification, &sequence_display_on);
     string_t pin_str;
     string_t pin_str;
     dialog_message_set_icon(bt->dialog_message, &I_BLE_Pairing_128x64, 0, 0);
     dialog_message_set_icon(bt->dialog_message, &I_BLE_Pairing_128x64, 0, 0);
     string_init_printf(pin_str, "Verify code\n%06d", pin);
     string_init_printf(pin_str, "Verify code\n%06d", pin);
@@ -80,6 +84,8 @@ Bt* bt_alloc() {
 
 
     // Setup statusbar view port
     // Setup statusbar view port
     bt->statusbar_view_port = bt_statusbar_view_port_alloc(bt);
     bt->statusbar_view_port = bt_statusbar_view_port_alloc(bt);
+    // Notification
+    bt->notification = furi_record_open("notification");
     // Gui
     // Gui
     bt->gui = furi_record_open("gui");
     bt->gui = furi_record_open("gui");
     gui_add_view_port(bt->gui, bt->statusbar_view_port, GuiLayerStatusBarLeft);
     gui_add_view_port(bt->gui, bt->statusbar_view_port, GuiLayerStatusBarLeft);
@@ -288,6 +294,9 @@ int32_t bt_srv() {
         if(message.type == BtMessageTypeUpdateStatusbar) {
         if(message.type == BtMessageTypeUpdateStatusbar) {
             // Update statusbar
             // Update statusbar
             bt_statusbar_update(bt);
             bt_statusbar_update(bt);
+            if(bt->status_changed_cb) {
+                bt->status_changed_cb(bt->status, bt->status_changed_ctx);
+            }
         } else if(message.type == BtMessageTypeUpdateBatteryLevel) {
         } else if(message.type == BtMessageTypeUpdateBatteryLevel) {
             // Update battery level
             // Update battery level
             furi_hal_bt_update_battery_level(message.data.battery_level);
             furi_hal_bt_update_battery_level(message.data.battery_level);

+ 17 - 2
applications/bt/bt_service/bt.h

@@ -9,13 +9,20 @@ extern "C" {
 
 
 typedef struct Bt Bt;
 typedef struct Bt Bt;
 
 
+typedef enum {
+    BtStatusOff,
+    BtStatusAdvertising,
+    BtStatusConnected,
+} BtStatus;
+
 typedef enum {
 typedef enum {
     BtProfileSerial,
     BtProfileSerial,
     BtProfileHidKeyboard,
     BtProfileHidKeyboard,
 } BtProfile;
 } BtProfile;
 
 
-/**
- * Change BLE Profile
+typedef void (*BtStatusChangedCallback)(BtStatus status, void* context);
+
+/** Change BLE Profile
  * @note Call of this function leads to 2nd core restart
  * @note Call of this function leads to 2nd core restart
  *
  *
  * @param bt        Bt instance
  * @param bt        Bt instance
@@ -25,6 +32,14 @@ typedef enum {
  */
  */
 bool bt_set_profile(Bt* bt, BtProfile profile);
 bool bt_set_profile(Bt* bt, BtProfile profile);
 
 
+/** Set callback for Bluetooth status change notification
+ *
+ * @param bt        Bt instance
+ * @param callback  BtStatusChangedCallback instance
+ * @param context   pointer to context
+ */
+void bt_set_status_changed_callback(Bt* bt, BtStatusChangedCallback callback, void* context);
+
 #ifdef __cplusplus
 #ifdef __cplusplus
 }
 }
 #endif
 #endif

+ 7 - 0
applications/bt/bt_service/bt_api.c

@@ -13,3 +13,10 @@ bool bt_set_profile(Bt* bt, BtProfile profile) {
 
 
     return result;
     return result;
 }
 }
+
+void bt_set_status_changed_callback(Bt* bt, BtStatusChangedCallback callback, void* context) {
+    furi_assert(bt);
+
+    bt->status_changed_cb = callback;
+    bt->status_changed_ctx = context;
+}

+ 4 - 6
applications/bt/bt_service/bt_i.h

@@ -12,17 +12,12 @@
 #include <dialogs/dialogs.h>
 #include <dialogs/dialogs.h>
 #include <power/power_service/power.h>
 #include <power/power_service/power.h>
 #include <applications/rpc/rpc.h>
 #include <applications/rpc/rpc.h>
+#include <applications/notification/notification.h>
 
 
 #include "../bt_settings.h"
 #include "../bt_settings.h"
 
 
 #define BT_API_UNLOCK_EVENT (1UL << 0)
 #define BT_API_UNLOCK_EVENT (1UL << 0)
 
 
-typedef enum {
-    BtStatusOff,
-    BtStatusAdvertising,
-    BtStatusConnected,
-} BtStatus;
-
 typedef enum {
 typedef enum {
     BtMessageTypeUpdateStatusbar,
     BtMessageTypeUpdateStatusbar,
     BtMessageTypeUpdateBatteryLevel,
     BtMessageTypeUpdateBatteryLevel,
@@ -51,6 +46,7 @@ struct Bt {
     BtStatus status;
     BtStatus status;
     BtProfile profile;
     BtProfile profile;
     osMessageQueueId_t message_queue;
     osMessageQueueId_t message_queue;
+    NotificationApp* notification;
     Gui* gui;
     Gui* gui;
     ViewPort* statusbar_view_port;
     ViewPort* statusbar_view_port;
     DialogsApp* dialogs;
     DialogsApp* dialogs;
@@ -60,4 +56,6 @@ struct Bt {
     RpcSession* rpc_session;
     RpcSession* rpc_session;
     osEventFlagsId_t rpc_event;
     osEventFlagsId_t rpc_event;
     osEventFlagsId_t api_event;
     osEventFlagsId_t api_event;
+    BtStatusChangedCallback status_changed_cb;
+    void* status_changed_ctx;
 };
 };

+ 0 - 138
applications/debug_tools/ble_keyboard/ble_keyboard.c

@@ -1,138 +0,0 @@
-#include <furi.h>
-#include <gui/gui.h>
-#include <input/input.h>
-#include <bt/bt_service/bt.h>
-#include <furi-hal-bt.h>
-#include <furi-hal-bt-hid.h>
-#include <furi-hal-usb-hid.h>
-
-#define TAG "BleKeyboardApp"
-
-typedef enum {
-    EventTypeInput,
-} EventType;
-
-typedef struct {
-    union {
-        InputEvent input;
-    };
-    EventType type;
-} BleKeyboardEvent;
-
-static void ble_keyboard_render_callback(Canvas* canvas, void* ctx) {
-    canvas_clear(canvas);
-
-    canvas_set_font(canvas, FontPrimary);
-    canvas_draw_str(canvas, 0, 10, "BLE keypad demo");
-
-    canvas_set_font(canvas, FontSecondary);
-    canvas_draw_str(canvas, 0, 63, "Hold [back] to exit");
-}
-
-static void ble_keyboard_input_callback(InputEvent* input_event, void* ctx) {
-    osMessageQueueId_t event_queue = ctx;
-
-    BleKeyboardEvent event;
-    event.type = EventTypeInput;
-    event.input = *input_event;
-    osMessageQueuePut(event_queue, &event, 0, osWaitForever);
-}
-
-int32_t ble_keyboard_app(void* p) {
-    Bt* bt = furi_record_open("bt");
-    if(!bt_set_profile(bt, BtProfileHidKeyboard)) {
-        FURI_LOG_E(TAG, "Failed to switch profile");
-        furi_record_close("bt");
-        return -1;
-    }
-    bool bt_turned_on = furi_hal_bt_is_active();
-    if(!bt_turned_on) {
-        furi_hal_bt_start_advertising();
-    }
-
-    osMessageQueueId_t event_queue = osMessageQueueNew(8, sizeof(BleKeyboardEvent), NULL);
-    furi_check(event_queue);
-    ViewPort* view_port = view_port_alloc();
-
-    view_port_draw_callback_set(view_port, ble_keyboard_render_callback, NULL);
-    view_port_input_callback_set(view_port, ble_keyboard_input_callback, event_queue);
-
-    // Open GUI and register view_port
-    Gui* gui = furi_record_open("gui");
-    gui_add_view_port(gui, view_port, GuiLayerFullscreen);
-
-    BleKeyboardEvent event;
-    while(1) {
-        osStatus_t event_status = osMessageQueueGet(event_queue, &event, NULL, osWaitForever);
-
-        if(event_status == osOK) {
-            if(event.type == EventTypeInput) {
-                if(event.input.type == InputTypeLong && event.input.key == InputKeyBack) {
-                    furi_hal_bt_hid_kb_release_all();
-                    break;
-                }
-
-                if(event.input.key == InputKeyBack) {
-                    if(event.input.type == InputTypePress) {
-                        furi_hal_bt_hid_kb_press(KEY_ESC);
-                    } else if(event.input.type == InputTypeRelease) {
-                        furi_hal_bt_hid_kb_release(KEY_ESC);
-                    }
-                }
-
-                if(event.input.key == InputKeyOk) {
-                    if(event.input.type == InputTypePress) {
-                        furi_hal_bt_hid_kb_press(KEY_ENTER);
-                    } else if(event.input.type == InputTypeRelease) {
-                        furi_hal_bt_hid_kb_release(KEY_ENTER);
-                    }
-                }
-
-                if(event.input.key == InputKeyRight) {
-                    if(event.input.type == InputTypePress) {
-                        furi_hal_bt_hid_kb_press(KEY_RIGHT_ARROW);
-                    } else if(event.input.type == InputTypeRelease) {
-                        furi_hal_bt_hid_kb_release(KEY_RIGHT_ARROW);
-                    }
-                }
-
-                if(event.input.key == InputKeyLeft) {
-                    if(event.input.type == InputTypePress) {
-                        furi_hal_bt_hid_kb_press(KEY_LEFT_ARROW);
-                    } else if(event.input.type == InputTypeRelease) {
-                        furi_hal_bt_hid_kb_release(KEY_LEFT_ARROW);
-                    }
-                }
-
-                if(event.input.key == InputKeyDown) {
-                    if(event.input.type == InputTypePress) {
-                        furi_hal_bt_hid_kb_press(KEY_DOWN_ARROW);
-                    } else if(event.input.type == InputTypeRelease) {
-                        furi_hal_bt_hid_kb_release(KEY_DOWN_ARROW);
-                    }
-                }
-
-                if(event.input.key == InputKeyUp) {
-                    if(event.input.type == InputTypePress) {
-                        furi_hal_bt_hid_kb_press(KEY_UP_ARROW);
-                    } else if(event.input.type == InputTypeRelease) {
-                        furi_hal_bt_hid_kb_release(KEY_UP_ARROW);
-                    }
-                }
-            }
-        }
-        view_port_update(view_port);
-    }
-
-    if(bt_turned_on) {
-        furi_hal_bt_stop_advertising();
-    }
-    // remove & free all stuff created by app
-    gui_remove_view_port(gui, view_port);
-    view_port_free(view_port);
-    osMessageQueueDelete(event_queue);
-    furi_record_close("gui");
-    bt_set_profile(bt, BtProfileSerial);
-    furi_record_close("bt");
-    return 0;
-}

+ 29 - 2
applications/gui/canvas.c

@@ -47,7 +47,7 @@ void canvas_reset(Canvas* canvas) {
 
 
     canvas_set_color(canvas, ColorBlack);
     canvas_set_color(canvas, ColorBlack);
     canvas_set_font(canvas, FontSecondary);
     canvas_set_font(canvas, FontSecondary);
-    canvas_set_font_direction(canvas, CanvasFontDirectionLeftToRight);
+    canvas_set_font_direction(canvas, CanvasDirectionLeftToRight);
 }
 }
 
 
 void canvas_commit(Canvas* canvas) {
 void canvas_commit(Canvas* canvas) {
@@ -115,7 +115,7 @@ void canvas_set_color(Canvas* canvas, Color color) {
     u8g2_SetDrawColor(&canvas->fb, color);
     u8g2_SetDrawColor(&canvas->fb, color);
 }
 }
 
 
-void canvas_set_font_direction(Canvas* canvas, CanvasFontDirection dir) {
+void canvas_set_font_direction(Canvas* canvas, CanvasDirection dir) {
     furi_assert(canvas);
     furi_assert(canvas);
     u8g2_SetFontDirection(&canvas->fb, dir);
     u8g2_SetFontDirection(&canvas->fb, dir);
 }
 }
@@ -304,6 +304,33 @@ void canvas_draw_disc(Canvas* canvas, uint8_t x, uint8_t y, uint8_t radius) {
     u8g2_DrawDisc(&canvas->fb, x, y, radius, U8G2_DRAW_ALL);
     u8g2_DrawDisc(&canvas->fb, x, y, radius, U8G2_DRAW_ALL);
 }
 }
 
 
+void canvas_draw_triangle(
+    Canvas* canvas,
+    uint8_t x,
+    uint8_t y,
+    uint8_t base,
+    uint8_t height,
+    CanvasDirection dir) {
+    furi_assert(canvas);
+    if(dir == CanvasDirectionBottomToTop) {
+        canvas_draw_line(canvas, x - base / 2, y, x + base / 2, y);
+        canvas_draw_line(canvas, x - base / 2, y, x, y - height + 1);
+        canvas_draw_line(canvas, x, y - height + 1, x + base / 2, y);
+    } else if(dir == CanvasDirectionTopToBottom) {
+        canvas_draw_line(canvas, x - base / 2, y, x + base / 2, y);
+        canvas_draw_line(canvas, x - base / 2, y, x, y + height - 1);
+        canvas_draw_line(canvas, x, y + height - 1, x + base / 2, y);
+    } else if(dir == CanvasDirectionRightToLeft) {
+        canvas_draw_line(canvas, x, y - base / 2, x, y + base / 2);
+        canvas_draw_line(canvas, x, y - base / 2, x - height + 1, y);
+        canvas_draw_line(canvas, x - height + 1, y, x, y + base / 2);
+    } else if(dir == CanvasDirectionLeftToRight) {
+        canvas_draw_line(canvas, x, y - base / 2, x, y + base / 2);
+        canvas_draw_line(canvas, x, y - base / 2, x + height - 1, y);
+        canvas_draw_line(canvas, x + height - 1, y, x, y + base / 2);
+    }
+}
+
 void canvas_draw_xbm(
 void canvas_draw_xbm(
     Canvas* canvas,
     Canvas* canvas,
     uint8_t x,
     uint8_t x,

+ 23 - 6
applications/gui/canvas.h

@@ -47,11 +47,11 @@ typedef enum {
 
 
 /** Font Direction */
 /** Font Direction */
 typedef enum {
 typedef enum {
-    CanvasFontDirectionLeftToRight,
-    CanvasFontDirectionTopToDown,
-    CanvasFontDirectionRightToLeft,
-    CanvasFontDirectionDownToTop,
-} CanvasFontDirection;
+    CanvasDirectionLeftToRight,
+    CanvasDirectionTopToBottom,
+    CanvasDirectionRightToLeft,
+    CanvasDirectionBottomToTop,
+} CanvasDirection;
 
 
 /** Font parameters */
 /** Font parameters */
 typedef struct {
 typedef struct {
@@ -116,7 +116,7 @@ void canvas_set_color(Canvas* canvas, Color color);
  * @param      canvas  Canvas instance
  * @param      canvas  Canvas instance
  * @param      dir     Direction font
  * @param      dir     Direction font
  */
  */
-void canvas_set_font_direction(Canvas* canvas, CanvasFontDirection dir);
+void canvas_set_font_direction(Canvas* canvas, CanvasDirection dir);
 
 
 /** Invert drawing color
 /** Invert drawing color
  *
  *
@@ -273,6 +273,23 @@ void canvas_draw_circle(Canvas* canvas, uint8_t x, uint8_t y, uint8_t r);
  */
  */
 void canvas_draw_disc(Canvas* canvas, uint8_t x, uint8_t y, uint8_t r);
 void canvas_draw_disc(Canvas* canvas, uint8_t x, uint8_t y, uint8_t r);
 
 
+/** Draw triangle with given base and height lengths and their intersection coordinate
+ *
+ * @param       canvas  Canvas instance
+ * @param       x       x coordinate of base and height intersection
+ * @param       y       y coordinate of base and height intersection
+ * @param       base    length of triangle side
+ * @param       height  length of triangle height
+ * @param       dir     CanvasDirection triangle orientaion
+ */
+void canvas_draw_triangle(
+    Canvas* canvas,
+    uint8_t x,
+    uint8_t y,
+    uint8_t base,
+    uint8_t height,
+    CanvasDirection dir);
+
 /** Draw glyph
 /** Draw glyph
  *
  *
  * @param      canvas  Canvas instance
  * @param      canvas  Canvas instance

+ 1 - 1
applications/gui/view_dispatcher.c

@@ -258,7 +258,7 @@ void view_dispatcher_handle_input(ViewDispatcher* view_dispatcher, InputEvent* e
         if(view_dispatcher->current_view) {
         if(view_dispatcher->current_view) {
             is_consumed = view_input(view_dispatcher->current_view, event);
             is_consumed = view_input(view_dispatcher->current_view, event);
         }
         }
-        if(!is_consumed && event->type == InputTypeShort) {
+        if(!is_consumed && (event->type == InputTypeShort || event->type == InputTypeLong)) {
             // TODO remove view navigation handlers
             // TODO remove view navigation handlers
             uint32_t view_id = VIEW_IGNORE;
             uint32_t view_id = VIEW_IGNORE;
             if(event->key == InputKeyBack) {
             if(event->key == InputKeyBack) {

Разница между файлами не показана из-за своего большого размера
+ 0 - 0
assets/compiled/assets_icons.c


+ 84 - 75
assets/compiled/assets_icons.h

@@ -36,20 +36,20 @@ extern const Icon A_Level3HijackActive_128x51;
 extern const Icon A_Level3Hijack_128x51;
 extern const Icon A_Level3Hijack_128x51;
 extern const Icon A_Level3LabActive_128x51;
 extern const Icon A_Level3LabActive_128x51;
 extern const Icon A_Level3Lab_128x51;
 extern const Icon A_Level3Lab_128x51;
-extern const Icon I_LevelUp2_07;
-extern const Icon I_LevelUp2_06;
 extern const Icon I_LevelUp2_04;
 extern const Icon I_LevelUp2_04;
 extern const Icon I_LevelUp2_05;
 extern const Icon I_LevelUp2_05;
 extern const Icon I_LevelUp2_01;
 extern const Icon I_LevelUp2_01;
-extern const Icon I_LevelUp2_02;
 extern const Icon I_LevelUp2_03;
 extern const Icon I_LevelUp2_03;
-extern const Icon I_LevelUp3_05;
-extern const Icon I_LevelUp3_04;
-extern const Icon I_LevelUp3_06;
-extern const Icon I_LevelUp3_07;
+extern const Icon I_LevelUp2_02;
+extern const Icon I_LevelUp2_06;
+extern const Icon I_LevelUp2_07;
 extern const Icon I_LevelUp3_03;
 extern const Icon I_LevelUp3_03;
-extern const Icon I_LevelUp3_02;
+extern const Icon I_LevelUp3_07;
 extern const Icon I_LevelUp3_01;
 extern const Icon I_LevelUp3_01;
+extern const Icon I_LevelUp3_06;
+extern const Icon I_LevelUp3_04;
+extern const Icon I_LevelUp3_02;
+extern const Icon I_LevelUp3_05;
 extern const Icon A_LevelUpPending_128x51;
 extern const Icon A_LevelUpPending_128x51;
 extern const Icon A_NoSdCard_128x51;
 extern const Icon A_NoSdCard_128x51;
 extern const Icon A_SleepActive_128x52;
 extern const Icon A_SleepActive_128x52;
@@ -58,77 +58,86 @@ extern const Icon A_TvActive_128x52;
 extern const Icon A_Tv_128x52;
 extern const Icon A_Tv_128x52;
 extern const Icon A_WavesActive_128x52;
 extern const Icon A_WavesActive_128x52;
 extern const Icon A_Waves_128x52;
 extern const Icon A_Waves_128x52;
-extern const Icon I_dir_10px;
 extern const Icon I_Nfc_10px;
 extern const Icon I_Nfc_10px;
-extern const Icon I_sub1_10px;
 extern const Icon I_ir_10px;
 extern const Icon I_ir_10px;
-extern const Icon I_ibutt_10px;
-extern const Icon I_unknown_10px;
 extern const Icon I_ble_10px;
 extern const Icon I_ble_10px;
+extern const Icon I_sub1_10px;
+extern const Icon I_dir_10px;
+extern const Icon I_unknown_10px;
+extern const Icon I_ibutt_10px;
 extern const Icon I_125_10px;
 extern const Icon I_125_10px;
 extern const Icon I_BLE_Pairing_128x64;
 extern const Icon I_BLE_Pairing_128x64;
-extern const Icon I_UsbTree_48x22;
-extern const Icon I_EviWaiting2_18x21;
+extern const Icon I_Voldwn_6x6;
+extern const Icon I_Volup_8x6;
+extern const Icon I_Button_18x18;
+extern const Icon I_Pressed_Button_13x13;
+extern const Icon I_Ble_disconnected_24x34;
+extern const Icon I_Space_65x18;
+extern const Icon I_Circles_47x47;
+extern const Icon I_Ok_btn_9x9;
+extern const Icon I_Ble_connected_38x34;
 extern const Icon I_Clock_18x18;
 extern const Icon I_Clock_18x18;
-extern const Icon I_Smile_18x18;
-extern const Icon I_EviSmile1_18x21;
-extern const Icon I_Percent_10x14;
 extern const Icon I_Error_18x18;
 extern const Icon I_Error_18x18;
-extern const Icon I_EviWaiting1_18x21;
 extern const Icon I_EviSmile2_18x21;
 extern const Icon I_EviSmile2_18x21;
-extern const Icon I_ButtonRightSmall_3x5;
+extern const Icon I_EviWaiting1_18x21;
+extern const Icon I_EviSmile1_18x21;
+extern const Icon I_EviWaiting2_18x21;
+extern const Icon I_UsbTree_48x22;
+extern const Icon I_Smile_18x18;
+extern const Icon I_Percent_10x14;
 extern const Icon I_ButtonLeft_4x7;
 extern const Icon I_ButtonLeft_4x7;
-extern const Icon I_ButtonLeftSmall_3x5;
-extern const Icon I_DFU_128x50;
-extern const Icon I_Warning_30x23;
-extern const Icon I_ButtonDown_7x4;
 extern const Icon I_ButtonRight_4x7;
 extern const Icon I_ButtonRight_4x7;
-extern const Icon I_ButtonCenter_7x7;
+extern const Icon I_ButtonDown_7x4;
 extern const Icon I_ButtonUp_7x4;
 extern const Icon I_ButtonUp_7x4;
+extern const Icon I_Warning_30x23;
+extern const Icon I_DFU_128x50;
+extern const Icon I_ButtonRightSmall_3x5;
+extern const Icon I_ButtonCenter_7x7;
+extern const Icon I_ButtonLeftSmall_3x5;
 extern const Icon I_DolphinOkay_41x43;
 extern const Icon I_DolphinOkay_41x43;
+extern const Icon I_DolphinFirstStart7_61x51;
 extern const Icon I_DolphinFirstStart4_67x53;
 extern const Icon I_DolphinFirstStart4_67x53;
-extern const Icon I_DolphinFirstStart2_59x51;
-extern const Icon I_DolphinFirstStart5_54x49;
+extern const Icon I_DolphinFirstStart3_57x48;
+extern const Icon I_Flipper_young_80x60;
 extern const Icon I_DolphinFirstStart0_70x53;
 extern const Icon I_DolphinFirstStart0_70x53;
+extern const Icon I_DolphinFirstStart2_59x51;
 extern const Icon I_DolphinFirstStart6_58x54;
 extern const Icon I_DolphinFirstStart6_58x54;
-extern const Icon I_DolphinFirstStart1_59x53;
+extern const Icon I_DolphinFirstStart5_54x49;
 extern const Icon I_DolphinFirstStart8_56x51;
 extern const Icon I_DolphinFirstStart8_56x51;
-extern const Icon I_DolphinFirstStart7_61x51;
-extern const Icon I_Flipper_young_80x60;
-extern const Icon I_DolphinFirstStart3_57x48;
-extern const Icon I_ArrowUpFilled_14x15;
+extern const Icon I_DolphinFirstStart1_59x53;
 extern const Icon I_ArrowUpEmpty_14x15;
 extern const Icon I_ArrowUpEmpty_14x15;
 extern const Icon I_ArrowDownEmpty_14x15;
 extern const Icon I_ArrowDownEmpty_14x15;
 extern const Icon I_ArrowDownFilled_14x15;
 extern const Icon I_ArrowDownFilled_14x15;
-extern const Icon I_PassportBottom_128x17;
+extern const Icon I_ArrowUpFilled_14x15;
+extern const Icon I_DoorRight_70x55;
 extern const Icon I_DoorLocked_10x56;
 extern const Icon I_DoorLocked_10x56;
 extern const Icon I_DoorLeft_70x55;
 extern const Icon I_DoorLeft_70x55;
 extern const Icon I_PassportLeft_6x47;
 extern const Icon I_PassportLeft_6x47;
-extern const Icon I_DoorRight_70x55;
 extern const Icon I_LockPopup_100x49;
 extern const Icon I_LockPopup_100x49;
-extern const Icon I_Mute_25x27;
+extern const Icon I_PassportBottom_128x17;
+extern const Icon I_Vol_up_25x27;
+extern const Icon I_Fill_marker_7x7;
 extern const Icon I_IrdaArrowUp_4x8;
 extern const Icon I_IrdaArrowUp_4x8;
+extern const Icon I_Down_hvr_25x27;
+extern const Icon I_Vol_up_hvr_25x27;
+extern const Icon I_Power_25x27;
+extern const Icon I_Vol_down_25x27;
+extern const Icon I_IrdaSend_128x64;
 extern const Icon I_Up_hvr_25x27;
 extern const Icon I_Up_hvr_25x27;
+extern const Icon I_Back_15x10;
 extern const Icon I_DolphinReadingSuccess_59x63;
 extern const Icon I_DolphinReadingSuccess_59x63;
+extern const Icon I_IrdaSendShort_128x34;
 extern const Icon I_Mute_hvr_25x27;
 extern const Icon I_Mute_hvr_25x27;
-extern const Icon I_Vol_down_25x27;
-extern const Icon I_Down_25x27;
-extern const Icon I_Power_hvr_25x27;
 extern const Icon I_IrdaLearnShort_128x31;
 extern const Icon I_IrdaLearnShort_128x31;
-extern const Icon I_IrdaArrowDown_4x8;
+extern const Icon I_Down_25x27;
+extern const Icon I_Up_25x27;
+extern const Icon I_Mute_25x27;
 extern const Icon I_Vol_down_hvr_25x27;
 extern const Icon I_Vol_down_hvr_25x27;
+extern const Icon I_Power_hvr_25x27;
 extern const Icon I_IrdaLearn_128x64;
 extern const Icon I_IrdaLearn_128x64;
-extern const Icon I_Down_hvr_25x27;
-extern const Icon I_Fill_marker_7x7;
-extern const Icon I_Power_25x27;
-extern const Icon I_Vol_up_25x27;
-extern const Icon I_Up_25x27;
-extern const Icon I_Back_15x10;
-extern const Icon I_IrdaSend_128x64;
-extern const Icon I_IrdaSendShort_128x34;
-extern const Icon I_Vol_up_hvr_25x27;
-extern const Icon I_KeySave_24x11;
+extern const Icon I_IrdaArrowDown_4x8;
 extern const Icon I_KeyBackspaceSelected_16x9;
 extern const Icon I_KeyBackspaceSelected_16x9;
+extern const Icon I_KeySave_24x11;
 extern const Icon I_KeySaveSelected_24x11;
 extern const Icon I_KeySaveSelected_24x11;
 extern const Icon I_KeyBackspace_16x9;
 extern const Icon I_KeyBackspace_16x9;
 extern const Icon A_125khz_14;
 extern const Icon A_125khz_14;
@@ -148,56 +157,56 @@ extern const Icon A_Sub1ghz_14;
 extern const Icon A_Tamagotchi_14;
 extern const Icon A_Tamagotchi_14;
 extern const Icon A_U2F_14;
 extern const Icon A_U2F_14;
 extern const Icon A_iButton_14;
 extern const Icon A_iButton_14;
-extern const Icon I_Detailed_chip_17x13;
 extern const Icon I_Medium_chip_22x21;
 extern const Icon I_Medium_chip_22x21;
+extern const Icon I_Detailed_chip_17x13;
 extern const Icon I_passport_happy1_46x49;
 extern const Icon I_passport_happy1_46x49;
+extern const Icon I_passport_happy2_46x49;
 extern const Icon I_passport_bad3_46x49;
 extern const Icon I_passport_bad3_46x49;
 extern const Icon I_passport_okay2_46x49;
 extern const Icon I_passport_okay2_46x49;
 extern const Icon I_passport_bad2_46x49;
 extern const Icon I_passport_bad2_46x49;
-extern const Icon I_passport_okay3_46x49;
-extern const Icon I_passport_bottom_128x18;
 extern const Icon I_passport_bad1_46x49;
 extern const Icon I_passport_bad1_46x49;
+extern const Icon I_passport_bottom_128x18;
 extern const Icon I_passport_happy3_46x49;
 extern const Icon I_passport_happy3_46x49;
-extern const Icon I_passport_happy2_46x49;
-extern const Icon I_passport_okay1_46x49;
 extern const Icon I_passport_left_6x46;
 extern const Icon I_passport_left_6x46;
+extern const Icon I_passport_okay3_46x49;
+extern const Icon I_passport_okay1_46x49;
 extern const Icon I_Health_16x16;
 extern const Icon I_Health_16x16;
-extern const Icon I_FaceCharging_29x14;
-extern const Icon I_BatteryBody_52x28;
-extern const Icon I_Voltage_16x16;
-extern const Icon I_Temperature_16x16;
 extern const Icon I_FaceNopower_29x14;
 extern const Icon I_FaceNopower_29x14;
-extern const Icon I_FaceNormal_29x14;
 extern const Icon I_Battery_16x16;
 extern const Icon I_Battery_16x16;
+extern const Icon I_BatteryBody_52x28;
 extern const Icon I_FaceConfused_29x14;
 extern const Icon I_FaceConfused_29x14;
-extern const Icon I_RFIDDolphinSuccess_108x57;
-extern const Icon I_RFIDBigChip_37x36;
-extern const Icon I_RFIDDolphinSend_97x61;
+extern const Icon I_FaceCharging_29x14;
+extern const Icon I_FaceNormal_29x14;
+extern const Icon I_Voltage_16x16;
+extern const Icon I_Temperature_16x16;
 extern const Icon I_RFIDDolphinReceive_97x61;
 extern const Icon I_RFIDDolphinReceive_97x61;
+extern const Icon I_RFIDDolphinSend_97x61;
+extern const Icon I_RFIDBigChip_37x36;
+extern const Icon I_RFIDDolphinSuccess_108x57;
 extern const Icon I_SDQuestion_35x43;
 extern const Icon I_SDQuestion_35x43;
 extern const Icon I_SDError_43x35;
 extern const Icon I_SDError_43x35;
 extern const Icon I_Cry_dolph_55x52;
 extern const Icon I_Cry_dolph_55x52;
-extern const Icon I_BadUsb_9x8;
-extern const Icon I_PlaceholderR_30x13;
-extern const Icon I_Lock_8x8;
 extern const Icon I_Battery_26x8;
 extern const Icon I_Battery_26x8;
 extern const Icon I_PlaceholderL_11x13;
 extern const Icon I_PlaceholderL_11x13;
-extern const Icon I_Battery_19x8;
-extern const Icon I_SDcardMounted_11x8;
-extern const Icon I_SDcardFail_11x8;
-extern const Icon I_USBConnected_15x8;
-extern const Icon I_Bluetooth_5x8;
 extern const Icon I_BT_Pair_9x8;
 extern const Icon I_BT_Pair_9x8;
+extern const Icon I_Bluetooth_5x8;
+extern const Icon I_BadUsb_9x8;
+extern const Icon I_PlaceholderR_30x13;
+extern const Icon I_USBConnected_15x8;
+extern const Icon I_Battery_19x8;
+extern const Icon I_Lock_8x8;
 extern const Icon I_Background_128x11;
 extern const Icon I_Background_128x11;
-extern const Icon I_Scanning_123x52;
+extern const Icon I_SDcardFail_11x8;
+extern const Icon I_SDcardMounted_11x8;
+extern const Icon I_Lock_7x8;
 extern const Icon I_Quest_7x8;
 extern const Icon I_Quest_7x8;
-extern const Icon I_Unlock_7x8;
+extern const Icon I_Scanning_123x52;
 extern const Icon I_MHz_25x11;
 extern const Icon I_MHz_25x11;
-extern const Icon I_Lock_7x8;
+extern const Icon I_Unlock_7x8;
+extern const Icon I_iButtonDolphinVerySuccess_108x52;
 extern const Icon I_DolphinMafia_115x62;
 extern const Icon I_DolphinMafia_115x62;
-extern const Icon I_DolphinExcited_64x63;
 extern const Icon I_iButtonDolphinSuccess_109x60;
 extern const Icon I_iButtonDolphinSuccess_109x60;
-extern const Icon I_iButtonDolphinVerySuccess_108x52;
-extern const Icon I_iButtonKey_49x44;
+extern const Icon I_DolphinExcited_64x63;
 extern const Icon I_DolphinNice_96x59;
 extern const Icon I_DolphinNice_96x59;
+extern const Icon I_iButtonKey_49x44;
 extern const Icon I_DolphinWait_61x59;
 extern const Icon I_DolphinWait_61x59;

BIN
assets/icons/BLE/BLE_HID/Ble_connected_38x34.png


BIN
assets/icons/BLE/BLE_HID/Ble_disconnected_24x34.png


BIN
assets/icons/BLE/BLE_HID/Button_18x18.png


BIN
assets/icons/BLE/BLE_HID/Circles_47x47.png


BIN
assets/icons/BLE/BLE_HID/Ok_btn_9x9.png


BIN
assets/icons/BLE/BLE_HID/Pressed_Button_13x13.png


BIN
assets/icons/BLE/BLE_HID/Space_65x18.png


BIN
assets/icons/BLE/BLE_HID/Voldwn_6x6.png


BIN
assets/icons/BLE/BLE_HID/Volup_8x6.png


+ 6 - 2
firmware/targets/f6/ble-glue/gap.c

@@ -75,7 +75,7 @@ SVCCTL_UserEvtFlowStatus_t SVCCTL_App_Notification( void *pckt )
             }
             }
             if(gap->enable_adv) {
             if(gap->enable_adv) {
                 // Restart advertising
                 // Restart advertising
-                gap_advertise_start(GapCommandAdvFast);
+                gap_advertise_start(GapStateAdvFast);
                 furi_hal_power_insomnia_exit();
                 furi_hal_power_insomnia_exit();
             }
             }
             BleEvent event = {.type = BleEventTypeDisconnected};
             BleEvent event = {.type = BleEventTypeDisconnected};
@@ -446,7 +446,11 @@ void gap_thread_stop() {
 static int32_t gap_app(void *context) {
 static int32_t gap_app(void *context) {
     GapCommand command;
     GapCommand command;
     while(1) {
     while(1) {
-        furi_check(osMessageQueueGet(gap->command_queue, &command, NULL, osWaitForever) == osOK);
+        osStatus status = osMessageQueueGet(gap->command_queue, &command, NULL, osWaitForever);
+        if(status != osOK) {
+            FURI_LOG_E(TAG, "Message queue get error: %d", status);
+            continue;
+        }
         osMutexAcquire(gap->state_mutex, osWaitForever);
         osMutexAcquire(gap->state_mutex, osWaitForever);
         if(command == GapCommandKillThread) {
         if(command == GapCommandKillThread) {
             break;
             break;

+ 2 - 2
firmware/targets/f6/ble-glue/hid_service.h

@@ -3,8 +3,8 @@
 #include <stdint.h>
 #include <stdint.h>
 #include <stdbool.h>
 #include <stdbool.h>
 
 
-#define HID_SVC_REPORT_MAP_MAX_LEN (80)
-#define HID_SVC_REPORT_MAX_LEN (8)
+#define HID_SVC_REPORT_MAP_MAX_LEN (120)
+#define HID_SVC_REPORT_MAX_LEN (9)
 #define HID_SVC_BOOT_KEYBOARD_INPUT_REPORT_MAX_LEN (8)
 #define HID_SVC_BOOT_KEYBOARD_INPUT_REPORT_MAX_LEN (8)
 #define HID_SVC_REPORT_REF_LEN (2)
 #define HID_SVC_REPORT_REF_LEN (2)
 #define HID_SVC_INFO_LEN (4)
 #define HID_SVC_INFO_LEN (4)

+ 57 - 2
firmware/targets/f6/furi-hal/furi-hal-bt-hid.c

@@ -13,16 +13,23 @@
 #define FURI_HAL_BT_HID_KB_KEYS_MAX (6)
 #define FURI_HAL_BT_HID_KB_KEYS_MAX (6)
 
 
 typedef struct {
 typedef struct {
+    // uint8_t report_id;
     uint8_t mods;
     uint8_t mods;
     uint8_t reserved;
     uint8_t reserved;
     uint8_t key[FURI_HAL_BT_HID_KB_KEYS_MAX];
     uint8_t key[FURI_HAL_BT_HID_KB_KEYS_MAX];
 } FuriHalBtHidKbReport;
 } FuriHalBtHidKbReport;
 
 
-// TODO rework with HID defines
+typedef struct {
+    uint8_t report_id;
+    uint8_t key;
+} FuriHalBtHidMediaReport;
+
+// TODO make composite HID device
 static uint8_t furi_hal_bt_hid_report_map_data[] = {
 static uint8_t furi_hal_bt_hid_report_map_data[] = {
     0x05, 0x01,       // Usage Page (Generic Desktop)
     0x05, 0x01,       // Usage Page (Generic Desktop)
     0x09, 0x06,       // Usage (Keyboard)
     0x09, 0x06,       // Usage (Keyboard)
     0xA1, 0x01,       // Collection (Application)
     0xA1, 0x01,       // Collection (Application)
+    // 0x85, 0x01,       // Report ID (1)
     0x05, 0x07,       // Usage Page (Key Codes)
     0x05, 0x07,       // Usage Page (Key Codes)
     0x19, 0xe0,       // Usage Minimum (224)
     0x19, 0xe0,       // Usage Minimum (224)
     0x29, 0xe7,       // Usage Maximum (231)
     0x29, 0xe7,       // Usage Maximum (231)
@@ -62,10 +69,31 @@ static uint8_t furi_hal_bt_hid_report_map_data[] = {
     0x95, 0x02,       // Report Count (2)
     0x95, 0x02,       // Report Count (2)
     0xB1, 0x02,       // Feature (Data, Variable, Absolute)
     0xB1, 0x02,       // Feature (Data, Variable, Absolute)
 
 
-    0xC0              // End Collection (Application)
+    0xC0,              // End Collection (Application)
+
+    // 0x05, 0x0C,        // Usage Page (Consumer)
+    // 0x09, 0x01,        // Usage (Consumer Control)
+    // 0xA1, 0x01,        // Collection (Application)
+    // 0x85, 0x02,        //   Report ID (2)
+    // 0x05, 0x0C,        //   Usage Page (Consumer)
+    // 0x15, 0x00,        //   Logical Minimum (0)
+    // 0x25, 0x01,        //   Logical Maximum (1)
+    // 0x75, 0x01,        //   Report Size (1)
+    // 0x95, 0x07,        //   Report Count (7)
+    // 0x09, 0xB5,        //   Usage (Scan Next Track)
+    // 0x09, 0xB6,        //   Usage (Scan Previous Track)
+    // 0x09, 0xB7,        //   Usage (Stop)
+    // 0x09, 0xB8,        //   Usage (Eject)
+    // 0x09, 0xCD,        //   Usage (Play/Pause)
+    // 0x09, 0xE2,        //   Usage (Mute)
+    // 0x09, 0xE9,        //   Usage (Volume Increment)
+    // 0x09, 0xEA,        //   Usage (Volume Decrement)
+    // 0x81, 0x02,        //   Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
+    // 0xC0,              // End Collection
 };
 };
 
 
 FuriHalBtHidKbReport* kb_report = NULL;
 FuriHalBtHidKbReport* kb_report = NULL;
+FuriHalBtHidMediaReport* media_report = NULL;
 
 
 void furi_hal_bt_hid_start() {
 void furi_hal_bt_hid_start() {
     // Start device info
     // Start device info
@@ -82,6 +110,7 @@ void furi_hal_bt_hid_start() {
     }
     }
     // Configure HID Keyboard
     // Configure HID Keyboard
     kb_report = furi_alloc(sizeof(FuriHalBtHidKbReport));
     kb_report = furi_alloc(sizeof(FuriHalBtHidKbReport));
+    media_report = furi_alloc(sizeof(FuriHalBtHidMediaReport));
     // Configure Report Map characteristic
     // Configure Report Map characteristic
     hid_svc_update_report_map(furi_hal_bt_hid_report_map_data, sizeof(furi_hal_bt_hid_report_map_data));
     hid_svc_update_report_map(furi_hal_bt_hid_report_map_data, sizeof(furi_hal_bt_hid_report_map_data));
     // Configure HID Information characteristic
     // Configure HID Information characteristic
@@ -107,11 +136,14 @@ void furi_hal_bt_hid_stop() {
         hid_svc_stop();
         hid_svc_stop();
     }
     }
     free(kb_report);
     free(kb_report);
+    free(media_report);
+    media_report = NULL;
     kb_report = NULL;
     kb_report = NULL;
 }
 }
 
 
 bool furi_hal_bt_hid_kb_press(uint16_t button) {
 bool furi_hal_bt_hid_kb_press(uint16_t button) {
     furi_assert(kb_report);
     furi_assert(kb_report);
+    // kb_report->report_id = 0x01;
     for (uint8_t i = 0; i < FURI_HAL_BT_HID_KB_KEYS_MAX; i++) {
     for (uint8_t i = 0; i < FURI_HAL_BT_HID_KB_KEYS_MAX; i++) {
         if (kb_report->key[i] == 0) {
         if (kb_report->key[i] == 0) {
             kb_report->key[i] = button & 0xFF;
             kb_report->key[i] = button & 0xFF;
@@ -124,6 +156,7 @@ bool furi_hal_bt_hid_kb_press(uint16_t button) {
 
 
 bool furi_hal_bt_hid_kb_release(uint16_t button) {
 bool furi_hal_bt_hid_kb_release(uint16_t button) {
     furi_assert(kb_report);
     furi_assert(kb_report);
+    // kb_report->report_id = 0x01;
     for (uint8_t i = 0; i < FURI_HAL_BT_HID_KB_KEYS_MAX; i++) {
     for (uint8_t i = 0; i < FURI_HAL_BT_HID_KB_KEYS_MAX; i++) {
         if (kb_report->key[i] == (button & 0xFF)) {
         if (kb_report->key[i] == (button & 0xFF)) {
             kb_report->key[i] = 0;
             kb_report->key[i] = 0;
@@ -136,6 +169,28 @@ bool furi_hal_bt_hid_kb_release(uint16_t button) {
 
 
 bool furi_hal_bt_hid_kb_release_all() {
 bool furi_hal_bt_hid_kb_release_all() {
     furi_assert(kb_report);
     furi_assert(kb_report);
+    // kb_report->report_id = 0x01;
     memset(kb_report, 0, sizeof(FuriHalBtHidKbReport));
     memset(kb_report, 0, sizeof(FuriHalBtHidKbReport));
     return hid_svc_update_input_report((uint8_t*)kb_report, sizeof(FuriHalBtHidKbReport));
     return hid_svc_update_input_report((uint8_t*)kb_report, sizeof(FuriHalBtHidKbReport));
 }
 }
+
+bool furi_hal_bt_hid_media_press(uint8_t button) {
+    furi_assert(media_report);
+    media_report->report_id = 0x02;
+    media_report->key |= (0x01 << button);
+    return hid_svc_update_input_report((uint8_t*)media_report, sizeof(FuriHalBtHidMediaReport));
+}
+
+bool furi_hal_bt_hid_media_release(uint8_t button) {
+    furi_assert(media_report);
+    media_report->report_id = 0x02;
+    media_report->key &= ~(0x01 << button);
+    return hid_svc_update_input_report((uint8_t*)media_report, sizeof(FuriHalBtHidMediaReport));
+}
+
+bool furi_hal_bt_hid_media_release_all() {
+    furi_assert(media_report);
+    media_report->report_id = 0x02;
+    media_report->key = 0x00;
+    return hid_svc_update_input_report((uint8_t*)media_report, sizeof(FuriHalBtHidMediaReport));
+}

+ 3 - 3
firmware/targets/f6/furi-hal/furi-hal-bt.c

@@ -126,9 +126,9 @@ bool furi_hal_bt_start_app(FuriHalBtProfile profile, BleEventCallback event_cb,
         } else if(profile == FuriHalBtProfileHidKeyboard) {
         } else if(profile == FuriHalBtProfileHidKeyboard) {
             // Change MAC address for HID profile
             // Change MAC address for HID profile
             config->mac_address[2]++;
             config->mac_address[2]++;
-            // Change name Flipper -> Clicker
-            const char* clicker_str = "Clicker";
-            memcpy(&config->adv_name[1], clicker_str, strlen(clicker_str) - 1);
+            // Change name Flipper -> Keynote
+            const char* clicker_str = "Keynote";
+            memcpy(&config->adv_name[1], clicker_str, strlen(clicker_str));
         }
         }
         ret = gap_init(config, event_cb, context);
         ret = gap_init(config, event_cb, context);
         if(!ret) {
         if(!ret) {

+ 6 - 2
firmware/targets/f7/ble-glue/gap.c

@@ -75,7 +75,7 @@ SVCCTL_UserEvtFlowStatus_t SVCCTL_App_Notification( void *pckt )
             }
             }
             if(gap->enable_adv) {
             if(gap->enable_adv) {
                 // Restart advertising
                 // Restart advertising
-                gap_advertise_start(GapCommandAdvFast);
+                gap_advertise_start(GapStateAdvFast);
                 furi_hal_power_insomnia_exit();
                 furi_hal_power_insomnia_exit();
             }
             }
             BleEvent event = {.type = BleEventTypeDisconnected};
             BleEvent event = {.type = BleEventTypeDisconnected};
@@ -446,7 +446,11 @@ void gap_thread_stop() {
 static int32_t gap_app(void *context) {
 static int32_t gap_app(void *context) {
     GapCommand command;
     GapCommand command;
     while(1) {
     while(1) {
-        furi_check(osMessageQueueGet(gap->command_queue, &command, NULL, osWaitForever) == osOK);
+        osStatus status = osMessageQueueGet(gap->command_queue, &command, NULL, osWaitForever);
+        if(status != osOK) {
+            FURI_LOG_E(TAG, "Message queue get error: %d", status);
+            continue;
+        }
         osMutexAcquire(gap->state_mutex, osWaitForever);
         osMutexAcquire(gap->state_mutex, osWaitForever);
         if(command == GapCommandKillThread) {
         if(command == GapCommandKillThread) {
             break;
             break;

+ 2 - 2
firmware/targets/f7/ble-glue/hid_service.h

@@ -3,8 +3,8 @@
 #include <stdint.h>
 #include <stdint.h>
 #include <stdbool.h>
 #include <stdbool.h>
 
 
-#define HID_SVC_REPORT_MAP_MAX_LEN (80)
-#define HID_SVC_REPORT_MAX_LEN (8)
+#define HID_SVC_REPORT_MAP_MAX_LEN (120)
+#define HID_SVC_REPORT_MAX_LEN (9)
 #define HID_SVC_BOOT_KEYBOARD_INPUT_REPORT_MAX_LEN (8)
 #define HID_SVC_BOOT_KEYBOARD_INPUT_REPORT_MAX_LEN (8)
 #define HID_SVC_REPORT_REF_LEN (2)
 #define HID_SVC_REPORT_REF_LEN (2)
 #define HID_SVC_INFO_LEN (4)
 #define HID_SVC_INFO_LEN (4)

+ 57 - 2
firmware/targets/f7/furi-hal/furi-hal-bt-hid.c

@@ -13,16 +13,23 @@
 #define FURI_HAL_BT_HID_KB_KEYS_MAX (6)
 #define FURI_HAL_BT_HID_KB_KEYS_MAX (6)
 
 
 typedef struct {
 typedef struct {
+    // uint8_t report_id;
     uint8_t mods;
     uint8_t mods;
     uint8_t reserved;
     uint8_t reserved;
     uint8_t key[FURI_HAL_BT_HID_KB_KEYS_MAX];
     uint8_t key[FURI_HAL_BT_HID_KB_KEYS_MAX];
 } FuriHalBtHidKbReport;
 } FuriHalBtHidKbReport;
 
 
-// TODO rework with HID defines
+typedef struct {
+    uint8_t report_id;
+    uint8_t key;
+} FuriHalBtHidMediaReport;
+
+// TODO make composite HID device
 static uint8_t furi_hal_bt_hid_report_map_data[] = {
 static uint8_t furi_hal_bt_hid_report_map_data[] = {
     0x05, 0x01,       // Usage Page (Generic Desktop)
     0x05, 0x01,       // Usage Page (Generic Desktop)
     0x09, 0x06,       // Usage (Keyboard)
     0x09, 0x06,       // Usage (Keyboard)
     0xA1, 0x01,       // Collection (Application)
     0xA1, 0x01,       // Collection (Application)
+    // 0x85, 0x01,       // Report ID (1)
     0x05, 0x07,       // Usage Page (Key Codes)
     0x05, 0x07,       // Usage Page (Key Codes)
     0x19, 0xe0,       // Usage Minimum (224)
     0x19, 0xe0,       // Usage Minimum (224)
     0x29, 0xe7,       // Usage Maximum (231)
     0x29, 0xe7,       // Usage Maximum (231)
@@ -62,10 +69,31 @@ static uint8_t furi_hal_bt_hid_report_map_data[] = {
     0x95, 0x02,       // Report Count (2)
     0x95, 0x02,       // Report Count (2)
     0xB1, 0x02,       // Feature (Data, Variable, Absolute)
     0xB1, 0x02,       // Feature (Data, Variable, Absolute)
 
 
-    0xC0              // End Collection (Application)
+    0xC0,              // End Collection (Application)
+
+    // 0x05, 0x0C,        // Usage Page (Consumer)
+    // 0x09, 0x01,        // Usage (Consumer Control)
+    // 0xA1, 0x01,        // Collection (Application)
+    // 0x85, 0x02,        //   Report ID (2)
+    // 0x05, 0x0C,        //   Usage Page (Consumer)
+    // 0x15, 0x00,        //   Logical Minimum (0)
+    // 0x25, 0x01,        //   Logical Maximum (1)
+    // 0x75, 0x01,        //   Report Size (1)
+    // 0x95, 0x07,        //   Report Count (7)
+    // 0x09, 0xB5,        //   Usage (Scan Next Track)
+    // 0x09, 0xB6,        //   Usage (Scan Previous Track)
+    // 0x09, 0xB7,        //   Usage (Stop)
+    // 0x09, 0xB8,        //   Usage (Eject)
+    // 0x09, 0xCD,        //   Usage (Play/Pause)
+    // 0x09, 0xE2,        //   Usage (Mute)
+    // 0x09, 0xE9,        //   Usage (Volume Increment)
+    // 0x09, 0xEA,        //   Usage (Volume Decrement)
+    // 0x81, 0x02,        //   Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
+    // 0xC0,              // End Collection
 };
 };
 
 
 FuriHalBtHidKbReport* kb_report = NULL;
 FuriHalBtHidKbReport* kb_report = NULL;
+FuriHalBtHidMediaReport* media_report = NULL;
 
 
 void furi_hal_bt_hid_start() {
 void furi_hal_bt_hid_start() {
     // Start device info
     // Start device info
@@ -82,6 +110,7 @@ void furi_hal_bt_hid_start() {
     }
     }
     // Configure HID Keyboard
     // Configure HID Keyboard
     kb_report = furi_alloc(sizeof(FuriHalBtHidKbReport));
     kb_report = furi_alloc(sizeof(FuriHalBtHidKbReport));
+    media_report = furi_alloc(sizeof(FuriHalBtHidMediaReport));
     // Configure Report Map characteristic
     // Configure Report Map characteristic
     hid_svc_update_report_map(furi_hal_bt_hid_report_map_data, sizeof(furi_hal_bt_hid_report_map_data));
     hid_svc_update_report_map(furi_hal_bt_hid_report_map_data, sizeof(furi_hal_bt_hid_report_map_data));
     // Configure HID Information characteristic
     // Configure HID Information characteristic
@@ -107,11 +136,14 @@ void furi_hal_bt_hid_stop() {
         hid_svc_stop();
         hid_svc_stop();
     }
     }
     free(kb_report);
     free(kb_report);
+    free(media_report);
+    media_report = NULL;
     kb_report = NULL;
     kb_report = NULL;
 }
 }
 
 
 bool furi_hal_bt_hid_kb_press(uint16_t button) {
 bool furi_hal_bt_hid_kb_press(uint16_t button) {
     furi_assert(kb_report);
     furi_assert(kb_report);
+    // kb_report->report_id = 0x01;
     for (uint8_t i = 0; i < FURI_HAL_BT_HID_KB_KEYS_MAX; i++) {
     for (uint8_t i = 0; i < FURI_HAL_BT_HID_KB_KEYS_MAX; i++) {
         if (kb_report->key[i] == 0) {
         if (kb_report->key[i] == 0) {
             kb_report->key[i] = button & 0xFF;
             kb_report->key[i] = button & 0xFF;
@@ -124,6 +156,7 @@ bool furi_hal_bt_hid_kb_press(uint16_t button) {
 
 
 bool furi_hal_bt_hid_kb_release(uint16_t button) {
 bool furi_hal_bt_hid_kb_release(uint16_t button) {
     furi_assert(kb_report);
     furi_assert(kb_report);
+    // kb_report->report_id = 0x01;
     for (uint8_t i = 0; i < FURI_HAL_BT_HID_KB_KEYS_MAX; i++) {
     for (uint8_t i = 0; i < FURI_HAL_BT_HID_KB_KEYS_MAX; i++) {
         if (kb_report->key[i] == (button & 0xFF)) {
         if (kb_report->key[i] == (button & 0xFF)) {
             kb_report->key[i] = 0;
             kb_report->key[i] = 0;
@@ -136,6 +169,28 @@ bool furi_hal_bt_hid_kb_release(uint16_t button) {
 
 
 bool furi_hal_bt_hid_kb_release_all() {
 bool furi_hal_bt_hid_kb_release_all() {
     furi_assert(kb_report);
     furi_assert(kb_report);
+    // kb_report->report_id = 0x01;
     memset(kb_report, 0, sizeof(FuriHalBtHidKbReport));
     memset(kb_report, 0, sizeof(FuriHalBtHidKbReport));
     return hid_svc_update_input_report((uint8_t*)kb_report, sizeof(FuriHalBtHidKbReport));
     return hid_svc_update_input_report((uint8_t*)kb_report, sizeof(FuriHalBtHidKbReport));
 }
 }
+
+bool furi_hal_bt_hid_media_press(uint8_t button) {
+    furi_assert(media_report);
+    media_report->report_id = 0x02;
+    media_report->key |= (0x01 << button);
+    return hid_svc_update_input_report((uint8_t*)media_report, sizeof(FuriHalBtHidMediaReport));
+}
+
+bool furi_hal_bt_hid_media_release(uint8_t button) {
+    furi_assert(media_report);
+    media_report->report_id = 0x02;
+    media_report->key &= ~(0x01 << button);
+    return hid_svc_update_input_report((uint8_t*)media_report, sizeof(FuriHalBtHidMediaReport));
+}
+
+bool furi_hal_bt_hid_media_release_all() {
+    furi_assert(media_report);
+    media_report->report_id = 0x02;
+    media_report->key = 0x00;
+    return hid_svc_update_input_report((uint8_t*)media_report, sizeof(FuriHalBtHidMediaReport));
+}

+ 3 - 3
firmware/targets/f7/furi-hal/furi-hal-bt.c

@@ -126,9 +126,9 @@ bool furi_hal_bt_start_app(FuriHalBtProfile profile, BleEventCallback event_cb,
         } else if(profile == FuriHalBtProfileHidKeyboard) {
         } else if(profile == FuriHalBtProfileHidKeyboard) {
             // Change MAC address for HID profile
             // Change MAC address for HID profile
             config->mac_address[2]++;
             config->mac_address[2]++;
-            // Change name Flipper -> Clicker
-            const char* clicker_str = "Clicker";
-            memcpy(&config->adv_name[1], clicker_str, strlen(clicker_str) - 1);
+            // Change name Flipper -> Keynote
+            const char* clicker_str = "Keynote";
+            memcpy(&config->adv_name[1], clicker_str, strlen(clicker_str));
         }
         }
         ret = gap_init(config, event_cb, context);
         ret = gap_init(config, event_cb, context);
         if(!ret) {
         if(!ret) {

+ 32 - 3
firmware/targets/furi-hal-include/furi-hal-bt-hid.h

@@ -3,6 +3,17 @@
 #include <stdint.h>
 #include <stdint.h>
 #include <stdbool.h>
 #include <stdbool.h>
 
 
+enum FuriHalBtHidMediKeys{
+    FuriHalBtHidMediaScanNext,
+    FuriHalBtHidMediaScanPrevious,
+    FuriHalBtHidMediaStop,
+    FuriHalBtHidMediaEject,
+    FuriHalBtHidMediaPlayPause,
+    FuriHalBtHidMediaMute,
+    FuriHalBtHidMediaVolumeUp,
+    FuriHalBtHidMediaVolumeDown,
+};
+
 /** Start Hid Keyboard Profile
 /** Start Hid Keyboard Profile
  */
  */
 void furi_hal_bt_hid_start();
 void furi_hal_bt_hid_start();
@@ -11,7 +22,7 @@ void furi_hal_bt_hid_start();
  */
  */
 void furi_hal_bt_hid_stop();
 void furi_hal_bt_hid_stop();
 
 
-/** Press key button
+/** Press keyboard button
  *
  *
  * @param button    button code from HID specification
  * @param button    button code from HID specification
  *
  *
@@ -19,7 +30,7 @@ void furi_hal_bt_hid_stop();
  */
  */
 bool furi_hal_bt_hid_kb_press(uint16_t button);
 bool furi_hal_bt_hid_kb_press(uint16_t button);
 
 
-/** Release key button
+/** Release keyboard button
  *
  *
  * @param button    button code from HID specification
  * @param button    button code from HID specification
  *
  *
@@ -27,8 +38,26 @@ bool furi_hal_bt_hid_kb_press(uint16_t button);
  */
  */
 bool furi_hal_bt_hid_kb_release(uint16_t button);
 bool furi_hal_bt_hid_kb_release(uint16_t button);
 
 
-/** Release all key buttons
+/** Release all keyboard buttons
  *
  *
  * @return          true on success
  * @return          true on success
  */
  */
 bool furi_hal_bt_hid_kb_release_all();
 bool furi_hal_bt_hid_kb_release_all();
+
+/** Release all media buttons
+ *
+ * @return          true on success
+ */
+bool furi_hal_bt_hid_media_press(uint8_t button);
+
+/** Release all media buttons
+ *
+ * @return          true on success
+ */
+bool furi_hal_bt_hid_media_release(uint8_t button);
+
+/** Release all media buttons
+ *
+ * @return          true on success
+ */
+bool furi_hal_bt_hid_media_release_all();

Некоторые файлы не были показаны из-за большого количества измененных файлов