QT C++ Practical Guide: Real-Time Communication Host Development

A detailed implementation plan for developing a high-performance real-time communication host, covering multiple communication interfaces, real-time data acquisition and storage, data waveform visualization, and low-latency optimization.

1. System Architecture Design

Utilizing layered architecture + plugin design, the core modules are decoupled, supporting rapid expansion of new communication interfaces. The architecture diagram is as follows:

┌───────────────────────┐
│       Main Interface Layer         │  <- Data waveform visualization, control buttons, log display
├───────────────────────┤
│       Business Logic Layer         │  <- Data caching, protocol parsing, state management
├───────────────────────┤
│       Communication Interface Layer         │  <- USB/BLE/WiFi/Serial plugins (dynamically loaded)
├───────────────────────┤
│       Data Storage Layer         │  <- SQLite/Binary files (asynchronous writing)
└───────────────────────┘

2. Key Technology Implementation

1. Plugin-based Multiple Communication Interfaces (Core Extension Point)

Encapsulating different communication interfaces through Qt plugin mechanism, the main program dynamically loads plugins, supporting hot swapping.

(1) Communication Interface Plugin Specification

Define a unified plugin interface <span>CommunicationPluginInterface</span>, all communication plugins must implement the following functions:

// CommunicationPluginInterface.h
#pragma once
#include <QObject>
#include <QByteArray>
#include <QMetaType>
enum class CommType { USB, BLE, WiFi, SerialPort };
enum class ConnStatus { Disconnected, Connecting, Connected, Error };
class CommunicationPluginInterface {
public:
    virtual ~CommunicationPluginInterface() = default;
    virtual CommType getType() const = 0;                          // Interface type
    virtual bool connect(const QString& params) = 0;               // Connect (parameters like port/address)
    virtual void disconnect() = 0;                                 // Disconnect
    virtual bool sendData(const QByteArray& data) = 0;             // Send data
    virtual QVector<QByteArray> receive() = 0;                     // Receive data (batch)
    virtual ConnStatus getStatus() const = 0;                      // Current status
    virtual QString getStatusDesc() const = 0;                     // Status description
signals:
    void dataReceived(const QByteArray& data);                     // Data arrival signal
    void statusChanged(ConnStatus status, const QString& desc);    // Status change signal
};
Q_DECLARE_INTERFACE(CommunicationPluginInterface, "com.yourcompany.CommPlugin/1.0")
(2) Typical Communication Plugin Implementation (USB Example)

Using the <span>libusb</span> library to implement the USB communication plugin, note the following:

  • Asynchronous IO: Use <span>libusb_hotplug</span> to monitor device plug/unplug events, using a separate thread to handle data read/write.

  • Low latency: Set reasonable timeout values (e.g., <span>LIBUSB_TRANSFER_TIMEOUT_MS=10</span>).

    // UsbPlugin.h
    #pragma once
    #include "CommunicationPluginInterface.h"
    #include <libusb-1.0/libusb.h>
    #include <QThread>
    #include <QQueue>
    #include <QMutex>
    class UsbPlugin : public QObject, public CommunicationPluginInterface {
        Q_OBJECT
        Q_INTERFACES(CommunicationPluginInterface)
    public:
        explicit UsbPlugin(QObject* parent = nullptr);
        ~UsbPlugin();
        CommType getType() const override { return CommType::USB; }
        bool connect(const QString& params) override;   // params format: "vid:pid"
        void disconnect() override;
        bool sendData(const QByteArray& data) override;
        QVector<QByteArray> receive() override;
        ConnStatus getStatus() const override { return m_status; }
        QString getStatusDesc() const override;
    private slots:
        void onUsbDataReceived();  // USB data arrival slot function
    private:
        libusb_device_handle* m_devHandle = nullptr;
        ConnStatus m_status = ConnStatus::Disconnected;
        QThread* m_workThread;     // Work thread (handles USB IO)
        QQueue<QByteArray> m_rxBuffer;  // Receive buffer (thread-safe)
        QMutex m_bufferMutex;      // Buffer lock
    };

(3) Dynamically Loading Plugins in the Main Program

In <span>MainWindow</span>, scan the plugin directory and load all compliant communication plugins:

// MainWindow.cpp
void MainWindow::loadCommunicationPlugins() {
    QString pluginPath = QCoreApplication::applicationDirPath() + "/plugins";
    QDir dir(pluginPath);
    foreach (QString fileName, dir.entryList(QDir::Files)) {
        QPluginLoader loader(dir.filePath(fileName));
        QObject* plugin = loader.instance();
        if (plugin) {
            CommunicationPluginInterface* commPlugin = 
                 qobject_cast<CommunicationPluginInterface*>(plugin);
            if (commPlugin) {
                m_plugins.append(commPlugin);
                // Add to communication type dropdown
                ui->cbCommType->addItem(commPlugin->getTypeName(), QVariant::fromValue(commPlugin));
                // Connect signals
                connect(commPlugin, &CommunicationPluginInterface::dataReceived,
                        this, &MainWindow::onDataReceived);
                connect(commPlugin, &CommunicationPluginInterface::statusChanged,
                        this, &MainWindow::onCommStatusChanged);
            }
        }
    }
}

2. Real-Time Data Acquisition and Storage (High Throughput Optimization)

Utilizing the producer-consumer model, separating data acquisition, processing, and storage processes to ensure no packet loss under high throughput.

(1) Data Acquisition Thread

Each communication plugin runs in a separate thread, responsible for real-time data reading and placing it into a shared queue:

// UsbPluginWorkThread.h (USB plugin work thread)
#pragma once
#include <QThread>
#include <QQueue>
#include <QMutex>
#include <QWaitCondition>
class UsbPlugin;
class UsbPluginWorkThread : public QThread {
    Q_OBJECT
public:
    explicit UsbPluginWorkThread(UsbPlugin* plugin, QObject* parent = nullptr);
    void stop() { m_stop = true; wait(); }
protected:
    void run() override;
private:
    UsbPlugin* m_plugin;
    bool m_stop = false;
    QQueue<QByteArray> m_rxQueue;  // Receive queue (thread-safe)
    QMutex m_queueMutex;
    QWaitCondition m_queueCond;
};
(2) Asynchronous Data Storage

Using SQLite + asynchronous writing, to avoid blocking the main thread. Improve writing efficiency through <span>QSqlQuery</span> prepared statements:

// DataStorage.h
#pragma once
#include <QObject>
#include <QSqlDatabase>
#include <QSqlQuery>
#include <QSqlError>
#include <QVector>
class DataStorage : public QObject {
    Q_OBJECT
public:
    static DataStorage* getInstance() {
        static DataStorage instance;
        return &instance;
    }
    bool init(const QString& dbPath = "data.db") {
        QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE");
        db.setDatabaseName(dbPath);
        if (!db.open()) {
            qDebug() << "Database open failed:" << db.lastError().text();
            return false;
        }
        // Create data table (timestamp, data length, raw data)
        QSqlQuery query;
        return query.exec("CREATE TABLE IF NOT EXISTS DataLog ("                          "id INTEGER PRIMARY KEY AUTOINCREMENT, "                          "timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, "                          "data_len INTEGER, "                          "raw_data BLOB)");
    }
    void asyncSave(const QByteArray& data) {
        // Use QtConcurrent for asynchronous writing
        QtConcurrent::run([data]() {
            QSqlQuery query;
            query.prepare("INSERT INTO DataLog (data_len, raw_data) VALUES (?, ?)");
            query.addBindValue(data.size());
            query.addBindValue(data);
            if (!query.exec()) {
                qDebug() << "Data storage failed:" << query.lastError().text();
            }
        });
    }
private:
    DataStorage() = default;
};
(3) Data Synchronization and Throttling
  • Shared Queue: Use <span>QQueue</span> + <span>QMutex</span> + <span>QWaitCondition</span> to implement a thread-safe circular buffer, avoiding memory overflow.

  • Flow Control: When the queue size exceeds a threshold (e.g., 10MB), discard old data or notify the acquisition end to slow down.

3. Data Waveform Visualization (Low Latency Rendering)

Using Qt custom controls + double buffering drawing, to achieve millisecond-level real-time waveform updates.

(1) Custom Waveform Control

Inherit from <span>QWidget</span>, override <span>paintEvent</span>, using <span>QImage</span> as a background buffer to reduce interface redraw times:

// WaveformWidget.h
#pragma once
#include <QWidget>
#include <QVector>
#include <QImage>
class WaveformWidget : public QWidget {
    Q_OBJECT
public:
    explicit WaveformWidget(QWidget* parent = nullptr);
    void updateData(const QVector<quint16>& newData);  // Update data
protected:
    void paintEvent(QPaintEvent* event) override;
private:
    QVector<quint16> m_data;       // Current display data
    QImage m_backBuffer;           // Background buffer
    int m_maxValue = 4096;         // Maximum data value (adjust based on sensor)
    int m_minValue = 0;
};
// WaveformWidget.cpp
void WaveformWidget::updateData(const QVector<quint16>& newData) {
    m_data = newData;
    update();  // Trigger redraw
}
void WaveformWidget::paintEvent(QPaintEvent* event) {
    Q_UNUSED(event);
    QPainter painter(this);
    painter.setRenderHint(QPainter::Antialiasing);
    // Initialize background buffer (only on first or size change)
    if (m_backBuffer.size() != size()) {
        m_backBuffer = QImage(size(), QImage::Format_RGB32);
        m_backBuffer.fill(Qt::black);
    }
    // Draw waveform (double buffering)
    QPainter bufferPainter(&m_backBuffer);
    bufferPainter.setPen(QPen(Qt::green, 1));
    int width = this->width();
    int height = this->height();
    int midY = height / 2;
    // Draw grid
    bufferPainter.setPen(QPen(Qt::darkGreen, 0.5));
    for (int x = 0; x < width; x += 20) bufferPainter.drawLine(x, 0, x, height);
    for (int y = 0; y < height; y += 20) bufferPainter.drawLine(0, y, width, y);
    // Draw waveform
    bufferPainter.beginPath();
    for (int i = 0; i < m_data.size(); ++i) {
        int x = static_cast<int>(static_cast<double>(i) / m_data.size() * width);
        int y = midY - static_cast<int>(static_cast<double>(m_data[i] - m_minValue) / (m_maxValue - m_minValue) * midY);
        if (i == 0) bufferPainter.moveTo(x, y);
        else bufferPainter.lineTo(x, y);
    }
    bufferPainter.strokePath();
    // Draw the background buffer to the screen
    painter.drawImage(0, 0, m_backBuffer);
}
(2) Real-Time Update Optimization
  • Data Downsampling: When the sampling rate is high (e.g., 1MHz), average or sample the data to reduce the amount of data drawn.

  • Timed Refresh: Use <span>QTimer</span> to control the refresh frequency (e.g., 50ms), balancing real-time performance and CPU usage.

    // Start timer in MainWindow
    m_refreshTimer = new QTimer(this);
    m_refreshTimer->setInterval(50);  // 20Hz refresh
    connect(m_refreshTimer, &QTimer::timeout, this, [this]() {
        if (m_currentPlugin) {
            auto data = m_currentPlugin->receive();
            QVector<quint16> displayData = downsample(data, 100);  // Downsample to 100 points
            ui->waveformWidget->updateData(displayData);
        }
    });
    m_refreshTimer->start();

4. Low Latency Optimization Strategies

  • Multithreaded Architecture: Separate communication, acquisition, storage, and rendering into different threads to avoid blocking the main thread.

  • Zero-Copy Data Transfer: Use <span>QSharedMemory</span> or <span>std::shared_ptr</span> to reduce data copying.

  • Memory Pool: Pre-allocate fixed-size memory blocks (e.g., circular buffer) to avoid dynamic memory allocation.

Leave a Comment