Skip to content

Commit 7d7edb1

Browse files
committed
pikvm/pikvm#1501: Switch: Option to disable HDMI dummy plug
1 parent 69d254d commit 7d7edb1

File tree

10 files changed

+139
-17
lines changed

10 files changed

+139
-17
lines changed

kvmd/apps/kvmd/api/switch.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ async def __set_port_params(self, req: Request) -> Response:
7979
param: validator(req.query.get(param))
8080
for (param, validator) in [
8181
("edid_id", (lambda arg: valid_switch_edid_id(arg, allow_default=True))),
82+
("dummy", valid_bool),
8283
("name", valid_switch_port_name),
8384
("atx_click_power_delay", valid_switch_atx_click_delay),
8485
("atx_click_power_long_delay", valid_switch_atx_click_delay),

kvmd/apps/kvmd/switch/__init__.py

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232

3333
from .types import Edid
3434
from .types import Edids
35+
from .types import Dummies
3536
from .types import Color
3637
from .types import Colors
3738
from .types import PortNames
@@ -68,14 +69,15 @@ def __init__(self) -> None:
6869
# =====
6970
class Switch: # pylint: disable=too-many-public-methods
7071
__X_EDIDS = "edids"
72+
__X_DUMMIES = "dummies"
7173
__X_COLORS = "colors"
7274
__X_PORT_NAMES = "port_names"
7375
__X_ATX_CP_DELAYS = "atx_cp_delays"
7476
__X_ATX_CPL_DELAYS = "atx_cpl_delays"
7577
__X_ATX_CR_DELAYS = "atx_cr_delays"
7678

7779
__X_ALL = frozenset([
78-
__X_EDIDS, __X_COLORS, __X_PORT_NAMES,
80+
__X_EDIDS, __X_DUMMIES, __X_COLORS, __X_PORT_NAMES,
7981
__X_ATX_CP_DELAYS, __X_ATX_CPL_DELAYS, __X_ATX_CR_DELAYS,
8082
])
8183

@@ -105,6 +107,12 @@ def __x_set_edids(self, edids: Edids, save: bool=True) -> None:
105107
if save:
106108
self.__save_notifier.notify()
107109

110+
def __x_set_dummies(self, dummies: Dummies, save: bool=True) -> None:
111+
self.__chain.set_dummies(dummies)
112+
self.__cache.set_dummies(dummies)
113+
if save:
114+
self.__save_notifier.notify()
115+
108116
def __x_set_colors(self, colors: Colors, save: bool=True) -> None:
109117
self.__chain.set_colors(colors)
110118
self.__cache.set_colors(colors)
@@ -236,6 +244,7 @@ async def set_port_params(
236244
self,
237245
port: int,
238246
edid_id: (str | None)=None,
247+
dummy: (bool | None)=None,
239248
name: (str | None)=None,
240249
atx_click_power_delay: (float | None)=None,
241250
atx_click_power_long_delay: (float | None)=None,
@@ -250,15 +259,16 @@ async def set_port_params(
250259
edids.assign(port, edid_id)
251260
self.__x_set_edids(edids)
252261

253-
for (key, value) in [
254-
(self.__X_PORT_NAMES, name),
255-
(self.__X_ATX_CP_DELAYS, atx_click_power_delay),
256-
(self.__X_ATX_CPL_DELAYS, atx_click_power_long_delay),
257-
(self.__X_ATX_CR_DELAYS, atx_click_reset_delay),
262+
for (reset, key, value) in [
263+
(None, self.__X_DUMMIES, dummy), # None can't be used now
264+
("", self.__X_PORT_NAMES, name),
265+
(0, self.__X_ATX_CP_DELAYS, atx_click_power_delay),
266+
(0, self.__X_ATX_CPL_DELAYS, atx_click_power_long_delay),
267+
(0, self.__X_ATX_CR_DELAYS, atx_click_reset_delay),
258268
]:
259269
if value is not None:
260270
new = getattr(self.__cache, f"get_{key}")()
261-
new[port] = (value or None) # None == reset to default
271+
new[port] = (None if value == reset else value) # Value or reset default
262272
getattr(self, f"_Switch__x_set_{key}")(new)
263273

264274
# =====
@@ -375,7 +385,7 @@ async def __systask_storage(self) -> None:
375385
prevs = dict.fromkeys(self.__X_ALL)
376386
while True:
377387
await self.__save_notifier.wait()
378-
while (await self.__save_notifier.wait(5)):
388+
while not (await self.__save_notifier.wait(5)):
379389
pass
380390
while True:
381391
try:

kvmd/apps/kvmd/switch/chain.py

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
from .lib import aioproc
3535

3636
from .types import Edids
37+
from .types import Dummies
3738
from .types import Colors
3839

3940
from .proto import Response
@@ -80,6 +81,11 @@ class _CmdSetEdids(_BaseCmd):
8081
edids: Edids
8182

8283

84+
@dataclasses.dataclass(frozen=True)
85+
class _CmdSetDummies(_BaseCmd):
86+
dummies: Dummies
87+
88+
8389
@dataclasses.dataclass(frozen=True)
8490
class _CmdSetColors(_BaseCmd):
8591
colors: Colors
@@ -189,7 +195,7 @@ def __init__(
189195
self.__actual = False
190196

191197
self.__edids = Edids()
192-
198+
self.__dummies = Dummies({})
193199
self.__colors = Colors()
194200

195201
self.__units: list[_UnitContext] = []
@@ -225,6 +231,9 @@ def set_downlink_beacon(self, unit: int, on: bool) -> None:
225231
def set_edids(self, edids: Edids) -> None:
226232
self.__queue_cmd(_CmdSetEdids(edids)) # Will be copied because of multiprocessing.Queue()
227233

234+
def set_dummies(self, dummies: Dummies) -> None:
235+
self.__queue_cmd(_CmdSetDummies(dummies))
236+
228237
def set_colors(self, colors: Colors) -> None:
229238
self.__queue_cmd(_CmdSetColors(colors))
230239

@@ -348,6 +357,9 @@ def __consume_commands(self) -> None:
348357
case _CmdSetEdids():
349358
self.__edids = cmd.edids
350359

360+
case _CmdSetDummies():
361+
self.__dummies = cmd.dummies
362+
351363
case _CmdSetColors():
352364
self.__colors = cmd.colors
353365

@@ -373,7 +385,7 @@ def __update_units(self, resp: Response) -> None:
373385

374386
def __adjust_quirks(self) -> None:
375387
for (unit, ctx) in enumerate(self.__units):
376-
if ctx.state is not None and (ctx.state.version.sw_dev or ctx.state.version.sw >= 7):
388+
if ctx.state is not None and ctx.state.version.is_fresh(7):
377389
ignore_hpd = (unit == 0 and self.__ignore_hpd_on_top)
378390
if ctx.state.quirks.ignore_hpd != ignore_hpd:
379391
get_logger().info("Applying quirk ignore_hpd=%s to [%d] ...",
@@ -403,6 +415,7 @@ def __ensure_config(self) -> None:
403415
self.__ensure_config_port(unit, ctx)
404416
if self.__actual:
405417
self.__ensure_config_edids(unit, ctx)
418+
self.__ensure_config_dummies(unit, ctx)
406419
self.__ensure_config_colors(unit, ctx)
407420

408421
def __ensure_config_port(self, unit: int, ctx: _UnitContext) -> None:
@@ -429,6 +442,19 @@ def __ensure_config_edids(self, unit: int, ctx: _UnitContext) -> None:
429442
ctx.changing_rid = self.__device.request_set_edid(unit, ch, edid)
430443
break # Busy globally
431444

445+
def __ensure_config_dummies(self, unit: int, ctx: _UnitContext) -> None:
446+
assert ctx.state is not None
447+
if ctx.state.version.is_fresh(8) and ctx.can_be_changed():
448+
for ch in range(4):
449+
port = self.get_virtual_port(unit, ch)
450+
dummy = self.__dummies[port]
451+
if ctx.state.video_dummies[ch] != dummy:
452+
get_logger().info("Changing dummy flag on port %d on [%d:%d]: %d -> %d ...",
453+
port, unit, ch,
454+
ctx.state.video_dummies[ch], dummy)
455+
ctx.changing_rid = self.__device.request_set_dummy(unit, ch, dummy)
456+
break # Busy globally (actually not but it can be changed in the firmware)
457+
432458
def __ensure_config_colors(self, unit: int, ctx: _UnitContext) -> None:
433459
assert self.__actual
434460
assert ctx.state is not None

kvmd/apps/kvmd/switch/device.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
from .proto import BodyAtxClick
4242
from .proto import BodySetEdid
4343
from .proto import BodyClearEdid
44+
from .proto import BodySetDummy
4445
from .proto import BodySetColors
4546
from .proto import BodySetQuirks
4647

@@ -164,6 +165,9 @@ def request_set_edid(self, unit: int, ch: int, edid: Edid) -> int:
164165
return self.__send_request(Header.SET_EDID, unit, BodySetEdid(ch, edid))
165166
return self.__send_request(Header.CLEAR_EDID, unit, BodyClearEdid(ch))
166167

168+
def request_set_dummy(self, unit: int, ch: int, on: bool) -> int:
169+
return self.__send_request(Header.SET_DUMMY, unit, BodySetDummy(ch, on))
170+
167171
def request_set_colors(self, unit: int, ch: int, colors: Colors) -> int:
168172
return self.__send_request(Header.SET_COLORS, unit, BodySetColors(ch, colors))
169173

kvmd/apps/kvmd/switch/proto.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ class Header(Packable, Unpackable):
6161
CLEAR_EDID = 10
6262
SET_COLORS = 12
6363
SET_QUIRKS = 13
64+
SET_DUMMY = 14
6465

6566
__struct = struct.Struct("<BHBB")
6667

@@ -96,6 +97,9 @@ class UnitVersion:
9697
sw: int
9798
sw_dev: bool
9899

100+
def is_fresh(self, version: int) -> bool:
101+
return (self.sw_dev or (self.sw >= version))
102+
99103

100104
@dataclasses.dataclass(frozen=True)
101105
class UnitFlags:
@@ -121,11 +125,12 @@ class UnitState(Unpackable): # pylint: disable=too-many-instance-attributes
121125
video_hpd: tuple[bool, bool, bool, bool, bool]
122126
video_edid: tuple[bool, bool, bool, bool]
123127
video_crc: tuple[int, int, int, int]
128+
video_dummies: tuple[bool, bool, bool, bool]
124129
usb_5v_sens: tuple[bool, bool, bool, bool]
125130
atx_busy: tuple[bool, bool, bool, bool]
126131
quirks: UnitQuirks
127132

128-
__struct = struct.Struct("<HHHBBHHHHHHBBBHHHHBxBB29x")
133+
__struct = struct.Struct("<HHHBBHHHHHHBBBHHHHBxBBB28x")
129134

130135
def compare_edid(self, ch: int, edid: Optional["Edid"]) -> bool:
131136
if edid is None:
@@ -142,7 +147,7 @@ def unpack(cls, data: bytes, offset: int=0) -> "UnitState": # pylint: disable=t
142147
sw_version, hw_version, flags, ch,
143148
beacons, nc0, nc1, nc2, nc3, nc4, nc5,
144149
video_5v_sens, video_hpd, video_edid, vc0, vc1, vc2, vc3,
145-
usb_5v_sens, atx_busy, quirks,
150+
usb_5v_sens, atx_busy, quirks, video_dummies,
146151
) = cls.__struct.unpack_from(data, offset=offset)
147152
return UnitState(
148153
version=UnitVersion(
@@ -163,6 +168,7 @@ def unpack(cls, data: bytes, offset: int=0) -> "UnitState": # pylint: disable=t
163168
video_hpd=cls.__make_flags5(video_hpd),
164169
video_edid=cls.__make_flags4(video_edid),
165170
video_crc=(vc0, vc1, vc2, vc3),
171+
video_dummies=cls.__make_flags4(video_dummies),
166172
usb_5v_sens=cls.__make_flags4(usb_5v_sens),
167173
atx_busy=cls.__make_flags4(atx_busy),
168174
quirks=UnitQuirks(ignore_hpd=bool(quirks & 0x01)),
@@ -270,6 +276,18 @@ def pack(self) -> bytes:
270276
return self.ch.to_bytes()
271277

272278

279+
@dataclasses.dataclass(frozen=True)
280+
class BodySetDummy(Packable):
281+
ch: int
282+
on: bool
283+
284+
def __post_init__(self) -> None:
285+
assert 0 <= self.ch <= 3
286+
287+
def pack(self) -> bytes:
288+
return self.ch.to_bytes() + self.on.to_bytes()
289+
290+
273291
@dataclasses.dataclass(frozen=True)
274292
class BodySetColors(Packable):
275293
ch: int

kvmd/apps/kvmd/switch/state.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
from typing import AsyncGenerator
2828

2929
from .types import Edids
30+
from .types import Dummies
3031
from .types import Color
3132
from .types import Colors
3233
from .types import PortNames
@@ -48,8 +49,8 @@ class _UnitInfo:
4849

4950

5051
# =====
51-
class StateCache: # pylint: disable=too-many-instance-attributes
52-
__FW_VERSION = 7
52+
class StateCache: # pylint: disable=too-many-instance-attributes,too-many-public-methods
53+
__FW_VERSION = 8
5354

5455
__FULL = 0xFFFF
5556
__SUMMARY = 0x01
@@ -62,6 +63,7 @@ class StateCache: # pylint: disable=too-many-instance-attributes
6263

6364
def __init__(self) -> None:
6465
self.__edids = Edids()
66+
self.__dummies = Dummies({})
6567
self.__colors = Colors()
6668
self.__port_names = PortNames({})
6769
self.__atx_cp_delays = AtxClickPowerDelays({})
@@ -77,6 +79,9 @@ def __init__(self) -> None:
7779
def get_edids(self) -> Edids:
7880
return self.__edids.copy()
7981

82+
def get_dummies(self) -> Dummies:
83+
return self.__dummies.copy()
84+
8085
def get_colors(self) -> Colors:
8186
return self.__colors
8287

@@ -226,6 +231,9 @@ def __inner_get_state(self, mask: int) -> dict: # pylint: disable=too-many-bran
226231
"reset": self.__atx_cr_delays[port],
227232
},
228233
},
234+
"video": {
235+
"dummy": self.__dummies[port],
236+
},
229237
})
230238
if x_edids:
231239
state["edids"]["used"].append(self.__edids.get_id_for_port(port))
@@ -327,6 +335,12 @@ def set_edids(self, edids: Edids) -> None:
327335
if changed:
328336
self.__bump_state(self.__EDIDS)
329337

338+
def set_dummies(self, dummies: Dummies) -> None:
339+
changed = (not self.__dummies.compare_on_ports(dummies, self.__get_ports()))
340+
self.__dummies = dummies.copy()
341+
if changed:
342+
self.__bump_state(self.__FULL)
343+
330344
def set_colors(self, colors: Colors) -> None:
331345
changed = (self.__colors != colors)
332346
self.__colors = colors

kvmd/apps/kvmd/switch/storage.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939

4040
from .types import Edid
4141
from .types import Edids
42+
from .types import Dummies
4243
from .types import Color
4344
from .types import Colors
4445
from .types import PortNames
@@ -52,6 +53,8 @@ class StorageContext:
5253
__F_EDIDS_ALL = "edids_all.json"
5354
__F_EDIDS_PORT = "edids_port.json"
5455

56+
__F_DUMMIES = "dummies.json"
57+
5558
__F_COLORS = "colors.json"
5659

5760
__F_PORT_NAMES = "port_names.json"
@@ -74,6 +77,9 @@ async def write_edids(self, edids: Edids) -> None:
7477
})
7578
await self.__write_json_keyvals(self.__F_EDIDS_PORT, edids.port)
7679

80+
async def write_dummies(self, dummies: Dummies) -> None:
81+
await self.__write_json_keyvals(self.__F_DUMMIES, dummies.kvs)
82+
7783
async def write_colors(self, colors: Colors) -> None:
7884
await self.__write_json_keyvals(self.__F_COLORS, {
7985
role: {
@@ -116,6 +122,10 @@ async def read_edids(self) -> Edids:
116122
port_edids = await self.__read_json_keyvals_int(self.__F_EDIDS_PORT)
117123
return Edids(all_edids, port_edids)
118124

125+
async def read_dummies(self) -> Dummies:
126+
kvs = await self.__read_json_keyvals_int(self.__F_DUMMIES)
127+
return Dummies({key: bool(value) for (key, value) in kvs.items()})
128+
119129
async def read_colors(self) -> Colors:
120130
raw = await self.__read_json_keyvals(self.__F_COLORS)
121131
return Colors(**{ # type: ignore

kvmd/apps/kvmd/switch/types.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,19 @@ def __setitem__(self, port: int, value: (_T | None)) -> None:
281281
else:
282282
self.kvs[port] = value
283283

284+
def __eq__(self, other: "_PortsDict[_T]") -> bool:
285+
if not isinstance(other, self.__class__):
286+
return False
287+
return (self.kvs == other.kvs)
288+
289+
290+
class Dummies(_PortsDict[bool]):
291+
def __init__(self, kvs: dict[int, bool]) -> None:
292+
super().__init__(True, kvs)
293+
294+
def copy(self) -> "Dummies":
295+
return Dummies(self.kvs)
296+
284297

285298
class PortNames(_PortsDict[str]):
286299
def __init__(self, kvs: dict[int, str]) -> None:

switch/switch.uf2

4.5 KB
Binary file not shown.

0 commit comments

Comments
 (0)