Files
flipper-bluepp/README.md
T
andy 73798387bb 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-05 01:41:28 +01:00

432 lines
22 KiB
Markdown

# Flipper Zero BLE tools
A suite of BLE, sub-GHz and power tools for the Flipper Zero. The BLE apps
share one central/GATT layer (`libble/`).
**Bluetooth (need the `build.sh` firmware)**
* **BT Inspector** — BLE scanner and GATT explorer, in the spirit of
BTInspector for iOS: discover devices, decode everything they broadcast, then
connect and inspect/read/write/subscribe, and log it all.
* **BLE HID Host** — connect a Bluetooth keyboard or mouse and watch its input
reports decoded live.
* **BLE Sensor Dashboard** — connect a sensor and watch its known-service
readings (battery, heart rate, temperature, humidity, pressure, ...) decoded
and updating live.
* **BLE Tracker Detector** — passively flag nearby item trackers (Apple Find
My / AirTag, Tile, Samsung SmartTag) with a "following you" warning.
* **BLE GATT Fuzzer** — enumerate a peripheral's whole GATT database, read every
readable value, and fuzz writable characteristics with boundary payloads,
logging every response. For authorised testing of devices you own.
* **BLE Beacon Toolkit** — broadcast an iBeacon or a raw advertising payload
with a chosen random MAC.
* **BLE GATT Server** — advertise the Flipper as a connectable serial GATT
peripheral and echo whatever a central writes.
**Sub-GHz (need the region unlock, on by default)**
* **Sub-GHz Scanner** — sweep RSSI across the full CC1101 range and log active
frequencies.
* **Sub-GHz Spectrum** — live spectrum bar graph and waterfall over a tunable
centre/span window.
**Tools**
* **Battery Health** — live fuel-gauge readings (voltage, current, temperature,
capacity, health, wear) with a voltage graph. Read-only.
> **Read this first.** These apps do not run on stock Flipper firmware. The
> Flipper's radio coprocessor ships with ST's "BLE Light" stack (advertise-only:
> it cannot scan or connect), and the sub-GHz region check blocks transmit
> off-region. They are delivered as a **custom firmware build** that swaps in
> ST's full BLE stack, exports the BLE command API to apps, and unlocks the
> sub-GHz region; the apps themselves are ordinary `.fap` files. `build.sh` does
> all of this. See [Why a custom firmware build](#why-a-custom-firmware-build).
> Battery Health is the only one that would also run on stock firmware.
## Features
* **Live scan list.** Every BLE advertiser in range, sorted by RSSI, refreshed
every 300 ms as you move around. Shows name (or "(no name)"), RSSI in dBm
with signal bars, address and vendor. Devices unseen for 60 s drop off.
* **Device page.** Address and address type, current/best RSSI and packet
count, advertising type (connectable or not), flags, advertised service UUIDs
(16/32/128-bit, named when known), service data, Tx power, appearance,
manufacturer data and raw advertisement / scan-response bytes. Updates live.
* **Identification.** Manufacturer IDs map to vendor names. Decoders for:
* Apple Continuity: iBeacon (UUID/major/minor/Tx), Proximity Pairing with
AirPods/Beats **model name** and **left / right / case battery** and
charging state, Nearby Info (device activity), Nearby Action, Find My,
AirDrop, Handoff, AirPlay, tethering.
* Microsoft Connected Devices beacon: device type (Windows desktop/laptop,
Xbox, Surface Hub, Android, iPhone, ...).
* Google Fast Pair model ID, Eddystone UID/URL/TLM (URL decoded), COVID
Exposure Notification.
* **Connect and interrogate.** OK on a connectable device connects (MTU is
negotiated), discovers all primary services, then characteristics with their
properties (`R`ead, `W`rite, `w`rite-without-response, `N`otify, `I`ndicate,
`B`roadcast).
* **Read device info** (first item in the service list): reads every readable
characteristic of the Generic Access, Device Information and Battery services
in one go: device name, appearance, model number, serial number, firmware /
hardware / software revision, manufacturer name, PnP ID, battery level.
* **Characteristic page.** Read any characteristic, including custom 128-bit
ones. Values are shown as hex plus decoded forms: printable string, u8/i8,
u16, u32, and SIG-specific formats (battery %, appearance, preferred
connection parameters, PnP ID, heart rate, temperature, humidity, pressure,
Tx power, date/time). Long values are fetched with read-long.
* **Write** a value back as text, hex bytes or a number (little-endian, sized to
the current value or the magnitude). Uses write-with-response when the
characteristic supports it, otherwise write-without-response. A readable
characteristic is re-read after a write.
* **Subscribe** to notifications / indications (the CCCD is located
automatically). Incoming values update the page and the history.
* **Value history.** Each value seen for the open characteristic (reads and
notifications) is kept with a timestamp, newest first.
* **Session log.** Every event is written with a date/time stamp to
`SD:/apps_data/bt_inspector/bti_YYYYMMDD_HHMMSS.log`: devices found (and
renamed), connects, services, characteristics, reads, writes, notifications,
errors. Copy it off the SD card (or via qFlipper) for analysis.
## Building and installing
Requirements: Linux or macOS with `git`, `python3` and a few GB of disk. The
firmware build system downloads its own ARM toolchain.
```sh
./build.sh # clone firmware 1.4.3, export the BLE API, build firmware + .fap
./build.sh flash # same, then flash over USB (Flipper connected, qFlipper closed)
./build.sh fap # rebuild only the .fap after editing the app
```
Options via environment variables: `FW_TAG` (firmware tag, default `1.4.3`),
`FW_DIR` (where to clone), `JOBS` (parallel compile jobs, default 4),
`BLE_API` (which ST API to export, see below).
The result is a standard update package in
`flipperzero-firmware/dist/f7-C/f7-update-local/`. It contains the firmware,
the **full radio stack**, and the app as `apps/Bluetooth/bt_inspector.fap` in
its resources. To install without USB flashing, copy that folder to the SD card
(for example `SD:/update/`) and open it in the Flipper's file browser.
Installing an official firmware later restores the light stack the same way.
The `.fap` alone is at `flipperzero-firmware/build/f7-firmware-C/.extapps/`;
after the firmware is installed once, app updates are just a file copy to
`SD:/apps/Bluetooth/`. It only loads on this firmware (API version 87.2 with
the exported symbols); the stock loader rejects it with an API mismatch.
To build the app with `ufbt` instead of the firmware tree, point `ufbt` at the
SDK the build produces:
```sh
ufbt update --local=flipperzero-firmware/dist/f7-C/flipper-z-f7-sdk-local.zip
cd bt_inspector && ufbt # -> dist/bt_inspector.fap ; `ufbt launch` installs it
```
The first build takes 5-15 minutes; later builds are incremental.
## Using the app
BT Inspector appears in the Flipper's main menu. While it runs, the Flipper's
own BLE advertising is stopped (and any phone connection dropped); it is
restored on exit.
| Screen | Up / Down | OK | Left | Right | Back |
|---|---|---|---|---|---|
| Scan list | select device | device page | | | exit app |
| Device page | scroll | Connect (if connectable) | | | scan list |
| Services | select | open service / read device info | | | disconnect, device page |
| Characteristics | select | open characteristic (auto-read) | | | services |
| Characteristic | scroll | Read | Notify / Unsub | Write | characteristics |
| Write menu | select | Text / Hex bytes / Number | | | characteristic |
Status messages ("Read OK (5 B)", "Write failed: Write not permitted",
"Notifications on", ...) appear on the characteristic page.
On stock firmware the app opens with an explanation instead of scanning.
## Log format
One event per line:
```
[2026-09-11 09:12:03] START stack=full advertising_was=1
[2026-09-11 09:12:04] DEVICE 5C:F3:70:9A:11:20 ADV_IND rssi=-58 name="Living Room" vendor=Apple adv=02 01 1A 0B FF 4C 00 ...
[2026-09-11 09:12:20] CONNECTING Living Room
[2026-09-11 09:12:21] CONNECTED Living Room mtu=247
[2026-09-11 09:12:21] SERVICE 180A Device Information handles 0x0010-0x001C
[2026-09-11 09:12:25] CHAR 2A26 handle 0x0016 props 0x02
[2026-09-11 09:12:26] READ 0x0016 = 31 2E 32 2E 30
[2026-09-11 09:12:40] WRITE 0x0021 = 01
[2026-09-11 09:12:41] NOTIFY 0x0024 = 64
[2026-09-11 09:13:00] DISCONNECT
```
A `DEVICE` line is written when a device is first seen and again when its name
changes. `INFO` lines come from "Read device info".
## Why a custom firmware build
Three facts about the Flipper Zero drive the design:
1. The STM32WB55's second core runs a prebuilt ST BLE stack. Flipper ships
`stm32wb5x_BLE_Stack_light_fw.bin` (peripheral + broadcaster only). ST's
`stm32wb5x_BLE_Stack_full_fw.bin` adds observer and central roles. The
firmware build system already supports it (`COPRO_STACK_TYPE=ble_full`) and
the firmware recognises it at runtime (`FuriHalBtStackFull`).
2. External `.fap` apps can only call functions listed in the firmware's API
table. The ST `aci_*` / `hci_*` command functions are not in it, so a stock
`.fap` cannot start a scan or a connection. `build.sh` adds ST's command
headers to the SDK (`lib/stm32wb.scons`), adds `extern "C"` guards to them
(the API table is C++), lets `fbt` regenerate `api_symbols.csv`, and enables
the selected functions. The API minor version becomes 87.2.
3. Every exported function is kept in the firmware image, so `BLE_API` trades
generality for flash. The full stack loads at 0x080CE000 (light: 0x080D7000);
the space between the firmware and the stack is the Flipper's internal
storage:
| `BLE_API` | exported | firmware | pages left, full stack | pages left, extended stack |
|---|---|---|---|---|
| `min` (app's own 15 imports) | 15 | 770 KB | 18 | 9 |
| `hci,gatt` (default) | 127 | 779 KB | 15 | 6 |
| `all` | 211 | ~788 KB | 13 | 4 |
| stock light-stack firmware | 0 | 768 KB | 27 | n/a |
Any comma list of `hci,gap,gatt,hal,l2cap` works. `extended` refers to
`stm32wb5x_BLE_Stack_full_extended_fw.bin` (BLE 5 extended advertising,
Coded PHY), which loads at 0x080C5000; it is not selected by this script.
### Extra firmware APIs exported to apps
Beyond the ST BLE commands, `build.sh` enables a set of firmware functions that
stock firmware keeps internal, so any `.fap` on this firmware can use them. The
set is a list of name patterns in `build.sh` (`EXPORT_ENABLE`); each enabled
symbol costs one API-table entry. Only
symbols named in `EXPORT_ENABLE` (plus the selected ST BLE commands) are
exported; headers added for their types do not drag in every function they
declare.
* **BLE peripheral internals** (`gap_*`, safe `ble_glue_*`, `furi_hal_bt_init`):
drive GAP directly, run a custom GATT server, change the advertised name and
the extra-beacon config, read connection state and radio-stack status,
without going through the firmware's serial profile. The coprocessor
firmware-update calls (`ble_glue_fus_stack_delete/install`) are deliberately
left disabled, since they can erase the radio stack.
* **Power / battery internals**: the gas-gauge readings (voltage, current,
temperature, remaining/full/design capacity, health, charge state) are
already exported by stock firmware. This build also enables
`furi_hal_power_insomnia_level`; `furi_hal_power_init` stays disabled.
* **Flash + option-byte internals** (`furi_hal_flash_*`): flash geometry,
free-page info, and raw program / erase / dword-write / option-byte access.
An app can read and modify internal flash directly. **This can brick the
device or wipe storage.** Enabled here for research; build with
`EXPORT_FLASH=0 ./build.sh` to leave these disabled.
* **RPC + loader internals**: the public `rpc_*` and `loader_*` APIs (RPC
session open/feed/close, app data exchange, launching and enqueuing apps,
lock/unlock, showing the app menu) are already exported by stock firmware and
are the supported way to script the Flipper from a host or launch other apps.
This build also adds the internal loader headers (`loader_i.h`,
`loader_menu.h`, `loader_queue.h`, `loader_applications.h`) so apps can use
the internal Loader types. The internal helper *functions* stay unexported:
they are not in the firmware's API-table link, so exporting them would break
the firmware build; only the public API is callable. `rpc_i.h` is not added
because it depends on the protobuf headers, which are not in the SDK.
`build.sh` also widens the GAP roles the firmware initialises
(`GAP_PERIPHERAL_ROLE | GAP_CENTRAL_ROLE | GAP_OBSERVER_ROLE`) in
`targets/f7/ble_glue/gap.c`. The app itself uses HCI-level scan and connect
commands, so this is belt-and-braces.
## How it works
```
bt_inspector.c UI (ViewDispatcher: scan list, text page, menus, inputs),
worker thread for blocking BLE operations, session log
ble_central.c scan / connect / GATT client on top of the ST stack.
Registers a handler with the firmware's BLE event dispatcher,
acknowledges its own events (advertising reports, our
connection's events) so the firmware's peripheral GAP never
sees them, and turns async ATT events into synchronous calls
ble_names.c vendor / UUID / appearance tables and value decoders
```
The app registers for BLE events, stops the Flipper's own advertising, then
issues `hci_le_set_scan_*`, `hci_le_create_connection` and `aci_gatt_*`
commands directly. Connection-complete events are distinguished from the
firmware's peripheral connections by the role field. Advertising reports are
parsed from the raw HCI payload (the ST header's `Advertising_Report_t` is not a
wire overlay).
## Sub-GHz region unlock (research)
`build.sh` also patches the firmware's sub-GHz region gate so transmit is
allowed on any frequency the CC1101 can tune, regardless of the device's
provisioned region: `furi_hal_region_is_frequency_allowed` and
`furi_hal_region_is_provisioned` are forced true. This is what most "unlocked"
Flipper firmwares do. The CC1101 PLL bands (about 300-348, 387-464 and
779-928 MHz) remain, because they are a hardware limit, not a regulatory one.
**Transmitting outside the frequencies and power levels allocated to you may be
illegal in your country. This is provided for research and authorised testing
only; you are responsible for operating within the law.** Build with
`SUBGHZ_UNLOCK=0 ./build.sh` to leave the region limits in place.
## Limitations
**Shared / hardware**
* **BLE only.** The STM32WB55 has no Bluetooth Classic (BR/EDR), so classic
devices cannot be seen or connected from any Flipper.
* **No pairing or bonding as central.** None of the central apps pair, so
anything gated behind an encrypted link is out of reach: encrypted
characteristics report "Insufficient auth" / "Insufficient encryption", and
keyboards that only send reports after bonding stay silent.
* At most two concurrent links (the firmware's BLE config), and while a BLE app
runs the Flipper's own advertising is stopped and any phone link dropped, then
restored on exit.
* The apps do not run at the same time; each one owns the radio while open.
* Built and checked against firmware 1.4.3 as `.fap` apps; on-device behaviour
needs a Flipper running the firmware from `build.sh` (full stack + exported
API). The apps will not load on stock firmware (missing exported symbols).
**BT Inspector**
* Apple battery levels come from what AirPods/Beats broadcast. Battery of
iPhones/Macs is only exposed to paired Apple devices.
* Vendor, UUID and Apple model tables are curated subsets; unknown IDs are
shown as hex. Apple Continuity layouts are community-documented and may drift.
* Value history is kept for the open characteristic; the log file has all of it.
* iOS-only BTInspector features (Shortcuts, Live Activities, background scanning
mapped by location) have no Flipper equivalent.
**BLE HID Host**
* Report-mode devices are decoded heuristically (8+ byte reports as keyboard,
shorter as mouse) rather than by parsing the Report Map; unusual layouts
(consumer-control keys, multi-touch, gamepads) may decode oddly or not at all.
* Keyboard decoding covers the common USB HID usage page; media/consumer keys
and vendor usages show as raw `[XX]`. No key-repeat or text reconstruction,
just the reports as they arrive.
* Only input reports are shown; output reports (LEDs) and feature reports are
not driven.
**BLE Sensor Dashboard**
* Only characteristics with a known SIG UUID are shown, decoded by the shared
value decoders (battery, heart rate, temperature, humidity, pressure, Tx
power, time, appearance, PnP ID). Vendor-specific/custom sensor
characteristics are ignored, and each reading is a single field (no graphs or
unit conversions beyond what the decoder prints). Up to 12 readings.
**BLE Tracker Detector**
* Detection is signature-based (Apple Find My/AirTag manufacturer data, Tile and
Samsung SmartTag service UUIDs). Trackers using other schemes, or ones that
rotate their advertising identity, may be missed; a device that changes its
random address is counted as new. The "following" flag is a heuristic (seen
repeatedly over ~2 minutes), not proof, and there is no de-anonymisation.
* Passive scan only; it never connects to or interacts with a tracker.
**BLE GATT Fuzzer**
* The fuzz pass writes a small fixed set of boundary payloads (empty, 0x00,
0xFF, 20 bytes of 'A') to writable characteristics; it is not a coverage-
guided or mutation fuzzer and does not chase crashes. **Writing arbitrary
values can misconfigure or brick the target.** Use only on devices you own or
are authorised to test.
* Reads are truncated to the value length the peripheral returns; the on-screen
feed shows the first bytes, the log has the run.
**BLE Beacon Toolkit**
* iBeacon mode expects exactly 21 entered bytes (16 UUID + 2 major + 2 minor +
1 Tx) and wraps them in the Apple manufacturer AD; Raw mode sends the entered
bytes verbatim, up to 31. There is no validation that a raw payload is
well-formed AD. Config changes take effect on the next Start (Stop first).
* Legacy advertising only (no extended advertising / BLE 5 long range).
**BLE GATT Server**
* It exposes the firmware's fixed serial GATT service (a 128-bit service with RX
and TX characteristics) and echoes writes; it is not an arbitrary
user-defined GATT schema. Building custom services from a `.fap` is not
supported by the exported API. It takes over the BLE profile while open and
restores the default (phone/RPC) profile on exit.
**Sub-GHz Scanner / Spectrum**
* RSSI is read one frequency at a time through the CC1101 with a fixed OOK
preset and a short settle delay, so a sweep is slow and can miss brief
transmissions between steps; the reading reflects that preset's bandwidth, not
a true FFT. Frequencies are limited to the CC1101 PLL bands.
* These use the shared radio directly; do not run them alongside the stock
Sub-GHz app. Transmitting is not performed, but see the sub-GHz unlock note
below regarding legality.
**Battery Health**
* All figures come straight from the fuel-gauge IC; "wear" is derived from
full-vs-design capacity and is only a rough estimate. Read-only: it never
changes charge settings. It is the one app that also runs on stock firmware.
**Exported firmware APIs (unlocks)**
* Every exported function stays in the firmware image, so larger `BLE_API`
settings and the extra exports cost flash and shrink internal storage (see the
table above). `EXPORT_FLASH=0` and `SUBGHZ_UNLOCK=0` opt out.
* Flash / option-byte access is raw and unguarded: a misbehaving app can wipe
storage or brick the device. Nothing in these apps uses it; it is exposed for
other apps built against this SDK.
* RPC/loader internals expose only the internal *types*; the internal helper
functions are not exported (they are not in the firmware API-table link). The
public RPC and loader APIs remain the supported surface.
* **Sub-GHz unlock removes the region check only.** The CC1101 PLL bands
(~300-348 / 387-464 / 779-928 MHz) still bound what the radio can tune, and
transmitting outside your local allocation may be illegal. Research and
authorised testing only; you are responsible for legal operation.
* The exported ST BLE command API and these unlocks are compile-verified, not
exercised on hardware.
## Troubleshooting
* **"Radio stack has no scanning"** on launch: the light stack is installed.
Install the package produced by `build.sh`, which includes `radio.bin` with
the full stack.
* **Build killed / out of memory**: `JOBS=2 ./build.sh`.
* **App refuses to load ("API mismatch" / unresolved symbol)**: the firmware on
the device is not the one from `build.sh`, or it was built with a smaller
`BLE_API` than the app needs. Reinstall the update package.
* **"API version is still WIP"** during a build: `fbt` found symbols the script
did not classify. Open `targets/f7/api_symbols.csv`, replace the remaining
`?` marks with `+` or `-`, rerun.
* **Connect failed: Timeout**: the device may not be connectable (see the
advertising type on the device page), or it is out of range. Random-address
devices that rotate their address may need re-selecting from the list.
## The BLE HID Host app
Select a device from the scan list; the app connects, finds the HID service
(0x1812), sets the protocol mode (boot when the device offers boot reports,
otherwise report mode), and subscribes to its input-report characteristics.
Keyboard reports are decoded to modifier + key names, mouse reports to button
state and dx/dy/wheel. Events scroll on screen (Up/Down to review history) and
are logged to `SD:/apps_data/ble_hid_host/hid_*.log` with the raw bytes. Back
disconnects and returns to scanning.
It cannot pair, so keyboards that require an encrypted link before sending
reports will connect but stay silent; open ones and most mice work.
## Layout
```
libble/ shared BLE central / GATT client + name & value decoders
bt_inspector/ BT Inspector app (application.fam, icon.png, bt_inspector.c)
ble_hid_host/ BLE HID Host app (HID-over-GATT setup + report decoding)
build.sh firmware build / flash script (stages libble into each app)
flipperzero-firmware/ created by build.sh (git-ignored)
```
`build.sh` copies `libble/*.{c,h}` into each app directory at build time, so
both apps compile against one source of truth. Add an app by dropping its
directory next to these and adding its name to `APPS=(...)` in `build.sh`.