How Do I Make My Program in Qt Continually Send A

2019-01-12 13:15发布

问题:

I am having trouble trying to get my program to continually send the string "move 200" while I hold down a button. I have the button set to auto repeat however it only sends once the button is released not while it is holding down. However while being held down the counter is adding how many times the message should have been sent. I am at a lost.

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

Code added following @dtech suggestion

    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!";}
    }

After releasing the button the serial monitor in the Arduino then shows everything sent and the robot moves

回答1:

I suggest you expand on your design somewhat:

  • have a repeating QTimer with an interval depending on the rate you want to send the string at, and the timer to the function that sends the string
  • connect the button's pressed signal to start the timer
  • connect the button's released signal to stop the timer

Events are sent only once, thus the handlers will be executed only once, if you want to keep on repeating it, you will have to use a timer or some other event driven way. You cannot use a loop as that would block the GUI thread and your application will stop responding.

Sure, you could use the button's auto repeat, and there is the option to adjust the triggering and repeating intervals, but a solution that puts a line between logic and GUI is better. You should really rely on the GUI for storing data or controlling the internal logic. The GUI should only be a front end.

You need more work on the serial port though. If you are going to use it from the GUI thread, you will have to use the non-blocking API. Which will require to extend on your implementation a little bit more. There is a good example on how to achieve that, you only need to modify it to simply enable the sending of further payloads once the previous payload has been successfully sent. In pseudo code:

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

Of course, you will also have to do some error checking, you can't just expect it to work. The example linked contains basic error handling.



回答2:

From the docs:

A push button emits the signal clicked() when it is activated by the mouse, the Spacebar or by a keyboard shortcut. Connect to this signal to perform the button's action. Push buttons also provide less commonly used signals, for example, pressed() and released().

So please use pressed/released instead of clicked.

clicked is send once a mouse clicks the button, maybe after it was released. I don't know how Qt "knows" to handle single and double clicks.

pressed is send by the "push down" action and released by the release action. So simply set your flag accordingly to the both signals.

BTW: You have to use some kind of loop around you sending function, typically calls periodically or always if your file io becomes writeable. Simply firing on a io will not do what you expect.



回答3:

A "blind" or unconfirmed auto-repeat is not a good idea, because presumably it takes the Arduino some time to react to the command. Given that by default you have no flow control anywhere, you'll overflow the buffers along the way - in the USB-to-serial chip (if any), and also in Arduino. Since your packets (lines) have no error checking, you'll end up executing junk commands on Arduino, with varying effects.

At the very minimum, the Arduino should send a message indicating that a command was finished. It can be a simple Serial.println("OK"). You would then send the next command as soon as you receive the successful reply.

This slows things down a bit since the next command can only be processed after you've finished receiving the reply and finished sending the command. Instead, you can pre-send one or more commands ahead of time, so that the Arduino is always busy.

We can leverage Qt to concisely model both the PC side of it, as well as Arduino.

A complete example follows, written in the literate programming style.

First, we'll need a local pipe to communicate between the PC and the mockup Arduino. This is much easier than using QLocalServer.

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

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

To manage the communications, the controller allows up to two commands "in flight" at any given time. This is a very simple controller - in production code, we should have an explicit state machine that would allow error handling etc. See e.g. 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 = {}) : 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);
};

A user interface provides a button and a status indicator:

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);
   }
};

We're mostly done with the PC side of things - the test setup will come later, inside of main.

We now turn to the Arduino side, and mock up a minimal Arduino environment. Recall that the Arduino "language" is really C++11! We implement Arduino functionality using Qt classes.

#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;

We can now write the Arduino code, exactly as it would appear on the real Arduino. The LineEditor is a class I find missing in Arduino - it provides asynchronous input tokenization, and allows interactive line editing when TTY is set. When run on an actual Arduino, you could call Line.setTTY(true) and connect to the Arduino via PUTTY or any other terminal program. Yes - PUTTY is a general-purpose terminal that can connect to a serial port.

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();
}

An adapter class executes the Arduino environment:

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;

Finally, we set up the test and connect all of the involved components. The Arduino object runs in its own thread.

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"

This concludes the example. You can copy-paste it into an empty main.cpp, or you can fetch the complete project from github.