using OxyPlot; using OxyPlot.Axes; using OxyPlot.Legends; using OxyPlot.Series; using Prism.Commands; using Prism.Events; using Prism.Ioc; using Prism.Mvvm; using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.IO; using System.Linq; using System.Windows.Media; using TeamAAS_VP.Enums; using TeamAAS_VP.Events; using TeamAAS_VP.Core; using TeamAAS_VP.Models; using TeamAAS_VP; using MaterialDesignThemes.Wpf; using TeamAAS_VP.ViewModels.DebugMod; using TeamAAS_VP.Views.DebugMod; using TeamAAS_VP.Controls; using TeamAAS_VP.Data; using System.Threading.Tasks; using NPOI.SS.Formula.Functions; using System.Windows; using TeamAAS_VP.Resources.Languages; using OxyPlot.Wpf; namespace TeamAAS_VP.ViewModels { public class HomeViewModel : BindableBase { IEventAggregator _eventAggregator; IContainerProvider _container; #region 属性 private ObservableCollection messageQueue=new ObservableCollection(); /// /// 运行信息队列 /// public ObservableCollection MessageQueue { get { return messageQueue; } set { SetProperty(ref messageQueue,value); } } private Management _management; public Management management { get { return _management; } set { SetProperty(ref _management, value); } } private double _CTTotalSeconds; public double CTTotalSeconds { get { return _CTTotalSeconds; } set { SetProperty(ref _CTTotalSeconds, value); } } private ProductModel _SelectProduct; public ProductModel SelectProduct { get { return _SelectProduct; } set { SetProperty(ref _SelectProduct,value); } } private bool CanLoad { get { if (SelectProduct!=null && IsLoadProduct) { return true; } else { return false; } } } private bool _IsLoadProduct = true; public bool IsLoadProduct { get { return _IsLoadProduct; } set { SetProperty(ref _IsLoadProduct, value); } } private bool _isCanExecute = true; public bool isCanExecute { get { return _isCanExecute; } set { SetProperty(ref _isCanExecute, value); } } private PlotModel _LockCurvePlotModel; /// /// 锁付曲线 /// public PlotModel LockCurvePlotModel { get { return _LockCurvePlotModel; } set { SetProperty(ref _LockCurvePlotModel, value); } } private ObservableCollection _LockResults = new ObservableCollection(); public ObservableCollection LockResults { get => _LockResults; set => _LockResults = value ; } // 曲线 Series 索引常量,避免魔法数字 private const int TorqueSeriesIndex = 0; private const int UpperLimitSeriesIndex = 1; private const int LowerLimitSeriesIndex = 2; private float _torqueUpperLimit = 1.0f; private float _torqueLowerLimit = 0.5f; // 锁付结果列表序号计数 private int _lockResultCounter; private const int MaxLockResultCount = 100; // 实时曲线刷新状态 private bool _isLiveLockCurve; private int _lastPlottedPointCount; private List _pendingWaveDatas; private bool _curveUpdateScheduled; private double _liveLockTorque; /// 实时锁付扭矩 public double LiveLockTorque { get { return _liveLockTorque; } set { SetProperty(ref _liveLockTorque, value); } } private double _liveLockTurns; /// 实时锁付圈数 public double LiveLockTurns { get { return _liveLockTurns; } set { SetProperty(ref _liveLockTurns, value); } } #endregion #region 命令 public DelegateCommand LoadedCommand { get; set; } private DelegateCommand _LoadProductCommand; public DelegateCommand LoadProductCommand => _LoadProductCommand ?? (_LoadProductCommand = new DelegateCommand(ExecuteLoadProductCommand).ObservesCanExecute(()=> CanLoad).ObservesProperty(()=> SelectProduct).ObservesProperty(()=> IsLoadProduct)); private DelegateCommand _ResetCounterCommand; public DelegateCommand ResetCounterCommand => _ResetCounterCommand ?? (_ResetCounterCommand = new DelegateCommand(ExecuteResetCounterCommand).ObservesCanExecute(() => DoCondition).ObservesProperty(()=>isCanExecute)); private bool DoCondition { get { return isCanExecute; } } private DelegateCommand _CopyCommand; public DelegateCommand CopyCommand => _CopyCommand ?? (_CopyCommand = new DelegateCommand(ExecuteCopyCommand)); #endregion #region 事件 #endregion public HomeViewModel(IEventAggregator ea, IContainerProvider container) { _eventAggregator = ea; _container = container; LoadedCommand = new DelegateCommand(OnLoad); _eventAggregator.GetEvent().Subscribe(AddMessageInvoke); //订阅权限登录事件 _eventAggregator.GetEvent().Subscribe(LoginChange); management = _container.Resolve(); management.PropertyChanged += Management_PropertyChanged; // 订阅主站轮询推送的锁付事件 _eventAggregator.GetEvent().Subscribe(OnLockFinish); _eventAggregator.GetEvent().Subscribe(OnLockCurveUpdate); } #region 方法 private void OnLoad() { LoadTorqueLimitsFromConfig(); InitLockCurvePlotModel(); LoadDemoLockResultIfNeeded(); } /// /// 从主站配置读取扭矩上下限(用于曲线参考线) /// private void LoadTorqueLimitsFromConfig() { var masterConfig = management.ModbusTcpMasterConfig; if (masterConfig == null) { return; } _torqueUpperLimit = masterConfig.TorqueUpperLimit; _torqueLowerLimit = masterConfig.TorqueLowerLimit; } async void ExecuteLoadProductCommand(ProductModel product) { if (product == null) return; var view = new ShowMessage(Lang.是否加载产品, $"{Lang.加载产品}:{product.Name}?"); //show the dialog var result = await DialogHost.Show(view, "RootDialog", null, null, null); if (result != null && result is bool) { if (((bool)result)) { IsLoadProduct = false; management.LoadProducts(product); DatabaseHelper.AddLoadProductRecord(product.ID, product.Name, false); IsLoadProduct = true; } } } void ExecuteCopyCommand(string textToCopy) { Clipboard.SetText(textToCopy); } /// /// 添加消息至消息队列 /// /// /// public void AddMessage(string msg, Color color) { App.Current.Dispatcher.BeginInvoke(new Action(() => { try { MessageQueue.Add(new TaskMessage() { DT = DateTime.Now, Message = msg, FontColol = new SolidColorBrush(color) }); LogHelper.WriteLogInfo(msg); } catch (Exception) { } })); } /// /// 添加消息至消息队列 /// /// /// public void AddMessage(string msg, MessageLevel level) { MessageStruct messageStruct = new MessageStruct() { Message = msg, level = level }; App.Current.Dispatcher.BeginInvoke(new Action(() => { AddMessage(messageStruct); })); } /// /// 添加消息至消息队列 /// /// public void AddMessage(MessageStruct messageStruct) { try { if (MessageQueue.Count >= 20) { MessageQueue.RemoveAt(0);// 从队列头部移除最早的消息 } Color color = Colors.Black; switch (messageStruct.level) { case MessageLevel.Info: color = Colors.Black; break; case MessageLevel.Debug: color = Colors.Green; break; case MessageLevel.Alarm: color = Colors.Orange; break; case MessageLevel.Error: color = Colors.Red; break; } MessageQueue.Add(new TaskMessage() { DT = DateTime.Now, Message = messageStruct.Message, FontColol = new SolidColorBrush(color) }); LogHelper.WriteLogInfo(messageStruct.Message); } catch (Exception) { } } public void AddMessageInvoke(MessageStruct messageStruct) { App.Current.Dispatcher.BeginInvoke(new Action(() => { AddMessage(messageStruct); })); } private void LoginChange(Models.User user) { } private void Management_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) { if (e.PropertyName=="CT") { CTTotalSeconds = management.CT.TotalSeconds; } } /// /// 重置计数 /// async void ExecuteResetCounterCommand() { if (management.CurrentProduct == null) return; var view = new ShowMessage(Lang.计数清零, Lang.是否重置当前产品的计数); //show the dialog var result = await DialogHost.Show(view, "RootDialog", null, null, null); if (result != null && result is bool) { if (((bool)result)) { isCanExecute = false; DatabaseHelper.ResetCounter(management.CurrentProduct.Name); isCanExecute = true; } } } /// /// 初始化锁付曲线图(实际扭矩 + 上下限参考线) /// private void InitLockCurvePlotModel() { var model = new PlotModel { Title = "锁付曲线", TitleFontSize = 16 }; var legend = new Legend { LegendPosition = LegendPosition.TopRight, LegendPlacement = LegendPlacement.Outside, LegendOrientation = LegendOrientation.Vertical, LegendBorderThickness = 0 }; model.Legends.Add(legend); // X 轴:锁付圈数 model.Axes.Add(new LinearAxis { Key = "X", Position = AxisPosition.Bottom, Title = "锁付圈数", Minimum = 0, MajorGridlineStyle = LineStyle.Solid, MinorGridlineStyle = LineStyle.Dot }); // Y 轴:扭矩 model.Axes.Add(new LinearAxis { Key = "Y", Position = AxisPosition.Left, Title = "力矩kgf", MajorGridlineStyle = LineStyle.Solid, MinorGridlineStyle = LineStyle.Dot }); // 实际锁付扭矩曲线 model.Series.Add(new LineSeries { Title = "锁付力矩曲线", StrokeThickness = 2, MarkerSize = 3, MarkerType = MarkerType.Circle, CanTrackerInterpolatePoints = false, Color = OxyColors.Blue }); // 扭矩上限参考线 model.Series.Add(new LineSeries { Title = "扭矩上限", StrokeThickness = 2, LineStyle = LineStyle.Dash, Color = OxyColors.Red }); // 扭矩下限参考线 model.Series.Add(new LineSeries { Title = "扭矩下限", StrokeThickness = 2, LineStyle = LineStyle.Dash, Color = OxyColors.Green }); LockCurvePlotModel = model; } /// /// 首次进入首页且无真实数据时,加载一条演示结果与曲线 /// private void LoadDemoLockResultIfNeeded() { if (LockResults.Count > 0) { return; } var demoResult = CreateDemoLockResult(); LockResults.Add(demoResult); _isLiveLockCurve = false; _lastPlottedPointCount = 0; RefreshLockCurvePlot(demoResult.WaveDatas, incremental: false); UpdateLiveLockValues(demoResult.WaveDatas); } /// /// 生成模拟锁付曲线与结果(仅用于界面展示) /// private LockResult CreateDemoLockResult() { var waveDatas = new List(); double turns = 0; double timeSec = 0; const double intervalSec = 0.05; const int pointCount = 80; double peakTorque = 0; for (int i = 0; i < pointCount; i++) { double progress = (double)i / (pointCount - 1); double torque; if (progress < 0.35) { torque = _torqueLowerLimit * 0.4 + progress * (_torqueLowerLimit * 0.8); } else if (progress < 0.75) { torque = _torqueLowerLimit + (progress - 0.35) / 0.4 * (_torqueUpperLimit - _torqueLowerLimit); } else { torque = _torqueUpperLimit * 0.96 + Math.Sin(i * 0.25) * (_torqueUpperLimit * 0.02); } peakTorque = Math.Max(peakTorque, torque); turns += 0.07; timeSec += intervalSec; waveDatas.Add(new WaveData { Torque = torque, Turns = turns, Time = timeSec, Speed = 110, LockAngle = turns * 360, Slopes = 0 }); } _lockResultCounter = 1; return new LockResult { Number = 1, Timestamp = DateTime.Now, LockTurns = (float)turns, LockTorque = (float)peakTorque, LockDuration = (float)(timeSec * 1000), LockPassed = true, ScrewdriverProgramNumber = 1, WaveDatas = waveDatas, IsDemoData = true }; } /// /// 移除演示数据,避免与真实锁付结果混显 /// private void RemoveDemoLockResults() { for (int i = LockResults.Count - 1; i >= 0; i--) { if (LockResults[i].IsDemoData) { LockResults.RemoveAt(i); } } if (LockResults.Count == 0) { _lockResultCounter = 0; } } /// /// 锁付过程中实时刷新曲线(合并 UI 刷新,增量追加点) /// private void OnLockCurveUpdate(System.Collections.Generic.List waveDatas) { if (waveDatas == null || waveDatas.Count == 0) { return; } _pendingWaveDatas = waveDatas; if (_curveUpdateScheduled) { return; } _curveUpdateScheduled = true; App.Current.Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Render, new Action(() => { _curveUpdateScheduled = false; var latestWaveDatas = _pendingWaveDatas; if (latestWaveDatas == null || latestWaveDatas.Count == 0) { return; } RemoveDemoLockResults(); _isLiveLockCurve = true; // 新一轮锁付从第 1 个点开始时,清掉上一轮曲线缓存 if (latestWaveDatas.Count <= 1) { _lastPlottedPointCount = 0; } RefreshLockCurvePlot(latestWaveDatas, incremental: true); UpdateLiveLockValues(latestWaveDatas); })); } /// /// 螺丝锁付完成时调用:写入结果列表、更新曲线、导出图片 /// private void OnLockFinish(LockResult result) { App.Current.Dispatcher.BeginInvoke(new Action(() => { try { RemoveDemoLockResults(); FillLockResultDisplayInfo(result); LockResults.Insert(0, result); TrimLockResults(); _isLiveLockCurve = false; _lastPlottedPointCount = 0; RefreshLockCurvePlot(result.WaveDatas, incremental: false); UpdateLiveLockValues(result.WaveDatas); ExportLockCurveImageIfNeeded(result.LockCurveImagePath); PersistLockResultData(result); string statusText = result.LockPassed ? "OK" : "NG"; AddMessage($"锁付完成 [{statusText}] 扭矩:{result.LockTorque:F3} 圈数:{result.LockTurns:F1} 程序:{result.ScrewdriverProgramNumber}", result.LockPassed ? MessageLevel.Info : MessageLevel.Alarm); } catch (Exception ex) { LogHelper.WriteLogError("更新锁付结果与曲线图时出错!", ex); } })); } /// /// 填充界面显示序号 /// private void FillLockResultDisplayInfo(LockResult result) { _lockResultCounter++; result.Number = _lockResultCounter; } /// /// 限制锁付结果列表最大条数,避免内存持续增长 /// private void TrimLockResults() { while (LockResults.Count > MaxLockResultCount) { LockResults.RemoveAt(LockResults.Count - 1); } } /// /// 更新实时扭矩/圈数显示(取曲线最新点) /// private void UpdateLiveLockValues(System.Collections.Generic.IList waveDatas) { if (waveDatas == null || waveDatas.Count == 0) { LiveLockTorque = 0; LiveLockTurns = 0; return; } var latest = waveDatas[waveDatas.Count - 1]; LiveLockTorque = latest.Torque; LiveLockTurns = latest.Turns; } /// /// 刷新锁付曲线(实时增量 / 完成全量) /// private void RefreshLockCurvePlot(System.Collections.Generic.IList waveDatas, bool incremental) { if (LockCurvePlotModel == null || waveDatas == null || waveDatas.Count == 0) { return; } var torqueSeries = LockCurvePlotModel.Series[TorqueSeriesIndex] as LineSeries; var upperSeries = LockCurvePlotModel.Series[UpperLimitSeriesIndex] as LineSeries; var lowerSeries = LockCurvePlotModel.Series[LowerLimitSeriesIndex] as LineSeries; if (torqueSeries == null || upperSeries == null || lowerSeries == null) { return; } bool canIncremental = incremental && _isLiveLockCurve && _lastPlottedPointCount > 0 && waveDatas.Count >= _lastPlottedPointCount; if (!canIncremental) { torqueSeries.Points.Clear(); upperSeries.Points.Clear(); lowerSeries.Points.Clear(); _lastPlottedPointCount = 0; } int startIndex = canIncremental ? _lastPlottedPointCount : 0; for (int i = startIndex; i < waveDatas.Count; i++) { var waveData = waveDatas[i]; torqueSeries.Points.Add(new DataPoint(GetPlotX(waveData), waveData.Torque)); } _lastPlottedPointCount = waveDatas.Count; UpdateLockCurveReferenceLines(torqueSeries, upperSeries, lowerSeries); UpdateLockCurveAxisTitle(); var xAxis = LockCurvePlotModel.GetAxis("X"); var yAxis = LockCurvePlotModel.GetAxis("Y"); xAxis?.Reset(); yAxis?.Reset(); LockCurvePlotModel.InvalidatePlot(true); } /// /// 实时阶段用时间(s)作 X 轴(圈数可能长时间不变);完成后用圈数 /// private double GetPlotX(WaveData waveData) { if (_isLiveLockCurve) { return waveData.Time; } return waveData.Turns; } /// /// 更新扭矩上下限参考线 /// private void UpdateLockCurveReferenceLines(LineSeries torqueSeries, LineSeries upperSeries, LineSeries lowerSeries) { upperSeries.Points.Clear(); lowerSeries.Points.Clear(); if (torqueSeries.Points.Count == 0) { return; } double minX = torqueSeries.Points.Min(p => p.X); double maxX = torqueSeries.Points.Max(p => p.X); if (Math.Abs(maxX - minX) < 1e-6) { maxX = minX + (_isLiveLockCurve ? 0.1 : 0.01); } upperSeries.Points.Add(new DataPoint(minX, _torqueUpperLimit)); upperSeries.Points.Add(new DataPoint(maxX, _torqueUpperLimit)); lowerSeries.Points.Add(new DataPoint(minX, _torqueLowerLimit)); lowerSeries.Points.Add(new DataPoint(maxX, _torqueLowerLimit)); } /// /// 根据实时/完成状态切换 X 轴标题 /// private void UpdateLockCurveAxisTitle() { var xAxis = LockCurvePlotModel.GetAxis("X") as LinearAxis; if (xAxis == null) { return; } xAxis.Title = _isLiveLockCurve ? "时间(s)" : "锁付圈数"; } /// /// 导出锁付 CSV(结果摘要 + 曲线流)并写入数据库 /// private void PersistLockResultData(LockResult result) { if (result.IsDemoData) { return; } Task.Run(async () => { try { if (!string.IsNullOrEmpty(result.CurveCsvPath)) { string csvDir = Path.GetDirectoryName(result.CurveCsvPath); if (!string.IsNullOrEmpty(csvDir) && !Directory.Exists(csvDir)) { Directory.CreateDirectory(csvDir); } await result.SaveToCsvAsync(result.CurveCsvPath); } string productName = management?.CurrentProduct?.Name; DatabaseHelper.AddLockResultRecord(result, result.CurveCsvPath, result.LockCurveImagePath, productName); } catch (Exception ex) { LogHelper.WriteLogError("持久化锁付数据时出错!", ex); } }); } /// /// 导出锁付曲线 PNG 图片 /// private void ExportLockCurveImageIfNeeded(string imagePath) { if (string.IsNullOrEmpty(imagePath)) { return; } PngExporter.Export( LockCurvePlotModel, imagePath, (int)LockCurvePlotModel.Width, (int)LockCurvePlotModel.Height); } #endregion } }