Browse Source

unify remap functions

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

97
remarkable_mouse/common.py

@ -7,17 +7,44 @@ from screeninfo import get_monitors, Monitor @@ -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): @@ -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('<Return>', lambda _: on_click())
window.columnconfigure(0, weight=1)
window.rowconfigure(0, weight=1)
@ -58,10 +87,46 @@ def get_region(orientation): @@ -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)
)

195
remarkable_mouse/evdev.py

@ -5,6 +5,10 @@ from screeninfo import get_monitors @@ -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(): @@ -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): @@ -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): @@ -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): @@ -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))

133
remarkable_mouse/pynput.py

@ -1,69 +1,19 @@ @@ -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) @@ -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]
)

Loading…
Cancel
Save