Эх сурвалжийг харах

chore: 加偶发对焦问题注释 + 自动对焦流程图(HTML) + 清理测试数据/工具/docs

- CoarseFocus加已知偶发问题注释(74000伪峰,复现不了,留待下次现场捕获)
- 新增 自动对焦流程图.html(完整流程+各阶段真实参数+限位+算法+协议)
- 删除 docs/ WellSpacing/ ToPng/ TestData/ Logs/ 及临时图,清理csproj失效排除行

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
huangjie 1 долоо хоног өмнө
parent
commit
fadf48be3d

+ 0 - 4
AutoFocusTool.csproj

@@ -21,10 +21,6 @@
     <Compile Remove="CalibTest\**\*.cs" />
     <Compile Remove="Calibrate\**\*.cs" />
     <Compile Remove="HScan\**\*.cs" />
-    <Compile Remove="ToPng\**\*.cs" />
-    <Compile Remove="WellSpacing\**\*.cs" />
-    <Compile Remove="Logs\**\*.cs" />
-    <Compile Remove="TestData\**\*.cs" />
     <Compile Remove="bin\**\*.cs" />
     <Compile Remove="obj\**\*.cs" />
     <Page Remove="SelfTest\**" />

+ 15 - 0
Calib/CalibrationEngine.cs

@@ -317,6 +317,21 @@ namespace AutoFocusTool.Calib
         /// <summary>
         /// 粗对焦:固定中心 ZCoarseCenter±ZCoarseHalf 按 ZCoarseStep 扫描,中央40% ROI 找焦点。
         /// 实测所有 well 焦面集中在 60000~120000,故用固定大窗口,不依赖 EEPROM 零点。
+        ///
+        /// 【已知偶发问题 2026-06-16,待复现】偶发某个 well 粗对焦选中 ~74000 的伪峰(真实焦面应在 ~86000-92000),
+        /// 导致精对焦被锁在 74000±FineZHalf 的小窗口、最终对焦模糊,该 well 输出弱峰(max/mean≈1.0)。
+        /// 现象特征:
+        ///   · 同一批标定里只有 1 个 well 坏,坏值固定在 ~74000,但坏的 well 编号在不同批次间变化
+        ///     (曾出现 well1 坏、well8 坏,非固定孔位、非“第一个标定的 well”)。
+        ///   · 人眼在对焦过程能看到清晰画面,但最终选出的不是它。
+        /// 已排除的假设(真机 WellSpacing zcurve/zcurvefar 多次实测均无法复现):
+        ///   · 串口大行程移动未到位(残留位+短延时复现 → 峰仍正确 92000)
+        ///   · 相机/光源冷启动预热(冷启动立即扫 → 峰仍正确 92000)
+        ///   · 清晰度算法(已修 ÷mean,其余 15 well 全部正确)
+        ///   · “第一个 well”时序(数据证明 well1 常常正常,坏的在序列中间)
+        /// 当前最可疑但未坐实:偶发时序导致某层电机未真正到位/画面在运动中被采样,在 74000 形成伪高频峰;
+        /// 或该次该孔在 74000 处恰有杂质/反光的真实但“错误”的清晰特征。
+        /// 复现率低,本轮多次测试未触发,留待下次现场捕获(建议届时开 DebugSave 存粗对焦每层图比对)。
         /// </summary>
         int CoarseFocus(int well)
         {

+ 0 - 123
ToPng/ToPng.cs

@@ -1,123 +0,0 @@
-using System;
-using System.IO;
-using System.Drawing;
-using System.Drawing.Imaging;
-using System.Runtime.InteropServices;
-using AutoFocusTool.Imaging;
-
-namespace AutoFocusTool
-{
-    /// <summary>
-    /// BMP→小PNG 诊断转换器(不依赖相机/串口,纯 System.Drawing)。
-    /// 把相机 15MB 的 24bpp BMP 缩成小 PNG 供肉眼/AI 视觉检查,并:
-    ///   - 跑 WellDetector 把"检测器眼里的圆"画上去(绿圆+圆心十字)
-    ///   - 画画面中心十字(白)+ 居中容差带(±12% 高度,黄色虚拟区,用上下两条线表示)
-    ///   - stdout 打印检测数值(圆心/半径/偏移/完整/拒绝原因)
-    /// 用法:ToPng.exe <输入.bmp 或 目录> [缩放宽度=900] [--nodetect]
-    /// 注意:磁盘上的 BMP 已被 FlipY 翻正(人眼朝向),WellDetector 在此朝向上跑,
-    ///       得到的圆位置即人眼所见;Y偏移符号与引擎内部(未翻转)相反,但位置/半径一致。
-    /// </summary>
-    internal class ToPng
-    {
-        static void Main(string[] args)
-        {
-            Console.OutputEncoding = System.Text.Encoding.UTF8;
-            if (args.Length == 0) { Console.WriteLine("用法: ToPng <bmp或目录> [宽度=900] [--nodetect]"); return; }
-
-            string input = args[0];
-            int targetW = 900;
-            bool detect = true;
-            for (int i = 1; i < args.Length; i++)
-            {
-                if (args[i] == "--nodetect") detect = false;
-                else if (int.TryParse(args[i], out int w)) targetW = w;
-            }
-
-            if (Directory.Exists(input))
-            {
-                foreach (var f in Directory.GetFiles(input, "*.bmp"))
-                    Convert(f, targetW, detect);
-            }
-            else if (File.Exists(input))
-            {
-                Convert(input, targetW, detect);
-            }
-            else Console.WriteLine($"找不到: {input}");
-        }
-
-        static void Convert(string bmpPath, int targetW, bool detect)
-        {
-            try
-            {
-                using var src = new Bitmap(bmpPath);
-                int W = src.Width, H = src.Height;
-                string outPath = Path.ChangeExtension(bmpPath, ".png");
-                if (bmpPath.EndsWith("_v.png", StringComparison.OrdinalIgnoreCase)) return; // 跳过已生成的
-
-                // 取 BGR24 原始字节供 WellDetector
-                WellCircle c = null;
-                if (detect)
-                {
-                    byte[] bgr = ToBgr24(src, W, H);
-                    c = WellDetector.Detect(bgr, W, H);
-                    Console.WriteLine($"{Path.GetFileName(bmpPath)}: " +
-                        (c.Found
-                          ? $"圆心({c.Cx:F0},{c.Cy:F0}) R={c.Radius:F0} 偏移X={c.OffsetXPct:F1}% Y={c.OffsetYPct:F1}% 完整={c.Complete} 面积={c.AreaPct:F1}% aspect={c.Aspect:F2} boxFill={c.BoxFill:F2} rConsist={c.RConsist:F2}"
-                          : $"未检出圆 [{c.RejectReason}] 面积={c.AreaPct:F1}%"));
-                }
-
-                // 叠加标注(在全分辨率上画,再缩放,线条清晰)
-                using (var g = Graphics.FromImage(src))
-                {
-                    // 画面中心十字(白)
-                    var cxp = W / 2f; var cyp = H / 2f;
-                    using var wp = new Pen(Color.White, 3);
-                    g.DrawLine(wp, cxp - 40, cyp, cxp + 40, cyp);
-                    g.DrawLine(wp, cxp, cyp - 40, cxp, cyp + 40);
-                    // 居中容差带(±12% 高度)黄色横线
-                    using var yp = new Pen(Color.Yellow, 2) { DashStyle = System.Drawing.Drawing2D.DashStyle.Dash };
-                    float tol = 0.12f * H;
-                    g.DrawLine(yp, 0, cyp - tol, W, cyp - tol);
-                    g.DrawLine(yp, 0, cyp + tol, W, cyp + tol);
-
-                    if (c != null && c.Found)
-                    {
-                        using var gp = new Pen(Color.Lime, 4);
-                        g.DrawEllipse(gp, (float)(c.Cx - c.Radius), (float)(c.Cy - c.Radius),
-                            (float)(c.Radius * 2), (float)(c.Radius * 2));
-                        // 检测圆心(红十字)
-                        using var rp = new Pen(Color.Red, 3);
-                        g.DrawLine(rp, (float)c.Cx - 30, (float)c.Cy, (float)c.Cx + 30, (float)c.Cy);
-                        g.DrawLine(rp, (float)c.Cx, (float)c.Cy - 30, (float)c.Cx, (float)c.Cy + 30);
-                    }
-                }
-
-                int targetH = (int)((long)targetW * H / W);
-                using var dst = new Bitmap(targetW, targetH);
-                using (var g = Graphics.FromImage(dst))
-                {
-                    g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
-                    g.DrawImage(src, 0, 0, targetW, targetH);
-                }
-                dst.Save(outPath, ImageFormat.Png);
-                Console.WriteLine($"  → {Path.GetFileName(outPath)} ({targetW}x{targetH})");
-            }
-            catch (Exception ex) { Console.WriteLine($"{bmpPath} 转换失败: {ex.Message}"); }
-        }
-
-        /// <summary>System.Drawing.Bitmap → 24bpp BGR 行优先字节(WellDetector 输入格式)。</summary>
-        static byte[] ToBgr24(Bitmap bmp, int W, int H)
-        {
-            var clone = bmp.PixelFormat == PixelFormat.Format24bppRgb ? bmp
-                       : bmp.Clone(new Rectangle(0, 0, W, H), PixelFormat.Format24bppRgb);
-            var data = clone.LockBits(new Rectangle(0, 0, W, H), ImageLockMode.ReadOnly, PixelFormat.Format24bppRgb);
-            byte[] buf = new byte[W * H * 3];
-            int rowBytes = W * 3;
-            for (int y = 0; y < H; y++)
-                Marshal.Copy(data.Scan0 + y * data.Stride, buf, y * rowBytes, rowBytes);
-            clone.UnlockBits(data);
-            if (!ReferenceEquals(clone, bmp)) clone.Dispose();
-            return buf;
-        }
-    }
-}

+ 0 - 20
ToPng/ToPng.csproj

@@ -1,20 +0,0 @@
-<Project Sdk="Microsoft.NET.Sdk">
-  <PropertyGroup>
-    <OutputType>Exe</OutputType>
-    <TargetFramework>net8.0-windows</TargetFramework>
-    <Nullable>disable</Nullable>
-    <ImplicitUsings>disable</ImplicitUsings>
-    <Platforms>x64</Platforms>
-    <PlatformTarget>x64</PlatformTarget>
-    <AssemblyName>ToPng</AssemblyName>
-    <RootNamespace>AutoFocusTool</RootNamespace>
-    <EnableDefaultCompileItems>false</EnableDefaultCompileItems>
-  </PropertyGroup>
-  <ItemGroup>
-    <PackageReference Include="System.Drawing.Common" Version="8.0.0" />
-  </ItemGroup>
-  <ItemGroup>
-    <Compile Include="..\Imaging\WellDetector.cs" />
-    <Compile Include="ToPng.cs" />
-  </ItemGroup>
-</Project>

+ 0 - 173
WellSpacing/WellSpacing.cs

@@ -1,173 +0,0 @@
-using System;
-using System.IO;
-using System.Threading;
-using AutoFocusTool.Serial;
-using AutoFocusTool.Imaging;
-using SerialCamera = AutoFocusTool.Camera.Camera;
-
-namespace AutoFocusTool
-{
-    /// <summary>
-    /// 只读诊断工具:逐个 well 移动到其 EEPROM 水平位置 → 抓帧 → 跑 WellDetector → 存图。
-    /// 用于验证“每个 well 移动过去后相机实际看到的是不是对应的孔、偏差多少”。
-    /// 不做对焦/不改标定,只在固定 Z(传入或默认)和固定曝光下拍一张。
-    ///
-    /// 用法:WellSpacing.exe [COM口=COM11] [相机index=2] [曝光=80] [Z=93000]
-    /// 存图目录:calib_result\well_check_yyyyMMdd_HHmmss\
-    /// </summary>
-    internal class WellSpacing
-    {
-        [STAThread]
-        static int Main(string[] args)
-        {
-            Console.OutputEncoding = System.Text.Encoding.UTF8;
-            if (args.Length > 0 && args[0] == "zcurve")
-                return ZCurve(args);
-            string port = args.Length > 0 ? args[0] : "COM11";
-            int camIdx = args.Length > 1 && int.TryParse(args[1], out int ci) ? ci : 2;
-            int exposure = args.Length > 2 && int.TryParse(args[2], out int ex) ? ex : 80;
-            int zPos = args.Length > 3 && int.TryParse(args[3], out int z) ? z : 93000;
-
-            int camW = 2592, camH = 1944;
-            void L(string m) => Console.WriteLine($"{DateTime.Now:HH:mm:ss} {m}");
-
-            string stamp = DateTime.Now.ToString("yyyyMMdd_HHmmss");
-            string outDir = $@"C:\claudeFile\TL\AutoFocusTool\TestData\well_check_{stamp}";
-            Directory.CreateDirectory(outDir);
-
-            L($"========== well 实拍检测  {port}  cam#{camIdx}  曝光{exposure}  Z{zPos} ==========");
-            L($"存图目录: {outDir}");
-
-            var motor = new HouseMotor(port) { Log = null };
-            if (!motor.Open()) { L($"✗ 打开 {port} 失败(串口被占用?)"); return 1; }
-            motor.MotorDelayMs = 1500;
-            int sn = motor.ShakeHands();
-            L($"握手 houseSn = {sn}");
-
-            var cam = new SerialCamera(camIdx, camW, camH, exposure);
-            int init = cam.Init();
-            if (init != 0) { L($"✗ 相机#{camIdx} 初始化失败 code={init}"); motor.Close(); return 2; }
-            cam.SetOpMode(0);
-            cam.SetExposure(exposure);
-            motor.OpenLED();
-            Thread.Sleep(300);
-
-            // 先到统一对焦 Z(所有 well 用同一 Z,只看水平是否对到不同孔)
-            motor.VerticalMoveTo(zPos, 1500);
-
-            try
-            {
-                for (int w = 1; w <= 16; w++)
-                {
-                    int hpos = motor.ReadWellHorizontalPos(w);
-                    if (hpos < 0) { L($"well{w,2}: 读EEPROM位置失败,跳过"); continue; }
-
-                    motor.HorizontalMoveTo(hpos, 1500);
-                    Thread.Sleep(200);
-                    cam.GrabRgb();             // 丢弃第一帧
-                    Thread.Sleep(100);
-                    cam.GrabRgb();
-                    byte[] buf = cam.GetSourceBuffer();
-
-                    var c = WellDetector.Detect(buf, camW, camH);
-                    string tag = c.Found
-                        ? $"圆心({c.Cx:F0},{c.Cy:F0}) R={c.Radius:F0} 偏移X={c.OffsetXPct:F1}% Y={c.OffsetYPct:F1}% 完整={c.Complete}"
-                        : $"未检出圆[{c.RejectReason}]";
-                    L($"well{w,2}: EEPROM水平={hpos,7}  {tag}");
-
-                    string detTag = c.Found ? (c.Complete ? "OK" : "partial") : "NOCIRCLE";
-                    string path = Path.Combine(outDir, $"well{w:D2}_hp{hpos}_{detTag}.bmp");
-                    ImageConverter.SaveBmp(buf, camW, camH, path);
-                }
-            }
-            finally
-            {
-                motor.CloseLED();
-                cam.Dispose();
-                motor.Close();
-            }
-
-            L($"========== 完成,图片在: {outDir} ==========");
-            return 0;
-        }
-
-        /// <summary>
-        /// Z清晰度曲线扫描:移到指定well的EEPROM水平位置,从zLo到zHi逐层抓帧算清晰度,
-        /// 同时记录灰度均值(验证亮度归一化是否带偏),每层存图。
-        /// 用法:WellSpacing.exe zcurve <COM> <相机idx> <well> [zLo=20000] [zHi=120000] [zStep=2000] [曝光=60]
-        /// </summary>
-        static int ZCurve(string[] a)
-        {
-            string port = a.Length > 1 ? a[1] : "COM11";
-            int camIdx = a.Length > 2 && int.TryParse(a[2], out int ci) ? ci : 2;
-            int well = a.Length > 3 && int.TryParse(a[3], out int wv) ? wv : 1;
-            int zLo = a.Length > 4 && int.TryParse(a[4], out int v4) ? v4 : 20000;
-            int zHi = a.Length > 5 && int.TryParse(a[5], out int v5) ? v5 : 120000;
-            int zStep = a.Length > 6 && int.TryParse(a[6], out int v6) ? v6 : 2000;
-            int exposure = a.Length > 7 && int.TryParse(a[7], out int v7) ? v7 : 60;
-            int camW = 2592, camH = 1944;
-            void L(string m) => Console.WriteLine($"{DateTime.Now:HH:mm:ss} {m}");
-
-            string stamp = DateTime.Now.ToString("yyyyMMdd_HHmmss");
-            string outDir = $@"C:\claudeFile\TL\AutoFocusTool\TestData\zcurve_w{well}_{stamp}";
-            Directory.CreateDirectory(outDir);
-            L($"==== Z清晰度曲线 well{well}  {port} cam#{camIdx}  Z[{zLo},{zHi}] step{zStep} 曝光{exposure} ====");
-            L($"存图: {outDir}");
-
-            var motor = new HouseMotor(port) { Log = null };
-            if (!motor.Open()) { L($"✗ 打开 {port} 失败"); return 1; }
-            motor.MotorDelayMs = 1500;
-            int sn = motor.ShakeHands();
-            L($"握手 houseSn={sn}");
-            int hpos = motor.ReadWellHorizontalPos(well);
-            if (hpos < 0) { L("✗ 读well水平位置失败"); motor.Close(); return 1; }
-
-            var cam = new SerialCamera(camIdx, camW, camH, exposure);
-            if (cam.Init() != 0) { L("✗ 相机初始化失败"); motor.Close(); return 2; }
-            cam.SetOpMode(0); cam.SetExposure(exposure);
-            motor.OpenLED(); Thread.Sleep(300);
-            motor.HorizontalMoveTo(hpos, 1500);
-            L($"已移到 well{well} 水平={hpos}");
-
-            double bestScore = -1; int bestZ = zLo;
-            try
-            {
-                for (int z = zLo; z <= zHi; z += zStep)
-                {
-                    motor.VerticalMoveTo(z, 1500);
-                    Thread.Sleep(150);
-                    cam.GrabRgb(); Thread.Sleep(80); cam.GrabRgb();
-                    byte[] buf = cam.GetSourceBuffer();
-
-                    int roiW = (int)(camW * 0.4), roiH = (int)(camH * 0.4);
-                    var roi = new System.Drawing.Rectangle((camW - roiW) / 2, (camH - roiH) / 2, roiW, roiH);
-                    double score = Sharpness.Compute(buf, camW, camH, roi);
-                    double mean = GrayMean(buf, camW, camH, roi);
-                    L($"z={z,6}  归一化分={score:F5}  亮度均值={mean:F1}");
-                    if (score > bestScore) { bestScore = score; bestZ = z; }
-
-                    ImageConverter.SaveBmp(buf, camW, camH,
-                        Path.Combine(outDir, $"z{z}_s{score:F4}.bmp"));
-                }
-            }
-            finally { motor.CloseLED(); cam.Dispose(); motor.Close(); }
-
-            L($"==== 峰值 z={bestZ} 分={bestScore:F5}  图在 {outDir} ====");
-            return 0;
-        }
-
-        /// <summary>中央ROI灰度均值(BGR→灰度,行优先)。</summary>
-        static double GrayMean(byte[] bgr, int W, int H, System.Drawing.Rectangle roi)
-        {
-            long sum = 0; int n = 0; int stride = W * 3;
-            for (int y = roi.Y; y < roi.Y + roi.Height && y < H; y++)
-                for (int x = roi.X; x < roi.X + roi.Width && x < W; x++)
-                {
-                    int p = y * stride + x * 3;
-                    sum += (bgr[p] * 29 + bgr[p + 1] * 150 + bgr[p + 2] * 77) >> 8;
-                    n++;
-                }
-            return n > 0 ? (double)sum / n : 0;
-        }
-    }
-}

+ 0 - 41
WellSpacing/WellSpacing.csproj

@@ -1,41 +0,0 @@
-<Project Sdk="Microsoft.NET.Sdk">
-
-  <PropertyGroup>
-    <OutputType>Exe</OutputType>
-    <TargetFramework>net8.0-windows</TargetFramework>
-    <Nullable>disable</Nullable>
-    <ImplicitUsings>disable</ImplicitUsings>
-    <UseWPF>true</UseWPF>
-    <Platforms>x64</Platforms>
-    <PlatformTarget>x64</PlatformTarget>
-    <AssemblyName>WellSpacing</AssemblyName>
-    <RootNamespace>AutoFocusTool</RootNamespace>
-    <EnableDefaultCompileItems>false</EnableDefaultCompileItems>
-  </PropertyGroup>
-
-  <ItemGroup>
-    <PackageReference Include="System.IO.Ports" Version="8.0.0" />
-    <PackageReference Include="System.Drawing.Common" Version="8.0.0" />
-  </ItemGroup>
-
-  <ItemGroup>
-    <Compile Include="..\Serial\Protocol.cs" />
-    <Compile Include="..\Serial\SerialMotor.cs" />
-    <Compile Include="..\Serial\HouseMotor.cs" />
-    <Compile Include="..\Camera\MVCAPI.cs" />
-    <Compile Include="..\Camera\CapInfoStruct.cs" />
-    <Compile Include="..\Camera\Camera.cs" />
-    <Compile Include="..\Imaging\WellDetector.cs" />
-    <Compile Include="..\Imaging\Sharpness.cs" />
-    <Compile Include="..\Imaging\ImageConverter.cs" />
-    <Compile Include="WellSpacing.cs" />
-  </ItemGroup>
-
-  <ItemGroup>
-    <None Include="..\DependFile\ccd\*.dll">
-      <Link>DependFile\ccd\%(Filename)%(Extension)</Link>
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </None>
-  </ItemGroup>
-
-</Project>

+ 0 - 442
docs/superpowers/plans/2026-06-16-autofocus-scan-range.md

@@ -1,442 +0,0 @@
-# AutoFocus 扫描范围扩大与电机限位 — 实现计划
-
-> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
-
-**Goal:** 扩大 AutoFocus 标定的水平找圆与 Z 对焦扫描范围,并对所有电机移动加行程限位,解决"well 没转进画面导致整段检不出圆"的问题。
-
-**Architecture:** 全部改动集中在 `Calib/CalibrationEngine.cs` 单文件。新增可配置参数字段 + 两个钳位方法 + 一个全行程水平定位方法;修改 `CoarseFocus`(固定大窗口)、`CalibrateWell`(调用新定位、扩精对焦半幅)。不改协议层与电机接口。
-
-**Tech Stack:** .NET 8 / WPF / C#。无单元测试框架——验证靠 `dotnet build` 编译 + 一个纯逻辑钳位自检(写进 `SelfTest` 控制台工程)+ 真机标定验证。
-
-设计依据:`docs/superpowers/specs/2026-06-16-autofocus-scan-range-design.md`
-
----
-
-## 文件结构
-
-- 修改:`Calib/CalibrationEngine.cs`
-  - 新增字段(限位 + 水平全行程粗扫 + Z 大窗口粗对焦 + 精对焦半幅/步距)
-  - 新增 `ClampH(int)`、`ClampZ(int)` 私有方法
-  - 新增 `HCoarseLocate(int well)` 方法(全行程粗扫定位 + 局部密扫)
-  - 改 `CoarseFocus`:固定中心 90000、半幅 30000、步距 2000
-  - 改 `CalibrateWell`:用 `HCoarseLocate` 替换 ②首次 `ScanForCenter`;精对焦半幅改 `FineZHalf`、步距改 `FineZStep`
-  - 所有 `_motor.HorizontalMoveTo` 经 `ClampH`,`_motor.VerticalMoveTo` 经 `ClampZ`
-
-无新增文件。`ClampH`/`ClampZ` 是两行 `Math.Max/Min`,逻辑通过编译 + 真机日志验证(越界写 Log),不单独造测试文件(无测试框架,`SelfTest` Main 需硬件)。
-
----
-
-## Task 1: 新增参数字段 + 钳位方法
-
-**Files:**
-- Modify: `Calib/CalibrationEngine.cs:27-43`(在现有参数字段区追加新字段)
-- Modify: `Calib/CalibrationEngine.cs:51`(在 `CenterRoi40` 之前新增钳位方法)
-
-- [ ] **Step 1: 追加新参数字段**
-
-在 `CalibrationEngine.cs` 第 43 行 `public int CenterScanExposure = 60;` 之后插入:
-
-```csharp
-
-        // ── 行程限位(所有电机移动前钳到该区间)──
-        /// <summary>水平电机行程下/上限脉冲(旧工程自检值 70000)。</summary>
-        public int HMin = 0, HMax = 70000;
-        /// <summary>垂直电机行程下/上限脉冲(旧工程软上限 125000)。</summary>
-        public int ZMin = 0, ZMaxPulse = 125000;
-
-        // ── 水平全行程粗扫定位 ──
-        /// <summary>水平全行程粗扫起点/终点/步距。命中完整圆即停。</summary>
-        public int HCoarseStart = 0;
-        public int HCoarseEnd = 70000;
-        public int HCoarseStep = 2000;
-
-        // ── Z 全范围粗对焦(固定中心大窗口)──
-        /// <summary>Z 粗对焦固定中心(实测焦面集中区间 60000~120000 的中点)。</summary>
-        public int ZCoarseCenter = 90000;
-        /// <summary>Z 粗对焦半幅 → 区间 60000~120000。</summary>
-        public int ZCoarseHalf = 30000;
-        /// <summary>Z 粗对焦步距 → 约 31 层。</summary>
-        public int ZCoarseStep = 2000;
-
-        // ── Z 精对焦(围绕粗峰)──
-        /// <summary>精对焦半幅(覆盖粗扫 ±2000 峰定位误差并留余量)。</summary>
-        public int FineZHalf = 6000;
-        /// <summary>精对焦步距 → 约 24 层(精度优先)。</summary>
-        public int FineZStep = 500;
-```
-
-- [ ] **Step 2: 新增钳位方法**
-
-在 `CalibrationEngine.cs` 第 51 行 `/// <summary>中央40% ROI...` 注释之前插入:
-
-```csharp
-        /// <summary>水平脉冲钳到 [HMin,HMax],越界写 Log。</summary>
-        int ClampH(int p)
-        {
-            int c = Math.Max(HMin, Math.Min(HMax, p));
-            if (c != p) Log?.Invoke($"  ⚠ 水平脉冲 {p} 越界,钳到 {c} [{HMin},{HMax}]");
-            return c;
-        }
-
-        /// <summary>垂直脉冲钳到 [ZMin,ZMaxPulse],越界写 Log。</summary>
-        int ClampZ(int p)
-        {
-            int c = Math.Max(ZMin, Math.Min(ZMaxPulse, p));
-            if (c != p) Log?.Invoke($"  ⚠ 垂直脉冲 {p} 越界,钳到 {c} [{ZMin},{ZMaxPulse}]");
-            return c;
-        }
-
-```
-
-- [ ] **Step 3: 编译验证**
-
-Run: `dotnet build AutoFocusTool.csproj -c Debug`
-Expected: 生成成功,0 错误(新字段/方法暂未被调用,会有 0 或仅"未使用"提示,可忽略)
-
-- [ ] **Step 4: 提交**
-
-```bash
-git add Calib/CalibrationEngine.cs
-git commit -m "feat(calib): 新增扫描范围参数与电机限位钳位方法
-
-Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
-```
-
----
-
-## Task 2: 改 CoarseFocus 为固定大窗口 + 钳位
-
-**Files:**
-- Modify: `Calib/CalibrationEngine.cs:280-300`(整个 `CoarseFocus` 方法)
-
-当前 `CoarseFocus(int well, int centerZ, int half, int layers)` 以传入的 EEPROM 零点为中心、半幅 `ZHalf=1500`、层数 `CoarseFocusLayers=7`。改为固定中心 `ZCoarseCenter=90000`、半幅 `ZCoarseHalf=30000`、步距 `ZCoarseStep=2000`,并对每个 Z 目标钳位。
-
-- [ ] **Step 1: 替换 CoarseFocus 方法体**
-
-将 `CalibrationEngine.cs:280-300` 的整个方法(从 `int CoarseFocus(...)` 到其结尾 `}`)替换为:
-
-```csharp
-        int CoarseFocus(int well)
-        {
-            int lo = ZCoarseCenter - ZCoarseHalf;
-            int hi = ZCoarseCenter + ZCoarseHalf;
-            int bestZ = ZCoarseCenter; double bestS = -1;
-
-            // P0-6: 粗对焦使用中央40%区域ROI,避免背景干扰
-            var centerROI = CenterRoi40();
-
-            int layers = 0;
-            for (int z = lo; z <= hi; z += ZCoarseStep)
-            {
-                layers++;
-                _motor.VerticalMoveTo(ClampZ(z), ScanDelayMs);
-                // P0-5: 丢弃旧帧
-                Grab();
-                var b = Grab();
-                double sc = Sharpness.Compute(b, W, H, centerROI); // 中央ROI(避免背景带偏)
-                if (sc > bestS) { bestS = sc; bestZ = z; }
-                OnStep?.Invoke($"粗对焦 Z={z} (区间{lo}~{hi})", null, null);
-            }
-            Log?.Invoke($"[well{well}] 粗对焦扫{layers}层 区间[{lo},{hi}] 步距{ZCoarseStep}");
-            return bestZ;
-        }
-```
-
-注意:方法签名由 `CoarseFocus(int well, int centerZ, int half, int layers)` 改为 `CoarseFocus(int well)`——调用方在 Task 4 同步修改。同时更新方法上方的 XML 注释(`CalibrationEngine.cs:275-279`),把"围绕 centerZ±half"改为"固定中心 ZCoarseCenter±ZCoarseHalf":
-
-```csharp
-        /// <summary>
-        /// 粗对焦:固定中心 ZCoarseCenter±ZCoarseHalf 按 ZCoarseStep 扫描,中央40% ROI 找焦点。
-        /// 实测所有 well 焦面集中在 60000~120000,故用固定大窗口,不依赖 EEPROM 零点。
-        /// </summary>
-```
-
-- [ ] **Step 2: 编译验证(预期此时报错)**
-
-Run: `dotnet build AutoFocusTool.csproj -c Debug`
-Expected: FAIL — `CalibrateWell` 仍按旧签名调用 `CoarseFocus(well, eepromZ, ZHalf, CoarseFocusLayers)`,报"无重载匹配"。这是预期的,Task 4 修复调用方。
-
-> 若希望每个 Task 都能独立编译通过,可将 Task 2、4 合并为一次提交执行。本计划按"先改被调方再改调用方"的顺序,最终在 Task 4 末尾统一编译通过。
-
-- [ ] **Step 3: 暂不提交,继续 Task 3、4**
-
-(CoarseFocus 与调用方需一起编译通过后再提交,见 Task 4 Step 末尾)
-
----
-
-## Task 3: 新增 HCoarseLocate(水平全行程粗扫定位 + 局部密扫)+ ScanForCenter 钳位
-
-**Files:**
-- Modify: `Calib/CalibrationEngine.cs:109-129`(`ScanForCenter` 内的移动加钳位)
-- Modify: `Calib/CalibrationEngine.cs:129`(在 `ScanForCenter` 方法后新增 `HCoarseLocate`)
-
-- [ ] **Step 1: ScanForCenter 的移动加钳位**
-
-将 `CalibrationEngine.cs:111-113` 这三行:
-
-```csharp
-                int hp = center - range + step * i;
-                if (hp < 0) continue;
-                _motor.HorizontalMoveTo(hp, actualDelay);
-```
-
-替换为(去掉 `if (hp < 0) continue;`,改用 ClampH 统一钳位):
-
-```csharp
-                int hp = ClampH(center - range + step * i);
-                _motor.HorizontalMoveTo(hp, actualDelay);
-```
-
-- [ ] **Step 2: 新增 HCoarseLocate 方法**
-
-在 `CalibrationEngine.cs` 第 129 行(`ScanForCenter` 方法的结尾 `}` 之后、`CalibrateWell` 之前)插入:
-
-```csharp
-        /// <summary>
-        /// 水平全行程粗扫定位 + 局部密扫居中。不依赖 EEPROM 水平位置准确:
-        /// ① 从 HCoarseStart 到 HCoarseEnd 按 HCoarseStep 扫,命中完整圆即停,记录该位置;
-        /// ② 以命中点为中心做局部密扫(ScanForCenter)优化 Y 居中;
-        /// ③ 全程未命中完整圆则取扫描中 |Y偏移| 最小且检出的位置;仍无返回 (-1,null)。
-        /// </summary>
-        (int bestHPos, WellCircle bestCircle) HCoarseLocate(int well)
-        {
-            _cam.SetExposure(CenterScanExposure);
-
-            int hitHPos = -1; WellCircle hitCircle = null;
-            int fallbackHPos = -1; WellCircle fallbackCircle = null;
-            double fallbackScore = double.MaxValue;
-
-            // ① 全行程粗扫,命中完整圆即停
-            for (int hp = HCoarseStart; hp <= HCoarseEnd; hp += HCoarseStep)
-            {
-                int p = ClampH(hp);
-                _motor.HorizontalMoveTo(p, ScanDelayMs);
-                var b = Grab();
-                var c = WellDetector.Detect(b, W, H);
-                DebugSave?.Invoke(b, $"hcoarse_w{well}_hp{p}");
-                Log?.Invoke(c.Found
-                    ? $"  粗扫水平{p}: Y偏移={c.OffsetYPct:F1}% 完整={c.Complete}"
-                    : $"  粗扫水平{p}: 未检出圆");
-                OnStep?.Invoke($"水平粗扫 hp={p}", c, null);
-                if (c.Found)
-                {
-                    double score = Math.Abs(c.OffsetYPct) + (c.Complete ? 0 : 100);
-                    if (score < fallbackScore) { fallbackScore = score; fallbackHPos = p; fallbackCircle = c; }
-                    if (c.Complete) { hitHPos = p; hitCircle = c; break; }
-                }
-            }
-
-            // 命中完整圆 → 局部密扫;否则用检出最优的降级点
-            int center = hitHPos >= 0 ? hitHPos : fallbackHPos;
-            if (center < 0)
-            {
-                Log?.Invoke($"[well{well}] ✗ 水平全行程未检出任何圆");
-                return (-1, null);
-            }
-
-            // ② 以命中/降级点为中心局部密扫居中(范围取粗扫步距量级)
-            int fineRange = HCoarseStep;
-            var fine = ScanForCenter(well, center, fineRange, FineScanSteps, 800);
-            if (fine.bestCircle != null) return (fine.bestHPos, fine.bestCircle);
-
-            // ③ 局部密扫没检出 → 回退到粗扫命中/降级结果
-            return (center, hitCircle ?? fallbackCircle);
-        }
-
-```
-
-- [ ] **Step 3: 暂不提交,继续 Task 4 后统一编译提交**
-
----
-
-## Task 4: 改 CalibrateWell(调用新定位、新粗对焦、扩精对焦半幅、全程钳位)
-
-**Files:**
-- Modify: `Calib/CalibrationEngine.cs:135-273`(`CalibrateWell` 方法多处)
-
-- [ ] **Step 1: 初始水平移动加钳位**
-
-将 `CalibrationEngine.cs:140` 这行:
-
-```csharp
-            if (!RetryMove(() => _motor.HorizontalMoveTo(eepromHPos, ScanDelayMs), $"well{well}初始水平"))
-```
-
-替换为:
-
-```csharp
-            if (!RetryMove(() => _motor.HorizontalMoveTo(ClampH(eepromHPos), ScanDelayMs), $"well{well}初始水平"))
-```
-
-- [ ] **Step 2: 粗对焦调用改新签名 + 钳位**
-
-将 `CalibrationEngine.cs:150-151`:
-
-```csharp
-            int coarseZ = CoarseFocus(well, eepromZ, ZHalf, CoarseFocusLayers);
-            _motor.VerticalMoveTo(coarseZ, ScanDelayMs);
-```
-
-替换为:
-
-```csharp
-            int coarseZ = CoarseFocus(well);
-            _motor.VerticalMoveTo(ClampZ(coarseZ), ScanDelayMs);
-```
-
-- [ ] **Step 3: ②居中改用 HCoarseLocate**
-
-将 `CalibrationEngine.cs:156-162`:
-
-```csharp
-            Log?.Invoke($"[well{well}] ②旋转居中(优化Y偏移)...");
-            var coarse = ScanForCenter(well, eepromHPos, HScanRange, HScanSteps);
-            int fineRange = Math.Max(300, 2 * HScanRange / Math.Max(1, HScanSteps - 1));
-            var fine = ScanForCenter(well, coarse.bestHPos, fineRange, FineScanSteps, 800);  // 细扫用800ms长延时确保检测准确
-
-            int bestHPos = fine.bestCircle != null ? fine.bestHPos : coarse.bestHPos;
-            WellCircle bestCircle = fine.bestCircle ?? coarse.bestCircle;
-```
-
-替换为:
-
-```csharp
-            Log?.Invoke($"[well{well}] ②水平全行程定位+居中...");
-            var located = HCoarseLocate(well);
-            if (located.bestHPos < 0)
-            {
-                Log?.Invoke($"[well{well}] ✗ 水平全行程未找到圆,跳过该well");
-                return new WellCalib { Well = well, Note = "水平全程未检出圆" };
-            }
-            int bestHPos = located.bestHPos;
-            WellCircle bestCircle = located.bestCircle;
-```
-
-- [ ] **Step 4: 居中后水平移动加钳位**
-
-将 `CalibrationEngine.cs:166`:
-
-```csharp
-            if (!RetryMove(() => _motor.HorizontalMoveTo(bestHPos, ScanDelayMs), $"well{well}居中水平"))
-```
-
-替换为:
-
-```csharp
-            if (!RetryMove(() => _motor.HorizontalMoveTo(ClampH(bestHPos), ScanDelayMs), $"well{well}居中水平"))
-```
-
-- [ ] **Step 5: 精对焦半幅/步距改用新字段 + 钳位**
-
-将 `CalibrationEngine.cs:211-217`:
-
-```csharp
-            int fineZHalf = Math.Max(200, ZHalf / 3);   // 精对焦围绕粗焦点小范围
-            int zstep = ZLayers > 1 ? 2 * fineZHalf / (ZLayers - 1) : 1;
-            var curve = new List<(int z, double s)>();
-            for (int i = 0; i < ZLayers; i++)
-            {
-                int z = Math.Max(0, coarseZ - fineZHalf) + zstep * i;
-                _motor.VerticalMoveTo(z, ScanDelayMs);
-```
-
-替换为(半幅用 `FineZHalf`,步距用 `FineZStep`,层数由半幅/步距算出,移动用 `ClampZ`):
-
-```csharp
-            int zstep = FineZStep > 0 ? FineZStep : 500;
-            int fineLayers = 2 * FineZHalf / zstep + 1;   // 半幅6000步距500 → 25层
-            var curve = new List<(int z, double s)>();
-            for (int i = 0; i < fineLayers; i++)
-            {
-                int z = coarseZ - FineZHalf + zstep * i;
-                _motor.VerticalMoveTo(ClampZ(z), ScanDelayMs);
-```
-
-- [ ] **Step 6: 精对焦循环内的层数显示与 OnStep 同步**
-
-将 `CalibrationEngine.cs:223`(精对焦 OnStep):
-
-```csharp
-                OnStep?.Invoke($"精对焦 {i + 1}/{ZLayers} Z={z} 分={sc:F4}", circle, null);
-```
-
-替换为:
-
-```csharp
-                OnStep?.Invoke($"精对焦 {i + 1}/{fineLayers} Z={z} 分={sc:F4}", circle, null);
-```
-
-- [ ] **Step 7: 最终对焦移动加钳位**
-
-将 `CalibrationEngine.cs:267`:
-
-```csharp
-            _motor.VerticalMoveTo(r.FocusZ, ScanDelayMs);
-```
-
-替换为:
-
-```csharp
-            _motor.VerticalMoveTo(ClampZ(r.FocusZ), ScanDelayMs);
-```
-
-- [ ] **Step 8: 编译验证(预期通过)**
-
-Run: `dotnet build AutoFocusTool.csproj -c Debug`
-Expected: 生成成功,0 错误。
-
-> 旧字段 `HScanRange`/`HScanSteps`/`ZHalf`/`ZLayers`/`CoarseFocusLayers` 现可能不再被引用,编译器不会报错(public 字段)。保留以兼容外部设置,无需删除。
-
-- [ ] **Step 9: 提交 Task 2~4**
-
-```bash
-git add Calib/CalibrationEngine.cs
-git commit -m "feat(calib): 水平全行程粗扫定位 + Z固定大窗口粗对焦 + 扩精对焦半幅
-
-- CoarseFocus 改固定中心90000±30000步距2000
-- 新增HCoarseLocate全行程粗扫(0~70000)命中完整圆即停+局部密扫
-- 精对焦半幅6000步距500, 所有电机移动经ClampH/ClampZ限位
-
-Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
-```
-
----
-
-## Task 5: 验证
-
-无单元测试框架,按以下方式验证。
-
-- [ ] **Step 1: 编译全解决方案**
-
-Run: `dotnet build AutoFocusTool.csproj -c Debug`
-Expected: 生成成功,0 错误。
-
-- [ ] **Step 2: 启动程序确认 UI 不崩溃**
-
-Run: `./bin/Debug/net8.0-windows/AutoFocusTool.exe`(后台启动)
-Expected: 主窗口正常显示。不点"自动初始化"则不触发相机(已知相机抓帧段错误不在本次范围)。
-
-- [ ] **Step 3: 真机验证(需连接设备,由用户执行)**
-
-清单:
-1. 扫描设备 → 连接相机+串口 → 勾选若干 well → 一键自动初始化。
-2. 观察标定窗口实时画面:水平粗扫阶段应能看到 well 圆转入画面并命中。
-3. 看日志确认:
-   - 出现 `粗扫水平{p}: ... 完整=True` 且命中即停。
-   - 出现 `粗对焦扫{N}层 区间[60000,120000] 步距2000`。
-   - 精对焦层数约 25 层。
-   - 若有越界,出现 `⚠ 水平脉冲 ... 越界` / `⚠ 垂直脉冲 ... 越界` 日志。
-4. 确认原先"找不到圆"的 well 现在能检出并完成标定。
-
-- [ ] **Step 4: 清理后台进程**
-
-Run: `tasklist | grep -i AutoFocus`,确认后停止测试进程。
-
----
-
-## Self-Review(计划自检结果)
-
-- **Spec 覆盖**:4.1参数→Task1;4.2钳位→Task1 + 各调用点(Task3/4);4.3水平全行程→Task3 + Task4 Step3;4.4 Z粗对焦→Task2 + Task4 Step2;4.5精对焦扩半幅→Task4 Step5;第7节错误处理(退化/钳位Log)→Task3 ③分支 + ClampH/Z;第8节测试→Task5。无遗漏。
-- **占位符**:无 TBD/TODO,每个代码步骤含完整代码。
-- **类型/签名一致性**:`CoarseFocus(int well)` Task2定义、Task4 Step2调用一致;`HCoarseLocate(int well)` 返回 `(int bestHPos, WellCircle bestCircle)`,Task4 Step3按此解构;新字段命名 `FineZHalf`/`FineZStep`/`ZCoarseCenter`/`ZCoarseHalf`/`ZCoarseStep`/`HCoarseStart`/`HCoarseEnd`/`HCoarseStep`/`HMin`/`HMax`/`ZMin`/`ZMaxPulse` 全计划一致。
-- **编译顺序**:Task2 改被调方后单独编译会失败(已注明),Task2~4 合并提交,Task4 Step8 编译通过——有意的"先改被调方再改调用方"顺序。

+ 0 - 594
docs/superpowers/plans/2026-06-16-diagnostics-ui-logging.md

@@ -1,594 +0,0 @@
-# 诊断/UI/日志 三子系统 实现计划
-
-> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
-
-**Goal:** 实现操作日志落盘、诊断工具 Z 清晰度曲线扫描、CalibWindow UI 数据显示调整三个独立子系统。
-
-**Architecture:** 三个子系统互相独立,按"日志 → 诊断 → UI"顺序实现(日志先做,后续调试受益)。日志用一个静态 `FileLogger` + 在 `MainWindow.Log` 和各按钮 Click 入口埋点;诊断扩展独立工具 `WellSpacing`;UI 改 `CalibWindow`。
-
-**Tech Stack:** .NET 8 / WPF / C#。无单元测试框架——验证靠 `dotnet build` 编译 + 运行观察 + 真机。
-
-设计依据:`docs/superpowers/specs/2026-06-16-diagnostics-ui-logging-design.md`
-
----
-
-## 文件结构
-
-- 新建:`Logging/FileLogger.cs` —— 静态文件日志器(启动建文件、线程安全写入)
-- 修改:`AutoFocusTool.csproj` —— 加 `Logs\**`、`TestData\**` 到 Compile Remove 排除
-- 修改:`MainWindow.xaml.cs` —— `Log()` 加落盘;构造时初始化 FileLogger
-- 修改:`MainWindow.Motor.cs` / `MainWindow.Camera.cs` / `MainWindow.Scan.cs` / `MainWindow.Calib.cs` —— 各按钮 Click 埋 `FileLogger.Action`
-- 修改:`WellSpacing/WellSpacing.cs` + `WellSpacing.csproj` —— 加 zcurve 模式、输出改 TestData/、引入 Sharpness
-- 修改:`CalibWindow.xaml` / `CalibWindow.xaml.cs` —— 删底部 Border、SetCurrentInfo 显示更详细
-- 修改:`MainWindow.Calib.cs` —— OnStep 回调补齐字段传给 CalibWindow
-
----
-
-## Task 1: FileLogger 静态日志器 + csproj 排除
-
-**Files:**
-- Create: `Logging/FileLogger.cs`
-- Modify: `AutoFocusTool.csproj`
-
-- [ ] **Step 1: 加 csproj 排除(Logs/TestData 不参与编译)**
-
-在 `AutoFocusTool.csproj` 中找到 `<Compile Remove="bin\**\*.cs" />` 这一行,在它之前插入两行:
-
-```xml
-    <Compile Remove="Logs\**\*.cs" />
-    <Compile Remove="TestData\**\*.cs" />
-```
-
-- [ ] **Step 2: 创建 FileLogger**
-
-创建 `Logging/FileLogger.cs`:
-
-```csharp
-using System;
-using System.IO;
-
-namespace AutoFocusTool.Logging
-{
-    /// <summary>
-    /// 静态文件日志器:程序启动时按时间戳建一个文件,本次运行所有日志写入它。
-    /// 线程安全(lock),每条 flush。格式:HH:mm:ss.fff  [级别]  [类别]  消息
-    /// </summary>
-    public static class FileLogger
-    {
-        private static readonly object _lock = new object();
-        private static string _path;
-
-        /// <summary>启动时调用一次:在 Logs/ 下建 yyyy-MM-dd_HHmmss.log。</summary>
-        public static void Init()
-        {
-            lock (_lock)
-            {
-                if (_path != null) return;
-                string dir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Logs");
-                // 实际运行时希望日志落到项目根的 Logs/,但 BaseDirectory 是 bin 输出目录;
-                // 用固定项目根更直观,便于查看。
-                dir = @"C:\claudeFile\TL\AutoFocusTool\Logs";
-                Directory.CreateDirectory(dir);
-                _path = Path.Combine(dir, $"{DateTime.Now:yyyy-MM-dd_HHmmss}.log");
-                Write("INFO", "APP", "==== 日志开始 ====");
-            }
-        }
-
-        public static void Info(string category, string msg) => Write("INFO", category, msg);
-        public static void Action(string category, string msg) => Write("ACTION", category, msg);
-        public static void Data(string category, string msg) => Write("DATA", category, msg);
-        public static void Warn(string category, string msg) => Write("WARN", category, msg);
-        public static void Error(string category, string msg) => Write("ERROR", category, msg);
-
-        private static void Write(string level, string category, string msg)
-        {
-            try
-            {
-                lock (_lock)
-                {
-                    if (_path == null) return;   // 未 Init 则静默丢弃
-                    string line = $"{DateTime.Now:HH:mm:ss.fff}  [{level,-6}] [{category,-6}] {msg}";
-                    File.AppendAllText(_path, line + Environment.NewLine);
-                }
-            }
-            catch { /* 日志失败不能影响主流程 */ }
-        }
-    }
-}
-```
-
-- [ ] **Step 3: 编译验证**
-
-Run: `dotnet build AutoFocusTool.csproj -c Debug`
-Expected: 生成成功,0 错误。(FileLogger 暂未被调用。)
-
-- [ ] **Step 4: 提交**
-
-```bash
-git add Logging/FileLogger.cs AutoFocusTool.csproj
-git commit -m "feat(log): 新增FileLogger静态文件日志器 + csproj排除Logs/TestData
-
-Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
-```
-
----
-
-## Task 2: MainWindow.Log 落盘 + 启动初始化
-
-**Files:**
-- Modify: `MainWindow.xaml.cs:35-54`(构造函数 + Log 方法)
-
-- [ ] **Step 1: 构造函数初始化 FileLogger**
-
-在 `MainWindow.xaml.cs` 找到构造函数:
-
-```csharp
-        public MainWindow()
-        {
-            InitializeComponent();
-            _scanner.Log = Log;
-            Closing += (s, e) => Cleanup();
-            Log("程序启动。先【扫描设备】,选舱室后【连接】。");
-        }
-```
-
-替换为(加 FileLogger.Init + using):
-
-```csharp
-        public MainWindow()
-        {
-            InitializeComponent();
-            AutoFocusTool.Logging.FileLogger.Init();
-            _scanner.Log = Log;
-            Closing += (s, e) => { AutoFocusTool.Logging.FileLogger.Info("APP", "程序关闭"); Cleanup(); };
-            Log("程序启动。先【扫描设备】,选舱室后【连接】。");
-        }
-```
-
-- [ ] **Step 2: Log() 同时落盘**
-
-找到 `Log` 方法:
-
-```csharp
-        private void Log(string msg)
-        {
-            void Append()
-            {
-                string line = $"{DateTime.Now:HH:mm:ss} {msg}\n";
-                TxtLog.AppendText(line);
-                TxtLog.ScrollToEnd();
-            }
-            if (Dispatcher.CheckAccess()) Append();
-            else Dispatcher.BeginInvoke((Action)Append);
-        }
-```
-
-替换为(追加一行落盘;类别统一 INFO/RUN,因 Log 是通用信息):
-
-```csharp
-        private void Log(string msg)
-        {
-            AutoFocusTool.Logging.FileLogger.Info("RUN", msg);
-            void Append()
-            {
-                string line = $"{DateTime.Now:HH:mm:ss} {msg}\n";
-                TxtLog.AppendText(line);
-                TxtLog.ScrollToEnd();
-            }
-            if (Dispatcher.CheckAccess()) Append();
-            else Dispatcher.BeginInvoke((Action)Append);
-        }
-```
-
-- [ ] **Step 2b: 新增 LogAction 辅助方法**
-
-在 `Log` 方法之后插入一个便捷方法(按钮埋点用,同时写界面+文件 ACTION 级):
-
-```csharp
-        /// <summary>记录界面操作(ACTION级落盘 + 界面日志)。</summary>
-        private void LogAction(string msg)
-        {
-            AutoFocusTool.Logging.FileLogger.Action("UI", msg);
-            void Append()
-            {
-                string line = $"{DateTime.Now:HH:mm:ss} ▶ {msg}\n";
-                TxtLog.AppendText(line);
-                TxtLog.ScrollToEnd();
-            }
-            if (Dispatcher.CheckAccess()) Append();
-            else Dispatcher.BeginInvoke((Action)Append);
-        }
-```
-
-- [ ] **Step 3: 编译验证**
-
-Run: `dotnet build AutoFocusTool.csproj -c Debug`
-Expected: 生成成功,0 错误。
-
-- [ ] **Step 4: 运行验证日志落盘**
-
-Run: `./bin/Debug/net8.0-windows/AutoFocusTool.exe`(启动后关闭),然后检查 `Logs/` 下生成了带时间戳的 .log 文件,内含"日志开始""程序启动"。
-
-- [ ] **Step 5: 提交**
-
-```bash
-git add MainWindow.xaml.cs
-git commit -m "feat(log): MainWindow.Log落盘 + 启动初始化 + LogAction埋点辅助
-
-Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
-```
-
----
-
-## Task 3: 各按钮 Click 埋点
-
-**Files:**
-- Modify: `MainWindow.Motor.cs`、`MainWindow.Camera.cs`、`MainWindow.Scan.cs`、`MainWindow.Calib.cs`
-
-原则:在每个按钮 `Click` 处理函数体**第一行**加 `LogAction(...)`,写明操作名+关键输入参数。`MotorAction("Z正转 X步", ...)` 这类已带名字的,其内部已经走 `Log` 落盘了,但按钮入口仍加 `LogAction` 记录"用户点击"这个动作和参数。
-
-- [ ] **Step 1: MainWindow.Motor.cs 埋点**
-
-`BtnZUp_Click`:在 `int step = ...` 之后、`MotorAction(...)` 之前加:
-```csharp
-            LogAction($"点击 Z正转, 步距={step}");
-```
-`BtnZDown_Click` 同理:
-```csharp
-            LogAction($"点击 Z反转, 步距={step}");
-```
-`BtnZReset_Click` 改为带函数体:
-```csharp
-        private void BtnZReset_Click(object sender, RoutedEventArgs e)
-        {
-            LogAction("点击 Z复位");
-            MotorAction("Z复位", () => _motor.VerticalReset());
-        }
-```
-`BtnZMoveTo_Click`:在 `int abs = ...` 之后加:
-```csharp
-            LogAction($"点击 Z绝对移动, 目标={abs}");
-```
-`BtnHFwd_Click`:在 `int step = ...` 之后加:
-```csharp
-            LogAction($"点击 水平正转, 步距={step}");
-```
-`BtnHBwd_Click`:在 `int step = ...` 之后加:
-```csharp
-            LogAction($"点击 水平反转, 步距={step}");
-```
-`BtnHReset_Click` 改为带函数体:
-```csharp
-        private void BtnHReset_Click(object sender, RoutedEventArgs e)
-        {
-            LogAction("点击 水平复位");
-            MotorAction("水平复位", () => _motor.HorizontalReset());
-        }
-```
-
-- [ ] **Step 2: MainWindow.Camera.cs 埋点**
-
-先 Read `MainWindow.Camera.cs` 确认按钮处理函数名(BtnLedOn/BtnLedOff/BtnGrab/TglLive/BtnSaveImg/BtnSetExp/BtnSetGain 等)。在每个 Click 函数体第一行加对应 `LogAction`,例如:
-```csharp
-        // BtnLedOn_Click 第一行
-        LogAction("点击 开光源");
-        // BtnLedOff_Click
-        LogAction("点击 关光源");
-        // BtnGrab_Click
-        LogAction("点击 抓一帧");
-        // TglLive_Click
-        LogAction($"点击 实时预览(切换)");
-        // BtnSaveImg_Click
-        LogAction("点击 存图");
-        // BtnSetExp_Click (在读取曝光值后)
-        LogAction($"点击 设置曝光={TxtExposure.Text}");
-        // BtnSetGain_Click (在读取增益后)
-        LogAction($"点击 设置增益 R={TxtGainR.Text} G={TxtGainG.Text} B={TxtGainB.Text}");
-```
-(按该文件实际函数名与变量调整文案;每个 Click 一行 LogAction。)
-
-- [ ] **Step 3: MainWindow.Scan.cs 埋点**
-
-Read `MainWindow.Scan.cs`,在 Z 扫描相关按钮(如 `BtnZScan_Click`、`BtnStop_Click`)函数体第一行加:
-```csharp
-        // BtnZScan_Click (读取起点/层距/层数后)
-        LogAction($"点击 Z扫描 起点={TxtScanStart.Text} 层距={TxtScanStep.Text} 层数={TxtScanCount.Text}");
-        // BtnStop_Click
-        LogAction("点击 停止扫描");
-```
-
-- [ ] **Step 4: MainWindow.Calib.cs / xaml.cs 其余按钮埋点**
-
-- `BtnScan_Click`(MainWindow.xaml.cs)第一行:`LogAction("点击 扫描设备");`
-- `BtnConnect_Click` 第一行:`LogAction($"点击 连接 舱室索引={CmbHouse.SelectedIndex}");`
-- `BtnDisconnect_Click` 第一行:`LogAction("点击 断开");`
-- `BtnGoWell_Click`(Calib)第一行:`LogAction($"点击 转到well={CmbWell.SelectedIndex + 1}");`
-- `BtnWellAll_Click`:`LogAction("点击 全选well");`
-- `BtnWellNone_Click`:`LogAction("点击 全不选well");`
-- `BtnAutoInit_Click` 第一行:`LogAction("点击 一键全自动初始化");`(具体 wells 列表在收集后已有 Log,可不重复)
-
-- [ ] **Step 5: 编译验证**
-
-Run: `dotnet build AutoFocusTool.csproj -c Debug`
-Expected: 生成成功,0 错误。
-
-- [ ] **Step 6: 提交**
-
-```bash
-git add MainWindow.Motor.cs MainWindow.Camera.cs MainWindow.Scan.cs MainWindow.Calib.cs MainWindow.xaml.cs
-git commit -m "feat(log): 各界面按钮Click埋点记录操作+参数
-
-Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
-```
-
----
-
-## Task 4: WellSpacing 加 zcurve 模式 + 输出改 TestData/
-
-**Files:**
-- Modify: `WellSpacing/WellSpacing.csproj`(引入 Sharpness.cs)
-- Modify: `WellSpacing/WellSpacing.cs`
-
-- [ ] **Step 1: csproj 引入 Sharpness**
-
-在 `WellSpacing/WellSpacing.csproj` 的 `<Compile Include="..\Imaging\WellDetector.cs" />` 之后加:
-```xml
-    <Compile Include="..\Imaging\Sharpness.cs" />
-```
-
-- [ ] **Step 2: 现有逐well模式输出目录改到 TestData/**
-
-在 `WellSpacing.cs` 找到:
-```csharp
-            string outDir = $@"C:\claudeFile\TL\AutoFocusTool\calib_result\well_check_{stamp}";
-```
-替换为:
-```csharp
-            string outDir = $@"C:\claudeFile\TL\AutoFocusTool\TestData\well_check_{stamp}";
-```
-
-- [ ] **Step 3: 加 zcurve 子命令**
-
-在 `WellSpacing.cs` 的 `Main` 方法最开头(`Console.OutputEncoding = ...` 之后)插入分支派发:
-
-```csharp
-            if (args.Length > 0 && args[0] == "zcurve")
-                return ZCurve(args);
-```
-
-然后在类内(`Main` 方法之后)新增 `ZCurve` 方法:
-
-```csharp
-        /// <summary>
-        /// Z清晰度曲线扫描:移到指定well的EEPROM水平位置,从zLo到zHi逐层抓帧算清晰度,
-        /// 同时记录灰度均值(验证亮度归一化是否带偏),每层存图。
-        /// 用法:WellSpacing.exe zcurve <COM> <相机idx> <well> [zLo=20000] [zHi=120000] [zStep=2000] [曝光=60]
-        /// </summary>
-        static int ZCurve(string[] a)
-        {
-            string port = a.Length > 1 ? a[1] : "COM11";
-            int camIdx = a.Length > 2 && int.TryParse(a[2], out int ci) ? ci : 2;
-            int well = a.Length > 3 && int.TryParse(a[3], out int wv) ? wv : 1;
-            int zLo = a.Length > 4 && int.TryParse(a[4], out int v4) ? v4 : 20000;
-            int zHi = a.Length > 5 && int.TryParse(a[5], out int v5) ? v5 : 120000;
-            int zStep = a.Length > 6 && int.TryParse(a[6], out int v6) ? v6 : 2000;
-            int exposure = a.Length > 7 && int.TryParse(a[7], out int v7) ? v7 : 60;
-            int camW = 2592, camH = 1944;
-            void L(string m) => Console.WriteLine($"{DateTime.Now:HH:mm:ss} {m}");
-
-            string stamp = DateTime.Now.ToString("yyyyMMdd_HHmmss");
-            string outDir = $@"C:\claudeFile\TL\AutoFocusTool\TestData\zcurve_w{well}_{stamp}";
-            Directory.CreateDirectory(outDir);
-            L($"==== Z清晰度曲线 well{well}  {port} cam#{camIdx}  Z[{zLo},{zHi}] step{zStep} 曝光{exposure} ====");
-            L($"存图: {outDir}");
-
-            var motor = new HouseMotor(port) { Log = null };
-            if (!motor.Open()) { L($"✗ 打开 {port} 失败"); return 1; }
-            motor.MotorDelayMs = 1500;
-            int sn = motor.ShakeHands();
-            L($"握手 houseSn={sn}");
-            int hpos = motor.ReadWellHorizontalPos(well);
-            if (hpos < 0) { L("✗ 读well水平位置失败"); motor.Close(); return 1; }
-
-            var cam = new SerialCamera(camIdx, camW, camH, exposure);
-            if (cam.Init() != 0) { L("✗ 相机初始化失败"); motor.Close(); return 2; }
-            cam.SetOpMode(0); cam.SetExposure(exposure);
-            motor.OpenLED(); Thread.Sleep(300);
-            motor.HorizontalMoveTo(hpos, 1500);
-            L($"已移到 well{well} 水平={hpos}");
-
-            double bestScore = -1; int bestZ = zLo;
-            try
-            {
-                for (int z = zLo; z <= zHi; z += zStep)
-                {
-                    motor.VerticalMoveTo(z, 1500);
-                    Thread.Sleep(150);
-                    cam.GrabRgb(); Thread.Sleep(80); cam.GrabRgb();
-                    byte[] buf = cam.GetSourceBuffer();
-
-                    // 中央40% ROI
-                    int roiW = (int)(camW * 0.4), roiH = (int)(camH * 0.4);
-                    var roi = new System.Drawing.Rectangle((camW - roiW) / 2, (camH - roiH) / 2, roiW, roiH);
-                    double score = Sharpness.Compute(buf, camW, camH, roi);
-                    double mean = GrayMean(buf, camW, camH, roi);
-                    L($"z={z,6}  归一化分={score:F5}  亮度均值={mean:F1}");
-                    if (score > bestScore) { bestScore = score; bestZ = z; }
-
-                    ImageConverter.SaveBmp(buf, camW, camH,
-                        Path.Combine(outDir, $"z{z}_s{score:F4}.bmp"));
-                }
-            }
-            finally { motor.CloseLED(); cam.Dispose(); motor.Close(); }
-
-            L($"==== 峰值 z={bestZ} 分={bestScore:F5}  图在 {outDir} ====");
-            return 0;
-        }
-
-        /// <summary>中央ROI灰度均值(BGR→灰度,行优先)。</summary>
-        static double GrayMean(byte[] bgr, int W, int H, System.Drawing.Rectangle roi)
-        {
-            long sum = 0; int n = 0; int stride = W * 3;
-            for (int y = roi.Y; y < roi.Y + roi.Height && y < H; y++)
-                for (int x = roi.X; x < roi.X + roi.Width && x < W; x++)
-                {
-                    int p = y * stride + x * 3;
-                    sum += (bgr[p] * 29 + bgr[p + 1] * 150 + bgr[p + 2] * 77) >> 8;
-                    n++;
-                }
-            return n > 0 ? (double)sum / n : 0;
-        }
-```
-
-- [ ] **Step 4: 编译验证**
-
-Run: `cd WellSpacing && dotnet build WellSpacing.csproj -c Debug`
-Expected: 生成成功,0 错误。
-
-- [ ] **Step 5: 提交**
-
-```bash
-git add WellSpacing/WellSpacing.cs WellSpacing/WellSpacing.csproj
-git commit -m "feat(diag): WellSpacing加Z清晰度曲线模式(记录分数+亮度均值)+输出改TestData
-
-Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
-```
-
-- [ ] **Step 6: 真机取证(用户执行,可选)**
-
-连 4 号舱跑:`WellSpacing.exe zcurve COM9 4 1 20000 120000 2000 60`
-观察输出:真实焦面 z 在哪、低 z 是否因亮度均值低而归一化分被抬高。这是后续决定是否改 Sharpness 的依据。
-
----
-
-## Task 5: CalibWindow UI 调整(删底部冗余 + 预览参数更详细)
-
-**Files:**
-- Modify: `CalibWindow.xaml`(删底部 Border + Grid 行)
-- Modify: `CalibWindow.xaml.cs`(删对应控件引用,SetCurrentInfo 更详细)
-- Modify: `MainWindow.Calib.cs:145-151`(OnStep 回调传更详细参数)
-
-- [ ] **Step 1: 删 CalibWindow.xaml 底部区**
-
-将 `CalibWindow.xaml` 的 Grid 行定义(第 7-11 行):
-```xml
-        <Grid.RowDefinitions>
-            <RowDefinition Height="Auto"/>
-            <RowDefinition Height="*"/>
-            <RowDefinition Height="120"/>
-        </Grid.RowDefinitions>
-```
-改为(去掉第三行):
-```xml
-        <Grid.RowDefinitions>
-            <RowDefinition Height="Auto"/>
-            <RowDefinition Height="*"/>
-        </Grid.RowDefinitions>
-```
-
-删除底部 Border 整块(第 28-34 行):
-```xml
-        <!-- 底部:仅当前参数/步骤文字(画面直接显示在上面对应well格内) -->
-        <Border Grid.Row="2" Background="#252526" Padding="8" CornerRadius="4">
-            <StackPanel VerticalAlignment="Center">
-                <TextBlock x:Name="TxtCurParams" Text="当前: --" Foreground="White" FontSize="14"/>
-                <TextBlock x:Name="TxtCurStep" Text="" Foreground="#DCDCAA" FontSize="13" Margin="0,4,0,0"/>
-            </StackPanel>
-        </Border>
-```
-(整段删除,无替代。)
-
-- [ ] **Step 2: 改 CalibWindow.xaml.cs 的 SetCurrentInfo**
-
-将现有 `SetCurrentInfo`(引用了已删除的 `TxtCurParams`/`TxtCurStep`):
-```csharp
-        public void SetCurrentInfo(string param, string step)
-        {
-            Dispatcher.Invoke(() =>
-            {
-                if (param != null) TxtCurParams.Text = param;
-                if (step != null) TxtCurStep.Text = step;
-                // 当前well格标题实时显示步骤+参数
-                if (_activeWell >= 1 && _activeWell <= 16)
-                {
-                    _cellCaps[_activeWell].Text = $"well {_activeWell} 标定中\n{step}\n{param}";
-                    _cellCaps[_activeWell].Foreground = Brushes.Yellow;
-                }
-            });
-        }
-```
-替换为(去掉底部控件引用,well 格标题显示完整参数):
-```csharp
-        public void SetCurrentInfo(string param, string step)
-        {
-            Dispatcher.Invoke(() =>
-            {
-                if (_activeWell >= 1 && _activeWell <= 16)
-                {
-                    _cellCaps[_activeWell].Text = $"well {_activeWell} 标定中\n{step}\n{param}";
-                    _cellCaps[_activeWell].Foreground = Brushes.Yellow;
-                }
-            });
-        }
-```
-
-- [ ] **Step 3: OnStep 回调传更详细参数**
-
-在 `MainWindow.Calib.cs` 找到 OnStep 回调(145-151 行):
-```csharp
-                        OnStep = (msg, circle, exp) =>
-                        {
-                            string param = circle != null && circle.Found
-                                ? $"圆心偏移 Y:{circle.OffsetYPct:F0}% X:{circle.OffsetXPct:F0}% 完整:{circle.Complete}"
-                                : "未检出well圆";
-                            win.SetCurrentInfo(param, msg);
-                        }
-```
-替换为(补 Z/水平/曝光等当前已知量;_lastZ/_lastH 是已有字段):
-```csharp
-                        OnStep = (msg, circle, exp) =>
-                        {
-                            string circlePart = circle != null && circle.Found
-                                ? $"圆心偏移 Y:{circle.OffsetYPct:F0}% X:{circle.OffsetXPct:F0}% 完整:{circle.Complete}"
-                                : "未检出well圆";
-                            string expPart = exp != null ? $" 曝光信息:{exp}" : "";
-                            string param = $"Z:{_lastZ} 水平:{_lastH} {circlePart}{expPart}";
-                            win.SetCurrentInfo(param, msg);
-                            FileLogger.Data("CALIB", $"{msg} | {param}");
-                        }
-```
-注意:在 `MainWindow.Calib.cs` 顶部 using 区加 `using AutoFocusTool.Logging;`(若尚无)。
-(`exp` 是 `ExposureInfo` 类型,`$"{exp}"` 走其 ToString;若无意义可仅保留 circlePart。该回调天然把标定步骤数据落盘。)
-
-- [ ] **Step 4: 编译验证**
-
-Run: `dotnet build AutoFocusTool.csproj -c Debug`
-Expected: 生成成功,0 错误。
-
-- [ ] **Step 5: 运行验证**
-
-Run: `./bin/Debug/net8.0-windows/AutoFocusTool.exe`,连接后跑自动初始化,确认标定窗口底部不再有冗余文字区、well 格标题显示 Z/水平/曝光/偏移等详细参数。
-
-- [ ] **Step 6: 提交**
-
-```bash
-git add CalibWindow.xaml CalibWindow.xaml.cs MainWindow.Calib.cs
-git commit -m "feat(ui): 删CalibWindow底部冗余孔位数据 + well格预览显示详细参数
-
-Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
-```
-
----
-
-## Self-Review(计划自检结果)
-
-- **Spec 覆盖**:
-  - 子系统1(诊断+目录)→ Task 4(zcurve模式、输出TestData、记录亮度均值)+ Task 1(csproj排除TestData)。
-  - 子系统2(UI)→ Task 5(删底部Border、预览参数详细)。
-  - 子系统3(日志)→ Task 1(FileLogger)+ Task 2(Log落盘+Init+LogAction)+ Task 3(按钮埋点)。
-  - 全覆盖,无遗漏。
-- **占位符**:无 TBD/TODO。Task 2/3 中按文件实际函数名调整文案的指引,均给了具体 LogAction 文本,非占位。
-- **类型/签名一致性**:`FileLogger.Init/Info/Action/Data/Warn/Error(category,msg)` 在 Task1 定义、Task2/3/5 一致调用;`LogAction(string)` Task2 定义、Task3 调用一致;`SetCurrentInfo(param, step)` 签名不变(Task5 只改内部实现),调用方 OnStep 一致。
-- **顺序**:日志(1-3)→诊断(4)→UI(5),与 spec 实现顺序建议一致;各 Task 独立可编译可提交。
-- **范围**:未改 Sharpness 算法与对焦逻辑(Task4 仅诊断取证),与 spec 范围一致。
-

+ 0 - 157
docs/superpowers/specs/2026-06-16-autofocus-scan-range-design.md

@@ -1,157 +0,0 @@
-# AutoFocus 扫描范围扩大与电机限位 — 设计文档
-
-日期:2026-06-16
-涉及文件:`Calib/CalibrationEngine.cs`(主要)、`Serial/HouseMotor.cs`(参考,不改)
-
-## 1. 背景与问题
-
-AutoFocusTool 自动标定单 well 时,偶发"找不到圆孔"。经排查确认现象为:
-**水平扫描的 7 个位置从头到尾都检不出圆,且标定窗口实时画面里始终看不到完整圆孔**——
-即 well 没有充分转进相机画面。
-
-根因:当前水平找圆以 well 的 EEPROM 位置为中心做 `±4000` 脉冲、7 步扫描
-(`CalibrationEngine.cs:30-31`)。当某 well 的 EEPROM 水平位置偏差大于该窗口时,
-well 圆始终落在画面外,整段扫描都检不出。
-
-同理 Z 对焦:粗对焦以 EEPROM Z 零点为中心 `±1500`(`ZHalf=1500`)扫描,
-若真实焦面离零点超过 1500 脉冲也会扫不到峰。
-
-## 2. 目标
-
-1. 水平找圆改为**全行程粗扫定位 + 局部密扫**,不再依赖 EEPROM 位置准确。
-2. Z 粗对焦改为**大范围固定窗口扫描**,覆盖实测焦面分布区间。
-3. 所有电机移动加**行程限位钳位**,水平 [0,70000]、垂直 [0,125000]。
-4. 关键扫描参数做成可配置字段,便于现场调整。
-
-## 3. 关键决策(已与用户确认)
-
-| 项 | 决策值 |
-|---|---|
-| 水平方案 | 全行程粗扫定位 + 局部密扫 |
-| 水平行程上限 | 可配置,默认 70000(来源:旧工程自检 `HouseBin.cs:468`) |
-| 水平粗扫步距 | 默认 2000,**命中完整圆即停**,不必扫完全程 |
-| Z 粗对焦中心 | 固定 90000(所有 well 统一;用户实测焦面均落在 60000~120000) |
-| Z 粗对焦区间 | 60000~120000(中心 90000,±30000) |
-| Z 粗对焦步距 | 2000 → 约 31 层 |
-| Z 精对焦半幅 | 6000(围绕粗峰 ±6000) |
-| Z 精对焦步距 | 500 → 约 24 层(B 方案:精度优先) |
-| 限位 | 水平 [0,HMax]、垂直 [0,ZMax],所有移动前钳位 |
-
-## 4. 详细设计
-
-### 4.1 新增/修改参数(`CalibrationEngine` 字段)
-
-```csharp
-// ── 行程限位(所有电机移动前钳到该区间)──
-public int HMin = 0, HMax = 70000;          // 水平行程上下限
-public int ZMin = 0, ZMaxPulse = 125000;    // 垂直行程上下限(旧工程软上限)
-
-// ── 水平全行程粗扫定位 ──
-public int HCoarseStart = 0;                // 粗扫起点
-public int HCoarseEnd   = 70000;            // 粗扫终点(= HMax)
-public int HCoarseStep  = 2000;             // 粗扫步距(够小不跨过 well)
-// 命中完整圆即停:扫到 Found && Complete 立即转入局部密扫
-
-// ── Z 全范围粗对焦(固定中心窗口)──
-public int ZCoarseCenter = 90000;           // 粗对焦中心(固定,所有 well 统一)
-public int ZCoarseHalf   = 30000;           // 粗对焦半幅 → 区间 60000~120000
-public int ZCoarseStep   = 2000;            // 粗对焦步距 → 约 31 层
-
-// ── Z 精对焦(围绕粗峰)──
-// 复用现有精对焦逻辑,半幅与步距改为:
-public int FineZHalf = 6000;                // 精对焦半幅(原 fineZHalf=500)
-public int FineZStep = 500;                 // 精对焦步距 → 约 24 层
-```
-
-保留现有 `HScanRange`/`HScanSteps`/`FineScanSteps` 用于**水平局部密扫**阶段
-(命中后在最佳点附近精调 Y 居中),无需删除。
-
-### 4.2 限位钳位(统一入口)
-
-新增两个私有方法,所有 `_motor.HorizontalMoveTo` / `_motor.VerticalMoveTo`
-调用前先过钳位,避免越界指令发给下位机:
-
-```csharp
-int ClampH(int p) => Math.Max(HMin, Math.Min(HMax, p));
-int ClampZ(int p) => Math.Max(ZMin, Math.Min(ZMaxPulse, p));
-```
-
-钳位发生时写一条 Log(便于发现 EEPROM 异常值或参数配置过界)。
-
-### 4.3 水平找圆:全行程粗扫定位 + 局部密扫
-
-新增方法 `HCoarseLocate`,替换 `CalibrateWell` 中"②旋转居中"
-对 `ScanForCenter(eepromHPos, HScanRange, ...)` 的首次调用:
-
-1. **全行程粗扫**:从 `HCoarseStart` 到 `HCoarseEnd`,步距 `HCoarseStep`,
-   每个位置 `Grab` + `WellDetector.Detect`。
-2. **命中即停**:一旦检出 `Found && Complete`,记录该粗扫位置 `hpHit`,立即停止粗扫。
-3. **局部密扫**:以 `hpHit` 为中心,调用现有 `ScanForCenter(well, hpHit, fineRange, FineScanSteps)`
-   做 Y 居中精调(`fineRange` 取 `HCoarseStep` 量级,约 ±2000)。
-4. **全程未命中**:若粗扫扫完全程仍无 `Found && Complete`,
-   退化为"取扫描中 |Y偏移| 最小且 Found 的位置";仍无则记 `Note="水平全程未检出圆"`,
-   该 well 标定失败(沿用现有失败返回风格)。
-
-粗扫阶段沿用 `CenterScanExposure` 固定中低曝光(well 是暗背景中灰盘)。
-
-### 4.4 Z 粗对焦:固定大窗口扫描
-
-修改 `CoarseFocus`:扫描中心由"传入的 EEPROM 零点"改为固定 `ZCoarseCenter=90000`,
-半幅 `ZCoarseHalf=30000`,步距 `ZCoarseStep=2000`:
-
-```
-for z in [ZCoarseCenter-ZCoarseHalf .. ZCoarseCenter+ZCoarseHalf] step ZCoarseStep:
-    move ClampZ(z); discard frame; grab; score = Sharpness(中央40% ROI)
-取最高分 z 作为 coarseZ
-```
-
-约 31 层。`CalibrateWell` 中调用处随之调整(不再传 `eepromZ` 作中心)。
-
-### 4.5 Z 精对焦:扩大半幅
-
-现有精对焦围绕 `coarseZ ± fineZHalf` 密扫。将半幅改用 `FineZHalf=6000`、
-步距 `FineZStep=500`(约 24 层)。半幅 6000 足以覆盖粗扫 ±2000 的峰值定位误差并留余量。
-保留现有 3 点平滑 + 抛物线插值峰顶逻辑不变。
-
-## 5. 数据流(单 well 标定)
-
-```
-读 EEPROM(水平位置仅作参考/不再作扫描中心) → 开灯
- ① Z 粗对焦:固定 90000±30000 步距2000 (31层) → coarseZ
- ② 水平全行程粗扫 0~70000 步距2000 命中完整圆即停 → hpHit
-    → 局部密扫 ScanForCenter(hpHit, ±~2000) → 最居中水平位置
- ③ 曝光二分(well 内 ROI,逻辑不变)
- ④ Z 精对焦:coarseZ±6000 步距500 (24层) + 平滑插值 → focusZ
- 存图 + 写 calibration.json
-```
-
-注:相比原流程,①②顺序保持"先对焦后居中"
-(对焦与居中耦合,焦准画面才检得出圆),但①改为固定大窗口、②改为全行程粗扫。
-
-## 6. 性能影响(需用户知晓)
-
-- Z 粗对焦:约 31 层 × (移动+`ScanDelayMs`350ms+抓帧) ≈ 单 well 15~20 秒。
-- Z 精对焦:约 24 层 ≈ 单 well 10~15 秒。
-- 水平全行程粗扫:最坏 35 步(命中即停通常更少)。
-- 16 个 well 全标定预计较原方案明显变慢(分钟级增加)。精度优先,已确认接受。
-
-## 7. 错误处理
-
-- 所有移动经 `ClampH`/`ClampZ` 钳位,越界写 Log 但不崩溃。
-- 水平全程未检出圆 → 退化策略(4.3 第 4 点),最终失败则记 Note 并跳过该 well。
-- 抓帧失败沿用现有 `Grab()` 重试 3 次机制。
-- Z 精对焦峰过弱(`PeakRatio<1.2`)沿用现有"弱峰/可能空well"告警。
-
-## 8. 测试
-
-- 现有 `SelfTest`/`SmokeTest` 子工程验证编译与基本调用不被破坏。
-- 真机验证:对一批已知 EEPROM 偏差较大的 well 运行标定,确认
-  (a) 水平全行程能找到圆;(b) Z 粗对焦命中 60000~120000 内焦面;
-  (c) 越界脉冲被钳位并有 Log。
-- 无真机时:纯参数/钳位逻辑可加轻量单元验证(`ClampH`/`ClampZ` 边界)。
-
-## 9. 不在本次范围
-
-- 不改协议层 `Protocol.cs`、不改 `HouseMotor` 的移动接口(限位放在 CalibrationEngine)。
-- 不改相机分辨率/崩溃问题(`GetSourceBuffer` 段错误)——另行处理。
-- 不做 UI 参数配置界面(参数先以字段默认值形式存在,后续可接入设置页)。

+ 0 - 107
docs/superpowers/specs/2026-06-16-diagnostics-ui-logging-design.md

@@ -1,107 +0,0 @@
-# 诊断工具 / UI数据显示 / 操作日志 — 设计文档
-
-日期:2026-06-16
-背景:4 号舱对焦失败排查中发现需要更好的诊断与可观测性。本设计涵盖三个独立子系统。
-
-## 总览
-
-| # | 子系统 | 目标 |
-|---|---|---|
-| 1 | 诊断工具 + 测试数据目录规范 | 扩展 WellSpacing 支持 Z 清晰度曲线扫描;所有诊断输出统一到 TestData/ |
-| 2 | UI 数据显示调整 | 删除 CalibWindow 底部冗余孔位数据;预览画面参数显示更详细 |
-| 3 | 操作日志系统 | 界面所有点击操作落盘记录,便于事后分析定位 |
-
-三者相对独立,可分别实现。下面分述。
-
----
-
-## 子系统 1:诊断工具扩展 + TestData 目录规范
-
-### 1.1 目录规范
-- 新建 `TestData/`(加入主工程 csproj 的 Compile Remove 排除,避免被编译)。
-- 所有诊断工具(WellSpacing 等)输出写到 `TestData/<工具名>_<时间戳>/`。
-- `calib_result/` 仅保留程序正式标定产物(`house*_well*_标定后.bmp` + calibration 存图),不再混入诊断文件。
-
-### 1.2 WellSpacing 扩展:Z 清晰度曲线模式
-现状:WellSpacing 已能逐 well 移动+拍照+检测+存图(固定 Z)。
-新增一个模式:对指定 well 做 **Z 清晰度曲线扫描**,定位真实焦面。
-
-- 用法:`WellSpacing.exe zcurve <COM> <相机idx> <well号> [zLo=20000] [zHi=120000] [zStep=2000] [曝光=60]`
-- 行为:移到该 well 的 EEPROM 水平位置 → 从 zLo 到 zHi 按 zStep 逐层:
-  - 移动到 z(绝对)→ 丢弃一帧 → 抓帧
-  - 算清晰度(中央40% ROI),同时记录该帧**灰度均值**(验证亮度归一化是否带偏)
-  - 存图到 `TestData/zcurve_w<well>_<时间戳>/z<值>_s<分数>.bmp`
-  - 控制台打印一行:`z=<值> 原始梯度=<sumSq/n> 亮度均值=<mean> 归一化分=<score>`
-- 输出末尾打印曲线峰值位置,便于人工判断真实焦面 Z。
-- 目的:用真实数据回答"4号舱焦面在哪""mean²归一化是否把暗的低Z层评高",为后续是否改 Sharpness 提供依据。
-
-保留 WellSpacing 现有的"逐 well 拍照"模式(默认无 zcurve 参数时跑原逻辑),只是输出目录改到 TestData/。
-
----
-
-## 子系统 2:UI 数据显示调整
-
-### 2.1 删除 CalibWindow 底部冗余区
-- 删除 `CalibWindow.xaml:29-34` 的底部 Border(`TxtCurParams` / `TxtCurStep`)。
-- 同步删除 `CalibWindow.xaml.cs` 中对这两个控件的引用(`SetCurrentInfo` 里对 `TxtCurParams.Text`/`TxtCurStep.Text` 的赋值)。
-- Grid 第 2 行(`Height="120"`)一并移除,网格区(well 4x4)占满下方空间。
-
-### 2.2 预览画面参数显示更详细
-现状:`SetCurrentInfo(param, step)` 把"步骤+偏移"写到当前 well 格的标题(cap)上。
-增强:每个 well 格标题在标定中实时显示更完整参数,至少包含:
-- 当前步骤(粗对焦/居中/曝光/精对焦 + 进度)
-- well 号、Z 当前值、水平位置、曝光值、清晰度分、圆偏移 X/Y、是否完整圆
-
-实现:扩展 `SetCurrentInfo` 的入参,或新增重载传入结构化数据(well/z/h/exp/score/offsetX/offsetY/complete),在 well 格标题用多行格式显示。调用方 `MainWindow.Calib.cs` 的 `OnStep` 回调补齐这些字段。
-
-主窗口 `MainWindow.xaml` 预览浮层(`TxtScore`/`TxtZ`/`TxtFps`)保留——那是手动测试用,不在本次删除范围。
-
----
-
-## 子系统 3:操作日志系统
-
-### 3.1 方式(已确认:甲 — 统一入口集中记录)
-- 给现有 `MainWindow.Log()` 增加落盘能力。
-- 每个按钮 `Click` 处理函数开头加一行 `LogAction(...)`,显式记录"操作名 + 关键参数"。
-- 操作执行后,在产生/修改数据处记录结果(如移动后的位置、设置的曝光值、标定结果)。
-
-### 3.2 文件组织(已确认:每次启动一个文件)
-- 目录:`Logs/`(加入 csproj 排除)。
-- 文件名:`Logs/<yyyy-MM-dd_HHmmss>.log`,程序启动时创建,本次运行所有日志写入同一文件。
-
-### 3.3 日志格式
-纯文本,每行一条,制表分隔,便于阅读和事后 grep:
-```
-HH:mm:ss.fff  [级别]  [类别]  消息
-```
-- 级别:INFO / ACTION / DATA / WARN / ERROR
-- 类别:UI / MOTOR / CAMERA / CALIB / SERIAL
-- 示例:
-```
-16:45:30.123  [ACTION] [UI]     点击"一键全自动初始化" wells=[1,2,3] 舱=4
-16:45:31.456  [DATA]   [MOTOR]  well1 读EEPROM 水平=71500 Z零点=...
-16:45:33.789  [DATA]   [CALIB]  well1 粗对焦 coarseZ=90000 峰比=1.04
-16:45:40.012  [WARN]   [CALIB]  well1 对焦峰弱(ratio=1.04)
-```
-
-### 3.4 实现要点
-- 新增一个轻量 `FileLogger` 类(静态或单例):启动时按时间戳建文件,提供 `Info/Action/Data/Warn/Error(category, msg)` 方法,线程安全(lock 写入),自动 flush。
-- `MainWindow.Log(msg)` 内部同时:① 追加到界面 `TxtLog`(现有行为)② 调 `FileLogger` 落盘。
-- 各按钮 `Click` 开头加 `LogAction`;关键数据点(移动结果、标定参数、异常)加 `LogData/LogWarn`。
-- `CalibrationEngine` 已有的 `Log?.Invoke(...)` 回调天然会流入 FileLogger(因为它接的就是 MainWindow.Log),标定过程数据自动入库。
-
-### 3.5 记录覆盖范围
-界面所有按钮:扫描、连接/断开、转well、开/关光源、抓帧、实时预览、存图、设曝光/增益、Z 正反转/复位/绝对移动、水平正反转/复位、Z扫描、停止、全选/全不选、一键自动初始化。
-每条记录"点击了什么 + 当时的输入参数 + 执行结果/产生的数据"。
-
----
-
-## 实现顺序建议
-1. 子系统 3(日志)——最先做,后续两项的调试都能受益于日志。
-2. 子系统 1(诊断工具)——用来定位 4 号舱焦面问题。
-3. 子系统 2(UI)——纯界面调整,独立。
-
-## 不在本次范围
-- 不改 Sharpness 清晰度算法本身(先用诊断工具 1.2 取证,确认后另行设计)。
-- 不改 CalibrationEngine 对焦逻辑(同上,待诊断数据)。
-- 不改主窗口预览浮层(保留)。

+ 0 - 210
相机自动对焦项目-源码分析清单.md

@@ -1,210 +0,0 @@
-# 相机自动对焦项目 · 源码分析清单(控制方式 + 参数 + 安全阈值)
-
-> 记录日期:2026-06-12
-> 来源:阅读 `C:\claudeFile\TL\源码` 下两套工程后的只读分析结论。
-> 用途:把"怎么控相机 / 怎么控马达 / 怎么控光源 + 物理参数 + 安全限位"一次说清,供你核对,确认后据此写第一步手动测试程序。
-> 配套:主文档 `相机自动对焦项目-总方案.md`(需求与算法逻辑看那份,本份只讲源码能落地的硬接口)。
-> 标注规则:**【确证】**=代码里直接读到;**【推测】**=据命名/引用推断,需真机或反编译确认。
-
----
-
-## 0. 源码里有两套工程
-
-| 工程 | 位置 | 作用 | 对我们的价值 |
-|---|---|---|---|
-| `ivf_tl_control_2.0` | 完整下位机控制系统(多项目解决方案) | 现役:控温、拍照、换气、对焦上传 | **主参考**:相机、串口马达、LED、对焦循环全在这 |
-| `aivfo_ccd` | 一个小 WPF 工程,含独立 `Camera.cs` | 像是相机调试/试验程序 | 相机操作更全(`SetExp`、`BitmapImage`、Raw 访问),**适合直接抄来做测试程序的相机层** |
-
-结论:测试程序以 `ivf_tl_control_2.0` 的健壮相机框架为骨架,补 `aivfo_ccd` 的便捷方法。
-
----
-
-## 一、相机控制【确证为主】
-
-### 1.1 是什么相机
-- 厂商 **Microview(微视)**,型号字符串硬编码 `"MVC2000"`,USB2.0 工业相机,约 200 万像素。
-- 命名空间 `using Microview;`(见 `aivfo_ccd\Util\Camera.cs:1`)。
-- **Bayer 传感器**(取 Raw 后需去马赛克转 RGB)。
-- SDK = `mvcapi.dll`(核心)+ `mvavi.dll`(录像),P/Invoke 封装在 `ivf_tl_CameraHelper\MVCAPI.cs`。
-
-### 1.2 连接 / 初始化顺序
-```
-new Camera(index, 1600, 1200, exposure)
- → Init()        ⇒ MV_Usb2Init("MVC2000", out index, ref capInfo, out hImager)
- → SetOpMode()   ⇒ MV_Usb2SetOpMode(hImager, nMode, false)
-```
-- `nMode`:**0 = 拍照(单帧)模式,1 = 实时图像模式**(`aivfo_ccd\Util\Camera.cs:282-285` 注释明确)。
-- 句柄 `IntPtr hImager` 贯穿后续所有调用。
-- 卸载:`UnInit() ⇒ MV_Usb2Uninit(ref hImager)`。
-
-### 1.3 抓单帧 + 取像素
-- 抓图:`GetRgbData() ⇒ MV_Usb2GetRgbData(hImager, ref capInfo, pDest)`。
-- 取数据:属性 `SourceBuffer` → `byte[]`,长度 `width*height*3`,**24bpp RGB(BGR 排列)**。
-- ⚠️ **相机原始数据上下颠倒**:`aivfo_ccd` 版取图后做了 `RotateNoneFlipY` 垂直翻转(`Camera.cs:185-208`)。显示/保存/算清晰度时要统一处理,否则 ROI 位置会上下错。
-- 另一条路径:`GetRawData()` → `RawToRgb()`(`MV_Usb2ConvertRawToRgb`),用于需要 Raw 的场合。
-- 默认分辨率 **1600 × 1200**。
-
-### 1.4 曝光与增益(曝光校正这一环靠它)
-- **曝光时间**:字段 `CapInfoStruct.Exposure`(int),**单位 100µs**(默认 400 → 40ms)。
-  - 设置入口:`SetPartOfCapInfo(int Exposure)`(`ivf_tl` 版)/ `SetExp(int newExp)`(`aivfo_ccd` 版,最简洁)⇒ `MV_Usb2SetPartOfCapInfo`。
-- **增益**:字段 `CapInfoStruct.Gain` 是 **byte[3],分别是 红 / 绿 / 蓝 三通道增益**(不是单一全局 gain),取值 0–255,默认约 R25 G14 B25。
-- 含义:**"曝光校正"在相机侧的可调量 = 曝光时间(100µs步进) + RGB 三通道增益**。
-
-### 1.5 预览 / 取流(可视化界面要用)
-- 贴窗口预览:`Usb2Start(控件句柄, x, y, w, h) ⇒ MV_Usb2Start(...)`;停 `Usb2Stop`;暂停 `MV_Usb2PausePreview`。
-- 帧回调(软件取流,不贴窗):`MV_Usb2SetFrameCallBack(hImager, 委托, lpUser)`。
-- 帧率:`MV_Usb2GetFrameRate`。
-- **对焦算分不需要预览**:拍照模式(nMode=0) + 循环 `GetRgbData()` 抓帧即可;要给人看实时画面再开 `Usb2Start` 或回调。
-
-### 1.6 存图
-- `SaveBmpPic(path)` 存 BMP;`SaveJPG` 实为原始字节直写(非真 JPEG,注意)。
-
----
-
-## 二、马达控制(串口)【确证为主】
-
-### 2.1 串口与协议
-- 串口参数:波特率 **9600**,8 数据位,1 停止位,无校验,读/写超时 **3000ms**。`System.IO.Ports.SerialPort` 事件驱动收数(`Channel.cs:84-109`)。
-- 帧格式(`Commander.cs:873-884` 注释):
-
-| 字节 | [0] | [1] | [2] | [3] | [4..n-2] | [n-1] |
-|---|---|---|---|---|---|---|
-| 含义 | 帧头 `0x5E` | 命令码 CMD | 序号 `0x00` | 整帧长 LTH | 辅助码+参数 | 校验 CRC |
-
-- **无独立帧尾**,末字节即校验。
-- **校验 = 累加和**:前 n-1 字节逐字节相加(byte 溢出截断)放末字节(`Commander.CreateORC` `Commander.cs:15-41`)。
-- **每条命令的回复长度写死**(`Commander.CustomProtocolLength` `Commander.cs:43-89`),接收端按固定长度从环形缓冲区取,不靠帧尾分包。
-- 回复 [n-2] = 下位机结果位,非 0 视为失败(`Channel.cs:186-188`)。
-
-### 2.2 有几个马达
-**只有 2 个步进马达**(不是 X+Y+Z 三轴):
-- **水平电机(Horizontal)**:辅助码高半字节 `1`。单轴,把样品/well 定位到水平位置。→ 对应方案里的"XY 皿孔定位",但实际是**单轴水平**。
-- **垂直电机(Vertical)= Z 轴 = 对焦轴**:辅助码高半字节 `2`。
-
-> ⚠️ 与方案文档的差异:方案里写"XY 两轴居中",**源码里水平只有一个轴**。皿孔居中是否真的只有单轴、还是另有机构,**需你真机确认**。
-
-### 2.3 电机命令(CMD=0x05,11 字节帧)
-辅助码 = 高半字节(轴) | 低半字节(动作):正转0 / 反转1 / 脱机2 / 复位3 / 绝对运动4。
-
-**Z 轴(垂直/对焦)——写程序重点:**
-| 动作 | 辅助码 | Commander 方法 | ComBin 封装(阻塞等待) |
-|---|---|---|---|
-| 复位(回零) | 0x23 | `CreateVerticalMotorResetCommand` (:998) | `VerticalMotorResetWait` (`ComBin.cs:509`) |
-| 正转(相对) | 0x20 | `CreateVerticalMotorForwardCommand` (:1009) | — |
-| 反转(相对) | 0x21 | `CreateVerticalMotorBackwardCommand` (:1080) | — |
-| **绝对运动** | 0x24 | `CreateVerticalMotorAbsoluteMovementCommand` (:1045) | **`VerticalMotorAbsoluteWait`** (`ComBin.cs:535`) |
-| 读位置 | — | `CreateReadVerticalMotorCommand`(CMD=0x18,辅助0x02,:187) | `ReadVerticalMotorWait` (`ComBin.cs:493`) |
-
-**水平电机:** 同结构,绝对运动 `HorizontalMotorAbsoluteWait`(`ComBin.cs:457`),读位置 `ReadHorizontalMotorWait`(`ComBin.cs:477`)。
-
-### 2.4 位置单位、行程、限位
-- **单位 = 步进电机脉冲数(步),不是 µm。**【确证】全代码无 µm 换算,字段一律 `Pulse`/`Position`。
-- 绝对运动用 32 位 int 脉冲值,**大端写入帧**(高字节在前,`Commander.cs:1056-1069`)。
-- **Z 层间距**:`House.verticalMotorSpacePulse`(每层脉冲间隔,`House.cs:86`);未配置时**硬编码默认 128 脉冲/层**(`HouseBin.cs:1125`)。
-- **Z 行程上限**:`TLSetting.verticalMotorPulseMax`(`TLSetting.cs:259`,测试值见 `StartMain.cs:106` = 125000)。对焦循环里多处 `if (currentVer > verticalMotorPulseMax) break;` 做**软限位**(`HouseBin.cs:1128/1266/1442`)。
-- **零点**:复位命令回机械零点;每个 well 的"焦准零点脉冲"存 EEPROM(`CreateReadEEPROMvertMtStartPulse` `Commander.cs:530`)。
-- **硬限位开关**:源码不可见,**【推测】由下位机固件处理**。需你确认行程下限/上限的物理保护。
-
-### 2.5 怎么知道移动到位了 —— 关键
-**开环 + 固定延时 + 事后回读**,没有闭环到位反馈:
-1. 指令阻塞等串口回复(`WaitOne`),但**回复只代表"下位机收到指令",不代表机械停稳**。超时 30s 重发,最多 3 次,失败重开串口。
-2. 回复后再 `Thread.Sleep(motorDelay)` 等运动完成。`motorDelay` = `TLSetting.motorDelay`(电机到位延时 ms,`TLSetting.cs:253`)。
-3. 移动后 `ReadVerticalMotorWait` 回读实际位置与目标比对,不一致记错误日志(`HouseBin.cs:1537/1610`)。
-- 位置解析 `Analysiser.ParseCurrentMotor`(`Analysiser.cs:110-117`):回复字节[4..7]拼 int32,失败返回 -1。
-
-> 对自动对焦的直接影响:**Z 每移一层,必须等 `motorDelay` 稳定后再抓图**,否则拍到运动模糊。`motorDelay` 的真机取值需标定。
-
----
-
-## 三、光源 / LED 控制【确证】
-
-> (你补充的环节)曝光校正除了相机曝光+增益,还有第三个旋钮:补光 LED。
-
-- **LED 开关(串口,CMD=0x09 设IO,7字节):**
-  - 开:`CreateOpenLEDCommand` = `5E 09 00 07 00 01 6F`(`Commander.cs:107`)→ `OpenLEDWait`(`ComBin.cs:655`)
-  - 关:`CreateCloseLEDCommand` = `5E 09 00 07 00 00 6E`(`Commander.cs:117`)→ `CloseLEDWait`(`ComBin.cs:670`)
-  - 流程:对焦/拍照前 `OpenLEDWait`,结束后 `CloseLEDWait`(`HouseBin.cs:1070/1139/1407/1452`)。
-- **LED 亮度**:⚠️ 代码里**只能从 EEPROM 读亮度值**(`CreateReadEEPROMLightNum` `Commander.cs:807` → `ReadEEPROMLightNumWait` `ComBin.cs:822`),**没找到动态"设置亮度"的串口命令**。【确证:只读未写】
-  - 含义:现役系统的光源亮度是**出厂/EEPROM 固定值**,运行时只开关、不调亮度。
-  - **待确认(重要)**:自动曝光校正若想调光源亮度,需要下位机支持"写亮度"命令。现有协议里没有 → 要么走相机曝光+增益这条路(已可调),要么请硬件方加一条写亮度指令。
-
-**结论:曝光校正三个可调量的现状**
-| 旋钮 | 能否程序调 | 接口 |
-|---|---|---|
-| 相机曝光时间 | ✅ 可调 | `SetExp/SetPartOfCapInfo`,单位100µs |
-| 相机 RGB 增益 | ✅ 可调 | `CapInfoStruct.Gain[3]`,0–255 |
-| LED 亮度 | ❌ 现仅开关+读取 | 需硬件加"写亮度"命令才可调 |
-| LED 开关 | ✅ 可调 | `OpenLEDWait/CloseLEDWait` |
-
-
----
-
-## 四、现有"对焦/选层"流程 —— 重大发现【确证】
-
-### 4.1 客户端不做选层,决策在服务器
-现役 C# 客户端**本身不计算"哪一层最清晰"**。它只做:沿 Z 逐层移动 → 每层抓图 → 存盘 → **HTTP 上传服务器** → 轮询向服务器**索要**"最清晰层"的相对位置。
-- 选层算法跑在**服务器端**,客户端只是上传+取结果。
-- **直接推论**:方案文档说的"7 组全选错",**问题大概率在服务器算法,或在传给服务器的几何参数**(Z 起点 / 层间距 / 抠图偏移),不在这个仓库的 C# 里。
-
-### 4.2 Z 序列拍摄循环(现成可复用)
-- 全孔:`HouseBin.cs:1106-1137`;单孔:`HouseBin.cs:1244-1281`。逻辑相同:
-  - 层数 = `House.autoFocusNumber`;
-  - 每层 Z = `对焦起点 verticalMotorPosition + 层间距 * i`(层间距默认 128 脉冲);
-  - 超 `verticalMotorPulseMax` 则 break;
-  - `VerticalMotorAbsoluteWait` 移动 → `Autofocus()` 抓图存盘上传。
-
-### 4.3 本地其实有一条"算分"通路,但没被用
-- `HouseBin.GetScore(...)`(`HouseBin.cs:1734`)→ 外部 DLL `Project2.dll` 的 `GetImageScoreAndSaveImage`(`AivfoHelper.cs:34`,导出序号 #5)。
-- **全仓库搜索:`GetScore` 除定义外零调用** —— 是死代码 / 调试遗留。
-- 即:本地具备"给一张图打清晰度分"的能力(在 `Project2.dll` 里,底层应是 OpenCV),但现役流程没拿它选层。
-- 具体用的是 Laplacian / Tenengrad / 方差中的哪种,**C# 源码里看不到,在 DLL/服务器里**,需反编译 `Project2.dll`(依赖 `opencv_world3416.dll`)或查服务器才能确证。
-
-### 4.4 抠图 ROI 偏移(选错的高嫌疑因素)
-- 每孔存 `leftOffset` / `bottomOffset`(抠图裁剪偏移,`HouseWellSettingDB.cs`)。
-- 这俩配错 → ROI 偏离胚胎 → 任何清晰度算法都会选错层。**【推测】值得优先排查。**
-
-### 4.5 DLL 职责一览
-| DLL | 作用 | 依据 |
-|---|---|---|
-| `mvcapi.dll` | 相机 SDK | 【确证】MVCAPI.cs 全部 P/Invoke |
-| `mvavi.dll` | 录像 | 【确证】 |
-| `Project2.dll`(newccd) | 当前图像处理:抠图#1 / 打分#5 / 存图#7 | 【确证】AivfoHelper.cs |
-| `opencv_world3416.dll` | Project2 的底层 CV | 【推测】 |
-| `CellProcessorDll.dll`/`CellCultureDllMulti64.dll`/`opencv_world342.dll`(ccd) | 旧图像处理链路 | 【推测】已被 newccd 取代,无 C# 引用 |
-| `MVBayerDec.dll`/`MVParm.dll` | Bayer 解码 / 相机参数 | 【推测】仅打包 |
-
----
-
-## 五、写"第一步手动测试程序"的落点(拿到你确认后开工)
-
-新程序是**独立小工程**,不动现役系统。各能力的现成接口:
-
-| 能力 | 直接复用 | 备注 |
-|---|---|---|
-| 连相机 | `Camera.Init()` + `SetOpMode(0)` | 抄 `aivfo_ccd\Util\Camera.cs` |
-| 调曝光 | `SetExp(int)` 单位100µs | |
-| 调增益 | `CapInfoStruct.Gain[0..2]` 0–255 | RGB 三通道 |
-| 抓帧 | `GetRgbData()` + `SourceBuffer` | 注意 FlipY 翻转 |
-| 实时预览 | `Usb2Start(句柄,...)` 或帧回调 | 可视化界面用 |
-| 开串口/电机 | `ComBin` + `Commander`(COM口、9600) | |
-| Z 绝对移动 | `VerticalMotorAbsoluteWait(pulse, motorDelay)` | 移完等 motorDelay 再抓图 |
-| 读 Z 位置 | `ReadVerticalMotorWait` | 事后校验 |
-| 水平移动 | `HorizontalMotorAbsoluteWait` | 皿孔定位 |
-| LED 开关 | `OpenLEDWait` / `CloseLEDWait` | 对焦前开、后关 |
-| 清晰度算分 | **自己写**(Tenengrad/Laplacian,OpenCvSharp 或纯 C#) | 不依赖服务器、不依赖 Project2.dll,方案要求本地可解释 |
-
-**最小闭环(手动):** 连相机 → 开 LED → 实时预览 + 显示当前帧清晰度分 → 手动按钮调曝光/增益、手动 Z±/水平± → 移动后回读位置 → 关 LED。打通这条就完成"第一步"。
-
----
-
-## 六、需要你真机/拍板确认的问题
-
-1. **水平轴 vs XY**:源码只有一个水平电机。皿孔居中真的只有单轴?还是另有未在此代码里的 XY 机构?(影响"居中"怎么做)
-2. **串口号**:现场相机串口是哪个 COM 口?(程序要选口)
-3. **motorDelay 真机值**:Z 移一层后等多少 ms 才稳?(决定抓图时机、防运动模糊)
-4. **Z 行程**:对焦零点、层间距(默认128脉冲)、上限(测试值125000)在你这台真机的实际值?固定范围拍几层?
-5. **光源亮度**:要不要程序调 LED 亮度?现协议只能开关+读取。若要调,得请硬件方加"写亮度"命令;否则曝光校正只用相机曝光+增益。
-6. **相机翻转**:保存/算分按 FlipY 后的图,对吗?
-7. **选层搬本地**:确认旧选层在服务器→我们新程序在本地自己算分选层(绕开服务器),对吗?
-
-> 确认 1/2/3/5/7 即可让我开始写第一步手动测试程序;4/6 可在联调时标定。

+ 137 - 0
自动对焦流程图.html

@@ -0,0 +1,137 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<title>自动对焦标定流程图 · AutoFocusTool</title>
+<style>
+  * { box-sizing: border-box; margin: 0; padding: 0; }
+  body { font-family: "Microsoft YaHei", sans-serif; background: #1e1e1e; color: #ddd; padding: 30px; line-height: 1.6; }
+  h1 { color: #4ec9b0; font-size: 26px; margin-bottom: 6px; }
+  h2 { color: #9cdcfe; font-size: 20px; margin: 28px 0 12px; border-left: 4px solid #4ec9b0; padding-left: 10px; }
+  .sub { color: #888; font-size: 13px; margin-bottom: 20px; }
+  .flow { display: flex; flex-direction: column; align-items: center; gap: 0; }
+  .step { background: #2d2d30; border: 2px solid #3c3c3c; border-radius: 8px; padding: 14px 20px; width: 760px; position: relative; }
+  .step.start { border-color: #1a7f37; background: #14301c; }
+  .step.phase { border-color: #4ec9b0; background: #18302c; }
+  .step.decision { border-color: #d79a3c; background: #332a18; border-radius: 30px; }
+  .step.done { border-color: #0e639c; background: #15293a; }
+  .step .t { font-size: 16px; font-weight: bold; color: #fff; margin-bottom: 6px; }
+  .step.phase .t { color: #4ec9b0; }
+  .step .d { font-size: 13px; color: #bbb; }
+  .step .p { font-size: 12px; color: #dcdcaa; margin-top: 8px; background: #1e1e1e; padding: 6px 10px; border-radius: 4px; font-family: Consolas, monospace; }
+  .arrow { width: 0; height: 0; border-left: 9px solid transparent; border-right: 9px solid transparent; border-top: 14px solid #555; margin: 4px 0; }
+  .arrow-label { font-size: 12px; color: #888; margin: 2px 0; }
+  table { border-collapse: collapse; width: 100%; margin: 12px 0; font-size: 13px; }
+  th, td { border: 1px solid #3c3c3c; padding: 8px 10px; text-align: left; }
+  th { background: #252526; color: #9cdcfe; }
+  td.param { font-family: Consolas, monospace; color: #dcdcaa; white-space: nowrap; }
+  td.val { font-family: Consolas, monospace; color: #4ec9b0; white-space: nowrap; }
+  .note { background: #332a18; border-left: 4px solid #d79a3c; padding: 10px 14px; margin: 14px 0; font-size: 13px; border-radius: 4px; }
+  .loop { border: 1px dashed #d79a3c; border-radius: 8px; padding: 10px; width: 800px; margin: 4px 0; }
+  .loop-title { color: #d79a3c; font-size: 13px; margin-bottom: 8px; text-align: center; }
+</style>
+</head>
+<body>
+
+<h1>自动对焦标定流程图</h1>
+<div class="sub">AutoFoczool · CalibrationEngine.CalibrateWell · 生成于 2026-06-16 · 所有参数取自当前代码真实默认值</div>
+
+<h2>一、整体流程(一键全自动初始化)</h2>
+<div class="flow">
+
+  <div class="step start"><div class="t">开始:一键全自动初始化</div><div class="d">扫描设备 → 连接舱室(相机+串口) → 勾选 well → 点击启动</div>
+    <div class="p">相机分辨率 2592×1944 · 串口 9600 8N1 · 移动读超时 12000ms / 查询读超时 3000ms</div></div>
+  <div class="arrow"></div>
+
+  <div class="step phase"><div class="t">开灯 + 准备</div><div class="d">OpenLED() → 等待 200ms → 创建 CalibrationEngine</div></div>
+  <div class="arrow"></div>
+
+  <div class="loop">
+    <div class="loop-title">↓↓↓ 对每个勾选的 well 循环执行 ↓↓↓</div>
+
+    <div class="flow">
+      <div class="step"><div class="t">读取该 well 的 EEPROM 参数</div>
+        <div class="d">ReadWellHorizontalPos(well) → 水平位置;ReadWellFocusZero(well) → Z零点(仅记录,粗对焦不依赖它)</div></div>
+      <div class="arrow"></div>
+
+      <div class="step"><div class="t">移到 EEPROM 水平位置 + 设中低曝光</div>
+        <div class="d">HorizontalMoveTo(钳位后位置),失败重试最多 3 次;SetExposure(60)</div>
+        <div class="p">水平钳位范围 [0, 220000] · 中低曝光 CenterScanExposure=60 (×100µs)</div></div>
+      <div class="arrow"></div>
+      <div class="arrow-label">① 粗对焦</div>
+
+      <div class="step phase"><div class="t">① 粗对焦(找大致焦面)</div>
+        <div class="d">固定中心大窗口逐层扫描,中央40%ROI算清晰度(Tenengrad÷mean),取最高分层</div>
+        <div class="p">中心 90000 · 半幅 ±30000 → 区间 [60000,120000] · 步距 2000 → 31层 · 每层等 350ms + 丢1帧</div></div>
+      <div class="arrow"></div>
+      <div class="arrow-label">② 水平居中</div>
+
+      <div class="step phase"><div class="t">② 水平微调居中(优化Y偏移)</div>
+        <div class="d">以EEPROM水平位为中心小范围扫描,检测well圆,选|Y偏移|最小且完整的位置</div>
+        <div class="p">半幅 ±2000 (远小于well间距9000的一半) · 9步 → 步距≈500 · 每步等800ms · 居中容差 |Y|<12%</div></div>
+      <div class="arrow"></div>
+      <div class="arrow-label">③ 曝光</div>
+
+      <div class="step phase"><div class="t">③ 曝光二分(well内ROI测光)</div>
+        <div class="d">二分搜索曝光值,使well盘面亮度适中(不过曝/不死黑),按 Over/Good/Under 判定</div>
+        <div class="p">曝光范围 [10, 800] (×100µs) · 每次设曝光后等 max(200, e/5)ms + 丢1帧</div></div>
+      <div class="arrow"></div>
+      <div class="arrow-label">④ 精对焦</div>
+
+      <div class="step phase"><div class="t">④ 精对焦(围绕粗焦点细扫 + 平滑插值)</div>
+        <div class="d">围绕粗对焦Z小范围密扫,well圆内0.95r ROI(未检出降级中央40%),3点平滑+抛物线插值求峰顶</div>
+        <div class="p">半幅 ±6000 · 步距 500 → 25层 · 中心=粗对焦Z · 每层等350ms+丢1帧 · Z钳位[0,125000]</div></div>
+      <div class="arrow"></div>
+
+      <div class="step"><div class="t">移到最清晰Z + 存图 + 记录结果</div>
+        <div class="d">VerticalMoveTo(focusZ);存标定后BMP;记录 水平/Y偏/曝光/focusZ/峰比</div>
+        <div class="p">合格判定:|Y偏|<12% 且 检出圆 且 峰比(max/mean)>1.2</div></div>
+    </div>
+  </div>
+  <div class="arrow"></div>
+  <div class="arrow-label">所有 well 完成</div>
+
+  <div class="step done"><div class="t">关灯 + 存档</div><div class="d">CloseLED();写 calibration.json;刷新缓存(转well立即用新结果)</div></div>
+
+</div>
+
+<h2>二、四个对焦阶段参数详表</h2>
+<table>
+  <tr><th>阶段</th><th>电机轴</th><th>中心</th><th>范围/半幅</th><th>步距(脉冲)</th><th>层数/步数</th><th>每步延时</th><th>ROI</th></tr>
+  <tr><td>①粗对焦</td><td>垂直Z</td><td class="val">90000(固定)</td><td class="val">±30000</td><td class="val">2000</td><td class="val">31</td><td class="val">350ms</td><td>中央40%</td></tr>
+  <tr><td>②水平居中</td><td>水平</td><td class="val">EEPROM水平位</td><td class="val">±2000</td><td class="val">≈500</td><td class="val">9</td><td class="val">800ms</td><td>well圆检测</td></tr>
+  <tr><td>③曝光二分</td><td>—</td><td>—</td><td class="val">[10,800]</td><td>二分</td><td class="val">~log₂</td><td class="val">max(200,e/5)ms</td><td>well内</td></tr>
+  <tr><td>④精对焦</td><td>垂直Z</td><td class="val">粗对焦Z</td><td class="val">±6000</td><td class="val">500</td><td class="val">25</td><td class="val">350ms</td><td>well圆0.95r</td></tr>
+</table>
+
+<h2>三、电机行程限位(安全钳位)</h2>
+<table>
+  <tr><th>轴</th><th>下限</th><th>上限</th><th>说明</th></tr>
+  <tr><td>水平(皿孔旋转)</td><td class="val">0</td><td class="val">220000</td><td>实测16个well EEPROM位置 70800~205800,间距约9000</td></tr>
+  <tr><td>垂直Z(对焦)</td><td class="val">0</td><td class="val">125000</td><td>旧工程软上限;实测焦面集中在 86000~92000</td></tr>
+</table>
+<div class="note">所有 HorizontalMoveTo / VerticalMoveTo 调用前都经 ClampH / ClampZ 钳位,越界写日志并截断到边界,防止撞机械限位。</div>
+
+<h2>四、清晰度算法(Sharpness.Compute)</h2>
+<table>
+  <tr><th>项</th><th>值/说明</th></tr>
+  <tr><td>主指标</td><td class="val">Tenengrad(Sobel梯度幅值平方和)</td></tr>
+  <tr><td>归一化</td><td class="val">sumSq / 像素数 / mean(除以亮度均值一次方)</td></tr>
+  <tr><td>峰值精修</td><td>3点滑动平均平滑 + 抛物线插值峰顶</td></tr>
+  <tr><td>对焦质量</td><td>峰比 max/mean,&lt;1.2 判定弱峰(可能空well/对焦失败)并告警</td></tr>
+</table>
+<div class="note">注:归一化用 ÷mean 而非 ÷mean²。早期用 ÷mean² 会因 Z 增大时画面变亮而把清晰帧压垮,导致选错最暗最糊层。改 ÷mean 后峰值正确落在真实焦面。</div>
+
+<h2>五、串口协议关键参数</h2>
+<table>
+  <tr><th>项</th><th>值</th></tr>
+  <tr><td>波特率</td><td class="val">9600 8N1</td></tr>
+  <tr><td>移动类命令读超时(CMD_MOTOR=0x05)</td><td class="val">12000ms(覆盖大行程移动到位耗时)</td></tr>
+  <tr><td>查询类命令读超时(握手/读EEPROM/读位置)</td><td class="val">3000ms</td></tr>
+  <tr><td>电机到位延时 MotorDelayMs</td><td class="val">默认1500ms(扫描时用更短的350/800ms提速)</td></tr>
+</table>
+<div class="note">移动命令是开环+固定延时:下位机回复只代表"收到指令",机械到位靠延时保证。大行程移动耗时随距离增长,故移动类读超时给到12秒。</div>
+
+</body>
+</html>