* [PATCH BlueZ v2 1/9] shared/gmap: add GMAP Service
2025-11-28 20:02 [PATCH BlueZ v2 0/9] Add TMAP & GMAP information services Pauli Virtanen
@ 2025-11-28 20:02 ` Pauli Virtanen
2025-11-28 20:58 ` Add TMAP & GMAP information services bluez.test.bot
2025-11-28 20:02 ` [PATCH BlueZ v2 2/9] test-gmap: add test for GMAP Service Pauli Virtanen
` (9 subsequent siblings)
10 siblings, 1 reply; 16+ messages in thread
From: Pauli Virtanen @ 2025-11-28 20:02 UTC (permalink / raw)
To: linux-bluetooth; +Cc: Pauli Virtanen
GMAP Service is just a simple service with bitmasks.
The values can be used to figure out which GMAP roles and features the
remote device claims it supports (matters for available mandatory
features). Also can advertise the same for remote clients.
---
Notes:
v2:
- remove one unnecessary #include
Makefile.am | 1 +
lib/bluetooth/uuid.h | 8 +
src/shared/gmap.c | 401 +++++++++++++++++++++++++++++++++++++++++++
src/shared/gmap.h | 70 ++++++++
4 files changed, 480 insertions(+)
create mode 100644 src/shared/gmap.c
create mode 100644 src/shared/gmap.h
diff --git a/Makefile.am b/Makefile.am
index 11e632c02..14dccafaf 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -243,6 +243,7 @@ shared_sources = src/shared/io.h src/shared/timeout.h \
src/shared/bass.h src/shared/bass.c \
src/shared/ccp.h src/shared/ccp.c \
src/shared/tmap.c src/shared/tmap.h \
+ src/shared/gmap.c src/shared/gmap.h \
src/shared/lc3.h src/shared/tty.h \
src/shared/bap-defs.h \
src/shared/asha.h src/shared/asha.c \
diff --git a/lib/bluetooth/uuid.h b/lib/bluetooth/uuid.h
index 771f7675d..82e948a23 100644
--- a/lib/bluetooth/uuid.h
+++ b/lib/bluetooth/uuid.h
@@ -216,6 +216,14 @@ extern "C" {
#define TMAS_UUID 0x1855
#define TMAP_ROLE_CHRC_UUID 0x2b51
+/* Gaming Audio Service */
+#define GMAS_UUID 0x1858
+#define GMAP_ROLE_CHRC_UUID 0x2c00
+#define GMAP_UGG_CHRC_UUID 0x2c01
+#define GMAP_UGT_CHRC_UUID 0x2c02
+#define GMAP_BGS_CHRC_UUID 0x2c03
+#define GMAP_BGR_CHRC_UUID 0x2c04
+
/* Coordinated Set Identification Profile(CSIP) */
#define CSIS_UUID 0x1846
#define CS_SIRK 0x2B84
diff --git a/src/shared/gmap.c b/src/shared/gmap.c
new file mode 100644
index 000000000..f571f3270
--- /dev/null
+++ b/src/shared/gmap.c
@@ -0,0 +1,401 @@
+// 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);
+ }
+}
+
+struct bt_gmap *bt_gmap_add_db(struct gatt_db *db)
+{
+ struct bt_gmap *gmap;
+ bt_uuid_t uuid;
+
+ if (!db || queue_find(instances, match_db, db))
+ return NULL;
+
+ gmap = new0(struct bt_gmap, 1);
+ gmap->db.db = gatt_db_ref(db);
+
+ bt_uuid16_create(&uuid, GMAS_UUID);
+ gmap->db.service = gatt_db_add_service(db, &uuid, true, 5*3);
+
+ 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->client)
+ return;
+
+ gmap->db.role.value = role & BT_GMAP_ROLE_MASK;
+
+ gmap_update_chrc(gmap);
+
+ /* Expose values only when first set */
+ gatt_db_service_set_active(gmap->db.service, true);
+}
+
+
+void bt_gmap_set_features(struct bt_gmap *gmap, uint32_t features)
+{
+ if (gmap->client)
+ return;
+
+ 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;
+
+ gmap_update_chrc(gmap);
+
+ /* Expose values only when first set */
+ gatt_db_service_set_active(gmap->db.service, true);
+}
diff --git a/src/shared/gmap.h b/src/shared/gmap.h
new file mode 100644
index 000000000..ef7d96e0a
--- /dev/null
+++ b/src/shared/gmap.h
@@ -0,0 +1,70 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ *
+ * BlueZ - Bluetooth protocol stack for Linux
+ *
+ * Copyright (C) 2025 Pauli Virtanen. All rights reserved.
+ *
+ */
+
+#include <stdbool.h>
+#include <inttypes.h>
+
+#define BT_GMAP_ROLE_UGG BIT(0)
+#define BT_GMAP_ROLE_UGT BIT(1)
+#define BT_GMAP_ROLE_BGS BIT(2)
+#define BT_GMAP_ROLE_BGR BIT(3)
+#define BT_GMAP_ROLE_MASK (BIT(4) - 1)
+
+#define BT_GMAP_UGG_MULTIPLEX (BIT(0) << 0)
+#define BT_GMAP_UGG_96KBPS (BIT(1) << 0)
+#define BT_GMAP_UGG_MULTISINK (BIT(2) << 0)
+#define BT_GMAP_UGG_FEATURE_MASK ((BIT(3) - 1) << 0)
+#define BT_GMAP_UGG_FEATURE_SHIFT 0
+
+#define BT_GMAP_UGT_SOURCE (BIT(0) << 8)
+#define BT_GMAP_UGT_80KBPS_SOURCE (BIT(1) << 8)
+#define BT_GMAP_UGT_SINK (BIT(2) << 8)
+#define BT_GMAP_UGT_64KBPS_SINK (BIT(3) << 8)
+#define BT_GMAP_UGT_MULTIPLEX (BIT(4) << 8)
+#define BT_GMAP_UGT_MULTISINK (BIT(5) << 8)
+#define BT_GMAP_UGT_MULTISOURCE (BIT(6) << 8)
+#define BT_GMAP_UGT_FEATURE_MASK ((BIT(7) - 1) << 8)
+#define BT_GMAP_UGT_FEATURE_SHIFT 8
+
+#define BT_GMAP_BGS_96KBPS (BIT(0) << 16)
+#define BT_GMAP_BGS_FEATURE_MASK ((BIT(1) - 1) << 16)
+#define BT_GMAP_BGS_FEATURE_SHIFT 16
+
+#define BT_GMAP_BGR_MULTISINK (BIT(0) << 24)
+#define BT_GMAP_BGR_MULTIPLEX (BIT(1) << 24)
+#define BT_GMAP_BGR_FEATURE_MASK ((BIT(2) - 1) << 24)
+#define BT_GMAP_BGR_FEATURE_SHIFT 24
+
+#define BT_GMAP_FEATURE_MASK (BT_GMAP_UGG_FEATURE_MASK | \
+ BT_GMAP_UGT_FEATURE_MASK | \
+ BT_GMAP_BGS_FEATURE_MASK | \
+ BT_GMAP_BGR_FEATURE_MASK)
+
+struct bt_gmap;
+
+typedef void (*bt_gmap_ready_func_t)(struct bt_gmap *gmap, void *user_data);
+typedef void (*bt_gmap_destroy_func_t)(void *user_data);
+typedef void (*bt_gmap_debug_func_t)(const char *str, void *user_data);
+
+struct bt_gmap *bt_gmap_ref(struct bt_gmap *gmap);
+void bt_gmap_unref(struct bt_gmap *gmap);
+
+struct bt_gmap *bt_gmap_attach(struct bt_gatt_client *client,
+ bt_gmap_ready_func_t ready, void *user_data);
+struct bt_gmap *bt_gmap_find(struct gatt_db *db);
+struct bt_gmap *bt_gmap_add_db(struct gatt_db *db);
+
+uint8_t bt_gmap_get_role(struct bt_gmap *gmap);
+uint32_t bt_gmap_get_features(struct bt_gmap *gmap);
+
+void bt_gmap_set_role(struct bt_gmap *gmas, uint8_t role);
+void bt_gmap_set_features(struct bt_gmap *gmas, uint32_t features);
+
+bool bt_gmap_set_debug(struct bt_gmap *gmap, bt_gmap_debug_func_t cb,
+ void *user_data, bt_gmap_destroy_func_t destroy);
--
2.51.1
^ permalink raw reply related [flat|nested] 16+ messages in thread* RE: Add TMAP & GMAP information services
2025-11-28 20:02 ` [PATCH BlueZ v2 1/9] shared/gmap: add GMAP Service Pauli Virtanen
@ 2025-11-28 20:58 ` bluez.test.bot
0 siblings, 0 replies; 16+ messages in thread
From: bluez.test.bot @ 2025-11-28 20:58 UTC (permalink / raw)
To: linux-bluetooth, pav
[-- Attachment #1: Type: text/plain, Size: 1261 bytes --]
This is automated email and please do not reply to this email!
Dear submitter,
Thank you for submitting the patches to the linux bluetooth mailing list.
This is a CI test results with your patch series:
PW Link:https://patchwork.kernel.org/project/bluetooth/list/?series=1028785
---Test result---
Test Summary:
CheckPatch PENDING 0.33 seconds
GitLint PENDING 0.30 seconds
BuildEll PASS 20.19 seconds
BluezMake PASS 637.42 seconds
MakeCheck PASS 21.70 seconds
MakeDistcheck PASS 239.12 seconds
CheckValgrind PASS 297.87 seconds
CheckSmatch PASS 344.65 seconds
bluezmakeextell PASS 180.66 seconds
IncrementalBuild PENDING 0.25 seconds
ScanBuild PASS 976.58 seconds
Details
##############################
Test: CheckPatch - PENDING
Desc: Run checkpatch.pl script
Output:
##############################
Test: GitLint - PENDING
Desc: Run gitlint
Output:
##############################
Test: IncrementalBuild - PENDING
Desc: Incremental build with the patches in the series
Output:
---
Regards,
Linux Bluetooth
^ permalink raw reply [flat|nested] 16+ messages in thread
* [PATCH BlueZ v2 2/9] test-gmap: add test for GMAP Service
2025-11-28 20:02 [PATCH BlueZ v2 0/9] Add TMAP & GMAP information services Pauli Virtanen
2025-11-28 20:02 ` [PATCH BlueZ v2 1/9] shared/gmap: add GMAP Service Pauli Virtanen
@ 2025-11-28 20:02 ` Pauli Virtanen
2025-11-28 20:02 ` [PATCH BlueZ v2 3/9] gmap: Add GMAP profile Pauli Virtanen
` (8 subsequent siblings)
10 siblings, 0 replies; 16+ messages in thread
From: Pauli Virtanen @ 2025-11-28 20:02 UTC (permalink / raw)
To: linux-bluetooth; +Cc: Pauli Virtanen
Add tests on GMAP service for reading the attributes.
---
Notes:
v2:
- no change
.gitignore | 1 +
Makefile.am | 6 +
unit/test-gmap.c | 496 +++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 503 insertions(+)
create mode 100644 unit/test-gmap.c
diff --git a/.gitignore b/.gitignore
index 48bf72a4a..ba29d9d5e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -119,6 +119,7 @@ unit/test-bap
unit/test-bass
unit/test-battery
unit/test-tmap
+unit/test-gmap
tools/mgmt-tester
tools/smp-tester
tools/gap-tester
diff --git a/Makefile.am b/Makefile.am
index 14dccafaf..7221e4cba 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -709,6 +709,12 @@ unit_test_tmap_SOURCES = unit/test-tmap.c $(btio_sources)
unit_test_tmap_LDADD = src/libshared-glib.la \
lib/libbluetooth-internal.la $(GLIB_LIBS)
+unit_tests += unit/test-gmap
+
+unit_test_gmap_SOURCES = unit/test-gmap.c $(btio_sources)
+unit_test_gmap_LDADD = src/libshared-glib.la \
+ lib/libbluetooth-internal.la $(GLIB_LIBS)
+
unit_tests += unit/test-battery
unit_test_battery_SOURCES = unit/test-battery.c
diff --git a/unit/test-gmap.c b/unit/test-gmap.c
new file mode 100644
index 000000000..6102ce65d
--- /dev/null
+++ b/unit/test-gmap.c
@@ -0,0 +1,496 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+/*
+ *
+ * BlueZ - Bluetooth protocol stack for Linux
+ *
+ * Copyright (C) 2025 Pauli Virtanen. All rights reserved.
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#define _GNU_SOURCE
+#include <unistd.h>
+#include <string.h>
+#include <sys/socket.h>
+#include <fcntl.h>
+
+#include <glib.h>
+
+#include "bluetooth/bluetooth.h"
+#include "bluetooth/uuid.h"
+#include "src/shared/util.h"
+#include "src/shared/tester.h"
+#include "src/shared/queue.h"
+#include "src/shared/att.h"
+#include "src/shared/gatt-db.h"
+#include "src/shared/gatt-client.h"
+#include "src/shared/gatt-server.h"
+#include "src/shared/io.h"
+#include "src/shared/gmap.h"
+
+struct test_config {
+ uint8_t role;
+ uint32_t features;
+ const struct iovec *setup_data;
+ const size_t setup_data_len;
+};
+
+struct test_data {
+ struct gatt_db *db;
+ struct bt_gatt_server *server;
+ struct bt_gatt_client *client;
+ struct bt_gmap *gmap;
+ size_t iovcnt;
+ struct iovec *iov;
+ const struct test_config *cfg;
+};
+
+#define iov_data(args...) ((const struct iovec[]) { args })
+
+#define define_test(name, setup, function, _cfg, args...) \
+ do { \
+ const struct iovec iov[] = { args }; \
+ static struct test_data data; \
+ data.iovcnt = ARRAY_SIZE(iov_data(args)); \
+ data.iov = util_iov_dup(iov, ARRAY_SIZE(iov_data(args))); \
+ data.cfg = _cfg; \
+ tester_add(name, &data, setup, function, \
+ test_teardown); \
+ } while (0)
+
+static void print_debug(const char *str, void *user_data)
+{
+ const char *prefix = user_data;
+
+ if (tester_use_debug())
+ tester_debug("%s%s", prefix, str);
+}
+
+static void test_teardown(const void *user_data)
+{
+ struct test_data *data = (void *)user_data;
+
+ bt_gatt_client_unref(data->client);
+
+ bt_gatt_server_unref(data->server);
+ util_iov_free(data->iov, data->iovcnt);
+
+ gatt_db_unref(data->db);
+
+ bt_gmap_unref(data->gmap);
+ tester_teardown_complete();
+}
+
+/* ATT: Exchange MTU Response (0x03) len 2
+ * Server RX MTU: 64
+ * ATT: Exchange MTU Request (0x02) len 2
+ * Client RX MTU: 64
+ * ATT: Read By Type Request (0x08) len 6
+ * Handle range: 0x0001-0xffff
+ * Attribute type: Server Supported Features (0x2b3a)
+ * ATT: Error Response (0x01) len 4
+ * Read By Type Request (0x08)
+ * Handle: 0x0001
+ * Error: Attribute Not Found (0x0a)
+ */
+#define GMAS_MTU_FEAT \
+ IOV_DATA(0x02, 0x40, 0x00), \
+ IOV_DATA(0x03, 0x40, 0x00), \
+ IOV_DATA(0x08, 0x01, 0x00, 0xff, 0xff, 0x3a, 0x2b), \
+ IOV_DATA(0x01, 0x08, 0x01, 0x00, 0x0a)
+
+/* ATT: Read By Group Type Request (0x10) len 6
+ * Handle range: 0x0001-0xffff
+ * Attribute group type: Primary Service (0x2800)
+ * ATT: Read By Group Type Response (0x11) len 37
+ * Attribute data length: 6
+ * Attribute group list: 1 entries
+ * Handle range: 0x0001-0x000f
+ * UUID: Gaming Audio Service (0x1858)
+ * ATT: Read By Group Type Request (0x10) len 6
+ * Handle range: 0x0006-0xffff
+ * Attribute group type: Primary Service (0x2800)
+ * ATT: Error Response (0x01) len 4
+ * Read By Group Type Request (0x10)
+ * Handle: 0x0004
+ * Error: Attribute Not Found (0x0a)
+ */
+#define GMAS_PRIMARY_SERVICE \
+ IOV_DATA(0x10, 0x01, 0x00, 0xff, 0xff, 0x00, 0x28), \
+ IOV_DATA(0x11, 0x06, \
+ 0x01, 0x00, 0x0f, 0x00, 0x58, 0x18), \
+ IOV_DATA(0x10, 0x10, 0x00, 0xff, 0xff, 0x00, 0x28), \
+ IOV_DATA(0x01, 0x10, 0x10, 0x00, 0x0a)
+
+
+/* ATT: Read By Group Type Request (0x10) len 6
+ * Handle range: 0x0001-0xffff
+ * Attribute group type: Secondary Service (0x2801)
+ * ATT: Error Response (0x01) len 4
+ * Read By Group Type Request (0x10)
+ * Handle: 0x0001
+ * Error: Attribute Not Found (0x0a)
+ */
+#define GMAS_SECONDARY_SERVICE \
+ IOV_DATA(0x10, 0x01, 0x00, 0xff, 0xff, 0x01, 0x28), \
+ IOV_DATA(0x01, 0x10, 0x01, 0x00, 0x0a)
+
+/* ATT: Read By Type Request (0x08) len 6
+ * Handle range: 0x0001-0x0005
+ * Attribute group type: Include (0x2802)
+ * ATT: Error Response (0x01) len 4
+ * Read By Group Type Request (0x10)
+ * Handle: 0x0001
+ * Error: Attribute Not Found (0x0a)
+ */
+#define GMAS_INCLUDE \
+ IOV_DATA(0x08, 0x01, 0x00, 0x0f, 0x00, 0x02, 0x28), \
+ IOV_DATA(0x01, 0x08, 0x01, 0x00, 0x0a)
+
+/* ATT: Read By Type Request (0x08) len 6
+ * Handle range: 0x0001-0x0003
+ * Attribute type: Characteristic (0x2803)
+ * ATT: Read By Type Response (0x09) len 57
+ * Attribute data length: 7
+ * Attribute data list: 8 entries
+ * Handle: 0x0002
+ * Value: 020300512b
+ * Properties: 0x02
+ * Read (0x02)
+ * Value Handle: 0x0003
+ * Value UUID: GMAP Role (0x2c00)
+ * ATT: Read By Type Response (0x09) len 57
+ * Attribute data length: 7
+ * Attribute data list: 8 entries
+ * Handle: 0x0004
+ * Value: 020300512b
+ * Properties: 0x02
+ * Read (0x02)
+ * Value Handle: 0x0005
+ * Value UUID: GMAP Features ({uuid})
+ * ATT: Read By Type Request (0x08) len 6
+ * Handle range: 0x0003-0x0004
+ * Attribute type: Characteristic (0x2803)
+ * ATT: Error Response (0x01) len 4
+ * Read By Type Request (0x08)
+ * Handle: 0x0022
+ * Error: Attribute Not Found (0x0a)
+ * ATT: Find Information Request (0x04)
+ * ATT: Error Response
+ */
+#define IOV_CONTENT(data...) data
+
+#define GMAS_FIND_CHRC(uuid) \
+ IOV_DATA(0x08, 0x01, 0x00, 0x0f, 0x00, 0x03, 0x28), \
+ IOV_DATA(0x09, 0x07, \
+ 0x02, 0x00, 0x02, 0x03, 0x00, 0x00, 0x2c, \
+ 0x04, 0x00, 0x02, 0x05, 0x00, uuid), \
+ IOV_DATA(0x08, 0x05, 0x00, 0x0f, 0x00, 0x03, 0x28), \
+ IOV_DATA(0x01, 0x08, 0x05, 0x00, 0x0a), \
+ IOV_DATA(0x04, 0x06, 0x00, 0x0f, 0x00), \
+ IOV_DATA(0x01, 0x04, 0x06, 0x00, 0x0a)
+
+#define UGG_UUID 0x01, 0x2c
+#define UGT_UUID 0x02, 0x2c
+#define BGS_UUID 0x03, 0x2c
+#define BGR_UUID 0x04, 0x2c
+
+#define ROLE_HND 0x03, 0x00
+#define FEAT_HND 0x05, 0x00
+
+/* ACL Data TX: Handle 42 flags 0x00 dlen 11
+ * ATT: Read By Type Request (0x08) len 6
+ * Handle range: 0x0001-0xffff
+ * Attribute type: Database Hash (0x2b2a)
+ * ATT: Error Response (0x01) len 4
+ * Read By Type Request (0x08)
+ * Handle: 0x0001
+ * Error: Attribute Not Found (0x0a)
+ */
+#define GMAS_DATABASE_HASH \
+ IOV_DATA(0x08, 0x01, 0x00, 0xff, 0xff, 0x2a, 0x2b), \
+ IOV_DATA(0x01, 0x08, 0x01, 0x00, 0x0a)
+
+
+#define GMAS_SETUP(uuid) \
+ GMAS_MTU_FEAT, \
+ GMAS_PRIMARY_SERVICE, \
+ GMAS_SECONDARY_SERVICE, \
+ GMAS_INCLUDE, \
+ GMAS_FIND_CHRC(IOV_CONTENT(uuid)), \
+ GMAS_DATABASE_HASH
+
+/* GATT Discover All procedure */
+static const struct iovec setup_data_ugg[] = { GMAS_SETUP(UGG_UUID) };
+static const struct iovec setup_data_ugt[] = { GMAS_SETUP(UGT_UUID) };
+static const struct iovec setup_data_bgs[] = { GMAS_SETUP(BGS_UUID) };
+static const struct iovec setup_data_bgr[] = { GMAS_SETUP(BGR_UUID) };
+
+static void setup_complete_cb(const void *user_data)
+{
+ tester_setup_complete();
+}
+
+static void test_setup_server(const void *user_data)
+{
+ struct test_data *data = (void *)user_data;
+ const struct test_config *cfg = data->cfg;
+ struct bt_att *att;
+ struct gatt_db *db;
+ struct io *io;
+
+ io = tester_setup_io(cfg->setup_data, cfg->setup_data_len);
+ g_assert(io);
+
+ tester_io_set_complete_func(setup_complete_cb);
+
+ db = gatt_db_new();
+ g_assert(db);
+
+ data->gmap = bt_gmap_add_db(db);
+ bt_gmap_set_debug(data->gmap, print_debug, "gmap:", NULL);
+
+ bt_gmap_set_role(data->gmap, data->cfg->role);
+ bt_gmap_set_features(data->gmap, data->cfg->features);
+
+ att = bt_att_new(io_get_fd(io), false);
+ g_assert(att);
+ bt_att_set_debug(att, BT_ATT_DEBUG, print_debug, "bt_att:", NULL);
+
+ data->server = bt_gatt_server_new(db, att, 64, 0);
+ g_assert(data->server);
+ bt_gatt_server_set_debug(data->server, print_debug, "bt_gatt_server:",
+ NULL);
+
+ tester_io_send();
+
+ bt_att_unref(att);
+ gatt_db_unref(db);
+}
+
+static void test_complete_cb(const void *user_data)
+{
+ tester_test_passed();
+}
+
+static void test_server(const void *user_data)
+{
+ struct test_data *data = (void *)user_data;
+ struct io *io;
+
+ io = tester_setup_io(data->iov, data->iovcnt);
+ g_assert(io);
+
+ tester_io_set_complete_func(test_complete_cb);
+
+ tester_io_send();
+}
+
+static void setup_ready_cb(bool success, uint8_t att_ecode, void *user_data)
+{
+ if (!success)
+ tester_setup_failed();
+ else
+ tester_setup_complete();
+}
+
+static void test_setup(const void *user_data)
+{
+ struct test_data *data = (void *)user_data;
+ const struct test_config *cfg = data->cfg;
+ struct bt_att *att;
+ struct gatt_db *db;
+ struct io *io;
+
+ io = tester_setup_io(cfg->setup_data, cfg->setup_data_len);
+ g_assert(io);
+
+ att = bt_att_new(io_get_fd(io), false);
+ g_assert(att);
+
+ bt_att_set_debug(att, BT_ATT_DEBUG, print_debug, "bt_att:", NULL);
+
+ db = gatt_db_new();
+ g_assert(db);
+
+ data->client = bt_gatt_client_new(db, att, 64, 0);
+ g_assert(data->client);
+
+ bt_gatt_client_set_debug(data->client, print_debug, "bt_gatt_client:",
+ NULL);
+
+ bt_gatt_client_ready_register(data->client, setup_ready_cb, data,
+ NULL);
+
+ bt_att_unref(att);
+ gatt_db_unref(db);
+}
+
+static void client_ready_cb(struct bt_gmap *gmap, void *user_data)
+{
+ struct test_data *data = (void *)user_data;
+
+ if (bt_gmap_get_role(gmap) != data->cfg->role) {
+ tester_test_failed();
+ return;
+ }
+
+ if (bt_gmap_get_features(gmap) != data->cfg->features) {
+ tester_test_failed();
+ return;
+ }
+
+ tester_test_passed();
+}
+
+static void test_client(const void *user_data)
+{
+ struct test_data *data = (void *)user_data;
+ struct io *io;
+
+ io = tester_setup_io(data->iov, data->iovcnt);
+ g_assert(io);
+
+ tester_io_set_complete_func(NULL);
+
+ data->gmap = bt_gmap_attach(data->client, client_ready_cb, data);
+ g_assert(data->gmap);
+
+ bt_gmap_set_debug(data->gmap, print_debug, "gmap:", NULL);
+}
+
+/* ATT: Read Request (0x0a) len 2
+ * Handle: 0x0003 Type: GMAP Role (0x2c00)
+ * ATT: Read Response (0x0b) len 24
+ * Value: _value
+ * Handle: 0x0003 Type: GMAP Role (0x2c00)
+ */
+
+#define READ_CHRC(hnd, value...) \
+ IOV_DATA(0x0a, hnd), \
+ IOV_DATA(0x0b, value)
+
+#define READ_ROLE(value...) READ_CHRC(IOV_CONTENT(ROLE_HND), value)
+#define READ_FEAT(value...) READ_CHRC(IOV_CONTENT(FEAT_HND), value)
+
+#define CGGIT_CHA(role, value) READ_ROLE(role), READ_FEAT(value)
+
+#define CGGIT_ROLE CGGIT_CHA(0x01, 0x00)
+#define CGGIT_ROLE_RFU CGGIT_CHA(0xf1, 0x00)
+
+const struct test_config cfg_read_role = {
+ .role = BT_GMAP_ROLE_UGG,
+ .setup_data = setup_data_ugg,
+ .setup_data_len = ARRAY_SIZE(setup_data_ugg),
+};
+
+#define CGGIT_UGG CGGIT_CHA(0x01, 0x01)
+#define CGGIT_UGG_RFU CGGIT_CHA(0x01, 0xf1)
+
+const struct test_config cfg_read_ugg = {
+ .role = BT_GMAP_ROLE_UGG,
+ .features = BT_GMAP_UGG_MULTIPLEX,
+ .setup_data = setup_data_ugg,
+ .setup_data_len = ARRAY_SIZE(setup_data_ugg),
+};
+
+#define CGGIT_UGT CGGIT_CHA(0x02, 0x01)
+#define CGGIT_UGT_RFU CGGIT_CHA(0x02, 0x81)
+
+const struct test_config cfg_read_ugt = {
+ .role = BT_GMAP_ROLE_UGT,
+ .features = BT_GMAP_UGT_SOURCE,
+ .setup_data = setup_data_ugt,
+ .setup_data_len = ARRAY_SIZE(setup_data_ugt),
+};
+
+#define CGGIT_BGS CGGIT_CHA(0x04, 0x01)
+#define CGGIT_BGS_RFU CGGIT_CHA(0x04, 0x81)
+
+const struct test_config cfg_read_bgs = {
+ .role = BT_GMAP_ROLE_BGS,
+ .features = BT_GMAP_BGS_96KBPS,
+ .setup_data = setup_data_bgs,
+ .setup_data_len = ARRAY_SIZE(setup_data_bgs),
+};
+
+#define CGGIT_BGR CGGIT_CHA(0x08, 0x01)
+#define CGGIT_BGR_RFU CGGIT_CHA(0x08, 0x81)
+
+const struct test_config cfg_read_bgr = {
+ .role = BT_GMAP_ROLE_BGR,
+ .features = BT_GMAP_BGR_MULTISINK,
+ .setup_data = setup_data_bgr,
+ .setup_data_len = ARRAY_SIZE(setup_data_bgr),
+};
+
+static void test_gmap_cl(void)
+{
+ /* Sec. 4.5.1 TMA Client */
+ define_test("GMAP/CL/CGGIT/CHA/BV-01-C [GMAP Role Read Characteristic, "
+ "Client]",
+ test_setup, test_client, &cfg_read_role, CGGIT_ROLE);
+ define_test("GMAP/CL/CGGIT/CHA/BV-03-C [UGG Features Read "
+ "Characteristic, Client]",
+ test_setup, test_client, &cfg_read_ugg, CGGIT_UGG);
+ define_test("GMAP/CL/CGGIT/CHA/BV-02-C [UGT Features Read "
+ "Characteristic, Client]",
+ test_setup, test_client, &cfg_read_ugt, CGGIT_UGT);
+ define_test("GMAP/CL/CGGIT/CHA/BV-04-C [BGS Features Read "
+ "Characteristic, Client]",
+ test_setup, test_client, &cfg_read_bgs, CGGIT_BGS);
+ define_test("GMAP/CL/CGGIT/CHA/BV-05-C [BGR Features Read "
+ "Characteristic, Client]",
+ test_setup, test_client, &cfg_read_bgr, CGGIT_BGR);
+
+ define_test("GMAP/CL/GMAS/BI-01-C [Client Ignores RFU Bits in GMAP "
+ "Role Characteristic]",
+ test_setup, test_client, &cfg_read_role, CGGIT_ROLE_RFU);
+ define_test("GMAP/CL/GMAS/BI-03-C [Client Ignores RFU Bits in UGG "
+ "Features Characteristic]",
+ test_setup, test_client, &cfg_read_ugg, CGGIT_UGG_RFU);
+ define_test("GMAP/CL/GMAS/BI-02-C [Client Ignores RFU Bit in UGT "
+ "Features Characteristic]",
+ test_setup, test_client, &cfg_read_ugt, CGGIT_UGT_RFU);
+ define_test("GMAP/CL/GMAS/BI-04-C [Client Ignores RFU Bits in BGS "
+ "Features Characteristic]",
+ test_setup, test_client, &cfg_read_bgs, CGGIT_BGS_RFU);
+ define_test("GMAP/CL/GMAS/BI-05-C [Client Ignores RFU Bits in BGR "
+ "Features Characteristic]",
+ test_setup, test_client, &cfg_read_bgr, CGGIT_BGR_RFU);
+}
+
+/* Step 1. in CGGIT/CHA skipped, should be unnecessary */
+#define SGGIT_CHA_ROLE READ_ROLE(0x01)
+#define SGGIT_CHA_FEAT READ_FEAT(0x01)
+
+static void test_gmap_sr(void)
+{
+ /* Sec. 4.6.2 GMA Server */
+ define_test("GMAP/SR/SGGIT/CHA/BV-01-C [Characteristic GGIT - GMAP "
+ "Role]",
+ test_setup_server, test_server, &cfg_read_role, SGGIT_CHA_ROLE);
+ define_test("GMAP/SR/SGGIT/CHA/BV-03-C [Characteristic GGIT - UGG "
+ "Features]",
+ test_setup_server, test_server, &cfg_read_ugg, SGGIT_CHA_FEAT);
+ define_test("GMAP/SR/SGGIT/CHA/BV-02-C [Characteristic GGIT - UGT "
+ "Features]",
+ test_setup_server, test_server, &cfg_read_ugt, SGGIT_CHA_FEAT);
+ define_test("GMAP/SR/SGGIT/CHA/BV-04-C [Characteristic GGIT - BGS "
+ "Features]",
+ test_setup_server, test_server, &cfg_read_bgs, SGGIT_CHA_FEAT);
+ define_test("GMAP/SR/SGGIT/CHA/BV-05-C [Characteristic GGIT - BGR "
+ "Features]",
+ test_setup_server, test_server, &cfg_read_bgr, SGGIT_CHA_FEAT);
+}
+
+int main(int argc, char *argv[])
+{
+ tester_init(&argc, &argv);
+ test_gmap_cl();
+ test_gmap_sr();
+
+ return tester_run();
+}
--
2.51.1
^ permalink raw reply related [flat|nested] 16+ messages in thread* [PATCH BlueZ v2 3/9] gmap: Add GMAP profile
2025-11-28 20:02 [PATCH BlueZ v2 0/9] Add TMAP & GMAP information services Pauli Virtanen
2025-11-28 20:02 ` [PATCH BlueZ v2 1/9] shared/gmap: add GMAP Service Pauli Virtanen
2025-11-28 20:02 ` [PATCH BlueZ v2 2/9] test-gmap: add test for GMAP Service Pauli Virtanen
@ 2025-11-28 20:02 ` Pauli Virtanen
2025-11-28 20:02 ` [PATCH BlueZ v2 4/9] doc: org.bluez.MediaEndpoint: add SupportedFeatures Pauli Virtanen
` (7 subsequent siblings)
10 siblings, 0 replies; 16+ messages in thread
From: Pauli Virtanen @ 2025-11-28 20:02 UTC (permalink / raw)
To: linux-bluetooth; +Cc: Pauli Virtanen
Read GMAP properties from remote devices and enable advertising the
values for local services.
---
Notes:
v2:
- remove one unnecessary #include
Makefile.plugins | 5 ++
configure.ac | 7 ++
profiles/audio/gmap.c | 200 ++++++++++++++++++++++++++++++++++++++++++
3 files changed, 212 insertions(+)
create mode 100644 profiles/audio/gmap.c
diff --git a/Makefile.plugins b/Makefile.plugins
index c976c66f4..3572ee845 100644
--- a/Makefile.plugins
+++ b/Makefile.plugins
@@ -151,6 +151,11 @@ builtin_modules += tmap
builtin_sources += profiles/audio/tmap.c
endif
+if GMAP
+builtin_modules += gmap
+builtin_sources += profiles/audio/gmap.c
+endif
+
if ASHA
builtin_modules += asha
builtin_sources += profiles/audio/asha.h profiles/audio/asha.c
diff --git a/configure.ac b/configure.ac
index 29a322cb5..16b81aca3 100644
--- a/configure.ac
+++ b/configure.ac
@@ -234,6 +234,13 @@ if test "${enable_tmap}" != "no"; then
AC_DEFINE(HAVE_TMAP, 1, [Define to 1 if you have TMAP support.])
fi
+AC_ARG_ENABLE(gmap, AS_HELP_STRING([--disable-gmap],
+ [disable GMAP profile]), [enable_gmap=${enableval}])
+AM_CONDITIONAL(GMAP, test "${enable_gmap}" != "no")
+if test "${enable_gmap}" != "no"; then
+ AC_DEFINE(HAVE_GMAP, 1, [Define to 1 if you have GMAP support.])
+fi
+
AC_ARG_ENABLE(asha, AS_HELP_STRING([--disable-asha],
[disable ASHA support]), [enable_asha=${enableval}])
AM_CONDITIONAL(ASHA, test "${enable_asha}" != "no")
diff --git a/profiles/audio/gmap.c b/profiles/audio/gmap.c
new file mode 100644
index 000000000..810bc783c
--- /dev/null
+++ b/profiles/audio/gmap.c
@@ -0,0 +1,200 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ *
+ * BlueZ - Bluetooth protocol stack for Linux
+ *
+ * Copyright (C) 2025 Pauli Virtanen. All rights reserved.
+ *
+ *
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#define _GNU_SOURCE
+
+#include <ctype.h>
+#include <stdbool.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <errno.h>
+
+#include <glib.h>
+
+#include "gdbus/gdbus.h"
+
+#include "bluetooth/bluetooth.h"
+#include "bluetooth/hci.h"
+#include "bluetooth/sdp.h"
+#include "bluetooth/uuid.h"
+
+#include "src/dbus-common.h"
+#include "src/shared/util.h"
+#include "src/shared/att.h"
+#include "src/shared/queue.h"
+#include "src/shared/gatt-db.h"
+#include "src/shared/gatt-client.h"
+#include "src/shared/gatt-server.h"
+#include "src/shared/gmap.h"
+
+#include "btio/btio.h"
+#include "src/plugin.h"
+#include "src/adapter.h"
+#include "src/gatt-database.h"
+#include "src/device.h"
+#include "src/profile.h"
+#include "src/service.h"
+#include "src/log.h"
+#include "src/error.h"
+
+#define GMAS_UUID_STR "00001858-0000-1000-8000-00805f9b34fb"
+
+static void gmap_debug(const char *str, void *user_data)
+{
+ DBG_IDX(0xffff, "%s", str);
+}
+
+static void service_ready(struct bt_gmap *gmap, void *user_data)
+{
+ struct btd_service *service = user_data;
+
+ btd_service_connecting_complete(service, 0);
+}
+
+static struct bt_gmap *add_service(struct btd_service *service)
+{
+ struct btd_device *device = btd_service_get_device(service);
+ struct bt_gatt_client *client = btd_device_get_gatt_client(device);
+ struct bt_gmap *gmap = btd_service_get_user_data(service);
+
+ if (gmap)
+ return gmap;
+
+ gmap = bt_gmap_attach(client, service_ready, service);
+ if (!gmap) {
+ error("GMAP client unable to attach");
+ return NULL;
+ }
+
+ bt_gmap_set_debug(gmap, gmap_debug, NULL, NULL);
+
+ btd_service_set_user_data(service, gmap);
+ return gmap;
+}
+
+static void remove_service(struct btd_service *service)
+{
+ struct bt_gmap *gmap = btd_service_get_user_data(service);
+
+ if (!gmap)
+ return;
+
+ btd_service_set_user_data(service, NULL);
+ bt_gmap_unref(gmap);
+}
+
+static int gmap_accept(struct btd_service *service)
+{
+ struct btd_device *device = btd_service_get_device(service);
+ struct bt_gmap *gmap;
+ char addr[18];
+
+ ba2str(device_get_address(device), addr);
+ DBG("%s", addr);
+
+ gmap = add_service(service);
+ if (!gmap)
+ return -EINVAL;
+
+ return 0;
+}
+
+static int gmap_disconnect(struct btd_service *service)
+{
+ remove_service(service);
+
+ btd_service_disconnecting_complete(service, 0);
+ return 0;
+}
+
+static int gmap_probe(struct btd_service *service)
+{
+ struct btd_device *device = btd_service_get_device(service);
+ char addr[18];
+
+ ba2str(device_get_address(device), addr);
+ DBG("%s", addr);
+ return 0;
+}
+
+static void gmap_remove(struct btd_service *service)
+{
+ struct btd_device *device = btd_service_get_device(service);
+ char addr[18];
+
+ ba2str(device_get_address(device), addr);
+ DBG("%s", addr);
+}
+
+static int gmap_adapter_probe(struct btd_profile *p,
+ struct btd_adapter *adapter)
+{
+ struct btd_gatt_database *database = btd_adapter_get_database(adapter);
+ struct bt_gmap *gmap;
+
+ DBG("Add GMAP server %s", adapter_get_path(adapter));
+ gmap = bt_gmap_add_db(btd_gatt_database_get_db(database));
+
+ bt_gmap_set_debug(gmap, gmap_debug, NULL, NULL);
+ return 0;
+}
+
+static void gmap_adapter_remove(struct btd_profile *p,
+ struct btd_adapter *adapter)
+{
+ struct btd_gatt_database *database = btd_adapter_get_database(adapter);
+ struct bt_gmap *gmap;
+
+ DBG("Remove GMAP server %s", adapter_get_path(adapter));
+ gmap = bt_gmap_find(btd_gatt_database_get_db(database));
+ bt_gmap_unref(gmap);
+}
+
+static struct btd_profile gmap_profile = {
+ .name = "gmap",
+ .priority = BTD_PROFILE_PRIORITY_MEDIUM,
+ .remote_uuid = GMAS_UUID_STR,
+
+ .device_probe = gmap_probe,
+ .device_remove = gmap_remove,
+ .accept = gmap_accept,
+ .disconnect = gmap_disconnect,
+
+ .adapter_probe = gmap_adapter_probe,
+ .adapter_remove = gmap_adapter_remove,
+
+ .experimental = true,
+};
+
+static int gmap_init(void)
+{
+ int err;
+
+ err = btd_profile_register(&gmap_profile);
+ if (err)
+ return err;
+
+ return 0;
+}
+
+static void gmap_exit(void)
+{
+ btd_profile_unregister(&gmap_profile);
+}
+
+BLUETOOTH_PLUGIN_DEFINE(gmap, VERSION, BLUETOOTH_PLUGIN_PRIORITY_DEFAULT,
+ gmap_init, gmap_exit)
--
2.51.1
^ permalink raw reply related [flat|nested] 16+ messages in thread* [PATCH BlueZ v2 4/9] doc: org.bluez.MediaEndpoint: add SupportedFeatures
2025-11-28 20:02 [PATCH BlueZ v2 0/9] Add TMAP & GMAP information services Pauli Virtanen
` (2 preceding siblings ...)
2025-11-28 20:02 ` [PATCH BlueZ v2 3/9] gmap: Add GMAP profile Pauli Virtanen
@ 2025-11-28 20:02 ` Pauli Virtanen
2025-11-28 20:02 ` [PATCH BlueZ v2 5/9] bap: add SupportedFeatures for MediaEndpoints Pauli Virtanen
` (6 subsequent siblings)
10 siblings, 0 replies; 16+ messages in thread
From: Pauli Virtanen @ 2025-11-28 20:02 UTC (permalink / raw)
To: linux-bluetooth; +Cc: Pauli Virtanen
Add SupportedFeatures field for indicating TMAP & GMAP roles and
features.
---
Notes:
v2:
- make it a dict of uuid -> features
doc/org.bluez.MediaEndpoint.rst | 108 ++++++++++++++++++++++++++++++++
1 file changed, 108 insertions(+)
diff --git a/doc/org.bluez.MediaEndpoint.rst b/doc/org.bluez.MediaEndpoint.rst
index c1ce1d562..8202b2fdf 100644
--- a/doc/org.bluez.MediaEndpoint.rst
+++ b/doc/org.bluez.MediaEndpoint.rst
@@ -299,3 +299,111 @@ Indicates QoS capabilities.
:uint32 PreferredMaximumDelay:
Indicates endpoint preferred maximum presentation delay.
+
+dict SupportedFeatures [readonly, ISO only, experimental]
+`````````````````````````````````````````````````````````
+
+Dictionary representing supported features for different
+services. Keys are service UUIDs, values arrays of strings.
+
+Services:
+
+:array{string} 00001855-0000-1000-8000-00805f9b34fb:
+
+ TMAP Features. Possible items:
+
+ :"cg":
+
+ Call Gateway
+
+ :"ct":
+
+ Call Terminal
+
+ :"ums":
+
+ Unicast Media Sender
+
+ :"umr":
+
+ Unicast Media Receiver
+
+ :"bms":
+
+ Broadcast Media Sender
+
+ :"bmr":
+
+ Broadcast Media Receiver
+
+:array{string} 00001858-0000-1000-8000-00805f9b34fb:
+
+ GMAP Features. Possible items:
+
+ :"ugg":
+
+ Unicast Game Gateway
+
+ :"ugt":
+
+ Unicast Game Terminal
+
+ :"bgs":
+
+ Broadcast Game Sender
+
+ :"bgr":
+
+ Broadcast Game Receiver
+
+ :"ugg-multiplex":
+
+ UGG Multiplex feature support
+
+ :"ugg-96kbps-source":
+
+ UGG 96 kbps Source feature support
+
+ :"ugg-multisink":
+
+ UGG Multisink feature support
+
+ :"ugt-source":
+
+ UGT Source feature support
+
+ :"ugt-80kbps-source":
+
+ UGT 80 kbps Source feature support
+
+ :"ugt-sink":
+
+ UGT Sink feature support
+
+ :"ugt-64kbps-sink":
+
+ UGT 64 kbps Sink feature support
+
+ :"ugt-multiplex":
+
+ UGT Multiplex feature support
+
+ :"ugt-multisink":
+
+ UGT Multisink feature support
+
+ :"ugt-multisource":
+
+ UGT Multisource feature support
+
+ :"bgs-96kbps":
+
+ BGS 96 kbps feature support
+
+ :"bgr-multisink":
+
+ BGR Multisink feature support
+
+ :"bgr-multiplex":
+
+ BGR Multiplex feature support
--
2.51.1
^ permalink raw reply related [flat|nested] 16+ messages in thread* [PATCH BlueZ v2 5/9] bap: add SupportedFeatures for MediaEndpoints
2025-11-28 20:02 [PATCH BlueZ v2 0/9] Add TMAP & GMAP information services Pauli Virtanen
` (3 preceding siblings ...)
2025-11-28 20:02 ` [PATCH BlueZ v2 4/9] doc: org.bluez.MediaEndpoint: add SupportedFeatures Pauli Virtanen
@ 2025-11-28 20:02 ` Pauli Virtanen
2025-11-28 20:02 ` [PATCH BlueZ v2 6/9] profile: add after_uuids for ordering profile startup Pauli Virtanen
` (5 subsequent siblings)
10 siblings, 0 replies; 16+ messages in thread
From: Pauli Virtanen @ 2025-11-28 20:02 UTC (permalink / raw)
To: linux-bluetooth; +Cc: Pauli Virtanen
Indicate TMAP & GMAP capabilities in SupportedFeatures.
---
Notes:
v2:
- make it a dict uuid -> features
profiles/audio/bap.c | 122 +++++++++++++++++++++++++++++++++++++++++++
1 file changed, 122 insertions(+)
diff --git a/profiles/audio/bap.c b/profiles/audio/bap.c
index b07d65e68..0dcc57eb5 100644
--- a/profiles/audio/bap.c
+++ b/profiles/audio/bap.c
@@ -46,6 +46,8 @@
#include "src/shared/gatt-client.h"
#include "src/shared/gatt-server.h"
#include "src/shared/bap.h"
+#include "src/shared/tmap.h"
+#include "src/shared/gmap.h"
#include "btio/btio.h"
#include "src/plugin.h"
@@ -65,6 +67,10 @@
#define MEDIA_ENDPOINT_INTERFACE "org.bluez.MediaEndpoint1"
#define MEDIA_INTERFACE "org.bluez.Media1"
+#define VCS_UUID_STR "00001844-0000-1000-8000-00805f9b34fb"
+#define TMAS_UUID_STR "00001855-0000-1000-8000-00805f9b34fb"
+#define GMAS_UUID_STR "00001858-0000-1000-8000-00805f9b34fb"
+
struct bap_setup;
typedef void (*bap_setup_ready_func_t)(struct bap_setup *setup, int code,
@@ -438,6 +444,120 @@ static gboolean get_qos(const GDBusPropertyTable *property,
return TRUE;
}
+static bool probe_tmap_role(struct bap_ep *ep, uint32_t data)
+{
+ struct gatt_db *db = bt_bap_get_db(ep->data->bap, true);
+
+ return bt_tmap_get_role(bt_tmap_find(db)) & data;
+}
+
+static bool probe_gmap_role(struct bap_ep *ep, uint32_t data)
+{
+ struct gatt_db *db = bt_bap_get_db(ep->data->bap, true);
+
+ return bt_gmap_get_role(bt_gmap_find(db)) & data;
+}
+
+static bool probe_gmap_feature(struct bap_ep *ep, uint32_t data)
+{
+ struct gatt_db *db = bt_bap_get_db(ep->data->bap, true);
+
+ return bt_gmap_get_features(bt_gmap_find(db)) & data;
+}
+
+struct feature {
+ const char *name;
+ bool (*probe)(struct bap_ep *ep, uint32_t data);
+ uint32_t data;
+};
+
+static const struct feature tmap_features[] = {
+ { "cg", probe_tmap_role, BT_TMAP_ROLE_CG },
+ { "ct", probe_tmap_role, BT_TMAP_ROLE_CT },
+ { "ums", probe_tmap_role, BT_TMAP_ROLE_UMS },
+ { "umr", probe_tmap_role, BT_TMAP_ROLE_UMR },
+ { "bms", probe_tmap_role, BT_TMAP_ROLE_BMS },
+ { "bmr", probe_tmap_role, BT_TMAP_ROLE_BMR },
+};
+
+static const struct feature gmap_features[] = {
+ { "ugg", probe_gmap_role, BT_GMAP_ROLE_UGG },
+ { "ugt", probe_gmap_role, BT_GMAP_ROLE_UGT },
+ { "bgs", probe_gmap_role, BT_GMAP_ROLE_BGS },
+ { "bgr", probe_gmap_role, BT_GMAP_ROLE_BGR },
+ { "ugg-multiplex", probe_gmap_feature, BT_GMAP_UGG_MULTIPLEX },
+ { "ugg-96kbps-source", probe_gmap_feature, BT_GMAP_UGG_96KBPS },
+ { "ugg-multisink", probe_gmap_feature, BT_GMAP_UGG_MULTISINK },
+ { "ugt-source", probe_gmap_feature, BT_GMAP_UGT_SOURCE },
+ { "ugt-80kbps-source", probe_gmap_feature,
+ BT_GMAP_UGT_80KBPS_SOURCE },
+ { "ugt-sink", probe_gmap_feature, BT_GMAP_UGT_SINK },
+ { "ugt-64kbps-sink", probe_gmap_feature, BT_GMAP_UGT_64KBPS_SINK },
+ { "ugt-multiplex", probe_gmap_feature, BT_GMAP_UGT_MULTIPLEX },
+ { "ugt-multisink", probe_gmap_feature, BT_GMAP_UGT_MULTISINK },
+ { "ugt-multisource", probe_gmap_feature, BT_GMAP_UGT_MULTISOURCE },
+ { "bgs-96kbps", probe_gmap_feature, BT_GMAP_BGS_96KBPS },
+ { "bgr-multisink", probe_gmap_feature, BT_GMAP_BGR_MULTISINK },
+ { "bgr-multiplex", probe_gmap_feature, BT_GMAP_BGR_MULTIPLEX },
+};
+
+static const struct {
+ const char *uuid;
+ const struct feature *items;
+ size_t count;
+} features[] = {
+ { TMAS_UUID_STR, tmap_features, ARRAY_SIZE(tmap_features) },
+ { GMAS_UUID_STR, gmap_features, ARRAY_SIZE(gmap_features) },
+};
+
+static gboolean supported_features(const GDBusPropertyTable *property,
+ DBusMessageIter *iter, void *data)
+{
+ struct bap_ep *ep = data;
+ DBusMessageIter dict, entry, variant, list;
+ size_t i, j;
+
+ dbus_message_iter_open_container(iter, DBUS_TYPE_ARRAY, "{sv}", &dict);
+
+ for (i = 0; i < ARRAY_SIZE(features); ++i) {
+ for (j = 0; j < features[i].count; ++j) {
+ const struct feature *feature = &features[i].items[j];
+
+ if (feature->probe(ep, feature->data))
+ break;
+ }
+ if (j == features[i].count)
+ continue;
+
+ dbus_message_iter_open_container(&dict, DBUS_TYPE_DICT_ENTRY,
+ NULL, &entry);
+ dbus_message_iter_append_basic(&entry, DBUS_TYPE_STRING,
+ &features[i].uuid);
+ dbus_message_iter_open_container(&entry, DBUS_TYPE_VARIANT,
+ "as", &variant);
+ dbus_message_iter_open_container(&variant, DBUS_TYPE_ARRAY,
+ "s", &list);
+
+ for (j = 0; j < features[i].count; ++j) {
+ const struct feature *feature = &features[i].items[j];
+
+ if (!feature->probe(ep, feature->data))
+ continue;
+
+ dbus_message_iter_append_basic(&list, DBUS_TYPE_STRING,
+ &feature->name);
+ }
+
+ dbus_message_iter_close_container(&variant, &list);
+ dbus_message_iter_close_container(&entry, &variant);
+ dbus_message_iter_close_container(&dict, &entry);
+ }
+
+ dbus_message_iter_close_container(iter, &dict);
+
+ return TRUE;
+}
+
static const GDBusPropertyTable ep_properties[] = {
{ "UUID", "s", get_uuid, NULL, NULL,
G_DBUS_PROPERTY_FLAG_EXPERIMENTAL },
@@ -457,6 +577,8 @@ static const GDBusPropertyTable ep_properties[] = {
G_DBUS_PROPERTY_FLAG_EXPERIMENTAL },
{ "QoS", "a{sv}", get_qos, NULL, qos_exists,
G_DBUS_PROPERTY_FLAG_EXPERIMENTAL },
+ { "SupportedFeatures", "a{sv}", supported_features, NULL, NULL,
+ G_DBUS_PROPERTY_FLAG_EXPERIMENTAL },
{ }
};
--
2.51.1
^ permalink raw reply related [flat|nested] 16+ messages in thread* [PATCH BlueZ v2 6/9] profile: add after_uuids for ordering profile startup
2025-11-28 20:02 [PATCH BlueZ v2 0/9] Add TMAP & GMAP information services Pauli Virtanen
` (4 preceding siblings ...)
2025-11-28 20:02 ` [PATCH BlueZ v2 5/9] bap: add SupportedFeatures for MediaEndpoints Pauli Virtanen
@ 2025-11-28 20:02 ` Pauli Virtanen
2025-12-01 17:00 ` Luiz Augusto von Dentz
2025-11-28 20:02 ` [PATCH BlueZ v2 7/9] device: use after_uuids in service autoconnect and sort also GATT Pauli Virtanen
` (4 subsequent siblings)
10 siblings, 1 reply; 16+ messages in thread
From: Pauli Virtanen @ 2025-11-28 20:02 UTC (permalink / raw)
To: linux-bluetooth; +Cc: Pauli Virtanen
Add btd_profile::after_uuids to specify the profile connect/accept order
for autoconnect. This is a "soft" dependency so it doesn't fail if the
other services fail to start nor try to start them if they otherwise
wouldn't.
Add btd_profile_sort_list() for sorting a list according to profile
ordering, taking account priority and after_uuids.
Add btd_profile_find_remote_uuid() lookup utility, needed when using
btd_profile_sort_list() with uuid lists.
---
Notes:
v2:
- new commit
src/profile.c | 89 +++++++++++++++++++++++++++++++++++++++++++++++++++
src/profile.h | 13 ++++++++
2 files changed, 102 insertions(+)
diff --git a/src/profile.c b/src/profile.c
index 66405d7e4..eaf368a4a 100644
--- a/src/profile.c
+++ b/src/profile.c
@@ -773,6 +773,30 @@ static struct btd_profile *btd_profile_find_uuid(const char *uuid)
return NULL;
}
+struct btd_profile *btd_profile_find_remote_uuid(const char *uuid)
+{
+ GSList *l, *next;
+
+ for (l = profiles; l != NULL; l = next) {
+ struct btd_profile *p = l->data;
+
+ if (!g_strcmp0(p->remote_uuid, uuid))
+ return p;
+ next = g_slist_next(l);
+ }
+
+ for (l = ext_profiles; l != NULL; l = next) {
+ struct ext_profile *ext = l->data;
+ struct btd_profile *p = &ext->p;
+
+ if (!g_strcmp0(p->remote_uuid, uuid))
+ return p;
+ next = g_slist_next(l);
+ }
+
+ return NULL;
+}
+
int btd_profile_register(struct btd_profile *profile)
{
if (profile->experimental && !(g_dbus_get_flags() &
@@ -2650,3 +2674,68 @@ void btd_profile_cleanup(void)
g_dbus_unregister_interface(btd_get_dbus_connection(),
"/org/bluez", "org.bluez.ProfileManager1");
}
+
+/* Stable sort of a list according to profile priority & after_uuids */
+GSList *btd_profile_sort_list(GSList *list, btd_profile_list_get get,
+ void *user_data)
+{
+ GSList *result = NULL;
+ GSList *remain = list;
+ GHashTable *uuids;
+ GSList *entry;
+ const struct btd_profile *p;
+
+ uuids = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
+
+ /* Unsatisfied remote uuids */
+ for (entry = remain; entry; entry = g_slist_next(entry)) {
+ p = get(entry->data, user_data);
+ if (p->remote_uuid)
+ g_hash_table_add(uuids, g_strdup(p->remote_uuid));
+ }
+
+ /* Sort */
+ while (remain) {
+ GSList *first_entry = remain;
+ int max_priority = INT_MIN;
+
+ /* Find max priority */
+ for (entry = remain; entry; entry = g_slist_next(entry)) {
+ p = get(entry->data, user_data);
+ if (p->priority > max_priority) {
+ first_entry = entry;
+ max_priority = p->priority;
+ }
+ }
+
+ /* Find max priority entry with no unsatisfied dependencies */
+ for (entry = remain; entry; entry = g_slist_next(entry)) {
+ const char **uuid;
+
+ p = get(entry->data, user_data);
+ if (p->priority < max_priority)
+ continue;
+
+ for (uuid = p->after_uuids; uuid && *uuid; uuid++)
+ if (g_hash_table_contains(uuids, *uuid))
+ break;
+ if (!(uuid && *uuid))
+ break;
+ }
+
+ /* If cyclic dependencies: break preserving priority & order */
+ if (!entry)
+ entry = first_entry;
+
+ p = get(entry->data, user_data);
+ if (p->remote_uuid)
+ g_hash_table_remove(uuids, p->remote_uuid);
+
+ remain = g_slist_remove_link(remain, entry);
+ result = g_slist_concat(result, entry);
+ }
+
+ g_hash_table_destroy(uuids);
+
+ return result;
+}
diff --git a/src/profile.h b/src/profile.h
index 424ce55ad..a8c2443e3 100644
--- a/src/profile.h
+++ b/src/profile.h
@@ -38,6 +38,12 @@ struct btd_profile {
*/
bool testing;
+ /* Indicates the profile should be ordered after profiles providing
+ * these remote uuids when connecting. A NULL-terminated array of
+ * strings.
+ */
+ const char **after_uuids;
+
int (*device_probe) (struct btd_service *service);
void (*device_remove) (struct btd_service *service);
@@ -76,3 +82,10 @@ bool btd_profile_remove_custom_prop(const char *uuid, const char *name);
void btd_profile_init(void);
void btd_profile_cleanup(void);
+
+struct btd_profile *btd_profile_find_remote_uuid(const char *uuid);
+
+typedef const struct btd_profile *(*btd_profile_list_get)(void *item,
+ void *user_data);
+GSList *btd_profile_sort_list(GSList *list, btd_profile_list_get get,
+ void *user_data);
--
2.51.1
^ permalink raw reply related [flat|nested] 16+ messages in thread* Re: [PATCH BlueZ v2 6/9] profile: add after_uuids for ordering profile startup
2025-11-28 20:02 ` [PATCH BlueZ v2 6/9] profile: add after_uuids for ordering profile startup Pauli Virtanen
@ 2025-12-01 17:00 ` Luiz Augusto von Dentz
2025-12-01 17:32 ` Pauli Virtanen
0 siblings, 1 reply; 16+ messages in thread
From: Luiz Augusto von Dentz @ 2025-12-01 17:00 UTC (permalink / raw)
To: Pauli Virtanen; +Cc: linux-bluetooth
Hi Pauli,
On Fri, Nov 28, 2025 at 3:03 PM Pauli Virtanen <pav@iki.fi> wrote:
>
> Add btd_profile::after_uuids to specify the profile connect/accept order
> for autoconnect. This is a "soft" dependency so it doesn't fail if the
> other services fail to start nor try to start them if they otherwise
> wouldn't.
>
> Add btd_profile_sort_list() for sorting a list according to profile
> ordering, taking account priority and after_uuids.
>
> Add btd_profile_find_remote_uuid() lookup utility, needed when using
> btd_profile_sort_list() with uuid lists.
> ---
>
> Notes:
> v2:
> - new commit
>
> src/profile.c | 89 +++++++++++++++++++++++++++++++++++++++++++++++++++
> src/profile.h | 13 ++++++++
> 2 files changed, 102 insertions(+)
>
> diff --git a/src/profile.c b/src/profile.c
> index 66405d7e4..eaf368a4a 100644
> --- a/src/profile.c
> +++ b/src/profile.c
> @@ -773,6 +773,30 @@ static struct btd_profile *btd_profile_find_uuid(const char *uuid)
> return NULL;
> }
>
> +struct btd_profile *btd_profile_find_remote_uuid(const char *uuid)
> +{
> + GSList *l, *next;
> +
> + for (l = profiles; l != NULL; l = next) {
> + struct btd_profile *p = l->data;
> +
> + if (!g_strcmp0(p->remote_uuid, uuid))
> + return p;
> + next = g_slist_next(l);
> + }
> +
> + for (l = ext_profiles; l != NULL; l = next) {
> + struct ext_profile *ext = l->data;
> + struct btd_profile *p = &ext->p;
> +
> + if (!g_strcmp0(p->remote_uuid, uuid))
> + return p;
> + next = g_slist_next(l);
> + }
> +
> + return NULL;
> +}
> +
> int btd_profile_register(struct btd_profile *profile)
> {
> if (profile->experimental && !(g_dbus_get_flags() &
> @@ -2650,3 +2674,68 @@ void btd_profile_cleanup(void)
> g_dbus_unregister_interface(btd_get_dbus_connection(),
> "/org/bluez", "org.bluez.ProfileManager1");
> }
> +
> +/* Stable sort of a list according to profile priority & after_uuids */
> +GSList *btd_profile_sort_list(GSList *list, btd_profile_list_get get,
> + void *user_data)
> +{
> + GSList *result = NULL;
> + GSList *remain = list;
> + GHashTable *uuids;
> + GSList *entry;
> + const struct btd_profile *p;
> +
> + uuids = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
> +
> + /* Unsatisfied remote uuids */
> + for (entry = remain; entry; entry = g_slist_next(entry)) {
> + p = get(entry->data, user_data);
> + if (p->remote_uuid)
> + g_hash_table_add(uuids, g_strdup(p->remote_uuid));
> + }
> +
> + /* Sort */
> + while (remain) {
> + GSList *first_entry = remain;
> + int max_priority = INT_MIN;
> +
> + /* Find max priority */
> + for (entry = remain; entry; entry = g_slist_next(entry)) {
> + p = get(entry->data, user_data);
> + if (p->priority > max_priority) {
> + first_entry = entry;
> + max_priority = p->priority;
> + }
> + }
> +
> + /* Find max priority entry with no unsatisfied dependencies */
> + for (entry = remain; entry; entry = g_slist_next(entry)) {
> + const char **uuid;
> +
> + p = get(entry->data, user_data);
> + if (p->priority < max_priority)
> + continue;
> +
> + for (uuid = p->after_uuids; uuid && *uuid; uuid++)
> + if (g_hash_table_contains(uuids, *uuid))
> + break;
> + if (!(uuid && *uuid))
> + break;
> + }
> +
> + /* If cyclic dependencies: break preserving priority & order */
> + if (!entry)
> + entry = first_entry;
> +
> + p = get(entry->data, user_data);
> + if (p->remote_uuid)
> + g_hash_table_remove(uuids, p->remote_uuid);
> +
> + remain = g_slist_remove_link(remain, entry);
> + result = g_slist_concat(result, entry);
> + }
> +
> + g_hash_table_destroy(uuids);
> +
> + return result;
> +}
We might need to build some unit testing for this sort of function,
for checking if we handle cyclic/circular dependencies, if we don't
end up with duplicated entries in the list as well, not sure how much
testing you have done in this respect since we only really have one
plugin using after_uuids.
> diff --git a/src/profile.h b/src/profile.h
> index 424ce55ad..a8c2443e3 100644
> --- a/src/profile.h
> +++ b/src/profile.h
> @@ -38,6 +38,12 @@ struct btd_profile {
> */
> bool testing;
>
> + /* Indicates the profile should be ordered after profiles providing
> + * these remote uuids when connecting. A NULL-terminated array of
> + * strings.
> + */
> + const char **after_uuids;
Hmm, I wonder if this shouldn't be a struct with uuids list and
function to be called when connected? Perhaps having a macro that
initializes the list would be a good idea as well.
> +
> int (*device_probe) (struct btd_service *service);
> void (*device_remove) (struct btd_service *service);
>
> @@ -76,3 +82,10 @@ bool btd_profile_remove_custom_prop(const char *uuid, const char *name);
>
> void btd_profile_init(void);
> void btd_profile_cleanup(void);
> +
> +struct btd_profile *btd_profile_find_remote_uuid(const char *uuid);
> +
> +typedef const struct btd_profile *(*btd_profile_list_get)(void *item,
> + void *user_data);
> +GSList *btd_profile_sort_list(GSList *list, btd_profile_list_get get,
> + void *user_data);
Id use struct queue as return if possible.
> --
> 2.51.1
>
>
--
Luiz Augusto von Dentz
^ permalink raw reply [flat|nested] 16+ messages in thread* Re: [PATCH BlueZ v2 6/9] profile: add after_uuids for ordering profile startup
2025-12-01 17:00 ` Luiz Augusto von Dentz
@ 2025-12-01 17:32 ` Pauli Virtanen
2025-12-01 17:57 ` Luiz Augusto von Dentz
0 siblings, 1 reply; 16+ messages in thread
From: Pauli Virtanen @ 2025-12-01 17:32 UTC (permalink / raw)
To: Luiz Augusto von Dentz; +Cc: linux-bluetooth
ma, 2025-12-01 kello 12:00 -0500, Luiz Augusto von Dentz kirjoitti:
> Hi Pauli,
>
> On Fri, Nov 28, 2025 at 3:03 PM Pauli Virtanen <pav@iki.fi> wrote:
> >
> > Add btd_profile::after_uuids to specify the profile connect/accept order
> > for autoconnect. This is a "soft" dependency so it doesn't fail if the
> > other services fail to start nor try to start them if they otherwise
> > wouldn't.
> >
> > Add btd_profile_sort_list() for sorting a list according to profile
> > ordering, taking account priority and after_uuids.
> >
> > Add btd_profile_find_remote_uuid() lookup utility, needed when using
> > btd_profile_sort_list() with uuid lists.
> > ---
> >
> > Notes:
> > v2:
> > - new commit
> >
> > src/profile.c | 89 +++++++++++++++++++++++++++++++++++++++++++++++++++
> > src/profile.h | 13 ++++++++
> > 2 files changed, 102 insertions(+)
> >
> > diff --git a/src/profile.c b/src/profile.c
> > index 66405d7e4..eaf368a4a 100644
> > --- a/src/profile.c
> > +++ b/src/profile.c
> > @@ -773,6 +773,30 @@ static struct btd_profile *btd_profile_find_uuid(const char *uuid)
> > return NULL;
> > }
> >
> > +struct btd_profile *btd_profile_find_remote_uuid(const char *uuid)
> > +{
> > + GSList *l, *next;
> > +
> > + for (l = profiles; l != NULL; l = next) {
> > + struct btd_profile *p = l->data;
> > +
> > + if (!g_strcmp0(p->remote_uuid, uuid))
> > + return p;
> > + next = g_slist_next(l);
> > + }
> > +
> > + for (l = ext_profiles; l != NULL; l = next) {
> > + struct ext_profile *ext = l->data;
> > + struct btd_profile *p = &ext->p;
> > +
> > + if (!g_strcmp0(p->remote_uuid, uuid))
> > + return p;
> > + next = g_slist_next(l);
> > + }
> > +
> > + return NULL;
> > +}
> > +
> > int btd_profile_register(struct btd_profile *profile)
> > {
> > if (profile->experimental && !(g_dbus_get_flags() &
> > @@ -2650,3 +2674,68 @@ void btd_profile_cleanup(void)
> > g_dbus_unregister_interface(btd_get_dbus_connection(),
> > "/org/bluez", "org.bluez.ProfileManager1");
> > }
> > +
> > +/* Stable sort of a list according to profile priority & after_uuids */
> > +GSList *btd_profile_sort_list(GSList *list, btd_profile_list_get get,
> > + void *user_data)
> > +{
> > + GSList *result = NULL;
> > + GSList *remain = list;
> > + GHashTable *uuids;
> > + GSList *entry;
> > + const struct btd_profile *p;
> > +
> > + uuids = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
> > +
> > + /* Unsatisfied remote uuids */
> > + for (entry = remain; entry; entry = g_slist_next(entry)) {
> > + p = get(entry->data, user_data);
> > + if (p->remote_uuid)
> > + g_hash_table_add(uuids, g_strdup(p->remote_uuid));
> > + }
> > +
> > + /* Sort */
> > + while (remain) {
> > + GSList *first_entry = remain;
> > + int max_priority = INT_MIN;
> > +
> > + /* Find max priority */
> > + for (entry = remain; entry; entry = g_slist_next(entry)) {
> > + p = get(entry->data, user_data);
> > + if (p->priority > max_priority) {
> > + first_entry = entry;
> > + max_priority = p->priority;
> > + }
> > + }
> > +
> > + /* Find max priority entry with no unsatisfied dependencies */
> > + for (entry = remain; entry; entry = g_slist_next(entry)) {
> > + const char **uuid;
> > +
> > + p = get(entry->data, user_data);
> > + if (p->priority < max_priority)
> > + continue;
> > +
> > + for (uuid = p->after_uuids; uuid && *uuid; uuid++)
> > + if (g_hash_table_contains(uuids, *uuid))
> > + break;
> > + if (!(uuid && *uuid))
> > + break;
> > + }
> > +
> > + /* If cyclic dependencies: break preserving priority & order */
> > + if (!entry)
> > + entry = first_entry;
> > +
> > + p = get(entry->data, user_data);
> > + if (p->remote_uuid)
> > + g_hash_table_remove(uuids, p->remote_uuid);
> > +
> > + remain = g_slist_remove_link(remain, entry);
> > + result = g_slist_concat(result, entry);
> > + }
> > +
> > + g_hash_table_destroy(uuids);
> > +
> > + return result;
> > +}
>
> We might need to build some unit testing for this sort of function,
> for checking if we handle cyclic/circular dependencies, if we don't
> end up with duplicated entries in the list as well, not sure how much
> testing you have done in this respect since we only really have one
> plugin using after_uuids.
I think the function should be correct, but yes unit tests would be
good to have here.
> > diff --git a/src/profile.h b/src/profile.h
> > index 424ce55ad..a8c2443e3 100644
> > --- a/src/profile.h
> > +++ b/src/profile.h
> > @@ -38,6 +38,12 @@ struct btd_profile {
> > */
> > bool testing;
> >
> > + /* Indicates the profile should be ordered after profiles providing
> > + * these remote uuids when connecting. A NULL-terminated array of
> > + * strings.
> > + */
> > + const char **after_uuids;
>
> Hmm, I wonder if this shouldn't be a struct with uuids list and
> function to be called when connected? Perhaps having a macro that
> initializes the list would be a good idea as well.
So
struct btd_profile_uuid_cb {
void (*func)(struct btd_service *service);
unsigned int count;
const char *remote_uuids[];
};
struct btd_profile {
...
const struct btd_profile_uuid_cb *after;
};
or maybe you meant something different like separate callbacks for each
UUID?
>
> > +
> > int (*device_probe) (struct btd_service *service);
> > void (*device_remove) (struct btd_service *service);
> >
> > @@ -76,3 +82,10 @@ bool btd_profile_remove_custom_prop(const char *uuid, const char *name);
> >
> > void btd_profile_init(void);
> > void btd_profile_cleanup(void);
> > +
> > +struct btd_profile *btd_profile_find_remote_uuid(const char *uuid);
> > +
> > +typedef const struct btd_profile *(*btd_profile_list_get)(void *item,
> > + void *user_data);
> > +GSList *btd_profile_sort_list(GSList *list, btd_profile_list_get get,
> > + void *user_data);
>
> Id use struct queue as return if possible.
device.c used GSList for the pending list, but it could be queue in
principle
>
> > --
> > 2.51.1
> >
> >
>
--
Pauli Virtanen
^ permalink raw reply [flat|nested] 16+ messages in thread* Re: [PATCH BlueZ v2 6/9] profile: add after_uuids for ordering profile startup
2025-12-01 17:32 ` Pauli Virtanen
@ 2025-12-01 17:57 ` Luiz Augusto von Dentz
0 siblings, 0 replies; 16+ messages in thread
From: Luiz Augusto von Dentz @ 2025-12-01 17:57 UTC (permalink / raw)
To: Pauli Virtanen; +Cc: linux-bluetooth
Hi Pauli,
On Mon, Dec 1, 2025 at 12:33 PM Pauli Virtanen <pav@iki.fi> wrote:
>
> ma, 2025-12-01 kello 12:00 -0500, Luiz Augusto von Dentz kirjoitti:
> > Hi Pauli,
> >
> > On Fri, Nov 28, 2025 at 3:03 PM Pauli Virtanen <pav@iki.fi> wrote:
> > >
> > > Add btd_profile::after_uuids to specify the profile connect/accept order
> > > for autoconnect. This is a "soft" dependency so it doesn't fail if the
> > > other services fail to start nor try to start them if they otherwise
> > > wouldn't.
> > >
> > > Add btd_profile_sort_list() for sorting a list according to profile
> > > ordering, taking account priority and after_uuids.
> > >
> > > Add btd_profile_find_remote_uuid() lookup utility, needed when using
> > > btd_profile_sort_list() with uuid lists.
> > > ---
> > >
> > > Notes:
> > > v2:
> > > - new commit
> > >
> > > src/profile.c | 89 +++++++++++++++++++++++++++++++++++++++++++++++++++
> > > src/profile.h | 13 ++++++++
> > > 2 files changed, 102 insertions(+)
> > >
> > > diff --git a/src/profile.c b/src/profile.c
> > > index 66405d7e4..eaf368a4a 100644
> > > --- a/src/profile.c
> > > +++ b/src/profile.c
> > > @@ -773,6 +773,30 @@ static struct btd_profile *btd_profile_find_uuid(const char *uuid)
> > > return NULL;
> > > }
> > >
> > > +struct btd_profile *btd_profile_find_remote_uuid(const char *uuid)
> > > +{
> > > + GSList *l, *next;
> > > +
> > > + for (l = profiles; l != NULL; l = next) {
> > > + struct btd_profile *p = l->data;
> > > +
> > > + if (!g_strcmp0(p->remote_uuid, uuid))
> > > + return p;
> > > + next = g_slist_next(l);
> > > + }
> > > +
> > > + for (l = ext_profiles; l != NULL; l = next) {
> > > + struct ext_profile *ext = l->data;
> > > + struct btd_profile *p = &ext->p;
> > > +
> > > + if (!g_strcmp0(p->remote_uuid, uuid))
> > > + return p;
> > > + next = g_slist_next(l);
> > > + }
> > > +
> > > + return NULL;
> > > +}
> > > +
> > > int btd_profile_register(struct btd_profile *profile)
> > > {
> > > if (profile->experimental && !(g_dbus_get_flags() &
> > > @@ -2650,3 +2674,68 @@ void btd_profile_cleanup(void)
> > > g_dbus_unregister_interface(btd_get_dbus_connection(),
> > > "/org/bluez", "org.bluez.ProfileManager1");
> > > }
> > > +
> > > +/* Stable sort of a list according to profile priority & after_uuids */
> > > +GSList *btd_profile_sort_list(GSList *list, btd_profile_list_get get,
> > > + void *user_data)
> > > +{
> > > + GSList *result = NULL;
> > > + GSList *remain = list;
> > > + GHashTable *uuids;
> > > + GSList *entry;
> > > + const struct btd_profile *p;
> > > +
> > > + uuids = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
> > > +
> > > + /* Unsatisfied remote uuids */
> > > + for (entry = remain; entry; entry = g_slist_next(entry)) {
> > > + p = get(entry->data, user_data);
> > > + if (p->remote_uuid)
> > > + g_hash_table_add(uuids, g_strdup(p->remote_uuid));
> > > + }
> > > +
> > > + /* Sort */
> > > + while (remain) {
> > > + GSList *first_entry = remain;
> > > + int max_priority = INT_MIN;
> > > +
> > > + /* Find max priority */
> > > + for (entry = remain; entry; entry = g_slist_next(entry)) {
> > > + p = get(entry->data, user_data);
> > > + if (p->priority > max_priority) {
> > > + first_entry = entry;
> > > + max_priority = p->priority;
> > > + }
> > > + }
> > > +
> > > + /* Find max priority entry with no unsatisfied dependencies */
> > > + for (entry = remain; entry; entry = g_slist_next(entry)) {
> > > + const char **uuid;
> > > +
> > > + p = get(entry->data, user_data);
> > > + if (p->priority < max_priority)
> > > + continue;
> > > +
> > > + for (uuid = p->after_uuids; uuid && *uuid; uuid++)
> > > + if (g_hash_table_contains(uuids, *uuid))
> > > + break;
> > > + if (!(uuid && *uuid))
> > > + break;
> > > + }
> > > +
> > > + /* If cyclic dependencies: break preserving priority & order */
> > > + if (!entry)
> > > + entry = first_entry;
> > > +
> > > + p = get(entry->data, user_data);
> > > + if (p->remote_uuid)
> > > + g_hash_table_remove(uuids, p->remote_uuid);
> > > +
> > > + remain = g_slist_remove_link(remain, entry);
> > > + result = g_slist_concat(result, entry);
> > > + }
> > > +
> > > + g_hash_table_destroy(uuids);
> > > +
> > > + return result;
> > > +}
> >
> > We might need to build some unit testing for this sort of function,
> > for checking if we handle cyclic/circular dependencies, if we don't
> > end up with duplicated entries in the list as well, not sure how much
> > testing you have done in this respect since we only really have one
> > plugin using after_uuids.
>
> I think the function should be correct, but yes unit tests would be
> good to have here.
I don't think it is wrong, it seems correct at first glance, the
problem is once it is integrated we can run into corner cases, so
having some way to introduce tests for corner cases is in my opinion
required.
>
> > > diff --git a/src/profile.h b/src/profile.h
> > > index 424ce55ad..a8c2443e3 100644
> > > --- a/src/profile.h
> > > +++ b/src/profile.h
> > > @@ -38,6 +38,12 @@ struct btd_profile {
> > > */
> > > bool testing;
> > >
> > > + /* Indicates the profile should be ordered after profiles providing
> > > + * these remote uuids when connecting. A NULL-terminated array of
> > > + * strings.
> > > + */
> > > + const char **after_uuids;
> >
> > Hmm, I wonder if this shouldn't be a struct with uuids list and
> > function to be called when connected? Perhaps having a macro that
> > initializes the list would be a good idea as well.
>
> So
>
> struct btd_profile_uuid_cb {
> void (*func)(struct btd_service *service);
> unsigned int count;
> const char *remote_uuids[];
> };
>
> struct btd_profile {
> ...
>
> const struct btd_profile_uuid_cb *after;
> };
>
> or maybe you meant something different like separate callbacks for each
> UUID?
Yeah, that seems alright, I guess we could have one per uuid as well
in case the profile really need to do something for each profile it
connects, but we need one for when everything has been connected
anyway so Id leave just one callback for now until there is a use case
where it needs callbacks to be run on a per uuid basis.
> >
> > > +
> > > int (*device_probe) (struct btd_service *service);
> > > void (*device_remove) (struct btd_service *service);
> > >
> > > @@ -76,3 +82,10 @@ bool btd_profile_remove_custom_prop(const char *uuid, const char *name);
> > >
> > > void btd_profile_init(void);
> > > void btd_profile_cleanup(void);
> > > +
> > > +struct btd_profile *btd_profile_find_remote_uuid(const char *uuid);
> > > +
> > > +typedef const struct btd_profile *(*btd_profile_list_get)(void *item,
> > > + void *user_data);
> > > +GSList *btd_profile_sort_list(GSList *list, btd_profile_list_get get,
> > > + void *user_data);
> >
> > Id use struct queue as return if possible.
>
> device.c used GSList for the pending list, but it could be queue in
> principle
Ah ok, well then I leave it to you if you feel like it doesn't take
too much effort to convert the device.c to also use a queue that would
be great, otherwise I'm fine with GSList as well.
> >
> > > --
> > > 2.51.1
> > >
> > >
> >
>
> --
> Pauli Virtanen
--
Luiz Augusto von Dentz
^ permalink raw reply [flat|nested] 16+ messages in thread
* [PATCH BlueZ v2 7/9] device: use after_uuids in service autoconnect and sort also GATT
2025-11-28 20:02 [PATCH BlueZ v2 0/9] Add TMAP & GMAP information services Pauli Virtanen
` (5 preceding siblings ...)
2025-11-28 20:02 ` [PATCH BlueZ v2 6/9] profile: add after_uuids for ordering profile startup Pauli Virtanen
@ 2025-11-28 20:02 ` Pauli Virtanen
2025-11-28 20:02 ` [PATCH BlueZ v2 8/9] service: add btd_profile::ready callback when after_uuids ready Pauli Virtanen
` (3 subsequent siblings)
10 siblings, 0 replies; 16+ messages in thread
From: Pauli Virtanen @ 2025-11-28 20:02 UTC (permalink / raw)
To: linux-bluetooth; +Cc: Pauli Virtanen
Use btd_profile_sort_list() for selecting the order in which services
are connected: first by priority, then by after_uuids.
Probe and accept also GATT services in profile order. Previously this
was done in the order they were in GATT db.
---
Notes:
v2:
- new commit
src/device.c | 74 +++++++++++++++++++++++++++++++++++++++++++---------
1 file changed, 62 insertions(+), 12 deletions(-)
diff --git a/src/device.c b/src/device.c
index eb184633a..3ecbfd67c 100644
--- a/src/device.c
+++ b/src/device.c
@@ -2523,14 +2523,6 @@ static struct btd_service *find_connectable_service(struct btd_device *dev,
return NULL;
}
-static int service_prio_cmp(gconstpointer a, gconstpointer b)
-{
- struct btd_profile *p1 = btd_service_get_profile(a);
- struct btd_profile *p2 = btd_service_get_profile(b);
-
- return p2->priority - p1->priority;
-}
-
bool btd_device_all_services_allowed(struct btd_device *dev)
{
GSList *l;
@@ -2581,6 +2573,12 @@ void btd_device_update_allowed_services(struct btd_device *dev)
}
}
+static const struct btd_profile *get_service_profile(void *data,
+ void *user_data)
+{
+ return btd_service_get_profile(data);
+}
+
static GSList *create_pending_list(struct btd_device *dev, const char *uuid)
{
struct btd_service *service;
@@ -2619,10 +2617,13 @@ static GSList *create_pending_list(struct btd_device *dev, const char *uuid)
BTD_SERVICE_STATE_DISCONNECTED)
continue;
- dev->pending = g_slist_insert_sorted(dev->pending, service,
- service_prio_cmp);
+ dev->pending = g_slist_append(dev->pending, service);
}
+ /* Connect in priority order */
+ dev->pending = btd_profile_sort_list(dev->pending, get_service_profile,
+ NULL);
+
return dev->pending;
}
@@ -4630,9 +4631,42 @@ done:
service_accept(service, btd_device_is_initiator(device));
}
+
+static const struct btd_profile *get_gatt_profile(void *data, void *user_data)
+{
+ struct gatt_db_attribute *attr = data;
+ struct btd_profile *profile;
+ bt_uuid_t uuid;
+ struct btd_profile *dummy_profile = user_data;
+ char *uuid_str = (char *)dummy_profile->remote_uuid;
+
+ gatt_db_attribute_get_service_uuid(attr, &uuid);
+ bt_uuid_to_string(&uuid, uuid_str, MAX_LEN_UUID_STR);
+
+ profile = btd_profile_find_remote_uuid(uuid_str);
+ if (!profile)
+ profile = dummy_profile;
+
+ return profile;
+}
+
+static void get_gatt_attrs(struct gatt_db_attribute *attr,
+ void *user_data)
+{
+ GSList **list = user_data;
+
+ *list = g_slist_append(*list, attr);
+}
+
static void device_add_gatt_services(struct btd_device *device)
{
char addr[18];
+ GSList *attrs = NULL;
+ GSList *entry;
+ char uuid_str[MAX_LEN_UUID_STR];
+ struct btd_profile dummy_profile = {
+ .remote_uuid = uuid_str,
+ };
ba2str(&device->bdaddr, addr);
@@ -4641,18 +4675,34 @@ static void device_add_gatt_services(struct btd_device *device)
return;
}
- gatt_db_foreach_service(device->db, NULL, add_gatt_service, device);
+ /* Probe and accept in profile priority order */
+ gatt_db_foreach_service(device->db, NULL, get_gatt_attrs, &attrs);
+
+ attrs = btd_profile_sort_list(attrs, get_gatt_profile,
+ &dummy_profile);
+
+ for (entry = attrs; entry; entry = g_slist_next(entry))
+ add_gatt_service(entry->data, device);
+
+ g_slist_free(attrs);
}
static void device_accept_gatt_profiles(struct btd_device *device)
{
GSList *l;
bool initiator = btd_device_is_initiator(device);
+ GSList *services;
DBG("initiator %s", initiator ? "true" : "false");
- for (l = device->services; l != NULL; l = g_slist_next(l))
+ /* Accept in profile priority order */
+ services = g_slist_copy(device->services);
+ services = btd_profile_sort_list(services, get_service_profile, NULL);
+
+ for (l = services; l != NULL; l = g_slist_next(l))
service_accept(l->data, initiator);
+
+ g_slist_free(services);
}
static void device_remove_gatt_service(struct btd_device *device,
--
2.51.1
^ permalink raw reply related [flat|nested] 16+ messages in thread* [PATCH BlueZ v2 8/9] service: add btd_profile::ready callback when after_uuids ready
2025-11-28 20:02 [PATCH BlueZ v2 0/9] Add TMAP & GMAP information services Pauli Virtanen
` (6 preceding siblings ...)
2025-11-28 20:02 ` [PATCH BlueZ v2 7/9] device: use after_uuids in service autoconnect and sort also GATT Pauli Virtanen
@ 2025-11-28 20:02 ` Pauli Virtanen
2025-11-28 20:02 ` [PATCH BlueZ v2 9/9] bap: have unicast client wait for VCS & TMAS & GMAP Pauli Virtanen
` (2 subsequent siblings)
10 siblings, 0 replies; 16+ messages in thread
From: Pauli Virtanen @ 2025-11-28 20:02 UTC (permalink / raw)
To: linux-bluetooth; +Cc: Pauli Virtanen
Add btd_profile::ready that is called when after_uuids dependencies have
finished connecting.
---
Notes:
v2:
- new commit
src/profile.h | 3 ++
src/service.c | 100 ++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 103 insertions(+)
diff --git a/src/profile.h b/src/profile.h
index a8c2443e3..28504331e 100644
--- a/src/profile.h
+++ b/src/profile.h
@@ -52,6 +52,9 @@ struct btd_profile {
int (*accept) (struct btd_service *service);
+ /* Emitted when services in after_uuids are no longer connecting */
+ void (*ready)(struct btd_service *service);
+
int (*adapter_probe) (struct btd_profile *p,
struct btd_adapter *adapter);
void (*adapter_remove) (struct btd_profile *p,
diff --git a/src/service.c b/src/service.c
index 7690a1261..95950aa50 100644
--- a/src/service.c
+++ b/src/service.c
@@ -26,6 +26,8 @@
#include "bluetooth/bluetooth.h"
#include "bluetooth/sdp.h"
+#include "src/shared/queue.h"
+
#include "log.h"
#include "backtrace.h"
@@ -43,6 +45,8 @@ struct btd_service {
int err;
bool is_allowed;
bool initiator;
+ struct queue *depends;
+ struct queue *dependents;
};
struct service_state_callback {
@@ -71,6 +75,46 @@ static const char *state2str(btd_service_state_t state)
return NULL;
}
+static void depends_ready(void *item, void *user_data)
+{
+ struct btd_service *service = item;
+ struct btd_service *dep = user_data;
+ char addr[18];
+
+ if (dep && !queue_remove(service->depends, dep))
+ return;
+ if (!service->depends || !queue_isempty(service->depends))
+ return;
+
+ queue_destroy(service->depends, NULL);
+ service->depends = NULL;
+
+ ba2str(device_get_address(service->device), addr);
+ DBG("%p: device %s profile %s dependencies ready", service,
+ addr, service->profile->name);
+
+ switch (service->state) {
+ case BTD_SERVICE_STATE_CONNECTING:
+ case BTD_SERVICE_STATE_CONNECTED:
+ if (service->profile->ready)
+ service->profile->ready(service);
+ break;
+ case BTD_SERVICE_STATE_UNAVAILABLE:
+ case BTD_SERVICE_STATE_DISCONNECTING:
+ case BTD_SERVICE_STATE_DISCONNECTED:
+ break;
+ }
+}
+
+static void service_ready(struct btd_service *service)
+{
+ queue_foreach(service->dependents, depends_ready, service);
+ queue_destroy(service->dependents, NULL);
+ service->dependents = NULL;
+
+ depends_ready(service, NULL);
+}
+
static void change_state(struct btd_service *service, btd_service_state_t state,
int err)
{
@@ -98,6 +142,9 @@ static void change_state(struct btd_service *service, btd_service_state_t state,
cb->cb(service, old, state, cb->user_data);
}
+ if (state != BTD_SERVICE_STATE_CONNECTING)
+ service_ready(service);
+
if (state == BTD_SERVICE_STATE_DISCONNECTED)
service->initiator = false;
}
@@ -111,6 +158,20 @@ struct btd_service *btd_service_ref(struct btd_service *service)
return service;
}
+static void depends_remove(void *item, void *user_data)
+{
+ struct btd_service *service = item;
+
+ queue_remove(service->dependents, user_data);
+}
+
+static void dependents_remove(void *item, void *user_data)
+{
+ struct btd_service *service = item;
+
+ queue_remove(service->depends, user_data);
+}
+
void btd_service_unref(struct btd_service *service)
{
service->ref--;
@@ -120,6 +181,11 @@ void btd_service_unref(struct btd_service *service)
if (service->ref > 0)
return;
+ queue_foreach(service->depends, depends_remove, service);
+ queue_foreach(service->dependents, dependents_remove, service);
+ queue_destroy(service->depends, NULL);
+ queue_destroy(service->dependents, NULL);
+
g_free(service);
}
@@ -172,6 +238,37 @@ void service_remove(struct btd_service *service)
btd_service_unref(service);
}
+static void add_depends(struct btd_service *service)
+{
+ const char **uuid;
+
+ queue_foreach(service->depends, depends_remove, service);
+ queue_destroy(service->depends, NULL);
+ service->depends = queue_new();
+
+ for (uuid = service->profile->after_uuids; uuid && *uuid; uuid++) {
+ struct btd_service *dep;
+
+ dep = btd_device_get_service(service->device, *uuid);
+ if (!dep)
+ continue;
+
+ /* Profiles are sorted vs after_uuids, so the dependency will
+ * have started connecting before us if it is going to connect.
+ */
+ if (dep->state != BTD_SERVICE_STATE_CONNECTING)
+ continue;
+ if (queue_find(service->depends, NULL, dep))
+ continue;
+
+ queue_push_tail(service->depends, dep);
+
+ if (!dep->dependents)
+ dep->dependents = queue_new();
+ queue_push_tail(dep->dependents, service);
+ }
+}
+
int service_accept(struct btd_service *service, bool initiator)
{
char addr[18];
@@ -199,6 +296,7 @@ int service_accept(struct btd_service *service, bool initiator)
}
service->initiator = initiator;
+ add_depends(service);
err = service->profile->accept(service);
if (!err)
@@ -265,6 +363,8 @@ int btd_service_connect(struct btd_service *service)
return -ECONNABORTED;
}
+ add_depends(service);
+
err = profile->connect(service);
if (err == 0) {
service->initiator = true;
--
2.51.1
^ permalink raw reply related [flat|nested] 16+ messages in thread* [PATCH BlueZ v2 9/9] bap: have unicast client wait for VCS & TMAS & GMAP
2025-11-28 20:02 [PATCH BlueZ v2 0/9] Add TMAP & GMAP information services Pauli Virtanen
` (7 preceding siblings ...)
2025-11-28 20:02 ` [PATCH BlueZ v2 8/9] service: add btd_profile::ready callback when after_uuids ready Pauli Virtanen
@ 2025-11-28 20:02 ` Pauli Virtanen
2025-12-01 14:43 ` [PATCH BlueZ v2 0/9] Add TMAP & GMAP information services Luiz Augusto von Dentz
2025-12-01 16:50 ` patchwork-bot+bluetooth
10 siblings, 0 replies; 16+ messages in thread
From: Pauli Virtanen @ 2025-11-28 20:02 UTC (permalink / raw)
To: linux-bluetooth; +Cc: Pauli Virtanen
Have unicast client to wait for VCS, TMAS, and GMAP before creating
endpoints and transports, so that their information is available at that
point.
---
Notes:
v2:
- use the generic mechanism to do this
profiles/audio/bap.c | 41 ++++++++++++++++++++++++++++++++++++++---
1 file changed, 38 insertions(+), 3 deletions(-)
diff --git a/profiles/audio/bap.c b/profiles/audio/bap.c
index 0dcc57eb5..8e6a7c57f 100644
--- a/profiles/audio/bap.c
+++ b/profiles/audio/bap.c
@@ -145,6 +145,8 @@ struct bap_data {
GIOChannel *listen_io;
unsigned int io_id;
unsigned int cig_update_id;
+ bool services_ready;
+ bool bap_ready;
};
static struct queue *sessions;
@@ -2137,10 +2139,10 @@ static bool pac_found_bcast(struct bt_bap_pac *lpac, struct bt_bap_pac *rpac,
return true;
}
-static void bap_ready(struct bt_bap *bap, void *user_data)
+static void bap_ucast_start(struct bap_data *data)
{
- struct btd_service *service = user_data;
- struct bap_data *data = btd_service_get_user_data(service);
+ struct btd_service *service = data->service;
+ struct bt_bap *bap = data->bap;
DBG("bap %p", bap);
@@ -3723,6 +3725,29 @@ static void pa_and_big_sync(struct bap_setup *setup)
}
}
+static void bap_ready(struct bt_bap *bap, void *user_data)
+{
+ struct btd_service *service = user_data;
+ struct bap_data *data = btd_service_get_user_data(service);
+
+ DBG("bap %p", bap);
+
+ data->bap_ready = true;
+ if (data->services_ready)
+ bap_ucast_start(data);
+}
+
+static void bap_services_ready(struct btd_service *service)
+{
+ struct bap_data *data = btd_service_get_user_data(service);
+
+ DBG("%p", data);
+
+ data->services_ready = true;
+ if (data->bap_ready)
+ bap_ucast_start(data);
+}
+
static int bap_bcast_probe(struct btd_service *service)
{
struct btd_device *device = btd_service_get_device(service);
@@ -3877,6 +3902,9 @@ static int bap_accept(struct btd_service *service)
return -EINVAL;
}
+ data->bap_ready = false;
+ data->services_ready = false;
+
if (!bt_bap_attach(data->bap, client)) {
error("BAP unable to attach");
return -EINVAL;
@@ -3996,10 +4024,17 @@ static struct btd_profile bap_profile = {
.device_remove = bap_remove,
.accept = bap_accept,
.disconnect = bap_disconnect,
+ .ready = bap_services_ready,
.adapter_probe = bap_adapter_probe,
.adapter_remove = bap_adapter_remove,
.auto_connect = true,
.experimental = true,
+ .after_uuids = (const char *[]) {
+ VCS_UUID_STR,
+ TMAS_UUID_STR,
+ GMAS_UUID_STR,
+ NULL
+ },
};
static struct btd_profile bap_bcast_profile = {
--
2.51.1
^ permalink raw reply related [flat|nested] 16+ messages in thread* Re: [PATCH BlueZ v2 0/9] Add TMAP & GMAP information services
2025-11-28 20:02 [PATCH BlueZ v2 0/9] Add TMAP & GMAP information services Pauli Virtanen
` (8 preceding siblings ...)
2025-11-28 20:02 ` [PATCH BlueZ v2 9/9] bap: have unicast client wait for VCS & TMAS & GMAP Pauli Virtanen
@ 2025-12-01 14:43 ` Luiz Augusto von Dentz
2025-12-01 16:50 ` patchwork-bot+bluetooth
10 siblings, 0 replies; 16+ messages in thread
From: Luiz Augusto von Dentz @ 2025-12-01 14:43 UTC (permalink / raw)
To: Pauli Virtanen; +Cc: linux-bluetooth
Hi Pauli,
On Fri, Nov 28, 2025 at 3:02 PM Pauli Virtanen <pav@iki.fi> wrote:
>
> Add support for TMAP and GMAP services. They contain only device
> audio capability bitmasks.
>
> v2:
> - Rework the service wait to be general mechanism that also determines
> the service autoconnect order.
>
> This is now slightly more involved, but this sort of "soft" ordering
> dependency must know which services the autoconnect mechanism is going
> to start.
>
> Sorting autoconnect services is one of the ways to do it, probably
> makes sense also otherwise, and we could insert service conflict
> handling at the same place.
>
> - Make org.bluez.MediaEndpoint::SupportedFeatures per-uuid dict
>
> ---
>
> Expose the values from remote devices in
> org.bluez.MediaEndpoint->SupportedFeatures
>
> This maybe could also be org.bluez.Device->SupportedFeatures instead,
> but MediaEndpoint looks OK too.
This is definitely too specialized to be in the Device interface, so I
think it being in MediaEndpoint is actually the proper place.
> Sound server can use theses to determine which mandatory capabilities
> devices have.
>
> TODO (maybe later): add way to configure advertised local service
> values, e.g. via config file.
For the local SupportedFeatures Id expect the application registering
the MediaEndpoint to populate what they support, even though these
features are considered device wide, not per PACS, production system
will probably stick with a single entity registering endpoints (e.g.
pipewire), so it seems a lot simpler to have them adds its features
via MediaEndpoint.SupportedFeatures thus making the API symmetric with
remote.
> Pauli Virtanen (9):
> shared/gmap: add GMAP Service
> test-gmap: add test for GMAP Service
> gmap: Add GMAP profile
> doc: org.bluez.MediaEndpoint: add SupportedFeatures
> bap: add SupportedFeatures for MediaEndpoints
> profile: add after_uuids for ordering profile startup
> device: use after_uuids in service autoconnect and sort also GATT
> service: add btd_profile::ready callback when after_uuids ready
> bap: have unicast client wait for VCS & TMAS & GMAP
>
> .gitignore | 1 +
> Makefile.am | 7 +
> Makefile.plugins | 5 +
> configure.ac | 7 +
> doc/org.bluez.MediaEndpoint.rst | 108 +++++++
> lib/bluetooth/uuid.h | 8 +
> profiles/audio/bap.c | 163 ++++++++++-
> profiles/audio/gmap.c | 200 +++++++++++++
> src/device.c | 74 ++++-
> src/profile.c | 89 ++++++
> src/profile.h | 16 ++
> src/service.c | 100 +++++++
> src/shared/gmap.c | 401 ++++++++++++++++++++++++++
> src/shared/gmap.h | 70 +++++
> unit/test-gmap.c | 496 ++++++++++++++++++++++++++++++++
> 15 files changed, 1730 insertions(+), 15 deletions(-)
> create mode 100644 profiles/audio/gmap.c
> create mode 100644 src/shared/gmap.c
> create mode 100644 src/shared/gmap.h
> create mode 100644 unit/test-gmap.c
>
> --
> 2.51.1
>
>
--
Luiz Augusto von Dentz
^ permalink raw reply [flat|nested] 16+ messages in thread* Re: [PATCH BlueZ v2 0/9] Add TMAP & GMAP information services
2025-11-28 20:02 [PATCH BlueZ v2 0/9] Add TMAP & GMAP information services Pauli Virtanen
` (9 preceding siblings ...)
2025-12-01 14:43 ` [PATCH BlueZ v2 0/9] Add TMAP & GMAP information services Luiz Augusto von Dentz
@ 2025-12-01 16:50 ` patchwork-bot+bluetooth
10 siblings, 0 replies; 16+ messages in thread
From: patchwork-bot+bluetooth @ 2025-12-01 16:50 UTC (permalink / raw)
To: Pauli Virtanen; +Cc: linux-bluetooth
Hello:
This series was applied to bluetooth/bluez.git (master)
by Luiz Augusto von Dentz <luiz.von.dentz@intel.com>:
On Fri, 28 Nov 2025 22:02:19 +0200 you wrote:
> Add support for TMAP and GMAP services. They contain only device
> audio capability bitmasks.
>
> v2:
> - Rework the service wait to be general mechanism that also determines
> the service autoconnect order.
>
> [...]
Here is the summary with links:
- [BlueZ,v2,1/9] shared/gmap: add GMAP Service
https://git.kernel.org/pub/scm/bluetooth/bluez.git/?id=f11433431e92
- [BlueZ,v2,2/9] test-gmap: add test for GMAP Service
https://git.kernel.org/pub/scm/bluetooth/bluez.git/?id=5312066e67c8
- [BlueZ,v2,3/9] gmap: Add GMAP profile
https://git.kernel.org/pub/scm/bluetooth/bluez.git/?id=c6b1aed97b6a
- [BlueZ,v2,4/9] doc: org.bluez.MediaEndpoint: add SupportedFeatures
https://git.kernel.org/pub/scm/bluetooth/bluez.git/?id=d4332653009c
- [BlueZ,v2,5/9] bap: add SupportedFeatures for MediaEndpoints
https://git.kernel.org/pub/scm/bluetooth/bluez.git/?id=71a3aa554f4f
- [BlueZ,v2,6/9] profile: add after_uuids for ordering profile startup
(no matching commit)
- [BlueZ,v2,7/9] device: use after_uuids in service autoconnect and sort also GATT
(no matching commit)
- [BlueZ,v2,8/9] service: add btd_profile::ready callback when after_uuids ready
(no matching commit)
- [BlueZ,v2,9/9] bap: have unicast client wait for VCS & TMAS & GMAP
(no matching commit)
You are awesome, thank you!
--
Deet-doot-dot, I am a bot.
https://korg.docs.kernel.org/patchwork/pwbot.html
^ permalink raw reply [flat|nested] 16+ messages in thread