* [RFC BlueZ 01/10] doc: Add new telephony related profiles interfaces
2025-05-28 8:59 [RFC BlueZ 00/10] New Telephony interface for HSP, HFP and CCP Frédéric Danis
@ 2025-05-28 8:59 ` Frédéric Danis
2025-05-28 10:33 ` New Telephony interface for HSP, HFP and CCP bluez.test.bot
2025-05-28 8:59 ` [RFC BlueZ 02/10] audio/telephony: Add shared interfaces implementation Frédéric Danis
` (8 subsequent siblings)
9 siblings, 1 reply; 13+ messages in thread
From: Frédéric Danis @ 2025-05-28 8:59 UTC (permalink / raw)
To: linux-bluetooth
These are interfaces are meant to be generic to the telephony related
"headset" profiles like HSP HS, HFP HF, and CCP.
---
Makefile.am | 4 +
doc/org.bluez.TelephonyAg.rst | 200 ++++++++++++++++++++++++++++++++
doc/org.bluez.TelephonyCall.rst | 144 +++++++++++++++++++++++
3 files changed, 348 insertions(+)
create mode 100644 doc/org.bluez.TelephonyAg.rst
create mode 100644 doc/org.bluez.TelephonyCall.rst
diff --git a/Makefile.am b/Makefile.am
index 0f0037d5b..c53970d17 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -388,6 +388,7 @@ man_MANS += doc/org.bluez.obex.Client.5 doc/org.bluez.obex.Session.5 \
doc/org.bluez.obex.Message.5 \
doc/org.bluez.obex.AgentManager.5 doc/org.bluez.obex.Agent.5 \
doc/org.bluez.obex.Image.5
+man_MANS += doc/org.bluez.TelephonyAg.5 doc/org.bluez.TelephonyCall.5
endif
manual_pages += src/bluetoothd.8
manual_pages += doc/hci.7 doc/mgmt.7 doc/l2cap.7 doc/rfcomm.7 doc/sco.7
@@ -422,6 +423,7 @@ manual_pages += doc/org.bluez.obex.Client.5 doc/org.bluez.obex.Session.5 \
doc/org.bluez.obex.Message.5 \
doc/org.bluez.obex.AgentManager.5 doc/org.bluez.obex.Agent.5 \
doc/org.bluez.obex.Image.5
+manual_pages += doc/org.bluez.TelephonyAg.5 doc/org.bluez.TelephonyCall.5
EXTRA_DIST += src/genbuiltin src/bluetooth.conf \
src/main.conf profiles/network/network.conf \
@@ -506,6 +508,8 @@ EXTRA_DIST += doc/org.bluez.obex.Client.rst doc/org.bluez.obex.Session.rst \
doc/org.bluez.obex.AgentManager.rst doc/org.bluez.obex.Agent.rst \
doc/org.bluez.obex.Image.rst
+EXTRA_DIST += doc/org.bluez.TelephonyAg.rst doc/org.bluez.TelephonyCall.rst
+
EXTRA_DIST += doc/pics-opp.txt doc/pixit-opp.txt \
doc/pts-opp.txt
diff --git a/doc/org.bluez.TelephonyAg.rst b/doc/org.bluez.TelephonyAg.rst
new file mode 100644
index 000000000..e0a78a6a1
--- /dev/null
+++ b/doc/org.bluez.TelephonyAg.rst
@@ -0,0 +1,200 @@
+======================
+org.bluez.TelephonyAg1
+======================
+
+-----------------------------------------------------
+BlueZ D-Bus Telephony Audio Gateway API documentation
+-----------------------------------------------------
+
+:Version: BlueZ
+:Date: May 2025
+:Manual section: 5
+:Manual group: Linux System Administration
+
+Interface
+=========
+
+:Service: org.bluez
+:Interface: org.bluez.TelephonyAg1
+:Object path: [variable prefix]/{hci0,hci1,...}/dev_XX_XX_XX_XX_XX_XX/{ccp,hfp,hsp}
+
+Methods
+-------
+
+object Dial(string number)
+``````````````````````````
+
+ Call number, if number is void try to call last dialed number.
+ Initiates a new outgoing call. Returns the object path to the newly
+ created call.
+
+ The number must be a string containing the following characters:
+ `[0-9+*#,ABCD]{1,80}` The character set can contain numbers, `+`, `*`,
+ `#`, `,` and the letters `A` to `D`. Besides this sanity checking no
+ further number validation is performed. It is assumed that the gateway
+ and/or the network will perform further validation.
+
+ If number is an empty string, it will try to call last dialed number.
+
+ NOTE: If an active call (single or multiparty) exists, then it is
+ automatically put on hold if the dial procedure is successful.
+
+ Possible Errors:
+
+ :org.bluez.Error.InvalidState:
+ :org.bluez.Error.InvalidArguments:
+ :org.bluez.Error.NotSupported:
+ :org.bluez.Error.Failed:
+
+void SwapCalls()
+````````````````
+
+ Swaps Active and Held calls. The effect of this is that all calls (0 or
+ more including calls in a multi-party conversation) that were Active
+ are now Held, and all calls (0 or more) that were Held are now Active.
+
+ GSM specification does not allow calls to be swapped in the case where
+ Held, Active and Waiting calls exist. Some modems implement this
+ anyway, thus it is manufacturer specific whether this method will
+ succeed in the case of Held, Active and Waiting calls.
+
+ Possible Errors:
+ :org.bluez.Error.InvalidState
+ :org.bluez.Error.Failed
+
+void ReleaseAndAnswer()
+```````````````````````
+
+ Releases currently active call (0 or more) and answers the currently
+ waiting call. Please note that if the current call is a multiparty
+ call, then all parties in the multi-party call will be released.
+
+ Possible Errors:
+ :org.bluez.Error.InvalidState
+ :org.bluez.Error.Failed
+
+void ReleaseAndSwap()
+`````````````````````
+
+ Releases currently active call (0 or more) and activates any currently
+ held calls. Please note that if the current call is a multiparty call,
+ then all parties in the multi-party call will be released.
+
+ Possible Errors:
+ :org.bluez.Error.InvalidState
+ :org.bluez.Error.Failed
+
+void HoldAndAnswer()
+````````````````````
+
+ Puts the current call (including multi-party calls) on hold and answers
+ the currently waiting call. Calling this function when a user already
+ has a both Active and Held calls is invalid, since in GSM a user can
+ have only a single Held call at a time.
+
+ Possible Errors:
+ :org.bluez.Error.InvalidState
+ :org.bluez.Error.Failed
+
+void HangupAll()
+````````````````
+
+ Releases all calls except waiting calls. This includes multiparty
+ calls.
+
+ Possible Errors:
+ :org.bluez.Error.InvalidState
+ :org.bluez.Error.Failed
+
+void HangupActive()
+```````````````````
+
+ Releases active calls. This includes multiparty active calls.
+
+ Possible Errors:
+ :org.bluez.Error.InvalidState
+ :org.bluez.Error.Failed
+
+void HangupHeld()
+`````````````````
+
+ Releases held calls except waiting calls. This includes multiparty
+ held calls.
+
+ Possible Errors:
+ :org.bluez.Error.InvalidState
+ :org.bluez.Error.Failed
+
+array{object} CreateMultiparty()
+````````````````````````````````
+
+ Joins active and held calls together into a multi-party call. If one of
+ the calls is already a multi-party call, then the other call is added
+ to the multiparty conversation. Returns the new list of calls
+ participating in the multiparty call.
+
+ There can only be one subscriber controlled multi-party call according
+ to the GSM specification.
+
+ Possible Errors:
+ :org.bluez.Error.InvalidState
+ :org.bluez.Error.Failed
+
+void SendTones(string tones)
+````````````````````````````
+
+ Sends the DTMF tones to the network. The tones have a fixed duration.
+ Tones can be one of: '0' - '9', '*', '#', 'A', 'B', 'C', 'D'. The last
+ four are typically not used in normal circumstances.
+
+ Possible Errors:
+ :org.bluez.Error.InvalidState
+ :org.bluez.Error.InvalidArgs
+ :org.bluez.Error.Failed
+
+Properties
+----------
+
+string State [readonly]
+```````````````````````
+
+ Contains the state of the current connection.
+
+ Possible values:
+
+ :"connecting":
+
+ RFComm connection in progress
+
+ :"slc_connecting":
+
+ Service Level Connection in progress
+
+ :"connected":
+
+ RFComm and Service Level Connection are connected
+
+ :"disconnecting":
+
+ No further use of this object is allowed, it will be destroyed
+ shortly
+
+boolean Service [readonly]
+``````````````````````````
+
+ Network service availability.
+
+byte Signal [readonly]
+``````````````````````
+
+ Network level signal from 0 to 5.
+
+boolean Roaming [readonly]
+``````````````````````````
+
+ Network roaming usage.
+
+byte BattChg [readonly]
+```````````````````````
+
+ Battery level from 0 to 5.
diff --git a/doc/org.bluez.TelephonyCall.rst b/doc/org.bluez.TelephonyCall.rst
new file mode 100644
index 000000000..63f862378
--- /dev/null
+++ b/doc/org.bluez.TelephonyCall.rst
@@ -0,0 +1,144 @@
+========================
+org.bluez.TelephonyCall1
+========================
+
+--------------------------------------------
+BlueZ D-Bus Telephony Call API documentation
+--------------------------------------------
+
+:Version: BlueZ
+:Date: May 2025
+:Manual section: 5
+:Manual group: Linux System Administration
+
+Interface
+=========
+
+:Service: org.bluez
+:Interface: org.bluez.TelephonyCall1
+:Object path: [variable prefix]/{hci0,hci1,...}/dev_XX_XX_XX_XX_XX_XX/{ccp,hfp,hsp}/callX
+
+Methods
+-------
+
+void Answer()
+`````````````
+
+ Answers an incoming call. Only valid if the state of the call is
+ "incoming".
+
+ Possible Errors:
+ :org.bluez.Error.InvalidState
+ :org.bluez.Error.Failed
+
+void Hangup()
+`````````````
+
+ Hangs up the call.
+
+ For an incoming call, the call is hung up using ATH or equivalent. For
+ a waiting call, the remote party is notified by using the User
+ Determined User Busy (UDUB) condition. This is generally implemented
+ using CHLD=0.
+
+ Please note that the GSM specification does not allow the release of a
+ held call when a waiting call exists. This is because 27.007 allows
+ CHLD=1X to operate only on active calls. Hence a held call cannot be
+ hung up without affecting the state of the incoming call (e.g. using
+ other CHLD alternatives). Most manufacturers provide vendor extensions
+ that do allow the state of the held call to be modified using CHLD=1X
+ or equivalent. It should be noted that Bluetooth HFP specifies the
+ classic 27.007 behavior and does not allow CHLD=1X to modify the state
+ of held calls.
+
+ Based on the discussion above, it should also be noted that releasing a
+ particular party of a held multiparty call might not be possible on
+ some implementations. It is recommended for the applications to
+ structure their UI accordingly.
+
+ NOTE: Releasing active calls does not produce side-effects. That is the
+ state of held or waiting calls is not affected. As an exception, in the
+ case where a single active call and a waiting call are present,
+ releasing the active call will result in the waiting call transitioning
+ to the 'incoming' state.
+
+ Possible Errors:
+ :org.bluez.Error.InvalidState
+ :org.bluez.Error.Failed
+
+Properties
+----------
+
+string LineIdentification [readonly]
+````````````````````````````````````
+
+ Contains the Line Identification information returned by the network,
+ if present. For incoming calls this is effectively the CLIP. For
+ outgoing calls this attribute will hold the dialed number, or the COLP
+ if received by the audio gateway.
+
+ Please note that COLP may be different from the dialed number. A
+ special "withheld" value means the remote party refused to provide
+ caller ID and the "override category" option was not provisioned for
+ the current subscriber.
+
+string IncomingLine [readonly, optional]
+````````````````````````````````````````
+
+ Contains the Called Line Identification information returned by the
+ network.
+ This is only available for incoming calls and indicates the local
+ subscriber number which was dialed by the remote party. This is useful
+ for subscribers which have a multiple line service with their network
+ provider and would like to know what line the call is coming in on.
+
+string Name [readonly]
+``````````````````````
+
+ Contains the Name Identification information returned by the network,
+ if present.
+
+boolean Multiparty [readonly]
+`````````````````````````````
+
+ Contains the indication if the call is part of a multiparty call or
+ not.
+
+ Notifications if a call becomes part or leaves a multiparty call are
+ sent.
+
+string State [readonly]
+```````````````````````
+
+ Contains the state of the current call.
+
+ Possible values:
+
+ :"active":
+
+ The call is active
+
+ :"held":
+
+ The call is on hold
+
+ :"dialing":
+
+ The call is being dialed
+
+ :"alerting":
+
+ The remote party is being alerted
+
+ :"incoming":
+
+ Incoming call in progress
+
+ :"waiting":
+
+ Call is waiting
+
+ :"disconnected":
+
+ No further use of this object is allowed, it will be
+ destroyed shortly
--
2.43.0
^ permalink raw reply related [flat|nested] 13+ messages in thread* RE: New Telephony interface for HSP, HFP and CCP
2025-05-28 8:59 ` [RFC BlueZ 01/10] doc: Add new telephony related profiles interfaces Frédéric Danis
@ 2025-05-28 10:33 ` bluez.test.bot
0 siblings, 0 replies; 13+ messages in thread
From: bluez.test.bot @ 2025-05-28 10:33 UTC (permalink / raw)
To: linux-bluetooth, frederic.danis
[-- Attachment #1: Type: text/plain, Size: 1532 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=966926
---Test result---
Test Summary:
CheckPatch PENDING 0.26 seconds
GitLint PENDING 0.30 seconds
BuildEll PASS 20.18 seconds
BluezMake PASS 2624.40 seconds
MakeCheck PASS 20.41 seconds
MakeDistcheck FAIL 8.76 seconds
CheckValgrind PASS 275.83 seconds
CheckSmatch PASS 302.61 seconds
bluezmakeextell PASS 128.49 seconds
IncrementalBuild PENDING 0.26 seconds
ScanBuild PASS 897.49 seconds
Details
##############################
Test: CheckPatch - PENDING
Desc: Run checkpatch.pl script
Output:
##############################
Test: GitLint - PENDING
Desc: Run gitlint
Output:
##############################
Test: MakeDistcheck - FAIL
Desc: Run Bluez Make Distcheck
Output:
make[2]: *** No rule to make target 'doc/mgmt-api.txt', needed by 'distdir-am'. Stop.
make[1]: *** [Makefile:12236: distdir] Error 2
make: *** [Makefile:12312: dist] Error 2
##############################
Test: IncrementalBuild - PENDING
Desc: Incremental build with the patches in the series
Output:
---
Regards,
Linux Bluetooth
^ permalink raw reply [flat|nested] 13+ messages in thread
* [RFC BlueZ 02/10] audio/telephony: Add shared interfaces implementation
2025-05-28 8:59 [RFC BlueZ 00/10] New Telephony interface for HSP, HFP and CCP Frédéric Danis
2025-05-28 8:59 ` [RFC BlueZ 01/10] doc: Add new telephony related profiles interfaces Frédéric Danis
@ 2025-05-28 8:59 ` Frédéric Danis
2025-05-28 8:59 ` [RFC BlueZ 03/10] audio/telephony: Add skeleton for HFP profile Frédéric Danis
` (7 subsequent siblings)
9 siblings, 0 replies; 13+ messages in thread
From: Frédéric Danis @ 2025-05-28 8:59 UTC (permalink / raw)
To: linux-bluetooth
---
profiles/audio/telephony.c | 713 +++++++++++++++++++++++++++++++++++++
profiles/audio/telephony.h | 110 ++++++
2 files changed, 823 insertions(+)
create mode 100644 profiles/audio/telephony.c
create mode 100644 profiles/audio/telephony.h
diff --git a/profiles/audio/telephony.c b/profiles/audio/telephony.c
new file mode 100644
index 000000000..34f690c99
--- /dev/null
+++ b/profiles/audio/telephony.c
@@ -0,0 +1,713 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ *
+ * BlueZ - Bluetooth protocol stack for Linux
+ *
+ * Copyright © 2025 Collabora Ltd.
+ *
+ *
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#define _GNU_SOURCE
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <errno.h>
+#include <limits.h>
+
+#include <stdint.h>
+
+#include <glib.h>
+
+#include "lib/bluetooth.h"
+#include "bluetooth/sdp.h"
+#include "bluetooth/sdp_lib.h"
+#include "lib/uuid.h"
+
+#include "gdbus/gdbus.h"
+
+#include "btio/btio.h"
+#include "src/adapter.h"
+#include "src/btd.h"
+#include "src/dbus-common.h"
+#include "src/device.h"
+#include "src/error.h"
+#include "src/log.h"
+#include "src/plugin.h"
+#include "src/profile.h"
+#include "src/service.h"
+#include "src/shared/hfp.h"
+
+#include "telephony.h"
+
+#define TELEPHONY_AG_INTERFACE "org.bluez.TelephonyAg1"
+#define TELEPHONY_CALL_INTERFACE "org.bluez.TelephonyCall1"
+
+struct telephony {
+ struct btd_service *service;
+ struct btd_device *device;
+ char *path;
+ uint8_t id;
+ bdaddr_t src;
+ bdaddr_t dst;
+ void *profile_data;
+ struct telephony_callbacks *cbs;
+ enum connection_state state;
+ bool network_service;
+ uint8_t signal;
+ bool roaming;
+ uint8_t battchg;
+};
+
+static const char *state_to_string(enum connection_state state)
+{
+ switch (state) {
+ case CONNECTING:
+ return "connecting";
+ case SLC_CONNECTING:
+ return "slc_connecting";
+ case CONNECTED:
+ return "connected";
+ case DISCONNECTING:
+ return "disconnecting";
+ }
+
+ return NULL;
+}
+
+static const char *call_state_to_string(enum call_state state)
+{
+ switch (state) {
+ case CALL_STATE_ACTIVE:
+ return "active";
+ case CALL_STATE_HELD:
+ return "held";
+ case CALL_STATE_DIALING:
+ return "dialing";
+ case CALL_STATE_ALERTING:
+ return "alerting";
+ case CALL_STATE_INCOMING:
+ return "incoming";
+ case CALL_STATE_WAITING:
+ return "waiting";
+ case CALL_STATE_DISCONNECTED:
+ return "disconnected";
+ }
+
+ return NULL;
+}
+
+struct telephony *telephony_new(struct btd_service *service,
+ void *profile_data,
+ struct telephony_callbacks *cbs)
+{
+ struct btd_device *device = btd_service_get_device(service);
+ const char *path = device_get_path(device);
+ struct btd_adapter *adapter = device_get_adapter(device);
+ struct btd_profile *p = btd_service_get_profile(service);
+ struct telephony *ag;
+
+ ag = g_new0(struct telephony, 1);
+ bacpy(&ag->src, btd_adapter_get_address(adapter));
+ bacpy(&ag->dst, device_get_address(device));
+ ag->service = btd_service_ref(service);
+ ag->device = btd_device_ref(device);
+ ag->path = g_strdup_printf("%s/%s", path, p->name);
+ ag->profile_data = profile_data;
+ ag->cbs = cbs;
+
+ return ag;
+}
+
+void telephony_free(struct telephony *telephony)
+{
+ g_free(telephony->path);
+ g_free(telephony);
+}
+
+static DBusMessage *dial(DBusConnection *conn, DBusMessage *msg,
+ void *user_data)
+{
+ struct telephony *telephony = user_data;
+
+ if (telephony->cbs && telephony->cbs->dial)
+ return telephony->cbs->dial(conn, msg,
+ telephony->profile_data);
+
+ return btd_error_not_supported(msg);
+}
+
+static DBusMessage *swap_calls(DBusConnection *conn, DBusMessage *msg,
+ void *user_data)
+{
+ struct telephony *telephony = user_data;
+
+ if (telephony->cbs && telephony->cbs->swap_calls)
+ return telephony->cbs->swap_calls(conn, msg,
+ telephony->profile_data);
+
+ return btd_error_not_supported(msg);
+}
+
+static DBusMessage *release_and_answer(DBusConnection *conn, DBusMessage *msg,
+ void *user_data)
+{
+ struct telephony *telephony = user_data;
+
+ if (telephony->cbs && telephony->cbs->release_and_answer)
+ return telephony->cbs->release_and_answer(conn, msg,
+ telephony->profile_data);
+
+ return btd_error_not_supported(msg);
+}
+
+static DBusMessage *release_and_swap(DBusConnection *conn, DBusMessage *msg,
+ void *user_data)
+{
+ struct telephony *telephony = user_data;
+
+ if (telephony->cbs && telephony->cbs->release_and_swap)
+ return telephony->cbs->release_and_swap(conn, msg,
+ telephony->profile_data);
+
+ return btd_error_not_supported(msg);
+}
+
+static DBusMessage *hold_and_answer(DBusConnection *conn, DBusMessage *msg,
+ void *user_data)
+{
+ struct telephony *telephony = user_data;
+
+ if (telephony->cbs && telephony->cbs->hold_and_answer)
+ return telephony->cbs->hold_and_answer(conn, msg,
+ telephony->profile_data);
+
+ return btd_error_not_supported(msg);
+}
+
+static DBusMessage *hangup_all(DBusConnection *conn, DBusMessage *msg,
+ void *user_data)
+{
+ struct telephony *telephony = user_data;
+
+ if (telephony->cbs && telephony->cbs->hangup_all)
+ return telephony->cbs->hangup_all(conn, msg,
+ telephony->profile_data);
+
+ return btd_error_not_supported(msg);
+}
+
+static DBusMessage *create_multiparty(DBusConnection *conn, DBusMessage *msg,
+ void *user_data)
+{
+ struct telephony *telephony = user_data;
+
+ if (telephony->cbs && telephony->cbs->create_multiparty)
+ return telephony->cbs->create_multiparty(conn, msg,
+ telephony->profile_data);
+
+ return btd_error_not_supported(msg);
+}
+
+static DBusMessage *send_tones(DBusConnection *conn, DBusMessage *msg,
+ void *user_data)
+{
+ struct telephony *telephony = user_data;
+
+ if (telephony->cbs && telephony->cbs->send_tones)
+ return telephony->cbs->send_tones(conn, msg,
+ telephony->profile_data);
+
+ return btd_error_not_supported(msg);
+}
+
+static gboolean property_get_state(const GDBusPropertyTable *property,
+ DBusMessageIter *iter,
+ void *user_data)
+{
+ struct telephony *telephony = user_data;
+ const char *string;
+
+ string = state_to_string(telephony->state);
+
+ dbus_message_iter_append_basic(iter, DBUS_TYPE_STRING, &string);
+
+ return TRUE;
+}
+
+static gboolean property_get_service(const GDBusPropertyTable *property,
+ DBusMessageIter *iter,
+ void *user_data)
+{
+ struct telephony *telephony = user_data;
+ dbus_bool_t value;
+
+ value = telephony->network_service;
+
+ dbus_message_iter_append_basic(iter, DBUS_TYPE_BOOLEAN, &value);
+
+ return TRUE;
+}
+
+static gboolean property_get_signal(const GDBusPropertyTable *property,
+ DBusMessageIter *iter,
+ void *user_data)
+{
+ struct telephony *telephony = user_data;
+
+ dbus_message_iter_append_basic(iter, DBUS_TYPE_BYTE,
+ &telephony->signal);
+
+ return TRUE;
+}
+
+static gboolean property_get_roaming(const GDBusPropertyTable *property,
+ DBusMessageIter *iter,
+ void *user_data)
+{
+ struct telephony *telephony = user_data;
+ dbus_bool_t value;
+
+ value = telephony->roaming;
+
+ dbus_message_iter_append_basic(iter, DBUS_TYPE_BOOLEAN, &value);
+
+ return TRUE;
+}
+
+static gboolean property_get_battchg(const GDBusPropertyTable *property,
+ DBusMessageIter *iter,
+ void *user_data)
+{
+ struct telephony *telephony = user_data;
+
+ dbus_message_iter_append_basic(iter, DBUS_TYPE_BYTE,
+ &telephony->battchg);
+
+ return TRUE;
+}
+
+static const GDBusMethodTable telephony_methods[] = {
+ { GDBUS_ASYNC_METHOD("Dial", GDBUS_ARGS({"number", "s"}), NULL,
+ dial) },
+ { GDBUS_ASYNC_METHOD("SwapCalls", NULL, NULL, swap_calls) },
+ { GDBUS_ASYNC_METHOD("ReleaseAndAnswer", NULL, NULL,
+ release_and_answer) },
+ { GDBUS_ASYNC_METHOD("ReleaseAndSwap", NULL, NULL,
+ release_and_swap) },
+ { GDBUS_ASYNC_METHOD("HoldAndAnswer", NULL, NULL,
+ hold_and_answer) },
+ { GDBUS_ASYNC_METHOD("HangupAll", NULL, NULL, hangup_all) },
+ { GDBUS_ASYNC_METHOD("CreateMultiparty", NULL,
+ GDBUS_ARGS({ "calls", "ao" }),
+ create_multiparty) },
+ { GDBUS_ASYNC_METHOD("SendTones", GDBUS_ARGS({"number", "s"}), NULL,
+ send_tones) },
+ { }
+};
+
+static const GDBusPropertyTable telephony_properties[] = {
+ { "State", "s", property_get_state },
+ { "Service", "b", property_get_service },
+ { "Signal", "y", property_get_signal },
+ { "Roaming", "b", property_get_roaming },
+ { "BattChg", "y", property_get_battchg },
+ { }
+};
+
+static void path_unregister(void *data)
+{
+ struct telephony *telephony = data;
+
+ DBG("Unregistered interface %s on path %s", TELEPHONY_AG_INTERFACE,
+ telephony->path);
+}
+
+int telephony_register_interface(struct telephony *telephony)
+{
+ if (telephony->cbs == NULL)
+ return -EINVAL;
+
+ if (!g_dbus_register_interface(btd_get_dbus_connection(),
+ telephony->path,
+ TELEPHONY_AG_INTERFACE,
+ telephony_methods, NULL,
+ telephony_properties, telephony,
+ path_unregister)) {
+ return -EINVAL;
+ }
+
+ DBG("Registered interface %s on path %s", TELEPHONY_AG_INTERFACE,
+ telephony->path);
+
+ return 0;
+}
+
+void telephony_unregister_interface(struct telephony *telephony)
+{
+ g_dbus_unregister_interface(btd_get_dbus_connection(), telephony->path,
+ TELEPHONY_AG_INTERFACE);
+}
+
+struct btd_service *telephony_get_service(struct telephony *telephony)
+{
+ return telephony->service;
+}
+
+struct btd_device *telephony_get_device(struct telephony *telephony)
+{
+ return telephony->device;
+}
+
+const char *telephony_get_path(struct telephony *telephony)
+{
+ return telephony->path;
+}
+
+bdaddr_t telephony_get_src(struct telephony *telephony)
+{
+ return telephony->src;
+}
+
+bdaddr_t telephony_get_dst(struct telephony *telephony)
+{
+ return telephony->dst;
+}
+
+void *telephony_get_profile_data(struct telephony *telephony)
+{
+ return telephony->profile_data;
+}
+
+void telephony_set_state(struct telephony *telephony,
+ enum connection_state state)
+{
+ char address[18];
+
+ if (telephony->state == state)
+ return;
+
+ ba2str(&telephony->dst, address);
+ DBG("device %s state %s -> %s", address,
+ state_to_string(telephony->state),
+ state_to_string(state));
+
+ telephony->state = state;
+
+ g_dbus_emit_property_changed(btd_get_dbus_connection(),
+ telephony->path, TELEPHONY_AG_INTERFACE,
+ "State");
+}
+
+enum connection_state telephony_get_state(struct telephony *telephony)
+{
+ return telephony->state;
+}
+
+void telephony_set_network_service(struct telephony *telephony, bool service)
+{
+ char address[18];
+
+ if (telephony->network_service == service)
+ return;
+
+ ba2str(&telephony->dst, address);
+ DBG("device %s network service %u -> %u", address,
+ telephony->network_service,
+ service);
+
+ telephony->network_service = service;
+
+ g_dbus_emit_property_changed(btd_get_dbus_connection(),
+ telephony->path, TELEPHONY_AG_INTERFACE,
+ "Service");
+}
+
+bool telephony_get_network_service(struct telephony *telephony)
+{
+ return telephony->network_service;
+}
+
+void telephony_set_signal(struct telephony *telephony, uint8_t signal)
+{
+ char address[18];
+
+ if (telephony->signal == signal)
+ return;
+
+ ba2str(&telephony->dst, address);
+ DBG("device %s signal %u -> %u", address, telephony->signal, signal);
+
+ telephony->signal = signal;
+
+ g_dbus_emit_property_changed(btd_get_dbus_connection(),
+ telephony->path, TELEPHONY_AG_INTERFACE,
+ "Signal");
+}
+
+uint8_t telephony_get_signal(struct telephony *telephony)
+{
+ return telephony->signal;
+}
+
+void telephony_set_roaming(struct telephony *telephony, bool roaming)
+{
+ char address[18];
+
+ if (telephony->roaming == roaming)
+ return;
+
+ ba2str(&telephony->dst, address);
+ DBG("device %s roaming %u -> %u", address,
+ telephony->roaming,
+ roaming);
+
+ telephony->roaming = roaming;
+
+ g_dbus_emit_property_changed(btd_get_dbus_connection(),
+ telephony->path, TELEPHONY_AG_INTERFACE,
+ "Roaming");
+}
+
+bool telephony_get_roaming(struct telephony *telephony)
+{
+ return telephony->roaming;
+}
+
+void telephony_set_battchg(struct telephony *telephony, uint8_t battchg)
+{
+ char address[18];
+
+ if (telephony->battchg == battchg)
+ return;
+
+ ba2str(&telephony->dst, address);
+ DBG("device %s battchg %u -> %u", address, telephony->battchg, battchg);
+
+ telephony->battchg = battchg;
+
+ g_dbus_emit_property_changed(btd_get_dbus_connection(),
+ telephony->path, TELEPHONY_AG_INTERFACE,
+ "BattChg");
+}
+
+uint8_t telephony_get_battchg(struct telephony *telephony)
+{
+ return telephony->battchg;
+}
+
+struct call *telephony_new_call(struct telephony *telephony,
+ enum call_state state,
+ void *user_data)
+{
+ struct call *call;
+
+ call = g_new0(struct call, 1);
+ call->device = telephony;
+ call->state = state;
+ call->idx = telephony->id++;
+ call->path = g_strdup_printf("%s/call%u", telephony->path, call->idx);
+
+ return call;
+}
+
+void telephony_free_call(struct call *call)
+{
+ if (call->pending_msg)
+ dbus_message_unref(call->pending_msg);
+
+ g_free(call->name);
+ g_free(call->incoming_line);
+ g_free(call->line_id);
+ g_free(call->path);
+ g_free(call);
+}
+
+static DBusMessage *call_answer(DBusConnection *conn, DBusMessage *msg,
+ void *call_data)
+{
+ struct call *call = call_data;
+ struct telephony *telephony = call->device;
+
+ return telephony->cbs->call_answer(conn, msg, call_data);
+}
+
+static DBusMessage *call_hangup(DBusConnection *conn, DBusMessage *msg,
+ void *call_data)
+{
+ struct call *call = call_data;
+ struct telephony *telephony = call->device;
+
+ return telephony->cbs->call_hangup(conn, msg, call_data);
+}
+
+static gboolean call_line_id_exists(const GDBusPropertyTable *property,
+ void *data)
+{
+ struct call *call = data;
+
+ return call->line_id != NULL;
+}
+
+static gboolean call_property_get_line_id(
+ const GDBusPropertyTable *property,
+ DBusMessageIter *iter, void *data)
+{
+ struct call *call = data;
+
+ if (call->line_id == NULL)
+ return FALSE;
+
+ dbus_message_iter_append_basic(iter, DBUS_TYPE_STRING, &call->line_id);
+
+ return TRUE;
+}
+
+static gboolean call_incoming_line_exists(const GDBusPropertyTable *property,
+ void *data)
+{
+ struct call *call = data;
+
+ return call->incoming_line != NULL;
+}
+
+static gboolean call_property_get_incoming_line(
+ const GDBusPropertyTable *property,
+ DBusMessageIter *iter, void *data)
+{
+ struct call *call = data;
+
+ if (call->incoming_line == NULL)
+ return FALSE;
+
+ dbus_message_iter_append_basic(iter, DBUS_TYPE_STRING,
+ &call->incoming_line);
+
+ return TRUE;
+}
+
+static gboolean call_name_exists(const GDBusPropertyTable *property,
+ void *data)
+{
+ struct call *call = data;
+
+ return call->name != NULL;
+}
+
+static gboolean call_property_get_name(const GDBusPropertyTable *property,
+ DBusMessageIter *iter, void *data)
+{
+ struct call *call = data;
+
+ if (call->name == NULL)
+ return FALSE;
+
+ dbus_message_iter_append_basic(iter, DBUS_TYPE_STRING, &call->name);
+
+ return TRUE;
+}
+
+static gboolean call_property_get_multiparty(
+ const GDBusPropertyTable *property,
+ DBusMessageIter *iter, void *data)
+{
+ struct call *call = data;
+ dbus_bool_t value;
+
+ value = call->multiparty;
+
+ dbus_message_iter_append_basic(iter, DBUS_TYPE_BOOLEAN, &value);
+
+ return TRUE;
+}
+
+static gboolean call_property_get_state(const GDBusPropertyTable *property,
+ DBusMessageIter *iter, void *data)
+{
+ struct call *call = data;
+ const char *string;
+
+ string = call_state_to_string(call->state);
+
+ dbus_message_iter_append_basic(iter, DBUS_TYPE_STRING, &string);
+
+ return TRUE;
+}
+
+static const GDBusMethodTable telephony_call_methods[] = {
+ { GDBUS_ASYNC_METHOD("Answer", NULL, NULL, call_answer) },
+ { GDBUS_ASYNC_METHOD("Hangup", NULL, NULL, call_hangup) },
+ { }
+};
+
+static const GDBusPropertyTable telephony_call_properties[] = {
+ { "LineIdentification", "s", call_property_get_line_id, NULL,
+ call_line_id_exists },
+ { "IncomingLine", "s", call_property_get_incoming_line, NULL,
+ call_incoming_line_exists },
+ { "Name", "s", call_property_get_name, NULL, call_name_exists },
+ { "Multiparty", "b", call_property_get_multiparty },
+ { "State", "s", call_property_get_state },
+ { }
+};
+
+static void call_path_unregister(void *user_data)
+{
+ struct call *call = user_data;
+
+ DBG("Unregistered interface %s on path %s", TELEPHONY_CALL_INTERFACE,
+ call->path);
+
+ telephony_free_call(call);
+}
+
+int telephony_call_register_interface(struct call *call)
+{
+ if (call->device->cbs == NULL)
+ return -EINVAL;
+
+ if (!g_dbus_register_interface(btd_get_dbus_connection(),
+ call->path,
+ TELEPHONY_CALL_INTERFACE,
+ telephony_call_methods, NULL,
+ telephony_call_properties, call,
+ call_path_unregister)) {
+ return -EINVAL;
+ }
+
+ DBG("Registered interface %s on path %s", TELEPHONY_CALL_INTERFACE,
+ call->path);
+
+ return 0;
+}
+
+void telephony_call_unregister_interface(struct call *call)
+{
+ g_dbus_unregister_interface(btd_get_dbus_connection(),
+ call->path,
+ TELEPHONY_CALL_INTERFACE);
+}
+
+void telephony_set_call_state(struct call *call, enum call_state state)
+{
+ if (call->state == state)
+ return;
+
+ DBG("%s state %s -> %s", call->path, call_state_to_string(call->state),
+ call_state_to_string(state));
+
+ call->state = state;
+
+ g_dbus_emit_property_changed(btd_get_dbus_connection(),
+ call->device->path, TELEPHONY_CALL_INTERFACE,
+ "State");
+}
+
+enum call_state telephony_get_call_state(struct call *call)
+{
+ return call->state;
+}
diff --git a/profiles/audio/telephony.h b/profiles/audio/telephony.h
new file mode 100644
index 000000000..362b163ab
--- /dev/null
+++ b/profiles/audio/telephony.h
@@ -0,0 +1,110 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ *
+ * BlueZ - Bluetooth protocol stack for Linux
+ *
+ * Copyright © 2025 Collabora Ltd.
+ *
+ *
+ */
+
+enum connection_state {
+ CONNECTING = 0,
+ SLC_CONNECTING,
+ CONNECTED,
+ DISCONNECTING
+};
+
+enum call_state {
+ CALL_STATE_ACTIVE = 0,
+ CALL_STATE_HELD,
+ CALL_STATE_DIALING,
+ CALL_STATE_ALERTING,
+ CALL_STATE_INCOMING,
+ CALL_STATE_WAITING,
+ CALL_STATE_DISCONNECTED,
+};
+
+struct telephony;
+
+struct telephony_callbacks {
+ DBusMessage *(*dial)(DBusConnection *conn, DBusMessage *msg,
+ void *profile_data);
+ DBusMessage *(*swap_calls)(DBusConnection *conn, DBusMessage *msg,
+ void *profile_data);
+ DBusMessage *(*release_and_answer)(DBusConnection *conn,
+ DBusMessage *msg,
+ void *profile_data);
+ DBusMessage *(*release_and_swap)(DBusConnection *conn,
+ DBusMessage *msg,
+ void *profile_data);
+ DBusMessage *(*hold_and_answer)(DBusConnection *conn,
+ DBusMessage *msg,
+ void *profile_data);
+ DBusMessage *(*hangup_all)(DBusConnection *conn, DBusMessage *msg,
+ void *profile_data);
+ DBusMessage *(*create_multiparty)(DBusConnection *conn,
+ DBusMessage *msg,
+ void *profile_data);
+ DBusMessage *(*send_tones)(DBusConnection *conn, DBusMessage *msg,
+ void *profile_data);
+
+ DBusMessage *(*call_answer)(DBusConnection *conn, DBusMessage *msg,
+ void *call_data);
+ DBusMessage *(*call_hangup)(DBusConnection *conn, DBusMessage *msg,
+ void *call_data);
+ const char *(*call_get_line_id)(void *call_data);
+ const char *(*call_get_incoming_line)(void *call_data);
+ const char *(*call_get_name)(void *call_data);
+ bool (*call_get_multiparty)(void *call_data);
+ enum call_state (*call_get_state)(void *call_data);
+};
+
+struct call {
+ struct telephony *device;
+ char *path;
+ uint8_t idx;
+
+ char *line_id;
+ char *incoming_line;
+ char *name;
+ bool multiparty;
+ enum call_state state;
+
+ DBusMessage *pending_msg;
+};
+
+struct telephony *telephony_new(struct btd_service *service,
+ void *profile_data,
+ struct telephony_callbacks *cbs);
+void telephony_free(struct telephony *telephony);
+int telephony_register_interface(struct telephony *telephony);
+void telephony_unregister_interface(struct telephony *telephony);
+
+struct btd_service *telephony_get_service(struct telephony *telephony);
+struct btd_device *telephony_get_device(struct telephony *telephony);
+const char *telephony_get_path(struct telephony *telephony);
+bdaddr_t telephony_get_src(struct telephony *telephony);
+bdaddr_t telephony_get_dst(struct telephony *telephony);
+void *telephony_get_profile_data(struct telephony *telephony);
+void telephony_set_state(struct telephony *telephony,
+ enum connection_state state);
+enum connection_state telephony_get_state(struct telephony *telephony);
+void telephony_set_network_service(struct telephony *telephony, bool service);
+bool telephony_get_network_service(struct telephony *telephony);
+void telephony_set_signal(struct telephony *telephony, uint8_t signal);
+uint8_t telephony_get_signal(struct telephony *telephony);
+void telephony_set_roaming(struct telephony *telephony, bool roaming);
+bool telephony_get_roaming(struct telephony *telephony);
+void telephony_set_battchg(struct telephony *telephony, uint8_t battchg);
+uint8_t telephony_get_battchg(struct telephony *telephony);
+
+struct call *telephony_new_call(struct telephony *telephony,
+ enum call_state state,
+ void *user_data);
+void telephony_free_call(struct call *call);
+int telephony_call_register_interface(struct call *call);
+void telephony_call_unregister_interface(struct call *call);
+
+void telephony_set_call_state(struct call *call, enum call_state state);
+enum call_state telephony_get_call_state(struct call *call);
--
2.43.0
^ permalink raw reply related [flat|nested] 13+ messages in thread* [RFC BlueZ 03/10] audio/telephony: Add skeleton for HFP profile
2025-05-28 8:59 [RFC BlueZ 00/10] New Telephony interface for HSP, HFP and CCP Frédéric Danis
2025-05-28 8:59 ` [RFC BlueZ 01/10] doc: Add new telephony related profiles interfaces Frédéric Danis
2025-05-28 8:59 ` [RFC BlueZ 02/10] audio/telephony: Add shared interfaces implementation Frédéric Danis
@ 2025-05-28 8:59 ` Frédéric Danis
2025-05-28 8:59 ` [RFC BlueZ 04/10] audio/hfp-hf: Add HFP SLC connection and event support Frédéric Danis
` (6 subsequent siblings)
9 siblings, 0 replies; 13+ messages in thread
From: Frédéric Danis @ 2025-05-28 8:59 UTC (permalink / raw)
To: linux-bluetooth
---
Makefile.plugins | 5 +
configure.ac | 7 ++
profiles/audio/hfp-hf.c | 218 ++++++++++++++++++++++++++++++++++++++++
3 files changed, 230 insertions(+)
create mode 100644 profiles/audio/hfp-hf.c
diff --git a/Makefile.plugins b/Makefile.plugins
index bae4363d0..66e8033e4 100644
--- a/Makefile.plugins
+++ b/Makefile.plugins
@@ -150,3 +150,8 @@ if ASHA
builtin_modules += asha
builtin_sources += profiles/audio/asha.h profiles/audio/asha.c
endif
+
+if HFP
+builtin_modules += hfp
+builtin_sources += profiles/audio/telephony.c profiles/audio/hfp-hf.c
+endif
diff --git a/configure.ac b/configure.ac
index 1e089aaa7..b3dc40626 100644
--- a/configure.ac
+++ b/configure.ac
@@ -238,6 +238,13 @@ if test "${enable_asha}" != "no"; then
AC_DEFINE(HAVE_ASHA, 1, [Define to 1 if you have ASHA support.])
fi
+AC_ARG_ENABLE(hfp, AS_HELP_STRING([--disable-hfp],
+ [disable HFP support]), [enable_hfp=${enableval}])
+AM_CONDITIONAL(HFP, test "${enable_hfp}" != "no")
+if test "${enable_hfp}" != "no"; then
+ AC_DEFINE(HAVE_HFP, 1, [Define to 1 if you have HFP support.])
+fi
+
AC_ARG_ENABLE(tools, AS_HELP_STRING([--disable-tools],
[disable Bluetooth tools]), [enable_tools=${enableval}])
AM_CONDITIONAL(TOOLS, test "${enable_tools}" != "no")
diff --git a/profiles/audio/hfp-hf.c b/profiles/audio/hfp-hf.c
new file mode 100644
index 000000000..aa74cd515
--- /dev/null
+++ b/profiles/audio/hfp-hf.c
@@ -0,0 +1,218 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ *
+ * BlueZ - Bluetooth protocol stack for Linux
+ *
+ * Copyright (C) 2014 Intel Corporation. All rights reserved.
+ * Copyright © 2025 Collabora Ltd.
+ *
+ *
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#define _GNU_SOURCE
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <errno.h>
+#include <limits.h>
+
+#include <stdint.h>
+
+#include <glib.h>
+
+#include "lib/bluetooth.h"
+#include "bluetooth/sdp.h"
+#include "bluetooth/sdp_lib.h"
+#include "lib/uuid.h"
+
+#include "gdbus/gdbus.h"
+
+#include "btio/btio.h"
+#include "src/adapter.h"
+#include "src/btd.h"
+#include "src/dbus-common.h"
+#include "src/device.h"
+#include "src/log.h"
+#include "src/plugin.h"
+#include "src/profile.h"
+#include "src/service.h"
+
+#include "telephony.h"
+
+struct hfp_device {
+ struct telephony *telephony;
+ uint16_t version;
+ GIOChannel *io;
+};
+
+static void device_destroy(struct hfp_device *dev)
+{
+ DBG("%s", telephony_get_path(dev->telephony));
+
+ if (dev->io) {
+ g_io_channel_unref(dev->io);
+ dev->io = NULL;
+ }
+
+ telephony_unregister_interface(dev->telephony);
+}
+
+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);
+
+ DBG("");
+
+ if (err) {
+ error("%s", err->message);
+ goto failed;
+ }
+
+ g_io_channel_set_close_on_unref(chan, FALSE);
+
+ btd_service_connecting_complete(service, 0);
+
+ return;
+
+failed:
+ g_io_channel_shutdown(chan, TRUE, NULL);
+ device_destroy(dev);
+}
+
+struct telephony_callbacks hfp_callbacks = {
+};
+
+static int hfp_connect(struct btd_service *service)
+{
+ struct hfp_device *dev;
+ struct btd_profile *p;
+ const sdp_record_t *rec;
+ sdp_list_t *list, *protos;
+ sdp_profile_desc_t *desc;
+ int channel;
+ bdaddr_t src, dst;
+ GError *err = NULL;
+
+ DBG("");
+
+ dev = btd_service_get_user_data(service);
+
+ p = btd_service_get_profile(service);
+ rec = btd_device_get_record(telephony_get_device(dev->telephony),
+ p->remote_uuid);
+ if (!rec)
+ return -EIO;
+
+ if (sdp_get_profile_descs(rec, &list) == 0) {
+ desc = list->data;
+ dev->version = desc->version;
+ }
+ sdp_list_free(list, free);
+
+ if (sdp_get_access_protos(rec, &protos) < 0) {
+ error("unable to get access protocols from record");
+ return -EIO;
+ }
+
+ channel = sdp_get_proto_port(protos, RFCOMM_UUID);
+ sdp_list_foreach(protos, (sdp_list_func_t) sdp_list_free, NULL);
+ sdp_list_free(protos, NULL);
+ if (channel <= 0) {
+ error("unable to get RFCOMM channel from record");
+ return -EIO;
+ }
+
+ src = telephony_get_src(dev->telephony);
+ dst = telephony_get_dst(dev->telephony);
+ dev->io = bt_io_connect(connect_cb, dev,
+ 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_CHANNEL, channel,
+ BT_IO_OPT_INVALID);
+ if (dev->io == NULL) {
+ error("unable to get start connection");
+ return -EIO;
+ }
+
+ return telephony_register_interface(dev->telephony);
+}
+
+static int hfp_disconnect(struct btd_service *service)
+{
+ DBG("");
+
+ btd_service_disconnecting_complete(service, 0);
+
+ return 0;
+}
+
+static int hfp_probe(struct btd_service *service)
+{
+ struct btd_device *device = btd_service_get_device(service);
+ const char *path = device_get_path(device);
+ struct hfp_device *dev;
+
+ DBG("%s", path);
+
+ dev = g_new0(struct hfp_device, 1);
+ if (!dev)
+ return -EINVAL;
+
+ dev->telephony = telephony_new(service, dev, &hfp_callbacks);
+ btd_service_set_user_data(service, dev);
+
+ return 0;
+}
+
+static void hfp_remove(struct btd_service *service)
+{
+ struct btd_device *device = btd_service_get_device(service);
+ const char *path = device_get_path(device);
+ struct hfp_device *dev;
+
+ DBG("%s", path);
+
+ dev = btd_service_get_user_data(service);
+
+ telephony_free(dev->telephony);
+ g_free(dev);
+
+ btd_service_unref(service);
+}
+
+static struct btd_profile hfp_hf_profile = {
+ .name = "hfp",
+ .priority = BTD_PROFILE_PRIORITY_MEDIUM,
+
+ .remote_uuid = HFP_AG_UUID,
+ .device_probe = hfp_probe,
+ .device_remove = hfp_remove,
+
+ .auto_connect = true,
+ .connect = hfp_connect,
+ .disconnect = hfp_disconnect,
+
+ .experimental = true,
+};
+
+static int hfp_init(void)
+{
+ btd_profile_register(&hfp_hf_profile);
+
+ return 0;
+}
+
+static void hfp_exit(void)
+{
+ btd_profile_unregister(&hfp_hf_profile);
+}
+
+BLUETOOTH_PLUGIN_DEFINE(hfp, VERSION, BLUETOOTH_PLUGIN_PRIORITY_DEFAULT,
+ hfp_init, hfp_exit)
--
2.43.0
^ permalink raw reply related [flat|nested] 13+ messages in thread* [RFC BlueZ 04/10] audio/hfp-hf: Add HFP SLC connection and event support
2025-05-28 8:59 [RFC BlueZ 00/10] New Telephony interface for HSP, HFP and CCP Frédéric Danis
` (2 preceding siblings ...)
2025-05-28 8:59 ` [RFC BlueZ 03/10] audio/telephony: Add skeleton for HFP profile Frédéric Danis
@ 2025-05-28 8:59 ` Frédéric Danis
2025-05-28 8:59 ` [RFC BlueZ 05/10] audio/hfp-hf: Add dial support Frédéric Danis
` (5 subsequent siblings)
9 siblings, 0 replies; 13+ messages in thread
From: Frédéric Danis @ 2025-05-28 8:59 UTC (permalink / raw)
To: linux-bluetooth
---
profiles/audio/hfp-hf.c | 904 ++++++++++++++++++++++++++++++++++++++++
1 file changed, 904 insertions(+)
diff --git a/profiles/audio/hfp-hf.c b/profiles/audio/hfp-hf.c
index aa74cd515..5068fff01 100644
--- a/profiles/audio/hfp-hf.c
+++ b/profiles/audio/hfp-hf.c
@@ -40,19 +40,114 @@
#include "src/plugin.h"
#include "src/profile.h"
#include "src/service.h"
+#include "src/shared/hfp.h"
#include "telephony.h"
+#define CALL_IND_NO_CALL_IN_PROGRESS 0x00
+#define CALL_IND_CALL_IN_PROGRESS 0x01
+
+#define CHLD_FEAT_REL 0x00000001
+#define CHLD_FEAT_REL_ACC 0x00000002
+#define CHLD_FEAT_REL_X 0x00000004
+#define CHLD_FEAT_HOLD_ACC 0x00000008
+#define CHLD_FEAT_PRIV_X 0x00000010
+#define CHLD_FEAT_MERGE 0x00000020
+#define CHLD_FEAT_MERGE_DETACH 0x00000040
+
+#define HFP_HF_FEAT_ECNR 0x00000001
+#define HFP_HF_FEAT_3WAY 0x00000002
+#define HFP_HF_FEAT_CLIP 0x00000004
+#define HFP_HF_FEAT_VOICE_RECOGNITION 0x00000008
+#define HFP_HF_FEAT_REMOTE_VOLUME_CONTROL 0x00000010
+#define HFP_HF_FEAT_ENHANCED_CALL_STATUS 0x00000020
+#define HFP_HF_FEAT_ENHANCED_CALL_CONTROL 0x00000040
+#define HFP_HF_FEAT_CODEC_NEGOTIATION 0x00000080
+#define HFP_HF_FEAT_HF_INDICATORS 0x00000100
+#define HFP_HF_FEAT_ESCO_S4_T2 0x00000200
+#define HFP_HF_FEAT_ENHANCED_VOICE_RECOGNITION_STATUS 0x00000400
+#define HFP_HF_FEAT_VOICE_RECOGNITION_TEXT 0x00000800
+
+#define HFP_AG_FEAT_3WAY 0x00000001
+#define HFP_AG_FEAT_ECNR 0x00000002
+#define HFP_AG_FEAT_VOICE_RECOGNITION 0x00000004
+#define HFP_AG_FEAT_IN_BAND_RING_TONE 0x00000008
+#define HFP_AG_FEAT_ATTACH_VOICE_TAG 0x00000010
+#define HFP_AG_FEAT_REJECT_CALL 0x00000020
+#define HFP_AG_FEAT_ENHANCED_CALL_STATUS 0x00000040
+#define HFP_AG_FEAT_ENHANCED_CALL_CONTROL 0x00000080
+#define HFP_AG_FEAT_EXTENDED_RES_CODE 0x00000100
+#define HFP_AG_FEAT_CODEC_NEGOTIATION 0x00000200
+#define HFP_AG_FEAT_HF_INDICATORS 0x00000400
+#define HFP_AG_FEAT_ESCO_S4_T2 0x00000800
+#define HFP_AG_FEAT_ENHANCED_VOICE_RECOGNITION_STATUS 0x00001000
+#define HFP_AG_FEAT_VOICE_RECOGNITION_TEXT 0x00001000
+
+#define HFP_HF_FEATURES (HFP_HF_FEAT_ECNR | HFP_HF_FEAT_3WAY |\
+ HFP_HF_FEAT_CLIP | HFP_HF_FEAT_REMOTE_VOLUME_CONTROL |\
+ HFP_HF_FEAT_ENHANCED_CALL_STATUS | HFP_HF_FEAT_ESCO_S4_T2)
+
+enum hfp_indicator {
+ HFP_INDICATOR_SERVICE = 0,
+ HFP_INDICATOR_CALL,
+ HFP_INDICATOR_CALLSETUP,
+ HFP_INDICATOR_CALLHELD,
+ HFP_INDICATOR_SIGNAL,
+ HFP_INDICATOR_ROAM,
+ HFP_INDICATOR_BATTCHG,
+ HFP_INDICATOR_LAST
+};
+
+enum call_setup {
+ CIND_CALLSETUP_NONE = 0,
+ CIND_CALLSETUP_INCOMING,
+ CIND_CALLSETUP_DIALING,
+ CIND_CALLSETUP_ALERTING
+};
+
+enum call_held {
+ CIND_CALLHELD_NONE = 0,
+ CIND_CALLHELD_HOLD_AND_ACTIVE,
+ CIND_CALLHELD_HOLD
+};
+
+typedef void (*ciev_func_t)(uint8_t val, void *user_data);
+
+struct indicator {
+ uint8_t index;
+ uint32_t min;
+ uint32_t max;
+ uint32_t val;
+ ciev_func_t cb;
+};
+
struct hfp_device {
struct telephony *telephony;
uint16_t version;
GIOChannel *io;
+ enum connection_state state;
+ uint32_t hfp_hf_features;
+ uint32_t features;
+ struct hfp_hf *hf;
+ struct indicator ag_ind[HFP_INDICATOR_LAST];
+ uint32_t chld_features;
+ bool call;
+ enum call_setup call_setup;
+ enum call_held call_held;
+ GSList *calls;
};
static void device_destroy(struct hfp_device *dev)
{
DBG("%s", telephony_get_path(dev->telephony));
+ telephony_set_state(dev->telephony, DISCONNECTING);
+
+ if (dev->hf) {
+ hfp_hf_unref(dev->hf);
+ dev->hf = NULL;
+ }
+
if (dev->io) {
g_io_channel_unref(dev->io);
dev->io = NULL;
@@ -61,6 +156,790 @@ static void device_destroy(struct hfp_device *dev)
telephony_unregister_interface(dev->telephony);
}
+static void slc_error(struct hfp_device *dev)
+{
+ error("Could not create SLC - dropping connection");
+ hfp_hf_disconnect(dev->hf);
+}
+
+static void set_chld_feat(struct hfp_device *dev, char *feat)
+{
+ DBG(" %s", feat);
+
+ if (strcmp(feat, "0") == 0)
+ dev->chld_features |= CHLD_FEAT_REL;
+ else if (strcmp(feat, "1") == 0)
+ dev->chld_features |= CHLD_FEAT_REL_ACC;
+ else if (strcmp(feat, "1x") == 0)
+ dev->chld_features |= CHLD_FEAT_REL_X;
+ else if (strcmp(feat, "2") == 0)
+ dev->chld_features |= CHLD_FEAT_HOLD_ACC;
+ else if (strcmp(feat, "2x") == 0)
+ dev->chld_features |= CHLD_FEAT_PRIV_X;
+ else if (strcmp(feat, "3") == 0)
+ dev->chld_features |= CHLD_FEAT_MERGE;
+ else if (strcmp(feat, "4") == 0)
+ dev->chld_features |= CHLD_FEAT_MERGE_DETACH;
+}
+
+static const char *cme_error_to_string(uint8_t cme_error)
+{
+ switch (cme_error) {
+ case 0: return "AG failure";
+ case 1: return "no connection to phone";
+ case 3: return "operation not allowed";
+ case 4: return "operation not supported";
+ case 5: return "PH-SIM PIN required";
+ case 10: return "SIM not inserted";
+ case 11: return "SIM PIN required";
+ case 12: return "SIM PUK required";
+ case 13: return "SIM failure";
+ case 14: return "SIM busy";
+ case 16: return "incorrect password";
+ case 17: return "SIM PIN2 required";
+ case 18: return "SIM PUK2 required";
+ case 20: return "memory full";
+ case 21: return "invalid index";
+ case 23: return "memory failure";
+ case 24: return "text string too long";
+ case 25: return "invalid characters in text string";
+ case 26: return "dial string too long";
+ case 27: return "invalid characters in dial string";
+ case 30: return "no network service";
+ case 31: return "network Timeout";
+ case 32: return "network not allowed - Emergency calls only";
+ default: return "Unknown CME error";
+ }
+}
+
+static void cmd_complete_cb(enum hfp_result result, enum hfp_error cme_err,
+ void *user_data)
+{
+ DBusMessage *msg = user_data;
+ DBusMessage *reply = NULL;
+
+ DBG("%u", result);
+
+ if (msg == NULL)
+ return;
+
+ switch (result) {
+ case HFP_RESULT_OK:
+ reply = g_dbus_create_reply(msg, DBUS_TYPE_INVALID);
+ break;
+ case HFP_RESULT_NO_CARRIER:
+ reply = btd_error_failed(msg, "no-carrier");
+ break;
+ case HFP_RESULT_ERROR:
+ reply = btd_error_failed(msg, "unknown");
+ break;
+ case HFP_RESULT_BUSY:
+ reply = btd_error_busy(msg);
+ break;
+ case HFP_RESULT_NO_ANSWER:
+ reply = btd_error_failed(msg, "no-answer");
+ break;
+ case HFP_RESULT_DELAYED:
+ reply = btd_error_failed(msg, "delayed");
+ break;
+ case HFP_RESULT_REJECTED:
+ reply = btd_error_failed(msg, "rejected");
+ break;
+ case HFP_RESULT_CME_ERROR:
+ reply = btd_error_failed(msg, cme_error_to_string(cme_err));
+ break;
+ case HFP_RESULT_CONNECT:
+ case HFP_RESULT_RING:
+ case HFP_RESULT_NO_DIALTONE:
+ default:
+ reply = btd_error_failed(msg, "unknown");
+ error("hf-client: Unknown error code %d", result);
+ break;
+ }
+
+ if (reply) {
+ g_dbus_send_message(btd_get_dbus_connection(), reply);
+ dbus_message_unref(msg);
+ }
+}
+
+static void ciev_cb(struct hfp_context *context, void *user_data)
+{
+ struct hfp_device *dev = user_data;
+ unsigned int index, val;
+ int i;
+
+ DBG("");
+
+ if (!hfp_context_get_number(context, &index))
+ return;
+
+ if (!hfp_context_get_number(context, &val))
+ return;
+
+ for (i = 0; i < HFP_INDICATOR_LAST; i++) {
+ if (dev->ag_ind[i].index != index)
+ continue;
+
+ if (dev->ag_ind[i].cb) {
+ dev->ag_ind[i].val = val;
+ dev->ag_ind[i].cb(val, dev);
+ return;
+ }
+ }
+}
+
+static void slc_completed(struct hfp_device *dev)
+{
+ int i;
+ struct indicator *ag_ind;
+
+ DBG("");
+
+ ag_ind = dev->ag_ind;
+
+ telephony_set_state(dev->telephony, CONNECTED);
+
+ /* Notify Android with indicators */
+ for (i = 0; i < HFP_INDICATOR_LAST; i++) {
+ if (!ag_ind[i].cb)
+ continue;
+
+ ag_ind[i].cb(ag_ind[i].val, dev);
+ }
+
+ /* TODO: register unsolicited results handlers */
+
+ hfp_hf_register(dev->hf, ciev_cb, "+CIEV", dev, NULL);
+
+ if (!hfp_hf_send_command(dev->hf, cmd_complete_cb, NULL, "AT+COPS=3,0"))
+ info("hf-client: Could not send AT+COPS=3,0");
+}
+
+static void slc_chld_resp(enum hfp_result result, enum hfp_error cme_err,
+ void *user_data)
+{
+ struct hfp_device *dev = user_data;
+
+ DBG("");
+
+ hfp_hf_unregister(dev->hf, "+CHLD");
+
+ if (result != HFP_RESULT_OK) {
+ error("hf-client: CHLD error: %d", result);
+ slc_error(dev);
+ return;
+ }
+
+ slc_completed(dev);
+}
+
+static void slc_chld_cb(struct hfp_context *context, void *user_data)
+{
+ struct hfp_device *dev = user_data;
+ char feat[3];
+
+ if (!hfp_context_open_container(context))
+ goto failed;
+
+ while (hfp_context_get_unquoted_string(context, feat, sizeof(feat)))
+ set_chld_feat(dev, feat);
+
+ if (!hfp_context_close_container(context))
+ goto failed;
+
+ return;
+
+failed:
+ error("hf-client: Error on CHLD response");
+ slc_error(dev);
+}
+
+static void slc_cmer_resp(enum hfp_result result, enum hfp_error cme_err,
+ void *user_data)
+{
+ struct hfp_device *dev = user_data;
+
+ DBG("");
+
+ if (result != HFP_RESULT_OK) {
+ error("hf-client: CMER error: %d", result);
+ goto failed;
+ }
+
+ /* Continue with SLC creation */
+ if (!(dev->features & HFP_AG_FEAT_3WAY)) {
+ slc_completed(dev);
+ return;
+ }
+
+ if (!hfp_hf_register(dev->hf, slc_chld_cb, "+CHLD", dev, NULL)) {
+ error("hf-client: Could not register +CHLD");
+ goto failed;
+ }
+
+ if (!hfp_hf_send_command(dev->hf, slc_chld_resp, dev, "AT+CHLD=?")) {
+ error("hf-client: Could not send AT+CHLD");
+ goto failed;
+ }
+
+ return;
+
+failed:
+ slc_error(dev);
+}
+
+static void slc_cind_status_resp(enum hfp_result result,
+ enum hfp_error cme_err,
+ void *user_data)
+{
+ struct hfp_device *dev = user_data;
+
+ DBG("");
+
+ hfp_hf_unregister(dev->hf, "+CIND");
+
+ if (result != HFP_RESULT_OK) {
+ error("hf-client: CIND error: %d", result);
+ goto failed;
+ }
+
+ /* Continue with SLC creation */
+ if (!hfp_hf_send_command(dev->hf, slc_cmer_resp, dev,
+ "AT+CMER=3,0,0,1")) {
+ error("hf-client: Counld not send AT+CMER");
+ goto failed;
+ }
+
+ return;
+
+failed:
+ slc_error(dev);
+}
+
+static void set_indicator_value(uint8_t index, unsigned int val,
+ struct indicator *ag_ind, struct hfp_device *dev)
+{
+ int i;
+
+ for (i = 0; i < HFP_INDICATOR_LAST; i++) {
+ if (index != ag_ind[i].index)
+ continue;
+
+ ag_ind[i].val = val;
+ ag_ind[i].cb(val, dev);
+ return;
+ }
+}
+
+static void slc_cind_status_cb(struct hfp_context *context,
+ void *user_data)
+{
+ struct hfp_device *dev = user_data;
+ uint8_t index = 1;
+
+ DBG("");
+
+ while (hfp_context_has_next(context)) {
+ uint32_t val;
+
+ if (!hfp_context_get_number(context, &val)) {
+ error("hf-client: Error on CIND status response");
+ return;
+ }
+
+ set_indicator_value(index++, val, dev->ag_ind, dev);
+ }
+}
+
+static void slc_cind_resp(enum hfp_result result, enum hfp_error cme_err,
+ void *user_data)
+{
+ struct hfp_device *dev = user_data;
+
+ DBG("");
+
+ hfp_hf_unregister(dev->hf, "+CIND");
+
+ if (result != HFP_RESULT_OK) {
+ error("hf-client: CIND error: %d", result);
+ goto failed;
+ }
+
+ /* Continue with SLC creation */
+ if (!hfp_hf_register(dev->hf, slc_cind_status_cb, "+CIND", dev,
+ NULL)) {
+ error("hf-client: Counld not register +CIND");
+ goto failed;
+ }
+
+ if (!hfp_hf_send_command(dev->hf, slc_cind_status_resp, dev,
+ "AT+CIND?")) {
+ error("hf-client: Counld not send AT+CIND?");
+ goto failed;
+ }
+
+ return;
+
+failed:
+ slc_error(dev);
+}
+
+static void ciev_service_cb(uint8_t val, void *user_data)
+{
+ struct hfp_device *dev = user_data;
+
+ DBG("");
+
+ if (val > 1) {
+ error("hf-client: Incorrect state %u:", val);
+ return;
+ }
+
+ telephony_set_network_service(dev->telephony, val);
+}
+
+static void activate_calls(gpointer data, gpointer user_data)
+{
+ struct call *call = data;
+
+ if (call->state == CALL_STATE_DIALING ||
+ call->state == CALL_STATE_ALERTING ||
+ call->state == CALL_STATE_INCOMING)
+ telephony_set_call_state(call, CALL_STATE_ACTIVE);
+}
+
+static void deactivate_active_calls(gpointer data, gpointer user_data)
+{
+ struct call *call = data;
+ struct hfp_device *dev = user_data;
+
+ if (call->state == CALL_STATE_ACTIVE) {
+ telephony_set_call_state(call, CALL_STATE_DISCONNECTED);
+ dev->calls = g_slist_remove(dev->calls, call);
+ telephony_call_unregister_interface(call);
+ }
+}
+
+static void ciev_call_cb(uint8_t val, void *user_data)
+{
+ struct hfp_device *dev = user_data;
+
+ DBG("");
+
+ if (val > CALL_IND_CALL_IN_PROGRESS) {
+ error("hf-client: Incorrect call state %u:", val);
+ return;
+ }
+
+ if (dev->call == val)
+ return;
+
+ dev->call = !!val;
+
+ if (dev->call == TRUE)
+ g_slist_foreach(dev->calls, activate_calls, dev);
+ else
+ g_slist_foreach(dev->calls, deactivate_active_calls, dev);
+}
+
+static void callsetup_deactivate(gpointer data, gpointer user_data)
+{
+ struct call *call = data;
+ struct hfp_device *dev = user_data;
+
+ if (call->state == CALL_STATE_DIALING ||
+ call->state == CALL_STATE_ALERTING ||
+ call->state == CALL_STATE_INCOMING ||
+ call->state == CALL_STATE_WAITING) {
+ telephony_set_call_state(call, CALL_STATE_DISCONNECTED);
+ dev->calls = g_slist_remove(dev->calls, call);
+ telephony_call_unregister_interface(call);
+ }
+}
+
+static void callsetup_alerting(gpointer data, gpointer user_data)
+{
+ struct call *call = data;
+
+ if (call->state == CALL_STATE_DIALING)
+ telephony_set_call_state(call, CALL_STATE_ALERTING);
+}
+
+static void ciev_callsetup_cb(uint8_t val, void *user_data)
+{
+ struct hfp_device *dev = user_data;
+
+ DBG("");
+
+ if (val > CIND_CALLSETUP_ALERTING) {
+ error("hf-client: Incorrect call setup state %u:", val);
+ return;
+ }
+
+ if (dev->call_setup == val)
+ return;
+
+ dev->call_setup = val;
+
+ if (dev->call_setup == CIND_CALLSETUP_NONE) {
+ g_slist_foreach(dev->calls, callsetup_deactivate, dev);
+ } else if (dev->call_setup == CIND_CALLSETUP_INCOMING) {
+ bool found = FALSE;
+ GSList *l;
+
+ for (l = dev->calls; l; l = l->next) {
+ struct call *call = l->data;
+
+ if (call->state == CALL_STATE_INCOMING ||
+ call->state == CALL_STATE_WAITING) {
+ DBG("incoming call already in progress (%d)",
+ call->state);
+ found = TRUE;
+ break;
+ }
+ }
+
+ if (!found) {
+ struct call *call;
+
+ call = telephony_new_call(dev->telephony,
+ CALL_STATE_INCOMING,
+ NULL);
+ if (telephony_call_register_interface(call)) {
+ telephony_free_call(call);
+ return;
+ }
+ dev->calls = g_slist_append(dev->calls, call);
+ }
+ } else if (dev->call_setup == CIND_CALLSETUP_DIALING) {
+ bool found = FALSE;
+ GSList *l;
+
+ for (l = dev->calls; l; l = l->next) {
+ struct call *call = l->data;
+
+ if (call->state == CALL_STATE_DIALING ||
+ call->state == CALL_STATE_ALERTING) {
+ DBG("dialing call already in progress (%d)",
+ call->state);
+ found = TRUE;
+ break;
+ }
+ }
+
+ if (!found) {
+ struct call *call;
+
+ call = telephony_new_call(dev->telephony,
+ CALL_STATE_DIALING,
+ NULL);
+ if (telephony_call_register_interface(call)) {
+ telephony_free_call(call);
+ return;
+ }
+ dev->calls = g_slist_append(dev->calls, call);
+ }
+ } else if (dev->call_setup == CIND_CALLSETUP_ALERTING) {
+ g_slist_foreach(dev->calls, callsetup_alerting, dev);
+ }
+}
+
+static void ciev_callheld_cb(uint8_t val, void *user_data)
+{
+ struct hfp_device *dev = user_data;
+
+ DBG("");
+
+ if (val > CIND_CALLHELD_HOLD) {
+ error("hf-client: Incorrect call held state %u:", val);
+ return;
+ }
+
+ dev->call_held = val;
+
+ if (dev->call_held == CIND_CALLHELD_NONE) {
+ GSList *l;
+ bool found_waiting = FALSE;
+
+ for (l = dev->calls; l; l = l->next) {
+ struct call *call = l->data;
+
+ if (call->state != CALL_STATE_WAITING)
+ continue;
+
+ telephony_set_call_state(call,
+ CALL_STATE_DISCONNECTED);
+ found_waiting = TRUE;
+ dev->calls = g_slist_remove(dev->calls, call);
+ telephony_call_unregister_interface(call);
+ }
+
+ if (!found_waiting) {
+ for (l = dev->calls; l; l = l->next) {
+ struct call *call = l->data;
+
+ if (call->state != CALL_STATE_HELD)
+ continue;
+
+ telephony_set_call_state(call,
+ CALL_STATE_DISCONNECTED);
+ dev->calls = g_slist_remove(dev->calls, call);
+ telephony_call_unregister_interface(call);
+ }
+ }
+ } else if (dev->call_held == CIND_CALLHELD_HOLD_AND_ACTIVE) {
+ GSList *l;
+
+ for (l = dev->calls; l; l = l->next) {
+ struct call *call = l->data;
+
+ if (call->state == CALL_STATE_ACTIVE)
+ telephony_set_call_state(call,
+ CALL_STATE_HELD);
+ else if (call->state == CALL_STATE_HELD)
+ telephony_set_call_state(call,
+ CALL_STATE_ACTIVE);
+ }
+ } else if (dev->call_held == CIND_CALLHELD_HOLD) {
+ GSList *l;
+
+ for (l = dev->calls; l; l = l->next) {
+ struct call *call = l->data;
+
+ if (call->state == CALL_STATE_ACTIVE ||
+ call->state == CALL_STATE_WAITING)
+ telephony_set_call_state(call, CALL_STATE_HELD);
+ }
+ }
+}
+
+static void ciev_signal_cb(uint8_t val, void *user_data)
+{
+ struct hfp_device *dev = user_data;
+
+ DBG("");
+
+ if (val > 5) {
+ error("hf-client: Incorrect signal value %u:", val);
+ return;
+ }
+
+ telephony_set_signal(dev->telephony, val);
+}
+
+static void ciev_roam_cb(uint8_t val, void *user_data)
+{
+ struct hfp_device *dev = user_data;
+
+ DBG("");
+
+ if (val > 1) {
+ error("hf-client: Incorrect roaming state %u:", val);
+ return;
+ }
+
+ telephony_set_roaming(dev->telephony, val);
+}
+
+static void ciev_battchg_cb(uint8_t val, void *user_data)
+{
+ struct hfp_device *dev = user_data;
+
+ DBG("");
+
+ if (val > 5) {
+ error("hf-client: Incorrect battery charge value %u:", val);
+ return;
+ }
+
+ telephony_set_battchg(dev->telephony, val);
+}
+
+static void set_indicator_parameters(uint8_t index, const char *indicator,
+ unsigned int min,
+ unsigned int max,
+ struct indicator *ag_ind)
+{
+ DBG("%s, %i", indicator, index);
+
+ /* TODO: Verify min/max values ? */
+
+ if (strcmp("service", indicator) == 0) {
+ ag_ind[HFP_INDICATOR_SERVICE].index = index;
+ ag_ind[HFP_INDICATOR_SERVICE].min = min;
+ ag_ind[HFP_INDICATOR_SERVICE].max = max;
+ ag_ind[HFP_INDICATOR_SERVICE].cb = ciev_service_cb;
+ return;
+ }
+
+ if (strcmp("call", indicator) == 0) {
+ ag_ind[HFP_INDICATOR_CALL].index = index;
+ ag_ind[HFP_INDICATOR_CALL].min = min;
+ ag_ind[HFP_INDICATOR_CALL].max = max;
+ ag_ind[HFP_INDICATOR_CALL].cb = ciev_call_cb;
+ return;
+ }
+
+ if (strcmp("callsetup", indicator) == 0) {
+ ag_ind[HFP_INDICATOR_CALLSETUP].index = index;
+ ag_ind[HFP_INDICATOR_CALLSETUP].min = min;
+ ag_ind[HFP_INDICATOR_CALLSETUP].max = max;
+ ag_ind[HFP_INDICATOR_CALLSETUP].cb = ciev_callsetup_cb;
+ return;
+ }
+
+ if (strcmp("callheld", indicator) == 0) {
+ ag_ind[HFP_INDICATOR_CALLHELD].index = index;
+ ag_ind[HFP_INDICATOR_CALLHELD].min = min;
+ ag_ind[HFP_INDICATOR_CALLHELD].max = max;
+ ag_ind[HFP_INDICATOR_CALLHELD].cb = ciev_callheld_cb;
+ return;
+ }
+
+ if (strcmp("signal", indicator) == 0) {
+ ag_ind[HFP_INDICATOR_SIGNAL].index = index;
+ ag_ind[HFP_INDICATOR_SIGNAL].min = min;
+ ag_ind[HFP_INDICATOR_SIGNAL].max = max;
+ ag_ind[HFP_INDICATOR_SIGNAL].cb = ciev_signal_cb;
+ return;
+ }
+
+ if (strcmp("roam", indicator) == 0) {
+ ag_ind[HFP_INDICATOR_ROAM].index = index;
+ ag_ind[HFP_INDICATOR_ROAM].min = min;
+ ag_ind[HFP_INDICATOR_ROAM].max = max;
+ ag_ind[HFP_INDICATOR_ROAM].cb = ciev_roam_cb;
+ return;
+ }
+
+ if (strcmp("battchg", indicator) == 0) {
+ ag_ind[HFP_INDICATOR_BATTCHG].index = index;
+ ag_ind[HFP_INDICATOR_BATTCHG].min = min;
+ ag_ind[HFP_INDICATOR_BATTCHG].max = max;
+ ag_ind[HFP_INDICATOR_BATTCHG].cb = ciev_battchg_cb;
+ return;
+ }
+
+ error("hf-client: Unknown indicator: %s", indicator);
+}
+
+static void slc_cind_cb(struct hfp_context *context, void *user_data)
+{
+ struct hfp_device *dev = user_data;
+ int index = 1;
+
+ DBG("");
+
+ while (hfp_context_has_next(context)) {
+ char name[255];
+ unsigned int min, max;
+
+ /* e.g ("callsetup",(0-3)) */
+ if (!hfp_context_open_container(context))
+ break;
+
+ if (!hfp_context_get_string(context, name, sizeof(name))) {
+ error("hf-client: Could not get string");
+ goto failed;
+ }
+
+ if (!hfp_context_open_container(context)) {
+ error("hf-client: Could not open container");
+ goto failed;
+ }
+
+ if (!hfp_context_get_range(context, &min, &max)) {
+ if (!hfp_context_get_number(context, &min)) {
+ error("hf-client: Could not get number");
+ goto failed;
+ }
+
+ if (!hfp_context_get_number(context, &max)) {
+ error("hf-client: Could not get number");
+ goto failed;
+ }
+ }
+
+ if (!hfp_context_close_container(context)) {
+ error("hf-client: Could not close container");
+ goto failed;
+ }
+
+ if (!hfp_context_close_container(context)) {
+ error("hf-client: Could not close container");
+ goto failed;
+ }
+
+ set_indicator_parameters(index, name, min, max, dev->ag_ind);
+ index++;
+ }
+
+ return;
+
+failed:
+ error("hf-client: Error on CIND response");
+ slc_error(dev);
+}
+
+static void slc_brsf_cb(struct hfp_context *context, void *user_data)
+{
+ unsigned int feat;
+ struct hfp_device *dev = user_data;
+
+ DBG("");
+
+ if (hfp_context_get_number(context, &feat))
+ dev->features = feat;
+}
+
+static void slc_brsf_resp(enum hfp_result result, enum hfp_error cme_err,
+ void *user_data)
+{
+ struct hfp_device *dev = user_data;
+
+ hfp_hf_unregister(dev->hf, "+BRSF");
+
+ if (result != HFP_RESULT_OK) {
+ error("BRSF error: %d", result);
+ goto failed;
+ }
+
+ /* Continue with SLC creation */
+ if (!hfp_hf_register(dev->hf, slc_cind_cb, "+CIND", dev, NULL)) {
+ error("hf-client: Could not register for +CIND");
+ goto failed;
+ }
+
+ if (!hfp_hf_send_command(dev->hf, slc_cind_resp, dev, "AT+CIND=?")) {
+ error("hf-client: Could not send AT+CIND command");
+ goto failed;
+ }
+
+ return;
+
+failed:
+ slc_error(dev);
+}
+
+static bool create_slc(struct hfp_device *dev)
+{
+ DBG("");
+
+ if (!hfp_hf_register(dev->hf, slc_brsf_cb, "+BRSF", dev, NULL))
+ return false;
+
+ return hfp_hf_send_command(dev->hf, slc_brsf_resp, dev, "AT+BRSF=%u",
+ dev->hfp_hf_features);
+}
+
+static void hfp_disconnect_watch(void *user_data)
+{
+ DBG("");
+
+ device_destroy(user_data);
+}
+
static void connect_cb(GIOChannel *chan, GError *err, gpointer user_data)
{
struct hfp_device *dev = user_data;
@@ -73,8 +952,25 @@ static void connect_cb(GIOChannel *chan, GError *err, gpointer user_data)
goto failed;
}
+ dev->hf = hfp_hf_new(g_io_channel_unix_get_fd(chan));
+ if (!dev->hf) {
+ error("Could not create hfp io");
+ goto failed;
+ }
+
g_io_channel_set_close_on_unref(chan, FALSE);
+ hfp_hf_set_close_on_unref(dev->hf, true);
+ hfp_hf_set_disconnect_handler(dev->hf, hfp_disconnect_watch,
+ dev, NULL);
+
+ if (!create_slc(dev)) {
+ error("Could not start SLC creation");
+ hfp_hf_disconnect(dev->hf);
+ goto failed;
+ }
+
+ telephony_set_state(dev->telephony, SLC_CONNECTING);
btd_service_connecting_complete(service, 0);
return;
@@ -146,8 +1042,15 @@ static int hfp_connect(struct btd_service *service)
static int hfp_disconnect(struct btd_service *service)
{
+ struct hfp_device *dev;
+
DBG("");
+ dev = btd_service_get_user_data(service);
+
+ if (dev->hf)
+ hfp_hf_disconnect(dev->hf);
+
btd_service_disconnecting_complete(service, 0);
return 0;
@@ -166,6 +1069,7 @@ static int hfp_probe(struct btd_service *service)
return -EINVAL;
dev->telephony = telephony_new(service, dev, &hfp_callbacks);
+ dev->hfp_hf_features = HFP_HF_FEATURES;
btd_service_set_user_data(service, dev);
return 0;
--
2.43.0
^ permalink raw reply related [flat|nested] 13+ messages in thread* [RFC BlueZ 05/10] audio/hfp-hf: Add dial support
2025-05-28 8:59 [RFC BlueZ 00/10] New Telephony interface for HSP, HFP and CCP Frédéric Danis
` (3 preceding siblings ...)
2025-05-28 8:59 ` [RFC BlueZ 04/10] audio/hfp-hf: Add HFP SLC connection and event support Frédéric Danis
@ 2025-05-28 8:59 ` Frédéric Danis
2025-05-28 8:59 ` [RFC BlueZ 06/10] audio/hfp-hf: Add hangup all calls support Frédéric Danis
` (4 subsequent siblings)
9 siblings, 0 replies; 13+ messages in thread
From: Frédéric Danis @ 2025-05-28 8:59 UTC (permalink / raw)
To: linux-bluetooth
---
profiles/audio/hfp-hf.c | 72 +++++++++++++++++++++++++++++++++++++++++
1 file changed, 72 insertions(+)
diff --git a/profiles/audio/hfp-hf.c b/profiles/audio/hfp-hf.c
index 5068fff01..395f7ba61 100644
--- a/profiles/audio/hfp-hf.c
+++ b/profiles/audio/hfp-hf.c
@@ -36,6 +36,7 @@
#include "src/btd.h"
#include "src/dbus-common.h"
#include "src/device.h"
+#include "src/error.h"
#include "src/log.h"
#include "src/plugin.h"
#include "src/profile.h"
@@ -980,7 +981,78 @@ failed:
device_destroy(dev);
}
+static void hfp_dial_cb(enum hfp_result result, enum hfp_error cme_err,
+ void *user_data)
+{
+ struct call *call = user_data;
+ DBusMessage *msg = call->pending_msg;
+ DBusMessage *reply;
+ struct hfp_device *dev = telephony_get_profile_data(call->device);
+
+ DBG("");
+
+ call->pending_msg = NULL;
+
+ if (result != HFP_RESULT_OK) {
+ error("Dialing error: %d", result);
+ reply = g_dbus_create_error(msg, ERROR_INTERFACE
+ ".Failed",
+ "Dial command failed: %d", result);
+ g_dbus_send_message(btd_get_dbus_connection(), reply);
+ telephony_free_call(call);
+ return;
+ }
+
+ if (telephony_call_register_interface(call)) {
+ telephony_free_call(call);
+ return;
+ }
+
+ dev->calls = g_slist_append(dev->calls, call);
+
+ g_dbus_send_reply(btd_get_dbus_connection(), msg, DBUS_TYPE_INVALID);
+}
+
+static DBusMessage *hfp_dial(DBusConnection *conn, DBusMessage *msg,
+ void *profile_data)
+{
+ struct hfp_device *dev = profile_data;
+ const char *number;
+ struct call *call;
+
+ if (!dbus_message_get_args(msg, NULL, DBUS_TYPE_STRING, &number,
+ DBUS_TYPE_INVALID)) {
+ return btd_error_invalid_args(msg);
+ }
+
+ call = telephony_new_call(dev->telephony, CALL_STATE_DIALING, NULL);
+ call->pending_msg = dbus_message_ref(msg);
+
+ if (number != NULL && number[0] != '\0') {
+ DBG("Dialing %s", number);
+
+ call->line_id = g_strdup(number);
+
+ if (!hfp_hf_send_command(dev->hf, hfp_dial_cb, call,
+ "ATD%s;", number))
+ goto failed;
+ } else {
+ DBG("Redialing");
+
+ if (!hfp_hf_send_command(dev->hf, hfp_dial_cb, call,
+ "AT+BLDN"))
+ goto failed;
+ }
+
+ return NULL;
+
+failed:
+ telephony_free_call(call);
+ return btd_error_failed(msg, "Dial command failed");
+}
+
struct telephony_callbacks hfp_callbacks = {
+ .dial = hfp_dial,
};
static int hfp_connect(struct btd_service *service)
--
2.43.0
^ permalink raw reply related [flat|nested] 13+ messages in thread* [RFC BlueZ 06/10] audio/hfp-hf: Add hangup all calls support
2025-05-28 8:59 [RFC BlueZ 00/10] New Telephony interface for HSP, HFP and CCP Frédéric Danis
` (4 preceding siblings ...)
2025-05-28 8:59 ` [RFC BlueZ 05/10] audio/hfp-hf: Add dial support Frédéric Danis
@ 2025-05-28 8:59 ` Frédéric Danis
2025-05-28 8:59 ` [RFC BlueZ 07/10] audio/hfp-hf: Add answer a specific call support Frédéric Danis
` (3 subsequent siblings)
9 siblings, 0 replies; 13+ messages in thread
From: Frédéric Danis @ 2025-05-28 8:59 UTC (permalink / raw)
To: linux-bluetooth
---
profiles/audio/hfp-hf.c | 57 +++++++++++++++++++++++++++++++++++++++++
1 file changed, 57 insertions(+)
diff --git a/profiles/audio/hfp-hf.c b/profiles/audio/hfp-hf.c
index 395f7ba61..5cd5f663b 100644
--- a/profiles/audio/hfp-hf.c
+++ b/profiles/audio/hfp-hf.c
@@ -1051,8 +1051,65 @@ failed:
return btd_error_failed(msg, "Dial command failed");
}
+static DBusMessage *hfp_hangup_all(DBusConnection *conn, DBusMessage *msg,
+ void *profile_data)
+{
+ struct hfp_device *dev = profile_data;
+ bool found_active = FALSE;
+ bool found_held = FALSE;
+ GSList *l;
+
+ DBG("");
+
+ for (l = dev->calls; l; l = l->next) {
+ struct call *call = l->data;
+
+ switch (call->state) {
+ case CALL_STATE_ACTIVE:
+ case CALL_STATE_DIALING:
+ case CALL_STATE_ALERTING:
+ case CALL_STATE_INCOMING:
+ found_active = TRUE;
+ break;
+ case CALL_STATE_HELD:
+ case CALL_STATE_WAITING:
+ found_held = TRUE;
+ break;
+ case CALL_STATE_DISCONNECTED:
+ break;
+ }
+ }
+
+ if (!found_active && !found_held)
+ return btd_error_failed(msg, "No call to hang up");
+
+ if (found_held) {
+ if (!hfp_hf_send_command(dev->hf, cmd_complete_cb,
+ found_active ? NULL : dbus_message_ref(msg),
+ "AT+CHLD=0")) {
+ warn("Failed to hangup held calls");
+ goto failed;
+ }
+ }
+
+ if (found_active) {
+ if (!hfp_hf_send_command(dev->hf, cmd_complete_cb,
+ dbus_message_ref(msg),
+ "AT+CHUP")) {
+ warn("Failed to hangup active calls");
+ goto failed;
+ }
+ }
+
+ return NULL;
+
+failed:
+ return btd_error_failed(msg, "Hang up all command failed");
+}
+
struct telephony_callbacks hfp_callbacks = {
.dial = hfp_dial,
+ .hangup_all = hfp_hangup_all,
};
static int hfp_connect(struct btd_service *service)
--
2.43.0
^ permalink raw reply related [flat|nested] 13+ messages in thread* [RFC BlueZ 07/10] audio/hfp-hf: Add answer a specific call support
2025-05-28 8:59 [RFC BlueZ 00/10] New Telephony interface for HSP, HFP and CCP Frédéric Danis
` (5 preceding siblings ...)
2025-05-28 8:59 ` [RFC BlueZ 06/10] audio/hfp-hf: Add hangup all calls support Frédéric Danis
@ 2025-05-28 8:59 ` Frédéric Danis
2025-05-28 8:59 ` [RFC BlueZ 08/10] client/telephony: Add new submenu Frédéric Danis
` (2 subsequent siblings)
9 siblings, 0 replies; 13+ messages in thread
From: Frédéric Danis @ 2025-05-28 8:59 UTC (permalink / raw)
To: linux-bluetooth
---
profiles/audio/hfp-hf.c | 22 ++++++++++++++++++++++
1 file changed, 22 insertions(+)
diff --git a/profiles/audio/hfp-hf.c b/profiles/audio/hfp-hf.c
index 5cd5f663b..5737c60cf 100644
--- a/profiles/audio/hfp-hf.c
+++ b/profiles/audio/hfp-hf.c
@@ -1107,9 +1107,31 @@ failed:
return btd_error_failed(msg, "Hang up all command failed");
}
+static DBusMessage *call_answer(DBusConnection *conn, DBusMessage *msg,
+ void *call_data)
+{
+ struct call *call = call_data;
+ struct hfp_device *dev = telephony_get_profile_data(call->device);
+
+ DBG("");
+
+ if (call->state != CALL_STATE_INCOMING)
+ return btd_error_failed(msg, "Invalid state call");
+
+ if (!hfp_hf_send_command(dev->hf, cmd_complete_cb,
+ dbus_message_ref(msg), "ATA"))
+ goto failed;
+
+ return NULL;
+
+failed:
+ return btd_error_failed(msg, "Answer command failed");
+}
+
struct telephony_callbacks hfp_callbacks = {
.dial = hfp_dial,
.hangup_all = hfp_hangup_all,
+ .call_answer = call_answer,
};
static int hfp_connect(struct btd_service *service)
--
2.43.0
^ permalink raw reply related [flat|nested] 13+ messages in thread* [RFC BlueZ 08/10] client/telephony: Add new submenu
2025-05-28 8:59 [RFC BlueZ 00/10] New Telephony interface for HSP, HFP and CCP Frédéric Danis
` (6 preceding siblings ...)
2025-05-28 8:59 ` [RFC BlueZ 07/10] audio/hfp-hf: Add answer a specific call support Frédéric Danis
@ 2025-05-28 8:59 ` Frédéric Danis
2025-05-28 8:59 ` [RFC BlueZ 09/10] audio/hfp-hf: Remove call interface during profile disconnection Frédéric Danis
2025-05-28 8:59 ` [RFC BlueZ 10/10] audio/hfp-hf: Create existing call during SLC phase Frédéric Danis
9 siblings, 0 replies; 13+ messages in thread
From: Frédéric Danis @ 2025-05-28 8:59 UTC (permalink / raw)
To: linux-bluetooth
---
Makefile.tools | 9 +-
client/bluetoothctl-telephony.rst | 95 ++++++
client/main.c | 3 +
client/telephony.c | 524 ++++++++++++++++++++++++++++++
client/telephony.h | 12 +
5 files changed, 640 insertions(+), 3 deletions(-)
create mode 100644 client/bluetoothctl-telephony.rst
create mode 100644 client/telephony.c
create mode 100644 client/telephony.h
diff --git a/Makefile.tools b/Makefile.tools
index e60c31b1d..0f4eeb063 100644
--- a/Makefile.tools
+++ b/Makefile.tools
@@ -15,7 +15,8 @@ client_bluetoothctl_SOURCES = client/main.c \
client/player.h client/player.c \
client/mgmt.h client/mgmt.c \
client/assistant.h client/assistant.c \
- client/hci.h client/hci.c
+ client/hci.h client/hci.c \
+ client/telephony.h client/telephony.c
client_bluetoothctl_LDADD = lib/libbluetooth-internal.la \
gdbus/libgdbus-internal.la src/libshared-glib.la \
$(GLIB_LIBS) $(DBUS_LIBS) -lreadline
@@ -351,7 +352,8 @@ man_MANS += tools/rctest.1 tools/l2ping.1 tools/btattach.1 tools/isotest.1 \
client/bluetoothctl-advertise.1 client/bluetoothctl-endpoint.1 \
client/bluetoothctl-gatt.1 client/bluetoothctl-player.1 \
client/bluetoothctl-scan.1 client/bluetoothctl-transport.1 \
- client/bluetoothctl-assistant.1 client/bluetoothctl-hci.1
+ client/bluetoothctl-assistant.1 client/bluetoothctl-hci.1 \
+ client/bluetoothctl-telephony.1
endif
@@ -484,7 +486,8 @@ manual_pages += tools/hciattach.1 tools/hciconfig.1 \
client/bluetoothctl-scan.1 \
client/bluetoothctl-transport.1 \
client/bluetoothctl-assistant.1 \
- client/bluetoothctl-hci.1
+ client/bluetoothctl-hci.1 \
+ client/bluetoothctl-telephony.1
if HID2HCI
udevdir = $(UDEV_DIR)
diff --git a/client/bluetoothctl-telephony.rst b/client/bluetoothctl-telephony.rst
new file mode 100644
index 000000000..a6870e781
--- /dev/null
+++ b/client/bluetoothctl-telephony.rst
@@ -0,0 +1,95 @@
+======================
+bluetoothctl-telephony
+======================
+
+-----------------
+Telephony Submenu
+-----------------
+
+:Version: BlueZ
+:Copyright: Free use of this software is granted under the terms of the GNU
+ Lesser General Public Licenses (LGPL).
+:Date: May 2025
+:Manual section: 1
+:Manual group: Linux System Administration
+
+SYNOPSIS
+========
+
+**bluetoothctl** [--options] [telephony.commands]
+
+Telephony Commands
+==================
+
+list
+----
+
+List available audio gateways.
+
+:Usage: **> list**
+
+show
+----
+
+Show audio gateway information.
+
+:Usage: **> show [audiogw]**
+
+select
+------
+
+Select default audio gateway.
+
+:Usage: **> select <audiogw>**
+
+dial
+----
+
+Dial number.
+
+:Usage: **> dial <number> [audiogw]**
+
+hangup-all
+----------
+
+Hangup all calls.
+
+:Usage: **> hangup-all**
+
+list-calls
+----------
+
+List available calls.
+
+:Usage: **> list-calls**
+
+show-call
+---------
+
+Show call information.
+
+:Usage: **> show-call <call>**
+
+answer
+------
+
+Answer call.
+
+:Usage: **> answer <call>**
+
+hangup
+------
+
+Hangup call.
+
+:Usage: **> hangup <call>**
+
+RESOURCES
+=========
+
+http://www.bluez.org
+
+REPORTING BUGS
+==============
+
+linux-bluetooth@vger.kernel.org
diff --git a/client/main.c b/client/main.c
index c2321952b..93fb434a9 100644
--- a/client/main.c
+++ b/client/main.c
@@ -38,6 +38,7 @@
#include "mgmt.h"
#include "assistant.h"
#include "hci.h"
+#include "telephony.h"
/* String display constants */
#define COLORED_NEW COLOR_GREEN "NEW" COLOR_OFF
@@ -3467,6 +3468,7 @@ int main(int argc, char *argv[])
mgmt_add_submenu();
assistant_add_submenu();
hci_add_submenu();
+ telephony_add_submenu();
bt_shell_set_prompt(PROMPT_OFF, NULL);
bt_shell_handle_non_interactive_help();
@@ -3507,6 +3509,7 @@ int main(int argc, char *argv[])
mgmt_remove_submenu();
assistant_remove_submenu();
hci_remove_submenu();
+ telephony_remove_submenu();
g_dbus_client_unref(client);
diff --git a/client/telephony.c b/client/telephony.c
new file mode 100644
index 000000000..eb4c032b0
--- /dev/null
+++ b/client/telephony.c
@@ -0,0 +1,524 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ *
+ * BlueZ - Bluetooth protocol stack for Linux
+ *
+ * Copyright © 2025 Collabora Ltd.
+ *
+ *
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#define _GNU_SOURCE
+#include <stdlib.h>
+
+#include <glib.h>
+
+#include "gdbus/gdbus.h"
+
+#include "src/shared/shell.h"
+#include "print.h"
+#include "telephony.h"
+
+/* String display constants */
+#define COLORED_NEW COLOR_GREEN "NEW" COLOR_OFF
+#define COLORED_CHG COLOR_YELLOW "CHG" COLOR_OFF
+#define COLORED_DEL COLOR_RED "DEL" COLOR_OFF
+
+#define BLUEZ_TELEPHONY_AG_INTERFACE "org.bluez.TelephonyAg1"
+#define BLUEZ_TELEPHONY_CALL_INTERFACE "org.bluez.TelephonyCall1"
+
+static DBusConnection *dbus_conn;
+static GDBusProxy *default_ag;
+static GList *ags;
+static GList *calls;
+
+static GDBusClient *client;
+
+static bool check_default_ag(void)
+{
+ if (!default_ag) {
+ bt_shell_printf("No default audio gateway available\n");
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+static char *generic_generator(const char *text, int state, GList *source)
+{
+ static int index;
+
+ if (!source)
+ return NULL;
+
+ if (!state)
+ index = 0;
+
+ return g_dbus_proxy_path_lookup(source, &index, text);
+}
+
+static char *ag_generator(const char *text, int state)
+{
+ return generic_generator(text, state, ags);
+}
+
+static char *call_generator(const char *text, int state)
+{
+ return generic_generator(text, state, calls);
+}
+
+static char *proxy_description(GDBusProxy *proxy, const char *title,
+ const char *description)
+{
+ const char *path;
+
+ path = g_dbus_proxy_get_path(proxy);
+
+ return g_strdup_printf("%s%s%s%s %s ",
+ description ? "[" : "",
+ description ? : "",
+ description ? "] " : "",
+ title, path);
+}
+
+static void print_ag(void *data, void *user_data)
+{
+ GDBusProxy *proxy = data;
+ const char *description = user_data;
+ char *str;
+
+ str = proxy_description(proxy, "Telephony", description);
+
+ bt_shell_printf("%s%s\n", str,
+ default_ag == proxy ? "[default]" : "");
+
+ g_free(str);
+}
+
+static void print_call(void *data, void *user_data)
+{
+ GDBusProxy *proxy = data;
+ const char *description = user_data;
+ const char *path, *line_id;
+ DBusMessageIter iter;
+
+ path = g_dbus_proxy_get_path(proxy);
+
+ if (g_dbus_proxy_get_property(proxy, "LineIdentification", &iter))
+ dbus_message_iter_get_basic(&iter, &line_id);
+ else
+ line_id = "<unknown>";
+
+ bt_shell_printf("%s%s%sCall %s %s\n", description ? "[" : "",
+ description ? : "",
+ description ? "] " : "",
+ path, line_id);
+}
+
+static void cmd_list(int argc, char *arg[])
+{
+ g_list_foreach(ags, print_ag, NULL);
+
+ return bt_shell_noninteractive_quit(EXIT_SUCCESS);
+}
+
+static void cmd_show(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_AG_INTERFACE);
+ if (!proxy) {
+ bt_shell_printf("Audio gateway %s not available\n",
+ argv[1]);
+ return bt_shell_noninteractive_quit(EXIT_FAILURE);
+ }
+ }
+
+ bt_shell_printf("Audio gateway %s\n", g_dbus_proxy_get_path(proxy));
+
+ print_property(proxy, "State");
+ print_property(proxy, "Service");
+ print_property(proxy, "Signal");
+ print_property(proxy, "Roaming");
+ print_property(proxy, "BattChg");
+
+ return bt_shell_noninteractive_quit(EXIT_SUCCESS);
+}
+
+static void cmd_select(int argc, char *argv[])
+{
+ GDBusProxy *proxy;
+
+ proxy = g_dbus_proxy_lookup(ags, NULL, argv[1],
+ BLUEZ_TELEPHONY_AG_INTERFACE);
+ if (proxy == NULL) {
+ bt_shell_printf("Audio gateway %s not available\n", argv[1]);
+ return bt_shell_noninteractive_quit(EXIT_FAILURE);
+ }
+
+ if (default_ag == proxy)
+ return bt_shell_noninteractive_quit(EXIT_SUCCESS);
+
+ default_ag = proxy;
+ print_ag(proxy, NULL);
+
+ return bt_shell_noninteractive_quit(EXIT_SUCCESS);
+}
+
+static void dial_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 answer: %s\n", error.name);
+ dbus_error_free(&error);
+ return bt_shell_noninteractive_quit(EXIT_FAILURE);
+ }
+
+ bt_shell_printf("Dial successful\n");
+
+ return bt_shell_noninteractive_quit(EXIT_FAILURE);
+}
+
+static void dial_setup(DBusMessageIter *iter, void *user_data)
+{
+ const char *number = user_data;
+
+ dbus_message_iter_append_basic(iter, DBUS_TYPE_STRING, &number);
+}
+
+static void cmd_dial(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_AG_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, "Dial", dial_setup,
+ dial_reply, argv[1], NULL) == FALSE) {
+ bt_shell_printf("Failed to dial\n");
+ return bt_shell_noninteractive_quit(EXIT_FAILURE);
+ }
+
+ bt_shell_printf("Attempting to dial\n");
+}
+
+static void hangupall_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 answer: %s\n", error.name);
+ dbus_error_free(&error);
+ return bt_shell_noninteractive_quit(EXIT_FAILURE);
+ }
+
+ bt_shell_printf("Hangup all successful\n");
+
+ return bt_shell_noninteractive_quit(EXIT_FAILURE);
+}
+
+static void cmd_hangupall(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_AG_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, "HangupAll", NULL,
+ hangupall_reply, NULL, NULL) == FALSE) {
+ bt_shell_printf("Failed to hangup all calls\n");
+ return bt_shell_noninteractive_quit(EXIT_FAILURE);
+ }
+
+ bt_shell_printf("Attempting to hangup all calls\n");
+}
+
+static void cmd_list_calls(int argc, char *arg[])
+{
+ g_list_foreach(calls, print_call, NULL);
+
+ return bt_shell_noninteractive_quit(EXIT_SUCCESS);
+}
+
+static void cmd_show_call(int argc, char *argv[])
+{
+ GDBusProxy *proxy;
+
+ if (argc < 2)
+ return bt_shell_noninteractive_quit(EXIT_FAILURE);
+
+ proxy = g_dbus_proxy_lookup(ags, NULL, argv[1],
+ BLUEZ_TELEPHONY_CALL_INTERFACE);
+ if (!proxy) {
+ bt_shell_printf("Call %s not available\n", argv[1]);
+ return bt_shell_noninteractive_quit(EXIT_FAILURE);
+ }
+
+ bt_shell_printf("Call %s\n", g_dbus_proxy_get_path(proxy));
+
+ print_property(proxy, "LineIdentification");
+ print_property(proxy, "IncomingLine");
+ print_property(proxy, "Name");
+ print_property(proxy, "Multiparty");
+ print_property(proxy, "State");
+
+ return bt_shell_noninteractive_quit(EXIT_SUCCESS);
+}
+
+static void 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 answer: %s\n", error.name);
+ dbus_error_free(&error);
+ return bt_shell_noninteractive_quit(EXIT_FAILURE);
+ }
+
+ bt_shell_printf("Answer successful\n");
+
+ return bt_shell_noninteractive_quit(EXIT_FAILURE);
+}
+
+static void cmd_answer_call(int argc, char *argv[])
+{
+ GDBusProxy *proxy;
+
+ if (argc < 2)
+ return bt_shell_noninteractive_quit(EXIT_FAILURE);
+
+ proxy = g_dbus_proxy_lookup(calls, NULL, argv[1],
+ BLUEZ_TELEPHONY_CALL_INTERFACE);
+ if (!proxy) {
+ bt_shell_printf("Call %s not available\n", argv[1]);
+ return bt_shell_noninteractive_quit(EXIT_FAILURE);
+ }
+
+ if (g_dbus_proxy_method_call(proxy, "Answer", NULL,
+ answer_reply, NULL, NULL) == FALSE) {
+ bt_shell_printf("Failed to answer call\n");
+ return bt_shell_noninteractive_quit(EXIT_FAILURE);
+ }
+
+ bt_shell_printf("Attempting to answer\n");
+}
+
+static void hangup_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 answer: %s\n", error.name);
+ dbus_error_free(&error);
+ return bt_shell_noninteractive_quit(EXIT_FAILURE);
+ }
+
+ bt_shell_printf("Hangup successful\n");
+
+ return bt_shell_noninteractive_quit(EXIT_FAILURE);
+}
+
+static void cmd_hangup_call(int argc, char *argv[])
+{
+ GDBusProxy *proxy;
+
+ if (argc < 2)
+ return bt_shell_noninteractive_quit(EXIT_FAILURE);
+
+ proxy = g_dbus_proxy_lookup(calls, NULL, argv[1],
+ BLUEZ_TELEPHONY_CALL_INTERFACE);
+ if (!proxy) {
+ bt_shell_printf("Call %s not available\n", argv[1]);
+ return bt_shell_noninteractive_quit(EXIT_FAILURE);
+ }
+
+ if (g_dbus_proxy_method_call(proxy, "Hangup", NULL,
+ hangup_reply, NULL, NULL) == FALSE) {
+ bt_shell_printf("Failed to hangup call\n");
+ return bt_shell_noninteractive_quit(EXIT_FAILURE);
+ }
+
+ bt_shell_printf("Attempting to hangup\n");
+}
+
+static void ag_added(GDBusProxy *proxy)
+{
+ ags = g_list_append(ags, proxy);
+
+ if (default_ag == NULL)
+ default_ag = proxy;
+
+ print_ag(proxy, COLORED_NEW);
+}
+
+static void call_added(GDBusProxy *proxy)
+{
+ calls = g_list_append(calls, proxy);
+
+ print_call(proxy, COLORED_NEW);
+}
+
+static void proxy_added(GDBusProxy *proxy, void *user_data)
+{
+ const char *interface;
+
+ interface = g_dbus_proxy_get_interface(proxy);
+
+ if (!strcmp(interface, BLUEZ_TELEPHONY_AG_INTERFACE))
+ ag_added(proxy);
+ else if (!strcmp(interface, BLUEZ_TELEPHONY_CALL_INTERFACE))
+ call_added(proxy);
+}
+
+static void ag_removed(GDBusProxy *proxy)
+{
+ print_ag(proxy, COLORED_DEL);
+
+ if (default_ag == proxy)
+ default_ag = NULL;
+
+ ags = g_list_remove(ags, proxy);
+}
+
+static void call_removed(GDBusProxy *proxy)
+{
+ calls = g_list_remove(calls, proxy);
+
+ print_call(proxy, COLORED_DEL);
+}
+
+static void proxy_removed(GDBusProxy *proxy, void *user_data)
+{
+ const char *interface;
+
+ interface = g_dbus_proxy_get_interface(proxy);
+
+ if (!strcmp(interface, BLUEZ_TELEPHONY_AG_INTERFACE))
+ ag_removed(proxy);
+ else if (!strcmp(interface, BLUEZ_TELEPHONY_CALL_INTERFACE))
+ call_removed(proxy);
+}
+
+static void ag_property_changed(GDBusProxy *proxy, const char *name,
+ DBusMessageIter *iter)
+{
+ char *str;
+
+ str = proxy_description(proxy, "audiogw", COLORED_CHG);
+ print_iter(str, name, iter);
+ g_free(str);
+}
+
+static void call_property_changed(GDBusProxy *proxy, const char *name,
+ DBusMessageIter *iter)
+{
+ char *str;
+
+ str = proxy_description(proxy, "call", COLORED_CHG);
+ print_iter(str, name, iter);
+ g_free(str);
+}
+
+static void property_changed(GDBusProxy *proxy, const char *name,
+ DBusMessageIter *iter, void *user_data)
+{
+ const char *interface;
+
+ interface = g_dbus_proxy_get_interface(proxy);
+
+ if (!strcmp(interface, BLUEZ_TELEPHONY_AG_INTERFACE))
+ ag_property_changed(proxy, name, iter);
+ else if (!strcmp(interface, BLUEZ_TELEPHONY_CALL_INTERFACE))
+ call_property_changed(proxy, name, iter);
+}
+
+static void telephony_menu_pre_run(const struct bt_shell_menu *menu)
+{
+ dbus_conn = bt_shell_get_env("DBUS_CONNECTION");
+ if (!dbus_conn || client)
+ return;
+
+ client = g_dbus_client_new(dbus_conn, "org.bluez", "/org/bluez");
+
+ g_dbus_client_set_proxy_handlers(client, proxy_added, proxy_removed,
+ property_changed, NULL);
+}
+
+static const struct bt_shell_menu telephony_menu = {
+ .name = "telephony",
+ .desc = "Telephony Submenu",
+ .pre_run = telephony_menu_pre_run,
+ .entries = {
+ { "list", NULL, cmd_list, "List available audio gateway" },
+ { "show", "[audiogw]", cmd_show, "Audio gateway information",
+ ag_generator},
+ { "select", "<audiogw>", cmd_select,
+ "Select default audio gateway",
+ ag_generator},
+ { "dial", "<number> [audiogw]", cmd_dial, "Dial number",
+ ag_generator},
+ { "hangup-all", "[audiogw]", cmd_hangupall, "Hangup all calls",
+ ag_generator},
+ { "list-calls", NULL, cmd_list_calls, "List calls" },
+ { "show-call", "<call>", cmd_show_call, "Show call information",
+ call_generator},
+ { "answer", "<call>", cmd_answer_call, "Answer call",
+ call_generator},
+ { "hangup", "<call>", cmd_hangup_call, "Hangup call",
+ call_generator},
+ {} },
+};
+
+void telephony_add_submenu(void)
+{
+ bt_shell_add_submenu(&telephony_menu);
+}
+
+void telephony_remove_submenu(void)
+{
+ g_dbus_client_unref(client);
+}
diff --git a/client/telephony.h b/client/telephony.h
new file mode 100644
index 000000000..a869da240
--- /dev/null
+++ b/client/telephony.h
@@ -0,0 +1,12 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ *
+ * BlueZ - Bluetooth protocol stack for Linux
+ *
+ * Copyright © 2025 Collabora Ltd.
+ *
+ *
+ */
+
+void telephony_add_submenu(void);
+void telephony_remove_submenu(void);
--
2.43.0
^ permalink raw reply related [flat|nested] 13+ messages in thread* [RFC BlueZ 09/10] audio/hfp-hf: Remove call interface during profile disconnection
2025-05-28 8:59 [RFC BlueZ 00/10] New Telephony interface for HSP, HFP and CCP Frédéric Danis
` (7 preceding siblings ...)
2025-05-28 8:59 ` [RFC BlueZ 08/10] client/telephony: Add new submenu Frédéric Danis
@ 2025-05-28 8:59 ` Frédéric Danis
2025-05-28 8:59 ` [RFC BlueZ 10/10] audio/hfp-hf: Create existing call during SLC phase Frédéric Danis
9 siblings, 0 replies; 13+ messages in thread
From: Frédéric Danis @ 2025-05-28 8:59 UTC (permalink / raw)
To: linux-bluetooth
---
profiles/audio/hfp-hf.c | 11 +++++++++++
1 file changed, 11 insertions(+)
diff --git a/profiles/audio/hfp-hf.c b/profiles/audio/hfp-hf.c
index 5737c60cf..f05e5d38f 100644
--- a/profiles/audio/hfp-hf.c
+++ b/profiles/audio/hfp-hf.c
@@ -1191,6 +1191,15 @@ static int hfp_connect(struct btd_service *service)
return telephony_register_interface(dev->telephony);
}
+static void remove_calls(gpointer data, gpointer user_data)
+{
+ struct call *call = data;
+ struct hfp_device *dev = user_data;
+
+ dev->calls = g_slist_remove(dev->calls, call);
+ telephony_call_unregister_interface(call);
+}
+
static int hfp_disconnect(struct btd_service *service)
{
struct hfp_device *dev;
@@ -1199,6 +1208,8 @@ static int hfp_disconnect(struct btd_service *service)
dev = btd_service_get_user_data(service);
+ g_slist_foreach(dev->calls, remove_calls, dev);
+
if (dev->hf)
hfp_hf_disconnect(dev->hf);
--
2.43.0
^ permalink raw reply related [flat|nested] 13+ messages in thread* [RFC BlueZ 10/10] audio/hfp-hf: Create existing call during SLC phase
2025-05-28 8:59 [RFC BlueZ 00/10] New Telephony interface for HSP, HFP and CCP Frédéric Danis
` (8 preceding siblings ...)
2025-05-28 8:59 ` [RFC BlueZ 09/10] audio/hfp-hf: Remove call interface during profile disconnection Frédéric Danis
@ 2025-05-28 8:59 ` Frédéric Danis
9 siblings, 0 replies; 13+ messages in thread
From: Frédéric Danis @ 2025-05-28 8:59 UTC (permalink / raw)
To: linux-bluetooth
---
profiles/audio/hfp-hf.c | 21 ++++++++++++++++++---
1 file changed, 18 insertions(+), 3 deletions(-)
diff --git a/profiles/audio/hfp-hf.c b/profiles/audio/hfp-hf.c
index f05e5d38f..5f8fde1cd 100644
--- a/profiles/audio/hfp-hf.c
+++ b/profiles/audio/hfp-hf.c
@@ -538,10 +538,25 @@ static void ciev_call_cb(uint8_t val, void *user_data)
dev->call = !!val;
- if (dev->call == TRUE)
- g_slist_foreach(dev->calls, activate_calls, dev);
- else
+ if (dev->call == TRUE) {
+ if (dev->calls == NULL) {
+ /* Create already active call during SLC */
+ struct call *call;
+
+ call = telephony_new_call(dev->telephony,
+ CALL_STATE_ACTIVE,
+ NULL);
+ if (telephony_call_register_interface(call)) {
+ telephony_free_call(call);
+ return;
+ }
+ dev->calls = g_slist_append(dev->calls, call);
+ } else {
+ g_slist_foreach(dev->calls, activate_calls, dev);
+ }
+ } else {
g_slist_foreach(dev->calls, deactivate_active_calls, dev);
+ }
}
static void callsetup_deactivate(gpointer data, gpointer user_data)
--
2.43.0
^ permalink raw reply related [flat|nested] 13+ messages in thread