Compare commits
1 Commits
6120625fa9
...
d1f621f0b4
| Author | SHA1 | Date | |
|---|---|---|---|
| d1f621f0b4 |
@@ -267,20 +267,27 @@ def _probe_static_open(mon, mc, monitor_id: int) -> dict:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.debug("Monitor %d: color_preset unsupported: %s", monitor_id, e)
|
logger.debug("Monitor %d: color_preset unsupported: %s", monitor_id, e)
|
||||||
|
|
||||||
# Picture / scene mode (VCP 0xDC) — not exposed by monitorcontrol's
|
# Picture / scene mode (VCP 0xDC). Trickier than color preset because
|
||||||
# high-level API, so probe via raw VCP transport.
|
# many monitors (LG ultrawides included) respond to READS but silently
|
||||||
|
# drop every WRITE - they implement the register but not the feature.
|
||||||
|
# The capability string is the most reliable signal: a monitor that
|
||||||
|
# really implements picture mode declares its supported codes under
|
||||||
|
# cmds[0xDC]. If 0xDC isn't declared, treat the feature as unsupported
|
||||||
|
# to avoid exposing a non-functional select.
|
||||||
|
cmds = caps.get("cmds") or {}
|
||||||
|
declared = cmds.get(PICTURE_MODE_VCP)
|
||||||
|
if declared:
|
||||||
try:
|
try:
|
||||||
mon.vcp.get_vcp_feature(PICTURE_MODE_VCP)
|
mon.vcp.get_vcp_feature(PICTURE_MODE_VCP)
|
||||||
static["picture_mode_supported"] = True
|
static["picture_mode_supported"] = True
|
||||||
cmds = caps.get("cmds") or {}
|
|
||||||
declared = cmds.get(PICTURE_MODE_VCP)
|
|
||||||
codes = sorted(declared) if declared else sorted(PICTURE_MODE_LABELS.keys())
|
|
||||||
static["available_picture_modes"] = [
|
static["available_picture_modes"] = [
|
||||||
{"code": c, "label": PICTURE_MODE_LABELS.get(c, f"Mode {c}")}
|
{"code": c, "label": PICTURE_MODE_LABELS.get(c, f"Mode {c}")}
|
||||||
for c in codes
|
for c in sorted(declared)
|
||||||
]
|
]
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.debug("Monitor %d: picture_mode unsupported: %s", monitor_id, e)
|
logger.debug("Monitor %d: picture_mode declared but unreadable: %s", monitor_id, e)
|
||||||
|
else:
|
||||||
|
logger.debug("Monitor %d: picture_mode (VCP 0xDC) not declared in capability string", monitor_id)
|
||||||
|
|
||||||
return static
|
return static
|
||||||
|
|
||||||
@@ -498,6 +505,31 @@ def set_power(monitor_id: int, on: bool) -> bool:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _verify_after_set(getter, expected, *, retries: int = 3, delay: float = 0.1) -> bool:
|
||||||
|
"""Poll a DDC/CI getter to confirm the monitor actually applied a write.
|
||||||
|
|
||||||
|
DDC/CI writes are fire-and-forget at the protocol level: a successful
|
||||||
|
send does not mean the monitor honored the value. Many monitors silently
|
||||||
|
drop writes for codes their firmware doesn't really implement (LG's
|
||||||
|
ColorPreset / Picture Mode are common offenders). Without this check the
|
||||||
|
API would report `success: true` while the monitor sat unchanged.
|
||||||
|
|
||||||
|
Compares both raw and `.value` forms so enum/int mismatches don't flag a
|
||||||
|
spurious failure.
|
||||||
|
"""
|
||||||
|
expected_int = getattr(expected, "value", expected)
|
||||||
|
for _ in range(retries):
|
||||||
|
time.sleep(delay)
|
||||||
|
try:
|
||||||
|
actual = getter()
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
actual_int = getattr(actual, "value", actual)
|
||||||
|
if actual == expected or actual_int == expected_int:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def set_contrast(monitor_id: int, value: int) -> bool:
|
def set_contrast(monitor_id: int, value: int) -> bool:
|
||||||
"""Set contrast for a specific monitor (0-100) via DDC/CI."""
|
"""Set contrast for a specific monitor (0-100) via DDC/CI."""
|
||||||
mc = _load_monitorcontrol()
|
mc = _load_monitorcontrol()
|
||||||
@@ -511,6 +543,9 @@ def set_contrast(monitor_id: int, value: int) -> bool:
|
|||||||
return False
|
return False
|
||||||
with ddc_monitors[monitor_id] as monitor:
|
with ddc_monitors[monitor_id] as monitor:
|
||||||
monitor.set_contrast(value)
|
monitor.set_contrast(value)
|
||||||
|
if not _verify_after_set(monitor.get_contrast, value):
|
||||||
|
logger.warning("Monitor %d: contrast %d not applied", monitor_id, value)
|
||||||
|
return False
|
||||||
_invalidate_cache()
|
_invalidate_cache()
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -536,6 +571,11 @@ def set_input_source(monitor_id: int, source: str) -> bool:
|
|||||||
return False
|
return False
|
||||||
with ddc_monitors[monitor_id] as monitor:
|
with ddc_monitors[monitor_id] as monitor:
|
||||||
monitor.set_input_source(target)
|
monitor.set_input_source(target)
|
||||||
|
# Source switches can briefly disrupt the DDC/CI link; allow a
|
||||||
|
# longer settle window before declaring failure.
|
||||||
|
if not _verify_after_set(monitor.get_input_source, target, retries=5, delay=0.2):
|
||||||
|
logger.warning("Monitor %d: input source %s not applied", monitor_id, source)
|
||||||
|
return False
|
||||||
_invalidate_cache()
|
_invalidate_cache()
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -561,6 +601,12 @@ def set_color_preset(monitor_id: int, preset: str) -> bool:
|
|||||||
return False
|
return False
|
||||||
with ddc_monitors[monitor_id] as monitor:
|
with ddc_monitors[monitor_id] as monitor:
|
||||||
monitor.set_color_preset(target)
|
monitor.set_color_preset(target)
|
||||||
|
if not _verify_after_set(monitor.get_color_preset, target):
|
||||||
|
logger.warning(
|
||||||
|
"Monitor %d: color preset %s not applied (monitor silently rejected)",
|
||||||
|
monitor_id, preset,
|
||||||
|
)
|
||||||
|
return False
|
||||||
_invalidate_cache()
|
_invalidate_cache()
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -584,6 +630,16 @@ def set_picture_mode(monitor_id: int, code: int) -> bool:
|
|||||||
return False
|
return False
|
||||||
with ddc_monitors[monitor_id] as monitor:
|
with ddc_monitors[monitor_id] as monitor:
|
||||||
monitor.vcp.set_vcp_feature(PICTURE_MODE_VCP, code)
|
monitor.vcp.set_vcp_feature(PICTURE_MODE_VCP, code)
|
||||||
|
# Raw VCP read returns (current, maximum) — only compare current.
|
||||||
|
def _read_picture_mode():
|
||||||
|
current, _ = monitor.vcp.get_vcp_feature(PICTURE_MODE_VCP)
|
||||||
|
return current
|
||||||
|
if not _verify_after_set(_read_picture_mode, code):
|
||||||
|
logger.warning(
|
||||||
|
"Monitor %d: picture mode code %d not applied (monitor silently rejected)",
|
||||||
|
monitor_id, code,
|
||||||
|
)
|
||||||
|
return False
|
||||||
_invalidate_cache()
|
_invalidate_cache()
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
Reference in New Issue
Block a user