mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-01-29 00:51:53 +08:00
otg keyboard leds
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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},
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user