using System; using System.Linq; using System.Threading; using AutoFocusTool.Devices; using AutoFocusTool.Serial; using AutoFocusTool.Imaging; using AutoFocusTool.Calib; using SerialCamera = AutoFocusTool.Camera.Camera; namespace AutoFocusTool { /// /// 真机冒烟测试:验证第二轮修复,不跑完整标定(不大幅驱动电机)。 /// 用法:SmokeTest.exe [testHouse=1] 1号舱=空皿测试舱,9号舱=有皿(已标定) /// 验证项: /// 1) 设备扫描 + COM/相机映射 /// 2) P0-1: CalibrationManager 对已标定舱返回JSON参数;未标定降级EEPROM /// 3) P0-2/3: 设两档曝光,确认"丢帧后第2帧"亮度跟随曝光(旧帧污染会导致不跟随) /// 4) 相机抓帧通路 /// internal class SmokeTest { const int W = 2592, H = 1944; const string JsonPath = @"C:\claudeFile\TL\AutoFocusTool\calibration.json"; static int pass = 0, fail = 0; static void Ok(string m) { Console.WriteLine($" [PASS] {m}"); pass++; } static void No(string m) { Console.WriteLine($" [FAIL] {m}"); fail++; } static void Info(string m) { Console.WriteLine($" · {m}"); } static void Main(string[] args) { Console.OutputEncoding = System.Text.Encoding.UTF8; int testHouse = args.Length > 0 && int.TryParse(args[0], out int t) ? t : 1; Console.WriteLine($"==== 真机冒烟测试 (测试舱={testHouse}, 空皿用1号舱) ====\n"); // ── 1) 设备扫描 ── Console.WriteLine("【1】设备扫描 + 映射"); var scanner = new DeviceScanner { Log = m => Info(m) }; var houses = scanner.ScanAll(); if (houses.Count == 0) { No("未发现任何舱室,硬件未连接?终止"); Done(); return; } Ok($"发现 {houses.Count} 个舱室: {string.Join(",", houses.Select(h => h.HouseSn))}"); foreach (var h in houses) Info($"舱{h.HouseSn} @ {h.PortName} 相机#{h.CcdIndex} CCDSN={h.CcdSn} 有相机={h.HasCamera}"); // ── 2) P0-1 业务闭环:CalibrationManager 参数来源 ── Console.WriteLine("\n【2】P0-1 标定闭环 (CalibrationManager)"); TestCalibManager(); // ── 3+4) 相机通路 + P0-2/3 丢帧曝光跟随 ── Console.WriteLine($"\n【3】相机通路 + P0-2/3 丢帧曝光跟随 (舱{testHouse})"); var th = houses.FirstOrDefault(x => x.HouseSn == testHouse && x.HasCamera) ?? houses.FirstOrDefault(x => x.HasCamera); if (th == null) { No("无可用相机舱,跳过相机测试"); Done(); return; } if (th.HouseSn != testHouse) Info($"舱{testHouse}不可用,改用舱{th.HouseSn}"); TestCameraExposure(th); Done(); } static void Done() { Console.WriteLine($"\n==== 结果: {pass} 通过 / {fail} 失败 ===="); Console.WriteLine(fail == 0 ? "SMOKE TEST PASS" : "SMOKE TEST HAS FAILURES"); } // P0-1: 验证 CalibrationManager 优先用JSON、降级EEPROM static void TestCalibManager() { if (!System.IO.File.Exists(JsonPath)) { No($"calibration.json 不存在: {JsonPath}"); return; } var file = CalibrationFile.Load(JsonPath); Info($"JSON: tlSn={file.TlSn} date={file.Date} 舱数={file.Houses.Count}"); var mgr = new CalibrationManager(); // 对JSON里第一个舱的well1: 应判定为已标定并返回JSON的水平脉冲(不读EEPROM,故传null motor安全?) // GetWellParams 合格时不触发EEPROM读取,可传null验证纯JSON路径 var hc = file.Houses.FirstOrDefault(); if (hc == null) { No("JSON无舱室数据"); return; } var w1 = hc.Wells.FirstOrDefault(w => w.CircleFound && w.PeakRatio > 1.2); if (w1 == null) { Info("JSON中无合格well(都PeakRatio<=1.2),闭环逻辑会全部降级EEPROM——符合空皿预期"); } // 验证 HasValidCalibration 判据 if (w1 != null) { bool valid = mgr.HasValidCalibration(hc.House, w1.Well); if (valid) Ok($"舱{hc.House} well{w1.Well} 判定为已标定(峰比{w1.PeakRatio:F2}) → 转well将用JSON参数 H={w1.HorizontalPulse} Z={w1.FocusZ} 曝光={w1.Exposure}"); else No($"舱{hc.House} well{w1.Well} 应合格却判不合格"); // 合格路径不读EEPROM,传null motor验证不抛异常 try { var (hp, z, exp) = mgr.GetWellParams(hc.House, w1.Well, null); if (hp == w1.HorizontalPulse && z == w1.FocusZ && exp == w1.Exposure) Ok($"GetWellParams 返回JSON参数(未触碰EEPROM): H={hp} Z={z} 曝光={exp}"); else No($"GetWellParams 返回值与JSON不符: H={hp}/{w1.HorizontalPulse} Z={z}/{w1.FocusZ} exp={exp}/{w1.Exposure}"); } catch (Exception ex) { No($"合格well不应读EEPROM却抛异常: {ex.Message}"); } } // 未标定舱(如99): 应降级,exp返回-1 bool invalid = mgr.HasValidCalibration(99, 1); if (!invalid) Ok("未标定舱99 判定为不合格 → 将降级EEPROM (符合预期)"); else No("未标定舱99 不应判合格"); } // P0-2/3: 设两档曝光,丢帧后第2帧亮度应跟随曝光 static void TestCameraExposure(HouseDevice h) { HouseMotor motor = null; SerialCamera cam = null; try { motor = new HouseMotor(h.PortName) { Log = _ => { }, MotorDelayMs = 1500 }; if (!motor.Open()) { No($"{h.PortName} 打开失败"); return; } cam = new SerialCamera(h.CcdIndex, W, H, 120); if (cam.Init() != 0) { No("相机初始化失败"); return; } cam.SetOpMode(0); motor.OpenLED(); Thread.Sleep(300); // 抓帧通路 if (cam.GrabRgb() != 0) { No("抓帧失败"); return; } var b0 = cam.GetSourceBuffer(); if (b0 == null || b0.Length != W * H * 3) { No($"帧尺寸异常: {b0?.Length}"); return; } Ok($"抓帧成功,帧大小 {b0.Length} 字节 ({W}x{H}x3)"); // P0-2/3 核心: 模拟引擎"设曝光→丢帧→第2帧"序列,验证亮度跟随 double lowMean = SetExpGrabStable(cam, 30); double highMean = SetExpGrabStable(cam, 150); Info($"曝光30 → 亮度均值={lowMean:F1}"); Info($"曝光150 → 亮度均值={highMean:F1}"); if (highMean > lowMean + 3) Ok($"丢帧后第2帧亮度随曝光上升({lowMean:F1}→{highMean:F1}),无旧帧污染"); else No($"曝光升高但亮度未跟随({lowMean:F1}→{highMean:F1}),可能旧帧污染或光路异常"); motor.CloseLED(); } catch (Exception ex) { No($"相机测试异常: {ex.Message}"); } finally { try { cam?.UnInit(); cam?.Dispose(); } catch { } try { motor?.Close(); motor?.Dispose(); } catch { } } } // 复刻 CalibrationEngine 曝光二分里的稳定抓帧: 设曝光→等待→丢第1帧→用第2帧 static double SetExpGrabStable(SerialCamera cam, int e) { cam.SetExposure(e); Thread.Sleep(Math.Max(200, e / 5)); cam.GrabRgb(); // 丢弃第1帧(可能旧曝光) cam.GrabRgb(); // 第2帧 return Mean(cam.GetSourceBuffer()); } static double Mean(byte[] bgr) { long sum = 0; int n = 0; for (int i = 0; i + 2 < bgr.Length; i += 300) { sum += bgr[i] + bgr[i + 1] + bgr[i + 2]; n += 3; } return n > 0 ? sum / (double)n : 0; } } }