using System; using System.Collections.Generic; using System.IO; using IvfTl.AutoFocus.Calib; namespace IvfTl.AutoFocus.Storage { /// /// 标定结果存储入口(M2-04)。 /// 依据:项目文档/开发计划/2026-06-17-M2-本地自动对焦子计划.md (M2-04)、需求文档/12 §2.7、03 §4。 /// /// 职责: /// 1) 真相源——把标定结果写本地 calibration.json(复用 CalibrationFile.Save; /// 读回经 CalibrationFile.Load 已带 IncludeFields=true,public 字段不会静默丢失,03 §4)。 /// 2) 库内镜像——把单条 WellCalib 解包后通过注入的回调写入 house_autofocus_calibration /// (scene 0 基准 upsert / 1 日常 append)。本程序集不引用 SqlSugar/Entity,故库写以 /// 委托回调形式由 control 端(AppData→DBService.SaveAutofocusCalibration)实现,避免反向依赖。 /// /// scene:0=出厂基准(调试页一键标定,M2-05) 1=日常对焦(放皿/门/定时触发,M2-03 走此路径)。 /// 任一步骤异常都被本类吞掉(经 Log 回调记日志),绝不向上抛,保证不崩对焦/采集线程。 /// public sealed class CalibrationStore { /// /// 库镜像回调:参数依次为 /// (tlSn, houseSn, wellSn, scene, focusZ, exposure, horizontalPulse, peakRatio, /// circleFound, centerOffsetPct, note, source)。 /// 由 control 端绑定到 DBService.SaveAutofocusCalibration;为空则只写 JSON、不镜像库。 /// public Action DbMirror { get; set; } /// /// M2-06 安全门降级用:读 scene=0 出厂基准的最清晰层 Z 脉冲(FocusZ)回调。 /// 参数:(tlSn, houseSn, wellSn);返回该 well 的基准 FocusZ,无基准返回 null。 /// 由 control 端绑定到 DBService.GetBaselineFocusZ(查 house_autofocus_calibration scene=0); /// 为空或返回 null 时,安全门关闭路径无基准可用 → 本轮跳过该 well(不污染拍摄,记日志)。 /// 本程序集不引用 SqlSugar/Entity,故以委托回调形式由 control 端实现,避免反向依赖(同 DbMirror)。 /// public Func BaselineReader { get; set; } /// 日志回调(可空)。 public Action Log { get; set; } /// calibration.json 路径(真相源)。由部署配置注入,不写死测试外壳常量路径。 public string JsonPath { get; set; } /// 结果来源标记,写入库镜像 source 列。 public string Source { get; set; } = "LOCAL_JSON"; /// /// 保存单个 well 的标定结果:先写 JSON 真相源(按 tlSn/houseSn/wellSn 合并到既有档案), /// 再镜像库(scene 0 upsert / 1 append)。 /// public void SaveCalibration(WellCalib wc, string tlSn, int houseSn, int wellSn, int scene, string port = null, int ccdIndex = 0, string ccdSn = null) { if (wc == null) return; // ① 写 JSON 真相源(容错:JSON 失败不影响库镜像,反之亦然)。 try { WriteJson(wc, tlSn, houseSn, wellSn, port, ccdIndex, ccdSn); } catch (Exception ex) { Log?.Invoke($"[标定存储]写 calibration.json 失败:{ex.Message}"); } // ② 镜像库(通过注入回调,由 control 端落 SqlSugar)。 try { DbMirror?.Invoke(tlSn, houseSn, wellSn, scene, wc.FocusZ, wc.Exposure, wc.HorizontalPulse, (decimal)wc.PeakRatio, wc.CircleFound, (decimal)wc.CenterOffsetPct, wc.Note ?? "", Source); } catch (Exception ex) { Log?.Invoke($"[标定存储]镜像 house_autofocus_calibration 失败:{ex.Message}"); } } /// /// 把 WellCalib 合并写入 calibration.json:读出既有档案(无则新建),按 house/well 覆盖该 well 条目后回写。 /// private void WriteJson(WellCalib wc, string tlSn, int houseSn, int wellSn, string port, int ccdIndex, string ccdSn) { if (string.IsNullOrEmpty(JsonPath)) return; CalibrationFile file = null; if (File.Exists(JsonPath)) { try { file = CalibrationFile.Load(JsonPath); } catch (Exception ex) { Log?.Invoke($"[标定存储]读旧 calibration.json 失败,将重建:{ex.Message}"); } } if (file == null) file = new CalibrationFile(); if (string.IsNullOrEmpty(file.TlSn)) file.TlSn = tlSn ?? ""; file.Date = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"); var house = file.Houses.Find(h => h.House == houseSn); if (house == null) { house = new HouseCalib { House = houseSn, Port = port ?? "", CcdIndex = ccdIndex, CcdSn = ccdSn ?? "" }; file.Houses.Add(house); } else { if (port != null) house.Port = port; if (ccdSn != null) house.CcdSn = ccdSn; if (ccdIndex != 0) house.CcdIndex = ccdIndex; } // wc.Well 可能未被调用方填好,这里以入参 wellSn 为准对齐 well 序号。 wc.Well = wellSn; int idx = house.Wells.FindIndex(w => w.Well == wellSn); if (idx >= 0) house.Wells[idx] = wc; else house.Wells.Add(wc); file.Save(JsonPath); } } }