This commit is contained in:
Devaev Maxim
2020-02-28 04:44:05 +03:00
parent a84b6bd31a
commit 1470ebe6fa
11 changed files with 241 additions and 121 deletions

View File

@@ -33,7 +33,7 @@ class BaseHid(BasePlugin):
def start(self) -> None:
pass
def get_state(self) -> Dict:
async def get_state(self) -> Dict:
raise NotImplementedError
async def poll_state(self) -> AsyncGenerator[Dict, None]:

View File

@@ -20,17 +20,12 @@
# ========================================================================== #
import asyncio
import concurrent.futures
import multiprocessing
import multiprocessing.queues
import queue
import functools
from typing import Dict
from typing import AsyncGenerator
from typing import Any
from .... import aiomulti
from ....yamlconf import Option
from ....validators.basic import valid_bool
@@ -54,10 +49,10 @@ class Plugin(BaseHid):
noop: bool,
) -> None:
self.__changes_queue: multiprocessing.queues.Queue = multiprocessing.Queue()
self.__state_notifier = aiomulti.AioProcessNotifier()
self.__keyboard_proc = KeyboardProcess(noop=noop, changes_queue=self.__changes_queue, **keyboard)
self.__mouse_proc = MouseProcess(noop=noop, changes_queue=self.__changes_queue, **mouse)
self.__keyboard_proc = KeyboardProcess(noop=noop, state_notifier=self.__state_notifier, **keyboard)
self.__mouse_proc = MouseProcess(noop=noop, state_notifier=self.__state_notifier, **mouse)
@classmethod
def get_plugin_options(cls) -> Dict:
@@ -81,31 +76,30 @@ class Plugin(BaseHid):
self.__keyboard_proc.start()
self.__mouse_proc.start()
def get_state(self) -> Dict:
keyboard_state = self.__keyboard_proc.get_state()
mouse_state = self.__mouse_proc.get_state()
async def get_state(self) -> Dict:
keyboard_state = await self.__keyboard_proc.get_state()
mouse_state = await self.__mouse_proc.get_state()
return {
"online": (keyboard_state["online"] and mouse_state["online"]),
"keyboard": {"features": {"leds": True}, **keyboard_state},
"keyboard": {
"online": keyboard_state["online"],
"leds": {
"caps": keyboard_state["caps"],
"scroll": keyboard_state["scroll"],
"num": keyboard_state["num"],
},
},
"mouse": mouse_state,
}
async def poll_state(self) -> AsyncGenerator[Dict, None]:
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
prev_state: Dict = {}
while True:
state = await self.get_state()
if state != prev_state:
yield state
prev_state = state
await self.__state_notifier.wait()
async def reset(self) -> None:
self.__keyboard_proc.send_reset_event()

View File

@@ -30,12 +30,13 @@ import errno
import time
from typing import Dict
from typing import Any
import setproctitle
from ....logging import get_logger
from .... import aiomulti
# =====
class BaseEvent:
@@ -48,7 +49,7 @@ class BaseDeviceProcess(multiprocessing.Process): # pylint: disable=too-many-in
name: str,
read_size: int,
initial_state: Dict,
changes_queue: multiprocessing.queues.Queue,
state_notifier: aiomulti.AioProcessNotifier,
device_path: str,
select_timeout: float,
@@ -61,7 +62,6 @@ class BaseDeviceProcess(multiprocessing.Process): # pylint: disable=too-many-in
self.__name = name
self.__read_size = read_size
self.__changes_queue = changes_queue
self.__device_path = device_path
self.__select_timeout = select_timeout
@@ -71,7 +71,7 @@ class BaseDeviceProcess(multiprocessing.Process): # pylint: disable=too-many-in
self.__fd = -1
self.__events_queue: multiprocessing.queues.Queue = multiprocessing.Queue()
self.__state_shared = multiprocessing.Manager().dict(online=True, **initial_state) # type: ignore
self.__state_flags = aiomulti.AioSharedFlags({"online": True, **initial_state}, state_notifier)
self.__stop_event = multiprocessing.Event()
def run(self) -> None:
@@ -100,8 +100,8 @@ class BaseDeviceProcess(multiprocessing.Process): # pylint: disable=too-many-in
self.__close_device()
def get_state(self) -> Dict:
return dict(self.__state_shared)
async def get_state(self) -> Dict:
return (await self.__state_flags.get())
# =====
@@ -111,6 +111,9 @@ class BaseDeviceProcess(multiprocessing.Process): # pylint: disable=too-many-in
def _process_read_report(self, report: bytes) -> None:
pass
def _update_state(self, **kwargs: bool) -> None:
self.__state_flags.update(**kwargs)
# =====
def _stop(self) -> None:
@@ -134,11 +137,6 @@ 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:
@@ -153,7 +151,7 @@ class BaseDeviceProcess(multiprocessing.Process): # pylint: disable=too-many-in
try:
written = os.write(self.__fd, report)
if written == len(report):
self._update_state("online", True)
self.__state_flags.update(online=True)
return True
else:
logger.error("HID-%s write() error: written (%s) != report length (%d)",
@@ -223,7 +221,7 @@ class BaseDeviceProcess(multiprocessing.Process): # pylint: disable=too-many-in
if self.__fd >= 0:
try:
if select.select([], [self.__fd], [], self.__select_timeout)[1]:
self._update_state("online", True)
self.__state_flags.update(online=True)
return True
else:
logger.debug("HID-%s is busy/unplugged (write select)", self.__name)
@@ -231,7 +229,7 @@ class BaseDeviceProcess(multiprocessing.Process): # pylint: disable=too-many-in
logger.error("Can't select() for write HID-%s: %s: %s", self.__name, type(err).__name__, err)
self.__close_device()
self._update_state("online", False)
self.__state_flags.update(online=False)
return False
def __close_device(self) -> None:

View File

@@ -68,7 +68,7 @@ class KeyboardProcess(BaseDeviceProcess):
super().__init__(
name="keyboard",
read_size=1,
initial_state={"leds": {"caps": False, "scroll": False, "num": False}},
initial_state={"caps": False, "scroll": False, "num": False},
**kwargs,
)
@@ -98,11 +98,11 @@ 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),
})
self._update_state(
caps=bool(report[0] & 2),
scroll=bool(report[0] & 4),
num=bool(report[0] & 1),
)
# =====

View File

@@ -23,9 +23,9 @@
import os
import signal
import asyncio
import dataclasses
import multiprocessing
import multiprocessing.queues
import dataclasses
import queue
import struct
import errno
@@ -40,6 +40,7 @@ import setproctitle
from ...logging import get_logger
from ... import aiotools
from ... import aiomulti
from ... import gpio
from ... import keymap
@@ -141,8 +142,6 @@ class Plugin(BaseHid, multiprocessing.Process): # pylint: disable=too-many-inst
common_retries: int,
retries_delay: float,
noop: bool,
state_poll: float,
) -> None:
multiprocessing.Process.__init__(self, daemon=True)
@@ -158,12 +157,18 @@ class Plugin(BaseHid, multiprocessing.Process): # pylint: disable=too-many-inst
self.__retries_delay = retries_delay
self.__noop = noop
self.__state_poll = state_poll
self.__lock = asyncio.Lock()
self.__events_queue: multiprocessing.queues.Queue = multiprocessing.Queue()
self.__online_shared = multiprocessing.Value("i", 1)
self.__state_notifier = aiomulti.AioProcessNotifier()
self.__state_flags = aiomulti.AioSharedFlags({
"online": True,
"caps": False,
"scroll": False,
"num": False,
}, self.__state_notifier)
self.__stop_event = multiprocessing.Event()
@classmethod
@@ -179,30 +184,35 @@ class Plugin(BaseHid, multiprocessing.Process): # pylint: disable=too-many-inst
"common_retries": Option(100, type=valid_int_f1),
"retries_delay": Option(0.1, type=valid_float_f01),
"noop": Option(False, type=valid_bool),
"state_poll": Option(0.1, type=valid_float_f01),
}
def start(self) -> None:
get_logger(0).info("Starting HID daemon ...")
multiprocessing.Process.start(self)
def get_state(self) -> Dict:
online = bool(self.__online_shared.value)
async def get_state(self) -> Dict:
state = await self.__state_flags.get()
return {
"online": online,
"keyboard": {"features": {"leds": False}, "online": online},
"mouse": {"online": online},
"online": state["online"],
"keyboard": {
"online": state["online"],
"leds": {
"caps": state["caps"],
"scroll": state["scroll"],
"num": state["num"],
},
},
"mouse": {"online": state["online"]},
}
async def poll_state(self) -> AsyncGenerator[Dict, None]:
prev_state: Dict = {}
while self.is_alive():
state = self.get_state()
while True:
state = await self.get_state()
if state != prev_state:
yield state
prev_state = state
await asyncio.sleep(self.__state_poll)
await self.__state_notifier.wait()
@aiotools.atomic
async def reset(self) -> None:
@@ -275,19 +285,13 @@ class Plugin(BaseHid, multiprocessing.Process): # pylint: disable=too-many-inst
while not self.__stop_event.is_set():
try:
with self.__get_serial() as tty:
passed = 0
while not (self.__stop_event.is_set() and self.__events_queue.qsize() == 0):
try:
event: _BaseEvent = self.__events_queue.get(timeout=0.05)
event: _BaseEvent = self.__events_queue.get(timeout=0.1)
except queue.Empty:
if passed >= 20: # 20 * 0.05 = 1 sec
self.__process_command(tty, b"\x01\x00\x00\x00\x00") # Ping
passed = 0
else:
passed += 1
self.__process_command(tty, b"\x01\x00\x00\x00\x00") # Ping
else:
self.__process_command(tty, event.make_command())
passed = 0
except serial.SerialException as err:
if err.errno == errno.ENOENT:
@@ -341,16 +345,24 @@ class Plugin(BaseHid, multiprocessing.Process): # pylint: disable=too-many-inst
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.__online_shared.value = 1
self.__state_flags.update(online=True)
return
elif code == 0x24: # Rebooted?
logger.error("No previous command state inside HID, seems it was rebooted")
self.__online_shared.value = 1
self.__state_flags.update(online=True)
return
elif code == 0x20: # Done
if error_occured:
logger.info("Success!")
self.__online_shared.value = 1
self.__state_flags.update(online=True)
return
elif code & 0x80: # Pong with leds
self.__state_flags.update(
online=True,
caps=bool(code & 0b00000001),
scroll=bool(code & 0x00000010),
num=bool(code & 0x00000100),
)
return
else:
logger.error("Invalid response from HID: request=%r; code=0x%x", request, code)
@@ -358,7 +370,7 @@ class Plugin(BaseHid, multiprocessing.Process): # pylint: disable=too-many-inst
common_retries -= 1
error_occured = True
self.__online_shared.value = 0
self.__state_flags.update(online=False)
if common_retries and read_retries:
logger.error("Retries left: common_retries=%d; read_retries=%d", common_retries, read_retries)