Add support for PiKVM Switch and related features

This commit introduces several new components and improvements:
- Added Switch module with firmware update and configuration support
- Implemented new media streaming capabilities
- Updated various UI elements and CSS styles
- Enhanced keyboard and mouse event handling
- Added new validators and configuration options
- Updated Python version support to 3.13
- Improved error handling and logging
This commit is contained in:
mofeng-git
2025-02-01 01:08:36 +00:00
parent 5db37797ea
commit 7b3335ea94
117 changed files with 5342 additions and 479 deletions

View File

@@ -37,6 +37,7 @@ from ...validators.basic import valid_string_list
from ...validators.hid import valid_hid_key
from ...validators.hid import valid_hid_mouse_move
from ...keyboard.mappings import WebModifiers
from ...mouse import MouseRange
from .. import BasePlugin
@@ -64,11 +65,13 @@ class BaseHid(BasePlugin): # pylint: disable=too-many-instance-attributes
self.__mouse_x_range = (mouse_x_min, mouse_x_max)
self.__mouse_y_range = (mouse_y_min, mouse_y_max)
self.__jiggler_enabled = jiggler_enabled
self.__jiggler_active = jiggler_active
self.__jiggler_interval = jiggler_interval
self.__jiggler_absolute = True
self.__activity_ts = 0
self.__j_enabled = jiggler_enabled
self.__j_active = jiggler_active
self.__j_interval = jiggler_interval
self.__j_absolute = True
self.__j_activity_ts = 0
self.__j_last_x = 0
self.__j_last_y = 0
@classmethod
def _get_base_options(cls) -> dict[str, Any]:
@@ -83,7 +86,7 @@ class BaseHid(BasePlugin): # pylint: disable=too-many-instance-attributes
"max": Option(MouseRange.MAX, type=valid_hid_mouse_move, unpack_as="mouse_y_max"),
},
"jiggler": {
"enabled": Option(False, type=valid_bool, unpack_as="jiggler_enabled"),
"enabled": Option(True, type=valid_bool, unpack_as="jiggler_enabled"),
"active": Option(False, type=valid_bool, unpack_as="jiggler_active"),
"interval": Option(60, type=valid_int_f1, unpack_as="jiggler_interval"),
},
@@ -137,13 +140,25 @@ class BaseHid(BasePlugin): # pylint: disable=too-many-instance-attributes
# =====
def send_key_events(self, keys: Iterable[tuple[str, bool]], no_ignore_keys: bool=False) -> None:
async def send_key_events(
self,
keys: Iterable[tuple[str, bool]],
no_ignore_keys: bool=False,
slow: bool=False,
) -> None:
for (key, state) in keys:
if no_ignore_keys or key not in self.__ignore_keys:
self.send_key_event(key, state)
if slow:
await asyncio.sleep(0.02)
self.send_key_event(key, state, False)
def send_key_event(self, key: str, state: bool) -> None:
def send_key_event(self, key: str, state: bool, finish: bool) -> None:
self._send_key_event(key, state)
if state and finish and (key not in WebModifiers.ALL and key != "PrintScreen"):
# Считаем что PrintScreen это модификатор для Alt+SysRq+...
# По-хорошему надо учитывать факт нажатия на Alt, но можно и забить.
self._send_key_event(key, False)
self.__bump_activity()
def _send_key_event(self, key: str, state: bool) -> None:
@@ -161,6 +176,8 @@ class BaseHid(BasePlugin): # pylint: disable=too-many-instance-attributes
# =====
def send_mouse_move_event(self, to_x: int, to_y: int) -> None:
self.__j_last_x = to_x
self.__j_last_y = to_y
if self.__mouse_x_range != MouseRange.RANGE:
to_x = MouseRange.remap(to_x, *self.__mouse_x_range)
if self.__mouse_y_range != MouseRange.RANGE:
@@ -229,37 +246,38 @@ class BaseHid(BasePlugin): # pylint: disable=too-many-instance-attributes
handler(*xy)
def __bump_activity(self) -> None:
self.__activity_ts = int(time.monotonic())
self.__j_activity_ts = int(time.monotonic())
def _set_jiggler_absolute(self, absolute: bool) -> None:
self.__jiggler_absolute = absolute
self.__j_absolute = absolute
def _set_jiggler_active(self, active: bool) -> None:
if self.__jiggler_enabled:
self.__jiggler_active = active
if self.__j_enabled:
self.__j_active = active
def _get_jiggler_state(self) -> dict:
return {
"jiggler": {
"enabled": self.__jiggler_enabled,
"active": self.__jiggler_active,
"interval": self.__jiggler_interval,
"enabled": self.__j_enabled,
"active": self.__j_active,
"interval": self.__j_interval,
},
}
# =====
async def systask(self) -> None:
factor = 1
while True:
if self.__jiggler_active and (self.__activity_ts + self.__jiggler_interval < int(time.monotonic())):
for _ in range(5):
if self.__jiggler_absolute:
self.send_mouse_move_event(100 * factor, 100 * factor)
else:
self.send_mouse_relative_event(10 * factor, 10 * factor)
factor *= -1
await asyncio.sleep(0.1)
if self.__j_active and (self.__j_activity_ts + self.__j_interval < int(time.monotonic())):
if self.__j_absolute:
(x, y) = (self.__j_last_x, self.__j_last_y)
for move in [100, -100, 100, -100, 0]:
self.send_mouse_move_event(MouseRange.normalize(x + move), MouseRange.normalize(y + move))
await asyncio.sleep(0.1)
else:
for move in [10, -10, 10, -10]:
self.send_mouse_relative_event(move, move)
await asyncio.sleep(0.1)
await asyncio.sleep(1)

View File

@@ -26,6 +26,7 @@ import struct
from ....keyboard.mappings import KEYMAP
from ....mouse import MouseRange
from ....mouse import MouseDelta
from .... import tools
from .... import bitbang
@@ -162,8 +163,8 @@ class MouseRelativeEvent(BaseEvent):
delta_y: int
def __post_init__(self) -> None:
assert -127 <= self.delta_x <= 127
assert -127 <= self.delta_y <= 127
assert MouseDelta.MIN <= self.delta_x <= MouseDelta.MAX
assert MouseDelta.MIN <= self.delta_y <= MouseDelta.MAX
def make_request(self) -> bytes:
return _make_request(struct.pack(">Bbbxx", 0x15, self.delta_x, self.delta_y))
@@ -175,8 +176,8 @@ class MouseWheelEvent(BaseEvent):
delta_y: int
def __post_init__(self) -> None:
assert -127 <= self.delta_x <= 127
assert -127 <= self.delta_y <= 127
assert MouseDelta.MIN <= self.delta_x <= MouseDelta.MAX
assert MouseDelta.MIN <= self.delta_y <= MouseDelta.MAX
def make_request(self) -> bytes:
# Горизонтальная прокрутка пока не поддерживается

View File

@@ -23,6 +23,7 @@
import math
from ....mouse import MouseRange
from ....mouse import MouseDelta
# =====
@@ -79,7 +80,7 @@ class Mouse: # pylint: disable=too-many-instance-attributes
def process_wheel(self, delta_x: int, delta_y: int) -> bytes:
_ = delta_x
assert -127 <= delta_y <= 127
assert MouseDelta.MIN <= delta_y <= MouseDelta.MAX
self.__wheel_y = (1 if delta_y > 0 else 255)
if not self.__absolute:
return self.__make_relative_cmd()
@@ -110,6 +111,6 @@ class Mouse: # pylint: disable=too-many-instance-attributes
])
def __fix_relative(self, value: int) -> int:
assert -127 <= value <= 127
assert MouseDelta.MIN <= value <= MouseDelta.MAX
value = math.ceil(value / 3)
return (value if value >= 0 else (255 + value))

View File

@@ -27,6 +27,7 @@ from ....keyboard.mappings import UsbKey
from ....keyboard.mappings import KEYMAP
from ....mouse import MouseRange
from ....mouse import MouseDelta
# =====
@@ -144,8 +145,8 @@ class MouseRelativeEvent(BaseEvent):
delta_y: int
def __post_init__(self) -> None:
assert -127 <= self.delta_x <= 127
assert -127 <= self.delta_y <= 127
assert MouseDelta.MIN <= self.delta_x <= MouseDelta.MAX
assert MouseDelta.MIN <= self.delta_y <= MouseDelta.MAX
@dataclasses.dataclass(frozen=True)
@@ -154,8 +155,8 @@ class MouseWheelEvent(BaseEvent):
delta_y: int
def __post_init__(self) -> None:
assert -127 <= self.delta_x <= 127
assert -127 <= self.delta_y <= 127
assert MouseDelta.MIN <= self.delta_x <= MouseDelta.MAX
assert MouseDelta.MIN <= self.delta_y <= MouseDelta.MAX
def make_mouse_report(

View File

@@ -153,7 +153,6 @@ class MouseProcess(BaseDeviceProcess):
move_x = self.__x
move_y = self.__y
else:
assert self.__x == self.__y == 0
if relative_event is not None:
move_x = relative_event.delta_x
move_y = relative_event.delta_y
@@ -177,5 +176,3 @@ class MouseProcess(BaseDeviceProcess):
def __clear_state(self) -> None:
self.__pressed_buttons = 0
self.__x = 0
self.__y = 0