diff --git a/.travis.yml b/.travis.yml index 3e7cbec..c5a02d6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -44,6 +44,8 @@ script: # # Setup build for ARM 32-bit # + # Ensure folder is empty + - rm -Rf $PWD/debian-stable-arm/* - cross-sysroot --distribution debian --distribution-version stable --architecture armhf --build-root $PWD/debian-stable-arm requirements.dep # Set environment variables @@ -58,6 +60,8 @@ script: # # Setup build for ARM 64-bit # + # Ensure folder is empty + - rm -Rf $PWD/debian-stable-arm64/* - cross-sysroot --distribution debian --distribution-version stable --architecture arm64 --build-root $PWD/debian-stable-arm64 requirements.dep # Set environment variables diff --git a/common/gattlib_common.c b/common/gattlib_common.c index 363191a..485a75c 100644 --- a/common/gattlib_common.c +++ b/common/gattlib_common.c @@ -1,13 +1,95 @@ +#if defined(WITH_PYTHON) + #include +#endif + +#include + #include "gattlib_internal.h" void gattlib_register_notification(gatt_connection_t* connection, gattlib_event_handler_t notification_handler, void* user_data) { - connection->notification_handler = notification_handler; - connection->notification_user_data = user_data; + connection->notification.type = NATIVE_NOTIFICATION; + connection->notification.notification_handler = notification_handler; + connection->notification.user_data = user_data; } void gattlib_register_indication(gatt_connection_t* connection, gattlib_event_handler_t indication_handler, void* user_data) { - connection->indication_handler = indication_handler; - connection->indication_user_data = user_data; + connection->indication.type = NATIVE_NOTIFICATION; + connection->indication.notification_handler = indication_handler; + connection->indication.user_data = user_data; +} + +void gattlib_register_on_disconnect(gatt_connection_t *connection, gattlib_disconnection_handler_t handler, void* user_data) { + connection->disconnection.type = NATIVE_DISCONNECTION; + connection->disconnection.disconnection_handler = handler; + connection->disconnection.user_data = user_data; +} + +#if defined(WITH_PYTHON) +void gattlib_register_notification_python(gatt_connection_t* connection, PyObject *notification_handler, PyObject *user_data) { + connection->notification.type = PYTHON; + connection->notification.python_handler = notification_handler; + connection->notification.user_data = user_data; +} + +void gattlib_register_indication_python(gatt_connection_t* connection, PyObject *indication_handler, PyObject *user_data) { + connection->indication.type = PYTHON; + connection->indication.python_handler = indication_handler; + connection->indication.user_data = user_data; +} + +void gattlib_register_on_disconnect_python(gatt_connection_t *connection, PyObject *handler, PyObject *user_data) { + connection->disconnection.type = PYTHON; + connection->disconnection.python_handler = handler; + connection->disconnection.user_data = user_data; +} +#endif + +bool gattlib_has_valid_handler(struct gattlib_handler *handler) { + return ((handler->type != UNKNOWN) && (handler->notification_handler != NULL)); +} + +void gattlib_call_notification_handler(struct gattlib_handler *handler, const uuid_t* uuid, const uint8_t* data, size_t data_length) { + if (handler->type == NATIVE_NOTIFICATION) { + handler->notification_handler(uuid, data, data_length, handler->user_data); + } +#if defined(WITH_PYTHON) + else if (handler->type == PYTHON) { + char uuid_str[MAX_LEN_UUID_STR + 1]; + PyGILState_STATE d_gstate; + + gattlib_uuid_to_string(uuid, uuid_str, sizeof(uuid_str)); + + d_gstate = PyGILState_Ensure(); + + PyObject *arglist = Py_BuildValue("(sIIO)", uuid_str, data, data_length, handler->user_data); + PyEval_CallObject((PyObject *)handler->notification_handler, arglist); + + PyGILState_Release(d_gstate); + } +#endif + else { + fprintf(stderr, "Invalid notification handler.\n"); + } +} + +void gattlib_call_disconnection_handler(struct gattlib_handler *handler) { + if (handler->type == NATIVE_NOTIFICATION) { + handler->disconnection_handler(handler->user_data); + } +#if defined(WITH_PYTHON) + else if (handler->type == PYTHON) { + PyGILState_STATE d_gstate; + d_gstate = PyGILState_Ensure(); + + PyObject *arglist = Py_BuildValue("(O)", handler->user_data); + PyEval_CallObject((PyObject *)handler->disconnection_handler, arglist); + + PyGILState_Release(d_gstate); + } +#endif + else { + fprintf(stderr, "Invalid disconnection handler.\n"); + } } void bt_uuid_to_uuid(bt_uuid_t* bt_uuid, uuid_t* uuid) { diff --git a/common/gattlib_internal_defs.h b/common/gattlib_internal_defs.h index 7517a5d..3b04cc8 100644 --- a/common/gattlib_internal_defs.h +++ b/common/gattlib_internal_defs.h @@ -1,19 +1,32 @@ #ifndef __GATTLIB_INTERNAL_DEFS_H__ #define __GATTLIB_INTERNAL_DEFS_H__ +#include + #include "gattlib.h" +enum handler_type { UNKNOWN = 0, NATIVE_NOTIFICATION, NATIVE_DISCONNECTION, PYTHON }; + +struct gattlib_handler { + enum handler_type type; + union { + gattlib_event_handler_t notification_handler; + gattlib_disconnection_handler_t disconnection_handler; + void* python_handler; + }; + void* user_data; +}; + struct _gatt_connection_t { void* context; - gattlib_event_handler_t notification_handler; - void* notification_user_data; - - gattlib_event_handler_t indication_handler; - void* indication_user_data; - - gattlib_disconnection_handler_t disconnection_handler; - void* disconnection_user_data; + struct gattlib_handler notification; + struct gattlib_handler indication; + struct gattlib_handler disconnection; }; +bool gattlib_has_valid_handler(struct gattlib_handler *handler); +void gattlib_call_disconnection_handler(struct gattlib_handler *handler); +void gattlib_call_notification_handler(struct gattlib_handler *handler, const uuid_t* uuid, const uint8_t* data, size_t data_length); + #endif diff --git a/dbus/CMakeLists.txt b/dbus/CMakeLists.txt index 74708d1..d78626b 100644 --- a/dbus/CMakeLists.txt +++ b/dbus/CMakeLists.txt @@ -93,6 +93,21 @@ endif() set(gattlib_LIBS ${GLIB_LDFLAGS} ${GIO_UNIX_LDFLAGS}) +# +# Add Python Support +# +pkg_search_module(PYTHON python3) +if (NOT PYTHON_FOUND) + pkg_search_module(PYTHON python) +endif() + +if (PYTHON_FOUND) + include_directories(${PYTHON_INCLUDE_DIRS}) + list(APPEND gattlib_LIBS ${PYTHON_LDFLAGS}) + + add_definitions(-DWITH_PYTHON) +endif() + # Gattlib add_library(gattlib SHARED ${gattlib_SRCS}) target_link_libraries(gattlib ${gattlib_LIBS}) diff --git a/dbus/gattlib.c b/dbus/gattlib.c index 0688185..fc29e14 100644 --- a/dbus/gattlib.c +++ b/dbus/gattlib.c @@ -280,11 +280,9 @@ gboolean on_handle_device_property_change( while (g_variant_iter_loop (iter, "{&sv}", &key, &value)) { if (strcmp(key, "Connected") == 0) { if (!g_variant_get_boolean(value)) { - printf("on_handle_device_property_change: Connected FALSE\n"); // Disconnection case - if (connection->disconnection_handler) { - printf("[C] Call disconnection handler:%p\n", connection->disconnection_handler); - connection->disconnection_handler(connection->disconnection_user_data); + if (gattlib_has_valid_handler(&connection->disconnection)) { + gattlib_call_disconnection_handler(&connection->disconnection); } } } else if (strcmp(key, "ServicesResolved") == 0) { @@ -447,11 +445,6 @@ int gattlib_disconnect(gatt_connection_t* connection) { return GATTLIB_SUCCESS; } -void gattlib_register_on_disconnect(gatt_connection_t *connection, gattlib_disconnection_handler_t handler, void* user_data) { - connection->disconnection_handler = handler; - connection->disconnection_user_data = user_data; -} - // Bluez was using org.bluez.Device1.GattServices until 5.37 to expose the list of available GATT Services #if BLUEZ_VERSION < BLUEZ_VERSIONS(5, 38) int gattlib_discover_primary(gatt_connection_t* connection, gattlib_primary_service_t** services, int* services_count) { @@ -1417,7 +1410,7 @@ gboolean on_handle_battery_level_property_change( static guint8 percentage; gatt_connection_t* connection = user_data; - if (connection->notification_handler) { + if (gattlib_has_valid_handler(&connection->notification)) { // Retrieve 'Value' from 'arg_changed_properties' if (g_variant_n_children (arg_changed_properties) > 0) { GVariantIter *iter; @@ -1431,9 +1424,9 @@ gboolean on_handle_battery_level_property_change( // GATT connection notifiying to Battery level percentage = g_variant_get_byte(value); - connection->notification_handler(&m_battery_level_uuid, - (const uint8_t*)&percentage, sizeof(percentage), - connection->notification_user_data); + gattlib_call_notification_handler(&connection->notification, + &m_battery_level_uuid, + (const uint8_t*)&percentage, sizeof(percentage)); break; } } @@ -1451,7 +1444,7 @@ static gboolean on_handle_characteristic_property_change( { gatt_connection_t* connection = user_data; - if (connection->notification_handler) { + if (gattlib_has_valid_handler(&connection->notification)) { // Retrieve 'Value' from 'arg_changed_properties' if (g_variant_n_children (arg_changed_properties) > 0) { GVariantIter *iter; @@ -1470,7 +1463,8 @@ static gboolean on_handle_characteristic_property_change( MAX_LEN_UUID_STR + 1, &uuid); - connection->notification_handler(&uuid, data, data_length, connection->notification_user_data); + gattlib_call_notification_handler(&connection->notification, + &uuid, data, data_length); break; } } diff --git a/examples/ble_scan/ble_scan.py b/examples/ble_scan/ble_scan.py new file mode 100755 index 0000000..6da5d8d --- /dev/null +++ b/examples/ble_scan/ble_scan.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python3 + +# export LD_LIBRARY_PATH=/home/olivier/dev/gattlib/build/dbus/:$LD_LIBRARY_PATH + +from gattlib import adapter + +adapters = adapter.Adapter.list() +print("BLE Adapters: %s" % adapters) + + +def on_discovered_device(device): + print("Discovered '%s'" % device) + # device.connect() + # device.discover() + + +default_adapter = adapter.Adapter() + +default_adapter.open() +default_adapter.scan_enable(on_discovered_device, 10) diff --git a/gattlib-py/TODO.md b/gattlib-py/TODO.md new file mode 100644 index 0000000..8777cda --- /dev/null +++ b/gattlib-py/TODO.md @@ -0,0 +1,4 @@ +- Add advertisement data + +- Add C function to list BLE adapters +- Add indication support diff --git a/gattlib-py/examples/nordic_thingy/nordic_thingy.py b/gattlib-py/examples/nordic_thingy/nordic_thingy.py new file mode 100755 index 0000000..91a65c2 --- /dev/null +++ b/gattlib-py/examples/nordic_thingy/nordic_thingy.py @@ -0,0 +1,193 @@ +#!/usr/bin/env python3 + +import argparse +import struct +import sys +import threading + +from dbus.mainloop.glib import DBusGMainLoop +try: + from gi.repository import GLib, GObject +except ImportError: + import gobject as GObject +import sys + +import numpy +from matplotlib.pylab import * +from mpl_toolkits.axes_grid1 import host_subplot +import matplotlib.animation as animation + +from gattlib import device, uuid + +last_measures = { + 'temperature': { 'value': None, 'min': None, 'max': None }, + 'pressure': { 'value': None, 'min': None, 'max': None }, + 'humidity': { 'value': None, 'min': None, 'max': None }, +} + + +def temperature_notification(value, user_data): + last_measures['temperature']['value'] = float("%d.%d" % (value[0], value[1])) + print("Temperature: %f" % last_measures['temperature']['value']) + + +def pressure_notification(value, user_data): + (pressure_integer, pressure_decimal) = struct.unpack("= xmax - 1.00: + temp_line.axes.set_xlim(x - xmax + 1.0, x + 1.0) + hum_line.axes.set_xlim(x - xmax + 1.0, x + 1.0) + + return temp_line, hum_line + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description='Gattlib example for Nordic Thingy') + parser.add_argument('mac', type=str, help='Mac Address of the GATT device to connect') + args = parser.parse_args() + + NORDIC_THINGY_WEATHER_STATION_SERVICE = uuid.gattlib_uuid_str_to_int("EF680200-9B35-4933-9B10-52FFA9740042") + NORDIC_THINGY_TEMPERATURE_CHAR = uuid.gattlib_uuid_str_to_int("EF680201-9B35-4933-9B10-52FFA9740042") + NORDIC_THINGY_PRESSURE_CHAR = uuid.gattlib_uuid_str_to_int("EF680202-9B35-4933-9B10-52FFA9740042") + NORDIC_THINGY_HUMIDITY_CHAR = uuid.gattlib_uuid_str_to_int("EF680203-9B35-4933-9B10-52FFA9740042") + NORDIC_THINGY_AIR_QUALITY_CHAR = uuid.gattlib_uuid_str_to_int("EF680204-9B35-4933-9B10-52FFA9740042") + + gatt_device = device.Device(adapter=None, addr=args.mac) + gatt_device.connect() + + temperature_characteristic = gatt_device.characteristics[NORDIC_THINGY_TEMPERATURE_CHAR] + pressure_characteristic = gatt_device.characteristics[NORDIC_THINGY_PRESSURE_CHAR] + humidity_characteristic = gatt_device.characteristics[NORDIC_THINGY_HUMIDITY_CHAR] + air_quality_characteristic = gatt_device.characteristics[NORDIC_THINGY_AIR_QUALITY_CHAR] + + # Initialize graph + threading.Thread(target=graph_init).start() + + try: + DBusGMainLoop(set_as_default=True) + mainloop = GLib.MainLoop() + + temperature_characteristic.register_notification(temperature_notification) + temperature_characteristic.notification_start() + + pressure_characteristic.register_notification(pressure_notification) + pressure_characteristic.notification_start() + + humidity_characteristic.register_notification(humidity_notification) + humidity_characteristic.notification_start() + + mainloop.run() + except KeyboardInterrupt: + mainloop.quit() + finally: + humidity_characteristic.notification_stop() + pressure_characteristic.notification_stop() + temperature_characteristic.notification_stop() + gatt_device.disconnect() diff --git a/gattlib-py/examples/read_write/read_write.py b/gattlib-py/examples/read_write/read_write.py new file mode 100755 index 0000000..d57ec39 --- /dev/null +++ b/gattlib-py/examples/read_write/read_write.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python3 + +import argparse + +from gattlib import device, uuid + +parser = argparse.ArgumentParser(description='Gattlib read_write example') +parser.add_argument('mac', type=str, help='Mac Address of the GATT device to connect') +parser.add_argument('action', choices=['read', 'write'], help='Tell if we want to read/write the GATT characteristic') +parser.add_argument('uuid', type=str, help='UUID of the GATT Characteristic') +parser.add_argument('value', type=str, nargs='?', help='Value to write to the GATT characteristic') +args = parser.parse_args() + +gatt_device = device.Device(adapter=None, addr=args.mac) +gatt_device.connect() + +uuid = uuid.gattlib_uuid_str_to_int(args.uuid) +if uuid not in gatt_device.characteristics: + raise RuntimeError("Failed to find GATT characteristic '%s'" % args.uuid) + +characteristic = gatt_device.characteristics[uuid] + +if args.action == "read": + value = characteristic.read() + print(value) +elif args.action == "write": + characteristic.write(value) + +gatt_device.disconnect() diff --git a/gattlib-py/gattlib/__init__.py b/gattlib-py/gattlib/__init__.py new file mode 100644 index 0000000..7a0112b --- /dev/null +++ b/gattlib-py/gattlib/__init__.py @@ -0,0 +1,90 @@ +from ctypes import * + +gattlib = CDLL("libgattlib.so") + + +# typedef struct { +# uint8_t data[16]; +# } uint128_t; +class GattlibUuid128(Structure): + _fields_ = [("data", c_byte * 16)] + + +# typedef struct { +# uint8_t type; +# union { +# uint16_t uuid16; +# uint32_t uuid32; +# uint128_t uuid128; +# } value; +# } uuid_t; +class GattlibUuidValue(Union): + _fields_ = [("uuid16", c_ushort), ("uuid32", c_uint), ("uuid128", GattlibUuid128)] + + +class GattlibUuid(Structure): + _fields_ = [("type", c_byte), ("value", GattlibUuidValue)] + + +# typedef struct { +# uint16_t attr_handle_start; +# uint16_t attr_handle_end; +# uuid_t uuid; +# } gattlib_primary_service_t; +class GattlibPrimaryService(Structure): + _fields_ = [("attr_handle_start", c_ushort), + ("attr_handle_end", c_ushort), + ("uuid", GattlibUuid)] + + +# typedef struct { +# uint16_t handle; +# uint8_t properties; +# uint16_t value_handle; +# uuid_t uuid; +# } gattlib_characteristic_t; +class GattlibCharacteristic(Structure): + _fields_ = [("handle", c_ushort), + ("properties", c_byte), + ("value_handle", c_ushort), + ("uuid", GattlibUuid)] + + +# int gattlib_adapter_open(const char* adapter_name, void** adapter); +gattlib_adapter_open = gattlib.gattlib_adapter_open +gattlib_adapter_open.argtypes = [c_char_p, POINTER(c_void_p)] + +# typedef void (*gattlib_discovered_device_t)(const char* addr, const char* name) +gattlib_discovered_device_type = CFUNCTYPE(None, c_char_p, c_char_p) + +# int gattlib_discover_primary(gatt_connection_t* connection, gattlib_primary_service_t** services, int* services_count); +gattlib_discover_primary = gattlib.gattlib_discover_primary +gattlib_discover_primary.argtypes = [c_void_p, POINTER(POINTER(GattlibPrimaryService)), POINTER(c_int)] + +# int gattlib_discover_char(gatt_connection_t* connection, gattlib_characteristic_t** characteristics, int* characteristic_count); +gattlib_discover_char = gattlib.gattlib_discover_char +gattlib_discover_char.argtypes = [c_void_p, POINTER(POINTER(GattlibCharacteristic)), POINTER(c_int)] + +# int gattlib_read_char_by_uuid(gatt_connection_t* connection, uuid_t* uuid, void** buffer, size_t* buffer_len); +gattlib_read_char_by_uuid = gattlib.gattlib_read_char_by_uuid +gattlib_read_char_by_uuid.argtypes = [c_void_p, POINTER(GattlibUuid), POINTER(c_void_p), POINTER(c_size_t)] + +# int gattlib_write_char_by_uuid(gatt_connection_t* connection, uuid_t* uuid, const void* buffer, size_t buffer_len) +gattlib_write_char_by_uuid = gattlib.gattlib_write_char_by_uuid +gattlib_write_char_by_uuid.argtypes = [c_void_p, POINTER(GattlibUuid), c_void_p, c_size_t] + +# int gattlib_notification_start(gatt_connection_t* connection, const uuid_t* uuid); +gattlib_notification_start = gattlib.gattlib_notification_start +gattlib_notification_start.argtypes = [c_void_p, POINTER(GattlibUuid)] + +# int gattlib_notification_stop(gatt_connection_t* connection, const uuid_t* uuid); +gattlib_notification_stop = gattlib.gattlib_notification_stop +gattlib_notification_stop.argtypes = [c_void_p, POINTER(GattlibUuid)] + +# void gattlib_register_notification_python(gatt_connection_t* connection, PyObject *notification_handler, PyObject *user_data) +gattlib_register_notification = gattlib.gattlib_register_notification_python +gattlib_register_notification.argtypes = [c_void_p, py_object, py_object] + +# void gattlib_register_on_disconnect_python(gatt_connection_t *connection, PyObject *handler, PyObject *user_data) +gattlib_register_on_disconnect = gattlib.gattlib_register_on_disconnect_python +gattlib_register_on_disconnect.argtypes = [c_void_p, py_object, py_object] diff --git a/gattlib-py/gattlib/adapter.py b/gattlib-py/gattlib/adapter.py new file mode 100644 index 0000000..2c97c71 --- /dev/null +++ b/gattlib-py/gattlib/adapter.py @@ -0,0 +1,38 @@ +from gattlib import * +from .device import Device +from .exception import handle_return + +class Adapter: + def __init__(self, name=c_char_p(None)): + self._name = name + self._adapter = c_void_p(None) + + @property + def name(self): + return self._name + + @staticmethod + def list(): + #TODO: Add support + return [] + + def open(self): + return gattlib_adapter_open(self._name, byref(self._adapter)) + + def close(self): + return gattlib.gattlib_adapter_close(self._adapter) + + def on_discovered_device(self, addr, name): + device = Device(self, addr, name) + self.on_discovered_device_callback(device) + + def scan_enable(self, on_discovered_device_callback, timeout): + assert on_discovered_device_callback != None + self.on_discovered_device_callback = on_discovered_device_callback + + ret = gattlib.gattlib_adapter_scan_enable(self._adapter, gattlib_discovered_device_type(self.on_discovered_device), timeout) + handle_return(ret) + + def scan_disable(self): + ret = gattlib.gattlib_adapter_scan_disable(self._adapter) + handle_return(ret) diff --git a/gattlib-py/gattlib/device.py b/gattlib-py/gattlib/device.py new file mode 100644 index 0000000..baab058 --- /dev/null +++ b/gattlib-py/gattlib/device.py @@ -0,0 +1,164 @@ +import logging +import uuid + +from gattlib import * +from .exception import handle_return, DeviceError +from .gatt import GattService, GattCharacteristic +from .uuid import gattlib_uuid_to_int + +CONNECTION_OPTIONS_LEGACY_BDADDR_LE_PUBLIC = (1 << 0) +CONNECTION_OPTIONS_LEGACY_BDADDR_LE_RANDOM = (1 << 1) +CONNECTION_OPTIONS_LEGACY_BT_SEC_LOW = (1 << 2) +CONNECTION_OPTIONS_LEGACY_BT_SEC_MEDIUM = (1 << 3) +CONNECTION_OPTIONS_LEGACY_BT_SEC_HIGH = (1 << 4) + +CONNECTION_OPTIONS_LEGACY_DEFAULT = \ + CONNECTION_OPTIONS_LEGACY_BDADDR_LE_PUBLIC | \ + CONNECTION_OPTIONS_LEGACY_BDADDR_LE_RANDOM | \ + CONNECTION_OPTIONS_LEGACY_BT_SEC_LOW + + +class Device: + + def __init__(self, adapter, addr, name=None): + self._adapter = adapter + if type(addr) == str: + self._addr = addr.encode("utf-8") + else: + self._addr = addr + self._name = name + self._connection = c_void_p(None) + + # Keep track if notification handler has been initialized + self._is_notification_init = False + + # Dictionnary for GATT characteristic callback + self._gatt_characteristic_callbacks = {} + + @property + def id(self): + return self._addr.decode("utf-8") + + @property + def connection(self): + return self._connection + + def connect(self, options=CONNECTION_OPTIONS_LEGACY_DEFAULT): + if self._adapter: + adapter_name = self._adapter.name + else: + adapter_name = None + + self._connection = gattlib.gattlib_connect(adapter_name, self._addr, options) + if self._connection == 0: + raise DeviceError() + + @staticmethod + def disconnection_callback(user_data): + this = user_data + + this.disconnection_callback(this.disconnection_user_data) + + def register_on_disconnect(self, callback, user_data): + self.disconnection_callback = callback + self.disconnection_user_data = user_data + + gattlib.gattlib_register_on_disconnect(self._connection, Device.disconnection_callback, self) + + def disconnect(self): + ret = gattlib.gattlib_disconnect(self._connection) + handle_return(ret) + + def discover(self): + # + # Discover GATT Services + # + _services = POINTER(GattlibPrimaryService)() + _services_count = c_int(0) + ret = gattlib_discover_primary(self._connection, byref(_services), byref(_services_count)) + handle_return(ret) + + self._services = {} + for i in range(0, _services_count.value): + service = GattService(self, _services[i]) + self._services[service.short_uuid] = service + + logging.debug("Service UUID:0x%x" % service.short_uuid) + + # + # Discover GATT Characteristics + # + _characteristics = POINTER(GattlibCharacteristic)() + _characteristics_count = c_int(0) + ret = gattlib_discover_char(self._connection, byref(_characteristics), byref(_characteristics_count)) + handle_return(ret) + + self._characteristics = {} + for i in range(0, _characteristics_count.value): + characteristic = GattCharacteristic(self, _characteristics[i]) + self._characteristics[characteristic.short_uuid] = characteristic + + logging.debug("Characteristic UUID:0x%x" % characteristic.short_uuid) + + @property + def services(self): + if not hasattr(self, '_services'): + logging.warning("Start GATT discovery implicitly") + self.discover() + + return self._services + + @property + def characteristics(self): + if not hasattr(self, '_characteristics'): + logging.warning("Start GATT discovery implicitly") + self.discover() + + return self._characteristics + + @staticmethod + def notification_callback(uuid_str, data, data_len, user_data): + this = user_data + + notification_uuid = uuid.UUID(uuid_str) + + short_uuid = notification_uuid.int + if short_uuid not in this._gatt_characteristic_callbacks: + raise RuntimeError("UUID '%s' is expected to be part of the notification list") + else: + characteristic_callback = this._gatt_characteristic_callbacks[short_uuid] + + # value = bytearray(data_len) + # for i in range(data_len): + # value[i] = data[i] + + pointer_type = POINTER(c_byte * data_len) + c_bytearray = cast(data, pointer_type) + + value = bytearray(data_len) + for i in range(data_len): + value[i] = c_bytearray.contents[i] & 0xFF + + # Call GATT characteristic Notification callback + characteristic_callback['callback'](value, characteristic_callback['user_data']) + + def _notification_init(self): + if self._is_notification_init: + return + + self._is_notification_init = True + + gattlib_register_notification(self._connection, Device.notification_callback, self) + + def _notification_add_gatt_characteristic_callback(self, gatt_characteristic, callback, user_data): + if not self._is_notification_init: + self._notification_init() + + self._gatt_characteristic_callbacks[gatt_characteristic.short_uuid] = { 'callback': callback, 'user_data': user_data } + + def __str__(self): + name = self._name + if name: + return str(name) + else: + return str(self._addr) diff --git a/gattlib-py/gattlib/exception.py b/gattlib-py/gattlib/exception.py new file mode 100644 index 0000000..8702f43 --- /dev/null +++ b/gattlib-py/gattlib/exception.py @@ -0,0 +1,44 @@ +GATTLIB_SUCCESS = 0 +GATTLIB_INVALID_PARAMETER = 1 +GATTLIB_NOT_FOUND = 2 +GATTLIB_OUT_OF_MEMORY = 3 +GATTLIB_NOT_SUPPORTED = 4 +GATTLIB_DEVICE_ERROR = 5 +GATTLIB_ERROR_DBUS = 6 + +class GattlibException(Exception): + pass + +class InvalidParameter(GattlibException): + pass + +class NotFound(GattlibException): + pass + +class OutOfMemory(GattlibException): + pass + +class NotSupported(GattlibException): + pass + +class DeviceError(GattlibException): + pass + +class DBusError(GattlibException): + pass + +def handle_return(ret): + if ret == GATTLIB_INVALID_PARAMETER: + raise InvalidParameter() + elif ret == GATTLIB_NOT_FOUND: + raise NotFound() + elif ret == GATTLIB_OUT_OF_MEMORY: + raise OutOfMemory() + elif ret == GATTLIB_NOT_SUPPORTED: + raise NotSupported() + elif ret == GATTLIB_DEVICE_ERROR: + raise DeviceError() + elif ret == GATTLIB_ERROR_DBUS: + raise DBusError() + elif ret != 0: + raise RuntimeError("Gattlib exception %d" % ret) diff --git a/gattlib-py/gattlib/gatt.py b/gattlib-py/gattlib/gatt.py new file mode 100644 index 0000000..fa98272 --- /dev/null +++ b/gattlib-py/gattlib/gatt.py @@ -0,0 +1,77 @@ +from gattlib import * +from .uuid import gattlib_uuid_to_uuid, gattlib_uuid_to_int +from .exception import handle_return + + +class GattService(): + + def __init__(self, device, gattlib_primary_service): + self._device = device + self._gattlib_primary_service = gattlib_primary_service + + @property + def uuid(self): + return gattlib_uuid_to_uuid(self._gattlib_primary_service.uuid) + + @property + def short_uuid(self): + return gattlib_uuid_to_int(self._gattlib_primary_service.uuid) + + +class GattCharacteristic(): + + def __init__(self, device, gattlib_characteristic): + self._device = device + self._gattlib_characteristic = gattlib_characteristic + + @property + def uuid(self): + return gattlib_uuid_to_uuid(self._gattlib_characteristic.uuid) + + @property + def short_uuid(self): + return gattlib_uuid_to_int(self._gattlib_characteristic.uuid) + + @property + def connection(self): + return self._device.connection + + def read(self, callback=None): + if callback: + raise RuntimeError("Not supported yet") + else: + _buffer = c_void_p(None) + _buffer_len = c_size_t(0) + + ret = gattlib_read_char_by_uuid(self.connection, self._gattlib_characteristic.uuid, byref(_buffer), byref(_buffer_len)) + + pointer_type = POINTER(c_byte * _buffer_len.value) + c_bytearray = cast(_buffer, pointer_type) + + value = bytearray(_buffer_len.value) + for i in range(_buffer_len.value): + value[i] = c_bytearray.contents[i] + + return value + + def write(self, data): + buffer_type = c_char * len(data) + buffer = data + buffer_len = len(data) + + ret = gattlib_write_char_by_uuid(self.connection, self._gattlib_characteristic.uuid, buffer_type.from_buffer_copy(buffer), buffer_len) + handle_return(ret) + + def register_notification(self, callback, user_data=None): + self._device._notification_add_gatt_characteristic_callback(self, callback, user_data) + + def notification_start(self): + ret = gattlib_notification_start(self.connection, self._gattlib_characteristic.uuid) + handle_return(ret) + + def notification_stop(self): + ret = gattlib_notification_stop(self.connection, self._gattlib_characteristic.uuid) + handle_return(ret) + + def __str__(self): + return str(self.uuid) diff --git a/gattlib-py/gattlib/uuid.py b/gattlib-py/gattlib/uuid.py new file mode 100644 index 0000000..62194dc --- /dev/null +++ b/gattlib-py/gattlib/uuid.py @@ -0,0 +1,47 @@ +import re +from uuid import UUID + +from gattlib import * + +SDP_UUID16 = 0x19 +SDP_UUID32 = 0x1A +SDP_UUID128 = 0x1C + +GATT_STANDARD_UUID_FORMAT = re.compile("(\S+)-0000-1000-8000-00805f9b34fb", flags=re.IGNORECASE) + + +def gattlib_uuid_to_uuid(gattlib_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: + return UUID(fields=(gattlib_uuid.value.uuid32, 0x0000, 0x1000, 0x80, 0x00, 0x00805f9b34fb)) + elif gattlib_uuid.type == SDP_UUID128: + data = bytes(gattlib_uuid.value.uuid128.data) + return UUID(bytes=data) + else: + return ValueError("Gattlib UUID not recognized (type:0x%x)" % gattlib_uuid.type) + + +def gattlib_uuid_to_int(gattlib_uuid): + if gattlib_uuid.type == SDP_UUID16: + return gattlib_uuid.value.uuid16 + elif gattlib_uuid.type == SDP_UUID32: + return gattlib_uuid.value.uuid32 + elif gattlib_uuid.type == SDP_UUID128: + 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) + + +def gattlib_uuid_str_to_int(uuid_str): + # Check if the string could already encode a UUID16 or UUID32 + if len(uuid_str) <= 8: + return int(uuid_str, 16) + + # Check if it is a standard UUID or not + match = GATT_STANDARD_UUID_FORMAT.search(uuid_str) + if match: + return int(match.group(1), 16) + else: + return UUID(uuid_str).int diff --git a/requirements.dep b/requirements.dep index 7380eee..9bb0864 100644 --- a/requirements.dep +++ b/requirements.dep @@ -1,3 +1,5 @@ libbluetooth3 libbluetooth-dev -libglib2.0-dev \ No newline at end of file +libglib2.0-dev +python3-dev +libpython3-dev \ No newline at end of file