diff options
author | Maxim Devaev <[email protected]> | 2021-07-23 05:11:04 +0300 |
---|---|---|
committer | Maxim Devaev <[email protected]> | 2021-07-23 05:17:00 +0300 |
commit | cbc3a4ceef781a6f40a7bd079b0dab470159f31d (patch) | |
tree | 8795fcc755033840c804d40a6e2de0ac0f49688e /kvmd | |
parent | bc73e74161f61f3d56f7395fec161febe65e93e6 (diff) |
rewrite otg hid
Diffstat (limited to 'kvmd')
-rw-r--r-- | kvmd/plugins/hid/otg/__init__.py | 22 | ||||
-rw-r--r-- | kvmd/plugins/hid/otg/device.py | 126 | ||||
-rw-r--r-- | kvmd/plugins/hid/otg/keyboard.py | 50 | ||||
-rw-r--r-- | kvmd/plugins/hid/otg/mouse.py | 57 |
4 files changed, 119 insertions, 136 deletions
diff --git a/kvmd/plugins/hid/otg/__init__.py b/kvmd/plugins/hid/otg/__init__.py index 9a9a2aef..779c8417 100644 --- a/kvmd/plugins/hid/otg/__init__.py +++ b/kvmd/plugins/hid/otg/__init__.py @@ -63,20 +63,18 @@ class Plugin(BaseHid): def get_plugin_options(cls) -> Dict: return { "keyboard": { - "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), - "reopen_delay": Option(0.5, type=valid_float_f01), + "device": Option("", type=valid_abs_path, unpack_as="device_path"), + "select_timeout": Option(0.1, type=valid_float_f01), + "queue_timeout": Option(0.1, type=valid_float_f01), + "write_retries": Option(150, type=valid_int_f1), }, "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), - "reopen_delay": Option(0.5, type=valid_float_f01), - "absolute": Option(True, type=valid_bool), - "horizontal_wheel": Option(True, type=valid_bool), + "device": Option("", type=valid_abs_path, unpack_as="device_path"), + "select_timeout": Option(0.1, type=valid_float_f01), + "queue_timeout": Option(0.1, type=valid_float_f01), + "write_retries": Option(150, type=valid_int_f1), + "absolute": Option(True, type=valid_bool), + "horizontal_wheel": Option(True, type=valid_bool), }, "noop": Option(False, type=valid_bool), } diff --git a/kvmd/plugins/hid/otg/device.py b/kvmd/plugins/hid/otg/device.py index 28feece8..95795f1e 100644 --- a/kvmd/plugins/hid/otg/device.py +++ b/kvmd/plugins/hid/otg/device.py @@ -28,6 +28,7 @@ import errno import time from typing import Dict +from typing import Generator from ....logging import get_logger @@ -52,9 +53,8 @@ class BaseDeviceProcess(multiprocessing.Process): # pylint: disable=too-many-in device_path: str, select_timeout: float, + queue_timeout: float, write_retries: int, - write_retries_delay: float, - reopen_delay: float, noop: bool, ) -> None: @@ -67,9 +67,8 @@ class BaseDeviceProcess(multiprocessing.Process): # pylint: disable=too-many-in self.__device_path = device_path self.__select_timeout = select_timeout + self.__queue_timeout = queue_timeout self.__write_retries = write_retries - self.__write_retries_delay = write_retries_delay - self.__reopen_delay = reopen_delay self.__noop = noop self.__fd = -1 @@ -77,26 +76,39 @@ class BaseDeviceProcess(multiprocessing.Process): # pylint: disable=too-many-in self.__state_flags = aiomulti.AioSharedFlags({"online": True, **initial_state}, notifier) self.__stop_event = multiprocessing.Event() - def run(self) -> None: + def run(self) -> None: # pylint: disable=too-many-branches logger = aioproc.settle(f"HID-{self.__name}", f"hid-{self.__name}") + report = b"" + retries = 0 while not self.__stop_event.is_set(): try: while not self.__stop_event.is_set(): - if self.__ensure_device(): # Check device and process reports if needed + if self.__ensure_device(): self.__read_all_reports() + try: - event = self.__events_queue.get(timeout=0.1) + event = self.__events_queue.get(timeout=self.__queue_timeout) except queue.Empty: if not self.__udc.can_operate(): - self._clear_queue() - self.__close_device() + self.__state_flags.update(online=False) else: - if not self._process_event(event): - self._clear_queue() + # Посылка свежих репортов важнее старого + for report in self._process_event(event): + retries = self.__write_retries + if self.__ensure_device(): + if self.__write_report(report): + retries = 0 + continue + + # Повторение последнего репорта до победного или пока не кончатся попытки + if retries > 0 and self.__ensure_device(): + if self.__write_report(report): + retries = 0 + else: + retries -= 1 + except Exception: logger.exception("Unexpected HID-%s error", self.__name) - self._clear_queue() - self.__close_device() time.sleep(1) self.__close_device() @@ -106,8 +118,10 @@ class BaseDeviceProcess(multiprocessing.Process): # pylint: disable=too-many-in # ===== - def _process_event(self, event: BaseEvent) -> bool: - raise NotImplementedError + def _process_event(self, event: BaseEvent) -> Generator[bytes, None, None]: # pylint: disable=unused-argument + if self is not None: # XXX: Vulture and pylint hack + raise NotImplementedError() + yield def _process_read_report(self, report: bytes) -> None: pass @@ -131,53 +145,43 @@ class BaseDeviceProcess(multiprocessing.Process): # pylint: disable=too-many-in def _clear_queue(self) -> None: tools.clear_queue(self.__events_queue) - def _ensure_write(self, report: bytes, reopen: bool=False, close: bool=False) -> bool: - if reopen: + def _cleanup_write(self, report: bytes) -> None: + assert not self.is_alive() + assert self.__fd < 0 + if self.__ensure_device(): + self.__write_report(report) self.__close_device() - try: - if self.__ensure_device(): - return self.__write_report(report) - return False - finally: - if close: - self.__close_device() # ===== def __write_report(self, report: bytes) -> bool: + assert report + if self.__noop: return True assert self.__fd >= 0 logger = get_logger() - retries = self.__write_retries - while retries: - try: - written = os.write(self.__fd, report) - if written == len(report): - self.__state_flags.update(online=True) - return True - else: - 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 ( - # https://github.com/raspberrypi/linux/commit/61b7f805dc2fd364e0df682de89227e94ce88e25 - err.errno == errno.EAGAIN # pylint: disable=no-member - or err.errno == errno.ESHUTDOWN # pylint: disable=no-member - ): - logger.debug("HID-%s busy/unplugged (write): %s", self.__name, tools.efmt(err)) - else: - logger.exception("Can't write report to HID-%s", self.__name) - - retries -= 1 - - if retries: - logger.debug("HID-%s write retries left: %d", self.__name, retries) - time.sleep(self.__write_retries_delay) + try: + written = os.write(self.__fd, report) + if written == len(report): + self.__state_flags.update(online=True) + return True + else: + 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 ( + # https://github.com/raspberrypi/linux/commit/61b7f805dc2fd364e0df682de89227e94ce88e25 + err.errno == errno.EAGAIN # pylint: disable=no-member + or err.errno == errno.ESHUTDOWN # pylint: disable=no-member + ): + logger.debug("HID-%s busy/unplugged (write): %s", self.__name, tools.efmt(err)) + else: + logger.exception("Can't write report to HID-%s", self.__name) - self.__close_device() + self.__state_flags.update(online=False) return False def __read_all_reports(self) -> None: @@ -213,27 +217,25 @@ class BaseDeviceProcess(multiprocessing.Process): # pylint: disable=too-many-in logger = get_logger() if self.__fd < 0: - if self.__udc.can_operate(): - try: - flags = os.O_NONBLOCK - flags |= (os.O_RDWR if self.__read_size else os.O_WRONLY) - self.__fd = os.open(self.__device_path, flags) - except Exception as err: - logger.error("Can't open HID-%s device %s: %s", self.__name, self.__device_path, tools.efmt(err)) - time.sleep(self.__reopen_delay) - else: - time.sleep(self.__reopen_delay) + try: + flags = os.O_NONBLOCK + flags |= (os.O_RDWR if self.__read_size else os.O_WRONLY) + self.__fd = os.open(self.__device_path, flags) + except Exception as err: + logger.error("Can't open HID-%s device %s: %s", + self.__name, self.__device_path, tools.efmt(err)) if self.__fd >= 0: try: if select.select([], [self.__fd], [], self.__select_timeout)[1]: - self.__state_flags.update(online=True) + # Закомментировано, потому что иногда запись доступна, но устройство отключено + # self.__state_flags.update(online=True) return True else: + # Если запись недоступна, то скорее всего устройство отключено logger.debug("HID-%s is busy/unplugged (write select)", self.__name) except Exception as err: logger.error("Can't select() for write HID-%s: %s", self.__name, tools.efmt(err)) - self.__close_device() self.__state_flags.update(online=False) return False diff --git a/kvmd/plugins/hid/otg/keyboard.py b/kvmd/plugins/hid/otg/keyboard.py index acc0381f..3225d8d2 100644 --- a/kvmd/plugins/hid/otg/keyboard.py +++ b/kvmd/plugins/hid/otg/keyboard.py @@ -24,6 +24,7 @@ from typing import Tuple from typing import List from typing import Set from typing import Iterable +from typing import Generator from typing import Optional from typing import Any @@ -61,7 +62,7 @@ class KeyboardProcess(BaseDeviceProcess): def cleanup(self) -> None: self._stop() get_logger().info("Clearing HID-keyboard events ...") - self._ensure_write(b"\x00" * 8, close=True) # Release all keys and modifiers + self._cleanup_write(b"\x00" * 8) # Release all keys and modifiers def send_clear_event(self) -> None: self._clear_queue() @@ -87,60 +88,49 @@ class KeyboardProcess(BaseDeviceProcess): # ===== - def _process_event(self, event: BaseEvent) -> bool: - if isinstance(event, ClearEvent): - return self.__process_clear_event() - elif isinstance(event, ResetEvent): - return self.__process_clear_event(reopen=True) + def _process_event(self, event: BaseEvent) -> Generator[bytes, None, None]: + if isinstance(event, (ClearEvent, ResetEvent)): + yield self.__process_clear_event() elif isinstance(event, ModifierEvent): - return self.__process_modifier_event(event) + yield from self.__process_modifier_event(event) elif isinstance(event, KeyEvent): - return self.__process_key_event(event) - raise RuntimeError(f"Not implemented event: {event}") + yield from self.__process_key_event(event) + else: + raise RuntimeError(f"Not implemented event: {event}") - def __process_clear_event(self, reopen: bool=False) -> bool: + def __process_clear_event(self) -> bytes: self.__clear_modifiers() self.__clear_keys() - return self.__send_current_state(reopen=reopen) + return self.__make_report() - def __process_modifier_event(self, event: ModifierEvent) -> bool: + def __process_modifier_event(self, event: ModifierEvent) -> Generator[bytes, None, None]: if event.modifier in self.__pressed_modifiers: # Ранее нажатый модификатор отжимаем self.__pressed_modifiers.remove(event.modifier) - if not self.__send_current_state(): - return False + yield self.__make_report() if event.state: # Нажимаем если нужно self.__pressed_modifiers.add(event.modifier) - return self.__send_current_state() - return True + yield self.__make_report() - def __process_key_event(self, event: KeyEvent) -> bool: + def __process_key_event(self, event: KeyEvent) -> Generator[bytes, None, None]: if event.key in self.__pressed_keys: # Ранее нажатую клавишу отжимаем self.__pressed_keys[self.__pressed_keys.index(event.key)] = None - if not self.__send_current_state(): - return False + yield self.__make_report() elif event.state and None not in self.__pressed_keys: # Если нужно нажать что-то новое, но свободных слотов нет - отжимаем всё self.__clear_keys() - if not self.__send_current_state(): - return False + yield self.__make_report() if event.state: # Нажимаем если нужно self.__pressed_keys[self.__pressed_keys.index(None)] = event.key - return self.__send_current_state() - return True + yield self.__make_report() # ===== - def __send_current_state(self, reopen: bool=False) -> bool: - report = make_keyboard_report(self.__pressed_modifiers, self.__pressed_keys) - if not self._ensure_write(report, reopen=reopen): - self.__clear_modifiers() - self.__clear_keys() - return False - return True + def __make_report(self) -> bytes: + return make_keyboard_report(self.__pressed_modifiers, self.__pressed_keys) def __clear_modifiers(self) -> None: self.__pressed_modifiers.clear() diff --git a/kvmd/plugins/hid/otg/mouse.py b/kvmd/plugins/hid/otg/mouse.py index 1625e499..620effc5 100644 --- a/kvmd/plugins/hid/otg/mouse.py +++ b/kvmd/plugins/hid/otg/mouse.py @@ -20,6 +20,7 @@ # ========================================================================== # +from typing import Generator from typing import Optional from typing import Any @@ -65,7 +66,7 @@ class MouseProcess(BaseDeviceProcess): wheel_x=(0 if self.__horizontal_wheel else None), wheel_y=0, ) - self._ensure_write(report, close=True) # Release all buttons + self._cleanup_write(report) # Release all buttons def send_clear_event(self) -> None: self._clear_queue() @@ -91,56 +92,52 @@ class MouseProcess(BaseDeviceProcess): # ===== - def _process_event(self, event: BaseEvent) -> bool: - if isinstance(event, ClearEvent): - return self.__process_clear_event() - elif isinstance(event, ResetEvent): - return self.__process_clear_event(reopen=True) + def _process_event(self, event: BaseEvent) -> Generator[bytes, None, None]: + if isinstance(event, (ClearEvent, ResetEvent)): + yield self.__process_clear_event() elif isinstance(event, MouseButtonEvent): - return self.__process_button_event(event) + yield from self.__process_button_event(event) elif isinstance(event, MouseMoveEvent): - return self.__process_move_event(event) + yield self.__process_move_event(event) elif isinstance(event, MouseRelativeEvent): - return self.__process_relative_event(event) + yield self.__process_relative_event(event) elif isinstance(event, MouseWheelEvent): - return self.__process_wheel_event(event) - raise RuntimeError(f"Not implemented event: {event}") + yield self.__process_wheel_event(event) + else: + raise RuntimeError(f"Not implemented event: {event}") - def __process_clear_event(self, reopen: bool=False) -> bool: + def __process_clear_event(self) -> bytes: self.__clear_state() - return self.__send_current_state(reopen=reopen) + return self.__make_report() - def __process_button_event(self, event: MouseButtonEvent) -> bool: + def __process_button_event(self, event: MouseButtonEvent) -> Generator[bytes, None, None]: if event.code & self.__pressed_buttons: # Ранее нажатую кнопку отжимаем self.__pressed_buttons &= ~event.code - if not self.__send_current_state(): - return False + yield self.__make_report() if event.state: # Нажимаем если нужно self.__pressed_buttons |= event.code - return self.__send_current_state() - return True + yield self.__make_report() - def __process_move_event(self, event: MouseMoveEvent) -> bool: + def __process_move_event(self, event: MouseMoveEvent) -> bytes: self.__x = event.to_fixed_x self.__y = event.to_fixed_y - return self.__send_current_state() + return self.__make_report() - def __process_relative_event(self, event: MouseRelativeEvent) -> bool: - return self.__send_current_state(relative_event=event) + def __process_relative_event(self, event: MouseRelativeEvent) -> bytes: + return self.__make_report(relative_event=event) - def __process_wheel_event(self, event: MouseWheelEvent) -> bool: - return self.__send_current_state(wheel_event=event) + def __process_wheel_event(self, event: MouseWheelEvent) -> bytes: + return self.__make_report(wheel_event=event) # ===== - def __send_current_state( + def __make_report( self, relative_event: Optional[MouseRelativeEvent]=None, wheel_event: Optional[MouseWheelEvent]=None, - reopen: bool=False, - ) -> bool: + ) -> bytes: if self.__absolute: assert relative_event is None @@ -160,7 +157,7 @@ class MouseProcess(BaseDeviceProcess): else: wheel_x = wheel_y = 0 - report = make_mouse_report( + return make_mouse_report( absolute=self.__absolute, buttons=self.__pressed_buttons, move_x=move_x, @@ -168,10 +165,6 @@ class MouseProcess(BaseDeviceProcess): wheel_x=(wheel_x if self.__horizontal_wheel else None), wheel_y=wheel_y, ) - if not self._ensure_write(report, reopen=reopen): - self.__clear_state() - return False - return True def __clear_state(self) -> None: self.__pressed_buttons = 0 |