mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2025-12-12 01:00:29 +08:00
gpio diff events mode
This commit is contained in:
parent
3852d0a456
commit
90d8e745e3
@ -42,10 +42,7 @@ class UserGpioApi:
|
||||
|
||||
@exposed_http("GET", "/gpio")
|
||||
async def __state_handler(self, _: Request) -> Response:
|
||||
return make_json_response({
|
||||
"model": (await self.__user_gpio.get_model()),
|
||||
"state": (await self.__user_gpio.get_state()),
|
||||
})
|
||||
return make_json_response(await self.__user_gpio.get_state())
|
||||
|
||||
@exposed_http("POST", "/gpio/switch")
|
||||
async def __switch_handler(self, req: Request) -> Response:
|
||||
|
||||
@ -100,8 +100,9 @@ class StreamerH264NotSupported(OperationError):
|
||||
# =====
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class _SubsystemEventSource:
|
||||
get_state: (Callable[[], Coroutine[Any, Any, dict]] | None) = None
|
||||
poll_state: (Callable[[], AsyncGenerator[dict, None]] | None) = None
|
||||
get_state: (Callable[[], Coroutine[Any, Any, dict]] | None) = None
|
||||
trigger_state: (Callable[[], Coroutine[Any, Any, None]] | None) = None
|
||||
poll_state: (Callable[[], AsyncGenerator[dict, None]] | None) = None
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
@ -127,6 +128,7 @@ class _Subsystem:
|
||||
sub.add_source(
|
||||
event_type=event_type,
|
||||
get_state=getattr(obj, "get_state", None),
|
||||
trigger_state=getattr(obj, "trigger_state", None),
|
||||
poll_state=getattr(obj, "poll_state", None),
|
||||
)
|
||||
return sub
|
||||
@ -135,17 +137,20 @@ class _Subsystem:
|
||||
self,
|
||||
event_type: str,
|
||||
get_state: (Callable[[], Coroutine[Any, Any, dict]] | None),
|
||||
trigger_state: (Callable[[], Coroutine[Any, Any, None]] | None),
|
||||
poll_state: (Callable[[], AsyncGenerator[dict, None]] | None),
|
||||
) -> "_Subsystem":
|
||||
|
||||
assert event_type
|
||||
assert event_type not in self.sources, (self, event_type)
|
||||
assert get_state or poll_state, (self, event_type)
|
||||
self.sources[event_type] = _SubsystemEventSource(get_state, poll_state)
|
||||
self.sources[event_type] = _SubsystemEventSource(get_state, trigger_state, poll_state)
|
||||
return self
|
||||
|
||||
|
||||
class KvmdServer(HttpServer): # pylint: disable=too-many-arguments,too-many-instance-attributes
|
||||
__EV_GPIO_STATE = "gpio_state"
|
||||
|
||||
def __init__( # pylint: disable=too-many-arguments,too-many-locals
|
||||
self,
|
||||
auth_manager: AuthManager,
|
||||
@ -195,11 +200,11 @@ class KvmdServer(HttpServer): # pylint: disable=too-many-arguments,too-many-ins
|
||||
|
||||
self.__subsystems = [
|
||||
_Subsystem.make(auth_manager, "Auth manager"),
|
||||
_Subsystem.make(user_gpio, "User-GPIO", "gpio_state").add_source("gpio_model_state", user_gpio.get_model, None),
|
||||
_Subsystem.make(hid, "HID", "hid_state").add_source("hid_keymaps_state", self.__hid_api.get_keymaps, None),
|
||||
_Subsystem.make(user_gpio, "User-GPIO", self.__EV_GPIO_STATE),
|
||||
_Subsystem.make(hid, "HID", "hid_state").add_source("hid_keymaps_state", self.__hid_api.get_keymaps, None, None),
|
||||
_Subsystem.make(atx, "ATX", "atx_state"),
|
||||
_Subsystem.make(msd, "MSD", "msd_state"),
|
||||
_Subsystem.make(streamer, "Streamer", "streamer_state").add_source("streamer_ocr_state", self.__streamer_api.get_ocr, None),
|
||||
_Subsystem.make(streamer, "Streamer", "streamer_state").add_source("streamer_ocr_state", self.__streamer_api.get_ocr, None, None),
|
||||
*[
|
||||
_Subsystem.make(info_manager.get_submanager(sub), f"Info manager ({sub})", f"info_{sub}_state",)
|
||||
for sub in sorted(info_manager.get_subs())
|
||||
@ -244,12 +249,13 @@ class KvmdServer(HttpServer): # pylint: disable=too-many-arguments,too-many-ins
|
||||
@exposed_http("GET", "/ws")
|
||||
async def __ws_handler(self, req: Request) -> WebSocketResponse:
|
||||
stream = valid_bool(req.query.get("stream", True))
|
||||
async with self._ws_session(req, stream=stream) as ws:
|
||||
legacy = valid_bool(req.query.get("legacy", True))
|
||||
async with self._ws_session(req, stream=stream, legacy=legacy) as ws:
|
||||
states = [
|
||||
(event_type, src.get_state())
|
||||
for sub in self.__subsystems
|
||||
for (event_type, src) in sub.sources.items()
|
||||
if src.get_state
|
||||
if src.get_state and not src.trigger_state
|
||||
]
|
||||
events = dict(zip(
|
||||
map(operator.itemgetter(0), states),
|
||||
@ -259,6 +265,10 @@ class KvmdServer(HttpServer): # pylint: disable=too-many-arguments,too-many-ins
|
||||
ws.send_event(event_type, events.pop(event_type))
|
||||
for (event_type, _) in states
|
||||
])
|
||||
for sub in self.__subsystems:
|
||||
for src in sub.sources.values():
|
||||
if src.trigger_state:
|
||||
await src.trigger_state()
|
||||
await ws.send_event("loop", {})
|
||||
return (await self._ws_loop(ws))
|
||||
|
||||
@ -347,12 +357,27 @@ class KvmdServer(HttpServer): # pylint: disable=too-many-arguments,too-many-ins
|
||||
prev = cur
|
||||
await self.__streamer_notifier.wait()
|
||||
|
||||
async def __poll_state(self, event_type: str, poller: AsyncGenerator[dict, None]) -> None:
|
||||
async for state in poller:
|
||||
await self._broadcast_ws_event(event_type, state)
|
||||
|
||||
async def __stream_snapshoter(self) -> None:
|
||||
await self.__snapshoter.run(
|
||||
is_live=self.__has_stream_clients,
|
||||
notifier=self.__streamer_notifier,
|
||||
)
|
||||
|
||||
async def __poll_state(self, event_type: str, poller: AsyncGenerator[dict, None]) -> None:
|
||||
if event_type == self.__EV_GPIO_STATE:
|
||||
await self.__poll_gpio_state(poller)
|
||||
else:
|
||||
async for state in poller:
|
||||
await self._broadcast_ws_event(event_type, state)
|
||||
|
||||
async def __poll_gpio_state(self, poller: AsyncGenerator[dict, None]) -> None:
|
||||
prev: dict = {"state": {"inputs": {}, "outputs": {}}}
|
||||
async for state in poller:
|
||||
await self._broadcast_ws_event(self.__EV_GPIO_STATE, state, legacy=False)
|
||||
if "model" in state: # We have only "model"+"state" or "model" event
|
||||
prev = state
|
||||
await self._broadcast_ws_event("gpio_model_state", prev["model"], legacy=True)
|
||||
else:
|
||||
prev["state"]["inputs"].update(state["state"].get("inputs", {}))
|
||||
prev["state"]["outputs"].update(state["state"].get("outputs", {}))
|
||||
await self._broadcast_ws_event(self.__EV_GPIO_STATE, prev["state"], legacy=True)
|
||||
|
||||
@ -21,6 +21,7 @@
|
||||
|
||||
|
||||
import asyncio
|
||||
import copy
|
||||
|
||||
from typing import AsyncGenerator
|
||||
from typing import Callable
|
||||
@ -68,12 +69,12 @@ class GpioChannelIsBusyError(IsBusyError, GpioError):
|
||||
class _GpioInput:
|
||||
def __init__(
|
||||
self,
|
||||
channel: str,
|
||||
ch: str,
|
||||
config: Section,
|
||||
driver: BaseUserGpioDriver,
|
||||
) -> None:
|
||||
|
||||
self.__channel = channel
|
||||
self.__ch = ch
|
||||
self.__pin: str = str(config.pin)
|
||||
self.__inverted: bool = config.inverted
|
||||
|
||||
@ -100,7 +101,7 @@ class _GpioInput:
|
||||
}
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"Input({self.__channel}, driver={self.__driver}, pin={self.__pin})"
|
||||
return f"Input({self.__ch}, driver={self.__driver}, pin={self.__pin})"
|
||||
|
||||
__repr__ = __str__
|
||||
|
||||
@ -108,13 +109,13 @@ class _GpioInput:
|
||||
class _GpioOutput: # pylint: disable=too-many-instance-attributes
|
||||
def __init__(
|
||||
self,
|
||||
channel: str,
|
||||
ch: str,
|
||||
config: Section,
|
||||
driver: BaseUserGpioDriver,
|
||||
notifier: aiotools.AioNotifier,
|
||||
) -> None:
|
||||
|
||||
self.__channel = channel
|
||||
self.__ch = ch
|
||||
self.__pin: str = str(config.pin)
|
||||
self.__inverted: bool = config.inverted
|
||||
|
||||
@ -224,7 +225,7 @@ class _GpioOutput: # pylint: disable=too-many-instance-attributes
|
||||
await self.__driver.write(self.__pin, (state ^ self.__inverted))
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"Output({self.__channel}, driver={self.__driver}, pin={self.__pin})"
|
||||
return f"Output({self.__ch}, driver={self.__driver}, pin={self.__pin})"
|
||||
|
||||
__repr__ = __str__
|
||||
|
||||
@ -232,9 +233,8 @@ class _GpioOutput: # pylint: disable=too-many-instance-attributes
|
||||
# =====
|
||||
class UserGpio:
|
||||
def __init__(self, config: Section, otg_config: Section) -> None:
|
||||
self.__view = config.view
|
||||
|
||||
self.__notifier = aiotools.AioNotifier()
|
||||
self.__full_state_requested = True
|
||||
|
||||
self.__drivers = {
|
||||
driver: get_ugpio_driver_class(drv_config.type)(
|
||||
@ -249,45 +249,64 @@ class UserGpio:
|
||||
self.__inputs: dict[str, _GpioInput] = {}
|
||||
self.__outputs: dict[str, _GpioOutput] = {}
|
||||
|
||||
for (channel, ch_config) in tools.sorted_kvs(config.scheme):
|
||||
for (ch, ch_config) in tools.sorted_kvs(config.scheme):
|
||||
driver = self.__drivers[ch_config.driver]
|
||||
if ch_config.mode == UserGpioModes.INPUT:
|
||||
self.__inputs[channel] = _GpioInput(channel, ch_config, driver)
|
||||
self.__inputs[ch] = _GpioInput(ch, ch_config, driver)
|
||||
else: # output:
|
||||
self.__outputs[channel] = _GpioOutput(channel, ch_config, driver, self.__notifier)
|
||||
self.__outputs[ch] = _GpioOutput(ch, ch_config, driver, self.__notifier)
|
||||
|
||||
async def get_model(self) -> dict:
|
||||
return {
|
||||
"scheme": {
|
||||
"inputs": {channel: gin.get_scheme() for (channel, gin) in self.__inputs.items()},
|
||||
"outputs": {
|
||||
channel: gout.get_scheme()
|
||||
for (channel, gout) in self.__outputs.items()
|
||||
if not gout.is_const()
|
||||
},
|
||||
},
|
||||
"view": self.__make_view(),
|
||||
}
|
||||
self.__scheme = self.__make_scheme()
|
||||
self.__view = self.__make_view(config.view)
|
||||
|
||||
async def get_state(self) -> dict:
|
||||
return {
|
||||
"inputs": {channel: await gin.get_state() for (channel, gin) in self.__inputs.items()},
|
||||
"model": {
|
||||
"scheme": copy.deepcopy(self.__scheme),
|
||||
"view": copy.deepcopy(self.__view),
|
||||
},
|
||||
"state": (await self.__get_io_state()),
|
||||
}
|
||||
|
||||
async def trigger_state(self) -> None:
|
||||
self.__full_state_requested = True
|
||||
self.__notifier.notify()
|
||||
|
||||
async def poll_state(self) -> AsyncGenerator[dict, None]:
|
||||
prev: dict = {"inputs": {}, "outputs": {}}
|
||||
while True: # pylint: disable=too-many-nested-blocks
|
||||
if self.__full_state_requested:
|
||||
self.__full_state_requested = False
|
||||
full = await self.get_state()
|
||||
prev = copy.deepcopy(full["state"])
|
||||
yield full
|
||||
else:
|
||||
new = await self.__get_io_state()
|
||||
diff: dict = {}
|
||||
for sub in ["inputs", "outputs"]:
|
||||
for ch in new[sub]:
|
||||
if new[sub][ch] != prev[sub][ch]:
|
||||
if sub not in diff:
|
||||
diff[sub] = {}
|
||||
diff[sub][ch] = new[sub][ch]
|
||||
if diff:
|
||||
prev = copy.deepcopy(new)
|
||||
yield {"state": diff}
|
||||
await self.__notifier.wait()
|
||||
|
||||
async def __get_io_state(self) -> dict:
|
||||
return {
|
||||
"inputs": {
|
||||
ch: (await gin.get_state())
|
||||
for (ch, gin) in self.__inputs.items()
|
||||
},
|
||||
"outputs": {
|
||||
channel: await gout.get_state()
|
||||
for (channel, gout) in self.__outputs.items()
|
||||
ch: (await gout.get_state())
|
||||
for (ch, gout) in self.__outputs.items()
|
||||
if not gout.is_const()
|
||||
},
|
||||
}
|
||||
|
||||
async def poll_state(self) -> AsyncGenerator[dict, None]:
|
||||
prev_state: dict = {}
|
||||
while True:
|
||||
state = await self.get_state()
|
||||
if state != prev_state:
|
||||
yield state
|
||||
prev_state = state
|
||||
await self.__notifier.wait()
|
||||
|
||||
def sysprep(self) -> None:
|
||||
get_logger(0).info("Preparing User-GPIO drivers ...")
|
||||
for (_, driver) in tools.sorted_kvs(self.__drivers):
|
||||
@ -307,28 +326,43 @@ class UserGpio:
|
||||
except Exception:
|
||||
get_logger().exception("Can't cleanup driver %s", driver)
|
||||
|
||||
async def switch(self, channel: str, state: bool, wait: bool) -> None:
|
||||
gout = self.__outputs.get(channel)
|
||||
async def switch(self, ch: str, state: bool, wait: bool) -> None:
|
||||
gout = self.__outputs.get(ch)
|
||||
if gout is None:
|
||||
raise GpioChannelNotFoundError()
|
||||
await gout.switch(state, wait)
|
||||
|
||||
async def pulse(self, channel: str, delay: float, wait: bool) -> None:
|
||||
gout = self.__outputs.get(channel)
|
||||
async def pulse(self, ch: str, delay: float, wait: bool) -> None:
|
||||
gout = self.__outputs.get(ch)
|
||||
if gout is None:
|
||||
raise GpioChannelNotFoundError()
|
||||
await gout.pulse(delay, wait)
|
||||
|
||||
# =====
|
||||
|
||||
def __make_view(self) -> dict:
|
||||
def __make_scheme(self) -> dict:
|
||||
return {
|
||||
"header": {"title": self.__make_view_title()},
|
||||
"table": self.__make_view_table(),
|
||||
"inputs": {
|
||||
ch: gin.get_scheme()
|
||||
for (ch, gin) in self.__inputs.items()
|
||||
},
|
||||
"outputs": {
|
||||
ch: gout.get_scheme()
|
||||
for (ch, gout) in self.__outputs.items()
|
||||
if not gout.is_const()
|
||||
},
|
||||
}
|
||||
|
||||
def __make_view_title(self) -> list[dict]:
|
||||
raw_title = self.__view["header"]["title"]
|
||||
# =====
|
||||
|
||||
def __make_view(self, view: dict) -> dict:
|
||||
return {
|
||||
"header": {"title": self.__make_view_title(view)},
|
||||
"table": self.__make_view_table(view),
|
||||
}
|
||||
|
||||
def __make_view_title(self, view: dict) -> list[dict]:
|
||||
raw_title = view["header"]["title"]
|
||||
title: list[dict] = []
|
||||
if isinstance(raw_title, list):
|
||||
for item in raw_title:
|
||||
@ -342,9 +376,9 @@ class UserGpio:
|
||||
title.append(self.__make_item_label(f"#{raw_title}"))
|
||||
return title
|
||||
|
||||
def __make_view_table(self) -> list[list[dict] | None]:
|
||||
def __make_view_table(self, view: dict) -> list[list[dict] | None]:
|
||||
table: list[list[dict] | None] = []
|
||||
for row in self.__view["table"]:
|
||||
for row in view["table"]:
|
||||
if len(row) == 0:
|
||||
table.append(None)
|
||||
continue
|
||||
|
||||
@ -384,7 +384,7 @@ class HttpServer:
|
||||
break
|
||||
return ws.wsr
|
||||
|
||||
async def _broadcast_ws_event(self, event_type: str, event: (dict | None)) -> None:
|
||||
async def _broadcast_ws_event(self, event_type: str, event: (dict | None), legacy: (bool | None)=None) -> None:
|
||||
if self.__ws_sessions:
|
||||
await asyncio.gather(*[
|
||||
ws.send_event(event_type, event)
|
||||
@ -393,6 +393,7 @@ class HttpServer:
|
||||
not ws.wsr.closed
|
||||
and ws.wsr._req is not None # pylint: disable=protected-access
|
||||
and ws.wsr._req.transport is not None # pylint: disable=protected-access
|
||||
and (legacy is None or ws.kwargs.get("legacy") == legacy)
|
||||
)
|
||||
], return_exceptions=True)
|
||||
|
||||
|
||||
@ -32,26 +32,18 @@ export function Gpio(__recorder) {
|
||||
|
||||
/************************************************************************/
|
||||
|
||||
var __state = null;
|
||||
var __has_model = false;
|
||||
|
||||
/************************************************************************/
|
||||
|
||||
self.setState = function(state) {
|
||||
if (state) {
|
||||
for (let ch in state.inputs) {
|
||||
for (let el of $$(`__gpio-led-${ch}`)) {
|
||||
__setLedState(el, state.inputs[ch].state);
|
||||
}
|
||||
if (state.model) {
|
||||
__applyModel(state.model);
|
||||
__has_model = true;
|
||||
}
|
||||
for (let ch in state.outputs) {
|
||||
for (let type of ["switch", "button"]) {
|
||||
for (let el of $$(`__gpio-${type}-${ch}`)) {
|
||||
tools.el.setEnabled(el, state.outputs[ch].online && !state.outputs[ch].busy);
|
||||
}
|
||||
}
|
||||
for (let el of $$(`__gpio-switch-${ch}`)) {
|
||||
el.checked = state.outputs[ch].state;
|
||||
}
|
||||
if (__has_model && state.state) {
|
||||
__applyState(state.state);
|
||||
}
|
||||
} else {
|
||||
for (let el of $$("__gpio-led")) {
|
||||
@ -62,11 +54,33 @@ export function Gpio(__recorder) {
|
||||
tools.el.setEnabled(el, false);
|
||||
}
|
||||
}
|
||||
__has_model = false;
|
||||
}
|
||||
__state = state;
|
||||
};
|
||||
|
||||
self.setModel = function(model) {
|
||||
var __applyState = function(state) {
|
||||
if (state.inputs) {
|
||||
for (let ch in state.inputs) {
|
||||
for (let el of $$(`__gpio-led-${ch}`)) {
|
||||
__setLedState(el, state.inputs[ch].state);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (state.outputs) {
|
||||
for (let ch in state.outputs) {
|
||||
for (let type of ["switch", "button"]) {
|
||||
for (let el of $$(`__gpio-${type}-${ch}`)) {
|
||||
tools.el.setEnabled(el, state.outputs[ch].online && !state.outputs[ch].busy);
|
||||
}
|
||||
}
|
||||
for (let el of $$(`__gpio-switch-${ch}`)) {
|
||||
el.checked = state.outputs[ch].state;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var __applyModel = function(model) {
|
||||
tools.feature.setEnabled($("gpio-dropdown"), model.view.table.length);
|
||||
if (model.view.table.length) {
|
||||
let title = [];
|
||||
@ -81,23 +95,23 @@ export function Gpio(__recorder) {
|
||||
$("gpio-menu-button").innerHTML = title.join(" ");
|
||||
}
|
||||
|
||||
let content = "<table class=\"kv\">";
|
||||
let html = "<table class=\"kv\">";
|
||||
for (let row of model.view.table) {
|
||||
if (row === null) {
|
||||
content += "</table><hr><table class=\"kv\">";
|
||||
html += "</table><hr><table class=\"kv\">";
|
||||
} else {
|
||||
content += "<tr>";
|
||||
html += "<tr>";
|
||||
for (let item of row) {
|
||||
if (item.type === "output") {
|
||||
item.scheme = model.scheme.outputs[item.channel];
|
||||
}
|
||||
content += `<td align="center">${__createItem(item)}</td>`;
|
||||
html += `<td align="center">${__createItem(item)}</td>`;
|
||||
}
|
||||
content += "</tr>";
|
||||
html += "</tr>";
|
||||
}
|
||||
}
|
||||
content += "</table>";
|
||||
$("gpio-menu").innerHTML = content;
|
||||
html += "</table>";
|
||||
$("gpio-menu").innerHTML = html;
|
||||
|
||||
for (let ch in model.scheme.outputs) {
|
||||
for (let el of $$(`__gpio-switch-${ch}`)) {
|
||||
@ -111,8 +125,6 @@ export function Gpio(__recorder) {
|
||||
tools.feature.setEnabled($("v3-usb-breaker"), ("__v3_usb_breaker__" in model.scheme.outputs));
|
||||
tools.feature.setEnabled($("v4-locator"), ("__v4_locator__" in model.scheme.outputs));
|
||||
tools.feature.setEnabled($("system-tool-wol"), ("__wol__" in model.scheme.outputs));
|
||||
|
||||
self.setState(__state);
|
||||
};
|
||||
|
||||
var __createItem = function(item) {
|
||||
@ -167,9 +179,9 @@ export function Gpio(__recorder) {
|
||||
}
|
||||
};
|
||||
|
||||
var __setLedState = function(el, state) {
|
||||
var __setLedState = function(el, on) {
|
||||
let color = el.getAttribute("data-color");
|
||||
if (state) {
|
||||
if (on) {
|
||||
el.classList.add(`led-${color}`);
|
||||
el.classList.remove("led-gray");
|
||||
} else {
|
||||
@ -221,7 +233,7 @@ export function Gpio(__recorder) {
|
||||
var __sendPost = function(url, params) {
|
||||
tools.httpPost(url, params, function(http) {
|
||||
if (http.status === 409) {
|
||||
wm.error("Performing another operation for this GPIO channel.<br>Please try again later");
|
||||
wm.error("Performing another operation for this GPIO channel.<br>Please try again later.");
|
||||
} else if (http.status !== 200) {
|
||||
wm.error("GPIO error", http.responseText);
|
||||
}
|
||||
|
||||
@ -282,7 +282,7 @@ export function Session() {
|
||||
|
||||
tools.httpGet("/api/auth/check", null, function(http) {
|
||||
if (http.status === 200) {
|
||||
__ws = new WebSocket(`${tools.is_https ? "wss" : "ws"}://${location.host}/api/ws`);
|
||||
__ws = new WebSocket(`${tools.is_https ? "wss" : "ws"}://${location.host}/api/ws?legacy=0`);
|
||||
__ws.sendHidEvent = (event) => __sendHidEvent(__ws, event.event_type, event.event);
|
||||
__ws.onopen = __wsOpenHandler;
|
||||
__ws.onmessage = __wsMessageHandler;
|
||||
@ -359,7 +359,6 @@ export function Session() {
|
||||
case "info_fan_state": __setAboutInfoFan(data.event); break;
|
||||
case "info_system_state": __setAboutInfoSystem(data.event); break;
|
||||
case "info_extras_state": __setExtras(data.event); break;
|
||||
case "gpio_model_state": __gpio.setModel(data.event); break;
|
||||
case "gpio_state": __gpio.setState(data.event); break;
|
||||
case "hid_keymaps_state": __hid.setKeymaps(data.event); break;
|
||||
case "hid_state": __hid.setState(data.event); break;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user