blob: 291135db32ba039a484000586c317aead930fdc3 [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/gmap.h"
#define DBG(_gmap, fmt, arg...) \
gmap_debug(_gmap, "%s:%s() " fmt, __FILE__, __func__, ## arg)
struct bt_gmas_attr {
struct bt_gmap *gmap;
const char *name;
struct gatt_db_attribute *attr;
uint8_t value;
};
struct bt_gmas_db {
struct gatt_db *db;
struct gatt_db_attribute *service;
struct bt_gmas_attr role;
struct bt_gmas_attr ugg;
struct bt_gmas_attr ugt;
struct bt_gmas_attr bgs;
struct bt_gmas_attr bgr;
};
struct bt_gmap {
int ref_count;
struct bt_gatt_client *client;
struct bt_gmas_db db;
int idle_id;
bt_gmap_ready_func_t ready_func;
void *ready_data;
bt_gmap_debug_func_t debug_func;
bt_gmap_destroy_func_t debug_destroy;
void *debug_data;
};
static struct queue *instances;
static void gmap_free(void *data)
{
struct bt_gmap *gmap = data;
if (gmap->client) {
bt_gatt_client_idle_unregister(gmap->client, gmap->idle_id);
bt_gatt_client_unref(gmap->client);
} else {
gatt_db_remove_service(gmap->db.db, gmap->db.service);
gatt_db_unref(gmap->db.db);
}
queue_remove(instances, gmap);
if (queue_isempty(instances)) {
queue_destroy(instances, NULL);
instances = NULL;
}
free(gmap);
}
struct bt_gmap *bt_gmap_ref(struct bt_gmap *gmap)
{
if (!gmap)
return NULL;
__sync_fetch_and_add(&gmap->ref_count, 1);
return gmap;
}
void bt_gmap_unref(struct bt_gmap *gmap)
{
if (!gmap)
return;
if (__sync_sub_and_fetch(&gmap->ref_count, 1))
return;
gmap_free(gmap);
}
static void gmap_debug(struct bt_gmap *gmap, const char *format, ...)
{
va_list ap;
if (!gmap || !format || !gmap->debug_func)
return;
va_start(ap, format);
util_debug_va(gmap->debug_func, gmap->debug_data, format, ap);
va_end(ap);
}
bool bt_gmap_set_debug(struct bt_gmap *gmap, bt_gmap_debug_func_t cb,
void *user_data, bt_gmap_destroy_func_t destroy)
{
if (!gmap)
return false;
if (gmap->debug_destroy)
gmap->debug_destroy(gmap->debug_data);
gmap->debug_func = cb;
gmap->debug_destroy = destroy;
gmap->debug_data = user_data;
return true;
}
uint8_t bt_gmap_get_role(struct bt_gmap *gmap)
{
if (!gmap)
return 0;
return gmap->db.role.value & BT_GMAP_ROLE_MASK;
}
uint32_t bt_gmap_get_features(struct bt_gmap *gmap)
{
if (!gmap)
return 0;
return (((uint32_t)gmap->db.ugg.value << BT_GMAP_UGG_FEATURE_SHIFT) |
((uint32_t)gmap->db.ugt.value << BT_GMAP_UGT_FEATURE_SHIFT) |
((uint32_t)gmap->db.bgs.value << BT_GMAP_BGS_FEATURE_SHIFT) |
((uint32_t)gmap->db.bgr.value << BT_GMAP_BGR_FEATURE_SHIFT)) &
BT_GMAP_FEATURE_MASK;
}
/*
* GMA Client
*/
static void gmap_attr_read(bool success, uint8_t att_ecode,
const uint8_t *value, uint16_t length,
void *user_data)
{
struct bt_gmas_attr *attr = user_data;
struct bt_gmap *gmap = attr->gmap;
struct iovec iov = { .iov_base = (void *)value, .iov_len = length };
uint8_t v;
if (!success) {
DBG(gmap, "Unable to read %s: error 0x%02x",
attr->name, att_ecode);
return;
}
if (!util_iov_pull_u8(&iov, &v)) {
DBG(gmap, "Invalid %s", attr->name);
return;
}
DBG(gmap, "%s Value 0x%x", attr->name, v);
attr->value = v;
}
static void foreach_gmap_char(struct gatt_db_attribute *attr, void *user_data)
{
struct bt_gmap *gmap = user_data;
uint16_t value_handle;
bt_uuid_t uuid, uuid_attr;
struct {
const uint32_t uuid;
struct bt_gmas_attr *attr;
const char *name;
} attrs[] = {
{ GMAP_ROLE_CHRC_UUID, &gmap->db.role, "Role" },
{ GMAP_UGG_CHRC_UUID, &gmap->db.ugg, "UGG Features" },
{ GMAP_UGT_CHRC_UUID, &gmap->db.ugt, "UGT Features" },
{ GMAP_BGS_CHRC_UUID, &gmap->db.bgs, "BGS Features" },
{ GMAP_BGR_CHRC_UUID, &gmap->db.bgr, "BGR Features" },
};
unsigned int i;
if (!gatt_db_attribute_get_char_data(attr, NULL, &value_handle,
NULL, NULL, &uuid))
return;
for (i = 0; i < ARRAY_SIZE(attrs); ++i) {
bt_uuid16_create(&uuid_attr, attrs[i].uuid);
if (bt_uuid_cmp(&uuid, &uuid_attr))
continue;
attrs[i].attr->gmap = gmap;
attrs[i].attr->name = attrs[i].name;
DBG(gmap, "GMAS %s Char found: handle 0x%04x",
attrs[i].name, value_handle);
bt_gatt_client_read_value(gmap->client, value_handle,
gmap_attr_read, attrs[i].attr,
NULL);
return;
}
}
static void foreach_gmap_service(struct gatt_db_attribute *attr,
void *user_data)
{
struct bt_gmap *gmap = user_data;
gatt_db_service_set_claimed(attr, true);
gatt_db_service_foreach_char(attr, foreach_gmap_char, gmap);
}
static void gmap_idle(void *data)
{
struct bt_gmap *gmap = data;
gmap->idle_id = 0;
if (!instances)
instances = queue_new();
queue_push_tail(instances, gmap);
if (gmap->ready_func)
gmap->ready_func(gmap, gmap->ready_data);
}
struct bt_gmap *bt_gmap_attach(struct bt_gatt_client *client,
bt_gmap_ready_func_t ready, void *user_data)
{
struct bt_gmap *gmap;
bt_uuid_t uuid;
if (!client)
return NULL;
client = bt_gatt_client_clone(client);
if (!client)
return NULL;
gmap = new0(struct bt_gmap, 1);
gmap->client = client;
gmap->ready_func = ready;
gmap->ready_data = user_data;
gmap->db.db = bt_gatt_client_get_db(gmap->client);
bt_uuid16_create(&uuid, GMAS_UUID);
gatt_db_foreach_service(gmap->db.db, &uuid, foreach_gmap_service, gmap);
gmap->idle_id = bt_gatt_client_idle_register(gmap->client, gmap_idle,
gmap, NULL);
return bt_gmap_ref(gmap);
}
/*
* GMAS Service
*/
static void gmas_attr_read(struct gatt_db_attribute *attrib,
unsigned int id, uint16_t offset,
uint8_t opcode, struct bt_att *att,
void *user_data)
{
struct bt_gmas_attr *attr = user_data;
struct iovec iov = {
.iov_base = &attr->value,
.iov_len = sizeof(attr->value)
};
gatt_db_attribute_read_result(attrib, id, 0, iov.iov_base,
iov.iov_len);
}
static bool match_db(const void *data, const void *match_data)
{
const struct bt_gmap *gmap = data;
return gmap->db.db == match_data;
}
struct bt_gmap *bt_gmap_find(struct gatt_db *db)
{
return db ? queue_find(instances, match_db, db) : NULL;
}
static void gmap_update_chrc(struct bt_gmap *gmap)
{
struct {
const uint32_t uuid;
uint8_t role;
struct bt_gmas_attr *attr;
const char *name;
} attrs[] = {
{ GMAP_ROLE_CHRC_UUID, 0, &gmap->db.role, "Role" },
{ GMAP_UGG_CHRC_UUID, BT_GMAP_ROLE_UGG, &gmap->db.ugg,
"UGG Features" },
{ GMAP_UGT_CHRC_UUID, BT_GMAP_ROLE_UGT, &gmap->db.ugt,
"UGT Features" },
{ GMAP_BGS_CHRC_UUID, BT_GMAP_ROLE_BGS, &gmap->db.bgs,
"BGS Features" },
{ GMAP_BGR_CHRC_UUID, BT_GMAP_ROLE_BGR, &gmap->db.bgr,
"BGR Features" },
};
unsigned int i;
bt_uuid_t uuid;
for (i = 0; i < ARRAY_SIZE(attrs); ++i) {
if (attrs[i].attr->attr)
continue;
attrs[i].attr->gmap = gmap;
attrs[i].attr->name = attrs[i].name;
if (attrs[i].role && !(gmap->db.role.value & attrs[i].role))
continue;
bt_uuid16_create(&uuid, attrs[i].uuid);
attrs[i].attr->attr = gatt_db_service_add_characteristic(
gmap->db.service,
&uuid,
BT_ATT_PERM_READ,
BT_GATT_CHRC_PROP_READ,
gmas_attr_read, NULL,
attrs[i].attr);
gatt_db_attribute_set_fixed_length(attrs[i].attr->attr, 1);
}
}
static void gmap_init_service(struct bt_gmas_db *db)
{
bt_uuid_t uuid;
if (db->service) {
gatt_db_remove_service(db->db, db->service);
db->service = NULL;
db->role.attr = NULL;
db->ugg.attr = NULL;
db->ugt.attr = NULL;
db->bgs.attr = NULL;
db->bgr.attr = NULL;
}
bt_uuid16_create(&uuid, GMAS_UUID);
db->service = gatt_db_add_service(db->db, &uuid, true, 5*2 + 1);
}
struct bt_gmap *bt_gmap_add_db(struct gatt_db *db)
{
struct bt_gmap *gmap;
if (!db || queue_find(instances, match_db, db))
return NULL;
gmap = new0(struct bt_gmap, 1);
gmap->db.db = gatt_db_ref(db);
gmap_init_service(&gmap->db);
if (!instances)
instances = queue_new();
queue_push_tail(instances, gmap);
return bt_gmap_ref(gmap);
}
void bt_gmap_set_role(struct bt_gmap *gmap, uint8_t role)
{
if (!gmap || gmap->client)
return;
role &= BT_GMAP_ROLE_MASK;
if (role == gmap->db.role.value)
return;
DBG(gmap, "set role 0x%02x", role);
gmap->db.role.value = role;
/* Removing role must remove feature chrc too; reset svc if needed */
if (role && ((!(role & BT_GMAP_ROLE_UGG) && gmap->db.ugg.attr) ||
(!(role & BT_GMAP_ROLE_UGT) && gmap->db.ugt.attr) ||
(!(role & BT_GMAP_ROLE_BGS) && gmap->db.bgs.attr) ||
(!(role & BT_GMAP_ROLE_BGR) && gmap->db.bgr.attr)))
gmap_init_service(&gmap->db);
gmap_update_chrc(gmap);
/* Expose values only when first set and active */
gatt_db_service_set_active(gmap->db.service, role != 0);
}
void bt_gmap_set_features(struct bt_gmap *gmap, uint32_t features)
{
if (!gmap || gmap->client)
return;
features &= BT_GMAP_FEATURE_MASK;
if (features == bt_gmap_get_features(gmap))
return;
DBG(gmap, "set features 0x%08x", features);
gmap->db.ugg.value = (features & BT_GMAP_UGG_FEATURE_MASK)
>> BT_GMAP_UGG_FEATURE_SHIFT;
gmap->db.ugt.value = (features & BT_GMAP_UGT_FEATURE_MASK)
>> BT_GMAP_UGT_FEATURE_SHIFT;
gmap->db.bgs.value = (features & BT_GMAP_BGS_FEATURE_MASK)
>> BT_GMAP_BGS_FEATURE_SHIFT;
gmap->db.bgr.value = (features & BT_GMAP_BGR_FEATURE_MASK)
>> BT_GMAP_BGR_FEATURE_SHIFT;
}