Separate libevdev, pynput solutions into modules
This commit is contained in:
120
remarkable_mouse/evdev.py
Normal file
120
remarkable_mouse/evdev.py
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
import logging
|
||||||
|
import struct
|
||||||
|
|
||||||
|
logging.basicConfig(format='%(message)s')
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Maximum value that can be reported by the Wacom driver for the X axis
|
||||||
|
MAX_ABS_X = 20967
|
||||||
|
|
||||||
|
# Maximum value that can be reported by the Wacom driver for the Y axis
|
||||||
|
MAX_ABS_Y = 15725
|
||||||
|
|
||||||
|
|
||||||
|
def create_local_device():
|
||||||
|
"""
|
||||||
|
Create a virtual input device on this host that has the same
|
||||||
|
characteristics as a Wacom tablet.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
virtual input device
|
||||||
|
"""
|
||||||
|
import libevdev
|
||||||
|
device = libevdev.Device()
|
||||||
|
|
||||||
|
# Set device properties to emulate those of Wacom tablets
|
||||||
|
device.name = 'reMarkable tablet'
|
||||||
|
device.id = {
|
||||||
|
'bustype': 24,
|
||||||
|
'vendor': 1386,
|
||||||
|
'product': 0,
|
||||||
|
'version': 54
|
||||||
|
}
|
||||||
|
|
||||||
|
# Enable buttons supported by the digitizer
|
||||||
|
device.enable(libevdev.EV_KEY.BTN_TOOL_PEN)
|
||||||
|
device.enable(libevdev.EV_KEY.BTN_TOOL_RUBBER)
|
||||||
|
device.enable(libevdev.EV_KEY.BTN_TOUCH)
|
||||||
|
device.enable(libevdev.EV_KEY.BTN_STYLUS)
|
||||||
|
device.enable(libevdev.EV_KEY.BTN_STYLUS2)
|
||||||
|
|
||||||
|
# Enable position, tilt, distance and pressure change events
|
||||||
|
device.enable(
|
||||||
|
libevdev.EV_ABS.ABS_X,
|
||||||
|
libevdev.InputAbsInfo(
|
||||||
|
minimum=0,
|
||||||
|
maximum=MAX_ABS_X
|
||||||
|
)
|
||||||
|
)
|
||||||
|
device.enable(
|
||||||
|
libevdev.EV_ABS.ABS_Y,
|
||||||
|
libevdev.InputAbsInfo(
|
||||||
|
minimum=0,
|
||||||
|
maximum=MAX_ABS_Y
|
||||||
|
)
|
||||||
|
)
|
||||||
|
device.enable(
|
||||||
|
libevdev.EV_ABS.ABS_PRESSURE,
|
||||||
|
libevdev.InputAbsInfo(
|
||||||
|
minimum=0,
|
||||||
|
maximum=4095
|
||||||
|
)
|
||||||
|
)
|
||||||
|
device.enable(
|
||||||
|
libevdev.EV_ABS.ABS_DISTANCE,
|
||||||
|
libevdev.InputAbsInfo(
|
||||||
|
minimum=0,
|
||||||
|
maximum=255
|
||||||
|
)
|
||||||
|
)
|
||||||
|
device.enable(
|
||||||
|
libevdev.EV_ABS.ABS_TILT_X,
|
||||||
|
libevdev.InputAbsInfo(
|
||||||
|
minimum=-9000,
|
||||||
|
maximum=9000
|
||||||
|
)
|
||||||
|
)
|
||||||
|
device.enable(
|
||||||
|
libevdev.EV_ABS.ABS_TILT_Y,
|
||||||
|
libevdev.InputAbsInfo(
|
||||||
|
minimum=-9000,
|
||||||
|
maximum=9000
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return device.create_uinput_device()
|
||||||
|
|
||||||
|
|
||||||
|
def pipe_device(args, remote_device, local_device):
|
||||||
|
"""
|
||||||
|
Pipe events from a remote device to a local device.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
args: argparse arguments
|
||||||
|
remote_device (paramiko.ChannelFile): read-only stream of input events
|
||||||
|
local_device: local virtual input device to write events to
|
||||||
|
"""
|
||||||
|
import libevdev
|
||||||
|
# While debug mode is active, we log events grouped together between
|
||||||
|
# SYN_REPORT events. Pending events for the next log are stored here
|
||||||
|
pending_events = []
|
||||||
|
|
||||||
|
while True:
|
||||||
|
e_time, e_millis, e_type, e_code, e_value = struct.unpack('2IHHi', remote_device.read(16))
|
||||||
|
e_bit = libevdev.evbit(e_type, e_code)
|
||||||
|
event = libevdev.InputEvent(e_bit, value=e_value)
|
||||||
|
|
||||||
|
local_device.send_events([event])
|
||||||
|
|
||||||
|
if args.debug:
|
||||||
|
if e_bit == libevdev.EV_SYN.SYN_REPORT:
|
||||||
|
event_repr = ', '.join(
|
||||||
|
'{} = {}'.format(
|
||||||
|
event.code.name,
|
||||||
|
event.value
|
||||||
|
) for event in pending_events
|
||||||
|
)
|
||||||
|
log.debug('{}.{:0>6} - {}'.format(e_time, e_millis, event_repr))
|
||||||
|
pending_events = []
|
||||||
|
else:
|
||||||
|
pending_events += [event]
|
||||||
101
remarkable_mouse/pynput.py
Normal file
101
remarkable_mouse/pynput.py
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
import logging
|
||||||
|
import struct
|
||||||
|
|
||||||
|
logging.basicConfig(format='%(message)s')
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
stylus_width = 15725
|
||||||
|
stylus_height = 20951
|
||||||
|
# finger_width = 767
|
||||||
|
# finger_height = 1023
|
||||||
|
|
||||||
|
|
||||||
|
# remap wacom coordinates in various orientations
|
||||||
|
def fit(x, y, stylus_width, stylus_height, monitor, orientation):
|
||||||
|
|
||||||
|
if orientation == 'vertical':
|
||||||
|
y = stylus_height - y
|
||||||
|
elif orientation == 'right':
|
||||||
|
x, y = y, x
|
||||||
|
stylus_width, stylus_height = stylus_height, stylus_width
|
||||||
|
elif orientation == 'left':
|
||||||
|
x, y = stylus_height - y, stylus_width - x
|
||||||
|
stylus_width, stylus_height = stylus_height, stylus_width
|
||||||
|
|
||||||
|
ratio_width, ratio_height = monitor.width / stylus_width, monitor.height / stylus_height
|
||||||
|
scaling = ratio_width if ratio_width > ratio_height else ratio_height
|
||||||
|
|
||||||
|
return (
|
||||||
|
scaling * (x - (stylus_width - monitor.width / scaling) / 2),
|
||||||
|
scaling * (y - (stylus_height - monitor.height / scaling) / 2)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def read_tablet(args, remote_device):
|
||||||
|
"""Loop forever and map evdev events to mouse"""
|
||||||
|
|
||||||
|
from screeninfo import get_monitors
|
||||||
|
from pynput.mouse import Button, Controller
|
||||||
|
|
||||||
|
lifted = True
|
||||||
|
new_x = new_y = False
|
||||||
|
|
||||||
|
mouse = Controller()
|
||||||
|
|
||||||
|
monitor = get_monitors()[args.monitor]
|
||||||
|
log.debug('Chose monitor: {}'.format(monitor))
|
||||||
|
|
||||||
|
while True:
|
||||||
|
_, _, e_type, e_code, e_value = struct.unpack('2IHHi', remote_device.read(16))
|
||||||
|
|
||||||
|
if e_type == e_type_abs:
|
||||||
|
|
||||||
|
# handle x direction
|
||||||
|
if e_code == e_code_stylus_xpos:
|
||||||
|
log.debug(e_value)
|
||||||
|
x = e_value
|
||||||
|
new_x = True
|
||||||
|
|
||||||
|
# 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 > args.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 = fit(x, y, stylus_width, stylus_height, monitor, args.orientation)
|
||||||
|
mouse.move(
|
||||||
|
monitor.x + mapped_x - mouse.position[0],
|
||||||
|
monitor.y + mapped_y - mouse.position[1]
|
||||||
|
)
|
||||||
|
new_x = new_y = False
|
||||||
@@ -14,133 +14,16 @@ import paramiko
|
|||||||
logging.basicConfig(format='%(message)s')
|
logging.basicConfig(format='%(message)s')
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
# Maximum value that can be reported by the Wacom driver for the X axis
|
|
||||||
MAX_ABS_X = 20967
|
|
||||||
|
|
||||||
# Maximum value that can be reported by the Wacom driver for the Y axis
|
|
||||||
MAX_ABS_Y = 15725
|
|
||||||
|
|
||||||
|
|
||||||
# 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
|
|
||||||
|
|
||||||
stylus_width = 15725
|
|
||||||
stylus_height = 20951
|
|
||||||
# finger_width = 767
|
|
||||||
# finger_height = 1023
|
|
||||||
|
|
||||||
|
|
||||||
# remap wacom coordinates in various orientations
|
|
||||||
def fit(x, y, stylus_width, stylus_height, monitor, orientation):
|
|
||||||
|
|
||||||
if orientation == 'vertical':
|
|
||||||
y = stylus_height - y
|
|
||||||
elif orientation == 'right':
|
|
||||||
x, y = y, x
|
|
||||||
stylus_width, stylus_height = stylus_height, stylus_width
|
|
||||||
elif orientation == 'left':
|
|
||||||
x, y = stylus_height - y, stylus_width - x
|
|
||||||
stylus_width, stylus_height = stylus_height, stylus_width
|
|
||||||
|
|
||||||
ratio_width, ratio_height = monitor.width / stylus_width, monitor.height / stylus_height
|
|
||||||
scaling = ratio_width if ratio_width > ratio_height else ratio_height
|
|
||||||
|
|
||||||
return (
|
|
||||||
scaling * (x - (stylus_width - monitor.width / scaling) / 2),
|
|
||||||
scaling * (y - (stylus_height - monitor.height / scaling) / 2)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def create_local_device():
|
|
||||||
"""
|
|
||||||
Create a virtual input device on this host that has the same
|
|
||||||
characteristics as a Wacom tablet.
|
|
||||||
|
|
||||||
:returns: virtual input device
|
|
||||||
"""
|
|
||||||
import libevdev
|
|
||||||
device = libevdev.Device()
|
|
||||||
|
|
||||||
# Set device properties to emulate those of Wacom tablets
|
|
||||||
device.name = 'reMarkable tablet'
|
|
||||||
device.id = {
|
|
||||||
'bustype': 24,
|
|
||||||
'vendor': 1386,
|
|
||||||
'product': 0,
|
|
||||||
'version': 54
|
|
||||||
}
|
|
||||||
|
|
||||||
# Enable buttons supported by the digitizer
|
|
||||||
device.enable(libevdev.EV_KEY.BTN_TOOL_PEN)
|
|
||||||
device.enable(libevdev.EV_KEY.BTN_TOOL_RUBBER)
|
|
||||||
device.enable(libevdev.EV_KEY.BTN_TOUCH)
|
|
||||||
device.enable(libevdev.EV_KEY.BTN_STYLUS)
|
|
||||||
device.enable(libevdev.EV_KEY.BTN_STYLUS2)
|
|
||||||
|
|
||||||
# Enable position, tilt, distance and pressure change events
|
|
||||||
device.enable(
|
|
||||||
libevdev.EV_ABS.ABS_X,
|
|
||||||
libevdev.InputAbsInfo(
|
|
||||||
minimum=0,
|
|
||||||
maximum=MAX_ABS_X
|
|
||||||
)
|
|
||||||
)
|
|
||||||
device.enable(
|
|
||||||
libevdev.EV_ABS.ABS_Y,
|
|
||||||
libevdev.InputAbsInfo(
|
|
||||||
minimum=0,
|
|
||||||
maximum=MAX_ABS_Y
|
|
||||||
)
|
|
||||||
)
|
|
||||||
device.enable(
|
|
||||||
libevdev.EV_ABS.ABS_PRESSURE,
|
|
||||||
libevdev.InputAbsInfo(
|
|
||||||
minimum=0,
|
|
||||||
maximum=4095
|
|
||||||
)
|
|
||||||
)
|
|
||||||
device.enable(
|
|
||||||
libevdev.EV_ABS.ABS_DISTANCE,
|
|
||||||
libevdev.InputAbsInfo(
|
|
||||||
minimum=0,
|
|
||||||
maximum=255
|
|
||||||
)
|
|
||||||
)
|
|
||||||
device.enable(
|
|
||||||
libevdev.EV_ABS.ABS_TILT_X,
|
|
||||||
libevdev.InputAbsInfo(
|
|
||||||
minimum=-9000,
|
|
||||||
maximum=9000
|
|
||||||
)
|
|
||||||
)
|
|
||||||
device.enable(
|
|
||||||
libevdev.EV_ABS.ABS_TILT_Y,
|
|
||||||
libevdev.InputAbsInfo(
|
|
||||||
minimum=-9000,
|
|
||||||
maximum=9000
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
return device.create_uinput_device()
|
|
||||||
|
|
||||||
def open_remote_device(args, file='/dev/input/event0'):
|
def open_remote_device(args, file='/dev/input/event0'):
|
||||||
"""
|
"""
|
||||||
Open a remote input device via SSH.
|
Open a remote input device via SSH.
|
||||||
|
|
||||||
:param args: command-line arguments
|
Args:
|
||||||
:param file: path to the input device on the device
|
args: argparse arguments
|
||||||
:returns: read-only stream of input events
|
file (str): path to the input device on the device
|
||||||
|
Returns:
|
||||||
|
(paramiko.ChannelFile): read-only stream of input events
|
||||||
"""
|
"""
|
||||||
log.info("Connecting to input '{}' on '{}'".format(file, args.address))
|
log.info("Connecting to input '{}' on '{}'".format(file, args.address))
|
||||||
|
|
||||||
@@ -180,97 +63,6 @@ def open_remote_device(args, file='/dev/input/event0'):
|
|||||||
|
|
||||||
return stdout
|
return stdout
|
||||||
|
|
||||||
def pipe_device(args, remote_device, local_device):
|
|
||||||
"""
|
|
||||||
Pipe events from a remote device to a local device.
|
|
||||||
|
|
||||||
:param args: command-line arguments
|
|
||||||
:param remote_device: stream of events to read from
|
|
||||||
:param local_device: local virtual device to write events to
|
|
||||||
"""
|
|
||||||
import libevdev
|
|
||||||
# While debug mode is active, we log events grouped together between
|
|
||||||
# SYN_REPORT events. Pending events for the next log are stored here
|
|
||||||
pending_events = []
|
|
||||||
|
|
||||||
while True:
|
|
||||||
e_time, e_millis, e_type, e_code, e_value = struct.unpack('2IHHi', remote_device.read(16))
|
|
||||||
e_bit = libevdev.evbit(e_type, e_code)
|
|
||||||
event = libevdev.InputEvent(e_bit, value=e_value)
|
|
||||||
|
|
||||||
local_device.send_events([event])
|
|
||||||
|
|
||||||
if args.debug:
|
|
||||||
if e_bit == libevdev.EV_SYN.SYN_REPORT:
|
|
||||||
event_repr = ', '.join(
|
|
||||||
'{} = {}'.format(
|
|
||||||
event.code.name,
|
|
||||||
event.value
|
|
||||||
) for event in pending_events
|
|
||||||
)
|
|
||||||
log.debug('{}.{:0>6} - {}'.format(e_time, e_millis, event_repr))
|
|
||||||
pending_events = []
|
|
||||||
else:
|
|
||||||
pending_events += [event]
|
|
||||||
|
|
||||||
|
|
||||||
def read_tablet(args):
|
|
||||||
"""Loop forever and map evdev events to mouse"""
|
|
||||||
|
|
||||||
from screeninfo import get_monitors
|
|
||||||
from pynput.mouse import Button, Controller
|
|
||||||
|
|
||||||
lifted = True
|
|
||||||
new_x = new_y = False
|
|
||||||
|
|
||||||
mouse = Controller()
|
|
||||||
|
|
||||||
monitor = get_monitors()[args.monitor]
|
|
||||||
log.debug('Chose monitor: {}'.format(monitor))
|
|
||||||
|
|
||||||
stdout = open_remote_device(args)
|
|
||||||
|
|
||||||
while True:
|
|
||||||
_, _, e_type, e_code, e_value = struct.unpack('2IHHi', stdout.read(16))
|
|
||||||
|
|
||||||
if e_type == e_type_abs:
|
|
||||||
|
|
||||||
# handle x direction
|
|
||||||
if e_code == e_code_stylus_xpos:
|
|
||||||
log.debug(e_value)
|
|
||||||
x = e_value
|
|
||||||
new_x = True
|
|
||||||
|
|
||||||
# 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 > args.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 = fit(x, y, stylus_width, stylus_height, monitor, args.orientation)
|
|
||||||
mouse.move(
|
|
||||||
monitor.x + mapped_x - mouse.position[0],
|
|
||||||
monitor.y + mapped_y - mouse.position[1]
|
|
||||||
)
|
|
||||||
new_x = new_y = False
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
try:
|
try:
|
||||||
@@ -282,10 +74,12 @@ def main():
|
|||||||
parser.add_argument('--orientation', default='left', choices=['vertical', 'left', 'right'])
|
parser.add_argument('--orientation', default='left', choices=['vertical', 'left', 'right'])
|
||||||
parser.add_argument('--monitor', default=0, type=int, metavar='NUM', help="monitor to use")
|
parser.add_argument('--monitor', default=0, type=int, metavar='NUM', help="monitor to use")
|
||||||
parser.add_argument('--threshold', default=1000, type=int, help="stylus pressure threshold (default 1000)")
|
parser.add_argument('--threshold', default=1000, type=int, help="stylus pressure threshold (default 1000)")
|
||||||
parser.add_argument('--evdev', action='store_true', default=False, help="use evdev to support pen tilt (requires root, libev)")
|
parser.add_argument('--evdev', action='store_true', default=False, help="use evdev to support pen tilt (requires root, no OSX support)")
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
remote_device = open_remote_device(args)
|
||||||
|
|
||||||
if args.debug:
|
if args.debug:
|
||||||
logging.getLogger('').setLevel(logging.DEBUG)
|
logging.getLogger('').setLevel(logging.DEBUG)
|
||||||
log.setLevel(logging.DEBUG)
|
log.setLevel(logging.DEBUG)
|
||||||
@@ -294,6 +88,8 @@ def main():
|
|||||||
log.setLevel(logging.INFO)
|
log.setLevel(logging.INFO)
|
||||||
|
|
||||||
if args.evdev:
|
if args.evdev:
|
||||||
|
from remarkable_mouse.evdev import create_local_device, pipe_device
|
||||||
|
|
||||||
try:
|
try:
|
||||||
local_device = create_local_device()
|
local_device = create_local_device()
|
||||||
log.info("Created virtual input device '{}'".format(local_device.devnode))
|
log.info("Created virtual input device '{}'".format(local_device.devnode))
|
||||||
@@ -302,10 +98,12 @@ def main():
|
|||||||
log.error('Make sure you run this program as root')
|
log.error('Make sure you run this program as root')
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
remote_device = open_remote_device(args)
|
|
||||||
pipe_device(args, remote_device, local_device)
|
pipe_device(args, remote_device, local_device)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
read_tablet(args)
|
from remarkable_mouse.pynput import read_tablet
|
||||||
|
read_tablet(args, remote_device)
|
||||||
|
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
pass
|
pass
|
||||||
except EOFError:
|
except EOFError:
|
||||||
|
|||||||
Reference in New Issue
Block a user