CalibrationManager.cs 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182
  1. using System;
  2. using System.Collections.Concurrent;
  3. using System.Collections.Generic;
  4. using System.Linq;
  5. using IvfTl.AutoFocus.Calib;
  6. using ivf_tl_Com;
  7. namespace IvfTl.ControlHost.Debug
  8. {
  9. /// <summary>
  10. /// 调试页"16 孔标定协作"管理器(D2-02 Task3.2a)。按 sid 持有每个调试会话的 CalibrationCoordinator,
  11. /// 把 Task3.1 的纯逻辑协作器接到真实硬件/引擎/落库:
  12. /// · 标定全程复用 operate 那个 DebugSession 借到的 lease(HardwareUser.OperateDebug)——control 采集该舱已暂停,
  13. /// 这里【绝不再 Acquire/Release lease】,只读 s.Lease.Serial / s.Lease.Camera;
  14. /// · 逐 well 注入 per-well 范围(HouseBin.ReadWellFocusRange) + 中心(DB优先,缺失回退硬件EEPROM),
  15. /// 调 CalibrationEngine.CalibrateWell,结果落 scene=0(调试页=出厂基准)。
  16. /// 线程安全:_byS id 用 ConcurrentDictionary;单次标定的并发由 CalibrationCoordinator 内部 _lock 保证。
  17. /// </summary>
  18. public sealed class CalibrationManager
  19. {
  20. private readonly DebugSessionManager _debug;
  21. // 按 houseSn 取该舱 HouseBin 的注入委托(Program.cs 传 sn => AppData.Instance.GetHouseBin(sn))。
  22. // 用委托而非直接静态依赖,便于单测注入假对象;真实 HouseBin 提供 ReadWellFocusRange/阈值/tlSn/AutofocusStore。
  23. private readonly Func<int, HouseBin> _houseBinOf;
  24. private readonly Action<string> _log;
  25. // 按 sid 管协作器:一个调试会话一份。Stop 后从字典移除。
  26. private readonly ConcurrentDictionary<string, CalibrationCoordinator> _bySid
  27. = new ConcurrentDictionary<string, CalibrationCoordinator>();
  28. // (Task3.2b) 标定时预览用的"每 sid 最新一帧 BGR + 宽高"缓冲。
  29. // 标定中相机被 CalibrationEngine 独占抓帧,推流线程绝不能再 GrabStable(会和标定争相机原生锁);
  30. // 改由引擎 OnFrame 每次抓帧把原始帧塞进这里,推流线程只读这块缓冲、不碰相机。
  31. // byte[] 整块引用赋值是原子的,读端拿到的要么是整块旧帧要么是整块新帧、不会撕裂,故无需锁。
  32. private sealed class FrameSlot
  33. {
  34. public volatile byte[] Bgr;
  35. public int Width;
  36. public int Height;
  37. }
  38. private readonly ConcurrentDictionary<string, FrameSlot> _lastFrameBySid
  39. = new ConcurrentDictionary<string, FrameSlot>();
  40. // OnFrame 高频触发(标定线程每次 Grab),存帧要轻:只做一次整块引用赋值 + 记宽高。
  41. private void StoreLatestFrame(string sid, byte[] bgr, int w, int h)
  42. {
  43. if (bgr == null) return;
  44. var slot = _lastFrameBySid.GetOrAdd(sid, _ => new FrameSlot());
  45. slot.Width = w;
  46. slot.Height = h;
  47. slot.Bgr = bgr; // 最后赋 volatile 引用,保证宽高先就位再发布该帧
  48. }
  49. /// <summary>
  50. /// (Task3.2b) 该 sid 是否正在标定(有活跃协作器且 IsRunning)。推流线程据此决定:
  51. /// true→读 OnFrame 缓冲帧(不 GrabStable);false→走原 GrabStable。
  52. /// </summary>
  53. public bool IsCalibrating(string sid)
  54. {
  55. return sid != null
  56. && _bySid.TryGetValue(sid, out var c)
  57. && c.GetProgress().IsRunning;
  58. }
  59. /// <summary>
  60. /// (Task3.2b) 取该 sid 最新一帧(引擎 OnFrame 喂的原始 BGR)。无帧返回 false。
  61. /// 标定中即使此刻无帧,推流线程也绝不 GrabStable(避免争相机锁),跳过等下一帧即可。
  62. /// </summary>
  63. public bool TryGetLatestFrame(string sid, out byte[] bgr, out int w, out int h)
  64. {
  65. bgr = null; w = 0; h = 0;
  66. if (sid == null || !_lastFrameBySid.TryGetValue(sid, out var slot)) return false;
  67. var buf = slot.Bgr; // 读 volatile 引用一次
  68. if (buf == null) return false;
  69. bgr = buf; w = slot.Width; h = slot.Height;
  70. return true;
  71. }
  72. public CalibrationManager(DebugSessionManager debug, Func<int, HouseBin> houseBinOf, Action<string> log = null)
  73. {
  74. _debug = debug ?? throw new ArgumentNullException(nameof(debug));
  75. _houseBinOf = houseBinOf ?? throw new ArgumentNullException(nameof(houseBinOf));
  76. _log = log ?? (_ => { });
  77. }
  78. /// <summary>
  79. /// 起一次 16 孔标定。校验 sid → 取该舱 HouseBin/阈值 → 构造协作器(calibrateOne 闭包接真实引擎) → Start。
  80. /// wells 为空默认 1..16。返回 Okay(无 result) / Fail(SESSION_EXPIRED|NO_HANDLE|BUSY)。
  81. /// </summary>
  82. public DebugCommandResult StartCalibrate(string sid, IReadOnlyList<int> wells)
  83. {
  84. if (!_debug.TryGet(sid, out var s))
  85. return DebugCommandResult.Fail("SESSION_EXPIRED", "会话不存在或已过期");
  86. if (s.Lease?.Serial == null || s.Lease?.Camera == null)
  87. return DebugCommandResult.Fail("NO_HANDLE", "借用串口/相机句柄为空");
  88. var houseBin = _houseBinOf(s.HouseSn);
  89. if (houseBin == null)
  90. return DebugCommandResult.Fail("NO_HANDLE", $"舱{s.HouseSn}无 HouseBin");
  91. // 合格阈值:tl_setting.focusPeakRatioThreshold ?? 1.2(与现网 CalibrationEngine 弱峰判定一致)。
  92. double peakThreshold = (double)(houseBin.FocusPeakRatioThreshold ?? 1.2m);
  93. string tlSn = houseBin.CalibTlSn;
  94. string port = houseBin.House?.housePort;
  95. string ccdSn = houseBin.House?.ccdSn;
  96. var targets = (wells != null && wells.Count > 0)
  97. ? wells.ToList()
  98. : Enumerable.Range(1, 16).ToList();
  99. // calibrateOne 闭包(逐 well 在协作器后台线程跑):范围注入 + 中心兜底 + CalibrateWell + 落 scene=0。
  100. Func<int, WellCalib> calibrateOne = well =>
  101. {
  102. var range = houseBin.ReadWellFocusRange(well);
  103. // DB优先 + 硬件兜底:DB 中心缺失(≤0)时回读硬件 EEPROM,避免中心=0 致 Z 粗扫错过真实焦面(沿用采集对焦 path B 范式)。
  104. int hCenter = range.HCenter > 0 ? range.HCenter : Math.Max(0, s.Lease.Serial.ReadWellHorizontalPosWait(well));
  105. int vCenter = range.VCenter > 0 ? range.VCenter : Math.Max(0, s.Lease.Serial.ReadWellFocusZeroWait(well));
  106. var engine = new CalibrationEngine(s.Lease.Serial, s.Lease.Camera)
  107. {
  108. Log = msg => _log($"[calib][舱{s.HouseSn}][well{well}]{msg}"),
  109. // (Task3.2b) 引擎每次抓帧把原始 BGR 帧喂进 per-sid 缓冲,供推流线程标定时读取(替代 GrabStable,避免争相机锁)。
  110. OnFrame = buf => StoreLatestFrame(sid, buf, s.Lease.Camera.Width, s.Lease.Camera.Height),
  111. HFineRange = range.HHalf, // 水平微调半幅(围绕 HCenter)
  112. ZCoarseCenter = vCenter, // Z 粗扫中心=该 well 清晰位(取代引擎固定 90000)
  113. ZCoarseHalf = range.VHalf, // Z 粗扫半幅
  114. ExpLo = range.ExpLo, // 曝光二分下限
  115. ExpHi = range.ExpHi, // 曝光二分上限
  116. };
  117. var wc = engine.CalibrateWell(well, hCenter, vCenter);
  118. // 调试页一键标定 = 出厂基准 → scene=0(upsert)。存储失败不崩标定(CalibrationStore 内部已吞异常)。
  119. try
  120. {
  121. houseBin.AutofocusStore?.SaveCalibration(wc, tlSn, s.HouseSn, well, scene: 0,
  122. port: port, ccdSn: ccdSn);
  123. }
  124. catch (Exception ex) { _log($"[calib][舱{s.HouseSn}][well{well}] 落库失败(已忽略): {ex.Message}"); }
  125. return wc;
  126. };
  127. var coordinator = new CalibrationCoordinator(calibrateOne, peakThreshold,
  128. msg => _log($"[calib][舱{s.HouseSn}]{msg}"));
  129. _bySid[sid] = coordinator;
  130. coordinator.Start(targets);
  131. _log($"[calib] 起标定 sid={sid} 舱{s.HouseSn} wells=[{string.Join(",", targets)}] 阈值={peakThreshold:F2}");
  132. return DebugCommandResult.Okay();
  133. }
  134. /// <summary>轮询进度:返回 CalibProgress 快照(无对应 sid → SESSION_EXPIRED)。</summary>
  135. public DebugCommandResult GetProgress(string sid)
  136. {
  137. if (sid != null && _bySid.TryGetValue(sid, out var c))
  138. return DebugCommandResult.Okay(c.GetProgress());
  139. return DebugCommandResult.Fail("SESSION_EXPIRED", "无对应标定任务(会话不存在或未起标定)");
  140. }
  141. /// <summary>单孔重标(转调协作器 Recalibrate)。无对应 sid → SESSION_EXPIRED;批量跑中协作器自身忽略。</summary>
  142. public DebugCommandResult Recalibrate(string sid, int wellSn)
  143. {
  144. if (sid != null && _bySid.TryGetValue(sid, out var c))
  145. {
  146. bool ok = c.Recalibrate(wellSn);
  147. return DebugCommandResult.Okay(ok);
  148. }
  149. return DebugCommandResult.Fail("SESSION_EXPIRED", "无对应标定任务(会话不存在或未起标定)");
  150. }
  151. /// <summary>中止标定并从字典移除该 sid 的协作器(lease 仍由 operate 的 DebugSession 持有,这里不还)。</summary>
  152. public DebugCommandResult Stop(string sid)
  153. {
  154. if (sid != null && _bySid.TryRemove(sid, out var c))
  155. {
  156. c.Stop();
  157. _lastFrameBySid.TryRemove(sid, out _); // (Task3.2b) 清该 sid 帧缓冲,避免陈旧帧 + 释放内存
  158. _log($"[calib] 停标定 sid={sid}");
  159. }
  160. return DebugCommandResult.Okay();
  161. }
  162. }
  163. }