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

Air Mouse with old BLE profile (#131)

Co-authored-by: あく <alleteam@gmail.com>
Nikolay Minaylov 1 год назад
Родитель
Сommit
7d8a26f959

+ 2 - 0
.catalog/README.md

@@ -0,0 +1,2 @@
+# Air Mouse app for Video Game Module
+The Air Mouse app turns Flipper Zero with the module into a computer mouse. Control the pointer on your computer by tilting and rotating your Flipper Zero with the help of the motion-tracking sensor inside the module. Simply connect your Flipper Zero to a computer or smartphone via Bluetooth or USB.

BIN
.catalog/screenshots/1.png


BIN
.catalog/screenshots/2.png


+ 196 - 85
air_mouse_app.c

@@ -1,129 +1,240 @@
 #include <furi.h>
 #include <furi_hal.h>
+#include <furi_hal_bt_hid.h>
+#include <bt/bt_service/bt.h>
 #include <gui/gui.h>
-#include <dialogs/dialogs.h>
+#include <gui/view_dispatcher.h>
+#include <gui/modules/submenu.h>
+#include <gui/modules/dialog_ex.h>
 #include "imu_mouse.h"
-#include "air_mouse_icons.h"
+#include "views/air_mouse_view.h"
+#include <furi_hal_usb_hid.h>
+#include <storage/storage.h>
 
 #define TAG "SensorModule"
 
+#define BLE_HID_KEYS_PATH "/ext/apps_data/hid_ble/.bt_hid.keys"
+
 typedef struct {
     Gui* gui;
-    ViewPort* view_port;
-    FuriMessageQueue* input_queue;
-
+    ViewDispatcher* view_dispatcher;
+    Submenu* start_submenu;
+    DialogEx* error_dialog;
+    AirMouseView* air_mouse_view;
     FuriHalSpiBusHandle* icm42688p_device;
     ICM42688P* icm42688p;
-    bool icm42688p_valid;
+    FuriHalUsbInterface* usb_mode_prev;
+    Bt* bt;
+} AirMouseApp;
+
+typedef enum {
+    AirMouseViewError,
+    AirMouseViewStartSubmenu,
+    AirMouseViewMain,
+} AirMouseViews;
+
+enum StertSubmenuIndex {
+    StartSubmenuIndexUsb,
+    StartSubmenuIndexBle,
+    StartSubmenuIndexBleReset,
+};
+
+static const ImuHidApi hid_api_usb = {
+    .mouse_move = furi_hal_hid_mouse_move,
+    .mouse_key_press = furi_hal_hid_mouse_press,
+    .mouse_key_release = furi_hal_hid_mouse_release,
+    .mouse_scroll = furi_hal_hid_mouse_scroll,
+    .report_rate_max = 200,
+};
+
+static const ImuHidApi hid_api_ble = {
+    .mouse_move = furi_hal_bt_hid_mouse_move,
+    .mouse_key_press = furi_hal_bt_hid_mouse_press,
+    .mouse_key_release = furi_hal_bt_hid_mouse_release,
+    .mouse_scroll = furi_hal_bt_hid_mouse_scroll,
+    .report_rate_max = 30,
+};
+
+static void ble_hid_remove_pairing(void) {
+    Bt* bt = furi_record_open(RECORD_BT);
+    bt_disconnect(bt);
+
+    // Wait 2nd core to update nvm storage
+    furi_delay_ms(200);
+
+    bt_keys_storage_set_storage_path(bt, BLE_HID_KEYS_PATH);
+    bt_forget_bonded_devices(bt);
+
+    // Wait 2nd core to update nvm storage
+    furi_delay_ms(200);
+    bt_keys_storage_set_default_path(bt);
+
+    furi_check(bt_set_profile(bt, BtProfileSerial));
+    furi_record_close(RECORD_BT);
+}
+
+static void ble_hid_connection_status_callback(BtStatus status, void* context) {
+    furi_assert(context);
+    AirMouseApp* app = context;
+    bool connected = (status == BtStatusConnected);
+    air_mouse_view_set_connected_status(app->air_mouse_view, connected);
+}
+
+static Bt* ble_hid_init(AirMouseApp* app) {
+    Bt* bt = furi_record_open(RECORD_BT);
+    bt_disconnect(bt);
 
-    ImuThread* imu_thread;
-} SensorModuleApp;
+    // Wait 2nd core to update nvm storage
+    furi_delay_ms(200);
 
-static void render_callback(Canvas* canvas, void* ctx) {
-    UNUSED(ctx);
-    canvas_clear(canvas);
-    canvas_set_color(canvas, ColorBlack);
+    bt_keys_storage_set_storage_path(bt, BLE_HID_KEYS_PATH);
 
-    canvas_draw_icon(canvas, 64 + 14, 8, &I_Circles_47x47);
-    canvas_draw_icon(canvas, 83 + 14, 27, &I_Left_mouse_icon_9x9);
-    canvas_draw_icon(canvas, 83 + 14, 11, &I_Right_mouse_icon_9x9);
+    furi_check(bt_set_profile(bt, BtProfileHidKeyboard));
 
-    canvas_set_font(canvas, FontPrimary);
-    canvas_draw_str(canvas, 0, 14, "Air Mouse");
-    canvas_set_font(canvas, FontSecondary);
-    canvas_draw_str(canvas, 0, 56, "Press Back to exit");
+    furi_hal_bt_start_advertising();
+    bt_set_status_changed_callback(bt, ble_hid_connection_status_callback, app);
+    return bt;
 }
 
-static void input_callback(InputEvent* input_event, void* ctx) {
-    SensorModuleApp* app = ctx;
-    furi_message_queue_put(app->input_queue, input_event, 0);
+static void ble_hid_deinit(Bt* bt) {
+    bt_set_status_changed_callback(bt, NULL, NULL);
+    bt_disconnect(bt);
+
+    // Wait 2nd core to update nvm storage
+    furi_delay_ms(200);
+    bt_keys_storage_set_default_path(bt);
+
+    furi_check(bt_set_profile(bt, BtProfileSerial));
+    furi_record_close(RECORD_BT);
 }
 
-static SensorModuleApp* sensor_module_alloc(void) {
-    SensorModuleApp* app = malloc(sizeof(SensorModuleApp));
+static uint32_t air_mouse_exit(void* context) {
+    UNUSED(context);
+    return VIEW_NONE;
+}
 
-    app->icm42688p_device = malloc(sizeof(FuriHalSpiBusHandle));
-    memcpy(app->icm42688p_device, &furi_hal_spi_bus_handle_external, sizeof(FuriHalSpiBusHandle));
-    app->icm42688p_device->cs = &gpio_ext_pc3;
-    app->icm42688p = icm42688p_alloc(app->icm42688p_device, &gpio_ext_pb2);
-    app->icm42688p_valid = icm42688p_init(app->icm42688p);
+static uint32_t air_mouse_return_to_menu(void* context) {
+    UNUSED(context);
+    return AirMouseViewStartSubmenu;
+}
+
+static void air_mouse_hid_deinit(void* context) {
+    furi_assert(context);
+    AirMouseApp* app = context;
+
+    if(app->bt) {
+        ble_hid_deinit(app->bt);
+        app->bt = NULL;
+    } else if(app->usb_mode_prev) {
+        furi_hal_usb_set_config(app->usb_mode_prev, NULL);
+        app->usb_mode_prev = NULL;
+    }
+}
 
-    app->input_queue = furi_message_queue_alloc(8, sizeof(InputEvent));
+static void air_mouse_submenu_callback(void* context, uint32_t index) {
+    furi_assert(context);
+    AirMouseApp* app = context;
+    if(index == StartSubmenuIndexUsb) {
+        app->usb_mode_prev = furi_hal_usb_get_config();
+        furi_hal_usb_unlock();
+        furi_hal_usb_set_config(&usb_hid, NULL);
+
+        air_mouse_view_set_hid_api(app->air_mouse_view, &hid_api_usb, false);
+        view_dispatcher_switch_to_view(app->view_dispatcher, AirMouseViewMain);
+    } else if(index == StartSubmenuIndexBle) {
+        app->bt = ble_hid_init(app);
+
+        air_mouse_view_set_hid_api(app->air_mouse_view, &hid_api_ble, true);
+        view_dispatcher_switch_to_view(app->view_dispatcher, AirMouseViewMain);
+    } else if(index == StartSubmenuIndexBleReset) {
+        ble_hid_remove_pairing();
+    }
+}
 
-    app->view_port = view_port_alloc();
-    view_port_draw_callback_set(app->view_port, render_callback, app);
-    view_port_input_callback_set(app->view_port, input_callback, app);
+static AirMouseApp* air_mouse_alloc(void) {
+    AirMouseApp* app = malloc(sizeof(AirMouseApp));
 
     app->gui = furi_record_open(RECORD_GUI);
-    gui_add_view_port(app->gui, app->view_port, GuiLayerFullscreen);
+    app->view_dispatcher = view_dispatcher_alloc();
+    view_dispatcher_enable_queue(app->view_dispatcher);
+    view_dispatcher_attach_to_gui(app->view_dispatcher, app->gui, ViewDispatcherTypeFullscreen);
+
+    app->air_mouse_view = air_mouse_view_alloc(air_mouse_hid_deinit, app);
+    view_set_previous_callback(
+        air_mouse_view_get_view(app->air_mouse_view), air_mouse_return_to_menu);
+    view_dispatcher_add_view(
+        app->view_dispatcher, AirMouseViewMain, air_mouse_view_get_view(app->air_mouse_view));
+
+    app->start_submenu = submenu_alloc();
+    submenu_add_item(
+        app->start_submenu, "USB Remote", StartSubmenuIndexUsb, air_mouse_submenu_callback, app);
+    submenu_add_item(
+        app->start_submenu,
+        "Bluetooth Remote",
+        StartSubmenuIndexBle,
+        air_mouse_submenu_callback,
+        app);
+    submenu_add_item(
+        app->start_submenu,
+        "Remove Pairing",
+        StartSubmenuIndexBleReset,
+        air_mouse_submenu_callback,
+        app);
+    view_set_previous_callback(submenu_get_view(app->start_submenu), air_mouse_exit);
+    view_dispatcher_add_view(
+        app->view_dispatcher, AirMouseViewStartSubmenu, submenu_get_view(app->start_submenu));
+
+    app->error_dialog = dialog_ex_alloc();
+    dialog_ex_set_header(app->error_dialog, "Sensor Module error", 63, 0, AlignCenter, AlignTop);
+    dialog_ex_set_text(app->error_dialog, "Module not conntected", 63, 30, AlignCenter, AlignTop);
+    view_set_previous_callback(dialog_ex_get_view(app->error_dialog), air_mouse_exit);
+    view_dispatcher_add_view(
+        app->view_dispatcher, AirMouseViewError, dialog_ex_get_view(app->error_dialog));
 
     return app;
 }
 
-static void sensor_module_free(SensorModuleApp* app) {
-    gui_remove_view_port(app->gui, app->view_port);
-    furi_record_close(RECORD_GUI);
-    view_port_free(app->view_port);
+static void air_mouse_free(AirMouseApp* app) {
+    view_dispatcher_remove_view(app->view_dispatcher, AirMouseViewStartSubmenu);
+    submenu_free(app->start_submenu);
+    view_dispatcher_remove_view(app->view_dispatcher, AirMouseViewError);
+    dialog_ex_free(app->error_dialog);
+    view_dispatcher_remove_view(app->view_dispatcher, AirMouseViewMain);
+    air_mouse_view_free(app->air_mouse_view);
 
-    furi_message_queue_free(app->input_queue);
+    view_dispatcher_free(app->view_dispatcher);
 
-    if(app->imu_thread) {
-        imu_stop(app->imu_thread);
-        app->imu_thread = NULL;
-    }
-
-    if(!icm42688p_deinit(app->icm42688p)) {
-        FURI_LOG_E(TAG, "Failed to deinitialize ICM42688P");
-    }
-
-    icm42688p_free(app->icm42688p);
-    free(app->icm42688p_device);
+    furi_record_close(RECORD_GUI);
 
     free(app);
 }
 
 int32_t air_mouse_app(void* arg) {
     UNUSED(arg);
-    SensorModuleApp* app = sensor_module_alloc();
-
-    if(!app->icm42688p_valid) {
-        DialogsApp* dialogs = furi_record_open(RECORD_DIALOGS);
-        DialogMessage* message = dialog_message_alloc();
-        dialog_message_set_header(message, "Sensor Module error", 63, 0, AlignCenter, AlignTop);
+    AirMouseApp* app = air_mouse_alloc();
 
-        dialog_message_set_text(message, "Module not conntected", 63, 30, AlignCenter, AlignTop);
-        dialog_message_show(dialogs, message);
-        dialog_message_free(message);
-        furi_record_close(RECORD_DIALOGS);
+    app->icm42688p_device = malloc(sizeof(FuriHalSpiBusHandle));
+    memcpy(app->icm42688p_device, &furi_hal_spi_bus_handle_external, sizeof(FuriHalSpiBusHandle));
+    app->icm42688p_device->cs = &gpio_ext_pc3;
+    app->icm42688p = icm42688p_alloc(app->icm42688p_device, &gpio_ext_pb2);
+    bool icm42688p_valid = icm42688p_init(app->icm42688p);
 
-        sensor_module_free(app);
-        return 0;
+    if(icm42688p_valid) {
+        air_mouse_view_set_device(app->air_mouse_view, app->icm42688p);
+        view_dispatcher_switch_to_view(app->view_dispatcher, AirMouseViewStartSubmenu);
+    } else {
+        view_dispatcher_switch_to_view(app->view_dispatcher, AirMouseViewError);
     }
 
-    view_port_update(app->view_port);
-    app->imu_thread = imu_start(app->icm42688p);
-
-    while(1) {
-        InputEvent input;
-        if(furi_message_queue_get(app->input_queue, &input, FuriWaitForever) == FuriStatusOk) {
-            if((input.key == InputKeyBack) && (input.type == InputTypeShort)) {
-                break;
-            } else if(input.key == InputKeyOk) {
-                if(input.type == InputTypePress) {
-                    imu_mouse_key_press(app->imu_thread, ImuMouseKeyLeft, true);
-                } else if(input.type == InputTypeRelease) {
-                    imu_mouse_key_press(app->imu_thread, ImuMouseKeyLeft, false);
-                }
-            } else if(input.key == InputKeyUp) {
-                if(input.type == InputTypePress) {
-                    imu_mouse_key_press(app->imu_thread, ImuMouseKeyRight, true);
-                } else if(input.type == InputTypeRelease) {
-                    imu_mouse_key_press(app->imu_thread, ImuMouseKeyRight, false);
-                }
-            }
-        }
+    view_dispatcher_run(app->view_dispatcher);
+
+    if(!icm42688p_deinit(app->icm42688p)) {
+        FURI_LOG_E(TAG, "Failed to deinitialize ICM42688P");
     }
 
-    sensor_module_free(app);
+    icm42688p_free(app->icm42688p);
+    free(app->icm42688p_device);
+
+    air_mouse_free(app);
     return 0;
 }

+ 2 - 1
application.fam

@@ -1,6 +1,7 @@
 App(
-    appid="air_mouse",
+    appid="vgm_air_mouse",
     name="Air Mouse",
+    fap_description="Video Game Module Air Mouse",
     apptype=FlipperAppType.EXTERNAL,
     entry_point="air_mouse_app",
     stack_size=4 * 1024,

BIN
assets/Ble_connected_15x15.png


BIN
assets/Ble_disconnected_15x15.png


BIN
assets/Pressed_Button_13x13.png


BIN
assets/Scroll_icon_9x9.png


+ 58 - 20
imu_mouse.c

@@ -10,9 +10,9 @@
 #define FILTER_SAMPLE_FREQ 1000.f
 #define FILTER_BETA 0.08f
 
-#define HID_RATE_DIV 5
-
-#define SENSITIVITY_K 30.f
+#define SCROLL_RATE_DIV 50
+#define SCROLL_SENSITIVITY_K 0.25f
+#define MOUSE_SENSITIVITY_K 30.f
 #define EXP_RATE 1.1f
 
 #define IMU_CALI_AVG 64
@@ -24,11 +24,13 @@ typedef enum {
     ImuMouseRightRelease = (1 << 3),
     ImuMouseLeftPress = (1 << 4),
     ImuMouseLeftRelease = (1 << 5),
+    ImuMouseScrollOn = (1 << 6),
+    ImuMouseScrollOff = (1 << 7),
 } ImuThreadFlags;
 
 #define FLAGS_ALL                                                                 \
     (ImuMouseStop | ImuMouseNewData | ImuMouseRightPress | ImuMouseRightRelease | \
-     ImuMouseLeftPress | ImuMouseLeftRelease)
+     ImuMouseLeftPress | ImuMouseLeftRelease | ImuMouseScrollOn | ImuMouseScrollOff)
 
 typedef struct {
     float q0;
@@ -43,6 +45,7 @@ typedef struct {
 struct ImuThread {
     FuriThread* thread;
     ICM42688P* icm42688p;
+    const ImuHidApi* hid;
     ImuProcessedData processed_data;
 };
 
@@ -139,15 +142,17 @@ static int8_t mouse_exp_rate(float in) {
 static int32_t imu_thread(void* context) {
     furi_assert(context);
     ImuThread* imu = context;
+    furi_assert(imu->hid);
 
     float yaw_last = 0.f;
     float pitch_last = 0.f;
+    float scroll_pitch = 0.f;
     float diff_x = 0.f;
     float diff_y = 0.f;
     uint32_t sample_cnt = 0;
+    uint32_t hid_rate_div = FILTER_SAMPLE_FREQ / imu->hid->report_rate_max;
 
-    FuriHalUsbInterface* usb_mode_prev = furi_hal_usb_get_config();
-    furi_hal_usb_set_config(&usb_hid, NULL);
+    bool scroll_mode = false;
 
     calibrate_gyro(imu);
 
@@ -168,16 +173,23 @@ static int32_t imu_thread(void* context) {
         }
 
         if(events & ImuMouseRightPress) {
-            furi_hal_hid_mouse_press(HID_MOUSE_BTN_RIGHT);
+            imu->hid->mouse_key_press(HID_MOUSE_BTN_RIGHT);
         }
         if(events & ImuMouseRightRelease) {
-            furi_hal_hid_mouse_release(HID_MOUSE_BTN_RIGHT);
+            imu->hid->mouse_key_release(HID_MOUSE_BTN_RIGHT);
         }
         if(events & ImuMouseLeftPress) {
-            furi_hal_hid_mouse_press(HID_MOUSE_BTN_LEFT);
+            imu->hid->mouse_key_press(HID_MOUSE_BTN_LEFT);
         }
         if(events & ImuMouseLeftRelease) {
-            furi_hal_hid_mouse_release(HID_MOUSE_BTN_LEFT);
+            imu->hid->mouse_key_release(HID_MOUSE_BTN_LEFT);
+        }
+        if(events & ImuMouseScrollOn) {
+            scroll_pitch = pitch_last;
+            scroll_mode = true;
+        }
+        if(events & ImuMouseScrollOff) {
+            scroll_mode = false;
         }
 
         if(events & ImuMouseNewData) {
@@ -187,23 +199,43 @@ static int32_t imu_thread(void* context) {
                 icm42688_fifo_read(imu->icm42688p, &data);
                 imu_process_data(imu, &data);
 
-                if((imu->processed_data.pitch > -75.f) && (imu->processed_data.pitch < 75.f) &&
-                   (isfinite(imu->processed_data.pitch) != 0)) {
-                    diff_x += imu_angle_diff(yaw_last, imu->processed_data.yaw) * SENSITIVITY_K;
-                    diff_y +=
-                        imu_angle_diff(pitch_last, -imu->processed_data.pitch) * SENSITIVITY_K;
+                if((imu->processed_data.pitch < -75.f) || (imu->processed_data.pitch > 75.f) ||
+                   (isfinite(imu->processed_data.pitch) == 0)) {
+                    continue;
+                }
 
+                if(scroll_mode) {
                     yaw_last = imu->processed_data.yaw;
                     pitch_last = -imu->processed_data.pitch;
 
                     sample_cnt++;
-                    if(sample_cnt >= HID_RATE_DIV) {
+                    if(sample_cnt >= SCROLL_RATE_DIV) {
+                        sample_cnt = 0;
+
+                        float scroll_speed =
+                            -imu_angle_diff(scroll_pitch, -imu->processed_data.pitch) *
+                            SCROLL_SENSITIVITY_K;
+                        scroll_speed = CLAMP(scroll_speed, 127.f, -127.f);
+
+                        imu->hid->mouse_scroll(scroll_speed);
+                    }
+                } else {
+                    diff_x +=
+                        imu_angle_diff(yaw_last, imu->processed_data.yaw) * MOUSE_SENSITIVITY_K;
+                    diff_y += imu_angle_diff(pitch_last, -imu->processed_data.pitch) *
+                              MOUSE_SENSITIVITY_K;
+
+                    yaw_last = imu->processed_data.yaw;
+                    pitch_last = -imu->processed_data.pitch;
+
+                    sample_cnt++;
+                    if(sample_cnt >= hid_rate_div) {
                         sample_cnt = 0;
 
                         float mouse_x = CLAMP(diff_x, 127.f, -127.f);
                         float mouse_y = CLAMP(diff_y, 127.f, -127.f);
 
-                        furi_hal_hid_mouse_move(mouse_exp_rate(mouse_x), mouse_exp_rate(mouse_y));
+                        imu->hid->mouse_move(mouse_exp_rate(mouse_x), mouse_exp_rate(mouse_y));
 
                         diff_x -= (float)(int8_t)mouse_x;
                         diff_y -= (float)(int8_t)mouse_y;
@@ -213,8 +245,7 @@ static int32_t imu_thread(void* context) {
         }
     }
 
-    furi_hal_hid_mouse_release(HID_MOUSE_BTN_RIGHT | HID_MOUSE_BTN_LEFT);
-    furi_hal_usb_set_config(usb_mode_prev, NULL);
+    imu->hid->mouse_key_release(HID_MOUSE_BTN_RIGHT | HID_MOUSE_BTN_LEFT);
 
     icm42688_fifo_disable(imu->icm42688p);
 
@@ -233,9 +264,16 @@ void imu_mouse_key_press(ImuThread* imu, ImuMouseKey key, bool state) {
     furi_thread_flags_set(furi_thread_get_id(imu->thread), flag);
 }
 
-ImuThread* imu_start(ICM42688P* icm42688p) {
+void imu_mouse_scroll_mode(ImuThread* imu, bool enable) {
+    furi_assert(imu);
+    uint32_t flag = (enable) ? (ImuMouseScrollOn) : (ImuMouseScrollOff);
+    furi_thread_flags_set(furi_thread_get_id(imu->thread), flag);
+}
+
+ImuThread* imu_start(ICM42688P* icm42688p, const ImuHidApi* hid) {
     ImuThread* imu = malloc(sizeof(ImuThread));
     imu->icm42688p = icm42688p;
+    imu->hid = hid;
     imu->thread = furi_thread_alloc_ex("ImuThread", 4096, imu_thread, imu);
     furi_thread_start(imu->thread);
 

+ 11 - 1
imu_mouse.h

@@ -2,6 +2,14 @@
 
 #include "sensors/ICM42688P.h"
 
+typedef struct {
+    bool (*mouse_move)(int8_t dx, int8_t dy);
+    bool (*mouse_key_press)(uint8_t button);
+    bool (*mouse_key_release)(uint8_t button);
+    bool (*mouse_scroll)(int8_t value);
+    uint32_t report_rate_max;
+} ImuHidApi;
+
 typedef enum {
     ImuMouseKeyRight,
     ImuMouseKeyLeft,
@@ -9,8 +17,10 @@ typedef enum {
 
 typedef struct ImuThread ImuThread;
 
-ImuThread* imu_start(ICM42688P* icm42688p);
+ImuThread* imu_start(ICM42688P* icm42688p, const ImuHidApi* hid);
 
 void imu_stop(ImuThread* imu);
 
 void imu_mouse_key_press(ImuThread* imu, ImuMouseKey key, bool state);
+
+void imu_mouse_scroll_mode(ImuThread* imu, bool enable);

+ 174 - 0
views/air_mouse_view.c

@@ -0,0 +1,174 @@
+#include "air_mouse_view.h"
+#include <gui/elements.h>
+#include "../imu_mouse.h"
+#include "vgm_air_mouse_icons.h"
+
+struct AirMouseView {
+    View* view;
+    void* imu_device;
+    ImuThread* imu;
+    const ImuHidApi* hid_api;
+    AirMouseViewExit exit_callback;
+    void* context;
+};
+
+typedef struct {
+    bool left_pressed;
+    bool up_pressed;
+    bool ok_pressed;
+    bool connected;
+    bool show_ble_icon;
+} AirMouseModel;
+
+static void air_mouse_view_draw_callback(Canvas* canvas, void* context) {
+    furi_assert(context);
+    AirMouseModel* model = context;
+
+    canvas_set_color(canvas, ColorBlack);
+
+    if(model->show_ble_icon) {
+        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_draw_icon(canvas, 78, 8, &I_Circles_47x47);
+
+    if(model->ok_pressed) {
+        canvas_set_bitmap_mode(canvas, 1);
+        canvas_draw_icon(canvas, 95, 25, &I_Pressed_Button_13x13);
+        canvas_set_bitmap_mode(canvas, 0);
+        canvas_set_color(canvas, ColorWhite);
+    }
+    canvas_draw_icon(canvas, 97, 27, &I_Left_mouse_icon_9x9);
+    canvas_set_color(canvas, ColorBlack);
+
+    if(model->up_pressed) {
+        canvas_set_bitmap_mode(canvas, 1);
+        canvas_draw_icon(canvas, 95, 9, &I_Pressed_Button_13x13);
+        canvas_set_bitmap_mode(canvas, 0);
+        canvas_set_color(canvas, ColorWhite);
+    }
+    canvas_draw_icon(canvas, 97, 11, &I_Right_mouse_icon_9x9);
+    canvas_set_color(canvas, ColorBlack);
+
+    if(model->left_pressed) {
+        canvas_set_bitmap_mode(canvas, 1);
+        canvas_draw_icon(canvas, 79, 25, &I_Pressed_Button_13x13);
+        canvas_set_bitmap_mode(canvas, 0);
+        canvas_set_color(canvas, ColorWhite);
+    }
+    canvas_draw_icon(canvas, 81, 27, &I_Scroll_icon_9x9);
+    canvas_set_color(canvas, ColorBlack);
+
+    canvas_set_font(canvas, FontPrimary);
+    canvas_draw_str(canvas, 17, 12, "Air Mouse");
+    canvas_set_font(canvas, FontSecondary);
+    canvas_draw_str(canvas, 0, 56, "Press Back to exit");
+}
+
+static bool air_mouse_view_input_callback(InputEvent* event, void* context) {
+    furi_assert(context);
+    AirMouseView* air_mouse = context;
+    bool consumed = false;
+
+    if(event->key == InputKeyOk) {
+        if((event->type == InputTypePress) || (event->type == InputTypeRelease)) {
+            bool state = (event->type == InputTypePress);
+            imu_mouse_key_press(air_mouse->imu, ImuMouseKeyLeft, state);
+            with_view_model(
+                air_mouse->view, AirMouseModel * model, { model->ok_pressed = state; }, true);
+        }
+        consumed = true;
+    } else if(event->key == InputKeyUp) {
+        if((event->type == InputTypePress) || (event->type == InputTypeRelease)) {
+            bool state = (event->type == InputTypePress);
+            imu_mouse_key_press(air_mouse->imu, ImuMouseKeyRight, state);
+            with_view_model(
+                air_mouse->view, AirMouseModel * model, { model->up_pressed = state; }, true);
+        }
+        consumed = true;
+    } else if(event->key == InputKeyLeft) {
+        if((event->type == InputTypePress) || (event->type == InputTypeRelease)) {
+            bool state = (event->type == InputTypePress);
+            imu_mouse_scroll_mode(air_mouse->imu, state);
+            with_view_model(
+                air_mouse->view, AirMouseModel * model, { model->left_pressed = state; }, true);
+        }
+        consumed = true;
+    }
+
+    return consumed;
+}
+
+static void air_mouse_view_enter(void* context) {
+    furi_assert(context);
+    AirMouseView* air_mouse = context;
+    furi_assert(air_mouse->imu == NULL);
+    air_mouse->imu = imu_start(air_mouse->imu_device, air_mouse->hid_api);
+}
+
+static void air_mouse_view_exit(void* context) {
+    furi_assert(context);
+    AirMouseView* air_mouse = context;
+    imu_stop(air_mouse->imu);
+    air_mouse->imu = NULL;
+    if(air_mouse->exit_callback) {
+        air_mouse->exit_callback(air_mouse->context);
+    }
+}
+
+AirMouseView* air_mouse_view_alloc(AirMouseViewExit exit_callback, void* context) {
+    AirMouseView* air_mouse = malloc(sizeof(AirMouseView));
+    air_mouse->view = view_alloc();
+    air_mouse->exit_callback = exit_callback;
+    air_mouse->context = context;
+    view_set_context(air_mouse->view, air_mouse);
+    view_allocate_model(air_mouse->view, ViewModelTypeLocking, sizeof(AirMouseModel));
+    view_set_draw_callback(air_mouse->view, air_mouse_view_draw_callback);
+    view_set_input_callback(air_mouse->view, air_mouse_view_input_callback);
+    view_set_enter_callback(air_mouse->view, air_mouse_view_enter);
+    view_set_exit_callback(air_mouse->view, air_mouse_view_exit);
+
+    with_view_model(
+        air_mouse->view, AirMouseModel * model, { model->connected = true; }, true);
+
+    return air_mouse;
+}
+
+void air_mouse_view_free(AirMouseView* air_mouse) {
+    furi_assert(air_mouse);
+    view_free(air_mouse->view);
+    free(air_mouse);
+}
+
+View* air_mouse_view_get_view(AirMouseView* air_mouse) {
+    furi_assert(air_mouse);
+    return air_mouse->view;
+}
+
+void air_mouse_view_set_device(AirMouseView* air_mouse, void* imu_device) {
+    furi_assert(air_mouse);
+    air_mouse->imu_device = imu_device;
+}
+
+void air_mouse_view_set_hid_api(
+    AirMouseView* air_mouse,
+    const ImuHidApi* hid,
+    bool is_ble_interface) {
+    furi_assert(air_mouse);
+    air_mouse->hid_api = hid;
+    with_view_model(
+        air_mouse->view,
+        AirMouseModel * model,
+        { model->show_ble_icon = is_ble_interface; },
+        false);
+}
+
+void air_mouse_view_set_connected_status(AirMouseView* air_mouse, bool connected) {
+    furi_assert(air_mouse);
+    with_view_model(
+        air_mouse->view, AirMouseModel * model, { model->connected = connected; }, true);
+}

+ 23 - 0
views/air_mouse_view.h

@@ -0,0 +1,23 @@
+#pragma once
+
+#include <gui/view.h>
+#include "../imu_mouse.h"
+
+typedef void (*AirMouseViewExit)(void* context);
+
+typedef struct AirMouseView AirMouseView;
+
+AirMouseView* air_mouse_view_alloc(AirMouseViewExit exit_callback, void* context);
+
+void air_mouse_view_free(AirMouseView* air_mouse);
+
+View* air_mouse_view_get_view(AirMouseView* air_mouse);
+
+void air_mouse_view_set_hid_api(
+    AirMouseView* air_mouse,
+    const ImuHidApi* hid,
+    bool is_ble_interface);
+
+void air_mouse_view_set_device(AirMouseView* air_mouse, void* imu_device);
+
+void air_mouse_view_set_connected_status(AirMouseView* air_mouse, bool connected);