For agentic workers: REQUIRED SUB-SKILL:用
superpowers:subagent-driven-development或superpowers: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.cs,SqlSugarScope 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-改造执行框架与自动对焦数据层.md 与 2026-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 一致)后方可删;验证期保留本地副本作比对/排障基准。ZCoarseCenter=90000±30000 步距 2000、精 ±6000 步距 500),勿引入 §3.5 死参数(HScanRange/ZHalf/ZLayers 等旧未用字段)。Git 说明:根目录非 git 仓库,"Commit"步骤无法执行。每个 Task 末尾检查点用"保存并核对文件 + grep 残留"代替。
| 事实 | 位置(精确) |
|---|---|
| 四步标定引擎 + 签名 | 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 后段 |
| 引擎对硬件的依赖(移植要替换的面) | CalibrationEngine 持 readonly HouseMotor _motor(:20)、readonly SerialCamera _cam(:21);调 _motor.VerticalMoveTo/HorizontalMoveTo、_cam.SetExposure/GrabRgb/GetSourceBuffer、cam.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.cs、Serial/HouseMotor.cs、Serial/SerialMotor.cs、Camera/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-1591:currentVer = 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_setting 扩 focus_layer_spacing_pulse(无默认)/focus_layer_count(默认5)/focus_layer_down(默认2)/focus_peak_ratio_threshold(默认1.2);house_well_setting 扩 focus_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 高风险项)。
目标:把 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.cs、Imaging/WellDetector.cs、Imaging/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 阈值)一字不动。ExposureMeter 的 MeanLo=95/MeanHi=150/SatMax=2.0(:26-27)一字不动。Calib/CalibrationEngine.cs(移植,改硬件依赖面,见下)。Calib/WellCalib.cs、Calib/CalibrationFile.cs、Calib/HouseCalib.cs:从 autofocustool 对应文件搬运结果 POCO(WellCalib 含 Well/HorizontalPulse/Exposure/FocusZ/CenterOffsetPct/PeakRatio/CircleFound/Note,CalibrationFile 的 Houses 等为 public 字段 — 保留字段语义,M2-04 落 JSON 据此)。CalibrationEngine(HouseMotor motor, SerialCamera cam)(原 :76)→ 改为 CalibrationEngine(IvfTl.Hardware.ISerialChannel serial, IvfTl.Hardware.ICamera cam)。字段 readonly HouseMotor _motor(:20)→readonly ISerialChannel _serial;readonly SerialCamera _cam(:21) 类型改 ICamera。W=cam.Width;H=cam.Height(:79) 保留(ICamera.Width/Height 已在 13 §3.3 定义)。_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(...),重试框架保留。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 仅参考,不进配置解析链,不回写)。ZCoarseCenter=90000/ZCoarseHalf=30000/ZCoarseStep=2000/CoarseSettleMs=2000/FineZHalf=6000/FineZStep=500/ScanDelayMs=350),勿引入死参数。autofocustool/SelfTest/*、SmokeTest/*、Calibrate/*、CalibTest/*、ToPng/*、MainWindow*.cs、Serial/*(HAL 已替代)、Camera/*(HAL 已替代)、Devices/DeviceScanner.cs(HAL ScanDevices 已替代)。new 任何硬件;调用方(M2-03/M2-05)在 Acquire(AutoFocus) 拿到 lease 后,用 new CalibrationEngine(lease.Serial, lease.Camera) 构造。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)。目标:评估本地对焦化后,机旁 C# 自带的 native 打分链(GetScore→AivfoHelper.GetImageScoreAndSaveImage)是否还需保留(即"本地对焦后是否还拍多层打分")。只评估 + 登记,本 Task 不删代码。
为何:审核报告维度③第12项 + 新发现 V-007——文档把"打分"纯归云端 data-transmission,但机旁 C# 端另有一套 native 评分(HouseBin.cs:2450 GetScore→:2454 AivfoHelper.GetImageScoreAndSaveImage,picture.cs:150 image_score)。本地对焦只找 1 清晰层(12 §2.1),不再依赖"拍多层→打分选最高分层";这套机旁打分的去留需厘清。
GetScore/GetImageScoreAndSaveImage/AivfoHelper 在 control 端全部调用点;确认 GetScore(:2450) 的调用方(对焦流 Autofocus(...) 内是否调用、是否把分数写入 ImageDTO.Clearest/picture.image_score 用于选层)。待验证清单.md 落 V-007 承接行(与 M2-01b 关联);在 交接卡.md 记结论(同源/去留)。不改源码。→ 真机+联调确认(V-007)。目标:实现 §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 任一;真实断言,非占位):
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* 全非空 + Device* 非空 → Resolve 取 well 值(断言 Spacing/Count 来自 well)。Well* 全空 + Device* 非空 → 取设备级值(断言来自 device,LayerDown 用 device.LayerDown)。WellSpacingPulse=null + DeviceSpacingPulse=null → Assert.Throws<FocusConfigMissingException>,消息含 TlSn。WellMoveDownLayer=3 + DeviceLayerDown=2 → 解析后 LayerDown=3。[ ] 检查点 M2-02:核对 PhotoLayerCalculator.cs 仅 using System(无 HAL/SqlSugar/WPF);测试用例覆盖上述 6 类。本地 net6 若无法跑 net8 测试宿主:在 待验证清单.md 记 net6 下用独立纯逻辑测试项目(仅引用本 .cs)验证(登记 V-024/V-025 为真机/真实库链路确认;纯逻辑单测本地可补跑)。
目标:把 StartAutoFocus() 从"先服务器(GetAutoFocusServiceEvent)后本地 DB(GetAutoFocusDBEvent)"改为调本地 CalibrationEngine 算 FocusZ,填充 _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==...) 取)。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.verticalMotorPosition;cfg 由 M2-04/M2-07 提供的就近优先原始值(well=_wellSettings 对应项、device=TLSetting)构造。House.autoFocusNumber 改用 cfg.LayerCount;越界判断 currentVer > TLSetting.verticalMotorPulseMax 保留,:1593)。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)。
目标:把每次标定结果写本地 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 字段,否则空对象静默失效)。
DBServiceImplNo.cs:15 using SqlSugar、:34 SqlSugarScope Db、Db.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.sql 的 house_autofocus_calibration(tlSn/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 取自 WellCalib(PeakRatio/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)。
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 迁移列一一对应;JsonSerializerOptions 含 IncludeFields=true;scene=0/1 分支符合 §2.7 约束。保存并核对。库写/JSON 读写 → 待 net8+DB(登记 V-026/V-027)。
目标:在 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。CalibrateWell → 结果列表项含峰比、是否伪峰(峰比 < tl_setting.focus_peak_ratio_threshold 或 CircleFound==false 判红);UI 合格绿/伪峰红。IsStopClearest 式标志 / CancellationToken),每 well 间检查。SaveCalibration(..., scene:0)(基准 upsert)+ file.Save。focus_peak_ratio_threshold、存档 scene=0。保存并核对。沙盒验证 → 待 net8+真机(登记 V-028;伪峰承接 V-021、真胚胎峰比 V-022)。目标:场景 B 不新增触发——放皿/门/定时既有触发置 FirstClearest=true 后,主循环已走 StartAutoFocus()(M2-03 已本地化)。本 Task 确认接通并加"稳定后再启用"的开关/灰度。
为何:12 §2.3 三类触发已实现,03 §2/§7.2 场景 B 稳定后再启用(产线自动对焦,受 03 §6 三前置约束)。
StartDish、门 DoorStateChanged、定时 AppData.cs:425 StartPushMessageThread 置 FirstClearest=true 的路径未被 M2-03 改动(M2-03 只改 StartAutoFocus 内部来源);主循环 :642 经 StartAutoFocus()→_autoFocusPhoto(本地 FocusZ)→:645 GetClearest→N 层拍照(M2-03 改公式后)出图。tl_setting 标志或 house.autoFocus,:168 FirstClearest=_house.autoFocus)控制场景 B 本地对焦启用,默认关,03 §6 三前置(74000 伪峰、真胚胎峰比、EEPROM 回写)有结论后再开(不在 M2 强行打开)。目标:自动对焦完成后,界面允许手动调整实际拍摄层数/间距/下移层数,持久化到 well 级(house_well_setting 的 M2 扩列 focus_layer_spacing_pulse/focus_layer_count + 复用 move_down_layer),下次该 well 沿用。
为何:12 §2.6——对焦范围广、实际拍摄范围窄,二者不同;手调值写 well 级覆盖。
ivf_tl_Entity/DBEntitys/HouseWellSettingDB.cs 补 focusLayerSpacingPulse/focusLayerCount(well 级可空覆盖,对齐 sql 迁移 :63-64),moveDownLayer 复用既有。写入用 Db.Updateable<HouseWellSettingDB>()(按 tlSn/houseSn/wellSn 更新对应 well 行)。FocusLayerRawConfig 时 well 级非空 → §2.5 就近优先取 well 覆盖(M2-02 Resolve 已实现),实现"下次该 well 沿用"。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/清理
IncludeFields=true 保留。