Enviar una secuencia de comandos y esperar respuesta

Tengo que actualizar el firmware y la configuración en un dispositivo conectado a un puerto serie. Como esto se hace mediante una secuencia de comandos, envío un comando y espero hasta que reciba una respuesta. Dentro de la respuesta (muchas líneas), busco una cadena que indique si la operación finalizó correctamente.

Serial->write(“boot”, 1000); Serial->waitForKeyword(“boot successful”); Serial->sendFile(“image.dat”); … 

Así que he creado un nuevo hilo para este método de locking de lectura / escritura. Dentro del hilo, utilizo las funciones waitForX (). Si llamo a watiForKeyword () llamará a readLines () hasta que detecte la palabra clave o el tiempo de espera

 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 () lee todo lo disponible y lo separa en líneas, cada línea se coloca dentro de una QStringList y para obtener una cadena que llamo getLine () que devuelve la primera cadena de la lista y la elimina.

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

El problema es que si envío un archivo a través del terminal para probar la aplicación, las líneas de lectura () solo leerán una pequeña parte del archivo (5 líneas más o menos). Dado que estas líneas no contienen la palabra clave. la función se ejecutará una vez más, pero esta vez no espera el tiempo de espera, readLines simplemente devuelve falso inmediatamente. ¿Qué pasa? Además, no estoy seguro si este es el enfoque correcto … ¿Alguien sabe cómo enviar una secuencia de comandos y esperar una respuesta cada vez?

QStateMachine para hacer esto simple. Recordemos cómo deseaba que se vea ese código:

 Serial->write(“boot”, 1000); Serial->waitForKeyword(“boot successful”); Serial->sendFile(“image.dat”); 

Vamos a ponerlo en una clase que tenga miembros de estados explícitos para cada estado en el que el progtwigdor pueda estar. También tendremos generadores de acciones send , expect , etc. que atan acciones dadas a los estados.

 // https://github.com/KubaO/stackoverflown/tree/master/questions/comm-commands-32486198 #include  #include  #include  [...] 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; } }; 

Esto es completamente funcional, ¡código completo para el progtwigdor! Completamente asíncrono, sin locking, y también maneja los tiempos de espera.

Es posible tener una infraestructura que genere los estados sobre la marcha, para que no tenga que crear manualmente todos los estados. El código es mucho más pequeño y en mi humilde opinión es más fácil de competir si tienes estados explícitos. Solo para los protocolos de comunicación complejos con 50-100 + estados, tendría sentido eliminar los estados nombrados explícitos.

El AppPipe es un conducto bidireccional sencillo dentro del proceso que se puede usar como sustituto de un puerto serie real:

 // 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 { [...] }; 

StatefulObject contiene una máquina de estado, algunas señales básicas útiles para supervisar el progreso de la máquina de estado y el método connectSignals utilizado para conectar las señales con los estados:

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

State y FinalState son simples envoltorios de estado con nombre en el estilo de Qt 3. Nos permiten declarar el estado y darle un nombre de una vez.

 template  struct NamedState : S { NamedState(QState * parent, const char * name) : S(parent) { this->setObjectName(QLatin1String(name)); } }; typedef NamedState State; typedef NamedState FinalState; 

Los generadores de acción son bastante simples también. El significado de un generador de acciones es “hacer algo cuando se ingresa un estado dado”. El estado para actuar siempre se da como primer argumento. El segundo argumento y los siguientes son específicos de la acción dada. A veces, una acción también puede necesitar un estado objective, por ejemplo, si tiene éxito o falla.

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

La prueba de hasLine simplemente verifica todas las líneas que se pueden leer desde el dispositivo para una aguja determinada. Esto funciona bien para este simple protocolo de comunicaciones. Necesitaría maquinaria más compleja si sus comunicaciones fueran más complicadas. Es necesario leer todas las líneas, incluso si encuentra su aguja. Esto se debe a que esta prueba se invoca a partir de la señal de readyRead , y en esa señal debe leer todos los datos que cumplen un criterio elegido. Aquí, el criterio es que los datos forman una línea completa.

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

Agregar transiciones protegidas a los estados es un poco engorroso con la API predeterminada, por lo que lo envolveremos para que sea más fácil de usar y para que los generadores de acciones sean legibles:

 template  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  static GuardedSignalTransition * addTransition(QState * src, QAbstractState *target, const QObject * sender, const char * signal, F && guard) { auto t = new GuardedSignalTransition::type> (sender, signal, std::forward(guard)); t->setTargetState(target); src->addTransition(t); return t; } 

Eso es todo. Si tuviera un dispositivo real, eso es todo lo que necesita. Como no tengo su dispositivo, crearé otro StatefulObject para emular el comportamiento presunto del dispositivo:

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

Ahora hagámoslo todo muy bien visualizado. Tendremos una ventana con un navegador de texto que muestra el contenido de las comunicaciones. A continuación se muestran los botones para iniciar / detener el progtwigdor o el dispositivo, y las tags que indican el estado del dispositivo emulado y el progtwigdor:

captura de pantalla

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

Conectaremos las AppPipe s del dispositivo y del progtwigdor. También visualizaremos lo que el progtwigdor está enviando y recibiendo:

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

Finalmente, conectaremos los botones y las tags:

  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" 

El Programmer y el Device podrían vivir en cualquier hilo. Los dejé en el hilo principal, ya que no hay motivo para moverlos, pero podrías ponerlos en un hilo dedicado, o cada uno en su propio hilo, o en hilos compartidos con otros objetos, etc. Es completamente transparente desde AppPipe admite comunicaciones a través de los hilos. Este también sería el caso si se utilizara AppPipe lugar de AppPipe . Todo lo que importa es que cada instancia de un QIODevice se utiliza solo desde un subproceso. Todo lo demás sucede a través de conexiones de señal / ranura.

Por ejemplo, si quisiera que el Programmer viviera en un hilo dedicado, agregaría lo siguiente en algún lugar del main :

  // fix QThread brokenness struct Thread : QThread { ~Thread() { quit(); wait(); } }; Thread progThread; prog.moveToThread(&progThread); progThread.start(); 

Un pequeño ayudante formatea los datos para que sea más fácil de leer:

 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("
%1 ").arg(QLatin1String(prefix))); return QString::fromLatin1("%2 %3
") .arg(QLatin1String(color)).arg(QLatin1String(prefix)).arg(text); }

No estoy seguro de que este sea el enfoque correcto.

Estás encuestando con waitForReadyRead() . Pero dado que el puerto serie es un QIODevice , emitirá una QIODevice::readyRead() cuando algo llegue al puerto serie. ¿Por qué no conectar esta señal a su código de análisis de entrada? No hay necesidad de waitForReadyRead() .

También / por otro lado: “… esta vez no espera el tiempo de espera, ReadLines simplemente devuelve falso inmediatamente. ¿Qué sucede?”

Citando la documentación:

Si waitForReadyRead () devuelve falso, la conexión se ha cerrado o se ha producido un error.

(énfasis mío) Desde mi experiencia como desarrollador integrado, no es imposible que pongas el dispositivo en una especie de modo de “actualización de firmware” y, al hacerlo, el dispositivo se reinició en un modo de inicio especial (no ejecutando el firmware que estamos a punto de actualizar) y así cerramos la conexión. No hay forma de saberlo a menos que esté documentado / tenga contacto con los desarrolladores del dispositivo. No es tan obvio que verifique el uso de un terminal en serie para escribir sus comandos y atestiguar que utilizo minicom conectado a diario a mis dispositivos y que es bastante flexible durante el reinicio, es bueno para mí.