27 Commits
Author SHA1 Message Date
andyandClaude Fable 5.1 f2297d0a79 Add GPLv3 license and per-file copyright headers
License Flipper Blue++ under GPL-3.0-or-later, matching the Flipper Zero
firmware it builds on and links against. Add the verbatim GPLv3 text as
LICENSE, an SPDX + copyright header to every original source file and
build.sh, and a License section to the README noting the app sources are
(C) 2026 Andy Dixon while the upstream firmware/SDK remain (C) Flipper
Devices Inc., both GPLv3. Headers are comment-only; the full suite still
builds and all faps link.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LEaD4ZeAfvWLBUxXp8G11r
2026-09-11 15:10:53 +01:00
andyandClaude Fable 5.1 8a535df7e2 Default BLE_API to all
Export the full ST gap/gatt/hci/hal/l2cap command set by default instead of
just hci,gatt, so apps built against this SDK get the whole BLE API without
a rebuild. Costs ~9 KB more firmware flash: 788 KB, still 13 free pages to
the full stack. Update the build.sh comments and README (env-var table and
flash-budget table) to mark `all` as the default. Builds clean; all faps
link and resolve.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LEaD4ZeAfvWLBUxXp8G11r
2026-09-11 15:03:38 +01:00
andy 17653ee6d7 Rewrite README around the full-stack BLE firmware, detail every app
Recentre the README on Flipper Blue++ as a custom full-stack Bluetooth
firmware plus its app suite, rather than a single BT Inspector app that grew
extra sections. New structure: intro, build/install (with the env-var table),
a detailed per-app section for all ten apps (what each does, controls, logs),
then the firmware internals (full stack, exported API + flash table, extra
exports, sub-GHz unlock), how the BLE apps work, limitations, troubleshooting
and repo layout.
2026-09-11 14:56:40 +01:00
andy fb1b495512 Rename project to Flipper Blue++
Point every app's fap_weburl at the renamed repo
(github.com/andydixon/flipper-bluepp) and recenter the build.sh header on the
full-stack BLE firmware and the app suite rather than a single app. GitHub
repo renamed from flipper-bt-inspector; the slug drops "++" since GitHub repo
names cannot contain "+".
2026-09-11 14:52:46 +01:00
andy 4a09b3e963 Fix stale spectrum bins on skipped sub-GHz frequencies
In the all-bands sweep the CC1101 band gaps are skipped, but their bins kept
whatever a previous pass left there, drawing phantom bars. Set skipped bins
to the -127 dBm floor.
2026-09-11 14:33:18 +01:00
andy 9e62ec5cfe Fix use-after-free on exit in BLE GATT Server
Teardown freed the view port before unhooking the serial and status
callbacks, which run on the BLE/BT thread and call view_port_update. A late
notification or status change during exit could touch freed memory. Unhook
both callbacks and restore the default profile first, then free the view port.
2026-09-11 14:33:18 +01:00
andy e3adcda565 Fix out-of-bounds read decoding Apple iBeacon advertisements
The iBeacon branch guarded on `l >= 20` but reads p[20] (the Tx-power byte),
so a payload clamped to exactly 20 bytes read one byte past the advertising
data. Require the full 21 bytes (16 UUID + 2 major + 2 minor + 1 Tx).
2026-09-11 14:33:18 +01:00
andy c3b20ee0f9 Document the new apps in README limitations
Add per-app limitation blocks for BLE Sensor Dashboard, Tracker Detector,
GATT Fuzzer, Beacon Toolkit, GATT Server, the Sub-GHz Scanner/Spectrum pair,
and Battery Health, and generalise the shared BLE notes from two apps to the
whole suite (no pairing, one app owns the radio at a time).
2026-09-11 14:22:04 +01:00
andy c0ff957159 Add Battery Health monitor app
Live fuel-gauge readings (voltage, current, temperature, remaining/full/design capacity, health, wear) with a rolling voltage graph. Read-only; the only app that also runs on stock firmware.
2026-09-11 14:08:30 +01:00
andy 527da68ec3 Add Sub-GHz Spectrum app
Live spectrum bar graph and scrolling waterfall over a tunable centre/span window; Left/Right retune, Up/Down change span.
2026-09-11 14:08:30 +01:00
andy c23380f2dd Add Sub-GHz Scanner app
Sweeps RSSI across the CC1101 bands in fixed steps, shows a spectrum, and logs frequencies above a threshold. Uses the region-unlocked firmware and an exported CC1101 preset.
2026-09-11 14:08:30 +01:00
andy 7fe1972d28 Add BLE GATT Server/emulator app
Brings up the Flipper as a connectable serial GATT peripheral via the bt service profile API and echoes what a central writes. Restores the default profile on exit.
2026-09-11 14:08:30 +01:00
andy 3d57cb315c Add BLE Beacon Toolkit app
Broadcasts an iBeacon or a raw advertising payload with a chosen random MAC via the exported extra-beacon API. Needs the full BLE stack.
2026-09-11 14:08:30 +01:00
andy e12381f6da 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.
2026-09-11 14:08:30 +01:00
andy ba76e2855d Add BLE Tracker Detector app
Passively scans and flags item trackers by advertising signature (Apple Find My/AirTag, Tile, Samsung SmartTag), with a following-you warning for a tracker seen persistently. Read-only; reuses libble.
2026-09-11 14:08:30 +01:00
andy 3f35d672a2 Add BLE Sensor Dashboard app
Connects to a selected peripheral, finds readable/notifying characteristics with known SIG meanings, and shows them decoded and updating live. Reuses libble (central + value decoders).
2026-09-11 14:08:30 +01:00
andy eb34f0e61c Prep for the app suite: libble staging gate + README overview
Only stage libble/ into apps that include ble_central.h (so non-BLE apps
don't compile the central layer). Rewrite the README overview to list the
full app suite and note that everything except Battery Health needs the
custom firmware.
2026-09-11 14:08:05 +01:00
andy 0610a70655 Update README limitations for both apps and the unlocks
Restructure the Limitations section into shared/hardware, BT Inspector, BLE
HID Host, and exported-API (unlock) groups. Document the HID host's
heuristic report decoding and keyboard-usage coverage, the flash/option-byte
brick risk, that RPC/loader exposes only internal types, and that the sub-GHz
unlock removes the region check only (PLL bands and the law still apply).
2026-09-11 13:49:44 +01:00
andy ccd8304fbf Remove sub-GHz region/frequency TX limits (research build)
Patch furi_hal_region.c so furi_hal_region_is_frequency_allowed and
furi_hal_region_is_provisioned always return true, allowing transmit on any
frequency the CC1101 can tune regardless of provisioned region. The hardware
PLL bands (~300-348 / 387-464 / 779-928 MHz) remain since they are physical.
Guarded by SUBGHZ_UNLOCK (default 1); build SUBGHZ_UNLOCK=0 to keep limits.
README documents that transmitting outside your local allocation may be
illegal and this is for research/authorised testing only.
2026-09-11 13:24:31 +01:00
andy bce03918b3 Fix API export policy: enable added headers, not their functions
The RPC/loader commit added internal headers and relied on the "?" fallback
that enabled every newly-discovered symbol. That exported internal loader
helpers (loader_queue_*, loader_menu_alloc, ...) which are not in the
firmware API-table link, breaking the full firmware build; the earlier
per-app `fap` check did not link the table so it went unnoticed.

Change the api_symbols.csv editor so newly-discovered Headers stay enabled
(their declarations must reach the API table) but new Functions/Variables
default to disabled unless explicitly listed in EXPORT_ENABLE. Drop the
over-broad ^rpc_/^loader_ patterns (the public APIs are already exported by
stock firmware). Result: internal loader types are available to apps, the
internal helper functions stay unexported, and the full firmware links.
2026-09-11 13:23:36 +01:00
andy b1efa9cd4e 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.
2026-09-11 13:14:57 +01:00
andy 4b887b1ffb Export RPC + loader internals to apps
The public rpc_* and loader_* APIs are already exported by stock firmware
(RPC sessions, app data exchange, launch/enqueue/lock/menu); enable them
explicitly via EXPORT_ENABLE so they stay on. Add the internal loader
header set (loader_i.h, loader_menu.h, loader_queue.h, loader_applications.h)
to the SDK so the internal Loader handle and the menu/queue helpers are
reachable from apps. rpc_i.h is skipped: it depends on the protobuf headers
which are not part of the app SDK. API version -> 87.3; firmware links.
2026-09-11 13:10:25 +01:00
andy 613848eddf Export flash + option-byte internals to apps
Enable all furi_hal_flash_* functions: geometry, free-page info, raw
program/erase/dword-write, and option-byte read/set/apply. Lets an app
read and modify internal flash directly (research use). Guarded by
EXPORT_FLASH (default 1); build with EXPORT_FLASH=0 to leave disabled.
17 symbols exported; firmware links and the app builds.
2026-09-11 13:08:58 +01:00
andy 2ca4adbeeb Export remaining power/battery internal to apps
The gas-gauge readings (voltage, current, temperature, capacity, health,
charge state) are already exported by stock firmware; add the one remaining
safe reader, furi_hal_power_insomnia_level. furi_hal_power_init stays
disabled because re-initialising the power HAL from an app is unsafe.
2026-09-11 13:08:28 +01:00
andy 9fbbd1aeda Export BLE peripheral internals (GAP + BLE glue) to apps
Add a data-driven firmware-symbol export mechanism (EXPORT_ENABLE list in
build.sh, applied by the api_symbols.csv editor) and use it to enable all
gap_* functions, the safe ble_glue_* status/lifecycle calls, and
furi_hal_bt_init. Apps can now drive GAP directly, run a custom GATT
server, set the advertised name / extra-beacon config, and read radio-stack
status. The coprocessor FUS stack delete/install calls stay disabled to
avoid erasing the radio firmware. 19 symbols newly exported; firmware links.
2026-09-11 13:07:37 +01:00
andy 999b20f378 Convert to external .fap; export ST BLE command API from the firmware
build.sh now adds ST's aci_*/hci_* headers to the SDK (with extern "C"
guards, the API table is C++), lets fbt regenerate api_symbols.csv and
enables the selected functions (BLE_API=hci,gatt default; min; all).
The app builds as a normal .fap shipped inside the update package and
loadable from SD, saving ~12 KB of firmware flash versus the built-in
(min mode: ~22 KB). qsort replaced with an insertion sort since libc's
qsort is not exported to apps. README documents modes and flash budget.
2026-09-11 11:56:17 +01:00
andy e863616f82 Add BT Inspector: BLE scanner and GATT explorer for Flipper Zero
Built-in firmware app (needs the full ST BLE stack) with live RSSI-sorted
scan list, advertisement decoding (Apple Continuity, Microsoft CDP, Fast
Pair, Eddystone), connect/discover/read/write/notify, device-info summary,
value history and a timestamped session log. build.sh produces a firmware
update package with COPRO_STACK_TYPE=ble_full and the app compiled in.
2026-09-11 08:39:51 +01:00
13 changed files with 61 additions and 179 deletions
+13 -12
View File
@@ -59,24 +59,25 @@ static void draw_cb(Canvas* c, void* ctx) {
char buf[64];
canvas_clear(c);
canvas_set_font(c, FontPrimary);
canvas_draw_str(c, 2, 9, "Battery Health");
const char* st = m->charged ? "full" : m->charging ? "chging" : "dischg";
snprintf(buf, sizeof(buf), "Battery %u%%", m->pct);
canvas_draw_str(c, 2, 9, buf);
const char* st = m->charged ? "full" : m->charging ? "charging" : "discharging";
canvas_set_font(c, FontSecondary);
canvas_draw_str_aligned(c, 126, 9, AlignRight, AlignBottom, st);
canvas_draw_line(c, 0, 11, 127, 11);
// Compact rows, each kept within the 128 px width.
snprintf(buf, sizeof(buf), "%u%% %.2fV %s", m->pct, (double)m->v, st);
canvas_draw_str(c, 2, 20, buf);
snprintf(buf, sizeof(buf), "%dmA %.0fC VBUS%.1f", (int)(m->i * 1000), (double)m->temp, (double)m->vbus);
canvas_draw_str(c, 2, 30, buf);
int wear = m->design_mah ? 100 - (int)(m->full_mah * 100 / m->design_mah) : 0;
snprintf(buf, sizeof(buf), "%.3f V %+.0f mA %.1f C", (double)m->v, (double)(m->i * 1000), (double)m->temp);
canvas_draw_str(c, 2, 21, buf);
snprintf(
buf, sizeof(buf), "%lu/%lumAh H%u%% W%d%%", (unsigned long)m->rem_mah,
(unsigned long)m->full_mah, m->health, wear);
canvas_draw_str(c, 2, 40, buf);
buf, sizeof(buf), "%lu/%lu mAh (design %lu)", (unsigned long)m->rem_mah,
(unsigned long)m->full_mah, (unsigned long)m->design_mah);
canvas_draw_str(c, 2, 31, buf);
int wear = m->design_mah ? 100 - (int)(m->full_mah * 100 / m->design_mah) : 0;
snprintf(buf, sizeof(buf), "Health %u%% Wear %d%% VBUS %.2fV", m->health, wear, (double)m->vbus);
canvas_draw_str(c, 2, 41, buf);
// Voltage graph, auto-scaled to 3.3-4.2 V window.
int gx = 2, gy = 44, gw = GRAPH_N, gh = 17;
int gx = 2, gy = 45, gw = GRAPH_N, gh = 17;
canvas_draw_frame(c, gx, gy, gw + 2, gh + 2);
for(int k = 0; k < m->graph_count; k++) {
float v = m->graph[k];
Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 B

After

Width:  |  Height:  |  Size: 95 B

+9 -9
View File
@@ -65,20 +65,20 @@ static void draw_cb(Canvas* c, void* ctx) {
canvas_set_font(c, FontPrimary);
canvas_draw_str(c, 2, 9, "GATT Server");
canvas_set_font(c, FontSecondary);
canvas_draw_line(c, 0, 11, 127, 11);
const char* st = app->status == BtStatusConnected ? "connected" :
app->status == BtStatusAdvertising ? "advertising" :
app->status == BtStatusOff ? "off" :
"unavailable";
snprintf(buf, sizeof(buf), "Serial: %s", st);
canvas_draw_str(c, 2, 22, buf);
snprintf(buf, sizeof(buf), "RX %lu B TX %lu B", (unsigned long)app->rx_bytes, (unsigned long)app->tx_bytes);
canvas_draw_str(c, 2, 33, buf);
snprintf(buf, sizeof(buf), "Echo %s OK=toggle", app->echo ? "on" : "off");
canvas_draw_str(c, 2, 44, buf);
canvas_draw_str_aligned(c, 126, 9, AlignRight, AlignBottom, st);
canvas_draw_line(c, 0, 11, 127, 11);
canvas_draw_str(c, 2, 22, "Serial GATT service (RX/TX)");
snprintf(buf, sizeof(buf), "RX %lu B TX %lu B", (unsigned long)app->rx_bytes, (unsigned long)app->tx_bytes);
canvas_draw_str(c, 2, 34, buf);
snprintf(buf, sizeof(buf), "Echo: %s (OK toggles)", app->echo ? "on" : "off");
canvas_draw_str(c, 2, 45, buf);
if(app->last[0]) {
snprintf(buf, sizeof(buf), "last: %.14s", app->last);
canvas_draw_str(c, 2, 58, buf);
snprintf(buf, sizeof(buf), "last: %s", app->last);
canvas_draw_str(c, 2, 57, buf);
}
furi_mutex_release(app->mutex);
}
+4 -21
View File
@@ -84,7 +84,6 @@ typedef struct {
Sub subs[SUBS_MAX];
int sub_count;
bool connected_ok;
volatile bool cancel;
char setup_msg[48];
} App;
@@ -96,8 +95,6 @@ typedef struct {
// ---- logging ----------------------------------------------------------------
static App* s_app; // popup Back callback (popup view ctx isn't the App)
static void app_log(App* app, const char* fmt, ...) {
if(!app->log) return;
DateTime dt;
@@ -243,13 +240,7 @@ static int32_t worker_thread(void* ctx) {
while(furi_message_queue_get(app->jobs, &job, FuriWaitForever) == FuriStatusOk) {
if(job == JobQuit) break;
if(job == JobConnect) {
bool ok = bc_connect(app->bc, &app->dev, 8000) && hid_setup(app);
if(app->cancel) {
if(bc_is_connected(app->bc)) bc_disconnect(app->bc);
bc_scan_start(app->bc);
ok = false;
}
app->connected_ok = ok;
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);
@@ -451,11 +442,9 @@ static uint32_t nav_mon_back(void* ctx) {
return ViewScan;
}
static uint32_t nav_connecting_back(void* ctx) {
static uint32_t nav_stay_popup(void* ctx) {
UNUSED(ctx);
s_app->cancel = true;
s_app->cur_view = ViewScan;
return ViewScan;
return ViewPopup;
}
// ---- events -----------------------------------------------------------------
@@ -519,17 +508,12 @@ static bool custom_event_cb(void* ctx, uint32_t event) {
false);
if(have) {
app_log(app, "CONNECTING %s", dev_label(&app->dev));
app->cancel = false;
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->cancel) {
app->cancel = false;
break;
}
if(app->connected_ok) {
char hdr[40];
snprintf(hdr, sizeof(hdr), "%s", dev_label(&app->dev));
@@ -570,7 +554,6 @@ int32_t ble_hid_host_app(void* p) {
UNUSED(p);
App* app = malloc(sizeof(App));
memset(app, 0, sizeof(App));
s_app = app;
app->gui = furi_record_open(RECORD_GUI);
app->storage = furi_record_open(RECORD_STORAGE);
app->jobs = furi_message_queue_alloc(8, sizeof(JobType));
@@ -599,7 +582,7 @@ int32_t ble_hid_host_app(void* p) {
view_dispatcher_add_view(app->vd, ViewMonitor, app->mon_view);
app->popup = popup_alloc();
view_set_previous_callback(popup_get_view(app->popup), nav_connecting_back);
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);
+6 -23
View File
@@ -70,12 +70,9 @@ typedef struct {
BcService services[BC_MAX_SERVICES];
BcChar chars[BC_MAX_CHARS];
bool connected_ok;
volatile bool cancel;
char setup_msg[40];
} App;
static App* s_app; // for the popup Back callback (popup view ctx isn't the App)
static const char* dev_label(const BcDevice* d) {
return d->name[0] ? d->name : "(no name)";
}
@@ -142,15 +139,9 @@ static int32_t worker_thread(void* ctx) {
JobType job;
while(furi_message_queue_get(app->jobs, &job, FuriWaitForever) == FuriStatusOk) {
if(job == JobQuit) break;
if(job == JobConnect) {
bool ok = bc_connect(app->bc, &app->dev, 8000) && sensor_setup(app);
if(app->cancel) { // user pressed Back while we were busy
if(bc_is_connected(app->bc)) bc_disconnect(app->bc);
bc_scan_start(app->bc);
ok = false;
}
app->connected_ok = ok;
} else if(job == JobDisconnect) {
if(job == JobConnect)
app->connected_ok = bc_connect(app->bc, &app->dev, 8000) && sensor_setup(app);
else if(job == JobDisconnect) {
bc_disconnect(app->bc);
bc_scan_start(app->bc);
}
@@ -297,11 +288,9 @@ static uint32_t scan_exit(void* ctx) {
UNUSED(ctx);
return VIEW_NONE;
}
static uint32_t connecting_back(void* ctx) {
static uint32_t stay_popup(void* ctx) {
UNUSED(ctx);
s_app->cancel = true; // worker will drop the half-made connection
s_app->cur = ViewScan;
return ViewScan;
return ViewPopup;
}
static void popup_cb(void* ctx) {
@@ -354,7 +343,6 @@ static bool custom_cb(void* ctx, uint32_t event) {
popup_set_context(app->popup, app);
popup_set_header(app->popup, "Connecting...", 64, 26, AlignCenter, AlignCenter);
popup_set_text(app->popup, dev_label(&app->dev), 64, 40, AlignCenter, AlignCenter);
app->cancel = false;
app->cur = ViewPopup;
view_dispatcher_switch_to_view(app->vd, ViewPopup);
furi_message_queue_put(app->jobs, &(JobType){JobConnect}, 0);
@@ -362,10 +350,6 @@ static bool custom_cb(void* ctx, uint32_t event) {
} break;
case EvtNotify: on_notify(app); break;
case EvtJobDone:
if(app->cancel) { // user already backed out to the scan list
app->cancel = false;
break;
}
if(app->connected_ok) {
with_view_model(
app->readings_view, ReadingsModel * m,
@@ -399,7 +383,6 @@ int32_t ble_sensor_app(void* p) {
UNUSED(p);
App* app = malloc(sizeof(App));
memset(app, 0, sizeof(App));
s_app = app;
app->gui = furi_record_open(RECORD_GUI);
app->jobs = furi_message_queue_alloc(8, sizeof(JobType));
app->notify_q = furi_message_queue_alloc(16, sizeof(NotifyMsg));
@@ -427,7 +410,7 @@ int32_t ble_sensor_app(void* p) {
view_dispatcher_add_view(app->vd, ViewReadings, app->readings_view);
app->popup = popup_alloc();
view_set_previous_callback(popup_get_view(app->popup), connecting_back);
view_set_previous_callback(popup_get_view(app->popup), stay_popup);
view_dispatcher_add_view(app->vd, ViewPopup, popup_get_view(app->popup));
app->timer = furi_timer_alloc(timer_cb, FuriTimerTypePeriodic, app);
+11 -11
View File
@@ -89,7 +89,6 @@ typedef struct {
BleCentral* bc;
Tracker db[BC_MAX_DEVICES];
size_t db_count;
BcDevice snap[BC_MAX_DEVICES]; // snapshot scratch (kept off the stack)
} App;
static void app_log(App* app, const char* fmt, ...) {
@@ -108,7 +107,7 @@ static void app_log(App* app, const char* fmt, ...) {
}
static void tick(App* app) {
BcDevice* snap = app->snap;
BcDevice snap[BC_MAX_DEVICES];
size_t n = bc_snapshot(app->bc, snap, BC_MAX_DEVICES);
uint32_t now = furi_get_tick();
for(size_t i = 0; i < n; i++) {
@@ -157,6 +156,15 @@ static void tick(App* app) {
m->items[i].hits = app->db[i].hits;
m->items[i].warned = app->db[i].warned;
}
for(size_t i = 1; i < m->count; i++) { // insertion sort by rssi
Tracker t = m->items[i];
size_t j = i;
while(j > 0 && m->items[j - 1].rssi < t.rssi) {
m->items[j] = m->items[j - 1];
j--;
}
m->items[j] = t;
}
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;
@@ -219,14 +227,8 @@ static uint32_t exit_cb(void* ctx) {
return VIEW_NONE;
}
static bool tracker_custom(void* ctx, uint32_t event) {
if(event == 1) tick((App*)ctx);
return true;
}
static void timer_cb(void* ctx) {
App* app = ctx;
view_dispatcher_send_custom_event(app->vd, 1);
tick((App*)ctx);
}
int32_t ble_tracker_app(void* p) {
@@ -237,8 +239,6 @@ int32_t ble_tracker_app(void* p) {
app->storage = furi_record_open(RECORD_STORAGE);
app->vd = view_dispatcher_alloc();
view_dispatcher_set_event_callback_context(app->vd, app);
view_dispatcher_set_custom_event_callback(app->vd, tracker_custom);
view_dispatcher_attach_to_gui(app->vd, app->gui, ViewDispatcherTypeFullscreen);
app->view = view_alloc();
view_allocate_model(app->view, ViewModelTypeLocking, sizeof(ListModel));
+3 -56
View File
@@ -66,7 +66,6 @@ typedef struct {
BcDevice devs[BC_MAX_DEVICES];
size_t count, sel, top;
bool scanning;
uint8_t scan_err;
} ScanModel;
typedef struct {
@@ -129,7 +128,6 @@ typedef struct {
JobType job;
bool job_ok;
volatile bool cancel;
FuriString* status;
FuriString* info_text;
} App;
@@ -330,10 +328,7 @@ static void scan_draw(Canvas* c, void* m_) {
canvas_set_font(c, FontPrimary);
canvas_draw_str(c, 2, 9, "BT Inspector");
canvas_set_font(c, FontSecondary);
if(m->scanning)
furi_string_printf(s, "%u found", (unsigned)m->count);
else
furi_string_printf(s, "err 0x%02X", m->scan_err);
furi_string_printf(s, m->scanning ? "%u found" : "%u (stopped)", (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...");
@@ -394,8 +389,7 @@ static void scan_enter(void* ctx) {
app->cur_view = ViewScan;
bool ok = bc_scan_start(app->bc);
if(!ok) app_log(app, "SCAN START FAILED 0x%02X", bc_last_error(app->bc));
uint8_t err = ok ? 0 : bc_last_error(app->bc);
with_view_model(app->scan_view, ScanModel * m, { m->scanning = ok; m->scan_err = err; }, true);
with_view_model(app->scan_view, ScanModel * m, { m->scanning = ok; }, true);
}
// ---- page view (scrollable text with optional buttons) -----------------------
@@ -597,32 +591,6 @@ static uint16_t char_end_handle(App* app) {
return app->services[app->svc_sel].end;
}
// If the device advertised no name, read the GAP Device Name (0x2A00) over the
// connection so the device page and service list show its real name, not just
// the vendor. Runs on the worker thread right after service discovery.
static void resolve_name(App* app) {
if(app->dev.name[0]) return; // already have an advertised name
for(int i = 0; i < app->svc_count; i++) {
BcService* s = &app->services[i];
if(!(s->uuid_len == 2 && (s->uuid[0] | (s->uuid[1] << 8)) == 0x1800)) continue; // GAP
BcChar tmp[8];
int n = bc_discover_chars(app->bc, s, tmp, COUNT_OF(tmp));
for(int k = 0; k < n; k++) {
uint16_t u = tmp[k].uuid_len == 2 ? (tmp[k].uuid[0] | (tmp[k].uuid[1] << 8)) : 0;
if(u != 0x2A00 || !(tmp[k].props & CHAR_PROP_READ)) continue;
uint8_t val[BC_NAME_MAX + 1];
int len = bc_read(app->bc, tmp[k].value_handle, val, BC_NAME_MAX);
if(len > 0) {
memcpy(app->dev.name, val, len);
app->dev.name[len] = 0;
app_log(app, "NAME resolved: %s", app->dev.name);
}
return;
}
return; // found GAP but no readable name
}
}
static void do_info(App* app) {
furi_string_printf(app->info_text, "%s\nMTU %u\n", dev_label(&app->dev), bc_mtu(app->bc));
FuriString* label = furi_string_alloc();
@@ -671,7 +639,6 @@ static int32_t worker_thread(void* ctx) {
int n = bc_discover_services(app->bc, app->services, BC_MAX_SERVICES);
app->svc_count = n < 0 ? 0 : n;
ok = n >= 0;
if(ok) resolve_name(app);
} break;
case JobChars: {
int n = bc_discover_chars(app->bc, &app->services[app->svc_sel], app->chars, BC_MAX_CHARS);
@@ -881,10 +848,7 @@ static uint32_t nav_exit(void* ctx) {
static uint32_t nav_stay_popup(void* ctx) {
UNUSED(ctx);
// Back on a connect/discover popup: abort and return to the scan list.
nav_app->cancel = true;
nav_app->cur_view = ViewScan;
return ViewScan;
return ViewPopup;
}
static uint32_t nav_page_back(void* ctx) {
@@ -947,16 +911,6 @@ static void on_tick(App* app) {
}
static void on_job_done(App* app) {
if(app->cancel) { // user backed out of the connect/discover popup
app->cancel = false;
if(app->job != JobDisconnect) {
if(bc_is_connected(app->bc))
post_job(app, JobDisconnect);
else
bc_scan_start(app->bc);
}
return;
}
switch(app->job) {
case JobConnect:
if(app->job_ok) {
@@ -1079,7 +1033,6 @@ static bool custom_event_cb(void* ctx, uint32_t event) {
case EvtPageCenter:
if(app->page_kind == PageDetails) {
app_log(app, "CONNECTING %s", dev_label(&app->dev));
app->cancel = false;
show_popup(app, "Connecting...", dev_label(&app->dev), false);
post_job(app, JobConnect);
} else if(app->page_kind == PageChar) {
@@ -1101,12 +1054,6 @@ static bool custom_event_cb(void* ctx, uint32_t event) {
case EvtJobDone: on_job_done(app); break;
case EvtNotify: on_notify(app); break;
case EvtPopupDone:
if(app->cancel) {
app->cancel = false;
bc_scan_start(app->bc);
show_view(app, ViewScan);
break;
}
if(app->after_popup == AfterPopupServices) {
show_view(app, ViewServices);
} else {
-27
View File
@@ -116,33 +116,6 @@ if changed:
PY
fi
# Don't validate the CPU2-controlled secure-region option bytes against the
# stock reference. The updater's OB validation stage compares device option
# bytes to scripts/ob.data; that reference is tuned for the LIGHT stack (SFSA
# 0x080D7000, SBRV 0x0D7000>>2). Installing the FULL stack makes CPU2 move the
# secure boundary (SFSA -> 0xCE, SBRV -> 0x0CE000>>2, plus the SRAM2 secure
# boundaries), so the compare fails with "Uncorr. value mismatch #[8-93]" right
# after the radio installs. These bytes are read-only (only CPU2 sets them) and
# are correct-by-construction once the radio install has succeeded, so exclude
# them from the compare. The user-controllable OBs (RDP, BOR, boot config,
# watchdogs, PCROP, WRP) are still validated. obdata.py skips '#'-commented lines.
python3 - scripts/ob.data <<'PY'
import sys
p = sys.argv[1]
secure = {"SFSA", "FSD", "DDS", "C2OPT", "NBRSD", "SNBRSA", "BRSD", "SBRSA", "SBRV"}
out, changed = [], False
for line in open(p).read().splitlines():
name = line.split(":", 1)[0].strip()
if name in secure and not line.startswith("#"):
out.append("# " + line + " # Blue++: CPU2-set for full stack, not validated")
changed = True
else:
out.append(line)
if changed:
open(p, "w").write("\n".join(out) + "\n")
print(">> patched " + p + ": exclude CPU2 secure-region OBs from validation")
PY
# Stage each app into applications_user, with the shared libble sources copied
# alongside so the fap glob (*.c) picks them up.
mkdir -p applications_user
+9 -16
View File
@@ -9,7 +9,6 @@
#include <ble/core/auto/ble_vs_codes.h>
#include <ble/core/auto/ble_hci_le.h>
#include <ble/core/auto/ble_gatt_aci.h>
#include <ble/core/auto/ble_gap_aci.h>
#include <string.h>
#include <stdlib.h>
@@ -315,11 +314,9 @@ void bc_free(BleCentral* bc) {
bool bc_scan_start(BleCentral* bc) {
if(bc->scanning) return true;
// Active scan, 50 ms interval / 30 ms window, public address, no duplicate
// filtering (so RSSI stays live), accept-all filter policy. The firmware's
// GAP is initialised, so we must use the GAP observation procedure; raw
// hci_le_set_scan_* returns "command disallowed" in that state.
tBleStatus st = aci_gap_start_observation_proc(0x0050, 0x0030, 0x01, OWN_ADDR_TYPE, 0x00, 0x00);
// active scan, 50 ms interval / 30 ms window; no duplicate filtering so RSSI stays live
tBleStatus st = hci_le_set_scan_parameters(0x01, 0x50, 0x30, OWN_ADDR_TYPE, 0x00);
if(st == BLE_STATUS_SUCCESS) st = hci_le_set_scan_enable(0x01, 0x00);
if(st != BLE_STATUS_SUCCESS) {
FURI_LOG_E(TAG, "scan start failed: 0x%02X", st);
bc->last_error = st;
@@ -331,7 +328,7 @@ bool bc_scan_start(BleCentral* bc) {
void bc_scan_stop(BleCentral* bc) {
if(!bc->scanning) return;
aci_gap_terminate_gap_proc(GAP_OBSERVATION_PROC);
hci_le_set_scan_enable(0x00, 0x00);
bc->scanning = false;
}
@@ -351,12 +348,10 @@ size_t bc_snapshot(BleCentral* bc, BcDevice* out, size_t max) {
d->logged = true;
}
furi_mutex_release(bc->mutex);
// Order by discovery time (first seen) so the list is stable and doesn't
// jump around as RSSI changes. Insertion sort, n <= 32.
for(size_t i = 1; i < n; i++) {
for(size_t i = 1; i < n; i++) { // insertion sort by RSSI, strongest first (n <= 32)
BcDevice d = out[i];
size_t j = i;
while(j > 0 && out[j - 1].first_seen > d.first_seen) {
while(j > 0 && out[j - 1].rssi < d.rssi) {
out[j] = out[j - 1];
j--;
}
@@ -412,11 +407,9 @@ bool bc_connect(BleCentral* bc, const BcDevice* dev, uint32_t timeout_ms) {
furi_event_flag_clear(bc->flags, FlagConnected | FlagConnFailed | FlagDisconnected | FlagDone);
bc->connecting = true;
bc->mtu = 23;
// Observation must fully stop before starting a connection procedure.
furi_delay_ms(100);
// scan 60/30 ms, conn interval 30-50 ms, latency 0, supervision 5 s
tBleStatus st = aci_gap_create_connection(
0x60, 0x30, dev->addr_type, dev->addr, OWN_ADDR_TYPE, 0x18, 0x28, 0, 0x01F4, 0, 0);
tBleStatus st = hci_le_create_connection(
0x60, 0x30, 0x00, dev->addr_type, dev->addr, OWN_ADDR_TYPE, 0x18, 0x28, 0, 0x01F4, 0, 0);
if(st != BLE_STATUS_SUCCESS) {
FURI_LOG_E(TAG, "create_connection failed: 0x%02X", st);
bc->connecting = false;
@@ -425,7 +418,7 @@ bool bc_connect(BleCentral* bc, const BcDevice* dev, uint32_t timeout_ms) {
}
uint32_t f = furi_event_flag_wait(bc->flags, FlagConnected | FlagConnFailed, FuriFlagWaitAny, timeout_ms);
if(f & FuriFlagError) {
aci_gap_terminate_gap_proc(GAP_DIRECT_CONNECTION_ESTABLISHMENT_PROC);
hci_le_create_connection_cancel();
f = furi_event_flag_wait(bc->flags, FlagConnected | FlagConnFailed, FuriFlagWaitAny, 2000);
if(!(f & FlagConnected)) {
bc->connecting = false;
Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 B

After

Width:  |  Height:  |  Size: 95 B

+2 -2
View File
@@ -146,7 +146,7 @@ static void draw_cb(Canvas* c, void* ctx) {
(unsigned long)(m->range_end / 1000 % 1000));
canvas_draw_str(c, 2, 20, buf);
snprintf(
buf, sizeof(buf), "pk %lu.%03luM %ddBm", (unsigned long)(m->peak_freq / 1000000),
buf, sizeof(buf), "peak %lu.%03lu MHz %d dBm", (unsigned long)(m->peak_freq / 1000000),
(unsigned long)(m->peak_freq / 1000 % 1000), (int)m->peak_rssi);
canvas_draw_str(c, 2, 29, buf);
@@ -159,7 +159,7 @@ static void draw_cb(Canvas* c, void* ctx) {
int h = (int)((r + 110) / 70.0f * gh);
canvas_draw_line(c, i, gy + gh - h, i, gy + gh);
}
canvas_draw_str(c, 2, 63, "OK:pause <>:band");
canvas_draw_str(c, 2, 63, "OK pause <>band Back exit");
furi_mutex_release(app->mutex);
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 B

After

Width:  |  Height:  |  Size: 95 B

+4 -2
View File
@@ -95,10 +95,12 @@ static void draw_cb(Canvas* c, void* ctx) {
canvas_clear(c);
canvas_set_font(c, FontSecondary);
snprintf(
buf, sizeof(buf), "%lu.%03lu +-%luk", (unsigned long)(m->center / 1000000),
buf, sizeof(buf), "%lu.%03lu MHz +-%lu kHz", (unsigned long)(m->center / 1000000),
(unsigned long)(m->center / 1000 % 1000), (unsigned long)(m->span / 2000));
canvas_draw_str(c, 1, 8, buf);
snprintf(buf, sizeof(buf), "%ddBm", (int)m->peak_rssi);
snprintf(
buf, sizeof(buf), "pk %lu.%03lu %d", (unsigned long)(m->peak_freq / 1000000),
(unsigned long)(m->peak_freq / 1000 % 1000), (int)m->peak_rssi);
canvas_draw_str_aligned(c, 127, 8, AlignRight, AlignBottom, buf);
// spectrum, top band 10..30