diff options
-rw-r--r-- | kvmd/apps/otg/__init__.py | 27 | ||||
-rw-r--r-- | kvmd/plugins/hid/otg/__init__.py | 38 | ||||
-rw-r--r-- | kvmd/plugins/hid/otg/hid.py | 7 | ||||
-rw-r--r-- | kvmd/plugins/hid/otg/mouse.py | 169 |
4 files changed, 227 insertions, 14 deletions
diff --git a/kvmd/apps/otg/__init__.py b/kvmd/apps/otg/__init__.py index cd17252f..2d5350ea 100644 --- a/kvmd/apps/otg/__init__.py +++ b/kvmd/apps/otg/__init__.py @@ -20,6 +20,7 @@ # ========================================================================== # +import re import time import argparse @@ -66,7 +67,7 @@ def _check_config(config: Section) -> None: raise RuntimeError("Nothing to do") -def _cmd_start(config: Section) -> None: +def _cmd_start(config: Section) -> None: # pylint: disable=too-many-statements # https://www.kernel.org/doc/Documentation/usb/gadget_configfs.txt # https://www.isticktoit.net/?p=1383 @@ -115,6 +116,26 @@ def _cmd_start(config: Section) -> None: ) symlink(func_path, join(config_path, "hid.usb0")) + # https://github.com/NicoHood/HID/blob/0835e6a/src/SingleReport/SingleAbsoluteMouse.cpp + # Репорт взят отсюда ^^^, но изменен диапазон значений координат перемещений. + # Автор предлагает использовать -32768...32767, но семерка почему-то не хочет работать + # с отрицательными значениями координат, как не хочет хавать 65536 и 32768. + # Так что мы ей скармливаем диапазон 0...32767, и передаем рукожопам из микрософта привет, + # потому что линуксы прекрасно работают с любыми двухбайтовыми диапазонами. + func_path = join(gadget_path, "functions/hid.usb1") # Mouse + mkdir(func_path) + _write(join(func_path, "protocol"), "0") + _write(join(func_path, "subclass"), "0") + _write(join(func_path, "report_length"), "6") + with open(join(func_path, "report_desc"), "wb") as report_file: + report_file.write( + b"\x05\x01\x09\x02\xA1\x01\x05\x09\x19\x01\x29\x08" + b"\x15\x00\x25\x01\x95\x08\x75\x01\x81\x02\x05\x01\x09\x30" + b"\x09\x31\x16\x00\x00\x26\xFF\x7F\x75\x10\x95\x02\x81\x02" + b"\x09\x38\x15\x81\x25\x7f\x75\x08\x95\x01\x81\x06\xc0" + ) + symlink(func_path, join(config_path, "hid.usb1")) + if config.kvmd.msd.type == "otg": func_path = join(gadget_path, "functions/mass_storage.usb0") mkdir(func_path) @@ -141,14 +162,14 @@ def _cmd_stop(config: Section) -> None: config_path = join(gadget_path, "configs/c.1") for func in listdir(config_path): - if func.endswith(".usb0"): + if re.search(r"\.usb\d+$", func): unlink(join(config_path, func)) rmdir(join(config_path, "strings/0x409")) rmdir(config_path) funcs_path = join(gadget_path, "functions") for func in listdir(funcs_path): - if func.endswith(".usb0"): + if re.search(r"\.usb\d+$", func): rmdir(join(funcs_path, func)) rmdir(join(gadget_path, "strings/0x409")) diff --git a/kvmd/plugins/hid/otg/__init__.py b/kvmd/plugins/hid/otg/__init__.py index d17ad179..7129e5d4 100644 --- a/kvmd/plugins/hid/otg/__init__.py +++ b/kvmd/plugins/hid/otg/__init__.py @@ -37,6 +37,7 @@ from ....validators.os import valid_abs_path from .. import BaseHid from .keyboard import KeyboardProcess +from .mouse import MouseProcess # ===== @@ -44,11 +45,13 @@ class Plugin(BaseHid): def __init__( # pylint: disable=super-init-not-called self, keyboard: Dict[str, Any], + mouse: Dict[str, Any], noop: bool, state_poll: float, ) -> None: self.__keyboard_proc = KeyboardProcess(noop=noop, **keyboard) + self.__mouse_proc = MouseProcess(noop=noop, **mouse) self.__state_poll = state_poll @@ -64,19 +67,33 @@ class Plugin(BaseHid): "write_retries_delay": Option(0.1, type=valid_float_f01), }, + "mouse": { + "device": Option("", type=valid_abs_path, unpack_as="device_path"), + "select_timeout": Option(1.0, type=valid_float_f01), + "write_retries": Option(5, type=valid_int_f1), + "write_retries_delay": Option(0.1, type=valid_float_f01), + }, + "noop": Option(False, type=valid_bool), "state_poll": Option(0.1, type=valid_float_f01), } def start(self) -> None: self.__keyboard_proc.start() + self.__mouse_proc.start() def get_state(self) -> Dict: - return {"online": self.__keyboard_proc.is_online()} + keyboard_online = self.__keyboard_proc.is_online() + mouse_online = self.__mouse_proc.is_online() + return { + "online": (keyboard_online and mouse_online), + "keyboard": {"online": keyboard_online}, + "mouse": {"online": mouse_online}, + } async def poll_state(self) -> AsyncGenerator[Dict, None]: prev_state: Dict = {} - while self.__keyboard_proc.is_alive(): + while self.__keyboard_proc.is_alive() and self.__mouse_proc.is_alive(): state = self.get_state() if state != prev_state: yield self.get_state() @@ -85,23 +102,28 @@ class Plugin(BaseHid): async def reset(self) -> None: self.__keyboard_proc.send_reset_event() + self.__mouse_proc.send_reset_event() async def cleanup(self) -> None: - self.__keyboard_proc.cleanup() + try: + self.__keyboard_proc.cleanup() + finally: + self.__mouse_proc.cleanup() # ===== async def send_key_event(self, key: str, state: bool) -> None: self.__keyboard_proc.send_key_event(key, state) - async def send_mouse_move_event(self, to_x: int, to_y: int) -> None: - pass - async def send_mouse_button_event(self, button: str, state: bool) -> None: - pass + self.__mouse_proc.send_button_event(button, state) + + async def send_mouse_move_event(self, to_x: int, to_y: int) -> None: + self.__mouse_proc.send_move_event(to_x, to_y) async def send_mouse_wheel_event(self, delta_y: int) -> None: - pass + self.__mouse_proc.send_wheel_event(delta_y) async def clear_events(self) -> None: self.__keyboard_proc.send_clear_event() + self.__mouse_proc.send_clear_event() diff --git a/kvmd/plugins/hid/otg/hid.py b/kvmd/plugins/hid/otg/hid.py index e0cf1120..2d8b9245 100644 --- a/kvmd/plugins/hid/otg/hid.py +++ b/kvmd/plugins/hid/otg/hid.py @@ -126,11 +126,12 @@ class DeviceProcess(multiprocessing.Process): # pylint: disable=too-many-instan self.__online_shared.value = 1 return True else: - logger.error("HID-%s write error: written (%s) != report length (%d)", + logger.error("HID-%s write() error: written (%s) != report length (%d)", self.__name, written, len(report)) except Exception as err: if isinstance(err, OSError) and err.errno == errno.EAGAIN: # pylint: disable=no-member - logger.error("HID-%s is busy/unplugged: %s: %s", self.__name, type(err).__name__, err) # TODO debug + logger.error("HID-%s busy/unplugged (write): %s: %s", # TODO debug + self.__name, type(err).__name__, err) else: logger.exception("Can't write report to HID-%s", self.__name) @@ -164,7 +165,7 @@ class DeviceProcess(multiprocessing.Process): # pylint: disable=too-many-instan self.__online_shared.value = 1 return True else: - logger.error("HID-%s is busy/unplugged", self.__name) # TODO debug + logger.error("HID-%s is busy/unplugged (select)", self.__name) # TODO debug except Exception as err: logger.error("Can't select() HID-%s: %s: %s", self.__name, type(err).__name__, err) self._close_device() diff --git a/kvmd/plugins/hid/otg/mouse.py b/kvmd/plugins/hid/otg/mouse.py new file mode 100644 index 00000000..4132a1cc --- /dev/null +++ b/kvmd/plugins/hid/otg/mouse.py @@ -0,0 +1,169 @@ +# ========================================================================== # +# # +# KVMD - The main Pi-KVM daemon. # +# # +# Copyright (C) 2018 Maxim Devaev <[email protected]> # +# # +# This program is free software: you can redistribute it and/or modify # +# it under the terms of the GNU General Public License as published by # +# the Free Software Foundation, either version 3 of the License, or # +# (at your option) any later version. # +# # +# This program is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +# GNU General Public License for more details. # +# # +# You should have received a copy of the GNU General Public License # +# along with this program. If not, see <https://www.gnu.org/licenses/>. # +# # +# ========================================================================== # + + +import struct +import dataclasses + +from typing import Any + +from ....logging import get_logger + +from .hid import BaseEvent +from .hid import DeviceProcess + + +# ===== +_BUTTONS = { + "left": 0x1, + "right": 0x2, + "middle": 0x4, +} + + +# ===== +class _ClearEvent(BaseEvent): + pass + + +class _ResetEvent(BaseEvent): + pass + + [email protected](frozen=True) +class _ButtonEvent(BaseEvent): + code: int + state: bool + + [email protected](frozen=True) +class _MoveEvent(BaseEvent): + to_x: int + to_y: int + + def __post_init__(self) -> None: + assert -32768 <= self.to_x <= 32767 + assert -32768 <= self.to_y <= 32767 + + [email protected](frozen=True) +class _WheelEvent(BaseEvent): + delta_y: int + + def __post_init__(self) -> None: + assert -127 <= self.delta_y <= 127 + + +# ===== +class MouseProcess(DeviceProcess): + def __init__(self, **kwargs: Any) -> None: + super().__init__(name="mouse", **kwargs) + + self.__pressed_buttons: int = 0 + self.__x = 0 + self.__y = 0 + + def cleanup(self) -> None: + self._stop() + get_logger().info("Clearing HID-mouse events ...") + if self._ensure_device(): + try: + self._write_report(self.__make_report(0, self.__x, self.__y, 0)) # Release all buttons + finally: + self._close_device() + + def send_clear_event(self) -> None: + self._queue_event(_ClearEvent()) + + def send_reset_event(self) -> None: + self._queue_event(_ResetEvent()) + + def send_button_event(self, button: str, state: bool) -> None: + assert button in _BUTTONS + self._queue_event(_ButtonEvent(code=_BUTTONS[button], state=state)) + + def send_move_event(self, to_x: int, to_y: int) -> None: + self._queue_event(_MoveEvent(to_x, to_y)) + + def send_wheel_event(self, delta_y: int) -> None: + self._queue_event(_WheelEvent(delta_y)) + + # ===== + + def _process_event(self, event: BaseEvent) -> None: + if isinstance(event, _ClearEvent): + self.__process_clear_event() + elif isinstance(event, _ResetEvent): + self.__process_clear_event(reopen=True) + elif isinstance(event, _ButtonEvent): + self.__process_button_event(event) + elif isinstance(event, _MoveEvent): + self.__process_move_event(event) + elif isinstance(event, _WheelEvent): + self.__process_wheel_event(event) + + def __process_clear_event(self, reopen: bool=False) -> None: + self.__clear_state() + if reopen: + self._close_device() + self.__send_current_state(0) + + def __process_button_event(self, event: _ButtonEvent) -> None: + if event.code & self.__pressed_buttons: + # Ранее нажатую кнопку отжимаем + self.__pressed_buttons &= ~event.code + if not self.__send_current_state(0): + return + if event.state: + # Нажимаем если нужно + self.__pressed_buttons |= event.code + self.__send_current_state(0) + + def __process_move_event(self, event: _MoveEvent) -> None: + self.__x = event.to_x + self.__y = event.to_y + self.__send_current_state(0) + + def __process_wheel_event(self, event: _WheelEvent) -> None: + self.__send_current_state(event.delta_y) + + def __send_current_state(self, delta_y: int) -> bool: + ok = False + if self._ensure_device(): + ok = self._write_report(self.__make_report( + buttons=self.__pressed_buttons, + to_x=self.__x, + to_y=self.__y, + delta_y=delta_y, + )) + if not ok: + self.__clear_state() + return ok + + def __clear_state(self) -> None: + self.__pressed_buttons = 0 + self.__x = 0 + self.__y = 0 + + def __make_report(self, buttons: int, to_x: int, to_y: int, delta_y: int) -> bytes: + to_x = (to_x + 32768) // 2 + to_y = (to_y + 32768) // 2 + return struct.pack("<BHHb", buttons, to_x, to_y, delta_y) |