serial example c++ qt serial-port blocking

c++ - example - qt serial port arduino



Enviar una secuencia de comandos y esperar respuesta (2)

No estoy seguro de que este sea el enfoque correcto.

Estás waitForReadyRead() con waitForReadyRead() . Pero como el puerto serie es un QIODevice , emitirá una QIODevice::readyRead() nula de QIODevice::readyRead() cuando algo llegue al puerto serie. ¿Por qué no conecta 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 false inmediatamente. ¿Qué pasa?"

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 coloque el dispositivo en una especie de modo de "actualización de firmware", y al hacerlo, el dispositivo se reinicia en un modo de arranque especial (no ejecuta el firmware estamos a punto de actualizar) y así cerró la conexión. No hay forma de saberlo a menos que esté documentado / tenga contacto con los desarrolladores del dispositivo. No es tan obvio verificar usando un terminal en serie para escribir sus comandos y ser testigo de eso, uso minicom conectado diariamente a mis dispositivos y es bastante resistente durante el reinicio, bueno para mí.

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 recibir una respuesta. Dentro del answere (muchas líneas) busco una cadena que indique si la operación ha finalizado con éxito.

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

Así que he creado un nuevo hilo para este método de lectura / escritura de bloqueo. 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 readLines () solo leerá 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 se espera el tiempo de espera, readLines solo devuelve false inmediatamente. Que 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 simplificar esto. Recordemos cómo deseabas que se viera ese código:

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

Pongámoslo en una clase que tenga miembros de estado explícitos para cada estado en el que pueda estar el programador. También tendremos generadores de acciones que send , expect , etc.

// https://github.com/KubaO/n/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; } };

¡Este es un código completamente funcional para el programador! Completamente asíncrono, sin bloqueo, 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 comprender si tiene estados explícitos. Solo para protocolos de comunicación complejos con 50-100 + estados tendría sentido deshacerse de los estados con nombre explícito.

AppPipe es una tubería bidireccional simple dentro del proceso que se puede utilizar como un sustituto para un puerto serie real:

// See http://.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 monitorear 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<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(); } };

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

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;

Los generadores de acción también son bastante simples. 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 posteriores son específicos de la acción dada. A veces, una acción también puede necesitar un estado objetivo, 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<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); }

La prueba hasLine simplemente verifica todas las líneas que se pueden leer desde el dispositivo para una aguja determinada. Esto funciona bien para este protocolo de comunicaciones simple. Necesitaría maquinaria más compleja si sus comunicaciones estuvieran más involucradas. Es necesario leer todas las líneas, incluso si encuentra su aguja. Esto se debe a que esta prueba se invoca desde la señal readyRead , y en esa señal debe leer todos los datos que cumplan 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 ajustaremos para que sea más fácil de usar y para que los generadores de acción sean legibles:

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

Eso es todo: si tenía un dispositivo real, eso es todo lo que necesita. Como no tengo su dispositivo, crearé otro StatefulObject para emular el supuesto comportamiento 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. Debajo habrá botones para iniciar / detener el programador o el dispositivo, y etiquetas que indican el estado del dispositivo emulado y el programador:

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 los AppPipe s del dispositivo y del programador. También visualizaremos lo que el programador 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("&gt;", "blue", data)); }); Q::connect(&prog.pipe(), &AppPipe::hasIncoming, &comms, [&](const QByteArray & data){ comms.append(formatData("&lt;", "green", data)); });

Finalmente, conectaremos los botones y etiquetas:

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 pueden vivir en cualquier hilo. Los dejé en el hilo principal ya que no hay razón para moverlos, pero podría poner ambos en un hilo dedicado, o cada uno en su propio hilo, o en hilos compartidos con otros objetos, etc. Es completamente transparente ya que AppPipe admite comunicaciones a través de los hilos. Este también sería el caso si se utilizara AppPipe lugar de AppPipe . Lo único que importa es que cada instancia de un QIODevice se use desde un solo hilo. Todo lo demás sucede a través de conexiones de señal / ranura.

Por ejemplo, si quisieras que el Programmer viviera en un hilo dedicado, agregarías lo siguiente en algún lugar de 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("<br/>%1 ").arg(QLatin1String(prefix))); return QString::fromLatin1("<font color=/"%1/">%2 %3</font><br/>") .arg(QLatin1String(color)).arg(QLatin1String(prefix)).arg(text); }