|
@@ -5,12 +5,12 @@ using ivf_tl_Entity.DebugEntitys;
|
|
|
using ivf_tl_Entity.DTO;
|
|
using ivf_tl_Entity.DTO;
|
|
|
using ivf_tl_Entity.GlobalEnums;
|
|
using ivf_tl_Entity.GlobalEnums;
|
|
|
using ivf_tl_Operate.Converts;
|
|
using ivf_tl_Operate.Converts;
|
|
|
|
|
+using ivf_tl_Operate.Debug;
|
|
|
|
|
+using ivf_tl_Operate.Helpers;
|
|
|
using ivf_tl_Services;
|
|
using ivf_tl_Services;
|
|
|
using IvfTl.Hardware;
|
|
using IvfTl.Hardware;
|
|
|
using IvfTl.Hardware.Impl;
|
|
using IvfTl.Hardware.Impl;
|
|
|
-using IvfTl.AutoFocus.Calib;
|
|
|
|
|
using IvfTl.AutoFocus.Layout;
|
|
using IvfTl.AutoFocus.Layout;
|
|
|
-using IvfTl.AutoFocus.Storage;
|
|
|
|
|
using Newtonsoft.Json;
|
|
using Newtonsoft.Json;
|
|
|
using Newtonsoft.Json.Linq;
|
|
using Newtonsoft.Json.Linq;
|
|
|
using System;
|
|
using System;
|
|
@@ -56,6 +56,23 @@ namespace ivf_tl_Operate.ViewModel
|
|
|
/// </summary>
|
|
/// </summary>
|
|
|
private IHardwareLease _halLease = null;
|
|
private IHardwareLease _halLease = null;
|
|
|
|
|
|
|
|
|
|
+ // ── 自动对焦重构 D2-02-T3.4:调试页标定改"经 control 协作",不再 operate 本地自建 CalibrationEngine ──
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// 调试会话客户端:经 control 本地 HTTP /debug/acquire 借该舱硬件,拿 sessionId(存 CurrentSessionId)。
|
|
|
|
|
+ /// control 独立进程持有真硬件;operate 进程内 HAL 是空壳(从不 ScanDevices),故借硬件/预览/标定全走此 HTTP 协作。
|
|
|
|
|
+ /// 借到后起心跳;ComHouseUnit 归还。会话失效回调提示重进调试。
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ private DebugSessionClient _sessionClient;
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// 16 孔标定协作客户端:复用同一 sessionId,封 control /debug/calibrate/{start,progress,recalibrate,stop}。
|
|
|
|
|
+ /// acquire 成功后构造;ComHouseUnit 释放。
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ private CalibrationClient _calibClient;
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>标定进度轮询任务的取消源(IsRunning 转 false 或停止标定时取消)。</summary>
|
|
|
|
|
+ private System.Threading.CancellationTokenSource _calibPollCts;
|
|
|
|
|
+
|
|
|
/// <summary>
|
|
/// <summary>
|
|
|
/// 当前housesn
|
|
/// 当前housesn
|
|
|
/// </summary>
|
|
/// </summary>
|
|
@@ -251,6 +268,16 @@ namespace ivf_tl_Operate.ViewModel
|
|
|
try
|
|
try
|
|
|
{
|
|
{
|
|
|
var currentHouse = HouseList.FirstOrDefault(x => x.houseSn == CurrentHouseId);
|
|
var currentHouse = HouseList.FirstOrDefault(x => x.houseSn == CurrentHouseId);
|
|
|
|
|
+ if (currentHouse == null)
|
|
|
|
|
+ {
|
|
|
|
|
+ AddMessageInfo($"[{CurrentHouseId}]未获取到舱室信息,无法进入调试");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // D2-02-T3.4:先经 control HTTP 协作借该舱调试会话拿 sessionId(预览/标定/会话命令均依赖)。
|
|
|
|
|
+ // control 独立进程持真硬件;operate 进程内 HAL 是空壳(从不 ScanDevices)、下面的本地 lease.Serial 取不到,
|
|
|
|
|
+ // 故借硬件的"真正入口"是这里的 HTTP acquire。放在本地 HAL 借用之前,确保 sessionId 一定先建立。
|
|
|
|
|
+ await AcquireDebugSessionAsync(currentHouse.houseSn);
|
|
|
|
|
|
|
|
// M1-03/B2 HAL: 向 HAL 申请该舱前台借用——HAL 暂停 control 对本舱采集(T1.3)并让采集端让出串口/相机,
|
|
// M1-03/B2 HAL: 向 HAL 申请该舱前台借用——HAL 暂停 control 对本舱采集(T1.3)并让采集端让出串口/相机,
|
|
|
// 调试页随后复用借用到的同一物理句柄(lease.Serial/lease.Camera),不再 new 第二个 ComBin/Camera 开同口/同相机。
|
|
// 调试页随后复用借用到的同一物理句柄(lease.Serial/lease.Camera),不再 new 第二个 ComBin/Camera 开同口/同相机。
|
|
@@ -359,6 +386,10 @@ namespace ivf_tl_Operate.ViewModel
|
|
|
{
|
|
{
|
|
|
try
|
|
try
|
|
|
{
|
|
{
|
|
|
|
|
+ // D2-02-T3.4:先停标定轮询、归还 control 协作会话(停心跳),再归还本地 HAL 借用。
|
|
|
|
|
+ StopCalibPolling();
|
|
|
|
|
+ ReleaseDebugSession();
|
|
|
|
|
+
|
|
|
// M1-B2:物理串口/相机由采集端(control)持有,调试只归还借用,绝不 ClosePort/UnInit 关物理口——
|
|
// M1-B2:物理串口/相机由采集端(control)持有,调试只归还借用,绝不 ClosePort/UnInit 关物理口——
|
|
|
// 否则会把采集端的口/相机一并关掉。归还后 HAL 触发 ResumeCapture 恢复本舱采集(T1.3)。
|
|
// 否则会把采集端的口/相机一并关掉。归还后 HAL 触发 ResumeCapture 恢复本舱采集(T1.3)。
|
|
|
if (_halLease != null)
|
|
if (_halLease != null)
|
|
@@ -377,6 +408,75 @@ namespace ivf_tl_Operate.ViewModel
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ #region D2-02-T3.4 调试会话协作(借硬件拿 sessionId / 归还)
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// 向 control 借该舱调试会话:POST /debug/acquire {houseSn} → 拿 sessionId 存 CurrentSessionId,
|
|
|
|
|
+ /// 并构造 16 孔标定协作客户端 CalibrationClient(复用同一 sessionId)。
|
|
|
|
|
+ /// control 借用成功会起心跳;会话失效(SESSION_EXPIRED)回调提示工程师重进调试。
|
|
|
|
|
+ /// 借不到(BUSY/会话异常)只提示不抛——预览/标定将因 CurrentSessionId 为空而拒绝执行。
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ private async Task AcquireDebugSessionAsync(int houseSn)
|
|
|
|
|
+ {
|
|
|
|
|
+ try
|
|
|
|
|
+ {
|
|
|
|
|
+ // 关旧会话(重复初始化时)。
|
|
|
|
|
+ ReleaseDebugSession();
|
|
|
|
|
+
|
|
|
|
|
+ var client = new DebugSessionClient(ControlClient.BaseUrl);
|
|
|
|
|
+ client.OnSessionExpired = () =>
|
|
|
|
|
+ {
|
|
|
|
|
+ CurrentSessionId = null;
|
|
|
|
|
+ AddMessageInfo("⚠ 调试会话已失效(control 端超时回收),请退出后重新进入调试");
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ var r = await client.AcquireAsync(houseSn);
|
|
|
|
|
+ if (!r.Ok || string.IsNullOrEmpty(r.SessionId))
|
|
|
|
|
+ {
|
|
|
|
|
+ try { client.Dispose(); } catch { }
|
|
|
|
|
+ AddMessageInfo($"[{houseSn}]借调试会话失败:{r.Code}/{r.Error}(预览与标定不可用,请确认 control 已托管该舱)");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ _sessionClient = client;
|
|
|
|
|
+ CurrentSessionId = r.SessionId;
|
|
|
|
|
+ // 标定协作客户端复用同一 baseUrl + sessionId。
|
|
|
|
|
+ _calibClient = new CalibrationClient(ControlClient.BaseUrl, r.SessionId);
|
|
|
|
|
+
|
|
|
|
|
+ string cultMsg = r.Cultivating ? $"(注意:该舱培养中,胚胎 {r.EmbryoCount} 枚)" : "";
|
|
|
|
|
+ AddMessageInfo($"[{houseSn}]调试会话已建立 sessionId={r.SessionId}{cultMsg}");
|
|
|
|
|
+ }
|
|
|
|
|
+ catch (Exception ex)
|
|
|
|
|
+ {
|
|
|
|
|
+ ExLog(ex, "AcquireDebugSessionAsync");
|
|
|
|
|
+ AddMessageInfo($"[{houseSn}]借调试会话异常:{ex.Message}");
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>归还调试会话(停心跳、通知 control)并释放协作客户端。幂等。
|
|
|
|
|
+ /// release 的 HTTP POST 放后台跑(避免 control 失联时 HttpClient 默认 100s 超时卡死 UI 线程);
|
|
|
|
|
+ /// DebugSessionClient.ReleaseAsync 在 await POST 前已同步置闸+停心跳,故心跳即时停止、不必等 POST。</summary>
|
|
|
|
|
+ private void ReleaseDebugSession()
|
|
|
|
|
+ {
|
|
|
|
|
+ var calib = _calibClient; _calibClient = null;
|
|
|
|
|
+ if (calib != null) { try { calib.Dispose(); } catch { } }
|
|
|
|
|
+
|
|
|
|
|
+ var sc = _sessionClient; _sessionClient = null;
|
|
|
|
|
+ if (sc != null)
|
|
|
|
|
+ {
|
|
|
|
|
+ // 后台 best-effort 归还+释放;失败也无妨(control 端 TTL 看门狗会超时回收会话)。
|
|
|
|
|
+ Task.Run(async () =>
|
|
|
|
|
+ {
|
|
|
|
|
+ try { await sc.ReleaseAsync(); } catch { }
|
|
|
|
|
+ try { sc.Dispose(); } catch { }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ CurrentSessionId = null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ #endregion
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
/// <summary>
|
|
/// <summary>
|
|
|
/// 读温度
|
|
/// 读温度
|
|
|
/// </summary>
|
|
/// </summary>
|
|
@@ -1117,194 +1217,235 @@ namespace ivf_tl_Operate.ViewModel
|
|
|
AddPicEvent?.Invoke(picFullName, CurrentWell, CurrentFocal);
|
|
AddPicEvent?.Invoke(picFullName, CurrentWell, CurrentFocal);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- #region M2-05 场景A 一键全自动标定(沙盒)
|
|
|
|
|
|
|
+ #region 场景A 一键全自动标定(经 control 协作,D2-02-T3.4 重构)
|
|
|
|
|
|
|
|
/// <summary>
|
|
/// <summary>
|
|
|
- /// M2-05 场景A 工程师调试页一键全自动标定(人盯安全沙盒,03 §2/§7.1)。
|
|
|
|
|
- /// 流程:对选中 well 列表逐 well 跑 M2-01 四步标定引擎(CalibrationEngine) →
|
|
|
|
|
- /// 收集 WellCalib → 实时 UI 合格绿/伪峰红 → 结果作出厂基准 scene=0 落库 + 写 calibration.json。
|
|
|
|
|
- ///
|
|
|
|
|
- /// 硬件复用:调试页 ComHouseInit 已经 Acquire(OperateDebug) 前台借用(HAL 暂停 control 采集),
|
|
|
|
|
- /// 借用到的 lease.Serial/lease.Camera 即与采集端同一物理句柄(M1-B2)。标定引擎直接用这两个 HAL 接口,
|
|
|
|
|
- /// 不再 new ComBin/Camera、不二次 Open/Init 同 COM 口/相机。互斥由调试页持有的前台借用保证。
|
|
|
|
|
- /// 中止:IsStopCalibrate 标志,每 well 间检查,工程师可随时中止,已完成 well 结果保留。
|
|
|
|
|
- /// 异常:单 well 失败/存储失败不崩 UI,记日志继续;整体异常兜底,IsCalibrating 复位。
|
|
|
|
|
|
|
+ /// 一键全自动标定(D2-02-T3.4 重构为"经 control 协作")。
|
|
|
|
|
+ /// 旧实现:operate 本地 new CalibrationEngine 逐 well 跑 + CalibrationStore 写 JSON/scene0(已停用)。
|
|
|
|
|
+ /// 新实现:operate 仅发 /debug/calibrate/start(经 CalibrationClient,复用 acquire 的 sessionId) →
|
|
|
|
|
+ /// 起轮询 /debug/calibrate/progress(~800ms) 把每孔状态映射到 16 格 UI(合格绿/伪峰橙/失败红 + FocusZ/峰比/偏移)。
|
|
|
|
|
+ /// 标定由 control 协作端真正执行(持真硬件)、scene=0 基准由 control 端落库。
|
|
|
|
|
+ /// 前提:ComHouseInit 已成功 acquire(CurrentSessionId 非空、_calibClient 已构造)。
|
|
|
|
|
+ /// 中止:StopCalibrate → /debug/calibrate/stop + 停轮询。
|
|
|
///
|
|
///
|
|
|
- /// ⚠ 待验证 V-055..V-059:16well 跑通(关联 V-004 算法严谨性)/合格绿伪峰红/基准scene0落库+JSON/
|
|
|
|
|
- /// 借用期间采集暂停/中止响应。
|
|
|
|
|
|
|
+ /// ⚠ 待验证(compile-pending,operate.exe 卡死未编译):start/progress/stop 协作往返、16 格映射、合格判定一致。
|
|
|
/// </summary>
|
|
/// </summary>
|
|
|
/// <param name="wells">勾选的 well 列表(空/null 视为全 16 well)。</param>
|
|
/// <param name="wells">勾选的 well 列表(空/null 视为全 16 well)。</param>
|
|
|
public async Task OneClickCalibrate(IEnumerable<int> wells = null)
|
|
public async Task OneClickCalibrate(IEnumerable<int> wells = null)
|
|
|
{
|
|
{
|
|
|
- await Task.Run(() =>
|
|
|
|
|
|
|
+ if (IsCalibrating)
|
|
|
{
|
|
{
|
|
|
- if (IsCalibrating)
|
|
|
|
|
- {
|
|
|
|
|
- AddMessageInfo("[一键标定]已在标定中,请勿重复触发");
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
- if (Serial == null || Cam == null)
|
|
|
|
|
- {
|
|
|
|
|
- AddMessageInfo("[一键标定]请先初始化舱室(串口/相机未就绪)");
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
- if (CurrentHouse == null)
|
|
|
|
|
- {
|
|
|
|
|
- AddMessageInfo("[一键标定]未获取到舱室信息");
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ AddMessageInfo("[一键标定]已在标定中,请勿重复触发");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (_calibClient == null || string.IsNullOrEmpty(CurrentSessionId))
|
|
|
|
|
+ {
|
|
|
|
|
+ AddMessageInfo("[一键标定]未建立调试会话(请先初始化舱室借硬件),无法发起协作标定");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (CurrentHouse == null)
|
|
|
|
|
+ {
|
|
|
|
|
+ AddMessageInfo("[一键标定]未获取到舱室信息");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- IsCalibrating = true;
|
|
|
|
|
- IsStopCalibrate = false;
|
|
|
|
|
|
|
+ // 勾选 well:未传则默认 16 well;去重、按序、限定 1-16。
|
|
|
|
|
+ var wellList = (wells ?? Enumerable.Range(1, 16))
|
|
|
|
|
+ .Where(w => w >= 1 && w <= 16).Distinct().OrderBy(w => w).ToList();
|
|
|
|
|
+ if (wellList.Count == 0) wellList = Enumerable.Range(1, 16).ToList();
|
|
|
|
|
|
|
|
- // M8-P3b:一键标定为关键命令入口,建立操作日志 scope(统一 traceId 串联本次标定内的 HTTP/串口/相机埋点)。
|
|
|
|
|
- using var _opScope = Aivfo.OperationLog.OperationLogger.Begin("对焦调试", "一键标定",
|
|
|
|
|
- houseSn: CurrentHouseId);
|
|
|
|
|
- try { _opScope.Input(new { houseSn = CurrentHouseId }); } catch { }
|
|
|
|
|
|
|
+ IsCalibrating = true;
|
|
|
|
|
+ IsStopCalibrate = false;
|
|
|
|
|
|
|
|
- // 勾选 well:未传则默认 16 well;去重、按序、限定 1-16。
|
|
|
|
|
- var wellList = (wells != null ? wells : Enumerable.Range(1, 16))
|
|
|
|
|
- .Where(w => w >= 1 && w <= 16).Distinct().OrderBy(w => w).ToList();
|
|
|
|
|
- if (wellList.Count == 0) wellList = Enumerable.Range(1, 16).ToList();
|
|
|
|
|
|
|
+ try
|
|
|
|
|
+ {
|
|
|
|
|
+ // M8-P3b:一键标定发起为关键命令入口,记一条操作日志(start 同步发起;异步轮询单独不再串 scope,
|
|
|
|
|
+ // 避免 using scope 在方法返回即 Dispose 后被后台轮询误用)。
|
|
|
|
|
+ using (var _opScope = Aivfo.OperationLog.OperationLogger.Begin("对焦调试", "一键标定发起(协作)", houseSn: CurrentHouseId))
|
|
|
|
|
+ {
|
|
|
|
|
+ try { _opScope.Input(new { houseSn = CurrentHouseId, wells = wellList.ToArray() }); } catch { }
|
|
|
|
|
|
|
|
- // 合格判据阈值:tl_setting.focus_peak_ratio_threshold,缺省 1.2(03 §7.1)。
|
|
|
|
|
- double peakThreshold = (tLSetting != null && tLSetting.focusPeakRatioThreshold.HasValue)
|
|
|
|
|
- ? (double)tLSetting.focusPeakRatioThreshold.Value : 1.2;
|
|
|
|
|
|
|
+ ResetCalibResults(wellList);
|
|
|
|
|
|
|
|
- ResetCalibResults(wellList);
|
|
|
|
|
|
|
+ // D2-02-T3.4:标定由 control 协作端执行(scene=0 落库由 control 端处理)。
|
|
|
|
|
+ // operate 仅发 start + 轮询进度刷新 16 格 UI,不再 operate 本地 new CalibrationEngine/CalibrationStore。
|
|
|
|
|
+ var r = await _calibClient.StartAsync(wellList);
|
|
|
|
|
+ if (!r.Ok)
|
|
|
|
|
+ {
|
|
|
|
|
+ IsCalibrating = false;
|
|
|
|
|
+ AddMessageInfo($"[一键标定]发起失败:{r.Code}/{r.Error}");
|
|
|
|
|
+ try { _opScope.Fail($"start 失败:{r.Code}/{r.Error}"); } catch { }
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ try { _opScope.Output(new { started = true }).Success(); } catch { }
|
|
|
|
|
+ }
|
|
|
|
|
+ AddMessageInfo($"[一键标定]已发起(control 协作):舱{CurrentHouse.houseSn} 共 {wellList.Count} well,开始轮询进度...");
|
|
|
|
|
|
|
|
- // 标定结果存储:calibration.json 真相源(12 §2.7/03 §4,IncludeFields=true 由 CalibrationFile.Load 保证)。
|
|
|
|
|
- // operate 端无 SqlSugar 直连(走 HttpHelper),故 DbMirror=null:仅写 JSON 真相源;
|
|
|
|
|
- // house_autofocus_calibration 的 scene=0 基准 upsert 由 control 端 AppData.AutofocusStore 镜像(M2-04)。
|
|
|
|
|
- var store = new CalibrationStore
|
|
|
|
|
- {
|
|
|
|
|
- JsonPath = System.IO.Path.Combine(
|
|
|
|
|
- System.AppDomain.CurrentDomain.BaseDirectory, @"DependFile\AutoFocus\calibration.json"),
|
|
|
|
|
- Source = "OPERATE_DEBUG_SCENE0",
|
|
|
|
|
- Log = msg => AddMessageInfo($"[一键标定]{msg}"),
|
|
|
|
|
- // DbMirror 为空 → 只写 JSON 不镜像库(operate 无 DB 直连)。
|
|
|
|
|
- };
|
|
|
|
|
- try
|
|
|
|
|
|
|
+ // 起轮询任务,把 control 端进度映射到 16 格 UI;IsRunning 转 false 即停。
|
|
|
|
|
+ StartCalibPolling();
|
|
|
|
|
+ }
|
|
|
|
|
+ catch (Exception ex)
|
|
|
|
|
+ {
|
|
|
|
|
+ IsCalibrating = false;
|
|
|
|
|
+ ExLog(ex, "OneClickCalibrate");
|
|
|
|
|
+ AddMessageInfo($"[一键标定]整体异常:{ex.Message}");
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// D2-02-T3.4 单孔重标:经 control 协作端对该 well 重新标定一次,并起轮询刷新该格。
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ public async Task RecalibrateWell(int wellSn)
|
|
|
|
|
+ {
|
|
|
|
|
+ if (_calibClient == null || string.IsNullOrEmpty(CurrentSessionId))
|
|
|
|
|
+ {
|
|
|
|
|
+ AddMessageInfo("[重标]未建立调试会话,无法重标");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (IsCalibrating)
|
|
|
|
|
+ {
|
|
|
|
|
+ AddMessageInfo("[重标]批量标定进行中,请先中止或等待结束");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (wellSn < 1 || wellSn > 16)
|
|
|
|
|
+ {
|
|
|
|
|
+ AddMessageInfo("[重标]well 只能为 1-16");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ try
|
|
|
|
|
+ {
|
|
|
|
|
+ IsCalibrating = true;
|
|
|
|
|
+ IsStopCalibrate = false;
|
|
|
|
|
+ UpdateCalibResult(wellSn, item => { item.State = WellCalibState.Running; item.Note = "重标中..."; });
|
|
|
|
|
+ var r = await _calibClient.RecalibrateAsync(wellSn);
|
|
|
|
|
+ if (!r.Ok)
|
|
|
{
|
|
{
|
|
|
- var jsonDir = System.IO.Path.GetDirectoryName(store.JsonPath);
|
|
|
|
|
- if (!string.IsNullOrEmpty(jsonDir) && !System.IO.Directory.Exists(jsonDir))
|
|
|
|
|
- System.IO.Directory.CreateDirectory(jsonDir);
|
|
|
|
|
|
|
+ IsCalibrating = false;
|
|
|
|
|
+ AddMessageInfo($"[重标]well{wellSn} 发起失败:{r.Code}/{r.Error}");
|
|
|
|
|
+ return;
|
|
|
}
|
|
}
|
|
|
- catch (Exception exDir)
|
|
|
|
|
|
|
+ AddMessageInfo($"[重标]well{wellSn} 已发起,轮询进度...");
|
|
|
|
|
+ StartCalibPolling();
|
|
|
|
|
+ }
|
|
|
|
|
+ catch (Exception ex)
|
|
|
|
|
+ {
|
|
|
|
|
+ IsCalibrating = false;
|
|
|
|
|
+ ExLog(ex, "RecalibrateWell");
|
|
|
|
|
+ AddMessageInfo($"[重标]well{wellSn} 异常:{ex.Message}");
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>工程师中止一键标定:通知 control 停标,停本地轮询。</summary>
|
|
|
|
|
+ public void StopCalibrate()
|
|
|
|
|
+ {
|
|
|
|
|
+ IsStopCalibrate = true;
|
|
|
|
|
+ AddMessageInfo("[一键标定]收到中止请求,通知 control 停止...");
|
|
|
|
|
+ var calib = _calibClient;
|
|
|
|
|
+ if (calib != null)
|
|
|
|
|
+ {
|
|
|
|
|
+ Task.Run(async () =>
|
|
|
{
|
|
{
|
|
|
- AddMessageInfo($"[一键标定]创建 calibration.json 目录失败(将仅 UI 显示):{exDir.Message}");
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ try { await calib.StopAsync(); } catch (Exception ex) { ExLog(ex, "StopCalibrate.StopAsync"); }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ // 轮询任务读到 IsRunning=false 后会自停;此处也主动停一次兜底。
|
|
|
|
|
+ StopCalibPolling();
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- string tlSn = (tLSetting != null && !string.IsNullOrEmpty(tLSetting.tlSn))
|
|
|
|
|
- ? tLSetting.tlSn : (CurrentHouse.tlSn ?? AppData.Instance.TlSn);
|
|
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// 起标定进度轮询(~800ms 一次):调 control /debug/calibrate/progress,
|
|
|
|
|
+ /// 把每孔 WellCalibProgressDto(State 数字枚举)映射到 16 格 WellCalibUiItem。
|
|
|
|
|
+ /// IsRunning 转 false(或会话失效返回 null)即停轮询并复位 IsCalibrating。
|
|
|
|
|
+ /// 同一时刻只跑一个轮询任务(先停旧的)。
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ private void StartCalibPolling()
|
|
|
|
|
+ {
|
|
|
|
|
+ StopCalibPolling();
|
|
|
|
|
+ var cts = new System.Threading.CancellationTokenSource();
|
|
|
|
|
+ _calibPollCts = cts;
|
|
|
|
|
+ var token = cts.Token;
|
|
|
|
|
+ var calib = _calibClient;
|
|
|
|
|
|
|
|
|
|
+ Task.Run(async () =>
|
|
|
|
|
+ {
|
|
|
try
|
|
try
|
|
|
{
|
|
{
|
|
|
- // M1-B2:借用到的 lease.Serial/lease.Camera 本就是 HAL 接口(ISerialChannel/ICamera),
|
|
|
|
|
- // 直接喂给 M2-01 引擎,无需再经 DebugSerial/CameraAdapter 包 operate 具体类型(已退役)。
|
|
|
|
|
- var serial = Serial;
|
|
|
|
|
- var cam = Cam;
|
|
|
|
|
- var engine = new CalibrationEngine(serial, cam)
|
|
|
|
|
|
|
+ while (!token.IsCancellationRequested)
|
|
|
{
|
|
{
|
|
|
- Log = msg => AddMessageInfo(msg)
|
|
|
|
|
- };
|
|
|
|
|
-
|
|
|
|
|
- AddMessageInfo($"[一键标定]开始:舱{CurrentHouse.houseSn} 共 {wellList.Count} well,合格峰比阈值={peakThreshold:F2}");
|
|
|
|
|
|
|
+ var dto = await calib.PollProgressAsync();
|
|
|
|
|
+ if (token.IsCancellationRequested) break;
|
|
|
|
|
|
|
|
- foreach (var well in wellList)
|
|
|
|
|
- {
|
|
|
|
|
- if (IsStopCalibrate)
|
|
|
|
|
|
|
+ if (dto == null)
|
|
|
{
|
|
{
|
|
|
- AddMessageInfo("[一键标定]已被中止,停止后续 well");
|
|
|
|
|
|
|
+ // 会话失效/无进度:停轮询(会话失效另由 DebugSessionClient.OnSessionExpired 提示)。
|
|
|
|
|
+ AddMessageInfo("[一键标定]进度查询无结果(会话可能已失效),停止轮询");
|
|
|
break;
|
|
break;
|
|
|
}
|
|
}
|
|
|
- UpdateCalibResult(well, item => { item.State = WellCalibState.Running; item.Note = "标定中..."; });
|
|
|
|
|
- CurrentWell = well;
|
|
|
|
|
- try
|
|
|
|
|
- {
|
|
|
|
|
- // EEPROM 仅作扫描中心(参考),不进配置解析链、不回写(§2.5)。
|
|
|
|
|
- int hpos = serial.ReadWellHorizontalPosWait(well);
|
|
|
|
|
- int zZero = serial.ReadWellFocusZeroWait(well);
|
|
|
|
|
- var wc = engine.CalibrateWell(well, Math.Max(0, hpos), Math.Max(0, zZero));
|
|
|
|
|
|
|
|
|
|
- if (wc == null)
|
|
|
|
|
- {
|
|
|
|
|
- UpdateCalibResult(well, item =>
|
|
|
|
|
- {
|
|
|
|
|
- item.State = WellCalibState.Failed;
|
|
|
|
|
- item.Note = "无结果";
|
|
|
|
|
- });
|
|
|
|
|
- AddMessageInfo($"[一键标定]{well}号 well 无标定结果");
|
|
|
|
|
- continue;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ MapProgressToUi(dto);
|
|
|
|
|
|
|
|
- // 合格判据:circleFound && peakRatio>阈值 → 绿;否则(未检圆/伪峰/弱峰) → 红。
|
|
|
|
|
- bool qualified = wc.CircleFound && wc.PeakRatio > peakThreshold;
|
|
|
|
|
- UpdateCalibResult(well, item =>
|
|
|
|
|
- {
|
|
|
|
|
- item.State = qualified ? WellCalibState.Qualified : WellCalibState.FakePeak;
|
|
|
|
|
- item.FocusZ = wc.FocusZ;
|
|
|
|
|
- item.PeakRatio = wc.PeakRatio;
|
|
|
|
|
- item.CenterOffsetPct = wc.CenterOffsetPct;
|
|
|
|
|
- item.CircleFound = wc.CircleFound;
|
|
|
|
|
- item.Exposure = wc.Exposure;
|
|
|
|
|
- item.Note = qualified
|
|
|
|
|
- ? $"合格 Z={wc.FocusZ} 峰比={wc.PeakRatio:F2}"
|
|
|
|
|
- : $"伪峰 Z={wc.FocusZ} 峰比={wc.PeakRatio:F2} {(wc.CircleFound ? "" : "未检圆")}{wc.Note}";
|
|
|
|
|
- });
|
|
|
|
|
- AddMessageInfo($"[一键标定]{well}号 well {(qualified ? "✓合格(绿)" : "✗伪峰(红)")} " +
|
|
|
|
|
- $"FocusZ={wc.FocusZ} 峰比={wc.PeakRatio:F2} 偏移={wc.CenterOffsetPct:F1}%");
|
|
|
|
|
-
|
|
|
|
|
- // 出厂基准 scene=0 落库 + 写 calibration.json(存储失败不崩 UI,CalibrationStore 内部已吞异常)。
|
|
|
|
|
- try
|
|
|
|
|
- {
|
|
|
|
|
- store.SaveCalibration(wc, tlSn, CurrentHouse.houseSn, well, scene: 0,
|
|
|
|
|
- port: CurrentHouse.housePort, ccdSn: CurrentHouse.ccdSn);
|
|
|
|
|
- }
|
|
|
|
|
- catch (Exception exStore)
|
|
|
|
|
- {
|
|
|
|
|
- AddMessageInfo($"[一键标定]{well}号 well 结果存档失败:{exStore.Message}");
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- catch (Exception exWell)
|
|
|
|
|
|
|
+ if (!dto.IsRunning)
|
|
|
{
|
|
{
|
|
|
- // 单 well 异常不崩 UI:记日志/标红,继续下一 well。
|
|
|
|
|
- UpdateCalibResult(well, item =>
|
|
|
|
|
- {
|
|
|
|
|
- item.State = WellCalibState.Failed;
|
|
|
|
|
- item.Note = $"异常:{exWell.Message}";
|
|
|
|
|
- });
|
|
|
|
|
- ExLog(exWell, $"OneClickCalibrate.well{well}");
|
|
|
|
|
- AddMessageInfo($"[一键标定]{well}号 well 标定异常:{exWell.Message}");
|
|
|
|
|
|
|
+ int okCount = CalibResults.Count(x => x.State == WellCalibState.Qualified);
|
|
|
|
|
+ AddMessageInfo($"[一键标定]结束:合格 {okCount}/{dto.Total}(scene=0 基准由 control 协作端落库)");
|
|
|
|
|
+ break;
|
|
|
}
|
|
}
|
|
|
|
|
+ await Task.Delay(800, token);
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- int okCount = CalibResults.Count(x => x.State == WellCalibState.Qualified);
|
|
|
|
|
- AddMessageInfo($"[一键标定]结束:合格 {okCount}/{wellList.Count}(结果已存档为出厂基准 scene=0 + calibration.json)");
|
|
|
|
|
- // M8-P3b:记录标定结果到操作日志 scope。
|
|
|
|
|
- try { _opScope.Output(new { okCount, total = wellList.Count }).Success(); } catch { }
|
|
|
|
|
}
|
|
}
|
|
|
|
|
+ catch (TaskCanceledException) { /* 主动停 */ }
|
|
|
catch (Exception ex)
|
|
catch (Exception ex)
|
|
|
{
|
|
{
|
|
|
- ExLog(ex, "OneClickCalibrate");
|
|
|
|
|
- AddMessageInfo($"[一键标定]整体异常:{ex.Message}");
|
|
|
|
|
- // M8-P3b:标定整体异常标记失败。
|
|
|
|
|
- try { _opScope.Fail(ex.GetType().Name + ": " + ex.Message); } catch { }
|
|
|
|
|
|
|
+ ExLog(ex, "CalibPolling");
|
|
|
|
|
+ AddMessageInfo($"[一键标定]轮询异常:{ex.Message}");
|
|
|
}
|
|
}
|
|
|
finally
|
|
finally
|
|
|
{
|
|
{
|
|
|
IsCalibrating = false;
|
|
IsCalibrating = false;
|
|
|
}
|
|
}
|
|
|
- });
|
|
|
|
|
|
|
+ }, token);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /// <summary>工程师中止一键标定。</summary>
|
|
|
|
|
- public void StopCalibrate()
|
|
|
|
|
|
|
+ /// <summary>停标定进度轮询(取消轮询任务)。幂等。</summary>
|
|
|
|
|
+ private void StopCalibPolling()
|
|
|
{
|
|
{
|
|
|
- IsStopCalibrate = true;
|
|
|
|
|
- AddMessageInfo("[一键标定]收到中止请求,将在当前 well 结束后停止");
|
|
|
|
|
|
|
+ var cts = _calibPollCts;
|
|
|
|
|
+ _calibPollCts = null;
|
|
|
|
|
+ if (cts != null) { try { cts.Cancel(); } catch { } try { cts.Dispose(); } catch { } }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /// <summary>把 control 进度 DTO 的每孔状态映射到 16 格 UI(State 为数字枚举:0待标/1标定中/2合格/3伪峰/4失败)。</summary>
|
|
|
|
|
+ private void MapProgressToUi(CalibProgressDto dto)
|
|
|
|
|
+ {
|
|
|
|
|
+ if (dto?.Wells == null) return;
|
|
|
|
|
+ foreach (var w in dto.Wells)
|
|
|
|
|
+ {
|
|
|
|
|
+ var well = w; // 闭包捕获
|
|
|
|
|
+ UpdateCalibResult(well.WellSn, item =>
|
|
|
|
|
+ {
|
|
|
|
|
+ item.State = (WellCalibState)well.State; // 0..4 与 operate 端枚举顺序一致
|
|
|
|
|
+ item.FocusZ = well.FocusZ;
|
|
|
|
|
+ item.Exposure = well.Exposure;
|
|
|
|
|
+ item.PeakRatio = well.PeakRatio;
|
|
|
|
|
+ item.CenterOffsetPct = well.CenterOffsetPct;
|
|
|
|
|
+ item.CircleFound = well.CircleFound;
|
|
|
|
|
+ item.Note = string.IsNullOrEmpty(well.Note)
|
|
|
|
|
+ ? StateNote((WellCalibState)well.State, well.FocusZ, well.PeakRatio)
|
|
|
|
|
+ : well.Note;
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static string StateNote(WellCalibState st, int focusZ, double peakRatio) => st switch
|
|
|
|
|
+ {
|
|
|
|
|
+ WellCalibState.Pending => "待标定",
|
|
|
|
|
+ WellCalibState.Running => "标定中...",
|
|
|
|
|
+ WellCalibState.Qualified => $"合格 Z={focusZ} 峰比={peakRatio:F2}",
|
|
|
|
|
+ WellCalibState.FakePeak => $"伪峰 Z={focusZ} 峰比={peakRatio:F2}",
|
|
|
|
|
+ WellCalibState.Failed => "失败",
|
|
|
|
|
+ _ => ""
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
private void ResetCalibResults(List<int> wells)
|
|
private void ResetCalibResults(List<int> wells)
|
|
|
{
|
|
{
|
|
|
void build()
|
|
void build()
|
|
@@ -1591,5 +1732,108 @@ namespace ivf_tl_Operate.ViewModel
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
#endregion
|
|
#endregion
|
|
|
|
|
+
|
|
|
|
|
+ #region D2-02-T3.5 保存 per-well 对焦搜索范围(半幅)
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// 当前编辑的 well 级"水平搜索半幅"(围绕 horizontalMotorPosition)。空字符串=留空继承设备级(保存写 null)。
|
|
|
|
|
+ /// View TextBox 双向绑定;校验在保存时做(非空须为 ≥0 整数)。
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ [ObservableProperty] private string currentHFocusRange = "";
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// 当前编辑的 well 级"垂直搜索半幅"(围绕 eepromClearPosition/清晰位)。空=继承设备级(写 null)。校验:≥0。
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ [ObservableProperty] private string currentVFocusRange = "";
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// 取该 well 当前 well 级半幅覆盖值回填两个输入框(空=继承设备级,显示空占位)。供"载入半幅"按钮调用。
|
|
|
|
|
+ /// 复用 ManualTuneWell 作为锚点 well(与手调拍摄层同一选定 well)。
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ public void LoadWellFocusRange(int well)
|
|
|
|
|
+ {
|
|
|
|
|
+ ManualTuneWell = well;
|
|
|
|
|
+ var ws = houseWellSettingList.FirstOrDefault(x => x.houseSn == CurrentHouseId && x.wellSn == well);
|
|
|
|
|
+ CurrentHFocusRange = ws?.horizontalFocusRange?.ToString() ?? "";
|
|
|
|
|
+ CurrentVFocusRange = ws?.verticalFocusRange?.ToString() ?? "";
|
|
|
|
|
+ AddMessageInfo($"[对焦半幅]已载入 well{well} 当前覆盖:水平={(ws?.horizontalFocusRange?.ToString() ?? "继承")} 垂直={(ws?.verticalFocusRange?.ToString() ?? "继承")}");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// D2-02-T3.5 保存该 well 对焦搜索半幅:校验通过后写 house_well_setting well 级覆盖
|
|
|
|
|
+ /// (horizontal_focus_range/vertical_focus_range)。留空字段=继承设备级(写 null)。
|
|
|
|
|
+ /// 复用现有 well 设置保存通道 HttpHelper.WellUpdateApi(同 SetWellMotorPosition/SaveManualLayerTune),
|
|
|
|
|
+ /// 走 /well/update;后端 well/update 已支持这两列(Java HouseWellSettingUpdate.horizontalFocusRange/verticalFocusRange)。
|
|
|
|
|
+ /// 字段名与 Java DTO 完全一致:horizontalFocusRange / verticalFocusRange。
|
|
|
|
|
+ /// 返回 true=保存成功。
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ public bool SaveWellFocusRange()
|
|
|
|
|
+ {
|
|
|
|
|
+ try
|
|
|
|
|
+ {
|
|
|
|
|
+ if (CurrentHouse == null)
|
|
|
|
|
+ {
|
|
|
|
|
+ AddMessageInfo("[对焦半幅]未获取到舱室信息");
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (ManualTuneWell < 1 || ManualTuneWell > 16)
|
|
|
|
|
+ {
|
|
|
|
|
+ AddMessageInfo("[对焦半幅]well 只能为 1-16");
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 校验:非空须为 ≥0 整数;留空=继承设备级(写 null)。
|
|
|
|
|
+ int? hRange;
|
|
|
|
|
+ if (string.IsNullOrWhiteSpace(CurrentHFocusRange)) hRange = null;
|
|
|
|
|
+ else if (int.TryParse(CurrentHFocusRange.Trim(), out int hv) && hv >= 0) hRange = hv;
|
|
|
|
|
+ else { AddMessageInfo("[对焦半幅]水平半幅只能输入 ≥0 整数(留空=继承设备级)"); return false; }
|
|
|
|
|
+
|
|
|
|
|
+ int? vRange;
|
|
|
|
|
+ if (string.IsNullOrWhiteSpace(CurrentVFocusRange)) vRange = null;
|
|
|
|
|
+ else if (int.TryParse(CurrentVFocusRange.Trim(), out int vv) && vv >= 0) vRange = vv;
|
|
|
|
|
+ else { AddMessageInfo("[对焦半幅]垂直半幅只能输入 ≥0 整数(留空=继承设备级)"); return false; }
|
|
|
|
|
+
|
|
|
|
|
+ // M8 埋点:保存对焦半幅为关键业务操作(统一 traceId 串其内 WellUpdateApi HTTP 埋点)。
|
|
|
|
|
+ using var _op = Aivfo.OperationLog.OperationLogger.Begin("对焦调试", "保存对焦搜索半幅",
|
|
|
|
|
+ houseSn: CurrentHouseId);
|
|
|
|
|
+ try { _op.Input(new { houseSn = CurrentHouseId, well = ManualTuneWell, horizontalFocusRange = hRange, verticalFocusRange = vRange }); } catch { }
|
|
|
|
|
+
|
|
|
|
|
+ // body 字段名与 Java HouseWellSettingUpdate 完全一致;留空=null 清空继承设备级(与 SaveManualLayerTune 一致)。
|
|
|
|
|
+ string body = JsonConvert.SerializeObject(new
|
|
|
|
|
+ {
|
|
|
|
|
+ tlSn = CurrentHouse.tlSn,
|
|
|
|
|
+ houseSn = CurrentHouse.houseSn,
|
|
|
|
|
+ wellSn = ManualTuneWell,
|
|
|
|
|
+ horizontalFocusRange = hRange, // null=继承设备级 focus_h_range_default
|
|
|
|
|
+ verticalFocusRange = vRange, // null=继承设备级 focus_v_range_default
|
|
|
|
|
+ });
|
|
|
|
|
+ bool ok = AppData.Instance.HttpHelper.WellUpdateApi(body);
|
|
|
|
|
+ if (!ok)
|
|
|
|
|
+ {
|
|
|
|
|
+ AddMessageInfo("[对焦半幅]保存失败(接口返回失败)");
|
|
|
|
|
+ try { _op.Fail("WellUpdateApi 返回失败"); } catch { }
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 本地内存同步,使取数一致(不重载设置即生效)。
|
|
|
|
|
+ var ws = houseWellSettingList.FirstOrDefault(x => x.houseSn == CurrentHouseId && x.wellSn == ManualTuneWell);
|
|
|
|
|
+ if (ws != null)
|
|
|
|
|
+ {
|
|
|
|
|
+ ws.horizontalFocusRange = hRange;
|
|
|
|
|
+ ws.verticalFocusRange = vRange;
|
|
|
|
|
+ }
|
|
|
|
|
+ AddMessageInfo($"[对焦半幅]well{ManualTuneWell} 已保存:水平={(hRange?.ToString() ?? "继承")} 垂直={(vRange?.ToString() ?? "继承")}(写 house_well_setting well 级覆盖,留空继承设备级)");
|
|
|
|
|
+ try { _op.Output(new { ok = true }).Success(); } catch { }
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
+ catch (Exception ex)
|
|
|
|
|
+ {
|
|
|
|
|
+ ExLog(ex, "SaveWellFocusRange");
|
|
|
|
|
+ AddMessageInfo($"[对焦半幅]保存异常:{ex.Message}");
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ #endregion
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|