|
@@ -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 范围一致。
|
|
|
|
|
-
|
|
|