diff --git a/README.md b/README.md index 4b77261..386fe72 100644 --- a/README.md +++ b/README.md @@ -94,6 +94,9 @@ which use the device may be confused by that and could crash. `networktablet` will display a status line for every touch/motion event it receives. +Addendum: + +The `python-app` directory contains an optional program implementing a workaround for the common problem of `networktablet` executing and connecting and receiving input events but GIMP being unable to see a device named "Network Tablet". You may need to install the dependencies with `pip3 install -r requirements.txt` Part 2: App ----------- diff --git a/python-app/main.py b/python-app/main.py new file mode 100755 index 0000000..09545dc --- /dev/null +++ b/python-app/main.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python3 +import subprocess +import pyautogui +from collections import defaultdict + + +INTMAX = 65536 +WIDTH, HEIGHT = pyautogui.size() +ASPECT_RATIOS = defaultdict(lambda: (16, 9)) +default_ratios = [ + ("Galaxy Note 4", (16, 9)), +] + + +class Event: + + def __str__(self): + return "{}({})".format( + self.__class__.__name__, + ', '.join( + ["{}={}".format(k, v) for k, v in self.__dict__.items()])) + + +class PositionEvent(Event): + """Model of position events.""" + + def __init__(self, x, y, pressure): + super().__init__() + self.x = x + self.y = y + self.pressure = pressure + + +class ButtonEvent(Event): + """Model of button events.""" + + def __init__(self, button_id, button_status): + super().__init__() + self.id = button_id + self.status = button_status + + +class PositionManager: + """Model for handling relative vs. absolute movement and track button states.""" + + def __init__(self, device, aspect_ratio=None): + if aspect_ratio is None: + try: + r = ASPECT_RATIOS[device] + self.xscale = r[0] / r[1] + except KeyError: + raise ValueError("Sorry, that is an unsupported device.") + self.last_pos_event = None + self._deltas = [0, 0] + # only track movement delta when btn id -1 state is 1 + self.button_states = defaultdict(lambda: 0) + + def consume(self, event): + if isinstance(event, PositionEvent): + if self.last_pos_event is not None: + # skip non-relative movement events + if self.button_states.get(-1, None) == 1: + self._deltas[0] = ( + event.x - self.last_pos_event.x) * self.xscale * HEIGHT / INTMAX + self._deltas[1] = ( + event.y - self.last_pos_event.y) * HEIGHT / INTMAX + self.last_pos_event = event + elif isinstance(event, ButtonEvent): + self.button_states[event.id] = event.status + return tuple(self._deltas) + + @property + def deltas(self): + return tuple(self._deltas) + + +def process_line(l): + if l.startswith("."): + return PositionEvent(*[int(i.split(":")[1].strip()) for i in l.split(",")]) + elif l.startswith("sent button"): + ss = l.split(":")[1].strip() + return ButtonEvent(*[int(i.strip()) for i in ss.split(",")]) + else: + raise ValueError("Invalid command string: {}".format(l)) + + +def main(): + # necessary to prevent pyautogui calls from causing unnecessary delays + pyautogui.PAUSE = 0 + cmd = ["../driver-uinput/networktablet"] + try: + p = subprocess.Popen(cmd, stdout=subprocess.PIPE) + except FileNotFoundError: + print("ERROR: Did you build the binary?") + quit() + + movement_manager = PositionManager("Galaxy Note 4") + while True: + line = p.stdout.readline().decode("utf-8") + if not line: + break + try: + e = process_line(line) + print(e) + movement_manager.consume(e) + dx, dy = movement_manager.deltas + pyautogui.moveRel(dx, dy, 0) + + if movement_manager.button_states.get(0, None) == 1: + pyautogui.mouseDown(button="left") + else: + pyautogui.mouseUp(button="left") + except ValueError: + pass + + +if __name__ == '__main__': + main() diff --git a/python-app/requirements.txt b/python-app/requirements.txt new file mode 100644 index 0000000..1f21f99 --- /dev/null +++ b/python-app/requirements.txt @@ -0,0 +1 @@ +pyautogui