From dc0340583ecd2f76939edfc85cf69417743c088d Mon Sep 17 00:00:00 2001 From: Devaev Maxim Date: Wed, 28 Oct 2020 21:19:19 +0300 Subject: splitting serial --- kvmd/plugins/hid/serial.py | 463 ------------------------------------ kvmd/plugins/hid/serial/__init__.py | 422 ++++++++++++++++++++++++++++++++ kvmd/plugins/hid/serial/gpio.py | 71 ++++++ 3 files changed, 493 insertions(+), 463 deletions(-) delete mode 100644 kvmd/plugins/hid/serial.py create mode 100644 kvmd/plugins/hid/serial/__init__.py create mode 100644 kvmd/plugins/hid/serial/gpio.py (limited to 'kvmd/plugins/hid') diff --git a/kvmd/plugins/hid/serial.py b/kvmd/plugins/hid/serial.py deleted file mode 100644 index a45cd89a..00000000 --- a/kvmd/plugins/hid/serial.py +++ /dev/null @@ -1,463 +0,0 @@ -# ========================================================================== # -# # -# KVMD - The main Pi-KVM daemon. # -# # -# Copyright (C) 2018 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 multiprocessing -import dataclasses -import queue -import struct -import errno -import time - -from typing import Tuple -from typing import List -from typing import Dict -from typing import Iterable -from typing import AsyncGenerator -from typing import Optional - -import gpiod -import serial - -from ...logging import get_logger - -from ...keyboard.mappings import KEYMAP - -from ... import env -from ... import tools -from ... import aiotools -from ... import aiomulti -from ... import aioproc -from ... import aiogp - -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 ...validators.hw import valid_gpio_pin_optional - -from . import BaseHid - - -# ===== -class _RequestError(Exception): - def __init__(self, msg: str, online: bool=False) -> None: - super().__init__(msg) - self.msg = msg - self.online = online - - -class _PermRequestError(_RequestError): - pass - - -class _TempRequestError(_RequestError): - pass - - -# ===== -class _BaseEvent: - def make_command(self) -> bytes: - raise NotImplementedError - - -class _ClearEvent(_BaseEvent): - def make_command(self) -> bytes: - return b"\x10\x00\x00\x00\x00" - - -@dataclasses.dataclass(frozen=True) -class _KeyEvent(_BaseEvent): - name: str - state: bool - - def __post_init__(self) -> None: - assert self.name in KEYMAP - - def make_command(self) -> bytes: - code = KEYMAP[self.name].serial.code - return struct.pack(">BBBxx", 0x11, code, int(self.state)) - - -@dataclasses.dataclass(frozen=True) -class _MouseButtonEvent(_BaseEvent): - name: str - state: bool - - def __post_init__(self) -> None: - assert self.name in ["left", "right", "middle", "up", "down"] - - def make_command(self) -> bytes: - (code, state_pressed, is_main) = { - "left": (0b10000000, 0b00001000, True), - "right": (0b01000000, 0b00000100, True), - "middle": (0b00100000, 0b00000010, True), - "up": (0b10000000, 0b00001000, False), # Back - "down": (0b01000000, 0b00000100, False), # Forward - }[self.name] - if self.state: - code |= state_pressed - if is_main: - main_code = code - extra_code = 0 - else: - main_code = 0 - extra_code = code - return struct.pack(">BBBxx", 0x13, main_code, extra_code) - - -@dataclasses.dataclass(frozen=True) -class _MouseMoveEvent(_BaseEvent): - to_x: int - to_y: int - - def __post_init__(self) -> None: - assert -32768 <= self.to_x <= 32767 - assert -32768 <= self.to_y <= 32767 - - def make_command(self) -> bytes: - return struct.pack(">Bhh", 0x12, self.to_x, self.to_y) - - -@dataclasses.dataclass(frozen=True) -class _MouseWheelEvent(_BaseEvent): - delta_x: int - delta_y: int - - def __post_init__(self) -> None: - assert -127 <= self.delta_x <= 127 - assert -127 <= self.delta_y <= 127 - - def make_command(self) -> bytes: - # Горизонтальная прокрутка пока не поддерживается - return struct.pack(">Bxbxx", 0x14, self.delta_y) - - -class _Gpio: - def __init__(self, reset_pin: int, reset_delay: float) -> None: - self.__reset_pin = reset_pin - self.__reset_delay = reset_delay - - self.__chip: Optional[gpiod.Chip] = None - self.__reset_line: Optional[gpiod.Line] = None - self.__reset_wip = False - - def open(self) -> None: - if self.__reset_pin >= 0: - assert self.__chip is None - assert self.__reset_line is None - self.__chip = gpiod.Chip(env.GPIO_DEVICE_PATH) - self.__reset_line = self.__chip.get_line(self.__reset_pin) - self.__reset_line.request("kvmd::hid-serial::reset", gpiod.LINE_REQ_DIR_OUT, default_vals=[0]) - - def close(self) -> None: - if self.__chip: - try: - self.__chip.close() - except Exception: - pass - - @aiotools.atomic - async def reset(self) -> None: - if self.__reset_pin >= 0: - assert self.__reset_line - if not self.__reset_wip: - self.__reset_wip = True - try: - await aiogp.pulse(self.__reset_line, self.__reset_delay, 1) - finally: - self.__reset_wip = False - get_logger(0).info("Reset HID performed") - else: - get_logger(0).info("Another reset HID in progress") - - -# ===== -class Plugin(BaseHid, multiprocessing.Process): # pylint: disable=too-many-instance-attributes - def __init__( # pylint: disable=too-many-arguments,super-init-not-called - self, - reset_pin: int, - reset_delay: float, - - 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.__gpio = _Gpio(reset_pin, reset_delay) - - self.__events_queue: "multiprocessing.Queue[_BaseEvent]" = multiprocessing.Queue() - - self.__notifier = aiomulti.AioProcessNotifier() - self.__state_flags = aiomulti.AioSharedFlags({ - "online": True, - "caps": False, - "scroll": False, - "num": False, - }, self.__notifier) - - self.__stop_event = multiprocessing.Event() - - @classmethod - def get_plugin_options(cls) -> Dict: - return { - "reset_pin": Option(-1, type=valid_gpio_pin_optional), - "reset_delay": Option(0.1, type=valid_float_f01), - - "device": Option("", type=valid_abs_path, unpack_as="device_path"), - "speed": Option(115200, type=valid_tty_speed), - "read_timeout": Option(2.0, type=valid_float_f01), - "read_retries": Option(10, type=valid_int_f1), - "common_retries": Option(100, type=valid_int_f1), - "retries_delay": Option(0.1, type=valid_float_f01), - "errors_threshold": Option(5, type=valid_int_f0), - "noop": Option(False, type=valid_bool), - } - - def sysprep(self) -> None: - self.__gpio.open() - get_logger(0).info("Starting HID daemon ...") - self.start() - - async def get_state(self) -> Dict: - state = await self.__state_flags.get() - return { - "online": state["online"], - "keyboard": { - "online": state["online"], - "leds": { - "caps": state["caps"], - "scroll": state["scroll"], - "num": state["num"], - }, - }, - "mouse": {"online": state["online"]}, - } - - 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() - - @aiotools.atomic - async def reset(self) -> None: - await self.__gpio.reset() - - @aiotools.atomic - async def cleanup(self) -> None: - logger = get_logger(0) - try: - if self.is_alive(): - logger.info("Stopping HID daemon ...") - self.__stop_event.set() - if self.exitcode is not None: - self.join() - if os.path.exists(self.__device_path): - get_logger().info("Clearing HID events ...") - try: - with self.__get_serial() as tty: - self.__process_command(tty, b"\x10\x00\x00\x00\x00") - except Exception: - logger.exception("Can't clear HID events") - finally: - self.__gpio.close() - - # ===== - - def send_key_events(self, keys: Iterable[Tuple[str, bool]]) -> None: - for (key, state) in keys: - self.__queue_event(_KeyEvent(key, state)) - - def send_mouse_button_event(self, button: str, state: bool) -> None: - self.__queue_event(_MouseButtonEvent(button, state)) - - def send_mouse_move_event(self, to_x: int, to_y: int) -> None: - self.__queue_event(_MouseMoveEvent(to_x, to_y)) - - def send_mouse_wheel_event(self, delta_x: int, delta_y: int) -> None: - self.__queue_event(_MouseWheelEvent(delta_x, delta_y)) - - def clear_events(self) -> None: - # FIXME: Если очистка производится со стороны процесса хида, то возможна гонка между - # очисткой и добавлением события _ClearEvent. Неприятно, но не смертельно. - # Починить блокировкой после перехода на асинхронные очереди. - tools.clear_queue(self.__events_queue) - self.__queue_event(_ClearEvent()) - - def __queue_event(self, event: _BaseEvent) -> None: - if not self.__stop_event.is_set(): - self.__events_queue.put_nowait(event) - - def run(self) -> None: # pylint: disable=too-many-branches - logger = get_logger(0) - - logger.info("Started HID pid=%d", os.getpid()) - aioproc.ignore_sigint() - aioproc.rename_process("hid") - - while not self.__stop_event.is_set(): - try: - with self.__get_serial() as tty: - while not (self.__stop_event.is_set() and self.__events_queue.qsize() == 0): - try: - event = self.__events_queue.get(timeout=0.1) - except queue.Empty: - self.__process_command(tty, b"\x01\x00\x00\x00\x00") # Ping - else: - if not self.__process_command(tty, event.make_command()): - self.clear_events() - - except Exception as err: - self.clear_events() - if isinstance(err, serial.SerialException) and err.errno == errno.ENOENT: # pylint: disable=no-member - logger.error("Missing HID serial device: %s", self.__device_path) - else: - logger.exception("Unexpected HID error") - time.sleep(1) - - def __get_serial(self) -> serial.Serial: - return serial.Serial(self.__device_path, self.__speed, timeout=self.__read_timeout) - - def __process_command(self, tty: serial.Serial, command: bytes) -> bool: - return self.__process_request(tty, self.__make_request(command)) - - def __process_request(self, tty: serial.Serial, request: bytes) -> bool: # pylint: disable=too-many-branches - logger = get_logger() - error_messages: List[str] = [] - live_log_errors = False - - common_retries = self.__common_retries - read_retries = self.__read_retries - error_retval = False - - while common_retries and read_retries: - response = self.__send_request(tty, request) - try: - if len(response) < 4: - read_retries -= 1 - raise _TempRequestError(f"No response from HID: request={request!r}") - - assert len(response) == 4, response - if self.__make_crc16(response[-4:-2]) != struct.unpack(">H", response[-2:])[0]: - request = self.__make_request(b"\x02\x00\x00\x00\x00") # Repeat an answer - raise _TempRequestError("Invalid response CRC; requesting response again ...") - - code = response[1] - if code == 0x48: # Request timeout # pylint: disable=no-else-raise - raise _TempRequestError(f"Got request timeout from HID: request={request!r}") - elif code == 0x40: # CRC Error - raise _TempRequestError(f"Got CRC error of request from HID: request={request!r}") - elif code == 0x45: # Unknown command - raise _PermRequestError(f"HID did not recognize the request={request!r}", online=True) - elif code == 0x24: # Rebooted? - raise _PermRequestError("No previous command state inside HID, seems it was rebooted", online=True) - elif code == 0x20: # Done - self.__state_flags.update(online=True) - return True - elif code & 0x80: # Pong with leds - self.__state_flags.update( - online=True, - caps=bool(code & 0b00000001), - scroll=bool(code & 0x00000010), - num=bool(code & 0x00000100), - ) - return True - raise _TempRequestError(f"Invalid response from HID: request={request!r}; code=0x{code:02X}") - - except _RequestError as err: - common_retries -= 1 - self.__state_flags.update(online=err.online) - error_retval = err.online - - if live_log_errors: - logger.error(err.msg) - else: - error_messages.append(err.msg) - if len(error_messages) > self.__errors_threshold: - for msg in error_messages: - logger.error(msg) - error_messages = [] - live_log_errors = True - - if isinstance(err, _PermRequestError): - break - if common_retries and read_retries: - time.sleep(self.__retries_delay) - - for msg in error_messages: - logger.error(msg) - if not (common_retries and read_retries): - logger.error("Can't process HID request due many errors: %r", request) - return error_retval - - def __send_request(self, tty: serial.Serial, request: bytes) -> bytes: - if not self.__noop: - if tty.in_waiting: - tty.read(tty.in_waiting) - assert tty.write(request) == len(request) - response = tty.read(4) - else: - response = b"\x33\x20" # Magic + OK - response += struct.pack(">H", self.__make_crc16(response)) - return response - - def __make_request(self, command: bytes) -> bytes: - request = b"\x33" + command - request += struct.pack(">H", self.__make_crc16(request)) - assert len(request) == 8, (request, command) - return request - - def __make_crc16(self, data: bytes) -> int: - crc = 0xFFFF - for byte in data: - crc = crc ^ byte - for _ in range(8): - if crc & 0x0001 == 0: - crc = crc >> 1 - else: - crc = crc >> 1 - crc = crc ^ 0xA001 - return crc diff --git a/kvmd/plugins/hid/serial/__init__.py b/kvmd/plugins/hid/serial/__init__.py new file mode 100644 index 00000000..2b1c0af6 --- /dev/null +++ b/kvmd/plugins/hid/serial/__init__.py @@ -0,0 +1,422 @@ +# ========================================================================== # +# # +# KVMD - The main Pi-KVM daemon. # +# # +# Copyright (C) 2018 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 multiprocessing +import dataclasses +import queue +import struct +import errno +import time + +from typing import Tuple +from typing import List +from typing import Dict +from typing import Iterable +from typing import AsyncGenerator + +import serial + +from ....logging import get_logger + +from ....keyboard.mappings import KEYMAP + +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 ....validators.hw import valid_gpio_pin_optional + +from .. import BaseHid + +from .gpio import Gpio + + +# ===== +class _RequestError(Exception): + def __init__(self, msg: str, online: bool=False) -> None: + super().__init__(msg) + self.msg = msg + self.online = online + + +class _PermRequestError(_RequestError): + pass + + +class _TempRequestError(_RequestError): + pass + + +# ===== +class _BaseEvent: + def make_command(self) -> bytes: + raise NotImplementedError + + +class _ClearEvent(_BaseEvent): + def make_command(self) -> bytes: + return b"\x10\x00\x00\x00\x00" + + +@dataclasses.dataclass(frozen=True) +class _KeyEvent(_BaseEvent): + name: str + state: bool + + def __post_init__(self) -> None: + assert self.name in KEYMAP + + def make_command(self) -> bytes: + code = KEYMAP[self.name].serial.code + return struct.pack(">BBBxx", 0x11, code, int(self.state)) + + +@dataclasses.dataclass(frozen=True) +class _MouseButtonEvent(_BaseEvent): + name: str + state: bool + + def __post_init__(self) -> None: + assert self.name in ["left", "right", "middle", "up", "down"] + + def make_command(self) -> bytes: + (code, state_pressed, is_main) = { + "left": (0b10000000, 0b00001000, True), + "right": (0b01000000, 0b00000100, True), + "middle": (0b00100000, 0b00000010, True), + "up": (0b10000000, 0b00001000, False), # Back + "down": (0b01000000, 0b00000100, False), # Forward + }[self.name] + if self.state: + code |= state_pressed + if is_main: + main_code = code + extra_code = 0 + else: + main_code = 0 + extra_code = code + return struct.pack(">BBBxx", 0x13, main_code, extra_code) + + +@dataclasses.dataclass(frozen=True) +class _MouseMoveEvent(_BaseEvent): + to_x: int + to_y: int + + def __post_init__(self) -> None: + assert -32768 <= self.to_x <= 32767 + assert -32768 <= self.to_y <= 32767 + + def make_command(self) -> bytes: + return struct.pack(">Bhh", 0x12, self.to_x, self.to_y) + + +@dataclasses.dataclass(frozen=True) +class _MouseWheelEvent(_BaseEvent): + delta_x: int + delta_y: int + + def __post_init__(self) -> None: + assert -127 <= self.delta_x <= 127 + assert -127 <= self.delta_y <= 127 + + def make_command(self) -> bytes: + # Горизонтальная прокрутка пока не поддерживается + return struct.pack(">Bxbxx", 0x14, self.delta_y) + + +# ===== +class Plugin(BaseHid, multiprocessing.Process): # pylint: disable=too-many-instance-attributes + def __init__( # pylint: disable=too-many-arguments,super-init-not-called + self, + reset_pin: int, + reset_delay: float, + + 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.__gpio = Gpio(reset_pin, reset_delay) + + self.__events_queue: "multiprocessing.Queue[_BaseEvent]" = multiprocessing.Queue() + + self.__notifier = aiomulti.AioProcessNotifier() + self.__state_flags = aiomulti.AioSharedFlags({ + "online": True, + "caps": False, + "scroll": False, + "num": False, + }, self.__notifier) + + self.__stop_event = multiprocessing.Event() + + @classmethod + def get_plugin_options(cls) -> Dict: + return { + "reset_pin": Option(-1, type=valid_gpio_pin_optional), + "reset_delay": Option(0.1, type=valid_float_f01), + + "device": Option("", type=valid_abs_path, unpack_as="device_path"), + "speed": Option(115200, type=valid_tty_speed), + "read_timeout": Option(2.0, type=valid_float_f01), + "read_retries": Option(10, type=valid_int_f1), + "common_retries": Option(100, type=valid_int_f1), + "retries_delay": Option(0.1, type=valid_float_f01), + "errors_threshold": Option(5, type=valid_int_f0), + "noop": Option(False, type=valid_bool), + } + + def sysprep(self) -> None: + self.__gpio.open() + get_logger(0).info("Starting HID daemon ...") + self.start() + + async def get_state(self) -> Dict: + state = await self.__state_flags.get() + return { + "online": state["online"], + "keyboard": { + "online": state["online"], + "leds": { + "caps": state["caps"], + "scroll": state["scroll"], + "num": state["num"], + }, + }, + "mouse": {"online": state["online"]}, + } + + 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() + + @aiotools.atomic + async def reset(self) -> None: + await self.__gpio.reset() + + @aiotools.atomic + async def cleanup(self) -> None: + logger = get_logger(0) + try: + if self.is_alive(): + logger.info("Stopping HID daemon ...") + self.__stop_event.set() + if self.exitcode is not None: + self.join() + if os.path.exists(self.__device_path): + get_logger().info("Clearing HID events ...") + try: + with self.__get_serial() as tty: + self.__process_command(tty, b"\x10\x00\x00\x00\x00") + except Exception: + logger.exception("Can't clear HID events") + finally: + self.__gpio.close() + + # ===== + + def send_key_events(self, keys: Iterable[Tuple[str, bool]]) -> None: + for (key, state) in keys: + self.__queue_event(_KeyEvent(key, state)) + + def send_mouse_button_event(self, button: str, state: bool) -> None: + self.__queue_event(_MouseButtonEvent(button, state)) + + def send_mouse_move_event(self, to_x: int, to_y: int) -> None: + self.__queue_event(_MouseMoveEvent(to_x, to_y)) + + def send_mouse_wheel_event(self, delta_x: int, delta_y: int) -> None: + self.__queue_event(_MouseWheelEvent(delta_x, delta_y)) + + def clear_events(self) -> None: + # FIXME: Если очистка производится со стороны процесса хида, то возможна гонка между + # очисткой и добавлением события _ClearEvent. Неприятно, но не смертельно. + # Починить блокировкой после перехода на асинхронные очереди. + tools.clear_queue(self.__events_queue) + self.__queue_event(_ClearEvent()) + + def __queue_event(self, event: _BaseEvent) -> None: + if not self.__stop_event.is_set(): + self.__events_queue.put_nowait(event) + + def run(self) -> None: # pylint: disable=too-many-branches + logger = get_logger(0) + + logger.info("Started HID pid=%d", os.getpid()) + aioproc.ignore_sigint() + aioproc.rename_process("hid") + + while not self.__stop_event.is_set(): + try: + with self.__get_serial() as tty: + while not (self.__stop_event.is_set() and self.__events_queue.qsize() == 0): + try: + event = self.__events_queue.get(timeout=0.1) + except queue.Empty: + self.__process_command(tty, b"\x01\x00\x00\x00\x00") # Ping + else: + if not self.__process_command(tty, event.make_command()): + self.clear_events() + + except Exception as err: + self.clear_events() + if isinstance(err, serial.SerialException) and err.errno == errno.ENOENT: # pylint: disable=no-member + logger.error("Missing HID serial device: %s", self.__device_path) + else: + logger.exception("Unexpected HID error") + time.sleep(1) + + def __get_serial(self) -> serial.Serial: + return serial.Serial(self.__device_path, self.__speed, timeout=self.__read_timeout) + + def __process_command(self, tty: serial.Serial, command: bytes) -> bool: + return self.__process_request(tty, self.__make_request(command)) + + def __process_request(self, tty: serial.Serial, request: bytes) -> bool: # pylint: disable=too-many-branches + logger = get_logger() + error_messages: List[str] = [] + live_log_errors = False + + common_retries = self.__common_retries + read_retries = self.__read_retries + error_retval = False + + while common_retries and read_retries: + response = self.__send_request(tty, request) + try: + if len(response) < 4: + read_retries -= 1 + raise _TempRequestError(f"No response from HID: request={request!r}") + + assert len(response) == 4, response + if self.__make_crc16(response[-4:-2]) != struct.unpack(">H", response[-2:])[0]: + request = self.__make_request(b"\x02\x00\x00\x00\x00") # Repeat an answer + raise _TempRequestError("Invalid response CRC; requesting response again ...") + + code = response[1] + if code == 0x48: # Request timeout # pylint: disable=no-else-raise + raise _TempRequestError(f"Got request timeout from HID: request={request!r}") + elif code == 0x40: # CRC Error + raise _TempRequestError(f"Got CRC error of request from HID: request={request!r}") + elif code == 0x45: # Unknown command + raise _PermRequestError(f"HID did not recognize the request={request!r}", online=True) + elif code == 0x24: # Rebooted? + raise _PermRequestError("No previous command state inside HID, seems it was rebooted", online=True) + elif code == 0x20: # Done + self.__state_flags.update(online=True) + return True + elif code & 0x80: # Pong with leds + self.__state_flags.update( + online=True, + caps=bool(code & 0b00000001), + scroll=bool(code & 0x00000010), + num=bool(code & 0x00000100), + ) + return True + raise _TempRequestError(f"Invalid response from HID: request={request!r}; code=0x{code:02X}") + + except _RequestError as err: + common_retries -= 1 + self.__state_flags.update(online=err.online) + error_retval = err.online + + if live_log_errors: + logger.error(err.msg) + else: + error_messages.append(err.msg) + if len(error_messages) > self.__errors_threshold: + for msg in error_messages: + logger.error(msg) + error_messages = [] + live_log_errors = True + + if isinstance(err, _PermRequestError): + break + if common_retries and read_retries: + time.sleep(self.__retries_delay) + + for msg in error_messages: + logger.error(msg) + if not (common_retries and read_retries): + logger.error("Can't process HID request due many errors: %r", request) + return error_retval + + def __send_request(self, tty: serial.Serial, request: bytes) -> bytes: + if not self.__noop: + if tty.in_waiting: + tty.read(tty.in_waiting) + assert tty.write(request) == len(request) + response = tty.read(4) + else: + response = b"\x33\x20" # Magic + OK + response += struct.pack(">H", self.__make_crc16(response)) + return response + + def __make_request(self, command: bytes) -> bytes: + request = b"\x33" + command + request += struct.pack(">H", self.__make_crc16(request)) + assert len(request) == 8, (request, command) + return request + + def __make_crc16(self, data: bytes) -> int: + crc = 0xFFFF + for byte in data: + crc = crc ^ byte + for _ in range(8): + if crc & 0x0001 == 0: + crc = crc >> 1 + else: + crc = crc >> 1 + crc = crc ^ 0xA001 + return crc diff --git a/kvmd/plugins/hid/serial/gpio.py b/kvmd/plugins/hid/serial/gpio.py new file mode 100644 index 00000000..a3e4018b --- /dev/null +++ b/kvmd/plugins/hid/serial/gpio.py @@ -0,0 +1,71 @@ +# ========================================================================== # +# # +# KVMD - The main Pi-KVM daemon. # +# # +# Copyright (C) 2018 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 typing import Optional + +import gpiod + +from ....logging import get_logger + +from .... import env +from .... import aiotools +from .... import aiogp + + +# ===== +class Gpio: + def __init__(self, reset_pin: int, reset_delay: float) -> None: + self.__reset_pin = reset_pin + self.__reset_delay = reset_delay + + self.__chip: Optional[gpiod.Chip] = None + self.__reset_line: Optional[gpiod.Line] = None + self.__reset_wip = False + + def open(self) -> None: + if self.__reset_pin >= 0: + assert self.__chip is None + assert self.__reset_line is None + self.__chip = gpiod.Chip(env.GPIO_DEVICE_PATH) + self.__reset_line = self.__chip.get_line(self.__reset_pin) + self.__reset_line.request("kvmd::hid-serial::reset", gpiod.LINE_REQ_DIR_OUT, default_vals=[0]) + + def close(self) -> None: + if self.__chip: + try: + self.__chip.close() + except Exception: + pass + + @aiotools.atomic + async def reset(self) -> None: + if self.__reset_pin >= 0: + assert self.__reset_line + if not self.__reset_wip: + self.__reset_wip = True + try: + await aiogp.pulse(self.__reset_line, self.__reset_delay, 1) + finally: + self.__reset_wip = False + get_logger(0).info("Reset HID performed") + else: + get_logger(0).info("Another reset HID in progress") -- cgit v1.2.3 From 08b96b7ada3363f599c9f58d091d9a06d54c2031 Mon Sep 17 00:00:00 2001 From: Devaev Maxim Date: Wed, 28 Oct 2020 22:29:27 +0300 Subject: serial phy layer --- kvmd/plugins/hid/_mcu/__init__.py | 421 +++++++++++++++++++++++++++++++++++ kvmd/plugins/hid/_mcu/gpio.py | 71 ++++++ kvmd/plugins/hid/serial.py | 98 +++++++++ kvmd/plugins/hid/serial/__init__.py | 422 ------------------------------------ kvmd/plugins/hid/serial/gpio.py | 71 ------ 5 files changed, 590 insertions(+), 493 deletions(-) create mode 100644 kvmd/plugins/hid/_mcu/__init__.py create mode 100644 kvmd/plugins/hid/_mcu/gpio.py create mode 100644 kvmd/plugins/hid/serial.py delete mode 100644 kvmd/plugins/hid/serial/__init__.py delete mode 100644 kvmd/plugins/hid/serial/gpio.py (limited to 'kvmd/plugins/hid') diff --git a/kvmd/plugins/hid/_mcu/__init__.py b/kvmd/plugins/hid/_mcu/__init__.py new file mode 100644 index 00000000..bc978499 --- /dev/null +++ b/kvmd/plugins/hid/_mcu/__init__.py @@ -0,0 +1,421 @@ +# ========================================================================== # +# # +# KVMD - The main Pi-KVM daemon. # +# # +# Copyright (C) 2018 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 multiprocessing +import dataclasses +import contextlib +import queue +import struct +import time + +from typing import Tuple +from typing import List +from typing import Dict +from typing import Iterable +from typing import Generator +from typing import AsyncGenerator + +from ....logging import get_logger + +from ....keyboard.mappings import KEYMAP + +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.hw import valid_gpio_pin_optional + +from .. import BaseHid + +from .gpio import Gpio + + +# ===== +class _RequestError(Exception): + def __init__(self, msg: str, online: bool=False) -> None: + super().__init__(msg) + self.msg = msg + self.online = online + + +class _PermRequestError(_RequestError): + pass + + +class _TempRequestError(_RequestError): + pass + + +# ===== +class _BaseEvent: + def make_command(self) -> bytes: + raise NotImplementedError + + +class _ClearEvent(_BaseEvent): + def make_command(self) -> bytes: + return b"\x10\x00\x00\x00\x00" + + +@dataclasses.dataclass(frozen=True) +class _KeyEvent(_BaseEvent): + name: str + state: bool + + def __post_init__(self) -> None: + assert self.name in KEYMAP + + def make_command(self) -> bytes: + code = KEYMAP[self.name].serial.code + return struct.pack(">BBBxx", 0x11, code, int(self.state)) + + +@dataclasses.dataclass(frozen=True) +class _MouseButtonEvent(_BaseEvent): + name: str + state: bool + + def __post_init__(self) -> None: + assert self.name in ["left", "right", "middle", "up", "down"] + + def make_command(self) -> bytes: + (code, state_pressed, is_main) = { + "left": (0b10000000, 0b00001000, True), + "right": (0b01000000, 0b00000100, True), + "middle": (0b00100000, 0b00000010, True), + "up": (0b10000000, 0b00001000, False), # Back + "down": (0b01000000, 0b00000100, False), # Forward + }[self.name] + if self.state: + code |= state_pressed + if is_main: + main_code = code + extra_code = 0 + else: + main_code = 0 + extra_code = code + return struct.pack(">BBBxx", 0x13, main_code, extra_code) + + +@dataclasses.dataclass(frozen=True) +class _MouseMoveEvent(_BaseEvent): + to_x: int + to_y: int + + def __post_init__(self) -> None: + assert -32768 <= self.to_x <= 32767 + assert -32768 <= self.to_y <= 32767 + + def make_command(self) -> bytes: + return struct.pack(">Bhh", 0x12, self.to_x, self.to_y) + + +@dataclasses.dataclass(frozen=True) +class _MouseWheelEvent(_BaseEvent): + delta_x: int + delta_y: int + + def __post_init__(self) -> None: + assert -127 <= self.delta_x <= 127 + assert -127 <= self.delta_y <= 127 + + def make_command(self) -> bytes: + # Горизонтальная прокрутка пока не поддерживается + return struct.pack(">Bxbxx", 0x14, self.delta_y) + + +# ===== +class BasePhyConnection: + def send(self, request: bytes, receive: int) -> bytes: + raise NotImplementedError + + +class BasePhy: + def has_device(self) -> bool: + raise NotImplementedError + + @contextlib.contextmanager + def connected(self) -> Generator[BasePhyConnection, None, None]: + raise NotImplementedError + + +class BaseMcuHid(BaseHid, multiprocessing.Process): # pylint: disable=too-many-instance-attributes + def __init__( # pylint: disable=too-many-arguments,super-init-not-called + self, + phy: BasePhy, + + reset_pin: int, + reset_delay: float, + + read_retries: int, + common_retries: int, + retries_delay: float, + errors_threshold: int, + noop: bool, + ) -> None: + + multiprocessing.Process.__init__(self, daemon=True) + + self.__read_retries = read_retries + self.__common_retries = common_retries + self.__retries_delay = retries_delay + self.__errors_threshold = errors_threshold + self.__noop = noop + + self.__phy = phy + self.__gpio = Gpio(reset_pin, reset_delay) + + self.__events_queue: "multiprocessing.Queue[_BaseEvent]" = multiprocessing.Queue() + + self.__notifier = aiomulti.AioProcessNotifier() + self.__state_flags = aiomulti.AioSharedFlags({ + "online": True, + "caps": False, + "scroll": False, + "num": False, + }, self.__notifier) + + self.__stop_event = multiprocessing.Event() + + @classmethod + def get_plugin_options(cls) -> Dict: + return { + "reset_pin": Option(-1, type=valid_gpio_pin_optional), + "reset_delay": Option(0.1, type=valid_float_f01), + + "read_retries": Option(10, type=valid_int_f1), + "common_retries": Option(100, type=valid_int_f1), + "retries_delay": Option(0.1, type=valid_float_f01), + "errors_threshold": Option(5, type=valid_int_f0), + "noop": Option(False, type=valid_bool), + } + + def sysprep(self) -> None: + self.__gpio.open() + get_logger(0).info("Starting HID daemon ...") + self.start() + + async def get_state(self) -> Dict: + state = await self.__state_flags.get() + return { + "online": state["online"], + "keyboard": { + "online": state["online"], + "leds": { + "caps": state["caps"], + "scroll": state["scroll"], + "num": state["num"], + }, + }, + "mouse": {"online": state["online"]}, + } + + 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() + + @aiotools.atomic + async def reset(self) -> None: + await self.__gpio.reset() + + @aiotools.atomic + async def cleanup(self) -> None: + logger = get_logger(0) + try: + if self.is_alive(): + logger.info("Stopping HID daemon ...") + self.__stop_event.set() + if self.exitcode is not None: + self.join() + if self.__phy.has_device(): + get_logger().info("Clearing HID events ...") + try: + with self.__phy.connected() as conn: + self.__process_command(conn, b"\x10\x00\x00\x00\x00") + except Exception: + logger.exception("Can't clear HID events") + finally: + self.__gpio.close() + + # ===== + + def send_key_events(self, keys: Iterable[Tuple[str, bool]]) -> None: + for (key, state) in keys: + self.__queue_event(_KeyEvent(key, state)) + + def send_mouse_button_event(self, button: str, state: bool) -> None: + self.__queue_event(_MouseButtonEvent(button, state)) + + def send_mouse_move_event(self, to_x: int, to_y: int) -> None: + self.__queue_event(_MouseMoveEvent(to_x, to_y)) + + def send_mouse_wheel_event(self, delta_x: int, delta_y: int) -> None: + self.__queue_event(_MouseWheelEvent(delta_x, delta_y)) + + def clear_events(self) -> None: + # FIXME: Если очистка производится со стороны процесса хида, то возможна гонка между + # очисткой и добавлением события _ClearEvent. Неприятно, но не смертельно. + # Починить блокировкой после перехода на асинхронные очереди. + tools.clear_queue(self.__events_queue) + self.__queue_event(_ClearEvent()) + + def __queue_event(self, event: _BaseEvent) -> None: + if not self.__stop_event.is_set(): + self.__events_queue.put_nowait(event) + + def run(self) -> None: # pylint: disable=too-many-branches + logger = get_logger(0) + + logger.info("Started HID pid=%d", os.getpid()) + aioproc.ignore_sigint() + aioproc.rename_process("hid") + + while not self.__stop_event.is_set(): + try: + if self.__phy.has_device(): + with self.__phy.connected() as conn: + while not (self.__stop_event.is_set() and self.__events_queue.qsize() == 0): + try: + event = self.__events_queue.get(timeout=0.1) + except queue.Empty: + self.__process_command(conn, b"\x01\x00\x00\x00\x00") # Ping + else: + if not self.__process_command(conn, event.make_command()): + self.clear_events() + else: + logger.error("Missing HID device") + time.sleep(1) + except Exception: + self.clear_events() + logger.exception("Unexpected HID error") + time.sleep(1) + + def __process_command(self, conn: BasePhyConnection, command: bytes) -> bool: + return self.__process_request(conn, self.__make_request(command)) + + def __process_request(self, conn: BasePhyConnection, request: bytes) -> bool: # pylint: disable=too-many-branches + logger = get_logger() + error_messages: List[str] = [] + live_log_errors = False + + common_retries = self.__common_retries + read_retries = self.__read_retries + error_retval = False + + while common_retries and read_retries: + response = self.__send_request(conn, request) + try: + if len(response) < 4: + read_retries -= 1 + raise _TempRequestError(f"No response from HID: request={request!r}") + + assert len(response) == 4, response + if self.__make_crc16(response[-4:-2]) != struct.unpack(">H", response[-2:])[0]: + request = self.__make_request(b"\x02\x00\x00\x00\x00") # Repeat an answer + raise _TempRequestError("Invalid response CRC; requesting response again ...") + + code = response[1] + if code == 0x48: # Request timeout # pylint: disable=no-else-raise + raise _TempRequestError(f"Got request timeout from HID: request={request!r}") + elif code == 0x40: # CRC Error + raise _TempRequestError(f"Got CRC error of request from HID: request={request!r}") + elif code == 0x45: # Unknown command + raise _PermRequestError(f"HID did not recognize the request={request!r}", online=True) + elif code == 0x24: # Rebooted? + raise _PermRequestError("No previous command state inside HID, seems it was rebooted", online=True) + elif code == 0x20: # Done + self.__state_flags.update(online=True) + return True + elif code & 0x80: # Pong with leds + self.__state_flags.update( + online=True, + caps=bool(code & 0b00000001), + scroll=bool(code & 0x00000010), + num=bool(code & 0x00000100), + ) + return True + raise _TempRequestError(f"Invalid response from HID: request={request!r}; code=0x{code:02X}") + + except _RequestError as err: + common_retries -= 1 + self.__state_flags.update(online=err.online) + error_retval = err.online + + if live_log_errors: + logger.error(err.msg) + else: + error_messages.append(err.msg) + if len(error_messages) > self.__errors_threshold: + for msg in error_messages: + logger.error(msg) + error_messages = [] + live_log_errors = True + + if isinstance(err, _PermRequestError): + break + if common_retries and read_retries: + time.sleep(self.__retries_delay) + + for msg in error_messages: + logger.error(msg) + if not (common_retries and read_retries): + logger.error("Can't process HID request due many errors: %r", request) + return error_retval + + def __send_request(self, conn: BasePhyConnection, request: bytes) -> bytes: + if not self.__noop: + response = conn.send(request, 4) + else: + response = b"\x33\x20" # Magic + OK + response += struct.pack(">H", self.__make_crc16(response)) + return response + + def __make_request(self, command: bytes) -> bytes: + request = b"\x33" + command + request += struct.pack(">H", self.__make_crc16(request)) + assert len(request) == 8, (request, command) + return request + + def __make_crc16(self, data: bytes) -> int: + crc = 0xFFFF + for byte in data: + crc = crc ^ byte + for _ in range(8): + if crc & 0x0001 == 0: + crc = crc >> 1 + else: + crc = crc >> 1 + crc = crc ^ 0xA001 + return crc diff --git a/kvmd/plugins/hid/_mcu/gpio.py b/kvmd/plugins/hid/_mcu/gpio.py new file mode 100644 index 00000000..830819f0 --- /dev/null +++ b/kvmd/plugins/hid/_mcu/gpio.py @@ -0,0 +1,71 @@ +# ========================================================================== # +# # +# KVMD - The main Pi-KVM daemon. # +# # +# Copyright (C) 2018 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 typing import Optional + +import gpiod + +from ....logging import get_logger + +from .... import env +from .... import aiotools +from .... import aiogp + + +# ===== +class Gpio: + def __init__(self, reset_pin: int, reset_delay: float) -> None: + self.__reset_pin = reset_pin + self.__reset_delay = reset_delay + + self.__chip: Optional[gpiod.Chip] = None + self.__reset_line: Optional[gpiod.Line] = None + self.__reset_wip = False + + def open(self) -> None: + if self.__reset_pin >= 0: + assert self.__chip is None + assert self.__reset_line is None + self.__chip = gpiod.Chip(env.GPIO_DEVICE_PATH) + self.__reset_line = self.__chip.get_line(self.__reset_pin) + self.__reset_line.request("kvmd::hid-mcu::reset", gpiod.LINE_REQ_DIR_OUT, default_vals=[0]) + + def close(self) -> None: + if self.__chip: + try: + self.__chip.close() + except Exception: + pass + + @aiotools.atomic + async def reset(self) -> None: + if self.__reset_pin >= 0: + assert self.__reset_line + if not self.__reset_wip: + self.__reset_wip = True + try: + await aiogp.pulse(self.__reset_line, self.__reset_delay, 1) + finally: + self.__reset_wip = False + get_logger(0).info("Reset HID performed") + else: + get_logger(0).info("Another reset HID in progress") diff --git a/kvmd/plugins/hid/serial.py b/kvmd/plugins/hid/serial.py new file mode 100644 index 00000000..d5de1cc7 --- /dev/null +++ b/kvmd/plugins/hid/serial.py @@ -0,0 +1,98 @@ +# ========================================================================== # +# # +# KVMD - The main Pi-KVM daemon. # +# # +# Copyright (C) 2018 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 contextlib + +from typing import Dict +from typing import Generator +from typing import Any + +import serial + +from ...yamlconf import Option + +from ...validators.basic import valid_float_f01 +from ...validators.os import valid_abs_path +from ...validators.hw import valid_tty_speed + +from ._mcu import BasePhyConnection +from ._mcu import BasePhy +from ._mcu import BaseMcuHid + + +# ===== +class _SerialPhyConnection(BasePhyConnection): + def __init__(self, tty: serial.Serial) -> None: + self.__tty = tty + + def send(self, request: bytes, receive: int) -> bytes: + if self.__tty.in_waiting: + self.__tty.read_all() + assert self.__tty.write(request) == len(request) + return self.__tty.read(receive) + + +class _SerialPhy(BasePhy): + 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 + + def has_device(self) -> bool: + return os.path.exists(self.__device_path) + + @contextlib.contextmanager + def connected(self) -> Generator[_SerialPhyConnection, None, None]: # type: ignore + with serial.Serial(self.__device_path, self.__speed, timeout=self.__read_timeout) as tty: + yield _SerialPhyConnection(tty) + + +# ===== +class Plugin(BaseMcuHid): + def __init__( + self, + device_path: str, + speed: int, + read_timeout: float, + **kwargs: Any, + ) -> None: + + super().__init__( + phy=_SerialPhy(device_path, speed, read_timeout), + **kwargs, + ) + + @classmethod + def get_plugin_options(cls) -> Dict: + return { + "device": Option("", type=valid_abs_path, unpack_as="device_path"), + "speed": Option(115200, type=valid_tty_speed), + "read_timeout": Option(2.0, type=valid_float_f01), + **BaseMcuHid.get_plugin_options(), + } diff --git a/kvmd/plugins/hid/serial/__init__.py b/kvmd/plugins/hid/serial/__init__.py deleted file mode 100644 index 2b1c0af6..00000000 --- a/kvmd/plugins/hid/serial/__init__.py +++ /dev/null @@ -1,422 +0,0 @@ -# ========================================================================== # -# # -# KVMD - The main Pi-KVM daemon. # -# # -# Copyright (C) 2018 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 multiprocessing -import dataclasses -import queue -import struct -import errno -import time - -from typing import Tuple -from typing import List -from typing import Dict -from typing import Iterable -from typing import AsyncGenerator - -import serial - -from ....logging import get_logger - -from ....keyboard.mappings import KEYMAP - -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 ....validators.hw import valid_gpio_pin_optional - -from .. import BaseHid - -from .gpio import Gpio - - -# ===== -class _RequestError(Exception): - def __init__(self, msg: str, online: bool=False) -> None: - super().__init__(msg) - self.msg = msg - self.online = online - - -class _PermRequestError(_RequestError): - pass - - -class _TempRequestError(_RequestError): - pass - - -# ===== -class _BaseEvent: - def make_command(self) -> bytes: - raise NotImplementedError - - -class _ClearEvent(_BaseEvent): - def make_command(self) -> bytes: - return b"\x10\x00\x00\x00\x00" - - -@dataclasses.dataclass(frozen=True) -class _KeyEvent(_BaseEvent): - name: str - state: bool - - def __post_init__(self) -> None: - assert self.name in KEYMAP - - def make_command(self) -> bytes: - code = KEYMAP[self.name].serial.code - return struct.pack(">BBBxx", 0x11, code, int(self.state)) - - -@dataclasses.dataclass(frozen=True) -class _MouseButtonEvent(_BaseEvent): - name: str - state: bool - - def __post_init__(self) -> None: - assert self.name in ["left", "right", "middle", "up", "down"] - - def make_command(self) -> bytes: - (code, state_pressed, is_main) = { - "left": (0b10000000, 0b00001000, True), - "right": (0b01000000, 0b00000100, True), - "middle": (0b00100000, 0b00000010, True), - "up": (0b10000000, 0b00001000, False), # Back - "down": (0b01000000, 0b00000100, False), # Forward - }[self.name] - if self.state: - code |= state_pressed - if is_main: - main_code = code - extra_code = 0 - else: - main_code = 0 - extra_code = code - return struct.pack(">BBBxx", 0x13, main_code, extra_code) - - -@dataclasses.dataclass(frozen=True) -class _MouseMoveEvent(_BaseEvent): - to_x: int - to_y: int - - def __post_init__(self) -> None: - assert -32768 <= self.to_x <= 32767 - assert -32768 <= self.to_y <= 32767 - - def make_command(self) -> bytes: - return struct.pack(">Bhh", 0x12, self.to_x, self.to_y) - - -@dataclasses.dataclass(frozen=True) -class _MouseWheelEvent(_BaseEvent): - delta_x: int - delta_y: int - - def __post_init__(self) -> None: - assert -127 <= self.delta_x <= 127 - assert -127 <= self.delta_y <= 127 - - def make_command(self) -> bytes: - # Горизонтальная прокрутка пока не поддерживается - return struct.pack(">Bxbxx", 0x14, self.delta_y) - - -# ===== -class Plugin(BaseHid, multiprocessing.Process): # pylint: disable=too-many-instance-attributes - def __init__( # pylint: disable=too-many-arguments,super-init-not-called - self, - reset_pin: int, - reset_delay: float, - - 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.__gpio = Gpio(reset_pin, reset_delay) - - self.__events_queue: "multiprocessing.Queue[_BaseEvent]" = multiprocessing.Queue() - - self.__notifier = aiomulti.AioProcessNotifier() - self.__state_flags = aiomulti.AioSharedFlags({ - "online": True, - "caps": False, - "scroll": False, - "num": False, - }, self.__notifier) - - self.__stop_event = multiprocessing.Event() - - @classmethod - def get_plugin_options(cls) -> Dict: - return { - "reset_pin": Option(-1, type=valid_gpio_pin_optional), - "reset_delay": Option(0.1, type=valid_float_f01), - - "device": Option("", type=valid_abs_path, unpack_as="device_path"), - "speed": Option(115200, type=valid_tty_speed), - "read_timeout": Option(2.0, type=valid_float_f01), - "read_retries": Option(10, type=valid_int_f1), - "common_retries": Option(100, type=valid_int_f1), - "retries_delay": Option(0.1, type=valid_float_f01), - "errors_threshold": Option(5, type=valid_int_f0), - "noop": Option(False, type=valid_bool), - } - - def sysprep(self) -> None: - self.__gpio.open() - get_logger(0).info("Starting HID daemon ...") - self.start() - - async def get_state(self) -> Dict: - state = await self.__state_flags.get() - return { - "online": state["online"], - "keyboard": { - "online": state["online"], - "leds": { - "caps": state["caps"], - "scroll": state["scroll"], - "num": state["num"], - }, - }, - "mouse": {"online": state["online"]}, - } - - 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() - - @aiotools.atomic - async def reset(self) -> None: - await self.__gpio.reset() - - @aiotools.atomic - async def cleanup(self) -> None: - logger = get_logger(0) - try: - if self.is_alive(): - logger.info("Stopping HID daemon ...") - self.__stop_event.set() - if self.exitcode is not None: - self.join() - if os.path.exists(self.__device_path): - get_logger().info("Clearing HID events ...") - try: - with self.__get_serial() as tty: - self.__process_command(tty, b"\x10\x00\x00\x00\x00") - except Exception: - logger.exception("Can't clear HID events") - finally: - self.__gpio.close() - - # ===== - - def send_key_events(self, keys: Iterable[Tuple[str, bool]]) -> None: - for (key, state) in keys: - self.__queue_event(_KeyEvent(key, state)) - - def send_mouse_button_event(self, button: str, state: bool) -> None: - self.__queue_event(_MouseButtonEvent(button, state)) - - def send_mouse_move_event(self, to_x: int, to_y: int) -> None: - self.__queue_event(_MouseMoveEvent(to_x, to_y)) - - def send_mouse_wheel_event(self, delta_x: int, delta_y: int) -> None: - self.__queue_event(_MouseWheelEvent(delta_x, delta_y)) - - def clear_events(self) -> None: - # FIXME: Если очистка производится со стороны процесса хида, то возможна гонка между - # очисткой и добавлением события _ClearEvent. Неприятно, но не смертельно. - # Починить блокировкой после перехода на асинхронные очереди. - tools.clear_queue(self.__events_queue) - self.__queue_event(_ClearEvent()) - - def __queue_event(self, event: _BaseEvent) -> None: - if not self.__stop_event.is_set(): - self.__events_queue.put_nowait(event) - - def run(self) -> None: # pylint: disable=too-many-branches - logger = get_logger(0) - - logger.info("Started HID pid=%d", os.getpid()) - aioproc.ignore_sigint() - aioproc.rename_process("hid") - - while not self.__stop_event.is_set(): - try: - with self.__get_serial() as tty: - while not (self.__stop_event.is_set() and self.__events_queue.qsize() == 0): - try: - event = self.__events_queue.get(timeout=0.1) - except queue.Empty: - self.__process_command(tty, b"\x01\x00\x00\x00\x00") # Ping - else: - if not self.__process_command(tty, event.make_command()): - self.clear_events() - - except Exception as err: - self.clear_events() - if isinstance(err, serial.SerialException) and err.errno == errno.ENOENT: # pylint: disable=no-member - logger.error("Missing HID serial device: %s", self.__device_path) - else: - logger.exception("Unexpected HID error") - time.sleep(1) - - def __get_serial(self) -> serial.Serial: - return serial.Serial(self.__device_path, self.__speed, timeout=self.__read_timeout) - - def __process_command(self, tty: serial.Serial, command: bytes) -> bool: - return self.__process_request(tty, self.__make_request(command)) - - def __process_request(self, tty: serial.Serial, request: bytes) -> bool: # pylint: disable=too-many-branches - logger = get_logger() - error_messages: List[str] = [] - live_log_errors = False - - common_retries = self.__common_retries - read_retries = self.__read_retries - error_retval = False - - while common_retries and read_retries: - response = self.__send_request(tty, request) - try: - if len(response) < 4: - read_retries -= 1 - raise _TempRequestError(f"No response from HID: request={request!r}") - - assert len(response) == 4, response - if self.__make_crc16(response[-4:-2]) != struct.unpack(">H", response[-2:])[0]: - request = self.__make_request(b"\x02\x00\x00\x00\x00") # Repeat an answer - raise _TempRequestError("Invalid response CRC; requesting response again ...") - - code = response[1] - if code == 0x48: # Request timeout # pylint: disable=no-else-raise - raise _TempRequestError(f"Got request timeout from HID: request={request!r}") - elif code == 0x40: # CRC Error - raise _TempRequestError(f"Got CRC error of request from HID: request={request!r}") - elif code == 0x45: # Unknown command - raise _PermRequestError(f"HID did not recognize the request={request!r}", online=True) - elif code == 0x24: # Rebooted? - raise _PermRequestError("No previous command state inside HID, seems it was rebooted", online=True) - elif code == 0x20: # Done - self.__state_flags.update(online=True) - return True - elif code & 0x80: # Pong with leds - self.__state_flags.update( - online=True, - caps=bool(code & 0b00000001), - scroll=bool(code & 0x00000010), - num=bool(code & 0x00000100), - ) - return True - raise _TempRequestError(f"Invalid response from HID: request={request!r}; code=0x{code:02X}") - - except _RequestError as err: - common_retries -= 1 - self.__state_flags.update(online=err.online) - error_retval = err.online - - if live_log_errors: - logger.error(err.msg) - else: - error_messages.append(err.msg) - if len(error_messages) > self.__errors_threshold: - for msg in error_messages: - logger.error(msg) - error_messages = [] - live_log_errors = True - - if isinstance(err, _PermRequestError): - break - if common_retries and read_retries: - time.sleep(self.__retries_delay) - - for msg in error_messages: - logger.error(msg) - if not (common_retries and read_retries): - logger.error("Can't process HID request due many errors: %r", request) - return error_retval - - def __send_request(self, tty: serial.Serial, request: bytes) -> bytes: - if not self.__noop: - if tty.in_waiting: - tty.read(tty.in_waiting) - assert tty.write(request) == len(request) - response = tty.read(4) - else: - response = b"\x33\x20" # Magic + OK - response += struct.pack(">H", self.__make_crc16(response)) - return response - - def __make_request(self, command: bytes) -> bytes: - request = b"\x33" + command - request += struct.pack(">H", self.__make_crc16(request)) - assert len(request) == 8, (request, command) - return request - - def __make_crc16(self, data: bytes) -> int: - crc = 0xFFFF - for byte in data: - crc = crc ^ byte - for _ in range(8): - if crc & 0x0001 == 0: - crc = crc >> 1 - else: - crc = crc >> 1 - crc = crc ^ 0xA001 - return crc diff --git a/kvmd/plugins/hid/serial/gpio.py b/kvmd/plugins/hid/serial/gpio.py deleted file mode 100644 index a3e4018b..00000000 --- a/kvmd/plugins/hid/serial/gpio.py +++ /dev/null @@ -1,71 +0,0 @@ -# ========================================================================== # -# # -# KVMD - The main Pi-KVM daemon. # -# # -# Copyright (C) 2018 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 typing import Optional - -import gpiod - -from ....logging import get_logger - -from .... import env -from .... import aiotools -from .... import aiogp - - -# ===== -class Gpio: - def __init__(self, reset_pin: int, reset_delay: float) -> None: - self.__reset_pin = reset_pin - self.__reset_delay = reset_delay - - self.__chip: Optional[gpiod.Chip] = None - self.__reset_line: Optional[gpiod.Line] = None - self.__reset_wip = False - - def open(self) -> None: - if self.__reset_pin >= 0: - assert self.__chip is None - assert self.__reset_line is None - self.__chip = gpiod.Chip(env.GPIO_DEVICE_PATH) - self.__reset_line = self.__chip.get_line(self.__reset_pin) - self.__reset_line.request("kvmd::hid-serial::reset", gpiod.LINE_REQ_DIR_OUT, default_vals=[0]) - - def close(self) -> None: - if self.__chip: - try: - self.__chip.close() - except Exception: - pass - - @aiotools.atomic - async def reset(self) -> None: - if self.__reset_pin >= 0: - assert self.__reset_line - if not self.__reset_wip: - self.__reset_wip = True - try: - await aiogp.pulse(self.__reset_line, self.__reset_delay, 1) - finally: - self.__reset_wip = False - get_logger(0).info("Reset HID performed") - else: - get_logger(0).info("Another reset HID in progress") -- cgit v1.2.3 From e54449fd8ef0ea98aea881d25c9bb134a3bc2e11 Mon Sep 17 00:00:00 2001 From: Devaev Maxim Date: Thu, 29 Oct 2020 02:29:40 +0300 Subject: renamed serial keymap codes to mcu --- kvmd/plugins/hid/_mcu/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'kvmd/plugins/hid') diff --git a/kvmd/plugins/hid/_mcu/__init__.py b/kvmd/plugins/hid/_mcu/__init__.py index bc978499..71981e0d 100644 --- a/kvmd/plugins/hid/_mcu/__init__.py +++ b/kvmd/plugins/hid/_mcu/__init__.py @@ -93,7 +93,7 @@ class _KeyEvent(_BaseEvent): assert self.name in KEYMAP def make_command(self) -> bytes: - code = KEYMAP[self.name].serial.code + code = KEYMAP[self.name].mcu.code return struct.pack(">BBBxx", 0x11, code, int(self.state)) -- cgit v1.2.3 From aaef672ac2ce5dd2cee17ad571e6cab35f284ee4 Mon Sep 17 00:00:00 2001 From: Devaev Maxim Date: Thu, 29 Oct 2020 07:03:59 +0300 Subject: kvmd spi driver --- kvmd/plugins/hid/spi.py | 154 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 154 insertions(+) create mode 100644 kvmd/plugins/hid/spi.py (limited to 'kvmd/plugins/hid') diff --git a/kvmd/plugins/hid/spi.py b/kvmd/plugins/hid/spi.py new file mode 100644 index 00000000..77d2f34c --- /dev/null +++ b/kvmd/plugins/hid/spi.py @@ -0,0 +1,154 @@ +# ========================================================================== # +# # +# KVMD - The main Pi-KVM daemon. # +# # +# Copyright (C) 2018 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 contextlib +import time + +from typing import List +from typing import Dict +from typing import Generator +from typing import Any + +import spidev + +from ...yamlconf import Option + +from ...validators.basic import valid_int_f0 +from ...validators.basic import valid_int_f1 +from ...validators.basic import valid_float_f0 +from ...validators.basic import valid_float_f01 + +from ._mcu import BasePhyConnection +from ._mcu import BasePhy +from ._mcu import BaseMcuHid + + +# ===== +class SpiPhyError(Exception): + pass + + +class _SpiPhyConnection(BasePhyConnection): + def __init__( + self, + spi: spidev.SpiDev, + read_timeout: float, + read_delay: float, + ) -> None: + + self.__spi = spi + self.__read_timeout = read_timeout + self.__read_delay = read_delay + + def send(self, request: bytes, receive: int) -> bytes: + assert 0 < receive <= len(request) + + dummy = b"\x00" * len(request) + deadline_ts = time.time() + self.__read_timeout + while time.time() < deadline_ts: + garbage = bytes(self.__spi.xfer(dummy)) + if garbage == dummy: + break + else: + raise SpiPhyError("Timeout reached while reading a garbage") + + self.__spi.xfer(request) + + response: List[int] = [] + dummy = b"\x00" * receive + deadline_ts = time.time() + self.__read_timeout + found = False + while time.time() < deadline_ts: + if not found: + time.sleep(self.__read_delay) + for byte in self.__spi.xfer(dummy): + if not found: + if byte == 0: + continue + found = True + response.append(byte) + if len(response) >= receive: + break + if len(response) >= receive: + break + else: + raise SpiPhyError("Timeout reached while responce waiting") + + assert len(response) == receive + return bytes(response) + + +class _SpiPhy(BasePhy): + def __init__( + self, + bus: int, + chip: int, + max_freq: int, + read_timeout: float, + read_delay: float, + ) -> None: + + self.__bus = bus + self.__chip = chip + self.__max_freq = max_freq + self.__read_timeout = read_timeout + self.__read_delay = read_delay + + def has_device(self) -> bool: + return os.path.exists(f"/dev/spidev{self.__bus}.{self.__chip}") + + @contextlib.contextmanager + def connected(self) -> Generator[_SpiPhyConnection, None, None]: # type: ignore + with contextlib.closing(spidev.SpiDev(self.__bus, self.__chip)) as spi: + spi.mode = 0 + spi.max_speed_hz = self.__max_freq + yield _SpiPhyConnection(spi, self.__read_timeout, self.__read_delay) + + +# ===== +class Plugin(BaseMcuHid): + def __init__( + self, + bus: int, + chip: int, + max_freq: int, + read_timeout: float, + read_delay: float, + **kwargs: Any, + ) -> None: + + super().__init__( + phy=_SpiPhy(bus, chip, max_freq, read_timeout, read_delay), + **kwargs, + ) + + @classmethod + def get_plugin_options(cls) -> Dict: + return { + "bus": Option(0, type=valid_int_f0), + "chip": Option(0, type=valid_int_f0), + "max_freq": Option(1000000, type=valid_int_f1), + "read_timeout": Option(2.0, type=valid_float_f01), + "read_delay": Option(0.001, type=valid_float_f0), + **BaseMcuHid.get_plugin_options(), + } -- cgit v1.2.3 From a5dbc1adeaa0d790206580d5f94c7503808ea1f0 Mon Sep 17 00:00:00 2001 From: Devaev Maxim Date: Thu, 29 Oct 2020 13:51:58 +0300 Subject: refactoring --- kvmd/plugins/hid/_mcu/__init__.py | 4 ++-- kvmd/plugins/hid/serial.py | 7 ++++--- kvmd/plugins/hid/spi.py | 33 +++++++++++++++++---------------- 3 files changed, 23 insertions(+), 21 deletions(-) (limited to 'kvmd/plugins/hid') diff --git a/kvmd/plugins/hid/_mcu/__init__.py b/kvmd/plugins/hid/_mcu/__init__.py index 71981e0d..f762567b 100644 --- a/kvmd/plugins/hid/_mcu/__init__.py +++ b/kvmd/plugins/hid/_mcu/__init__.py @@ -153,7 +153,7 @@ class _MouseWheelEvent(_BaseEvent): # ===== class BasePhyConnection: - def send(self, request: bytes, receive: int) -> bytes: + def send(self, request: bytes) -> bytes: raise NotImplementedError @@ -396,7 +396,7 @@ class BaseMcuHid(BaseHid, multiprocessing.Process): # pylint: disable=too-many- def __send_request(self, conn: BasePhyConnection, request: bytes) -> bytes: if not self.__noop: - response = conn.send(request, 4) + response = conn.send(request) else: response = b"\x33\x20" # Magic + OK response += struct.pack(">H", self.__make_crc16(response)) diff --git a/kvmd/plugins/hid/serial.py b/kvmd/plugins/hid/serial.py index d5de1cc7..9bfafc69 100644 --- a/kvmd/plugins/hid/serial.py +++ b/kvmd/plugins/hid/serial.py @@ -45,11 +45,12 @@ class _SerialPhyConnection(BasePhyConnection): def __init__(self, tty: serial.Serial) -> None: self.__tty = tty - def send(self, request: bytes, receive: int) -> bytes: + def send(self, request: bytes) -> bytes: + assert len(request) == 8 if self.__tty.in_waiting: self.__tty.read_all() - assert self.__tty.write(request) == len(request) - return self.__tty.read(receive) + assert self.__tty.write(request) == 8 + return self.__tty.read(4) class _SerialPhy(BasePhy): diff --git a/kvmd/plugins/hid/spi.py b/kvmd/plugins/hid/spi.py index 77d2f34c..6020e04e 100644 --- a/kvmd/plugins/hid/spi.py +++ b/kvmd/plugins/hid/spi.py @@ -31,6 +31,8 @@ from typing import Any import spidev +from ...logging import get_logger + from ...yamlconf import Option from ...validators.basic import valid_int_f0 @@ -44,10 +46,6 @@ from ._mcu import BaseMcuHid # ===== -class SpiPhyError(Exception): - pass - - class _SpiPhyConnection(BasePhyConnection): def __init__( self, @@ -60,41 +58,44 @@ class _SpiPhyConnection(BasePhyConnection): self.__read_timeout = read_timeout self.__read_delay = read_delay - def send(self, request: bytes, receive: int) -> bytes: - assert 0 < receive <= len(request) + self.__empty8 = b"\x00" * 8 + self.__empty4 = b"\x00" * 4 + + def send(self, request: bytes) -> bytes: + assert len(request) == 8 - dummy = b"\x00" * len(request) deadline_ts = time.time() + self.__read_timeout while time.time() < deadline_ts: - garbage = bytes(self.__spi.xfer(dummy)) - if garbage == dummy: + garbage = bytes(self.__spi.xfer(self.__empty8)) + if garbage == self.__empty8: break else: - raise SpiPhyError("Timeout reached while reading a garbage") + get_logger(0).error("SPI timeout reached while reading the a garbage") + return b"" self.__spi.xfer(request) response: List[int] = [] - dummy = b"\x00" * receive deadline_ts = time.time() + self.__read_timeout found = False while time.time() < deadline_ts: if not found: time.sleep(self.__read_delay) - for byte in self.__spi.xfer(dummy): + for byte in self.__spi.xfer(self.__empty4): if not found: if byte == 0: continue found = True response.append(byte) - if len(response) >= receive: + if len(response) >= 4: break - if len(response) >= receive: + if len(response) >= 4: break else: - raise SpiPhyError("Timeout reached while responce waiting") + get_logger(0).error("SPI timeout reached while responce waiting") + return b"" - assert len(response) == receive + assert len(response) == 4 return bytes(response) -- cgit v1.2.3 From e07cdd60f393eb17184afe4ea1730f3c5745ea6f Mon Sep 17 00:00:00 2001 From: Devaev Maxim Date: Tue, 3 Nov 2020 10:15:54 +0300 Subject: spi firmware --- kvmd/plugins/hid/_mcu/__init__.py | 8 +++--- kvmd/plugins/hid/_mcu/gpio.py | 13 +++++++--- kvmd/plugins/hid/spi.py | 54 +++++++++++++++++++-------------------- 3 files changed, 41 insertions(+), 34 deletions(-) (limited to 'kvmd/plugins/hid') diff --git a/kvmd/plugins/hid/_mcu/__init__.py b/kvmd/plugins/hid/_mcu/__init__.py index f762567b..3f925cd8 100644 --- a/kvmd/plugins/hid/_mcu/__init__.py +++ b/kvmd/plugins/hid/_mcu/__init__.py @@ -172,6 +172,7 @@ class BaseMcuHid(BaseHid, multiprocessing.Process): # pylint: disable=too-many- phy: BasePhy, reset_pin: int, + reset_inverted: bool, reset_delay: float, read_retries: int, @@ -190,7 +191,7 @@ class BaseMcuHid(BaseHid, multiprocessing.Process): # pylint: disable=too-many- self.__noop = noop self.__phy = phy - self.__gpio = Gpio(reset_pin, reset_delay) + self.__gpio = Gpio(reset_pin, reset_inverted, reset_delay) self.__events_queue: "multiprocessing.Queue[_BaseEvent]" = multiprocessing.Queue() @@ -207,8 +208,9 @@ class BaseMcuHid(BaseHid, multiprocessing.Process): # pylint: disable=too-many- @classmethod def get_plugin_options(cls) -> Dict: return { - "reset_pin": Option(-1, type=valid_gpio_pin_optional), - "reset_delay": Option(0.1, type=valid_float_f01), + "reset_pin": Option(-1, type=valid_gpio_pin_optional), + "reset_inverted": Option(False, type=valid_bool), + "reset_delay": Option(0.1, type=valid_float_f01), "read_retries": Option(10, type=valid_int_f1), "common_retries": Option(100, type=valid_int_f1), diff --git a/kvmd/plugins/hid/_mcu/gpio.py b/kvmd/plugins/hid/_mcu/gpio.py index 830819f0..87f4b547 100644 --- a/kvmd/plugins/hid/_mcu/gpio.py +++ b/kvmd/plugins/hid/_mcu/gpio.py @@ -33,8 +33,15 @@ from .... import aiogp # ===== class Gpio: - def __init__(self, reset_pin: int, reset_delay: float) -> None: + def __init__( + self, + reset_pin: int, + reset_inverted: bool, + reset_delay: float, + ) -> None: + self.__reset_pin = reset_pin + self.__reset_inverted = reset_inverted self.__reset_delay = reset_delay self.__chip: Optional[gpiod.Chip] = None @@ -47,7 +54,7 @@ class Gpio: assert self.__reset_line is None self.__chip = gpiod.Chip(env.GPIO_DEVICE_PATH) self.__reset_line = self.__chip.get_line(self.__reset_pin) - self.__reset_line.request("kvmd::hid-mcu::reset", gpiod.LINE_REQ_DIR_OUT, default_vals=[0]) + self.__reset_line.request("kvmd::hid-mcu::reset", gpiod.LINE_REQ_DIR_OUT, default_vals=[int(self.__reset_inverted)]) def close(self) -> None: if self.__chip: @@ -63,7 +70,7 @@ class Gpio: if not self.__reset_wip: self.__reset_wip = True try: - await aiogp.pulse(self.__reset_line, self.__reset_delay, 1) + await aiogp.pulse(self.__reset_line, self.__reset_delay, 1, self.__reset_inverted) finally: self.__reset_wip = False get_logger(0).info("Reset HID performed") diff --git a/kvmd/plugins/hid/spi.py b/kvmd/plugins/hid/spi.py index 6020e04e..d71a4094 100644 --- a/kvmd/plugins/hid/spi.py +++ b/kvmd/plugins/hid/spi.py @@ -27,6 +27,7 @@ import time from typing import List from typing import Dict from typing import Generator +from typing import Callable from typing import Any import spidev @@ -49,31 +50,18 @@ from ._mcu import BaseMcuHid class _SpiPhyConnection(BasePhyConnection): def __init__( self, - spi: spidev.SpiDev, + xfer: Callable[[bytes], bytes], read_timeout: float, read_delay: float, ) -> None: - self.__spi = spi + self.__xfer = xfer self.__read_timeout = read_timeout self.__read_delay = read_delay - self.__empty8 = b"\x00" * 8 - self.__empty4 = b"\x00" * 4 - def send(self, request: bytes) -> bytes: assert len(request) == 8 - - deadline_ts = time.time() + self.__read_timeout - while time.time() < deadline_ts: - garbage = bytes(self.__spi.xfer(self.__empty8)) - if garbage == self.__empty8: - break - else: - get_logger(0).error("SPI timeout reached while reading the a garbage") - return b"" - - self.__spi.xfer(request) + self.__xfer(request) response: List[int] = [] deadline_ts = time.time() + self.__read_timeout @@ -81,21 +69,19 @@ class _SpiPhyConnection(BasePhyConnection): while time.time() < deadline_ts: if not found: time.sleep(self.__read_delay) - for byte in self.__spi.xfer(self.__empty4): + for byte in self.__xfer(b"\x00" * (4 - len(response))): if not found: if byte == 0: continue found = True response.append(byte) - if len(response) >= 4: + if len(response) == 4: break - if len(response) >= 4: + if len(response) == 4: break else: get_logger(0).error("SPI timeout reached while responce waiting") return b"" - - assert len(response) == 4 return bytes(response) @@ -105,6 +91,7 @@ class _SpiPhy(BasePhy): bus: int, chip: int, max_freq: int, + block_usec: int, read_timeout: float, read_delay: float, ) -> None: @@ -112,6 +99,7 @@ class _SpiPhy(BasePhy): self.__bus = bus self.__chip = chip self.__max_freq = max_freq + self.__block_usec = block_usec self.__read_timeout = read_timeout self.__read_delay = read_delay @@ -123,7 +111,15 @@ class _SpiPhy(BasePhy): with contextlib.closing(spidev.SpiDev(self.__bus, self.__chip)) as spi: spi.mode = 0 spi.max_speed_hz = self.__max_freq - yield _SpiPhyConnection(spi, self.__read_timeout, self.__read_delay) + + def xfer(data: bytes) -> bytes: + return spi.xfer(data, self.__max_freq, self.__block_usec) + + yield _SpiPhyConnection( + xfer=xfer, + read_timeout=self.__read_timeout, + read_delay=self.__read_delay, + ) # ===== @@ -133,23 +129,25 @@ class Plugin(BaseMcuHid): bus: int, chip: int, max_freq: int, + block_usec: int, read_timeout: float, read_delay: float, **kwargs: Any, ) -> None: super().__init__( - phy=_SpiPhy(bus, chip, max_freq, read_timeout, read_delay), + phy=_SpiPhy(bus, chip, max_freq, block_usec, read_timeout, read_delay), **kwargs, ) @classmethod def get_plugin_options(cls) -> Dict: return { - "bus": Option(0, type=valid_int_f0), - "chip": Option(0, type=valid_int_f0), - "max_freq": Option(1000000, type=valid_int_f1), - "read_timeout": Option(2.0, type=valid_float_f01), - "read_delay": Option(0.001, type=valid_float_f0), + "bus": Option(0, type=valid_int_f0), + "chip": Option(0, type=valid_int_f0), + "max_freq": Option(400000, type=valid_int_f1), + "block_usec": Option(1, type=valid_int_f0), + "read_timeout": Option(2.0, type=valid_float_f01), + "read_delay": Option(0.001, type=valid_float_f0), **BaseMcuHid.get_plugin_options(), } -- cgit v1.2.3 From a8a075c203e2a458ff51ce0d4152ab09a4710d65 Mon Sep 17 00:00:00 2001 From: Devaev Maxim Date: Tue, 10 Nov 2020 13:38:55 +0300 Subject: another try --- kvmd/plugins/hid/spi.py | 57 +++++++++++++++++++++++++------------------------ 1 file changed, 29 insertions(+), 28 deletions(-) (limited to 'kvmd/plugins/hid') diff --git a/kvmd/plugins/hid/spi.py b/kvmd/plugins/hid/spi.py index d71a4094..3dcd0dab 100644 --- a/kvmd/plugins/hid/spi.py +++ b/kvmd/plugins/hid/spi.py @@ -36,9 +36,9 @@ from ...logging import get_logger 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_f0 from ...validators.basic import valid_float_f01 from ._mcu import BasePhyConnection @@ -52,26 +52,33 @@ class _SpiPhyConnection(BasePhyConnection): self, xfer: Callable[[bytes], bytes], read_timeout: float, - read_delay: float, ) -> None: self.__xfer = xfer self.__read_timeout = read_timeout - self.__read_delay = read_delay def send(self, request: bytes) -> bytes: assert len(request) == 8 + assert request[0] == 0x33 + + deadline_ts = time.time() + self.__read_timeout + dummy = b"\x00" * 8 + while time.time() < deadline_ts: + if bytes(self.__xfer(dummy)) == dummy: + break + else: + get_logger(0).error("SPI timeout reached while garbage reading") + return b"" + self.__xfer(request) response: List[int] = [] deadline_ts = time.time() + self.__read_timeout found = False while time.time() < deadline_ts: - if not found: - time.sleep(self.__read_delay) for byte in self.__xfer(b"\x00" * (4 - len(response))): if not found: - if byte == 0: + if byte != 0x33: continue found = True response.append(byte) @@ -90,18 +97,18 @@ class _SpiPhy(BasePhy): self, bus: int, chip: int, + cs: bool, max_freq: int, block_usec: int, read_timeout: float, - read_delay: float, ) -> None: self.__bus = bus self.__chip = chip + self.__cs = cs self.__max_freq = max_freq self.__block_usec = block_usec self.__read_timeout = read_timeout - self.__read_delay = read_delay def has_device(self) -> bool: return os.path.exists(f"/dev/spidev{self.__bus}.{self.__chip}") @@ -110,6 +117,7 @@ class _SpiPhy(BasePhy): def connected(self) -> Generator[_SpiPhyConnection, None, None]: # type: ignore with contextlib.closing(spidev.SpiDev(self.__bus, self.__chip)) as spi: spi.mode = 0 + spi.no_cs = (not self.__cs) spi.max_speed_hz = self.__max_freq def xfer(data: bytes) -> bytes: @@ -118,36 +126,29 @@ class _SpiPhy(BasePhy): yield _SpiPhyConnection( xfer=xfer, read_timeout=self.__read_timeout, - read_delay=self.__read_delay, ) # ===== class Plugin(BaseMcuHid): - def __init__( - self, - bus: int, - chip: int, - max_freq: int, - block_usec: int, - read_timeout: float, - read_delay: float, - **kwargs: Any, - ) -> None: - - super().__init__( - phy=_SpiPhy(bus, chip, max_freq, block_usec, read_timeout, read_delay), - **kwargs, - ) + def __init__(self, **kwargs: Any) -> None: + phy_kwargs: Dict = {key: kwargs.pop(key) for key in self.__get_phy_options()} + super().__init__(phy=_SpiPhy(**phy_kwargs), **kwargs) @classmethod def get_plugin_options(cls) -> Dict: + return { + **cls.__get_phy_options(), + **BaseMcuHid.get_plugin_options(), + } + + @classmethod + def __get_phy_options(cls) -> Dict: return { "bus": Option(0, type=valid_int_f0), "chip": Option(0, type=valid_int_f0), - "max_freq": Option(400000, type=valid_int_f1), + "cs": Option(False, type=valid_bool), + "max_freq": Option(200000, type=valid_int_f1), "block_usec": Option(1, type=valid_int_f0), - "read_timeout": Option(2.0, type=valid_float_f01), - "read_delay": Option(0.001, type=valid_float_f0), - **BaseMcuHid.get_plugin_options(), + "read_timeout": Option(0.5, type=valid_float_f01), } -- cgit v1.2.3 From fef625aee52015dfdca0a96c31eced033663cb97 Mon Sep 17 00:00:00 2001 From: Devaev Maxim Date: Wed, 11 Nov 2020 12:56:34 +0300 Subject: refactoring --- kvmd/plugins/hid/serial.py | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) (limited to 'kvmd/plugins/hid') diff --git a/kvmd/plugins/hid/serial.py b/kvmd/plugins/hid/serial.py index 9bfafc69..dee992b6 100644 --- a/kvmd/plugins/hid/serial.py +++ b/kvmd/plugins/hid/serial.py @@ -47,6 +47,7 @@ class _SerialPhyConnection(BasePhyConnection): def send(self, request: bytes) -> bytes: assert len(request) == 8 + assert request[0] == 0x33 if self.__tty.in_waiting: self.__tty.read_all() assert self.__tty.write(request) == 8 @@ -76,24 +77,21 @@ class _SerialPhy(BasePhy): # ===== class Plugin(BaseMcuHid): - def __init__( - self, - device_path: str, - speed: int, - read_timeout: float, - **kwargs: Any, - ) -> None: - - super().__init__( - phy=_SerialPhy(device_path, speed, read_timeout), - **kwargs, - ) + def __init__(self, **kwargs: Any) -> None: + phy_kwargs: Dict = {key: kwargs.pop(key) for key in self.__get_phy_options()} + super().__init__(phy=_SerialPhy(**phy_kwargs), **kwargs) @classmethod def get_plugin_options(cls) -> Dict: + return { + **cls.__get_phy_options(), + **BaseMcuHid.get_plugin_options(), + } + + @classmethod + def __get_phy_options(cls) -> Dict: return { "device": Option("", type=valid_abs_path, unpack_as="device_path"), "speed": Option(115200, type=valid_tty_speed), "read_timeout": Option(2.0, type=valid_float_f01), - **BaseMcuHid.get_plugin_options(), } -- cgit v1.2.3 From 0140cba0dcc983e7649f407059636d26727f110f Mon Sep 17 00:00:00 2001 From: Devaev Maxim Date: Thu, 12 Nov 2020 18:33:35 +0300 Subject: software cs --- kvmd/plugins/hid/spi.py | 64 ++++++++++++++++++++++++++++++++++++------------- 1 file changed, 47 insertions(+), 17 deletions(-) (limited to 'kvmd/plugins/hid') diff --git a/kvmd/plugins/hid/spi.py b/kvmd/plugins/hid/spi.py index 3dcd0dab..cf48f803 100644 --- a/kvmd/plugins/hid/spi.py +++ b/kvmd/plugins/hid/spi.py @@ -28,9 +28,11 @@ from typing import List from typing import Dict from typing import Generator from typing import Callable +from typing import Optional from typing import Any import spidev +import gpiod from ...logging import get_logger @@ -40,6 +42,9 @@ 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.hw import valid_gpio_pin_optional + +from ... import env from ._mcu import BasePhyConnection from ._mcu import BasePhy @@ -92,12 +97,14 @@ class _SpiPhyConnection(BasePhyConnection): return bytes(response) -class _SpiPhy(BasePhy): +class _SpiPhy(BasePhy): # pylint: disable=too-many-instance-attributes def __init__( self, bus: int, chip: int, - cs: bool, + hw_cs: bool, + sw_cs_pin: int, + cs_high: bool, max_freq: int, block_usec: int, read_timeout: float, @@ -105,7 +112,9 @@ class _SpiPhy(BasePhy): self.__bus = bus self.__chip = chip - self.__cs = cs + self.__hw_cs = hw_cs + self.__sw_cs_pin = sw_cs_pin + self.__cs_high = cs_high self.__max_freq = max_freq self.__block_usec = block_usec self.__read_timeout = read_timeout @@ -115,18 +124,37 @@ class _SpiPhy(BasePhy): @contextlib.contextmanager def connected(self) -> Generator[_SpiPhyConnection, None, None]: # type: ignore - with contextlib.closing(spidev.SpiDev(self.__bus, self.__chip)) as spi: - spi.mode = 0 - spi.no_cs = (not self.__cs) - spi.max_speed_hz = self.__max_freq - - def xfer(data: bytes) -> bytes: - return spi.xfer(data, self.__max_freq, self.__block_usec) + with self.__sw_cs_connected() as sw_cs_line: + with contextlib.closing(spidev.SpiDev(self.__bus, self.__chip)) as spi: + spi.mode = 0 + spi.no_cs = (not self.__hw_cs) + if self.__hw_cs: + spi.cshigh = self.__cs_high + spi.max_speed_hz = self.__max_freq + + def xfer(data: bytes) -> bytes: + try: + if sw_cs_line is not None: + sw_cs_line.set_value(int(self.__cs_high)) + return spi.xfer(data, self.__max_freq, self.__block_usec) + finally: + if sw_cs_line is not None: + sw_cs_line.set_value(int(not self.__cs_high)) + + yield _SpiPhyConnection( + xfer=xfer, + read_timeout=self.__read_timeout, + ) - yield _SpiPhyConnection( - xfer=xfer, - read_timeout=self.__read_timeout, - ) + @contextlib.contextmanager + def __sw_cs_connected(self) -> Generator[Optional[gpiod.Line], None, None]: + if self.__sw_cs_pin > 0: + with contextlib.closing(gpiod.Chip(env.GPIO_DEVICE_PATH)) as chip: + line = chip.get_line(self.__sw_cs_pin) + line.request("kvmd::hid-mcu::sw_cs", gpiod.LINE_REQ_DIR_OUT, default_vals=[int(not self.__cs_high)]) + yield line + else: + yield None # ===== @@ -145,9 +173,11 @@ class Plugin(BaseMcuHid): @classmethod def __get_phy_options(cls) -> Dict: return { - "bus": Option(0, type=valid_int_f0), - "chip": Option(0, type=valid_int_f0), - "cs": Option(False, type=valid_bool), + "bus": Option(-1, type=valid_int_f0), + "chip": Option(-1, type=valid_int_f0), + "hw_cs": Option(False, type=valid_bool), + "sw_cs_pin": Option(-1, type=valid_gpio_pin_optional), + "cs_high": Option(False, type=valid_bool), "max_freq": Option(200000, type=valid_int_f1), "block_usec": Option(1, type=valid_int_f0), "read_timeout": Option(0.5, type=valid_float_f01), -- cgit v1.2.3 From c144f41c1d45f05e76ab95d591d9f47ff59e5a6e Mon Sep 17 00:00:00 2001 From: Devaev Maxim Date: Thu, 12 Nov 2020 20:35:47 +0300 Subject: removed cshigh option --- kvmd/plugins/hid/spi.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) (limited to 'kvmd/plugins/hid') diff --git a/kvmd/plugins/hid/spi.py b/kvmd/plugins/hid/spi.py index cf48f803..7eaa447f 100644 --- a/kvmd/plugins/hid/spi.py +++ b/kvmd/plugins/hid/spi.py @@ -104,7 +104,6 @@ class _SpiPhy(BasePhy): # pylint: disable=too-many-instance-attributes chip: int, hw_cs: bool, sw_cs_pin: int, - cs_high: bool, max_freq: int, block_usec: int, read_timeout: float, @@ -114,7 +113,6 @@ class _SpiPhy(BasePhy): # pylint: disable=too-many-instance-attributes self.__chip = chip self.__hw_cs = hw_cs self.__sw_cs_pin = sw_cs_pin - self.__cs_high = cs_high self.__max_freq = max_freq self.__block_usec = block_usec self.__read_timeout = read_timeout @@ -128,18 +126,16 @@ class _SpiPhy(BasePhy): # pylint: disable=too-many-instance-attributes with contextlib.closing(spidev.SpiDev(self.__bus, self.__chip)) as spi: spi.mode = 0 spi.no_cs = (not self.__hw_cs) - if self.__hw_cs: - spi.cshigh = self.__cs_high spi.max_speed_hz = self.__max_freq def xfer(data: bytes) -> bytes: try: if sw_cs_line is not None: - sw_cs_line.set_value(int(self.__cs_high)) + sw_cs_line.set_value(0) return spi.xfer(data, self.__max_freq, self.__block_usec) finally: if sw_cs_line is not None: - sw_cs_line.set_value(int(not self.__cs_high)) + sw_cs_line.set_value(1) yield _SpiPhyConnection( xfer=xfer, @@ -151,7 +147,7 @@ class _SpiPhy(BasePhy): # pylint: disable=too-many-instance-attributes if self.__sw_cs_pin > 0: with contextlib.closing(gpiod.Chip(env.GPIO_DEVICE_PATH)) as chip: line = chip.get_line(self.__sw_cs_pin) - line.request("kvmd::hid-mcu::sw_cs", gpiod.LINE_REQ_DIR_OUT, default_vals=[int(not self.__cs_high)]) + line.request("kvmd::hid-mcu::sw_cs", gpiod.LINE_REQ_DIR_OUT, default_vals=[1]) yield line else: yield None @@ -177,7 +173,6 @@ class Plugin(BaseMcuHid): "chip": Option(-1, type=valid_int_f0), "hw_cs": Option(False, type=valid_bool), "sw_cs_pin": Option(-1, type=valid_gpio_pin_optional), - "cs_high": Option(False, type=valid_bool), "max_freq": Option(200000, type=valid_int_f1), "block_usec": Option(1, type=valid_int_f0), "read_timeout": Option(0.5, type=valid_float_f01), -- cgit v1.2.3 From 0984f0cb36c5881669010c62e986cc8bc7fe019a Mon Sep 17 00:00:00 2001 From: Devaev Maxim Date: Thu, 12 Nov 2020 20:49:33 +0300 Subject: fixed xfer answer --- kvmd/plugins/hid/spi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'kvmd/plugins/hid') diff --git a/kvmd/plugins/hid/spi.py b/kvmd/plugins/hid/spi.py index 7eaa447f..8af03483 100644 --- a/kvmd/plugins/hid/spi.py +++ b/kvmd/plugins/hid/spi.py @@ -81,7 +81,7 @@ class _SpiPhyConnection(BasePhyConnection): deadline_ts = time.time() + self.__read_timeout found = False while time.time() < deadline_ts: - for byte in self.__xfer(b"\x00" * (4 - len(response))): + for byte in self.__xfer(b"\x00" * (5 - len(response))): if not found: if byte != 0x33: continue -- cgit v1.2.3