Add auxiliary monitor configuration and enhance trigger logic for KVM switching

This commit is contained in:
Lago
2026-03-27 15:36:37 +01:00
parent 208ad243a7
commit c8ab5ad9bc
7 changed files with 310 additions and 43 deletions
+71 -11
View File
@@ -11,7 +11,7 @@ from app.config import (
SUPPORTED_TARGET_PORTS,
)
from app.ddm import DDMBackend, RealDDMBackend
from app.hardware import HardwareScan, MonitorBackend, RealMonitorBackend
from app.hardware import HardwareScan, MonitorBackend, RealMonitorBackend, TriggerMonitorCandidate
@dataclass(slots=True)
@@ -24,8 +24,10 @@ class ServiceStatus:
samsung_session_attempt_count: int = 0
waiting_for_samsung_disconnect: bool = False
trigger_input_code: int | None = None
active_trigger_monitor_id: str | None = None
trigger_target_port: str | None = None
trigger_matches_device_port: bool = False
trigger_monitor_candidates: list[dict[str, int | str | None]] = field(default_factory=list)
alienware_detected: bool = False
alienware_input_code: int | None = None
ddm_slot: int | None = None
@@ -44,8 +46,10 @@ class ServiceStatus:
"samsung_session_attempt_count": self.samsung_session_attempt_count,
"waiting_for_samsung_disconnect": self.waiting_for_samsung_disconnect,
"trigger_input_code": self.trigger_input_code,
"active_trigger_monitor_id": self.active_trigger_monitor_id,
"trigger_target_port": self.trigger_target_port,
"trigger_matches_device_port": self.trigger_matches_device_port,
"trigger_monitor_candidates": list(self.trigger_monitor_candidates),
"alienware_detected": self.alienware_detected,
"alienware_input_code": self.alienware_input_code,
"ddm_slot": self.ddm_slot,
@@ -100,8 +104,11 @@ class KvmSwitcherService:
with self._state_lock:
return self._status.to_dict()
def save_settings(self, device_port: str) -> dict[str, Any]:
new_config = AppConfig(device_port=device_port)
def save_settings(self, device_port: str, auxiliary_monitor_id: str | None = None) -> dict[str, Any]:
new_config = AppConfig(
device_port=device_port,
auxiliary_monitor_id=auxiliary_monitor_id,
)
self.config_store.save(new_config)
return self.poll_once()
@@ -112,7 +119,11 @@ class KvmSwitcherService:
errors = list(scan.errors)
errors.extend(config.validate())
blocking_errors = [error for error in errors if _is_blocking_error(error)]
trigger_input_code, active_trigger_monitor_id, trigger_candidates = self._resolve_trigger_state(
config=config,
scan=scan,
errors=errors,
)
ddm_slot = self.ddm_backend.resolve_alienware_slot(force=False)
supports_monitor_targeting = bool(
@@ -125,9 +136,10 @@ class KvmSwitcherService:
errors.append("Alienware monitorcontrol backend is not ready.")
elif ddm_slot is None and not supports_monitor_targeting:
errors.append("Alienware monitorcontrol target could not be resolved.")
blocking_errors = [error for error in errors if _is_blocking_error(error)]
trigger_target_port = self._port_name_for_input_code(scan.trigger_input_code)
desired_port = self._resolve_desired_port(config, scan)
trigger_target_port = self._port_name_for_input_code(trigger_input_code)
desired_port = self._resolve_desired_port(config, trigger_input_code)
trigger_matches_device_port = desired_port is not None
last_switch_result = "idle"
with self._state_lock:
@@ -150,6 +162,7 @@ class KvmSwitcherService:
self._samsung_session_successful = True
else:
last_switch_result, last_switch_at, scan, errors = self._attempt_switch_sequence(
config=config,
ddm_slot=ddm_slot,
desired_port=desired_port,
scan=scan,
@@ -171,9 +184,11 @@ class KvmSwitcherService:
samsung_session_successful=self._samsung_session_successful,
samsung_session_attempt_count=self._samsung_session_attempt_count,
waiting_for_samsung_disconnect=self._samsung_session_successful or self._samsung_session_attempt_count >= 3,
trigger_input_code=scan.trigger_input_code,
trigger_input_code=trigger_input_code,
active_trigger_monitor_id=active_trigger_monitor_id,
trigger_target_port=trigger_target_port,
trigger_matches_device_port=trigger_matches_device_port,
trigger_monitor_candidates=[candidate.to_dict() for candidate in trigger_candidates],
alienware_detected=scan.alienware_detected,
alienware_input_code=scan.alienware_input_code,
ddm_slot=ddm_slot,
@@ -201,9 +216,8 @@ class KvmSwitcherService:
@staticmethod
def _resolve_desired_port(
config: AppConfig,
scan: HardwareScan,
trigger_input: int | None,
) -> str | None:
trigger_input = scan.trigger_input_code
if trigger_input is None:
return None
desired_codes = KvmSwitcherService._port_input_codes(config.device_port)
@@ -243,6 +257,7 @@ class KvmSwitcherService:
def _attempt_switch_sequence(
self,
config: AppConfig,
ddm_slot: int | None,
desired_port: str,
scan: HardwareScan,
@@ -274,8 +289,13 @@ class KvmSwitcherService:
if not verify_scan.samsung_present:
return "waiting_for_reconnect", last_switch_at, scan, errors
verify_trigger_input, _, _ = self._resolve_trigger_state(
config=config,
scan=verify_scan,
errors=errors,
)
desired_codes = self._port_input_codes(desired_port)
if verify_scan.trigger_input_code not in desired_codes:
if verify_trigger_input not in desired_codes:
return "waiting_for_trigger_match", last_switch_at, scan, errors
if verify_scan.alienware_input_code is None:
errors.append(
@@ -290,6 +310,39 @@ class KvmSwitcherService:
return "max_attempts_waiting_for_disconnect", last_switch_at, scan, errors
@staticmethod
def _resolve_trigger_state(
config: AppConfig,
scan: HardwareScan,
errors: list[str],
) -> tuple[int | None, str | None, list[TriggerMonitorCandidate]]:
trigger_input_code = scan.trigger_input_code
active_trigger_monitor_id = scan.trigger_monitor_id
candidates = list(scan.trigger_candidates)
configured_id = config.auxiliary_monitor_id
if configured_id is None:
return trigger_input_code, active_trigger_monitor_id, candidates
configured_candidate = next(
(candidate for candidate in candidates if candidate.id == configured_id),
None,
)
if configured_candidate is None:
errors.append(
f"Configured auxiliary monitor '{configured_id}' was not found among detected trigger monitors."
)
return trigger_input_code, active_trigger_monitor_id, candidates
errors[:] = [error for error in errors if not _is_trigger_ambiguity_error(error)]
trigger_input_code = configured_candidate.input_code
active_trigger_monitor_id = configured_candidate.id
if trigger_input_code is None:
errors.append(
f"Unable to read configured auxiliary monitor input source: {configured_candidate.label}."
)
return trigger_input_code, active_trigger_monitor_id, candidates
def _dedupe_errors(errors: list[str]) -> list[str]:
unique: list[str] = []
@@ -308,6 +361,13 @@ def _is_blocking_error(error: str) -> bool:
non_blocking_prefixes = (
"Unable to read Alienware target monitor input source:",
"Alienware input could not be read after switch; assuming success because switch command completed.",
"Samsung trigger monitor did not expose a Samsung model in DDC/CI; using the only non-Alienware DDC monitor as trigger.",
)
return not any(normalized.startswith(prefix) for prefix in non_blocking_prefixes)
def _is_trigger_ambiguity_error(error: str) -> bool:
normalized = error.strip()
return normalized in {
"Multiple Samsung DDC monitors were detected; trigger monitor is ambiguous.",
"Multiple non-Alienware DDC monitors were detected and Samsung did not expose model info; trigger monitor is ambiguous.",
}