有没有更有效的方法从串行端口读取值并实时更新图表 - WPF
Is there a more efficient way to read a value from Serial Port and Update a Chart in realtime - WPF
使用实时图表,我正在创建一个实时图表,它会更新从串行端口读取的值。现在我可以让它工作了,但我认为我没有尽可能高效地执行此操作,因为我没有使用 C# 和 WPF 的经验。
我将从串行端口读取的数据存储在 SerialCommunication class 中。然后我使用一个按钮开始一个新任务,它打开串口并更新我的图表。
我的问题是我希望能够在每次 Serial class 接收到新值时更新图表,但是,我的图表在 Read() 函数中更新,该函数从开始一个新的任务,我觉得这可能会导致线程问题。
如有任何建议,我们将不胜感激。
连载class
public class SerialCommunication
{
private string _value;
SerialPort serialPort = null;
public SerialCommunication()
{
InitializeComms();
}
private void InitializeComms()
{
try
{
serialPort = new SerialPort("COM6", 115200, Parity.None, 8, StopBits.One); // Update this to avoid hard coding COM port and BAUD rate
serialPort.DataReceived += new SerialDataReceivedEventHandler(DataReceivedHandler);
}
catch (Exception ex) { MessageBox.Show(ex.Message); }
}
~SerialCommunication()
{
if(serialPort.IsOpen)
serialPort.Close();
}
public void ReceiveData()
{
try
{
if (!serialPort.IsOpen)
serialPort.Open();
}
catch(Exception ex) { MessageBox.Show(ex.Message); }
}
public void StopReceivingData()
{
try
{
if (serialPort.IsOpen)
serialPort.Close();
}
catch(Exception ex) { MessageBox.Show(ex.Message); }
}
public event EventHandler DataReceived;
private void OnDataReceived(EventArgs e)
{
DataReceived?.Invoke(this, e);
}
// read the data in the DataReceivedHandler
// Event Handler
public void DataReceivedHandler(object sender, SerialDataReceivedEventArgs e)
{
try
{
SerialPort sp = (SerialPort)sender;
_value = sp.ReadLine();
}
catch (Exception ex) { MessageBox.Show(ex.Message); }
OnDataReceived(EventArgs.Empty);
}
}
Time of Flight class 使用 beto-rodriguez
从 LiveCharts 中获取的代码,根据从串口读取的传感器值更新图表
public TimeOfFlight()
{
InitializeComponent();
// attach an event handler to update graph
serial.DataReceived += new EventHandler(UpdateChart);
// Use PlotData class for graph data which will use this config every time
var mapper = Mappers.Xy<PlotData>()
.X(model => model.DateTime.Ticks)
.Y(model => model.Value);
// Save mapper globally
Charting.For<PlotData>(mapper);
chartValues = new ChartValues<PlotData>();
//lets set how to display the X Labels
XFormatter = value => new DateTime((long)value).ToString("hh:mm:ss");
YFormatter = x => x.ToString("N0");
//AxisStep forces the distance between each separator in the X axis
AxisStep = TimeSpan.FromSeconds(1).Ticks;
//AxisUnit forces lets the axis know that we are plotting seconds
//this is not always necessary, but it can prevent wrong labeling
AxisUnit = TimeSpan.TicksPerSecond;
SetAxisLimits(DateTime.Now);
//ZoomingMode = ZoomingOptions.X;
IsReading = false;
DataContext = this;
}
public ChartValues<PlotData> chartValues { get; set; }
public Func<double, string> XFormatter { get; set; }
public Func<double, string> YFormatter { get; set; }
public double AxisStep { get; set; }
public double AxisUnit { get; set; }
public double AxisMax
{
set
{
_axisXMax = value;
OnPropertyChanged("AxisMax");
}
get { return _axisXMax; }
}
public double AxisMin
{
set
{
_axisXMin = value;
OnPropertyChanged("AxisMin");
}
get { return _axisXMin; }
}
public bool IsReading { get; set; }
private void StartStopGraph(object sender, RoutedEventArgs e)
{
IsReading = !IsReading;
if (IsReading)
{
serial.ReceiveData();
}
else
serial.StopReceivingData();
}
public void UpdateChart(object sender, EventArgs e) // new task
{
try
{
var now = DateTime.Now;
// can chartValues.Add be called from INotifyPropertyChanged in
// SerialCommunication.cs and would this cause an issue with the
chartValues.Add(new PlotData
{
DateTime = now,
Value = 0 // update this
});
SetAxisLimits(now);
//lets only use the last 150 values
if (chartValues.Count > 1000) chartValues.RemoveAt(0);
}
catch (Exception ex) { MessageBox.Show(ex.Message); }
}
private void SetAxisLimits(DateTime now)
{
AxisMax = now.Ticks + TimeSpan.FromSeconds(1).Ticks; // lets force the axis to be 1 second ahead
AxisMin = now.Ticks - TimeSpan.FromSeconds(8).Ticks; // and 8 seconds behind
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName = null)
{
if (PropertyChanged != null)
PropertyChanged.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
绘图数据class
public class PlotData
{
public DateTime DateTime { get; set; }
public int Value { get; set; }
}
是的,在我看来这不是最好的方式。
首先,我不会在您的 SerialCommunication class 中与 INotifyPropertyChanged 或 business/view 逻辑有任何关系 class,从架构的角度来看,它应该管理串行的打开和关闭设备,任何确实到达的数据都应该通过事件传递到应用的其他部分。
其次,为什么要使用循环?您已经订阅了 DataReceived,因此只需读取 DataReceivedHandler 中的数据并将其传递给所述事件即可。该处理程序将在不同的线程上调用,但订阅您的事件的处理程序可以根据需要分派到主 GUI 线程。
这让我想到了第三点:如果您的数据绑定正确(而不是直接更新您的控件),那么您不需要这样做,只需更新您的数据值并将其留给 WPF 数据绑定引擎根据需要更新。
在实际应用程序中,您可能希望以特定时间间隔进行更新,为此您需要将数据值推送到队列中,然后一次处理所有这些值。执行此操作的正确方法是在 C# 中使用异步任务。不要使用线程,绝对不要使用 Thread.Sleep...线程是一种过时的技术,除了一些非常特殊的情况外,它在 C# 中没有立足之地。
最干净的方法是使用 Reactive Extensions (IObservable) 将串行通信实现为独立模块。
这样,您的 WPF 应用程序可以在适当的线程上下文中订阅可观察到的消息,并在每次收到新消息时更新 UI。
使用实时图表,我正在创建一个实时图表,它会更新从串行端口读取的值。现在我可以让它工作了,但我认为我没有尽可能高效地执行此操作,因为我没有使用 C# 和 WPF 的经验。
我将从串行端口读取的数据存储在 SerialCommunication class 中。然后我使用一个按钮开始一个新任务,它打开串口并更新我的图表。
我的问题是我希望能够在每次 Serial class 接收到新值时更新图表,但是,我的图表在 Read() 函数中更新,该函数从开始一个新的任务,我觉得这可能会导致线程问题。 如有任何建议,我们将不胜感激。
连载class
public class SerialCommunication
{
private string _value;
SerialPort serialPort = null;
public SerialCommunication()
{
InitializeComms();
}
private void InitializeComms()
{
try
{
serialPort = new SerialPort("COM6", 115200, Parity.None, 8, StopBits.One); // Update this to avoid hard coding COM port and BAUD rate
serialPort.DataReceived += new SerialDataReceivedEventHandler(DataReceivedHandler);
}
catch (Exception ex) { MessageBox.Show(ex.Message); }
}
~SerialCommunication()
{
if(serialPort.IsOpen)
serialPort.Close();
}
public void ReceiveData()
{
try
{
if (!serialPort.IsOpen)
serialPort.Open();
}
catch(Exception ex) { MessageBox.Show(ex.Message); }
}
public void StopReceivingData()
{
try
{
if (serialPort.IsOpen)
serialPort.Close();
}
catch(Exception ex) { MessageBox.Show(ex.Message); }
}
public event EventHandler DataReceived;
private void OnDataReceived(EventArgs e)
{
DataReceived?.Invoke(this, e);
}
// read the data in the DataReceivedHandler
// Event Handler
public void DataReceivedHandler(object sender, SerialDataReceivedEventArgs e)
{
try
{
SerialPort sp = (SerialPort)sender;
_value = sp.ReadLine();
}
catch (Exception ex) { MessageBox.Show(ex.Message); }
OnDataReceived(EventArgs.Empty);
}
}
Time of Flight class 使用 beto-rodriguez
从 LiveCharts 中获取的代码,根据从串口读取的传感器值更新图表public TimeOfFlight()
{
InitializeComponent();
// attach an event handler to update graph
serial.DataReceived += new EventHandler(UpdateChart);
// Use PlotData class for graph data which will use this config every time
var mapper = Mappers.Xy<PlotData>()
.X(model => model.DateTime.Ticks)
.Y(model => model.Value);
// Save mapper globally
Charting.For<PlotData>(mapper);
chartValues = new ChartValues<PlotData>();
//lets set how to display the X Labels
XFormatter = value => new DateTime((long)value).ToString("hh:mm:ss");
YFormatter = x => x.ToString("N0");
//AxisStep forces the distance between each separator in the X axis
AxisStep = TimeSpan.FromSeconds(1).Ticks;
//AxisUnit forces lets the axis know that we are plotting seconds
//this is not always necessary, but it can prevent wrong labeling
AxisUnit = TimeSpan.TicksPerSecond;
SetAxisLimits(DateTime.Now);
//ZoomingMode = ZoomingOptions.X;
IsReading = false;
DataContext = this;
}
public ChartValues<PlotData> chartValues { get; set; }
public Func<double, string> XFormatter { get; set; }
public Func<double, string> YFormatter { get; set; }
public double AxisStep { get; set; }
public double AxisUnit { get; set; }
public double AxisMax
{
set
{
_axisXMax = value;
OnPropertyChanged("AxisMax");
}
get { return _axisXMax; }
}
public double AxisMin
{
set
{
_axisXMin = value;
OnPropertyChanged("AxisMin");
}
get { return _axisXMin; }
}
public bool IsReading { get; set; }
private void StartStopGraph(object sender, RoutedEventArgs e)
{
IsReading = !IsReading;
if (IsReading)
{
serial.ReceiveData();
}
else
serial.StopReceivingData();
}
public void UpdateChart(object sender, EventArgs e) // new task
{
try
{
var now = DateTime.Now;
// can chartValues.Add be called from INotifyPropertyChanged in
// SerialCommunication.cs and would this cause an issue with the
chartValues.Add(new PlotData
{
DateTime = now,
Value = 0 // update this
});
SetAxisLimits(now);
//lets only use the last 150 values
if (chartValues.Count > 1000) chartValues.RemoveAt(0);
}
catch (Exception ex) { MessageBox.Show(ex.Message); }
}
private void SetAxisLimits(DateTime now)
{
AxisMax = now.Ticks + TimeSpan.FromSeconds(1).Ticks; // lets force the axis to be 1 second ahead
AxisMin = now.Ticks - TimeSpan.FromSeconds(8).Ticks; // and 8 seconds behind
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName = null)
{
if (PropertyChanged != null)
PropertyChanged.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
绘图数据class
public class PlotData
{
public DateTime DateTime { get; set; }
public int Value { get; set; }
}
是的,在我看来这不是最好的方式。
首先,我不会在您的 SerialCommunication class 中与 INotifyPropertyChanged 或 business/view 逻辑有任何关系 class,从架构的角度来看,它应该管理串行的打开和关闭设备,任何确实到达的数据都应该通过事件传递到应用的其他部分。
其次,为什么要使用循环?您已经订阅了 DataReceived,因此只需读取 DataReceivedHandler 中的数据并将其传递给所述事件即可。该处理程序将在不同的线程上调用,但订阅您的事件的处理程序可以根据需要分派到主 GUI 线程。
这让我想到了第三点:如果您的数据绑定正确(而不是直接更新您的控件),那么您不需要这样做,只需更新您的数据值并将其留给 WPF 数据绑定引擎根据需要更新。
在实际应用程序中,您可能希望以特定时间间隔进行更新,为此您需要将数据值推送到队列中,然后一次处理所有这些值。执行此操作的正确方法是在 C# 中使用异步任务。不要使用线程,绝对不要使用 Thread.Sleep...线程是一种过时的技术,除了一些非常特殊的情况外,它在 C# 中没有立足之地。
最干净的方法是使用 Reactive Extensions (IObservable) 将串行通信实现为独立模块。
这样,您的 WPF 应用程序可以在适当的线程上下文中订阅可观察到的消息,并在每次收到新消息时更新 UI。