serial phy layer

This commit is contained in:
Devaev Maxim 2020-10-28 22:29:27 +03:00
parent dc0340583e
commit 08b96b7ada
5 changed files with 146 additions and 47 deletions

View File

@ -52,6 +52,8 @@ class BasePlugin:
def get_plugin_class(sub: str, name: str) -> Type[BasePlugin]: def get_plugin_class(sub: str, name: str) -> Type[BasePlugin]:
assert sub assert sub
assert name assert name
if name.startswith("_"):
raise UnknownPluginError(f"Unknown plugin '{sub}/{name}'")
try: try:
module = importlib.import_module(f"kvmd.plugins.{sub}.{name}") module = importlib.import_module(f"kvmd.plugins.{sub}.{name}")
except ModuleNotFoundError: except ModuleNotFoundError:

View File

@ -23,19 +23,18 @@
import os import os
import multiprocessing import multiprocessing
import dataclasses import dataclasses
import contextlib
import queue import queue
import struct import struct
import errno
import time import time
from typing import Tuple from typing import Tuple
from typing import List from typing import List
from typing import Dict from typing import Dict
from typing import Iterable from typing import Iterable
from typing import Generator
from typing import AsyncGenerator from typing import AsyncGenerator
import serial
from ....logging import get_logger from ....logging import get_logger
from ....keyboard.mappings import KEYMAP from ....keyboard.mappings import KEYMAP
@ -51,8 +50,6 @@ from ....validators.basic import valid_bool
from ....validators.basic import valid_int_f0 from ....validators.basic import valid_int_f0
from ....validators.basic import valid_int_f1 from ....validators.basic import valid_int_f1
from ....validators.basic import valid_float_f01 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 ....validators.hw import valid_gpio_pin_optional
from .. import BaseHid from .. import BaseHid
@ -155,15 +152,28 @@ class _MouseWheelEvent(_BaseEvent):
# ===== # =====
class Plugin(BaseHid, multiprocessing.Process): # pylint: disable=too-many-instance-attributes 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 def __init__( # pylint: disable=too-many-arguments,super-init-not-called
self, self,
phy: BasePhy,
reset_pin: int, reset_pin: int,
reset_delay: float, reset_delay: float,
device_path: str,
speed: int,
read_timeout: float,
read_retries: int, read_retries: int,
common_retries: int, common_retries: int,
retries_delay: float, retries_delay: float,
@ -173,15 +183,13 @@ class Plugin(BaseHid, multiprocessing.Process): # pylint: disable=too-many-inst
multiprocessing.Process.__init__(self, daemon=True) 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.__read_retries = read_retries
self.__common_retries = common_retries self.__common_retries = common_retries
self.__retries_delay = retries_delay self.__retries_delay = retries_delay
self.__errors_threshold = errors_threshold self.__errors_threshold = errors_threshold
self.__noop = noop self.__noop = noop
self.__phy = phy
self.__gpio = Gpio(reset_pin, reset_delay) self.__gpio = Gpio(reset_pin, reset_delay)
self.__events_queue: "multiprocessing.Queue[_BaseEvent]" = multiprocessing.Queue() self.__events_queue: "multiprocessing.Queue[_BaseEvent]" = multiprocessing.Queue()
@ -202,9 +210,6 @@ class Plugin(BaseHid, multiprocessing.Process): # pylint: disable=too-many-inst
"reset_pin": Option(-1, type=valid_gpio_pin_optional), "reset_pin": Option(-1, type=valid_gpio_pin_optional),
"reset_delay": Option(0.1, type=valid_float_f01), "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), "read_retries": Option(10, type=valid_int_f1),
"common_retries": Option(100, type=valid_int_f1), "common_retries": Option(100, type=valid_int_f1),
"retries_delay": Option(0.1, type=valid_float_f01), "retries_delay": Option(0.1, type=valid_float_f01),
@ -254,11 +259,11 @@ class Plugin(BaseHid, multiprocessing.Process): # pylint: disable=too-many-inst
self.__stop_event.set() self.__stop_event.set()
if self.exitcode is not None: if self.exitcode is not None:
self.join() self.join()
if os.path.exists(self.__device_path): if self.__phy.has_device():
get_logger().info("Clearing HID events ...") get_logger().info("Clearing HID events ...")
try: try:
with self.__get_serial() as tty: with self.__phy.connected() as conn:
self.__process_command(tty, b"\x10\x00\x00\x00\x00") self.__process_command(conn, b"\x10\x00\x00\x00\x00")
except Exception: except Exception:
logger.exception("Can't clear HID events") logger.exception("Can't clear HID events")
finally: finally:
@ -299,31 +304,28 @@ class Plugin(BaseHid, multiprocessing.Process): # pylint: disable=too-many-inst
while not self.__stop_event.is_set(): while not self.__stop_event.is_set():
try: try:
with self.__get_serial() as tty: if self.__phy.has_device():
with self.__phy.connected() as conn:
while not (self.__stop_event.is_set() and self.__events_queue.qsize() == 0): while not (self.__stop_event.is_set() and self.__events_queue.qsize() == 0):
try: try:
event = self.__events_queue.get(timeout=0.1) event = self.__events_queue.get(timeout=0.1)
except queue.Empty: except queue.Empty:
self.__process_command(tty, b"\x01\x00\x00\x00\x00") # Ping self.__process_command(conn, b"\x01\x00\x00\x00\x00") # Ping
else: else:
if not self.__process_command(tty, event.make_command()): if not self.__process_command(conn, event.make_command()):
self.clear_events() 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: else:
logger.error("Missing HID device")
time.sleep(1)
except Exception:
self.clear_events()
logger.exception("Unexpected HID error") logger.exception("Unexpected HID error")
time.sleep(1) time.sleep(1)
def __get_serial(self) -> serial.Serial: def __process_command(self, conn: BasePhyConnection, command: bytes) -> bool:
return serial.Serial(self.__device_path, self.__speed, timeout=self.__read_timeout) return self.__process_request(conn, self.__make_request(command))
def __process_command(self, tty: serial.Serial, command: bytes) -> bool: def __process_request(self, conn: BasePhyConnection, request: bytes) -> bool: # pylint: disable=too-many-branches
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() logger = get_logger()
error_messages: List[str] = [] error_messages: List[str] = []
live_log_errors = False live_log_errors = False
@ -333,7 +335,7 @@ class Plugin(BaseHid, multiprocessing.Process): # pylint: disable=too-many-inst
error_retval = False error_retval = False
while common_retries and read_retries: while common_retries and read_retries:
response = self.__send_request(tty, request) response = self.__send_request(conn, request)
try: try:
if len(response) < 4: if len(response) < 4:
read_retries -= 1 read_retries -= 1
@ -392,12 +394,9 @@ class Plugin(BaseHid, multiprocessing.Process): # pylint: disable=too-many-inst
logger.error("Can't process HID request due many errors: %r", request) logger.error("Can't process HID request due many errors: %r", request)
return error_retval return error_retval
def __send_request(self, tty: serial.Serial, request: bytes) -> bytes: def __send_request(self, conn: BasePhyConnection, request: bytes) -> bytes:
if not self.__noop: if not self.__noop:
if tty.in_waiting: response = conn.send(request, 4)
tty.read(tty.in_waiting)
assert tty.write(request) == len(request)
response = tty.read(4)
else: else:
response = b"\x33\x20" # Magic + OK response = b"\x33\x20" # Magic + OK
response += struct.pack(">H", self.__make_crc16(response)) response += struct.pack(">H", self.__make_crc16(response))

View File

@ -47,7 +47,7 @@ class Gpio:
assert self.__reset_line is None assert self.__reset_line is None
self.__chip = gpiod.Chip(env.GPIO_DEVICE_PATH) self.__chip = gpiod.Chip(env.GPIO_DEVICE_PATH)
self.__reset_line = self.__chip.get_line(self.__reset_pin) 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]) self.__reset_line.request("kvmd::hid-mcu::reset", gpiod.LINE_REQ_DIR_OUT, default_vals=[0])
def close(self) -> None: def close(self) -> None:
if self.__chip: if self.__chip:

View File

@ -0,0 +1,98 @@
# ========================================================================== #
# #
# KVMD - The main Pi-KVM daemon. #
# #
# Copyright (C) 2018 Maxim Devaev <mdevaev@gmail.com> #
# #
# 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 <https://www.gnu.org/licenses/>. #
# #
# ========================================================================== #
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(),
}

View File

@ -83,7 +83,7 @@ def main() -> None:
"kvmd.plugins", "kvmd.plugins",
"kvmd.plugins.auth", "kvmd.plugins.auth",
"kvmd.plugins.hid", "kvmd.plugins.hid",
"kvmd.plugins.hid.serial", "kvmd.plugins.hid._mcu",
"kvmd.plugins.hid.otg", "kvmd.plugins.hid.otg",
"kvmd.plugins.hid.bt", "kvmd.plugins.hid.bt",
"kvmd.plugins.atx", "kvmd.plugins.atx",