HomeViewModel.cs 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794
  1. using OxyPlot;
  2. using OxyPlot.Axes;
  3. using OxyPlot.Legends;
  4. using OxyPlot.Series;
  5. using Prism.Commands;
  6. using Prism.Events;
  7. using Prism.Ioc;
  8. using Prism.Mvvm;
  9. using System;
  10. using System.Collections.Generic;
  11. using System.Collections.ObjectModel;
  12. using System.IO;
  13. using System.Linq;
  14. using System.Windows.Media;
  15. using TeamAAS_VP.Enums;
  16. using TeamAAS_VP.Events;
  17. using TeamAAS_VP.Core;
  18. using TeamAAS_VP.Models;
  19. using TeamAAS_VP;
  20. using MaterialDesignThemes.Wpf;
  21. using TeamAAS_VP.ViewModels.DebugMod;
  22. using TeamAAS_VP.Views.DebugMod;
  23. using TeamAAS_VP.Controls;
  24. using TeamAAS_VP.Data;
  25. using System.Threading.Tasks;
  26. using NPOI.SS.Formula.Functions;
  27. using System.Windows;
  28. using TeamAAS_VP.Resources.Languages;
  29. using OxyPlot.Wpf;
  30. namespace TeamAAS_VP.ViewModels
  31. {
  32. public class HomeViewModel : BindableBase
  33. {
  34. IEventAggregator _eventAggregator;
  35. IContainerProvider _container;
  36. #region 属性
  37. private ObservableCollection<TaskMessage> messageQueue=new ObservableCollection<TaskMessage>();
  38. /// <summary>
  39. /// 运行信息队列
  40. /// </summary>
  41. public ObservableCollection<TaskMessage> MessageQueue
  42. {
  43. get { return messageQueue; }
  44. set { SetProperty(ref messageQueue,value); }
  45. }
  46. private Management _management;
  47. public Management management
  48. {
  49. get { return _management; }
  50. set { SetProperty(ref _management, value); }
  51. }
  52. private double _CTTotalSeconds;
  53. public double CTTotalSeconds
  54. {
  55. get { return _CTTotalSeconds; }
  56. set { SetProperty(ref _CTTotalSeconds, value); }
  57. }
  58. private ProductModel _SelectProduct;
  59. public ProductModel SelectProduct
  60. {
  61. get { return _SelectProduct; }
  62. set { SetProperty(ref _SelectProduct,value); }
  63. }
  64. private bool CanLoad
  65. {
  66. get
  67. {
  68. if (SelectProduct!=null && IsLoadProduct)
  69. {
  70. return true;
  71. }
  72. else
  73. {
  74. return false;
  75. }
  76. }
  77. }
  78. private bool _IsLoadProduct = true;
  79. public bool IsLoadProduct
  80. {
  81. get { return _IsLoadProduct; }
  82. set { SetProperty(ref _IsLoadProduct, value); }
  83. }
  84. private bool _isCanExecute = true;
  85. public bool isCanExecute
  86. {
  87. get { return _isCanExecute; }
  88. set { SetProperty(ref _isCanExecute, value); }
  89. }
  90. private PlotModel _LockCurvePlotModel;
  91. /// <summary>
  92. /// 锁付曲线
  93. /// </summary>
  94. public PlotModel LockCurvePlotModel
  95. {
  96. get { return _LockCurvePlotModel; }
  97. set { SetProperty(ref _LockCurvePlotModel, value); }
  98. }
  99. private ObservableCollection<LockResult> _LockResults = new ObservableCollection<LockResult>();
  100. public ObservableCollection<LockResult> LockResults { get => _LockResults; set => _LockResults = value ; }
  101. // 曲线 Series 索引常量,避免魔法数字
  102. private const int TorqueSeriesIndex = 0;
  103. private const int UpperLimitSeriesIndex = 1;
  104. private const int LowerLimitSeriesIndex = 2;
  105. private float _torqueUpperLimit = 1.0f;
  106. private float _torqueLowerLimit = 0.5f;
  107. // 锁付结果列表序号计数
  108. private int _lockResultCounter;
  109. private const int MaxLockResultCount = 100;
  110. // 实时曲线刷新状态
  111. private bool _isLiveLockCurve;
  112. private int _lastPlottedPointCount;
  113. private List<WaveData> _pendingWaveDatas;
  114. private bool _curveUpdateScheduled;
  115. private double _liveLockTorque;
  116. /// <summary>实时锁付扭矩</summary>
  117. public double LiveLockTorque
  118. {
  119. get { return _liveLockTorque; }
  120. set { SetProperty(ref _liveLockTorque, value); }
  121. }
  122. private double _liveLockTurns;
  123. /// <summary>实时锁付圈数</summary>
  124. public double LiveLockTurns
  125. {
  126. get { return _liveLockTurns; }
  127. set { SetProperty(ref _liveLockTurns, value); }
  128. }
  129. #endregion
  130. #region 命令
  131. public DelegateCommand LoadedCommand { get; set; }
  132. private DelegateCommand<ProductModel> _LoadProductCommand;
  133. public DelegateCommand<ProductModel> LoadProductCommand =>
  134. _LoadProductCommand ?? (_LoadProductCommand = new DelegateCommand<ProductModel>(ExecuteLoadProductCommand).ObservesCanExecute(()=> CanLoad).ObservesProperty(()=> SelectProduct).ObservesProperty(()=> IsLoadProduct));
  135. private DelegateCommand _ResetCounterCommand;
  136. public DelegateCommand ResetCounterCommand =>
  137. _ResetCounterCommand ?? (_ResetCounterCommand = new DelegateCommand(ExecuteResetCounterCommand).ObservesCanExecute(() => DoCondition).ObservesProperty(()=>isCanExecute));
  138. private bool DoCondition
  139. {
  140. get { return isCanExecute; }
  141. }
  142. private DelegateCommand<string> _CopyCommand;
  143. public DelegateCommand<string> CopyCommand =>
  144. _CopyCommand ?? (_CopyCommand = new DelegateCommand<string>(ExecuteCopyCommand));
  145. #endregion
  146. #region 事件
  147. #endregion
  148. public HomeViewModel(IEventAggregator ea, IContainerProvider container)
  149. {
  150. _eventAggregator = ea;
  151. _container = container;
  152. LoadedCommand = new DelegateCommand(OnLoad);
  153. _eventAggregator.GetEvent<TaskMessageNotification>().Subscribe(AddMessageInvoke);
  154. //订阅权限登录事件
  155. _eventAggregator.GetEvent<UserLoginNotification>().Subscribe(LoginChange);
  156. management = _container.Resolve<Management>();
  157. management.PropertyChanged += Management_PropertyChanged;
  158. // 订阅主站轮询推送的锁付事件
  159. _eventAggregator.GetEvent<LockFinishNotification>().Subscribe(OnLockFinish);
  160. _eventAggregator.GetEvent<LockCurveUpdateNotification>().Subscribe(OnLockCurveUpdate);
  161. }
  162. #region 方法
  163. private void OnLoad()
  164. {
  165. LoadTorqueLimitsFromConfig();
  166. InitLockCurvePlotModel();
  167. LoadDemoLockResultIfNeeded();
  168. }
  169. /// <summary>
  170. /// 从主站配置读取扭矩上下限(用于曲线参考线)
  171. /// </summary>
  172. private void LoadTorqueLimitsFromConfig()
  173. {
  174. var masterConfig = management.ModbusTcpMasterConfig;
  175. if (masterConfig == null)
  176. {
  177. return;
  178. }
  179. _torqueUpperLimit = masterConfig.TorqueUpperLimit;
  180. _torqueLowerLimit = masterConfig.TorqueLowerLimit;
  181. }
  182. async void ExecuteLoadProductCommand(ProductModel product)
  183. {
  184. if (product == null) return;
  185. var view = new ShowMessage(Lang.是否加载产品, $"{Lang.加载产品}:{product.Name}?");
  186. //show the dialog
  187. var result = await DialogHost.Show(view, "RootDialog", null, null, null);
  188. if (result != null && result is bool)
  189. {
  190. if (((bool)result))
  191. {
  192. IsLoadProduct = false;
  193. management.LoadProducts(product);
  194. DatabaseHelper.AddLoadProductRecord(product.ID, product.Name, false);
  195. IsLoadProduct = true;
  196. }
  197. }
  198. }
  199. void ExecuteCopyCommand(string textToCopy)
  200. {
  201. Clipboard.SetText(textToCopy);
  202. }
  203. /// <summary>
  204. /// 添加消息至消息队列
  205. /// </summary>
  206. /// <param name="msg"></param>
  207. /// <param name="color"></param>
  208. public void AddMessage(string msg, Color color)
  209. {
  210. App.Current.Dispatcher.BeginInvoke(new Action(() => {
  211. try
  212. {
  213. MessageQueue.Add(new TaskMessage()
  214. {
  215. DT = DateTime.Now,
  216. Message = msg,
  217. FontColol = new SolidColorBrush(color)
  218. });
  219. LogHelper.WriteLogInfo(msg);
  220. }
  221. catch (Exception)
  222. {
  223. }
  224. }));
  225. }
  226. /// <summary>
  227. /// 添加消息至消息队列
  228. /// </summary>
  229. /// <param name="msg"></param>
  230. /// <param name="color"></param>
  231. public void AddMessage(string msg, MessageLevel level)
  232. {
  233. MessageStruct messageStruct = new MessageStruct() { Message = msg, level = level };
  234. App.Current.Dispatcher.BeginInvoke(new Action(() => {
  235. AddMessage(messageStruct);
  236. }));
  237. }
  238. /// <summary>
  239. /// 添加消息至消息队列
  240. /// </summary>
  241. /// <param name="messageStruct"></param>
  242. public void AddMessage(MessageStruct messageStruct)
  243. {
  244. try
  245. {
  246. if (MessageQueue.Count >= 20)
  247. {
  248. MessageQueue.RemoveAt(0);// 从队列头部移除最早的消息
  249. }
  250. Color color = Colors.Black;
  251. switch (messageStruct.level)
  252. {
  253. case MessageLevel.Info:
  254. color = Colors.Black;
  255. break;
  256. case MessageLevel.Debug:
  257. color = Colors.Green;
  258. break;
  259. case MessageLevel.Alarm:
  260. color = Colors.Orange;
  261. break;
  262. case MessageLevel.Error:
  263. color = Colors.Red;
  264. break;
  265. }
  266. MessageQueue.Add(new TaskMessage()
  267. {
  268. DT = DateTime.Now,
  269. Message = messageStruct.Message,
  270. FontColol = new SolidColorBrush(color)
  271. });
  272. LogHelper.WriteLogInfo(messageStruct.Message);
  273. }
  274. catch (Exception)
  275. {
  276. }
  277. }
  278. public void AddMessageInvoke(MessageStruct messageStruct)
  279. {
  280. App.Current.Dispatcher.BeginInvoke(new Action(() => {
  281. AddMessage(messageStruct);
  282. }));
  283. }
  284. private void LoginChange(Models.User user)
  285. {
  286. }
  287. private void Management_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
  288. {
  289. if (e.PropertyName=="CT")
  290. {
  291. CTTotalSeconds = management.CT.TotalSeconds;
  292. }
  293. }
  294. /// <summary>
  295. /// 重置计数
  296. /// </summary>
  297. async void ExecuteResetCounterCommand()
  298. {
  299. if (management.CurrentProduct == null) return;
  300. var view = new ShowMessage(Lang.计数清零, Lang.是否重置当前产品的计数);
  301. //show the dialog
  302. var result = await DialogHost.Show(view, "RootDialog", null, null, null);
  303. if (result != null && result is bool)
  304. {
  305. if (((bool)result))
  306. {
  307. isCanExecute = false;
  308. DatabaseHelper.ResetCounter(management.CurrentProduct.Name);
  309. isCanExecute = true;
  310. }
  311. }
  312. }
  313. /// <summary>
  314. /// 初始化锁付曲线图(实际扭矩 + 上下限参考线)
  315. /// </summary>
  316. private void InitLockCurvePlotModel()
  317. {
  318. var model = new PlotModel
  319. {
  320. Title = "锁付曲线",
  321. TitleFontSize = 16
  322. };
  323. var legend = new Legend
  324. {
  325. LegendPosition = LegendPosition.TopRight,
  326. LegendPlacement = LegendPlacement.Outside,
  327. LegendOrientation = LegendOrientation.Vertical,
  328. LegendBorderThickness = 0
  329. };
  330. model.Legends.Add(legend);
  331. // X 轴:锁付圈数
  332. model.Axes.Add(new LinearAxis
  333. {
  334. Key = "X",
  335. Position = AxisPosition.Bottom,
  336. Title = "锁付圈数",
  337. Minimum = 0,
  338. MajorGridlineStyle = LineStyle.Solid,
  339. MinorGridlineStyle = LineStyle.Dot
  340. });
  341. // Y 轴:扭矩
  342. model.Axes.Add(new LinearAxis
  343. {
  344. Key = "Y",
  345. Position = AxisPosition.Left,
  346. Title = "力矩kgf",
  347. MajorGridlineStyle = LineStyle.Solid,
  348. MinorGridlineStyle = LineStyle.Dot
  349. });
  350. // 实际锁付扭矩曲线
  351. model.Series.Add(new LineSeries
  352. {
  353. Title = "锁付力矩曲线",
  354. StrokeThickness = 2,
  355. MarkerSize = 3,
  356. MarkerType = MarkerType.Circle,
  357. CanTrackerInterpolatePoints = false,
  358. Color = OxyColors.Blue
  359. });
  360. // 扭矩上限参考线
  361. model.Series.Add(new LineSeries
  362. {
  363. Title = "扭矩上限",
  364. StrokeThickness = 2,
  365. LineStyle = LineStyle.Dash,
  366. Color = OxyColors.Red
  367. });
  368. // 扭矩下限参考线
  369. model.Series.Add(new LineSeries
  370. {
  371. Title = "扭矩下限",
  372. StrokeThickness = 2,
  373. LineStyle = LineStyle.Dash,
  374. Color = OxyColors.Green
  375. });
  376. LockCurvePlotModel = model;
  377. }
  378. /// <summary>
  379. /// 首次进入首页且无真实数据时,加载一条演示结果与曲线
  380. /// </summary>
  381. private void LoadDemoLockResultIfNeeded()
  382. {
  383. if (LockResults.Count > 0)
  384. {
  385. return;
  386. }
  387. var demoResult = CreateDemoLockResult();
  388. LockResults.Add(demoResult);
  389. _isLiveLockCurve = false;
  390. _lastPlottedPointCount = 0;
  391. RefreshLockCurvePlot(demoResult.WaveDatas, incremental: false);
  392. UpdateLiveLockValues(demoResult.WaveDatas);
  393. }
  394. /// <summary>
  395. /// 生成模拟锁付曲线与结果(仅用于界面展示)
  396. /// </summary>
  397. private LockResult CreateDemoLockResult()
  398. {
  399. var waveDatas = new List<WaveData>();
  400. double turns = 0;
  401. double timeSec = 0;
  402. const double intervalSec = 0.05;
  403. const int pointCount = 80;
  404. double peakTorque = 0;
  405. for (int i = 0; i < pointCount; i++)
  406. {
  407. double progress = (double)i / (pointCount - 1);
  408. double torque;
  409. if (progress < 0.35)
  410. {
  411. torque = _torqueLowerLimit * 0.4 + progress * (_torqueLowerLimit * 0.8);
  412. }
  413. else if (progress < 0.75)
  414. {
  415. torque = _torqueLowerLimit + (progress - 0.35) / 0.4 * (_torqueUpperLimit - _torqueLowerLimit);
  416. }
  417. else
  418. {
  419. torque = _torqueUpperLimit * 0.96 + Math.Sin(i * 0.25) * (_torqueUpperLimit * 0.02);
  420. }
  421. peakTorque = Math.Max(peakTorque, torque);
  422. turns += 0.07;
  423. timeSec += intervalSec;
  424. waveDatas.Add(new WaveData
  425. {
  426. Torque = torque,
  427. Turns = turns,
  428. Time = timeSec,
  429. Speed = 110,
  430. LockAngle = turns * 360,
  431. Slopes = 0
  432. });
  433. }
  434. _lockResultCounter = 1;
  435. return new LockResult
  436. {
  437. Number = 1,
  438. Timestamp = DateTime.Now,
  439. LockTurns = (float)turns,
  440. LockTorque = (float)peakTorque,
  441. LockDuration = (float)(timeSec * 1000),
  442. LockPassed = true,
  443. ScrewdriverProgramNumber = 1,
  444. WaveDatas = waveDatas,
  445. IsDemoData = true
  446. };
  447. }
  448. /// <summary>
  449. /// 移除演示数据,避免与真实锁付结果混显
  450. /// </summary>
  451. private void RemoveDemoLockResults()
  452. {
  453. for (int i = LockResults.Count - 1; i >= 0; i--)
  454. {
  455. if (LockResults[i].IsDemoData)
  456. {
  457. LockResults.RemoveAt(i);
  458. }
  459. }
  460. if (LockResults.Count == 0)
  461. {
  462. _lockResultCounter = 0;
  463. }
  464. }
  465. /// <summary>
  466. /// 锁付过程中实时刷新曲线(合并 UI 刷新,增量追加点)
  467. /// </summary>
  468. private void OnLockCurveUpdate(System.Collections.Generic.List<WaveData> waveDatas)
  469. {
  470. if (waveDatas == null || waveDatas.Count == 0)
  471. {
  472. return;
  473. }
  474. _pendingWaveDatas = waveDatas;
  475. if (_curveUpdateScheduled)
  476. {
  477. return;
  478. }
  479. _curveUpdateScheduled = true;
  480. App.Current.Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Render, new Action(() =>
  481. {
  482. _curveUpdateScheduled = false;
  483. var latestWaveDatas = _pendingWaveDatas;
  484. if (latestWaveDatas == null || latestWaveDatas.Count == 0)
  485. {
  486. return;
  487. }
  488. RemoveDemoLockResults();
  489. _isLiveLockCurve = true;
  490. // 新一轮锁付从第 1 个点开始时,清掉上一轮曲线缓存
  491. if (latestWaveDatas.Count <= 1)
  492. {
  493. _lastPlottedPointCount = 0;
  494. }
  495. RefreshLockCurvePlot(latestWaveDatas, incremental: true);
  496. UpdateLiveLockValues(latestWaveDatas);
  497. }));
  498. }
  499. /// <summary>
  500. /// 螺丝锁付完成时调用:写入结果列表、更新曲线、导出图片
  501. /// </summary>
  502. private void OnLockFinish(LockResult result)
  503. {
  504. App.Current.Dispatcher.BeginInvoke(new Action(() =>
  505. {
  506. try
  507. {
  508. RemoveDemoLockResults();
  509. FillLockResultDisplayInfo(result);
  510. LockResults.Insert(0, result);
  511. TrimLockResults();
  512. _isLiveLockCurve = false;
  513. _lastPlottedPointCount = 0;
  514. RefreshLockCurvePlot(result.WaveDatas, incremental: false);
  515. UpdateLiveLockValues(result.WaveDatas);
  516. ExportLockCurveImageIfNeeded(result.LockCurveImagePath);
  517. PersistLockResultData(result);
  518. string statusText = result.LockPassed ? "OK" : "NG";
  519. AddMessage($"锁付完成 [{statusText}] 扭矩:{result.LockTorque:F3} 圈数:{result.LockTurns:F1} 程序:{result.ScrewdriverProgramNumber}",
  520. result.LockPassed ? MessageLevel.Info : MessageLevel.Alarm);
  521. }
  522. catch (Exception ex)
  523. {
  524. LogHelper.WriteLogError("更新锁付结果与曲线图时出错!", ex);
  525. }
  526. }));
  527. }
  528. /// <summary>
  529. /// 填充界面显示序号
  530. /// </summary>
  531. private void FillLockResultDisplayInfo(LockResult result)
  532. {
  533. _lockResultCounter++;
  534. result.Number = _lockResultCounter;
  535. }
  536. /// <summary>
  537. /// 限制锁付结果列表最大条数,避免内存持续增长
  538. /// </summary>
  539. private void TrimLockResults()
  540. {
  541. while (LockResults.Count > MaxLockResultCount)
  542. {
  543. LockResults.RemoveAt(LockResults.Count - 1);
  544. }
  545. }
  546. /// <summary>
  547. /// 更新实时扭矩/圈数显示(取曲线最新点)
  548. /// </summary>
  549. private void UpdateLiveLockValues(System.Collections.Generic.IList<WaveData> waveDatas)
  550. {
  551. if (waveDatas == null || waveDatas.Count == 0)
  552. {
  553. LiveLockTorque = 0;
  554. LiveLockTurns = 0;
  555. return;
  556. }
  557. var latest = waveDatas[waveDatas.Count - 1];
  558. LiveLockTorque = latest.Torque;
  559. LiveLockTurns = latest.Turns;
  560. }
  561. /// <summary>
  562. /// 刷新锁付曲线(实时增量 / 完成全量)
  563. /// </summary>
  564. private void RefreshLockCurvePlot(System.Collections.Generic.IList<WaveData> waveDatas, bool incremental)
  565. {
  566. if (LockCurvePlotModel == null || waveDatas == null || waveDatas.Count == 0)
  567. {
  568. return;
  569. }
  570. var torqueSeries = LockCurvePlotModel.Series[TorqueSeriesIndex] as LineSeries;
  571. var upperSeries = LockCurvePlotModel.Series[UpperLimitSeriesIndex] as LineSeries;
  572. var lowerSeries = LockCurvePlotModel.Series[LowerLimitSeriesIndex] as LineSeries;
  573. if (torqueSeries == null || upperSeries == null || lowerSeries == null)
  574. {
  575. return;
  576. }
  577. bool canIncremental = incremental
  578. && _isLiveLockCurve
  579. && _lastPlottedPointCount > 0
  580. && waveDatas.Count >= _lastPlottedPointCount;
  581. if (!canIncremental)
  582. {
  583. torqueSeries.Points.Clear();
  584. upperSeries.Points.Clear();
  585. lowerSeries.Points.Clear();
  586. _lastPlottedPointCount = 0;
  587. }
  588. int startIndex = canIncremental ? _lastPlottedPointCount : 0;
  589. for (int i = startIndex; i < waveDatas.Count; i++)
  590. {
  591. var waveData = waveDatas[i];
  592. torqueSeries.Points.Add(new DataPoint(GetPlotX(waveData), waveData.Torque));
  593. }
  594. _lastPlottedPointCount = waveDatas.Count;
  595. UpdateLockCurveReferenceLines(torqueSeries, upperSeries, lowerSeries);
  596. UpdateLockCurveAxisTitle();
  597. var xAxis = LockCurvePlotModel.GetAxis("X");
  598. var yAxis = LockCurvePlotModel.GetAxis("Y");
  599. xAxis?.Reset();
  600. yAxis?.Reset();
  601. LockCurvePlotModel.InvalidatePlot(true);
  602. }
  603. /// <summary>
  604. /// 实时阶段用时间(s)作 X 轴(圈数可能长时间不变);完成后用圈数
  605. /// </summary>
  606. private double GetPlotX(WaveData waveData)
  607. {
  608. if (_isLiveLockCurve)
  609. {
  610. return waveData.Time;
  611. }
  612. return waveData.Turns;
  613. }
  614. /// <summary>
  615. /// 更新扭矩上下限参考线
  616. /// </summary>
  617. private void UpdateLockCurveReferenceLines(LineSeries torqueSeries, LineSeries upperSeries, LineSeries lowerSeries)
  618. {
  619. upperSeries.Points.Clear();
  620. lowerSeries.Points.Clear();
  621. if (torqueSeries.Points.Count == 0)
  622. {
  623. return;
  624. }
  625. double minX = torqueSeries.Points.Min(p => p.X);
  626. double maxX = torqueSeries.Points.Max(p => p.X);
  627. if (Math.Abs(maxX - minX) < 1e-6)
  628. {
  629. maxX = minX + (_isLiveLockCurve ? 0.1 : 0.01);
  630. }
  631. upperSeries.Points.Add(new DataPoint(minX, _torqueUpperLimit));
  632. upperSeries.Points.Add(new DataPoint(maxX, _torqueUpperLimit));
  633. lowerSeries.Points.Add(new DataPoint(minX, _torqueLowerLimit));
  634. lowerSeries.Points.Add(new DataPoint(maxX, _torqueLowerLimit));
  635. }
  636. /// <summary>
  637. /// 根据实时/完成状态切换 X 轴标题
  638. /// </summary>
  639. private void UpdateLockCurveAxisTitle()
  640. {
  641. var xAxis = LockCurvePlotModel.GetAxis("X") as LinearAxis;
  642. if (xAxis == null)
  643. {
  644. return;
  645. }
  646. xAxis.Title = _isLiveLockCurve ? "时间(s)" : "锁付圈数";
  647. }
  648. /// <summary>
  649. /// 导出锁付 CSV(结果摘要 + 曲线流)并写入数据库
  650. /// </summary>
  651. private void PersistLockResultData(LockResult result)
  652. {
  653. if (result.IsDemoData)
  654. {
  655. return;
  656. }
  657. Task.Run(async () =>
  658. {
  659. try
  660. {
  661. if (!string.IsNullOrEmpty(result.CurveCsvPath))
  662. {
  663. string csvDir = Path.GetDirectoryName(result.CurveCsvPath);
  664. if (!string.IsNullOrEmpty(csvDir) && !Directory.Exists(csvDir))
  665. {
  666. Directory.CreateDirectory(csvDir);
  667. }
  668. await result.SaveToCsvAsync(result.CurveCsvPath);
  669. }
  670. string productName = management?.CurrentProduct?.Name;
  671. DatabaseHelper.AddLockResultRecord(result, result.CurveCsvPath, result.LockCurveImagePath, productName);
  672. }
  673. catch (Exception ex)
  674. {
  675. LogHelper.WriteLogError("持久化锁付数据时出错!", ex);
  676. }
  677. });
  678. }
  679. /// <summary>
  680. /// 导出锁付曲线 PNG 图片
  681. /// </summary>
  682. private void ExportLockCurveImageIfNeeded(string imagePath)
  683. {
  684. if (string.IsNullOrEmpty(imagePath))
  685. {
  686. return;
  687. }
  688. PngExporter.Export(
  689. LockCurvePlotModel,
  690. imagePath,
  691. (int)LockCurvePlotModel.Width,
  692. (int)LockCurvePlotModel.Height);
  693. }
  694. #endregion
  695. }
  696. }