2015-09-09 51 views
5

Muszę zaktualizować oprogramowanie układowe i ustawienia na urządzeniu podłączonym do portu szeregowego. Ponieważ odbywa się to za pomocą sekwencji poleceń, wysyłam polecenie i czekam, aż otrzymam odpowiedź. Wewnątrz odpowiedzi (wiele linii) szukam łańcucha, który wskazuje, czy operacja zakończyła się pomyślnie.Wysyłanie sekwencji poleceń i oczekiwanie na odpowiedź

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

Stworzyłem nowy wątek dla tej metody blokowania odczytu/zapisu. Wewnątrz wątku korzystam z funkcji waitForX(). Jeśli zadzwonię watiForKeyword() będzie ona wywołać readlines() aż wykryje słowa lub 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() odczytuje wszystko dostępne i rozdziela go na linie, każda linia jest umieszczona wewnątrz QStringList i aby uzyskać ciąg I wywołują funkcję getLine(), która zwraca pierwszy ciąg na liście i usuwa go.

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

Problem jest, jeśli mogę wysłać plik za pośrednictwem terminala do testowania readlines app() będzie czytać tylko Smale część pliku (5 wierszy lub tak). Ponieważ te wiersze nie zawierają słowa kluczowego. funkcja uruchomi się jeszcze raz, ale tym razem nie będzie czekać na timeout, readLines po prostu zwraca false natychmiast. Co jest nie tak? Również nie jestem shure, czy to jest właściwe podejście ... Czy ktoś wie, jak wysłać sekwencję poleceń i czekać na odpowiedź za każdym razem?

+0

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

Odpowiedz

9

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:

screenshot

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("&gt;", "blue", data)); 
    }); 
    Q::connect(&prog.pipe(), &AppPipe::hasIncoming, &comms, [&](const QByteArray & data){ 
     comms.append(formatData("&lt;", "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); 
} 
+1

To powinno być 100+ odpowiedzi, nie mogłem się powstrzymać od komentowania :) – Mike

1

Nie jestem pewien, czy to właściwe podejście.

Sondujesz pod numerem waitForReadyRead(). Ale ponieważ port szeregowy to QIODevice, wyemituje sygnał void QIODevice::readyRead(), gdy coś pojawi się na porcie szeregowym. Dlaczego nie podłączyć tego sygnału do kodu wejściowego parsowania? Nie ma potrzeby dla waitForReadyRead().

Również/z drugiej strony: "... tym razem nie czeka na timeout, readLines po prostu zwraca false natychmiast. Co jest niewłaściwe?"

Cytowanie dokumentację:

Jeśli waitForReadyRead() zwraca false, połączenie zostało zamknięte lub wystąpił błąd.

(Kopalnia nacisk) Z mojego doświadczenia jako osadzonego dewelopera, to nie jest wykluczone, że można umieścić urządzenie w rodzaju trybu „upgrade firmware”, i że w ten sposób urządzenie ponownie uruchomiony w specjalnym bagażniku (nie uruchamiając oprogramowania układowego, które chcesz zaktualizować), a tym samym zamknął połączenie. Nie ma sposobu, aby powiedzieć, chyba że jest to udokumentowane/masz kontakt z twórcami urządzeń. Nie tak oczywiste, aby sprawdzić przy użyciu terminala szeregowego, aby wpisać swoje polecenia i świadek, że używam minicom codziennie podłączony do moich urządzeń i jest dość odporny na ponowne uruchomienie - dobrze dla mnie.

+0

Podczas testowania programu korzystam z dwóch wirtualnych portów com . Jeden podłączony do aplikacji, a drugi do realterm. Wysyłam pliki przez Realterm do i sprawdzam, co robi aplikacja. Jeśli podłączę kod readLine bezpośrednio do sygnału readyRead(), to eventloop nie ma szansy na aktualizację zdarzeń, ponieważ tylko obracam wewnątrz funkcji waitForKeyword(). Czy istnieje sposób na zmusić aktualizację wydarzenia? Jeśli tak, jak mam to wdrożyć w moich funkcjach? – silversircel

+1

@silversircel Nie zapisuj kodu pseudo-synchronicznego. W ogóle nie powinieneś używać żadnych metod 'waitForXxxx'. Żaden. Zapomnij, że one istnieją i pomyśl, jak je rozwiązać. Wszystko, co możesz zrobić i musisz zrobić, to połączyć się z sygnałem 'readyRead'. Wszystko będzie działać, ponieważ nie blokujesz już wywołań metod 'wait'. –

+0

@KubaOber Ale jak mogę czekać, jeśli wszystko jest asynchroniczne? Dokładnie to, z czym mam problem. Jeśli wyślę polecenie i otrzymam odpowiedź, która prowadzi do gniazda. Czy powinienem sprawdzić każde możliwe słowo kluczowe w dużej skrzynce przełącznika i zdecydować, co dalej? Musi być jakiś wzór projektu. Potrzebuję czegoś takiego jak automat stanów ... wykonaj krok 1, a następnie wykonaj krok 2, a następnie ... Jak zaimplementować coś takiego z sygnałami i gniazdami? – silversircel