Send events to a virtual input device
Instead of sending mouse move and click events to the system’s main device, create a separate virtual input device that registers itself as a Wacom tablet. Most importantly, this enables pressure and tilt sensitivity which is picked up by programs such as GIMP or Krita. Because this uses the `libevdev` library that is only supported on Linux, this commit breaks compatibility with Windows and (probably) macOS. Furthermore, because creating virtual input devices is restricted to root, the script must now be run with `sudo`. Failing to do so will most likely trigger a permission error. CLI changes ----------- * Drop the `--orientation` flag. Orientation of the device can now be configured just like any other Wacom device using `xinput`: ``` xinput --set-prop "reMarkable tablet stylus" "Wacom Rotation" <orientation> ``` where `<orientation>` is one of 0 (for “right” orientation), 1 (for “portrait” orientation), 2 (for “left” orientation) or 3 (for “reversed portrait” orientation). * Drop the `--monitor` flag. This can also be configured using `xinput` instead: ``` xinput --map-to-output "reMarkable tablet stylus" <output> ``` where `<output>` is the name of an output currently connected to the device, as listed by `xrandr` (e.g. LVDS1). * Drop the `--offset` flag. This didn’t seem to be used anywhere in the code. * Drop the `--threshold` flag. The pressure threshold required to trigger a click event can be configured using `xinput`: ``` xinput --set-prop "reMarkable tablet stylus" "Wacom Pressure Threshold" 1000 ``` where `1000` can be replaced by an arbitrary pressure threshold. On my machine, the default seems to be 26. The pressure profile (mapping the actual pressure put on the stylus to the pressure actually received by the drawing programs) can also be adjusted using the following prop: ``` xinput --set-prop "reMarkable tablet stylus" "Wacom Pressurecurve" 50 0 100 50 ``` Dependencies changes -------------------- Replaced dependency pynput with libevdev (which requires that libevdev is present on the system). Dropped dependency `screeninfo` because assigning the input to a monitor is no longer done through this program.
This commit is contained in:
@@ -8,61 +8,99 @@ import os
|
|||||||
import sys
|
import sys
|
||||||
import struct
|
import struct
|
||||||
from getpass import getpass
|
from getpass import getpass
|
||||||
|
|
||||||
|
import libevdev
|
||||||
import paramiko
|
import paramiko
|
||||||
from screeninfo import get_monitors
|
|
||||||
from pynput.mouse import Button, Controller
|
|
||||||
|
|
||||||
# evtype_sync = 0
|
|
||||||
# evtype_key = 1
|
|
||||||
e_type_abs = 3
|
|
||||||
|
|
||||||
# evcode_stylus_distance = 25
|
|
||||||
# evcode_stylus_xtilt = 26
|
|
||||||
# evcode_stylus_ytilt = 27
|
|
||||||
e_code_stylus_xpos = 1
|
|
||||||
e_code_stylus_ypos = 0
|
|
||||||
e_code_stylus_pressure = 24
|
|
||||||
# evcode_finger_xpos = 53
|
|
||||||
# evcode_finger_ypos = 54
|
|
||||||
# evcode_finger_pressure = 58
|
|
||||||
|
|
||||||
stylus_width = 15725
|
|
||||||
stylus_height = 20951
|
|
||||||
# finger_width = 767
|
|
||||||
# finger_height = 1023
|
|
||||||
|
|
||||||
mouse = Controller()
|
|
||||||
logging.basicConfig(format='%(message)s')
|
logging.basicConfig(format='%(message)s')
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
# mouse state
|
# Maximum value that can be reported by the Wacom driver for the X axis
|
||||||
LIFTED = 0
|
MAX_ABS_X = 20967
|
||||||
PRESSED = 1
|
|
||||||
|
|
||||||
|
# Maximum value that can be reported by the Wacom driver for the Y axis
|
||||||
|
MAX_ABS_Y = 15725
|
||||||
|
|
||||||
# remap wacom coordinates in various orientations
|
def create_local_device():
|
||||||
def fit(x, y, stylus_width, stylus_height, monitor, orientation):
|
"""
|
||||||
|
Create a virtual input device on this host that has the same
|
||||||
|
characteristics as a Wacom tablet.
|
||||||
|
|
||||||
if orientation == 'vertical':
|
:returns: virtual input device
|
||||||
y = stylus_height - y
|
"""
|
||||||
elif orientation == 'right':
|
device = libevdev.Device()
|
||||||
x, y = y, x
|
|
||||||
stylus_width, stylus_height = stylus_height, stylus_width
|
|
||||||
elif orientation == 'left':
|
|
||||||
x, y = stylus_height - y, stylus_width - x
|
|
||||||
stylus_width, stylus_height = stylus_height, stylus_width
|
|
||||||
|
|
||||||
ratio_width, ratio_height = monitor.width / stylus_width, monitor.height / stylus_height
|
# Set device properties to emulate those of Wacom tablets
|
||||||
scaling = ratio_width if ratio_width > ratio_height else ratio_height
|
device.name = 'reMarkable tablet'
|
||||||
|
device.id = {
|
||||||
|
'bustype': 24,
|
||||||
|
'vendor': 1386,
|
||||||
|
'product': 0,
|
||||||
|
'version': 54
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
# Enable buttons supported by the digitizer
|
||||||
scaling * (x - (stylus_width - monitor.width / scaling) / 2),
|
device.enable(libevdev.EV_KEY.BTN_TOOL_PEN)
|
||||||
scaling * (y - (stylus_height - monitor.height / scaling) / 2)
|
device.enable(libevdev.EV_KEY.BTN_TOOL_RUBBER)
|
||||||
|
device.enable(libevdev.EV_KEY.BTN_TOUCH)
|
||||||
|
device.enable(libevdev.EV_KEY.BTN_STYLUS)
|
||||||
|
device.enable(libevdev.EV_KEY.BTN_STYLUS2)
|
||||||
|
|
||||||
|
# Enable position, tilt, distance and pressure change events
|
||||||
|
device.enable(
|
||||||
|
libevdev.EV_ABS.ABS_X,
|
||||||
|
libevdev.InputAbsInfo(
|
||||||
|
minimum=0,
|
||||||
|
maximum=MAX_ABS_X
|
||||||
|
)
|
||||||
|
)
|
||||||
|
device.enable(
|
||||||
|
libevdev.EV_ABS.ABS_Y,
|
||||||
|
libevdev.InputAbsInfo(
|
||||||
|
minimum=0,
|
||||||
|
maximum=MAX_ABS_Y
|
||||||
|
)
|
||||||
|
)
|
||||||
|
device.enable(
|
||||||
|
libevdev.EV_ABS.ABS_PRESSURE,
|
||||||
|
libevdev.InputAbsInfo(
|
||||||
|
minimum=0,
|
||||||
|
maximum=4095
|
||||||
|
)
|
||||||
|
)
|
||||||
|
device.enable(
|
||||||
|
libevdev.EV_ABS.ABS_DISTANCE,
|
||||||
|
libevdev.InputAbsInfo(
|
||||||
|
minimum=0,
|
||||||
|
maximum=255
|
||||||
|
)
|
||||||
|
)
|
||||||
|
device.enable(
|
||||||
|
libevdev.EV_ABS.ABS_TILT_X,
|
||||||
|
libevdev.InputAbsInfo(
|
||||||
|
minimum=-9000,
|
||||||
|
maximum=9000
|
||||||
|
)
|
||||||
|
)
|
||||||
|
device.enable(
|
||||||
|
libevdev.EV_ABS.ABS_TILT_Y,
|
||||||
|
libevdev.InputAbsInfo(
|
||||||
|
minimum=-9000,
|
||||||
|
maximum=9000
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
return device.create_uinput_device()
|
||||||
|
|
||||||
def open_eventfile(args, file='/dev/input/event0'):
|
def open_remote_device(args, file='/dev/input/event0'):
|
||||||
"""ssh to reMarkable and open event0"""
|
"""
|
||||||
|
Open a remote input device via SSH.
|
||||||
|
|
||||||
|
:param args: command-line arguments
|
||||||
|
:param file: path to the input device on the device
|
||||||
|
:returns: read-only stream of input events
|
||||||
|
"""
|
||||||
|
log.info("Connecting to input '{}' on '{}'".format(file, args.address))
|
||||||
|
|
||||||
if args.key is not None:
|
if args.key is not None:
|
||||||
password = None
|
password = None
|
||||||
@@ -94,90 +132,76 @@ def open_eventfile(args, file='/dev/input/event0'):
|
|||||||
pkey=pkey,
|
pkey=pkey,
|
||||||
look_for_keys=False
|
look_for_keys=False
|
||||||
)
|
)
|
||||||
print("Connected to {}".format(args.address))
|
|
||||||
|
|
||||||
# Start reading events
|
# Start reading events
|
||||||
_, stdout, _ = client.exec_command('cat ' + file)
|
_, stdout, _ = client.exec_command('cat ' + file)
|
||||||
|
|
||||||
return stdout
|
return stdout
|
||||||
|
|
||||||
|
def pipe_device(args, remote_device, local_device):
|
||||||
|
"""
|
||||||
|
Pipe events from a remote device to a local device.
|
||||||
|
|
||||||
def read_tablet(args):
|
:param args: command-line arguments
|
||||||
"""Loop forever and map evdev events to mouse"""
|
:param remote_device: stream of events to read from
|
||||||
|
:param local_device: local virtual device to write events to
|
||||||
state = LIFTED
|
"""
|
||||||
new_x = new_y = False
|
# While debug mode is active, we log events grouped together between
|
||||||
|
# SYN_REPORT events. Pending events for the next log are stored here
|
||||||
monitor = get_monitors()[args.monitor]
|
pending_events = []
|
||||||
log.debug('Chose monitor: {}'.format(monitor))
|
|
||||||
|
|
||||||
stdout = open_eventfile(args)
|
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
_, _, e_type, e_code, e_value = struct.unpack('2IHHi', stdout.read(16))
|
e_time, e_millis, e_type, e_code, e_value = struct.unpack('2IHHi', remote_device.read(16))
|
||||||
|
e_bit = libevdev.evbit(e_type, e_code)
|
||||||
|
event = libevdev.InputEvent(e_bit, value=e_value)
|
||||||
|
|
||||||
if e_type == e_type_abs:
|
local_device.send_events([event])
|
||||||
|
|
||||||
# handle x direction
|
if args.debug:
|
||||||
if e_code == e_code_stylus_xpos:
|
if e_bit == libevdev.EV_SYN.SYN_REPORT:
|
||||||
log.debug(e_value)
|
event_repr = ', '.join(
|
||||||
x = e_value
|
'{} = {}'.format(
|
||||||
new_x = True
|
event.code.name,
|
||||||
|
event.value
|
||||||
# handle y direction
|
) for event in pending_events
|
||||||
if e_code == e_code_stylus_ypos:
|
|
||||||
log.debug('\t{}'.format(e_value))
|
|
||||||
y = e_value
|
|
||||||
new_y = True
|
|
||||||
|
|
||||||
# handle draw
|
|
||||||
if e_code == e_code_stylus_pressure:
|
|
||||||
log.debug('\t\t{}'.format(e_value))
|
|
||||||
if e_value > args.threshold:
|
|
||||||
if state == LIFTED:
|
|
||||||
log.info('PRESS')
|
|
||||||
state = PRESSED
|
|
||||||
mouse.press(Button.left)
|
|
||||||
else:
|
|
||||||
if state == PRESSED:
|
|
||||||
log.info('RELEASE')
|
|
||||||
state = LIFTED
|
|
||||||
mouse.release(Button.left)
|
|
||||||
|
|
||||||
|
|
||||||
# only move when x and y are updated for smoother mouse
|
|
||||||
if new_x and new_y:
|
|
||||||
mapped_x, mapped_y = fit(x, y, stylus_width, stylus_height, monitor, args.orientation)
|
|
||||||
mouse.move(
|
|
||||||
monitor.x + mapped_x - mouse.position[0],
|
|
||||||
monitor.y + mapped_y - mouse.position[1]
|
|
||||||
)
|
)
|
||||||
new_x = new_y = False
|
log.debug('{}.{:0>6} - {}'.format(e_time, e_millis, event_repr))
|
||||||
|
pending_events = []
|
||||||
|
else:
|
||||||
|
pending_events += [event]
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
|
||||||
try:
|
try:
|
||||||
parser = argparse.ArgumentParser(description="use reMarkable tablet as a mouse input")
|
parser = argparse.ArgumentParser(description="use reMarkable tablet as a mouse input")
|
||||||
parser.add_argument('--orientation', default='left', choices=['vertical', 'left', 'right'])
|
|
||||||
parser.add_argument('--monitor', default=0, type=int, metavar='NUM', help="monitor to use")
|
|
||||||
parser.add_argument('--offset', default=(0, 0), type=int, metavar=('x', 'y'), nargs=2, help="offset mapped region on monitor")
|
|
||||||
parser.add_argument('--debug', action='store_true', default=False, help="enable debug messages")
|
parser.add_argument('--debug', action='store_true', default=False, help="enable debug messages")
|
||||||
parser.add_argument('--key', type=str, metavar='PATH', help="ssh private key")
|
parser.add_argument('--key', type=str, metavar='PATH', help="ssh private key")
|
||||||
parser.add_argument('--password', default=None, type=str, help="ssh password")
|
parser.add_argument('--password', default=None, type=str, help="ssh password")
|
||||||
parser.add_argument('--address', default='10.11.99.1', type=str, help="device address")
|
parser.add_argument('--address', default='10.11.99.1', type=str, help="device address")
|
||||||
parser.add_argument('--threshold', default=1000, type=int, help="stylus pressure threshold (default 1000)")
|
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
if args.debug:
|
if args.debug:
|
||||||
print('Debugging enabled...')
|
|
||||||
logging.getLogger('').setLevel(logging.DEBUG)
|
logging.getLogger('').setLevel(logging.DEBUG)
|
||||||
log.setLevel(logging.DEBUG)
|
log.setLevel(logging.DEBUG)
|
||||||
|
log.info('Debugging enabled...')
|
||||||
|
else:
|
||||||
|
logging.getLogger('').setLevel(logging.INFO)
|
||||||
|
log.setLevel(logging.INFO)
|
||||||
|
|
||||||
read_tablet(args)
|
try:
|
||||||
|
local_device = create_local_device()
|
||||||
|
log.info("Created virtual input device '{}'".format(local_device.devnode))
|
||||||
|
except PermissionError:
|
||||||
|
log.error('Insufficient permissions for creating a virtual input device')
|
||||||
|
log.error('Make sure you run this program as root')
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
remote_device = open_remote_device(args)
|
||||||
|
pipe_device(args, remote_device, local_device)
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
pass
|
pass
|
||||||
|
except EOFError:
|
||||||
|
pass
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
main()
|
main()
|
||||||
|
|||||||
Reference in New Issue
Block a user