Files
flipper-bluepp/libble/ble_central.c
T
andyandClaude Fable 5.1 6fbfef6358 Fix BLE apps not scanning: use ACI GAP procedures, not raw HCI
On device, scanning failed (BT Inspector showed the scan as stopped and found
nothing). The firmware's GAP layer is initialised by the bt service, and ST's
stack disallows raw hci_le_set_scan_* / hci_le_create_connection in that state.
Switch the shared central layer to the GAP procedures: aci_gap_start_
observation_proc / aci_gap_terminate_gap_proc(GAP_OBSERVATION_PROC) for
scanning, and aci_gap_create_connection (+ terminate the direct-connection proc
to cancel) for connecting, with a short settle after stopping observation.
Also surface the raw BLE status code on BT Inspector's scan screen ("err 0xNN")
so any remaining failure is diagnosable on device. Needs the gap command group
exported (BLE_API default is all).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LEaD4ZeAfvWLBUxXp8G11r
2026-09-10 23:46:09 +01:00

551 lines
19 KiB
C

// SPDX-License-Identifier: GPL-3.0-or-later
// Copyright (C) 2026 Andy Dixon <andy@dixon.cx>
#include "ble_central.h"
#include <furi_hal.h>
#include <furi_ble/event_dispatcher.h>
#include <ble/core/ble_defs.h>
#include <ble/core/ble_std.h>
#include <ble/core/auto/ble_types.h>
#include <ble/core/auto/ble_vs_codes.h>
#include <ble/core/auto/ble_hci_le.h>
#include <ble/core/auto/ble_gatt_aci.h>
#include <ble/core/auto/ble_gap_aci.h>
#include <string.h>
#include <stdlib.h>
#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
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;
BcHciEvent* pkt = (BcHciEvent*)((BcHciPkt*)event)->data;
switch(pkt->evt) {
case HCI_LE_META_EVT_CODE: {
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;
}
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: {
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;
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, public address, no duplicate
// filtering (so RSSI stays live), accept-all filter policy. The firmware's
// GAP is initialised, so we must use the GAP observation procedure; raw
// hci_le_set_scan_* returns "command disallowed" in that state.
tBleStatus st = aci_gap_start_observation_proc(0x0050, 0x0030, 0x01, OWN_ADDR_TYPE, 0x00, 0x00);
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;
aci_gap_terminate_gap_proc(GAP_OBSERVATION_PROC);
bc->scanning = false;
}
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);
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;
}
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;
// Observation must fully stop before starting a connection procedure.
furi_delay_ms(100);
// scan 60/30 ms, conn interval 30-50 ms, latency 0, supervision 5 s
tBleStatus st = aci_gap_create_connection(
0x60, 0x30, dev->addr_type, dev->addr, OWN_ADDR_TYPE, 0x18, 0x28, 0, 0x01F4, 0, 0);
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) {
aci_gap_terminate_gap_proc(GAP_DIRECT_CONNECTION_ESTABLISHMENT_PROC);
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);
}