根据新版本调整set_pattern:0~65535

This commit is contained in:
xinzhu.yin
2026-07-07 18:59:44 +08:00
parent 9fa811a9eb
commit 32aaef1c0f
24 changed files with 3016 additions and 279 deletions

View File

@@ -13,6 +13,20 @@ from app.ucd.device import (
list_devices,
)
from app.ucd.service import PatternService, PatternSession, SignalService
from app.solid_color_scale import (
SOLID_COLOR_MAX,
SOLID_COLOR_MIN,
SOLID_COLOR_MIN_BPC,
byte_to_solid_color,
clamp_solid_color,
ensure_solid_color_pattern,
migrate_pattern_params,
pack_solid_color_hw_registers,
pct_to_solid_color,
solid_color_rgb,
solid_color_to_byte,
solid_color_to_pct,
)
__all__ = [
"SignalService",
@@ -25,4 +39,15 @@ __all__ = [
"EventBus",
"ConnectionChanged",
"DeviceKind",
"SOLID_COLOR_MIN",
"SOLID_COLOR_MAX",
"SOLID_COLOR_MIN_BPC",
"clamp_solid_color",
"pct_to_solid_color",
"solid_color_to_pct",
"solid_color_to_byte",
"byte_to_solid_color",
"solid_color_rgb",
"migrate_pattern_params",
"ensure_solid_color_pattern",
]

View File

@@ -43,6 +43,8 @@ from app.ucd.domain import (
is_ycbcr,
)
from app.ucd.enum import UCDEnum
from app.ucd.pg_status import read_pattern_generator_status_lines
from app.solid_color_scale import SOLID_COLOR_MIN_BPC, clamp_solid_color, pack_solid_color_hw_registers
log = logging.getLogger(__name__)
_DEVICE_LOCK_TIMEOUT_SECONDS = 8.0
@@ -108,6 +110,7 @@ class _UcdSdkBackend:
self.current_pattern_params = None
self.current_pattern_index = 0
self.last_error = None
self._solid_color_stream_ready = False
def search_device(self):
"""搜索可用设备"""
@@ -213,6 +216,10 @@ class _UcdSdkBackend:
self.current_pattern_params = None
self.current_pattern_index = 0
self.current_interface = "HDMI"
self._solid_color_stream_ready = False
def _invalidate_solid_color_stream(self) -> None:
self._solid_color_stream_ready = False
def _force_cleanup(self):
"""强制清理所有状态"""
@@ -440,6 +447,7 @@ class _UcdSdkBackend:
pg, _ = self.get_tx_modules()
log.info("UcdSdk.set_video_mode calling pg.set_vm()")
pg.set_vm(vm=video_mode)
self._invalidate_solid_color_stream()
self._stop_audio_output()
log.info("UcdSdk.set_video_mode done")
self._last_sent_config = current_config
@@ -681,6 +689,104 @@ class _UcdSdkBackend:
log.warning("UcdSdk.set_pattern_params unsupported pattern=%s", getattr(pattern, "name", pattern))
return False
def _solid_color_pattern_ids(self):
return {
UCDEnum.VideoPatternInfo.VideoPatternParams.SolidColor,
UCDEnum.VideoPatternInfo.VideoPattern.SolidColor,
}
def _is_solid_color_pattern(self, pattern) -> bool:
return pattern in self._solid_color_pattern_ids()
def _write_pg_solid_color_registers(self, pg, rgb_list) -> None:
"""直接写入 SolidColor 硬件寄存器65535 刻度,无 SDK 左移)。"""
from ctypes import c_uint
from UniTAP.libs.lib_tsi import ci
param0, param1 = pack_solid_color_hw_registers(rgb_list)
pg._become_active()
pg._PatternGenerator__io.set(ci.TSI_PG_PREDEF_PATTERN_PARAM_1, param0, c_uint)
pg._PatternGenerator__io.set(ci.TSI_PG_PREDEF_PATTERN_PARAM_2, param1, c_uint)
log.info(
"UcdSdk._write_pg_solid_color_registers rgb=%s hw=(0x%08X, 0x%08X)",
rgb_list,
param0,
param1,
)
def apply_solid_color(self, rgb, *, force_reapply: bool = False) -> bool:
"""发送 SolidColor顺序对齐 ``work_with_solid_color.py``。
首次(或信号格式变化后):``set_pattern → set_vm → apply() → set_pattern_params``。
同一会话内后续仅 ``set_pattern_params``,避免重复 ``apply()`` 导致 HDMI 重锁、
灰阶测量出现锯齿形 Gamma 曲线。
"""
if not self.status or not self.role:
return False
try:
pg, _ = self.get_tx_modules()
pattern = UCDEnum.VideoPatternInfo.get_video_pattern("solidcolor")
if pattern is None:
log.error("UcdSdk.apply_solid_color failed: solidcolor pattern not found")
return False
rgb_list = [clamp_solid_color(v) for v in list(rgb)[:3]]
if len(rgb_list) < 3:
log.error("UcdSdk.apply_solid_color invalid rgb=%s", rgb)
return False
if self.current_timing is None:
self.current_timing = self._resolve_timing(pg)
if self.current_timing is None:
log.error("UcdSdk.apply_solid_color current_timing is None")
self.last_error = "current_timing is None"
return False
bpc_bumped = False
if self.color_info.bpc < SOLID_COLOR_MIN_BPC:
log.info(
"UcdSdk.apply_solid_color bump bpc %s -> %s for SolidColor video mode",
self.color_info.bpc,
SOLID_COLOR_MIN_BPC,
)
self.color_info.bpc = SOLID_COLOR_MIN_BPC
bpc_bumped = True
self.current_pattern = pattern
self.current_pattern_param = UniTAP.SolidColorParams(
first=rgb_list[0],
second=rgb_list[1],
third=rgb_list[2],
)
need_full_setup = (
force_reapply
or bpc_bumped
or not self._solid_color_stream_ready
)
if need_full_setup:
log.info("UcdSdk.apply_solid_color full setup rgb=%s", rgb_list)
pg.set_pattern(pattern=pattern)
self.set_video_mode()
if not self._apply_pg_output(pg):
log.error("UcdSdk.apply_solid_color pg.apply failed")
self._invalidate_solid_color_stream()
return False
self._solid_color_stream_ready = True
else:
log.info("UcdSdk.apply_solid_color params-only rgb=%s", rgb_list)
self._write_pg_solid_color_registers(pg, rgb_list)
log.info("UcdSdk.apply_solid_color done")
return True
except Exception:
log.exception("UcdSdk.apply_solid_color exception")
self._invalidate_solid_color_stream()
return False
def apply_pattern(self):
"""应用当前pattern"""
if self.current_pattern is not None:
@@ -715,6 +821,7 @@ class _UcdSdkBackend:
def set_ucd_params(self, config):
"""设置UCD323参数"""
self._invalidate_solid_color_stream()
self.last_error = None
self.config = config
test_type = self.config.current_test_type
@@ -799,6 +906,10 @@ class _UcdSdkBackend:
log.error("UcdSdk.send_current_pattern_params failed: set_pattern returned False")
return False
if self._is_solid_color_pattern(self.current_pattern):
log.info("UcdSdk.send_current_pattern_params using apply_solid_color")
return self.apply_solid_color(pattern_params)
log.info("UcdSdk.send_current_pattern_params calling run()")
self.run()
log.info("UcdSdk.send_current_pattern_params done")
@@ -814,12 +925,7 @@ class _UcdSdkBackend:
try:
log.info("UcdSdk.send_solid_rgb_pattern rgb=%s", rgb)
self.current_pattern = UCDEnum.VideoPatternInfo.get_video_pattern("solidcolor")
if self.current_pattern is None:
log.error("UcdSdk.send_solid_rgb_pattern failed: solidcolor pattern not found")
return False
return self.send_current_pattern_params(list(rgb))
return self.apply_solid_color(list(rgb))
except Exception:
log.exception("UcdSdk.send_solid_rgb_pattern exception")
return False
@@ -839,6 +945,20 @@ class _UcdSdkBackend:
except Exception:
return False
def get_pattern_generator_status_lines(
self,
pattern_hint: str | None = None,
) -> list[str]:
"""读取 PG 当前状态,格式对齐 UCD Console 的 Pattern Generator Status。"""
if not self.status or not self.role:
return []
try:
pg, _ = self.get_tx_modules()
return read_pattern_generator_status_lines(pg, pattern_hint=pattern_hint)
except Exception:
log.exception("UcdSdk.get_pattern_generator_status_lines failed")
return []
# ─── §3 DeviceInfo / list_devices ────────────────────────────────
@@ -1244,6 +1364,13 @@ class UCD323Device(IUcdDevice):
self._state = UcdState.APPLIED
return ok
def get_pattern_generator_status_lines(
self,
pattern_hint: str | None = None,
) -> list[str]:
with self._acquire_device_lock("get_pattern_generator_status_lines"):
return self._sdk.get_pattern_generator_status_lines(pattern_hint=pattern_hint)
# --- 内部 ---
def _apply_dynamic_range(self, signal: SignalFormat) -> None:
@@ -1262,6 +1389,13 @@ class UCD323Device(IUcdDevice):
raise UcdConfigError("IMAGE pattern 必须提供 image_path")
return bool(self._sdk.send_image_pattern(pattern.image_path))
if pattern.kind is PatternKind.SOLID:
if pattern.solid_rgb is None:
raise UcdConfigError("SOLID pattern 必须提供 solid_rgb")
if not self._sdk.apply_solid_color(list(pattern.solid_rgb)):
raise UcdApplyFailed("apply_solid_color 返回 False")
return True
# 预定义图案路径:复用 controller.set_pattern + run()
video_pattern = UCDEnum.VideoPatternInfo.get_video_pattern(pattern.kind.value)
@@ -1270,11 +1404,7 @@ class UCD323Device(IUcdDevice):
self._sdk.current_pattern = video_pattern
params: list[int] | None = None
if pattern.kind is PatternKind.SOLID:
if pattern.solid_rgb is None:
raise UcdConfigError("SOLID pattern 必须提供 solid_rgb")
params = list(pattern.solid_rgb)
elif pattern.extras:
if pattern.extras:
params = list(pattern.extras)
if not self._sdk.set_pattern(video_pattern, params):

111
app/ucd/pg_status.py Normal file
View File

@@ -0,0 +1,111 @@
"""从 UniTAP Pattern Generator 读取状态并格式化为日志行。"""
from __future__ import annotations
import logging
import os
import re
from typing import Any
import UniTAP
from UniTAP.common import ColorInfo
log = logging.getLogger(__name__)
def _enum_display_name(name: str) -> str:
spaced = re.sub(r"([a-z])([A-Z])", r"\1 \2", name)
return spaced.replace("_", " ")
def _format_pattern_name(pg, pattern_hint: str | None = None) -> str:
if pattern_hint:
return pattern_hint
pattern = getattr(pg, "_pattern", None)
if isinstance(pattern, UniTAP.VideoPattern):
return _enum_display_name(pattern.name)
if isinstance(pattern, str):
return os.path.basename(pattern) or "Image File"
pattern_id = getattr(pg, "_pattern_id", None)
if pattern_id is not None:
return _enum_display_name(getattr(pattern_id, "name", str(pattern_id)))
return ""
def _format_cef(ci: ColorInfo) -> str:
fmt_map = {
ColorInfo.ColorFormat.CF_RGB: "RGB",
ColorInfo.ColorFormat.CF_YCbCr_422: "YCbCr 4:2:2",
ColorInfo.ColorFormat.CF_YCbCr_444: "YCbCr 4:4:4",
ColorInfo.ColorFormat.CF_YCbCr_420: "YCbCr 4:2:0",
ColorInfo.ColorFormat.CF_Y_ONLY: "Y only",
ColorInfo.ColorFormat.CF_RAW: "RAW",
ColorInfo.ColorFormat.CF_DSC: "DSC",
}
fmt = fmt_map.get(
ci.color_format,
ci.color_format.name.replace("CF_", "").replace("_", " "),
)
if ci.color_format == ColorInfo.ColorFormat.CF_RGB:
range_str = (
"Legacy RGB mode"
if ci.dynamic_range == ColorInfo.DynamicRange.DR_VESA
else "Limited range"
)
elif ci.dynamic_range == ColorInfo.DynamicRange.DR_VESA:
range_str = "Full range"
else:
range_str = "Limited range"
return f"{fmt}/{range_str}"
def _max_bitrate_gbps(pg, bpp: int) -> float:
caps = pg._caps()
effective_bpp = bpp if bpp > 0 else 24
return round(caps.max_pix_rate * effective_bpp / 1_000_000_000.0, 3)
def collect_pattern_generator_status(
pg,
*,
pattern_hint: str | None = None,
) -> dict[str, Any]:
vm = pg.get_stream_video_mode()
pg_status = pg.status()
ci = vm.color_info
refresh_hz = vm.timing.frame_rate / 1000.0 if vm.timing.frame_rate else 0.0
timing = f"{vm.timing.hactive} x {vm.timing.vactive} @ {refresh_hz:g}Hz"
current_br = vm.bit_rate
max_br = _max_bitrate_gbps(pg, ci.bpp)
return {
"total_bitrate": f"{current_br:.3f} / {max_br:.3f} Gbps",
"timing": timing,
"pattern": _format_pattern_name(pg, pattern_hint),
"cef": _format_cef(ci),
"bpc": str(ci.bpc),
"status": str(pg_status.error),
}
def format_pattern_generator_status_log(status: dict[str, Any]) -> list[str]:
return [
f"Total bitrate: {status['total_bitrate']}",
f" Timing: {status['timing']}",
f" Pattern: {status['pattern']}",
f" CEF: {status['cef']}",
f" BPC: {status['bpc']}",
f" Status: {status['status']}",
]
def read_pattern_generator_status_lines(
pg,
*,
pattern_hint: str | None = None,
) -> list[str]:
status = collect_pattern_generator_status(pg, pattern_hint=pattern_hint)
return format_pattern_generator_status_log(status)

View File

@@ -2,6 +2,7 @@
from __future__ import annotations
import logging
import os
from app.ucd.domain import (
Colorimetry,
@@ -33,6 +34,21 @@ class SignalService:
def __init__(self, device: IUcdDevice, bus: EventBus):
self._dev = device
self._bus = bus
self._pg_status_log_callback = None
def set_pg_status_log_callback(self, callback) -> None:
"""设置 PG 状态日志回调:``callback(lines: list[str]) -> None``。"""
self._pg_status_log_callback = callback
def _notify_pattern_sent(self, pattern_hint: str | None = None) -> None:
if not self._pg_status_log_callback:
return
try:
lines = self.get_pattern_generator_status_lines(pattern_hint)
if lines:
self._pg_status_log_callback(lines)
except Exception:
log.exception("SignalService._notify_pattern_sent failed")
# -- 高层接口 ------------------------------------------------
@@ -59,17 +75,30 @@ class SignalService:
self._dev.apply()
return changed
def send_pattern(self, pattern: PatternSpec) -> None:
def send_pattern(
self,
pattern: PatternSpec,
*,
pattern_hint: str | None = None,
) -> None:
"""在已 configure 的信号上仅更新图案后 apply。"""
log.info("SignalService.send_pattern pattern=%s", pattern.kind.value)
self._dev.set_pattern(pattern)
self._dev.apply()
self._notify_pattern_sent(pattern_hint)
def send_solid_rgb(self, rgb: tuple[int, int, int] | list[int]) -> None:
self.send_pattern(solid_rgb_pattern(rgb))
rgb_list = list(rgb)
self.send_pattern(
solid_rgb_pattern(rgb_list),
pattern_hint=f"Solid Color {rgb_list}",
)
def send_image(self, path: str) -> None:
self.send_pattern(image_pattern(path))
self.send_pattern(
image_pattern(path),
pattern_hint=os.path.basename(path) or "Image File",
)
def update_signal_format(
self,
@@ -127,9 +156,26 @@ class SignalService:
"""按 :class:`PQConfig` 写入色彩 / Timing / 当前 Pattern不 apply 输出)。"""
return bool(self._dev.set_ucd_params(config))
def send_pattern_params(self, params) -> bool:
def send_pattern_params(
self,
params,
*,
pattern_hint: str | None = None,
) -> bool:
"""以 ``params`` 更新当前 pattern 的参数并 apply。"""
return bool(self._dev.send_current_pattern_params(params))
ok = bool(self._dev.send_current_pattern_params(params))
if ok:
self._notify_pattern_sent(pattern_hint)
return ok
def get_pattern_generator_status_lines(
self,
pattern_hint: str | None = None,
) -> list[str]:
getter = getattr(self._dev, "get_pattern_generator_status_lines", None)
if not callable(getter):
return []
return list(getter(pattern_hint) or [])
def apply_and_run(self, config, pattern_params) -> bool:
"""``set_ucd_params`` + ``set_pattern`` + ``run`` 的复合操作。"""
@@ -380,7 +426,13 @@ class PatternService:
raise IndexError(f"pattern 索引越界: {index}")
pattern_param = session.pattern_params[index]
if not self.app.signal_service.send_pattern_params(pattern_param):
pattern_hint = None
if session.display_names and index < len(session.display_names):
pattern_hint = session.display_names[index]
if not self.app.signal_service.send_pattern_params(
pattern_param,
pattern_hint=pattern_hint,
):
raise RuntimeError(f"发送 pattern 失败: {index}")
return pattern_param

View File

@@ -0,0 +1,2 @@
"""兼容层:请改用 ``app.solid_color_scale``。"""
from app.solid_color_scale import * # noqa: F403