summaryrefslogtreecommitdiff
path: root/kvmd
diff options
context:
space:
mode:
authorDevaev Maxim <[email protected]>2018-07-11 00:06:56 +0000
committerDevaev Maxim <[email protected]>2018-07-11 00:06:56 +0000
commit008b9ca2f2d7daf96e975635ddd4c38c35bb4bdb (patch)
tree5f8c9b8921efb1258922c468c5d1bfc3c2bfc9c1 /kvmd
parentdb56bf90db213f994dc3c433638dc497ead94096 (diff)
arduino-based hid
Diffstat (limited to 'kvmd')
-rw-r--r--kvmd/Makefile4
-rw-r--r--kvmd/configs/kvmd/v1.yaml9
-rw-r--r--kvmd/kvmd/__init__.py11
-rw-r--r--kvmd/kvmd/hid.py151
-rw-r--r--kvmd/kvmd/keyboard.py171
-rw-r--r--kvmd/kvmd/server.py22
-rw-r--r--kvmd/requirements.txt1
-rw-r--r--kvmd/testenv/Dockerfile1
-rw-r--r--kvmd/testenv/kvmd.yaml9
-rw-r--r--kvmd/testenv/requirements.txt1
10 files changed, 179 insertions, 201 deletions
diff --git a/kvmd/Makefile b/kvmd/Makefile
index 0f23dfb3..97af5774 100644
--- a/kvmd/Makefile
+++ b/kvmd/Makefile
@@ -1,8 +1,10 @@
TESTENV_IMAGE ?= kvmd-testenv
+TESTENV_HID ?= /dev/ttyS10
TESTENV_VIDEO ?= /dev/video0
TESTENV_LOOP ?= /dev/loop7
TESTENV_CMD ?= /bin/bash -c " \
- nginx -c /testenv/nginx.conf \
+ (socat PTY,link=$(TESTENV_HID) PTY,link=/dev/ttyS11 &) \
+ && nginx -c /testenv/nginx.conf \
&& ln -s $(TESTENV_VIDEO) /dev/kvmd-streamer \
&& (losetup -d /dev/kvmd-msd || true) \
&& losetup /dev/kvmd-msd /root/loop.img \
diff --git a/kvmd/configs/kvmd/v1.yaml b/kvmd/configs/kvmd/v1.yaml
index 3946e730..71d361e8 100644
--- a/kvmd/configs/kvmd/v1.yaml
+++ b/kvmd/configs/kvmd/v1.yaml
@@ -4,12 +4,9 @@ kvmd:
port: 8081
heartbeat: 3.0
- keyboard:
- pinout:
- clock: 17
- data: 4
-
- pulse: 0.0002
+ hid:
+ device: /dev/ttyAMA0
+ speed: 115200
atx:
pinout:
diff --git a/kvmd/kvmd/__init__.py b/kvmd/kvmd/__init__.py
index 6f2321c3..8288e8e3 100644
--- a/kvmd/kvmd/__init__.py
+++ b/kvmd/kvmd/__init__.py
@@ -3,7 +3,7 @@ import asyncio
from .application import init
from .logging import get_logger
-from .keyboard import Keyboard
+from .hid import Hid
from .atx import Atx
from .msd import MassStorageDevice
from .streamer import Streamer
@@ -18,10 +18,9 @@ def main() -> None:
with gpio.bcm():
loop = asyncio.get_event_loop()
- keyboard = Keyboard(
- clock=int(config["keyboard"]["pinout"]["clock"]),
- data=int(config["keyboard"]["pinout"]["data"]),
- pulse=float(config["keyboard"]["pulse"]),
+ hid = Hid(
+ device_path=str(config["hid"]["device"]),
+ speed=int(config["hid"]["speed"]),
)
atx = Atx(
@@ -52,7 +51,7 @@ def main() -> None:
)
Server(
- keyboard=keyboard,
+ hid=hid,
atx=atx,
msd=msd,
streamer=streamer,
diff --git a/kvmd/kvmd/hid.py b/kvmd/kvmd/hid.py
new file mode 100644
index 00000000..9ad5ba62
--- /dev/null
+++ b/kvmd/kvmd/hid.py
@@ -0,0 +1,151 @@
+import re
+import asyncio
+import multiprocessing
+import multiprocessing.queues
+import queue
+
+from typing import Set
+from typing import NamedTuple
+from typing import Union
+
+import serial
+
+from .logging import get_logger
+
+from . import gpio
+
+
+# =====
+class _KeyEvent(NamedTuple):
+ key: str
+ state: bool
+
+
+def _key_to_bytes(key: str) -> bytes:
+ # https://www.arduino.cc/reference/en/language/functions/usb/keyboard/
+ # Also locate Keyboard.h
+
+ match = re.match(r"(Digit|Key)([0-9A-Z])", key)
+ code: Union[str, int, None]
+ if match:
+ code = match.group(2)
+ else:
+ code = { # type: ignore
+ "Escape": 0xB1, "Backspace": 0xB2,
+ "Tab": 0xB3, "Enter": 0xB0,
+ "Insert": 0xD1, "Delete": 0xD4,
+ "Home": 0xD2, "End": 0xD5,
+ "PageUp": 0xD3, "PageDown": 0xD6,
+ "ArrowLeft": 0xD8, "ArrowRight": 0xD7,
+ "ArrowUp": 0xDA, "ArrowDown": 0xD9,
+
+ "CapsLock": 0xC1,
+ "ShiftLeft": 0x81, "ShiftRight": 0x85,
+ "ControlLeft": 0x80, "ControlRight": 0x84,
+ "AltLeft": 0x82, "AltRight": 0x86,
+ "MetaLeft": 0x83, "MetaRight": 0x87,
+
+ "Backquote": "`", "Minus": "-", "Equal": "=", "Space": " ",
+ "BracketLeft": "[", "BracketRight": "]", "Semicolon": ";", "Quote": "'",
+ "Comma": ",", "Period": ".", "Slash": "/", "Backslash": "\\",
+
+ "F1": 0xC2, "F2": 0xC3, "F3": 0xC4, "F4": 0xC5,
+ "F5": 0xC6, "F6": 0xC7, "F7": 0xC8, "F8": 0xC9,
+ "F9": 0xCA, "F10": 0xCB, "F11": 0xCC, "F12": 0xCD,
+ }.get(key)
+ if isinstance(code, str):
+ return bytes(code, encoding="ascii") # type: ignore
+ elif isinstance(code, int):
+ return bytes([code])
+ return b""
+
+
+class Hid(multiprocessing.Process):
+ def __init__(
+ self,
+ device_path: str,
+ speed: int,
+ ) -> None:
+
+ super().__init__(daemon=True)
+
+ self.__device_path = device_path
+ self.__speed = speed
+
+ self.__pressed_keys: Set[str] = set()
+ self.__lock = asyncio.Lock()
+ self.__queue: multiprocessing.queues.Queue = multiprocessing.Queue()
+
+ self.__stop_event = multiprocessing.Event()
+
+ def start(self) -> None:
+ get_logger().info("Starting HID daemon ...")
+ super().start()
+
+ async def send_key_event(self, key: str, state: bool) -> None:
+ if not self.__stop_event.is_set():
+ async with self.__lock:
+ if state and key not in self.__pressed_keys:
+ self.__pressed_keys.add(key)
+ self.__queue.put(_KeyEvent(key, state))
+ elif not state and key in self.__pressed_keys:
+ self.__pressed_keys.remove(key)
+ self.__queue.put(_KeyEvent(key, state))
+
+ async def clear_events(self) -> None:
+ if not self.__stop_event.is_set():
+ async with self.__lock:
+ self.__unsafe_clear_events()
+
+ async def cleanup(self) -> None:
+ async with self.__lock:
+ if self.is_alive():
+ self.__unsafe_clear_events()
+ get_logger().info("Stopping keyboard daemon ...")
+ self.__stop_event.set()
+ self.join()
+ else:
+ get_logger().warning("Emergency cleaning up keyboard events ...")
+ self.__emergency_clear_events()
+
+ def __unsafe_clear_events(self) -> None:
+ for key in self.__pressed_keys:
+ self.__queue.put(_KeyEvent(key, False))
+ self.__pressed_keys.clear()
+
+ def __emergency_clear_events(self) -> None:
+ try:
+ with serial.Serial(self.__device_path, self.__speed) as tty:
+ self.__send_clear_hid(tty)
+ except Exception:
+ get_logger().exception("Can't execute emergency clear events")
+
+ def run(self) -> None:
+ with gpio.bcm():
+ try:
+ with serial.Serial(self.__device_path, self.__speed) as tty:
+ while True:
+ try:
+ event = self.__queue.get(timeout=0.1)
+ except queue.Empty:
+ pass
+ else:
+ self.__send_key_event(tty, event)
+ if self.__stop_event.is_set() and self.__queue.qsize() == 0:
+ break
+ except Exception:
+ get_logger().exception("Unhandled exception")
+ raise
+
+ def __send_key_event(self, tty: serial.Serial, event: _KeyEvent) -> None:
+ key_bytes = _key_to_bytes(event.key)
+ if key_bytes:
+ assert len(key_bytes) == 1, (event, key_bytes)
+ tty.write(
+ b"\01"
+ + (b"\01" if event.state else b"\00")
+ + key_bytes
+ )
+
+ def __send_clear_hid(self, tty: serial.Serial) -> None:
+ tty.write(b"\00")
diff --git a/kvmd/kvmd/keyboard.py b/kvmd/kvmd/keyboard.py
deleted file mode 100644
index f8307d32..00000000
--- a/kvmd/kvmd/keyboard.py
+++ /dev/null
@@ -1,171 +0,0 @@
-import asyncio
-import multiprocessing
-import multiprocessing.queues
-import queue
-import time
-
-from typing import List
-from typing import Set
-from typing import NamedTuple
-
-from .logging import get_logger
-
-from . import gpio
-
-
-# =====
-class _KeyEvent(NamedTuple):
- key: str
- state: bool
-
-
-def _key_event_to_ps2_codes(event: _KeyEvent) -> List[int]:
- # https://techdocs.altium.com/display/FPGA/PS2+Keyboard+Scan+Codes
- # http://www.vetra.com/scancodes.html
-
- get_logger().info(str(event))
-
- if event.key == "PrintScreen":
- return ([0xE0, 0x12, 0xE0, 0x7C] if event.state else [0xE0, 0xF0, 0x7C, 0xE0, 0xF0, 0x12])
- # TODO: pause/break
- else:
- codes = {
- "Escape": [0x76], "Backspace": [0x66],
- "Tab": [0x0D], "Enter": [0x5A],
- "Insert": [0xE0, 0x70], "Delete": [0xE0, 0x71],
- "Home": [0xE0, 0x6C], "End": [0xE0, 0x69],
- "PageUp": [0xE0, 0x7D], "PageDown": [0xE0, 0x7A],
- "ArrowLeft": [0xE0, 0x6B], "ArrowRight": [0xE0, 0x74],
- "ArrowUp": [0xE0, 0x75], "ArrowDown": [0xE0, 0x72],
-
- "CapsLock": [0x58],
- "ScrollLock": [0x7E], "NumLock": [0x77],
- "ShiftLeft": [0x12], "ShiftRight": [0x59],
- "ControlLeft": [0x14], "ControlRight": [0xE0, 0x14],
- "AltLeft": [0x11], "AltRight": [0xE0, 0x11],
- "MetaLeft": [0xE0, 0x1F], "MetaRight": [0xE0, 0x27],
-
- "Backquote": [0x0E], "Minus": [0x4E], "Equal": [0x55], "Space": [0x29],
- "BracketLeft": [0x54], "BracketRight": [0x5B], "Semicolon": [0x4C], "Quote": [0x52],
- "Comma": [0x41], "Period": [0x49], "Slash": [0x4A], "Backslash": [0x5D],
-
- "Digit1": [0x16], "Digit2": [0x1E], "Digit3": [0x26], "Digit4": [0x25], "Digit5": [0x2E],
- "Digit6": [0x36], "Digit7": [0x3D], "Digit8": [0x3E], "Digit9": [0x46], "Digit0": [0x45],
-
- "KeyQ": [0x15], "KeyW": [0x1D], "KeyE": [0x24], "KeyR": [0x2D], "KeyT": [0x2C],
- "KeyY": [0x35], "KeyU": [0x3C], "KeyI": [0x43], "KeyO": [0x44], "KeyP": [0x4D],
- "KeyA": [0x1C], "KeyS": [0x1B], "KeyD": [0x23], "KeyF": [0x2B], "KeyG": [0x34],
- "KeyH": [0x33], "KeyJ": [0x3B], "KeyK": [0x42], "KeyL": [0x4B], "KeyZ": [0x1A],
- "KeyX": [0x22], "KeyC": [0x21], "KeyV": [0x2A], "KeyB": [0x32], "KeyN": [0x31],
- "KeyM": [0x3A],
-
- "F1": [0x05], "F2": [0x06], "F3": [0x04], "F4": [0x0C],
- "F5": [0x03], "F6": [0x0B], "F7": [0x83], "F8": [0x0A],
- "F9": [0x01], "F10": [0x09], "F11": [0x78], "F12": [0x07],
-
- # TODO: keypad
- }.get(event.key, [])
- if codes:
- if not event.state:
- assert 1 <= len(codes) <= 2, (event, codes)
- if len(codes) == 1:
- codes = [0xF0, codes[0]]
- elif len(codes) == 2:
- codes = [codes[0], 0xF0, codes[1]]
- return codes
- return []
-
-
-class Keyboard(multiprocessing.Process):
- # http://dkudrow.blogspot.com/2013/08/ps2-keyboard-emulation-with-arduino-uno.html
-
- def __init__(
- self,
- clock: int,
- data: int,
- pulse: float,
- ) -> None:
-
- super().__init__(daemon=True)
-
- self.__clock = gpio.set_output(clock, initial=True)
- self.__data = gpio.set_output(data, initial=True)
- self.__pulse = pulse
-
- self.__pressed_keys: Set[str] = set()
- self.__lock = asyncio.Lock()
- self.__queue: multiprocessing.queues.Queue = multiprocessing.Queue()
-
- self.__stop_event = multiprocessing.Event()
-
- def start(self) -> None:
- get_logger().info("Starting keyboard daemon ...")
- super().start()
-
- async def send_event(self, key: str, state: bool) -> None:
- if not self.__stop_event.is_set():
- async with self.__lock:
- if state and key not in self.__pressed_keys:
- self.__pressed_keys.add(key)
- self.__queue.put(_KeyEvent(key, state))
- elif not state and key in self.__pressed_keys:
- self.__pressed_keys.remove(key)
- self.__queue.put(_KeyEvent(key, state))
-
- async def clear_events(self) -> None:
- if not self.__stop_event.is_set():
- async with self.__lock:
- self.__unsafe_clear_events()
-
- async def cleanup(self) -> None:
- async with self.__lock:
- if self.is_alive():
- self.__unsafe_clear_events()
- get_logger().info("Stopping keyboard daemon ...")
- self.__stop_event.set()
- self.join()
- else:
- get_logger().warning("Emergency cleaning up keyboard events ...")
- self.__emergency_clear_events()
-
- def __unsafe_clear_events(self) -> None:
- for key in self.__pressed_keys:
- self.__queue.put(_KeyEvent(key, False))
- self.__pressed_keys.clear()
-
- def __emergency_clear_events(self) -> None:
- for key in self.__pressed_keys:
- for code in _key_event_to_ps2_codes(_KeyEvent(key, False)):
- self.__send_byte(code)
-
- def run(self) -> None:
- with gpio.bcm():
- try:
- while True:
- try:
- event = self.__queue.get(timeout=0.1)
- except queue.Empty:
- pass
- else:
- for code in _key_event_to_ps2_codes(event):
- self.__send_byte(code)
- if self.__stop_event.is_set() and self.__queue.qsize() == 0:
- break
- except Exception:
- get_logger().exception("Unhandled exception")
- raise
-
- def __send_byte(self, code: int) -> None:
- code_bits = list(map(bool, bin(code)[2:].zfill(8)))
- code_bits.reverse()
- message = [False] + code_bits + [(not sum(code_bits) % 2), True]
- for bit in message:
- self.__send_bit(bit)
-
- def __send_bit(self, bit: bool) -> None:
- gpio.write(self.__clock, True)
- gpio.write(self.__data, bool(bit))
- time.sleep(self.__pulse)
- gpio.write(self.__clock, False)
- time.sleep(self.__pulse)
- gpio.write(self.__clock, True)
diff --git a/kvmd/kvmd/server.py b/kvmd/kvmd/server.py
index ed26ec1b..34c4e97d 100644
--- a/kvmd/kvmd/server.py
+++ b/kvmd/kvmd/server.py
@@ -13,7 +13,7 @@ from typing import Type
import aiohttp.web
-from .keyboard import Keyboard
+from .hid import Hid
from .atx import Atx
@@ -66,7 +66,7 @@ def _json_200(result: Optional[Dict]=None) -> aiohttp.web.Response:
class Server: # pylint: disable=too-many-instance-attributes
def __init__(
self,
- keyboard: Keyboard,
+ hid: Hid,
atx: Atx,
msd: MassStorageDevice,
streamer: Streamer,
@@ -79,7 +79,7 @@ class Server: # pylint: disable=too-many-instance-attributes
loop: asyncio.AbstractEventLoop,
) -> None:
- self.__keyboard = keyboard
+ self.__hid = hid
self.__atx = atx
self.__msd = msd
self.__streamer = streamer
@@ -99,7 +99,7 @@ class Server: # pylint: disable=too-many-instance-attributes
self.__reset_streamer = False
def run(self, host: str, port: int) -> None:
- self.__keyboard.start()
+ self.__hid.start()
app = aiohttp.web.Application(loop=self.__loop)
@@ -119,7 +119,7 @@ class Server: # pylint: disable=too-many-instance-attributes
app.on_cleanup.append(self.__on_cleanup)
self.__system_tasks.extend([
- self.__loop.create_task(self.__keyboard_watchdog()),
+ self.__loop.create_task(self.__hid_watchdog()),
self.__loop.create_task(self.__stream_controller()),
self.__loop.create_task(self.__poll_dead_sockets()),
self.__loop.create_task(self.__poll_atx_state()),
@@ -143,7 +143,7 @@ class Server: # pylint: disable=too-many-instance-attributes
key = str(event.get("key", ""))[:64].strip()
state = event.get("state")
if key and state in [True, False]:
- await self.__keyboard.send_event(key, state)
+ await self.__hid.send_key_event(key, state)
continue
else:
logger.error("Invalid websocket event: %r", event)
@@ -240,15 +240,15 @@ class Server: # pylint: disable=too-many-instance-attributes
await self.__remove_socket(ws)
async def __on_cleanup(self, _: aiohttp.web.Application) -> None:
- await self.__keyboard.cleanup()
+ await self.__hid.cleanup()
await self.__streamer.cleanup()
await self.__msd.cleanup()
@_system_task
- async def __keyboard_watchdog(self) -> None:
- while self.__keyboard.is_alive():
+ async def __hid_watchdog(self) -> None:
+ while self.__hid.is_alive():
await asyncio.sleep(0.1)
- raise RuntimeError("Keyboard dead")
+ raise RuntimeError("HID is dead")
@_system_task
async def __stream_controller(self) -> None:
@@ -311,7 +311,7 @@ class Server: # pylint: disable=too-many-instance-attributes
async def __remove_socket(self, ws: aiohttp.web.WebSocketResponse) -> None:
async with self.__sockets_lock:
- await self.__keyboard.clear_events()
+ await self.__hid.clear_events()
try:
self.__sockets.remove(ws)
get_logger().info("Removed client socket: remote=%s; id=%d; active=%d",
diff --git a/kvmd/requirements.txt b/kvmd/requirements.txt
index bc7501dc..5a2205e0 100644
--- a/kvmd/requirements.txt
+++ b/kvmd/requirements.txt
@@ -3,3 +3,4 @@ aiohttp
aiofiles
pyudev
pyyaml
+pyserial
diff --git a/kvmd/testenv/Dockerfile b/kvmd/testenv/Dockerfile
index 12744df3..c8bd0c23 100644
--- a/kvmd/testenv/Dockerfile
+++ b/kvmd/testenv/Dockerfile
@@ -33,6 +33,7 @@ RUN pacman -Syy \
python-pip \
nginx \
mjpg-streamer-pikvm \
+ socat \
&& pacman -Sc --noconfirm
COPY testenv/requirements.txt requirements.txt
diff --git a/kvmd/testenv/kvmd.yaml b/kvmd/testenv/kvmd.yaml
index d27aacfc..8fc928c3 100644
--- a/kvmd/testenv/kvmd.yaml
+++ b/kvmd/testenv/kvmd.yaml
@@ -4,12 +4,9 @@ kvmd:
port: 8081
heartbeat: 3.0
- keyboard:
- pinout:
- clock: 17
- data: 4
-
- pulse: 0.0002
+ hid:
+ device: /dev/ttyS10
+ speed: 115200
atx:
pinout:
diff --git a/kvmd/testenv/requirements.txt b/kvmd/testenv/requirements.txt
index d054c0ee..66450f38 100644
--- a/kvmd/testenv/requirements.txt
+++ b/kvmd/testenv/requirements.txt
@@ -3,5 +3,6 @@ aiohttp
aiofiles
pyudev
pyyaml
+pyserial
bumpversion
tox