Создаем GUI приложение для общения по COM порту

Тут я хочу показать как сделать простое GUI приложение чтобы передавать команды arduino и считывать ответ.

В качестве фреймворка для работы с интерфейсом я буду использовать Qt.

Более того Qt включает в себя огромное количество библиотечек для работы с чем только душе угодно. В частности для нас будет актуальна возможность реализация общения по COM порту.

Собираем плату

Значит сделал я такую схемку (да простят меня ардуинщики и физики и вообще…). Короче не будем вдаваться в подробности ее корректности. По крайне мере она работает.

scheme01.png

Короче на ней есть два светодиода подключенный к цифровым портам и фоторезистор подключенный к аналоговому порту. Будем писать интерфейс который позволит переключать светодиоды и считывать значения фоторезистора.

Для управления ардуино я буду использовать следующую схему

Вот такая у меня получилась программа для ардуино

void setup() {
  pinMode(2, OUTPUT); // устанавливаем 2 и 3 пин на отправку данных, [привет Игорю Федотову]
  pinMode(3, OUTPUT);
  Serial.begin(9600);
}

void loop() {
  if (Serial.available() > 0) {
    char a = Serial.read();

    if (a == 'b') {
      digitalWrite(3, LOW);
      digitalWrite(2, HIGH);
      Serial.print("Blue is on");
    } else if (a == 'r') {
      digitalWrite(2, LOW);
      digitalWrite(3, HIGH);
      Serial.print("Red is on");
    } else if (a == 'p') {
      int val = analogRead(0);
      Serial.print(val);
    }
  }
}

Создаем приложение

И так открываем Qt Creator и создаем новый проект

scheme01.png

выбираем тип приложения

scheme01.png

даем имя

scheme01.png

тут далее

scheme01.png

и тут

scheme01.png

ну и все собственно

scheme01.png

увидим что-то такое

scheme01.png

чтобы ошибки ушли. Надо один раз запустить приложение. Для этого кликаем на зеленый треугольник (либо жмем Ctrl+R)

scheme01.png

запустится наше интерфейс, который пока выглядит вот так:

scheme01.png

Собираем форму

закрываем его и начинаем править. Для этого переключимся на форму двойным щелчком на mainwindow.ui

scheme01.png

собственно, увидим нашу форму:

scheme01.png

Теперь начнем перетаскивать на нее элементы формы. Сначала я хочу, чтобы у меня было поле в котором можно ввести порт к которому я буду коннектиться для общения с ардуино.

Делается это так:

scheme01.png

Не смотрите что пока все криво, мы в конце добавим немного магии чтобы все выровнять.

Теперь сделаем две кнопки для включения красного и синего светодиодов

scheme01.png

и еще поле где будем показывать показания с датчика освещённости

scheme01.png

а, ну и еще лог, в который будем писать, что отвечает нам ардуинка

scheme01.png

А теперь чуток магии чтобы все это выровнялось

scheme01.png

если немного разъяснить, то тут я объединяю компоненты в горизонтальные группы (чтобы выбрать несколько элементов я удерживаю Ctrl)

А потом кликаю на последнюю кнопку, чтобы выровнять группы по сетке.

Можно запустить (Ctrl+R) и проверить как все работает

scheme01.png

теперь нам присвоить имена нашим полям для ввода, чтобы была возможность обратится к ним из кода программы. Кликаем на поле для ввода порта, и справа в списке свойств находим в самом верху свойство objectName

меняем его на txtPort

scheme01.png

у остальных полей поменяем потом.

Добавляем обработчик нажатия на кнопку

Теперь давайте добавим реакцию на клик кнопки “Включить синий” для этого кликаем на нее правой кнопкой мыши и выбираем “Перейти к слоту”

scheme01.png

в Qt используется так называемый механизм сигналов (signal) и слотов (slot) это расширений C++, которое позволяет связывать событие с реакцией на событие более-менее естественный образом.

В общем открывается такое окно где виден список сигналов, которые может послать кнопка. Например, когда вы просто нажали на кнопку, но еще не отпустили кнопку мышь, генерируется сигнал pressed, а если же вы отпустил мышь, то сгенерируется сигнал clicked.

Короче суть в том, что мы к этому сигнал сейчас привяжем функцию (или если мыслить в рамках Qt – слот)

выбираем clicked и жмем OK:

scheme01.png

нас кидает в редактор кода

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

// ЭТО КОНСТУРУКТОР
MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this);
}

// ЭТО ДЕСТРУКТОР
MainWindow::~MainWindow()
{
    delete ui;
}

void MainWindow::on_pushButton_clicked()
{
    // вот эта функция только что добавилась
    // сейчас в нее впишем реакцию на клик кнопки
}

не будем мудрствовать и для начала давайте просто выведем какое-нибудь сообщение, правим функцию

void MainWindow::on_pushButton_clicked()
{
   QMessageBox::information(this, "Сообщение", "Ура! Нажали на кнопку!");
}

скорее всего вы увидите, что QMessageBox подчеркнуто и выдается ошибка:

scheme01.png

это потому что мы не подключим заголовочный файл с этим классом, ставим курсор на QMessageBox и жмем Alt+Enter

scheme01.png

и видим, как вверху добавился необходимый include

теперь давайте запустим приложение и проверим что кнопка действительно заработала.

Жмем Ctrl+R и кликаем на кнопку

scheme01.png

Красота! =)

Выводим значение из txtPort

Давайте теперь попробуем сделать так чтобы по нажатию на кнопку выводилось содержимое поля для ввода порта. Подправим функцию:

void MainWindow::on_pushButton_clicked()
{
   // У QT свой тип для работы со строками QString
   // обращением к элементам на форме всегда идет через ui
   // очевидно что ui->txtPort -- это указатель на поле для ввода порта
   // ну а ui->txtPort->text() -- это вызов функции которая вернет текстовое содержимое поля
   QString port = ui->txtPort->text();
   // QString очень крутой класс, и позволяет нам склеивать строки на лету
   // т.е.  "Выбранный порт: " + port -- означает подклеить содержимое txtPort к фразе "Выбранный порт"
   QMessageBox::information(this, "Сообщение", "Выбранный порт: " + port);
}

проверяем:

scheme01.png

Замечательно! =)

Посылаем сообщение arduino

Для работы с COM портом у QT есть класс QSerialPort, давайте подправим функцию чтобы она отправил символ “b” в порт и тем самым зажгла светодиод:

void MainWindow::on_pushButton_clicked()
{
    // создали экземпляр для общения по последовательному порту
    QSerialPort serialPort;

    // указали имя к какому порту будем подключаться
    serialPort.setPortName(this->ui->txtPort->text());
    // указали скорость
    serialPort.setBaudRate(QSerialPort::Baud9600);

    // пробуем подключится
    if (!serialPort.open(QIODevice::ReadWrite)) {
        // если подключится не получится, то покажем сообщение с ошибкой
        QMessageBox::warning(this, "Ошибка", "Не удалось подключится к порту");
        return;
    }

    // отправляем строку с b нашей арудинкой
    serialPort.write("b"); // очень важно, что именно двойные кавычки
    serialPort.waitForBytesWritten(); // ждем пока дойдет

    // и не знаю с чем тут связано, но, чтобы сообщение дошло
    // надо обязательно прочитать не пришло ли нам чего в ответ
    //
    // функция waitForReadyRead(10) проверят не появилось
    // в ближайшие 10 миллисекунд чего-нибудь в ответ
    while (serialPort.waitForReadyRead(10)) {
        // и если появилось мы просто это читаем в пустоту
        serialPort.readAll();

        // сам while необходим для того что ответ приходит порциями
        // и мы хотим считать все что отправилось
    }

    // ну и закрываем порт
    serialPort.close();
}

он опять будет ругаться на QSerialPort (ставьте курсор на него, жмите Alt+Enter, выбирайте #include <QtSerialPort/QSerialPort>, ну либо руками сами добавляйте)

Пробуем запустить. Жмем Ctrl+R

И видим кучу ошибок:

scheme01.png

дело в том, что QSerialPort специализированная библиотечка, и чтобы ее начать использовать надо попросить QT использовать ее dll файл.

Как это сделать. Для этого есть документация!) В ней указано что надо прописать класс заработал.

У QtCreator встроенная документация по Qt, так что ставим курсор на QSerialPort и жмем F1

scheme01.png

собственно, тут видно какой заголовочный файл (Header) достаточно указать, и еще что-то про qmake

qmake – это такой себе компилятор QT, который делает из QT C++ кода обычный C++ код, его поведение настраивается в *.pro файле

Кликаем в левом меню Редактор

scheme01.png

далее выбираем GuiController.pro – собственно pro файл, ну или проще говоря project файл

scheme01.png

в самом верху в нем строчка:

QT       += core gui

добавим к ней снизу

QT       += core gui
QT += serialport  # прям из документации скопировал

пробуем запустить снова (Ctrl+R)

scheme01.png

уже по крайней мере запустилось!) Попробуем нажать на кнопку

scheme01.png

Ну да мы ведь порт не указали, так что логично

Пробуем зажечь светодиод

Собираем и подключаем нашу ардуинку. Заливаем на нее программу, за одно проверяем какой порт использовать

scheme01.png

запускаем нашу программу, вводим порт COM3 и смотрим:

scheme01.png

Вроде работает! =)

Давайте теперь добавим то что вернет arduino в ответ на зажжение светодиода, как мы помним там у нас есть:

Serial.print("Blue is on");

а в нашем обработчике мы в холостую читаем данные:

void MainWindow::on_pushButton_clicked()
{
    // ...

    while (serialPort.waitForReadyRead(10)) {
        serialPort.readAll();
    }

    serialPort.close();
}

попробуем сохранить результат в какую-нибудь переменную и вывести ее в наше большое поле для текста.

Только сначала дадим ему осознанное имя. Переключаемся на mainwindow.ui

scheme01.png

теперь идем обратно в mainwindow.cpp и правим on_pushButton_clicked:

void MainWindow::on_pushButton_clicked()
{
    QSerialPort serialPort;

    serialPort.setPortName(this->ui->txtPort->text());
    serialPort.setBaudRate(QSerialPort::Baud9600);

    if (!serialPort.open(QIODevice::ReadWrite)) {
        QMessageBox::warning(this, "Ошибка", "Не удалось подключится к порту");
        return;
    }

    serialPort.write("b");
    serialPort.waitForBytesWritten();
    // ВСЕ ЧТО ВЫШЕ ЭТОГО НЕ ТРОГАЕМ

    QByteArray data;  // специальный тип QT для хранения последовательности байтов
    while (serialPort.waitForReadyRead(10)) {
        // вместо холостого чтения накапливаем результат в переменную data
        data.append(serialPort.readAll());
    }

    // добавляем строку с содержимым data в поле txtOutput
    ui->txtOutput->append(data);

    serialPort.close();
}

проверяем

scheme01.png

у нас в программе никаких “Blue is on” так что можно с чистой совестью заявить, что это отправило нам ардуино.

Правим красную кнопку

По аналогии с синей кнопкой добавим обработчик на красную.

scheme01.png

выбираем сигнал

scheme01.png

копипастим обработчик:

void MainWindow::on_pushButton_2_clicked()
{
    QSerialPort serialPort;

    serialPort.setPortName(this->ui->txtPort->text());
    serialPort.setBaudRate(QSerialPort::Baud9600);

    if (!serialPort.open(QIODevice::ReadWrite)) {
        QMessageBox::warning(this, "Ошибка", "Не удалось подключится к порту");
        return;
    }

    serialPort.write("r"); // меняем тут b на r
    serialPort.waitForBytesWritten();

    QByteArray data;
    while (serialPort.waitForReadyRead(10)) {
        data.append(serialPort.readAll());
    }

    ui->txtOutput->append(data);

    serialPort.close();
}

запускаем и проверяем:

scheme01.png

в общем показывать, что диоды у меня действительно переключаются мне чего-то лень, но вот сообщения по крайне мере точно приходят с ардуино =)

Читаем значения датчика освещённости

Дадим осознанное имя поля для значения фоторезистора

scheme01.png

И добавим обработчик кнопке обновить:

scheme01.png

выбрали сигнал

scheme01.png

и пишем код (по сути копипастим с правкой в двух местах):

void MainWindow::on_pushButton_3_clicked()
{
    QSerialPort serialPort;

    serialPort.setPortName(this->ui->txtPort->text());
    serialPort.setBaudRate(QSerialPort::Baud9600);

    if (!serialPort.open(QIODevice::ReadWrite)) {
        QMessageBox::warning(this, "Ошибка", "Не удалось подключится к порту");
        return;
    }

    serialPort.write("p"); // меняем тут на p
    serialPort.waitForBytesWritten();

    QByteArray data;
    while (serialPort.waitForReadyRead(10)) {
        data.append(serialPort.readAll());
    }

    ui->txtLight->setText(data); // тут вставляем значение в txtLight

    serialPort.close();
}

проверяем

scheme01.png

Ура!

Обновляем значения датчика освещённости в режиме реального времени

Тыкать все время на кнопку обновить не очень интересно. Лучше, чтобы это делалось на автомате. При чем не именно кликанье на кнопку, а просто команда запроса значения и вывода его на форму. Допустим раз в секунду или чаще.

Для этого есть возможность добавить таймер, ему можно указать вызывать некоторую функцию с каким-то промежутком.

Делается это так. В конструкторе добавляем таймер

MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this); // это не трогаем

    // создали таймер, привязали его к форме
    // если ругается на QTimer ставим на него курсор и нажимаем Alt+Enter
    // чтоб на автомате добавить #include <QTimer>
    QTimer *timer = new QTimer(this);

    // подключили сигнал timeout, к слоту нажатия на кнопку
    connect(timer, SIGNAL(timeout()), this, SLOT(on_pushButton_3_clicked()));

    // запускаем со значением вызывать раз в 300мс
    timer->start(300);
}

// ...


void MainWindow::on_pushButton_3_clicked()
{
    QSerialPort serialPort;

    serialPort.setPortName(this->ui->txtPort->text());
    serialPort.setBaudRate(QSerialPort::Baud9600);

    if (!serialPort.open(QIODevice::ReadWrite)) {
        // закомментим вызовы QMessageBox, так как эта функция будет вызываться таймером
        // и информационное сообщение будет блокировать работу программы
        //QMessageBox::warning(this, "Ошибка", "Не удалось подключится к порту");
        return; // просто оставляем ретёрн, типа не подключились ну и бог с ним
    }

    // ... остальное все также
}

Теперь как только мы укажем правильный порт, начнут приходить значения:

scheme01.png

Вот собственно и все!

БОНУС

Писать порт все время лениво. Есть возможность получить список всех доступных портов. Делается это так.

Сначала идем на mainwindow.ui и удаляем txtPort, на место него ставим Combo Box

scheme01.png

правим ему свойства

scheme01.png

возвращаемся к коду mainwindow.cpp и в конструкторе добавляем:

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

    QTimer *timer = new QTimer(this);
    connect(timer, SIGNAL(timeout()), this, SLOT(on_pushButton_3_clicked()));
    timer->start(300);

    // ВОТ ЭТОТ цикл добавляем
    foreach (const QSerialPortInfo &serialPortInfo, QSerialPortInfo::availablePorts())
    {
        ui->cmbPort->addItem(serialPortInfo.portName());
    }
}

void MainWindow::on_pushButton_clicked()
{
    QSerialPort serialPort;

    // тут меняем чтобы бралось значение из cmbPort
    serialPort.setPortName(this->ui->cmbPort->currentText());

    // ...
}

void MainWindow::on_pushButton_2_clicked()
{
    QSerialPort serialPort;

    // и тут
    serialPort.setPortName(this->ui->cmbPort->currentText());

    // ...
}

void MainWindow::on_pushButton_3_clicked()
{
    QSerialPort serialPort;

    // и здесь
    serialPort.setPortName(this->ui->cmbPort->currentText());

    // ...
}

запускаем:

scheme01.png

Прикольно, что прям сразу подключается! Такие дела =)