457 lines
16 KiB
C#
457 lines
16 KiB
C#
|
|
using System;
|
|||
|
|
using System.Collections.Generic;
|
|||
|
|
using System.Linq;
|
|||
|
|
using System.Threading;
|
|||
|
|
using System.Threading.Tasks;
|
|||
|
|
using System.Windows;
|
|||
|
|
using System.Windows.Media;
|
|||
|
|
using System.Windows.Media.Imaging;
|
|||
|
|
using System.Windows.Threading;
|
|||
|
|
using Modbus.Device;
|
|||
|
|
using Modbus;
|
|||
|
|
using OxyPlot;
|
|||
|
|
using OxyPlot.Axes;
|
|||
|
|
using OxyPlot.Legends;
|
|||
|
|
using OxyPlot.Series;
|
|||
|
|
using System.Configuration;
|
|||
|
|
using System.Net.Sockets;
|
|||
|
|
using System.Windows.Interop;
|
|||
|
|
using 口罩泄露定制款;
|
|||
|
|
|
|||
|
|
namespace LineChartDemo
|
|||
|
|
{
|
|||
|
|
public partial class MainWindow : Window
|
|||
|
|
{
|
|||
|
|
#region 图表相关变量
|
|||
|
|
private PlotModel _plotModel;
|
|||
|
|
private LineSeries _exhaleSeries;
|
|||
|
|
private LineSeries _inhaleSeries;
|
|||
|
|
private List<DataPoint> _exhaleData = new List<DataPoint>();
|
|||
|
|
private List<DataPoint> _inhaleData = new List<DataPoint>();
|
|||
|
|
private DispatcherTimer _dataTimer;
|
|||
|
|
private double _timeCounter = 0;
|
|||
|
|
private const int MAX_DATA_POINTS = 30; // 最多显示30个数据点
|
|||
|
|
private Function ma;
|
|||
|
|
private DataChange c = new DataChange();
|
|||
|
|
|
|||
|
|
// Modbus通信
|
|||
|
|
private TcpClient _tcpClient;
|
|||
|
|
private IModbusMaster _modbusMaster;
|
|||
|
|
|
|||
|
|
// 坐标轴引用
|
|||
|
|
private LinearAxis _xAxis;
|
|||
|
|
private LinearAxis _yAxis;
|
|||
|
|
|
|||
|
|
// 性能优化相关
|
|||
|
|
private bool _isResizing = false;
|
|||
|
|
private readonly object _lockObj = new object();
|
|||
|
|
private CancellationTokenSource _resizeTokenSource;
|
|||
|
|
#endregion
|
|||
|
|
|
|||
|
|
#region Modbus相关配置
|
|||
|
|
private readonly ushort _OutBreathAddress = 0x1398; // D5016
|
|||
|
|
private readonly ushort _InBreathAddress = 0x1396; // D5014
|
|||
|
|
#endregion
|
|||
|
|
|
|||
|
|
public MainWindow()
|
|||
|
|
{
|
|||
|
|
InitializeComponent();
|
|||
|
|
RenderOptions.ProcessRenderMode = RenderMode.Default;
|
|||
|
|
InitializeModbusTcp();
|
|||
|
|
InitializePlot();
|
|||
|
|
InitializeEvents();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#region 窗口大小变化事件
|
|||
|
|
protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo)
|
|||
|
|
{
|
|||
|
|
base.OnRenderSizeChanged(sizeInfo);
|
|||
|
|
|
|||
|
|
if (!_isResizing)
|
|||
|
|
{
|
|||
|
|
_isResizing = true;
|
|||
|
|
_dataTimer?.Stop();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
_resizeTokenSource?.Cancel();
|
|||
|
|
_resizeTokenSource = new CancellationTokenSource();
|
|||
|
|
var token = _resizeTokenSource.Token;
|
|||
|
|
|
|||
|
|
Task.Delay(50, token).ContinueWith(_ =>
|
|||
|
|
{
|
|||
|
|
Dispatcher.Invoke(() =>
|
|||
|
|
{
|
|||
|
|
if (!token.IsCancellationRequested)
|
|||
|
|
{
|
|||
|
|
_plotModel?.InvalidatePlot(true);
|
|||
|
|
_isResizing = false;
|
|||
|
|
if (StopBtn.IsEnabled)
|
|||
|
|
{
|
|||
|
|
_dataTimer?.Start();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
}, token);
|
|||
|
|
}
|
|||
|
|
#endregion
|
|||
|
|
|
|||
|
|
#region Modbus初始化
|
|||
|
|
private void InitializeModbusTcp()
|
|||
|
|
{
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
string plcIp = ConfigurationManager.AppSettings["PLC2_IP"];
|
|||
|
|
int plcPort = int.Parse(ConfigurationManager.AppSettings["PLC2_Port"] ?? "502");
|
|||
|
|
|
|||
|
|
_tcpClient = new TcpClient();
|
|||
|
|
var connectResult = _tcpClient.BeginConnect(plcIp, plcPort, null, null);
|
|||
|
|
var success = connectResult.AsyncWaitHandle.WaitOne(3000);
|
|||
|
|
|
|||
|
|
if (!success || !_tcpClient.Connected)
|
|||
|
|
throw new Exception("连接PLC超时,请检查IP和端口");
|
|||
|
|
|
|||
|
|
_modbusMaster = ModbusIpMaster.CreateIp(_tcpClient);
|
|||
|
|
_modbusMaster.Transport.ReadTimeout = 1000;
|
|||
|
|
_modbusMaster.Transport.WriteTimeout = 1000;
|
|||
|
|
|
|||
|
|
ma = new Function(_modbusMaster);
|
|||
|
|
StatusText.Text = "状态:Modbus连接成功";
|
|||
|
|
StatusText.Foreground = new SolidColorBrush(Color.FromRgb(76, 175, 80));
|
|||
|
|
}
|
|||
|
|
catch (Exception ex)
|
|||
|
|
{
|
|||
|
|
ShowError($"Modbus初始化失败: {ex.Message}");
|
|||
|
|
StatusText.Text = "状态:连接失败";
|
|||
|
|
StatusText.Foreground = new SolidColorBrush(Color.FromRgb(239, 83, 80));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private void ShowError(string message) => MessageBox.Show(message, "错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
|||
|
|
#endregion
|
|||
|
|
|
|||
|
|
#region 图表初始化
|
|||
|
|
private void InitializePlot()
|
|||
|
|
{
|
|||
|
|
_plotModel = new PlotModel
|
|||
|
|
{
|
|||
|
|
Title = "呼气流量与吸气流量监测(最新30个数据)",
|
|||
|
|
TitleFontSize = 18,
|
|||
|
|
TitleFontWeight = OxyPlot.FontWeights.Bold,
|
|||
|
|
Background = OxyColors.White,
|
|||
|
|
PlotAreaBackground = OxyColor.FromRgb(250, 252, 255),
|
|||
|
|
//AnimationDuration = TimeSpan.Zero
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
var legend = new Legend
|
|||
|
|
{
|
|||
|
|
LegendPlacement = LegendPlacement.Outside,
|
|||
|
|
LegendPosition = LegendPosition.TopRight,
|
|||
|
|
FontSize = 14
|
|||
|
|
};
|
|||
|
|
_plotModel.Legends.Add(legend);
|
|||
|
|
|
|||
|
|
_exhaleSeries = new LineSeries
|
|||
|
|
{
|
|||
|
|
Title = "呼气流量 (L/min)",
|
|||
|
|
Color = OxyColor.FromRgb(239, 83, 80),
|
|||
|
|
StrokeThickness = 2,
|
|||
|
|
MarkerType = MarkerType.None,
|
|||
|
|
MarkerSize = 4
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
_inhaleSeries = new LineSeries
|
|||
|
|
{
|
|||
|
|
Title = "吸气流量 (L/min)",
|
|||
|
|
Color = OxyColor.FromRgb(66, 165, 245),
|
|||
|
|
StrokeThickness = 2,
|
|||
|
|
MarkerType = MarkerType.None,
|
|||
|
|
MarkerSize = 4
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
_plotModel.Series.Add(_exhaleSeries);
|
|||
|
|
_plotModel.Series.Add(_inhaleSeries);
|
|||
|
|
|
|||
|
|
_xAxis = new LinearAxis
|
|||
|
|
{
|
|||
|
|
Position = AxisPosition.Bottom,
|
|||
|
|
Title = "时间 (秒)",
|
|||
|
|
TitleFontSize = 14,
|
|||
|
|
AxislineColor = OxyColors.LightGray,
|
|||
|
|
MajorGridlineColor = OxyColor.FromRgb(220, 231, 243),
|
|||
|
|
MajorGridlineStyle = LineStyle.Solid,
|
|||
|
|
MinorGridlineStyle = LineStyle.None,
|
|||
|
|
FontSize = 12,
|
|||
|
|
Minimum = 0
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
_yAxis = new LinearAxis
|
|||
|
|
{
|
|||
|
|
Position = AxisPosition.Left,
|
|||
|
|
Title = "流量 (L/min)",
|
|||
|
|
TitleFontSize = 14,
|
|||
|
|
AxislineColor = OxyColors.LightGray,
|
|||
|
|
MajorGridlineColor = OxyColor.FromRgb(220, 231, 243),
|
|||
|
|
MajorGridlineStyle = LineStyle.Solid,
|
|||
|
|
MinorGridlineStyle = LineStyle.None,
|
|||
|
|
FontSize = 12,
|
|||
|
|
MajorStep = 5
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
_plotModel.Axes.Add(_xAxis);
|
|||
|
|
_plotModel.Axes.Add(_yAxis);
|
|||
|
|
|
|||
|
|
PlotView.Model = _plotModel;
|
|||
|
|
}
|
|||
|
|
#endregion
|
|||
|
|
|
|||
|
|
#region 事件与定时器初始化
|
|||
|
|
private void InitializeEvents()
|
|||
|
|
{
|
|||
|
|
_dataTimer = new DispatcherTimer
|
|||
|
|
{
|
|||
|
|
Interval = TimeSpan.FromMilliseconds(500)
|
|||
|
|
};
|
|||
|
|
_dataTimer.Tick += async (s, e) => await UpdateData();
|
|||
|
|
|
|||
|
|
StartBtn.Click += (s, e) =>
|
|||
|
|
{
|
|||
|
|
_dataTimer.Start();
|
|||
|
|
_ = UpdateData();
|
|||
|
|
StartBtn.IsEnabled = false;
|
|||
|
|
StopBtn.IsEnabled = true;
|
|||
|
|
StatusText.Text = "状态:正在监测";
|
|||
|
|
StatusText.Foreground = new SolidColorBrush(Color.FromRgb(76, 175, 80));
|
|||
|
|
_exhaleSeries.MarkerType = MarkerType.Circle;
|
|||
|
|
_inhaleSeries.MarkerType = MarkerType.Circle;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
StopBtn.Click += (s, e) =>
|
|||
|
|
{
|
|||
|
|
_dataTimer.Stop();
|
|||
|
|
StartBtn.IsEnabled = true;
|
|||
|
|
StopBtn.IsEnabled = false;
|
|||
|
|
StatusText.Text = "状态:已停止";
|
|||
|
|
StatusText.Foreground = new SolidColorBrush(Color.FromRgb(156, 39, 176));
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
ClearBtn.Click += (s, e) =>
|
|||
|
|
{
|
|||
|
|
lock (_lockObj)
|
|||
|
|
{
|
|||
|
|
_exhaleData.Clear();
|
|||
|
|
_inhaleData.Clear();
|
|||
|
|
_exhaleSeries.Points.Clear();
|
|||
|
|
_inhaleSeries.Points.Clear();
|
|||
|
|
_timeCounter = 0;
|
|||
|
|
AdjustYAxisRange();
|
|||
|
|
AdjustXAxisRange();
|
|||
|
|
_plotModel.InvalidatePlot(true);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
#endregion
|
|||
|
|
|
|||
|
|
#region 数据更新与处理(核心:限制20个数据点)
|
|||
|
|
private async Task UpdateData()
|
|||
|
|
{
|
|||
|
|
if (_isResizing) return;
|
|||
|
|
|
|||
|
|
if (!IsModbusConnected())
|
|||
|
|
{
|
|||
|
|
StatusText.Text = "状态:Modbus已断开";
|
|||
|
|
StatusText.Foreground = new SolidColorBrush(Color.FromRgb(239, 83, 80));
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
var (exhaleValue, inhaleValue) = await Task.Run(() => ReadModbusData(),
|
|||
|
|
CancellationToken.None);
|
|||
|
|
|
|||
|
|
lock (_lockObj)
|
|||
|
|
{
|
|||
|
|
// 添加新数据点
|
|||
|
|
var exhalePoint = new DataPoint(_timeCounter, exhaleValue);
|
|||
|
|
var inhalePoint = new DataPoint(_timeCounter, inhaleValue);
|
|||
|
|
_exhaleData.Add(exhalePoint);
|
|||
|
|
_inhaleData.Add(inhalePoint);
|
|||
|
|
|
|||
|
|
// 关键:只保留最新的30个数据点
|
|||
|
|
if (_exhaleData.Count > MAX_DATA_POINTS)
|
|||
|
|
{
|
|||
|
|
// 移除最旧的数据点(超出20个的部分)
|
|||
|
|
int removeCount = _exhaleData.Count - MAX_DATA_POINTS;
|
|||
|
|
_exhaleData.RemoveRange(0, removeCount);
|
|||
|
|
_inhaleData.RemoveRange(0, removeCount);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
_timeCounter += 0.5;
|
|||
|
|
|
|||
|
|
// 更新折线数据
|
|||
|
|
_exhaleSeries.Points.Clear();
|
|||
|
|
_inhaleSeries.Points.Clear();
|
|||
|
|
_exhaleData.ForEach(p => _exhaleSeries.Points.Add(p));
|
|||
|
|
_inhaleData.ForEach(p => _inhaleSeries.Points.Add(p));
|
|||
|
|
|
|||
|
|
// 调整坐标轴范围(基于当前20个数据)
|
|||
|
|
AdjustYAxisRange();
|
|||
|
|
AdjustXAxisRange();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
_plotModel.InvalidatePlot(false);
|
|||
|
|
}
|
|||
|
|
catch (Exception ex)
|
|||
|
|
{
|
|||
|
|
StatusText.Text = $"状态:数据错误 - {ex.Message}";
|
|||
|
|
StatusText.Foreground = new SolidColorBrush(Color.FromRgb(239, 83, 80));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private (float exhale, float inhale) ReadModbusData()
|
|||
|
|
{
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
ushort[] exhalationData = _modbusMaster?.ReadHoldingRegisters(0x01, _OutBreathAddress, 2);
|
|||
|
|
ushort[] inhalationData = _modbusMaster?.ReadHoldingRegisters(0x01, _InBreathAddress, 2);
|
|||
|
|
|
|||
|
|
float exhaleValue = c.UshortToFloat(exhalationData[1], exhalationData[0]);
|
|||
|
|
float inhaleValue = c.UshortToFloat(inhalationData[1], inhalationData[0]);
|
|||
|
|
|
|||
|
|
return ((float)Math.Round(exhaleValue, 4), (float)Math.Round(inhaleValue, 4));
|
|||
|
|
}
|
|||
|
|
catch (Exception ex)
|
|||
|
|
{
|
|||
|
|
throw new Exception($"Modbus读取错误:{ex.Message}");
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private void AdjustYAxisRange()
|
|||
|
|
{
|
|||
|
|
var allYValues = new List<double>();
|
|||
|
|
allYValues.AddRange(_exhaleData.Select(p => p.Y));
|
|||
|
|
allYValues.AddRange(_inhaleData.Select(p => p.Y));
|
|||
|
|
|
|||
|
|
if (!allYValues.Any())
|
|||
|
|
{
|
|||
|
|
// 无数据时默认范围(预留一定空间,避免后续数据突然增大时轴范围不足)
|
|||
|
|
_yAxis.Minimum = 0;
|
|||
|
|
_yAxis.Maximum = 10; // 初始值设大一些,应对可能的大数值
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
double currentMin = allYValues.Min();
|
|||
|
|
double currentMax = allYValues.Max();
|
|||
|
|
|
|||
|
|
// 计算当前数据的波动范围
|
|||
|
|
double dataRange = currentMax - currentMin;
|
|||
|
|
|
|||
|
|
// 1. 底部留固定余量(避免数据贴底)
|
|||
|
|
double bottomPadding = Math.Max(0.5, dataRange * 0.1); // 取两者较大值,确保至少0.5
|
|||
|
|
double newMin = currentMin - bottomPadding;
|
|||
|
|
// 若数据全为正数,Y轴从0开始更合理(可选,根据业务场景)
|
|||
|
|
if (newMin < 0 && currentMin >= 0)
|
|||
|
|
newMin = 0;
|
|||
|
|
|
|||
|
|
// 2. 顶部留动态余量(随数据增大自动扩展)
|
|||
|
|
double topPadding = Math.Max(1, dataRange * 0.2); // 顶部余量稍大,应对突发大值
|
|||
|
|
double newMax = currentMax + topPadding;
|
|||
|
|
|
|||
|
|
// 3. 确保Y轴范围不会“收缩”(避免数据忽大忽小时轴频繁跳动)
|
|||
|
|
// 仅当新范围的上限大于当前轴上限时,才更新上限
|
|||
|
|
if (newMax > _yAxis.Maximum)
|
|||
|
|
{
|
|||
|
|
_yAxis.Maximum = newMax;
|
|||
|
|
}
|
|||
|
|
// 下限可根据当前数据动态收缩(但不小于0,若数据全为正)
|
|||
|
|
_yAxis.Minimum = newMin;
|
|||
|
|
|
|||
|
|
// 强制设置轴的“弹性”,允许数据超出当前范围时自动扩展(OxyPlot特性)
|
|||
|
|
_yAxis.IsZoomEnabled = false; // 禁止用户手动缩放(避免干扰自动范围)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private void AdjustXAxisRange()
|
|||
|
|
{
|
|||
|
|
if (_exhaleData.Count == 0 && _inhaleData.Count == 0)
|
|||
|
|
{
|
|||
|
|
_xAxis.Maximum = 1;
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// X轴范围基于最新20个数据的时间范围
|
|||
|
|
double maxX = Math.Max(
|
|||
|
|
_exhaleData.DefaultIfEmpty().Max(p => p.X),
|
|||
|
|
_inhaleData.DefaultIfEmpty().Max(p => p.X)
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
// 当数据满20个时,X轴范围固定为这20个数据的时间跨度
|
|||
|
|
if (_exhaleData.Count >= MAX_DATA_POINTS)
|
|||
|
|
{
|
|||
|
|
double minX = _exhaleData.Min(p => p.X);
|
|||
|
|
_xAxis.Minimum = minX;
|
|||
|
|
_xAxis.Maximum = maxX + 0.5; // 右侧留一点余量
|
|||
|
|
}
|
|||
|
|
else
|
|||
|
|
{
|
|||
|
|
_xAxis.Minimum = 0;
|
|||
|
|
_xAxis.Maximum = maxX < 1 ? 1 : maxX * 1.1;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private bool IsModbusConnected()
|
|||
|
|
{
|
|||
|
|
return _tcpClient != null && _tcpClient.Connected && _modbusMaster != null;
|
|||
|
|
}
|
|||
|
|
#endregion
|
|||
|
|
|
|||
|
|
#region 窗口加载与资源释放
|
|||
|
|
private void Window_Loaded(object sender, RoutedEventArgs e)
|
|||
|
|
{
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
string imagePath = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Resources/sleep2.jpg");
|
|||
|
|
if (System.IO.File.Exists(imagePath))
|
|||
|
|
{
|
|||
|
|
var bitmap = new BitmapImage();
|
|||
|
|
bitmap.BeginInit();
|
|||
|
|
bitmap.UriSource = new Uri(imagePath, UriKind.Absolute);
|
|||
|
|
bitmap.DecodePixelWidth = (int)this.Width;
|
|||
|
|
bitmap.CacheOption = BitmapCacheOption.OnLoad;
|
|||
|
|
bitmap.EndInit();
|
|||
|
|
|
|||
|
|
Background = new ImageBrush
|
|||
|
|
{
|
|||
|
|
ImageSource = bitmap,
|
|||
|
|
Stretch = Stretch.UniformToFill,
|
|||
|
|
//CacheOption = BitmapCacheOption.OnLoad
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
else
|
|||
|
|
{
|
|||
|
|
Console.WriteLine($"背景图片不存在: {imagePath}");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 获取屏幕工作区(排除任务栏)
|
|||
|
|
var workingArea = SystemParameters.WorkArea;
|
|||
|
|
// 计算居中位置
|
|||
|
|
this.Left = workingArea.Left + (workingArea.Width - this.Width) / 2;
|
|||
|
|
this.Top = workingArea.Top + (workingArea.Height - this.Height) / 2;
|
|||
|
|
|
|||
|
|
}
|
|||
|
|
catch (Exception ex)
|
|||
|
|
{
|
|||
|
|
Console.WriteLine($"加载背景失败: {ex.Message}");
|
|||
|
|
Background = Brushes.White;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
protected override void OnClosed(EventArgs e)
|
|||
|
|
{
|
|||
|
|
base.OnClosed(e);
|
|||
|
|
_dataTimer?.Stop();
|
|||
|
|
_tcpClient?.Close();
|
|||
|
|
_resizeTokenSource?.Dispose();
|
|||
|
|
}
|
|||
|
|
#endregion
|
|||
|
|
}
|
|||
|
|
}
|