HouseDebugPageViewModel.cs 64 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488
  1. using CommunityToolkit.Mvvm.ComponentModel;
  2. using ivf_tl_Entity.CameraEntitys;
  3. using ivf_tl_Entity.ComEntitys;
  4. using ivf_tl_Entity.DebugEntitys;
  5. using ivf_tl_Entity.DTO;
  6. using ivf_tl_Entity.GlobalEnums;
  7. using ivf_tl_Operate.Converts;
  8. using ivf_tl_Services;
  9. using IvfTl.Hardware;
  10. using IvfTl.Hardware.Impl;
  11. using IvfTl.AutoFocus.Calib;
  12. using IvfTl.AutoFocus.Layout;
  13. using IvfTl.AutoFocus.Storage;
  14. using Newtonsoft.Json;
  15. using Newtonsoft.Json.Linq;
  16. using System;
  17. using System.Collections.Generic;
  18. using System.Collections.ObjectModel;
  19. using System.Configuration;
  20. using System.DirectoryServices.ActiveDirectory;
  21. using System.IO;
  22. using System.Linq;
  23. using System.Reflection.Metadata;
  24. using System.Text;
  25. using System.Threading.Tasks;
  26. using System.Windows;
  27. using static ivf_tl_Operate.CustomUserControls.RecordBox;
  28. using System.Windows.Documents;
  29. namespace ivf_tl_Operate.ViewModel
  30. {
  31. public partial class HouseDebugPageViewModel : ObservableObject
  32. {
  33. public event Action<string> MessageEvent;
  34. public event Action<string, int, int> AddPicEvent;
  35. public event Action OpenVideoEvent;
  36. private ComBin comBin = null;
  37. public Camera camera = null;
  38. /// <summary>
  39. /// M1-03 HAL 前台借用凭证。打开调试页时向 HAL 申请该舱独占借用(HardwareUser.OperateDebug),
  40. /// HAL 自动暂停 control 对该舱的后台采集;关闭调试页 Dispose 归还,HAL 恢复采集。
  41. /// ⚠ 待验证 V-024:借用→暂停采集→归还恢复时序(调试取图 vs 后台采集切换不占用/不死锁,01 §3 M1 必过)。
  42. /// 说明(代码隔离 + 范围控制):调试页 ComBin/Camera 仍用 operate 侧 ivf_tl_Entity 具体类型——
  43. /// 因调试栈方法更全(Write*EEPROM / 电机正反向 / RawToRgb 等),control 侧 SerialHelper ComBin 未覆盖,
  44. /// 折叠进 HAL ISerialChannel 属 M2 统一。M1 通过 HAL 借用闸门保证"同舱同一时刻单一使用者",
  45. /// 杜绝与后台采集同时 Open 同 COM 口/Init 同相机。
  46. /// </summary>
  47. private IHardwareLease _halLease = null;
  48. /// <summary>
  49. /// 当前housesn
  50. /// </summary>
  51. public int CurrentHouseId { get; set; } = 1;
  52. /// <summary>
  53. /// 当前ccdID
  54. /// </summary>
  55. public int CurrentCCDId { get; set; } = 0;
  56. /// <summary>
  57. /// 所有舱室
  58. /// </summary>
  59. public List<HouseInfo> HouseList { get; set; } = new List<HouseInfo>();
  60. /// <summary>
  61. /// 仪器设置
  62. /// </summary>
  63. public TLSetting tLSetting { get; set; } = new TLSetting();
  64. /// <summary>
  65. /// 自动对焦和拍照位置
  66. /// </summary>
  67. public List<HousePhotographLocation> ccdPhoto = new List<HousePhotographLocation>();
  68. /// <summary>
  69. /// 水平电机位置
  70. /// </summary>
  71. public List<HouseWellSetting> houseWellSettingList = new List<HouseWellSetting>();
  72. [ObservableProperty]
  73. private ObservableCollection<string> messageInfoList = new ObservableCollection<string>();
  74. [ObservableProperty]
  75. private decimal temperature = 0m;
  76. [ObservableProperty]
  77. private decimal pressure = 0m;
  78. [ObservableProperty]
  79. private string doorState = null;
  80. [ObservableProperty]
  81. private string ledState = null;
  82. [ObservableProperty]
  83. private int currentWell = 0;
  84. [ObservableProperty]
  85. private int currentFocal = 0;
  86. /// <summary>
  87. /// 结束抓图
  88. /// </summary>
  89. public bool IsStop { get; set; } = false;
  90. /// <summary>
  91. /// M2-05 一键标定中止标志(工程师可中止;每 well 间检查)。
  92. /// </summary>
  93. public bool IsStopCalibrate { get; set; } = false;
  94. /// <summary>
  95. /// M2-05 一键标定是否进行中(UI 据此禁用重复触发)。
  96. /// </summary>
  97. [ObservableProperty]
  98. private bool isCalibrating = false;
  99. /// <summary>
  100. /// M2-05 场景A 一键标定 16 well 实时结果(合格绿/伪峰红,含 FocusZ/峰比/偏移)。
  101. /// View 绑定此集合做 4x4 实时呈现(沿用调试页内呈现,不另开 CalibWindow)。
  102. /// </summary>
  103. public ObservableCollection<WellCalibUiItem> CalibResults { get; } = new ObservableCollection<WellCalibUiItem>();
  104. /// <summary>
  105. /// 垂直电机当前位置
  106. /// </summary>
  107. [ObservableProperty]
  108. private int currentVer = 0;
  109. /// <summary>
  110. /// 水平电机当前位置
  111. /// </summary>
  112. [ObservableProperty]
  113. private int currentHor = 0;
  114. [ObservableProperty]
  115. private HouseInfo currentHouse = null;
  116. [ObservableProperty]
  117. private int houseVentTimeE = 0;
  118. public HouseDebugPageViewModel()
  119. {
  120. //HouseList = AppData.Instance.HttpHelper.GetSettingHouseApi(AppData.Instance.TlSn);
  121. //CurrentHouse = HouseList.FirstOrDefault(x => x.houseSn == 1);
  122. }
  123. private void ExLog(Exception ex, string name)
  124. {
  125. AppData.Instance.LogHelper.ExceptionLog(ex, $"HouseDebugPageViewModel.{name}", LogEnum.RunException);
  126. }
  127. private void ErrorLog(string message, LogEnum logType)
  128. {
  129. AppData.Instance.LogHelper.TLLog($"HouseDebugPageViewModel.{message}", logType);
  130. }
  131. public void Start(ref string errora)
  132. {
  133. try
  134. {
  135. SerialBin serialBin = new SerialBin();
  136. serialBin.TLLogEvent += AppData.Instance.LogHelper.TLLog;
  137. serialBin.ExceptionLogEvent += AppData.Instance.LogHelper.ExceptionLog;
  138. serialBin.HouseLogEvent += AppData.Instance.LogHelper.HouseLog;
  139. var errorList = serialBin.UpdataCamera();
  140. AppData.Instance.LogHelper.TLLog($"ccdidsn:{JsonConvert.SerializeObject(serialBin.CCDidSn)}", LogEnum.RunRecord);
  141. if (errorList.Any())
  142. {
  143. errora = $"获取相机Id和CCDSN错误{JsonConvert.SerializeObject(errorList)}";
  144. AppData.Instance.LogHelper.TLLog(errora, LogEnum.RunRecord);
  145. return;
  146. }
  147. errorList = serialBin.Start(ConfigurationManager.AppSettings["autoFocus"].ToString());
  148. AppData.Instance.LogHelper.TLLog($"舱室信息:{JsonConvert.SerializeObject(serialBin.SerialModels)}", LogEnum.RunRecord);
  149. AppData.Instance.LogHelper.TLLog($"E方数据:{JsonConvert.SerializeObject(serialBin.HouseEEPROInfos)}", LogEnum.RunRecord);
  150. if (errorList.Any())
  151. {
  152. errora = $"获取串口信息错误{JsonConvert.SerializeObject(errorList)}";
  153. AppData.Instance.LogHelper.TLLog(errora, LogEnum.RunRecord);
  154. return;
  155. }
  156. var modelCount = serialBin.SerialModels.Count;
  157. if (modelCount != 11)
  158. {
  159. //string messageinfo = $"检测到{modelCount}个模块,是否继续运行?";
  160. string messageinfo = $" Detected {modelCount} cabin, are you sure to run?";
  161. MessageBoxResult aaa = MessageBox.Show(messageinfo, KeyToStringConvert.GetLanguageStringByKey("C0171"), MessageBoxButton.YesNo, MessageBoxImage.Question, MessageBoxResult.No, MessageBoxOptions.DefaultDesktopOnly);
  162. if (aaa != MessageBoxResult.Yes)
  163. {
  164. errora = "结束";
  165. return;
  166. }
  167. }
  168. List<int> listIntRunHoues = serialBin.SerialModels.Select(x => x.houseSn).ToList();
  169. TLInitData tLInitData = new TLInitData();
  170. tLInitData.tlSn = $"NEO-1-{serialBin.TLNum}";
  171. tLInitData.softwareVersion = "V2.0.0";
  172. tLInitData.verticalMotorPulseMax = 125000;
  173. tLInitData.houseLinkDataList = serialBin.SerialModels.OrderBy(x => x.houseSn).ToList();
  174. foreach (var item in serialBin.HouseEEPROInfos)
  175. {
  176. item.tlSn = tLInitData.tlSn;
  177. }
  178. tLInitData.houseEEPROInitDTOList = serialBin.HouseEEPROInfos.OrderBy(x => x.houseSn).ToList();
  179. AppData.Instance.LogHelper.TLLog($"舱室信息:{JsonConvert.SerializeObject(tLInitData)}", LogEnum.RunRecord);
  180. SettingDataApiData settingDataApiData = AppData.Instance.HttpHelper.GetSettingDataApi(tLInitData);
  181. if (settingDataApiData == null)
  182. {
  183. errora = $"{tLInitData.tlSn}获取仪器设置失败";
  184. return;
  185. }
  186. AppData.Instance.LogHelper.TLLog(JsonConvert.SerializeObject(settingDataApiData), LogEnum.RunRecord);
  187. tLSetting = AppData.Instance.ConvertHelper.ConvertToTLSetting(settingDataApiData.tlInfo, settingDataApiData.tlSetting);
  188. ccdPhoto = settingDataApiData.housePhotographLocation;
  189. houseWellSettingList = settingDataApiData.houseWellSettingList;
  190. HouseList = settingDataApiData.houseList;
  191. CurrentHouse = HouseList.FirstOrDefault(x => x.houseSn == 1);
  192. }
  193. catch (Exception ex)
  194. {
  195. AppData.Instance.LogHelper.ExceptionLog(ex, $"调试模式初始化", LogEnum.RunException);
  196. errora = $"调试模式初始化异常:{ex.Message}";
  197. }
  198. }
  199. public async Task ComHouseInit()
  200. {
  201. await Task.Run(async () =>
  202. {
  203. try
  204. {
  205. var currentHouse = HouseList.FirstOrDefault(x => x.houseSn == CurrentHouseId);
  206. // M1-03 HAL: 先向 HAL 申请该舱前台借用——HAL 暂停 control 对本舱采集并释放其串口/相机占用,
  207. // 避免与后台采集同时打开同一 COM 口/同一相机句柄(13 §④ operate 调试接入)。
  208. try
  209. {
  210. var gate = HardwareAccessLayer.Instance.GetHouseGate(currentHouse.houseSn);
  211. _halLease = gate.Acquire(HardwareUser.OperateDebug);
  212. if (_halLease == null)
  213. {
  214. AddMessageInfo($"[{currentHouse.houseSn}]该舱正被占用(采集/对焦),借用超时,稍后重试");
  215. return;
  216. }
  217. }
  218. catch (Exception le)
  219. {
  220. // HAL 未初始化(未 ScanDevices)等异常:记录后按降级继续(M1 调试可独立打开),借用语义待 M7 真机验证。
  221. AddMessageInfo($"[{currentHouse.houseSn}]HAL 借用异常(降级直连):{le.Message}");
  222. }
  223. // 调试串口/相机仍用 operate 侧具体类型(方法更全),但已在前台借用保护下打开(同舱单一使用者)。
  224. comBin = new ComBin(currentHouse.houseSn, currentHouse.housePort);
  225. comBin.CommandLogEvent += ComBin_CommandLogEvent;
  226. comBin.ErrorLogEvent += ComBin_ErrorLogEvent;
  227. comBin.ExceptionLogEvent += ComBin_ExceptionLogEvent;
  228. comBin.AddMessageInfoEvent += AddMessageInfo;
  229. var openPort = comBin.OpenPort();
  230. if (openPort)
  231. {
  232. AddMessageInfo($"[{currentHouse.houseSn}][{currentHouse.housePort}]舱室串口打开成功");
  233. }
  234. else
  235. {
  236. AddMessageInfo($"[{currentHouse.houseSn}][{currentHouse.housePort}]舱室串口打开失败");
  237. return;
  238. }
  239. // M1-03 HAL: 相机在前台借用保护下打开(同舱单一使用者);具体类型仍用 operate 侧(RawToRgb 等更全)。
  240. camera = new Camera(CurrentCCDId, currentHouse.ccdWidth, currentHouse.ccdHeight, currentHouse.ccdExposure);
  241. int cameraInit = camera.Init();
  242. AddMessageInfo($"[CCD模块初始化]CCDID={CurrentCCDId},SN={currentHouse.ccdSn},宽度={currentHouse.ccdWidth},高度={currentHouse.ccdHeight},曝光={currentHouse.ccdExposure}初始化结果:{cameraInit}[注:0 表示调用成功]");
  243. if (cameraInit != 0)
  244. {
  245. return;
  246. }
  247. int cameraSetMode = camera.SetOpMode();
  248. AddMessageInfo($"[CCD模块初始化]开启图像捕捉结果:{cameraSetMode}[注:0 表示调用成功]");
  249. if (cameraSetMode != 0)
  250. {
  251. return;
  252. }
  253. var currentHorSetting = houseWellSettingList.FirstOrDefault(x => x.houseSn == currentHouse.houseSn && x.wellSn == 1);
  254. if (currentHorSetting == null)
  255. {
  256. AddMessageInfo($"[{currentHouse.houseSn}][{currentHouse.housePort}][未获取到1号well的水平电机位置]");
  257. return;
  258. }
  259. //OpenVideoEvent?.Invoke();
  260. var verNewValue = currentHorSetting.eepromClearPosition;
  261. var cc = ccdPhoto.FirstOrDefault(x => x.houseSn == currentHouse.houseSn && x.wellSn == 1);
  262. if (cc != null)
  263. {
  264. verNewValue = cc.clearestPosition;
  265. }
  266. CustomProtocol custom = new CustomProtocol();
  267. comBin.ShakeHandsWait(custom);
  268. OpenLed();
  269. comBin.HorizontalMotorResetWait(custom, tLSetting.motorDelay);
  270. CurrentHor = 0;
  271. CurrentWell = 0;
  272. comBin.HorizontalMotorAbsoluteWait(custom, tLSetting.motorDelay, currentHorSetting.horizontalMotorPosition);
  273. CurrentHor = currentHorSetting.horizontalMotorPosition;
  274. CurrentWell = 1;
  275. comBin.VerticalMotorResetWait(custom, tLSetting.motorDelay);
  276. CurrentVer = 0;
  277. CurrentFocal = 0;
  278. comBin.VerticalMotorAbsoluteWait(custom, tLSetting.motorDelay, verNewValue, currentHorSetting.horizontalMotorPosition, 1, 1, 1);
  279. CurrentVer = verNewValue;
  280. CurrentFocal = 1;
  281. RedTem();
  282. RedPre();
  283. RedDoor();
  284. RedVentTime();
  285. return;
  286. }
  287. catch (Exception ex)
  288. {
  289. AppData.Instance.LogHelper.ExceptionLog(ex, "调试模式初始化", LogEnum.RunException);
  290. AddMessageInfo($"初始化异常:{ex.Message}");
  291. return;
  292. }
  293. });
  294. }
  295. public bool ComHouseUnit()
  296. {
  297. try
  298. {
  299. if (comBin != null) AddMessageInfo($"[{currentHouse.houseSn}][{currentHouse.housePort}]舱室串口卸载结果:{comBin.ClosePort()}");
  300. if (camera != null) AddMessageInfo($"[CCD{currentHouse.ccdId} 卸载{camera.UnInit()}]");
  301. comBin = null;
  302. camera = null;
  303. // M1-03 HAL: 归还前台借用,HAL 恢复 control 对本舱采集(ResumeCapture)。
  304. if (_halLease != null)
  305. {
  306. try { _halLease.Dispose(); } catch { }
  307. _halLease = null;
  308. }
  309. return true;
  310. }
  311. catch (Exception ex)
  312. {
  313. AppData.Instance.LogHelper.ExceptionLog(ex, "调试模式卸载", LogEnum.RunException);
  314. AddMessageInfo($"卸载异常:{ex.Message}");
  315. return false;
  316. }
  317. }
  318. /// <summary>
  319. /// 读温度
  320. /// </summary>
  321. public void RedTem()
  322. {
  323. if (comBin == null) return;
  324. Temperature = comBin.TemperatureWait(new CustomProtocol());
  325. }
  326. /// <summary>
  327. /// 读压力
  328. /// </summary>
  329. public void RedPre()
  330. {
  331. if (comBin == null) return;
  332. Pressure = comBin.PressureWait(new CustomProtocol());
  333. }
  334. /// <summary>
  335. /// 读舱门
  336. /// </summary>
  337. public void RedDoor()
  338. {
  339. if (comBin == null) return;
  340. string DoorStateString = comBin.DoorStatusWait(new CustomProtocol()).ToString();
  341. if(DoorStateString == "关闭")
  342. {
  343. DoorState = KeyToStringConvert.GetLanguageStringByKey("C0305");
  344. }
  345. if (DoorStateString == "打开")
  346. {
  347. DoorState = KeyToStringConvert.GetLanguageStringByKey("C0306");
  348. }
  349. }
  350. /// <summary>
  351. /// 打开Led灯
  352. /// </summary>
  353. public void OpenLed()
  354. {
  355. if (comBin == null) return;
  356. comBin.OpenLEDWait(new CustomProtocol());
  357. LedState = KeyToStringConvert.GetLanguageStringByKey("C0306");
  358. //LedState = "开启";
  359. }
  360. /// <summary>
  361. /// 关闭Led灯
  362. /// </summary>
  363. public void CloseLed()
  364. {
  365. if (comBin == null) return;
  366. comBin.CloseLEDWait(new CustomProtocol());
  367. //LedState = "关闭";
  368. LedState = KeyToStringConvert.GetLanguageStringByKey("C0305");
  369. }
  370. /// <summary>
  371. /// 打开进气阀
  372. /// </summary>
  373. public void OpenIntake()
  374. {
  375. if (comBin == null) return;
  376. comBin.OpenIntakeValveWait(new CustomProtocol(), tLSetting.valueDelay);
  377. }
  378. /// <summary>
  379. /// 关闭进气阀
  380. /// </summary>
  381. public void CloseIntake()
  382. {
  383. if (comBin == null) return;
  384. comBin.CloseIntakeValveWait(new CustomProtocol(), tLSetting.valueDelay);
  385. }
  386. /// <summary>
  387. /// 打开排气阀
  388. /// </summary>
  389. public void OpenExhaust()
  390. {
  391. if (comBin == null) return;
  392. comBin.OpenExhaustValveWait(new CustomProtocol(), tLSetting.valueDelay);
  393. }
  394. /// <summary>
  395. /// 关闭排气阀
  396. /// </summary>
  397. public void CloseExhaust()
  398. {
  399. if (comBin == null) return;
  400. comBin.CloseExhaustValveWait(new CustomProtocol(), tLSetting.valueDelay);
  401. }
  402. /// <summary>
  403. /// 舱室补气
  404. /// </summary>
  405. public void HouseAeration()
  406. {
  407. if (comBin == null) return;
  408. comBin.HouseAerationWait(new CustomProtocol());
  409. }
  410. /// <summary>
  411. /// 舱室排气
  412. /// </summary>
  413. public void HouseVent()
  414. {
  415. if (comBin == null) return;
  416. comBin.HouseVentWait(new CustomProtocol());
  417. }
  418. /// <summary>
  419. /// 写舱室进气阀时间
  420. /// </summary>
  421. /// <param name="newValue"></param>
  422. public void WriteOpenIntakeTime(int newValue)
  423. {
  424. if (comBin == null) return;
  425. comBin.WriteEEPROOpenIntakeTimeWait(new CustomProtocol(), newValue);
  426. }
  427. /// <summary>
  428. /// 写舱室排气阀时间
  429. /// </summary>
  430. /// <param name="newValue"></param>
  431. public void WriteOpenVentTime(int newValue)
  432. {
  433. if (comBin == null) return;
  434. comBin.WriteEEPROOpenVentTimeWait(new CustomProtocol(), newValue);
  435. HouseVentTimeE = newValue;
  436. }
  437. /// <summary>
  438. /// 读舱室排气阀打开时间
  439. /// </summary>
  440. public void RedVentTime()
  441. {
  442. if (comBin == null) return;
  443. HouseVentTimeE = comBin.ReadEEPROMVentWait(new CustomProtocol());//ReadEEPROMVentWait
  444. }
  445. /// <summary>
  446. /// 写垂直电机间隔脉冲
  447. /// </summary>
  448. /// <param name="newValue"></param>
  449. public void WriteOVerSpace(int newValue)
  450. {
  451. if (comBin == null) return;
  452. comBin.WriteEEPROMvertMtScanPluseWait(new CustomProtocol(), newValue);
  453. }
  454. /// <summary>
  455. /// 保存水平电机位置
  456. /// </summary>
  457. /// <param name="newValue"></param>
  458. public void SaveWellHor()
  459. {
  460. if (comBin == null) return;
  461. // M8-P3b:手调保存水平电机位置(关键命令入口,module=对焦调试)。
  462. Aivfo.OperationLog.OperationLogger.Run("对焦调试", "手调保存水平电机位置",
  463. () => comBin.WriteEEPROMhoriMtWellHorHorWait(new CustomProtocol(), CurrentWell, CurrentHor),
  464. input: new { houseSn = CurrentHouseId, well = CurrentWell, hor = CurrentHor });
  465. }
  466. #region 电机运动
  467. /// <summary>
  468. /// 水平电机复位
  469. /// </summary>
  470. public void HorizontalMotorReset()
  471. {
  472. if (comBin == null) return;
  473. // M8-P3b:电机控制命令埋点(module=对焦调试)。
  474. Aivfo.OperationLog.OperationLogger.Run("对焦调试", "水平电机复位",
  475. () => comBin.HorizontalMotorResetWait(new CustomProtocol(), tLSetting.motorDelay),
  476. input: new { houseSn = CurrentHouseId });
  477. CurrentHor = 0;
  478. CurrentWell = 0;
  479. }
  480. /// <summary>
  481. /// 水平电机正向
  482. /// </summary>
  483. public void HorizontalMotorForward(int value)
  484. {
  485. if (comBin == null) return;
  486. comBin.HorizontalMotorForwardWait(new CustomProtocol(), tLSetting.motorDelay, value);
  487. CurrentHor += value;
  488. }
  489. /// <summary>
  490. /// 水平电机反向
  491. /// </summary>
  492. public void HorizontalMotorBackward(int value)
  493. {
  494. if (comBin == null) return;
  495. comBin.HorizontalMotorBackward(new CustomProtocol(), tLSetting.motorDelay, value);
  496. CurrentHor -= value;
  497. }
  498. /// <summary>
  499. /// 水平电机到目标well
  500. /// </summary>
  501. /// <param name="newWell"></param>
  502. public void HorizontalMotorToWell(int newWell)
  503. {
  504. if (comBin == null) return;
  505. var currentHorSetting = houseWellSettingList.FirstOrDefault(x => x.houseSn == CurrentHouseId && x.wellSn == newWell);
  506. if (currentHorSetting == null)
  507. {
  508. AddMessageInfo($"[未获取到{newWell}号well的水平电机位置]");
  509. return;
  510. }
  511. comBin.HorizontalMotorAbsoluteWait(new CustomProtocol(), tLSetting.motorDelay, currentHorSetting.horizontalMotorPosition);
  512. CurrentHor = currentHorSetting.horizontalMotorPosition;
  513. CurrentWell = newWell;
  514. }
  515. /// <summary>
  516. /// 垂直电机复位
  517. /// </summary>
  518. public void VerticalMotorReset()
  519. {
  520. if (comBin == null) return;
  521. // M8-P3b:电机控制命令埋点(module=对焦调试)。
  522. Aivfo.OperationLog.OperationLogger.Run("对焦调试", "垂直电机复位",
  523. () => comBin.VerticalMotorResetWait(new CustomProtocol(), tLSetting.motorDelay),
  524. input: new { houseSn = CurrentHouseId });
  525. CurrentVer = 0;
  526. }
  527. /// <summary>
  528. /// 垂直电机正向运动
  529. /// </summary>
  530. public void VerticalMotorForward(int newValue)
  531. {
  532. if (comBin == null) return;
  533. comBin.VerticalMotorForwardWait(new CustomProtocol(), tLSetting.motorDelay, newValue);
  534. CurrentVer += newValue;
  535. }
  536. /// <summary>
  537. /// 垂直电机反向运动
  538. /// </summary>
  539. public void VerticalMotorBackward(int newValue)
  540. {
  541. if (comBin == null) return;
  542. comBin.VerticalMotorBackwardWait(new CustomProtocol(), tLSetting.motorDelay, newValue);
  543. CurrentVer -= newValue;
  544. }
  545. /// <summary>
  546. /// 垂直电机绝对运动
  547. /// </summary>
  548. public void VerticalMotorAbsolute(int newValue)
  549. {
  550. if (comBin == null) return;
  551. comBin.VerticalMotorAbsoluteWait(new CustomProtocol(), tLSetting.motorDelay, newValue, 1, 1, 1, 1);
  552. CurrentVer = newValue;
  553. }
  554. /// <summary>
  555. /// 一键电机准备
  556. /// </summary>
  557. public void MototReady()
  558. {
  559. var currentHorSetting = houseWellSettingList.FirstOrDefault(x => x.houseSn == CurrentHouseId && x.wellSn == 1);
  560. if (currentHorSetting == null)
  561. {
  562. AddMessageInfo($"[未获取到1号well的水平电机位置]");
  563. return;
  564. }
  565. var verNewValue = currentHorSetting.eepromClearPosition;
  566. var cc = ccdPhoto.FirstOrDefault(x => x.houseSn == CurrentHouseId && x.wellSn == 1);
  567. if (cc != null)
  568. {
  569. verNewValue = cc.clearestPosition;
  570. }
  571. CustomProtocol custom = new CustomProtocol();
  572. comBin.HorizontalMotorResetWait(custom, tLSetting.motorDelay);
  573. CurrentHor = 0;
  574. CurrentWell = 0;
  575. comBin.HorizontalMotorAbsoluteWait(custom, tLSetting.motorDelay, currentHorSetting.horizontalMotorPosition);
  576. CurrentHor = currentHorSetting.horizontalMotorPosition;
  577. CurrentWell = 1;
  578. comBin.VerticalMotorResetWait(custom, tLSetting.motorDelay);
  579. CurrentVer = 0;
  580. CurrentFocal = 0;
  581. comBin.VerticalMotorAbsoluteWait(custom, tLSetting.motorDelay, verNewValue, currentHorSetting.horizontalMotorPosition, 1, 1, 1);
  582. CurrentVer = verNewValue;
  583. CurrentFocal = 1;
  584. }
  585. /// <summary>
  586. /// 单张抓拍
  587. /// </summary>
  588. public bool SavePic(string fullName)
  589. {
  590. try
  591. {
  592. if (GetPicData() == 0)
  593. {
  594. if (camera == null) return false;
  595. return camera.SavePic(fullName, CurrentHouse.ccdWidth, CurrentHouse.ccdHeight);
  596. }
  597. return false;
  598. }
  599. catch (Exception ex)
  600. {
  601. AppData.Instance.LogHelper.ExceptionLog(ex, $"单张抓拍", LogEnum.RunException);
  602. AddMessageInfo($"单张抓拍异常:{ex.Message}");
  603. return false;
  604. }
  605. }
  606. /// <summary>
  607. /// 水平抓图
  608. /// </summary>
  609. public void ShuiPingZhuaPai()
  610. {
  611. try
  612. {
  613. IsStop = false;
  614. if (comBin == null) return;
  615. CustomProtocol custom = new CustomProtocol();
  616. custom.logDateTime = DateTime.Now;
  617. if (IsStop) return;
  618. comBin.HorizontalMotorResetWait(custom, tLSetting.motorDelay);
  619. CurrentHor = 0;
  620. CurrentWell = 0;
  621. HouseWellSetting currentHorSetting = null;
  622. string dtnow = string.Format("{0:yyyy-MM-dd-HH-mm-ss}", custom.logDateTime); //24小时制
  623. string path = AppData.Instance.LogHelper.GetDeBugShuiPingDirectory(CurrentHouse.houseSn, dtnow);
  624. if (!Directory.Exists(path))
  625. {
  626. Directory.CreateDirectory(path);
  627. }
  628. for (int i = 1; i <= 16; i++)
  629. {
  630. currentHorSetting = houseWellSettingList.FirstOrDefault(x => x.houseSn == CurrentHouseId && x.wellSn == i);
  631. if (currentHorSetting == null)
  632. {
  633. AddMessageInfo($"[未获取到1号well的水平电机位置]");
  634. continue;
  635. }
  636. if (IsStop) return;
  637. if (comBin == null) return;
  638. comBin.HorizontalMotorAbsoluteWait(custom, tLSetting.motorDelay, currentHorSetting.horizontalMotorPosition);
  639. if (IsStop) return;
  640. CurrentHor = currentHorSetting.horizontalMotorPosition;
  641. CurrentWell = i;
  642. string filename = $"{dtnow}-house{CurrentHouse.houseSn}-well{CurrentWell}-{CurrentFocal}-{CurrentHor}-{CurrentVer}.jpg";
  643. string fullName = $"{path}{filename}";
  644. if (camera == null) return;
  645. if (SavePic(fullName))
  646. {
  647. AddPic(fullName);
  648. }
  649. }
  650. }
  651. catch (Exception ex)
  652. {
  653. AppData.Instance.LogHelper.ExceptionLog(ex, $"水平抓拍", LogEnum.RunException);
  654. AddMessageInfo($"水平抓拍异常:{ex.Message}");
  655. }
  656. }
  657. /// <summary>
  658. /// 清晰图层抓图
  659. /// </summary>
  660. public async Task AutoFocusPic(int focalCount, int xun)
  661. {
  662. await Task.Run(async () =>
  663. {
  664. try
  665. {
  666. IsStop = false;
  667. //var currentHorSetting = houseWellSettingList.FirstOrDefault(x => x.houseSn == CurrentHouseId && x.wellSn == 1);
  668. //if (currentHorSetting == null)
  669. //{
  670. // AddMessageInfo($"[未获取到1号well的水平电机位置]");
  671. // return;
  672. //}
  673. var startAutoFocus = -1;
  674. var cc = ccdPhoto.FirstOrDefault(x => x.houseSn == CurrentHouseId && x.wellSn == CurrentWell);
  675. if (cc == null)
  676. {
  677. AddMessageInfo($"[未获取到{CurrentWell}号well的自动对焦起点]");
  678. return;
  679. }
  680. startAutoFocus = cc.autoFocusPosition;
  681. CustomProtocol custom = new CustomProtocol();
  682. string dtnow = string.Format("{0:yyyy-MM-dd-HH-mm-ss}", DateTime.Now); //24小时制
  683. string path = null;
  684. string fullName = null;
  685. for (int k = 0; k < xun; k++)
  686. {
  687. if (IsStop) return;
  688. if (comBin == null) return;
  689. comBin.VerticalMotorResetWait(custom, tLSetting.motorDelay);
  690. CurrentVer = 0;
  691. CurrentFocal = 0;
  692. var verSpacePulse = 128;
  693. if (CurrentHouse.verticalMotorSpacePulse.HasValue) verSpacePulse = CurrentHouse.verticalMotorSpacePulse.Value;
  694. path = $"{AppData.Instance.LogHelper.GetDeBugQingXiDirectory(CurrentHouse.houseSn, CurrentWell, dtnow)}{(k + 1)}_{CurrentHor}";
  695. if (!Directory.Exists(path)) Directory.CreateDirectory(path);
  696. for (int i = 0; i < focalCount; i++)
  697. {
  698. CurrentFocal = i + 1;
  699. var currentVerValue = startAutoFocus + (i * verSpacePulse);
  700. if (IsStop) return;
  701. if (comBin == null) return;
  702. if(currentVerValue > tLSetting.verticalMotorPulseMax)
  703. {
  704. AddMessageInfo($"清晰图层抓图超出垂直电机上限:{tLSetting.verticalMotorPulseMax}");
  705. return;
  706. }
  707. comBin.VerticalMotorAbsoluteWait(custom, tLSetting.motorDelay, currentVerValue, 1, 1, 1, 1);
  708. CurrentVer = currentVerValue;
  709. fullName = $"{path}\\{currentVerValue}.jpg";
  710. if (IsStop) return;
  711. if (camera == null) return;
  712. if (SavePic(fullName)) AddPic(fullName);
  713. }
  714. }
  715. }
  716. catch (Exception ex)
  717. {
  718. AppData.Instance.LogHelper.ExceptionLog(ex, $"清晰图层抓图", LogEnum.RunException);
  719. AddMessageInfo($"清晰图层抓图异常:{ex.Message}");
  720. }
  721. });
  722. }
  723. /// <summary>
  724. /// 抓图
  725. /// </summary>
  726. public int GetPicData()
  727. {
  728. try
  729. {
  730. if (camera == null) return -1;
  731. int GetRgbData = -1;
  732. bool isWuTu = false;
  733. if (isWuTu)
  734. {
  735. GetRgbData = camera.GetRawData();//获取字节流
  736. AddMessageInfo($"[抓拍结果:{GetRgbData}][注:0表示成功]");
  737. if (GetRgbData == 0)
  738. {
  739. GetRgbData = camera.RawToRgb();
  740. AddMessageInfo($"[rawToRgb结果:{GetRgbData}][注:0表示成功]");
  741. }
  742. }
  743. else
  744. {
  745. GetRgbData = camera.GetRgbData();//获取字节流
  746. AddMessageInfo($"[抓拍结果:{GetRgbData}][注:0表示成功]");
  747. }
  748. return GetRgbData;
  749. }
  750. catch (Exception ex)
  751. {
  752. AppData.Instance.LogHelper.ExceptionLog(ex, $"调试模式抓拍", LogEnum.RunException);
  753. AddMessageInfo($"相机抓拍异常:{ex.Message}");
  754. return -1;
  755. }
  756. }
  757. #endregion
  758. #region 相机操作
  759. /// <summary>
  760. /// 设置曝光值
  761. /// </summary>
  762. /// <param name="newExporuse"></param>
  763. public void SetExposure(int newExporuse)
  764. {
  765. AddMessageInfo($"设置曝光结果:{camera.SetPartOfCapInfo(newExporuse)}");
  766. }
  767. //public bool OpenVideo(IntPtr mControlPtr, int left, int top, int width, int height)
  768. //{
  769. // try
  770. // {
  771. // var a = camera.Usb2Start(mControlPtr, left, top, width, height);
  772. // AddMessageInfo($"图像打开结果[0表示成功]:{a}");
  773. // return a == 0;
  774. // }
  775. // catch (Exception ex)
  776. // {
  777. // ExLog(ex, "OpenVideo");
  778. // AddMessageInfo($"图像打开异常:{ex.Message}");
  779. // return false;
  780. // }
  781. //}
  782. //public bool CloseVideo()
  783. //{
  784. // try
  785. // {
  786. // var a = camera.Usb2Stop();
  787. // AddMessageInfo($"图像关闭结果[0表示成功]:{a}");
  788. // return a == 0;
  789. // }
  790. // catch (Exception ex)
  791. // {
  792. // ExLog(ex, "CloseVideo");
  793. // AddMessageInfo($"图像关闭异常:{ex.Message}");
  794. // return false;
  795. // }
  796. //}
  797. #endregion
  798. #region 参数保存
  799. /// <summary>
  800. /// 保存舱室参数
  801. /// </summary>
  802. /// <returns></returns>
  803. public bool SetHouseInfo()
  804. {
  805. try
  806. {
  807. if (CurrentHouse == null) return true;
  808. string body = JsonConvert.SerializeObject(new
  809. {
  810. tlSn = CurrentHouse.tlSn,
  811. houseSn = CurrentHouse.houseSn,
  812. ccdExposure = CurrentHouse.ccdExposure,
  813. inletValveOpeningTime = CurrentHouse.inletValveOpeningTime,
  814. verticalMotorSpacePulse = CurrentHouse.verticalMotorSpacePulse,
  815. });
  816. return AppData.Instance.HttpHelper.HouseDebuggingApi(body);
  817. }
  818. catch (Exception ex)
  819. {
  820. ExLog(ex, "SetHouseExposure");
  821. return false;
  822. }
  823. }
  824. /// <summary>
  825. /// 保存当前well位置
  826. /// </summary>
  827. /// <returns></returns>
  828. public bool SetWellMotorPosition()
  829. {
  830. try
  831. {
  832. if (CurrentHouse == null)
  833. {
  834. AddMessageInfo("获取舱室信息失败");
  835. }
  836. string body = JsonConvert.SerializeObject(new
  837. {
  838. horizontalMotorPosition = CurrentHor,
  839. houseSn = CurrentHouse.houseSn,
  840. tlSn = CurrentHouse.tlSn,
  841. wellSn = CurrentWell,
  842. });
  843. return AppData.Instance.HttpHelper.WellUpdateApi(body);
  844. }
  845. catch (Exception ex)
  846. {
  847. ExLog(ex, "SetWellMotorPosition");
  848. return false;
  849. }
  850. }
  851. /// <summary>
  852. /// 保存自动对焦起点
  853. /// </summary>
  854. /// <returns></returns>
  855. public bool SetWellAutoFocus()
  856. {
  857. try
  858. {
  859. if (CurrentHouse == null)
  860. {
  861. AddMessageInfo("获取舱室信息失败");
  862. }
  863. string body = JsonConvert.SerializeObject(new
  864. {
  865. verticalMotorPosition = CurrentVer,
  866. houseSn = CurrentHouse.houseSn,
  867. tlSn = CurrentHouse.tlSn,
  868. wellSn = CurrentWell,
  869. });
  870. return AppData.Instance.HttpHelper.WellUpdateApi(body);
  871. }
  872. catch (Exception ex)
  873. {
  874. ExLog(ex, "SetWellAutoFocus");
  875. return false;
  876. }
  877. }
  878. #endregion
  879. private void ComBin_ExceptionLogEvent(Exception exception, string arg2, string arg3, LogEnum @enum)
  880. {
  881. AppData.Instance.LogHelper.ExceptionLog(exception, $"{arg2}{arg3}", @enum);
  882. }
  883. private void ComBin_ErrorLogEvent(string arg1, LogEnum @enum)
  884. {
  885. AppData.Instance.LogHelper.TLLog(arg1, @enum);
  886. }
  887. private void ComBin_CommandLogEvent(int arg1, DateTime time, string arg3, LogEnum @enum)
  888. {
  889. AppData.Instance.LogHelper.HouseLog(arg1, time, arg3, @enum);
  890. }
  891. public void AddMessageInfo(string mess)
  892. {
  893. MessageEvent?.Invoke(mess);
  894. }
  895. public void AddPic(string picFullName)
  896. {
  897. AddPicEvent?.Invoke(picFullName, CurrentWell, CurrentFocal);
  898. }
  899. #region M2-05 场景A 一键全自动标定(沙盒)
  900. /// <summary>
  901. /// M2-05 场景A 工程师调试页一键全自动标定(人盯安全沙盒,03 §2/§7.1)。
  902. /// 流程:对选中 well 列表逐 well 跑 M2-01 四步标定引擎(CalibrationEngine) →
  903. /// 收集 WellCalib → 实时 UI 合格绿/伪峰红 → 结果作出厂基准 scene=0 落库 + 写 calibration.json。
  904. ///
  905. /// 硬件复用:调试页 ComHouseInit 已经 Acquire(OperateDebug) 前台借用(HAL 暂停 control 采集),
  906. /// 并自持 comBin/camera。标定引擎经 DebugSerialAdapter/DebugCameraAdapter 驱动【已打开】的调试栈硬件,
  907. /// 不二次 Open/Init 同 COM 口/相机(见适配器说明)。互斥由调试页持有的前台借用保证。
  908. /// 中止:IsStopCalibrate 标志,每 well 间检查,工程师可随时中止,已完成 well 结果保留。
  909. /// 异常:单 well 失败/存储失败不崩 UI,记日志继续;整体异常兜底,IsCalibrating 复位。
  910. ///
  911. /// ⚠ 待验证 V-055..V-059:16well 跑通(关联 V-004 算法严谨性)/合格绿伪峰红/基准scene0落库+JSON/
  912. /// 借用期间采集暂停/中止响应。
  913. /// </summary>
  914. /// <param name="wells">勾选的 well 列表(空/null 视为全 16 well)。</param>
  915. public async Task OneClickCalibrate(IEnumerable<int> wells = null)
  916. {
  917. await Task.Run(() =>
  918. {
  919. if (IsCalibrating)
  920. {
  921. AddMessageInfo("[一键标定]已在标定中,请勿重复触发");
  922. return;
  923. }
  924. if (comBin == null || camera == null)
  925. {
  926. AddMessageInfo("[一键标定]请先初始化舱室(串口/相机未就绪)");
  927. return;
  928. }
  929. if (CurrentHouse == null)
  930. {
  931. AddMessageInfo("[一键标定]未获取到舱室信息");
  932. return;
  933. }
  934. IsCalibrating = true;
  935. IsStopCalibrate = false;
  936. // M8-P3b:一键标定为关键命令入口,建立操作日志 scope(统一 traceId 串联本次标定内的 HTTP/串口/相机埋点)。
  937. using var _opScope = Aivfo.OperationLog.OperationLogger.Begin("对焦调试", "一键标定",
  938. houseSn: CurrentHouseId);
  939. try { _opScope.Input(new { houseSn = CurrentHouseId }); } catch { }
  940. // 勾选 well:未传则默认 16 well;去重、按序、限定 1-16。
  941. var wellList = (wells != null ? wells : Enumerable.Range(1, 16))
  942. .Where(w => w >= 1 && w <= 16).Distinct().OrderBy(w => w).ToList();
  943. if (wellList.Count == 0) wellList = Enumerable.Range(1, 16).ToList();
  944. // 合格判据阈值:tl_setting.focus_peak_ratio_threshold,缺省 1.2(03 §7.1)。
  945. double peakThreshold = (tLSetting != null && tLSetting.focusPeakRatioThreshold.HasValue)
  946. ? (double)tLSetting.focusPeakRatioThreshold.Value : 1.2;
  947. ResetCalibResults(wellList);
  948. // 标定结果存储:calibration.json 真相源(12 §2.7/03 §4,IncludeFields=true 由 CalibrationFile.Load 保证)。
  949. // operate 端无 SqlSugar 直连(走 HttpHelper),故 DbMirror=null:仅写 JSON 真相源;
  950. // house_autofocus_calibration 的 scene=0 基准 upsert 由 control 端 AppData.AutofocusStore 镜像(M2-04)。
  951. var store = new CalibrationStore
  952. {
  953. JsonPath = System.IO.Path.Combine(
  954. System.AppDomain.CurrentDomain.BaseDirectory, @"DependFile\AutoFocus\calibration.json"),
  955. Source = "OPERATE_DEBUG_SCENE0",
  956. Log = msg => AddMessageInfo($"[一键标定]{msg}"),
  957. // DbMirror 为空 → 只写 JSON 不镜像库(operate 无 DB 直连)。
  958. };
  959. try
  960. {
  961. var jsonDir = System.IO.Path.GetDirectoryName(store.JsonPath);
  962. if (!string.IsNullOrEmpty(jsonDir) && !System.IO.Directory.Exists(jsonDir))
  963. System.IO.Directory.CreateDirectory(jsonDir);
  964. }
  965. catch (Exception exDir)
  966. {
  967. AddMessageInfo($"[一键标定]创建 calibration.json 目录失败(将仅 UI 显示):{exDir.Message}");
  968. }
  969. string tlSn = (tLSetting != null && !string.IsNullOrEmpty(tLSetting.tlSn))
  970. ? tLSetting.tlSn : (CurrentHouse.tlSn ?? AppData.Instance.TlSn);
  971. try
  972. {
  973. // 经适配器把调试栈已打开的 comBin/camera 暴露为 HAL 接口,复用 M2-01 引擎(不重复实现算法)。
  974. var serial = new DebugSerialAdapter(comBin, tLSetting != null ? tLSetting.motorDelay : 1500);
  975. var cam = new DebugCameraAdapter(camera);
  976. var engine = new CalibrationEngine(serial, cam)
  977. {
  978. Log = msg => AddMessageInfo(msg)
  979. };
  980. AddMessageInfo($"[一键标定]开始:舱{CurrentHouse.houseSn} 共 {wellList.Count} well,合格峰比阈值={peakThreshold:F2}");
  981. foreach (var well in wellList)
  982. {
  983. if (IsStopCalibrate)
  984. {
  985. AddMessageInfo("[一键标定]已被中止,停止后续 well");
  986. break;
  987. }
  988. UpdateCalibResult(well, item => { item.State = WellCalibState.Running; item.Note = "标定中..."; });
  989. CurrentWell = well;
  990. try
  991. {
  992. // EEPROM 仅作扫描中心(参考),不进配置解析链、不回写(§2.5)。
  993. int hpos = serial.ReadWellHorizontalPosWait(well);
  994. int zZero = serial.ReadWellFocusZeroWait(well);
  995. var wc = engine.CalibrateWell(well, Math.Max(0, hpos), Math.Max(0, zZero));
  996. if (wc == null)
  997. {
  998. UpdateCalibResult(well, item =>
  999. {
  1000. item.State = WellCalibState.Failed;
  1001. item.Note = "无结果";
  1002. });
  1003. AddMessageInfo($"[一键标定]{well}号 well 无标定结果");
  1004. continue;
  1005. }
  1006. // 合格判据:circleFound && peakRatio>阈值 → 绿;否则(未检圆/伪峰/弱峰) → 红。
  1007. bool qualified = wc.CircleFound && wc.PeakRatio > peakThreshold;
  1008. UpdateCalibResult(well, item =>
  1009. {
  1010. item.State = qualified ? WellCalibState.Qualified : WellCalibState.FakePeak;
  1011. item.FocusZ = wc.FocusZ;
  1012. item.PeakRatio = wc.PeakRatio;
  1013. item.CenterOffsetPct = wc.CenterOffsetPct;
  1014. item.CircleFound = wc.CircleFound;
  1015. item.Exposure = wc.Exposure;
  1016. item.Note = qualified
  1017. ? $"合格 Z={wc.FocusZ} 峰比={wc.PeakRatio:F2}"
  1018. : $"伪峰 Z={wc.FocusZ} 峰比={wc.PeakRatio:F2} {(wc.CircleFound ? "" : "未检圆")}{wc.Note}";
  1019. });
  1020. AddMessageInfo($"[一键标定]{well}号 well {(qualified ? "✓合格(绿)" : "✗伪峰(红)")} " +
  1021. $"FocusZ={wc.FocusZ} 峰比={wc.PeakRatio:F2} 偏移={wc.CenterOffsetPct:F1}%");
  1022. // 出厂基准 scene=0 落库 + 写 calibration.json(存储失败不崩 UI,CalibrationStore 内部已吞异常)。
  1023. try
  1024. {
  1025. store.SaveCalibration(wc, tlSn, CurrentHouse.houseSn, well, scene: 0,
  1026. port: CurrentHouse.housePort, ccdSn: CurrentHouse.ccdSn);
  1027. }
  1028. catch (Exception exStore)
  1029. {
  1030. AddMessageInfo($"[一键标定]{well}号 well 结果存档失败:{exStore.Message}");
  1031. }
  1032. }
  1033. catch (Exception exWell)
  1034. {
  1035. // 单 well 异常不崩 UI:记日志/标红,继续下一 well。
  1036. UpdateCalibResult(well, item =>
  1037. {
  1038. item.State = WellCalibState.Failed;
  1039. item.Note = $"异常:{exWell.Message}";
  1040. });
  1041. ExLog(exWell, $"OneClickCalibrate.well{well}");
  1042. AddMessageInfo($"[一键标定]{well}号 well 标定异常:{exWell.Message}");
  1043. }
  1044. }
  1045. int okCount = CalibResults.Count(x => x.State == WellCalibState.Qualified);
  1046. AddMessageInfo($"[一键标定]结束:合格 {okCount}/{wellList.Count}(结果已存档为出厂基准 scene=0 + calibration.json)");
  1047. // M8-P3b:记录标定结果到操作日志 scope。
  1048. try { _opScope.Output(new { okCount, total = wellList.Count }).Success(); } catch { }
  1049. }
  1050. catch (Exception ex)
  1051. {
  1052. ExLog(ex, "OneClickCalibrate");
  1053. AddMessageInfo($"[一键标定]整体异常:{ex.Message}");
  1054. // M8-P3b:标定整体异常标记失败。
  1055. try { _opScope.Fail(ex.GetType().Name + ": " + ex.Message); } catch { }
  1056. }
  1057. finally
  1058. {
  1059. IsCalibrating = false;
  1060. }
  1061. });
  1062. }
  1063. /// <summary>工程师中止一键标定。</summary>
  1064. public void StopCalibrate()
  1065. {
  1066. IsStopCalibrate = true;
  1067. AddMessageInfo("[一键标定]收到中止请求,将在当前 well 结束后停止");
  1068. }
  1069. private void ResetCalibResults(List<int> wells)
  1070. {
  1071. void build()
  1072. {
  1073. CalibResults.Clear();
  1074. foreach (var w in wells)
  1075. CalibResults.Add(new WellCalibUiItem { Well = w, State = WellCalibState.Pending, Note = "待标定" });
  1076. }
  1077. // 集合改动需在 UI 线程(ObservableCollection 绑定)。
  1078. var disp = System.Windows.Application.Current?.Dispatcher;
  1079. if (disp != null && !disp.CheckAccess()) disp.Invoke(build); else build();
  1080. }
  1081. private void UpdateCalibResult(int well, Action<WellCalibUiItem> mutate)
  1082. {
  1083. void apply()
  1084. {
  1085. var item = CalibResults.FirstOrDefault(x => x.Well == well);
  1086. if (item == null)
  1087. {
  1088. item = new WellCalibUiItem { Well = well };
  1089. CalibResults.Add(item);
  1090. }
  1091. mutate(item);
  1092. }
  1093. var disp = System.Windows.Application.Current?.Dispatcher;
  1094. if (disp != null && !disp.CheckAccess()) disp.Invoke(apply); else apply();
  1095. }
  1096. #endregion
  1097. #region M2-07 对焦后手动微调拍摄层(层数/层间距/下移)持久化 well 级
  1098. /// <summary>
  1099. /// M2-07 手调"实际拍摄层数"(well 级覆盖)。空字符串=留空继承设备级(保存写 null)。
  1100. /// View TextBox 双向绑定;校验在保存/预览时做(层数≥1)。
  1101. /// </summary>
  1102. [ObservableProperty] private string manualLayerCount = "";
  1103. /// <summary>
  1104. /// M2-07 手调"实际拍摄层间距脉冲"(well 级覆盖)。空=继承设备级(写 null)。校验:>0。
  1105. /// </summary>
  1106. [ObservableProperty] private string manualLayerSpacing = "";
  1107. /// <summary>
  1108. /// M2-07 手调"对焦起点下移层数"(复用 move_down_layer)。空=继承设备级。
  1109. /// 校验:≥0 且 &lt;层数(下移层须落在拍摄范围内,§2.4 清晰层=第 down 层)。
  1110. /// </summary>
  1111. [ObservableProperty] private string manualMoveDown = "";
  1112. /// <summary>
  1113. /// M2-07 实时预览各层绝对 Z 位置(调 M2-02 ComputeLayerPositions 生成,关联 V-045)。
  1114. /// View 绑定此集合显示 N 层位置列表,让工程师看到手调效果。
  1115. /// </summary>
  1116. public ObservableCollection<LayerPreviewItem> LayerPreview { get; } = new ObservableCollection<LayerPreviewItem>();
  1117. /// <summary>
  1118. /// M2-07 预览/手调锚点 well(默认随水平电机当前 well / 一键标定取 CurrentWell)。
  1119. /// </summary>
  1120. [ObservableProperty] private int manualTuneWell = 1;
  1121. /// <summary>
  1122. /// 取该 well 当前生效的"实际拍摄层"配置(well 级覆盖优先,空则回退设备级),
  1123. /// 回填三个手调输入框 + 触发一次预览。供"载入当前值"按钮调用。
  1124. /// </summary>
  1125. public void LoadManualLayerSetting(int well)
  1126. {
  1127. ManualTuneWell = well;
  1128. var ws = houseWellSettingList.FirstOrDefault(x => x.houseSn == CurrentHouseId && x.wellSn == well);
  1129. // well 级覆盖非空显示 well 值,否则显示继承的设备级值作占位(§2.5 就近优先)。
  1130. int? wellCount = ws?.focusLayerCount;
  1131. int? wellSpacing = ws?.focusLayerSpacingPulse;
  1132. // 下移复用 moveDownLayer(well 级为非空 int 列;0 视为有效覆盖值)。
  1133. int? wellDown = ws != null ? (int?)ws.moveDownLayer : null;
  1134. ManualLayerCount = (wellCount ?? tLSetting?.focusLayerCount)?.ToString() ?? "";
  1135. ManualLayerSpacing = (wellSpacing ?? tLSetting?.focusLayerSpacingPulse)?.ToString() ?? "";
  1136. ManualMoveDown = (wellDown ?? tLSetting?.focusLayerDown)?.ToString() ?? "";
  1137. PreviewManualLayers();
  1138. }
  1139. /// <summary>
  1140. /// 取该 well 最近标定的 FocusZ 作预览锚点(M2-05 一键标定结果 CalibResults 优先;
  1141. /// 回退 ccdPhoto 的 clearestPosition 即 scene0/1 库下发的清晰位置)。
  1142. /// 返回 null 表示无标定可用(须提示先标定)。
  1143. /// </summary>
  1144. private int? GetPreviewFocusZ(int well)
  1145. {
  1146. var calib = CalibResults.FirstOrDefault(x => x.Well == well
  1147. && (x.State == WellCalibState.Qualified || x.State == WellCalibState.FakePeak));
  1148. if (calib != null && calib.FocusZ > 0) return calib.FocusZ;
  1149. // 回退:调试页加载的清晰位置(scene0/1 库值 clearestPosition)。
  1150. var cc = ccdPhoto.FirstOrDefault(x => x.houseSn == CurrentHouseId && x.wellSn == well);
  1151. if (cc != null && cc.clearestPosition > 0) return cc.clearestPosition;
  1152. return null;
  1153. }
  1154. /// <summary>
  1155. /// 解析三个手调输入框(空=继承设备级取回退值),返回有效 (count,spacing,down) 或 null(非法/缺失)。
  1156. /// validateMsg 携带校验失败原因。
  1157. /// </summary>
  1158. private (int count, int spacing, int down)? ResolveEffectiveTune(out string validateMsg)
  1159. {
  1160. validateMsg = null;
  1161. // 解析层数:空→设备级回退;非空→必须 int。
  1162. int? count;
  1163. if (string.IsNullOrWhiteSpace(ManualLayerCount)) count = tLSetting?.focusLayerCount;
  1164. else if (int.TryParse(ManualLayerCount.Trim(), out int c)) count = c;
  1165. else { validateMsg = "层数只能输入整数"; return null; }
  1166. int? spacing;
  1167. if (string.IsNullOrWhiteSpace(ManualLayerSpacing)) spacing = tLSetting?.focusLayerSpacingPulse;
  1168. else if (int.TryParse(ManualLayerSpacing.Trim(), out int s)) spacing = s;
  1169. else { validateMsg = "层间距只能输入整数"; return null; }
  1170. int? down;
  1171. if (string.IsNullOrWhiteSpace(ManualMoveDown)) down = tLSetting?.focusLayerDown;
  1172. else if (int.TryParse(ManualMoveDown.Trim(), out int d)) down = d;
  1173. else { validateMsg = "下移层数只能输入整数"; return null; }
  1174. if (!count.HasValue) { validateMsg = "层数未配置(设备级也为空),请填层数或先初始化设备级"; return null; }
  1175. if (!spacing.HasValue) { validateMsg = "层间距未配置(设备级也为空),请填层间距或先初始化设备级(§2.5 不兜底)"; return null; }
  1176. if (!down.HasValue) { validateMsg = "下移层数未配置(设备级也为空),请填下移层数"; return null; }
  1177. // 校验:层数≥1、间距>0、下移≥0 且 <层数(下移层须落在拍摄范围内)。
  1178. if (count.Value < 1) { validateMsg = "层数必须 ≥ 1"; return null; }
  1179. if (spacing.Value <= 0) { validateMsg = "层间距必须 > 0"; return null; }
  1180. if (down.Value < 0) { validateMsg = "下移层数必须 ≥ 0"; return null; }
  1181. if (down.Value >= count.Value) { validateMsg = $"下移层数必须 < 层数({count.Value}),否则清晰层落在拍摄范围外"; return null; }
  1182. return (count.Value, spacing.Value, down.Value);
  1183. }
  1184. /// <summary>
  1185. /// M2-07 实时预览:用该 well 标定 FocusZ + 当前手调值,调 M2-02
  1186. /// ComputeLayerPositions(focusZ, cfg, pulseMax) 生成各层绝对 Z,刷新 LayerPreview(关联 V-045)。
  1187. /// 无标定锚点 → 清空预览并提示先标定;校验非法 → 清空预览并提示。
  1188. /// </summary>
  1189. public void PreviewManualLayers()
  1190. {
  1191. try
  1192. {
  1193. var focusZ = GetPreviewFocusZ(ManualTuneWell);
  1194. if (!focusZ.HasValue)
  1195. {
  1196. SetLayerPreview(null);
  1197. AddMessageInfo($"[手调拍摄层]well{ManualTuneWell} 无标定结果,请先对该 well 标定(或确认已下发清晰位置)再预览");
  1198. return;
  1199. }
  1200. var eff = ResolveEffectiveTune(out string msg);
  1201. if (eff == null)
  1202. {
  1203. SetLayerPreview(null);
  1204. AddMessageInfo($"[手调拍摄层]预览失败:{msg}");
  1205. return;
  1206. }
  1207. var cfg = new FocusLayerConfig
  1208. {
  1209. LayerCount = eff.Value.count,
  1210. LayerSpacingPulse = eff.Value.spacing,
  1211. LayerDown = eff.Value.down,
  1212. };
  1213. int pulseMax = tLSetting != null ? tLSetting.verticalMotorPulseMax : 0;
  1214. int[] positions = PhotoLayerCalculator.ComputeLayerPositions(focusZ.Value, cfg, pulseMax);
  1215. var items = new List<LayerPreviewItem>();
  1216. for (int i = 0; i < positions.Length; i++)
  1217. {
  1218. items.Add(new LayerPreviewItem
  1219. {
  1220. LayerIndex = i,
  1221. ZPulse = positions[i],
  1222. IsFocusLayer = (i == eff.Value.down), // 第 down 层 = 清晰层(FocusZ 锚点)
  1223. });
  1224. }
  1225. SetLayerPreview(items);
  1226. AddMessageInfo($"[手调拍摄层]well{ManualTuneWell} 预览:锚点FocusZ={focusZ.Value} 层数={eff.Value.count} " +
  1227. $"间距={eff.Value.spacing} 下移={eff.Value.down} → 第0层Z={positions[0]} 第{positions.Length - 1}层Z={positions[positions.Length - 1]}");
  1228. }
  1229. catch (Exception ex)
  1230. {
  1231. SetLayerPreview(null);
  1232. ExLog(ex, "PreviewManualLayers");
  1233. AddMessageInfo($"[手调拍摄层]预览异常:{ex.Message}");
  1234. }
  1235. }
  1236. /// <summary>
  1237. /// M2-07 保存为该 well 默认:校验通过后,把手调值写 house_well_setting well 级覆盖
  1238. /// (focus_layer_spacing_pulse/focus_layer_count + 复用 move_down_layer)。
  1239. /// 复用现有 operate 端 well 设置保存通道 HttpHelper.WellUpdateApi(同 SetWellMotorPosition/SetWellAutoFocus),
  1240. /// 走 /api/tl/control/setting/house/well/update。留空字段=继承设备级(写 null)。
  1241. /// 下次该 well 经 §2.5 就近优先(PhotoLayerCalculator.Resolve)沿用 well 覆盖值。
  1242. /// 返回 true=保存成功。
  1243. /// </summary>
  1244. public bool SaveManualLayerTune()
  1245. {
  1246. try
  1247. {
  1248. if (CurrentHouse == null)
  1249. {
  1250. AddMessageInfo("[手调拍摄层]未获取到舱室信息");
  1251. return false;
  1252. }
  1253. // 校验:层数≥1、间距>0、下移≥0 且 <层数;非法不保存。
  1254. var eff = ResolveEffectiveTune(out string msg);
  1255. if (eff == null)
  1256. {
  1257. AddMessageInfo($"[手调拍摄层]保存被拦截(非法值):{msg}");
  1258. return false;
  1259. }
  1260. // 留空字段=继承设备级 → 写 null(well 级覆盖留空,§2.5)。非空字段写实际值。
  1261. int? wellCount = string.IsNullOrWhiteSpace(ManualLayerCount) ? (int?)null : int.Parse(ManualLayerCount.Trim());
  1262. int? wellSpacing = string.IsNullOrWhiteSpace(ManualLayerSpacing) ? (int?)null : int.Parse(ManualLayerSpacing.Trim());
  1263. // 下移复用 move_down_layer(库列非空);留空时回退取生效值(eff.down)写回,不破坏旧列语义。
  1264. int wellMoveDown = string.IsNullOrWhiteSpace(ManualMoveDown) ? eff.Value.down : int.Parse(ManualMoveDown.Trim());
  1265. // 复用现有 well 设置保存通道:与 SetWellMotorPosition/SetWellAutoFocus 同一 WellUpdateApi。
  1266. // 后端按 tlSn/houseSn/wellSn 定位 house_well_setting 行做部分更新(仅本次携带的字段)。
  1267. string body = JsonConvert.SerializeObject(new
  1268. {
  1269. tlSn = CurrentHouse.tlSn,
  1270. houseSn = CurrentHouse.houseSn,
  1271. wellSn = ManualTuneWell,
  1272. focusLayerSpacingPulse = wellSpacing, // null=继承设备级
  1273. focusLayerCount = wellCount, // null=继承设备级
  1274. moveDownLayer = wellMoveDown, // 复用既有列(下移)
  1275. });
  1276. bool ok = AppData.Instance.HttpHelper.WellUpdateApi(body);
  1277. if (!ok)
  1278. {
  1279. AddMessageInfo("[手调拍摄层]保存失败(接口返回失败)");
  1280. return false;
  1281. }
  1282. // 本地内存同步,使"下次该 well 沿用"在不重载设置时也即时生效(预览/取数一致)。
  1283. var ws = houseWellSettingList.FirstOrDefault(x => x.houseSn == CurrentHouseId && x.wellSn == ManualTuneWell);
  1284. if (ws != null)
  1285. {
  1286. ws.focusLayerCount = wellCount;
  1287. ws.focusLayerSpacingPulse = wellSpacing;
  1288. ws.moveDownLayer = wellMoveDown;
  1289. }
  1290. AddMessageInfo($"[手调拍摄层]well{ManualTuneWell} 已存为默认:层数={(wellCount?.ToString() ?? "继承")} " +
  1291. $"间距={(wellSpacing?.ToString() ?? "继承")} 下移={wellMoveDown}(写 house_well_setting well 级覆盖,留空继承设备级)");
  1292. return true;
  1293. }
  1294. catch (Exception ex)
  1295. {
  1296. ExLog(ex, "SaveManualLayerTune");
  1297. AddMessageInfo($"[手调拍摄层]保存异常:{ex.Message}");
  1298. return false;
  1299. }
  1300. }
  1301. private void SetLayerPreview(List<LayerPreviewItem> items)
  1302. {
  1303. void build()
  1304. {
  1305. LayerPreview.Clear();
  1306. if (items != null)
  1307. foreach (var it in items) LayerPreview.Add(it);
  1308. }
  1309. var disp = System.Windows.Application.Current?.Dispatcher;
  1310. if (disp != null && !disp.CheckAccess()) disp.Invoke(build); else build();
  1311. }
  1312. #endregion
  1313. }
  1314. }