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
This commit is contained in:
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
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

50
tuhi.py
View File

@ -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):

View File

@ -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 = """
<annotation name='org.freedesktop.DBus.Property.EmitsChangedSignal' value='true'/>
</property>
<method name='StartPairing'>
<method name='StartSearch'>
<annotation name='org.freedesktop.DBus.Method.NoReply' value='true'/>
</method>
<method name='StopPairing'>
<method name='StopSearch'>
<annotation name='org.freedesktop.DBus.Method.NoReply' value='true'/>
</method>
<method name='Pair'>
<arg name='address' type='s' direction='in'/>
<arg name='result' type='i' direction='out'/>
</method>
<signal name='PairingStopped'>
<signal name='SearchStopped'>
<arg name='status' type='i' />
</signal>
<signal name='PairableDevice'>
<arg name='info' type='a{sv}' />
<arg name='info' type='o' />
</signal>
</interface>
@ -54,6 +48,10 @@ INTROSPECTION_XML = """
<annotation name='org.freedesktop.DBus.Property.EmitsChangedSignal' value='true'/>
</property>
<method name='Pair'>
<arg name='result' type='i' direction='out'/>
</method>
<method name='Listen'>
<annotation name='org.freedesktop.DBus.Method.NoReply' value='true'/>
</method>
@ -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