Alex Spataru
  • Home
  • Blog
  • Projects
    • Serial Studio
  • Home
  • Blog
  • Projects
    • Serial Studio
Search

Blog posts, ideas & rambles...

Using Qt Widgets in your QML/QtQuick applications

3/20/2021

0 Comments

 
There is no denying that QtQuick has a lot of handy features and allows programmers to build nice GUI applications with ease. However, sometimes we would like to integrate the functionality of "legacy" Qt Widgets with our QML applications.

For example, QtQuick does not have a direct replacement of the QPlainTextEdit widget (which is very useful if you need to log large amounts of data or if you want to build a custom code editor). Other examples of such widgets are the QTableWidget and third-party widgets (such as Qwt or QScintilla).

I bumped into this issue while developing an appropriate way to create a QML-friendly serial output console for Serial Studio. Using the standard QML TextArea resulted too slow for displaying high-frequency data from a serial port device.

To fix this, I decided to use the QPlainTextEdit widget for displaying incoming serial data and use QQuickPaintedItem to render the widget in the QML interface. 

We'll begin our unorthodox programming ritual with the following header code:
#ifndef UI_QML_PLAINTEXTEDIT_H
#define UI_QML_PLAINTEXTEDIT_H

#include <QPainter>
#include <QPlainTextEdit>
#include <QQuickPaintedItem>

class QmlPlainTextEdit : public QQuickPaintedItem
{
public:
    QmlPlainTextEdit(QQuickItem *parent = 0);
    ~QmlPlainTextEdit();

    virtual bool event(QEvent *event) override;
    virtual void paint(QPainter *painter) override;
    virtual bool eventFilter(QObject *watched, QEvent *event) override;

    QPlainTextEdit *textEdit() const;

private slots:
    void updateWidgetSize();

protected:
    void processMouseEvents(QMouseEvent *event);
    void processWheelEvents(QWheelEvent *event);

private:
    QPlainTextEdit *m_textEdit;
};

#endif

​And here is the implementation code for this class:
#include "QmlPlainTextEdit.h"

/**
 * Constructor function
 */
QmlPlainTextEdit::QmlPlainTextEdit(QQuickItem *parent)
    : QQuickPaintedItem(parent)
    , m_textEdit(new QPlainTextEdit)
{
    // Set item flags
    setFlag(ItemHasContents, true);
    setFlag(ItemAcceptsInputMethod, true);
    setFlag(ItemIsFocusScope, true);
    setAcceptedMouseButtons(Qt::AllButtons);

    // Install event filter for widget
    textEdit()->installEventFilter(this);

    // Set the QML item's implicit size
    auto hint = textEdit()->sizeHint();
    setImplicitSize(hint.width(), hint.height());

    // Setup default options
    textEdit()->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
    textEdit()->setSizeAdjustPolicy(QPlainTextEdit::AdjustToContents);

    // Resize QPlainTextEdit to fit QML item
    connect(this, &QQuickPaintedItem::widthChanged, this,
            &QmlPlainTextEdit::updateWidgetSize);
    connect(this, &QQuickPaintedItem::heightChanged, this,
            &QmlPlainTextEdit::updateWidgetSize);
}

/**
 * Destructor function
 */
QmlPlainTextEdit::~QmlPlainTextEdit()
{
    m_textEdit->deleteLater();
}

/**
 * Handle application events manually
 */
bool QmlPlainTextEdit::event(QEvent *event)
{
    switch (event->type())
    {
        case QEvent::FocusIn:
            forceActiveFocus();
            return QQuickPaintedItem::event(event);
            break;
        case QEvent::Wheel:
            processWheelEvents(static_cast<QWheelEvent *>(event));
            return true;
            break;
        case QEvent::MouseButtonPress:
        case QEvent::MouseButtonRelease:
        case QEvent::MouseButtonDblClick:
        case QEvent::MouseMove:
            processMouseEvents(static_cast<QMouseEvent *>(event));
            return true;
            break;
        default:
            break;
    }

    return QApplication::sendEvent(textEdit(), event);
}

/**
 * Render the text edit on the given @a painter
 */
void QmlPlainTextEdit::paint(QPainter *painter)
{
    if (painter)
        textEdit()->render(painter);
}

/**
 * Custom event filter to manage redraw requests
 */
bool QmlPlainTextEdit::eventFilter(QObject *watched, QEvent *event)
{
    Q_ASSERT(m_textEdit);

    if (watched == textEdit())
    {
        switch (event->type())
        {
            case QEvent::Paint:
            case QEvent::UpdateRequest:
                update();
                break;
            default:
                break;
        }
    }

    return QQuickPaintedItem::eventFilter(watched, event);
}

/**
 * Resizes the text editor widget to fit inside the QML item.
 */
void QmlPlainTextEdit::updateWidgetSize()
{
    textEdit()->setFixedSize(width(), height());
    update();
}

/**
 * Hack: call the appropriate protected mouse event handler function 
 *       of the QPlainTextEdit item depending on event type.
 */
void QmlPlainTextEdit::processMouseEvents(QMouseEvent *event)
{
    // Subclass QPlainTextEdit so that we can call protected functions
    class Hack : public QPlainTextEdit
    {
    public:
        using QPlainTextEdit::mouseDoubleClickEvent;
        using QPlainTextEdit::mouseMoveEvent;
        using QPlainTextEdit::mousePressEvent;
        using QPlainTextEdit::mouseReleaseEvent;
    };

    // Call appropiate function
    auto hack = static_cast<Hack *>(textEdit());
    switch (event->type())
    {
        case QEvent::MouseButtonPress:
            hack->mousePressEvent(event);
            break;
        case QEvent::MouseMove:
            hack->mouseMoveEvent(event);
            break;
        case QEvent::MouseButtonRelease:
            hack->mouseReleaseEvent(event);
            break;
        case QEvent::MouseButtonDblClick:
            hack->mouseDoubleClickEvent(event);
            break;
        default:
            break;
    }
}

/**
 * Hack: call the protected wheel event handler function of the
 *       QPlainTextEdit item
 */
void QmlPlainTextEdit::processWheelEvents(QWheelEvent *event)
{
    // Subclass QPlainTextEdit so that we can call protected functions
    class Hack : public QPlainTextEdit
    {
    public:
        using QPlainTextEdit::wheelEvent;
    };

    // Call wheel event handler
    static_cast<Hack *>(textEdit())->wheelEvent(event);
}

Note: in the processMouseEvents() and processWheelEvents() functions, we are writing potentially illegal C++ code because we are indirectly accessing protected functions of the QPlainTextEdit class. Unfortunately, this is the only way that I managed to get the widget to accept mouse/wheel events. If you have a better solution, you are welcome to share it :)

To use this class from the QML interface, we need to add the following code in the main() function (or before initializing the QML engine):
qmlRegisterType<QmlPlainTextEdit>("QtHacks", 1, 0, "QmlPlainTextEdit");


Finally, here is an example of using QmlPlainTextEdit from QML:
import QtQuick 2.12
import QtQuick.Window 2.12

import QtHacks 1.0

ApplicationWindow {
    visible: true
    width: 640
    height: 480
    title: qsTr("QML PlainTextEdit Example")

    QmlPlainTextEdit {
        anchors.fill: parent
        anchors.margins: 8
    }
}

You can also add functions to read/write widget properties from QML using the Q_PROPERTY macro. You can check the complete QmlPlainTextEdit code in the following links:
  • Console.qml
  • QmlPlainTextEdit.h
  • QmlPlainTextEdit.cpp

Please take into account that these files are specific to Serial Studio, so you will need to modify them to use them in a "portable" manner in your projects.

Finally, you can follow this approach with any QWidget based object that you need to use from your QML interface. I have used this approach with a QTableWidget for a proprietary project without any major issues.
0 Comments

Introducing Serial Studio, a dashboard software for serial port projects

12/29/2020

26 Comments

 
Did you ever have the need to display data from a microcontroller on a dashboard, and spent more time developing (and fixing) your dashboard software, than actually working on your MCU project?

Well, I did, multiple times. Let me put you in context, I participate in several CanSat competition programs through KA'AN SAT, a representative team at my university. A CanSat is “a simulation of a real satellite, integrated within the volume and shape of a soft drink can” (European Space Agency, more info). One of the main tasks in these competitions is to develop software for the ground station. The ground station software (GSS) receives telemetry from the CanSat in real time through a serial device (generally a XBee), displays it and exports it to a CSV/Excel file for post-mission analysis. 

From the start, we developed the GSS with Qt to support multiple operating systems (in case that one of our computers experienced problems during the competition) and because Qt/QML is very convenient for developing eye-catching user interfaces.

Here is a screenshot of the 2019 ground station software, and a photo of the GSS running & displaying telemetry during the CUCEI CanSat competition:
If you are interested, the source code for the 2019 GSS is available here. The software worked quite well for all its intents & purposes (we got the first place after all).

The problems came half a year later, when I found myself working on multiple projects that required some kind of data acquisition with serial devices. For example, some members of ROCH (another representative team in our university, which participates in the NASA Human Rover Exploration Challenge) wanted to integrate our GSS with their rover as a side-project during quarantine.

The result was disastrous; I got a call late at night and we ended up pulling an all-nighter to come up with a way to adapt the GSS with the telemetry that they were receiving. Finally, the software worked, but the UI integration was horrible & data export was not really functional (see the screenshot below to get an idea):
Picture
Both projects (the CanSat and the rover) had a similar telemetry format (sensor readings & OBC status data separated with commas). However, the information itself had a different order.
 
After that experience, I decided that I had enough of writing separate dashboard software for every project that I got involved with. I needed to come up with a way to have the same dashboard/GSS software to work with all projects, without the need of modifying the GSS code at the last minute.
 
The initial solution that I came up with was to create a JSON-based communication protocol between the GSS and the microcontroller, and thus Serial Studio was born (its initial name was SigLAB, but I changed my mind later). 

​Basically, the microcontroller sends the following information through the serial port:
  • Project title.
  • Current sensor readings & OBC status.
  • What each reading meant, its measurement units and what should the GSS do with that reading (e.g. create a real-time graph of the measured atmospheric pressure).

All this information can be easily represented in a JSON document, for example:
{
   "t":"KAANSATQRO",
   "g":[
      {
         "t":"Mission Status",
         "d":[
            {
               "t":"Runtime",
               "v":"%value%",
               "u":"ms"
            },
            {
               "t":"Packet count",
               "v":"%value%"
            },
            {
               "t":"Battery voltage",
               "v":"%value%",
               "g":true,
               "u":"V"
            }
         ]
      },
      {
         "t":"Sensor Readings",
         "d":[
            {
               "t":"Temperature",
               "v":"%value%",
               "g":true,
               "u":"°C"
            },
            {
               "t":"Altitude",
               "v":"%value%",
               "u":"m"
            },
            {
               "t":"Pressure",
               "v":"%value%",
               "u":"KPa",
               "g":true
            },
            {
               "t":"External Temperature",
               "v":"%value%",
               "g":true,
               "u":"°C"
            },
            {
               "t":"Humidity",
               "v":"%9",
               "g":true,
               "u":"%value%"
            }
         ]
      }
   ]
}
​
​As you can see, we have the following structure:
  • Project title
  • Array of data groups
    • For each group:
      • Group title
      • Array of datasets
        • For each dataset:
          • Title
          • Value
          • Units
          • Graph request (or not)

A group consists of values that are closely related to each other, for example:
  • OBC status (first group in our example)
  • Sensor readings (second group in our example)
  • Accelerometer readings (X, Y, Z)
  • GPS readings
  • Etc.

​On the other hand, datasets represent what each individual value means, and what we should do with it.

On Serial Studio, this information is displayed in the following manner:
Picture
As you can probably deduce, each “window” corresponds to a group in our JSON document (I hid the graphs in the screenshot to avoid confusion). 

This approach works beautifully for small projects. However, for more complex projects, creating and sending a large JSON document through serial (or through radio signals, and then through a serial port) becomes quite problematic. The solution? Load the same JSON document from your computer, instruct the microcontroller to send ONLY the sensor/data readings & let Serial Studio figure out the rest by using the indices of each received value in a comma-separated data frame.

Doing so lets you have the best from both worlds:
  • You don’t need to write specific dashboard/GSS software for each project (and you get all the nice features that we described earlier).
  • And you don’t need to create & transmit a large JSON document from your microcontroller (heck, the person working on the microcontroller software doesn't event need to know what JSON is or how it works).
​
A JSON “map” document looks like this:
{
   "t":"%1",
   "g":[
      {
         "t":"Mission Status",
         "d":[
            {
               "t":"Runtime",
               "v":"%2",
               "u":"ms"
            },
            {
               "t":"Packet count",
               "v":"%3"
            },
            {
               "t":"Battery voltage",
               "v":"%4",
               "g":true,
               "u":"V",
               "w":"bar",
               "min":3.6,
               "max":4.3
            }
         ]
      },
      {
         "t":"Sensor Readings",
         "d":[
            {
               "t":"Temperature",
               "v":"%5",
               "g":true,
               "u":"°C",
               "w":"bar",
               "min":0,
               "max":80
            },
            {
               "t":"Altitude",
               "v":"%6",
               "u":"m",
               "w":"bar",
               "min":0,
               "max":3000
            },
            {
               "t":"Pressure",
               "v":"%7",
               "u":"KPa",
               "g":true,
               "w":"bar",
               "min":54,
               "max":102
            },
            {
               "t":"External Temperature",
               "v":"%8",
               "g":true,
               "u":"°C",
               "w":"bar",
               "min":0,
               "max":80
            },
            {
               "t":"Humidity",
               "v":"%9",
               "g":true,
               "u":"%",
               "w":"bar",
               "min":0,
               "max":100
            }
         ]
      },
      {
         "t":"GPS",
         "w":"map",
         "d":[
            {
               "t":"GPS Time",
               "v":"%10"
            },
            {
               "t":"Longitude",
               "v":"%11",
               "u":"°E",
               "w":"lon"
            },
            {
               "t":"Latitude",
               "v":"%12",
               "u":"°N",
               "w":"lat"
            },
            {
               "t":"Altitude",
               "v":"%13",
               "u":"m"
            },
            {
               "t":"No. Sats",
               "v":"%14"
            }
         ]
      },
      {
         "t":"Accelerometer",
         "w":"accelerometer",
         "d":[
            {
               "t":"X",
               "v":"%15",
               "u":"m/s^2",
               "g":true,
               "w":"x"
            },
            {
               "t":"Y",
               "v":"%16",
               "u":"m/s^2",
               "g":true,
               "w":"y"
            },
            {
               "t":"Z",
               "v":"%17",
               "u":"m/s^2",
               "g":true,
               "w":"z"
            }
         ]
      },
      {
         "t":"Gyroscope",
         "w":"gyro",
         "d":[
            {
               "t":"X",
               "v":"%18",
               "u":"°",
               "g":true,
               "w":"yaw"
            },
            {
               "t":"Y",
               "v":"%19",
               "u":"°",
               "g":true,
               "w":"roll"
            },
            {
               "t":"Z",
               "v":"%20",
               "u":"°",
               "g":true,
               "w":"pitch"
            }
         ]
      }
   ]
}

​As you can guess, Serial Studio will replace the %1,%2,%3,...,%20 values with the values at the corresponding index in a comma-separated data frame. The corresponding ​sprintf() format sent by the microcontroller for the given JSON map is:
/*KAANSATQRO,%s,%s,%s,%s,%s,%s,%,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s*/

You may have noticed some "w" keys in some places, these are used to build widgets (which are explained here). Finally, here is the obligatory GIF showing the usage of Serial Studio:
Picture
Pretty cool, right? If you are interested in using Serial Studio for your projects, here are the relevant links:
  • Github repo: https://github.com/Serial-Studio/Serial-Studio/
  • Documentation (GitHub wiki): https://github.com/Serial-Studio/Serial-Studio/wiki/Communication-Protocol​

The prebuilt binaries/installers for Windows, macOS & GNU/Linux are available through GitHub releases: https://github.com/Serial-Studio/Serial-Studio/releases/
​

Minimal example (with Arduino)

Suppose that we want to graph the an ADC reading with an Arduino & export the data to a CSV table. Here is the Arduino code:
#define ADC_PIN A0

void setup() {
   // Initialize Serial port at 9600 bauds
   Serial.begin(9600);
   
   // Configure analog input
   pinMode(ADC_PIN, INPUT);
}

void loop() {
   // Read voltage @ ADC_PIN
   int adc_value = analogRead(ADC_PIN);
   float voltage = adc_value * (5.0 / 1023.0);

   // Send current ms & reading through serial
   Serial.print("/*");        // Frame start sequence  [/*]
   Serial.print(millis());    // Add MCU runtime       [ms]
   Serial.print(",");         // Separator character   [,]
   Serial.print(voltage);     // Add voltage           [V]
   Serial.print("*/");        // Frame finish sequence [*/]
   
   // Wait 50 ms
   delay(50);
}

​Deploy this code to your Arduino and create a JSON file with the following contents:
{
   "t":"Minimal Example",
   "g":[
      {
         "t":"MCU Status",
         "d":[
            {
               "t":"Runtime",
               "v":"%1",
               "u":"ms"
            },
            {
               "t":"ADC reading",
               "v":"%2",
               "g":true,
               "u":"V",
               "w":"bar",
               "min":0,
               "max":5
            }
         ]
      }
   ]
} 

​Open Serial Studio & import the JSON file into Serial Studio by selecting the "manual" radio button in the top-left corner of the app & clicking on the "Change map file" button. Finally, select the appropriate COM port. If everything goes well, you should see a screen similar to this one:
Picture
If you click on the "Open current CSV" button, you will be able to see all the received information in an Excel/Calc table:
Picture
If you have any doubts, ideas or bug reports, feel free to add a comment, contact me or open up an issue at GitHub. Hopefully some random person on the internet will find this useful :)
Download Serial Studio (Windows, macOS & GNU/Linux)
26 Comments

    Alex Spataru

    Website author

    Archives

    March 2021
    December 2020

    Categories

    All
    FOSS
    Hacks
    Microcontrollers
    QML
    Qt

    RSS Feed

Powered by Create your own unique website with customizable templates.
  • Home
  • Blog
  • Projects
    • Serial Studio