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.
This commit is contained in:
@@ -0,0 +1,6 @@
|
|||||||
|
flipperzero-firmware/
|
||||||
|
.venv/
|
||||||
|
build.log
|
||||||
|
*.log
|
||||||
|
__pycache__/
|
||||||
|
dist/
|
||||||
@@ -0,0 +1,197 @@
|
|||||||
|
# BT Inspector for Flipper Zero
|
||||||
|
|
||||||
|
A BLE scanner and GATT explorer for the Flipper Zero, in the spirit of
|
||||||
|
BTInspector for iOS: discover nearby Bluetooth Low Energy devices, see their
|
||||||
|
signal strength and everything they broadcast, then connect and inspect their
|
||||||
|
services and characteristics: read, write, subscribe, and log it all.
|
||||||
|
|
||||||
|
> **Read this first.** This app cannot run on stock Flipper firmware. The
|
||||||
|
> Flipper's radio coprocessor ships with ST's "BLE Light" stack, which can only
|
||||||
|
> advertise (peripheral role). It cannot scan for or connect to other devices.
|
||||||
|
> BT Inspector is therefore delivered as a **custom firmware build** that swaps
|
||||||
|
> in ST's full BLE stack and compiles the app in. `build.sh` does all of this.
|
||||||
|
> See [Why a custom firmware build](#why-a-custom-firmware-build).
|
||||||
|
|
||||||
|
## 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, add the app, build with the full BLE stack
|
||||||
|
./build.sh flash # same, then flash over USB (Flipper connected, qFlipper closed)
|
||||||
|
```
|
||||||
|
|
||||||
|
Options via environment variables: `FW_TAG` (firmware tag, default `1.4.3`),
|
||||||
|
`FW_DIR` (where to clone), `JOBS` (parallel compile jobs, default 4).
|
||||||
|
|
||||||
|
The result is a standard update package in
|
||||||
|
`flipperzero-firmware/dist/f7-C/f7-update-local/`. 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: the updater installs the firmware **and the
|
||||||
|
full radio stack**. Installing an official firmware later restores the light
|
||||||
|
stack the same way.
|
||||||
|
|
||||||
|
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 `.fap`
|
||||||
|
cannot start a scan or a connection. Compiling the app into the firmware
|
||||||
|
(`FlipperAppType.APP`, added with `--extra-int-apps`) gives it access.
|
||||||
|
3. The full stack is 34 KB larger and loads at 0x080CE000 instead of
|
||||||
|
0x080D7000. The built image still leaves 12 flash pages (52 KB) for the
|
||||||
|
Flipper's internal storage (stock leaves 27).
|
||||||
|
|
||||||
|
`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).
|
||||||
|
|
||||||
|
## Limitations
|
||||||
|
|
||||||
|
* **BLE only.** The STM32WB55 has no Bluetooth Classic (BR/EDR), so classic
|
||||||
|
devices cannot be seen from any Flipper.
|
||||||
|
* 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.
|
||||||
|
* Pairing/bonding with the target is not implemented; characteristics that
|
||||||
|
require encryption report "Insufficient auth" / "Insufficient encryption".
|
||||||
|
* Tested by compiling into firmware 1.4.3; on-device behaviour needs a Flipper
|
||||||
|
with the full stack installed.
|
||||||
|
|
||||||
|
## 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`.
|
||||||
|
* **Updater link errors about `bt`/`rpc`**: the app manifest must not `require`
|
||||||
|
the `bt` service (the same app list is linked into the tiny updater image).
|
||||||
|
* **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.
|
||||||
|
|
||||||
|
## Layout
|
||||||
|
|
||||||
|
```
|
||||||
|
bt_inspector/ app sources + application.fam
|
||||||
|
build.sh firmware build / flash script
|
||||||
|
flipperzero-firmware/ created by build.sh (git-ignored)
|
||||||
|
```
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
# Built-in (firmware-linked) app: it needs the ST GAP/GATT/HCI command API,
|
||||||
|
# which the firmware does not export to .fap apps. See ../build.sh.
|
||||||
|
App(
|
||||||
|
appid="bt_inspector",
|
||||||
|
name="BT Inspector",
|
||||||
|
apptype=FlipperAppType.APP,
|
||||||
|
entry_point="bt_inspector_app",
|
||||||
|
requires=["gui", "storage"],
|
||||||
|
stack_size=4 * 1024,
|
||||||
|
icon="A_Plugins_14",
|
||||||
|
order=25,
|
||||||
|
)
|
||||||
@@ -0,0 +1,514 @@
|
|||||||
|
#include "ble_central.h"
|
||||||
|
#include <furi_hal.h>
|
||||||
|
#include <furi_ble/event_dispatcher.h>
|
||||||
|
#include <ble/ble.h>
|
||||||
|
#include <interface/patterns/ble_thread/tl/tl.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
|
||||||
|
#define TAG "BtInspector"
|
||||||
|
|
||||||
|
#define OWN_ADDR_TYPE GAP_PUBLIC_ADDR // firmware programs a public identity address
|
||||||
|
#define PROC_TIMEOUT_MS 5000
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
FlagConnected = 1 << 0,
|
||||||
|
FlagConnFailed = 1 << 1,
|
||||||
|
FlagDisconnected = 1 << 2,
|
||||||
|
FlagDone = 1 << 3,
|
||||||
|
} BcFlag;
|
||||||
|
|
||||||
|
struct BleCentral {
|
||||||
|
FuriMutex* mutex; // guards devices[]
|
||||||
|
FuriEventFlag* flags;
|
||||||
|
GapSvcEventHandler* handler;
|
||||||
|
BcNotifyCb notify_cb;
|
||||||
|
void* notify_ctx;
|
||||||
|
|
||||||
|
BcDevice devices[BC_MAX_DEVICES];
|
||||||
|
bool scanning;
|
||||||
|
|
||||||
|
volatile bool connecting;
|
||||||
|
volatile bool connected;
|
||||||
|
volatile uint16_t conn_handle;
|
||||||
|
uint16_t mtu;
|
||||||
|
uint8_t proc_error; // from ACI_GATT_PROC_COMPLETE
|
||||||
|
uint8_t att_error; // from ACI_GATT_ERROR_RESP
|
||||||
|
uint8_t last_error;
|
||||||
|
|
||||||
|
// procedure output buffers, owned by the caller of the running procedure
|
||||||
|
BcService* svc_out;
|
||||||
|
size_t svc_max, svc_count;
|
||||||
|
BcChar* chr_out;
|
||||||
|
size_t chr_max, chr_count;
|
||||||
|
uint8_t* val_out;
|
||||||
|
size_t val_max, val_len;
|
||||||
|
uint16_t found_cccd;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---- advertising -----------------------------------------------------------
|
||||||
|
|
||||||
|
const uint8_t* bc_adv_find(const uint8_t* data, uint8_t len, uint8_t type, uint8_t* out_len) {
|
||||||
|
uint8_t i = 0;
|
||||||
|
while(i < len) {
|
||||||
|
uint8_t l = data[i];
|
||||||
|
if(l == 0 || i + 1 + l > len) break;
|
||||||
|
if(data[i + 1] == type) {
|
||||||
|
*out_len = l - 1;
|
||||||
|
return data + i + 2;
|
||||||
|
}
|
||||||
|
i += 1 + l;
|
||||||
|
}
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void update_name(BcDevice* d) {
|
||||||
|
uint8_t l = 0;
|
||||||
|
const uint8_t* n = bc_adv_find(d->adv, d->adv_len, 0x09, &l);
|
||||||
|
if(!n) n = bc_adv_find(d->rsp, d->rsp_len, 0x09, &l);
|
||||||
|
if(!n) n = bc_adv_find(d->adv, d->adv_len, 0x08, &l);
|
||||||
|
if(!n) n = bc_adv_find(d->rsp, d->rsp_len, 0x08, &l);
|
||||||
|
if(!n) return;
|
||||||
|
if(l > BC_NAME_MAX) l = BC_NAME_MAX;
|
||||||
|
if(strlen(d->name) == l && memcmp(d->name, n, l) == 0) return;
|
||||||
|
memcpy(d->name, n, l);
|
||||||
|
d->name[l] = 0;
|
||||||
|
d->logged = false; // re-log on name change
|
||||||
|
}
|
||||||
|
|
||||||
|
static void handle_adv_report(BleCentral* bc, const uint8_t* p, size_t len) {
|
||||||
|
if(len < 1) return;
|
||||||
|
uint8_t n = p[0];
|
||||||
|
size_t i = 1;
|
||||||
|
uint32_t now = furi_get_tick();
|
||||||
|
furi_mutex_acquire(bc->mutex, FuriWaitForever);
|
||||||
|
while(n-- && i + 9 <= len) {
|
||||||
|
uint8_t evt = p[i], at = p[i + 1];
|
||||||
|
const uint8_t* addr = p + i + 2;
|
||||||
|
uint8_t dl = p[i + 8];
|
||||||
|
const uint8_t* data = p + i + 9;
|
||||||
|
if(i + 9 + dl + 1 > len) break;
|
||||||
|
int8_t rssi = (int8_t)p[i + 9 + dl];
|
||||||
|
i += 10 + dl;
|
||||||
|
if(at > 1) at -= 2; // resolved private -> underlying type
|
||||||
|
|
||||||
|
BcDevice* d = NULL;
|
||||||
|
BcDevice* victim = NULL;
|
||||||
|
for(size_t k = 0; k < BC_MAX_DEVICES; k++) {
|
||||||
|
BcDevice* c = &bc->devices[k];
|
||||||
|
if(c->used && c->addr_type == at && memcmp(c->addr, addr, 6) == 0) {
|
||||||
|
d = c;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if(!victim || !c->used || (victim->used && c->last_seen < victim->last_seen)) victim = c;
|
||||||
|
}
|
||||||
|
if(!d) {
|
||||||
|
if(evt == SCAN_RSP) continue; // never seen its advertisement; wait for it
|
||||||
|
d = victim;
|
||||||
|
memset(d, 0, sizeof(*d));
|
||||||
|
d->used = true;
|
||||||
|
memcpy(d->addr, addr, 6);
|
||||||
|
d->addr_type = at;
|
||||||
|
d->first_seen = now;
|
||||||
|
d->rssi_max = rssi;
|
||||||
|
}
|
||||||
|
d->last_seen = now;
|
||||||
|
d->packets++;
|
||||||
|
d->rssi = rssi;
|
||||||
|
if(rssi > d->rssi_max) d->rssi_max = rssi;
|
||||||
|
if(dl > BC_ADV_MAX) dl = BC_ADV_MAX;
|
||||||
|
if(evt == SCAN_RSP) {
|
||||||
|
memcpy(d->rsp, data, dl);
|
||||||
|
d->rsp_len = dl;
|
||||||
|
} else {
|
||||||
|
d->evt_type = evt;
|
||||||
|
memcpy(d->adv, data, dl);
|
||||||
|
d->adv_len = dl;
|
||||||
|
}
|
||||||
|
update_name(d);
|
||||||
|
}
|
||||||
|
furi_mutex_release(bc->mutex);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- event handler (runs on the BLE glue thread) ---------------------------
|
||||||
|
|
||||||
|
static BleEventAckStatus bc_event_handler(void* event, void* context) {
|
||||||
|
BleCentral* bc = context;
|
||||||
|
hci_event_pckt* pkt = (hci_event_pckt*)((hci_uart_pckt*)event)->data;
|
||||||
|
|
||||||
|
switch(pkt->evt) {
|
||||||
|
case HCI_LE_META_EVT_CODE: {
|
||||||
|
evt_le_meta_event* meta = (evt_le_meta_event*)pkt->data;
|
||||||
|
if(meta->subevent == HCI_LE_ADVERTISING_REPORT_SUBEVT_CODE) {
|
||||||
|
handle_adv_report(bc, meta->data, pkt->plen - 1);
|
||||||
|
return BleEventAckFlowEnable;
|
||||||
|
}
|
||||||
|
if(meta->subevent == HCI_LE_CONNECTION_COMPLETE_SUBEVT_CODE ||
|
||||||
|
meta->subevent == HCI_LE_ENHANCED_CONNECTION_COMPLETE_SUBEVT_CODE) {
|
||||||
|
// both layouts start with Status, Connection_Handle, Role
|
||||||
|
hci_le_connection_complete_event_rp0* e = (void*)meta->data;
|
||||||
|
if(!bc->connecting) return BleEventNotAck;
|
||||||
|
if(e->Status == 0 && e->Role != 0) return BleEventNotAck; // we are slave: not ours
|
||||||
|
bc->connecting = false;
|
||||||
|
if(e->Status == 0) {
|
||||||
|
bc->conn_handle = e->Connection_Handle;
|
||||||
|
bc->connected = true;
|
||||||
|
furi_event_flag_set(bc->flags, FlagConnected);
|
||||||
|
} else {
|
||||||
|
bc->last_error = e->Status;
|
||||||
|
furi_event_flag_set(bc->flags, FlagConnFailed);
|
||||||
|
}
|
||||||
|
return BleEventAckFlowEnable;
|
||||||
|
}
|
||||||
|
return BleEventNotAck;
|
||||||
|
}
|
||||||
|
case HCI_DISCONNECTION_COMPLETE_EVT_CODE: {
|
||||||
|
hci_disconnection_complete_event_rp0* e = (void*)pkt->data;
|
||||||
|
if(bc->connected && e->Connection_Handle == bc->conn_handle) {
|
||||||
|
bc->connected = false;
|
||||||
|
bc->conn_handle = 0;
|
||||||
|
furi_event_flag_set(bc->flags, FlagDisconnected);
|
||||||
|
return BleEventAckFlowEnable;
|
||||||
|
}
|
||||||
|
return BleEventNotAck;
|
||||||
|
}
|
||||||
|
case HCI_VENDOR_SPECIFIC_DEBUG_EVT_CODE: {
|
||||||
|
evt_blecore_aci* aci = (evt_blecore_aci*)pkt->data;
|
||||||
|
uint16_t ch = aci->data[0] | (aci->data[1] << 8); // every event below starts with it
|
||||||
|
if(!bc->connected || ch != bc->conn_handle) return BleEventNotAck;
|
||||||
|
|
||||||
|
switch(aci->ecode) {
|
||||||
|
case ACI_ATT_READ_BY_GROUP_TYPE_RESP_VSEVT_CODE: {
|
||||||
|
aci_att_read_by_group_type_resp_event_rp0* e = (void*)aci->data;
|
||||||
|
uint8_t rl = e->Attribute_Data_Length;
|
||||||
|
if(!bc->svc_out || (rl != 6 && rl != 20)) break;
|
||||||
|
for(uint8_t i = 0; i + rl <= e->Data_Length && bc->svc_count < bc->svc_max; i += rl) {
|
||||||
|
const uint8_t* r = e->Attribute_Data_List + i;
|
||||||
|
BcService* s = &bc->svc_out[bc->svc_count++];
|
||||||
|
s->start = r[0] | (r[1] << 8);
|
||||||
|
s->end = r[2] | (r[3] << 8);
|
||||||
|
s->uuid_len = rl - 4;
|
||||||
|
memcpy(s->uuid, r + 4, s->uuid_len);
|
||||||
|
}
|
||||||
|
} break;
|
||||||
|
case ACI_ATT_READ_BY_TYPE_RESP_VSEVT_CODE: {
|
||||||
|
aci_att_read_by_type_resp_event_rp0* e = (void*)aci->data;
|
||||||
|
uint8_t rl = e->Handle_Value_Pair_Length;
|
||||||
|
if(!bc->chr_out || (rl != 7 && rl != 21)) break;
|
||||||
|
for(uint8_t i = 0; i + rl <= e->Data_Length && bc->chr_count < bc->chr_max; i += rl) {
|
||||||
|
const uint8_t* r = e->Handle_Value_Pair_Data + i;
|
||||||
|
BcChar* c = &bc->chr_out[bc->chr_count++];
|
||||||
|
memset(c, 0, sizeof(*c));
|
||||||
|
c->decl_handle = r[0] | (r[1] << 8);
|
||||||
|
c->props = r[2];
|
||||||
|
c->value_handle = r[3] | (r[4] << 8);
|
||||||
|
c->uuid_len = rl - 5;
|
||||||
|
memcpy(c->uuid, r + 5, c->uuid_len);
|
||||||
|
}
|
||||||
|
} break;
|
||||||
|
case ACI_ATT_FIND_INFO_RESP_VSEVT_CODE: {
|
||||||
|
aci_att_find_info_resp_event_rp0* e = (void*)aci->data;
|
||||||
|
uint8_t rl = e->Format == 1 ? 4 : 18;
|
||||||
|
for(uint8_t i = 0; i + rl <= e->Event_Data_Length; i += rl) {
|
||||||
|
const uint8_t* r = e->Handle_UUID_Pair + i;
|
||||||
|
if(e->Format == 1 && r[2] == 0x02 && r[3] == 0x29 && !bc->found_cccd)
|
||||||
|
bc->found_cccd = r[0] | (r[1] << 8);
|
||||||
|
}
|
||||||
|
} break;
|
||||||
|
case ACI_ATT_READ_RESP_VSEVT_CODE:
|
||||||
|
case ACI_ATT_READ_BLOB_RESP_VSEVT_CODE: {
|
||||||
|
aci_att_read_resp_event_rp0* e = (void*)aci->data;
|
||||||
|
if(!bc->val_out) break;
|
||||||
|
size_t n = e->Event_Data_Length;
|
||||||
|
if(bc->val_len + n > bc->val_max) n = bc->val_max - bc->val_len;
|
||||||
|
memcpy(bc->val_out + bc->val_len, e->Attribute_Value, n);
|
||||||
|
bc->val_len += n;
|
||||||
|
} break;
|
||||||
|
case ACI_ATT_EXCHANGE_MTU_RESP_VSEVT_CODE: {
|
||||||
|
aci_att_exchange_mtu_resp_event_rp0* e = (void*)aci->data;
|
||||||
|
bc->mtu = e->Server_RX_MTU;
|
||||||
|
} break;
|
||||||
|
case ACI_GATT_ERROR_RESP_VSEVT_CODE: {
|
||||||
|
aci_gatt_error_resp_event_rp0* e = (void*)aci->data;
|
||||||
|
bc->att_error = e->Error_Code;
|
||||||
|
} break;
|
||||||
|
case ACI_GATT_PROC_COMPLETE_VSEVT_CODE: {
|
||||||
|
aci_gatt_proc_complete_event_rp0* e = (void*)aci->data;
|
||||||
|
bc->proc_error = e->Error_Code;
|
||||||
|
furi_event_flag_set(bc->flags, FlagDone);
|
||||||
|
} break;
|
||||||
|
case ACI_GATT_PROC_TIMEOUT_VSEVT_CODE:
|
||||||
|
bc->proc_error = 0xFE;
|
||||||
|
furi_event_flag_set(bc->flags, FlagDone);
|
||||||
|
break;
|
||||||
|
case ACI_GATT_INDICATION_VSEVT_CODE:
|
||||||
|
case ACI_GATT_NOTIFICATION_VSEVT_CODE: { // same layout
|
||||||
|
if(aci->ecode == ACI_GATT_INDICATION_VSEVT_CODE) aci_gatt_confirm_indication(bc->conn_handle);
|
||||||
|
aci_gatt_notification_event_rp0* e = (void*)aci->data;
|
||||||
|
if(bc->notify_cb)
|
||||||
|
bc->notify_cb(
|
||||||
|
e->Attribute_Handle, e->Attribute_Value, e->Attribute_Value_Length,
|
||||||
|
bc->notify_ctx);
|
||||||
|
} break;
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
return BleEventAckFlowEnable;
|
||||||
|
}
|
||||||
|
default: return BleEventNotAck;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- lifecycle ---------------------------------------------------------------
|
||||||
|
|
||||||
|
bool bc_supported(void) {
|
||||||
|
return furi_hal_bt_get_radio_stack() == FuriHalBtStackFull;
|
||||||
|
}
|
||||||
|
|
||||||
|
BleCentral* bc_alloc(BcNotifyCb cb, void* ctx) {
|
||||||
|
BleCentral* bc = malloc(sizeof(BleCentral));
|
||||||
|
bc->mutex = furi_mutex_alloc(FuriMutexTypeNormal);
|
||||||
|
bc->flags = furi_event_flag_alloc();
|
||||||
|
bc->notify_cb = cb;
|
||||||
|
bc->notify_ctx = ctx;
|
||||||
|
bc->mtu = 23;
|
||||||
|
bc->handler = ble_event_dispatcher_register_svc_handler(bc_event_handler, bc);
|
||||||
|
return bc;
|
||||||
|
}
|
||||||
|
|
||||||
|
void bc_free(BleCentral* bc) {
|
||||||
|
bc_scan_stop(bc);
|
||||||
|
if(bc->connected) bc_disconnect(bc);
|
||||||
|
ble_event_dispatcher_unregister_svc_handler(bc->handler);
|
||||||
|
furi_event_flag_free(bc->flags);
|
||||||
|
furi_mutex_free(bc->mutex);
|
||||||
|
free(bc);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- scanning ----------------------------------------------------------------
|
||||||
|
|
||||||
|
bool bc_scan_start(BleCentral* bc) {
|
||||||
|
if(bc->scanning) return true;
|
||||||
|
// 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;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
bc->scanning = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void bc_scan_stop(BleCentral* bc) {
|
||||||
|
if(!bc->scanning) return;
|
||||||
|
hci_le_set_scan_enable(0x00, 0x00);
|
||||||
|
bc->scanning = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int cmp_rssi(const void* a, const void* b) {
|
||||||
|
return ((const BcDevice*)b)->rssi - ((const BcDevice*)a)->rssi;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t bc_snapshot(BleCentral* bc, BcDevice* out, size_t max) {
|
||||||
|
size_t n = 0;
|
||||||
|
uint32_t now = furi_get_tick();
|
||||||
|
furi_mutex_acquire(bc->mutex, FuriWaitForever);
|
||||||
|
for(size_t k = 0; k < BC_MAX_DEVICES && n < max; k++) {
|
||||||
|
BcDevice* d = &bc->devices[k];
|
||||||
|
if(!d->used) continue;
|
||||||
|
if(now - d->last_seen > BC_STALE_MS) {
|
||||||
|
d->used = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
out[n++] = *d;
|
||||||
|
d->logged = true;
|
||||||
|
}
|
||||||
|
furi_mutex_release(bc->mutex);
|
||||||
|
qsort(out, n, sizeof(BcDevice), cmp_rssi);
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool bc_get_device(BleCentral* bc, const uint8_t* addr, BcDevice* out) {
|
||||||
|
bool found = false;
|
||||||
|
furi_mutex_acquire(bc->mutex, FuriWaitForever);
|
||||||
|
for(size_t k = 0; k < BC_MAX_DEVICES; k++) {
|
||||||
|
BcDevice* d = &bc->devices[k];
|
||||||
|
if(d->used && memcmp(d->addr, addr, 6) == 0) {
|
||||||
|
*out = *d;
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
furi_mutex_release(bc->mutex);
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- connection --------------------------------------------------------------
|
||||||
|
|
||||||
|
static bool wait_done(BleCentral* bc, uint32_t timeout) {
|
||||||
|
uint32_t f = furi_event_flag_wait(bc->flags, FlagDone | FlagDisconnected, FuriFlagWaitAny, timeout);
|
||||||
|
if(f & FuriFlagError) {
|
||||||
|
bc->last_error = 0xFE;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if(f & FlagDisconnected) {
|
||||||
|
bc->last_error = 0xFD;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if(bc->proc_error) {
|
||||||
|
bc->last_error = bc->att_error ? bc->att_error : bc->proc_error;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void proc_begin(BleCentral* bc) {
|
||||||
|
furi_event_flag_clear(bc->flags, FlagDone);
|
||||||
|
bc->proc_error = 0;
|
||||||
|
bc->att_error = 0;
|
||||||
|
bc->last_error = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool bc_connect(BleCentral* bc, const BcDevice* dev, uint32_t timeout_ms) {
|
||||||
|
if(bc->connected) return true;
|
||||||
|
bc_scan_stop(bc);
|
||||||
|
furi_event_flag_clear(bc->flags, FlagConnected | FlagConnFailed | FlagDisconnected | FlagDone);
|
||||||
|
bc->connecting = true;
|
||||||
|
bc->mtu = 23;
|
||||||
|
// scan 60/30 ms, conn interval 30-50 ms, latency 0, supervision 5 s
|
||||||
|
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;
|
||||||
|
bc->last_error = st;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
uint32_t f = furi_event_flag_wait(bc->flags, FlagConnected | FlagConnFailed, FuriFlagWaitAny, timeout_ms);
|
||||||
|
if(f & FuriFlagError) {
|
||||||
|
hci_le_create_connection_cancel();
|
||||||
|
f = furi_event_flag_wait(bc->flags, FlagConnected | FlagConnFailed, FuriFlagWaitAny, 2000);
|
||||||
|
if(!(f & FlagConnected)) {
|
||||||
|
bc->connecting = false;
|
||||||
|
bc->last_error = 0xFE;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(!(f & FlagConnected)) return false;
|
||||||
|
|
||||||
|
proc_begin(bc);
|
||||||
|
if(aci_gatt_exchange_config(bc->conn_handle) == BLE_STATUS_SUCCESS) wait_done(bc, 2000);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void bc_disconnect(BleCentral* bc) {
|
||||||
|
if(!bc->connected) return;
|
||||||
|
furi_event_flag_clear(bc->flags, FlagDisconnected);
|
||||||
|
if(hci_disconnect(bc->conn_handle, 0x13) == BLE_STATUS_SUCCESS)
|
||||||
|
furi_event_flag_wait(bc->flags, FlagDisconnected, FuriFlagWaitAny, 3000);
|
||||||
|
bc->connected = false;
|
||||||
|
bc->conn_handle = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool bc_is_connected(BleCentral* bc) {
|
||||||
|
return bc->connected;
|
||||||
|
}
|
||||||
|
uint16_t bc_mtu(BleCentral* bc) {
|
||||||
|
return bc->mtu;
|
||||||
|
}
|
||||||
|
uint8_t bc_last_error(BleCentral* bc) {
|
||||||
|
return bc->last_error;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- GATT client -------------------------------------------------------------
|
||||||
|
|
||||||
|
int bc_discover_services(BleCentral* bc, BcService* out, size_t max) {
|
||||||
|
if(!bc->connected) return -1;
|
||||||
|
proc_begin(bc);
|
||||||
|
bc->svc_out = out;
|
||||||
|
bc->svc_max = max;
|
||||||
|
bc->svc_count = 0;
|
||||||
|
bool ok = aci_gatt_disc_all_primary_services(bc->conn_handle) == BLE_STATUS_SUCCESS &&
|
||||||
|
wait_done(bc, PROC_TIMEOUT_MS);
|
||||||
|
bc->svc_out = NULL;
|
||||||
|
return (ok || bc->svc_count) ? (int)bc->svc_count : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int bc_discover_chars(BleCentral* bc, const BcService* svc, BcChar* out, size_t max) {
|
||||||
|
if(!bc->connected) return -1;
|
||||||
|
proc_begin(bc);
|
||||||
|
bc->chr_out = out;
|
||||||
|
bc->chr_max = max;
|
||||||
|
bc->chr_count = 0;
|
||||||
|
bool ok = aci_gatt_disc_all_char_of_service(bc->conn_handle, svc->start, svc->end) ==
|
||||||
|
BLE_STATUS_SUCCESS &&
|
||||||
|
wait_done(bc, PROC_TIMEOUT_MS);
|
||||||
|
bc->chr_out = NULL;
|
||||||
|
return (ok || bc->chr_count) ? (int)bc->chr_count : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int bc_read(BleCentral* bc, uint16_t value_handle, uint8_t* out, size_t max) {
|
||||||
|
if(!bc->connected) return -1;
|
||||||
|
bc->val_out = out;
|
||||||
|
bc->val_max = max;
|
||||||
|
bc->val_len = 0;
|
||||||
|
proc_begin(bc);
|
||||||
|
bool ok = aci_gatt_read_char_value(bc->conn_handle, value_handle) == BLE_STATUS_SUCCESS &&
|
||||||
|
wait_done(bc, PROC_TIMEOUT_MS);
|
||||||
|
// A full-size response means there may be more: fetch the rest with read-long.
|
||||||
|
size_t chunk = bc->mtu - 1;
|
||||||
|
while(ok && bc->val_len % chunk == 0 && bc->val_len > 0 && bc->val_len < max) {
|
||||||
|
size_t before = bc->val_len;
|
||||||
|
proc_begin(bc);
|
||||||
|
bool more = aci_gatt_read_long_char_value(bc->conn_handle, value_handle, before) ==
|
||||||
|
BLE_STATUS_SUCCESS &&
|
||||||
|
wait_done(bc, PROC_TIMEOUT_MS);
|
||||||
|
if(!more || bc->val_len == before) break; // short/failed continuation: keep what we have
|
||||||
|
}
|
||||||
|
bc->val_out = NULL;
|
||||||
|
return ok ? (int)bc->val_len : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool bc_write(BleCentral* bc, uint16_t value_handle, const uint8_t* data, uint8_t len, bool with_resp) {
|
||||||
|
if(!bc->connected) return false;
|
||||||
|
proc_begin(bc);
|
||||||
|
tBleStatus st;
|
||||||
|
if(with_resp) {
|
||||||
|
st = aci_gatt_write_char_value(bc->conn_handle, value_handle, len, data);
|
||||||
|
if(st != BLE_STATUS_SUCCESS) {
|
||||||
|
bc->last_error = st;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return wait_done(bc, PROC_TIMEOUT_MS);
|
||||||
|
}
|
||||||
|
st = aci_gatt_write_without_resp(bc->conn_handle, value_handle, len, data);
|
||||||
|
bc->last_error = st;
|
||||||
|
return st == BLE_STATUS_SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool bc_set_notify(BleCentral* bc, BcChar* c, uint16_t end_handle, bool enable) {
|
||||||
|
if(!bc->connected) return false;
|
||||||
|
if(!c->cccd_handle) {
|
||||||
|
if(end_handle <= c->value_handle) return false;
|
||||||
|
proc_begin(bc);
|
||||||
|
bc->found_cccd = 0;
|
||||||
|
bool ok = aci_gatt_disc_all_char_desc(bc->conn_handle, c->value_handle + 1, end_handle) ==
|
||||||
|
BLE_STATUS_SUCCESS &&
|
||||||
|
wait_done(bc, PROC_TIMEOUT_MS);
|
||||||
|
if(!bc->found_cccd) {
|
||||||
|
if(ok) bc->last_error = 0x0A;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
c->cccd_handle = bc->found_cccd;
|
||||||
|
}
|
||||||
|
uint8_t v[2] = {0, 0};
|
||||||
|
if(enable) v[0] = (c->props & CHAR_PROP_NOTIFY) ? 0x01 : 0x02;
|
||||||
|
proc_begin(bc);
|
||||||
|
tBleStatus st = aci_gatt_write_char_desc(bc->conn_handle, c->cccd_handle, 2, v);
|
||||||
|
if(st != BLE_STATUS_SUCCESS) {
|
||||||
|
bc->last_error = st;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return wait_done(bc, PROC_TIMEOUT_MS);
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <furi.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
|
||||||
|
#define BC_MAX_DEVICES 32
|
||||||
|
#define BC_ADV_MAX 31
|
||||||
|
#define BC_NAME_MAX 29
|
||||||
|
#define BC_MAX_SERVICES 16
|
||||||
|
#define BC_MAX_CHARS 48
|
||||||
|
#define BC_VALUE_MAX 256
|
||||||
|
#define BC_STALE_MS 60000
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint8_t addr[6];
|
||||||
|
uint8_t addr_type; // 0 public, 1 random (resolved 2/3 mapped down)
|
||||||
|
uint8_t evt_type; // last non-SCAN_RSP advertising type
|
||||||
|
int8_t rssi;
|
||||||
|
int8_t rssi_max;
|
||||||
|
uint32_t first_seen; // furi ticks
|
||||||
|
uint32_t last_seen;
|
||||||
|
uint32_t packets;
|
||||||
|
uint8_t adv[BC_ADV_MAX];
|
||||||
|
uint8_t adv_len;
|
||||||
|
uint8_t rsp[BC_ADV_MAX];
|
||||||
|
uint8_t rsp_len;
|
||||||
|
char name[BC_NAME_MAX + 1];
|
||||||
|
bool used;
|
||||||
|
bool logged; // false until the UI has written it to the log
|
||||||
|
} BcDevice;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint16_t start, end;
|
||||||
|
uint8_t uuid_len;
|
||||||
|
uint8_t uuid[16];
|
||||||
|
} BcService;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint16_t decl_handle, value_handle, cccd_handle;
|
||||||
|
uint8_t props; // CHAR_PROP_* bits
|
||||||
|
uint8_t uuid_len;
|
||||||
|
uint8_t uuid[16];
|
||||||
|
} BcChar;
|
||||||
|
|
||||||
|
typedef void (*BcNotifyCb)(uint16_t value_handle, const uint8_t* data, uint8_t len, void* ctx);
|
||||||
|
|
||||||
|
typedef struct BleCentral BleCentral;
|
||||||
|
|
||||||
|
bool bc_supported(void); // true when the copro runs the full BLE stack
|
||||||
|
BleCentral* bc_alloc(BcNotifyCb cb, void* ctx);
|
||||||
|
void bc_free(BleCentral* bc);
|
||||||
|
|
||||||
|
bool bc_scan_start(BleCentral* bc);
|
||||||
|
void bc_scan_stop(BleCentral* bc);
|
||||||
|
// Copies live devices sorted by RSSI, evicts stale ones, marks returned ones as logged.
|
||||||
|
size_t bc_snapshot(BleCentral* bc, BcDevice* out, size_t max);
|
||||||
|
bool bc_get_device(BleCentral* bc, const uint8_t* addr, BcDevice* out);
|
||||||
|
|
||||||
|
bool bc_connect(BleCentral* bc, const BcDevice* dev, uint32_t timeout_ms);
|
||||||
|
void bc_disconnect(BleCentral* bc);
|
||||||
|
bool bc_is_connected(BleCentral* bc);
|
||||||
|
uint16_t bc_mtu(BleCentral* bc);
|
||||||
|
uint8_t bc_last_error(BleCentral* bc); // ATT/BLE error, 0xFE timeout, 0xFD disconnected
|
||||||
|
|
||||||
|
int bc_discover_services(BleCentral* bc, BcService* out, size_t max);
|
||||||
|
int bc_discover_chars(BleCentral* bc, const BcService* svc, BcChar* out, size_t max);
|
||||||
|
int bc_read(BleCentral* bc, uint16_t value_handle, uint8_t* out, size_t max);
|
||||||
|
bool bc_write(BleCentral* bc, uint16_t value_handle, const uint8_t* data, uint8_t len, bool with_resp);
|
||||||
|
// end_handle: last handle that can belong to this characteristic (next decl - 1, or service end)
|
||||||
|
bool bc_set_notify(BleCentral* bc, BcChar* c, uint16_t end_handle, bool enable);
|
||||||
|
|
||||||
|
// First AD structure of `type` in adv data; returns payload pointer or NULL.
|
||||||
|
const uint8_t* bc_adv_find(const uint8_t* data, uint8_t len, uint8_t type, uint8_t* out_len);
|
||||||
@@ -0,0 +1,418 @@
|
|||||||
|
#include "ble_names.h"
|
||||||
|
#include <string.h>
|
||||||
|
#include <ctype.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint16_t id;
|
||||||
|
const char* name;
|
||||||
|
} IdName;
|
||||||
|
|
||||||
|
static const char* lookup(const IdName* t, size_t n, uint16_t id) {
|
||||||
|
for(size_t i = 0; i < n; i++)
|
||||||
|
if(t[i].id == id) return t[i].name;
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
#define LOOKUP(t, id) lookup(t, COUNT_OF(t), id)
|
||||||
|
|
||||||
|
// ponytail: curated subset of the SIG company table; unknown IDs print as hex.
|
||||||
|
static const IdName companies[] = {
|
||||||
|
{0x0000, "Ericsson"}, {0x0001, "Nokia"}, {0x0002, "Intel"},
|
||||||
|
{0x0003, "IBM"}, {0x0004, "Toshiba"}, {0x0006, "Microsoft"},
|
||||||
|
{0x0008, "Motorola"}, {0x000A, "CSR"}, {0x000D, "Texas Instr."},
|
||||||
|
{0x000F, "Broadcom"}, {0x001D, "Qualcomm"}, {0x0025, "NXP"},
|
||||||
|
{0x0030, "ST Micro"}, {0x0046, "MediaTek"}, {0x0047, "Bluegiga"},
|
||||||
|
{0x004C, "Apple"}, {0x004F, "Nike"}, {0x0059, "Nordic Semi"},
|
||||||
|
{0x005D, "Realtek"}, {0x0065, "HP"}, {0x006B, "Polar"},
|
||||||
|
{0x0075, "Samsung"}, {0x0087, "Garmin"}, {0x009E, "Bose"},
|
||||||
|
{0x00C4, "LG"}, {0x00D2, "Dialog Semi"}, {0x00E0, "Google"},
|
||||||
|
{0x0110, "Nikon"}, {0x012D, "Sony"}, {0x0131, "Cypress"},
|
||||||
|
{0x0157, "Huami/Amazfit"}, {0x0171, "Amazon"}, {0x01DA, "Logitech"},
|
||||||
|
{0x02E5, "Espressif"}, {0x038F, "Xiaomi"}, {0x0499, "Ruuvi"},
|
||||||
|
{0x05A7, "Sonos"}, {0x0822, "Adafruit"},
|
||||||
|
};
|
||||||
|
|
||||||
|
static const IdName uuids[] = {
|
||||||
|
// Services
|
||||||
|
{0x1800, "Generic Access"}, {0x1801, "Generic Attribute"},
|
||||||
|
{0x1802, "Immediate Alert"}, {0x1803, "Link Loss"},
|
||||||
|
{0x1804, "Tx Power"}, {0x1805, "Current Time"},
|
||||||
|
{0x1808, "Glucose"}, {0x1809, "Health Thermometer"},
|
||||||
|
{0x180A, "Device Information"}, {0x180D, "Heart Rate"},
|
||||||
|
{0x180F, "Battery"}, {0x1810, "Blood Pressure"},
|
||||||
|
{0x1811, "Alert Notification"}, {0x1812, "HID"},
|
||||||
|
{0x1813, "Scan Parameters"}, {0x1814, "Running Speed/Cadence"},
|
||||||
|
{0x1815, "Automation IO"}, {0x1816, "Cycling Speed/Cadence"},
|
||||||
|
{0x1818, "Cycling Power"}, {0x1819, "Location/Navigation"},
|
||||||
|
{0x181A, "Environmental Sensing"}, {0x181B, "Body Composition"},
|
||||||
|
{0x181C, "User Data"}, {0x181D, "Weight Scale"},
|
||||||
|
{0x181E, "Bond Management"}, {0x181F, "Cont. Glucose Monitor"},
|
||||||
|
{0x1822, "Pulse Oximeter"}, {0x1824, "Transport Discovery"},
|
||||||
|
{0x1826, "Fitness Machine"}, {0x1827, "Mesh Provisioning"},
|
||||||
|
{0x1828, "Mesh Proxy"}, {0x1843, "Audio Input Control"},
|
||||||
|
{0x1844, "Volume Control"}, {0x1846, "Coordinated Set Id"},
|
||||||
|
{0x1848, "Media Control"}, {0x184E, "Audio Stream Control"},
|
||||||
|
{0x1850, "Published Audio Cap."}, {0x1854, "Hearing Access"},
|
||||||
|
{0x1855, "Telephony/Media Audio"},
|
||||||
|
{0xFE2C, "Google Fast Pair"}, {0xFEAA, "Eddystone"},
|
||||||
|
{0xFD6F, "Exposure Notification"}, {0xFE95, "Xiaomi Mi"},
|
||||||
|
{0xFDAB, "Xiaomi"}, {0xFEE7, "Tencent"},
|
||||||
|
{0xFE59, "Nordic DFU"}, {0xFEF5, "Dialog"},
|
||||||
|
{0xFE07, "Sonos"}, {0xFEBE, "Bose"},
|
||||||
|
{0xFEED, "Tile"}, {0xFEEC, "Tile"},
|
||||||
|
{0xFE9F, "Google"}, {0xFEF3, "Google Nearby"},
|
||||||
|
// Characteristics
|
||||||
|
{0x2A00, "Device Name"}, {0x2A01, "Appearance"},
|
||||||
|
{0x2A02, "Peripheral Privacy Flag"},{0x2A03, "Reconnection Address"},
|
||||||
|
{0x2A04, "Pref. Conn. Parameters"}, {0x2A05, "Service Changed"},
|
||||||
|
{0x2A06, "Alert Level"}, {0x2A07, "Tx Power Level"},
|
||||||
|
{0x2A08, "Date Time"}, {0x2A19, "Battery Level"},
|
||||||
|
{0x2A1C, "Temperature Measurement"},{0x2A23, "System ID"},
|
||||||
|
{0x2A24, "Model Number"}, {0x2A25, "Serial Number"},
|
||||||
|
{0x2A26, "Firmware Revision"}, {0x2A27, "Hardware Revision"},
|
||||||
|
{0x2A28, "Software Revision"}, {0x2A29, "Manufacturer Name"},
|
||||||
|
{0x2A2A, "IEEE Regulatory Cert"}, {0x2A2B, "Current Time"},
|
||||||
|
{0x2A37, "Heart Rate Measurement"}, {0x2A38, "Body Sensor Location"},
|
||||||
|
{0x2A39, "Heart Rate Control Pt"}, {0x2A4A, "HID Information"},
|
||||||
|
{0x2A4B, "Report Map"}, {0x2A4C, "HID Control Point"},
|
||||||
|
{0x2A4D, "Report"}, {0x2A4E, "Protocol Mode"},
|
||||||
|
{0x2A50, "PnP ID"}, {0x2A53, "RSC Measurement"},
|
||||||
|
{0x2A5B, "CSC Measurement"}, {0x2A63, "Cycling Power Meas."},
|
||||||
|
{0x2A6D, "Pressure"}, {0x2A6E, "Temperature"},
|
||||||
|
{0x2A6F, "Humidity"}, {0x2A9D, "Weight Measurement"},
|
||||||
|
{0x2AA6, "Central Addr Resolution"},{0x2AC9, "Resolvable Priv Addr Only"},
|
||||||
|
{0x2AD2, "Indoor Bike Data"}, {0x2B29, "Client Supported Features"},
|
||||||
|
{0x2B2A, "Database Hash"}, {0x2B3A, "Server Supported Features"},
|
||||||
|
// Descriptors
|
||||||
|
{0x2900, "Extended Properties"}, {0x2901, "User Description"},
|
||||||
|
{0x2902, "Client Char Config"}, {0x2903, "Server Char Config"},
|
||||||
|
{0x2904, "Presentation Format"}, {0x2906, "Valid Range"},
|
||||||
|
{0x2908, "Report Reference"},
|
||||||
|
};
|
||||||
|
|
||||||
|
static const char* appearances[] = {
|
||||||
|
"Unknown", "Phone", "Computer", "Watch",
|
||||||
|
"Clock", "Display", "Remote Control", "Eye-glasses",
|
||||||
|
"Tag", "Keyring", "Media Player", "Barcode Scanner",
|
||||||
|
"Thermometer", "Heart Rate Sensor", "Blood Pressure", "HID",
|
||||||
|
"Glucose Meter", "Running Sensor", "Cycling", "Control Device",
|
||||||
|
"Network Device", "Sensor", "Light Fixture", "Fan",
|
||||||
|
"HVAC", "Air Conditioning", "Humidifier", "Heating",
|
||||||
|
"Access Control", "Motorized Device", "Power Device", "Light Source",
|
||||||
|
"Window Covering", "Audio Sink", "Audio Source", "Motorized Vehicle",
|
||||||
|
"Domestic Appliance", "Wearable Audio", "Aircraft", "AV Equipment",
|
||||||
|
"Display Equipment", "Hearing Aid", "Gaming", "Signage",
|
||||||
|
};
|
||||||
|
|
||||||
|
static const char* hid_sub[] = {
|
||||||
|
"HID", "Keyboard", "Mouse", "Joystick", "Gamepad", "Digitizer",
|
||||||
|
"Card Reader", "Digital Pen", "Barcode Scanner", "Touchpad", "Presentation Remote"};
|
||||||
|
|
||||||
|
const char* ble_company_name(uint16_t id) {
|
||||||
|
return LOOKUP(companies, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
static const uint8_t sig_base[16] = {
|
||||||
|
0xFB, 0x34, 0x9B, 0x5F, 0x80, 0x00, 0x00, 0x80, 0x00, 0x10, 0x00, 0x00, 0, 0, 0x00, 0x00};
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
const char* uuid; // canonical text, upper case
|
||||||
|
const char* name;
|
||||||
|
} UuidName;
|
||||||
|
static const UuidName uuids128[] = {
|
||||||
|
{"6E400001-B5A3-F393-E0A9-E50E24DCCA9E", "Nordic UART"},
|
||||||
|
{"6E400002-B5A3-F393-E0A9-E50E24DCCA9E", "Nordic UART RX"},
|
||||||
|
{"6E400003-B5A3-F393-E0A9-E50E24DCCA9E", "Nordic UART TX"},
|
||||||
|
{"7905F431-B5CE-4E99-A40F-4B1E122D00D0", "Apple Notification Center"},
|
||||||
|
{"89D3502B-0F36-433A-8EF4-C502AD55F8DC", "Apple Media"},
|
||||||
|
{"D0611E78-BBB4-4591-A5F8-487910AE4366", "Apple Continuity"},
|
||||||
|
{"9FA480E0-4967-4542-9390-D343DC5D04AE", "Apple Nearby"},
|
||||||
|
{"8667556C-9A37-4C91-84ED-54EE27D90049", "Apple Continuity Char"},
|
||||||
|
{"AF0BADB1-5B99-43CD-917A-A77BC549E3CC", "Apple Nearby Char"},
|
||||||
|
};
|
||||||
|
|
||||||
|
const char* ble_uuid_name(const uint8_t* uuid, uint8_t len) {
|
||||||
|
if(len == 2) return LOOKUP(uuids, uuid[0] | (uuid[1] << 8));
|
||||||
|
if(len != 16) return NULL;
|
||||||
|
if(memcmp(uuid, sig_base, 12) == 0 && uuid[14] == 0 && uuid[15] == 0)
|
||||||
|
return LOOKUP(uuids, uuid[12] | (uuid[13] << 8));
|
||||||
|
FuriString* s = furi_string_alloc();
|
||||||
|
ble_uuid_str(s, uuid, 16);
|
||||||
|
const char* r = NULL;
|
||||||
|
for(size_t i = 0; i < COUNT_OF(uuids128); i++)
|
||||||
|
if(furi_string_cmp_str(s, uuids128[i].uuid) == 0) {
|
||||||
|
r = uuids128[i].name;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
furi_string_free(s);
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* ble_appearance_name(uint16_t a) {
|
||||||
|
uint16_t cat = a >> 6, sub = a & 0x3F;
|
||||||
|
if(cat == 15 && sub < COUNT_OF(hid_sub)) return hid_sub[sub];
|
||||||
|
if(cat < COUNT_OF(appearances)) return appearances[cat];
|
||||||
|
if(cat == 49) return "Pulse Oximeter";
|
||||||
|
if(cat == 50) return "Weight Scale";
|
||||||
|
if(cat == 52) return "Glucose Monitor";
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* ble_adv_type_name(uint8_t t) {
|
||||||
|
static const char* n[] = {"ADV_IND", "ADV_DIRECT_IND", "ADV_SCAN_IND", "ADV_NONCONN_IND", "SCAN_RSP"};
|
||||||
|
return t < COUNT_OF(n) ? n[t] : "?";
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* ble_att_error_name(uint8_t c) {
|
||||||
|
static const IdName e[] = {
|
||||||
|
{0x01, "Invalid handle"}, {0x02, "Read not permitted"},
|
||||||
|
{0x03, "Write not permitted"},{0x04, "Invalid PDU"},
|
||||||
|
{0x05, "Insufficient auth"}, {0x06, "Not supported"},
|
||||||
|
{0x07, "Invalid offset"}, {0x08, "Insufficient authz"},
|
||||||
|
{0x0A, "Attribute not found"},{0x0D, "Invalid length"},
|
||||||
|
{0x0E, "Unlikely error"}, {0x0F, "Insufficient encryption"},
|
||||||
|
{0x11, "Insufficient resources"},
|
||||||
|
{0xFD, "Disconnected"}, {0xFE, "Timeout"},
|
||||||
|
};
|
||||||
|
return LOOKUP(e, c);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ble_hex(FuriString* out, const uint8_t* data, size_t len) {
|
||||||
|
for(size_t i = 0; i < len; i++) furi_string_cat_printf(out, "%s%02X", i ? " " : "", data[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ble_uuid_str(FuriString* out, const uint8_t* u, uint8_t len) {
|
||||||
|
if(len == 2) {
|
||||||
|
furi_string_cat_printf(out, "%04X", u[0] | (u[1] << 8));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 128-bit UUIDs are little-endian on the wire
|
||||||
|
for(int i = 15; i >= 0; i--) {
|
||||||
|
furi_string_cat_printf(out, "%02X", u[i]);
|
||||||
|
if(i == 12 || i == 10 || i == 8 || i == 6) furi_string_cat_str(out, "-");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ble_addr_str(FuriString* out, const uint8_t* a) {
|
||||||
|
furi_string_cat_printf(
|
||||||
|
out, "%02X:%02X:%02X:%02X:%02X:%02X", a[5], a[4], a[3], a[2], a[1], a[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Apple Continuity -------------------------------------------------------
|
||||||
|
// ponytail: community-documented, unofficial; model/battery layouts may drift.
|
||||||
|
static const IdName apple_models[] = {
|
||||||
|
{0x0220, "AirPods"}, {0x0F20, "AirPods 2"}, {0x1320, "AirPods 3"},
|
||||||
|
{0x1920, "AirPods 4"}, {0x1B20, "AirPods 4 ANC"}, {0x0E20, "AirPods Pro"},
|
||||||
|
{0x1420, "AirPods Pro 2"}, {0x2420, "AirPods Pro 2 USB-C"},
|
||||||
|
{0x0A20, "AirPods Max"}, {0x1F20, "AirPods Max USB-C"},
|
||||||
|
{0x0320, "Powerbeats3"}, {0x0B20, "Powerbeats Pro"}, {0x1220, "Powerbeats 4"},
|
||||||
|
{0x0520, "BeatsX"}, {0x0620, "Beats Solo3"}, {0x0920, "Beats Studio3"},
|
||||||
|
{0x1020, "Beats Flex"}, {0x1120, "Beats Studio Buds"},{0x1620, "Beats Fit Pro"},
|
||||||
|
{0x1720, "Beats Studio Buds+"},{0x1820, "Beats Studio Pro"}, {0x1C20, "Beats Solo Buds"},
|
||||||
|
};
|
||||||
|
|
||||||
|
static const IdName apple_nearby_actions[] = {
|
||||||
|
{0x01, "Apple TV Setup"}, {0x04, "Mobile Backup"}, {0x05, "Watch Setup"},
|
||||||
|
{0x06, "Apple TV Pair"}, {0x07, "Internet Relay"}, {0x08, "WiFi Password"},
|
||||||
|
{0x09, "iOS Setup"}, {0x0A, "Repair"}, {0x0B, "Speaker Setup"},
|
||||||
|
{0x0C, "Apple Pay"}, {0x0D, "Whole Home Audio"}, {0x0E, "Dev Tools Pairing"},
|
||||||
|
{0x0F, "Answered Call"}, {0x10, "Ended Call"}, {0x11, "DD Ping"},
|
||||||
|
{0x12, "DD Pong"}, {0x13, "Remote AutoFill"},{0x14, "Companion Link"},
|
||||||
|
{0x15, "Remote Mgmt"}, {0x17, "Remote Display"},
|
||||||
|
};
|
||||||
|
|
||||||
|
static const IdName apple_nearby_info[] = {
|
||||||
|
{0x01, "Activity reporting off"}, {0x03, "Idle"},
|
||||||
|
{0x05, "Audio, screen off"}, {0x07, "Screen on"},
|
||||||
|
{0x09, "Video playing"}, {0x0A, "Watch on wrist"},
|
||||||
|
{0x0B, "Recent interaction"}, {0x0D, "Driving"},
|
||||||
|
{0x0E, "Phone call"},
|
||||||
|
};
|
||||||
|
|
||||||
|
static void pct(FuriString* out, const char* label, uint8_t nib) {
|
||||||
|
if(nib == 0xF)
|
||||||
|
furi_string_cat_printf(out, " %s:?", label);
|
||||||
|
else
|
||||||
|
furi_string_cat_printf(out, " %s:%u%%", label, nib * 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void decode_apple(FuriString* out, const uint8_t* d, uint8_t len) {
|
||||||
|
uint8_t i = 0;
|
||||||
|
while(i + 2 <= len) {
|
||||||
|
uint8_t type = d[i], l = d[i + 1];
|
||||||
|
const uint8_t* p = d + i + 2;
|
||||||
|
if(i + 2 + l > len) l = len - i - 2;
|
||||||
|
switch(type) {
|
||||||
|
case 0x02:
|
||||||
|
furi_string_cat_str(out, " iBeacon");
|
||||||
|
if(l >= 20) {
|
||||||
|
furi_string_cat_str(out, " ");
|
||||||
|
for(int k = 0; k < 16; k++) furi_string_cat_printf(out, "%02X", p[k]);
|
||||||
|
furi_string_cat_printf(
|
||||||
|
out, "\n major %u minor %u tx %d", (p[16] << 8) | p[17], (p[18] << 8) | p[19],
|
||||||
|
(int8_t)p[20]);
|
||||||
|
}
|
||||||
|
furi_string_cat_str(out, "\n");
|
||||||
|
break;
|
||||||
|
case 0x05: furi_string_cat_str(out, " AirDrop\n"); break;
|
||||||
|
case 0x07: {
|
||||||
|
furi_string_cat_str(out, " Proximity Pairing");
|
||||||
|
if(l >= 3) {
|
||||||
|
uint16_t model = (p[1] << 8) | p[2];
|
||||||
|
const char* m = LOOKUP(apple_models, model);
|
||||||
|
if(m)
|
||||||
|
furi_string_cat_printf(out, ": %s", m);
|
||||||
|
else
|
||||||
|
furi_string_cat_printf(out, ": model %04X", model);
|
||||||
|
}
|
||||||
|
if(l >= 6) {
|
||||||
|
furi_string_cat_str(out, "\n Battery");
|
||||||
|
pct(out, "L", p[4] >> 4);
|
||||||
|
pct(out, "R", p[4] & 0xF);
|
||||||
|
pct(out, "Case", p[5] & 0xF);
|
||||||
|
if(p[5] & 0x70)
|
||||||
|
furi_string_cat_printf(
|
||||||
|
out, " chg:%s%s%s", (p[5] & 0x20) ? "L" : "", (p[5] & 0x10) ? "R" : "",
|
||||||
|
(p[5] & 0x40) ? "C" : "");
|
||||||
|
}
|
||||||
|
furi_string_cat_str(out, "\n");
|
||||||
|
} break;
|
||||||
|
case 0x09: furi_string_cat_str(out, " AirPlay Target\n"); break;
|
||||||
|
case 0x0A: furi_string_cat_str(out, " AirPlay Source\n"); break;
|
||||||
|
case 0x0B: furi_string_cat_str(out, " Magic Switch (Watch)\n"); break;
|
||||||
|
case 0x0C: furi_string_cat_str(out, " Handoff\n"); break;
|
||||||
|
case 0x0D: furi_string_cat_str(out, " Tethering Target\n"); break;
|
||||||
|
case 0x0E: furi_string_cat_str(out, " Tethering Source\n"); break;
|
||||||
|
case 0x0F: {
|
||||||
|
const char* a = l >= 2 ? LOOKUP(apple_nearby_actions, p[1]) : NULL;
|
||||||
|
furi_string_cat_printf(out, " Nearby Action: %s\n", a ? a : "?");
|
||||||
|
} break;
|
||||||
|
case 0x10: {
|
||||||
|
const char* s = l >= 1 ? LOOKUP(apple_nearby_info, p[0] & 0x0F) : NULL;
|
||||||
|
furi_string_cat_printf(out, " Nearby Info: %s\n", s ? s : "iOS device");
|
||||||
|
} break;
|
||||||
|
case 0x12: furi_string_cat_str(out, " Find My\n"); break;
|
||||||
|
default: furi_string_cat_printf(out, " Type %02X (%u B)\n", type, l); break;
|
||||||
|
}
|
||||||
|
i += 2 + l;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Microsoft Connected Devices Platform beacon ---------------------------
|
||||||
|
static const IdName ms_types[] = {
|
||||||
|
{1, "Xbox One"}, {6, "iPhone"}, {7, "iPad"},
|
||||||
|
{8, "Android"}, {9, "Windows Desktop"}, {11, "Windows Phone"},
|
||||||
|
{12, "Linux"}, {13, "Windows IoT"}, {14, "Surface Hub"},
|
||||||
|
{15, "Windows Laptop"}, {16, "Windows Tablet"},
|
||||||
|
};
|
||||||
|
|
||||||
|
void ble_decode_mfg(FuriString* out, uint16_t company, const uint8_t* d, uint8_t len) {
|
||||||
|
if(company == 0x004C) {
|
||||||
|
decode_apple(out, d, len);
|
||||||
|
} else if(company == 0x0006 && len >= 2 && d[0] == 0x01) {
|
||||||
|
const char* t = LOOKUP(ms_types, d[1] & 0x1F);
|
||||||
|
furi_string_cat_printf(out, " Windows CDP: %s\n", t ? t : "?");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ble_decode_svc_data(FuriString* out, uint16_t uuid, const uint8_t* d, uint8_t len) {
|
||||||
|
if(uuid == 0xFE2C && len >= 3) {
|
||||||
|
furi_string_cat_printf(out, " Fast Pair model %02X%02X%02X\n", d[0], d[1], d[2]);
|
||||||
|
} else if(uuid == 0xFEAA && len >= 1) {
|
||||||
|
const char* f = d[0] == 0x00 ? "UID" : d[0] == 0x10 ? "URL" : d[0] == 0x20 ? "TLM" : "?";
|
||||||
|
furi_string_cat_printf(out, " Eddystone %s\n", f);
|
||||||
|
if(d[0] == 0x10 && len > 2) {
|
||||||
|
static const char* pre[] = {"http://www.", "https://www.", "http://", "https://"};
|
||||||
|
furi_string_cat_printf(out, " %s", d[2] < 4 ? pre[d[2]] : "");
|
||||||
|
for(uint8_t i = 3; i < len; i++)
|
||||||
|
if(isprint(d[i])) furi_string_push_back(out, d[i]);
|
||||||
|
furi_string_cat_str(out, "\n");
|
||||||
|
}
|
||||||
|
} else if(uuid == 0xFD6F) {
|
||||||
|
furi_string_cat_str(out, " COVID Exposure Notification\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool all_printable(const uint8_t* d, size_t len) {
|
||||||
|
if(!len) return false;
|
||||||
|
for(size_t i = 0; i < len; i++)
|
||||||
|
if(!isprint(d[i]) && d[i] != '\n' && d[i] != '\r' && d[i] != '\t') return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ble_decode_value(FuriString* out, const uint8_t* uuid, uint8_t uuid_len, const uint8_t* d, size_t len) {
|
||||||
|
uint16_t u = 0;
|
||||||
|
if(uuid_len == 2) u = uuid[0] | (uuid[1] << 8);
|
||||||
|
else if(uuid_len == 16 && memcmp(uuid, sig_base, 12) == 0) u = uuid[12] | (uuid[13] << 8);
|
||||||
|
|
||||||
|
switch(u) {
|
||||||
|
case 0x2A19:
|
||||||
|
if(len >= 1) furi_string_cat_printf(out, "%u%%\n", d[0]);
|
||||||
|
return;
|
||||||
|
case 0x2A01:
|
||||||
|
if(len >= 2) {
|
||||||
|
uint16_t a = d[0] | (d[1] << 8);
|
||||||
|
const char* n = ble_appearance_name(a);
|
||||||
|
furi_string_cat_printf(out, "%s (0x%04X)\n", n ? n : "?", a);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
case 0x2A04:
|
||||||
|
if(len >= 8)
|
||||||
|
furi_string_cat_printf(
|
||||||
|
out, "int %u-%u ms, lat %u, to %u ms\n", (d[0] | (d[1] << 8)) * 5 / 4,
|
||||||
|
(d[2] | (d[3] << 8)) * 5 / 4, d[4] | (d[5] << 8), (d[6] | (d[7] << 8)) * 10);
|
||||||
|
return;
|
||||||
|
case 0x2A50:
|
||||||
|
if(len >= 7)
|
||||||
|
furi_string_cat_printf(
|
||||||
|
out, "%s VID %04X PID %04X ver %04X\n", d[0] == 1 ? "SIG" : d[0] == 2 ? "USB" : "?",
|
||||||
|
d[1] | (d[2] << 8), d[3] | (d[4] << 8), d[5] | (d[6] << 8));
|
||||||
|
return;
|
||||||
|
case 0x2A37:
|
||||||
|
if(len >= 2) {
|
||||||
|
uint16_t bpm = (d[0] & 1) ? (d[1] | (d[2] << 8)) : d[1];
|
||||||
|
furi_string_cat_printf(out, "%u bpm\n", bpm);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
case 0x2A6E:
|
||||||
|
if(len >= 2) {
|
||||||
|
int16_t t = d[0] | (d[1] << 8);
|
||||||
|
furi_string_cat_printf(out, "%d.%02d C\n", t / 100, abs(t % 100));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
case 0x2A6F:
|
||||||
|
if(len >= 2) {
|
||||||
|
uint16_t h = d[0] | (d[1] << 8);
|
||||||
|
furi_string_cat_printf(out, "%u.%02u %%RH\n", h / 100, h % 100);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
case 0x2A6D:
|
||||||
|
if(len >= 4) {
|
||||||
|
uint32_t p = d[0] | (d[1] << 8) | (d[2] << 16) | ((uint32_t)d[3] << 24);
|
||||||
|
furi_string_cat_printf(out, "%lu.%lu hPa\n", p / 1000, (p / 100) % 10);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
case 0x2A07:
|
||||||
|
if(len >= 1) furi_string_cat_printf(out, "%d dBm\n", (int8_t)d[0]);
|
||||||
|
return;
|
||||||
|
case 0x2A2B:
|
||||||
|
case 0x2A08:
|
||||||
|
if(len >= 7)
|
||||||
|
furi_string_cat_printf(
|
||||||
|
out, "%04u-%02u-%02u %02u:%02u:%02u\n", d[0] | (d[1] << 8), d[2], d[3], d[4], d[5],
|
||||||
|
d[6]);
|
||||||
|
return;
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
// Generic: string if printable, plus small integer interpretations.
|
||||||
|
if(all_printable(d, len)) {
|
||||||
|
furi_string_cat_str(out, "\"");
|
||||||
|
for(size_t i = 0; i < len; i++) furi_string_push_back(out, d[i]);
|
||||||
|
furi_string_cat_str(out, "\"\n");
|
||||||
|
}
|
||||||
|
if(len == 1) furi_string_cat_printf(out, "u8 %u / i8 %d\n", d[0], (int8_t)d[0]);
|
||||||
|
if(len == 2) furi_string_cat_printf(out, "u16 %u\n", d[0] | (d[1] << 8));
|
||||||
|
if(len == 4)
|
||||||
|
furi_string_cat_printf(
|
||||||
|
out, "u32 %lu\n", (uint32_t)(d[0] | (d[1] << 8) | (d[2] << 16) | ((uint32_t)d[3] << 24)));
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <furi.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
// Lookup tables and decoders. All return static strings or NULL when unknown.
|
||||||
|
const char* ble_company_name(uint16_t id);
|
||||||
|
const char* ble_uuid_name(const uint8_t* uuid, uint8_t len); // len 2 or 16, little-endian
|
||||||
|
const char* ble_appearance_name(uint16_t appearance);
|
||||||
|
const char* ble_adv_type_name(uint8_t evt_type);
|
||||||
|
const char* ble_att_error_name(uint8_t code);
|
||||||
|
|
||||||
|
void ble_hex(FuriString* out, const uint8_t* data, size_t len);
|
||||||
|
void ble_uuid_str(FuriString* out, const uint8_t* uuid, uint8_t len);
|
||||||
|
void ble_addr_str(FuriString* out, const uint8_t* addr);
|
||||||
|
|
||||||
|
// Manufacturer-specific data decoders (Apple Continuity, Microsoft CDP, ...)
|
||||||
|
void ble_decode_mfg(FuriString* out, uint16_t company, const uint8_t* d, uint8_t len);
|
||||||
|
// Service data decoders (Fast Pair, Eddystone, Exposure Notification, ...)
|
||||||
|
void ble_decode_svc_data(FuriString* out, uint16_t uuid, const uint8_t* d, uint8_t len);
|
||||||
|
// Characteristic value decoder: appends "decoded" representation(s) of a value.
|
||||||
|
void ble_decode_value(FuriString* out, const uint8_t* uuid, uint8_t uuid_len, const uint8_t* d, size_t len);
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,68 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Build a Flipper Zero firmware update package that contains BT Inspector.
|
||||||
|
#
|
||||||
|
# Why a firmware build and not a plain .fap:
|
||||||
|
# * The stock radio coprocessor image is ST's "BLE Light" stack: peripheral/
|
||||||
|
# broadcaster only. Scanning and connecting as a central need the "full"
|
||||||
|
# stack (COPRO_STACK_TYPE=ble_full, already supported by fbt).
|
||||||
|
# * The firmware does not export the ST GAP/GATT/HCI command API to external
|
||||||
|
# apps, so the app is compiled in as a built-in ("--extra-int-apps").
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ./build.sh build -> flipperzero-firmware/dist/f7-C/f7-update-*/ (update package)
|
||||||
|
# ./build.sh flash build and flash over USB (Flipper connected, qFlipper closed)
|
||||||
|
# FW_TAG=1.4.3 ./build.sh pin a different firmware tag (default 1.4.3)
|
||||||
|
# JOBS=2 ./build.sh limit parallel compile jobs (default 4)
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
APP_SRC="$HERE/bt_inspector"
|
||||||
|
FW_DIR="${FW_DIR:-$HERE/flipperzero-firmware}"
|
||||||
|
FW_TAG="${FW_TAG:-1.4.3}"
|
||||||
|
FW_REPO="https://github.com/flipperdevices/flipperzero-firmware.git"
|
||||||
|
|
||||||
|
if [ ! -d "$FW_DIR/.git" ]; then
|
||||||
|
echo ">> cloning firmware $FW_TAG into $FW_DIR"
|
||||||
|
git clone --depth 1 --branch "$FW_TAG" --recurse-submodules --shallow-submodules "$FW_REPO" "$FW_DIR"
|
||||||
|
fi
|
||||||
|
|
||||||
|
cd "$FW_DIR"
|
||||||
|
|
||||||
|
# Let GAP take the central/observer roles as well as peripheral (harmless on the
|
||||||
|
# full stack; the app itself uses HCI-level scan/connect commands).
|
||||||
|
GAP_C=targets/f7/ble_glue/gap.c
|
||||||
|
if grep -q "^ GAP_PERIPHERAL_ROLE,$" "$GAP_C"; then
|
||||||
|
sed -i 's/^ GAP_PERIPHERAL_ROLE,$/ GAP_PERIPHERAL_ROLE | GAP_CENTRAL_ROLE | GAP_OBSERVER_ROLE,/' "$GAP_C"
|
||||||
|
echo ">> patched $GAP_C: GAP roles = peripheral|central|observer"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Link (or refresh) the app into the user apps directory.
|
||||||
|
mkdir -p applications_user
|
||||||
|
rm -rf applications_user/bt_inspector
|
||||||
|
cp -r "$APP_SRC" applications_user/bt_inspector
|
||||||
|
|
||||||
|
FBT_ARGS=(
|
||||||
|
COMPACT=1 DEBUG=0 # release build, same as official images
|
||||||
|
COPRO_STACK_TYPE=ble_full
|
||||||
|
COPRO_STACK_BIN=stm32wb5x_BLE_Stack_full_fw.bin
|
||||||
|
--extra-int-apps=bt_inspector
|
||||||
|
)
|
||||||
|
|
||||||
|
echo ">> building update package (full BLE stack + BT Inspector)"
|
||||||
|
./fbt -j"${JOBS:-4}" "${FBT_ARGS[@]}" updater_package
|
||||||
|
|
||||||
|
PKG=$(ls -d dist/f7-C/f7-update-* 2>/dev/null | head -1)
|
||||||
|
echo
|
||||||
|
echo ">> update package: $PKG"
|
||||||
|
echo ">> radio stack in package:"
|
||||||
|
grep -E "^Radio" "$PKG/update.fuf" || true
|
||||||
|
echo ">> firmware size:"
|
||||||
|
ls -l "$PKG/firmware.dfu"
|
||||||
|
echo
|
||||||
|
echo "Install: copy '$PKG' to the SD card (e.g. /ext/update/) and run it from"
|
||||||
|
echo "the file browser, or use: ./build.sh flash"
|
||||||
|
|
||||||
|
if [ "${1:-}" = "flash" ]; then
|
||||||
|
echo ">> flashing over USB"
|
||||||
|
./fbt -j"${JOBS:-4}" "${FBT_ARGS[@]}" flash_usb_full
|
||||||
|
fi
|
||||||
Reference in New Issue
Block a user