464 lines
17 KiB
C#
464 lines
17 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
|
||
{
|
||
private readonly string _lang = ConfigurationManager.AppSettings["Language"] ?? "zh-CN";
|
||
#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);
|
||
bool success = connectResult.AsyncWaitHandle.WaitOne(3000);
|
||
|
||
if (!success || !_tcpClient.Connected)
|
||
throw new Exception(_lang == "en-US" ? "PLC connect timeout" : "连接PLC超时,请检查IP和端口");
|
||
|
||
_modbusMaster = ModbusIpMaster.CreateIp(_tcpClient);
|
||
_modbusMaster.Transport.ReadTimeout = 1000;
|
||
_modbusMaster.Transport.WriteTimeout = 1000;
|
||
|
||
ma = new Function(_modbusMaster);
|
||
|
||
StatusText.Text = _lang == "en-US" ? "Status: Modbus connected" : "状态:Modbus连接成功";
|
||
StatusText.Foreground = new SolidColorBrush(Color.FromRgb(76, 175, 80));
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
ShowError(_lang == "en-US" ? $"Modbus init failed: {ex.Message}" : $"Modbus初始化失败: {ex.Message}");
|
||
StatusText.Text = _lang == "en-US" ? "Status: Connect failed" : "状态:连接失败";
|
||
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 = _lang == "en-US"
|
||
? "Flow Monitoring (Latest 30 Points)"
|
||
: "呼气流量与吸气流量监测(最新30个数据)",
|
||
TitleFontSize = 18,
|
||
TitleFontWeight = OxyPlot.FontWeights.Bold,
|
||
Background = OxyColors.White,
|
||
PlotAreaBackground = OxyColor.FromRgb(250, 252, 255),
|
||
};
|
||
|
||
var legend = new Legend
|
||
{
|
||
LegendPlacement = LegendPlacement.Outside,
|
||
LegendPosition = LegendPosition.TopRight,
|
||
FontSize = 14
|
||
};
|
||
_plotModel.Legends.Add(legend);
|
||
|
||
_exhaleSeries = new LineSeries
|
||
{
|
||
Title = _lang == "en-US" ? "Exhale Flow (L/min)" : "呼气流量 (L/min)",
|
||
Color = OxyColor.FromRgb(239, 83, 80),
|
||
StrokeThickness = 2,
|
||
MarkerType = MarkerType.None,
|
||
MarkerSize = 4
|
||
};
|
||
|
||
_inhaleSeries = new LineSeries
|
||
{
|
||
Title = _lang == "en-US" ? "Inhale Flow (L/min)" : "吸气流量 (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 = _lang == "en-US" ? "Time (s)" : "时间 (秒)",
|
||
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 = _lang == "en-US" ? "Flow (L/min)" : "流量 (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 = _lang == "en-US" ? "Status: Monitoring" : "状态:正在监测";
|
||
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 = _lang == "en-US" ? "Status: Stopped" : "状态:已停止";
|
||
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
|
||
}
|
||
}
|