Browse Source

unify remap functions

master
evan 3 years ago
parent
commit
31e1d776c1
  1. 97
      remarkable_mouse/common.py
  2. 195
      remarkable_mouse/evdev.py
  3. 97
      remarkable_mouse/pynput.py

97
remarkable_mouse/common.py

@ -7,17 +7,44 @@ from screeninfo import get_monitors, Monitor
logging.basicConfig(format='%(message)s') logging.basicConfig(format='%(message)s')
log = logging.getLogger('remouse') 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): def get_monitor(region, monitor_num, orientation):
if region is not None: """ Get info of where we want to map the tablet to
monitor = get_region(orientation)
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: else:
monitor = get_monitors()[monitor_num] monitor = get_monitors()[monitor_num]
return monitor 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): 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: try:
import tkinter as tk import tkinter as tk
from tkinter import ttk 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 # 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 # 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(): def on_click():
nonlocal selected_pos nonlocal window_bounds
selected_pos = Monitor( window_bounds = (
window.winfo_x(), window.winfo_x(), window.winfo_y(),
window.winfo_y(), window.winfo_width(), window.winfo_height()
window.winfo_width(),
window.winfo_height(),
name="Fake monitor from region selection"
) )
window.destroy() window.destroy()
confirm = ttk.Button(window, text="Drag and resize this button to the desired mouse range, then click", confirm = ttk.Button(
command=on_click) 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)) confirm.grid(column=0, row=0, sticky=(tk.N, tk.S, tk.E, tk.W))
window.bind('<Return>', lambda _: on_click())
window.columnconfigure(0, weight=1) window.columnconfigure(0, weight=1)
window.rowconfigure(0, weight=1) window.rowconfigure(0, weight=1)
@ -58,10 +87,46 @@ def get_region(orientation):
else: else:
window.geometry("936x702") window.geometry("936x702")
# block here
window.mainloop() window.mainloop()
if selected_pos is None: if window_bounds is None:
log.debug("Window closed without giving mouse range") log.debug("Window closed without giving mouse range")
sys.exit(1) 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)
)

195
remarkable_mouse/evdev.py

@ -5,6 +5,10 @@ from screeninfo import get_monitors
import time import time
from itertools import cycle from itertools import cycle
from socket import timeout as TimeoutError 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') logging.basicConfig(format='%(message)s')
log = logging.getLogger('remouse') log = logging.getLogger('remouse')
@ -133,160 +137,14 @@ def create_local_device():
return device.create_uinput_device() return device.create_uinput_device()
# map computer screen coordinates to rM pen coordinates def read_tablet(rm_inputs, *, orientation, monitor_num, region, threshold, mode):
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):
"""Pipe rM evdev events to local device """Pipe rM evdev events to local device
Args: Args:
rm_inputs (dictionary of paramiko.ChannelFile): dict of pen, button rm_inputs (dictionary of paramiko.ChannelFile): dict of pen, button
and touch input streams and touch input streams
orientation (str): tablet orientation orientation (str): tablet orientation
monitor (int): monitor number to map to monitor_num (int): monitor number to map to
threshold (int): pressure threshold threshold (int): pressure threshold
mode (str): mapping mode mode (str): mapping mode
""" """
@ -294,17 +152,12 @@ def read_tablet(rm_inputs, *, orientation, monitor, threshold, mode):
local_device = create_local_device() local_device = create_local_device()
log.debug("Created virtual input device '{}'".format(local_device.devnode)) log.debug("Created virtual input device '{}'".format(local_device.devnode))
configure_xinput( monitor = get_monitor(region, monitor_num, orientation)
orientation=orientation,
monitor=monitor,
threshold=threshold,
mode=mode,
)
import libevdev
pending_events = [] pending_events = []
x = y = 0
# loop inputs forever # loop inputs forever
# for input_name, stream in cycle(rm_inputs.items()): # for input_name, stream in cycle(rm_inputs.items()):
stream = rm_inputs['pen'] 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_time, e_millis, e_type, e_code, e_value = struct.unpack('2IHHi', data)
e_bit = libevdev.evbit(e_type, e_code) 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):
local_device.send_events([event]) # 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])
else:
local_device.send_events([e])
# While debug mode is active, we log events grouped together between # While debug mode is active, we log events grouped together between
# SYN_REPORT events. Pending events for the next log are stored here # 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: if e_bit == libevdev.EV_SYN.SYN_REPORT:
event_repr = ', '.join( event_repr = ', '.join(
'{} = {}'.format( '{} = {}'.format(
event.code.name, e.code.name,
event.value e.value
) for event in pending_events ) for event in pending_events
) )
log.debug('{}.{:0>6} - {}'.format(e_time, e_millis, event_repr)) log.debug('{}.{:0>6} - {}'.format(e_time, e_millis, event_repr))

97
remarkable_mouse/pynput.py

@ -1,69 +1,19 @@
import logging import logging
import struct import struct
from screeninfo import get_monitors 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') logging.basicConfig(format='%(message)s')
log = logging.getLogger('remouse') 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 digitizer dimensions
wacom_width = 15725
wacom_height = 20967
# touchscreen dimensions # touchscreen dimensions
# finger_width = 767 # finger_width = 767
# finger_height = 1023 # 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): def read_tablet(rm_inputs, *, orientation, monitor_num, region, threshold, mode):
"""Loop forever and map evdev events to mouse """Loop forever and map evdev events to mouse
@ -79,48 +29,42 @@ def read_tablet(rm_inputs, *, orientation, monitor_num, region, threshold, mode)
from pynput.mouse import Button, Controller from pynput.mouse import Button, Controller
lifted = True
new_x = new_y = False
mouse = Controller() mouse = Controller()
monitor = get_monitor(monitor_num, region, orientation) monitor = get_monitor(region, monitor_num, orientation)
log.debug('Chose monitor: {}'.format(monitor)) log.debug('Chose monitor: {}'.format(monitor))
x = y = 0
while True: while True:
_, _, e_type, e_code, e_value = struct.unpack('2IHHi', rm_inputs['pen'].read(16)) _, _, 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 # handle x direction
if e_code == e_code_stylus_xpos: if e.matches(EV_ABS.ABS_Y):
log.debug(e_value) log.debug(e.value)
x = e_value x = e.value
new_x = True
# handle y direction # handle y direction
if e_code == e_code_stylus_ypos: if e.matches(EV_ABS.ABS_X):
log.debug('\t{}'.format(e_value)) log.debug('\t{}'.format(e.value))
y = e_value y = e.value
new_y = True
# handle draw # handle draw
if e_code == e_code_stylus_pressure: if e.matches(EV_KEY.BTN_TOUCH):
log.debug('\t\t{}'.format(e_value)) log.debug('\t\t{}'.format(e.value))
if e_value > threshold: if e.value == 1:
if lifted:
log.debug('PRESS') log.debug('PRESS')
lifted = False
mouse.press(Button.left) mouse.press(Button.left)
else: else:
if not lifted:
log.debug('RELEASE') log.debug('RELEASE')
lifted = True
mouse.release(Button.left) mouse.release(Button.left)
if e.matches(EV_SYN):
# only move when x and y are updated for smoother mouse
if new_x and new_y:
mapped_x, mapped_y = remap( mapped_x, mapped_y = remap(
x, y, x, y,
wacom_width, wacom_height, wacom_width, wacom_height,
@ -131,4 +75,3 @@ def read_tablet(rm_inputs, *, orientation, monitor_num, region, threshold, mode)
monitor.x + mapped_x - mouse.position[0], monitor.x + mapped_x - mouse.position[0],
monitor.y + mapped_y - mouse.position[1] monitor.y + mapped_y - mouse.position[1]
) )
new_x = new_y = False

Loading…
Cancel
Save