Procházet zdrojové kódy

Add usb_consumer_control from https://github.com/xMasterX/all-the-plugins

git-subtree-dir: usb_consumer_control
git-subtree-mainline: 30d0df5152fa66c7cabdc0f65a8d4e4e01489541
git-subtree-split: acaec816000ae557881289dda2490ff0901094b6
Willy-JL před 1 rokem
rodič
revize
986af356d7

+ 52 - 0
usb_consumer_control/.gitignore

@@ -0,0 +1,52 @@
+# Prerequisites
+*.d
+
+# Object files
+*.o
+*.ko
+*.obj
+*.elf
+
+# Linker output
+*.ilk
+*.map
+*.exp
+
+# Precompiled Headers
+*.gch
+*.pch
+
+# Libraries
+*.lib
+*.a
+*.la
+*.lo
+
+# Shared objects (inc. Windows DLLs)
+*.dll
+*.so
+*.so.*
+*.dylib
+
+# Executables
+*.exe
+*.out
+*.app
+*.i*86
+*.x86_64
+*.hex
+
+# Debug files
+*.dSYM/
+*.su
+*.idb
+*.pdb
+
+# Kernel Module Compile Results
+*.mod*
+*.cmd
+.tmp_versions/
+modules.order
+Module.symvers
+Mkfile.old
+dkms.conf

+ 1 - 0
usb_consumer_control/.gitsubtree

@@ -0,0 +1 @@
+https://github.com/xMasterX/all-the-plugins dev non_catalog_apps/usb_consumer_control

+ 201 - 0
usb_consumer_control/LICENSE

@@ -0,0 +1,201 @@
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.

+ 9 - 0
usb_consumer_control/README.md

@@ -0,0 +1,9 @@
+# USB Consumer Control
+This is a Flipper Zero application for sending Consumer Control Button (CCB) presses as a USB HID device. This is useful for researching how various devices handle USB CCBs, for example in the context of kiosk breakouts. 
+
+A more detailed writeup of the topic can be found [here](https://github.com/piraija/usb-hid-and-run).
+
+# Credits
+Project built by [Erik Alm](https://www.linkedin.com/in/erik-alm).
+
+Inspiration drawn from [FlipperZeroUSBKeyboard](https://github.com/huuck/FlipperZeroUSBKeyboard) and [usb_hid_autofire](https://github.com/pbek/usb_hid_autofire).

+ 12 - 0
usb_consumer_control/application.fam

@@ -0,0 +1,12 @@
+App(
+    appid="usb_ccb",
+    name="USB Consumer Control",
+    apptype=FlipperAppType.EXTERNAL,
+    entry_point="usb_ccb_app",
+    stack_size=1 * 1024,
+    fap_author="piraija",
+    fap_category="USB",
+    fap_description="USB Consumer Control",
+    fap_icon="usb_ccb_10px.png",
+    fap_version="1.0", 
+)

+ 18 - 0
usb_consumer_control/docs/building.md

@@ -0,0 +1,18 @@
+# Building
+
+```shell
+# Clone Flipper Zero firmware
+git clone --recursive https://github.com/flipperdevices/flipperzero-firmware.git
+
+# Clone application
+cd firmware/applications_user && git clone https://github.com/WithSecureLabs/usb-consumer-control.git
+
+# Install toolchain
+cd .. && ./fbt
+
+# Build application
+./fbt fap_usb_ccb
+
+# Copy built application to your SD card
+cp build/f7-firmware-D/.extapps/usb_ccb.fap <SD Card root>/apps/usb/ 
+```

+ 3 - 0
usb_consumer_control/docs/changelog.md

@@ -0,0 +1,3 @@
+## v.1.0
+
+Initial release.

binární
usb_consumer_control/screenshots/1.png


binární
usb_consumer_control/screenshots/2.png


+ 149 - 0
usb_consumer_control/usb_ccb.c

@@ -0,0 +1,149 @@
+#include "usb_ccb.h"
+#include <furi.h>
+#include <furi_hal.h>
+#include <notification/notification_messages.h>
+
+#define TAG "UsbCcbApp"
+
+enum UsbDebugSubmenuIndex {
+    UsbCcbSubmenuIndexAbout,
+    UsbCcbSubmenuIndexHelp,
+    UsbCcbSubmenuIndexStart,
+};
+
+void usb_ccb_submenu_callback(void* context, uint32_t index) {
+    furi_assert(context);
+    UsbCcb* app = context;
+    if(index == UsbCcbSubmenuIndexAbout) {
+        app->view_id = UsbCcbViewAbout;
+        view_dispatcher_switch_to_view(app->view_dispatcher, UsbCcbViewAbout);
+    } else if(index == UsbCcbSubmenuIndexHelp) {
+        app->view_id = UsbCcbViewHelp;
+        view_dispatcher_switch_to_view(app->view_dispatcher, UsbCcbViewHelp);
+    } else if(index == UsbCcbSubmenuIndexStart) {
+        app->view_id = UsbCcbViewStart;
+        view_dispatcher_switch_to_view(app->view_dispatcher, UsbCcbViewStart);
+    }
+}
+
+void usb_ccb_dialog_callback(DialogExResult result, void* context) {
+    furi_assert(context);
+    UsbCcb* app = context;
+    if(result == DialogExResultLeft) {
+        view_dispatcher_stop(app->view_dispatcher);
+    } else if(result == DialogExResultRight) {
+        view_dispatcher_switch_to_view(app->view_dispatcher, app->view_id); // Show last view
+    } else if(result == DialogExResultCenter) {
+        view_dispatcher_switch_to_view(app->view_dispatcher, UsbCcbViewSubmenu);
+    }
+}
+
+uint32_t usb_ccb_exit_confirm_view(void* context) {
+    UNUSED(context);
+    return UsbCcbViewExitConfirm;
+}
+
+uint32_t usb_ccb_exit(void* context) {
+    UNUSED(context);
+    return VIEW_NONE;
+}
+
+UsbCcb* usb_ccb_app_alloc() {
+    UsbCcb* app = malloc(sizeof(UsbCcb));
+
+    // Gui
+    app->gui = furi_record_open(RECORD_GUI);
+
+    // Notifications
+    app->notifications = furi_record_open(RECORD_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_set_header(app->submenu, "USB Consumer Control");
+    submenu_add_item(app->submenu, "About", UsbCcbSubmenuIndexAbout, usb_ccb_submenu_callback, app);
+    submenu_add_item(app->submenu, "Help", UsbCcbSubmenuIndexHelp, usb_ccb_submenu_callback, app);
+    submenu_add_item(app->submenu, "Start", UsbCcbSubmenuIndexStart, usb_ccb_submenu_callback, app);
+    view_set_previous_callback(submenu_get_view(app->submenu), usb_ccb_exit);
+    view_dispatcher_add_view(app->view_dispatcher, UsbCcbViewSubmenu, submenu_get_view(app->submenu));
+
+    // Dialog view
+    app->dialog = dialog_ex_alloc();
+    dialog_ex_set_result_callback(app->dialog, usb_ccb_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_center_button_text(app->dialog, "Menu");
+    dialog_ex_set_header(app->dialog, "Exit or return to menu?", 64, 11, AlignCenter, AlignTop);
+    view_dispatcher_add_view(app->view_dispatcher, UsbCcbViewExitConfirm, dialog_ex_get_view(app->dialog));
+
+    // About view
+    app->usb_ccb_about = usb_ccb_about_alloc();
+    view_set_previous_callback(usb_ccb_about_get_view(app->usb_ccb_about), usb_ccb_exit_confirm_view);
+    view_dispatcher_add_view(app->view_dispatcher, UsbCcbViewAbout, usb_ccb_about_get_view(app->usb_ccb_about));
+
+    // Help view
+    app->usb_ccb_help = usb_ccb_help_alloc();
+    view_set_previous_callback(usb_ccb_help_get_view(app->usb_ccb_help), usb_ccb_exit_confirm_view);
+    view_dispatcher_add_view(app->view_dispatcher, UsbCcbViewHelp, usb_ccb_help_get_view(app->usb_ccb_help));
+
+    // Start view
+    app->usb_ccb_start = usb_ccb_start_alloc();
+    view_set_previous_callback(usb_ccb_start_get_view(app->usb_ccb_start), usb_ccb_exit_confirm_view);
+    view_dispatcher_add_view(app->view_dispatcher, UsbCcbViewStart, usb_ccb_start_get_view(app->usb_ccb_start));
+
+    app->view_id = UsbCcbViewSubmenu;
+    view_dispatcher_switch_to_view(app->view_dispatcher, app->view_id);
+
+    return app;
+}
+
+void usb_ccb_app_free(UsbCcb* app) {
+    furi_assert(app);
+
+    // Reset notification
+    notification_internal_message(app->notifications, &sequence_reset_blue);
+
+    // Free views
+    view_dispatcher_remove_view(app->view_dispatcher, UsbCcbViewSubmenu);
+    submenu_free(app->submenu);
+    view_dispatcher_remove_view(app->view_dispatcher, UsbCcbViewExitConfirm);
+    dialog_ex_free(app->dialog);
+    view_dispatcher_remove_view(app->view_dispatcher, UsbCcbViewAbout);
+    usb_ccb_about_free(app->usb_ccb_about);
+    view_dispatcher_remove_view(app->view_dispatcher, UsbCcbViewHelp);
+    usb_ccb_help_free(app->usb_ccb_help);
+    view_dispatcher_remove_view(app->view_dispatcher, UsbCcbViewStart);
+    usb_ccb_start_free(app->usb_ccb_start);
+    view_dispatcher_free(app->view_dispatcher);
+    // Close records
+    furi_record_close(RECORD_GUI);
+    app->gui = NULL;
+    furi_record_close(RECORD_NOTIFICATION);
+    app->notifications = NULL;
+
+    // Free rest
+    free(app);
+}
+
+int32_t usb_ccb_app(void* p) {
+    UNUSED(p);
+    // Switch profile to Hid
+    UsbCcb* app = usb_ccb_app_alloc();
+
+    FuriHalUsbInterface* usb_mode_prev = furi_hal_usb_get_config();
+    furi_hal_usb_unlock();
+    furi_check(furi_hal_usb_set_config(&usb_hid, NULL) == true);
+
+    view_dispatcher_run(app->view_dispatcher);
+
+    // Change back profile
+    furi_hal_usb_set_config(usb_mode_prev, NULL);
+    usb_ccb_app_free(app);
+
+    return 0;
+}

+ 33 - 0
usb_consumer_control/usb_ccb.h

@@ -0,0 +1,33 @@
+#pragma once
+
+#include <furi.h>
+#include <gui/gui.h>
+#include <gui/view.h>
+#include <gui/view_dispatcher.h>
+#include <notification/notification.h>
+
+#include <gui/modules/submenu.h>
+#include <gui/modules/dialog_ex.h>
+#include "views/usb_ccb_about.h"
+#include "views/usb_ccb_help.h"
+#include "views/usb_ccb_start.h"
+
+typedef struct {
+    Gui* gui;
+    NotificationApp* notifications;
+    ViewDispatcher* view_dispatcher;
+    Submenu* submenu;
+    DialogEx* dialog;
+    UsbCcbAbout* usb_ccb_about;
+    UsbCcbHelp* usb_ccb_help;
+    UsbCcbStart* usb_ccb_start;
+    uint32_t view_id;
+} UsbCcb;
+
+typedef enum {
+    UsbCcbViewSubmenu,
+    UsbCcbViewAbout,
+    UsbCcbViewHelp,
+    UsbCcbViewStart,
+    UsbCcbViewExitConfirm,
+} UsbCcbView;

binární
usb_consumer_control/usb_ccb_10px.png


+ 68 - 0
usb_consumer_control/views/usb_ccb_about.c

@@ -0,0 +1,68 @@
+#include "usb_ccb_about.h"
+#include <furi.h>
+#include <furi_hal_usb_hid.h>
+#include <gui/elements.h>
+
+struct UsbCcbAbout {
+    View* view;
+};
+
+typedef struct {
+    bool left_pressed;
+    bool up_pressed;
+    bool right_pressed;
+    bool down_pressed;
+    bool ok_pressed;
+    bool back_pressed;
+    bool connected;
+} UsbCcbAboutModel;
+
+static void usb_ccb_about_draw_callback(Canvas* canvas, void* context) {
+    furi_assert(context);
+
+    canvas_set_font(canvas, FontSecondary);
+    canvas_draw_str(canvas, 0, 7, "A tool to send USB CCBs,");
+    canvas_draw_str(canvas, 0, 16, "to launch & control apps.");
+    canvas_draw_str(canvas, 0, 28, "For security research.");
+    canvas_draw_str(canvas, 0, 37, "Only use with permission.");
+    canvas_draw_str(canvas, 0, 49, "More info at: github.com");
+    canvas_draw_str(canvas, 0, 58, "/piraija/usb-hid-and-run");
+}
+
+static bool usb_ccb_about_input_callback(InputEvent* event, void* context) {
+    furi_assert(context);
+    bool consumed = false;
+
+    if(event->type == InputTypeLong && event->key == InputKeyBack) {
+        furi_hal_hid_kb_release_all();
+    } 
+
+    return consumed;
+}
+
+UsbCcbAbout* usb_ccb_about_alloc() {
+    UsbCcbAbout* usb_ccb_about = malloc(sizeof(UsbCcbAbout));
+    usb_ccb_about->view = view_alloc();
+    view_set_context(usb_ccb_about->view, usb_ccb_about);
+    view_allocate_model(usb_ccb_about->view, ViewModelTypeLocking, sizeof(UsbCcbAboutModel));
+    view_set_draw_callback(usb_ccb_about->view, usb_ccb_about_draw_callback);
+    view_set_input_callback(usb_ccb_about->view, usb_ccb_about_input_callback);
+
+    return usb_ccb_about;
+}
+
+void usb_ccb_about_free(UsbCcbAbout* usb_ccb_about) {
+    furi_assert(usb_ccb_about);
+    view_free(usb_ccb_about->view);
+    free(usb_ccb_about);
+}
+
+View* usb_ccb_about_get_view(UsbCcbAbout* usb_ccb_about) {
+    furi_assert(usb_ccb_about);
+    return usb_ccb_about->view;
+}
+
+void usb_ccb_about_set_connected_status(UsbCcbAbout* usb_ccb_about, bool connected) {
+    furi_assert(usb_ccb_about);
+    with_view_model(usb_ccb_about->view, UsbCcbAboutModel * model, { model->connected = connected; }, true);
+}

+ 13 - 0
usb_consumer_control/views/usb_ccb_about.h

@@ -0,0 +1,13 @@
+#pragma once
+
+#include <gui/view.h>
+
+typedef struct UsbCcbAbout UsbCcbAbout;
+
+UsbCcbAbout* usb_ccb_about_alloc();
+
+void usb_ccb_about_free(UsbCcbAbout* usb_ccb_about);
+
+View* usb_ccb_about_get_view(UsbCcbAbout* usb_ccb_about);
+
+void usb_ccb_about_set_connected_status(UsbCcbAbout* usb_ccb_about, bool connected);

+ 69 - 0
usb_consumer_control/views/usb_ccb_help.c

@@ -0,0 +1,69 @@
+#include "usb_ccb_help.h"
+#include <furi.h>
+#include <furi_hal_usb_hid.h>
+#include <gui/elements.h>
+
+struct UsbCcbHelp {
+    View* view;
+};
+
+typedef struct {
+    bool left_pressed;
+    bool up_pressed;
+    bool right_pressed;
+    bool down_pressed;
+    bool ok_pressed;
+    bool back_pressed;
+    bool connected;
+} UsbCcbHelpModel;
+
+static void usb_ccb_help_draw_callback(Canvas* canvas, void* context) {
+    furi_assert(context);
+
+    canvas_set_font(canvas, FontSecondary);
+    canvas_draw_str(canvas, 0, 8, "Exit to menu: hold [back].");
+    canvas_draw_str(canvas, 0, 18, "Adjust delay between keys:");
+    canvas_draw_str(canvas, 0, 26, "push [up] or [down].");
+    canvas_draw_str(canvas, 0, 36, "Cycle between keys:");
+    canvas_draw_str(canvas, 0, 44, "push/hold [left] or [right].");
+    canvas_draw_str(canvas, 0, 54, "Toggle sending keys: push [ok].");
+    canvas_draw_str(canvas, 0, 62, "Send one key: double click [ok].");
+}
+
+static bool usb_ccb_help_input_callback(InputEvent* event, void* context) {
+    furi_assert(context);
+    bool consumed = false;
+
+    if(event->type == InputTypeLong && event->key == InputKeyBack) {
+        furi_hal_hid_kb_release_all();
+    } 
+
+    return consumed;
+}
+
+UsbCcbHelp* usb_ccb_help_alloc() {
+    UsbCcbHelp* usb_ccb_help = malloc(sizeof(UsbCcbHelp));
+    usb_ccb_help->view = view_alloc();
+    view_set_context(usb_ccb_help->view, usb_ccb_help);
+    view_allocate_model(usb_ccb_help->view, ViewModelTypeLocking, sizeof(UsbCcbHelpModel));
+    view_set_draw_callback(usb_ccb_help->view, usb_ccb_help_draw_callback);
+    view_set_input_callback(usb_ccb_help->view, usb_ccb_help_input_callback);
+
+    return usb_ccb_help;
+}
+
+void usb_ccb_help_free(UsbCcbHelp* usb_ccb_help) {
+    furi_assert(usb_ccb_help);
+    view_free(usb_ccb_help->view);
+    free(usb_ccb_help);
+}
+
+View* usb_ccb_help_get_view(UsbCcbHelp* usb_ccb_help) {
+    furi_assert(usb_ccb_help);
+    return usb_ccb_help->view;
+}
+
+void usb_ccb_help_set_connected_status(UsbCcbHelp* usb_ccb_help, bool connected) {
+    furi_assert(usb_ccb_help);
+    with_view_model(usb_ccb_help->view, UsbCcbHelpModel * model, { model->connected = connected; }, true);
+}

+ 13 - 0
usb_consumer_control/views/usb_ccb_help.h

@@ -0,0 +1,13 @@
+#pragma once
+
+#include <gui/view.h>
+
+typedef struct UsbCcbHelp UsbCcbHelp;
+
+UsbCcbHelp* usb_ccb_help_alloc();
+
+void usb_ccb_help_free(UsbCcbHelp* usb_ccb_help);
+
+View* usb_ccb_help_get_view(UsbCcbHelp* usb_ccb_help);
+
+void usb_ccb_help_set_connected_status(UsbCcbHelp* usb_ccb_help, bool connected);

+ 686 - 0
usb_consumer_control/views/usb_ccb_start.c

@@ -0,0 +1,686 @@
+#include "usb_ccb_start.h"
+#include <furi.h>
+#include <furi_hal_usb_hid.h>
+#include <gui/elements.h>
+
+struct UsbCcbStart {
+    View* view;
+};
+
+typedef struct {
+    bool left_pressed;
+    bool up_pressed;
+    bool right_pressed;
+    bool down_pressed;
+    bool ok_pressed;
+    bool back_pressed;
+    bool connected;
+} UsbCcbStartModel;
+
+typedef struct {
+    uint16_t value;
+    const char* name;
+} HID_CONSUMER;
+
+// All of the strings are prefixed with "HID_CONSUMER_", which has been removed to save some space.
+HID_CONSUMER hidConsumerArray[] = {
+    {0x00, "UNASSIGNED"}, // Generic Consumer Control Device
+    {0x01, "CONTROL"},
+    {0x02, "NUMERIC_KEY_PAD"},
+    {0x03, "PROGRAMMABLE_BUTTONS"},
+    {0x04, "MICROPHONE"},
+    {0x05, "HEADPHONE"},
+    {0x06, "GRAPHIC_EQUALIZER"},
+    {0x20, "PLUS_10"}, // Numeric Key Pad
+    {0x21, "PLUS_100"},
+    {0x22, "AM_PM"},
+    {0x30, "POWER"}, // General Controls
+    {0x31, "RESET"},
+    {0x32, "SLEEP"},
+    {0x33, "SLEEP_AFTER"},
+    {0x34, "SLEEP_MODE"},
+    {0x35, "ILLUMINATION"},
+    {0x36, "FUNCTION_BUTTONS"},
+    {0x40, "MENU"}, // Menu Controls
+    {0x41, "MENU_PICK"},
+    {0x42, "MENU_UP"},
+    {0x43, "MENU_DOWN"},
+    {0x44, "MENU_LEFT"},
+    {0x45, "MENU_RIGHT"},
+    {0x46, "MENU_ESCAPE"},
+    {0x47, "MENU_VALUE_INCREASE"},
+    {0x48, "MENU_VALUE_DECREASE"},
+    {0x60, "DATA_ON_SCREEN"}, // Display Controls
+    {0x61, "CLOSED_CAPTION"},
+    {0x62, "CLOSED_CAPTION_SELECT"},
+    {0x63, "VCR_TV"},
+    {0x64, "BROADCAST_MODE"},
+    {0x65, "SNAPSHOT"},
+    {0x66, "STILL"},
+    {0x80, "SELECTION"}, // Selection Controls
+    {0x81, "ASSIGN_SELECTION"},
+    {0x82, "MODE_STEP"},
+    {0x83, "RECALL_LAST"},
+    {0x84, "ENTER_CHANNEL"},
+    {0x85, "ORDER_MOVIE"},
+    {0x86, "CHANNEL"},
+    {0x87, "MEDIA_SELECTION"},
+    {0x88, "MEDIA_SELECT_COMPUTER"},
+    {0x89, "MEDIA_SELECT_TV"},
+    {0x8A, "MEDIA_SELECT_WWW"},
+    {0x8B, "MEDIA_SELECT_DVD"},
+    {0x8C, "MEDIA_SELECT_TELEPHONE"},
+    {0x8D, "MEDIA_SELECT_PROGRAM_GUIDE"},
+    {0x8E, "MEDIA_SELECT_VIDEO_PHONE"},
+    {0x8F, "MEDIA_SELECT_GAMES"},
+    {0x90, "MEDIA_SELECT_MESSAGES"},
+    {0x91, "MEDIA_SELECT_CD"},
+    {0x92, "MEDIA_SELECT_VCR"},
+    {0x93, "MEDIA_SELECT_TUNER"},
+    {0x94, "QUIT"},
+    {0x95, "HELP"},
+    {0x96, "MEDIA_SELECT_TAPE"},
+    {0x97, "MEDIA_SELECT_CABLE"},
+    {0x98, "MEDIA_SELECT_SATELLITE"},
+    {0x99, "MEDIA_SELECT_SECURITY"},
+    {0x9A, "MEDIA_SELECT_HOME"},
+    {0x9B, "MEDIA_SELECT_CALL"},
+    {0x9C, "CHANNEL_INCREMENT"},
+    {0x9D, "CHANNEL_DECREMENT"},
+    {0x9E, "MEDIA_SELECT_SAP"},
+    {0xA0, "VCR_PLUS"},
+    {0xA1, "ONCE"},
+    {0xA2, "DAILY"},
+    {0xA3, "WEEKLY"},
+    {0xA4, "MONTHLY"},
+    {0xB0, "PLAY"}, // Transport Controls
+    {0xB1, "PAUSE"},
+    {0xB2, "RECORD"},
+    {0xB3, "FAST_FORWARD"},
+    {0xB4, "REWIND"},
+    {0xB5, "SCAN_NEXT_TRACK"},
+    {0xB6, "SCAN_PREVIOUS_TRACK"},
+    {0xB7, "STOP"},
+    {0xB8, "EJECT"},
+    {0xB9, "RANDOM_PLAY"},
+    {0xBA, "SELECT_DISC"},
+    {0xBB, "ENTER_DISC"},
+    {0xBC, "REPEAT"},
+    {0xBD, "TRACKING"},
+    {0xBE, "TRACK_NORMAL"},
+    {0xBF, "SLOW_TRACKING"},
+    {0xC0, "FRAME_FORWARD"},
+    {0xC1, "FRAME_BACK"},
+    {0xC2, "MARK"}, // Search Controls
+    {0xC3, "CLEAR_MARK"},
+    {0xC4, "REPEAT_FROM_MARK"},
+    {0xC5, "RETURN_TO_MARK"},
+    {0xC6, "SEARCH_MARK_FORWARD"},
+    {0xC7, "SEARCH_MARK_BACKWARDS"},
+    {0xC8, "COUNTER_RESET"},
+    {0xC9, "SHOW_COUNTER"},
+    {0xCA, "TRACKING_INCREMENT"},
+    {0xCB, "TRACKING_DECREMENT"},
+    {0xCC, "STOP_EJECT"},
+    {0xCD, "PLAY_PAUSE"},
+    {0xCE, "PLAY_SKIP"},
+    {0xE0, "VOLUME"}, // Audio Controls
+    {0xE1, "BALANCE"},
+    {0xE2, "MUTE"},
+    {0xE3, "BASS"},
+    {0xE4, "TREBLE"},
+    {0xE5, "BASS_BOOST"},
+    {0xE6, "SURROUND_MODE"},
+    {0xE7, "LOUDNESS"},
+    {0xE8, "MPX"},
+    {0xE9, "VOLUME_INCREMENT"},
+    {0xEA, "VOLUME_DECREMENT"},
+    {0xF0, "SPEED_SELECT"}, // Speed Controls
+    {0xF1, "PLAYBACK_SPEED"},
+    {0xF2, "STANDARD_PLAY"},
+    {0xF3, "LONG_PLAY"},
+    {0xF4, "EXTENDED_PLAY"},
+    {0xF5, "SLOW"},
+    {0x100, "FAN_ENABLE"}, // Home and Security Controls
+    {0x101, "FAN_SPEED"},
+    {0x102, "LIGHT_ENABLE"},
+    {0x103, "LIGHT_ILLUMINATION_LEVEL"},
+    {0x104, "CLIMATE_CONTROL_ENABLE"},
+    {0x105, "ROOM_TEMPERATURE"},
+    {0x106, "SECURITY_ENABLE"},
+    {0x107, "FIRE_ALARM"},
+    {0x108, "POLICE_ALARM"},
+    {0x109, "PROXIMITY"},
+    {0x10A, "MOTION"},
+    {0x10B, "DURESS_ALARM"},
+    {0x10C, "HOLDUP_ALARM"},
+    {0x10D, "MEDICAL_ALARM"},
+    {0x150, "BALANCE_RIGHT"}, // Speaker Channels
+    {0x151, "BALANCE_LEFT"},
+    {0x152, "BASS_INCREMENT"},
+    {0x153, "BASS_DECREMENT"},
+    {0x154, "TREBLE_INCREMENT"},
+    {0x155, "TREBLE_DECREMENT"},
+    {0x160, "SPEAKER_SYSTEM"},
+    {0x161, "CHANNEL_LEFT"},
+    {0x162, "CHANNEL_RIGHT"},
+    {0x163, "CHANNEL_CENTER"},
+    {0x164, "CHANNEL_FRONT"},
+    {0x165, "CHANNEL_CENTER_FRONT"},
+    {0x166, "CHANNEL_SIDE"},
+    {0x167, "CHANNEL_SURROUND"},
+    {0x168, "CHANNEL_LOW_FREQUENCY_ENHANCEMENT"},
+    {0x169, "CHANNEL_TOP"},
+    {0x16A, "CHANNEL_UNKNOWN"},
+    {0x170, "SUB_CHANNEL"}, // PC Theatre
+    {0x171, "SUB_CHANNEL_INCREMENT"},
+    {0x172, "SUB_CHANNEL_DECREMENT"},
+    {0x173, "ALTERNATE_AUDIO_INCREMENT"},
+    {0x174, "ALTERNATE_AUDIO_DECREMENT"},
+    {0x180, "APPLICATION_LAUNCH_BUTTONS"}, // Application Launch Buttons
+    {0x181, "AL_LAUNCH_BUTTON_CONFIGURATION_TOOL"},
+    {0x182, "AL_PROGRAMMABLE_BUTTON_CONFIGURATION"},
+    {0x183, "AL_CONSUMER_CONTROL_CONFIGURATION"},
+    {0x184, "AL_WORD_PROCESSOR"},
+    {0x185, "AL_TEXT_EDITOR"},
+    {0x186, "AL_SPREADSHEET"},
+    {0x187, "AL_GRAPHICS_EDITOR"},
+    {0x188, "AL_PRESENTATION_APP"},
+    {0x189, "AL_DATABASE_APP"},
+    {0x18A, "AL_EMAIL_READER"},
+    {0x18B, "AL_NEWSREADER"},
+    {0x18C, "AL_VOICEMAIL"},
+    {0x18D, "AL_CONTACTS_ADDRESS_BOOK"},
+    {0x18E, "AL_CALENDAR_SCHEDULE"},
+    {0x18F, "AL_TASK_PROJECT_MANAGER"},
+    {0x190, "AL_LOG_JOURNAL_TIMECARD"},
+    {0x191, "AL_CHECKBOOK_FINANCE"},
+    {0x192, "AL_CALCULATOR"},
+    {0x193, "AL_A_V_CAPTURE_PLAYBACK"},
+    {0x194, "AL_LOCAL_MACHINE_BROWSER"},
+    {0x195, "AL_LAN_WAN_BROWSER"},
+    {0x196, "AL_INTERNET_BROWSER"},
+    {0x197, "AL_REMOTE_NETWORKING_ISP_CONNECT"},
+    {0x198, "AL_NETWORK_CONFERENCE"},
+    {0x199, "AL_NETWORK_CHAT"},
+    {0x19A, "AL_TELEPHONY_DIALER"},
+    {0x19B, "AL_LOGON"},
+    {0x19C, "AL_LOGOFF"},
+    {0x19D, "AL_LOGON_LOGOFF"},
+    {0x19E, "AL_TERMINAL_LOCK_SCREENSAVER"},
+    {0x19F, "AL_CONTROL_PANEL"},
+    {0x1A0, "AL_COMMAND_LINE_PROCESSOR_RUN"},
+    {0x1A1, "AL_PROCESS_TASK_MANAGER"},
+    {0x1A2, "AL_SELECT_TASK_APPLICATION"},
+    {0x1A3, "AL_NEXT_TASK_APPLICATION"},
+    {0x1A4, "AL_PREVIOUS_TASK_APPLICATION"},
+    {0x1A5, "AL_PREEMPTIVE_HALT_TASK_APPLICATION"},
+    {0x1A6, "AL_INTEGRATED_HELP_CENTER"},
+    {0x1A7, "AL_DOCUMENTS"},
+    {0x1A8, "AL_THESAURUS"},
+    {0x1A9, "AL_DICTIONARY"},
+    {0x1AA, "AL_DESKTOP"},
+    {0x1AB, "AL_SPELL_CHECK"},
+    {0x1AC, "AL_GRAMMAR_CHECK"},
+    {0x1AD, "AL_WIRELESS_STATUS"},
+    {0x1AE, "AL_KEYBOARD_LAYOUT"},
+    {0x1AF, "AL_VIRUS_PROTECTION"},
+    {0x1B0, "AL_ENCRYPTION"},
+    {0x1B1, "AL_SCREEN_SAVER"},
+    {0x1B2, "AL_ALARMS"},
+    {0x1B3, "AL_CLOCK"},
+    {0x1B4, "AL_FILE_BROWSER"},
+    {0x1B5, "AL_POWER_STATUS"},
+    {0x1B6, "AL_IMAGE_BROWSER"},
+    {0x1B7, "AL_AUDIO_BROWSER"},
+    {0x1B8, "AL_MOVIE_BROWSER"},
+    {0x1B9, "AL_DIGITAL_RIGHTS_MANAGER"},
+    {0x1BA, "AL_DIGITAL_WALLET"},
+    {0x1BC, "AL_INSTANT_MESSAGING"},
+    {0x1BD, "AL_OEM_FEATURES_TIPS_TUTORIAL_BROWSER"},
+    {0x1BE, "AL_OEM_HELP"},
+    {0x1BF, "AL_ONLINE_COMMUNITY"},
+    {0x1C0, "AL_ENTERTAINMENT_CONTENT_BROWSER"},
+    {0x1C1, "AL_ONLINE_SHOPPING_BROWSER"},
+    {0x1C2, "AL_SMARTCARD_INFORMATION_HELP"},
+    {0x1C3, "AL_MARKET_MONITOR_FINANCE_BROWSER"},
+    {0x1C4, "AL_CUSTOMIZED_CORPORATE_NEWS_BROWSER"},
+    {0x1C5, "AL_ONLINE_ACTIVITY_BROWSER"},
+    {0x1C6, "AL_RESEARCH_SEARCH_BROWSER"},
+    {0x1C7, "AL_AUDIO_PLAYER"},
+    {0x200, "GENERIC_GUI_APPLICATION_CONTROLS"}, // Generic GUI Application Controls
+    {0x201, "AC_NEW"},
+    {0x202, "AC_OPEN"},
+    {0x203, "AC_CLOSE"},
+    {0x204, "AC_EXIT"},
+    {0x205, "AC_MAXIMIZE"},
+    {0x206, "AC_MINIMIZE"},
+    {0x207, "AC_SAVE"},
+    {0x208, "AC_PRINT"},
+    {0x209, "AC_PROPERTIES"},
+    {0x21A, "AC_UNDO"},
+    {0x21B, "AC_COPY"},
+    {0x21C, "AC_CUT"},
+    {0x21D, "AC_PASTE"},
+    {0x21E, "AC_SELECT_ALL"},
+    {0x21F, "AC_FIND"},
+    {0x220, "AC_FIND_AND_REPLACE"},
+    {0x221, "AC_SEARCH"},
+    {0x222, "AC_GO_TO"},
+    {0x223, "AC_HOME"},
+    {0x224, "AC_BACK"},
+    {0x225, "AC_FORWARD"},
+    {0x226, "AC_STOP"},
+    {0x227, "AC_REFRESH"},
+    {0x228, "AC_PREVIOUS_LINK"},
+    {0x229, "AC_NEXT_LINK"},
+    {0x22A, "AC_BOOKMARKS"},
+    {0x22B, "AC_HISTORY"},
+    {0x22C, "AC_SUBSCRIPTIONS"},
+    {0x22D, "AC_ZOOM_IN"},
+    {0x22E, "AC_ZOOM_OUT"},
+    {0x22F, "AC_ZOOM"},
+    {0x230, "AC_FULL_SCREEN_VIEW"},
+    {0x231, "AC_NORMAL_VIEW"},
+    {0x232, "AC_VIEW_TOGGLE"},
+    {0x233, "AC_SCROLL_UP"},
+    {0x234, "AC_SCROLL_DOWN"},
+    {0x235, "AC_SCROLL"},
+    {0x236, "AC_PAN_LEFT"},
+    {0x237, "AC_PAN_RIGHT"},
+    {0x238, "AC_PAN"},
+    {0x239, "AC_NEW_WINDOW"},
+    {0x23A, "AC_TILE_HORIZONTALLY"},
+    {0x23B, "AC_TILE_VERTICALLY"},
+    {0x23C, "AC_FORMAT"},
+    {0x23D, "AC_EDIT"},
+    {0x23E, "AC_BOLD"},
+    {0x23F, "AC_ITALICS"},
+    {0x240, "AC_UNDERLINE"},
+    {0x241, "AC_STRIKETHROUGH"},
+    {0x242, "AC_SUBSCRIPT"},
+    {0x243, "AC_SUPERSCRIPT"},
+    {0x244, "AC_ALL_CAPS"},
+    {0x245, "AC_ROTATE"},
+    {0x246, "AC_RESIZE"},
+    {0x247, "AC_FLIP_HORIZONTAL"},
+    {0x248, "AC_FLIP_VERTICAL"},
+    {0x249, "AC_MIRROR_HORIZONTAL"},
+    {0x24A, "AC_MIRROR_VERTICAL"},
+    {0x24B, "AC_FONT_SELECT"},
+    {0x24C, "AC_FONT_COLOR"},
+    {0x24D, "AC_FONT_SIZE"},
+    {0x24E, "AC_JUSTIFY_LEFT"},
+    {0x24F, "AC_JUSTIFY_CENTER_H"},
+    {0x250, "AC_JUSTIFY_RIGHT"},
+    {0x251, "AC_JUSTIFY_BLOCK_H"},
+    {0x252, "AC_JUSTIFY_TOP"},
+    {0x253, "AC_JUSTIFY_CENTER_V"},
+    {0x254, "AC_JUSTIFY_BOTTOM"},
+    {0x255, "AC_JUSTIFY_BLOCK_V"},
+    {0x256, "AC_INDENT_DECREASE"},
+    {0x257, "AC_INDENT_INCREASE"},
+    {0x258, "AC_NUMBERED_LIST"},
+    {0x259, "AC_RESTART_NUMBERING"},
+    {0x25A, "AC_BULLETED_LIST"},
+    {0x25B, "AC_PROMOTE"},
+    {0x25C, "AC_DEMOTE"},
+    {0x25D, "AC_YES"},
+    {0x25E, "AC_NO"},
+    {0x25F, "AC_CANCEL"},
+    {0x260, "AC_CATALOG"},
+    {0x261, "AC_BUY_CHECKOUT"},
+    {0x262, "AC_ADD_TO_CART"},
+    {0x263, "AC_EXPAND"},
+    {0x264, "AC_EXPAND_ALL"},
+    {0x265, "AC_COLLAPSE"},
+    {0x266, "AC_COLLAPSE_ALL"},
+    {0x267, "AC_PRINT_PREVIEW"},
+    {0x268, "AC_PASTE_SPECIAL"},
+    {0x269, "AC_INSERT_MODE"},
+    {0x26A, "AC_DELETE"},
+    {0x26B, "AC_LOCK"},
+    {0x26C, "AC_UNLOCK"},
+    {0x26D, "AC_PROTECT"},
+    {0x26E, "AC_UNPROTECT"},
+    {0x26F, "AC_ATTACH_COMMENT"},
+    {0x270, "AC_DELETE_COMMENT"},
+    {0x271, "AC_VIEW_COMMENT"},
+    {0x272, "AC_SELECT_WORD"},
+    {0x273, "AC_SELECT_SENTENCE"},
+    {0x274, "AC_SELECT_PARAGRAPH"},
+    {0x275, "AC_SELECT_COLUMN"},
+    {0x276, "AC_SELECT_ROW"},
+    {0x277, "AC_SELECT_TABLE"},
+    {0x278, "AC_SELECT_OBJECT"},
+    {0x279, "AC_REDO_REPEAT"},
+    {0x27A, "AC_SORT"},
+    {0x27B, "AC_SORT_ASCENDING"},
+    {0x27C, "AC_SORT_DESCENDING"},
+    {0x27D, "AC_FILTER"},
+    {0x27E, "AC_SET_CLOCK"},
+    {0x27F, "AC_VIEW_CLOCK"},
+    {0x280, "AC_SELECT_TIME_ZONE"},
+    {0x281, "AC_EDIT_TIME_ZONES"},
+    {0x282, "AC_SET_ALARM"},
+    {0x283, "AC_CLEAR_ALARM"},
+    {0x284, "AC_SNOOZE_ALARM"},
+    {0x285, "AC_RESET_ALARM"},
+    {0x286, "AC_SYNCHRONIZE"},
+    {0x287, "AC_SEND_RECEIVE"},
+    {0x288, "AC_SEND_TO"},
+    {0x289, "AC_REPLY"},
+    {0x28A, "AC_REPLY_ALL"},
+    {0x28B, "AC_FORWARD_MSG"},
+    {0x28C, "AC_SEND"},
+    {0x28D, "AC_ATTACH_FILE"},
+    {0x28E, "AC_UPLOAD"},
+    {0x28F, "AC_DOWNLOAD_SAVE_TARGET_AS"},
+    {0x290, "AC_SET_BORDERS"},
+    {0x291, "AC_INSERT_ROW"},
+    {0x292, "AC_INSERT_COLUMN"},
+    {0x293, "AC_INSERT_FILE"},
+    {0x294, "AC_INSERT_PICTURE"},
+    {0x295, "AC_INSERT_OBJECT"},
+    {0x296, "AC_INSERT_SYMBOL"},
+    {0x297, "AC_SAVE_AND_CLOSE"},
+    {0x298, "AC_RENAME"},
+    {0x299, "AC_MERGE"},
+    {0x29A, "AC_SPLIT"},
+    {0x29B, "AC_DISRIBUTE_HORIZONTALLY"},
+    {0x29C, "AC_DISTRIBUTE_VERTICALLY"}
+};
+
+const int hidConsumerArraySize = sizeof(hidConsumerArray) / sizeof(hidConsumerArray[0]);
+
+void strrev(char* arr, int start, int end) {
+    char temp;
+
+    if (start >= end)
+        return;
+
+    temp = *(arr + start);
+    *(arr + start) = *(arr + end);
+    *(arr + end) = temp;
+
+    start++;
+    end--;
+    strrev(arr, start, end);
+}
+
+char *itoa(int number, char *arr, int base){
+    int i = 0, r, negative = 0;
+
+    if (number == 0)
+    {
+        arr[i] = '0';
+        arr[i + 1] = '\0';
+        return arr;
+    }
+
+    if (number < 0 && base == 10)
+    {
+        number *= -1;
+        negative = 1;
+    }
+
+    while (number != 0)
+    {
+        r = number % base;
+        arr[i] = (r > 9) ? (r - 10) + 'a' : r + '0';
+        i++;
+        number /= base;
+    }
+
+    if (negative)
+    {
+        arr[i] = '-';
+        i++;
+    }
+
+    strrev(arr, 0, i - 1);
+
+    arr[i] = '\0';
+
+    return arr;
+}
+
+// Function to convert a single hex digit to its character representation
+char hexDigitToChar(uint8_t digit) {
+    if (digit < 10) {
+        return '0' + digit;
+    } else {
+        return 'A' + (digit - 10);
+    }
+}
+
+// Function to convert a uint16_t value to its hex string representation
+void uint16ToHexString(uint16_t value, char* hexString) {
+    hexString[0] = '0';
+    hexString[1] = 'x';
+    int startIndex = 2;
+    for (int i = 3; i >= 0; --i) {
+        uint8_t digit = (value >> (i * 4)) & 0xF;
+        if (digit != 0 || startIndex != 2) {
+            hexString[startIndex++] = hexDigitToChar(digit);
+        }
+    }
+    if (startIndex == 2) { // If there are no non-zero digits, display at least "0x0"
+        hexString[startIndex++] = '0';
+    }
+    hexString[startIndex] = '\0';
+}
+
+bool is_running = false;
+uint32_t autofire_delay = 1000;
+char autofire_delay_str[12];
+char hexString[7];
+
+// Start at the Application Launch Buttons by default, since that's the most interesting
+int i = 153;
+int currentSubsetIndex = 13;
+
+// Array to store indexes of different CCB subsets
+// The idea would be to try using these different subsets against different types of devices
+int hidConsumerSubsets[] = { 
+    0, // Generic Consumer Control Device
+    7, // Numeric Key Pad
+    10, // General Controls
+    17, // Menu Controls
+    26, // Display Controls
+    33, // Selection Controls
+    69, // Transport Controls (nice)
+    87, // Search Controls
+    100, // Audio Controls
+    111, // Speed Controls
+    117, // Home and Security Controls
+    131, // Speaker Channels
+    148, // PC Theatre
+    153, // Application Launch Buttons
+    224 // Generic GUI Application Controls
+};
+
+const int hidConsumerSubsetsSize = sizeof(hidConsumerSubsets) / sizeof(hidConsumerSubsets[0]);
+
+const char* getConsumerSubsetName(int i) {
+    if (i >= 0 && i < 7) {
+        return "Generic Consumer Control";
+    } else if (i >= 7 && i < 10) {
+        return "Numeric Key Pad";
+    } else if (i >= 10 && i < 17) {
+        return "General Controls";
+    } else if (i >= 17 && i < 26) {
+        return "Menu Controls";
+    } else if (i >= 26 && i < 33) {
+        return "Display Controls";
+    } else if (i >= 33 && i < 69) {
+        return "Selection Controls";
+    } else if (i >= 69 && i < 87) {
+        return "Transport Controls";
+    } else if (i >= 87 && i < 100) {
+        return "Search Controls";
+    } else if (i >= 100 && i < 111) {
+        return "Audio Controls";
+    } else if (i >= 111 && i < 117) {
+        return "Speed Controls";
+    } else if (i >= 117 && i < 131) {
+        return "Home and Security Controls";
+    } else if (i >= 131 && i < 148) {
+        return "Speaker Channels";
+    } else if (i >= 148 && i < 153) {
+        return "PC Theatre";
+    } else if (i >= 153 && i < 224) {
+        return "Application Launch Buttons";
+    } else if (i >= 224) {
+        return "GUI Application Controls";
+    }
+    // Won't ever happen though
+    return "Invalid Index";
+}
+
+// This is the main program loop
+static void usb_ccb_start_draw_callback(Canvas* canvas, void* context) {
+    furi_assert(context);
+    itoa(autofire_delay, autofire_delay_str, 10);
+    uint16ToHexString(hidConsumerArray[i].value, hexString);
+
+    canvas_clear(canvas);
+
+    canvas_set_font(canvas, FontPrimary);
+    canvas_draw_str(canvas, 0, 10, is_running ? "Running" : "Not running");
+
+    canvas_set_font(canvas, FontSecondary);
+    canvas_draw_str(canvas, 0, 24, "Delay [ms]: ");
+    canvas_draw_str(canvas, 50, 24, autofire_delay_str);
+
+    canvas_draw_str(canvas, 0, 38, "Current key subset:");
+    canvas_draw_str(canvas, 0, 46, getConsumerSubsetName(i));
+
+    canvas_draw_str(canvas, 0, 56, is_running ? "Sent:                HID_CONSUMER_" : "Next:                HID_CONSUMER_");
+    canvas_draw_str(canvas, 24, 56, hexString);
+    canvas_draw_str(canvas, 0, 64, hidConsumerArray[i].name);
+
+    if(is_running) {
+        uint16_t consumer_key = hidConsumerArray[i].value;
+        // Sending the consumer control button
+        furi_delay_us(autofire_delay * 500);
+        furi_hal_hid_consumer_key_press(consumer_key);
+        furi_delay_us(2000); // Hold the key pressed for a short amount of time
+        
+        // Stop sending the consumer control button
+        furi_hal_hid_consumer_key_release(consumer_key);
+        furi_delay_us(autofire_delay * 500);
+
+        // Cycle onto next consumer control button
+        i += 1;
+
+        // Stop once we've cycled all consumer control buttons
+        if(i == hidConsumerArraySize){
+            i = 0; 
+            is_running = false;
+        }
+    }
+}
+
+// This function is the controller
+static void usb_ccb_start_process(UsbCcbStart* usb_ccb_start, InputEvent* event) {
+    with_view_model(
+        usb_ccb_start->view,
+        UsbCcbStartModel * model,
+        {
+            if (event->type == InputTypeLong) {
+                if (event->key == InputKeyRight) {
+                    model->right_pressed = true;
+                    currentSubsetIndex = (currentSubsetIndex + 1) % hidConsumerSubsetsSize;
+                    i = hidConsumerSubsets[currentSubsetIndex];
+                } else if (event->key == InputKeyLeft) {
+                    model->left_pressed = true;
+                    currentSubsetIndex = (currentSubsetIndex - 1 + hidConsumerSubsetsSize) % hidConsumerSubsetsSize;
+                    i = hidConsumerSubsets[currentSubsetIndex];
+                }
+            } else if(event->type == InputTypePress) {
+                if(event->key == InputKeyRight) {
+                    model->right_pressed = true;
+                    i = (i + 1) % hidConsumerArraySize;
+                } else if(event->key == InputKeyLeft) {
+                    model->left_pressed = true;
+                    i = (i - 1 + hidConsumerArraySize) % hidConsumerArraySize;
+                } else if(event->key == InputKeyDown) {
+                    model->down_pressed = true;
+                    if(autofire_delay > 0) {
+                            autofire_delay -= 100;
+                    }
+                } else if(event->key == InputKeyUp) {
+                    model->up_pressed = true;
+                    autofire_delay += 100;
+                } else if(event->key == InputKeyOk) {
+                    model->ok_pressed = true;
+                    is_running = !is_running;
+                } else if(event->key == InputKeyBack) {
+                    model->back_pressed = true;
+                }
+            } else if(event->type == InputTypeRelease) {
+                if(event->key == InputKeyUp) {
+                    model->up_pressed = false;
+                } else if(event->key == InputKeyDown) {
+                    model->down_pressed = false;
+                } else if(event->key == InputKeyLeft) {
+                    model->left_pressed = false;
+                } else if(event->key == InputKeyRight) {
+                    model->right_pressed = false;
+                } else if(event->key == InputKeyOk) {
+                    model->ok_pressed = false;
+                } else if(event->key == InputKeyBack) {
+                    model->back_pressed = false;
+                }
+            } else if(event->type == InputTypeShort) {
+                if(event->key == InputKeyBack) {
+                }
+            }
+            
+        },
+        true);
+}
+
+static bool usb_ccb_start_input_callback(InputEvent* event, void* context) {
+    furi_assert(context);
+    UsbCcbStart* usb_ccb_start = context;
+    bool consumed = false;
+
+    if(event->type == InputTypeLong && event->key == InputKeyBack) {
+        furi_hal_hid_kb_release_all();
+    } else {
+        usb_ccb_start_process(usb_ccb_start, event);
+        consumed = true;
+    }
+
+    return consumed;
+}
+
+UsbCcbStart* usb_ccb_start_alloc() {
+    UsbCcbStart* usb_ccb_start = malloc(sizeof(UsbCcbStart));
+    usb_ccb_start->view = view_alloc();
+    view_set_context(usb_ccb_start->view, usb_ccb_start);
+    view_allocate_model(usb_ccb_start->view, ViewModelTypeLocking, sizeof(UsbCcbStartModel));
+    view_set_draw_callback(usb_ccb_start->view, usb_ccb_start_draw_callback);
+    view_set_input_callback(usb_ccb_start->view, usb_ccb_start_input_callback);
+
+    return usb_ccb_start;
+}
+
+void usb_ccb_start_free(UsbCcbStart* usb_ccb_start) {
+    furi_assert(usb_ccb_start);
+    view_free(usb_ccb_start->view);
+    free(usb_ccb_start);
+    i = 0;
+}
+
+View* usb_ccb_start_get_view(UsbCcbStart* usb_ccb_start) {
+    furi_assert(usb_ccb_start);
+    return usb_ccb_start->view;
+}
+
+void usb_ccb_start_set_connected_status(UsbCcbStart* usb_ccb_start, bool connected) {
+    furi_assert(usb_ccb_start);
+    with_view_model(usb_ccb_start->view, UsbCcbStartModel * model, { model->connected = connected; }, true);
+}

+ 13 - 0
usb_consumer_control/views/usb_ccb_start.h

@@ -0,0 +1,13 @@
+#pragma once
+
+#include <gui/view.h>
+
+typedef struct UsbCcbStart UsbCcbStart;
+
+UsbCcbStart* usb_ccb_start_alloc();
+
+void usb_ccb_start_free(UsbCcbStart* usb_ccb_start);
+
+View* usb_ccb_start_get_view(UsbCcbStart* usb_ccb_start);
+
+void usb_ccb_start_set_connected_status(UsbCcbStart* usb_ccb_start, bool connected);