# M1 · 合并跑通子计划 Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development 或 superpowers:executing-plans 逐 Task 执行。步骤用 checkbox(`- [ ]`)追踪。 > 父依据:`需求文档/01-架构与合并方案.md`(合并形态权威)、`需求文档/13-统一硬件访问层接口定义.md`(HAL 接口)、`00-需求总览.md` §4/§5(D1/D2/D7)。 > 里程碑:**M1 合并跑通**(见 `00-需求总览.md` §7、`需求文档/09/12`)。 **Goal:** 把 `ivf_tl_operate_2.0`(前台 WPF)与 `ivf_tl_control_2.0`(采集服务)合并为单进程:operate 登录成功后托管 control 的 `StartRun()` 为后台线程;删除 control 独立登录窗 `Window1`,全程序单登录;落地统一硬件访问层(HAL)单例,让 control 采集 / operate 调试 / 自动对焦三方改为向 HAL 借用串口与相机句柄,去掉各自直接 `new ComBin/SerialPort/Camera`,达成"调试取图 vs 后台采集"切换不报端口占用、不死锁的最小验证(01 §3 必过)。 **Architecture:** 单进程托管(D1)。主进程 = operate(`App.xaml` StartupUri=`MainWindow.xaml`)。control 各子工程(Com / CameraHelper / SerialHelper / Controller / Services / Entity / Util)作为类库并入合并解决方案,被托管入口 = `ivf_tl_Control.StartMain.StartRun()`(后台线程),不再有 control 的 UI/登录窗。三方硬件访问统一经新建的 `IvfTl.Hardware` 程序集(HAL 单例,接口签名见 13 文档 §3),HAL 内部用接口把旧 `ComBin`/`Channel`/`Camera` 包起来——代码隔离原则(01 §5):不重写老协议逻辑,只把它们封进 HAL 实现。 **Tech Stack:** C# / WPF / .NET(operate/control 目标 `net8.0-windows`,autofocustool 已 .NET 8);MVCAPI.dll(MVC2000 相机 P/Invoke);SerialPort 5E 串口协议。新增程序集 `IvfTl.Hardware`(HAL 接口 + 实现)。 --- ## 范围与约束说明(务必先读) **本地环境现实**(已实测,见 `项目文档/开发计划/2026-06-17-改造执行框架与自动对焦数据层.md`):本机 **dotnet 6**,而 operate/control 项目目标 **net8.0-windows**;无下位机、无相机、无运行中的微服务/中间件;根目录非 git 仓库。→ **本计划的代码无法本地构建或运行**。 因此本计划每个 Task 的"构建/运行验证"一律改为:**代码完成 → 登记 `项目文档/进度/待验证清单.md` 对应 V 项(依赖 net8 工具链 / 真机),到 M7 集中测**。本计划**不写**任何假装能本地 `dotnet build` / 运行的命令。能在本机完成并核对的只有:源码改动、接口/类型签名一致性、调用点替换是否齐全(用 grep/codegraph 核对残留 `new ComBin`/`new Camera`/`new SerialPort`)。 **HAL 接口骨架来源**:`13-统一硬件访问层接口定义.md` §3 已给出**只签名**的接口(`IHardwareAccessLayer / ISerialChannel / ICamera / IHouseHandle / IHouseGate / IHardwareLease / ICameraGate` + `HouseDeviceInfo / HardwareUser / DoorState`)。本计划负责:① 把这套接口落成 `.cs` 文件;② 写出以旧代码为包装的实现;③ 改三方调用点。**接口签名以 13 文档为准,本计划不重新发明签名。** **代码隔离原则(01 §5,全程遵守)**: - 不顺手重构 `ComBin`/`Channel`/`Camera`/`HouseBin` 的老协议逻辑;HAL 实现内部**调用**旧类,不改旧类内部。 - 死代码先不删(`Window1` 的注释块、调试页旧 `ClosePort` 等),合并稳定后再清理;本计划仅"停用 + 隔离"。 - **`ivf_tl_control_2.0` 整目录退役(删除)= 合并收尾动作,不在本计划执行**。解锁判据(01 §5.5,全满足才删):① 本 M1 真机验证通过(V-011~V-016 等) ② operate 现以 3 条 ProjectReference 引用的 `ivf_tl_Control`/`IvfTl.Hardware`/`IvfTl.AutoFocus` 物理并入合并解决方案 ③ 消解 operate/control 同名程序集(`ivf_tl_Entity`/`ivf_tl_Services`)冲突 ④ 改完引用编译 0 error。届时另起清理 Task。 - 关键 hack 单独记录(control `StartRun` 里 MessageBox + SendMessage 点 Yes 的 hack、明文账号密码)。 - 改前用 codegraph / grep 查调用面(C# 符号提取有限,必要时直接读调用方)。 **Git 说明**:根目录非 git 仓库,"Commit"步骤无法执行。每个 Task 末尾检查点用"保存并核对文件 + grep 残留"代替。 --- ## 已 codegraph / 源码核对的关键事实(步骤据此编写) | 事实 | 位置(精确) | |------|------| | operate 启动入口:StartupUri=MainWindow.xaml | `ivf_tl_operate_2.0/ivf_tl_Operate/App.xaml`(`OnStartup` 仅做 Mutex+异常注册,`App.xaml.cs:30-43`) | | operate 登录 + 主页加载点(StartRun 要挂这里) | `ivf_tl_operate_2.0/ivf_tl_Operate/MainWindow.xaml.cs:44-59`(`MainWindow_Loaded`:`new LoginWindow(this).ShowDialog()` → 成功后 `LoadPage(mainPageView)`) | | operate 登录窗(保留,全程序唯一登录) | `ivf_tl_operate_2.0/ivf_tl_Operate/Windows/LoginWindow.xaml.cs:31`(`LoginWindow(Window w)`) | | control 被托管入口 | `ivf_tl_control_2.0/ivf_tl_Control/StartMain.cs:34 StartRun()` → `InitTL`(66) → `InitHouse`(183) → `AppData.StartAsync().Wait()`(52) | | control 登录窗(删除目标)+ 其内 StartRun 调用 | `ivf_tl_control_2.0/ivf_tl_ControlTest/Window1.xaml.cs:25 Window1()`;`Window1_Loaded1:51` 内 `Login()` + `new StartMain().StartRun()`(77-78);`Button_Click:174` 也调 StartRun | | StartRun 内的 MessageBox+SendMessage hack | `StartMain.cs:97-141`(`InitTL`:模块数≠11 时弹 `MessageBox.Show(...)`,3 秒后 `FindWindow/EnumChildWindows/SendMessage(BM_CLICK)` 自动点 Yes) | | 两个 AppData 单例命名冲突 | control `ivf_tl_Control.AppData`(`AppData.cs:36` Lazy 单例,`Login()`/`StartAsync()`/`LogService`);operate `ivf_tl_Operate.AppData`(独立单例) | | operate 调试硬件 new 点 | `ivf_tl_operate_2.0/ivf_tl_Operate/ViewModel/HouseDebugPageViewModel.cs:35 ComBin comBin`、`:36 Camera camera`、`:213 new ComBin(houseSn, housePort)`、`:230 new Camera(CCDId, w, h, exposure)`(类型 `ivf_tl_Entity.ComEntitys.ComBin` / `ivf_tl_Entity.CameraEntitys.Camera`) | | control 采集硬件 new 点 | `ivf_tl_control_2.0/ivf_tl_Com/HouseBin.cs:159 new ComBin`、`:450 / :844 new Camera`;`SerialBin.cs:78 new Camera`、`:190 / :364 new ComBin`;`ivf_tl_SerialHelper/Channel.cs:128 new SerialPort()` | | 自动对焦蓝本(HAL 接口以它为蓝本) | `autofocustool/Serial/Protocol.cs`、`Serial/SerialMotor.cs`、`Serial/HouseMotor.cs`、`Camera/Camera.cs`(M1 不接入对焦业务,仅留接口位) | > **注**:M1 只做"合并 + HAL 地基 + 调试/采集两方接入 + 最小互斥验证"。自动对焦三方接入(`HardwareUser.AutoFocus`)属 M2,本计划只在 HAL 接口/枚举里留位,不写对焦借用逻辑。 --- ## 待验证清单登记项(本计划新增/对应,统一记入 `项目文档/进度/待验证清单.md`) | V 项 | 待验证内容 | 依赖 | 出处 | |------|-----------|------|------| | **V-011** | 单进程启动:operate 前台 + control `StartRun()` 后台线程跑通,**无 control 登录窗**、无第二个 UI 窗口;control 初始化日志正常输出 | net8 工具链 + 真机(下位机 11 模块) | M1-01 / M1-02(01 §6) | | **V-012** | 全程序单登录:只弹 operate `LoginWindow`,登录态同时满足 operate 与 control(control 的 `AppData.Login` 用同一账号成功) | net8 + 真机/可达网关 | M1-02 | | **V-013** | 硬件互斥最小验证(01 §3 M1 必过):调试页"取一帧图"与 control 后台采集切换,不报端口占用、不死锁;前台借用时该舱采集暂停、归还后恢复采集节拍 | net8 + 真机 | M1-03(13 §⑤ V6) | | V-014 | HAL `ScanDevices` 设备发现稳定:相机 index/COM 口开机漂移 + CCDSN→index 配对正确(禁止写死) | 真机多次重启 | M1-03(13 §⑤ V7) | | V-015 | StartRun 去 hack 后行为:模块数≠11 的"继续运行"判定改为静默/日志后,初始化分支仍正确(不再依赖自动点 Yes) | 真机(模块数异常场景) | M1-01 | | V-016 | 统一相机锁 `ICameraGate`:合并后 control 采集 `GrabRgb` 与 operate 调试 `SetExposure/GrabStable` 并发不段错误(替代三套各自 static 锁) | net8 + 真机 | M1-03(13 §⑤ V5) | > 字节序/写 EEPROM/读超时等(13 §⑤ V1/V2/V3/V8)属 M2 对焦链验证,M1 不展开,但 HAL 实现里照 13 接口注释预留属性(`MoveReadTimeoutMs` 等),登记时标注"M2 验"。 --- ## Task M1-01 · operate 单进程托管 control 的 StartRun() 为后台线程 **出口验收**:合并解决方案中,operate 登录成功后在后台线程调用 `ivf_tl_Control.StartMain.StartRun()`,control 采集逻辑启动且不再弹任何 control 窗口;`StartRun` 内的弹窗自动点 Yes hack 改为静默/日志。→ 登记 V-011、V-015。 - [ ] **步骤 1 — 把 control 子工程并入合并解决方案(作为类库)。** 改什么:在合并解决方案里引用 control 的类库工程 `ivf_tl_Control`(含 `StartMain`/`AppData`)、`ivf_tl_Com`、`ivf_tl_CameraHelper`、`ivf_tl_SerialHelper`、`ivf_tl_Controller`、`ivf_tl_ServicesImpl`、`ivf_tl_Entity`(control 侧)、`ivf_tl_UtilHelper`。**不引用** `ivf_tl_ControlTest`(启动壳,含 `Window1`,见 M1-02)。 为何:01 §2 落地步骤 1——control 各子工程作为类库并入;ControlTest 是独立 UI 壳,合并后不需要。 隔离:仅加引用,不动 control 各类库内部代码。 ⚠ 命名冲突:control 有 `ivf_tl_Control.AppData`(`AppData.cs:36`),operate 有 `ivf_tl_Operate.AppData`,**命名空间不同不会编译冲突**,但语义是两个独立单例(01 §2 步骤 3)。M1 阶段**保持两个单例并存**(control 的归 control 后台用,operate 的归前台用),不强行合并为统一上下文——合并上下文是更大的重构,留待后续;本步只确认两者命名空间隔离、无类型名直接碰撞。 登记:构建依赖 net8 → 记 V-011("合并解决方案可编译"作为 V-011 前置)。 - [ ] **步骤 2 — 去掉 `StartRun()` 内的 MessageBox + SendMessage 自动点 Yes hack,改静默/日志。** 文件:`ivf_tl_control_2.0/ivf_tl_Control/StartMain.cs:97-141`(`InitTL` 内 `modelCount != 11` 分支)。 改什么:把 `MessageBox.Show(...)` + `Task.Delay(3000)` + `FindWindow/EnumChildWindows/GetClassName/GetWindowText/SendMessage(BM_CLICK)` 这段"弹窗再用 user32 自动点 Yes"的 hack 替换为:直接 `AppData.LogService.TLLog($"检测到 {modelCount} 个模块(期望 11),按配置策略继续/中止", LogEnum.RunRecord)`,并按既有 `errora="结束"` 语义保留"是否继续"的可配置开关(默认继续,配置项控制)。`FindWindow/SendMessage` 等 `[DllImport]` 声明(`StartMain.cs:321-332`)可暂留(死代码先不删,01 §5),仅停止调用。 为何:合并后是单进程无人值守前台,不能弹模态框;原 hack 是"自己弹框再用 Win32 替用户点 Yes"的临时做法(01 §5 要求单独记录此 hack)。改静默后初始化在后台线程跑,不阻塞 UI。 隔离:只改这一分支的交互方式,不动 `InitTL` 其余的 `SerialBin.UpdataCamera()/Start()`、`UpdateTLInfoController` 等采集初始化逻辑。 登记:模块数异常场景需真机验证 → V-015。 - [ ] **步骤 3 — 在 operate 登录成功后用后台线程调用 `StartRun()`。** 文件:`ivf_tl_operate_2.0/ivf_tl_Operate/MainWindow.xaml.cs:44-59`(`MainWindow_Loaded`)。 改什么:在 `new LoginWindow(this).ShowDialog()` 返回 `true`(登录成功)、`LoadPage(mainPageView)` 之后,新增后台启动: ```csharp System.Threading.Tasks.Task.Run(() => { var startMain = new ivf_tl_Control.StartMain(); // 已并入的 control 类库 string err = startMain.StartRun(); // 阻塞跑 InitTL→InitHouse→StartAsync if (!string.IsNullOrEmpty(err)) ivf_tl_Services.Log4netHelper.WriteLog($"control 后台启动失败:{err}"); }); ``` 为何:01 §2 步骤 2——operate 启动、登录成功后调用 `StartMain.StartRun()`。`StartRun` 内部本就阻塞(`AppData.StartAsync().Wait()`,`StartMain.cs:52`),必须放后台线程,避免卡 UI 线程。复刻原 `Window1_Loaded1`(`Window1.xaml.cs:60-87`)里"Task.Run 内调 StartRun + 失败记日志"的形态,但**去掉 `Environment.Exit(0)`**(合并后 control 启动失败不应杀整个前台进程,仅记录/降级,配合 R5 自愈策略——M1 先记日志,自愈属后续)。 隔离:仅在 operate 既有登录成功回调里追加一段后台启动;不改 LoginWindow、不改 control StartRun 主体(步骤 2 已单独处理 hack)。 关键 hack 记录:control 端 `AppData.Login(account,password)` 与 operate 登录是两套账号体系——M1-02 处理"单登录"如何驱动 control 的 Login(见 M1-02 步骤 2)。 登记:后台线程跑通 + control 初始化日志 → V-011。 - [ ] **检查点 M1-01**:grep 确认 operate `MainWindow_Loaded` 已含 `StartMain().StartRun()` 后台调用;`StartMain.cs:97-141` 已无 `MessageBox.Show`/`SendMessage` 调用(声明可留)。保存并核对文件。构建/运行 → 待 net8+真机(V-011/V-015)。 --- ## Task M1-02 · 去 control 登录窗 Window1、全程序单登录 **出口验收**:合并程序启动只出现 operate `LoginWindow`,无 `Window1`;登录态同时满足前台 operate 与后台 control(control `AppData.Login` 用同一账号登录成功)。→ 登记 V-012。 - [ ] **步骤 1 — 排除 `ivf_tl_ControlTest`(Window1 所在工程)出合并启动路径。** 文件:`ivf_tl_control_2.0/ivf_tl_ControlTest/Window1.xaml`、`Window1.xaml.cs`(启动壳)。 改什么:合并解决方案**不把 `ivf_tl_ControlTest` 作为启动项**,且 operate 不引用该工程(M1-01 步骤 1 已约定不引用)。`Window1` 的 App.config 启动逻辑由 operate 启动流程接管。`Window1.xaml.cs` 文件本身**先不删**(死代码先不删,01 §5),仅从合并产物的启动路径剔除。 为何:01 §2——删除 control 的 `Window1` 登录窗与独立 UI,全程序单一登录入口(operate `LoginWindow`)。`Window1` 是 control 唯一的窗口入口(codegraph:`StartRun` 仅 2 处调用均在 `Window1.xaml.cs`),剔除它即去掉 control 全部独立 UI。 隔离:不改 Window1 内部;只在解决方案/引用层面剔除。 登记:启动无 Window1 → V-011/V-012(真机/运行验)。 - [ ] **步骤 2 — 把 Window1 的登录/初始化逻辑并入 operate 登录后的初始化流程。** 文件:来源 `ivf_tl_control_2.0/ivf_tl_ControlTest/Window1.xaml.cs:51-87`(`Window1_Loaded1`);落点 `ivf_tl_operate_2.0/ivf_tl_Operate/MainWindow.xaml.cs`(M1-01 步骤 3 新增的后台块内)。 改什么:把 `Window1_Loaded1` 里 control 启动所必需的前置—— ① `ivf_tl_Control.AppData.Instance.Login(account, password)`(control 端登录,`Window1.xaml.cs:68`); ② `PathHelper.pan = cacheDisk` / `AppData.Instance.LogService.Pan = cacheDisk`(缓存盘,`:74-75`); —— 搬进 operate 后台启动块,在调用 `StartRun()` 之前执行。account/password 取自 operate 登录成功后的凭据(与 operate `LoginWindow` 同一账号,见 01 §2 步骤 4:control 原读 App.config 账号的逻辑并入 operate 登录后流程)。cacheDisk 仍可从 control 侧 config 读(M1 不做配置统一,那是 M5)。 为何:01 §2 步骤 4——control 原 `Window1` 的登录/初始化(读 App.config 账号)逻辑并入 operate 登录后的初始化流程,实现"单登录驱动两端"。 ⚠ 关键差异记录:operate 与 control 是**两套账号/登录服务**。M1 的"单登录"指 UI 上只有一个登录窗;底层 control 仍需用账号调它自己的 `AppData.Login`。若两端账号体系不一致(V-012 要验),M1 先用 operate 登录得到的账号同时喂给 control `Login`;若 control 端登录失败,按 M1-01 步骤 3 的"记日志不退进程"处理。**不去掉 `Window1` 里 `Environment.Exit(0)` 的等价物**——合并后不杀进程。 隔离:搬逻辑、不改 `AppData.Login` 内部实现。 登记:单登录 + 两端登录态 → V-012。 - [ ] **检查点 M1-02**:grep 确认合并启动路径无 `Window1`/`ivf_tl_ControlTest` 引用;operate 后台启动块已含 control `AppData.Instance.Login(...)` + `PathHelper.pan` 赋值,且在 `StartRun()` 之前。保存并核对。运行验 → V-012。 --- ## Task M1-03 · 落地统一硬件访问层(HAL)单例,三方借用、去直接 new **出口验收**:新建 `IvfTl.Hardware` 程序集落地 13 文档 §3 接口 + 以旧代码为包装的实现;HAL 单例在 operate 启动时初始化一次;operate 调试(`HouseDebugPageViewModel`)与 control 采集(`HouseBin`/`SerialBin`)改为向 HAL 借用 `ISerialChannel`/`ICamera`,删除各自 `new ComBin`/`new Camera`/`new SerialPort`;调试取图 vs 采集切换不冲突。→ 登记 V-013/V-014/V-016。 - [ ] **步骤 1 — 新建 `IvfTl.Hardware` 程序集,落地 13 文档 §3 接口(只签名)。** 文件(新建):`IvfTl.Hardware/IHardwareAccessLayer.cs`、`ISerialChannel.cs`、`ICamera.cs`、`Concurrency.cs`(含 `IHouseGate`/`IHardwareLease`/`ICameraGate`/`HardwareUser`)、`Models.cs`(`HouseDeviceInfo`/`IHouseHandle`/`DoorState`)。命名空间 `IvfTl.Hardware`(13 §3 建议)。 改什么:逐字落地 13 文档 §3.1–3.4 的接口签名(`IHardwareAccessLayer.ScanDevices/GetSerial/GetCamera/GetHouse/ShutdownAll`;`ISerialChannel` 的 `Open/Close/SendWait/ShakeHandsWait/各电机 Wait/温压门/IO/LED` + `MoveReadTimeoutMs/QueryReadTimeoutMs/MotorSettleMs`;`ICamera` 的 `Init/SetOpMode/SetExposure/SetGain/GrabRgb/GetFrameBuffer/GrabStable/StartPreview/StopPreview`;并发组三接口 + 枚举)。 为何:13 文档已是权威接口骨架,HAL 必须以它为契约,三方才能统一借用。 隔离:纯新增接口文件,不依赖任何旧工程;签名以 13 文档为准,不改签名。 登记:编译依赖 net8 → 随 V-011 前置。 - [ ] **步骤 2 — 写 HAL 实现:用接口把旧 `Channel`/`ComBin`/`Camera` 包起来(不改旧逻辑)。** 文件(新建):`IvfTl.Hardware/Impl/HardwareAccessLayer.cs`(单例,内部 `portName→ISerialChannel`、`cameraIndex→ICamera` 字典)、`SerialChannelImpl.cs`、`CameraImpl.cs`、`CameraGateImpl.cs`、`HouseGateImpl.cs`。 改什么(代码隔离的核心): - `SerialChannelImpl` **内部持有并调用** control 旧串口栈(`ivf_tl_SerialHelper/Channel.cs` 的 `SerialPort` + `ivf_tl_Com/SerialBin`/`ComBin` 的同步 `XxxWait` 方法),把 `ISerialChannel.HorizontalMoveToWait/TemperatureWait/...` 转发到旧 `ComBin` 的对应方法。**不重写**协议/收帧逻辑(保留 `Commander`/`Analysiser` 那套)。 - `CameraImpl` **内部持有** 旧 `Camera`(`ivf_tl_CameraHelper/Camera.cs`),`ICamera.Init/SetExposure/GrabRgb/...` 转发到旧 `Camera.InitCamera/SetPartOfCapInfo/GetRgbData/Usb2Start...`;`GrabStable` 用旧抓帧 + 13 §3.5 的"丢残留帧 + 到位延时 + 重试"语义包装。 - `CameraGateImpl` 提供**全进程唯一**相机锁(`Invoke`),所有 `CameraImpl` 的 native 调用经它串行化——**替代** control/operate/af 三套各自的 static `locker`/`Locker`(13 §②/§3.5:三把不互斥的锁合并为一把)。 - `HardwareAccessLayer` 单例:`GetSerial(portName)` 惰性建唯一 `SerialChannelImpl` 并缓存(重复调用返回同一实例 → 杜绝同口重复 `SerialPort.Open()`);`GetCamera(index)` 同理;`ScanDevices` 复刻 af `DeviceScanner.ScanAll`(或 control `SerialBin.UpdataCamera` 的枚举)做 index/COM/CCDSN 配对。 为何:01 §3/§5 + 13 §②——合并后三套硬件代码争用同一物理资源,HAL 必须"每 COM 口/每相机唯一持有者 + 跨调用者统一锁 + 按舱借用"。用接口把老代码包进实现,是代码隔离原则的直接落地。 隔离:HAL 实现**调用**旧类,**不修改**旧类内部;旧类暂保留其自身的 static 锁字段(死代码先不删),但所有经 HAL 的 native 调用一律走 `ICameraGate`。 登记:统一锁并发安全 → V-016;设备发现稳定 → V-014。 - [ ] **步骤 3 — HAL 单例在 operate 启动时初始化一次。** 文件:`ivf_tl_operate_2.0/ivf_tl_Operate/MainWindow.xaml.cs`(M1-01 步骤 3 后台启动块内,`StartRun()` 之前)或 operate `App.OnStartup`(`App.xaml.cs:30`)。 改什么:在 control `StartRun()` 之前 `HardwareAccessLayer.Instance.ScanDevices()` 完成设备发现,确保后台采集与前台调试拿到的是同一组句柄。 为何:13 §3.1——"合并后由宿主(operate 主进程)在启动时初始化一次";必须早于 control 采集启动,否则采集线程仍会自己枚举/打开。 隔离:仅新增一行初始化调用。 登记:随 V-011/V-014。 - [ ] **步骤 4 — control 采集改为向 HAL 借用,删除采集端直接 new。** 文件:`ivf_tl_control_2.0/ivf_tl_Com/HouseBin.cs:159`(`new ComBin`)、`:450`/`:844`(`new Camera`);`ivf_tl_Com/SerialBin.cs:78`(`new Camera`)、`:190`/`:364`(`new ComBin`)。 改什么:把这些 `new` 替换为从 HAL 取:`HardwareAccessLayer.Instance.GetSerial(port)` / `GetCamera(index)`;采集每舱节拍按 13 §④ 用 `using var lease = houseGate.Acquire(HardwareUser.ControlCapture, timeoutMs)`,**拿不到(前台正在调试)则跳过本轮该舱**,下一节拍重试(低优先级让路);响应 `PauseCapture/ResumeCapture`。原采集流程(温压门/电机/补排气/按曝光抓帧)改调 `lease.Serial.*` / `lease.Camera.*`。 为何:01 §3 + 13 §④——control 采集是后台节拍、优先级低于前台调试/对焦;这是 V-013 互斥验证的采集侧。 隔离:替换 new 与抓取方式,不改采集业务节拍/算法;旧 `ComBin`/`Camera` 类保留(被 HAL 包装)。 ⚠ 范围控制:`SerialBin.UpdataCamera/Start`(开机枚举,`SerialBin.cs:78/190` 在 `StartRun→InitTL` 阶段)若已被 HAL `ScanDevices` 取代,则改为复用 HAL 发现结果;若时序上 HAL 尚未就绪,M1 可保留枚举但**枚举后不再独立持有句柄**,统一交还 HAL。此处时序以步骤 3 的初始化顺序为准。 登记:采集侧借用 + 互斥让路 → V-013。 - [ ] **步骤 5 — operate 调试改为向 HAL 借用,删除调试端直接 new。** 文件:`ivf_tl_operate_2.0/ivf_tl_Operate/ViewModel/HouseDebugPageViewModel.cs:35-36`(字段 `ComBin comBin`/`Camera camera`)、`:213`(`new ComBin(currentHouse.houseSn, currentHouse.housePort)`)、`:230`(`new Camera(CurrentCCDId, ccdWidth, ccdHeight, ccdExposure)`)。 改什么:按 13 §④ operate 调试接入——打开调试页:`var house = HAL.GetHouse(houseSn)`,`using var lease = houseGate.Acquire(HardwareUser.OperateDebug)`(HAL 自动暂停 control 对该舱采集);调试动作改调 `lease.Serial.HorizontalMoveToWait/...`、`lease.Camera.SetExposure/GrabStable/StartPreview`(替代现状 VM 自建 `comBin`/`camera` 与 `camera.SetPartOfCapInfo`);关闭调试页 `lease.Dispose()` 归还,HAL 恢复采集——**删掉调试页自己的 ClosePort/UnInit**(统一关闭路径,杜绝句柄泄漏;旧关闭方法体先留作死代码不删,仅不再调用)。 为何:13 §④ + 01 §3——调试是前台高优先级借用,借用即暂停该舱采集;这是 V-013 互斥验证的调试侧,也是 01 §3 "调试取一帧图 vs 后台采集切换不冲突"的最小验证主场景。 隔离:替换硬件获取与调用入口,不重写调试页 UI 绑定逻辑;曝光入口 `SetExposure`(VM)改为转 `lease.Camera.SetExposure`。 登记:调试取图 vs 采集切换不冲突、暂停/恢复时序 → V-013(01 §3 M1 必过)。 - [ ] **检查点 M1-03**:grep 全仓确认 `new ComBin` / `new Camera` / `new SerialPort` 在 `HouseBin.cs`/`SerialBin.cs`/`HouseDebugPageViewModel.cs` 内**仅剩 HAL 实现工程**里的包装调用,业务侧已替换为 `HardwareAccessLayer.Instance.Get*` / `lease.*`。确认调试页旧 `ClosePort/UnInit` 不再被调用。保存并核对。互斥最小验证 → 待 net8+真机(V-013/V-014/V-016)。 --- ## 风险与回退(排查顺序) > 合并后启动/硬件/登录三类故障,按以下顺序定位(先看日志,再二分隔离)。 **A. 合并后启动失败(进程起不来 / 后台 control 不动)** 1. 看 operate `Log4netHelper` 日志 + control `AppData.LogService.TLLog`(`RunRecord/RunException`):定位卡在 `InitTL`(相机/串口枚举)还是 `InitHouse` 还是 `StartAsync`。 2. 二分:临时注释 M1-01 步骤 3 的 `StartRun()` 后台块——若 operate 前台正常起,说明问题在 control 托管侧(继续看 control 日志);若前台仍崩,问题在 operate 合并引用/HAL 初始化。 3. 检查命名冲突:两个 `AppData` 是否被误用(确认前台用 `ivf_tl_Operate.AppData`、后台用 `ivf_tl_Control.AppData`)。 4. 回退:M1-01/02/03 三 Task 解耦,可逐 Task 回退——先回退 M1-03(HAL)保留"合并+单登录",确认合并骨架本身可起,再单独排 HAL。 **B. 硬件争用(端口占用 / 相机句柄崩 / 死锁)** 1. 端口占用:grep 确认无残留 `new ComBin/new SerialPort`(检查点 M1-03);确认 `GetSerial(port)` 对同口返回同一实例(HAL 字典命中)。 2. 相机段错误:确认所有 native 调用经唯一 `ICameraGate.Invoke`,旧三套 static 锁未被绕过(V-016)。 3. 死锁:检查 `IHouseGate.Acquire` 的超时与"采集拿不到即跳过本轮"是否生效(采集不得无限等前台);调试 `lease.Dispose()` 是否在所有路径(含异常)都归还——用 `using` 保证。 4. 回退:HAL 互斥若不稳,临时令 control 采集对"正在调试的舱"硬跳过(粗粒度暂停),保 V-013 最小过,再细化优先级。 **C. 登录流程断(登录后黑屏 / control 未登录 / 重复登录)** 1. 确认只挂了 operate `LoginWindow`、`Window1` 已剔除(M1-02 步骤 1)。 2. control 未登录:看 control `AppData.Login` 返回值日志(M1-02 步骤 2)——账号是否正确透传、网关是否可达。 3. 登录后无主页:确认 `MainWindow_Loaded` 里 `LoadPage(mainPageView)` 仍在 StartRun 后台块**之前/并行**执行,未被后台阻塞(StartRun 在独立 `Task.Run`,不应卡 UI)。 4. 回退:control 登录失败时按 M1-01 步骤 3 "记日志不退进程"降级——前台可用、后台采集缺失,不影响登录验证(V-012 与 V-011 解耦)。 --- ## 自检(编写完成时核对) - [x] 无占位符(无 TBD / "类似上面" / "同 control" 之外的具名引用):所有"同 control"均指明具体文件行号来源。 - [x] 文件路径精确到行:operate `MainWindow.xaml.cs:44-59`、`HouseDebugPageViewModel.cs:35-36/213/230`;control `StartMain.cs:34/66/97-141`、`Window1.xaml.cs:25/51-87/77`、`HouseBin.cs:159/450/844`、`SerialBin.cs:78/190/364`、`Channel.cs:128`、`AppData.cs:36`。 - [x] 类名/方法名 codegraph + 源码核对真实存在:`StartMain.StartRun/InitTL/InitHouse`、`Window1.Window1_Loaded1`、`AppData.Instance/Login/StartAsync`、`ComBin`、`Camera`、`MainWindow_Loaded`、`LoginWindow(Window)`、HAL 接口名(13 §3 原文)。 - [x] 体现代码隔离:HAL 用接口包装旧 `Channel/ComBin/Camera`,不改旧逻辑;死代码先不删;hack 单独记录。 - [x] 每 Task 标注真机/构建验证点(V-011~V-016),且明确"本地不可构建,登记待验证清单到 M7 集中测"。 - [x] 含风险与回退排查顺序(启动/硬件/登录三类)。