发送一系列命令并等待响应
我必须更新连接到串行端口的设备上的固件和设置。 由于这是通过一系列命令完成的,我发送一个命令并等待,直到我收到一个答案。 在answere里面(很多行),我search一个string来表示操作是否成功完成。
Serial->write(“boot”, 1000); Serial->waitForKeyword(“boot successful”); Serial->sendFile(“image.dat”); …
所以我为这个阻塞读/写方法创build了一个新的线程。 在线程内部,我使用了waitForX()函数。 如果我调用watiForKeyword(),它将调用readLines(),直到它检测到关键字或超时
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()读取所有可用的内容并将其分隔成行,每行放在一个QStringList中,并得到一个string,我调用getLine(),它返回列表中的第一个string并删除它。
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; }
问题是如果我通过terminal发送一个文件来testing应用程序readLines()将只读取文件的一个微妙的部分(5行左右)。 由于这些行不包含关键字。 该函数将再次运行,但是这次dosnt等待超时,readLine立即返回false。 怎么了 ? 另外,我不知道这是否是正确的方法…有谁知道如何发送一个命令序列,并等待每一个响应?
让我们使用QStateMachine
来简化。 让我们回想一下你希望这样的代码看起来如何:
Serial->write(“boot”, 1000); Serial->waitForKeyword(“boot successful”); Serial->sendFile(“image.dat”);
让我们把它放在一个有程序员可以进入的每个状态的明确状态成员的类中。我们还将有动作生成器send
, expect
等将给定的动作附加到状态。
// 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; } };
这是程序员完整的function,完整的代码! 完全asynchronous,非阻塞,它也处理超时。
可以使基础架构即时生成状态,以便您不必手动创build所有状态。 代码更小,恕我直言,更容易理解,如果你有明确的国家。 只有对于具有50-100 +状态的复杂通信协议,才能清除明确的命名状态。
AppPipe
是一个简单的进程内双向pipe道,可以作为一个真正的串行端口使用:
// 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
持有一个状态机,一些用于监视状态机进度的基本信号,以及用于连接信号和状态的connectSignals
方法:
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
和FinalState
是Qt 3风格的简单命名状态包装。它们允许我们一次性声明状态并为其命名。
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;
动作生成器也非常简单。 动作发生器的含义是“在给定状态input时做某事”。 总是作为第一个论点来给予采取行动的国家。 第二个和随后的论点是特定的行动。 有时,一个动作也可能需要一个目标状态,例如,如果成功或失败。
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); }
hasLine
testing只是简单地检查可以从设备读取给定针的所有线。 这对于这个简单的通信协议工作正常。 如果您的通讯涉及更多,则需要更复杂的机器。 即使find针头,也需要阅读所有的文字。 这是因为这个testing是从readyRead
信号中调用的,在那个信号中,你必须读取所有符合select标准的数据。 这里的标准是数据形成一个完整的线。
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; }
添加防护转换到状态对于默认的API来说有点麻烦,所以我们将它包装起来以使它更易于使用,并且保持动作生成器的可读性:
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; }
就是这样 – 如果你有一个真正的设备,这就是你所需要的。 由于我没有你的设备,我将创build另一个StatefulObject
来模拟推测的设备行为:
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; } };
现在让我们把这一切都很好地形象化。 我们将有一个窗口和一个文本浏览器,显示通信的内容。 下面是启动/停止编程器或设备的button,以及表示仿真设备和编程器状态的标签:
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();
我们将连接设备和程序员的AppPipe
。 我们还将可视化程序员发送和接收的内容:
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)); });
最后,我们将连接button和标签:
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"
Programmer
和Device
可以居住在任何线程。 我把它们留在主线程中,因为没有理由把它们移出去,但是你可以把它们放到一个专用的线程中,或者放到它自己的线程中,或者放到与其他对象共享的线程中,等等。它是完全透明的,因为AppPipe
支持跨线程的通信。 如果使用QSerialPort
而不是AppPipe
,情况也是AppPipe
。 重要的是QIODevice
每个实例只能从一个线程使用。 其他一切都通过信号/插槽连接进行。
例如,如果你想让Programmer
生活在一个专门的线程中,你可以在main
添加以下内容:
// fix QThread brokenness struct Thread : QThread { ~Thread() { quit(); wait(); } }; Thread progThread; prog.moveToThread(&progThread); progThread.start();
一个小助手格式化数据,使其更容易阅读:
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); }
我不确定这是否正确。
您正在使用waitForReadyRead()
轮询。 但是由于串行端口是一个QIODevice
,当串行端口到达时,它将发出一个无效的QIODevice::readyRead()
信号。 为什么不把这个信号连接到你的inputparsing代码? 不需要waitForReadyRead()
。
另一方面:“…这次不等待超时,readLine立即返回false,什么错误?
引用文档:
如果waitForReadyRead()返回false, 则连接已closures或发生错误。
(我的重点)从我作为embedded式开发人员的经验来看,将设备置于“固件升级”模式并不是不可能的,通过这样做,设备重新启动进入特殊的启动模式(不运行固件即将更新),从而closures连接。 没有办法说,除非有文件logging/你有设备开发人员的联系。 使用串行terminal来检查命令并不是很明显,并且certificate我使用每天连接到我的设备的minicom
,并且在重新启动时它很有弹性 – 对我很好。