Files
pqAutomationApp/app/ucd/service.py
2026-06-11 16:29:36 +08:00

413 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""UCD 服务层SignalService硬件编排+ PatternService测试发图"""
from __future__ import annotations
import logging
from app.ucd.domain import (
EventBus,
PatternSpec,
SignalFormat,
TimingSpec,
UcdState,
image_pattern,
solid_rgb_pattern,
)
from app.ucd.device import IUcdDevice
log = logging.getLogger(__name__)
# ─── SignalService ────────────────────────────────────────────────────────
class SignalService:
"""协调 SignalFormat / Timing / Pattern 的写入与提交。"""
def __init__(self, device: IUcdDevice, bus: EventBus):
self._dev = device
self._bus = bus
# -- 高层接口 ------------------------------------------------
def apply(
self,
*,
signal: SignalFormat,
timing: TimingSpec,
pattern: PatternSpec,
) -> bool:
"""一次性提交信号格式 + timing + 图案。
Returns:
``format_changed``——本次相对上一次 :meth:`apply` 是否变化。
"""
log.info(
"SignalService.apply signal=%s timing=%s pattern=%s",
signal,
timing,
pattern.kind.value,
)
changed = self._dev.configure(signal, timing)
self._dev.set_pattern(pattern)
self._dev.apply()
return changed
def send_pattern(self, pattern: PatternSpec) -> None:
"""在已 configure 的信号上仅更新图案后 apply。"""
log.info("SignalService.send_pattern pattern=%s", pattern.kind.value)
self._dev.set_pattern(pattern)
self._dev.apply()
def send_solid_rgb(self, rgb: tuple[int, int, int] | list[int]) -> None:
self.send_pattern(solid_rgb_pattern(rgb))
def send_image(self, path: str) -> None:
self.send_pattern(image_pattern(path))
def update_signal_format(
self,
*,
color_space: str,
output_format: str,
bit_depth: str,
data_range: str = "Full",
max_cll: int | None = None,
max_fall: int | None = None,
) -> bool:
"""仅将信号格式提交到 SDK沿用上一次的 timing不切换图案。
UI 字符串先经域层解析做参数校验;解析失败抛 :class:`UcdConfigError`。
"""
_ = build_signal_format(
color_space=color_space,
output_format=output_format,
bit_depth=bit_depth,
data_range=data_range,
)
return self._dev.apply_signal_format(
color_space=color_space,
color_format=output_format,
bit_depth=bit_depth,
data_range=data_range,
max_cll=max_cll,
max_fall=max_fall,
)
# -- 透传给上层的查询 ---------------------------------------
@property
def device(self) -> IUcdDevice:
return self._dev
def current_resolution(self) -> tuple[int, int]:
return self._dev.current_resolution()
@property
def is_connected(self) -> bool:
"""UCD 设备是否已打开。供 GUI 做前置校验。"""
return self._dev.state != UcdState.CLOSED
@property
def format_changed(self) -> bool:
"""最近一次视频模式提交是否相对上次发生变化。"""
return self._dev.format_changed
@property
def last_error(self) -> str | None:
return self._dev.last_error
def apply_config(self, config) -> bool:
"""按 :class:`PQConfig` 写入色彩 / Timing / 当前 Pattern不 apply 输出)。"""
return bool(self._dev.set_ucd_params(config))
def send_pattern_params(self, params) -> bool:
"""以 ``params`` 更新当前 pattern 的参数并 apply。"""
return bool(self._dev.send_current_pattern_params(params))
def apply_and_run(self, config, pattern_params) -> bool:
"""``set_ucd_params`` + ``set_pattern`` + ``run`` 的复合操作。"""
return bool(self._dev.apply_config_and_run(config, pattern_params))
def stage_test_profile(
self,
config,
*,
color_space: str,
output_format: str,
bit_depth: str,
data_range: str = "Full",
max_cll: int | None = None,
max_fall: int | None = None,
) -> bool:
"""按 PQConfig stage 色彩/Timing/Pattern 类型,并提交 UI 覆盖的信号格式。
自动化测试在发图前调用;等价于 ``apply_config`` + ``update_signal_format``。
"""
if not self.apply_config(config):
return False
return self.update_signal_format(
color_space=color_space,
output_format=output_format,
bit_depth=bit_depth,
data_range=data_range,
max_cll=max_cll,
max_fall=max_fall,
)
__all__ = [
"SignalService",
"build_signal_format",
"build_signal_format_from_profile",
"build_timing",
"solid_rgb_pattern",
"image_pattern",
"SignalFormat",
"TimingSpec",
"PatternSpec",
"PatternKind",
"Colorimetry",
"DynamicRange",
"UcdError",
]
# --- PatternService ---
import copy
from dataclasses import dataclass
from app.data_range_converter import convert_pattern_params
from app.pq.pq_config import get_pattern
@dataclass
class PatternSession:
mode: str
test_type: str
active_config: object
pattern_params: list[list[int]]
total_patterns: int
display_names: list[str]
class PatternService:
def __init__(self, app):
self.app = app
def _build_apply_config_error(self, test_type):
timing = self.app.config.current_test_types.get(test_type, {}).get("timing", "-")
detail = ""
err = self.app.signal_service.last_error
if err:
detail = f", detail={err}"
return f"UCD profile apply_config failed for {test_type}, timing={timing}{detail}"
def _stage_profile(
self,
active_config,
*,
color_space: str,
output_format: str,
bit_depth: str,
data_range: str = "Full",
max_cll: int | None = None,
max_fall: int | None = None,
test_type: str,
log_details: bool = False,
log_title: str = "",
log_fields: list[tuple[str, str]] | None = None,
) -> None:
if log_details and log_title:
self._log("=" * 50, "separator")
self._log(log_title, "info")
self._log("=" * 50, "separator")
for label, value in log_fields or []:
self._log(f" {label}: {value}", "info")
if not self.app.signal_service.stage_test_profile(
active_config,
color_space=color_space,
output_format=output_format,
bit_depth=bit_depth,
data_range=data_range,
max_cll=max_cll,
max_fall=max_fall,
):
raise RuntimeError(self._build_apply_config_error(test_type))
if log_details:
self._log("信号格式设置成功", "success")
def prepare_session(self, mode, *, test_type=None, log_details=False):
test_type = test_type or self.app.config.current_test_type
if hasattr(self.app.config, "set_current_test_type"):
self.app.config.set_current_test_type(test_type)
if not self.app.config.set_current_pattern(mode):
raise ValueError(f"未知的图案模式: {mode}")
active_config = self.app.config
source_params = self._get_source_pattern_params(mode)
if test_type == "screen_module":
screen_cfg = self.app.config.current_test_types.get("screen_module", {})
color_space = (
self.app.screen_module_color_space_var.get()
if hasattr(self.app, "screen_module_color_space_var")
else screen_cfg.get("colorimetry", "sRGB")
)
data_range = (
self.app.screen_module_data_range_var.get()
if hasattr(self.app, "screen_module_data_range_var")
else screen_cfg.get("data_range", "Full")
)
bit_depth = (
self.app.screen_module_bit_depth_var.get()
if hasattr(self.app, "screen_module_bit_depth_var")
else f"{int(screen_cfg.get('bpc', 8))}bit"
)
output_format = (
self.app.screen_module_output_format_var.get()
if hasattr(self.app, "screen_module_output_format_var")
else screen_cfg.get("color_format", "RGB")
)
self._stage_profile(
active_config,
color_space=color_space,
data_range=data_range,
bit_depth=bit_depth,
output_format=output_format,
test_type=test_type,
log_details=log_details,
log_title="设置屏模组信号格式:",
log_fields=[
("色彩空间", color_space),
("色彩格式", output_format),
("数据范围", data_range),
("编码位深", bit_depth),
("Timing", self.app.config.current_test_types[test_type]["timing"]),
],
)
elif test_type == "sdr_movie":
data_range = self.app.sdr_data_range_var.get()
converted_params = convert_pattern_params(
source_params, data_range=data_range, verbose=False
)
active_config = self.app.config.get_temp_config_with_converted_params(
mode=mode, converted_params=converted_params
)
if hasattr(active_config, "set_current_test_type"):
active_config.set_current_test_type(test_type)
self._stage_profile(
active_config,
color_space=self.app.sdr_color_space_var.get(),
data_range=data_range,
bit_depth=self.app.sdr_bit_depth_var.get(),
output_format=self.app.sdr_output_format_var.get(),
test_type=test_type,
log_details=log_details,
log_title="设置 SDR 信号格式:",
log_fields=[
("色彩空间", self.app.sdr_color_space_var.get()),
("色彩格式", self.app.sdr_output_format_var.get()),
("Gamma", self.app.sdr_gamma_type_var.get()),
("数据范围", data_range),
("编码位深", self.app.sdr_bit_depth_var.get()),
],
)
if log_details:
self._log(f"图案参数已设置,共 {len(converted_params)} 个图案", "success")
elif test_type == "hdr_movie":
data_range = self.app.hdr_data_range_var.get()
converted_params = convert_pattern_params(
source_params, data_range=data_range, verbose=False
)
active_config = self.app.config.get_temp_config_with_converted_params(
mode=mode, converted_params=converted_params
)
if hasattr(active_config, "set_current_test_type"):
active_config.set_current_test_type(test_type)
self._stage_profile(
active_config,
color_space=self.app.hdr_color_space_var.get(),
data_range=data_range,
bit_depth=self.app.hdr_bit_depth_var.get(),
output_format=self.app.hdr_output_format_var.get(),
max_cll=self.app.hdr_maxcll_var.get(),
max_fall=self.app.hdr_maxfall_var.get(),
test_type=test_type,
log_details=log_details,
log_title="设置 HDR 信号格式:",
log_fields=[
("色彩空间", self.app.hdr_color_space_var.get()),
("色彩格式", self.app.hdr_output_format_var.get()),
("数据范围", data_range),
("编码位深", self.app.hdr_bit_depth_var.get()),
("MaxCLL", self.app.hdr_maxcll_var.get()),
("MaxFALL", self.app.hdr_maxfall_var.get()),
],
)
if log_details:
self._log(f"图案参数已设置,共 {len(converted_params)} 个图案", "success")
else:
raise ValueError(f"不支持的测试类型: {test_type}")
pattern_params = copy.deepcopy(active_config.current_pattern["pattern_params"])
return PatternSession(
mode=mode,
test_type=test_type,
active_config=active_config,
pattern_params=pattern_params,
total_patterns=len(pattern_params),
display_names=self._get_display_names(mode, len(pattern_params)),
)
def send_session_pattern(self, session, index):
if index < 0 or index >= session.total_patterns:
raise IndexError(f"pattern 索引越界: {index}")
pattern_param = session.pattern_params[index]
if not self.app.signal_service.send_pattern_params(pattern_param):
raise RuntimeError(f"发送 pattern 失败: {index}")
return pattern_param
def send_rgb(self, rgb, *, session=None, test_type=None):
active_session = session or self.prepare_session(
"rgb",
test_type=test_type,
log_details=False,
)
converted_rgb = self._convert_rgb_for_test_type(rgb, active_session.test_type)
self.app.signal_service.send_solid_rgb(converted_rgb)
return True
def _get_source_pattern_params(self, mode):
return copy.deepcopy(get_pattern(mode)["pattern_params"])
def _get_display_names(self, mode, total_patterns):
if mode == "accuracy":
return self.app.config.get_accuracy_color_names()
if mode == "custom" and hasattr(self.app.config, "get_temp_pattern_names"):
return self.app.config.get_temp_pattern_names()
return [f"P {index + 1}" for index in range(total_patterns)]
def _convert_rgb_for_test_type(self, rgb, test_type):
if test_type == "sdr_movie":
data_range = self.app.sdr_data_range_var.get()
elif test_type == "hdr_movie":
data_range = self.app.hdr_data_range_var.get()
else:
data_range = "Full"
return convert_pattern_params([list(rgb)], data_range=data_range, verbose=False)[0]
def _log(self, message, level):
if hasattr(self.app, "log_gui"):
self.app.log_gui.log(message, level=level)