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:
parent
f6519016f9
commit
aa1a5e6689
49
README.md
49
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
|
||||
|
|
50
tuhi.py
50
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):
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue