diff --git a/kvmd/apps/kvmd/api/switch.py b/kvmd/apps/kvmd/api/switch.py index bf91b83e..d89f1b84 100644 --- a/kvmd/apps/kvmd/api/switch.py +++ b/kvmd/apps/kvmd/api/switch.py @@ -79,6 +79,7 @@ class SwitchApi: param: validator(req.query.get(param)) for (param, validator) in [ ("edid_id", (lambda arg: valid_switch_edid_id(arg, allow_default=True))), + ("dummy", valid_bool), ("name", valid_switch_port_name), ("atx_click_power_delay", valid_switch_atx_click_delay), ("atx_click_power_long_delay", valid_switch_atx_click_delay), diff --git a/kvmd/apps/kvmd/switch/__init__.py b/kvmd/apps/kvmd/switch/__init__.py index f0cc98f2..6f2337a4 100644 --- a/kvmd/apps/kvmd/switch/__init__.py +++ b/kvmd/apps/kvmd/switch/__init__.py @@ -32,6 +32,7 @@ from .lib import Inotify from .types import Edid from .types import Edids +from .types import Dummies from .types import Color from .types import Colors from .types import PortNames @@ -68,6 +69,7 @@ class SwitchUnknownEdidError(SwitchOperationError): # ===== class Switch: # pylint: disable=too-many-public-methods __X_EDIDS = "edids" + __X_DUMMIES = "dummies" __X_COLORS = "colors" __X_PORT_NAMES = "port_names" __X_ATX_CP_DELAYS = "atx_cp_delays" @@ -75,7 +77,7 @@ class Switch: # pylint: disable=too-many-public-methods __X_ATX_CR_DELAYS = "atx_cr_delays" __X_ALL = frozenset([ - __X_EDIDS, __X_COLORS, __X_PORT_NAMES, + __X_EDIDS, __X_DUMMIES, __X_COLORS, __X_PORT_NAMES, __X_ATX_CP_DELAYS, __X_ATX_CPL_DELAYS, __X_ATX_CR_DELAYS, ]) @@ -105,6 +107,12 @@ class Switch: # pylint: disable=too-many-public-methods if save: self.__save_notifier.notify() + def __x_set_dummies(self, dummies: Dummies, save: bool=True) -> None: + self.__chain.set_dummies(dummies) + self.__cache.set_dummies(dummies) + if save: + self.__save_notifier.notify() + def __x_set_colors(self, colors: Colors, save: bool=True) -> None: self.__chain.set_colors(colors) self.__cache.set_colors(colors) @@ -236,6 +244,7 @@ class Switch: # pylint: disable=too-many-public-methods self, port: int, edid_id: (str | None)=None, + dummy: (bool | None)=None, name: (str | None)=None, atx_click_power_delay: (float | None)=None, atx_click_power_long_delay: (float | None)=None, @@ -250,15 +259,16 @@ class Switch: # pylint: disable=too-many-public-methods edids.assign(port, edid_id) self.__x_set_edids(edids) - for (key, value) in [ - (self.__X_PORT_NAMES, name), - (self.__X_ATX_CP_DELAYS, atx_click_power_delay), - (self.__X_ATX_CPL_DELAYS, atx_click_power_long_delay), - (self.__X_ATX_CR_DELAYS, atx_click_reset_delay), + for (reset, key, value) in [ + (None, self.__X_DUMMIES, dummy), # None can't be used now + ("", self.__X_PORT_NAMES, name), + (0, self.__X_ATX_CP_DELAYS, atx_click_power_delay), + (0, self.__X_ATX_CPL_DELAYS, atx_click_power_long_delay), + (0, self.__X_ATX_CR_DELAYS, atx_click_reset_delay), ]: if value is not None: new = getattr(self.__cache, f"get_{key}")() - new[port] = (value or None) # None == reset to default + new[port] = (None if value == reset else value) # Value or reset default getattr(self, f"_Switch__x_set_{key}")(new) # ===== @@ -375,7 +385,7 @@ class Switch: # pylint: disable=too-many-public-methods prevs = dict.fromkeys(self.__X_ALL) while True: await self.__save_notifier.wait() - while (await self.__save_notifier.wait(5)): + while not (await self.__save_notifier.wait(5)): pass while True: try: diff --git a/kvmd/apps/kvmd/switch/chain.py b/kvmd/apps/kvmd/switch/chain.py index 7137f5b9..4bd9b4eb 100644 --- a/kvmd/apps/kvmd/switch/chain.py +++ b/kvmd/apps/kvmd/switch/chain.py @@ -34,6 +34,7 @@ from .lib import aiotools from .lib import aioproc from .types import Edids +from .types import Dummies from .types import Colors from .proto import Response @@ -80,6 +81,11 @@ class _CmdSetEdids(_BaseCmd): edids: Edids +@dataclasses.dataclass(frozen=True) +class _CmdSetDummies(_BaseCmd): + dummies: Dummies + + @dataclasses.dataclass(frozen=True) class _CmdSetColors(_BaseCmd): colors: Colors @@ -189,7 +195,7 @@ class Chain: # pylint: disable=too-many-instance-attributes self.__actual = False self.__edids = Edids() - + self.__dummies = Dummies({}) self.__colors = Colors() self.__units: list[_UnitContext] = [] @@ -225,6 +231,9 @@ class Chain: # pylint: disable=too-many-instance-attributes def set_edids(self, edids: Edids) -> None: self.__queue_cmd(_CmdSetEdids(edids)) # Will be copied because of multiprocessing.Queue() + def set_dummies(self, dummies: Dummies) -> None: + self.__queue_cmd(_CmdSetDummies(dummies)) + def set_colors(self, colors: Colors) -> None: self.__queue_cmd(_CmdSetColors(colors)) @@ -348,6 +357,9 @@ class Chain: # pylint: disable=too-many-instance-attributes case _CmdSetEdids(): self.__edids = cmd.edids + case _CmdSetDummies(): + self.__dummies = cmd.dummies + case _CmdSetColors(): self.__colors = cmd.colors @@ -373,7 +385,7 @@ class Chain: # pylint: disable=too-many-instance-attributes def __adjust_quirks(self) -> None: for (unit, ctx) in enumerate(self.__units): - if ctx.state is not None and (ctx.state.version.sw_dev or ctx.state.version.sw >= 7): + if ctx.state is not None and ctx.state.version.is_fresh(7): ignore_hpd = (unit == 0 and self.__ignore_hpd_on_top) if ctx.state.quirks.ignore_hpd != ignore_hpd: get_logger().info("Applying quirk ignore_hpd=%s to [%d] ...", @@ -403,6 +415,7 @@ class Chain: # pylint: disable=too-many-instance-attributes self.__ensure_config_port(unit, ctx) if self.__actual: self.__ensure_config_edids(unit, ctx) + self.__ensure_config_dummies(unit, ctx) self.__ensure_config_colors(unit, ctx) def __ensure_config_port(self, unit: int, ctx: _UnitContext) -> None: @@ -429,6 +442,19 @@ class Chain: # pylint: disable=too-many-instance-attributes ctx.changing_rid = self.__device.request_set_edid(unit, ch, edid) break # Busy globally + def __ensure_config_dummies(self, unit: int, ctx: _UnitContext) -> None: + assert ctx.state is not None + if ctx.state.version.is_fresh(8) and ctx.can_be_changed(): + for ch in range(4): + port = self.get_virtual_port(unit, ch) + dummy = self.__dummies[port] + if ctx.state.video_dummies[ch] != dummy: + get_logger().info("Changing dummy flag on port %d on [%d:%d]: %d -> %d ...", + port, unit, ch, + ctx.state.video_dummies[ch], dummy) + ctx.changing_rid = self.__device.request_set_dummy(unit, ch, dummy) + break # Busy globally (actually not but it can be changed in the firmware) + def __ensure_config_colors(self, unit: int, ctx: _UnitContext) -> None: assert self.__actual assert ctx.state is not None diff --git a/kvmd/apps/kvmd/switch/device.py b/kvmd/apps/kvmd/switch/device.py index a057a50d..df532804 100644 --- a/kvmd/apps/kvmd/switch/device.py +++ b/kvmd/apps/kvmd/switch/device.py @@ -41,6 +41,7 @@ from .proto import BodySetBeacon from .proto import BodyAtxClick from .proto import BodySetEdid from .proto import BodyClearEdid +from .proto import BodySetDummy from .proto import BodySetColors from .proto import BodySetQuirks @@ -164,6 +165,9 @@ class Device: return self.__send_request(Header.SET_EDID, unit, BodySetEdid(ch, edid)) return self.__send_request(Header.CLEAR_EDID, unit, BodyClearEdid(ch)) + def request_set_dummy(self, unit: int, ch: int, on: bool) -> int: + return self.__send_request(Header.SET_DUMMY, unit, BodySetDummy(ch, on)) + def request_set_colors(self, unit: int, ch: int, colors: Colors) -> int: return self.__send_request(Header.SET_COLORS, unit, BodySetColors(ch, colors)) diff --git a/kvmd/apps/kvmd/switch/proto.py b/kvmd/apps/kvmd/switch/proto.py index 2081a4b6..3c39d238 100644 --- a/kvmd/apps/kvmd/switch/proto.py +++ b/kvmd/apps/kvmd/switch/proto.py @@ -61,6 +61,7 @@ class Header(Packable, Unpackable): CLEAR_EDID = 10 SET_COLORS = 12 SET_QUIRKS = 13 + SET_DUMMY = 14 __struct = struct.Struct(" bool: + return (self.sw_dev or (self.sw >= version)) + @dataclasses.dataclass(frozen=True) class UnitFlags: @@ -121,11 +125,12 @@ class UnitState(Unpackable): # pylint: disable=too-many-instance-attributes video_hpd: tuple[bool, bool, bool, bool, bool] video_edid: tuple[bool, bool, bool, bool] video_crc: tuple[int, int, int, int] + video_dummies: tuple[bool, bool, bool, bool] usb_5v_sens: tuple[bool, bool, bool, bool] atx_busy: tuple[bool, bool, bool, bool] quirks: UnitQuirks - __struct = struct.Struct(" bool: if edid is None: @@ -142,7 +147,7 @@ class UnitState(Unpackable): # pylint: disable=too-many-instance-attributes 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, quirks, + usb_5v_sens, atx_busy, quirks, video_dummies, ) = cls.__struct.unpack_from(data, offset=offset) return UnitState( version=UnitVersion( @@ -163,6 +168,7 @@ class UnitState(Unpackable): # pylint: disable=too-many-instance-attributes video_hpd=cls.__make_flags5(video_hpd), video_edid=cls.__make_flags4(video_edid), video_crc=(vc0, vc1, vc2, vc3), + video_dummies=cls.__make_flags4(video_dummies), usb_5v_sens=cls.__make_flags4(usb_5v_sens), atx_busy=cls.__make_flags4(atx_busy), quirks=UnitQuirks(ignore_hpd=bool(quirks & 0x01)), @@ -270,6 +276,18 @@ class BodyClearEdid(Packable): return self.ch.to_bytes() +@dataclasses.dataclass(frozen=True) +class BodySetDummy(Packable): + ch: int + on: bool + + def __post_init__(self) -> None: + assert 0 <= self.ch <= 3 + + def pack(self) -> bytes: + return self.ch.to_bytes() + self.on.to_bytes() + + @dataclasses.dataclass(frozen=True) class BodySetColors(Packable): ch: int diff --git a/kvmd/apps/kvmd/switch/state.py b/kvmd/apps/kvmd/switch/state.py index 84f01cfd..92231791 100644 --- a/kvmd/apps/kvmd/switch/state.py +++ b/kvmd/apps/kvmd/switch/state.py @@ -27,6 +27,7 @@ import time from typing import AsyncGenerator from .types import Edids +from .types import Dummies from .types import Color from .types import Colors from .types import PortNames @@ -48,8 +49,8 @@ class _UnitInfo: # ===== -class StateCache: # pylint: disable=too-many-instance-attributes - __FW_VERSION = 7 +class StateCache: # pylint: disable=too-many-instance-attributes,too-many-public-methods + __FW_VERSION = 8 __FULL = 0xFFFF __SUMMARY = 0x01 @@ -62,6 +63,7 @@ class StateCache: # pylint: disable=too-many-instance-attributes def __init__(self) -> None: self.__edids = Edids() + self.__dummies = Dummies({}) self.__colors = Colors() self.__port_names = PortNames({}) self.__atx_cp_delays = AtxClickPowerDelays({}) @@ -77,6 +79,9 @@ class StateCache: # pylint: disable=too-many-instance-attributes def get_edids(self) -> Edids: return self.__edids.copy() + def get_dummies(self) -> Dummies: + return self.__dummies.copy() + def get_colors(self) -> Colors: return self.__colors @@ -226,6 +231,9 @@ class StateCache: # pylint: disable=too-many-instance-attributes "reset": self.__atx_cr_delays[port], }, }, + "video": { + "dummy": self.__dummies[port], + }, }) if x_edids: state["edids"]["used"].append(self.__edids.get_id_for_port(port)) @@ -327,6 +335,12 @@ class StateCache: # pylint: disable=too-many-instance-attributes if changed: self.__bump_state(self.__EDIDS) + def set_dummies(self, dummies: Dummies) -> None: + changed = (not self.__dummies.compare_on_ports(dummies, self.__get_ports())) + self.__dummies = dummies.copy() + if changed: + self.__bump_state(self.__FULL) + def set_colors(self, colors: Colors) -> None: changed = (self.__colors != colors) self.__colors = colors diff --git a/kvmd/apps/kvmd/switch/storage.py b/kvmd/apps/kvmd/switch/storage.py index 6e3a0a76..f8348009 100644 --- a/kvmd/apps/kvmd/switch/storage.py +++ b/kvmd/apps/kvmd/switch/storage.py @@ -39,6 +39,7 @@ from .lib import get_logger from .types import Edid from .types import Edids +from .types import Dummies from .types import Color from .types import Colors from .types import PortNames @@ -52,6 +53,8 @@ class StorageContext: __F_EDIDS_ALL = "edids_all.json" __F_EDIDS_PORT = "edids_port.json" + __F_DUMMIES = "dummies.json" + __F_COLORS = "colors.json" __F_PORT_NAMES = "port_names.json" @@ -74,6 +77,9 @@ class StorageContext: }) await self.__write_json_keyvals(self.__F_EDIDS_PORT, edids.port) + async def write_dummies(self, dummies: Dummies) -> None: + await self.__write_json_keyvals(self.__F_DUMMIES, dummies.kvs) + async def write_colors(self, colors: Colors) -> None: await self.__write_json_keyvals(self.__F_COLORS, { role: { @@ -116,6 +122,10 @@ class StorageContext: port_edids = await self.__read_json_keyvals_int(self.__F_EDIDS_PORT) return Edids(all_edids, port_edids) + async def read_dummies(self) -> Dummies: + kvs = await self.__read_json_keyvals_int(self.__F_DUMMIES) + return Dummies({key: bool(value) for (key, value) in kvs.items()}) + async def read_colors(self) -> Colors: raw = await self.__read_json_keyvals(self.__F_COLORS) return Colors(**{ # type: ignore diff --git a/kvmd/apps/kvmd/switch/types.py b/kvmd/apps/kvmd/switch/types.py index 33a7f3ad..475ec30f 100644 --- a/kvmd/apps/kvmd/switch/types.py +++ b/kvmd/apps/kvmd/switch/types.py @@ -281,6 +281,19 @@ class _PortsDict(Generic[_T]): else: self.kvs[port] = value + def __eq__(self, other: "_PortsDict[_T]") -> bool: + if not isinstance(other, self.__class__): + return False + return (self.kvs == other.kvs) + + +class Dummies(_PortsDict[bool]): + def __init__(self, kvs: dict[int, bool]) -> None: + super().__init__(True, kvs) + + def copy(self) -> "Dummies": + return Dummies(self.kvs) + class PortNames(_PortsDict[str]): def __init__(self, kvs: dict[int, str]) -> None: diff --git a/switch/switch.uf2 b/switch/switch.uf2 index e5a1725e..6e1346ce 100644 Binary files a/switch/switch.uf2 and b/switch/switch.uf2 differ diff --git a/web/share/js/kvm/switch.js b/web/share/js/kvm/switch.js index a33e2fd0..9d261cc4 100644 --- a/web/share/js/kvm/switch.js +++ b/web/share/js/kvm/switch.js @@ -459,7 +459,7 @@ export function Switch() { let create_content = function(el_parent) { let html = ` - +
Port name:
-
- `; + + let fw = model.units[model.ports[port].unit].firmware; + if (fw.devbuild || fw.version >= 8) { + html += ` +
+
+ + + + +
Simulate display on inactive port: +
+ + +
+
+ `; + } + + html += "
"; for (let kv of Object.entries(atx_actions)) { html += ` @@ -491,6 +515,7 @@ export function Switch() { `; } html += "
"; + el_parent.innerHTML = html; let el_selector = $("__switch-port-edid-selector"); @@ -521,6 +546,7 @@ export function Switch() { let params = { "port": port, "edid_id": $("__switch-port-edid-selector").value, + "dummy": $("__switch-port-dummy-switch").checked, "name": $("__switch-port-name-input").value, }; for (let action of Object.keys(atx_actions)) {