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