2

My Qt application talks to a serial device, and occasionally has to wait for this device to send a byte. To accomplish this, I create a new eventloop that exits as soon as there is information available in the serial buffer:

unsigned char MyClass::waitForDevice(int timeout)
{
    QEventLoop wait;
    connect(d_serial, SIGNAL(readyRead()), &wait, SLOT(quit()));
    if (timeout > 0)
        QTimer::singleShot(timeout, &wait, SLOT(quit()));

    wait.exec();
    return static_cast<unsigned char>(d_serial->read(1)[0]);
}

Now the problem is that, while the application is waiting, i.e. while the eventloop is running, I need to be able to communicate to the serial device when a button is pressed in the GUI. Naively, I tried connecting a signal to a slot that does this, but I found that the slot is only executed after the eventloop is terminated.

I tried, without any luck, to have a seperate QThread running that calls qApp->processEvents() in an infinite loop, which is terminated when the eventloop is terminated. This didn't work, and I'm not quite sure why not. What is the canonical way to resolve this?

huysentruitw
  • 27,376
  • 9
  • 90
  • 133
JorenHeit
  • 3,877
  • 2
  • 22
  • 28
  • 5
    Solution: Only do reading in a slot, which gets called by `readyRead()` signal. And never block the event loop, so you can't have a `waitForXxxx` method like that. Instead, connect the signals to slots, and do things in the slots. Never block. If you seem to need to block, at that point you need to `return`, and then continue when you get the right signal. – hyde Aug 31 '15 at 14:30
  • 2
    local event loops and processEvents() lead into a world of pain, I can only recommend not to use them. – Frank Osterfeld Aug 31 '15 at 15:51

2 Answers2

7

You're thinking synchronously in a pre-C++1z world. In C++14 (and prior) asynchronous programming, there is mostly no place for a notion of a wait that is implemented as a function that returns when the wait is over (switch-based coroutine hacks excepted). You are also not using the fact that your application is stateful, and the state transitions can be expressed in a state machine.

Instead, you should simply act on data being available. Presumably, your application can be in multiple states. One of the states - the one where you have to wait for input - is simply exited when the input arrives.

The example below uses a simple process-local pipe, but it would work exactly the same if you were using a serial port - both are a QIODevice and emit requisite signals. We start with the project file.

# async-comms-32309737.pro
QT       += widgets core-private
TARGET = async-comms-32309737
CONFIG   += c++11
TEMPLATE = app
SOURCES += main.cpp

To make things simple, the pipe implementation reuses the QRingBuffer private class from Qt. See this question for more fleshed-out implementation(s).

// main.cpp
#include <QtWidgets>
#include <private/qringbuffer_p.h>

/// A simple point-to-point intra-application pipe. This class is not thread-safe.
class AppPipe : public QIODevice {
   Q_OBJECT
   AppPipe * m_other { nullptr };
   QRingBuffer m_buf;
public:
   AppPipe(AppPipe * other, QObject * parent = 0) : QIODevice(parent), m_other(other) {
      open(QIODevice::ReadWrite);
   }
   void setOther(AppPipe * other) { m_other = other; }
   qint64 writeData(const char * data, qint64 maxSize) Q_DECL_OVERRIDE {
      if (!maxSize) return maxSize;
      m_other->m_buf.append(QByteArray(data, maxSize));
      emit m_other->readyRead();
      return maxSize;
   }
   qint64 readData(char * data, qint64 maxLength) Q_DECL_OVERRIDE {
      return m_buf.read(data, maxLength);
   }
   qint64 bytesAvailable() const Q_DECL_OVERRIDE {
      return m_buf.size() + QIODevice::bytesAvailable();
   }
   bool isSequential() const Q_DECL_OVERRIDE { return true; }
};

We start with a simple UI, with one button to restart the state machine, another to transmit a single byte that will be received by the client, and a label that indicates the current state of the state machine.

screenshot of the example

int main(int argc, char *argv[])
{
   QApplication a { argc, argv };
   QWidget ui;
   QGridLayout grid { &ui };
   QLabel state;
   QPushButton restart { "Restart" }, transmit { "Transmit" };
   grid.addWidget(&state, 0, 0, 1, 2);
   grid.addWidget(&restart, 1, 0);
   grid.addWidget(&transmit, 1, 1);
   ui.show();

We now create the simulated device and the client pipe endpoints.

   AppPipe device { nullptr };
   AppPipe client { &device };
   device.setOther(&client);

The state machine has three states. The s_init is the initial state, and is exited after a 1.5s delay. The s_wait state is only exited when we receive some data (a byte or more) from the device in that state. In this example, receiving the data in other states has no effect. The machine is set to restart automatically when stopped.

   QStateMachine sm;
   QState
         s_init { &sm },    // Exited after a delay
         s_wait { &sm },    // Waits for data to arrive
         s_end { &sm };     // Final state
   QTimer timer;
   timer.setSingleShot(true);

   sm.setInitialState(&s_init);
   QObject::connect(&sm, &QStateMachine::stopped, &sm, &QStateMachine::start);
   QObject::connect(&s_init, &QState::entered, [&]{ timer.start(1500); });
   s_init.addTransition(&timer, SIGNAL(timeout()), &s_wait);
   s_wait.addTransition(&client, SIGNAL(readyRead()), &s_end);

To visualize the state machine's progress, we assign the state label's text property in each of the states:

   s_init.assignProperty(&state, "text", "Waiting for timeout.");
   s_wait.assignProperty(&state, "text", "Waiting for data.");
   s_end.assignProperty(&state, "text", "Done.");

Finally, the restart button stops the state machine - it will self-restart then. The transmit button simulates the device sending one byte of data.

   QObject::connect(&restart, &QPushButton::clicked, &sm, &QStateMachine::stop);
   QObject::connect(&transmit, &QPushButton::clicked, [&]{
      device.write("*", 1);
   });

We start the machine, enter the event loop, and let Qt follow our directions onwards from here. The main.moc file is included for it contains the metadata for AppPipe.

   sm.start();
   return a.exec();
}

#include "main.moc"
Community
  • 1
  • 1
Kuba hasn't forgotten Monica
  • 95,931
  • 16
  • 151
  • 313
  • Wow, thanks for such a detailed answer. It seems I have to rethink a bit more of the application. I did not quite understand though what you meant in your opening paragraph. What is it about this approach that makes it so C++11/14 (aside from the lambda's)? – JorenHeit Aug 31 '15 at 19:47
  • @JorenHeit What I mean is that this approach is specifically **pre**-C++14. In C++14, you can write synchronous-looking code, if doing so is convenient, while still producing behavior that yields control away from the function and retains its state. Qt lacks integration with it, though, because there is at the moment IIRC one (1) compiler that can deal with it and that's about it. – Kuba hasn't forgotten Monica Aug 31 '15 at 20:31
  • I'm quite familiar with C++14 but I have no idea what you are talking about... could you be more specific? What features are you referring to? – JorenHeit Aug 31 '15 at 20:44
  • @JorenHeit Stackless and stackful `yield` is supposed to be possible in C++14, right? With that, you can write code that has the syntax of Basic and expressiveness of, well, C++14 :) – Kuba hasn't forgotten Monica Aug 31 '15 at 23:53
  • Aaaaah I thought that would be it, but AFAK that's a proposed feature for C++1y. Correct me if I'm wrong. – JorenHeit Sep 01 '15 at 15:24
  • @JorenHeit Yeah, I'm wrong. It's a proposed feature for C++ 1z I guess, since 1x was 11, 1y was 14... – Kuba hasn't forgotten Monica Sep 01 '15 at 16:10
  • Whoops, typo. Yes that's what I meant. Thanks! – JorenHeit Sep 01 '15 at 17:54
-2

There are several Types of which Signals and Slots can be connected.

See: http://doc.qt.io/qt-4.8/qt.html#ConnectionType-enum

Have you tried Qt::DirectConnection: connect(d_serial, SIGNAL(readyRead()), &wait, SLOT(quit()),Qt::DirectConnection); ?

Haselnussstrauch
  • 333
  • 2
  • 10
  • 2
    The direct connection is implied if the objects live in the same thread, so the last argument is unnecessary. If you are trying it with objects in different threads, then the target slot must be thread-safe. Generally speaking, there are few uses for explicit direct connections - normally they'd be used when invoking thread-safe slots in objects that run in threads without an event loop, or when invoking thread-safe slots to pre-empt any events in the target thread's event loop. – Kuba hasn't forgotten Monica Aug 31 '15 at 14:37
  • Ok, that sounds logical. @Kuba – Haselnussstrauch Aug 31 '15 at 19:36