summaryrefslogtreecommitdiff
path: root/kvmd
diff options
context:
space:
mode:
Diffstat (limited to 'kvmd')
-rw-r--r--kvmd/kvmd/hid.py89
-rw-r--r--kvmd/kvmd/server.py62
-rw-r--r--kvmd/web/css/stream.css8
-rw-r--r--kvmd/web/js/hid.js34
-rw-r--r--kvmd/web/js/keyboard.js6
-rw-r--r--kvmd/web/js/mouse.js31
-rw-r--r--kvmd/web/js/stream.js4
-rw-r--r--kvmd/web/svg/stream-mouse-cursor.svg68
8 files changed, 230 insertions, 72 deletions
diff --git a/kvmd/kvmd/hid.py b/kvmd/kvmd/hid.py
index 0a3511b7..0f192fdf 100644
--- a/kvmd/kvmd/hid.py
+++ b/kvmd/kvmd/hid.py
@@ -2,6 +2,7 @@ import asyncio
import multiprocessing
import multiprocessing.queues
import queue
+import struct
import pkgutil
from typing import Dict
@@ -22,16 +23,25 @@ def _get_keymap() -> Dict[str, int]:
_KEYMAP = _get_keymap()
-def _keymap(key: str) -> bytes:
- code = _KEYMAP.get(key)
- return (bytes([code]) if code else b"") # type: ignore
-
-
class _KeyEvent(NamedTuple):
key: str
state: bool
+class _MouseMoveEvent(NamedTuple):
+ to_x: int
+ to_y: int
+
+
+class _MouseButtonEvent(NamedTuple):
+ button: str
+ state: bool
+
+
+class _MouseWheelEvent(NamedTuple):
+ delta_y: int
+
+
class Hid(multiprocessing.Process):
def __init__(
self,
@@ -45,6 +55,7 @@ class Hid(multiprocessing.Process):
self.__speed = speed
self.__pressed_keys: Set[str] = set()
+ self.__pressed_mouse_buttons: Set[str] = set()
self.__lock = asyncio.Lock()
self.__queue: multiprocessing.queues.Queue = multiprocessing.Queue()
@@ -56,14 +67,6 @@ class Hid(multiprocessing.Process):
# TODO: add reset or power switching
- def get_state(self) -> Dict:
- return {
- "features": {
- "keyboard": True, # Always
- "mouse": False, # TODO
- },
- }
-
async def send_key_event(self, key: str, state: bool) -> None:
if not self.__stop_event.is_set():
async with self.__lock:
@@ -74,6 +77,26 @@ class Hid(multiprocessing.Process):
self.__pressed_keys.remove(key)
self.__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))
+
+ 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))
+
+ 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))
+
async def clear_events(self) -> None:
if not self.__stop_event.is_set():
async with self.__lock:
@@ -87,10 +110,13 @@ class Hid(multiprocessing.Process):
self.__stop_event.set()
self.join()
else:
- get_logger().warning("Emergency cleaning up keyboard events ...")
+ get_logger().warning("Emergency cleaning up HID events ...")
self.__emergency_clear_events()
def __unsafe_clear_events(self) -> None:
+ for button in self.__pressed_mouse_buttons:
+ self.__queue.put(_MouseButtonEvent(button, False))
+ self.__pressed_mouse_buttons.clear()
for key in self.__pressed_keys:
self.__queue.put(_KeyEvent(key, False))
self.__pressed_keys.clear()
@@ -100,7 +126,7 @@ class Hid(multiprocessing.Process):
with serial.Serial(self.__device_path, self.__speed) as tty:
self.__send_clear_hid(tty)
except Exception:
- get_logger().exception("Can't execute emergency clear events")
+ get_logger().exception("Can't execute emergency clear HID events")
def run(self) -> None:
try:
@@ -111,7 +137,14 @@ class Hid(multiprocessing.Process):
except queue.Empty:
pass
else:
- self.__send_key_event(tty, event)
+ 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)
if self.__stop_event.is_set() and self.__queue.qsize() == 0:
break
except Exception:
@@ -119,8 +152,9 @@ class Hid(multiprocessing.Process):
raise
def __send_key_event(self, tty: serial.Serial, event: _KeyEvent) -> None:
- key_bytes = _keymap(event.key)
- if key_bytes:
+ code = _KEYMAP.get(event.key)
+ if code:
+ key_bytes = bytes([code])
assert len(key_bytes) == 1, (event, key_bytes)
tty.write(
b"\01"
@@ -129,5 +163,24 @@ class Hid(multiprocessing.Process):
+ 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")
diff --git a/kvmd/kvmd/server.py b/kvmd/kvmd/server.py
index 3b116d7c..c204b7d9 100644
--- a/kvmd/kvmd/server.py
+++ b/kvmd/kvmd/server.py
@@ -137,8 +137,6 @@ class Server: # pylint: disable=too-many-instance-attributes
app.router.add_get("/ws", self.__ws_handler)
- app.router.add_get("/hid", self.__hid_state_handler)
-
app.router.add_get("/atx", self.__atx_state_handler)
app.router.add_post("/atx/click", self.__atx_click_handler)
@@ -161,9 +159,13 @@ class Server: # pylint: disable=too-many-instance-attributes
aiohttp.web.run_app(app, host=host, port=port, print=self.__run_app_print)
+ # ===== INFO
+
async def __info_handler(self, _: aiohttp.web.Request) -> aiohttp.web.WebSocketResponse:
return _json(_get_system_info())
+ # ===== WEBSOCKET
+
async def __ws_handler(self, request: aiohttp.web.Request) -> aiohttp.web.WebSocketResponse:
logger = get_logger(0)
ws = aiohttp.web.WebSocketResponse(heartbeat=self.__heartbeat)
@@ -176,23 +178,51 @@ class Server: # pylint: disable=too-many-instance-attributes
except Exception as err:
logger.error("Can't parse JSON event from websocket: %s", err)
else:
- if event.get("event_type") == "ping":
+ event_type = event.get("event_type")
+ if event_type == "ping":
await ws.send_str(json.dumps({"msg_type": "pong"}))
- elif event.get("event_type") == "key":
- key = str(event.get("key", ""))[:64].strip()
- state = event.get("state")
- if key and state in [True, False]:
- await self.__hid.send_key_event(key, state)
- elif event.get("event_type") in ["mouse_move", "mouse_button", "mouse_wheel"]:
- pass
+ elif event_type == "key":
+ await self.__handle_ws_key_event(event)
+ elif event_type == "mouse_move":
+ await self.__handle_ws_mouse_move_event(event)
+ elif event_type == "mouse_button":
+ await self.__handle_ws_mouse_button_event(event)
+ elif event_type == "mouse_wheel":
+ await self.__handle_ws_mouse_wheel_event(event)
else:
- logger.error("Invalid websocket event: %r", event)
+ logger.error("Unknown websocket event: %r", event)
else:
break
return ws
- async def __hid_state_handler(self, _: aiohttp.web.Request) -> aiohttp.web.Response:
- return _json(self.__hid.get_state())
+ async def __handle_ws_key_event(self, event: Dict) -> None:
+ key = str(event.get("key", ""))[:64].strip()
+ state = event.get("state")
+ if key and state in [True, False]:
+ await self.__hid.send_key_event(key, state)
+
+ async def __handle_ws_mouse_move_event(self, event: Dict) -> None:
+ try:
+ to_x = int(event["to"]["x"])
+ to_y = int(event["to"]["y"])
+ except Exception:
+ return
+ await self.__hid.send_mouse_move_event(to_x, to_y)
+
+ async def __handle_ws_mouse_button_event(self, event: Dict) -> None:
+ button = str(event.get("button", ""))[:64].strip()
+ state = event.get("state")
+ if button and state in [True, False]:
+ await self.__hid.send_mouse_button_event(button, state)
+
+ async def __handle_ws_mouse_wheel_event(self, event: Dict) -> None:
+ try:
+ delta_y = int(event["delta"]["y"])
+ except Exception:
+ return
+ await self.__hid.send_mouse_wheel_event(delta_y)
+
+ # ===== ATX
async def __atx_state_handler(self, _: aiohttp.web.Request) -> aiohttp.web.Response:
return _json(self.__atx.get_state())
@@ -212,6 +242,8 @@ class Server: # pylint: disable=too-many-instance-attributes
await self.__broadcast_event("atx_click", button=None) # type: ignore
return _json({"clicked": button})
+ # ===== MSD
+
async def __msd_state_handler(self, _: aiohttp.web.Request) -> aiohttp.web.Response:
return _json(self.__msd.get_state())
@@ -261,6 +293,8 @@ class Server: # pylint: disable=too-many-instance-attributes
logger.info("Written %d bytes to mass-storage device", written)
return _json({"written": written})
+ # ===== STREAMER
+
async def __streamer_state_handler(self, _: aiohttp.web.Request) -> aiohttp.web.Response:
return _json(self.__streamer.get_state())
@@ -268,6 +302,8 @@ class Server: # pylint: disable=too-many-instance-attributes
self.__reset_streamer = True
return _json()
+ # =====
+
def __run_app_print(self, text: str) -> None:
logger = get_logger()
for line in text.strip().splitlines():
diff --git a/kvmd/web/css/stream.css b/kvmd/web/css/stream.css
index 96b19810..fa892545 100644
--- a/kvmd/web/css/stream.css
+++ b/kvmd/web/css/stream.css
@@ -2,7 +2,6 @@ img#stream-image {
width: 640px;
height: 480px;
display: inline-block;
- border: var(--dark-border);
background-color: var(--bg-color-stream-screen);
}
@@ -19,9 +18,7 @@ img.stream-image-inactive {
div#stream-box {
position: relative;
display: inline-block;
-}
-div.stream-box-active: {
- cursor: crosshair;
+ border: var(--dark-border);
}
div.stream-box-inactive::after {
cursor: wait;
@@ -34,3 +31,6 @@ div.stream-box-inactive::after {
display: inline-block;
background: radial-gradient(transparent 20%, black);
}
+div.stream-box-mouse-enabled {
+ cursor: url("../svg/stream-mouse-cursor.svg"), pointer;
+}
diff --git a/kvmd/web/js/hid.js b/kvmd/web/js/hid.js
index a8a5d653..cb6c37bf 100644
--- a/kvmd/web/js/hid.js
+++ b/kvmd/web/js/hid.js
@@ -1,10 +1,4 @@
var hid = new function() {
- var __install_timer = null;
- var __installed = false;
-
- var __hidden_attr = null;
- var __visibility_change_attr = null;
-
this.init = function() {
keyboard.init();
mouse.init();
@@ -32,32 +26,12 @@ var hid = new function() {
};
this.installCapture = function(ws) {
- var http = tools.makeRequest("GET", "/kvmd/hid", function() {
- if (http.readyState === 4) {
- if (http.status === 200) {
- features = JSON.parse(http.responseText).result.features;
- if (features.mouse) {
- mouse.setSocket(ws);
- }
- keyboard.setSocket(ws);
- __installed = true;
- } else {
- tools.error("Can't resolve HID features:", http.responseText);
- __install_timer = setTimeout(() => hid.installCapture(ws), 1000);
- }
- }
- });
+ keyboard.setSocket(ws);
+ mouse.setSocket(ws);
};
this.clearCapture = function() {
- if (__install_timer) {
- clearTimeout(__install_timer);
- __install_timer = null;
- }
- if (__installed) {
- mouse.setSocket(null);
- keyboard.setSocket(null);
- __installed = false;
- }
+ mouse.setSocket(null);
+ keyboard.setSocket(null);
};
}
diff --git a/kvmd/web/js/keyboard.js b/kvmd/web/js/keyboard.js
index 9d3f65e6..3618f447 100644
--- a/kvmd/web/js/keyboard.js
+++ b/kvmd/web/js/keyboard.js
@@ -27,8 +27,10 @@ var keyboard = new function() {
};
this.setSocket = function(ws) {
- keyboard.releaseAll();
- __ws = ws;
+ if (ws !== __ws) {
+ keyboard.releaseAll();
+ __ws = ws;
+ }
keyboard.updateLeds();
};
diff --git a/kvmd/web/js/mouse.js b/kvmd/web/js/mouse.js
index b9d27e67..ba4eb264 100644
--- a/kvmd/web/js/mouse.js
+++ b/kvmd/web/js/mouse.js
@@ -2,9 +2,12 @@ var mouse = new function() {
var __ws = null;
var __current_pos = {x: 0, y:0};
var __sent_pos = {x: 0, y:0};
+ var __stream_hovered = false;
this.init = function() {
el_stream_box = $("stream-box");
+ el_stream_box.onmouseenter = __hoverStream;
+ el_stream_box.onmouseleave = __leaveStream;
el_stream_box.onmousedown = (event) => __buttonHandler(event, true);
el_stream_box.onmouseup = (event) => __buttonHandler(event, false);
el_stream_box.oncontextmenu = (event) => event.preventDefault();
@@ -15,11 +18,25 @@ var mouse = new function() {
this.setSocket = function(ws) {
__ws = ws;
+ if (ws) {
+ $("stream-box").classList.add("stream-box-mouse-enabled");
+ } else {
+ $("stream-box").classList.remove("stream-box-mouse-enabled");
+ }
+ };
+
+ var __hoverStream = function() {
+ __stream_hovered = true;
+ mouse.updateLeds();
+ };
+
+ var __leaveStream = function() {
+ __stream_hovered = false;
+ mouse.updateLeds();
};
this.updateLeds = function() {
- var focused = (__ws && document.activeElement === $("stream-window"));
- $("hid-mouse-led").className = (focused ? "led-on" : "led-off");
+ $("hid-mouse-led").className = (__ws && __stream_hovered ? "led-on" : "led-off");
};
var __buttonHandler = function(event, state) {
@@ -56,15 +73,23 @@ var mouse = new function() {
if (pos.x !== __sent_pos.x || pos.y !== __sent_pos.y) {
tools.debug("Mouse move:", pos);
if (__ws) {
+ el_stream_image = $("stream-image");
__ws.send(JSON.stringify({
event_type: "mouse_move",
- to: pos,
+ to: {
+ x: __translate(pos.x, 0, el_stream_image.clientWidth, -32768, 32767),
+ y: __translate(pos.y, 0, el_stream_image.clientHeight, -32768, 32767),
+ },
}));
}
__sent_pos = pos;
}
};
+ var __translate = function(x, a, b, c, d) {
+ return Math.round((x - a) / (b - a) * (d - c) + c);
+ };
+
var __wheelHandler = function(event) {
// https://learn.javascript.ru/mousewheel
if (event.preventDefault) {
diff --git a/kvmd/web/js/stream.js b/kvmd/web/js/stream.js
index 51e00a96..e7bd82d4 100644
--- a/kvmd/web/js/stream.js
+++ b/kvmd/web/js/stream.js
@@ -11,14 +11,14 @@ var stream = new function() {
tools.info("Refreshing stream ...");
__prev_state = false;
$("stream-image").className = "stream-image-inactive";
- $("stream-box").className = "stream-box-inactive";
+ $("stream-box").classList.add("stream-box-inactive");
$("stream-led").className = "led-off";
$("stream-reset-button").disabled = true;
} else if (!__prev_state) {
__refreshImage();
__prev_state = true;
$("stream-image").className = "stream-image-active";
- $("stream-box").className = "stream-box-active";
+ $("stream-box").classList.remove("stream-box-inactive");
$("stream-led").className = "led-on";
$("stream-reset-button").disabled = false;
}
diff --git a/kvmd/web/svg/stream-mouse-cursor.svg b/kvmd/web/svg/stream-mouse-cursor.svg
new file mode 100644
index 00000000..ff852ef6
--- /dev/null
+++ b/kvmd/web/svg/stream-mouse-cursor.svg
@@ -0,0 +1,68 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="2.6458335mm"
+ height="2.6458335mm"
+ viewBox="0 0 2.6458335 2.6458335"
+ version="1.1"
+ id="svg8"
+ sodipodi:docname="stream-mouse-cursor.svg"
+ inkscape:version="0.92.2 2405546, 2018-03-11">
+ <defs
+ id="defs2" />
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="31.678384"
+ inkscape:cx="13.114187"
+ inkscape:cy="8.129091"
+ inkscape:document-units="mm"
+ inkscape:current-layer="layer1"
+ showgrid="false"
+ units="px"
+ inkscape:window-width="1920"
+ inkscape:window-height="1020"
+ inkscape:window-x="0"
+ inkscape:window-y="30"
+ inkscape:window-maximized="1"
+ fit-margin-top="0"
+ fit-margin-left="0"
+ fit-margin-right="0"
+ fit-margin-bottom="0" />
+ <metadata
+ id="metadata5">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(-102.90015,-148.41315)">
+ <circle
+ style="opacity:1;fill:#5b90bb;fill-opacity:1;stroke:#e8e8e8;stroke-width:0.26458332;stroke-linecap:square;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:75.59055328;stroke-opacity:0.80303034;paint-order:normal"
+ id="path4915"
+ cx="104.22307"
+ cy="149.73607"
+ r="1.1906251" />
+ </g>
+</svg>