2018-01-12 06:30:46 +01:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
#
|
|
|
|
# This program is free software; you can redistribute it and/or modify
|
|
|
|
# it under the terms of the GNU General Public License as published by
|
|
|
|
# the Free Software Foundation; either version 2 of the License, or
|
|
|
|
# (at your option) any later version.
|
|
|
|
#
|
|
|
|
# This program is distributed in the hope that it will be useful,
|
|
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
# GNU General Public License for more details.
|
|
|
|
#
|
|
|
|
|
|
|
|
|
|
|
|
import binascii
|
2018-01-28 23:42:05 +01:00
|
|
|
import calendar
|
2018-02-07 16:04:57 +01:00
|
|
|
import enum
|
2018-01-12 06:30:46 +01:00
|
|
|
import logging
|
|
|
|
import threading
|
|
|
|
import time
|
2018-01-19 06:29:18 +01:00
|
|
|
import uuid
|
2018-01-24 12:06:16 +01:00
|
|
|
import errno
|
2018-01-12 06:30:46 +01:00
|
|
|
from gi.repository import GObject
|
2018-01-24 06:12:03 +01:00
|
|
|
from .drawing import Drawing
|
2018-02-13 20:01:56 +01:00
|
|
|
from .uhid import UHIDDevice
|
2018-01-12 06:30:46 +01:00
|
|
|
|
2018-01-15 15:53:32 +01:00
|
|
|
logger = logging.getLogger('tuhi.wacom')
|
2018-01-12 06:30:46 +01:00
|
|
|
|
|
|
|
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'
|
|
|
|
|
2018-01-30 14:06:29 +01:00
|
|
|
MYSTERIOUS_NOTIFICATION_SERVICE_UUID = '3a340720-c572-11e5-86c5-0002a5d5c51b'
|
|
|
|
MYSTERIOUS_NOTIFICATION_CHRC_UUID = '3a340721-c572-11e5-86c5-0002a5d5c51b'
|
|
|
|
|
2018-01-12 20:14:26 +01:00
|
|
|
|
2018-02-07 16:04:57 +01:00
|
|
|
@enum.unique
|
|
|
|
class Protocol(enum.Enum):
|
|
|
|
UNKNOWN = 'unknown'
|
|
|
|
SPARK = 'spark'
|
|
|
|
SLATE = 'slate'
|
|
|
|
INTUOS_PRO = 'intuos-pro'
|
|
|
|
|
|
|
|
|
2018-02-11 11:35:07 +01:00
|
|
|
@enum.unique
|
|
|
|
class DeviceMode(enum.Enum):
|
|
|
|
REGISTER = 1
|
|
|
|
LISTEN = 2
|
2018-02-13 20:01:56 +01:00
|
|
|
LIVE = 3
|
2018-02-11 11:35:07 +01:00
|
|
|
|
|
|
|
|
2018-02-13 20:01:56 +01:00
|
|
|
wacom_live_rdesc_template = [
|
|
|
|
0x05, 0x0d, # Usage Page (Digitizers) 0
|
|
|
|
0x09, 0x02, # Usage (Pen) 2
|
|
|
|
0xa1, 0x01, # Collection (Application) 4
|
|
|
|
0x85, 0x01, # .Report ID (1) 6
|
|
|
|
0x09, 0x20, # .Usage (Stylus) 8
|
|
|
|
0xa1, 0x00, # .Collection (Physical) 10
|
|
|
|
0x09, 0x32, # ..Usage (In Range) 12
|
|
|
|
0x15, 0x00, # ..Logical Minimum (0) 14
|
|
|
|
0x25, 0x01, # ..Logical Maximum (1) 16
|
|
|
|
0x95, 0x01, # ..Report Count (1) 18
|
|
|
|
0x75, 0x01, # ..Report Size (1) 20
|
|
|
|
0x81, 0x02, # ..Input (Data,Var,Abs) 22
|
|
|
|
0x95, 0x07, # ..Report Count (7) 24
|
|
|
|
0x81, 0x03, # ..Input (Cnst,Var,Abs) 26
|
|
|
|
0x05, 0x01, # ..Usage Page (Generic Desktop) 43
|
|
|
|
0x09, 0x30, # ..Usage (X) 45
|
|
|
|
0x75, 0x10, # ..Report Size (16) 47
|
|
|
|
0x95, 0x01, # ..Report Count (1) 49
|
|
|
|
0x55, 0x0e, # ..Unit Exponent (-2) 51
|
|
|
|
0x65, 0x11, # ..Unit (Centimeter,SILinear) 53
|
|
|
|
0x46, 0xec, 0x09, # ..Physical Maximum (2540) 55
|
2018-02-13 20:01:56 +01:00
|
|
|
'width', # ..Logical Maximum (TBD) 58
|
2018-02-13 20:01:56 +01:00
|
|
|
0x81, 0x02, # ..Input (Data,Var,Abs) 61
|
|
|
|
0x09, 0x31, # ..Usage (Y) 63
|
|
|
|
0x46, 0x9d, 0x06, # ..Physical Maximum (1693) 65
|
2018-02-13 20:01:56 +01:00
|
|
|
'height', # ..Logical Maximum (TBD) 68
|
2018-02-13 20:01:56 +01:00
|
|
|
0x81, 0x02, # ..Input (Data,Var,Abs) 71
|
|
|
|
0x05, 0x0d, # ..Usage Page (Digitizers) 73
|
|
|
|
0x09, 0x30, # ..Usage (Tip Pressure) 75
|
2018-02-13 20:01:56 +01:00
|
|
|
0x26, 0xff, 0x07, # ..Logical Maximum (2047) 77
|
2018-02-13 20:01:56 +01:00
|
|
|
0x81, 0x02, # ..Input (Data,Var,Abs) 80
|
|
|
|
0xc0, # .End Collection 82
|
|
|
|
0xc0, # End Collection 83
|
|
|
|
]
|
|
|
|
|
|
|
|
|
2018-02-13 20:01:56 +01:00
|
|
|
def flatten(items):
|
|
|
|
'''flatten an array of mixed int and arrays into a simple array of int'''
|
|
|
|
for item in items:
|
|
|
|
if isinstance(item, int):
|
|
|
|
yield item
|
|
|
|
else:
|
|
|
|
yield from flatten(item)
|
|
|
|
|
|
|
|
|
2018-01-12 06:30:46 +01:00
|
|
|
def signed_char_to_int(v):
|
2018-01-15 00:58:51 +01:00
|
|
|
return int.from_bytes([v], byteorder='little', signed=True)
|
2018-01-12 06:30:46 +01:00
|
|
|
|
|
|
|
|
|
|
|
def b2hex(bs):
|
|
|
|
'''Convert bytes() to a two-letter hex string in the form "1a 2b c3"'''
|
2018-01-29 12:04:51 +01:00
|
|
|
hx = binascii.hexlify(bs).decode('ascii')
|
2018-01-12 06:30:46 +01:00
|
|
|
return ' '.join([''.join(s) for s in zip(hx[::2], hx[1::2])])
|
|
|
|
|
|
|
|
|
|
|
|
def list2hex(l):
|
|
|
|
'''Converts a list of integers to a two-letter hex string in the form
|
|
|
|
"1a 2b c3"'''
|
2018-01-29 11:38:14 +01:00
|
|
|
return ' '.join([f'{x:02x}' for x in l])
|
2018-01-12 06:30:46 +01:00
|
|
|
|
|
|
|
|
|
|
|
class NordicData(list):
|
|
|
|
def __init__(self, bs):
|
|
|
|
super().__init__(bs[2:])
|
|
|
|
self.opcode = bs[0]
|
|
|
|
self.length = bs[1]
|
|
|
|
|
2018-01-12 07:18:20 +01:00
|
|
|
|
2018-01-12 06:30:46 +01:00
|
|
|
class WacomException(Exception):
|
2018-01-24 12:06:16 +01:00
|
|
|
errno = errno.ENOSYS
|
2018-01-12 06:30:46 +01:00
|
|
|
|
|
|
|
|
|
|
|
class WacomEEAGAINException(WacomException):
|
2018-01-24 12:06:16 +01:00
|
|
|
errno = errno.EAGAIN
|
2018-01-12 06:30:46 +01:00
|
|
|
|
|
|
|
|
2018-01-17 17:01:12 +01:00
|
|
|
class WacomWrongModeException(WacomException):
|
2018-01-24 12:06:16 +01:00
|
|
|
errno = errno.EBADE
|
2018-01-17 17:01:12 +01:00
|
|
|
|
|
|
|
|
2018-02-02 00:28:26 +01:00
|
|
|
class WacomNotRegisteredException(WacomException):
|
2018-01-24 12:06:16 +01:00
|
|
|
errno = errno.EACCES
|
2018-01-12 06:30:46 +01:00
|
|
|
|
|
|
|
|
|
|
|
class WacomTimeoutException(WacomException):
|
2018-01-24 12:06:16 +01:00
|
|
|
errno = errno.ETIME
|
2018-01-12 06:30:46 +01:00
|
|
|
|
|
|
|
|
|
|
|
class WacomCorruptDataException(WacomException):
|
2018-01-24 12:06:16 +01:00
|
|
|
errno = errno.EPROTO
|
2018-01-12 06:30:46 +01:00
|
|
|
|
|
|
|
|
2018-02-08 13:33:28 +01:00
|
|
|
class WacomProtocolLowLevelComm(GObject.Object):
|
2018-01-29 12:04:51 +01:00
|
|
|
'''
|
2018-02-07 16:31:49 +01:00
|
|
|
Internal class to handle the communication with the Wacom device.
|
2018-02-08 13:33:28 +01:00
|
|
|
No-one should directly instanciate this.
|
2018-01-12 07:18:40 +01:00
|
|
|
|
2018-02-07 18:52:48 +01:00
|
|
|
|
2018-01-12 07:18:40 +01:00
|
|
|
:param device: the BlueZDevice object that is this wacom device
|
2018-01-29 12:04:51 +01:00
|
|
|
'''
|
2018-01-12 07:18:40 +01:00
|
|
|
|
2018-02-08 13:33:28 +01:00
|
|
|
def __init__(self, device):
|
2018-01-12 07:18:40 +01:00
|
|
|
GObject.Object.__init__(self)
|
2018-01-12 06:30:46 +01:00
|
|
|
self.device = device
|
2018-02-14 02:20:36 +01:00
|
|
|
self.nordic_answer = []
|
2018-01-24 11:20:03 +01:00
|
|
|
self.fw_logger = logging.getLogger('tuhi.fw')
|
2018-02-08 11:10:28 +01:00
|
|
|
|
2018-01-12 06:30:46 +01:00
|
|
|
device.connect_gatt_value(NORDIC_UART_CHRC_RX_UUID,
|
|
|
|
self._on_nordic_data_received)
|
|
|
|
|
|
|
|
def _on_nordic_data_received(self, name, value):
|
2018-01-29 12:04:51 +01:00
|
|
|
self.fw_logger.debug(f'RX Nordic <-- {list2hex(value)}')
|
2018-02-14 02:20:36 +01:00
|
|
|
self.nordic_answer += value
|
2018-01-12 06:30:46 +01:00
|
|
|
|
|
|
|
def send_nordic_command(self, command, arguments):
|
|
|
|
chrc = self.device.characteristics[NORDIC_UART_CHRC_TX_UUID]
|
|
|
|
data = [command, len(arguments), *arguments]
|
2018-01-24 11:20:03 +01:00
|
|
|
self.fw_logger.debug(f'TX Nordic --> {command:02x} / {len(arguments):02x} / {list2hex(arguments)}')
|
2018-01-12 06:30:46 +01:00
|
|
|
chrc.write_value(data)
|
|
|
|
|
|
|
|
def check_nordic_incoming(self):
|
2018-02-14 02:20:36 +01:00
|
|
|
if not self.nordic_answer:
|
2018-02-08 13:33:28 +01:00
|
|
|
raise WacomTimeoutException(f'{self.device.name}: Timeout while reading data')
|
2018-01-12 06:30:46 +01:00
|
|
|
|
|
|
|
answer = self.nordic_answer
|
|
|
|
length = answer[1]
|
|
|
|
args = answer[2:]
|
2018-02-14 02:20:36 +01:00
|
|
|
if length > len(args):
|
2018-01-29 12:04:51 +01:00
|
|
|
raise WacomException(f'error while processing answer, should get an answer of size {length} instead of {len(args)}')
|
2018-02-14 02:20:36 +01:00
|
|
|
self.nordic_answer = self.nordic_answer[length + 2:] # opcode + len
|
2018-01-12 06:30:46 +01:00
|
|
|
return NordicData(answer)
|
|
|
|
|
|
|
|
def wait_nordic_data(self, expected_opcode, timeout):
|
|
|
|
t = time.time()
|
2018-02-14 02:20:36 +01:00
|
|
|
while not self.nordic_answer and time.time() - t < timeout:
|
2018-01-12 06:30:46 +01:00
|
|
|
time.sleep(0.1)
|
|
|
|
|
|
|
|
data = self.check_nordic_incoming()
|
|
|
|
|
|
|
|
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:
|
2018-01-29 12:04:51 +01:00
|
|
|
raise WacomException(f'unexpected opcode: {data.opcode:02x}')
|
2018-01-12 06:30:46 +01:00
|
|
|
else:
|
|
|
|
if data.opcode != expected_opcode:
|
2018-01-29 12:04:51 +01:00
|
|
|
raise WacomException(f'unexpected opcode: {data.opcode:02x}')
|
2018-01-12 06:30:46 +01:00
|
|
|
|
|
|
|
return data
|
|
|
|
|
|
|
|
def check_ack(self, data):
|
|
|
|
if len(data) != 1:
|
|
|
|
str_b = binascii.hexlify(bytes(data))
|
2018-01-29 12:04:51 +01:00
|
|
|
raise WacomException(f'unexpected data: {str_b}')
|
2018-01-12 06:30:46 +01:00
|
|
|
if data[0] == 0x07:
|
2018-02-02 00:28:26 +01:00
|
|
|
raise WacomNotRegisteredException(f'wrong device, please re-register')
|
2018-01-12 06:30:46 +01:00
|
|
|
if data[0] == 0x02:
|
2018-01-29 12:04:51 +01:00
|
|
|
raise WacomEEAGAINException(f'unexpected answer: {data[0]:02x}')
|
2018-01-17 17:01:12 +01:00
|
|
|
if data[0] == 0x01:
|
2018-01-29 12:04:51 +01:00
|
|
|
raise WacomWrongModeException(f'wrong device mode')
|
2018-02-12 06:20:19 +01:00
|
|
|
if data[0] == 0x05:
|
|
|
|
raise WacomCorruptDataException(f'invalid opcode')
|
2018-01-12 06:30:46 +01:00
|
|
|
|
|
|
|
def send_nordic_command_sync(self,
|
|
|
|
command,
|
|
|
|
expected_opcode,
|
|
|
|
arguments=None):
|
|
|
|
if arguments is None:
|
|
|
|
arguments = [0x00]
|
|
|
|
|
|
|
|
self.send_nordic_command(command, arguments)
|
|
|
|
|
|
|
|
if expected_opcode is None:
|
|
|
|
return None
|
|
|
|
|
|
|
|
args = self.wait_nordic_data(expected_opcode, 5)
|
|
|
|
|
|
|
|
if expected_opcode == 0xb3: # generic ACK
|
|
|
|
self.check_ack(args)
|
|
|
|
|
|
|
|
return args
|
|
|
|
|
2018-02-08 13:33:28 +01:00
|
|
|
|
2018-02-08 15:03:57 +01:00
|
|
|
class WacomRegisterHelper(WacomProtocolLowLevelComm):
|
|
|
|
'''
|
|
|
|
Class used to register a device. This class is only useful for
|
|
|
|
the very first register commands and attempts to detect the type of
|
|
|
|
device based on the responses.
|
|
|
|
|
|
|
|
Once register_device has finished, the correct protocol is returned.
|
|
|
|
This may later be used for init_protocol() to instantiate the
|
|
|
|
right class.
|
|
|
|
'''
|
|
|
|
__gsignals__ = {
|
2018-02-12 02:32:33 +01:00
|
|
|
# Signal sent when the device requires the user to press the
|
|
|
|
# physical button
|
|
|
|
'button-press-required': (GObject.SignalFlags.RUN_FIRST, None, ()),
|
2018-02-08 15:03:57 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def is_spark(cls, device):
|
|
|
|
return MYSTERIOUS_NOTIFICATION_CHRC_UUID not in device.characteristics
|
|
|
|
|
2018-02-09 05:08:50 +01:00
|
|
|
def register_device(self, uuid):
|
|
|
|
protocol = Protocol.UNKNOWN
|
2018-02-08 15:03:57 +01:00
|
|
|
args = [int(i) for i in binascii.unhexlify(uuid)]
|
|
|
|
|
|
|
|
if self.is_spark(self.device):
|
2018-02-09 05:08:50 +01:00
|
|
|
# The spark replies with b3 01 01 when in pairing mode
|
|
|
|
# Usually that triggers a WacomWrongModeException but here it's
|
|
|
|
# expected
|
2018-02-08 15:03:57 +01:00
|
|
|
try:
|
|
|
|
self.send_nordic_command_sync(command=0xe6,
|
|
|
|
expected_opcode=0xb3,
|
|
|
|
arguments=args)
|
|
|
|
except WacomWrongModeException:
|
|
|
|
# this is expected
|
|
|
|
pass
|
2018-02-09 05:08:50 +01:00
|
|
|
|
|
|
|
# The "press button now command" on the spark
|
2018-02-08 15:03:57 +01:00
|
|
|
self.send_nordic_command(command=0xe3,
|
|
|
|
arguments=[0x01])
|
2018-02-09 05:08:50 +01:00
|
|
|
protocol = Protocol.SPARK
|
|
|
|
else:
|
|
|
|
# Slate requires a button press in response to e7 directly
|
|
|
|
self.send_nordic_command(command=0xe7, arguments=args)
|
2018-02-08 15:03:57 +01:00
|
|
|
|
|
|
|
logger.info('Press the button now to confirm')
|
|
|
|
self.emit('button-press-required')
|
|
|
|
|
2018-02-09 05:08:50 +01:00
|
|
|
# Wait for the button confirmation event, or any error
|
|
|
|
data = self.wait_nordic_data([0xe4, 0xb3], 10)
|
|
|
|
|
|
|
|
if protocol == Protocol.UNKNOWN:
|
|
|
|
if data.opcode == 0xe4:
|
|
|
|
protocol = Protocol.SLATE
|
|
|
|
else:
|
|
|
|
raise WacomException(f'unexpected opcode to register reply: {data.opcode:02x}')
|
|
|
|
|
2018-02-08 15:03:57 +01:00
|
|
|
return protocol
|
|
|
|
|
|
|
|
|
2018-02-08 13:33:28 +01:00
|
|
|
class WacomProtocolBase(WacomProtocolLowLevelComm):
|
|
|
|
'''
|
|
|
|
Internal class to handle the basic communications with the Wacom device.
|
|
|
|
No-one should directly instanciate this.
|
|
|
|
|
|
|
|
|
|
|
|
:param device: the BlueZDevice object that is this wacom device
|
|
|
|
:param uuid: the UUID {to be} assigned to the device
|
|
|
|
'''
|
|
|
|
protocol = Protocol.UNKNOWN
|
|
|
|
|
|
|
|
__gsignals__ = {
|
2018-02-12 02:32:33 +01:00
|
|
|
# Signal sent for each single drawing that becomes available. The
|
|
|
|
# drawing is the signal's argument
|
2018-02-08 13:33:28 +01:00
|
|
|
'drawing':
|
|
|
|
(GObject.SignalFlags.RUN_FIRST, None, (GObject.TYPE_PYOBJECT,)),
|
|
|
|
# battery level in %, boolean for is-charging
|
|
|
|
"battery-status":
|
|
|
|
(GObject.SignalFlags.RUN_FIRST, None, (GObject.TYPE_INT, GObject.TYPE_BOOLEAN)),
|
|
|
|
}
|
|
|
|
|
|
|
|
def __init__(self, device, uuid):
|
|
|
|
super().__init__(device)
|
|
|
|
self._uuid = uuid
|
2018-02-13 20:01:56 +01:00
|
|
|
self._timestamp = 0
|
2018-02-08 13:33:28 +01:00
|
|
|
self.pen_data_buffer = []
|
2018-02-13 20:01:56 +01:00
|
|
|
self._uhid_device = None
|
2018-02-08 13:33:28 +01:00
|
|
|
|
|
|
|
device.connect_gatt_value(WACOM_CHRC_LIVE_PEN_DATA_UUID,
|
|
|
|
self._on_pen_data_changed)
|
|
|
|
device.connect_gatt_value(WACOM_OFFLINE_CHRC_PEN_DATA_UUID,
|
|
|
|
self._on_pen_data_received)
|
|
|
|
|
|
|
|
def _on_pen_data_changed(self, name, value):
|
|
|
|
logger.debug(binascii.hexlify(bytes(value)))
|
|
|
|
|
|
|
|
if value[0] == 0x10:
|
|
|
|
pressure = int.from_bytes(value[2:4], byteorder='little')
|
|
|
|
buttons = int(value[10])
|
2018-02-13 20:01:56 +01:00
|
|
|
logger.debug(f'New Pen Data: pressure: {pressure}, button: {buttons}')
|
2018-02-08 13:33:28 +01:00
|
|
|
elif value[0] == 0xa2:
|
|
|
|
# entering proximity event
|
|
|
|
length = value[1]
|
2018-02-13 20:01:56 +01:00
|
|
|
# timestamp is now in ms
|
|
|
|
timestamp = int.from_bytes(value[4:], byteorder='little') * 5
|
|
|
|
self._timestamp = timestamp
|
|
|
|
logger.debug(f'Pen entered proximity, timestamp: {timestamp}')
|
2018-02-08 13:33:28 +01:00
|
|
|
elif value[0] == 0xa1:
|
|
|
|
# data event
|
|
|
|
length = value[1]
|
|
|
|
if length % 6 != 0:
|
|
|
|
logger.error(f'wrong data: {binascii.hexlify(bytes(value))}')
|
|
|
|
return
|
|
|
|
data = value[2:]
|
|
|
|
while data:
|
|
|
|
if bytes(data) == b'\xff\xff\xff\xff\xff\xff':
|
2018-02-13 20:01:56 +01:00
|
|
|
logger.debug(f'Pen left proximity')
|
2018-02-13 20:01:56 +01:00
|
|
|
|
|
|
|
if self._uhid_device is not None:
|
|
|
|
self._uhid_device.call_input_event([1, 0, 0, 0, 0, 0, 0, 0])
|
|
|
|
|
2018-02-08 13:33:28 +01:00
|
|
|
else:
|
|
|
|
x = int.from_bytes(data[0:2], byteorder='little')
|
|
|
|
y = int.from_bytes(data[2:4], byteorder='little')
|
|
|
|
pressure = int.from_bytes(data[4:6], byteorder='little')
|
2018-02-13 20:01:56 +01:00
|
|
|
logger.debug(f'New Pen Data: ({x},{y}), pressure: {pressure}')
|
2018-02-13 20:01:56 +01:00
|
|
|
|
|
|
|
if self._uhid_device is not None:
|
|
|
|
self._uhid_device.call_input_event([1, 1, *data[:6]])
|
|
|
|
|
2018-02-08 13:33:28 +01:00
|
|
|
data = data[6:]
|
2018-02-13 20:01:56 +01:00
|
|
|
self._timestamp += 5
|
2018-02-08 13:33:28 +01:00
|
|
|
|
|
|
|
def _on_pen_data_received(self, name, data):
|
|
|
|
self.fw_logger.debug(f'RX Pen <-- {list2hex(data)}')
|
|
|
|
self.pen_data_buffer.extend(data)
|
|
|
|
|
2018-01-12 06:30:46 +01:00
|
|
|
def check_connection(self):
|
2018-02-07 16:31:49 +01:00
|
|
|
args = [int(i) for i in binascii.unhexlify(self._uuid)]
|
2018-01-12 06:30:46 +01:00
|
|
|
self.send_nordic_command_sync(command=0xe6,
|
|
|
|
expected_opcode=0xb3,
|
|
|
|
arguments=args)
|
|
|
|
|
|
|
|
def e3_command(self):
|
|
|
|
self.send_nordic_command_sync(command=0xe3,
|
|
|
|
expected_opcode=0xb3)
|
|
|
|
|
2018-02-09 06:25:50 +01:00
|
|
|
def time_to_bytes(self):
|
2018-01-28 23:42:05 +01:00
|
|
|
# Device time is UTC
|
2018-02-08 05:11:14 +01:00
|
|
|
current_time = time.strftime('%y%m%d%H%M%S', time.gmtime())
|
2018-02-09 06:25:50 +01:00
|
|
|
return [int(i) for i in binascii.unhexlify(current_time)]
|
|
|
|
|
|
|
|
def time_from_bytes(self, data):
|
|
|
|
assert len(data) >= 6
|
|
|
|
str_timestamp = ''.join([f'{d:02x}' for d in data])
|
|
|
|
return time.strptime(str_timestamp, '%y%m%d%H%M%S')
|
|
|
|
|
|
|
|
def set_time(self):
|
|
|
|
args = self.time_to_bytes()
|
2018-01-12 06:30:46 +01:00
|
|
|
self.send_nordic_command_sync(command=0xb6,
|
|
|
|
expected_opcode=0xb3,
|
|
|
|
arguments=args)
|
|
|
|
|
|
|
|
def read_time(self):
|
|
|
|
data = self.send_nordic_command_sync(command=0xb6,
|
|
|
|
expected_opcode=0xbd)
|
2018-02-09 06:25:50 +01:00
|
|
|
ts = self.time_from_bytes(data)
|
2018-02-09 05:35:49 +01:00
|
|
|
logger.debug(f'b6 returned time: UTC {time.strftime("%y%m%d%H%M%S", ts)}')
|
2018-01-12 06:30:46 +01:00
|
|
|
# FIXME: check if data matches self.current_time
|
|
|
|
|
|
|
|
def get_battery_info(self):
|
|
|
|
data = self.send_nordic_command_sync(command=0xb9,
|
|
|
|
expected_opcode=0xba)
|
|
|
|
return int(data[0]), data[1] == 1
|
|
|
|
|
|
|
|
def get_firmware_version(self, arg):
|
|
|
|
data = self.send_nordic_command_sync(command=0xb7,
|
|
|
|
expected_opcode=0xb8,
|
|
|
|
arguments=(arg,))
|
2018-01-30 14:53:17 +01:00
|
|
|
fw = ''.join([hex(d)[2:] for d in data[1:]])
|
2018-01-12 06:30:46 +01:00
|
|
|
return fw.upper()
|
|
|
|
|
2018-02-07 18:57:38 +01:00
|
|
|
def get_name(self):
|
2018-01-12 06:30:46 +01:00
|
|
|
data = self.send_nordic_command_sync(command=0xbb,
|
|
|
|
expected_opcode=0xbc)
|
2018-02-07 18:57:38 +01:00
|
|
|
return bytes(data)
|
2018-01-12 06:30:46 +01:00
|
|
|
|
|
|
|
def get_dimensions(self, arg):
|
|
|
|
possible_args = {
|
2018-01-12 20:08:51 +01:00
|
|
|
'width': 3,
|
|
|
|
'height': 4,
|
2018-01-12 06:30:46 +01:00
|
|
|
}
|
|
|
|
args = [possible_args[arg], 0x00]
|
|
|
|
data = self.send_nordic_command_sync(command=0xea,
|
|
|
|
expected_opcode=0xeb,
|
|
|
|
arguments=args)
|
|
|
|
if len(data) != 6:
|
|
|
|
str_data = binascii.hexlify(bytes(data))
|
2018-01-12 20:01:44 +01:00
|
|
|
raise WacomCorruptDataException(f'unexpected answer for get_dimensions: {str_data}')
|
2018-01-16 10:09:54 +01:00
|
|
|
return int.from_bytes(data[2:4], byteorder='little')
|
2018-01-12 06:30:46 +01:00
|
|
|
|
|
|
|
def ec_command(self):
|
|
|
|
args = [0x06, 0x00, 0x00, 0x00, 0x00, 0x00]
|
|
|
|
self.send_nordic_command_sync(command=0xec,
|
|
|
|
expected_opcode=0xb3,
|
|
|
|
arguments=args)
|
|
|
|
|
2018-02-13 20:01:56 +01:00
|
|
|
def start_live(self, fd):
|
2018-02-13 20:01:56 +01:00
|
|
|
w = self.get_dimensions('width')
|
|
|
|
h = self.get_dimensions('height')
|
2018-01-12 06:30:46 +01:00
|
|
|
self.send_nordic_command_sync(command=0xb1,
|
|
|
|
expected_opcode=0xb3)
|
2018-02-13 20:01:56 +01:00
|
|
|
logger.debug(f'Starting wacom live mode on fd: {fd}')
|
|
|
|
|
2018-02-13 20:01:56 +01:00
|
|
|
rdesc = wacom_live_rdesc_template[:]
|
|
|
|
for i, v in enumerate(rdesc):
|
|
|
|
if v == 'width':
|
|
|
|
rdesc[i] = [0x26, list(int.to_bytes(w, 2, 'little', signed=True))]
|
|
|
|
elif v == 'height':
|
|
|
|
rdesc[i] = [0x26, list(int.to_bytes(h, 2, 'little', signed=True))]
|
|
|
|
|
2018-02-13 20:01:56 +01:00
|
|
|
uhid_device = UHIDDevice(fd)
|
2018-02-13 20:01:56 +01:00
|
|
|
uhid_device.rdesc = list(flatten(rdesc))
|
2018-02-13 20:01:56 +01:00
|
|
|
uhid_device.name = self.device.name
|
|
|
|
uhid_device.info = (5, 0x056a, 0x0001)
|
|
|
|
uhid_device.create_kernel_device()
|
|
|
|
self._uhid_device = uhid_device
|
2018-01-12 06:30:46 +01:00
|
|
|
|
|
|
|
def stop_live(self):
|
|
|
|
args = [0x02]
|
|
|
|
self.send_nordic_command_sync(command=0xb1,
|
|
|
|
expected_opcode=0xb3,
|
|
|
|
arguments=args)
|
|
|
|
|
|
|
|
def b1_command(self):
|
|
|
|
args = [0x01]
|
|
|
|
self.send_nordic_command_sync(command=0xb1,
|
|
|
|
expected_opcode=0xb3,
|
|
|
|
arguments=args)
|
|
|
|
|
|
|
|
def is_data_available(self):
|
|
|
|
data = self.send_nordic_command_sync(command=0xc1,
|
|
|
|
expected_opcode=0xc2)
|
2018-02-08 13:38:59 +01:00
|
|
|
n = int.from_bytes(data[0:2], byteorder='big')
|
2018-01-12 06:30:46 +01:00
|
|
|
logger.debug(f'Drawings available: {n}')
|
|
|
|
return n > 0
|
|
|
|
|
2018-02-07 18:52:48 +01:00
|
|
|
def get_stroke_data(self):
|
2018-02-08 13:38:59 +01:00
|
|
|
data = self.send_nordic_command_sync(command=0xc5,
|
|
|
|
expected_opcode=[0xc7, 0xcd])
|
|
|
|
# FIXME: Sometimes the 0xc7 is missing on the spark? Not in any of
|
|
|
|
# the btsnoop logs but I only rarely get a c7 response here
|
|
|
|
count = 0
|
|
|
|
if data.opcode == 0xc7:
|
|
|
|
count = int.from_bytes(data[0:4], byteorder='little')
|
|
|
|
data = self.wait_nordic_data(0xcd, 5)
|
|
|
|
# logger.debug(f'cc returned {data} ')
|
|
|
|
|
|
|
|
str_timestamp = ''.join([f'{d:02x}' for d in data])
|
2018-01-29 12:04:51 +01:00
|
|
|
timestamp = time.strptime(str_timestamp, '%y%m%d%H%M%S')
|
2018-01-12 06:30:46 +01:00
|
|
|
return count, timestamp
|
|
|
|
|
|
|
|
def start_reading(self):
|
|
|
|
data = self.send_nordic_command_sync(command=0xc3,
|
|
|
|
expected_opcode=0xc8)
|
|
|
|
if data[0] != 0xbe:
|
2018-01-29 12:04:51 +01:00
|
|
|
raise WacomException(f'unexpected answer: {data[0]:02x}')
|
2018-01-12 06:30:46 +01:00
|
|
|
|
|
|
|
def wait_for_end_read(self):
|
|
|
|
data = self.wait_nordic_data(0xc8, 5)
|
|
|
|
if data[0] != 0xed:
|
2018-01-29 12:04:51 +01:00
|
|
|
raise WacomException(f'unexpected answer: {data[0]:02x}')
|
2018-02-08 13:38:59 +01:00
|
|
|
data = self.wait_nordic_data(0xc9, 5)
|
|
|
|
crc = data
|
2018-01-12 06:30:46 +01:00
|
|
|
crc = int(binascii.hexlify(bytes(crc)), 16)
|
|
|
|
pen_data = self.pen_data_buffer
|
|
|
|
self.pen_data_buffer = []
|
|
|
|
if crc != binascii.crc32(bytes(pen_data)):
|
2018-02-08 13:38:59 +01:00
|
|
|
logger.error("CRCs don't match")
|
2018-01-12 06:30:46 +01:00
|
|
|
return pen_data
|
|
|
|
|
2018-02-08 13:38:59 +01:00
|
|
|
def retrieve_data(self):
|
|
|
|
try:
|
|
|
|
self.check_connection()
|
|
|
|
self.e3_command()
|
|
|
|
self.set_time()
|
|
|
|
battery, charging = self.get_battery_info()
|
|
|
|
if charging:
|
|
|
|
logger.debug(f'device is plugged in and charged at {battery}%')
|
|
|
|
else:
|
|
|
|
logger.debug(f'device is discharging: {battery}%')
|
|
|
|
self.emit('battery-status', battery, charging)
|
|
|
|
if self.read_offline_data() == 0:
|
|
|
|
logger.info('no data to retrieve')
|
|
|
|
except WacomEEAGAINException:
|
|
|
|
logger.warning('no data, please make sure the LED is blue and the button is pressed to switch it back to green')
|
|
|
|
|
2018-01-12 06:30:46 +01:00
|
|
|
def ack_transaction(self):
|
|
|
|
self.send_nordic_command_sync(command=0xca,
|
2018-02-08 13:38:59 +01:00
|
|
|
expected_opcode=None)
|
2018-01-12 06:30:46 +01:00
|
|
|
|
|
|
|
def next_pen_data(self, data, offset):
|
|
|
|
debug_data = []
|
|
|
|
bitmask = data[offset]
|
|
|
|
opcode = 0
|
|
|
|
offset += 1
|
2018-02-14 00:45:49 +01:00
|
|
|
debug_data.append(f'{bitmask:02x} ({bitmask:08b}) |')
|
2018-01-12 06:30:46 +01:00
|
|
|
args_length = bin(bitmask).count('1')
|
|
|
|
args = data[offset:offset + args_length]
|
|
|
|
formatted_args = []
|
|
|
|
n = 0
|
|
|
|
for i in range(2):
|
|
|
|
if (1 << i) & bitmask:
|
|
|
|
debug_data.append(f'{args[n]:02x}')
|
|
|
|
opcode |= args[n] << (i * 8)
|
|
|
|
formatted_args.append(args[n])
|
|
|
|
n += 1
|
|
|
|
else:
|
|
|
|
formatted_args.append(0)
|
|
|
|
debug_data.append(' ')
|
|
|
|
debug_data.append(f'|')
|
|
|
|
for i in range(2, 8):
|
|
|
|
if (1 << i) & bitmask:
|
|
|
|
debug_data.append(f'{args[n]:02x}')
|
|
|
|
formatted_args.append(args[n])
|
|
|
|
n += 1
|
|
|
|
else:
|
|
|
|
formatted_args.append(0)
|
|
|
|
debug_data.append(' ')
|
|
|
|
logger.debug(f'{" ".join(debug_data)}')
|
2018-01-12 19:52:22 +01:00
|
|
|
return bitmask, opcode, args, formatted_args, offset + args_length
|
2018-01-12 06:30:46 +01:00
|
|
|
|
|
|
|
def get_coordinate(self, bitmask, n, data, v, dv):
|
|
|
|
# drop the first 2 bytes as they are not valuable here
|
|
|
|
bitmask >>= 2
|
|
|
|
data = data[2:]
|
|
|
|
is_rel = False
|
|
|
|
|
|
|
|
full_coord_bitmask = 0b11 << (2 * n)
|
|
|
|
delta_coord_bitmask = 0b10 << (2 * n)
|
|
|
|
if (bitmask & full_coord_bitmask) == full_coord_bitmask:
|
2018-01-16 10:09:54 +01:00
|
|
|
v = int.from_bytes(data[2 * n:2 * n + 2], byteorder='little')
|
2018-01-12 06:30:46 +01:00
|
|
|
dv = 0
|
|
|
|
elif bitmask & delta_coord_bitmask:
|
|
|
|
dv += signed_char_to_int(data[2 * n + 1])
|
|
|
|
is_rel = True
|
|
|
|
return v, dv, is_rel
|
|
|
|
|
|
|
|
def parse_pen_data(self, data, timestamp):
|
2018-01-29 12:04:51 +01:00
|
|
|
'''
|
2018-01-15 07:02:49 +01:00
|
|
|
:param timestamp: a tuple with 9 entries, corresponding to the
|
|
|
|
local time
|
2018-01-29 12:04:51 +01:00
|
|
|
'''
|
2018-01-12 06:30:46 +01:00
|
|
|
offset = 0
|
|
|
|
x, y, p = 0, 0, 0
|
|
|
|
dx, dy, dp = 0, 0, 0
|
|
|
|
|
2018-01-28 23:42:05 +01:00
|
|
|
timestamp = int(calendar.timegm(timestamp))
|
2018-01-12 06:30:46 +01:00
|
|
|
drawings = []
|
|
|
|
drawing = None
|
|
|
|
stroke = None
|
|
|
|
while offset < len(data):
|
2018-01-12 19:52:22 +01:00
|
|
|
bitmask, opcode, raw_args, args, offset = self.next_pen_data(data, offset)
|
2018-01-12 06:30:46 +01:00
|
|
|
if opcode == 0x3800:
|
|
|
|
logger.info(f'beginning of sequence')
|
2018-02-08 13:33:28 +01:00
|
|
|
drawing = Drawing(self.device.name, (self.width, self.height), timestamp)
|
2018-01-12 06:30:46 +01:00
|
|
|
drawings.append(drawing)
|
|
|
|
continue
|
|
|
|
elif opcode == 0xeeff:
|
|
|
|
# some sort of headers
|
2018-01-15 00:57:16 +01:00
|
|
|
time_offset = int.from_bytes(raw_args[4:], byteorder='little')
|
2018-01-12 19:52:22 +01:00
|
|
|
logger.info(f'time offset since boot: {time_offset * 0.005} secs')
|
2018-01-24 06:12:03 +01:00
|
|
|
stroke = drawing.new_stroke()
|
2018-01-12 06:30:46 +01:00
|
|
|
continue
|
|
|
|
if bytes(args) == b'\xff\xff\xff\xff\xff\xff\xff\xff':
|
|
|
|
logger.info(f'end of sequence')
|
|
|
|
continue
|
|
|
|
if bytes(args) == b'\x00\x00\xff\xff\xff\xff\xff\xff':
|
|
|
|
logger.info(f'end of stroke')
|
|
|
|
stroke = None
|
|
|
|
continue
|
|
|
|
|
|
|
|
if stroke is None:
|
2018-01-24 06:12:03 +01:00
|
|
|
stroke = drawing.new_stroke()
|
2018-01-12 06:30:46 +01:00
|
|
|
|
2018-01-12 20:08:51 +01:00
|
|
|
x, dx, xrel = self.get_coordinate(bitmask, 0, args, x, dx)
|
|
|
|
y, dy, yrel = self.get_coordinate(bitmask, 1, args, y, dy)
|
2018-01-12 06:30:46 +01:00
|
|
|
p, dp, prel = self.get_coordinate(bitmask, 2, args, p, dp)
|
|
|
|
|
|
|
|
x += dx
|
|
|
|
y += dy
|
|
|
|
p += dp
|
|
|
|
|
2018-01-12 20:01:44 +01:00
|
|
|
logger.info(f'point at {x},{y} ({dx:+}, {dy:+}) with pressure {p} ({dp:+})')
|
2018-01-12 06:30:46 +01:00
|
|
|
|
|
|
|
if bitmask & 0b00111100 == 0:
|
|
|
|
continue
|
|
|
|
if xrel or yrel or prel:
|
2018-01-24 06:12:03 +01:00
|
|
|
stroke.new_rel((dx, dy), dp)
|
2018-01-12 06:30:46 +01:00
|
|
|
else:
|
2018-01-24 06:12:03 +01:00
|
|
|
stroke.new_abs((x, y), p)
|
2018-01-12 06:30:46 +01:00
|
|
|
|
|
|
|
return drawings
|
|
|
|
|
|
|
|
def read_offline_data(self):
|
|
|
|
self.b1_command()
|
|
|
|
transaction_count = 0
|
|
|
|
while self.is_data_available():
|
|
|
|
count, timestamp = self.get_stroke_data()
|
2018-02-09 06:25:50 +01:00
|
|
|
logger.info(f'receiving {count} bytes drawn on UTC {time.strftime("%y%m%d%H%M%S", timestamp)}')
|
2018-01-12 06:30:46 +01:00
|
|
|
self.start_reading()
|
|
|
|
pen_data = self.wait_for_end_read()
|
|
|
|
str_pen = binascii.hexlify(bytes(pen_data))
|
2018-01-29 12:04:51 +01:00
|
|
|
logger.info(f'received {str_pen}')
|
2018-01-12 06:30:46 +01:00
|
|
|
prefix = pen_data[:4]
|
|
|
|
# not sure if we really need this check
|
|
|
|
# note: \x38\x62\x74 translates to '8bt'
|
|
|
|
if bytes(prefix) == b'\x62\x38\x62\x74':
|
|
|
|
drawings = self.parse_pen_data(pen_data, timestamp)
|
2018-01-12 07:18:40 +01:00
|
|
|
for drawing in drawings:
|
|
|
|
self.emit('drawing', drawing)
|
2018-01-12 06:30:46 +01:00
|
|
|
self.ack_transaction()
|
|
|
|
transaction_count += 1
|
|
|
|
return transaction_count
|
|
|
|
|
2018-02-13 01:32:19 +01:00
|
|
|
def set_name(self, name):
|
|
|
|
# On the Spark, the name needs a trailing linebreak, otherwise the
|
|
|
|
# firmware gets confused.
|
|
|
|
args = [ord(c) for c in name] + [0x0a]
|
|
|
|
data = self.send_nordic_command_sync(command=0xbb,
|
|
|
|
arguments=args,
|
|
|
|
expected_opcode=0xb3)
|
|
|
|
return bytes(data)
|
|
|
|
|
2018-02-08 15:03:57 +01:00
|
|
|
def register_device_finish(self):
|
2018-02-08 13:38:59 +01:00
|
|
|
self.send_nordic_command_sync(command=0xe5,
|
|
|
|
arguments=None,
|
|
|
|
expected_opcode=0xb3)
|
2018-01-17 17:01:12 +01:00
|
|
|
self.set_time()
|
|
|
|
self.read_time()
|
2018-02-07 18:57:38 +01:00
|
|
|
name = self.get_name()
|
|
|
|
logger.info(f'device name is {name}')
|
2018-01-17 17:01:12 +01:00
|
|
|
fw_high = self.get_firmware_version(0)
|
|
|
|
fw_low = self.get_firmware_version(1)
|
|
|
|
logger.info(f'firmware is {fw_high}-{fw_low}')
|
|
|
|
|
2018-02-13 20:01:56 +01:00
|
|
|
def live_mode(self, mode, uhid):
|
|
|
|
try:
|
|
|
|
if mode:
|
|
|
|
self.check_connection()
|
|
|
|
self.start_live(uhid)
|
|
|
|
else:
|
|
|
|
self.stop_live()
|
|
|
|
except WacomEEAGAINException:
|
|
|
|
logger.warning("no data, please make sure the LED is blue and the button is pressed to switch it back to green")
|
|
|
|
|
2018-02-07 18:52:48 +01:00
|
|
|
|
2018-02-08 13:38:59 +01:00
|
|
|
class WacomProtocolSpark(WacomProtocolBase):
|
2018-02-07 18:52:48 +01:00
|
|
|
'''
|
2018-02-08 13:38:59 +01:00
|
|
|
Subclass to handle the communication oddities with the Wacom Spark-like
|
2018-02-07 18:52:48 +01:00
|
|
|
devices.
|
|
|
|
|
|
|
|
:param device: the BlueZDevice object that is this wacom device
|
2018-02-08 13:38:59 +01:00
|
|
|
:param uuid: the UUID {to be} assigned to the device
|
2018-02-07 18:52:48 +01:00
|
|
|
'''
|
|
|
|
width = 21600
|
|
|
|
height = 14800
|
2018-02-08 13:38:59 +01:00
|
|
|
protocol = Protocol.SPARK
|
2018-02-07 18:52:48 +01:00
|
|
|
|
|
|
|
|
2018-02-08 13:38:59 +01:00
|
|
|
class WacomProtocolSlate(WacomProtocolSpark):
|
2018-02-07 18:52:48 +01:00
|
|
|
'''
|
2018-02-08 13:38:59 +01:00
|
|
|
Subclass to handle the communication oddities with the Wacom Slate-like
|
2018-02-07 18:52:48 +01:00
|
|
|
devices.
|
|
|
|
|
|
|
|
:param device: the BlueZDevice object that is this wacom device
|
2018-02-08 13:38:59 +01:00
|
|
|
:param uuid: the UUID {to be} assigned to the device
|
2018-02-07 18:52:48 +01:00
|
|
|
'''
|
|
|
|
width = 21600
|
|
|
|
height = 14800
|
2018-02-08 13:38:59 +01:00
|
|
|
protocol = Protocol.SLATE
|
|
|
|
|
|
|
|
def __init__(self, device, uuid):
|
2018-02-09 05:13:27 +01:00
|
|
|
super().__init__(device, uuid)
|
2018-02-08 13:38:59 +01:00
|
|
|
device.connect_gatt_value(MYSTERIOUS_NOTIFICATION_CHRC_UUID,
|
|
|
|
self._on_mysterious_data_received)
|
|
|
|
|
|
|
|
def _on_mysterious_data_received(self, name, value):
|
|
|
|
self.fw_logger.debug(f'mysterious: {binascii.hexlify(bytes(value))}')
|
|
|
|
|
|
|
|
def ack_transaction(self):
|
|
|
|
self.send_nordic_command_sync(command=0xca,
|
|
|
|
expected_opcode=0xb3)
|
2018-02-07 18:52:48 +01:00
|
|
|
|
|
|
|
def is_data_available(self):
|
|
|
|
data = self.send_nordic_command_sync(command=0xc1,
|
|
|
|
expected_opcode=0xc2)
|
2018-02-08 13:38:59 +01:00
|
|
|
n = int.from_bytes(data[0:2], byteorder='little')
|
2018-02-07 18:52:48 +01:00
|
|
|
logger.debug(f'Drawings available: {n}')
|
|
|
|
return n > 0
|
|
|
|
|
|
|
|
def get_stroke_data(self):
|
2018-02-08 13:38:59 +01:00
|
|
|
data = self.send_nordic_command_sync(command=0xcc,
|
|
|
|
expected_opcode=0xcf)
|
|
|
|
# logger.debug(f'cc returned {data} ')
|
|
|
|
count = int.from_bytes(data[0:4], byteorder='little')
|
2018-02-09 06:25:50 +01:00
|
|
|
timestamp = self.time_from_bytes(data[4:])
|
2018-02-07 18:52:48 +01:00
|
|
|
return count, timestamp
|
|
|
|
|
2018-02-08 15:03:57 +01:00
|
|
|
def register_device_finish(self):
|
2018-02-08 13:38:59 +01:00
|
|
|
self.set_time()
|
|
|
|
self.read_time()
|
|
|
|
self.ec_command()
|
|
|
|
name = self.get_name()
|
|
|
|
logger.info(f'device name is {name}')
|
|
|
|
w = self.get_dimensions('width')
|
|
|
|
h = self.get_dimensions('height')
|
|
|
|
if self.width != w or self.height != h:
|
2018-02-09 07:13:38 +01:00
|
|
|
logger.error(f'incompatible dimensions: {w}x{h}')
|
2018-02-08 13:38:59 +01:00
|
|
|
fw_high = self.get_firmware_version(0)
|
|
|
|
fw_low = self.get_firmware_version(1)
|
|
|
|
logger.info(f'firmware is {fw_high}-{fw_low}')
|
2018-02-07 18:52:48 +01:00
|
|
|
|
|
|
|
def retrieve_data(self):
|
|
|
|
try:
|
|
|
|
self.check_connection()
|
|
|
|
self.set_time()
|
|
|
|
battery, charging = self.get_battery_info()
|
|
|
|
if charging:
|
|
|
|
logger.debug(f'device is plugged in and charged at {battery}%')
|
|
|
|
else:
|
|
|
|
logger.debug(f'device is discharging: {battery}%')
|
|
|
|
self.emit('battery-status', battery, charging)
|
2018-02-08 13:38:59 +01:00
|
|
|
self.width = w = self.get_dimensions('width')
|
|
|
|
self.height = h = self.get_dimensions('height')
|
|
|
|
logger.debug(f'dimensions: {w}x{h}')
|
|
|
|
|
|
|
|
fw_high = self.get_firmware_version(0)
|
|
|
|
fw_low = self.get_firmware_version(1)
|
|
|
|
logger.debug(f'firmware is {fw_high}-{fw_low}')
|
|
|
|
self.ec_command()
|
2018-02-07 18:52:48 +01:00
|
|
|
if self.read_offline_data() == 0:
|
|
|
|
logger.info('no data to retrieve')
|
|
|
|
except WacomEEAGAINException:
|
|
|
|
logger.warning('no data, please make sure the LED is blue and the button is pressed to switch it back to green')
|
|
|
|
|
2018-02-08 13:38:59 +01:00
|
|
|
def wait_for_end_read(self):
|
|
|
|
data = self.wait_nordic_data(0xc8, 5)
|
|
|
|
if data[0] != 0xed:
|
|
|
|
raise WacomException(f'unexpected answer: {data[0]:02x}')
|
|
|
|
crc = data[1:]
|
|
|
|
crc.reverse()
|
|
|
|
crc = int(binascii.hexlify(bytes(crc)), 16)
|
|
|
|
pen_data = self.pen_data_buffer
|
|
|
|
self.pen_data_buffer = []
|
|
|
|
if crc != binascii.crc32(bytes(pen_data)):
|
|
|
|
raise WacomCorruptDataException("CRCs don't match")
|
|
|
|
return pen_data
|
2018-01-17 17:01:12 +01:00
|
|
|
|
2018-02-07 16:31:49 +01:00
|
|
|
|
|
|
|
class WacomDevice(GObject.Object):
|
|
|
|
'''
|
|
|
|
Class to communicate with the Wacom device. Communication is handled in
|
|
|
|
a separate thread.
|
|
|
|
|
|
|
|
:param device: the BlueZDevice object that is this wacom device
|
|
|
|
'''
|
|
|
|
|
|
|
|
__gsignals__ = {
|
2018-02-12 02:32:33 +01:00
|
|
|
# Signal sent for each single drawing that becomes available. The
|
|
|
|
# drawing is the signal's argument
|
2018-02-07 16:31:49 +01:00
|
|
|
'drawing':
|
|
|
|
(GObject.SignalFlags.RUN_FIRST, None, (GObject.TYPE_PYOBJECT,)),
|
2018-02-12 02:32:33 +01:00
|
|
|
# Signal sent when a device connection (register or listen) is
|
|
|
|
# complete. Carries the exception object or None on success
|
2018-02-07 16:31:49 +01:00
|
|
|
'done':
|
|
|
|
(GObject.SignalFlags.RUN_FIRST, None, (GObject.TYPE_PYOBJECT, )),
|
2018-02-12 02:32:33 +01:00
|
|
|
# Signal sent when the device requires the user to press the
|
|
|
|
# physical button'''
|
2018-02-07 16:31:49 +01:00
|
|
|
'button-press-required':
|
|
|
|
(GObject.SignalFlags.RUN_FIRST, None, ()),
|
|
|
|
# battery level in %, boolean for is-charging
|
|
|
|
"battery-status":
|
|
|
|
(GObject.SignalFlags.RUN_FIRST, None, (GObject.TYPE_INT, GObject.TYPE_BOOLEAN)),
|
|
|
|
}
|
|
|
|
|
|
|
|
def __init__(self, device, config=None):
|
|
|
|
GObject.Object.__init__(self)
|
|
|
|
self._device = device
|
|
|
|
self.thread = None
|
|
|
|
self._is_running = False
|
|
|
|
self._config = None
|
2018-02-08 15:03:57 +01:00
|
|
|
self._wacom_protocol = None
|
2018-02-07 16:31:49 +01:00
|
|
|
|
|
|
|
try:
|
|
|
|
self._config = config.devices[device.address]
|
|
|
|
except KeyError:
|
|
|
|
# unregistered device
|
|
|
|
self._uuid = None
|
|
|
|
else:
|
|
|
|
self._uuid = self._config['uuid']
|
2018-02-07 18:52:48 +01:00
|
|
|
|
2018-02-08 15:03:57 +01:00
|
|
|
# retrieve the protocol from the config file
|
|
|
|
protocol = Protocol.UNKNOWN
|
2018-02-07 16:31:49 +01:00
|
|
|
try:
|
2018-02-07 18:52:48 +01:00
|
|
|
protocol = next(p for p in Protocol if p.value == self._config['Protocol'])
|
2018-02-07 16:31:49 +01:00
|
|
|
except StopIteration:
|
|
|
|
logger.error(f'Unknown protocol in configuration: {self._config["Protocol"]}')
|
|
|
|
raise WacomCorruptDataException(f'Unknown Protocol {self._config["Protocol"]}')
|
|
|
|
|
2018-02-08 15:03:57 +01:00
|
|
|
if protocol == Protocol.UNKNOWN:
|
|
|
|
raise WacomCorruptDataException(f'Missing Protocol entry from config file. Please delete config file and re-register device')
|
|
|
|
|
|
|
|
self._init_protocol(protocol)
|
2018-02-07 18:52:48 +01:00
|
|
|
|
2018-02-08 15:03:57 +01:00
|
|
|
def _init_protocol(self, protocol):
|
2018-02-07 18:52:48 +01:00
|
|
|
if protocol == Protocol.SPARK:
|
|
|
|
self._wacom_protocol = WacomProtocolSpark(self._device, self._uuid)
|
|
|
|
elif protocol == Protocol.SLATE:
|
|
|
|
self._wacom_protocol = WacomProtocolSlate(self._device, self._uuid)
|
|
|
|
else:
|
|
|
|
# FIXME: change to an assert once intuos-pro is implemented, we
|
|
|
|
# never get here
|
|
|
|
logger.error(f'Unknown Protocol {protocol}')
|
|
|
|
raise WacomCorruptDataException(f'Protocol "{protocol}" not implemented')
|
|
|
|
|
2018-02-09 05:35:49 +01:00
|
|
|
logger.debug(f'{self._device.name} is using protocol {protocol}')
|
2018-02-07 18:52:48 +01:00
|
|
|
|
2018-02-12 02:42:05 +01:00
|
|
|
self._wacom_protocol.connect(
|
|
|
|
'drawing',
|
|
|
|
lambda protocol, drawing, self: self.emit('drawing', drawing),
|
|
|
|
self)
|
|
|
|
self._wacom_protocol.connect(
|
|
|
|
'battery-status',
|
|
|
|
lambda prot, percent, is_charging, self: self.emit('battery-status', percent, is_charging),
|
|
|
|
self)
|
2018-02-07 16:31:49 +01:00
|
|
|
|
|
|
|
@GObject.Property
|
|
|
|
def uuid(self):
|
|
|
|
assert self._uuid is not None
|
|
|
|
return self._uuid
|
|
|
|
|
|
|
|
@GObject.Property
|
|
|
|
def protocol(self):
|
2018-02-07 18:52:48 +01:00
|
|
|
assert self._wacom_protocol is not None
|
|
|
|
return self._wacom_protocol.protocol
|
2018-02-07 16:31:49 +01:00
|
|
|
|
2018-02-02 00:28:26 +01:00
|
|
|
def register_device(self):
|
2018-01-19 07:04:05 +01:00
|
|
|
self._uuid = uuid.uuid4().hex[:12]
|
2018-02-07 16:31:49 +01:00
|
|
|
logger.debug(f'{self._device.address}: registering device, assigned {self.uuid}')
|
2018-02-08 15:03:57 +01:00
|
|
|
|
|
|
|
wp = WacomRegisterHelper(self._device)
|
2018-02-12 02:42:05 +01:00
|
|
|
s = wp.connect('button-press-required',
|
|
|
|
lambda protocol, self: self.emit('button-press-required'),
|
|
|
|
self)
|
2018-02-08 15:03:57 +01:00
|
|
|
protocol = wp.register_device(self._uuid)
|
|
|
|
wp.disconnect(s)
|
|
|
|
del wp
|
|
|
|
|
|
|
|
self._init_protocol(protocol)
|
|
|
|
self._wacom_protocol.register_device_finish()
|
|
|
|
|
2018-02-02 00:28:26 +01:00
|
|
|
logger.info('registration completed')
|
2018-01-19 07:04:05 +01:00
|
|
|
self.notify('uuid')
|
2018-01-17 17:01:12 +01:00
|
|
|
|
2018-02-11 11:35:07 +01:00
|
|
|
def _run(self, *args, **kwargs):
|
2018-01-15 15:16:51 +01:00
|
|
|
if self._is_running:
|
2018-02-07 16:31:49 +01:00
|
|
|
logger.error(f'{self._device.address}: already synching, ignoring this request')
|
2018-01-15 15:16:51 +01:00
|
|
|
return
|
|
|
|
|
2018-02-11 11:35:07 +01:00
|
|
|
mode = args[0]
|
|
|
|
|
2018-02-07 16:31:49 +01:00
|
|
|
logger.debug(f'{self._device.address}: starting')
|
2018-01-15 15:16:51 +01:00
|
|
|
self._is_running = True
|
2018-01-24 12:06:16 +01:00
|
|
|
exception = None
|
2018-01-15 15:17:43 +01:00
|
|
|
try:
|
2018-02-13 20:01:56 +01:00
|
|
|
if mode == DeviceMode.LIVE:
|
|
|
|
assert self._wacom_protocol is not None
|
|
|
|
self._wacom_protocol.live_mode(args[1], args[2])
|
|
|
|
elif mode == DeviceMode.REGISTER:
|
2018-02-02 00:28:26 +01:00
|
|
|
self.register_device()
|
2018-01-17 17:01:12 +01:00
|
|
|
else:
|
2018-02-07 16:31:49 +01:00
|
|
|
assert self._wacom_protocol is not None
|
|
|
|
self._wacom_protocol.retrieve_data()
|
2018-01-24 12:06:16 +01:00
|
|
|
except WacomException as e:
|
|
|
|
logger.error(f'**** Exception: {e} ****')
|
|
|
|
exception = e
|
2018-01-15 15:17:43 +01:00
|
|
|
finally:
|
2018-01-15 15:16:51 +01:00
|
|
|
self._is_running = False
|
2018-01-29 12:04:51 +01:00
|
|
|
self.emit('done', exception)
|
2018-01-12 06:30:46 +01:00
|
|
|
|
2018-02-11 11:35:07 +01:00
|
|
|
def start_listen(self):
|
|
|
|
self.thread = threading.Thread(target=self._run, args=(DeviceMode.LISTEN,))
|
|
|
|
self.thread.start()
|
|
|
|
|
2018-02-13 20:01:56 +01:00
|
|
|
def start_live(self, uhid_fd):
|
|
|
|
self.thread = threading.Thread(target=self._run, args=(DeviceMode.LIVE, True, uhid_fd))
|
|
|
|
self.thread.start()
|
|
|
|
|
|
|
|
def stop_live(self):
|
|
|
|
self.thread = threading.Thread(target=self._run, args=(DeviceMode.LIVE, False, -1))
|
|
|
|
self.thread.start()
|
|
|
|
|
2018-02-11 11:35:07 +01:00
|
|
|
def start_register(self):
|
|
|
|
self.thread = threading.Thread(target=self._run, args=(DeviceMode.REGISTER,))
|
2018-01-12 06:30:46 +01:00
|
|
|
self.thread.start()
|