Programowanie C++ Qt4 w systemie Gnu-Linux/Przykładowa AplikacjaRS232

Wstęp edytuj

W tym dziale chciał bym jeszcze przedstawić kawałek programu, w którym okno dialogowe będzie użyte do parametryzacji portu rs232. Na początek przedstawię diagram jak to ma wyglądać.

 
Diagram Klasy

Plan działania:

  1. W konstruktorze klasy okna głównego tworzymy obiekt klasy okna dialogowego
  2. Sygnał cliked() menu ustawienia portu łączymy z slotem show() okna dialogowego
  3. W funckji (slocie) OpenSerialPort() wywołujemy funkcję
    nastawy SetingsDialog->PortSetings();
    
    Funkcja zwróci nam aktualne nastawy portu, skonfiguruje port komputera i ostatecznie otwiera port do pracy.


Plik nagłówkowy klasy okna dialogowego edytuj

Na Początek przedstawię klasę okna dialogowego. To okno ustawień portu szeregowego.

 #ifndef MAINWINDOW_H
class PortSetingsDialog : public QDialog
{
    Q_OBJECT

public:
/*nowa klasa zagnieżdżona. Chcąc się do takiej klasy odwołać z zewnątrz trzeba będzie posłużyć się operatorem zakresu
PortSetingsDialog::nastawy JakisObjekt;
//przykładowa deklaracja. To tak aby mieć pewność że nastawy dotyczą konkretnej klasy związanej z portem szeregowym!!!
*/
    class nastawy{
        public:     //klasa właściwie struktura danych wszystkie składowe publiczne
        QString NazwaPortu;
        qint32 BaudRate;
        QSerialPort::DataBits dataBits;
        QSerialPort::Parity parity;
        QSerialPort::StopBits stopBits;
        QSerialPort::FlowControl flowControl;
    };

    explicit PortSetingsDialog(QWidget *parent = 0);
    ~PortSetingsDialog();

private:
    Ui::PortSetingsDialog *ui;
    void FormInitialize(); //inicjalizacja formatki okna dialogowego
    nastawy UstawieniaAktualne; //zadeklarowane jako prywatne - brak dostępu z zewnątrz
    void NastawyUpdate(); //funkca odświeży zmienną UstawieniaAktualne
public:
    nastawy PortSetings(){return UstawieniaAktualne;} // funkcja dostępna z poza klasy (publiczna)
private slots:
    void on_buttonBox_accepted();
};

Główną funkcją tej klasy będzie funkcja:

 nastawy PortSetings(){return UstawieniaAktualne;}


To właśnie z tej funkcji będzie korzystała aplikacja okna głównego w celu pozyskania nastaw. Klasa nastawy zdefiniowana jest jako zagnieżdżona. Celem takiej operacji jest wymuszenie jawnego odniesienia się o jakie nastawy chodzi. Deklarując zmienną w innej klasie musimy posłużyć się operatorem zakresu. Tak jak w przykładzie poniżej:

PortSetingsDialog::nastawy moja_zmienna //Przykładowa deklaracja zmiennej typu "nastawy"


Plik implementacji funkcja FormInitialize() edytuj

Jest to funkcja, która przygotuje nam formatkę aplikacji.

 void PortSetingsDialog::FormInitialize()
    {
        //ile portów szeregowych w kompie przeglądanie w pętli.
        //można jak poniżej
        /*    foreach (const QSerialPortInfo &info, QSerialPortInfo::availablePorts())
            {

              ui->PortNameBox->addItem(info.portName());
            }
        */

        //A można na piechotę

        const QList<QSerialPortInfo> &PortList=QSerialPortInfo::availablePorts();//(1)
        //Wyżej obiekt tymczasowy zwracany przez funkcję istnieje dopóki, dopóty istnieje referencja do niego.

        for (int i=0; i< PortList.count();i++)
           {
            //W C++ do referencji nie można nic przypisać. Referencję można tylko znacjonalizować.
            const QSerialPortInfo &info =PortList.value(i); //(2) ale można to zrobić kilka razy.
            ui->PortNameBox->addItem(info.portName());
           }

        //prędkość transmisji damy 2 do wyboru
        ui->BoudRateBox->addItem("9,6k",QSerialPort::Baud9600); //QVariant dość fajna sztuczka
        ui->BoudRateBox->addItem("2,4k",QSerialPort::Baud2400);

        //bity danych nie będzie wyboru
        ui->DataBitsBox->addItem("8",QSerialPort::Data8);
        ui->DataBitsBox->addItem("7",QSerialPort::Data7);

        //bity stopu
        ui->StopBitsBox->addItem("Jeden",QSerialPort::OneStop);
        ui->StopBitsBox->addItem("Dwa",QSerialPort::TwoStop);

        //Sterowanie transmisją
        ui->FlowControlBox->addItem("brak",QSerialPort::NoFlowControl);
        ui->FlowControlBox->addItem("Sprzęt",QSerialPort::HardwareControl);
        ui->FlowControlBox->addItem("Xon/Xoff",QSerialPort::SoftwareControl);

        //kontrola parzystości
        ui->ParityBitsBox->addItem("brak",QSerialPort::NoParity);
        ui->ParityBitsBox->addItem("even",QSerialPort::EvenParity);
        ui->ParityBitsBox->addItem("odd",QSerialPort::OddParity);
    }


QList edytuj

Czas przyjrzeć się troszkę kontenerowi QList. Funkcja

QSerialPortInfo::availablePorts()

zwraca nam obiekt typu QList<QSerialPortInfo>. Funkcja zwraca kontener zawierający obiekty typu QSerialPortInfo. Taki kontener można przeglądać na kilka sposobów:

  1. Wykorzystanie pętli foreach patrz komentarz.
  2. Na piechotę, tak jak to przedstawiłem w programie.
  3. Skorzystać z iteratora. W bibliotece Qt4 do dyspozycji są dwa rodzaje iteratorów.

W Programie przedstawionym postanowiłem pobawić się referencjami. Ma to na celu zademonstrowanie sposobu posługiwania się referencją w C++.

 const QList<QSerialPortInfo> &PortList=QSerialPortInfo::availablePorts()

Ten zapis(połączenie deklaracji z inicjalizacją) czytamy następująco:

  • PortList jest referencją
  • Do kontenera typu QList
  • Zawierającego obiekty typu QSerialPortInfo
  • Referencja jest zainicjalizowana tymczasowym obiektem zwróconym przez funkcję QSerialPortInfo::availablePorts()
  • "Tymczasowy" obiekt będzie istniał dopóty, dopóki będzie istniała referencja do niego.
const QSerialPortInfo &info =PortList.value(i)

Podobnie jak w przykładzie poprzednim tak i tutaj inicjujemy referencję obiektem tymczasowym zwróconym przez funkcję. Dodatku robimy to w pętli kilka razy.


Trzecim możliwym sposobem przebieżki po kontenerze QList jest skorzystanie z iteratora. W bibliotece STL iterator to wskaźnik na obiekt przechowywany w kontenerze. Podobnie postąpili twórcy biblioteki Qt tworząc dla swoich kontenerów iterator w podobnym stylu.

QList<QSerialPortInfo> PortList=QSerialPortInfo::availablePorts();
QList<QSerialPortInfo>::iterator i;
for (i=PortList.begin() ; i<PortList.end();i++)
   {
      //ui->PortNameBox->addItem(i->portName());
      ui->PortNameBox->addItem((*i).portName()); //zapis do wyboru oba robią to samo
   }
  • Zmienna i jest wskaźnikiem na obiekt typu QSerialPortInfo w kontenerze PortList. Ponieważ i jest wskaźnikiem. Dlatego zapsi (*i) "To na co pokazuje wskaźnik i" oznacza obiekt. Dlatego możemy użyć operatora "."


QComboBox, QVariant, enum edytuj

QComboBox jest wizualnym widgetem umożliwiającym dokonywanie wyboru z listy możliwości. Taki pojedynczy item z listy składa się z:

addItem(const QIcon & icon, const QString & text, const QVariant & userData = QVariant())


Składniki takie jak text oraz icon to są elementy wizualne. Element QVariant & userData może być pojemnikiem na dowolny obiekt zdefiniowany w bibliotece QT. W programie wykorzystano to pole do przechowywania parametrów portu.

Jest natomiast mały problem z przechowywaniem typów użytkownika. Dlatego typy enum przechowuje się jak zwykłą zmienną typu int. Bo enum jest właściwie zmienną int. Poniżej zamieszczę jak dokonać konwersji między typami enum, int, QVariant:

  • Tak możemy wpisać enum do QVariant. Przykład dla parametru prędkość portu.
 
QVariant temp = QSerialPort::Baud9600; // QSerialPort::Baud9600 to właściwie jest zmienna int przyjmuje wartość 9600
ui->BoudRateBox->addItem("9,6k",temp);
  • Tak możemy tego enuma z QVaiant wydostać. Przykład dla parametru "sterowanie przepływem danych"
QVariant temp;
temp=ui->FlowControlBox->currentData();
int a(temp.toInt());    //inicjalizacja zmiennej a w sposób charakterystyczny dla obiektów.
//int a =temp.toInt(); 
UstawieniaAktualne.flowControl=static_cast <QSerialPort::FlowControl>(a);   //rzutowanie int na enum

Ostatnim działaniem jest rzutowanie typu int na enum. Gdybyś spróbował tak:

 UstawieniaAktualne.Flow_Control=a;

To kompilator stanie okoniem z powodu niezgodności typów. Zbuntuje się, pomimo iż enum i int są właściwie tym samym.


Okno główne pliki nagłówkowy edytuj

Wiele do opisywanie nie ma. Okno główne aplikacji będzie wzbogacone o menu zarządzania portem, oraz slot dzięki któremu będziemy mogli odebrać dane z portu USART.

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include "portsetingsdialog.h"


namespace Ui {
class MainWindow;
}

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    explicit MainWindow(QWidget *parent = 0);
    ~MainWindow();

private slots:

    void DaneDostepne();                    //slot, który się uruchomi gdy na porcie są dane do odebrania
    void on_actionOpen_Port_triggered();    //przycisk otwarcia portu
    void SendPuschButtonClicked();          //Przycisk wyślij dane 
    void on_actionPort_Close_triggered();   //zamknij port przycisk

private:
    Ui::MainWindow *ui;
    PortSetingsDialog *psw; //Port Setings Window - Okienko z nastawami
    QSerialPort PortCom;    //Obiekt reprezentujący port szeregowy
};

#endif // MAINWINDOW_H


Okno główne plik implementacji edytuj

Na początek konstruktor okna głównego

 MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
    {
        ui->setupUi(this);
        psw = new PortSetingsDialog();          //Tworzymy okno z ustawieniami "Port Seting Window"
        //psw = new PortSetingsDialog(this);    //Dlaczego blokuje się menu w ubuntu ??
        
        //Poniżej połączenie sygnału kliknięcia przycisku ze slotem pokaż okno 
        connect(ui->actionUstawienia_Portu, SIGNAL(triggered()), psw, SLOT(show()));
    }

Na wyjaśnienie zasługują dwa elementy

  • psw obiekt reprezentujący okno z ustawieniami portu. Obiekt jest powołany do życia ale jeszcze nie widoczny
  • funkcja connect łączy sygnał cliked() z menu aplikacji ze slotem show() onka pswPort Seting Window. Po kliknięciu przycisku okienko ukaże się na ekranie.
void MainWindow::on_actionOpen_Port_triggered()
    {
        PortSetingsDialog::nastawy temp = psw->PortSetings();//żądanie nastaw dla konfiguracji portu
        PortCom.setPortName(temp.NazwaPortu);
        PortCom.setBaudRate(temp.BaudRate);
        PortCom.setDataBits(temp.dataBits);
        PortCom.setParity(temp.parity);
        PortCom.setStopBits(temp.stopBits);
        PortCom.setFlowControl(temp.flowControl);
        if (PortCom.open(QSerialPort::ReadWrite)) //1
            {
                connect(&PortCom, SIGNAL(readyRead()), this, SLOT(DaneDostepne())); //2 
                connect(ui->SendToolButton, SIGNAL(clicked()), this, SLOT(SendPuschButtonClicked()));// 3
            //i tak dalej ukrywanie przycisku otwóż port, pokazanie przycisku zamkni port
            }
    }

Wyjaśnienia do kodu funkcji:

  1. - Jeśli otwarcie portu się powiedzie to za pomocą polecenia connect uaktywniam dwie funkcje.
  2. - Polecenie connect spowoduje, że na sygnał "kliknięto przycisk wyślij" zostanie uruchomiona funkcja zapisu do portu RS232.
  3. - Polecenie connect spowoduje, że na sygnał "Przybyły zewnętrzne dane na port RS232" zostanie uruchomiona funkcja mająca na celu odczytanie tych danych z portu


const PortSetingsDialog::nastawy &temp = psw->PortSetings()

To wspominana już wcześniej przeze mnie funkcja służąca do pobrania nastaw portu z okienka dialogowego. Właśnie dla tej jednej linii kodu potrzebne było nam to okienko ;P


Gdyby nie polecenie "connect" funkcje te nie miały szans by zostać uruchomione. Odwrotnej akcji dokonujemy w funkcji odpowiadającej za obsługę przycisku "zamknij port"

 void MainWindow::on_actionPort_Close_triggered()
    {
        disconnect(&PortCom, SIGNAL(readyRead()), this, SLOT(DaneDostepne())); //a co nie wolno ??
        disconnect(ui->SendToolButton, SIGNAL(clicked()), this, SLOT(SendPuschButtonClicked()));
        PortCom.close();
        //i tak dalej. Np. pokaż przycisk otwóż port i wszystkie akcje, które przyjdą do głowy.
        //związane z zamknięciem portu
    }


Za wysłanie danych na port odpowiada funkcja:

 void MainWindow::SendPuschButtonClicked()
    {
        PortCom.write((ui->lineEdit->text()+QChar(13)+QChar(10)).toUtf8());
    }

Wysyła Portem tekst z pola LineEdit wzbogacony o znaki końca linii . Znaki te przydadzą się do zidentyfikowania końca transmisji w funkcji dane odebrane.


Okno główne plik implementacji funkcja DaneDostępne() edytuj

Poniżej przedstawię funkcję służącą do odebrania danych z portu. Ta króciutka funkcja wymaga kilku wyjaśnień.

void MainWindow::DaneDostepne() //coś przyszło na port
    {
        static QByteArray line="";   //line jest zmienną tupu static.
                                     //Co pociąga za sobą pewną fajną funkcjonalność. Zmienna ta nie przestaje istnieć po wyjściu z funkcji.
        line=line+PortCom.readAll(); //Budowanie linijki aż do wystąpienia znaku końca transmisji ramki. 
        //QChar OstatniOdebranyZnak = *(line.end()-1);
        if (*(line.end()-1)==QChar(10))  //czy koniec linii??
            {
                ui->textBrowser->append(line); //tak koniec ramki dodaj tekst na listę
                line.clear();                  //wyczyść linijkę 
            }

        }


Zmienna static przykład zastosowania edytuj

Funkcja void MainWindow::DaneDostepne() swoim działaniem przypomina troszkę przerwanie. Nie wiadomo kiedy zostanie wywołana i nie wiadomo ile bajtów będzie na porcie w momencie wywołania. Chodzi mi o to, że sygnał &PortCom, SIGNAL(readyRead() może przyść gdy transmisja nie będzie kompletna. I gdy wyślemy na port dane "Ala Ma 2 koty i psa" odebrać możemy tylko "Al" w kolejnym wywołaniu "a Ma 2 koty i ps" i w ostatnim samo "a". Dlatego fajnie wiedzieć kiedy transmisja się kończy(jakiś umówiony znak końca linii nie mający szans wystąpić w normalnej transmisji), i do tego momentu budować telegram. W naszym wypadku koniec transmisji zwiastować będzie znak o kodzie "10(#A)"

 line=line+PortCom.readAll();

Taka linijka kodu jak powyżej nie miała by szansy działać w żadnej funkcji gdyż po opuszczeniu nawiasów {} zmienna line przestawała by istnieć. I właśnie do tego, aby ta zmienna nie przestawała istnieć nawet gdy funkcja skończy swoje działanie służy nam przydomek static. Zmienna lokalna zachowuje się wówczas jak zmienna globalna (trwa ze swoją zawartością aż do końca żywota programu).

Iteraratory ponownie edytuj

Nie wiem czy to można nazwać iteratorem, ale funkcja line.end() zwraca wskaźnik ustawiony na koniec strumienia danych. Więc jeśli odejmiemy od tej wartości 1, to otrzymamy wskaźnik na ostatni znak. Jeśli ostatni znak będzie umówionym znakiem końca linii to możemy uznać że ramka przyszła w całości.


Działanie aplikacji Testowanie Portu edytuj

Najpierw należy zmostkować port szeregowy. Jeśli jesteśmy posiadaczami klasycznego RS232 to należy zaopatrzyć się we wtyczkę RS ze zwartymi pinami nr2 i nr3. Chodzi o to aby zrobić elektryczny mostek między pinami Rx a Tx. To testów na porcie RS232 zawsze używam kawałka druta zapewniając sobie w ten sposób błyskawiczny ComeBack wysłanych portem danych.

 
Aplikacja do obsługi RS232 - Zwarte piny Rx Tx

Dostęp do portu RS232 edytuj

Właściciele przelotek USB-UART, aby z poziomu użytkownika uzyskać dostęp do portu szeregowego muszą zrobić co następuje.

Sprawdzić grupę, do której należy plik urządzenia szeregowego poleceneniem

ls -l /dev/ttyUSB0 
crw-rw---- 1 root dialout 188, 0 sie 31 20:35 /dev/ttyUSB0

Z tego co wypluje polecenie dowiadujemy się, że grupa dialout jest właścicielem pliku urządzenia. Dlatego należy dodać naszego użytkownika do tej grupy. Przykładowe polecenie:

sudo usermod -g dialout użytkownik


Do Pobrania Źródła edytuj

http://chomikuj.pl/mysiadziura/www.marekk.dreamhosters.com/AplikacjaPortRS232,4173570190.7z