Есть ли более эффективный способ считывания значения из последовательного порта и обновления диаграммы в режиме реального времени - WPF - PullRequest
0 голосов
/ 07 ноября 2018

Используя Live Charts, я создаю график в реальном времени, который обновляется значениями, считанными с последовательного порта. Теперь я могу заставить это работать, но я не думаю, что я делаю это так эффективно, как мог бы, поскольку я неопытен в использовании 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 ]

0 голосов
/ 08 ноября 2018

Самым чистым способом было бы реализовать вашу последовательную связь как отдельный модуль с использованием Reactive Extensions (IObservable).

Таким образом, ваше приложение WPF может подписаться на сообщение, наблюдаемое в соответствующем контексте потоков, и обновлять пользовательский интерфейс при каждом получении нового сообщения.

0 голосов
/ 08 ноября 2018

Да, не лучшим образом, ИМО.

Прежде всего, я бы не стал ничего делать с INotifyPropertyChanged или логикой бизнес / представления в вашем классе SerialCommunication, с архитектурной точки зрения все, что нужно сделать для управления открытием и закрытием последовательного устройства, а также с любыми поступающими данными. должны передаваться другим частям вашего приложения через событие.

Во-вторых, почему вы используете цикл? Вы уже подписаны на DataReceived, поэтому просто прочитайте данные в вашем DataReceivedHandler и передайте их указанному событию. Этот обработчик будет вызываться в другом потоке, но обработчики, подписавшиеся на ваше событие, могут отправлять их в основной поток GUI, если это необходимо.

Что подводит меня к третьему пункту: если вы правильно связываете данные (в отличие от непосредственного обновления ваших элементов управления), вам не нужно этого делать, просто обновите значения данных и оставьте их механизму связывания данных WPF для обновите по мере необходимости.

В реальном приложении вы можете выполнять обновления через определенный интервал, и для этого вам нужно будет поместить ваши значения данных в очередь, а затем обработать их все сразу. Правильный способ сделать это в C # с асинхронной задачей. Не используйте потоки и определенно не используйте Thread.Sleep .... потоки являются устаревшей технологией, которой нет места в C #, за исключением нескольких очень специфических обстоятельств.

...