Add BLE HID Host app; share the central layer via libble/

Extract the reusable BLE central/GATT client and name/value decoders into
libble/ (moved out of bt_inspector). build.sh now stages libble/*.{c,h}
into each app dir at build time and builds every app in APPS=(...).

New app ble_hid_host: connects to a BLE keyboard or mouse, discovers the
HID service (0x1812), sets protocol mode (boot when offered, else report),
subscribes to the input-report characteristics, and decodes keyboard
(modifiers + key names) and mouse (buttons + dx/dy/wheel) reports live.
Events scroll on screen and are logged with raw bytes to
apps_data/ble_hid_host/. No pairing, so encryption-gated keyboards stay
silent; open devices and most mice work. Both faps build and pass APPCHK.
This commit is contained in:
2026-09-11 13:14:57 +01:00
parent 4b887b1ffb
commit b1efa9cd4e
11 changed files with 781 additions and 17 deletions
+16
View File
@@ -0,0 +1,16 @@
# External app. Needs firmware built by ../build.sh: full BLE stack plus the
# ST aci_*/hci_* command API exported to apps.
App(
appid="ble_hid_host",
name="BLE HID Host",
apptype=FlipperAppType.EXTERNAL,
entry_point="ble_hid_host_app",
requires=["gui", "storage"],
stack_size=4 * 1024,
fap_category="Bluetooth",
fap_icon="icon.png",
fap_version="1.0",
fap_description="Connect a BLE keyboard or mouse and watch its input reports",
fap_author="andydixon",
fap_weburl="https://github.com/andydixon/flipper-bt-inspector",
)
+638
View File
@@ -0,0 +1,638 @@
// BLE HID Host: connect to a Bluetooth Low Energy keyboard or mouse and show
// its input reports live. Reuses the libble central/GATT layer. Needs the
// full BLE stack + exported ST command API (see ../build.sh).
#include <furi.h>
#include <furi_hal.h>
#include <gui/gui.h>
#include <gui/view.h>
#include <gui/view_dispatcher.h>
#include <gui/elements.h>
#include <gui/modules/popup.h>
#include <storage/storage.h>
#include <ble/core/ble_defs.h>
#include <string.h>
#include <stdarg.h>
#include "ble_central.h"
#include "ble_names.h"
#include "hid_decode.h"
#define TAG "BleHidHost"
#define LOG_DIR EXT_PATH("apps_data/ble_hid_host")
#define ROWS 3
#define EVENTS_MAX 32
#define SUBS_MAX 8
#define HID_NOTIFY_MAX 32
#define UUID_HID 0x1812
#define UUID_PROTO_MODE 0x2A4E
#define UUID_REPORT 0x2A4D
#define UUID_BOOT_KBD 0x2A22
#define UUID_BOOT_MOUSE 0x2A33
#define UUID_CTRL_POINT 0x2A4C
typedef enum { ViewScan, ViewMonitor, ViewPopup } ViewId;
typedef enum {
EvtTick,
EvtSelect,
EvtNotify,
EvtJobDone,
EvtPopupDone,
} AppEvent;
typedef enum { JobConnect, JobDisconnect, JobQuit } JobType;
typedef enum { KindAuto, KindKbd, KindMouse } ReportKind;
typedef struct {
BcDevice devs[BC_MAX_DEVICES];
size_t count, sel, top;
bool scanning;
} ScanModel;
typedef struct {
char lines[EVENTS_MAX][40];
int count; // total ever, index with % EVENTS_MAX
char header[40];
char status[40];
size_t scroll;
} MonModel;
typedef struct {
uint16_t handle;
ReportKind kind;
} Sub;
typedef struct {
Gui* gui;
ViewDispatcher* vd;
View* scan_view;
View* mon_view;
Popup* popup;
FuriTimer* timer;
Storage* storage;
File* log;
BleCentral* bc;
FuriThread* worker;
FuriMessageQueue* jobs;
FuriMessageQueue* notify_q;
ViewId cur_view;
BcDevice dev;
BcService services[BC_MAX_SERVICES];
BcChar chars[BC_MAX_CHARS];
Sub subs[SUBS_MAX];
int sub_count;
bool connected_ok;
char setup_msg[48];
} App;
typedef struct {
uint16_t handle;
uint8_t len;
uint8_t data[HID_NOTIFY_MAX];
} NotifyMsg;
// ---- logging ----------------------------------------------------------------
static void app_log(App* app, const char* fmt, ...) {
if(!app->log) return;
DateTime dt;
furi_hal_rtc_get_datetime(&dt);
FuriString* s = furi_string_alloc_printf(
"[%04u-%02u-%02u %02u:%02u:%02u] ", dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second);
va_list va;
va_start(va, fmt);
furi_string_cat_vprintf(s, fmt, va);
va_end(va);
furi_string_cat_str(s, "\n");
storage_file_write(app->log, furi_string_get_cstr(s), furi_string_size(s));
storage_file_sync(app->log);
furi_string_free(s);
}
static void log_open(App* app) {
DateTime dt;
furi_hal_rtc_get_datetime(&dt);
storage_simply_mkdir(app->storage, LOG_DIR);
FuriString* p = furi_string_alloc_printf(
LOG_DIR "/hid_%04u%02u%02u_%02u%02u%02u.log", dt.year, dt.month, dt.day, dt.hour, dt.minute,
dt.second);
app->log = storage_file_alloc(app->storage);
if(!storage_file_open(app->log, furi_string_get_cstr(p), FSAM_WRITE, FSOM_CREATE_ALWAYS)) {
storage_file_free(app->log);
app->log = NULL;
}
furi_string_free(p);
}
// ---- monitor event feed -----------------------------------------------------
static void mon_add(App* app, const char* text) {
with_view_model(
app->mon_view,
MonModel * m,
{
strncpy(m->lines[m->count % EVENTS_MAX], text, sizeof(m->lines[0]) - 1);
m->lines[m->count % EVENTS_MAX][sizeof(m->lines[0]) - 1] = 0;
m->count++;
m->scroll = 0; // jump to newest
},
app->cur_view == ViewMonitor);
}
static void mon_set_status(App* app, const char* header, const char* status) {
with_view_model(
app->mon_view,
MonModel * m,
{
if(header) strncpy(m->header, header, sizeof(m->header) - 1);
if(status) strncpy(m->status, status, sizeof(m->status) - 1);
},
app->cur_view == ViewMonitor);
}
static const char* dev_label(const BcDevice* d) {
return d->name[0] ? d->name : "(no name)";
}
// ---- HID setup (worker thread) ----------------------------------------------
static uint16_t char_end_handle(App* app, int svc, int idx, int nchars) {
if(idx + 1 < nchars) return app->chars[idx + 1].decl_handle - 1;
return app->services[svc].end;
}
static uint16_t uuid16(const BcChar* c) {
return c->uuid_len == 2 ? (c->uuid[0] | (c->uuid[1] << 8)) : 0;
}
static bool hid_setup(App* app) {
app->sub_count = 0;
int nsvc = bc_discover_services(app->bc, app->services, BC_MAX_SERVICES);
if(nsvc < 0) {
snprintf(app->setup_msg, sizeof(app->setup_msg), "Service discovery failed");
return false;
}
int hid = -1;
for(int i = 0; i < nsvc; i++)
if(app->services[i].uuid_len == 2 &&
(app->services[i].uuid[0] | (app->services[i].uuid[1] << 8)) == UUID_HID)
hid = i;
if(hid < 0) {
snprintf(app->setup_msg, sizeof(app->setup_msg), "No HID service (0x1812)");
return false;
}
int nch = bc_discover_chars(app->bc, &app->services[hid], app->chars, BC_MAX_CHARS);
if(nch < 0) {
snprintf(app->setup_msg, sizeof(app->setup_msg), "Char discovery failed");
return false;
}
bool boot_available = false;
int proto_idx = -1, ctrl_idx = -1;
for(int i = 0; i < nch; i++) {
uint16_t u = uuid16(&app->chars[i]);
if(u == UUID_BOOT_KBD || u == UUID_BOOT_MOUSE) boot_available = true;
if(u == UUID_PROTO_MODE) proto_idx = i;
if(u == UUID_CTRL_POINT) ctrl_idx = i;
}
// Protocol mode: prefer boot (fixed report layout) when the device offers it.
if(proto_idx >= 0) {
uint8_t mode = boot_available ? 0 : 1;
bc_write(app->bc, app->chars[proto_idx].value_handle, &mode, 1, false);
app_log(app, "SET protocol mode = %s", mode ? "report" : "boot");
}
// Keep the device out of suspend.
if(ctrl_idx >= 0) {
uint8_t exit_suspend = 1;
bc_write(app->bc, app->chars[ctrl_idx].value_handle, &exit_suspend, 1, false);
}
for(int i = 0; i < nch && app->sub_count < SUBS_MAX; i++) {
BcChar* c = &app->chars[i];
uint16_t u = uuid16(c);
bool is_input = (u == UUID_BOOT_KBD || u == UUID_BOOT_MOUSE || u == UUID_REPORT);
bool boot = (u == UUID_BOOT_KBD || u == UUID_BOOT_MOUSE);
if(!is_input || !(c->props & (CHAR_PROP_NOTIFY | CHAR_PROP_INDICATE))) continue;
if(boot_available && u == UUID_REPORT) continue; // using boot chars instead
if(!boot_available && boot) continue;
uint16_t end = char_end_handle(app, hid, i, nch);
if(bc_set_notify(app->bc, c, end, true)) {
ReportKind k = u == UUID_BOOT_KBD ? KindKbd : u == UUID_BOOT_MOUSE ? KindMouse : KindAuto;
app->subs[app->sub_count++] = (Sub){.handle = c->value_handle, .kind = k};
app_log(app, "SUBSCRIBE handle 0x%04X uuid 0x%04X", c->value_handle, u);
}
}
if(!app->sub_count) {
snprintf(app->setup_msg, sizeof(app->setup_msg), "HID service has no input reports");
return false;
}
snprintf(
app->setup_msg, sizeof(app->setup_msg), "%s mode, %d report(s)",
boot_available ? "boot" : "report", app->sub_count);
return true;
}
static int32_t worker_thread(void* ctx) {
App* app = ctx;
JobType job;
while(furi_message_queue_get(app->jobs, &job, FuriWaitForever) == FuriStatusOk) {
if(job == JobQuit) break;
if(job == JobConnect) {
app->connected_ok = bc_connect(app->bc, &app->dev, 8000) && hid_setup(app);
} else if(job == JobDisconnect) {
bc_disconnect(app->bc);
bc_scan_start(app->bc);
}
view_dispatcher_send_custom_event(app->vd, EvtJobDone);
}
return 0;
}
static void notify_cb(uint16_t handle, const uint8_t* data, uint8_t len, void* ctx) {
App* app = ctx;
NotifyMsg m = {.handle = handle, .len = len > HID_NOTIFY_MAX ? HID_NOTIFY_MAX : len};
memcpy(m.data, data, m.len);
if(furi_message_queue_put(app->notify_q, &m, 0) == FuriStatusOk)
view_dispatcher_send_custom_event(app->vd, EvtNotify);
}
// ---- report decoding --------------------------------------------------------
static ReportKind kind_for(App* app, uint16_t handle, uint8_t len) {
for(int i = 0; i < app->sub_count; i++)
if(app->subs[i].handle == handle) {
if(app->subs[i].kind != KindAuto) return app->subs[i].kind;
break;
}
return len >= 8 ? KindKbd : KindMouse; // boot-report heuristic for report-mode inputs
}
static void decode_kbd(FuriString* out, const uint8_t* d, uint8_t len) {
if(len < 3) {
furi_string_cat_str(out, "(empty)");
return;
}
hid_mods_str(out, d[0]);
bool any = false;
for(uint8_t i = 2; i < len && i < 8; i++) {
if(!d[i]) continue;
const char* n = hid_key_name(d[i]);
if(n)
furi_string_cat_printf(out, "%s ", n);
else
furi_string_cat_printf(out, "[%02X] ", d[i]);
any = true;
}
if(!any && d[0] == 0) furi_string_cat_str(out, "(release)");
}
static void decode_mouse(FuriString* out, const uint8_t* d, uint8_t len) {
if(len < 1) return;
uint8_t b = d[0];
furi_string_cat_printf(
out, "%s%s%s", (b & 1) ? "L" : "-", (b & 2) ? "R" : "-", (b & 4) ? "M" : "-");
if(len >= 3) furi_string_cat_printf(out, " dx%+d dy%+d", (int8_t)d[1], (int8_t)d[2]);
if(len >= 4 && d[3]) furi_string_cat_printf(out, " whl%+d", (int8_t)d[3]);
}
static void on_notify(App* app) {
NotifyMsg m;
while(furi_message_queue_get(app->notify_q, &m, 0) == FuriStatusOk) {
FuriString* line = furi_string_alloc();
FuriString* raw = furi_string_alloc();
ble_hex(raw, m.data, m.len);
if(kind_for(app, m.handle, m.len) == KindKbd)
decode_kbd(line, m.data, m.len);
else
decode_mouse(line, m.data, m.len);
app_log(app, "REPORT 0x%04X %s | %s", m.handle, furi_string_get_cstr(line), furi_string_get_cstr(raw));
mon_add(app, furi_string_get_cstr(line));
furi_string_free(raw);
furi_string_free(line);
}
}
// ---- scan view --------------------------------------------------------------
static const char* vendor_of(const BcDevice* d) {
uint8_t l = 0;
const uint8_t* mm = bc_adv_find(d->adv, d->adv_len, 0xFF, &l);
if(!mm) mm = bc_adv_find(d->rsp, d->rsp_len, 0xFF, &l);
if(!mm || l < 2) return NULL;
return ble_company_name(mm[0] | (mm[1] << 8));
}
static void scan_draw(Canvas* c, void* m_) {
ScanModel* m = m_;
FuriString* s = furi_string_alloc();
canvas_clear(c);
canvas_set_font(c, FontPrimary);
canvas_draw_str(c, 2, 9, "BLE HID Host");
canvas_set_font(c, FontSecondary);
furi_string_printf(s, "%u dev", (unsigned)m->count);
canvas_draw_str_aligned(c, 126, 9, AlignRight, AlignBottom, furi_string_get_cstr(s));
canvas_draw_line(c, 0, 11, 127, 11);
if(!m->count) canvas_draw_str_aligned(c, 64, 40, AlignCenter, AlignCenter, "Scanning...");
for(size_t r = 0; r < ROWS && m->top + r < m->count; r++) {
const BcDevice* d = &m->devs[m->top + r];
int y = 13 + r * 17;
bool sel = (m->top + r == m->sel);
if(sel) {
canvas_draw_rbox(c, 0, y, 122, 17, 2);
canvas_set_color(c, ColorWhite);
}
canvas_set_font(c, FontPrimary);
furi_string_set_str(s, dev_label(d));
elements_string_fit_width(c, s, 90);
canvas_draw_str(c, 3, y + 8, furi_string_get_cstr(s));
canvas_set_font(c, FontSecondary);
furi_string_printf(s, "%d", d->rssi);
canvas_draw_str_aligned(c, 119, y + 8, AlignRight, AlignBottom, furi_string_get_cstr(s));
furi_string_reset(s);
ble_addr_str(s, d->addr);
const char* v = vendor_of(d);
if(v) furi_string_cat_printf(s, " %s", v);
elements_string_fit_width(c, s, 116);
canvas_draw_str(c, 3, y + 16, furi_string_get_cstr(s));
if(sel) canvas_set_color(c, ColorBlack);
}
if(m->count > ROWS) elements_scrollbar_pos(c, 125, 13, 51, m->sel, m->count);
furi_string_free(s);
}
static bool scan_input(InputEvent* e, void* ctx) {
App* app = ctx;
if(e->type != InputTypeShort && e->type != InputTypeRepeat) return false;
if(e->key == InputKeyOk && e->type == InputTypeShort) {
view_dispatcher_send_custom_event(app->vd, EvtSelect);
return true;
}
if(e->key != InputKeyUp && e->key != InputKeyDown) return false;
with_view_model(
app->scan_view,
ScanModel * m,
{
if(e->key == InputKeyUp && m->sel > 0) m->sel--;
if(e->key == InputKeyDown && m->sel + 1 < m->count) m->sel++;
if(m->sel < m->top) m->top = m->sel;
if(m->sel >= m->top + ROWS) m->top = m->sel - ROWS + 1;
},
true);
return true;
}
static void scan_enter(void* ctx) {
App* app = ctx;
app->cur_view = ViewScan;
bool ok = bc_scan_start(app->bc);
with_view_model(app->scan_view, ScanModel * m, { m->scanning = ok; }, true);
}
// ---- monitor view -----------------------------------------------------------
static void mon_draw(Canvas* c, void* m_) {
MonModel* m = m_;
canvas_clear(c);
canvas_set_font(c, FontPrimary);
canvas_draw_str(c, 2, 9, m->header[0] ? m->header : "HID");
canvas_set_font(c, FontSecondary);
canvas_draw_line(c, 0, 11, 127, 11);
canvas_draw_str(c, 2, 21, m->status);
int total = m->count;
int visible = 4; // event lines
int shown = total < visible ? total : visible;
for(int r = 0; r < shown; r++) {
// newest at bottom; scroll shifts the window back into history
int idx = total - 1 - (int)m->scroll - (shown - 1 - r);
if(idx < 0 || idx < total - EVENTS_MAX) continue;
canvas_draw_str(c, 2, 33 + r * 10, m->lines[idx % EVENTS_MAX]);
}
if(total > visible) elements_scrollbar_pos(c, 126, 23, 41, m->scroll, total);
}
static bool mon_input(InputEvent* e, void* ctx) {
App* app = ctx;
if(e->type != InputTypeShort && e->type != InputTypeRepeat) return false;
if(e->key != InputKeyUp && e->key != InputKeyDown) return false;
with_view_model(
app->mon_view,
MonModel * m,
{
int maxs = m->count > 4 ? m->count - 4 : 0;
if(e->key == InputKeyDown && (int)m->scroll < maxs) m->scroll++; // into history
if(e->key == InputKeyUp && m->scroll > 0) m->scroll--;
},
true);
return true;
}
static uint32_t nav_exit(void* ctx) {
UNUSED(ctx);
return VIEW_NONE;
}
// Back from the monitor: disconnect and return to scanning.
static uint32_t nav_mon_back(void* ctx) {
App* app = ctx;
furi_message_queue_put(app->jobs, &(JobType){JobDisconnect}, 0);
app->cur_view = ViewScan;
return ViewScan;
}
static uint32_t nav_stay_popup(void* ctx) {
UNUSED(ctx);
return ViewPopup;
}
// ---- events -----------------------------------------------------------------
static void popup_cb(void* ctx) {
App* app = ctx;
view_dispatcher_send_custom_event(app->vd, EvtPopupDone);
}
static void show_popup(App* app, const char* header, const char* text, bool timeout) {
popup_reset(app->popup);
popup_set_callback(app->popup, popup_cb);
popup_set_context(app->popup, app);
popup_set_header(app->popup, header, 64, 18, AlignCenter, AlignCenter);
popup_set_text(app->popup, text, 64, 40, AlignCenter, AlignCenter);
if(timeout) {
popup_set_timeout(app->popup, 1800);
popup_enable_timeout(app->popup);
} else {
popup_disable_timeout(app->popup);
}
app->cur_view = ViewPopup;
view_dispatcher_switch_to_view(app->vd, ViewPopup);
}
static void on_tick(App* app) {
if(app->cur_view != ViewScan) return;
with_view_model(
app->scan_view,
ScanModel * m,
{
uint8_t sel_addr[6];
bool had = m->count > 0;
if(had) memcpy(sel_addr, m->devs[m->sel].addr, 6);
m->count = bc_snapshot(app->bc, m->devs, BC_MAX_DEVICES);
if(had)
for(size_t i = 0; i < m->count; i++)
if(memcmp(m->devs[i].addr, sel_addr, 6) == 0) m->sel = i;
if(m->sel >= m->count) m->sel = m->count ? m->count - 1 : 0;
if(m->sel < m->top) m->top = m->sel;
if(m->sel >= m->top + ROWS) m->top = m->sel - ROWS + 1;
},
true);
}
static bool custom_event_cb(void* ctx, uint32_t event) {
App* app = ctx;
switch(event) {
case EvtTick: on_tick(app); break;
case EvtSelect: {
bool have = false;
with_view_model(
app->scan_view,
ScanModel * m,
{
if(m->count) {
app->dev = m->devs[m->sel];
have = true;
}
},
false);
if(have) {
app_log(app, "CONNECTING %s", dev_label(&app->dev));
show_popup(app, "Connecting...", dev_label(&app->dev), false);
furi_message_queue_put(app->jobs, &(JobType){JobConnect}, 0);
}
} break;
case EvtNotify: on_notify(app); break;
case EvtJobDone:
if(app->connected_ok) {
char hdr[40];
snprintf(hdr, sizeof(hdr), "%s", dev_label(&app->dev));
with_view_model(app->mon_view, MonModel * m, { m->count = 0; m->scroll = 0; }, false);
mon_set_status(app, hdr, app->setup_msg);
app_log(app, "CONNECTED %s: %s", dev_label(&app->dev), app->setup_msg);
app->cur_view = ViewMonitor;
view_dispatcher_switch_to_view(app->vd, ViewMonitor);
} else if(bc_is_connected(app->bc)) {
// connected but HID setup failed: report and drop
app_log(app, "SETUP FAILED %s: %s", dev_label(&app->dev), app->setup_msg);
bc_disconnect(app->bc);
show_popup(app, "Not a HID device", app->setup_msg, true);
} else {
const char* n = ble_att_error_name(bc_last_error(app->bc));
app_log(app, "CONNECT FAILED %s", dev_label(&app->dev));
show_popup(app, "Connect failed", n ? n : "timeout", true);
}
break;
case EvtPopupDone:
bc_scan_start(app->bc);
app->cur_view = ViewScan;
view_dispatcher_switch_to_view(app->vd, ViewScan);
break;
default: return false;
}
return true;
}
static void timer_cb(void* ctx) {
App* app = ctx;
view_dispatcher_send_custom_event(app->vd, EvtTick);
}
// ---- setup / teardown -------------------------------------------------------
int32_t ble_hid_host_app(void* p) {
UNUSED(p);
App* app = malloc(sizeof(App));
memset(app, 0, sizeof(App));
app->gui = furi_record_open(RECORD_GUI);
app->storage = furi_record_open(RECORD_STORAGE);
app->jobs = furi_message_queue_alloc(8, sizeof(JobType));
app->notify_q = furi_message_queue_alloc(16, sizeof(NotifyMsg));
app->vd = view_dispatcher_alloc();
view_dispatcher_set_event_callback_context(app->vd, app);
view_dispatcher_set_custom_event_callback(app->vd, custom_event_cb);
view_dispatcher_attach_to_gui(app->vd, app->gui, ViewDispatcherTypeFullscreen);
app->scan_view = view_alloc();
view_allocate_model(app->scan_view, ViewModelTypeLocking, sizeof(ScanModel));
view_set_context(app->scan_view, app);
view_set_draw_callback(app->scan_view, scan_draw);
view_set_input_callback(app->scan_view, scan_input);
view_set_enter_callback(app->scan_view, scan_enter);
view_set_previous_callback(app->scan_view, nav_exit);
view_dispatcher_add_view(app->vd, ViewScan, app->scan_view);
app->mon_view = view_alloc();
view_allocate_model(app->mon_view, ViewModelTypeLocking, sizeof(MonModel));
view_set_context(app->mon_view, app);
view_set_draw_callback(app->mon_view, mon_draw);
view_set_input_callback(app->mon_view, mon_input);
view_set_previous_callback(app->mon_view, nav_mon_back);
view_dispatcher_add_view(app->vd, ViewMonitor, app->mon_view);
app->popup = popup_alloc();
view_set_previous_callback(popup_get_view(app->popup), nav_stay_popup);
view_dispatcher_add_view(app->vd, ViewPopup, popup_get_view(app->popup));
app->timer = furi_timer_alloc(timer_cb, FuriTimerTypePeriodic, app);
if(!bc_supported()) {
show_popup(
app, "Full BLE stack needed",
"This firmware runs BLE Light. Build with ble_full (see build.sh).", false);
view_set_previous_callback(popup_get_view(app->popup), nav_exit);
view_dispatcher_run(app->vd);
goto cleanup;
}
log_open(app);
bool was_adv = furi_hal_bt_is_active();
furi_hal_bt_stop_advertising();
furi_delay_ms(100);
app->bc = bc_alloc(notify_cb, app);
app_log(app, "START");
app->worker = furi_thread_alloc_ex("HidHostWorker", 3072, worker_thread, app);
furi_thread_start(app->worker);
furi_timer_start(app->timer, furi_ms_to_ticks(300));
view_dispatcher_switch_to_view(app->vd, ViewScan);
view_dispatcher_run(app->vd);
furi_timer_stop(app->timer);
furi_message_queue_put(app->jobs, &(JobType){JobQuit}, 0);
furi_thread_join(app->worker);
furi_thread_free(app->worker);
bc_free(app->bc);
if(was_adv) furi_hal_bt_start_advertising();
app_log(app, "STOP");
if(app->log) {
storage_file_close(app->log);
storage_file_free(app->log);
}
cleanup:
furi_timer_free(app->timer);
view_dispatcher_remove_view(app->vd, ViewScan);
view_dispatcher_remove_view(app->vd, ViewMonitor);
view_dispatcher_remove_view(app->vd, ViewPopup);
view_free(app->scan_view);
view_free(app->mon_view);
popup_free(app->popup);
view_dispatcher_free(app->vd);
furi_message_queue_free(app->jobs);
furi_message_queue_free(app->notify_q);
furi_record_close(RECORD_STORAGE);
furi_record_close(RECORD_GUI);
free(app);
return 0;
}
+74
View File
@@ -0,0 +1,74 @@
#include "hid_decode.h"
// USB HID Usage Table (keyboard/keypad page 0x07), common subset.
const char* hid_key_name(uint8_t u) {
static char one[2];
if(u >= 0x04 && u <= 0x1D) { // a-z
one[0] = 'a' + (u - 0x04);
one[1] = 0;
return one;
}
if(u >= 0x1E && u <= 0x26) { // 1-9
one[0] = '1' + (u - 0x1E);
one[1] = 0;
return one;
}
switch(u) {
case 0x27: return "0";
case 0x28: return "Enter";
case 0x29: return "Esc";
case 0x2A: return "Bksp";
case 0x2B: return "Tab";
case 0x2C: return "Space";
case 0x2D: return "-";
case 0x2E: return "=";
case 0x2F: return "[";
case 0x30: return "]";
case 0x31: return "\\";
case 0x33: return ";";
case 0x34: return "'";
case 0x35: return "`";
case 0x36: return ",";
case 0x37: return ".";
case 0x38: return "/";
case 0x39: return "Caps";
case 0x3A: return "F1";
case 0x3B: return "F2";
case 0x3C: return "F3";
case 0x3D: return "F4";
case 0x3E: return "F5";
case 0x3F: return "F6";
case 0x40: return "F7";
case 0x41: return "F8";
case 0x42: return "F9";
case 0x43: return "F10";
case 0x44: return "F11";
case 0x45: return "F12";
case 0x46: return "PrtSc";
case 0x47: return "ScrLk";
case 0x48: return "Pause";
case 0x49: return "Ins";
case 0x4A: return "Home";
case 0x4B: return "PgUp";
case 0x4C: return "Del";
case 0x4D: return "End";
case 0x4E: return "PgDn";
case 0x4F: return "Right";
case 0x50: return "Left";
case 0x51: return "Down";
case 0x52: return "Up";
case 0x53: return "NumLk";
default: return NULL;
}
}
void hid_mods_str(FuriString* out, uint8_t m) {
if(m & 0x01) furi_string_cat_str(out, "LCtrl+");
if(m & 0x02) furi_string_cat_str(out, "LShift+");
if(m & 0x04) furi_string_cat_str(out, "LAlt+");
if(m & 0x08) furi_string_cat_str(out, "LGui+");
if(m & 0x10) furi_string_cat_str(out, "RCtrl+");
if(m & 0x20) furi_string_cat_str(out, "RShift+");
if(m & 0x40) furi_string_cat_str(out, "RAlt+");
if(m & 0x80) furi_string_cat_str(out, "RGui+");
}
+8
View File
@@ -0,0 +1,8 @@
#pragma once
#include <furi.h>
#include <stdint.h>
// Human-readable name for a USB HID keyboard usage id (0 => NULL).
const char* hid_key_name(uint8_t usage);
// Append active modifier names ("Ctrl+Shift+") for a boot-keyboard modifier byte.
void hid_mods_str(FuriString* out, uint8_t mods);
Binary file not shown.

After

Width:  |  Height:  |  Size: 95 B