* [PATCH BlueZ 01/12] audio/hfp-hf: Add HFP HF server and SDP record
2026-09-25 13:25 [PATCH BlueZ 00/12] hfp-hf: Enhance HFP Hands-Free profile support Frédéric Danis
@ 2026-09-25 13:25 ` Frédéric Danis
2026-09-25 15:37 ` hfp-hf: Enhance HFP Hands-Free profile support bluez.test.bot
2026-09-25 13:25 ` [PATCH BlueZ 02/12] audio/hfp-hf: Add MediaEndpoint for HFP codecs Frédéric Danis
` (10 subsequent siblings)
11 siblings, 1 reply; 14+ messages in thread
From: Frédéric Danis @ 2026-09-25 13:25 UTC (permalink / raw)
To: linux-bluetooth
---
profiles/audio/hfp-hf.c | 253 ++++++++++++++++++++++++++++++++++++++++
1 file changed, 253 insertions(+)
diff --git a/profiles/audio/hfp-hf.c b/profiles/audio/hfp-hf.c
index 8de2d7a62..102a8006f 100644
--- a/profiles/audio/hfp-hf.c
+++ b/profiles/audio/hfp-hf.c
@@ -46,6 +46,23 @@
#include "telephony.h"
+#define HFP_HF_VERSION 0x0109
+#define HFP_HF_DEFAULT_CHANNEL 7
+
+#define HFP_HF_SDP_ECNR 0x0001
+#define HFP_HF_SDP_3WAY 0x0002
+#define HFP_HF_SDP_CLIP 0x0004
+#define HFP_HF_SDP_VOICE_RECOGNITION 0x0008
+#define HFP_HF_SDP_REMOTE_VOLUME_CONTROL 0x0010
+#define HFP_HF_SDP_WIDE_BAND_SPEECH 0x0020
+#define HFP_HF_SDP_ENHANCED_VOICE_RECOGNITION_STATUS 0x0040
+#define HFP_HF_SDP_VOICE_RECOGNITION_TEXT 0x0080
+#define HFP_HF_SDP_SUPER_WIDE_BAND_SPEECH 0x0100
+
+#define HFP_HF_SDP_FEATURES (HFP_HF_SDP_ECNR | HFP_HF_SDP_3WAY |\
+ HFP_HF_SDP_CLIP |\
+ HFP_HF_SDP_REMOTE_VOLUME_CONTROL)
+
#define URI "tel"
#define URI_PREFIX URI ":"
@@ -57,11 +74,31 @@ struct hfp_device {
struct queue *calls;
};
+struct hfp_server {
+ struct btd_adapter *adapter;
+ GIOChannel *io;
+ uint32_t record_id;
+};
+
+static GSList *servers;
+
static void hfp_hf_debug(const char *str, void *user_data)
{
DBG_IDX(0xffff, "%s", str);
}
+static struct hfp_server *find_server(GSList *list, struct btd_adapter *a)
+{
+ for (; list; list = list->next) {
+ struct hfp_server *server = list->data;
+
+ if (server->adapter == a)
+ return server;
+ }
+
+ return NULL;
+}
+
static enum call_state hfp_call_status_to_call_state(
enum hfp_call_status status)
{
@@ -490,6 +527,219 @@ static void hfp_remove(struct btd_service *service)
g_free(dev);
}
+static sdp_record_t *hfp_record(void)
+{
+ sdp_record_t *record;
+ uuid_t root_uuid, hfphf, genericaudio, l2cap, rfcomm;
+ sdp_list_t *root, *svclass_id, *aproto, *proto[2], *apseq, *pfseq;
+ sdp_data_t *channel, *features;
+ uint8_t hf_channel = HFP_HF_DEFAULT_CHANNEL;
+ sdp_profile_desc_t profile;
+ uint16_t feat = HFP_HF_SDP_FEATURES;
+
+ record = sdp_record_alloc();
+ if (!record) {
+ error("Unable to allocate new service record");
+ return NULL;
+ }
+
+ sdp_uuid16_create(&root_uuid, PUBLIC_BROWSE_GROUP);
+ root = sdp_list_append(NULL, &root_uuid);
+ sdp_set_browse_groups(record, root);
+
+ /* Service Class ID List */
+ sdp_uuid16_create(&hfphf, HANDSFREE_SVCLASS_ID);
+ svclass_id = sdp_list_append(NULL, &hfphf);
+ sdp_uuid16_create(&genericaudio, GENERIC_AUDIO_SVCLASS_ID);
+ svclass_id = sdp_list_append(svclass_id, &genericaudio);
+ sdp_set_service_classes(record, svclass_id);
+
+ /* Protocol Descriptor List */
+ sdp_uuid16_create(&l2cap, L2CAP_UUID);
+ proto[0] = sdp_list_append(NULL, &l2cap);
+ apseq = sdp_list_append(NULL, proto[0]);
+
+ sdp_uuid16_create(&rfcomm, RFCOMM_UUID);
+ proto[1] = sdp_list_append(NULL, &rfcomm);
+ channel = sdp_data_alloc(SDP_UINT8, &hf_channel);
+ proto[1] = sdp_list_append(proto[1], channel);
+ apseq = sdp_list_append(apseq, proto[1]);
+
+ aproto = sdp_list_append(NULL, apseq);
+ sdp_set_access_protos(record, aproto);
+
+ /* Bluetooth Profile Descriptor List */
+ sdp_uuid16_create(&profile.uuid, HANDSFREE_PROFILE_ID);
+ profile.version = HFP_HF_VERSION;
+ pfseq = sdp_list_append(NULL, &profile);
+ sdp_set_profile_descs(record, pfseq);
+
+ sdp_set_info_attr(record, "Hands-Free unit", NULL, NULL);
+
+ features = sdp_data_alloc(SDP_UINT16, &feat);
+ sdp_attr_add(record, SDP_ATTR_SUPPORTED_FEATURES, features);
+
+ free(channel);
+ sdp_list_free(proto[0], NULL);
+ sdp_list_free(proto[1], NULL);
+ sdp_list_free(pfseq, NULL);
+ sdp_list_free(aproto, NULL);
+ sdp_list_free(apseq, NULL);
+ sdp_list_free(svclass_id, NULL);
+ sdp_list_free(root, NULL);
+
+ return record;
+}
+
+static void server_connect_cb(GIOChannel *chan, GError *err, gpointer data)
+{
+ uint8_t channel;
+ bdaddr_t src, dst;
+ char address[18];
+ GError *gerr = NULL;
+ struct btd_device *device;
+ struct btd_service *service;
+ struct hfp_device *dev;
+ const sdp_record_t *rec;
+ sdp_list_t *list;
+ sdp_profile_desc_t *desc;
+
+ if (err) {
+ error("%s", err->message);
+ return;
+ }
+
+ bt_io_get(chan, &gerr,
+ BT_IO_OPT_SOURCE_BDADDR, &src,
+ BT_IO_OPT_DEST_BDADDR, &dst,
+ BT_IO_OPT_CHANNEL, &channel,
+ BT_IO_OPT_INVALID);
+ if (gerr) {
+ error("%s", gerr->message);
+ g_error_free(gerr);
+ g_io_channel_shutdown(chan, TRUE, NULL);
+ return;
+ }
+
+ ba2str(&dst, address);
+ DBG("Incoming connection from %s on Channel %d", address, channel);
+
+ device = btd_adapter_find_device(adapter_find(&src), &dst,
+ BDADDR_BREDR);
+ if (!device)
+ return;
+
+ service = btd_device_get_service(device, HFP_AG_UUID);
+ if (!service)
+ return;
+
+ dev = btd_service_get_user_data(service);
+
+ rec = btd_device_get_record(telephony_get_device(dev->telephony),
+ HFP_AG_UUID);
+ if (!rec)
+ return;
+
+ if (sdp_get_profile_descs(rec, &list) == 0) {
+ desc = list->data;
+ dev->version = desc->version;
+ }
+ sdp_list_free(list, free);
+
+ telephony_register_interface(dev->telephony);
+
+ connect_cb(chan, err, dev);
+}
+
+static GIOChannel *server_socket(struct btd_adapter *adapter)
+{
+ GIOChannel *io;
+ GError *err = NULL;
+
+ io = bt_io_listen(server_connect_cb, NULL, NULL, NULL, &err,
+ BT_IO_OPT_SOURCE_BDADDR,
+ btd_adapter_get_address(adapter),
+ BT_IO_OPT_CHANNEL, HFP_HF_DEFAULT_CHANNEL,
+ BT_IO_OPT_SEC_LEVEL, BT_IO_SEC_MEDIUM,
+ BT_IO_OPT_INVALID);
+ if (!io) {
+ error("%s", err->message);
+ g_error_free(err);
+ }
+
+ return io;
+}
+
+static int hfp_adapter_probe(struct btd_profile *p,
+ struct btd_adapter *adapter)
+{
+ struct hfp_server *server;
+ sdp_record_t *record;
+
+ DBG("path %s", adapter_get_path(adapter));
+
+ server = find_server(servers, adapter);
+ if (server != NULL)
+ goto done;
+
+ server = g_new0(struct hfp_server, 1);
+
+ server->io = server_socket(adapter);
+ if (!server->io) {
+ g_free(server);
+ return -1;
+ }
+
+done:
+ record = hfp_record();
+ if (!record) {
+ error("Unable to allocate new service record");
+ g_free(server);
+ return -1;
+ }
+
+ if (adapter_service_add(adapter, record) < 0) {
+ error("Unable to register HFP HF service record");
+ sdp_record_free(record);
+ g_free(server);
+ return -1;
+ }
+ server->record_id = record->handle;
+
+ server->adapter = btd_adapter_ref(adapter);
+
+ servers = g_slist_append(servers, server);
+
+ return 0;
+}
+
+static void hfp_adapter_remove(struct btd_profile *p,
+ struct btd_adapter *adapter)
+{
+ struct hfp_server *server;
+
+ DBG("path %s", adapter_get_path(adapter));
+
+ server = find_server(servers, adapter);
+ if (!server)
+ return;
+
+ if (server->io) {
+ g_io_channel_shutdown(server->io, TRUE, NULL);
+ g_io_channel_unref(server->io);
+ }
+
+ if (server->record_id != 0) {
+ adapter_service_remove(adapter, server->record_id);
+ server->record_id = 0;
+ }
+
+ servers = g_slist_remove(servers, server);
+
+ btd_adapter_unref(server->adapter);
+ g_free(server);
+}
+
static struct btd_profile hfp_hf_profile = {
.name = "hfp",
.priority = BTD_PROFILE_PRIORITY_MEDIUM,
@@ -502,6 +752,9 @@ static struct btd_profile hfp_hf_profile = {
.connect = hfp_connect,
.disconnect = hfp_disconnect,
+ .adapter_probe = hfp_adapter_probe,
+ .adapter_remove = hfp_adapter_remove,
+
.experimental = true,
};
--
2.43.0
^ permalink raw reply related [flat|nested] 14+ messages in thread* [PATCH BlueZ 02/12] audio/hfp-hf: Add MediaEndpoint for HFP codecs
2026-09-25 13:25 [PATCH BlueZ 00/12] hfp-hf: Enhance HFP Hands-Free profile support Frédéric Danis
2026-09-25 13:25 ` [PATCH BlueZ 01/12] audio/hfp-hf: Add HFP HF server and SDP record Frédéric Danis
@ 2026-09-25 13:25 ` Frédéric Danis
2026-09-25 13:25 ` [PATCH BlueZ 03/12] client/player: Add MediaEndpoints for HFP HF codecs Frédéric Danis
` (9 subsequent siblings)
11 siblings, 0 replies; 14+ messages in thread
From: Frédéric Danis @ 2026-09-25 13:25 UTC (permalink / raw)
To: linux-bluetooth
Register a MediaEndpoint1 (org.bluez.MediaEndpoint1) for each HFP
codec supported by the remote AG, exposing SCO audio through the
standard media transport API.
Adds a SCO listening/connecting socket in hfp-hf, endpoint
registration/unregistration tied to the HFP connection lifecycle,
and wires MediaTransport creation/configuration for the HFP_AG_UUID
endpoint in media.c.
Assisted-by: Claude:claude-sonnet-5
---
profiles/audio/hfp-hf.c | 415 +++++++++++++++++++++++++++++++++++++
profiles/audio/hfp-hf.h | 26 +++
profiles/audio/media.c | 76 +++++++
profiles/audio/media.h | 6 +
profiles/audio/transport.c | 158 ++++++++++++++
5 files changed, 681 insertions(+)
create mode 100644 profiles/audio/hfp-hf.h
diff --git a/profiles/audio/hfp-hf.c b/profiles/audio/hfp-hf.c
index 102a8006f..dddd56261 100644
--- a/profiles/audio/hfp-hf.c
+++ b/profiles/audio/hfp-hf.c
@@ -44,8 +44,12 @@
#include "src/shared/hfp.h"
#include "src/shared/queue.h"
+#include "hfp-hf.h"
+#include "media.h"
#include "telephony.h"
+#define MEDIA_ENDPOINT_INTERFACE "org.bluez.MediaEndpoint1"
+
#define HFP_HF_VERSION 0x0109
#define HFP_HF_DEFAULT_CHANNEL 7
@@ -70,14 +74,23 @@ struct hfp_device {
struct telephony *telephony;
uint16_t version;
GIOChannel *io;
+ GIOChannel *sco_io;
+ uint16_t imtu;
+ uint16_t omtu;
struct hfp_hf *hf;
struct queue *calls;
+ uint8_t codec;
+ unsigned int resume_id;
+ hfp_hf_sco_closed_cb sco_closed_cb;
+ void *sco_closed_data;
};
struct hfp_server {
struct btd_adapter *adapter;
GIOChannel *io;
+ GIOChannel *sco_io;
uint32_t record_id;
+ GSList *endpoints;
};
static GSList *servers;
@@ -87,6 +100,22 @@ static void hfp_hf_debug(const char *str, void *user_data)
DBG_IDX(0xffff, "%s", str);
}
+static char *make_endpoint_path(struct telephony *telephony, uint8_t codec)
+{
+ char *path;
+ int err;
+
+ err = asprintf(&path, "%s/sep%u", telephony_get_path(telephony),
+ codec);
+ if (err < 0) {
+ error("Could not allocate path for remote %s",
+ device_get_path(telephony_get_device(telephony)));
+ return NULL;
+ }
+
+ return path;
+}
+
static struct hfp_server *find_server(GSList *list, struct btd_adapter *a)
{
for (; list; list = list->next) {
@@ -99,6 +128,20 @@ static struct hfp_server *find_server(GSList *list, struct btd_adapter *a)
return NULL;
}
+static void unregister_endpoint(gpointer data, gpointer user_data)
+{
+ struct media_endpoint *ep = data;
+ struct telephony *telephony = user_data;
+ char *path;
+
+ path = make_endpoint_path(telephony, media_endpoint_get_codec(ep));
+ if (path) {
+ g_dbus_unregister_interface(btd_get_dbus_connection(),
+ path, MEDIA_ENDPOINT_INTERFACE);
+ free(path);
+ }
+}
+
static enum call_state hfp_call_status_to_call_state(
enum hfp_call_status status)
{
@@ -128,6 +171,8 @@ static bool call_id_cmp(const void *data, const void *match_data)
static void device_destroy(struct hfp_device *dev)
{
+ struct hfp_server *server;
+
DBG("%s", telephony_get_path(dev->telephony));
telephony_set_state(dev->telephony, DISCONNECTING);
@@ -137,11 +182,22 @@ static void device_destroy(struct hfp_device *dev)
dev->hf = NULL;
}
+ if (dev->sco_io) {
+ g_io_channel_unref(dev->sco_io);
+ dev->sco_io = NULL;
+ }
+
if (dev->io) {
g_io_channel_unref(dev->io);
dev->io = NULL;
}
+ server = find_server(servers,
+ device_get_adapter(telephony_get_device(dev->telephony)));
+ if (server)
+ g_slist_foreach(server->endpoints, unregister_endpoint,
+ dev->telephony);
+
telephony_unregister_interface(dev->telephony);
}
@@ -296,10 +352,68 @@ static void hfp_disconnect_watch(void *user_data)
device_destroy(user_data);
}
+static gboolean get_uuid(const GDBusPropertyTable *property,
+ DBusMessageIter *iter, void *data)
+{
+ const char *uuid;
+
+ uuid = HFP_HS_UUID;
+
+ dbus_message_iter_append_basic(iter, DBUS_TYPE_STRING, &uuid);
+
+ return TRUE;
+}
+
+static gboolean get_device(const GDBusPropertyTable *property,
+ DBusMessageIter *iter, void *data)
+{
+ struct hfp_device *dev = data;
+ const char *path;
+
+ path = device_get_path(telephony_get_device(dev->telephony));
+
+ dbus_message_iter_append_basic(iter, DBUS_TYPE_OBJECT_PATH, &path);
+
+ return TRUE;
+}
+
+static const GDBusMethodTable hfp_hf_ep_methods[] = {
+ { },
+};
+
+static const GDBusPropertyTable hfp_hf_ep_properties[] = {
+ { "UUID", "s", get_uuid, NULL, NULL,
+ G_DBUS_PROPERTY_FLAG_EXPERIMENTAL },
+ { "Device", "o", get_device, NULL, NULL,
+ G_DBUS_PROPERTY_FLAG_EXPERIMENTAL },
+ { }
+};
+
+static void register_endpoint(gpointer data, gpointer user_data)
+{
+ struct media_endpoint *ep = data;
+ struct hfp_device *dev = user_data;
+ char *path = NULL;
+
+ path = make_endpoint_path(dev->telephony,
+ media_endpoint_get_codec(ep));
+ if (path) {
+ if (g_dbus_register_interface(btd_get_dbus_connection(),
+ path, MEDIA_ENDPOINT_INTERFACE,
+ hfp_hf_ep_methods, NULL,
+ hfp_hf_ep_properties,
+ dev, NULL) == FALSE) {
+ error("Could not register remote ep %s", path);
+ }
+ free(path);
+ }
+}
+
static void connect_cb(GIOChannel *chan, GError *err, gpointer user_data)
{
struct hfp_device *dev = user_data;
struct btd_service *service = telephony_get_service(dev->telephony);
+ struct hfp_server *server;
DBG("");
@@ -333,6 +447,10 @@ static void connect_cb(GIOChannel *chan, GError *err, gpointer user_data)
telephony_set_state(dev->telephony, SESSION_CONNECTING);
btd_service_connecting_complete(service, 0);
+ server = find_server(servers,
+ device_get_adapter(telephony_get_device(dev->telephony)));
+ g_slist_foreach(server->endpoints, register_endpoint, dev);
+
return;
failed:
@@ -507,6 +625,8 @@ static int hfp_probe(struct btd_service *service)
return -EINVAL;
dev->telephony = telephony_new(service, dev, &hfp_callbacks);
+ /* Use CVSD codec by default */
+ dev->codec = 1;
btd_service_set_user_data(service, dev);
telephony_add_uri_scheme(dev->telephony, URI);
@@ -670,6 +790,296 @@ static GIOChannel *server_socket(struct btd_adapter *adapter)
return io;
}
+static gboolean sco_io_cb(GIOChannel *chan, GIOCondition cond, void *data)
+{
+ struct hfp_device *dev = data;
+
+ if (cond & G_IO_NVAL)
+ return FALSE;
+
+ DBG("sco connection released");
+ g_io_channel_shutdown(dev->sco_io, TRUE, NULL);
+ g_io_channel_unref(dev->sco_io);
+ dev->sco_io = NULL;
+
+ /* The remote end hung up the SCO connection instead of us
+ * releasing it locally, e.g. via Suspend/Release on the media
+ * transport. Notify the transport so it can update its state
+ * and release/suspend accordingly.
+ */
+ if (dev->sco_closed_cb)
+ dev->sco_closed_cb(dev, dev->sco_closed_data);
+
+ return FALSE;
+}
+
+struct connect_data {
+ struct hfp_device *dev;
+ void (*cb)(int status, void *data);
+ void *cb_user_data;
+};
+
+static void sco_connect_complete(struct connect_data *connect_data, int status)
+{
+ if (!connect_data || !connect_data->cb)
+ goto done;
+
+ connect_data->cb(status, connect_data->cb_user_data);
+
+done:
+ g_free(connect_data);
+}
+
+static void sco_connect_cb(GIOChannel *io, GError *err, gpointer user_data)
+{
+ struct connect_data *connect_data = user_data;
+ bdaddr_t src, dst;
+ char addr[18];
+ uint16_t handle;
+ struct btd_adapter *adapter;
+ struct btd_device *device;
+ struct btd_service *service;
+ struct hfp_device *dev;
+ struct hfp_server *server;
+ struct media_endpoint *endpoint = NULL;
+ GSList *l;
+ char *path = NULL;
+ uint16_t imtu, omtu;
+
+ if (err) {
+ error("Connecting failed: %s\n", err->message);
+ sco_connect_complete(connect_data, -EIO);
+ return;
+ }
+
+ if (!bt_io_get(io, &err,
+ BT_IO_OPT_SOURCE_BDADDR, &src,
+ BT_IO_OPT_DEST_BDADDR, &dst,
+ BT_IO_OPT_DEST, addr,
+ BT_IO_OPT_HANDLE, &handle,
+ BT_IO_OPT_IMTU, &imtu,
+ BT_IO_OPT_OMTU, &omtu,
+ BT_IO_OPT_INVALID)) {
+ error("Unable to get destination address: %s\n", err->message);
+ g_clear_error(&err);
+ sco_connect_complete(connect_data, -EIO);
+ return;
+ }
+
+ DBG("Successfully connected to %s. handle=%u imtu=%u omtu=%u",
+ addr, handle, imtu, omtu);
+
+ adapter = adapter_find(&src);
+ if (!adapter) {
+ sco_connect_complete(connect_data, -EIO);
+ return;
+ }
+
+ device = btd_adapter_find_device(adapter, &dst, BDADDR_BREDR);
+ if (!device) {
+ sco_connect_complete(connect_data, -EIO);
+ return;
+ }
+
+ service = btd_device_get_service(device, HFP_AG_UUID);
+ if (!service) {
+ sco_connect_complete(connect_data, -EIO);
+ return;
+ }
+
+ dev = btd_service_get_user_data(service);
+
+ server = find_server(servers, adapter);
+ if (server == NULL) {
+ sco_connect_complete(connect_data, -EIO);
+ return;
+ }
+
+ for (l = server->endpoints; l; l = l->next) {
+ if (media_endpoint_get_codec(l->data) == dev->codec) {
+ endpoint = l->data;
+ break;
+ }
+ }
+ if (endpoint == NULL) {
+ sco_connect_complete(connect_data, -EIO);
+ return;
+ }
+
+ path = make_endpoint_path(dev->telephony, dev->codec);
+ if (path == NULL) {
+ error("Could not allocate path for remote %s",
+ device_get_path(telephony_get_device(dev->telephony)));
+ sco_connect_complete(connect_data, -ENOMEM);
+ return;
+ }
+
+ if (!hfp_hf_set_configuration(endpoint, path, NULL, dev, NULL)) {
+ free(path);
+ sco_connect_complete(connect_data, -EIO);
+ return;
+ }
+ free(path);
+
+ dev->imtu = imtu;
+ dev->omtu = omtu;
+ dev->sco_io = g_io_channel_ref(io);
+
+ g_io_add_watch(io, G_IO_ERR | G_IO_HUP | G_IO_NVAL,
+ (GIOFunc) sco_io_cb, dev);
+
+ sco_connect_complete(connect_data, 0);
+}
+
+bool hfp_hf_sco_listen(struct btd_adapter *adapter, void *endpoint)
+{
+ struct hfp_server *server;
+ GError *err = NULL;
+
+ DBG("path %s, codec %u", adapter_get_path(adapter),
+ media_endpoint_get_codec(endpoint));
+
+ server = find_server(servers, adapter);
+ if (server == NULL)
+ return false;
+
+ server->endpoints = g_slist_append(server->endpoints, endpoint);
+
+ if (server->sco_io)
+ return true;
+
+ server->sco_io = bt_io_listen(sco_connect_cb, NULL, NULL, NULL,
+ &err,
+ BT_IO_OPT_SOURCE_BDADDR,
+ btd_adapter_get_address(server->adapter),
+ BT_IO_OPT_INVALID);
+ if (server->sco_io) {
+ DBG("SCO server started");
+ return true;
+ }
+
+ server->endpoints = g_slist_remove(server->endpoints, endpoint);
+ error("%s", err->message);
+ g_error_free(err);
+
+ return false;
+}
+
+void hfp_hf_sco_remove(struct btd_adapter *adapter, void *endpoint)
+{
+ struct hfp_server *server;
+
+ DBG("path %s, codec %u", adapter_get_path(adapter),
+ media_endpoint_get_codec(endpoint));
+
+ server = find_server(servers, adapter);
+ if (server == NULL) {
+ error("No server for %s codec %u", adapter_get_path(adapter),
+ media_endpoint_get_codec(endpoint));
+ return;
+ }
+
+ server->endpoints = g_slist_remove(server->endpoints, endpoint);
+
+ if (server->sco_io && g_slist_length(server->endpoints) == 0) {
+ g_io_channel_shutdown(server->sco_io, TRUE, NULL);
+ g_io_channel_unref(server->sco_io);
+ server->sco_io = NULL;
+ DBG("SCO server stopped");
+ }
+}
+
+struct btd_device *hfp_hf_get_device(struct hfp_device *dev)
+{
+ return telephony_get_device(dev->telephony);
+}
+
+int hfp_hf_device_get_fd(struct hfp_device *dev)
+{
+ if (!dev->sco_io)
+ return -1;
+
+ return g_io_channel_unix_get_fd(dev->sco_io);
+}
+
+uint16_t hfp_hf_device_get_imtu(struct hfp_device *dev)
+{
+ return dev->imtu;
+}
+
+uint16_t hfp_hf_device_get_omtu(struct hfp_device *dev)
+{
+ return dev->omtu;
+}
+
+static gboolean sco_start_cb(gpointer data)
+{
+ struct connect_data *connect_data = data;
+
+ if (connect_data && connect_data->cb) {
+ connect_data->cb(0, connect_data->cb_user_data);
+ g_free(connect_data);
+ }
+
+ return FALSE;
+}
+
+unsigned int hfp_hf_sco_start(struct hfp_device *dev, void *cb, void *user_data)
+{
+ bdaddr_t src, dst;
+ struct connect_data *connect_data;
+ GError *err = NULL;
+ GIOChannel *io;
+
+ DBG("codec %u", dev->codec);
+
+ connect_data = g_new0(struct connect_data, 1);
+ connect_data->dev = dev;
+ connect_data->cb = cb;
+ connect_data->cb_user_data = user_data;
+
+ src = telephony_get_src(dev->telephony);
+ dst = telephony_get_dst(dev->telephony);
+ if (!dev->sco_io) {
+ io = bt_io_connect(sco_connect_cb, connect_data, NULL, &err,
+ BT_IO_OPT_SOURCE_BDADDR, &src,
+ BT_IO_OPT_DEST_BDADDR, &dst,
+ BT_IO_OPT_SEC_LEVEL, BT_IO_SEC_MEDIUM,
+ BT_IO_OPT_INVALID);
+ if (!io) {
+ error("%s", err->message);
+ g_error_free(err);
+ g_free(connect_data);
+ return 0;
+ }
+ } else {
+ g_idle_add(sco_start_cb, connect_data);
+ }
+
+ return (++dev->resume_id);
+}
+
+unsigned int hfp_hf_sco_stop(struct hfp_device *dev)
+{
+ if (!dev->sco_io)
+ return 0;
+
+ DBG("codec %u", dev->codec);
+
+ g_io_channel_shutdown(dev->sco_io, TRUE, NULL);
+ g_io_channel_unref(dev->sco_io);
+ dev->sco_io = NULL;
+
+ return dev->resume_id;
+}
+
+void hfp_hf_set_sco_closed_cb(struct hfp_device *dev,
+ hfp_hf_sco_closed_cb cb, void *user_data)
+{
+ dev->sco_closed_cb = cb;
+ dev->sco_closed_data = user_data;
+}
+
static int hfp_adapter_probe(struct btd_profile *p,
struct btd_adapter *adapter)
{
@@ -724,6 +1134,11 @@ static void hfp_adapter_remove(struct btd_profile *p,
if (!server)
return;
+ if (server->sco_io) {
+ g_io_channel_shutdown(server->sco_io, TRUE, NULL);
+ g_io_channel_unref(server->sco_io);
+ }
+
if (server->io) {
g_io_channel_shutdown(server->io, TRUE, NULL);
g_io_channel_unref(server->io);
diff --git a/profiles/audio/hfp-hf.h b/profiles/audio/hfp-hf.h
new file mode 100644
index 000000000..6dfa3b12f
--- /dev/null
+++ b/profiles/audio/hfp-hf.h
@@ -0,0 +1,26 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ *
+ * BlueZ - Bluetooth protocol stack for Linux
+ *
+ * Copyright © 2025 Collabora Ltd.
+ *
+ *
+ */
+
+struct hfp_device;
+
+typedef void (*hfp_hf_sco_closed_cb) (struct hfp_device *dev, void *user_data);
+
+bool hfp_hf_sco_listen(struct btd_adapter *adapter, void *endpoint);
+void hfp_hf_sco_remove(struct btd_adapter *adapter, void *endpoint);
+unsigned int hfp_hf_sco_start(struct hfp_device *dev, void *cb,
+ void *user_data);
+unsigned int hfp_hf_sco_stop(struct hfp_device *dev);
+void hfp_hf_set_sco_closed_cb(struct hfp_device *dev,
+ hfp_hf_sco_closed_cb cb, void *user_data);
+
+struct btd_device *hfp_hf_get_device(struct hfp_device *dev);
+int hfp_hf_device_get_fd(struct hfp_device *dev);
+uint16_t hfp_hf_device_get_imtu(struct hfp_device *dev);
+uint16_t hfp_hf_device_get_omtu(struct hfp_device *dev);
diff --git a/profiles/audio/media.c b/profiles/audio/media.c
index dd1d4e571..e8418280e 100644
--- a/profiles/audio/media.c
+++ b/profiles/audio/media.c
@@ -66,6 +66,9 @@
#ifdef HAVE_A2DP
#include "a2dp.h"
#endif
+#ifdef HAVE_HFP
+#include "hfp-hf.h"
+#endif
#define MEDIA_INTERFACE "org.bluez.Media1"
#define MEDIA_ENDPOINT_INTERFACE "org.bluez.MediaEndpoint1"
@@ -119,6 +122,7 @@ struct media_endpoint {
#endif
struct bt_bap_pac *pac;
struct bt_asha_device *asha;
+ bool sco;
char *sender; /* Endpoint DBus bus id */
char *path; /* Endpoint object path */
char *uuid; /* Endpoint property UUID */
@@ -234,6 +238,11 @@ static void media_endpoint_destroy(struct media_endpoint *endpoint)
endpoint->pac = NULL;
}
+ if (endpoint->sco) {
+ hfp_hf_sco_remove(endpoint->adapter->btd_adapter, endpoint);
+ endpoint->sco = false;
+ }
+
g_dbus_remove_watch(btd_get_dbus_connection(), endpoint->watch);
g_free(endpoint->capabilities);
g_free(endpoint->metadata);
@@ -1430,6 +1439,64 @@ static bool endpoint_init_asha(struct media_endpoint *endpoint,
return true;
}
+gboolean hfp_hf_set_configuration(struct media_endpoint *endpoint,
+ const char *ep_path,
+ media_endpoint_cb_t cb,
+ void *user_data,
+ GDestroyNotify destroy)
+{
+ struct hfp_device *hfp_dev = user_data;
+ struct btd_device *device = hfp_hf_get_device(hfp_dev);
+ DBusConnection *conn = btd_get_dbus_connection();
+ DBusMessage *msg;
+ DBusMessageIter iter;
+ struct media_transport *transport;
+ const char *path;
+
+ msg = dbus_message_new_method_call(endpoint->sender, endpoint->path,
+ MEDIA_ENDPOINT_INTERFACE,
+ "SetConfiguration");
+ if (msg == NULL) {
+ error("Couldn't allocate D-Bus message");
+ return FALSE;
+ }
+
+ transport = find_device_transport(endpoint, device);
+ if (transport == NULL) {
+ transport = media_transport_create(device, ep_path, NULL, 0,
+ endpoint, hfp_dev);
+ if (transport == NULL)
+ return FALSE;
+
+ endpoint->transports = g_slist_append(endpoint->transports,
+ transport);
+ }
+
+ dbus_message_iter_init_append(msg, &iter);
+
+ path = media_transport_get_path(transport);
+ dbus_message_iter_append_basic(&iter, DBUS_TYPE_OBJECT_PATH, &path);
+
+ g_dbus_get_properties(conn, path, "org.bluez.MediaTransport1", &iter);
+
+ return media_endpoint_async_call(msg, endpoint, transport,
+ cb, user_data, destroy, -1);
+}
+
+static bool endpoint_init_sco(struct media_endpoint *endpoint,
+ int *err)
+{
+ if (!(g_dbus_get_flags() & G_DBUS_FLAG_ENABLE_EXPERIMENTAL)) {
+ DBG("D-Bus experimental not enabled");
+ *err = -ENOTSUP;
+ return false;
+ }
+
+ endpoint->sco = true;
+
+ return hfp_hf_sco_listen(endpoint->adapter->btd_adapter, endpoint);
+}
+
static bool endpoint_properties_exists(const char *uuid,
struct btd_device *dev,
void *user_data)
@@ -1559,6 +1626,11 @@ static bool experimental_asha_supported(struct btd_adapter *adapter)
return g_dbus_get_flags() & G_DBUS_FLAG_ENABLE_EXPERIMENTAL;
}
+static bool experimental_sco_supported(struct btd_adapter *adapter)
+{
+ return g_dbus_get_flags() & G_DBUS_FLAG_ENABLE_EXPERIMENTAL;
+}
+
static const struct media_endpoint_init {
const char *uuid;
bool (*func)(struct media_endpoint *endpoint, int *err);
@@ -1580,6 +1652,10 @@ static const struct media_endpoint_init {
experimental_bcast_sink_ep_supported },
{ ASHA_PROFILE_UUID, endpoint_init_asha,
experimental_asha_supported },
+#ifdef HAVE_HFP
+ { HFP_AG_UUID, endpoint_init_sco,
+ experimental_sco_supported },
+#endif
};
static struct media_endpoint *
diff --git a/profiles/audio/media.h b/profiles/audio/media.h
index 43a85b1d6..a84634fff 100644
--- a/profiles/audio/media.h
+++ b/profiles/audio/media.h
@@ -82,3 +82,9 @@ typedef void (*local_player_added_t)(struct local_player *lp, void *user_data);
unsigned int local_player_register_watch(local_player_added_t cb,
void *user_data);
void local_player_unregister_watch(unsigned int id);
+
+gboolean hfp_hf_set_configuration(struct media_endpoint *endpoint,
+ const char *ep_path,
+ media_endpoint_cb_t cb,
+ void *user_data,
+ GDestroyNotify destroy);
diff --git a/profiles/audio/transport.c b/profiles/audio/transport.c
index e3f3df50e..8cfc98a52 100644
--- a/profiles/audio/transport.c
+++ b/profiles/audio/transport.c
@@ -53,6 +53,10 @@
#include "asha.h"
#endif
+#ifdef HAVE_HFP
+#include "hfp-hf.h"
+#endif
+
#include "media.h"
#include "transport.h"
#include "vcp.h"
@@ -1748,6 +1752,15 @@ static const GDBusPropertyTable transport_asha_properties[] = {
};
#endif /* HAVE_ASHA */
+static const GDBusPropertyTable transport_hfp_properties[] = {
+ { "Device", "o", get_device },
+ { "Endpoint", "o", get_endpoint, NULL, endpoint_exists },
+ { "UUID", "s", get_uuid },
+ { "Codec", "y", get_codec },
+ { "State", "s", get_state },
+ { }
+};
+
#ifdef HAVE_A2DP
static void transport_a2dp_destroy(void *data)
{
@@ -2644,6 +2657,140 @@ static void *transport_asha_init(struct media_transport *transport, void *data)
}
#endif /* HAVE_ASHA */
+#ifdef HAVE_HFP
+static void hfp_transport_sco_closed(struct hfp_device *hfp_dev,
+ void *user_data)
+{
+ struct media_transport *transport = user_data;
+
+ DBG("%s", transport->path);
+
+ /* The remote hung up the SCO connection on its own, without us
+ * requesting a Release/Suspend. Bring the transport back down so
+ * its state reflects reality.
+ */
+ if (transport->owner)
+ media_transport_remove_owner(transport);
+ else if (transport->state != TRANSPORT_STATE_IDLE)
+ transport_set_state(transport, TRANSPORT_STATE_IDLE);
+}
+
+static void *transport_hfp_init(struct media_transport *transport, void *data)
+{
+ /* We just store the struct hfp_device on the transport */
+ hfp_hf_set_sco_closed_cb(data, hfp_transport_sco_closed, transport);
+
+ return data;
+}
+
+static void transport_hfp_destroy(void *data)
+{
+ hfp_hf_set_sco_closed_cb(data, NULL, NULL);
+}
+
+static void hfp_transport_resume_cb(int status, void *user_data)
+{
+ struct media_owner *owner = user_data;
+ struct media_transport *transport = owner->transport;
+ struct hfp_device *hfp_dev = transport->data;
+ int fd;
+ uint16_t imtu, omtu;
+ gboolean ret;
+
+ DBG("");
+
+ if (!transport) {
+ DBG("Lost owner while connecting, bailing");
+ return;
+ }
+
+ fd = hfp_hf_device_get_fd(hfp_dev);
+ imtu = hfp_hf_device_get_imtu(hfp_dev);
+ omtu = hfp_hf_device_get_omtu(hfp_dev);
+
+ media_transport_set_fd(transport, fd, imtu, omtu);
+
+ owner->pending->id = 0;
+ ret = g_dbus_send_reply(btd_get_dbus_connection(),
+ owner->pending->msg,
+ DBUS_TYPE_UNIX_FD, &fd,
+ DBUS_TYPE_UINT16, &imtu,
+ DBUS_TYPE_UINT16, &omtu,
+ DBUS_TYPE_INVALID);
+ if (!ret) {
+ media_transport_remove_owner(transport);
+ return;
+ }
+
+ media_owner_remove(owner);
+
+ transport_set_state(transport, TRANSPORT_STATE_ACTIVE);
+}
+
+static guint transport_hfp_resume(struct media_transport *transport,
+ struct media_owner *owner)
+{
+ struct hfp_device *hfp_dev = transport->data;
+
+ return hfp_hf_sco_start(hfp_dev, hfp_transport_resume_cb, owner);
+}
+
+static gboolean hfp_transport_suspend_cb(void *user_data)
+{
+ struct media_owner *owner = user_data;
+ struct media_transport *transport = owner->transport;
+
+ /* Release always succeeds */
+ if (owner->pending) {
+ owner->pending->id = 0;
+ media_request_reply(owner->pending, 0);
+ media_owner_remove(owner);
+ }
+
+ media_transport_remove_owner(transport);
+ return FALSE;
+}
+
+static guint transport_hfp_suspend(struct media_transport *transport,
+ struct media_owner *owner)
+{
+ struct hfp_device *hfp_dev = transport->data;
+ guint ret = 0;
+
+ hfp_hf_sco_stop(hfp_dev);
+
+ if (owner) {
+ /* Return the g_idle_add() source id here, not
+ * hfp_hf_sco_stop()'s return value (dev->resume_id). The
+ * latter is unrelated and is 0 whenever dev->sco_io is
+ * already NULL, e.g. right after the peer has hung up SCO.
+ * release() below treats a 0 id as "no pending async
+ * operation" and immediately, synchronously frees owner via
+ * media_transport_remove_owner() -- while the idle callback
+ * queued here still holds a pointer to it, causing a
+ * use-after-free crash in hfp_transport_suspend_cb() on the
+ * next mainloop iteration.
+ */
+ ret = g_idle_add(hfp_transport_suspend_cb, owner);
+ } else {
+ /* We won't have a callback to set the final state */
+ transport_set_state(transport, TRANSPORT_STATE_IDLE);
+ }
+
+ return ret;
+}
+
+static void transport_hfp_cancel(struct media_transport *transport, guint id)
+{
+ /* id is the g_idle_add() source id returned by transport_hfp_suspend()
+ * for its still-pending hfp_transport_suspend_cb() callback. Cancel it
+ * so it can never fire with a dangling owner pointer if owner is torn
+ * down through another path first.
+ */
+ g_source_remove(id);
+}
+#endif /* HAVE_HFP */
+
#define TRANSPORT_OPS(_uuid, _props, _set_owner, _remove_owner, _init, \
_resume, _suspend, _cancel, _set_state, _get_stream, \
_get_volume, _set_volume, _set_delay, _update_links, \
@@ -2704,6 +2851,14 @@ static void *transport_asha_init(struct media_transport *transport, void *data)
transport_asha_get_volume, transport_asha_set_volume, \
NULL, NULL, NULL, NULL)
+#define HFP_OPS(_uuid) \
+ TRANSPORT_OPS(_uuid, transport_hfp_properties, NULL, NULL, \
+ transport_hfp_init, \
+ transport_hfp_resume, transport_hfp_suspend, \
+ transport_hfp_cancel, NULL, NULL, \
+ NULL, NULL, \
+ NULL, NULL, transport_hfp_destroy, NULL)
+
static const struct media_transport_ops transport_ops[] = {
#ifdef HAVE_A2DP
A2DP_OPS(A2DP_SOURCE_UUID, transport_a2dp_src_init,
@@ -2730,6 +2885,9 @@ static const struct media_transport_ops transport_ops[] = {
#ifdef HAVE_ASHA
ASHA_OPS(ASHA_PROFILE_UUID),
#endif /* HAVE_ASHA */
+#ifdef HAVE_HFP
+ HFP_OPS(HFP_AG_UUID),
+#endif /* HAVE_HFP */
};
static const struct media_transport_ops *
--
2.43.0
^ permalink raw reply related [flat|nested] 14+ messages in thread* [PATCH BlueZ 03/12] client/player: Add MediaEndpoints for HFP HF codecs
2026-09-25 13:25 [PATCH BlueZ 00/12] hfp-hf: Enhance HFP Hands-Free profile support Frédéric Danis
2026-09-25 13:25 ` [PATCH BlueZ 01/12] audio/hfp-hf: Add HFP HF server and SDP record Frédéric Danis
2026-09-25 13:25 ` [PATCH BlueZ 02/12] audio/hfp-hf: Add MediaEndpoint for HFP codecs Frédéric Danis
@ 2026-09-25 13:25 ` Frédéric Danis
2026-09-25 13:25 ` [PATCH BlueZ 04/12] shared/hfp: Add hangup all calls support Frédéric Danis
` (8 subsequent siblings)
11 siblings, 0 replies; 14+ messages in thread
From: Frédéric Danis @ 2026-09-25 13:25 UTC (permalink / raw)
To: linux-bluetooth
From: Frédéric Danis <frederic.danis.oss@gmail.com>
Assisted-by: Claude:claude-sonnet-5
---
client/player.c | 18 ++++++++++++++++--
1 file changed, 16 insertions(+), 2 deletions(-)
diff --git a/client/player.c b/client/player.c
index ae2ce66f0..88221076c 100644
--- a/client/player.c
+++ b/client/player.c
@@ -1363,6 +1363,18 @@ static const struct capabilities {
LC3_DATA(LC3_FREQ_ANY, LC3_DURATION_ANY, 26,
240),
UTIL_IOV_INIT()),
+
+ /* SCO CVSD:
+ */
+ CODEC_CAPABILITIES("hfp_ag/cvsd", HFP_AG_UUID, 1,
+ UTIL_IOV_INIT(),
+ UTIL_IOV_INIT()),
+
+ /* SCO mSBC:
+ */
+ CODEC_CAPABILITIES("hfp_ag/msbc", HFP_AG_UUID, 2,
+ UTIL_IOV_INIT(),
+ UTIL_IOV_INIT()),
};
struct codec_preset {
@@ -3069,8 +3081,10 @@ static void endpoint_free(void *data)
if (ep->msg)
dbus_message_unref(ep->msg);
- queue_destroy(ep->preset->custom, free);
- ep->preset->custom = NULL;
+ if (ep->preset) {
+ queue_destroy(ep->preset->custom, free);
+ ep->preset->custom = NULL;
+ }
if (ep->codec == 0xff)
free(ep->preset);
--
2.43.0
^ permalink raw reply related [flat|nested] 14+ messages in thread* [PATCH BlueZ 04/12] shared/hfp: Add hangup all calls support
2026-09-25 13:25 [PATCH BlueZ 00/12] hfp-hf: Enhance HFP Hands-Free profile support Frédéric Danis
` (2 preceding siblings ...)
2026-09-25 13:25 ` [PATCH BlueZ 03/12] client/player: Add MediaEndpoints for HFP HF codecs Frédéric Danis
@ 2026-09-25 13:25 ` Frédéric Danis
2026-09-25 13:25 ` [PATCH BlueZ 05/12] audio/hfp-hf: " Frédéric Danis
` (7 subsequent siblings)
11 siblings, 0 replies; 14+ messages in thread
From: Frédéric Danis @ 2026-09-25 13:25 UTC (permalink / raw)
To: linux-bluetooth
From: Frédéric Danis <frederic.danis.oss@gmail.com>
This releases all calls except waiting calls. This includes
multiparty calls.
---
src/shared/hfp.c | 47 +++++++++++++++++++++++++++++++++++++++++++++++
src/shared/hfp.h | 3 +++
2 files changed, 50 insertions(+)
diff --git a/src/shared/hfp.c b/src/shared/hfp.c
index 59e09f80e..f9fa53ae4 100644
--- a/src/shared/hfp.c
+++ b/src/shared/hfp.c
@@ -2970,6 +2970,53 @@ bool hfp_hf_swap_calls(struct hfp_hf *hfp,
return hfp_hf_send_command(hfp, resp_cb, user_data, "AT+CHLD=2");
}
+bool hfp_hf_hangup_all(struct hfp_hf *hfp,
+ hfp_response_func_t resp_cb,
+ void *user_data)
+{
+ bool found_active = false;
+ bool found_held = false;
+ const struct queue_entry *entry;
+
+ if (!hfp)
+ return false;
+
+ DBG(hfp, "");
+
+ for (entry = queue_get_entries(hfp->calls); entry;
+ entry = entry->next) {
+ struct hf_call *call = entry->data;
+
+ if (call_setup_match(call, NULL) ||
+ call_active_match(call, NULL)) {
+ found_active = true;
+ } else if (call_held_match(call, NULL)) {
+ found_held = true;
+ }
+ }
+
+ if (!found_active && !found_held)
+ return false;
+
+ if (found_held && (hfp->chlds & HFP_CHLD_0)) {
+ if (!hfp_hf_send_command(hfp, resp_cb, user_data,
+ "AT+CHLD=0")) {
+ DBG(hfp, "Failed to hangup held calls");
+ return false;
+ }
+ }
+
+ if (found_active) {
+ if (!hfp_hf_send_command(hfp, resp_cb, user_data,
+ "AT+CHUP")) {
+ DBG(hfp, "Failed to hangup active calls");
+ return false;
+ }
+ }
+
+ return true;
+}
+
bool hfp_hf_call_answer(struct hfp_hf *hfp, uint id,
hfp_response_func_t resp_cb,
void *user_data)
diff --git a/src/shared/hfp.h b/src/shared/hfp.h
index 201777605..77ada40b6 100644
--- a/src/shared/hfp.h
+++ b/src/shared/hfp.h
@@ -258,6 +258,9 @@ bool hfp_hf_release_and_accept(struct hfp_hf *hfp,
bool hfp_hf_swap_calls(struct hfp_hf *hfp,
hfp_response_func_t resp_cb,
void *user_data);
+bool hfp_hf_hangup_all(struct hfp_hf *hfp,
+ hfp_response_func_t resp_cb,
+ void *user_data);
bool hfp_hf_call_answer(struct hfp_hf *hfp, uint id,
hfp_response_func_t resp_cb,
void *user_data);
--
2.43.0
^ permalink raw reply related [flat|nested] 14+ messages in thread* [PATCH BlueZ 05/12] audio/hfp-hf: Add hangup all calls support
2026-09-25 13:25 [PATCH BlueZ 00/12] hfp-hf: Enhance HFP Hands-Free profile support Frédéric Danis
` (3 preceding siblings ...)
2026-09-25 13:25 ` [PATCH BlueZ 04/12] shared/hfp: Add hangup all calls support Frédéric Danis
@ 2026-09-25 13:25 ` Frédéric Danis
2026-09-25 13:25 ` [PATCH BlueZ 06/12] shared/hfp: Add send tones support Frédéric Danis
` (6 subsequent siblings)
11 siblings, 0 replies; 14+ messages in thread
From: Frédéric Danis @ 2026-09-25 13:25 UTC (permalink / raw)
To: linux-bluetooth
From: Frédéric Danis <frederic.danis.oss@gmail.com>
This releases all calls except waiting calls. This includes
multiparty calls.
---
profiles/audio/hfp-hf.c | 15 +++++++++++++++
1 file changed, 15 insertions(+)
diff --git a/profiles/audio/hfp-hf.c b/profiles/audio/hfp-hf.c
index dddd56261..287b79f1f 100644
--- a/profiles/audio/hfp-hf.c
+++ b/profiles/audio/hfp-hf.c
@@ -503,6 +503,20 @@ static DBusMessage *dial(DBusConnection *conn, DBusMessage *msg,
return NULL;
}
+static DBusMessage *hangup_all(DBusConnection *conn, DBusMessage *msg,
+ void *profile_data)
+{
+ struct hfp_device *dev = profile_data;
+ bool ret;
+
+ ret = hfp_hf_hangup_all(dev->hf, cmd_complete,
+ dbus_message_ref(msg));
+ if (!ret)
+ return btd_error_failed(msg, "Hang up all command failed");
+
+ return NULL;
+}
+
static DBusMessage *call_answer(DBusConnection *conn, DBusMessage *msg,
void *call_data)
{
@@ -535,6 +549,7 @@ static DBusMessage *call_hangup(DBusConnection *conn, DBusMessage *msg,
struct telephony_callbacks hfp_callbacks = {
.dial = dial,
+ .hangup_all = hangup_all,
.call_answer = call_answer,
.call_hangup = call_hangup,
};
--
2.43.0
^ permalink raw reply related [flat|nested] 14+ messages in thread* [PATCH BlueZ 06/12] shared/hfp: Add send tones support
2026-09-25 13:25 [PATCH BlueZ 00/12] hfp-hf: Enhance HFP Hands-Free profile support Frédéric Danis
` (4 preceding siblings ...)
2026-09-25 13:25 ` [PATCH BlueZ 05/12] audio/hfp-hf: " Frédéric Danis
@ 2026-09-25 13:25 ` Frédéric Danis
2026-09-25 13:25 ` [PATCH BlueZ 07/12] audio/hfp-hf: Add Send Tones support Frédéric Danis
` (5 subsequent siblings)
11 siblings, 0 replies; 14+ messages in thread
From: Frédéric Danis @ 2026-09-25 13:25 UTC (permalink / raw)
To: linux-bluetooth
From: Frédéric Danis <frederic.danis.oss@gmail.com>
---
src/shared/hfp.c | 18 ++++++++++++++++++
src/shared/hfp.h | 3 +++
2 files changed, 21 insertions(+)
diff --git a/src/shared/hfp.c b/src/shared/hfp.c
index f9fa53ae4..dffad6fee 100644
--- a/src/shared/hfp.c
+++ b/src/shared/hfp.c
@@ -3017,6 +3017,24 @@ bool hfp_hf_hangup_all(struct hfp_hf *hfp,
return true;
}
+bool hfp_hf_send_tones(struct hfp_hf *hfp, const char *tones,
+ hfp_response_func_t resp_cb,
+ void *user_data)
+{
+ if (!hfp)
+ return false;
+
+ DBG(hfp, "");
+
+ if (!queue_find(hfp->calls, call_active_match, NULL)) {
+ DBG(hfp, "hf: No active call to send tones");
+ return false;
+ }
+
+ return hfp_hf_send_command(hfp, resp_cb, user_data, "AT+VTS=%s",
+ tones);
+}
+
bool hfp_hf_call_answer(struct hfp_hf *hfp, uint id,
hfp_response_func_t resp_cb,
void *user_data)
diff --git a/src/shared/hfp.h b/src/shared/hfp.h
index 77ada40b6..11610018e 100644
--- a/src/shared/hfp.h
+++ b/src/shared/hfp.h
@@ -261,6 +261,9 @@ bool hfp_hf_swap_calls(struct hfp_hf *hfp,
bool hfp_hf_hangup_all(struct hfp_hf *hfp,
hfp_response_func_t resp_cb,
void *user_data);
+bool hfp_hf_send_tones(struct hfp_hf *hfp, const char *tones,
+ hfp_response_func_t resp_cb,
+ void *user_data);
bool hfp_hf_call_answer(struct hfp_hf *hfp, uint id,
hfp_response_func_t resp_cb,
void *user_data);
--
2.43.0
^ permalink raw reply related [flat|nested] 14+ messages in thread* [PATCH BlueZ 07/12] audio/hfp-hf: Add Send Tones support
2026-09-25 13:25 [PATCH BlueZ 00/12] hfp-hf: Enhance HFP Hands-Free profile support Frédéric Danis
` (5 preceding siblings ...)
2026-09-25 13:25 ` [PATCH BlueZ 06/12] shared/hfp: Add send tones support Frédéric Danis
@ 2026-09-25 13:25 ` Frédéric Danis
2026-09-25 13:25 ` [PATCH BlueZ 08/12] client/telephony: Add Send Tones menu entry Frédéric Danis
` (4 subsequent siblings)
11 siblings, 0 replies; 14+ messages in thread
From: Frédéric Danis @ 2026-09-25 13:25 UTC (permalink / raw)
To: linux-bluetooth
From: Frédéric Danis <frederic.danis.oss@gmail.com>
---
profiles/audio/hfp-hf.c | 21 +++++++++++++++++++++
1 file changed, 21 insertions(+)
diff --git a/profiles/audio/hfp-hf.c b/profiles/audio/hfp-hf.c
index 287b79f1f..1843b6758 100644
--- a/profiles/audio/hfp-hf.c
+++ b/profiles/audio/hfp-hf.c
@@ -517,6 +517,26 @@ static DBusMessage *hangup_all(DBusConnection *conn, DBusMessage *msg,
return NULL;
}
+static DBusMessage *send_tones(DBusConnection *conn, DBusMessage *msg,
+ void *profile_data)
+{
+ struct hfp_device *dev = profile_data;
+ const char *tones;
+ bool ret;
+
+ if (!dbus_message_get_args(msg, NULL, DBUS_TYPE_STRING, &tones,
+ DBUS_TYPE_INVALID)) {
+ return btd_error_invalid_args(msg);
+ }
+
+ ret = hfp_hf_send_tones(dev->hf, tones, cmd_complete,
+ dbus_message_ref(msg));
+ if (!ret)
+ return btd_error_failed(msg, "Send tones command failed");
+
+ return NULL;
+}
+
static DBusMessage *call_answer(DBusConnection *conn, DBusMessage *msg,
void *call_data)
{
@@ -550,6 +570,7 @@ static DBusMessage *call_hangup(DBusConnection *conn, DBusMessage *msg,
struct telephony_callbacks hfp_callbacks = {
.dial = dial,
.hangup_all = hangup_all,
+ .send_tones = send_tones,
.call_answer = call_answer,
.call_hangup = call_hangup,
};
--
2.43.0
^ permalink raw reply related [flat|nested] 14+ messages in thread* [PATCH BlueZ 08/12] client/telephony: Add Send Tones menu entry
2026-09-25 13:25 [PATCH BlueZ 00/12] hfp-hf: Enhance HFP Hands-Free profile support Frédéric Danis
` (6 preceding siblings ...)
2026-09-25 13:25 ` [PATCH BlueZ 07/12] audio/hfp-hf: Add Send Tones support Frédéric Danis
@ 2026-09-25 13:25 ` Frédéric Danis
2026-09-25 13:25 ` [PATCH BlueZ 09/12] audio/hfp-hf: Add multi calls support Frédéric Danis
` (3 subsequent siblings)
11 siblings, 0 replies; 14+ messages in thread
From: Frédéric Danis @ 2026-09-25 13:25 UTC (permalink / raw)
To: linux-bluetooth
From: Frédéric Danis <frederic.danis.oss@gmail.com>
---
client/telephony.c | 47 ++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 47 insertions(+)
diff --git a/client/telephony.c b/client/telephony.c
index cc92fe33a..ab2a93430 100644
--- a/client/telephony.c
+++ b/client/telephony.c
@@ -277,6 +277,51 @@ static void cmd_hangupall(int argc, char *argv[])
bt_shell_printf("Attempting to hangup all calls\n");
}
+static void send_tones_reply(DBusMessage *message, void *user_data)
+{
+ DBusError error;
+
+ dbus_error_init(&error);
+
+ if (dbus_set_error_from_message(&error, message) == TRUE) {
+ bt_shell_printf("Failed to send tones: %s\n", error.name);
+ dbus_error_free(&error);
+ return bt_shell_noninteractive_quit(EXIT_FAILURE);
+ }
+
+ bt_shell_printf("Send tones successful\n");
+
+ return bt_shell_noninteractive_quit(EXIT_FAILURE);
+}
+
+static void cmd_send_tones(int argc, char *argv[])
+{
+ GDBusProxy *proxy;
+
+ if (argc < 3) {
+ if (check_default_ag() == FALSE)
+ return bt_shell_noninteractive_quit(EXIT_FAILURE);
+
+ proxy = default_ag;
+ } else {
+ proxy = g_dbus_proxy_lookup(ags, NULL, argv[2],
+ BLUEZ_TELEPHONY_INTERFACE);
+ if (!proxy) {
+ bt_shell_printf("Audio gateway %s not available\n",
+ argv[1]);
+ return bt_shell_noninteractive_quit(EXIT_FAILURE);
+ }
+ }
+
+ if (g_dbus_proxy_method_call(proxy, "SendTones", dial_setup,
+ send_tones_reply, argv[1], NULL) == FALSE) {
+ bt_shell_printf("Failed to send tones\n");
+ return bt_shell_noninteractive_quit(EXIT_FAILURE);
+ }
+
+ bt_shell_printf("Attempting to send tones\n");
+}
+
static void cmd_list_calls(int argc, char *arg[])
{
g_list_foreach(calls, print_call, NULL);
@@ -507,6 +552,8 @@ static const struct bt_shell_menu telephony_menu = {
ag_generator},
{ "hangup-all", "[telephony]", cmd_hangupall, "Hangup all calls",
ag_generator},
+ { "send-tones", "<tones> [telephony]", cmd_send_tones, "Send tones",
+ ag_generator},
{ "list-calls", NULL, cmd_list_calls, "List calls" },
{ "show-call", "<call>", cmd_show_call, "Show call information",
call_generator},
--
2.43.0
^ permalink raw reply related [flat|nested] 14+ messages in thread* [PATCH BlueZ 09/12] audio/hfp-hf: Add multi calls support
2026-09-25 13:25 [PATCH BlueZ 00/12] hfp-hf: Enhance HFP Hands-Free profile support Frédéric Danis
` (7 preceding siblings ...)
2026-09-25 13:25 ` [PATCH BlueZ 08/12] client/telephony: Add Send Tones menu entry Frédéric Danis
@ 2026-09-25 13:25 ` Frédéric Danis
2026-09-25 13:25 ` [PATCH BlueZ 10/12] client/telephony: " Frédéric Danis
` (2 subsequent siblings)
11 siblings, 0 replies; 14+ messages in thread
From: Frédéric Danis @ 2026-09-25 13:25 UTC (permalink / raw)
To: linux-bluetooth
From: Frédéric Danis <frederic.danis.oss@gmail.com>
This implements the SwapCalls, ReleaseAndAnswer, ReleaseAndSwap and
HoldAndAnswer methods from org.bluez.Telephony1.
---
profiles/audio/hfp-hf.c | 142 ++++++++++++++++++++++++++++++++++++++++
1 file changed, 142 insertions(+)
diff --git a/profiles/audio/hfp-hf.c b/profiles/audio/hfp-hf.c
index 1843b6758..25dce517f 100644
--- a/profiles/audio/hfp-hf.c
+++ b/profiles/audio/hfp-hf.c
@@ -503,6 +503,144 @@ static DBusMessage *dial(DBusConnection *conn, DBusMessage *msg,
return NULL;
}
+static DBusMessage *swap_calls(DBusConnection *conn, DBusMessage *msg,
+ void *profile_data)
+{
+ struct hfp_device *dev = profile_data;
+ const struct queue_entry *entry;
+ bool found_held = false;
+ bool ret;
+
+ for (entry = queue_get_entries(dev->calls); entry;
+ entry = entry->next) {
+ struct call *call = entry->data;
+
+ if (call->state == CALL_STATE_HELD) {
+ found_held = true;
+ break;
+ }
+ }
+
+ if (!found_held) {
+ return btd_error_failed(msg,
+ "Swap calls command failed: "
+ "no held calls");
+ }
+
+ ret = hfp_hf_swap_calls(dev->hf, cmd_complete,
+ dbus_message_ref(msg));
+ if (!ret)
+ return btd_error_failed(msg, "Swap calls command failed");
+
+ return NULL;
+}
+
+static DBusMessage *release_and_answer(DBusConnection *conn, DBusMessage *msg,
+ void *profile_data)
+{
+ struct hfp_device *dev = profile_data;
+ const struct queue_entry *entry;
+ bool found_active = false;
+ bool found_waiting = false;
+ bool ret;
+
+ for (entry = queue_get_entries(dev->calls); entry;
+ entry = entry->next) {
+ struct call *call = entry->data;
+
+ if (call->state == CALL_STATE_ACTIVE)
+ found_active = true;
+ else if (call->state == CALL_STATE_WAITING)
+ found_waiting = true;
+ }
+
+ if (!found_active || !found_waiting) {
+ return btd_error_failed(msg,
+ "Release and answer command failed: "
+ "no active and waiting calls");
+ }
+
+ ret = hfp_hf_release_and_accept(dev->hf, cmd_complete,
+ dbus_message_ref(msg));
+ if (!ret)
+ return btd_error_failed(msg,
+ "Release and answer command failed");
+
+ return NULL;
+}
+
+static DBusMessage *release_and_swap(DBusConnection *conn, DBusMessage *msg,
+ void *profile_data)
+{
+ struct hfp_device *dev = profile_data;
+ const struct queue_entry *entry;
+ bool found_active = false;
+ bool found_held = false;
+ bool ret;
+
+ for (entry = queue_get_entries(dev->calls); entry;
+ entry = entry->next) {
+ struct call *call = entry->data;
+
+ if (call->state == CALL_STATE_WAITING) {
+ return btd_error_failed(msg,
+ "Release and swap command failed: "
+ "waiting call exists");
+ } else if (call->state == CALL_STATE_ACTIVE)
+ found_active = true;
+ else if (call->state == CALL_STATE_HELD)
+ found_held = true;
+ }
+
+ if (!found_active || !found_held) {
+ return btd_error_failed(msg,
+ "Release and swap command failed: "
+ "no active and held calls");
+ }
+
+ ret = hfp_hf_release_and_accept(dev->hf, cmd_complete,
+ dbus_message_ref(msg));
+ if (!ret)
+ return btd_error_failed(msg,
+ "Release and swap command failed");
+
+ return NULL;
+}
+
+static DBusMessage *hold_and_answer(DBusConnection *conn, DBusMessage *msg,
+ void *profile_data)
+{
+ struct hfp_device *dev = profile_data;
+ const struct queue_entry *entry;
+ bool found_active = false;
+ bool found_waiting = false;
+ bool ret;
+
+ for (entry = queue_get_entries(dev->calls); entry;
+ entry = entry->next) {
+ struct call *call = entry->data;
+
+ if (call->state == CALL_STATE_ACTIVE)
+ found_active = true;
+ else if (call->state == CALL_STATE_WAITING)
+ found_waiting = true;
+ }
+
+ if (!found_active || !found_waiting) {
+ return btd_error_failed(msg,
+ "Hold and answer command failed: "
+ "no active and waiting calls");
+ }
+
+ ret = hfp_hf_swap_calls(dev->hf, cmd_complete,
+ dbus_message_ref(msg));
+ if (!ret)
+ return btd_error_failed(msg,
+ "Hold and answer command failed");
+
+ return NULL;
+}
+
static DBusMessage *hangup_all(DBusConnection *conn, DBusMessage *msg,
void *profile_data)
{
@@ -569,6 +707,10 @@ static DBusMessage *call_hangup(DBusConnection *conn, DBusMessage *msg,
struct telephony_callbacks hfp_callbacks = {
.dial = dial,
+ .swap_calls = swap_calls,
+ .release_and_answer = release_and_answer,
+ .release_and_swap = release_and_swap,
+ .hold_and_answer = hold_and_answer,
.hangup_all = hangup_all,
.send_tones = send_tones,
.call_answer = call_answer,
--
2.43.0
^ permalink raw reply related [flat|nested] 14+ messages in thread* [PATCH BlueZ 10/12] client/telephony: Add multi calls support
2026-09-25 13:25 [PATCH BlueZ 00/12] hfp-hf: Enhance HFP Hands-Free profile support Frédéric Danis
` (8 preceding siblings ...)
2026-09-25 13:25 ` [PATCH BlueZ 09/12] audio/hfp-hf: Add multi calls support Frédéric Danis
@ 2026-09-25 13:25 ` Frédéric Danis
2026-09-25 13:25 ` [PATCH BlueZ 11/12] shared/hfp: Add codecs support Frédéric Danis
2026-09-25 13:25 ` [PATCH BlueZ 12/12] audio/hfp-hf: " Frédéric Danis
11 siblings, 0 replies; 14+ messages in thread
From: Frédéric Danis @ 2026-09-25 13:25 UTC (permalink / raw)
To: linux-bluetooth
From: Frédéric Danis <frederic.danis.oss@gmail.com>
This adds menu entries for the SwapCalls, ReleaseAndAnswer,
ReleaseAndSwap and HoldAndAnswer methods from org.bluez.Telephony1.
---
client/telephony.c | 190 +++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 190 insertions(+)
diff --git a/client/telephony.c b/client/telephony.c
index ab2a93430..858c02c0b 100644
--- a/client/telephony.c
+++ b/client/telephony.c
@@ -232,6 +232,187 @@ static void cmd_dial(int argc, char *argv[])
bt_shell_printf("Attempting to dial\n");
}
+static void swap_calls_reply(DBusMessage *message, void *user_data)
+{
+ DBusError error;
+
+ dbus_error_init(&error);
+
+ if (dbus_set_error_from_message(&error, message) == TRUE) {
+ bt_shell_printf("Failed to swap calls: %s\n", error.name);
+ dbus_error_free(&error);
+ return bt_shell_noninteractive_quit(EXIT_FAILURE);
+ }
+
+ bt_shell_printf("Swap calls successful\n");
+
+ return bt_shell_noninteractive_quit(EXIT_FAILURE);
+}
+
+static void cmd_swap_calls(int argc, char *argv[])
+{
+ GDBusProxy *proxy;
+
+ if (argc < 2) {
+ if (check_default_ag() == FALSE)
+ return bt_shell_noninteractive_quit(EXIT_FAILURE);
+
+ proxy = default_ag;
+ } else {
+ proxy = g_dbus_proxy_lookup(ags, NULL, argv[1],
+ BLUEZ_TELEPHONY_INTERFACE);
+ if (!proxy) {
+ bt_shell_printf("Audio gateway %s not available\n",
+ argv[1]);
+ return bt_shell_noninteractive_quit(EXIT_FAILURE);
+ }
+ }
+
+ if (g_dbus_proxy_method_call(proxy, "SwapCalls", NULL,
+ swap_calls_reply, NULL, NULL) == FALSE) {
+ bt_shell_printf("Failed to swap calls\n");
+ return bt_shell_noninteractive_quit(EXIT_FAILURE);
+ }
+
+ bt_shell_printf("Attempting to swap calls\n");
+}
+
+static void release_answer_reply(DBusMessage *message, void *user_data)
+{
+ DBusError error;
+
+ dbus_error_init(&error);
+
+ if (dbus_set_error_from_message(&error, message) == TRUE) {
+ bt_shell_printf("Failed to release and answer: %s\n",
+ error.name);
+ dbus_error_free(&error);
+ return bt_shell_noninteractive_quit(EXIT_FAILURE);
+ }
+
+ bt_shell_printf("Release and answer successful\n");
+
+ return bt_shell_noninteractive_quit(EXIT_FAILURE);
+}
+
+static void cmd_release_answer(int argc, char *argv[])
+{
+ GDBusProxy *proxy;
+
+ if (argc < 2) {
+ if (check_default_ag() == FALSE)
+ return bt_shell_noninteractive_quit(EXIT_FAILURE);
+
+ proxy = default_ag;
+ } else {
+ proxy = g_dbus_proxy_lookup(ags, NULL, argv[1],
+ BLUEZ_TELEPHONY_INTERFACE);
+ if (!proxy) {
+ bt_shell_printf("Audio gateway %s not available\n",
+ argv[1]);
+ return bt_shell_noninteractive_quit(EXIT_FAILURE);
+ }
+ }
+
+ if (g_dbus_proxy_method_call(proxy, "ReleaseAndAnswer", NULL,
+ release_answer_reply, NULL, NULL) == FALSE) {
+ bt_shell_printf("Failed to release and answer\n");
+ return bt_shell_noninteractive_quit(EXIT_FAILURE);
+ }
+
+ bt_shell_printf("Attempting to release and answer\n");
+}
+
+static void release_swap_reply(DBusMessage *message, void *user_data)
+{
+ DBusError error;
+
+ dbus_error_init(&error);
+
+ if (dbus_set_error_from_message(&error, message) == TRUE) {
+ bt_shell_printf("Failed to release and swap: %s\n", error.name);
+ dbus_error_free(&error);
+ return bt_shell_noninteractive_quit(EXIT_FAILURE);
+ }
+
+ bt_shell_printf("Release and swap successful\n");
+
+ return bt_shell_noninteractive_quit(EXIT_FAILURE);
+}
+
+static void cmd_release_swap(int argc, char *argv[])
+{
+ GDBusProxy *proxy;
+
+ if (argc < 2) {
+ if (check_default_ag() == FALSE)
+ return bt_shell_noninteractive_quit(EXIT_FAILURE);
+
+ proxy = default_ag;
+ } else {
+ proxy = g_dbus_proxy_lookup(ags, NULL, argv[1],
+ BLUEZ_TELEPHONY_INTERFACE);
+ if (!proxy) {
+ bt_shell_printf("Audio gateway %s not available\n",
+ argv[1]);
+ return bt_shell_noninteractive_quit(EXIT_FAILURE);
+ }
+ }
+
+ if (g_dbus_proxy_method_call(proxy, "ReleaseAndSwap", NULL,
+ release_swap_reply, NULL, NULL) == FALSE) {
+ bt_shell_printf("Failed to release and swap\n");
+ return bt_shell_noninteractive_quit(EXIT_FAILURE);
+ }
+
+ bt_shell_printf("Attempting to release and swap\n");
+}
+
+static void hold_answer_reply(DBusMessage *message, void *user_data)
+{
+ DBusError error;
+
+ dbus_error_init(&error);
+
+ if (dbus_set_error_from_message(&error, message) == TRUE) {
+ bt_shell_printf("Failed to hold and answer: %s\n", error.name);
+ dbus_error_free(&error);
+ return bt_shell_noninteractive_quit(EXIT_FAILURE);
+ }
+
+ bt_shell_printf("Hold and answer successful\n");
+
+ return bt_shell_noninteractive_quit(EXIT_FAILURE);
+}
+
+static void cmd_hold_answer(int argc, char *argv[])
+{
+ GDBusProxy *proxy;
+
+ if (argc < 2) {
+ if (check_default_ag() == FALSE)
+ return bt_shell_noninteractive_quit(EXIT_FAILURE);
+
+ proxy = default_ag;
+ } else {
+ proxy = g_dbus_proxy_lookup(ags, NULL, argv[1],
+ BLUEZ_TELEPHONY_INTERFACE);
+ if (!proxy) {
+ bt_shell_printf("Audio gateway %s not available\n",
+ argv[1]);
+ return bt_shell_noninteractive_quit(EXIT_FAILURE);
+ }
+ }
+
+ if (g_dbus_proxy_method_call(proxy, "HoldAndAnswer", NULL,
+ hold_answer_reply, NULL, NULL) == FALSE) {
+ bt_shell_printf("Failed to hold and answer\n");
+ return bt_shell_noninteractive_quit(EXIT_FAILURE);
+ }
+
+ bt_shell_printf("Attempting to hold and answer\n");
+}
+
static void hangupall_reply(DBusMessage *message, void *user_data)
{
DBusError error;
@@ -550,6 +731,15 @@ static const struct bt_shell_menu telephony_menu = {
ag_generator},
{ "dial", "<number> [telephony]", cmd_dial, "Dial number",
ag_generator},
+ { "swap-calls", "[telephony]", cmd_swap_calls, "Swap calls",
+ ag_generator},
+ { "release-answer", "[telephony]", cmd_release_answer,
+ "Release and answer",
+ ag_generator},
+ { "release-swap", "[telephony]", cmd_release_swap, "Release and swap",
+ ag_generator},
+ { "hold-answer", "[telephony]", cmd_hold_answer, "Hold and answer",
+ ag_generator},
{ "hangup-all", "[telephony]", cmd_hangupall, "Hangup all calls",
ag_generator},
{ "send-tones", "<tones> [telephony]", cmd_send_tones, "Send tones",
--
2.43.0
^ permalink raw reply related [flat|nested] 14+ messages in thread* [PATCH BlueZ 11/12] shared/hfp: Add codecs support
2026-09-25 13:25 [PATCH BlueZ 00/12] hfp-hf: Enhance HFP Hands-Free profile support Frédéric Danis
` (9 preceding siblings ...)
2026-09-25 13:25 ` [PATCH BlueZ 10/12] client/telephony: " Frédéric Danis
@ 2026-09-25 13:25 ` Frédéric Danis
2026-09-25 13:25 ` [PATCH BlueZ 12/12] audio/hfp-hf: " Frédéric Danis
11 siblings, 0 replies; 14+ messages in thread
From: Frédéric Danis @ 2026-09-25 13:25 UTC (permalink / raw)
To: linux-bluetooth
From: Frédéric Danis <frederic.danis.oss@gmail.com>
This send the AT+BAC command with the supported codec list during
SLC connection.
On reception of +BCS event to select a codec to use it replies with
the same codec if it is available, or re-send the AT+BAC to re-trigger
codec selection.
---
src/shared/hfp.c | 164 +++++++++++++++++++++++++++++++++++++++++++++--
src/shared/hfp.h | 7 ++
2 files changed, 166 insertions(+), 5 deletions(-)
diff --git a/src/shared/hfp.c b/src/shared/hfp.c
index dffad6fee..cf12f0637 100644
--- a/src/shared/hfp.c
+++ b/src/shared/hfp.c
@@ -100,6 +100,7 @@ struct hfp_hf {
struct hfp_hf_callbacks *callbacks;
void *callbacks_data;
+ uint32_t hf_features;
uint32_t features;
struct indicator ag_ind[HFP_INDICATOR_LAST];
bool service;
@@ -2235,6 +2236,101 @@ static void clip_cb(struct hfp_context *context, void *user_data)
hfp->callbacks_data);
}
+static bool available_codecs_update(struct hfp_hf *hfp,
+ hfp_response_func_t resp_cb,
+ void *user_data)
+{
+ uint8_t codecs[UINT8_MAX];
+ uint8_t len;
+ uint32_t codecs_str_len;
+ char *codecs_str, *ptr;
+
+ len = hfp->callbacks->get_codecs(codecs, UINT8_MAX,
+ hfp->callbacks_data);
+ if (!len) {
+ DBG(hfp, "hf: Failed to get supported codecs");
+ return false;
+ }
+
+ /* Each codec can be up to 3 digits + comma + null terminator */
+ codecs_str_len = len * 4 + 1;
+ codecs_str = malloc(codecs_str_len);
+ if (!codecs_str) {
+ DBG(hfp, "hf: Failed to allocate memory for codecs");
+ return false;
+ }
+
+ ptr = codecs_str;
+ for (uint8_t i = 0; i < len; i++) {
+ int ret;
+
+ ret = snprintf(ptr, codecs_str_len - (ptr - codecs_str),
+ "%u,", codecs[i]);
+ if (ret < 0 || ret >= codecs_str_len - (ptr - codecs_str)) {
+ DBG(hfp, "hf: Failed to format codecs string");
+ free(codecs_str);
+ return false;
+ }
+ ptr += ret;
+ }
+ /* Remove the trailing comma */
+ if (ptr != codecs_str) {
+ ptr--;
+ *ptr = '\0';
+ }
+
+ if (!hfp_hf_send_command(hfp, resp_cb, user_data, "AT+BAC=%s",
+ codecs_str)) {
+ DBG(hfp, "hf: Could not send AT+BAC=%s", codecs_str);
+ free(codecs_str);
+ return false;
+ }
+
+ free(codecs_str);
+
+ return true;
+}
+
+static void bac_resp(enum hfp_result result, enum hfp_error cme_err,
+ void *user_data)
+{
+ struct hfp_hf *hfp = user_data;
+
+ if (result != HFP_RESULT_OK)
+ DBG(hfp, "hf: BAC error: %d", result);
+}
+
+static void bcs_resp(enum hfp_result result, enum hfp_error cme_err,
+ void *user_data)
+{
+ struct hfp_hf *hfp = user_data;
+
+ if (result != HFP_RESULT_OK)
+ DBG(hfp, "hf: BCS error: %d", result);
+}
+
+static void bcs_cb(struct hfp_context *context, void *user_data)
+{
+ struct hfp_hf *hfp = user_data;
+ unsigned int val;
+
+ if (!hfp_context_get_number(context, &val))
+ return;
+
+ if (!hfp->callbacks->select_codec(val, hfp->callbacks_data)) {
+ DBG(hfp, "hf: Codec selection failed: %d", val);
+
+ if (!available_codecs_update(hfp, bac_resp, hfp)) {
+ DBG(hfp, "hf: Could not re-trigger codec update");
+ return;
+ }
+ }
+
+ if (!hfp_hf_send_command(hfp, bcs_resp, hfp, "AT+BCS=%u", val)) {
+ DBG(hfp, "hf: Could not send AT+BCS=%u", val);
+ }
+}
+
static void nrec_resp(enum hfp_result result, enum hfp_error cme_err,
void *user_data)
{
@@ -2794,17 +2890,15 @@ static void slc_brsf_cb(struct hfp_context *context, void *user_data)
hfp->features = feat;
}
-static void slc_brsf_resp(enum hfp_result result, enum hfp_error cme_err,
+static void slc_bac_resp(enum hfp_result result, enum hfp_error cme_err,
void *user_data)
{
struct hfp_hf *hfp = user_data;
DBG(hfp, "");
- hfp_hf_unregister(hfp, "+BRSF");
-
if (result != HFP_RESULT_OK) {
- DBG(hfp, "BRSF error: %d", result);
+ DBG(hfp, "hf: BAC error: %d", result);
goto failed;
}
@@ -2829,6 +2923,46 @@ failed:
hfp->callbacks_data);
}
+static void slc_brsf_resp(enum hfp_result result, enum hfp_error cme_err,
+ void *user_data)
+{
+ struct hfp_hf *hfp = user_data;
+
+ DBG(hfp, "");
+
+ hfp_hf_unregister(hfp, "+BRSF");
+
+ if (result != HFP_RESULT_OK) {
+ DBG(hfp, "BRSF error: %d", result);
+ goto failed;
+ }
+
+ /* Continue with SLC creation */
+ if (!(hfp->hf_features & HFP_HF_FEAT_CODEC_NEGOTIATION) ||
+ !(hfp->features & HFP_AG_FEAT_CODEC_NEGOTIATION)) {
+ /* Jump to next setup state */
+ slc_bac_resp(HFP_RESULT_OK, cme_err, user_data);
+ return;
+ }
+
+ if (!hfp_hf_register(hfp, bcs_cb, "+BCS", hfp, NULL)) {
+ DBG(hfp, "hf: Could not register for +BCS");
+ result = HFP_RESULT_ERROR;
+ goto failed;
+ }
+
+ if (!available_codecs_update(hfp, slc_bac_resp, hfp)) {
+ DBG(hfp, "hf: Could not send AT+BAC");
+ result = HFP_RESULT_ERROR;
+ goto failed;
+ }
+
+failed:
+ if (hfp->callbacks->session_ready)
+ hfp->callbacks->session_ready(result, cme_err,
+ hfp->callbacks_data);
+}
+
bool hfp_hf_session_register(struct hfp_hf *hfp,
struct hfp_hf_callbacks *callbacks,
void *callbacks_data)
@@ -2849,11 +2983,15 @@ bool hfp_hf_session(struct hfp_hf *hfp)
DBG(hfp, "");
+ hfp->hf_features = HFP_HF_FEATURES;
+ if (hfp->callbacks->get_codecs && hfp->callbacks->select_codec)
+ hfp->hf_features |= HFP_HF_FEAT_CODEC_NEGOTIATION;
+
if (!hfp_hf_register(hfp, slc_brsf_cb, "+BRSF", hfp, NULL))
return false;
return hfp_hf_send_command(hfp, slc_brsf_resp, hfp,
- "AT+BRSF=%u", HFP_HF_FEATURES);
+ "AT+BRSF=%u", hfp->hf_features);
}
const char *hfp_hf_call_get_number(struct hfp_hf *hfp, uint id)
@@ -3090,3 +3228,19 @@ bool hfp_hf_call_hangup(struct hfp_hf *hfp, uint id,
return false;
}
+
+bool hfp_hf_request_codec_connection(struct hfp_hf *hfp,
+ hfp_response_func_t resp_cb,
+ void *user_data)
+{
+ if (!hfp)
+ return false;
+
+ if (!(hfp->hf_features & HFP_HF_FEAT_CODEC_NEGOTIATION) ||
+ !(hfp->features & HFP_AG_FEAT_CODEC_NEGOTIATION)) {
+ DBG(hfp, "hf: Codec negotiation not supported");
+ return false;
+ }
+
+ return hfp_hf_send_command(hfp, resp_cb, user_data, "AT+BCC");
+}
diff --git a/src/shared/hfp.h b/src/shared/hfp.h
index 11610018e..a3b956cf5 100644
--- a/src/shared/hfp.h
+++ b/src/shared/hfp.h
@@ -220,6 +220,9 @@ struct hfp_hf_callbacks {
void (*call_line_id_updated)(uint id, const char *number, uint type,
void *user_data);
void (*call_mpty_updated)(uint id, bool mpty, void *user_data);
+ uint8_t (*get_codecs)(uint8_t *codecs, uint8_t max_codecs,
+ void *user_data);
+ bool (*select_codec)(uint8_t codec, void *user_data);
};
struct hfp_hf *hfp_hf_new(int fd);
@@ -270,3 +273,7 @@ bool hfp_hf_call_answer(struct hfp_hf *hfp, uint id,
bool hfp_hf_call_hangup(struct hfp_hf *hfp, uint id,
hfp_response_func_t resp_cb,
void *user_data);
+
+bool hfp_hf_request_codec_connection(struct hfp_hf *hfp,
+ hfp_response_func_t resp_cb,
+ void *user_data);
--
2.43.0
^ permalink raw reply related [flat|nested] 14+ messages in thread* [PATCH BlueZ 12/12] audio/hfp-hf: Add codecs support
2026-09-25 13:25 [PATCH BlueZ 00/12] hfp-hf: Enhance HFP Hands-Free profile support Frédéric Danis
` (10 preceding siblings ...)
2026-09-25 13:25 ` [PATCH BlueZ 11/12] shared/hfp: Add codecs support Frédéric Danis
@ 2026-09-25 13:25 ` Frédéric Danis
11 siblings, 0 replies; 14+ messages in thread
From: Frédéric Danis @ 2026-09-25 13:25 UTC (permalink / raw)
To: linux-bluetooth
From: Frédéric Danis <frederic.danis.oss@gmail.com>
---
profiles/audio/hfp-hf.c | 171 +++++++++++++++++++++++++++++++++++-----
1 file changed, 152 insertions(+), 19 deletions(-)
diff --git a/profiles/audio/hfp-hf.c b/profiles/audio/hfp-hf.c
index 25dce517f..4e626653d 100644
--- a/profiles/audio/hfp-hf.c
+++ b/profiles/audio/hfp-hf.c
@@ -65,11 +65,19 @@
#define HFP_HF_SDP_FEATURES (HFP_HF_SDP_ECNR | HFP_HF_SDP_3WAY |\
HFP_HF_SDP_CLIP |\
- HFP_HF_SDP_REMOTE_VOLUME_CONTROL)
+ HFP_HF_SDP_REMOTE_VOLUME_CONTROL |\
+ HFP_HF_SDP_WIDE_BAND_SPEECH |\
+ HFP_HF_SDP_SUPER_WIDE_BAND_SPEECH)
#define URI "tel"
#define URI_PREFIX URI ":"
+struct connect_data {
+ struct hfp_device *dev;
+ void (*cb)(int status, void *data);
+ void *cb_user_data;
+};
+
struct hfp_device {
struct telephony *telephony;
uint16_t version;
@@ -81,6 +89,7 @@ struct hfp_device {
struct queue *calls;
uint8_t codec;
unsigned int resume_id;
+ struct connect_data *pending_connect;
hfp_hf_sco_closed_cb sco_closed_cb;
void *sco_closed_data;
};
@@ -169,6 +178,20 @@ static bool call_id_cmp(const void *data, const void *match_data)
return call->idx == id;
}
+static void sco_connect_complete(struct connect_data *connect_data, int status)
+{
+ if (!connect_data)
+ return;
+
+ if (connect_data->dev->pending_connect == connect_data)
+ connect_data->dev->pending_connect = NULL;
+
+ if (connect_data->cb)
+ connect_data->cb(status, connect_data->cb_user_data);
+
+ g_free(connect_data);
+}
+
static void device_destroy(struct hfp_device *dev)
{
struct hfp_server *server;
@@ -177,6 +200,9 @@ static void device_destroy(struct hfp_device *dev)
telephony_set_state(dev->telephony, DISCONNECTING);
+ if (dev->pending_connect)
+ sco_connect_complete(dev->pending_connect, -ENOTCONN);
+
if (dev->hf) {
hfp_hf_unref(dev->hf);
dev->hf = NULL;
@@ -329,6 +355,49 @@ static void hfp_hf_call_line_id_updated(uint id, const char *number,
telephony_call_set_line_id(call, number);
}
+static uint8_t hfp_hf_get_codecs(uint8_t *codecs, uint8_t max_codecs,
+ void *user_data)
+{
+ struct hfp_device *dev = user_data;
+ struct btd_adapter *adapter;
+ struct hfp_server *server;
+ GSList *l;
+ uint8_t i = 0;
+
+ adapter = device_get_adapter(telephony_get_device(dev->telephony));
+ server = find_server(servers, adapter);
+
+ for (l = server->endpoints; l; l = l->next) {
+ codecs[i++] = media_endpoint_get_codec(l->data);
+ if (i >= max_codecs)
+ break;
+ }
+
+ return i;
+}
+
+static bool hfp_hf_select_codec(uint8_t codec, void *user_data)
+{
+ struct hfp_device *dev = user_data;
+ struct btd_adapter *adapter;
+ struct hfp_server *server;
+ GSList *l;
+
+ DBG("Selecting codec: %u", codec);
+ adapter = device_get_adapter(telephony_get_device(dev->telephony));
+ server = find_server(servers, adapter);
+
+ for (l = server->endpoints; l; l = l->next) {
+ if (media_endpoint_get_codec(l->data) == codec) {
+ dev->codec = codec;
+ return true;
+ }
+ }
+
+ DBG("Unsupported codec: %u", codec);
+ return false;
+}
+
static struct hfp_hf_callbacks hf_session_callbacks = {
.session_ready = hfp_hf_session_ready_cb,
.update_indicator = hfp_hf_update_indicator,
@@ -338,6 +407,8 @@ static struct hfp_hf_callbacks hf_session_callbacks = {
.call_removed = hfp_hf_call_removed,
.call_status_updated = hfp_hf_call_status_updated,
.call_line_id_updated = hfp_hf_call_line_id_updated,
+ .get_codecs = hfp_hf_get_codecs,
+ .select_codec = hfp_hf_select_codec,
};
static void hfp_disconnect_watch(void *user_data)
@@ -991,23 +1062,6 @@ static gboolean sco_io_cb(GIOChannel *chan, GIOCondition cond, void *data)
return FALSE;
}
-struct connect_data {
- struct hfp_device *dev;
- void (*cb)(int status, void *data);
- void *cb_user_data;
-};
-
-static void sco_connect_complete(struct connect_data *connect_data, int status)
-{
- if (!connect_data || !connect_data->cb)
- goto done;
-
- connect_data->cb(status, connect_data->cb_user_data);
-
-done:
- g_free(connect_data);
-}
-
static void sco_connect_cb(GIOChannel *io, GError *err, gpointer user_data)
{
struct connect_data *connect_data = user_data;
@@ -1109,6 +1163,62 @@ static void sco_connect_cb(GIOChannel *io, GError *err, gpointer user_data)
sco_connect_complete(connect_data, 0);
}
+static void confirm_cb(GIOChannel *io, gpointer user_data)
+{
+ bdaddr_t src, dst;
+ char address[18];
+ struct btd_device *device;
+ struct btd_service *service;
+ struct hfp_device *dev = NULL;
+ int voice;
+ GError *err = NULL;
+
+ if (!bt_io_get(io, &err,
+ BT_IO_OPT_SOURCE_BDADDR, &src,
+ BT_IO_OPT_DEST_BDADDR, &dst,
+ BT_IO_OPT_DEST, address,
+ BT_IO_OPT_INVALID)) {
+ error("Unable to get destination address: %s", err->message);
+ g_clear_error(&err);
+ goto drop;
+ }
+
+ DBG("Incoming SCO connection from %s", address);
+
+ device = btd_adapter_find_device(adapter_find(&src), &dst,
+ BDADDR_BREDR);
+ if (!device)
+ goto drop;
+
+ service = btd_device_get_service(device, HFP_AG_UUID);
+ if (!service)
+ goto drop;
+
+ dev = btd_service_get_user_data(service);
+ voice = dev->codec == 1 ? BT_VOICE_CVSD_16BIT : BT_VOICE_TRANSPARENT;
+
+ if (!bt_io_set(io, &err, BT_IO_OPT_VOICE, voice, BT_IO_OPT_INVALID)) {
+ error("Could not set voice settings on SCO IO: %s",
+ err->message);
+ g_error_free(err);
+ goto drop;
+ }
+
+ if (!bt_io_accept(io, sco_connect_cb, dev->pending_connect, NULL,
+ &err)) {
+ error("bt_io_accept() failed: %s", err->message);
+ g_error_free(err);
+ goto drop;
+ }
+
+ return;
+
+drop:
+ if (dev && dev->pending_connect)
+ sco_connect_complete(dev->pending_connect, -EIO);
+ g_io_channel_shutdown(io, TRUE, NULL);
+}
+
bool hfp_hf_sco_listen(struct btd_adapter *adapter, void *endpoint)
{
struct hfp_server *server;
@@ -1126,7 +1236,7 @@ bool hfp_hf_sco_listen(struct btd_adapter *adapter, void *endpoint)
if (server->sco_io)
return true;
- server->sco_io = bt_io_listen(sco_connect_cb, NULL, NULL, NULL,
+ server->sco_io = bt_io_listen(NULL, confirm_cb, NULL, NULL,
&err,
BT_IO_OPT_SOURCE_BDADDR,
btd_adapter_get_address(server->adapter),
@@ -1190,6 +1300,16 @@ uint16_t hfp_hf_device_get_omtu(struct hfp_device *dev)
return dev->omtu;
}
+static void req_codec_connection_complete(enum hfp_result res,
+ enum hfp_error cme_err,
+ void *user_data)
+{
+ struct connect_data *connect_data = user_data;
+
+ if (res != HFP_RESULT_OK)
+ sco_connect_complete(connect_data, -EIO);
+}
+
static gboolean sco_start_cb(gpointer data)
{
struct connect_data *connect_data = data;
@@ -1211,6 +1331,11 @@ unsigned int hfp_hf_sco_start(struct hfp_device *dev, void *cb, void *user_data)
DBG("codec %u", dev->codec);
+ if (dev->pending_connect) {
+ error("SCO connect already in progress");
+ return 0;
+ }
+
connect_data = g_new0(struct connect_data, 1);
connect_data->dev = dev;
connect_data->cb = cb;
@@ -1219,6 +1344,13 @@ unsigned int hfp_hf_sco_start(struct hfp_device *dev, void *cb, void *user_data)
src = telephony_get_src(dev->telephony);
dst = telephony_get_dst(dev->telephony);
if (!dev->sco_io) {
+ if (hfp_hf_request_codec_connection(dev->hf,
+ req_codec_connection_complete,
+ connect_data)) {
+ dev->pending_connect = connect_data;
+ goto done;
+ }
+
io = bt_io_connect(sco_connect_cb, connect_data, NULL, &err,
BT_IO_OPT_SOURCE_BDADDR, &src,
BT_IO_OPT_DEST_BDADDR, &dst,
@@ -1234,6 +1366,7 @@ unsigned int hfp_hf_sco_start(struct hfp_device *dev, void *cb, void *user_data)
g_idle_add(sco_start_cb, connect_data);
}
+done:
return (++dev->resume_id);
}
--
2.43.0
^ permalink raw reply related [flat|nested] 14+ messages in thread