Files

457 lines
16 KiB
C#
Raw Permalink Normal View History

2026-05-04 14:46:58 +08:00
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
}
}