Merge branch 'master' into wip/tuhigui

pull/145/head
Peter Hutterer 2019-08-09 15:22:07 +10:00
commit ee5640b783
3 changed files with 95 additions and 83 deletions

View File

@ -292,7 +292,6 @@ class TuhiKeteDevice(_DBusObject):
else: else:
logger.debug(f'{self}: Download done') logger.debug(f'{self}: Download done')
def _on_properties_changed(self, proxy, changed_props, invalidated_props): def _on_properties_changed(self, proxy, changed_props, invalidated_props):
if changed_props is None: if changed_props is None:
return return

View File

@ -241,6 +241,7 @@ class TuhiDevice(GObject.Object):
self.mode = DeviceMode.LISTEN self.mode = DeviceMode.LISTEN
def _on_listening_updated(self, dbus_device, pspec): def _on_listening_updated(self, dbus_device, pspec):
# Callback when a DBus client calls Start/Stop listening
self.notify('listening') self.notify('listening')
def _on_live_updated(self, dbus_device, pspec): def _on_live_updated(self, dbus_device, pspec):
@ -274,6 +275,10 @@ class TuhiDevice(GObject.Object):
class Tuhi(GObject.Object): class Tuhi(GObject.Object):
'''
The Tuhi object is the main entry point and glue object between the
backend and the DBus server.
'''
__gsignals__ = { __gsignals__ = {
'device-added': 'device-added':
(GObject.SignalFlags.RUN_FIRST, None, (GObject.TYPE_PYOBJECT,)), (GObject.SignalFlags.RUN_FIRST, None, (GObject.TYPE_PYOBJECT,)),
@ -305,8 +310,10 @@ class Tuhi(GObject.Object):
for dev in self.bluez.devices: for dev in self.bluez.devices:
self._add_device(self.bluez, dev) self._add_device(self.bluez, dev)
self.bluez.connect('device-added', self._on_bluez_device_updated) self.bluez.connect('device-added',
self.bluez.connect('device-updated', self._on_bluez_device_updated) lambda mgr, dev: self._add_device(mgr, dev, True))
self.bluez.connect('device-updated',
lambda mgr, dev: self._add_device(mgr, dev, True))
def _on_tuhi_bus_name_lost(self, dbus_server): def _on_tuhi_bus_name_lost(self, dbus_server):
self.emit('terminate') self.emit('terminate')
@ -327,14 +334,6 @@ class Tuhi(GObject.Object):
for addr in unregistered: for addr in unregistered:
del self.devices[addr] del self.devices[addr]
@classmethod
def _device_in_register_mode(cls, bluez_device):
if bluez_device.vendor_id not in WACOM_COMPANY_IDS:
return False
manufacturer_data = bluez_device.manufacturer_data
return manufacturer_data is not None and len(manufacturer_data) == 4
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
@ -348,27 +347,40 @@ class Tuhi(GObject.Object):
# restart discovery if some users are already in the listening mode # restart discovery if some users are already in the listening mode
self._on_listening_updated(None, None) self._on_listening_updated(None, None)
def _add_device(self, manager, bluez_device, hotplugged=False): def _add_device(self, manager, bluez_device, from_live_update=False):
# Note: this function gets called every time the bluez device '''
# changes a property too (like signal strength). IOW, it gets called Process a new BlueZ device that may be one of our devices.
# every second or so.
uuid = None This function is called once during intial setup to enumerate the
BlueZ devices and for every BlueZ device property change. Including
RSSI which will give you a value every second or so.
# check if the device is already known by us .. :param from_live_update: True if this function was called from a BlueZ
device property update. False when called during the initial setup
stage.
'''
# We have a reverse-engineered protocol. Let's not talk to anyone
# who doesn't look like we know them to avoid potentially bricking a
# device.
if bluez_device.vendor_id not in WACOM_COMPANY_IDS:
return
# check if the device is already known to us
try: try:
config = self.config.devices[bluez_device.address] config = self.config.devices[bluez_device.address]
uuid = config['uuid'] uuid = config['uuid']
except KeyError: except KeyError:
pass uuid = None
if uuid is None and bluez_device.vendor_id not in WACOM_COMPANY_IDS: # if we got here from a currently live BlueZ device,
return
# if the device has been 'hotplugged' in the bluez stack,
# ManufacturerData is reliable. Else, consider the device not in # ManufacturerData is reliable. Else, consider the device not in
# register mode # register mode
if hotplugged and Tuhi._device_in_register_mode(bluez_device): #
# When the device is in register mode (blue light blinking), the
# manufacturer is merely 4 bytes. This will reset to 7 bytes even
# when the device simply times out and does not register fully.
if from_live_update and len(bluez_device.manufacturer_data or []) == 4:
mode = DeviceMode.REGISTER mode = DeviceMode.REGISTER
else: else:
mode = DeviceMode.LISTEN mode = DeviceMode.LISTEN
@ -392,9 +404,6 @@ class Tuhi(GObject.Object):
elif d.listening: elif d.listening:
d.listen() d.listen()
def _on_bluez_device_updated(self, manager, bluez_device):
self._add_device(manager, bluez_device, True)
def _on_listening_updated(self, tuhi_dbus_device, pspec): def _on_listening_updated(self, tuhi_dbus_device, pspec):
listen = self._search_stop_handler is not None listen = self._search_stop_handler is not None
for dev in self.devices.values(): for dev in self.devices.values():

View File

@ -28,18 +28,15 @@ from .uhid import UHIDDevice
logger = logging.getLogger('tuhi.wacom') logger = logging.getLogger('tuhi.wacom')
NORDIC_UART_SERVICE_UUID = '6e400001-b5a3-f393-e0a9-e50e24dcca9e' NORDIC_UART_SERVICE_UUID = '6e400001-b5a3-f393-e0a9-e50e24dcca9e' # NOQA
NORDIC_UART_CHRC_TX_UUID = '6e400002-b5a3-f393-e0a9-e50e24dcca9e' NORDIC_UART_CHRC_TX_UUID = '6e400002-b5a3-f393-e0a9-e50e24dcca9e' # NOQA
NORDIC_UART_CHRC_RX_UUID = '6e400003-b5a3-f393-e0a9-e50e24dcca9e' NORDIC_UART_CHRC_RX_UUID = '6e400003-b5a3-f393-e0a9-e50e24dcca9e' # NOQA
WACOM_LIVE_SERVICE_UUID = '00001523-1212-efde-1523-785feabcd123' # NOQA
WACOM_LIVE_SERVICE_UUID = '00001523-1212-efde-1523-785feabcd123' WACOM_CHRC_LIVE_PEN_DATA_UUID = '00001524-1212-efde-1523-785feabcd123' # NOQA
WACOM_CHRC_LIVE_PEN_DATA_UUID = '00001524-1212-efde-1523-785feabcd123' WACOM_OFFLINE_SERVICE_UUID = 'ffee0001-bbaa-9988-7766-554433221100' # NOQA
WACOM_OFFLINE_CHRC_PEN_DATA_UUID = 'ffee0003-bbaa-9988-7766-554433221100' # NOQA
WACOM_OFFLINE_SERVICE_UUID = 'ffee0001-bbaa-9988-7766-554433221100' SYSEVENT_NOTIFICATION_SERVICE_UUID = '3a340720-c572-11e5-86c5-0002a5d5c51b' # NOQA
WACOM_OFFLINE_CHRC_PEN_DATA_UUID = 'ffee0003-bbaa-9988-7766-554433221100' SYSEVENT_NOTIFICATION_CHRC_UUID = '3a340721-c572-11e5-86c5-0002a5d5c51b' # NOQA
MYSTERIOUS_NOTIFICATION_SERVICE_UUID = '3a340720-c572-11e5-86c5-0002a5d5c51b'
MYSTERIOUS_NOTIFICATION_CHRC_UUID = '3a340721-c572-11e5-86c5-0002a5d5c51b'
@enum.unique @enum.unique
@ -141,6 +138,8 @@ class DataLogger(object):
This uses a logger for stdout, but it also writes the log files to disk This uses a logger for stdout, but it also writes the log files to disk
for future re-use. for future re-use.
Targets for log are $HOME/.share/tuhi/12:AB:23:CD:.../<timestamp>.yml
''' '''
class _Nordic(object): class _Nordic(object):
source = 'NORDIC' source = 'NORDIC'
@ -163,8 +162,8 @@ class DataLogger(object):
def recv(self, data): def recv(self, data):
return self.parent._recv(self.source, data) return self.parent._recv(self.source, data)
class _Mysterious(object): class _SysEvent(object):
source = 'MYSTERIOUS' source = 'SYSEVENT'
def __init__(self, parent): def __init__(self, parent):
self.parent = parent self.parent = parent
@ -173,21 +172,21 @@ class DataLogger(object):
return self.parent._recv(self.source, data) return self.parent._recv(self.source, data)
commands = { commands = {
0xb1: 'start/stop live', 0xb1: 'start/stop live',
0xb6: 'set time', 0xb6: 'set time',
0xb7: 'get firmware', 0xb7: 'get firmware',
0xb9: 'read battery info', 0xb9: 'read battery info',
0xbb: 'get/set name', 0xbb: 'get/set name',
0xc1: 'check for data', 0xc1: 'check for data',
0xc3: 'start reading', 0xc3: 'start reading',
0xc5: 'fetch data', 0xc5: 'fetch data',
0xc8: 'end of data', 0xc8: 'end of data',
0xca: 'ack transaction', 0xca: 'ack transaction',
0xcc: 'fetch data', 0xcc: 'fetch data',
0xea: 'get dimensions', 0xea: 'get dimensions',
0xe5: 'finish registering', 0xe5: 'finish registering',
0xe6: 'check connection', 0xe6: 'check connection',
0xdb: 'get name', 0xdb: 'get name',
} }
def __init__(self, bluez_device): def __init__(self, bluez_device):
@ -204,7 +203,7 @@ class DataLogger(object):
self.nordic = DataLogger._Nordic(self) self.nordic = DataLogger._Nordic(self)
self.pen = DataLogger._Pen(self) self.pen = DataLogger._Pen(self)
self.mysterious = DataLogger._Mysterious(self) self.sysevent = DataLogger._SysEvent(self)
self.logfile = None self.logfile = None
def _on_bluez_connected(self, bluez_device): def _on_bluez_connected(self, bluez_device):
@ -237,10 +236,12 @@ class DataLogger(object):
def _recv(self, source, data): def _recv(self, source, data):
if source in ['NORDIC', 'PEN']: if source in ['NORDIC', 'PEN']:
def _convert(values): return list2hex(values) def _convert(values):
return list2hex(values)
convert = _convert convert = _convert
else: else:
def _convert(values): return binascii.hexlify(bytes(values)) def _convert(values):
return binascii.hexlify(bytes(values))
convert = _convert convert = _convert
self.logger.debug(f'{self.btaddr}: RX {source} <-- {convert(data)}') self.logger.debug(f'{self.btaddr}: RX {source} <-- {convert(data)}')
@ -253,7 +254,7 @@ class DataLogger(object):
def _send(self, source, data): def _send(self, source, data):
command = data[0] command = data[0]
arguments = data[1:] arguments = data[2:]
if data[0] in self.commands: if data[0] in self.commands:
self.logger.debug(f'command: {self.commands[data[0]]}') self.logger.debug(f'command: {self.commands[data[0]]}')
@ -288,6 +289,10 @@ class WacomEEAGAINException(WacomException):
errno = errno.EAGAIN errno = errno.EAGAIN
class WacomUnsupportedCommandException(WacomException):
errno = errno.ENOMSG
class WacomWrongModeException(WacomException): class WacomWrongModeException(WacomException):
errno = errno.EBADE errno = errno.EBADE
@ -453,8 +458,8 @@ class WacomPacketHandlerUnknownFixedStrokeDataIntuosPro(WacomPacketHandler):
class WacomProtocolLowLevelComm(GObject.Object): class WacomProtocolLowLevelComm(GObject.Object):
''' '''
Internal class to handle the communication with the Wacom device. Internal class to handle the communication with the Wacom device.
No-one should directly instanciate this. No-one should directly instanciate this, use the device-specific
subclass instead (e.g. WacomProtocolIntuosPro).
:param device: the BlueZDevice object that is this wacom device :param device: the BlueZDevice object that is this wacom device
''' '''
@ -480,30 +485,29 @@ class WacomProtocolLowLevelComm(GObject.Object):
self.fw_logger.nordic.send(data) self.fw_logger.nordic.send(data)
chrc.write_value(data) chrc.write_value(data)
def check_nordic_incoming(self): def pop_next_message(self):
answer = self.nordic_answer answer = self.nordic_answer
length = answer[1] length = answer[1]
args = answer[2:] args = answer[2:]
if length > len(args): if length > len(args):
raise WacomException(f'error while processing answer, should get an answer of size {length} instead of {len(args)}') raise WacomException(f'Invalid answer message length: expected {length}, got {len(args)}')
self.nordic_answer = self.nordic_answer[length + 2:] # opcode + len self.nordic_answer = self.nordic_answer[length + 2:] # opcode + len
return NordicData(answer) return NordicData(answer[:length + 2])
def wait_nordic_data(self, expected_opcode, timeout=None): def wait_nordic_data(self, expected_opcode, timeout=None):
if not self.nordic_event.acquire(timeout=timeout): if not self.nordic_event.acquire(timeout=timeout):
# timeout # timeout
raise WacomTimeoutException(f'{self.device.name}: Timeout while reading data') raise WacomTimeoutException(f'{self.device.name}: Timeout while reading data')
data = self.check_nordic_incoming() data = self.pop_next_message()
# logger.debug(f'received {data.opcode:02x} / {data.length:02x} / {b2hex(bytes(data))}') # logger.debug(f'received {data.opcode:02x} / {data.length:02x} / {b2hex(bytes(data))}')
if isinstance(expected_opcode, list): if not isinstance(expected_opcode, list):
if data.opcode not in expected_opcode: expected_opcode = [expected_opcode]
raise WacomException(f'unexpected opcode: {data.opcode:02x}')
else: if data.opcode not in expected_opcode:
if data.opcode != expected_opcode: raise WacomException(f'unexpected opcode: {data.opcode:02x}')
raise WacomException(f'unexpected opcode: {data.opcode:02x}')
return data return data
@ -516,7 +520,7 @@ class WacomProtocolLowLevelComm(GObject.Object):
elif data[0] == 0x02: elif data[0] == 0x02:
raise WacomEEAGAINException(f'unexpected answer: {data[0]:02x}') raise WacomEEAGAINException(f'unexpected answer: {data[0]:02x}')
elif data[0] == 0x05: elif data[0] == 0x05:
raise WacomCorruptDataException(f'invalid opcode') raise WacomUnsupportedCommandException(f'invalid opcode')
elif data[0] == 0x07: elif data[0] == 0x07:
raise WacomNotRegisteredException(f'wrong device, please re-register') raise WacomNotRegisteredException(f'wrong device, please re-register')
elif data[0] != 0x00: elif data[0] != 0x00:
@ -560,7 +564,7 @@ class WacomRegisterHelper(WacomProtocolLowLevelComm):
@classmethod @classmethod
def is_spark(cls, device): def is_spark(cls, device):
return MYSTERIOUS_NOTIFICATION_CHRC_UUID not in device.characteristics return SYSEVENT_NOTIFICATION_CHRC_UUID not in device.characteristics
def register_device(self, uuid): def register_device(self, uuid):
protocol = Protocol.UNKNOWN protocol = Protocol.UNKNOWN
@ -1002,7 +1006,7 @@ class WacomProtocolBase(WacomProtocolLowLevelComm):
arguments=None) arguments=None)
self.set_time() self.set_time()
self.read_time() self.read_time()
name = self.get_name() self.get_name()
self.get_firmware_version() self.get_firmware_version()
def live_mode(self, mode, uhid): def live_mode(self, mode, uhid):
@ -1061,8 +1065,8 @@ class WacomProtocolSlate(WacomProtocolSpark):
def __init__(self, device, uuid): def __init__(self, device, uuid):
super().__init__(device, uuid) super().__init__(device, uuid)
device.connect_gatt_value(MYSTERIOUS_NOTIFICATION_CHRC_UUID, device.connect_gatt_value(SYSEVENT_NOTIFICATION_CHRC_UUID,
self._on_mysterious_data_received) self._on_sysevent_data_received)
def live_mode(self, mode, uhid): def live_mode(self, mode, uhid):
# Slate tablet has two models A5 and A4 # Slate tablet has two models A5 and A4
@ -1075,8 +1079,8 @@ class WacomProtocolSlate(WacomProtocolSpark):
return super().live_mode(mode, uhid) return super().live_mode(mode, uhid)
def _on_mysterious_data_received(self, name, value): def _on_sysevent_data_received(self, name, value):
self.fw_logger.mysterious.recv(value) self.fw_logger.sysevent.recv(value)
def ack_transaction(self): def ack_transaction(self):
self.send_nordic_command_sync(command=0xca) self.send_nordic_command_sync(command=0xca)
@ -1100,7 +1104,7 @@ class WacomProtocolSlate(WacomProtocolSpark):
self.set_time() self.set_time()
self.read_time() self.read_time()
self.ec_command() self.ec_command()
name = self.get_name() self.get_name()
w, h = self.get_dimensions() w, h = self.get_dimensions()
if self.width != w or self.height != h: if self.width != w or self.height != h:
@ -1122,7 +1126,7 @@ class WacomProtocolSlate(WacomProtocolSpark):
self.height = h self.height = h
self.notify('dimensions') self.notify('dimensions')
fw = self.get_firmware_version() self.get_firmware_version()
self.ec_command() self.ec_command()
if self.read_offline_data() == 0: if self.read_offline_data() == 0:
logger.info('no data to retrieve') logger.info('no data to retrieve')
@ -1308,9 +1312,9 @@ class WacomDevice(GObject.Object):
def _init_protocol(self, protocol): def _init_protocol(self, protocol):
protocols = { protocols = {
Protocol.SPARK: WacomProtocolSpark, Protocol.SPARK: WacomProtocolSpark,
Protocol.SLATE: WacomProtocolSlate, Protocol.SLATE: WacomProtocolSlate,
Protocol.INTUOS_PRO: WacomProtocolIntuosPro, Protocol.INTUOS_PRO: WacomProtocolIntuosPro,
} }
if protocol not in protocols: if protocol not in protocols: