Compare commits

...

19 Commits

Author SHA1 Message Date
Olivier Martin f99558d9b8 gattlib-py: Make pylint pass (and fix issue) 2024-04-24 14:37:28 +02:00
Olivier Martin b3c5d2d1ed common/gattlib_common: Add support to compare short UUID with long one 2024-04-24 14:37:28 +02:00
Olivier Martin 9fb48aacb6 gattlib-py/examples/advertisement_data: Fix example 2024-04-24 14:37:28 +02:00
Olivier Martin dbe599dbfb examples/notification: Port example to new gattlib API 2024-04-24 14:37:28 +02:00
Olivier Martin f79e90ce02 gattlib-py: Added support for manufacturer data from GATT advertising 2024-04-11 22:01:28 +02:00
Olivier Martin 35566d198a gattlib-py: Fix setup.py when building source package 2024-04-11 22:01:28 +02:00
Olivier Martin f4ed88eb31 mainloop/gattlib_glib_mainloop: Fix non initialized variable 2024-04-11 22:01:28 +02:00
Olivier Martin aaab2dc74e gattlib_adapter_close: Do not block mutex while waiting for scan_loop_thread to complete 2024-04-11 00:28:24 +02:00
Olivier Martin 880f1d2cd0 Add support to retrieve all manufacturer data from GATT advertisement packets 2024-04-10 10:53:45 +02:00
Olivier Martin 8a108495a1 Change 'gattlib_devices_dump_state()' to 'gattlib_adapter_dump_state()' 2024-04-10 10:53:30 +02:00
Olivier Martin 880ff269e5 gattlib_char: Ensure there is no buffer overflow when we initialize list of GATT characteristic 2024-04-10 10:51:58 +02:00
Olivier Martin 76353f8659 Do not build Python support by default and build examples by default 2024-04-10 10:50:48 +02:00
Olivier Martin db04a0eb5c Introduce gattlib_connection_is_valid() to not access 'connection->device' 2024-04-08 23:10:47 +02:00
Olivier Martin 2d771d9390 Fix gattlib_connection_is_connected 2024-04-08 23:09:09 +02:00
Olivier Martin 5406a97e57 More logging 2024-04-08 23:07:57 +02:00
Olivier Martin 4acf4aa0ab connection: Ensure device_object_path is not freed multiple time 2024-04-08 23:07:57 +02:00
Olivier Martin f609f7d507 adapter: Added support to open multiple time the same adapter 2024-04-08 23:07:57 +02:00
Olivier Martin dc009029fa Log when the adapter or device has been released 2024-04-08 12:01:53 +02:00
Olivier Martin cdd62f6d35 Return specific error code when the adapter or device has been removed 2024-04-08 10:08:57 +02:00
30 changed files with 1438 additions and 429 deletions

View File

@ -6,14 +6,14 @@ jobs:
steps:
- uses: actions/checkout@v4
- run: sudo apt install libbluetooth-dev
- run: mkdir build && pushd build && cmake -DCMAKE_BUILD_TYPE=Debug .. && make
- run: mkdir build && pushd build && cmake -DCMAKE_BUILD_TYPE=Debug -DGATTLIB_PYTHON_INTERFACE=ON .. && make
build-release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: sudo apt install libbluetooth-dev doxygen
- run: mkdir build && pushd build && cmake -DCMAKE_BUILD_TYPE=Release -DGATTLIB_BUILD_DOCS=ON .. && make
- run: mkdir build && pushd build && cmake -DCMAKE_BUILD_TYPE=Release -DGATTLIB_BUILD_DOCS=ON -DGATTLIB_PYTHON_INTERFACE=ON .. && make
- run: pushd build && cpack ..
if: startsWith(github.ref, 'refs/tags/')
env:
@ -41,7 +41,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- run: sudo apt install libbluetooth-dev
- run: mkdir build && pushd build && cmake -DGATTLIB_FORCE_DBUS=TRUE -DCMAKE_BUILD_TYPE=Release .. && make
- run: mkdir build && pushd build && cmake -DGATTLIB_FORCE_DBUS=TRUE -DCMAKE_BUILD_TYPE=Release -DGATTLIB_PYTHON_INTERFACE=ON .. && make
build-release-without-python-support:
runs-on: ubuntu-latest
@ -50,6 +50,14 @@ jobs:
- run: sudo apt install libbluetooth-dev
- run: mkdir build && pushd build && cmake -DCMAKE_BUILD_TYPE=Release -DGATTLIB_PYTHON_INTERFACE=OFF .. && make
test-pylint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: python3 -m pip install PyGObject>=3.44.0
- run: python3 -m pip install pylint
- run: python3 -m pylint gattlib-py/gattlib --rcfile gattlib-py/.pylintrc
generate-python-binary-packages:
runs-on: ubuntu-latest
steps:

View File

@ -13,10 +13,10 @@ include(CrossCompilation.cmake)
project(gattlib)
#TODO: Gattlib examples must be ported to new gattlib_connect()
option(GATTLIB_BUILD_EXAMPLES "Build GattLib examples" NO)
option(GATTLIB_BUILD_EXAMPLES "Build GattLib examples" YES)
option(GATTLIB_SHARED_LIB "Build GattLib as a shared library" YES)
option(GATTLIB_BUILD_DOCS "Build GattLib docs" NO)
option(GATTLIB_PYTHON_INTERFACE "Build GattLib Python Interface" YES)
option(GATTLIB_PYTHON_INTERFACE "Build GattLib Python Interface" NO)
option(GATTLIB_ENABLE_ADDRESS_SANITIZER "Enable address sanitizer" NO)
find_package(PkgConfig REQUIRED)
@ -119,7 +119,7 @@ if(GATTLIB_BUILD_EXAMPLES)
add_subdirectory(examples/find_eddystone)
add_subdirectory(examples/read_write)
#add_subdirectory(examples/read_write_memory)
#add_subdirectory(examples/notification)
add_subdirectory(examples/notification)
#add_subdirectory(examples/nordic_uart)
add_subdirectory(tests/test_continuous_connection)

View File

@ -303,15 +303,14 @@ int gattlib_discover_desc(gattlib_connection_t* connection, gattlib_descriptor_t
* @param mac_address is the MAC address of the device to get the RSSI
* @param advertisement_data is an array of Service UUID and their respective data
* @param advertisement_data_count is the number of elements in the advertisement_data array
* @param manufacturer_id is the ID of the Manufacturer ID
* @param manufacturer_data is the data following Manufacturer ID
* @param manufacturer_data_size is the size of manufacturer_data
* @param manufacturer_data is an array of `gattlib_manufacturer_data_t`
* @param manufacturer_data_count is the number of entry in `gattlib_manufacturer_data_t` array
*
* @return GATTLIB_SUCCESS on success or GATTLIB_* error code
*/
int gattlib_get_advertisement_data(gattlib_connection_t *connection,
gattlib_advertisement_data_t **advertisement_data, size_t *advertisement_data_count,
uint16_t *manufacturer_id, uint8_t **manufacturer_data, size_t *manufacturer_data_size)
gattlib_manufacturer_data_t** manufacturer_data, size_t* manufacturer_data_count)
{
return GATTLIB_NOT_SUPPORTED;
}
@ -323,15 +322,14 @@ int gattlib_get_advertisement_data(gattlib_connection_t *connection,
* @param mac_address is the MAC address of the device to get the RSSI
* @param advertisement_data is an array of Service UUID and their respective data
* @param advertisement_data_count is the number of elements in the advertisement_data array
* @param manufacturer_id is the ID of the Manufacturer ID
* @param manufacturer_data is the data following Manufacturer ID
* @param manufacturer_data_size is the size of manufacturer_data
* @param manufacturer_data is an array of `gattlib_manufacturer_data_t`
* @param manufacturer_data_count is the number of entry in `gattlib_manufacturer_data_t` array
*
* @return GATTLIB_SUCCESS on success or GATTLIB_* error code
*/
int gattlib_get_advertisement_data_from_mac(gattlib_adapter_t* adapter, const char *mac_address,
gattlib_advertisement_data_t **advertisement_data, size_t *advertisement_data_count,
uint16_t *manufacturer_id, uint8_t **manufacturer_data, size_t *manufacturer_data_size)
gattlib_manufacturer_data_t** manufacturer_data, size_t* manufacturer_data_count)
{
return GATTLIB_NOT_SUPPORTED;
}

View File

@ -28,8 +28,8 @@ mkdir ${gattlib_py_package_dir}/ci/
cp -r ${ROOT_PATH}/ci/install-bluez.sh ${gattlib_py_package_dir}/ci/
# Create MANIFEST.in
rm -f MANIFEST.in
cat <<EOT >> MANIFEST.in
rm -f ${gattlib_py_package_dir}/MANIFEST.in
cat <<EOT >> ${gattlib_py_package_dir}/MANIFEST.in
graft common
graft bluez
graft dbus
@ -51,7 +51,7 @@ python3 -m cibuildwheel --output-dir dist
python setup.py sdist
# Move generated artifact to project root path
ls dist/*
rm -Rf ${ROOT_PATH}/dist
mv dist ${ROOT_PATH}
popd

View File

@ -54,11 +54,14 @@ static gpointer _gattlib_connected_device_thread(gpointer data) {
g_rec_mutex_lock(&m_gattlib_mutex);
if (!gattlib_connection_is_connected(connection)) {
GATTLIB_LOG(GATTLIB_ERROR, "_gattlib_connected_device_thread: Device is not connected (state:%s)",
device_state_str[connection->device->state]);
g_rec_mutex_unlock(&m_gattlib_mutex);
return NULL;
}
if (!gattlib_has_valid_handler(&connection->on_connection)) {
GATTLIB_LOG(GATTLIB_ERROR, "_gattlib_connected_device_thread: Handler is not valid");
g_rec_mutex_unlock(&m_gattlib_mutex);
return NULL;
}
@ -84,7 +87,8 @@ static void* _connected_device_thread_args_allocator(va_list args) {
}
void gattlib_on_connected_device(gattlib_connection_t* connection) {
if (!gattlib_device_is_valid(connection->device)) {
if (!gattlib_connection_is_valid(connection)) {
GATTLIB_LOG(GATTLIB_ERROR, "gattlib_on_connected_device: Device is not valid");
return;
}

View File

@ -33,7 +33,8 @@ void gattlib_disconnected_device_python_callback(gattlib_connection_t* connectio
void gattlib_on_disconnected_device(gattlib_connection_t* connection) {
g_rec_mutex_lock(&m_gattlib_mutex);
if (!gattlib_connection_is_connected(connection)) {
if (!gattlib_connection_is_valid(connection)) {
GATTLIB_LOG(GATTLIB_ERROR, "gattlib_on_disconnected_device: Device not valid");
g_rec_mutex_unlock(&m_gattlib_mutex);
return;
}

View File

@ -20,8 +20,9 @@ int gattlib_register_notification(gattlib_connection_t* connection, gattlib_even
goto EXIT;
}
if (!gattlib_device_is_valid(connection->device)) {
ret = GATTLIB_INVALID_PARAMETER;
if (!gattlib_connection_is_valid(connection)) {
GATTLIB_LOG(GATTLIB_ERROR, "gattlib_register_notification: Device not valid");
ret = GATTLIB_DEVICE_DISCONNECTED;
goto EXIT;
}
@ -57,8 +58,9 @@ int gattlib_register_indication(gattlib_connection_t* connection, gattlib_event_
goto EXIT;
}
if (!gattlib_device_is_valid(connection->device)) {
ret = GATTLIB_INVALID_PARAMETER;
if (!gattlib_connection_is_valid(connection)) {
GATTLIB_LOG(GATTLIB_ERROR, "gattlib_register_indication: Device not valid");
ret = GATTLIB_DEVICE_DISCONNECTED;
goto EXIT;
}
@ -91,8 +93,9 @@ int gattlib_register_on_disconnect(gattlib_connection_t *connection, gattlib_dis
goto EXIT;
}
if (!gattlib_device_is_valid(connection->device)) {
ret = GATTLIB_INVALID_PARAMETER;
if (!gattlib_connection_is_valid(connection)) {
GATTLIB_LOG(GATTLIB_ERROR, "gattlib_register_on_disconnect: Device not valid");
ret = GATTLIB_DEVICE_DISCONNECTED;
goto EXIT;
}
@ -158,9 +161,54 @@ int gattlib_string_to_uuid(const char *str, size_t n, uuid_t *uuid) {
return ret;
}
int gattlib_uuid_to_uuid128(const uuid_t *uuid, uuid_t *long_uuid) {
if (uuid->type == SDP_UUID128) {
memcpy(long_uuid, uuid, sizeof(uuid_t));
return 0;
}
long_uuid->type = SDP_UUID128;
long_uuid->value.uuid128.data[0] = 0xEF;
long_uuid->value.uuid128.data[1] = 0x68;
long_uuid->value.uuid128.data[2] = 0x00;
long_uuid->value.uuid128.data[3] = 0x00;
long_uuid->value.uuid128.data[4] = 0x9B;
long_uuid->value.uuid128.data[5] = 0x35;
long_uuid->value.uuid128.data[6] = 0x49;
long_uuid->value.uuid128.data[7] = 0x33;
long_uuid->value.uuid128.data[8] = 0x9B;
long_uuid->value.uuid128.data[9] = 0x10;
long_uuid->value.uuid128.data[10] = 0x52;
long_uuid->value.uuid128.data[11] = 0xFF;
long_uuid->value.uuid128.data[12] = 0xA9;
long_uuid->value.uuid128.data[13] = 0x74;
long_uuid->value.uuid128.data[14] = 0x00;
long_uuid->value.uuid128.data[15] = 0x42;
if (uuid->type == SDP_UUID32) {
long_uuid->value.uuid128.data[0] = (uuid->value.uuid32 >> 24) & 0xFF;
long_uuid->value.uuid128.data[1] = (uuid->value.uuid32 >> 16) & 0xFF;
long_uuid->value.uuid128.data[2] = (uuid->value.uuid32 >> 8) & 0xFF;
long_uuid->value.uuid128.data[3] = uuid->value.uuid32 & 0xFF;
} else if (uuid->type == SDP_UUID16) {
long_uuid->value.uuid128.data[2] = uuid->value.uuid16 >> 8;
long_uuid->value.uuid128.data[3] = uuid->value.uuid16 & 0xFF;
}
return 0;
}
int gattlib_uuid_cmp(const uuid_t *uuid1, const uuid_t *uuid2) {
if (uuid1->type != uuid2->type) {
return 1;
// Convert all UUID to UUID128 format to be compared
uuid_t uuid128_1, uuid128_2;
gattlib_uuid_to_uuid128(uuid1, &uuid128_1);
gattlib_uuid_to_uuid128(uuid2, &uuid128_2);
if (memcmp(&uuid128_1.value.uuid128, &uuid128_2.value.uuid128, sizeof(uuid1->value.uuid128)) == 0) {
return 0;
} else {
return 2;
}
} else if (uuid1->type == SDP_UUID16) {
if (uuid1->value.uuid16 == uuid2->value.uuid16) {
return 0;

View File

@ -10,6 +10,38 @@
// It could happen when using Python wrapper.
GSList *m_adapter_list;
static int stricmp(char const *a, char const *b) {
for (;; a++, b++) {
int d = tolower((unsigned char)*a) - tolower((unsigned char)*b);
if (d != 0 || !*a)
return d;
}
}
static gint _is_adapter_id(gconstpointer a, gconstpointer b) {
const gattlib_adapter_t* adapter = a;
const char* adapter_id = b;
return stricmp(adapter->id, adapter_id);
}
gattlib_adapter_t* gattlib_adapter_from_id(const char* adapter_id) {
gattlib_adapter_t* adapter = NULL;
g_rec_mutex_lock(&m_gattlib_mutex);
GSList *adapter_entry = g_slist_find_custom(m_adapter_list, adapter_id, _is_adapter_id);
if (adapter_entry == NULL) {
goto EXIT;
}
adapter = adapter_entry->data;
EXIT:
g_rec_mutex_unlock(&m_gattlib_mutex);
return adapter;
}
bool gattlib_adapter_is_valid(gattlib_adapter_t* adapter) {
bool is_valid;
@ -71,21 +103,56 @@ bool gattlib_device_is_valid(gattlib_device_t* device) {
return device_is_valid.found;
}
struct _connection_is_valid {
gattlib_connection_t* connection;
bool is_valid;
};
static gint _is_device_connection(gconstpointer a, gconstpointer b) {
const gattlib_device_t* device = a;
return (&device->connection == b) ? 0 : -1; // We need to return 0 when it matches
}
static void _gattlib_connection_is_valid(gpointer data, gpointer user_data) {
gattlib_adapter_t* adapter = data;
struct _connection_is_valid* connection_is_valid = user_data;
//printf("_gattlib_connection_is_connected: Check device in adapter:%s\n", adapter->id);
GSList *device_entry = g_slist_find_custom(adapter->devices, connection_is_valid->connection, _is_device_connection);
if (device_entry == NULL) {
//printf("_gattlib_connection_is_connected: Did not find device %s\n", connection_is_connected->connection->device->device_id);
return;
}
connection_is_valid->is_valid = true;
}
bool gattlib_connection_is_valid(gattlib_connection_t* connection) {
struct _connection_is_valid connection_is_valid = {
.connection = connection,
.is_valid = false
};
g_rec_mutex_lock(&m_gattlib_mutex);
//printf("gattlib_connection_is_connected A");
g_slist_foreach(m_adapter_list, _gattlib_connection_is_valid, &connection_is_valid);
//printf("gattlib_connection_is_connected B");
g_rec_mutex_unlock(&m_gattlib_mutex);
return connection_is_valid.is_valid;
}
struct _connection_is_connected {
gattlib_connection_t* connection;
bool is_connected;
};
static gint _is_device_connection(gconstpointer a, gconstpointer b) {
const gattlib_device_t* device = a;
return (&device->connection == b);
}
static void _gattlib_connection_is_connected(gpointer data, gpointer user_data) {
gattlib_adapter_t* adapter = data;
struct _connection_is_connected* connection_is_connected = user_data;
GSList *device_entry = g_slist_find_custom(adapter->devices, user_data, _is_device_connection);
GSList *device_entry = g_slist_find_custom(adapter->devices, connection_is_connected->connection, _is_device_connection);
if (device_entry == NULL) {
return;
}

View File

@ -50,7 +50,8 @@ int gattlib_device_set_state(gattlib_adapter_t* adapter, const char* device_id,
g_rec_mutex_lock(&m_gattlib_mutex);
if (!gattlib_adapter_is_valid(adapter)) {
ret = GATTLIB_INVALID_PARAMETER;
GATTLIB_LOG(GATTLIB_ERROR, "gattlib_device_set_state: Adapter not valid");
ret = GATTLIB_ADAPTER_CLOSE;
goto EXIT;
}
@ -183,10 +184,13 @@ static void _gattlib_device_dump_state(gpointer data, gpointer user_data) {
GATTLIB_LOG(GATTLIB_DEBUG, "\t%s: %s", device->device_id, device_state_str[device->state]);
}
void gattlib_devices_dump_state(gattlib_adapter_t* adapter) {
void gattlib_adapter_dump_state(gattlib_adapter_t* adapter) {
g_rec_mutex_lock(&m_gattlib_mutex);
GATTLIB_LOG(GATTLIB_ERROR, "gattlib_devices_dump_state: Adapter is_scanning:%d", adapter->backend.ble_scan.is_scanning);
if (!gattlib_adapter_is_valid(adapter)) {
GATTLIB_LOG(GATTLIB_ERROR, "gattlib_devices_dump_state: Adapter not valid");
goto EXIT;
}

View File

@ -30,27 +30,29 @@ static void on_eddystone_discovered_device(gattlib_adapter_t* adapter, const cha
struct on_eddystone_discovered_device_arg *callback_data = user_data;
gattlib_advertisement_data_t *advertisement_data = NULL;
size_t advertisement_data_count;
uint16_t manufacturer_id;
uint8_t *manufacturer_data = NULL;
size_t manufacturer_data_size;
gattlib_manufacturer_data_t* manufacturer_data = NULL;
size_t manufacturer_data_count = 0;
int ret;
ret = gattlib_get_advertisement_data_from_mac(adapter, addr,
&advertisement_data, &advertisement_data_count,
&manufacturer_id, &manufacturer_data, &manufacturer_data_size);
&manufacturer_data, &manufacturer_data_count);
if (ret != 0) {
return;
}
callback_data->discovered_device_cb(adapter, addr, name,
advertisement_data, advertisement_data_count,
manufacturer_id, manufacturer_data, manufacturer_data_size,
manufacturer_data, manufacturer_data_count,
callback_data->user_data);
if (advertisement_data != NULL) {
free(advertisement_data);
}
if (manufacturer_data != NULL) {
for (uintptr_t i = 0; i < manufacturer_data_count; i++) {
free(manufacturer_data[i].data);
}
free(manufacturer_data);
}
}

View File

@ -68,6 +68,9 @@ struct _gattlib_adapter {
// Context specific to the backend implementation (eg: dbus backend)
struct _gattlib_adapter_backend backend;
// BLE adapter id (could be its DBUS device path on Linux)
char* id;
// BLE adapter name
char* name;
@ -121,6 +124,7 @@ extern GSList *m_adapter_list;
// This structure is used for inter-thread communication
extern struct gattlib_signal m_gattlib_signal;
gattlib_adapter_t* gattlib_adapter_from_id(const char* adapter_id);
bool gattlib_adapter_is_valid(gattlib_adapter_t* adapter);
bool gattlib_adapter_is_scanning(gattlib_adapter_t* adapter);
int gattlib_adapter_ref(gattlib_adapter_t* adapter);
@ -129,6 +133,13 @@ int gattlib_adapter_unref(gattlib_adapter_t* adapter);
bool gattlib_device_is_valid(gattlib_device_t* device);
int gattlib_device_ref(gattlib_device_t* device);
int gattlib_device_unref(gattlib_device_t* device);
/**
* This function is similar to 'gattlib_device_is_valid()' except we check if
* the connection (connected or not) still belongs to a valid device.
*
* It is to avoid to use 'connection->device' when the device has been freed
*/
bool gattlib_connection_is_valid(gattlib_connection_t* connection);
bool gattlib_connection_is_connected(gattlib_connection_t* connection);
void gattlib_handler_dispatch_to_thread(struct gattlib_handler* handler, void (*python_callback)(),
@ -154,7 +165,7 @@ int gattlib_devices_are_disconnected(gattlib_adapter_t* adapter);
int gattlib_devices_free(gattlib_adapter_t* adapter);
#ifdef DEBUG
void gattlib_devices_dump_state(gattlib_adapter_t* adapter);
void gattlib_adapter_dump_state(gattlib_adapter_t* adapter);
#endif
#if defined(WITH_PYTHON)

View File

@ -34,14 +34,18 @@ int gattlib_mainloop(void* (*task)(void* arg), void *arg) {
GError* error;
if (m_main_loop != NULL) {
GATTLIB_LOG(GATTLIB_ERROR, "Main loop is already running: %s", error->message);
g_error_free(error);
GATTLIB_LOG(GATTLIB_ERROR, "Main loop is already running");
return GATTLIB_BUSY;
}
m_main_loop = g_main_loop_new(NULL, FALSE);
GThread *task_thread = g_thread_try_new("gattlib_task", _execute_task, &execute_task_arg, &error);
if (task_thread == NULL) {
GATTLIB_LOG(GATTLIB_ERROR, "Could not create task for main loop: %s", error->message);
g_error_free(error);
return GATTLIB_UNEXPECTED;
}
g_main_loop_run(m_main_loop);
g_main_loop_unref(m_main_loop);

View File

@ -21,7 +21,8 @@ static void _on_device_connect(gattlib_connection_t* connection) {
g_rec_mutex_lock(&m_gattlib_mutex);
if (!gattlib_device_is_valid(connection->device)) {
if (!gattlib_connection_is_valid(connection)) {
GATTLIB_LOG(GATTLIB_ERROR, "_on_device_connect: Device not valid");
goto EXIT;
}
@ -141,7 +142,8 @@ static gboolean _stop_connect_func(gpointer data) {
g_rec_mutex_lock(&m_gattlib_mutex);
if (!gattlib_device_is_valid(connection->device)) {
if (!gattlib_connection_is_valid(connection)) {
GATTLIB_LOG(GATTLIB_ERROR, "_stop_connect_func: Device not valid");
goto EXIT;
}
@ -199,7 +201,8 @@ int gattlib_connect(gattlib_adapter_t* adapter, const char *dst,
g_rec_mutex_lock(&m_gattlib_mutex);
if (!gattlib_adapter_is_valid(adapter)) {
ret = GATTLIB_INVALID_PARAMETER;
GATTLIB_LOG(GATTLIB_ERROR, "gattlib_connect: Adapter not valid");
ret = GATTLIB_ADAPTER_CLOSE;
goto EXIT;
}
@ -285,7 +288,10 @@ int gattlib_connect(gattlib_adapter_t* adapter, const char *dst,
return GATTLIB_SUCCESS;
FREE_DEVICE:
g_rec_mutex_lock(&m_gattlib_mutex);
free(device->connection.backend.device_object_path);
device->connection.backend.device_object_path = NULL;
g_rec_mutex_unlock(&m_gattlib_mutex);
// destroy default adapter
if(adapter == NULL) {
@ -324,7 +330,11 @@ void gattlib_connection_free(gattlib_connection_t* connection) {
connection->backend.connection_timeout_id = 0;
}
free(connection->backend.device_object_path);
if (connection->backend.device_object_path != NULL) {
free(connection->backend.device_object_path);
connection->backend.device_object_path = NULL;
}
g_list_free_full(connection->backend.dbus_objects, g_object_unref);
disconnect_all_notifications(&connection->backend);
@ -414,9 +424,10 @@ int gattlib_discover_primary(gattlib_connection_t* connection, gattlib_primary_s
return GATTLIB_INVALID_PARAMETER;
}
if (!gattlib_device_is_valid(connection->device)) {
if (!gattlib_connection_is_valid(connection)) {
GATTLIB_LOG(GATTLIB_ERROR, "gattlib_discover_primary: Device not valid");
g_rec_mutex_unlock(&m_gattlib_mutex);
return GATTLIB_INVALID_PARAMETER;
return GATTLIB_DEVICE_DISCONNECTED;
}
// Increase 'bluez_device' reference counter to avoid to keep the lock longer
@ -506,8 +517,9 @@ int gattlib_discover_primary(gattlib_connection_t* connection, gattlib_primary_s
goto EXIT;
}
if (!gattlib_device_is_valid(connection->device)) {
ret = GATTLIB_INVALID_PARAMETER;
if (!gattlib_connection_is_valid(connection)) {
GATTLIB_LOG(GATTLIB_ERROR, "gattlib_discover_primary: Device not valid");
ret = GATTLIB_DEVICE_DISCONNECTED;
goto EXIT;
}
@ -667,9 +679,10 @@ int gattlib_discover_char_range(gattlib_connection_t* connection, uint16_t start
// Increase bluez_device object reference counter to avoid to keep locking the mutex
g_rec_mutex_lock(&m_gattlib_mutex);
if (!gattlib_device_is_valid(connection->device)) {
if (!gattlib_connection_is_valid(connection)) {
GATTLIB_LOG(GATTLIB_ERROR, "gattlib_discover_char_range: Device not valid");
g_rec_mutex_unlock(&m_gattlib_mutex);
return GATTLIB_INVALID_PARAMETER;
return GATTLIB_DEVICE_DISCONNECTED;
}
OrgBluezDevice1* bluez_device = connection->backend.bluez_device;
@ -830,7 +843,7 @@ EXIT:
static void add_characteristics_from_service(struct _gattlib_connection_backend* backend, GDBusObjectManager *device_manager,
const char* service_object_path,
unsigned int start, unsigned int end,
gattlib_characteristic_t* characteristic_list, int* count)
gattlib_characteristic_t* characteristic_list, int count_max, int* count)
{
GError *error = NULL;
@ -872,6 +885,7 @@ static void add_characteristics_from_service(struct _gattlib_connection_backend*
continue;
}
if (strcmp(property_value, service_object_path)) {
// This GATT characteristic is not for the current GATT service. Ignore it
g_object_unref(characteristic);
continue;
} else {
@ -886,6 +900,12 @@ static void add_characteristics_from_service(struct _gattlib_connection_backend*
continue;
}
// Sanity check to avoid buffer overflow
if (*count >= count_max) {
GATTLIB_LOG(GATTLIB_WARNING, "Skip GATT characteristic %s. Not enough space in the GATT characteristic array.", object_path);
continue;
}
characteristic_list[*count].handle = handle;
characteristic_list[*count].value_handle = handle;
characteristic_list[*count].properties = 0;
@ -926,8 +946,9 @@ int gattlib_discover_char_range(gattlib_connection_t* connection, uint16_t start
g_rec_mutex_lock(&m_gattlib_mutex);
if (!gattlib_device_is_valid(connection->device)) {
ret = GATTLIB_INVALID_PARAMETER;
if (!gattlib_connection_is_valid(connection)) {
GATTLIB_LOG(GATTLIB_ERROR, "gattlib_discover_char_range: Device not valid");
ret = GATTLIB_DEVICE_DISCONNECTED;
goto EXIT;
}
@ -982,6 +1003,12 @@ int gattlib_discover_char_range(gattlib_connection_t* connection, uint16_t start
if (interface) {
g_object_unref(interface);
// Sanity check to avoid buffer overflow
if (count >= count_max) {
GATTLIB_LOG(GATTLIB_WARNING, "Skip battery characteristic. Not enough space in the GATT characteristic array.");
continue;
}
characteristic_list[count].handle = 0;
characteristic_list[count].value_handle = 0;
characteristic_list[count].properties = GATTLIB_CHARACTERISTIC_READ | GATTLIB_CHARACTERISTIC_NOTIFY;
@ -1024,7 +1051,8 @@ int gattlib_discover_char_range(gattlib_connection_t* connection, uint16_t start
}
// Add all characteristics attached to this service
add_characteristics_from_service(&connection->backend, device_manager, object_path, start, end, characteristic_list, &count);
add_characteristics_from_service(&connection->backend, device_manager, object_path, start, end, characteristic_list,
count_max, &count);
g_object_unref(service_proxy);
}
@ -1057,8 +1085,9 @@ int get_bluez_device_from_mac(struct _gattlib_adapter *adapter, const char *mac_
g_rec_mutex_lock(&m_gattlib_mutex);
if (!gattlib_adapter_is_valid(adapter)) {
GATTLIB_LOG(GATTLIB_ERROR, "get_bluez_device_from_mac: Adapter not valid");
g_rec_mutex_unlock(&m_gattlib_mutex);
return GATTLIB_INVALID_PARAMETER;
return GATTLIB_ADAPTER_CLOSE;
}
if (adapter->backend.adapter_proxy == NULL) {
@ -1104,9 +1133,10 @@ int gattlib_get_rssi(gattlib_connection_t *connection, int16_t *rssi)
return GATTLIB_INVALID_PARAMETER;
}
if (!gattlib_device_is_valid(connection->device)) {
if (!gattlib_connection_is_valid(connection)) {
GATTLIB_LOG(GATTLIB_ERROR, "gattlib_get_rssi: Device not valid");
g_rec_mutex_unlock(&m_gattlib_mutex);
return GATTLIB_INVALID_PARAMETER;
return GATTLIB_DEVICE_DISCONNECTED;
}
// device is actually a GObject. Increasing its reference counter prevents to

View File

@ -28,10 +28,21 @@ int gattlib_adapter_open(const char* adapter_name, gattlib_adapter_t** adapter)
adapter_name = GATTLIB_DEFAULT_ADAPTER;
}
GATTLIB_LOG(GATTLIB_DEBUG, "Open bluetooth adapter %s", adapter_name);
snprintf(object_path, sizeof(object_path), "/org/bluez/%s", adapter_name);
// Check if adapter has already be loaded
g_rec_mutex_lock(&m_gattlib_mutex);
*adapter = gattlib_adapter_from_id(object_path);
if (*adapter != NULL) {
GATTLIB_LOG(GATTLIB_DEBUG, "Bluetooth adapter %s has already been opened. Re-use it", adapter_name);
gattlib_adapter_ref(*adapter);
g_rec_mutex_unlock(&m_gattlib_mutex);
return GATTLIB_SUCCESS;
}
g_rec_mutex_unlock(&m_gattlib_mutex);
GATTLIB_LOG(GATTLIB_DEBUG, "Open bluetooth adapter %s", adapter_name);
adapter_proxy = org_bluez_adapter1_proxy_new_for_bus_sync(
G_BUS_TYPE_SYSTEM, G_DBUS_PROXY_FLAGS_NONE,
"org.bluez",
@ -58,6 +69,7 @@ int gattlib_adapter_open(const char* adapter_name, gattlib_adapter_t** adapter)
}
// Initialize stucture
gattlib_adapter->id = strdup(object_path);
gattlib_adapter->name = strdup(adapter_name);
gattlib_adapter->reference_counter = 1;
gattlib_adapter->backend.adapter_proxy = adapter_proxy;
@ -145,6 +157,7 @@ static void device_manager_on_added_device1_signal(const char* device1_path, gat
g_rec_mutex_lock(&m_gattlib_mutex);
if (!gattlib_adapter_is_valid(gattlib_adapter)) {
GATTLIB_LOG(GATTLIB_ERROR, "device_manager_on_added_device1_signal: Adapter not valid");
g_rec_mutex_unlock(&m_gattlib_mutex);
g_object_unref(device1);
return;
@ -225,6 +238,7 @@ on_interface_proxy_properties_changed (GDBusObjectManagerClient *device_manager,
g_rec_mutex_lock(&m_gattlib_mutex);
if (!gattlib_adapter_is_valid(gattlib_adapter)) {
GATTLIB_LOG(GATTLIB_ERROR, "on_interface_proxy_properties_changed: Adapter not valid");
goto EXIT;
}
@ -298,6 +312,7 @@ static gboolean _stop_scan_on_timeout(gpointer data) {
g_rec_mutex_lock(&m_gattlib_mutex);
if (!gattlib_adapter_is_valid(gattlib_adapter)) {
GATTLIB_LOG(GATTLIB_ERROR, "_stop_scan_on_timeout: Adapter not valid");
g_rec_mutex_unlock(&m_gattlib_mutex);
return FALSE;
}
@ -329,6 +344,7 @@ static void* _ble_scan_loop_thread(void* args) {
g_rec_mutex_lock(&m_gattlib_mutex);
if (!gattlib_adapter_is_valid(gattlib_adapter)) {
GATTLIB_LOG(GATTLIB_ERROR, "_ble_scan_loop_thread: Adapter not valid (1)");
goto EXIT;
}
@ -356,6 +372,7 @@ static void* _ble_scan_loop_thread(void* args) {
// Confirm gattlib_adapter is still valid
if (!gattlib_adapter_is_valid(gattlib_adapter)) {
GATTLIB_LOG(GATTLIB_ERROR, "_ble_scan_loop_thread: Adapter not valid (2)");
goto EXIT;
}
@ -490,7 +507,8 @@ int gattlib_adapter_scan_enable_with_filter(gattlib_adapter_t* adapter, uuid_t *
g_rec_mutex_lock(&m_gattlib_mutex);
if (!gattlib_adapter_is_valid(adapter)) {
ret = GATTLIB_INVALID_PARAMETER;
GATTLIB_LOG(GATTLIB_ERROR, "gattlib_adapter_scan_enable_with_filter: Adapter not valid (1)");
ret = GATTLIB_ADAPTER_CLOSE;
goto EXIT;
}
@ -524,7 +542,8 @@ int gattlib_adapter_scan_enable_with_filter(gattlib_adapter_t* adapter, uuid_t *
// Ensure the adapter is still valid when we get the mutex again
if (!gattlib_adapter_is_valid(adapter)) {
ret = GATTLIB_INVALID_PARAMETER;
GATTLIB_LOG(GATTLIB_ERROR, "gattlib_adapter_scan_enable_with_filter: Adapter not valid (2)");
ret = GATTLIB_ADAPTER_CLOSE;
goto EXIT;
}
@ -546,7 +565,8 @@ int gattlib_adapter_scan_enable_with_filter_non_blocking(gattlib_adapter_t* adap
g_rec_mutex_lock(&m_gattlib_mutex);
if (!gattlib_adapter_is_valid(adapter)) {
ret = GATTLIB_INVALID_PARAMETER;
GATTLIB_LOG(GATTLIB_ERROR, "gattlib_adapter_scan_enable_with_filter_non_blocking: Adapter not valid (2)");
ret = GATTLIB_ADAPTER_CLOSE;
goto EXIT;
}
@ -584,7 +604,8 @@ int gattlib_adapter_scan_disable(gattlib_adapter_t* adapter) {
g_rec_mutex_lock(&m_gattlib_mutex);
if (!gattlib_adapter_is_valid(adapter)) {
ret = GATTLIB_INVALID_PARAMETER;
GATTLIB_LOG(GATTLIB_ERROR, "gattlib_adapter_scan_disable: Adapter not valid");
ret = GATTLIB_ADAPTER_CLOSE;
goto EXIT;
}
@ -642,15 +663,11 @@ int gattlib_adapter_close(gattlib_adapter_t* adapter) {
bool are_devices_disconnected;
int ret = GATTLIB_SUCCESS;
//
// TODO: Should we use reference counting to be able to open multiple times an adapter
// without freeing it on the first gattlib_adapter_close()
//
g_rec_mutex_lock(&m_gattlib_mutex);
if (!gattlib_adapter_is_valid(adapter)) {
ret = GATTLIB_INVALID_PARAMETER;
GATTLIB_LOG(GATTLIB_ERROR, "gattlib_adapter_close: Adapter not valid");
ret = GATTLIB_ADAPTER_CLOSE;
goto EXIT;
}
@ -670,18 +687,26 @@ int gattlib_adapter_close(gattlib_adapter_t* adapter) {
GATTLIB_LOG(GATTLIB_DEBUG, "Close bluetooth adapter %s", adapter->name);
if (adapter->backend.ble_scan.is_scanning) {
GATTLIB_LOG(GATTLIB_DEBUG, "Bluetooth adapter %s was scanning. We stop the scan", adapter->name);
gattlib_adapter_scan_disable(adapter);
// We must release gattlib mutex to not block the library
// We must also increase reference counter to not wait for a thread that has been freed
GThread *scan_loop_thread = adapter->backend.ble_scan.scan_loop_thread;
g_thread_ref(scan_loop_thread);
g_rec_mutex_unlock(&m_gattlib_mutex);
_wait_scan_loop_stop_scanning(adapter);
g_thread_join(adapter->backend.ble_scan.scan_loop_thread);
// At this stage scan_loop_thread should have completed
g_rec_mutex_lock(&m_gattlib_mutex);
g_thread_unref(scan_loop_thread);
}
// Unref/Free the adapter
gattlib_adapter_unref(adapter);
// Remove adapter from the global list
m_adapter_list = g_slist_remove(m_adapter_list, adapter);
EXIT:
g_rec_mutex_unlock(&m_gattlib_mutex);
return ret;
@ -720,6 +745,11 @@ int gattlib_adapter_unref(gattlib_adapter_t* adapter) {
adapter->backend.adapter_proxy = NULL;
}
if (adapter->id != NULL) {
free(adapter->id);
adapter->id = NULL;
}
if (adapter->name != NULL) {
free(adapter->name);
adapter->name = NULL;
@ -727,9 +757,12 @@ int gattlib_adapter_unref(gattlib_adapter_t* adapter) {
gattlib_devices_free(adapter);
// Remove adapter from the global list
m_adapter_list = g_slist_remove(m_adapter_list, adapter);
free(adapter);
EXIT:
g_rec_mutex_unlock(&m_gattlib_mutex);
return ret;
}
}

View File

@ -10,14 +10,14 @@
int gattlib_get_advertisement_data(gattlib_connection_t *connection,
gattlib_advertisement_data_t **advertisement_data, size_t *advertisement_data_count,
uint16_t *manufacturer_id, uint8_t **manufacturer_data, size_t *manufacturer_data_size)
gattlib_manufacturer_data_t** manufacturer_data, size_t* manufacturer_data_count)
{
return GATTLIB_NOT_SUPPORTED;
}
int gattlib_get_advertisement_data_from_mac(gattlib_adapter_t* adapter, const char *mac_address,
gattlib_advertisement_data_t **advertisement_data, size_t *advertisement_data_count,
uint16_t *manufacturer_id, uint8_t **manufacturer_data, size_t *manufacturer_data_size)
gattlib_manufacturer_data_t** manufacturer_data, size_t* manufacturer_data_count)
{
return GATTLIB_NOT_SUPPORTED;
}
@ -26,7 +26,7 @@ int gattlib_get_advertisement_data_from_mac(gattlib_adapter_t* adapter, const ch
int get_advertisement_data_from_device(OrgBluezDevice1 *bluez_device1,
gattlib_advertisement_data_t **advertisement_data, size_t *advertisement_data_count,
uint16_t *manufacturer_id, uint8_t **manufacturer_data, size_t *manufacturer_data_size)
gattlib_manufacturer_data_t** manufacturer_data, size_t* manufacturer_data_count)
{
GVariant *manufacturer_data_variant;
GVariant *service_data_variant;
@ -35,36 +35,41 @@ int get_advertisement_data_from_device(OrgBluezDevice1 *bluez_device1,
return GATTLIB_INVALID_PARAMETER;
}
*manufacturer_id = 0;
*manufacturer_data_size = 0;
*manufacturer_data_count = 0;
*manufacturer_data = NULL;
manufacturer_data_variant = org_bluez_device1_get_manufacturer_data(bluez_device1);
if (manufacturer_data_variant != NULL) {
if (g_variant_n_children(manufacturer_data_variant) != 1) {
GATTLIB_LOG(GATTLIB_WARNING, "Manufacturer Data with multiple children: %s",
g_variant_print(manufacturer_data_variant, TRUE));
return GATTLIB_NOT_SUPPORTED;
}
GVariant* manufacturer_data_dict = g_variant_get_child_value(manufacturer_data_variant, 0);
GVariantIter *iter;
GVariant* values;
g_variant_get(manufacturer_data_dict, "{qv}", manufacturer_id, &values);
*manufacturer_data_size = g_variant_n_children(values);
*manufacturer_data = calloc(*manufacturer_data_size, sizeof(guchar));
*manufacturer_data_count = g_variant_n_children(manufacturer_data_variant);
*manufacturer_data = malloc(sizeof(gattlib_manufacturer_data_t) * (*manufacturer_data_count));
if (*manufacturer_data == NULL) {
return GATTLIB_OUT_OF_MEMORY;
}
GVariant* value;
g_variant_get(values, "ay", &iter);
size_t index = 0;
for (uintptr_t i = 0; i < *manufacturer_data_count; i++) {
GVariant* manufacturer_data_dict = g_variant_get_child_value(manufacturer_data_variant, i);
GVariantIter *iter;
GVariant* values;
uint16_t manufacturer_id = 0;
while ((value = g_variant_iter_next_value(iter)) != NULL) {
g_variant_get(value, "y", &(*manufacturer_data)[index++]);
g_variant_unref(value);
g_variant_get(manufacturer_data_dict, "{qv}", &manufacturer_id, &values);
(*manufacturer_data)[i].manufacturer_id = manufacturer_id;
(*manufacturer_data)[i].data_size = g_variant_n_children(values);
(*manufacturer_data)[i].data = calloc((*manufacturer_data)[i].data_size, sizeof(guchar));
if ((*manufacturer_data)[i].data == NULL) {
return GATTLIB_OUT_OF_MEMORY;
}
// Copy manufacturer data to structure
GVariant* value;
g_variant_get(values, "ay", &iter);
size_t index = 0;
while ((value = g_variant_iter_next_value(iter)) != NULL) {
g_variant_get(value, "y", (*manufacturer_data)[i].data[index++]);
g_variant_unref(value);
}
g_variant_iter_free(iter);
}
g_variant_iter_free(iter);
}
service_data_variant = org_bluez_device1_get_service_data(bluez_device1);
@ -114,7 +119,7 @@ int get_advertisement_data_from_device(OrgBluezDevice1 *bluez_device1,
int gattlib_get_advertisement_data(gattlib_connection_t *connection,
gattlib_advertisement_data_t **advertisement_data, size_t *advertisement_data_count,
uint16_t *manufacturer_id, uint8_t **manufacturer_data, size_t *manufacturer_data_size)
gattlib_manufacturer_data_t** manufacturer_data, size_t* manufacturer_data_count)
{
int ret;
@ -125,9 +130,9 @@ int gattlib_get_advertisement_data(gattlib_connection_t *connection,
return GATTLIB_INVALID_PARAMETER;
}
if (!gattlib_device_is_valid(connection->device)) {
if (!gattlib_connection_is_valid(connection)) {
g_rec_mutex_unlock(&m_gattlib_mutex);
return GATTLIB_INVALID_PARAMETER;
return GATTLIB_DEVICE_DISCONNECTED;
}
// device is actually a GObject. Increasing its reference counter prevents to
@ -138,7 +143,7 @@ int gattlib_get_advertisement_data(gattlib_connection_t *connection,
ret = get_advertisement_data_from_device(dbus_device,
advertisement_data, advertisement_data_count,
manufacturer_id, manufacturer_data, manufacturer_data_size);
manufacturer_data, manufacturer_data_count);
g_object_unref(dbus_device);
@ -147,7 +152,7 @@ int gattlib_get_advertisement_data(gattlib_connection_t *connection,
int gattlib_get_advertisement_data_from_mac(gattlib_adapter_t* adapter, const char *mac_address,
gattlib_advertisement_data_t **advertisement_data, size_t *advertisement_data_count,
uint16_t *manufacturer_id, uint8_t **manufacturer_data, size_t *manufacturer_data_size)
gattlib_manufacturer_data_t** manufacturer_data, size_t* manufacturer_data_count)
{
OrgBluezDevice1 *bluez_device1;
int ret;
@ -164,7 +169,7 @@ int gattlib_get_advertisement_data_from_mac(gattlib_adapter_t* adapter, const ch
ret = get_advertisement_data_from_device(bluez_device1,
advertisement_data, advertisement_data_count,
manufacturer_id, manufacturer_data, manufacturer_data_size);
manufacturer_data, manufacturer_data_count);
g_object_unref(bluez_device1);

View File

@ -38,14 +38,13 @@ static const char* adapter_name;
static void ble_advertising_device(gattlib_adapter_t* adapter, const char* addr, const char* name, void *user_data) {
gattlib_advertisement_data_t *advertisement_data;
size_t advertisement_data_count;
uint16_t manufacturer_id;
uint8_t *manufacturer_data;
size_t manufacturer_data_size;
gattlib_manufacturer_data_t* manufacturer_data = NULL;
size_t manufacturer_data_count = 0;
int ret;
ret = gattlib_get_advertisement_data_from_mac(adapter, addr,
&advertisement_data, &advertisement_data_count,
&manufacturer_id, &manufacturer_data, &manufacturer_data_size);
&manufacturer_data, &manufacturer_data_count);
if (ret != 0) {
return;
}
@ -56,10 +55,13 @@ static void ble_advertising_device(gattlib_adapter_t* adapter, const char* addr,
printf("Device %s: ", addr);
}
for (size_t i = 0; i < manufacturer_data_size; i++) {
printf("%02x ", manufacturer_data[i]);
for (size_t i = 0; i < manufacturer_data_count; i++) {
printf("- Manufacturer data for id 0x%x: ", manufacturer_data[i].manufacturer_id);
for (size_t j = 0; j < manufacturer_data[i].data_size; j++) {
printf("%02x ", manufacturer_data[i].data[j]);
}
printf("\n");
}
printf("\n");
}
static void* ble_task(void *arg) {

View File

@ -45,7 +45,7 @@ const char* m_adapter_name;
*/
void on_eddystone_found(gattlib_adapter_t* adapter, const char* addr, const char* name,
gattlib_advertisement_data_t *advertisement_data, size_t advertisement_data_count,
uint16_t manufacturer_id, uint8_t *manufacturer_data, size_t manufacturer_data_size,
gattlib_manufacturer_data_t* manufacturer_data, size_t manufacturer_data_count,
void *user_data)
{
puts("Found Eddystone device");

View File

@ -2,7 +2,7 @@
*
* GattLib - GATT Library
*
* Copyright (C) 2016-2021 Olivier Martin <olivier@labapart.org>
* Copyright (C) 2016-2024 Olivier Martin <olivier@labapart.org>
*
*
* This program is free software; you can redistribute it and/or modify
@ -22,8 +22,9 @@
*/
#include <assert.h>
#include <ctype.h>
#include <glib.h>
#include <signal.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
@ -33,13 +34,28 @@
#include "gattlib.h"
static uuid_t g_notify_uuid;
static uuid_t g_write_uuid;
#define BLE_SCAN_TIMEOUT 10
static GMainLoop *m_main_loop;
static struct {
char *adapter_name;
char* mac_address;
uuid_t gatt_notification_uuid;
uuid_t gatt_write_uuid;
long int gatt_write_data;
} m_argument;
void notification_handler(const uuid_t* uuid, const uint8_t* data, size_t data_length, void* user_data) {
int i;
// Declaration of thread condition variable
static pthread_cond_t m_connection_terminated = PTHREAD_COND_INITIALIZER;
// declaring mutex
static pthread_mutex_t m_connection_terminated_lock = PTHREAD_MUTEX_INITIALIZER;
static void usage(char *argv[]) {
printf("%s <device_address> <notification_characteristic_uuid> [<write_characteristic_uuid> <write_characteristic_data>]\n", argv[0]);
}
static void notification_handler(const uuid_t* uuid, const uint8_t* data, size_t data_length, void* user_data) {
uintptr_t i;
printf("Notification Handler: ");
@ -49,94 +65,127 @@ void notification_handler(const uuid_t* uuid, const uint8_t* data, size_t data_l
printf("\n");
}
static void on_user_abort(int arg) {
g_main_loop_quit(m_main_loop);
static void on_device_connect(gattlib_adapter_t* adapter, const char *dst, gattlib_connection_t* connection, int error, void* user_data) {
int ret;
if (m_argument.gatt_write_data != 0) {
ret = gattlib_write_char_by_uuid(connection, &m_argument.gatt_write_uuid, &m_argument.gatt_write_data, 1);
if (ret != GATTLIB_SUCCESS) {
}
}
ret = gattlib_register_notification(connection, notification_handler, NULL);
if (ret) {
GATTLIB_LOG(GATTLIB_ERROR, "Fail to register notification callback.");
goto EXIT;
}
ret = gattlib_notification_start(connection, &m_argument.gatt_notification_uuid);
if (ret) {
GATTLIB_LOG(GATTLIB_ERROR, "Fail to start notification.");
goto EXIT;
}
GATTLIB_LOG(GATTLIB_INFO, "Wait for notification for 20 seconds...");
g_usleep(20 * G_USEC_PER_SEC);
EXIT:
gattlib_disconnect(connection, false /* wait_disconnection */);
pthread_mutex_lock(&m_connection_terminated_lock);
pthread_cond_signal(&m_connection_terminated);
pthread_mutex_unlock(&m_connection_terminated_lock);
}
static void usage(char *argv[]) {
printf("%s <device_address> <notification_characteristic_uuid> [<write_characteristic_uuid> <write_characteristic_hex_data> ...]\n", argv[0]);
static int stricmp(char const *a, char const *b) {
for (;; a++, b++) {
int d = tolower((unsigned char)*a) - tolower((unsigned char)*b);
if (d != 0 || !*a)
return d;
}
}
static void ble_discovered_device(gattlib_adapter_t* adapter, const char* addr, const char* name, void *user_data) {
int ret;
int16_t rssi;
if (stricmp(addr, m_argument.mac_address) != 0) {
return;
}
ret = gattlib_get_rssi_from_mac(adapter, addr, &rssi);
if (ret == 0) {
GATTLIB_LOG(GATTLIB_INFO, "Found bluetooth device '%s' with RSSI:%d", m_argument.mac_address, rssi);
} else {
GATTLIB_LOG(GATTLIB_INFO, "Found bluetooth device '%s'", m_argument.mac_address);
}
ret = gattlib_connect(adapter, addr, GATTLIB_CONNECTION_OPTIONS_NONE, on_device_connect, NULL);
if (ret != GATTLIB_SUCCESS) {
GATTLIB_LOG(GATTLIB_ERROR, "Failed to connect to the bluetooth device '%s'", addr);
}
}
static void* ble_task(void* arg) {
char* addr = arg;
gattlib_adapter_t* adapter;
int ret;
ret = gattlib_adapter_open(m_argument.adapter_name, &adapter);
if (ret) {
GATTLIB_LOG(GATTLIB_ERROR, "Failed to open adapter.");
return NULL;
}
ret = gattlib_adapter_scan_enable(adapter, ble_discovered_device, BLE_SCAN_TIMEOUT, addr);
if (ret) {
GATTLIB_LOG(GATTLIB_ERROR, "Failed to scan.");
return NULL;
}
// Wait for the device to be connected
pthread_mutex_lock(&m_connection_terminated_lock);
pthread_cond_wait(&m_connection_terminated, &m_connection_terminated_lock);
pthread_mutex_unlock(&m_connection_terminated_lock);
return NULL;
}
int main(int argc, char *argv[]) {
int ret;
int argid;
gattlib_connection_t* connection;
if (argc < 3) {
usage(argv);
return 1;
}
if (gattlib_string_to_uuid(argv[2], strlen(argv[2]) + 1, &g_notify_uuid) < 0) {
if (gattlib_string_to_uuid(argv[2], strlen(argv[2]) + 1, &m_argument.gatt_notification_uuid) < 0) {
usage(argv);
return 1;
}
if (argc > 3) {
if (gattlib_string_to_uuid(argv[3], strlen(argv[3]) + 1, &g_write_uuid) < 0) {
if (argc == 5) {
if (gattlib_string_to_uuid(argv[3], strlen(argv[3]) + 1, &m_argument.gatt_write_uuid) < 0) {
usage(argv);
return 1;
}
sscanf(argv[4], "%ld", &m_argument.gatt_write_data);
}
#ifdef GATTLIB_LOG_BACKEND_SYSLOG
openlog("gattlib_notification", LOG_CONS | LOG_NDELAY | LOG_PERROR | LOG_PID, LOG_USER);
setlogmask(LOG_UPTO(LOG_DEBUG));
#endif
connection = gattlib_connect(NULL, argv[1], GATTLIB_CONNECTION_OPTIONS_LEGACY_DEFAULT);
if (connection == NULL) {
GATTLIB_LOG(GATTLIB_ERROR, "Fail to connect to the bluetooth device.");
return 1;
}
gattlib_register_notification(connection, notification_handler, NULL);
#ifdef GATTLIB_LOG_BACKEND_SYSLOG
openlog("gattlib_notification", LOG_CONS | LOG_NDELAY | LOG_PERROR, LOG_USER);
setlogmask(LOG_UPTO(LOG_DEBUG));
setlogmask(LOG_UPTO(LOG_INFO));
#endif
ret = gattlib_notification_start(connection, &g_notify_uuid);
if (ret) {
GATTLIB_LOG(GATTLIB_ERROR, "Fail to start notification.");
goto DISCONNECT;
m_argument.adapter_name = NULL;
m_argument.mac_address = argv[1];
ret = gattlib_mainloop(ble_task, NULL);
if (ret != GATTLIB_SUCCESS) {
GATTLIB_LOG(GATTLIB_ERROR, "Failed to create gattlib mainloop");
}
// Optional byte writes to make to trigger notifications
for (argid = 4; argid < argc; argid ++) {
unsigned char data[256];
char * charp;
unsigned char * datap;
for (charp = argv[4], datap = data; charp[0] && charp[1]; charp += 2, datap ++) {
sscanf(charp, "%02hhx", datap);
}
ret = gattlib_write_char_by_uuid(connection, &g_write_uuid, data, datap - data);
if (ret != GATTLIB_SUCCESS) {
if (ret == GATTLIB_NOT_FOUND) {
GATTLIB_LOG(GATTLIB_ERROR, "Could not find GATT Characteristic with UUID %s.", argv[3]);
} else {
GATTLIB_LOG(GATTLIB_ERROR, "Error while writing GATT Characteristic with UUID %s (ret:%d)",
argv[3], ret);
}
goto DISCONNECT;
}
}
// Catch CTRL-C
signal(SIGINT, on_user_abort);
m_main_loop = g_main_loop_new(NULL, 0);
g_main_loop_run(m_main_loop);
// In case we quit the main loop, clean the connection
gattlib_notification_stop(connection, &g_notify_uuid);
g_main_loop_unref(m_main_loop);
DISCONNECT:
gattlib_disconnect(connection, false /* wait_disconnection */);
puts("Done");
return ret;
return 0;
}

646
gattlib-py/.pylintrc Normal file
View File

@ -0,0 +1,646 @@
[MAIN]
# Analyse import fallback blocks. This can be used to support both Python 2 and
# 3 compatible code, which means that the block might have code that exists
# only in one or another interpreter, leading to false positives when analysed.
analyse-fallback-blocks=no
# Clear in-memory caches upon conclusion of linting. Useful if running pylint
# in a server-like mode.
clear-cache-post-run=no
# Load and enable all available extensions. Use --list-extensions to see a list
# all available extensions.
#enable-all-extensions=
# In error mode, messages with a category besides ERROR or FATAL are
# suppressed, and no reports are done by default. Error mode is compatible with
# disabling specific errors.
#errors-only=
# Always return a 0 (non-error) status code, even if lint errors are found.
# This is primarily useful in continuous integration scripts.
#exit-zero=
# A comma-separated list of package or module names from where C extensions may
# be loaded. Extensions are loading into the active Python interpreter and may
# run arbitrary code.
extension-pkg-allow-list=
# A comma-separated list of package or module names from where C extensions may
# be loaded. Extensions are loading into the active Python interpreter and may
# run arbitrary code. (This is an alternative name to extension-pkg-allow-list
# for backward compatibility.)
extension-pkg-whitelist=
# Return non-zero exit code if any of these messages/categories are detected,
# even if score is above --fail-under value. Syntax same as enable. Messages
# specified are enabled, while categories only check already-enabled messages.
fail-on=
# Specify a score threshold under which the program will exit with error.
fail-under=10
# Interpret the stdin as a python script, whose filename needs to be passed as
# the module_or_package argument.
#from-stdin=
# Files or directories to be skipped. They should be base names, not paths.
ignore=CVS
# Add files or directories matching the regular expressions patterns to the
# ignore-list. The regex matches against paths and can be in Posix or Windows
# format. Because '\\' represents the directory delimiter on Windows systems,
# it can't be used as an escape character.
ignore-paths=
# Files or directories matching the regular expression patterns are skipped.
# The regex matches against base names, not paths. The default value ignores
# Emacs file locks
ignore-patterns=^\.#
# List of module names for which member attributes should not be checked
# (useful for modules/projects where namespaces are manipulated during runtime
# and thus existing member attributes cannot be deduced by static analysis). It
# supports qualified module names, as well as Unix pattern matching.
ignored-modules=
# Python code to execute, usually for sys.path manipulation such as
# pygtk.require().
#init-hook=
# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the
# number of processors available to use, and will cap the count on Windows to
# avoid hangs.
jobs=1
# Control the amount of potential inferred values when inferring a single
# object. This can help the performance when dealing with large functions or
# complex, nested conditions.
limit-inference-results=100
# List of plugins (as comma separated values of python module names) to load,
# usually to register additional checkers.
load-plugins=
# Pickle collected data for later comparisons.
persistent=yes
# Minimum Python version to use for version dependent checks. Will default to
# the version used to run pylint.
py-version=3.10
# Discover python modules and packages in the file system subtree.
recursive=no
# Add paths to the list of the source roots. Supports globbing patterns. The
# source root is an absolute path or a path relative to the current working
# directory used to determine a package namespace for modules located under the
# source root.
source-roots=
# When enabled, pylint would attempt to guess common misconfiguration and emit
# user-friendly hints instead of false-positive error messages.
suggestion-mode=yes
# Allow loading of arbitrary C extensions. Extensions are imported into the
# active Python interpreter and may run arbitrary code.
unsafe-load-any-extension=no
# In verbose mode, extra non-checker-related info will be displayed.
#verbose=
[BASIC]
# Naming style matching correct argument names.
argument-naming-style=snake_case
# Regular expression matching correct argument names. Overrides argument-
# naming-style. If left empty, argument names will be checked with the set
# naming style.
#argument-rgx=
# Naming style matching correct attribute names.
attr-naming-style=snake_case
# Regular expression matching correct attribute names. Overrides attr-naming-
# style. If left empty, attribute names will be checked with the set naming
# style.
#attr-rgx=
# Bad variable names which should always be refused, separated by a comma.
bad-names=foo,
bar,
baz,
toto,
tutu,
tata
# Bad variable names regexes, separated by a comma. If names match any regex,
# they will always be refused
bad-names-rgxs=
# Naming style matching correct class attribute names.
class-attribute-naming-style=any
# Regular expression matching correct class attribute names. Overrides class-
# attribute-naming-style. If left empty, class attribute names will be checked
# with the set naming style.
#class-attribute-rgx=
# Naming style matching correct class constant names.
class-const-naming-style=UPPER_CASE
# Regular expression matching correct class constant names. Overrides class-
# const-naming-style. If left empty, class constant names will be checked with
# the set naming style.
#class-const-rgx=
# Naming style matching correct class names.
class-naming-style=PascalCase
# Regular expression matching correct class names. Overrides class-naming-
# style. If left empty, class names will be checked with the set naming style.
#class-rgx=
# Naming style matching correct constant names.
const-naming-style=UPPER_CASE
# Regular expression matching correct constant names. Overrides const-naming-
# style. If left empty, constant names will be checked with the set naming
# style.
#const-rgx=
# Minimum line length for functions/classes that require docstrings, shorter
# ones are exempt.
docstring-min-length=-1
# Naming style matching correct function names.
function-naming-style=snake_case
# Regular expression matching correct function names. Overrides function-
# naming-style. If left empty, function names will be checked with the set
# naming style.
#function-rgx=
# Good variable names which should always be accepted, separated by a comma.
good-names=i,
j,
k,
ex,
Run,
_
# Good variable names regexes, separated by a comma. If names match any regex,
# they will always be accepted
good-names-rgxs=
# Include a hint for the correct naming format with invalid-name.
include-naming-hint=no
# Naming style matching correct inline iteration names.
inlinevar-naming-style=any
# Regular expression matching correct inline iteration names. Overrides
# inlinevar-naming-style. If left empty, inline iteration names will be checked
# with the set naming style.
#inlinevar-rgx=
# Naming style matching correct method names.
method-naming-style=snake_case
# Regular expression matching correct method names. Overrides method-naming-
# style. If left empty, method names will be checked with the set naming style.
#method-rgx=
# Naming style matching correct module names.
module-naming-style=snake_case
# Regular expression matching correct module names. Overrides module-naming-
# style. If left empty, module names will be checked with the set naming style.
#module-rgx=
# Colon-delimited sets of names that determine each other's naming style when
# the name regexes allow several styles.
name-group=
# Regular expression which should only match function or class names that do
# not require a docstring.
no-docstring-rgx=^_
# List of decorators that produce properties, such as abc.abstractproperty. Add
# to this list to register other decorators that produce valid properties.
# These decorators are taken in consideration only for invalid-name.
property-classes=abc.abstractproperty
# Regular expression matching correct type alias names. If left empty, type
# alias names will be checked with the set naming style.
#typealias-rgx=
# Regular expression matching correct type variable names. If left empty, type
# variable names will be checked with the set naming style.
#typevar-rgx=
# Naming style matching correct variable names.
variable-naming-style=snake_case
# Regular expression matching correct variable names. Overrides variable-
# naming-style. If left empty, variable names will be checked with the set
# naming style.
#variable-rgx=
[CLASSES]
# Warn about protected attribute access inside special methods
check-protected-access-in-special-methods=no
# List of method names used to declare (i.e. assign) instance attributes.
defining-attr-methods=__init__,
__new__,
setUp,
asyncSetUp,
__post_init__
# List of member names, which should be excluded from the protected access
# warning.
exclude-protected=_asdict,_fields,_replace,_source,_make,os._exit
# List of valid names for the first argument in a class method.
valid-classmethod-first-arg=cls
# List of valid names for the first argument in a metaclass class method.
valid-metaclass-classmethod-first-arg=mcs
[DESIGN]
# List of regular expressions of class ancestor names to ignore when counting
# public methods (see R0903)
exclude-too-few-public-methods=
# List of qualified class names to ignore when counting class parents (see
# R0901)
ignored-parents=
# Maximum number of arguments for function / method.
max-args=5
# Maximum number of attributes for a class (see R0902).
max-attributes=20
# Maximum number of boolean expressions in an if statement (see R0916).
max-bool-expr=5
# Maximum number of branch for function / method body.
max-branches=20
# Maximum number of locals for function / method body.
max-locals=20
# Maximum number of parents for a class (see R0901).
max-parents=7
# Maximum number of public methods for a class (see R0904).
max-public-methods=20
# Maximum number of return / yield for function / method body.
max-returns=6
# Maximum number of statements in function / method body.
max-statements=50
# Minimum number of public methods for a class (see R0903).
min-public-methods=2
[EXCEPTIONS]
# Exceptions that will emit a warning when caught.
overgeneral-exceptions=builtins.BaseException,builtins.Exception
[FORMAT]
# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
expected-line-ending-format=
# Regexp for a line that is allowed to be longer than the limit.
ignore-long-lines=^\s*(# )?<?https?://\S+>?$
# Number of spaces of indent required inside a hanging or continued line.
indent-after-paren=4
# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
# tab).
indent-string=' '
# Maximum number of characters on a single line.
max-line-length=140
# Maximum number of lines in a module.
max-module-lines=1000
# Allow the body of a class to be on the same line as the declaration if body
# contains single statement.
single-line-class-stmt=no
# Allow the body of an if to be on the same line as the test if there is no
# else.
single-line-if-stmt=no
[IMPORTS]
# List of modules that can be imported at any level, not just the top level
# one.
allow-any-import-level=
# Allow explicit reexports by alias from a package __init__.
allow-reexport-from-package=no
# Allow wildcard imports from modules that define __all__.
allow-wildcard-with-all=no
# Deprecated modules which should not be used, separated by a comma.
deprecated-modules=
# Output a graph (.gv or any supported image format) of external dependencies
# to the given file (report RP0402 must not be disabled).
ext-import-graph=
# Output a graph (.gv or any supported image format) of all (i.e. internal and
# external) dependencies to the given file (report RP0402 must not be
# disabled).
import-graph=
# Output a graph (.gv or any supported image format) of internal dependencies
# to the given file (report RP0402 must not be disabled).
int-import-graph=
# Force import order to recognize a module as part of the standard
# compatibility libraries.
known-standard-library=
# Force import order to recognize a module as part of a third party library.
known-third-party=enchant
# Couples of modules and preferred modules, separated by a comma.
preferred-modules=
[LOGGING]
# The type of string formatting that logging methods do. `old` means using %
# formatting, `new` is for `{}` formatting.
logging-format-style=old
# Logging modules to check that the string format arguments are in logging
# function parameter format.
logging-modules=logging
[MESSAGES CONTROL]
# Only show warnings with the listed confidence levels. Leave empty to show
# all. Valid levels: HIGH, CONTROL_FLOW, INFERENCE, INFERENCE_FAILURE,
# UNDEFINED.
confidence=HIGH,
CONTROL_FLOW,
INFERENCE,
INFERENCE_FAILURE,
UNDEFINED
# Disable the message, report, category or checker with the given id(s). You
# can either give multiple identifiers separated by comma (,) or put this
# option multiple times (only on the command line, not in the configuration
# file where it should appear only once). You can also use "--disable=all" to
# disable everything first and then re-enable specific checks. For example, if
# you want to run only the similarities checker, you can use "--disable=all
# --enable=similarities". If you want to run only the classes checker, but have
# no Warning level messages displayed, use "--disable=all --enable=classes
# --disable=W".
disable=raw-checker-failed,
bad-inline-option,
locally-disabled,
file-ignored,
suppressed-message,
useless-suppression,
deprecated-pragma,
use-implicit-booleaness-not-comparison-to-string,
use-implicit-booleaness-not-comparison-to-zero,
use-symbolic-message-instead,
superfluous-parens,
no-else-return,
unused-argument,
fixme,
too-few-public-methods,
too-many-arguments,
global-statement
# Enable the message, report, category or checker with the given id(s). You can
# either give multiple identifier separated by comma (,) or put this option
# multiple time (only on the command line, not in the configuration file where
# it should appear only once). See also the "--disable" option for examples.
enable=
[METHOD_ARGS]
# List of qualified names (i.e., library.method) which require a timeout
# parameter e.g. 'requests.api.get,requests.api.post'
timeout-methods=requests.api.delete,requests.api.get,requests.api.head,requests.api.options,requests.api.patch,requests.api.post,requests.api.put,requests.api.request
[MISCELLANEOUS]
# List of note tags to take in consideration, separated by a comma.
notes=FIXME,
XXX,
TODO
# Regular expression of note tags to take in consideration.
notes-rgx=
[REFACTORING]
# Maximum number of nested blocks for function / method body
max-nested-blocks=5
# Complete name of functions that never returns. When checking for
# inconsistent-return-statements if a never returning function is called then
# it will be considered as an explicit return statement and no message will be
# printed.
never-returning-functions=sys.exit,argparse.parse_error
# Let 'consider-using-join' be raised when the separator to join on would be
# non-empty (resulting in expected fixes of the type: ``"- " + " -
# ".join(items)``)
suggest-join-with-non-empty-separator=yes
[REPORTS]
# Python expression which should return a score less than or equal to 10. You
# have access to the variables 'fatal', 'error', 'warning', 'refactor',
# 'convention', and 'info' which contain the number of messages in each
# category, as well as 'statement' which is the total number of statements
# analyzed. This score is used by the global evaluation report (RP0004).
evaluation=max(0, 0 if fatal else 10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10))
# Template used to display messages. This is a python new-style format string
# used to format the message information. See doc for all details.
msg-template=
# Set the output format. Available formats are: text, parseable, colorized,
# json2 (improved json format), json (old json format) and msvs (visual
# studio). You can also give a reporter class, e.g.
# mypackage.mymodule.MyReporterClass.
#output-format=
# Tells whether to display a full report or only the messages.
reports=no
# Activate the evaluation score.
score=yes
[SIMILARITIES]
# Comments are removed from the similarity computation
ignore-comments=yes
# Docstrings are removed from the similarity computation
ignore-docstrings=yes
# Imports are removed from the similarity computation
ignore-imports=yes
# Signatures are removed from the similarity computation
ignore-signatures=yes
# Minimum lines number of a similarity.
min-similarity-lines=4
[SPELLING]
# Limits count of emitted suggestions for spelling mistakes.
max-spelling-suggestions=4
# Spelling dictionary name. No available dictionaries : You need to install
# both the python package and the system dependency for enchant to work.
spelling-dict=
# List of comma separated words that should be considered directives if they
# appear at the beginning of a comment and should not be checked.
spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy:
# List of comma separated words that should not be checked.
spelling-ignore-words=
# A path to a file that contains the private dictionary; one word per line.
spelling-private-dict-file=
# Tells whether to store unknown words to the private dictionary (see the
# --spelling-private-dict-file option) instead of raising a message.
spelling-store-unknown-words=no
[STRING]
# This flag controls whether inconsistent-quotes generates a warning when the
# character used as a quote delimiter is used inconsistently within a module.
check-quote-consistency=no
# This flag controls whether the implicit-str-concat should generate a warning
# on implicit string concatenation in sequences defined over several lines.
check-str-concat-over-line-jumps=no
[TYPECHECK]
# List of decorators that produce context managers, such as
# contextlib.contextmanager. Add to this list to register other decorators that
# produce valid context managers.
contextmanager-decorators=contextlib.contextmanager
# List of members which are set dynamically and missed by pylint inference
# system, and so shouldn't trigger E1101 when accessed. Python regular
# expressions are accepted.
generated-members=
# Tells whether to warn about missing members when the owner of the attribute
# is inferred to be None.
ignore-none=yes
# This flag controls whether pylint should warn about no-member and similar
# checks whenever an opaque object is returned when inferring. The inference
# can return multiple potential results while evaluating a Python object, but
# some branches might not be evaluated, which results in partial inference. In
# that case, it might be useful to still emit no-member and other checks for
# the rest of the inferred objects.
ignore-on-opaque-inference=yes
# List of symbolic message names to ignore for Mixin members.
ignored-checks-for-mixins=no-member,
not-async-context-manager,
not-context-manager,
attribute-defined-outside-init
# List of class names for which member attributes should not be checked (useful
# for classes with dynamically set attributes). This supports the use of
# qualified names.
ignored-classes=optparse.Values,thread._local,_thread._local,argparse.Namespace
# Show a hint with possible names when a member name was not found. The aspect
# of finding the hint is based on edit distance.
missing-member-hint=yes
# The minimum edit distance a name should have in order to be considered a
# similar match for a missing member name.
missing-member-hint-distance=1
# The total number of similar names that should be taken in consideration when
# showing a hint for a missing member.
missing-member-max-choices=1
# Regex pattern to define which classes are considered mixins.
mixin-class-rgx=.*[Mm]ixin
# List of decorators that change the signature of a decorated function.
signature-mutators=
[VARIABLES]
# List of additional names supposed to be defined in builtins. Remember that
# you should avoid defining new builtins when possible.
additional-builtins=
# Tells whether unused global variables should be treated as a violation.
allow-global-unused-variables=yes
# List of names allowed to shadow builtins
allowed-redefined-builtins=
# List of strings which can identify a callback function by name. A callback
# name must start or end with one of those strings.
callbacks=cb_,
_cb
# A regular expression matching the name of dummy variables (i.e. expected to
# not be used).
dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_
# Argument names that match this expression will be ignored.
ignored-argument-names=_.*|^ignored_|^unused_
# Tells whether we should check for unused import in __init__ files.
init-import=no
# List of qualified module names which can have objects that can redefine
# builtins.
redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io

View File

@ -15,7 +15,7 @@ args = parser.parse_args()
def on_discovered_ble_device(device, user_data):
advertisement_data, manufacturer_id, manufacturer_data = device.get_advertisement_data()
advertisement_data, manufacturer_data = device.get_advertisement_data()
print("Device Advertisement Data: %s" % manufacturer_data)

View File

@ -4,14 +4,16 @@
# Copyright (c) 2016-2024, Olivier Martin <olivier@labapart.org>
#
"""Gattlib C types and functions"""
from ctypes import *
import logging
import pathlib
try:
# '_version.py' is generated by 'setup.py'
from ._version import __version__
except:
from ._version import __version__ #pylint: disable=import-error
except: #pylint: disable=bare-except
pass
logger = logging.getLogger(__name__)
@ -24,6 +26,7 @@ except OSError:
gattlib = CDLL('libgattlib.so')
def native_logging(level: int, string: str):
"""Convert Gattlib logging to Python logging."""
if level == 3:
logger.debug(string)
elif level == 2:
@ -50,6 +53,7 @@ except AttributeError:
# uint8_t data[16];
# } uint128_t;
class GattlibUuid128(Structure):
"""Python class representing the C structure 'uint128_t'."""
_fields_ = [("data", c_byte * 16)]
@ -62,10 +66,12 @@ class GattlibUuid128(Structure):
# } value;
# } uuid_t;
class GattlibUuidValue(Union):
"""Python class representing the C structure of the value of 'uuid_t'."""
_fields_ = [("uuid16", c_ushort), ("uuid32", c_uint), ("uuid128", GattlibUuid128)]
class GattlibUuid(Structure):
"""Python class representing the C structure 'uuid_t'."""
_fields_ = [("type", c_byte), ("value", GattlibUuidValue)]
@ -75,6 +81,7 @@ class GattlibUuid(Structure):
# uuid_t uuid;
# } gattlib_primary_service_t;
class GattlibPrimaryService(Structure):
"""Python class representing the C structure 'gattlib_primary_service_t'."""
_fields_ = [("attr_handle_start", c_ushort),
("attr_handle_end", c_ushort),
("uuid", GattlibUuid)]
@ -87,6 +94,7 @@ class GattlibPrimaryService(Structure):
# uuid_t uuid;
# } gattlib_characteristic_t;
class GattlibCharacteristic(Structure):
"""Python class representing the C structure 'gattlib_characteristic_t'."""
_fields_ = [("handle", c_ushort),
("properties", c_byte),
("value_handle", c_ushort),
@ -99,10 +107,22 @@ class GattlibCharacteristic(Structure):
# size_t data_length;
# } gattlib_advertisement_data_t;
class GattlibAdvertisementData(Structure):
"""Python class representing the C structure 'gattlib_advertisement_data_t'."""
_fields_ = [("uuid", GattlibUuid),
("data", c_void_p),
("data_length", c_size_t)]
# typedef struct {
# uint16_t manufacturer_id;
# uint8_t* data;
# size_t data_size;
# } gattlib_manufacturer_data_t;
class GattlibManufacturerData(Structure):
"""Python class representing the C structure 'gattlib_manufacturer_data_t'."""
_fields_ = [("manufacturer_id", c_ushort),
("data", c_void_p),
("data_size", c_size_t)]
# int gattlib_adapter_open(const char* adapter_name, gattlib_adapter_t** adapter);
gattlib_adapter_open = gattlib.gattlib_adapter_open
@ -118,7 +138,8 @@ gattlib_discovered_device_python_callback = gattlib.gattlib_discovered_device_py
gattlib_discovered_device_python_callback.argtypes = [c_void_p, c_char_p, c_char_p, py_object]
gattlib_discovered_device_python_callback.restype = c_void_p
# void gattlib_connected_device_python_callback(gattlib_adapter_t* adapter, const char *dst, gattlib_connection_t* connection, int error, void* user_data);
# void gattlib_connected_device_python_callback(gattlib_adapter_t* adapter, const char *dst, gattlib_connection_t* connection,
# int error, void* user_data);
gattlib_connected_device_python_callback = gattlib.gattlib_connected_device_python_callback
gattlib_connected_device_python_callback.argtypes = [c_void_p, c_char_p, c_void_p, c_int, py_object]
gattlib_connected_device_python_callback.restype = c_void_p
@ -138,10 +159,12 @@ gattlib_python_callback_args = gattlib.gattlib_python_callback_args
gattlib_python_callback_args.argtypes = [py_object, py_object]
gattlib_python_callback_args.restype = c_void_p
# int gattlib_adapter_scan_enable_with_filter_non_blocking(gattlib_adapter_t* adapter, uuid_t **uuid_list, int16_t rssi_threshold, uint32_t enabled_filters,
# int gattlib_adapter_scan_enable_with_filter_non_blocking(gattlib_adapter_t* adapter, uuid_t **uuid_list,
# int16_t rssi_threshold, uint32_t enabled_filters,
# gattlib_discovered_device_t discovered_device_cb, size_t timeout, void *user_data)
gattlib_adapter_scan_enable_with_filter_non_blocking = gattlib.gattlib_adapter_scan_enable_with_filter_non_blocking
gattlib_adapter_scan_enable_with_filter_non_blocking.argtypes = [c_void_p, POINTER(POINTER(GattlibUuid)), c_int16, c_uint32, c_void_p, c_size_t, c_void_p]
gattlib_adapter_scan_enable_with_filter_non_blocking.argtypes = [c_void_p, POINTER(POINTER(GattlibUuid)),
c_int16, c_uint32, c_void_p, c_size_t, c_void_p]
# int gattlib_adapter_scan_eddystone(gattlib_adapter_t* adapter, int16_t rssi_threshold, uint32_t eddsytone_types,
# gattlib_discovered_device_with_data_t discovered_device_cb, size_t timeout, void *user_data)
@ -210,15 +233,19 @@ gattlib_get_rssi_from_mac.argtypes = [c_void_p, c_char_p, POINTER(c_int16)]
# int gattlib_get_advertisement_data(gattlib_connection_t *connection,
# gattlib_advertisement_data_t **advertisement_data, size_t *advertisement_data_count,
# uint16_t *manufacturer_id, uint8_t **manufacturer_data, size_t *manufacturer_data_size)
# gattlib_manufacturer_data_t** manufacturer_data, size_t* manufacturer_data_count)
gattlib_get_advertisement_data = gattlib.gattlib_get_advertisement_data
gattlib_get_advertisement_data.argtypes = [c_void_p, POINTER(POINTER(GattlibAdvertisementData)), POINTER(c_size_t), POINTER(c_uint16), POINTER(c_void_p), POINTER(c_size_t)]
gattlib_get_advertisement_data.argtypes = [c_void_p,
POINTER(POINTER(GattlibAdvertisementData)), POINTER(c_size_t),
POINTER(POINTER(GattlibManufacturerData)), POINTER(c_size_t)]
# int gattlib_get_advertisement_data_from_mac(gattlib_adapter_t* adapter, const char *mac_address,
# gattlib_advertisement_data_t **advertisement_data, size_t *advertisement_data_length,
# uint16_t *manufacturer_id, uint8_t **manufacturer_data, size_t *manufacturer_data_size)
# gattlib_manufacturer_data_t** manufacturer_data, size_t* manufacturer_data_count)
gattlib_get_advertisement_data_from_mac = gattlib.gattlib_get_advertisement_data_from_mac
gattlib_get_advertisement_data_from_mac.argtypes = [c_void_p, c_char_p, POINTER(POINTER(GattlibAdvertisementData)), POINTER(c_size_t), POINTER(c_uint16), POINTER(c_void_p), POINTER(c_size_t)]
gattlib_get_advertisement_data_from_mac.argtypes = [c_void_p, c_char_p,
POINTER(POINTER(GattlibAdvertisementData)), POINTER(c_size_t),
POINTER(POINTER(GattlibManufacturerData)), POINTER(c_size_t)]
# int gattlib_mainloop_python(PyObject *handler, PyObject *user_data)
gattlib_mainloop = gattlib.gattlib_mainloop_python

View File

@ -4,13 +4,15 @@
# Copyright (c) 2016-2024, Olivier Martin <olivier@labapart.org>
#
"""Gattlib Adapter API"""
import threading
from uuid import UUID
from gattlib import *
from gattlib import * #pylint: disable=wildcard-import,unused-wildcard-import
from .device import Device
from .exception import handle_return, AdapterNotOpened
from .uuid import gattlib_uuid_to_int
from .exception import handle_return
from .helpers import convert_gattlib_advertisement_c_data_to_dict
GATTLIB_DISCOVER_FILTER_USE_UUID = (1 << 0)
GATTLIB_DISCOVER_FILTER_USE_RSSI = (1 << 1)
@ -38,12 +40,14 @@ EDDYSTONE_URL_SCHEME_PREFIX = {
class Adapter:
"""Bluetooth adapter."""
def __init__(self, name=c_char_p(None)):
self._name = name
self._adapter = c_void_p(None)
self._is_opened = False # Note: 'self._adapter != c_void_p(None)' does not seem to return the expected result
self._lock = threading.Lock()
self._on_discovered_device_callback = None
self._on_discovered_device_user_callback = None
def __str__(self) -> str:
if self._name:
@ -53,20 +57,20 @@ class Adapter:
@property
def name(self):
"""Return adapter name."""
return self._name
@staticmethod
def list():
# TODO: Add support
return []
#@staticmethod
#def list():
# # TODO: Add support
# return []
def open(self):
self._lock.acquire()
if self._is_opened:
self._lock.release()
return
"""Open adapter."""
with self._lock:
if self._is_opened:
return
try:
self._adapter = c_void_p(None)
ret = gattlib_adapter_open(self._name, byref(self._adapter))
if ret == 0:
@ -75,27 +79,24 @@ class Adapter:
self._name = gattlib_adapter_get_name(self._adapter)
else:
handle_return(ret)
finally:
self._lock.release()
def close(self):
self._lock.acquire()
try:
"""Close adapter."""
with self._lock:
if self._adapter:
ret = gattlib.gattlib_adapter_close(self._adapter)
handle_return(ret)
self._is_opened = False
self._adapter = None
finally:
self._lock.release()
# Use a closure to return a method that can be called by the C-library (see: https://stackoverflow.com/a/7261524/6267288)
def get_on_discovered_device_callback(self):
"""Return a callback for newly discovered device."""
def on_discovered_device(adapter, addr, name, user_data):
try:
device = Device(self, addr, name)
self.on_discovered_device_user_callback(device, user_data)
except Exception as e:
self._on_discovered_device_user_callback(device, user_data)
except Exception as e: #pylint: disable=broad-exception-caught
logger.exception(e)
return on_discovered_device
@ -115,11 +116,11 @@ class Adapter:
@param timeout: defines the duration of the Bluetooth scanning. When timeout=None or 0, we scan indefinitely.
@param user_data: is the data passed to the callback `discovered_device_cb()`
"""
assert on_discovered_device_callback != None
self.on_discovered_device_user_callback = on_discovered_device_callback
# Save callback to prevent it to be cleaned by garbage collector see
# comment: https://stackoverflow.com/questions/7259794/how-can-i-get-methods-to-work-as-callbacks-with-python-ctypes#comment38658391_7261524
self.on_discovered_device_callback = self.get_on_discovered_device_callback()
assert on_discovered_device_callback is not None
self._on_discovered_device_user_callback = on_discovered_device_callback
# Save callback to prevent it to be cleaned by garbage collector see comment:
# https://stackoverflow.com/questions/7259794/how-can-i-get-methods-to-work-as-callbacks-with-python-ctypes#comment38658391_7261524
self._on_discovered_device_callback = self.get_on_discovered_device_callback()
# Ensure BLE adapter it opened
if not self._is_opened:
@ -164,41 +165,23 @@ class Adapter:
uuid_list, rssi, enabled_filters,
gattlib_discovered_device_python_callback,
timeout,
gattlib_python_callback_args(self.on_discovered_device_callback, user_data))
gattlib_python_callback_args(self._on_discovered_device_callback, user_data))
handle_return(ret)
@staticmethod
def on_discovered_ble_device_with_details(adapter, mac_addr, name, advertisement_data_buffer, advertisement_data_count,
manufacturer_id, manufacturer_data_buffer, manufacturer_data_size,
manufacturer_data_buffer, manufacturer_data_count,
user_data):
advertisement_data = {}
manufacturer_data = None
for i in range(0, advertisement_data_count):
service_data = advertisement_data_buffer[i]
uuid = gattlib_uuid_to_int(service_data.uuid)
pointer_type = POINTER(c_byte * service_data.data_length)
c_bytearray = cast(service_data.data, pointer_type)
data = bytearray(service_data.data_length)
for i in range(service_data.data_length):
data[i] = c_bytearray.contents[i] & 0xFF
advertisement_data[uuid] = data
if manufacturer_data_size > 0:
pointer_type = POINTER(c_byte * manufacturer_data_size)
c_bytearray = cast(manufacturer_data_buffer, pointer_type)
manufacturer_data = bytearray(manufacturer_data_size)
for i in range(manufacturer_data_size):
manufacturer_data[i] = c_bytearray.contents[i] & 0xFF
"""Callback invoked when a new device has been discovered."""
advertisement_data, manufacturer_data = convert_gattlib_advertisement_c_data_to_dict(
advertisement_data_buffer, advertisement_data_count,
manufacturer_data_buffer, manufacturer_data_count)
device = Device(user_data["adapter"], mac_addr, name)
user_data["callback"](device, advertisement_data, manufacturer_id, manufacturer_data, user_data["user_data"])
user_data["callback"](device, advertisement_data, manufacturer_data, user_data["user_data"])
def scan_eddystone_enable(self, on_discovered_device_callback, eddystone_filters, timeout, rssi_threshold=None, user_data=None):
"""Enable BLE scan for Eddystone devices."""
# Ensure BLE adapter it opened
if not self._is_opened:
self.open()
@ -221,10 +204,16 @@ class Adapter:
handle_return(ret)
def scan_disable(self):
"""Disable BLE scan."""
ret = gattlib.gattlib_adapter_scan_disable(self._adapter)
handle_return(ret)
def get_rssi_from_mac(self, mac_address):
"""
Return RSSI of a device defined by its MAC address.
Note: The RSSI is 0 when the device is connected.
"""
if isinstance(mac_address, str):
mac_address = mac_address.encode("utf-8")
@ -233,46 +222,19 @@ class Adapter:
return rssi.value
def gattlib_get_advertisement_data_from_mac(self, mac_address):
"""Return advertisement and manufacturer data of the device."""
if isinstance(mac_address, str):
mac_address = mac_address.encode("utf-8")
_advertisement_data = POINTER(GattlibAdvertisementData)()
_advertisement_data_count = c_size_t(0)
_manufacturer_id = c_uint16(0)
_manufacturer_data = c_void_p(None)
_manufacturer_data_len = c_size_t(0)
_manufacturer_data = POINTER(GattlibManufacturerData)()
_manufacturer_data_count = c_size_t(0)
ret = gattlib_get_advertisement_data_from_mac(self._adapter, mac_address,
byref(_advertisement_data), byref(_advertisement_data_count),
byref(_manufacturer_id),
byref(_manufacturer_data), byref(_manufacturer_data_len))
byref(_manufacturer_data), byref(_manufacturer_data_count))
handle_return(ret)
advertisement_data = {}
manufacturer_data = None
for i in range(0, _advertisement_data_count.value):
service_data = _advertisement_data[i]
uuid = gattlib_uuid_to_int(service_data.uuid)
pointer_type = POINTER(c_byte * service_data.data_length)
c_bytearray = cast(service_data.data, pointer_type)
data = bytearray(service_data.data_length)
for i in range(service_data.data_length):
data[i] = c_bytearray.contents[i] & 0xFF
advertisement_data[uuid] = data
if _manufacturer_data_len.value > 0:
pointer_type = POINTER(c_byte * _manufacturer_data_len.value)
c_bytearray = cast(_manufacturer_data, pointer_type)
manufacturer_data = bytearray(_manufacturer_data_len.value)
for i in range(_manufacturer_data_len.value):
manufacturer_data[i] = c_bytearray.contents[i] & 0xFF
gattlib_free_mem(_advertisement_data)
gattlib_free_mem(_manufacturer_data)
return advertisement_data, _manufacturer_id.value, manufacturer_data
return convert_gattlib_advertisement_c_data_to_dict(
_advertisement_data, _advertisement_data_count, _manufacturer_data, _manufacturer_data_count)

View File

@ -4,16 +4,17 @@
# Copyright (c) 2016-2024, Olivier Martin <olivier@labapart.org>
#
"""Gattlib Device API"""
from __future__ import annotations
import logging
import uuid
import threading
from typing import TYPE_CHECKING
from gattlib import *
from .exception import handle_return, DeviceError, InvalidParameter
from gattlib import * #pylint: disable=wildcard-import,unused-wildcard-import
from .exception import handle_return, InvalidParameter
from .gatt import GattService, GattCharacteristic
from .uuid import gattlib_uuid_to_int
from .helpers import convert_gattlib_advertisement_c_data_to_dict
if TYPE_CHECKING:
from .adapter import Adapter
@ -31,10 +32,10 @@ CONNECTION_OPTIONS_LEGACY_DEFAULT = \
class Device:
"""GATT device"""
def __init__(self, adapter: Adapter, addr: str, name: str = None):
self._adapter = adapter
if type(addr) == str:
if isinstance(addr, str):
self._addr = addr.encode("utf-8")
else:
self._addr = addr
@ -45,8 +46,12 @@ class Device:
# We use a lock on disconnection to ensure the memory is safely freed
self._disconnection_lock = threading.Lock()
self._services: dict[int, GattService] = {}
self._characteristics: dict[int, GattCharacteristic] = {}
self.on_connection_callback = None
self.on_connection_error_callback = None
self.disconnection_callback = None
# Keep track if notification handler has been initialized
self._is_notification_init = False
@ -65,6 +70,7 @@ class Device:
@property
def connection(self):
"""Return Gattlib connection C handle."""
if self._connection:
return self._connection
else:
@ -72,9 +78,11 @@ class Device:
@property
def is_connected(self) -> bool:
"""Return True if the device is connected."""
return (self._connection is not None)
def connect(self, options=CONNECTION_OPTIONS_LEGACY_DEFAULT):
"""Connect the device."""
def _on_connection(adapter: c_void_p, mac_address: c_char_p, connection: c_void_p, error: c_int, user_data: py_object):
if error:
self._connection = None
@ -86,7 +94,7 @@ class Device:
if self._adapter is None:
adapter = None
else:
adapter = self._adapter._adapter
adapter = self._adapter._adapter #pylint: disable=protected-access
ret = gattlib_connect(adapter, self._addr, options,
gattlib_connected_device_python_callback,
@ -94,16 +102,19 @@ class Device:
handle_return(ret)
def on_connection(self, user_data: py_object):
if self.on_connection_callback:
self.on_connection_callback(self, user_data)
"""Method called on device connection."""
if callable(self.on_connection_callback):
self.on_connection_callback(self, user_data) #pylint: disable=not-callable
def on_connection_error(self, error: c_int, user_data: py_object):
"""Method called on device connection error."""
logger.error("Failed to connect due to error '0x%x'", error)
if self.on_connection_error_callback:
self.on_connection_error_callback(self, error, user_data)
if callable(self.on_connection_error_callback):
self.on_connection_error_callback(self, error, user_data) #pylint: disable=not-callable
@property
def rssi(self):
"""Return connection RSSI."""
_rssi = c_int16(0)
if self._connection:
ret = gattlib_get_rssi(self._connection, byref(_rssi))
@ -113,56 +124,50 @@ class Device:
return self._adapter.get_rssi_from_mac(self._addr)
def register_on_disconnect(self, callback, user_data=None):
"""Register disconnection callback."""
self.disconnection_callback = callback
def on_disconnection(user_data):
self._disconnection_lock.acquire()
with self._disconnection_lock:
if self.disconnection_callback:
self.disconnection_callback()
if self.disconnection_callback:
self.disconnection_callback()
# On disconnection, we do not need the list of GATT services and GATT characteristics
if self._services_ptr:
gattlib_free_mem(self._services_ptr)
self._services_ptr = None
if self._characteristics_ptr:
gattlib_free_mem(self._characteristics_ptr)
self._characteristics_ptr = None
# On disconnection, we do not need the list of GATT services and GATT characteristics
if self._services_ptr:
gattlib_free_mem(self._services_ptr)
self._services_ptr = None
if self._characteristics_ptr:
gattlib_free_mem(self._characteristics_ptr)
self._characteristics_ptr = None
# Reset the connection handler
self._connection = None
self._disconnection_lock.release()
# Reset the connection handler
self._connection = None
gattlib_register_on_disconnect(self.connection,
gattlib_disconnected_device_python_callback,
gattlib_python_callback_args(on_disconnection, user_data))
def disconnect(self, wait_disconnection: bool = False):
self._connection_lock.acquire()
try:
"""Disconnect connected device."""
with self._connection_lock:
if self._connection:
ret = gattlib_disconnect(self.connection, wait_disconnection)
handle_return(ret)
self._connection = None
finally:
self._connection_lock.release()
def discover(self):
#
# Discover GATT Services
#
"""Discover GATT Services."""
self._services_ptr = POINTER(GattlibPrimaryService)()
_services_count = c_int(0)
ret = gattlib_discover_primary(self.connection, byref(self._services_ptr), byref(_services_count))
services_count = c_int(0)
ret = gattlib_discover_primary(self.connection, byref(self._services_ptr), byref(services_count))
handle_return(ret)
self._services = {}
for i in range(0, _services_count.value):
for i in range(0, services_count.value):
service = GattService(self, self._services_ptr[i])
self._services[service.short_uuid] = service
logger.debug("Service UUID:0x%x" % service.short_uuid)
logger.debug("Service UUID:0x%x", service.short_uuid)
#
# Discover GATT Characteristics
@ -177,59 +182,33 @@ class Device:
characteristic = GattCharacteristic(self, self._characteristics_ptr[i])
self._characteristics[characteristic.short_uuid] = characteristic
logger.debug("Characteristic UUID:0x%x" % characteristic.short_uuid)
logger.debug("Characteristic UUID:0x%x", characteristic.short_uuid)
def get_advertisement_data(self):
_advertisement_data = POINTER(GattlibAdvertisementData)()
_advertisement_data_count = c_size_t(0)
_manufacturer_id = c_uint16(0)
_manufacturer_data = c_void_p(None)
_manufacturer_data_len = c_size_t(0)
"""Return advertisement and manufacturer data of the device."""
advertisement_data = POINTER(GattlibAdvertisementData)()
advertisement_data_count = c_size_t(0)
manufacturer_data = POINTER(GattlibManufacturerData)()
manufacturer_data_count = c_size_t(0)
if self._connection is None:
ret = gattlib_get_advertisement_data_from_mac(self._adapter._adapter, self._addr,
byref(_advertisement_data), byref(_advertisement_data_count),
byref(_manufacturer_id),
byref(_manufacturer_data), byref(_manufacturer_data_len))
ret = gattlib_get_advertisement_data_from_mac(self._adapter._adapter, self._addr, #pylint: disable=protected-access
byref(advertisement_data), byref(advertisement_data_count),
byref(manufacturer_data), byref(manufacturer_data_count))
else:
ret = gattlib_get_advertisement_data(self._connection,
byref(_advertisement_data), byref(_advertisement_data_count),
byref(_manufacturer_id),
byref(_manufacturer_data), byref(_manufacturer_data_len))
byref(advertisement_data), byref(advertisement_data_count),
byref(manufacturer_data), byref(manufacturer_data_count))
handle_return(ret)
advertisement_data = {}
manufacturer_data = None
for i in range(0, _advertisement_data_count.value):
service_data = _advertisement_data[i]
uuid = gattlib_uuid_to_int(service_data.uuid)
pointer_type = POINTER(c_byte * service_data.data_length)
c_bytearray = cast(service_data.data, pointer_type)
data = bytearray(service_data.data_length)
for i in range(service_data.data_length):
data[i] = c_bytearray.contents[i] & 0xFF
advertisement_data[uuid] = data
if _manufacturer_data_len.value > 0:
pointer_type = POINTER(c_byte * _manufacturer_data_len.value)
c_bytearray = cast(_manufacturer_data, pointer_type)
manufacturer_data = bytearray(_manufacturer_data_len.value)
for i in range(_manufacturer_data_len.value):
manufacturer_data[i] = c_bytearray.contents[i] & 0xFF
gattlib_free_mem(_advertisement_data)
gattlib_free_mem(_manufacturer_data)
return advertisement_data, _manufacturer_id.value, manufacturer_data
return convert_gattlib_advertisement_c_data_to_dict( #pylint: disable=protected-access
advertisement_data, advertisement_data_count,
manufacturer_data, manufacturer_data_count)
@property
def services(self):
def services(self) -> dict[int, GattService]:
"""Return a GATT Service dictionary - the GATT UUID being the key."""
if not hasattr(self, '_services'):
logger.warning("Start GATT discovery implicitly")
self.discover()
@ -237,7 +216,8 @@ class Device:
return self._services
@property
def characteristics(self):
def characteristics(self) -> dict[int, GattCharacteristic]:
"""Return a GATT Characteristic dictionary - the GATT UUID being the key."""
if not hasattr(self, '_characteristics'):
logger.warning("Start GATT discovery implicitly")
self.discover()
@ -245,16 +225,17 @@ class Device:
return self._characteristics
@staticmethod
def notification_callback(uuid_str, data, data_len, user_data):
def _notification_callback(uuid_str, data, data_len, user_data):
"""Helper method to call back characteristic callback."""
this = user_data
notification_uuid = uuid.UUID(uuid_str)
short_uuid = notification_uuid.int
if short_uuid not in this._gatt_characteristic_callbacks:
if short_uuid not in this._gatt_characteristic_callbacks: #pylint: disable=protected-access
raise RuntimeError("UUID '%s' is expected to be part of the notification list")
else:
characteristic_callback = this._gatt_characteristic_callbacks[short_uuid]
characteristic_callback = this._gatt_characteristic_callbacks[short_uuid] #pylint: disable=protected-access
# value = bytearray(data_len)
# for i in range(data_len):
@ -278,7 +259,7 @@ class Device:
gattlib_register_notification(self._connection,
gattlib_notification_device_python_callback,
gattlib_python_callback_args(Device.notification_callback, self))
gattlib_python_callback_args(Device._notification_callback, self))
def _notification_add_gatt_characteristic_callback(self, gatt_characteristic, callback, user_data):
if not callable(callback):

View File

@ -4,6 +4,8 @@
# Copyright (c) 2016-2024, Olivier Martin <olivier@labapart.org>
#
"""Gattlib Exceptions"""
GATTLIB_SUCCESS = 0
GATTLIB_INVALID_PARAMETER = 1
GATTLIB_NOT_FOUND = 2
@ -15,6 +17,8 @@ GATTLIB_DEVICE_NOT_CONNECTED = 7
GATTLIB_NO_ADAPTER = 8
GATTLIB_BUSY = 9
GATTLIB_UNEXPECTED = 10
GATTLIB_ADAPTER_CLOSE = 11
GATTLIB_DEVICE_DISCONNECTED = 12
GATTLIB_ERROR_MODULE_MASK = 0xF0000000
GATTLIB_ERROR_DBUS = 0x10000000
@ -23,36 +27,43 @@ GATTLIB_ERROR_INTERNAL = 0x80000000
class GattlibException(Exception):
pass
"""Generic Gattlib exception."""
class NoAdapter(GattlibException):
pass
"""Gattlib exception raised when no adapter is present."""
class Busy(GattlibException):
pass
"""Gattlib busy exception."""
class Unexpected(GattlibException):
pass
"""Gattlib unexpected exception."""
class AdapterNotOpened(GattlibException):
pass
"""Gattlib exception raised when adapter is not opened yet."""
class InvalidParameter(GattlibException):
pass
"""Gattlib invalid parameter exception."""
class NotFound(GattlibException):
pass
"""Gattlib not found exception."""
class OutOfMemory(GattlibException):
pass
"""Gattlib out of memory exception."""
class NotSupported(GattlibException):
pass
"""Gattlib not supported exception."""
class NotConnected(GattlibException):
pass
"""Gattlib exception raised when device is not connected."""
class AdapterClose(GattlibException):
"""Gattlib exception raised when the adapter is closed."""
class Disconnected(GattlibException):
"""Gattlib exception raised when the device is disconnected."""
class DeviceError(GattlibException):
"""Gattlib device exception."""
def __init__(self, adapter: str = None, mac_address: str = None) -> None:
self.adapter = adapter
self.mac_address = mac_address
@ -61,44 +72,50 @@ class DeviceError(GattlibException):
return f"Error with device {self.mac_address} on adapter {self.adapter}"
class DBusError(GattlibException):
"""Gattlib DBUS exception."""
def __init__(self, domain: int, code: int) -> None:
self.domain = domain
self.code = code
def __str__(self) -> str:
if self.domain == 238 and self.code == 60964:
return f"DBus Error: le-connection-abort-by-local"
return "DBus Error: le-connection-abort-by-local"
elif self.domain == 238 and self.code == 60952:
return f"DBus Error: Timeout was reached"
return "DBus Error: Timeout was reached"
elif self.domain == 238 and self.code == 60964:
return f"DBus Error: Timeout was reached"
return "DBus Error: Timeout was reached"
else:
return f"DBus Error domain={self.domain},code={self.code}"
def handle_return(ret):
"""Function to convert gattlib error to Python exception."""
if ret == GATTLIB_INVALID_PARAMETER:
raise InvalidParameter()
elif ret == GATTLIB_NOT_FOUND:
if ret == GATTLIB_NOT_FOUND:
raise NotFound()
elif ret == GATTLIB_OUT_OF_MEMORY:
if ret == GATTLIB_OUT_OF_MEMORY:
raise OutOfMemory()
elif ret == GATTLIB_TIMEOUT:
if ret == GATTLIB_TIMEOUT:
raise TimeoutError()
elif ret == GATTLIB_NOT_SUPPORTED:
if ret == GATTLIB_NOT_SUPPORTED:
raise NotSupported()
elif ret == GATTLIB_DEVICE_ERROR:
if ret == GATTLIB_DEVICE_ERROR:
raise DeviceError()
elif ret == GATTLIB_DEVICE_NOT_CONNECTED:
if ret == GATTLIB_DEVICE_NOT_CONNECTED:
raise NotConnected()
elif ret == GATTLIB_NO_ADAPTER:
if ret == GATTLIB_NO_ADAPTER:
raise NoAdapter()
elif ret == GATTLIB_BUSY:
if ret == GATTLIB_BUSY:
raise Busy()
elif ret == GATTLIB_UNEXPECTED:
if ret == GATTLIB_UNEXPECTED:
raise Unexpected()
elif (ret & GATTLIB_ERROR_MODULE_MASK) == GATTLIB_ERROR_DBUS:
if ret == GATTLIB_ADAPTER_CLOSE:
raise AdapterClose()
if ret == GATTLIB_DEVICE_DISCONNECTED:
raise Disconnected()
if (ret & GATTLIB_ERROR_MODULE_MASK) == GATTLIB_ERROR_DBUS:
raise DBusError((ret >> 8) & 0xFFF, ret & 0xFFFF)
elif ret == -22: # From '-EINVAL'
if ret == -22: # From '-EINVAL'
raise ValueError("Gattlib value error")
elif ret != 0:
raise RuntimeError("Gattlib exception %d" % ret)
if ret != 0:
raise RuntimeError(f"Gattlib exception {ret}")

View File

@ -4,23 +4,29 @@
# Copyright (c) 2016-2024, Olivier Martin <olivier@labapart.org>
#
from gattlib import *
"""Module for GATT Service, Characteristic and Stream."""
from uuid import UUID
from gattlib import * #pylint: disable=wildcard-import,unused-wildcard-import
from .uuid import gattlib_uuid_to_uuid, gattlib_uuid_to_int
from .exception import handle_return, InvalidParameter
class GattStream():
"""GATT Stream class."""
def __init__(self, fd, mtu):
self._fd = fd
self._mtu = mtu
@property
def mtu(self):
"""Return connection MTU."""
# Remove ATT Header (3 bytes)
return self._mtu - 3
def write(self, data, mtu=None):
"""Write data to GATT stream."""
if mtu is None:
mtu = self.mtu
@ -35,44 +41,51 @@ class GattStream():
gattlib.gattlib_write_char_stream_write(self._fd, buffer_type.from_buffer_copy(buffer), buffer_len)
def close(self):
"""Close GATT stream."""
gattlib.gattlib_write_char_stream_close(self._fd)
class GattService():
"""GATT Service class."""
def __init__(self, device, gattlib_primary_service):
self._device = device
self._gattlib_primary_service = gattlib_primary_service
@property
def uuid(self):
def uuid(self) -> UUID:
"""Return GATT service UUID"""
return gattlib_uuid_to_uuid(self._gattlib_primary_service.uuid)
@property
def short_uuid(self):
def short_uuid(self) -> int:
"""Return GATT service short UUID"""
return gattlib_uuid_to_int(self._gattlib_primary_service.uuid)
class GattCharacteristic():
"""GATT Characteristic class."""
def __init__(self, device, gattlib_characteristic):
self._device = device
self._gattlib_characteristic = gattlib_characteristic
@property
def uuid(self):
def uuid(self) -> UUID:
"""Read UUID characteristic."""
return gattlib_uuid_to_uuid(self._gattlib_characteristic.uuid)
@property
def short_uuid(self):
"""Return GATT characteristic short UUID"""
return gattlib_uuid_to_int(self._gattlib_characteristic.uuid)
@property
def connection(self):
"""Return Gattlib connection C handle."""
return self._device.connection
def read(self, callback=None):
if callback:
"""Read GATT characteristic."""
if callback: #pylint: disable=no-else-raise
raise NotImplementedError()
else:
_buffer = c_void_p(None)
@ -92,6 +105,7 @@ class GattCharacteristic():
return value
def write(self, data, without_response=False):
"""Write data to GATT characteristic."""
if not isinstance(data, bytes) and not isinstance(data, bytearray):
raise TypeError("Data must be of bytes type to know its size.")
@ -100,12 +114,17 @@ class GattCharacteristic():
buffer_len = len(data)
if without_response:
ret = gattlib_write_without_response_char_by_uuid(self.connection, self._gattlib_characteristic.uuid, buffer_type.from_buffer_copy(buffer), buffer_len)
ret = gattlib_write_without_response_char_by_uuid(self.connection,
self._gattlib_characteristic.uuid,
buffer_type.from_buffer_copy(buffer), buffer_len)
else:
ret = gattlib_write_char_by_uuid(self.connection, self._gattlib_characteristic.uuid, buffer_type.from_buffer_copy(buffer), buffer_len)
ret = gattlib_write_char_by_uuid(self.connection,
self._gattlib_characteristic.uuid,
buffer_type.from_buffer_copy(buffer), buffer_len)
handle_return(ret)
def stream_open(self):
"""Open GATT stream from GATT characteristic."""
_stream = c_void_p(None)
_mtu = c_uint16(0)
@ -115,20 +134,24 @@ class GattCharacteristic():
return GattStream(_stream, _mtu.value)
def register_notification(self, callback, user_data=None):
"""Register callback for notification on this GATT characteristic."""
if not callable(callback):
raise InvalidParameter("Notification callback is not callable.")
self._device._notification_add_gatt_characteristic_callback(self, callback, user_data)
self._device._notification_add_gatt_characteristic_callback(self, callback, user_data) #pylint: disable=protected-access
def unregister_notification(self):
self._device._notification_remove_gatt_characteristic_callback(self)
"""Unregister all notification callbacks."""
self._device._notification_remove_gatt_characteristic_callback(self) #pylint: disable=protected-access
def notification_start(self):
"""Start GATT notification."""
ret = gattlib_notification_start(self.connection, self._gattlib_characteristic.uuid)
handle_return(ret)
def notification_stop(self):
""" Could raise gattlib.exception.NotFound if notification has not been registered"""
"""Stop GATT notification."""
# Could raise gattlib.exception.NotFound if notification has not been registered
ret = gattlib_notification_stop(self.connection, self._gattlib_characteristic.uuid)
handle_return(ret)

View File

@ -0,0 +1,49 @@
#
# SPDX-License-Identifier: BSD-3-Clause
#
# Copyright (c) 2024, Olivier Martin <olivier@labapart.org>
#
"""Module for helper functions for Gattlib module."""
from gattlib import * #pylint: disable=wildcard-import,unused-wildcard-import
from .uuid import gattlib_uuid_to_int
def convert_gattlib_advertisement_c_data_to_dict(advertisement_c_data, advertisement_c_data_count,
manufacturer_c_data, manufacturer_c_data_count):
"""Helper function to convert advertisement and manufacturer c-data to Python dictionary"""
advertisement_data = {}
manufacturer_data = {}
for i in range(0, advertisement_c_data_count.value):
service_data = advertisement_c_data[i]
uuid = gattlib_uuid_to_int(service_data.uuid)
pointer_type = POINTER(c_byte * service_data.data_length)
c_bytearray = cast(service_data.data, pointer_type)
data = bytearray(service_data.data_length)
for i in range(service_data.data_length):
data[i] = c_bytearray.contents[i] & 0xFF
advertisement_data[uuid] = data
gattlib_free_mem(service_data.data)
for i in range(0, manufacturer_c_data_count.value):
_manufacturer_c_data = manufacturer_c_data[i]
pointer_type = POINTER(c_byte * _manufacturer_c_data.data_size.value)
c_bytearray = cast(_manufacturer_c_data.data, pointer_type)
data = bytearray(_manufacturer_c_data.data_size.value)
for j in range(_manufacturer_c_data.data_size.value):
data[j] = c_bytearray.contents[j] & 0xFF
manufacturer_data[_manufacturer_c_data.manufacturer_id] = data
gattlib_free_mem(_manufacturer_c_data.data)
gattlib_free_mem(advertisement_c_data)
gattlib_free_mem(manufacturer_c_data)
return advertisement_data, manufacturer_data

View File

@ -4,6 +4,8 @@
# Copyright (c) 2016-2024, Olivier Martin <olivier@labapart.org>
#
"""Module for exposing main loop for Gattlib execution."""
import threading
import time
import traceback
@ -19,7 +21,7 @@ task_exception: Exception = None
def _user_thread_main(task):
"""Main entry point for the thread that will run user's code."""
global gobject_mainloop, task_returned_code, task_exception
global task_returned_code, task_exception
try:
# Wait for GLib main loop to start running before starting user code.
@ -32,7 +34,7 @@ def _user_thread_main(task):
# Run user's code.
task_returned_code = task()
except Exception as ex:
except Exception as ex: #pylint: disable=broad-except
logger.error("Exception in %s: %s: %s", task, type(ex), str(ex))
traceback.print_exception(type(ex), ex, ex.__traceback__)
task_exception = ex
@ -40,7 +42,12 @@ def _user_thread_main(task):
gobject_mainloop.quit()
def run_mainloop_with(task):
global gobject_mainloop, task_returned_code, task_exception
"""
Run main loop with the given task.
The main loop ends when the task has completed.
"""
global gobject_mainloop
if gobject_mainloop:
raise RuntimeError("A mainloop is already running")

View File

@ -4,19 +4,22 @@
# Copyright (c) 2016-2024, Olivier Martin <olivier@labapart.org>
#
"""Module to manipulate Gattlib UUID in Python environment."""
import re
from uuid import UUID
from gattlib import *
from gattlib import * #pylint: disable=wildcard-import,unused-wildcard-import
SDP_UUID16 = 0x19
SDP_UUID32 = 0x1A
SDP_UUID128 = 0x1C
GATT_STANDARD_UUID_FORMAT = re.compile("(\S+)-0000-1000-8000-00805f9b34fb", flags=re.IGNORECASE)
GATT_STANDARD_UUID_FORMAT = re.compile(r"(\S+)-0000-1000-8000-00805f9b34fb", flags=re.IGNORECASE)
def gattlib_uuid_to_uuid(gattlib_uuid):
def gattlib_uuid_to_uuid(gattlib_uuid) -> UUID:
"""Convert Gattlib UUID to Python UUID"""
if gattlib_uuid.type == SDP_UUID16:
return UUID(fields=(gattlib_uuid.value.uuid16, 0x0000, 0x1000, 0x80, 0x00, 0x00805f9b34fb))
elif gattlib_uuid.type == SDP_UUID32:
@ -25,10 +28,11 @@ def gattlib_uuid_to_uuid(gattlib_uuid):
data = bytes(gattlib_uuid.value.uuid128.data)
return UUID(bytes=data)
else:
return ValueError("Gattlib UUID not recognized (type:0x%x)" % gattlib_uuid.type)
return ValueError(f"Gattlib UUID not recognized (type:0x{gattlib_uuid.type:02x})")
def gattlib_uuid_to_int(gattlib_uuid):
def gattlib_uuid_to_int(gattlib_uuid) -> int:
"""Convert Gattlib UUID to integer."""
if gattlib_uuid.type == SDP_UUID16:
return gattlib_uuid.value.uuid16
elif gattlib_uuid.type == SDP_UUID32:
@ -37,10 +41,11 @@ def gattlib_uuid_to_int(gattlib_uuid):
data = bytes(gattlib_uuid.value.uuid128.data)
return int.from_bytes(data, byteorder='big')
else:
return ValueError("Gattlib UUID not recognized (type:0x%x)" % gattlib_uuid.type)
return ValueError(f"Gattlib UUID not recognized (type:0x{gattlib_uuid.type:02x})")
def gattlib_uuid_str_to_int(uuid_str: str) -> int:
"""Convert uuid string to integer"""
# Check if the string could already encode a UUID16 or UUID32
if len(uuid_str) <= 8:
return int(uuid_str, 16)

View File

@ -14,6 +14,8 @@ from setuptools import setup, find_packages, Extension
from setuptools.command.build_ext import build_ext
import subprocess
SETUP_DIR = os.path.dirname(os.path.realpath(__file__))
# Name of the directory containing the python sources
python_module_name = "gattlib"
# Specified where the CMakeLists.txt is located
@ -23,14 +25,28 @@ native_source_dir = os.environ.get("NATIVE_SOURCE_DIR", ".")
git_version_command = subprocess.Popen(['git', 'describe', '--abbrev=7', '--dirty', '--always', '--tags'],
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
stdout, stderr = git_version_command.communicate()
git_version = stdout.decode('utf-8').strip()
if git_version_command.returncode == 0:
git_version = stdout.decode('utf-8').strip()
else:
git_version = None
#
# Create '_version.py'
#
package_version = os.environ.get('GATTLIB_PY_VERSION', git_version)
with open(os.path.join("gattlib", "_version.py"), "w") as f:
f.write(f"__version__ = \"{package_version}\"\n")
GATTLIB_VERSION_FILE = os.path.join(SETUP_DIR, "gattlib", "_version.py")
# Case we are building from source package
if package_version is None:
with open(GATTLIB_VERSION_FILE, "r") as f:
gattlib_version_statement = f.read()
res = re.search(r'__version__ = "(.*)"', gattlib_version_statement)
package_version = res.group(1)
if package_version:
with open(GATTLIB_VERSION_FILE, "w") as f:
f.write(f"__version__ = \"{package_version}\"\n")
class CMakeExtension(Extension):
@ -77,6 +93,8 @@ class CMakeBuild(build_ext):
f"-DCMAKE_LIBRARY_OUTPUT_DIRECTORY={cmake_library_output_dir}",
f"-DPYTHON_EXECUTABLE={sys.executable}",
f"-DCMAKE_BUILD_TYPE={cfg}", # not used on MSVC, but no harm
"-DGATTLIB_PYTHON_INTERFACE=ON",
"-DGATTLIB_BUILD_EXAMPLES=OFF",
]
build_args = []
# Adding CMake arguments set as environment variable
@ -161,7 +179,7 @@ setup(
author_email="olivier@labapart.com",
description="Python wrapper for gattlib library",
url="https://github.com/labapart/gattlib/gattlib-py",
long_description=open('README.md').read(),
long_description=open(os.path.join(SETUP_DIR, 'README.md')).read(),
long_description_content_type='text/markdown',
packages=find_packages(),
install_requires=[

View File

@ -52,6 +52,8 @@ extern "C" {
#define GATTLIB_NO_ADAPTER 8
#define GATTLIB_BUSY 9
#define GATTLIB_UNEXPECTED 10
#define GATTLIB_ADAPTER_CLOSE 11
#define GATTLIB_DEVICE_DISCONNECTED 12
#define GATTLIB_ERROR_MODULE_MASK 0xF0000000
#define GATTLIB_ERROR_DBUS 0x10000000
#define GATTLIB_ERROR_BLUEZ 0x20000000
@ -162,6 +164,15 @@ typedef struct {
size_t data_length; /**< Length of data attached to the GATT Service */
} gattlib_advertisement_data_t;
/**
* Structure to represent manufacturer data from GATT advertisement packet
*/
typedef struct {
uint16_t manufacturer_id;
uint8_t* data;
size_t data_size;
} gattlib_manufacturer_data_t;
typedef void (*gattlib_event_handler_t)(const uuid_t* uuid, const uint8_t* data, size_t data_length, void* user_data);
/**
@ -190,14 +201,13 @@ typedef void (*gattlib_discovered_device_t)(gattlib_adapter_t* adapter, const ch
* @param name is the name of BLE device if advertised
* @param advertisement_data is an array of Service UUID and their respective data
* @param advertisement_data_count is the number of elements in the advertisement_data array
* @param manufacturer_id is the ID of the Manufacturer ID
* @param manufacturer_data is the data following Manufacturer ID
* @param manufacturer_data_size is the size of manufacturer_data
* @param manufacturer_data is an array of `gattlib_manufacturer_data_t`
* @param manufacturer_data_count is the number of entry in `gattlib_manufacturer_data_t` array
* @param user_data Data defined when calling `gattlib_register_on_disconnect()`
*/
typedef void (*gattlib_discovered_device_with_data_t)(gattlib_adapter_t* adapter, const char* addr, const char* name,
gattlib_advertisement_data_t *advertisement_data, size_t advertisement_data_count,
uint16_t manufacturer_id, uint8_t *manufacturer_data, size_t manufacturer_data_size,
gattlib_manufacturer_data_t* manufacturer_data, size_t manufacturer_data_count,
void *user_data);
/**
@ -669,8 +679,8 @@ int gattlib_get_rssi(gattlib_connection_t *connection, int16_t *rssi);
/**
* @brief Function to retrieve RSSI from a MAC Address
*
* @note: This function is mainly used before a connection is established. Once the connection
* established, the function `gattlib_get_rssi()` should be preferred.
* @note: This function must be used before a connection is established. Once the connection
* established, the function will return a null RSSI.
*
* @param adapter is the adapter the new device has been seen
* @param mac_address is the MAC address of the device to get the RSSI
@ -686,15 +696,14 @@ int gattlib_get_rssi_from_mac(gattlib_adapter_t* adapter, const char *mac_addres
* @param connection Active GATT connection
* @param advertisement_data is an array of Service UUID and their respective data
* @param advertisement_data_count is the number of elements in the advertisement_data array
* @param manufacturer_id is the ID of the Manufacturer ID
* @param manufacturer_data is the data following Manufacturer ID
* @param manufacturer_data_size is the size of manufacturer_data
* @param manufacturer_data is an array of `gattlib_manufacturer_data_t`
* @param manufacturer_data_count is the number of entry in `gattlib_manufacturer_data_t` array
*
* @return GATTLIB_SUCCESS on success or GATTLIB_* error code
*/
int gattlib_get_advertisement_data(gattlib_connection_t *connection,
gattlib_advertisement_data_t **advertisement_data, size_t *advertisement_data_count,
uint16_t *manufacturer_id, uint8_t **manufacturer_data, size_t *manufacturer_data_size);
gattlib_manufacturer_data_t** manufacturer_data, size_t* manufacturer_data_count);
/**
* @brief Function to retrieve Advertisement Data from a MAC Address
@ -703,15 +712,14 @@ int gattlib_get_advertisement_data(gattlib_connection_t *connection,
* @param mac_address is the MAC address of the device to get the RSSI
* @param advertisement_data is an array of Service UUID and their respective data
* @param advertisement_data_count is the number of elements in the advertisement_data array
* @param manufacturer_id is the ID of the Manufacturer ID
* @param manufacturer_data is the data following Manufacturer ID
* @param manufacturer_data_size is the size of manufacturer_data
* @param manufacturer_data is an array of `gattlib_manufacturer_data_t`
* @param manufacturer_data_count is the number of entry in `gattlib_manufacturer_data_t` array
*
* @return GATTLIB_SUCCESS on success or GATTLIB_* error code
*/
int gattlib_get_advertisement_data_from_mac(gattlib_adapter_t* adapter, const char *mac_address,
gattlib_advertisement_data_t **advertisement_data, size_t *advertisement_data_count,
uint16_t *manufacturer_id, uint8_t **manufacturer_data, size_t *manufacturer_data_size);
gattlib_manufacturer_data_t** manufacturer_data, size_t* manufacturer_data_count);
/**
* @brief Convert a UUID into a string