Browse Source

Separate libevdev, pynput solutions into modules

master
evan 5 years ago
parent
commit
e86a5dbe42
  1. 120
      remarkable_mouse/evdev.py
  2. 101
      remarkable_mouse/pynput.py
  3. 230
      remarkable_mouse/remarkable_mouse.py
  4. 4
      setup.py

120
remarkable_mouse/evdev.py

@ -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

@ -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

230
remarkable_mouse/remarkable_mouse.py

@ -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:

4
setup.py

@ -20,7 +20,9 @@ setup(
}, },
install_requires=[ install_requires=[
'paramiko', 'paramiko',
'libevdev' 'libevdev',
'pynput',
'screeninfo'
], ],
classifiers=[ classifiers=[
"Programming Language :: Python :: 3", "Programming Language :: Python :: 3",

Loading…
Cancel
Save