瀏覽代碼

replace badbt with badkb

remove ID/BTID lock, allow using all scripts on every type of connection
move app to Tools folder
remake settings to allow compiling and using on UL
use badusb folder for scripts and assets, use badkb in root for settings and btkeys
MX 2 年之前
父節點
當前提交
3d5cbb3eef
共有 47 個文件被更改,包括 3741 次插入2300 次删除
  1. 0 16
      base_pack/bad_bt/application.fam
  2. 0 333
      base_pack/bad_bt/bad_bt_app.c
  3. 0 39
      base_pack/bad_bt/bad_bt_app.h
  4. 0 792
      base_pack/bad_bt/helpers/ducky_script.c
  5. 0 154
      base_pack/bad_bt/helpers/ducky_script.h
  6. 0 201
      base_pack/bad_bt/helpers/ducky_script_commands.c
  7. 0 44
      base_pack/bad_bt/helpers/ducky_script_i.h
  8. 0 30
      base_pack/bad_bt/scenes/bad_bt_scene.c
  9. 0 104
      base_pack/bad_bt/scenes/bad_bt_scene_config.c
  10. 0 7
      base_pack/bad_bt/scenes/bad_bt_scene_config.h
  11. 0 47
      base_pack/bad_bt/scenes/bad_bt_scene_config_layout.c
  12. 0 52
      base_pack/bad_bt/scenes/bad_bt_scene_config_mac.c
  13. 0 45
      base_pack/bad_bt/scenes/bad_bt_scene_config_name.c
  14. 0 61
      base_pack/bad_bt/scenes/bad_bt_scene_error.c
  15. 0 49
      base_pack/bad_bt/scenes/bad_bt_scene_file_select.c
  16. 0 56
      base_pack/bad_bt/scenes/bad_bt_scene_work.c
  17. 0 233
      base_pack/bad_bt/views/bad_bt_view.c
  18. 0 29
      base_pack/bad_bt/views/bad_bt_view.h
  19. 11 0
      base_pack/bad_kb/application.fam
  20. 440 0
      base_pack/bad_kb/bad_kb_app.c
  21. 11 0
      base_pack/bad_kb/bad_kb_app.h
  22. 113 0
      base_pack/bad_kb/bad_kb_app_i.h
  23. 9 0
      base_pack/bad_kb/bad_kb_paths.h
  24. 416 0
      base_pack/bad_kb/helpers/ble_hid.c
  25. 107 0
      base_pack/bad_kb/helpers/ble_hid.h
  26. 320 0
      base_pack/bad_kb/helpers/ble_hid_svc.c
  27. 29 0
      base_pack/bad_kb/helpers/ble_hid_svc.h
  28. 935 0
      base_pack/bad_kb/helpers/ducky_script.c
  29. 86 0
      base_pack/bad_kb/helpers/ducky_script.h
  30. 215 0
      base_pack/bad_kb/helpers/ducky_script_commands.c
  31. 73 0
      base_pack/bad_kb/helpers/ducky_script_i.h
  32. 1 0
      base_pack/bad_kb/helpers/ducky_script_keycodes.c
  33. 0 0
      base_pack/bad_kb/icon.png
  34. 30 0
      base_pack/bad_kb/scenes/bad_kb_scene.c
  35. 8 8
      base_pack/bad_kb/scenes/bad_kb_scene.h
  36. 186 0
      base_pack/bad_kb/scenes/bad_kb_scene_config.c
  37. 9 0
      base_pack/bad_kb/scenes/bad_kb_scene_config.h
  38. 50 0
      base_pack/bad_kb/scenes/bad_kb_scene_config_bt_mac.c
  39. 47 0
      base_pack/bad_kb/scenes/bad_kb_scene_config_bt_name.c
  40. 48 0
      base_pack/bad_kb/scenes/bad_kb_scene_config_layout.c
  41. 62 0
      base_pack/bad_kb/scenes/bad_kb_scene_config_usb_name.c
  42. 50 0
      base_pack/bad_kb/scenes/bad_kb_scene_config_usb_vidpid.c
  43. 49 0
      base_pack/bad_kb/scenes/bad_kb_scene_error.c
  44. 55 0
      base_pack/bad_kb/scenes/bad_kb_scene_file_select.c
  45. 59 0
      base_pack/bad_kb/scenes/bad_kb_scene_work.c
  46. 299 0
      base_pack/bad_kb/views/bad_kb_view.c
  47. 23 0
      base_pack/bad_kb/views/bad_kb_view.h

+ 0 - 16
base_pack/bad_bt/application.fam

@@ -1,16 +0,0 @@
-App(
-    appid="bad_bt",
-    name="Bad BT",
-    apptype=FlipperAppType.EXTERNAL,
-    entry_point="bad_bt_app",
-    requires=[
-        "gui",
-        "dialogs",
-    ],
-    stack_size=2 * 1024,
-    order=70,
-    fap_libs=["assets"],
-    fap_category="Bluetooth",
-    fap_icon="images/badbt_10px.png",
-    fap_icon_assets="images",
-)

+ 0 - 333
base_pack/bad_bt/bad_bt_app.c

@@ -1,333 +0,0 @@
-#include "bad_bt_app.h"
-#include <furi.h>
-#include <furi_hal.h>
-#include <storage/storage.h>
-#include <lib/toolbox/path.h>
-#include <lib/flipper_format/flipper_format.h>
-
-#include <bt/bt_service/bt_i.h>
-#include <bt/bt_service/bt.h>
-
-#define BAD_BT_SETTINGS_FILE_NAME ".badbt.settings"
-#define BAD_BT_APP_PATH_BOUND_KEYS_FOLDER EXT_PATH("badbt")
-#define BAD_BT_APP_PATH_BOUND_KEYS_FILE BAD_BT_APP_PATH_BOUND_KEYS_FOLDER "/.badbt.keys"
-
-#define BAD_BT_SETTINGS_PATH BAD_BT_APP_BASE_CONFIG_FOLDER "/" BAD_BT_SETTINGS_FILE_NAME
-
-static bool bad_bt_app_custom_event_callback(void* context, uint32_t event) {
-    furi_assert(context);
-    BadBtApp* app = context;
-    return scene_manager_handle_custom_event(app->scene_manager, event);
-}
-
-static bool bad_bt_app_back_event_callback(void* context) {
-    furi_assert(context);
-    BadBtApp* app = context;
-    return scene_manager_handle_back_event(app->scene_manager);
-}
-
-static void bad_bt_app_tick_event_callback(void* context) {
-    furi_assert(context);
-    BadBtApp* app = context;
-    scene_manager_handle_tick_event(app->scene_manager);
-}
-
-static void bad_bt_load_settings(BadBtApp* app) {
-    furi_string_reset(app->keyboard_layout);
-    strcpy(app->config.bt_name, "");
-    memcpy(
-        app->config.bt_mac,
-        furi_hal_bt_get_profile_mac_addr(FuriHalBtProfileHidKeyboard),
-        BAD_BT_MAC_ADDRESS_LEN);
-
-    Storage* storage = furi_record_open(RECORD_STORAGE);
-    FlipperFormat* file = flipper_format_file_alloc(storage);
-    if(flipper_format_file_open_existing(file, BAD_BT_SETTINGS_PATH)) {
-        FuriString* tmp_str = furi_string_alloc();
-        if(!flipper_format_read_string(file, "Keyboard_Layout", app->keyboard_layout)) {
-            furi_string_reset(app->keyboard_layout);
-        }
-        if(!flipper_format_read_bool(file, "BT_Remember", &(app->bt_remember), 1)) {
-            app->bt_remember = false;
-        }
-        if(flipper_format_read_string(file, "Bt_Name", tmp_str) && !furi_string_empty(tmp_str)) {
-            strcpy(app->config.bt_name, furi_string_get_cstr(tmp_str));
-        } else {
-            strcpy(app->config.bt_name, "");
-        }
-        if(!flipper_format_read_hex(
-               file, "Bt_Mac", (uint8_t*)&app->config.bt_mac, BAD_BT_MAC_ADDRESS_LEN)) {
-            memcpy(
-                app->config.bt_mac,
-                furi_hal_bt_get_profile_mac_addr(FuriHalBtProfileHidKeyboard),
-                BAD_BT_MAC_ADDRESS_LEN);
-        }
-        furi_string_free(tmp_str);
-        flipper_format_file_close(file);
-    }
-    flipper_format_free(file);
-
-    if(!furi_string_empty(app->keyboard_layout)) {
-        FileInfo layout_file_info;
-        FS_Error file_check_err = storage_common_stat(
-            storage, furi_string_get_cstr(app->keyboard_layout), &layout_file_info);
-        if(file_check_err != FSE_OK) {
-            furi_string_reset(app->keyboard_layout);
-            return;
-        }
-        if(layout_file_info.size != 256) {
-            furi_string_reset(app->keyboard_layout);
-        }
-    }
-
-    furi_record_close(RECORD_STORAGE);
-}
-
-static void bad_bt_save_settings(BadBtApp* app) {
-    Storage* storage = furi_record_open(RECORD_STORAGE);
-    FlipperFormat* file = flipper_format_file_alloc(storage);
-    if(flipper_format_file_open_always(file, BAD_BT_SETTINGS_PATH)) {
-        flipper_format_write_string(file, "Keyboard_Layout", app->keyboard_layout);
-        flipper_format_write_bool(file, "BT_Remember", &(app->bt_remember), 1);
-        flipper_format_write_string_cstr(file, "Bt_Name", app->config.bt_name);
-        flipper_format_write_hex(
-            file, "Bt_Mac", (uint8_t*)&app->config.bt_mac, BAD_BT_MAC_ADDRESS_LEN);
-        flipper_format_file_close(file);
-    }
-    flipper_format_free(file);
-    furi_record_close(RECORD_STORAGE);
-}
-
-void bad_bt_reload_worker(BadBtApp* app) {
-    bad_bt_script_close(app->bad_bt_script);
-    app->bad_bt_script = bad_bt_script_open(app->file_path, app->bt, app);
-    bad_bt_script_set_keyboard_layout(app->bad_bt_script, app->keyboard_layout);
-}
-
-void bad_kb_config_refresh_menu(BadBtApp* app) {
-    scene_manager_next_scene(app->scene_manager, BadBtSceneConfig);
-    scene_manager_previous_scene(app->scene_manager);
-}
-
-int32_t bad_bt_config_switch_mode(BadBtApp* app) {
-    bad_bt_reload_worker(app);
-    furi_hal_bt_start_advertising();
-    bad_kb_config_refresh_menu(app);
-    return 0;
-}
-
-void bad_bt_config_switch_remember_mode(BadBtApp* app) {
-    if(app->bt_remember) {
-        furi_hal_bt_set_profile_pairing_method(
-            FuriHalBtProfileHidKeyboard, GapPairingPinCodeVerifyYesNo);
-        bt_set_profile_mac_address(app->bt, (uint8_t*)&BAD_BT_BOUND_MAC_ADDRESS);
-        bt_enable_peer_key_update(app->bt);
-    } else {
-        furi_hal_bt_set_profile_pairing_method(FuriHalBtProfileHidKeyboard, GapPairingNone);
-        bt_set_profile_mac_address(app->bt, app->config.bt_mac);
-        bt_disable_peer_key_update(app->bt);
-    }
-    bad_bt_reload_worker(app);
-}
-
-int32_t bad_bt_connection_init(BadBtApp* app) {
-    // Set original name and mac address in prev config
-    strcpy(
-        app->prev_config.bt_name, furi_hal_bt_get_profile_adv_name(FuriHalBtProfileHidKeyboard));
-
-    memcpy(app->prev_config.bt_mac, furi_hal_version_get_ble_mac(), BAD_BT_MAC_ADDRESS_LEN);
-
-    bt_timeout = bt_hid_delays[LevelRssi39_0];
-    bt_disconnect(app->bt);
-    // Wait 2nd core to update nvm storage
-    furi_delay_ms(200);
-    bt_keys_storage_set_storage_path(app->bt, BAD_BT_APP_PATH_BOUND_KEYS_FILE);
-    if(strcmp(app->config.bt_name, "") != 0) {
-        furi_hal_bt_set_profile_adv_name(FuriHalBtProfileHidKeyboard, app->config.bt_name);
-    }
-    if(app->bt_remember) {
-        furi_hal_bt_set_profile_mac_addr(
-            FuriHalBtProfileHidKeyboard, (uint8_t*)&BAD_BT_BOUND_MAC_ADDRESS);
-        furi_hal_bt_set_profile_pairing_method(
-            FuriHalBtProfileHidKeyboard, GapPairingPinCodeVerifyYesNo);
-    } else {
-        if(memcmp(
-               app->config.bt_mac, (uint8_t*)&BAD_BT_EMPTY_MAC_ADDRESS, BAD_BT_MAC_ADDRESS_LEN) !=
-           0) {
-            furi_hal_bt_set_profile_mac_addr(FuriHalBtProfileHidKeyboard, app->config.bt_mac);
-        }
-        furi_hal_bt_set_profile_pairing_method(FuriHalBtProfileHidKeyboard, GapPairingNone);
-    }
-    bt_set_profile(app->bt, BtProfileHidKeyboard);
-    if(strcmp(app->config.bt_name, "") == 0) {
-        strcpy(app->config.bt_name, furi_hal_bt_get_profile_adv_name(FuriHalBtProfileHidKeyboard));
-    }
-    if(memcmp(app->config.bt_mac, (uint8_t*)&BAD_BT_EMPTY_MAC_ADDRESS, BAD_BT_MAC_ADDRESS_LEN) ==
-       0) {
-        memcpy(
-            app->config.bt_mac,
-            furi_hal_bt_get_profile_mac_addr(FuriHalBtProfileHidKeyboard),
-            BAD_BT_MAC_ADDRESS_LEN);
-    }
-
-    furi_hal_bt_start_advertising();
-    if(app->bt_remember) {
-        bt_enable_peer_key_update(app->bt);
-    } else {
-        bt_disable_peer_key_update(app->bt);
-    }
-
-    return 0;
-}
-
-void bad_bt_connection_deinit(BadBtApp* app) {
-    bt_disconnect(app->bt);
-    // Wait 2nd core to update nvm storage
-    furi_delay_ms(200);
-    bt_keys_storage_set_default_path(app->bt);
-    furi_hal_bt_set_profile_adv_name(FuriHalBtProfileHidKeyboard, app->prev_config.bt_name);
-    furi_hal_bt_set_profile_mac_addr(FuriHalBtProfileHidKeyboard, app->prev_config.bt_mac);
-    furi_hal_bt_set_profile_pairing_method(
-        FuriHalBtProfileHidKeyboard, GapPairingPinCodeVerifyYesNo);
-    bt_set_profile(app->bt, BtProfileSerial);
-    bt_enable_peer_key_update(app->bt);
-}
-
-BadBtApp* bad_bt_app_alloc(char* arg) {
-    BadBtApp* app = malloc(sizeof(BadBtApp));
-
-    app->bad_bt_script = NULL;
-
-    app->file_path = furi_string_alloc();
-    app->keyboard_layout = furi_string_alloc();
-    if(arg && strlen(arg)) {
-        furi_string_set(app->file_path, arg);
-    }
-
-    Storage* storage = furi_record_open(RECORD_STORAGE);
-    storage_simply_mkdir(storage, BAD_BT_APP_BASE_CONFIG_FOLDER);
-    furi_record_close(RECORD_STORAGE);
-
-    bad_bt_load_settings(app);
-
-    app->gui = furi_record_open(RECORD_GUI);
-    app->notifications = furi_record_open(RECORD_NOTIFICATION);
-    app->dialogs = furi_record_open(RECORD_DIALOGS);
-
-    app->view_dispatcher = view_dispatcher_alloc();
-    view_dispatcher_enable_queue(app->view_dispatcher);
-
-    app->scene_manager = scene_manager_alloc(&bad_bt_scene_handlers, app);
-
-    view_dispatcher_set_event_callback_context(app->view_dispatcher, app);
-    view_dispatcher_set_tick_event_callback(
-        app->view_dispatcher, bad_bt_app_tick_event_callback, 500);
-    view_dispatcher_set_custom_event_callback(
-        app->view_dispatcher, bad_bt_app_custom_event_callback);
-    view_dispatcher_set_navigation_event_callback(
-        app->view_dispatcher, bad_bt_app_back_event_callback);
-
-    Bt* bt = furi_record_open(RECORD_BT);
-    app->bt = bt;
-    app->bt->suppress_pin_screen = true;
-
-    // Custom Widget
-    app->widget = widget_alloc();
-    view_dispatcher_add_view(
-        app->view_dispatcher, BadBtAppViewError, widget_get_view(app->widget));
-
-    app->var_item_list = variable_item_list_alloc();
-    view_dispatcher_add_view(
-        app->view_dispatcher, BadBtAppViewConfig, variable_item_list_get_view(app->var_item_list));
-
-    app->bad_bt_view = bad_bt_alloc();
-    view_dispatcher_add_view(
-        app->view_dispatcher, BadBtAppViewWork, bad_bt_get_view(app->bad_bt_view));
-
-    app->text_input = text_input_alloc();
-    view_dispatcher_add_view(
-        app->view_dispatcher, BadBtAppViewConfigName, text_input_get_view(app->text_input));
-
-    app->byte_input = byte_input_alloc();
-    view_dispatcher_add_view(
-        app->view_dispatcher, BadBtAppViewConfigMac, byte_input_get_view(app->byte_input));
-
-    view_dispatcher_attach_to_gui(app->view_dispatcher, app->gui, ViewDispatcherTypeFullscreen);
-
-    app->conn_init_thread = furi_thread_alloc_ex(
-        "BadBtConnInit", 1024, (FuriThreadCallback)bad_bt_connection_init, app);
-    furi_thread_start(app->conn_init_thread);
-    if(!furi_string_empty(app->file_path)) {
-        app->bad_bt_script = bad_bt_script_open(app->file_path, app->bt, app);
-        bad_bt_script_set_keyboard_layout(app->bad_bt_script, app->keyboard_layout);
-        scene_manager_next_scene(app->scene_manager, BadBtSceneWork);
-    } else {
-        furi_string_set(app->file_path, BAD_BT_APP_BASE_FOLDER);
-        scene_manager_next_scene(app->scene_manager, BadBtSceneFileSelect);
-    }
-
-    return app;
-}
-
-void bad_bt_app_free(BadBtApp* app) {
-    furi_assert(app);
-
-    if(app->bad_bt_script) {
-        bad_bt_script_close(app->bad_bt_script);
-        app->bad_bt_script = NULL;
-    }
-
-    // Views
-    view_dispatcher_remove_view(app->view_dispatcher, BadBtAppViewWork);
-    bad_bt_free(app->bad_bt_view);
-
-    // Custom Widget
-    view_dispatcher_remove_view(app->view_dispatcher, BadBtAppViewError);
-    widget_free(app->widget);
-
-    // Variable item list
-    view_dispatcher_remove_view(app->view_dispatcher, BadBtAppViewConfig);
-    variable_item_list_free(app->var_item_list);
-
-    // Text Input
-    view_dispatcher_remove_view(app->view_dispatcher, BadBtAppViewConfigName);
-    text_input_free(app->text_input);
-
-    // Byte Input
-    view_dispatcher_remove_view(app->view_dispatcher, BadBtAppViewConfigMac);
-    byte_input_free(app->byte_input);
-
-    // View dispatcher
-    view_dispatcher_free(app->view_dispatcher);
-    scene_manager_free(app->scene_manager);
-
-    // Restore bt config
-    app->bt->suppress_pin_screen = false;
-    if(app->conn_init_thread) {
-        furi_thread_join(app->conn_init_thread);
-        furi_thread_free(app->conn_init_thread);
-        bad_bt_connection_deinit(app);
-    }
-
-    // Close records
-    furi_record_close(RECORD_GUI);
-    furi_record_close(RECORD_NOTIFICATION);
-    furi_record_close(RECORD_DIALOGS);
-    furi_record_close(RECORD_BT);
-
-    bad_bt_save_settings(app);
-
-    furi_string_free(app->file_path);
-    furi_string_free(app->keyboard_layout);
-
-    free(app);
-}
-
-int32_t bad_bt_app(void* p) {
-    BadBtApp* bad_bt_app = bad_bt_app_alloc((char*)p);
-
-    view_dispatcher_run(bad_bt_app->view_dispatcher);
-
-    bad_bt_app_free(bad_bt_app);
-    return 0;
-}

+ 0 - 39
base_pack/bad_bt/bad_bt_app.h

@@ -1,39 +0,0 @@
-#pragma once
-
-#include "scenes/bad_bt_scene.h"
-#include "helpers/ducky_script.h"
-
-#include <gui/gui.h>
-#include <assets_icons.h>
-#include <gui/scene_manager.h>
-#include <dialogs/dialogs.h>
-#include <notification/notification_messages.h>
-#include "bad_bt_icons.h"
-
-#define BAD_BT_APP_BASE_FOLDER EXT_PATH("badusb")
-#define BAD_BT_APP_BASE_CONFIG_FOLDER EXT_PATH("badbt")
-#define BAD_BT_APP_PATH_LAYOUT_FOLDER BAD_BT_APP_BASE_FOLDER "/assets/layouts"
-#define BAD_BT_APP_SCRIPT_EXTENSION ".txt"
-#define BAD_BT_APP_LAYOUT_EXTENSION ".kl"
-
-typedef enum BadBtCustomEvent {
-    BadBtAppCustomEventTextEditResult,
-    BadBtAppCustomEventByteInputDone,
-    BadBtCustomEventErrorBack
-} BadBtCustomEvent;
-
-typedef enum {
-    BadBtAppViewError,
-    BadBtAppViewWork,
-    BadBtAppViewConfig,
-    BadBtAppViewConfigMac,
-    BadBtAppViewConfigName
-} BadBtAppView;
-
-void bad_bt_config_switch_remember_mode(BadBtApp* app);
-
-int32_t bad_bt_connection_init(BadBtApp* app);
-
-void bad_bt_connection_deinit(BadBtApp* app);
-
-void bad_kb_config_refresh_menu(BadBtApp* app);

+ 0 - 792
base_pack/bad_bt/helpers/ducky_script.c

@@ -1,792 +0,0 @@
-#include <furi.h>
-#include <furi_hal.h>
-#include <gui/gui.h>
-#include <input/input.h>
-#include <lib/toolbox/args.h>
-#include <furi_hal_bt_hid.h>
-#include <bt/bt_service/bt.h>
-#include <storage/storage.h>
-#include "ducky_script.h"
-#include "ducky_script_i.h"
-#include <dolphin/dolphin.h>
-#include <toolbox/hex.h>
-#include "../bad_bt_app.h"
-
-const uint8_t BAD_BT_BOUND_MAC_ADDRESS[BAD_BT_MAC_ADDRESS_LEN] =
-    {0x41, 0x4a, 0xef, 0xb6, 0xa9, 0xd4};
-const uint8_t BAD_BT_EMPTY_MAC_ADDRESS[BAD_BT_MAC_ADDRESS_LEN] =
-    {0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
-
-#define TAG "BadBT"
-#define WORKER_TAG TAG "Worker"
-
-#define BADBT_ASCII_TO_KEY(script, x) \
-    (((uint8_t)x < 128) ? (script->layout[(uint8_t)x]) : HID_KEYBOARD_NONE)
-
-/**
- * Delays for waiting between HID key press and key release
-*/
-const uint8_t bt_hid_delays[LevelRssiNum] = {
-    60, // LevelRssi122_100
-    55, // LevelRssi99_80
-    50, // LevelRssi79_60
-    47, // LevelRssi59_40
-    34, // LevelRssi39_0
-};
-
-uint8_t bt_timeout = 0;
-
-static LevelRssiRange bt_remote_rssi_range(Bt* bt) {
-    uint8_t rssi;
-
-    if(!bt_remote_rssi(bt, &rssi)) return LevelRssiError;
-
-    if(rssi <= 39)
-        return LevelRssi39_0;
-    else if(rssi <= 59)
-        return LevelRssi59_40;
-    else if(rssi <= 79)
-        return LevelRssi79_60;
-    else if(rssi <= 99)
-        return LevelRssi99_80;
-    else if(rssi <= 122)
-        return LevelRssi122_100;
-
-    return LevelRssiError;
-}
-
-static inline void update_bt_timeout(Bt* bt) {
-    LevelRssiRange r = bt_remote_rssi_range(bt);
-    if(r < LevelRssiNum) {
-        bt_timeout = bt_hid_delays[r];
-        FURI_LOG_D(WORKER_TAG, "BLE Key timeout : %u", bt_timeout);
-    }
-}
-
-typedef enum {
-    WorkerEvtStartStop = (1 << 0),
-    WorkerEvtPauseResume = (1 << 1),
-    WorkerEvtEnd = (1 << 2),
-    WorkerEvtConnect = (1 << 3),
-    WorkerEvtDisconnect = (1 << 4),
-} WorkerEvtFlags;
-
-static const char ducky_cmd_id[] = {"ID"};
-static const char ducky_cmd_bt_id[] = {"BT_ID"};
-
-static const uint8_t numpad_keys[10] = {
-    HID_KEYPAD_0,
-    HID_KEYPAD_1,
-    HID_KEYPAD_2,
-    HID_KEYPAD_3,
-    HID_KEYPAD_4,
-    HID_KEYPAD_5,
-    HID_KEYPAD_6,
-    HID_KEYPAD_7,
-    HID_KEYPAD_8,
-    HID_KEYPAD_9,
-};
-
-uint32_t ducky_get_command_len(const char* line) {
-    uint32_t len = strlen(line);
-    for(uint32_t i = 0; i < len; i++) {
-        if(line[i] == ' ') return i;
-    }
-    return 0;
-}
-
-bool ducky_is_line_end(const char chr) {
-    return ((chr == ' ') || (chr == '\0') || (chr == '\r') || (chr == '\n'));
-}
-
-uint16_t ducky_get_keycode(BadBtScript* bad_bt, const char* param, bool accept_chars) {
-    uint16_t keycode = ducky_get_keycode_by_name(param);
-    if(keycode != HID_KEYBOARD_NONE) {
-        return keycode;
-    }
-
-    if((accept_chars) && (strlen(param) > 0)) {
-        return (BADBT_ASCII_TO_KEY(bad_bt, param[0]) & 0xFF);
-    }
-    return 0;
-}
-
-bool ducky_get_number(const char* param, uint32_t* val) {
-    uint32_t value = 0;
-    if(sscanf(param, "%lu", &value) == 1) {
-        *val = value;
-        return true;
-    }
-    return false;
-}
-
-void ducky_numlock_on(BadBtScript* bad_bt) {
-    UNUSED(bad_bt);
-    if((furi_hal_bt_hid_get_led_state() & HID_KB_LED_NUM) == 0) {
-        furi_hal_bt_hid_kb_press(HID_KEYBOARD_LOCK_NUM_LOCK);
-        furi_delay_ms(bt_timeout);
-        furi_hal_bt_hid_kb_release(HID_KEYBOARD_LOCK_NUM_LOCK);
-    }
-}
-
-bool ducky_numpad_press(BadBtScript* bad_bt, const char num) {
-    UNUSED(bad_bt);
-    if((num < '0') || (num > '9')) return false;
-
-    uint16_t key = numpad_keys[num - '0'];
-    furi_hal_bt_hid_kb_press(key);
-    furi_delay_ms(bt_timeout);
-    furi_hal_bt_hid_kb_release(key);
-
-    return true;
-}
-
-bool ducky_altchar(BadBtScript* bad_bt, const char* charcode) {
-    uint8_t i = 0;
-    bool state = false;
-
-    furi_hal_bt_hid_kb_press(KEY_MOD_LEFT_ALT);
-
-    while(!ducky_is_line_end(charcode[i])) {
-        state = ducky_numpad_press(bad_bt, charcode[i]);
-        if(state == false) break;
-        i++;
-    }
-
-    furi_hal_bt_hid_kb_release(KEY_MOD_LEFT_ALT);
-
-    return state;
-}
-
-bool ducky_altstring(BadBtScript* bad_bt, const char* param) {
-    uint32_t i = 0;
-    bool state = false;
-
-    while(param[i] != '\0') {
-        if((param[i] < ' ') || (param[i] > '~')) {
-            i++;
-            continue; // Skip non-printable chars
-        }
-
-        char temp_str[4];
-        snprintf(temp_str, 4, "%u", param[i]);
-
-        state = ducky_altchar(bad_bt, temp_str);
-        if(state == false) break;
-        i++;
-    }
-    return state;
-}
-
-int32_t ducky_error(BadBtScript* bad_bt, const char* text, ...) {
-    va_list args;
-    va_start(args, text);
-
-    vsnprintf(bad_bt->st.error, sizeof(bad_bt->st.error), text, args);
-
-    va_end(args);
-    return SCRIPT_STATE_ERROR;
-}
-
-bool ducky_string(BadBtScript* bad_bt, const char* param) {
-    uint32_t i = 0;
-
-    while(param[i] != '\0') {
-        if(param[i] != '\n') {
-            uint16_t keycode = BADBT_ASCII_TO_KEY(bad_bt, param[i]);
-            if(keycode != HID_KEYBOARD_NONE) {
-                furi_hal_bt_hid_kb_press(keycode);
-                furi_delay_ms(bt_timeout);
-                furi_hal_bt_hid_kb_release(keycode);
-            }
-        } else {
-            furi_hal_bt_hid_kb_press(HID_KEYBOARD_RETURN);
-            furi_delay_ms(bt_timeout);
-            furi_hal_bt_hid_kb_release(HID_KEYBOARD_RETURN);
-        }
-        i++;
-    }
-    bad_bt->stringdelay = 0;
-    return true;
-}
-
-static bool ducky_string_next(BadBtScript* bad_bt) {
-    if(bad_bt->string_print_pos >= furi_string_size(bad_bt->string_print)) {
-        return true;
-    }
-
-    char print_char = furi_string_get_char(bad_bt->string_print, bad_bt->string_print_pos);
-
-    if(print_char != '\n') {
-        uint16_t keycode = BADBT_ASCII_TO_KEY(bad_bt, print_char);
-        if(keycode != HID_KEYBOARD_NONE) {
-            furi_hal_bt_hid_kb_press(keycode);
-            furi_delay_ms(bt_timeout);
-            furi_hal_bt_hid_kb_release(keycode);
-        }
-    } else {
-        furi_hal_bt_hid_kb_press(HID_KEYBOARD_RETURN);
-        furi_delay_ms(bt_timeout);
-        furi_hal_bt_hid_kb_release(HID_KEYBOARD_RETURN);
-    }
-
-    bad_bt->string_print_pos++;
-
-    return false;
-}
-
-static int32_t ducky_parse_line(BadBtScript* bad_bt, FuriString* line) {
-    uint32_t line_len = furi_string_size(line);
-    const char* line_tmp = furi_string_get_cstr(line);
-
-    if(line_len == 0) {
-        return SCRIPT_STATE_NEXT_LINE; // Skip empty lines
-    }
-    FURI_LOG_D(WORKER_TAG, "line:%s", line_tmp);
-
-    // Ducky Lang Functions
-    int32_t cmd_result = ducky_execute_cmd(bad_bt, line_tmp);
-    if(cmd_result != SCRIPT_STATE_CMD_UNKNOWN) {
-        return cmd_result;
-    }
-
-    // Special keys + modifiers
-    uint16_t key = ducky_get_keycode(bad_bt, line_tmp, false);
-    if(key == HID_KEYBOARD_NONE) {
-        return ducky_error(bad_bt, "No keycode defined for %s", line_tmp);
-    }
-    if((key & 0xFF00) != 0) {
-        // It's a modifier key
-        uint32_t offset = ducky_get_command_len(line_tmp) + 1;
-        // ducky_get_command_len() returns 0 without space, so check for != 1
-        if(offset != 1 && line_len > offset) {
-            // It's also a key combination
-            key |= ducky_get_keycode(bad_bt, line_tmp + offset, true);
-        }
-    }
-    furi_hal_bt_hid_kb_press(key);
-    furi_delay_ms(bt_timeout);
-    furi_hal_bt_hid_kb_release(key);
-
-    return 0;
-}
-
-static bool ducky_set_bt_id(BadBtScript* bad_bt, const char* line) {
-    size_t line_len = strlen(line);
-    size_t mac_len = BAD_BT_MAC_ADDRESS_LEN * 3;
-    if(line_len < mac_len + 1) return false; // MAC + at least 1 char for name
-
-    uint8_t mac[BAD_BT_MAC_ADDRESS_LEN];
-    for(size_t i = 0; i < BAD_BT_MAC_ADDRESS_LEN; i++) {
-        char a = line[i * 3];
-        char b = line[i * 3 + 1];
-        if((a < 'A' && a > 'F') || (a < '0' && a > '9') || (b < 'A' && b > 'F') ||
-           (b < '0' && b > '9') || !hex_char_to_uint8(a, b, &mac[i])) {
-            return false;
-        }
-    }
-    furi_hal_bt_reverse_mac_addr(mac);
-
-    furi_hal_bt_set_profile_adv_name(FuriHalBtProfileHidKeyboard, line + mac_len);
-    bt_set_profile_mac_address(bad_bt->bt, mac);
-    return true;
-}
-
-static bool ducky_script_preload(BadBtScript* bad_bt, File* script_file) {
-    uint8_t ret = 0;
-    uint32_t line_len = 0;
-
-    furi_string_reset(bad_bt->line);
-
-    do {
-        ret = storage_file_read(script_file, bad_bt->file_buf, FILE_BUFFER_LEN);
-        for(uint16_t i = 0; i < ret; i++) {
-            if(bad_bt->file_buf[i] == '\n' && line_len > 0) {
-                bad_bt->st.line_nb++;
-                line_len = 0;
-            } else {
-                if(bad_bt->st.line_nb == 0) { // Save first line
-                    furi_string_push_back(bad_bt->line, bad_bt->file_buf[i]);
-                }
-                line_len++;
-            }
-        }
-        if(storage_file_eof(script_file)) {
-            if(line_len > 0) {
-                bad_bt->st.line_nb++;
-                break;
-            }
-        }
-    } while(ret > 0);
-
-    const char* line_tmp = furi_string_get_cstr(bad_bt->line);
-    if(bad_bt->app->switch_mode_thread) {
-        furi_thread_join(bad_bt->app->switch_mode_thread);
-        furi_thread_free(bad_bt->app->switch_mode_thread);
-        bad_bt->app->switch_mode_thread = NULL;
-    }
-    // Looking for ID or BT_ID command at first line
-    bad_bt->set_usb_id = false;
-    bad_bt->set_bt_id = false;
-    bad_bt->has_usb_id = strncmp(line_tmp, ducky_cmd_id, strlen(ducky_cmd_id)) == 0;
-    // TODO: We setting has_usb_id to its value but ignoring it for now and not using anywhere here, may be used in a future to detect script type
-    bad_bt->has_bt_id = strncmp(line_tmp, ducky_cmd_bt_id, strlen(ducky_cmd_bt_id)) == 0;
-    if(bad_bt->has_bt_id) {
-        if(!bad_bt->app->bt_remember) {
-            bad_bt->set_bt_id = ducky_set_bt_id(bad_bt, &line_tmp[strlen(ducky_cmd_bt_id) + 1]);
-        }
-    }
-
-    bad_kb_config_refresh_menu(bad_bt->app);
-
-    if(!bad_bt->set_bt_id) {
-        const char* bt_name = bad_bt->app->config.bt_name;
-        const uint8_t* bt_mac = bad_bt->app->bt_remember ? (uint8_t*)&BAD_BT_BOUND_MAC_ADDRESS :
-                                                           bad_bt->app->config.bt_mac;
-        bool reset_name = strncmp(
-            bt_name,
-            furi_hal_bt_get_profile_adv_name(FuriHalBtProfileHidKeyboard),
-            BAD_BT_ADV_NAME_MAX_LEN);
-        bool reset_mac = memcmp(
-            bt_mac,
-            furi_hal_bt_get_profile_mac_addr(FuriHalBtProfileHidKeyboard),
-            BAD_BT_MAC_ADDRESS_LEN);
-        if(reset_name && reset_mac) {
-            furi_hal_bt_set_profile_adv_name(FuriHalBtProfileHidKeyboard, bt_name);
-        } else if(reset_name) {
-            bt_set_profile_adv_name(bad_bt->bt, bt_name);
-        }
-        if(reset_mac) {
-            bt_set_profile_mac_address(bad_bt->bt, bt_mac);
-        }
-    }
-
-    storage_file_seek(script_file, 0, true);
-    furi_string_reset(bad_bt->line);
-
-    return true;
-}
-
-static int32_t ducky_script_execute_next(BadBtScript* bad_bt, File* script_file) {
-    int32_t delay_val = 0;
-
-    if(bad_bt->repeat_cnt > 0) {
-        bad_bt->repeat_cnt--;
-        delay_val = ducky_parse_line(bad_bt, bad_bt->line_prev);
-        if(delay_val == SCRIPT_STATE_NEXT_LINE) { // Empty line
-            return 0;
-        } else if(delay_val == SCRIPT_STATE_STRING_START) { // Print string with delays
-            return delay_val;
-        } else if(delay_val == SCRIPT_STATE_WAIT_FOR_BTN) { // wait for button
-            return delay_val;
-        } else if(delay_val < 0) { // Script error
-            bad_bt->st.error_line = bad_bt->st.line_cur - 1;
-            FURI_LOG_E(WORKER_TAG, "Unknown command at line %u", bad_bt->st.line_cur - 1U);
-            return SCRIPT_STATE_ERROR;
-        } else {
-            return (delay_val + bad_bt->defdelay);
-        }
-    }
-
-    furi_string_set(bad_bt->line_prev, bad_bt->line);
-    furi_string_reset(bad_bt->line);
-
-    while(1) {
-        if(bad_bt->buf_len == 0) {
-            bad_bt->buf_len = storage_file_read(script_file, bad_bt->file_buf, FILE_BUFFER_LEN);
-            if(storage_file_eof(script_file)) {
-                if((bad_bt->buf_len < FILE_BUFFER_LEN) && (bad_bt->file_end == false)) {
-                    bad_bt->file_buf[bad_bt->buf_len] = '\n';
-                    bad_bt->buf_len++;
-                    bad_bt->file_end = true;
-                }
-            }
-
-            bad_bt->buf_start = 0;
-            if(bad_bt->buf_len == 0) return SCRIPT_STATE_END;
-        }
-        for(uint8_t i = bad_bt->buf_start; i < (bad_bt->buf_start + bad_bt->buf_len); i++) {
-            if(bad_bt->file_buf[i] == '\n' && furi_string_size(bad_bt->line) > 0) {
-                bad_bt->st.line_cur++;
-                bad_bt->buf_len = bad_bt->buf_len + bad_bt->buf_start - (i + 1);
-                bad_bt->buf_start = i + 1;
-                furi_string_trim(bad_bt->line);
-                delay_val = ducky_parse_line(bad_bt, bad_bt->line);
-                if(delay_val == SCRIPT_STATE_NEXT_LINE) { // Empty line
-                    return 0;
-                } else if(delay_val == SCRIPT_STATE_STRING_START) { // Print string with delays
-                    return delay_val;
-                } else if(delay_val == SCRIPT_STATE_WAIT_FOR_BTN) { // wait for button
-                    return delay_val;
-                } else if(delay_val < 0) {
-                    bad_bt->st.error_line = bad_bt->st.line_cur;
-                    FURI_LOG_E(WORKER_TAG, "Unknown command at line %u", bad_bt->st.line_cur);
-                    return SCRIPT_STATE_ERROR;
-                } else {
-                    return (delay_val + bad_bt->defdelay);
-                }
-            } else {
-                furi_string_push_back(bad_bt->line, bad_bt->file_buf[i]);
-            }
-        }
-        bad_bt->buf_len = 0;
-        if(bad_bt->file_end) return SCRIPT_STATE_END;
-    }
-
-    return 0;
-}
-
-static void bad_bt_bt_hid_state_callback(BtStatus status, void* context) {
-    furi_assert(context);
-    BadBtScript* bad_bt = context;
-    bool state = (status == BtStatusConnected);
-
-    if(state == true) {
-        LevelRssiRange r = bt_remote_rssi_range(bad_bt->bt);
-        if(r != LevelRssiError) {
-            bt_timeout = bt_hid_delays[r];
-        }
-        furi_thread_flags_set(furi_thread_get_id(bad_bt->thread), WorkerEvtConnect);
-    } else {
-        furi_thread_flags_set(furi_thread_get_id(bad_bt->thread), WorkerEvtDisconnect);
-    }
-}
-
-static uint32_t bad_bt_flags_get(uint32_t flags_mask, uint32_t timeout) {
-    uint32_t flags = furi_thread_flags_get();
-    furi_check((flags & FuriFlagError) == 0);
-    if(flags == 0) {
-        flags = furi_thread_flags_wait(flags_mask, FuriFlagWaitAny, timeout);
-        furi_check(((flags & FuriFlagError) == 0) || (flags == (unsigned)FuriFlagErrorTimeout));
-    } else {
-        uint32_t state = furi_thread_flags_clear(flags);
-        furi_check((state & FuriFlagError) == 0);
-    }
-    return flags;
-}
-
-static int32_t bad_bt_worker(void* context) {
-    BadBtScript* bad_bt = context;
-
-    BadBtWorkerState worker_state = BadBtStateInit;
-    int32_t delay_val = 0;
-
-    FURI_LOG_I(WORKER_TAG, "Init");
-    File* script_file = storage_file_alloc(furi_record_open(RECORD_STORAGE));
-    bad_bt->line = furi_string_alloc();
-    bad_bt->line_prev = furi_string_alloc();
-    bad_bt->string_print = furi_string_alloc();
-
-    bt_set_status_changed_callback(bad_bt->bt, bad_bt_bt_hid_state_callback, bad_bt);
-
-    while(1) {
-        if(worker_state == BadBtStateInit) { // State: initialization
-            if(storage_file_open(
-                   script_file,
-                   furi_string_get_cstr(bad_bt->file_path),
-                   FSAM_READ,
-                   FSOM_OPEN_EXISTING)) {
-                if((ducky_script_preload(bad_bt, script_file)) && (bad_bt->st.line_nb > 0)) {
-                    if(furi_hal_bt_is_connected()) {
-                        worker_state = BadBtStateIdle; // Ready to run
-                    } else {
-                        worker_state = BadBtStateNotConnected; // Not connected
-                    }
-
-                } else {
-                    worker_state = BadBtStateScriptError; // Script preload error
-                }
-            } else {
-                FURI_LOG_E(WORKER_TAG, "File open error");
-                worker_state = BadBtStateFileError; // File open error
-            }
-            bad_bt->st.state = worker_state;
-
-        } else if(worker_state == BadBtStateNotConnected) { // State: Not connected
-            uint32_t flags = bad_bt_flags_get(
-                WorkerEvtEnd | WorkerEvtConnect | WorkerEvtDisconnect | WorkerEvtStartStop,
-                FuriWaitForever);
-
-            if(flags & WorkerEvtEnd) {
-                break;
-            } else if(flags & WorkerEvtConnect) {
-                worker_state = BadBtStateIdle; // Ready to run
-            } else if(flags & WorkerEvtStartStop) {
-                worker_state = BadBtStateWillRun; // Will run when connected
-            }
-            bad_bt->st.state = worker_state;
-
-        } else if(worker_state == BadBtStateIdle) { // State: ready to start
-            uint32_t flags = bad_bt_flags_get(
-                WorkerEvtEnd | WorkerEvtStartStop | WorkerEvtConnect | WorkerEvtDisconnect,
-                FuriWaitForever);
-
-            if(flags & WorkerEvtEnd) {
-                break;
-            } else if(flags & WorkerEvtStartStop) { // Start executing script
-                delay_val = 0;
-                bad_bt->buf_len = 0;
-                bad_bt->st.line_cur = 0;
-                bad_bt->defdelay = 0;
-                bad_bt->stringdelay = 0;
-                bad_bt->repeat_cnt = 0;
-                bad_bt->key_hold_nb = 0;
-                bad_bt->file_end = false;
-                storage_file_seek(script_file, 0, true);
-                bad_bt_script_set_keyboard_layout(bad_bt, bad_bt->keyboard_layout);
-                worker_state = BadBtStateRunning;
-            } else if(flags & WorkerEvtDisconnect) {
-                worker_state = BadBtStateNotConnected; // Disconnected
-            }
-            bad_bt->st.state = worker_state;
-
-        } else if(worker_state == BadBtStateWillRun) { // State: start on connection
-            uint32_t flags = bad_bt_flags_get(
-                WorkerEvtEnd | WorkerEvtConnect | WorkerEvtDisconnect | WorkerEvtStartStop,
-                FuriWaitForever);
-
-            if(flags & WorkerEvtEnd) {
-                break;
-            } else if(flags & WorkerEvtConnect) { // Start executing script
-                delay_val = 0;
-                bad_bt->buf_len = 0;
-                bad_bt->st.line_cur = 0;
-                bad_bt->defdelay = 0;
-                bad_bt->stringdelay = 0;
-                bad_bt->repeat_cnt = 0;
-                bad_bt->file_end = false;
-                storage_file_seek(script_file, 0, true);
-                // extra time for PC to recognize Flipper as keyboard
-                flags = furi_thread_flags_wait(
-                    WorkerEvtEnd | WorkerEvtDisconnect | WorkerEvtStartStop,
-                    FuriFlagWaitAny | FuriFlagNoClear,
-                    1500);
-                if(flags == (unsigned)FuriFlagErrorTimeout) {
-                    // If nothing happened - start script execution
-                    worker_state = BadBtStateRunning;
-                } else if(flags & WorkerEvtStartStop) {
-                    worker_state = BadBtStateIdle;
-                    furi_thread_flags_clear(WorkerEvtStartStop);
-                }
-
-                update_bt_timeout(bad_bt->bt);
-
-                bad_bt_script_set_keyboard_layout(bad_bt, bad_bt->keyboard_layout);
-            } else if(flags & WorkerEvtStartStop) { // Cancel scheduled execution
-                worker_state = BadBtStateNotConnected;
-            }
-            bad_bt->st.state = worker_state;
-
-        } else if(worker_state == BadBtStateRunning) { // State: running
-            uint16_t delay_cur = (delay_val > 1000) ? (1000) : (delay_val);
-            uint32_t flags = furi_thread_flags_wait(
-                WorkerEvtEnd | WorkerEvtStartStop | WorkerEvtConnect | WorkerEvtDisconnect,
-                FuriFlagWaitAny,
-                delay_cur);
-
-            delay_val -= delay_cur;
-            if(!(flags & FuriFlagError)) {
-                if(flags & WorkerEvtEnd) {
-                    break;
-                } else if(flags & WorkerEvtStartStop) {
-                    worker_state = BadBtStateIdle; // Stop executing script
-
-                    furi_hal_bt_hid_kb_release_all();
-
-                } else if(flags & WorkerEvtDisconnect) {
-                    worker_state = BadBtStateNotConnected; // Disconnected
-
-                    furi_hal_bt_hid_kb_release_all();
-                }
-                bad_bt->st.state = worker_state;
-                continue;
-            } else if(
-                (flags == (unsigned)FuriFlagErrorTimeout) ||
-                (flags == (unsigned)FuriFlagErrorResource)) {
-                if(delay_val > 0) {
-                    bad_bt->st.delay_remain--;
-                    continue;
-                }
-                bad_bt->st.state = BadBtStateRunning;
-                delay_val = ducky_script_execute_next(bad_bt, script_file);
-                if(delay_val == SCRIPT_STATE_ERROR) { // Script error
-                    delay_val = 0;
-                    worker_state = BadBtStateScriptError;
-                    bad_bt->st.state = worker_state;
-
-                    furi_hal_bt_hid_kb_release_all();
-
-                } else if(delay_val == SCRIPT_STATE_END) { // End of script
-                    delay_val = 0;
-                    worker_state = BadBtStateIdle;
-                    bad_bt->st.state = BadBtStateDone;
-
-                    furi_hal_bt_hid_kb_release_all();
-
-                    continue;
-                } else if(delay_val == SCRIPT_STATE_STRING_START) { // Start printing string with delays
-                    delay_val = bad_bt->defdelay;
-                    bad_bt->string_print_pos = 0;
-                    worker_state = BadBtStateStringDelay;
-                } else if(delay_val == SCRIPT_STATE_WAIT_FOR_BTN) { // set state to wait for user input
-                    worker_state = BadBtStateWaitForBtn;
-                    bad_bt->st.state = BadBtStateWaitForBtn; // Show long delays
-                } else if(delay_val > 1000) {
-                    bad_bt->st.state = BadBtStateDelay; // Show long delays
-                    bad_bt->st.delay_remain = delay_val / 1000;
-                }
-            } else {
-                furi_check((flags & FuriFlagError) == 0);
-            }
-        } else if(worker_state == BadBtStateWaitForBtn) { // State: Wait for button Press
-            uint16_t delay_cur = (delay_val > 1000) ? (1000) : (delay_val);
-            uint32_t flags = furi_thread_flags_wait(
-                WorkerEvtEnd | WorkerEvtStartStop | WorkerEvtPauseResume | WorkerEvtConnect |
-                    WorkerEvtDisconnect,
-                FuriFlagWaitAny,
-                delay_cur);
-            if(!(flags & FuriFlagError)) {
-                if(flags & WorkerEvtEnd) {
-                    break;
-                } else if(flags & WorkerEvtStartStop) {
-                    delay_val = 0;
-                    worker_state = BadBtStateRunning;
-                } else if(flags & WorkerEvtDisconnect) {
-                    worker_state = BadBtStateNotConnected; // Disconnected
-                    furi_hal_hid_kb_release_all();
-                }
-                bad_bt->st.state = worker_state;
-                continue;
-            }
-        } else if(worker_state == BadBtStateStringDelay) { // State: print string with delays
-            uint32_t flags = furi_thread_flags_wait(
-                WorkerEvtEnd | WorkerEvtStartStop | WorkerEvtPauseResume | WorkerEvtConnect |
-                    WorkerEvtDisconnect,
-                FuriFlagWaitAny,
-                bad_bt->stringdelay);
-
-            if(!(flags & FuriFlagError)) {
-                if(flags & WorkerEvtEnd) {
-                    break;
-                } else if(flags & WorkerEvtStartStop) {
-                    worker_state = BadBtStateIdle; // Stop executing script
-
-                    furi_hal_bt_hid_kb_release_all();
-
-                } else if(flags & WorkerEvtDisconnect) {
-                    worker_state = BadBtStateNotConnected; // Disconnected
-
-                    furi_hal_bt_hid_kb_release_all();
-                }
-                bad_bt->st.state = worker_state;
-                continue;
-            } else if(
-                (flags == (unsigned)FuriFlagErrorTimeout) ||
-                (flags == (unsigned)FuriFlagErrorResource)) {
-                bool string_end = ducky_string_next(bad_bt);
-                if(string_end) {
-                    bad_bt->stringdelay = 0;
-                    worker_state = BadBtStateRunning;
-                }
-            } else {
-                furi_check((flags & FuriFlagError) == 0);
-            }
-        } else if(
-            (worker_state == BadBtStateFileError) ||
-            (worker_state == BadBtStateScriptError)) { // State: error
-            uint32_t flags =
-                bad_bt_flags_get(WorkerEvtEnd, FuriWaitForever); // Waiting for exit command
-
-            if(flags & WorkerEvtEnd) {
-                break;
-            }
-        }
-
-        update_bt_timeout(bad_bt->bt);
-    }
-
-    bt_set_status_changed_callback(bad_bt->bt, NULL, NULL);
-
-    storage_file_close(script_file);
-    storage_file_free(script_file);
-    furi_string_free(bad_bt->line);
-    furi_string_free(bad_bt->line_prev);
-    furi_string_free(bad_bt->string_print);
-
-    FURI_LOG_I(WORKER_TAG, "End");
-
-    return 0;
-}
-
-static void bad_bt_script_set_default_keyboard_layout(BadBtScript* bad_bt) {
-    furi_assert(bad_bt);
-    furi_string_set_str(bad_bt->keyboard_layout, "");
-    memset(bad_bt->layout, HID_KEYBOARD_NONE, sizeof(bad_bt->layout));
-    memcpy(bad_bt->layout, hid_asciimap, MIN(sizeof(hid_asciimap), sizeof(bad_bt->layout)));
-}
-
-BadBtScript* bad_bt_script_open(FuriString* file_path, Bt* bt, BadBtApp* app) {
-    furi_assert(file_path);
-
-    BadBtScript* bad_bt = malloc(sizeof(BadBtScript));
-    bad_bt->app = app;
-    bad_bt->file_path = furi_string_alloc();
-    furi_string_set(bad_bt->file_path, file_path);
-    bad_bt->keyboard_layout = furi_string_alloc();
-    bad_bt_script_set_default_keyboard_layout(bad_bt);
-
-    bad_bt->st.state = BadBtStateInit;
-    bad_bt->st.error[0] = '\0';
-
-    bad_bt->bt = bt;
-
-    bad_bt->thread = furi_thread_alloc_ex("BadBtWorker", 2048, bad_bt_worker, bad_bt);
-    furi_thread_start(bad_bt->thread);
-    return bad_bt;
-}
-
-void bad_bt_script_close(BadBtScript* bad_bt) {
-    furi_assert(bad_bt);
-    furi_record_close(RECORD_STORAGE);
-    furi_thread_flags_set(furi_thread_get_id(bad_bt->thread), WorkerEvtEnd);
-    furi_thread_join(bad_bt->thread);
-    furi_thread_free(bad_bt->thread);
-    furi_string_free(bad_bt->file_path);
-    furi_string_free(bad_bt->keyboard_layout);
-    free(bad_bt);
-}
-
-void bad_bt_script_set_keyboard_layout(BadBtScript* bad_bt, FuriString* layout_path) {
-    furi_assert(bad_bt);
-
-    if((bad_bt->st.state == BadBtStateRunning) || (bad_bt->st.state == BadBtStateDelay)) {
-        // do not update keyboard layout while a script is running
-        return;
-    }
-
-    File* layout_file = storage_file_alloc(furi_record_open(RECORD_STORAGE));
-    if(!furi_string_empty(layout_path)) { //-V1051
-        furi_string_set(bad_bt->keyboard_layout, layout_path);
-        if(storage_file_open(
-               layout_file, furi_string_get_cstr(layout_path), FSAM_READ, FSOM_OPEN_EXISTING)) {
-            uint16_t layout[128];
-            if(storage_file_read(layout_file, layout, sizeof(layout)) == sizeof(layout)) {
-                memcpy(bad_bt->layout, layout, sizeof(layout));
-            }
-        }
-        storage_file_close(layout_file);
-    } else {
-        bad_bt_script_set_default_keyboard_layout(bad_bt);
-    }
-    storage_file_free(layout_file);
-}
-
-void bad_bt_script_toggle(BadBtScript* bad_bt) {
-    furi_assert(bad_bt);
-    furi_thread_flags_set(furi_thread_get_id(bad_bt->thread), WorkerEvtStartStop);
-}
-
-BadBtState* bad_bt_script_get_state(BadBtScript* bad_bt) {
-    furi_assert(bad_bt);
-    return &(bad_bt->st);
-}

+ 0 - 154
base_pack/bad_bt/helpers/ducky_script.h

@@ -1,154 +0,0 @@
-#pragma once
-
-#ifdef __cplusplus
-extern "C" {
-#endif
-
-#include <furi.h>
-#include <furi_hal.h>
-#include <bt/bt_service/bt_i.h>
-
-#include <gui/view_dispatcher.h>
-#include <gui/modules/widget.h>
-#include <gui/modules/variable_item_list.h>
-#include <gui/modules/text_input.h>
-#include <gui/modules/byte_input.h>
-#include "../views/bad_bt_view.h"
-
-#define FILE_BUFFER_LEN 16
-
-typedef enum {
-    LevelRssi122_100,
-    LevelRssi99_80,
-    LevelRssi79_60,
-    LevelRssi59_40,
-    LevelRssi39_0,
-    LevelRssiNum,
-    LevelRssiError = 0xFF,
-} LevelRssiRange;
-
-extern const uint8_t bt_hid_delays[LevelRssiNum];
-
-extern uint8_t bt_timeout;
-
-typedef enum {
-    BadBtStateInit,
-    BadBtStateNotConnected,
-    BadBtStateIdle,
-    BadBtStateWillRun,
-    BadBtStateRunning,
-    BadBtStateDelay,
-    BadBtStateStringDelay,
-    BadBtStateWaitForBtn,
-    BadBtStateDone,
-    BadBtStateScriptError,
-    BadBtStateFileError,
-} BadBtWorkerState;
-
-struct BadBtState {
-    BadBtWorkerState state;
-    uint32_t pin;
-    uint16_t line_cur;
-    uint16_t line_nb;
-    uint32_t delay_remain;
-    uint16_t error_line;
-    char error[64];
-};
-
-typedef struct BadBtApp BadBtApp;
-
-typedef struct {
-    FuriHalUsbHidConfig hid_cfg;
-    FuriThread* thread;
-    BadBtState st;
-
-    FuriString* file_path;
-    FuriString* keyboard_layout;
-    uint8_t file_buf[FILE_BUFFER_LEN + 1];
-    uint8_t buf_start;
-    uint8_t buf_len;
-    bool file_end;
-
-    uint32_t defdelay;
-    uint32_t stringdelay;
-    uint16_t layout[128];
-
-    FuriString* line;
-    FuriString* line_prev;
-    uint32_t repeat_cnt;
-    uint8_t key_hold_nb;
-
-    bool set_usb_id;
-    bool set_bt_id;
-    bool has_usb_id;
-    bool has_bt_id;
-
-    FuriString* string_print;
-    size_t string_print_pos;
-
-    Bt* bt;
-    BadBtApp* app;
-} BadBtScript;
-
-BadBtScript* bad_bt_script_open(FuriString* file_path, Bt* bt, BadBtApp* app);
-
-void bad_bt_script_close(BadBtScript* bad_bt);
-
-void bad_bt_script_set_keyboard_layout(BadBtScript* bad_bt, FuriString* layout_path);
-
-void bad_bt_script_start(BadBtScript* bad_bt);
-
-void bad_bt_script_stop(BadBtScript* bad_bt);
-
-void bad_bt_script_toggle(BadBtScript* bad_bt);
-
-BadBtState* bad_bt_script_get_state(BadBtScript* bad_bt);
-
-#define BAD_BT_ADV_NAME_MAX_LEN FURI_HAL_BT_ADV_NAME_LENGTH
-#define BAD_BT_MAC_ADDRESS_LEN GAP_MAC_ADDR_SIZE
-
-// this is the MAC address used when we do not forget paired device (BOUND STATE)
-extern const uint8_t BAD_BT_BOUND_MAC_ADDRESS[BAD_BT_MAC_ADDRESS_LEN];
-extern const uint8_t BAD_BT_EMPTY_MAC_ADDRESS[BAD_BT_MAC_ADDRESS_LEN];
-
-typedef enum {
-    BadBtAppErrorNoFiles,
-    BadBtAppErrorCloseRpc,
-} BadBtAppError;
-
-typedef struct {
-    char bt_name[BAD_BT_ADV_NAME_MAX_LEN];
-    uint8_t bt_mac[BAD_BT_MAC_ADDRESS_LEN];
-    GapPairing bt_mode;
-} BadBtConfig;
-
-struct BadBtApp {
-    Gui* gui;
-    ViewDispatcher* view_dispatcher;
-    SceneManager* scene_manager;
-    NotificationApp* notifications;
-    DialogsApp* dialogs;
-    Widget* widget;
-    VariableItemList* var_item_list;
-    TextInput* text_input;
-    ByteInput* byte_input;
-
-    BadBtAppError error;
-    FuriString* file_path;
-    FuriString* keyboard_layout;
-    BadBt* bad_bt_view;
-    BadBtScript* bad_bt_script;
-
-    Bt* bt;
-    bool bt_remember;
-    BadBtConfig config;
-    BadBtConfig prev_config;
-    FuriThread* conn_init_thread;
-    FuriThread* switch_mode_thread;
-};
-
-int32_t bad_bt_config_switch_mode(BadBtApp* app);
-
-#ifdef __cplusplus
-}
-#endif

+ 0 - 201
base_pack/bad_bt/helpers/ducky_script_commands.c

@@ -1,201 +0,0 @@
-#include <furi_hal.h>
-#include <furi_hal_bt_hid.h>
-#include "ducky_script.h"
-#include "ducky_script_i.h"
-
-typedef int32_t (*DuckyCmdCallback)(BadBtScript* bad_bt, const char* line, int32_t param);
-
-typedef struct {
-    char* name;
-    DuckyCmdCallback callback;
-    int32_t param;
-} DuckyCmd;
-
-static int32_t ducky_fnc_delay(BadBtScript* bad_bt, const char* line, int32_t param) {
-    UNUSED(param);
-
-    line = &line[ducky_get_command_len(line) + 1];
-    uint32_t delay_val = 0;
-    bool state = ducky_get_number(line, &delay_val);
-    if((state) && (delay_val > 0)) {
-        return (int32_t)delay_val;
-    }
-
-    return ducky_error(bad_bt, "Invalid number %s", line);
-}
-
-static int32_t ducky_fnc_defdelay(BadBtScript* bad_bt, const char* line, int32_t param) {
-    UNUSED(param);
-
-    line = &line[ducky_get_command_len(line) + 1];
-    bool state = ducky_get_number(line, &bad_bt->defdelay);
-    if(!state) {
-        return ducky_error(bad_bt, "Invalid number %s", line);
-    }
-    return 0;
-}
-
-static int32_t ducky_fnc_strdelay(BadBtScript* bad_bt, const char* line, int32_t param) {
-    UNUSED(param);
-
-    line = &line[ducky_get_command_len(line) + 1];
-    bool state = ducky_get_number(line, &bad_bt->stringdelay);
-    if(!state) {
-        return ducky_error(bad_bt, "Invalid number %s", line);
-    }
-    return 0;
-}
-
-static int32_t ducky_fnc_string(BadBtScript* bad_bt, const char* line, int32_t param) {
-    line = &line[ducky_get_command_len(line) + 1];
-    furi_string_set_str(bad_bt->string_print, line);
-    if(param == 1) {
-        furi_string_cat(bad_bt->string_print, "\n");
-    }
-
-    if(bad_bt->stringdelay == 0) { // stringdelay not set - run command immidiately
-        bool state = ducky_string(bad_bt, furi_string_get_cstr(bad_bt->string_print));
-        if(!state) {
-            return ducky_error(bad_bt, "Invalid string %s", line);
-        }
-    } else { // stringdelay is set - run command in thread to keep handling external events
-        return SCRIPT_STATE_STRING_START;
-    }
-
-    return 0;
-}
-
-static int32_t ducky_fnc_repeat(BadBtScript* bad_bt, const char* line, int32_t param) {
-    UNUSED(param);
-
-    line = &line[ducky_get_command_len(line) + 1];
-    bool state = ducky_get_number(line, &bad_bt->repeat_cnt);
-    if((!state) || (bad_bt->repeat_cnt == 0)) {
-        return ducky_error(bad_bt, "Invalid number %s", line);
-    }
-    return 0;
-}
-
-static int32_t ducky_fnc_sysrq(BadBtScript* bad_bt, const char* line, int32_t param) {
-    UNUSED(param);
-
-    line = &line[ducky_get_command_len(line) + 1];
-    uint16_t key = ducky_get_keycode(bad_bt, line, true);
-
-    furi_hal_bt_hid_kb_press(KEY_MOD_LEFT_ALT | HID_KEYBOARD_PRINT_SCREEN);
-    furi_hal_bt_hid_kb_press(key);
-    furi_delay_ms(bt_timeout);
-    furi_hal_bt_hid_kb_release(key);
-    furi_hal_bt_hid_kb_release(KEY_MOD_LEFT_ALT | HID_KEYBOARD_PRINT_SCREEN);
-    return 0;
-}
-
-static int32_t ducky_fnc_altchar(BadBtScript* bad_bt, const char* line, int32_t param) {
-    UNUSED(param);
-
-    line = &line[ducky_get_command_len(line) + 1];
-    ducky_numlock_on(bad_bt);
-    bool state = ducky_altchar(bad_bt, line);
-    if(!state) {
-        return ducky_error(bad_bt, "Invalid altchar %s", line);
-    }
-    return 0;
-}
-
-static int32_t ducky_fnc_altstring(BadBtScript* bad_bt, const char* line, int32_t param) {
-    UNUSED(param);
-
-    line = &line[ducky_get_command_len(line) + 1];
-    ducky_numlock_on(bad_bt);
-    bool state = ducky_altstring(bad_bt, line);
-    if(!state) {
-        return ducky_error(bad_bt, "Invalid altstring %s", line);
-    }
-    return 0;
-}
-
-static int32_t ducky_fnc_hold(BadBtScript* bad_bt, const char* line, int32_t param) {
-    UNUSED(param);
-
-    line = &line[ducky_get_command_len(line) + 1];
-    uint16_t key = ducky_get_keycode(bad_bt, line, true);
-    if(key == HID_KEYBOARD_NONE) {
-        return ducky_error(bad_bt, "No keycode defined for %s", line);
-    }
-    bad_bt->key_hold_nb++;
-    if(bad_bt->key_hold_nb > (HID_KB_MAX_KEYS - 1)) {
-        return ducky_error(bad_bt, "Too many keys are hold");
-    }
-    furi_hal_bt_hid_kb_press(key);
-
-    return 0;
-}
-
-static int32_t ducky_fnc_release(BadBtScript* bad_bt, const char* line, int32_t param) {
-    UNUSED(param);
-
-    line = &line[ducky_get_command_len(line) + 1];
-    uint16_t key = ducky_get_keycode(bad_bt, line, true);
-    if(key == HID_KEYBOARD_NONE) {
-        return ducky_error(bad_bt, "No keycode defined for %s", line);
-    }
-    if(bad_bt->key_hold_nb == 0) {
-        return ducky_error(bad_bt, "No keys are hold");
-    }
-    bad_bt->key_hold_nb--;
-    furi_hal_bt_hid_kb_release(key);
-    return 0;
-}
-
-static int32_t ducky_fnc_waitforbutton(BadBtScript* bad_bt, const char* line, int32_t param) {
-    UNUSED(param);
-    UNUSED(bad_bt);
-    UNUSED(line);
-
-    return SCRIPT_STATE_WAIT_FOR_BTN;
-}
-
-static const DuckyCmd ducky_commands[] = {
-    {"REM", NULL, -1},
-    {"ID", NULL, -1},
-    {"BT_ID", NULL, -1},
-    {"DELAY", ducky_fnc_delay, -1},
-    {"STRING", ducky_fnc_string, 0},
-    {"STRINGLN", ducky_fnc_string, 1},
-    {"DEFAULT_DELAY", ducky_fnc_defdelay, -1},
-    {"DEFAULTDELAY", ducky_fnc_defdelay, -1},
-    {"STRINGDELAY", ducky_fnc_strdelay, -1},
-    {"STRING_DELAY", ducky_fnc_strdelay, -1},
-    {"REPEAT", ducky_fnc_repeat, -1},
-    {"SYSRQ", ducky_fnc_sysrq, -1},
-    {"ALTCHAR", ducky_fnc_altchar, -1},
-    {"ALTSTRING", ducky_fnc_altstring, -1},
-    {"ALTCODE", ducky_fnc_altstring, -1},
-    {"HOLD", ducky_fnc_hold, -1},
-    {"RELEASE", ducky_fnc_release, -1},
-    {"WAIT_FOR_BUTTON_PRESS", ducky_fnc_waitforbutton, -1},
-};
-
-#define TAG "BadBT"
-#define WORKER_TAG TAG "Worker"
-
-int32_t ducky_execute_cmd(BadBtScript* bad_bt, const char* line) {
-    size_t cmd_word_len = strcspn(line, " ");
-    for(size_t i = 0; i < COUNT_OF(ducky_commands); i++) {
-        size_t cmd_compare_len = strlen(ducky_commands[i].name);
-
-        if(cmd_compare_len != cmd_word_len) {
-            continue;
-        }
-
-        if(strncmp(line, ducky_commands[i].name, cmd_compare_len) == 0) {
-            if(ducky_commands[i].callback == NULL) {
-                return 0;
-            } else {
-                return ((ducky_commands[i].callback)(bad_bt, line, ducky_commands[i].param));
-            }
-        }
-    }
-
-    return SCRIPT_STATE_CMD_UNKNOWN;
-}

+ 0 - 44
base_pack/bad_bt/helpers/ducky_script_i.h

@@ -1,44 +0,0 @@
-#pragma once
-
-#ifdef __cplusplus
-extern "C" {
-#endif
-
-#include <furi.h>
-#include <furi_hal.h>
-#include "ducky_script.h"
-
-#define SCRIPT_STATE_ERROR (-1)
-#define SCRIPT_STATE_END (-2)
-#define SCRIPT_STATE_NEXT_LINE (-3)
-#define SCRIPT_STATE_CMD_UNKNOWN (-4)
-#define SCRIPT_STATE_STRING_START (-5)
-#define SCRIPT_STATE_WAIT_FOR_BTN (-6)
-
-uint16_t ducky_get_keycode(BadBtScript* bad_bt, const char* param, bool accept_chars);
-
-uint32_t ducky_get_command_len(const char* line);
-
-bool ducky_is_line_end(const char chr);
-
-uint16_t ducky_get_keycode_by_name(const char* param);
-
-bool ducky_get_number(const char* param, uint32_t* val);
-
-void ducky_numlock_on(BadBtScript* bad_bt);
-
-bool ducky_numpad_press(BadBtScript* bad_bt, const char num);
-
-bool ducky_altchar(BadBtScript* bad_bt, const char* charcode);
-
-bool ducky_altstring(BadBtScript* bad_bt, const char* param);
-
-bool ducky_string(BadBtScript* bad_bt, const char* param);
-
-int32_t ducky_execute_cmd(BadBtScript* bad_bt, const char* line);
-
-int32_t ducky_error(BadBtScript* bad_bt, const char* text, ...);
-
-#ifdef __cplusplus
-}
-#endif

+ 0 - 30
base_pack/bad_bt/scenes/bad_bt_scene.c

@@ -1,30 +0,0 @@
-#include "bad_bt_scene.h"
-
-// Generate scene on_enter handlers array
-#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_enter,
-void (*const bad_bt_scene_on_enter_handlers[])(void*) = {
-#include "bad_bt_scene_config.h"
-};
-#undef ADD_SCENE
-
-// Generate scene on_event handlers array
-#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_event,
-bool (*const bad_bt_scene_on_event_handlers[])(void* context, SceneManagerEvent event) = {
-#include "bad_bt_scene_config.h"
-};
-#undef ADD_SCENE
-
-// Generate scene on_exit handlers array
-#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_exit,
-void (*const bad_bt_scene_on_exit_handlers[])(void* context) = {
-#include "bad_bt_scene_config.h"
-};
-#undef ADD_SCENE
-
-// Initialize scene handlers configuration structure
-const SceneManagerHandlers bad_bt_scene_handlers = {
-    .on_enter_handlers = bad_bt_scene_on_enter_handlers,
-    .on_event_handlers = bad_bt_scene_on_event_handlers,
-    .on_exit_handlers = bad_bt_scene_on_exit_handlers,
-    .scene_num = BadBtSceneNum,
-};

+ 0 - 104
base_pack/bad_bt/scenes/bad_bt_scene_config.c

@@ -1,104 +0,0 @@
-#include "../bad_bt_app.h"
-#include "../helpers/ducky_script.h"
-#include "furi_hal_power.h"
-
-enum VarItemListIndex {
-    VarItemListIndexKeyboardLayout,
-    VarItemListIndexBtRemember,
-    VarItemListIndexBtDeviceName,
-    VarItemListIndexBtMacAddress,
-    VarItemListIndexRandomizeBtMac,
-};
-
-void bad_bt_scene_config_bt_remember_callback(VariableItem* item) {
-    BadBtApp* bad_bt = variable_item_get_context(item);
-    bad_bt->bt_remember = variable_item_get_current_value_index(item);
-    variable_item_set_current_value_text(item, bad_bt->bt_remember ? "ON" : "OFF");
-    view_dispatcher_send_custom_event(bad_bt->view_dispatcher, VarItemListIndexBtRemember);
-}
-
-void bad_bt_scene_config_var_item_list_callback(void* context, uint32_t index) {
-    BadBtApp* bad_bt = context;
-    view_dispatcher_send_custom_event(bad_bt->view_dispatcher, index);
-}
-
-void bad_bt_scene_config_on_enter(void* context) {
-    BadBtApp* bad_bt = context;
-    VariableItemList* var_item_list = bad_bt->var_item_list;
-    VariableItem* item;
-
-    item = variable_item_list_add(var_item_list, "Keyboard layout", 0, NULL, bad_bt);
-
-    item = variable_item_list_add(
-        var_item_list, "BT Remember", 2, bad_bt_scene_config_bt_remember_callback, bad_bt);
-    variable_item_set_current_value_index(item, bad_bt->bt_remember);
-    variable_item_set_current_value_text(item, bad_bt->bt_remember ? "ON" : "OFF");
-
-    item = variable_item_list_add(var_item_list, "BT Device Name", 0, NULL, bad_bt);
-    if(bad_bt->bad_bt_script->set_bt_id) {
-        variable_item_set_locked(item, true, "Script has\nBT_ID cmd!\nLocked to\nset Name!");
-    }
-
-    item = variable_item_list_add(var_item_list, "BT MAC Address", 0, NULL, bad_bt);
-    if(bad_bt->bt_remember) {
-        variable_item_set_locked(item, true, "Remember\nmust be Off!");
-    } else if(bad_bt->bad_bt_script->set_bt_id) {
-        variable_item_set_locked(item, true, "Script has\nBT_ID cmd!\nLocked to\nset MAC!");
-    }
-
-    item = variable_item_list_add(var_item_list, "Randomize BT MAC", 0, NULL, bad_bt);
-    if(bad_bt->bt_remember) {
-        variable_item_set_locked(item, true, "Remember\nmust be Off!");
-    } else if(bad_bt->bad_bt_script->set_bt_id) {
-        variable_item_set_locked(item, true, "Script has\nBT_ID cmd!\nLocked to\nset MAC!");
-    }
-
-    variable_item_list_set_enter_callback(
-        var_item_list, bad_bt_scene_config_var_item_list_callback, bad_bt);
-
-    variable_item_list_set_selected_item(
-        var_item_list, scene_manager_get_scene_state(bad_bt->scene_manager, BadBtSceneConfig));
-
-    view_dispatcher_switch_to_view(bad_bt->view_dispatcher, BadBtAppViewConfig);
-}
-
-bool bad_bt_scene_config_on_event(void* context, SceneManagerEvent event) {
-    BadBtApp* bad_bt = context;
-    bool consumed = false;
-
-    if(event.type == SceneManagerEventTypeCustom) {
-        scene_manager_set_scene_state(bad_bt->scene_manager, BadBtSceneConfig, event.event);
-        consumed = true;
-        switch(event.event) {
-        case VarItemListIndexKeyboardLayout:
-            scene_manager_next_scene(bad_bt->scene_manager, BadBtSceneConfigLayout);
-            break;
-        case VarItemListIndexBtRemember:
-            bad_bt_config_switch_remember_mode(bad_bt);
-            scene_manager_previous_scene(bad_bt->scene_manager);
-            scene_manager_next_scene(bad_bt->scene_manager, BadBtSceneConfig);
-            break;
-        case VarItemListIndexBtDeviceName:
-            scene_manager_next_scene(bad_bt->scene_manager, BadBtSceneConfigName);
-            break;
-        case VarItemListIndexBtMacAddress:
-            scene_manager_next_scene(bad_bt->scene_manager, BadBtSceneConfigMac);
-            break;
-        case VarItemListIndexRandomizeBtMac:
-            furi_hal_random_fill_buf(bad_bt->config.bt_mac, BAD_BT_MAC_ADDRESS_LEN);
-            bt_set_profile_mac_address(bad_bt->bt, bad_bt->config.bt_mac);
-            break;
-        default:
-            break;
-        }
-    }
-
-    return consumed;
-}
-
-void bad_bt_scene_config_on_exit(void* context) {
-    BadBtApp* bad_bt = context;
-    VariableItemList* var_item_list = bad_bt->var_item_list;
-
-    variable_item_list_reset(var_item_list);
-}

+ 0 - 7
base_pack/bad_bt/scenes/bad_bt_scene_config.h

@@ -1,7 +0,0 @@
-ADD_SCENE(bad_bt, file_select, FileSelect)
-ADD_SCENE(bad_bt, work, Work)
-ADD_SCENE(bad_bt, error, Error)
-ADD_SCENE(bad_bt, config, Config)
-ADD_SCENE(bad_bt, config_layout, ConfigLayout)
-ADD_SCENE(bad_bt, config_name, ConfigName)
-ADD_SCENE(bad_bt, config_mac, ConfigMac)

+ 0 - 47
base_pack/bad_bt/scenes/bad_bt_scene_config_layout.c

@@ -1,47 +0,0 @@
-#include "../bad_bt_app.h"
-#include "furi_hal_power.h"
-#include <storage/storage.h>
-
-static bool bad_bt_layout_select(BadBtApp* bad_bt) {
-    furi_assert(bad_bt);
-
-    FuriString* predefined_path;
-    predefined_path = furi_string_alloc();
-    if(!furi_string_empty(bad_bt->keyboard_layout)) {
-        furi_string_set(predefined_path, bad_bt->keyboard_layout);
-    } else {
-        furi_string_set(predefined_path, BAD_BT_APP_PATH_LAYOUT_FOLDER);
-    }
-
-    DialogsFileBrowserOptions browser_options;
-    dialog_file_browser_set_basic_options(
-        &browser_options, BAD_BT_APP_LAYOUT_EXTENSION, &I_keyboard_10px);
-    browser_options.base_path = BAD_BT_APP_PATH_LAYOUT_FOLDER;
-    browser_options.skip_assets = false;
-
-    // Input events and views are managed by file_browser
-    bool res = dialog_file_browser_show(
-        bad_bt->dialogs, bad_bt->keyboard_layout, predefined_path, &browser_options);
-
-    furi_string_free(predefined_path);
-    return res;
-}
-
-void bad_bt_scene_config_layout_on_enter(void* context) {
-    BadBtApp* bad_bt = context;
-
-    if(bad_bt_layout_select(bad_bt)) {
-        bad_bt_script_set_keyboard_layout(bad_bt->bad_bt_script, bad_bt->keyboard_layout);
-    }
-    scene_manager_previous_scene(bad_bt->scene_manager);
-}
-
-bool bad_bt_scene_config_layout_on_event(void* context, SceneManagerEvent event) {
-    UNUSED(context);
-    UNUSED(event);
-    return false;
-}
-
-void bad_bt_scene_config_layout_on_exit(void* context) {
-    UNUSED(context);
-}

+ 0 - 52
base_pack/bad_bt/scenes/bad_bt_scene_config_mac.c

@@ -1,52 +0,0 @@
-#include "../bad_bt_app.h"
-
-#define TAG "BadBtConfigMac"
-
-void bad_bt_scene_config_mac_byte_input_callback(void* context) {
-    BadBtApp* bad_bt = context;
-
-    view_dispatcher_send_custom_event(bad_bt->view_dispatcher, BadBtAppCustomEventByteInputDone);
-}
-
-void bad_bt_scene_config_mac_on_enter(void* context) {
-    BadBtApp* bad_bt = context;
-
-    furi_hal_bt_reverse_mac_addr(bad_bt->config.bt_mac);
-
-    // Setup view
-    ByteInput* byte_input = bad_bt->byte_input;
-    byte_input_set_header_text(byte_input, "Set BT MAC address");
-    byte_input_set_result_callback(
-        byte_input,
-        bad_bt_scene_config_mac_byte_input_callback,
-        NULL,
-        bad_bt,
-        bad_bt->config.bt_mac,
-        GAP_MAC_ADDR_SIZE);
-    view_dispatcher_switch_to_view(bad_bt->view_dispatcher, BadBtAppViewConfigMac);
-}
-
-bool bad_bt_scene_config_mac_on_event(void* context, SceneManagerEvent event) {
-    BadBtApp* bad_bt = context;
-    bool consumed = false;
-
-    if(event.type == SceneManagerEventTypeCustom) {
-        if(event.event == BadBtAppCustomEventByteInputDone) {
-            scene_manager_previous_scene(bad_bt->scene_manager);
-            consumed = true;
-        }
-    }
-    return consumed;
-}
-
-void bad_bt_scene_config_mac_on_exit(void* context) {
-    BadBtApp* bad_bt = context;
-
-    furi_hal_bt_reverse_mac_addr(bad_bt->config.bt_mac);
-
-    bt_set_profile_mac_address(bad_bt->bt, bad_bt->config.bt_mac);
-
-    // Clear view
-    byte_input_set_result_callback(bad_bt->byte_input, NULL, NULL, NULL, NULL, 0);
-    byte_input_set_header_text(bad_bt->byte_input, "");
-}

+ 0 - 45
base_pack/bad_bt/scenes/bad_bt_scene_config_name.c

@@ -1,45 +0,0 @@
-#include "../bad_bt_app.h"
-
-static void bad_bt_scene_config_name_text_input_callback(void* context) {
-    BadBtApp* bad_bt = context;
-
-    view_dispatcher_send_custom_event(bad_bt->view_dispatcher, BadBtAppCustomEventTextEditResult);
-}
-
-void bad_bt_scene_config_name_on_enter(void* context) {
-    BadBtApp* bad_bt = context;
-    TextInput* text_input = bad_bt->text_input;
-
-    text_input_set_header_text(text_input, "Set BT device name");
-
-    text_input_set_result_callback(
-        text_input,
-        bad_bt_scene_config_name_text_input_callback,
-        bad_bt,
-        bad_bt->config.bt_name,
-        BAD_BT_ADV_NAME_MAX_LEN,
-        true);
-
-    view_dispatcher_switch_to_view(bad_bt->view_dispatcher, BadBtAppViewConfigName);
-}
-
-bool bad_bt_scene_config_name_on_event(void* context, SceneManagerEvent event) {
-    BadBtApp* bad_bt = context;
-    bool consumed = false;
-
-    if(event.type == SceneManagerEventTypeCustom) {
-        consumed = true;
-        if(event.event == BadBtAppCustomEventTextEditResult) {
-            bt_set_profile_adv_name(bad_bt->bt, bad_bt->config.bt_name);
-        }
-        scene_manager_previous_scene(bad_bt->scene_manager);
-    }
-    return consumed;
-}
-
-void bad_bt_scene_config_name_on_exit(void* context) {
-    BadBtApp* bad_bt = context;
-    TextInput* text_input = bad_bt->text_input;
-
-    text_input_reset(text_input);
-}

+ 0 - 61
base_pack/bad_bt/scenes/bad_bt_scene_error.c

@@ -1,61 +0,0 @@
-#include "../bad_bt_app.h"
-
-static void
-    bad_bt_scene_error_event_callback(GuiButtonType result, InputType type, void* context) {
-    furi_assert(context);
-    BadBtApp* app = context;
-
-    if((result == GuiButtonTypeLeft) && (type == InputTypeShort)) {
-        view_dispatcher_send_custom_event(app->view_dispatcher, BadBtCustomEventErrorBack);
-    }
-}
-
-void bad_bt_scene_error_on_enter(void* context) {
-    BadBtApp* app = context;
-
-    if(app->error == BadBtAppErrorNoFiles) {
-        widget_add_icon_element(app->widget, 0, 0, &I_SDQuestion_35x43);
-        widget_add_string_multiline_element(
-            app->widget,
-            81,
-            4,
-            AlignCenter,
-            AlignTop,
-            FontSecondary,
-            "No SD card or\napp data found.\nThis app will not\nwork without\nrequired files.");
-        widget_add_button_element(
-            app->widget, GuiButtonTypeLeft, "Back", bad_bt_scene_error_event_callback, app);
-    } else if(app->error == BadBtAppErrorCloseRpc) {
-        widget_add_icon_element(app->widget, 78, 0, &I_ActiveConnection_50x64);
-        widget_add_string_multiline_element(
-            app->widget, 3, 2, AlignLeft, AlignTop, FontPrimary, "Connection\nis active!");
-        widget_add_string_multiline_element(
-            app->widget,
-            3,
-            30,
-            AlignLeft,
-            AlignTop,
-            FontSecondary,
-            "Disconnect from\nPC or phone to\nuse this function.");
-    }
-
-    view_dispatcher_switch_to_view(app->view_dispatcher, BadBtAppViewError);
-}
-
-bool bad_bt_scene_error_on_event(void* context, SceneManagerEvent event) {
-    BadBtApp* app = context;
-    bool consumed = false;
-
-    if(event.type == SceneManagerEventTypeCustom) {
-        if(event.event == BadBtCustomEventErrorBack) {
-            view_dispatcher_stop(app->view_dispatcher);
-            consumed = true;
-        }
-    }
-    return consumed;
-}
-
-void bad_bt_scene_error_on_exit(void* context) {
-    BadBtApp* app = context;
-    widget_reset(app->widget);
-}

+ 0 - 49
base_pack/bad_bt/scenes/bad_bt_scene_file_select.c

@@ -1,49 +0,0 @@
-#include "../bad_bt_app.h"
-#include <furi_hal_power.h>
-#include <storage/storage.h>
-
-static bool bad_bt_file_select(BadBtApp* bad_bt) {
-    furi_assert(bad_bt);
-
-    DialogsFileBrowserOptions browser_options;
-    dialog_file_browser_set_basic_options(
-        &browser_options, BAD_BT_APP_SCRIPT_EXTENSION, &I_badbt_10px);
-    browser_options.base_path = BAD_BT_APP_BASE_FOLDER;
-    browser_options.skip_assets = true;
-
-    // Input events and views are managed by file_browser
-    bool res = dialog_file_browser_show(
-        bad_bt->dialogs, bad_bt->file_path, bad_bt->file_path, &browser_options);
-
-    return res;
-}
-
-void bad_bt_scene_file_select_on_enter(void* context) {
-    BadBtApp* bad_bt = context;
-
-    if(bad_bt->bad_bt_script) {
-        bad_bt_script_close(bad_bt->bad_bt_script);
-        bad_bt->bad_bt_script = NULL;
-    }
-
-    if(bad_bt_file_select(bad_bt)) {
-        bad_bt->bad_bt_script = bad_bt_script_open(bad_bt->file_path, bad_bt->bt, bad_bt);
-        bad_bt_script_set_keyboard_layout(bad_bt->bad_bt_script, bad_bt->keyboard_layout);
-
-        scene_manager_next_scene(bad_bt->scene_manager, BadBtSceneWork);
-    } else {
-        view_dispatcher_stop(bad_bt->view_dispatcher);
-    }
-}
-
-bool bad_bt_scene_file_select_on_event(void* context, SceneManagerEvent event) {
-    UNUSED(context);
-    UNUSED(event);
-    // BadBtApp* bad_bt = context;
-    return false;
-}
-
-void bad_bt_scene_file_select_on_exit(void* context) {
-    UNUSED(context);
-    // BadBtApp* bad_bt = context;
-}

+ 0 - 56
base_pack/bad_bt/scenes/bad_bt_scene_work.c

@@ -1,56 +0,0 @@
-#include "../helpers/ducky_script.h"
-#include "../bad_bt_app.h"
-#include "../views/bad_bt_view.h"
-#include <furi_hal.h>
-#include "toolbox/path.h"
-
-void bad_bt_scene_work_button_callback(InputKey key, void* context) {
-    furi_assert(context);
-    BadBtApp* app = context;
-    view_dispatcher_send_custom_event(app->view_dispatcher, key);
-}
-
-bool bad_bt_scene_work_on_event(void* context, SceneManagerEvent event) {
-    BadBtApp* app = context;
-    bool consumed = false;
-
-    if(event.type == SceneManagerEventTypeCustom) {
-        if(event.event == InputKeyLeft) {
-            if(bad_bt_is_idle_state(app->bad_bt_view)) {
-                scene_manager_next_scene(app->scene_manager, BadBtSceneConfig);
-            }
-            consumed = true;
-        } else if(event.event == InputKeyOk) {
-            bad_bt_script_toggle(app->bad_bt_script);
-            consumed = true;
-        }
-    } else if(event.type == SceneManagerEventTypeTick) {
-        bad_bt_set_state(app->bad_bt_view, bad_bt_script_get_state(app->bad_bt_script));
-    }
-    return consumed;
-}
-
-void bad_bt_scene_work_on_enter(void* context) {
-    BadBtApp* app = context;
-
-    FuriString* file_name;
-    file_name = furi_string_alloc();
-    path_extract_filename(app->file_path, file_name, true);
-    bad_bt_set_file_name(app->bad_bt_view, furi_string_get_cstr(file_name));
-    furi_string_free(file_name);
-
-    FuriString* layout;
-    layout = furi_string_alloc();
-    path_extract_filename(app->keyboard_layout, layout, true);
-    bad_bt_set_layout(app->bad_bt_view, furi_string_get_cstr(layout));
-    furi_string_free(layout);
-
-    bad_bt_set_state(app->bad_bt_view, bad_bt_script_get_state(app->bad_bt_script));
-
-    bad_bt_set_button_callback(app->bad_bt_view, bad_bt_scene_work_button_callback, app);
-    view_dispatcher_switch_to_view(app->view_dispatcher, BadBtAppViewWork);
-}
-
-void bad_bt_scene_work_on_exit(void* context) {
-    UNUSED(context);
-}

+ 0 - 233
base_pack/bad_bt/views/bad_bt_view.c

@@ -1,233 +0,0 @@
-#include "bad_bt_view.h"
-#include "../helpers/ducky_script.h"
-#include "../bad_bt_app.h"
-#include <toolbox/path.h>
-#include <gui/elements.h>
-#include <assets_icons.h>
-
-#define MAX_NAME_LEN 64
-
-typedef struct {
-    char file_name[MAX_NAME_LEN];
-    char layout[MAX_NAME_LEN];
-    BadBtState state;
-    uint8_t anim_frame;
-} BadBtModel;
-
-static void bad_bt_draw_callback(Canvas* canvas, void* _model) {
-    BadBtModel* model = _model;
-
-    FuriString* disp_str;
-    disp_str = furi_string_alloc_set("(BT) ");
-    furi_string_cat_str(disp_str, model->file_name);
-    elements_string_fit_width(canvas, disp_str, 128 - 2);
-    canvas_set_font(canvas, FontSecondary);
-    canvas_draw_str(canvas, 2, 8, furi_string_get_cstr(disp_str));
-
-    if(strlen(model->layout) == 0) {
-        furi_string_set(disp_str, "(default)");
-    } else {
-        furi_string_reset(disp_str);
-        furi_string_push_back(disp_str, '(');
-        for(size_t i = 0; i < strlen(model->layout); i++)
-            furi_string_push_back(disp_str, model->layout[i]);
-        furi_string_push_back(disp_str, ')');
-    }
-    if(model->state.pin) {
-        furi_string_cat_printf(disp_str, "  PIN: %ld", model->state.pin);
-    }
-    elements_string_fit_width(canvas, disp_str, 128 - 2);
-    canvas_draw_str(
-        canvas, 2, 8 + canvas_current_font_height(canvas), furi_string_get_cstr(disp_str));
-
-    furi_string_reset(disp_str);
-
-    if((model->state.state == BadBtStateIdle) || (model->state.state == BadBtStateDone) ||
-       (model->state.state == BadBtStateNotConnected)) {
-        elements_button_center(canvas, "Run");
-        elements_button_left(canvas, "Config");
-    } else if((model->state.state == BadBtStateRunning) || (model->state.state == BadBtStateDelay)) {
-        elements_button_center(canvas, "Stop");
-    } else if(model->state.state == BadBtStateWaitForBtn) {
-        elements_button_center(canvas, "Press to continue");
-    } else if(model->state.state == BadBtStateWillRun) {
-        elements_button_center(canvas, "Cancel");
-    }
-
-    if(model->state.state == BadBtStateNotConnected) {
-        canvas_draw_icon(canvas, 4, 26, &I_Clock_18x18);
-        canvas_set_font(canvas, FontPrimary);
-        canvas_draw_str_aligned(canvas, 127, 31, AlignRight, AlignBottom, "Connect to");
-        canvas_draw_str_aligned(canvas, 127, 43, AlignRight, AlignBottom, "a device");
-    } else if(model->state.state == BadBtStateWillRun) {
-        canvas_draw_icon(canvas, 4, 26, &I_Clock_18x18);
-        canvas_set_font(canvas, FontPrimary);
-        canvas_draw_str_aligned(canvas, 127, 31, AlignRight, AlignBottom, "Will run");
-        canvas_draw_str_aligned(canvas, 127, 43, AlignRight, AlignBottom, "on connect");
-    } else if(model->state.state == BadBtStateFileError) {
-        canvas_draw_icon(canvas, 4, 26, &I_Error_18x18);
-        canvas_set_font(canvas, FontPrimary);
-        canvas_draw_str_aligned(canvas, 127, 31, AlignRight, AlignBottom, "File");
-        canvas_draw_str_aligned(canvas, 127, 43, AlignRight, AlignBottom, "ERROR");
-    } else if(model->state.state == BadBtStateScriptError) {
-        canvas_draw_icon(canvas, 4, 26, &I_Error_18x18);
-        canvas_set_font(canvas, FontPrimary);
-        canvas_draw_str_aligned(canvas, 127, 33, AlignRight, AlignBottom, "ERROR:");
-        canvas_set_font(canvas, FontSecondary);
-        furi_string_printf(disp_str, "line %u", model->state.error_line);
-        canvas_draw_str_aligned(
-            canvas, 127, 46, AlignRight, AlignBottom, furi_string_get_cstr(disp_str));
-        furi_string_reset(disp_str);
-        furi_string_set_str(disp_str, model->state.error);
-        elements_string_fit_width(canvas, disp_str, canvas_width(canvas));
-        canvas_draw_str_aligned(
-            canvas, 127, 56, AlignRight, AlignBottom, furi_string_get_cstr(disp_str));
-        furi_string_reset(disp_str);
-    } else if(model->state.state == BadBtStateIdle) {
-        canvas_draw_icon(canvas, 4, 26, &I_Smile_18x18);
-        canvas_set_font(canvas, FontBigNumbers);
-        canvas_draw_str_aligned(canvas, 114, 40, AlignRight, AlignBottom, "0");
-        canvas_draw_icon(canvas, 117, 26, &I_Percent_10x14);
-    } else if(model->state.state == BadBtStateRunning) {
-        if(model->anim_frame == 0) {
-            canvas_draw_icon(canvas, 4, 23, &I_EviSmile1_18x21);
-        } else {
-            canvas_draw_icon(canvas, 4, 23, &I_EviSmile2_18x21);
-        }
-        canvas_set_font(canvas, FontBigNumbers);
-        furi_string_printf(
-            disp_str, "%u", ((model->state.line_cur - 1) * 100) / model->state.line_nb);
-        canvas_draw_str_aligned(
-            canvas, 114, 40, AlignRight, AlignBottom, furi_string_get_cstr(disp_str));
-        furi_string_reset(disp_str);
-        canvas_draw_icon(canvas, 117, 26, &I_Percent_10x14);
-    } else if(model->state.state == BadBtStateDone) {
-        canvas_draw_icon(canvas, 4, 23, &I_EviSmile1_18x21);
-        canvas_set_font(canvas, FontBigNumbers);
-        canvas_draw_str_aligned(canvas, 114, 40, AlignRight, AlignBottom, "100");
-        furi_string_reset(disp_str);
-        canvas_draw_icon(canvas, 117, 26, &I_Percent_10x14);
-    } else if(model->state.state == BadBtStateDelay) {
-        if(model->anim_frame == 0) {
-            canvas_draw_icon(canvas, 4, 23, &I_EviWaiting1_18x21);
-        } else {
-            canvas_draw_icon(canvas, 4, 23, &I_EviWaiting2_18x21);
-        }
-        canvas_set_font(canvas, FontBigNumbers);
-        furi_string_printf(
-            disp_str, "%u", ((model->state.line_cur - 1) * 100) / model->state.line_nb);
-        canvas_draw_str_aligned(
-            canvas, 114, 40, AlignRight, AlignBottom, furi_string_get_cstr(disp_str));
-        furi_string_reset(disp_str);
-        canvas_draw_icon(canvas, 117, 26, &I_Percent_10x14);
-        canvas_set_font(canvas, FontSecondary);
-        furi_string_printf(disp_str, "delay %lus", model->state.delay_remain);
-        canvas_draw_str_aligned(
-            canvas, 127, 50, AlignRight, AlignBottom, furi_string_get_cstr(disp_str));
-        furi_string_reset(disp_str);
-    } else {
-        canvas_draw_icon(canvas, 4, 26, &I_Clock_18x18);
-    }
-
-    furi_string_free(disp_str);
-}
-
-static bool bad_bt_input_callback(InputEvent* event, void* context) {
-    furi_assert(context);
-    BadBt* bad_bt = context;
-    bool consumed = false;
-
-    if(event->type == InputTypeShort) {
-        if((event->key == InputKeyLeft) || (event->key == InputKeyOk)) {
-            consumed = true;
-            furi_assert(bad_bt->callback);
-            bad_bt->callback(event->key, bad_bt->context);
-        }
-    }
-
-    return consumed;
-}
-
-BadBt* bad_bt_alloc() {
-    BadBt* bad_bt = malloc(sizeof(BadBt));
-
-    bad_bt->view = view_alloc();
-    view_allocate_model(bad_bt->view, ViewModelTypeLocking, sizeof(BadBtModel));
-    view_set_context(bad_bt->view, bad_bt);
-    view_set_draw_callback(bad_bt->view, bad_bt_draw_callback);
-    view_set_input_callback(bad_bt->view, bad_bt_input_callback);
-
-    return bad_bt;
-}
-
-void bad_bt_free(BadBt* bad_bt) {
-    furi_assert(bad_bt);
-    view_free(bad_bt->view);
-    free(bad_bt);
-}
-
-View* bad_bt_get_view(BadBt* bad_bt) {
-    furi_assert(bad_bt);
-    return bad_bt->view;
-}
-
-void bad_bt_set_button_callback(BadBt* bad_bt, BadBtButtonCallback callback, void* context) {
-    furi_assert(bad_bt);
-    furi_assert(callback);
-    with_view_model(
-        bad_bt->view,
-        BadBtModel * model,
-        {
-            UNUSED(model);
-            bad_bt->callback = callback;
-            bad_bt->context = context;
-        },
-        true);
-}
-
-void bad_bt_set_file_name(BadBt* bad_bt, const char* name) {
-    furi_assert(name);
-    with_view_model(
-        bad_bt->view, BadBtModel * model, { strlcpy(model->file_name, name, MAX_NAME_LEN); }, true);
-}
-
-void bad_bt_set_layout(BadBt* bad_bt, const char* layout) {
-    furi_assert(layout);
-    with_view_model(
-        bad_bt->view, BadBtModel * model, { strlcpy(model->layout, layout, MAX_NAME_LEN); }, true);
-}
-
-void bad_bt_set_state(BadBt* bad_bt, BadBtState* st) {
-    furi_assert(st);
-    uint32_t pin = 0;
-    if(bad_bt->context != NULL) {
-        BadBtApp* app = bad_bt->context;
-        if(app->bt != NULL) {
-            pin = app->bt->pin;
-        }
-    }
-    st->pin = pin;
-    with_view_model(
-        bad_bt->view,
-        BadBtModel * model,
-        {
-            memcpy(&(model->state), st, sizeof(BadBtState));
-            model->anim_frame ^= 1;
-        },
-        true);
-}
-
-bool bad_bt_is_idle_state(BadBt* bad_bt) {
-    bool is_idle = false;
-    with_view_model(
-        bad_bt->view,
-        BadBtModel * model,
-        {
-            if((model->state.state == BadBtStateIdle) || (model->state.state == BadBtStateDone) ||
-               (model->state.state == BadBtStateNotConnected)) {
-                is_idle = true;
-            }
-        },
-        false);
-    return is_idle;
-}

+ 0 - 29
base_pack/bad_bt/views/bad_bt_view.h

@@ -1,29 +0,0 @@
-#pragma once
-
-#include <gui/view.h>
-
-typedef void (*BadBtButtonCallback)(InputKey key, void* context);
-
-typedef struct {
-    View* view;
-    BadBtButtonCallback callback;
-    void* context;
-} BadBt;
-
-typedef struct BadBtState BadBtState;
-
-BadBt* bad_bt_alloc();
-
-void bad_bt_free(BadBt* bad_bt);
-
-View* bad_bt_get_view(BadBt* bad_bt);
-
-void bad_bt_set_button_callback(BadBt* bad_bt, BadBtButtonCallback callback, void* context);
-
-void bad_bt_set_file_name(BadBt* bad_bt, const char* name);
-
-void bad_bt_set_layout(BadBt* bad_bt, const char* layout);
-
-void bad_bt_set_state(BadBt* bad_bt, BadBtState* st);
-
-bool bad_bt_is_idle_state(BadBt* bad_bt);

+ 11 - 0
base_pack/bad_kb/application.fam

@@ -0,0 +1,11 @@
+App(
+    appid="bad_kb",
+    name="Bad KB",
+    apptype=FlipperAppType.EXTERNAL,
+    entry_point="bad_kb_app",
+    stack_size=2 * 1024,
+    order=70,
+    fap_icon="icon.png",
+    fap_category="Tools",
+    fap_libs=["assets"],
+)

+ 440 - 0
base_pack/bad_kb/bad_kb_app.c

@@ -0,0 +1,440 @@
+#include "bad_kb_app_i.h"
+#include <furi.h>
+#include <furi_hal.h>
+#include <storage/storage.h>
+#include <lib/toolbox/path.h>
+#include <lib/flipper_format/flipper_format.h>
+#include <bt/bt_service/bt_i.h>
+#include "helpers/ducky_script_i.h"
+
+// Adjusts to serial MAC +2 in app init
+uint8_t BAD_KB_BOUND_MAC[GAP_MAC_ADDR_SIZE] = {0};
+
+static bool bad_kb_app_custom_event_callback(void* context, uint32_t event) {
+    furi_assert(context);
+    BadKbApp* app = context;
+    return scene_manager_handle_custom_event(app->scene_manager, event);
+}
+
+static bool bad_kb_app_back_event_callback(void* context) {
+    furi_assert(context);
+    BadKbApp* app = context;
+    return scene_manager_handle_back_event(app->scene_manager);
+}
+
+static void bad_kb_app_tick_event_callback(void* context) {
+    furi_assert(context);
+    BadKbApp* app = context;
+    scene_manager_handle_tick_event(app->scene_manager);
+}
+
+static void bad_kb_load_settings(BadKbApp* app) {
+    furi_string_reset(app->keyboard_layout);
+    BadKbConfig* cfg = &app->config;
+
+    Storage* storage = furi_record_open(RECORD_STORAGE);
+    FlipperFormat* file = flipper_format_file_alloc(storage);
+    if(flipper_format_file_open_existing(file, BAD_KB_SETTINGS_PATH)) {
+        FuriString* tmp_str = furi_string_alloc();
+        if(!flipper_format_read_string(file, "Keyboard_Layout", app->keyboard_layout)) {
+            furi_string_reset(app->keyboard_layout);
+        }
+        if(flipper_format_read_string(file, "Bt_Name", tmp_str) && !furi_string_empty(tmp_str)) {
+            strlcpy(cfg->ble.name, furi_string_get_cstr(tmp_str), sizeof(cfg->ble.name));
+        } else {
+            strcpy(cfg->ble.name, "");
+        }
+        if(!flipper_format_read_hex(
+               file, "Bt_Mac", (uint8_t*)&cfg->ble.mac, sizeof(cfg->ble.mac))) {
+            memset(cfg->ble.mac, 0, sizeof(cfg->ble.mac));
+        }
+        if(flipper_format_read_string(file, "Usb_Manuf", tmp_str) && !furi_string_empty(tmp_str)) {
+            strlcpy(cfg->usb.manuf, furi_string_get_cstr(tmp_str), sizeof(cfg->usb.manuf));
+        } else {
+            strcpy(cfg->usb.manuf, "");
+        }
+        if(flipper_format_read_string(file, "Usb_Product", tmp_str) &&
+           !furi_string_empty(tmp_str)) {
+            strlcpy(cfg->usb.product, furi_string_get_cstr(tmp_str), sizeof(cfg->usb.product));
+        } else {
+            strcpy(cfg->usb.product, "");
+        }
+        if(!flipper_format_read_uint32(file, "Usb_Vid", &cfg->usb.vid, 1)) {
+            cfg->usb.vid = 0;
+        }
+        if(!flipper_format_read_uint32(file, "Usb_Pid", &cfg->usb.pid, 1)) {
+            cfg->usb.pid = 0;
+        }
+        if(!flipper_format_read_bool(file, "Bt_Remember", &cfg->bt_remember, 1)) {
+            cfg->bt_remember = false;
+        }
+        if(!flipper_format_read_bool(file, "Is_Ble", &cfg->is_ble, 1)) {
+            cfg->is_ble = false;
+        }
+        furi_string_free(tmp_str);
+        flipper_format_file_close(file);
+    }
+    flipper_format_free(file);
+
+    if(!furi_string_empty(app->keyboard_layout)) {
+        FileInfo layout_file_info;
+        FS_Error file_check_err = storage_common_stat(
+            storage, furi_string_get_cstr(app->keyboard_layout), &layout_file_info);
+        if(file_check_err != FSE_OK) {
+            furi_string_reset(app->keyboard_layout);
+            return;
+        }
+        if(layout_file_info.size != 256) {
+            furi_string_reset(app->keyboard_layout);
+        }
+    }
+
+    furi_record_close(RECORD_STORAGE);
+}
+
+static void bad_kb_save_settings(BadKbApp* app) {
+    BadKbConfig* cfg = &app->config;
+    Storage* storage = furi_record_open(RECORD_STORAGE);
+    FlipperFormat* file = flipper_format_file_alloc(storage);
+    if(flipper_format_file_open_always(file, BAD_KB_SETTINGS_PATH)) {
+        flipper_format_write_string(file, "Keyboard_Layout", app->keyboard_layout);
+        flipper_format_write_string_cstr(file, "Bt_Name", cfg->ble.name);
+        flipper_format_write_hex(file, "Bt_Mac", (uint8_t*)&cfg->ble.mac, sizeof(cfg->ble.mac));
+        flipper_format_write_string_cstr(file, "Usb_Manuf", cfg->usb.manuf);
+        flipper_format_write_string_cstr(file, "Usb_Product", cfg->usb.product);
+        flipper_format_write_uint32(file, "Usb_Vid", &cfg->usb.vid, 1);
+        flipper_format_write_uint32(file, "Usb_Pid", &cfg->usb.pid, 1);
+        flipper_format_write_bool(file, "Bt_Remember", &cfg->bt_remember, 1);
+        flipper_format_write_bool(file, "Is_Ble", &cfg->is_ble, 1);
+        flipper_format_file_close(file);
+    }
+    flipper_format_free(file);
+    furi_record_close(RECORD_STORAGE);
+}
+
+void bad_kb_app_show_loading_popup(BadKbApp* app, bool show) {
+    if(show) {
+        // Raise timer priority so that animations can play
+        furi_timer_set_thread_priority(FuriTimerThreadPriorityElevated);
+        view_dispatcher_switch_to_view(app->view_dispatcher, BadKbAppViewLoading);
+    } else {
+        // Restore default timer priority
+        furi_timer_set_thread_priority(FuriTimerThreadPriorityNormal);
+    }
+}
+
+int32_t bad_kb_conn_apply(BadKbApp* app) {
+    if(app->is_bt) {
+        bt_timeout = bt_hid_delays[LevelRssi39_0];
+        bt_disconnect(app->bt);
+        furi_delay_ms(200);
+        bt_keys_storage_set_storage_path(app->bt, BAD_KB_KEYS_PATH);
+
+        // Setup new config
+        BadKbConfig* cfg = app->set_bt_id ? &app->id_config : &app->config;
+        memcpy(&app->cur_ble_cfg, &cfg->ble, sizeof(cfg->ble));
+        app->cur_ble_cfg.bonding = app->bt_remember;
+        if(app->bt_remember) {
+            app->cur_ble_cfg.pairing = GapPairingPinCodeVerifyYesNo;
+        } else {
+            app->cur_ble_cfg.pairing = GapPairingNone;
+            memcpy(app->cur_ble_cfg.mac, BAD_KB_BOUND_MAC, sizeof(BAD_KB_BOUND_MAC));
+        }
+
+        // Set profile
+        app->ble_hid = bt_profile_start(app->bt, ble_profile_hid, &app->cur_ble_cfg);
+        furi_check(app->ble_hid);
+
+        // Advertise even if BT is off in settings
+        furi_hal_bt_start_advertising();
+
+        app->conn_mode = BadKbConnModeBt;
+
+    } else {
+        // Unlock RPC connections
+        furi_hal_usb_unlock();
+
+        // Context will apply with set_config only if pointer address is different, so we use a copy
+        FuriHalUsbHidConfig* cur_usb_cfg = malloc(sizeof(FuriHalUsbHidConfig));
+
+        // Setup new config
+        BadKbConfig* cfg = app->set_usb_id ? &app->id_config : &app->config;
+        memcpy(cur_usb_cfg, &cfg->usb, sizeof(cfg->usb));
+
+        // Set profile
+        furi_check(furi_hal_usb_set_config(&usb_hid, cur_usb_cfg));
+        if(app->cur_usb_cfg) free(app->cur_usb_cfg);
+        app->cur_usb_cfg = cur_usb_cfg;
+
+        app->conn_mode = BadKbConnModeUsb;
+    }
+
+    return 0;
+}
+
+void bad_kb_conn_reset(BadKbApp* app) {
+    if(app->conn_mode == BadKbConnModeBt) {
+        bt_disconnect(app->bt);
+        furi_delay_ms(200);
+        bt_keys_storage_set_default_path(app->bt);
+        furi_check(bt_profile_restore_default(app->bt));
+    } else if(app->conn_mode == BadKbConnModeUsb) {
+        // TODO: maybe also restore USB context?
+        furi_check(furi_hal_usb_set_config(app->prev_usb_mode, NULL));
+    }
+
+    app->conn_mode = BadKbConnModeNone;
+}
+
+void bad_kb_config_adjust(BadKbConfig* cfg) {
+    // Avoid empty name
+    if(cfg->ble.name[0] == '\0') {
+        snprintf(
+            cfg->ble.name, sizeof(cfg->ble.name), "Control %s", furi_hal_version_get_name_ptr());
+    }
+
+    const uint8_t* normal_mac = furi_hal_version_get_ble_mac();
+    uint8_t empty_mac[sizeof(cfg->ble.mac)] = {0};
+    uint8_t default_mac[sizeof(cfg->ble.mac)] = {0x6c, 0x7a, 0xd8, 0xac, 0x57, 0x72}; //furi_hal_bt
+    if(memcmp(cfg->ble.mac, empty_mac, sizeof(cfg->ble.mac)) == 0 ||
+       memcmp(cfg->ble.mac, normal_mac, sizeof(cfg->ble.mac)) == 0 ||
+       memcmp(cfg->ble.mac, default_mac, sizeof(cfg->ble.mac)) == 0) {
+        memcpy(cfg->ble.mac, normal_mac, sizeof(cfg->ble.mac));
+        cfg->ble.mac[2]++;
+    }
+
+    // Use defaults if vid or pid are unset
+    if(cfg->usb.vid == 0) cfg->usb.vid = 0x046D;
+    if(cfg->usb.pid == 0) cfg->usb.pid = 0xC529;
+}
+
+void bad_kb_config_refresh(BadKbApp* app) {
+    bt_set_status_changed_callback(app->bt, NULL, NULL);
+    furi_hal_hid_set_state_callback(NULL, NULL);
+    if(app->bad_kb_script) {
+        furi_thread_flags_set(furi_thread_get_id(app->bad_kb_script->thread), WorkerEvtDisconnect);
+    }
+    if(app->conn_init_thread) {
+        furi_thread_join(app->conn_init_thread);
+    }
+
+    bool apply = false;
+    if(app->is_bt) {
+        BadKbConfig* cfg = app->set_bt_id ? &app->id_config : &app->config;
+        bad_kb_config_adjust(cfg);
+
+        if(app->conn_mode != BadKbConnModeBt) {
+            apply = true;
+            bad_kb_conn_reset(app);
+        } else {
+            BleProfileHidParams* cur = &app->cur_ble_cfg;
+            apply = apply || cfg->ble.bonding != app->bt_remember;
+            apply = apply || strncmp(cfg->ble.name, cur->name, sizeof(cfg->ble.name));
+            apply = apply || memcmp(cfg->ble.mac, cur->mac, sizeof(cfg->ble.mac));
+        }
+    } else {
+        BadKbConfig* cfg = app->set_usb_id ? &app->id_config : &app->config;
+        bad_kb_config_adjust(cfg);
+
+        if(app->conn_mode != BadKbConnModeUsb) {
+            apply = true;
+            bad_kb_conn_reset(app);
+        } else {
+            FuriHalUsbHidConfig* cur = app->cur_usb_cfg;
+            apply = apply || cfg->usb.vid != cur->vid;
+            apply = apply || cfg->usb.pid != cur->pid;
+            apply = apply || strncmp(cfg->usb.manuf, cur->manuf, sizeof(cur->manuf));
+            apply = apply || strncmp(cfg->usb.product, cur->product, sizeof(cur->product));
+        }
+    }
+
+    if(apply) {
+        bad_kb_conn_apply(app);
+    }
+
+    if(app->bad_kb_script) {
+        BadKbScript* script = app->bad_kb_script;
+        script->st.is_bt = app->is_bt;
+        script->bt = app->is_bt ? app->bt : NULL;
+        bool connected;
+        if(app->is_bt) {
+            bt_set_status_changed_callback(app->bt, bad_kb_bt_hid_state_callback, script);
+            connected = furi_hal_bt_is_connected();
+        } else {
+            furi_hal_hid_set_state_callback(bad_kb_usb_hid_state_callback, script);
+            connected = furi_hal_hid_is_connected();
+        }
+        if(connected) {
+            furi_thread_flags_set(furi_thread_get_id(script->thread), WorkerEvtConnect);
+        }
+    }
+
+    // Reload config page
+    scene_manager_next_scene(app->scene_manager, BadKbSceneConfig);
+    scene_manager_previous_scene(app->scene_manager);
+
+    // Update settings
+    if((app->config.bt_remember != app->bt_remember) || (app->config.is_ble != app->is_bt)) {
+        app->config.bt_remember = app->bt_remember;
+        app->config.is_ble = app->is_bt;
+        bad_kb_save_settings(app);
+    }
+}
+
+BadKbApp* bad_kb_app_alloc(char* arg) {
+    BadKbApp* app = malloc(sizeof(BadKbApp));
+
+    app->bad_kb_script = NULL;
+
+    app->file_path = furi_string_alloc();
+    app->keyboard_layout = furi_string_alloc();
+    if(arg && strlen(arg)) {
+        furi_string_set(app->file_path, arg);
+    }
+
+    bad_kb_load_settings(app);
+
+    app->gui = furi_record_open(RECORD_GUI);
+    app->notifications = furi_record_open(RECORD_NOTIFICATION);
+    app->dialogs = furi_record_open(RECORD_DIALOGS);
+
+    app->view_dispatcher = view_dispatcher_alloc();
+    view_dispatcher_enable_queue(app->view_dispatcher);
+
+    app->scene_manager = scene_manager_alloc(&bad_kb_scene_handlers, app);
+
+    view_dispatcher_set_event_callback_context(app->view_dispatcher, app);
+    view_dispatcher_set_tick_event_callback(
+        app->view_dispatcher, bad_kb_app_tick_event_callback, 250);
+    view_dispatcher_set_custom_event_callback(
+        app->view_dispatcher, bad_kb_app_custom_event_callback);
+    view_dispatcher_set_navigation_event_callback(
+        app->view_dispatcher, bad_kb_app_back_event_callback);
+
+    Bt* bt = furi_record_open(RECORD_BT);
+    app->bt = bt;
+    app->bt->suppress_pin_screen = true;
+    app->is_bt = app->config.is_ble;
+    app->bt_remember = app->config.bt_remember;
+    bad_kb_config_adjust(&app->config);
+
+    // Save prev config
+    app->prev_usb_mode = furi_hal_usb_get_config();
+
+    // Adjust BT remember MAC to be serial MAC +2
+    memcpy(BAD_KB_BOUND_MAC, furi_hal_version_get_ble_mac(), sizeof(BAD_KB_BOUND_MAC));
+    BAD_KB_BOUND_MAC[2] += 2;
+
+    // Custom Widget
+    app->widget = widget_alloc();
+    view_dispatcher_add_view(
+        app->view_dispatcher, BadKbAppViewWidget, widget_get_view(app->widget));
+
+    app->var_item_list = variable_item_list_alloc();
+    view_dispatcher_add_view(
+        app->view_dispatcher,
+        BadKbAppViewVarItemList,
+        variable_item_list_get_view(app->var_item_list));
+
+    app->bad_kb_view = bad_kb_alloc();
+    view_dispatcher_add_view(
+        app->view_dispatcher, BadKbAppViewWork, bad_kb_get_view(app->bad_kb_view));
+
+    app->text_input = text_input_alloc();
+    view_dispatcher_add_view(
+        app->view_dispatcher, BadKbAppViewTextInput, text_input_get_view(app->text_input));
+
+    app->byte_input = byte_input_alloc();
+    view_dispatcher_add_view(
+        app->view_dispatcher, BadKbAppViewByteInput, byte_input_get_view(app->byte_input));
+
+    app->loading = loading_alloc();
+    view_dispatcher_add_view(
+        app->view_dispatcher, BadKbAppViewLoading, loading_get_view(app->loading));
+
+    view_dispatcher_attach_to_gui(app->view_dispatcher, app->gui, ViewDispatcherTypeFullscreen);
+
+    app->conn_mode = BadKbConnModeNone;
+    app->conn_init_thread =
+        furi_thread_alloc_ex("BadKbConnInit", 1024, (FuriThreadCallback)bad_kb_conn_apply, app);
+    furi_thread_start(app->conn_init_thread);
+    if(!furi_string_empty(app->file_path)) {
+        app->bad_kb_script = bad_kb_script_open(app->file_path, app->is_bt ? app->bt : NULL, app);
+        bad_kb_script_set_keyboard_layout(app->bad_kb_script, app->keyboard_layout);
+        scene_manager_next_scene(app->scene_manager, BadKbSceneWork);
+    } else {
+        furi_string_set(app->file_path, BAD_USB_APP_BASE_FOLDER);
+        scene_manager_next_scene(app->scene_manager, BadKbSceneFileSelect);
+    }
+
+    return app;
+}
+
+void bad_kb_app_free(BadKbApp* app) {
+    furi_assert(app);
+
+    if(app->bad_kb_script) {
+        bad_kb_script_close(app->bad_kb_script);
+        app->bad_kb_script = NULL;
+    }
+
+    // Views
+    view_dispatcher_remove_view(app->view_dispatcher, BadKbAppViewWork);
+    bad_kb_free(app->bad_kb_view);
+
+    // Custom Widget
+    view_dispatcher_remove_view(app->view_dispatcher, BadKbAppViewWidget);
+    widget_free(app->widget);
+
+    // Variable item list
+    view_dispatcher_remove_view(app->view_dispatcher, BadKbAppViewVarItemList);
+    variable_item_list_free(app->var_item_list);
+
+    // Text Input
+    view_dispatcher_remove_view(app->view_dispatcher, BadKbAppViewTextInput);
+    text_input_free(app->text_input);
+
+    // Byte Input
+    view_dispatcher_remove_view(app->view_dispatcher, BadKbAppViewByteInput);
+    byte_input_free(app->byte_input);
+
+    // Loading
+    view_dispatcher_remove_view(app->view_dispatcher, BadKbAppViewLoading);
+    loading_free(app->loading);
+
+    // View dispatcher
+    view_dispatcher_free(app->view_dispatcher);
+    scene_manager_free(app->scene_manager);
+
+    // Restore connection config
+    app->bt->suppress_pin_screen = false;
+    if(app->conn_init_thread) {
+        furi_thread_join(app->conn_init_thread);
+        furi_thread_free(app->conn_init_thread);
+        app->conn_init_thread = NULL;
+    }
+    bad_kb_conn_reset(app);
+    if(app->cur_usb_cfg) free(app->cur_usb_cfg);
+
+    // Close records
+    furi_record_close(RECORD_GUI);
+    furi_record_close(RECORD_NOTIFICATION);
+    furi_record_close(RECORD_DIALOGS);
+    furi_record_close(RECORD_BT);
+
+    bad_kb_save_settings(app);
+
+    furi_string_free(app->file_path);
+    furi_string_free(app->keyboard_layout);
+
+    free(app);
+}
+
+int32_t bad_kb_app(void* p) {
+    BadKbApp* bad_kb_app = bad_kb_app_alloc((char*)p);
+
+    view_dispatcher_run(bad_kb_app->view_dispatcher);
+
+    bad_kb_app_free(bad_kb_app);
+    return 0;
+}

+ 11 - 0
base_pack/bad_kb/bad_kb_app.h

@@ -0,0 +1,11 @@
+#pragma once
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+typedef struct BadKbApp BadKbApp;
+
+#ifdef __cplusplus
+}
+#endif

+ 113 - 0
base_pack/bad_kb/bad_kb_app_i.h

@@ -0,0 +1,113 @@
+#pragma once
+
+#include "bad_kb_app.h"
+#include "scenes/bad_kb_scene.h"
+#include "helpers/ducky_script.h"
+#include "helpers/ble_hid.h"
+#include "bad_kb_paths.h"
+
+#include <gui/gui.h>
+#include <assets_icons.h>
+#include <gui/view_dispatcher.h>
+#include <gui/scene_manager.h>
+#include <gui/modules/submenu.h>
+#include <dialogs/dialogs.h>
+#include <notification/notification_messages.h>
+#include <gui/modules/variable_item_list.h>
+#include <gui/modules/text_input.h>
+#include <gui/modules/byte_input.h>
+#include <gui/modules/loading.h>
+#include <gui/modules/widget.h>
+#include "views/bad_kb_view.h"
+#include <furi_hal_usb.h>
+
+#define BAD_KB_APP_SCRIPT_EXTENSION ".txt"
+#define BAD_KB_APP_LAYOUT_EXTENSION ".kl"
+
+extern uint8_t BAD_KB_BOUND_MAC[GAP_MAC_ADDR_SIZE]; // For remember mode
+
+typedef enum BadKbCustomEvent {
+    BadKbAppCustomEventTextInputDone,
+    BadKbAppCustomEventByteInputDone,
+    BadKbCustomEventErrorBack
+} BadKbCustomEvent;
+
+typedef enum {
+    BadKbAppErrorNoFiles,
+} BadKbAppError;
+
+typedef struct {
+    BleProfileHidParams ble;
+    FuriHalUsbHidConfig usb;
+    bool bt_remember;
+    bool is_ble;
+} BadKbConfig;
+
+typedef enum {
+    BadKbConnModeNone,
+    BadKbConnModeUsb,
+    BadKbConnModeBt,
+} BadKbConnMode;
+
+struct BadKbApp {
+    Gui* gui;
+    ViewDispatcher* view_dispatcher;
+    SceneManager* scene_manager;
+    NotificationApp* notifications;
+    DialogsApp* dialogs;
+    Widget* widget;
+    VariableItemList* var_item_list;
+    TextInput* text_input;
+    ByteInput* byte_input;
+    Loading* loading;
+
+    char bt_name_buf[FURI_HAL_BT_ADV_NAME_LENGTH];
+    uint8_t bt_mac_buf[GAP_MAC_ADDR_SIZE];
+    char usb_name_buf[32];
+    uint16_t usb_vidpid_buf[2];
+
+    BadKbAppError error;
+    FuriString* file_path;
+    FuriString* keyboard_layout;
+    BadKb* bad_kb_view;
+    BadKbScript* bad_kb_script;
+
+    Bt* bt;
+    bool is_bt;
+    bool bt_remember;
+    BadKbConfig config; // User options
+    BadKbConfig id_config; // ID and BT_ID values
+
+    bool set_bt_id;
+    bool set_usb_id;
+    bool has_bt_id;
+    bool has_usb_id;
+
+    FuriHalBleProfileBase* ble_hid;
+    FuriHalUsbInterface* prev_usb_mode;
+
+    BleProfileHidParams cur_ble_cfg;
+    FuriHalUsbHidConfig* cur_usb_cfg;
+
+    BadKbConnMode conn_mode;
+    FuriThread* conn_init_thread;
+};
+
+typedef enum {
+    BadKbAppViewWidget,
+    BadKbAppViewWork,
+    BadKbAppViewVarItemList,
+    BadKbAppViewByteInput,
+    BadKbAppViewTextInput,
+    BadKbAppViewLoading,
+} BadKbAppView;
+
+void bad_kb_app_show_loading_popup(BadKbApp* app, bool show);
+
+int32_t bad_kb_conn_apply(BadKbApp* app);
+
+void bad_kb_conn_reset(BadKbApp* app);
+
+void bad_kb_config_refresh(BadKbApp* app);
+
+void bad_kb_config_adjust(BadKbConfig* cfg);

+ 9 - 0
base_pack/bad_kb/bad_kb_paths.h

@@ -0,0 +1,9 @@
+#pragma once
+
+#include <storage/storage.h>
+
+#define BAD_KB_APP_BASE_FOLDER EXT_PATH("badkb")
+#define BAD_USB_APP_BASE_FOLDER EXT_PATH("badusb")
+#define BAD_KB_KEYS_PATH BAD_KB_APP_BASE_FOLDER "/.bad_kb.keys"
+#define BAD_KB_SETTINGS_PATH BAD_KB_APP_BASE_FOLDER "/.bad_kb.settings"
+#define BAD_KB_APP_PATH_LAYOUT_FOLDER BAD_USB_APP_BASE_FOLDER "/assets/layouts"

+ 416 - 0
base_pack/bad_kb/helpers/ble_hid.c

@@ -0,0 +1,416 @@
+#include "ble_hid.h"
+
+#include <furi_hal_usb_hid.h>
+#include <services/dev_info_service.h>
+#include <services/battery_service.h>
+#include "ble_hid_svc.h"
+
+#include <furi.h>
+#include <usb_hid.h>
+#include <ble/ble.h>
+
+#define HID_INFO_BASE_USB_SPECIFICATION (0x0101)
+#define HID_INFO_COUNTRY_CODE (0x00)
+#define BLE_PROFILE_HID_INFO_FLAG_REMOTE_WAKE_MSK (0x01)
+#define BLE_PROFILE_HID_INFO_FLAG_NORMALLY_CONNECTABLE_MSK (0x02)
+
+#define BLE_PROFILE_HID_KB_MAX_KEYS (6)
+#define BLE_PROFILE_CONSUMER_MAX_KEYS (1)
+
+// Report ids cant be 0
+enum HidReportId {
+    ReportIdKeyboard = 1,
+    ReportIdMouse = 2,
+    ReportIdConsumer = 3,
+};
+// Report numbers corresponded to the report id with an offset of 1
+enum HidInputNumber {
+    ReportNumberKeyboard = 0,
+    ReportNumberMouse = 1,
+    ReportNumberConsumer = 2,
+};
+
+typedef struct {
+    uint8_t mods;
+    uint8_t reserved;
+    uint8_t key[BLE_PROFILE_HID_KB_MAX_KEYS];
+} FURI_PACKED FuriHalBtHidKbReport;
+
+typedef struct {
+    uint8_t btn;
+    int8_t x;
+    int8_t y;
+    int8_t wheel;
+} FURI_PACKED FuriHalBtHidMouseReport;
+
+typedef struct {
+    uint16_t key[BLE_PROFILE_CONSUMER_MAX_KEYS];
+} FURI_PACKED FuriHalBtHidConsumerReport;
+
+// keyboard+mouse+consumer hid report
+static const uint8_t ble_profile_hid_report_map_data[] = {
+    // Keyboard Report
+    HID_USAGE_PAGE(HID_PAGE_DESKTOP),
+    HID_USAGE(HID_DESKTOP_KEYBOARD),
+    HID_COLLECTION(HID_APPLICATION_COLLECTION),
+    HID_REPORT_ID(ReportIdKeyboard),
+    HID_USAGE_PAGE(HID_DESKTOP_KEYPAD),
+    HID_USAGE_MINIMUM(HID_KEYBOARD_L_CTRL),
+    HID_USAGE_MAXIMUM(HID_KEYBOARD_R_GUI),
+    HID_LOGICAL_MINIMUM(0),
+    HID_LOGICAL_MAXIMUM(1),
+    HID_REPORT_SIZE(1),
+    HID_REPORT_COUNT(8),
+    HID_INPUT(HID_IOF_DATA | HID_IOF_VARIABLE | HID_IOF_ABSOLUTE),
+    HID_REPORT_COUNT(1),
+    HID_REPORT_SIZE(8),
+    HID_INPUT(HID_IOF_CONSTANT | HID_IOF_VARIABLE | HID_IOF_ABSOLUTE),
+    HID_USAGE_PAGE(HID_PAGE_LED),
+    HID_REPORT_COUNT(8),
+    HID_REPORT_SIZE(1),
+    HID_USAGE_MINIMUM(1),
+    HID_USAGE_MAXIMUM(8),
+    HID_OUTPUT(HID_IOF_DATA | HID_IOF_VARIABLE | HID_IOF_ABSOLUTE),
+    HID_REPORT_COUNT(BLE_PROFILE_HID_KB_MAX_KEYS),
+    HID_REPORT_SIZE(8),
+    HID_LOGICAL_MINIMUM(0),
+    HID_LOGICAL_MAXIMUM(101),
+    HID_USAGE_PAGE(HID_DESKTOP_KEYPAD),
+    HID_USAGE_MINIMUM(0),
+    HID_USAGE_MAXIMUM(101),
+    HID_INPUT(HID_IOF_DATA | HID_IOF_ARRAY | HID_IOF_ABSOLUTE),
+    HID_END_COLLECTION,
+    // Mouse Report
+    HID_USAGE_PAGE(HID_PAGE_DESKTOP),
+    HID_USAGE(HID_DESKTOP_MOUSE),
+    HID_COLLECTION(HID_APPLICATION_COLLECTION),
+    HID_USAGE(HID_DESKTOP_POINTER),
+    HID_COLLECTION(HID_PHYSICAL_COLLECTION),
+    HID_REPORT_ID(ReportIdMouse),
+    HID_USAGE_PAGE(HID_PAGE_BUTTON),
+    HID_USAGE_MINIMUM(1),
+    HID_USAGE_MAXIMUM(3),
+    HID_LOGICAL_MINIMUM(0),
+    HID_LOGICAL_MAXIMUM(1),
+    HID_REPORT_COUNT(3),
+    HID_REPORT_SIZE(1),
+    HID_INPUT(HID_IOF_DATA | HID_IOF_VARIABLE | HID_IOF_ABSOLUTE),
+    HID_REPORT_SIZE(1),
+    HID_REPORT_COUNT(5),
+    HID_INPUT(HID_IOF_CONSTANT | HID_IOF_VARIABLE | HID_IOF_ABSOLUTE),
+    HID_USAGE_PAGE(HID_PAGE_DESKTOP),
+    HID_USAGE(HID_DESKTOP_X),
+    HID_USAGE(HID_DESKTOP_Y),
+    HID_USAGE(HID_DESKTOP_WHEEL),
+    HID_LOGICAL_MINIMUM(-127),
+    HID_LOGICAL_MAXIMUM(127),
+    HID_REPORT_SIZE(8),
+    HID_REPORT_COUNT(3),
+    HID_INPUT(HID_IOF_DATA | HID_IOF_VARIABLE | HID_IOF_RELATIVE),
+    HID_END_COLLECTION,
+    HID_END_COLLECTION,
+    // Consumer Report
+    HID_USAGE_PAGE(HID_PAGE_CONSUMER),
+    HID_USAGE(HID_CONSUMER_CONTROL),
+    HID_COLLECTION(HID_APPLICATION_COLLECTION),
+    HID_REPORT_ID(ReportIdConsumer),
+    HID_LOGICAL_MINIMUM(0),
+    HID_RI_LOGICAL_MAXIMUM(16, 0x3FF),
+    HID_USAGE_MINIMUM(0),
+    HID_RI_USAGE_MAXIMUM(16, 0x3FF),
+    HID_REPORT_COUNT(BLE_PROFILE_CONSUMER_MAX_KEYS),
+    HID_REPORT_SIZE(16),
+    HID_INPUT(HID_IOF_DATA | HID_IOF_ARRAY | HID_IOF_ABSOLUTE),
+    HID_END_COLLECTION,
+};
+
+typedef struct {
+    FuriHalBleProfileBase base;
+
+    FuriHalBtHidKbReport* kb_report;
+    FuriHalBtHidMouseReport* mouse_report;
+    FuriHalBtHidConsumerReport* consumer_report;
+
+    BleServiceBattery* battery_svc;
+    BleServiceDevInfo* dev_info_svc;
+    BleServiceHid* hid_svc;
+} BleProfileHid;
+_Static_assert(offsetof(BleProfileHid, base) == 0, "Wrong layout");
+
+static FuriHalBleProfileBase* ble_profile_hid_start(FuriHalBleProfileParams profile_params) {
+    UNUSED(profile_params);
+
+    BleProfileHid* profile = malloc(sizeof(BleProfileHid));
+
+    profile->base.config = ble_profile_hid;
+
+    profile->battery_svc = ble_svc_battery_start(true);
+    profile->dev_info_svc = ble_svc_dev_info_start();
+    profile->hid_svc = ble_svc_hid_start();
+
+    // Configure HID Keyboard
+    profile->kb_report = malloc(sizeof(FuriHalBtHidKbReport));
+    profile->mouse_report = malloc(sizeof(FuriHalBtHidMouseReport));
+    profile->consumer_report = malloc(sizeof(FuriHalBtHidConsumerReport));
+
+    // Configure Report Map characteristic
+    ble_svc_hid_update_report_map(
+        profile->hid_svc,
+        ble_profile_hid_report_map_data,
+        sizeof(ble_profile_hid_report_map_data));
+    // Configure HID Information characteristic
+    uint8_t hid_info_val[4] = {
+        HID_INFO_BASE_USB_SPECIFICATION & 0x00ff,
+        (HID_INFO_BASE_USB_SPECIFICATION & 0xff00) >> 8,
+        HID_INFO_COUNTRY_CODE,
+        BLE_PROFILE_HID_INFO_FLAG_REMOTE_WAKE_MSK |
+            BLE_PROFILE_HID_INFO_FLAG_NORMALLY_CONNECTABLE_MSK,
+    };
+    ble_svc_hid_update_info(profile->hid_svc, hid_info_val);
+
+    return &profile->base;
+}
+
+static void ble_profile_hid_stop(FuriHalBleProfileBase* profile) {
+    furi_check(profile);
+    furi_check(profile->config == ble_profile_hid);
+
+    BleProfileHid* hid_profile = (BleProfileHid*)profile;
+    ble_svc_battery_stop(hid_profile->battery_svc);
+    ble_svc_dev_info_stop(hid_profile->dev_info_svc);
+    ble_svc_hid_stop(hid_profile->hid_svc);
+
+    free(hid_profile->kb_report);
+    free(hid_profile->mouse_report);
+    free(hid_profile->consumer_report);
+}
+
+bool ble_profile_hid_kb_press(FuriHalBleProfileBase* profile, uint16_t button) {
+    furi_check(profile);
+    furi_check(profile->config == ble_profile_hid);
+
+    BleProfileHid* hid_profile = (BleProfileHid*)profile;
+    FuriHalBtHidKbReport* kb_report = hid_profile->kb_report;
+    for(uint8_t i = 0; i < BLE_PROFILE_HID_KB_MAX_KEYS; i++) {
+        if(kb_report->key[i] == 0) {
+            kb_report->key[i] = button & 0xFF;
+            break;
+        }
+    }
+    kb_report->mods |= (button >> 8);
+    return ble_svc_hid_update_input_report(
+        hid_profile->hid_svc,
+        ReportNumberKeyboard,
+        (uint8_t*)kb_report,
+        sizeof(FuriHalBtHidKbReport));
+}
+
+bool ble_profile_hid_kb_release(FuriHalBleProfileBase* profile, uint16_t button) {
+    furi_check(profile);
+    furi_check(profile->config == ble_profile_hid);
+
+    BleProfileHid* hid_profile = (BleProfileHid*)profile;
+
+    FuriHalBtHidKbReport* kb_report = hid_profile->kb_report;
+    for(uint8_t i = 0; i < BLE_PROFILE_HID_KB_MAX_KEYS; i++) {
+        if(kb_report->key[i] == (button & 0xFF)) {
+            kb_report->key[i] = 0;
+            break;
+        }
+    }
+    kb_report->mods &= ~(button >> 8);
+    return ble_svc_hid_update_input_report(
+        hid_profile->hid_svc,
+        ReportNumberKeyboard,
+        (uint8_t*)kb_report,
+        sizeof(FuriHalBtHidKbReport));
+}
+
+bool ble_profile_hid_kb_release_all(FuriHalBleProfileBase* profile) {
+    furi_check(profile);
+    furi_check(profile->config == ble_profile_hid);
+
+    BleProfileHid* hid_profile = (BleProfileHid*)profile;
+    FuriHalBtHidKbReport* kb_report = hid_profile->kb_report;
+    for(uint8_t i = 0; i < BLE_PROFILE_HID_KB_MAX_KEYS; i++) {
+        kb_report->key[i] = 0;
+    }
+    kb_report->mods = 0;
+    return ble_svc_hid_update_input_report(
+        hid_profile->hid_svc,
+        ReportNumberKeyboard,
+        (uint8_t*)kb_report,
+        sizeof(FuriHalBtHidKbReport));
+}
+
+bool ble_profile_hid_consumer_key_press(FuriHalBleProfileBase* profile, uint16_t button) {
+    furi_check(profile);
+    furi_check(profile->config == ble_profile_hid);
+
+    BleProfileHid* hid_profile = (BleProfileHid*)profile;
+    FuriHalBtHidConsumerReport* consumer_report = hid_profile->consumer_report;
+    for(uint8_t i = 0; i < BLE_PROFILE_CONSUMER_MAX_KEYS; i++) { //-V1008
+        if(consumer_report->key[i] == 0) {
+            consumer_report->key[i] = button;
+            break;
+        }
+    }
+    return ble_svc_hid_update_input_report(
+        hid_profile->hid_svc,
+        ReportNumberConsumer,
+        (uint8_t*)consumer_report,
+        sizeof(FuriHalBtHidConsumerReport));
+}
+
+bool ble_profile_hid_consumer_key_release(FuriHalBleProfileBase* profile, uint16_t button) {
+    furi_check(profile);
+    furi_check(profile->config == ble_profile_hid);
+
+    BleProfileHid* hid_profile = (BleProfileHid*)profile;
+    FuriHalBtHidConsumerReport* consumer_report = hid_profile->consumer_report;
+    for(uint8_t i = 0; i < BLE_PROFILE_CONSUMER_MAX_KEYS; i++) { //-V1008
+        if(consumer_report->key[i] == button) {
+            consumer_report->key[i] = 0;
+            break;
+        }
+    }
+    return ble_svc_hid_update_input_report(
+        hid_profile->hid_svc,
+        ReportNumberConsumer,
+        (uint8_t*)consumer_report,
+        sizeof(FuriHalBtHidConsumerReport));
+}
+
+bool ble_profile_hid_consumer_key_release_all(FuriHalBleProfileBase* profile) {
+    furi_check(profile);
+    furi_check(profile->config == ble_profile_hid);
+
+    BleProfileHid* hid_profile = (BleProfileHid*)profile;
+    FuriHalBtHidConsumerReport* consumer_report = hid_profile->consumer_report;
+    for(uint8_t i = 0; i < BLE_PROFILE_CONSUMER_MAX_KEYS; i++) { //-V1008
+        consumer_report->key[i] = 0;
+    }
+    return ble_svc_hid_update_input_report(
+        hid_profile->hid_svc,
+        ReportNumberConsumer,
+        (uint8_t*)consumer_report,
+        sizeof(FuriHalBtHidConsumerReport));
+}
+
+bool ble_profile_hid_mouse_move(FuriHalBleProfileBase* profile, int8_t dx, int8_t dy) {
+    furi_check(profile);
+    furi_check(profile->config == ble_profile_hid);
+
+    BleProfileHid* hid_profile = (BleProfileHid*)profile;
+    FuriHalBtHidMouseReport* mouse_report = hid_profile->mouse_report;
+    mouse_report->x = dx;
+    mouse_report->y = dy;
+    bool state = ble_svc_hid_update_input_report(
+        hid_profile->hid_svc,
+        ReportNumberMouse,
+        (uint8_t*)mouse_report,
+        sizeof(FuriHalBtHidMouseReport));
+    mouse_report->x = 0;
+    mouse_report->y = 0;
+    return state;
+}
+
+bool ble_profile_hid_mouse_press(FuriHalBleProfileBase* profile, uint8_t button) {
+    furi_check(profile);
+    furi_check(profile->config == ble_profile_hid);
+
+    BleProfileHid* hid_profile = (BleProfileHid*)profile;
+    FuriHalBtHidMouseReport* mouse_report = hid_profile->mouse_report;
+    mouse_report->btn |= button;
+    return ble_svc_hid_update_input_report(
+        hid_profile->hid_svc,
+        ReportNumberMouse,
+        (uint8_t*)mouse_report,
+        sizeof(FuriHalBtHidMouseReport));
+}
+
+bool ble_profile_hid_mouse_release(FuriHalBleProfileBase* profile, uint8_t button) {
+    furi_check(profile);
+    furi_check(profile->config == ble_profile_hid);
+
+    BleProfileHid* hid_profile = (BleProfileHid*)profile;
+    FuriHalBtHidMouseReport* mouse_report = hid_profile->mouse_report;
+    mouse_report->btn &= ~button;
+    return ble_svc_hid_update_input_report(
+        hid_profile->hid_svc,
+        ReportNumberMouse,
+        (uint8_t*)mouse_report,
+        sizeof(FuriHalBtHidMouseReport));
+}
+
+bool ble_profile_hid_mouse_release_all(FuriHalBleProfileBase* profile) {
+    furi_check(profile);
+    furi_check(profile->config == ble_profile_hid);
+
+    BleProfileHid* hid_profile = (BleProfileHid*)profile;
+    FuriHalBtHidMouseReport* mouse_report = hid_profile->mouse_report;
+    mouse_report->btn = 0;
+    return ble_svc_hid_update_input_report(
+        hid_profile->hid_svc,
+        ReportNumberMouse,
+        (uint8_t*)mouse_report,
+        sizeof(FuriHalBtHidMouseReport));
+}
+
+bool ble_profile_hid_mouse_scroll(FuriHalBleProfileBase* profile, int8_t delta) {
+    furi_check(profile);
+    furi_check(profile->config == ble_profile_hid);
+
+    BleProfileHid* hid_profile = (BleProfileHid*)profile;
+    FuriHalBtHidMouseReport* mouse_report = hid_profile->mouse_report;
+    mouse_report->wheel = delta;
+    bool state = ble_svc_hid_update_input_report(
+        hid_profile->hid_svc,
+        ReportNumberMouse,
+        (uint8_t*)mouse_report,
+        sizeof(FuriHalBtHidMouseReport));
+    mouse_report->wheel = 0;
+    return state;
+}
+
+static GapConfig template_config = {
+    .adv_service_uuid = HUMAN_INTERFACE_DEVICE_SERVICE_UUID,
+    .appearance_char = GAP_APPEARANCE_KEYBOARD,
+    .bonding_mode = true,
+    .pairing_method = GapPairingPinCodeVerifyYesNo,
+    .conn_param =
+        {
+            .conn_int_min = 0x18, // 30 ms
+            .conn_int_max = 0x24, // 45 ms
+            .slave_latency = 0,
+            .supervisor_timeout = 0,
+        },
+};
+
+static void ble_profile_hid_get_config(GapConfig* config, FuriHalBleProfileParams profile_params) {
+    BleProfileHidParams* hid_profile_params = profile_params;
+
+    furi_check(config);
+    memcpy(config, &template_config, sizeof(GapConfig));
+
+    // Set mac address
+    memcpy(config->mac_address, hid_profile_params->mac, sizeof(config->mac_address));
+
+    // Set advertise name
+    config->adv_name[0] = furi_hal_version_get_ble_local_device_name_ptr()[0];
+    strlcpy(config->adv_name + 1, hid_profile_params->name, sizeof(config->adv_name) - 1);
+
+    // Set bonding mode
+    config->bonding_mode = hid_profile_params->bonding;
+
+    // Set pairing method
+    config->pairing_method = hid_profile_params->pairing;
+}
+
+static const FuriHalBleProfileTemplate profile_callbacks = {
+    .start = ble_profile_hid_start,
+    .stop = ble_profile_hid_stop,
+    .get_gap_config = ble_profile_hid_get_config,
+};
+
+const FuriHalBleProfileTemplate* ble_profile_hid = &profile_callbacks;

+ 107 - 0
base_pack/bad_kb/helpers/ble_hid.h

@@ -0,0 +1,107 @@
+#pragma once
+
+#include <furi_ble/profile_interface.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/** 
+ * Optional arguments to pass along with profile template as 
+ * FuriHalBleProfileParams for tuning profile behavior 
+ **/
+typedef struct {
+    char name[FURI_HAL_BT_ADV_NAME_LENGTH]; /**< Full device name  */
+    uint8_t mac[GAP_MAC_ADDR_SIZE]; /**< Full device address */
+    bool bonding; /**< Save paired devices */
+    GapPairing pairing; /**< Pairing security method */
+} BleProfileHidParams;
+
+/** Hid Keyboard Profile descriptor */
+extern const FuriHalBleProfileTemplate* ble_profile_hid;
+
+/** Press keyboard button
+ *
+ * @param profile   profile instance
+ * @param button    button code from HID specification
+ *
+ * @return          true on success
+ */
+bool ble_profile_hid_kb_press(FuriHalBleProfileBase* profile, uint16_t button);
+
+/** Release keyboard button
+ *
+ * @param profile   profile instance
+ * @param button    button code from HID specification
+ *
+ * @return          true on success
+ */
+bool ble_profile_hid_kb_release(FuriHalBleProfileBase* profile, uint16_t button);
+
+/** Release all keyboard buttons
+ *
+ * @param profile   profile instance
+ * @return          true on success
+ */
+bool ble_profile_hid_kb_release_all(FuriHalBleProfileBase* profile);
+
+/** Set the following consumer key to pressed state and send HID report
+ *
+ * @param profile   profile instance
+ * @param button    key code
+ */
+bool ble_profile_hid_consumer_key_press(FuriHalBleProfileBase* profile, uint16_t button);
+
+/** Set the following consumer key to released state and send HID report
+ *
+ * @param profile   profile instance
+ * @param button    key code
+ */
+bool ble_profile_hid_consumer_key_release(FuriHalBleProfileBase* profile, uint16_t button);
+
+/** Set consumer key to released state and send HID report
+ *
+ * @param profile   profile instance
+ * @param button    key code
+ */
+bool ble_profile_hid_consumer_key_release_all(FuriHalBleProfileBase* profile);
+
+/** Set mouse movement and send HID report
+ *
+ * @param profile    profile instance
+ * @param      dx    x coordinate delta
+ * @param      dy    y coordinate delta
+ */
+bool ble_profile_hid_mouse_move(FuriHalBleProfileBase* profile, int8_t dx, int8_t dy);
+
+/** Set mouse button to pressed state and send HID report
+ *
+ * @param profile   profile instance
+ * @param   button  key code
+ */
+bool ble_profile_hid_mouse_press(FuriHalBleProfileBase* profile, uint8_t button);
+
+/** Set mouse button to released state and send HID report
+ *
+ * @param profile   profile instance
+ * @param   button  key code
+ */
+bool ble_profile_hid_mouse_release(FuriHalBleProfileBase* profile, uint8_t button);
+
+/** Set mouse button to released state and send HID report
+ *
+ * @param profile   profile instance
+ * @param   button  key code
+ */
+bool ble_profile_hid_mouse_release_all(FuriHalBleProfileBase* profile);
+
+/** Set mouse wheel position and send HID report
+ *
+ * @param profile   profile instance
+ * @param    delta  number of scroll steps
+ */
+bool ble_profile_hid_mouse_scroll(FuriHalBleProfileBase* profile, int8_t delta);
+
+#ifdef __cplusplus
+}
+#endif

+ 320 - 0
base_pack/bad_kb/helpers/ble_hid_svc.c

@@ -0,0 +1,320 @@
+#include "ble_hid_svc.h"
+#include "app_common.h"
+#include <ble/ble.h>
+#include <furi_ble/event_dispatcher.h>
+#include <furi_ble/gatt.h>
+
+#include <furi.h>
+#include <stdint.h>
+
+#define TAG "BleHid"
+
+#define BLE_SVC_HID_REPORT_MAP_MAX_LEN (255)
+#define BLE_SVC_HID_REPORT_MAX_LEN (255)
+#define BLE_SVC_HID_REPORT_REF_LEN (2)
+#define BLE_SVC_HID_INFO_LEN (4)
+#define BLE_SVC_HID_CONTROL_POINT_LEN (1)
+
+#define BLE_SVC_HID_INPUT_REPORT_COUNT (3)
+#define BLE_SVC_HID_OUTPUT_REPORT_COUNT (0)
+#define BLE_SVC_HID_FEATURE_REPORT_COUNT (0)
+#define BLE_SVC_HID_REPORT_COUNT                                        \
+    (BLE_SVC_HID_INPUT_REPORT_COUNT + BLE_SVC_HID_OUTPUT_REPORT_COUNT + \
+     BLE_SVC_HID_FEATURE_REPORT_COUNT)
+
+typedef enum {
+    HidSvcGattCharacteristicProtocolMode = 0,
+    HidSvcGattCharacteristicReportMap,
+    HidSvcGattCharacteristicInfo,
+    HidSvcGattCharacteristicCtrlPoint,
+    HidSvcGattCharacteristicCount,
+} HidSvcGattCharacteristicId;
+
+typedef struct {
+    uint8_t report_idx;
+    uint8_t report_type;
+} HidSvcReportId;
+
+static_assert(sizeof(HidSvcReportId) == sizeof(uint16_t), "HidSvcReportId must be 2 bytes");
+
+static const Service_UUID_t ble_svc_hid_uuid = {
+    .Service_UUID_16 = HUMAN_INTERFACE_DEVICE_SERVICE_UUID,
+};
+
+static bool ble_svc_hid_char_desc_data_callback(
+    const void* context,
+    const uint8_t** data,
+    uint16_t* data_len) {
+    const HidSvcReportId* report_id = context;
+    *data_len = sizeof(HidSvcReportId);
+    if(data) {
+        *data = (const uint8_t*)report_id;
+    }
+    return false;
+}
+
+typedef struct {
+    const void* data_ptr;
+    uint16_t data_len;
+} HidSvcDataWrapper;
+
+static bool ble_svc_hid_report_data_callback(
+    const void* context,
+    const uint8_t** data,
+    uint16_t* data_len) {
+    const HidSvcDataWrapper* report_data = context;
+    if(data) {
+        *data = report_data->data_ptr;
+        *data_len = report_data->data_len;
+    } else {
+        *data_len = BLE_SVC_HID_REPORT_MAP_MAX_LEN;
+    }
+    return false;
+}
+
+static const BleGattCharacteristicParams ble_svc_hid_chars[HidSvcGattCharacteristicCount] = {
+    [HidSvcGattCharacteristicProtocolMode] =
+        {.name = "Protocol Mode",
+         .data_prop_type = FlipperGattCharacteristicDataFixed,
+         .data.fixed.length = 1,
+         .uuid.Char_UUID_16 = PROTOCOL_MODE_CHAR_UUID,
+         .uuid_type = UUID_TYPE_16,
+         .char_properties = CHAR_PROP_READ | CHAR_PROP_WRITE_WITHOUT_RESP,
+         .security_permissions = ATTR_PERMISSION_NONE,
+         .gatt_evt_mask = GATT_NOTIFY_ATTRIBUTE_WRITE,
+         .is_variable = CHAR_VALUE_LEN_CONSTANT},
+    [HidSvcGattCharacteristicReportMap] =
+        {.name = "Report Map",
+         .data_prop_type = FlipperGattCharacteristicDataCallback,
+         .data.callback.fn = ble_svc_hid_report_data_callback,
+         .data.callback.context = NULL,
+         .uuid.Char_UUID_16 = REPORT_MAP_CHAR_UUID,
+         .uuid_type = UUID_TYPE_16,
+         .char_properties = CHAR_PROP_READ,
+         .security_permissions = ATTR_PERMISSION_NONE,
+         .gatt_evt_mask = GATT_DONT_NOTIFY_EVENTS,
+         .is_variable = CHAR_VALUE_LEN_VARIABLE},
+    [HidSvcGattCharacteristicInfo] =
+        {.name = "HID Information",
+         .data_prop_type = FlipperGattCharacteristicDataFixed,
+         .data.fixed.length = BLE_SVC_HID_INFO_LEN,
+         .data.fixed.ptr = NULL,
+         .uuid.Char_UUID_16 = HID_INFORMATION_CHAR_UUID,
+         .uuid_type = UUID_TYPE_16,
+         .char_properties = CHAR_PROP_READ,
+         .security_permissions = ATTR_PERMISSION_NONE,
+         .gatt_evt_mask = GATT_DONT_NOTIFY_EVENTS,
+         .is_variable = CHAR_VALUE_LEN_CONSTANT},
+    [HidSvcGattCharacteristicCtrlPoint] =
+        {.name = "HID Control Point",
+         .data_prop_type = FlipperGattCharacteristicDataFixed,
+         .data.fixed.length = BLE_SVC_HID_CONTROL_POINT_LEN,
+         .uuid.Char_UUID_16 = HID_CONTROL_POINT_CHAR_UUID,
+         .uuid_type = UUID_TYPE_16,
+         .char_properties = CHAR_PROP_WRITE_WITHOUT_RESP,
+         .security_permissions = ATTR_PERMISSION_NONE,
+         .gatt_evt_mask = GATT_NOTIFY_ATTRIBUTE_WRITE,
+         .is_variable = CHAR_VALUE_LEN_CONSTANT},
+};
+
+static const BleGattCharacteristicDescriptorParams ble_svc_hid_char_descr_template = {
+    .uuid_type = UUID_TYPE_16,
+    .uuid.Char_UUID_16 = REPORT_REFERENCE_DESCRIPTOR_UUID,
+    .max_length = BLE_SVC_HID_REPORT_REF_LEN,
+    .data_callback.fn = ble_svc_hid_char_desc_data_callback,
+    .security_permissions = ATTR_PERMISSION_NONE,
+    .access_permissions = ATTR_ACCESS_READ_WRITE,
+    .gatt_evt_mask = GATT_DONT_NOTIFY_EVENTS,
+    .is_variable = CHAR_VALUE_LEN_CONSTANT,
+};
+
+static const BleGattCharacteristicParams ble_svc_hid_report_template = {
+    .name = "Report",
+    .data_prop_type = FlipperGattCharacteristicDataCallback,
+    .data.callback.fn = ble_svc_hid_report_data_callback,
+    .data.callback.context = NULL,
+    .uuid.Char_UUID_16 = REPORT_CHAR_UUID,
+    .uuid_type = UUID_TYPE_16,
+    .char_properties = CHAR_PROP_READ | CHAR_PROP_NOTIFY,
+    .security_permissions = ATTR_PERMISSION_NONE,
+    .gatt_evt_mask = GATT_DONT_NOTIFY_EVENTS,
+    .is_variable = CHAR_VALUE_LEN_VARIABLE,
+};
+
+struct BleServiceHid {
+    uint16_t svc_handle;
+    BleGattCharacteristicInstance chars[HidSvcGattCharacteristicCount];
+    BleGattCharacteristicInstance input_report_chars[BLE_SVC_HID_INPUT_REPORT_COUNT];
+    BleGattCharacteristicInstance output_report_chars[BLE_SVC_HID_OUTPUT_REPORT_COUNT];
+    BleGattCharacteristicInstance feature_report_chars[BLE_SVC_HID_FEATURE_REPORT_COUNT];
+    GapSvcEventHandler* event_handler;
+};
+
+static BleEventAckStatus ble_svc_hid_event_handler(void* event, void* context) {
+    UNUSED(context);
+
+    BleEventAckStatus ret = BleEventNotAck;
+    hci_event_pckt* event_pckt = (hci_event_pckt*)(((hci_uart_pckt*)event)->data);
+    evt_blecore_aci* blecore_evt = (evt_blecore_aci*)event_pckt->data;
+    // aci_gatt_attribute_modified_event_rp0* attribute_modified;
+    if(event_pckt->evt == HCI_VENDOR_SPECIFIC_DEBUG_EVT_CODE) {
+        if(blecore_evt->ecode == ACI_GATT_ATTRIBUTE_MODIFIED_VSEVT_CODE) {
+            // Process modification events
+            ret = BleEventAckFlowEnable;
+        } else if(blecore_evt->ecode == ACI_GATT_SERVER_CONFIRMATION_VSEVT_CODE) {
+            // Process notification confirmation
+            ret = BleEventAckFlowEnable;
+        }
+    }
+    return ret;
+}
+
+BleServiceHid* ble_svc_hid_start() {
+    BleServiceHid* hid_svc = malloc(sizeof(BleServiceHid));
+
+    // Register event handler
+    hid_svc->event_handler =
+        ble_event_dispatcher_register_svc_handler(ble_svc_hid_event_handler, hid_svc);
+    /**
+     *  Add Human Interface Device Service
+     */
+    if(!ble_gatt_service_add(
+           UUID_TYPE_16,
+           &ble_svc_hid_uuid,
+           PRIMARY_SERVICE,
+           2 + /* protocol mode */
+               (4 * BLE_SVC_HID_INPUT_REPORT_COUNT) + (3 * BLE_SVC_HID_OUTPUT_REPORT_COUNT) +
+               (3 * BLE_SVC_HID_FEATURE_REPORT_COUNT) + 1 + 2 + 2 +
+               2, /* Service + Report Map + HID Information + HID Control Point */
+           &hid_svc->svc_handle)) {
+        free(hid_svc);
+        return NULL;
+    }
+
+    // Maintain previously defined characteristic order
+    ble_gatt_characteristic_init(
+        hid_svc->svc_handle,
+        &ble_svc_hid_chars[HidSvcGattCharacteristicProtocolMode],
+        &hid_svc->chars[HidSvcGattCharacteristicProtocolMode]);
+
+    uint8_t protocol_mode = 1;
+    ble_gatt_characteristic_update(
+        hid_svc->svc_handle,
+        &hid_svc->chars[HidSvcGattCharacteristicProtocolMode],
+        &protocol_mode);
+
+    // reports
+    BleGattCharacteristicDescriptorParams ble_svc_hid_char_descr;
+    BleGattCharacteristicParams report_char;
+    HidSvcReportId report_id;
+
+    memcpy(
+        &ble_svc_hid_char_descr, &ble_svc_hid_char_descr_template, sizeof(ble_svc_hid_char_descr));
+    memcpy(&report_char, &ble_svc_hid_report_template, sizeof(report_char));
+
+    ble_svc_hid_char_descr.data_callback.context = &report_id;
+    report_char.descriptor_params = &ble_svc_hid_char_descr;
+
+    typedef struct {
+        uint8_t report_type;
+        uint8_t report_count;
+        BleGattCharacteristicInstance* chars;
+    } HidSvcReportCharProps;
+
+    HidSvcReportCharProps hid_report_chars[] = {
+        {0x01, BLE_SVC_HID_INPUT_REPORT_COUNT, hid_svc->input_report_chars},
+        {0x02, BLE_SVC_HID_OUTPUT_REPORT_COUNT, hid_svc->output_report_chars},
+        {0x03, BLE_SVC_HID_FEATURE_REPORT_COUNT, hid_svc->feature_report_chars},
+    };
+
+    for(size_t report_type_idx = 0; report_type_idx < COUNT_OF(hid_report_chars);
+        report_type_idx++) {
+        report_id.report_type = hid_report_chars[report_type_idx].report_type;
+        for(size_t report_idx = 0; report_idx < hid_report_chars[report_type_idx].report_count;
+            report_idx++) {
+            report_id.report_idx = report_idx + 1;
+            ble_gatt_characteristic_init(
+                hid_svc->svc_handle,
+                &report_char,
+                &hid_report_chars[report_type_idx].chars[report_idx]);
+        }
+    }
+
+    // Setup remaining characteristics
+    for(size_t i = HidSvcGattCharacteristicReportMap; i < HidSvcGattCharacteristicCount; i++) {
+        ble_gatt_characteristic_init(
+            hid_svc->svc_handle, &ble_svc_hid_chars[i], &hid_svc->chars[i]);
+    }
+
+    return hid_svc;
+}
+
+bool ble_svc_hid_update_report_map(BleServiceHid* hid_svc, const uint8_t* data, uint16_t len) {
+    furi_assert(data);
+    furi_assert(hid_svc);
+
+    HidSvcDataWrapper report_data = {
+        .data_ptr = data,
+        .data_len = len,
+    };
+    return ble_gatt_characteristic_update(
+        hid_svc->svc_handle, &hid_svc->chars[HidSvcGattCharacteristicReportMap], &report_data);
+}
+
+bool ble_svc_hid_update_input_report(
+    BleServiceHid* hid_svc,
+    uint8_t input_report_num,
+    uint8_t* data,
+    uint16_t len) {
+    furi_assert(data);
+    furi_assert(hid_svc);
+    furi_assert(input_report_num < BLE_SVC_HID_INPUT_REPORT_COUNT);
+
+    HidSvcDataWrapper report_data = {
+        .data_ptr = data,
+        .data_len = len,
+    };
+    return ble_gatt_characteristic_update(
+        hid_svc->svc_handle, &hid_svc->input_report_chars[input_report_num], &report_data);
+}
+
+bool ble_svc_hid_update_info(BleServiceHid* hid_svc, uint8_t* data) {
+    furi_assert(data);
+    furi_assert(hid_svc);
+
+    return ble_gatt_characteristic_update(
+        hid_svc->svc_handle, &hid_svc->chars[HidSvcGattCharacteristicInfo], &data);
+}
+
+void ble_svc_hid_stop(BleServiceHid* hid_svc) {
+    furi_assert(hid_svc);
+    ble_event_dispatcher_unregister_svc_handler(hid_svc->event_handler);
+    // Delete characteristics
+    for(size_t i = 0; i < HidSvcGattCharacteristicCount; i++) {
+        ble_gatt_characteristic_delete(hid_svc->svc_handle, &hid_svc->chars[i]);
+    }
+
+    typedef struct {
+        uint8_t report_count;
+        BleGattCharacteristicInstance* chars;
+    } HidSvcReportCharProps;
+
+    HidSvcReportCharProps hid_report_chars[] = {
+        {BLE_SVC_HID_INPUT_REPORT_COUNT, hid_svc->input_report_chars},
+        {BLE_SVC_HID_OUTPUT_REPORT_COUNT, hid_svc->output_report_chars},
+        {BLE_SVC_HID_FEATURE_REPORT_COUNT, hid_svc->feature_report_chars},
+    };
+
+    for(size_t report_type_idx = 0; report_type_idx < COUNT_OF(hid_report_chars);
+        report_type_idx++) {
+        for(size_t report_idx = 0; report_idx < hid_report_chars[report_type_idx].report_count;
+            report_idx++) {
+            ble_gatt_characteristic_delete(
+                hid_svc->svc_handle, &hid_report_chars[report_type_idx].chars[report_idx]);
+        }
+    }
+
+    // Delete service
+    ble_gatt_service_delete(hid_svc->svc_handle);
+    free(hid_svc);
+}

+ 29 - 0
base_pack/bad_kb/helpers/ble_hid_svc.h

@@ -0,0 +1,29 @@
+#pragma once
+
+#include <stdint.h>
+#include <stdbool.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+typedef struct BleServiceHid BleServiceHid;
+
+BleServiceHid* ble_svc_hid_start();
+
+void ble_svc_hid_stop(BleServiceHid* service);
+
+bool ble_svc_hid_update_report_map(BleServiceHid* service, const uint8_t* data, uint16_t len);
+
+bool ble_svc_hid_update_input_report(
+    BleServiceHid* service,
+    uint8_t input_report_num,
+    uint8_t* data,
+    uint16_t len);
+
+// Expects data to be of length BLE_SVC_HID_INFO_LEN (4 bytes)
+bool ble_svc_hid_update_info(BleServiceHid* service, uint8_t* data);
+
+#ifdef __cplusplus
+}
+#endif

+ 935 - 0
base_pack/bad_kb/helpers/ducky_script.c

@@ -0,0 +1,935 @@
+#include "../bad_kb_app_i.h"
+#include <furi.h>
+#include <furi_hal.h>
+#include <gui/gui.h>
+#include <input/input.h>
+#include <lib/toolbox/args.h>
+#include <furi_hal_usb_hid.h>
+#include "ble_hid.h"
+#include <storage/storage.h>
+#include "ducky_script.h"
+#include "ducky_script_i.h"
+#include <dolphin/dolphin.h>
+#include <toolbox/hex.h>
+
+#define TAG "BadKb"
+#define WORKER_TAG TAG "Worker"
+
+#define BADKB_ASCII_TO_KEY(script, x) \
+    (((uint8_t)x < 128) ? (script->layout[(uint8_t)x]) : HID_KEYBOARD_NONE)
+
+// Delays for waiting between HID key press and key release
+const uint8_t bt_hid_delays[LevelRssiNum] = {
+    60, // LevelRssi122_100
+    55, // LevelRssi99_80
+    50, // LevelRssi79_60
+    47, // LevelRssi59_40
+    34, // LevelRssi39_0
+};
+
+uint8_t bt_timeout = 0;
+
+static LevelRssiRange bt_remote_rssi_range(Bt* bt) {
+    uint8_t rssi;
+
+    if(!bt_remote_rssi(bt, &rssi)) return LevelRssiError;
+
+    if(rssi <= 39)
+        return LevelRssi39_0;
+    else if(rssi <= 59)
+        return LevelRssi59_40;
+    else if(rssi <= 79)
+        return LevelRssi79_60;
+    else if(rssi <= 99)
+        return LevelRssi99_80;
+    else if(rssi <= 122)
+        return LevelRssi122_100;
+
+    return LevelRssiError;
+}
+
+static inline void update_bt_timeout(Bt* bt) {
+    LevelRssiRange r = bt_remote_rssi_range(bt);
+    if(r < LevelRssiNum) {
+        bt_timeout = bt_hid_delays[r];
+        FURI_LOG_D(WORKER_TAG, "BLE Key timeout : %u", bt_timeout);
+    }
+}
+
+static const char ducky_cmd_id[] = {"ID"};
+static const char ducky_cmd_bt_id[] = {"BT_ID"};
+
+static const uint8_t numpad_keys[10] = {
+    HID_KEYPAD_0,
+    HID_KEYPAD_1,
+    HID_KEYPAD_2,
+    HID_KEYPAD_3,
+    HID_KEYPAD_4,
+    HID_KEYPAD_5,
+    HID_KEYPAD_6,
+    HID_KEYPAD_7,
+    HID_KEYPAD_8,
+    HID_KEYPAD_9,
+};
+
+uint32_t ducky_get_command_len(const char* line) {
+    uint32_t len = strlen(line);
+    for(uint32_t i = 0; i < len; i++) {
+        if(line[i] == ' ') return i;
+    }
+    return 0;
+}
+
+bool ducky_is_line_end(const char chr) {
+    return ((chr == ' ') || (chr == '\0') || (chr == '\r') || (chr == '\n'));
+}
+
+uint16_t ducky_get_keycode(BadKbScript* bad_kb, const char* param, bool accept_chars) {
+    uint16_t keycode = ducky_get_keycode_by_name(param);
+    if(keycode != HID_KEYBOARD_NONE) {
+        return keycode;
+    }
+
+    if((accept_chars) && (strlen(param) > 0)) {
+        return (BADKB_ASCII_TO_KEY(bad_kb, param[0]) & 0xFF);
+    }
+    return 0;
+}
+
+bool ducky_get_number(const char* param, uint32_t* val) {
+    uint32_t value = 0;
+    if(sscanf(param, "%lu", &value) == 1) {
+        *val = value;
+        return true;
+    }
+    return false;
+}
+
+uint8_t furi_hal_bt_hid_get_led_state() {
+    // FIXME
+    return 0;
+}
+
+void ducky_numlock_on(BadKbScript* bad_kb) {
+    if(bad_kb->bt) {
+        if((furi_hal_bt_hid_get_led_state() & HID_KB_LED_NUM) == 0) {
+            ble_profile_hid_kb_press(bad_kb->app->ble_hid, HID_KEYBOARD_LOCK_NUM_LOCK);
+            furi_delay_ms(bt_timeout);
+            ble_profile_hid_kb_release(bad_kb->app->ble_hid, HID_KEYBOARD_LOCK_NUM_LOCK);
+        }
+    } else {
+        if((furi_hal_hid_get_led_state() & HID_KB_LED_NUM) == 0) {
+            furi_hal_hid_kb_press(HID_KEYBOARD_LOCK_NUM_LOCK);
+            furi_hal_hid_kb_release(HID_KEYBOARD_LOCK_NUM_LOCK);
+        }
+    }
+}
+
+bool ducky_numpad_press(BadKbScript* bad_kb, const char num) {
+    if((num < '0') || (num > '9')) return false;
+
+    uint16_t key = numpad_keys[num - '0'];
+    if(bad_kb->bt) {
+        ble_profile_hid_kb_press(bad_kb->app->ble_hid, key);
+        furi_delay_ms(bt_timeout);
+        ble_profile_hid_kb_release(bad_kb->app->ble_hid, key);
+    } else {
+        furi_hal_hid_kb_press(key);
+        furi_hal_hid_kb_release(key);
+    }
+
+    return true;
+}
+
+bool ducky_altchar(BadKbScript* bad_kb, const char* charcode) {
+    uint8_t i = 0;
+    bool state = false;
+
+    if(bad_kb->bt) {
+        ble_profile_hid_kb_press(bad_kb->app->ble_hid, KEY_MOD_LEFT_ALT);
+    } else {
+        furi_hal_hid_kb_press(KEY_MOD_LEFT_ALT);
+    }
+
+    while(!ducky_is_line_end(charcode[i])) {
+        state = ducky_numpad_press(bad_kb, charcode[i]);
+        if(state == false) break;
+        i++;
+    }
+
+    if(bad_kb->bt) {
+        ble_profile_hid_kb_release(bad_kb->app->ble_hid, KEY_MOD_LEFT_ALT);
+    } else {
+        furi_hal_hid_kb_release(KEY_MOD_LEFT_ALT);
+    }
+    return state;
+}
+
+bool ducky_altstring(BadKbScript* bad_kb, const char* param) {
+    uint32_t i = 0;
+    bool state = false;
+
+    while(param[i] != '\0') {
+        if((param[i] < ' ') || (param[i] > '~')) {
+            i++;
+            continue; // Skip non-printable chars
+        }
+
+        char temp_str[4];
+        snprintf(temp_str, 4, "%u", param[i]);
+
+        state = ducky_altchar(bad_kb, temp_str);
+        if(state == false) break;
+        i++;
+    }
+    return state;
+}
+
+int32_t ducky_error(BadKbScript* bad_kb, const char* text, ...) {
+    va_list args;
+    va_start(args, text);
+
+    vsnprintf(bad_kb->st.error, sizeof(bad_kb->st.error), text, args);
+
+    va_end(args);
+    return SCRIPT_STATE_ERROR;
+}
+
+bool ducky_string(BadKbScript* bad_kb, const char* param) {
+    uint32_t i = 0;
+
+    while(param[i] != '\0') {
+        if(param[i] != '\n') {
+            uint16_t keycode = BADKB_ASCII_TO_KEY(bad_kb, param[i]);
+            if(keycode != HID_KEYBOARD_NONE) {
+                if(bad_kb->bt) {
+                    ble_profile_hid_kb_press(bad_kb->app->ble_hid, keycode);
+                    furi_delay_ms(bt_timeout);
+                    ble_profile_hid_kb_release(bad_kb->app->ble_hid, keycode);
+                } else {
+                    furi_hal_hid_kb_press(keycode);
+                    furi_hal_hid_kb_release(keycode);
+                }
+            }
+        } else {
+            if(bad_kb->bt) {
+                ble_profile_hid_kb_press(bad_kb->app->ble_hid, HID_KEYBOARD_RETURN);
+                furi_delay_ms(bt_timeout);
+                ble_profile_hid_kb_release(bad_kb->app->ble_hid, HID_KEYBOARD_RETURN);
+            } else {
+                furi_hal_hid_kb_press(HID_KEYBOARD_RETURN);
+                furi_hal_hid_kb_release(HID_KEYBOARD_RETURN);
+            }
+        }
+        i++;
+    }
+    bad_kb->stringdelay = 0;
+    return true;
+}
+
+static bool ducky_string_next(BadKbScript* bad_kb) {
+    if(bad_kb->string_print_pos >= furi_string_size(bad_kb->string_print)) {
+        return true;
+    }
+
+    char print_char = furi_string_get_char(bad_kb->string_print, bad_kb->string_print_pos);
+
+    if(print_char != '\n') {
+        uint16_t keycode = BADKB_ASCII_TO_KEY(bad_kb, print_char);
+        if(keycode != HID_KEYBOARD_NONE) {
+            if(bad_kb->bt) {
+                ble_profile_hid_kb_press(bad_kb->app->ble_hid, keycode);
+                furi_delay_ms(bt_timeout);
+                ble_profile_hid_kb_release(bad_kb->app->ble_hid, keycode);
+            } else {
+                furi_hal_hid_kb_press(keycode);
+                furi_hal_hid_kb_release(keycode);
+            }
+        }
+    } else {
+        if(bad_kb->bt) {
+            ble_profile_hid_kb_press(bad_kb->app->ble_hid, HID_KEYBOARD_RETURN);
+            furi_delay_ms(bt_timeout);
+            ble_profile_hid_kb_release(bad_kb->app->ble_hid, HID_KEYBOARD_RETURN);
+        } else {
+            furi_hal_hid_kb_press(HID_KEYBOARD_RETURN);
+            furi_hal_hid_kb_release(HID_KEYBOARD_RETURN);
+        }
+    }
+
+    bad_kb->string_print_pos++;
+
+    return false;
+}
+
+static int32_t ducky_parse_line(BadKbScript* bad_kb, FuriString* line) {
+    uint32_t line_len = furi_string_size(line);
+    const char* line_tmp = furi_string_get_cstr(line);
+
+    if(line_len == 0) {
+        return SCRIPT_STATE_NEXT_LINE; // Skip empty lines
+    }
+    FURI_LOG_D(WORKER_TAG, "line:%s", line_tmp);
+
+    // Ducky Lang Functions
+    int32_t cmd_result = ducky_execute_cmd(bad_kb, line_tmp);
+    if(cmd_result != SCRIPT_STATE_CMD_UNKNOWN) {
+        return cmd_result;
+    }
+
+    // Special keys + modifiers
+    uint16_t key = ducky_get_keycode(bad_kb, line_tmp, false);
+    if(key == HID_KEYBOARD_NONE) {
+        return ducky_error(bad_kb, "No keycode defined for %s", line_tmp);
+    }
+    if((key & 0xFF00) != 0) {
+        // It's a modifier key
+        uint32_t offset = ducky_get_command_len(line_tmp) + 1;
+        // ducky_get_command_len() returns 0 without space, so check for != 1
+        if(offset != 1 && line_len > offset) {
+            // It's also a key combination
+            key |= ducky_get_keycode(bad_kb, line_tmp + offset, true);
+        }
+    }
+    if(bad_kb->bt) {
+        ble_profile_hid_kb_press(bad_kb->app->ble_hid, key);
+        furi_delay_ms(bt_timeout);
+        ble_profile_hid_kb_release(bad_kb->app->ble_hid, key);
+    } else {
+        furi_hal_hid_kb_press(key);
+        furi_hal_hid_kb_release(key);
+    }
+    return 0;
+}
+
+static bool ducky_set_usb_id(BadKbScript* bad_kb, const char* line) {
+    FuriHalUsbHidConfig* cfg = &bad_kb->app->id_config.usb;
+
+    if(sscanf(line, "%lX:%lX", &cfg->vid, &cfg->pid) == 2) {
+        cfg->manuf[0] = '\0';
+        cfg->product[0] = '\0';
+
+        uint8_t id_len = ducky_get_command_len(line);
+        if(!ducky_is_line_end(line[id_len + 1])) {
+            sscanf(&line[id_len + 1], "%31[^\r\n:]:%31[^\r\n]", cfg->manuf, cfg->product);
+        }
+        FURI_LOG_D(
+            WORKER_TAG,
+            "set usb id: %04lX:%04lX mfr:%s product:%s",
+            cfg->vid,
+            cfg->pid,
+            cfg->manuf,
+            cfg->product);
+        return true;
+    }
+    return false;
+}
+
+static bool ducky_set_bt_id(BadKbScript* bad_kb, const char* line) {
+    BadKbConfig* cfg = &bad_kb->app->id_config;
+
+    size_t line_len = strlen(line);
+    size_t mac_len = sizeof(cfg->ble.mac) * 3; // 2 text chars + separator per byte
+    if(line_len < mac_len + 1) return false; // MAC + at least 1 char for name
+
+    for(size_t i = 0; i < sizeof(cfg->ble.mac); i++) {
+        char a = line[i * 3];
+        char b = line[i * 3 + 1];
+        if((a < 'A' && a > 'F') || (a < '0' && a > '9') || (b < 'A' && b > 'F') ||
+           (b < '0' && b > '9') || !hex_char_to_uint8(a, b, &cfg->ble.mac[i])) {
+            return false;
+        }
+    }
+    furi_hal_bt_reverse_mac_addr(cfg->ble.mac);
+
+    strlcpy(cfg->ble.name, line + mac_len, sizeof(cfg->ble.name));
+    FURI_LOG_D(WORKER_TAG, "set bt id: %s", line);
+    return true;
+}
+
+static void ducky_script_preload(BadKbScript* bad_kb, File* script_file) {
+    BadKbApp* app = bad_kb->app;
+    uint8_t ret = 0;
+    uint32_t line_len = 0;
+
+    furi_string_reset(bad_kb->line);
+
+    do {
+        ret = storage_file_read(script_file, bad_kb->file_buf, FILE_BUFFER_LEN);
+        for(uint16_t i = 0; i < ret; i++) {
+            if(bad_kb->file_buf[i] == '\n' && line_len > 0) {
+                bad_kb->st.line_nb++;
+                line_len = 0;
+            } else {
+                if(bad_kb->st.line_nb == 0) { // Save first line
+                    furi_string_push_back(bad_kb->line, bad_kb->file_buf[i]);
+                }
+                line_len++;
+            }
+        }
+        if(storage_file_eof(script_file)) {
+            if(line_len > 0) {
+                bad_kb->st.line_nb++;
+                break;
+            }
+        }
+    } while(ret > 0);
+
+    // Looking for ID or BT_ID command at first line
+    const char* line_tmp = furi_string_get_cstr(bad_kb->line);
+    app->set_usb_id = false;
+    app->set_bt_id = false;
+    app->has_usb_id = strncmp(line_tmp, ducky_cmd_id, strlen(ducky_cmd_id)) == 0;
+    app->has_bt_id = strncmp(line_tmp, ducky_cmd_bt_id, strlen(ducky_cmd_bt_id)) == 0;
+
+    if(app->has_usb_id) {
+        //app->is_bt = false;
+        app->set_usb_id = ducky_set_usb_id(bad_kb, &line_tmp[strlen(ducky_cmd_id) + 1]);
+    } else if(app->has_bt_id) {
+        //app->is_bt = true;
+        app->set_bt_id = ducky_set_bt_id(bad_kb, &line_tmp[strlen(ducky_cmd_bt_id) + 1]);
+    }
+
+    storage_file_seek(script_file, 0, true);
+    furi_string_reset(bad_kb->line);
+}
+
+static int32_t ducky_script_execute_next(BadKbScript* bad_kb, File* script_file) {
+    int32_t delay_val = 0;
+
+    if(bad_kb->repeat_cnt > 0) {
+        bad_kb->repeat_cnt--;
+        delay_val = ducky_parse_line(bad_kb, bad_kb->line_prev);
+        if(delay_val == SCRIPT_STATE_NEXT_LINE) { // Empty line
+            return 0;
+        } else if(delay_val == SCRIPT_STATE_STRING_START) { // Print string with delays
+            return delay_val;
+        } else if(delay_val == SCRIPT_STATE_WAIT_FOR_BTN) { // wait for button
+            return delay_val;
+        } else if(delay_val < 0) { // Script error
+            bad_kb->st.error_line = bad_kb->st.line_cur - 1;
+            FURI_LOG_E(WORKER_TAG, "Unknown command at line %zu", bad_kb->st.line_cur - 1U);
+            return SCRIPT_STATE_ERROR;
+        } else {
+            return (delay_val + bad_kb->defdelay);
+        }
+    }
+
+    furi_string_set(bad_kb->line_prev, bad_kb->line);
+    furi_string_reset(bad_kb->line);
+
+    while(1) {
+        if(bad_kb->buf_len == 0) {
+            bad_kb->buf_len = storage_file_read(script_file, bad_kb->file_buf, FILE_BUFFER_LEN);
+            if(storage_file_eof(script_file)) {
+                if((bad_kb->buf_len < FILE_BUFFER_LEN) && (bad_kb->file_end == false)) {
+                    bad_kb->file_buf[bad_kb->buf_len] = '\n';
+                    bad_kb->buf_len++;
+                    bad_kb->file_end = true;
+                }
+            }
+
+            bad_kb->buf_start = 0;
+            if(bad_kb->buf_len == 0) return SCRIPT_STATE_END;
+        }
+        for(uint8_t i = bad_kb->buf_start; i < (bad_kb->buf_start + bad_kb->buf_len); i++) {
+            if(bad_kb->file_buf[i] == '\n' && furi_string_size(bad_kb->line) > 0) {
+                bad_kb->st.line_cur++;
+                bad_kb->buf_len = bad_kb->buf_len + bad_kb->buf_start - (i + 1);
+                bad_kb->buf_start = i + 1;
+                furi_string_trim(bad_kb->line);
+                delay_val = ducky_parse_line(bad_kb, bad_kb->line);
+                if(delay_val == SCRIPT_STATE_NEXT_LINE) { // Empty line
+                    return 0;
+                } else if(delay_val == SCRIPT_STATE_STRING_START) { // Print string with delays
+                    return delay_val;
+                } else if(delay_val == SCRIPT_STATE_WAIT_FOR_BTN) { // wait for button
+                    return delay_val;
+                } else if(delay_val < 0) {
+                    bad_kb->st.error_line = bad_kb->st.line_cur;
+                    FURI_LOG_E(WORKER_TAG, "Unknown command at line %zu", bad_kb->st.line_cur);
+                    return SCRIPT_STATE_ERROR;
+                } else {
+                    return (delay_val + bad_kb->defdelay);
+                }
+            } else {
+                furi_string_push_back(bad_kb->line, bad_kb->file_buf[i]);
+            }
+        }
+        bad_kb->buf_len = 0;
+        if(bad_kb->file_end) return SCRIPT_STATE_END;
+    }
+
+    return 0;
+}
+
+void bad_kb_bt_hid_state_callback(BtStatus status, void* context) {
+    furi_assert(context);
+    BadKbScript* bad_kb = context;
+    bool state = (status == BtStatusConnected);
+
+    if(state == true) {
+        LevelRssiRange r = bt_remote_rssi_range(bad_kb->bt);
+        if(r != LevelRssiError) {
+            bt_timeout = bt_hid_delays[r];
+        }
+        furi_thread_flags_set(furi_thread_get_id(bad_kb->thread), WorkerEvtConnect);
+    } else {
+        furi_thread_flags_set(furi_thread_get_id(bad_kb->thread), WorkerEvtDisconnect);
+    }
+}
+
+void bad_kb_usb_hid_state_callback(bool state, void* context) {
+    furi_assert(context);
+    BadKbScript* bad_kb = context;
+
+    if(state == true) {
+        furi_thread_flags_set(furi_thread_get_id(bad_kb->thread), WorkerEvtConnect);
+    } else {
+        furi_thread_flags_set(furi_thread_get_id(bad_kb->thread), WorkerEvtDisconnect);
+    }
+}
+
+static uint32_t bad_kb_flags_get(uint32_t flags_mask, uint32_t timeout) {
+    uint32_t flags = furi_thread_flags_get();
+    furi_check((flags & FuriFlagError) == 0);
+    if(flags == 0) {
+        flags = furi_thread_flags_wait(flags_mask, FuriFlagWaitAny, timeout);
+        furi_check(((flags & FuriFlagError) == 0) || (flags == (unsigned)FuriFlagErrorTimeout));
+    } else {
+        uint32_t state = furi_thread_flags_clear(flags);
+        furi_check((state & FuriFlagError) == 0);
+    }
+    return flags;
+}
+
+static int32_t bad_kb_worker(void* context) {
+    BadKbScript* bad_kb = context;
+
+    BadKbWorkerState worker_state = BadKbStateInit;
+    BadKbWorkerState pause_state = BadKbStateRunning;
+    int32_t delay_val = 0;
+
+    FURI_LOG_I(WORKER_TAG, "Init");
+    File* script_file = storage_file_alloc(furi_record_open(RECORD_STORAGE));
+    bad_kb->line = furi_string_alloc();
+    bad_kb->line_prev = furi_string_alloc();
+    bad_kb->string_print = furi_string_alloc();
+    bad_kb->st.elapsed = 0;
+
+    while(1) {
+        uint32_t start = furi_get_tick();
+        if(worker_state == BadKbStateInit) { // State: initialization
+            start = 0;
+            FURI_LOG_D(WORKER_TAG, "init start");
+            if(storage_file_open(
+                   script_file,
+                   furi_string_get_cstr(bad_kb->file_path),
+                   FSAM_READ,
+                   FSOM_OPEN_EXISTING)) {
+                ducky_script_preload(bad_kb, script_file);
+                if(bad_kb->st.line_nb > 0) {
+                    bad_kb_config_refresh(bad_kb->app);
+                    worker_state = BadKbStateNotConnected; // Refresh will set connected flag
+                } else {
+                    worker_state = BadKbStateScriptError; // Script preload error
+                }
+            } else {
+                FURI_LOG_E(WORKER_TAG, "File open error");
+                worker_state = BadKbStateFileError; // File open error
+            }
+            bad_kb->st.state = worker_state;
+            FURI_LOG_D(WORKER_TAG, "init done");
+
+        } else if(worker_state == BadKbStateNotConnected) { // State: Not connected
+            start = 0;
+            FURI_LOG_D(WORKER_TAG, "not connected wait");
+            uint32_t flags = bad_kb_flags_get(
+                WorkerEvtEnd | WorkerEvtConnect | WorkerEvtDisconnect | WorkerEvtStartStop,
+                FuriWaitForever);
+            FURI_LOG_D(WORKER_TAG, "not connected flags: %lu", flags);
+
+            if(flags & WorkerEvtEnd) {
+                break;
+            } else if(flags & WorkerEvtConnect) {
+                worker_state = BadKbStateIdle; // Ready to run
+            } else if(flags & WorkerEvtStartStop) {
+                worker_state = BadKbStateWillRun; // Will run when connected
+            }
+            bad_kb->st.state = worker_state;
+
+        } else if(worker_state == BadKbStateIdle) { // State: ready to start
+            start = 0;
+            FURI_LOG_D(WORKER_TAG, "idle wait");
+            uint32_t flags = bad_kb_flags_get(
+                WorkerEvtEnd | WorkerEvtStartStop | WorkerEvtConnect | WorkerEvtDisconnect,
+                FuriWaitForever);
+            FURI_LOG_D(WORKER_TAG, "idle flags: %lu", flags);
+
+            if(flags & WorkerEvtEnd) {
+                break;
+            } else if(flags & WorkerEvtStartStop) { // Start executing script
+                //dolphin_deed(DolphinDeedBadKbPlayScript);
+                delay_val = 0;
+                bad_kb->buf_len = 0;
+                bad_kb->st.line_cur = 0;
+                bad_kb->defdelay = 0;
+                bad_kb->stringdelay = 0;
+                bad_kb->repeat_cnt = 0;
+                bad_kb->key_hold_nb = 0;
+                bad_kb->file_end = false;
+                storage_file_seek(script_file, 0, true);
+                bad_kb_script_set_keyboard_layout(bad_kb, bad_kb->keyboard_layout);
+                worker_state = BadKbStateRunning;
+                bad_kb->st.elapsed = 0;
+            } else if(flags & WorkerEvtDisconnect) {
+                worker_state = BadKbStateNotConnected; // Disconnected
+            }
+            bad_kb->st.state = worker_state;
+
+        } else if(worker_state == BadKbStateWillRun) { // State: start on connection
+            start = 0;
+            FURI_LOG_D(WORKER_TAG, "will run wait");
+            uint32_t flags = bad_kb_flags_get(
+                WorkerEvtEnd | WorkerEvtConnect | WorkerEvtDisconnect | WorkerEvtStartStop,
+                FuriWaitForever);
+            FURI_LOG_D(WORKER_TAG, "will run flags: %lu", flags);
+
+            if(flags & WorkerEvtEnd) {
+                break;
+            } else if(flags & WorkerEvtConnect) { // Start executing script
+                //dolphin_deed(DolphinDeedBadKbPlayScript);
+                delay_val = 0;
+                bad_kb->buf_len = 0;
+                bad_kb->st.line_cur = 0;
+                bad_kb->defdelay = 0;
+                bad_kb->stringdelay = 0;
+                bad_kb->repeat_cnt = 0;
+                bad_kb->file_end = false;
+                storage_file_seek(script_file, 0, true);
+                // extra time for PC to recognize Flipper as keyboard
+                flags = furi_thread_flags_wait(
+                    WorkerEvtEnd | WorkerEvtDisconnect | WorkerEvtStartStop,
+                    FuriFlagWaitAny | FuriFlagNoClear,
+                    bad_kb->bt ? 3000 : 1500);
+                if(flags == (unsigned)FuriFlagErrorTimeout) {
+                    // If nothing happened - start script execution
+                    worker_state = BadKbStateRunning;
+                    bad_kb->st.elapsed = 0;
+                } else if(flags & WorkerEvtStartStop) {
+                    worker_state = BadKbStateIdle;
+                    furi_thread_flags_clear(WorkerEvtStartStop);
+                }
+                if(bad_kb->bt) {
+                    update_bt_timeout(bad_kb->bt);
+                }
+                bad_kb_script_set_keyboard_layout(bad_kb, bad_kb->keyboard_layout);
+            } else if(flags & WorkerEvtStartStop) { // Cancel scheduled execution
+                worker_state = BadKbStateNotConnected;
+            }
+            bad_kb->st.state = worker_state;
+
+        } else if(worker_state == BadKbStateRunning) { // State: running
+            FURI_LOG_D(WORKER_TAG, "running");
+            uint16_t delay_cur = (delay_val > 100) ? (100) : (delay_val);
+            uint32_t flags = furi_thread_flags_wait(
+                WorkerEvtEnd | WorkerEvtStartStop | WorkerEvtPauseResume | WorkerEvtConnect |
+                    WorkerEvtDisconnect,
+                FuriFlagWaitAny,
+                delay_cur);
+            FURI_LOG_D(WORKER_TAG, "running flags: %lu", flags);
+
+            delay_val -= delay_cur;
+            if(!(flags & FuriFlagError)) {
+                if(flags & WorkerEvtEnd) {
+                    break;
+                } else if(flags & WorkerEvtStartStop) {
+                    worker_state = BadKbStateIdle; // Stop executing script
+                    if(bad_kb->bt) {
+                        ble_profile_hid_kb_release_all(bad_kb->app->ble_hid);
+                    } else {
+                        furi_hal_hid_kb_release_all();
+                    }
+                } else if(flags & WorkerEvtDisconnect) {
+                    worker_state = BadKbStateNotConnected; // Disconnected
+                    if(bad_kb->bt) {
+                        ble_profile_hid_kb_release_all(bad_kb->app->ble_hid);
+                    } else {
+                        furi_hal_hid_kb_release_all();
+                    }
+                } else if(flags & WorkerEvtPauseResume) {
+                    pause_state = BadKbStateRunning;
+                    worker_state = BadKbStatePaused; // Pause
+                }
+                bad_kb->st.state = worker_state;
+                bad_kb->st.elapsed += (furi_get_tick() - start);
+                continue;
+            } else if(
+                (flags == (unsigned)FuriFlagErrorTimeout) ||
+                (flags == (unsigned)FuriFlagErrorResource)) {
+                if(delay_val > 0) {
+                    bad_kb->st.delay_remain--;
+                    bad_kb->st.elapsed += (furi_get_tick() - start);
+                    continue;
+                }
+                bad_kb->st.state = BadKbStateRunning;
+                delay_val = ducky_script_execute_next(bad_kb, script_file);
+                if(delay_val == SCRIPT_STATE_ERROR) { // Script error
+                    delay_val = 0;
+                    worker_state = BadKbStateScriptError;
+                    bad_kb->st.state = worker_state;
+                    if(bad_kb->bt) {
+                        ble_profile_hid_kb_release_all(bad_kb->app->ble_hid);
+                    } else {
+                        furi_hal_hid_kb_release_all();
+                    }
+                } else if(delay_val == SCRIPT_STATE_END) { // End of script
+                    delay_val = 0;
+                    worker_state = BadKbStateIdle;
+                    bad_kb->st.state = BadKbStateDone;
+                    if(bad_kb->bt) {
+                        ble_profile_hid_kb_release_all(bad_kb->app->ble_hid);
+                    } else {
+                        furi_hal_hid_kb_release_all();
+                    }
+                    bad_kb->st.elapsed += (furi_get_tick() - start);
+                    continue;
+                } else if(delay_val == SCRIPT_STATE_STRING_START) { // Start printing string with delays
+                    delay_val = bad_kb->defdelay;
+                    bad_kb->string_print_pos = 0;
+                    worker_state = BadKbStateStringDelay;
+                } else if(delay_val == SCRIPT_STATE_WAIT_FOR_BTN) { // set state to wait for user input
+                    worker_state = BadKbStateWaitForBtn;
+                    bad_kb->st.state = BadKbStateWaitForBtn; // Show long delays
+                } else if(delay_val > 100) {
+                    bad_kb->st.state = BadKbStateDelay; // Show long delays
+                    bad_kb->st.delay_remain = delay_val / 100;
+                }
+            } else {
+                furi_check((flags & FuriFlagError) == 0);
+            }
+        } else if(worker_state == BadKbStateWaitForBtn) { // State: Wait for button Press
+            start = 0;
+            FURI_LOG_D(WORKER_TAG, "button wait");
+            uint32_t flags = bad_kb_flags_get(
+                WorkerEvtEnd | WorkerEvtStartStop | WorkerEvtPauseResume | WorkerEvtConnect |
+                    WorkerEvtDisconnect,
+                FuriWaitForever);
+            FURI_LOG_D(WORKER_TAG, "button flags: %lu", flags);
+            if(!(flags & FuriFlagError)) {
+                if(flags & WorkerEvtEnd) {
+                    break;
+                } else if(flags & WorkerEvtStartStop) {
+                    delay_val = 0;
+                    worker_state = BadKbStateRunning;
+                } else if(flags & WorkerEvtDisconnect) {
+                    worker_state = BadKbStateNotConnected; // Disconnected
+                    if(bad_kb->bt) {
+                        ble_profile_hid_kb_release_all(bad_kb->app->ble_hid);
+                    } else {
+                        furi_hal_hid_kb_release_all();
+                    }
+                }
+                bad_kb->st.state = worker_state;
+                continue;
+            }
+        } else if(worker_state == BadKbStatePaused) { // State: Paused
+            start = 0;
+            FURI_LOG_D(WORKER_TAG, "paused wait");
+            uint32_t flags = bad_kb_flags_get(
+                WorkerEvtEnd | WorkerEvtStartStop | WorkerEvtPauseResume | WorkerEvtConnect |
+                    WorkerEvtDisconnect,
+                FuriWaitForever);
+            FURI_LOG_D(WORKER_TAG, "paused flags: %lu", flags);
+            if(!(flags & FuriFlagError)) {
+                if(flags & WorkerEvtEnd) {
+                    break;
+                } else if(flags & WorkerEvtStartStop) {
+                    worker_state = BadKbStateIdle; // Stop executing script
+                    bad_kb->st.state = worker_state;
+                    if(bad_kb->bt) {
+                        ble_profile_hid_kb_release_all(bad_kb->app->ble_hid);
+                    } else {
+                        furi_hal_hid_kb_release_all();
+                    }
+                } else if(flags & WorkerEvtDisconnect) {
+                    worker_state = BadKbStateNotConnected; // Disconnected
+                    bad_kb->st.state = worker_state;
+                    if(bad_kb->bt) {
+                        ble_profile_hid_kb_release_all(bad_kb->app->ble_hid);
+                    } else {
+                        furi_hal_hid_kb_release_all();
+                    }
+                } else if(flags & WorkerEvtPauseResume) {
+                    if(pause_state == BadKbStateRunning) {
+                        if(delay_val > 0) {
+                            bad_kb->st.state = BadKbStateDelay;
+                            bad_kb->st.delay_remain = delay_val / 100;
+                        } else {
+                            bad_kb->st.state = BadKbStateRunning;
+                            delay_val = 0;
+                        }
+                        worker_state = BadKbStateRunning; // Resume
+                    } else if(pause_state == BadKbStateStringDelay) {
+                        bad_kb->st.state = BadKbStateRunning;
+                        worker_state = BadKbStateStringDelay; // Resume
+                    }
+                }
+                continue;
+            }
+        } else if(worker_state == BadKbStateStringDelay) { // State: print string with delays
+            FURI_LOG_D(WORKER_TAG, "delay wait");
+            uint32_t flags = bad_kb_flags_get(
+                WorkerEvtEnd | WorkerEvtStartStop | WorkerEvtPauseResume | WorkerEvtConnect |
+                    WorkerEvtDisconnect,
+                bad_kb->stringdelay);
+            FURI_LOG_D(WORKER_TAG, "delay flags: %lu", flags);
+
+            if(!(flags & FuriFlagError)) {
+                if(flags & WorkerEvtEnd) {
+                    break;
+                } else if(flags & WorkerEvtStartStop) {
+                    worker_state = BadKbStateIdle; // Stop executing script
+                    if(bad_kb->bt) {
+                        ble_profile_hid_kb_release_all(bad_kb->app->ble_hid);
+                    } else {
+                        furi_hal_hid_kb_release_all();
+                    }
+                } else if(flags & WorkerEvtDisconnect) {
+                    worker_state = BadKbStateNotConnected; // Disconnected
+                    if(bad_kb->bt) {
+                        ble_profile_hid_kb_release_all(bad_kb->app->ble_hid);
+                    } else {
+                        furi_hal_hid_kb_release_all();
+                    }
+                } else if(flags & WorkerEvtPauseResume) {
+                    pause_state = BadKbStateStringDelay;
+                    worker_state = BadKbStatePaused; // Pause
+                }
+                bad_kb->st.state = worker_state;
+                bad_kb->st.elapsed += (furi_get_tick() - start);
+                continue;
+            } else if(
+                (flags == (unsigned)FuriFlagErrorTimeout) ||
+                (flags == (unsigned)FuriFlagErrorResource)) {
+                bool string_end = ducky_string_next(bad_kb);
+                if(string_end) {
+                    bad_kb->stringdelay = 0;
+                    worker_state = BadKbStateRunning;
+                }
+            } else {
+                furi_check((flags & FuriFlagError) == 0);
+            }
+        } else if(
+            (worker_state == BadKbStateFileError) ||
+            (worker_state == BadKbStateScriptError)) { // State: error
+            start = 0;
+            FURI_LOG_D(WORKER_TAG, "error wait");
+            uint32_t flags =
+                bad_kb_flags_get(WorkerEvtEnd, FuriWaitForever); // Waiting for exit command
+            FURI_LOG_D(WORKER_TAG, "error flags: %lu", flags);
+
+            if(flags & WorkerEvtEnd) {
+                break;
+            }
+        }
+        if(bad_kb->bt) {
+            update_bt_timeout(bad_kb->bt);
+        }
+        if(start) {
+            bad_kb->st.elapsed += (furi_get_tick() - start);
+        }
+    }
+
+    bt_set_status_changed_callback(bad_kb->app->bt, NULL, NULL);
+    furi_hal_hid_set_state_callback(NULL, NULL);
+
+    storage_file_close(script_file);
+    storage_file_free(script_file);
+    furi_string_free(bad_kb->line);
+    furi_string_free(bad_kb->line_prev);
+    furi_string_free(bad_kb->string_print);
+
+    FURI_LOG_I(WORKER_TAG, "End");
+
+    return 0;
+}
+
+static void bad_kb_script_set_default_keyboard_layout(BadKbScript* bad_kb) {
+    furi_assert(bad_kb);
+    furi_string_set_str(bad_kb->keyboard_layout, "");
+    memset(bad_kb->layout, HID_KEYBOARD_NONE, sizeof(bad_kb->layout));
+    memcpy(bad_kb->layout, hid_asciimap, MIN(sizeof(hid_asciimap), sizeof(bad_kb->layout)));
+}
+
+BadKbScript* bad_kb_script_open(FuriString* file_path, Bt* bt, BadKbApp* app) {
+    furi_assert(file_path);
+
+    BadKbScript* bad_kb = malloc(sizeof(BadKbScript));
+    bad_kb->app = app;
+    bad_kb->file_path = furi_string_alloc();
+    furi_string_set(bad_kb->file_path, file_path);
+    bad_kb->keyboard_layout = furi_string_alloc();
+    bad_kb_script_set_default_keyboard_layout(bad_kb);
+
+    bad_kb->st.state = BadKbStateInit;
+    bad_kb->st.error[0] = '\0';
+    bad_kb->st.is_bt = !!bt;
+
+    bad_kb->bt = bt;
+
+    bad_kb->thread = furi_thread_alloc_ex("BadKbWorker", 2048, bad_kb_worker, bad_kb);
+    furi_thread_start(bad_kb->thread);
+    return bad_kb;
+} //-V773
+
+void bad_kb_script_close(BadKbScript* bad_kb) {
+    furi_assert(bad_kb);
+    furi_record_close(RECORD_STORAGE);
+    furi_thread_flags_set(furi_thread_get_id(bad_kb->thread), WorkerEvtEnd);
+    furi_thread_join(bad_kb->thread);
+    furi_thread_free(bad_kb->thread);
+    furi_string_free(bad_kb->file_path);
+    furi_string_free(bad_kb->keyboard_layout);
+    free(bad_kb);
+}
+
+void bad_kb_script_set_keyboard_layout(BadKbScript* bad_kb, FuriString* layout_path) {
+    furi_assert(bad_kb);
+
+    if((bad_kb->st.state == BadKbStateRunning) || (bad_kb->st.state == BadKbStateDelay)) {
+        // do not update keyboard layout while a script is running
+        return;
+    }
+
+    File* layout_file = storage_file_alloc(furi_record_open(RECORD_STORAGE));
+    if(!furi_string_empty(layout_path)) { //-V1051
+        furi_string_set(bad_kb->keyboard_layout, layout_path);
+        if(storage_file_open(
+               layout_file, furi_string_get_cstr(layout_path), FSAM_READ, FSOM_OPEN_EXISTING)) {
+            uint16_t layout[128];
+            if(storage_file_read(layout_file, layout, sizeof(layout)) == sizeof(layout)) {
+                memcpy(bad_kb->layout, layout, sizeof(layout));
+            }
+        }
+        storage_file_close(layout_file);
+    } else {
+        bad_kb_script_set_default_keyboard_layout(bad_kb);
+    }
+    storage_file_free(layout_file);
+}
+
+void bad_kb_script_start_stop(BadKbScript* bad_kb) {
+    furi_assert(bad_kb);
+    furi_thread_flags_set(furi_thread_get_id(bad_kb->thread), WorkerEvtStartStop);
+}
+
+void bad_kb_script_pause_resume(BadKbScript* bad_kb) {
+    furi_assert(bad_kb);
+    furi_thread_flags_set(furi_thread_get_id(bad_kb->thread), WorkerEvtPauseResume);
+}
+
+BadKbState* bad_kb_script_get_state(BadKbScript* bad_kb) {
+    furi_assert(bad_kb);
+    return &(bad_kb->st);
+}

+ 86 - 0
base_pack/bad_kb/helpers/ducky_script.h

@@ -0,0 +1,86 @@
+#pragma once
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <furi.h>
+#include <furi_hal.h>
+#include <bt/bt_service/bt.h>
+
+#include "../bad_kb_app.h"
+
+typedef enum {
+    LevelRssi122_100,
+    LevelRssi99_80,
+    LevelRssi79_60,
+    LevelRssi59_40,
+    LevelRssi39_0,
+    LevelRssiNum,
+    LevelRssiError = 0xFF,
+} LevelRssiRange;
+
+extern const uint8_t bt_hid_delays[LevelRssiNum];
+
+extern uint8_t bt_timeout;
+
+typedef enum {
+    WorkerEvtStartStop = (1 << 0),
+    WorkerEvtPauseResume = (1 << 1),
+    WorkerEvtEnd = (1 << 2),
+    WorkerEvtConnect = (1 << 3),
+    WorkerEvtDisconnect = (1 << 4),
+} WorkerEvtFlags;
+
+typedef enum {
+    BadKbStateInit,
+    BadKbStateNotConnected,
+    BadKbStateIdle,
+    BadKbStateWillRun,
+    BadKbStateRunning,
+    BadKbStateDelay,
+    BadKbStateStringDelay,
+    BadKbStateWaitForBtn,
+    BadKbStatePaused,
+    BadKbStateDone,
+    BadKbStateScriptError,
+    BadKbStateFileError,
+} BadKbWorkerState;
+
+typedef struct {
+    BadKbWorkerState state;
+    bool is_bt;
+    uint32_t pin;
+    size_t line_cur;
+    size_t line_nb;
+    uint32_t delay_remain;
+    size_t error_line;
+    char error[64];
+    uint32_t elapsed;
+} BadKbState;
+
+typedef struct BadKbScript BadKbScript;
+
+BadKbScript* bad_kb_script_open(FuriString* file_path, Bt* bt, BadKbApp* app);
+
+void bad_kb_script_close(BadKbScript* bad_kb);
+
+void bad_kb_script_set_keyboard_layout(BadKbScript* bad_kb, FuriString* layout_path);
+
+void bad_kb_script_start(BadKbScript* bad_kb);
+
+void bad_kb_script_stop(BadKbScript* bad_kb);
+
+void bad_kb_script_start_stop(BadKbScript* bad_kb);
+
+void bad_kb_script_pause_resume(BadKbScript* bad_kb);
+
+BadKbState* bad_kb_script_get_state(BadKbScript* bad_kb);
+
+void bad_kb_bt_hid_state_callback(BtStatus status, void* context);
+
+void bad_kb_usb_hid_state_callback(bool state, void* context);
+
+#ifdef __cplusplus
+}
+#endif

+ 215 - 0
base_pack/bad_kb/helpers/ducky_script_commands.c

@@ -0,0 +1,215 @@
+#include "../bad_kb_app_i.h"
+#include <furi_hal.h>
+#include <furi_hal_usb_hid.h>
+#include "ble_hid.h"
+#include "ducky_script.h"
+#include "ducky_script_i.h"
+
+typedef int32_t (*DuckyCmdCallback)(BadKbScript* bad_kb, const char* line, int32_t param);
+
+typedef struct {
+    char* name;
+    DuckyCmdCallback callback;
+    int32_t param;
+} DuckyCmd;
+
+static int32_t ducky_fnc_delay(BadKbScript* bad_kb, const char* line, int32_t param) {
+    UNUSED(param);
+
+    line = &line[ducky_get_command_len(line) + 1];
+    uint32_t delay_val = 0;
+    bool state = ducky_get_number(line, &delay_val);
+    if((state) && (delay_val > 0)) {
+        return (int32_t)delay_val;
+    }
+
+    return ducky_error(bad_kb, "Invalid number %s", line);
+}
+
+static int32_t ducky_fnc_defdelay(BadKbScript* bad_kb, const char* line, int32_t param) {
+    UNUSED(param);
+
+    line = &line[ducky_get_command_len(line) + 1];
+    bool state = ducky_get_number(line, &bad_kb->defdelay);
+    if(!state) {
+        return ducky_error(bad_kb, "Invalid number %s", line);
+    }
+    return 0;
+}
+
+static int32_t ducky_fnc_strdelay(BadKbScript* bad_kb, const char* line, int32_t param) {
+    UNUSED(param);
+
+    line = &line[ducky_get_command_len(line) + 1];
+    bool state = ducky_get_number(line, &bad_kb->stringdelay);
+    if(!state) {
+        return ducky_error(bad_kb, "Invalid number %s", line);
+    }
+    return 0;
+}
+
+static int32_t ducky_fnc_string(BadKbScript* bad_kb, const char* line, int32_t param) {
+    line = &line[ducky_get_command_len(line) + 1];
+    furi_string_set_str(bad_kb->string_print, line);
+    if(param == 1) {
+        furi_string_cat(bad_kb->string_print, "\n");
+    }
+
+    if(bad_kb->stringdelay == 0) { // stringdelay not set - run command immediately
+        bool state = ducky_string(bad_kb, furi_string_get_cstr(bad_kb->string_print));
+        if(!state) {
+            return ducky_error(bad_kb, "Invalid string %s", line);
+        }
+    } else { // stringdelay is set - run command in thread to keep handling external events
+        return SCRIPT_STATE_STRING_START;
+    }
+
+    return 0;
+}
+
+static int32_t ducky_fnc_repeat(BadKbScript* bad_kb, const char* line, int32_t param) {
+    UNUSED(param);
+
+    line = &line[ducky_get_command_len(line) + 1];
+    bool state = ducky_get_number(line, &bad_kb->repeat_cnt);
+    if((!state) || (bad_kb->repeat_cnt == 0)) {
+        return ducky_error(bad_kb, "Invalid number %s", line);
+    }
+    return 0;
+}
+
+static int32_t ducky_fnc_sysrq(BadKbScript* bad_kb, const char* line, int32_t param) {
+    UNUSED(param);
+
+    line = &line[ducky_get_command_len(line) + 1];
+    uint16_t key = ducky_get_keycode(bad_kb, line, true);
+    if(bad_kb->bt) {
+        ble_profile_hid_kb_press(
+            bad_kb->app->ble_hid, KEY_MOD_LEFT_ALT | HID_KEYBOARD_PRINT_SCREEN);
+        ble_profile_hid_kb_press(bad_kb->app->ble_hid, key);
+        furi_delay_ms(bt_timeout);
+        ble_profile_hid_kb_release_all(bad_kb->app->ble_hid);
+    } else {
+        furi_hal_hid_kb_press(KEY_MOD_LEFT_ALT | HID_KEYBOARD_PRINT_SCREEN);
+        furi_hal_hid_kb_press(key);
+        furi_hal_hid_kb_release_all();
+    }
+    return 0;
+}
+
+static int32_t ducky_fnc_altchar(BadKbScript* bad_kb, const char* line, int32_t param) {
+    UNUSED(param);
+
+    line = &line[ducky_get_command_len(line) + 1];
+    ducky_numlock_on(bad_kb);
+    bool state = ducky_altchar(bad_kb, line);
+    if(!state) {
+        return ducky_error(bad_kb, "Invalid altchar %s", line);
+    }
+    return 0;
+}
+
+static int32_t ducky_fnc_altstring(BadKbScript* bad_kb, const char* line, int32_t param) {
+    UNUSED(param);
+
+    line = &line[ducky_get_command_len(line) + 1];
+    ducky_numlock_on(bad_kb);
+    bool state = ducky_altstring(bad_kb, line);
+    if(!state) {
+        return ducky_error(bad_kb, "Invalid altstring %s", line);
+    }
+    return 0;
+}
+
+static int32_t ducky_fnc_hold(BadKbScript* bad_kb, const char* line, int32_t param) {
+    UNUSED(param);
+
+    line = &line[ducky_get_command_len(line) + 1];
+    uint16_t key = ducky_get_keycode(bad_kb, line, true);
+    if(key == HID_KEYBOARD_NONE) {
+        return ducky_error(bad_kb, "No keycode defined for %s", line);
+    }
+    bad_kb->key_hold_nb++;
+    if(bad_kb->key_hold_nb > (HID_KB_MAX_KEYS - 1)) {
+        return ducky_error(bad_kb, "Too many keys are hold");
+    }
+    if(bad_kb->bt) {
+        ble_profile_hid_kb_press(bad_kb->app->ble_hid, key);
+    } else {
+        furi_hal_hid_kb_press(key);
+    }
+    return 0;
+}
+
+static int32_t ducky_fnc_release(BadKbScript* bad_kb, const char* line, int32_t param) {
+    UNUSED(param);
+
+    line = &line[ducky_get_command_len(line) + 1];
+    uint16_t key = ducky_get_keycode(bad_kb, line, true);
+    if(key == HID_KEYBOARD_NONE) {
+        return ducky_error(bad_kb, "No keycode defined for %s", line);
+    }
+    if(bad_kb->key_hold_nb == 0) {
+        return ducky_error(bad_kb, "No keys are hold");
+    }
+    bad_kb->key_hold_nb--;
+    if(bad_kb->bt) {
+        ble_profile_hid_kb_release(bad_kb->app->ble_hid, key);
+    } else {
+        furi_hal_hid_kb_release(key);
+    }
+    return 0;
+}
+
+static int32_t ducky_fnc_waitforbutton(BadKbScript* bad_kb, const char* line, int32_t param) {
+    UNUSED(param);
+    UNUSED(bad_kb);
+    UNUSED(line);
+
+    return SCRIPT_STATE_WAIT_FOR_BTN;
+}
+
+static const DuckyCmd ducky_commands[] = {
+    {"REM", NULL, -1},
+    {"ID", NULL, -1},
+    {"BT_ID", NULL, -1},
+    {"DELAY", ducky_fnc_delay, -1},
+    {"STRING", ducky_fnc_string, 0},
+    {"STRINGLN", ducky_fnc_string, 1},
+    {"DEFAULT_DELAY", ducky_fnc_defdelay, -1},
+    {"DEFAULTDELAY", ducky_fnc_defdelay, -1},
+    {"STRINGDELAY", ducky_fnc_strdelay, -1},
+    {"STRING_DELAY", ducky_fnc_strdelay, -1},
+    {"REPEAT", ducky_fnc_repeat, -1},
+    {"SYSRQ", ducky_fnc_sysrq, -1},
+    {"ALTCHAR", ducky_fnc_altchar, -1},
+    {"ALTSTRING", ducky_fnc_altstring, -1},
+    {"ALTCODE", ducky_fnc_altstring, -1},
+    {"HOLD", ducky_fnc_hold, -1},
+    {"RELEASE", ducky_fnc_release, -1},
+    {"WAIT_FOR_BUTTON_PRESS", ducky_fnc_waitforbutton, -1},
+};
+
+#define TAG "BadKb"
+#define WORKER_TAG TAG "Worker"
+
+int32_t ducky_execute_cmd(BadKbScript* bad_kb, const char* line) {
+    size_t cmd_word_len = strcspn(line, " ");
+    for(size_t i = 0; i < COUNT_OF(ducky_commands); i++) {
+        size_t cmd_compare_len = strlen(ducky_commands[i].name);
+
+        if(cmd_compare_len != cmd_word_len) {
+            continue;
+        }
+
+        if(strncmp(line, ducky_commands[i].name, cmd_compare_len) == 0) {
+            if(ducky_commands[i].callback == NULL) {
+                return 0;
+            } else {
+                return ((ducky_commands[i].callback)(bad_kb, line, ducky_commands[i].param));
+            }
+        }
+    }
+
+    return SCRIPT_STATE_CMD_UNKNOWN;
+}

+ 73 - 0
base_pack/bad_kb/helpers/ducky_script_i.h

@@ -0,0 +1,73 @@
+#pragma once
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <furi.h>
+#include <furi_hal.h>
+#include "ducky_script.h"
+
+#define SCRIPT_STATE_ERROR (-1)
+#define SCRIPT_STATE_END (-2)
+#define SCRIPT_STATE_NEXT_LINE (-3)
+#define SCRIPT_STATE_CMD_UNKNOWN (-4)
+#define SCRIPT_STATE_STRING_START (-5)
+#define SCRIPT_STATE_WAIT_FOR_BTN (-6)
+
+#define FILE_BUFFER_LEN 16
+
+struct BadKbScript {
+    FuriThread* thread;
+    BadKbState st;
+
+    FuriString* file_path;
+    FuriString* keyboard_layout;
+    uint8_t file_buf[FILE_BUFFER_LEN + 1];
+    uint8_t buf_start;
+    uint8_t buf_len;
+    bool file_end;
+
+    uint32_t defdelay;
+    uint32_t stringdelay;
+    uint16_t layout[128];
+
+    FuriString* line;
+    FuriString* line_prev;
+    uint32_t repeat_cnt;
+    uint8_t key_hold_nb;
+
+    FuriString* string_print;
+    size_t string_print_pos;
+
+    Bt* bt;
+    BadKbApp* app;
+};
+
+uint16_t ducky_get_keycode(BadKbScript* bad_kb, const char* param, bool accept_chars);
+
+uint32_t ducky_get_command_len(const char* line);
+
+bool ducky_is_line_end(const char chr);
+
+uint16_t ducky_get_keycode_by_name(const char* param);
+
+bool ducky_get_number(const char* param, uint32_t* val);
+
+void ducky_numlock_on(BadKbScript* bad_kb);
+
+bool ducky_numpad_press(BadKbScript* bad_kb, const char num);
+
+bool ducky_altchar(BadKbScript* bad_kb, const char* charcode);
+
+bool ducky_altstring(BadKbScript* bad_kb, const char* param);
+
+bool ducky_string(BadKbScript* bad_kb, const char* param);
+
+int32_t ducky_execute_cmd(BadKbScript* bad_kb, const char* line);
+
+int32_t ducky_error(BadKbScript* bad_kb, const char* text, ...);
+
+#ifdef __cplusplus
+}
+#endif

+ 1 - 0
base_pack/bad_bt/helpers/ducky_script_keycodes.c → base_pack/bad_kb/helpers/ducky_script_keycodes.c

@@ -1,4 +1,5 @@
 #include <furi_hal.h>
+#include <furi_hal_usb_hid.h>
 #include "ducky_script_i.h"
 
 typedef struct {

+ 0 - 0
base_pack/bad_bt/images/badbt_10px.png → base_pack/bad_kb/icon.png


+ 30 - 0
base_pack/bad_kb/scenes/bad_kb_scene.c

@@ -0,0 +1,30 @@
+#include "bad_kb_scene.h"
+
+// Generate scene on_enter handlers array
+#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_enter,
+void (*const bad_kb_scene_on_enter_handlers[])(void*) = {
+#include "bad_kb_scene_config.h"
+};
+#undef ADD_SCENE
+
+// Generate scene on_event handlers array
+#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_event,
+bool (*const bad_kb_scene_on_event_handlers[])(void* context, SceneManagerEvent event) = {
+#include "bad_kb_scene_config.h"
+};
+#undef ADD_SCENE
+
+// Generate scene on_exit handlers array
+#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_exit,
+void (*const bad_kb_scene_on_exit_handlers[])(void* context) = {
+#include "bad_kb_scene_config.h"
+};
+#undef ADD_SCENE
+
+// Initialize scene handlers configuration structure
+const SceneManagerHandlers bad_kb_scene_handlers = {
+    .on_enter_handlers = bad_kb_scene_on_enter_handlers,
+    .on_event_handlers = bad_kb_scene_on_event_handlers,
+    .on_exit_handlers = bad_kb_scene_on_exit_handlers,
+    .scene_num = BadKbSceneNum,
+};

+ 8 - 8
base_pack/bad_bt/scenes/bad_bt_scene.h → base_pack/bad_kb/scenes/bad_kb_scene.h

@@ -3,27 +3,27 @@
 #include <gui/scene_manager.h>
 
 // Generate scene id and total number
-#define ADD_SCENE(prefix, name, id) BadBtScene##id,
+#define ADD_SCENE(prefix, name, id) BadKbScene##id,
 typedef enum {
-#include "bad_bt_scene_config.h"
-    BadBtSceneNum,
-} BadBtScene;
+#include "bad_kb_scene_config.h"
+    BadKbSceneNum,
+} BadKbScene;
 #undef ADD_SCENE
 
-extern const SceneManagerHandlers bad_bt_scene_handlers;
+extern const SceneManagerHandlers bad_kb_scene_handlers;
 
 // Generate scene on_enter handlers declaration
 #define ADD_SCENE(prefix, name, id) void prefix##_scene_##name##_on_enter(void*);
-#include "bad_bt_scene_config.h"
+#include "bad_kb_scene_config.h"
 #undef ADD_SCENE
 
 // Generate scene on_event handlers declaration
 #define ADD_SCENE(prefix, name, id) \
     bool prefix##_scene_##name##_on_event(void* context, SceneManagerEvent event);
-#include "bad_bt_scene_config.h"
+#include "bad_kb_scene_config.h"
 #undef ADD_SCENE
 
 // Generate scene on_exit handlers declaration
 #define ADD_SCENE(prefix, name, id) void prefix##_scene_##name##_on_exit(void* context);
-#include "bad_bt_scene_config.h"
+#include "bad_kb_scene_config.h"
 #undef ADD_SCENE

+ 186 - 0
base_pack/bad_kb/scenes/bad_kb_scene_config.c

@@ -0,0 +1,186 @@
+#include "../bad_kb_app_i.h"
+#include "furi_hal_power.h"
+#include "furi_hal_usb.h"
+
+enum VarItemListIndex {
+    VarItemListIndexKeyboardLayout,
+    VarItemListIndexConnection,
+};
+
+enum VarItemListIndexBt {
+    VarItemListIndexBtRemember = VarItemListIndexConnection + 1,
+    VarItemListIndexBtDeviceName,
+    VarItemListIndexBtMacAddress,
+    VarItemListIndexBtRandomizeMac,
+};
+
+enum VarItemListIndexUsb {
+    VarItemListIndexUsbManufacturer = VarItemListIndexConnection + 1,
+    VarItemListIndexUsbProductName,
+    VarItemListIndexUsbVidPid,
+    VarItemListIndexUsbRandomizeVidPid,
+};
+
+void bad_kb_scene_config_connection_callback(VariableItem* item) {
+    BadKbApp* bad_kb = variable_item_get_context(item);
+    bad_kb->is_bt = variable_item_get_current_value_index(item);
+    variable_item_set_current_value_text(item, bad_kb->is_bt ? "BT" : "USB");
+    view_dispatcher_send_custom_event(bad_kb->view_dispatcher, VarItemListIndexConnection);
+}
+
+void bad_kb_scene_config_bt_remember_callback(VariableItem* item) {
+    BadKbApp* bad_kb = variable_item_get_context(item);
+    bad_kb->bt_remember = variable_item_get_current_value_index(item);
+    variable_item_set_current_value_text(item, bad_kb->bt_remember ? "ON" : "OFF");
+    view_dispatcher_send_custom_event(bad_kb->view_dispatcher, VarItemListIndexBtRemember);
+}
+
+void bad_kb_scene_config_var_item_list_callback(void* context, uint32_t index) {
+    BadKbApp* bad_kb = context;
+    view_dispatcher_send_custom_event(bad_kb->view_dispatcher, index);
+}
+
+void bad_kb_scene_config_on_enter(void* context) {
+    BadKbApp* bad_kb = context;
+    VariableItemList* var_item_list = bad_kb->var_item_list;
+    VariableItem* item;
+
+    item = variable_item_list_add(var_item_list, "Keyboard layout", 0, NULL, bad_kb);
+
+    item = variable_item_list_add(
+        var_item_list, "Connection", 2, bad_kb_scene_config_connection_callback, bad_kb);
+    variable_item_set_current_value_index(item, bad_kb->is_bt);
+    variable_item_set_current_value_text(item, bad_kb->is_bt ? "BT" : "USB");
+    /*if(bad_kb->has_usb_id) {
+        variable_item_set_locked(item, true, "Script has\nID cmd!\nLocked to\nUSB Mode!");
+    } else if(bad_kb->has_bt_id) {
+        variable_item_set_locked(item, true, "Script has\nBT_ID cmd!\nLocked to\nBT Mode!");
+    }*/
+
+    if(bad_kb->is_bt) {
+        item = variable_item_list_add(
+            var_item_list, "BT Remember", 2, bad_kb_scene_config_bt_remember_callback, bad_kb);
+        variable_item_set_current_value_index(item, bad_kb->bt_remember);
+        variable_item_set_current_value_text(item, bad_kb->bt_remember ? "ON" : "OFF");
+
+        item = variable_item_list_add(var_item_list, "BT Device Name", 0, NULL, bad_kb);
+        if(bad_kb->set_bt_id) {
+            variable_item_set_locked(item, true, "Script has\nBT_ID cmd!\nLocked to\nset Name!");
+        }
+
+        item = variable_item_list_add(var_item_list, "BT MAC Address", 0, NULL, bad_kb);
+        if(bad_kb->bt_remember) {
+            variable_item_set_locked(item, true, "Remember\nmust be Off!");
+        } else if(bad_kb->set_bt_id) {
+            variable_item_set_locked(item, true, "Script has\nBT_ID cmd!\nLocked to\nset MAC!");
+        }
+
+        item = variable_item_list_add(var_item_list, "Randomize BT MAC", 0, NULL, bad_kb);
+        if(bad_kb->bt_remember) {
+            variable_item_set_locked(item, true, "Remember\nmust be Off!");
+        } else if(bad_kb->set_bt_id) {
+            variable_item_set_locked(item, true, "Script has\nBT_ID cmd!\nLocked to\nset MAC!");
+        }
+    } else {
+        item = variable_item_list_add(var_item_list, "USB Manufacturer", 0, NULL, bad_kb);
+        if(bad_kb->set_usb_id) {
+            variable_item_set_locked(item, true, "Script has\nID cmd!\nLocked to\nset Mname!");
+        }
+
+        item = variable_item_list_add(var_item_list, "USB Product Name", 0, NULL, bad_kb);
+        if(bad_kb->set_usb_id) {
+            variable_item_set_locked(item, true, "Script has\nID cmd!\nLocked to\nset Pname!");
+        }
+
+        item = variable_item_list_add(var_item_list, "USB VID and PID", 0, NULL, bad_kb);
+        if(bad_kb->set_usb_id) {
+            variable_item_set_locked(item, true, "Script has\nID cmd!\nLocked to\nset IDs!");
+        }
+
+        item = variable_item_list_add(var_item_list, "Randomize USB VID:PID", 0, NULL, bad_kb);
+        if(bad_kb->set_usb_id) {
+            variable_item_set_locked(item, true, "Script has\nID cmd!\nLocked to\nset IDs!");
+        }
+    }
+
+    variable_item_list_set_enter_callback(
+        var_item_list, bad_kb_scene_config_var_item_list_callback, bad_kb);
+
+    variable_item_list_set_selected_item(
+        var_item_list, scene_manager_get_scene_state(bad_kb->scene_manager, BadKbSceneConfig));
+
+    view_dispatcher_switch_to_view(bad_kb->view_dispatcher, BadKbAppViewVarItemList);
+}
+
+bool bad_kb_scene_config_on_event(void* context, SceneManagerEvent event) {
+    BadKbApp* bad_kb = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        scene_manager_set_scene_state(bad_kb->scene_manager, BadKbSceneConfig, event.event);
+        consumed = true;
+        switch(event.event) {
+        case VarItemListIndexKeyboardLayout:
+            scene_manager_next_scene(bad_kb->scene_manager, BadKbSceneConfigLayout);
+            break;
+        case VarItemListIndexConnection:
+            bad_kb_config_refresh(bad_kb);
+            break;
+        default:
+            break;
+        }
+        if(bad_kb->is_bt) {
+            switch(event.event) {
+            case VarItemListIndexBtRemember:
+                bad_kb_config_refresh(bad_kb);
+                break;
+            case VarItemListIndexBtDeviceName:
+                scene_manager_next_scene(bad_kb->scene_manager, BadKbSceneConfigBtName);
+                break;
+            case VarItemListIndexBtMacAddress:
+                scene_manager_next_scene(bad_kb->scene_manager, BadKbSceneConfigBtMac);
+                break;
+            case VarItemListIndexBtRandomizeMac:
+                furi_hal_random_fill_buf(bad_kb->config.ble.mac, sizeof(bad_kb->config.ble.mac));
+                bad_kb_config_refresh(bad_kb);
+                break;
+            default:
+                break;
+            }
+        } else {
+            switch(event.event) {
+            case VarItemListIndexUsbManufacturer:
+                scene_manager_set_scene_state(
+                    bad_kb->scene_manager, BadKbSceneConfigUsbName, true);
+                scene_manager_next_scene(bad_kb->scene_manager, BadKbSceneConfigUsbName);
+                break;
+            case VarItemListIndexUsbProductName:
+                scene_manager_set_scene_state(
+                    bad_kb->scene_manager, BadKbSceneConfigUsbName, false);
+                scene_manager_next_scene(bad_kb->scene_manager, BadKbSceneConfigUsbName);
+                break;
+            case VarItemListIndexUsbVidPid:
+                scene_manager_next_scene(bad_kb->scene_manager, BadKbSceneConfigUsbVidPid);
+                break;
+            case VarItemListIndexUsbRandomizeVidPid:
+                furi_hal_random_fill_buf(
+                    (void*)bad_kb->usb_vidpid_buf, sizeof(bad_kb->usb_vidpid_buf));
+                bad_kb->config.usb.vid = bad_kb->usb_vidpid_buf[0];
+                bad_kb->config.usb.pid = bad_kb->usb_vidpid_buf[1];
+                bad_kb_config_refresh(bad_kb);
+                break;
+            default:
+                break;
+            }
+        }
+    }
+
+    return consumed;
+}
+
+void bad_kb_scene_config_on_exit(void* context) {
+    BadKbApp* bad_kb = context;
+    VariableItemList* var_item_list = bad_kb->var_item_list;
+
+    variable_item_list_reset(var_item_list);
+}

+ 9 - 0
base_pack/bad_kb/scenes/bad_kb_scene_config.h

@@ -0,0 +1,9 @@
+ADD_SCENE(bad_kb, file_select, FileSelect)
+ADD_SCENE(bad_kb, work, Work)
+ADD_SCENE(bad_kb, error, Error)
+ADD_SCENE(bad_kb, config, Config)
+ADD_SCENE(bad_kb, config_layout, ConfigLayout)
+ADD_SCENE(bad_kb, config_bt_name, ConfigBtName)
+ADD_SCENE(bad_kb, config_bt_mac, ConfigBtMac)
+ADD_SCENE(bad_kb, config_usb_name, ConfigUsbName)
+ADD_SCENE(bad_kb, config_usb_vidpid, ConfigUsbVidPid)

+ 50 - 0
base_pack/bad_kb/scenes/bad_kb_scene_config_bt_mac.c

@@ -0,0 +1,50 @@
+#include "../bad_kb_app_i.h"
+
+void bad_kb_scene_config_bt_mac_byte_input_callback(void* context) {
+    BadKbApp* bad_kb = context;
+
+    view_dispatcher_send_custom_event(bad_kb->view_dispatcher, BadKbAppCustomEventByteInputDone);
+}
+
+void bad_kb_scene_config_bt_mac_on_enter(void* context) {
+    BadKbApp* bad_kb = context;
+    ByteInput* byte_input = bad_kb->byte_input;
+
+    memcpy(bad_kb->bt_mac_buf, bad_kb->config.ble.mac, sizeof(bad_kb->bt_mac_buf));
+    furi_hal_bt_reverse_mac_addr(bad_kb->bt_mac_buf);
+    byte_input_set_header_text(byte_input, "Set BT MAC address");
+
+    byte_input_set_result_callback(
+        byte_input,
+        bad_kb_scene_config_bt_mac_byte_input_callback,
+        NULL,
+        bad_kb,
+        bad_kb->bt_mac_buf,
+        sizeof(bad_kb->bt_mac_buf));
+
+    view_dispatcher_switch_to_view(bad_kb->view_dispatcher, BadKbAppViewByteInput);
+}
+
+bool bad_kb_scene_config_bt_mac_on_event(void* context, SceneManagerEvent event) {
+    BadKbApp* bad_kb = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        consumed = true;
+        if(event.event == BadKbAppCustomEventByteInputDone) {
+            furi_hal_bt_reverse_mac_addr(bad_kb->bt_mac_buf);
+            memcpy(bad_kb->config.ble.mac, bad_kb->bt_mac_buf, sizeof(bad_kb->config.ble.mac));
+            bad_kb_config_refresh(bad_kb);
+        }
+        scene_manager_previous_scene(bad_kb->scene_manager);
+    }
+    return consumed;
+}
+
+void bad_kb_scene_config_bt_mac_on_exit(void* context) {
+    BadKbApp* bad_kb = context;
+    ByteInput* byte_input = bad_kb->byte_input;
+
+    byte_input_set_result_callback(byte_input, NULL, NULL, NULL, NULL, 0);
+    byte_input_set_header_text(byte_input, "");
+}

+ 47 - 0
base_pack/bad_kb/scenes/bad_kb_scene_config_bt_name.c

@@ -0,0 +1,47 @@
+#include "../bad_kb_app_i.h"
+
+static void bad_kb_scene_config_bt_name_text_input_callback(void* context) {
+    BadKbApp* bad_kb = context;
+
+    view_dispatcher_send_custom_event(bad_kb->view_dispatcher, BadKbAppCustomEventTextInputDone);
+}
+
+void bad_kb_scene_config_bt_name_on_enter(void* context) {
+    BadKbApp* bad_kb = context;
+    TextInput* text_input = bad_kb->text_input;
+
+    strlcpy(bad_kb->bt_name_buf, bad_kb->config.ble.name, sizeof(bad_kb->bt_name_buf));
+    text_input_set_header_text(text_input, "Set BT device name");
+
+    text_input_set_result_callback(
+        text_input,
+        bad_kb_scene_config_bt_name_text_input_callback,
+        bad_kb,
+        bad_kb->bt_name_buf,
+        sizeof(bad_kb->bt_name_buf),
+        true);
+
+    view_dispatcher_switch_to_view(bad_kb->view_dispatcher, BadKbAppViewTextInput);
+}
+
+bool bad_kb_scene_config_bt_name_on_event(void* context, SceneManagerEvent event) {
+    BadKbApp* bad_kb = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        consumed = true;
+        if(event.event == BadKbAppCustomEventTextInputDone) {
+            strlcpy(bad_kb->config.ble.name, bad_kb->bt_name_buf, sizeof(bad_kb->config.ble.name));
+            bad_kb_config_refresh(bad_kb);
+        }
+        scene_manager_previous_scene(bad_kb->scene_manager);
+    }
+    return consumed;
+}
+
+void bad_kb_scene_config_bt_name_on_exit(void* context) {
+    BadKbApp* bad_kb = context;
+    TextInput* text_input = bad_kb->text_input;
+
+    text_input_reset(text_input);
+}

+ 48 - 0
base_pack/bad_kb/scenes/bad_kb_scene_config_layout.c

@@ -0,0 +1,48 @@
+#include "../bad_kb_app_i.h"
+#include "furi_hal_power.h"
+#include "furi_hal_usb.h"
+#include <storage/storage.h>
+
+static bool bad_kb_layout_select(BadKbApp* bad_kb) {
+    furi_assert(bad_kb);
+
+    FuriString* predefined_path;
+    predefined_path = furi_string_alloc();
+    if(!furi_string_empty(bad_kb->keyboard_layout)) {
+        furi_string_set(predefined_path, bad_kb->keyboard_layout);
+    } else {
+        furi_string_set(predefined_path, BAD_KB_APP_PATH_LAYOUT_FOLDER);
+    }
+
+    DialogsFileBrowserOptions browser_options;
+    dialog_file_browser_set_basic_options(
+        &browser_options, BAD_KB_APP_LAYOUT_EXTENSION, &I_keyboard_10px);
+    browser_options.base_path = BAD_KB_APP_PATH_LAYOUT_FOLDER;
+    browser_options.skip_assets = false;
+
+    // Input events and views are managed by file_browser
+    bool res = dialog_file_browser_show(
+        bad_kb->dialogs, bad_kb->keyboard_layout, predefined_path, &browser_options);
+
+    furi_string_free(predefined_path);
+    return res;
+}
+
+void bad_kb_scene_config_layout_on_enter(void* context) {
+    BadKbApp* bad_kb = context;
+
+    if(bad_kb_layout_select(bad_kb)) {
+        bad_kb_script_set_keyboard_layout(bad_kb->bad_kb_script, bad_kb->keyboard_layout);
+    }
+    scene_manager_previous_scene(bad_kb->scene_manager);
+}
+
+bool bad_kb_scene_config_layout_on_event(void* context, SceneManagerEvent event) {
+    UNUSED(context);
+    UNUSED(event);
+    return false;
+}
+
+void bad_kb_scene_config_layout_on_exit(void* context) {
+    UNUSED(context);
+}

+ 62 - 0
base_pack/bad_kb/scenes/bad_kb_scene_config_usb_name.c

@@ -0,0 +1,62 @@
+#include "../bad_kb_app_i.h"
+
+static void bad_kb_scene_config_usb_name_text_input_callback(void* context) {
+    BadKbApp* bad_kb = context;
+
+    view_dispatcher_send_custom_event(bad_kb->view_dispatcher, BadKbAppCustomEventTextInputDone);
+}
+
+void bad_kb_scene_config_usb_name_on_enter(void* context) {
+    BadKbApp* bad_kb = context;
+    TextInput* text_input = bad_kb->text_input;
+
+    if(scene_manager_get_scene_state(bad_kb->scene_manager, BadKbSceneConfigUsbName)) {
+        strlcpy(bad_kb->usb_name_buf, bad_kb->config.usb.manuf, sizeof(bad_kb->usb_name_buf));
+        text_input_set_header_text(text_input, "Set USB manufacturer name");
+    } else {
+        strlcpy(bad_kb->usb_name_buf, bad_kb->config.usb.product, sizeof(bad_kb->usb_name_buf));
+        text_input_set_header_text(text_input, "Set USB product name");
+    }
+
+    text_input_set_result_callback(
+        text_input,
+        bad_kb_scene_config_usb_name_text_input_callback,
+        bad_kb,
+        bad_kb->usb_name_buf,
+        sizeof(bad_kb->usb_name_buf),
+        true);
+
+    view_dispatcher_switch_to_view(bad_kb->view_dispatcher, BadKbAppViewTextInput);
+}
+
+bool bad_kb_scene_config_usb_name_on_event(void* context, SceneManagerEvent event) {
+    BadKbApp* bad_kb = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        consumed = true;
+        if(event.event == BadKbAppCustomEventTextInputDone) {
+            if(scene_manager_get_scene_state(bad_kb->scene_manager, BadKbSceneConfigUsbName)) {
+                strlcpy(
+                    bad_kb->config.usb.manuf,
+                    bad_kb->usb_name_buf,
+                    sizeof(bad_kb->config.usb.product));
+            } else {
+                strlcpy(
+                    bad_kb->config.usb.product,
+                    bad_kb->usb_name_buf,
+                    sizeof(bad_kb->config.usb.product));
+            }
+            bad_kb_config_refresh(bad_kb);
+        }
+        scene_manager_previous_scene(bad_kb->scene_manager);
+    }
+    return consumed;
+}
+
+void bad_kb_scene_config_usb_name_on_exit(void* context) {
+    BadKbApp* bad_kb = context;
+    TextInput* text_input = bad_kb->text_input;
+
+    text_input_reset(text_input);
+}

+ 50 - 0
base_pack/bad_kb/scenes/bad_kb_scene_config_usb_vidpid.c

@@ -0,0 +1,50 @@
+#include "../bad_kb_app_i.h"
+
+void bad_kb_scene_config_usb_vidpid_byte_input_callback(void* context) {
+    BadKbApp* bad_kb = context;
+
+    view_dispatcher_send_custom_event(bad_kb->view_dispatcher, BadKbAppCustomEventByteInputDone);
+}
+
+void bad_kb_scene_config_usb_vidpid_on_enter(void* context) {
+    BadKbApp* bad_kb = context;
+    ByteInput* byte_input = bad_kb->byte_input;
+
+    bad_kb->usb_vidpid_buf[0] = __REVSH(bad_kb->config.usb.vid);
+    bad_kb->usb_vidpid_buf[1] = __REVSH(bad_kb->config.usb.pid);
+    byte_input_set_header_text(byte_input, "Set USB VID:PID");
+
+    byte_input_set_result_callback(
+        byte_input,
+        bad_kb_scene_config_usb_vidpid_byte_input_callback,
+        NULL,
+        bad_kb,
+        (void*)bad_kb->usb_vidpid_buf,
+        sizeof(bad_kb->usb_vidpid_buf));
+
+    view_dispatcher_switch_to_view(bad_kb->view_dispatcher, BadKbAppViewByteInput);
+}
+
+bool bad_kb_scene_config_usb_vidpid_on_event(void* context, SceneManagerEvent event) {
+    BadKbApp* bad_kb = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        consumed = true;
+        if(event.event == BadKbAppCustomEventByteInputDone) {
+            bad_kb->config.usb.vid = __REVSH(bad_kb->usb_vidpid_buf[0]);
+            bad_kb->config.usb.pid = __REVSH(bad_kb->usb_vidpid_buf[1]);
+            bad_kb_config_refresh(bad_kb);
+        }
+        scene_manager_previous_scene(bad_kb->scene_manager);
+    }
+    return consumed;
+}
+
+void bad_kb_scene_config_usb_vidpid_on_exit(void* context) {
+    BadKbApp* bad_kb = context;
+    ByteInput* byte_input = bad_kb->byte_input;
+
+    byte_input_set_result_callback(byte_input, NULL, NULL, NULL, NULL, 0);
+    byte_input_set_header_text(byte_input, "");
+}

+ 49 - 0
base_pack/bad_kb/scenes/bad_kb_scene_error.c

@@ -0,0 +1,49 @@
+#include "../bad_kb_app_i.h"
+
+static void
+    bad_kb_scene_error_event_callback(GuiButtonType result, InputType type, void* context) {
+    furi_assert(context);
+    BadKbApp* app = context;
+
+    if((result == GuiButtonTypeLeft) && (type == InputTypeShort)) {
+        view_dispatcher_send_custom_event(app->view_dispatcher, BadKbCustomEventErrorBack);
+    }
+}
+
+void bad_kb_scene_error_on_enter(void* context) {
+    BadKbApp* app = context;
+
+    if(app->error == BadKbAppErrorNoFiles) {
+        widget_add_icon_element(app->widget, 0, 0, &I_SDQuestion_35x43);
+        widget_add_string_multiline_element(
+            app->widget,
+            81,
+            4,
+            AlignCenter,
+            AlignTop,
+            FontSecondary,
+            "No SD card or\napp data found.\nThis app will not\nwork without\nrequired files.");
+        widget_add_button_element(
+            app->widget, GuiButtonTypeLeft, "Back", bad_kb_scene_error_event_callback, app);
+    }
+
+    view_dispatcher_switch_to_view(app->view_dispatcher, BadKbAppViewWidget);
+}
+
+bool bad_kb_scene_error_on_event(void* context, SceneManagerEvent event) {
+    BadKbApp* app = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == BadKbCustomEventErrorBack) {
+            view_dispatcher_stop(app->view_dispatcher);
+            consumed = true;
+        }
+    }
+    return consumed;
+}
+
+void bad_kb_scene_error_on_exit(void* context) {
+    BadKbApp* app = context;
+    widget_reset(app->widget);
+}

+ 55 - 0
base_pack/bad_kb/scenes/bad_kb_scene_file_select.c

@@ -0,0 +1,55 @@
+#include "../bad_kb_app_i.h"
+#include <furi_hal_power.h>
+#include <furi_hal_usb.h>
+#include <storage/storage.h>
+
+static bool bad_kb_file_select(BadKbApp* bad_kb) {
+    furi_assert(bad_kb);
+
+    bad_kb_app_show_loading_popup(bad_kb, true);
+    Storage* storage = furi_record_open(RECORD_STORAGE);
+    storage_simply_mkdir(storage, BAD_KB_APP_BASE_FOLDER);
+    furi_record_close(RECORD_STORAGE);
+    bad_kb_app_show_loading_popup(bad_kb, false);
+
+    DialogsFileBrowserOptions browser_options;
+    dialog_file_browser_set_basic_options(
+        &browser_options, BAD_KB_APP_SCRIPT_EXTENSION, &I_badusb_10px);
+    browser_options.base_path = BAD_USB_APP_BASE_FOLDER;
+    browser_options.skip_assets = true;
+
+    // Input events and views are managed by file_browser
+    bool res = dialog_file_browser_show(
+        bad_kb->dialogs, bad_kb->file_path, bad_kb->file_path, &browser_options);
+
+    return res;
+}
+
+void bad_kb_scene_file_select_on_enter(void* context) {
+    BadKbApp* bad_kb = context;
+
+    if(bad_kb->bad_kb_script) {
+        bad_kb_script_close(bad_kb->bad_kb_script);
+        bad_kb->bad_kb_script = NULL;
+    }
+
+    if(bad_kb_file_select(bad_kb)) {
+        bad_kb->bad_kb_script =
+            bad_kb_script_open(bad_kb->file_path, bad_kb->is_bt ? bad_kb->bt : NULL, bad_kb);
+        bad_kb_script_set_keyboard_layout(bad_kb->bad_kb_script, bad_kb->keyboard_layout);
+
+        scene_manager_next_scene(bad_kb->scene_manager, BadKbSceneWork);
+    } else {
+        view_dispatcher_stop(bad_kb->view_dispatcher);
+    }
+}
+
+bool bad_kb_scene_file_select_on_event(void* context, SceneManagerEvent event) {
+    UNUSED(context);
+    UNUSED(event);
+    return false;
+}
+
+void bad_kb_scene_file_select_on_exit(void* context) {
+    UNUSED(context);
+}

+ 59 - 0
base_pack/bad_kb/scenes/bad_kb_scene_work.c

@@ -0,0 +1,59 @@
+#include "../helpers/ducky_script.h"
+#include "../bad_kb_app_i.h"
+#include "../views/bad_kb_view.h"
+#include <furi_hal.h>
+#include "toolbox/path.h"
+
+void bad_kb_scene_work_button_callback(InputKey key, void* context) {
+    furi_assert(context);
+    BadKbApp* app = context;
+    view_dispatcher_send_custom_event(app->view_dispatcher, key);
+}
+
+bool bad_kb_scene_work_on_event(void* context, SceneManagerEvent event) {
+    BadKbApp* app = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == InputKeyLeft) {
+            if(bad_kb_is_idle_state(app->bad_kb_view)) {
+                scene_manager_next_scene(app->scene_manager, BadKbSceneConfig);
+            }
+            consumed = true;
+        } else if(event.event == InputKeyOk) {
+            bad_kb_script_start_stop(app->bad_kb_script);
+            consumed = true;
+        } else if(event.event == InputKeyRight) {
+            bad_kb_script_pause_resume(app->bad_kb_script);
+            consumed = true;
+        }
+    } else if(event.type == SceneManagerEventTypeTick) {
+        bad_kb_set_state(app->bad_kb_view, bad_kb_script_get_state(app->bad_kb_script));
+    }
+    return consumed;
+}
+
+void bad_kb_scene_work_on_enter(void* context) {
+    BadKbApp* app = context;
+
+    FuriString* file_name;
+    file_name = furi_string_alloc();
+    path_extract_filename(app->file_path, file_name, true);
+    bad_kb_set_file_name(app->bad_kb_view, furi_string_get_cstr(file_name));
+    furi_string_free(file_name);
+
+    FuriString* layout;
+    layout = furi_string_alloc();
+    path_extract_filename(app->keyboard_layout, layout, true);
+    bad_kb_set_layout(app->bad_kb_view, furi_string_get_cstr(layout));
+    furi_string_free(layout);
+
+    bad_kb_set_state(app->bad_kb_view, bad_kb_script_get_state(app->bad_kb_script));
+
+    bad_kb_set_button_callback(app->bad_kb_view, bad_kb_scene_work_button_callback, app);
+    view_dispatcher_switch_to_view(app->view_dispatcher, BadKbAppViewWork);
+}
+
+void bad_kb_scene_work_on_exit(void* context) {
+    UNUSED(context);
+}

+ 299 - 0
base_pack/bad_kb/views/bad_kb_view.c

@@ -0,0 +1,299 @@
+#include "../bad_kb_app_i.h"
+#include "bad_kb_view.h"
+#include "../helpers/ducky_script.h"
+#include <toolbox/path.h>
+#include <gui/elements.h>
+#include <assets_icons.h>
+#include <bt/bt_service/bt_i.h>
+
+#define MAX_NAME_LEN 64
+
+struct BadKb {
+    View* view;
+    BadKbButtonCallback callback;
+    void* context;
+};
+
+typedef struct {
+    char file_name[MAX_NAME_LEN];
+    char layout[MAX_NAME_LEN];
+    BadKbState state;
+    bool pause_wait;
+    uint8_t anim_frame;
+} BadKbModel;
+
+static void bad_kb_draw_callback(Canvas* canvas, void* _model) {
+    BadKbModel* model = _model;
+    BadKbWorkerState state = model->state.state;
+
+    FuriString* disp_str = furi_string_alloc_set(
+        state == BadKbStateInit ? "( . . . )" :
+        model->state.is_bt      ? "(BT) " :
+                                  "(USB) ");
+    furi_string_cat_str(disp_str, model->file_name);
+    elements_string_fit_width(canvas, disp_str, 128 - 2);
+    canvas_set_font(canvas, FontSecondary);
+    canvas_draw_str(canvas, 2, 8, furi_string_get_cstr(disp_str));
+
+    if(strlen(model->layout) == 0) {
+        furi_string_set(disp_str, "(default)");
+    } else {
+        furi_string_printf(disp_str, "(%s)", model->layout);
+    }
+    if(model->state.pin) {
+        furi_string_cat_printf(disp_str, "  PIN: %ld", model->state.pin);
+    } else {
+        uint32_t e = model->state.elapsed;
+        furi_string_cat_printf(disp_str, "  %02lu:%02lu.%ld", e / 60 / 1000, e / 1000, e % 1000);
+    }
+    elements_string_fit_width(canvas, disp_str, 128 - 2);
+    canvas_draw_str(
+        canvas, 2, 8 + canvas_current_font_height(canvas), furi_string_get_cstr(disp_str));
+
+    furi_string_reset(disp_str);
+
+    canvas_draw_icon(canvas, 22, 24, &I_UsbTree_48x22);
+
+    if((state == BadKbStateIdle) || (state == BadKbStateDone) ||
+       (state == BadKbStateNotConnected)) {
+        elements_button_center(canvas, "Run");
+        elements_button_left(canvas, "Config");
+    } else if((state == BadKbStateRunning) || (state == BadKbStateDelay)) {
+        elements_button_center(canvas, "Stop");
+        if(!model->pause_wait) {
+            elements_button_right(canvas, "Pause");
+        }
+    } else if(state == BadKbStatePaused) {
+        elements_button_center(canvas, "End");
+        elements_button_right(canvas, "Resume");
+    } else if(state == BadKbStateWaitForBtn) {
+        elements_button_center(canvas, "Press to continue");
+    } else if(state == BadKbStateWillRun) {
+        elements_button_center(canvas, "Cancel");
+    }
+
+    if(state == BadKbStateNotConnected) {
+        canvas_draw_icon(canvas, 4, 26, &I_Clock_18x18);
+        canvas_set_font(canvas, FontPrimary);
+        canvas_draw_str_aligned(canvas, 127, 31, AlignRight, AlignBottom, "Connect to");
+        canvas_draw_str_aligned(canvas, 127, 43, AlignRight, AlignBottom, "a device");
+    } else if(state == BadKbStateWillRun) {
+        canvas_draw_icon(canvas, 4, 26, &I_Clock_18x18);
+        canvas_set_font(canvas, FontPrimary);
+        canvas_draw_str_aligned(canvas, 127, 31, AlignRight, AlignBottom, "Will run");
+        canvas_draw_str_aligned(canvas, 127, 43, AlignRight, AlignBottom, "on connect");
+    } else if(state == BadKbStateFileError) {
+        canvas_draw_icon(canvas, 4, 26, &I_Error_18x18);
+        canvas_set_font(canvas, FontPrimary);
+        canvas_draw_str_aligned(canvas, 127, 31, AlignRight, AlignBottom, "File");
+        canvas_draw_str_aligned(canvas, 127, 43, AlignRight, AlignBottom, "ERROR");
+    } else if(state == BadKbStateScriptError) {
+        canvas_draw_icon(canvas, 4, 26, &I_Error_18x18);
+        furi_string_printf(disp_str, "line %zu", model->state.error_line);
+        canvas_draw_str_aligned(
+            canvas, 127, 46, AlignRight, AlignBottom, furi_string_get_cstr(disp_str));
+        furi_string_set_str(disp_str, model->state.error);
+        elements_string_fit_width(canvas, disp_str, canvas_width(canvas));
+        canvas_draw_str_aligned(
+            canvas, 127, 56, AlignRight, AlignBottom, furi_string_get_cstr(disp_str));
+        canvas_set_font(canvas, FontPrimary);
+        canvas_draw_str_aligned(canvas, 127, 33, AlignRight, AlignBottom, "ERROR:");
+    } else if(state == BadKbStateIdle) {
+        canvas_draw_icon(canvas, 4, 26, &I_Smile_18x18);
+        furi_string_printf(disp_str, "0/%zu", model->state.line_nb);
+        canvas_draw_str_aligned(
+            canvas, 124, 47, AlignRight, AlignBottom, furi_string_get_cstr(disp_str));
+        canvas_set_font(canvas, FontBigNumbers);
+        canvas_draw_str_aligned(canvas, 112, 37, AlignRight, AlignBottom, "0");
+        canvas_draw_icon(canvas, 115, 23, &I_Percent_10x14);
+    } else if(state == BadKbStateRunning) {
+        if(model->anim_frame == 0) {
+            canvas_draw_icon(canvas, 4, 23, &I_EviSmile1_18x21);
+        } else {
+            canvas_draw_icon(canvas, 4, 23, &I_EviSmile2_18x21);
+        }
+        furi_string_printf(disp_str, "%zu/%zu", model->state.line_cur, model->state.line_nb);
+        canvas_draw_str_aligned(
+            canvas, 124, 47, AlignRight, AlignBottom, furi_string_get_cstr(disp_str));
+        canvas_set_font(canvas, FontBigNumbers);
+        furi_string_printf(
+            disp_str, "%zu", ((model->state.line_cur - 1) * 100) / model->state.line_nb);
+        canvas_draw_str_aligned(
+            canvas, 112, 37, AlignRight, AlignBottom, furi_string_get_cstr(disp_str));
+        canvas_draw_icon(canvas, 115, 23, &I_Percent_10x14);
+    } else if(state == BadKbStateDone) {
+        canvas_draw_icon(canvas, 4, 23, &I_EviSmile1_18x21);
+        furi_string_printf(disp_str, "%zu/%zu", model->state.line_nb, model->state.line_nb);
+        canvas_draw_str_aligned(
+            canvas, 124, 47, AlignRight, AlignBottom, furi_string_get_cstr(disp_str));
+        canvas_set_font(canvas, FontBigNumbers);
+        canvas_draw_str_aligned(canvas, 112, 37, AlignRight, AlignBottom, "100");
+        canvas_draw_icon(canvas, 115, 23, &I_Percent_10x14);
+    } else if(state == BadKbStateDelay) {
+        if(model->anim_frame == 0) {
+            canvas_draw_icon(canvas, 4, 23, &I_EviWaiting1_18x21);
+        } else {
+            canvas_draw_icon(canvas, 4, 23, &I_EviWaiting2_18x21);
+        }
+        uint32_t delay = model->state.delay_remain / 10;
+        if(delay) {
+            furi_string_printf(disp_str, "Delay %lus", delay);
+            canvas_draw_str_aligned(
+                canvas, 4, 61, AlignLeft, AlignBottom, furi_string_get_cstr(disp_str));
+        }
+        furi_string_printf(disp_str, "%zu/%zu", model->state.line_cur, model->state.line_nb);
+        canvas_draw_str_aligned(
+            canvas, 124, 47, AlignRight, AlignBottom, furi_string_get_cstr(disp_str));
+        canvas_set_font(canvas, FontBigNumbers);
+        furi_string_printf(
+            disp_str, "%zu", ((model->state.line_cur - 1) * 100) / model->state.line_nb);
+        canvas_draw_str_aligned(
+            canvas, 112, 37, AlignRight, AlignBottom, furi_string_get_cstr(disp_str));
+        canvas_draw_icon(canvas, 115, 23, &I_Percent_10x14);
+    } else if((state == BadKbStatePaused) || (state == BadKbStateWaitForBtn)) {
+        if(model->anim_frame == 0) {
+            canvas_draw_icon(canvas, 4, 23, &I_EviWaiting1_18x21);
+        } else {
+            canvas_draw_icon(canvas, 4, 23, &I_EviWaiting2_18x21);
+        }
+        if(state != BadKbStateWaitForBtn) {
+            canvas_draw_str_aligned(canvas, 4, 61, AlignLeft, AlignBottom, "Paused");
+        }
+        furi_string_printf(disp_str, "%zu/%zu", model->state.line_cur, model->state.line_nb);
+        canvas_draw_str_aligned(
+            canvas, 124, 47, AlignRight, AlignBottom, furi_string_get_cstr(disp_str));
+        canvas_set_font(canvas, FontBigNumbers);
+        furi_string_printf(
+            disp_str, "%zu", ((model->state.line_cur - 1) * 100) / model->state.line_nb);
+        canvas_draw_str_aligned(
+            canvas, 112, 37, AlignRight, AlignBottom, furi_string_get_cstr(disp_str));
+        canvas_draw_icon(canvas, 115, 23, &I_Percent_10x14);
+    } else {
+        canvas_draw_icon(canvas, 4, 26, &I_Clock_18x18);
+    }
+
+    furi_string_free(disp_str);
+}
+
+static bool bad_kb_input_callback(InputEvent* event, void* context) {
+    furi_assert(context);
+    BadKb* bad_kb = context;
+    bool consumed = false;
+
+    if(event->type == InputTypeShort) {
+        if(event->key == InputKeyLeft) {
+            consumed = true;
+            furi_assert(bad_kb->callback);
+            bad_kb->callback(event->key, bad_kb->context);
+        } else if(event->key == InputKeyOk) {
+            with_view_model(
+                bad_kb->view, BadKbModel * model, { model->pause_wait = false; }, true);
+            consumed = true;
+            furi_assert(bad_kb->callback);
+            bad_kb->callback(event->key, bad_kb->context);
+        } else if(event->key == InputKeyRight) {
+            with_view_model(
+                bad_kb->view,
+                BadKbModel * model,
+                {
+                    if((model->state.state == BadKbStateRunning) ||
+                       (model->state.state == BadKbStateDelay)) {
+                        model->pause_wait = true;
+                    }
+                },
+                true);
+            consumed = true;
+            furi_assert(bad_kb->callback);
+            bad_kb->callback(event->key, bad_kb->context);
+        }
+    }
+
+    return consumed;
+}
+
+BadKb* bad_kb_alloc() {
+    BadKb* bad_kb = malloc(sizeof(BadKb));
+
+    bad_kb->view = view_alloc();
+    view_allocate_model(bad_kb->view, ViewModelTypeLocking, sizeof(BadKbModel));
+    view_set_context(bad_kb->view, bad_kb);
+    view_set_draw_callback(bad_kb->view, bad_kb_draw_callback);
+    view_set_input_callback(bad_kb->view, bad_kb_input_callback);
+
+    return bad_kb;
+}
+
+void bad_kb_free(BadKb* bad_kb) {
+    furi_assert(bad_kb);
+    view_free(bad_kb->view);
+    free(bad_kb);
+}
+
+View* bad_kb_get_view(BadKb* bad_kb) {
+    furi_assert(bad_kb);
+    return bad_kb->view;
+}
+
+void bad_kb_set_button_callback(BadKb* bad_kb, BadKbButtonCallback callback, void* context) {
+    furi_assert(bad_kb);
+    furi_assert(callback);
+    with_view_model(
+        bad_kb->view,
+        BadKbModel * model,
+        {
+            UNUSED(model);
+            bad_kb->callback = callback;
+            bad_kb->context = context;
+        },
+        true);
+}
+
+void bad_kb_set_file_name(BadKb* bad_kb, const char* name) {
+    furi_assert(name);
+    with_view_model(
+        bad_kb->view, BadKbModel * model, { strlcpy(model->file_name, name, MAX_NAME_LEN); }, true);
+}
+
+void bad_kb_set_layout(BadKb* bad_kb, const char* layout) {
+    furi_assert(layout);
+    with_view_model(
+        bad_kb->view, BadKbModel * model, { strlcpy(model->layout, layout, MAX_NAME_LEN); }, true);
+}
+
+void bad_kb_set_state(BadKb* bad_kb, BadKbState* st) {
+    furi_assert(st);
+    uint32_t pin = 0;
+    if(bad_kb->context != NULL) {
+        BadKbApp* app = bad_kb->context;
+        if(app->bt != NULL) {
+            pin = app->bt->pin;
+        }
+    }
+    st->pin = pin;
+    with_view_model(
+        bad_kb->view,
+        BadKbModel * model,
+        {
+            memcpy(&(model->state), st, sizeof(BadKbState));
+            model->anim_frame ^= 1;
+            if(model->state.state == BadKbStatePaused) {
+                model->pause_wait = false;
+            }
+        },
+        true);
+}
+
+bool bad_kb_is_idle_state(BadKb* bad_kb) {
+    bool is_idle = false;
+    with_view_model(
+        bad_kb->view,
+        BadKbModel * model,
+        {
+            if((model->state.state == BadKbStateIdle) || (model->state.state == BadKbStateDone) ||
+               (model->state.state == BadKbStateNotConnected)) {
+                is_idle = true;
+            }
+        },
+        false);
+    return is_idle;
+}

+ 23 - 0
base_pack/bad_kb/views/bad_kb_view.h

@@ -0,0 +1,23 @@
+#pragma once
+
+#include <gui/view.h>
+#include "../helpers/ducky_script.h"
+
+typedef struct BadKb BadKb;
+typedef void (*BadKbButtonCallback)(InputKey key, void* context);
+
+BadKb* bad_kb_alloc();
+
+void bad_kb_free(BadKb* bad_kb);
+
+View* bad_kb_get_view(BadKb* bad_kb);
+
+void bad_kb_set_button_callback(BadKb* bad_kb, BadKbButtonCallback callback, void* context);
+
+void bad_kb_set_file_name(BadKb* bad_kb, const char* name);
+
+void bad_kb_set_layout(BadKb* bad_kb, const char* layout);
+
+void bad_kb_set_state(BadKb* bad_kb, BadKbState* st);
+
+bool bad_kb_is_idle_state(BadKb* bad_kb);