rewrite otg hid

This commit is contained in:
Maxim Devaev 2021-07-23 05:11:04 +03:00
parent bc73e74161
commit cbc3a4ceef
4 changed files with 119 additions and 136 deletions

View File

@ -64,17 +64,15 @@ class Plugin(BaseHid):
return { return {
"keyboard": { "keyboard": {
"device": Option("", type=valid_abs_path, unpack_as="device_path"), "device": Option("", type=valid_abs_path, unpack_as="device_path"),
"select_timeout": Option(1.0, type=valid_float_f01), "select_timeout": Option(0.1, type=valid_float_f01),
"write_retries": Option(5, type=valid_int_f1), "queue_timeout": Option(0.1, type=valid_float_f01),
"write_retries_delay": Option(0.1, type=valid_float_f01), "write_retries": Option(150, type=valid_int_f1),
"reopen_delay": Option(0.5, type=valid_float_f01),
}, },
"mouse": { "mouse": {
"device": Option("", type=valid_abs_path, unpack_as="device_path"), "device": Option("", type=valid_abs_path, unpack_as="device_path"),
"select_timeout": Option(1.0, type=valid_float_f01), "select_timeout": Option(0.1, type=valid_float_f01),
"write_retries": Option(5, type=valid_int_f1), "queue_timeout": Option(0.1, type=valid_float_f01),
"write_retries_delay": Option(0.1, type=valid_float_f01), "write_retries": Option(150, type=valid_int_f1),
"reopen_delay": Option(0.5, type=valid_float_f01),
"absolute": Option(True, type=valid_bool), "absolute": Option(True, type=valid_bool),
"horizontal_wheel": Option(True, type=valid_bool), "horizontal_wheel": Option(True, type=valid_bool),
}, },

View File

@ -28,6 +28,7 @@ import errno
import time import time
from typing import Dict from typing import Dict
from typing import Generator
from ....logging import get_logger from ....logging import get_logger
@ -52,9 +53,8 @@ class BaseDeviceProcess(multiprocessing.Process): # pylint: disable=too-many-in
device_path: str, device_path: str,
select_timeout: float, select_timeout: float,
queue_timeout: float,
write_retries: int, write_retries: int,
write_retries_delay: float,
reopen_delay: float,
noop: bool, noop: bool,
) -> None: ) -> None:
@ -67,9 +67,8 @@ class BaseDeviceProcess(multiprocessing.Process): # pylint: disable=too-many-in
self.__device_path = device_path self.__device_path = device_path
self.__select_timeout = select_timeout self.__select_timeout = select_timeout
self.__queue_timeout = queue_timeout
self.__write_retries = write_retries self.__write_retries = write_retries
self.__write_retries_delay = write_retries_delay
self.__reopen_delay = reopen_delay
self.__noop = noop self.__noop = noop
self.__fd = -1 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.__state_flags = aiomulti.AioSharedFlags({"online": True, **initial_state}, notifier)
self.__stop_event = multiprocessing.Event() 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}") logger = aioproc.settle(f"HID-{self.__name}", f"hid-{self.__name}")
report = b""
retries = 0
while not self.__stop_event.is_set(): while not self.__stop_event.is_set():
try: try:
while not self.__stop_event.is_set(): 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() self.__read_all_reports()
try: try:
event = self.__events_queue.get(timeout=0.1) event = self.__events_queue.get(timeout=self.__queue_timeout)
except queue.Empty: except queue.Empty:
if not self.__udc.can_operate(): if not self.__udc.can_operate():
self._clear_queue() self.__state_flags.update(online=False)
self.__close_device()
else: 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: except Exception:
logger.exception("Unexpected HID-%s error", self.__name) logger.exception("Unexpected HID-%s error", self.__name)
self._clear_queue()
self.__close_device()
time.sleep(1) time.sleep(1)
self.__close_device() self.__close_device()
@ -106,8 +118,10 @@ class BaseDeviceProcess(multiprocessing.Process): # pylint: disable=too-many-in
# ===== # =====
def _process_event(self, event: BaseEvent) -> bool: def _process_event(self, event: BaseEvent) -> Generator[bytes, None, None]: # pylint: disable=unused-argument
raise NotImplementedError if self is not None: # XXX: Vulture and pylint hack
raise NotImplementedError()
yield
def _process_read_report(self, report: bytes) -> None: def _process_read_report(self, report: bytes) -> None:
pass pass
@ -131,28 +145,24 @@ class BaseDeviceProcess(multiprocessing.Process): # pylint: disable=too-many-in
def _clear_queue(self) -> None: def _clear_queue(self) -> None:
tools.clear_queue(self.__events_queue) tools.clear_queue(self.__events_queue)
def _ensure_write(self, report: bytes, reopen: bool=False, close: bool=False) -> bool: def _cleanup_write(self, report: bytes) -> None:
if reopen: assert not self.is_alive()
self.__close_device() assert self.__fd < 0
try:
if self.__ensure_device(): if self.__ensure_device():
return self.__write_report(report) self.__write_report(report)
return False
finally:
if close:
self.__close_device() self.__close_device()
# ===== # =====
def __write_report(self, report: bytes) -> bool: def __write_report(self, report: bytes) -> bool:
assert report
if self.__noop: if self.__noop:
return True return True
assert self.__fd >= 0 assert self.__fd >= 0
logger = get_logger() logger = get_logger()
retries = self.__write_retries
while retries:
try: try:
written = os.write(self.__fd, report) written = os.write(self.__fd, report)
if written == len(report): if written == len(report):
@ -171,13 +181,7 @@ class BaseDeviceProcess(multiprocessing.Process): # pylint: disable=too-many-in
else: else:
logger.exception("Can't write report to HID-%s", self.__name) logger.exception("Can't write report to HID-%s", self.__name)
retries -= 1 self.__state_flags.update(online=False)
if retries:
logger.debug("HID-%s write retries left: %d", self.__name, retries)
time.sleep(self.__write_retries_delay)
self.__close_device()
return False return False
def __read_all_reports(self) -> None: def __read_all_reports(self) -> None:
@ -213,27 +217,25 @@ class BaseDeviceProcess(multiprocessing.Process): # pylint: disable=too-many-in
logger = get_logger() logger = get_logger()
if self.__fd < 0: if self.__fd < 0:
if self.__udc.can_operate():
try: try:
flags = os.O_NONBLOCK flags = os.O_NONBLOCK
flags |= (os.O_RDWR if self.__read_size else os.O_WRONLY) flags |= (os.O_RDWR if self.__read_size else os.O_WRONLY)
self.__fd = os.open(self.__device_path, flags) self.__fd = os.open(self.__device_path, flags)
except Exception as err: except Exception as err:
logger.error("Can't open HID-%s device %s: %s", self.__name, self.__device_path, tools.efmt(err)) logger.error("Can't open HID-%s device %s: %s",
time.sleep(self.__reopen_delay) self.__name, self.__device_path, tools.efmt(err))
else:
time.sleep(self.__reopen_delay)
if self.__fd >= 0: if self.__fd >= 0:
try: try:
if select.select([], [self.__fd], [], self.__select_timeout)[1]: if select.select([], [self.__fd], [], self.__select_timeout)[1]:
self.__state_flags.update(online=True) # Закомментировано, потому что иногда запись доступна, но устройство отключено
# self.__state_flags.update(online=True)
return True return True
else: else:
# Если запись недоступна, то скорее всего устройство отключено
logger.debug("HID-%s is busy/unplugged (write select)", self.__name) logger.debug("HID-%s is busy/unplugged (write select)", self.__name)
except Exception as err: except Exception as err:
logger.error("Can't select() for write HID-%s: %s", self.__name, tools.efmt(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) self.__state_flags.update(online=False)
return False return False

View File

@ -24,6 +24,7 @@ from typing import Tuple
from typing import List from typing import List
from typing import Set from typing import Set
from typing import Iterable from typing import Iterable
from typing import Generator
from typing import Optional from typing import Optional
from typing import Any from typing import Any
@ -61,7 +62,7 @@ class KeyboardProcess(BaseDeviceProcess):
def cleanup(self) -> None: def cleanup(self) -> None:
self._stop() self._stop()
get_logger().info("Clearing HID-keyboard events ...") 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: def send_clear_event(self) -> None:
self._clear_queue() self._clear_queue()
@ -87,60 +88,49 @@ class KeyboardProcess(BaseDeviceProcess):
# ===== # =====
def _process_event(self, event: BaseEvent) -> bool: def _process_event(self, event: BaseEvent) -> Generator[bytes, None, None]:
if isinstance(event, ClearEvent): if isinstance(event, (ClearEvent, ResetEvent)):
return self.__process_clear_event() yield self.__process_clear_event()
elif isinstance(event, ResetEvent):
return self.__process_clear_event(reopen=True)
elif isinstance(event, ModifierEvent): elif isinstance(event, ModifierEvent):
return self.__process_modifier_event(event) yield from self.__process_modifier_event(event)
elif isinstance(event, KeyEvent): elif isinstance(event, KeyEvent):
return self.__process_key_event(event) yield from self.__process_key_event(event)
else:
raise RuntimeError(f"Not implemented event: {event}") 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_modifiers()
self.__clear_keys() 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: if event.modifier in self.__pressed_modifiers:
# Ранее нажатый модификатор отжимаем # Ранее нажатый модификатор отжимаем
self.__pressed_modifiers.remove(event.modifier) self.__pressed_modifiers.remove(event.modifier)
if not self.__send_current_state(): yield self.__make_report()
return False
if event.state: if event.state:
# Нажимаем если нужно # Нажимаем если нужно
self.__pressed_modifiers.add(event.modifier) self.__pressed_modifiers.add(event.modifier)
return self.__send_current_state() yield self.__make_report()
return True
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: if event.key in self.__pressed_keys:
# Ранее нажатую клавишу отжимаем # Ранее нажатую клавишу отжимаем
self.__pressed_keys[self.__pressed_keys.index(event.key)] = None self.__pressed_keys[self.__pressed_keys.index(event.key)] = None
if not self.__send_current_state(): yield self.__make_report()
return False
elif event.state and None not in self.__pressed_keys: elif event.state and None not in self.__pressed_keys:
# Если нужно нажать что-то новое, но свободных слотов нет - отжимаем всё # Если нужно нажать что-то новое, но свободных слотов нет - отжимаем всё
self.__clear_keys() self.__clear_keys()
if not self.__send_current_state(): yield self.__make_report()
return False
if event.state: if event.state:
# Нажимаем если нужно # Нажимаем если нужно
self.__pressed_keys[self.__pressed_keys.index(None)] = event.key self.__pressed_keys[self.__pressed_keys.index(None)] = event.key
return self.__send_current_state() yield self.__make_report()
return True
# ===== # =====
def __send_current_state(self, reopen: bool=False) -> bool: def __make_report(self) -> bytes:
report = make_keyboard_report(self.__pressed_modifiers, self.__pressed_keys) return 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 __clear_modifiers(self) -> None: def __clear_modifiers(self) -> None:
self.__pressed_modifiers.clear() self.__pressed_modifiers.clear()

View File

@ -20,6 +20,7 @@
# ========================================================================== # # ========================================================================== #
from typing import Generator
from typing import Optional from typing import Optional
from typing import Any from typing import Any
@ -65,7 +66,7 @@ class MouseProcess(BaseDeviceProcess):
wheel_x=(0 if self.__horizontal_wheel else None), wheel_x=(0 if self.__horizontal_wheel else None),
wheel_y=0, 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: def send_clear_event(self) -> None:
self._clear_queue() self._clear_queue()
@ -91,56 +92,52 @@ class MouseProcess(BaseDeviceProcess):
# ===== # =====
def _process_event(self, event: BaseEvent) -> bool: def _process_event(self, event: BaseEvent) -> Generator[bytes, None, None]:
if isinstance(event, ClearEvent): if isinstance(event, (ClearEvent, ResetEvent)):
return self.__process_clear_event() yield self.__process_clear_event()
elif isinstance(event, ResetEvent):
return self.__process_clear_event(reopen=True)
elif isinstance(event, MouseButtonEvent): elif isinstance(event, MouseButtonEvent):
return self.__process_button_event(event) yield from self.__process_button_event(event)
elif isinstance(event, MouseMoveEvent): elif isinstance(event, MouseMoveEvent):
return self.__process_move_event(event) yield self.__process_move_event(event)
elif isinstance(event, MouseRelativeEvent): elif isinstance(event, MouseRelativeEvent):
return self.__process_relative_event(event) yield self.__process_relative_event(event)
elif isinstance(event, MouseWheelEvent): elif isinstance(event, MouseWheelEvent):
return self.__process_wheel_event(event) yield self.__process_wheel_event(event)
else:
raise RuntimeError(f"Not implemented event: {event}") 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() 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: if event.code & self.__pressed_buttons:
# Ранее нажатую кнопку отжимаем # Ранее нажатую кнопку отжимаем
self.__pressed_buttons &= ~event.code self.__pressed_buttons &= ~event.code
if not self.__send_current_state(): yield self.__make_report()
return False
if event.state: if event.state:
# Нажимаем если нужно # Нажимаем если нужно
self.__pressed_buttons |= event.code self.__pressed_buttons |= event.code
return self.__send_current_state() yield self.__make_report()
return True
def __process_move_event(self, event: MouseMoveEvent) -> bool: def __process_move_event(self, event: MouseMoveEvent) -> bytes:
self.__x = event.to_fixed_x self.__x = event.to_fixed_x
self.__y = event.to_fixed_y self.__y = event.to_fixed_y
return self.__send_current_state() return self.__make_report()
def __process_relative_event(self, event: MouseRelativeEvent) -> bool: def __process_relative_event(self, event: MouseRelativeEvent) -> bytes:
return self.__send_current_state(relative_event=event) return self.__make_report(relative_event=event)
def __process_wheel_event(self, event: MouseWheelEvent) -> bool: def __process_wheel_event(self, event: MouseWheelEvent) -> bytes:
return self.__send_current_state(wheel_event=event) return self.__make_report(wheel_event=event)
# ===== # =====
def __send_current_state( def __make_report(
self, self,
relative_event: Optional[MouseRelativeEvent]=None, relative_event: Optional[MouseRelativeEvent]=None,
wheel_event: Optional[MouseWheelEvent]=None, wheel_event: Optional[MouseWheelEvent]=None,
reopen: bool=False, ) -> bytes:
) -> bool:
if self.__absolute: if self.__absolute:
assert relative_event is None assert relative_event is None
@ -160,7 +157,7 @@ class MouseProcess(BaseDeviceProcess):
else: else:
wheel_x = wheel_y = 0 wheel_x = wheel_y = 0
report = make_mouse_report( return make_mouse_report(
absolute=self.__absolute, absolute=self.__absolute,
buttons=self.__pressed_buttons, buttons=self.__pressed_buttons,
move_x=move_x, move_x=move_x,
@ -168,10 +165,6 @@ class MouseProcess(BaseDeviceProcess):
wheel_x=(wheel_x if self.__horizontal_wheel else None), wheel_x=(wheel_x if self.__horizontal_wheel else None),
wheel_y=wheel_y, wheel_y=wheel_y,
) )
if not self._ensure_write(report, reopen=reopen):
self.__clear_state()
return False
return True
def __clear_state(self) -> None: def __clear_state(self) -> None:
self.__pressed_buttons = 0 self.__pressed_buttons = 0