mofeng-git 7b3335ea94 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
2025-02-01 01:08:36 +00:00

296 lines
8.8 KiB
Python

# ========================================================================== #
# #
# KVMD - The main PiKVM daemon. #
# #
# Copyright (C) 2018-2024 Maxim Devaev <mdevaev@gmail.com> #
# #
# This program is free software: you can redistribute it and/or modify #
# it under the terms of the GNU General Public License as published by #
# the Free Software Foundation, either version 3 of the License, or #
# (at your option) any later version. #
# #
# This program is distributed in the hope that it will be useful, #
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
# GNU General Public License for more details. #
# #
# You should have received a copy of the GNU General Public License #
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
# #
# ========================================================================== #
import struct
import dataclasses
from typing import Optional
from .types import Edid
from .types import Colors
# =====
class Packable:
def pack(self) -> bytes:
raise NotImplementedError()
class Unpackable:
@classmethod
def unpack(cls, data: bytes, offset: int=0) -> "Unpackable":
raise NotImplementedError()
# =====
@dataclasses.dataclass(frozen=True)
class Header(Packable, Unpackable):
proto: int
rid: int
op: int
unit: int
NAK = 0
BOOTLOADER = 2
REBOOT = 3
STATE = 4
SWITCH = 5
BEACON = 6
ATX_LEDS = 7
ATX_CLICK = 8
SET_EDID = 9
CLEAR_EDID = 10
SET_COLORS = 12
__struct = struct.Struct("<BHBB")
SIZE = __struct.size
def pack(self) -> bytes:
return self.__struct.pack(self.proto, self.rid, self.op, self.unit)
@classmethod
def unpack(cls, data: bytes, offset: int=0) -> "Header":
return Header(*cls.__struct.unpack_from(data, offset=offset))
@dataclasses.dataclass(frozen=True)
class Nak(Unpackable):
reason: int
INVALID_COMMAND = 0
BUSY = 1
NO_DOWNLINK = 2
DOWNLINK_OVERFLOW = 3
__struct = struct.Struct("<B")
@classmethod
def unpack(cls, data: bytes, offset: int=0) -> "Nak":
return Nak(*cls.__struct.unpack_from(data, offset=offset))
@dataclasses.dataclass(frozen=True)
class UnitFlags:
changing_busy: bool
flashing_busy: bool
has_downlink: bool
@dataclasses.dataclass(frozen=True)
class UnitState(Unpackable): # pylint: disable=too-many-instance-attributes
sw_version: int
hw_version: int
flags: UnitFlags
ch: int
beacons: tuple[bool, bool, bool, bool, bool, bool]
np_crc: tuple[int, int, int, int, int, int]
video_5v_sens: tuple[bool, bool, bool, bool, bool]
video_hpd: tuple[bool, bool, bool, bool, bool]
video_edid: tuple[bool, bool, bool, bool]
video_crc: tuple[int, int, int, int]
usb_5v_sens: tuple[bool, bool, bool, bool]
atx_busy: tuple[bool, bool, bool, bool]
__struct = struct.Struct("<HHHBBHHHHHHBBBHHHHBxB30x")
def compare_edid(self, ch: int, edid: Optional["Edid"]) -> bool:
if edid is None:
# Сойдет любой невалидный EDID
return (not self.video_edid[ch])
return (
self.video_edid[ch] == edid.valid
and self.video_crc[ch] == edid.crc
)
@classmethod
def unpack(cls, data: bytes, offset: int=0) -> "UnitState": # pylint: disable=too-many-locals
(
sw_version, hw_version, flags, ch,
beacons, nc0, nc1, nc2, nc3, nc4, nc5,
video_5v_sens, video_hpd, video_edid, vc0, vc1, vc2, vc3,
usb_5v_sens, atx_busy,
) = cls.__struct.unpack_from(data, offset=offset)
return UnitState(
sw_version,
hw_version,
flags=UnitFlags(
changing_busy=bool(flags & 0x80),
flashing_busy=bool(flags & 0x40),
has_downlink=bool(flags & 0x02),
),
ch=ch,
beacons=cls.__make_flags6(beacons),
np_crc=(nc0, nc1, nc2, nc3, nc4, nc5),
video_5v_sens=cls.__make_flags5(video_5v_sens),
video_hpd=cls.__make_flags5(video_hpd),
video_edid=cls.__make_flags4(video_edid),
video_crc=(vc0, vc1, vc2, vc3),
usb_5v_sens=cls.__make_flags4(usb_5v_sens),
atx_busy=cls.__make_flags4(atx_busy),
)
@classmethod
def __make_flags6(cls, mask: int) -> tuple[bool, bool, bool, bool, bool, bool]:
return (
bool(mask & 0x01), bool(mask & 0x02), bool(mask & 0x04),
bool(mask & 0x08), bool(mask & 0x10), bool(mask & 0x20),
)
@classmethod
def __make_flags5(cls, mask: int) -> tuple[bool, bool, bool, bool, bool]:
return (
bool(mask & 0x01), bool(mask & 0x02), bool(mask & 0x04),
bool(mask & 0x08), bool(mask & 0x10),
)
@classmethod
def __make_flags4(cls, mask: int) -> tuple[bool, bool, bool, bool]:
return (bool(mask & 0x01), bool(mask & 0x02), bool(mask & 0x04), bool(mask & 0x08))
@dataclasses.dataclass(frozen=True)
class UnitAtxLeds(Unpackable):
power: tuple[bool, bool, bool, bool]
hdd: tuple[bool, bool, bool, bool]
__struct = struct.Struct("<B")
@classmethod
def unpack(cls, data: bytes, offset: int=0) -> "UnitAtxLeds":
(mask,) = cls.__struct.unpack_from(data, offset=offset)
return UnitAtxLeds(
power=(bool(mask & 0x01), bool(mask & 0x02), bool(mask & 0x04), bool(mask & 0x08)),
hdd=(bool(mask & 0x10), bool(mask & 0x20), bool(mask & 0x40), bool(mask & 0x80)),
)
# =====
@dataclasses.dataclass(frozen=True)
class BodySwitch(Packable):
ch: int
def __post_init__(self) -> None:
assert 0 <= self.ch <= 4
def pack(self) -> bytes:
return self.ch.to_bytes()
@dataclasses.dataclass(frozen=True)
class BodySetBeacon(Packable):
ch: int
on: bool
def __post_init__(self) -> None:
assert 0 <= self.ch <= 5
def pack(self) -> bytes:
return self.ch.to_bytes() + self.on.to_bytes()
@dataclasses.dataclass(frozen=True)
class BodyAtxClick(Packable):
ch: int
action: int
delay_ms: int
POWER = 0
RESET = 1
__struct = struct.Struct("<BBH")
def __post_init__(self) -> None:
assert 0 <= self.ch <= 3
assert self.action in [self.POWER, self.RESET]
assert 1 <= self.delay_ms <= 0xFFFF
def pack(self) -> bytes:
return self.__struct.pack(self.ch, self.action, self.delay_ms)
@dataclasses.dataclass(frozen=True)
class BodySetEdid(Packable):
ch: int
edid: Edid
def __post_init__(self) -> None:
assert 0 <= self.ch <= 3
def pack(self) -> bytes:
return self.ch.to_bytes() + self.edid.pack()
@dataclasses.dataclass(frozen=True)
class BodyClearEdid(Packable):
ch: int
def __post_init__(self) -> None:
assert 0 <= self.ch <= 3
def pack(self) -> bytes:
return self.ch.to_bytes()
@dataclasses.dataclass(frozen=True)
class BodySetColors(Packable):
ch: int
colors: Colors
def __post_init__(self) -> None:
assert 0 <= self.ch <= 5
def pack(self) -> bytes:
return self.ch.to_bytes() + self.colors.pack()
# =====
@dataclasses.dataclass(frozen=True)
class Request:
header: Header
body: (Packable | None) = dataclasses.field(default=None)
def pack(self) -> bytes:
msg = self.header.pack()
if self.body is not None:
msg += self.body.pack()
return msg
@dataclasses.dataclass(frozen=True)
class Response:
header: Header
body: Unpackable
@classmethod
def unpack(cls, msg: bytes) -> Optional["Response"]:
header = Header.unpack(msg)
match header.op:
case Header.NAK:
return Response(header, Nak.unpack(msg, Header.SIZE))
case Header.STATE:
return Response(header, UnitState.unpack(msg, Header.SIZE))
case Header.ATX_LEDS:
return Response(header, UnitAtxLeds.unpack(msg, Header.SIZE))
# raise RuntimeError(f"Unknown OP in the header: {header!r}")
return None