using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using IvfTl.AutoFocus.Calib; using ivf_tl_Com; namespace IvfTl.ControlHost.Debug { /// /// 调试页"16 孔标定协作"管理器(D2-02 Task3.2a)。按 sid 持有每个调试会话的 CalibrationCoordinator, /// 把 Task3.1 的纯逻辑协作器接到真实硬件/引擎/落库: /// · 标定全程复用 operate 那个 DebugSession 借到的 lease(HardwareUser.OperateDebug)——control 采集该舱已暂停, /// 这里【绝不再 Acquire/Release lease】,只读 s.Lease.Serial / s.Lease.Camera; /// · 逐 well 注入 per-well 范围(HouseBin.ReadWellFocusRange) + 中心(DB优先,缺失回退硬件EEPROM), /// 调 CalibrationEngine.CalibrateWell,结果落 scene=0(调试页=出厂基准)。 /// 线程安全:_byS id 用 ConcurrentDictionary;单次标定的并发由 CalibrationCoordinator 内部 _lock 保证。 /// public sealed class CalibrationManager { private readonly DebugSessionManager _debug; // 按 houseSn 取该舱 HouseBin 的注入委托(Program.cs 传 sn => AppData.Instance.GetHouseBin(sn))。 // 用委托而非直接静态依赖,便于单测注入假对象;真实 HouseBin 提供 ReadWellFocusRange/阈值/tlSn/AutofocusStore。 private readonly Func _houseBinOf; private readonly Action _log; // 按 sid 管协作器:一个调试会话一份。Stop 后从字典移除。 private readonly ConcurrentDictionary _bySid = new ConcurrentDictionary(); // (Task3.2b) 标定时预览用的"每 sid 最新一帧 BGR + 宽高"缓冲。 // 标定中相机被 CalibrationEngine 独占抓帧,推流线程绝不能再 GrabStable(会和标定争相机原生锁); // 改由引擎 OnFrame 每次抓帧把原始帧塞进这里,推流线程只读这块缓冲、不碰相机。 // byte[] 整块引用赋值是原子的,读端拿到的要么是整块旧帧要么是整块新帧、不会撕裂,故无需锁。 private sealed class FrameSlot { public volatile byte[] Bgr; public int Width; public int Height; } private readonly ConcurrentDictionary _lastFrameBySid = new ConcurrentDictionary(); // OnFrame 高频触发(标定线程每次 Grab),存帧要轻:只做一次整块引用赋值 + 记宽高。 private void StoreLatestFrame(string sid, byte[] bgr, int w, int h) { if (bgr == null) return; var slot = _lastFrameBySid.GetOrAdd(sid, _ => new FrameSlot()); slot.Width = w; slot.Height = h; slot.Bgr = bgr; // 最后赋 volatile 引用,保证宽高先就位再发布该帧 } /// /// (Task3.2b) 该 sid 是否正在标定(有活跃协作器且 IsRunning)。推流线程据此决定: /// true→读 OnFrame 缓冲帧(不 GrabStable);false→走原 GrabStable。 /// public bool IsCalibrating(string sid) { return sid != null && _bySid.TryGetValue(sid, out var c) && c.GetProgress().IsRunning; } /// /// (Task3.2b) 取该 sid 最新一帧(引擎 OnFrame 喂的原始 BGR)。无帧返回 false。 /// 标定中即使此刻无帧,推流线程也绝不 GrabStable(避免争相机锁),跳过等下一帧即可。 /// public bool TryGetLatestFrame(string sid, out byte[] bgr, out int w, out int h) { bgr = null; w = 0; h = 0; if (sid == null || !_lastFrameBySid.TryGetValue(sid, out var slot)) return false; var buf = slot.Bgr; // 读 volatile 引用一次 if (buf == null) return false; bgr = buf; w = slot.Width; h = slot.Height; return true; } public CalibrationManager(DebugSessionManager debug, Func houseBinOf, Action log = null) { _debug = debug ?? throw new ArgumentNullException(nameof(debug)); _houseBinOf = houseBinOf ?? throw new ArgumentNullException(nameof(houseBinOf)); _log = log ?? (_ => { }); } /// /// 起一次 16 孔标定。校验 sid → 取该舱 HouseBin/阈值 → 构造协作器(calibrateOne 闭包接真实引擎) → Start。 /// wells 为空默认 1..16。返回 Okay(无 result) / Fail(SESSION_EXPIRED|NO_HANDLE|BUSY)。 /// public DebugCommandResult StartCalibrate(string sid, IReadOnlyList wells) { if (!_debug.TryGet(sid, out var s)) return DebugCommandResult.Fail("SESSION_EXPIRED", "会话不存在或已过期"); if (s.Lease?.Serial == null || s.Lease?.Camera == null) return DebugCommandResult.Fail("NO_HANDLE", "借用串口/相机句柄为空"); var houseBin = _houseBinOf(s.HouseSn); if (houseBin == null) return DebugCommandResult.Fail("NO_HANDLE", $"舱{s.HouseSn}无 HouseBin"); // 合格阈值:tl_setting.focusPeakRatioThreshold ?? 1.2(与现网 CalibrationEngine 弱峰判定一致)。 double peakThreshold = (double)(houseBin.FocusPeakRatioThreshold ?? 1.2m); string tlSn = houseBin.CalibTlSn; string port = houseBin.House?.housePort; string ccdSn = houseBin.House?.ccdSn; var targets = (wells != null && wells.Count > 0) ? wells.ToList() : Enumerable.Range(1, 16).ToList(); // calibrateOne 闭包(逐 well 在协作器后台线程跑):范围注入 + 中心兜底 + CalibrateWell + 落 scene=0。 Func calibrateOne = well => { var range = houseBin.ReadWellFocusRange(well); // DB优先 + 硬件兜底:DB 中心缺失(≤0)时回读硬件 EEPROM,避免中心=0 致 Z 粗扫错过真实焦面(沿用采集对焦 path B 范式)。 int hCenter = range.HCenter > 0 ? range.HCenter : Math.Max(0, s.Lease.Serial.ReadWellHorizontalPosWait(well)); int vCenter = range.VCenter > 0 ? range.VCenter : Math.Max(0, s.Lease.Serial.ReadWellFocusZeroWait(well)); var engine = new CalibrationEngine(s.Lease.Serial, s.Lease.Camera) { Log = msg => _log($"[calib][舱{s.HouseSn}][well{well}]{msg}"), // (Task3.2b) 引擎每次抓帧把原始 BGR 帧喂进 per-sid 缓冲,供推流线程标定时读取(替代 GrabStable,避免争相机锁)。 OnFrame = buf => StoreLatestFrame(sid, buf, s.Lease.Camera.Width, s.Lease.Camera.Height), HFineRange = range.HHalf, // 水平微调半幅(围绕 HCenter) ZCoarseCenter = vCenter, // Z 粗扫中心=该 well 清晰位(取代引擎固定 90000) ZCoarseHalf = range.VHalf, // Z 粗扫半幅 ExpLo = range.ExpLo, // 曝光二分下限 ExpHi = range.ExpHi, // 曝光二分上限 }; var wc = engine.CalibrateWell(well, hCenter, vCenter); // 调试页一键标定 = 出厂基准 → scene=0(upsert)。存储失败不崩标定(CalibrationStore 内部已吞异常)。 try { houseBin.AutofocusStore?.SaveCalibration(wc, tlSn, s.HouseSn, well, scene: 0, port: port, ccdSn: ccdSn); } catch (Exception ex) { _log($"[calib][舱{s.HouseSn}][well{well}] 落库失败(已忽略): {ex.Message}"); } return wc; }; var coordinator = new CalibrationCoordinator(calibrateOne, peakThreshold, msg => _log($"[calib][舱{s.HouseSn}]{msg}")); _bySid[sid] = coordinator; coordinator.Start(targets); _log($"[calib] 起标定 sid={sid} 舱{s.HouseSn} wells=[{string.Join(",", targets)}] 阈值={peakThreshold:F2}"); return DebugCommandResult.Okay(); } /// 轮询进度:返回 CalibProgress 快照(无对应 sid → SESSION_EXPIRED)。 public DebugCommandResult GetProgress(string sid) { if (sid != null && _bySid.TryGetValue(sid, out var c)) return DebugCommandResult.Okay(c.GetProgress()); return DebugCommandResult.Fail("SESSION_EXPIRED", "无对应标定任务(会话不存在或未起标定)"); } /// 单孔重标(转调协作器 Recalibrate)。无对应 sid → SESSION_EXPIRED;批量跑中协作器自身忽略。 public DebugCommandResult Recalibrate(string sid, int wellSn) { if (sid != null && _bySid.TryGetValue(sid, out var c)) { bool ok = c.Recalibrate(wellSn); return DebugCommandResult.Okay(ok); } return DebugCommandResult.Fail("SESSION_EXPIRED", "无对应标定任务(会话不存在或未起标定)"); } /// 中止标定并从字典移除该 sid 的协作器(lease 仍由 operate 的 DebugSession 持有,这里不还)。 public DebugCommandResult Stop(string sid) { if (sid != null && _bySid.TryRemove(sid, out var c)) { c.Stop(); _lastFrameBySid.TryRemove(sid, out _); // (Task3.2b) 清该 sid 帧缓冲,避免陈旧帧 + 释放内存 _log($"[calib] 停标定 sid={sid}"); } return DebugCommandResult.Okay(); } /// /// (Critical 并发修复)停该 sid 标定并【等当前孔跑完】后再返回,供 DebugSessionManager 在 /// 会话释放(Release/SweepExpired)Dispose lease 之前调用:确保标定后台线程已退出、不再操作即将 Dispose 的 lease, /// 之后 lease 才 Dispose、采集才恢复,消除标定线程与采集线程争用同一硬件的窗口。 /// 无对应 sid(该会话没在标定)→ no-op 返回 true,不阻塞 Dispose。超时返回 false(协作器内当前孔卡死)。 /// public bool StopAndWait(string sid, int timeoutMs) { if (sid == null || !_bySid.TryRemove(sid, out var c)) return true; bool ok = c.StopAndWait(timeoutMs); _lastFrameBySid.TryRemove(sid, out _); // 清该 sid 帧缓冲(同 Stop) _log($"[calib] 停并等标定 sid={sid} ok={ok}"); return ok; } } }