Wykorzystajmy numer QStateMachine
, aby było to proste. Przypomnijmy, jak chciał taki kod będzie wyglądać.
Serial->write(“boot”, 1000);
Serial->waitForKeyword(“boot successful”);
Serial->sendFile(“image.dat”);
Ujmę to w klasie, który ma wyraźne członków stanu dla każdego państwa, programista może być w Będziemy mieć również generatory akcji send
, expect
, etc. które dołączają określone działania do stanów.
// 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; }
};
To w pełni funkcjonalny, kompletny kod dla programisty! Całkowicie asynchroniczny, nie blokujący, a także obsługuje limity czasu.
Możliwe jest posiadanie infrastruktury, która generuje stany w locie, dzięki czemu nie trzeba ręcznie tworzyć wszystkich stanów. Kod jest znacznie mniejszy, a IMHO łatwiejsze do zrozumienia, jeśli masz wyraźne stany. Tylko dla złożonych protokołów komunikacyjnych z 50-100 + stanami byłoby pozbywanie się jawnie nazwanych stanów.
AppPipe
jest prosty wewnątrzprocesowej rury dwukierunkowego, które mogą być stosowane jako Podstawiony dla fizycznego portu szeregowego:
// 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
posiada maszynę stanu, niektóre sygnały podstawowe ułatwiające monitorowanie postęp machiny państwowej, a metoda connectSignals
używany do łączenia sygnałów z państw:
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
i FinalState
są proste nazwane owijarki państwowe w stylu Qt 3. Pozwalają nam zadeklarować stan i nadać mu nazwę za jednym zamachem.
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;
Generatory akcji są również bardzo proste. Znaczenie generatora akcji to "zrób coś, gdy dany stan zostanie wprowadzony". Stan, na którym należy działać, jest zawsze podawany jako pierwszy argument. Drugi i kolejne argumenty są specyficzne dla danego działania. Czasami działanie może również wymagać stanu docelowego, np. jeśli się powiedzie lub się nie powiedzie.
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);
}
Test hasLine
prostu sprawdza wszystkie linie, które można odczytać z urządzenia do danej igły. To działa dobrze dla tego prostego protokołu komunikacyjnego. Gdyby twoja komunikacja była bardziej zaangażowana, potrzebowałbyś bardziej złożonych maszyn. Konieczne jest przeczytanie wszystkich linii, nawet jeśli znajdziesz swoją igłę. To dlatego, że ten test jest wywoływany z sygnału readyRead
, a w tym sygnale należy odczytać wszystkie dane, które spełniają wybrane kryterium. Tutaj kryterium jest, że dane tworzą pełną linię.
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;
}
Dodawanie strzeżone przejścia do stanów jest nieco kłopotliwe z domyślnego interfejsu API, więc będziemy zawijać je łatwiej używać, a do utrzymania generatory działania powyższe czytelny:
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;
}
to o tym - jeśli masz prawdziwe urządzenie, to wszystko, czego potrzebujesz. Ponieważ nie mam urządzenie, będę tworzyć inny StatefulObject
naśladować przypuszczalną zachowanie urządzenia:
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; }
};
Teraz zróbmy to wszystko ładnie wizualizowane. Będziemy mieć okno z przeglądarką tekstową pokazującą zawartość komunikacji. Poniżej będzie to przyciski start/stop programatora lub urządzenia, a także etykiety wskazujące stan emulowane urządzenia i programatora:
Będziemy podłączyć urządzenie i programisty AppPipe
s .Będziemy także wizualizować co programista jest wysyłanie i odbieranie:
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));
});
Wreszcie będziemy łączyć przyciski i etykiety:
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
i Device
mógł żyć w każdym wątku. Pozostawiłem je w głównym wątku, ponieważ nie ma powodu, aby je przenosić, ale można umieścić je w dedykowanym wątku lub każdy w swoim wątku lub w wątki udostępnione innym obiektom itp. Jest całkowicie przezroczysty od AppPipe
obsługuje komunikację między wątkami. Dotyczy to również sytuacji, w której zastosowano QSerialPort
zamiast AppPipe
. Ważne jest tylko to, że każde wystąpienie QIODevice
jest używane tylko z jednego wątku. Wszystko inne dzieje się poprzez połączenia sygnał/gniazdo.
E.g. gdybyś chciał Programmer
żyć w dedykowanym wątku, należy dodać następujące gdzieś w main
:
// fix QThread brokenness
struct Thread : QThread { ~Thread() { quit(); wait(); } };
Thread progThread;
prog.moveToThread(&progThread);
progThread.start();
Trochę pomocnik formatuje dane, aby ułatwić czytać:
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);
}
Bez znajomości czym jest klasa Serial, nie można odpowiedzieć na twoje pierwotne pytanie, dlaczego ignorowała resztę pliku. Należy jednak zauważyć, że w portach szeregowych Linux urządzenia nie zachowują się jak gniazda w odniesieniu do niezablokowującego IO, więc może to być powód. (Zasadniczo nie można używać nieblokujących wejść/wyjść z portami szeregowymi, dlatego oficjalna klasa QSerialPort, która została dodana w Qt 5.1, symuluje asynchroniczną komunikację z wątkiem). – MuchToLearn