summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSpookHCK <[email protected]>2021-11-21 10:12:49 -0700
committerGitHub <[email protected]>2021-11-21 20:12:49 +0300
commitfee323ecc8c03dc7965ae602e40d69ee131ad459 (patch)
treeaf6ef3b27a6e132b2905ec47f27ba3b40e4d16f9
parent444603cbb21e4c97e5d685e631c6390b88f3e664 (diff)
PWAY 16-port SH1601A Driver (#67)
* pway 16-port SH1601A driver Minimum viable driver. * Update to pWay 16-port driver * Rename kvmd/pway.py to plugins/ugpio/pway.py Fixed file mods * Delete pway.py * Update pway.py Fixed file changes to only one... * fixed spaces on line 181
-rw-r--r--kvmd/plugins/ugpio/pway.py200
1 files changed, 200 insertions, 0 deletions
diff --git a/kvmd/plugins/ugpio/pway.py b/kvmd/plugins/ugpio/pway.py
new file mode 100644
index 00000000..1c5a71da
--- /dev/null
+++ b/kvmd/plugins/ugpio/pway.py
@@ -0,0 +1,200 @@
+# ========================================================================== #
+# #
+# KVMD - The main Pi-KVM daemon. #
+# #
+# #
+# Copyright (C) 2018-2021 Maxim Devaev <[email protected]> #
+# #
+# Modified by SppokHCK September 2021 <Find me on Discord spook#8911> #
+# #
+# 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 re
+import multiprocessing
+import functools
+import errno
+import time
+
+from typing import Tuple
+from typing import Dict
+from typing import Optional
+
+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
+ def __init__(
+ self,
+ instance_name: str,
+ notifier: aiotools.AioNotifier,
+
+ device_path: str,
+ speed: int,
+ read_timeout: float,
+ protocol: int,
+ ) -> None:
+
+ super().__init__(instance_name, notifier)
+
+ self.__device_path = device_path
+ self.__speed = speed
+ self.__read_timeout = read_timeout
+ self.__protocol = protocol
+
+ self.__ctl_queue: "multiprocessing.Queue[int]" = multiprocessing.Queue()
+ self.__channel_queue: "multiprocessing.Queue[Optional[int]]" = multiprocessing.Queue()
+ self.__channel: Optional[int] = -1
+
+ self.__proc: Optional[multiprocessing.Process] = 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(19200, type=valid_tty_speed),
+ # speed for pWAY 16-Port 19200 8n1.
+ "read_timeout": Option(2.0, type=valid_float_f01),
+ "protocol": Option(1, type=functools.partial(valid_number, min=1, max=2)),
+ }
+
+ def register_input(self, pin: int, debounce: float) -> None:
+ _ = pin
+ _ = debounce
+
+ def register_output(self, pin: int, initial: Optional[bool]) -> None:
+ _ = pin
+ _ = initial
+
+ 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
+ await 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.exitcode is not None:
+ self.__proc.join()
+
+ async def read(self, pin: int) -> bool:
+ if not self.__is_online():
+ raise GpioDriverOfflineError(self)
+ return (self.__channel == pin)
+
+ async def write(self, pin: int, state: bool) -> None:
+ if not self.__is_online():
+ raise GpioDriverOfflineError(self)
+ if state and (0 <= pin <= 16):
+ self.__ctl_queue.put_nowait(pin)
+
+ # =====
+
+ def __is_online(self) -> bool:
+ return (
+ self.__proc is not None
+ and self.__proc.is_alive()
+ and self.__channel is not None
+ )
+
+ def __serial_worker(self) -> None:
+ logger = aioproc.settle(str(self), f"gpio-pway-{self._instance_name}")
+ while not self.__stop_event.is_set():
+ try:
+ with self.__get_serial() as tty:
+ data = b""
+ self.__channel_queue.put_nowait(-1)
+
+ # Switch and then recieve the state.
+ # FIXME: Get actual state without modifying the current.
+ # I'm lazy and like the idea of the KVM resetting to port 1 on reboot of the PiKVM.
+ self.__send_channel(tty, 0)
+
+ 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[Optional[int], bytes]:
+ channel: Optional[int] = None
+ if tty.in_waiting:
+ data += tty.read_all()
+ # When you switch ports you see something like "VGA_SWITCH_CONTROL=[0-15]" for ports 1-16
+ found = re.findall(b"VGA_SWITCH_CONTROL=[0-9]*", data)
+ if found:
+ channel = int(found[0].decode().split('=')[1]) + 1
+ data = data[-8:]
+ return (channel, data)
+
+ def __send_channel(self, tty: serial.Serial, channel: int) -> None:
+ # Set a channel by sending PS [1-16]
+ cmd = (b"PS")
+ if channel == 0:
+ # when it initializes this will push us to port 1 and set us back to defaults.
+ tty.write(b"%s\r" % (cmd))
+ tty.flush()
+ else:
+ # Basically send `PS [1-16]` that switches the port
+ tty.write(b"%s %d\r" % (cmd, channel))
+ tty.flush()
+
+ def __str__(self) -> str:
+ return f"pway({self._instance_name})"
+
+ __repr__ = __str__