Отправка последовательности команд и ожидание ответа
Я должен обновить прошивку и настройки на устройстве, подключенном к последовательному порту. Поскольку это выполняется с помощью последовательности команд, я отправляю команду и жду, пока получу ответ. Внутри ответа (много строк) я ищу строку, которая указывает, успешно ли завершена операция.
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. В чем дело? Также я не уверен, что это правильный подход... Кто-нибудь знает, как отправить последовательность команд и каждый раз ждать ответа?
2 ответа
Давайте использовать QStateMachine
чтобы сделать это просто. Давайте вспомним, как вы хотели, чтобы такой код выглядел:
Serial->write(“boot”, 1000);
Serial->waitForKeyword(“boot successful”);
Serial->sendFile(“image.dat”);
Давайте поместим его в класс, который имеет явные члены для каждого состояния, в котором может находиться программист. У нас также будут генераторы действий send
, expect
и т. д., которые прикрепляют данные действия к состояниям.
// https://github.com/KubaO/stackrun/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://stackru.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;
Генераторы действий тоже довольно просты. Смысл генератора действий - "сделать что-то, когда данное состояние введено". Состояние, на которое нужно действовать, всегда указывается в качестве первого аргумента. Второй и последующие аргументы являются специфическими для данного действия. Иногда для действия может также потребоваться целевое состояние, например, если оно успешно или неудачно.
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();
Мы свяжем устройство и программиста AppPipe
s. Мы также представим, что программист отправляет и получает:
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));
});
Наконец, мы подключим кнопки и метки:
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
используется только из одного потока. Все остальное происходит через соединение сигнал / слот.
Например, если вы хотели 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()
сигнал, когда что-то прибудет на последовательный порт. Почему бы не подключить этот сигнал к вашему входному разбору кода? Нет необходимости waitForReadyRead()
,
Также / с другой стороны: "... на этот раз тайм-аут не ждет, readLines просто немедленно возвращают false. Что не так?"
Цитирование документации:
Если waitForReadyRead() возвращает false, соединение было закрыто или произошла ошибка.
(выделение мое) Исходя из моего опыта в качестве разработчика встроенных программ, не исключено, что вы переводите устройство в режим "обновления прошивки" и, таким образом, устройство перезагружается в специальный режим загрузки (не работает прошивка, собираемся обновить) и таким образом закрыли соединение. Невозможно сказать, если это не задокументировано / у вас есть контакты с разработчиками устройства. Не так очевидно, чтобы проверить использование последовательного терминала для ввода ваших команд и засвидетельствовать, что я использую minicom
ежедневно подключается к моим устройствам, и это довольно устойчиво при перезагрузке - хорошо для меня.