2026-06-17-M2-本地自动对焦子计划.md 35 KB

M2 · 本地自动对焦子计划 Implementation Plan

For agentic workers: REQUIRED SUB-SKILL:用 superpowers:subagent-driven-developmentsuperpowers:executing-plans 逐 Task 执行;步骤用 checkbox(- [ ])追踪。 父依据:需求文档/03-自动对焦集成方案.md(算法移植范围/四步标定/两场景/协议统一/EEPROM 回写 D9)、需求文档/12-工作计划表与自动对焦数据设计.md §2(对焦找 1 清晰层拍 N 层、两套参数、§2.4 层位置公式、§2.5 就近优先、§2.6 对焦后手调、§2.7 标定存储)、需求文档/13-统一硬件访问层接口定义.md(M1 已落地 HAL;对焦端接入 §④)、进度/文档源码审核报告.md(机旁 native 打分 HouseBin.cs:2446、V-007)。 里程碑:M2 本地自动对焦(覆盖工作计划表 M2-01 ~ M2-07 + M2-01b)。

Goal: 把 autofocustool 的四步标定算法(Sharpness / WellDetector / ExposureMeter / CalibrationEngine + 依赖)移植进合并端,接在 M1 落地的 HAL 之上(用 ISerialChannel/ICamera 取代 autofocustool 自带的 HouseMotor/SerialMotor/Camera);把 HouseBin.StartAutoFocus()ivf_tl_control_2.0/ivf_tl_Com/HouseBin.cs:1359)从"先服务器后本地 DB"改为调本地 CalibrationEngine 算出 FocusZ;按 §2.4 公式 + §2.5 就近优先配置生成各拍摄层位置;标定结果写本地 calibration.json 并镜像入 house_autofocus_calibration(scene 区分);提供场景 A 调试页一键标定、场景 B 放皿后自动对焦接既有触发、对焦后界面手调拍摄层并持久化 well 级。

Architecture: 算法接在统一硬件访问层之上、control 业务流之内(03 §2),与采集/调试共用同一硬件持有者,不另开串口/相机。新建对焦业务程序集(建议 IvfTl.AutoFocus,见 M2-01)封装移植后的算法 + 配置解析 + 层位置计算 + 标定结果落库/落 JSON。对焦借用走 HardwareAccessLayer.Instance.GetHouseGate(houseSn).Acquire(HardwareUser.AutoFocus)(前台优先于 ControlCapture,13 §3.4/§④)。库内镜像走 control 端既有 SqlSugar 直连(ivf_tl_ServicesImpl/DBServices/DBServiceImplNo.csSqlSugarScope Db),不经 Java。calPhotoPosition(D5 方案 A)保留属 M3 微服务侧;M2 在 control 端只负责把本地 FocusZ 灌进既有"各层拍照位置"机制。

Tech Stack: C# / .NET(合并端目标 net8.0-windows,autofocustool 已 .NET 8);纯 C# 图像算法(无 OpenCV);HAL(IvfTl.Hardware,M1 产出);SqlSugar(control DB 直连);System.Text.Json(calibration.json,IncludeFields=true)。


范围与约束说明(务必先读)

本地环境现实(与 M1 一致,见 项目文档/开发计划/2026-06-17-改造执行框架与自动对焦数据层.md2026-06-17-M1-合并跑通子计划.md):本机 dotnet 6,合并端项目目标 net8.0-windows;无下位机、无相机、无运行中微服务/中间件;根目录非 git 仓库。→ 本计划的代码无法本地构建或运行

因此每个 Task 的"构建/运行验证"一律改为:代码完成 → 登记 项目文档/进度/待验证清单.md 对应 V 项 → 到 M7 集中测。本计划不写任何假装能本地 dotnet build/运行的命令。能在本机完成并核对的只有:源码改动、签名/类型一致性、调用点替换是否齐全(grep/codegraph 核对残留 new HouseMotor/new SerialMotor/new Camera/GetAutoFocusServiceEvent)。

M2 编译依赖 M1-00(程序集统一)尚未完成:M1 合并代码因 M1-00(程序集/命名空间统一、合并解决方案可编译)未做,当前整体不能编译(见 M1 计划,"合并解决方案可编译"作为 V-011 前置)。M2 代码写在合并端之上,同样要等 M1-00 完成 + net8 工具链才能整体编译。本计划所有"代码完成"判定 = 源码改动齐全 + grep/codegraph 核对,不含编译通过;整体编译/运行统一并入 M1-00→net8→M7 测试链。

唯一可纯逻辑单测的部分是 M2-02(拍摄层位置计算 + 配置解析)。它被设计为不依赖硬件/数据库/net8 项目的独立纯函数类,写真实单元测试(给定值→期望各层位置;配置三场景)。即便本地 net6 跑不了 net8 项目的测试宿主,逻辑本身仍应放在可独立编译/可移植到 net6 测试项目验证的位置(纯 C#,无 WPF/HAL/SqlSugar 引用)。

代码隔离原则(01 §5,全程遵守)

  • 移植算法类时不重构其内部逻辑;只把它对硬件的依赖(HouseMotor/SerialMotor/Camera)换成 HAL 接口(ISerialChannel/ICamera),认准 03 §1 踩坑修复(÷mean 一次、丢残留帧、到位延时、按命令分超时),逐字保留
  • 不移植测试外壳SelfTest/SmokeTest/Calibrate/CalibTest/ToPng/CalibWindow/MainWindow.*(03 §1 明列)。这些是 autofocustool 的 GUI/命令行驱动壳,合并端由 control 业务流与调试页驱动。
  • autofocustool 整目录退役(删除)= 移植收尾动作,不在本计划执行。现状:编译上已无任何工程引用(算法移植入 IvfTl.AutoFocus),但它是 Protocol.cs 下位机协议权威与对焦算法逐行比对参照。解锁判据(01 §5.5):M2 真机验证通过(V-020 + 对焦链 V-041~V-067,移植结果与原 autofocustool 一致)后方可删;验证期保留本地副本作比对/排障基准。
  • 死参数勿动:只移植 03 §3.2 在用参数(粗 ZCoarseCenter=90000±30000 步距 2000、精 ±6000 步距 500),勿引入 §3.5 死参数(HScanRange/ZHalf/ZLayers 等旧未用字段)。

Git 说明:根目录非 git 仓库,"Commit"步骤无法执行。每个 Task 末尾检查点用"保存并核对文件 + grep 残留"代替。


已 codegraph / 源码核对的关键事实(步骤据此编写)

事实 位置(精确)
四步标定引擎 + 签名 autofocustool/Calib/CalibrationEngine.cs:16 class CalibrationEngine:76 CalibrationEngine(HouseMotor motor, SerialCamera cam):181 public WellCalib CalibrateWell(int well, int eepromHPos, int eepromZ);四步 CoarseFocus:339/ScanForCenter:149/曝光二分 :222(ExposureMeter.BinarySearch)/精对焦在 CalibrateWell 后段
引擎对硬件的依赖(移植要替换的面) CalibrationEnginereadonly HouseMotor _motor(:20)、readonly SerialCamera _cam(:21);调 _motor.VerticalMoveTo/HorizontalMoveTo_cam.SetExposure/GrabRgb/GetSourceBuffercam.Width/Height(:79)
清晰度 ÷mean 一次(勿改回 ÷mean²) autofocustool/Imaging/Sharpness.cs:105 return sumSq / n / mean;(:99-104 注释佐证:改 mean 后峰落 92000)
well 圆检测(纯 C#,无 OpenCV) autofocustool/Imaging/WellDetector.cs:31 static class WellDetector:37 Detect(byte[] bgr24,int width,int height,int downsample=4);返回 WellCircle(:7)
曝光评价 + 二分 autofocustool/Imaging/ExposureMeter.cs:24 static class ExposureMeter:30 Measure(...):78 BinarySearch(int lo,int hi,Func<int,ExposureInfo> grabMeasure,...);返回 ExposureInfo(:6)
协议(最权威,HAL 蓝本) autofocustool/Serial/Protocol.csSerial/HouseMotor.csSerial/SerialMotor.csCamera/Camera.cs(M1 已以此为蓝本落 HAL;M2 不再移植硬件类,改用 HAL)
标定结果落 JSON 的现状 autofocustool/Calibrate/Calibrate.cs:81-83 new CalibrationFile{...}; file.Houses.Add(houseCalib); file.Save(JsonPath)(测试外壳,不移植;落库/落 JSON 逻辑由 M2-04 在合并端重写)
StartAutoFocus 本地化目标点 ivf_tl_control_2.0/ivf_tl_Com/HouseBin.cs:1359 private bool StartAutoFocus():1367 _autoFocusPhoto = GetAutoFocusServiceEvent?.Invoke(House.houseSn, wellList)(先服务器);:1379 _autoFocusPhoto = GetAutoFocusDBEvent?.Invoke(...)(后本地 DB)
触发框架(不动,仅改对焦来源) 主循环 HouseBin.cs:618 if(!FirstClearest) 仅间隔拍照(CCD);:642 if(!StartAutoFocus())continue; :644 FirstClearest=false; :645 if(GetClearest(...)) 对焦+拍照。FirstClearest 由定时/门/放皿置 true(12 §2.3:AppData.cs:425/HouseBin.cs DoorStateChanged/StartDish
对焦事件委托类型 HouseBin.cs:47 event Func<int,Dictionary<int,DateTime?>,List<HouseWellPhoto>> GetAutoFocusServiceEvent:52 ...GetAutoFocusDBEvent:375 List<HouseWellPhoto> _autoFocusPhoto
对焦结果载体(FocusZ 锚点存这里) ivf_tl_Entity/GlobalEntitys/HouseWellPhoto.cs:9 class HouseWellPhoto:15 int wellSn:20 int verticalMotorPosition(= 对焦清晰层锚点,下游按它生成各层)
N 层拍摄当前用旧公式(M2 要改) HouseBin.cs:1583-1591currentVer = currentAutoFocus.verticalMotorPosition + (House.verticalMotorSpacePulse.Value * i),无 focus_layer_down,无值时硬编码 128(:1590) — 须改为 §2.4 公式 + §2.5 配置链
机旁 native 打分(M2-01b/V-007) HouseBin.cs:2450 GetScore(...):2454 AivfoHelper.GetImageScoreAndSaveImage(...)(native P/Invoke 打分);审核报告维度③第12项、新发现 V-007 已登记"本地对焦化后这套机旁评分是否一并废弃"
HAL 入口(对焦借用) ivf_tl_control_2.0/IvfTl.Hardware/IHardwareAccessLayer.cs:33 GetHouse:36 GetHouseGate(int houseSn)HardwareUser.AutoFocus 已留位(13 §3.4,M1 已落地枚举/接口);接入方式 13 §④
control DB 访问方式(M2-04 据此) ivf_tl_ServicesImpl/DBServices/DBServiceImplNo.cs:15 using SqlSugar:34 SqlSugarScope Db:87 Db.Updateable(...)/:91 Db.Insertable(...)C# SqlSugar 直连,非 Java
数据层迁移已就绪(建表/扩列) sql/migrations/2026-06-17-autofocus-data-layer.sql:建 house_autofocus_calibration(scene/focus_z/exposure/horizontal_pulse/peak_ratio/circle_found/center_offset_pct/...);tl_settingfocus_layer_spacing_pulse(无默认)/focus_layer_count(默认5)/focus_layer_down(默认2)/focus_peak_ratio_threshold(默认1.2);house_well_settingfocus_layer_spacing_pulse/focus_layer_count(可空覆盖),下移复用 move_down_layer

待验证清单登记项(本计划新增,统一记入 项目文档/进度/待验证清单.md

V 项 待验证内容 依赖 风险 出处
V-020 算法移植后接 HAL ISerialChannel/ICamera 跑通:粗/精对焦 Z 扫描、曝光二分、well 圆检测在合并端硬件借用下与 autofocustool 原结果一致 net8 + 真机 M2-01
V-021 74000 伪峰:粗对焦偶发选中 ~74000(真焦面 86000–92000),开 DebugSave 存图复现/缓解(03 §6.1,上线前置) 真机(含胚胎/反光) M2-01/M2-03
V-022 真胚胎峰比 > 1.5(仅空皿测过,峰比 ~1.1;03 §6.2) 真机 + 真胚胎 M2-01
V-023 对焦借用与 control 采集互斥:Acquire(AutoFocus) 时该舱采集暂停、对焦完归还后恢复,不撞帧/不死锁(13 §④ control 采集让路) net8 + 真机 M2-03
V-024 §2.4 层位置公式生成的各层绝对 Z 下发正确(含 focus_layer_down 偏移、边界钳位 verticalMotorPulseMax net8 + 真机 M2-02/M2-03
V-025 §2.5 配置就近优先在真实库数据上的解析(well 覆盖 / 继承设备级 / 双空报错告警),层间距未配置时确实报错不兜底 真机库 + net8 M2-02
V-026 house_autofocus_calibration 镜像写入:scene=0 基准 upsert 每 well 唯一、scene=1 日常 append;清理任务只删 scene=1 net8 + DB M2-04
V-027 calibration.json 读写:IncludeFields=true 生效(CalibrationFile 为 public 字段,否则空对象静默失效,03 §4) net8 M2-04
V-028 场景 A 调试页一键标定(沙盒):16 well 逐个跑,合格(峰比>阈值)绿/伪峰红,结果存档(03 §7.1,算法严谨性安全沙盒) net8 + 真机 M2-05
V-029 场景 B 放皿后自动对焦:既有触发(放皿/门/定时)→本地对焦→拍照位置正确→正常出 N 层图(03 §7.2,稳定后再启用) net8 + 真机 M2-06
V-030 对焦后界面手调拍摄层(层数/间距/下移)持久化 well 级(house_well_setting),下次该 well 沿用 net8 + DB M2-07
V-007(既有,本计划承接) 机旁 native 打分 HouseBin.cs:2454 GetImageScoreAndSaveImage 与云端打分链是否同源;本地对焦化后这套机旁评分是否一并废弃(影响 M2-01b 去留) 真机 + 联调 审核报告新发现 V-007 / M2-01b

注:V-021/V-022(74000 伪峰、真胚胎峰比)与 EEPROM 回写(D9,03 §4,归 M2-04/M3 视决策)是 03 §6 列的产线场景 B 上线三前置,必须有结论或缓解(对齐 12 §1.4 高风险项)。


Task M2-01 · 移植对焦算法类入合并端,对接 HAL

目标:把 Sharpness / WellDetector / ExposureMeter / CalibrationEngine(+ 结果类 WellCircle/ExposureInfo/WellCalib)移植进合并端新建程序集,并把 CalibrationEngine 对硬件的依赖从 autofocustool 自带的 HouseMotor/SerialMotor/Camera 改为 HAL 的 ISerialChannel/ICamera不移植测试外壳。

为何:03 §1/§2——算法接在 HAL 之上、control 业务流之内,与采集/调试共用同一硬件持有者,不另开串口/相机。

  • 落点:新建程序集 ivf_tl_control_2.0/IvfTl.AutoFocus/(命名空间 IvfTl.AutoFocus),与 IvfTl.Hardware 同级并入合并解决方案。子目录:
    • Imaging/Sharpness.csImaging/WellDetector.csImaging/ExposureMeter.cs整文件逐字搬运,命名空间由 AutoFocusTool.Imaging 改为 IvfTl.AutoFocus.Imaging;这三个是纯像素算法,不依赖硬件,搬运后改 namespace 即可)。
    • Sharpness.cs:105 return sumSq / n / mean; 一字不动(÷mean 一次)。
    • WellDetector.Detect 的判据常数(:34 MinAreaPct=4.0:113 aspect/boxFill/rConsist/rFrac 阈值)一字不动。
    • ExposureMeterMeanLo=95/MeanHi=150/SatMax=2.0(:26-27)一字不动。
    • Calib/CalibrationEngine.cs(移植,改硬件依赖面,见下)。
    • Calib/WellCalib.csCalib/CalibrationFile.csCalib/HouseCalib.cs:从 autofocustool 对应文件搬运结果 POCO(WellCalibWell/HorizontalPulse/Exposure/FocusZ/CenterOffsetPct/PeakRatio/CircleFound/NoteCalibrationFileHouses 等为 public 字段 — 保留字段语义,M2-04 落 JSON 据此)。
  • CalibrationEngine 改造点(核心)
    • 构造签名 CalibrationEngine(HouseMotor motor, SerialCamera cam)(原 :76)→ 改为 CalibrationEngine(IvfTl.Hardware.ISerialChannel serial, IvfTl.Hardware.ICamera cam)。字段 readonly HouseMotor _motor(:20)→readonly ISerialChannel _serialreadonly SerialCamera _cam(:21) 类型改 ICameraW=cam.Width;H=cam.Height(:79) 保留(ICamera.Width/Height 已在 13 §3.3 定义)。
    • 方法体内硬件调用逐一映射到 HAL 同名方法(13 §3.2/3.3 接口本以 af 为蓝本,映射近 1:1):
    • _motor.VerticalMoveTo(z, ScanDelayMs)(:353/:361/:197)→ _serial.VerticalMoveToWait(z, ScanDelayMs)
    • _motor.HorizontalMoveTo(hp, delay)(:159/:186/:211)→ _serial.HorizontalMoveToWait(hp, delay)
    • _cam.SetExposure(e)(:155/:191/:224/:71@Calibrate)→ _cam.SetExposure(e)(同名)
    • Grab()(:120) 内 _cam.GrabRgb() + _cam.GetSourceBuffer() → 用 _cam.GrabRgb() + _cam.GetFrameBuffer()(13 §3.3 命名;保留"丢残留帧"双 Grab 语义,:355/:363-364)。或改用 ICamera.GrabStable(preDelayMs,discardStale:true,retry)(13 §3.5 内置丢帧),二选一,保留丢一帧语义
    • RetryMove(()=>_motor.HorizontalMoveTo(...))(:109/:186/:211) → 委托内改 _serial.HorizontalMoveToWait(...),重试框架保留。
    • EEPROM 读取入参来源CalibrateWell(well, eepromHPos, eepromZ)(:181) 的 eepromHPos/eepromZ 在 autofocustool 由 motor.ReadWellHorizontalPos/ReadWellFocusZero(Calibrate.cs:59-60)取。合并端改由 _serial.ReadWellHorizontalPosWait(well) / _serial.ReadWellFocusZeroWait(well)(13 §3.2)取,作为扫描中心/参考(§2.5:EEPROM 仅参考,不进配置解析链,不回写)。
    • 移植参数只保留 03 §3.2 在用项(ZCoarseCenter=90000/ZCoarseHalf=30000/ZCoarseStep=2000/CoarseSettleMs=2000/FineZHalf=6000/FineZStep=500/ScanDelayMs=350),勿引入死参数。
  • 不移植清单(不要搬):autofocustool/SelfTest/*SmokeTest/*Calibrate/*CalibTest/*ToPng/*MainWindow*.csSerial/*(HAL 已替代)、Camera/*(HAL 已替代)、Devices/DeviceScanner.cs(HAL ScanDevices 已替代)。
  • 与 HAL 衔接:算法不再 new 任何硬件;调用方(M2-03/M2-05)在 Acquire(AutoFocus) 拿到 lease 后,用 new CalibrationEngine(lease.Serial, lease.Camera) 构造。
  • 检查点 M2-01:grep 确认 IvfTl.AutoFocus new HouseMotor/new SerialMotor/new Camera/using AutoFocusTool.Serial/using AutoFocusTool.Camera 残留;Sharpness.cs:105 仍为 sumSq / n / mean。保存并核对。构建/算法回归 → 待 M1-00+net8+真机(登记 V-020/V-021/V-022)。

Task M2-01b · 机旁 native 打分(HouseBin.cs:2450/2454)去留评估(V-007)

目标:评估本地对焦化后,机旁 C# 自带的 native 打分链(GetScoreAivfoHelper.GetImageScoreAndSaveImage)是否还需保留(即"本地对焦后是否还拍多层打分")。只评估 + 登记,本 Task 不删代码

为何:审核报告维度③第12项 + 新发现 V-007——文档把"打分"纯归云端 data-transmission,但机旁 C# 端另有一套 native 评分HouseBin.cs:2450 GetScore:2454 AivfoHelper.GetImageScoreAndSaveImagepicture.cs:150 image_score)。本地对焦只找 1 清晰层(12 §2.1),不再依赖"拍多层→打分选最高分层";这套机旁打分的去留需厘清。

  • 核对:grep GetScore/GetImageScoreAndSaveImage/AivfoHelper 在 control 端全部调用点;确认 GetScore(:2450) 的调用方(对焦流 Autofocus(...) 内是否调用、是否把分数写入 ImageDTO.Clearest/picture.image_score 用于选层)。
  • 评估结论写进交接卡 + 待验证清单
    • 若机旁打分仅服务"云端选层"语义 → 随本地对焦化废弃(但删除动作归 M3 微服务改造或后续清理,M2 不删,遵循代码隔离"死代码先不删")。
    • 若机旁打分另有用途(如本地图片质量标注/留档)→ 标"保留待定"。
  • 检查点 M2-01b:在 待验证清单.mdV-007 承接行(与 M2-01b 关联);在 交接卡.md 记结论(同源/去留)。不改源码。→ 真机+联调确认(V-007)。

Task M2-02 · 拍摄层位置计算 + 配置解析(纯逻辑,可单测)★唯一真实单测

目标:实现 §2.4 层位置公式与 §2.5 就近优先配置解析,作为不依赖硬件/DB/WPF/HAL 的纯函数类写真实单元测试

为何:12 §2.4/§2.5——FocusZ(动态锚点)与拍摄层参数(静态配置)正交结合;配置就近优先 well→设备级→报错,不用魔法数兜底。这是 M2 唯一能纯逻辑单测的部分。

  • 落点ivf_tl_control_2.0/IvfTl.AutoFocus/Layout/PhotoLayerCalculator.cs(命名空间 IvfTl.AutoFocus.Layout只引用 BCL,不引用 HAL/SqlSugar/WPF — 保证可移植到独立 net6 测试项目)。
  • [ ] 配置入参类型(纯数据,调用方从库填好后传入,解析与取数解耦)

    public sealed class FocusLayerConfig            // 单一来源已解析好的有效配置
    {
      public int LayerSpacingPulse { get; init; } // 层间距脉冲(必填,无默认)
      public int LayerCount { get; init; }        // 层数
      public int LayerDown { get; init; }         // 下移层数(对焦起点在清晰层下方几层)
    }
    public sealed class FocusLayerRawConfig         // 两级原始值(可空),喂给就近优先解析
    {
      public int? WellSpacingPulse, WellLayerCount, WellMoveDownLayer;   // house_well_setting 覆盖
      public int? DeviceSpacingPulse, DeviceLayerCount, DeviceLayerDown; // tl_setting 设备级
      public string TlSn; public int HouseSn; public int WellSn;         // 报错信息用
    }
    
  • [ ] 方法签名

    // §2.5 就近优先解析:well 覆盖(非空) > 设备级 > 报错。
    // 层间距两级皆空 → 抛 FocusConfigMissingException("设备{TlSn}对焦层间距未配置,请先初始化")。
    // 下移:well.MoveDownLayer 非空覆盖 device.LayerDown(对齐 sql 注释:复用 move_down_layer)。
    public static FocusLayerConfig Resolve(FocusLayerRawConfig raw);
    
    // §2.4 公式:对焦起点(第0层)=FocusZ - LayerDown×Spacing;第i层=起点 + i×Spacing (i=0..Count-1)。
    // 返回各层绝对 Z 脉冲(长度=LayerCount)。pulseMax>0 时对越界层钳位/截断(对齐 verticalMotorPulseMax)。
    public static int[] ComputeLayerPositions(int focusZ, FocusLayerConfig cfg, int pulseMax = 0);
    
    • FocusConfigMissingException : Exception(同文件)——层间距缺失专用异常,调用方据此弹"请先配置"告警(§2.5 不兜底)。
  • [ ] 单元测试ivf_tl_control_2.0/IvfTl.AutoFocus.Tests/PhotoLayerCalculatorTests.cs,xUnit/MSTest 任一;真实断言,非占位):

    • 公式用例(对齐 12 §2.4 示意)focusZ=88434, spacing=128, down=2, count=5 → 期望 [88178, 88306, 88434, 88562, 88690](第0层=88434−2×128=88178;清晰层在第2层)。断言数组逐元素相等、长度=5。
    • 公式边界down=0, count=3 → 起点=focusZ;pulseMax 小于末层 → 末层被钳/截断(按实现约定断言)。
    • 配置场景①(well 覆盖)Well* 全非空 + Device* 非空Resolve 取 well 值(断言 Spacing/Count 来自 well)。
    • 配置场景②(继承)Well* 全空 + Device* 非空 → 取设备级值(断言来自 device,LayerDown 用 device.LayerDown)。
    • 配置场景③(双空报错)WellSpacingPulse=null + DeviceSpacingPulse=nullAssert.Throws<FocusConfigMissingException>,消息含 TlSn
    • 下移覆盖WellMoveDownLayer=3 + DeviceLayerDown=2 → 解析后 LayerDown=3。
  • [ ] 检查点 M2-02:核对 PhotoLayerCalculator.csusing System(无 HAL/SqlSugar/WPF);测试用例覆盖上述 6 类。本地 net6 若无法跑 net8 测试宿主:在 待验证清单.md 记 net6 下用独立纯逻辑测试项目(仅引用本 .cs)验证(登记 V-024/V-025 为真机/真实库链路确认;纯逻辑单测本地可补跑)。


Task M2-03 · StartAutoFocus 本地化(HouseBin.cs:1359)

目标:把 StartAutoFocus() 从"先服务器(GetAutoFocusServiceEvent)后本地 DB(GetAutoFocusDBEvent)"改为调本地 CalibrationEngineFocusZ,填充 _autoFocusPhoto;并把 N 层拍摄公式(:1583-1591)改为 §2.4 公式 + M2-02 配置链。触发框架(FirstClearest/定时/门/放皿)与拍照框架不动

为何:12 §2.3——StartAutoFocus 现走云端选层链(04 文档要删),本轮唯一改的点是对焦结果来源改本地;03 §3——本地四步标定算出 FocusZ 取代 clearPosition

  • [ ] 改造点 1(对焦来源,HouseBin.cs:1366-1386):保留 IsCCD() 前置判断(:1361)与 _autoFocusPhoto.Clear()(:1366)。删除/停用 :1367 GetAutoFocusServiceEvent?.Invoke(...):1379 GetAutoFocusDBEvent?.Invoke(...) 两条云端/DB 取数(事件声明 :47/:52 先保留不删,遵循代码隔离),改为:

    // M2-03 本地对焦:对本舱 wellList 逐 well 调本地 CalibrationEngine 算 FocusZ,填 _autoFocusPhoto。
    var gate = HardwareAccessLayer.Instance.GetHouseGate(House.houseSn);
    using var lease = gate.Acquire(HardwareUser.AutoFocus, timeoutMs);   // 前台优先,HAL 暂停本舱采集
    if (lease == null) { /* 拿不到→日志, return false 让本轮跳过 */ }
    var engine = new IvfTl.AutoFocus.Calib.CalibrationEngine(lease.Serial, lease.Camera){ Log=... };
    foreach (var well in wellList.Keys) {
      int hpos = lease.Serial.ReadWellHorizontalPosWait(well);   // EEPROM 仅作扫描中心(参考)
      int zZero = lease.Serial.ReadWellFocusZeroWait(well);
      var wc = engine.CalibrateWell(well, hpos, Math.Max(0,zZero));
      _autoFocusPhoto.Add(new HouseWellPhoto{ wellSn=well, verticalMotorPosition=wc.FocusZ });
    }
    return _autoFocusPhoto.Any();
    
    • verticalMotorPosition 承载 FocusZ 锚点(与原 clearPosition 同位,下游 :1555/:1707 已按 _autoFocusPhoto.FirstOrDefault(x=>x.wellSn==...) 取)。
    • 标定结果同时落 JSON/库由 M2-04 完成(在 CalibrateWell 后调 M2-04 的存储入口)。
  • [ ] 改造点 2(N 层公式,HouseBin.cs:1583-1591):现状 currentVer = currentAutoFocus.verticalMotorPosition + spacePulse*i(无下移、硬编码 128)→ 改为先用 M2-02 Resolve 取配置、ComputeLayerPositions(focusZ, cfg, TLSetting.verticalMotorPulseMax) 生成各层,循环取第 i 层绝对 Z:

    • focusZ = currentAutoFocus.verticalMotorPositioncfg 由 M2-04/M2-07 提供的就近优先原始值(well=_wellSettings 对应项、device=TLSetting)构造。
    • 保留既有循环结构(House.autoFocusNumber 改用 cfg.LayerCount;越界判断 currentVer > TLSetting.verticalMotorPulseMax 保留,:1593)。
    • 层间距未配置(M2-02 抛 FocusConfigMissingException)→ 写告警日志 + 本舱本轮跳过(§2.5 不兜底,不用 128 魔法数)。
  • [ ] 保留不动:主循环 :618 if(!FirstClearest):642 if(!StartAutoFocus())continue:644 FirstClearest=false:645 GetClearest(...)、放皿/门/定时置 FirstClearest=true 的触发(12 §2.3,三类触发已实现,本轮不新增)。

  • [ ] 与 HAL 衔接:对焦借用走 GetHouseGate(houseSn).Acquire(HardwareUser.AutoFocus)(13 §④,前台优先于 ControlCapture);using 归还后 HAL 自动 ResumeCapture。control 采集线程拿不到该舱借用时跳过本轮(已是 M1 互斥语义)。

  • [ ] 检查点 M2-03:grep 确认 StartAutoFocus 内已无 GetAutoFocusServiceEvent?.Invoke/GetAutoFocusDBEvent?.Invoke 生效调用;N 层循环已无硬编码 128(:1590-1591);new CalibrationEngine(lease.Serial, lease.Camera) 存在。保存并核对。构建/真机 → 待 M1-00+net8+真机(登记 V-023/V-024;伪峰承接 V-021)。


Task M2-04 · 标定结果落 calibration.json + 镜像 house_autofocus_calibration

目标:把每次标定结果写本地 calibration.json(真相源)并镜像入 house_autofocus_calibration(scene=0 基准 / scene=1 日常)。给出 JSON 读写(IncludeFields=true)与库写入(C# SqlSugar 直连)。

为何:12 §2.7——本地 JSON 真相源 + 单表镜像,scene 区分基准/日常,基准永不被清理、唯一 upsert,日常 append;03 §4——JSON 反序列化 IncludeFields=true 陷阱(CalibrationFile 为 public 字段,否则空对象静默失效)。

  • 数据访问方式核对结论(已核):control 端用 SqlSugar 直连DBServiceImplNo.cs:15 using SqlSugar:34 SqlSugarScope DbDb.Insertable/Updateable)。→ 镜像写入用 C# SqlSugar 直连不经 Java(Java 侧 entity/mapper 属 M3,本表的中央查询由 M3 视需要补,M2 只负责本地→库写入)。
  • [ ] 落点(DB 镜像)

    • ivf_tl_Entity/DBEntitys/HouseAutofocusCalibrationDB.cs:SqlSugar 实体,字段对齐 sql/migrations/2026-06-17-autofocus-data-layer.sqlhouse_autofocus_calibrationtlSn/houseSn/wellSn/scene/focusZ/exposure/horizontalPulse/peakRatio/circleFound/centerOffsetPct/calibTime/source/note + 审计列 createBy/createTime/updateBy/updateTime/deleted/platformId)。[SugarColumn] 映射列名(蛇形)。
    • IvfTl.AutoFocus/Storage/CalibrationStore.cs(或并入 ivf_tl_ServicesImpl):写入入口

      void SaveCalibration(WellCalib wc, string tlSn, int houseSn, int scene, string source="LOCAL_JSON");
      
    • scene=0:先按 (tlSn,houseSn,wellSn,scene=0,deleted==null) 查,存在则 Db.Updateable(upsert,每 well 唯一),否则 Db.Insertable(§2.7 约束2)。

    • scene=1:直接 Db.Insertable(append 留历史)。

    • peakRatio/circleFound/centerOffsetPct 取自 WellCalibPeakRatio/CircleFound/CenterOffsetPct)。

  • [ ] 落点(JSON 真相源)IvfTl.AutoFocus/Storage/CalibrationFile.cs(从 autofocustool 搬运并复用其 public 字段结构)+ Load/Save

    static readonly JsonSerializerOptions Opt = new(){ IncludeFields = true, WriteIndented = true };
    // ⚠ IncludeFields=true 必须保留:CalibrationFile/HouseCalib/WellCalib 多为 public 字段,
    //   缺此选项 System.Text.Json 反序列化得空对象,闭环静默失效(03 §4)。
    
    • JSON 路径不写死到 autofocustool 的 C:\claudeFile\...(那是测试外壳常量,Calibrate.cs:22);改为配置/合并端工作目录可定位的路径(与 03 §2.7"机旁 calibration.json"语义一致,路径由部署配置)。
  • [ ] 调用衔接:M2-03(场景 B 日常)调 SaveCalibration(wc, ..., scene:1);M2-05(场景 A 基准标定)调 scene:0。两者都先 file.Save(jsonPath)(JSON 为准)再镜像库。

  • [ ] 检查点 M2-04:核对实体字段与 sql 迁移列一一对应;JsonSerializerOptionsIncludeFields=true;scene=0/1 分支符合 §2.7 约束。保存并核对。库写/JSON 读写 → 待 net8+DB(登记 V-026/V-027)。


Task M2-05 · 场景 A 工程师调试页一键标定(沙盒)

目标:在 operate 调试页(HouseDebugPageViewModel)加"一键标定":对选中舱的 16 well 逐个跑四步标定,合格(峰比>阈值)绿/伪峰红,结果存档(scene=0 基准)。有人盯、可中止

为何:03 §2/§7.1——场景 A 是算法严谨性的安全沙盒,先在这里验证;标定结果作出厂基准。

  • 落点ivf_tl_operate_2.0/ivf_tl_Operate/ViewModel/HouseDebugPageViewModel.cs(M1 已将其硬件改为 HAL 借用,:35/:36/:213/:230 旧 new 已替换)。新增命令 OneClickCalibrateCommand
    • using var lease = HAL.GetHouseGate(houseSn).Acquire(HardwareUser.OperateDebug)(调试页本就持前台借用);用 lease.Serial/lease.Camera 构造 CalibrationEngine
    • 逐 well CalibrateWell → 结果列表项含峰比、是否伪峰(峰比 < tl_setting.focus_peak_ratio_thresholdCircleFound==false 判红);UI 合格绿/伪峰红。
    • 支持中止(IsStopClearest 式标志 / CancellationToken),每 well 间检查。
    • 跑完调 M2-04 SaveCalibration(..., scene:0)(基准 upsert)+ file.Save
  • 检查点 M2-05:核对命令存在、用 lease 借用、绿/红判据接 focus_peak_ratio_threshold、存档 scene=0。保存并核对。沙盒验证 → 待 net8+真机(登记 V-028;伪峰承接 V-021、真胚胎峰比 V-022)。

Task M2-06 · 场景 B 放皿后自动对焦接既有触发

目标:场景 B 不新增触发——放皿/门/定时既有触发置 FirstClearest=true 后,主循环已走 StartAutoFocus()(M2-03 已本地化)。本 Task 确认接通并加"稳定后再启用"的开关/灰度。

为何:12 §2.3 三类触发已实现,03 §2/§7.2 场景 B 稳定后再启用(产线自动对焦,受 03 §6 三前置约束)。

  • 核对接通:放皿 StartDish、门 DoorStateChanged、定时 AppData.cs:425 StartPushMessageThreadFirstClearest=true 的路径未被 M2-03 改动(M2-03 只改 StartAutoFocus 内部来源);主循环 :642StartAutoFocus()_autoFocusPhoto(本地 FocusZ)→:645 GetClearest→N 层拍照(M2-03 改公式后)出图。
  • 灰度开关:加设备级开关(如复用/新增 tl_setting 标志或 house.autoFocus,:168 FirstClearest=_house.autoFocus)控制场景 B 本地对焦启用,默认关,03 §6 三前置(74000 伪峰、真胚胎峰比、EEPROM 回写)有结论后再开(不在 M2 强行打开)。
  • 检查点 M2-06:grep 确认三类触发路径未被破坏;灰度开关默认关。保存并核对。放皿→对焦→拍照链路 → 待 net8+真机(登记 V-029)。

Task M2-07 · 对焦后界面手调拍摄层(层数/间距/下移)持久化 well 级

目标:自动对焦完成后,界面允许手动调整实际拍摄层数/间距/下移层数,持久化到 well 级(house_well_setting 的 M2 扩列 focus_layer_spacing_pulse/focus_layer_count + 复用 move_down_layer),下次该 well 沿用。

为何:12 §2.6——对焦范围广、实际拍摄范围窄,二者不同;手调值写 well 级覆盖。

  • 落点(UI):在调试页/对焦结果页加三字段编辑(层数/间距/下移)+ "保存为该 well 默认"按钮(12 §2.6 提的"仅本次/存为默认",本计划做"存为默认"持久化;"仅本次"为内存值传入 M2-03 公式)。
  • 落点(持久化,SqlSugar 直连)ivf_tl_Entity/DBEntitys/HouseWellSettingDB.csfocusLayerSpacingPulse/focusLayerCount(well 级可空覆盖,对齐 sql 迁移 :63-64),moveDownLayer 复用既有。写入用 Db.Updateable<HouseWellSettingDB>()(按 tlSn/houseSn/wellSn 更新对应 well 行)。
  • 与配置链衔接:写入后,M2-03 下次取 FocusLayerRawConfig 时 well 级非空 → §2.5 就近优先取 well 覆盖(M2-02 Resolve 已实现),实现"下次该 well 沿用"。
  • 检查点 M2-07:核对 HouseWellSettingDB 扩列与 sql 一致;保存命令写 well 行;与 M2-02 Resolve 的 well 覆盖路径自洽。保存并核对。手调持久化 → 待 net8+DB(登记 V-030)。

执行顺序与依赖

M2-01 (移植算法+接HAL) ──┬─► M2-03 (StartAutoFocus本地化) ──┬─► M2-05 (场景A沙盒)
M2-02 (层位置+配置纯逻辑)─┘                                  ├─► M2-06 (场景B接触发)
M2-04 (落JSON+库镜像) ────────────► (被 M2-03/M2-05 调用)     └─► M2-07 (手调持久化well级)
M2-01b (机旁打分去留评估) ── 独立,可随时做,结论喂 M3/清理
  • M2-02 无前置,可最先做(唯一可本地纯逻辑单测)。
  • M2-03 依赖 M2-01(算法类)+ M2-02(公式/配置)+ M2-04(存储入口)。
  • M2-05/M2-06/M2-07 依赖 M2-03。
  • 整体编译/运行依赖 M1-00(程序集统一)+ net8 工具链,集中并入 M7。

自检清单(成文时核对)

  • 无占位符/伪代码混入"已完成"判定;所有"构建/真机"步骤改"代码完成→登记 V 项→M7 集中测"。
  • 文件:行号经 codegraph/Read 核对(StartAutoFocus 实测 :1359,文档 §标 1351 为近似;N 层公式 :1583-1591;native 打分 :2450/:2454;Sharpness ÷mean :105;CalibrateWell :181)。
  • M2-02 为纯逻辑、写真实单测、不引用 HAL/DB/WPF。
  • §2.5 不用魔法数兜底(层间距缺失抛异常);EEPROM 仅参考不进解析链。
  • 数据访问方式已核:control 用 SqlSugar 直连(非 Java);JSON IncludeFields=true 保留。