From 4775a31e27e9c12b8a7cc959ae815068d56cd295 Mon Sep 17 00:00:00 2001 From: Up Date: Wed, 16 Aug 2023 01:20:44 +0800 Subject: [PATCH 1/2] Add GPIO driver for gzvich gz-hk401x 4port HDMI USB KVM --- kvmd/plugins/ugpio/gz_hk401x.py | 191 ++++++++++++++++++++++++++++++++ 1 file changed, 191 insertions(+) create mode 100644 kvmd/plugins/ugpio/gz_hk401x.py diff --git a/kvmd/plugins/ugpio/gz_hk401x.py b/kvmd/plugins/ugpio/gz_hk401x.py new file mode 100644 index 000000000..74cd43752 --- /dev/null +++ b/kvmd/plugins/ugpio/gz_hk401x.py @@ -0,0 +1,191 @@ +# ========================================================================== # +# # +# KVMD - The main PiKVM daemon. # +# # +# Copyright (C) 2018-2023 Maxim Devaev # +# 2021-2021 Sebastian Goscik # +# # +# 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 re +import multiprocessing +import functools +import errno +import time + +from typing import Callable +from typing import Any + +import serial + +from ...logging import get_logger + +from ... import aiotools +from ... import aiomulti +from ... import aioproc + +from ...yamlconf import Option + +from ...validators.basic import valid_number +from ...validators.basic import valid_float_f01 +from ...validators.os import valid_abs_path +from ...validators.hw import valid_tty_speed + +from . import GpioDriverOfflineError +from . import BaseUserGpioDriver + + +# ===== +class Plugin(BaseUserGpioDriver): # pylint: disable=too-many-instance-attributes + + MIN_CHANNEL = 0 + MAX_CHANNEL = 3 + + def __init__( + self, + instance_name: str, + notifier: aiotools.AioNotifier, + + device_path: str, + speed: int, + read_timeout: float, + ) -> None: + + super().__init__(instance_name, notifier) + + self.__device_path = device_path + self.__speed = speed + self.__read_timeout = read_timeout + + self.__ctl_queue: "multiprocessing.Queue[int]" = multiprocessing.Queue() + self.__channel_queue: "multiprocessing.Queue[int | None]" = multiprocessing.Queue() + self.__channel: (int | None) = -1 + + self.__proc: (multiprocessing.Process | None) = None + self.__stop_event = multiprocessing.Event() + + @classmethod + def get_plugin_options(cls) -> dict: + return { + "device": Option("", type=valid_abs_path, unpack_as="device_path"), + "speed": Option(9600, type=valid_tty_speed), + "read_timeout": Option(2.0, type=valid_float_f01), + } + + @classmethod + def get_pin_validator(cls) -> Callable[[Any], Any]: + return functools.partial(valid_number, min=Plugin.MIN_CHANNEL, max=Plugin.MAX_CHANNEL, name="GZ-HK401X channel") + + def prepare(self) -> None: + assert self.__proc is None + self.__proc = multiprocessing.Process(target=self.__serial_worker, daemon=True) + self.__proc.start() + + async def run(self) -> None: + while True: + (got, channel) = await aiomulti.queue_get_last(self.__channel_queue, 1) + if got and self.__channel != channel: + self.__channel = channel + self._notifier.notify() + + async def cleanup(self) -> None: + if self.__proc is not None: + if self.__proc.is_alive(): + get_logger(0).info("Stopping %s daemon ...", self) + self.__stop_event.set() + if self.__proc.is_alive() or self.__proc.exitcode is not None: + self.__proc.join() + + async def read(self, pin: str) -> bool: + if not self.__is_online(): + raise GpioDriverOfflineError(self) + return (self.__channel == int(pin)) + + async def write(self, pin: str, state: bool) -> None: + if not self.__is_online(): + raise GpioDriverOfflineError(self) + if state: + self.__ctl_queue.put_nowait(int(pin)) + + # ===== + + def __is_online(self) -> bool: + return ( + self.__proc is not None + and self.__proc.is_alive() + ) + + def __serial_worker(self) -> None: + logger = aioproc.settle(str(self), f"gpio-gz-hk401x-{self._instance_name}") + while not self.__stop_event.is_set(): + try: + with self.__get_serial() as tty: + logger.info("Opened Serial device %s with baud rate: %s for driver %s", self.__device_path, self.__speed, self._instance_name) + data = b"" + self.__channel_queue.put_nowait(-1) + + while not self.__stop_event.is_set(): + (channel, data) = self.__recv_channel(tty, data) + if channel is not None: + self.__channel_queue.put_nowait(channel) + + (got, channel) = aiomulti.queue_get_last_sync(self.__ctl_queue, 0.1) # type: ignore + if got: + assert channel is not None + self.__send_channel(tty, channel) + + except Exception as err: + self.__channel_queue.put_nowait(None) + if isinstance(err, serial.SerialException) and err.errno == errno.ENOENT: # pylint: disable=no-member + logger.error("Missing %s serial device: %s", self, self.__device_path) + else: + logger.exception("Unexpected %s error", self) + time.sleep(1) + + def __get_serial(self) -> serial.Serial: + return serial.Serial(self.__device_path, self.__speed, timeout=self.__read_timeout) + + def __recv_channel(self, tty: serial.Serial, data: bytes) -> tuple[(int | None), bytes]: + channel: (int | None) = None + if tty.in_waiting: + data += tty.read_all() + get_logger(0).debug('Driver %s received serial data" %s', self._instance_name, data) + if len(data) != 1: + get_logger(0).warning('Driver %s received invalid data: "%s" .', self._instance_name, data) + else: + response = int.from_bytes(data, 'little', signed=False) + if response < Plugin.MIN_CHANNEL + 1 or response > Plugin.MAX_CHANNEL + 1: + get_logger(0).warning('Driver %s received invalid serial data: "%s" .', self._instance_name, data) + else: + channel = response - 1 + data = b"" + return (channel, data) + + def __send_channel(self, tty: serial.Serial, channel: int) -> None: + assert 0 <= channel <= 3 + channel += 1 + channel_byte = 0x30 + channel + cmd = bytearray(b'\xfe\x00\x33') + cmd.append(channel_byte) + cmd.append(0xaa) + tty.write(bytes(cmd)) + tty.flush() + + def __str__(self) -> str: + return f"GZ-HK401X({self._instance_name})" + + __repr__ = __str__ From df17960f057f2a4304d32d4217a55bba93f2400e Mon Sep 17 00:00:00 2001 From: Up Date: Wed, 16 Aug 2023 01:22:19 +0800 Subject: [PATCH 2/2] Add copyrights --- kvmd/plugins/ugpio/gz_hk401x.py | 1 + 1 file changed, 1 insertion(+) diff --git a/kvmd/plugins/ugpio/gz_hk401x.py b/kvmd/plugins/ugpio/gz_hk401x.py index 74cd43752..59220cbcf 100644 --- a/kvmd/plugins/ugpio/gz_hk401x.py +++ b/kvmd/plugins/ugpio/gz_hk401x.py @@ -4,6 +4,7 @@ # # # Copyright (C) 2018-2023 Maxim Devaev # # 2021-2021 Sebastian Goscik # +# 2023-2023 Up # # # # 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 #