Ich muss Firmware und Einstellungen auf einem Gerät aktualisieren, das an eine serielle Schnittstelle angeschlossen ist. Da dies durch eine Folge von Befehlen geschieht, sende ich einen Befehl und warte, bis ich eine Antwort erhalte. Innerhalb der Antwort (viele Zeilen) suche ich nach einer Zeichenfolge, die angibt, ob die Operation erfolgreich abgeschlossen wurde.
Serial->write(“boot”, 1000);
Serial->waitForKeyword(“boot successful”);
Serial->sendFile(“image.dat”);
…
Also habe ich einen neuen Thread für diese blockierende Lese-/Schreibmethode erstellt. Innerhalb des Threads verwende ich die Funktionen waitForX(). Wenn ich watiForKeyword() aufrufe, wird readLines() aufgerufen, bis es das Schlüsselwort oder die Zeitüberschreitung erkennt
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() liest alles verfügbare und trennt es in Zeilen, jede Zeile wird in eine QStringList platziert und um eine Zeichenfolge zu erhalten, rufe ich getLine() auf, die die erste Zeichenfolge in der Liste zurückgibt und löscht.
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;
}
Das Problem ist, wenn ich eine Datei per Terminal sende, um die App zu testen, liest readLines() nur einen kleinen Teil der Datei (5 Zeilen oder so). Da diese Zeilen das Schlüsselwort nicht enthalten. Die Funktion wird noch einmal ausgeführt, aber dieses Mal wartet sie nicht auf das Timeout, readLines gibt einfach sofort false zurück. Was ist falsch ? Ich bin mir auch nicht sicher, ob dies der richtige Ansatz ist … Weiß jemand, wie man eine Sequenz von Befehlen sendet und jedes Mal auf eine Antwort wartet?
Lassen Sie uns verwenden QStateMachine
um dies einfach zu machen. Erinnern wir uns, wie Sie sich gewünscht haben, dass ein solcher Code aussehen würde:
Serial->write("boot", 1000);
Serial->waitForKeyword("boot successful");
Serial->sendFile("image.dat");
Stellen wir es in eine Klasse, die explizite Zustandsmitglieder für jeden Zustand hat, in dem sich der Programmierer befinden könnte. Wir werden auch Aktionsgeneratoren haben send
, expect
usw., die gegebene Aktionen an Zustände anhängen.
// 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; }
};
Dies ist ein voll funktionsfähiger, vollständiger Code für den Programmierer! Völlig asynchron, nicht blockierend und verarbeitet auch Zeitüberschreitungen.
Es ist möglich, eine Infrastruktur zu haben, die die Zustände on-the-fly generiert, sodass Sie nicht alle Zustände manuell erstellen müssen. Der Code ist viel kleiner und meiner Meinung nach einfacher zu verstehen, wenn Sie explizite Zustände haben. Nur für komplexe Kommunikationsprotokolle mit 50-100+ Zuständen wäre es sinnvoll, explizit benannte Zustände loszuwerden.
Die AppPipe
ist eine einfache prozessinterne bidirektionale Pipe, die als Ersatz für eine echte serielle Schnittstelle verwendet werden kann:
// 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 {
[...]
};
Die StatefulObject
enthält eine Zustandsmaschine, einige grundlegende Signale, die zum Überwachen des Fortschritts der Zustandsmaschine nützlich sind, und die connectSignals
Methode zur Verbindung der Signale mit den Zuständen:
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(); }
};
Die State
und FinalState
sind einfache Named State Wrapper im Stil von Qt 3. Sie erlauben es uns, den Zustand zu deklarieren und ihm einen Namen in einem Rutsch zu geben.
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;
Die Aktionsgeneratoren sind ebenfalls recht einfach. Die Bedeutung eines Aktionsgenerators ist “etwas tun, wenn ein bestimmter Zustand erreicht wird”. Als erstes Argument wird immer der Zustand angegeben, auf den reagiert werden soll. Das zweite und die folgenden Argumente sind spezifisch für die gegebene Aktion. Manchmal benötigt eine Aktion auch einen Zielzustand, zB ob sie erfolgreich ist oder fehlschlägt.
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);
}
Die hasLine
test prüft einfach alle Zeilen, die vom Gerät für eine bestimmte Nadel gelesen werden können. Dies funktioniert gut für dieses einfache Kommunikationsprotokoll. Sie würden komplexere Maschinen benötigen, wenn Ihre Kommunikation stärker involviert wäre. Es ist notwendig, alle Zeilen zu lesen, auch wenn Sie Ihre Nadel finden. Das liegt daran, dass dieser Test von aufgerufen wird readyRead
Signal, und in diesem Signal müssen Sie alle Daten lesen, die ein ausgewähltes Kriterium erfüllen. Hier ist das Kriterium, dass die Daten eine vollständige Zeile bilden.
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;
}
Das Hinzufügen von geschützten Übergängen zu Zuständen ist mit der Standard-API etwas umständlich, daher werden wir sie umschließen, um die Verwendung zu vereinfachen und die obigen Aktionsgeneratoren lesbar zu halten:
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
return t;
}
Das war es auch schon – wenn Sie ein echtes Gerät hätten, ist das alles, was Sie brauchen. Da ich Ihr Gerät nicht habe, werde ich ein anderes erstellen StatefulObject
um das vermutete Geräteverhalten zu emulieren:
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; }
};
Lassen Sie uns nun alles schön visualisieren. Wir haben ein Fenster mit einem Textbrowser, der den Inhalt der Kommunikation anzeigt. Darunter befinden sich Schaltflächen zum Starten/Stoppen des Programmiergeräts oder des Geräts sowie Beschriftungen, die den Status des emulierten Geräts und des Programmiergeräts anzeigen:

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();
Wir verbinden das Gerät und das Programmiergerät AppPipe
S. Wir visualisieren auch, was der Programmierer sendet und empfängt:
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));
});
Schließlich verbinden wir die Schaltflächen und Beschriftungen:
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"
Die Programmer
und Device
könnte in jedem Thread leben. Ich habe sie im Hauptthread gelassen, da es keinen Grund gibt, sie zu verschieben, aber Sie könnten beide in einen dedizierten Thread oder jeden in einen eigenen Thread oder in Threads einfügen, die mit anderen Objekten geteilt werden usw. Seitdem ist es vollständig transparent AppPipe
unterstützt die Kommunikation über die Threads hinweg. Dies wäre auch der Fall, wenn QSerialPort
wurde stattdessen verwendet AppPipe
. Wichtig ist nur, dass jede Instanz von a QIODevice
wird nur von einem Thread verwendet. Alles andere passiert über Signal/Slot-Verbindungen.
Wollte man zB das Programmer
Um in einem eigenen Thread zu leben, würden Sie Folgendes irgendwo hinzufügen main
:
// fix QThread brokenness
struct Thread : QThread { ~Thread() { quit(); wait(); } };
Thread progThread;
prog.moveToThread(&progThread);
progThread.start();
Ein kleiner Helfer formatiert die Daten, um sie besser lesbar zu machen:
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);
}
Ohne zu wissen, was die Serial-Klasse ist, kann Ihre ursprüngliche Frage, warum der Rest der Datei ignoriert wurde, nicht beantwortet werden. Beachten Sie jedoch, dass sich Geräte mit serieller Schnittstelle unter Linux in Bezug auf nicht blockierende E / A nicht wie Sockets verhalten. Dies könnte der Grund sein. (Sie können grundsätzlich keine nicht blockierende E/A mit seriellen Ports verwenden, deshalb simuliert die offizielle QSerialPort-Klasse, die in Qt 5.1 hinzugefügt wurde, asynchrone Kommunikation mit einem Thread.)
– VielZuLernen
2. Oktober 2017 um 23:36 Uhr