Blocking serial port. QSerialPort + QThread.

I had the task of writing software for controlling the radiator of the X-ray. Namely: realize the protocol of data transfer between the PC and the radiator of the X-ray and create the user-defined functions "Set parameters", "Enable X-ray", "Switch off X-ray".

The X-ray radiator is controlled by data transfer via the serial port, which is also called a COM port, but it is applicable only for Windows.

Qt has the QSerialPort class, which provides functions for accessing the serial port.
I want to show you my implementation of this task.


Project structure

Emitter.pro - project profile

Headers:

  • emitter.h - header file of the emitter class.
  • mainwindow.h - header file of the main program window.

Sources:

  • emitter.cpp - implementation file of the emitter class.
  • main.cpp
  • mainwindow.cpp - implementation file of the main program window.

Forms:

  • mainwindow.ui - file form of the main program window

Emitter class

In the project's profile, we add serialport to use the QtSerialPort module.

QT       += core gui serialport

emitter.hpp

#ifndef EMITTER_H
#define EMITTER_H

#include <QSerialPort>
#include <QTimer>

class Emitter : public QObject
{
    Q_OBJECT
public:
    explicit Emitter(QObject *parent = 0);
    // Connection Test Function.
    bool isConnected() const;

public slots:
    // Slot for setting parameters.
    void setFeatures(int voltage, int current, int workTime, int coolTime);
    // X-ray inclusion slot.
    void turnOnXRay();
    // X-ray shutdown slot.
    void turnOffXRay();

private:
    enum class Command: quint8; // Announcement of the transfer
    // Connection function.
    void connectToEmitter();
    // The most important function that implements the exchange protocol with the emitter.
    QByteArray writeAndRead(Command com, quint8 y1 = 0, quint8 y2 = 0, quint8 *m = nullptr);
    // Emitter command: Emitter status.
    bool commandS();
    // Emitter command: Set parameters.
    void commandP(quint16 volt, quint16 curr, quint16 workTime, quint16 coolTime);
    // Emitter command: Switch on the emitter.
    void commandN();
    // Emitter command: Switch off the emitter.
    void commandF();

    const quint8 STARTBYTE = 0x40;  // Beginning of the parcel.
    const quint8 DEV = 0;  // Emitter ID.

    // Available commands sent to the radiator. It is sent by the third byte in the parcel.
    enum class Command : quint8
    {
        S = 0x53, // Command request status.
        P = 0x50, // Command for setting parameters.
        N = 0x4e, // Command to turn on the X-ray.
        F = 0x46  // X-ray shutdown command.
    };

    // Emitter messages.
    enum class MessageCommandS : quint8
    {
        OK = 0x00, // All Oк.
        XRAY_ON = 0x01, // Emission included.
        XRAY_STARTING = 0x02, // Emitter output to the mode.
        XRAY_TRAIN = 0x03, // There is training.
        COOLING = 0x04 // Cooling.
    };

    // Emitter errors.
    enum class ErrorCommandS : quint8
    {
        NO_ERROR = 0x00, // Without errors
        XRAY_CONTROL_ERROR = 0x01, // X-ray tube control error.
        MODE_ERROR = 0x02, // Error setting the specified mode.
        VOLTAGE_ERROR = 0x03, // Exceeding the voltage threshold.
        CURRENT_ERROR = 0x04, // Exceed the current threshold.
        PROTECTIVE_BOX_ERROR = 0x05, // The protective box is open.
        LOW_SUPPLY_VOLTAGE = 0x06, // Low supply voltage.
        DISCONNECTION = 0x07, // No confirmation of connection (more than 1 s)
        OVERHEAT = 0x08 // Overheat.
    };

    QSerialPort *m_pSerialPort;
    bool m_isConnected;

    QTimer *m_pTimerCheckConnection;
};

#endif // EMITTER_H

"Communication" with the radiator occurs according to a certain protocol.

Brief description of the radiator protocol:

  1. The host is the computer. He is always the first to send a command. When receiving each parcel, the microprocessor of the device must send back the corresponding response command.
  2. Each packet isas follows:
    @ dev com y1 y2 m1 ... mn CRC
    @ - start byte, the sign of the beginning of the transmission (always equal to 0x40).
    dev - Device ID. ID controller and computer must be different.
    com is a command.
    y1 - The least significant byte of the word that determines the number of data bytes to send.
    y2 - high byte of a word. which determines the number of data bytes sent.
    m1 - first byte of data.
    mn - the last byte of the data.
    CRC -byte of the checksum. The checksum is considered as the sum of all bytes, beginning with the first byte of the instruction and ending with the last byte of the data.

Work with the serial port will be in synchronous mode, i.e. using the write and waitForBytesWritten functions for writing and waitForReadyRead and readAll for reading.

emitter.cpp

#include "emitter.h"

#include <QDebug>
#include <QThread>

Emitter::Emitter(QObject *parent) : QObject(parent)
{
    // Инициализация последовательного порта.
    m_pSerialPort = new QSerialPort(this);
    m_pSerialPort->setPortName("COM3");
    // Скорость передачи данных. 19200 бит/с.
    m_pSerialPort->setBaudRate(QSerialPort::Baud19200);
    m_pSerialPort->setDataBits(QSerialPort::Data8);
    m_pSerialPort->setParity(QSerialPort::NoParity);
    m_pSerialPort->setStopBits(QSerialPort::OneStop);
    m_pSerialPort->setFlowControl(QSerialPort::NoFlowControl);

    /* In this radiator with the included X-ray, it is necessary, at least once a second,
     send a command to the status request to confirm that the connection is not disconnected.
     Otherwise, the x-ray will be turned off. To do this, create a timer with an interval of 1 second. */
    m_pTimerCheckConnection = new QTimer(this);
    m_pTimerCheckConnection->setInterval(1000);

    /* After a time of 1 s, the status request command is called.
     This is where the lambda function is used, not to create a slot.
     It would be possible to make commandS private slot, but in this case at connect
     through the old form of recording (on SIGNAL, SLOT)
     This slot would be accessible to external objects. */
    connect(m_pTimerCheckConnection, &QTimer::timeout, [this](){
        commandS();
    });

    // We connect the serial port.
    connectToEmitter();
}

bool Emitter::isConnected() const
{
    return m_isConnected;
}

void Emitter::setFeatures(int voltage, int current, int workTime, int coolTime)
{
    if (isConnected())
    {
        commandP(voltage, current, workTime, coolTime);
    }
}

void Emitter::turnOnXRay()
{
    if (isConnected())
    {
        commandN();
    }
}

void Emitter::turnOffXRay()
{
    if (isConnected())
    {
        commandF();
    }
}

void Emitter::connectToEmitter()
{
    if (m_pSerialPort->open(QSerialPort::ReadWrite))
    {
        // We are convinced that the radiator of the X-ray is connected to the serial port.
        m_isConnected = commandS();
        if (m_isConnected)
        {
            qDebug() << "The radiograph of the X-ray is connected.";
        }
        else
        {
            qDebug() << "A different device is connected to the serial port of the radiator of the X-ray!";
        }
    }
    else
    {
        qDebug() << "The serial port is not connected. ";
         m_isConnected = false;
    }
}

QByteArray Emitter::writeAndRead(Command com, quint8 y1, quint8 y2, quint8 *m)
{
    quint16 y = y2 * 256 + y1;

    // Data sent to the serial port.
    QByteArray sentData;
    sentData.resize(6 + y);
    sentData[0] = STARTBYTE;
    sentData[1] = DEV;

    // Checksum = the sum of all bytes, starting with the byte of the command.
    quint8 crc = 0;
    crc += sentData[2] = static_cast<quint8>(com);
    crc += sentData[3] = y1;
    crc += sentData[4] = y2;

    // If there are data bytes, then they are also included in the checksum.
    if (y && m)
    {
        for (int i = 0; i < y; ++i)
        {
            crc += sentData[5 + i] = m[i];
        }
    }

    // The last byte is the checksum.
    sentData[5 + y] = crc;

    // Write to the serial port and wait for up to 100 ms until the recording is done.
    m_pSerialPort->write(sentData);
    m_pSerialPort->waitForBytesWritten(100);

    // We go to sleep, waiting for the microcontroller of the radiator to process the data and answer.
    this->thread()->msleep(50);
    // We read the data from the emitter.
    m_pSerialPort->waitForReadyRead(50);
    return m_pSerialPort->readAll();
}

bool Emitter::commandS() // Command request status.
{
    // We send the emitter.
    QByteArray receivedData = writeAndRead(Command::S);

    /* Must come exactly 8 bytes.
     The answer should be: @ dev com 2 0 s e CRC.
     Otherwise the answer did not come from the radiator. */
    if (receivedData.size() == 8)
    {
        quint8 crc = 0;
        for (int i = 1; i < receivedData.size() - 1; ++i)
        {
            crc += receivedData[i];
        }
        if (crc != static_cast<quint8>(receivedData[receivedData.size() - 1]))
            qDebug() << "CRC does not match!" << crc << static_cast<quint8>(receivedData[receivedData.size() - 1]);

        // If the condition is correct, then our radiographer of the X-ray replied.
        if (receivedData[0] == STARTBYTE &&
            static_cast<Command>(receivedData.at(2)) == Command::S &&
            receivedData[3] == 2 &&
            receivedData[4] == 0)
        {
            MessageCommandS message = static_cast<MessageCommandS>(receivedData.at(5));
            switch (message)
            {
            case MessageCommandS::OK:
                qDebug() << "Everything is fine.";
                break;
            case MessageCommandS::XRAY_ON:
                qDebug() << "Radiation is on.";
                break;
            case MessageCommandS::XRAY_STARTING:
                qDebug() << "Output of radiation to the mode.";
                break;
            case MessageCommandS::XRAY_TRAIN:
                qDebug() << "There is training.";
                break;
            case MessageCommandS::COOLING:
                qDebug() << "Cooling.";
                break;
            }
            ErrorCommandS error = static_cast<ErrorCommandS>(receivedData.at(6));
            switch (error)
            {
            case ErrorCommandS::NO_ERROR:
                qDebug() << "No mistakes";
                break;
            case ErrorCommandS::XRAY_CONTROL_ERROR:
                qDebug() << "X-ray tube control error.";
                break;
            case ErrorCommandS::MODE_ERROR:
                qDebug() << "It is not possible to set the preset mode on the handset.";
                break;
            case ErrorCommandS::VOLTAGE_ERROR:
                qDebug() << "The voltage threshold is exceeded.";
                break;
            case ErrorCommandS::CURRENT_ERROR:
                qDebug() << "The current threshold is exceeded.";
                break;
            case ErrorCommandS::PROTECTIVE_BOX_ERROR:
                qDebug() << "The protective box is open.";
                break;
            case ErrorCommandS::LOW_SUPPLY_VOLTAGE:
                qDebug() << "Low supply voltage.";
                break;
            case ErrorCommandS::DISCONNECTION:
                qDebug() << "Lack of communication with the host (more than 1s).";
                break;
            case ErrorCommandS::OVERHEAT:
                qDebug() << "Overheat.";
                break;
            }
            // Return true, because the answer came from the radiator. 
            return true;
        }
    }
    else
    {
        qDebug() << "Error. The answer does not meet expectations.";
    }
    return false;
}

// Command to set parameters.
void Emitter::commandP(quint16 volt, quint16 curr, quint16 workTime, quint16 coolTime)
{
    quint8 m[8];

    // 8 bytes of data, 4 words. The first low byte of the word, the second highest byte of the word.
    m[0] = volt - (volt/256) * 256;
    m[1] = volt/256;
    m[2] = curr - (curr/256) * 256;
    m[3] = curr/256;
    m[4] = workTime - (workTime/256) * 256;
    m[5] = workTime/256;
    m[6] = coolTime - (coolTime/256) * 256;
    m[7] = coolTime/256;
    QByteArray receivedData = writeAndRead(Command::P, 8, 0, m);

    /* Must come exactly 14 bytes.
     The answer should be: @ dev com 8 0 volt curr workTime coolTime CRC.
     volt, curr, workTime, coolTime - words in 2 bytes.
     Otherwise the answer did not come from the radiator. */
    if (receivedData.size() == 14)
    {
        quint8 crc = 0;
        for (int i = 1; i < receivedData.size() - 1; ++i)
        {
            crc += receivedData[i];
        }
        if (crc != static_cast<quint8>(receivedData[receivedData.size() - 1]))
            qDebug() << "CRC does not match!" << crc << static_cast<quint8>(receivedData[receivedData.size() - 1]);

        // The response from the radiator is the current parameter values.
        if (receivedData[0] == STARTBYTE &&
            static_cast<Command>(receivedData.at(2)) == Command::P &&
            receivedData[3] == 8 &&
            receivedData[4] == 0)
        {
            quint16 receivedVolt = receivedData[5] + receivedData[6] * 256;
            quint16 receivedCurr = receivedData[7] + receivedData[8] * 256;
            quint16 receivedWorkTime = receivedData[9] + receivedData[10] * 256;
            quint16 receivedCoolTime = receivedData[11] + receivedData[12] * 256;
            qDebug() << "Данные : " << receivedVolt << receivedCurr << receivedWorkTime << receivedCoolTime;
        }
    }
    else
    {
        qDebug() << "Error. The answer does not meet expectations.";
    }
}

// X-ray shutdown command.
void Emitter::commandN()
{
    QByteArray receivedData = writeAndRead(Command::N);

    /* It should come exactly 6 bytes.
     The answer should be: @ dev com 0 0 CRC.
     Otherwise the answer did not come from the radiator. */
    if (receivedData.size() == 6)
    {
        quint8 crc = 0;
        for (int i = 1; i < receivedData.size() - 1; ++i)
        {
            crc += receivedData[i];
        }
        if (crc != static_cast<quint8>(receivedData[receivedData.size() - 1]))
            qDebug() << "CRC does not match!" << crc << static_cast<quint8>(receivedData[receivedData.size() - 1]);

        // Turn on the connection expiration timer if the X-ray is on.
        if (receivedData[0] == STARTBYTE &&
            static_cast<Command>(receivedData.at(2)) == Command::N &&
            receivedData[3] == 0 &&
            receivedData[4] == 0)
        {
            qDebug() << "X-rays included.";
            m_pTimerCheckConnection->start();
        }
    }
    else
    {
        qDebug() << "Error. The answer does not meet expectations.";
    }
}

void Emitter::commandF() // Turn off the X-ray.
{
    QByteArray receivedData = writeAndRead(Command::F);

    /* It should come exactly 6 bytes.
     The answer should be: @ dev com 0 0 CRC.
     Otherwise the answer did not come from the radiator. */
    if (receivedData.size() == 6)
    {
        quint8 crc = 0;
        for (int i = 1; i < receivedData.size() - 1; ++i)
        {
            crc += receivedData[i];
        }
        if (crc != static_cast<quint8>(receivedData[receivedData.size() - 1]))
            qDebug() << "CRC does not match!" << crc << static_cast<quint8>(receivedData[receivedData.size() - 1]);

        // Turn off the connection confirmation timer.
        if (receivedData[0] == STARTBYTE &&
            static_cast<Command>(receivedData.at(2)) == Command::F &&
            receivedData[3] == 0 &&
            receivedData[4] == 0)
        {
            qDebug() << "The X-ray is turned off.";
            m_pTimerCheckConnection->stop();
        }
    }
    else
    {
        qDebug() << "Error. The answer does not meet expectations.";
    }
}

mainwindow.h

Here one signal is declared for passing parameters, and the object-emitter-object-stream pair is created.

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include <QThread>

#include "emitter.h"

namespace Ui {
class MainWindow;
}

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    explicit MainWindow(QWidget *parent = 0);
    ~MainWindow();

signals:
    // This signal will be sent by pressing the "Set" button to transfer the parameters to the emitter.
    void setFeaturesRequested(int voltage, int current, int workTime, int coolTime);

private:
    Ui::MainWindow *ui;

    // Emitter and a separate stream in which it will work.
    QThread *m_pThread;
    Emitter *m_pEmitter;
};

#endif // MAINWINDOW_H

mainwindow.ui

Primitive design. But the best for demonstrating the work of the radiator is not required.
Widgets spinBox, spinBox_2, spinBox_3, spinBox_4 are renamed to spinBoxVoltage, spinBoxCurrent, spinBoxWorkTime, spinBoxCoolTime, respectively. The buttons pushButton, pushButton_2, pushButton_3 are renamed to pushButtonSet, pushButtonTurnOn, pushButtonTurnOff, respectively. This is done for the convenience of accessing these objects in the mainwindow.cpp implementation file.

mainwindow.cpp

Here the radiator object is moved to a separate stream. The operation of the user interface is also described.

#include "mainwindow.h"
#include "ui_mainwindow.h"

#include <QMessageBox>

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

    m_pThread = new QThread(this);
    // You do not need to specify a parent. The parent will be the stream, when we move our object emitter into it.
    m_pEmitter = new Emitter;

    /* We move the radiator object into a separate stream so that the synchronous pending operations are not blocked
     the main GUI stream. Create a connection: Delete the radiator object when the flow ends.
     Run the thread.*/
    m_pEmitter->moveToThread(m_pThread);
    connect(m_pThread, SIGNAL(finished()), m_pEmitter, SLOT(deleteLater()));
    m_pThread->start();

    // Verify the connection.
    if (m_pEmitter->isConnected())
    {
        ui->pushButtonTurnOff->setEnabled(false);
    }
    else
    {
        ui->pushButtonSet->setEnabled(false);
        ui->pushButtonTurnOn->setEnabled(false);
        ui->pushButtonTurnOff->setEnabled(false);
        QMessageBox::critical(this, "Connection error", "Connect the X-ray radiator to the COM3 serial port"
                              " and restart the program.", QMessageBox::Ok);
    }

    // When you click on the "Enable" button, turn on the X-ray and switch the state of the buttons.
    connect(ui->pushButtonTurnOn, &QPushButton::clicked, m_pEmitter, &Emitter::turnOnXRay);
    connect(ui->pushButtonTurnOn, &QPushButton::clicked, [this](){
        ui->pushButtonTurnOn->setEnabled(false);
        ui->pushButtonTurnOff->setEnabled(true);
    });

    // When you click on the "Disable" button, turn off the X-ray and switch the state of the buttons.
    connect(ui->pushButtonTurnOff, &QPushButton::clicked, m_pEmitter, &Emitter::turnOffXRay);
    connect(ui->pushButtonTurnOff, &QPushButton::clicked, [this](){
        ui->pushButtonTurnOn->setEnabled(true);
        ui->pushButtonTurnOff->setEnabled(false);
    });

    // When you click on the "Set" button, all the relevant parameters are displayed in the spinBox.
    connect(ui->pushButtonSet, &QPushButton::clicked, [this](){
        emit setFeaturesRequested(ui->spinBoxVoltage->value(),
                                  ui->spinBoxCurrent->value(),
                                  ui->spinBoxWorkTime->value(),
                                  ui->spinBoxCoolTime->value());
    });
    connect(this, &MainWindow::setFeaturesRequested, m_pEmitter, &Emitter::setFeatures);
}

MainWindow::~MainWindow()
{
    // Before deleting the main window, we wait until the thread finishes.
    m_pThread->quit();
    m_pThread->wait(1000);

    delete ui;
}

Conclusion

As a result: A radiator class has been created, where encapsulation is observed. There are 4 public functions, 3 of which are slots. There are internal functions of the radiator, the operation of which is implemented in accordance with the described synchronous protocol. The use of the emitter object in pairing with the stream object is shown, which makes it possible not to block the main stream even if the read-write operations require delays.

We recommend hosting TIMEWEB
We recommend hosting TIMEWEB
Stable hosting, on which the social network EVILEG is located. For projects on Django we recommend VDS hosting.

Do you like it? Share on social networks!

a
  • May 3, 2018, 6:49 a.m.

Nice article, in my opinion it's much clearer than the official example at http://doc.qt.io/qt-5/qtserialport-blockingmaster-example.html.
I have the following question: what would be the most elegant way to programmatically set the port name, given that the application may offer a QComboBox that lists every serial port reachable by the host device? How would you do it?

Thanks for reply. This is not hard to do. This described in "Terminal" example by Qt http://doc.qt.io/qt-5/qtserialport-terminal-example.html ,  in SettingsDialog http://doc.qt.io/qt-5/qtserialport-terminal-settingsdialog-cpp.html

void SettingsDialog::fillPortsInfo()
{
    m_ui->serialPortInfoListBox->clear();
    QString description;
    QString manufacturer;
    QString serialNumber;
    const auto infos = QSerialPortInfo::availablePorts();
    for (const QSerialPortInfo &info : infos) {
        QStringList list;
        description = info.description();
        manufacturer = info.manufacturer();
        serialNumber = info.serialNumber();
        list << info.portName()
             << (!description.isEmpty() ? description : blankString)
             << (!manufacturer.isEmpty() ? manufacturer : blankString)
             << (!serialNumber.isEmpty() ? serialNumber : blankString)
             << info.systemLocation()
             << (info.vendorIdentifier() ? QString::number(info.vendorIdentifier(), 16) : blankString)
             << (info.productIdentifier() ? QString::number(info.productIdentifier(), 16) : blankString);

        m_ui->serialPortInfoListBox->addItem(list.first(), list);
    }

    m_ui->serialPortInfoListBox->addItem(tr("Custom"));
}

Thanks for the answer. But I meant to ask a different thing. I already knew about QSerialPortInfo, but I wanted to know what would be the best way of setting the current portname in the Emitter class. I'd like the update to be thread-safe, since Emiter extends QThread. What I'm asking is if something like the following is the right thing to do:

// emitter.h
void Emitter::setPortName(const QString& portName) {
    m_pSerialPort->setPortName(portName);
}

// inside some view file with a pointer to an instance of Emitter
Emitter* emitter = ...
QComboBox* portNamesComboBox = ... // assume it contains a list of portNames extracted from QSerialPortInfo
QPushButton* changePortBtn = new QPushButton(tr("Change"));

connect(changePortBtn , QPushButton::clicked, this, [this,portNamesComboBox]() {
    auto newPortName  = portNamesComboBox.currentText();
    emitter->setPortName(newPortName);
});

Class Emitter does not extend QThread. They work in pair. They do not inherit each other.

In the article i do so:

connect(ui->pushButtonTurnOn, &QPushButton::clicked, m_pEmitter, &Emitter::turnOnXRay);
connect(ui->pushButtonTurnOn, &QPushButton::clicked, [this](){
        ui->pushButtonTurnOn->setEnabled(false);
        ui->pushButtonTurnOff->setEnabled(true);
});
This works.

Comments

Only authorized users can post comments.
Please, Log in or Sign up
AD

C ++ - Test 004. Pointers, Arrays and Loops

  • Result:50points,
  • Rating points-4
m

C ++ - Test 004. Pointers, Arrays and Loops

  • Result:80points,
  • Rating points4
m

C ++ - Test 004. Pointers, Arrays and Loops

  • Result:20points,
  • Rating points-10
Last comments
ИМ
Игорь МаксимовNov. 22, 2024, 10:51 p.m.
Django - Tutorial 017. Customize the login page to Django Добрый вечер Евгений! Я сделал себе авторизацию аналогичную вашей, все работает, кроме возврата к предидущей странице. Редеректит всегда на главную, хотя в логах сервера вижу запросы на правильн…
Evgenii Legotckoi
Evgenii LegotckoiNov. 1, 2024, 12:37 a.m.
Django - Lesson 064. How to write a Python Markdown extension Добрый день. Да, можно. Либо через такие же плагины, либо с постобработкой через python библиотеку Beautiful Soup
A
ALO1ZEOct. 19, 2024, 6:19 p.m.
Fb3 file reader on Qt Creator Подскажите как это запустить? Я не шарю в программировании и кодинге. Скачал и установаил Qt, но куча ошибок выдается и не запустить. А очень надо fb3 переконвертировать в html
ИМ
Игорь МаксимовOct. 5, 2024, 5:51 p.m.
Django - Lesson 064. How to write a Python Markdown extension Приветствую Евгений! У меня вопрос. Можно ли вставлять свои классы в разметку редактора markdown? Допустим имея стандартную разметку: <ul> <li></li> <li></l…
d
dblas5July 5, 2024, 9:02 p.m.
QML - Lesson 016. SQLite database and the working with it in QML Qt Здравствуйте, возникает такая проблема (я новичок): ApplicationWindow неизвестный элемент. (М300) для TextField и Button аналогично. Могу предположить, что из-за более новой верси…
Now discuss on the forum
m
moogoNov. 22, 2024, 6:17 p.m.
Mosquito Spray System Effective Mosquito Systems for Backyard | Eco-Friendly Misting Control Device & Repellent Spray - Moogo ; Upgrade your backyard with our mosquito-repellent device! Our misters conce…
Evgenii Legotckoi
Evgenii LegotckoiJune 25, 2024, 1:11 a.m.
добавить qlineseries в функции Я тут. Работы оень много. Отправил его в бан.
t
tonypeachey1Nov. 15, 2024, 5:04 p.m.
google domain [url=https://google.com/]domain[/url] domain [http://www.example.com link title]
NSProject
NSProjectJune 4, 2022, 1:49 p.m.
Всё ещё разбираюсь с кешем. В следствии прочтения данной статьи. Я принял для себя решение сделать кеширование свойств менеджера модели LikeDislike. И так как установка evileg_core для меня не была возможна, ибо он писался…

Follow us in social networks