2026-05-24 10:49:28 +08:00
|
|
|
|
"""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` 变量;
|
|
|
|
|
|
所有输入都是显式参数,便于单测。
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
|
|
import logging
|
|
|
|
|
|
import threading
|
|
|
|
|
|
|
|
|
|
|
|
from app.ucd_domain import (
|
|
|
|
|
|
Colorimetry,
|
|
|
|
|
|
DynamicRange,
|
|
|
|
|
|
EventBus,
|
|
|
|
|
|
PatternKind,
|
|
|
|
|
|
PatternSpec,
|
|
|
|
|
|
SignalFormat,
|
|
|
|
|
|
TimingSpec,
|
|
|
|
|
|
UcdError,
|
|
|
|
|
|
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_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 的写入与提交。
|
|
|
|
|
|
|
|
|
|
|
|
使用线程锁串行化所有对外的 ``apply_*`` 调用,避免多个测试线程
|
|
|
|
|
|
同时操作 UCD 造成 SDK 状态错乱。
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
def __init__(self, device: IUcdDevice, bus: EventBus):
|
|
|
|
|
|
self._dev = device
|
|
|
|
|
|
self._bus = bus
|
|
|
|
|
|
self._lock = threading.RLock()
|
|
|
|
|
|
|
|
|
|
|
|
# -- 高层接口 ------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
def apply(
|
|
|
|
|
|
self,
|
|
|
|
|
|
*,
|
|
|
|
|
|
signal: SignalFormat,
|
|
|
|
|
|
timing: TimingSpec,
|
|
|
|
|
|
pattern: PatternSpec,
|
|
|
|
|
|
) -> bool:
|
|
|
|
|
|
"""一次性提交信号格式 + timing + 图案。
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
``format_changed``——本次相对上一次 :meth:`apply` 是否变化。
|
|
|
|
|
|
"""
|
|
|
|
|
|
with self._lock:
|
|
|
|
|
|
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。"""
|
|
|
|
|
|
with self._lock:
|
|
|
|
|
|
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))
|
|
|
|
|
|
|
2026-05-24 11:02:37 +08:00
|
|
|
|
# -- 过渡期 API(Phase 2)-----------------------------------
|
|
|
|
|
|
# 现有 GUI 回调以"仅更新信号格式、不切换图案"的方式调用
|
|
|
|
|
|
# ``ucd.apply_signal_format(color_space=..., color_format=..., bit_depth=...)``。
|
|
|
|
|
|
# 新代码统一通过本方法走 SignalService;内部仍委托给底层
|
|
|
|
|
|
# controller 的同名旧接口,迁移完成后将替换为纯净实现。
|
|
|
|
|
|
|
|
|
|
|
|
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:
|
|
|
|
|
|
"""仅将信号格式 stage 到 SDK(沿用上一次的 timing),不切换图案。
|
|
|
|
|
|
|
|
|
|
|
|
UI 字符串先经域层解析做参数校验;解析失败抛 :class:`UcdConfigError`。
|
|
|
|
|
|
"""
|
|
|
|
|
|
# 解析仅做校验;当前实现走 raw controller 的旧 API
|
|
|
|
|
|
_ = build_signal_format(
|
|
|
|
|
|
color_space=color_space,
|
|
|
|
|
|
output_format=output_format,
|
|
|
|
|
|
bit_depth=bit_depth,
|
|
|
|
|
|
data_range=data_range,
|
|
|
|
|
|
)
|
|
|
|
|
|
ctrl = getattr(self._dev, "raw_controller", None)
|
|
|
|
|
|
if ctrl is None:
|
|
|
|
|
|
raise UcdError("update_signal_format 暂仅支持 UCD323Device")
|
|
|
|
|
|
with self._lock:
|
|
|
|
|
|
return bool(
|
|
|
|
|
|
ctrl.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,
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-05-24 10:49:28 +08:00
|
|
|
|
# -- 透传给上层的查询 ---------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
|
def device(self) -> IUcdDevice:
|
|
|
|
|
|
return self._dev
|
|
|
|
|
|
|
|
|
|
|
|
def current_resolution(self) -> tuple[int, int]:
|
|
|
|
|
|
return self._dev.current_resolution()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
__all__ = [
|
|
|
|
|
|
"SignalService",
|
|
|
|
|
|
"build_signal_format",
|
|
|
|
|
|
"build_timing",
|
|
|
|
|
|
"solid_rgb_pattern",
|
|
|
|
|
|
"image_pattern",
|
|
|
|
|
|
# 重导出常用域类型方便上层 import 一次到位
|
|
|
|
|
|
"SignalFormat",
|
|
|
|
|
|
"TimingSpec",
|
|
|
|
|
|
"PatternSpec",
|
|
|
|
|
|
"PatternKind",
|
|
|
|
|
|
"Colorimetry",
|
|
|
|
|
|
"DynamicRange",
|
|
|
|
|
|
"UcdError",
|
|
|
|
|
|
]
|