发送一系列命令并等待响应
我必须更新连接到串行端口的设备上的固件和设置.由于这是由一系列命令完成的,因此我发送一个命令并等待我收到答复.在 answere(多行)中,我搜索指示操作是否成功完成的字符串.
I have to update firmware and settings on a device connected to a serial port. Since this is done by a sequence of commands, I send a command and wait until I recive an answer. Inside the answere (many lines) I search for a string that indicates if the operation is finished successfully.
Serial->write("boot", 1000);
Serial->waitForKeyword("boot successful");
Serial->sendFile("image.dat");
…
所以我为这个阻塞读/写方法创建了一个新线程.在线程内部,我使用了 waitForX() 函数.如果我调用 watiForKeyword() 它将调用 readLines() 直到它检测到关键字或超时
So I’ve created a new Thread for this blocking read/write method. Inside the thread I make use of the waitForX() functions. If I call watiForKeyword() it will call readLines() until it detects the keyword or timesout
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(),它返回列表中的第一个字符串并删除它.
readLines() reads everything available and separates it into lines , each line is placed inside a QStringList and to get a string I call getLine() which returns the first string in the list and deletes it.
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, "
");
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, "
");
}
// rest
buffer.append(begin, -1);
return true;
}
问题是,如果我通过终端发送文件来测试应用程序 readLines() 只会读取文件的一小部分(5 行左右).由于这些行不包含关键字.该函数将再次运行,但这次它不等待超时,readLines 立即返回 false.怎么了 ?另外,我不确定这是否是正确的方法......有谁知道如何发送一系列命令并每次都等待响应?
The problem is if I send a file via terminal to test the app readLines() will only read a smale part of the file ( 5 Lines or so). Since these lines do not contain the keyword. the function will run once again, but this time it dosnt wait for timeout, readLines just return false immediately. Whats wrong ? Also I'm not shure if this is the right approach... Does anyone know how to send a sequenze of commands and wait for a response each time?
推荐答案
让我们使用 QStateMachine
来简化这个问题.让我们回想一下您希望这样的代码看起来如何:
Let's use QStateMachine
to make this simple. Let's recall how you wished such code would look:
Serial->write("boot", 1000);
Serial->waitForKeyword("boot successful");
Serial->sendFile("image.dat");
让我们将它放在一个类中,该类具有程序员可能处于的每个状态的显式状态成员.我们还将拥有动作生成器 send
、expect
等.将给定的动作附加到状态.
Let's put it in a class that has explicit state members for each state the programmer could be in. We'll also have action generators send
, expect
, etc. that attach given actions to states.
// 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
");
expect(&s_boot, &m_port, "boot successful", &s_send, 1000, &s_failed);
send (&s_send, &m_port, ":HULLOTHERE
:00000001FF
");
expect(&s_send, &m_port, "load successful", &s_ok, 1000, &s_failed);
}
AppPipe & pipe() { return m_port; }
};
这是一个功能齐全、完整的程序员代码!完全异步、非阻塞,也能处理超时.
This is fully functional, complete code for the programmer! Completely asynchronous, non-blocking, and it handles timeouts, too.
可以拥有动态生成状态的基础设施,这样您就不必手动创建所有状态.如果您有明确的状态,代码要小得多,恕我直言更容易理解.只有对于具有 50-100 多个状态的复杂通信协议,摆脱显式命名状态才有意义.
It's possible to have infrastructure that generates the states on-the-fly, so that you don't have to manually create all the states. The code is much smaller and IMHO easier to comperehend if you have explicit states. Only for complex communication protocols with 50-100+ states would it make sense to get rid of explicit named states.
AppPipe
是一个简单的进程内双向管道,可用作真正串行端口的替代品:
The AppPipe
is a simple intra-process bidirectional pipe that can be used as a stand-in for a real serial port:
// 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
方法:>
The StatefulObject
holds a state machine, some basic signals useful for monitoring the state machine's progress, and the connectSignals
method used to connect the signals with the states:
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 风格的简单命名状态包装器.它们允许我们一次性声明状态并为其命名.
The State
and FinalState
are simple named state wrappers in the style of Qt 3. They allow us to declare the state and give it a name in one go.
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;
动作生成器也很简单.动作生成器的意思是在进入给定状态时做某事".要执行的状态始终作为第一个参数给出.第二个和后续参数特定于给定的操作.有时,一个动作也可能需要一个目标状态,例如如果成功或失败.
The action generators are quite simple, too. The meaning of an action generator is "do something when a given state is entered". The state to act on is always given as the first argument. The second and subsequent arguments are specific to the given action. Sometimes, an action might need a target state as well, e.g. if it succeeds or fails.
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
信号中调用的,并且在该信号中您必须读取满足所选标准的所有数据.这里的标准是数据形成整行.
The hasLine
test simply checks all lines that can be read from the device for a given needle. This works fine for this simple communications protocol. You'd need more complex machinery if your communications were more involved. It is necessary to read all the lines, even if you find your needle. That's because this test is invoked from the readyRead
signal, and in that signal you must read all the data that fulfills a chosen criterion. Here, the criterion is that the data forms a full line.
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 向状态添加受保护的转换有点麻烦,因此我们将对其进行包装以使其更易于使用,并保持上面的动作生成器的可读性:
Adding guarded transitions to states is a bit cumbersome with the default API, so we will wrap it to make it easier to use, and to keep the action generators above readable:
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
来模拟假定的设备行为:
That's about it - if you had a real device, that's all you need. Since I don't have your device, I'll create another StatefulObject
to emulate the presumed device behavior:
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
");
expect(&s_firmware, &m_dev, ":00000001FF", &s_loaded);
send (&s_loaded, &m_dev, "load successful
");
}
Q_SLOT void stop() { m_mach.stop(); }
AppPipe & pipe() { return m_dev; }
};
现在让我们很好地可视化.我们将有一个带有文本浏览器的窗口,显示通信内容.下面是启动/停止编程器或设备的按钮,以及指示仿真设备和编程器状态的标签:
Now let's make it all nicely visualized. We'll have a window with a text browser showing the contents of the communications. Below it will be buttons to start/stop the programmer or the device, and labels indicating the state of the emulated device and the programmer:
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
.我们还将可视化程序员发送和接收的内容:
We'll connect the device's and programmer's AppPipe
s. We'll also visualize what the programmer is sending and receiving:
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));
});
最后,我们将连接按钮和标签:
Finally, we'll connect the buttons and labels:
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
,情况也会如此.重要的是 QIODevice
的每个实例仅在一个线程中使用.其他一切都通过信号/插槽连接发生.
The Programmer
and Device
could live in any thread. I've left them in the main thread since there's no reason to move them out, but you could put both into a dedicated thread, or each into its own thread, or into threads shared with other objects, etc. It's completely transparent since AppPipe
supports communications across the threads. This would also be the case if QSerialPort
was used instead of AppPipe
. All that matters is that each instance of a QIODevice
is used from one thread only. Everything else happens via signal/slot connections.
例如如果您希望 Programmer
位于专用线程中,您可以在 main
的某处添加以下内容:
E.g. if you wanted the Programmer
to live in a dedicated thread, you'd add the following somewhere in main
:
// fix QThread brokenness
struct Thread : QThread { ~Thread() { quit(); wait(); } };
Thread progThread;
prog.moveToThread(&progThread);
progThread.start();
一个小助手格式化数据以使其更易于阅读:
A little helper formats the data to make it easier to read:
static QString formatData(const char * prefix, const char * color, const QByteArray & data) {
auto text = QString::fromLatin1(data).toHtmlEscaped();
if (text.endsWith('
')) text.truncate(text.size() - 1);
text.replace(QLatin1Char('
'), 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);
}
相关文章