mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2025-12-14 02:00:32 +08:00
allow short edids, import full edid on with kvmd-edidconf
This commit is contained in:
parent
2c4f7f1458
commit
b86f4cd437
@ -61,23 +61,17 @@ def _print_edid(edid: Edid) -> None:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def _read_out2_edid() -> (Edid | None):
|
def _find_out2_edid_path() -> str:
|
||||||
card = os.path.basename(os.readlink("/dev/dri/by-path/platform-gpu-card"))
|
card = os.path.basename(os.readlink("/dev/dri/by-path/platform-gpu-card"))
|
||||||
path = f"/sys/devices/platform/gpu/drm/{card}/{card}-HDMI-A-2"
|
path = f"/sys/devices/platform/gpu/drm/{card}/{card}-HDMI-A-2"
|
||||||
with open(os.path.join(path, "status")) as file:
|
with open(os.path.join(path, "status")) as file:
|
||||||
if file.read().startswith("d"):
|
if file.read().startswith("d"):
|
||||||
return None
|
raise SystemExit("No display found")
|
||||||
with open(os.path.join(path, "edid"), "rb") as file:
|
return os.path.join(path, "edid")
|
||||||
data = file.read()
|
|
||||||
if len(data) == 0:
|
|
||||||
return None
|
|
||||||
return Edid.from_file(os.path.join(path, "edid"), allow_short=True)
|
|
||||||
|
|
||||||
|
|
||||||
def _adopt_out2_ids(dest: Edid) -> None:
|
def _adopt_out2_ids(dest: Edid) -> None:
|
||||||
src = _read_out2_edid()
|
src = Edid.from_file(_find_out2_edid_path())
|
||||||
if src is None:
|
|
||||||
raise SystemExit("No display found")
|
|
||||||
dest.set_monitor_name(src.get_monitor_name())
|
dest.set_monitor_name(src.get_monitor_name())
|
||||||
try:
|
try:
|
||||||
dest.get_monitor_serial()
|
dest.get_monitor_serial()
|
||||||
@ -123,7 +117,9 @@ def main(argv: (list[str] | None)=None) -> None: # pylint: disable=too-many-bra
|
|||||||
parser.add_argument("--import-preset", choices=presets,
|
parser.add_argument("--import-preset", choices=presets,
|
||||||
help="Restore default EDID or choose the preset", metavar=f"{{ {' | '.join(presets)} }}",)
|
help="Restore default EDID or choose the preset", metavar=f"{{ {' | '.join(presets)} }}",)
|
||||||
parser.add_argument("--import-display-ids", action="store_true",
|
parser.add_argument("--import-display-ids", action="store_true",
|
||||||
help="On PiKVM V4, import and adopt IDs from physical display connected to OUT2")
|
help="On PiKVM V4, import and adopt IDs from a physical display connected to the OUT2 port")
|
||||||
|
parser.add_argument("--import-display", action="store_true",
|
||||||
|
help="On PiKVM V4, import full EDID from a physical display connected to the OUT2 port")
|
||||||
parser.add_argument("--set-audio", type=valid_bool,
|
parser.add_argument("--set-audio", type=valid_bool,
|
||||||
help="Enable or disable audio", metavar="<yes|no>")
|
help="Enable or disable audio", metavar="<yes|no>")
|
||||||
parser.add_argument("--set-mfc-id",
|
parser.add_argument("--set-mfc-id",
|
||||||
@ -155,6 +151,9 @@ def main(argv: (list[str] | None)=None) -> None: # pylint: disable=too-many-bra
|
|||||||
imp = f"_{imp}"
|
imp = f"_{imp}"
|
||||||
options.imp = os.path.join(options.presets_path, f"{imp}.hex")
|
options.imp = os.path.join(options.presets_path, f"{imp}.hex")
|
||||||
|
|
||||||
|
if options.import_display:
|
||||||
|
options.imp = _find_out2_edid_path()
|
||||||
|
|
||||||
orig_edid_path = options.edid_path
|
orig_edid_path = options.edid_path
|
||||||
if options.imp:
|
if options.imp:
|
||||||
options.export_hex = options.edid_path
|
options.export_hex = options.edid_path
|
||||||
|
|||||||
@ -59,13 +59,19 @@ class EdidInfo:
|
|||||||
except ParsedEdidNoBlockError:
|
except ParsedEdidNoBlockError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
audio: bool = False
|
||||||
|
try:
|
||||||
|
audio = parsed.get_audio()
|
||||||
|
except ParsedEdidNoBlockError:
|
||||||
|
pass
|
||||||
|
|
||||||
return EdidInfo(
|
return EdidInfo(
|
||||||
mfc_id=parsed.get_mfc_id(),
|
mfc_id=parsed.get_mfc_id(),
|
||||||
product_id=parsed.get_product_id(),
|
product_id=parsed.get_product_id(),
|
||||||
serial=parsed.get_serial(),
|
serial=parsed.get_serial(),
|
||||||
monitor_name=monitor_name,
|
monitor_name=monitor_name,
|
||||||
monitor_serial=monitor_serial,
|
monitor_serial=monitor_serial,
|
||||||
audio=parsed.get_audio(),
|
audio=audio,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -76,14 +82,14 @@ class Edid:
|
|||||||
crc: int = dataclasses.field(default=0)
|
crc: int = dataclasses.field(default=0)
|
||||||
valid: bool = dataclasses.field(default=False)
|
valid: bool = dataclasses.field(default=False)
|
||||||
info: (EdidInfo | None) = dataclasses.field(default=None)
|
info: (EdidInfo | None) = dataclasses.field(default=None)
|
||||||
|
_packed: bytes = dataclasses.field(default=b"")
|
||||||
__HEADER = b"\x00\xFF\xFF\xFF\xFF\xFF\xFF\x00"
|
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
def __post_init__(self) -> None:
|
||||||
assert len(self.name) > 0
|
assert len(self.name) > 0
|
||||||
assert len(self.data) == 256
|
assert len(self.data) in [128, 256]
|
||||||
object.__setattr__(self, "crc", bitbang.make_crc16(self.data))
|
object.__setattr__(self, "_packed", (self.data + (b"\x00" * 128))[:256])
|
||||||
object.__setattr__(self, "valid", self.data.startswith(self.__HEADER))
|
object.__setattr__(self, "crc", bitbang.make_crc16(self._packed)) # Calculate CRC for filled data
|
||||||
|
object.__setattr__(self, "valid", ParsedEdid.is_header_valid(self.data))
|
||||||
try:
|
try:
|
||||||
object.__setattr__(self, "info", EdidInfo.from_data(self.data))
|
object.__setattr__(self, "info", EdidInfo.from_data(self.data))
|
||||||
except Exception:
|
except Exception:
|
||||||
@ -93,7 +99,7 @@ class Edid:
|
|||||||
return "".join(f"{item:0{2}X}" for item in self.data)
|
return "".join(f"{item:0{2}X}" for item in self.data)
|
||||||
|
|
||||||
def pack(self) -> bytes:
|
def pack(self) -> bytes:
|
||||||
return self.data
|
return self._packed
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_data(cls, name: str, data: (str | bytes | None)) -> "Edid":
|
def from_data(cls, name: str, data: (str | bytes | None)) -> "Edid":
|
||||||
@ -101,14 +107,14 @@ class Edid:
|
|||||||
return Edid(name, b"\x00" * 256)
|
return Edid(name, b"\x00" * 256)
|
||||||
|
|
||||||
if isinstance(data, bytes):
|
if isinstance(data, bytes):
|
||||||
if data.startswith(cls.__HEADER):
|
if ParsedEdid.is_header_valid(cls.data):
|
||||||
return Edid(name, data) # Бинарный едид
|
return Edid(name, data) # Бинарный едид
|
||||||
data_hex = data.decode() # Текстовый едид, прочитанный как бинарный из файла
|
data_hex = data.decode() # Текстовый едид, прочитанный как бинарный из файла
|
||||||
else: # isinstance(data, str)
|
else: # isinstance(data, str)
|
||||||
data_hex = str(data) # Текстовый едид
|
data_hex = str(data) # Текстовый едид
|
||||||
|
|
||||||
data_hex = re.sub(r"\s", "", data_hex)
|
data_hex = re.sub(r"\s", "", data_hex)
|
||||||
assert len(data_hex) == 512
|
assert len(data_hex) in [256, 512]
|
||||||
data = bytes([
|
data = bytes([
|
||||||
int(data_hex[index:index + 2], 16)
|
int(data_hex[index:index + 2], 16)
|
||||||
for index in range(0, len(data_hex), 2)
|
for index in range(0, len(data_hex), 2)
|
||||||
|
|||||||
21
kvmd/edid.py
21
kvmd/edid.py
@ -80,27 +80,29 @@ _CEA_SPEAKERS = 4
|
|||||||
class Edid:
|
class Edid:
|
||||||
# https://en.wikipedia.org/wiki/Extended_Display_Identification_Data
|
# https://en.wikipedia.org/wiki/Extended_Display_Identification_Data
|
||||||
|
|
||||||
def __init__(self, data: bytes, allow_short: bool=False) -> None:
|
def __init__(self, data: bytes) -> None:
|
||||||
if allow_short:
|
|
||||||
assert len(data) in [_SHORT, _LONG], f"Invalid EDID length: {len(data)}, should be {_SHORT} or {_LONG} bytes"
|
assert len(data) in [_SHORT, _LONG], f"Invalid EDID length: {len(data)}, should be {_SHORT} or {_LONG} bytes"
|
||||||
else:
|
self.__long = (len(data) == _LONG)
|
||||||
assert len(data) == _LONG, f"Invalid EDID length: {len(data)}, should be {_LONG} bytes"
|
if self.__long:
|
||||||
assert data[126] == 1, "Zero extensions number"
|
assert data[126] == 1, "Zero extensions number"
|
||||||
assert (data[_CEA + 0], data[_CEA + 1]) == (0x02, 0x03), "Can't find CEA extension"
|
assert (data[_CEA + 0], data[_CEA + 1]) == (0x02, 0x03), "Can't find CEA extension"
|
||||||
self.__data = list(data)
|
self.__data = list(data)
|
||||||
self.__long = (len(data) == _LONG)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_file(cls, path: str, allow_short: bool=False) -> "Edid":
|
def is_header_valid(cls, data: bytes) -> bool:
|
||||||
|
return data.startswith(b"\x00\xFF\xFF\xFF\xFF\xFF\xFF\x00")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_file(cls, path: str) -> "Edid":
|
||||||
with _smart_open(path, "rb") as file:
|
with _smart_open(path, "rb") as file:
|
||||||
data = file.read()
|
data = file.read()
|
||||||
if not data.startswith(b"\x00\xFF\xFF\xFF\xFF\xFF\xFF\x00"):
|
if not cls.is_header_valid(data):
|
||||||
text = re.sub(r"\s", "", data.decode())
|
text = re.sub(r"\s", "", data.decode())
|
||||||
data = bytes([
|
data = bytes([
|
||||||
int(text[index:index + 2], 16)
|
int(text[index:index + 2], 16)
|
||||||
for index in range(0, len(text), 2)
|
for index in range(0, len(text), 2)
|
||||||
])
|
])
|
||||||
return Edid(data, allow_short)
|
return Edid(data)
|
||||||
|
|
||||||
def write_hex(self, path: str) -> None:
|
def write_hex(self, path: str) -> None:
|
||||||
self.__update_checksums()
|
self.__update_checksums()
|
||||||
@ -236,7 +238,8 @@ class Edid:
|
|||||||
self.__data[_CEA + 3] &= (0xFF - 0b01000000) # ~X
|
self.__data[_CEA + 3] &= (0xFF - 0b01000000) # ~X
|
||||||
|
|
||||||
def __parse_cea(self) -> tuple[list[_CeaBlock], bytes]:
|
def __parse_cea(self) -> tuple[list[_CeaBlock], bytes]:
|
||||||
assert self.__long, "This EDID does not contain any CEA blocks"
|
if not self.__long:
|
||||||
|
raise EdidNoBlockError("This EDID does not contain any CEA blocks")
|
||||||
|
|
||||||
cea = self.__data[_CEA:]
|
cea = self.__data[_CEA:]
|
||||||
dtd_begin = cea[2]
|
dtd_begin = cea[2]
|
||||||
|
|||||||
@ -50,7 +50,7 @@ def valid_switch_edid_data(arg: Any) -> str:
|
|||||||
name = "switch EDID data"
|
name = "switch EDID data"
|
||||||
arg = valid_stripped_string(arg, name=name)
|
arg = valid_stripped_string(arg, name=name)
|
||||||
arg = re.sub(r"\s", "", arg)
|
arg = re.sub(r"\s", "", arg)
|
||||||
return check_re_match(arg, name, "(?i)^[0-9a-f]{512}$").upper()
|
return check_re_match(arg, name, "(?i)^([0-9a-f]{256}|[0-9a-f]{512})$").upper()
|
||||||
|
|
||||||
|
|
||||||
def valid_switch_color(arg: Any, allow_default: bool) -> str:
|
def valid_switch_color(arg: Any, allow_default: bool) -> str:
|
||||||
|
|||||||
@ -94,6 +94,9 @@ def test_fail__valid_switch_edid_id__allowed_default(arg: Any) -> None:
|
|||||||
|
|
||||||
# =====
|
# =====
|
||||||
@pytest.mark.parametrize("arg", [
|
@pytest.mark.parametrize("arg", [
|
||||||
|
"f" * 256,
|
||||||
|
"0" * 256,
|
||||||
|
"1a" * 128,
|
||||||
"f" * 512,
|
"f" * 512,
|
||||||
"0" * 512,
|
"0" * 512,
|
||||||
"1a" * 256,
|
"1a" * 256,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user