|
|
@@ -0,0 +1,131 @@
|
|
|
+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
|
|
|
+{
|
|
|
+ /// <summary>
|
|
|
+ /// 调试页"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 保证。
|
|
|
+ /// </summary>
|
|
|
+ public sealed class CalibrationManager
|
|
|
+ {
|
|
|
+ private readonly DebugSessionManager _debug;
|
|
|
+ // 按 houseSn 取该舱 HouseBin 的注入委托(Program.cs 传 sn => AppData.Instance.GetHouseBin(sn))。
|
|
|
+ // 用委托而非直接静态依赖,便于单测注入假对象;真实 HouseBin 提供 ReadWellFocusRange/阈值/tlSn/AutofocusStore。
|
|
|
+ private readonly Func<int, HouseBin> _houseBinOf;
|
|
|
+ private readonly Action<string> _log;
|
|
|
+
|
|
|
+ // 按 sid 管协作器:一个调试会话一份。Stop 后从字典移除。
|
|
|
+ private readonly ConcurrentDictionary<string, CalibrationCoordinator> _bySid
|
|
|
+ = new ConcurrentDictionary<string, CalibrationCoordinator>();
|
|
|
+
|
|
|
+ public CalibrationManager(DebugSessionManager debug, Func<int, HouseBin> houseBinOf, Action<string> log = null)
|
|
|
+ {
|
|
|
+ _debug = debug ?? throw new ArgumentNullException(nameof(debug));
|
|
|
+ _houseBinOf = houseBinOf ?? throw new ArgumentNullException(nameof(houseBinOf));
|
|
|
+ _log = log ?? (_ => { });
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// 起一次 16 孔标定。校验 sid → 取该舱 HouseBin/阈值 → 构造协作器(calibrateOne 闭包接真实引擎) → Start。
|
|
|
+ /// wells 为空默认 1..16。返回 Okay(无 result) / Fail(SESSION_EXPIRED|NO_HANDLE|BUSY)。
|
|
|
+ /// </summary>
|
|
|
+ public DebugCommandResult StartCalibrate(string sid, IReadOnlyList<int> 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<int, WellCalib> 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}"),
|
|
|
+ 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();
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>轮询进度:返回 CalibProgress 快照(无对应 sid → SESSION_EXPIRED)。</summary>
|
|
|
+ public DebugCommandResult GetProgress(string sid)
|
|
|
+ {
|
|
|
+ if (sid != null && _bySid.TryGetValue(sid, out var c))
|
|
|
+ return DebugCommandResult.Okay(c.GetProgress());
|
|
|
+ return DebugCommandResult.Fail("SESSION_EXPIRED", "无对应标定任务(会话不存在或未起标定)");
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>单孔重标(转调协作器 Recalibrate)。无对应 sid → SESSION_EXPIRED;批量跑中协作器自身忽略。</summary>
|
|
|
+ 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", "无对应标定任务(会话不存在或未起标定)");
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>中止标定并从字典移除该 sid 的协作器(lease 仍由 operate 的 DebugSession 持有,这里不还)。</summary>
|
|
|
+ public DebugCommandResult Stop(string sid)
|
|
|
+ {
|
|
|
+ if (sid != null && _bySid.TryRemove(sid, out var c))
|
|
|
+ {
|
|
|
+ c.Stop();
|
|
|
+ _log($"[calib] 停标定 sid={sid}");
|
|
|
+ }
|
|
|
+ return DebugCommandResult.Okay();
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|