blob: 696c610aab86d52291ab56aa61890be3f1b78501 [file]
// SPDX-License-Identifier: LGPL-2.1-or-later
/*
*
* BlueZ - Bluetooth protocol stack for Linux
*
* Copyright (C) 2025 Pauli Virtanen. All rights reserved.
*
*/
#define _GNU_SOURCE
#include <inttypes.h>
#include <string.h>
#include <stdlib.h>
#include <stdbool.h>
#include <unistd.h>
#include <errno.h>
#include "bluetooth/bluetooth.h"
#include "bluetooth/uuid.h"
#include "src/shared/queue.h"
#include "src/shared/util.h"
#include "src/shared/timeout.h"
#include "src/shared/att.h"
#include "src/shared/gatt-db.h"
#include "src/shared/gatt-server.h"
#include "src/shared/gatt-client.h"
#include "src/shared/tmap.h"
#include "src/shared/bap.h"
#define DBG(_tmap, fmt, arg...) \
tmap_debug(_tmap, "%s:%s() " fmt, __FILE__, __func__, ## arg)
struct bt_tmas_db {
struct gatt_db *db;
struct gatt_db_attribute *service;
struct gatt_db_attribute *role;
uint16_t role_value;
};
struct bt_tmap {
int ref_count;
struct bt_gatt_client *client;
struct bt_tmas_db db;
int idle_id;
bt_tmap_ready_func_t ready_func;
void *ready_data;
bt_tmap_debug_func_t debug_func;
bt_tmap_destroy_func_t debug_destroy;
void *debug_data;
};
static struct queue *instances;
static void tmap_free(void *data)
{
struct bt_tmap *tmap = data;
if (tmap->client) {
bt_gatt_client_idle_unregister(tmap->client, tmap->idle_id);
bt_gatt_client_unref(tmap->client);
} else {
gatt_db_remove_service(tmap->db.db, tmap->db.service);
gatt_db_unref(tmap->db.db);
}
queue_remove(instances, tmap);
if (queue_isempty(instances)) {
queue_destroy(instances, NULL);
instances = NULL;
}
free(tmap);
}
struct bt_tmap *bt_tmap_ref(struct bt_tmap *tmap)
{
if (!tmap)
return NULL;
__sync_fetch_and_add(&tmap->ref_count, 1);
return tmap;
}
void bt_tmap_unref(struct bt_tmap *tmap)
{
if (!tmap)
return;
if (__sync_sub_and_fetch(&tmap->ref_count, 1))
return;
tmap_free(tmap);
}
static void tmap_debug(struct bt_tmap *tmap, const char *format, ...)
{
va_list ap;
if (!tmap || !format || !tmap->debug_func)
return;
va_start(ap, format);
util_debug_va(tmap->debug_func, tmap->debug_data, format, ap);
va_end(ap);
}
bool bt_tmap_set_debug(struct bt_tmap *tmap, bt_tmap_debug_func_t cb,
void *user_data, bt_tmap_destroy_func_t destroy)
{
if (!tmap)
return false;
if (tmap->debug_destroy)
tmap->debug_destroy(tmap->debug_data);
tmap->debug_func = cb;
tmap->debug_destroy = destroy;
tmap->debug_data = user_data;
return true;
}
uint16_t bt_tmap_get_role(struct bt_tmap *tmap)
{
if (!tmap)
return 0;
return tmap->db.role_value;
}
/*
* TMA Client
*/
static void tmap_role_read(bool success, uint8_t att_ecode,
const uint8_t *value, uint16_t length,
void *user_data)
{
struct bt_tmap *tmap = user_data;
struct iovec iov = { .iov_base = (void *)value, .iov_len = length };
uint16_t role;
if (!success) {
DBG(tmap, "Unable to read Role: error 0x%02x", att_ecode);
return;
}
if (!util_iov_pull_le16(&iov, &role)) {
DBG(tmap, "Invalid Role");
return;
}
DBG(tmap, "Role 0x%x", role);
tmap->db.role_value = role & BT_TMAP_ROLE_MASK;
}
static void foreach_tmap_char(struct gatt_db_attribute *attr, void *user_data)
{
struct bt_tmap *tmap = user_data;
uint16_t value_handle;
bt_uuid_t uuid, uuid_role;
if (!gatt_db_attribute_get_char_data(attr, NULL, &value_handle,
NULL, NULL, &uuid))
return;
bt_uuid16_create(&uuid_role, TMAP_ROLE_CHRC_UUID);
if (!bt_uuid_cmp(&uuid, &uuid_role)) {
DBG(tmap, "TMAS Role Char found: handle 0x%04x", value_handle);
bt_gatt_client_read_value(tmap->client, value_handle,
tmap_role_read, tmap, NULL);
return;
}
}
static void foreach_tmap_service(struct gatt_db_attribute *attr,
void *user_data)
{
struct bt_tmap *tmap = user_data;
gatt_db_service_set_claimed(attr, true);
gatt_db_service_foreach_char(attr, foreach_tmap_char, tmap);
}
static void tmap_idle(void *data)
{
struct bt_tmap *tmap = data;
tmap->idle_id = 0;
if (!instances)
instances = queue_new();
queue_push_tail(instances, tmap);
if (tmap->ready_func)
tmap->ready_func(tmap, tmap->ready_data);
}
struct bt_tmap *bt_tmap_attach(struct bt_gatt_client *client,
bt_tmap_ready_func_t ready, void *user_data)
{
struct bt_tmap *tmap;
bt_uuid_t uuid;
if (!client)
return NULL;
client = bt_gatt_client_clone(client);
if (!client)
return NULL;
tmap = new0(struct bt_tmap, 1);
tmap->client = client;
tmap->ready_func = ready;
tmap->ready_data = user_data;
tmap->db.db = bt_gatt_client_get_db(tmap->client);
bt_uuid16_create(&uuid, TMAS_UUID);
gatt_db_foreach_service(tmap->db.db, &uuid, foreach_tmap_service, tmap);
tmap->idle_id = bt_gatt_client_idle_register(tmap->client, tmap_idle,
tmap, NULL);
return bt_tmap_ref(tmap);
}
/*
* TMAS Service
*/
static void tmas_role_read(struct gatt_db_attribute *attrib,
unsigned int id, uint16_t offset,
uint8_t opcode, struct bt_att *att,
void *user_data)
{
struct bt_tmas_db *db = user_data;
uint8_t role[2];
put_le16(db->role_value, role);
gatt_db_attribute_read_result(attrib, id, 0, role, sizeof(role));
}
static bool match_db(const void *data, const void *match_data)
{
const struct bt_tmap *tmap = data;
return tmap->db.db == match_data;
}
struct bt_tmap *bt_tmap_find(struct gatt_db *db)
{
return db ? queue_find(instances, match_db, db) : NULL;
}
struct bt_tmap *bt_tmap_add_db(struct gatt_db *db)
{
struct bt_tmap *tmap;
bt_uuid_t uuid;
if (!db || queue_find(instances, match_db, db))
return NULL;
tmap = new0(struct bt_tmap, 1);
tmap->db.db = gatt_db_ref(db);
bt_uuid16_create(&uuid, TMAS_UUID);
tmap->db.service = gatt_db_add_service(db, &uuid, true, 3);
bt_uuid16_create(&uuid, TMAP_ROLE_CHRC_UUID);
tmap->db.role = gatt_db_service_add_characteristic(
tmap->db.service,
&uuid,
BT_ATT_PERM_READ,
BT_GATT_CHRC_PROP_READ,
tmas_role_read, NULL,
&tmap->db);
if (!instances)
instances = queue_new();
queue_push_tail(instances, tmap);
return bt_tmap_ref(tmap);
}
void bt_tmap_set_role(struct bt_tmap *tmap, uint16_t role)
{
if (!tmap || tmap->client)
return;
role &= BT_TMAP_ROLE_MASK;
if (role == tmap->db.role_value)
return;
DBG(tmap, "set role 0x%02x", role);
tmap->db.role_value = role;
/* Expose when first set and present. Role does not have Notify. */
gatt_db_service_set_active(tmap->db.service, role != 0);
}