Broadcast Receive State — Field Guide, State Machines & BlueZ Client
Complete breakdown of the Broadcast Receive State characteristic: every field, the PA sync state machine, BIG encryption states, and a full BlueZ GATT notification handler in C.
1. What Is the Broadcast Receive State Characteristic?
Think of the Broadcast Receive State (BRS) characteristic as the server’s live status dashboard for a single broadcast source. Every time something changes — the server found the PA, lost sync, got an encryption key, started decrypting — it updates this characteristic and sends a GATT notification to all subscribed clients.
A server can have multiple BRS characteristics, one for each broadcast source it is tracking. Each instance is independent and carries the state for one source. If the server is not tracking any source on a particular BRS characteristic, that characteristic will have a zero-length value.
2. Broadcast Receive State — Full Field Layout
3. PA_Sync_State Machine
The PA_Sync_State field transitions between 5 states. Understanding these transitions is critical for writing a robust BASS client that reacts correctly to every notification.
|
0x00
Not Synchronized to PA |
Initial state
|
0x01
SyncInfo Request Waiting for PAST from client |
|
Add/Modify Source
(PA_Sync ≠ 0x00) ↓
|
timeout → 0x04 No PAST
▶
|
Add/Modify Source
(PA_Sync=0x01 and server supports PAST) ↓
|
|
0x03
Failed to Sync to PA |
PA found → synced
▶
sync failed
◀
|
0x02
Synchronized to PA ✓ |
|
↑
(reset to Not Synced
when synced state is entered again) |
Modify Source (PA_Sync=0x00)
or server loses PA ◀
→ back to 0x00 Not Synced
|
0x04
No PAST PAST timeout or PAST not supported |
4. BIG_Encryption State Machine
The BIG_Encryption field tells you the encryption status of the broadcast stream. This only becomes meaningful once the server has synced to the PA (PA_Sync_State = 0x02).
|
0x00 — Not Encrypted
BIS streams have no encryption |
server detects unencrypted BIS |
◀
|
|
0x01 — Broadcast_Code Required
Server found encrypted BIS, waiting for key Server detects encrypted BIS and has no key
↕
Client writes Set Broadcast_Code operation
|
||
|
0x02 — Decrypting ✓
Correct key, audio flowing |
↙ ↘ |
0x03 — Bad_Code ✗
Wrong key sent; Bad_Code field shows it |
5. Notification Behaviour — When Does the Server Notify?
6. BlueZ GATT Client — Subscribe and Parse Notifications
Here is a complete BlueZ C program that connects to a BASS server, enables notifications on all Broadcast Receive State characteristics, parses each notification, and prints human-readable state information. This is the kind of code you would run on your Ubuntu Linux development machine to debug a BASS server implementation.
/* bass_client.c
* BlueZ D-Bus GATT client for Broadcast Audio Scan Service (BASS)
*
* Compile:
* gcc -o bass_client bass_client.c \
* $(pkg-config --cflags --libs gio-2.0 glib-2.0)
*
* Usage:
* ./bass_client AA:BB:CC:DD:EE:FF
*
* Requires: BlueZ 5.50+, device already paired.
*/
#include <gio/gio.h>
#include <glib.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
/* BASS UUID strings (128-bit form for D-Bus path matching) */
#define BASS_SERVICE_UUID "0000184f-0000-1000-8000-00805f9b34fb"
#define BRS_CHAR_UUID "00002bc1-0000-1000-8000-00805f9b34fb"
#define CP_CHAR_UUID "00002bc0-0000-1000-8000-00805f9b34fb"
/* ─── PA Sync State human-readable labels ────────────────────── */
static const char *pa_sync_state_str(uint8_t state)
{
switch (state) {
case 0x00: return "Not synchronized to PA";
case 0x01: return "SyncInfo Request (send PAST!)";
case 0x02: return "Synchronized to PA";
case 0x03: return "Failed to synchronize to PA";
case 0x04: return "No PAST (timeout)";
default: return "RFU";
}
}
/* ─── BIG Encryption state human-readable labels ─────────────── */
static const char *big_enc_str(uint8_t enc)
{
switch (enc) {
case 0x00: return "Not encrypted";
case 0x01: return "Broadcast_Code required";
case 0x02: return "Decrypting (correct key)";
case 0x03: return "Bad_Code (wrong key!)";
default: return "RFU";
}
}
/* ─── Parse and print a Broadcast Receive State value ────────── */
static void parse_brs(const uint8_t *data, gsize len)
{
if (len == 0) {
printf(" [BRS] Empty — no source tracked\n");
return;
}
if (len < 15) {
printf(" [BRS] Truncated packet (len=%zu)\n", len);
return;
}
int i = 0;
uint8_t source_id = data[i++];
uint8_t addr_type = data[i++];
/* Address is 6 bytes, little-endian */
const uint8_t *addr = &data[i]; i += 6;
uint8_t adv_sid = data[i++];
/* Broadcast_ID is 3 bytes little-endian */
uint32_t broadcast_id = data[i] | (data[i+1]<<8) | (data[i+2]<<16);
i += 3;
uint8_t pa_sync = data[i++];
uint8_t big_enc = data[i++];
printf("\n ┌─────────────────────────────────────────\n");
printf(" │ Source_ID : 0x%02X\n", source_id);
printf(" │ Address : %02X:%02X:%02X:%02X:%02X:%02X (%s)\n",
addr[5], addr[4], addr[3], addr[2], addr[1], addr[0],
addr_type == 0 ? "Public" : "Random");
printf(" │ Adv SID : 0x%02X\n", adv_sid);
printf(" │ Broadcast_ID : 0x%06X\n", broadcast_id);
printf(" │ PA_Sync_State : 0x%02X — %s\n",
pa_sync, pa_sync_state_str(pa_sync));
printf(" │ BIG_Encryption : 0x%02X — %s\n",
big_enc, big_enc_str(big_enc));
/* Bad_Code: present only when BIG_Encryption == 0x03 */
if (big_enc == 0x03) {
if (i + 16 <= (int)len) {
printf(" │ Bad_Code : ");
for (int k = 0; k < 16; k++)
printf("%02X ", data[i + k]);
printf("\n");
}
i += 16;
}
if (i >= (int)len) {
printf(" └─────────────────────────────────────────\n");
return;
}
uint8_t num_subgroups = data[i++];
printf(" │ Num_Subgroups : %d\n", num_subgroups);
for (int sg = 0; sg < num_subgroups && i + 5 <= (int)len; sg++) {
uint32_t bis_sync = data[i] | (data[i+1]<<8) |
(data[i+2]<<16) | (data[i+3]<<24);
i += 4;
uint8_t meta_len = data[i++];
printf(" │ Subgroup[%d]\n", sg);
if (bis_sync == 0xFFFFFFFF)
printf(" │ BIS_Sync_State: 0xFFFFFFFF — Failed to sync to BIG\n");
else
printf(" │ BIS_Sync_State: 0x%08X (bits = synced BIS indexes)\n",
bis_sync);
printf(" │ Metadata_Len : %d bytes\n", meta_len);
i += meta_len; /* skip metadata payload */
}
printf(" └─────────────────────────────────────────\n");
}
/* ─── D-Bus signal handler for PropertiesChanged ─────────────── */
static void on_properties_changed(GDBusProxy *proxy,
GVariant *changed,
GStrv invalidated,
gpointer user_data)
{
GVariant *val = g_variant_lookup_value(changed, "Value", NULL);
if (!val) return;
gsize len;
const uint8_t *data = g_variant_get_fixed_array(
val, &len, sizeof(uint8_t));
printf("\n[NOTIFICATION] Broadcast Receive State updated:\n");
parse_brs(data, len);
g_variant_unref(val);
}
/* ─── Enable notifications (write 0x0001 to CCCD) ────────────── */
static void enable_notifications(GDBusProxy *char_proxy)
{
GError *err = NULL;
uint16_t cccd = 0x0001; /* notifications enable */
GVariant *options = g_variant_new("a{sv}", NULL);
/* Use BlueZ StartNotify method on the characteristic */
GVariant *ret = g_dbus_proxy_call_sync(char_proxy,
"StartNotify",
NULL,
G_DBUS_CALL_FLAGS_NONE,
-1, NULL, &err);
if (err) {
g_printerr("StartNotify failed: %s\n", err->message);
g_error_free(err);
return;
}
printf("[+] Notifications enabled on BRS characteristic\n");
if (ret) g_variant_unref(ret);
(void)cccd; (void)options;
}
/* ─── main ───────────────────────────────────────────────────── */
int main(int argc, char **argv)
{
if (argc < 2) {
fprintf(stderr, "Usage: %s <BT-addr>\n", argv[0]);
return 1;
}
GError *err = NULL;
GMainLoop *loop = g_main_loop_new(NULL, FALSE);
/* Connect to BlueZ object manager over system D-Bus */
GDBusObjectManager *mgr =
g_dbus_object_manager_client_new_for_bus_sync(
G_BUS_TYPE_SYSTEM,
G_DBUS_OBJECT_MANAGER_CLIENT_FLAGS_NONE,
"org.bluez", "/",
NULL, NULL, NULL, NULL, &err);
if (err) {
g_printerr("Failed to connect to BlueZ: %s\n", err->message);
return 1;
}
/* Walk all objects, find BRS characteristics for our device */
GList *objects = g_dbus_object_manager_get_objects(mgr);
for (GList *l = objects; l; l = l->next) {
GDBusObject *obj = G_DBUS_OBJECT(l->data);
GDBusInterface *iface = g_dbus_object_get_interface(
obj, "org.bluez.GattCharacteristic1");
if (!iface) continue;
GDBusProxy *proxy = G_DBUS_PROXY(iface);
GVariant *uuid_v = g_dbus_proxy_get_cached_property(proxy, "UUID");
if (!uuid_v) { g_object_unref(iface); continue; }
const gchar *uuid = g_variant_get_string(uuid_v, NULL);
if (g_str_equal(uuid, BRS_CHAR_UUID)) {
printf("[+] Found BRS characteristic: %s\n",
g_dbus_proxy_get_object_path(proxy));
/* Connect to PropertiesChanged for live notifications */
g_signal_connect(proxy, "g-properties-changed",
G_CALLBACK(on_properties_changed), NULL);
/* Enable notifications */
enable_notifications(proxy);
/* Do an initial read to get current state */
GVariant *read_ret = g_dbus_proxy_call_sync(
proxy, "ReadValue",
g_variant_new("(a{sv})", NULL),
G_DBUS_CALL_FLAGS_NONE, -1, NULL, &err);
if (read_ret) {
GVariant *arr;
g_variant_get(read_ret, "(@ay)", &arr);
gsize len;
const uint8_t *d = g_variant_get_fixed_array(
arr, &len, sizeof(uint8_t));
printf("[*] Initial BRS read:\n");
parse_brs(d, len);
g_variant_unref(arr);
g_variant_unref(read_ret);
}
}
g_variant_unref(uuid_v);
g_object_unref(iface);
}
g_list_free_full(objects, g_object_unref);
printf("\n[*] Listening for BRS notifications. Press Ctrl-C to quit.\n");
g_main_loop_run(loop);
g_object_unref(mgr);
g_main_loop_unref(loop);
return 0;
}
7. Reading the BIS_Sync_State Bitfield
The BIS_Sync_State is a 4-byte bitfield. Bit 0 corresponds to BIS index 1, bit 1 corresponds to BIS index 2, and so on up to bit 30 for BIS index 31. The special value 0xFFFFFFFF means the server failed to synchronise to the BIG entirely.
/*
* decode_bis_sync_state()
*
* Prints which BIS indexes the server is currently synced to.
*
* Example:
* bis_sync_state = 0x00000003
* → Bit 0 = 1 (synced to BIS index 1)
* → Bit 1 = 1 (synced to BIS index 2)
* → Bits 2-30 = 0 (not synced to BIS indexes 3-31)
*
* This means: stereo audio — both left (BIS 1) and right (BIS 2)
* streams are being received.
*/
static void decode_bis_sync_state(uint32_t state)
{
if (state == 0xFFFFFFFF) {
printf(" BIS sync: FAILED to sync to BIG\n");
return;
}
if (state == 0x00000000) {
printf(" BIS sync: not synced to any BIS\n");
return;
}
printf(" BIS sync: synced to BIS indexes: ");
for (int bit = 0; bit < 31; bit++) {
if (state & (1u << bit))
printf("%d ", bit + 1); /* BIS index = bit + 1 */
}
printf("\n");
}
/* ─── Example BIS_Sync values and meanings ─────────────────────
*
* 0x00000000 = not synced to any BIS
* 0x00000001 = synced to BIS 1 only (e.g. mono left)
* 0x00000002 = synced to BIS 2 only (e.g. mono right)
* 0x00000003 = synced to BIS 1 and BIS 2 (stereo)
* 0x00000007 = synced to BIS 1, 2, 3
* 0xFFFFFFFF = failed to sync to BIG (total failure)
*
* ────────────────────────────────────────────────────────────── */
8. Complete Flow — Airport PA System Example
Let us trace through a real scenario: you walk into an airport. Your hearing aid (BASS Server) wants to receive the gate announcements broadcast over LE Audio. Your phone (BASS Client) does the work.
9. Common Issues and Debugging Tips
Monitoring BASS Traffic with btmon
# Run btmon in one terminal to capture all HCI traffic
sudo btmon -w bass_capture.btsnoop
# In another terminal, run your BASS client or bluetoothctl operations
# btmon output will show ATT Write Requests, Notifications, and Error Responses
# To look specifically at ATT Write Req frames (opcode 0x12):
# Filter output with grep:
sudo btmon | grep -A5 "ATT Write Request"
# After capturing, open the .btsnoop file in Wireshark for full decode:
wireshark bass_capture.btsnoop &
# In Wireshark, filter for BASS traffic:
# Filter bar: btatt.handle == 0x0013
# Or by UUID: btatt.uuid16 == 0x2bc0
10. Series Summary
Now that you understand BASS completely, the natural next step is exploring the Basic Audio Profile (BAP) which defines the Broadcast Source side — how a device sets up a BIG and transmits BISes using the LE Isochronous channels. The BAP spec also covers the full coordinator role that a phone plays when it orchestrates hearing aids in a binaural hearing aid system.
