mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2025-12-13 01:30:31 +08:00
new hid protocol with crc
This commit is contained in:
parent
5bec2ff144
commit
de1bed956c
@ -19,9 +19,16 @@ kvmd:
|
|||||||
pinout:
|
pinout:
|
||||||
reset: 4
|
reset: 4
|
||||||
|
|
||||||
|
reset_delay: 0.1
|
||||||
|
|
||||||
device: "/dev/kvmd-hid"
|
device: "/dev/kvmd-hid"
|
||||||
speed: 115200
|
speed: 115200
|
||||||
reset_delay: 0.1
|
read_timeout: 2.0
|
||||||
|
read_retries: 10
|
||||||
|
common_retries: 100
|
||||||
|
retries_delay: 0.1
|
||||||
|
|
||||||
|
state_poll: 1.0
|
||||||
|
|
||||||
atx:
|
atx:
|
||||||
pinout:
|
pinout:
|
||||||
|
|||||||
@ -19,9 +19,16 @@ kvmd:
|
|||||||
pinout:
|
pinout:
|
||||||
reset: 4
|
reset: 4
|
||||||
|
|
||||||
|
reset_delay: 0.1
|
||||||
|
|
||||||
device: "/dev/kvmd-hid"
|
device: "/dev/kvmd-hid"
|
||||||
speed: 115200
|
speed: 115200
|
||||||
reset_delay: 0.1
|
read_timeout: 2.0
|
||||||
|
read_retries: 10
|
||||||
|
common_retries: 100
|
||||||
|
retries_delay: 0.1
|
||||||
|
|
||||||
|
state_poll: 1.0
|
||||||
|
|
||||||
atx:
|
atx:
|
||||||
pinout:
|
pinout:
|
||||||
|
|||||||
@ -17,3 +17,4 @@ monitor_speed = 115200
|
|||||||
|
|
||||||
lib_deps =
|
lib_deps =
|
||||||
HID-Project@2.4.4
|
HID-Project@2.4.4
|
||||||
|
TimerOne@1.1
|
||||||
|
|||||||
199
hid/src/main.cpp
199
hid/src/main.cpp
@ -1,5 +1,6 @@
|
|||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
#include <HID-Project.h>
|
#include <HID-Project.h>
|
||||||
|
#include <TimerOne.h>
|
||||||
|
|
||||||
#include "inline.h"
|
#include "inline.h"
|
||||||
#include "keymap.h"
|
#include "keymap.h"
|
||||||
@ -7,35 +8,42 @@
|
|||||||
|
|
||||||
#define CMD_SERIAL Serial1
|
#define CMD_SERIAL Serial1
|
||||||
#define CMD_SERIAL_SPEED 115200
|
#define CMD_SERIAL_SPEED 115200
|
||||||
|
#define CMD_RECV_TIMEOUT 100000
|
||||||
|
|
||||||
#define CMD_MOUSE_LEFT 0b10000000
|
#define PROTO_MAGIC 0x33
|
||||||
#define CMD_MOUSE_LEFT_STATE 0b00001000
|
#define PROTO_CRC_POLINOM 0xA001
|
||||||
#define CMD_MOUSE_RIGHT 0b01000000
|
// -----------------------------------------
|
||||||
#define CMD_MOUSE_RIGHT_STATE 0b00000100
|
#define PROTO_RESP_OK 0x20
|
||||||
|
#define PROTO_RESP_NONE 0x24
|
||||||
#define REPORT_INTERVAL 100
|
#define PROTO_RESP_CRC_ERROR 0x40
|
||||||
|
#define PROTO_RESP_INVALID_ERROR 0x45
|
||||||
|
#define PROTO_RESP_TIMEOUT_ERROR 0x48
|
||||||
|
// -----------------------------------------
|
||||||
|
#define PROTO_CMD_PING 0x01
|
||||||
|
#define PROTO_CMD_REPEAT 0x02
|
||||||
|
#define PROTO_CMD_RESET_HID 0x10
|
||||||
|
#define PROTO_CMD_KEY_EVENT 0x11
|
||||||
|
#define PROTO_CMD_MOUSE_MOVE_EVENT 0x12
|
||||||
|
#define PROTO_CMD_MOUSE_BUTTON_EVENT 0x13
|
||||||
|
#define PROTO_CMD_MOUSE_WHEEL_EVENT 0x14
|
||||||
|
// -----------------------------------------
|
||||||
|
#define PROTO_CMD_MOUSE_BUTTON_LEFT_SELECT 0b10000000
|
||||||
|
#define PROTO_CMD_MOUSE_BUTTON_LEFT_STATE 0b00001000
|
||||||
|
#define PROTO_CMD_MOUSE_BUTTON_RIGHT_SELECT 0b01000000
|
||||||
|
#define PROTO_CMD_MOUSE_BUTTON_RIGHT_STATE 0b00000100
|
||||||
|
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
INLINE void readNoop() {
|
INLINE void cmdResetHid(const uint8_t *buffer) { // 0 bytes
|
||||||
for (int count = 0; count < 4; ++count) {
|
|
||||||
CMD_SERIAL.read();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
INLINE void cmdResetHid() { // 0 bytes
|
|
||||||
readNoop();
|
|
||||||
BootKeyboard.releaseAll();
|
BootKeyboard.releaseAll();
|
||||||
SingleAbsoluteMouse.releaseAll();
|
SingleAbsoluteMouse.releaseAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
INLINE void cmdKeyEvent() { // 2 bytes
|
INLINE void cmdKeyEvent(const uint8_t *buffer) { // 2 bytes
|
||||||
KeyboardKeycode code = keymap((uint8_t)CMD_SERIAL.read());
|
KeyboardKeycode code = keymap(buffer[0]);
|
||||||
uint8_t state = CMD_SERIAL.read();
|
|
||||||
CMD_SERIAL.read(); // unused
|
|
||||||
CMD_SERIAL.read(); // unused
|
|
||||||
if (code != KEY_ERROR_UNDEFINED) {
|
if (code != KEY_ERROR_UNDEFINED) {
|
||||||
if (state) {
|
if (buffer[1]) {
|
||||||
BootKeyboard.press(code);
|
BootKeyboard.press(code);
|
||||||
} else {
|
} else {
|
||||||
BootKeyboard.release(code);
|
BootKeyboard.release(code);
|
||||||
@ -43,28 +51,29 @@ INLINE void cmdKeyEvent() { // 2 bytes
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
INLINE void cmdMouseMoveEvent() { // 4 bytes
|
INLINE void cmdMouseMoveEvent(const uint8_t *buffer) { // 4 bytes
|
||||||
int x = (int)CMD_SERIAL.read() << 8;
|
int x = (int)buffer[0] << 8;
|
||||||
x |= (int)CMD_SERIAL.read();
|
x |= (int)buffer[1];
|
||||||
int y = (int)CMD_SERIAL.read() << 8;
|
|
||||||
y |= (int)CMD_SERIAL.read();
|
int y = (int)buffer[2] << 8;
|
||||||
|
y |= (int)buffer[3];
|
||||||
|
|
||||||
SingleAbsoluteMouse.moveTo(x, y);
|
SingleAbsoluteMouse.moveTo(x, y);
|
||||||
}
|
}
|
||||||
|
|
||||||
INLINE void cmdMouseButtonEvent() { // 1 byte
|
INLINE void cmdMouseButtonEvent(const uint8_t *buffer) { // 1 byte
|
||||||
uint8_t state = CMD_SERIAL.read();
|
uint8_t state = buffer[0];
|
||||||
CMD_SERIAL.read(); // unused
|
|
||||||
CMD_SERIAL.read(); // unused
|
if (state & PROTO_CMD_MOUSE_BUTTON_LEFT_SELECT) {
|
||||||
CMD_SERIAL.read(); // unused
|
if (state & PROTO_CMD_MOUSE_BUTTON_LEFT_STATE) {
|
||||||
if (state & CMD_MOUSE_LEFT) {
|
|
||||||
if (state & CMD_MOUSE_LEFT_STATE) {
|
|
||||||
SingleAbsoluteMouse.press(MOUSE_LEFT);
|
SingleAbsoluteMouse.press(MOUSE_LEFT);
|
||||||
} else {
|
} else {
|
||||||
SingleAbsoluteMouse.release(MOUSE_LEFT);
|
SingleAbsoluteMouse.release(MOUSE_LEFT);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (state & CMD_MOUSE_RIGHT) {
|
|
||||||
if (state & CMD_MOUSE_RIGHT_STATE) {
|
if (state & PROTO_CMD_MOUSE_BUTTON_RIGHT_SELECT) {
|
||||||
|
if (state & PROTO_CMD_MOUSE_BUTTON_RIGHT_STATE) {
|
||||||
SingleAbsoluteMouse.press(MOUSE_RIGHT);
|
SingleAbsoluteMouse.press(MOUSE_RIGHT);
|
||||||
} else {
|
} else {
|
||||||
SingleAbsoluteMouse.release(MOUSE_RIGHT);
|
SingleAbsoluteMouse.release(MOUSE_RIGHT);
|
||||||
@ -72,45 +81,113 @@ INLINE void cmdMouseButtonEvent() { // 1 byte
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
INLINE void cmdMouseWheelEvent() { // 2 bytes
|
INLINE void cmdMouseWheelEvent(const uint8_t *buffer) { // 2 bytes
|
||||||
CMD_SERIAL.read(); // delta_x is not supported by hid-project now
|
// delta_x is not supported by hid-project now
|
||||||
signed char delta_y = CMD_SERIAL.read();
|
signed char delta_y = buffer[1];
|
||||||
CMD_SERIAL.read(); // unused
|
|
||||||
CMD_SERIAL.read(); // unused
|
|
||||||
SingleAbsoluteMouse.move(0, 0, delta_y);
|
SingleAbsoluteMouse.move(0, 0, delta_y);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
|
INLINE uint16_t makeCrc16(const uint8_t *buffer, const unsigned length) {
|
||||||
|
uint16_t crc = 0xFFFF;
|
||||||
|
|
||||||
|
for (unsigned byte_count = 0; byte_count < length; ++byte_count) {
|
||||||
|
crc = crc ^ buffer[byte_count];
|
||||||
|
for (unsigned bit_count = 0; bit_count < 8; ++bit_count) {
|
||||||
|
if ((crc & 0x0001) == 0) {
|
||||||
|
crc = crc >> 1;
|
||||||
|
} else {
|
||||||
|
crc = crc >> 1;
|
||||||
|
crc = crc ^ PROTO_CRC_POLINOM;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return crc;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
volatile bool cmd_recv_timed_out = false;
|
||||||
|
|
||||||
|
INLINE void recvTimerStop(bool flag) {
|
||||||
|
Timer1.stop();
|
||||||
|
cmd_recv_timed_out = flag;
|
||||||
|
}
|
||||||
|
|
||||||
|
INLINE void resetCmdRecvTimeout() {
|
||||||
|
recvTimerStop(false);
|
||||||
|
Timer1.initialize(CMD_RECV_TIMEOUT);
|
||||||
|
}
|
||||||
|
|
||||||
|
INLINE void sendCmdResponse(uint8_t code=0) {
|
||||||
|
static uint8_t prev_code = PROTO_RESP_NONE;
|
||||||
|
if (code == 0) {
|
||||||
|
code = prev_code; // Repeat the last code
|
||||||
|
} else {
|
||||||
|
prev_code = code;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t buffer[4];
|
||||||
|
buffer[0] = PROTO_MAGIC;
|
||||||
|
buffer[1] = code;
|
||||||
|
uint16_t crc = makeCrc16(buffer, 2);
|
||||||
|
buffer[2] = (uint8_t)(crc >> 8);
|
||||||
|
buffer[3] = (uint8_t)(crc & 0xFF);
|
||||||
|
|
||||||
|
recvTimerStop(false);
|
||||||
|
CMD_SERIAL.write(buffer, 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
void intRecvTimedOut() {
|
||||||
|
recvTimerStop(true);
|
||||||
|
}
|
||||||
|
|
||||||
void setup() {
|
void setup() {
|
||||||
CMD_SERIAL.begin(CMD_SERIAL_SPEED);
|
|
||||||
BootKeyboard.begin();
|
BootKeyboard.begin();
|
||||||
SingleAbsoluteMouse.begin();
|
SingleAbsoluteMouse.begin();
|
||||||
|
|
||||||
|
Timer1.attachInterrupt(intRecvTimedOut);
|
||||||
|
CMD_SERIAL.begin(CMD_SERIAL_SPEED);
|
||||||
}
|
}
|
||||||
|
|
||||||
void loop() {
|
void loop() {
|
||||||
static unsigned long last_report = 0;
|
uint8_t buffer[8];
|
||||||
bool cmd_processed = false;
|
unsigned index = 0;
|
||||||
|
|
||||||
if (CMD_SERIAL.available() >= 5) {
|
while (true) {
|
||||||
switch ((uint8_t)CMD_SERIAL.read()) {
|
if (CMD_SERIAL.available() > 0) {
|
||||||
case 0: cmdResetHid(); break;
|
buffer[index] = (uint8_t)CMD_SERIAL.read();
|
||||||
case 1: cmdKeyEvent(); break;
|
if (index == 7) {
|
||||||
case 2: cmdMouseMoveEvent(); break;
|
uint16_t crc = (uint16_t)buffer[6] << 8;
|
||||||
case 3: cmdMouseButtonEvent(); break;
|
crc |= (uint16_t)buffer[7];
|
||||||
case 4: cmdMouseWheelEvent(); break;
|
|
||||||
default: readNoop(); break;
|
|
||||||
}
|
|
||||||
cmd_processed = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
unsigned long now = millis();
|
if (makeCrc16(buffer, 6) == crc) {
|
||||||
if (
|
# define HANDLE(_handler) { _handler(buffer + 2); sendCmdResponse(PROTO_RESP_OK); break; }
|
||||||
cmd_processed
|
switch (buffer[1]) {
|
||||||
|| (now >= last_report && now - last_report >= REPORT_INTERVAL)
|
case PROTO_CMD_RESET_HID: HANDLE(cmdResetHid);
|
||||||
|| (now < last_report && ((unsigned long) -1) - last_report + now >= REPORT_INTERVAL)
|
case PROTO_CMD_KEY_EVENT: HANDLE(cmdKeyEvent);
|
||||||
) {
|
case PROTO_CMD_MOUSE_MOVE_EVENT: HANDLE(cmdMouseMoveEvent);
|
||||||
CMD_SERIAL.write(0);
|
case PROTO_CMD_MOUSE_BUTTON_EVENT: HANDLE(cmdMouseButtonEvent);
|
||||||
last_report = now;
|
case PROTO_CMD_MOUSE_WHEEL_EVENT: HANDLE(cmdMouseWheelEvent);
|
||||||
|
|
||||||
|
case PROTO_CMD_PING: sendCmdResponse(PROTO_RESP_OK); break;
|
||||||
|
case PROTO_CMD_REPEAT: sendCmdResponse(); break;
|
||||||
|
default: sendCmdResponse(PROTO_RESP_INVALID_ERROR); break;
|
||||||
|
}
|
||||||
|
# undef HANDLE
|
||||||
|
} else {
|
||||||
|
sendCmdResponse(PROTO_RESP_CRC_ERROR);
|
||||||
|
}
|
||||||
|
index = 0;
|
||||||
|
} else {
|
||||||
|
resetCmdRecvTimeout();
|
||||||
|
index += 1;
|
||||||
|
}
|
||||||
|
} else if (index > 0 && cmd_recv_timed_out) {
|
||||||
|
sendCmdResponse(PROTO_RESP_TIMEOUT_ERROR);
|
||||||
|
index = 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -35,9 +35,18 @@ def main() -> None:
|
|||||||
|
|
||||||
hid = Hid(
|
hid = Hid(
|
||||||
reset=int(config["hid"]["pinout"]["reset"]),
|
reset=int(config["hid"]["pinout"]["reset"]),
|
||||||
|
|
||||||
|
reset_delay=float(config["hid"]["reset_delay"]),
|
||||||
|
|
||||||
device_path=str(config["hid"]["device"]),
|
device_path=str(config["hid"]["device"]),
|
||||||
speed=int(config["hid"]["speed"]),
|
speed=int(config["hid"]["speed"]),
|
||||||
reset_delay=float(config["hid"]["reset_delay"]),
|
read_timeout=float(config["hid"]["read_timeout"]),
|
||||||
|
read_retries=int(config["hid"]["read_retries"]),
|
||||||
|
common_retries=int(config["hid"]["common_retries"]),
|
||||||
|
retries_delay=float(config["hid"]["retries_delay"]),
|
||||||
|
noop=bool(config["hid"].get("noop", False)),
|
||||||
|
|
||||||
|
state_poll=float(config["hid"]["state_poll"]),
|
||||||
)
|
)
|
||||||
|
|
||||||
atx = Atx(
|
atx = Atx(
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import time
|
|||||||
from typing import Dict
|
from typing import Dict
|
||||||
from typing import Set
|
from typing import Set
|
||||||
from typing import NamedTuple
|
from typing import NamedTuple
|
||||||
|
from typing import AsyncGenerator
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
import serial
|
import serial
|
||||||
@ -33,41 +34,92 @@ class _KeyEvent(NamedTuple):
|
|||||||
key: str
|
key: str
|
||||||
state: bool
|
state: bool
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def is_valid(key: str) -> bool:
|
||||||
|
return (key in _KEYMAP)
|
||||||
|
|
||||||
|
def make_command(self) -> bytes:
|
||||||
|
code = _KEYMAP[self.key]
|
||||||
|
key_bytes = bytes([code])
|
||||||
|
assert len(key_bytes) == 1, (self, key_bytes, code)
|
||||||
|
state_bytes = (b"\x01" if self.state else b"\x00")
|
||||||
|
return b"\x11" + key_bytes + state_bytes + b"\x00\x00"
|
||||||
|
|
||||||
|
|
||||||
class _MouseMoveEvent(NamedTuple):
|
class _MouseMoveEvent(NamedTuple):
|
||||||
to_x: int
|
to_x: int
|
||||||
to_y: int
|
to_y: int
|
||||||
|
|
||||||
|
def make_command(self) -> bytes:
|
||||||
|
to_x = min(max(-32768, self.to_x), 32767)
|
||||||
|
to_y = min(max(-32768, self.to_y), 32767)
|
||||||
|
return b"\x12" + struct.pack(">hh", to_x, to_y)
|
||||||
|
|
||||||
|
|
||||||
class _MouseButtonEvent(NamedTuple):
|
class _MouseButtonEvent(NamedTuple):
|
||||||
button: str
|
button: str
|
||||||
state: bool
|
state: bool
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def is_valid(button: str) -> bool:
|
||||||
|
return (button in ["left", "right"])
|
||||||
|
|
||||||
|
def make_command(self) -> bytes:
|
||||||
|
code = 0
|
||||||
|
if self.button == "left":
|
||||||
|
code = (0b10000000 | (0b00001000 if self.state else 0))
|
||||||
|
elif self.button == "right":
|
||||||
|
code = (0b01000000 | (0b00000100 if self.state else 0))
|
||||||
|
assert code, self
|
||||||
|
return b"\x13" + bytes([code]) + b"\x00\x00\x00"
|
||||||
|
|
||||||
|
|
||||||
class _MouseWheelEvent(NamedTuple):
|
class _MouseWheelEvent(NamedTuple):
|
||||||
delta_y: int
|
delta_y: int
|
||||||
|
|
||||||
|
def make_command(self) -> bytes:
|
||||||
|
delta_y = min(max(-128, self.delta_y), 127)
|
||||||
|
return b"\x14\x00" + struct.pack(">b", delta_y) + b"\x00\x00"
|
||||||
|
|
||||||
|
|
||||||
class Hid(multiprocessing.Process): # pylint: disable=too-many-instance-attributes
|
class Hid(multiprocessing.Process): # pylint: disable=too-many-instance-attributes
|
||||||
def __init__(
|
def __init__( # pylint: disable=too-many-arguments
|
||||||
self,
|
self,
|
||||||
reset: int,
|
reset: int,
|
||||||
|
reset_delay: float,
|
||||||
|
|
||||||
device_path: str,
|
device_path: str,
|
||||||
speed: int,
|
speed: int,
|
||||||
reset_delay: float,
|
read_timeout: float,
|
||||||
|
read_retries: int,
|
||||||
|
common_retries: int,
|
||||||
|
retries_delay: float,
|
||||||
|
noop: bool,
|
||||||
|
|
||||||
|
state_poll: float,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
||||||
super().__init__(daemon=True)
|
super().__init__(daemon=True)
|
||||||
|
|
||||||
self.__reset = gpio.set_output(reset)
|
self.__reset = gpio.set_output(reset)
|
||||||
|
self.__reset_delay = reset_delay
|
||||||
|
|
||||||
self.__device_path = device_path
|
self.__device_path = device_path
|
||||||
self.__speed = speed
|
self.__speed = speed
|
||||||
self.__reset_delay = reset_delay
|
self.__read_timeout = read_timeout
|
||||||
|
self.__read_retries = read_retries
|
||||||
|
self.__common_retries = common_retries
|
||||||
|
self.__retries_delay = retries_delay
|
||||||
|
self.__noop = noop
|
||||||
|
|
||||||
|
self.__state_poll = state_poll
|
||||||
|
|
||||||
self.__pressed_keys: Set[str] = set()
|
self.__pressed_keys: Set[str] = set()
|
||||||
self.__pressed_mouse_buttons: Set[str] = set()
|
self.__pressed_mouse_buttons: Set[str] = set()
|
||||||
self.__lock = asyncio.Lock()
|
self.__lock = asyncio.Lock()
|
||||||
self.__queue: multiprocessing.queues.Queue = multiprocessing.Queue()
|
self.__events_queue: multiprocessing.queues.Queue = multiprocessing.Queue()
|
||||||
|
|
||||||
|
self.__ok_shared = multiprocessing.Value("i", 1)
|
||||||
|
|
||||||
self.__stop_event = multiprocessing.Event()
|
self.__stop_event = multiprocessing.Event()
|
||||||
|
|
||||||
@ -75,6 +127,14 @@ class Hid(multiprocessing.Process): # pylint: disable=too-many-instance-attribu
|
|||||||
get_logger().info("Starting HID daemon ...")
|
get_logger().info("Starting HID daemon ...")
|
||||||
super().start()
|
super().start()
|
||||||
|
|
||||||
|
def get_state(self) -> Dict:
|
||||||
|
return {"ok": bool(self.__ok_shared.value)}
|
||||||
|
|
||||||
|
async def poll_state(self) -> AsyncGenerator[Dict, None]:
|
||||||
|
while self.is_alive():
|
||||||
|
yield self.get_state()
|
||||||
|
await asyncio.sleep(self.__state_poll)
|
||||||
|
|
||||||
async def reset(self) -> None:
|
async def reset(self) -> None:
|
||||||
async with self.__lock:
|
async with self.__lock:
|
||||||
gpio.write(self.__reset, True)
|
gpio.write(self.__reset, True)
|
||||||
@ -84,32 +144,34 @@ class Hid(multiprocessing.Process): # pylint: disable=too-many-instance-attribu
|
|||||||
async def send_key_event(self, key: str, state: bool) -> None:
|
async def send_key_event(self, key: str, state: bool) -> None:
|
||||||
if not self.__stop_event.is_set():
|
if not self.__stop_event.is_set():
|
||||||
async with self.__lock:
|
async with self.__lock:
|
||||||
|
if _KeyEvent.is_valid(key):
|
||||||
if state and key not in self.__pressed_keys:
|
if state and key not in self.__pressed_keys:
|
||||||
self.__pressed_keys.add(key)
|
self.__pressed_keys.add(key)
|
||||||
self.__queue.put(_KeyEvent(key, state))
|
self.__events_queue.put(_KeyEvent(key, state))
|
||||||
elif not state and key in self.__pressed_keys:
|
elif not state and key in self.__pressed_keys:
|
||||||
self.__pressed_keys.remove(key)
|
self.__pressed_keys.remove(key)
|
||||||
self.__queue.put(_KeyEvent(key, state))
|
self.__events_queue.put(_KeyEvent(key, state))
|
||||||
|
|
||||||
async def send_mouse_move_event(self, to_x: int, to_y: int) -> None:
|
async def send_mouse_move_event(self, to_x: int, to_y: int) -> None:
|
||||||
if not self.__stop_event.is_set():
|
if not self.__stop_event.is_set():
|
||||||
async with self.__lock:
|
async with self.__lock:
|
||||||
self.__queue.put(_MouseMoveEvent(to_x, to_y))
|
self.__events_queue.put(_MouseMoveEvent(to_x, to_y))
|
||||||
|
|
||||||
async def send_mouse_button_event(self, button: str, state: bool) -> None:
|
async def send_mouse_button_event(self, button: str, state: bool) -> None:
|
||||||
if not self.__stop_event.is_set():
|
if not self.__stop_event.is_set():
|
||||||
async with self.__lock:
|
async with self.__lock:
|
||||||
|
if _MouseButtonEvent.is_valid(button):
|
||||||
if state and button not in self.__pressed_mouse_buttons:
|
if state and button not in self.__pressed_mouse_buttons:
|
||||||
self.__pressed_mouse_buttons.add(button)
|
self.__pressed_mouse_buttons.add(button)
|
||||||
self.__queue.put(_MouseButtonEvent(button, state))
|
self.__events_queue.put(_MouseButtonEvent(button, state))
|
||||||
elif not state and button in self.__pressed_mouse_buttons:
|
elif not state and button in self.__pressed_mouse_buttons:
|
||||||
self.__pressed_mouse_buttons.remove(button)
|
self.__pressed_mouse_buttons.remove(button)
|
||||||
self.__queue.put(_MouseButtonEvent(button, state))
|
self.__events_queue.put(_MouseButtonEvent(button, state))
|
||||||
|
|
||||||
async def send_mouse_wheel_event(self, delta_y: int) -> None:
|
async def send_mouse_wheel_event(self, delta_y: int) -> None:
|
||||||
if not self.__stop_event.is_set():
|
if not self.__stop_event.is_set():
|
||||||
async with self.__lock:
|
async with self.__lock:
|
||||||
self.__queue.put(_MouseWheelEvent(delta_y))
|
self.__events_queue.put(_MouseWheelEvent(delta_y))
|
||||||
|
|
||||||
async def clear_events(self) -> None:
|
async def clear_events(self) -> None:
|
||||||
if not self.__stop_event.is_set():
|
if not self.__stop_event.is_set():
|
||||||
@ -120,7 +182,7 @@ class Hid(multiprocessing.Process): # pylint: disable=too-many-instance-attribu
|
|||||||
async with self.__lock:
|
async with self.__lock:
|
||||||
if self.is_alive():
|
if self.is_alive():
|
||||||
self.__unsafe_clear_events()
|
self.__unsafe_clear_events()
|
||||||
get_logger().info("Stopping keyboard daemon ...")
|
get_logger().info("Stopping HID daemon ...")
|
||||||
self.__stop_event.set()
|
self.__stop_event.set()
|
||||||
self.join()
|
self.join()
|
||||||
else:
|
else:
|
||||||
@ -130,17 +192,17 @@ class Hid(multiprocessing.Process): # pylint: disable=too-many-instance-attribu
|
|||||||
|
|
||||||
def __unsafe_clear_events(self) -> None:
|
def __unsafe_clear_events(self) -> None:
|
||||||
for button in self.__pressed_mouse_buttons:
|
for button in self.__pressed_mouse_buttons:
|
||||||
self.__queue.put(_MouseButtonEvent(button, False))
|
self.__events_queue.put(_MouseButtonEvent(button, False))
|
||||||
self.__pressed_mouse_buttons.clear()
|
self.__pressed_mouse_buttons.clear()
|
||||||
for key in self.__pressed_keys:
|
for key in self.__pressed_keys:
|
||||||
self.__queue.put(_KeyEvent(key, False))
|
self.__events_queue.put(_KeyEvent(key, False))
|
||||||
self.__pressed_keys.clear()
|
self.__pressed_keys.clear()
|
||||||
|
|
||||||
def __emergency_clear_events(self) -> None:
|
def __emergency_clear_events(self) -> None:
|
||||||
if os.path.exists(self.__device_path):
|
if os.path.exists(self.__device_path):
|
||||||
try:
|
try:
|
||||||
with serial.Serial(self.__device_path, self.__speed) as tty:
|
with self.__get_serial() as tty:
|
||||||
self.__send_clear_hid(tty)
|
self.__process_request(tty, b"\x10\x00\x00\x00\x00")
|
||||||
except Exception:
|
except Exception:
|
||||||
get_logger().exception("Can't execute emergency clear HID events")
|
get_logger().exception("Can't execute emergency clear HID events")
|
||||||
|
|
||||||
@ -148,70 +210,102 @@ class Hid(multiprocessing.Process): # pylint: disable=too-many-instance-attribu
|
|||||||
signal.signal(signal.SIGINT, signal.SIG_IGN)
|
signal.signal(signal.SIGINT, signal.SIG_IGN)
|
||||||
setproctitle.setproctitle("[hid] " + setproctitle.getproctitle())
|
setproctitle.setproctitle("[hid] " + setproctitle.getproctitle())
|
||||||
try:
|
try:
|
||||||
with serial.Serial(self.__device_path, self.__speed) as tty:
|
with self.__get_serial() as tty:
|
||||||
hid_ready = False
|
passed = 0
|
||||||
while True:
|
while not (self.__stop_event.is_set() and self.__events_queue.qsize() == 0):
|
||||||
if hid_ready:
|
|
||||||
try:
|
try:
|
||||||
event = self.__queue.get(timeout=0.05)
|
event = self.__events_queue.get(timeout=0.05)
|
||||||
except queue.Empty:
|
except queue.Empty:
|
||||||
pass
|
if passed >= 20: # 20 * 0.05 = 1 sec
|
||||||
|
self.__process_request(tty, b"\x01\x00\x00\x00\x00") # Ping
|
||||||
|
passed = 0
|
||||||
else:
|
else:
|
||||||
if isinstance(event, _KeyEvent):
|
passed += 1
|
||||||
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)
|
|
||||||
else:
|
else:
|
||||||
raise RuntimeError("Unknown HID event")
|
self.__process_request(tty, event.make_command())
|
||||||
hid_ready = False
|
passed = 0
|
||||||
|
|
||||||
if tty.in_waiting:
|
|
||||||
while tty.in_waiting:
|
|
||||||
tty.read(tty.in_waiting)
|
|
||||||
hid_ready = True
|
|
||||||
else:
|
|
||||||
time.sleep(0.05)
|
|
||||||
|
|
||||||
if self.__stop_event.is_set() and self.__queue.qsize() == 0:
|
|
||||||
break
|
|
||||||
except Exception:
|
except Exception:
|
||||||
get_logger().exception("Unhandled exception")
|
get_logger().exception("Unhandled exception")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def __send_key_event(self, tty: serial.Serial, event: _KeyEvent) -> None:
|
def __get_serial(self) -> serial.Serial:
|
||||||
code = _KEYMAP.get(event.key)
|
return serial.Serial(self.__device_path, self.__speed, timeout=self.__read_timeout)
|
||||||
if code:
|
|
||||||
key_bytes = bytes([code])
|
|
||||||
assert len(key_bytes) == 1, (event, key_bytes)
|
|
||||||
tty.write(
|
|
||||||
b"\01"
|
|
||||||
+ key_bytes
|
|
||||||
+ (b"\01" if event.state else b"\00")
|
|
||||||
+ b"\00\00"
|
|
||||||
)
|
|
||||||
|
|
||||||
def __send_mouse_move_event(self, tty: serial.Serial, event: _MouseMoveEvent) -> None:
|
def __process(self, tty: serial.Serial, command: bytes) -> None:
|
||||||
to_x = min(max(-32768, event.to_x), 32767)
|
self.__process_request(tty, self.__make_request(command))
|
||||||
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:
|
def __process_request(self, tty: serial.Serial, request: bytes) -> None: # pylint: disable=too-many-branches
|
||||||
if event.button == "left":
|
logger = get_logger()
|
||||||
code = (0b10000000 | (0b00001000 if event.state else 0))
|
|
||||||
elif event.button == "right":
|
common_retries = self.__common_retries
|
||||||
code = (0b01000000 | (0b00000100 if event.state else 0))
|
read_retries = self.__read_retries
|
||||||
|
error_occured = False
|
||||||
|
|
||||||
|
while common_retries and read_retries:
|
||||||
|
if not self.__noop:
|
||||||
|
if tty.in_waiting:
|
||||||
|
tty.read(tty.in_waiting)
|
||||||
|
|
||||||
|
assert tty.write(request) == len(request)
|
||||||
|
response = tty.read(4)
|
||||||
else:
|
else:
|
||||||
code = 0
|
response = b"\x33\x20" # Magic + OK
|
||||||
if code:
|
response += struct.pack(">H", self.__make_crc16(response))
|
||||||
tty.write(b"\03" + bytes([code]) + b"\00\00\00")
|
|
||||||
|
|
||||||
def __send_mouse_wheel_event(self, tty: serial.Serial, event: _MouseWheelEvent) -> None:
|
if len(response) < 4:
|
||||||
delta_y = min(max(-128, event.delta_y), 127)
|
logger.error("No response from HID: request=%r", request)
|
||||||
tty.write(b"\04\00" + struct.pack(">b", delta_y) + b"\00\00")
|
read_retries -= 1
|
||||||
|
else:
|
||||||
|
assert len(response) == 4, response
|
||||||
|
if self.__make_crc16(response[-4:-2]) != struct.unpack(">H", response[-2:])[0]:
|
||||||
|
get_logger().error("Invalid response CRC; requesting response again ...")
|
||||||
|
request = self.__make_request(b"\x02\x00\x00\x00\x00") # Repeat an answer
|
||||||
|
else:
|
||||||
|
code = response[1]
|
||||||
|
if code == 0x48: # Request timeout
|
||||||
|
logger.error("Got request timeout from HID: request=%r", request)
|
||||||
|
elif code == 0x40: # CRC Error
|
||||||
|
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.__ok_shared.value = 1
|
||||||
|
return
|
||||||
|
elif code == 0x24: # Rebooted?
|
||||||
|
logger.error("No previous command state inside HID, seems it was rebooted")
|
||||||
|
self.__ok_shared.value = 1
|
||||||
|
return
|
||||||
|
elif code == 0x20: # Done
|
||||||
|
if error_occured:
|
||||||
|
logger.info("Success!")
|
||||||
|
self.__ok_shared.value = 1
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
logger.error("Invalid response from HID: request=%r; code=0x%x", request, code)
|
||||||
|
|
||||||
def __send_clear_hid(self, tty: serial.Serial) -> None:
|
common_retries -= 1
|
||||||
tty.write(b"\00\00\00\00\00")
|
error_occured = True
|
||||||
|
self.__ok_shared.value = 0
|
||||||
|
|
||||||
|
if common_retries and read_retries:
|
||||||
|
logger.error("Retries left: common_retries=%d; read_retries=%d", common_retries, read_retries)
|
||||||
|
time.sleep(self.__retries_delay)
|
||||||
|
|
||||||
|
logger.error("Can't process HID request due many errors: %r", request)
|
||||||
|
|
||||||
|
def __make_request(self, command: bytes) -> bytes:
|
||||||
|
request = b"\x33" + command
|
||||||
|
request += struct.pack(">H", self.__make_crc16(request))
|
||||||
|
assert len(request) == 8, (request, command)
|
||||||
|
return request
|
||||||
|
|
||||||
|
def __make_crc16(self, data: bytes) -> int:
|
||||||
|
crc = 0xFFFF
|
||||||
|
for byte in data:
|
||||||
|
crc = crc ^ byte
|
||||||
|
for _ in range(8):
|
||||||
|
if crc & 0x0001 == 0:
|
||||||
|
crc = crc >> 1
|
||||||
|
else:
|
||||||
|
crc = crc >> 1
|
||||||
|
crc = crc ^ 0xA001
|
||||||
|
return crc
|
||||||
|
|||||||
@ -144,6 +144,7 @@ def _system_task(method: Callable) -> Callable:
|
|||||||
async def wrap(self: "Server") -> None:
|
async def wrap(self: "Server") -> None:
|
||||||
try:
|
try:
|
||||||
await method(self)
|
await method(self)
|
||||||
|
raise RuntimeError("Dead system task: %s" % (method))
|
||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
pass
|
pass
|
||||||
except Exception:
|
except Exception:
|
||||||
@ -201,6 +202,7 @@ def _valid_int(name: str, value: Optional[str], min_value: Optional[int]=None, m
|
|||||||
|
|
||||||
class _Events(Enum):
|
class _Events(Enum):
|
||||||
INFO_STATE = "info_state"
|
INFO_STATE = "info_state"
|
||||||
|
HID_STATE = "hid_state"
|
||||||
ATX_STATE = "atx_state"
|
ATX_STATE = "atx_state"
|
||||||
MSD_STATE = "msd_state"
|
MSD_STATE = "msd_state"
|
||||||
STREAMER_STATE = "streamer_state"
|
STREAMER_STATE = "streamer_state"
|
||||||
@ -357,6 +359,7 @@ class Server: # pylint: disable=too-many-instance-attributes
|
|||||||
await self.__register_socket(ws)
|
await self.__register_socket(ws)
|
||||||
await asyncio.gather(*[
|
await asyncio.gather(*[
|
||||||
self.__broadcast_event(_Events.INFO_STATE, (await self.__make_info())),
|
self.__broadcast_event(_Events.INFO_STATE, (await self.__make_info())),
|
||||||
|
self.__broadcast_event(_Events.HID_STATE, self.__hid.get_state()),
|
||||||
self.__broadcast_event(_Events.ATX_STATE, self.__atx.get_state()),
|
self.__broadcast_event(_Events.ATX_STATE, self.__atx.get_state()),
|
||||||
self.__broadcast_event(_Events.MSD_STATE, self.__msd.get_state()),
|
self.__broadcast_event(_Events.MSD_STATE, self.__msd.get_state()),
|
||||||
self.__broadcast_event(_Events.STREAMER_STATE, (await self.__streamer.get_state())),
|
self.__broadcast_event(_Events.STREAMER_STATE, (await self.__streamer.get_state())),
|
||||||
@ -567,12 +570,6 @@ class Server: # pylint: disable=too-many-instance-attributes
|
|||||||
|
|
||||||
# ===== SYSTEM TASKS
|
# ===== SYSTEM TASKS
|
||||||
|
|
||||||
@_system_task
|
|
||||||
async def __hid_watchdog(self) -> None:
|
|
||||||
while self.__hid.is_alive():
|
|
||||||
await asyncio.sleep(0.1)
|
|
||||||
raise RuntimeError("HID is dead")
|
|
||||||
|
|
||||||
@_system_task
|
@_system_task
|
||||||
async def __stream_controller(self) -> None:
|
async def __stream_controller(self) -> None:
|
||||||
prev = 0
|
prev = 0
|
||||||
@ -606,6 +603,11 @@ class Server: # pylint: disable=too-many-instance-attributes
|
|||||||
await self.__remove_socket(ws)
|
await self.__remove_socket(ws)
|
||||||
await asyncio.sleep(0.1)
|
await asyncio.sleep(0.1)
|
||||||
|
|
||||||
|
@_system_task
|
||||||
|
async def __poll_hid_state(self) -> None:
|
||||||
|
async for state in self.__hid.poll_state():
|
||||||
|
await self.__broadcast_event(_Events.HID_STATE, state)
|
||||||
|
|
||||||
@_system_task
|
@_system_task
|
||||||
async def __poll_atx_state(self) -> None:
|
async def __poll_atx_state(self) -> None:
|
||||||
async for state in self.__atx.poll_state():
|
async for state in self.__atx.poll_state():
|
||||||
|
|||||||
@ -16,9 +16,17 @@ kvmd:
|
|||||||
pinout:
|
pinout:
|
||||||
reset: 4
|
reset: 4
|
||||||
|
|
||||||
|
reset_delay: 0.1
|
||||||
|
|
||||||
device: /dev/ttyS10
|
device: /dev/ttyS10
|
||||||
speed: 115200
|
speed: 115200
|
||||||
reset_delay: 0.1
|
read_timeout: 2.0
|
||||||
|
read_retries: 10
|
||||||
|
common_retries: 100
|
||||||
|
retries_delay: 0.1
|
||||||
|
noop: true
|
||||||
|
|
||||||
|
state_poll: 1.0
|
||||||
|
|
||||||
atx:
|
atx:
|
||||||
pinout:
|
pinout:
|
||||||
@ -36,7 +44,7 @@ kvmd:
|
|||||||
target: 12
|
target: 12
|
||||||
reset: 13
|
reset: 13
|
||||||
|
|
||||||
device: "/dev/kvmd-msd"
|
device: /dev/kvmd-msd
|
||||||
init_delay: 2.0
|
init_delay: 2.0
|
||||||
reset_delay: 1.0
|
reset_delay: 1.0
|
||||||
write_meta: true
|
write_meta: true
|
||||||
|
|||||||
@ -62,6 +62,11 @@ function Hid() {
|
|||||||
__mouse.setSocket(ws);
|
__mouse.setSocket(ws);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
self.setState = function(state) {
|
||||||
|
__keyboard.setState(state);
|
||||||
|
__mouse.setState(state);
|
||||||
|
};
|
||||||
|
|
||||||
var __releaseAll = function() {
|
var __releaseAll = function() {
|
||||||
__keyboard.releaseAll();
|
__keyboard.releaseAll();
|
||||||
};
|
};
|
||||||
|
|||||||
@ -4,6 +4,7 @@ function Keyboard() {
|
|||||||
/********************************************************************************/
|
/********************************************************************************/
|
||||||
|
|
||||||
var __ws = null;
|
var __ws = null;
|
||||||
|
var __ok = true;
|
||||||
|
|
||||||
var __keys = [].slice.call($$$("div#keyboard-desktop div.keyboard-block div.keyboard-row div.key"));
|
var __keys = [].slice.call($$$("div#keyboard-desktop div.keyboard-block div.keyboard-row div.key"));
|
||||||
var __modifiers = [].slice.call($$$("div#keyboard-desktop div.keyboard-block div.keyboard-row div.modifier"));
|
var __modifiers = [].slice.call($$$("div#keyboard-desktop div.keyboard-block div.keyboard-row div.modifier"));
|
||||||
@ -53,6 +54,10 @@ function Keyboard() {
|
|||||||
__updateLeds();
|
__updateLeds();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
self.setState = function(state) {
|
||||||
|
__ok = state.ok;
|
||||||
|
};
|
||||||
|
|
||||||
self.releaseAll = function() {
|
self.releaseAll = function() {
|
||||||
__keys.concat(__modifiers).forEach(function(el_key) {
|
__keys.concat(__modifiers).forEach(function(el_key) {
|
||||||
if (__isActive(el_key)) {
|
if (__isActive(el_key)) {
|
||||||
@ -73,8 +78,13 @@ function Keyboard() {
|
|||||||
|| $("keyboard-window").classList.contains("window-active")
|
|| $("keyboard-window").classList.contains("window-active")
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
|
if (__ok) {
|
||||||
$("hid-keyboard-led").className = "led-green";
|
$("hid-keyboard-led").className = "led-green";
|
||||||
$("hid-keyboard-led").title = "Keyboard captured";
|
$("hid-keyboard-led").title = "Keyboard captured";
|
||||||
|
} else {
|
||||||
|
$("hid-keyboard-led").className = "led-yellow";
|
||||||
|
$("hid-keyboard-led").title = "Keyboard captured, HID offline";
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
$("hid-keyboard-led").className = "led-gray";
|
$("hid-keyboard-led").className = "led-gray";
|
||||||
$("hid-keyboard-led").title = "Keyboard free";
|
$("hid-keyboard-led").title = "Keyboard free";
|
||||||
|
|||||||
@ -4,6 +4,7 @@ function Mouse() {
|
|||||||
/********************************************************************************/
|
/********************************************************************************/
|
||||||
|
|
||||||
var __ws = null;
|
var __ws = null;
|
||||||
|
var __ok = true;
|
||||||
|
|
||||||
var __current_pos = {x: 0, y:0};
|
var __current_pos = {x: 0, y:0};
|
||||||
var __sent_pos = {x: 0, y:0};
|
var __sent_pos = {x: 0, y:0};
|
||||||
@ -44,6 +45,10 @@ function Mouse() {
|
|||||||
__updateLeds();
|
__updateLeds();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
self.setState = function(state) {
|
||||||
|
__ok = state.ok;
|
||||||
|
};
|
||||||
|
|
||||||
var __hoverStream = function() {
|
var __hoverStream = function() {
|
||||||
__stream_hovered = true;
|
__stream_hovered = true;
|
||||||
__updateLeds();
|
__updateLeds();
|
||||||
@ -57,8 +62,13 @@ function Mouse() {
|
|||||||
var __updateLeds = function() {
|
var __updateLeds = function() {
|
||||||
if (__ws && (__stream_hovered || tools.browser.is_ios)) {
|
if (__ws && (__stream_hovered || tools.browser.is_ios)) {
|
||||||
// Mouse is always available on iOS via touchscreen
|
// Mouse is always available on iOS via touchscreen
|
||||||
|
if (__ok) {
|
||||||
$("hid-mouse-led").className = "led-green";
|
$("hid-mouse-led").className = "led-green";
|
||||||
$("hid-mouse-led").title = "Mouse tracked";
|
$("hid-mouse-led").title = "Mouse tracked";
|
||||||
|
} else {
|
||||||
|
$("hid-mouse-led").className = "led-yellow";
|
||||||
|
$("hid-mouse-led").title = "Mouse tracked, HID offline";
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
$("hid-mouse-led").className = "led-gray";
|
$("hid-mouse-led").className = "led-gray";
|
||||||
$("hid-mouse-led").title = "Mouse free";
|
$("hid-mouse-led").title = "Mouse free";
|
||||||
|
|||||||
@ -84,6 +84,8 @@ function Session() {
|
|||||||
} else if (event.msg_type === "event") {
|
} else if (event.msg_type === "event") {
|
||||||
if (event.msg.event === "info_state") {
|
if (event.msg.event === "info_state") {
|
||||||
__setKvmdInfo(event.msg.event_attrs);
|
__setKvmdInfo(event.msg.event_attrs);
|
||||||
|
} else if (event.msg.event === "hid_state") {
|
||||||
|
__hid.setState(event.msg.event_attrs);
|
||||||
} else if (event.msg.event === "atx_state") {
|
} else if (event.msg.event === "atx_state") {
|
||||||
__atx.setState(event.msg.event_attrs);
|
__atx.setState(event.msg.event_attrs);
|
||||||
} else if (event.msg.event === "msd_state") {
|
} else if (event.msg.event === "msd_state") {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user