Senden einer Folge von Befehlen und Warten auf Antwort

Lesezeit: 13 Minuten

Senden einer Folge von Befehlen und Warten auf Antwort
Silbersirkel

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?

  • 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

1646904009 884 Senden einer Folge von Befehlen und Warten auf Antwort
Kuba hat Monica nicht vergessen

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, expectusw., 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:

Bildschirmfoto

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 AppPipeS. 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("&gt;", "blue", data));
   });
   Q::connect(&prog.pipe(), &AppPipe::hasIncoming, &comms, [&](const QByteArray & data){
      comms.append(formatData("&lt;", "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);
}

  • Dies sollte mehr als 100 Antworten sein, ich konnte mich nicht davon abhalten zu kommentieren 🙂

    – Mike

    26. August 2016 um 22:18 Uhr

  • @KubaOber Könnten Sie mehr über die bewachten Übergänge erklären?

    – wanyancan

    4. September 2021 um 17:04 Uhr

  • @wanyancan, könnte helfen: Verknüpfung

    – LRDPRDX

    17. November 2021 um 11:40 Uhr

Ich bin mir nicht sicher, ob das wirklich der richtige Ansatz ist.

Du befragst mit waitForReadyRead(). Aber da die serielle Schnittstelle eine ist QIODevicees wird eine Leere emittieren QIODevice::readyRead() signalisiert, wenn etwas an der seriellen Schnittstelle ankommt. Warum verbinden Sie dieses Signal nicht mit Ihrem Eingabe-Parsing-Code? Kein Bedarf für waitForReadyRead().

Auch/andererseits: “… dieses Mal wartet es nicht auf Timeout, readLines gibt einfach sofort false zurück. Was ist falsch?”

Zitieren der Dokumentation:

Wenn waitForReadyRead() false zurückgibt, die Verbindung wurde geschlossen oder es ist ein Fehler aufgetreten.

(Hervorhebung von mir) Aus meiner Erfahrung als Embedded-Entwickler ist es nicht unmöglich, dass Sie das Gerät in eine Art “Firmware-Upgrade” -Modus versetzen und das Gerät dadurch in einen speziellen Boot-Modus neu gestartet wird (die Firmware wird nicht ausgeführt). bin gerade dabei zu updaten) und damit die Verbindung geschlossen. Keine Möglichkeit zu sagen, es sei denn, es ist dokumentiert / Sie haben Kontakt mit den Geräteentwicklern. Nicht so offensichtlich, mit einem seriellen Terminal zu überprüfen, um Ihre Befehle einzugeben und das zu bezeugen, verwende ich minicom täglich mit meinen Geräten verbunden und es ist beim Neustart ziemlich robust – gut für mich.

  • Beim Testen des Programms verwende ich zwei virtuelle COM-Ports. Einer ist mit der App verbunden und der andere mit Realterm. Ich sende Dateien per Realterm an und überprüfe, was die App macht. Wenn ich den readLine-Code direkt mit dem readyRead()-Signal verbinde, hat die Eventloop keine Chance, Ereignisse zu aktualisieren, da ich nur innerhalb von waitForKeyword() rotiere. Gibt es eine Möglichkeit, die Ereignisschleife zum Aktualisieren zu zwingen? Wenn ja, wie setze ich das in meinen Funktionen um?

    – Silbersirkel

    9. September 2015 um 19:30 Uhr

  • @silversircel Schreiben Sie keinen pseudosynchronen Code. Sie sollten keine verwenden waitForXxxx Methoden überhaupt. Keiner. Vergiss, dass es sie gibt, und überlege dann, wie du sie lösen kannst. Alles, was Sie dann tun können und müssen, ist, sich mit dem zu verbinden readyRead Signal. Und es wird alles funktionieren, da Sie nicht mehr mit blockieren wait Methodenaufrufe.

    – Kuba hat Monica nicht vergessen

    9. September 2015 um 20:12 Uhr


  • @KubaOber Aber wie warte ich, wenn alles asynchron ist? Das ist genau der Punkt, an dem ich Probleme habe. Wenn ich einen Befehl sende und eine Antwort erhalte, führt das zu einem Slot. Sollte ich in einem großen Switch-Fall nach jedem möglichen Keyword suchen und entscheiden, was als nächstes zu tun ist? Es muss eine Art Designmuster geben. Ich brauche so etwas wie eine Zustandsmaschine … mach Schritt 1, dann mach Schritt 2, dann … Wie realisiere ich so etwas mit Signalen und Slots?

    – Silbersirkel

    9. September 2015 um 20:30 Uhr

  • @silversircel Bingo. QStateMachine. Ich wollte, dass du erkennst, dass es das ist, was du brauchst 🙂

    – Kuba hat Monica nicht vergessen

    9. September 2015 um 21:51 Uhr

987310cookie-checkSenden einer Folge von Befehlen und Warten auf Antwort

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy