summaryrefslogtreecommitdiff
path: root/kvmd/apps
diff options
context:
space:
mode:
authorDevaev Maxim <[email protected]>2019-02-07 05:45:36 +0300
committerDevaev Maxim <[email protected]>2019-02-07 05:45:36 +0300
commitde1bed956cc714e8b750e3df22a563eac0800876 (patch)
tree8d5bea19bd8380313eab5ee1cdfb75784fdb3dc7 /kvmd/apps
parent5bec2ff1449832be374d69ece14e7a63d5d7379a (diff)
new hid protocol with crc
Diffstat (limited to 'kvmd/apps')
-rw-r--r--kvmd/apps/kvmd/__init__.py11
-rw-r--r--kvmd/apps/kvmd/hid.py262
-rw-r--r--kvmd/apps/kvmd/server.py14
3 files changed, 196 insertions, 91 deletions
diff --git a/kvmd/apps/kvmd/__init__.py b/kvmd/apps/kvmd/__init__.py
index e8b74abc..e9daea32 100644
--- a/kvmd/apps/kvmd/__init__.py
+++ b/kvmd/apps/kvmd/__init__.py
@@ -35,9 +35,18 @@ def main() -> None:
hid = Hid(
reset=int(config["hid"]["pinout"]["reset"]),
+
+ reset_delay=float(config["hid"]["reset_delay"]),
+
device_path=str(config["hid"]["device"]),
speed=int(config["hid"]["speed"]),
- reset_delay=float(config["hid"]["reset_delay"]),
+ read_timeout=float(config["hid"]["read_timeout"]),
+ read_retries=int(config["hid"]["read_retries"]),
+ common_retries=int(config["hid"]["common_retries"]),
+ retries_delay=float(config["hid"]["retries_delay"]),
+ noop=bool(config["hid"].get("noop", False)),
+
+ state_poll=float(config["hid"]["state_poll"]),
)
atx = Atx(
diff --git a/kvmd/apps/kvmd/hid.py b/kvmd/apps/kvmd/hid.py
index 26cfa898..3f329c64 100644
--- a/kvmd/apps/kvmd/hid.py
+++ b/kvmd/apps/kvmd/hid.py
@@ -11,6 +11,7 @@ import time
from typing import Dict
from typing import Set
from typing import NamedTuple
+from typing import AsyncGenerator
import yaml
import serial
@@ -33,41 +34,92 @@ class _KeyEvent(NamedTuple):
key: str
state: bool
+ @staticmethod
+ def is_valid(key: str) -> bool:
+ return (key in _KEYMAP)
+
+ def make_command(self) -> bytes:
+ code = _KEYMAP[self.key]
+ key_bytes = bytes([code])
+ assert len(key_bytes) == 1, (self, key_bytes, code)
+ state_bytes = (b"\x01" if self.state else b"\x00")
+ return b"\x11" + key_bytes + state_bytes + b"\x00\x00"
+
class _MouseMoveEvent(NamedTuple):
to_x: int
to_y: int
+ def make_command(self) -> bytes:
+ to_x = min(max(-32768, self.to_x), 32767)
+ to_y = min(max(-32768, self.to_y), 32767)
+ return b"\x12" + struct.pack(">hh", to_x, to_y)
+
class _MouseButtonEvent(NamedTuple):
button: str
state: bool
+ @staticmethod
+ def is_valid(button: str) -> bool:
+ return (button in ["left", "right"])
+
+ def make_command(self) -> bytes:
+ code = 0
+ if self.button == "left":
+ code = (0b10000000 | (0b00001000 if self.state else 0))
+ elif self.button == "right":
+ code = (0b01000000 | (0b00000100 if self.state else 0))
+ assert code, self
+ return b"\x13" + bytes([code]) + b"\x00\x00\x00"
+
class _MouseWheelEvent(NamedTuple):
delta_y: int
+ def make_command(self) -> bytes:
+ delta_y = min(max(-128, self.delta_y), 127)
+ return b"\x14\x00" + struct.pack(">b", delta_y) + b"\x00\x00"
+
class Hid(multiprocessing.Process): # pylint: disable=too-many-instance-attributes
- def __init__(
+ def __init__( # pylint: disable=too-many-arguments
self,
reset: int,
+ reset_delay: float,
+
device_path: str,
speed: int,
- reset_delay: float,
+ read_timeout: float,
+ read_retries: int,
+ common_retries: int,
+ retries_delay: float,
+ noop: bool,
+
+ state_poll: float,
) -> None:
super().__init__(daemon=True)
self.__reset = gpio.set_output(reset)
+ self.__reset_delay = reset_delay
+
self.__device_path = device_path
self.__speed = speed
- self.__reset_delay = reset_delay
+ self.__read_timeout = read_timeout
+ self.__read_retries = read_retries
+ self.__common_retries = common_retries
+ self.__retries_delay = retries_delay
+ self.__noop = noop
+
+ self.__state_poll = state_poll
self.__pressed_keys: Set[str] = set()
self.__pressed_mouse_buttons: Set[str] = set()
self.__lock = asyncio.Lock()
- self.__queue: multiprocessing.queues.Queue = multiprocessing.Queue()
+ self.__events_queue: multiprocessing.queues.Queue = multiprocessing.Queue()
+
+ self.__ok_shared = multiprocessing.Value("i", 1)
self.__stop_event = multiprocessing.Event()
@@ -75,6 +127,14 @@ class Hid(multiprocessing.Process): # pylint: disable=too-many-instance-attribu
get_logger().info("Starting HID daemon ...")
super().start()
+ def get_state(self) -> Dict:
+ return {"ok": bool(self.__ok_shared.value)}
+
+ async def poll_state(self) -> AsyncGenerator[Dict, None]:
+ while self.is_alive():
+ yield self.get_state()
+ await asyncio.sleep(self.__state_poll)
+
async def reset(self) -> None:
async with self.__lock:
gpio.write(self.__reset, True)
@@ -84,32 +144,34 @@ class Hid(multiprocessing.Process): # pylint: disable=too-many-instance-attribu
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))
+ if _KeyEvent.is_valid(key):
+ if state and key not in self.__pressed_keys:
+ self.__pressed_keys.add(key)
+ self.__events_queue.put(_KeyEvent(key, state))
+ elif not state and key in self.__pressed_keys:
+ self.__pressed_keys.remove(key)
+ self.__events_queue.put(_KeyEvent(key, state))
async def send_mouse_move_event(self, to_x: int, to_y: int) -> None:
if not self.__stop_event.is_set():
async with self.__lock:
- self.__queue.put(_MouseMoveEvent(to_x, to_y))
+ self.__events_queue.put(_MouseMoveEvent(to_x, to_y))
async def send_mouse_button_event(self, button: str, state: bool) -> None:
if not self.__stop_event.is_set():
async with self.__lock:
- if state and button not in self.__pressed_mouse_buttons:
- self.__pressed_mouse_buttons.add(button)
- self.__queue.put(_MouseButtonEvent(button, state))
- elif not state and button in self.__pressed_mouse_buttons:
- self.__pressed_mouse_buttons.remove(button)
- self.__queue.put(_MouseButtonEvent(button, state))
+ if _MouseButtonEvent.is_valid(button):
+ if state and button not in self.__pressed_mouse_buttons:
+ self.__pressed_mouse_buttons.add(button)
+ self.__events_queue.put(_MouseButtonEvent(button, state))
+ elif not state and button in self.__pressed_mouse_buttons:
+ self.__pressed_mouse_buttons.remove(button)
+ self.__events_queue.put(_MouseButtonEvent(button, state))
async def send_mouse_wheel_event(self, delta_y: int) -> None:
if not self.__stop_event.is_set():
async with self.__lock:
- self.__queue.put(_MouseWheelEvent(delta_y))
+ self.__events_queue.put(_MouseWheelEvent(delta_y))
async def clear_events(self) -> None:
if not self.__stop_event.is_set():
@@ -120,7 +182,7 @@ class Hid(multiprocessing.Process): # pylint: disable=too-many-instance-attribu
async with self.__lock:
if self.is_alive():
self.__unsafe_clear_events()
- get_logger().info("Stopping keyboard daemon ...")
+ get_logger().info("Stopping HID daemon ...")
self.__stop_event.set()
self.join()
else:
@@ -130,17 +192,17 @@ class Hid(multiprocessing.Process): # pylint: disable=too-many-instance-attribu
def __unsafe_clear_events(self) -> None:
for button in self.__pressed_mouse_buttons:
- self.__queue.put(_MouseButtonEvent(button, False))
+ self.__events_queue.put(_MouseButtonEvent(button, False))
self.__pressed_mouse_buttons.clear()
for key in self.__pressed_keys:
- self.__queue.put(_KeyEvent(key, False))
+ self.__events_queue.put(_KeyEvent(key, False))
self.__pressed_keys.clear()
def __emergency_clear_events(self) -> None:
if os.path.exists(self.__device_path):
try:
- with serial.Serial(self.__device_path, self.__speed) as tty:
- self.__send_clear_hid(tty)
+ with self.__get_serial() as tty:
+ self.__process_request(tty, b"\x10\x00\x00\x00\x00")
except Exception:
get_logger().exception("Can't execute emergency clear HID events")
@@ -148,70 +210,102 @@ class Hid(multiprocessing.Process): # pylint: disable=too-many-instance-attribu
signal.signal(signal.SIGINT, signal.SIG_IGN)
setproctitle.setproctitle("[hid] " + setproctitle.getproctitle())
try:
- with serial.Serial(self.__device_path, self.__speed) as tty:
- hid_ready = False
- while True:
- if hid_ready:
- try:
- event = self.__queue.get(timeout=0.05)
- except queue.Empty:
- pass
+ with self.__get_serial() as tty:
+ passed = 0
+ while not (self.__stop_event.is_set() and self.__events_queue.qsize() == 0):
+ try:
+ event = self.__events_queue.get(timeout=0.05)
+ except queue.Empty:
+ if passed >= 20: # 20 * 0.05 = 1 sec
+ self.__process_request(tty, b"\x01\x00\x00\x00\x00") # Ping
+ passed = 0
else:
- if isinstance(event, _KeyEvent):
- self.__send_key_event(tty, event)
- elif isinstance(event, _MouseMoveEvent):
- self.__send_mouse_move_event(tty, event)
- elif isinstance(event, _MouseButtonEvent):
- self.__send_mouse_button_event(tty, event)
- elif isinstance(event, _MouseWheelEvent):
- self.__send_mouse_wheel_event(tty, event)
- else:
- raise RuntimeError("Unknown HID event")
- hid_ready = False
-
- if tty.in_waiting:
- while tty.in_waiting:
- tty.read(tty.in_waiting)
- hid_ready = True
+ passed += 1
else:
- time.sleep(0.05)
-
- if self.__stop_event.is_set() and self.__queue.qsize() == 0:
- break
+ self.__process_request(tty, event.make_command())
+ passed = 0
except Exception:
get_logger().exception("Unhandled exception")
raise
- def __send_key_event(self, tty: serial.Serial, event: _KeyEvent) -> None:
- code = _KEYMAP.get(event.key)
- if code:
- key_bytes = bytes([code])
- assert len(key_bytes) == 1, (event, key_bytes)
- tty.write(
- b"\01"
- + key_bytes
- + (b"\01" if event.state else b"\00")
- + b"\00\00"
- )
-
- def __send_mouse_move_event(self, tty: serial.Serial, event: _MouseMoveEvent) -> None:
- to_x = min(max(-32768, event.to_x), 32767)
- to_y = min(max(-32768, event.to_y), 32767)
- tty.write(b"\02" + struct.pack(">hh", to_x, to_y))
-
- def __send_mouse_button_event(self, tty: serial.Serial, event: _MouseButtonEvent) -> None:
- if event.button == "left":
- code = (0b10000000 | (0b00001000 if event.state else 0))
- elif event.button == "right":
- code = (0b01000000 | (0b00000100 if event.state else 0))
- else:
- code = 0
- if code:
- tty.write(b"\03" + bytes([code]) + b"\00\00\00")
-
- def __send_mouse_wheel_event(self, tty: serial.Serial, event: _MouseWheelEvent) -> None:
- delta_y = min(max(-128, event.delta_y), 127)
- tty.write(b"\04\00" + struct.pack(">b", delta_y) + b"\00\00")
-
- def __send_clear_hid(self, tty: serial.Serial) -> None:
- tty.write(b"\00\00\00\00\00")
+ def __get_serial(self) -> serial.Serial:
+ return serial.Serial(self.__device_path, self.__speed, timeout=self.__read_timeout)
+
+ def __process(self, tty: serial.Serial, command: bytes) -> None:
+ self.__process_request(tty, self.__make_request(command))
+
+ def __process_request(self, tty: serial.Serial, request: bytes) -> None: # pylint: disable=too-many-branches
+ logger = get_logger()
+
+ common_retries = self.__common_retries
+ read_retries = self.__read_retries
+ error_occured = False
+
+ while common_retries and read_retries:
+ 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))
+
+ if len(response) < 4:
+ logger.error("No response from HID: request=%r", request)
+ read_retries -= 1
+ else:
+ assert len(response) == 4, response
+ if self.__make_crc16(response[-4:-2]) != struct.unpack(">H", response[-2:])[0]:
+ get_logger().error("Invalid response CRC; requesting response again ...")
+ request = self.__make_request(b"\x02\x00\x00\x00\x00") # Repeat an answer
+ else:
+ code = response[1]
+ if code == 0x48: # Request timeout
+ logger.error("Got request timeout from HID: request=%r", request)
+ elif code == 0x40: # CRC Error
+ logger.error("Got CRC error of request from HID: request=%r", request)
+ elif code == 0x45: # Unknown command
+ logger.error("HID did not recognize the request=%r", request)
+ self.__ok_shared.value = 1
+ return
+ elif code == 0x24: # Rebooted?
+ logger.error("No previous command state inside HID, seems it was rebooted")
+ self.__ok_shared.value = 1
+ return
+ elif code == 0x20: # Done
+ if error_occured:
+ logger.info("Success!")
+ self.__ok_shared.value = 1
+ return
+ else:
+ logger.error("Invalid response from HID: request=%r; code=0x%x", request, code)
+
+ common_retries -= 1
+ error_occured = True
+ self.__ok_shared.value = 0
+
+ if common_retries and read_retries:
+ logger.error("Retries left: common_retries=%d; read_retries=%d", common_retries, read_retries)
+ time.sleep(self.__retries_delay)
+
+ logger.error("Can't process HID request due many errors: %r", request)
+
+ 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/apps/kvmd/server.py b/kvmd/apps/kvmd/server.py
index f3e7ddc8..37e6ca5b 100644
--- a/kvmd/apps/kvmd/server.py
+++ b/kvmd/apps/kvmd/server.py
@@ -144,6 +144,7 @@ def _system_task(method: Callable) -> Callable:
async def wrap(self: "Server") -> None:
try:
await method(self)
+ raise RuntimeError("Dead system task: %s" % (method))
except asyncio.CancelledError:
pass
except Exception:
@@ -201,6 +202,7 @@ def _valid_int(name: str, value: Optional[str], min_value: Optional[int]=None, m
class _Events(Enum):
INFO_STATE = "info_state"
+ HID_STATE = "hid_state"
ATX_STATE = "atx_state"
MSD_STATE = "msd_state"
STREAMER_STATE = "streamer_state"
@@ -357,6 +359,7 @@ class Server: # pylint: disable=too-many-instance-attributes
await self.__register_socket(ws)
await asyncio.gather(*[
self.__broadcast_event(_Events.INFO_STATE, (await self.__make_info())),
+ self.__broadcast_event(_Events.HID_STATE, self.__hid.get_state()),
self.__broadcast_event(_Events.ATX_STATE, self.__atx.get_state()),
self.__broadcast_event(_Events.MSD_STATE, self.__msd.get_state()),
self.__broadcast_event(_Events.STREAMER_STATE, (await self.__streamer.get_state())),
@@ -568,12 +571,6 @@ class Server: # pylint: disable=too-many-instance-attributes
# ===== SYSTEM TASKS
@_system_task
- async def __hid_watchdog(self) -> None:
- while self.__hid.is_alive():
- await asyncio.sleep(0.1)
- raise RuntimeError("HID is dead")
-
- @_system_task
async def __stream_controller(self) -> None:
prev = 0
shutdown_at = 0.0
@@ -607,6 +604,11 @@ class Server: # pylint: disable=too-many-instance-attributes
await asyncio.sleep(0.1)
@_system_task
+ async def __poll_hid_state(self) -> None:
+ async for state in self.__hid.poll_state():
+ await self.__broadcast_event(_Events.HID_STATE, state)
+
+ @_system_task
async def __poll_atx_state(self) -> None:
async for state in self.__atx.poll_state():
await self.__broadcast_event(_Events.ATX_STATE, state)