Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pressure sensing on Windows, dynamic monitor selection #90

Open
wants to merge 23 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 11 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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
```

5 changes: 5 additions & 0 deletions pyvenv.cfg
Original file line number Diff line number Diff line change
@@ -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
21 changes: 20 additions & 1 deletion remarkable_mouse/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import logging
import sys
from screeninfo import get_monitors, Monitor

from .codes import codes, types

logging.basicConfig(format='%(message)s')
Expand All @@ -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

Expand All @@ -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
Expand Down
9 changes: 7 additions & 2 deletions remarkable_mouse/evdev.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -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:
Expand All @@ -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:
Expand Down
204 changes: 204 additions & 0 deletions remarkable_mouse/pen.py
Original file line number Diff line number Diff line change
@@ -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)
Loading