├── .gitignore ├── LICENSE ├── README.md ├── examples ├── connect.py ├── discovery.py ├── read_descriptor.py └── read_firmware_version.py ├── gatt ├── __init__.py ├── errors.py ├── gatt.py ├── gatt_linux.py └── gatt_stubs.py ├── gattctl.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | venv/ 83 | ENV/ 84 | 85 | # Spyder project settings 86 | .spyderproject 87 | 88 | # Rope project settings 89 | .ropeproject 90 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Senic GmbH 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bluetooth GATT SDK for Python 2 | The Bluetooth GATT SDK for Python helps you implementing and communicating with any Bluetooth Low Energy device that has a GATT profile. As of now it supports: 3 | 4 | * Discovering nearby Bluetooth Low Energy devices 5 | * Connecting and disconnecting devices 6 | * Implementing your custom GATT profile 7 | * Accessing all GATT services 8 | * Accessing all GATT characteristics 9 | * Reading characteristic values 10 | * Writing characteristic values 11 | * Subscribing for characteristic value change notifications 12 | 13 | Currently Linux is the only platform supported by this library. Unlike other libraries this GATT SDK is based directly on the mature and stable D-Bus API of BlueZ to interact with Bluetooth devices. In the future we would like to make this library platform-independent by integrating with more Bluetooth APIs of other operating systems such as MacOS and Windows. 14 | 15 | ## Prerequisites 16 | The GATT SDK requires [Python 3.4+](https://www.python.org). Currently Linux is the only supported operating system and therefor it needs a recent installation of [BlueZ](http://www.bluez.org/). It is tested to work fine with BlueZ 5.44, any release >= 5.38 should however work, too. 17 | 18 | ## Installation 19 | These instructions assume a Debian-based Linux. 20 | 21 | On Linux the [BlueZ](http://www.bluez.org/) library is necessary to access your built-in Bluetooth controller or Bluetooth USB dongle. Some Linux distributions provide a more up-to-date BlueZ package, some other distributions only install older versions that don't implement all Bluetooth features needed for this SDK. In those cases you want to either update BlueZ or build it from sources. 22 | 23 | ### Updating/installing BlueZ via apt-get 24 | 25 | 1. `bluetoothd --version` Obtains the version of the pre-installed BlueZ. `bluetoothd` daemon must run at startup to expose the Bluetooth API via D-Bus. 26 | 2. `sudo apt-get install --no-install-recommends bluetooth` Installs BlueZ 27 | 3. If the installed version is too old, proceed with next step: [Installing BlueZ from sources](#installing-bluez-from-sources) 28 | 29 | ### Installing BlueZ from sources 30 | 31 | The `bluetoothd` daemon provides BlueZ's D-Bus interfaces that is accessed by the GATT SDK to communicate with Bluetooth devices. The following commands download BlueZ 5.44 sources, built them and replace any pre-installed `bluetoothd` daemon. It's not suggested to remove any pre-installed BlueZ package as its deinstallation might remove necessary Bluetooth drivers as well. 32 | 33 | 1. `sudo systemctl stop bluetooth` 34 | 2. `sudo apt-get update` 35 | 3. `sudo apt-get install libusb-dev libdbus-1-dev libglib2.0-dev libudev-dev libical-dev libreadline-dev libdbus-glib-1-dev unzip` 36 | 4. `cd` 37 | 5. `mkdir bluez` 38 | 6. `cd bluez` 39 | 7. `wget http://www.kernel.org/pub/linux/bluetooth/bluez-5.44.tar.xz` 40 | 8. `tar xf bluez-5.44.tar.xz` 41 | 9. `cd bluez-5.44` 42 | 10. `./configure --prefix=/usr --sysconfdir=/etc --localstatedir=/var --enable-library` 43 | 11. `make` 44 | 12. `sudo make install` 45 | 13. `sudo ln -svf /usr/libexec/bluetooth/bluetoothd /usr/sbin/` 46 | 14. `sudo install -v -dm755 /etc/bluetooth` 47 | 15. `sudo install -v -m644 src/main.conf /etc/bluetooth/main.conf` 48 | 16. `sudo systemctl daemon-reload` 49 | 17. `sudo systemctl start bluetooth` 50 | 18. `bluetoothd --version` # should now print 5.44 51 | 52 | Please note that some distributions might use a different directory for system deamons, apply step 13 only as needed. 53 | 54 | ### Enabling your Bluetooth adapter 55 | 56 | 1. `echo "power on" | sudo bluetoothctl` Enables your built-in Bluetooth adapter or external Bluetooth USB dongle 57 | 58 | ### Using BlueZ commandline tools 59 | BlueZ also provides an interactive commandline tool to interact with Bluetooth devices. You know that your BlueZ installation is working fine if it discovers any Bluetooth devices nearby. 60 | 61 | `sudo bluetoothctl` Starts an interactive mode to talk to BlueZ 62 | * `power on` Enables the Bluetooth adapter 63 | * `scan on` Start Bluetooth device scanning and lists all found devices with MAC addresses 64 | * `connect AA:BB:CC:DD:EE:FF` Connects to a Bluetooth device with specified MAC address 65 | * `exit` Quits the interactive mode 66 | 67 | ### Installing GATT SDK for Python 68 | 69 | To install this GATT module and the Python3 D-Bus dependency globally, run: 70 | 71 | ``` 72 | sudo pip3 install gatt 73 | sudo apt-get install python3-dbus 74 | ``` 75 | 76 | #### Running the GATT control script 77 | 78 | To test if your setup is working, run the `gattctl` tool that is part of this SDK. Note that it must be run as root because on Linux, Bluetooth discovery by default is a restricted operation. 79 | 80 | ``` 81 | sudo gattctl --discover 82 | sudo gattctl --connect AA:BB:CC:DD:EE:FF # Replace the MAC address with your Bluetooth device's MAC address 83 | sudo gattctl --help # To list all available commands 84 | ``` 85 | 86 | ## SDK Usage 87 | 88 | This SDK requires you to create subclasses of `gatt.DeviceManager` and `gatt.Device`. The other two classes `gatt.Service` and `gatt.Characteristic` are not supposed to be subclassed. 89 | 90 | `gatt.DeviceManager` manages all known Bluetooth devices and provides a device discovery to discover nearby Bluetooth Low Energy devices. You want to subclass this manager to access Bluetooth devices as they are discovered as well as to restrict the set of devices to those that you actually want to support by your manager implementation. By default `gatt.DeviceManager` discovers and returns all Bluetooth devices but you can restrict that by overriding `gatt.DeviceManager.make_device()`. 91 | 92 | `gatt.Device` is the base class for your Bluetooth device. You will need to subclass it to implement the Bluetooth GATT profile of your choice. Override `gatt.Device.services_resolved()` to interact with the GATT profile, i.e. start reading from and writing to characteristics or subscribe to characteristic value change notifications. 93 | 94 | ### Discovering nearby Bluetooth Low Energy devices 95 | 96 | The SDK entry point is the `DeviceManager` class. Check the following example to dicover any Bluetooth Low Energy device nearby. 97 | 98 | ```python 99 | import gatt 100 | 101 | class AnyDeviceManager(gatt.DeviceManager): 102 | def device_discovered(self, device): 103 | print("Discovered [%s] %s" % (device.mac_address, device.alias())) 104 | 105 | manager = AnyDeviceManager(adapter_name='hci0') 106 | manager.start_discovery() 107 | manager.run() 108 | ``` 109 | 110 | Please note that communication with your Bluetooth adapter happens over BlueZ's D-Bus API, hence an event loop needs to be run in order to receive all Bluetooth related events. You can start and stop the event loop via `run()` and `stop()` calls to your `DeviceManager` instance. 111 | 112 | ### Connecting to a Bluetooth Low Energy device and printing all its information 113 | 114 | Once `gatt.DeviceManager` has discovered a Bluetooth device you can use the `gatt.Device` instance that you retrieved from `gatt.DeviceManager.device_discovered()` to connect to it. Alternatively you can create a new instance of `gatt.Device` using the name of your Bluetooth adapter (typically `hci0`) and the device's MAC address. 115 | 116 | The following implementation of `gatt.Device` connects to any Bluetooth device and prints all relevant events: 117 | 118 | ```python 119 | import gatt 120 | 121 | manager = gatt.DeviceManager(adapter_name='hci0') 122 | 123 | class AnyDevice(gatt.Device): 124 | def connect_succeeded(self): 125 | super().connect_succeeded() 126 | print("[%s] Connected" % (self.mac_address)) 127 | 128 | def connect_failed(self, error): 129 | super().connect_failed(error) 130 | print("[%s] Connection failed: %s" % (self.mac_address, str(error))) 131 | 132 | def disconnect_succeeded(self): 133 | super().disconnect_succeeded() 134 | print("[%s] Disconnected" % (self.mac_address)) 135 | 136 | def services_resolved(self): 137 | super().services_resolved() 138 | 139 | print("[%s] Resolved services" % (self.mac_address)) 140 | for service in self.services: 141 | print("[%s] Service [%s]" % (self.mac_address, service.uuid)) 142 | for characteristic in service.characteristics: 143 | print("[%s] Characteristic [%s]" % (self.mac_address, characteristic.uuid)) 144 | 145 | 146 | device = AnyDevice(mac_address='AA:BB:CC:DD:EE:FF', manager=manager) 147 | device.connect() 148 | 149 | manager.run() 150 | ``` 151 | 152 | As with device discovery, remember to start the Bluetooth event loop with `gatt.DeviceManager.run()`. 153 | 154 | ### Reading and writing characteristic values 155 | 156 | As soon as `gatt.Device.services_resolved()` has been called by the SDK, you can access all GATT services and characteristics. Services are stored in the `services` attribute of `gatt.Device` and each `gatt.Service` instance has a `characteristics` attribute. 157 | 158 | To read a characteristic value first get the characteristic and then call `read_value()`. `gatt.Device.characteristic_value_updated()` will be called when the value has been retrieved. 159 | 160 | The following example reads the device's firmware version after all services and characteristics have been resolved: 161 | 162 | ```python 163 | import gatt 164 | 165 | manager = gatt.DeviceManager(adapter_name='hci0') 166 | 167 | class AnyDevice(gatt.Device): 168 | def services_resolved(self): 169 | super().services_resolved() 170 | 171 | device_information_service = next( 172 | s for s in self.services 173 | if s.uuid == '0000180a-0000-1000-8000-00805f9b34fb') 174 | 175 | firmware_version_characteristic = next( 176 | c for c in device_information_service.characteristics 177 | if c.uuid == '00002a26-0000-1000-8000-00805f9b34fb') 178 | 179 | firmware_version_characteristic.read_value() 180 | 181 | def characteristic_value_updated(self, characteristic, value): 182 | print("Firmware version:", value.decode("utf-8")) 183 | 184 | 185 | device = AnyDevice(mac_address='AA:BB:CC:DD:EE:FF', manager=manager) 186 | device.connect() 187 | 188 | manager.run() 189 | ``` 190 | 191 | To write a characteristic value simply call `write_value(value)` on the characteristic with `value` being an array of bytes. Then `characteristic_write_value_succeeded()` or `characteristic_write_value_failed(error)` will be called on your `gatt.Device` instance. 192 | 193 | ### Subscribing for characteristic value changes 194 | 195 | To subscribe for characteristic value change notifications call `enable_notifications()` on the characteristic. Then, on your `gatt.Device` instance, `characteristic_enable_notification_succeeded()` or `characteristic_enable_notification_failed()` will be called. Every time the Bluetooth device sends a new value, `characteristic_value_updated()` will be called. 196 | 197 | ## Support 198 | 199 | Please [open an issue](https://github.com/getsenic/gatt-python/issues) for this repository. 200 | 201 | ## Contributing 202 | 203 | Contributions are welcome via pull requests. Please open an issue first in case you want to discus your possible improvements to this SDK. 204 | 205 | ## License 206 | 207 | The GATT SDK for Python is available under the MIT License. 208 | -------------------------------------------------------------------------------- /examples/connect.py: -------------------------------------------------------------------------------- 1 | import gatt 2 | 3 | from argparse import ArgumentParser 4 | 5 | 6 | class AnyDevice(gatt.Device): 7 | def connect_succeeded(self): 8 | super().connect_succeeded() 9 | print("[%s] Connected" % (self.mac_address)) 10 | 11 | def connect_failed(self, error): 12 | super().connect_failed(error) 13 | print("[%s] Connection failed: %s" % (self.mac_address, str(error))) 14 | 15 | def disconnect_succeeded(self): 16 | super().disconnect_succeeded() 17 | print("[%s] Disconnected" % (self.mac_address)) 18 | 19 | def services_resolved(self): 20 | super().services_resolved() 21 | 22 | print("[%s] Resolved services" % (self.mac_address)) 23 | for service in self.services: 24 | print("[%s] Service [%s]" % (self.mac_address, service.uuid)) 25 | for characteristic in service.characteristics: 26 | print("[%s] Characteristic [%s]" % (self.mac_address, characteristic.uuid)) 27 | 28 | 29 | arg_parser = ArgumentParser(description="GATT Connect Demo") 30 | arg_parser.add_argument('mac_address', help="MAC address of device to connect") 31 | args = arg_parser.parse_args() 32 | 33 | print("Connecting...") 34 | 35 | manager = gatt.DeviceManager(adapter_name='hci0') 36 | 37 | device = AnyDevice(manager=manager, mac_address=args.mac_address) 38 | device.connect() 39 | 40 | manager.run() 41 | -------------------------------------------------------------------------------- /examples/discovery.py: -------------------------------------------------------------------------------- 1 | import gatt 2 | 3 | class AnyDeviceManager(gatt.DeviceManager): 4 | def device_discovered(self, device): 5 | print("[%s] Discovered, alias = %s" % (device.mac_address, device.alias())) 6 | 7 | manager = AnyDeviceManager(adapter_name='hci0') 8 | manager.start_discovery() 9 | manager.run() 10 | -------------------------------------------------------------------------------- /examples/read_descriptor.py: -------------------------------------------------------------------------------- 1 | import gatt 2 | 3 | from argparse import ArgumentParser 4 | 5 | 6 | class AnyDevice(gatt.Device): 7 | def connect_succeeded(self): 8 | super().connect_succeeded() 9 | print("[%s] Connected" % (self.mac_address)) 10 | 11 | def connect_failed(self, error): 12 | super().connect_failed(error) 13 | print("[%s] Connection failed: %s" % (self.mac_address, str(error))) 14 | 15 | def disconnect_succeeded(self): 16 | super().disconnect_succeeded() 17 | print("[%s] Disconnected" % (self.mac_address)) 18 | 19 | def services_resolved(self): 20 | super().services_resolved() 21 | 22 | print("[%s] Resolved services" % (self.mac_address)) 23 | for service in self.services: 24 | print("[%s]\tService [%s]" % (self.mac_address, service.uuid)) 25 | for characteristic in service.characteristics: 26 | print("[%s]\t\tCharacteristic [%s]" % (self.mac_address, characteristic.uuid)) 27 | for descriptor in characteristic.descriptors: 28 | print("[%s]\t\t\tDescriptor [%s] (%s)" % (self.mac_address, descriptor.uuid, descriptor.read_value())) 29 | 30 | def descriptor_read_value_failed(self, descriptor, error): 31 | print('descriptor_value_failed') 32 | 33 | 34 | arg_parser = ArgumentParser(description="GATT Connect Demo") 35 | arg_parser.add_argument('mac_address', help="MAC address of device to connect") 36 | args = arg_parser.parse_args() 37 | 38 | print("Connecting...") 39 | 40 | manager = gatt.DeviceManager(adapter_name='hci0') 41 | 42 | device = AnyDevice(manager=manager, mac_address=args.mac_address) 43 | device.connect() 44 | 45 | manager.run() 46 | -------------------------------------------------------------------------------- /examples/read_firmware_version.py: -------------------------------------------------------------------------------- 1 | import gatt 2 | 3 | from argparse import ArgumentParser 4 | 5 | class AnyDevice(gatt.Device): 6 | def services_resolved(self): 7 | super().services_resolved() 8 | 9 | device_information_service = next( 10 | s for s in self.services 11 | if s.uuid == '0000180a-0000-1000-8000-00805f9b34fb') 12 | 13 | firmware_version_characteristic = next( 14 | c for c in device_information_service.characteristics 15 | if c.uuid == '00002a26-0000-1000-8000-00805f9b34fb') 16 | 17 | firmware_version_characteristic.read_value() 18 | 19 | def characteristic_value_updated(self, characteristic, value): 20 | print("Firmware version:", value.decode("utf-8")) 21 | 22 | 23 | arg_parser = ArgumentParser(description="GATT Read Firmware Version Demo") 24 | arg_parser.add_argument('mac_address', help="MAC address of device to connect") 25 | args = arg_parser.parse_args() 26 | 27 | manager = gatt.DeviceManager(adapter_name='hci0') 28 | 29 | device = AnyDevice(manager=manager, mac_address=args.mac_address) 30 | device.connect() 31 | 32 | manager.run() 33 | -------------------------------------------------------------------------------- /gatt/__init__.py: -------------------------------------------------------------------------------- 1 | from .gatt import DeviceManager, Device, Service, Characteristic 2 | -------------------------------------------------------------------------------- /gatt/errors.py: -------------------------------------------------------------------------------- 1 | class AccessDenied(Exception): 2 | pass 3 | 4 | 5 | class Failed(Exception): 6 | pass 7 | 8 | 9 | class InProgress(Exception): 10 | pass 11 | 12 | 13 | class InvalidValueLength(Exception): 14 | pass 15 | 16 | 17 | class NotAuthorized(Exception): 18 | pass 19 | 20 | 21 | class NotReady(Exception): 22 | pass 23 | 24 | 25 | class NotPermitted(Exception): 26 | pass 27 | 28 | 29 | class NotSupported(Exception): 30 | pass 31 | -------------------------------------------------------------------------------- /gatt/gatt.py: -------------------------------------------------------------------------------- 1 | import os 2 | import platform 3 | 4 | if platform.system() == 'Linux': 5 | if os.environ.get('LINUX_WITHOUT_DBUS', '0') == '0': 6 | from .gatt_linux import * 7 | else: 8 | from .gatt_stubs import * 9 | else: 10 | # TODO: Add support for more platforms 11 | from .gatt_stubs import * 12 | -------------------------------------------------------------------------------- /gatt/gatt_linux.py: -------------------------------------------------------------------------------- 1 | try: 2 | import dbus 3 | import dbus.mainloop.glib 4 | except ImportError: 5 | import sys 6 | print("Module 'dbus' not found") 7 | print("Please run: sudo apt-get install python3-dbus") 8 | print("See also: https://github.com/getsenic/gatt-python#installing-gatt-sdk-for-python") 9 | sys.exit(1) 10 | 11 | import re 12 | 13 | from gi.repository import GObject 14 | 15 | from . import errors 16 | 17 | 18 | dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) 19 | dbus.mainloop.glib.threads_init() 20 | 21 | 22 | class DeviceManager: 23 | """ 24 | Entry point for managing BLE GATT devices. 25 | 26 | This class is intended to be subclassed to manage a specific set of GATT devices. 27 | """ 28 | 29 | def __init__(self, adapter_name): 30 | self.listener = None 31 | self.adapter_name = adapter_name 32 | 33 | self._bus = dbus.SystemBus() 34 | try: 35 | adapter_object = self._bus.get_object('org.bluez', '/org/bluez/' + adapter_name) 36 | except dbus.exceptions.DBusException as e: 37 | raise _error_from_dbus_error(e) 38 | object_manager_object = self._bus.get_object("org.bluez", "/") 39 | self._adapter = dbus.Interface(adapter_object, 'org.bluez.Adapter1') 40 | self._adapter_properties = dbus.Interface(self._adapter, 'org.freedesktop.DBus.Properties') 41 | self._object_manager = dbus.Interface(object_manager_object, "org.freedesktop.DBus.ObjectManager") 42 | self._device_path_regex = re.compile('^/org/bluez/' + adapter_name + '/dev((_[A-Z0-9]{2}){6})$') 43 | self._devices = {} 44 | self._discovered_devices = {} 45 | self._interface_added_signal = None 46 | self._properties_changed_signal = None 47 | self._main_loop = None 48 | 49 | self.update_devices() 50 | 51 | @property 52 | def is_adapter_powered(self): 53 | return self._adapter_properties.Get('org.bluez.Adapter1', 'Powered') == 1 54 | 55 | @is_adapter_powered.setter 56 | def is_adapter_powered(self, powered): 57 | return self._adapter_properties.Set('org.bluez.Adapter1', 'Powered', dbus.Boolean(powered)) 58 | 59 | def run(self): 60 | """ 61 | Starts the main loop that is necessary to receive Bluetooth events from the Bluetooth adapter. 62 | 63 | This call blocks until you call `stop()` to stop the main loop. 64 | """ 65 | 66 | if self._main_loop: 67 | return 68 | 69 | self._interface_added_signal = self._bus.add_signal_receiver( 70 | self._interfaces_added, 71 | dbus_interface='org.freedesktop.DBus.ObjectManager', 72 | signal_name='InterfacesAdded') 73 | 74 | # TODO: Also listen to 'interfaces removed' events? 75 | 76 | self._properties_changed_signal = self._bus.add_signal_receiver( 77 | self._properties_changed, 78 | dbus_interface=dbus.PROPERTIES_IFACE, 79 | signal_name='PropertiesChanged', 80 | arg0='org.bluez.Device1', 81 | path_keyword='path') 82 | 83 | def disconnect_signals(): 84 | for device in self._devices.values(): 85 | device.invalidate() 86 | self._properties_changed_signal.remove() 87 | self._interface_added_signal.remove() 88 | 89 | self._main_loop = GObject.MainLoop() 90 | try: 91 | self._main_loop.run() 92 | disconnect_signals() 93 | except Exception: 94 | disconnect_signals() 95 | raise 96 | 97 | def stop(self): 98 | """ 99 | Stops the main loop started with `start()` 100 | """ 101 | if self._main_loop: 102 | self._main_loop.quit() 103 | self._main_loop = None 104 | 105 | def _manage_device(self, device): 106 | existing_device = self._devices.get(device.mac_address) 107 | if existing_device is not None: 108 | existing_device.invalidate() 109 | self._devices[device.mac_address] = device 110 | 111 | def update_devices(self): 112 | managed_objects = self._object_manager.GetManagedObjects().items() 113 | possible_mac_addresses = [self._mac_address(path) for path, _ in managed_objects] 114 | mac_addresses = [m for m in possible_mac_addresses if m is not None] 115 | new_mac_addresses = [m for m in mac_addresses if m not in self._devices] 116 | for mac_address in new_mac_addresses: 117 | self.make_device(mac_address) 118 | # TODO: Remove devices from `_devices` that are no longer managed, i.e. deleted 119 | 120 | def devices(self): 121 | """ 122 | Returns all known Bluetooth devices. 123 | """ 124 | self.update_devices() 125 | return self._devices.values() 126 | 127 | def start_discovery(self, service_uuids=[]): 128 | """Starts a discovery for BLE devices with given service UUIDs. 129 | 130 | :param service_uuids: Filters the search to only return devices with given UUIDs. 131 | """ 132 | 133 | discovery_filter = {'Transport': 'le'} 134 | if service_uuids: # D-Bus doesn't like empty lists, it needs to guess the type 135 | discovery_filter['UUIDs'] = service_uuids 136 | 137 | try: 138 | self._adapter.SetDiscoveryFilter(discovery_filter) 139 | self._adapter.StartDiscovery() 140 | except dbus.exceptions.DBusException as e: 141 | if e.get_dbus_name() == 'org.bluez.Error.NotReady': 142 | raise errors.NotReady( 143 | "Bluetooth adapter not ready. " 144 | "Set `is_adapter_powered` to `True` or run 'echo \"power on\" | sudo bluetoothctl'.") 145 | if e.get_dbus_name() == 'org.bluez.Error.InProgress': 146 | # Discovery was already started - ignore exception 147 | pass 148 | else: 149 | raise _error_from_dbus_error(e) 150 | 151 | def stop_discovery(self): 152 | """ 153 | Stops the discovery started with `start_discovery` 154 | """ 155 | try: 156 | self._adapter.StopDiscovery() 157 | except dbus.exceptions.DBusException as e: 158 | if (e.get_dbus_name() == 'org.bluez.Error.Failed') and (e.get_dbus_message() == 'No discovery started'): 159 | pass 160 | else: 161 | raise _error_from_dbus_error(e) 162 | 163 | def _interfaces_added(self, path, interfaces): 164 | self._device_discovered(path, interfaces) 165 | 166 | def _properties_changed(self, interface, changed, invalidated, path): 167 | # TODO: Handle `changed` and `invalidated` properties and update device 168 | self._device_discovered(path, [interface]) 169 | 170 | def _device_discovered(self, path, interfaces): 171 | if 'org.bluez.Device1' not in interfaces: 172 | return 173 | mac_address = self._mac_address(path) 174 | if not mac_address: 175 | return 176 | device = self._devices.get(mac_address) or self.make_device(mac_address) 177 | if device is not None: 178 | self.device_discovered(device) 179 | 180 | def device_discovered(self, device): 181 | device.advertised() 182 | 183 | def _mac_address(self, device_path): 184 | match = self._device_path_regex.match(device_path) 185 | if not match: 186 | return None 187 | return match.group(1)[1:].replace('_', ':').lower() 188 | 189 | def make_device(self, mac_address): 190 | """ 191 | Makes and returns a `Device` instance with specified MAC address. 192 | 193 | Override this method to return a specific subclass instance of `Device`. 194 | Return `None` if the specified device shall not be supported by this class. 195 | """ 196 | return Device(mac_address=mac_address, manager=self) 197 | 198 | def add_device(self, mac_address): 199 | """ 200 | Adds a device with given MAC address without discovery. 201 | """ 202 | # TODO: Implement 203 | pass 204 | 205 | def remove_device(self, mac_address): 206 | """ 207 | Removes a device with the given MAC address 208 | """ 209 | # TODO: Implement 210 | pass 211 | 212 | def remove_all_devices(self, skip_alias=None): 213 | self.update_devices() 214 | 215 | keys_to_be_deleted = [] 216 | for key, device in self._devices.items(): 217 | if skip_alias and device.alias() == skip_alias: 218 | continue 219 | mac_address = device.mac_address.replace(':', '_').upper() 220 | path = '/org/bluez/%s/dev_%s' % (self.adapter_name, mac_address) 221 | self._adapter.RemoveDevice(path) 222 | keys_to_be_deleted.append(key) 223 | 224 | for key in keys_to_be_deleted: 225 | del self._devices[key] 226 | 227 | self.update_devices() 228 | 229 | 230 | 231 | class Device: 232 | def __init__(self, mac_address, manager, managed=True): 233 | """ 234 | Represents a BLE GATT device. 235 | 236 | This class is intended to be sublcassed with a device-specific implementations 237 | that reflect the device's GATT profile. 238 | 239 | :param mac_address: MAC address of this device 240 | :manager: `DeviceManager` that shall manage this device 241 | :managed: If False, the created device will not be managed by the device manager 242 | Particularly of interest for sub classes of `DeviceManager` who want 243 | to decide on certain device properties if they then create a subclass 244 | instance of that `Device` or not. 245 | """ 246 | 247 | self.mac_address = mac_address 248 | self.manager = manager 249 | self.services = [] 250 | 251 | self._bus = manager._bus 252 | self._object_manager = manager._object_manager 253 | 254 | # TODO: Device needs to be created if it's not yet known to bluetoothd, see "test-device" in bluez-5.43/test/ 255 | self._device_path = '/org/bluez/%s/dev_%s' % (manager.adapter_name, mac_address.replace(':', '_').upper()) 256 | device_object = self._bus.get_object('org.bluez', self._device_path) 257 | self._object = dbus.Interface(device_object, 'org.bluez.Device1') 258 | self._properties = dbus.Interface(self._object, 'org.freedesktop.DBus.Properties') 259 | self._properties_signal = None 260 | self._connect_retry_attempt = None 261 | 262 | if managed: 263 | manager._manage_device(self) 264 | 265 | def advertised(self): 266 | """ 267 | Called when an advertisement package has been received from the device. Requires device discovery to run. 268 | """ 269 | pass 270 | 271 | def is_registered(self): 272 | # TODO: Implement, see __init__ 273 | return False 274 | 275 | def register(self): 276 | # TODO: Implement, see __init__ 277 | return 278 | 279 | def invalidate(self): 280 | self._disconnect_signals() 281 | 282 | def connect(self): 283 | """ 284 | Connects to the device. Blocks until the connection was successful. 285 | """ 286 | self._connect_retry_attempt = 0 287 | self._connect_signals() 288 | self._connect() 289 | 290 | def _connect(self): 291 | self._connect_retry_attempt += 1 292 | try: 293 | self._object.Connect() 294 | if not self.services and self.is_services_resolved(): 295 | self.services_resolved() 296 | 297 | except dbus.exceptions.DBusException as e: 298 | if (e.get_dbus_name() == 'org.freedesktop.DBus.Error.UnknownObject'): 299 | self.connect_failed(errors.Failed("Device does not exist, check adapter name and MAC address.")) 300 | elif ((e.get_dbus_name() == 'org.bluez.Error.Failed') and 301 | (e.get_dbus_message() == "Operation already in progress")): 302 | pass 303 | elif ((self._connect_retry_attempt < 5) and 304 | (e.get_dbus_name() == 'org.bluez.Error.Failed') and 305 | (e.get_dbus_message() == "Software caused connection abort")): 306 | self._connect() 307 | elif (e.get_dbus_name() == 'org.freedesktop.DBus.Error.NoReply'): 308 | # TODO: How to handle properly? 309 | # Reproducable when we repeatedly shut off Nuimo immediately after its flashing Bluetooth icon appears 310 | self.connect_failed(_error_from_dbus_error(e)) 311 | else: 312 | self.connect_failed(_error_from_dbus_error(e)) 313 | 314 | def _connect_signals(self): 315 | if self._properties_signal is None: 316 | self._properties_signal = self._properties.connect_to_signal('PropertiesChanged', self.properties_changed) 317 | self._connect_service_signals() 318 | 319 | def _connect_service_signals(self): 320 | for service in self.services: 321 | service._connect_signals() 322 | 323 | def connect_succeeded(self): 324 | """ 325 | Will be called when `connect()` has finished connecting to the device. 326 | Will not be called if the device was already connected. 327 | """ 328 | pass 329 | 330 | def connect_failed(self, error): 331 | """ 332 | Called when the connection could not be established. 333 | """ 334 | self._disconnect_signals() 335 | 336 | def disconnect(self): 337 | """ 338 | Disconnects from the device, if connected. 339 | """ 340 | self._object.Disconnect() 341 | 342 | def disconnect_succeeded(self): 343 | """ 344 | Will be called when the device has disconnected. 345 | """ 346 | self._disconnect_signals() 347 | self.services = [] 348 | 349 | def _disconnect_signals(self): 350 | if self._properties_signal is not None: 351 | self._properties_signal.remove() 352 | self._properties_signal = None 353 | self._disconnect_service_signals() 354 | 355 | def _disconnect_service_signals(self): 356 | for service in self.services: 357 | service._disconnect_signals() 358 | 359 | def is_connected(self): 360 | """ 361 | Returns `True` if the device is connected, otherwise `False`. 362 | """ 363 | return self._properties.Get('org.bluez.Device1', 'Connected') == 1 364 | 365 | def is_services_resolved(self): 366 | """ 367 | Returns `True` is services are discovered, otherwise `False`. 368 | """ 369 | return self._properties.Get('org.bluez.Device1', 'ServicesResolved') == 1 370 | 371 | def alias(self): 372 | """ 373 | Returns the device's alias (name). 374 | """ 375 | try: 376 | return self._properties.Get('org.bluez.Device1', 'Alias') 377 | except dbus.exceptions.DBusException as e: 378 | if e.get_dbus_name() == 'org.freedesktop.DBus.Error.UnknownObject': 379 | # BlueZ sometimes doesn't provide an alias, we then simply return `None`. 380 | # Might occur when device was deleted as the following issue points out: 381 | # https://github.com/blueman-project/blueman/issues/460 382 | return None 383 | else: 384 | raise _error_from_dbus_error(e) 385 | 386 | def properties_changed(self, sender, changed_properties, invalidated_properties): 387 | """ 388 | Called when a device property has changed or got invalidated. 389 | """ 390 | if 'Connected' in changed_properties: 391 | if changed_properties['Connected']: 392 | self.connect_succeeded() 393 | else: 394 | self.disconnect_succeeded() 395 | 396 | if ('ServicesResolved' in changed_properties and changed_properties['ServicesResolved'] == 1 and 397 | not self.services): 398 | self.services_resolved() 399 | 400 | def services_resolved(self): 401 | """ 402 | Called when all device's services and characteristics got resolved. 403 | """ 404 | self._disconnect_service_signals() 405 | 406 | services_regex = re.compile(self._device_path + '/service[0-9abcdef]{4}$') 407 | managed_services = [ 408 | service for service in self._object_manager.GetManagedObjects().items() 409 | if services_regex.match(service[0])] 410 | self.services = [Service( 411 | device=self, 412 | path=service[0], 413 | uuid=service[1]['org.bluez.GattService1']['UUID']) for service in managed_services] 414 | 415 | self._connect_service_signals() 416 | 417 | def characteristic_value_updated(self, characteristic, value): 418 | """ 419 | Called when a characteristic value has changed. 420 | """ 421 | # To be implemented by subclass 422 | pass 423 | 424 | def characteristic_read_value_failed(self, characteristic, error): 425 | """ 426 | Called when a characteristic value read command failed. 427 | """ 428 | # To be implemented by subclass 429 | pass 430 | 431 | def characteristic_write_value_succeeded(self, characteristic): 432 | """ 433 | Called when a characteristic value write command succeeded. 434 | """ 435 | # To be implemented by subclass 436 | pass 437 | 438 | def characteristic_write_value_failed(self, characteristic, error): 439 | """ 440 | Called when a characteristic value write command failed. 441 | """ 442 | # To be implemented by subclass 443 | pass 444 | 445 | def characteristic_enable_notifications_succeeded(self, characteristic): 446 | """ 447 | Called when a characteristic notifications enable command succeeded. 448 | """ 449 | # To be implemented by subclass 450 | pass 451 | 452 | def characteristic_enable_notifications_failed(self, characteristic, error): 453 | """ 454 | Called when a characteristic notifications enable command failed. 455 | """ 456 | # To be implemented by subclass 457 | pass 458 | 459 | def descriptor_read_value_failed(self, descriptor, error): 460 | """ 461 | Called when a descriptor read command failed. 462 | """ 463 | # To be implemented by subclass 464 | pass 465 | 466 | 467 | class Service: 468 | """ 469 | Represents a GATT service. 470 | """ 471 | 472 | def __init__(self, device, path, uuid): 473 | # TODO: Don'T requore `path` argument, it can be calculated from device's path and uuid 474 | self.device = device 475 | self.uuid = uuid 476 | self._path = path 477 | self._bus = device._bus 478 | self._object_manager = device._object_manager 479 | self._object = self._bus.get_object('org.bluez', self._path) 480 | self.characteristics = [] 481 | self.characteristics_resolved() 482 | 483 | def _connect_signals(self): 484 | self._connect_characteristic_signals() 485 | 486 | def _connect_characteristic_signals(self): 487 | for characteristic in self.characteristics: 488 | characteristic._connect_signals() 489 | 490 | def _disconnect_signals(self): 491 | self._disconnect_characteristic_signals() 492 | 493 | def _disconnect_characteristic_signals(self): 494 | for characteristic in self.characteristics: 495 | characteristic._disconnect_signals() 496 | 497 | def characteristics_resolved(self): 498 | """ 499 | Called when all service's characteristics got resolved. 500 | """ 501 | self._disconnect_characteristic_signals() 502 | 503 | characteristics_regex = re.compile(self._path + '/char[0-9abcdef]{4}$') 504 | managed_characteristics = [ 505 | char for char in self._object_manager.GetManagedObjects().items() 506 | if characteristics_regex.match(char[0])] 507 | self.characteristics = [Characteristic( 508 | service=self, 509 | path=c[0], 510 | uuid=c[1]['org.bluez.GattCharacteristic1']['UUID']) for c in managed_characteristics] 511 | 512 | self._connect_characteristic_signals() 513 | 514 | 515 | class Descriptor: 516 | """ 517 | Represents a GATT Descriptor which can contain metadata or configuration of its characteristic. 518 | """ 519 | 520 | def __init__(self, characteristic, path, uuid): 521 | self.characteristic = characteristic 522 | self.uuid = uuid 523 | self._bus = characteristic._bus 524 | self._path = path 525 | self._object = self._bus.get_object('org.bluez', self._path) 526 | 527 | def read_value(self, offset=0): 528 | """ 529 | Reads the value of this descriptor. 530 | 531 | When successful, the value will be returned, otherwise `descriptor_read_value_failed()` of the related 532 | device is invoked. 533 | """ 534 | try: 535 | val = self._object.ReadValue( 536 | {'offset': dbus.UInt16(offset, variant_level=1)}, 537 | dbus_interface='org.bluez.GattDescriptor1') 538 | return val 539 | except dbus.exceptions.DBusException as e: 540 | error = _error_from_dbus_error(e) 541 | self.service.device.descriptor_read_value_failed(self, error=error) 542 | 543 | 544 | class Characteristic: 545 | """ 546 | Represents a GATT characteristic. 547 | """ 548 | 549 | def __init__(self, service, path, uuid): 550 | # TODO: Don't require `path` parameter, it can be calculated from service's path and uuid 551 | self.service = service 552 | self.uuid = uuid 553 | self._path = path 554 | self._bus = service._bus 555 | self._object_manager = service._object_manager 556 | self._object = self._bus.get_object('org.bluez', self._path) 557 | self._properties = dbus.Interface(self._object, "org.freedesktop.DBus.Properties") 558 | self._properties_signal = None 559 | 560 | descriptor_regex = re.compile(self._path + '/desc[0-9abcdef]{4}$') 561 | self.descriptors = [ 562 | Descriptor(self, desc[0], desc[1]['org.bluez.GattDescriptor1']['UUID']) 563 | for desc in self._object_manager.GetManagedObjects().items() 564 | if descriptor_regex.match(desc[0]) 565 | ] 566 | 567 | def _connect_signals(self): 568 | if self._properties_signal is None: 569 | self._properties_signal = self._properties.connect_to_signal('PropertiesChanged', self.properties_changed) 570 | 571 | def _disconnect_signals(self): 572 | if self._properties_signal is not None: 573 | self._properties_signal.remove() 574 | self._properties_signal = None 575 | 576 | def properties_changed(self, properties, changed_properties, invalidated_properties): 577 | value = changed_properties.get('Value') 578 | """ 579 | Called when a Characteristic property has changed. 580 | """ 581 | if value is not None: 582 | self.service.device.characteristic_value_updated(characteristic=self, value=bytes(value)) 583 | 584 | def read_value(self, offset=0): 585 | """ 586 | Reads the value of this characteristic. 587 | 588 | When successful, `characteristic_value_updated()` of the related device will be called, 589 | otherwise `characteristic_read_value_failed()` is invoked. 590 | """ 591 | try: 592 | return self._object.ReadValue( 593 | {'offset': dbus.UInt16(offset, variant_level=1)}, 594 | dbus_interface='org.bluez.GattCharacteristic1') 595 | except dbus.exceptions.DBusException as e: 596 | error = _error_from_dbus_error(e) 597 | self.service.device.characteristic_read_value_failed(self, error=error) 598 | 599 | def write_value(self, value, offset=0): 600 | """ 601 | Attempts to write a value to the characteristic. 602 | 603 | Success or failure will be notified by calls to `write_value_succeeded` or `write_value_failed` respectively. 604 | 605 | :param value: array of bytes to be written 606 | :param offset: offset from where to start writing the bytes (defaults to 0) 607 | """ 608 | bytes = [dbus.Byte(b) for b in value] 609 | 610 | try: 611 | self._object.WriteValue( 612 | bytes, 613 | {'offset': dbus.UInt16(offset, variant_level=1)}, 614 | reply_handler=self._write_value_succeeded, 615 | error_handler=self._write_value_failed, 616 | dbus_interface='org.bluez.GattCharacteristic1') 617 | except dbus.exceptions.DBusException as e: 618 | self._write_value_failed(self, error=e) 619 | 620 | def _write_value_succeeded(self): 621 | """ 622 | Called when the write request has succeeded. 623 | """ 624 | self.service.device.characteristic_write_value_succeeded(characteristic=self) 625 | 626 | def _write_value_failed(self, dbus_error): 627 | """ 628 | Called when the write request has failed. 629 | """ 630 | error = _error_from_dbus_error(dbus_error) 631 | self.service.device.characteristic_write_value_failed(characteristic=self, error=error) 632 | 633 | def enable_notifications(self, enabled=True): 634 | """ 635 | Enables or disables value change notifications. 636 | 637 | Success or failure will be notified by calls to `characteristic_enable_notifications_succeeded` 638 | or `enable_notifications_failed` respectively. 639 | 640 | Each time when the device notifies a new value, `characteristic_value_updated()` of the related 641 | device will be called. 642 | """ 643 | try: 644 | if enabled: 645 | self._object.StartNotify( 646 | reply_handler=self._enable_notifications_succeeded, 647 | error_handler=self._enable_notifications_failed, 648 | dbus_interface='org.bluez.GattCharacteristic1') 649 | else: 650 | self._object.StopNotify( 651 | reply_handler=self._enable_notifications_succeeded, 652 | error_handler=self._enable_notifications_failed, 653 | dbus_interface='org.bluez.GattCharacteristic1') 654 | except dbus.exceptions.DBusException as e: 655 | self._enable_notifications_failed(error=e) 656 | 657 | def _enable_notifications_succeeded(self): 658 | """ 659 | Called when notification enabling has succeeded. 660 | """ 661 | self.service.device.characteristic_enable_notifications_succeeded(characteristic=self) 662 | 663 | def _enable_notifications_failed(self, dbus_error): 664 | """ 665 | Called when notification enabling has failed. 666 | """ 667 | if ((dbus_error.get_dbus_name() == 'org.bluez.Error.Failed') and 668 | ((dbus_error.get_dbus_message() == "Already notifying") or 669 | (dbus_error.get_dbus_message() == "No notify session started"))): 670 | # Ignore cases where notifications where already enabled or already disabled 671 | return 672 | error = _error_from_dbus_error(dbus_error) 673 | self.service.device.characteristic_enable_notifications_failed(characteristic=self, error=error) 674 | 675 | 676 | def _error_from_dbus_error(e): 677 | return { 678 | 'org.bluez.Error.Failed': errors.Failed(e.get_dbus_message()), 679 | 'org.bluez.Error.InProgress': errors.InProgress(e.get_dbus_message()), 680 | 'org.bluez.Error.InvalidValueLength': errors.InvalidValueLength(e.get_dbus_message()), 681 | 'org.bluez.Error.NotAuthorized': errors.NotAuthorized(e.get_dbus_message()), 682 | 'org.bluez.Error.NotPermitted': errors.NotPermitted(e.get_dbus_message()), 683 | 'org.bluez.Error.NotSupported': errors.NotSupported(e.get_dbus_message()), 684 | 'org.freedesktop.DBus.Error.AccessDenied': errors.AccessDenied("Root permissions required") 685 | }.get(e.get_dbus_name(), errors.Failed(e.get_dbus_message())) 686 | -------------------------------------------------------------------------------- /gatt/gatt_stubs.py: -------------------------------------------------------------------------------- 1 | class DeviceManager: 2 | pass 3 | 4 | 5 | class Device: 6 | pass 7 | 8 | 9 | class Service: 10 | pass 11 | 12 | 13 | class Characteristic: 14 | pass 15 | -------------------------------------------------------------------------------- /gattctl.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from argparse import ArgumentParser 4 | import gatt 5 | 6 | device_manager = None 7 | 8 | 9 | class AnyDeviceManager(gatt.DeviceManager): 10 | """ 11 | An implementation of ``gatt.DeviceManager`` that discovers any GATT device 12 | and prints all discovered devices. 13 | """ 14 | 15 | def device_discovered(self, device): 16 | print("[%s] Discovered, alias = %s" % (device.mac_address, device.alias())) 17 | 18 | def make_device(self, mac_address): 19 | return AnyDevice(mac_address=mac_address, manager=self) 20 | 21 | 22 | class AnyDevice(gatt.Device): 23 | """ 24 | An implementation of ``gatt.Device`` that connects to any GATT device 25 | and prints all services and characteristics. 26 | """ 27 | 28 | def __init__(self, mac_address, manager, auto_reconnect=False): 29 | super().__init__(mac_address=mac_address, manager=manager) 30 | self.auto_reconnect = auto_reconnect 31 | 32 | def connect(self): 33 | print("Connecting...") 34 | super().connect() 35 | 36 | def connect_succeeded(self): 37 | super().connect_succeeded() 38 | print("[%s] Connected" % (self.mac_address)) 39 | 40 | def connect_failed(self, error): 41 | super().connect_failed(error) 42 | print("[%s] Connection failed: %s" % (self.mac_address, str(error))) 43 | 44 | def disconnect_succeeded(self): 45 | super().disconnect_succeeded() 46 | 47 | print("[%s] Disconnected" % (self.mac_address)) 48 | if self.auto_reconnect: 49 | self.connect() 50 | 51 | def services_resolved(self): 52 | super().services_resolved() 53 | 54 | print("[%s] Resolved services" % (self.mac_address)) 55 | for service in self.services: 56 | print("[%s] Service [%s]" % (self.mac_address, service.uuid)) 57 | for characteristic in service.characteristics: 58 | print("[%s] Characteristic [%s]" % (self.mac_address, characteristic.uuid)) 59 | 60 | 61 | def main(): 62 | arg_parser = ArgumentParser(description="GATT SDK Demo") 63 | arg_parser.add_argument( 64 | '--adapter', 65 | default='hci0', 66 | help="Name of Bluetooth adapter, defaults to 'hci0'") 67 | arg_commands_group = arg_parser.add_mutually_exclusive_group(required=True) 68 | arg_commands_group.add_argument( 69 | '--power-on', 70 | action='store_true', 71 | help="Powers the adapter on") 72 | arg_commands_group.add_argument( 73 | '--power-off', 74 | action='store_true', 75 | help="Powers the adapter off") 76 | arg_commands_group.add_argument( 77 | '--powered', 78 | action='store_true', 79 | help="Print the adapter's power state") 80 | arg_commands_group.add_argument( 81 | '--discover', 82 | action='store_true', 83 | help="Lists all nearby GATT devices") 84 | arg_commands_group.add_argument( 85 | '--connect', 86 | metavar='address', 87 | type=str, 88 | help="Connect to a GATT device with a given MAC address") 89 | arg_commands_group.add_argument( 90 | '--auto', 91 | metavar='address', 92 | type=str, 93 | help="Connect and automatically reconnect to a GATT device with a given MAC address") 94 | arg_commands_group.add_argument( 95 | '--disconnect', 96 | metavar='address', 97 | type=str, 98 | help="Disconnect a GATT device with a given MAC address") 99 | args = arg_parser.parse_args() 100 | 101 | global device_manager 102 | device_manager = AnyDeviceManager(adapter_name=args.adapter) 103 | 104 | if args.power_on: 105 | device_manager.is_adapter_powered = True 106 | print("Powered on") 107 | return 108 | elif args.power_off: 109 | device_manager.is_adapter_powered = False 110 | print("Powered off") 111 | return 112 | elif args.powered: 113 | print("Powered: ", device_manager.is_adapter_powered) 114 | return 115 | if args.discover: 116 | device_manager.start_discovery() 117 | elif args.connect: 118 | device = AnyDevice(mac_address=args.connect, manager=device_manager) 119 | device.connect() 120 | elif args.auto: 121 | device = AnyDevice(mac_address=args.auto, manager=device_manager, auto_reconnect=True) 122 | device.connect() 123 | elif args.disconnect: 124 | device = AnyDevice(mac_address=args.disconnect, manager=device_manager) 125 | device.disconnect() 126 | return 127 | 128 | print("Terminate with Ctrl+C") 129 | try: 130 | device_manager.run() 131 | except KeyboardInterrupt: 132 | pass 133 | 134 | 135 | if __name__ == '__main__': 136 | main() 137 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name='gatt', 5 | packages=['gatt'], 6 | version='0.2.7', 7 | description='Bluetooth GATT SDK for Python', 8 | keywords='gatt', 9 | url='https://github.com/getsenic/gatt-python', 10 | download_url='https://github.com/getsenic/gatt-python/archive/0.2.7.tar.gz', 11 | author='Senic GmbH', 12 | author_email='developers@senic.com', 13 | license='MIT', 14 | py_modules=['gattctl'], 15 | entry_points={ 16 | 'console_scripts': ['gattctl = gattctl:main'] 17 | } 18 | ) 19 | --------------------------------------------------------------------------------