#!/usr/bin/env python3 """Restore this RK3528 terminal's verified HDMI timing after X11 hotplug.""" import argparse import fcntl import os from pathlib import Path import re import signal import subprocess import sys import time from dataclasses import dataclass OUTPUT = "HDMI-1" MODE = "2560x1440_60_fixed297" # The vendor PHY rejects the EDID's 241.70 MHz timing. Keep the verified PLL. TIMING = "297.00 2560 2608 2640 2720 1440 1443 1448 1820 +HSync -VSync".split() POLL_SECONDS = 2 SETTLE_SECONDS = 2 def log(message): print(time.strftime("%Y-%m-%d %H:%M:%S"), message, flush=True) def xrandr(*args): result = subprocess.run(["xrandr", *args], capture_output=True, text=True, timeout=10, check=False) if result.returncode: raise RuntimeError((result.stderr or result.stdout).strip()[:400]) return result.stdout @dataclass class DisplayState: connected: bool geometry: str active_mode: str modes: set defined: bool @property def healthy(self): return (self.connected and self.geometry == "2560x1440+0+0" and self.active_mode == MODE) def parse_state(query): connected, geometry, active = False, "", "" modes, defined, in_output = set(), False, False for line in query.splitlines(): fields = line.split() if not fields: continue if fields[0] == MODE: defined = True # Includes a globally defined but detached mode. if not line[0].isspace(): in_output = fields[0] == OUTPUT if in_output: connected = len(fields) > 1 and fields[1] == "connected" geometry = next((f for f in fields[2:] if re.fullmatch(r"\d+x\d+\+\d+\+\d+", f)), "") elif in_output and len(fields) > 1 and re.match(r"^\d+(?:\.\d+)?[+*]*$", fields[1]): modes.add(fields[0]) if "*" in " ".join(fields[1:]): active = fields[0] return DisplayState(connected, geometry, active, modes, defined) class Guard: def __init__(self, runner=xrandr, reporter=log): self.run = runner self.log = reporter self.bad_since = None self.next_retry = 0 self.failures = 0 self.last_status = None def report(self, status): if status != self.last_status: self.log(status) self.last_status = status def step(self, now): try: # Xorg already receives DRM hotplug events. Read its current state # without probing every EDID/mode: the vendor driver logs clock # warnings for those probes, adding unnecessary work while streaming. state = parse_state(self.run("--current")) except (OSError, RuntimeError, subprocess.TimeoutExpired) as error: self.bad_since = None self.report("X11 unavailable; waiting: " + str(error)[:400]) return False if not state.connected: self.bad_since = None self.next_retry = 0 self.failures = 0 self.report(OUTPUT + " disconnected; no mode changes") return False if state.healthy: self.bad_since = None self.next_retry = 0 self.failures = 0 self.report("healthy: " + MODE) return True self.report("mode drift: " + (state.active_mode or "off") + " " + state.geometry) if self.bad_since is None: self.bad_since = now if now - self.bad_since < SETTLE_SECONDS or now < self.next_retry: return False try: if not state.defined: self.run("--newmode", MODE, *TIMING) if MODE not in state.modes: self.run("--addmode", OUTPUT, MODE) self.run("--output", OUTPUT, "--mode", MODE, "--pos", "0x0") if not parse_state(self.run("--current")).healthy: raise RuntimeError("mode was not retained after apply") except (OSError, RuntimeError, subprocess.TimeoutExpired) as error: self.failures = min(self.failures + 1, 6) delay = min(60, 2 ** self.failures) # Base backoff on completion, including a slow/timed-out modeset. self.next_retry = time.monotonic() + delay self.log("restore failed; retry in %ds: %s" % (delay, str(error)[:400])) return False self.bad_since = None self.next_retry = 0 self.failures = 0 self.report("restored: " + MODE + " (stream process untouched)") return True def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--once", action="store_true", help="initial display recovery with a 20s retry window, then exit") args = parser.parse_args() os.environ.setdefault("DISPLAY", ":0") os.environ.setdefault("XAUTHORITY", str(Path.home() / ".Xauthority")) runtime = Path(os.environ.get("XDG_RUNTIME_DIR", str(Path.home() / ".cache/vdi-terminal"))) runtime.mkdir(parents=True, exist_ok=True) # Same lock for startup and the systemd service. Never run competing guards. with (runtime / "vdi-display-guard.lock").open("a") as lock: try: fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB) except BlockingIOError: log("display guard already running") return 0 stop = False def request_stop(signum, frame): nonlocal stop stop = True signal.signal(signal.SIGTERM, request_stop) signal.signal(signal.SIGINT, request_stop) guard = Guard() deadline = time.monotonic() + 20 while not stop: if guard.step(time.monotonic()) and args.once: return 0 if args.once and time.monotonic() >= deadline: return 1 time.sleep(POLL_SECONDS) return 0 if __name__ == "__main__": sys.exit(main())