diff --git a/remarkable_mouse/common.py b/remarkable_mouse/common.py index 80ee305..c6c280d 100644 --- a/remarkable_mouse/common.py +++ b/remarkable_mouse/common.py @@ -7,17 +7,44 @@ from screeninfo import get_monitors, Monitor logging.basicConfig(format='%(message)s') log = logging.getLogger('remouse') -# get info of where we want to map the tablet to +wacom_width = 15725 +wacom_height = 20967 + def get_monitor(region, monitor_num, orientation): - if region is not None: - monitor = get_region(orientation) + """ Get info of where we want to map the tablet to + + Args: + region (boolean): whether to prompt the user to select a region + monitor_num (int): index of monitor to use. Implies region=False + orientation (str): Location of tablet charging port. + ('top', 'bottom', 'left', 'right') + + Returns: + screeninfo.Monitor + """ + + if region: + x, y, width, height = get_region(orientation) + monitor = Monitor( + x, y, width, height, + name="Fake monitor from region selection" + ) else: monitor = get_monitors()[monitor_num] return monitor -# Pop up a window, ask the user to move the window, and then get the position of the window's contents def get_region(orientation): + """ Show tkwindow to user to select mouse bounds + + Args: + orientation (str): Location of tablet charging port. + ('top', 'bottom', 'left', 'right') + + Returns: + x (int), y (int), width (int), height (int) + """ + try: import tkinter as tk from tkinter import ttk @@ -30,23 +57,25 @@ def get_region(orientation): # A bit of an ugly hack to get this function to run synchronously # Ideally would use full async support, but this solution required minimal changes to rest of code - selected_pos = None + window_bounds = None def on_click(): - nonlocal selected_pos - selected_pos = Monitor( - window.winfo_x(), - window.winfo_y(), - window.winfo_width(), - window.winfo_height(), - name="Fake monitor from region selection" + nonlocal window_bounds + window_bounds = ( + window.winfo_x(), window.winfo_y(), + window.winfo_width(), window.winfo_height() ) window.destroy() - confirm = ttk.Button(window, text="Drag and resize this button to the desired mouse range, then click", - command=on_click) + confirm = ttk.Button( + window, + text="Resize and move this window, then click or press Enter", + command=on_click + ) confirm.grid(column=0, row=0, sticky=(tk.N, tk.S, tk.E, tk.W)) + window.bind('', lambda _: on_click()) + window.columnconfigure(0, weight=1) window.rowconfigure(0, weight=1) @@ -58,10 +87,46 @@ def get_region(orientation): else: window.geometry("936x702") + # block here window.mainloop() - if selected_pos is None: + if window_bounds is None: log.debug("Window closed without giving mouse range") sys.exit(1) - return selected_pos + return window_bounds + + +# remap wacom coordinates to screen coordinates +def remap(x, y, wacom_width, wacom_height, monitor_width, + monitor_height, mode, orientation): + + if orientation == 'bottom': + y = wacom_height - y + elif orientation == 'right': + x, y = wacom_height - y, wacom_width - x + wacom_width, wacom_height = wacom_height, wacom_width + elif orientation == 'left': + x, y = y, x + wacom_width, wacom_height = wacom_height, wacom_width + elif orientation == 'top': + x = wacom_width - x + + ratio_width, ratio_height = monitor_width / wacom_width, monitor_height / wacom_height + + if mode == 'fill': + scaling_x = max(ratio_width, ratio_height) + scaling_y = scaling_x + elif mode == 'fit': + scaling_x = min(ratio_width, ratio_height) + scaling_y = scaling_x + elif mode == 'stretch': + scaling_x = ratio_width + scaling_y = ratio_height + else: + raise NotImplementedError + + return ( + scaling_x * (x - (wacom_width - monitor_width / scaling_x) / 2), + scaling_y * (y - (wacom_height - monitor_height / scaling_y) / 2) + ) diff --git a/remarkable_mouse/evdev.py b/remarkable_mouse/evdev.py index 0fd1d8e..4101e1d 100644 --- a/remarkable_mouse/evdev.py +++ b/remarkable_mouse/evdev.py @@ -5,6 +5,10 @@ from screeninfo import get_monitors import time from itertools import cycle from socket import timeout as TimeoutError +import libevdev +from libevdev import EV_SYN, EV_KEY, EV_ABS + +from .common import get_monitor, remap, wacom_width, wacom_height logging.basicConfig(format='%(message)s') log = logging.getLogger('remouse') @@ -133,160 +137,14 @@ def create_local_device(): return device.create_uinput_device() -# map computer screen coordinates to rM pen coordinates -def map_comp2pen(x, y, wacom_width, wacom_height, monitor_width, - monitor_height, mode, orientation=None): - - if orientation in ('bottom', 'top'): - x, y = y, x - monitor_width, monitor_height = monitor_height, monitor_width - - ratio_width, ratio_height = wacom_width / monitor_width, wacom_height / monitor_height - - if mode == 'fit': - scaling = max(ratio_width, ratio_height) - elif mode == 'fill': - scaling = min(ratio_width, ratio_height) - else: - raise NotImplementedError - - return ( - scaling * (x - (monitor_width - wacom_width / scaling) / 2), - scaling * (y - (monitor_height - wacom_height / scaling) / 2) - ) - -# map computer screen coordinates to rM touch coordinates -def map_comp2touch(x, y, touch_width, touch_height, monitor_width, - monitor_height, mode, orientation=None): - - if orientation in ('left', 'right'): - x, y = y, x - monitor_width, monitor_height = monitor_height, monitor_width - - ratio_width, ratio_height = touch_width / monitor_width, touch_height / monitor_height - - if mode == 'fit': - scaling = max(ratio_width, ratio_height) - elif mode == 'fill': - scaling = min(ratio_width, ratio_height) - else: - raise NotImplementedError - - return ( - scaling * (x - (monitor_width - touch_width / scaling) / 2), - scaling * (y - (monitor_height - touch_height / scaling) / 2) - ) - -def configure_xinput(*, orientation, monitor, threshold, mode): - """ - Configure screen mapping settings from rM to local machine - - Args: - orientation (str): location of charging port relative to screen - monitor (int): monitor number to use - threshold (int): pressure threshold for detecting pen - mode (str): scaling mode when mapping reMarkable pen coordinates to screen - """ - - # give time for virtual device creation before running xinput commands - time.sleep(1) - - # ----- Pen ----- - - # set orientation with xinput - orientation = {'left': 0, 'bottom': 1, 'top': 2, 'right': 3}[orientation] - result = subprocess.run( - 'xinput --set-prop "reMarkable pen stylus" "Wacom Rotation" {}'.format(orientation), - capture_output=True, - shell=True - ) - if result.returncode != 0: - log.warning("Error setting orientation: %s", result.stderr.decode('utf8')) - - # set monitor to use - monitor = get_monitors()[monitor] - log.debug('Chose monitor: {}'.format(monitor)) - result = subprocess.run( - 'xinput --map-to-output "reMarkable pen stylus" {}'.format(monitor.name), - capture_output=True, - shell=True - ) - if result.returncode != 0: - log.warning("Error setting monitor: %s", result.stderr.decode('utf8')) - - # set stylus pressure - result = subprocess.run( - 'xinput --set-prop "reMarkable pen stylus" "Wacom Pressure Threshold" {}'.format(threshold), - capture_output=True, - shell=True - ) - if result.returncode != 0: - log.warning("Error setting pressure threshold: %s", result.stderr.decode('utf8')) - - # set fitting mode - min_x, min_y = map_comp2pen( - 0, 0, - MAX_ABS_X, MAX_ABS_Y, monitor.width, monitor.height, - mode, - orientation - ) - max_x, max_y = map_comp2pen( - monitor.width, monitor.height, - MAX_ABS_X, MAX_ABS_Y, monitor.width, monitor.height, - mode, - orientation - ) - log.debug("Wacom tablet area: {} {} {} {}".format(min_x, min_y, max_x, max_y)) - result = subprocess.run( - 'xinput --set-prop "reMarkable pen stylus" "Wacom Tablet Area" \ - {} {} {} {}'.format(min_x, min_y, max_x, max_y), - capture_output=True, - shell=True - ) - if result.returncode != 0: - log.warning("Error setting fit: %s", result.stderr.decode('utf8')) - - # ----- Touch ----- - - # Set touch fitting mode - mt_min_x, mt_min_y = map_comp2touch( - 0, 0, - MT_MAX_ABS_X, MT_MAX_ABS_Y, monitor.width, monitor.height, - mode, - orientation - ) - mt_max_x, mt_max_y = map_comp2touch( - monitor.width, monitor.height, - MT_MAX_ABS_X, MT_MAX_ABS_Y, monitor.width, monitor.height, - mode, - orientation - ) - log.debug("Multi-touch area: {} {} {} {}".format(mt_min_x, mt_min_y, mt_max_x * 2, mt_max_y * 2)) - result = subprocess.run( - 'xinput --set-prop "reMarkable pen touch" "Wacom Tablet Area" \ - {} {} {} {}'.format(mt_min_x, mt_min_y, mt_max_x, mt_max_y), - capture_output=True, - shell=True - ) - if result.returncode != 0: - log.warning("Error setting fit: %s", result.stderr.decode('utf8')) - result = subprocess.run( # Just need to rotate the touchscreen -90 so that it matches the wacom sensor. - 'xinput --set-prop "reMarkable pen touch" "Coordinate Transformation Matrix" 0 1 0 -1 0 1 0 0 1', - capture_output=True, - shell=True - ) - if result.returncode != 0: - log.warning("Error setting orientation: %s", result.stderr.decode('utf8')) - - -def read_tablet(rm_inputs, *, orientation, monitor, threshold, mode): +def read_tablet(rm_inputs, *, orientation, monitor_num, region, threshold, mode): """Pipe rM evdev events to local device Args: rm_inputs (dictionary of paramiko.ChannelFile): dict of pen, button and touch input streams orientation (str): tablet orientation - monitor (int): monitor number to map to + monitor_num (int): monitor number to map to threshold (int): pressure threshold mode (str): mapping mode """ @@ -294,17 +152,12 @@ def read_tablet(rm_inputs, *, orientation, monitor, threshold, mode): local_device = create_local_device() log.debug("Created virtual input device '{}'".format(local_device.devnode)) - configure_xinput( - orientation=orientation, - monitor=monitor, - threshold=threshold, - mode=mode, - ) - - import libevdev + monitor = get_monitor(region, monitor_num, orientation) pending_events = [] + x = y = 0 + # loop inputs forever # for input_name, stream in cycle(rm_inputs.items()): stream = rm_inputs['pen'] @@ -317,9 +170,31 @@ def read_tablet(rm_inputs, *, orientation, monitor, threshold, mode): e_time, e_millis, e_type, e_code, e_value = struct.unpack('2IHHi', data) e_bit = libevdev.evbit(e_type, e_code) - event = libevdev.InputEvent(e_bit, value=e_value) + e = libevdev.InputEvent(e_bit, value=e_value) + + local_device.send_events([e]) + + if e.matches(EV_ABS): + + # handle x direction + if e.matches(EV_ABS.ABS_Y): + x = e.value + + # handle y direction + if e.matches(EV_ABS.ABS_X): + y = e.value + + elif e.matches(EV_SYN): + mapped_x, mapped_y = remap( + x, y, + wacom_width, wacom_height, + monitor.width, monitor.height, + mode, orientation + ) + local_device.send_events([e]) - local_device.send_events([event]) + else: + local_device.send_events([e]) # While debug mode is active, we log events grouped together between # SYN_REPORT events. Pending events for the next log are stored here @@ -327,8 +202,8 @@ def read_tablet(rm_inputs, *, orientation, monitor, threshold, mode): if e_bit == libevdev.EV_SYN.SYN_REPORT: event_repr = ', '.join( '{} = {}'.format( - event.code.name, - event.value + e.code.name, + e.value ) for event in pending_events ) log.debug('{}.{:0>6} - {}'.format(e_time, e_millis, event_repr)) diff --git a/remarkable_mouse/pynput.py b/remarkable_mouse/pynput.py index b9d2e76..9dc9b90 100644 --- a/remarkable_mouse/pynput.py +++ b/remarkable_mouse/pynput.py @@ -1,69 +1,19 @@ import logging import struct from screeninfo import get_monitors +import libevdev +from libevdev import EV_SYN, EV_KEY, EV_ABS -from .common import get_monitor +from .common import get_monitor, remap, wacom_width, wacom_height logging.basicConfig(format='%(message)s') log = logging.getLogger('remouse') -# 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 - # wacom digitizer dimensions -wacom_width = 15725 -wacom_height = 20967 # touchscreen dimensions # finger_width = 767 # finger_height = 1023 - -# remap wacom coordinates to screen coordinates -def remap(x, y, wacom_width, wacom_height, monitor_width, - monitor_height, mode, orientation): - - if orientation == 'bottom': - y = wacom_height - y - elif orientation == 'right': - x, y = wacom_height - y, wacom_width - x - wacom_width, wacom_height = wacom_height, wacom_width - elif orientation == 'left': - x, y = y, x - wacom_width, wacom_height = wacom_height, wacom_width - elif orientation == 'top': - x = wacom_width - x - - ratio_width, ratio_height = monitor_width / wacom_width, monitor_height / wacom_height - - if mode == 'fill': - scaling_x = max(ratio_width, ratio_height) - scaling_y = scaling_x - elif mode == 'fit': - scaling_x = min(ratio_width, ratio_height) - scaling_y = scaling_x - elif mode == 'stretch': - scaling_x = ratio_width - scaling_y = ratio_height - else: - raise NotImplementedError - - return ( - scaling_x * (x - (wacom_width - monitor_width / scaling_x) / 2), - scaling_y * (y - (wacom_height - monitor_height / scaling_y) / 2) - ) - - def read_tablet(rm_inputs, *, orientation, monitor_num, region, threshold, mode): """Loop forever and map evdev events to mouse @@ -79,56 +29,49 @@ def read_tablet(rm_inputs, *, orientation, monitor_num, region, threshold, mode) from pynput.mouse import Button, Controller - lifted = True - new_x = new_y = False - mouse = Controller() - monitor = get_monitor(monitor_num, region, orientation) + monitor = get_monitor(region, monitor_num, orientation) log.debug('Chose monitor: {}'.format(monitor)) + x = y = 0 + while True: _, _, e_type, e_code, e_value = struct.unpack('2IHHi', rm_inputs['pen'].read(16)) - if e_type == e_type_abs: + e_bit = libevdev.evbit(e_type, e_code) + e = libevdev.InputEvent(e_bit, value=e_value) + + if e.matches(EV_ABS): # handle x direction - if e_code == e_code_stylus_xpos: - log.debug(e_value) - x = e_value - new_x = True + if e.matches(EV_ABS.ABS_Y): + log.debug(e.value) + x = e.value # handle y direction - 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 > threshold: - if lifted: - log.debug('PRESS') - lifted = False - mouse.press(Button.left) - else: - if not lifted: - log.debug('RELEASE') - lifted = True - 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 = remap( - x, y, - wacom_width, wacom_height, - monitor.width, monitor.height, - mode, orientation - ) - mouse.move( - monitor.x + mapped_x - mouse.position[0], - monitor.y + mapped_y - mouse.position[1] - ) - new_x = new_y = False + if e.matches(EV_ABS.ABS_X): + log.debug('\t{}'.format(e.value)) + y = e.value + + # handle draw + if e.matches(EV_KEY.BTN_TOUCH): + log.debug('\t\t{}'.format(e.value)) + if e.value == 1: + log.debug('PRESS') + mouse.press(Button.left) + else: + log.debug('RELEASE') + mouse.release(Button.left) + + if e.matches(EV_SYN): + mapped_x, mapped_y = remap( + x, y, + wacom_width, wacom_height, + monitor.width, monitor.height, + mode, orientation + ) + mouse.move( + monitor.x + mapped_x - mouse.position[0], + monitor.y + mapped_y - mouse.position[1] + )