@@ -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)
new file mode 100644
@@ -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
@@ -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);
new file mode 100644
@@ -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);
+}
new file mode 100644
@@ -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);