<alp/ble.h> — Bluetooth Low Energy
Peripheral (advertise + GATT server) and central (scan + connect + GATT client) over one portable surface.
Header
#include <alp/ble.h>
The host handle is a singleton
alp_ble_open() takes no arguments — there is no role or config struct. It returns the system BLE host singleton (the first call triggers controller init; later calls return the same pointer), or NULL if the controller failed to come up (radio absent, RF blocked). The same handle both advertises and scans/connects — peripheral and central are surfaces on one host, not a mode you pick at open.
alp_ble_t *ble = alp_ble_open();
if (!ble) { /* controller init failed */ }
// ...
alp_ble_close(ble); // NULL-safe, idempotent; last close shuts the controller down
Peripheral: advertise + GATT server
alp_ble_uuid_t srv_uuid = { .b = { /* 128-bit UUID, little-endian */ } };
// Register services BEFORE advertising (see the ordering note).
alp_ble_char_def_t chars[] = {
{ .uuid = { .b = { /* ... */ } },
.properties = ALP_BLE_GATT_PROP_READ | ALP_BLE_GATT_PROP_NOTIFY,
.initial_value = NULL, .initial_len = 0 },
};
alp_ble_service_def_t def = { .service_uuid = srv_uuid, .chars = chars, .num_chars = 1 };
alp_ble_attr_handle_t handles[1];
alp_ble_gatt_register_service(ble, &def, handles); // handles[i] filled in declaration order
alp_ble_advertise_start(ble, &(alp_ble_adv_config_t){
.name = "ALP-Sensor",
.services = &srv_uuid,
.num_services = 1,
.interval_min_ms = 100,
.interval_max_ms = 200,
.connectable = true,
});
// Later, once a peer has subscribed:
alp_ble_gatt_notify(ble, conn, handles[0], payload, len); // len ≤ ATT_MTU − 3
Characteristic property bits
OR the BT-SIG values into alp_ble_char_def_t::properties:
| Bit | Meaning |
|---|---|
ALP_BLE_GATT_PROP_READ (0x02) | Peer may read. |
ALP_BLE_GATT_PROP_WRITE (0x08) | Peer may write. |
ALP_BLE_GATT_PROP_NOTIFY (0x10) | Server may notify. |
ALP_BLE_GATT_PROP_INDICATE (0x20) | Server may indicate (confirmed notify). |
alp_ble_gatt_register_service() must run before alp_ble_advertise_start(). On backends whose host stack only adds services to a not-yet-started GATT server (NimBLE's ble_gatts_start() / ble_gatts_mutable() constraint on the CC3501E backend), registering while already advertising, scanning, or connected is refused with ALP_ERR_BUSY.
Central: scan + connect + GATT client
void on_device(const alp_ble_scan_result_t *r, void *user) { /* ... */ }
alp_ble_scan_start(ble, /*active=*/true, on_device, NULL); // cb runs synchronously, per packet
alp_ble_scan_stop(ble);
alp_ble_conn_t *conn = NULL;
alp_ble_connect(ble, &peer_addr, /*timeout_ms=*/5000, &conn);
uint8_t buf[32];
size_t n = 0;
alp_ble_gatt_read(conn, handle, buf, sizeof buf, &n, /*timeout_ms=*/2000);
alp_ble_gatt_write(conn, handle, data, len, /*timeout_ms=*/2000); // with response; len ≤ ATT_MTU − 3
alp_ble_disconnect(conn);
The scan callback is invoked synchronously on the calling thread, once per advertisement, from inside alp_ble_scan_start before it returns. Closing the radio handle from within the callback (a "self-close") is safe — teardown is deferred until scan_start returns. See <alp/peripheral.h> for the shared callback-context / re-entry contract (issue #756).
Advertising data budget
name plus advertised service UUIDs must fit the 31-byte legacy adv PDU. The mandatory Flags element always eats 3 bytes, then each field costs 2 + payload:
budget = 31 − 3 (Flags) − (2 + strlen(name)) − (2 + 16·num_services)
So the local name is ≤ 26 chars alone, or ≤ 8 chars alongside one 128-bit service UUID. An over-budget adv_config is rejected with ALP_ERR_INVAL — identically on the Zephyr and CC3501E backends (the budget is enforced centrally in alp_ble_advertise_start() before backend dispatch).
Backends
- AEN family — BLE rides the on-module TI CC3501E coprocessor (shared with Wi-Fi) over the inter-chip SPI bridge; the CC3501E runs its own NimBLE host. Silicon-validated on the E1M-AEN801. See the CC3501E section.
- Zephyr backend — the active SoC's BLE controller under the Zephyr
bthost stack. - Yocto/Linux backend — BlueZ over D-Bus.
- Bare-metal —
ALP_ERR_NOSUPPORT(BLE is off the bare-metal path).
The application API is identical across backends.
board.yaml
iot:
ble: true
Default-initialiser macro
alp_ble_adv_config_t cfg = ALP_BLE_ADV_CONFIG_DEFAULT("alp-sensor");
ALP_BLE_ADV_CONFIG_DEFAULT(id) (v0.10) sets the advertised name from id plus: services = NULL, num_services = 0, interval_min_ms = 100, interval_max_ms = 200, connectable = true.
See the shared macro contract for the C++ compound-literal caveat that applies to every ALP_*_CONFIG_DEFAULT.
Examples
aen-cc3501e-ble-gatt— silicon-proven GATT-server advertise path on the E1M-AEN801 (server-only bench)aen-cc3501e-gatt-register— the dynamic GATT-registration + ordering-guard gate
See also
<alp/iot.h>— Wi-Fi + MQTT<alp/security.h>— pairing crypto