Skip to main content

<alp/ble.h> — Bluetooth Low Energy

Peripheral (advertise + GATT server) and central (scan + connect + GATT client) over one portable surface.

#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:

BitMeaning
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).
Register before you advertise

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 bt host stack.
  • Yocto/Linux backend — BlueZ over D-Bus.
  • Bare-metalALP_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

See also

Questions about this page? Discuss in Community Forum