Monday, April 10, 2023

Real-Time Temperature Plot using Qt and QCustomPlot

Hello! Everyone, in this post I will show you how to plot the data in real-time using Qt and QCustomPlot library.

Temperature Real-Time Plot using Qt and QCustomPlot

In this post, I am using a temperature sensor LM35 which is connected to Arduino UNO, and the Arduino UNO is converting the analog values received from the sensor into a corresponding digital value, which is then transmitted over Serial Terminal, and this data is then received by Qt using QSerialPort library, and then plotted using QCustomPlot library.

I am not going to explain the LM35 sensor part, as this is very basic and can be found in several other posts on my blog post itself, one such blog post is available below.

Real-Time Temperature Monitoring using MATLAB & Python - Embedded Laboratory

QtCharts vs QCustomPlot

QtCharts is the charting library provided by Qt company while the QCustomPlot library is from some another vendor, now the questions might be asked why I am using the QCustomPlot library instead of QtCharts, and the reason is I don't know 😊.
What I have read on several Qt forums is that the QtChart library is poorly designed, but to be very honest with you guys I am not an expert so I don't know if this is true or not, I will try to share my personal experience, initially, I started using the QtCharts library, and simple things were working fine, but then I moved to a complex part of updating the data in real-time, and unfortunately I couldn't make it work, I spent several hours on finding a fix, but nothing works, I am 100% that I am doing something wrong but I don't have a solution and just as a random try I started with the QCustomPlot library, and I am really liking this, and this is the reason that this blog post I am using the QCustomPlot library.

Challenges Faced

I would like to list down the challenges I faced while developing this small project, and they are as below.

The first challenge was using the QCustomPlot library with Qt6 as a CMake project, for some of you this might be not a must, but these days I switched to CMake and hence it was quite shocking for me why I am not able to use the QCustomPlot library in my Qt CMake project. But then thanks to "Leger Charlie" a GitHub user, who has already solved this problem and has a repository for this, I used his library as a git submodule project for my development.

leger50/QCustomPlot-library (github.com)

Another problem I was facing was setting the ticker properly for X-axis, and the reason was also very simple, here I was using the class "QCPAxisTickerTime", the problem was my time was in seconds since Epoch time, while this class only considers the pure time, and the solution was to use the class "QCPAxisTickerDateTime" to format my ticker on X-Axis.

Source Code

I prepared a temperature viewer class which is handling all the things from getting the data from the serial port and plotting the data.

The following is the content of the main.cpp file.

#include "temperatureviewer.h"
#include <QApplication>

int main(int argc, char *argv[])
{
  QApplication a(argc, argv);
  TemperatureViewer w;

  w.show();
  return a.exec();
}

This is very simple, we created an application and just showed this application, and all the things are happening inside the application class instance.

The following is the content of the temperatureviewer.h file.


#ifndef TEMPERATUREVIEWER_H
#define TEMPERATUREVIEWER_H

#include <QMainWindow>
#include <QDebug>
#include <QTimer>
#include <QSerialPort>
#include <QSerialPortInfo>


QT_BEGIN_NAMESPACE
namespace Ui { class TemperatureViewer; }
QT_END_NAMESPACE

class TemperatureViewer : public QMainWindow

{
  Q_OBJECT

public:
  TemperatureViewer(QWidget *parent = nullptr);
  ~TemperatureViewer();

private slots:
  void on_btn_ConnectDisconnect_clicked();

public slots:
  void read_data( void );

private:
  Ui::TemperatureViewer *ui;
  bool connect_status = false;
  QSerialPort m_serial;
  void refreshGraph( void );
};

#endif // TEMPERATUREVIEWER_H

I will try to explain the usage of all members and methods of this class.

  • "connect_status": this member is used to track the status of the QSerialPort connection status, if the connection is successful its value is true else false.
  • "m_serial": this member is a "QSerialPort" object, and we will communicate with the actual serial port using this object.
  • "refreshGraph": this method is used to update/refresh the graph every second.

The following is the content of the temperatureviewer.cpp file.

#include "temperatureviewer.h"
#include "./ui_temperatureviewer.h"
#include <QDateTime>

const qint8 SECONDS_SHOW_ON_GRAPH = 120;  // Display 120 seconds on the graph
QList<double> time_axis;
QList<double> temperature_axis;
static qint64 temp_data_idx = 0;   // to be used later for x-axis range setting
static qint64 startTime;

TemperatureViewer::TemperatureViewer(QWidget *parent) : QMainWindow(parent) , ui(new Ui::TemperatureViewer)
{
  ui->setupUi(this);

  qDebug() << "Detecting Available Serial Ports";

  QList<QSerialPortInfo> serial_port_infos = QSerialPortInfo::availablePorts();
  for (const QSerialPortInfo &port_info : serial_port_infos )
  {
    qDebug() << "Port:" << port_info.portName();
    // Add these found com ports to the combo box
    ui->cb_COMP->addItem(port_info.portName());
  }

  // Plotting Stuff
  // Create graph and assign data to it
  ui->customPlot->addGraph();
  // give axis some labels
  ui->customPlot->xAxis->setLabel("Time");
  ui->customPlot->yAxis->setLabel("Temperature");
  QColor color(40, 110, 255);
  ui->customPlot->graph(0)->setLineStyle( QCPGraph::lsLine );
  ui->customPlot->graph(0)->setPen( QPen(color.lighter(30)) );
  ui->customPlot->graph(0)->setBrush( QBrush(color) );

  // configure bottom axis to show date instead of number:
  QSharedPointer<QCPAxisTickerDateTime> date_time_ticker(new QCPAxisTickerDateTime);
  date_time_ticker->setDateTimeFormat("hh:mm:ss");
  ui->customPlot->xAxis->setTicker(date_time_ticker);

  // make left and bottom axes transfer their ranges to right and top axes:
  connect(ui->customPlot->xAxis, SIGNAL(rangeChanged(QCPRange)), ui->customPlot->xAxis2, SLOT(setRange(QCPRange)));
  connect(ui->customPlot->yAxis, SIGNAL(rangeChanged(QCPRange)), ui->customPlot->yAxis2, SLOT(setRange(QCPRange)));

  // Allow user to drag axis ranges with mouse, zoom with mouse wheel and select graphs by clicking:
  ui->customPlot->setInteractions(QCP::iRangeDrag | QCP::iRangeZoom | QCP::iSelectPlottables);

  // first we create and prepare a text layout element:
  QCPTextElement *title = new QCPTextElement(ui->customPlot);
  title->setText("Temperature Real-Time Plot");
  title->setFont(QFont("sans", 12, QFont::Bold));
  // then we add it to the main plot layout:
  // insert an empty row above the axis rect
  ui->customPlot->plotLayout()->insertRow(0);
  // place the title in the empty cell we've just created
  ui->customPlot->plotLayout()->addElement(0, 0, title);

  // Start Timer to Refresh the graph
  QTimer *timer = new QTimer(this);
  connect(timer, &QTimer::timeout, this, &TemperatureViewer::refreshGraph );
  // Start Timer @ 1 second
  timer->start(1000);
}

TemperatureViewer::~TemperatureViewer()
{
  if( m_serial.isOpen() )
  {
    // if serial port is open close it
    m_serial.close();
  }
  delete ui;
}


void TemperatureViewer::on_btn_ConnectDisconnect_clicked( void )
{
  double now;
  // if false, we have to connect, else disconnect
  if( connect_status == false )
  {
    qInfo() << "Connecting...";
    m_serial.setBaudRate( QSerialPort::Baud9600 );
    m_serial.setDataBits( QSerialPort::Data8 );
    m_serial.setParity( QSerialPort::NoParity );
    m_serial.setStopBits( QSerialPort::OneStop );
    m_serial.setFlowControl( QSerialPort::NoFlowControl );
    // Select the COM Port from Combo Box
    m_serial.setPortName( ui->cb_COMP->currentText() );;
    if( m_serial.open( QIODevice::ReadWrite ) )
    {
      qDebug() << "Serial Port Opened Successfully";
      m_serial.write("Hello World from Qt\r\n");
      connect_status = true;
      ui->btn_ConnectDisconnect->setText("Disconnect");
      // disable the combo box
      ui->cb_COMP->setEnabled(false);
      // Connect Signal and Slots
      connect(&m_serial, SIGNAL( readyRead() ), this, SLOT(read_data() ) );

      startTime = QDateTime::currentSecsSinceEpoch();
      now = startTime;
      // set axes ranges, so we see all data:
      ui->customPlot->xAxis->setRange( now, now+SECONDS_SHOW_ON_GRAPH);
      ui->customPlot->yAxis->setRange(0, 60);
    }
    else
    {
      qDebug() << "Unable to open the Selected Serial Port" << m_serial.error();
    }
  }
  else
  {
    qInfo() << "Disconnecting...";
    // close the serial port
    m_serial.close();
    connect_status = false;
    ui->btn_ConnectDisconnect->setText("Connect");
    // Enable the combo box
    ui->cb_COMP->setEnabled(true);
  }
}

void TemperatureViewer::read_data()
{
  char data;
  float temp_value;
  double now = QDateTime::currentSecsSinceEpoch();

  while( m_serial.bytesAvailable() )
  {
    // Read one byte at a time
    m_serial.read(&data, 1);
    if( (data != '\r') && (data != '\n') )
    {
      // convert adc counts back to the temperature value
      // temperature = (adc counts * VCC in mV/ADC Resolution)/10mV
      temp_value = (data * 500.0 / 1023.0);
      time_axis.append(now);
      temperature_axis.append(temp_value);
      temp_data_idx++;
      qDebug() << temp_data_idx << temp_value;
    }
  }
}

void TemperatureViewer::refreshGraph( void )
{
  double now = QDateTime::currentSecsSinceEpoch();
  if( connect_status )
  {
    ui->customPlot->graph()->setData( time_axis, temperature_axis);
    // if time has elapsed then only start shifting the graph
    if( ((qint64)(now) - startTime) > SECONDS_SHOW_ON_GRAPH )
    {
      // If SECONDS_SHOW_GRAPH
      ui->customPlot->xAxis->setRange(now, SECONDS_SHOW_ON_GRAPH, Qt::AlignRight);
    }
    ui->customPlot->replot();
  }
}

In the constructor code of this class we are doing the following things:

  • With the help of the "QSerialPortInfo" class, we are getting the information on the available serial ports, and these port names are added to the combo box so that they can be selected by the user.
  • A graph is added, where labels for the x and y axis are specified, and some other configuration is done like line color and brush
  • Then the x-axis ticker is updated in an hour, minute and seconds format.
  • Interactions are also added to the graph for dragging, zooming and scrolling.
  • A title is specified in the graph.

In the destructor part of the code, we are closing the serial port if open and deleting the UI object.

Then we have a button named as connect button, and whenever this button is clicked a signal is emitted internally, and we have mapped a slot for this clicked event, and hence the "on_btn_ConnectDisiconnect_clicked" function is called. In this function, if the connection status is false, we are configuring the serial port and opening it, and also changing the name of the connect button to disconnect, as next time the same button will be used for disconnecting from the com port.

Then the "read_data" function is called whenever we receive some data over the serial port, and once this data is received it is decoded and converted into the temperature value, and then these values are updated in the list along with the timestamp.

Plotting of this data in real-time is done using the function "refreshGraph" function here graph is updated periodically, here a timer is configured to update the graph every second.

NOTE: I am not a C++ programmer, I have mostly used C in my life and now started learning C++, so it might be possible I made some silly mistakes that can be avoided by some expert, any feedback is welcomed.

The code is available on my GitHub Repository and can also be downloaded by clicking here.

No comments:

Post a Comment