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
pull/8/head
Peter Hutterer 2018-01-19 12:48:53 +10:00
parent f6519016f9
commit aa1a5e6689
3 changed files with 120 additions and 147 deletions

View File

@ -40,15 +40,15 @@ org.freedesktop.tuhi1.Manager
initialization is independent of the Bluetooth pairing process. A Tuhi initialization is independent of the Bluetooth pairing process. A Tuhi
paired device may or may not be paired over Bluetooth. paired device may or may not be paired over Bluetooth.
Method: StartPairing() -> () Method: StartSearch() -> ()
Start listening to available devices in pairing mode for an Start searching for available devices in pairing mode for an
unspecified timeout. When the timeout expires or an error occurs, a 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 Stop listening to available devices in pairing mode. If called after
StartPairing() and before a PairingStopped signal has been received, StartSearch() and before a Searchtopped signal has been received,
this method triggers the PairingStopped signal. That signal indicates this method triggers the SearchStopped signal. That signal indicates
success or an error. success or an error.
If this method is called before StartPairing() or after the 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 Note that between callling StopPairing() and the PairingStopped signal
arriving, PairableDevice signals may still arrive. arriving, PairableDevice signals may still arrive.
Method: Pair(s) -> (i) Signal: PairableDevice(o)
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})
Indicates that a device is available for pairing. This signal may be 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. signal is sent once per available device.
The argument is a key-value dictionary, with keys as strings and value When this signal is sent, a org.freedesktop.tuhi1.Device object was
as key-dependent entity. created, the object path is the argument to this signal.
Tuhi guarantees that the following keys are available: A client must immediately call Pair() on that object if pairing with
* "name" - the device name as string that object is desired. See the documentation for that interface
* "address" - the device's Bluetooth MAC address as string 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) Signal: PairingStopped(i)
Sent when the pairing has stopped. An argument of 0 indicates a 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. upon timeout, the property is set to False.
Read-only 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() -> () Method: Listen() -> ()
Listen for data from this device. This method starts listening for Listen for data from this device. This method starts listening for
events on the device for an unspecified timeout. When the timeout events on the device for an unspecified timeout. When the timeout

50
tuhi.py
View File

@ -75,30 +75,38 @@ class TuhiDevice(GObject.Object):
real device) with the frontend DBusServer object that exports the device real device) with the frontend DBusServer object that exports the device
over Tuhi's DBus interface 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) GObject.Object.__init__(self)
self._tuhi_dbus_device = tuhi_dbus_device self._tuhi_dbus_device = tuhi_dbus_device
self._wacom_device = WacomDevice(bluez_device) self._wacom_device = WacomDevice(bluez_device)
self._wacom_device.connect('drawing', self._on_drawing_received) self._wacom_device.connect('drawing', self._on_drawing_received)
self._wacom_device.connect('done', self._on_fetching_finished, bluez_device) self._wacom_device.connect('done', self._on_fetching_finished, bluez_device)
self.drawings = [] self.drawings = []
self.pairing_mode = False self.paired = paired
bluez_device.connect('connected', self._on_bluez_device_connected) bluez_device.connect('connected', self._on_bluez_device_connected)
bluez_device.connect('disconnected', self._on_bluez_device_disconnected) bluez_device.connect('disconnected', self._on_bluez_device_disconnected)
self._bluez_device = bluez_device self._bluez_device = bluez_device
self._tuhi_dbus_device.connect('pair-requested', self._on_pair_requested)
def connect_device(self): def connect_device(self):
self._bluez_device.connect_device() self._bluez_device.connect_device()
def _on_bluez_device_connected(self, bluez_device): def _on_bluez_device_connected(self, bluez_device):
logger.debug('{}: connected'.format(bluez_device.address)) 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 self.pairing_mode = False
def _on_bluez_device_disconnected(self, bluez_device): def _on_bluez_device_disconnected(self, bluez_device):
logger.debug('{}: disconnected'.format(bluez_device.address)) 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): def _on_drawing_received(self, device, drawing):
logger.debug('Drawing received') logger.debug('Drawing received')
d = TuhiDrawing(device.name, (0, 0), drawing.timestamp) d = TuhiDrawing(device.name, (0, 0), drawing.timestamp)
@ -144,9 +152,8 @@ class Tuhi(GObject.Object):
GObject.Object.__init__(self) GObject.Object.__init__(self)
self.server = TuhiDBusServer() self.server = TuhiDBusServer()
self.server.connect('bus-name-acquired', self._on_tuhi_bus_name_acquired) 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('search-start-requested', self._on_start_search_requested)
self.server.connect('pairing-stop-requested', self._on_stop_pairing_requested) self.server.connect('search-stop-requested', self._on_stop_search_requested)
self.server.connect('pair-device-requested', self._on_pair_device_requested)
self.bluez = BlueZDeviceManager() self.bluez = BlueZDeviceManager()
self.bluez.connect('device-added', self._on_bluez_device_added) self.bluez.connect('device-added', self._on_bluez_device_added)
self.bluez.connect('device-updated', self._on_bluez_device_updated) self.bluez.connect('device-updated', self._on_bluez_device_updated)
@ -155,29 +162,22 @@ class Tuhi(GObject.Object):
self.devices = {} self.devices = {}
self._pairing_stop_handler = None self._search_stop_handler = None
def _on_tuhi_bus_name_acquired(self, dbus_server): def _on_tuhi_bus_name_acquired(self, dbus_server):
self.bluez.connect_to_bluez() self.bluez.connect_to_bluez()
def _on_start_pairing_requested(self, dbus_server, stop_handler): def _on_start_search_requested(self, dbus_server, stop_handler):
self._pairing_stop_handler = stop_handler self._search_stop_handler = stop_handler
self.bluez.start_discovery(timeout=30) 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 # If you request to stop, you get a successful stop and we ignore
# anything the server does underneath # anything the server does underneath
self._pairing_stop_handler(0) self._search_stop_handler(0)
self._pairing_stop_handler = None self._search_stop_handler = None
self.bluez.stop_discovery() self.bluez.stop_discovery()
self._pairable_device_handler = None self._search_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()
@classmethod @classmethod
def _is_pairing_device(cls, bluez_device): def _is_pairing_device(cls, bluez_device):
@ -201,19 +201,21 @@ class Tuhi(GObject.Object):
def _on_bluez_discovery_started(self, manager): def _on_bluez_discovery_started(self, manager):
# Something else may turn discovery mode on, we don't care about # Something else may turn discovery mode on, we don't care about
# it then # it then
if not self._pairing_stop_handler: if not self._search_stop_handler:
return return
def _on_bluez_discovery_stopped(self, manager): def _on_bluez_discovery_stopped(self, manager):
if self._pairing_stop_handler is not None: if self._search_stop_handler is not None:
self._pairing_stop_handler(0) self._search_stop_handler(0)
def _on_bluez_device_updated(self, manager, bluez_device): def _on_bluez_device_updated(self, manager, bluez_device):
if bluez_device.vendor_id != WACOM_COMPANY_ID: if bluez_device.vendor_id != WACOM_COMPANY_ID:
return return
if Tuhi._is_pairing_device(bluez_device): 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): def main(args):

View File

@ -11,7 +11,6 @@
# GNU General Public License for more details. # GNU General Public License for more details.
# #
import os
import logging import logging
from gi.repository import GObject, Gio, GLib from gi.repository import GObject, Gio, GLib
@ -25,25 +24,20 @@ INTROSPECTION_XML = """
<annotation name='org.freedesktop.DBus.Property.EmitsChangedSignal' value='true'/> <annotation name='org.freedesktop.DBus.Property.EmitsChangedSignal' value='true'/>
</property> </property>
<method name='StartPairing'> <method name='StartSearch'>
<annotation name='org.freedesktop.DBus.Method.NoReply' value='true'/> <annotation name='org.freedesktop.DBus.Method.NoReply' value='true'/>
</method> </method>
<method name='StopPairing'> <method name='StopSearch'>
<annotation name='org.freedesktop.DBus.Method.NoReply' value='true'/> <annotation name='org.freedesktop.DBus.Method.NoReply' value='true'/>
</method> </method>
<method name='Pair'> <signal name='SearchStopped'>
<arg name='address' type='s' direction='in'/>
<arg name='result' type='i' direction='out'/>
</method>
<signal name='PairingStopped'>
<arg name='status' type='i' /> <arg name='status' type='i' />
</signal> </signal>
<signal name='PairableDevice'> <signal name='PairableDevice'>
<arg name='info' type='a{sv}' /> <arg name='info' type='o' />
</signal> </signal>
</interface> </interface>
@ -54,6 +48,10 @@ INTROSPECTION_XML = """
<annotation name='org.freedesktop.DBus.Property.EmitsChangedSignal' value='true'/> <annotation name='org.freedesktop.DBus.Property.EmitsChangedSignal' value='true'/>
</property> </property>
<method name='Pair'>
<arg name='result' type='i' direction='out'/>
</method>
<method name='Listen'> <method name='Listen'>
<annotation name='org.freedesktop.DBus.Method.NoReply' value='true'/> <annotation name='org.freedesktop.DBus.Method.NoReply' value='true'/>
</method> </method>
@ -80,33 +78,49 @@ class TuhiDBusDevice(GObject.Object):
Class representing a DBus object for a Tuhi device. This class only Class representing a DBus object for a Tuhi device. This class only
handles the DBus bits, communication with the device is done elsewhere. 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) GObject.Object.__init__(self)
self.name = device.name self.name = device.name
self.btaddr = device.address self.btaddr = device.address
self.width, self.height = 0, 0 self.width, self.height = 0, 0
self.drawings = [] self.drawings = []
self.paired = paired
objpath = device.address.replace(':', '_') objpath = device.address.replace(':', '_')
self.objpath = "{}/{}".format(BASE_PATH, objpath) 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): def _register_object(self, connection):
introspection = Gio.DBusNodeInfo.new_for_xml(INTROSPECTION_XML) introspection = Gio.DBusNodeInfo.new_for_xml(INTROSPECTION_XML)
intf = introspection.lookup_interface(INTF_DEVICE) intf = introspection.lookup_interface(INTF_DEVICE)
Gio.DBusConnection.register_object(connection, return connection.register_object(self.objpath,
self.objpath, intf,
intf, self._method_cb,
self._method_cb, self._property_read_cb,
self._property_read_cb, self._property_write_cb)
self._property_write_cb)
def _method_cb(self, connection, sender, objpath, interface, methodname, args, invocation): def _method_cb(self, connection, sender, objpath, interface, methodname, args, invocation):
if interface != INTF_DEVICE: if interface != INTF_DEVICE:
return None 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() self._listen()
invocation.return_value() invocation.return_value()
elif methodname == 'GetJSONData': elif methodname == 'GetJSONData':
@ -133,6 +147,9 @@ class TuhiDBusDevice(GObject.Object):
def _property_write_cb(self): def _property_write_cb(self):
pass pass
def _pair(self):
self.emit('pair-requested')
def _listen(self): def _listen(self):
# FIXME: start listen asynchronously # FIXME: start listen asynchronously
# FIXME: update property when listen finishes # FIXME: update property when listen finishes
@ -155,18 +172,13 @@ class TuhiDBusServer(GObject.Object):
(GObject.SIGNAL_RUN_FIRST, None, ()), (GObject.SIGNAL_RUN_FIRST, None, ()),
# Signal arguments: # Signal arguments:
# pairing_stop_handler(status) # search_stop_handler(status)
# to be called when the pairing process has terminated, with # to be called when the search process has terminated, with
# an integer status code (0 == success, negative errno) # an integer status code (0 == success, negative errno)
"pairing-start-requested": "search-start-requested":
(GObject.SIGNAL_RUN_FIRST, None, (GObject.TYPE_PYOBJECT,)), (GObject.SIGNAL_RUN_FIRST, None, (GObject.TYPE_PYOBJECT,)),
"pairing-stop-requested": "search-stop-requested":
(GObject.SIGNAL_RUN_FIRST, None, ()), (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): def __init__(self):
@ -179,7 +191,7 @@ class TuhiDBusServer(GObject.Object):
self._bus_aquired, self._bus_aquired,
self._bus_name_aquired, self._bus_name_aquired,
self._bus_name_lost) self._bus_name_lost)
self._is_pairing = False self._is_searching = False
def _bus_aquired(self, connection, name): def _bus_aquired(self, connection, name):
introspection = Gio.DBusNodeInfo.new_for_xml(INTROSPECTION_XML) introspection = Gio.DBusNodeInfo.new_for_xml(INTROSPECTION_XML)
@ -203,104 +215,66 @@ class TuhiDBusServer(GObject.Object):
if interface != INTF_MANAGER: if interface != INTF_MANAGER:
return None return None
if methodname == 'StartPairing': if methodname == 'StartSearch':
self._start_pairing() self._start_search()
invocation.return_value() invocation.return_value()
elif methodname == 'StopPairing': elif methodname == 'StopSearch':
self._stop_pairing() self._stop_search()
invocation.return_value() 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): def _property_read_cb(self, connection, sender, objpath, interface, propname):
if interface != INTF_MANAGER: if interface != INTF_MANAGER:
return None return None
if propname == 'Devices': 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 return None
def _property_write_cb(self): def _property_write_cb(self):
pass pass
def _start_pairing(self): def _start_search(self):
if self._is_pairing: if self._is_searching:
return return
self._is_pairing = True self._is_searching = True
self.emit("pairing-start-requested", self._on_pairing_stop) self.emit("search-start-requested", self._on_search_stop)
def _stop_pairing(self): def _stop_search(self):
if not self._is_pairing: if not self._is_searching:
return return
self._is_pairing = False self._is_searching = False
self.emit("pairing-stop-requested") self.emit("search-stop-requested")
def _pair(self, address): def _on_search_stop(self, status):
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):
""" """
Called by whoever handles the pairing-start-requested signal Called by whoever handles the search-start-requested signal
""" """
logger.debug("Pairing has stopped") logger.debug("Search has stopped")
self._is_pairing = False self._is_searching = False
status = GLib.Variant.new_int32(status) status = GLib.Variant.new_int32(status)
status = GLib.Variant.new_tuple(status) status = GLib.Variant.new_tuple(status)
self._connection.emit_signal(None, BASE_PATH, INTF_MANAGER, self._connection.emit_signal(None, BASE_PATH, INTF_MANAGER,
"PairingStopped", status) "SearchStopped", status)
self._pairable_devices = {}
def notify_pairable_device(self, device): for dev in self._devices:
""" if dev.paired:
Notify the client that a pairable device is available. continue
"""
if not self._is_pairing:
return
logger.debug("Pairable device: {}".format(device)) dev.remove()
self._devices = [d for d in self._devices if d.paired]
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))
def cleanup(self): def cleanup(self):
Gio.bus_unown_name(self._dbus) Gio.bus_unown_name(self._dbus)
def create_device(self, device): def create_device(self, device, paired=True):
dev = TuhiDBusDevice(device, self._connection) dev = TuhiDBusDevice(device, self._connection, paired)
self._devices.append(dev) 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 return dev