Merge f348740416
into 5f5ce71aef
commit
025e4efe47
23
README.md
23
README.md
|
@ -33,10 +33,27 @@ The following interfaces are provided:
|
|||
```
|
||||
org.freedesktop.tuhi1.Manager
|
||||
|
||||
Property: Devices (ao)
|
||||
Property: Devices (ao)
|
||||
Array of object paths to known (previously paired, but not necessarily
|
||||
connected) devices.
|
||||
|
||||
Array of object paths to known (previously paired, but not necessarily
|
||||
connected) devices.
|
||||
Property: PairableDevices (ass)
|
||||
Array of (bluetooth address, name) of pairable devices that are in
|
||||
proximity.
|
||||
|
||||
This property will only return data when the Manager is in Pairing mode
|
||||
|
||||
Method: StartPairing() -> ()
|
||||
Listen to available devices in pairing mode for an unspecified timeout.
|
||||
When the timeout expires, a PairingComplete signal is sent indicating
|
||||
success or error.
|
||||
|
||||
Returns: nothing
|
||||
|
||||
Method: Pair(s) -> (i)
|
||||
Pairs the given device specified by its bluetooth mac address.
|
||||
|
||||
Returns: 0 on success or a negative errno on failure
|
||||
|
||||
org.freedesktop.tuhi1.Device
|
||||
|
||||
|
|
54
tuhi.py
54
tuhi.py
|
@ -82,20 +82,22 @@ class TuhiDevice(GObject.Object):
|
|||
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
|
||||
|
||||
bluez_device.connect('connected', self._on_bluez_device_connected)
|
||||
bluez_device.connect('disconnected', self._on_bluez_device_disconnected)
|
||||
bluez_device.connect_device()
|
||||
self._bluez_device = bluez_device
|
||||
|
||||
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._wacom_device.start(self.pairing_mode)
|
||||
self.pairing_mode = False
|
||||
|
||||
def _on_bluez_device_disconnected(self, bluez_device):
|
||||
# FIXME: immediately try to reconnect, at least until the DBusServer
|
||||
# is hooked up correctly
|
||||
logger.debug('{}: disconnected'.format(bluez_device.address))
|
||||
bluez_device.connect_device()
|
||||
|
||||
def _on_drawing_received(self, device, drawing):
|
||||
logger.debug('Drawing received')
|
||||
|
@ -142,22 +144,64 @@ 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('start-pairing-requested', self._on_start_pairing_requested)
|
||||
self.server.connect('pair-device-requested', self._on_pair_device_requested)
|
||||
self.bluez = BlueZDeviceManager()
|
||||
self.bluez.connect('device-added', self._on_bluez_device_added)
|
||||
self.bluez.connect('device-updated', self._on_bluez_device_updated)
|
||||
self.bluez.connect('discovery-stopped', self._on_discovery_stopped)
|
||||
|
||||
self.devices = {}
|
||||
|
||||
def _on_tuhi_bus_name_acquired(self, dbus_server):
|
||||
self.bluez.connect_to_bluez()
|
||||
|
||||
def _on_start_pairing_requested(self, dbus_server):
|
||||
self.bluez.start_discovery(30)
|
||||
|
||||
@classmethod
|
||||
def _is_pairing_device(cls, bluez_device):
|
||||
if bluez_device.vendor_id != WACOM_COMPANY_ID:
|
||||
return False
|
||||
|
||||
manufacturer_data = bluez_device.get_manufacturer_data(WACOM_COMPANY_ID)
|
||||
return len(manufacturer_data) == 4
|
||||
|
||||
def _on_bluez_device_added(self, manager, bluez_device):
|
||||
if bluez_device.vendor_id != WACOM_COMPANY_ID:
|
||||
return
|
||||
|
||||
if Tuhi._is_pairing_device(bluez_device):
|
||||
return
|
||||
|
||||
tuhi_dbus_device = self.server.create_device(bluez_device)
|
||||
d = TuhiDevice(bluez_device, tuhi_dbus_device)
|
||||
self.devices[bluez_device.address] = d
|
||||
|
||||
def _on_bluez_device_updated(self, manager, bluez_device):
|
||||
if bluez_device.vendor_id != WACOM_COMPANY_ID:
|
||||
return
|
||||
|
||||
if not Tuhi._is_pairing_device(bluez_device):
|
||||
return
|
||||
|
||||
self.server.add_pairing_device(bluez_device)
|
||||
|
||||
def _on_discovery_stopped(self, bluez_device):
|
||||
self.server.reset_pairing_devices()
|
||||
|
||||
def _on_pair_device_requested(self, manager, address):
|
||||
bluez_device = self.server.get_pairing_device(address)
|
||||
if bluez_device is None:
|
||||
# FIXME: we should return the dbus method an error
|
||||
return
|
||||
|
||||
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()
|
||||
|
||||
|
||||
def main(args):
|
||||
desc = "Daemon to extract the pen stroke data from Wacom SmartPad devices"
|
||||
|
|
53
tuhi/ble.py
53
tuhi/ble.py
|
@ -95,6 +95,8 @@ class BlueZDevice(GObject.Object):
|
|||
(GObject.SIGNAL_RUN_FIRST, None, ()),
|
||||
"disconnected":
|
||||
(GObject.SIGNAL_RUN_FIRST, None, ()),
|
||||
"updated":
|
||||
(GObject.SIGNAL_RUN_FIRST, None, ()),
|
||||
}
|
||||
|
||||
def __init__(self, om, obj):
|
||||
|
@ -140,6 +142,12 @@ class BlueZDevice(GObject.Object):
|
|||
return (self.interface.get_cached_property('Connected').unpack() and
|
||||
self.interface.get_cached_property('ServicesResolved').unpack())
|
||||
|
||||
def get_manufacturer_data(self, vendor_id):
|
||||
md = self.interface.get_cached_property('ManufacturerData')
|
||||
if md is not None and vendor_id in md.keys():
|
||||
return md[vendor_id]
|
||||
return None
|
||||
|
||||
def resolve(self, om):
|
||||
"""
|
||||
Resolve the GattServices and GattCharacteristics. This function does
|
||||
|
@ -231,6 +239,8 @@ class BlueZDevice(GObject.Object):
|
|||
elif 'ServicesResolved' in properties:
|
||||
if properties['ServicesResolved']:
|
||||
self.emit('connected')
|
||||
elif 'RSSI' in properties:
|
||||
self.emit('updated')
|
||||
|
||||
def connect_gatt_value(self, uuid, callback):
|
||||
"""
|
||||
|
@ -256,6 +266,10 @@ class BlueZDeviceManager(GObject.Object):
|
|||
__gsignals__ = {
|
||||
"device-added":
|
||||
(GObject.SIGNAL_RUN_FIRST, None, (GObject.TYPE_PYOBJECT,)),
|
||||
"device-updated":
|
||||
(GObject.SIGNAL_RUN_FIRST, None, (GObject.TYPE_PYOBJECT,)),
|
||||
"discovery-stopped":
|
||||
(GObject.SIGNAL_RUN_FIRST, None, ()),
|
||||
}
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
|
@ -285,6 +299,43 @@ class BlueZDeviceManager(GObject.Object):
|
|||
for obj in self._om.get_objects():
|
||||
self._process_object(obj)
|
||||
|
||||
def _discovery_timeout_expired(self):
|
||||
for obj in self._om.get_objects():
|
||||
i = obj.get_interface(ORG_BLUEZ_ADAPTER1)
|
||||
if i is None:
|
||||
continue
|
||||
|
||||
objpath = obj.get_object_path()
|
||||
i.StopDiscovery()
|
||||
logger.debug('Discovery stopped on: {}'.format(objpath))
|
||||
|
||||
self.emit('discovery-stopped')
|
||||
return False
|
||||
|
||||
def start_discovery(self, timeout):
|
||||
"""
|
||||
Start discovery mode for the specified timeout.
|
||||
|
||||
A value of 0 for the timeout means infinite.
|
||||
"""
|
||||
for obj in self._om.get_objects():
|
||||
i = obj.get_interface(ORG_BLUEZ_ADAPTER1)
|
||||
if i is None:
|
||||
continue
|
||||
|
||||
objpath = obj.get_object_path()
|
||||
i.StartDiscovery()
|
||||
logger.debug('Discovery started on: {}'.format(objpath))
|
||||
if timeout >= 0:
|
||||
logger.debug('Setting the timeout to {}'.format(timeout))
|
||||
GObject.timeout_add_seconds(timeout, self._discovery_timeout_expired)
|
||||
|
||||
def _on_dev_updated(self, dev):
|
||||
"""Callback for Device's properties-changed"""
|
||||
logger.debug('Object updated: {}'.format(dev.name))
|
||||
|
||||
self.emit("device-updated", dev)
|
||||
|
||||
def _on_om_object_added(self, om, obj):
|
||||
"""Callback for ObjectManager's object-added"""
|
||||
objpath = obj.get_object_path()
|
||||
|
@ -318,11 +369,11 @@ class BlueZDeviceManager(GObject.Object):
|
|||
def _process_adapter(self, obj):
|
||||
objpath = obj.get_object_path()
|
||||
logger.debug('Adapter: {}'.format(objpath))
|
||||
# FIXME: call StartDiscovery if we want to pair
|
||||
|
||||
def _process_device(self, obj):
|
||||
dev = BlueZDevice(self._om, obj)
|
||||
self.devices.append(dev)
|
||||
dev.connect("updated", self._on_dev_updated)
|
||||
self.emit("device-added", dev)
|
||||
|
||||
def _process_characteristic(self, obj):
|
||||
|
|
|
@ -23,6 +23,19 @@ INTROSPECTION_XML = """
|
|||
<property type='ao' name='Devices' access='read'>
|
||||
<annotation name='org.freedesktop.DBus.Property.EmitsChangedSignal' value='true'/>
|
||||
</property>
|
||||
<property type='a(ss)' name='PairingDevices' access='read'>
|
||||
<annotation name='org.freedesktop.DBus.Property.EmitsChangedSignal' value='true'/>
|
||||
</property>
|
||||
|
||||
<method name='StartPairing'>
|
||||
<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>
|
||||
|
||||
</interface>
|
||||
|
||||
<interface name='org.freedesktop.tuhi1.Device'>
|
||||
|
@ -131,11 +144,16 @@ class TuhiDBusServer(GObject.Object):
|
|||
__gsignals__ = {
|
||||
"bus-name-acquired":
|
||||
(GObject.SIGNAL_RUN_FIRST, None, ()),
|
||||
"start-pairing-requested":
|
||||
(GObject.SIGNAL_RUN_FIRST, None, ()),
|
||||
"pair-device-requested":
|
||||
(GObject.SIGNAL_RUN_FIRST, None, (GObject.TYPE_STRING,)),
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
GObject.Object.__init__(self)
|
||||
self._devices = []
|
||||
self._pairing_devices = {}
|
||||
self._dbus = Gio.bus_own_name(Gio.BusType.SESSION,
|
||||
BUS_NAME,
|
||||
Gio.BusNameOwnerFlags.NONE,
|
||||
|
@ -161,8 +179,17 @@ class TuhiDBusServer(GObject.Object):
|
|||
def _bus_name_lost(self, connection, name):
|
||||
pass
|
||||
|
||||
def _method_cb(self):
|
||||
pass
|
||||
def _method_cb(self, connection, sender, objpath, interface, methodname, args, invocation):
|
||||
if interface != INTF_MANAGER:
|
||||
return None
|
||||
|
||||
if methodname == 'StartPairing':
|
||||
self._start_pairing()
|
||||
invocation.return_value()
|
||||
elif methodname == 'Pair':
|
||||
self.emit('pair-device-requested', args[0])
|
||||
result = GLib.Variant.new_int32(0)
|
||||
invocation.return_value(GLib.Variant.new_tuple(result))
|
||||
|
||||
def _property_read_cb(self, connection, sender, objpath, interface, propname):
|
||||
if interface != INTF_MANAGER:
|
||||
|
@ -170,12 +197,21 @@ class TuhiDBusServer(GObject.Object):
|
|||
|
||||
if propname == 'Devices':
|
||||
return GLib.Variant.new_objv([d.objpath for d in self._devices])
|
||||
elif propname == 'PairingDevices':
|
||||
return GLib.Variant.new_array(GLib.VariantType.new('(ss)'),
|
||||
[GLib.Variant.new_tuple(
|
||||
GLib.Variant.new_string(k),
|
||||
GLib.Variant.new_string(v.name))
|
||||
for k, v in self._pairing_devices.items()])
|
||||
|
||||
return None
|
||||
|
||||
def _property_write_cb(self):
|
||||
pass
|
||||
|
||||
def _start_pairing(self):
|
||||
self.emit("start-pairing-requested")
|
||||
|
||||
def cleanup(self):
|
||||
Gio.bus_unown_name(self._dbus)
|
||||
|
||||
|
@ -183,3 +219,19 @@ class TuhiDBusServer(GObject.Object):
|
|||
dev = TuhiDBusDevice(device, self._connection)
|
||||
self._devices.append(dev)
|
||||
return dev
|
||||
|
||||
def add_pairing_device(self, bluez_device):
|
||||
address = bluez_device.address
|
||||
if address in self._pairing_devices:
|
||||
return
|
||||
|
||||
self._pairing_devices[address] = bluez_device
|
||||
|
||||
def reset_pairing_devices(self):
|
||||
self._pairing_devices = {}
|
||||
|
||||
def get_pairing_device(self, address):
|
||||
if address not in self._pairing_devices:
|
||||
return None
|
||||
|
||||
return self._pairing_devices[address]
|
||||
|
|
|
@ -97,6 +97,10 @@ class WacomEEAGAINException(WacomException):
|
|||
pass
|
||||
|
||||
|
||||
class WacomWrongModeException(WacomException):
|
||||
pass
|
||||
|
||||
|
||||
class WacomNotPairedException(WacomException):
|
||||
pass
|
||||
|
||||
|
@ -241,6 +245,8 @@ class WacomDevice(GObject.Object):
|
|||
raise WacomNotPairedException(f"wrong device, please redo pairing")
|
||||
if data[0] == 0x02:
|
||||
raise WacomEEAGAINException(f"unexpected answer: {data[0]:02x}")
|
||||
if data[0] == 0x01:
|
||||
raise WacomWrongModeException(f"wrong device mode")
|
||||
|
||||
def send_nordic_command_sync(self,
|
||||
command,
|
||||
|
@ -571,6 +577,57 @@ class WacomDevice(GObject.Object):
|
|||
except WacomEEAGAINException:
|
||||
logger.warning("no data, please make sure the LED is blue and the button is pressed to switch it back to green")
|
||||
|
||||
def register_device_slate(self):
|
||||
self.register_connection()
|
||||
logger.info("Press the button now to confirm")
|
||||
data = self.wait_nordic_data([0xe4, 0xb3], 10)
|
||||
if data.opcode == 0xb3:
|
||||
# generic ACK
|
||||
self.check_ack(data)
|
||||
self.set_time()
|
||||
self.read_time()
|
||||
self.ec_command()
|
||||
self.bb_command()
|
||||
w = self.get_dimensions('width')
|
||||
h = self.get_dimensions('height')
|
||||
if self.width != w or self.height != h:
|
||||
logger.error(f'Uncompatible dimensions: {w}x{h}')
|
||||
fw_high = self.get_firmware_version(0)
|
||||
fw_low = self.get_firmware_version(1)
|
||||
logger.info(f'firmware is {fw_high}-{fw_low}')
|
||||
logger.info("pairing completed")
|
||||
|
||||
def register_device_spark(self):
|
||||
try:
|
||||
self.check_connection()
|
||||
except WacomWrongModeException:
|
||||
# this is expected
|
||||
pass
|
||||
self.send_nordic_command(command=0xe3,
|
||||
arguments=[0x01])
|
||||
logger.info("Press the button now to confirm")
|
||||
# Wait for the button confirmation event, or any error
|
||||
data = self.wait_nordic_data([0xe4, 0xb3], 10)
|
||||
if data.opcode == 0xb3:
|
||||
# generic ACK
|
||||
self.check_ack(data)
|
||||
self.send_nordic_command_sync(command=0xe5,
|
||||
arguments=None,
|
||||
expected_opcode=0xb3)
|
||||
self.set_time()
|
||||
self.read_time()
|
||||
self.bb_command()
|
||||
fw_high = self.get_firmware_version(0)
|
||||
fw_low = self.get_firmware_version(1)
|
||||
logger.info(f'firmware is {fw_high}-{fw_low}')
|
||||
logger.info("pairing completed")
|
||||
|
||||
def register_device(self):
|
||||
if self.is_slate():
|
||||
self.register_device_slate()
|
||||
else:
|
||||
self.register_device_spark()
|
||||
|
||||
def run(self):
|
||||
if self._is_running:
|
||||
logger.error('{}: already synching, ignoring this request'.format(self.device.address))
|
||||
|
@ -579,11 +636,16 @@ class WacomDevice(GObject.Object):
|
|||
logger.debug('{}: starting'.format(self.device.address))
|
||||
self._is_running = True
|
||||
try:
|
||||
self.retrieve_data()
|
||||
if self._pairing_mode:
|
||||
self.register_device()
|
||||
else:
|
||||
self.retrieve_data()
|
||||
finally:
|
||||
self._pairing_mode = False
|
||||
self._is_running = False
|
||||
self.emit("done")
|
||||
|
||||
def start(self):
|
||||
def start(self, pairing_mode):
|
||||
self._pairing_mode = pairing_mode
|
||||
self.thread = threading.Thread(target=self.run)
|
||||
self.thread.start()
|
||||
|
|
Loading…
Reference in New Issue