SmokeTest.cs 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161
  1. using System;
  2. using System.Linq;
  3. using System.Threading;
  4. using AutoFocusTool.Devices;
  5. using AutoFocusTool.Serial;
  6. using AutoFocusTool.Imaging;
  7. using AutoFocusTool.Calib;
  8. using SerialCamera = AutoFocusTool.Camera.Camera;
  9. namespace AutoFocusTool
  10. {
  11. /// <summary>
  12. /// 真机冒烟测试:验证第二轮修复,不跑完整标定(不大幅驱动电机)。
  13. /// 用法:SmokeTest.exe [testHouse=1] 1号舱=空皿测试舱,9号舱=有皿(已标定)
  14. /// 验证项:
  15. /// 1) 设备扫描 + COM/相机映射
  16. /// 2) P0-1: CalibrationManager 对已标定舱返回JSON参数;未标定降级EEPROM
  17. /// 3) P0-2/3: 设两档曝光,确认"丢帧后第2帧"亮度跟随曝光(旧帧污染会导致不跟随)
  18. /// 4) 相机抓帧通路
  19. /// </summary>
  20. internal class SmokeTest
  21. {
  22. const int W = 2592, H = 1944;
  23. const string JsonPath = @"C:\claudeFile\TL\AutoFocusTool\calibration.json";
  24. static int pass = 0, fail = 0;
  25. static void Ok(string m) { Console.WriteLine($" [PASS] {m}"); pass++; }
  26. static void No(string m) { Console.WriteLine($" [FAIL] {m}"); fail++; }
  27. static void Info(string m) { Console.WriteLine($" · {m}"); }
  28. static void Main(string[] args)
  29. {
  30. Console.OutputEncoding = System.Text.Encoding.UTF8;
  31. int testHouse = args.Length > 0 && int.TryParse(args[0], out int t) ? t : 1;
  32. Console.WriteLine($"==== 真机冒烟测试 (测试舱={testHouse}, 空皿用1号舱) ====\n");
  33. // ── 1) 设备扫描 ──
  34. Console.WriteLine("【1】设备扫描 + 映射");
  35. var scanner = new DeviceScanner { Log = m => Info(m) };
  36. var houses = scanner.ScanAll();
  37. if (houses.Count == 0) { No("未发现任何舱室,硬件未连接?终止"); Done(); return; }
  38. Ok($"发现 {houses.Count} 个舱室: {string.Join(",", houses.Select(h => h.HouseSn))}");
  39. foreach (var h in houses)
  40. Info($"舱{h.HouseSn} @ {h.PortName} 相机#{h.CcdIndex} CCDSN={h.CcdSn} 有相机={h.HasCamera}");
  41. // ── 2) P0-1 业务闭环:CalibrationManager 参数来源 ──
  42. Console.WriteLine("\n【2】P0-1 标定闭环 (CalibrationManager)");
  43. TestCalibManager();
  44. // ── 3+4) 相机通路 + P0-2/3 丢帧曝光跟随 ──
  45. Console.WriteLine($"\n【3】相机通路 + P0-2/3 丢帧曝光跟随 (舱{testHouse})");
  46. var th = houses.FirstOrDefault(x => x.HouseSn == testHouse && x.HasCamera)
  47. ?? houses.FirstOrDefault(x => x.HasCamera);
  48. if (th == null) { No("无可用相机舱,跳过相机测试"); Done(); return; }
  49. if (th.HouseSn != testHouse) Info($"舱{testHouse}不可用,改用舱{th.HouseSn}");
  50. TestCameraExposure(th);
  51. Done();
  52. }
  53. static void Done()
  54. {
  55. Console.WriteLine($"\n==== 结果: {pass} 通过 / {fail} 失败 ====");
  56. Console.WriteLine(fail == 0 ? "SMOKE TEST PASS" : "SMOKE TEST HAS FAILURES");
  57. }
  58. // P0-1: 验证 CalibrationManager 优先用JSON、降级EEPROM
  59. static void TestCalibManager()
  60. {
  61. if (!System.IO.File.Exists(JsonPath)) { No($"calibration.json 不存在: {JsonPath}"); return; }
  62. var file = CalibrationFile.Load(JsonPath);
  63. Info($"JSON: tlSn={file.TlSn} date={file.Date} 舱数={file.Houses.Count}");
  64. var mgr = new CalibrationManager();
  65. // 对JSON里第一个舱的well1: 应判定为已标定并返回JSON的水平脉冲(不读EEPROM,故传null motor安全?)
  66. // GetWellParams 合格时不触发EEPROM读取,可传null验证纯JSON路径
  67. var hc = file.Houses.FirstOrDefault();
  68. if (hc == null) { No("JSON无舱室数据"); return; }
  69. var w1 = hc.Wells.FirstOrDefault(w => w.CircleFound && w.PeakRatio > 1.2);
  70. if (w1 == null) { Info("JSON中无合格well(都PeakRatio<=1.2),闭环逻辑会全部降级EEPROM——符合空皿预期"); }
  71. // 验证 HasValidCalibration 判据
  72. if (w1 != null)
  73. {
  74. bool valid = mgr.HasValidCalibration(hc.House, w1.Well);
  75. if (valid) Ok($"舱{hc.House} well{w1.Well} 判定为已标定(峰比{w1.PeakRatio:F2}) → 转well将用JSON参数 H={w1.HorizontalPulse} Z={w1.FocusZ} 曝光={w1.Exposure}");
  76. else No($"舱{hc.House} well{w1.Well} 应合格却判不合格");
  77. // 合格路径不读EEPROM,传null motor验证不抛异常
  78. try {
  79. var (hp, z, exp) = mgr.GetWellParams(hc.House, w1.Well, null);
  80. if (hp == w1.HorizontalPulse && z == w1.FocusZ && exp == w1.Exposure)
  81. Ok($"GetWellParams 返回JSON参数(未触碰EEPROM): H={hp} Z={z} 曝光={exp}");
  82. else No($"GetWellParams 返回值与JSON不符: H={hp}/{w1.HorizontalPulse} Z={z}/{w1.FocusZ} exp={exp}/{w1.Exposure}");
  83. } catch (Exception ex) { No($"合格well不应读EEPROM却抛异常: {ex.Message}"); }
  84. }
  85. // 未标定舱(如99): 应降级,exp返回-1
  86. bool invalid = mgr.HasValidCalibration(99, 1);
  87. if (!invalid) Ok("未标定舱99 判定为不合格 → 将降级EEPROM (符合预期)");
  88. else No("未标定舱99 不应判合格");
  89. }
  90. // P0-2/3: 设两档曝光,丢帧后第2帧亮度应跟随曝光
  91. static void TestCameraExposure(HouseDevice h)
  92. {
  93. HouseMotor motor = null; SerialCamera cam = null;
  94. try
  95. {
  96. motor = new HouseMotor(h.PortName) { Log = _ => { }, MotorDelayMs = 1500 };
  97. if (!motor.Open()) { No($"{h.PortName} 打开失败"); return; }
  98. cam = new SerialCamera(h.CcdIndex, W, H, 120);
  99. if (cam.Init() != 0) { No("相机初始化失败"); return; }
  100. cam.SetOpMode(0);
  101. motor.OpenLED();
  102. Thread.Sleep(300);
  103. // 抓帧通路
  104. if (cam.GrabRgb() != 0) { No("抓帧失败"); return; }
  105. var b0 = cam.GetSourceBuffer();
  106. if (b0 == null || b0.Length != W * H * 3) { No($"帧尺寸异常: {b0?.Length}"); return; }
  107. Ok($"抓帧成功,帧大小 {b0.Length} 字节 ({W}x{H}x3)");
  108. // P0-2/3 核心: 模拟引擎"设曝光→丢帧→第2帧"序列,验证亮度跟随
  109. double lowMean = SetExpGrabStable(cam, 30);
  110. double highMean = SetExpGrabStable(cam, 150);
  111. Info($"曝光30 → 亮度均值={lowMean:F1}");
  112. Info($"曝光150 → 亮度均值={highMean:F1}");
  113. if (highMean > lowMean + 3)
  114. Ok($"丢帧后第2帧亮度随曝光上升({lowMean:F1}→{highMean:F1}),无旧帧污染");
  115. else
  116. No($"曝光升高但亮度未跟随({lowMean:F1}→{highMean:F1}),可能旧帧污染或光路异常");
  117. motor.CloseLED();
  118. }
  119. catch (Exception ex) { No($"相机测试异常: {ex.Message}"); }
  120. finally
  121. {
  122. try { cam?.UnInit(); cam?.Dispose(); } catch { }
  123. try { motor?.Close(); motor?.Dispose(); } catch { }
  124. }
  125. }
  126. // 复刻 CalibrationEngine 曝光二分里的稳定抓帧: 设曝光→等待→丢第1帧→用第2帧
  127. static double SetExpGrabStable(SerialCamera cam, int e)
  128. {
  129. cam.SetExposure(e);
  130. Thread.Sleep(Math.Max(200, e / 5));
  131. cam.GrabRgb(); // 丢弃第1帧(可能旧曝光)
  132. cam.GrabRgb(); // 第2帧
  133. return Mean(cam.GetSourceBuffer());
  134. }
  135. static double Mean(byte[] bgr)
  136. {
  137. long sum = 0; int n = 0;
  138. for (int i = 0; i + 2 < bgr.Length; i += 300) { sum += bgr[i] + bgr[i + 1] + bgr[i + 2]; n += 3; }
  139. return n > 0 ? sum / (double)n : 0;
  140. }
  141. }
  142. }