From aa1a5e66893a13e92183cfff7d6d5c5afe5cff81 Mon Sep 17 00:00:00 2001 From: Peter Hutterer Date: Fri, 19 Jan 2018 12:48:53 +1000 Subject: [PATCH] Revamp the pairing process and rename to Search The previous process had a problem: the device object didn't exist until after pairing was complete. But to pair we need some user interaction (press button on device) and thus the ability to send notifications from the device to the dbus client at the right time. This wasn't possible with the previous approach. The new approach: * renames Start/StopPairing to Start/StopSearch to indicate we're just searching, not pairing in the manager * creates a org.freedesktop.tuhi1.Device object when a suitable device is found. That object is not in Manager.Devices, it's "hidden" unless you know the object path. * Sends a PairableDevice signal with the new device's object path * Requires the client to call Pair() on the device * If the timeout expires without pairing, the device is removed again --- README.md | 49 +++++++------ tuhi.py | 50 +++++++------- tuhi/dbusserver.py | 168 +++++++++++++++++++-------------------------- 3 files changed, 120 insertions(+), 147 deletions(-) diff --git a/README.md b/README.md index 9414934..1a90f39 100644 --- a/README.md +++ b/README.md @@ -40,15 +40,15 @@ org.freedesktop.tuhi1.Manager initialization is independent of the Bluetooth pairing process. A Tuhi paired device may or may not be paired over Bluetooth. - Method: StartPairing() -> () - Start listening to available devices in pairing mode for an + Method: StartSearch() -> () + Start searching for available devices in pairing mode for an unspecified timeout. When the timeout expires or an error occurs, a - PairingStopped signal is sent indicating success or error. + SearchStopped signal is sent indicating success or error. - Method: StopPairing() -> () + Method: StopSearch() -> () Stop listening to available devices in pairing mode. If called after - StartPairing() and before a PairingStopped signal has been received, - this method triggers the PairingStopped signal. That signal indicates + StartSearch() and before a Searchtopped signal has been received, + this method triggers the SearchStopped signal. That signal indicates success or an error. If this method is called before StartPairing() or after the @@ -57,31 +57,21 @@ org.freedesktop.tuhi1.Manager Note that between callling StopPairing() and the PairingStopped signal arriving, PairableDevice signals may still arrive. - Method: Pair(s) -> (i) - Pairs the given device specified by its bluetooth MAC address, i.e. - the value of "address" in the PairableDevice signal argument. - - Pairing a device may take a long time, a client must use asynchronous - method invocation to avoid DBus timeouts. - - Invocations of Pair() before StartPairing() has been invoked or after a - PairingStopped() signal may result in an error. - - Returns: 0 on success or a negative errno on failure - - Signal: PairableDevice(a{sv}) + Signal: PairableDevice(o) Indicates that a device is available for pairing. This signal may be - sent after a StartPairing() call and before PairingStopped(). This + sent after a StartSearch() call and before SearchStopped(). This signal is sent once per available device. - The argument is a key-value dictionary, with keys as strings and value - as key-dependent entity. + When this signal is sent, a org.freedesktop.tuhi1.Device object was + created, the object path is the argument to this signal. - Tuhi guarantees that the following keys are available: - * "name" - the device name as string - * "address" - the device's Bluetooth MAC address as string + A client must immediately call Pair() on that object if pairing with + that object is desired. See the documentation for that interface + for details. - Unknown keys must be ignored by a client. + When the search timeout expires, the device is removed by the daemon + again. Note that until the device is paired, the device is not listed + in the managers Devices property. Signal: PairingStopped(i) Sent when the pairing has stopped. An argument of 0 indicates a @@ -125,6 +115,13 @@ org.freedesktop.tuhi1.Device upon timeout, the property is set to False. Read-only + Method: Pair() -> (i) + Pair the device. If the device is already paired, calls to this method + immediately return success. + + Otherwise, the device is paired and this function returns success (0) + or a negative errno on failure. + Method: Listen() -> () Listen for data from this device. This method starts listening for events on the device for an unspecified timeout. When the timeout diff --git a/tuhi.py b/tuhi.py index fd7afaa..d8af9ff 100755 --- a/tuhi.py +++ b/tuhi.py @@ -75,30 +75,38 @@ class TuhiDevice(GObject.Object): real device) with the frontend DBusServer object that exports the device over Tuhi's DBus interface """ - def __init__(self, bluez_device, tuhi_dbus_device): + def __init__(self, bluez_device, tuhi_dbus_device, paired=True): GObject.Object.__init__(self) self._tuhi_dbus_device = tuhi_dbus_device self._wacom_device = WacomDevice(bluez_device) self._wacom_device.connect('drawing', self._on_drawing_received) self._wacom_device.connect('done', self._on_fetching_finished, bluez_device) self.drawings = [] - self.pairing_mode = False + self.paired = paired bluez_device.connect('connected', self._on_bluez_device_connected) bluez_device.connect('disconnected', self._on_bluez_device_disconnected) self._bluez_device = bluez_device + self._tuhi_dbus_device.connect('pair-requested', self._on_pair_requested) + def connect_device(self): self._bluez_device.connect_device() def _on_bluez_device_connected(self, bluez_device): logger.debug('{}: connected'.format(bluez_device.address)) - self._wacom_device.start(self.pairing_mode) + self._wacom_device.start(not self.paired) self.pairing_mode = False def _on_bluez_device_disconnected(self, bluez_device): logger.debug('{}: disconnected'.format(bluez_device.address)) + def _on_pair_requested(self, dbus_device): + if self.paired: + return + + self.connect_device() + def _on_drawing_received(self, device, drawing): logger.debug('Drawing received') d = TuhiDrawing(device.name, (0, 0), drawing.timestamp) @@ -144,9 +152,8 @@ class Tuhi(GObject.Object): GObject.Object.__init__(self) self.server = TuhiDBusServer() self.server.connect('bus-name-acquired', self._on_tuhi_bus_name_acquired) - self.server.connect('pairing-start-requested', self._on_start_pairing_requested) - self.server.connect('pairing-stop-requested', self._on_stop_pairing_requested) - self.server.connect('pair-device-requested', self._on_pair_device_requested) + self.server.connect('search-start-requested', self._on_start_search_requested) + self.server.connect('search-stop-requested', self._on_stop_search_requested) self.bluez = BlueZDeviceManager() self.bluez.connect('device-added', self._on_bluez_device_added) self.bluez.connect('device-updated', self._on_bluez_device_updated) @@ -155,29 +162,22 @@ class Tuhi(GObject.Object): self.devices = {} - self._pairing_stop_handler = None + self._search_stop_handler = None def _on_tuhi_bus_name_acquired(self, dbus_server): self.bluez.connect_to_bluez() - def _on_start_pairing_requested(self, dbus_server, stop_handler): - self._pairing_stop_handler = stop_handler + def _on_start_search_requested(self, dbus_server, stop_handler): + self._search_stop_handler = stop_handler self.bluez.start_discovery(timeout=30) - def _on_stop_pairing_requested(self, dbus_server): + def _on_stop_search_requested(self, dbus_server): # If you request to stop, you get a successful stop and we ignore # anything the server does underneath - self._pairing_stop_handler(0) - self._pairing_stop_handler = None + self._search_stop_handler(0) + self._search_stop_handler = None self.bluez.stop_discovery() - self._pairable_device_handler = None - - def _on_pair_device_requested(self, dbusserver, bluez_device): - tuhi_dbus_device = self.server.create_device(bluez_device) - d = TuhiDevice(bluez_device, tuhi_dbus_device) - d.pairing_mode = True - self.devices[bluez_device.address] = d - d.connect_device() + self._search_device_handler = None @classmethod def _is_pairing_device(cls, bluez_device): @@ -201,19 +201,21 @@ class Tuhi(GObject.Object): def _on_bluez_discovery_started(self, manager): # Something else may turn discovery mode on, we don't care about # it then - if not self._pairing_stop_handler: + if not self._search_stop_handler: return def _on_bluez_discovery_stopped(self, manager): - if self._pairing_stop_handler is not None: - self._pairing_stop_handler(0) + if self._search_stop_handler is not None: + self._search_stop_handler(0) def _on_bluez_device_updated(self, manager, bluez_device): if bluez_device.vendor_id != WACOM_COMPANY_ID: return if Tuhi._is_pairing_device(bluez_device): - self.server.notify_pairable_device(bluez_device) + tuhi_dbus_device = self.server.create_device(bluez_device, paired=False) + d = TuhiDevice(bluez_device, tuhi_dbus_device, paired=False) + self.devices[bluez_device.address] = d def main(args): diff --git a/tuhi/dbusserver.py b/tuhi/dbusserver.py index f1f9b1a..1747b72 100755 --- a/tuhi/dbusserver.py +++ b/tuhi/dbusserver.py @@ -11,7 +11,6 @@ # GNU General Public License for more details. # -import os import logging from gi.repository import GObject, Gio, GLib @@ -25,25 +24,20 @@ INTROSPECTION_XML = """ - + - + - - - - - - + - + @@ -54,6 +48,10 @@ INTROSPECTION_XML = """ + + + + @@ -80,33 +78,49 @@ class TuhiDBusDevice(GObject.Object): Class representing a DBus object for a Tuhi device. This class only handles the DBus bits, communication with the device is done elsewhere. """ - def __init__(self, device, connection): + __gsignals__ = { + "pair-requested": + (GObject.SIGNAL_RUN_FIRST, None, ()), + } + + def __init__(self, device, connection, paired=True): GObject.Object.__init__(self) self.name = device.name self.btaddr = device.address self.width, self.height = 0, 0 self.drawings = [] + self.paired = paired objpath = device.address.replace(':', '_') self.objpath = "{}/{}".format(BASE_PATH, objpath) - self._register_object(connection) + self._connection = connection + self._dbusid = self._register_object(connection) + + def remove(self): + self._connection.unregister_object(self._dbusid) + self._dbusid = None def _register_object(self, connection): introspection = Gio.DBusNodeInfo.new_for_xml(INTROSPECTION_XML) intf = introspection.lookup_interface(INTF_DEVICE) - Gio.DBusConnection.register_object(connection, - self.objpath, - intf, - self._method_cb, - self._property_read_cb, - self._property_write_cb) + return connection.register_object(self.objpath, + intf, + self._method_cb, + self._property_read_cb, + self._property_write_cb) def _method_cb(self, connection, sender, objpath, interface, methodname, args, invocation): if interface != INTF_DEVICE: return None - if methodname == 'Listen': + if methodname == 'Pair': + # FIXME: we should cache the method invocation here, wait for a + # successful result from Tuhi and then return the value + self._pair() + result = GLib.Variant.new_int32(0) + invocation.return_value(GLib.Variant.new_tuple(result)) + elif methodname == 'Listen': self._listen() invocation.return_value() elif methodname == 'GetJSONData': @@ -133,6 +147,9 @@ class TuhiDBusDevice(GObject.Object): def _property_write_cb(self): pass + def _pair(self): + self.emit('pair-requested') + def _listen(self): # FIXME: start listen asynchronously # FIXME: update property when listen finishes @@ -155,18 +172,13 @@ class TuhiDBusServer(GObject.Object): (GObject.SIGNAL_RUN_FIRST, None, ()), # Signal arguments: - # pairing_stop_handler(status) - # to be called when the pairing process has terminated, with + # search_stop_handler(status) + # to be called when the search process has terminated, with # an integer status code (0 == success, negative errno) - "pairing-start-requested": + "search-start-requested": (GObject.SIGNAL_RUN_FIRST, None, (GObject.TYPE_PYOBJECT,)), - "pairing-stop-requested": + "search-stop-requested": (GObject.SIGNAL_RUN_FIRST, None, ()), - # Signal arguments: - # address - # string of the Bluetooth device address - "pair-device-requested": - (GObject.SIGNAL_RUN_FIRST, None, (GObject.TYPE_PYOBJECT,)), } def __init__(self): @@ -179,7 +191,7 @@ class TuhiDBusServer(GObject.Object): self._bus_aquired, self._bus_name_aquired, self._bus_name_lost) - self._is_pairing = False + self._is_searching = False def _bus_aquired(self, connection, name): introspection = Gio.DBusNodeInfo.new_for_xml(INTROSPECTION_XML) @@ -203,104 +215,66 @@ class TuhiDBusServer(GObject.Object): if interface != INTF_MANAGER: return None - if methodname == 'StartPairing': - self._start_pairing() + if methodname == 'StartSearch': + self._start_search() invocation.return_value() - elif methodname == 'StopPairing': - self._stop_pairing() + elif methodname == 'StopSearch': + self._stop_search() invocation.return_value() - elif methodname == 'Pair': - result = self._pair(args[0]) - result = GLib.Variant.new_int32(result) - invocation.return_value(GLib.Variant.new_tuple(result)) def _property_read_cb(self, connection, sender, objpath, interface, propname): if interface != INTF_MANAGER: return None if propname == 'Devices': - return GLib.Variant.new_objv([d.objpath for d in self._devices]) + return GLib.Variant.new_objv([d.objpath for d in self._devices if d.paired]) return None def _property_write_cb(self): pass - def _start_pairing(self): - if self._is_pairing: + def _start_search(self): + if self._is_searching: return - self._is_pairing = True - self.emit("pairing-start-requested", self._on_pairing_stop) + self._is_searching = True + self.emit("search-start-requested", self._on_search_stop) - def _stop_pairing(self): - if not self._is_pairing: + def _stop_search(self): + if not self._is_searching: return - self._is_pairing = False - self.emit("pairing-stop-requested") + self._is_searching = False + self.emit("search-stop-requested") - def _pair(self, address): - if not self._is_pairing: - return os.errno.ECONNREFUSED - - if address not in self._pairable_devices: - return os.errno.ENODEV - - self.emit('pair-device-requested', self._pairable_devices[address]) - - # FIXME: we should cache the method invocation here, wait for a - # successful result from Tuhi and then return the value - return 0 - - def _on_pairing_stop(self, status): + def _on_search_stop(self, status): """ - Called by whoever handles the pairing-start-requested signal + Called by whoever handles the search-start-requested signal """ - logger.debug("Pairing has stopped") - self._is_pairing = False + logger.debug("Search has stopped") + self._is_searching = False status = GLib.Variant.new_int32(status) status = GLib.Variant.new_tuple(status) self._connection.emit_signal(None, BASE_PATH, INTF_MANAGER, - "PairingStopped", status) - self._pairable_devices = {} + "SearchStopped", status) - def notify_pairable_device(self, device): - """ - Notify the client that a pairable device is available. - """ - if not self._is_pairing: - return + for dev in self._devices: + if dev.paired: + continue - logger.debug("Pairable device: {}".format(device)) - - address = device.address - if address in self._pairable_devices: - return - - self._pairable_devices[address] = device - - b = GLib.VariantBuilder(GLib.VariantType.new('a{sv}')) - - key = GLib.Variant.new_string('name') - value = GLib.Variant.new_variant(GLib.Variant.new_string(device.name)) - de = GLib.Variant.new_dict_entry(key, value) - b.add_value(de) - - key = GLib.Variant.new_string('address') - value = GLib.Variant.new_variant(GLib.Variant.new_string(device.address)) - de = GLib.Variant.new_dict_entry(key, value) - b.add_value(de) - - array = b.end() - self._connection.emit_signal(None, BASE_PATH, INTF_MANAGER, - "PairableDevice", - GLib.Variant.new_tuple(array)) + dev.remove() + self._devices = [d for d in self._devices if d.paired] def cleanup(self): Gio.bus_unown_name(self._dbus) - def create_device(self, device): - dev = TuhiDBusDevice(device, self._connection) + def create_device(self, device, paired=True): + dev = TuhiDBusDevice(device, self._connection, paired) self._devices.append(dev) + if not paired: + arg = GLib.Variant.new_object_path(dev.objpath) + self._connection.emit_signal(None, BASE_PATH, INTF_MANAGER, + "PairableDevice", + GLib.Variant.new_tuple(arg)) return dev