From 9585fae947d2e7e7ebdd7c626f629b6ee1f757f4 Mon Sep 17 00:00:00 2001 From: hulk Date: Fri, 18 Sep 2026 00:58:42 +0800 Subject: [PATCH] =?UTF-8?q?feat(DLPilot):=20=E7=94=B5=E6=BA=90=E8=87=AA?= =?UTF-8?q?=E6=A3=80=E8=A1=A5=E5=85=A8=20FPDO=20=E6=8C=A1=E4=BD=8D?= =?UTF-8?q?=E5=88=A4=E5=AE=9A;=20=E5=8A=9F=E7=8E=87=E6=A3=80=E6=B5=8B?= =?UTF-8?q?=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DLPilot/AGENTS.md | 5 ++ DLPilot/README.md | 17 +++- DLPilot/src/app_gui.py | 176 ++++++++++++++++++++++++++------------ DLPilot/src/dlpc_rs485.py | 6 +- 4 files changed, 141 insertions(+), 63 deletions(-) diff --git a/DLPilot/AGENTS.md b/DLPilot/AGENTS.md index 59cf28a..816495d 100644 --- a/DLPilot/AGENTS.md +++ b/DLPilot/AGENTS.md @@ -111,6 +111,11 @@ DLPilot/ M303=u16 mV(12V 轨窗口 11400~12600),M304=s16 补码 mA(正=正向,固件原样上总线, 上位机按有符号解析,FF 06=−250)。封装在 `payload_u16_be/payload_s16_be/format_power`, 回归向量在 selftest_rs485.py(真机抓取帧)。 +- **电源自检序列**(M301 协议→M302 挡位最大电流→M303 实测母线电压,帧驱动状态机 + `_pd_on_frame/_pd_seq`,超时 3s/步):FPDO 判定 = 电压就近挡(标准 PD 固定挡 + 5/9/12/15/20/28V,±5% 容差)+ M302 挡位电流;**12V 且 ≥3A 绿底,挡位不符红底, + 无匹配挡 ERR**。功率检测 = M303+M304 → V/mA/W。应答按 TYPE=0xF0 判成功; + CH224Q 电压配置寄存器(0x0A)只写不可读,挡位只能实测判定。 ## 5. 构建 / 发布流水线(全部在 src/ 下执行) diff --git a/DLPilot/README.md b/DLPilot/README.md index b98b4b6..eaac4e8 100644 --- a/DLPilot/README.md +++ b/DLPilot/README.md @@ -52,10 +52,11 @@ DLPilot/ ``` 左列(HDMI 源端) 右列(光机控制 RS485) ┌ 显示器选择区域 [刷新] ┐ ┌ 串口选择 + 波特率/8-N-1 参数/连接 ┐ -├ 支持的模式: 分辨率[应用][恢复] + 明细 ┐ ├ 串口数据 — 收发日志(译码+hex) ┤ -├ EDID 信息(表格) ┘ ├ 光机命令 — 快捷按钮+TEC │ -├ 测试图案 / 纯色 / 自定义 / 测试卡区域 │ 预设下拉 / 自定义 / 温度读取 │ -├ EDID 原始数据 [逐字节解析] ┘ └ 快捷滑条 — 光强/定时/TEC温度 ┘ +├ 支持的模式: 分辨率[应用][恢复] + 明细 ┐ ├ 电源检查 — PD自检/FPDO挡位/功率读数 ┤ +├ EDID 信息 ┘ ├ 串口数据 — 收发日志(译码+hex) ┤ +├ 测试图案 / 纯色 / 校准图案 / 测试卡区域 │ ├ 光机命令 — 快捷按钮+TEC │ +├ EDID 原始数据 [逐字节解析] ┘ │ 预设下拉 / 温度电流读取 / 自定义 │ +└─────────────────────────────┘ └ 快捷滑条 — 光强/定时/TEC温度 ┘ ``` 1. 左上显示器下拉选择目标屏(多屏时注意别选成主屏),"刷新"重新枚举; @@ -99,6 +100,12 @@ GUI"光机控制"区:选串口 → 连接 → 点快捷按钮或命令下拉/ 多口描述相同时自动附物理位置消歧。日志区实时显示 `→ 命令` / `← 译码结果 (原始 hex)`, 错误自动附释义(如 `err:3(I2C NACK)`)。 +"电源检查"卡:**电源自检**依序发 M301(协议)→M302(挡位最大电流)→M303(实测母线电压), +按实测电压就近判定 PD 固定挡(5/9/12/15/20/28V,容差 ±5%):**12V 挡且 ≥3A 绿底 +"FPDO: 12V@3A"**;挡位不符(如 9V 挡)**红底**显示实际挡位;不落在任何标准挡显示 +**ERR**(CH224Q 电压配置寄存器只写不可读,挡位只能靠实测判定)。**功率检测**依序发 +M303+M304,读数框显示 电压(V) / 电流(mA) / 功率(W,=电压×电流)。 + | 命令 | 功能 | 响应示例 | |---|---|---| | `M730S0` / `M730S1` | 开 / 关光机 | `ok` | @@ -106,6 +113,8 @@ GUI"光机控制"区:选串口 → 连接 → 点快捷按钮或命令下拉/ | `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 | +| `M301` | 查电源协议(CH224Q 握手结果) | ASCII:`PD` / `QC3.0` / `EPR` … | +| `M302` | 查 PD 挡位最大电流(u16 大端 **mA**,50mA/LSB 固件已换算) | 0x0BB8 = 3000 mA = 3 A | | `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` | diff --git a/DLPilot/src/app_gui.py b/DLPilot/src/app_gui.py index 7145ca3..f0c9c6d 100644 --- a/DLPilot/src/app_gui.py +++ b/DLPilot/src/app_gui.py @@ -95,12 +95,17 @@ class App(ctk.CTk): 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 = dlpc_rs485.DLPC485(self._dlpc_on_event, self._dlpc_on_status, + on_frame=self._dlpc_on_frame) self._dlpc_connected = False self._poll_job = None self._tl_order = [] # 温度查询应答顺序: 'DMD'/'LAMP' 先到先得 self._pd_seq = [] # 电源自检待执行步骤: [(命令, kind), ...] + self._pd_values = {} # 自检采集值: {'ma': 挡位电流, 'mv': 实测电压} self._pd_timer = None # 自检单步超时定时器 + self._pwr_seq = [] # 功率检测待执行步骤 + self._pwr_values = {} # 功率检测采集值: {'mv', 'ma'} + self._pwr_timer = None self._build() self._load_displays() # 主窗口按 ESC 也能关闭所有投屏(投屏窗口失焦时的兜底出口) @@ -340,8 +345,10 @@ class App(ctk.CTk): 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)) + self.chip_fpdo = ctk.CTkLabel(rowp1, textvariable=self.var_fpdo, width=150, + height=26, corner_radius=6, fg_color=CHIP_IDLE, + text_color=CHIP_IDLE_TEXT, anchor='w', padx=8) + self.chip_fpdo.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, @@ -349,8 +356,7 @@ class App(ctk.CTk): 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)) + anchor='w', padx=8).pack(side='left', padx=(8, 0)) # -- 光机命令 -- # 标题行右侧: 电流实时读数 + 查电流按钮(okR..G..B.. 应答解析) @@ -713,6 +719,13 @@ class App(ctk.CTk): # ============================================================ # 光机控制(RS485) # ============================================================ + def _dlpc_on_frame(self, fr): + """结构化应答帧(串口线程): 电源自检/功率检测序列进行中时路由给状态机""" + if self._pd_seq: + self._dlpc_log_q.put(('pdframe', fr)) + elif self._pwr_seq: + self._dlpc_log_q.put(('pwrframe', fr)) + def _dlpc_on_event(self, line): # 温度查询应答: okTL:<0.1°C>, 依发送顺序归入 DMD/LAMP 实时显示框 m = re.search(r'← okTL:(-?\d+)', line) @@ -765,10 +778,12 @@ class App(ctk.CTk): self.var_lamp.set(text) elif kind == 'current': self.var_current.set(text) - self._dlpc_log(text) - if (self._pd_seq and kind == 'event' - and text.startswith('← ')): - self._pd_on_reply(text) # 电源自检: 应答路由到状态机 + elif kind == 'pdframe': + self._pd_step_done(payload.type_code == 0xF0, payload) + elif kind == 'pwrframe': + self._pwr_step_done(payload) + else: + self._dlpc_log(text) except queue.Empty: pass connected = self.dlpc.is_connected() @@ -853,14 +868,15 @@ class App(ctk.CTk): # -- 电源检查: 自检序列 + 功率读数 -- def _pd_selftest(self): - """电源自检: 按序自动执行 M301/M302(电压检测命令待固件定义后在序列追加), - 每步应答刷新状态位: 绿=正常回包, 红=错误/超时, 灰=未检测""" + """电源自检: 依序 M301(协议) → M302(挡位最大电流) → M303(实测母线电压), + 应答帧驱动序列前进(成功=TYPE 0xF0), 结束后按 _pd_fpdo_verdict 判定 FPDO""" 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_seq = [('M301', 'pd'), ('M302', 'fpdo_ma'), ('M303', 'meas_mv')] + self._pd_values = {} self._pd_chip_reset() self._dlpc_log('电源自检: 开始') self._pd_step_next() @@ -868,72 +884,116 @@ class App(ctk.CTk): def _pd_chip_reset(self): self.chip_pd.configure(fg_color=CHIP_IDLE, text_color=CHIP_IDLE_TEXT, text='PD') + self.chip_fpdo.configure(fg_color=CHIP_IDLE, text_color=CHIP_IDLE_TEXT) self.var_fpdo.set('FPDO: —') def _pd_step_next(self): if not self._pd_seq: + self._pd_fpdo_verdict() self._dlpc_log('电源自检: 完成') return self.dlpc.send(self._pd_seq[0][0]) - self._pd_timer = self.after(3000, lambda: self._pd_step_done(False, '超时')) + self._pd_timer = self.after(3000, lambda: self._pd_step_done(False, None)) - 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=''): + def _pd_step_done(self, ok, fr): 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}') + cmd, kind = self._pd_seq.pop(0) + raw = dlpc_rs485.payload_bytes(fr) if fr is not None else b'' + if ok and kind == 'pd': + proto = raw.decode('ascii', 'replace').strip() + good = proto in ('PD', 'EPR') + self.chip_pd.configure(fg_color=CHIP_OK if good else CHIP_ERR, + text_color='#FFFFFF', text=proto or 'PD') + elif ok and kind == 'fpdo_ma' and len(raw) == 2: + self._pd_values['ma'] = dlpc_rs485.payload_u16_be(raw) + elif ok and kind == 'meas_mv' and len(raw) == 2: + self._pd_values['mv'] = dlpc_rs485.payload_u16_be(raw) if ok: self._pd_step_next() - else: # 出错即中止, 后续状态位保持未检测灰 + else: # 出错即中止 self._pd_seq.clear() - why = '超时无应答' if not line.startswith('← ') else '应答错误' - self._dlpc_log(f'电源自检: {why}, 序列中止') + if kind == 'pd': + self.chip_pd.configure(fg_color=CHIP_ERR, text_color='#FFFFFF', + text='ERR') + elif kind == 'fpdo_ma': + self.chip_fpdo.configure(fg_color=CHIP_ERR, text_color='#FFFFFF') + self.var_fpdo.set('FPDO: ERR') + self._dlpc_log('电源自检: 应答错误/超时, 序列中止') + + def _pd_fpdo_verdict(self): + """FPDO 判定: 电压挡位由 M303 实测母线电压就近判定(标准 PD 固定挡 + 5/9/12/15/20/28V, 容差 ±5%; CH224Q 0x0A 只写不可读, 挡位只能实测判定), + 电流为 M302 挡位最大电流。默认挡 12V 且 ≥3A -> 绿底; + 挡位不符(如 9V 挡) -> 红底; 实测电压不落在任何标准挡 -> ERR""" + mv = self._pd_values.get('mv') + ma = self._pd_values.get('ma') + if mv is None or ma is None: + self.chip_fpdo.configure(fg_color=CHIP_ERR, text_color='#FFFFFF') + self.var_fpdo.set('FPDO: ERR') + return + gear = next((g for g in (5, 9, 12, 15, 20, 28) + if abs(mv - g * 1000) <= g * 50), None) # ±5% = g*50 mV + a_txt = f'{ma / 1000:g}' + if gear is None: + self.chip_fpdo.configure(fg_color=CHIP_ERR, text_color='#FFFFFF') + self.var_fpdo.set('FPDO: ERR') + elif gear == 12 and ma >= 3000: + self.chip_fpdo.configure(fg_color=CHIP_OK, text_color='#FFFFFF') + self.var_fpdo.set(f'FPDO: 12V@{a_txt}A') + else: + self.chip_fpdo.configure(fg_color=CHIP_ERR, text_color='#FFFFFF') + self.var_fpdo.set(f'FPDO: {gear}V@{a_txt}A') def _pwr_query(self): - """功率检测: 命令待固件实现, 先占位(定义后在此发送并接 _pwr_update)""" + """功率检测: M303 实测电压 + M304 实测电流(按有符号) -> 换算功率""" if not self.dlpc.is_connected(): self.lbl_dlpc_st.configure(text='未连接串口', text_color='#E07A3F') return - self._dlpc_log('功率检测: 命令待固件实现') + if self._pwr_seq: + return + self._pwr_seq = [('M303', 'mv'), ('M304', 'ma')] + self._pwr_values = {} + self._dlpc_log('功率检测: 开始') + self._pwr_step_next() - def _pwr_update(self, volts, amps): - """功率读数框更新入口: 电压/电流到手后换算功率并刷新""" - self.var_pwr.set(f'电压: {volts:.1f} V 电流: {amps:.2f} A ' - f'功率: {volts * amps:.1f} W') + def _pwr_step_next(self): + if not self._pwr_seq: + v_mv = self._pwr_values.get('mv', 0) + ma = self._pwr_values.get('ma', 0) + self._pwr_update(v_mv, ma) + self._dlpc_log('功率检测: 完成') + return + self.dlpc.send(self._pwr_seq[0][0]) + self._pwr_timer = self.after(3000, lambda: self._pwr_step_done(None)) + + def _pwr_step_done(self, fr): + if not self._pwr_seq: + return + if self._pwr_timer: + self.after_cancel(self._pwr_timer) + self._pwr_timer = None + cmd, kind = self._pwr_seq.pop(0) + raw = dlpc_rs485.payload_bytes(fr) if fr is not None else b'' + if fr is None or fr.type_code != 0xF0 or len(raw) != 2: + self._pwr_seq.clear() + self.var_pwr.set('电压: ERR 电流: — 功率: —') + self._dlpc_log('功率检测: 应答错误/超时, 中止') + return + if kind == 'mv': + self._pwr_values['mv'] = dlpc_rs485.payload_u16_be(raw) + elif kind == 'ma': + self._pwr_values['ma'] = dlpc_rs485.payload_s16_be(raw) + self._pwr_step_next() + + def _pwr_update(self, v_mv, ma): + """功率读数框更新: 电压 V / 电流 mA / 功率 W""" + v = v_mv / 1000 + self.var_pwr.set(f'电压: {v:.2f} V 电流: {ma} mA ' + f'功率: {v * ma / 1000:.2f} W') def _light_entry_apply(self, _evt=None): """滑条数值输入框回车: 校验并限幅 54-800, 同步回滑条""" @@ -1000,11 +1060,13 @@ class App(ctk.CTk): self._dlpc_send(f'M8S{v}') def destroy(self): - try: # 取消待触发的轮询/自检超时回调, 避免销毁后 Tcl 报错 + try: # 取消待触发的轮询/自检/功率超时回调, 避免销毁后 Tcl 报错 if self._poll_job: self.after_cancel(self._poll_job) if self._pd_timer: self.after_cancel(self._pd_timer) + if self._pwr_timer: + self.after_cancel(self._pwr_timer) except Exception: pass try: # 两种退出路径(关窗/--seconds 自动关)都收掉串口线程 diff --git a/DLPilot/src/dlpc_rs485.py b/DLPilot/src/dlpc_rs485.py index 4dab529..2d7c689 100644 --- a/DLPilot/src/dlpc_rs485.py +++ b/DLPilot/src/dlpc_rs485.py @@ -196,8 +196,8 @@ class DLPC485: 回调在后台线程触发, GUI 侧需自行投递回主线程(见 hdmi_gui 的轮询队列)。 """ - def __init__(self, on_event, on_status): - self.on_event, self.on_status = on_event, on_status + def __init__(self, on_event, on_status, on_frame=None): + self.on_event, self.on_status, self.on_frame = on_event, on_status, on_frame self.q = queue.Queue() self._target = None # 想态 (port, baud, bytesize, parity, stopbits) self._open_key = None # 当前已打开串口的参数元组 @@ -314,6 +314,8 @@ class DLPC485: data = self.ser.read(256) if data: for fr in self.parser.feed(data): + if self.on_frame is not None: + self.on_frame(fr) # 结构化帧(二进制载荷场景用) self.on_event(f'← {describe(fr)} ' f'[{fr.raw.hex(" ").upper()}]') except OSError: