diff --git a/tools/kete.py b/tools/kete.py index 751493a..c112a39 100755 --- a/tools/kete.py +++ b/tools/kete.py @@ -292,7 +292,6 @@ class TuhiKeteDevice(_DBusObject): else: logger.debug(f'{self}: Download done') - def _on_properties_changed(self, proxy, changed_props, invalidated_props): if changed_props is None: return diff --git a/tuhi/base.py b/tuhi/base.py index 59a5a02..0c84772 100644 --- a/tuhi/base.py +++ b/tuhi/base.py @@ -241,6 +241,7 @@ class TuhiDevice(GObject.Object): self.mode = DeviceMode.LISTEN def _on_listening_updated(self, dbus_device, pspec): + # Callback when a DBus client calls Start/Stop listening self.notify('listening') def _on_live_updated(self, dbus_device, pspec): @@ -274,6 +275,10 @@ class TuhiDevice(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__ = { 'device-added': (GObject.SignalFlags.RUN_FIRST, None, (GObject.TYPE_PYOBJECT,)), @@ -305,8 +310,10 @@ class Tuhi(GObject.Object): for dev in self.bluez.devices: self._add_device(self.bluez, dev) - self.bluez.connect('device-added', self._on_bluez_device_updated) - self.bluez.connect('device-updated', self._on_bluez_device_updated) + self.bluez.connect('device-added', + 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): self.emit('terminate') @@ -327,14 +334,6 @@ class Tuhi(GObject.Object): for addr in unregistered: 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): # Something else may turn discovery mode on, we don't care about # it then @@ -348,27 +347,40 @@ class Tuhi(GObject.Object): # restart discovery if some users are already in the listening mode self._on_listening_updated(None, None) - def _add_device(self, manager, bluez_device, hotplugged=False): - # Note: this function gets called every time the bluez device - # changes a property too (like signal strength). IOW, it gets called - # every second or so. + def _add_device(self, manager, bluez_device, from_live_update=False): + ''' + Process a new BlueZ device that may be one of our devices. - 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: config = self.config.devices[bluez_device.address] uuid = config['uuid'] except KeyError: - pass + uuid = None - if uuid is None and bluez_device.vendor_id not in WACOM_COMPANY_IDS: - return - - # if the device has been 'hotplugged' in the bluez stack, + # if we got here from a currently live BlueZ device, # ManufacturerData is reliable. Else, consider the device not in # 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 else: mode = DeviceMode.LISTEN @@ -392,9 +404,6 @@ class Tuhi(GObject.Object): elif d.listening: 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): listen = self._search_stop_handler is not None for dev in self.devices.values(): diff --git a/tuhi/wacom.py b/tuhi/wacom.py index 9ca68f9..7de5629 100644 --- a/tuhi/wacom.py +++ b/tuhi/wacom.py @@ -28,18 +28,15 @@ from .uhid import UHIDDevice logger = logging.getLogger('tuhi.wacom') -NORDIC_UART_SERVICE_UUID = '6e400001-b5a3-f393-e0a9-e50e24dcca9e' -NORDIC_UART_CHRC_TX_UUID = '6e400002-b5a3-f393-e0a9-e50e24dcca9e' -NORDIC_UART_CHRC_RX_UUID = '6e400003-b5a3-f393-e0a9-e50e24dcca9e' - -WACOM_LIVE_SERVICE_UUID = '00001523-1212-efde-1523-785feabcd123' -WACOM_CHRC_LIVE_PEN_DATA_UUID = '00001524-1212-efde-1523-785feabcd123' - -WACOM_OFFLINE_SERVICE_UUID = 'ffee0001-bbaa-9988-7766-554433221100' -WACOM_OFFLINE_CHRC_PEN_DATA_UUID = 'ffee0003-bbaa-9988-7766-554433221100' - -MYSTERIOUS_NOTIFICATION_SERVICE_UUID = '3a340720-c572-11e5-86c5-0002a5d5c51b' -MYSTERIOUS_NOTIFICATION_CHRC_UUID = '3a340721-c572-11e5-86c5-0002a5d5c51b' +NORDIC_UART_SERVICE_UUID = '6e400001-b5a3-f393-e0a9-e50e24dcca9e' # NOQA +NORDIC_UART_CHRC_TX_UUID = '6e400002-b5a3-f393-e0a9-e50e24dcca9e' # NOQA +NORDIC_UART_CHRC_RX_UUID = '6e400003-b5a3-f393-e0a9-e50e24dcca9e' # NOQA +WACOM_LIVE_SERVICE_UUID = '00001523-1212-efde-1523-785feabcd123' # NOQA +WACOM_CHRC_LIVE_PEN_DATA_UUID = '00001524-1212-efde-1523-785feabcd123' # NOQA +WACOM_OFFLINE_SERVICE_UUID = 'ffee0001-bbaa-9988-7766-554433221100' # NOQA +WACOM_OFFLINE_CHRC_PEN_DATA_UUID = 'ffee0003-bbaa-9988-7766-554433221100' # NOQA +SYSEVENT_NOTIFICATION_SERVICE_UUID = '3a340720-c572-11e5-86c5-0002a5d5c51b' # NOQA +SYSEVENT_NOTIFICATION_CHRC_UUID = '3a340721-c572-11e5-86c5-0002a5d5c51b' # NOQA @enum.unique @@ -141,6 +138,8 @@ class DataLogger(object): This uses a logger for stdout, but it also writes the log files to disk for future re-use. + Targets for log are $HOME/.share/tuhi/12:AB:23:CD:.../.yml + ''' class _Nordic(object): source = 'NORDIC' @@ -163,8 +162,8 @@ class DataLogger(object): def recv(self, data): return self.parent._recv(self.source, data) - class _Mysterious(object): - source = 'MYSTERIOUS' + class _SysEvent(object): + source = 'SYSEVENT' def __init__(self, parent): self.parent = parent @@ -173,21 +172,21 @@ class DataLogger(object): return self.parent._recv(self.source, data) commands = { - 0xb1: 'start/stop live', - 0xb6: 'set time', - 0xb7: 'get firmware', - 0xb9: 'read battery info', - 0xbb: 'get/set name', - 0xc1: 'check for data', - 0xc3: 'start reading', - 0xc5: 'fetch data', - 0xc8: 'end of data', - 0xca: 'ack transaction', - 0xcc: 'fetch data', - 0xea: 'get dimensions', - 0xe5: 'finish registering', - 0xe6: 'check connection', - 0xdb: 'get name', + 0xb1: 'start/stop live', + 0xb6: 'set time', + 0xb7: 'get firmware', + 0xb9: 'read battery info', + 0xbb: 'get/set name', + 0xc1: 'check for data', + 0xc3: 'start reading', + 0xc5: 'fetch data', + 0xc8: 'end of data', + 0xca: 'ack transaction', + 0xcc: 'fetch data', + 0xea: 'get dimensions', + 0xe5: 'finish registering', + 0xe6: 'check connection', + 0xdb: 'get name', } def __init__(self, bluez_device): @@ -204,7 +203,7 @@ class DataLogger(object): self.nordic = DataLogger._Nordic(self) self.pen = DataLogger._Pen(self) - self.mysterious = DataLogger._Mysterious(self) + self.sysevent = DataLogger._SysEvent(self) self.logfile = None def _on_bluez_connected(self, bluez_device): @@ -237,10 +236,12 @@ class DataLogger(object): def _recv(self, source, data): if source in ['NORDIC', 'PEN']: - def _convert(values): return list2hex(values) + def _convert(values): + return list2hex(values) convert = _convert else: - def _convert(values): return binascii.hexlify(bytes(values)) + def _convert(values): + return binascii.hexlify(bytes(values)) convert = _convert self.logger.debug(f'{self.btaddr}: RX {source} <-- {convert(data)}') @@ -253,7 +254,7 @@ class DataLogger(object): def _send(self, source, data): command = data[0] - arguments = data[1:] + arguments = data[2:] if data[0] in self.commands: self.logger.debug(f'command: {self.commands[data[0]]}') @@ -288,6 +289,10 @@ class WacomEEAGAINException(WacomException): errno = errno.EAGAIN +class WacomUnsupportedCommandException(WacomException): + errno = errno.ENOMSG + + class WacomWrongModeException(WacomException): errno = errno.EBADE @@ -453,8 +458,8 @@ class WacomPacketHandlerUnknownFixedStrokeDataIntuosPro(WacomPacketHandler): class WacomProtocolLowLevelComm(GObject.Object): ''' 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 ''' @@ -480,30 +485,29 @@ class WacomProtocolLowLevelComm(GObject.Object): self.fw_logger.nordic.send(data) chrc.write_value(data) - def check_nordic_incoming(self): + def pop_next_message(self): answer = self.nordic_answer length = answer[1] args = answer[2:] 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 - return NordicData(answer) + return NordicData(answer[:length + 2]) def wait_nordic_data(self, expected_opcode, timeout=None): if not self.nordic_event.acquire(timeout=timeout): # timeout 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))}') - if isinstance(expected_opcode, list): - if data.opcode not in expected_opcode: - raise WacomException(f'unexpected opcode: {data.opcode:02x}') - else: - if data.opcode != expected_opcode: - raise WacomException(f'unexpected opcode: {data.opcode:02x}') + if not isinstance(expected_opcode, list): + expected_opcode = [expected_opcode] + + if data.opcode not in expected_opcode: + raise WacomException(f'unexpected opcode: {data.opcode:02x}') return data @@ -516,7 +520,7 @@ class WacomProtocolLowLevelComm(GObject.Object): elif data[0] == 0x02: raise WacomEEAGAINException(f'unexpected answer: {data[0]:02x}') elif data[0] == 0x05: - raise WacomCorruptDataException(f'invalid opcode') + raise WacomUnsupportedCommandException(f'invalid opcode') elif data[0] == 0x07: raise WacomNotRegisteredException(f'wrong device, please re-register') elif data[0] != 0x00: @@ -560,7 +564,7 @@ class WacomRegisterHelper(WacomProtocolLowLevelComm): @classmethod 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): protocol = Protocol.UNKNOWN @@ -1002,7 +1006,7 @@ class WacomProtocolBase(WacomProtocolLowLevelComm): arguments=None) self.set_time() self.read_time() - name = self.get_name() + self.get_name() self.get_firmware_version() def live_mode(self, mode, uhid): @@ -1061,8 +1065,8 @@ class WacomProtocolSlate(WacomProtocolSpark): def __init__(self, device, uuid): super().__init__(device, uuid) - device.connect_gatt_value(MYSTERIOUS_NOTIFICATION_CHRC_UUID, - self._on_mysterious_data_received) + device.connect_gatt_value(SYSEVENT_NOTIFICATION_CHRC_UUID, + self._on_sysevent_data_received) def live_mode(self, mode, uhid): # Slate tablet has two models A5 and A4 @@ -1075,8 +1079,8 @@ class WacomProtocolSlate(WacomProtocolSpark): return super().live_mode(mode, uhid) - def _on_mysterious_data_received(self, name, value): - self.fw_logger.mysterious.recv(value) + def _on_sysevent_data_received(self, name, value): + self.fw_logger.sysevent.recv(value) def ack_transaction(self): self.send_nordic_command_sync(command=0xca) @@ -1100,7 +1104,7 @@ class WacomProtocolSlate(WacomProtocolSpark): self.set_time() self.read_time() self.ec_command() - name = self.get_name() + self.get_name() w, h = self.get_dimensions() if self.width != w or self.height != h: @@ -1122,7 +1126,7 @@ class WacomProtocolSlate(WacomProtocolSpark): self.height = h self.notify('dimensions') - fw = self.get_firmware_version() + self.get_firmware_version() self.ec_command() if self.read_offline_data() == 0: logger.info('no data to retrieve') @@ -1308,9 +1312,9 @@ class WacomDevice(GObject.Object): def _init_protocol(self, protocol): protocols = { - Protocol.SPARK: WacomProtocolSpark, - Protocol.SLATE: WacomProtocolSlate, - Protocol.INTUOS_PRO: WacomProtocolIntuosPro, + Protocol.SPARK: WacomProtocolSpark, + Protocol.SLATE: WacomProtocolSlate, + Protocol.INTUOS_PRO: WacomProtocolIntuosPro, } if protocol not in protocols: