From 6e24efc81e9a99605e39ae22a775553b9aad0f5c Mon Sep 17 00:00:00 2001 From: jacobbar Date: Mon, 13 Mar 2023 22:11:44 +0700 Subject: Add CH9329 Serial to HID support (#121) * Add ch9329 plugin * refactoring ch9329 * refactor ch9329 and cleanup * refactoring * fixing lint errors * clarifying list type --- kvmd/plugins/hid/ch9329/__init__.py | 271 ++++++++++++++++++++++++++++++++++++ kvmd/plugins/hid/ch9329/keyboard.py | 71 ++++++++++ kvmd/plugins/hid/ch9329/mouse.py | 115 +++++++++++++++ kvmd/plugins/hid/ch9329/tty.py | 62 +++++++++ 4 files changed, 519 insertions(+) create mode 100644 kvmd/plugins/hid/ch9329/__init__.py create mode 100644 kvmd/plugins/hid/ch9329/keyboard.py create mode 100644 kvmd/plugins/hid/ch9329/mouse.py create mode 100644 kvmd/plugins/hid/ch9329/tty.py (limited to 'kvmd') diff --git a/kvmd/plugins/hid/ch9329/__init__.py b/kvmd/plugins/hid/ch9329/__init__.py new file mode 100644 index 00000000..0b4d4a14 --- /dev/null +++ b/kvmd/plugins/hid/ch9329/__init__.py @@ -0,0 +1,271 @@ +# ========================================================================== # +# # +# KVMD - The main PiKVM daemon. # +# # +# Copyright (C) 2018-2022 Maxim Devaev # +# # +# 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 . # +# # +# ========================================================================== # + + +import multiprocessing +import queue +import time + +from typing import Iterable +from typing import AsyncGenerator + +from ....logging import get_logger + +from .... import tools +from .... import aiotools +from .... import aiomulti +from .... import aioproc + +from ....yamlconf import Option + +from ....validators.basic import valid_bool +from ....validators.basic import valid_int_f0 +from ....validators.basic import valid_int_f1 +from ....validators.basic import valid_float_f01 +from ....validators.os import valid_abs_path +from ....validators.hw import valid_tty_speed + +from .. import BaseHid + +from .tty import TTY +from .mouse import Mouse +from .keyboard import Keyboard + +from .tty import get_info + + +class _ResError(Exception): + def __init__(self, msg: str) -> None: + super().__init__(msg) + self.msg = msg + + +# ===== +class Plugin(BaseHid, multiprocessing.Process): # pylint: disable=too-many-instance-attributes + def __init__( # pylint: disable=too-many-arguments,super-init-not-called + self, + device_path: str, + speed: int, + read_timeout: float, + read_retries: int, + common_retries: int, + retries_delay: float, + errors_threshold: int, + noop: bool, + ) -> None: + + multiprocessing.Process.__init__(self, daemon=True) + + self.__device_path = device_path + self.__speed = speed + self.__read_timeout = read_timeout + self.__read_retries = read_retries + self.__common_retries = common_retries + self.__retries_delay = retries_delay + self.__errors_threshold = errors_threshold + self.__noop = noop + + self.__reset_required_event = multiprocessing.Event() + self.__cmd_queue: "multiprocessing.Queue[list]" = multiprocessing.Queue() + + self.__notifier = aiomulti.AioProcessNotifier() + self.__state_flags = aiomulti.AioSharedFlags({ + "online": 0, + "busy": 0, + "status": 0, + }, self.__notifier, type=int) + + self.__stop_event = multiprocessing.Event() + self.__tty = TTY(device_path, speed, read_timeout) + self.__keyboard = Keyboard() + self.__mouse = Mouse() + + @classmethod + def get_plugin_options(cls) -> dict: + return { + "device": Option("/dev/kvmd-hid", type=valid_abs_path, unpack_as="device_path"), + "speed": Option(9600, type=valid_tty_speed), + "read_timeout": Option(0.3, type=valid_float_f01), + "read_retries": Option(5, type=valid_int_f1), + "common_retries": Option(5, type=valid_int_f1), + "retries_delay": Option(0.5, type=valid_float_f01), + "errors_threshold": Option(5, type=valid_int_f0), + "noop": Option(False, type=valid_bool), + } + + def sysprep(self) -> None: + get_logger(0).info("Starting HID daemon ...") + self.start() + + async def get_state(self) -> dict: + state = await self.__state_flags.get() + online = bool(state["online"]) + active_mouse = self.__mouse.active() + absolute = (active_mouse == "usb") + keyboard_leds = await self.__keyboard.leds() + + return { + "online": online, + "busy": False, + "connected": None, + "keyboard": { + "online": True, + "leds": keyboard_leds, + "outputs": {"available": [], "active": ""}, + }, + "mouse": { + "online": True, + "absolute": absolute, + "outputs": { + "available": ["usb", "usb_rel"], + "active": active_mouse + }, + }, + } + + async def poll_state(self) -> AsyncGenerator[dict, None]: + prev_state: dict = {} + while True: + state = await self.get_state() + if state != prev_state: + yield state + prev_state = state + await self.__notifier.wait() + + async def reset(self) -> None: + self.__reset_required_event.set() + + @aiotools.atomic_fg + async def cleanup(self) -> None: + if self.is_alive(): + get_logger(0).info("Stopping HID daemon ...") + self.__stop_event.set() + if self.is_alive() or self.exitcode is not None: + self.join() + + # ===== + + def send_key_events(self, keys: Iterable[tuple[str, bool]]) -> None: + for (key, state) in keys: + self.__queue_cmd(self.__keyboard.key(key, state)) + + def send_mouse_button_event(self, button: str, state: bool) -> None: + self.__queue_cmd(self.__mouse.button(button, state)) + + def send_mouse_move_event(self, to_x: int, to_y: int) -> None: + self.__queue_cmd(self.__mouse.move(to_x, to_y)) + + def send_mouse_wheel_event(self, delta_x: int, delta_y: int) -> None: + self.__queue_cmd(self.__mouse.wheel(delta_x, delta_y)) + + def send_mouse_relative_event(self, delta_x: int, delta_y: int) -> None: + self.__queue_cmd(self.__mouse.relative(delta_x, delta_y)) + + def set_params(self, keyboard_output: (str | None)=None, mouse_output: (str | None)=None) -> None: + if mouse_output is not None: + get_logger(0).info("HID : mouse output = %s", mouse_output) + self.__mouse.set_active(mouse_output) + self.__notifier.notify() + + def set_connected(self, connected: bool) -> None: + pass + + def clear_events(self) -> None: + tools.clear_queue(self.__cmd_queue) + + def __queue_cmd(self, cmd: list[int], clear: bool=False) -> None: + if not self.__stop_event.is_set(): + if clear: + # FIXME: Если очистка производится со стороны процесса хида, то возможна гонка между + # очисткой и добавлением нового события. Неприятно, но не смертельно. + # Починить блокировкой после перехода на асинхронные очереди. + tools.clear_queue(self.__cmd_queue) + self.__cmd_queue.put_nowait(cmd) + + def run(self) -> None: # pylint: disable=too-many-branches + logger = aioproc.settle("HID", "hid") + while not self.__stop_event.is_set(): + try: + # self.__tty.connect() + self.__hid_loop() + except Exception: + logger.exception("Unexpected error in the run loop") + time.sleep(1) + + def __hid_loop(self) -> None: + while not self.__stop_event.is_set(): + try: + + while not (self.__stop_event.is_set() and self.__cmd_queue.qsize() == 0): + if self.__reset_required_event.is_set(): + try: + self.__set_state_busy(True) + # self.__process_request(conn, RESET) + finally: + self.__reset_required_event.clear() + try: + cmd = self.__cmd_queue.get(timeout=0.1) + # get_logger(0).info(f"HID : cmd = {cmd}") + except queue.Empty: + self.__process_cmd(get_info()) + else: + self.__process_cmd(cmd) + except Exception: + self.clear_events() + get_logger(0).exception("Unexpected error in the HID loop") + time.sleep(2) + + def __process_cmd(self, cmd: list[int]) -> bool: # pylint: disable=too-many-branches + error_retval = False + try: + res = self.__tty.send(cmd) + # get_logger(0).info(f"HID response = {res}") + if len(res) < 4: + raise _ResError("No response from HID - might be disconnected") + + if not self.__tty.check_res(res): + raise _ResError("Invalid response checksum ...") + + # Response Error + if res[4] == 1 and res[5] != 0: + raise _ResError("Command error code = " + str(res[5])) + + # get_info response + if res[3] == 0x81: + self.__keyboard.set_leds(res[7]) + self.__notifier.notify() + + self.__set_state_online(True) + return True + + except Exception as err: + self.__set_state_online(False) + get_logger(0).info(err) + time.sleep(2) + error_retval = False + + return error_retval + + def __set_state_online(self, online: bool) -> None: + self.__state_flags.update(online=int(online)) + + def __set_state_busy(self, busy: bool) -> None: + self.__state_flags.update(busy=int(busy)) diff --git a/kvmd/plugins/hid/ch9329/keyboard.py b/kvmd/plugins/hid/ch9329/keyboard.py new file mode 100644 index 00000000..70ccf547 --- /dev/null +++ b/kvmd/plugins/hid/ch9329/keyboard.py @@ -0,0 +1,71 @@ +# ========================================================================== # +# # +# KVMD - The main PiKVM daemon. # +# # +# Copyright (C) 2018-2022 Maxim Devaev # +# # +# 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 . # +# # +# ========================================================================== # + +from .... import aiomulti + +from ....keyboard.mappings import KEYMAP + + +class Keyboard: + def __init__(self) -> None: + + self.__notifier = aiomulti.AioProcessNotifier() + self.__leds = aiomulti.AioSharedFlags({ + "num": False, + "caps": False, + "scroll": False, + }, self.__notifier, type=bool) + + self.__active_keys: list[list] = [] + + def key(self, key: str, state: bool) -> list[int]: + if state: + self.__active_keys.append([key, self.__is_modifier(key)]) + else: + self.__active_keys.remove([key, self.__is_modifier(key)]) + return self.__key() + + async def leds(self) -> dict: + leds = await self.__leds.get() + return leds + + def set_leds(self, led_byte: int) -> None: + num = bool(led_byte & 1) + caps = bool((led_byte >> 1) & 1) + scroll = bool((led_byte >> 2) & 1) + self.__leds.update(num=num, caps=caps, scroll=scroll) + + def __key(self) -> list[int]: + cmd = [0x00, 0x02, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] + counter = 0 + for key in self.__active_keys: + if key[1]: + cmd[3 + counter] = self.__keycode(key[0]) + else: + cmd[5 + counter] = self.__keycode(key[0]) + counter += 1 + return cmd + + def __keycode(self, key: str) -> int: + return KEYMAP[key].usb.code + + def __is_modifier(self, key: str) -> bool: + return KEYMAP[key].usb.is_modifier diff --git a/kvmd/plugins/hid/ch9329/mouse.py b/kvmd/plugins/hid/ch9329/mouse.py new file mode 100644 index 00000000..034aff92 --- /dev/null +++ b/kvmd/plugins/hid/ch9329/mouse.py @@ -0,0 +1,115 @@ +# ========================================================================== # +# # +# KVMD - The main PiKVM daemon. # +# # +# Copyright (C) 2018-2022 Maxim Devaev # +# # +# 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 . # +# # +# ========================================================================== # + +import math + +from ....mouse import MouseRange + + +class Mouse: # pylint: disable=too-many-instance-attributes + def __init__(self) -> None: + self.__active = "usb" + self.__button = "" + self.__clicked = False + self.__to_x = [0, 0] + self.__to_y = [0, 0] + self.__wheel_y = 0 + self.__delta_x = 0 + self.__delta_y = 0 + + def button(self, button: str, clicked: bool) -> list[int]: + self.__button = button + self.__clicked = clicked + self.__wheel_y = 0 + if self.__active != "usb": + self.__to_x = [0, 0] + self.__to_y = [0, 0] + return self.__absolute() + + def move(self, to_x: int, to_y: int) -> list[int]: + assert MouseRange.MIN <= to_x <= MouseRange.MAX + assert MouseRange.MIN <= to_y <= MouseRange.MAX + self.__to_x = self.__to_fixed(to_x) + self.__to_y = self.__to_fixed(to_y) + self.__wheel_y = 0 + return self.__absolute() + + def wheel(self, delta_x: int, delta_y: int) -> list[int]: + assert -127 <= delta_y <= 127 + _ = delta_x + self.__wheel_y = 1 if delta_y > 0 else 255 + return self.__absolute() + + def relative(self, delta_x: int, delta_y: int) -> list[int]: + assert -127 <= delta_x <= 127 + assert -127 <= delta_y <= 127 + delta_x = math.ceil(delta_x / 3) + delta_y = math.ceil(delta_y / 3) + self.__delta_x = delta_x if delta_x >= 0 else 255 + delta_x + self.__delta_y = delta_y if delta_y >= 0 else 255 + delta_y + return self.__relative() + + def active(self) -> str: + return self.__active + + def set_active(self, name: str) -> None: + self.__active = name + + def __absolute(self) -> list[int]: + code = 0x00 + if self.__clicked: + code = self.__button_code(self.__button) + cmd = [0x00, 0x04, 0x07, 0x02, code, 0x00, 0x00, 0x00, 0x00, 0x00] + if len(self.__to_x) == 2: + cmd[6] = self.__to_x[0] + cmd[5] = self.__to_x[1] + if len(self.__to_y) == 2: + cmd[8] = self.__to_y[0] + cmd[7] = self.__to_y[1] + if self.__wheel_y: + cmd[9] = self.__wheel_y + return cmd + + def __relative(self) -> list[int]: + code = 0x00 + if self.__clicked: + code = self.__button_code(self.__button) + cmd = [0x00, 0x05, 0x05, 0x01, code, self.__delta_x, self.__delta_y, 0x00] + return cmd + + def __button_code(self, name: str) -> int: + code = 0x00 + match name: + case "left": + code = 0x01 + case "right": + code = 0x02 + case "middle": + code = 0x04 + case "up": + code = 0x08 + case "down": + code = 0x10 + return code + + def __to_fixed(self, num: int) -> list[int]: + to_fixed = math.ceil(MouseRange.remap(num, 0, MouseRange.MAX) / 8) + return [to_fixed >> 8, to_fixed & 0xFF] diff --git a/kvmd/plugins/hid/ch9329/tty.py b/kvmd/plugins/hid/ch9329/tty.py new file mode 100644 index 00000000..2e8cb22e --- /dev/null +++ b/kvmd/plugins/hid/ch9329/tty.py @@ -0,0 +1,62 @@ +# ========================================================================== # +# # +# KVMD - The main PiKVM daemon. # +# # +# Copyright (C) 2018-2022 Maxim Devaev # +# # +# 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 . # +# # +# ========================================================================== # + + +import os +import serial + + +class TTY: + def __init__(self, device_path: str, speed: int, read_timeout: float) -> None: + self.__tty = serial.Serial(device_path, speed, timeout=read_timeout) + self.__device_path = device_path + + def has_device(self) -> bool: + return os.path.exists(self.__device_path) + + def send(self, cmd: list[int]) -> list[int]: + cmd = self.__wrap_cmd(cmd) + self.__tty.write(serial.to_bytes(cmd)) + data = list(self.__tty.read(5)) + if data and data[4]: + more_data = list(self.__tty.read(data[4] + 1)) + data.extend(more_data) + return data + + def check_res(self, res: list[int]) -> bool: + res_sum = res.pop() + return (self.__checksum(res) == res_sum) + + def __wrap_cmd(self, cmd: list[int]) -> list[int]: + cmd.insert(0, 0xAB) + cmd.insert(0, 0x57) + cmd.append(self.__checksum(cmd)) + return cmd + + def __checksum(self, cmd: list[int]) -> int: + return sum(cmd) % 256 + + +def get_info() -> list[int]: + return [0x00, 0x01, 0x00] + +# RESET = [0x00,0x0F,0x00] +# GET_INFO = [0x00,0x01,0x00] -- cgit v1.2.3 From 6689008840dca1a0d3587192d1087854b1d64e6e Mon Sep 17 00:00:00 2001 From: jacobbar Date: Tue, 14 Mar 2023 21:27:44 +0700 Subject: Adds CH9329 Serial to HID Plugin Support (#122) * Add ch9329 plugin * refactoring ch9329 * refactor ch9329 and cleanup * refactoring * fixing lint errors * clarifying list type * fix mouse multiple buttons * remove unused var --------- Co-authored-by: Maxim Devaev --- kvmd/plugins/hid/ch9329/__init__.py | 4 ++-- kvmd/plugins/hid/ch9329/mouse.py | 35 +++++++++++++++++------------------ 2 files changed, 19 insertions(+), 20 deletions(-) (limited to 'kvmd') diff --git a/kvmd/plugins/hid/ch9329/__init__.py b/kvmd/plugins/hid/ch9329/__init__.py index 0b4d4a14..d8fea2de 100644 --- a/kvmd/plugins/hid/ch9329/__init__.py +++ b/kvmd/plugins/hid/ch9329/__init__.py @@ -256,11 +256,11 @@ class Plugin(BaseHid, multiprocessing.Process): # pylint: disable=too-many-inst self.__set_state_online(True) return True - except Exception as err: + except _ResError as err: self.__set_state_online(False) get_logger(0).info(err) - time.sleep(2) error_retval = False + time.sleep(2) return error_retval diff --git a/kvmd/plugins/hid/ch9329/mouse.py b/kvmd/plugins/hid/ch9329/mouse.py index 034aff92..da2259c4 100644 --- a/kvmd/plugins/hid/ch9329/mouse.py +++ b/kvmd/plugins/hid/ch9329/mouse.py @@ -27,8 +27,7 @@ from ....mouse import MouseRange class Mouse: # pylint: disable=too-many-instance-attributes def __init__(self) -> None: self.__active = "usb" - self.__button = "" - self.__clicked = False + self.__buttons = 0x00 self.__to_x = [0, 0] self.__to_y = [0, 0] self.__wheel_y = 0 @@ -36,8 +35,11 @@ class Mouse: # pylint: disable=too-many-instance-attributes self.__delta_y = 0 def button(self, button: str, clicked: bool) -> list[int]: - self.__button = button - self.__clicked = clicked + code = self.__button_code(button) + if code and self.__buttons: + self.__buttons &= ~code + if clicked: + self.__buttons |= code self.__wheel_y = 0 if self.__active != "usb": self.__to_x = [0, 0] @@ -74,25 +76,22 @@ class Mouse: # pylint: disable=too-many-instance-attributes self.__active = name def __absolute(self) -> list[int]: - code = 0x00 - if self.__clicked: - code = self.__button_code(self.__button) - cmd = [0x00, 0x04, 0x07, 0x02, code, 0x00, 0x00, 0x00, 0x00, 0x00] - if len(self.__to_x) == 2: - cmd[6] = self.__to_x[0] - cmd[5] = self.__to_x[1] - if len(self.__to_y) == 2: - cmd[8] = self.__to_y[0] - cmd[7] = self.__to_y[1] + cmd = [ + 0x00, 0x04, 0x07, 0x02, + self.__buttons, + self.__to_x[1], self.__to_x[0], + self.__to_y[1], self.__to_y[0], + 0x00] if self.__wheel_y: cmd[9] = self.__wheel_y return cmd def __relative(self) -> list[int]: - code = 0x00 - if self.__clicked: - code = self.__button_code(self.__button) - cmd = [0x00, 0x05, 0x05, 0x01, code, self.__delta_x, self.__delta_y, 0x00] + cmd = [ + 0x00, 0x05, 0x05, 0x01, + self.__buttons, + self.__delta_x, self.__delta_y, + 0x00] return cmd def __button_code(self, name: str) -> int: -- cgit v1.2.3 From 4220fe590839379ec67fa78c077c1d309cd5eb91 Mon Sep 17 00:00:00 2001 From: jacobbar Date: Thu, 16 Mar 2023 04:26:29 +0700 Subject: fixes ch9329 plugin multiple keyboard keys (#123) * Add ch9329 plugin --------- Co-authored-by: Maxim Devaev --- kvmd/plugins/hid/ch9329/keyboard.py | 31 ++++++++++++++++++++----------- 1 file changed, 20 insertions(+), 11 deletions(-) (limited to 'kvmd') diff --git a/kvmd/plugins/hid/ch9329/keyboard.py b/kvmd/plugins/hid/ch9329/keyboard.py index 70ccf547..cfbe3ece 100644 --- a/kvmd/plugins/hid/ch9329/keyboard.py +++ b/kvmd/plugins/hid/ch9329/keyboard.py @@ -33,14 +33,22 @@ class Keyboard: "caps": False, "scroll": False, }, self.__notifier, type=bool) - - self.__active_keys: list[list] = [] + self.__modifiers = 0x00 + self.__active_keys: list[int] = [] def key(self, key: str, state: bool) -> list[int]: + modifier = self.__is_modifier(key) + code = self.__keycode(key) + if not state: + if not modifier and code in self.__active_keys: + self.__active_keys.remove(code) + if modifier and self.__modifiers: + self.__modifiers &= ~code if state: - self.__active_keys.append([key, self.__is_modifier(key)]) - else: - self.__active_keys.remove([key, self.__is_modifier(key)]) + if not modifier and len(self.__active_keys) < 6: + self.__active_keys.append(code) + if modifier: + self.__modifiers |= code return self.__key() async def leds(self) -> dict: @@ -54,13 +62,14 @@ class Keyboard: self.__leds.update(num=num, caps=caps, scroll=scroll) def __key(self) -> list[int]: - cmd = [0x00, 0x02, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] + cmd = [ + 0x00, 0x02, 0x08, + self.__modifiers, + 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] counter = 0 - for key in self.__active_keys: - if key[1]: - cmd[3 + counter] = self.__keycode(key[0]) - else: - cmd[5 + counter] = self.__keycode(key[0]) + for code in self.__active_keys: + cmd[5 + counter] = code counter += 1 return cmd -- cgit v1.2.3 From ecf3faf984804a16f2def94b35359dd12bfdd0ac Mon Sep 17 00:00:00 2001 From: Maxim Devaev Date: Tue, 21 Mar 2023 03:43:20 +0200 Subject: refactoring --- kvmd/plugins/hid/ch9329/__init__.py | 29 ++++---- kvmd/plugins/hid/ch9329/keyboard.py | 72 +++++++++----------- kvmd/plugins/hid/ch9329/mouse.py | 127 ++++++++++++++++-------------------- 3 files changed, 100 insertions(+), 128 deletions(-) (limited to 'kvmd') diff --git a/kvmd/plugins/hid/ch9329/__init__.py b/kvmd/plugins/hid/ch9329/__init__.py index d8fea2de..c419a72d 100644 --- a/kvmd/plugins/hid/ch9329/__init__.py +++ b/kvmd/plugins/hid/ch9329/__init__.py @@ -117,26 +117,23 @@ class Plugin(BaseHid, multiprocessing.Process): # pylint: disable=too-many-inst async def get_state(self) -> dict: state = await self.__state_flags.get() - online = bool(state["online"]) - active_mouse = self.__mouse.active() - absolute = (active_mouse == "usb") - keyboard_leds = await self.__keyboard.leds() - + absolute = self.__mouse.is_absolute() + leds = await self.__keyboard.get_leds() return { - "online": online, + "online": state["online"], "busy": False, "connected": None, "keyboard": { - "online": True, - "leds": keyboard_leds, + "online": state["online"], + "leds": leds, "outputs": {"available": [], "active": ""}, }, "mouse": { - "online": True, + "online": state["online"], "absolute": absolute, "outputs": { "available": ["usb", "usb_rel"], - "active": active_mouse + "active": ("usb" if absolute else "usb_rel"), }, }, } @@ -165,24 +162,24 @@ class Plugin(BaseHid, multiprocessing.Process): # pylint: disable=too-many-inst def send_key_events(self, keys: Iterable[tuple[str, bool]]) -> None: for (key, state) in keys: - self.__queue_cmd(self.__keyboard.key(key, state)) + self.__queue_cmd(self.__keyboard.process_key(key, state)) def send_mouse_button_event(self, button: str, state: bool) -> None: - self.__queue_cmd(self.__mouse.button(button, state)) + self.__queue_cmd(self.__mouse.process_button(button, state)) def send_mouse_move_event(self, to_x: int, to_y: int) -> None: - self.__queue_cmd(self.__mouse.move(to_x, to_y)) + self.__queue_cmd(self.__mouse.process_move(to_x, to_y)) def send_mouse_wheel_event(self, delta_x: int, delta_y: int) -> None: - self.__queue_cmd(self.__mouse.wheel(delta_x, delta_y)) + self.__queue_cmd(self.__mouse.process_wheel(delta_x, delta_y)) def send_mouse_relative_event(self, delta_x: int, delta_y: int) -> None: - self.__queue_cmd(self.__mouse.relative(delta_x, delta_y)) + self.__queue_cmd(self.__mouse.process_relative(delta_x, delta_y)) def set_params(self, keyboard_output: (str | None)=None, mouse_output: (str | None)=None) -> None: if mouse_output is not None: get_logger(0).info("HID : mouse output = %s", mouse_output) - self.__mouse.set_active(mouse_output) + self.__mouse.set_absolute(mouse_output == "usb") self.__notifier.notify() def set_connected(self, connected: bool) -> None: diff --git a/kvmd/plugins/hid/ch9329/keyboard.py b/kvmd/plugins/hid/ch9329/keyboard.py index cfbe3ece..26eb7e6a 100644 --- a/kvmd/plugins/hid/ch9329/keyboard.py +++ b/kvmd/plugins/hid/ch9329/keyboard.py @@ -19,62 +19,50 @@ # # # ========================================================================== # + from .... import aiomulti from ....keyboard.mappings import KEYMAP +# ===== class Keyboard: def __init__(self) -> None: - - self.__notifier = aiomulti.AioProcessNotifier() self.__leds = aiomulti.AioSharedFlags({ "num": False, "caps": False, "scroll": False, - }, self.__notifier, type=bool) - self.__modifiers = 0x00 + }, aiomulti.AioProcessNotifier(), bool) + self.__modifiers = 0 self.__active_keys: list[int] = [] - def key(self, key: str, state: bool) -> list[int]: - modifier = self.__is_modifier(key) - code = self.__keycode(key) - if not state: - if not modifier and code in self.__active_keys: - self.__active_keys.remove(code) - if modifier and self.__modifiers: - self.__modifiers &= ~code - if state: - if not modifier and len(self.__active_keys) < 6: - self.__active_keys.append(code) - if modifier: - self.__modifiers |= code - return self.__key() - - async def leds(self) -> dict: - leds = await self.__leds.get() - return leds - def set_leds(self, led_byte: int) -> None: - num = bool(led_byte & 1) - caps = bool((led_byte >> 1) & 1) - scroll = bool((led_byte >> 2) & 1) - self.__leds.update(num=num, caps=caps, scroll=scroll) + self.__leds.update( + num=bool(led_byte & 1), + caps=bool((led_byte >> 1) & 1), + scroll=bool((led_byte >> 2) & 1), + ) - def __key(self) -> list[int]: + async def get_leds(self) -> dict[str, bool]: + return (await self.__leds.get()) + + def process_key(self, key: str, state: bool) -> list[int]: + code = KEYMAP[key].usb.code + is_modifier = KEYMAP[key].usb.is_modifier + if state: + if is_modifier: + self.__modifiers |= code + elif len(self.__active_keys) < 6 and code not in self.__active_keys: + self.__active_keys.append(code) + else: + if is_modifier: + self.__modifiers &= ~code + elif code in self.__active_keys: + self.__active_keys.remove(code) cmd = [ - 0x00, 0x02, 0x08, - self.__modifiers, - 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] - counter = 0 - for code in self.__active_keys: - cmd[5 + counter] = code - counter += 1 + 0, 0x02, 0x08, self.__modifiers, 0, + 0, 0, 0, 0, 0, 0, + ] + for (index, code) in enumerate(self.__active_keys): + cmd[index + 5] = code return cmd - - def __keycode(self, key: str) -> int: - return KEYMAP[key].usb.code - - def __is_modifier(self, key: str) -> bool: - return KEYMAP[key].usb.is_modifier diff --git a/kvmd/plugins/hid/ch9329/mouse.py b/kvmd/plugins/hid/ch9329/mouse.py index da2259c4..5e5497d2 100644 --- a/kvmd/plugins/hid/ch9329/mouse.py +++ b/kvmd/plugins/hid/ch9329/mouse.py @@ -19,84 +19,28 @@ # # # ========================================================================== # + import math from ....mouse import MouseRange +# ===== class Mouse: # pylint: disable=too-many-instance-attributes def __init__(self) -> None: - self.__active = "usb" - self.__buttons = 0x00 - self.__to_x = [0, 0] - self.__to_y = [0, 0] + self.__absolute = True + self.__buttons = 0 self.__wheel_y = 0 - self.__delta_x = 0 - self.__delta_y = 0 - def button(self, button: str, clicked: bool) -> list[int]: - code = self.__button_code(button) - if code and self.__buttons: - self.__buttons &= ~code - if clicked: - self.__buttons |= code - self.__wheel_y = 0 - if self.__active != "usb": - self.__to_x = [0, 0] - self.__to_y = [0, 0] - return self.__absolute() + def set_absolute(self, flag: bool) -> None: + self.__absolute = flag - def move(self, to_x: int, to_y: int) -> list[int]: - assert MouseRange.MIN <= to_x <= MouseRange.MAX - assert MouseRange.MIN <= to_y <= MouseRange.MAX - self.__to_x = self.__to_fixed(to_x) - self.__to_y = self.__to_fixed(to_y) - self.__wheel_y = 0 - return self.__absolute() + def is_absolute(self) -> bool: + return self.__absolute - def wheel(self, delta_x: int, delta_y: int) -> list[int]: - assert -127 <= delta_y <= 127 - _ = delta_x - self.__wheel_y = 1 if delta_y > 0 else 255 - return self.__absolute() - - def relative(self, delta_x: int, delta_y: int) -> list[int]: - assert -127 <= delta_x <= 127 - assert -127 <= delta_y <= 127 - delta_x = math.ceil(delta_x / 3) - delta_y = math.ceil(delta_y / 3) - self.__delta_x = delta_x if delta_x >= 0 else 255 + delta_x - self.__delta_y = delta_y if delta_y >= 0 else 255 + delta_y - return self.__relative() - - def active(self) -> str: - return self.__active - - def set_active(self, name: str) -> None: - self.__active = name - - def __absolute(self) -> list[int]: - cmd = [ - 0x00, 0x04, 0x07, 0x02, - self.__buttons, - self.__to_x[1], self.__to_x[0], - self.__to_y[1], self.__to_y[0], - 0x00] - if self.__wheel_y: - cmd[9] = self.__wheel_y - return cmd - - def __relative(self) -> list[int]: - cmd = [ - 0x00, 0x05, 0x05, 0x01, - self.__buttons, - self.__delta_x, self.__delta_y, - 0x00] - return cmd - - def __button_code(self, name: str) -> int: + def process_button(self, button: str, state: bool) -> list[int]: code = 0x00 - match name: + match button: case "left": code = 0x01 case "right": @@ -107,8 +51,51 @@ class Mouse: # pylint: disable=too-many-instance-attributes code = 0x08 case "down": code = 0x10 - return code + if code: + if state: + self.__buttons |= code + else: + self.__buttons &= ~code + self.__wheel_y = 0 + return self.__make_absolute_cmd(0, 0) + + def process_move(self, to_x: int, to_y: int) -> list[int]: + self.__wheel_y = 0 + return self.__make_absolute_cmd(to_x, to_y) + + def process_wheel(self, delta_x: int, delta_y: int) -> list[int]: + _ = delta_x + assert -127 <= delta_y <= 127 + self.__wheel_y = (1 if delta_y > 0 else 255) + return self.__make_absolute_cmd(0, 0) + + def __make_absolute_cmd(self, to_x: int, to_y: int) -> list[int]: + fixed_x = self.__fix_absolute(to_x) + fixed_y = self.__fix_absolute(to_y) + return [ + 0, 0x04, 0x07, 0x02, + self.__buttons, + fixed_x[1], fixed_x[0], + fixed_y[1], fixed_y[0], + self.__wheel_y, + ] + + def __fix_absolute(self, value: int) -> tuple[int, int]: + assert MouseRange.MIN <= value <= MouseRange.MAX + to_fixed = math.ceil(MouseRange.remap(value, 0, MouseRange.MAX) / 8) + return (to_fixed >> 8, to_fixed & 0xFF) + + def process_relative(self, delta_x: int, delta_y: int) -> list[int]: + delta_x = self.__fix_relative(delta_x) + delta_y = self.__fix_relative(delta_y) + return [ + 0, 0x05, 0x05, 0x01, + self.__buttons, + delta_x, delta_y, + self.__wheel_y, + ] - def __to_fixed(self, num: int) -> list[int]: - to_fixed = math.ceil(MouseRange.remap(num, 0, MouseRange.MAX) / 8) - return [to_fixed >> 8, to_fixed & 0xFF] + def __fix_relative(self, value: int) -> int: + assert -127 <= value <= 127 + value = math.ceil(value / 3) + return (value if value >= 0 else (255 + value)) -- cgit v1.2.3 From ddfe21d2b06f840d15387b5f1c896951fd099070 Mon Sep 17 00:00:00 2001 From: Maxim Devaev Date: Tue, 21 Mar 2023 08:38:36 +0200 Subject: ch9329: fixed abs mouse --- kvmd/plugins/hid/ch9329/mouse.py | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) (limited to 'kvmd') diff --git a/kvmd/plugins/hid/ch9329/mouse.py b/kvmd/plugins/hid/ch9329/mouse.py index 5e5497d2..918bd270 100644 --- a/kvmd/plugins/hid/ch9329/mouse.py +++ b/kvmd/plugins/hid/ch9329/mouse.py @@ -30,6 +30,8 @@ class Mouse: # pylint: disable=too-many-instance-attributes def __init__(self) -> None: self.__absolute = True self.__buttons = 0 + self.__to_x = (0, 0) + self.__to_y = (0, 0) self.__wheel_y = 0 def set_absolute(self, flag: bool) -> None: @@ -57,34 +59,34 @@ class Mouse: # pylint: disable=too-many-instance-attributes else: self.__buttons &= ~code self.__wheel_y = 0 - return self.__make_absolute_cmd(0, 0) + return self.__make_absolute_cmd() def process_move(self, to_x: int, to_y: int) -> list[int]: + self.__to_x = self.__fix_absolute(to_x) + self.__to_y = self.__fix_absolute(to_y) self.__wheel_y = 0 - return self.__make_absolute_cmd(to_x, to_y) + return self.__make_absolute_cmd() + + def __fix_absolute(self, value: int) -> tuple[int, int]: + assert MouseRange.MIN <= value <= MouseRange.MAX + to_fixed = math.ceil(MouseRange.remap(value, 0, MouseRange.MAX) / 8) + return (to_fixed >> 8, to_fixed & 0xFF) def process_wheel(self, delta_x: int, delta_y: int) -> list[int]: _ = delta_x assert -127 <= delta_y <= 127 self.__wheel_y = (1 if delta_y > 0 else 255) - return self.__make_absolute_cmd(0, 0) + return self.__make_absolute_cmd() - def __make_absolute_cmd(self, to_x: int, to_y: int) -> list[int]: - fixed_x = self.__fix_absolute(to_x) - fixed_y = self.__fix_absolute(to_y) + def __make_absolute_cmd(self) -> list[int]: return [ 0, 0x04, 0x07, 0x02, self.__buttons, - fixed_x[1], fixed_x[0], - fixed_y[1], fixed_y[0], + self.__to_x[1], self.__to_x[0], + self.__to_y[1], self.__to_y[0], self.__wheel_y, ] - def __fix_absolute(self, value: int) -> tuple[int, int]: - assert MouseRange.MIN <= value <= MouseRange.MAX - to_fixed = math.ceil(MouseRange.remap(value, 0, MouseRange.MAX) / 8) - return (to_fixed >> 8, to_fixed & 0xFF) - def process_relative(self, delta_x: int, delta_y: int) -> list[int]: delta_x = self.__fix_relative(delta_x) delta_y = self.__fix_relative(delta_y) -- cgit v1.2.3 From c58430258758db06c198b3c456449eb05eee017d Mon Sep 17 00:00:00 2001 From: Maxim Devaev Date: Tue, 21 Mar 2023 08:48:32 +0200 Subject: ch9329: fixed rel mouse --- kvmd/plugins/hid/ch9329/mouse.py | 3 +++ 1 file changed, 3 insertions(+) (limited to 'kvmd') diff --git a/kvmd/plugins/hid/ch9329/mouse.py b/kvmd/plugins/hid/ch9329/mouse.py index 918bd270..83c4eef3 100644 --- a/kvmd/plugins/hid/ch9329/mouse.py +++ b/kvmd/plugins/hid/ch9329/mouse.py @@ -58,6 +58,9 @@ class Mouse: # pylint: disable=too-many-instance-attributes self.__buttons |= code else: self.__buttons &= ~code + if not self.__absolute: + self.__to_x = (0, 0) + self.__to_y = (0, 0) self.__wheel_y = 0 return self.__make_absolute_cmd() -- cgit v1.2.3 From be4269fe61963fce556d5cf633694529adddde1d Mon Sep 17 00:00:00 2001 From: Maxim Devaev Date: Mon, 29 May 2023 17:34:24 +0300 Subject: refactoring --- kvmd/plugins/hid/ch9329/__init__.py | 51 ++++++++-------------------- kvmd/plugins/hid/ch9329/chip.py | 67 +++++++++++++++++++++++++++++++++++++ kvmd/plugins/hid/ch9329/tty.py | 62 ---------------------------------- 3 files changed, 81 insertions(+), 99 deletions(-) create mode 100644 kvmd/plugins/hid/ch9329/chip.py delete mode 100644 kvmd/plugins/hid/ch9329/tty.py (limited to 'kvmd') diff --git a/kvmd/plugins/hid/ch9329/__init__.py b/kvmd/plugins/hid/ch9329/__init__.py index c419a72d..ba4f8b3b 100644 --- a/kvmd/plugins/hid/ch9329/__init__.py +++ b/kvmd/plugins/hid/ch9329/__init__.py @@ -45,18 +45,11 @@ from ....validators.hw import valid_tty_speed from .. import BaseHid -from .tty import TTY +from .chip import ChipResponseError +from .chip import Chip from .mouse import Mouse from .keyboard import Keyboard -from .tty import get_info - - -class _ResError(Exception): - def __init__(self, msg: str) -> None: - super().__init__(msg) - self.msg = msg - # ===== class Plugin(BaseHid, multiprocessing.Process): # pylint: disable=too-many-instance-attributes @@ -94,7 +87,7 @@ class Plugin(BaseHid, multiprocessing.Process): # pylint: disable=too-many-inst }, self.__notifier, type=int) self.__stop_event = multiprocessing.Event() - self.__tty = TTY(device_path, speed, read_timeout) + self.__chip = Chip(device_path, speed, read_timeout) self.__keyboard = Keyboard() self.__mouse = Mouse() @@ -201,7 +194,7 @@ class Plugin(BaseHid, multiprocessing.Process): # pylint: disable=too-many-inst logger = aioproc.settle("HID", "hid") while not self.__stop_event.is_set(): try: - # self.__tty.connect() + # self.__chip.connect() self.__hid_loop() except Exception: logger.exception("Unexpected error in the run loop") @@ -222,7 +215,7 @@ class Plugin(BaseHid, multiprocessing.Process): # pylint: disable=too-many-inst cmd = self.__cmd_queue.get(timeout=0.1) # get_logger(0).info(f"HID : cmd = {cmd}") except queue.Empty: - self.__process_cmd(get_info()) + self.__process_cmd([]) else: self.__process_cmd(cmd) except Exception: @@ -231,35 +224,19 @@ class Plugin(BaseHid, multiprocessing.Process): # pylint: disable=too-many-inst time.sleep(2) def __process_cmd(self, cmd: list[int]) -> bool: # pylint: disable=too-many-branches - error_retval = False try: - res = self.__tty.send(cmd) - # get_logger(0).info(f"HID response = {res}") - if len(res) < 4: - raise _ResError("No response from HID - might be disconnected") - - if not self.__tty.check_res(res): - raise _ResError("Invalid response checksum ...") - - # Response Error - if res[4] == 1 and res[5] != 0: - raise _ResError("Command error code = " + str(res[5])) - - # get_info response - if res[3] == 0x81: - self.__keyboard.set_leds(res[7]) - self.__notifier.notify() - - self.__set_state_online(True) - return True - - except _ResError as err: + led_byte = self.__chip.xfer(cmd) + except ChipResponseError as err: self.__set_state_online(False) get_logger(0).info(err) - error_retval = False time.sleep(2) - - return error_retval + else: + if led_byte >= 0: + self.__keyboard.set_leds(led_byte) + self.__notifier.notify() + self.__set_state_online(True) + return True + return False def __set_state_online(self, online: bool) -> None: self.__state_flags.update(online=int(online)) diff --git a/kvmd/plugins/hid/ch9329/chip.py b/kvmd/plugins/hid/ch9329/chip.py new file mode 100644 index 00000000..df6292ad --- /dev/null +++ b/kvmd/plugins/hid/ch9329/chip.py @@ -0,0 +1,67 @@ +# ========================================================================== # +# # +# KVMD - The main PiKVM daemon. # +# # +# Copyright (C) 2018-2022 Maxim Devaev # +# # +# 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 . # +# # +# ========================================================================== # + + +import serial + + +# ===== +class ChipResponseError(Exception): + pass + + +# ===== +class Chip: + def __init__(self, device_path: str, speed: int, read_timeout: float) -> None: + self.__tty = serial.Serial(device_path, speed, timeout=read_timeout) + self.__device_path = device_path + + def xfer(self, cmd: list[int]) -> int: + self.__send(cmd) + return self.__recv() + + def __send(self, cmd: list[int]) -> None: + # RESET = [0x00,0x0F,0x00] + # GET_INFO = [0x00,0x01,0x00] + if len(cmd) == 0: + cmd = [0x00, 0x01, 0x00] + cmd = [0x57, 0xAB, *cmd, self.__make_checksum(cmd)] + self.__tty.write(serial.to_bytes(cmd)) + + def __recv(self) -> int: + data = self.__tty.read(5) + if len(data) < 5: + raise ChipResponseError("Too short response, HID might be disconnected") + + if data and data[4]: + data += self.__tty.read(data[4] + 1) + + if self.__make_checksum(data[:-1]) != data[-1]: + raise ChipResponseError("Invalid response checksum") + + if data[4] == 1 and data[5] != 0: + raise ChipResponseError(f"Response error code = {data[5]!r}") + + # led_byte (info) response + return (data[7] if data[3] == 0x81 else -1) + + def __make_checksum(self, cmd: (list[int] | bytes)) -> int: + return (sum(cmd) % 256) diff --git a/kvmd/plugins/hid/ch9329/tty.py b/kvmd/plugins/hid/ch9329/tty.py deleted file mode 100644 index 2e8cb22e..00000000 --- a/kvmd/plugins/hid/ch9329/tty.py +++ /dev/null @@ -1,62 +0,0 @@ -# ========================================================================== # -# # -# KVMD - The main PiKVM daemon. # -# # -# Copyright (C) 2018-2022 Maxim Devaev # -# # -# 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 . # -# # -# ========================================================================== # - - -import os -import serial - - -class TTY: - def __init__(self, device_path: str, speed: int, read_timeout: float) -> None: - self.__tty = serial.Serial(device_path, speed, timeout=read_timeout) - self.__device_path = device_path - - def has_device(self) -> bool: - return os.path.exists(self.__device_path) - - def send(self, cmd: list[int]) -> list[int]: - cmd = self.__wrap_cmd(cmd) - self.__tty.write(serial.to_bytes(cmd)) - data = list(self.__tty.read(5)) - if data and data[4]: - more_data = list(self.__tty.read(data[4] + 1)) - data.extend(more_data) - return data - - def check_res(self, res: list[int]) -> bool: - res_sum = res.pop() - return (self.__checksum(res) == res_sum) - - def __wrap_cmd(self, cmd: list[int]) -> list[int]: - cmd.insert(0, 0xAB) - cmd.insert(0, 0x57) - cmd.append(self.__checksum(cmd)) - return cmd - - def __checksum(self, cmd: list[int]) -> int: - return sum(cmd) % 256 - - -def get_info() -> list[int]: - return [0x00, 0x01, 0x00] - -# RESET = [0x00,0x0F,0x00] -# GET_INFO = [0x00,0x01,0x00] -- cgit v1.2.3 From ee9ff3cd468e3f285876d469f194463a3ac624c1 Mon Sep 17 00:00:00 2001 From: Maxim Devaev Date: Mon, 29 May 2023 22:40:30 +0300 Subject: ch9329: fixed checksum --- kvmd/plugins/hid/ch9329/chip.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'kvmd') diff --git a/kvmd/plugins/hid/ch9329/chip.py b/kvmd/plugins/hid/ch9329/chip.py index df6292ad..8f038042 100644 --- a/kvmd/plugins/hid/ch9329/chip.py +++ b/kvmd/plugins/hid/ch9329/chip.py @@ -43,7 +43,8 @@ class Chip: # GET_INFO = [0x00,0x01,0x00] if len(cmd) == 0: cmd = [0x00, 0x01, 0x00] - cmd = [0x57, 0xAB, *cmd, self.__make_checksum(cmd)] + cmd = [0x57, 0xAB] + cmd + cmd.append(self.__make_checksum(cmd)) self.__tty.write(serial.to_bytes(cmd)) def __recv(self) -> int: -- cgit v1.2.3 From 5a36dec615b0c50f845455777a4cc2b1b8408478 Mon Sep 17 00:00:00 2001 From: Maxim Devaev Date: Tue, 30 May 2023 01:34:15 +0300 Subject: ch9329: removed unused params --- kvmd/plugins/hid/ch9329/__init__.py | 25 +++---------------------- 1 file changed, 3 insertions(+), 22 deletions(-) (limited to 'kvmd') diff --git a/kvmd/plugins/hid/ch9329/__init__.py b/kvmd/plugins/hid/ch9329/__init__.py index ba4f8b3b..53ac2342 100644 --- a/kvmd/plugins/hid/ch9329/__init__.py +++ b/kvmd/plugins/hid/ch9329/__init__.py @@ -36,9 +36,6 @@ from .... import aioproc from ....yamlconf import Option -from ....validators.basic import valid_bool -from ....validators.basic import valid_int_f0 -from ....validators.basic import valid_int_f1 from ....validators.basic import valid_float_f01 from ....validators.os import valid_abs_path from ....validators.hw import valid_tty_speed @@ -58,11 +55,6 @@ class Plugin(BaseHid, multiprocessing.Process): # pylint: disable=too-many-inst device_path: str, speed: int, read_timeout: float, - read_retries: int, - common_retries: int, - retries_delay: float, - errors_threshold: int, - noop: bool, ) -> None: multiprocessing.Process.__init__(self, daemon=True) @@ -70,11 +62,6 @@ class Plugin(BaseHid, multiprocessing.Process): # pylint: disable=too-many-inst self.__device_path = device_path self.__speed = speed self.__read_timeout = read_timeout - self.__read_retries = read_retries - self.__common_retries = common_retries - self.__retries_delay = retries_delay - self.__errors_threshold = errors_threshold - self.__noop = noop self.__reset_required_event = multiprocessing.Event() self.__cmd_queue: "multiprocessing.Queue[list]" = multiprocessing.Queue() @@ -94,14 +81,9 @@ class Plugin(BaseHid, multiprocessing.Process): # pylint: disable=too-many-inst @classmethod def get_plugin_options(cls) -> dict: return { - "device": Option("/dev/kvmd-hid", type=valid_abs_path, unpack_as="device_path"), - "speed": Option(9600, type=valid_tty_speed), - "read_timeout": Option(0.3, type=valid_float_f01), - "read_retries": Option(5, type=valid_int_f1), - "common_retries": Option(5, type=valid_int_f1), - "retries_delay": Option(0.5, type=valid_float_f01), - "errors_threshold": Option(5, type=valid_int_f0), - "noop": Option(False, type=valid_bool), + "device": Option("/dev/kvmd-hid", type=valid_abs_path, unpack_as="device_path"), + "speed": Option(9600, type=valid_tty_speed), + "read_timeout": Option(0.3, type=valid_float_f01), } def sysprep(self) -> None: @@ -203,7 +185,6 @@ class Plugin(BaseHid, multiprocessing.Process): # pylint: disable=too-many-inst def __hid_loop(self) -> None: while not self.__stop_event.is_set(): try: - while not (self.__stop_event.is_set() and self.__cmd_queue.qsize() == 0): if self.__reset_required_event.is_set(): try: -- cgit v1.2.3 From ce81c872eaf8422beae9e33ffce133957f26a797 Mon Sep 17 00:00:00 2001 From: Maxim Devaev Date: Thu, 1 Jun 2023 16:54:08 +0300 Subject: ch9329: fixed mac issue (thanks @jacobb) --- kvmd/plugins/hid/ch9329/mouse.py | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) (limited to 'kvmd') diff --git a/kvmd/plugins/hid/ch9329/mouse.py b/kvmd/plugins/hid/ch9329/mouse.py index 83c4eef3..ea21150a 100644 --- a/kvmd/plugins/hid/ch9329/mouse.py +++ b/kvmd/plugins/hid/ch9329/mouse.py @@ -58,11 +58,11 @@ class Mouse: # pylint: disable=too-many-instance-attributes self.__buttons |= code else: self.__buttons &= ~code - if not self.__absolute: - self.__to_x = (0, 0) - self.__to_y = (0, 0) self.__wheel_y = 0 - return self.__make_absolute_cmd() + if not self.__absolute: + return self.__make_relative_cmd() + else: + return self.__make_absolute_cmd() def process_move(self, to_x: int, to_y: int) -> list[int]: self.__to_x = self.__fix_absolute(to_x) @@ -79,7 +79,16 @@ class Mouse: # pylint: disable=too-many-instance-attributes _ = delta_x assert -127 <= delta_y <= 127 self.__wheel_y = (1 if delta_y > 0 else 255) - return self.__make_absolute_cmd() + if not self.__absolute: + return self.__make_relative_cmd() + else: + return self.__make_absolute_cmd() + + def process_relative(self, delta_x: int, delta_y: int) -> list[int]: + self.__delta_x = self.__fix_relative(delta_x) + self.__delta_y = self.__fix_relative(delta_y) + self.__wheel_y = 0 + return self.__make_relative_cmd() def __make_absolute_cmd(self) -> list[int]: return [ @@ -89,16 +98,14 @@ class Mouse: # pylint: disable=too-many-instance-attributes self.__to_y[1], self.__to_y[0], self.__wheel_y, ] - - def process_relative(self, delta_x: int, delta_y: int) -> list[int]: - delta_x = self.__fix_relative(delta_x) - delta_y = self.__fix_relative(delta_y) + + def __make_relative_cmd(self) -> list[int]: return [ 0, 0x05, 0x05, 0x01, self.__buttons, - delta_x, delta_y, + self.__delta_x, self.__delta_y, self.__wheel_y, - ] + ] def __fix_relative(self, value: int) -> int: assert -127 <= value <= 127 -- cgit v1.2.3 From 2730b1184012f74968c90b35687b679a0a15b747 Mon Sep 17 00:00:00 2001 From: Maxim Devaev Date: Thu, 1 Jun 2023 17:07:40 +0300 Subject: ch9329: using bytes instead of list[int] --- kvmd/plugins/hid/ch9329/__init__.py | 8 ++++---- kvmd/plugins/hid/ch9329/chip.py | 12 ++++++------ kvmd/plugins/hid/ch9329/keyboard.py | 4 ++-- kvmd/plugins/hid/ch9329/mouse.py | 34 ++++++++++++++++++---------------- 4 files changed, 30 insertions(+), 28 deletions(-) (limited to 'kvmd') diff --git a/kvmd/plugins/hid/ch9329/__init__.py b/kvmd/plugins/hid/ch9329/__init__.py index 53ac2342..1a46c10e 100644 --- a/kvmd/plugins/hid/ch9329/__init__.py +++ b/kvmd/plugins/hid/ch9329/__init__.py @@ -64,7 +64,7 @@ class Plugin(BaseHid, multiprocessing.Process): # pylint: disable=too-many-inst self.__read_timeout = read_timeout self.__reset_required_event = multiprocessing.Event() - self.__cmd_queue: "multiprocessing.Queue[list]" = multiprocessing.Queue() + self.__cmd_queue: "multiprocessing.Queue[bytes]" = multiprocessing.Queue() self.__notifier = aiomulti.AioProcessNotifier() self.__state_flags = aiomulti.AioSharedFlags({ @@ -163,7 +163,7 @@ class Plugin(BaseHid, multiprocessing.Process): # pylint: disable=too-many-inst def clear_events(self) -> None: tools.clear_queue(self.__cmd_queue) - def __queue_cmd(self, cmd: list[int], clear: bool=False) -> None: + def __queue_cmd(self, cmd: bytes, clear: bool=False) -> None: if not self.__stop_event.is_set(): if clear: # FIXME: Если очистка производится со стороны процесса хида, то возможна гонка между @@ -196,7 +196,7 @@ class Plugin(BaseHid, multiprocessing.Process): # pylint: disable=too-many-inst cmd = self.__cmd_queue.get(timeout=0.1) # get_logger(0).info(f"HID : cmd = {cmd}") except queue.Empty: - self.__process_cmd([]) + self.__process_cmd(b"") else: self.__process_cmd(cmd) except Exception: @@ -204,7 +204,7 @@ class Plugin(BaseHid, multiprocessing.Process): # pylint: disable=too-many-inst get_logger(0).exception("Unexpected error in the HID loop") time.sleep(2) - def __process_cmd(self, cmd: list[int]) -> bool: # pylint: disable=too-many-branches + def __process_cmd(self, cmd: bytes) -> bool: # pylint: disable=too-many-branches try: led_byte = self.__chip.xfer(cmd) except ChipResponseError as err: diff --git a/kvmd/plugins/hid/ch9329/chip.py b/kvmd/plugins/hid/ch9329/chip.py index 8f038042..d883099b 100644 --- a/kvmd/plugins/hid/ch9329/chip.py +++ b/kvmd/plugins/hid/ch9329/chip.py @@ -34,17 +34,17 @@ class Chip: self.__tty = serial.Serial(device_path, speed, timeout=read_timeout) self.__device_path = device_path - def xfer(self, cmd: list[int]) -> int: + def xfer(self, cmd: bytes) -> int: self.__send(cmd) return self.__recv() - def __send(self, cmd: list[int]) -> None: + def __send(self, cmd: bytes) -> None: # RESET = [0x00,0x0F,0x00] # GET_INFO = [0x00,0x01,0x00] if len(cmd) == 0: - cmd = [0x00, 0x01, 0x00] - cmd = [0x57, 0xAB] + cmd - cmd.append(self.__make_checksum(cmd)) + cmd = b"\x00\x01\x00" + cmd = b"\x57\xAB" + cmd + cmd += self.__make_checksum(cmd).to_bytes() self.__tty.write(serial.to_bytes(cmd)) def __recv(self) -> int: @@ -64,5 +64,5 @@ class Chip: # led_byte (info) response return (data[7] if data[3] == 0x81 else -1) - def __make_checksum(self, cmd: (list[int] | bytes)) -> int: + def __make_checksum(self, cmd: bytes) -> int: return (sum(cmd) % 256) diff --git a/kvmd/plugins/hid/ch9329/keyboard.py b/kvmd/plugins/hid/ch9329/keyboard.py index 26eb7e6a..9aa16d9d 100644 --- a/kvmd/plugins/hid/ch9329/keyboard.py +++ b/kvmd/plugins/hid/ch9329/keyboard.py @@ -46,7 +46,7 @@ class Keyboard: async def get_leds(self) -> dict[str, bool]: return (await self.__leds.get()) - def process_key(self, key: str, state: bool) -> list[int]: + def process_key(self, key: str, state: bool) -> bytes: code = KEYMAP[key].usb.code is_modifier = KEYMAP[key].usb.is_modifier if state: @@ -65,4 +65,4 @@ class Keyboard: ] for (index, code) in enumerate(self.__active_keys): cmd[index + 5] = code - return cmd + return bytes(cmd) diff --git a/kvmd/plugins/hid/ch9329/mouse.py b/kvmd/plugins/hid/ch9329/mouse.py index ea21150a..0a0cfbcc 100644 --- a/kvmd/plugins/hid/ch9329/mouse.py +++ b/kvmd/plugins/hid/ch9329/mouse.py @@ -32,6 +32,8 @@ class Mouse: # pylint: disable=too-many-instance-attributes self.__buttons = 0 self.__to_x = (0, 0) self.__to_y = (0, 0) + self.__delta_x = 0 + self.__delta_y = 0 self.__wheel_y = 0 def set_absolute(self, flag: bool) -> None: @@ -40,7 +42,7 @@ class Mouse: # pylint: disable=too-many-instance-attributes def is_absolute(self) -> bool: return self.__absolute - def process_button(self, button: str, state: bool) -> list[int]: + def process_button(self, button: str, state: bool) -> bytes: code = 0x00 match button: case "left": @@ -60,11 +62,11 @@ class Mouse: # pylint: disable=too-many-instance-attributes self.__buttons &= ~code self.__wheel_y = 0 if not self.__absolute: - return self.__make_relative_cmd() + return self.__make_relative_cmd() else: - return self.__make_absolute_cmd() + return self.__make_absolute_cmd() - def process_move(self, to_x: int, to_y: int) -> list[int]: + def process_move(self, to_x: int, to_y: int) -> bytes: self.__to_x = self.__fix_absolute(to_x) self.__to_y = self.__fix_absolute(to_y) self.__wheel_y = 0 @@ -75,37 +77,37 @@ class Mouse: # pylint: disable=too-many-instance-attributes to_fixed = math.ceil(MouseRange.remap(value, 0, MouseRange.MAX) / 8) return (to_fixed >> 8, to_fixed & 0xFF) - def process_wheel(self, delta_x: int, delta_y: int) -> list[int]: + def process_wheel(self, delta_x: int, delta_y: int) -> bytes: _ = delta_x assert -127 <= delta_y <= 127 self.__wheel_y = (1 if delta_y > 0 else 255) if not self.__absolute: - return self.__make_relative_cmd() + return self.__make_relative_cmd() else: - return self.__make_absolute_cmd() - - def process_relative(self, delta_x: int, delta_y: int) -> list[int]: + return self.__make_absolute_cmd() + + def process_relative(self, delta_x: int, delta_y: int) -> bytes: self.__delta_x = self.__fix_relative(delta_x) self.__delta_y = self.__fix_relative(delta_y) self.__wheel_y = 0 return self.__make_relative_cmd() - def __make_absolute_cmd(self) -> list[int]: - return [ + def __make_absolute_cmd(self) -> bytes: + return bytes([ 0, 0x04, 0x07, 0x02, self.__buttons, self.__to_x[1], self.__to_x[0], self.__to_y[1], self.__to_y[0], self.__wheel_y, - ] - - def __make_relative_cmd(self) -> list[int]: - return [ + ]) + + def __make_relative_cmd(self) -> bytes: + return bytes([ 0, 0x05, 0x05, 0x01, self.__buttons, self.__delta_x, self.__delta_y, self.__wheel_y, - ] + ]) def __fix_relative(self, value: int) -> int: assert -127 <= value <= 127 -- cgit v1.2.3 From 2e6f0da14142e2f5c3bd548052fa9f189e2d00eb Mon Sep 17 00:00:00 2001 From: Maxim Devaev Date: Thu, 1 Jun 2023 17:35:16 +0300 Subject: ch9329: fixed int to byte conversion --- kvmd/plugins/hid/ch9329/chip.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'kvmd') diff --git a/kvmd/plugins/hid/ch9329/chip.py b/kvmd/plugins/hid/ch9329/chip.py index d883099b..e6524f10 100644 --- a/kvmd/plugins/hid/ch9329/chip.py +++ b/kvmd/plugins/hid/ch9329/chip.py @@ -44,8 +44,8 @@ class Chip: if len(cmd) == 0: cmd = b"\x00\x01\x00" cmd = b"\x57\xAB" + cmd - cmd += self.__make_checksum(cmd).to_bytes() - self.__tty.write(serial.to_bytes(cmd)) + cmd += self.__make_checksum(cmd).to_bytes(1, "big") + self.__tty.write(cmd) def __recv(self) -> int: data = self.__tty.read(5) -- cgit v1.2.3 From 8e2a5284183977d15c537ae5d26efd8fd7833cd6 Mon Sep 17 00:00:00 2001 From: Maxim Devaev Date: Mon, 10 Jul 2023 03:07:53 +0300 Subject: ch9329: reconnect logic --- kvmd/plugins/hid/ch9329/__init__.py | 33 +++++++++++++++++---------------- kvmd/plugins/hid/ch9329/chip.py | 22 ++++++++++++++++++---- 2 files changed, 35 insertions(+), 20 deletions(-) (limited to 'kvmd') diff --git a/kvmd/plugins/hid/ch9329/__init__.py b/kvmd/plugins/hid/ch9329/__init__.py index 1a46c10e..e7f518d5 100644 --- a/kvmd/plugins/hid/ch9329/__init__.py +++ b/kvmd/plugins/hid/ch9329/__init__.py @@ -43,6 +43,7 @@ from ....validators.hw import valid_tty_speed from .. import BaseHid from .chip import ChipResponseError +from .chip import ChipConnection from .chip import Chip from .mouse import Mouse from .keyboard import Keyboard @@ -176,7 +177,6 @@ class Plugin(BaseHid, multiprocessing.Process): # pylint: disable=too-many-inst logger = aioproc.settle("HID", "hid") while not self.__stop_event.is_set(): try: - # self.__chip.connect() self.__hid_loop() except Exception: logger.exception("Unexpected error in the run loop") @@ -185,28 +185,29 @@ class Plugin(BaseHid, multiprocessing.Process): # pylint: disable=too-many-inst def __hid_loop(self) -> None: while not self.__stop_event.is_set(): try: - while not (self.__stop_event.is_set() and self.__cmd_queue.qsize() == 0): - if self.__reset_required_event.is_set(): + with self.__chip.connected() as conn: + while not (self.__stop_event.is_set() and self.__cmd_queue.qsize() == 0): + if self.__reset_required_event.is_set(): + try: + self.__set_state_busy(True) + # self.__process_request(conn, RESET) + finally: + self.__reset_required_event.clear() try: - self.__set_state_busy(True) - # self.__process_request(conn, RESET) - finally: - self.__reset_required_event.clear() - try: - cmd = self.__cmd_queue.get(timeout=0.1) - # get_logger(0).info(f"HID : cmd = {cmd}") - except queue.Empty: - self.__process_cmd(b"") - else: - self.__process_cmd(cmd) + cmd = self.__cmd_queue.get(timeout=0.1) + # get_logger(0).info(f"HID : cmd = {cmd}") + except queue.Empty: + self.__process_cmd(conn, b"") + else: + self.__process_cmd(conn, cmd) except Exception: self.clear_events() get_logger(0).exception("Unexpected error in the HID loop") time.sleep(2) - def __process_cmd(self, cmd: bytes) -> bool: # pylint: disable=too-many-branches + def __process_cmd(self, conn: ChipConnection, cmd: bytes) -> bool: # pylint: disable=too-many-branches try: - led_byte = self.__chip.xfer(cmd) + led_byte = conn.xfer(cmd) except ChipResponseError as err: self.__set_state_online(False) get_logger(0).info(err) diff --git a/kvmd/plugins/hid/ch9329/chip.py b/kvmd/plugins/hid/ch9329/chip.py index e6524f10..b8631ec0 100644 --- a/kvmd/plugins/hid/ch9329/chip.py +++ b/kvmd/plugins/hid/ch9329/chip.py @@ -21,6 +21,9 @@ import serial +import contextlib + +from typing import Generator # ===== @@ -29,10 +32,9 @@ class ChipResponseError(Exception): # ===== -class Chip: - def __init__(self, device_path: str, speed: int, read_timeout: float) -> None: - self.__tty = serial.Serial(device_path, speed, timeout=read_timeout) - self.__device_path = device_path +class ChipConnection: + def __init__(self, tty: serial.Serial) -> None: + self.__tty = tty def xfer(self, cmd: bytes) -> int: self.__send(cmd) @@ -66,3 +68,15 @@ class Chip: def __make_checksum(self, cmd: bytes) -> int: return (sum(cmd) % 256) + + +class Chip: + def __init__(self, device_path: str, speed: int, read_timeout: float) -> None: + self.__device_path = device_path + self.__speed = speed + self.__read_timeout = read_timeout + + @contextlib.contextmanager + def connected(self) -> Generator[ChipConnection, None, None]: # type: ignore + with serial.Serial(self.__device_path, self.__speed, timeout=self.__read_timeout) as tty: + yield ChipConnection(tty) -- cgit v1.2.3