mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-02-02 11:01:53 +08:00
otg keyboard leds
This commit is contained in:
@@ -21,7 +21,7 @@
|
|||||||
|
|
||||||
|
|
||||||
# https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code
|
# https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code
|
||||||
# https://github.com/NicoHood/HID/blob/master/src/HID-APIs/ImprovedKeylayouts.h
|
# https://github.com/NicoHood/HID/blob/master/src/KeyboardLayouts/ImprovedKeylayouts.h
|
||||||
# https://gist.github.com/MightyPork/6da26e382a7ad91b5496ee55fdc73db2
|
# https://gist.github.com/MightyPork/6da26e382a7ad91b5496ee55fdc73db2
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -21,6 +21,11 @@
|
|||||||
|
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import concurrent.futures
|
||||||
|
import multiprocessing
|
||||||
|
import multiprocessing.queues
|
||||||
|
import queue
|
||||||
|
import functools
|
||||||
|
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
from typing import AsyncGenerator
|
from typing import AsyncGenerator
|
||||||
@@ -47,15 +52,12 @@ class Plugin(BaseHid):
|
|||||||
keyboard: Dict[str, Any],
|
keyboard: Dict[str, Any],
|
||||||
mouse: Dict[str, Any],
|
mouse: Dict[str, Any],
|
||||||
noop: bool,
|
noop: bool,
|
||||||
state_poll: float,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
||||||
self.__keyboard_proc = KeyboardProcess(noop=noop, **keyboard)
|
self.__changes_queue: multiprocessing.queues.Queue = multiprocessing.Queue()
|
||||||
self.__mouse_proc = MouseProcess(noop=noop, **mouse)
|
|
||||||
|
|
||||||
self.__state_poll = state_poll
|
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.__lock = asyncio.Lock()
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_plugin_options(cls) -> Dict:
|
def get_plugin_options(cls) -> Dict:
|
||||||
@@ -66,16 +68,13 @@ class Plugin(BaseHid):
|
|||||||
"write_retries": Option(5, type=valid_int_f1),
|
"write_retries": Option(5, type=valid_int_f1),
|
||||||
"write_retries_delay": Option(0.1, type=valid_float_f01),
|
"write_retries_delay": Option(0.1, type=valid_float_f01),
|
||||||
},
|
},
|
||||||
|
|
||||||
"mouse": {
|
"mouse": {
|
||||||
"device": Option("", type=valid_abs_path, unpack_as="device_path"),
|
"device": Option("", type=valid_abs_path, unpack_as="device_path"),
|
||||||
"select_timeout": Option(1.0, type=valid_float_f01),
|
"select_timeout": Option(1.0, type=valid_float_f01),
|
||||||
"write_retries": Option(5, type=valid_int_f1),
|
"write_retries": Option(5, type=valid_int_f1),
|
||||||
"write_retries_delay": Option(0.1, type=valid_float_f01),
|
"write_retries_delay": Option(0.1, type=valid_float_f01),
|
||||||
},
|
},
|
||||||
|
"noop": Option(False, type=valid_bool),
|
||||||
"noop": Option(False, type=valid_bool),
|
|
||||||
"state_poll": Option(0.1, type=valid_float_f01),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def start(self) -> None:
|
def start(self) -> None:
|
||||||
@@ -83,22 +82,30 @@ class Plugin(BaseHid):
|
|||||||
self.__mouse_proc.start()
|
self.__mouse_proc.start()
|
||||||
|
|
||||||
def get_state(self) -> Dict:
|
def get_state(self) -> Dict:
|
||||||
keyboard_online = self.__keyboard_proc.is_online()
|
keyboard_state = self.__keyboard_proc.get_state()
|
||||||
mouse_online = self.__mouse_proc.is_online()
|
mouse_state = self.__mouse_proc.get_state()
|
||||||
return {
|
return {
|
||||||
"online": (keyboard_online and mouse_online),
|
"online": (keyboard_state["online"] and mouse_state["online"]),
|
||||||
"keyboard": {"online": keyboard_online},
|
"keyboard": {"features": {"leds": True}, **keyboard_state},
|
||||||
"mouse": {"online": mouse_online},
|
"mouse": mouse_state,
|
||||||
}
|
}
|
||||||
|
|
||||||
async def poll_state(self) -> AsyncGenerator[Dict, None]:
|
async def poll_state(self) -> AsyncGenerator[Dict, None]:
|
||||||
prev_state: Dict = {}
|
loop = asyncio.get_running_loop()
|
||||||
while self.__keyboard_proc.is_alive() and self.__mouse_proc.is_alive():
|
wait_for_changes = functools.partial(self.__changes_queue.get, timeout=1)
|
||||||
state = self.get_state()
|
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
|
||||||
if state != prev_state:
|
prev_state: Dict = {}
|
||||||
yield self.get_state()
|
while True:
|
||||||
prev_state = state
|
state = self.get_state()
|
||||||
await asyncio.sleep(self.__state_poll)
|
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
|
||||||
|
|
||||||
async def reset(self) -> None:
|
async def reset(self) -> None:
|
||||||
self.__keyboard_proc.send_reset_event()
|
self.__keyboard_proc.send_reset_event()
|
||||||
|
|||||||
@@ -29,6 +29,9 @@ import queue
|
|||||||
import errno
|
import errno
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
from typing import Dict
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
import setproctitle
|
import setproctitle
|
||||||
|
|
||||||
from ....logging import get_logger
|
from ....logging import get_logger
|
||||||
@@ -43,6 +46,10 @@ class BaseDeviceProcess(multiprocessing.Process): # pylint: disable=too-many-in
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
name: str,
|
name: str,
|
||||||
|
read_size: int,
|
||||||
|
initial_state: Dict,
|
||||||
|
changes_queue: multiprocessing.queues.Queue,
|
||||||
|
|
||||||
device_path: str,
|
device_path: str,
|
||||||
select_timeout: float,
|
select_timeout: float,
|
||||||
write_retries: int,
|
write_retries: int,
|
||||||
@@ -53,6 +60,8 @@ class BaseDeviceProcess(multiprocessing.Process): # pylint: disable=too-many-in
|
|||||||
super().__init__(daemon=True)
|
super().__init__(daemon=True)
|
||||||
|
|
||||||
self.__name = name
|
self.__name = name
|
||||||
|
self.__read_size = read_size
|
||||||
|
self.__changes_queue = changes_queue
|
||||||
|
|
||||||
self.__device_path = device_path
|
self.__device_path = device_path
|
||||||
self.__select_timeout = select_timeout
|
self.__select_timeout = select_timeout
|
||||||
@@ -62,7 +71,7 @@ class BaseDeviceProcess(multiprocessing.Process): # pylint: disable=too-many-in
|
|||||||
|
|
||||||
self.__fd = -1
|
self.__fd = -1
|
||||||
self.__events_queue: multiprocessing.queues.Queue = multiprocessing.Queue()
|
self.__events_queue: multiprocessing.queues.Queue = multiprocessing.Queue()
|
||||||
self.__online_shared = multiprocessing.Value("i", 1)
|
self.__state_shared = multiprocessing.Manager().dict(online=True, **initial_state) # type: ignore
|
||||||
self.__stop_event = multiprocessing.Event()
|
self.__stop_event = multiprocessing.Event()
|
||||||
|
|
||||||
def run(self) -> None:
|
def run(self) -> None:
|
||||||
@@ -75,10 +84,12 @@ class BaseDeviceProcess(multiprocessing.Process): # pylint: disable=too-many-in
|
|||||||
while not self.__stop_event.is_set():
|
while not self.__stop_event.is_set():
|
||||||
try:
|
try:
|
||||||
while not self.__stop_event.is_set():
|
while not self.__stop_event.is_set():
|
||||||
|
if self.__ensure_device(): # Check device and process reports if needed
|
||||||
|
self.__read_all_reports()
|
||||||
try:
|
try:
|
||||||
event: BaseEvent = self.__events_queue.get(timeout=1)
|
event: BaseEvent = self.__events_queue.get(timeout=0.1)
|
||||||
except queue.Empty:
|
except queue.Empty:
|
||||||
self.__ensure_device() # Check device
|
pass
|
||||||
else:
|
else:
|
||||||
self._process_event(event)
|
self._process_event(event)
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -89,8 +100,18 @@ class BaseDeviceProcess(multiprocessing.Process): # pylint: disable=too-many-in
|
|||||||
|
|
||||||
self.__close_device()
|
self.__close_device()
|
||||||
|
|
||||||
def is_online(self) -> bool:
|
def get_state(self) -> Dict:
|
||||||
return bool(self.__online_shared.value and self.is_alive())
|
return dict(self.__state_shared)
|
||||||
|
|
||||||
|
# =====
|
||||||
|
|
||||||
|
def _process_event(self, event: BaseEvent) -> None:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def _process_read_report(self, report: bytes) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# =====
|
||||||
|
|
||||||
def _stop(self) -> None:
|
def _stop(self) -> None:
|
||||||
if self.is_alive():
|
if self.is_alive():
|
||||||
@@ -99,9 +120,6 @@ class BaseDeviceProcess(multiprocessing.Process): # pylint: disable=too-many-in
|
|||||||
if self.exitcode is not None:
|
if self.exitcode is not None:
|
||||||
self.join()
|
self.join()
|
||||||
|
|
||||||
def _process_event(self, event: BaseEvent) -> None:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def _queue_event(self, event: BaseEvent) -> None:
|
def _queue_event(self, event: BaseEvent) -> None:
|
||||||
self.__events_queue.put(event)
|
self.__events_queue.put(event)
|
||||||
|
|
||||||
@@ -116,6 +134,11 @@ class BaseDeviceProcess(multiprocessing.Process): # pylint: disable=too-many-in
|
|||||||
if close:
|
if close:
|
||||||
self.__close_device()
|
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:
|
def __write_report(self, report: bytes) -> bool:
|
||||||
@@ -130,7 +153,7 @@ class BaseDeviceProcess(multiprocessing.Process): # pylint: disable=too-many-in
|
|||||||
try:
|
try:
|
||||||
written = os.write(self.__fd, report)
|
written = os.write(self.__fd, report)
|
||||||
if written == len(report):
|
if written == len(report):
|
||||||
self.__online_shared.value = 1
|
self._update_state("online", True)
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
logger.error("HID-%s write() error: written (%s) != report length (%d)",
|
logger.error("HID-%s write() error: written (%s) != report length (%d)",
|
||||||
@@ -151,6 +174,33 @@ class BaseDeviceProcess(multiprocessing.Process): # pylint: disable=too-many-in
|
|||||||
self.__close_device()
|
self.__close_device()
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def __read_all_reports(self) -> None:
|
||||||
|
if self.__noop or self.__read_size == 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
assert self.__fd >= 0
|
||||||
|
logger = get_logger()
|
||||||
|
|
||||||
|
read = True
|
||||||
|
while read:
|
||||||
|
try:
|
||||||
|
read = bool(select.select([self.__fd], [], [], 0)[0])
|
||||||
|
except Exception as err:
|
||||||
|
logger.error("Can't select() for read HID-%s: %s: %s", self.__name, type(err).__name__, err)
|
||||||
|
break
|
||||||
|
|
||||||
|
if read:
|
||||||
|
try:
|
||||||
|
report = os.read(self.__fd, self.__read_size)
|
||||||
|
except Exception as err:
|
||||||
|
if isinstance(err, OSError) and err.errno == errno.EAGAIN: # pylint: disable=no-member
|
||||||
|
logger.debug("HID-%s busy/unplugged (read): %s: %s",
|
||||||
|
self.__name, type(err).__name__, err)
|
||||||
|
else:
|
||||||
|
logger.exception("Can't read report from HID-%s", self.__name)
|
||||||
|
else:
|
||||||
|
self._process_read_report(report)
|
||||||
|
|
||||||
def __ensure_device(self) -> bool:
|
def __ensure_device(self) -> bool:
|
||||||
if self.__noop:
|
if self.__noop:
|
||||||
return True
|
return True
|
||||||
@@ -159,25 +209,29 @@ class BaseDeviceProcess(multiprocessing.Process): # pylint: disable=too-many-in
|
|||||||
|
|
||||||
if self.__fd < 0:
|
if self.__fd < 0:
|
||||||
try:
|
try:
|
||||||
self.__fd = os.open(self.__device_path, os.O_WRONLY|os.O_NONBLOCK)
|
flags = os.O_NONBLOCK
|
||||||
|
flags |= (os.O_RDWR if self.__read_size else os.O_WRONLY)
|
||||||
|
self.__fd = os.open(self.__device_path, flags)
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
logger.error("Missing HID-%s device: %s", self.__name, self.__device_path)
|
logger.error("Missing HID-%s device: %s", self.__name, self.__device_path)
|
||||||
|
time.sleep(self.__select_timeout)
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
logger.error("Can't open HID-%s device: %s: %s: %s",
|
logger.error("Can't open HID-%s device: %s: %s: %s",
|
||||||
self.__name, self.__device_path, type(err).__name__, err)
|
self.__name, self.__device_path, type(err).__name__, err)
|
||||||
|
time.sleep(self.__select_timeout)
|
||||||
|
|
||||||
if self.__fd >= 0:
|
if self.__fd >= 0:
|
||||||
try:
|
try:
|
||||||
if select.select([], [self.__fd], [], self.__select_timeout)[1]:
|
if select.select([], [self.__fd], [], self.__select_timeout)[1]:
|
||||||
self.__online_shared.value = 1
|
self._update_state("online", True)
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
logger.debug("HID-%s is busy/unplugged (select)", self.__name)
|
logger.debug("HID-%s is busy/unplugged (write select)", self.__name)
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
logger.error("Can't select() HID-%s: %s: %s", self.__name, type(err).__name__, err)
|
logger.error("Can't select() for write HID-%s: %s: %s", self.__name, type(err).__name__, err)
|
||||||
self.__close_device()
|
self.__close_device()
|
||||||
|
|
||||||
self.__online_shared.value = 0
|
self._update_state("online", False)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def __close_device(self) -> None:
|
def __close_device(self) -> None:
|
||||||
|
|||||||
@@ -65,7 +65,12 @@ class _KeyEvent(BaseEvent):
|
|||||||
# =====
|
# =====
|
||||||
class KeyboardProcess(BaseDeviceProcess):
|
class KeyboardProcess(BaseDeviceProcess):
|
||||||
def __init__(self, **kwargs: Any) -> None:
|
def __init__(self, **kwargs: Any) -> None:
|
||||||
super().__init__(name="keyboard", **kwargs)
|
super().__init__(
|
||||||
|
name="keyboard",
|
||||||
|
read_size=1,
|
||||||
|
initial_state={"leds": {"caps": False, "scroll": False, "num": False}},
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
|
||||||
self.__pressed_modifiers: Set[keymap.OtgKey] = set()
|
self.__pressed_modifiers: Set[keymap.OtgKey] = set()
|
||||||
self.__pressed_keys: List[Optional[keymap.OtgKey]] = [None] * 6
|
self.__pressed_keys: List[Optional[keymap.OtgKey]] = [None] * 6
|
||||||
@@ -90,6 +95,17 @@ 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),
|
||||||
|
})
|
||||||
|
|
||||||
|
# =====
|
||||||
|
|
||||||
def _process_event(self, event: BaseEvent) -> None:
|
def _process_event(self, event: BaseEvent) -> None:
|
||||||
if isinstance(event, _ClearEvent):
|
if isinstance(event, _ClearEvent):
|
||||||
self.__process_clear_event()
|
self.__process_clear_event()
|
||||||
|
|||||||
@@ -61,7 +61,12 @@ class _WheelEvent(BaseEvent):
|
|||||||
# =====
|
# =====
|
||||||
class MouseProcess(BaseDeviceProcess):
|
class MouseProcess(BaseDeviceProcess):
|
||||||
def __init__(self, **kwargs: Any) -> None:
|
def __init__(self, **kwargs: Any) -> None:
|
||||||
super().__init__(name="mouse", **kwargs)
|
super().__init__(
|
||||||
|
name="mouse",
|
||||||
|
read_size=0,
|
||||||
|
initial_state={},
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
|
||||||
self.__pressed_buttons: int = 0
|
self.__pressed_buttons: int = 0
|
||||||
self.__x = 0
|
self.__x = 0
|
||||||
|
|||||||
@@ -191,7 +191,7 @@ class Plugin(BaseHid, multiprocessing.Process): # pylint: disable=too-many-inst
|
|||||||
online = bool(self.__online_shared.value)
|
online = bool(self.__online_shared.value)
|
||||||
return {
|
return {
|
||||||
"online": online,
|
"online": online,
|
||||||
"keyboard": {"online": online},
|
"keyboard": {"features": {"leds": False}, "online": online},
|
||||||
"mouse": {"online": online},
|
"mouse": {"online": online},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -158,7 +158,7 @@
|
|||||||
<div class="menu-item-content-text">
|
<div class="menu-item-content-text">
|
||||||
<table>
|
<table>
|
||||||
<tr>
|
<tr>
|
||||||
<td><img src="../share/svg/warning.svg" /></td>
|
<td><img class="sign" src="../share/svg/warning.svg" /></td>
|
||||||
<td><b>Mass Storage Device is offline</b></td>
|
<td><b>Mass Storage Device is offline</b></td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
@@ -169,7 +169,7 @@
|
|||||||
<div class="menu-item-content-text">
|
<div class="menu-item-content-text">
|
||||||
<table>
|
<table>
|
||||||
<tr>
|
<tr>
|
||||||
<td><img src="../share/svg/warning.svg" /></td>
|
<td><img class="sign" src="../share/svg/warning.svg" /></td>
|
||||||
<td><b>Current image is broken!</b><br><sub>Perhaps uploading was interrupted</sub></td>
|
<td><b>Current image is broken!</b><br><sub>Perhaps uploading was interrupted</sub></td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
@@ -180,7 +180,7 @@
|
|||||||
<div class="menu-item-content-text">
|
<div class="menu-item-content-text">
|
||||||
<table>
|
<table>
|
||||||
<tr>
|
<tr>
|
||||||
<td><img src="../share/svg/warning.svg" /></td>
|
<td><img class="sign" src="../share/svg/warning.svg" /></td>
|
||||||
<td><b>Current image is too big for CD-ROM!</b><br><sub>The device filesystem will be truncated to 2.2GiB</sub></td>
|
<td><b>Current image is too big for CD-ROM!</b><br><sub>The device filesystem will be truncated to 2.2GiB</sub></td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
@@ -191,7 +191,7 @@
|
|||||||
<div class="menu-item-content-text">
|
<div class="menu-item-content-text">
|
||||||
<table>
|
<table>
|
||||||
<tr>
|
<tr>
|
||||||
<td><img src="../share/svg/info.svg" /></td>
|
<td><img class="sign" src="../share/svg/info.svg" /></td>
|
||||||
<td><b>Current image is out of storage</b><br><sub>This image was connected manually using <b>kvmd-otgmsd</b></sub></td>
|
<td><b>Current image is out of storage</b><br><sub>This image was connected manually using <b>kvmd-otgmsd</b></sub></td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
@@ -202,7 +202,7 @@
|
|||||||
<div class="menu-item-content-text">
|
<div class="menu-item-content-text">
|
||||||
<table>
|
<table>
|
||||||
<tr>
|
<tr>
|
||||||
<td><img src="../share/svg/info.svg" /></td>
|
<td><img class="sign" src="../share/svg/info.svg" /></td>
|
||||||
<td><b>Another user uploads an image</b></td>
|
<td><b>Another user uploads an image</b></td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
@@ -311,7 +311,11 @@
|
|||||||
<button disabled data-force-hide-menu id="hid-pak-button">• ↳ Paste-as-Keys <sup><i>ascii-only</i></sup></button>
|
<button disabled data-force-hide-menu id="hid-pak-button">• ↳ Paste-as-Keys <sup><i>ascii-only</i></sup></button>
|
||||||
<hr>
|
<hr>
|
||||||
<div class="buttons-row">
|
<div class="buttons-row">
|
||||||
<button data-force-hide-menu data-shortcut="CapsLock" class="row50">• CapsLock</button>
|
<button data-force-hide-menu data-shortcut="CapsLock" class="row50">
|
||||||
|
•
|
||||||
|
<img class="inline hid-keyboard-leds hid-keyboard-caps-led led-gray feature-disabled" src="../share/svg/led-square.svg" />
|
||||||
|
CapsLock
|
||||||
|
</button>
|
||||||
<button data-force-hide-menu data-shortcut="MetaLeft" class="row50">• Left Win</button>
|
<button data-force-hide-menu data-shortcut="MetaLeft" class="row50">• Left Win</button>
|
||||||
</div>
|
</div>
|
||||||
<hr>
|
<hr>
|
||||||
@@ -427,7 +431,10 @@
|
|||||||
<div data-code="Backslash" class="key"><p>|<br>\</p></div>
|
<div data-code="Backslash" class="key"><p>|<br>\</p></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="keypad-row">
|
<div class="keypad-row">
|
||||||
<div data-code="CapsLock" class="key wide-3 left small"><p>Caps Lock</p></div>
|
<div data-code="CapsLock" class="key wide-3 left small">
|
||||||
|
<img class="inline hid-keyboard-leds hid-keyboard-caps-led led-gray feature-disabled" src="../share/svg/led-square.svg" />
|
||||||
|
<p>Caps Lock</p>
|
||||||
|
</div>
|
||||||
<div data-code="KeyA" class="key single"><p>A</p></div>
|
<div data-code="KeyA" class="key single"><p>A</p></div>
|
||||||
<div data-code="KeyS" class="key single"><p>S</p></div>
|
<div data-code="KeyS" class="key single"><p>S</p></div>
|
||||||
<div data-code="KeyD" class="key single"><p>D</p></div>
|
<div data-code="KeyD" class="key single"><p>D</p></div>
|
||||||
@@ -468,7 +475,10 @@
|
|||||||
<div class="keypad-block">
|
<div class="keypad-block">
|
||||||
<div class="keypad-row">
|
<div class="keypad-row">
|
||||||
<div data-code="PrintScreen" class="modifier small"><p><b>•</b><br>Pt/Sq</p></div>
|
<div data-code="PrintScreen" class="modifier small"><p><b>•</b><br>Pt/Sq</p></div>
|
||||||
<div data-code="ScrollLock" class="key small"><p>ScrLk</p></div>
|
<div data-code="ScrollLock" class="key small">
|
||||||
|
<img class="inline hid-keyboard-leds hid-keyboard-scroll-led led-gray feature-disabled" src="../share/svg/led-square.svg" />
|
||||||
|
<p>ScrLk</p>
|
||||||
|
</div>
|
||||||
<div data-code="Pause" class="key small"><p>P/Brk</p></div>
|
<div data-code="Pause" class="key small"><p>P/Brk</p></div>
|
||||||
</div>
|
</div>
|
||||||
<hr>
|
<hr>
|
||||||
@@ -515,7 +525,10 @@
|
|||||||
<div data-code="F12" class="key wide-0 margin-0 small"><p>F12</p></div>
|
<div data-code="F12" class="key wide-0 margin-0 small"><p>F12</p></div>
|
||||||
<div class="empty-key" style="width:5px"></div>
|
<div class="empty-key" style="width:5px"></div>
|
||||||
<div data-code="PrintScreen" class="modifier margin-0 small"><p><b>•</b><br>Pt/Sq</p></div>
|
<div data-code="PrintScreen" class="modifier margin-0 small"><p><b>•</b><br>Pt/Sq</p></div>
|
||||||
<div data-code="ScrollLock" class="key margin-0 small"><p>ScrLk</p></div>
|
<div data-code="ScrollLock" class="key margin-0 small">
|
||||||
|
<img class="inline hid-keyboard-leds hid-keyboard-scroll-led led-gray feature-disabled" src="../share/svg/led-square.svg" />
|
||||||
|
<p>ScrLk</p>
|
||||||
|
</div>
|
||||||
<div data-code="Pause" class="key margin-0 small"><p>P/Brk</p></div>
|
<div data-code="Pause" class="key margin-0 small"><p>P/Brk</p></div>
|
||||||
<div data-code="Insert" class="key margin-0 small"><p>Ins</p></div>
|
<div data-code="Insert" class="key margin-0 small"><p>Ins</p></div>
|
||||||
<div data-code="Home" class="key margin-0 small"><p>Home</p></div>
|
<div data-code="Home" class="key margin-0 small"><p>Home</p></div>
|
||||||
@@ -556,7 +569,10 @@
|
|||||||
<div data-code="Backslash" class="key wide-2 left" style="width:78px"><p>|<br>\</p></div>
|
<div data-code="Backslash" class="key wide-2 left" style="width:78px"><p>|<br>\</p></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="keypad-row">
|
<div class="keypad-row">
|
||||||
<div data-code="CapsLock" class="key wide-3 left small"><p>Caps Lock</p></div>
|
<div data-code="CapsLock" class="key wide-3 left small">
|
||||||
|
<img class="inline hid-keyboard-leds hid-keyboard-caps-led led-gray feature-disabled" src="../share/svg/led-square.svg" />
|
||||||
|
<p>Caps Lock</p>
|
||||||
|
</div>
|
||||||
<div data-code="KeyA" class="key single"><p>A</p></div>
|
<div data-code="KeyA" class="key single"><p>A</p></div>
|
||||||
<div data-code="KeyS" class="key single"><p>S</p></div>
|
<div data-code="KeyS" class="key single"><p>S</p></div>
|
||||||
<div data-code="KeyD" class="key single"><p>D</p></div>
|
<div data-code="KeyD" class="key single"><p>D</p></div>
|
||||||
|
|||||||
@@ -80,6 +80,10 @@ img.svg-gray {
|
|||||||
filter: invert(0.7);
|
filter: invert(0.7);
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
img.inline {
|
||||||
|
vertical-align: middle;
|
||||||
|
height: 0.8em;
|
||||||
|
}
|
||||||
|
|
||||||
button,
|
button,
|
||||||
select {
|
select {
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ ul#menu li a#menu-logo {
|
|||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
ul#menu img {
|
ul#menu li a.menu-item img {
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
@@ -150,3 +150,9 @@ ul#menu li div.menu-item-content hr {
|
|||||||
border: none;
|
border: none;
|
||||||
border-top: var(--border-control-thin);
|
border-top: var(--border-control-thin);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ul#menu li div.menu-item-content img.sign {
|
||||||
|
vertical-align: middle;
|
||||||
|
margin-right: 10px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
|
|
||||||
|
|
||||||
import {tools, $} from "../tools.js";
|
import {tools, $, $$$} from "../tools.js";
|
||||||
import {Keypad} from "../keypad.js";
|
import {Keypad} from "../keypad.js";
|
||||||
|
|
||||||
|
|
||||||
@@ -42,16 +42,16 @@ export function Keyboard() {
|
|||||||
|
|
||||||
$("keyboard-window").onkeydown = (event) => __keyboardHandler(event, true);
|
$("keyboard-window").onkeydown = (event) => __keyboardHandler(event, true);
|
||||||
$("keyboard-window").onkeyup = (event) => __keyboardHandler(event, false);
|
$("keyboard-window").onkeyup = (event) => __keyboardHandler(event, false);
|
||||||
$("keyboard-window").onfocus = __updateLeds;
|
$("keyboard-window").onfocus = __updateOnlineLeds;
|
||||||
$("keyboard-window").onblur = __updateLeds;
|
$("keyboard-window").onblur = __updateOnlineLeds;
|
||||||
|
|
||||||
$("stream-window").onkeydown = (event) => __keyboardHandler(event, true);
|
$("stream-window").onkeydown = (event) => __keyboardHandler(event, true);
|
||||||
$("stream-window").onkeyup = (event) => __keyboardHandler(event, false);
|
$("stream-window").onkeyup = (event) => __keyboardHandler(event, false);
|
||||||
$("stream-window").onfocus = __updateLeds;
|
$("stream-window").onfocus = __updateOnlineLeds;
|
||||||
$("stream-window").onblur = __updateLeds;
|
$("stream-window").onblur = __updateOnlineLeds;
|
||||||
|
|
||||||
window.addEventListener("focusin", __updateLeds);
|
window.addEventListener("focusin", __updateOnlineLeds);
|
||||||
window.addEventListener("focusout", __updateLeds);
|
window.addEventListener("focusout", __updateOnlineLeds);
|
||||||
|
|
||||||
if (tools.browser.is_mac) {
|
if (tools.browser.is_mac) {
|
||||||
// https://bugs.chromium.org/p/chromium/issues/detail?id=28089
|
// https://bugs.chromium.org/p/chromium/issues/detail?id=28089
|
||||||
@@ -68,12 +68,29 @@ export function Keyboard() {
|
|||||||
self.releaseAll();
|
self.releaseAll();
|
||||||
__ws = ws;
|
__ws = ws;
|
||||||
}
|
}
|
||||||
__updateLeds();
|
__updateOnlineLeds();
|
||||||
};
|
};
|
||||||
|
|
||||||
self.setState = function(state) {
|
self.setState = function(state) {
|
||||||
__online = state.online;
|
__online = state.online;
|
||||||
__updateLeds();
|
__updateOnlineLeds();
|
||||||
|
|
||||||
|
for (let el of $$$(".hid-keyboard-leds")) {
|
||||||
|
console.log(el, state.features.leds);
|
||||||
|
el.classList.toggle("feature-disabled", !state.features.leds);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let led of ["caps", "scroll", "num"]) {
|
||||||
|
for (let el of $$$(`.hid-keyboard-${led}-led`)) {
|
||||||
|
if (state.leds[led]) {
|
||||||
|
el.classList.add("led-green");
|
||||||
|
el.classList.remove("led-gray");
|
||||||
|
} else {
|
||||||
|
el.classList.add("led-gray");
|
||||||
|
el.classList.remove("led-green");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
self.releaseAll = function() {
|
self.releaseAll = function() {
|
||||||
@@ -84,7 +101,7 @@ export function Keyboard() {
|
|||||||
__keyboardHandler({code: code}, state);
|
__keyboardHandler({code: code}, state);
|
||||||
};
|
};
|
||||||
|
|
||||||
var __updateLeds = function() {
|
var __updateOnlineLeds = function() {
|
||||||
let is_captured = (
|
let is_captured = (
|
||||||
$("stream-window").classList.contains("window-active")
|
$("stream-window").classList.contains("window-active")
|
||||||
|| $("keyboard-window").classList.contains("window-active")
|
|| $("keyboard-window").classList.contains("window-active")
|
||||||
|
|||||||
@@ -65,12 +65,12 @@ export function Mouse() {
|
|||||||
self.setSocket = function(ws) {
|
self.setSocket = function(ws) {
|
||||||
__ws = ws;
|
__ws = ws;
|
||||||
$("stream-box").classList.toggle("stream-box-mouse-enabled", ws);
|
$("stream-box").classList.toggle("stream-box-mouse-enabled", ws);
|
||||||
__updateLeds();
|
__updateOnlineLeds();
|
||||||
};
|
};
|
||||||
|
|
||||||
self.setState = function(state) {
|
self.setState = function(state) {
|
||||||
__online = state.online;
|
__online = state.online;
|
||||||
__updateLeds();
|
__updateOnlineLeds();
|
||||||
};
|
};
|
||||||
|
|
||||||
self.releaseAll = function() {
|
self.releaseAll = function() {
|
||||||
@@ -79,15 +79,15 @@ export function Mouse() {
|
|||||||
|
|
||||||
var __hoverStream = function() {
|
var __hoverStream = function() {
|
||||||
__stream_hovered = true;
|
__stream_hovered = true;
|
||||||
__updateLeds();
|
__updateOnlineLeds();
|
||||||
};
|
};
|
||||||
|
|
||||||
var __leaveStream = function() {
|
var __leaveStream = function() {
|
||||||
__stream_hovered = false;
|
__stream_hovered = false;
|
||||||
__updateLeds();
|
__updateOnlineLeds();
|
||||||
};
|
};
|
||||||
|
|
||||||
var __updateLeds = function() {
|
var __updateOnlineLeds = function() {
|
||||||
let is_captured = (__stream_hovered || tools.browser.is_ios);
|
let is_captured = (__stream_hovered || tools.browser.is_ios);
|
||||||
let led = "led-gray";
|
let led = "led-gray";
|
||||||
let title = "Mouse free";
|
let title = "Mouse free";
|
||||||
|
|||||||
111
web/share/svg/led-square.svg
Normal file
111
web/share/svg/led-square.svg
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
|
||||||
|
<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"
|
||||||
|
version="1.1"
|
||||||
|
id="Layer_1"
|
||||||
|
x="0px"
|
||||||
|
y="0px"
|
||||||
|
viewBox="0 0 512 512"
|
||||||
|
style="enable-background:new 0 0 512 512;"
|
||||||
|
xml:space="preserve"
|
||||||
|
sodipodi:docname="led-square.svg"
|
||||||
|
inkscape:version="0.92.4 5da689c313, 2019-01-14"><metadata
|
||||||
|
id="metadata85"><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><defs
|
||||||
|
id="defs83">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</defs><sodipodi:namedview
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1"
|
||||||
|
objecttolerance="10"
|
||||||
|
gridtolerance="10"
|
||||||
|
guidetolerance="10"
|
||||||
|
inkscape:pageopacity="0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="1020"
|
||||||
|
id="namedview81"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:zoom="0.921875"
|
||||||
|
inkscape:cx="223.44119"
|
||||||
|
inkscape:cy="239.43047"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="30"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="Layer_1" />
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<path
|
||||||
|
d="M 460,0 H 52 C 23.28,0 0,23.28 0,52 v 408 c 0,28.72 23.28,52 52,52 h 408 c 28.72,0 52,-23.28 52,-52 V 52 C 512,23.28 488.72,0 460,0 Z"
|
||||||
|
id="path20"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
sodipodi:nodetypes="sssssssss" />
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<g
|
||||||
|
id="g50">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g52">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g54">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g56">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g58">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g60">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g62">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g64">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g66">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g68">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g70">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g72">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g74">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g76">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g78">
|
||||||
|
</g>
|
||||||
|
<rect
|
||||||
|
style="opacity:1;fill:none;fill-opacity:1;paint-order:normal"
|
||||||
|
id="rect4830"
|
||||||
|
width="478.37289"
|
||||||
|
height="351.45764"
|
||||||
|
x="26.033897"
|
||||||
|
y="58.576271" /></svg>
|
||||||
|
After Width: | Height: | Size: 2.2 KiB |
Reference in New Issue
Block a user