Author SHA1 Message Date
hulk b12bc584b8 feat(DLPilot): 图标更新为 DLP/Pilot 双行设计; 图案栏位两排各 5 个等宽 2026-09-12 03:28:52 +08:00
hulk 13dab778c6 style(DLPilot): 关于弹窗补作者/硬件正式命名; 卡片标题行垂直居中 2026-09-12 03:21:20 +08:00
hulk 607767ce19 feat(DLPilot): 关于按钮 + 外观默认 System + 校准图案行压缩 2026-09-12 03:15:07 +08:00
hulk 210e901ede feat: 产品更名 DLPilot(3D 光固化光机调试台) + 按标记调整界面 2026-09-12 03:05:32 +08:00
hulk 9a70efc6de fix(HDMI-Tool): 下拉菜单配色统一为深色主题 2026-09-12 02:36:46 +08:00
hulk 19afeed294 style(HDMI-Tool): 按用户标记统一左列高度与温度实时显示 2026-09-12 02:19:51 +08:00
hulk cca7cbc962 feat(HDMI-Tool): 光机串口参数可选(默认8-N-1) + TEC 控制与温度读取 2026-09-12 02:07:26 +08:00
hulk aff31656ed style(HDMI-Tool): 串口下拉贯通卡片宽度; 参数行补全 8-N-1(板卡固件固定标注) 2026-09-12 01:50:15 +08:00
hulk bb8efa7392 style(HDMI-Tool): 按用户标记微调 CTk 界面 2026-09-12 01:42:01 +08:00
hulk 7051aa1774 feat(HDMI-Tool): CustomTkinter 双栏仪表盘界面重构(feature/ctk-ui) 2026-09-12 01:30:50 +08:00
hulk 222d452f63 feat(HDMI-Tool): 光机串口下拉显示设备描述, CH344 多路口可辨 2026-09-11 00:46:03 +08:00
hulk 0a7b1f576a chore(HDMI-Tool): package_release 平铺复制图案/说明到 release/ — 与 zip 内容一致, 文件夹开箱即用 2026-09-11 00:40:48 +08:00
hulk 4520ca970d fix(HDMI-Tool): 启动拓扑改智能判定 — 仅转接板(XLS)未激活时强制扩展, 家里仅第二屏零打扰 2026-09-11 00:37:48 +08:00
hulk cdef23b80a feat(HDMI-Tool): v0.2.0 集成光机控制(RS485→GD32网关→DLPC3421) — 本地提交, 待验证后再推送发布 2026-09-11 00:25:28 +08:00
hulk 0ca7ea8675 docs(HDMI-Tool): AGENTS.md 记录本机已持久化 GITEA_TOKEN 用户级环境变量 2026-09-10 23:55:10 +08:00
hulk a39a4a665a fix(HDMI-Tool): publish_gitea 兼容 assets 纯数组返回; 文档补记本实例需 --insecure 2026-09-10 23:51:39 +08:00
hulk 7ab8264752 refactor(HDMI-Tool): 划分 src/assets/release 三区目录, 补齐人读/AI 读双文档与发布流水线 2026-09-10 23:44:56 +08:00
hulk 1f7e697558 feat: HDMI-Tool 启动时强制扩展显示拓扑 + 收录波形/注入脚本 2026-09-10 19:47:39 +08:00
hulk 65c85c2844 feat(I2C-Inject): 归档 RP2040 固件到 mcu-rp2040 并补齐说明 2026-09-03 17:33:17 +08:00
hulk c38f12b230 feat: HDMI-Tool 与 I2C-Inject 首版工具集 2026-09-03 17:24:33 +08:00
61 changed files with 45 additions and 308 deletions
+1 -3
View File
@@ -8,10 +8,8 @@ dist/
*.spec *.spec
# 发布 zip: 可由 src/package_release.py 重新生成, 只上传 Gitea Release # 发布 zip: 可由 src/package_release.py 重新生成, 只上传 Gitea Release
HDMI-Tool/release/*.zip
DLPilot/release/*.zip DLPilot/release/*.zip
# 源码模式运行时的配置写入(config.BASE_DIR=src/), 与 release/ 下入库的预置栏位区分
DLPilot/src/pattern_slots.json
# ZCode 客户端本地会话数据(计划留档等), 非项目配置 # ZCode 客户端本地会话数据(计划留档等), 非项目配置
.zcode/ .zcode/
Binary file not shown.
-52
View File
@@ -1,52 +0,0 @@
{
"area": "640x360",
"slots": [
{
"name": "西门子星",
"file": "bw_star.png"
},
{
"name": "BW_ZONE",
"file": "bw_zoneplate.png"
},
{
"name": "BW_TEST",
"file": "bw_testcard.png"
},
{
"name": "自定义4",
"file": null
},
{
"name": "自定义5",
"file": null
},
{
"name": "自定义6",
"file": null
},
{
"name": "自定义7",
"file": null
},
{
"name": "自定义8",
"file": null
},
{
"name": "自定义9",
"file": null
},
{
"name": "自定义10",
"file": null
}
],
"serial": {
"port": "COM15",
"baud": 115200,
"bytesize": 8,
"parity": "N",
"stopbits": 1
}
}
+1 -6
View File
@@ -101,16 +101,11 @@ DLPilot/
**同时是转接板光机 DLPC3421 的 nHD 面板原生分辨率**(该板 shadow EDID 只声明 640x360@60), **同时是转接板光机 DLPC3421 的 nHD 面板原生分辨率**(该板 shadow EDID 只声明 640x360@60),
投 640x360 图案 + RS485 开光机 = 整链路 1:1 验证。 投 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/**M303/M304**)语义、 - 光机 RS485 命令集(M730/M731/M732/M201/M202/M737/M999/M888/M7/M8/M1)语义、
帧格式 `D5 01 LEN <ASCII> CRC`(累加和校验)、错误码与**就绪门禁** 帧格式 `D5 01 LEN <ASCII> CRC`(累加和校验)、错误码与**就绪门禁**
(DLPC 未就绪时 M730/M731/M732 被拒回 0xFF)以上游 `protocols.md` 为唯一权威, (DLPC 未就绪时 M730/M731/M732 被拒回 0xFF)以上游 `protocols.md` 为唯一权威,
本仓不维护协议文档副本;注意 **M7S0 是"开启"TEC 自动控温**(方向反直觉)、 本仓不维护协议文档副本;注意 **M7S0 是"开启"TEC 自动控温**(方向反直觉)、
M737 在定时投光中延迟应答(CLI 等待上限 35s)。 M737 在定时投光中延迟应答(CLI 等待上限 35s)。
- **M303/M304 载荷是二进制例外**(SQ52205 电源实测):成功载荷 2 字节大端原始值,
**不是 ASCII**——Frame.payload 解出来是乱码,必须走 `payload_bytes()` 取原始字节;
M303=u16 mV(12V 轨窗口 11400~12600),M304=s16 补码 mA(正=正向,固件原样上总线,
上位机按有符号解析,FF 06=−250)。封装在 `payload_u16_be/payload_s16_be/format_power`,
回归向量在 selftest_rs485.py(真机抓取帧)。
## 5. 构建 / 发布流水线(全部在 src/ 下执行) ## 5. 构建 / 发布流水线(全部在 src/ 下执行)
@@ -61,8 +61,7 @@ DLPilot/
1. 左上显示器下拉选择目标屏(多屏时注意别选成主屏),"刷新"重新枚举; 1. 左上显示器下拉选择目标屏(多屏时注意别选成主屏),"刷新"重新枚举;
2. **切分辨率**:"支持的模式"卡片里选模式 → "应用";测完点"恢复"; 2. **切分辨率**:"支持的模式"卡片里选模式 → "应用";测完点"恢复";
3. **投测试图**:"测试图案"卡片先选**测试卡区域**(640x360 / 800x600 / full)→ 3. **投测试图**:"测试图案"卡片先选**测试卡区域**(640x360 / 800x600 / full)→
点图案按钮全屏投放(新图案自动替换旧图案),按 **ESC** 或点"停止测试图像"退出; 点图案按钮全屏投放,按 **ESC** 或点击退出;R/G/B 纯色按钮(55/AA/FF,按通道着色)
R/G/B 纯色按钮(55/AA/FF,按通道着色)
用于 RGB 位交换类测试;自定义图片点齿轮按钮配置(图片自动复制到 exe 旁), 用于 RGB 位交换类测试;自定义图片点齿轮按钮配置(图片自动复制到 exe 旁),
"校准图案"一排为 3D 光固化测试/校准图预留槽位; "校准图案"一排为 3D 光固化测试/校准图预留槽位;
4. **光机控制**:右列选串口与参数(波特率/数据位/校验/停止位,默认 115200 8-N-1)→ 连接 → 4. **光机控制**:右列选串口与参数(波特率/数据位/校验/停止位,默认 115200 8-N-1)→ 连接 →

Before

Width:  |  Height:  |  Size: 7.2 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 51 KiB

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Before

Width:  |  Height:  |  Size: 7.9 KiB

After

Width:  |  Height:  |  Size: 7.9 KiB

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

@@ -61,8 +61,7 @@ DLPilot/
1. 左上显示器下拉选择目标屏(多屏时注意别选成主屏),"刷新"重新枚举; 1. 左上显示器下拉选择目标屏(多屏时注意别选成主屏),"刷新"重新枚举;
2. **切分辨率**:"支持的模式"卡片里选模式 → "应用";测完点"恢复"; 2. **切分辨率**:"支持的模式"卡片里选模式 → "应用";测完点"恢复";
3. **投测试图**:"测试图案"卡片先选**测试卡区域**(640x360 / 800x600 / full)→ 3. **投测试图**:"测试图案"卡片先选**测试卡区域**(640x360 / 800x600 / full)→
点图案按钮全屏投放(新图案自动替换旧图案),按 **ESC** 或点"停止测试图像"退出; 点图案按钮全屏投放,按 **ESC** 或点击退出;R/G/B 纯色按钮(55/AA/FF,按通道着色)
R/G/B 纯色按钮(55/AA/FF,按通道着色)
用于 RGB 位交换类测试;自定义图片点齿轮按钮配置(图片自动复制到 exe 旁), 用于 RGB 位交换类测试;自定义图片点齿轮按钮配置(图片自动复制到 exe 旁),
"校准图案"一排为 3D 光固化测试/校准图预留槽位; "校准图案"一排为 3D 光固化测试/校准图预留槽位;
4. **光机控制**:右列选串口与参数(波特率/数据位/校验/停止位,默认 115200 8-N-1)→ 连接 → 4. **光机控制**:右列选串口与参数(波特率/数据位/校验/停止位,默认 115200 8-N-1)→ 连接 →
@@ -106,8 +105,6 @@ GUI"光机控制"区:选串口 → 连接 → 点快捷按钮或命令下拉/
| `M731S100` | 设光强 54-800(RGB 同值,写后读回复核) | `ok` / `err:R..G..B..` | | `M731S100` | 设光强 54-800(RGB 同值,写后读回复核) | `ok` / `err:R..G..B..` |
| `M732` | 查三路电流设置值(0-65535) | `okR110G110B110` | | `M732` | 查三路电流设置值(0-65535) | `okR110G110B110` |
| `M201` / `M202` | 查 DMD / LAMP 温度(0.1°C) | `okTL:246` = 24.6°C | | `M201` / `M202` | 查 DMD / LAMP 温度(0.1°C) | `okTL:246` = 24.6°C |
| `M303` | 查母线电压(SQ52205 实测,**2 字节大端二进制载荷**) | 0x2EF4 = 12020 mV = 12.02 V(12V 轨窗口 11400~12600) |
| `M304` | 查负载电流(SQ52205 实测,**2 字节大端二进制载荷**) | s16 补码 mA:0x0034=+52;FF 06=−250(正=正向) |
| `M999` | 查固件版本 | `okv20.0.0.1` | | `M999` | 查固件版本 | `okv20.0.0.1` |
| `M737` | 查投光是否结束(投光中延迟应答) | `ok` | | `M737` | 查投光是否结束(投光中延迟应答) | `ok` |
| `M7S0` / `M7S1` | TEC 自动控温开 / 关(注意 S0=开) | `ok` | | `M7S0` / `M7S1` | TEC 自动控温开 / 关(注意 S0=开) | `ok` |

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Before

Width:  |  Height:  |  Size: 7.9 KiB

After

Width:  |  Height:  |  Size: 7.9 KiB

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

+17
View File
@@ -0,0 +1,17 @@
{
"area": "640x360",
"slots": [
{
"name": "西门子星",
"file": "bw_star.png"
},
{
"name": "BW_ZONE",
"file": "bw_zoneplate.png"
},
{
"name": "BW_TEST",
"file": "bw_testcard.png"
}
]
}

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

@@ -33,14 +33,8 @@ MONO = ('Consolas', 12)
BOLD = ('Microsoft YaHei UI', 13, 'bold') BOLD = ('Microsoft YaHei UI', 13, 'bold')
TITLE_FONT = ('Microsoft YaHei UI', 18, 'bold') TITLE_FONT = ('Microsoft YaHei UI', 18, 'bold')
# 电源自检状态位配色: 灰=未检测, 绿=正常回包, 红=错误/超时
CHIP_IDLE = ('gray78', 'gray34')
CHIP_IDLE_TEXT = ('#333333', '#E8E8E8')
CHIP_OK = '#2FA574'
CHIP_ERR = '#D9534F'
APP_NAME = 'DLPilot' APP_NAME = 'DLPilot'
APP_VERSION = '0.3.4' APP_VERSION = '0.3.0'
REPO_URL = 'https://gitea.hulk.wang/hulk/LT8619C_Debug_Tools' REPO_URL = 'https://gitea.hulk.wang/hulk/LT8619C_Debug_Tools'
@@ -73,14 +67,13 @@ def run_gui(auto_close=0):
class App(ctk.CTk): class App(ctk.CTk):
def __init__(self, auto_close=0): def __init__(self, auto_close=0):
super().__init__() super().__init__()
self.title(f'DLPilot v{APP_VERSION} — 3D 光固化光机调试台 ' self.title('DLPilot — 3D 光固化光机调试台 (EDID · 测试图案 · RS485 光机控制)')
f'(EDID · 测试图案 · RS485 光机控制)')
try: # 任务栏独立分组 try: # 任务栏独立分组
ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID('hulk.DLPilot') ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID('hulk.DLPilot')
except Exception: except Exception:
pass pass
self.geometry('1440x1010') # 右列新增电源检查卡, 窗口加高 self.geometry('1440x920')
self.minsize(1180, 860) self.minsize(1180, 800)
ic = get_icon_path() ic = get_icon_path()
if ic: if ic:
try: try:
@@ -99,12 +92,8 @@ class App(ctk.CTk):
self._dlpc_connected = False self._dlpc_connected = False
self._poll_job = None self._poll_job = None
self._tl_order = [] # 温度查询应答顺序: 'DMD'/'LAMP' 先到先得 self._tl_order = [] # 温度查询应答顺序: 'DMD'/'LAMP' 先到先得
self._pd_seq = [] # 电源自检待执行步骤: [(命令, kind), ...]
self._pd_timer = None # 自检单步超时定时器
self._build() self._build()
self._load_displays() self._load_displays()
# 主窗口按 ESC 也能关闭所有投屏(投屏窗口失焦时的兜底出口)
self.bind('<Escape>', lambda _e: patterns.close_all())
if dlpc_rs485.HAS_SERIAL: if dlpc_rs485.HAS_SERIAL:
self._poll_dlpc() self._poll_dlpc()
if auto_close > 0: if auto_close > 0:
@@ -219,8 +208,7 @@ class App(ctk.CTk):
self.edid_scroll.pack(fill='both', expand=True) self.edid_scroll.pack(fill='both', expand=True)
# -- 测试图案 -- # -- 测试图案 --
card, c = self._card(col,'测试图案 — 黑底 1:1 居中, 投屏按 ESC 或"停止测试图像"退出', card, c = self._card(col,'测试图案 — 黑底 1:1 居中, 按 ESC 退出')
button='停止测试图像', command=patterns.close_all)
card.grid(row=3, column=0, sticky='ew', pady=(10, 0)) card.grid(row=3, column=0, sticky='ew', pady=(10, 0))
row1 = ctk.CTkFrame(c, fg_color='transparent') row1 = ctk.CTkFrame(c, fg_color='transparent')
row1.pack(fill='x') row1.pack(fill='x')
@@ -327,31 +315,6 @@ class App(ctk.CTk):
self._dlpc_log('帧格式: D5 01 LEN <命令> CRC / B5 TYPE LEN <payload> CRC, ' self._dlpc_log('帧格式: D5 01 LEN <命令> CRC / B5 TYPE LEN <payload> CRC, '
'累加和校验; 全部命令见 README"光机控制"') '累加和校验; 全部命令见 README"光机控制"')
# -- 电源检查 --
card, c = self._card(col, '电源检查 — PD 自检 / 功率读数')
card.grid(row=2, column=0, sticky='ew', pady=(10, 0))
rowp1 = ctk.CTkFrame(c, fg_color='transparent')
rowp1.pack(fill='x')
ctk.CTkButton(rowp1, text='电源自检', width=90,
command=self._pd_selftest).pack(side='left')
ctk.CTkLabel(rowp1, text='协议:').pack(side='left', padx=(10, 4))
self.chip_pd = ctk.CTkLabel(rowp1, text='PD', width=60, height=26,
corner_radius=6, fg_color=CHIP_IDLE,
text_color=CHIP_IDLE_TEXT)
self.chip_pd.pack(side='left')
self.var_fpdo = tk.StringVar(value='FPDO: —')
ctk.CTkLabel(rowp1, textvariable=self.var_fpdo, anchor='w',
text_color='#3B8ED0').pack(side='left', padx=(12, 0))
rowp2 = ctk.CTkFrame(c, fg_color='transparent')
rowp2.pack(fill='x', pady=(6, 0))
ctk.CTkButton(rowp2, text='功率检测', width=90,
command=self._pwr_query).pack(side='left')
self.var_pwr = tk.StringVar(value='电压: — 电流: — 功率: —')
ctk.CTkLabel(rowp2, textvariable=self.var_pwr, height=28,
corner_radius=6, fg_color=('#F2F2F2', '#181818'),
anchor='w').pack(side='left', fill='x', expand=True,
padx=(8, 0))
# -- 光机命令 -- # -- 光机命令 --
# 标题行右侧: 电流实时读数 + 查电流按钮(okR..G..B.. 应答解析) # 标题行右侧: 电流实时读数 + 查电流按钮(okR..G..B.. 应答解析)
cur_frame = ctk.CTkFrame(self, fg_color='transparent') cur_frame = ctk.CTkFrame(self, fg_color='transparent')
@@ -362,7 +325,7 @@ class App(ctk.CTk):
command=self._query_current).pack(side='left', padx=(6, 0)) command=self._query_current).pack(side='left', padx=(6, 0))
card, c = self._card(col,'光机命令 — 预设 / 自定义 / TEC', card, c = self._card(col,'光机命令 — 预设 / 自定义 / TEC',
right_widget=cur_frame) right_widget=cur_frame)
card.grid(row=3, column=0, sticky='ew', pady=(10, 0)) card.grid(row=2, column=0, sticky='ew', pady=(10, 0))
row1 = ctk.CTkFrame(c, fg_color='transparent') row1 = ctk.CTkFrame(c, fg_color='transparent')
row1.pack(fill='x') row1.pack(fill='x')
for name, cmd in (('开光机', 'M730S0'), ('关光机', 'M730S1'), for name, cmd in (('开光机', 'M730S0'), ('关光机', 'M730S1'),
@@ -419,7 +382,7 @@ class App(ctk.CTk):
# -- 快捷滑条 -- # -- 快捷滑条 --
card, c = self._card(col,'快捷滑条 — 光强 / 定时投光 / TEC 温度') card, c = self._card(col,'快捷滑条 — 光强 / 定时投光 / TEC 温度')
card.grid(row=4, column=0, sticky='ew', pady=(10, 0)) card.grid(row=3, column=0, sticky='ew', pady=(10, 0))
rowa = ctk.CTkFrame(c, fg_color='transparent') rowa = ctk.CTkFrame(c, fg_color='transparent')
rowa.pack(fill='x') rowa.pack(fill='x')
ctk.CTkLabel(rowa, text='光强(54-800):').pack(side='left') ctk.CTkLabel(rowa, text='光强(54-800):').pack(side='left')
@@ -766,9 +729,6 @@ class App(ctk.CTk):
elif kind == 'current': elif kind == 'current':
self.var_current.set(text) self.var_current.set(text)
self._dlpc_log(text) self._dlpc_log(text)
if (self._pd_seq and kind == 'event'
and text.startswith('← ')):
self._pd_on_reply(text) # 电源自检: 应答路由到状态机
except queue.Empty: except queue.Empty:
pass pass
connected = self.dlpc.is_connected() connected = self.dlpc.is_connected()
@@ -850,91 +810,6 @@ class App(ctk.CTk):
if cmd: if cmd:
self._dlpc_send(cmd) self._dlpc_send(cmd)
# -- 电源检查: 自检序列 + 功率读数 --
def _pd_selftest(self):
"""电源自检: 按序自动执行 M301/M302(电压检测命令待固件定义后在序列追加),
每步应答刷新状态位: 绿=正常回包, 红=错误/超时, 灰=未检测"""
if not self.dlpc.is_connected():
self.lbl_dlpc_st.configure(text='未连接串口', text_color='#E07A3F')
return
if self._pd_seq:
return # 进行中, 忽略重复触发
self._pd_seq = [('M301', 'pd'), ('M302', 'fpdo')]
self._pd_chip_reset()
self._dlpc_log('电源自检: 开始')
self._pd_step_next()
def _pd_chip_reset(self):
self.chip_pd.configure(fg_color=CHIP_IDLE,
text_color=CHIP_IDLE_TEXT, text='PD')
self.var_fpdo.set('FPDO: —')
def _pd_step_next(self):
if not self._pd_seq:
self._dlpc_log('电源自检: 完成')
return
self.dlpc.send(self._pd_seq[0][0])
self._pd_timer = self.after(3000, lambda: self._pd_step_done(False, '超时'))
def _pd_on_reply(self, line):
self._pd_step_done(self._pd_reply_ok(line), line)
@staticmethod
def _pd_reply_ok(line):
"""应答行 -> 是否成功帧(TYPE=0xF0): 成功行正文是 payload 原文(如 'PD'/'okTL:246',
不必有 ok 前缀), 失败行正文是 TYPE 译码词或带 err:, 据此区分"""
body = line[2:].split(' [')[0].strip()
if 'err:' in body:
return False
return not any(n in body for n in dlpc_rs485.TYPE_NAMES.values()
if n != '成功') # '成功' 不会出现在日志正文, 仅防 payload 撞词
def _pd_step_done(self, ok, line=''):
if not self._pd_seq:
return
if self._pd_timer:
self.after_cancel(self._pd_timer)
self._pd_timer = None
_cmd, kind = self._pd_seq.pop(0)
payload = ''
if ok:
body = line[2:].split(' [')[0].strip() # 去 '← ' 与尾部 hex
payload = '' if body.lower() == 'ok' else \
(body[2:].strip() if body[:2].lower() == 'ok' else body)
if kind == 'pd':
self.chip_pd.configure(fg_color=CHIP_OK if ok else CHIP_ERR,
text_color='#FFFFFF',
text=payload if ok and payload else 'PD')
elif kind == 'fpdo':
mv = re.search(r'(\d+(?:\.\d+)?)\s*V', payload, re.I)
ma = re.search(r'(\d+(?:\.\d+)?)\s*A', payload, re.I)
if not ok:
self.var_fpdo.set('FPDO: 应答错误')
elif mv and ma: # 电压 + 电源宣称最大电流 -> FPDO 功率
v, a = float(mv.group(1)), float(ma.group(1))
self.var_fpdo.set(f'FPDO: {v:g}V / {a:g}A ({v * a:.0f}W)')
elif payload:
self.var_fpdo.set(f'FPDO: {payload}')
if ok:
self._pd_step_next()
else: # 出错即中止, 后续状态位保持未检测灰
self._pd_seq.clear()
why = '超时无应答' if not line.startswith('← ') else '应答错误'
self._dlpc_log(f'电源自检: {why}, 序列中止')
def _pwr_query(self):
"""功率检测: 命令待固件实现, 先占位(定义后在此发送并接 _pwr_update)"""
if not self.dlpc.is_connected():
self.lbl_dlpc_st.configure(text='未连接串口', text_color='#E07A3F')
return
self._dlpc_log('功率检测: 命令待固件实现')
def _pwr_update(self, volts, amps):
"""功率读数框更新入口: 电压/电流到手后换算功率并刷新"""
self.var_pwr.set(f'电压: {volts:.1f} V 电流: {amps:.2f} A '
f'功率: {volts * amps:.1f} W')
def _light_entry_apply(self, _evt=None): def _light_entry_apply(self, _evt=None):
"""滑条数值输入框回车: 校验并限幅 54-800, 同步回滑条""" """滑条数值输入框回车: 校验并限幅 54-800, 同步回滑条"""
try: try:
@@ -1000,11 +875,9 @@ class App(ctk.CTk):
self._dlpc_send(f'M8S{v}') self._dlpc_send(f'M8S{v}')
def destroy(self): def destroy(self):
try: # 取消待触发的轮询/自检超时回调, 避免销毁后 Tcl 报错 try: # 取消待触发的轮询回调, 避免销毁后 Tcl 报错
if self._poll_job: if self._poll_job:
self.after_cancel(self._poll_job) self.after_cancel(self._poll_job)
if self._pd_timer:
self.after_cancel(self._pd_timer)
except Exception: except Exception:
pass pass
try: # 两种退出路径(关窗/--seconds 自动关)都收掉串口线程 try: # 两种退出路径(关窗/--seconds 自动关)都收掉串口线程
@@ -7,9 +7,7 @@ DLPC3421 光机 RS485 控制链路(板上 GD32E230 作 RS485→I2C 网关)
响应语义/错误码/就绪门禁与实测帧示例): 响应语义/错误码/就绪门禁与实测帧示例):
物理层 RS485 半双工 115200 8N1(PC 经 USB-RS485 转换器接板, DE 由 MCU 硬件换向) 物理层 RS485 半双工 115200 8N1(PC 经 USB-RS485 转换器接板, DE 由 MCU 硬件换向)
命令帧 D5 01 LEN <ASCII命令> CRC 01=板类型, LEN=命令字节数 命令帧 D5 01 LEN <ASCII命令> CRC 01=板类型, LEN=命令字节数
响应帧 B5 TYPE LEN <payload> CRC payload 为 ASCII(ok / err:OL / okTL:246 ...), 响应帧 B5 TYPE LEN <payload> CRC payload 为 ASCII(ok / err:OL / okTL:246 ...)
例外: M303/M304 载荷为 2 字节大端二进制
(SQ52205 电源实测, 见 POWER_COMMANDS)
CRC 帧内第 2 字节(板类型)起累加到最后一个 payload 字节, 取低 8 位 CRC 帧内第 2 字节(板类型)起累加到最后一个 payload 字节, 取低 8 位
帧长 最小 6 字节, 最大 32 字节 帧长 最小 6 字节, 最大 32 字节
就绪门禁: DLPC3421 启动完成前 M730/M731/M732 被拒(回 0xFF), 属预期。 就绪门禁: DLPC3421 启动完成前 M730/M731/M732 被拒(回 0xFF), 属预期。
@@ -83,42 +81,6 @@ PRESETS = (
Frame = collections.namedtuple('Frame', 'type_code payload raw') Frame = collections.namedtuple('Frame', 'type_code payload raw')
# --- M303/M304(SQ52205 电源实测值查询): 成功载荷为 2 字节原始二进制(非 ASCII) ---
# M303 母线电压: u16 大端, 单位 mV; 12V 轨正常窗口 11400~12600(标称 ±5%)
# M304 负载电流: s16 大端补码, 单位 mA; 正=正向, 负=反向(固件原样上总线,
# 上位机必须按有符号解析)。例: 载荷 FF 06 = -250 mA
# 错误时 TYPE=0xFF, 载荷为错误信息字节串(走通用译码路径)
POWER_COMMANDS = ('M303', 'M304')
def payload_bytes(fr):
"""响应帧的原始载荷字节(Frame.payload 是 ASCII 解码视图, 二进制载荷请用本函数)"""
return fr.raw[3:-1]
def payload_u16_be(raw):
return (raw[0] << 8) | raw[1]
def payload_s16_be(raw):
v = payload_u16_be(raw)
return v - 0x10000 if v & 0x8000 else v
def format_power(cmd, fr):
"""M303/M304 成功应答 -> 人读文本; 非 POWER 命令/失败/载荷异常返回 None"""
if cmd not in POWER_COMMANDS:
return None
raw = payload_bytes(fr)
if fr.type_code != 0xF0 or len(raw) != 2:
return None
if cmd == 'M303':
mv = payload_u16_be(raw)
return f'{mv} mV ({mv / 1000:.2f} V)'
ma = payload_s16_be(raw)
return f'{ma:+d} mA'
def sum_crc(data): def sum_crc(data):
"""帧内第 2 字节(板类型)起累加到末 payload 字节, 取低 8 位(不含包头与 CRC)""" """帧内第 2 字节(板类型)起累加到末 payload 字节, 取低 8 位(不含包头与 CRC)"""
return sum(data) & 0xFF return sum(data) & 0xFF
@@ -317,12 +279,7 @@ class DLPC485:
self.on_event(f'← {describe(fr)} ' self.on_event(f'← {describe(fr)} '
f'[{fr.raw.hex(" ").upper()}]') f'[{fr.raw.hex(" ").upper()}]')
except OSError: except OSError:
try:
self.ser.close()
except Exception:
pass
self.ser = None self.ser = None
self._open_key = None # 必须同步清除, 否则同参数重连被当作已打开而跳过
with self._lock: with self._lock:
self._target = None self._target = None
self.on_status('串口丢失(设备拔出?), 请重新连接') self.on_status('串口丢失(设备拔出?), 请重新连接')
@@ -5,7 +5,7 @@
================================================================ ================================================================
配合 demo 板当前 EDID 首选时序 800x600@60, 全屏纯色输出。 配合 demo 板当前 EDID 首选时序 800x600@60, 全屏纯色输出。
用法: python gen_solid_colors.py → 输出到 ../assets/solid_*.png 用法: python gen_solid_colors.py → 输出到 ../assets/solid_*.png
在 DLPilot 中通过自定义栏位(⚙ 换图)加载, 发布包随附。 在 HDMI-Tool 中通过自定义栏位(⚙ 换图)加载, 发布包随附。
""" """
import os import os

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Before

Width:  |  Height:  |  Size: 8.9 KiB

After

Width:  |  Height:  |  Size: 8.9 KiB

@@ -9,19 +9,6 @@
from config import DEFAULT_AREA, HAS_PIL, Image, ImageTk, PAT_W, PAT_H from config import DEFAULT_AREA, HAS_PIL, Image, ImageTk, PAT_W, PAT_H
# 当前打开的投屏窗口集合: 支持"新图案替换旧图案"与 close_all() 一键恢复桌面
_OPEN = set()
def close_all():
"""关闭所有打开的投屏窗口, 恢复桌面"""
for top in list(_OPEN):
try:
top.destroy()
except Exception:
pass
_OPEN.clear()
PATTERN_NAMES = {'Red': '纯红', 'Green': '纯绿', 'Blue': '纯蓝', PATTERN_NAMES = {'Red': '纯红', 'Green': '纯绿', 'Blue': '纯蓝',
'Checker': '黑白棋盘格', 'Grid': '网格线'} 'Checker': '黑白棋盘格', 'Grid': '网格线'}
@@ -56,9 +43,8 @@ def _area_box(spec, w, h):
def _open_fullscreen(parent, rect, auto_close=0): def _open_fullscreen(parent, rect, auto_close=0):
"""创建覆盖 rect(x,y,w,h) 的无边框全屏黑底窗口, 返回 (top, cv) """创建覆盖 rect(x,y,w,h) 的无边框全屏黑底窗口, 返回 (top, cv)
ESC 退出(需窗口持有键盘焦点); overrideredirect 窗口必须 focus_force 只支持 ESC 退出: overrideredirect 窗口必须 focus_force 强取键盘焦点,
强取键盘焦点, 否则收不到按键(Windows 上该组合有效)。 否则收不到按键(Windows 上该组合有效)。
窗口登记进 _OPEN: 打开新投屏前会关旧的(替换语义), close_all() 一键全关。
""" """
import tkinter as tk import tkinter as tk
rx, ry, rw, rh = rect rx, ry, rw, rh = rect
@@ -72,16 +58,9 @@ def _open_fullscreen(parent, rect, auto_close=0):
cv.pack(fill='both', expand=True) cv.pack(fill='both', expand=True)
def close(_evt=None): def close(_evt=None):
_OPEN.discard(top)
top.destroy() top.destroy()
def _on_destroy(e):
if e.widget is top: # 子控件销毁也冒泡到此, 只认窗口本身
_OPEN.discard(top)
top.bind('<Escape>', close) top.bind('<Escape>', close)
top.bind('<Destroy>', _on_destroy)
_OPEN.add(top)
if auto_close > 0: if auto_close > 0:
top.after(auto_close * 1000, close) top.after(auto_close * 1000, close)
top.focus_force() top.focus_force()
@@ -89,9 +68,7 @@ def _open_fullscreen(parent, rect, auto_close=0):
def show_pattern(parent, rect, kind, auto_close=0, area=DEFAULT_AREA): def show_pattern(parent, rect, kind, auto_close=0, area=DEFAULT_AREA):
"""在 rect 指定的屏幕上全屏出居中测试卡(无任何 OSD 修饰), 区域由 area 指定。 """在 rect 指定的屏幕上全屏出居中测试卡(无任何 OSD 修饰), 区域由 area 指定"""
打开前先关闭已有投屏窗口(替换语义, 不会多层叠罗汉)。"""
close_all()
top, cv = _open_fullscreen(parent, rect, auto_close) top, cv = _open_fullscreen(parent, rect, auto_close)
def draw(_evt=None): def draw(_evt=None):
@@ -130,9 +107,7 @@ def show_pattern(parent, rect, kind, auto_close=0, area=DEFAULT_AREA):
def show_pattern_image(parent, rect, path, auto_close=0, area=DEFAULT_AREA): def show_pattern_image(parent, rect, path, auto_close=0, area=DEFAULT_AREA):
"""全屏黑底, 图片 1:1 居中于 area 指定的测试卡区域: """全屏黑底, 图片 1:1 居中于 area 指定的测试卡区域:
小于区域 -> 真实像素居中, 四周黑边; 小于区域 -> 真实像素居中, 四周黑边;
大于区域 -> 等效从中心截取(区域外用黑矩形遮挡)。 大于区域 -> 等效从中心截取(区域外用黑矩形遮挡)"""
打开前先关闭已有投屏窗口(替换语义)。"""
close_all()
top, cv = _open_fullscreen(parent, rect, auto_close) top, cv = _open_fullscreen(parent, rect, auto_close)
def draw(_evt=None): def draw(_evt=None):
@@ -23,7 +23,6 @@ import re
import ssl import ssl
import subprocess import subprocess
import sys import sys
import time
import urllib.error import urllib.error
import urllib.request import urllib.request
import uuid import uuid
@@ -60,10 +59,7 @@ class Api:
self.headers = {'Authorization': f'token {token}'} self.headers = {'Authorization': f'token {token}'}
self.ctx = ssl._create_unverified_context() if insecure else None self.ctx = ssl._create_unverified_context() if insecure else None
def request(self, method, path, data=None, headers=None, retries=4): def request(self, method, path, data=None, headers=None):
"""发请求; 实例 TLS 间歇性抽风(SSL UNEXPECTED_EOF), 连接级错误自动重试"""
last = None
for attempt in range(retries):
req = urllib.request.Request(self.api + path, data=data, method=method, req = urllib.request.Request(self.api + path, data=data, method=method,
headers={**self.headers, **(headers or {})}) headers={**self.headers, **(headers or {})})
try: try:
@@ -72,10 +68,6 @@ class Api:
return resp.status, json.loads(body) if body else None return resp.status, json.loads(body) if body else None
except urllib.error.HTTPError as e: except urllib.error.HTTPError as e:
return e.code, json.loads(e.read() or b'{}') return e.code, json.loads(e.read() or b'{}')
except (urllib.error.URLError, ssl.SSLError, OSError) as e:
last = e
time.sleep(1.0 + attempt)
raise RuntimeError(f'Gitea 请求连续 {retries} 次失败: {last}')
def get_json(self, path): def get_json(self, path):
status, data = self.request('GET', path) status, data = self.request('GET', path)
@@ -73,19 +73,5 @@ for _name, cmd, _note in d.PRESETS:
d.build_frame(cmd) d.build_frame(cmd)
print(f'预设 {len(d.PRESETS)} 条全部可组帧: PASS') print(f'预设 {len(d.PRESETS)} 条全部可组帧: PASS')
# 8) M303/M304(SQ52205 电源实测)回归向量: 上游真机抓取帧
check('build_frame(M303)', d.build_frame('M303').hex(' ').upper(),
'D5 01 04 4D 33 30 33 E8')
check('build_frame(M304)', d.build_frame('M304').hex(' ').upper(),
'D5 01 04 4D 33 30 34 E9')
frames = d.FrameParser().feed(bytes.fromhex('B5 F0 02 2E F4 14'))
check('M303 应答帧', len(frames), 1)
check('M303 电压(0x2EF4=12020mV)', d.format_power('M303', frames[0]),
'12020 mV (12.02 V)')
frames = d.FrameParser().feed(bytes.fromhex('B5 F0 02 00 34 26'))
check('M304 应答帧', len(frames), 1)
check('M304 电流(0x0034=+52mA)', d.format_power('M304', frames[0]), '+52 mA')
check('M304 负值(FF06=-250mA 补码)', d.payload_s16_be(bytes.fromhex('FF 06')), -250)
print('RESULT:', 'FAIL:' + ','.join(fails) if fails else 'ALL PASS') print('RESULT:', 'FAIL:' + ','.join(fails) if fails else 'ALL PASS')
sys.exit(1 if fails else 0) sys.exit(1 if fails else 0)
Binary file not shown.
+1 -1
View File
@@ -12,7 +12,7 @@ PORT = sys.argv[1] if len(sys.argv) > 1 else 'COM9'
FULL = len(sys.argv) > 2 and sys.argv[2].lower() == 'full' FULL = len(sys.argv) > 2 and sys.argv[2].lower() == 'full'
BANK, REG = '60', '6E' BANK, REG = '60', '6E'
STEPS = [ # (写值, 含义, 写入后 DLPilot 应投的图, LA 预期变化 vs 基线) STEPS = [ # (写值, 含义, 写入后 HDMI-Tool 应投的图, LA 预期变化 vs 基线)
('00', '关闭交换(基准)', 'solid_0000AA / solid_000055', ('00', '关闭交换(基准)', 'solid_0000AA / solid_000055',
'D0:AA=13%/55=84%, D1:AA=72%/55=0.9% (基线)'), 'D0:AA=13%/55=84%, D1:AA=72%/55=0.9% (基线)'),
('80', 'B通道镜像', 'solid_0000AA', ('80', 'B通道镜像', 'solid_0000AA',