feat(HDMI-Tool): v0.2.0 集成光机控制(RS485→GD32网关→DLPC3421) — 本地提交, 待验证后再推送发布

This commit is contained in:
2026-09-11 00:25:28 +08:00
parent efaf4d658f
commit 2459b33162
8 changed files with 732 additions and 82 deletions
+3
View File
@@ -9,3 +9,6 @@ dist/
# 发布 zip: 可由 src/package_release.py 重新生成, 只上传 Gitea Release # 发布 zip: 可由 src/package_release.py 重新生成, 只上传 Gitea Release
HDMI-Tool/release/*.zip HDMI-Tool/release/*.zip
# ZCode 客户端本地会话数据(计划留档等), 非项目配置
.zcode/
+43 -21
View File
@@ -7,9 +7,11 @@
## 1. 项目定位 ## 1. 项目定位
仓库 `LT8619C_Debug_Tools` 是 LT8619CHDMI→RGB888)→ DLPC3421 投影转接板的**调试上位机工具集**, 仓库 `LT8619C_Debug_Tools` 是 LT8619CHDMI→RGB888)→ DLPC3421 投影转接板的**调试上位机工具集**,
上游主仓库(固件)为 `hulk/LT8619C_DLPC3421_HDMI`(同 Gitea 实例 `gitea.hulk.wang` 上游主仓库(固件)为 `hulk/LT8619C_DLPC3421_HDMI`(同 Gitea 实例 `gitea.hulk.wang`
工具工作在 HDMI **源端**(PC 显卡侧),与固件侧寄存器调试(I2C-Inject 工具)配合 仓库本机路径 `E:\Hulk-Coding\LT8619C_DLPC3421_HDMI`
寄存器语义判读基线(0x606E 位交换 / 0x606D 整组顺序)在上游仓库 `doc/architecture/`,不在本仓库。 HDMI-Tool 工作在两端:HDMI **源端**(PC 显卡侧,EDID/分辨率/图案)与**光机控制端**
RS485 → 板载 GD32E230 网关 → DLPC3421)。寄存器语义判读基线(0x606E 位交换 /
0x606D 整组顺序)与光机协议规程都在上游仓库 `doc/architecture/``protocols.md`,不在本仓库。
## 2. 目录契约(重要) ## 2. 目录契约(重要)
@@ -30,8 +32,10 @@ HDMI-Tool/
| 文件 | 角色 | | 文件 | 角色 |
|---|---| |---|---|
| `src/hdmi_gui.py` | 单文件主程序:GUI(tkinter) + 命令行,约 1500 行,纯标准库 | | `src/hdmi_gui.py` | 单文件主程序:GUI(tkinter) + 命令行 + 光机控制区, 纯标准库 + 可选依赖 |
| `src/build_exe.py` | PyInstaller 打包 → `release/HDMI-Tool.exe`onefile/noconsole,内嵌图标) | | `src/dlpc_rs485.py` | 光机 RS485 协议模块: 帧构造/增量解析/译码表/预设命令/串口线程(DLPC485) |
| `src/selftest_rs485.py` | 光机协议自检(对照上游实测帧向量, 无硬件回归, 改协议代码后必跑) |
| `src/build_exe.py` | PyInstaller 打包 → `release/HDMI-Tool.exe`onefile/noconsole,内嵌图标+pyserial |
| `src/package_release.py` | exe+图案+说明 → `release/HDMI-Tool-v<ver>-win64.zip`(校验栏位引用完整性) | | `src/package_release.py` | exe+图案+说明 → `release/HDMI-Tool-v<ver>-win64.zip`(校验栏位引用完整性) |
| `src/publish_gitea.py` | 建/复用 Gitea Release 并上传 ziptag 默认取 zip 文件名) | | `src/publish_gitea.py` | 建/复用 Gitea Release 并上传 ziptag 默认取 zip 文件名) |
| `src/hdmi_source.ps1/.bat` | 独立 PowerShell 命令行工具(自包含,与 GUI 无代码共享) | | `src/hdmi_source.ps1/.bat` | 独立 PowerShell 命令行工具(自包含,与 GUI 无代码共享) |
@@ -39,33 +43,49 @@ HDMI-Tool/
| `src/gen_solid_colors.py` | 生成 `assets/solid_*.png` ×10800x600 纯色,需 Pillow | | `src/gen_solid_colors.py` | 生成 `assets/solid_*.png` ×10800x600 纯色,需 Pillow |
| `src/la_wave_analyzer*.py` | 逻辑分析仪 CSV 波形判读(2/3 通道,40MHz 像素时钟假设) | | `src/la_wave_analyzer*.py` | 逻辑分析仪 CSV 波形判读(2/3 通道,40MHz 像素时钟假设) |
| `src/logos/make_ico.py` | svg/png → ico(写在其自身目录) | | `src/logos/make_ico.py` | svg/png → ico(写在其自身目录) |
| `release/pattern_slots.json` | 预置栏位:西门子星/波带片/综合测试卡,area=640x360 | | `release/pattern_slots.json` | 预置栏位:西门子星/波带片/综合测试卡,area=640x360`serial` 键记忆光机串口 |
## 3. 代码不变量(改 hdmi_gui.py 必须维持) ## 3. 代码不变量(改 hdmi_gui.py 必须维持)
1. **纯标准库**hdmi_gui.py 只准 import 标准库(ctypes/winreg/tkinter/…)。Pillow 是 1. **纯标准库 + 可选依赖**hdmi_gui.py 核心只准用标准库(ctypes/winreg/tkinter/…)。
*可选*加速(仅扩展自定义图片格式 PNG/GIF 之外的 JPG/BMP/WebP,缺失时必须照常工作。 可选依赖一律走"try-import + HAS_* 标志 + 缺失降级"模式:Pillow(自定义图片 JPG/BMP/WebP
2. **BASE_DIR 语义**frozenPyInstaller onefile)时 `BASE_DIR=exe 所在目录``sys.executable`), pyserial(光机控制)。缺失时对应功能禁用/提示安装,其余功能必须照常工作。
2. **pyserial 只准在 dlpc_rs485.py 里 import**hdmi_gui.py 通过 `dlpc_rs485.HAS_SERIAL`
`DLPC485` 类间接使用,不得直接 import serial(协议与串口细节收敛在单一模块)。
3. **BASE_DIR 语义**frozenPyInstaller onefile)时 `BASE_DIR=exe 所在目录``sys.executable`),
源码运行时 `=hdmi_gui.py 所在目录``__file__`)。所有用户数据(pattern_slots.json、 源码运行时 `=hdmi_gui.py 所在目录``__file__`)。所有用户数据(pattern_slots.json、
自定义图片)都写/读自 BASE_DIR **平铺**存放,不建子目录。 自定义图片)都写/读自 BASE_DIR **平铺**存放,不建子目录。
3. **自定义栏位 `file` 字段是裸文件名**:加载时 `os.path.join(BASE_DIR, file)` 解析, 4. **自定义栏位 `file` 字段是裸文件名**:加载时 `os.path.join(BASE_DIR, file)` 解析,
因此 pattern_slots.json 引用的图片必须与 exe 同目录 —— package_release.py 正是按此平铺打包。 因此 pattern_slots.json 引用的图片必须与 exe 同目录 —— package_release.py 正是按此平铺打包。
4. **exe 自包含**:图标经 `--add-data` 内嵌(运行时从 `_MEIPASS` 读);内置五种图案 5. **exe 自包含**:图标经 `--add-data` 内嵌(运行时从 `_MEIPASS` 读);内置五种图案
(红/绿/蓝/棋盘格/网格线)由程序绘制,运行时**不依赖** assets/solid/bw PNG 只服务自定义栏位。 (红/绿/蓝/棋盘格/网格线)由程序绘制,运行时**不依赖** assets/solid/bw PNG 只服务自定义栏位。
5. **DPI 感知**:进程启动即 `SetProcessDpiAwareness(2)`,图案窗口按物理像素定位,勿移除。 6. **DPI 感知**:进程启动即 `SetProcessDpiAwareness(2)`,图案窗口按物理像素定位,勿移除。
6. **启动强制扩展拓扑**`SetDisplayConfig(SDC_TOPOLOGY_EXTEND|SDC_APPLY)`,静默容错 7. **启动强制扩展拓扑**`SetDisplayConfig(SDC_TOPOLOGY_EXTEND|SDC_APPLY)`,静默容错
(失败不阻断启动)。动机:LT8619C 热插拔后 Windows 可能停留在"仅电脑屏幕"。 (失败不阻断启动)。动机:LT8619C 热插拔后 Windows 可能停留在"仅电脑屏幕"。
7. **分辨率切换是临时的**:走 `CHANGEDISPLAYSETTINGS`(不写注册表),"恢复"用会话开始时快照。 8. **分辨率切换是临时的**:走 `CHANGEDISPLAYSETTINGS`(不写注册表),"恢复"用会话开始时快照。
8. **EDID 来源是注册表**`HKLM\SYSTEM\CurrentControlSet\Enum\DISPLAY\...\Device Parameters\EDID`), 9. **EDID 来源是注册表**`HKLM\SYSTEM\CurrentControlSet\Enum\DISPLAY\...\Device Parameters\EDID`),
不是 WMI/DDCexe 的 noconsole 版无 stdout`--dump/--edid-fields` 只在源码/控制台版可用。 不是 WMI/DDCexe 的 noconsole 版无 stdout`--dump/--edid-fields` 只在源码/控制台版可用。
10. **光机串口线程回调不直接碰 GUI**DLPC485 的 on_event/on_status 在后台线程触发,
必须经 `queue.Queue` + `root.after(80, …)` 轮询投递回主线程(现行 `_poll_dlpc` 模式,
与 I2C-Inject 的 Injector 同源);退出路径(关窗与 `--seconds` 自动关)都会走到
`App.destroy()`,在那里 `dlpc.close()` 收线程。
11. **pattern_slots.json 格式**`{'area', 'slots', 'serial'}` 三键共存;load 兼容旧版
纯数组(无 area)与无 serial 键两种历史格式,save 永远写全三键。
## 4. 领域语义(测试值都有含义,勿"顺手改") ## 4. 领域语义(测试值都有含义,勿"顺手改")
- 纯色值 55/AA/FF55(01010101) 与 AA(10101010) 互为字节镜像对(检出位序颠倒/位交换), - 纯色值 55/AA/FF55(01010101) 与 AA(10101010) 互为字节镜像对(检出位序颠倒/位交换),
FF(11111111) 全高基准;R/G/B 三行对应 RGB888 三字节组(R=D[23:16]/G=D[15:8]/B=D[7:0])。 FF(11111111) 全高基准;R/G/B 三行对应 RGB888 三字节组(R=D[23:16]/G=D[15:8]/B=D[7:0])。
- 800x600@60demo 板 shadow EDID 首选时序(solid 系列分辨率与之一致)。 - 800x600@60LT8619C **demo 板** shadow EDID 首选时序(solid 系列分辨率与之一致)。
- 640x360:测试卡区域默认值(16:9 半分辨率,点对点检查用);full=铺满所选屏。 - 640x360:测试卡区域默认值(16:9 半分辨率,点对点检查用);full=铺满所选屏。
**同时是转接板光机 DLPC3421 的 nHD 面板原生分辨率**(该板 shadow EDID 只声明 640x360@60),
投 640x360 图案 + RS485 开光机 = 整链路 1:1 验证。
- bw 系列图案各自目的见 `src/gen_bw_patterns.py` 顶部清单注释。 - bw 系列图案各自目的见 `src/gen_bw_patterns.py` 顶部清单注释。
- 光机 RS485 命令集(M730/M731/M732/M201/M202/M737/M999/M888/M7/M8/M1)语义、
帧格式 `D5 01 LEN <ASCII> CRC`(累加和校验)、错误码与**就绪门禁**
DLPC 未就绪时 M730/M731/M732 被拒回 0xFF)以上游 `protocols.md` 为唯一权威,
本仓不维护协议文档副本;注意 **M7S0 是"开启"TEC 自动控温**(方向反直觉)、
M737 在定时投光中延迟应答(CLI 等待上限 35s)。
## 5. 构建 / 发布流水线(全部在 src/ 下执行) ## 5. 构建 / 发布流水线(全部在 src/ 下执行)
@@ -102,8 +122,10 @@ python publish_gitea.py HDMI-Tool-v<版本>-win64.zip --replace --insecure #
## 7. 验证清单(改完代码后) ## 7. 验证清单(改完代码后)
1. `python hdmi_gui.py --seconds 5` —— GUI 冒烟(自动开关); 1. `python selftest_rs485.py` —— 光机协议回归(改 dlpc_rs485.py 后必跑,向量来自上游 protocols.md);
2. `python hdmi_gui.py --test-pattern Checker --area 800x600 --seconds 3` —— 投图链路 2. `python hdmi_gui.py --seconds 5` —— GUI 冒烟(自动开关)
3. `python hdmi_gui.py --dump` —— EDID 解算不抛异常 3. `python hdmi_gui.py --test-pattern Checker --area 800x600 --seconds 3` —— 投图链路
4. `python build_exe.py` → 双击 release/HDMI-Tool.exe 确认图标/启动/投图 4. `python hdmi_gui.py --dump` —— EDID 解算不抛异常
5. `python package_release.py <版本>` —— 不报"引用图片缺失"即栏位完整性 OK。 5. `python hdmi_gui.py --dlpc-cmd M999`(不带 --port)—— 应明确报错而非崩溃(无硬件时的优雅降级);
6. `python build_exe.py` → 双击 release/HDMI-Tool.exe 确认图标/启动/投图/光机区渲染;
7. `python package_release.py <版本>` —— 不报"引用图片缺失"即栏位完整性 OK。
+95 -52
View File
@@ -1,13 +1,14 @@
# HDMI-Tool — HDMI 源端调试工具 # HDMI-Tool — HDMI 源端调试 + 光机控制工具
> LT8619CHDMI→RGB888)→ DLPC3421 投影转接板调试配套的 PC 端工具: > LT8619CHDMI→RGB888)→ DLPC3421 投影转接板调试配套的 PC 端工具:
> HDMI **源端**电脑显卡输出)完成 EDID 查看、分辨率切换、纯色/测试图案全屏投屏。 > HDMI **源端**(EDID 查看、分辨率切换、纯色/测试图案投屏)+ **光机控制**
> RS485 直连板上 GD32 网关,开关光机/设光强/查温度电流)。
> >
> - **只想用工具**:到 [Gitea Releases](https://gitea.hulk.wang/hulk/LT8619C_Debug_Tools/releases) > - **只想用工具**:到 [Gitea Releases](https://gitea.hulk.wang/hulk/LT8619C_Debug_Tools/releases)
> 下载 `HDMI-Tool-vX.Y.Z-win64.zip`,解压双击 `HDMI-Tool.exe` 即可(Windows 10/11 x64,免安装、无需 Python)。 > 下载 `HDMI-Tool-vX.Y.Z-win64.zip`,解压双击 `HDMI-Tool.exe` 即可(Windows 10/11 x64,免安装、无需 Python)。
> - **要改代码/重新打包**:见 §7、§8 > - **要改代码/重新打包**:见 §8、§9
> >
> 更新:2026-09-10 > 更新:2026-09-11
--- ---
@@ -19,9 +20,10 @@
| EDID 逐字节解析 | "EDID 原始数据"框右上"逐字节解析"按钮:按偏移逐字段展开基础块与 CTA-861 扩展块,每字段附原始字节 hex;命令行等价 `--edid-fields` | | EDID 逐字节解析 | "EDID 原始数据"框右上"逐字节解析"按钮:按偏移逐字段展开基础块与 CTA-861 扩展块,每字段附原始字节 hex;命令行等价 `--edid-fields` |
| 分辨率切换 | "分辨率"下拉列出显卡按该屏 EDID 提供的全部模式;**应用**=临时切换(不写注册表,重启/恢复即还原),**恢复**=回到本次会话切改前的模式 | | 分辨率切换 | "分辨率"下拉列出显卡按该屏 EDID 提供的全部模式;**应用**=临时切换(不写注册表,重启/恢复即还原),**恢复**=回到本次会话切改前的模式 |
| 测试图案投屏 | 全屏投到所选显示器,测试卡区域可选 `640x360 / 800x600 / full 铺满`;内置 纯红/纯绿/纯蓝/黑白棋盘格/网格线 + R/G/B 三行 55/AA/FF 纯色值按钮 + 3 个自定义图片栏位 | | 测试图案投屏 | 全屏投到所选显示器,测试卡区域可选 `640x360 / 800x600 / full 铺满`;内置 纯红/纯绿/纯蓝/黑白棋盘格/网格线 + R/G/B 三行 55/AA/FF 纯色值按钮 + 3 个自定义图片栏位 |
| 光机控制 | RS485 → 板载 GD32E230 网关 → DLPC3421:开关光机(可定时)、设光强、查电流/温度/版本、TEC 控制、软件重启;收发日志带协议译码(见 §5) |
| 显示拓扑控制 | 启动时强制扩展显示拓扑(复制/仅第二屏等模式下保证 demo 板屏幕独立可控) | | 显示拓扑控制 | 启动时强制扩展显示拓扑(复制/仅第二屏等模式下保证 demo 板屏幕独立可控) |
| 命令行模式 | 全部功能可脚本化调用,供自动化测试(见 §5、§6 | | 命令行模式 | 全部功能可脚本化调用,供自动化测试(见 §6、§7 |
| 波形分析脚本 | 逻辑分析仪 CSV 波形按占空比签名分段,输出行结构/占空比/消隐明细(见 §6.3 | | 波形分析脚本 | 逻辑分析仪 CSV 波形按占空比签名分段,输出行结构/占空比/消隐明细(见 §7.3 |
## 2. 下载与安装 ## 2. 下载与安装
@@ -34,8 +36,8 @@
``` ```
HDMI-Tool/ HDMI-Tool/
├── HDMI-Tool.exe 主程序(单文件自包含) ├── HDMI-Tool.exe 主程序(单文件自包含, 光机控制的 pyserial 已内置
├── pattern_slots.json 预置自定义栏位(指向下面的 bw 图案) ├── pattern_slots.json 预置自定义栏位 + 串口记忆
├── bw_*.png ×5 640x360 黑白测试图 ├── bw_*.png ×5 640x360 黑白测试图
├── solid_*.png ×10 800x600 纯色测试图 ├── solid_*.png ×10 800x600 纯色测试图
└── README.md 本说明 └── README.md 本说明
@@ -43,49 +45,77 @@ HDMI-Tool/
## 3. GUI 使用 ## 3. GUI 使用
主窗口自上而下块区域: 主窗口自上而下块区域:
``` ```
┌ 显示器: [GDI ... ▼] [刷新] [拓扑: 扩展 ▼] ┌ 显示器: [ ... ▼] [刷新]
├ 分辨率: [800x600@60 ▼] [应用] [恢复] ├ 分辨率: [800x600@60 ▼] [应用] [恢复] ├ 显示器/EDID
├ ── EDID 原始数据 ──────────── [逐字节解析] ─┤ │ 支持的模式(EDID 解算) | EDID 信息 ┘
│ (hex 文本框) │ ├ 测试图案 — 图案按钮 / R,G,B 0x55,0xAA,0xFF / 测试卡区域 / 自定义栏位 ┐
测试卡区域: (○640x360 ○800x600 ○full) ┤ 光机控制 — 串口 [连接] / 开关光机 / 光强 / 命令下拉 / 自定义命令 ┘ 投屏+光机
├ [纯红][纯绿][纯蓝][棋盘格][网格线] │ ← 图案按钮 │ └ 收发日志(带协议译码与原始 hex)
├ R: [55][AA][FF] G: [55][AA][FF] B: [55][AA][FF] │ ← 纯色值按钮 └ EDID 原始数据 — hex dump [逐字节解析] ┘
├ [自定义1][自定义2][自定义3] [⚙] │ ← 自定义栏位
└─────────────────────────────────────────────┘
``` ```
1. 顶部显示器下拉选择目标屏(多屏时注意别选成主屏),"刷新"重新枚举; 1. 顶部显示器下拉选择目标屏(多屏时注意别选成主屏),"刷新"重新枚举;
2. **切分辨率**第二行选模式 → "应用";测完点"恢复" 2. **切分辨率**:选模式 → "应用";测完点"恢复"
3. **投测试图** 3. **投测试图**先选**测试卡区域**640x360 / 800x600 / full)→ 点图案按钮全屏投放,
- 先在图案按钮行上方选**测试卡区域**640x360 / 800x600 / full); **ESC** 或点击退出;R/G/B 三行纯色按钮(55/AA/FF)用于 RGB 位交换类测试
- 点任意图案按钮即全屏投放;按 **ESC** 或点击退出 (55 与 AA 互为字节镜像对,FF 全高基准);自定义图片点"⚙"配置(图片自动复制到 exe 旁)
- R/G/B 三行纯色按钮(55/AA/FF)用于 RGB 位交换类测试:55(01010101) 与 4. 区域与自定义栏位配置自动保存在 exe 旁的 `pattern_slots.json`
AA(10101010) 互为字节镜像对,FF(11111111) 为全高基准;
- 自定义图片:点"⚙"配置名称与图片文件(图片会自动复制到 exe 旁边),点栏位名投放;
4. 测试卡区域与自定义栏位配置自动保存在 exe 旁的 `pattern_slots.json`
## 4. 典型测试场景 ## 4. 典型测试场景
``` ```
1. HDMI 接 demo 板 → 启动 HDMI-Tool → 刷新 → 选中 demo 板显示器 1. HDMI 接转接板 → 启动 HDMI-Tool → 刷新 → 选中板显示器
2. 分辨率切 800x600@60demo 板 shadow EDID 首选→ 测试卡区域选 800x600 2. 板 shadow EDID 首选 640x360@60 → 测试卡区域选 640x360(与光机面板原生 1:1)
3. 点 R/G/B 行纯色按钮投图 → 配合逻辑分析仪/万用表测转接板 RGB888 输出 3. "光机控制"连接串口 → M999 查版本确认链路通 → M730S0 开光机
4. 点 R/G/B 行纯色按钮投图 → 目视/逻辑分析仪核对 RGB888 输出
- 纯红/绿/蓝:验证通道映射(R=D[23:16], G=D[15:8], B=D[7:0] - 纯红/绿/蓝:验证通道映射(R=D[23:16], G=D[15:8], B=D[7:0]
- 55/AA 对:验证位交换(0x606E)——图像应为 50% 灰阶占空比 - 55/AA 对:验证位交换(0x606E)——图像应为 50% 灰阶占空比
4. 换 bw 图案(西门子星/波带片)看极限分辨率与混叠 5. M731S100 设光强 → M732 查电流读回复核 → M730S1 关光机
5. 测完"恢复"分辨率
``` ```
寄存器语义(0x606E 位交换 / 0x606D 整组顺序)的判读基线见上游主仓库 寄存器语义(0x606E 位交换 / 0x606D 整组顺序)与光机协议的判读基线见上游主仓库
[LT8619C_DLPC3421_HDMI](https://gitea.hulk.wang/hulk/LT8619C_DLPC3421_HDMI) 的 [LT8619C_DLPC3421_HDMI](https://gitea.hulk.wang/hulk/LT8619C_DLPC3421_HDMI) 的 `doc/architecture/`
`doc/architecture/`
## 5. 命令行模式(hdmi_gui.py ## 5. 光机控制(RS485 → GD32 网关 → DLPC3421
exe 版不支持 `--dump/--edid-fields`(noconsole 无控制台输出),请用源码方式跑: 物理链路:PC 经 **USB-RS485 转换器**接板卡 A/B 端子,板上 GD32E230 做 RS485→I2C 网关
控制 DLPC3421I2C 地址 0x1B)。串口参数 **115200, 8N1**,协议细节以上游固件仓
`protocols.md` 为准:命令帧 `D5 01 LEN <ASCII命令> CRC`、响应帧 `B5 TYPE LEN <payload> CRC`
CRC 为第 2 字节起累加和取低 8 位。
GUI"光机控制"区:选串口 → 连接 → 点快捷按钮或命令下拉/自定义输入发送;日志区实时显示
`→ 命令` / `← 译码结果 (原始 hex)`,错误自动附释义(如 `err:3I2C NACK`)。
| 命令 | 功能 | 响应示例 |
|---|---|---|
| `M730S0` / `M730S1` | 开 / 关光机 | `ok` |
| `M730S0T2000` | 开光机并定时 2000ms 自动关(≤32700 | `ok`(期间 M737 延迟应答) |
| `M731S100` | 设光强 54-800(RGB 同值,写后读回复核) | `ok` / `err:R..G..B..` |
| `M732` | 查三路电流设置值(0-65535) | `okR110G110B110` |
| `M201` / `M202` | 查 DMD / LAMP 温度(0.1°C | `okTL:246` = 24.6°C |
| `M999` | 查固件版本 | `okv20.0.0.1` |
| `M737` | 查投光是否结束(投光中延迟应答) | `ok` |
| `M7S0` / `M7S1` | TEC 自动控温开 / 关(注意 S0=开) | `ok` |
| `M8S330` | TEC 凝露点 33.0°C200-3500.1°C | `ok` / `err:OL` |
| `M1` | 协议握手 | `ok` |
| `M888` | 软件重启 | `ok` 后 100ms 复位 |
注意:**DLPC3421 启动完成前 M730/M731/M732 被就绪门禁拒绝**(回 0xFF),上电后稍等几秒再发。
命令行等价(源码方式):
```
python hdmi_gui.py --port COM9 --dlpc-cmd M730S0 --dlpc-cmd M999
逐条发送并打印译码响应
```
## 6. 命令行模式(hdmi_gui.py
exe 版无控制台,`--dump/--edid-fields` 的输出不可见(`--dlpc-cmd` 仍可执行,靠退出码判断),
调试请用源码方式:
``` ```
python hdmi_gui.py --dump 控制台输出 EDID 解算 python hdmi_gui.py --dump 控制台输出 EDID 解算
@@ -93,14 +123,16 @@ python hdmi_gui.py --edid-fields 控制台输出 EDID 逐字
python hdmi_gui.py --test-pattern Blue --area 800x600 --seconds 5 python hdmi_gui.py --test-pattern Blue --area 800x600 --seconds 5
出图 5 秒自动退出 出图 5 秒自动退出
python hdmi_gui.py --test-pattern Custom1 投自定义栏位 1 python hdmi_gui.py --test-pattern Custom1 投自定义栏位 1
python hdmi_gui.py --port COM9 --dlpc-cmd M730S0 RS485 发光机命令(可多次)
python hdmi_gui.py --seconds 5 GUI 冒烟测试, 5 秒自动关 python hdmi_gui.py --seconds 5 GUI 冒烟测试, 5 秒自动关
--test-pattern {Red,Green,Blue,Checker,Grid,Custom1..3} --test-pattern {Red,Green,Blue,Checker,Grid,Custom1..3}
--area {640x360,800x600,full} --monitor N --seconds S --area {640x360,800x600,full} --monitor N --seconds S
--port COMx --baud 115200 --dlpc-cmd CMD(可重复; 任一命令失败退出码 1)
``` ```
## 6. 辅助脚本(src/ 下) ## 7. 辅助脚本(src/ 下)
### 6.1 hdmi_source.bat / .ps1 — 源端命令行工具 ### 7.1 hdmi_source.bat / .ps1 — 源端命令行工具
与 GUI 互补,适合脚本串联: 与 GUI 互补,适合脚本串联:
@@ -111,7 +143,7 @@ tools\hdmi_source.bat -Mode 800x600 -Pattern -Restore
切模式→出图案→退出后自动恢复 切模式→出图案→退出后自动恢复
``` ```
### 6.2 gen_bw_patterns.py / gen_solid_colors.py — 测试图生成 ### 7.2 gen_bw_patterns.py / gen_solid_colors.py — 测试图生成
需 Pillow`py -m pip install pillow`)。重新生成 `assets/` 下全部图案: 需 Pillow`py -m pip install pillow`)。重新生成 `assets/` 下全部图案:
@@ -122,7 +154,7 @@ tools\hdmi_source.bat -Mode 800x600 -Pattern -Restore
- `gen_solid_colors.py` → 800x600 纯色图 ×10R/G/B 各 55/AA/FF + 三原色), - `gen_solid_colors.py` → 800x600 纯色图 ×10R/G/B 各 55/AA/FF + 三原色),
与 GUI 内置纯色按钮同源,供自定义栏位或其他工具加载。 与 GUI 内置纯色按钮同源,供自定义栏位或其他工具加载。
### 6.3 la_wave_analyzer.py / la_wave_analyzer3.py — 逻辑分析仪波形判读 ### 7.3 la_wave_analyzer.py / la_wave_analyzer3.py — 逻辑分析仪波形判读
分析 Kingst VIS 导出的边沿流 CSV(列:`Time[s], 通道A, 通道B[, 通道C]` 分析 Kingst VIS 导出的边沿流 CSV(列:`Time[s], 通道A, 通道B[, 通道C]`
10ns 分辨率),按 50ms 窗口占空比签名自动分段,输出每段的签名(FF/55/AA/00)、 10ns 分辨率),按 50ms 窗口占空比签名自动分段,输出每段的签名(FF/55/AA/00)、
@@ -133,12 +165,19 @@ python la_wave_analyzer.py <capture.csv> # 2 通道(默认判读 B 口 D0/
python la_wave_analyzer3.py <capture.csv> # 3 通道(RGB 三组同拍, ch8/9/10 python la_wave_analyzer3.py <capture.csv> # 3 通道(RGB 三组同拍, ch8/9/10
``` ```
## 7. 目录结构与从源码运行 ### 7.4 selftest_rs485.py — 光机协议自检(无硬件)
对照上游 `protocols.md` 实测帧向量回归帧构造/解析/译码:`python selftest_rs485.py`
全部通过退出码 0。改 `dlpc_rs485.py` 后必须跑一遍。
## 8. 目录结构与从源码运行
``` ```
HDMI-Tool/ HDMI-Tool/
├── src/ # 源代码(人工维护区) ├── src/ # 源代码(人工维护区)
│ ├── hdmi_gui.py # 主程序:GUI + 命令行, 纯标准库 │ ├── hdmi_gui.py # 主程序:GUI + 命令行(标准库; pyserial/Pillow 可选)
│ ├── dlpc_rs485.py # 光机 RS485 协议与串口链路(帧/解析/译码/线程)
│ ├── selftest_rs485.py # 光机协议自检(无硬件回归)
│ ├── build_exe.py # PyInstaller 打包 → release/ │ ├── build_exe.py # PyInstaller 打包 → release/
│ ├── package_release.py # 组装发布 zipexe+图案+说明) │ ├── package_release.py # 组装发布 zipexe+图案+说明)
│ ├── publish_gitea.py # 上传到 Gitea Release │ ├── publish_gitea.py # 上传到 Gitea Release
@@ -152,28 +191,32 @@ HDMI-Tool/
│ └── solid_*.png ×10 # 800x600 纯色系列 │ └── solid_*.png ×10 # 800x600 纯色系列
├── release/ # 构建与发布产物(机器生成区, 不手改) ├── release/ # 构建与发布产物(机器生成区, 不手改)
│ ├── HDMI-Tool.exe # 打包单文件版(改动跟随源码提交入库) │ ├── HDMI-Tool.exe # 打包单文件版(改动跟随源码提交入库)
│ ├── pattern_slots.json # exe 运行配置(默认栏位预置 bw 图案 │ ├── pattern_slots.json # exe 运行配置(栏位预置 + 串口记忆
│ └── *.zip # 发布包(不入库, 上传 Gitea Release │ └── *.zip # 发布包(不入库, 上传 Gitea Release
├── README.md # 本说明 ├── README.md # 本说明
└── AGENTS.md # 面向 AI/Agent 的工程档案(结构/约定/构建规程) └── AGENTS.md # 面向 AI/Agent 的工程档案(结构/约定/构建规程)
``` ```
从源码运行:`python src/hdmi_gui.py`,需 Python ≥3.10标准库无第三方依赖 从源码运行:`python src/hdmi_gui.py`,需 Python ≥3.10,标准库即可跑 EDID/投屏;
自定义图片要 JPG/BMP/WebP 需可选 Pillow 光机控制需 pyserial`py -m pip install pyserial`),自定义图片要 JPG/BMP/WebP 需可选 Pillow。
## 8. 构建与发布 ## 9. 构建与发布
```powershell ```powershell
cd src cd src
python build_exe.py # 打包 → ../release/HDMI-Tool.exe(自动装 PyInstaller python selftest_rs485.py # 光机协议回归(改 dlpc_rs485.py 后必跑)
python build_exe.py --debug # 额外出一个带控制台的调试变体(--dump 可用 python build_exe.py # 打包 → ../release/HDMI-Tool.exe(自动装 PyInstaller; 内置 pyserial
python package_release.py 0.1.0 # 组装 release/HDMI-Tool-v0.1.0-win64.zip python build_exe.py --debug # 额外出一个带控制台的调试变体(--dump 可见输出)
python publish_gitea.py HDMI-Tool-v0.1.0-win64.zip --insecure # 上传 Gitea Release(需 GITEA_TOKEN) python package_release.py 0.2.0 # 组装 release/HDMI-Tool-v0.2.0-win64.zip
python publish_gitea.py HDMI-Tool-v0.2.0-win64.zip --insecure # 上传 Gitea Release(需 GITEA_TOKEN)
``` ```
## 9. 注意事项 / FAQ ## 10. 注意事项 / FAQ
- **exe 放只读目录会怎样**:自定义图片与 `pattern_slots.json` 写在 exe 旁边,只读目录会导致栏位配置存不下 - **光机没反应排查**:① 串口/波特率(115200 8N1)与 A/B 接线;② 先发 `M999` 验证链路
③ 开光机回 0xFF 是 DLPC 就绪门禁——上电后等几秒;④ USB-RS485 转换器半双工,
发送时板上回包不会回环(正常);
- **exe 放只读目录会怎样**:自定义图片与 `pattern_slots.json` 写在 exe 旁边,只读目录会导致配置存不下;
- **重新打包后图标没变**:Windows 图标缓存,`ie4uinit -show` 或改个名即可; - **重新打包后图标没变**:Windows 图标缓存,`ie4uinit -show` 或改个名即可;
- **多屏序号**:切分辨率前先确认显示器序号(GUI 下拉框与 `hdmi_source.bat -List` 输出一致); - **多屏序号**:切分辨率前先确认显示器序号(GUI 下拉框与 `hdmi_source.bat -List` 输出一致);
- **残留进程**:修改源码重新打包前先结束残留的 HDMI-Tool 进程(`build_exe.py` 会自动 taskkill); - **残留进程**:修改源码重新打包前先结束残留的 HDMI-Tool 进程(`build_exe.py` 会自动 taskkill);
Binary file not shown.
+250
View File
@@ -0,0 +1,250 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
DLPC3421 光机 RS485 控制链路(板上 GD32E230 作 RS485→I2C 网关)
================================================================
协议以 ../LT8619C_DLPC3421_HDMI/protocols.md 为准(上游固件仓, 含逐条命令的
响应语义/错误码/就绪门禁与实测帧示例):
物理层 RS485 半双工 115200 8N1(PC 经 USB-RS485 转换器接板, DE 由 MCU 硬件换向)
命令帧 D5 01 LEN <ASCII命令> CRC 01=板类型, LEN=命令字节数
响应帧 B5 TYPE LEN <payload> CRC payload 为 ASCII(ok / err:OL / okTL:246 ...)
CRC 帧内第 2 字节(板类型)起累加到最后一个 payload 字节, 取低 8 位
帧长 最小 6 字节, 最大 32 字节
就绪门禁: DLPC3421 启动完成前 M730/M731/M732 被拒(回 0xFF), 属预期。
常用命令见 PRESETS(全表见 protocols.md); M7S0 是"开启"TEC 自动控温, 方向别记反。
仅标准库 + 可选 pyserial(缺失只影响串口功能, 同 hdmi_gui.py 的 Pillow 模式)。
"""
import collections
import queue
import re
import threading
import time
try:
import serial
import serial.tools.list_ports
HAS_SERIAL = True
except ImportError:
HAS_SERIAL = False
HEADER_TX = 0xD5 # 命令帧头(Host -> Device)
HEADER_RX = 0xB5 # 响应帧头(Device -> Host)
BOARD_TYPE = 0x01 # 板类型字节
FRAME_MIN, FRAME_MAX = 6, 32 # 整帧长度上下限(含帧头/CRC)
DEFAULT_BAUD = 115200
# --- 响应 TYPE 码语义(protocols.md "响应 TYPE 码"表) ---
TYPE_NAMES = {
0xF0: '成功',
0xF1: 'CRC 校验错',
0xF2: '包头错',
0xF3: '板类型错',
0xF4: '格式/缺参数',
0xFD: '参数越界/读回复核不一致',
0xFE: '未知命令',
0xFF: '通用错误(含 DLPC 就绪门禁拒绝)',
0xFA: '开光机通信失败',
0xFB: '关光机通信失败',
0xFC: '读回复核通信失败',
}
# --- payload 中 err:<N> 的 DLPC 侧错误码 ---
DLPC_ERR_NAMES = {
0: 'DLPC 成功',
1: 'DLPC 通用错误',
2: 'DLPC 通信超时',
3: 'I2C NACK(器件无应答)',
4: 'DLPC 参数无效',
5: 'DLPC 忙',
}
# GUI 预设命令表: (显示名, 命令, 说明), 顺序即下拉顺序
PRESETS = (
('开光机', 'M730S0', '开启 RGB 投光(需 DLPC 就绪)'),
('关光机', 'M730S1', '关闭投光并停定时器'),
('设光强', 'M731S100', 'RGB 三路同值, 范围 54-800, 写后读回复核'),
('查电流', 'M732', 'okR<n>G<n>B<n>, 三路电流原始值 0-65535'),
('查DMD温度', 'M201', 'okTL:246 = 24.6°C(有符号, 0.1°C)'),
('查LAMP温度', 'M202', '同 M201'),
('查投光结束', 'M737', '定时投光中不立即回包, 到期自动关灯后补发 ok'),
('查版本', 'M999', 'okv20.0.0.1 形式'),
('握手', 'M1', '协议握手, 回 ok'),
('TEC自动控温开', 'M7S0', 'S0=开启 PID 自动控温(方向别记反)'),
('TEC自动控温关', 'M7S1', 'S1=关闭'),
('凝露点33.0°C', 'M8S330', 'TEC 凝露点, 0.1°C 单位, 范围 200-350'),
('软件重启', 'M888', '回 ok 后 100ms 复位'),
)
Frame = collections.namedtuple('Frame', 'type_code payload raw')
def sum_crc(data):
"""帧内第 2 字节(板类型)起累加到末 payload 字节, 取低 8 位(不含包头与 CRC)"""
return sum(data) & 0xFF
def build_frame(cmd):
"""ASCII 命令字符串 -> 命令帧 bytes; 空命令/超长/非 ASCII 抛 ValueError"""
payload = cmd.strip().encode('ascii')
if not payload:
raise ValueError('空命令')
if len(payload) > FRAME_MAX - 4:
raise ValueError(f'命令过长(>{FRAME_MAX - 4} 字节, 帧上限 {FRAME_MAX})')
frame = bytes((HEADER_TX, BOARD_TYPE, len(payload))) + payload
return frame + bytes((sum_crc(frame[1:]),))
class FrameParser:
"""增量字节流 -> 完整响应帧: 帧头+LEN 定界, CRC 校验, 失配丢当前帧头重新同步。
payload 理论上可能含 0xB5 造成同步偏移(伪帧头 CRC 必错会被跳过),
下一帧自动恢复, 对调试工具可接受。
"""
def __init__(self):
self._buf = bytearray()
def feed(self, data):
"""喂入新收到的字节, 返回本次解析出的完整 Frame 列表"""
self._buf += data
frames = []
while True:
i = self._buf.find(HEADER_RX)
if i < 0:
self._buf.clear()
return frames
if i: # 丢弃帧头前的噪声
del self._buf[:i]
if len(self._buf) < 3: # 帧头/TYPE/LEN 尚不完整
return frames
length = self._buf[2]
if not FRAME_MIN - 4 <= length <= FRAME_MAX - 4:
del self._buf[:1] # LEN 非法 -> 伪帧头, 前进 1 字节重同步
continue
total = 4 + length
if len(self._buf) < total: # 整帧未收齐
return frames
raw = bytes(self._buf[:total])
if raw[-1] == sum_crc(raw[1:-1]):
frames.append(Frame(self._buf[1],
raw[3:-1].decode('ascii', 'replace').strip(), raw))
del self._buf[:total]
else: # CRC 失配 -> 丢帧头重同步
del self._buf[:1]
def describe(fr):
"""响应帧 -> 人读文本: TYPE 译码 + payload, err:N 追加 DLPC 错误释义"""
if fr.type_code == 0xF0:
text = fr.payload or 'ok'
else:
text = TYPE_NAMES.get(fr.type_code, f'TYPE=0x{fr.type_code:02X}')
if fr.payload:
text += f' {fr.payload}'
m = re.search(r'err:(\d+)', fr.payload)
if m:
text += f'{DLPC_ERR_NAMES.get(int(m.group(1)), "未知错误码")}'
return text
class DLPC485:
"""串口工作线程: 端口/波特率热切换 + 命令队列 + 回调(收发全在后台线程)。
on_event(str) 收发日志行(已译码, 附原始 hex)
on_status(str) 连接状态变化(已连接/断开/失败/设备拔出)
回调在后台线程触发, GUI 侧需自行投递回主线程(见 hdmi_gui 的轮询队列)。
"""
def __init__(self, on_event, on_status):
self.on_event, self.on_status = on_event, on_status
self.q = queue.Queue()
self._target = None # 想态 (port, baud); None=断开
self.ser = None
self.parser = FrameParser()
self._lock = threading.Lock()
threading.Thread(target=self._run, daemon=True).start()
@staticmethod
def list_ports():
"""枚举本机串口名(未装 pyserial 返回空表)"""
if not HAS_SERIAL:
return []
return [p.device for p in serial.tools.list_ports.comports()]
def open(self, port, baud=DEFAULT_BAUD):
if not HAS_SERIAL:
self.on_status('未安装 pyserial: py -m pip install pyserial')
return
with self._lock:
self._target = (port, int(baud))
def close(self):
with self._lock:
self._target = None
def is_connected(self):
with self._lock:
return self.ser is not None
def send(self, cmd):
"""发送一条 ASCII 命令(自动组帧); 未连接/命令非法走 on_status 提示"""
with self._lock:
ser = self.ser
if ser is None:
self.on_status('未连接串口, 命令被丢弃')
return
try:
frame = build_frame(cmd)
except ValueError as e:
self.on_status(f'命令非法: {e}')
return
self.q.put((cmd, frame))
def _run(self):
while True:
with self._lock:
target = self._target
cur = (self.ser.port, self.ser.baudrate) if self.ser else None
if target != cur: # 端口/波特率变化或断开: 统一在此切换
if self.ser is not None:
try:
self.ser.close()
except Exception:
pass
self.ser = None
self.on_status('已断开')
if target:
try:
self.ser = serial.Serial(target[0], target[1], timeout=0.15)
self.parser = FrameParser()
self.on_status(f'已连接 {target[0]} @ {target[1]}')
except Exception as e:
self.ser = None
with self._lock:
self._target = None
self.on_status(f'连接失败: {e}')
if self.ser is not None:
try:
item = None
try:
item = self.q.get_nowait()
except queue.Empty:
pass
if item is not None:
cmd, frame = item
self.ser.write(frame)
self.on_event(f'{cmd} [{frame.hex(" ").upper()}]')
data = self.ser.read(256)
if data:
for fr in self.parser.feed(data):
self.on_event(f'{describe(fr)} '
f'[{fr.raw.hex(" ").upper()}]')
except OSError:
self.ser = None
with self._lock:
self._target = None
self.on_status('串口丢失(设备拔出?), 请重新连接')
time.sleep(0.02)
+262 -7
View File
@@ -6,15 +6,18 @@ HDMI EDID 查看器 + 测试图案投屏工具
配合 LT8619C(HDMI 转 RGB888)→ DLPC3421 转接板调试使用。 配合 LT8619C(HDMI 转 RGB888)→ DLPC3421 转接板调试使用。
功能: 功能:
1. 枚举各显示输出口接的设备, 从注册表读取其 EDID 原始数据, 1. 枚举各显示输出口, 从注册表读取其 EDID 原始数据,
按 EDID 1.4 / CEA-861 标准解算: 厂商/产品/名称/尺寸/Gamma/输入类型、 按 EDID 1.4 / CEA-861 标准解算: 厂商/产品/名称/尺寸/Gamma/输入类型、
支持的全部模式(详细时序 DTD / 已建立时序 / 标准时序 / CEA VIC)、 支持的全部模式(详细时序 DTD / 已建立时序 / 标准时序 / CEA VIC)、
首选时序完整参数(像素时钟/消隐/同步极性)、CEA 扩展能力、校验和。 首选时序完整参数(像素时钟/消隐/同步极性)、CEA 扩展能力、校验和。
2. 向所选屏幕全屏投测试图案(黑底 1:1 居中, 测试卡区域可选 2. 向所选屏幕全屏投测试图案(黑底 1:1 居中, 测试卡区域可选
640x360 / 800x600 / full 铺满): 纯红 / 纯绿 / 纯蓝 / 黑白棋盘格 / 640x360 / 800x600 / full 铺满): 纯红 / 纯绿 / 纯蓝 / 黑白棋盘格 /
网格线 + 3 个自定义图片栏位, 点击或 ESC 退出。 网格线 + 3 个自定义图片栏位, 点击或 ESC 退出。
3. 光机控制: RS485 直连板上 GD32E230 网关(→DLPC3421), 开关光机/设光强/
查温度电流/定时投光, 协议见 dlpc_rs485.py 头注释(上游仓 protocols.md)。
仅用标准库(ctypes / winreg / tkinter), 无需 pip 安装任何依赖 仅用标准库(ctypes / winreg / tkinter), 无需 pip 安装任何依赖;
可选 pyserial(光机控制)与 Pillow(自定义图片 JPG/BMP/WebP), 缺了不影响其余功能。
命令行模式(自动化测试用): 命令行模式(自动化测试用):
python hdmi_gui.py 启动 GUI python hdmi_gui.py 启动 GUI
@@ -24,6 +27,8 @@ HDMI EDID 查看器 + 测试图案投屏工具
直接出图案, 3 秒自动退出 直接出图案, 3 秒自动退出
python hdmi_gui.py --test-pattern Blue --area 800x600 python hdmi_gui.py --test-pattern Blue --area 800x600
指定测试卡区域(full=铺满所选屏) 指定测试卡区域(full=铺满所选屏)
python hdmi_gui.py --port COM9 --dlpc-cmd M730S0 --dlpc-cmd M999
RS485 逐条发光机命令并打印译码响应
python hdmi_gui.py --seconds 5 GUI 5 秒自动关闭(冒烟测试) python hdmi_gui.py --seconds 5 GUI 5 秒自动关闭(冒烟测试)
""" """
@@ -33,11 +38,15 @@ import ctypes.wintypes as wt
import json import json
import math import math
import os import os
import queue
import shutil import shutil
import struct import struct
import sys import sys
import time
import winreg import winreg
import dlpc_rs485
# 进程级 DPI 感知: 保证图案窗口按物理像素精确定位/绘制 # 进程级 DPI 感知: 保证图案窗口按物理像素精确定位/绘制
try: try:
ctypes.windll.shcore.SetProcessDpiAwareness(2) ctypes.windll.shcore.SetProcessDpiAwareness(2)
@@ -65,11 +74,15 @@ SLOT_COUNT = 3
SLOT_CONFIG = os.path.join(BASE_DIR, 'pattern_slots.json') SLOT_CONFIG = os.path.join(BASE_DIR, 'pattern_slots.json')
IMG_EXTS = ['.png', '.gif'] + (['.jpg', '.jpeg', '.bmp', '.webp'] if HAS_PIL else []) IMG_EXTS = ['.png', '.gif'] + (['.jpg', '.jpeg', '.bmp', '.webp'] if HAS_PIL else [])
# 光机控制串口记忆(与栏位配置同存 pattern_slots.json 的 "serial" 键)
SERIAL_CFG = {'port': '', 'baud': dlpc_rs485.DEFAULT_BAUD}
def load_slots(): def load_slots():
"""读 pattern_slots.json, 返回 (slots, area)。 """读 pattern_slots.json, 返回 (slots, area)。
新格式为 {'area': 'WxH'|'full', 'slots': [...]}; 兼容旧版纯数组格式(无 area)。 新格式为 {'area': 'WxH'|'full', 'slots': [...], 'serial': {...}};
兼容旧版纯数组格式(无 area)与无 serial 键的中间格式。
""" """
slots = [{'name': f'自定义{i + 1}', 'file': None} for i in range(SLOT_COUNT)] slots = [{'name': f'自定义{i + 1}', 'file': None} for i in range(SLOT_COUNT)]
try: try:
@@ -80,6 +93,13 @@ def load_slots():
if isinstance(data, dict): if isinstance(data, dict):
area = str(data.get('area') or DEFAULT_AREA) area = str(data.get('area') or DEFAULT_AREA)
items = data.get('slots') or [] items = data.get('slots') or []
ser = data.get('serial')
if isinstance(ser, dict):
SERIAL_CFG['port'] = str(ser.get('port') or '')
try:
SERIAL_CFG['baud'] = int(ser.get('baud') or dlpc_rs485.DEFAULT_BAUD)
except (TypeError, ValueError):
pass
else: else:
area, items = DEFAULT_AREA, data area, items = DEFAULT_AREA, data
for i, item in enumerate(items[:SLOT_COUNT]): for i, item in enumerate(items[:SLOT_COUNT]):
@@ -91,7 +111,8 @@ def load_slots():
def save_slots(slots, area): def save_slots(slots, area):
with open(SLOT_CONFIG, 'w', encoding='utf-8') as f: with open(SLOT_CONFIG, 'w', encoding='utf-8') as f:
json.dump({'area': area, 'slots': slots}, f, ensure_ascii=False, indent=2) json.dump({'area': area, 'slots': slots, 'serial': SERIAL_CFG},
f, ensure_ascii=False, indent=2)
def copy_image_into_script_dir(src_path): def copy_image_into_script_dir(src_path):
@@ -1008,8 +1029,8 @@ def run_gui(auto_close=0):
class App(tk.Tk): class App(tk.Tk):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self.title('HDMI-Tool — EDID 查看器 + 测试图案 (LT8619C 转接板调试)') self.title('HDMI-Tool — EDID 查看器 + 测试图案 + 光机控制 (LT8619C 转接板调试)')
self.geometry('960x880') self.geometry('960x1100')
ic = get_icon_path() ic = get_icon_path()
if ic: if ic:
try: try:
@@ -1020,6 +1041,10 @@ def run_gui(auto_close=0):
self.slots, self.area = load_slots() self.slots, self.area = load_slots()
self._modes = {} # 'WxH@Hz' -> DEVMODEW(当前所选屏) self._modes = {} # 'WxH@Hz' -> DEVMODEW(当前所选屏)
self._orig_modes = {} # device -> 切改前的原始 DEVMODEW self._orig_modes = {} # device -> 切改前的原始 DEVMODEW
# 光机控制链路: 回调发生在串口线程, 经队列投递回 GUI 主线程轮询
self._dlpc_log_q = queue.Queue()
self.dlpc = dlpc_rs485.DLPC485(self._dlpc_on_event, self._dlpc_on_status)
self._dlpc_connected = False
self._build() self._build()
def _build(self): def _build(self):
@@ -1113,6 +1138,85 @@ def run_gui(auto_close=0):
ttk.Label(row2, text='自定义: 点击投图, ⚙ 配置名称与图片(图片保存在脚本目录)', ttk.Label(row2, text='自定义: 点击投图, ⚙ 配置名称与图片(图片保存在脚本目录)',
foreground='#666666').pack(side='left', padx=10) foreground='#666666').pack(side='left', padx=10)
# --- 光机控制: RS485 -> 板载 GD32E230 网关 -> DLPC3421 ---
grp2 = ttk.LabelFrame(
self, text='光机控制 — RS485 → 板载 GD32 网关 → DLPC3421 (115200 8N1)',
padding=(10, 6))
grp2.pack(fill='x', padx=8, pady=(6, 0))
if not dlpc_rs485.HAS_SERIAL:
ttk.Label(grp2, foreground='#B05000',
text='未安装 pyserial, 光机控制不可用 — '
'执行 py -m pip install pyserial 后重启本工具').pack(anchor='w')
else:
r1 = ttk.Frame(grp2)
r1.pack(fill='x')
ttk.Label(r1, text='串口:').pack(side='left')
self.cbo_dlpc = ttk.Combobox(r1, width=12, state='readonly',
values=dlpc_rs485.DLPC485.list_ports())
if SERIAL_CFG['port']:
self.cbo_dlpc.set(SERIAL_CFG['port'])
self.cbo_dlpc.pack(side='left', padx=(4, 2))
ttk.Button(r1, text='刷新', width=6,
command=self._dlpc_refresh).pack(side='left')
ttk.Label(r1, text='波特率:').pack(side='left', padx=(10, 0))
self.var_baud = tk.StringVar(value=str(SERIAL_CFG['baud']))
ttk.Combobox(r1, width=8, textvariable=self.var_baud,
values=('115200', '57600', '38400', '9600')
).pack(side='left', padx=4)
self.btn_dlpc = ttk.Button(r1, text='连接', width=8,
command=self._dlpc_toggle)
self.btn_dlpc.pack(side='left', padx=(10, 0))
self.lbl_dlpc_st = ttk.Label(r1, text='未连接', foreground='#666666')
self.lbl_dlpc_st.pack(side='left', padx=10)
r2 = ttk.Frame(grp2)
r2.pack(fill='x', pady=(4, 0))
for name, cmd in (('开光机', 'M730S0'), ('关光机', 'M730S1'),
('查电流', 'M732'), ('查版本', 'M999'),
('软件重启', 'M888')):
ttk.Button(r2, text=name, width=9,
command=lambda c=cmd: self._dlpc_send(c)
).pack(side='left', padx=3)
ttk.Label(r2, text='光强(54-800):').pack(side='left', padx=(12, 2))
self.var_light = tk.StringVar(value='100')
ttk.Entry(r2, textvariable=self.var_light, width=6).pack(side='left')
ttk.Button(r2, text='设光强', width=7,
command=self._dlpc_set_light).pack(side='left', padx=4)
r3 = ttk.Frame(grp2)
r3.pack(fill='x', pady=(4, 0))
ttk.Label(r3, text='命令:').pack(side='left')
self._dlpc_presets = [(n, c) for n, c, _ in dlpc_rs485.PRESETS]
self.cbo_dlpc_cmd = ttk.Combobox(
r3, state='readonly', width=26,
values=[f'{n} {c}' for n, c in self._dlpc_presets])
self.cbo_dlpc_cmd.current(0)
self.cbo_dlpc_cmd.pack(side='left', padx=(4, 2))
ttk.Button(r3, text='发送', width=7,
command=self._dlpc_send_preset).pack(side='left')
ttk.Label(r3, text='自定义:').pack(side='left', padx=(12, 2))
self.var_custom_cmd = tk.StringVar()
ent_cmd = ttk.Entry(r3, textvariable=self.var_custom_cmd, width=14)
ent_cmd.pack(side='left')
ent_cmd.bind('<Return>', lambda _e: self._dlpc_send_custom())
ttk.Button(r3, text='发送', width=7,
command=self._dlpc_send_custom).pack(side='left', padx=4)
ttk.Label(r3, text='如 M730S0T2000(定时 2 秒投光)',
foreground='#666666').pack(side='left', padx=6)
r4 = ttk.Frame(grp2)
r4.pack(fill='x', pady=(4, 0))
self.txt_dlpc = tk.Text(r4, height=4, font=('Consolas', 9),
state='disabled', wrap='none',
background='#101010', foreground='#C8C8C8')
sb4 = ttk.Scrollbar(r4, orient='vertical', command=self.txt_dlpc.yview)
self.txt_dlpc.configure(yscrollcommand=sb4.set)
self.txt_dlpc.pack(side='left', fill='both', expand=True)
sb4.pack(side='right', fill='y')
self._dlpc_log('帧格式: D5 01 LEN <命令> CRC / B5 TYPE LEN <payload> CRC, '
'累加和校验; 全部命令见 README"光机控制"')
self.after(80, self._poll_dlpc)
# --- 底部: EDID hex dump + 逐字节解析按钮 --- # --- 底部: EDID hex dump + 逐字节解析按钮 ---
bot = ttk.LabelFrame(self, text='EDID 原始数据', padding=4) bot = ttk.LabelFrame(self, text='EDID 原始数据', padding=4)
bot.pack(fill='both', expand=False, padx=8, pady=(6, 8)) bot.pack(fill='both', expand=False, padx=8, pady=(6, 8))
@@ -1181,6 +1285,104 @@ def run_gui(auto_close=0):
self.area = self.area_var.get() self.area = self.area_var.get()
save_slots(self.slots, self.area) save_slots(self.slots, self.area)
# ---- 光机控制(RS485) ----
def _dlpc_on_event(self, line):
self._dlpc_log_q.put(('event', line))
def _dlpc_on_status(self, msg):
self._dlpc_log_q.put(('status', msg))
def _poll_dlpc(self):
"""串口线程 -> GUI 主线程: 80ms 轮询日志队列并同步连接状态"""
try:
while True:
kind, text = self._dlpc_log_q.get_nowait()
if kind == 'status':
if text.startswith('已连接'):
color = '#2E7D32'
elif ('失败' in text or '丢失' in text or '' in text
or '非法' in text or '丢弃' in text):
color = '#B05000'
else:
color = '#666666'
self.lbl_dlpc_st.config(text=text, foreground=color)
self._dlpc_log(text)
except queue.Empty:
pass
connected = self.dlpc.is_connected()
if connected != self._dlpc_connected:
self._dlpc_connected = connected
self.btn_dlpc.config(text='断开' if connected else '连接')
self.after(80, self._poll_dlpc)
def _dlpc_log(self, line):
self.txt_dlpc.configure(state='normal')
self.txt_dlpc.insert('end', time.strftime('%H:%M:%S ') + line + '\n')
if int(self.txt_dlpc.index('end-1c').split('.')[0]) > 300:
self.txt_dlpc.delete('1.0', '2.0') # 只保留最近约 300 行
self.txt_dlpc.see('end')
self.txt_dlpc.configure(state='disabled')
def _dlpc_refresh(self):
ports = dlpc_rs485.DLPC485.list_ports()
self.cbo_dlpc['values'] = ports
if SERIAL_CFG['port'] in ports:
self.cbo_dlpc.set(SERIAL_CFG['port'])
elif ports:
self.cbo_dlpc.current(0)
def _dlpc_toggle(self):
if self.dlpc.is_connected():
self.dlpc.close()
return
port = self.cbo_dlpc.get().strip()
if not port:
self.lbl_dlpc_st.config(text='未选择串口', foreground='#B05000')
return
try:
baud = int(self.var_baud.get())
except ValueError:
self.lbl_dlpc_st.config(text='波特率非法', foreground='#B05000')
return
SERIAL_CFG['port'], SERIAL_CFG['baud'] = port, baud
save_slots(self.slots, self.area_var.get()) # 记住本次串口
self.dlpc.open(port, baud)
def _dlpc_send(self, cmd):
if not self.dlpc.is_connected():
self.lbl_dlpc_st.config(text='未连接串口', foreground='#B05000')
return
self.dlpc.send(cmd)
def _dlpc_send_preset(self):
i = self.cbo_dlpc_cmd.current()
if i >= 0:
self._dlpc_send(self._dlpc_presets[i][1])
def _dlpc_send_custom(self):
cmd = self.var_custom_cmd.get().strip()
if cmd:
self._dlpc_send(cmd)
def _dlpc_set_light(self):
from tkinter import messagebox
try:
v = int(self.var_light.get())
except ValueError:
messagebox.showinfo('光强非法', '光强需为 54-800 的整数')
return
if not 54 <= v <= 800:
messagebox.showinfo('光强越界', '光强范围 54-800(越界固件会回 err:OL)')
return
self._dlpc_send(f'M731S{v}')
def destroy(self):
try: # 两种退出路径(关窗/--seconds 自动关)都收掉串口线程
self.dlpc.close()
except Exception:
pass
super().destroy()
def _show_edid_fields(self): def _show_edid_fields(self):
"""逐字节字段解析弹窗(当前所选显示器), 自动换行文本视图""" """逐字节字段解析弹窗(当前所选显示器), 自动换行文本视图"""
from tkinter import messagebox from tkinter import messagebox
@@ -1384,8 +1586,50 @@ def run_dump(edid_fields=False):
return 0 return 0
def run_dlpc(port, baud, cmds):
"""RS485 直连模式: 逐条发送光机命令, 等响应打印译码结果(自动化脚本用)。
返回码: 任一命令非法/超时/响应非 0xF0 成功 -> 1, 全部成功 -> 0。
"""
if not dlpc_rs485.HAS_SERIAL:
print('错误: 未安装 pyserial, 请先执行 py -m pip install pyserial')
return 2
try:
ser = dlpc_rs485.serial.Serial(port, baud, timeout=0.1)
except Exception as e:
print(f'打开串口失败: {port} @ {baud}: {e}')
return 1
print(f'已连接 {port} @ {baud} (RS485 半双工, 单发单收)')
parser = dlpc_rs485.FrameParser()
rc = 0
for cmd in cmds:
try:
frame = dlpc_rs485.build_frame(cmd)
except ValueError as e:
print(f'{cmd} 命令非法: {e}')
rc = 1
continue
ser.write(frame)
print(f'{cmd} [{frame.hex(" ").upper()}]')
deadline = time.time() + 35.0 # M737 定时投光中延迟应答(定时上限 32.7s)
got = None
while got is None and time.time() < deadline:
data = ser.read(256)
if data:
got = next(iter(parser.feed(data)), None)
if got is None:
print('← (35s 超时无响应)')
rc = 1
else:
print(f'{dlpc_rs485.describe(got)} [{got.raw.hex(" ").upper()}]')
if got.type_code != 0xF0:
rc = 1
ser.close()
return rc
def main(): def main():
ap = argparse.ArgumentParser(description='HDMI EDID 查看器 + 测试图案') ap = argparse.ArgumentParser(description='HDMI EDID 查看器 + 测试图案 + 光机控制')
ap.add_argument('--dump', action='store_true', help='控制台输出 EDID 解算结果, 不开 GUI') ap.add_argument('--dump', action='store_true', help='控制台输出 EDID 解算结果, 不开 GUI')
ap.add_argument('--edid-fields', action='store_true', ap.add_argument('--edid-fields', action='store_true',
help='控制台输出 EDID 逐字节字段解析表(配合 --dump 风格, 不开 GUI); exe 版不支持') help='控制台输出 EDID 逐字节字段解析表(配合 --dump 风格, 不开 GUI); exe 版不支持')
@@ -1396,6 +1640,12 @@ def main():
help='测试卡区域: WxH 黑底居中 或 full 铺满所选显示器(默认 640x360)') help='测试卡区域: WxH 黑底居中 或 full 铺满所选显示器(默认 640x360)')
ap.add_argument('--monitor', type=int, default=1, help='--test-pattern 用的显示器序号(默认 1)') ap.add_argument('--monitor', type=int, default=1, help='--test-pattern 用的显示器序号(默认 1)')
ap.add_argument('--seconds', type=int, default=0, help='自动退出秒数(GUI/图案冒烟测试)') ap.add_argument('--seconds', type=int, default=0, help='自动退出秒数(GUI/图案冒烟测试)')
ap.add_argument('--port', default=None, help='光机 RS485 串口(如 COM9), 配合 --dlpc-cmd')
ap.add_argument('--baud', type=int, default=dlpc_rs485.DEFAULT_BAUD,
help='光机串口波特率(默认 115200)')
ap.add_argument('--dlpc-cmd', action='append', default=None, dest='dlpc_cmds',
metavar='CMD',
help='RS485 发送光机命令(可多次), 如 --dlpc-cmd M730S0 --dlpc-cmd M999')
args = ap.parse_args() args = ap.parse_args()
# LT8619C 板卡热插拔后可能停留在"仅电脑屏幕"拓扑, 先强制扩展, # LT8619C 板卡热插拔后可能停留在"仅电脑屏幕"拓扑, 先强制扩展,
@@ -1413,6 +1663,11 @@ def main():
'python hdmi_gui.py --edid-fields') 'python hdmi_gui.py --edid-fields')
sys.exit(1) sys.exit(1)
sys.exit(run_dump(edid_fields=args.edid_fields)) sys.exit(run_dump(edid_fields=args.edid_fields))
if args.dlpc_cmds:
if not args.port:
print('错误: --dlpc-cmd 需要配合 --port 指定串口(如 --port COM9)')
sys.exit(2)
sys.exit(run_dlpc(args.port, args.baud, args.dlpc_cmds))
if args.test_pattern: if args.test_pattern:
displays = get_displays() displays = get_displays()
if not displays: if not displays:
+77
View File
@@ -0,0 +1,77 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""dlpc_rs485 协议自检(无硬件): 对照上游 protocols.md 的实测帧向量做回归
用法: python selftest_rs485.py (全部通过退出码 0)
"""
import sys
import dlpc_rs485 as d
fails = []
def check(name, got, want):
ok = got == want
print(('PASS' if ok else 'FAIL'), name, '->', got, '' if ok else f'(期望 {want})')
if not ok:
fails.append(name)
# 1) 帧构造: protocols.md 实测示例 M730S0T2000
f = d.build_frame('M730S0T2000')
check('build_frame(M730S0T2000)', f.hex(' ').upper(),
'D5 01 0B 4D 37 33 30 53 30 54 32 30 30 30 8C')
# 2) 增量解析: ok 应答跨两个半包喂入
p = d.FrameParser()
r1 = p.feed(bytes.fromhex('B5 F0'))
r2 = p.feed(bytes.fromhex('02 6F 6B CC'))
check('split-feed frames', len(r1) + len(r2), 1)
fr = (r1 + r2)[0]
check('type_code', fr.type_code, 0xF0)
check('payload', fr.payload, 'ok')
check('describe(ok)', d.describe(fr), 'ok')
# 3) 前导噪声 + 0xFD err:OL 译码(字符串标记, 不附 DLPC 释义)
body = bytes.fromhex('FD 06 65 72 72 3A 4F 4C') # "err:OL"
good = bytes([0xB5]) + body + bytes([d.sum_crc(body)])
frames = d.FrameParser().feed(b'\x01\x02' + good)
check('noise+err-frame count', len(frames), 1)
check('describe(err:OL)', d.describe(frames[0]), '参数越界/读回复核不一致 err:OL')
# 4) 0xFA + 数字错误码 err:3 追加 DLPC 释义
body = bytes.fromhex('FA 05') + b'err:3'
fa = bytes([0xB5]) + body + bytes([d.sum_crc(body)])
frames = d.FrameParser().feed(fa)
check('describe(err:3)', d.describe(frames[0]),
'开光机通信失败 err:3(I2C NACK(器件无应答)')
# 5) CRC 坏帧被丢弃后继续同步到下一帧
bad = bytearray(good)
bad[-1] ^= 0xFF
frames = d.FrameParser().feed(bytes(bad) + good)
check('bad-crc then good', len(frames), 1)
check('recovered type', frames[0].type_code, 0xFD)
# 6) 非法输入拒绝
for bad_cmd in ('', ' '):
try:
d.build_frame(bad_cmd)
fails.append('empty-cmd-not-rejected')
except ValueError:
pass
try:
d.build_frame('M' * 40)
fails.append('long-cmd-not-rejected')
except ValueError:
pass
print('空/超长命令拒绝: PASS')
# 7) 预设命令全部可组帧
for _name, cmd, _note in d.PRESETS:
d.build_frame(cmd)
print(f'预设 {len(d.PRESETS)} 条全部可组帧: PASS')
print('RESULT:', 'FAIL:' + ','.join(fails) if fails else 'ALL PASS')
sys.exit(1 if fails else 0)
+2 -2
View File
@@ -7,7 +7,7 @@ LT8619CHDMI→RGB888)→ DLPC3421 投影转接板调试配套上位机工
| 工具 | 用途 | 获取 | | 工具 | 用途 | 获取 |
|---|---|---| |---|---|---|
| [HDMI-Tool](HDMI-Tool/) | HDMI 源端EDID 查看/逐字节解析、分辨率切换、纯色与测试图案全屏投屏 | 到 [Releases](https://gitea.hulk.wang/hulk/LT8619C_Debug_Tools/releases) 下 `HDMI-Tool-vX.Y.Z-win64.zip`,解压即用;详见 [HDMI-Tool/README.md](HDMI-Tool/README.md) | | [HDMI-Tool](HDMI-Tool/) | HDMI 源端EDID 查看/逐字节解析、分辨率切换、纯色与测试图案全屏投屏+ 光机控制(RS485→GD32 网关→DLPC3421:开关光机/设光强/查温度电流) | 到 [Releases](https://gitea.hulk.wang/hulk/LT8619C_Debug_Tools/releases) 下 `HDMI-Tool-vX.Y.Z-win64.zip`,解压即用;详见 [HDMI-Tool/README.md](HDMI-Tool/README.md) |
| [I2C-Inject](I2C-Inject/) | LT8619C 寄存器在线读写:RP2040 注入器在 demo 板 I2C 总线上择机注入(避让主控轮询,写后自动读回校验) | 需配合 RP2040 固件 `rp2040_inject.py`;详见 [I2C-Inject/README.md](I2C-Inject/README.md) | | [I2C-Inject](I2C-Inject/) | LT8619C 寄存器在线读写:RP2040 注入器在 demo 板 I2C 总线上择机注入(避让主控轮询,写后自动读回校验) | 需配合 RP2040 固件 `rp2040_inject.py`;详见 [I2C-Inject/README.md](I2C-Inject/README.md) |
## 环境要求 ## 环境要求
@@ -49,4 +49,4 @@ python publish_gitea.py HDMI-Tool-v<版本>-win64.zip --insecure # 上传 Gite
- 源码与打包产物(exe、pattern_slots.json)均入库;发布 zip 不入库,只上传 Gitea Release - 源码与打包产物(exe、pattern_slots.json)均入库;发布 zip 不入库,只上传 Gitea Release
- 两件工具的寄存器语义(0x606E 位交换 / 0x606D 整组顺序)均经 demo 板逻辑分析仪 - 两件工具的寄存器语义(0x606E 位交换 / 0x606D 整组顺序)均经 demo 板逻辑分析仪
双色交叉实测,过程数据与规程存于上游主仓库 `doc/architecture/` 双色交叉实测,过程数据与规程存于上游主仓库 `doc/architecture/`
- 更新:2026-09-10 - 更新:2026-09-11