Как сделать так, чтобы моя программа в Qt постоянно отправляла строку в мой Arduino?
У меня проблемы с попыткой заставить мою программу постоянно отправлять строку "move 200"
пока я удерживаю кнопку. У меня установлена кнопка автоповтора, но она отправляется только после того, как кнопка отпущена, а не при ее удержании. Однако, пока счетчик удерживается, счетчик добавляет, сколько раз сообщение должно было быть отправлено. Я в растерянности.
mainwindow.cpp
void MainWindow::on_forwardButton_clicked()
{
if(arduino->isWritable()){
arduino->write(command.toStdString().c_str());
qDebug() << i;
}else{
qDebug() << "Couldn't write to serial!";
}
ui->label->setText("Moving");
i++;
}
mainwindow.h
ifndef MAINWINDOW_H
define MAINWINDOW_H
include <QMainWindow>
include <QDialog>
include <QSerialPort>
namespace Ui {
class MainWindow;
}
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
explicit MainWindow(QWidget *parent = 0);
~MainWindow();
private slots:
void on_forwardButton_clicked();
private:
Ui::MainWindow *ui;
QSerialPort *arduino; //makes arduino a pointer to the SerialPort
bool arduino_is_available;
QString command = "move 200";
bool buttonReleased = false;
};
endif // MAINWINDOW_H
Код добавлен после предложения @dtech
pButtonTimer = new QTimer;
connect(pButtonTimer, SIGNAL(timeout()), this, SLOT(sendData()));
int i = 0;
void MainWindow::on_forwardButton_pressed()
{
pButtonTimer->start(1000);
ui->label->setText("Moving");
qDebug() << "Button Pushed";
}
void MainWindow::on_forwardButton_released()
{
pButtonTimer->stop();
}
void MainWindow::sendData(){
i++; //used to count how many times the command should have been sent
qDebug() << i << "sendData is running"; //notifies me the function has been called
if(arduino->isWritable()){
arduino->write(command.toStdString().c_str());
qDebug() << i << "arduino is writable with command " << command; //lets me know the Arduino is writable
}
else{qDebug() << "Couldn't write to serial!";}
}
После отпускания кнопки последовательный монитор в Arduino показывает все отправленное и робот движется
3 ответа
Я предлагаю вам немного расширить свой дизайн:
- повторять
QTimer
с интервалом, зависящим от скорости, с которой вы хотите отправить строку, и таймера функции, которая отправляет строку - подключить нажатой сигнал кнопки, чтобы запустить таймер
- подключить сигнал, отпущенный кнопкой, чтобы остановить таймер
События отправляются только один раз, таким образом, обработчики будут выполняться только один раз, если вы хотите продолжать повторять его, вам придется использовать таймер или какой-либо другой управляемый событиями способ. Вы не можете использовать цикл, поскольку он заблокирует поток GUI, и ваше приложение перестанет отвечать на запросы.
Конечно, вы можете использовать функцию автоповтора кнопки, и есть возможность настроить интервалы запуска и повторения, но решение, которое ставит грань между логикой и GUI, лучше. Вы действительно должны полагаться на графический интерфейс для хранения данных или управления внутренней логикой. GUI должен быть только внешним интерфейсом.
Вам нужно больше работы на последовательном порту, хотя. Если вы собираетесь использовать его из потока GUI, вам придется использовать неблокирующий API. Что потребует, чтобы расширить вашу реализацию немного больше. Есть хороший пример того, как этого добиться, вам нужно всего лишь изменить его, чтобы просто разрешить отправку дополнительных полезных данных после успешной отправки предыдущей полезной нагрузки. В псевдокоде:
on button press
start timer
on button release
stop timer
onTimeout
if (can send)
send
can send = false
onBytesWritten
accumulate bytes
if (payload is completed)
can send = true
reset payload byte counter
Конечно, вы также должны будете сделать некоторую проверку ошибок, вы не можете просто ожидать, что это сработает. Связанный пример содержит основную обработку ошибок.
Из документов:
Кнопка генерирует сигнал clicked(), когда она активируется мышью, пробелом или сочетанием клавиш. Подключитесь к этому сигналу, чтобы выполнить действие кнопки. Кнопки также предоставляют менее часто используемые сигналы, например, нажатие () и отпускание ().
Поэтому, пожалуйста, используйте нажатие / отпускание вместо нажатия.
clicked
посылается после нажатия кнопки мышью, возможно, после того, как она была отпущена. Я не знаю, как Qt "знает", как обрабатывать одиночные и двойные клики.
pressed
посылается действием "push down" и released
по действию релиза. Так что просто установите свой флаг в соответствии с обоими сигналами.
Кстати: вы должны использовать какой-то цикл вокруг отправляющей функции, обычно она вызывается периодически или всегда, если ваш файл становится доступным для записи. Просто стрельба по io не будет делать то, что вы ожидаете.
"Слепой" или неподтвержденный автоповтор не является хорошей идеей, потому что, по-видимому, Arduino требуется некоторое время, чтобы среагировать на команду. Учитывая, что по умолчанию у вас нет управления потоком, вы переполните буферы на этом пути - в чипе USB-to-serial (если есть), а также в Arduino. Поскольку в ваших пакетах (строках) нет проверки ошибок, вы в конечном итоге будете выполнять ненужные команды на Arduino с различными эффектами.
Как минимум, Arduino должен отправить сообщение, указывающее, что команда была выполнена. Это может быть просто Serial.println("OK")
, Затем вы отправите следующую команду, как только получите успешный ответ.
Это немного замедляет работу, так как следующая команда может быть обработана только после того, как вы закончили получать ответ и закончили отправку команды. Вместо этого вы можете заранее отправить одну или несколько команд, чтобы Arduino всегда был занят.
Мы можем использовать Qt, чтобы кратко смоделировать как сторону ПК, так и Arduino.
Далее следует полный пример, написанный в стиле грамотного программирования.
Во-первых, нам понадобится локальный канал для связи между ПК и макетом Arduino. Это намного проще, чем использовать QLocalServer
,
// https://github.com/KubaO/stackrun/tree/master/questions/comm-button-loop-43695121
#include <QtWidgets>
#include <private/qringbuffer_p.h>
#include <cctype>
class AppPipe; // See https://stackru.com/a/32317276/1329652
Для управления связью, контроллер допускает до двух команд "в полете" в любой момент времени. Это очень простой контроллер - в производственном коде у нас должен быть явный конечный автомат, который позволял бы обрабатывать ошибки и т. Д. См., Например, этот вопрос.
class Controller : public QObject {
Q_OBJECT
int m_sent = {}, m_received = {};
QPointer<QIODevice> m_dev;
QByteArray m_command;
QQueue<QByteArray> m_commands;
void sendCommand() {
if (m_command.isEmpty()) return;
while (m_commands.size() < 2) {
m_commands.enqueue(m_command);
m_dev->write(m_command);
m_dev->write("\n");
m_sent ++;
updateStatus();
}
}
Q_SLOT void updateStatus() {
emit statusChanged(m_sent, m_received, m_commands.size());
}
public:
Controller(QIODevice * dev, QObject * parent = {}) : QObject{parent}, m_dev(dev) {
connect(dev, &QIODevice::readyRead, [this]{
if (!m_dev->canReadLine()) return;
auto const replyFor = m_commands.dequeue();
m_received ++;
if (m_dev->readLine() == "OK\n" || m_dev->readLine() == "ERROR\n")
sendCommand();
updateStatus();
Q_UNUSED(replyFor);
});
QMetaObject::invokeMethod(this, "updateStatus", Qt::QueuedConnection);
}
Q_SLOT void setCommand(const QByteArray & cmd) {
m_command = cmd;
sendCommand();
}
Q_SLOT void stop() {
m_command.clear();
}
Q_SIGNAL void statusChanged(int sent, int received, int queueDepth);
};
Пользовательский интерфейс предоставляет кнопку и индикатор состояния:
class Ui : public QWidget {
Q_OBJECT
QFormLayout m_layout{this};
QPushButton m_move{"Move"};
QLabel m_status;
public:
Ui(QWidget * parent = {}) : QWidget{parent} {
setMinimumWidth(300);
m_layout.addWidget(&m_move);
m_layout.addWidget(&m_status);
connect(&m_move, &QPushButton::pressed, this, &Ui::moveActive);
connect(&m_move, &QPushButton::released, this, &Ui::inactive);
}
Q_SIGNAL void moveActive();
Q_SIGNAL void inactive();
Q_SLOT void setStatus(const QString & status) {
m_status.setText(status);
}
};
Мы в основном покончили с ПК - тестовая настройка появится позже, внутри main
,
Теперь перейдем к стороне Arduino и смоделируем минимальную среду Arduino. Напомним, что "язык" Arduino - это действительно C++11! Мы реализуем функциональность Arduino, используя классы Qt.
#define F(str) str
QElapsedTimer arduinoTimer;
unsigned long millis() {
return arduinoTimer.elapsed();
}
inline bool isSpace(int c) {
return ( isspace (c) == 0 ? false : true);
}
class Print {
public:
virtual size_t write(uint8_t) = 0;
size_t write(const char *str) {
if (str == nullptr) return 0;
return write((const uint8_t *)str, strlen(str));
}
virtual size_t write(const uint8_t *buffer, size_t size) = 0;
size_t write(const char *buffer, size_t size) {
return write((const uint8_t *)buffer, size);
}
size_t print(const char text[]) { return write(text); }
size_t println(const char text[]) { return write(text) + write("\n"); }
size_t println() { return write("\n"); }
};
class Stream : public Print {
public:
virtual int available() = 0;
virtual int read() = 0;
};
class HardwareSerial : public Stream {
QPointer<QIODevice> m_dev;
public:
void setDevice(QIODevice * dev) { m_dev = dev; }
void begin(int) {}
size_t write(uint8_t c) override {
return m_dev->putChar(c) ? 1 : 0;
}
size_t write(const uint8_t * buffer, size_t size) override {
return m_dev->write((const char*)buffer, size);
}
int read() override {
char c;
return m_dev->getChar(&c) ? c : -1;
}
int available() override {
return m_dev->bytesAvailable();
}
} Serial;
Теперь мы можем написать код Arduino в точности так, как он выглядит на реальном Arduino. LineEditor
это класс, который мне не хватает в Arduino - он обеспечивает асинхронный токенизация ввода и позволяет интерактивное редактирование строк, когда TTY
установлено. При запуске на реальном Arduino, вы можете позвонить Line.setTTY(true)
и подключитесь к Arduino через PUTTY или любую другую терминальную программу. Да - PUTTY - терминал общего назначения, который может подключаться к последовательному порту.
template <unsigned int N> class LineEditor {
char m_data[N];
char * m_ptr;
bool m_has : 1; ///< Have we got a complete line yet?
bool m_tty : 1; ///< Are we an interactive application (attached to a terminal)?
LineEditor(const LineEditor &) = delete;
LineEditor & operator=(const LineEditor &) = delete;
public:
LineEditor() : m_tty{false} { clear(); }
void clear() {
m_data[0] = '\0';
m_ptr = m_data;
m_has = false;
}
void input(Stream & str) {
auto const c = str.read();
if (c == '\r' || c == '\n') {
m_has = true;
m_ptr = m_data;
if (m_tty) str.println();
}
else if (m_tty && (c == '\b' || c == 0x7F)) {
if (m_ptr > m_data) {
*--m_ptr = '\0';
str.print(F("\b \b"));
}
}
else if (c >= 32 && c < 127 && m_ptr < m_data+N-1) {
*m_ptr++ = c;
*m_ptr = '\0';
if (m_tty) str.write(c);
}
}
void setTTY(bool tty) { m_tty = tty; }
bool isTTY() const { return m_tty; }
bool ready() const { return m_has; }
char * data() { return m_data; }
unsigned int size() const { return m_ptr-m_data; }
const char * getToken() {
if (!m_has) return nullptr;
char c;
while ((c = *m_ptr) && isSpace(c)) m_ptr++;
auto ret = m_ptr;
while ((c = *m_ptr) && !isSpace(c)) *m_ptr++ = tolower(c);
if (c)
*m_ptr++ = '\0'; // terminate the previous token
return ret;
}
};
LineEditor<32> Line;
void s_input();
void s_moveCommand();
struct {
unsigned long at = {};
void (*handler)() = s_input;
} state ;
void processLine() {
auto const cmd = Line.getToken();
auto const param = Line.getToken();
if (strcmp(cmd, "move") == 0 && param) {
char * end;
auto distance = strtol(param, &end, 10);
if (param != end && distance >= 0 && distance <= 10000) {
// valid move command - pretend that it took some time
state.at = millis() + 1000;
state.handler = s_moveCommand;
}
} else
Serial.println("ERROR");
Line.clear();
}
void s_moveCommand() {
Serial.println("OK");
state.at = {};
state.handler = s_input;
}
void s_input() {
while (Serial.available()) {
Line.input(Serial);
if (Line.ready())
return processLine();
}
}
void setup() {
Serial.begin(9600);
}
void loop() {
if (!state.at || millis() >= state.at)
state.handler();
}
Класс адаптера выполняет среду Arduino:
class Arduino : public QObject {
QBasicTimer m_loopTimer;
static QPointer<Arduino> m_instance;
void timerEvent(QTimerEvent * event) override {
if (event->timerId() == m_loopTimer.timerId())
loop();
}
public:
Arduino(QObject * parent = {}) : QObject{parent} {
Q_ASSERT(!m_instance);
m_instance = this;
m_loopTimer.start(0, this);
arduinoTimer.start();
setup();
}
};
QPointer<Arduino> Arduino::m_instance;
Наконец, мы настроили тест и подключили все задействованные компоненты. Arduino
Объект работает в своем собственном потоке.
class SafeThread : public QThread {
using QThread::run;
public:
~SafeThread() { quit(); wait(); }
};
int main(int argc, char ** argv) {
using Q = QObject;
QApplication app{argc, argv};
AppPipe ctlPipe(nullptr, QIODevice::ReadWrite | QIODevice::Text);
AppPipe serialPipe(&ctlPipe, QIODevice::ReadWrite | QIODevice::Text);
ctlPipe.addOther(&serialPipe);
Serial.setDevice(&serialPipe);
Controller ctl(&ctlPipe);
Ui ui;
Arduino arduino;
SafeThread thread;
arduino.moveToThread(&thread);
thread.start(QThread::LowPriority);
Q::connect(&ui, &Ui::moveActive, &ctl, [&]{ ctl.setCommand("move 200"); });
Q::connect(&ui, &Ui::inactive, &ctl, [&]{ ctl.stop(); });
Q::connect(&ctl, &Controller::statusChanged, &ui, [&](int s, int r, int d){
ui.setStatus(QStringLiteral("sent=%1 received=%2 queue depth=%3").arg(s).arg(r).arg(d));
});
ui.show();
return app.exec();
}
#include "main.moc"
Это завершает пример. Вы можете скопировать и вставить его в пустой main.cpp
Или вы можете получить полный проект с GitHub.