better api, refactoring

This commit is contained in:
Devaev Maxim 2018-07-02 21:17:19 +03:00
parent 87f8cb350b
commit 0582398521
6 changed files with 136 additions and 84 deletions

View File

@ -25,12 +25,12 @@ def main() -> None:
)
atx = Atx(
power_led=int(config["atx"]["leds"]["pinout"]["power"]),
hdd_led=int(config["atx"]["leds"]["pinout"]["hdd"]),
power_switch=int(config["atx"]["switches"]["pinout"]["power"]),
reset_switch=int(config["atx"]["switches"]["pinout"]["reset"]),
click_delay=float(config["atx"]["switches"]["click_delay"]),
long_click_delay=float(config["atx"]["switches"]["long_click_delay"]),
power_led=int(config["atx"]["pinout"]["power_led"]),
hdd_led=int(config["atx"]["pinout"]["hdd_led"]),
power_switch=int(config["atx"]["pinout"]["power_switch"]),
reset_switch=int(config["atx"]["pinout"]["reset_switch"]),
click_delay=float(config["atx"]["click_delay"]),
long_click_delay=float(config["atx"]["long_click_delay"]),
)
msd = MassStorageDevice(
@ -40,10 +40,12 @@ def main() -> None:
)
streamer = Streamer(
cap_power=int(config["video"]["pinout"]["cap"]),
conv_power=int(config["video"]["pinout"]["conv"]),
sync_delay=float(config["video"]["sync_delay"]),
cmd=list(map(str, config["video"]["cmd"])),
cap_power=int(config["streamer"]["pinout"]["cap"]),
conv_power=int(config["streamer"]["pinout"]["conv"]),
sync_delay=float(config["streamer"]["sync_delay"]),
width=int(config["streamer"]["size"]["width"]),
height=int(config["streamer"]["size"]["height"]),
cmd=list(map(str, config["streamer"]["cmd"])),
loop=loop,
)
@ -53,8 +55,8 @@ def main() -> None:
msd=msd,
streamer=streamer,
heartbeat=float(config["server"]["heartbeat"]),
atx_leds_poll=float(config["atx"]["leds"]["poll"]),
video_shutdown_delay=float(config["video"]["shutdown_delay"]),
atx_state_poll=float(config["atx"]["state_poll"]),
streamer_shutdown_delay=float(config["streamer"]["shutdown_delay"]),
msd_chunk_size=int(config["msd"]["chunk_size"]),
loop=loop,
).run(

View File

@ -1,6 +1,6 @@
import asyncio
from typing import Tuple
from typing import Dict
from .logging import get_logger
@ -29,11 +29,13 @@ class Atx:
self.__lock = asyncio.Lock()
def get_leds(self) -> Tuple[bool, bool]:
return (
not gpio.read(self.__power_led),
not gpio.read(self.__hdd_led),
)
def get_state(self) -> Dict:
return {
"leds": {
"power": (not gpio.read(self.__power_led)),
"hdd": (not gpio.read(self.__hdd_led)),
},
}
async def click_power(self) -> None:
if (await self.__click(self.__power_switch, self.__click_delay)):

View File

@ -118,8 +118,12 @@ class MassStorageDevice:
get_logger().info("Using bind %r as mass-storage device", self._bind)
try:
loop.run_until_complete(self.connect_to_kvm(no_delay=True))
except Exception:
get_logger().exception("Mass-storage device is not operational")
except Exception as err:
if isinstance(err, MassStorageError):
log = get_logger().warning
else:
log = get_logger().exception
log("Mass-storage device is not operational: %s", err)
self._bind = ""
else:
get_logger().warning("Missing bind; mass-storage device is not operational")
@ -133,7 +137,7 @@ class MassStorageDevice:
await asyncio.sleep(self.__init_delay)
path = locate_by_bind(self._bind)
if not path:
raise RuntimeError("Can't locate device by bind %r" % (self._bind))
raise MassStorageError("Can't locate device by bind %r" % (self._bind))
self.__device_info = explore_device(path)
get_logger().info("Mass-storage device switched to KVM: %s", self.__device_info)

View File

@ -1,9 +1,11 @@
import os
import signal
import asyncio
import json
import time
from typing import List
from typing import Dict
from typing import Set
from typing import Callable
from typing import Optional
@ -38,19 +40,29 @@ def _system_task(method: Callable) -> Callable:
def _exceptions_as_400(msg: str, exceptions: List[Type[Exception]]) -> Callable:
def make_wrapper(method: Callable) -> Callable:
async def wrap(self: "Server", request: aiohttp.web.Request) -> aiohttp.web.WebSocketResponse:
async def wrap(self: "Server", request: aiohttp.web.Request) -> aiohttp.web.Response:
try:
return (await method(self, request))
except tuple(exceptions) as err: # pylint: disable=catching-non-exception
get_logger().exception(msg)
return aiohttp.web.json_response({
"error": type(err).__name__,
"error_msg": str(err),
"ok": False,
"result": {
"error": type(err).__name__,
"error_msg": str(err),
},
}, status=400)
return wrap
return make_wrapper
def _json_200(result: Optional[Dict]=None) -> aiohttp.web.Response:
return aiohttp.web.json_response({
"ok": True,
"result": (result or {}),
})
class Server: # pylint: disable=too-many-instance-attributes
def __init__(
self,
@ -59,8 +71,8 @@ class Server: # pylint: disable=too-many-instance-attributes
msd: MassStorageDevice,
streamer: Streamer,
heartbeat: float,
atx_leds_poll: float,
video_shutdown_delay: float,
atx_state_poll: float,
streamer_shutdown_delay: float,
msd_chunk_size: int,
loop: asyncio.AbstractEventLoop,
) -> None:
@ -70,8 +82,8 @@ class Server: # pylint: disable=too-many-instance-attributes
self.__msd = msd
self.__streamer = streamer
self.__heartbeat = heartbeat
self.__video_shutdown_delay = video_shutdown_delay
self.__atx_leds_poll = atx_leds_poll
self.__streamer_shutdown_delay = streamer_shutdown_delay
self.__atx_state_poll = atx_state_poll
self.__msd_chunk_size = msd_chunk_size
self.__loop = loop
@ -80,17 +92,25 @@ class Server: # pylint: disable=too-many-instance-attributes
self.__system_tasks: List[asyncio.Task] = []
self.__restart_video = False
self.__reset_streamer = False
def run(self, host: str, port: int) -> None:
self.__keyboard.start()
app = aiohttp.web.Application(loop=self.__loop)
app.router.add_get("/", self.__root_handler)
app.router.add_get("/ws", self.__ws_handler)
app.router.add_get("/atx", self.__atx_state_handler)
app.router.add_post("/atx/click", self.__atx_click_handler)
app.router.add_get("/msd", self.__msd_state_handler)
app.router.add_post("/msd/connect", self.__msd_connect_handler)
app.router.add_post("/msd/write", self.__msd_write_handler)
app.router.add_get("/streamer", self.__streamer_state_handler)
app.router.add_post("/streamer/reset", self.__streamer_reset_handler)
app.on_shutdown.append(self.__on_shutdown)
app.on_cleanup.append(self.__on_cleanup)
@ -98,44 +118,55 @@ class Server: # pylint: disable=too-many-instance-attributes
self.__loop.create_task(self.__keyboard_watchdog()),
self.__loop.create_task(self.__stream_controller()),
self.__loop.create_task(self.__poll_dead_sockets()),
self.__loop.create_task(self.__poll_atx_leds()),
self.__loop.create_task(self.__poll_atx_state()),
])
aiohttp.web.run_app(app, host=host, port=port, print=self.__run_app_print)
async def __root_handler(self, _: aiohttp.web.Request) -> aiohttp.web.Response:
return aiohttp.web.Response(text="OK")
async def __ws_handler(self, request: aiohttp.web.Request) -> aiohttp.web.WebSocketResponse:
ws = aiohttp.web.WebSocketResponse(heartbeat=self.__heartbeat)
await ws.prepare(request)
await self.__register_socket(ws)
async for msg in ws:
if msg.type == aiohttp.web.WSMsgType.TEXT:
retval = await self.__execute_command(msg.data)
if retval:
await ws.send_str(retval)
await ws.send_str(json.dumps({"msg_type": "echo", "msg": msg.data}))
else:
break
return ws
async def __atx_state_handler(self, _: aiohttp.web.Request) -> aiohttp.web.Response:
return _json_200(self.__atx.get_state())
@_exceptions_as_400("Click error", [RuntimeError])
async def __atx_click_handler(self, request: aiohttp.web.Request) -> aiohttp.web.Response:
button = request.query.get("button")
if button == "power":
await self.__atx.click_power()
elif button == "power_long":
await self.__atx.click_power_long()
elif button == "reset":
await self.__atx.click_reset()
else:
raise RuntimeError("Missing or invalid 'button=%s'" % (button))
return _json_200({"clicked": button})
async def __msd_state_handler(self, _: aiohttp.web.Request) -> aiohttp.web.Response:
return aiohttp.web.json_response(self.__msd.get_state())
return _json_200(self.__msd.get_state())
@_exceptions_as_400("Mass-storage error", [MassStorageError, RuntimeError])
async def __msd_connect_handler(self, request: aiohttp.web.Request) -> aiohttp.web.Response:
to = request.query.get("to")
if to == "kvm":
await self.__msd.connect_to_kvm()
await self.__broadcast("EVENT msd connected_to_kvm")
await self.__broadcast_event("msd_state", state="connected_to_kvm") # type: ignore
elif to == "pc":
await self.__msd.connect_to_pc()
await self.__broadcast("EVENT msd connected_to_pc")
await self.__broadcast_event("msd_state", state="connected_to_pc") # type: ignore
else:
raise RuntimeError("Missing or invalid 'to=%s'" % (to))
return aiohttp.web.json_response(self.__msd.get_state())
return _json_200(self.__msd.get_state())
@_exceptions_as_400("Can't write image to mass-storage device", [MassStorageError, RuntimeError, OSError])
@_exceptions_as_400("Can't write data to mass-storage device", [MassStorageError, RuntimeError, OSError])
async def __msd_write_handler(self, request: aiohttp.web.Request) -> aiohttp.web.Response:
logger = get_logger(0)
reader = await request.multipart()
@ -146,18 +177,25 @@ class Server: # pylint: disable=too-many-instance-attributes
raise RuntimeError("Missing 'data' field")
async with self.__msd:
await self.__broadcast("EVENT msd busy")
await self.__broadcast_event("msd_state", state="busy") # type: ignore
logger.info("Writing image to mass-storage device ...")
while True:
chunk = await field.read_chunk(self.__msd_chunk_size)
if not chunk:
break
writed = await self.__msd.write(chunk)
await self.__broadcast("EVENT msd free")
await self.__broadcast_event("msd_state", state="free") # type: ignore
finally:
if writed != 0:
logger.info("Writed %d bytes to mass-storage device", writed)
return aiohttp.web.json_response({"writed": writed})
return _json_200({"writed": writed})
async def __streamer_state_handler(self, _: aiohttp.web.Request) -> aiohttp.web.Response:
return _json_200(self.__streamer.get_state())
async def __streamer_reset_handler(self, _: aiohttp.web.Request) -> aiohttp.web.Response:
self.__reset_streamer = True
return _json_200()
def __run_app_print(self, text: str) -> None:
logger = get_logger()
@ -198,16 +236,16 @@ class Server: # pylint: disable=too-many-instance-attributes
if not self.__streamer.is_running():
await self.__streamer.start()
elif prev > 0 and cur == 0:
shutdown_at = time.time() + self.__video_shutdown_delay
shutdown_at = time.time() + self.__streamer_shutdown_delay
elif prev == 0 and cur == 0 and time.time() > shutdown_at:
if self.__streamer.is_running():
await self.__streamer.stop()
if self.__restart_video:
if self.__reset_streamer:
if self.__streamer.is_running():
await self.__streamer.stop()
await self.__streamer.start()
self.__restart_video = False
self.__reset_streamer = False
prev = cur
await asyncio.sleep(0.1)
@ -221,36 +259,25 @@ class Server: # pylint: disable=too-many-instance-attributes
await asyncio.sleep(0.1)
@_system_task
async def __poll_atx_leds(self) -> None:
async def __poll_atx_state(self) -> None:
while True:
if self.__sockets:
await self.__broadcast("EVENT atx_leds %d %d" % (self.__atx.get_leds()))
await asyncio.sleep(self.__atx_leds_poll)
await self.__broadcast_event("atx_state", **self.__atx.get_state())
await asyncio.sleep(self.__atx_state_poll)
async def __broadcast(self, msg: str) -> None:
async def __broadcast_event(self, event: str, **kwargs: Dict) -> None:
await asyncio.gather(*[
ws.send_str(msg)
ws.send_str(json.dumps({
"msg_type": "event",
"msg": {
"event": event,
"event_attrs": kwargs,
},
}))
for ws in list(self.__sockets)
if not ws.closed and ws._req.transport # pylint: disable=protected-access
], return_exceptions=True)
async def __execute_command(self, command: str) -> Optional[str]:
(command, args) = (command.strip().split(" ", maxsplit=1) + [""])[:2]
if command == "CLICK":
method = {
"power": self.__atx.click_power,
"power_long": self.__atx.click_power_long,
"reset": self.__atx.click_reset,
}.get(args)
if method:
await method()
return None
elif command == "RESTART_VIDEO":
self.__restart_video = True
return None
get_logger().warning("Received an incorrect command: %r", command)
return "ERROR incorrect command"
async def __register_socket(self, ws: aiohttp.web.WebSocketResponse) -> None:
async with self.__sockets_lock:
self.__sockets.add(ws)

View File

@ -2,6 +2,7 @@ import asyncio
import asyncio.subprocess
from typing import List
from typing import Dict
from typing import Optional
from .logging import get_logger
@ -16,6 +17,8 @@ class Streamer: # pylint: disable=too-many-instance-attributes
cap_power: int,
conv_power: int,
sync_delay: float,
width: int,
height: int,
cmd: List[str],
loop: asyncio.AbstractEventLoop,
) -> None:
@ -25,7 +28,9 @@ class Streamer: # pylint: disable=too-many-instance-attributes
self.__cap_power = gpio.set_output(cap_power)
self.__conv_power = (gpio.set_output(conv_power) if conv_power > 0 else conv_power)
self.__sync_delay = sync_delay
self.__cmd = cmd
self.__width = width
self.__height = height
self.__cmd = [part.format(width=width, height=height) for part in cmd]
self.__loop = loop
@ -48,6 +53,15 @@ class Streamer: # pylint: disable=too-many-instance-attributes
def is_running(self) -> bool:
return bool(self.__proc_task)
def get_state(self) -> Dict:
return {
"is_running": self.is_running(),
"size": {
"width": self.__width,
"height": self.__height,
},
}
async def cleanup(self) -> None:
if self.is_running():
await self.stop()

View File

@ -8,21 +8,20 @@ kvmd:
pinout:
clock: 17
data: 4
pulse: 0.0002
atx:
leds:
pinout:
power: 16
hdd: 12
poll: 0.1
pinout:
power_led: 16
hdd_led: 12
power_switch: 26
reset_switch: 20
switches:
pinout:
power: 26
reset: 20
click_delay: 0.1
long_click_delay: 5.5
click_delay: 0.1
long_click_delay: 5.5
state_poll: 0.1
msd:
# FIXME: It's for laptop lol
@ -30,17 +29,21 @@ kvmd:
init_delay: 2.0
chunk_size: 8192
video:
streamer:
pinout:
cap: 21
conv: 25
sync_delay: 1.0
shutdown_delay: 10.0
size:
width: 720
height: 576
cmd:
- "/usr/bin/mjpg_streamer"
- "-i"
- "input_uvc.so -d /dev/video0 -e 2 -y -n -r 720x576"
- "input_uvc.so -d /dev/video0 -e 2 -y -n -r {width}x{height}"
- "-o"
- "output_http.so -l localhost -p 8082"