summaryrefslogtreecommitdiff
path: root/kvmd/plugins
diff options
context:
space:
mode:
authorDevaev Maxim <[email protected]>2020-02-20 11:11:39 +0300
committerDevaev Maxim <[email protected]>2020-02-20 11:11:39 +0300
commit6cd4a0a988da566243381d7387114f4cfe9eba5f (patch)
treed6ce16b110429a0c874421ce9e595e082ffff0ef /kvmd/plugins
parentd732b4f518ba59a69c608c8b2a46e5ec74ee294b (diff)
otg keyboard leds
Diffstat (limited to 'kvmd/plugins')
-rw-r--r--kvmd/plugins/hid/otg/__init__.py51
-rw-r--r--kvmd/plugins/hid/otg/device.py82
-rw-r--r--kvmd/plugins/hid/otg/keyboard.py18
-rw-r--r--kvmd/plugins/hid/otg/mouse.py7
-rw-r--r--kvmd/plugins/hid/serial.py2
5 files changed, 121 insertions, 39 deletions
diff --git a/kvmd/plugins/hid/otg/__init__.py b/kvmd/plugins/hid/otg/__init__.py
index c5dc1a7a..84c2803e 100644
--- a/kvmd/plugins/hid/otg/__init__.py
+++ b/kvmd/plugins/hid/otg/__init__.py
@@ -21,6 +21,11 @@
import asyncio
+import concurrent.futures
+import multiprocessing
+import multiprocessing.queues
+import queue
+import functools
from typing import Dict
from typing import AsyncGenerator
@@ -47,15 +52,12 @@ class Plugin(BaseHid):
keyboard: Dict[str, Any],
mouse: Dict[str, Any],
noop: bool,
- state_poll: float,
) -> None:
- self.__keyboard_proc = KeyboardProcess(noop=noop, **keyboard)
- self.__mouse_proc = MouseProcess(noop=noop, **mouse)
+ self.__changes_queue: multiprocessing.queues.Queue = multiprocessing.Queue()
- self.__state_poll = state_poll
-
- self.__lock = asyncio.Lock()
+ self.__keyboard_proc = KeyboardProcess(noop=noop, changes_queue=self.__changes_queue, **keyboard)
+ self.__mouse_proc = MouseProcess(noop=noop, changes_queue=self.__changes_queue, **mouse)
@classmethod
def get_plugin_options(cls) -> Dict:
@@ -66,16 +68,13 @@ class Plugin(BaseHid):
"write_retries": Option(5, type=valid_int_f1),
"write_retries_delay": Option(0.1, type=valid_float_f01),
},
-
"mouse": {
"device": Option("", type=valid_abs_path, unpack_as="device_path"),
"select_timeout": Option(1.0, type=valid_float_f01),
"write_retries": Option(5, type=valid_int_f1),
"write_retries_delay": Option(0.1, type=valid_float_f01),
},
-
- "noop": Option(False, type=valid_bool),
- "state_poll": Option(0.1, type=valid_float_f01),
+ "noop": Option(False, type=valid_bool),
}
def start(self) -> None:
@@ -83,22 +82,30 @@ class Plugin(BaseHid):
self.__mouse_proc.start()
def get_state(self) -> Dict:
- keyboard_online = self.__keyboard_proc.is_online()
- mouse_online = self.__mouse_proc.is_online()
+ keyboard_state = self.__keyboard_proc.get_state()
+ mouse_state = self.__mouse_proc.get_state()
return {
- "online": (keyboard_online and mouse_online),
- "keyboard": {"online": keyboard_online},
- "mouse": {"online": mouse_online},
+ "online": (keyboard_state["online"] and mouse_state["online"]),
+ "keyboard": {"features": {"leds": True}, **keyboard_state},
+ "mouse": mouse_state,
}
async def poll_state(self) -> AsyncGenerator[Dict, None]:
- prev_state: Dict = {}
- while self.__keyboard_proc.is_alive() and self.__mouse_proc.is_alive():
- state = self.get_state()
- if state != prev_state:
- yield self.get_state()
- prev_state = state
- await asyncio.sleep(self.__state_poll)
+ loop = asyncio.get_running_loop()
+ wait_for_changes = functools.partial(self.__changes_queue.get, timeout=1)
+ with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
+ prev_state: Dict = {}
+ while True:
+ state = self.get_state()
+ if state != prev_state:
+ yield state
+ prev_state = state
+ while True:
+ try:
+ await loop.run_in_executor(executor, wait_for_changes)
+ break
+ except queue.Empty:
+ pass
async def reset(self) -> None:
self.__keyboard_proc.send_reset_event()
diff --git a/kvmd/plugins/hid/otg/device.py b/kvmd/plugins/hid/otg/device.py
index 7da6dd18..0fbb63cb 100644
--- a/kvmd/plugins/hid/otg/device.py
+++ b/kvmd/plugins/hid/otg/device.py
@@ -29,6 +29,9 @@ import queue
import errno
import time
+from typing import Dict
+from typing import Any
+
import setproctitle
from ....logging import get_logger
@@ -43,6 +46,10 @@ class BaseDeviceProcess(multiprocessing.Process): # pylint: disable=too-many-in
def __init__(
self,
name: str,
+ read_size: int,
+ initial_state: Dict,
+ changes_queue: multiprocessing.queues.Queue,
+
device_path: str,
select_timeout: float,
write_retries: int,
@@ -53,6 +60,8 @@ class BaseDeviceProcess(multiprocessing.Process): # pylint: disable=too-many-in
super().__init__(daemon=True)
self.__name = name
+ self.__read_size = read_size
+ self.__changes_queue = changes_queue
self.__device_path = device_path
self.__select_timeout = select_timeout
@@ -62,7 +71,7 @@ class BaseDeviceProcess(multiprocessing.Process): # pylint: disable=too-many-in
self.__fd = -1
self.__events_queue: multiprocessing.queues.Queue = multiprocessing.Queue()
- self.__online_shared = multiprocessing.Value("i", 1)
+ self.__state_shared = multiprocessing.Manager().dict(online=True, **initial_state) # type: ignore
self.__stop_event = multiprocessing.Event()
def run(self) -> None:
@@ -75,10 +84,12 @@ class BaseDeviceProcess(multiprocessing.Process): # pylint: disable=too-many-in
while not self.__stop_event.is_set():
try:
while not self.__stop_event.is_set():
+ if self.__ensure_device(): # Check device and process reports if needed
+ self.__read_all_reports()
try:
- event: BaseEvent = self.__events_queue.get(timeout=1)
+ event: BaseEvent = self.__events_queue.get(timeout=0.1)
except queue.Empty:
- self.__ensure_device() # Check device
+ pass
else:
self._process_event(event)
except Exception:
@@ -89,8 +100,18 @@ class BaseDeviceProcess(multiprocessing.Process): # pylint: disable=too-many-in
self.__close_device()
- def is_online(self) -> bool:
- return bool(self.__online_shared.value and self.is_alive())
+ def get_state(self) -> Dict:
+ return dict(self.__state_shared)
+
+ # =====
+
+ def _process_event(self, event: BaseEvent) -> None:
+ raise NotImplementedError
+
+ def _process_read_report(self, report: bytes) -> None:
+ pass
+
+ # =====
def _stop(self) -> None:
if self.is_alive():
@@ -99,9 +120,6 @@ class BaseDeviceProcess(multiprocessing.Process): # pylint: disable=too-many-in
if self.exitcode is not None:
self.join()
- def _process_event(self, event: BaseEvent) -> None:
- raise NotImplementedError
-
def _queue_event(self, event: BaseEvent) -> None:
self.__events_queue.put(event)
@@ -116,6 +134,11 @@ class BaseDeviceProcess(multiprocessing.Process): # pylint: disable=too-many-in
if close:
self.__close_device()
+ def _update_state(self, key: str, value: Any) -> None:
+ if self.__state_shared[key] != value:
+ self.__state_shared[key] = value
+ self.__changes_queue.put(None)
+
# =====
def __write_report(self, report: bytes) -> bool:
@@ -130,7 +153,7 @@ class BaseDeviceProcess(multiprocessing.Process): # pylint: disable=too-many-in
try:
written = os.write(self.__fd, report)
if written == len(report):
- self.__online_shared.value = 1
+ self._update_state("online", True)
return True
else:
logger.error("HID-%s write() error: written (%s) != report length (%d)",
@@ -151,6 +174,33 @@ class BaseDeviceProcess(multiprocessing.Process): # pylint: disable=too-many-in
self.__close_device()
return False
+ def __read_all_reports(self) -> None:
+ if self.__noop or self.__read_size == 0:
+ return
+
+ assert self.__fd >= 0
+ logger = get_logger()
+
+ read = True
+ while read:
+ try:
+ read = bool(select.select([self.__fd], [], [], 0)[0])
+ except Exception as err:
+ logger.error("Can't select() for read HID-%s: %s: %s", self.__name, type(err).__name__, err)
+ break
+
+ if read:
+ try:
+ report = os.read(self.__fd, self.__read_size)
+ except Exception as err:
+ if isinstance(err, OSError) and err.errno == errno.EAGAIN: # pylint: disable=no-member
+ logger.debug("HID-%s busy/unplugged (read): %s: %s",
+ self.__name, type(err).__name__, err)
+ else:
+ logger.exception("Can't read report from HID-%s", self.__name)
+ else:
+ self._process_read_report(report)
+
def __ensure_device(self) -> bool:
if self.__noop:
return True
@@ -159,25 +209,29 @@ class BaseDeviceProcess(multiprocessing.Process): # pylint: disable=too-many-in
if self.__fd < 0:
try:
- self.__fd = os.open(self.__device_path, os.O_WRONLY|os.O_NONBLOCK)
+ flags = os.O_NONBLOCK
+ flags |= (os.O_RDWR if self.__read_size else os.O_WRONLY)
+ self.__fd = os.open(self.__device_path, flags)
except FileNotFoundError:
logger.error("Missing HID-%s device: %s", self.__name, self.__device_path)
+ time.sleep(self.__select_timeout)
except Exception as err:
logger.error("Can't open HID-%s device: %s: %s: %s",
self.__name, self.__device_path, type(err).__name__, err)
+ time.sleep(self.__select_timeout)
if self.__fd >= 0:
try:
if select.select([], [self.__fd], [], self.__select_timeout)[1]:
- self.__online_shared.value = 1
+ self._update_state("online", True)
return True
else:
- logger.debug("HID-%s is busy/unplugged (select)", self.__name)
+ logger.debug("HID-%s is busy/unplugged (write select)", self.__name)
except Exception as err:
- logger.error("Can't select() HID-%s: %s: %s", self.__name, type(err).__name__, err)
+ logger.error("Can't select() for write HID-%s: %s: %s", self.__name, type(err).__name__, err)
self.__close_device()
- self.__online_shared.value = 0
+ self._update_state("online", False)
return False
def __close_device(self) -> None:
diff --git a/kvmd/plugins/hid/otg/keyboard.py b/kvmd/plugins/hid/otg/keyboard.py
index 6cf8f72d..6c9ddfed 100644
--- a/kvmd/plugins/hid/otg/keyboard.py
+++ b/kvmd/plugins/hid/otg/keyboard.py
@@ -65,7 +65,12 @@ class _KeyEvent(BaseEvent):
# =====
class KeyboardProcess(BaseDeviceProcess):
def __init__(self, **kwargs: Any) -> None:
- super().__init__(name="keyboard", **kwargs)
+ super().__init__(
+ name="keyboard",
+ read_size=1,
+ initial_state={"leds": {"caps": False, "scroll": False, "num": False}},
+ **kwargs,
+ )
self.__pressed_modifiers: Set[keymap.OtgKey] = set()
self.__pressed_keys: List[Optional[keymap.OtgKey]] = [None] * 6
@@ -90,6 +95,17 @@ class KeyboardProcess(BaseDeviceProcess):
# =====
+ def _process_read_report(self, report: bytes) -> None:
+ # https://wiki.osdev.org/USB_Human_Interface_Devices#LED_lamps
+ assert len(report) == 1, report
+ self._update_state("leds", {
+ "caps": bool(report[0] & 2),
+ "scroll": bool(report[0] & 4),
+ "num": bool(report[0] & 1),
+ })
+
+ # =====
+
def _process_event(self, event: BaseEvent) -> None:
if isinstance(event, _ClearEvent):
self.__process_clear_event()
diff --git a/kvmd/plugins/hid/otg/mouse.py b/kvmd/plugins/hid/otg/mouse.py
index 172ed666..a31d7ae5 100644
--- a/kvmd/plugins/hid/otg/mouse.py
+++ b/kvmd/plugins/hid/otg/mouse.py
@@ -61,7 +61,12 @@ class _WheelEvent(BaseEvent):
# =====
class MouseProcess(BaseDeviceProcess):
def __init__(self, **kwargs: Any) -> None:
- super().__init__(name="mouse", **kwargs)
+ super().__init__(
+ name="mouse",
+ read_size=0,
+ initial_state={},
+ **kwargs,
+ )
self.__pressed_buttons: int = 0
self.__x = 0
diff --git a/kvmd/plugins/hid/serial.py b/kvmd/plugins/hid/serial.py
index 9927b3aa..d5561e2f 100644
--- a/kvmd/plugins/hid/serial.py
+++ b/kvmd/plugins/hid/serial.py
@@ -191,7 +191,7 @@ class Plugin(BaseHid, multiprocessing.Process): # pylint: disable=too-many-inst
online = bool(self.__online_shared.value)
return {
"online": online,
- "keyboard": {"online": online},
+ "keyboard": {"features": {"leds": False}, "online": online},
"mouse": {"online": online},
}