ソースを参照

docs(d2-02): 第三阶段 operate 调试页接入架构设计落盘——operate 两VM/两View 走 DebugSessionClient(心跳根治预览自断)+control 回补缓冲瓶op+真机V-012;2决策(抓图链分阶段不接/初始化串operate逐句command)+业务风险专章(借用让路停培养/崩溃回收/相机锁/use-after-free压测)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
huangjie 3 日 前
コミット
2a4da6b035

+ 139 - 0
项目文档/需求文档/specs/2026-06-24-D2-02-第三阶段-operate调试页接入-design.md

@@ -0,0 +1,139 @@
+# D2-02 · 第三阶段:operate 调试页完整接入 + 真机 V-012 · 架构设计
+
+> 立项:2026-06-24(经 brainstorming 逐项澄清,2 个实现期开放决策已与用户确认)。
+> 上游:`specs/2026-06-23-D2-02-调试页命令代理-design.md`(总设计,本文是其 §8/§14 第 5~6 步的落地)+ `specs/2026-06-24-D2-02-第二阶段-MJPEG实时预览-design.md`(预览,已并 main)。
+> 现状基线(已读源码核实,非臆测):
+> - control 后端 `DebugSessionManager.Execute`(命令分发 + 红线钳位)、`ControlHttpServer` `/debug/*` 端点、MJPEG 推流——**第一/二阶段已就绪并合 main**。
+> - operate 调试页 `HouseDebugPageViewModel`/`BufferDebugViewModel` **仍是同进程旧栈**(`HardwareAccessLayer.Instance.GetHouseGate().Acquire()` + `Serial/Cam` 直调,拆分后运行期已断)。
+> - operate `MjpegStreamClient`(预览解码)第二阶段已建好,缺的只是把 sessionId 喂进去。
+
+---
+
+## 1. 背景与本阶段定位
+
+operate/control 双进程拆分后,采集硬件(串口 `ComBin`、相机 `Camera`)整体在 control 进程,由 `HardwareAccessLayer` 单例持有。operate 进程里这个单例是空壳,**调试页所有硬件操作运行期已断**(点了没反应)。
+
+D2-02 总设计把恢复方案拆成三段:
+- **第一阶段**(已并 main):control 后端会话管理 + 命令分发 + 红线钳位,27 单测 + 真机冒烟过。
+- **第二阶段**(已并 main):MJPEG 实时预览(control 推流 + operate 解码),46 单测,待真机出图。
+- **第三阶段(本文)**:operate 调试页两个 VM + 两个 View 接入跨进程 client,补 control 缺的缓冲瓶 op,再做真机 V-012(把前两阶段欠的真机门控一并验掉)。完成后解锁 D3-04(删 operate 死串口/相机栈)。
+
+---
+
+## 2. 范围与边界(改什么 / 不改什么)
+
+### 2.1 范围内
+- operate 新建 `DebugSessionClient`(封 acquire/command/release/heartbeat + 心跳定时器 + 会话失效回调)。
+- 改 `HouseDebugPageViewModel`(舱 1-10)+ `BufferDebugViewModel`(缓冲瓶舱 11):硬件直调 → `client.Command`。
+- 改 `HouseDebugPageView.xaml.cs` + `BufferDebugView.xaml.cs`:【初始化】/【卸载】/【返回】走 acquire/release;舱页【初始化】后把 sessionId 喂给已有 MJPEG 预览。
+- 回补 control 端 `DebugSessionManager.Execute` 缺的**缓冲瓶专属 op**。
+- 真机 V-012(电机红线两轴守安全区间 + 借用让路 + 崩溃自动回收 + 预览出图 + use-after-free 压测)。
+
+### 2.2 范围外(本阶段明确不做)
+- **相机抓图链 + 写曝光不接**(用户决策一):单张抓拍 `SavePic`、水平 16 孔批量抓图 `ShuiPingZhuaPai`、清晰图层扫焦抓图 `AutoFocusPic`、`GetPicData`、`SetExposure`——这些靠 `Cam.SavePic/GrabRgb/GrabRaw/RawToRgb/SetExposure` 直调,control 后端目前**无相机抓图 op**。接通它涉及"图片在 control 进程落盘 + operate 如何取回展示",是独立一大块,作为紧接着的小阶段单独做。
+  - **不留坑**:本阶段相机只用于 MJPEG **实时预览模式**(`SetOpMode(1)`);下阶段接抓图要处理"实时↔单帧模式切换",此点登记在 §6 业务风险,本阶段先不动相机模式逻辑。
+  - 调试页上这些抓图按钮在本阶段**置灰/标注"下阶段接入"**,不让工程师点了报错(避免误判功能坏)。实时看画面调焦的需求已由 MJPEG 预览覆盖。
+- 不改 control 采集/对焦/换气/上传业务逻辑;不改 front;不加业务护栏(总设计 §2.3 决策,见 §6)。
+
+---
+
+## 3. 两个实现期决策(已与用户确认)
+
+| # | 决策 | 选定 | 理由 |
+|---|---|---|---|
+| 1 | 相机抓图链是否本阶段接 | **否,分阶段** | 实时看画面已由 MJPEG 预览覆盖;抓图存证含跨进程落盘/取回,独立成块降风险 |
+| 2 | 【初始化】那一串动作在哪端跑 | **operate 逐步发 command(总设计方案B)** | 编排逻辑+配置(孔位/清晰位/motorDelay)本就在 operate;control 零新增业务逻辑、只用已有命令分发,回归风险最低;本机回环多几趟往返无感;红线钳位已在 control 分发器逐条生效 |
+
+---
+
+## 4. operate 改造面(主体)
+
+### 4.1 新建 `DebugSessionClient`(operate/Debug/)
+封装四个 HTTP + 两个机制:
+- `Acquire(houseSn) → sessionId`;`Command(op, args) → result/code`;`Release()`(幂等);`Heartbeat()`。
+- **内置心跳定时器**:Acquire 成功后每 2~3s 自动打 `/debug/heartbeat`,Release 时停。**这是第二阶段"预览开 10s 后会话 TTL 超时自断"的根治**(第二阶段无心跳,只能 curl 手动续命)。
+- **会话失效回调**:任一 command 返回 `code=SESSION_EXPIRED`(HTTP 410)→ 触发回调,UI 弹"调试会话已超时,请重新进入调试",停心跳。
+- 复用 operate 现有 `HttpHelper`/`ControlClient.BaseUrl`(第二阶段已提 public)。
+
+### 4.2 改 `HouseDebugPageViewModel`(舱 1-10)
+- 删 `HardwareAccessLayer.Instance.GetHouseGate().Acquire()` + `_halLease` + `Serial`/`Cam` 直调。
+- 每个操作方法体:`Serial.XxxWait(args)` → `client.Command("Op", new {args})`。op 名对齐 control `Execute` 现有分发表(spec §7.1):读温压门换气、握手、灯、进/排气阀、补气/排气、电机(复位/前进/后退/到位,红线 control 端钳)、写 EEPROM。
+- **`OperationLogger.Run/Begin` 埋点原样保留 operate 侧**(谁点谁记,总设计 §8 明确);只是被包的动作从直调换成 `client.Command`。
+- `ComHouseInit` 的初始化串(握手→开灯→水平复位→移到1号孔→垂直复位→移到清晰位→读温压门换气)按方案 B 逐句改发 command;**相机 Init/SetOpMode 那一步删除**(相机归 control,预览端点自管),配置(`tLSetting.motorDelay`/孔位/清晰位)继续由 operate 持有并随 args 下发。
+- `ComHouseUnit` → `client.Release()`。
+
+### 4.3 改 `BufferDebugViewModel`(缓冲瓶舱 11)
+同 4.2 模式。缓冲瓶无相机、无电机;方法:`BufferState`(读瓶压+双温)、`Are`(补气)、`RedL`/`writeL`(灯亮度读写)、`writeE`(进气阀时间,buffer 帧)、`ShakeHands`。这些对应的 control op 部分**当前缺失**,见 §5。
+
+### 4.4 改两个 View 代码后置
+- `HouseDebugPageView.xaml.cs`:`Start_Click` → `await vm.ComHouseInit()` 内部已 acquire,把返回 sid 赋 `vm.CurrentSessionId` + client 起心跳 → `OpenVideo()`(已有,喂 sid 即出图);`End_Click`/`Return_Click` → `CloseVideo()` + `vm.ComHouseUnit()`(release + 停心跳)。**预览第二阶段已接,本阶段只补 sid 来源。**
+- `BufferDebugView.xaml.cs`:同上,无预览部分。
+- 抓图相关按钮置灰 + tooltip"相机抓图下阶段接入"。
+
+---
+
+## 5. control 回补:缓冲瓶命令登记(唯一改 control 处)
+
+现 `DebugSessionManager.Execute` 的分发表覆盖舱 1-10 的串口操作,但**缺缓冲瓶专属 op**。按 `BufferDebugViewModel` 现有方法 + `ISerialChannel` 已有底层方法逐一登记(writing-plans 阶段对照 `BufferDebugView` 全部按钮核全,不漏不臆造):
+
+| op | 底层(lease.Serial) | 返回 |
+|---|---|---|
+| `BufferState` | `BufferBottleStateWait()` | `{pressure, t1, t2}` |
+| `BufferAeration` | `BufferBottleAerationWait()` | bool |
+| `ReadLight` | `ReadLightBrightnessWait()` | int |
+| `WriteLight` | `WriteLightBrightnessWait(value)` | bool |
+| `WriteOpenIntakeTimeBuffer` | `WriteOpenIntakeTimeWait(value, isBuffer:true)` | bool |
+
+- **原则**:只在分发表新增 case,不动 `ISerialChannel`/`ComBin` 底层(底层方法已存在);control 业务逻辑零改动。
+- 这些 op 也要纳入 control 端纯逻辑单测(假 lease 验证路由到正确底层方法)。
+
+---
+
+## 6. 业务风险与影响面(时差培养箱视角 · 执行期重点盯)
+
+> 本章是用户硬要求:做的过程中结合培养箱业务特点审流程、算业务闭环与对其他功能的影响。
+
+1. **借用 = 该舱采集让路(养胚胎舱被借走的代价)**:`Acquire(OperateDebug)` 触发该舱 `MarkPause`,期间换气/控温/拍照/对焦全停。时差培养箱养活体胚胎——某舱正培养时被借去长时间调试,该舱胚胎停止培养直到 release。这是调试功能**固有语义**(合并前老系统、合并后单进程都如此),双进程仅跨进程化、行为一致。总设计 §2.3 决策不加硬护栏,靠监控页可见。**执行期落实**:真机验证监控页 `/status` 能显示该舱"调试中/借用中",确保旁人不误判该舱"坏了"也不误以为在正常培养。
+2. **崩溃/失联必须能恢复采集(安全闭环)**:operate 崩了若采集永久卡死 = 该舱胚胎永久停培养。control 看门狗 TTL 自动回收 + 预览 TCP 断快信号兜底(第一阶段已实现)。**本阶段把 operate 心跳接上才真正闭环**;真机务必验"杀 operate → 采集恢复"。
+3. **A 舱预览 + B 舱采集共用一把全进程相机锁**:互等串行(总设计 §9 认定可接受)。风险是预览持续占锁时 B 舱拍照排队。真机观察体感,卡顿再议(不在本阶段优化)。
+4. **预览中反复回收同舱的 use-after-free 窗口**(第二阶段标注的压测点):推流线程抓帧与会话回收 Dispose 相机有竞态,靠全局相机锁串行 + native HPCSE 兜底。**真机重点压测**确认不偶发崩溃;若崩,最小修=底层 `Camera.UnInit` 顺带置 `IsStart=false`(单独评估,不在本阶段擅动)。
+5. **相机模式切换坑(为下阶段登记,本阶段不踩)**:本阶段相机只用实时预览模式;下阶段接抓图需"实时↔单帧"切换,且抓图与预览共用相机。本阶段不动相机模式逻辑,避免给下阶段埋错。
+6. **红线电机安全**:水平 `[0,220000]`、垂直 Z `[0,125000]` 钳位在 control 分发器(已实现,基于回读真实物理位)。operate 端不可信。真机走位守区间,越界被 control 拒。
+
+**执行期纪律**:以上任一在实现/真机中发现与预期不符(如让路未触发暂停、回收未恢复采集、监控页不显示借用态、压测出现崩溃),立即停下与用户沟通,带业务闭环分析给方案,不自行硬改。
+
+---
+
+## 7. 测试策略
+
+### 7.1 纯逻辑单测(无需真机)
+- `DebugSessionClient`:请求体拼装(acquire/command/release/heartbeat 的 JSON 结构正确)、`SESSION_EXPIRED` 触发失效回调、心跳定时器启停、Release 幂等。用假 HTTP/可注入传输验证。
+- control 新增缓冲瓶 op:假 lease 验证 `BufferState`/`BufferAeration`/`ReadLight`/`WriteLight`/`WriteOpenIntakeTimeBuffer` 路由到正确底层方法;未知 op 返回 `BAD_OP`。
+
+### 7.2 真机门控(= V-012,Claude 自主跑,UAC 静默提权)
+1. **借串口让路时序**:acquire → 该舱采集暂停(/status 反映)→ 操作 → release → 采集恢复,不死锁/不双占。
+2. **电机红线两轴走位**:水平/垂直 reset/moveTo/forward/backward 真机走位,守安全区间,越界被拒(HTTP 400 `OUT_OF_RANGE`)。
+3. **MJPEG 真机出图**:【初始化】后 operate `<Image>` 看到该舱实时画面;画面方向正确(倒置则推流层补 Y 翻转,不改纯逻辑)。
+4. **崩溃自动回收**:杀 operate / 断预览 → control 看门狗或快信号回收 → 该舱采集恢复。
+5. **★use-after-free 压测**:预览中反复 release/超时回收同舱,确认不偶发崩溃。
+6. **缓冲瓶舱 11**:读双温/瓶压/补气/灯亮度真机走一遍。
+7. **EEPROM 写**:复用已入库 HIL 套件(`IvfTl.Hardware.HilTests`,默认零写入)守护帧长/地址。
+8. **监控页可见性**:/status 显示借用态(配合 §6.1)。
+
+---
+
+## 8. 与 D3-04 的依赖
+
+本阶段做完,operate 调试链路不再编译期引用 `ComBin`/`Camera` 死栈(改走 `DebugSessionClient`)——**除抓图链外**。抓图链下阶段接完才彻底无引用,D3-04(删死栈)在抓图阶段后解锁。本阶段先解锁"非抓图"部分的死栈引用。
+
+---
+
+## 9. 分步落地建议(writing-plans 阶段细化为 bite-sized 任务)
+
+1. control 回补缓冲瓶 op + 纯逻辑单测(最小、先行,不依赖 operate)。
+2. operate `DebugSessionClient`(HTTP + 心跳 + 失效回调)+ 纯逻辑单测。
+3. operate `HouseDebugPageViewModel` 接入 client(含 ComHouseInit 逐句 command)。
+4. operate `BufferDebugViewModel` 接入 client。
+5. operate 两个 View 后置(acquire/release/喂 sid/抓图按钮置灰)。
+6. Release 双编译 0 错 + 全量单测绿。
+7. 真机 V-012(§7.2,Claude 自主跑)。