Skip to content

Instantly share code, notes, and snippets.

@parthitce
Last active May 23, 2024 13:03
Show Gist options
  • Save parthitce/eb6b751df3235f7247babc4c9aba41d8 to your computer and use it in GitHub Desktop.
Save parthitce/eb6b751df3235f7247babc4c9aba41d8 to your computer and use it in GitHub Desktop.
ConnectDevice: Connect bluetooth device without scanning
/*
* bluez_adapter_connect.c - Connect with device without StartDiscovery
* - This example registers an agen with NoInputOutput capability for the purpose of
* auto pairing
* - Use ConnectDevice method to connect with device using provided MAC address
* - Usual signal subscription to get the details of the connected device
* - Introduced new signal handler to exit the program gracefully
*
* Note: As "ConnectDevice" is new API and in experimental state (but mostly stable)
* one need to use "-E" option when starting "bluetoothd". Systems running systemd can
* edit /lib/systemd/system/bluetooth.service in ExecStart option
*
* When this API is useful?
* - When you already have the MAC address of end bluetooth Device to connect with, then
* you don't need to scan for the device (with or without filter) and connect it.
* - StartDiscovery + Pair + Connect => ConnectDevice
*
* How you will have MAC address before scanning?
* - When you have other communication (wired or wireless) medium to exchange the MAC address
* - For example, NFC OOB can be used to exchange the MAC address
* - Testing Bluetooth with same device (MAC address known)
*
* - Here Agent capability is registered as "NoInputOutput" for experimental purpose only, in
* real world scenario, Pair + Connect involves real Agents.
* - Also note, bluez_agent_call_method and bluez_adapter_call_method are two different methods doing
* the same work with difference in interface name and object path. This exist just to make the
* understanding more clear.
*
* gcc `pkg-config --cflags glib-2.0 gio-2.0` -Wall -Wextra -o ./bin/bluez_adapter_connect ./bluez_adapter_connect.c `pkg-config --libs glib-2.0 gio-2.0`
*/
#include <glib.h>
#include <gio/gio.h>
#include <signal.h>
GMainLoop *loop;
GDBusConnection *con;
static void bluez_property_value(const gchar *key, GVariant *value)
{
const gchar *type = g_variant_get_type_string(value);
g_print("\t%s : ", key);
switch(*type) {
case 'o':
case 's':
g_print("%s\n", g_variant_get_string(value, NULL));
break;
case 'b':
g_print("%d\n", g_variant_get_boolean(value));
break;
case 'u':
g_print("%d\n", g_variant_get_uint32(value));
break;
case 'a':
/* TODO Handling only 'as', but not array of dicts */
if(g_strcmp0(type, "as"))
break;
g_print("\n");
const gchar *uuid;
GVariantIter i;
g_variant_iter_init(&i, value);
while(g_variant_iter_next(&i, "s", &uuid))
g_print("\t\t%s\n", uuid);
break;
default:
g_print("Other\n");
break;
}
}
typedef void (*method_cb_t)(GObject *, GAsyncResult *, gpointer);
static int bluez_adapter_call_method(const char *method, GVariant *param, method_cb_t method_cb)
{
g_dbus_connection_call(con,
"org.bluez",
/* TODO Find the adapter path runtime */
"/org/bluez/hci0",
"org.bluez.Adapter1",
method,
param,
NULL,
G_DBUS_CALL_FLAGS_NONE,
-1,
NULL,
method_cb,
(void *)method);
return 0;
}
static void bluez_result_async_cb(GObject *con,
GAsyncResult *res,
gpointer data)
{
const gchar *key = (gchar *)data;
GVariant *result = NULL;
GError *error = NULL;
result = g_dbus_connection_call_finish((GDBusConnection *)con, res, &error);
if(error != NULL) {
g_print("Unable to get result: %s\n", error->message);
return;
}
if(result) {
result = g_variant_get_child_value(result, 0);
bluez_property_value(key, result);
}
g_variant_unref(result);
}
static void bluez_device_appeared(GDBusConnection *sig,
const gchar *sender_name,
const gchar *object_path,
const gchar *interface,
const gchar *signal_name,
GVariant *parameters,
gpointer user_data)
{
(void)sig;
(void)sender_name;
(void)object_path;
(void)interface;
(void)signal_name;
(void)user_data;
GVariantIter *interfaces;
const char *object;
const gchar *interface_name;
GVariant *properties;
g_variant_get(parameters, "(&oa{sa{sv}})", &object, &interfaces);
while(g_variant_iter_next(interfaces, "{&s@a{sv}}", &interface_name, &properties)) {
if(g_strstr_len(g_ascii_strdown(interface_name, -1), -1, "device")) {
g_print("[ %s ]\n", object);
const gchar *property_name;
GVariantIter i;
GVariant *prop_val;
g_variant_iter_init(&i, properties);
while(g_variant_iter_next(&i, "{&sv}", &property_name, &prop_val))
bluez_property_value(property_name, prop_val);
g_variant_unref(prop_val);
}
g_variant_unref(properties);
}
return;
}
#define BT_ADDRESS_STRING_SIZE 18
static void bluez_device_disappeared(GDBusConnection *sig,
const gchar *sender_name,
const gchar *object_path,
const gchar *interface,
const gchar *signal_name,
GVariant *parameters,
gpointer user_data)
{
(void)sig;
(void)sender_name;
(void)object_path;
(void)interface;
(void)signal_name;
GVariantIter *interfaces;
const char *object;
const gchar *interface_name;
char address[BT_ADDRESS_STRING_SIZE] = {'\0'};
g_variant_get(parameters, "(&oas)", &object, &interfaces);
while(g_variant_iter_next(interfaces, "s", &interface_name)) {
if(g_strstr_len(g_ascii_strdown(interface_name, -1), -1, "device")) {
int i;
char *tmp = g_strstr_len(object, -1, "dev_") + 4;
for(i = 0; *tmp != '\0'; i++, tmp++) {
if(*tmp == '_') {
address[i] = ':';
continue;
}
address[i] = *tmp;
}
g_print("\nDevice %s removed\n", address);
g_main_loop_quit((GMainLoop *)user_data);
}
}
return;
}
static void bluez_signal_adapter_changed(GDBusConnection *conn,
const gchar *sender,
const gchar *path,
const gchar *interface,
const gchar *signal,
GVariant *params,
void *userdata)
{
(void)conn;
(void)sender;
(void)path;
(void)interface;
(void)userdata;
GVariantIter *properties = NULL;
GVariantIter *unknown = NULL;
const char *iface;
const char *key;
GVariant *value = NULL;
const gchar *signature = g_variant_get_type_string(params);
if(strcmp(signature, "(sa{sv}as)") != 0) {
g_print("Invalid signature for %s: %s != %s", signal, signature, "(sa{sv}as)");
goto done;
}
g_variant_get(params, "(&sa{sv}as)", &iface, &properties, &unknown);
while(g_variant_iter_next(properties, "{&sv}", &key, &value)) {
if(!g_strcmp0(key, "Powered")) {
if(!g_variant_is_of_type(value, G_VARIANT_TYPE_BOOLEAN)) {
g_print("Invalid argument type for %s: %s != %s", key,
g_variant_get_type_string(value), "b");
goto done;
}
g_print("Adapter is Powered \"%s\"\n", g_variant_get_boolean(value) ? "on" : "off");
}
if(!g_strcmp0(key, "Discovering")) {
if(!g_variant_is_of_type(value, G_VARIANT_TYPE_BOOLEAN)) {
g_print("Invalid argument type for %s: %s != %s", key,
g_variant_get_type_string(value), "b");
goto done;
}
g_print("Adapter scan \"%s\"\n", g_variant_get_boolean(value) ? "on" : "off");
}
}
done:
if(properties != NULL)
g_variant_iter_free(properties);
if(value != NULL)
g_variant_unref(value);
}
static int bluez_adapter_set_property(const char *prop, GVariant *value)
{
GVariant *result;
GError *error = NULL;
result = g_dbus_connection_call_sync(con,
"org.bluez",
"/org/bluez/hci0",
"org.freedesktop.DBus.Properties",
"Set",
g_variant_new("(ssv)", "org.bluez.Adapter1", prop, value),
NULL,
G_DBUS_CALL_FLAGS_NONE,
-1,
NULL,
&error);
if(error != NULL)
return 1;
g_variant_unref(result);
return 0;
}
static int bluez_adapter_connect_device(char **argv)
{
int rc;
GVariantBuilder *b = g_variant_builder_new(G_VARIANT_TYPE_VARDICT);
g_variant_builder_add(b, "{sv}", "Address", g_variant_new_string(argv[1]));
GVariant *device_dict = g_variant_builder_end(b);
g_variant_builder_unref(b);
rc = bluez_adapter_call_method("ConnectDevice",
g_variant_new_tuple(&device_dict, 1),
bluez_result_async_cb);
if(rc) {
g_print("Not able to call ConnectDevice\n");
return 1;
}
return 0;
}
#define AGENT_PATH "/org/bluez/AutoPinAgent"
static int bluez_agent_call_method(const gchar *method, GVariant *param)
{
GVariant *result;
GError *error = NULL;
result = g_dbus_connection_call_sync(con,
"org.bluez",
"/org/bluez",
"org.bluez.AgentManager1",
method,
param,
NULL,
G_DBUS_CALL_FLAGS_NONE,
-1,
NULL,
&error);
if(error != NULL) {
g_print("Register %s: %s\n", AGENT_PATH, error->message);
return 1;
}
g_variant_unref(result);
return 0;
}
static int bluez_register_autopair_agent(void)
{
int rc;
rc = bluez_agent_call_method("RegisterAgent", g_variant_new("(os)", AGENT_PATH, "NoInputNoOutput"));
if(rc)
return 1;
rc = bluez_agent_call_method("RequestDefaultAgent", g_variant_new("(o)", AGENT_PATH));
if(rc) {
bluez_agent_call_method("UnregisterAgent", g_variant_new("(o)", AGENT_PATH));
return 1;
}
return 0;
}
static void cleanup_handler(int signo)
{
if (signo == SIGINT) {
g_print("received SIGINT\n");
g_main_loop_quit(loop);
}
}
int main(int argc, char **argv)
{
int rc;
guint prop_changed;
guint iface_added;
guint iface_removed;
if(signal(SIGINT, cleanup_handler) == SIG_ERR)
g_print("can't catch SIGINT\n");
con = g_bus_get_sync(G_BUS_TYPE_SYSTEM, NULL, NULL);
if(con == NULL) {
g_print("Not able to get connection to system bus\n");
return 1;
}
loop = g_main_loop_new(NULL, FALSE);
prop_changed = g_dbus_connection_signal_subscribe(con,
"org.bluez",
"org.freedesktop.DBus.Properties",
"PropertiesChanged",
NULL,
"org.bluez.Adapter1",
G_DBUS_SIGNAL_FLAGS_NONE,
bluez_signal_adapter_changed,
NULL,
NULL);
iface_added = g_dbus_connection_signal_subscribe(con,
"org.bluez",
"org.freedesktop.DBus.ObjectManager",
"InterfacesAdded",
NULL,
NULL,
G_DBUS_SIGNAL_FLAGS_NONE,
bluez_device_appeared,
loop,
NULL);
iface_removed = g_dbus_connection_signal_subscribe(con,
"org.bluez",
"org.freedesktop.DBus.ObjectManager",
"InterfacesRemoved",
NULL,
NULL,
G_DBUS_SIGNAL_FLAGS_NONE,
bluez_device_disappeared,
loop,
NULL);
rc = bluez_adapter_set_property("Powered", g_variant_new("b", TRUE));
if(rc) {
g_print("Not able to enable the adapter\n");
goto fail;
}
rc = bluez_register_autopair_agent();
if(rc) {
g_print("Not able to register default autopair agent\n");
goto fail;
}
if(argc == 2) {
rc = bluez_adapter_connect_device(argv);
if(rc)
goto fail;
}
g_main_loop_run(loop);
rc = bluez_adapter_set_property("Powered", g_variant_new("b", FALSE));
if(rc)
g_print("Not able to disable the adapter\n");
fail:
g_dbus_connection_signal_unsubscribe(con, prop_changed);
g_dbus_connection_signal_unsubscribe(con, iface_added);
g_dbus_connection_signal_unsubscribe(con, iface_removed);
g_object_unref(con);
return 0;
}
@9293746
Copy link

9293746 commented Sep 16, 2022

Hello. Thank you for making this example to help people learn. However, I haven't been able to get this to work. I am running the latest bluez (5.64) with bluetoothd launched with the "-E" CLI parameter for experimental interface enablement, on a Raspberry Pi 4b

ps aux | grep bluetoothd
root       753  0.0  0.3   7200  3536 ?        Ss   12:22   0:00 /usr/local/libexec/bluetooth/bluetoothd -E
root     17102  0.0  0.0   7348   528 pts/1    S+   13:52   0:00 grep bluetoothd

When I run the tool to connect to a bluetooth classic wireless headphones device in discoverable mode, I get the following:

./bluez_adapter_connect 1C:36:BB:32:F5:13
Adapter is Powered "on"
Unable to get result: GDBus.Error:org.bluez.Error.AlreadyExists: Already Exists

If it is instead not in pairing mode I get

./bluez_adapter_connect 1C:36:BB:32:F5:13
Unable to get result: GDBus.Error:org.bluez.Error.AlreadyExists: Already Exists

I am able to pair and connect to this same device from bluetoothctl fine when it is in pairing mode:

# bluetoothctl
Agent registered
AdvertisementMonitor path registered
[bluetooth]# connect 1C:36:BB:32:F5:13
Attempting to connect to 1C:36:BB:32:F5:13
[CHG] Device 1C:36:BB:32:F5:13 Connected: yes
Connection successful
[CHG] Device 1C:36:BB:32:F5:13 ServicesResolved: yes

And when I use this code to try and connect to a "virtual peripheral" created with the LightBlue app on my iPad, I see one of the two following ouputs.

This is what I get if I don't have bluetoothctl in "scan on" mode:

./bluez_adapter_connect 41:3E:7B:64:29:A8
Adapter is Powered "on"
Unable to get result: GDBus.Error:org.bluez.Failed: 

This is what I get if I do have bluetoothctl in "scan on" mode:

./bluez_adapter_connect 5D:39:63:B1:1D:B1
Unable to get result: GDBus.Error:org.bluez.Error.AlreadyExists: Already Exists

So three things:

  1. According to the ConnectDevice API, it needs an "AddressType" parameter and "If this parameter is not present BR/EDR device is created." So that seems to mean that this code could never work if the BDADDR is a random type address, correct?
    This seems to be confirmed based on the fact that if I add a line
g_variant_builder_add(b, "{sv}", "AddressType", g_variant_new_string("random"));

right below the "Address" line, it seems to connect to the virtual device now, based on the below output:

./bluez_adapter_connect 5D:39:63:B1:1D:B1
Adapter is Powered "on"
[ /org/bluez/hci0/dev_5D_39_63_B1_1D_B1 ]
        Address : 5D:39:63:B1:1D:B1
        AddressType : random
        Alias : 5D-39-63-B1-1D-B1
        Paired : 0
        Trusted : 0
        Blocked : 0
        LegacyPairing : 0
        Connected : 1
        UUIDs : 
        Adapter : /org/bluez/hci0
        ServicesResolved : 0
        ConnectDevice : /org/bluez/hci0/dev_5D_39_63_B1_1D_B1
  1. However, that still doesn't explain why I can't connect to the BT classic device in either pairing or non-pairing mode. Any ideas on that?

  2. Any idea why there is the "Already Exists" error if I don't add the "random" AddressType?

@9293746
Copy link

9293746 commented Sep 16, 2022

p.s. if you'd like some code to improve that TODO in bluez_property_value() I used the following improvement for case a:

                case 'a':
                        if(strcmp("UUIDs", key) == 0){
                                g_print("\n");
                                const gchar *uuid;
                                GVariantIter i;
                                g_variant_iter_init(&i, value);
                                while(g_variant_iter_next(&i, "s", &uuid))
                                        g_print("\t\t%s\n", uuid);
                        }
                        else if(strcmp("ManufacturerData", key) == 0){
                                g_print("\n");
                                GVariantIter i;
                                short manufacturer_id;
                                GVariant *byte_array;
                                g_variant_iter_init(&i, value);
                                while(g_variant_iter_next(&i, "{qv}", &manufacturer_id, &byte_array)){
                                        g_print("\t\tManufacturerID: 0x%04x (%s)\n", manufacturer_id, bt_compidtostr(manufacturer_id));
                                        g_print("\t\tHex byte array:");
                                        char byte;
                                        GVariantIter ii;
                                        g_variant_iter_init(&ii, byte_array);
                                        while(g_variant_iter_next(&ii, "y", &byte)){
                                                g_print(" %02x", byte);
                                        }
                                        g_print("\n");
                                }
                        }
                        else if(strcmp("ServiceData", key) == 0){
                                g_print("\n");
                                GVariantIter i;
                                const gchar *uuid;;
                                GVariant *byte_array;
                                g_variant_iter_init(&i, value);
                                while(g_variant_iter_next(&i, "{sv}", &uuid, &byte_array)){
                                        g_print("\t\tUUID: %s\n", uuid);  
                                        g_print("\t\tHex byte array:");
                                        char byte;
                                        GVariantIter ii;
                                        g_variant_iter_init(&ii, byte_array);
                                        while(g_variant_iter_next(&ii, "y", &byte)){
                                                g_print(" %02x", byte);
                                        }
                                        g_print("\n");
                                }
                        }
                        else{   
                                g_print("value type '%s'\n", g_variant_get_type_string(value));
                                g_print("something unknown...\n");
                        }
                        break;

(I don't really understand how to just dynamically parse anything that comes in, because e.g. if I run g_variant_get_type_string(value) I get things like "a{qv}" or "a{sv}", but I need things like "{qv}" or "{sv}" and it seems weird and gross to just like chop off the "a" at the front of the string.)

@parthitce
Copy link
Author

@9293746 Unfortunately I haven't followed the bluez project changes for quit sometime. I would recommend to read the API level changes in doc/adapter.txt and doc/device.txt in bluez

@prabhat935
Copy link

Do you have any code example in which we are sending advertisements as peripheral role and connecting.

@parthitce
Copy link
Author

Do you have any code example in which we are sending advertisements as peripheral role and connecting.

No, I don't have that. But it should be possible to write one based on gdbus GATT API's. You can write me over email to parthiban@linumiz.com if you need help.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment