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-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
|
2019-08-19 03:59:02 +02:00
|
|
|
from pathlib import Path
|
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
|
2019-08-02 07:50:10 +02:00
|
|
|
import tuhi.protocol
|
2019-08-22 03:51:21 +02:00
|
|
|
from tuhi.protocol import NordicData, Interactions, Mode, ProtocolVersion, StrokeFile, UnexpectedDataError, DeviceError, MissingReplyError, AuthorizationError
|
2019-08-14 11:08:30 +02:00
|
|
|
from .util import list2hex, flatten
|
2019-08-21 05:19:54 +02:00
|
|
|
from tuhi.config import TuhiConfig
|
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
|
|
|
|
2019-07-31 06:12:36 +02:00
|
|
|
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
|
2018-01-30 14:06:29 +01:00
|
|
|
|
2018-01-12 20:14:26 +01:00
|
|
|
|
2019-08-23 03:36:19 +02:00
|
|
|
class IDGenerator(object):
|
|
|
|
_session = uuid.uuid4().hex
|
|
|
|
_instance = 0
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def current(cls):
|
|
|
|
return f'{cls._session}-{cls._instance}'
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def next(cls):
|
|
|
|
cls._instance += 1
|
|
|
|
return cls.current()
|
|
|
|
|
|
|
|
|
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
|
2018-02-15 11:44:14 +01:00
|
|
|
0x09, 0x01, # Usage (Digitizer) 2
|
2018-02-13 20:01:56 +01:00
|
|
|
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
|
2018-02-15 12:35:59 +01:00
|
|
|
0x05, 0x01, # ..Usage Page (Generic Desktop) 28
|
|
|
|
0x09, 0x30, # ..Usage (X) 30
|
|
|
|
0x75, 0x10, # ..Report Size (16) 32
|
|
|
|
0x95, 0x01, # ..Report Count (1) 34
|
|
|
|
0x55, 0x0d, # ..Unit Exponent (-3) 36
|
|
|
|
0x65, 0x11, # ..Unit (Centimeter,SILinear) 38
|
|
|
|
0x37, 'x_min', # ..Physical Minimum (TBD) 40
|
|
|
|
0x47, 'x_max', # ..Physical Maximum (TBD) 45
|
|
|
|
0x17, 'x_min', # ..Logical Minimum (TBD) 50
|
|
|
|
0x27, 'x_max', # ..Logical Maximum (TBD) 55
|
|
|
|
0x81, 0x02, # ..Input (Data,Var,Abs) 60
|
|
|
|
0x09, 0x31, # ..Usage (Y) 62
|
|
|
|
0x37, 'y_min', # ..Physical Minimum (TBD) 64
|
|
|
|
0x47, 'y_max', # ..Physical Maximum (TBD) 69
|
|
|
|
0x17, 'y_min', # ..Logical Minimum (TBD) 74
|
|
|
|
0x27, 'y_max', # ..Logical Maximum (TBD) 79
|
|
|
|
0x81, 0x02, # ..Input (Data,Var,Abs) 84
|
|
|
|
0x05, 0x0d, # ..Usage Page (Digitizers) 86
|
|
|
|
0x15, 0x00, # ..Logical Minimum (0) 88
|
|
|
|
0x09, 0x30, # ..Usage (Tip Pressure) 90
|
|
|
|
0x27, 'pressure', # ..Logical Maximum (TBD) 92
|
|
|
|
0x81, 0x02, # ..Input (Data,Var,Abs) 97
|
|
|
|
0xc0, # .End Collection 99
|
|
|
|
0xc0, # End Collection 100
|
2018-02-13 20:01:56 +01:00
|
|
|
]
|
|
|
|
|
|
|
|
|
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])])
|
|
|
|
|
|
|
|
|
2019-07-14 11:07:32 +02:00
|
|
|
def list2hexlist(l):
|
|
|
|
'''Converts a list of integers to a two-letter prefixed hex string in the form
|
|
|
|
"[0x1a, 0x32, 0xab]"'''
|
|
|
|
return '[' + ', '.join([f'{x:#04x}' for x in l]) + ']'
|
|
|
|
|
|
|
|
|
|
|
|
class DataLogger(object):
|
|
|
|
'''
|
|
|
|
A wrapper to log data transfer between the device and Tuhi. Use as::
|
|
|
|
|
|
|
|
logger = DataLogger()
|
wacom: use 'with' to group request/replies together
Previously, the YAML output was basically:
- send: [0xb9, 0x01, 0x00]
- recv: [0xba, 0x02, 0x44, 0x00]
using a context manager where replies are expeced we can now group the
replies in the yaml file:
with logger as _:
self.send_nordic_command(...)
self.wait_nordic_data(...)
And that will prodice a grouped entry in the YAML file:
- send: [0xb9, 0x01, 0x00]
recv: [0xba, 0x02, 0x44, 0x00]
Which means processing those becomes a bit easier.
Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2019-08-23 06:51:42 +02:00
|
|
|
with logger as _:
|
|
|
|
logger.nordic.request(nordic_data)
|
|
|
|
logger.nordic.recv([1, 2, 3...])
|
2019-07-14 11:07:32 +02:00
|
|
|
|
|
|
|
This uses a logger for stdout, but it also writes the log files to disk
|
wacom: use 'with' to group request/replies together
Previously, the YAML output was basically:
- send: [0xb9, 0x01, 0x00]
- recv: [0xba, 0x02, 0x44, 0x00]
using a context manager where replies are expeced we can now group the
replies in the yaml file:
with logger as _:
self.send_nordic_command(...)
self.wait_nordic_data(...)
And that will prodice a grouped entry in the YAML file:
- send: [0xb9, 0x01, 0x00]
recv: [0xba, 0x02, 0x44, 0x00]
Which means processing those becomes a bit easier.
Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2019-08-23 06:51:42 +02:00
|
|
|
for future re-use. The context manager ('with') helps to group the
|
|
|
|
requests/replies together in the yaml file.
|
2019-07-14 11:07:32 +02:00
|
|
|
|
2019-07-31 06:15:38 +02:00
|
|
|
Targets for log are $HOME/.share/tuhi/12:AB:23:CD:.../<timestamp>.yml
|
|
|
|
|
2019-07-14 11:07:32 +02:00
|
|
|
'''
|
|
|
|
class _Nordic(object):
|
|
|
|
source = 'NORDIC'
|
|
|
|
|
|
|
|
def __init__(self, parent):
|
|
|
|
self.parent = parent
|
|
|
|
|
|
|
|
def recv(self, data):
|
|
|
|
return self.parent._recv(self.source, data)
|
|
|
|
|
2019-08-23 05:35:37 +02:00
|
|
|
def request(self, request):
|
|
|
|
return self.parent._request(self.source, request)
|
2019-07-14 11:07:32 +02:00
|
|
|
|
|
|
|
class _Pen(object):
|
|
|
|
source = 'PEN'
|
|
|
|
|
|
|
|
def __init__(self, parent):
|
|
|
|
self.parent = parent
|
|
|
|
|
|
|
|
def recv(self, data):
|
|
|
|
return self.parent._recv(self.source, data)
|
|
|
|
|
2019-07-31 06:12:36 +02:00
|
|
|
class _SysEvent(object):
|
|
|
|
source = 'SYSEVENT'
|
2019-07-14 11:07:32 +02:00
|
|
|
|
|
|
|
def __init__(self, parent):
|
|
|
|
self.parent = parent
|
|
|
|
|
|
|
|
def recv(self, data):
|
|
|
|
return self.parent._recv(self.source, data)
|
|
|
|
|
|
|
|
def __init__(self, bluez_device):
|
|
|
|
self.logger = logging.getLogger('tuhi.fw')
|
|
|
|
self.device = bluez_device
|
|
|
|
self.btaddr = bluez_device.address
|
2019-08-22 08:45:48 +02:00
|
|
|
self.logdir = Path(TuhiConfig().log_dir, self.btaddr, 'raw')
|
2019-08-21 04:54:47 +02:00
|
|
|
self.logdir.mkdir(parents=True, exist_ok=True)
|
2019-07-14 11:07:32 +02:00
|
|
|
|
|
|
|
bluez_device.connect('connected', self._on_bluez_connected)
|
|
|
|
bluez_device.connect('disconnected', self._on_bluez_disconnected)
|
|
|
|
|
|
|
|
self.nordic = DataLogger._Nordic(self)
|
|
|
|
self.pen = DataLogger._Pen(self)
|
2019-07-31 06:12:36 +02:00
|
|
|
self.sysevent = DataLogger._SysEvent(self)
|
2019-07-14 11:07:32 +02:00
|
|
|
self.logfile = None
|
wacom: use 'with' to group request/replies together
Previously, the YAML output was basically:
- send: [0xb9, 0x01, 0x00]
- recv: [0xba, 0x02, 0x44, 0x00]
using a context manager where replies are expeced we can now group the
replies in the yaml file:
with logger as _:
self.send_nordic_command(...)
self.wait_nordic_data(...)
And that will prodice a grouped entry in the YAML file:
- send: [0xb9, 0x01, 0x00]
recv: [0xba, 0x02, 0x44, 0x00]
Which means processing those becomes a bit easier.
Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2019-08-23 06:51:42 +02:00
|
|
|
self._in_context = True
|
|
|
|
self._last_source = None
|
2019-07-14 11:07:32 +02:00
|
|
|
|
|
|
|
def _on_bluez_connected(self, bluez_device):
|
|
|
|
self._init_file()
|
|
|
|
|
|
|
|
def _on_bluez_disconnected(self, bluez_device):
|
|
|
|
self._close_file()
|
|
|
|
|
|
|
|
def _init_file(self):
|
|
|
|
if self.logfile is not None:
|
|
|
|
return
|
|
|
|
|
2019-08-15 06:41:40 +02:00
|
|
|
timestamp = int(time.time())
|
|
|
|
t = time.strftime('%Y-%m-%d-%H:%M:%S')
|
|
|
|
fname = f'log-{timestamp}-{t}.yaml'
|
2019-08-19 03:59:02 +02:00
|
|
|
path = Path(self.logdir, fname)
|
2019-07-14 11:07:32 +02:00
|
|
|
self.logfile = open(path, 'w+')
|
|
|
|
|
2019-08-23 03:36:19 +02:00
|
|
|
session_id = IDGenerator.next()
|
|
|
|
self.logger.debug(f'sessionid: {session_id}')
|
|
|
|
self.logfile.write(f'sessionid: {session_id}\n')
|
2019-07-14 11:07:32 +02:00
|
|
|
self.logfile.write(f'name: {self.device.name}\n')
|
|
|
|
self.logfile.write(f'bluetooth: {self.btaddr}\n')
|
2019-08-15 06:41:40 +02:00
|
|
|
self.logfile.write(f'time: {timestamp} # host time: {time.strftime("%Y-%m-%d %H:%M:%S")}\n')
|
2019-07-14 11:07:32 +02:00
|
|
|
self.logfile.write(f'data:\n')
|
|
|
|
|
|
|
|
def _close_file(self):
|
|
|
|
if self.logfile is None:
|
|
|
|
return
|
|
|
|
|
|
|
|
self.logfile.write(f'# closed at {time.strftime("%Y-%m-%d %H:%M:%S")}')
|
|
|
|
self.logfile.close()
|
|
|
|
self.logfile = None
|
|
|
|
|
|
|
|
def _recv(self, source, data):
|
|
|
|
if source in ['NORDIC', 'PEN']:
|
2019-08-02 02:57:21 +02:00
|
|
|
def _convert(values):
|
|
|
|
return list2hex(values)
|
2019-07-14 11:07:32 +02:00
|
|
|
convert = _convert
|
|
|
|
else:
|
2019-08-02 02:57:21 +02:00
|
|
|
def _convert(values):
|
|
|
|
return binascii.hexlify(bytes(values))
|
2019-07-14 11:07:32 +02:00
|
|
|
convert = _convert
|
|
|
|
|
|
|
|
self.logger.debug(f'{self.btaddr}: RX {source} <-- {convert(data)}')
|
|
|
|
self._init_file()
|
wacom: use 'with' to group request/replies together
Previously, the YAML output was basically:
- send: [0xb9, 0x01, 0x00]
- recv: [0xba, 0x02, 0x44, 0x00]
using a context manager where replies are expeced we can now group the
replies in the yaml file:
with logger as _:
self.send_nordic_command(...)
self.wait_nordic_data(...)
And that will prodice a grouped entry in the YAML file:
- send: [0xb9, 0x01, 0x00]
recv: [0xba, 0x02, 0x44, 0x00]
Which means processing those becomes a bit easier.
Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2019-08-23 06:51:42 +02:00
|
|
|
|
|
|
|
# If we're inside a context, group the request/reply together in the
|
|
|
|
# yaml file, unless the source changes. This means that for the
|
|
|
|
# majority of requests we get an entry like this:
|
|
|
|
#
|
|
|
|
# # GET_BATTERY
|
|
|
|
# - send: [0xb9, 0x01, 0x00]
|
|
|
|
# recv: [0xba, 0x02, 0x44, 0x00]
|
|
|
|
#
|
|
|
|
# Which makes YAML processing a lot easier.
|
|
|
|
if self._last_source != source:
|
|
|
|
self._in_context = False
|
|
|
|
self._last_source = source
|
|
|
|
self.logfile.write(f'# resetting source to {source}\n')
|
|
|
|
prefix = ' ' if self._in_context else ' -'
|
|
|
|
|
|
|
|
self.logfile.write(f'{prefix} recv: {list2hexlist(data)}\n')
|
2019-07-14 11:07:32 +02:00
|
|
|
if source != 'NORDIC':
|
2019-08-15 02:01:11 +02:00
|
|
|
self.logfile.write(f' source: {source}\n')
|
2019-07-14 11:07:32 +02:00
|
|
|
|
2019-08-23 05:35:37 +02:00
|
|
|
def _request(self, source, request):
|
2019-08-23 05:38:18 +02:00
|
|
|
if request.name:
|
|
|
|
self.logger.debug(f'command: {request.name}')
|
2019-08-23 05:35:37 +02:00
|
|
|
self.logger.debug(f'{self.btaddr}: TX {source} --> {request.opcode:02x} / {len(request):02x} / {list2hex(request)}')
|
2019-07-14 11:07:32 +02:00
|
|
|
|
|
|
|
self._init_file()
|
2019-08-23 05:38:18 +02:00
|
|
|
if request.name:
|
|
|
|
self.logfile.write(f'# {request.name}\n')
|
2019-08-23 05:35:37 +02:00
|
|
|
|
|
|
|
data = [request.opcode, len(request), *request]
|
2019-07-14 11:07:32 +02:00
|
|
|
self.logfile.write(f' - send: {list2hexlist(data)}\n')
|
|
|
|
if source != 'NORDIC':
|
2019-08-15 02:01:11 +02:00
|
|
|
self.logfile.write(f' source: {source}\n')
|
2019-07-14 11:07:32 +02:00
|
|
|
|
wacom: use 'with' to group request/replies together
Previously, the YAML output was basically:
- send: [0xb9, 0x01, 0x00]
- recv: [0xba, 0x02, 0x44, 0x00]
using a context manager where replies are expeced we can now group the
replies in the yaml file:
with logger as _:
self.send_nordic_command(...)
self.wait_nordic_data(...)
And that will prodice a grouped entry in the YAML file:
- send: [0xb9, 0x01, 0x00]
recv: [0xba, 0x02, 0x44, 0x00]
Which means processing those becomes a bit easier.
Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2019-08-23 06:51:42 +02:00
|
|
|
def __enter__(self):
|
|
|
|
self._in_context = True
|
|
|
|
return self
|
|
|
|
|
|
|
|
def __exit__(self, *args, **kwargs):
|
|
|
|
self._in_context = False
|
|
|
|
|
2019-07-14 11:07:32 +02:00
|
|
|
|
2018-02-15 08:16:10 +01:00
|
|
|
class WacomPacket(GObject.Object):
|
|
|
|
'''
|
|
|
|
A single protocol packet of variable length. The protocol format is a
|
|
|
|
single-byte bitmask followed by up to 8 bytes (depending on the number
|
|
|
|
of 1-bits in the bitmask). Each byte represents the matching bit in the
|
|
|
|
bitmask, i.e. the data is non-sparse.
|
|
|
|
|
|
|
|
If the bitmask has 0x1 and/or 0x2 set, those two bytes make up the
|
|
|
|
opcode of the command. So the possible layouts are:
|
|
|
|
|
|
|
|
| bitmask | opcode1 | opcode2 | payload ...
|
|
|
|
| bitmask | opcode1 | payload ...
|
|
|
|
| bitmask | opcode2 | payload ...
|
|
|
|
| bitmask | payload
|
|
|
|
|
|
|
|
On most normal packets containing motion data, the opcode is not
|
|
|
|
present.
|
|
|
|
|
|
|
|
Attributes:
|
|
|
|
bitmask .. single byte with a bitmask denoting the contents
|
|
|
|
opcode ... the 16-bit opcode or None for 'special' packets. Note that
|
|
|
|
the opcode is converted into an integer from the
|
|
|
|
little-endian protocol format
|
|
|
|
bytes .... a list of the payload bytes as sent by the device. This is
|
|
|
|
a non-sparse list matching the number of set bits in the
|
|
|
|
bitmask. it does not include the bitmask.
|
|
|
|
args ..... a sparse list of the payload bytes, expanded to match the
|
|
|
|
bitmask so that args[x] is the value for each bit x in
|
|
|
|
bitmask. it does not include the bitmask.
|
|
|
|
length ... length of the packet in bytes, including bitmask
|
|
|
|
'''
|
|
|
|
def __init__(self, data):
|
|
|
|
self.bitmask = data[0]
|
|
|
|
nbytes = bin(self.bitmask).count('1')
|
|
|
|
self.bytes = data[1:1 + nbytes]
|
|
|
|
self.length = nbytes + 1 # for the bitmask
|
|
|
|
|
|
|
|
idx = 0
|
|
|
|
# 2-byte opcode, but only if the bitmask is set for either byte
|
|
|
|
opcode = 0
|
|
|
|
if self.bitmask & 0x1:
|
|
|
|
opcode |= self.bytes[idx]
|
|
|
|
idx += 1
|
|
|
|
if self.bitmask & 0x2:
|
|
|
|
opcode |= self.bytes[idx] << 8
|
|
|
|
idx += 1
|
|
|
|
|
|
|
|
self.opcode = opcode if opcode else None
|
|
|
|
|
|
|
|
self.args = []
|
|
|
|
vals = self.bytes.copy()
|
|
|
|
mask = self.bitmask
|
|
|
|
while mask != 0:
|
|
|
|
self.args.append(vals.pop(0) if mask & 0x1 else 0x00)
|
|
|
|
mask >>= 1
|
|
|
|
|
|
|
|
def __repr__(self):
|
|
|
|
debug_data = []
|
|
|
|
debug_data.append(f'{self.bitmask:02x} ({self.bitmask:08b}) |')
|
|
|
|
if self.opcode:
|
|
|
|
debug_data.append(f'{self.opcode:04x} |')
|
|
|
|
else:
|
|
|
|
debug_data.append(f' |')
|
|
|
|
|
|
|
|
for i in range(2, 8): # start at 2 to skip the opcode
|
|
|
|
if self.bitmask & (1 << i):
|
|
|
|
debug_data.append(f'{self.args[i]:02x}')
|
|
|
|
else:
|
|
|
|
debug_data.append(' ')
|
|
|
|
return " ".join(debug_data)
|
|
|
|
|
|
|
|
|
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.
|
2019-07-31 06:15:38 +02:00
|
|
|
No-one should directly instanciate this, use the device-specific
|
|
|
|
subclass instead (e.g. WacomProtocolIntuosPro).
|
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 = []
|
2019-07-14 11:07:32 +02:00
|
|
|
self.fw_logger = DataLogger(device)
|
2018-02-15 16:19:54 +01:00
|
|
|
self.nordic_event = threading.Semaphore(value=0)
|
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):
|
2019-07-14 11:07:32 +02:00
|
|
|
self.fw_logger.nordic.recv(value)
|
2018-02-14 02:20:36 +01:00
|
|
|
self.nordic_answer += value
|
2018-02-15 16:19:54 +01:00
|
|
|
self.nordic_event.release()
|
2018-01-12 06:30:46 +01:00
|
|
|
|
2019-08-23 05:35:37 +02:00
|
|
|
def send_nordic_command(self, request):
|
2018-01-12 06:30:46 +01:00
|
|
|
chrc = self.device.characteristics[NORDIC_UART_CHRC_TX_UUID]
|
2019-08-23 05:35:37 +02:00
|
|
|
self.fw_logger.nordic.request(request)
|
|
|
|
|
|
|
|
data = [request.opcode, len(request), *request]
|
2018-01-12 06:30:46 +01:00
|
|
|
chrc.write_value(data)
|
|
|
|
|
2019-07-31 07:58:14 +02:00
|
|
|
def pop_next_message(self):
|
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):
|
2019-08-22 00:34:46 +02:00
|
|
|
raise UnexpectedDataError(answer, f'Invalid answer message length: expected {length}, got {len(args)}')
|
2018-02-14 02:20:36 +01:00
|
|
|
self.nordic_answer = self.nordic_answer[length + 2:] # opcode + len
|
2019-08-02 03:09:02 +02:00
|
|
|
return NordicData(answer[:length + 2])
|
2018-01-12 06:30:46 +01:00
|
|
|
|
2019-08-01 07:29:33 +02:00
|
|
|
# The callback used by the protocol messages
|
2019-08-09 06:01:02 +02:00
|
|
|
def nordic_data_exchange(self, request, requires_reply=False,
|
|
|
|
userdata=None, timeout=None):
|
wacom: use 'with' to group request/replies together
Previously, the YAML output was basically:
- send: [0xb9, 0x01, 0x00]
- recv: [0xba, 0x02, 0x44, 0x00]
using a context manager where replies are expeced we can now group the
replies in the yaml file:
with logger as _:
self.send_nordic_command(...)
self.wait_nordic_data(...)
And that will prodice a grouped entry in the YAML file:
- send: [0xb9, 0x01, 0x00]
recv: [0xba, 0x02, 0x44, 0x00]
Which means processing those becomes a bit easier.
Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2019-08-23 06:51:42 +02:00
|
|
|
with self.fw_logger as _:
|
|
|
|
if request is not None:
|
|
|
|
self.send_nordic_command(request)
|
|
|
|
if requires_reply:
|
|
|
|
if not self.nordic_event.acquire(timeout=timeout or 5):
|
|
|
|
return None
|
|
|
|
return self.pop_next_message()
|
2019-08-01 07:29:33 +02:00
|
|
|
|
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):
|
2019-07-31 06:12:36 +02:00
|
|
|
return SYSEVENT_NOTIFICATION_CHRC_UUID not in device.characteristics
|
2018-02-08 15:03:57 +01:00
|
|
|
|
2018-02-09 05:08:50 +01:00
|
|
|
def register_device(self, uuid):
|
2018-02-08 15:03:57 +01:00
|
|
|
if self.is_spark(self.device):
|
2019-08-09 06:01:37 +02:00
|
|
|
self.p = tuhi.protocol.Protocol(ProtocolVersion.SPARK, self.nordic_data_exchange)
|
2018-02-09 05:08:50 +01:00
|
|
|
# The spark replies with b3 01 01 when in pairing mode
|
2019-08-21 08:29:48 +02:00
|
|
|
# Usually that triggers a DeviceError but here it's
|
2018-02-09 05:08:50 +01:00
|
|
|
# expected
|
2018-02-08 15:03:57 +01:00
|
|
|
try:
|
2019-08-09 06:01:37 +02:00
|
|
|
self.p.execute(Interactions.CONNECT, uuid)
|
2019-08-22 03:51:38 +02:00
|
|
|
except AuthorizationError:
|
|
|
|
# this is expected
|
|
|
|
pass
|
2018-02-09 05:08:50 +01:00
|
|
|
|
|
|
|
# The "press button now command" on the spark
|
2019-08-09 06:01:37 +02:00
|
|
|
self.p.execute(Interactions.REGISTER_PRESS_BUTTON)
|
2018-02-09 05:08:50 +01:00
|
|
|
else:
|
2019-08-09 06:01:37 +02:00
|
|
|
# Default to Slate for now, it will handle the IntuosPro too
|
|
|
|
self.p = tuhi.protocol.Protocol(ProtocolVersion.SLATE, self.nordic_data_exchange)
|
|
|
|
self.p.execute(Interactions.REGISTER_PRESS_BUTTON, uuid)
|
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
|
2019-08-09 06:01:37 +02:00
|
|
|
protocol_version = self.p.execute(Interactions.REGISTER_WAIT_FOR_BUTTON).protocol_version
|
2018-02-09 05:08:50 +01:00
|
|
|
|
2019-08-09 06:01:37 +02:00
|
|
|
if protocol_version == ProtocolVersion.ANY:
|
2019-08-22 00:36:14 +02:00
|
|
|
raise tuhi.protocol.ProtocolError(f'Unknown protocol version: {protocol_version}')
|
2019-08-09 06:01:37 +02:00
|
|
|
|
2019-08-09 06:27:54 +02:00
|
|
|
return protocol_version
|
2018-02-08 15:03:57 +01:00
|
|
|
|
|
|
|
|
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
|
|
|
|
'''
|
2019-08-09 06:27:54 +02:00
|
|
|
protocol = ProtocolVersion.ANY
|
2018-02-08 13:33:28 +01:00
|
|
|
|
|
|
|
__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)),
|
|
|
|
}
|
|
|
|
|
2019-08-02 07:50:10 +02:00
|
|
|
def __init__(self, device, uuid, protocol_version=ProtocolVersion.ANY):
|
2018-02-08 13:33:28 +01:00
|
|
|
super().__init__(device)
|
2019-08-02 07:50:10 +02:00
|
|
|
self.p = tuhi.protocol.Protocol(protocol_version, self.nordic_data_exchange)
|
|
|
|
|
2018-02-08 13:33:28 +01:00
|
|
|
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-15 17:08:20 +01:00
|
|
|
self._last_pen_data_time = time.time() - 5 # initialize this in the past
|
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)
|
|
|
|
|
2019-08-29 01:34:08 +02:00
|
|
|
@GObject.Property
|
2019-07-15 02:27:57 +02:00
|
|
|
def dimensions(self):
|
|
|
|
return (self.width, self.height)
|
|
|
|
|
2018-02-08 13:33:28 +01:00
|
|
|
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):
|
2019-07-14 11:07:32 +02:00
|
|
|
self.fw_logger.pen.recv(data)
|
2018-02-08 13:33:28 +01:00
|
|
|
self.pen_data_buffer.extend(data)
|
2018-02-15 17:08:20 +01:00
|
|
|
self._last_pen_data_time = time.time()
|
2018-02-08 13:33:28 +01:00
|
|
|
|
2018-01-12 06:30:46 +01:00
|
|
|
def check_connection(self):
|
2019-08-02 07:50:10 +02:00
|
|
|
self.p.execute(Interactions.CONNECT, self._uuid)
|
2018-01-12 06:30:46 +01:00
|
|
|
|
|
|
|
def e3_command(self):
|
2019-08-02 07:50:10 +02:00
|
|
|
self.p.execute(Interactions.UNKNOWN_E3)
|
2018-01-12 06:30:46 +01:00
|
|
|
|
2018-02-16 04:00:38 +01:00
|
|
|
@classmethod
|
2018-02-09 06:25:50 +01:00
|
|
|
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):
|
2019-08-02 07:50:10 +02:00
|
|
|
self.p.execute(Interactions.SET_TIME, time.time())
|
2018-01-12 06:30:46 +01:00
|
|
|
|
|
|
|
def read_time(self):
|
2019-08-02 07:50:10 +02:00
|
|
|
ts = self.p.execute(Interactions.GET_TIME).timestamp
|
|
|
|
t = time.gmtime(ts)
|
|
|
|
logger.debug(f'device time: UTC {time.strftime("%y%m%d%H%M%S", t)}')
|
2018-02-15 06:26:20 +01:00
|
|
|
|
2019-08-02 07:50:10 +02:00
|
|
|
tdelta = time.mktime(time.gmtime()) - time.mktime(t)
|
2018-02-15 06:26:20 +01:00
|
|
|
if abs(tdelta) > 300:
|
|
|
|
logger.error(f'device time is out by more than 5 minutes')
|
2018-01-12 06:30:46 +01:00
|
|
|
|
|
|
|
def get_battery_info(self):
|
2019-08-02 07:50:10 +02:00
|
|
|
msg = self.p.execute(Interactions.GET_BATTERY)
|
|
|
|
logger.info(f'device battery: {msg.battery_percent}% ({"dis" if not msg.battery_is_charging else ""}charging)')
|
|
|
|
return msg.battery_percent, msg.battery_is_charging
|
2018-01-12 06:30:46 +01:00
|
|
|
|
2019-07-17 10:24:42 +02:00
|
|
|
def get_firmware_version(self):
|
2019-08-02 07:50:10 +02:00
|
|
|
fw = self.p.execute(Interactions.GET_FIRMWARE).firmware
|
2019-07-15 02:57:24 +02:00
|
|
|
logger.info(f'firmware is {fw}')
|
|
|
|
return fw
|
2018-01-12 06:30:46 +01:00
|
|
|
|
2018-02-07 18:57:38 +01:00
|
|
|
def get_name(self):
|
2019-08-02 07:50:10 +02:00
|
|
|
name = self.p.execute(Interactions.GET_NAME).name
|
2019-07-15 02:57:24 +02:00
|
|
|
logger.info(f'device name is {name}')
|
|
|
|
return name
|
2018-01-12 06:30:46 +01:00
|
|
|
|
2019-08-12 12:49:22 +02:00
|
|
|
def update_dimensions(self):
|
2019-08-12 11:56:31 +02:00
|
|
|
w = self.p.execute(Interactions.GET_WIDTH).width
|
|
|
|
h = self.p.execute(Interactions.GET_HEIGHT).height
|
2019-08-12 12:49:22 +02:00
|
|
|
ps = self.p.execute(Interactions.GET_POINT_SIZE).point_size
|
|
|
|
logger.info(f'dimensions: {w}x{h}, point size {ps}µm')
|
|
|
|
self.point_size = ps
|
|
|
|
self.width = w * ps
|
|
|
|
self.height = h * ps
|
|
|
|
self.notify('dimensions')
|
2019-08-12 11:56:31 +02:00
|
|
|
return w, h
|
2018-01-12 06:30:46 +01:00
|
|
|
|
2019-08-12 06:10:54 +02:00
|
|
|
def select_transfer_gatt(self):
|
|
|
|
self.p.execute(Interactions.SET_FILE_TRANSFER_REPORTING_TYPE)
|
2018-01-12 06:30:46 +01:00
|
|
|
|
2018-02-13 20:01:56 +01:00
|
|
|
def start_live(self, fd):
|
2019-08-02 07:50:10 +02:00
|
|
|
self.p.execute(Interactions.SET_MODE, Mode.LIVE)
|
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):
|
2018-02-15 12:33:50 +01:00
|
|
|
if isinstance(v, str):
|
|
|
|
v = getattr(self, v)
|
|
|
|
rdesc[i] = list(int.to_bytes(v, 4, 'little', signed=True))
|
2018-02-13 20:01:56 +01:00
|
|
|
|
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):
|
2019-08-02 07:50:10 +02:00
|
|
|
self.p.execute(Interactions.SET_MODE, Mode.IDLE)
|
2018-01-12 06:30:46 +01:00
|
|
|
|
2019-08-12 06:01:15 +02:00
|
|
|
def set_paper_mode(self):
|
2019-08-02 07:50:10 +02:00
|
|
|
self.p.execute(Interactions.SET_MODE, Mode.PAPER).execute()
|
2018-01-12 06:30:46 +01:00
|
|
|
|
2019-08-21 04:59:16 +02:00
|
|
|
def count_available_files(self):
|
|
|
|
n = self.p.execute(Interactions.AVAILABLE_FILES_COUNT).count
|
2018-01-12 06:30:46 +01:00
|
|
|
logger.debug(f'Drawings available: {n}')
|
2019-08-21 04:59:16 +02:00
|
|
|
return n
|
2018-01-12 06:30:46 +01:00
|
|
|
|
2018-02-07 18:52:48 +01:00
|
|
|
def get_stroke_data(self):
|
2019-08-02 07:50:10 +02:00
|
|
|
msg = self.p.execute(Interactions.GET_STROKES)
|
|
|
|
# logger.debug(f'cc returned {data} ')
|
|
|
|
return msg.count, msg.timestamp
|
2018-01-12 06:30:46 +01:00
|
|
|
|
2019-08-21 04:59:16 +02:00
|
|
|
def start_downloading_oldest_file(self):
|
|
|
|
self.p.execute(Interactions.DOWNLOAD_OLDEST_FILE)
|
2018-01-12 06:30:46 +01:00
|
|
|
|
2019-08-21 12:36:54 +02:00
|
|
|
def wait_for_end_read(self, timeout=5):
|
|
|
|
msg = None
|
|
|
|
while True:
|
|
|
|
try:
|
|
|
|
msg = self.p.execute(Interactions.WAIT_FOR_END_READ)
|
|
|
|
break
|
2019-08-22 00:30:48 +02:00
|
|
|
except MissingReplyError as e:
|
2019-08-21 12:36:54 +02:00
|
|
|
# if we're still reading pen data, try again
|
|
|
|
if time.time() - self._last_pen_data_time > timeout:
|
|
|
|
raise e
|
|
|
|
|
2018-01-12 06:30:46 +01:00
|
|
|
pen_data = self.pen_data_buffer
|
|
|
|
self.pen_data_buffer = []
|
2019-08-21 12:36:54 +02:00
|
|
|
if msg.crc != binascii.crc32(bytes(pen_data)):
|
|
|
|
raise UnexpectedDataError(pen_data, message='CRCs do not 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()
|
|
|
|
self.emit('battery-status', battery, charging)
|
2019-08-12 12:49:22 +02:00
|
|
|
self.update_dimensions()
|
2019-08-21 06:30:38 +02:00
|
|
|
if not self.read_offline_data():
|
2018-02-08 13:38:59 +01:00
|
|
|
logger.info('no data to retrieve')
|
2019-08-21 13:01:15 +02:00
|
|
|
except DeviceError as e:
|
|
|
|
if e.errorcode == DeviceError.ErrorCode.INVALID_STATE:
|
|
|
|
logger.warning('no data, please make sure the LED is blue and the button is pressed to switch it back to green')
|
|
|
|
else:
|
|
|
|
raise e
|
2018-02-08 13:38:59 +01:00
|
|
|
|
2019-08-21 04:59:16 +02:00
|
|
|
def delete_oldest_file(self):
|
|
|
|
self.p.execute(Interactions.DELETE_OLDEST_FILE)
|
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
|
|
|
|
|
2018-02-14 00:48:02 +01:00
|
|
|
def parse_pen_data_prefix(self, data):
|
2019-08-14 04:10:27 +02:00
|
|
|
file_format = b'\x62\x38\x62\x74'
|
2018-02-14 00:48:02 +01:00
|
|
|
prefix = data[:4]
|
2018-02-14 01:04:02 +01:00
|
|
|
offset = len(prefix)
|
2019-08-14 04:10:27 +02:00
|
|
|
if bytes(prefix) != file_format:
|
|
|
|
logger.debug(f'Unsupported file format {prefix} (require {file_format})')
|
2018-02-14 01:04:02 +01:00
|
|
|
return False, 0
|
2018-02-14 00:48:02 +01:00
|
|
|
|
2018-02-14 01:04:02 +01:00
|
|
|
return True, offset
|
2018-02-14 00:48:02 +01:00
|
|
|
|
2018-01-12 06:30:46 +01:00
|
|
|
def parse_pen_data(self, data, timestamp):
|
2018-01-29 12:04:51 +01:00
|
|
|
'''
|
2019-08-09 03:17:02 +02:00
|
|
|
:param timestamp: seconds since UNIX epoch
|
2018-01-29 12:04:51 +01:00
|
|
|
'''
|
2018-02-14 00:48:02 +01:00
|
|
|
|
2019-08-20 04:18:45 +02:00
|
|
|
f = StrokeFile(data)
|
2018-02-14 01:04:02 +01:00
|
|
|
drawing = Drawing(self.device.name, (self.width, self.height), timestamp)
|
2019-08-23 03:36:19 +02:00
|
|
|
drawing.session_id = IDGenerator.current()
|
2019-08-12 12:49:22 +02:00
|
|
|
ps = self.point_size
|
2018-02-14 01:04:02 +01:00
|
|
|
|
2019-08-20 04:18:45 +02:00
|
|
|
def normalize(p):
|
|
|
|
NORMALIZED_RANGE = 0x10000
|
|
|
|
return NORMALIZED_RANGE * p / self.pressure
|
2018-01-12 06:30:46 +01:00
|
|
|
|
2019-08-20 04:18:45 +02:00
|
|
|
for s in f.strokes:
|
|
|
|
stroke = drawing.new_stroke()
|
|
|
|
for p in s.points:
|
|
|
|
stroke.new_abs((p.x * ps, p.y * ps), normalize(p.p))
|
|
|
|
stroke.seal()
|
2018-02-14 03:41:34 +01:00
|
|
|
drawing.seal()
|
2018-02-14 04:06:56 +01:00
|
|
|
return drawing
|
2018-01-12 06:30:46 +01:00
|
|
|
|
|
|
|
def read_offline_data(self):
|
2019-08-12 06:01:15 +02:00
|
|
|
self.set_paper_mode()
|
2019-08-21 05:19:54 +02:00
|
|
|
file_count = self.count_available_files()
|
2019-08-21 06:30:38 +02:00
|
|
|
rc = file_count > 0
|
2019-08-21 05:19:54 +02:00
|
|
|
while file_count > 0:
|
2018-01-12 06:30:46 +01:00
|
|
|
count, timestamp = self.get_stroke_data()
|
2019-08-09 03:17:02 +02:00
|
|
|
logger.info(f'receiving {count} bytes drawn on UTC {time.strftime("%y%m%d%H%M%S", time.gmtime(timestamp))}')
|
2019-08-21 04:59:16 +02:00
|
|
|
self.start_downloading_oldest_file()
|
2018-01-12 06:30:46 +01:00
|
|
|
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}')
|
2019-08-09 03:17:02 +02:00
|
|
|
drawing = self.parse_pen_data(pen_data, timestamp)
|
2018-02-14 04:06:56 +01:00
|
|
|
if drawing:
|
2018-02-14 00:48:02 +01:00
|
|
|
self.emit('drawing', drawing)
|
2019-08-21 05:19:54 +02:00
|
|
|
file_count -= 1
|
|
|
|
if TuhiConfig().peek_at_drawing:
|
|
|
|
logger.info(f'Not deleting drawing from device')
|
|
|
|
if file_count > 0:
|
|
|
|
logger.info(f'{file_count} more files on device but I can only download the oldest one')
|
|
|
|
break
|
2019-08-21 04:59:16 +02:00
|
|
|
self.delete_oldest_file()
|
2019-08-21 06:30:38 +02:00
|
|
|
return rc
|
2018-01-12 06:30:46 +01:00
|
|
|
|
2018-02-13 01:32:19 +01:00
|
|
|
def set_name(self, name):
|
2019-08-02 07:50:10 +02:00
|
|
|
self.p.execute(Interactions.SET_NAME, name)
|
2018-02-13 01:32:19 +01:00
|
|
|
|
2018-02-08 15:03:57 +01:00
|
|
|
def register_device_finish(self):
|
2019-08-02 07:50:10 +02:00
|
|
|
self.p.execute(Interactions.REGISTER_COMPLETE)
|
2018-01-17 17:01:12 +01:00
|
|
|
self.set_time()
|
|
|
|
self.read_time()
|
2019-08-02 02:57:21 +02:00
|
|
|
self.get_name()
|
2019-07-15 02:57:24 +02:00
|
|
|
self.get_firmware_version()
|
2019-08-12 12:49:22 +02:00
|
|
|
self.update_dimensions()
|
2018-01-17 17:01:12 +01:00
|
|
|
|
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()
|
2019-08-21 13:01:15 +02:00
|
|
|
except DeviceError as e:
|
|
|
|
if e.errorcode == DeviceError.ErrorCode.INVALID_STATE:
|
|
|
|
logger.warning("no data, please make sure the LED is blue and the button is pressed to switch it back to green")
|
|
|
|
else:
|
|
|
|
raise e
|
2018-02-13 20:01:56 +01:00
|
|
|
|
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
|
|
|
'''
|
2019-08-14 01:29:36 +02:00
|
|
|
width = 21000 # physical: 210mm
|
|
|
|
x_min = 2100
|
|
|
|
x_max = 21000
|
2018-02-15 12:35:59 +01:00
|
|
|
|
2019-08-14 01:29:36 +02:00
|
|
|
height = 14800 # physical: 148mm
|
|
|
|
y_min = 0
|
|
|
|
y_max = 14800
|
2018-02-15 12:35:59 +01:00
|
|
|
|
2019-08-14 01:29:36 +02:00
|
|
|
pressure = 1023
|
2019-08-23 05:00:53 +02:00
|
|
|
point_size = 10
|
2019-08-09 06:27:54 +02:00
|
|
|
protocol = ProtocolVersion.SPARK
|
2018-02-07 18:52:48 +01:00
|
|
|
|
2019-08-21 01:11:19 +02:00
|
|
|
orientation = 'portrait'
|
|
|
|
|
2019-08-02 07:50:10 +02:00
|
|
|
def __init__(self, device, uuid, protocol_version=ProtocolVersion.SPARK):
|
|
|
|
assert(protocol_version >= ProtocolVersion.SPARK)
|
|
|
|
super().__init__(device, uuid, protocol_version=protocol_version)
|
|
|
|
|
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
|
2018-02-15 12:35:59 +01:00
|
|
|
x_min = 2500
|
|
|
|
x_max = 20600
|
|
|
|
|
2018-02-07 18:52:48 +01:00
|
|
|
height = 14800
|
2018-02-15 12:35:59 +01:00
|
|
|
y_min = 500
|
|
|
|
y_max = 14300
|
|
|
|
|
2018-02-15 11:51:51 +01:00
|
|
|
pressure = 2047
|
2019-08-23 05:00:53 +02:00
|
|
|
point_size = 10
|
2019-08-09 06:27:54 +02:00
|
|
|
protocol = ProtocolVersion.SLATE
|
2018-02-08 13:38:59 +01:00
|
|
|
|
2019-08-21 01:11:19 +02:00
|
|
|
orientation = 'portrait'
|
|
|
|
|
2019-08-02 07:50:10 +02:00
|
|
|
def __init__(self, device, uuid, protocol_version=ProtocolVersion.SLATE):
|
|
|
|
assert(protocol_version >= ProtocolVersion.SLATE)
|
|
|
|
super().__init__(device, uuid, protocol_version=protocol_version)
|
2019-07-31 06:12:36 +02:00
|
|
|
device.connect_gatt_value(SYSEVENT_NOTIFICATION_CHRC_UUID,
|
|
|
|
self._on_sysevent_data_received)
|
2018-02-08 13:38:59 +01:00
|
|
|
|
2019-05-02 15:03:09 +02:00
|
|
|
def live_mode(self, mode, uhid):
|
2019-08-29 04:15:08 +02:00
|
|
|
if mode:
|
|
|
|
# Slate tablet has two models A5 and A4
|
|
|
|
# Here, we read real tablet dimensions before
|
|
|
|
# starting live mode
|
|
|
|
self.update_dimensions()
|
|
|
|
self.x_max = self.width - 1000
|
|
|
|
self.y_max = self.height - 500
|
2019-05-02 15:03:09 +02:00
|
|
|
|
|
|
|
return super().live_mode(mode, uhid)
|
|
|
|
|
2019-07-31 06:12:36 +02:00
|
|
|
def _on_sysevent_data_received(self, name, value):
|
|
|
|
self.fw_logger.sysevent.recv(value)
|
2018-02-08 13:38:59 +01:00
|
|
|
|
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()
|
2019-08-12 06:10:54 +02:00
|
|
|
self.select_transfer_gatt()
|
2019-08-02 02:57:21 +02:00
|
|
|
self.get_name()
|
2019-08-12 12:49:22 +02:00
|
|
|
self.update_dimensions()
|
2019-07-15 02:27:57 +02:00
|
|
|
self.notify('dimensions')
|
|
|
|
|
2019-07-15 02:57:24 +02:00
|
|
|
self.get_firmware_version()
|
2019-07-14 10:08:03 +02:00
|
|
|
battery, charging = self.get_battery_info()
|
|
|
|
self.emit('battery-status', battery, charging)
|
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()
|
|
|
|
self.emit('battery-status', battery, charging)
|
2019-08-12 12:49:22 +02:00
|
|
|
self.update_dimensions()
|
2019-07-15 02:27:57 +02:00
|
|
|
self.notify('dimensions')
|
2018-02-08 13:38:59 +01:00
|
|
|
|
2019-08-02 02:57:21 +02:00
|
|
|
self.get_firmware_version()
|
2019-08-12 06:10:54 +02:00
|
|
|
self.select_transfer_gatt()
|
2019-08-21 06:30:38 +02:00
|
|
|
if not self.read_offline_data():
|
2018-02-07 18:52:48 +01:00
|
|
|
logger.info('no data to retrieve')
|
2019-08-21 13:01:15 +02:00
|
|
|
except DeviceError as e:
|
|
|
|
if e.errorcode == DeviceError.ErrorCode.INVALID_STATE:
|
|
|
|
logger.warning('no data, please make sure the LED is blue and the button is pressed to switch it back to green')
|
|
|
|
else:
|
|
|
|
raise e
|
2018-02-07 18:52:48 +01:00
|
|
|
|
2018-02-07 16:31:49 +01:00
|
|
|
|
2018-02-07 06:31:01 +01:00
|
|
|
class WacomProtocolIntuosPro(WacomProtocolSlate):
|
|
|
|
'''
|
|
|
|
Subclass to handle the communication oddities with the Wacom
|
|
|
|
IntuosPro-like devices.
|
|
|
|
|
|
|
|
:param device: the BlueZDevice object that is this wacom device
|
|
|
|
:param uuid: the UUID {to be} assigned to the device
|
|
|
|
'''
|
|
|
|
width = 44800
|
2018-02-15 12:35:59 +01:00
|
|
|
x_min = 0
|
|
|
|
x_max = 44800
|
|
|
|
|
2018-02-07 06:31:01 +01:00
|
|
|
height = 29600
|
2018-02-15 12:35:59 +01:00
|
|
|
y_min = 0
|
|
|
|
y_max = 29600
|
|
|
|
|
2019-08-14 02:16:27 +02:00
|
|
|
pressure = 8192
|
2019-08-23 05:00:53 +02:00
|
|
|
point_size = 5
|
2019-08-09 06:27:54 +02:00
|
|
|
protocol = ProtocolVersion.INTUOS_PRO
|
2018-02-07 06:31:01 +01:00
|
|
|
|
2019-08-21 01:11:19 +02:00
|
|
|
orientation = 'landscape'
|
|
|
|
|
2019-08-02 07:50:10 +02:00
|
|
|
def __init__(self, device, uuid, protocol_version=ProtocolVersion.INTUOS_PRO):
|
|
|
|
assert(protocol_version >= ProtocolVersion.INTUOS_PRO)
|
|
|
|
super().__init__(device, uuid, protocol_version=protocol_version)
|
2018-02-07 06:31:01 +01:00
|
|
|
|
2018-02-16 04:00:38 +01:00
|
|
|
@classmethod
|
2018-02-07 06:31:01 +01:00
|
|
|
def time_from_bytes(self, data):
|
|
|
|
seconds = int.from_bytes(data[0:4], byteorder='little')
|
2019-07-15 08:04:44 +02:00
|
|
|
return time.gmtime(seconds)
|
2018-02-07 06:31:01 +01:00
|
|
|
|
|
|
|
def parse_pen_data_prefix(self, data):
|
2019-08-14 04:10:27 +02:00
|
|
|
file_format = b'\x67\x82\x69\x65'
|
2018-02-07 06:31:01 +01:00
|
|
|
prefix = data[:4]
|
2019-08-14 04:10:27 +02:00
|
|
|
if bytes(prefix) != file_format:
|
|
|
|
logger.debug(f'Unsupported file format {prefix} (require {file_format})')
|
2018-02-07 06:31:01 +01:00
|
|
|
return False, 0
|
|
|
|
|
|
|
|
# This is the time the button was pressed after drawing, i.e. the
|
|
|
|
# end of the drawing
|
2019-08-14 04:13:09 +02:00
|
|
|
t = self.time_from_bytes(data[4:10])
|
2018-02-07 06:31:01 +01:00
|
|
|
|
2019-08-14 04:13:09 +02:00
|
|
|
# four bytes for the stroke count
|
|
|
|
nstrokes = int.from_bytes(data[10:14], byteorder='little')
|
2018-02-07 06:31:01 +01:00
|
|
|
|
2019-08-14 04:48:12 +02:00
|
|
|
timestamp = time.strftime('%Y%m%d-%H%M%S', t)
|
|
|
|
logger.debug(f'Drawing timestamp: {timestamp}, {nstrokes} strokes')
|
2018-02-16 04:49:07 +01:00
|
|
|
|
2019-08-14 04:13:09 +02:00
|
|
|
# Two trailing zero bytes we don't care about because we know what a
|
|
|
|
# zero looks like.
|
2018-02-07 06:31:01 +01:00
|
|
|
|
2019-08-14 04:13:09 +02:00
|
|
|
return True, 16
|
2018-02-07 06:31:01 +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
|
2019-07-14 10:10:35 +02:00
|
|
|
# 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
|
2019-07-14 10:32:34 +02:00
|
|
|
self._sync_state = 0
|
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-07 16:31:49 +01:00
|
|
|
try:
|
2019-08-09 06:27:54 +02:00
|
|
|
protocol = ProtocolVersion.from_string(self._config['Protocol'])
|
2019-08-21 12:49:58 +02:00
|
|
|
self._init_protocol(protocol)
|
|
|
|
except (KeyError, ValueError):
|
|
|
|
logger.error(f'Missing or invalid Protocol entry in config file. Treating this device as unregistered')
|
|
|
|
self._uuid = None
|
2018-02-07 18:52:48 +01:00
|
|
|
|
2018-02-08 15:03:57 +01:00
|
|
|
def _init_protocol(self, protocol):
|
2019-07-15 01:33:14 +02:00
|
|
|
protocols = {
|
2019-08-09 06:27:54 +02:00
|
|
|
ProtocolVersion.SPARK: WacomProtocolSpark,
|
|
|
|
ProtocolVersion.SLATE: WacomProtocolSlate,
|
|
|
|
ProtocolVersion.INTUOS_PRO: WacomProtocolIntuosPro,
|
2019-07-15 01:33:14 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if protocol not in protocols:
|
2019-08-21 08:33:23 +02:00
|
|
|
raise NotImplementedError(f'Protocol "{protocol}" not implemented')
|
2018-02-07 18:52:48 +01:00
|
|
|
|
2019-07-15 01:33:14 +02:00
|
|
|
pclass = protocols[protocol]
|
|
|
|
self._wacom_protocol = pclass(self._device, self._uuid)
|
2019-08-09 06:27:54 +02:00
|
|
|
logger.debug(f'{self._device.name} is using protocol {protocol.name}')
|
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)
|
2019-07-15 02:27:57 +02:00
|
|
|
self._wacom_protocol.connect('notify::dimensions', self._on_dimensions)
|
|
|
|
|
|
|
|
@GObject.Property
|
|
|
|
def dimensions(self):
|
|
|
|
return self._wacom_protocol.dimensions
|
|
|
|
|
|
|
|
def _on_dimensions(self, protocol, pspec):
|
|
|
|
self.notify('dimensions')
|
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
|
|
|
|
2019-07-14 10:32:34 +02:00
|
|
|
@GObject.Property
|
|
|
|
def sync_state(self):
|
|
|
|
return self._sync_state
|
|
|
|
|
|
|
|
@sync_state.setter
|
|
|
|
def sync_state(self, state):
|
|
|
|
self._sync_state = state
|
|
|
|
|
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]
|
|
|
|
|
2019-08-29 04:27:20 +02:00
|
|
|
logger.debug(f'{self._device.address}: starting for mode {mode.name}')
|
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
|
2019-07-14 10:32:34 +02:00
|
|
|
self.sync_state = 1
|
2018-02-07 16:31:49 +01:00
|
|
|
self._wacom_protocol.retrieve_data()
|
2019-08-22 00:36:14 +02:00
|
|
|
except DeviceError as e:
|
2018-01-24 12:06:16 +01:00
|
|
|
logger.error(f'**** Exception: {e} ****')
|
|
|
|
exception = e
|
2019-08-22 03:51:21 +02:00
|
|
|
except AuthorizationError as e:
|
|
|
|
logger.error(f'Authorization failed, device needs to be re-registered')
|
|
|
|
exception = e
|
2018-01-15 15:17:43 +01:00
|
|
|
finally:
|
2019-08-22 03:49:11 +02:00
|
|
|
self.sync_state = 0
|
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()
|