Related
I am using QOAuth2AuthorizationCodeFlow to perform OIDC authentication.
I can connect to the signal QAbstractOAuth::granted() and be notified when it worked with success. Fine.
My problem is: how to be notified when something wrong happened?
I tried to connect to the QAbstractOAuth2::error() signal but I am not notified.
I used:
QObject::connect(this, SIGNAL(error(const QString &, const QString &, const QUrl &)), this, SLOT(catchAll()));
In the application console I see the error reported by the server:
qt.networkauth.replyhandler: Error transferring https://idp.safenetid.com/auth/realms/2H31DFOIEQ-STA/protocol/openid-connect/token - server replied: Fake Bad request
So Qt detected the problem. It looks like the probem is detected in QHttpThreadDelegate::finishedSlot():
if (httpReply->statusCode() >= 400) {
// it's an error reply
QString msg = QLatin1String(QT_TRANSLATE_NOOP("QNetworkReply",
"Error transferring %1 - server replied: %2"));
msg = msg.arg(httpRequest.url().toString(), httpReply->reasonPhrase());
emit error(statusCodeFromHttp(httpReply->statusCode(), httpRequest.url()), msg);
}
An error() signal is emitted. But it is catched by Qt itself and not reported upstream to the application?
Note:
Sometimes the server I use replies with an HTTP error code 400 and message "User not found".
Here I faked the problem using mitmproxy to be able to reproduce the issue easily.
PS: I think it is a missing feature in Qt. So I created an issue for QOAuth2AuthorizationCodeFlow::requestAccessToken() at https://bugreports.qt.io/browse/QTBUG-102279
You can get notified by implementing your own OAuthHttpServerReplyHandler.
This handler has to be set to your QOAuth2AuthorizationCodeFlow by using its setReplyHandler(_).
The OAuthHttpServerReplyHandler has to override the void networkReplyFinished(QNetworkReply *reply) which will get called by QT.
tl;dr: Connect to QOAuth2AuthorizationCodeFlow::authorizationCallbackReceived,
and write your own handler.
Looking at the source code the problem seems to be that only one of these case statements will actually emit the error signal. They all return without a Q_EMIT error(...) call.
void QOAuth2AuthorizationCodeFlowPrivate::_q_handleCallback(const QVariantMap &data)
{
Q_Q(QOAuth2AuthorizationCodeFlow);
using Key = QAbstractOAuth2Private::OAuth2KeyString;
if (status != QAbstractOAuth::Status::NotAuthenticated) {
qCWarning(loggingCategory, "Unexpected call");
return;
}
Q_ASSERT(!state.isEmpty());
const QString error = data.value(Key::error).toString();
const QString code = data.value(Key::code).toString();
const QString receivedState = data.value(Key::state).toString();
if (error.size()) {
const QString uri = data.value(Key::errorUri).toString();
const QString description = data.value(Key::errorDescription).toString();
qCWarning(loggingCategory, "AuthenticationError: %s(%s): %s",
qPrintable(error), qPrintable(uri), qPrintable(description));
Q_EMIT q->error(error, description, uri);
return;
}
if (code.isEmpty()) {
qCWarning(loggingCategory, "AuthenticationError: Code not received");
return;
}
if (receivedState.isEmpty()) {
qCWarning(loggingCategory, "State not received");
return;
}
if (state != receivedState) {
qCWarning(loggingCategory, "State mismatch");
return;
}
setStatus(QAbstractOAuth::Status::TemporaryCredentialsReceived);
QVariantMap copy(data);
copy.remove(Key::code);
extraTokens = copy;
q->requestAccessToken(code);
}
But there also seems to be a misunderstanding as to what the error signal is for; Based on the parameters in its declaration,
void QAbstractOAuth2::error(const QString &error, const QString &errorDescription, const QUrl &uri)
The error signal is specifically for the HTTP 400 "Bad Request" response as described in rfc6749, rather than as a signal for all errors, which is why it won't emit signal on (client-side) errors for things like state mismatch (which could be a quite serious error).
If you are still keen on propagating the error signal, however, you can connect to the &QOAuth2AuthorizationCodeFlow::authorizationCallbackReceived signal as the private class does, then emit the error signal. It takes a bit of creativity because members in the code flow private class are not available, but it can be done.
For example, if you want to propagate error on a state mismatch, that might look like this:
{
static QString last_state;
QObject::connect(
&oauth2, &QOAuth2AuthorizationCodeFlow::stateChanged,
[&](const QString& state) {
last_state = state;
});
QObject::connect(
&oauth2, QOAuth2AuthorizationCodeFlow::authorizationCallbackReceived,
[&](const QVariantMap& data) {
const QString state = data.value("state").toString();
if(last_state != state) {
Q_EMIT oauth2.error(
"The state in request does not match the state in reply!",
"State Mismatch!",
QUrl());
}
});
}
Since this is an abuse of the error function, however, I would suggest putting all this in a different object or class; One that would have stateUpdated and onCallbackReceived slots and be capable of emitting a stateMismatch signal, maybe.
I have to update firmware and settings on a device connected to a serial port.
Since this is done by a sequence of commands, I send a command and wait until I recive an answer. Inside the answere (many lines) I search for a string that indicates if the operation is finished successfully.
Serial->write(“boot”, 1000);
Serial->waitForKeyword(“boot successful”);
Serial->sendFile(“image.dat”);
…
So I’ve created a new Thread for this blocking read/write method. Inside the thread I make use of the waitForX() functions.
If I call watiForKeyword() it will call readLines() until it detects the keyword or timesout
bool waitForKeyword(const QString &keyword)
{
QString str;
// read all lines
while(serial->readLines(10000))
{
// check each line
while((str = serial->getLine()) != "")
{
// found!
if(str.contains(keyword))
return true;
}
}
// timeout
return false;
}
readLines() reads everything available and separates it into lines , each line is placed inside a QStringList and to get a string I call getLine() which returns the first string in the list and deletes it.
bool SerialPort::readLines(int waitTimeout)
{
if(!waitForReadyRead(waitTimeout))
{
qDebug() << "Timeout reading" << endl;
return false;
}
QByteArray data = readAll();
while (waitForReadyRead(100))
data += readAll();
char* begin = data.data();
char* ptr = strstr(data, "\r\n");
while(ptr != NULL)
{
ptr+=2;
buffer.append(begin, ptr - begin);
emit readyReadLine(buffer);
lineBuffer.append(QString(buffer)); // store line in Qstringlist
buffer.clear();
begin = ptr;
ptr = strstr(begin, "\r\n");
}
// rest
buffer.append(begin, -1);
return true;
}
The problem is if I send a file via terminal to test the app readLines() will only read a smale part of the file ( 5 Lines or so). Since these lines do not contain the keyword. the function will run once again, but this time it dosnt wait for timeout, readLines just return false immediately.
Whats wrong ?
Also I'm not shure if this is the right approach... Does anyone know how to send a sequenze of commands and wait for a response each time?
Let's use QStateMachine to make this simple. Let's recall how you wished such code would look:
Serial->write("boot", 1000);
Serial->waitForKeyword("boot successful");
Serial->sendFile("image.dat");
Let's put it in a class that has explicit state members for each state the programmer could be in. We'll also have action generators send, expect, etc. that attach given actions to states.
// https://github.com/KubaO/stackoverflown/tree/master/questions/comm-commands-32486198
#include <QtWidgets>
#include <private/qringbuffer_p.h>
#include <type_traits>
[...]
class Programmer : public StatefulObject {
Q_OBJECT
AppPipe m_port { nullptr, QIODevice::ReadWrite, this };
State s_boot { &m_mach, "s_boot" },
s_send { &m_mach, "s_send" };
FinalState s_ok { &m_mach, "s_ok" },
s_failed { &m_mach, "s_failed" };
public:
Programmer(QObject * parent = 0) : StatefulObject(parent) {
connectSignals();
m_mach.setInitialState(&s_boot);
send (&s_boot, &m_port, "boot\n");
expect(&s_boot, &m_port, "boot successful", &s_send, 1000, &s_failed);
send (&s_send, &m_port, ":HULLOTHERE\n:00000001FF\n");
expect(&s_send, &m_port, "load successful", &s_ok, 1000, &s_failed);
}
AppPipe & pipe() { return m_port; }
};
This is fully functional, complete code for the programmer! Completely asynchronous, non-blocking, and it handles timeouts, too.
It's possible to have infrastructure that generates the states on-the-fly, so that you don't have to manually create all the states. The code is much smaller and IMHO easier to comperehend if you have explicit states. Only for complex communication protocols with 50-100+ states would it make sense to get rid of explicit named states.
The AppPipe is a simple intra-process bidirectional pipe that can be used as a stand-in for a real serial port:
// See http://stackoverflow.com/a/32317276/1329652
/// A simple point-to-point intra-process pipe. The other endpoint can live in any
/// thread.
class AppPipe : public QIODevice {
[...]
};
The StatefulObject holds a state machine, some basic signals useful for monitoring the state machine's progress, and the connectSignals method used to connect the signals with the states:
class StatefulObject : public QObject {
Q_OBJECT
Q_PROPERTY (bool running READ isRunning NOTIFY runningChanged)
protected:
QStateMachine m_mach { this };
StatefulObject(QObject * parent = 0) : QObject(parent) {}
void connectSignals() {
connect(&m_mach, &QStateMachine::runningChanged, this, &StatefulObject::runningChanged);
for (auto state : m_mach.findChildren<QAbstractState*>())
QObject::connect(state, &QState::entered, this, [this, state]{
emit stateChanged(state->objectName());
});
}
public:
Q_SLOT void start() { m_mach.start(); }
Q_SIGNAL void runningChanged(bool);
Q_SIGNAL void stateChanged(const QString &);
bool isRunning() const { return m_mach.isRunning(); }
};
The State and FinalState are simple named state wrappers in the style of Qt 3. They allow us to declare the state and give it a name in one go.
template <class S> struct NamedState : S {
NamedState(QState * parent, const char * name) : S(parent) {
this->setObjectName(QLatin1String(name));
}
};
typedef NamedState<QState> State;
typedef NamedState<QFinalState> FinalState;
The action generators are quite simple, too. The meaning of an action generator is "do something when a given state is entered". The state to act on is always given as the first argument. The second and subsequent arguments are specific to the given action. Sometimes, an action might need a target state as well, e.g. if it succeeds or fails.
void send(QAbstractState * src, QIODevice * dev, const QByteArray & data) {
QObject::connect(src, &QState::entered, dev, [dev, data]{
dev->write(data);
});
}
QTimer * delay(QState * src, int ms, QAbstractState * dst) {
auto timer = new QTimer(src);
timer->setSingleShot(true);
timer->setInterval(ms);
QObject::connect(src, &QState::entered, timer, static_cast<void (QTimer::*)()>(&QTimer::start));
QObject::connect(src, &QState::exited, timer, &QTimer::stop);
src->addTransition(timer, SIGNAL(timeout()), dst);
return timer;
}
void expect(QState * src, QIODevice * dev, const QByteArray & data, QAbstractState * dst,
int timeout = 0, QAbstractState * dstTimeout = nullptr)
{
addTransition(src, dst, dev, SIGNAL(readyRead()), [dev, data]{
return hasLine(dev, data);
});
if (timeout) delay(src, timeout, dstTimeout);
}
The hasLine test simply checks all lines that can be read from the device for a given needle. This works fine for this simple communications protocol. You'd need more complex machinery if your communications were more involved. It is necessary to read all the lines, even if you find your needle. That's because this test is invoked from the readyRead signal, and in that signal you must read all the data that fulfills a chosen criterion. Here, the criterion is that the data forms a full line.
static bool hasLine(QIODevice * dev, const QByteArray & needle) {
auto result = false;
while (dev->canReadLine()) {
auto line = dev->readLine();
if (line.contains(needle)) result = true;
}
return result;
}
Adding guarded transitions to states is a bit cumbersome with the default API, so we will wrap it to make it easier to use, and to keep the action generators above readable:
template <typename F>
class GuardedSignalTransition : public QSignalTransition {
F m_guard;
protected:
bool eventTest(QEvent * ev) Q_DECL_OVERRIDE {
return QSignalTransition::eventTest(ev) && m_guard();
}
public:
GuardedSignalTransition(const QObject * sender, const char * signal, F && guard) :
QSignalTransition(sender, signal), m_guard(std::move(guard)) {}
GuardedSignalTransition(const QObject * sender, const char * signal, const F & guard) :
QSignalTransition(sender, signal), m_guard(guard) {}
};
template <typename F> static GuardedSignalTransition<F> *
addTransition(QState * src, QAbstractState *target,
const QObject * sender, const char * signal, F && guard) {
auto t = new GuardedSignalTransition<typename std::decay<F>::type>
(sender, signal, std::forward<F>(guard));
t->setTargetState(target);
src->addTransition(t);
return t;
}
That's about it - if you had a real device, that's all you need. Since I don't have your device, I'll create another StatefulObject to emulate the presumed device behavior:
class Device : public StatefulObject {
Q_OBJECT
AppPipe m_dev { nullptr, QIODevice::ReadWrite, this };
State s_init { &m_mach, "s_init" },
s_booting { &m_mach, "s_booting" },
s_firmware { &m_mach, "s_firmware" };
FinalState s_loaded { &m_mach, "s_loaded" };
public:
Device(QObject * parent = 0) : StatefulObject(parent) {
connectSignals();
m_mach.setInitialState(&s_init);
expect(&s_init, &m_dev, "boot", &s_booting);
delay (&s_booting, 500, &s_firmware);
send (&s_firmware, &m_dev, "boot successful\n");
expect(&s_firmware, &m_dev, ":00000001FF", &s_loaded);
send (&s_loaded, &m_dev, "load successful\n");
}
Q_SLOT void stop() { m_mach.stop(); }
AppPipe & pipe() { return m_dev; }
};
Now let's make it all nicely visualized. We'll have a window with a text browser showing the contents of the communications. Below it will be buttons to start/stop the programmer or the device, and labels indicating the state of the emulated device and the programmer:
int main(int argc, char ** argv) {
using Q = QObject;
QApplication app{argc, argv};
Device dev;
Programmer prog;
QWidget w;
QGridLayout grid{&w};
QTextBrowser comms;
QPushButton devStart{"Start Device"}, devStop{"Stop Device"},
progStart{"Start Programmer"};
QLabel devState, progState;
grid.addWidget(&comms, 0, 0, 1, 3);
grid.addWidget(&devState, 1, 0, 1, 2);
grid.addWidget(&progState, 1, 2);
grid.addWidget(&devStart, 2, 0);
grid.addWidget(&devStop, 2, 1);
grid.addWidget(&progStart, 2, 2);
devStop.setDisabled(true);
w.show();
We'll connect the device's and programmer's AppPipes. We'll also visualize what the programmer is sending and receiving:
dev.pipe().addOther(&prog.pipe());
prog.pipe().addOther(&dev.pipe());
Q::connect(&prog.pipe(), &AppPipe::hasOutgoing, &comms, [&](const QByteArray & data){
comms.append(formatData(">", "blue", data));
});
Q::connect(&prog.pipe(), &AppPipe::hasIncoming, &comms, [&](const QByteArray & data){
comms.append(formatData("<", "green", data));
});
Finally, we'll connect the buttons and labels:
Q::connect(&devStart, &QPushButton::clicked, &dev, &Device::start);
Q::connect(&devStop, &QPushButton::clicked, &dev, &Device::stop);
Q::connect(&dev, &Device::runningChanged, &devStart, &QPushButton::setDisabled);
Q::connect(&dev, &Device::runningChanged, &devStop, &QPushButton::setEnabled);
Q::connect(&dev, &Device::stateChanged, &devState, &QLabel::setText);
Q::connect(&progStart, &QPushButton::clicked, &prog, &Programmer::start);
Q::connect(&prog, &Programmer::runningChanged, &progStart, &QPushButton::setDisabled);
Q::connect(&prog, &Programmer::stateChanged, &progState, &QLabel::setText);
return app.exec();
}
#include "main.moc"
The Programmer and Device could live in any thread. I've left them in the main thread since there's no reason to move them out, but you could put both into a dedicated thread, or each into its own thread, or into threads shared with other objects, etc. It's completely transparent since AppPipe supports communications across the threads. This would also be the case if QSerialPort was used instead of AppPipe. All that matters is that each instance of a QIODevice is used from one thread only. Everything else happens via signal/slot connections.
E.g. if you wanted the Programmer to live in a dedicated thread, you'd add the following somewhere in main:
// fix QThread brokenness
struct Thread : QThread { ~Thread() { quit(); wait(); } };
Thread progThread;
prog.moveToThread(&progThread);
progThread.start();
A little helper formats the data to make it easier to read:
static QString formatData(const char * prefix, const char * color, const QByteArray & data) {
auto text = QString::fromLatin1(data).toHtmlEscaped();
if (text.endsWith('\n')) text.truncate(text.size() - 1);
text.replace(QLatin1Char('\n'), QString::fromLatin1("<br/>%1 ").arg(QLatin1String(prefix)));
return QString::fromLatin1("<font color=\"%1\">%2 %3</font><br/>")
.arg(QLatin1String(color)).arg(QLatin1String(prefix)).arg(text);
}
I'm not sure indeed this is the right approach.
You're polling with waitForReadyRead(). But since the serial port is a QIODevice, it will emit a void QIODevice::readyRead() signal when something will arrive on the serial port. Why not connect this signal to your input parsing code? No need for waitForReadyRead().
Also/on the other hand: "...this time it doesn't wait for timeout, readLines just return false immediately. Whats wrong ?"
Quoting the documentation:
If waitForReadyRead() returns false, the connection has been
closed or an error has occurred.
(emphasis mine)
From my experience as an embedded developer, it is not impossible that you put the device into kind of a "firmware upgrade" mode, and that by doing so the device rebooted into a special boot mode (not running the firmware you're about to update) and thus closed the connection. No way to tell unless it's documented/you have contact with the device developers. Not so obvious to check using a serial terminal to type your commands and witness that, I use minicom daily connected to my devices and it's pretty resilient across reboot - good for me.
I'm using Qt to control a serial device. If I send a command to my serial device, I do something like serial->write("command \r\n"). I made a push button which changes the text inside a plain text widget to the response of the serial port. To get the response of the serial port, I'm using serial->readAll(). The problem is it shows the 2nd to last response rather than the one I was expecting. Does Qt have some sort of buffer which is keeping hold of this response?
EDIT
I botched it by using recursion and compared the strings recieved
You might be calling readAll before the response is available. You should hook your code to the readyRead signal to be notified each time new chunk of data is ready to be read. Keep in mind that readyRead can be emitted with any number of bytes available to read - at a minimum, it'll be just one byte. You can't expect the data to be chunked/blocked in any particular way, since the serial port doesn't act as a message-based communication device. Your receiver code must be able to piece the data together from small chunks and act accordingly when it got all the data it needs.
For example, suppose that the device responses have a fixed, known length. You'd only want to react when a complete response has arrived. E.g.:
class Protocol : public QObject {
Q_OBJECT
QBasicTimer m_timer;
QPointer<QIODevice> m_port;
int m_responseLength = 0;
int m_read = 0;
void timerEvent(QTimerEvent * ev) override {
if (ev->timerId() != m_timer.timerId()) return;
m_timer.stop();
emit timedOut();
}
void onData() {
m_read += m_port->bytesAvailable();
if (m_read < m_responseLength)
return;
m_timer.stop();
emit gotResponse(m_port->read(m_responseLength));
m_read -= m_responseLength;
m_responseLength = 0;
}
public:
Q_SIGNAL void gotResponse(const QByteArray &);
Q_SIGNAL void timedOut();
Q_SLOT void sendCommand(const QByteArray & cmd, int responseLength, int cmdTimeout) {
m_responseLength = responseLength;
m_port->write(cmd);
m_timer.start(cmdTimeout, this);
}
explicit Protocol(QIODevice * port, QObject * parent = nullptr) :
QObject(parent), m_port(port) {
connect(m_port, &QIODevice::readyRead, this, &Protocol::onData);
}
};
...
Protocol protocol(0,0);
protocol.sendCommand({"foo"}, 10, 500);
QMetaObject::Connection cmd1;
cmd1 = QObject::connect(&protocol, &Protocol::gotResponse, [&]{
QObject::disconnect(cmd1);
qDebug() << "got response to foo";
});
QObject::connect(&protocol, &Protocol::timedOut, []{ qDebug() << "timed out :("; });
I have mainwindow class with such slots in it:
void MainWindow::connect_to_server(const std::string& nickname,
const std::string& ip,
int port)
{
remote_server = new Server(nickname, ip, port);
connect(remote_server, SIGNAL(error()), SLOT(connection_failed()));
auto thread = new QThread(this);
remote_server->moveToThread(thread);
connect(thread, SIGNAL(started()), remote_server, SLOT(establish_connection()));
connect(remote_server, SIGNAL(stop_thread()), thread, SLOT(quit()));
thread->start();
}
void MainWindow::action_disconnect_triggered() {
if (remote_server == nullptr) {
return;
}
remote_server->disconnect();
remote_server = nullptr;
}
And Server class:
void Server::establish_connection() {
master_socket = std::move(
std::unique_ptr<QTcpSocket>(new QTcpSocket(nullptr))
);
master_socket->connectToHost(ip.c_str(), port);
master_socket->waitForConnected(timeout*1000);
if (master_socket->state() == QAbstractSocket::UnconnectedState) {
disconnect();
emit error();
}
emit stop_thread();
}
void Server::disconnect() {
if (master_socket) {
master_socket->disconnectFromHost();
}
}
Initially, I invoke MainWindow::connect_to_server where client successfully connected to remote server. Then, I invoke MainWindow::action_disconnect_triggered and on this stage I get such error:
Btw, when I run it in OS X 10.11, error does not emerge and all works properly. What am I doing wrong and how can I fix it?
remote_server->disconnect(); might be the issue here.
you do not send the event directly, but you call the function and it gets invoked in your main thread.
try QMetaObject::invokeMethod(remote_server, "disconnect", Qt::QueuedConnection); to see if this problem still exists
cheers
You are connecting the objects before moving to thread. That way Qt cant find it in the new one. Just move to thrrqd before everything and this should work.
I have to update firmware and settings on a device connected to a serial port.
Since this is done by a sequence of commands, I send a command and wait until I recive an answer. Inside the answere (many lines) I search for a string that indicates if the operation is finished successfully.
Serial->write(“boot”, 1000);
Serial->waitForKeyword(“boot successful”);
Serial->sendFile(“image.dat”);
…
So I’ve created a new Thread for this blocking read/write method. Inside the thread I make use of the waitForX() functions.
If I call watiForKeyword() it will call readLines() until it detects the keyword or timesout
bool waitForKeyword(const QString &keyword)
{
QString str;
// read all lines
while(serial->readLines(10000))
{
// check each line
while((str = serial->getLine()) != "")
{
// found!
if(str.contains(keyword))
return true;
}
}
// timeout
return false;
}
readLines() reads everything available and separates it into lines , each line is placed inside a QStringList and to get a string I call getLine() which returns the first string in the list and deletes it.
bool SerialPort::readLines(int waitTimeout)
{
if(!waitForReadyRead(waitTimeout))
{
qDebug() << "Timeout reading" << endl;
return false;
}
QByteArray data = readAll();
while (waitForReadyRead(100))
data += readAll();
char* begin = data.data();
char* ptr = strstr(data, "\r\n");
while(ptr != NULL)
{
ptr+=2;
buffer.append(begin, ptr - begin);
emit readyReadLine(buffer);
lineBuffer.append(QString(buffer)); // store line in Qstringlist
buffer.clear();
begin = ptr;
ptr = strstr(begin, "\r\n");
}
// rest
buffer.append(begin, -1);
return true;
}
The problem is if I send a file via terminal to test the app readLines() will only read a smale part of the file ( 5 Lines or so). Since these lines do not contain the keyword. the function will run once again, but this time it dosnt wait for timeout, readLines just return false immediately.
Whats wrong ?
Also I'm not shure if this is the right approach... Does anyone know how to send a sequenze of commands and wait for a response each time?
Let's use QStateMachine to make this simple. Let's recall how you wished such code would look:
Serial->write("boot", 1000);
Serial->waitForKeyword("boot successful");
Serial->sendFile("image.dat");
Let's put it in a class that has explicit state members for each state the programmer could be in. We'll also have action generators send, expect, etc. that attach given actions to states.
// https://github.com/KubaO/stackoverflown/tree/master/questions/comm-commands-32486198
#include <QtWidgets>
#include <private/qringbuffer_p.h>
#include <type_traits>
[...]
class Programmer : public StatefulObject {
Q_OBJECT
AppPipe m_port { nullptr, QIODevice::ReadWrite, this };
State s_boot { &m_mach, "s_boot" },
s_send { &m_mach, "s_send" };
FinalState s_ok { &m_mach, "s_ok" },
s_failed { &m_mach, "s_failed" };
public:
Programmer(QObject * parent = 0) : StatefulObject(parent) {
connectSignals();
m_mach.setInitialState(&s_boot);
send (&s_boot, &m_port, "boot\n");
expect(&s_boot, &m_port, "boot successful", &s_send, 1000, &s_failed);
send (&s_send, &m_port, ":HULLOTHERE\n:00000001FF\n");
expect(&s_send, &m_port, "load successful", &s_ok, 1000, &s_failed);
}
AppPipe & pipe() { return m_port; }
};
This is fully functional, complete code for the programmer! Completely asynchronous, non-blocking, and it handles timeouts, too.
It's possible to have infrastructure that generates the states on-the-fly, so that you don't have to manually create all the states. The code is much smaller and IMHO easier to comperehend if you have explicit states. Only for complex communication protocols with 50-100+ states would it make sense to get rid of explicit named states.
The AppPipe is a simple intra-process bidirectional pipe that can be used as a stand-in for a real serial port:
// See http://stackoverflow.com/a/32317276/1329652
/// A simple point-to-point intra-process pipe. The other endpoint can live in any
/// thread.
class AppPipe : public QIODevice {
[...]
};
The StatefulObject holds a state machine, some basic signals useful for monitoring the state machine's progress, and the connectSignals method used to connect the signals with the states:
class StatefulObject : public QObject {
Q_OBJECT
Q_PROPERTY (bool running READ isRunning NOTIFY runningChanged)
protected:
QStateMachine m_mach { this };
StatefulObject(QObject * parent = 0) : QObject(parent) {}
void connectSignals() {
connect(&m_mach, &QStateMachine::runningChanged, this, &StatefulObject::runningChanged);
for (auto state : m_mach.findChildren<QAbstractState*>())
QObject::connect(state, &QState::entered, this, [this, state]{
emit stateChanged(state->objectName());
});
}
public:
Q_SLOT void start() { m_mach.start(); }
Q_SIGNAL void runningChanged(bool);
Q_SIGNAL void stateChanged(const QString &);
bool isRunning() const { return m_mach.isRunning(); }
};
The State and FinalState are simple named state wrappers in the style of Qt 3. They allow us to declare the state and give it a name in one go.
template <class S> struct NamedState : S {
NamedState(QState * parent, const char * name) : S(parent) {
this->setObjectName(QLatin1String(name));
}
};
typedef NamedState<QState> State;
typedef NamedState<QFinalState> FinalState;
The action generators are quite simple, too. The meaning of an action generator is "do something when a given state is entered". The state to act on is always given as the first argument. The second and subsequent arguments are specific to the given action. Sometimes, an action might need a target state as well, e.g. if it succeeds or fails.
void send(QAbstractState * src, QIODevice * dev, const QByteArray & data) {
QObject::connect(src, &QState::entered, dev, [dev, data]{
dev->write(data);
});
}
QTimer * delay(QState * src, int ms, QAbstractState * dst) {
auto timer = new QTimer(src);
timer->setSingleShot(true);
timer->setInterval(ms);
QObject::connect(src, &QState::entered, timer, static_cast<void (QTimer::*)()>(&QTimer::start));
QObject::connect(src, &QState::exited, timer, &QTimer::stop);
src->addTransition(timer, SIGNAL(timeout()), dst);
return timer;
}
void expect(QState * src, QIODevice * dev, const QByteArray & data, QAbstractState * dst,
int timeout = 0, QAbstractState * dstTimeout = nullptr)
{
addTransition(src, dst, dev, SIGNAL(readyRead()), [dev, data]{
return hasLine(dev, data);
});
if (timeout) delay(src, timeout, dstTimeout);
}
The hasLine test simply checks all lines that can be read from the device for a given needle. This works fine for this simple communications protocol. You'd need more complex machinery if your communications were more involved. It is necessary to read all the lines, even if you find your needle. That's because this test is invoked from the readyRead signal, and in that signal you must read all the data that fulfills a chosen criterion. Here, the criterion is that the data forms a full line.
static bool hasLine(QIODevice * dev, const QByteArray & needle) {
auto result = false;
while (dev->canReadLine()) {
auto line = dev->readLine();
if (line.contains(needle)) result = true;
}
return result;
}
Adding guarded transitions to states is a bit cumbersome with the default API, so we will wrap it to make it easier to use, and to keep the action generators above readable:
template <typename F>
class GuardedSignalTransition : public QSignalTransition {
F m_guard;
protected:
bool eventTest(QEvent * ev) Q_DECL_OVERRIDE {
return QSignalTransition::eventTest(ev) && m_guard();
}
public:
GuardedSignalTransition(const QObject * sender, const char * signal, F && guard) :
QSignalTransition(sender, signal), m_guard(std::move(guard)) {}
GuardedSignalTransition(const QObject * sender, const char * signal, const F & guard) :
QSignalTransition(sender, signal), m_guard(guard) {}
};
template <typename F> static GuardedSignalTransition<F> *
addTransition(QState * src, QAbstractState *target,
const QObject * sender, const char * signal, F && guard) {
auto t = new GuardedSignalTransition<typename std::decay<F>::type>
(sender, signal, std::forward<F>(guard));
t->setTargetState(target);
src->addTransition(t);
return t;
}
That's about it - if you had a real device, that's all you need. Since I don't have your device, I'll create another StatefulObject to emulate the presumed device behavior:
class Device : public StatefulObject {
Q_OBJECT
AppPipe m_dev { nullptr, QIODevice::ReadWrite, this };
State s_init { &m_mach, "s_init" },
s_booting { &m_mach, "s_booting" },
s_firmware { &m_mach, "s_firmware" };
FinalState s_loaded { &m_mach, "s_loaded" };
public:
Device(QObject * parent = 0) : StatefulObject(parent) {
connectSignals();
m_mach.setInitialState(&s_init);
expect(&s_init, &m_dev, "boot", &s_booting);
delay (&s_booting, 500, &s_firmware);
send (&s_firmware, &m_dev, "boot successful\n");
expect(&s_firmware, &m_dev, ":00000001FF", &s_loaded);
send (&s_loaded, &m_dev, "load successful\n");
}
Q_SLOT void stop() { m_mach.stop(); }
AppPipe & pipe() { return m_dev; }
};
Now let's make it all nicely visualized. We'll have a window with a text browser showing the contents of the communications. Below it will be buttons to start/stop the programmer or the device, and labels indicating the state of the emulated device and the programmer:
int main(int argc, char ** argv) {
using Q = QObject;
QApplication app{argc, argv};
Device dev;
Programmer prog;
QWidget w;
QGridLayout grid{&w};
QTextBrowser comms;
QPushButton devStart{"Start Device"}, devStop{"Stop Device"},
progStart{"Start Programmer"};
QLabel devState, progState;
grid.addWidget(&comms, 0, 0, 1, 3);
grid.addWidget(&devState, 1, 0, 1, 2);
grid.addWidget(&progState, 1, 2);
grid.addWidget(&devStart, 2, 0);
grid.addWidget(&devStop, 2, 1);
grid.addWidget(&progStart, 2, 2);
devStop.setDisabled(true);
w.show();
We'll connect the device's and programmer's AppPipes. We'll also visualize what the programmer is sending and receiving:
dev.pipe().addOther(&prog.pipe());
prog.pipe().addOther(&dev.pipe());
Q::connect(&prog.pipe(), &AppPipe::hasOutgoing, &comms, [&](const QByteArray & data){
comms.append(formatData(">", "blue", data));
});
Q::connect(&prog.pipe(), &AppPipe::hasIncoming, &comms, [&](const QByteArray & data){
comms.append(formatData("<", "green", data));
});
Finally, we'll connect the buttons and labels:
Q::connect(&devStart, &QPushButton::clicked, &dev, &Device::start);
Q::connect(&devStop, &QPushButton::clicked, &dev, &Device::stop);
Q::connect(&dev, &Device::runningChanged, &devStart, &QPushButton::setDisabled);
Q::connect(&dev, &Device::runningChanged, &devStop, &QPushButton::setEnabled);
Q::connect(&dev, &Device::stateChanged, &devState, &QLabel::setText);
Q::connect(&progStart, &QPushButton::clicked, &prog, &Programmer::start);
Q::connect(&prog, &Programmer::runningChanged, &progStart, &QPushButton::setDisabled);
Q::connect(&prog, &Programmer::stateChanged, &progState, &QLabel::setText);
return app.exec();
}
#include "main.moc"
The Programmer and Device could live in any thread. I've left them in the main thread since there's no reason to move them out, but you could put both into a dedicated thread, or each into its own thread, or into threads shared with other objects, etc. It's completely transparent since AppPipe supports communications across the threads. This would also be the case if QSerialPort was used instead of AppPipe. All that matters is that each instance of a QIODevice is used from one thread only. Everything else happens via signal/slot connections.
E.g. if you wanted the Programmer to live in a dedicated thread, you'd add the following somewhere in main:
// fix QThread brokenness
struct Thread : QThread { ~Thread() { quit(); wait(); } };
Thread progThread;
prog.moveToThread(&progThread);
progThread.start();
A little helper formats the data to make it easier to read:
static QString formatData(const char * prefix, const char * color, const QByteArray & data) {
auto text = QString::fromLatin1(data).toHtmlEscaped();
if (text.endsWith('\n')) text.truncate(text.size() - 1);
text.replace(QLatin1Char('\n'), QString::fromLatin1("<br/>%1 ").arg(QLatin1String(prefix)));
return QString::fromLatin1("<font color=\"%1\">%2 %3</font><br/>")
.arg(QLatin1String(color)).arg(QLatin1String(prefix)).arg(text);
}
I'm not sure indeed this is the right approach.
You're polling with waitForReadyRead(). But since the serial port is a QIODevice, it will emit a void QIODevice::readyRead() signal when something will arrive on the serial port. Why not connect this signal to your input parsing code? No need for waitForReadyRead().
Also/on the other hand: "...this time it doesn't wait for timeout, readLines just return false immediately. Whats wrong ?"
Quoting the documentation:
If waitForReadyRead() returns false, the connection has been
closed or an error has occurred.
(emphasis mine)
From my experience as an embedded developer, it is not impossible that you put the device into kind of a "firmware upgrade" mode, and that by doing so the device rebooted into a special boot mode (not running the firmware you're about to update) and thus closed the connection. No way to tell unless it's documented/you have contact with the device developers. Not so obvious to check using a serial terminal to type your commands and witness that, I use minicom daily connected to my devices and it's pretty resilient across reboot - good for me.