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