building a qt 6.2 application — beginning usb comms

My Qt 6 application isn’t simple any more. It’s grown more complicated as I continue to add features and evolve it towards what I finally want. In this post I’m adding very basic serial communications via USB to my attached development boards. For this post I’m only communicating with the Raspberry Pi Pico W. I’ll be using QSerialPort to develop that functionality. I’ll show what additional source and libraries I had to install, the core modified code, and the application I had running on the Pico W to produce test output.

First things first. I had to install the headers and library files to support developing with Qt’s QSerialPort. Those extra packages are libqt6serialport6 and libqt6serialport6-dev. This will put the necessary header files down /usr/include/x86_64-linux-gnu/qt6/QtSerialPort/. You’ll also need to link against Qt6SerialPort.

Here’s the modified code module. I’ll highlight the pertinent sections after the code block.

#include <QStringList>#include <QHeaderView>#include <QTableWidgetItem>#include <QSplitter>#include <QPalette>#include <QSerialPort>#include <QByteArray>#include <QScrollBar>#include <QFont>#include "MainWindow.hpp"#include "devices.hpp"void MainWindow::addCentralWidget() {console = new QPlainTextEdit(this);QPalette colors = palette();colors.setColor(QPalette::Base, Qt::black);colors.setColor(QPalette::Text, Qt::white);QFont font("Monospace", 12);font.setStyleStrategy(QFont::PreferAntialias);console->setPalette(colors);console->setFont(font);serialPort = new QSerialPort(this);connect(serialPort, &QSerialPort::readyRead, this, &MainWindow::readSerialData);serialPort->setPortName("/dev/ttyACM0");serialPort->setBaudRate(QSerialPort::Baud115200);serialPort->setDataBits(QSerialPort::Data8);serialPort->setParity(QSerialPort::NoParity);serialPort->open(QIODevice::ReadOnly);table = new QTableWidget(this);table->setColumnCount(3);table->setRowCount(6);QStringList headers = {"Device", "Adapter Name", "Hex Identification"};table->setHorizontalHeaderLabels(headers);table->horizontalHeader()->setStretchLastSection(true);int row{0};for (auto [device, values] : getDevices()) {QString dev(device.c_str());QString v1(values[0].c_str());QString v2(values[1].c_str());table->setItem(row, 0, new QTableWidgetItem(dev));table->setItem(row, 1, new QTableWidgetItem(v1));table->setItem(row, 2, new QTableWidgetItem(v2));++row;}table->resizeColumnsToContents();QSplitter *splitter = new QSplitter(Qt::Vertical, this);splitter->addWidget(table);splitter->addWidget(console);setCentralWidget(splitter);}void MainWindow::readSerialData() {QByteArray data = serialPort->readAll();console->insertPlainText(data);QScrollBar *scrollBar = console->verticalScrollBar();scrollBar->setValue(scrollBar->maximum());}

The text box is being used again as a console. Starting at line 15, we set up the foreground and background colors and the font we want to see any text in. This gives us a console with white text on a black background, with a 12pt Monospaced font using anti-aliasing. The default font is horrible.

Lines 24 through 27 are critical to this working. While Qt calls the ability to handle callbacks as signals (the call) and slots (the callback), this is their nomenclature for a feature as old as C, which is the ability to call pointers to functions. This was a feature in the Unix widget toolkit, Motif, in which controls had the ability to be programatically configured with pointers to functions that were called when a given control was activated. Line 26 sets up the callback (MainWindow::readSerialData) to be called by the serialPort when any data is present. The callback reads that data and places it to be displayed on the console. This is all done asynchronously such that the GUI event queue isn’t blocked. This is very similar to what Java tries to do with its GUI toolkits. The callback is in lines 61-66. When the application is running, it will open the port to the Pico W and then begin to read the data that the Pico W sends out.

These are early days for this. I only wrote enough to get the callback going and to read from the port. You’ll note that the port is hard coded for the Pico W. In the future I’ll be able to pick any port just by clicking on the entry in the upper table and the console will be fully bi-directional. I’m also going to add the ability to log everything in the console. As a stretch goal I’d like to be able to edit Micropython source in the attached microcontroller as well. But that’s a stretch goal which is going to require almost as much work as all the work leading up to finishing the first three goals.

Now for the interesting part. The Pico W is running Circuit Python 8 beta 6.

# SPDX-FileCopyrightText: 2022 Liz Clark for Adafruit Industries## SPDX-License-Identifier: MITimport osimport timeimport sslimport wifiimport socketpoolimport microcontrollerimport adafruit_requestsimport jsonimport gc# Adafruit quotes URL#quotes_url = "https://www.adafruit.com/api/quotes.php"# Connect to your local hostspot.# WIFI_SSID and WIFI_PASSWORD should be defined in your settings.toml# file.#wifi.radio.connect(os.getenv('WIFI_SSID'), os.getenv('WIFI_PASSWORD'))pool = socketpool.SocketPool(wifi.radio)session = adafruit_requests.Session(pool, ssl.create_default_context())print("Fetching quotes from ", quotes_url)print()while True:try:time.sleep(5)gc.collect()# Get a quote.#response = session.get(quotes_url)dic = dict(json.loads(response.text)[0])qstr = dic['text'] + "."print("   " + qstr)print(".. " + dic['author'])print("")response.close()gc.collect()# pylint: disable=broad-exceptexcept ValueError as e:print("Error: bad JSON")response.close()except Exception as e:print("Error:\n", str(e))print("Resetting microcontroller in 10 seconds")time.sleep(10)microcontroller.reset()

The original code came from Adafruit. I cleaned up the while loop to make the output more readable. The original code just spat out the JSON strings that came back from the URL. I’ve done some formatting and cleanup instead.

That’s it for the time being. I’ll continue to clean up the C++ code. Hopefully I’ll have more progress to post by the weekend.

building a simple qt 6.2 application — adding a dock and a table

My Qt 6.2 application continues to evolve. I’ve now named it USB Device Monitor, or udm. It has a dock that can be switched to either left or right side and a table in the center with specific Linux information replacing the text editing widget I had before. All of this is building on from the basic plumbing entry and my earlier utility to read Linux USB devices (see links below).

Comments will be shorter than usual, with code more than commentary.

cmake_minimum_required (VERSION 3.25.0)## Build requirements#include_directories( /usr/include/x86_64-linux-gnu/qt6/ /usr/include/x86_64-linux-gnu/qt6/QtCore/ /usr/include/x86_64-linux-gnu/qt6/QtGui/ /usr/include/x86_64-linux-gnu/qt6/QtWidgets/ )link_libraries( Qt6Core Qt6Widgets Qt6Gui )add_compile_options( -fPIC -std=c++20 )## Project specific#project (usb-device-monitor)add_executable (udmmain.cppMainWindow.cppAddCentralWidget.cppAddWindowFileMenu.cppAddDockingPanel.cppdevices.cpp)

The last third of the CMake file shows that MainWindow is a rename of AppWindow, and three new files have been added, AddCentralWidget, AddDockingPanel and devices.

#pragma once#include <QApplication>#include <QMainWindow>#include <QMenuBar>#include <QPlainTextEdit>#include <QSettings>#include <QString>#include <QDockWidget>#include <QCheckBox>#include <QComboBox>#include <QTableWidget>class AppWindow : public QMainWindow {public:const QString SCOPE_NAME{"Qt6BasedSoftware"};const QString APP_NAME{"USBDeviceMonitor"};const QString APP_GEOMETRY{"app/geometry"};const QString APP_STATE{"app/state"};const int INITIAL_WIDTH{900};const int INITIAL_HEIGHT{600};AppWindow();private:QTableWidget *table;QPlainTextEdit *textEdit;void addCentralWidget();void createFileMenu();QMenu *fileMenu;QAction *newAction;QAction *openAction;QAction *saveAction;QAction *saveAsAction;QAction *aboutAction;QAction *exitAction;void addDockingPanel();QDockWidget *dock;QCheckBox*cb1,*cb1x,*cb2,*cb2x,*cb3,*cb3x,*cb4,*cb4x;QComboBox *comboBox;void closeEvent(QCloseEvent*);};
#include <QScreen>#include <QSize>#include <QStatusBar>#include "MainWindow.hpp"AppWindow::AppWindow() {QCoreApplication::setOrganizationName(SCOPE_NAME);QCoreApplication::setApplicationName(APP_NAME);setWindowTitle(APP_NAME);addCentralWidget();createFileMenu();addDockingPanel();statusBar()->showMessage(tr("Statusbar Placeholder"));QSettings settings(QCoreApplication::organizationName(),QCoreApplication::applicationName());if (settings.contains(APP_GEOMETRY) and settings.contains(APP_STATE)) {restoreGeometry(settings.value(APP_GEOMETRY).toByteArray());restoreState(settings.value(APP_STATE).toByteArray());cb1->setChecked(settings.value("dock/cb1").toBool());cb2->setChecked(settings.value("dock/cb2").toBool());cb3->setChecked(settings.value("dock/cb3").toBool());cb4->setChecked(settings.value("dock/cb4").toBool());comboBox->setCurrentIndex(settings.value("dock/combo1").toInt());return;}QScreen *primaryScreen = QGuiApplication::primaryScreen();QSize screenSize = primaryScreen->availableSize();auto screenWidth  = screenSize.width();auto screenHeight = screenSize.height();auto upperLeftX  = (screenWidth - INITIAL_WIDTH)/2;auto upperLeftY = (screenHeight - INITIAL_HEIGHT)/2;setGeometry(upperLeftX, upperLeftY, INITIAL_WIDTH, INITIAL_HEIGHT);}void AppWindow::closeEvent(QCloseEvent *event) {QSettings settings(QCoreApplication::organizationName(),QCoreApplication::applicationName());settings.setValue(APP_GEOMETRY, saveGeometry());settings.setValue(APP_STATE, saveState());settings.setValue("dock/cb1",cb1->isChecked());settings.setValue("dock/cb2",cb2->isChecked());settings.setValue("dock/cb3",cb3->isChecked());settings.setValue("dock/cb4",cb4->isChecked());settings.setValue("dock/combo1", comboBox->currentIndex());QMainWindow::closeEvent(event);}

The biggest changes in MainWindow besides the renaming is in the constructor and closeEvent. The constructor now calls external class functions to build the subsystems used in the application, and then recalls saved state in order to put the application back the way it was when it was closed the time before. I made a change to the conditional if block by removing the else and adding a return at the end of the if block. I’ve developed an aversion to cascading conditional statements and the “pyramid of doom” that they can create. I now organize my tests such that if I need to check for the health of the application then I’ll check that first and bail if there’s a problem, or as in this case, put the most common action that should occur nearly all the time, perform that task and then leave. The use of if/else blocks might look proper to purists, but I’m no longer a purist.

The closeEvent now does a bit of state saving. I didn’t mention it in the last post, but for Linux the save file is located in $HOME/.config/Qt6BasedSoftware/ USBDeviceMonitor.conf. That folder and file are defined in SCOPE_NAME and APP_NAME, respectively, in MainWindow.hpp, lines 16 and 17.

#include <QStringList>#include <QHeaderView>#include <QTableWidgetItem>#include "MainWindow.hpp"#include "devices.hpp"void AppWindow::addCentralWidget() {table = new QTableWidget(this);table->setColumnCount(3);table->setRowCount(6);QStringList headers = {"Device", "Adapter Name", "Hex Identification"};table->setHorizontalHeaderLabels(headers);table->horizontalHeader()->setStretchLastSection(true);int row{0};for (auto [device, values] : getDevices()) {QString dev(device.c_str());QString v1(values[0].c_str());QString v2(values[1].c_str());table->setItem(row, 0, new QTableWidgetItem(dev));table->setItem(row, 1, new QTableWidgetItem(v1));table->setItem(row, 2, new QTableWidgetItem(v2));++row;}table->resizeColumnsToContents();setCentralWidget(table);}

The class function addCentralWidget creates a three column by six row table, looking pretty much like a spreadsheet, to contain the data that the class function getDevices finds in the Linux operating system. The first half of this function sets up the table while the second half formats the data returned by the call to getDevices. This is the minimum code necessary to just display the data. No other functionality is enabled.

#pragma once#include <array>#include <map>#include <string>#include <iostream>typedef std::map<std::string, std::array<std::string,2>> device_collection;device_collection getDevices();
#include <algorithm>#include <filesystem>#include <regex>#include "devices.hpp"using std::array;using std::string;using std::pair;using std::regex;using std::regex_search;using std::replace;using std::smatch;using std::filesystem::directory_iterator;using std::filesystem::path;using std::filesystem::read_symlink;void split(const string &subj, const regex &rgx, array<string, 2> &vars) {smatch match;regex_search(subj, match, rgx);vars[0] = subj.substr(0, match.position(0));replace(vars[0].begin(), vars[0].end(), '_', ' ');vars[1] = match.str(0).erase(0,1);vars[1].erase(vars[1].end()-1);}device_collection getDevices() {const string DEVICE_PATH{"/dev/serial/by-id/"};const regex hregex{"_[0-9A-Fa-f]+-"};device_collection devices;for (const auto &entry : directory_iterator(DEVICE_PATH)) {if (entry.is_symlink()) {string slink = read_symlink(entry).stem().generic_string();string sname = entry.path().stem().generic_string().erase(0,4);array<string, 2> results;split(sname, hregex, results);devices[slink] = results;}}return devices;}

This is the utility I wrote about a week back where I was trying to write the same utility in Python, C++, and Rust. The original C++ utility was stand-alone. I removed the main and the printout, and just converted it into a function that returned a collection of the processed data to be displayed in the QTableWidget.

#include <QGroupBox>#include <QVBoxLayout>#include <QDockWidget>#include <QGridLayout>#include "MainWindow.hpp"void AppWindow::addDockingPanel() {dock = new QDockWidget(tr("Docking Panel"), this, Qt::Widget);dock->setObjectName(tr("Docking_Panel"));dock->setFeatures(QDockWidget::DockWidgetMovable);dock->setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea);addDockWidget(Qt::LeftDockWidgetArea, dock);QWidget *mainWidget = new QWidget;QGridLayout *grid = new QGridLayout;mainWidget->setLayout(grid);QGroupBox *checkboxSelection = new QGroupBox(tr("Non-exclusive Checkboxes"));QVBoxLayout *cslayout = new QVBoxLayout;cb1 = new QCheckBox(tr("One"), this);cslayout->addWidget(cb1, 0, Qt::AlignLeft);cb2 = new QCheckBox(tr("Two"), this);cslayout->addWidget(cb2, 1, Qt::AlignLeft);cb3 = new QCheckBox(tr("Three"), this);cslayout->addWidget(cb3, 2, Qt::AlignLeft);cb4 = new QCheckBox(tr("Four"), this);cslayout->addWidget(cb4, 3, Qt::AlignLeft);checkboxSelection->setLayout(cslayout);grid->addWidget(checkboxSelection, 0, 0);QGroupBox *groupBox = new QGroupBox(tr("Dropdown"), dock);QVBoxLayout *layout = new QVBoxLayout();comboBox = new QComboBox();comboBox->addItem(tr("One"));comboBox->addItem(tr("Two"));comboBox->addItem(tr("Three"));comboBox->addItem(tr("Four"));layout->addWidget(comboBox);groupBox->setLayout(layout);grid->addWidget(groupBox, 1, 0);QWidget *filler = new QWidget;QVBoxLayout *flayout = new QVBoxLayout;flayout->addSpacerItem(new QSpacerItem(20, 800, QSizePolicy::Minimum,QSizePolicy::Expanding));filler->setLayout(flayout);grid->addWidget(filler);dock->setWidget(mainWidget);}

This creates a dockable panel that can be moved either to the right or the left of the main application. It does nothing but display a few controls. QDockWidget has changed a bit since Qt 4. In particular, I had to create a single widget to set within the QDockWidget instance; that’s what mainWidget is for. I also had to create a special widget to encapsulate the QSpacerItem in order for all the other widgets to line up properly and not waste space on the dock.

The class function createFileMenu hasn’t changed.

Not much to say at this point except that now I need to decide what real functionality I want to enable in this monitor, and then add controls that actually do something. A bit more state is going to have to be preserved. The utility is going to have to monitor the USB devices to indicate when devices are added or removed. And I’m going to want to add a column to each row to hold additional user added information (text). So a bit more work to do.

Links

building a simple qt 6 application — basic plumbing

python vs c++ vs rust — a personal adventure