diff --git a/README.md b/README.md index a52f29e..2e216f4 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,9 @@ remouse ``` By default, `10.11.99.1` is used as the address. Find your password in the reMarkable's [settings menu](https://remarkablewiki.com/tech/ssh). If you are on Linux using X11, you can use the `--evdev` option for pressure support. +If you are on Windows, you can use the `--pen` option for pressure support. + +The monitor that the tablet will output to is dynamically changed to the one that the mouse is in. To output to only one monitor, use the `--monitor` flag. To use the `--region` flag, you may need to install the `python3-tk` or `python3-tkinter` package with your package manager. @@ -44,25 +47,26 @@ sudo --preserve-env=USER,PATH env remouse --evdev # Usage ``` -usage: remouse [-h] [--debug] [--key PATH] [--password PASSWORD] [--address ADDRESS] [--mode {fit,fill,stretch}] [--orientation {top,left,right,bottom}] [--monitor NUM] [--region] [--threshold THRESH] - [--evdev] +usage: remouse [-h] [--debug] [--key PATH] [--password PASSWORD] [--address ADDRESS] [--mode {fit,fill,stretch}] [--orientation {top,left,right,bottom}] [--monitor NUM] [--region] + [--threshold THRESH] [--evdev] [--pen] use reMarkable tablet as a mouse input -optional arguments: +options: -h, --help show this help message and exit --debug enable debug messages --key PATH ssh private key --password PASSWORD ssh password --address ADDRESS device address --mode {fit,fill,stretch} - Scale setting. Fit (default): take up the entire tablet, but not necessarily the entire monitor. Fill: take up the entire monitor, but not necessarily the entire tablet. Stretch: - take up both the entire tablet and monitor, but don't maintain aspect ratio. + Scale setting. Fit (default): take up the entire tablet, but not necessarily the entire monitor. Fill: take up the entire monitor, but not necessarily the entire + tablet. Stretch: take up both the entire tablet and monitor, but don't maintain aspect ratio. --orientation {top,left,right,bottom} position of tablet buttons - --monitor NUM monitor to output to - --region Use a GUI to position the output area. Overrides --monitor + --monitor NUM override automatic monitor selection + --region Use a GUI to position the output area. Overrides --monitor and automatic monitor selection --threshold THRESH stylus pressure threshold (default 600) --evdev use evdev to support pen pressure (requires root, Linux only) + --pen use pen input to support pen pressure in windows ``` diff --git a/pyvenv.cfg b/pyvenv.cfg new file mode 100644 index 0000000..451f68a --- /dev/null +++ b/pyvenv.cfg @@ -0,0 +1,5 @@ +home = C:\Users\Guest1\AppData\Local\Microsoft\WindowsApps\PythonSoftwareFoundation.Python.3.12_qbz5n2kfra8p0 +include-system-site-packages = false +version = 3.12.3 +executable = C:\Users\Guest1\AppData\Local\Microsoft\WindowsApps\PythonSoftwareFoundation.Python.3.12_qbz5n2kfra8p0\python.exe +command = C:\Users\Guest1\AppData\Local\Microsoft\WindowsApps\PythonSoftwareFoundation.Python.3.12_qbz5n2kfra8p0\python.exe -m venv E:\Programming Projects\remarkable_mouse_winpress diff --git a/remarkable_mouse/common.py b/remarkable_mouse/common.py index a320651..4836ec7 100644 --- a/remarkable_mouse/common.py +++ b/remarkable_mouse/common.py @@ -3,7 +3,6 @@ import logging import sys from screeninfo import get_monitors, Monitor - from .codes import codes, types logging.basicConfig(format='%(message)s') @@ -12,6 +11,25 @@ wacom_max_y = 15725 wacom_max_x = 20967 +def get_current_monitor_num(): + """ Get monitor number that the mouse is currently on + + Returns: + int: monitor number that mouse is currently on, or 0 if couldn't pick one + """ + from pynput.mouse import Controller + + mouse = Controller() + mouse_x = mouse.position[0] + mouse_y = mouse.position[1] + + for i, monitor in enumerate(get_monitors()): + if (mouse_x >= monitor.x and mouse_x < monitor.x + monitor.width and + mouse_y >= monitor.y and mouse_y < monitor.y + monitor.height): + return i + return 0 + + def get_monitor(region, monitor_num, orientation): """ Get info of where we want to map the tablet to @@ -20,6 +38,7 @@ def get_monitor(region, monitor_num, orientation): 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 diff --git a/remarkable_mouse/evdev.py b/remarkable_mouse/evdev.py index 19af7e0..4de0711 100644 --- a/remarkable_mouse/evdev.py +++ b/remarkable_mouse/evdev.py @@ -8,7 +8,7 @@ import libevdev from .codes import codes, types -from .common import get_monitor, remap, wacom_max_x, wacom_max_y, log_event +from .common import get_monitor, remap, wacom_max_x, wacom_max_y, log_event, get_current_monitor_num logging.basicConfig(format='%(message)s') log = logging.getLogger('remouse') @@ -76,7 +76,7 @@ def create_local_device(): return device.create_uinput_device() -def read_tablet(rm_inputs, *, orientation, monitor_num, region, threshold, mode): +def read_tablet(rm_inputs, *, orientation, monitor_num, region, threshold, mode, auto_monitor, monitor_update): """Pipe rM evdev events to local device Args: @@ -97,10 +97,15 @@ def read_tablet(rm_inputs, *, orientation, monitor_num, region, threshold, mode) x = y = 0 + # loop inputs forever # for input_name, stream in cycle(rm_inputs.items()): stream = rm_inputs['pen'] while True: + if auto_monitor and monitor_update[0] != monitor_num: + monitor_num=monitor_update[0] + monitor, (tot_width, tot_height) = get_monitor(region, monitor_num, orientation) + try: data = stream.read(16) except TimeoutError: diff --git a/remarkable_mouse/pen.py b/remarkable_mouse/pen.py new file mode 100644 index 0000000..00df643 --- /dev/null +++ b/remarkable_mouse/pen.py @@ -0,0 +1,204 @@ +import logging +import struct +import ctypes +import time +from screeninfo import get_monitors + +from .codes import codes, types +from .common import get_monitor, remap, wacom_max_x, wacom_max_y, log_event, get_current_monitor_num +from ctypes import * +from ctypes.wintypes import * + +logging.basicConfig(format='%(message)s') +log = logging.getLogger('remouse') +log.debug('Using pen injection') + +# Constants + +# Max values for linux / windows +MAX_ABS_PRESSURE=4095 +MAX_WIN_PRESSURE=1024 +MAX_ANGLE=90 +MAX_ABS_TILT=6300 + +# For penMask +PEN_MASK_NONE= 0x00000000 # Default +PEN_MASK_PRESSURE= 0x00000001 +PEN_MASK_ORIENTATION= 0x00000002 +PEN_MASK_TILT_X= 0x00000004 +PEN_MASK_TILT_Y= 0x00000008 + +# For penFlag +PEN_FLAG_NONE= 0x00000000 + +# For pointerType +PT_POINTER= 0x00000001 # All +PT_TOUCH= 0x00000002 +PT_PEN= 0x00000003 +PT_MOUSE= 0x00000004 + +#For pointerFlags +POINTER_FLAG_NONE= 0x00000000 # Default +POINTER_FLAG_NEW= 0x00000001 +POINTER_FLAG_INRANGE= 0x00000002 +POINTER_FLAG_INCONTACT= 0x00000004 +POINTER_FLAG_FIRSTBUTTON= 0x00000010 +POINTER_FLAG_SECONDBUTTON=0x00000020 +POINTER_FLAG_THIRDBUTTON= 0x00000040 +POINTER_FLAG_FOURTHBUTTON=0x00000080 +POINTER_FLAG_FIFTHBUTTON= 0x00000100 +POINTER_FLAG_PRIMARY= 0x00002000 +POINTER_FLAG_CONFIDENCE= 0x00004000 +POINTER_FLAG_CANCELED= 0x00008000 +POINTER_FLAG_DOWN= 0x00010000 +POINTER_FLAG_UPDATE= 0x00020000 +POINTER_FLAG_UP= 0x00040000 +POINTER_FLAG_WHEEL= 0x00080000 +POINTER_FLAG_HWHEEL= 0x00100000 +POINTER_FLAG_CAPTURECHANGED=0x00200000 + +# Structs Needed +class POINTER_INFO(Structure): + _fields_=[("pointerType",c_uint32), + ("pointerId",c_uint32), + ("frameId",c_uint32), + ("pointerFlags",c_int), + ("sourceDevice",HANDLE), + ("hwndTarget",HWND), + ("ptPixelLocation",POINT), + ("ptHimetricLocation",POINT), + ("ptPixelLocationRaw",POINT), + ("ptHimetricLocationRaw",POINT), + ("dwTime",DWORD), + ("historyCount",c_uint32), + ("inputData",c_int32), + ("dwKeyStates",DWORD), + ("PerformanceCount",c_uint64), + ("ButtonChangeType",c_int) + ] + +class POINTER_PEN_INFO(Structure): + _fields_=[("pointerInfo",POINTER_INFO), + ("penFlags",c_int), + ("penMask",c_int), + ("pressure", c_uint32), + ("rotation", c_uint32), + ("tiltX", c_int32), + ("tiltY", c_int32)] + +class DUMMYUNIONNAME(Structure): + _fields_=[("penInfo",POINTER_PEN_INFO) + ] + +class POINTER_TYPE_INFO(Structure): + _fields_=[("type",c_uint32), + ("penInfo",POINTER_PEN_INFO) + ] + +# Initialize Pointer and Touch info +pointerInfo = POINTER_INFO(pointerType=PT_PEN, + pointerId=0, + ptPixelLocation=POINT(950, 540), + pointerFlags=POINTER_FLAG_NEW) +penInfo = POINTER_PEN_INFO(pointerInfo=pointerInfo, + penMask=(PEN_MASK_PRESSURE | PEN_MASK_TILT_X | PEN_MASK_TILT_Y), + pressure=0, + tiltX=0, + tiltY=0) + +pointerTypeInfo = POINTER_TYPE_INFO(type=PT_PEN, + penInfo=penInfo) + +device = windll.user32.CreateSyntheticPointerDevice(PT_PEN, 1, 1) +print("Initialized Pen Injection as number ", device) +currently_down = False + +def applyPen(x=0, y=0, pressure=0, tiltX=0, tiltY=0): + global currently_down + if pressure > 0: + pointerTypeInfo.penInfo.pointerInfo.pointerFlags = (POINTER_FLAG_DOWN if not currently_down else POINTER_FLAG_UPDATE | POINTER_FLAG_INRANGE | POINTER_FLAG_INCONTACT) + currently_down = True + else: + pointerTypeInfo.penInfo.pointerInfo.pointerFlags = (POINTER_FLAG_UP if currently_down==True else POINTER_FLAG_UPDATE | POINTER_FLAG_INRANGE) + currently_down = False + + pointerTypeInfo.penInfo.pointerInfo.ptPixelLocation.x = x + pointerTypeInfo.penInfo.pointerInfo.ptPixelLocation.y = y + pointerTypeInfo.penInfo.pressure = pressure + pointerTypeInfo.penInfo.tiltX = tiltX + pointerTypeInfo.penInfo.tiltY = tiltY + + result = windll.user32.InjectSyntheticPointerInput(device, byref(pointerTypeInfo), 1) + if (result == False) and (log.level == logging.DEBUG): + error_code = ctypes.get_last_error() + print(f"Failed trying to update pen input. Error code: {error_code}") + print(f"Error message: {ctypes.WinError(error_code).strerror}") + + +def read_tablet(rm_inputs, *, orientation, monitor_num, region, threshold, mode, auto_monitor, monitor_update): + """Loop forever and map evdev events to mouse + + Args: + rm_inputs (dictionary of paramiko.ChannelFile): dict of pen, button + and touch input streams + orientation (str): tablet orientation + monitor_num (int): monitor number to map to + region (boolean): whether to selection mapping region with region tool + threshold (int): pressure threshold + mode (str): mapping mode + """ + + + monitor, _ = get_monitor(region, monitor_num, orientation) + log.debug('Chose monitor: {}'.format(monitor)) + + x = y = mapped_x = mapped_y = press = mapped_press = tiltX = tiltY = 0 + + stream = rm_inputs['pen'] + + while True: + if auto_monitor and monitor_update[0] != monitor_num: + monitor_num=monitor_update[0] + monitor, _ = get_monitor(region, monitor_num, orientation) + + try: + data = stream.read(16) + except TimeoutError: + continue + + e_time, e_millis, e_type, e_code, e_value = struct.unpack('2IHHi', data) + + # handle x direction + if codes[e_type][e_code] == 'ABS_X': + x = e_value + + # handle y direction + if codes[e_type][e_code] == 'ABS_Y': + y = e_value + + # handle pressure + if codes[e_type][e_code] == 'ABS_PRESSURE': + press = e_value + mapped_press = int(press* (MAX_WIN_PRESSURE / MAX_ABS_PRESSURE)) + + # handle tilt + if codes[e_type][e_code] == 'ABS_TILT_X': + tiltX = int(e_value*( MAX_ANGLE / MAX_ABS_TILT )) + + if codes[e_type][e_code] == 'ABS_TILT_Y': + tiltY = int(e_value*( MAX_ANGLE /MAX_ABS_TILT )) + + if codes[e_type][e_code] == 'SYN_REPORT': + + mapped_x, mapped_y = remap( + x, y, + wacom_max_x, wacom_max_y, + monitor.width, monitor.height, + mode, orientation, + ) + + # handle draw + applyPen(max(int(monitor.x+mapped_x),0), max(int(monitor.y+mapped_y),0), mapped_press, tiltX, tiltY) + + if log.level == logging.DEBUG: + log_event(e_time, e_millis, e_type, e_code, e_value) \ No newline at end of file diff --git a/remarkable_mouse/pynput.py b/remarkable_mouse/pynput.py index c5c83cc..c073e7d 100644 --- a/remarkable_mouse/pynput.py +++ b/remarkable_mouse/pynput.py @@ -4,17 +4,21 @@ # from .codes import EV_SYN, EV_ABS, ABS_X, ABS_Y, BTN_TOUCH from .codes import codes -from .common import get_monitor, remap, wacom_max_x, wacom_max_y, log_event +from .common import get_monitor, remap, wacom_max_x, wacom_max_y, log_event, get_current_monitor_num + + + logging.basicConfig(format='%(message)s') log = logging.getLogger('remouse') + # wacom digitizer dimensions # touchscreen dimensions # finger_width = 767 # finger_height = 1023 -def read_tablet(rm_inputs, *, orientation, monitor_num, region, threshold, mode): +def read_tablet(rm_inputs, *, orientation, monitor_num, region, threshold, mode, auto_monitor, monitor_update): """Loop forever and map evdev events to mouse Args: @@ -25,34 +29,43 @@ def read_tablet(rm_inputs, *, orientation, monitor_num, region, threshold, mode) region (boolean): whether to selection mapping region with region tool threshold (int): pressure threshold mode (str): mapping mode + auto_monitor (str) """ from pynput.mouse import Button, Controller mouse = Controller() + monitor, _ = get_monitor(region, monitor_num, orientation) + log.debug('Chose monitor: {}'.format(monitor)) x = y = 0 - stream = rm_inputs['pen'] + + stream = rm_inputs['pen'] while True: + if auto_monitor and monitor_update[0] != monitor_num: + monitor_num=monitor_update[0] + monitor, _ = get_monitor(region, monitor_num, orientation) + try: data = stream.read(16) except TimeoutError: continue + e_time, e_millis, e_type, e_code, e_value = struct.unpack('2IHHi', data) # handle x direction if codes[e_type][e_code] == 'ABS_X': x = e_value - + # handle y direction if codes[e_type][e_code] == 'ABS_Y': - y = e_value - + y = e_value + # handle draw if codes[e_type][e_code] == 'BTN_TOUCH': if e_value == 1: @@ -67,10 +80,13 @@ def read_tablet(rm_inputs, *, orientation, monitor_num, region, threshold, mode) monitor.width, monitor.height, mode, orientation, ) + + mouse.move( monitor.x + mapped_x - mouse.position[0], monitor.y + mapped_y - mouse.position[1] ) + if log.level == logging.DEBUG: log_event(e_time, e_millis, e_type, e_code, e_value) diff --git a/remarkable_mouse/remarkable_mouse.py b/remarkable_mouse/remarkable_mouse.py index 69a6717..8c7da69 100755 --- a/remarkable_mouse/remarkable_mouse.py +++ b/remarkable_mouse/remarkable_mouse.py @@ -8,10 +8,14 @@ import struct from getpass import getpass from itertools import cycle - +import platform +import threading +from .common import get_current_monitor_num import paramiko import paramiko.agent import paramiko.config +import time + logging.basicConfig(format='%(message)s') log = logging.getLogger('remouse') @@ -20,6 +24,8 @@ config_path = os.path.expanduser('~/.ssh/config') +POLL_TIME = 0.2 + def open_rm_inputs(*, address, key, password): """ Open a remote input device via SSH. @@ -125,6 +131,7 @@ def use_key(key): return {'pen': pen, 'touch': touch, 'button': button} + def main(): try: parser = argparse.ArgumentParser(description="use reMarkable tablet as a mouse input") @@ -137,13 +144,17 @@ def main(): Fill: take up the entire monitor, but not necessarily the entire tablet. Stretch: take up both the entire tablet and monitor, but don't maintain aspect ratio.""") parser.add_argument('--orientation', default='right', choices=['top', 'left', 'right', 'bottom'], help="position of tablet buttons") - parser.add_argument('--monitor', default=0, type=int, metavar='NUM', help="monitor to output to") - parser.add_argument('--region', action='store_true', default=False, help="Use a GUI to position the output area. Overrides --monitor") + parser.add_argument('--monitor', default=-1, type=int, metavar='NUM', help="override automatic monitor selection") + parser.add_argument('--region', action='store_true', default=False, help="Use a GUI to position the output area. Overrides --monitor and automatic monitor selection") parser.add_argument('--threshold', metavar='THRESH', default=600, type=int, help="stylus pressure threshold (default 600)") parser.add_argument('--evdev', action='store_true', default=False, help="use evdev to support pen pressure (requires root, Linux only)") + parser.add_argument('--pen', action='store_true', default=False, help="use pen input to support pen pressure in windows") args = parser.parse_args() + + automonitor = args.monitor == -1 and not args.region + if args.debug: log.setLevel(logging.DEBUG) print('Debugging enabled...') @@ -159,22 +170,44 @@ def main(): ) print("Connected to", args.address) + + # ----- Handle events ----- + if args.evdev: from remarkable_mouse.evdev import read_tablet - + elif args.pen: + from remarkable_mouse.pen import read_tablet else: from remarkable_mouse.pynput import read_tablet - read_tablet( - rm_inputs, - orientation=args.orientation, - monitor_num=args.monitor, - region=args.region, - threshold=args.threshold, - mode=args.mode, - ) + + check=threading.Condition() + + monitor_num_obj = [args.monitor] + + th = threading.Thread(target=read_tablet, kwargs={ + 'rm_inputs':rm_inputs, + 'orientation':args.orientation, + 'monitor_num': args.monitor, + 'region':args.region, + 'threshold':args.threshold, + 'mode':args.mode, + 'auto_monitor':automonitor, + 'monitor_update': monitor_num_obj + }, + daemon=True) + th.start() + + # checking every time slows down pen movement too much + while(True): + time.sleep(POLL_TIME) + if automonitor: + new_monitor = get_current_monitor_num() + if new_monitor != monitor_num_obj[0]: + monitor_num_obj[0] = new_monitor + except PermissionError: log.error('Insufficient permissions for creating a virtual input device')