diff --git a/README.md b/README.md index 99384b5..1e24af5 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,8 @@ services and characteristics: read, write, subscribe, and log it all. > 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. +> in ST's full BLE stack and exports the BLE command API to apps. The app itself +> is a normal `.fap`. `build.sh` does all of this. > See [Why a custom firmware build](#why-a-custom-firmware-build). ## Features @@ -62,19 +63,34 @@ 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 # 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). +`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/`. 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. +`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. @@ -128,12 +144,26 @@ Three facts about the Flipper Zero drive the design: 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). + 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. `build.sh` also widens the GAP roles the firmware initialises (`GAP_PERIPHERAL_ROLE | GAP_CENTRAL_ROLE | GAP_OBSERVER_ROLE`) in @@ -182,8 +212,12 @@ wire overlay). 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). +* **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. @@ -191,7 +225,7 @@ wire overlay). ## Layout ``` -bt_inspector/ app sources + application.fam +bt_inspector/ app sources, application.fam, icon.png build.sh firmware build / flash script flipperzero-firmware/ created by build.sh (git-ignored) ``` diff --git a/bt_inspector/application.fam b/bt_inspector/application.fam index ef86730..c883ad3 100644 --- a/bt_inspector/application.fam +++ b/bt_inspector/application.fam @@ -1,12 +1,16 @@ -# 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. +# External app. Needs firmware built by ../build.sh: full BLE stack plus the +# ST aci_*/hci_* command API exported to apps. App( appid="bt_inspector", name="BT Inspector", - apptype=FlipperAppType.APP, + apptype=FlipperAppType.EXTERNAL, entry_point="bt_inspector_app", requires=["gui", "storage"], stack_size=4 * 1024, - icon="A_Plugins_14", - order=25, + fap_category="Bluetooth", + fap_icon="icon.png", + fap_version="1.0", + fap_description="BLE scanner and GATT explorer (full-stack firmware)", + fap_author="andydixon", + fap_weburl="https://github.com/andydixon/flipper-bt-inspector", ) diff --git a/bt_inspector/ble_central.c b/bt_inspector/ble_central.c index 63e2a74..6c64ed9 100644 --- a/bt_inspector/ble_central.c +++ b/bt_inspector/ble_central.c @@ -1,13 +1,37 @@ #include "ble_central.h" #include #include -#include -#include +#include +#include +#include +#include +#include +#include #include #include #define TAG "BtInspector" +// Wire layout of the packets handed to dispatcher handlers (from ST's tl.h, +// redeclared here so the app only needs the exported SDK headers). +typedef struct __attribute__((packed)) { + uint8_t type; + uint8_t data[1]; +} BcHciPkt; +typedef struct __attribute__((packed)) { + uint8_t evt; + uint8_t plen; + uint8_t data[1]; +} BcHciEvent; +typedef struct __attribute__((packed)) { + uint8_t subevent; + uint8_t data[1]; +} BcLeMetaEvent; +typedef struct __attribute__((packed)) { + uint16_t ecode; + uint8_t data[1]; +} BcAciEvent; + #define OWN_ADDR_TYPE GAP_PUBLIC_ADDR // firmware programs a public identity address #define PROC_TIMEOUT_MS 5000 @@ -134,11 +158,11 @@ static void handle_adv_report(BleCentral* bc, const uint8_t* p, size_t len) { static BleEventAckStatus bc_event_handler(void* event, void* context) { BleCentral* bc = context; - hci_event_pckt* pkt = (hci_event_pckt*)((hci_uart_pckt*)event)->data; + BcHciEvent* pkt = (BcHciEvent*)((BcHciPkt*)event)->data; switch(pkt->evt) { case HCI_LE_META_EVT_CODE: { - evt_le_meta_event* meta = (evt_le_meta_event*)pkt->data; + BcLeMetaEvent* meta = (BcLeMetaEvent*)pkt->data; if(meta->subevent == HCI_LE_ADVERTISING_REPORT_SUBEVT_CODE) { handle_adv_report(bc, meta->data, pkt->plen - 1); return BleEventAckFlowEnable; @@ -173,7 +197,7 @@ static BleEventAckStatus bc_event_handler(void* event, void* context) { return BleEventNotAck; } case HCI_VENDOR_SPECIFIC_DEBUG_EVT_CODE: { - evt_blecore_aci* aci = (evt_blecore_aci*)pkt->data; + BcAciEvent* aci = (BcAciEvent*)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; @@ -306,9 +330,6 @@ void bc_scan_stop(BleCentral* bc) { 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; @@ -325,7 +346,15 @@ size_t bc_snapshot(BleCentral* bc, BcDevice* out, size_t max) { d->logged = true; } furi_mutex_release(bc->mutex); - qsort(out, n, sizeof(BcDevice), cmp_rssi); + 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].rssi < d.rssi) { + out[j] = out[j - 1]; + j--; + } + out[j] = d; + } return n; } diff --git a/bt_inspector/bt_inspector.c b/bt_inspector/bt_inspector.c index 740bde1..c4dd12a 100644 --- a/bt_inspector/bt_inspector.c +++ b/bt_inspector/bt_inspector.c @@ -10,7 +10,7 @@ #include #include #include -#include +#include #include #include #include "ble_central.h" diff --git a/bt_inspector/icon.png b/bt_inspector/icon.png new file mode 100644 index 0000000..2d6f2fb Binary files /dev/null and b/bt_inspector/icon.png differ diff --git a/build.sh b/build.sh index 5fe76e5..097b3b0 100755 --- a/build.sh +++ b/build.sh @@ -6,11 +6,16 @@ # 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"). +# apps. This script adds those headers to the SDK and accepts the new +# aci_*/hci_* symbols, so the app builds as a normal .fap (bumps the API +# minor version: the .fap only loads on this firmware). # # Usage: -# ./build.sh build -> flipperzero-firmware/dist/f7-C/f7-update-*/ (update package) +# ./build.sh build -> flipperzero-firmware/dist/f7-C/f7-update-*/ (update package, +# includes the .fap under apps/Bluetooth) + SDK zip for ufbt # ./build.sh flash build and flash over USB (Flipper connected, qFlipper closed) +# ./build.sh fap rebuild only the .fap (firmware already built once) +# BLE_API=all|min|hci,gap,gatt,hal,l2cap which ST API to export (default hci,gatt) # 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 @@ -36,6 +41,48 @@ if grep -q "^ GAP_PERIPHERAL_ROLE,$" "$GAP_C"; then echo ">> patched $GAP_C: GAP roles = peripheral|central|observer" fi +# Export the ST BLE command API (aci_*/hci_*) to external apps. +WB_SCONS=lib/stm32wb.scons +python3 - "$WB_SCONS" <<'PY' +import re, sys +p = sys.argv[1] +s = open(p).read() +block = """# BT Inspector: expose the ST BLE command API to .fap apps +env.Append( + SDK_HEADERS=[ + File("stm32wb_copro/wpan/ble/core/auto/ble_hci_le.h"), + File("stm32wb_copro/wpan/ble/core/auto/ble_gap_aci.h"), + File("stm32wb_copro/wpan/ble/core/auto/ble_gatt_aci.h"), + File("stm32wb_copro/wpan/ble/core/auto/ble_hal_aci.h"), + File("stm32wb_copro/wpan/ble/core/auto/ble_l2cap_aci.h"), + File("stm32wb_copro/wpan/ble/core/auto/ble_vs_codes.h"), + ], +) + +""" +s = re.sub(r"\n*# BT Inspector: expose.*?\n\)\n", "\n", s, flags=re.S) # drop any earlier copy +if block not in s: + s = s.replace('Return("lib")', block + 'Return("lib")', 1) # must run before Return + open(p, "w").write(s) + print(">> patched " + p + ": ST BLE headers added to SDK") +PY + +# ST's generated headers have no extern "C" guards; the API table is C++. +python3 - <<'PY' +import re +for name in ["ble_hci_le", "ble_gap_aci", "ble_gatt_aci", "ble_hal_aci", "ble_l2cap_aci"]: + p = f"lib/stm32wb_copro/wpan/ble/core/auto/{name}.h" + s = open(p).read() + if "__cplusplus" in s: + continue + guard = re.search(r"#define \w+_H__\n", s).group(0) + s = s.replace(guard, guard + '#ifdef __cplusplus\nextern "C" {\n#endif\n', 1) + i = s.rstrip().rfind("#endif") + s = s[:i] + "#ifdef __cplusplus\n}\n#endif\n" + s[i:] + open(p, "w").write(s) + print(">> patched " + p + ': extern "C" guards') +PY + # Link (or refresh) the app into the user apps directory. mkdir -p applications_user rm -rf applications_user/bt_inspector @@ -45,10 +92,58 @@ 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)" +API_CSV=targets/f7/api_symbols.csv +# BLE_API selects which ST command headers are exported to apps (each exported +# function is kept in the firmware image): +# hci,gatt scanning, connections, GATT client/server (~12 KB, default) +# all also gap, hal, l2cap (~20 KB) +# min only the functions BT Inspector imports (~1.5 KB) +BLE_API="${BLE_API:-hci,gatt}" +if grep -q "^Function,?," "$API_CSV" || ! grep -qE "^Header,\+,.*ble_hci_le\.h" "$API_CSV"; then + # fbt notices the new headers, rewrites the csv with '?' entries and stops. + echo ">> syncing API symbol table (expected to stop once)" + ./fbt -j"${JOBS:-4}" "${FBT_ARGS[@]}" api_check || true +fi +python3 - "$API_CSV" "$BLE_API" "$APP_SRC" <<'PY' +import re, sys, glob +csv_path, mode, app_src = sys.argv[1:4] +hdrs = {"hci": "ble_hci_le", "gap": "ble_gap_aci", "gatt": "ble_gatt_aci", "hal": "ble_hal_aci", "l2cap": "ble_l2cap_aci"} +owner = {} +for key, name in hdrs.items(): + src = open(f"lib/stm32wb_copro/wpan/ble/core/auto/{name}.h").read() + for fn in re.findall(r"^tBleStatus (\w+)\(", src, re.M): + owner[fn] = key +if mode == "all": + selected = set(owner) +elif mode == "min": + selected = set() + for f in glob.glob(app_src + "/*.c"): + selected |= set(re.findall(r"\b((?:aci_|hci_)\w+)\(", open(f).read())) +else: + want = set(mode.split(",")) + selected = {fn for fn, key in owner.items() if key in want} +out, n_on = [], 0 +for line in open(csv_path).read().splitlines(): + parts = line.split(",") + if len(parts) >= 3 and parts[0] == "Function" and parts[2] in owner: + parts[1] = "+" if parts[2] in selected else "-" + n_on += parts[1] == "+" + elif len(parts) >= 2 and parts[1] == "?": + parts[1] = "-" if parts[0] == "Variable" else "+" + out.append(",".join(parts)) +open(csv_path, "w").write("\n".join(out) + "\n") +print(f">> BLE API export ({mode}): {n_on} of {len(owner)} aci_*/hci_* functions enabled") +PY + +if [ "${1:-}" = "fap" ]; then + ./fbt -j"${JOBS:-4}" "${FBT_ARGS[@]}" fap_bt_inspector + echo ">> $(ls build/f7-firmware-C/.extapps/bt_inspector.fap)" + exit 0 +fi + +echo ">> building update package (full BLE stack + BT Inspector .fap)" ./fbt -j"${JOBS:-4}" "${FBT_ARGS[@]}" updater_package PKG=$(ls -d dist/f7-C/f7-update-* 2>/dev/null | head -1) @@ -58,6 +153,9 @@ echo ">> radio stack in package:" grep -E "^Radio" "$PKG/update.fuf" || true echo ">> firmware size:" ls -l "$PKG/firmware.dfu" +echo ">> app (also inside the package's resources, installed to apps/Bluetooth/):" +ls -l build/f7-firmware-C/.extapps/bt_inspector.fap +echo ">> SDK for building the app with ufbt: $(ls dist/f7-C/flipper-z-f7-sdk-*.zip)" 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"