Есть ли более эффективный способ считывания значения из последовательного порта и обновления диаграммы в реальном времени - WPF
Используя Живые диаграммы, я создаю график в реальном времени, который обновляется значениями, считанными с последовательного порта. Теперь я могу заставить это работать, но я не думаю, что я делаю это так эффективно, как мог бы, поскольку я неопытен, используя C# и WPF.
Я храню данные, считываемые с последовательного порта, в классе SerialCommunication. Затем я использую кнопку для запуска новой задачи, которая открывает последовательный порт и обновляет мой график.
Моя проблема в том, что я хочу иметь возможность обновлять график каждый раз, когда класс Serial получает новое значение, однако моя диаграмма обновляется в функции Read(), которая вызывается при запуске новой задачи, и я чувствую, что это может вызвать проблемы с многопоточностью., Любой совет будет принят во внимание.
Серийный класс
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, который обновляет диаграмму из значений датчиков, считанных из последовательного порта, используя код, полученный из LiveCharts beto-rodriguez
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));
}
}
Класс PlotData
public class PlotData
{
public DateTime DateTime { get; set; }
public int Value { get; set; }
}
2 ответа
Да, не самый лучший способ ИМО.
Прежде всего, я бы не стал ничего делать с INotifyPropertyChanged или логикой business/view в вашем классе SerialCommunication, с архитектурной точки зрения все, что он должен делать, управлять открытием и закрытием последовательного устройства, а любые поступающие данные должны передаваться в других частях вашего приложения через событие.
Во-вторых, почему вы используете цикл? Вы уже подписаны на DataReceived, поэтому просто прочитайте данные в вашем DataReceivedHandler и передайте их указанному событию. Этот обработчик будет вызываться в другом потоке, но обработчики, подписавшиеся на ваше событие, могут отправлять их в основной поток GUI, если это необходимо.
Что подводит меня к третьему пункту: если вы правильно связываете данные (в отличие от непосредственного обновления ваших элементов управления), вам не нужно это делать, просто обновите значения данных и оставьте их для механизма связывания данных WPF для обновления по мере необходимости.,
В реальном приложении вы можете выполнять обновления с определенным интервалом, и для этого вы хотите поместить свои данные в очередь, а затем обработать их все сразу. Правильный способ сделать это в C# с асинхронной задачей. Не используйте потоки, и определенно не используйте Thread.Sleep.... потоки - устаревшая технология, которой нет места в C#, за исключением нескольких очень специфических обстоятельств.
Самым чистым способом было бы реализовать вашу последовательную связь как отдельный модуль с использованием Reactive Extensions (IObservable).
Таким образом, ваше приложение WPF может подписаться на сообщение, наблюдаемое в соответствующем контексте потоков, и обновлять пользовательский интерфейс при каждом получении нового сообщения.