CalibrationStore.cs 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128
  1. using System;
  2. using System.Collections.Generic;
  3. using System.IO;
  4. using IvfTl.AutoFocus.Calib;
  5. namespace IvfTl.AutoFocus.Storage
  6. {
  7. /// <summary>
  8. /// 标定结果存储入口(M2-04)。
  9. /// 依据:项目文档/开发计划/2026-06-17-M2-本地自动对焦子计划.md (M2-04)、需求文档/12 §2.7、03 §4。
  10. ///
  11. /// 职责:
  12. /// 1) 真相源——把标定结果写本地 calibration.json(复用 CalibrationFile.Save;
  13. /// 读回经 CalibrationFile.Load 已带 IncludeFields=true,public 字段不会静默丢失,03 §4)。
  14. /// 2) 库内镜像——把单条 WellCalib 解包后通过注入的回调写入 house_autofocus_calibration
  15. /// (scene 0 基准 upsert / 1 日常 append)。本程序集不引用 SqlSugar/Entity,故库写以
  16. /// 委托回调形式由 control 端(AppData→DBService.SaveAutofocusCalibration)实现,避免反向依赖。
  17. ///
  18. /// scene:0=出厂基准(调试页一键标定,M2-05) 1=日常对焦(放皿/门/定时触发,M2-03 走此路径)。
  19. /// 任一步骤异常都被本类吞掉(经 Log 回调记日志),绝不向上抛,保证不崩对焦/采集线程。
  20. /// </summary>
  21. public sealed class CalibrationStore
  22. {
  23. /// <summary>
  24. /// 库镜像回调:参数依次为
  25. /// (tlSn, houseSn, wellSn, scene, focusZ, exposure, horizontalPulse, peakRatio,
  26. /// circleFound, centerOffsetPct, note, source)。
  27. /// 由 control 端绑定到 DBService.SaveAutofocusCalibration;为空则只写 JSON、不镜像库。
  28. /// </summary>
  29. public Action<string, int, int, int, int, int, int, decimal, bool, decimal, string, string> DbMirror { get; set; }
  30. /// <summary>
  31. /// M2-06 安全门降级用:读 scene=0 出厂基准的最清晰层 Z 脉冲(FocusZ)回调。
  32. /// 参数:(tlSn, houseSn, wellSn);返回该 well 的基准 FocusZ,无基准返回 null。
  33. /// 由 control 端绑定到 DBService.GetBaselineFocusZ(查 house_autofocus_calibration scene=0);
  34. /// 为空或返回 null 时,安全门关闭路径无基准可用 → 本轮跳过该 well(不污染拍摄,记日志)。
  35. /// 本程序集不引用 SqlSugar/Entity,故以委托回调形式由 control 端实现,避免反向依赖(同 DbMirror)。
  36. /// </summary>
  37. public Func<string, int, int, int?> BaselineReader { get; set; }
  38. /// <summary>
  39. /// 拍照按孔设曝光用:读该 well 标定曝光(×100µs)。
  40. /// 参数:(tlSn, houseSn, wellSn);返回该 well 曝光,无则 null(调用方兜底 House.ccdExposure)。
  41. /// 由 control 端绑定到 DBService.GetCalibratedExposure(scene=1 日常优先、回退 scene=0 基准)。
  42. /// 本程序集不引用 SqlSugar/Entity,故以委托回调形式由 control 端实现,避免反向依赖(同 DbMirror/BaselineReader)。
  43. /// </summary>
  44. public Func<string, int, int, int?> ExposureReader { get; set; }
  45. /// <summary>日志回调(可空)。</summary>
  46. public Action<string> Log { get; set; }
  47. /// <summary>calibration.json 路径(真相源)。由部署配置注入,不写死测试外壳常量路径。</summary>
  48. public string JsonPath { get; set; }
  49. /// <summary>结果来源标记,写入库镜像 source 列。</summary>
  50. public string Source { get; set; } = "LOCAL_JSON";
  51. /// <summary>
  52. /// 保存单个 well 的标定结果:先写 JSON 真相源(按 tlSn/houseSn/wellSn 合并到既有档案),
  53. /// 再镜像库(scene 0 upsert / 1 append)。
  54. /// </summary>
  55. public void SaveCalibration(WellCalib wc, string tlSn, int houseSn, int wellSn, int scene,
  56. string port = null, int ccdIndex = 0, string ccdSn = null)
  57. {
  58. if (wc == null) return;
  59. // ① 写 JSON 真相源(容错:JSON 失败不影响库镜像,反之亦然)。
  60. try
  61. {
  62. WriteJson(wc, tlSn, houseSn, wellSn, port, ccdIndex, ccdSn);
  63. }
  64. catch (Exception ex)
  65. {
  66. Log?.Invoke($"[标定存储]写 calibration.json 失败:{ex.Message}");
  67. }
  68. // ② 镜像库(通过注入回调,由 control 端落 SqlSugar)。
  69. try
  70. {
  71. DbMirror?.Invoke(tlSn, houseSn, wellSn, scene, wc.FocusZ, wc.Exposure, wc.HorizontalPulse,
  72. (decimal)wc.PeakRatio, wc.CircleFound, (decimal)wc.CenterOffsetPct, wc.Note ?? "", Source);
  73. }
  74. catch (Exception ex)
  75. {
  76. Log?.Invoke($"[标定存储]镜像 house_autofocus_calibration 失败:{ex.Message}");
  77. }
  78. }
  79. /// <summary>
  80. /// 把 WellCalib 合并写入 calibration.json:读出既有档案(无则新建),按 house/well 覆盖该 well 条目后回写。
  81. /// </summary>
  82. private void WriteJson(WellCalib wc, string tlSn, int houseSn, int wellSn, string port, int ccdIndex, string ccdSn)
  83. {
  84. if (string.IsNullOrEmpty(JsonPath)) return;
  85. CalibrationFile file = null;
  86. if (File.Exists(JsonPath))
  87. {
  88. try { file = CalibrationFile.Load(JsonPath); }
  89. catch (Exception ex) { Log?.Invoke($"[标定存储]读旧 calibration.json 失败,将重建:{ex.Message}"); }
  90. }
  91. if (file == null) file = new CalibrationFile();
  92. if (string.IsNullOrEmpty(file.TlSn)) file.TlSn = tlSn ?? "";
  93. file.Date = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
  94. var house = file.Houses.Find(h => h.House == houseSn);
  95. if (house == null)
  96. {
  97. house = new HouseCalib { House = houseSn, Port = port ?? "", CcdIndex = ccdIndex, CcdSn = ccdSn ?? "" };
  98. file.Houses.Add(house);
  99. }
  100. else
  101. {
  102. if (port != null) house.Port = port;
  103. if (ccdSn != null) house.CcdSn = ccdSn;
  104. if (ccdIndex != 0) house.CcdIndex = ccdIndex;
  105. }
  106. // wc.Well 可能未被调用方填好,这里以入参 wellSn 为准对齐 well 序号。
  107. wc.Well = wellSn;
  108. int idx = house.Wells.FindIndex(w => w.Well == wellSn);
  109. if (idx >= 0) house.Wells[idx] = wc;
  110. else house.Wells.Add(wc);
  111. file.Save(JsonPath);
  112. }
  113. }
  114. }