【问题标题】:Sending a sequence of commands and wait for response发送一系列命令并等待响应
【发布时间】:2015-12-05 19:33:38
【问题描述】:

我必须更新连接到串行端口的设备上的固件和设置。 因为这是通过一系列命令完成的,所以我发送一个命令并等到我收到答案。在被回答者(多行)中,我搜索一个指示操作是否成功完成的字符串。

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

所以我为这种阻塞读/写方法创建了一个新线程。在线程内部,我使用了 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 中并获取一个字符串,我调用 getLine() 它返回列表中的第一个字符串并将其删除。

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

问题是,如果我通过终端发送文件来测试应用程序 readLines() 只会读取文件的一小部分(5 行左右)。由于这些行不包含关键字。该函数将再次运行,但这次它不会等待超时,readLines 只是立即返回 false。 怎么了 ? 另外,我不确定这是否是正确的方法...有谁知道如何发送一系列命令并每次都等待响应?

【问题讨论】:

  • 在不知道 Serial 类是什么的情况下,无法回答您为什么忽略文件其余部分的原始问题。但是,请注意,在 linux 中,串行端口设备在非阻塞 IO 方面的行为不像套接字,所以这可能就是原因。 (基本上不能对串口使用非阻塞 I/O,这就是 Qt 5.1 中添加的官方 QSerialPort 类模拟与线程的异步通信的原因。)

标签: c++ qt serial-port blocking


【解决方案1】:

让我们使用QStateMachine 来简化此操作。让我们回想一下您希望这样的代码看起来如何:

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

让我们把它放在一个类中,该类对于程序员可能处于的每个状态都有明确的状态成员。我们还将有动作生成器sendexpect 等,将给定的动作附加到状态。

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

这是面向程序员的功能齐全、完整的代码!完全异步、非阻塞,它也可以处理超时。

可以拥有即时生成状态的基础架构,这样您就不必手动创建所有状态。如果您有明确的状态,代码要小得多,恕我直言更容易理解。只有对于具有 50-100 多个状态的复杂通信协议,摆脱显式命名状态才有意义。

AppPipe 是一个简单的进程内双向管道,可以用作真实串口的替代:

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

StateFinalState 是 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;

动作生成器也很简单。动作生成器的含义是“在进入给定状态时做某事”。要采取行动的状态总是作为第一个参数给出。第二个和后续参数特定于给定的操作。有时,一个动作也可能需要一个目标状态,例如是成功还是失败。

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 测试只检查可以从设备中读取给定针的所有行。这适用于这个简单的通信协议。如果您的通信涉及更多,您将需要更复杂的机器。即使您找到了针,也有必要阅读所有行。这是因为这个测试是从readyRead 信号中调用的,并且在该信号中,您必须读取满足所选标准的所有数据。这里的标准是数据形成一条完整的线。

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

就是这样 - 如果你有一个真正的设备,这就是你所需要的。由于我没有您的设备,我将创建另一个 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; }
};

现在让我们把这一切都很好地可视化。我们将有一个带有文本浏览器的窗口,显示通信内容。下面是启动/停止编程器或设备的按钮,以及指示仿真设备和编程器状态的标签:

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

我们将连接设备和程序员的AppPipes。我们还将可视化程序员发送和接收的内容:

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

最后,我们将连接按钮和标签:

   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"

ProgrammerDevice 可以存在于任何线程中。我将它们留在主线程中,因为没有理由将它们移出,但您可以将它们放入专用线程,或每个放入自己的线程,或放入与其他对象共享的线程等。它是完全透明的,因为@ 987654349@ 支持跨线程通信。如果使用QSerialPort 而不是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);
}

【讨论】:

  • 这应该是 100 多个答案,我无法阻止自己发表评论 :)
  • @KubaOber 你能解释更多关于受保护的转换吗?
  • @wanyancan,可能会有所帮助:link
【解决方案2】:

我不确定这确实是正确的方法。

您正在使用waitForReadyRead() 进行投票。但是由于串口是QIODevice,当有东西到达串口时,它会发出一个无效的QIODevice::readyRead()信号。为什么不将此信号连接到您的输入解析代码?不需要waitForReadyRead()

另外/另一方面:“...这一次它不等待超时,readLines 只是立即返回 false。有什么问题?”

引用文档:

如果 waitForReadyRead() 返回 false,连接已经 关闭或发生错误。

(强调我的) 根据我作为嵌入式开发人员的经验,您将设备置于“固件升级”模式并非不可能,并且通过这样做,设备会重新启动到特殊的启动模式(不运行您将要运行的固件)更新),从而关闭了连接。除非已记录在案/您已与设备开发人员联系,否则无法判断。使用串行终端输入命令并见证这一点并不那么明显,我每天使用minicom 连接到我的设备,它在重新启动时非常有弹性 - 对我有好处。

【讨论】:

  • 在测试程序时,我使用了两个虚拟 com 端口。一个连接到应用程序,另一个连接到 realterm。我通过 realterm 将文件发送到并检查应用程序的功能。如果我将 readLine 代码直接连接到 readyRead() 信号,则 eventloop 没有机会更新事件,因为我只在 waitForKeyword() 内旋转。有没有办法强制事件循环更新?如果是这样,我该如何在我的函数中实现它?
  • @silversircel 不要编写伪同步代码。您根本不应该使用任何 waitForXxxx 方法。没有。忘记它们的存在,然后想想如何解决它。然后,您所能做的和需要做的就是连接到readyRead 信号。这一切都会奏效,因为您不再使用 wait 方法调用进行阻塞。
  • @KubaOber 但是如果一切都是异步的,我该如何等待?这正是我遇到麻烦的地方。如果我发送一个命令并收到一个响应,这会导致一个插槽。我是否应该在一个大的 switch 案例中检查每个可能的关键字并决定下一步该做什么?必须有某种设计模式。我需要一个状态机之类的东西……执行第 1 步,然后执行第 2 步,然后……如何使用信号和插槽实现类似的东西?
  • @silversircel 宾果游戏。 QStateMachine。我想让你意识到这就是你所需要的:)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-11-18
  • 1970-01-01
  • 2011-10-23
  • 2022-10-12
相关资源
最近更新 更多