-
Notifications
You must be signed in to change notification settings - Fork 37
support connect/disconnect, support multiple callbacks #39
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,6 +4,8 @@ | |
| """ | ||
| import logging | ||
| import codecs | ||
| import dbus | ||
| import re | ||
|
|
||
| from bluepy import btle | ||
|
|
||
|
|
@@ -15,46 +17,83 @@ | |
| class BTLEConnection(btle.DefaultDelegate): | ||
| """Representation of a BTLE Connection.""" | ||
|
|
||
| def __init__(self, mac): | ||
| def __init__(self, mac, keep_connected=False): | ||
| """Initialize the connection.""" | ||
| btle.DefaultDelegate.__init__(self) | ||
|
|
||
| self._ifaces = self.get_hci_ifaces() | ||
| self._iface_idx = 0 | ||
|
|
||
| self._conn = None | ||
| self._mac = mac | ||
| self._callbacks = {} | ||
| self._keep_connected = keep_connected | ||
|
|
||
| def next_iface(self): | ||
| self._nr_conn_errors += 1 | ||
| self._iface_idx = (self._iface_idx + 1) % len(self._ifaces) | ||
| if self._nr_conn_errors >= len(self._ifaces)*2: | ||
| return False | ||
| return True | ||
|
|
||
| def get_hci_ifaces(self): | ||
| iface_list = [] | ||
| bus = dbus.SystemBus() | ||
| manager = dbus.Interface(bus.get_object("org.bluez", "/"), "org.freedesktop.DBus.ObjectManager") | ||
| objects = manager.GetManagedObjects() | ||
| for path, interfaces in objects.items(): | ||
| adapter = interfaces.get("org.bluez.Adapter1") | ||
| if adapter is None: | ||
| continue | ||
| iface_list.append(re.search(r'\d+$', path)[0]) | ||
| return iface_list | ||
|
|
||
| def __enter__(self): | ||
| """ | ||
| Context manager __enter__ for connecting the device | ||
| :rtype: btle.Peripheral | ||
| :return: | ||
| """ | ||
| self._conn = btle.Peripheral() | ||
| self._conn.withDelegate(self) | ||
| _LOGGER.debug("Trying to connect to %s", self._mac) | ||
| try: | ||
| self._conn.connect(self._mac) | ||
| except btle.BTLEException as ex: | ||
| _LOGGER.debug("Unable to connect to the device %s, retrying: %s", self._mac, ex) | ||
| try: | ||
| self._conn.connect(self._mac) | ||
| except Exception as ex2: | ||
| _LOGGER.debug("Second connection try to %s failed: %s", self._mac, ex2) | ||
| raise | ||
| conn_state = self._conn.getState() | ||
| except: | ||
| self._conn = None | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This will lose the reference to the potentially already existing Anycase, please don't use bare excepts but catch only exceptions you are expecting.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok, I can change that to catch only the errors which indicate that the connection was already lost and therefore a disconnect() call isn't needed. |
||
|
|
||
| if self._conn is None or conn_state != "conn": | ||
| self._conn = btle.Peripheral() | ||
| self._conn.withDelegate(self) | ||
| self._nr_conn_errors = 0 | ||
| _LOGGER.debug("Trying to connect to %s", self._mac) | ||
| while True: | ||
| # try to connect with all ifaces | ||
| try: | ||
| self._conn.connect(self._mac, iface=self._ifaces[self._iface_idx]) | ||
| break | ||
| except btle.BTLEException as ex: | ||
| _LOGGER.debug("Unable to connect to the device %s using iface %s, retrying: %s", self._mac, self._ifaces[self._iface_idx], ex) | ||
| try: | ||
| self._conn.connect(self._mac, iface=self._ifaces[self._iface_idx]) | ||
| break | ||
| except Exception as ex2: | ||
| _LOGGER.debug("Second connection try to %s using ifaces %s failed: %s", self._mac, self._ifaces[self._iface_idx], ex2) | ||
| if self.next_iface() is False: | ||
| # tried all ifaces, raise exception | ||
| raise | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This structure feels very error-prone, I think the correct way would be refactoring the connection code away from
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That makes sense. I'll refactor and add a |
||
|
|
||
| _LOGGER.debug("Connected to %s", self._mac) | ||
| return self | ||
|
|
||
| def __exit__(self, exc_type, exc_val, exc_tb): | ||
| if self._conn: | ||
| if self._conn and self._keep_connected is False: | ||
| self._conn.disconnect() | ||
| self._conn = None | ||
|
|
||
| def handleNotification(self, handle, data): | ||
| """Handle Callback from a Bluetooth (GATT) request.""" | ||
| _LOGGER.debug("Got notification from %s: %s", handle, codecs.encode(data, 'hex')) | ||
| if handle in self._callbacks: | ||
| self._callbacks[handle](data) | ||
| for callback in self._callbacks[handle]: | ||
| callback(data) | ||
|
|
||
| @property | ||
| def mac(self): | ||
|
|
@@ -63,7 +102,9 @@ def mac(self): | |
|
|
||
| def set_callback(self, handle, function): | ||
| """Set the callback for a Notification handle. It will be called with the parameter data, which is binary.""" | ||
| self._callbacks[handle] = function | ||
| if handle not in self._callbacks: | ||
| self._callbacks[handle] = [] | ||
| self._callbacks[handle].append(function) | ||
|
|
||
| def make_request(self, handle, value, timeout=DEFAULT_TIMEOUT, with_response=True): | ||
| """Write a GATT Command without callback - not utf-8.""" | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This would introduce hard dependency for dbus (which may or may not be available on all environments), so it's a no go. Could you please create a separate PR for adding optional support for multiple interfaces?
Also, are you sure bluepy does not provide an interface for enumerating over existing adapters? That would be the preferred way over manually scraping bluez responses.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unfortunately bluepy doesn't provide such a functionality, that was the first place I searched for. I could find this one from bluez which is the functionality I implemented:
https://github.com/pauloborges/bluez/blob/master/test/test-adapter
Another possible solution would be to remove this one from eq3bt and add an additional parameter "iface".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, I think the best solution is to refactor the connection handling (so that connect can be done separately outside the
__enter__) and exposeifaceparameter for it as you propose. The eq3bt cli tool could also optionally expose--interfaceoption, and the downstream users can use whatever they want (like dbus) to locate available interfaces.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just to be sure that we are on the same page:
connect(iface)methodifaceparameter toThermostatThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good to me!
connect(iface)will be useful for your use case, where you want to fall back to other interfaces if the connection failsifacetoThermostatallows forcing to use specific controller (e.g., via cli) during the initialization phase, in case someone wants to do that.The real fix for obtaining interfaces should be done inside
bluepy, but I think the library is not very actively maintained, notwithstanding the problem with dbus availability (iirc there was also some problems getting that library easily pip-installable due to some compiling needed during the setup process). Last time I did some dbus interfacing, I used python-dbus-next which is pure python only library.