blob: 86400606ac047ffe073b6326584206d1b9dad59d [file] [log] [blame]
/*
* Copyright (c) 2021 Nordic Semiconductor ASA
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <ctype.h>
#include <bluetooth/bluetooth.h>
#include <bluetooth/conn.h>
#include <bluetooth/iso.h>
#include <sys/byteorder.h>
#include <console/console.h>
#include <logging/log.h>
LOG_MODULE_REGISTER(iso_broadcast_receiver, LOG_LEVEL_DBG);
#define DEVICE_NAME CONFIG_BT_DEVICE_NAME
#define DEVICE_NAME_LEN (sizeof(DEVICE_NAME))
#define PA_RETRY_COUNT 6
#define ISO_RETRY_COUNT 10
struct iso_recv_stats {
uint32_t iso_recv_count;
uint32_t iso_lost_count;
};
static bool broadcaster_found;
static bool per_adv_lost;
static bool big_sync_lost;
static bool biginfo_received;
static bt_addr_le_t per_addr;
static uint8_t per_sid;
static uint16_t per_interval_ms;
static uint16_t iso_interval_ms;
static uint8_t bis_count;
static uint32_t last_received_counter;
static int64_t big_sync_start_time;
static size_t big_sync_count;
static struct iso_recv_stats stats_current_sync;
static struct iso_recv_stats stats_overall;
static K_SEM_DEFINE(sem_per_adv, 0, 1);
static K_SEM_DEFINE(sem_per_sync, 0, 1);
static K_SEM_DEFINE(sem_per_sync_lost, 0, 1);
static K_SEM_DEFINE(sem_per_big_info, 0, 1);
static K_SEM_DEFINE(sem_big_sync, 0, 1);
static K_SEM_DEFINE(sem_big_sync_lost, 0, 1);
static const char *phy2str(uint8_t phy)
{
switch (phy) {
case 0: return "No packets";
case BT_GAP_LE_PHY_1M: return "LE 1M";
case BT_GAP_LE_PHY_2M: return "LE 2M";
case BT_GAP_LE_PHY_CODED: return "LE Coded";
default: return "Unknown";
}
}
static bool data_cb(struct bt_data *data, void *user_data)
{
char *name = user_data;
uint8_t len;
switch (data->type) {
case BT_DATA_NAME_SHORTENED:
case BT_DATA_NAME_COMPLETE:
len = MIN(data->data_len, DEVICE_NAME_LEN - 1);
memcpy(name, data->data, len);
name[len] = '\0';
return false;
default:
return true;
}
}
static void scan_recv(const struct bt_le_scan_recv_info *info,
struct net_buf_simple *buf)
{
char le_addr[BT_ADDR_LE_STR_LEN];
char name[DEVICE_NAME_LEN];
if (broadcaster_found) {
return;
}
(void)memset(name, 0, sizeof(name));
bt_data_parse(buf, data_cb, name);
if (strncmp(DEVICE_NAME, name, strlen(DEVICE_NAME))) {
return;
}
bt_addr_le_to_str(info->addr, le_addr, sizeof(le_addr));
LOG_INF("Found broadcaster with address %s (RSSI %i)",
le_addr, info->rssi);
broadcaster_found = true;
per_sid = info->sid;
per_interval_ms = BT_CONN_INTERVAL_TO_MS(info->interval);
bt_addr_le_copy(&per_addr, info->addr);
k_sem_give(&sem_per_adv);
}
static struct bt_le_scan_cb scan_callbacks = {
.recv = scan_recv,
};
static void sync_cb(struct bt_le_per_adv_sync *sync,
struct bt_le_per_adv_sync_synced_info *info)
{
LOG_INF("Periodic advertisement synced");
k_sem_give(&sem_per_sync);
}
static void term_cb(struct bt_le_per_adv_sync *sync,
const struct bt_le_per_adv_sync_term_info *info)
{
LOG_INF("Periodic advertisement sync terminated");
per_adv_lost = true;
k_sem_give(&sem_per_sync_lost);
}
static void biginfo_cb(struct bt_le_per_adv_sync *sync,
const struct bt_iso_biginfo *biginfo)
{
if (biginfo_received) {
return;
}
LOG_INF("BIGinfo received: num_bis %u, nse %u, interval %.2f ms, "
"bn %u, pto %u, irc %u, max_pdu %u, sdu_interval %u us, "
"max_sdu %u, phy %s, %s framing, %sencrypted",
biginfo->num_bis, biginfo->sub_evt_count,
BT_CONN_INTERVAL_TO_MS((float)biginfo->iso_interval),
biginfo->burst_number, biginfo->offset, biginfo->rep_count,
biginfo->max_pdu, biginfo->sdu_interval, biginfo->max_sdu,
phy2str(biginfo->phy), biginfo->framing ? "with" : "without",
biginfo->encryption ? "" : "not ");
iso_interval_ms = BT_CONN_INTERVAL_TO_MS(biginfo->iso_interval);
bis_count = MIN(biginfo->num_bis, CONFIG_BT_ISO_MAX_CHAN);
biginfo_received = true;
k_sem_give(&sem_per_big_info);
}
static struct bt_le_per_adv_sync_cb sync_callbacks = {
.synced = sync_cb,
.term = term_cb,
.biginfo = biginfo_cb,
};
static void print_stats(char *name, struct iso_recv_stats *stats)
{
uint32_t total_packets;
total_packets = stats->iso_recv_count + stats->iso_lost_count;
LOG_INF("%s: Received %u/%u (%.2f%%) - Total packets lost %u",
name, stats->iso_recv_count, total_packets,
(float)stats->iso_recv_count * 100 / total_packets,
stats->iso_lost_count);
}
static void iso_recv(struct bt_iso_chan *chan,
const struct bt_iso_recv_info *info,
struct net_buf *buf)
{
uint32_t total_packets;
static bool stats_latest_arr[1000];
static size_t stats_latest_arr_pos;
/* NOTE: The packets received may be on different BISes */
if (info->flags & BT_ISO_FLAGS_VALID) {
stats_current_sync.iso_recv_count++;
stats_overall.iso_recv_count++;
stats_latest_arr[stats_latest_arr_pos++] = true;
} else {
stats_current_sync.iso_lost_count++;
stats_overall.iso_lost_count++;
stats_latest_arr[stats_latest_arr_pos++] = false;
}
if (stats_latest_arr_pos == sizeof(stats_latest_arr)) {
stats_latest_arr_pos = 0;
}
total_packets = stats_overall.iso_recv_count + stats_overall.iso_lost_count;
if ((total_packets % 100) == 0) {
struct iso_recv_stats stats_latest = { 0 };
for (int i = 0; i < ARRAY_SIZE(stats_latest_arr); i++) {
/* If we have not yet received 1000 packets, break
* early
*/
if (i == total_packets) {
break;
}
if (stats_latest_arr[i]) {
stats_latest.iso_recv_count++;
} else {
stats_latest.iso_lost_count++;
}
}
print_stats("Overall ", &stats_overall);
print_stats("Current Sync", &stats_current_sync);
print_stats("Latest 1000 ", &stats_latest);
LOG_INF(""); /* Empty line to separate the stats */
}
}
static void iso_connected(struct bt_iso_chan *chan)
{
LOG_INF("ISO Channel %p connected", chan);
big_sync_start_time = k_uptime_get();
k_sem_give(&sem_big_sync);
}
static void iso_disconnected(struct bt_iso_chan *chan, uint8_t reason)
{
/* Calculate cumulative moving average - Be aware that this may
* cause overflow at sufficiently large counts or durations
*/
static int64_t average_duration;
uint64_t big_sync_duration = k_uptime_get() - big_sync_start_time;
uint64_t total_duration = big_sync_duration + (big_sync_count - 1) * average_duration;
average_duration = total_duration / big_sync_count;
LOG_INF("ISO Channel %p disconnected with reason 0x%02x after "
"%llu milliseconds (average duration %llu)",
chan, reason, big_sync_duration, average_duration);
big_sync_lost = true;
k_sem_give(&sem_big_sync_lost);
}
static struct bt_iso_chan_ops iso_ops = {
.recv = iso_recv,
.connected = iso_connected,
.disconnected = iso_disconnected,
};
static struct bt_iso_chan_io_qos iso_rx_qos;
static struct bt_iso_chan_qos bis_iso_qos = {
.rx = &iso_rx_qos,
};
static int start_scan(void)
{
int err;
err = bt_le_scan_start(BT_LE_SCAN_ACTIVE, NULL);
if (err != 0) {
LOG_ERR("Scan start failed (err %d)", err);
return err;
}
LOG_INF("Scan started");
return 0;
}
static int stop_scan(void)
{
int err;
err = bt_le_scan_stop();
if (err != 0) {
LOG_ERR("Scan stop failed (err %d)", err);
return err;
}
LOG_INF("Scan stopped");
return 0;
}
static int create_pa_sync(struct bt_le_per_adv_sync **sync)
{
struct bt_le_per_adv_sync_param sync_create_param;
int err;
uint32_t sem_timeout;
LOG_INF("Creating Periodic Advertising Sync");
bt_addr_le_copy(&sync_create_param.addr, &per_addr);
sync_create_param.options = 0;
sync_create_param.sid = per_sid;
sync_create_param.skip = 0;
sync_create_param.timeout = (per_interval_ms * PA_RETRY_COUNT) / 10;
sem_timeout = per_interval_ms * PA_RETRY_COUNT;
err = bt_le_per_adv_sync_create(&sync_create_param, sync);
if (err != 0) {
LOG_ERR("Periodic advertisement sync create failed (err %d)",
err);
return err;
}
LOG_INF("Waiting for periodic sync");
err = k_sem_take(&sem_per_sync, K_MSEC(sem_timeout));
if (err != 0) {
LOG_INF("failed to take sem_per_sync (err %d)", err);
LOG_INF("Deleting Periodic Advertising Sync");
err = bt_le_per_adv_sync_delete(*sync);
if (err != 0) {
LOG_ERR("Failed to delete Periodic advertisement sync (err %d)",
err);
return err;
}
}
LOG_INF("Periodic sync established");
return 0;
}
static int create_big_sync(struct bt_iso_big **big, struct bt_le_per_adv_sync *sync)
{
int err;
uint32_t sem_timeout = per_interval_ms * PA_RETRY_COUNT;
uint32_t sync_timeout_ms;
static struct bt_iso_chan bis_iso_chan[CONFIG_BT_ISO_MAX_CHAN];
struct bt_iso_chan *bis[CONFIG_BT_ISO_MAX_CHAN];
struct bt_iso_big_sync_param big_sync_param = {
.bis_channels = bis,
.num_bis = 0,
.bis_bitfield = 0,
.mse = 0,
.sync_timeout = 0
};
for (int i = 0; i < ARRAY_SIZE(bis_iso_chan); i++) {
bis_iso_chan[i].ops = &iso_ops;
bis_iso_chan[i].qos = &bis_iso_qos;
bis[i] = &bis_iso_chan[i];
}
LOG_INF("Waiting for BIG info");
err = k_sem_take(&sem_per_big_info, K_MSEC(sem_timeout));
if (err != 0) {
LOG_ERR("failed to take sem_per_big_info (err %d)", err);
return err;
}
sync_timeout_ms = iso_interval_ms * ISO_RETRY_COUNT;
big_sync_param.sync_timeout = CLAMP(sync_timeout_ms / 10, 0x000A, 0x4000); /* 10 ms units */
big_sync_param.num_bis = bis_count;
/* BIS indexes start from 0x01, so add one to `i` */
for (int i = 1; i <= big_sync_param.num_bis; i++) {
big_sync_param.bis_bitfield |= BIT(i);
}
LOG_INF("Syncing to BIG");
err = bt_iso_big_sync(sync, &big_sync_param, big);
if (err != 0) {
LOG_ERR("BIG sync failed (err %d)", err);
return err;
}
LOG_INF("Waiting for BIG sync");
err = k_sem_take(&sem_big_sync, K_MSEC(sem_timeout));
if (err != 0) {
LOG_ERR("failed to take sem_big_sync (err %d)", err);
return err;
}
LOG_INF("BIG sync established");
big_sync_count++;
return 0;
}
static int cleanup(struct bt_le_per_adv_sync *sync, struct bt_iso_big *big)
{
int pa_err = 0;
int big_err = 0;
if (!per_adv_lost && sync) {
LOG_INF("Deleting Periodic advertisement Sync");
pa_err = bt_le_per_adv_sync_delete(sync);
if (pa_err != 0) {
LOG_ERR("Failed to delete Periodic advertisement sync (err %d)",
pa_err);
}
}
if (!big_sync_lost && big) {
LOG_INF("Terminating BIG Sync");
big_err = bt_iso_big_terminate(big);
if (big_err != 0) {
LOG_ERR("BIG terminate failed (err %d)", big_err);
}
}
if (pa_err != 0 || big_err != 0) {
LOG_ERR("Cleanup failed (%d), recommend restart application to "
"avoid any potential leftovers",
pa_err != 0 ? pa_err : big_err);
}
return pa_err != 0 ? pa_err : big_err;
}
static void reset_sems(void)
{
(void)k_sem_reset(&sem_per_adv);
(void)k_sem_reset(&sem_per_sync);
(void)k_sem_reset(&sem_per_sync_lost);
(void)k_sem_reset(&sem_per_big_info);
(void)k_sem_reset(&sem_big_sync);
(void)k_sem_reset(&sem_big_sync_lost);
}
int test_run_receiver(void)
{
struct bt_le_per_adv_sync *sync = NULL;
struct bt_iso_big *big = NULL;
int err;
static bool callbacks_registered;
broadcaster_found = false;
per_adv_lost = false;
big_sync_lost = false;
biginfo_received = false;
reset_sems();
if (!callbacks_registered) {
bt_le_per_adv_sync_cb_register(&sync_callbacks);
bt_le_scan_cb_register(&scan_callbacks);
callbacks_registered = true;
}
err = start_scan();
if (err != 0) {
return err;
}
LOG_INF("Waiting for periodic advertiser");
err = k_sem_take(&sem_per_adv, K_FOREVER);
if (err != 0) {
LOG_ERR("failed to take sem_per_adv (err %d)", err);
return err;
}
LOG_INF("Periodic advertiser found");
err = stop_scan();
if (err != 0) {
return err;
}
err = create_pa_sync(&sync);
if (err != 0) {
return err;
}
last_received_counter = 0;
memset(&stats_current_sync, 0, sizeof(stats_current_sync));
big_sync_start_time = 0;
err = create_big_sync(&big, sync);
if (err != 0) {
(void)cleanup(sync, big);
return err;
}
err = k_sem_take(&sem_big_sync_lost, K_FOREVER);
if (err != 0) {
LOG_ERR("failed to take sem_big_sync_lost (err %d)", err);
return err;
}
LOG_INF("BIG sync lost, returning");
/* TODO: Add support to cancel the test early */
return cleanup(sync, big);
}