diff options
Diffstat (limited to 'kvmd/plugins/hid/otg')
-rw-r--r-- | kvmd/plugins/hid/otg/__init__.py | 51 | ||||
-rw-r--r-- | kvmd/plugins/hid/otg/device.py | 82 | ||||
-rw-r--r-- | kvmd/plugins/hid/otg/keyboard.py | 18 | ||||
-rw-r--r-- | kvmd/plugins/hid/otg/mouse.py | 7 |
4 files changed, 120 insertions, 38 deletions
diff --git a/kvmd/plugins/hid/otg/__init__.py b/kvmd/plugins/hid/otg/__init__.py index c5dc1a7a..84c2803e 100644 --- a/kvmd/plugins/hid/otg/__init__.py +++ b/kvmd/plugins/hid/otg/__init__.py @@ -21,6 +21,11 @@ import asyncio +import concurrent.futures +import multiprocessing +import multiprocessing.queues +import queue +import functools from typing import Dict from typing import AsyncGenerator @@ -47,15 +52,12 @@ class Plugin(BaseHid): 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.__changes_queue: multiprocessing.queues.Queue = multiprocessing.Queue() - self.__state_poll = state_poll - - self.__lock = asyncio.Lock() + self.__keyboard_proc = KeyboardProcess(noop=noop, changes_queue=self.__changes_queue, **keyboard) + self.__mouse_proc = MouseProcess(noop=noop, changes_queue=self.__changes_queue, **mouse) @classmethod def get_plugin_options(cls) -> Dict: @@ -66,16 +68,13 @@ class Plugin(BaseHid): "write_retries": Option(5, type=valid_int_f1), "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), + "noop": Option(False, type=valid_bool), } def start(self) -> None: @@ -83,22 +82,30 @@ class Plugin(BaseHid): self.__mouse_proc.start() def get_state(self) -> Dict: - keyboard_online = self.__keyboard_proc.is_online() - mouse_online = self.__mouse_proc.is_online() + keyboard_state = self.__keyboard_proc.get_state() + mouse_state = self.__mouse_proc.get_state() return { - "online": (keyboard_online and mouse_online), - "keyboard": {"online": keyboard_online}, - "mouse": {"online": mouse_online}, + "online": (keyboard_state["online"] and mouse_state["online"]), + "keyboard": {"features": {"leds": True}, **keyboard_state}, + "mouse": mouse_state, } async def poll_state(self) -> AsyncGenerator[Dict, None]: - prev_state: Dict = {} - while self.__keyboard_proc.is_alive() and self.__mouse_proc.is_alive(): - state = self.get_state() - if state != prev_state: - yield self.get_state() - prev_state = state - await asyncio.sleep(self.__state_poll) + loop = asyncio.get_running_loop() + wait_for_changes = functools.partial(self.__changes_queue.get, timeout=1) + with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor: + prev_state: Dict = {} + while True: + state = self.get_state() + if state != prev_state: + yield state + prev_state = state + while True: + try: + await loop.run_in_executor(executor, wait_for_changes) + break + except queue.Empty: + pass async def reset(self) -> None: self.__keyboard_proc.send_reset_event() diff --git a/kvmd/plugins/hid/otg/device.py b/kvmd/plugins/hid/otg/device.py index 7da6dd18..0fbb63cb 100644 --- a/kvmd/plugins/hid/otg/device.py +++ b/kvmd/plugins/hid/otg/device.py @@ -29,6 +29,9 @@ import queue import errno import time +from typing import Dict +from typing import Any + import setproctitle from ....logging import get_logger @@ -43,6 +46,10 @@ class BaseDeviceProcess(multiprocessing.Process): # pylint: disable=too-many-in def __init__( self, name: str, + read_size: int, + initial_state: Dict, + changes_queue: multiprocessing.queues.Queue, + device_path: str, select_timeout: float, write_retries: int, @@ -53,6 +60,8 @@ class BaseDeviceProcess(multiprocessing.Process): # pylint: disable=too-many-in super().__init__(daemon=True) self.__name = name + self.__read_size = read_size + self.__changes_queue = changes_queue self.__device_path = device_path self.__select_timeout = select_timeout @@ -62,7 +71,7 @@ class BaseDeviceProcess(multiprocessing.Process): # pylint: disable=too-many-in self.__fd = -1 self.__events_queue: multiprocessing.queues.Queue = multiprocessing.Queue() - self.__online_shared = multiprocessing.Value("i", 1) + self.__state_shared = multiprocessing.Manager().dict(online=True, **initial_state) # type: ignore self.__stop_event = multiprocessing.Event() def run(self) -> None: @@ -75,10 +84,12 @@ class BaseDeviceProcess(multiprocessing.Process): # pylint: disable=too-many-in 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 + self.__read_all_reports() try: - event: BaseEvent = self.__events_queue.get(timeout=1) + event: BaseEvent = self.__events_queue.get(timeout=0.1) except queue.Empty: - self.__ensure_device() # Check device + pass else: self._process_event(event) except Exception: @@ -89,8 +100,18 @@ class BaseDeviceProcess(multiprocessing.Process): # pylint: disable=too-many-in self.__close_device() - def is_online(self) -> bool: - return bool(self.__online_shared.value and self.is_alive()) + def get_state(self) -> Dict: + return dict(self.__state_shared) + + # ===== + + def _process_event(self, event: BaseEvent) -> None: + raise NotImplementedError + + def _process_read_report(self, report: bytes) -> None: + pass + + # ===== def _stop(self) -> None: if self.is_alive(): @@ -99,9 +120,6 @@ class BaseDeviceProcess(multiprocessing.Process): # pylint: disable=too-many-in if self.exitcode is not None: self.join() - def _process_event(self, event: BaseEvent) -> None: - raise NotImplementedError - def _queue_event(self, event: BaseEvent) -> None: self.__events_queue.put(event) @@ -116,6 +134,11 @@ class BaseDeviceProcess(multiprocessing.Process): # pylint: disable=too-many-in if close: self.__close_device() + def _update_state(self, key: str, value: Any) -> None: + if self.__state_shared[key] != value: + self.__state_shared[key] = value + self.__changes_queue.put(None) + # ===== def __write_report(self, report: bytes) -> bool: @@ -130,7 +153,7 @@ class BaseDeviceProcess(multiprocessing.Process): # pylint: disable=too-many-in try: written = os.write(self.__fd, report) if written == len(report): - self.__online_shared.value = 1 + self._update_state("online", True) return True else: logger.error("HID-%s write() error: written (%s) != report length (%d)", @@ -151,6 +174,33 @@ class BaseDeviceProcess(multiprocessing.Process): # pylint: disable=too-many-in self.__close_device() return False + def __read_all_reports(self) -> None: + if self.__noop or self.__read_size == 0: + return + + assert self.__fd >= 0 + logger = get_logger() + + read = True + while read: + try: + read = bool(select.select([self.__fd], [], [], 0)[0]) + except Exception as err: + logger.error("Can't select() for read HID-%s: %s: %s", self.__name, type(err).__name__, err) + break + + if read: + try: + report = os.read(self.__fd, self.__read_size) + except Exception as err: + if isinstance(err, OSError) and err.errno == errno.EAGAIN: # pylint: disable=no-member + logger.debug("HID-%s busy/unplugged (read): %s: %s", + self.__name, type(err).__name__, err) + else: + logger.exception("Can't read report from HID-%s", self.__name) + else: + self._process_read_report(report) + def __ensure_device(self) -> bool: if self.__noop: return True @@ -159,25 +209,29 @@ class BaseDeviceProcess(multiprocessing.Process): # pylint: disable=too-many-in if self.__fd < 0: try: - self.__fd = os.open(self.__device_path, os.O_WRONLY|os.O_NONBLOCK) + 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 FileNotFoundError: logger.error("Missing HID-%s device: %s", self.__name, self.__device_path) + time.sleep(self.__select_timeout) except Exception as err: logger.error("Can't open HID-%s device: %s: %s: %s", self.__name, self.__device_path, type(err).__name__, err) + time.sleep(self.__select_timeout) if self.__fd >= 0: try: if select.select([], [self.__fd], [], self.__select_timeout)[1]: - self.__online_shared.value = 1 + self._update_state("online", True) return True else: - logger.debug("HID-%s is busy/unplugged (select)", self.__name) + logger.debug("HID-%s is busy/unplugged (write select)", self.__name) except Exception as err: - logger.error("Can't select() HID-%s: %s: %s", self.__name, type(err).__name__, err) + logger.error("Can't select() for write HID-%s: %s: %s", self.__name, type(err).__name__, err) self.__close_device() - self.__online_shared.value = 0 + self._update_state("online", False) return False def __close_device(self) -> None: diff --git a/kvmd/plugins/hid/otg/keyboard.py b/kvmd/plugins/hid/otg/keyboard.py index 6cf8f72d..6c9ddfed 100644 --- a/kvmd/plugins/hid/otg/keyboard.py +++ b/kvmd/plugins/hid/otg/keyboard.py @@ -65,7 +65,12 @@ class _KeyEvent(BaseEvent): # ===== class KeyboardProcess(BaseDeviceProcess): def __init__(self, **kwargs: Any) -> None: - super().__init__(name="keyboard", **kwargs) + super().__init__( + name="keyboard", + read_size=1, + initial_state={"leds": {"caps": False, "scroll": False, "num": False}}, + **kwargs, + ) self.__pressed_modifiers: Set[keymap.OtgKey] = set() self.__pressed_keys: List[Optional[keymap.OtgKey]] = [None] * 6 @@ -90,6 +95,17 @@ class KeyboardProcess(BaseDeviceProcess): # ===== + def _process_read_report(self, report: bytes) -> None: + # https://wiki.osdev.org/USB_Human_Interface_Devices#LED_lamps + assert len(report) == 1, report + self._update_state("leds", { + "caps": bool(report[0] & 2), + "scroll": bool(report[0] & 4), + "num": bool(report[0] & 1), + }) + + # ===== + def _process_event(self, event: BaseEvent) -> None: if isinstance(event, _ClearEvent): self.__process_clear_event() diff --git a/kvmd/plugins/hid/otg/mouse.py b/kvmd/plugins/hid/otg/mouse.py index 172ed666..a31d7ae5 100644 --- a/kvmd/plugins/hid/otg/mouse.py +++ b/kvmd/plugins/hid/otg/mouse.py @@ -61,7 +61,12 @@ class _WheelEvent(BaseEvent): # ===== class MouseProcess(BaseDeviceProcess): def __init__(self, **kwargs: Any) -> None: - super().__init__(name="mouse", **kwargs) + super().__init__( + name="mouse", + read_size=0, + initial_state={}, + **kwargs, + ) self.__pressed_buttons: int = 0 self.__x = 0 |