"""UCD 信号 / 图案应用服务层。 服务层是 GUI ↔ Driver 的唯一通道,负责: - 将 UI 字符串("BT.709"、"10bit"、"YCbCr 4:4:4" 等)翻译成 :class:`SignalFormat`; - 将各 panel 的 timing 字符串翻译成 :class:`TimingSpec`; - 协调 :meth:`IUcdDevice.configure` / ``set_pattern`` / ``apply`` 的调用顺序; - 通过 :class:`EventBus` 让 GUI 订阅状态变化,而非主动轮询。 本层不直接 import UniTAP,也不读取 :mod:`tkinter` 变量; 所有输入都是显式参数,便于单测。 线程安全由 :class:`UCD323Device` 的设备锁统一保证,本层不再重复加锁。 """ from __future__ import annotations import logging from app.ucd_domain import ( Colorimetry, DynamicRange, EventBus, PatternKind, PatternSpec, SignalFormat, TimingSpec, UcdError, UcdState, bit_depth_str_to_bpc, color_space_to_colorimetry, data_range_to_dynamic_range, output_format_to_color_format, parse_timing_str, ) from drivers.ucd_driver import IUcdDevice log = logging.getLogger(__name__) # ─── 视图字符串 → 值对象 转换工具 ──────────────────────────────── def build_signal_format( *, color_space: str, output_format: str, bit_depth: str, data_range: str = "Full", ) -> SignalFormat: """根据下拉框字符串组装 :class:`SignalFormat`。 各参数解析失败抛 :class:`UcdConfigError`。 """ return SignalFormat( color_format=output_format_to_color_format(output_format), colorimetry=color_space_to_colorimetry(color_space), bpc=bit_depth_str_to_bpc(bit_depth), dynamic_range=data_range_to_dynamic_range(data_range), ) def build_signal_format_from_profile( *, color_space: str, color_format: str, bpc: int, data_range: str = "Full", ) -> SignalFormat: """从 PQConfig test_type 条目组装 :class:`SignalFormat`。""" bit_depth = f"{int(bpc)}bit" return build_signal_format( color_space=color_space, output_format=color_format, bit_depth=bit_depth, data_range=data_range, ) def build_timing(timing_str: str) -> TimingSpec: """``"DMT 3840x2160@60Hz"`` → :class:`TimingSpec`。""" return parse_timing_str(timing_str) def solid_rgb_pattern(rgb: tuple[int, int, int] | list[int]) -> PatternSpec: r, g, b = rgb[0], rgb[1], rgb[2] return PatternSpec(kind=PatternKind.SOLID, solid_rgb=(int(r), int(g), int(b))) def image_pattern(path: str) -> PatternSpec: return PatternSpec(kind=PatternKind.IMAGE, image_path=path) # ─── 服务 ──────────────────────────────────────────────────────── 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)) __all__ = [ "SignalService", "build_signal_format", "build_signal_format_from_profile", "build_timing", "solid_rgb_pattern", "image_pattern", "SignalFormat", "TimingSpec", "PatternSpec", "PatternKind", "Colorimetry", "DynamicRange", "UcdError", ]