如何让 Qt 中的程序不断向我的 Arduino 发送字符串?

Posted

技术标签:

【中文标题】如何让 Qt 中的程序不断向我的 Arduino 发送字符串?【英文标题】:How Do I Make My Program in Qt Continually Send A String to My Arduino? 【发布时间】:2017-04-29 11:28:17 【问题描述】:

我在尝试让我的程序在按住按钮时不断发送字符串 "move 200" 时遇到了麻烦。我将按钮设置为自动重复,但是它仅在释放按钮时才发送,而不是在按住时发送。但是,在按住计数器时,会添加应该发送消息的次数。我迷路了。

ma​​inwindow.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++;


ma​​inwindow.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
        
        elseqDebug() << "Couldn't write to serial!";
    

释放按钮后,Arduino 中的串行监视器会显示发送的所有内容,并且机器人会移动

【问题讨论】:

使用Qt时,无需使用std::string。相反,使用QByteArrayarduino-&gt;write(command.toLatin1()); 【参考方案1】:

我建议你扩展你的设计:

有一个重复的QTimer,其间隔取决于您要发送字符串的速率,以及发送字符串的函数的计时器 连接按钮的按下信号以启动定时器 连接按钮释放信号停止定时器

事件只发送一次,因此处理程序只会执行一次,如果你想继续重复它,你将不得不使用计时器或其他一些事件驱动的方式。您不能使用循环,因为这会阻塞 GUI 线程并且您的应用程序将停止响应。

当然,您可以使用按钮的自动重复,并且可以选择调整触发和重复间隔,但是在逻辑和 GUI 之间划清界限的解决方案会更好。您应该真正依靠 GUI 来存储数据或控制内部逻辑。 GUI 应该只是一个前端。

不过,您需要在串行端口上做更多工作。如果要从 GUI 线程中使用它,则必须使用非阻塞 API。这将需要更多地扩展您的实施。有一个good example on how to achieve that,您只需修改它,以便在成功发送前一个有效负载后简单地启用进一步的有效负载发送。在伪代码中:

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

当然,您还必须进行一些错误检查,您不能只指望它能正常工作。链接的示例包含基本的错误处理。

【讨论】:

谢谢!我会试一试,并随时通知您。我没有指望事件只发送一次。 我按照你的建议做了,它似乎在按住按钮时将字符串写入缓冲区。然而,直到按钮被释放它才会真正发送它。 我应该使用 QThread 吗?你说是因为你提到了非阻塞,我不知道如何实现。 按住按钮时是否会重复“按下按钮”? 不,它只打印了一次,因为我关闭了按钮的自动重复。我从日志中收到以下输出:可用端口数:4 端口名称:“COM1”端口名称:“COM3”端口名称:“COM8”端口名称:“COM9”按钮按下 1 sendData 正在运行 1 arduino可通过命令“move 200”写入 2 sendData 正在运行 2 arduino 可通过命令“move 200”写入 3 sendData 正在运行 3 arduino 可通过命令“move 200”写入 4 sendData 正在运行 4 arduino 可通过命令“move 200”写入【参考方案2】:

来自文档:

当鼠标、空格键或键盘快捷键激活按钮时,它会发出 clicked() 信号。连接到此信号以执行按钮的操作。按钮还提供不太常用的信号,例如,pressed() 和 release()。

所以请使用按下/释放而不是单击。

clicked 会在鼠标点击按钮后发送,可能是在它被释放之后。我不知道 Qt 如何“知道”处理单击和双击。

pressed 由“下推”动作发送,released 由释放动作发送。因此,只需根据这两个信号设置您的标志。

顺便说一句:您必须在发送函数周围使用某种循环,如果您的文件 io 变得可写,通常会定期调用或始终调用。简单地触发 io 不会达到您的预期。

【讨论】:

我试过还是不行。它坐在那里并且在按钮被释放之前不会通过串行方式发送字符串。 @Ben:现在是开始使用调试器的好时机;) QT 是否按预期发出信号?您是否按预期变量“buttonReleased”切换。您的循环是否识别出变量的变化?也许您在不同的任务上下文中访问 var 并且您需要信号量?我不知道你程序的其余部分,一般来说,按下/释放是它必须工作的方式。【参考方案3】:

“盲目”或未经确认的自动重复不是一个好主意,因为大概 Arduino 需要一些时间来对命令做出反应。鉴于默认情况下您在任何地方都没有流量控制,因此您将沿途溢出缓冲区 - 在 USB 转串行芯片(如果有)以及 Arduino 中。由于您的数据包(行)没有错误检查,因此您最终会在 Arduino 上执行垃圾命令,效果各不相同。

至少,Arduino 应该发送一条消息,指示命令已完成。可以是简单的Serial.println("OK")。然后,您将在收到成功回复后立即发送下一个命令。

这会使事情变慢一点,因为只有在您完成接收回复并完成发送命令后才能处理下一个命令。相反,您可以提前发送一个或多个命令,让 Arduino 始终处于忙碌状态。

我们可以利用 Qt 对其 PC 端以及 Arduino 进行简明建模。

下面是一个完整的示例,以文学编程风格编写。

首先,我们需要一个本地管道来在 PC 和样机 Arduino 之间进行通信。这比使用QLocalServer 容易得多。

// https://github.com/KubaO/***n/tree/master/questions/comm-button-loop-43695121
#include <QtWidgets>
#include <private/qringbuffer_p.h>
#include <cctype>

class AppPipe; // See https://***.com/a/32317276/1329652

为了管理通信,控制器在任何给定时间最多允许两个“飞行中”命令。这是一个非常简单的控制器——在生产代码中,我们应该有一个明确的状态机,允许错误处理等。参见例如this question.

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 = ) : QObjectparent, 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_layoutthis;
   QPushButton m_move"Move";
   QLabel m_status;
public:
   Ui(QWidget * parent = ) : QWidgetparent 
      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);
   
;

我们主要完成了 PC 方面的工作 - 稍后将在 main 内部进行测试设置。

我们现在转向 Arduino 方面,并模拟一个最小的 Arduino 环境。回想一下,Arduino“语言”实际上是 C++11!我们使用 Qt 类实现 Arduino 功能。

#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) 并通过 PUTTY 或任何其他终端程序连接到 Arduino。是 - 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_ttyfalse  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 = ) : QObjectparent 
      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 appargc, 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 获取完整的项目。

【讨论】:

以上是关于如何让 Qt 中的程序不断向我的 Arduino 发送字符串?的主要内容,如果未能解决你的问题,请参考以下文章

qserialport 不向 arduino 发送字符

如何同时从 python 发送和接收数据到 arduino

将字符串从 QT 发送到 Arduino

如何拆分arduino代码中的数字?

串行 Mac OS X 不断冻结/锁定/消失 USB 到 Arduino

如何让 Vista 的讲述人向我朗读我的 Swing 组件?