blob: 5bc16ee686f1544cda176bd4c801bd3f8414572d [file] [log] [blame]
/*
* Copyright (c) 2023 Nordic Semiconductor ASA
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <zephyr/bluetooth/gatt.h>
#include <zephyr/bluetooth/audio/gmap.h>
#include <zephyr/sys/check.h>
#include <zephyr/logging/log.h>
#include "audio_internal.h"
LOG_MODULE_REGISTER(bt_gmap_server, CONFIG_BT_GMAP_LOG_LEVEL);
#define BT_GMAP_ROLE_MASK \
(BT_GMAP_ROLE_UGG | BT_GMAP_ROLE_UGT | BT_GMAP_ROLE_BGS | BT_GMAP_ROLE_BGR)
static uint8_t gmap_role;
static struct bt_gmap_feat gmap_features;
static ssize_t read_gmap_role(struct bt_conn *conn, const struct bt_gatt_attr *attr, void *buf,
uint16_t len, uint16_t offset)
{
LOG_DBG("role 0x%02X", gmap_role);
return bt_gatt_attr_read(conn, attr, buf, len, offset, &gmap_role, sizeof(gmap_role));
}
#if defined(CONFIG_BT_GMAP_UGG_SUPPORTED)
static ssize_t read_gmap_ugg_feat(struct bt_conn *conn, const struct bt_gatt_attr *attr, void *buf,
uint16_t len, uint16_t offset)
{
const uint8_t feat = (uint8_t)gmap_features.ugg_feat;
LOG_DBG("feat 0x%02X", feat);
return bt_gatt_attr_read(conn, attr, buf, len, offset, &feat, sizeof(feat));
}
static const struct bt_gatt_attr ugg_feat_chrc[] = {
BT_AUDIO_CHRC(BT_UUID_GMAP_UGG_FEAT, BT_GATT_CHRC_READ, BT_GATT_PERM_READ_ENCRYPT,
read_gmap_ugg_feat, NULL, NULL),
};
#endif /* CONFIG_BT_GMAP_UGG_SUPPORTED */
#if defined(CONFIG_BT_GMAP_UGT_SUPPORTED)
static ssize_t read_gmap_ugt_feat(struct bt_conn *conn, const struct bt_gatt_attr *attr, void *buf,
uint16_t len, uint16_t offset)
{
const uint8_t feat = (uint8_t)gmap_features.ugt_feat;
LOG_DBG("feat 0x%02X", feat);
return bt_gatt_attr_read(conn, attr, buf, len, offset, &feat, sizeof(feat));
}
static const struct bt_gatt_attr ugt_feat_chrc[] = {
BT_AUDIO_CHRC(BT_UUID_GMAP_UGT_FEAT, BT_GATT_CHRC_READ, BT_GATT_PERM_READ_ENCRYPT,
read_gmap_ugt_feat, NULL, NULL),
};
#endif /* CONFIG_BT_GMAP_UGT_SUPPORTED */
#if defined(CONFIG_BT_GMAP_BGS_SUPPORTED)
static ssize_t read_gmap_bgs_feat(struct bt_conn *conn, const struct bt_gatt_attr *attr, void *buf,
uint16_t len, uint16_t offset)
{
const uint8_t feat = (uint8_t)gmap_features.bgs_feat;
LOG_DBG("feat 0x%02X", feat);
return bt_gatt_attr_read(conn, attr, buf, len, offset, &feat, sizeof(feat));
}
static const struct bt_gatt_attr bgs_feat_chrc[] = {
BT_AUDIO_CHRC(BT_UUID_GMAP_BGS_FEAT, BT_GATT_CHRC_READ, BT_GATT_PERM_READ_ENCRYPT,
read_gmap_bgs_feat, NULL, NULL),
};
#endif /* CONFIG_BT_GMAP_BGS_SUPPORTED */
#if defined(CONFIG_BT_GMAP_BGR_SUPPORTED)
static ssize_t read_gmap_bgr_feat(struct bt_conn *conn, const struct bt_gatt_attr *attr, void *buf,
uint16_t len, uint16_t offset)
{
const uint8_t feat = (uint8_t)gmap_features.bgr_feat;
LOG_DBG("feat 0x%02X", feat);
return bt_gatt_attr_read(conn, attr, buf, len, offset, &feat, sizeof(feat));
}
static const struct bt_gatt_attr bgr_feat_chrc[] = {
BT_AUDIO_CHRC(BT_UUID_GMAP_BGR_FEAT, BT_GATT_CHRC_READ, BT_GATT_PERM_READ_ENCRYPT,
read_gmap_bgr_feat, NULL, NULL),
};
#endif /* CONFIG_BT_GMAP_BGR_SUPPORTED */
/* There are 4 optional characteristics - Use a dummy definition to allocate memory and then add the
* characteristics at will when registering or modifying the role(s)
*/
#define GMAS_DUMMY_CHRC BT_AUDIO_CHRC(0, 0, 0, NULL, NULL, NULL)
/* Gaming Audio Service attributes */
static struct bt_gatt_attr svc_attrs[] = {
BT_GATT_PRIMARY_SERVICE(BT_UUID_GMAS),
BT_AUDIO_CHRC(BT_UUID_GMAP_ROLE, BT_GATT_CHRC_READ, BT_GATT_PERM_READ_ENCRYPT,
read_gmap_role, NULL, NULL),
#if defined(CONFIG_BT_GMAP_UGG_SUPPORTED)
GMAS_DUMMY_CHRC,
#endif /* CONFIG_BT_GMAP_UGG_SUPPORTED */
#if defined(CONFIG_BT_GMAP_UGT_SUPPORTED)
GMAS_DUMMY_CHRC,
#endif /* CONFIG_BT_GMAP_UGT_SUPPORTED */
#if defined(CONFIG_BT_GMAP_BGS_SUPPORTED)
GMAS_DUMMY_CHRC,
#endif /* CONFIG_BT_GMAP_BGS_SUPPORTED */
#if defined(CONFIG_BT_GMAP_BGR_SUPPORTED)
GMAS_DUMMY_CHRC,
#endif /* CONFIG_BT_GMAP_BGR_SUPPORTED */
};
static struct bt_gatt_service gmas;
static bool valid_gmap_role(enum bt_gmap_role role)
{
if (role == 0 || (role & BT_GMAP_ROLE_MASK) != role) {
LOG_DBG("Invalid role %d", role);
}
if ((role & BT_GMAP_ROLE_UGG) != 0 && !IS_ENABLED(CONFIG_BT_GMAP_UGG_SUPPORTED)) {
LOG_DBG("Device does not support the UGG role");
return false;
}
if ((role & BT_GMAP_ROLE_UGT) != 0 && !IS_ENABLED(CONFIG_BT_GMAP_UGT_SUPPORTED)) {
LOG_DBG("Device does not support the UGT role");
return false;
}
if ((role & BT_GMAP_ROLE_BGS) != 0 && !IS_ENABLED(CONFIG_BT_GMAP_BGS_SUPPORTED)) {
LOG_DBG("Device does not support the BGS role");
return false;
}
if ((role & BT_GMAP_ROLE_BGR) != 0 && !IS_ENABLED(CONFIG_BT_GMAP_BGR_SUPPORTED)) {
LOG_DBG("Device does not support the BGR role");
return false;
}
return true;
}
static bool valid_gmap_features(enum bt_gmap_role role, struct bt_gmap_feat features)
{
/* Guard with BT_GMAP_UGG_SUPPORTED as the Kconfigs may not be available without it*/
#if defined(CONFIG_BT_GMAP_UGG_SUPPORTED)
if ((role & BT_GMAP_ROLE_UGG) != 0) {
enum bt_gmap_ugg_feat ugg_feat = features.ugg_feat;
if ((ugg_feat & BT_GMAP_UGG_FEAT_MULTIPLEX) != 0 &&
CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SRC_COUNT == 0) {
LOG_DBG("Cannot support BT_GMAP_UGG_FEAT_MULTIPLEX with "
"CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SRC_COUNT == 0");
return false;
}
if ((ugg_feat & BT_GMAP_UGG_FEAT_96KBPS_SOURCE) != 0 &&
CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SRC_COUNT == 0) {
LOG_DBG("Cannot support BT_GMAP_UGG_FEAT_96KBPS_SOURCE with "
"CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SRC_COUNT == 0");
return false;
}
if ((ugg_feat & BT_GMAP_UGG_FEAT_MULTISINK) != 0 &&
(CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SNK_COUNT < 2 ||
CONFIG_BT_BAP_UNICAST_CLIENT_GROUP_STREAM_COUNT < 2)) {
LOG_DBG("Cannot support BT_GMAP_UGG_FEAT_MULTISINK with "
"CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SNK_COUNT (%d) or "
"CONFIG_BT_BAP_UNICAST_CLIENT_GROUP_STREAM_COUNT (%d) < 2",
CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SNK_COUNT,
CONFIG_BT_BAP_UNICAST_CLIENT_GROUP_STREAM_COUNT);
return false;
}
}
#endif /* CONFIG_BT_BAP_UNICAST_CLIENT */
#if defined(CONFIG_BT_GMAP_UGT_SUPPORTED)
if ((role & BT_GMAP_ROLE_UGT) != 0) {
enum bt_gmap_ugt_feat ugt_feat = features.ugt_feat;
enum bt_gmap_bgr_feat bgr_feat = features.bgr_feat;
if ((ugt_feat & BT_GMAP_UGT_FEAT_SOURCE) == 0 &&
(ugt_feat & BT_GMAP_UGT_FEAT_SINK) == 0) {
LOG_DBG("Device shall support either BT_GMAP_UGT_FEAT_SOURCE or "
"BT_GMAP_UGT_FEAT_SINK");
return false;
}
if ((ugt_feat & BT_GMAP_UGT_FEAT_SOURCE) == 0 &&
((ugt_feat & BT_GMAP_UGT_FEAT_80KBPS_SOURCE) != 0 ||
(ugt_feat & BT_GMAP_UGT_FEAT_MULTISOURCE) != 0)) {
LOG_DBG("Device shall support BT_GMAP_UGT_FEAT_SOURCE if "
"BT_GMAP_UGT_FEAT_80KBPS_SOURCE or BT_GMAP_UGT_FEAT_MULTISOURCE is "
"supported");
return false;
}
if ((ugt_feat & BT_GMAP_UGT_FEAT_SOURCE) != 0 &&
CONFIG_BT_ASCS_ASE_SRC_COUNT == 0) {
LOG_DBG("Cannot support BT_GMAP_UGT_FEAT_SOURCE with "
"CONFIG_BT_ASCS_ASE_SRC_COUNT == 0");
return false;
}
if ((ugt_feat & BT_GMAP_UGT_FEAT_MULTISOURCE) != 0 &&
(CONFIG_BT_ASCS_ASE_SRC_COUNT < 2 || CONFIG_BT_ASCS_MAX_ACTIVE_ASES < 2)) {
LOG_DBG("Cannot support BT_GMAP_UGT_FEAT_MULTISOURCE with "
"CONFIG_BT_ASCS_ASE_SRC_COUNT (%d) or "
"CONFIG_BT_ASCS_MAX_ACTIVE_ASES (%d) < 2",
CONFIG_BT_ASCS_ASE_SRC_COUNT, CONFIG_BT_ASCS_MAX_ACTIVE_ASES);
return false;
}
if ((ugt_feat & BT_GMAP_UGT_FEAT_SINK) != 0 && CONFIG_BT_ASCS_ASE_SNK_COUNT == 0) {
LOG_DBG("Cannot support BT_GMAP_UGT_FEAT_SINK with "
"CONFIG_BT_ASCS_ASE_SNK_COUNT == 0");
return false;
}
if ((ugt_feat & BT_GMAP_UGT_FEAT_SINK) == 0 &&
((ugt_feat & BT_GMAP_UGT_FEAT_64KBPS_SINK) != 0 ||
(ugt_feat & BT_GMAP_UGT_FEAT_MULTISINK) != 0 ||
(ugt_feat & BT_GMAP_UGT_FEAT_MULTIPLEX) != 0)) {
LOG_DBG("Device shall support BT_GMAP_UGT_FEAT_SINK if "
"BT_GMAP_UGT_FEAT_64KBPS_SINK, BT_GMAP_UGT_FEAT_MULTISINK or "
"BT_GMAP_UGT_FEAT_MULTIPLEX is supported");
return false;
}
if ((ugt_feat & BT_GMAP_UGT_FEAT_MULTISINK) != 0 &&
(CONFIG_BT_ASCS_ASE_SNK_COUNT < 2 || CONFIG_BT_ASCS_MAX_ACTIVE_ASES < 2)) {
LOG_DBG("Cannot support BT_GMAP_UGT_FEAT_MULTISINK with "
"CONFIG_BT_ASCS_ASE_SNK_COUNT (%d) or "
"CONFIG_BT_ASCS_MAX_ACTIVE_ASES (%d) < 2",
CONFIG_BT_ASCS_ASE_SNK_COUNT, CONFIG_BT_ASCS_MAX_ACTIVE_ASES);
return false;
}
/* If the device supports both the UGT and BGT roles, then it needs have the same
* support for multiplexing for both roles
*/
if ((role & BT_GMAP_ROLE_BGR) != 0 && !IS_ENABLED(CONFIG_BT_GMAP_BGR_SUPPORTED) &&
(ugt_feat & BT_GMAP_UGT_FEAT_MULTIPLEX) !=
(bgr_feat & BT_GMAP_BGR_FEAT_MULTIPLEX)) {
LOG_DBG("Device shall support BT_GMAP_UGT_FEAT_MULTIPLEX if "
"BT_GMAP_BGR_FEAT_MULTIPLEX is supported, and vice versa");
}
}
#endif /* CONFIG_BT_GMAP_UGT_SUPPORTED */
#if defined(CONFIG_BT_GMAP_BGR_SUPPORTED)
if ((role & BT_GMAP_ROLE_BGR) != 0) {
enum bt_gmap_bgr_feat bgr_feat = features.bgr_feat;
if ((bgr_feat & BT_GMAP_BGR_FEAT_MULTISINK) != 0 &&
CONFIG_BT_BAP_BROADCAST_SNK_STREAM_COUNT < 2) {
LOG_DBG("Cannot support BT_GMAP_BGR_FEAT_MULTISINK with "
"CONFIG_BT_BAP_BROADCAST_SNK_STREAM_COUNT (%d) < 2",
CONFIG_BT_BAP_BROADCAST_SNK_STREAM_COUNT);
return false;
}
}
#endif /* CONFIG_BT_GMAP_BGR_SUPPORTED */
/* If the roles are not supported, then the feature characteristics are not instantiated and
* the feature values do not need to be checked, as they will never be read (thus ignore by
* the stack)
*/
return true;
}
static void update_service(enum bt_gmap_role role)
{
gmas.attrs = svc_attrs;
gmas.attr_count = 3; /* service + 2 attributes for BT_UUID_GMAP_ROLE */
/* Add characteristics based on the role selected and what is supported */
#if defined(CONFIG_BT_GMAP_UGG_SUPPORTED)
if (role & BT_GMAP_ROLE_UGG) {
memcpy(&gmas.attrs[gmas.attr_count], ugg_feat_chrc, sizeof(ugg_feat_chrc));
gmas.attr_count += ARRAY_SIZE(ugg_feat_chrc);
}
#endif /* CONFIG_BT_GMAP_UGG_SUPPORTED */
#if defined(CONFIG_BT_GMAP_UGT_SUPPORTED)
if (role & BT_GMAP_ROLE_UGT) {
memcpy(&gmas.attrs[gmas.attr_count], ugt_feat_chrc, sizeof(ugt_feat_chrc));
gmas.attr_count += ARRAY_SIZE(ugt_feat_chrc);
}
#endif /* CONFIG_BT_GMAP_UGT_SUPPORTED */
#if defined(CONFIG_BT_GMAP_BGS_SUPPORTED)
if (role & BT_GMAP_ROLE_BGS) {
memcpy(&gmas.attrs[gmas.attr_count], bgs_feat_chrc, sizeof(bgs_feat_chrc));
gmas.attr_count += ARRAY_SIZE(bgs_feat_chrc);
}
#endif /* CONFIG_BT_GMAP_BGS_SUPPORTED */
#if defined(CONFIG_BT_GMAP_BGR_SUPPORTED)
if (role & BT_GMAP_ROLE_BGR) {
memcpy(&gmas.attrs[gmas.attr_count], bgr_feat_chrc, sizeof(bgr_feat_chrc));
gmas.attr_count += ARRAY_SIZE(bgr_feat_chrc);
}
#endif /* CONFIG_BT_GMAP_BGR_SUPPORTED */
}
int bt_gmap_register(enum bt_gmap_role role, struct bt_gmap_feat features)
{
int err;
CHECKIF(!valid_gmap_role(role)) {
LOG_DBG("Invalid role: %d", role);
return -EINVAL;
}
CHECKIF(!valid_gmap_features(role, features)) {
LOG_DBG("Invalid features");
return -EINVAL;
}
update_service(role);
err = bt_gatt_service_register(&gmas);
if (err) {
LOG_DBG("Could not register the GMAS service");
return -ENOEXEC;
}
gmap_role = role;
gmap_features = features;
return 0;
}
int bt_gmap_set_role(enum bt_gmap_role role, struct bt_gmap_feat features)
{
int err;
if (gmap_role == 0) { /* not registered if this is 0 */
LOG_DBG("GMAP not registered");
return -ENOEXEC;
}
CHECKIF(!valid_gmap_role(role)) {
LOG_DBG("Invalid role: %d", role);
return -EINVAL;
}
CHECKIF(!valid_gmap_features(role, features)) {
LOG_DBG("Invalid features");
return -EINVAL;
}
if (gmap_role == role) {
LOG_DBG("No role change");
if (memcmp(&gmap_features, &features, sizeof(gmap_features)) == 0) {
LOG_DBG("No feature change");
return -EALREADY;
}
gmap_features = features;
return 0;
}
/* Re-register the service to trigger a db_changed() if the roles changed */
err = bt_gatt_service_unregister(&gmas);
if (err != 0) {
LOG_DBG("Failed to unregister service: %d", err);
return -ENOENT;
}
err = bt_gmap_register(role, gmap_features);
if (err != 0) {
LOG_DBG("Failed to update GMAS: %d", err);
return -ECANCELED;
}
return 0;
}