Add BLE GATT Fuzzer app
Connects, walks every service/characteristic, reads all readable values, and (on OK) writes boundary payloads to writable characteristics, logging every response. For authorised testing. Reuses libble.
This commit is contained in:
@@ -0,0 +1,15 @@
|
|||||||
|
# Needs firmware from ../build.sh (full BLE stack). Staged with libble/.
|
||||||
|
App(
|
||||||
|
appid="ble_gattfuzz",
|
||||||
|
name="BLE GATT Fuzzer",
|
||||||
|
apptype=FlipperAppType.EXTERNAL,
|
||||||
|
entry_point="ble_gattfuzz_app",
|
||||||
|
requires=["gui", "storage"],
|
||||||
|
stack_size=4 * 1024,
|
||||||
|
fap_category="Bluetooth",
|
||||||
|
fap_icon="icon.png",
|
||||||
|
fap_version="1.0",
|
||||||
|
fap_description="Enumerate, read and fuzz a peripheral's GATT database, logging responses",
|
||||||
|
fap_author="andydixon",
|
||||||
|
fap_weburl="https://github.com/andydixon/flipper-bt-inspector",
|
||||||
|
)
|
||||||
@@ -0,0 +1,434 @@
|
|||||||
|
// BLE GATT Fuzzer / logger: connect to a selected peripheral, walk every
|
||||||
|
// service and characteristic, read all readable values, and (optionally) write
|
||||||
|
// a set of boundary payloads to writable characteristics, logging every
|
||||||
|
// response and error. For authorised testing of devices you own. Reuses the
|
||||||
|
// libble central/GATT layer.
|
||||||
|
#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 <storage/storage.h>
|
||||||
|
#include <ble/core/ble_defs.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdarg.h>
|
||||||
|
#include "ble_central.h"
|
||||||
|
#include "ble_names.h"
|
||||||
|
|
||||||
|
#define TAG "BleGattFuzz"
|
||||||
|
#define LOG_DIR EXT_PATH("apps_data/ble_gattfuzz")
|
||||||
|
#define ROWS 3
|
||||||
|
#define FEED_MAX 64
|
||||||
|
#define FEED_LINE 40
|
||||||
|
|
||||||
|
typedef enum { ViewScan, ViewFeed } ViewId;
|
||||||
|
typedef enum { EvtTick, EvtSelect, EvtFeed, EvtJobDone } AppEvent;
|
||||||
|
typedef enum { JobRun, JobFuzz, JobDisconnect, JobQuit } JobType;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
BcDevice devs[BC_MAX_DEVICES];
|
||||||
|
size_t count, sel, top;
|
||||||
|
} ScanModel;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
char lines[FEED_MAX][FEED_LINE];
|
||||||
|
int count;
|
||||||
|
size_t scroll;
|
||||||
|
char header[40];
|
||||||
|
char status[28];
|
||||||
|
} FeedModel;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
Gui* gui;
|
||||||
|
ViewDispatcher* vd;
|
||||||
|
View* scan_view;
|
||||||
|
View* feed_view;
|
||||||
|
FuriTimer* timer;
|
||||||
|
Storage* storage;
|
||||||
|
File* log;
|
||||||
|
BleCentral* bc;
|
||||||
|
FuriThread* worker;
|
||||||
|
FuriMessageQueue* jobs;
|
||||||
|
ViewId cur;
|
||||||
|
BcDevice dev;
|
||||||
|
BcService services[BC_MAX_SERVICES];
|
||||||
|
BcChar chars[BC_MAX_CHARS];
|
||||||
|
bool fuzz_enabled;
|
||||||
|
} App;
|
||||||
|
|
||||||
|
static const char* dev_label(const BcDevice* d) {
|
||||||
|
return d->name[0] ? d->name : "(no name)";
|
||||||
|
}
|
||||||
|
|
||||||
|
static void feed(App* app, const char* fmt, ...) {
|
||||||
|
char line[FEED_LINE];
|
||||||
|
va_list va;
|
||||||
|
va_start(va, fmt);
|
||||||
|
vsnprintf(line, sizeof(line), fmt, va);
|
||||||
|
va_end(va);
|
||||||
|
if(app->log) {
|
||||||
|
storage_file_write(app->log, line, strlen(line));
|
||||||
|
storage_file_write(app->log, "\n", 1);
|
||||||
|
storage_file_sync(app->log);
|
||||||
|
}
|
||||||
|
with_view_model(
|
||||||
|
app->feed_view,
|
||||||
|
FeedModel * m,
|
||||||
|
{
|
||||||
|
strncpy(m->lines[m->count % FEED_MAX], line, FEED_LINE - 1);
|
||||||
|
m->lines[m->count % FEED_MAX][FEED_LINE - 1] = 0;
|
||||||
|
m->count++;
|
||||||
|
m->scroll = 0;
|
||||||
|
},
|
||||||
|
app->cur == ViewFeed);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void feed_status(App* app, const char* status) {
|
||||||
|
with_view_model(
|
||||||
|
app->feed_view, FeedModel * m, { strncpy(m->status, status, sizeof(m->status) - 1); },
|
||||||
|
app->cur == ViewFeed);
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint16_t char_end(App* app, int svc, int idx, int nch) {
|
||||||
|
if(idx + 1 < nch) return app->chars[idx + 1].decl_handle - 1;
|
||||||
|
return app->services[svc].end;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- worker: enumerate + read (+ optional fuzz) -----------------------------
|
||||||
|
|
||||||
|
static void run_enum(App* app, bool fuzz) {
|
||||||
|
if(!bc_is_connected(app->bc)) {
|
||||||
|
if(!bc_connect(app->bc, &app->dev, 8000)) {
|
||||||
|
feed(app, "connect failed");
|
||||||
|
feed_status(app, "connect failed");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
feed(app, "connected, mtu %u", bc_mtu(app->bc));
|
||||||
|
}
|
||||||
|
int nsvc = bc_discover_services(app->bc, app->services, BC_MAX_SERVICES);
|
||||||
|
if(nsvc < 0) {
|
||||||
|
feed(app, "service discovery failed");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
feed_status(app, fuzz ? "fuzzing..." : "reading...");
|
||||||
|
// Boundary payloads for the fuzz pass.
|
||||||
|
static const uint8_t p_zero[1] = {0x00};
|
||||||
|
static const uint8_t p_ff[1] = {0xFF};
|
||||||
|
static const uint8_t p_long[20] =
|
||||||
|
{0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41,
|
||||||
|
0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41};
|
||||||
|
struct {
|
||||||
|
const uint8_t* d;
|
||||||
|
uint8_t len;
|
||||||
|
const char* tag;
|
||||||
|
} pats[] = {{NULL, 0, "empty"}, {p_zero, 1, "0x00"}, {p_ff, 1, "0xFF"}, {p_long, 20, "20xA"}};
|
||||||
|
|
||||||
|
uint8_t val[BC_VALUE_MAX];
|
||||||
|
FuriString* s = furi_string_alloc();
|
||||||
|
for(int si = 0; si < nsvc; si++) {
|
||||||
|
furi_string_reset(s);
|
||||||
|
ble_uuid_str(s, app->services[si].uuid, app->services[si].uuid_len);
|
||||||
|
const char* sn = ble_uuid_name(app->services[si].uuid, app->services[si].uuid_len);
|
||||||
|
feed(app, "SVC %s %s", furi_string_get_cstr(s), sn ? sn : "");
|
||||||
|
int nch = bc_discover_chars(app->bc, &app->services[si], app->chars, BC_MAX_CHARS);
|
||||||
|
for(int ci = 0; ci < nch; ci++) {
|
||||||
|
BcChar* c = &app->chars[ci];
|
||||||
|
furi_string_reset(s);
|
||||||
|
ble_uuid_str(s, c->uuid, c->uuid_len);
|
||||||
|
if(c->props & CHAR_PROP_READ) {
|
||||||
|
int n = bc_read(app->bc, c->value_handle, val, BC_VALUE_MAX);
|
||||||
|
if(n >= 0) {
|
||||||
|
FuriString* h = furi_string_alloc();
|
||||||
|
ble_hex(h, val, n < 8 ? n : 8);
|
||||||
|
feed(app, " R %04X=%s", c->value_handle, furi_string_get_cstr(h));
|
||||||
|
furi_string_free(h);
|
||||||
|
} else {
|
||||||
|
feed(app, " R %04X err:%s", c->value_handle,
|
||||||
|
ble_att_error_name(bc_last_error(app->bc)) ? ble_att_error_name(bc_last_error(app->bc)) : "?");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bool writable = c->props & (CHAR_PROP_WRITE | CHAR_PROP_WRITE_WITHOUT_RESP);
|
||||||
|
if(fuzz && writable) {
|
||||||
|
bool wr = c->props & CHAR_PROP_WRITE;
|
||||||
|
for(size_t pi = 0; pi < COUNT_OF(pats); pi++) {
|
||||||
|
bool ok = bc_write(app->bc, c->value_handle, pats[pi].d, pats[pi].len, wr);
|
||||||
|
feed(app, " W %04X %s:%s", c->value_handle, pats[pi].tag, ok ? "ok" : "rej");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
UNUSED(char_end);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
furi_string_free(s);
|
||||||
|
feed(app, fuzz ? "fuzz pass done" : "read pass done");
|
||||||
|
feed_status(app, "done (OK=fuzz)");
|
||||||
|
}
|
||||||
|
|
||||||
|
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 == JobRun)
|
||||||
|
run_enum(app, false);
|
||||||
|
else if(job == JobFuzz)
|
||||||
|
run_enum(app, true);
|
||||||
|
else if(job == JobDisconnect) {
|
||||||
|
bc_disconnect(app->bc);
|
||||||
|
bc_scan_start(app->bc);
|
||||||
|
}
|
||||||
|
view_dispatcher_send_custom_event(app->vd, EvtJobDone);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- scan view --------------------------------------------------------------
|
||||||
|
|
||||||
|
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, "GATT Fuzzer");
|
||||||
|
canvas_set_font(c, FontSecondary);
|
||||||
|
furi_string_printf(s, "%u", (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);
|
||||||
|
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 = ViewScan;
|
||||||
|
bc_scan_start(app->bc);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- feed view --------------------------------------------------------------
|
||||||
|
|
||||||
|
static void feed_draw(Canvas* c, void* m_) {
|
||||||
|
FeedModel* m = m_;
|
||||||
|
canvas_clear(c);
|
||||||
|
canvas_set_font(c, FontPrimary);
|
||||||
|
canvas_draw_str(c, 2, 9, m->header[0] ? m->header : "GATT");
|
||||||
|
canvas_set_font(c, FontSecondary);
|
||||||
|
canvas_draw_str_aligned(c, 126, 9, AlignRight, AlignBottom, m->status);
|
||||||
|
canvas_draw_line(c, 0, 11, 127, 11);
|
||||||
|
int visible = 5;
|
||||||
|
int total = m->count;
|
||||||
|
int shown = total < visible ? total : visible;
|
||||||
|
for(int r = 0; r < shown; r++) {
|
||||||
|
int idx = total - 1 - (int)m->scroll - (shown - 1 - r);
|
||||||
|
if(idx < 0 || idx < total - FEED_MAX) continue;
|
||||||
|
canvas_draw_str(c, 2, 22 + r * 9, m->lines[idx % FEED_MAX]);
|
||||||
|
}
|
||||||
|
if(total > visible) elements_scrollbar_pos(c, 126, 12, 52, m->scroll, total);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool feed_input(InputEvent* e, void* ctx) {
|
||||||
|
App* app = ctx;
|
||||||
|
if(e->type != InputTypeShort && e->type != InputTypeRepeat) return false;
|
||||||
|
if(e->key == InputKeyOk && e->type == InputTypeShort) {
|
||||||
|
furi_message_queue_put(app->jobs, &(JobType){JobFuzz}, 0);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if(e->key != InputKeyUp && e->key != InputKeyDown) return false;
|
||||||
|
with_view_model(
|
||||||
|
app->feed_view,
|
||||||
|
FeedModel * m,
|
||||||
|
{
|
||||||
|
int maxs = m->count > 5 ? m->count - 5 : 0;
|
||||||
|
if(e->key == InputKeyDown && (int)m->scroll < maxs) m->scroll++;
|
||||||
|
if(e->key == InputKeyUp && m->scroll > 0) m->scroll--;
|
||||||
|
},
|
||||||
|
true);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint32_t feed_back(void* ctx) {
|
||||||
|
App* app = ctx;
|
||||||
|
furi_message_queue_put(app->jobs, &(JobType){JobDisconnect}, 0);
|
||||||
|
app->cur = ViewScan;
|
||||||
|
return ViewScan;
|
||||||
|
}
|
||||||
|
static uint32_t scan_exit(void* ctx) {
|
||||||
|
UNUSED(ctx);
|
||||||
|
return VIEW_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool custom_cb(void* ctx, uint32_t event) {
|
||||||
|
App* app = ctx;
|
||||||
|
switch(event) {
|
||||||
|
case EvtTick:
|
||||||
|
if(app->cur == ViewScan)
|
||||||
|
with_view_model(
|
||||||
|
app->scan_view,
|
||||||
|
ScanModel * m,
|
||||||
|
{
|
||||||
|
m->count = bc_snapshot(app->bc, m->devs, BC_MAX_DEVICES);
|
||||||
|
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);
|
||||||
|
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) {
|
||||||
|
with_view_model(
|
||||||
|
app->feed_view,
|
||||||
|
FeedModel * m,
|
||||||
|
{
|
||||||
|
m->count = 0;
|
||||||
|
m->scroll = 0;
|
||||||
|
snprintf(m->header, sizeof(m->header), "%s", dev_label(&app->dev));
|
||||||
|
strncpy(m->status, "working", sizeof(m->status) - 1);
|
||||||
|
},
|
||||||
|
false);
|
||||||
|
app->cur = ViewFeed;
|
||||||
|
view_dispatcher_switch_to_view(app->vd, ViewFeed);
|
||||||
|
furi_message_queue_put(app->jobs, &(JobType){JobRun}, 0);
|
||||||
|
}
|
||||||
|
} break;
|
||||||
|
case EvtJobDone: break;
|
||||||
|
default: return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void timer_cb(void* ctx) {
|
||||||
|
App* app = ctx;
|
||||||
|
view_dispatcher_send_custom_event(app->vd, EvtTick);
|
||||||
|
}
|
||||||
|
|
||||||
|
int32_t ble_gattfuzz_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->vd = view_dispatcher_alloc();
|
||||||
|
view_dispatcher_set_event_callback_context(app->vd, app);
|
||||||
|
view_dispatcher_set_custom_event_callback(app->vd, custom_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, scan_exit);
|
||||||
|
view_dispatcher_add_view(app->vd, ViewScan, app->scan_view);
|
||||||
|
|
||||||
|
app->feed_view = view_alloc();
|
||||||
|
view_allocate_model(app->feed_view, ViewModelTypeLocking, sizeof(FeedModel));
|
||||||
|
view_set_context(app->feed_view, app);
|
||||||
|
view_set_draw_callback(app->feed_view, feed_draw);
|
||||||
|
view_set_input_callback(app->feed_view, feed_input);
|
||||||
|
view_set_previous_callback(app->feed_view, feed_back);
|
||||||
|
view_dispatcher_add_view(app->vd, ViewFeed, app->feed_view);
|
||||||
|
|
||||||
|
app->timer = furi_timer_alloc(timer_cb, FuriTimerTypePeriodic, app);
|
||||||
|
|
||||||
|
if(!bc_supported()) {
|
||||||
|
view_dispatcher_switch_to_view(app->vd, ViewScan);
|
||||||
|
view_dispatcher_run(app->vd);
|
||||||
|
goto teardown;
|
||||||
|
}
|
||||||
|
|
||||||
|
storage_simply_mkdir(app->storage, LOG_DIR);
|
||||||
|
DateTime dt;
|
||||||
|
furi_hal_rtc_get_datetime(&dt);
|
||||||
|
FuriString* path = furi_string_alloc_printf(
|
||||||
|
LOG_DIR "/fuzz_%02u%02u%02u.log", dt.hour, dt.minute, dt.second);
|
||||||
|
app->log = storage_file_alloc(app->storage);
|
||||||
|
if(!storage_file_open(app->log, furi_string_get_cstr(path), FSAM_WRITE, FSOM_CREATE_ALWAYS)) {
|
||||||
|
storage_file_free(app->log);
|
||||||
|
app->log = NULL;
|
||||||
|
}
|
||||||
|
furi_string_free(path);
|
||||||
|
|
||||||
|
bool was_adv = furi_hal_bt_is_active();
|
||||||
|
furi_hal_bt_stop_advertising();
|
||||||
|
furi_delay_ms(100);
|
||||||
|
app->bc = bc_alloc(NULL, NULL);
|
||||||
|
app->worker = furi_thread_alloc_ex("BleFuzzW", 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();
|
||||||
|
if(app->log) {
|
||||||
|
storage_file_close(app->log);
|
||||||
|
storage_file_free(app->log);
|
||||||
|
}
|
||||||
|
|
||||||
|
teardown:
|
||||||
|
furi_timer_free(app->timer);
|
||||||
|
view_dispatcher_remove_view(app->vd, ViewScan);
|
||||||
|
view_dispatcher_remove_view(app->vd, ViewFeed);
|
||||||
|
view_free(app->scan_view);
|
||||||
|
view_free(app->feed_view);
|
||||||
|
view_dispatcher_free(app->vd);
|
||||||
|
furi_message_queue_free(app->jobs);
|
||||||
|
furi_record_close(RECORD_STORAGE);
|
||||||
|
furi_record_close(RECORD_GUI);
|
||||||
|
free(app);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 95 B |
@@ -21,7 +21,7 @@
|
|||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
APPS=(bt_inspector ble_hid_host ble_sensor ble_tracker) # app dirs under $HERE; BLE apps staged with libble/
|
APPS=(bt_inspector ble_hid_host ble_sensor ble_tracker ble_gattfuzz) # app dirs under $HERE; BLE apps staged with libble/
|
||||||
APP_SRC="$HERE/bt_inspector" # used only for the BLE_API=min symbol scan
|
APP_SRC="$HERE/bt_inspector" # used only for the BLE_API=min symbol scan
|
||||||
FW_DIR="${FW_DIR:-$HERE/flipperzero-firmware}"
|
FW_DIR="${FW_DIR:-$HERE/flipperzero-firmware}"
|
||||||
FW_TAG="${FW_TAG:-1.4.3}"
|
FW_TAG="${FW_TAG:-1.4.3}"
|
||||||
|
|||||||
Reference in New Issue
Block a user