問題描述
我必須更新連接到串行端口的設備上的固件和設置.由于這是由一系列命令完成的,因此我發送一個命令并等待我收到答復.在 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);
}
這篇關于發送一系列命令并等待響應的文章就介紹到這了,希望我們推薦的答案對大家有所幫助,也希望大家多多支持html5模板網!