Wykorzystanie elementów Win32 API w C++ Builder. Część II
W trakcie tego podrozdziału zapoznamy się z kolejnymi sposobami realizacji transmisji szeregowej poprzez interfejs RS 232C. Zostanie pokazany odmienny sposób odwoływania się do skonstruowanych przez nas funkcji Read_Comm() oraz Write_Comm(). Omówimy pewne aspekty transmisji nie buforowanej, pokażemy, jak można przesyłać i odbierać pliki oraz zapoznamy się z pewnymi dodatkowymi metodami kontroli poprawności działania użytych przez nas funkcji. Zobaczymy też, jak można czasowo próbkować port szeregowy, wykorzystując w tym celu komponent TTimer.
Wysyłamy znak po znaku
Bardzo często musimy szybko wysłać do urządzenia tylko jeden znak. Może to być jakiś wyjątkowy znak sterujący, np. rozkaz natychmiastowego wyłączenia się przyrządu. Postępując w sposób opisany w poprzednim podrozdziale, czyli wykorzystując buforowanie danych, napotkamy pewną trudność. Rozkaz wysyłany przez nas będzie musiał poczekać w buforze na swoją kolej. Może zdarzyć się i taka sytuacja, że zechcemy wysłać jakiś plik, nawet dosyć duży, i niekoniecznie w tym celu zechcemy zastanawiać się nad ustalaniem rozmiaru bufora komunikacyjnego. W takich przypadkach przychodzi nam z pomocą Win32 API, oferując funkcję:
BOOL TransmitCommChar(HANDLE hCommDev, char chTransmit);
Transmituje ona znak określony przez chTransmit do łącza identyfikowanego przez hCommDev. Bardzo ważną właściwością tej funkcji jest, że znak przez nią wysyłany będzie miał pierwszeństwo przed innymi wysyłanymi z bufora. Stosując ją należy jednak pamiętać, że nie można w ten sposób wysłać dwóch znaków jednocześnie, lecz co najwyżej „prawie” jednocześnie. Z reguły minimalne wymagane opóźnienie pomiędzy kolejnymi wywołaniami TransmitCommChar() wynosi 1 milisekundę. Chociaż jest ona rekomendowana przez API głównie do transmisji synchronicznej, to można ją też z powodzeniem stosować na potrzeby przesyłania asynchronicznego.
Poniżej została zaprezentowana przykładowa aplikacja wykorzystująca własności opisanej funkcji. Przy jej pomocy możemy wysłać dowolny znak lub plik wcześniej zapisany na dysku. Kolejne przyciski typu TSpeedButton, za pomocą których wysyłamy kolejne litery alfabetu, zgrupowane są w obszarze określonym przez komponent TBevel. Z przyciskiem Wyślij plik skojarzona jest funkcja obsługi zdarzenia SendFileClick().
Na rysunku 5.5 zaprezentowano wygląd formularza naszej aplikacji \KODY\BUILDER\RS_04\p_RS_04.bpr, zaś wydruk 5.4 przedstawia kompletny kod jej głównego modułu RS_04.cpp. Śledząc go ktoś dociekliwy na pewno zauważy, że sposób użycia TransmitCommChar() bardzo przypomina sposób wykorzystania makrowywołania rs_send() zaprezentowanego w rozdziale 4. (patrz wydruk 4.1).
Rysunek 5.5. Formularz projektu p_RS_04.bpr
|
|
Wydruk 5.4. Kod formularza aplikacji realizującej nie buforowaną transmisję szeregową
//--- kompilować z borlndmm.dll oraz cc3250mt.dll --------------
//----RS_04.cpp-------------
#include <vcl.h>
#include <stdio.h>
#pragma hdrstop
#include "RS_04.h"
#pragma package(smart_init)
#pragma resource "*.dfm"
TForm1 *Form1;
HANDLE hCommDev; // identyfikator portu
LPCTSTR lpFileName; // przechowuje nazwę portu
DCB dcb; // struktura kontroli portu szeregowego
//--------------------------------------------------------------------
int __fastcall Close_Comm(HANDLE hCommDev)
{
CloseHandle(hCommDev);
return TRUE;
}
//--------------------------------------------------------------------
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
}
//--------------------------------------------------------------------
void __fastcall TForm1::CloseCommClick(TObject *Sender)
{
Close_Comm(hCommDev);
Application->Terminate();
}
//--------------------------------------------------------------------
void __fastcall TForm1::OpenCommClick(TObject *Sender)
{
if (CheckBox1->Checked == TRUE) // wybór portu
lpFileName = "COM1";
if (CheckBox2->Checked == TRUE)
lpFileName = "COM2";
hCommDev = CreateFile(lpFileName, GENERIC_READ | GENERIC_WRITE,
0, NULL, OPEN_EXISTING, 0, NULL);
if (hCommDev != INVALID_HANDLE_VALUE) // sprawdza, czy port jest
// otwarty prawidłowo
{
dcb.DCBlength = sizeof(dcb);
GetCommState(hCommDev, &dcb);
if (CheckBox3->Checked == TRUE)
dcb.BaudRate=CBR_19200;
dcb.Parity = ODDPARITY; // ustawienie parzystości
dcb.StopBits = ONESTOPBIT; // bity stopu
dcb.ByteSize = 7; // bity danych
//-przykładowe ustawienia flag sterujących DCB-
dcb.fParity = TRUE; // sprawdzanie parzystości
dcb.fDtrControl = DTR_CONTROL_DISABLE;
dcb.fRtsControl = RTS_CONTROL_DISABLE;
dcb.fOutxCtsFlow = FALSE;
dcb.fOutxDsrFlow = FALSE;
dcb.fDsrSensitivity = FALSE;
dcb.fAbortOnError = FALSE;
dcb.fOutX = FALSE;
dcb.fInX = FALSE;
dcb.fErrorChar = FALSE;
dcb.fNull = FALSE;
SetCommState(hCommDev, &dcb);
}
else
{
switch ((int)hCommDev)
{
case IE_BADID:
MessageBox(NULL, "Niewłaściwa nazwa portu lub port jest"
" aktywny.", "Błąd !", MB_OK);
break;
};
}
}
//--------------------------------------------------------------------
void __fastcall TForm1::SendFileClick(TObject *Sender)
{
FILE *pstream;
char chTransmit;
if (hCommDev > 0)
{
if ((pstream = fopen("tekst.txt", "rt")) > 0)
{
while ((chTransmit = fgetc(pstream))!= EOF)
{
TransmitCommChar(hCommDev, chTransmit);
Sleep(1);
}
}
else
MessageBox(NULL, "Błąd otwarcia pliku.", "Błąd pliku!",
MB_OK);
}
else
MessageBox(NULL, "Port nie został otwarty do transmisji.",
"Błąd !", MB_OK);
}
//--------------------------------------------------------------------
void __fastcall TForm1::SpeedButton1Click(TObject *Sender)
{
if (hCommDev > 0)
TransmitCommChar(hCommDev, 'A');
else
MessageBox(NULL, "Port nie został otwarty do transmisji.",
"Błąd !", MB_OK);
}
//--------------------------------------------------------------------
void __fastcall TForm1::SpeedButton2Click(TObject *Sender)
{
if (hCommDev > 0)
TransmitCommChar(hCommDev, 'B');
else
MessageBox(NULL, "Port nie został otwarty do transmisji.",
"Błąd !", MB_OK);
}
//--------------------------------------------------------------------
void __fastcall TForm1::SpeedButton3Click(TObject *Sender)
{
if (hCommDev > 0)
TransmitCommChar(hCommDev, 'C');
else
MessageBox(NULL, "Port nie został otwarty do transmisji.",
"Błąd !", MB_OK);
}
//--------------------------------------------------------------------
void __fastcall TForm1::SpeedButton4Click(TObject *Sender)
{
if (hCommDev > 0)
TransmitCommChar(hCommDev, 'D');
else
MessageBox(NULL, "Port nie został otwarty do transmisji.",
"Błąd !", MB_OK);
}
//--------------------------------------------------------------------
void __fastcall TForm1::SpeedButton5Click(TObject *Sender)
{
if (hCommDev > 0)
TransmitCommChar(hCommDev, 'E');
else
MessageBox(NULL, "Port nie został otwarty do transmisji.",
"Błąd !", MB_OK);
}
//--------------------------------------------------------------------
Patrząc na teść przedstawionej aplikacji, można by odnieść wrażenie, że problem jest tak banalny, że nie wart głębszego zastanawiania się nad nim. Sceptykom opowiem pewną historię. Będąc na początku lat dziewięćdziesiątych w Amsterdamie, jeszcze za czasów studenckich, zaszedłem do sklepu RTV-AGD, trafiając na moment, w którym sprzedawca demonstrował jednemu z klientów prosty regulator oświetlenia (w naszych sklepach czegoś takiego wtedy nie widziałem). Jego głównymi, widocznymi elementami były dwa pokrętła: dwustopniowe włącz-wyłącz oraz trójstopniowe, dające trzy możliwe poziomy natężenia światła. W przeliczeniu kosztował on około 20$. Do tego oferowany był prosty program działający w Windows 3.11 w cenie 15$. W porównaniu do kursu ówczesnej złotówki koszt zestawu nie był mały, przynajmniej dla mnie. Obsługa programu sprowadzała się do wyboru jednego z pięciu przycisków. Zainteresowała mnie zasada jego działania. Instrukcja obsługi napisana była również po angielsku na jednej, małej kartce papieru. Przeczytałem, że naciśnięcie przycisku oznaczonego symbolem I powoduje wysłanie do urządzenia komendy „I”, zaś naciśnięcie O wysłanie komendy „O” itd. Podobną zasadą sterowania posługuje się bardzo wiele niekoniecznie prostszych urządzeń.
Na zakończenie tego fragmentu naszych rozważań chciałbym poruszyć jeszcze jeden temat. Ze względu na to, że powyższy algorytm jest jednym z prostszych i krótszych, jakie przedstawiamy w tej książce, pragnę w tym miejscu wytłumaczyć się ze sposobu, w jaki umieszczane są w programie pisane przez nas funkcje. Postępując zgodnie z kanonami programowania zorientowanego obiektowo w C++Builderze, nagłówek przykładowej funkcji:
int __fastcall Close_Comm(HANDLE hCommDev)
należałoby zapisać następująco:
int __fastcall TForm1::Close_Comm(HANDLE hCommDev)
przez co jawnie stałaby się obiektem klasy TForm1, która dziedziczy własności TForm, czyli bazowej klasy formularza. Postępując konsekwentnie (zresztą nie mamy innego wyboru), należy powyższą deklarację umieścić w pliku nagłówkowym RS_04.h, np. w sekcji private lub public, tak jak przedstawia to poniższy wydruk.
Wydruk 5.5. Zawartość pliku RS_04.h
//--------------------------------------------------------------------
#ifndef RS_04H
#define RS_04H
//--------------------------------------------------------------------
#include <Classes.hpp>
#include <Controls.hpp>
#include <StdCtrls.hpp>
#include <Forms.hpp>
#include <Buttons.hpp>
#include <ComCtrls.hpp>
#include <Dialogs.hpp>
#include <ExtCtrls.hpp>
//--------------------------------------------------------------------
class TForm1 : public TForm
{
__published: // IDE-managed Components
TButton *CloseComm;
TButton *OpenComm;
TButton *SendFile;
TCheckBox *CheckBox1;
TCheckBox *CheckBox2;
TCheckBox *CheckBox3;
TBevel *Bevel1;
TLabel *Label1;
TSpeedButton *SpeedButton1;
TSpeedButton *SpeedButton2;
TSpeedButton *SpeedButton3;
TSpeedButton *SpeedButton4;
TSpeedButton *SpeedButton5;
void __fastcall CloseCommClick(TObject *Sender);
void __fastcall OpenCommClick(TObject *Sender);
void __fastcall SendFileClick(TObject *Sender);
void __fastcall SpeedButton1Click(TObject *Sender);
void __fastcall SpeedButton2Click(TObject *Sender);
void __fastcall SpeedButton3Click(TObject *Sender);
void __fastcall SpeedButton4Click(TObject *Sender);
void __fastcall SpeedButton5Click(TObject *Sender);
private: // User declarations
int __fastcall TForm1::Close_Comm(HANDLE hCommDev);
public: // User declarations
__fastcall TForm1(TComponent* Owner);
};
//--------------------------------------------------------------------
extern PACKAGE TForm1 *Form1;
//--------------------------------------------------------------------
#endif
Użycie __fastcall TForm1:: niewątpliwie przyspiesza wywołanie napisanych przez nas funkcji takich jak Close_Comm(), Read_Comm(), Write_Comm(). Ponadto w ich treści można w prosty sposób z dużą oszczędnością kodu odwoływać się do innych obiektów formularza zadeklarowanych w sekcji __published, zwanych tutaj metodami. Jest to na pewno bardzo wygodne. Pamiętajmy jednak, że zajmujemy się specyficznymi aplikacjami komunikującymi się ze światem zewnętrznym. Nawet niewielka pomyłka programisty, być może mało istotna w innego typu programach (absolutnie nic im nie ujmując), dla nas może okazać się zgubna. Przekonasz się, jak dużo czasu zabiera testowanie takich algorytmów oraz ich konserwacja, która tak naprawdę nigdy się nie kończy. Używając __fastcall TForm1:: jest się niestety skazanym na pracę z dwoma (lub więcej) plikami jednocześnie. Dla Czytelników testujących tego typu programy może to być trochę niewygodne. Wiele osób ceni sobie możliwość szybkiego usunięcia funkcji z programu w momencie, kiedy nie jest już potrzebna jak również możliwość szybkiego zmodyfikowania jej nagłówka lub listy parametrów. Dlatego nowe funkcje będę deklarował „tradycyjnie”, używając jedynie konwencji __fastcall, również dla przejrzystości kodu.
Wysyłamy pliki
Czytając poprzednie podrozdziały nauczyliśmy się wysyłać pojedyncze znaki oraz ich ciągi w postaci jawnie deklarowanych C-łańcuchów. Poznaliśmy też prosty sposób przesłania pliku znak po znaku. Czas abyśmy zobaczyli, jak można je przesyłać i odbierać, wykorzystując bufor transmisyjny oraz inne funkcje Win32 API. Ale zanim przejdziemy do zasadniczego tematu tego fragmentu naszej książki, pragnę zaprezentować pewną strukturę wraz z jej pokrewnymi funkcjami. Jest nią COMMTIMEOUTS. Dodajmy w tym miejscu, że jej znajomość nie jest wcale wymagana do zrealizowania asynchronicznej transmisji szeregowej z poziomu Windows, niemniej jednak zdecydowałem się zamieścić jej opis w tym miejscu, aby zachować ciągłość rozważań na temat interesującej nas warstwy komunikacyjnej Win32 API. Zasoby tej struktury przedstawione są w tabeli 5.10. Udostępniają nam one informacje o tzw. czasach przeterminowania transmisji w trakcie przesyłania danych (ang. time - out of transmission). Jest to ważny termin, z którym niektórzy na pewno już się zetknęli. Pragnę jednak dodać, że o ile rozwiązując problemy związane ze sterowaniem komputerowym, niezbyt często korzysta się z jej usług, o tyle ta znajomość może być przydatna w przypadku realizowania różnego rodzaju przesyłania danych pomiędzy dyskami. W pewnych przypadkach COMMTIMEOUTS determinuje zachowanie się takich funkcji jak ReadFile() czy WriteFile().
Tabela 5.10.
Informacje zawarte w strukturze COMMTIMEOUTS
Typ Element struktury Właściwości
DWORD ReadIntervalTimeout Określa maksymalny czas (milisekundy), pomiędzy
pojawieniem się na linii komunikacyjnej dwu znaków.
W trakcie wykonywania ReadFile() czas jest
liczony od momentu pojawienia się pierwszego znaku.
Jeżeli przedział czasu pomiędzy nadejściem dwu
znaków przekracza tą wartość, oznacza to, że
operacja ReadFile() jest zakończona.
Wartość 0 oznacza, że nie ustalono wymaganego
okresu pomiędzy nadejściem dwu kolejnych
znaków.
Przypisanie wartości MAXDWORD powoduje, że
czytany znak jest pobierany z bufora natychmiast
po pojawieniu się tam.
DWORD ReadTotalTimeoutMultiplier Określa mnożnik (milisekundy) użyty do obliczenia
całkowitego przedziału czasu (przeterminowanie)
dla operacji czytania (odbioru). Dla wszystkich takich
operacji wartość ta jest mnożona przez liczbę bajtów
przewidzianą do odebrania z dysku lub łącza
komunikacyjnego.
DWORD ReadTotalTimeoutConstant Określa stałą (milisekundy) użytą do obliczania
czasu przeterminowania operacji czytania. Dla
wszystkich takich operacji wartość ta jest dodawana
do ReadTotalTimeoutMultiplier i do
oczekiwanej liczby nadchodzących bajtów.
DWORD WriteTotalTimeoutMultiplier Określa mnożnik (milisekundy) użyty do obliczenia
całkowitego przedziału czasu (przeterminowanie)
dla operacji zapisywania (wysyłania).
Dla wszystkich takich operacji wartość ta jest
mnożona przez liczbę bajtów przewidzianą do
wysłania (zapisywania).
0 oznacza, że nie ustalono czasu przeterminowania
dla operacji zapisu na dysku lub do łącza
komunikacyjnego.
DWORD WriteTotalTimeoutConstant Określa stałą (milisekundy) użytą do obliczania
czasu przeterminowania operacji wysyłania. Dla
wszystkich takich operacji wartość ta jest dodawana
do WriteTotalTimeoutMultiplier oraz do
oczekiwanej liczby wysyłanych bajtów.
0 oznacza, że nie ustalono czasu przeterminowania
dla operacji zapisu (wysyłania).
Win32 API strukturę tę definiuje jako:
typedef struct _COMMTIMEOUTS {
DWORD ReadIntervalTimeout;
...
} COMMTIMEOUTS, *LPCOMMTIMEOUTS;
Definicja ta tworzy dwa nowe słowa kluczowe: COMMTIMEOUTS (struktura) oraz LPCOMMTIMEOUTS (wskaźnik do struktury).
Łatwo się domyślić, że aktualne parametry przeterminowania operacji zapisu i odczytu np. z portu komunikacyjnego odczytamy za pomocą funkcji:
BOOL GetCommTimeouts(HANDLE hCommDev, LPCOMMTIMEOUTS lpCommTimeouts);
Własne ustawienia wpiszemy, korzystając z:
BOOL SetCommTimeouts(HANDLE hCommDev, LPCOMMTIMEOUTS lpCommTimeouts);
W obu przypadkach lpCommTimeouts jest wskaźnikiem struktury opisanej w tabeli 5. 10. Najprostszym sposobem użycia przedstawionych wyżej instrukcji jest poniższy fragment kodu:
{
...
COMMTIMEOUTS CommTimeouts;
...
GetCommTimeouts(hCommDev, &CommTimeouts);
CommTimeouts.ReadTotalTimeoutConstant = 1;
CommTimeouts.ReadIntervalTimeout = 1;
CommTimeouts.ReadTotalTimeoutMultiplier = 1;
SetCommTimeouts(hCommDev, &CommTimeouts);
...
}
co oznaczać będzie, że poszczególne znaki powinny być pobierane z bufora komunikacyjnego w odstępach 1 milisekundy. Trzeba jednak w tym miejscu zauważyć, że użyteczność zasobów struktury COMMTIMEOUTS w odniesieniu do aplikacji sterujących nowoczesnym urządzeniem zewnętrznym w pewnych wypadkach może okazać się wątpliwa. Głównym zadaniem takiej aplikacji będzie prawidłowe odczytanie zawartości bufora wejściowego, w którym powinna się znajdować już kompletna informacja pochodząca od przyrządu. W jakim czasie ona się tam znajdzie, decyduje już samo urządzenie. Nasze funkcje Read_Comm() oraz Write_Comm() powinny poradzić sobie z tym problemem bez potrzeby szukania pomocy z zewnątrz, może za wyjątkiem mojej ulubionej WaitCommEvent(). Powyższe deklaracje przedstawiłem w celu zachowania całości opisu tematu, ponadto mogą okazać się Czytelnikom w przyszłości pomocne.
Przejdźmy teraz do zasadniczego tematu obecnych rozważań, czyli do operacji plikowych. Zanim pokażemy przykład ich realizacji, tradycyjnie już powinniśmy zapoznać się z pewnymi bardzo wygodnymi w użyciu funkcjami API. Chociaż nie są one produktem Win32 API, jednak zostały zachowane w 32-bitowym środowisku w celu zapewnienia jego kompatybilności z aplikacjami 16-bitowymi. Zastosowana przez nas metoda przesyłania pliku będzie bardzo prosta: najpierw otworzymy w trybie do czytania wyprany zbiór, potem przeczytamy, umieszczając jednocześnie jego zawartość w buforze komunikacyjnym, zamkniemy go, a następnie przetransmitujemy jego zawartość do wybranego urządzenia.
Zacznijmy od otwarcia pliku w trybie do czytania. Czynność tę ułatwi nam funkcja:
HFILE _lopen(LPCSTR lpPathName, int iReadWrite);
Otwiera ona istniejący plik, umieszczając wskaźnik pliku na jego początku. lpPathName jest wskaźnikiem do C-łańcucha, reprezentującego nazwę pliku wraz z pełną ścieżką dostępu. iReadWrite określa tryb, w jakim plik chcemy otworzyć. Mamy następujące możliwości:
OF_READ — otwarcie pliku w trybie do czytania,
OF_READWRITE — otwarcie w trybie do czytania i zapisywania,
OF_WRITE — otwarcie tylko w trybie do zapisywania.
Możliwe jest też otwarcie pliku w tzw. trybie współdzielenia lub akcji:
OF_SHARE_COMPAT — możliwość wielokrotnego otwierania pliku w trybie
kompatybilnym z innymi trwającymi obecnie procesami.
OF_SHARE_DENY_NONE — otwarcie pliku bez naruszania innych trwających
operacji czytania lub zapisu do pliku.
OF_SHARE_DENY_READ — otwarcie pliku z jednoczesnym usunięciem z niego
innych operacji czytania.
OF_SHARE_DENY_WRITE — otwarcie pliku z jednoczesnym usunięciem innych
operacji zapisu do niego.
OF_SHARE_EXCLUSIVE — otwarcie pliku w trybie zastrzeżonym z jednoczesnym
usunięciem operacji zapisu i odczytu.
Każdą z podanych wyżej wartości bazowych argumentu iReadWrite można, korzystając z działań logicznych, zsumować z odpowiednimi wartościami stałych symbolicznych OF_SHARE_x. Funkcja _lopen() wywołana prawidłowo zwraca identyfikator pliku przechowywany we właściwości HFILE (Handle of File). W przeciwnym wypadku zwraca HFILE_ERROR, reprezentującą błędną wartość przydzielonego do lpPathName identyfikatora. Dane z otwartego już pliku przeczytamy, korzystając z:
UINT _lread(HFILE hFile, LPVOID lpBuffer, UINT uBytes);
Parametr hFile jest identyfikatorem pliku, lpBuffer jest wskaźnikiem do bufora, w którym przechowujemy dane gotowe do wysłania, zaś uBytes jest rozmiarem tego bufora. Wartością zwracaną przez tę funkcję jest liczba bajtów aktualnie przeczytanych, jeżeli jest ona mniejsza od uBytes. Czytanie musi być powtarzane do momentu wykrycia znacznika końca pliku EOF (End of File). W przypadku błędnego wywołania należy spodziewać się wartości HFILE_ERROR.
Po otwarciu i przeczytaniu danych każdy plik należy zamknąć. Zrobimy to za pomocą:
HFILE _lclose(HFILE hFile);
W momencie pomyślnego jej wykonania funkcja ta zwróci nam wartość 0, w przeciwnym razie otrzymamy HFILE_ERROR.
Patrząc na budowę przedstawionych funkcji, musimy dojść do wniosku, że coś nam one przypominają. To prawda, ReadFile()i CloseFile() są po prostu ich rozwinięciami. Brakuje nam jeszcze odniesienia do WriteFile(). Zaprezentujmy je zatem:
UINT _lwrite(HFILE hFile, LPCSTR lpBuffer, UINT uBytes);
Idąc tym tokiem rozumowania ktoś mógłby pomyśleć, że w takim razie odpowiednikiem CreateFile() będzie np. _lcreat(). W pewnym sensie to prawda, ale nie w przypadku transmisji korzystającej z portów komunikacyjnych. Jeżeli już szukalibyśmy analogii, to znajdziemy ją pod postacią funkcji OpenComm(), nie podtrzymywanej obecnie przez Win32 API.
Ogólnie rzecz biorąc, zamiast używanych dotychczas przez nas ReadFile() oraz WriteFile() moglibyśmy równie dobrze posługiwać się dużo prostszymi _lread()lub _lwrite(), w sposób jaki mogą ilustrować to zamieszczone poniżej przykłady zmodyfikowanych funkcji Write_Comm() oraz Read_Comm()wraz z ich możliwymi wywołaniami:
//----------------wysłanie danych-------------------------------------
int __fastcall Write_Comm(HANDLE hCommDev, LPCSTR lpBuffer,
UINT uBytes)
{
if (_lwrite((int)hCommDev, lpBuffer, uBytes) != HFILE_ERROR)
{
WaitCommEvent(hCommDev, &fdwEvtMask, NULL);
return TRUE;
}
else
return FALSE;
}
//-------------------odczyt danych--------------------------------
int __fastcall Read_Comm(HANDLE hCommDev, LPVOID lpBuffer,
DWORD Buf_Size)
{
UINT uBytes;
ClearCommError(hCommDev, &Errors, &Stat);
if (Stat.cbInQue > 0)
{
if (Stat.cbInQue > Buf_Size)
uBytes = Buf_Size;
else
uBytes = Stat.cbInQue;
_lread((int)hCommDev, lpBuffer, uBytes);
}
else
uBytes = 0;
return TRUE;
}
//--------------------------------------------------------------------
{
...
Write_Comm(hCommDev, Buffer_O, strlen(Buffer_O));
...
Read_Comm(hCommDev, &Buffer_I[0], sizeof(Buffer_I));
...
}
Identyczną sytuację będziemy mieli, odczytując lub zapisując plik dyskowy. Z powodzeniem można w tym celu wykorzystać uniwersalne ReadFile() oraz WriteFile().
Po tym być może nieco przydługim wstępie nadszedł czas, abyśmy napisali aplikację przenoszącą pliki beztypowe pomiędzy komputerem a innym urządzeniem zewnętrznym. Będziemy ją testować, łącząc się z innym komputerem, na którym uruchomiony jest jeden z terminali opisanych w rozdziale 3. Budowa tej aplikacji siłą rzeczy musi być już bardziej skomplikowana od tych przedstawionych wcześniej.
! Pliki beztypowe umożliwiają bezpośredni dostęp do ich zawartości, bez potrzeby wnikania w strukturę. Są one kompatybilne, czyli niesprzeczne z innymi plikami, co sprawia, że szczególnie dobrze nadają się do realizacji wszelkich operacji wejścia-wyjścia. Pliki takie, których elementy traktowane są jako ciągi bajtów niezidentyfikowanej struktury, są obecnie powszechnie stosowane do sterowania różnego rodzaju urządzeniami.
|
Projektując aplikację, której wygląd przedstawiony jest na rysunku 5. 6, wykorzystałem trzy komponenty TMemo. W pierwszym z nich wyświetlany będzie początek pliku aktualnie przeczytanego z dysku, zaś jego całość zostanie umieszczona w buforze wyjściowym. Zawartość Memo2 pokazywać będzie początek pliku aktualnie transmitowanego, zaś Memo3 odpowiedź drugiego komputera, również w formie jakiegoś pliku. Często postępujemy w ten sposób, że przed wysłaniem jeszcze raz oglądamy początek zbioru, aby upewnić się, czy rzeczywiście jest to ten plik, o który nam chodzi. Jeżeli chcielibyśmy obejrzeć cały zbiór, trzeba we właściwy sposób ustalić rozmiary odpowiednich buforów.
Do zaprojektowania formularza użyłem ponadto pojedynczych komponentów TDriveComboBox, TEdit, TDirectoryListBox, TFileListBox . Posłużą one do szybkiego określenia konkretnego zbioru danych. Aby pracowały one tak, jak w każdym standardowym dialogu Windows służącym do wyboru pliku, ich cechy należy ze sobą związać. Dla wygody uczynimy to w funkcji tworzącej nasz formularz:
void __fastcall TForm1::FormCreate(TObject *Sender)
{
DirectoryListBox1->FileList = FileListBox1;
DriveComboBox1->DirList = DirectoryListBox1;
FileListBox1->FileEdit = Edit1;
}
W kolejnym etapie musimy zaprojektować funkcję obsługi zdarzenia FileListBox1Change(). Zrobimy to następująco:
//-------------odczyt pliku z dysku---------------------------
void __fastcall TForm1::FileListBox1Change(TObject *Sender)
{
memset(Buffer_O, 0, cbOutQueue);
hfile_s = _lopen(FileListBox1->FileName.c_str(), OF_READ);
if (hfile_s != HFILE_ERROR)
_lread(hfile_s, &Buffer_O[0], cbOutQueue);
for (int i = 0; i <= cbOutQueue-1; i++)
if (Buffer_O[i] == NULL)
Buffer_O[i] = '.';
Memo1->Text = Buffer_O;
Memo2->Text = Buffer_O;
_lclose(hfile_s);
}
Oczywiście, jeżeli ktoś nie zgadza się z przedstawionym tu stylem programowania, równie dobrze kod powyższego zdarzenia może zapisać, korzystając wyłącznie ze standardowych funkcji Win32 API:
void __fastcall TForm1::FileListBox1Change(TObject *Sender)
{
HANDLE hfile_s; // identyfikator pliku źródłowego
DWORD dwSize; // liczba czytanych bajtów
memset(Buffer_O, 0, cbOutQueue);
// -- używamy funkcji Win32 API do czytania pliku -
hfile_s = CreateFile(FileListBox1->FileName.c_str(), GENERIC_READ,
FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);
if (hfile_s != INVALID_HANDLE_VALUE)
ReadFile(hfile_s, &Buffer_O[0], sizeof(Buffer_O), &dwSize,
NULL);
for (int i = 0; i <= cbOutQueue-1; i++)
if (Buffer_O[i] == NULL)
Buffer_O[i] = '-';
Memo1->Text = Buffer_O;
Memo2->Text = Buffer_O;
CloseHandle(hfile_s);
}
Budowa funkcji obsługi zdarzenia polegającego na wczytaniu wybranego pliku z dysku wymaga komentarza. Użyliśmy funkcji memset(), aby zapisać w buforze wyjściowym znak 0. Krótko mówiąc wyczyściliśmy wyjściowy bufor danych przed ulokowaniem tam informacji. Przypomnijmy w tym miejscu jej definicję:
void *memset(void *s, int c, size_t n);
lub
LPVOID memset(LPVOID s, int c, size_t n);
Funkcja ta umieszcza mniej znaczący bajt argumentu c w pierwszych n znakach tablicy s. Następnie korzystamy z metody c_str() zwracającej wskaźnik (char *) do pierwszego znaku C-łańcucha identyfikującego właściwość FileName obiektu FileListBox1: FileListBox1->FileName.c_str(), po czym kojarzymy z nim identyfikator pliku źródłowego hfile_s (source). Sprawdzając, czy przydzielony identyfikator nie jest pusty (lub błędny), wczytujemy wybrany plik do bufora wyjściowego Buffer_O (jeżeli bufor jest mniejszy niż rozmiar pliku, zobaczymy oczywiście tylko jego fragment). Zauważmy, że nie ma tu żadnej pętli. Dalsze instrukcje powodują zastąpienie kropkami lub kreskami ewentualnie wolnego obszaru bufora (wykonaliśmy to jedynie dla celów estetycznych). Następnie do cechy Text komponentów Memo1 oraz Memo2 wczytamy zawartość bufora. Zamknięcie pliku kończy działanie funkcji obsługi zdarzenia FileListBox1Change(). Jedyną jego rolą jest wyświetlenie wybranego przez nas zbioru. Oczywiście, że już w tym miejscu moglibyśmy wysłać zaznaczony plik, ale z praktycznego punktu widzenia zawsze lepiej jeszcze raz obejrzeć to, co mamy do wysłania. Odrobina ostrożności czasami się opłaca, być może w ostatniej chwili trzeba będzie coś jeszcze zmodyfikować? W następnym podrozdziale pokażemy, jak to zrobić.
Najważniejszym fragmentem aplikacji będzie funkcja obsługi zdarzenia SendClick(), które jest skojarzone z przyciskiem Wyślij. Wykorzystamy wskaźnik postępu TProgessBar, aby mieć możliwość śledzenia operacji wysyłania pliku. W takich przypadkach dobrze jest, jeżeli program daje nam znać, że coś się dzieje. Jego maksymalny rozmiar będzie odpowiadał rozmiarowi pliku, który otrzymamy, korzystając z bardzo pożytecznej funkcji Win32 API GetFileSize():
DWORD GetFileSize(HANDLE hFile, LPDWORD lpFileSizeHigh);
Gdzie hFile jest identyfikatorem otwartego pliku, lpFileSizeHigh jest wskaźnikiem do 32-bitowej zmiennej reprezentującej rozmiar pliku. Wskaźnik postępu uczynimy aktywnym, korzystając z jego własności StepIt(). Przypatrzmy się teraz pętli while, w której pobieramy plik z dysku, umieszczając go jednocześnie bajt po bajcie w buforze wyjściowym, by w efekcie przetransmitować go za pomocą dobrze już nam znanej funkcji Write_Comm(). Zauważmy tu bardzo ważną rzecz — plik transmitujemy bajt po bajcie! Chociaż standardowa wielkość bloku transmisji (rekordu) do bufora może wynieść nawet 4 kB, to w transmisji szeregowej zalecaną wielkością jest 1 bajt. Cóż, każdy plik składa się z całkowitych wielokrotności tej liczby. Jeżeli ktoś ma wątpliwości, może ustalić rozmiar transferowanego bloku danych np. na 128 bajtów. Trzeba wówczas wykazać się dużą dozą cierpliwości aby zobaczyć końcowy efekt transferu danych choćby na sąsiednim komputerze. Treść funkcji obsługi zdarzenia wysyłającego plik zamieszczona jest poniżej.
//----------wysyłanie pliku------------------------------
void __fastcall TForm1::SendClick(TObject *Sender)
{
DWORD FileSizeHigh;
ProgressBar1->Max=0;
...
memset(Buffer_O, 0, cbOutQueue);
if ((_lopen(FileListBox1->FileName.c_str(), OF_READ)) !=
HFILE_ERROR)
{
hfile_s = _lopen(FileListBox1->FileName.c_str(), OF_READ);
ProgressBar1->Max=GetFileSize((HANDLE)hfile_s,
&FileSizeHigh);
while (_lread(hfile_s, &Buffer_O[0], 1)) // przeczytanie 1
// bajta i umieszczenie go
// w buforze wyjściowym
Write_Comm(hCommDev, 1); // transmisja 1 bajta
ProgressBar1->StepIt();
}
_lclose(hfile_s);
}
...
}
Sposób odbioru przychodzących do naszego portu znaków będzie analogiczny do tego, jaki zaprezentowaliśmy przy okazji odbioru łańcuchów. Kompletny kod głównego modułu RS_05.cpp projektu \KODY\BUILDER\RS_05\p_RS_05.bpr realizującego transmisję plików przedstawiony jest na wydruku 5.6.
Rysunek 5.6. Formularz główny działającego projektu p_RS_05.bpr
|
|
Wydruk 5.6. Kod aplikacji realizującej transmisję plików.
//--- kompilować z borlndmm.dll oraz cc3250mt.dll --------------
//------RS_05.cpp----------
#include <vcl.h>
#pragma hdrstop
#include "RS_05.h"
#pragma package(smart_init)
#pragma resource "*.dfm"
#define cbOutQueue 1024 //rozmiar bufora danych wyjściowych
#define cbInQueue 1024 //rozmiar bufora danych wejściowych
//--------------------------------------------------------------------
TForm1 *Form1;
HFILE hfile_s; // identyfikator pliku źródłowego
char Buffer_O[cbOutQueue]; // bufor danych wyjściowych
char Buffer_I[cbInQueue]; // bufor danych wejściowych
DWORD Number_Bytes_Read; // Number bytes to read —
// liczba bajtów do czytania
HANDLE hCommDev; // identyfikator portu
LPCTSTR lpFileName; // wskaźnik do nazwy portu
DCB dcb; // struktura kontroli portu szeregowego
DWORD fdwEvtMask; // informacja o aktualnym stanie
// transmisji
COMSTAT Stat; // dodatkowa informacja o zasobach
// portu
DWORD Errors; // reprezentuje typ ewentualnego błędu
//--------------------------------------------------------------------
int __fastcall Close_Comm(HANDLE hCommDev)
{
CloseHandle(hCommDev);
return TRUE;
}
//--------------------------------------------------------------------
int __fastcall Write_Comm(HANDLE hCommDev,
DWORD nNumberOfBytesToWrite)
{
DWORD NumberOfBytesWritten;
if (WriteFile(hCommDev, &Buffer_O[0], nNumberOfBytesToWrite,
&NumberOfBytesWritten, NULL) > 0)
{
WaitCommEvent(hCommDev, &fdwEvtMask, NULL);
return TRUE;
}
else
return FALSE;
}
//--------------------------------------------------------------------
int __fastcall Read_Comm(HANDLE hCommDev,
LPDWORD lpNumberOfBytesRead, DWORD Buf_Size)
{
DWORD nNumberOfBytesToRead;
ClearCommError(hCommDev, &Errors ,&Stat);
if (Stat.cbInQue > 0)
{
if (Stat.cbInQue > Buf_Size)
nNumberOfBytesToRead = Buf_Size;
else
nNumberOfBytesToRead = Stat.cbInQue;
ReadFile(hCommDev, &Buffer_I[0], nNumberOfBytesToRead,
lpNumberOfBytesRead, NULL);
}
else
*lpNumberOfBytesRead = 0;
return TRUE;
}
//--------------------------------------------------------------------
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
}
//--------------------------------------------------------------------
void __fastcall TForm1::FormCreate(TObject *Sender)
{
DirectoryListBox1->FileList = FileListBox1;
DriveComboBox1->DirList = DirectoryListBox1;
FileListBox1->FileEdit = Edit1;
}
//------------wstępny odczyt pliku z dysku----------------------------
void __fastcall TForm1::FileListBox1Change(TObject *Sender)
{
memset(Buffer_O, 0, cbOutQueue);
hfile_s = _lopen(FileListBox1->FileName.c_str(), OF_READ);
if (hfile_s != HFILE_ERROR)
_lread(hfile_s, &Buffer_O[0], cbOutQueue);
for (int i = 0; i <= cbOutQueue - 1; i++)
if (Buffer_O[i] == NULL)
Buffer_O[i] = '.';
Memo1->Text = Buffer_O;
Memo2->Text = Buffer_O;
_lclose(hfile_s);
}
//-----------------zamknięcie portu i aplikacji-----------------------
void __fastcall TForm1::CloseCommClick(TObject *Sender)
{
Close_Comm(hCommDev);
Application->Terminate();
}
//---------------inicjalizacja portu----------------------------------
void __fastcall TForm1::OpenCommClick(TObject *Sender)
{
if (CheckBox1->Checked == TRUE) // wybór portu
lpFileName = "COM1";
if (CheckBox2->Checked == TRUE)
lpFileName = "COM2";
hCommDev = CreateFile(lpFileName, GENERIC_READ | GENERIC_WRITE,
0, NULL, OPEN_EXISTING, 0, NULL);
if (hCommDev != INVALID_HANDLE_VALUE) // sprawdza, czy port jest
// otwarty prawidłowo
{
SetupComm(hCommDev, cbInQueue, cbOutQueue);
dcb.DCBlength = sizeof(dcb);
GetCommState(hCommDev, &dcb);
if (CheckBox3->Checked == TRUE) // wybór prędkości
dcb.BaudRate = CBR_1200; // transmisji
if (CheckBox4->Checked == TRUE)
dcb.BaudRate = CBR_19200;
dcb.Parity = ODDPARITY; // ustawienie parzystości
dcb.StopBits = ONESTOPBIT; // bity stopu
dcb.ByteSize = 7; // bity danych
//-przykładowe ustawienia flag sterujących DCB-
dcb.fParity = TRUE; // sprawdzanie parzystości
dcb.fDtrControl = DTR_CONTROL_DISABLE;
dcb.fRtsControl = RTS_CONTROL_DISABLE;
dcb.fOutxCtsFlow = FALSE;
dcb.fOutxDsrFlow = FALSE;
dcb.fDsrSensitivity = FALSE;
dcb.fAbortOnError = FALSE;
dcb.fOutX = FALSE;
dcb.fInX = FALSE;
dcb.fErrorChar = FALSE;
dcb.fNull = FALSE;
SetCommState(hCommDev, &dcb);
GetCommMask(hCommDev, &fdwEvtMask);
SetCommMask(hCommDev, EV_TXEMPTY);
}
else
{
switch ((int)hCommDev)
{
case IE_BADID:
MessageBox(NULL, "Niewłaściwa nazwa portu lub port jest"
" aktywny.", "Błąd", MB_OK);
break;
};
}
}
//----------------wysłanie pliku--------------------------------------
void __fastcall TForm1::SendClick(TObject *Sender)
{
DWORD FileSizeHigh;
ProgressBar1->Max = 0;
if (hCommDev > 0)
{
memset(Buffer_O, 0, cbOutQueue);
if ((_lopen(FileListBox1->FileName.c_str(),OF_READ)) !=
HFILE_ERROR)
{
hfile_s = _lopen(FileListBox1->FileName.c_str(), OF_READ);
ProgressBar1->Max=GetFileSize((HANDLE)hfile_s,
&FileSizeHigh);
while (_lread(hfile_s, &Buffer_O[0], 1))
{
Write_Comm(hCommDev, 1); // wysłanie 1 bajta
ProgressBar1->StepIt();
}
_lclose(hfile_s);
FlushFileBuffers(hCommDev);
}
else
MessageBox(NULL, "Nie wybrano pliku do transmisji.",
"Błąd !", MB_OK);
}
else
MessageBox(NULL, "Niewłaściwa nazwa portu lub port jest"
" aktywny.", "Błąd", MB_OK);
}
//----------------------odbiór pliku----------------------------------
void __fastcall TForm1::ReceiveClick(TObject *Sender)
{
memset(Buffer_I, 0, cbInQueue);
PurgeComm(hCommDev, PURGE_TXABORT);
Read_Comm(hCommDev, &Number_Bytes_Read, sizeof(Buffer_I));
if (Number_Bytes_Read > 0) // jeżeli odebrano jakieś bajty
Memo3->Text = Buffer_I;
for (int i = 0; i <= cbInQueue - 1; i ++)
if (Buffer_I[i] == NULL)
Buffer_I[i] = '.';
Memo3->Text = Buffer_I;
}
//--------------------------------------------------------------------
Spoglądając jeszcze raz na powyższe zapisy, zwróćmy uwagę, że już na tym etapie wprowadziliśmy szereg zabezpieczeń w postaci komunikatów uniemożliwiających wysłanie nieistniejącego lub nie otwartego zbioru danych. Testując powyższy program wysłałem do sąsiedniego komputera, na którym uruchomiony był Terminal dla Win16 pewien plik *.cpp. W odpowiedzi kolega przysłał mi zbiór bootlog.txt. Format jego wyświetlania nie jest może zbyt zachęcający, ale chcę byśmy poznali właściwości kilku komponentów, za pomocą których można oglądać przychodzące informacje.
Czytając pliki z dysku można oczywiście równie dobrze posłużyć się rodziną funkcji FileCreate(), FileOpen(), FileSeek(), FileRead(), FileClose(), FileWrite(). Sposób ich użycia jest bardzo dokładnie opisany (wraz z przykładami) w plikach pomocy, dostępnych zarówno w Builderze jak i w Delphi, więc nie będziemy ich tu przepisywać.
Wykorzystanie komponentu TTimer
Być może niektórzy Czytelnicy poczuli się nieco zawiedzeni faktem przedstawienia w poprzednim akapicie tylko jednego programu obsługującego pliki. Mimo iż obecny fragment pracy ma być poświęcony komponentowi TTimer, to postaramy się przy tej okazji przemycić jeszcze parę cennych informacji na temat operacji plikowych.
Zaczniemy trochę przewrotnie. Pokażemy, jak można określić czas przybycia do naszego portu początku jakiejś większej porcji informacji, próbkując wybrane łącze. Zbudujemy tym razem już pokaźną aplikację. Dwa komponenty — pola edycji TRichEdit — pełnić będą rolę obszarów, w których będziemy wyświetlać dane lub pliki przeznaczone od wysłania oraz otrzymane informacje. Komponenty TOpenDialog, TSaveDialog oraz TMainMenu zapewnią naszej aplikacji wygodne otwieranie i zapisywanie plików. Komponent TTimer umożliwi czasowe próbkowanie wybranego portu szeregowego. Siedem przycisków typu TSpeedButton zgrupowanych w obszarze określonym komponentem TCoolBar pełnić będzie funkcje pomocne w edycji pliku. Dzięki pierwszemu z nich, nazwanemu przeze mnie FileOpen, będziemy mogli wybrać i otworzyć dany plik. Z przyciskiem tym skojarzona jest funkcja obsługi zdarzenia FileOpenClick(). Przyciskowi FileSave odpowiada zdarzenie FileSaveClick(), za pomocą którego będziemy mogli zapisać w postaci pliku dane lub informacje wprowadzane przez nas z klawiatury w obszarze komponentu RichEdit1. Zwrócimy uwagę, że aplikacja ta umożliwi nam już nie tylko wysyłanie plików, ale również danych wprowadzanych aktualnie z klawiatury. Następne przyciski zgrupowane w obszarze danych wysyłanych pełnić będą funkcje uproszczonego edytora IDE. Jak wszystkim wiadomo, edytor taki pozwala wykonywać działania na blokach tekstowych polegajace na ich usuwaniu, przenoszeniu lub kopiowaniu w inne miejsce. Aby przekopiować blok tekstu, należy go najpierw zaznaczyć. Treść funkcji obsługi zdarzenia CopyTextClick() jest bardzo prosta:
RichEdit1->CopyToClipboard();
Użycie właściwości CopyToClipboard obiektu TRichEdit spowoduje przekopiowane zaznaczonego fragmentu tekstu do schowka (ang. clipboard). Można go potem wstawić w inne miejsce, korzystając z przycisku PasteText, z którym skojarzona jest funkcja obsługi zdarzenia PasteTextClick(). Zaznaczony fragment można też usunąć, naciskając przycisk CutText, odpowiadający wywołaniu zdarzenia CutTextClick(). Każdy tego typu program powinien mieć możliwość czyszczenia buforów komunikacyjnych. Zadanie to wykonamy, wywołując funkcję obsługi zdarzenia CleanBuffersClick():
void __fastcall TForm1::CleanBuffersClick(TObject *Sender)
{
for (int i = 0; i <= cbInQueue - 1; i ++)
{
Buffer_I[i] = NULL;
RichEdit1->Text = Buffer_I;
}
for (int i = 0; i <= cbOutQueue - 1; i ++)
{
Buffer_O[i] = NULL;
RichEdit2->Text = Buffer_O;
}
// memset(Buffer_O, 0, cbOutQueue);
// memset(Buffer_I, 0, cbInQueue);
ProgressBar1->Max = 0;
}
Należy zwrócić uwagę, że samo użycie funkcji memset() zapewni nam jedynie umieszczenie wartości 0 w danym buforze, nie spowoduje natomiast usunięcia znaków z pola edycji RichEdit2. Aby uniknąć tego typu dwuznacznych sytuacji, bardzo często stosuje się konstrukcje takie jak przedstawiono powyżej. Korzystając z prostej pętli for , do bufora jawnie wpisujemy NULL i taki „poprawiony” bufor przypisujemy cesze Text danego komponentu. Można oczywiście, w zależności od potrzeb, zdarzenie takie rozbić na dwa oddzielne, polegające na osobnym czyszczeniu buforów. W tym przykładzie jedynie dla prostoty zapisu oba bufory komunikacyjne czyszczę jednocześnie. Dla wygody Użytkownika wszystkie powyższe zdarzenia zostały zdublowane w treści komponentu TMainMenu. W obszarze danych odbieranych umieszczony został przycisk typu TSpeedButton, nazwany przez nas ReceiveFileSave, z którym skojarzona jest funkcja obsługi zdarzenia ReceiveFileSaveClick(), zapewniającego możliwość niezależnego zapisania na dysku danych odbieranych z portu szeregowego. Należy jednak pamiętać, że odebrana informacja zostanie zapisana w formacie Rich Text Format!
Wszystkie początkowe ustawienia wykorzystywanych przez nas komponentów dialogowych, komponentu TTimer oraz opisy poszczególnych przycisków (cechy Hint) umieścimy w głównej funkcji naszego formularza:
void __fastcall TForm1::FormCreate(TObject *Sender)
{
OpenDialog1->InitialDir = ExtractFilePath(ParamStr(0));
OpenDialog1->Filter =
"*.dat, *.txt, *.cpp, *.c |*.dat; *.txt; *.cpp; *.c";
SaveDialog1->InitialDir = OpenDialog1->InitialDir;
SaveDialog1->Filter = "*.*|*.*";
Timer1->Enabled = FALSE;
Timer1->Interval = TIMER_INTERVAL; // przedział czasu
// próbkowania łącza
CheckComm->Enabled = FALSE;
FileOpen->Hint = "Otwórz plik";
FileOpen->ShowHint = TRUE;
FileSave->Hint = "Zapisz";
FileSave->ShowHint = TRUE;
CopyText->Hint = "Kopiuj";
CopyText->ShowHint = TRUE;
PasteText->Hint = "Wklej";
PasteText->ShowHint = TRUE;
CutText->Hint = "Wytnij";
CutText->ShowHint = TRUE;
CleanBuffers->Hint = "Wyczyść bufory";
CleanBuffers->ShowHint = TRUE;
ReceiveFileSave->Hint = "Zapisz otrzymane";
ReceiveFileSave->ShowHint = TRUE;
}
Sposób rozmieszczenia na ekranie poszczególnych komponentów, z których będzie korzystać nasza aplikacja, pokazano na rysunku 5.7.
Rysunek 5.7. Sposób rozmieszczenia poszczególnych komponentów w aplikacji realizującej transmisję szeregową z czasowym próbkowaniem łącza
|
|
Większość użytych przycisków pełnić będzie taką samą rolę jak we wcześniejszych programach, dlatego nie będziemy w tym miejscu szczegółowo omawiać ich znaczenia. Zastanowimy się jednak dokładniej nad parą zdarzeń ReceiveClick() (przycisk Odbierz, którego cechę Name określiłem jako Receive) oraz CheckCommClick() (przycisk Monitoruj łącze — CheckComm). Naciskając Odbierz, powodujemy wywołanie funkcji obsługi zdarzenia:
void __fastcall TForm1::ReceiveClick(TObject *Sender)
{
if (hCommDev > 0)
{
...
CheckComm->Enabled = TRUE;
GetCommMask(hCommDev, &fdwEvtMask);
SetCommMask(hCommDev, EV_RXCHAR);
bResult = Read_Comm(hCommDev, &Buffer_I[0], &Number_Bytes_Read,
sizeof(Buffer_I));
if (bResult && Number_Bytes_Read != 0)
{
RichEdit2->Text = Buffer_I;
Edit2->Text= " Dane zostały przetransferowane.";
}
}
...
}
Możemy albo spokojnie czekać, aż coś się pojawi w buforze wejściowym, albo po uaktywnieniu przycisku Monitoruj łącze cyklicznie je próbkować w poszukiwaniu pierwszego znaku, który się tam znajdzie. Teraz już wiadomo, dlaczego wcześniej użyliśmy maski EV_RXCHAR. Po wybraniu monitoringu łącza aplikacja zapyta, czy naprawdę chcemy to wykonać. Jeżeli potwierdzimy, spowodujemy załączenie funkcji obsługi zdarzenia TimerOnTimer() oraz wyświetlenie odpowiedniego komunikatu w komponencie Edit2. W przeciwnym wypadku przycisk Monitoruj łącze pozostanie wygaszony i Timer będzie nieaktywny. Załóżmy, że nacisnęliśmy OK.
void __fastcall TForm1::CheckCommClick(TObject *Sender)
{
if (Application->MessageBox(" Łącze będzie monitorowane do czasu"
" odebrania znaku." , "Uwaga!",
MB_OKCANCEL) != IDOK)
{
CheckComm->Enabled = FALSE;
Timer1->Enabled = FALSE;
Abort();
}
else
{
Timer1->Enabled = TRUE; // uaktywnia czasowe próbkowanie
// łącza
Edit2->Text = "Łącze jest monitorowane.";
}
...
}
Uaktywnimy tym samym czasowe, w odstępach 1 milisekundy, wyzwalanie zdarzenia:
void __fastcall TForm1::TimerOnTimer(TObject *Sender)
{
if (WaitCommEvent(hCommDev, &fdwEvtMask, NULL) > 0)// sprawdza czy
{ // nadszedł znak
Beep();
Edit2->Text = " Transfer danych.";
GetCommMask(hCommDev, &fdwEvtMask);
SetCommMask(hCommDev, EV_TXEMPTY);
Timer1->Enabled = FALSE;
CheckComm->Enabled = FALSE;
}
}
Będzie ono aż do skutku cyklicznie sprawdzać, czy do portu identyfikowanego przez hCommDev przyszedł jakiś znak. Wykorzystałem tu znaną już, niezwykle pożyteczną funkcję WaitCommEvent() z uprzednio wybraną dla fdwEvtMask stałą EV_RXCHAR. Jeżeli widoczny powyżej warunek będzie spełniony, aplikacja da nam sygnał dźwiękowy, że coś już jest w buforze. Można oczywiście w miejscu Beep() umieścić jakiś komunikat, np. w stylu MessageBox(). Należy przy tym pamiętać, aby w takiego typu konstrukcjach warunkowych zawsze dla funkcji WaitCommEvent() ustalać nową maskę, najlepiej EV_TXEMPTY. W przeciwnym razie będziemy mieli kłopoty z wysłaniem czegokolwiek. Jeżeli po usłyszeniu sygnału (lub zobaczeniu innego komunikatu) naciśniemy powtórnie Odbierz, będziemy mogli obejrzeć otrzymane dane i ewentualnie zapisać je do pliku. Ten typ próbkowania łącza musi się sam wyłączać, dlatego w zapisie funkcji obsługi zdarzenia TimerOnTimer() umieściłem instrukcję:
Timer1->Enabled = FALSE;
Trzeba zawsze o tym pamiętać, gdyż w przeciwnym razie bardzo łatwo zawiesimy program (i to dość poważnie). Ujmując rzecz ogólnie, należy mieć sporo wyczucia przy korzystaniu z niezwykle użytecznego Timera.
Na rysunku 5.8 pokazano wygląd naszej aplikacji, której projekt znajduje się w katalogu \KODY\BUILDER\RS_06\p_RS_06.bpr . Wydruk 5.7 przedstawia kompletny kod jej głównego modułu.
Rysunek 5.8. Formularz główny projektu p_RS_06.bpr po uruchomieniu
|
|
Wydruk 5.7. Kod aplikacji realizującej transmisję plików, wykorzystującej komponent TTimer
//--- kompilować z borlndmm.dll oraz cc3250mt.dll --------------
//--------RS_06.cpp-----
#include <vcl.h>
#pragma hdrstop
#include "RS_06.h"
#pragma package(smart_init)
#pragma resource "*.dfm"
#define cbOutQueue 1024 //rozmiar bufora danych wyjściowych
#define cbInQueue 1024 //rozmiar bufora danych wejściowych
#define TIMER_INTERVAL 1 //przedział czasu próbkowania Timera
TForm1 *Form1;
AnsiString New_File; // przechowuje nazwę pliku
HFILE hfile_s; // identyfikator pliku
char Buffer_O[cbOutQueue]; // bufor danych wyjściowych
char Buffer_I[cbInQueue]; // bufor danych wejściowych
DWORD Number_Bytes_Read; // liczba bajtów do czytania
HANDLE hCommDev; // identyfikator portu
LPCTSTR lpFileName; // wskaźnik do nazwy portu
DCB dcb; // struktura kontroli portu szeregowego
DWORD fdwEvtMask; //informacja o aktualnym stanie transmisji
COMSTAT Stat; // dodatkowa informacja o zasobach portu
DWORD Errors; // reprezentuje typ ewentualnego błędu
BOOL bResult ; // zmienna boolowska
//--------------------------------------------------------------------
int __fastcall Close_Comm(HANDLE hCommDev)
{
CloseHandle(hCommDev);
return TRUE;
}
//--------------------------------------------------------------------
int __fastcall Write_Comm(HANDLE hCommDev,
DWORD nNumberOfBytesToWrite)
{
DWORD NumberOfBytesWritten;
GetCommMask(hCommDev, &fdwEvtMask);
SetCommMask(hCommDev, EV_TXEMPTY);
if (WriteFile(hCommDev, &Buffer_O[0],
nNumberOfBytesToWrite, &NumberOfBytesWritten, NULL) > 0)
{
WaitCommEvent(hCommDev, &fdwEvtMask, NULL);
return TRUE;
}
else
return FALSE;
}
//--------------------------------------------------------------------
int __fastcall Read_Comm(HANDLE hCommDev,
LPDWORD lpNumberOfBytesRead, DWORD Buf_Size)
{
DWORD nNumberOfBytesToRead;
ClearCommError(hCommDev, &Errors ,&Stat);
if (Stat.cbInQue > 0)
{
if (Stat.cbInQue > Buf_Size)
nNumberOfBytesToRead = Buf_Size;
else
nNumberOfBytesToRead = Stat.cbInQue;
ReadFile(hCommDev, &Buffer_I[0], nNumberOfBytesToRead,
lpNumberOfBytesRead, NULL);
}
else
*lpNumberOfBytesRead = 0;
return TRUE;
}
//--------------------------------------------------------------------
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
}
//--------------------------------------------------------------------
void __fastcall TForm1::CloseCommClick(TObject *Sender)
{
Timer1->Enabled = FALSE;
CheckFileSave();
Close_Comm(hCommDev);
Application->Terminate();
}
//--------------------------------------------------------------------
void __fastcall TForm1::CheckFileSave(void)
{
if (RichEdit1->Modified)
{
switch(MessageBox(NULL, "Zawartość pliku lub okna została"
" zmieniona. Zapisać zmiany?", "Uwaga!",
MB_YESNOCANCEL | MB_ICONQUESTION))
{
case ID_YES : FileSaveClick(this);
case ID_CANCEL : Abort();
};
}
}
//--------------------------------------------------------------------
void __fastcall TForm1::FormCreate(TObject *Sender)
{
OpenDialog1->InitialDir = ExtractFilePath(ParamStr(0));
OpenDialog1->Filter =
"*.dat , *.txt, *.cpp, *.c | *.dat; *.txt; *.cpp; *.c";
SaveDialog1->InitialDir = OpenDialog1->InitialDir;
SaveDialog1->Filter = "*.*|*.*";
Timer1->Enabled = FALSE;
Timer1->Interval = TIMER_INTERVAL;
CheckComm->Enabled = FALSE;
FileOpen->Hint = "Otwórz plik.";
FileOpen->ShowHint = TRUE;
FileSave->Hint = "Zapisz.";
FileSave->ShowHint = TRUE;
CopyText->Hint = "Kopiuj.";
CopyText->ShowHint = TRUE;
PasteText->Hint = "Wklej.";
PasteText->ShowHint = TRUE;
CutText->Hint = "Wytnij.";
CutText->ShowHint = TRUE;
CleanBuffers->Hint = "Wyczyść bufory.";
CleanBuffers->ShowHint = TRUE;
ReceiveFileSave->Hint = "Zapisz otrzymane.";
ReceiveFileSave->ShowHint = TRUE;
}
//--------------------------------------------------------------------
void __fastcall TForm1::FileOpenClick(TObject *Sender)
{
CheckFileSave();
if (OpenDialog1->Execute())
{
RichEdit1->Lines->LoadFromFile(OpenDialog1->FileName);
RichEdit1->Modified = FALSE;
RichEdit1->ReadOnly =
OpenDialog1->Options.Contains(ofReadOnly);
}
}
//--------------------------------------------------------------------
void __fastcall TForm1::FileSaveClick(TObject *Sender)
{
if (! strcmp(New_File.c_str(), LoadStr(256).c_str()))
SaveAs1Click(Sender);
else
{
RichEdit1->Lines->SaveToFile(New_File);
RichEdit1->Modified = FALSE;
}
}
//--------------------------------------------------------------------
void __fastcall TForm1::CopyTextClick(TObject *Sender)
{
RichEdit1->CopyToClipboard();
}
//--------------------------------------------------------------------
void __fastcall TForm1::PasteTextClick(TObject *Sender)
{
RichEdit1->PasteFromClipboard();
}
//--------------------------------------------------------------------
void __fastcall TForm1::CutTextClick(TObject *Sender)
{
RichEdit1->CutToClipboard();
}
//--------------------------------------------------------------------
void __fastcall TForm1::UndoClick(TObject *Sender)
{
if (RichEdit1->HandleAllocated())
SendMessage(RichEdit1->Handle, EM_UNDO, 0, 0);
}
//--------------------------------------------------------------------
void __fastcall TForm1::SelectAllClick(TObject *Sender)
{
RichEdit1->SelectAll();
}
//--------------------------------------------------------------------
void __fastcall TForm1::CleanBuffersClick(TObject *Sender)
{
for (int i = 0; i <= cbInQueue - 1; i ++)
{
Buffer_I[i] = NULL;
RichEdit1->Text = Buffer_I;
}
for (int i = 0; i <= cbOutQueue - 1; i ++)
{
Buffer_O[i] = NULL;
RichEdit2->Text = Buffer_O;
}
//memset(Buffer_O, 0, cbOutQueue);
//memset(Buffer_I, 0, cbInQueue);
ProgressBar1->Max = 0;
}
//--------------------------------------------------------------------
void __fastcall TForm1::OpenCommClick(TObject *Sender)
{
if (CheckBox1->Checked == TRUE) // wybór portu
lpFileName = "COM1";
if (CheckBox2->Checked == TRUE)
lpFileName = "COM2";
hCommDev = CreateFile(lpFileName, GENERIC_READ | GENERIC_WRITE,
0, NULL, OPEN_EXISTING, 0, NULL);
if (hCommDev != INVALID_HANDLE_VALUE) // sprawdza, czy port jest
// otwarty prawidłowo
{
SetupComm(hCommDev, cbInQueue, cbOutQueue);
dcb.DCBlength = sizeof(dcb);
GetCommState(hCommDev, &dcb);
if (CheckBox3->Checked == TRUE) // wybór prędkości
dcb.BaudRate = CBR_1200;
if (CheckBox4->Checked == TRUE)
dcb.BaudRate = CBR_19200;
dcb.Parity = ODDPARITY; // ustawienie parzystości
dcb.StopBits = ONESTOPBIT; // bity stopu
dcb.ByteSize = 7; // bity danych
//-przykładowe ustawienia flag sterujących DCB-
dcb.fParity = TRUE;
dcb.fDtrControl = DTR_CONTROL_DISABLE;
dcb.fRtsControl = RTS_CONTROL_DISABLE;
dcb.fOutxCtsFlow = FALSE;
dcb.fOutxDsrFlow = FALSE;
dcb.fDsrSensitivity = FALSE;
dcb.fAbortOnError = FALSE;
dcb.fOutX = FALSE;
dcb.fInX = FALSE;
dcb.fErrorChar = FALSE;
dcb.fNull = FALSE;
dcb.EofChar = FALSE;
SetCommState(hCommDev, &dcb);
}
else
{
switch ((int)hCommDev)
{
case IE_BADID:
MessageBox(NULL, "Niewłaściwa nazwa portu lub port jest"
" aktywny.", "Błąd", MB_OK);
break;
};
}
}
//--------------------------------------------------------------------
void __fastcall TForm1::SendFileClick(TObject *Sender)
{
DWORD FileSizeHigh;
CheckComm->Enabled = FALSE;
ProgressBar1->Max = 0;
if ((_lopen(OpenDialog1->FileName.c_str(), OF_READ))!= HFILE_ERROR)
{
hfile_s =_lopen(OpenDialog1->FileName.c_str(), OF_READ );
ProgressBar1->Max = GetFileSize((HANDLE)hfile_s, &FileSizeHigh);
while (_lread(hfile_s, &Buffer_O[0], 1))
{
Write_Comm(hCommDev, 1);
ProgressBar1->StepIt();
}
_lclose(hfile_s);
FlushFileBuffers(hCommDev);
}
else
MessageBox(NULL, "Nie wybrano pliku do transmisji.", "Błąd !",
MB_OK);
}
//--------------------------------------------------------------------
void __fastcall TForm1::SendWrittenClick(TObject *Sender)
{
if (hCommDev > 0)
{
CheckComm->Enabled = FALSE;
try
{
strcpy(Buffer_O, RichEdit1->Lines->Text.c_str());
ProgressBar1->Max = 0;
ProgressBar1->Max = sizeof(RichEdit1->Text.c_str());
Write_Comm(hCommDev, strlen(Buffer_O));
ProgressBar1->StepIt();
FlushFileBuffers(hCommDev);
}
catch (...)
{
MessageBox(NULL, " Próba nadpisywania na pliku "
" wykorzystywanym przez inny proces."
" Uruchom ponownie aplikację. ",
" Błąd transmisji ", MB_OK);
}
}
else
MessageBox(NULL, "Niewłaściwa nazwa portu lub port jest"
" aktywny.", "Błąd", MB_OK);
}
//--------------------------------------------------------------------
void __fastcall TForm1::ReceiveClick(TObject *Sender)
{
if (hCommDev > 0)
{
CheckComm->Enabled = TRUE;
RichEdit2->Clear();
GetCommMask(hCommDev, &fdwEvtMask);
SetCommMask(hCommDev, EV_RXCHAR );
bResult = Read_Comm(hCommDev, &Number_Bytes_Read,
sizeof(Buffer_I));
if (bResult && Number_Bytes_Read != 0)
{
RichEdit2->Text = Buffer_I;
Edit2->Text= " Dane zostały przetransferowane.";
}
}
else
MessageBox(NULL, "Niewłaściwa nazwa portu lub port jest"
" aktywny.", "Błąd", MB_OK);
}
//--------------------------------------------------------------------
void __fastcall TForm1::OpenClick(TObject *Sender)
{
CheckFileSave();
if (OpenDialog1->Execute())
{
RichEdit1->Lines->LoadFromFile(OpenDialog1->FileName);
RichEdit1->Modified = FALSE;
RichEdit1->ReadOnly =
OpenDialog1->Options.Contains(ofReadOnly);
}
}
//--------------------------------------------------------------------
void __fastcall TForm1::SaveAs1Click(TObject *Sender)
{
if (SaveDialog1->Execute()) // dane będą zapisywane w
// formacie Rich!
{
RichEdit1->Lines->SaveToFile(SaveDialog1->FileName);
RichEdit1->Modified = FALSE;
}
}
//--------------------------------------------------------------------
void __fastcall TForm1::TimerOnTimer(TObject *Sender)
{
if (WaitCommEvent(hCommDev, &fdwEvtMask, NULL) > 0)// sprawdza czy
{ // nadszedł znak
Beep();
Edit2->Text = " Transfer danych.";
GetCommMask(hCommDev, &fdwEvtMask);
SetCommMask(hCommDev, EV_TXEMPTY);
Timer1->Enabled = FALSE;
CheckComm->Enabled = FALSE;
}
}
//--------------------------------------------------------------------
void __fastcall TForm1::ReceiveFileSaveClick(TObject *Sender)
{
if (SaveDialog1->Execute())
{
RichEdit2->Lines->SaveToFile(SaveDialog1->FileName);
RichEdit2->Modified = FALSE;
}
}
//--------------------------------------------------------------------
void __fastcall TForm1::NewClick(TObject *Sender)
{
CheckFileSave();
RichEdit1->Lines->Clear();
RichEdit1->Modified = FALSE;
}
//--------------------------------------------------------------------
void __fastcall TForm1::CheckCommClick(TObject *Sender)
{
if (Application->MessageBox(" Łącze będzie monitorowane do czasu"
" odebrania znaku." , "Uwaga!",
MB_OKCANCEL) != IDOK)
{
CheckComm->Enabled = FALSE;
Timer1->Enabled = FALSE;
Abort();
}
else
{
Timer1->Enabled = TRUE;
Edit2->Text = "Łącze jest monitorowane.";
}
/*if (MessageDlg(" Łącze będzie monitorowane do czasu odebrania"
" znaku.", mtConfirmation,
TMsgDlgButtons() << mbYes << mbNo, 0) == mrYes)
{
Timer1->Enabled = TRUE;
Edit2->Text = "Łącze jest monitorowane.";
}
else
{
CheckComm->Enabled = FALSE;
Timer1->Enabled = FALSE;
Abort();
}*/
}
//--------------------------------------------------------------------
Opisany program testowałem, łącząc się z pewnym w pełni zautomatyzowanym urządzeniem pomiarowym. Wysłałem z uprzednio przygotowanego standardowego pliku zapytanie o aktualną krzywą skalowania, jaką posługuje się ten przyrząd. W odpowiedzi miernik przysłał mi wszystkie niezbędne informacje. Mając dwa okna edycji mogę, spoglądając na otrzymane, dane wpisać w RichEdit1 swoje własne parametry nowej krzywej skalującej. Naciskając przycisk Wyślij wpisane, skojarzony z funkcją obsługi zdarzenia SendWrittenClick(), odpowiednio przeprogramuję miernik. Można go oczywiście testować łącząc się z innym komputerem, odbierając pliki lub pojedyncze znaki. Na powyższym przykładzie przedstawiono też różne sposoby wywoływania komunikatów Win32 API, takich jak: MessageBox() czy MessageDlg() oraz zaprezentowano ideę obsługi wyjątków pokazaną w funkcji obsługi zdarzenia SendWrittenClick().
Wszystko, co powiedzieliśmy na temat sposobów wyszukiwania początku ciągu znaków przychodzących do łącza, jest niewątpliwie pożyteczne, niemniej jednak w większości przypadków, z którymi spotykamy się w praktyce, bardziej interesuje nas możliwość cyklicznego odczytywania wskazań określonego przyrządu pomiarowego. Załóżmy, że chcemy mieć możliwość bieżącego odczytywania napięcia, natężenia prądu czy chociażby temperatury. Konstruując tego typu aplikacje, powinniśmy przewidzieć możliwość wyboru przedziału czasu próbkowania sygnałów, pojawiających się na wejściu wybranego portu szeregowego w czasie działania programu. Dosyć dobrze do tego celu nadaje się komponent TCSpinEdit. Dzięki odpowiedniemu wyborowi jego cechy Value będziemy mogli automatycznie dostosować do naszych potrzeb wartość cechy Interval (odstęp) komponentu TTimer. Pamiętamy, że odstęp czasu, w którym dokonujemy próbkowania łącza szeregowego, podajemy w milisekundach. Przykładowa aplikacja, za pomocą której będzie można odczytywać aktualne wskazania przyrządu, zbudowana będzie ze znanych nam już elementów. Dane odbierane wyświetlać będziemy za pomocą pola edycji TRichEdit. Aplikacja zaopatrzona będzie dodatkowo w przycisk uruchamiający pomiar ciągły i wyłączający go. W funkcji obsługi zdarzenia MeasureONClick(), które wywołujemy naciśnięciem przycisku Włącz pomiar, mamy:
void __fastcall TForm1::MeasureONClick(TObject *Sender)
{
if (hCommDev > 0) // powtórnie sprawdza czy port jest otwarty
{
strcpy(Buffer_O, query);
Timer1->Enabled = TRUE;
}
...
}
Rozkaz wysyłany do miernika zostanie skopiowany do obszaru pamięci wskazywanego przez Buffer_O. Ponadto wywołamy tu cyklicznie funkcję obsługi zdarzenia TimerOnTimer(), próbkującego wybrany port szeregowy:
void __fastcall TForm1::TimerOnTimer(TObject *Sender)
{
Write_Comm(hCommDev, strlen(Buffer_O));
Sleep(800);
Beep();
FlushFileBuffers(hCommDev);
Read_Comm(hCommDev, &Number_Bytes_Read, sizeof(Buffer_I));
if (Number_Bytes_Read > 0)
RichEdit1->Text = Buffer_I;
}
Jedyną rolą tej funkcji jest wysłanie zapytania do urządzenia i odebranie odpowiedzi. Czynność ta może być wykonywana wielokrotnie, bez jakichkolwiek ograniczeń czasowych. Użytkownik sam decyduje, kiedy zakończyć pomiar, naciskając przycisk Wyłącz pomiar. Oczywiście, odebrane dane z reguły należy odpowiednio zapisać na dysku i dobrze by było, gdybyśmy mieli możliwość założenia i otwarcia pliku jeszcze przed rozpoczęciem pomiarów. Myślę jednak, że wszystko to, co powiedzieliśmy do tej pory na temat operacji plikowych, w zupełności wystarczy nawet mniej wprawnemu Czytelnikowi, aby poradził sobie z problemem. Dane będą musiały być zapisywane „on line” (czyli w trakcie), gdyż zastosowany przeze mnie sposób wywoływania funkcji Write_Comm(), wysyłającej zapytanie do urządzenia oraz Read_Comm()i czytającej odpowiedź miernika wyklucza jakiekolwiek dalsze buforowanie danych ponad to, co aktualnie wyświetlamy na ekranie. W tego typu programach nigdy nie stosuje się jakiegoś szczególnego sposobu przechowywania danych w pamięci. Powód jest prosty: nigdy nie wiemy, ile informacji tak naprawdę otrzymamy. Pomiar równie dobrze może trwać pięć sekund jak i pięć lub piętnaście godzin, zresztą do tego tematu jeszcze powrócimy w następnych rozdziałach.
Na rysunku 5.9 zaprezentowano formularz projektu znajdującego się na załączonym krążku CD \KODY\BUILDER\RS_07\p_RS_07.bpr, obsługującego przyrząd dokonujący cyklicznego odczytu aktualnie mierzonej temperatury (w tym przypadku w stopniach Celsjusza). Wydruk 5.8 przedstawia zastosowany przeze mnie algorytm.
Rysunek 5.9. Formularz główny projektu p_RS_07.bpr
|
|
Wydruk 5.8. Kod aplikacji próbkującej wybrane łącze szeregowe w z góry zadanych odstępach czasu w poszukiwaniu aktualnych wskazań miernika cyfrowego
//--- kompilować z borlndmm.dll cc3250mt.dll -----------------------
//----RS_07.cpp-------------
#include <vcl.h>
#pragma hdrstop
#include "RS_07.h"
#pragma package(smart_init)
#pragma link "CSPIN"
#pragma resource "*.dfm"
#define cbOutQueue 16 //rozmiar bufora danych wyjściowych
#define cbInQueue 16 //rozmiar bufora danych wejściowych
TForm1 *Form1;
LPCTSTR query = "CDAT?\r\n"; // przykładowe zapytanie o
// temperaturę, zakończone parą
// znaków CR LF
char Buffer_O[cbOutQueue]; // bufor danych wyjściowych
char Buffer_I[cbInQueue]; // bufor danych wejściowych
DWORD Number_Bytes_Read; // liczba bajtów do czytania
HANDLE hCommDev; // identyfikator portu
LPCTSTR lpFileName; // wskaźnik do nazwy portu
DCB dcb; // struktura kontroli portu szeregowego
DWORD fdwEvtMask; // informacja o aktualnym stanie transmisji
COMSTAT Stat; // dodatkowa informacja o zasobach portu
DWORD Errors; // reprezentuje typ ewentualnego błędu
//--------------------------------------------------------------------
int __fastcall Close_Comm(HANDLE hCommDev)
{
CloseHandle(hCommDev);
return TRUE;
}
//--------------------------------------------------------------------
int __fastcall Write_Comm(HANDLE hCommDev,
DWORD nNumberOfBytesToWrite)
{
DWORD NumberOfBytesWritten;
if (WriteFile(hCommDev, &Buffer_O[0], nNumberOfBytesToWrite,
&NumberOfBytesWritten , NULL) > 0)
{
WaitCommEvent(hCommDev, &fdwEvtMask, NULL);
return TRUE;
}
else
return FALSE;
}
//--------------------------------------------------------------------
int __fastcall Read_Comm(HANDLE hCommDev,
LPDWORD lpNumberOfBytesRead, DWORD Buf_Size)
{
DWORD nNumberOfBytesToRead;
ClearCommError(hCommDev, &Errors ,&Stat);
if (Stat.cbInQue > 0)
{
if (Stat.cbInQue > Buf_Size)
nNumberOfBytesToRead = Buf_Size;
else
nNumberOfBytesToRead = Stat.cbInQue;
ReadFile(hCommDev, &Buffer_I[0], nNumberOfBytesToRead,
lpNumberOfBytesRead, NULL);
}
else
*lpNumberOfBytesRead = 0;
return TRUE;
}
//--------------------------------------------------------------------
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
}
//--------------------------------------------------------------------
void __fastcall TForm1::CloseCommClick(TObject *Sender)
{
Timer1->Enabled = FALSE;
Close_Comm(hCommDev);
Application->Terminate();
}
//--------------------------------------------------------------------
void __fastcall TForm1::FormCreate(TObject *Sender)
{
Timer1->Enabled = FALSE;
CSpinEdit1->Value = 100;
CSpinEdit1->ReadOnly = FALSE;
CSpinEdit1->Cursor = crNo;
CSpinEdit1->Hint = "Ręczne wpisywanie może być niebezpieczne !";
CSpinEdit1->ShowHint = TRUE;
CSpinEdit1->Increment = 100;
}
//--------------------------------------------------------------------
void __fastcall TForm1::CSpinEdit1Change(TObject *Sender)
{
if (CSpinEdit1->Value < 0) // uniemożliwia ustalenie wartości
// ujemnej
CSpinEdit1->Value = abs(CSpinEdit1->Value);
Timer1->Interval = CSpinEdit1->Value;
}
//--------------------------------------------------------------------
void __fastcall TForm1::MeasureOFFClick(TObject *Sender)
{
Timer1->Enabled = FALSE;
}
//--------------------------------------------------------------------
void __fastcall TForm1::OpenCommClick(TObject *Sender)
{
if (CheckBox1->Checked == TRUE) // wybór portu
lpFileName = "COM1";
if (CheckBox2->Checked == TRUE)
lpFileName = "COM2";
hCommDev = CreateFile(lpFileName, GENERIC_READ | GENERIC_WRITE,
0, NULL, OPEN_EXISTING, 0, NULL);
if (hCommDev != INVALID_HANDLE_VALUE) // sprawdza, czy port jest
// otwarty prawidłowo
{
SetupComm(hCommDev, cbInQueue, cbOutQueue);
dcb.DCBlength = sizeof(dcb);
GetCommState(hCommDev, &dcb);
if (CheckBox3->Checked == TRUE) // wybór prędkości transmisji
dcb.BaudRate=CBR_300;
if (CheckBox4->Checked == TRUE)
dcb.BaudRate=CBR_1200;
if (CheckBox5->Checked == TRUE)
dcb.BaudRate=CBR_9600;
dcb.Parity = ODDPARITY; // ustawienie parzystości
dcb.StopBits = ONESTOPBIT; // bity stopu
dcb.ByteSize = 7; // bity danych
//-przykładowe ustawienia flag sterujących DCB-
dcb.fParity = TRUE; // sprawdzanie parzystości
dcb.fDtrControl = DTR_CONTROL_DISABLE;
dcb.fRtsControl = RTS_CONTROL_DISABLE;
dcb.fOutxCtsFlow = FALSE;
dcb.fOutxDsrFlow = FALSE;
dcb.fDsrSensitivity = FALSE;
dcb.fAbortOnError = FALSE;
dcb.fOutX = FALSE;
dcb.fInX = FALSE;
dcb.fErrorChar = FALSE;
dcb.fNull = FALSE;
SetCommState(hCommDev, &dcb);
GetCommMask(hCommDev, &fdwEvtMask);
SetCommMask(hCommDev, EV_TXEMPTY);
}
else
{
switch ((int)hCommDev)
{
case IE_BADID:
MessageBox(NULL, "Niewłaściwa nazwa portu lub port jest"
" aktywny.", "Błąd", MB_OK);
break;
};
}
}
//--------------------------------------------------------------------
void __fastcall TForm1::MeasureONClick(TObject *Sender)
{
if (hCommDev > 0) // powtórnie sprawdza czy port jest otwarty
{
strcpy(Buffer_O, query);
Timer1->Enabled = TRUE;
}
else
MessageBox(NULL, "Port nie został otwarty do transmisji.",
"Błąd", MB_OK);
}
//--------------------------------------------------------------------
void __fastcall TForm1::TimerOnTimer(TObject *Sender)
{
Write_Comm(hCommDev, strlen(Buffer_O));
Sleep(100);
Beep();
FlushFileBuffers(hCommDev);
Read_Comm(hCommDev, &Number_Bytes_Read, sizeof(Buffer_I));
if (Number_Bytes_Read > 0)
RichEdit1->Text = Buffer_I;
}
//--------------------------------------------------------------------
Śledząc kod powyższego wydruku, można zauważyć, że program ten obsługuje miernik, który w odpowiedzi na komendę-zapytanie CDAT?\r\n automatycznie dokonuje odpowiedniego pomiaru, dodatkowo sygnalizując ten fakt brzęczkiem. W tym przykładzie przedział czasu odczytu z łącza ustaliłem na 1000 milisekund. Można oczywiście robić to szybciej, niemniej jednak musimy pamiętać o funkcji synchronizacji naszego interfejsu. Testowany przyrząd nie jest w stanie zwrócić odpowiedzi (czyli dokonać nowego pomiaru) częściej niż raz na sekundę, dlatego szybsze odpytywanie nie ma sensu. Przed przystąpieniem do projektowania tego typu algorytmów należy w pierwszej kolejności dokładnie przeczytać instrukcję obsługi urządzenia. --> Zwróćmy też uwagę, że urządzenie to wymagało, by wysyłane polecenie zakończyć parą znaków \r\n, czyli powrót karetki (CR) i znak nowego wiersza (LF). [Author:kdh]
Aplikacja nie lubi milczeć
Jak zapewne zauważyliśmy, projektując wszystkie przedstawione do tej pory programy, zwracaliśmy baczną uwagę na to, by aplikacja powiadamiała nas o problemach, jakie napotyka w czasie działania. Korzystaliśmy ze standardowych funkcji Windows, takich jak:
extern PACKAGE void __fastcall ShowMessage(const System::AnsiString
Msg);
extern PACKAGE int __fastcall MessageDlg(const System::AnsiString Msg, TMsgDlgType DlgType, TMsgDlgButtons Buttons, int HelpCtx);
gdzie:
enum TMsgDlgType { mtWarning, mtError, mtInformation, mtConfirmation,
mtCustom };
typedef Set<TMsgDlgBtn, mbYes, mbHelp> TMsgDlgButtons;
oraz z funkcji:
int __fastcall MessageBox(char * Text, char * Caption, int Flags);
Wszystkie one zostały już użyte w odpowiednich kontekstach, dlatego nie ma potrzeby ponownego prezentowania sposobu umieszczenia ich w programie.
W książce tej poruszamy temat komunikacji komputerowej poprzez interfejs RS 232C. Programy pisane przez nas mogą komunikować się z różnymi urządzeniami zewnętrznymi. Oprócz wysokiej sprawności i niezawodności muszą one posiadać nie spotykaną gdzie indziej cechę, polegającą na możliwości błyskawicznego zdiagnozowania, czy w ogóle jest się z kim łączyć, tzn. czy urządzenie zewnętrzne istnieje (jest włączone). Może zdarzyć się i taka sytuacja, że miernik najzwyczajniej w świecie może się popsuć w trakcie pomiaru lub z jakiś innych względów odmówić dalszej współpracy (uszkodzone łącze lub linia transmisyjna). Aplikacja sterująca przyrządem nie tylko nie powinna się wówczas zawiesić, ale jeszcze powiadomić nas o zaistniałej sytuacji. Stosowana przez nas do tej pory konstrukcja:
Read_Comm(hCommDev, &Number_Bytes_Read, sizeof(Buffer_I));
if (Number_Bytes_Read > 0)
{
// wyświetl odebrane dane
}
jest mało praktyczna — jeżeli dane przestaną napływać w wyniku uszkodzenia przyrządu, to na ekranie nie zobaczymy nic. Jedyną jej zaletą jest fakt, że program powinien dalej działać. Powiedzmy, że chcemy mieć informację o tym, że urządzenie się wyłączyło. W tym celu skorzystamy z funkcji Win32 API, zwracającej typ ostatniego błędu:
DWORD GetLastError(VOID);
Win32 API umożliwia nam też ustalanie własnego typu wartości danego błędu. Wystarczy użyć:
VOID SetLastError(DWORD fdwError);
gdzie fdwError określa kod ostatniego błędu. Wygodny sposób wykorzystania tych funkcji np. w ostatnio omawianej funkcji obsługi zdarzenia TimerOnTimer() mógłby wyglądać następująco:
DWORD dwError;
...
void __fastcall TForm1::TimerOnTimer(TObject *Sender)
{
Write_Comm(hCommDev, strlen(Buffer_O));
Sleep(100);
Beep();
FlushFileBuffers(hCommDev);
Read_Comm(hCommDev, &Number_Bytes_Read, sizeof(Buffer_I));
if (Number_Bytes_Read == 0)
{
SetLastError(0xFFFFFFFF);
dwError = GetLastError();
RichEdit1->Text = (IntToStr(dwError));
}
else
RichEdit1->Text = Buffer_I;
}
Przetestowałem nasz program z uwzględnieniem tej modyfikacji. W trakcie pomiaru odłączyłem miernik. Aplikacja wcale się nie zawiesiła, brzęczek (funkcja Beep()) był dalej aktywny, natomiast w polu edycji pojawił się następujący komunikat:
Rysunek 5.10. Działanie aplikacji w momencie odłączenia przyrządu pomiarowego
|
|
Już teraz wiemy, że urządzenie zostało po prostu wyłączone i należy jakoś zareagować na zaistniałą sytuację. Odczytałem tu jedynie liczbę reprezentującą ostatnio wykryty błąd. Ktoś mógłby postąpić bardziej wyszukanie, wyświetlając np. odpowiedni komunikat czy uruchamiając jakiś wymyślny sygnał dźwiękowy. Ważne jest jednak to, że w prosty sposób możemy zdiagnozować nawet tak poważny błąd w transmisji jak jej zatrzymanie. Wszystko wraca do normy po powtórnym włączeniu miernika.
Stosując GetLastError() nie tylko w obrębie programów komunikacyjnych, możemy spodziewać się najczęściej wartości 0xFFFFFFFF lub -1. Jeżeli zaś błąd nie wystąpił, oczekuje się ERROR_SUCCES, czyli 0. Należy pamiętać, że funkcja GetLastError() w głównej mierze bazuje na wątkach, dlatego pełne jej wykorzystanie może nastąpić tylko w ramach konkretnego wątku. Definiując błąd jako 0xFFFFFFFF, postąpiłem bardzo ostrożnie. Kody błędów funkcji Win32 API są 32-bitowe, przy czym bit numer 31 jest bitem bardziej znaczącym. Bit 29. jest z reguły zarezerwowany dla aplikacji, w których chcemy zdefiniować własne komunikaty. Należy go odpowiednio ustawić i wówczas nie napotkamy żadnego konfliktu z kodami błędów innych funkcji. Niemniej należy pamiętać, że będzie to prawdą wyłącznie wtedy, gdy korzystamy z funkcji Win32 API, które potrafią nam zwrócić kod ostatniego błędu.
Niekiedy wykorzystywanie funkcji błędów w sposób uproszczony może okazać się zwodnicze, szczególnie w aplikacjach odczytujących wyniki pomiarów w postaci liczb. Powyższy przykład jest tego ilustracją. Bardzo często stykamy się z sytuacją, w której zwracany kod błędu w postaci liczby może dość poważnie wprowadzić w błąd Użytkownika. W naszym przykładzie mierzona w stopniach Celsjusza temperatura może równie dobrze przyjmować wartości ujemne, zatem przedstawione rozwiązanie w tym konkretnym wypadku nie wydaje się zbyt fortunne. Bardziej przejrzysty i elegancki sposób skorzystania z usług pary funkcji SetLastError() oraz GetLastError() w kontekście naszego programu mógłby wyglądać tak, jak pokazano niżej. Wykorzystałem tu również funkcję FormatMessage() umożliwiającą przedstawienie wybranego komunikatu Windows w bardzo wygodnej dla Użytkownika formie.
LPVOID MsgBuf;
...
if (Number_Bytes_Read == 0)
{
Timer1->Enabled = FALSE;
SetLastError(ERROR_READ_FAULT);
FormatMessage(
FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM,
NULL, GetLastError(), MAKELANGID(LANG_NEUTRAL,
SUBLANG_DEFAULT), (LPTSTR) &MsgBuf, 0, NULL );
MessageBox(NULL, (LPTSTR) MsgBuf, "Błąd transmisji",
MB_OK|MB_ICONINFORMATION);
// zwolnienie bufora
LocalFree(MsgBuf);
}
else
RichEdit1->Text = Buffer_I;
W tym wypadku, jeżeli miernik z jakiś przyczyn przestanie odpowiadać, na ekranie ujrzymy pomocną informację:
Rysunek 5.11. Informacja pojawiająca się w trakcie działania aplikacji w przypadku utraty możliwości czytania z urządzenia
|
|
Przedstawiony sposób umożliwia wzbogacenie pisanych programów jeszcze w szereg innych komunikatów sygnalizujących błędy Win32 API. Poniżej zamieszczam kilka najbardziej użytecznych:
ERROR_BAD_UNIT — odnalezienie urządzenia jest niemożliwe.
ERROR_NOT_READY — urządzenie nie jest gotowe.
ERROR_BAD_COMMAND — urządzenie nie rozpoznaje polecenia.
ERROR_BAD_LENGTH — program wydał polecenie, ale jego długość jest niewłaściwa.
ERROR_WRITE_FAULT — system nie może zapisywać do określonego urządzenia.
ERROR_READ_FAULT — system nie może czytać z określonego urządzenia.
ERROR_GEN_FAILURE — urządzenie podłączone do komputera nie działa.
ERROR_OPEN_FAILED — system nie może otworzyć określonego urządzenia lub pliku.
ERROR_IO_DEVICE — żądanie nie mogło być wykonane z powodu błędu urządzenia We-Wy.
ERROR_SERIAL_NO_DEVICE — żądane urządzenie szeregowe nie zostało pomyślnie
zainicjalizowane. Program obsługi szeregowej zostanie usunięty z
pamięci.
ERROR_MORE_WRITES — operacja szeregowa We-Wy została zakończona przez inny zapis
do portu szeregowego.
ERROR_COUNTER_TIMEOUT — operacja szeregowa We-Wy została zakończona z powodu
przekroczenia limitu czasu (tzw. błąd przeterminowania).
Alternatywnym, dużo prostszym ale równie skutecznym sposobem powiadomienia nas o wystąpieniu ewentualnego błędu w trakcie transmisji jest zastosowanie bardzo prostej konstrukcji, w której programista przyjmuje, że wystąpienie jakiejś unikalnej pary znaków sygnalizować będzie niepowodzenie przy odczycie danych.
if (Number_Bytes_Read > 0)
{
// pokaż wynik odczytu
}
else
{
Beep();
Form1->RichEdit1->Text = "0x"; // błędna wartość pomiaru
}
Metodę taką z powodzeniem stosuje się na etapie projektowania i testowania aplikacji, gdzie koncentrujemy się głównie na sprawdzeniu poprawności zaprojektowanego algorytmu, zaś elegancja jego działania odgrywa mniej znaczącą rolę. Należy zwrócić uwagę, że wywołania tego typu komunikatów możemy z powodzeniem umieścić w odpowiednim miejscu funkcji Read_Comm(). Dzięki temu zyskamy nieco na długości kodu, ale, jak się wydaje, algorytmy stracą wówczas na przejrzystości.
Funkcję GetLastError() często stosujemy do sprawdzania rezultatu wykonania operacji otwarcia lub utworzenia pliku przeznaczonego do transferu. Jeżeli zechcemy mieć informację o tym, czy nowo otwarty plik nie jest już przypadkiem wykorzystywany przez inny trwający proces możemy posłużyć się następującą konstrukcją:
HANDLE hfile_s;
DWORD dwError;
LPVOID MsgBuf;
...
hfile_s = CreateFile(FileListBox1->FileName.c_str(), GENERIC_READ,
0, NULL, OPEN_EXISTING, 0, NULL);
...
switch (dwError = GetLastError())
{
case ERROR_SHARING_VIOLATION : {
FormatMessage(
FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM,
NULL, dwError, MAKELANGID(LANG_NEUTRAL,
SUBLANG_DEFAULT), (LPTSTR) &MsgBuf, 0, NULL );
MessageBox(NULL, (LPTSTR) MsgBuf, "Uwaga",
MB_OK|MB_ICONINFORMATION);
LocalFree(MsgBuf);
break;
}
...
}
Przy próbie otwarcia do transmisji pliku aktualnie wykorzystywanego przez inny program lub pliku będącego częścią macierzystej aplikacji na ekranie ujrzymy pomocną informację:
Rysunek 5.12. Informacja otrzymywana przez aplikację przy próbie otwarcia pliku wykorzystywanego przez inny trwający obecnie proces
|
|
Nie oznacza to oczywiście, że takiego zbioru nie będziemy już w stanie przetransmitować, niemniej jednak przy wszelkich operacjach na tego rodzaju plikach należy zachować ostrożność.
Podsumowanie
W podrozdziale omówiliśmy kolejne, podstawowe funkcje pomocne w procesie tworzenia oprogramowania komunikacyjnego. Czytając o nich poznaliśmy zarówno zalety jak i wady niektórych komponentów przy wyświetlaniu informacji odbieranych z portu szeregowego. Nabraliśmy też pewnych doświadczeń, projektując aplikacje przesyłające i odbierające pliki. Zapoznaliśmy się też z praktycznym wykorzystaniem komponentu TTimer, za pomocą którego możemy próbkować wybrany port szeregowy w poszukiwaniu nadchodzących danych. Większość wspomnianych tematów potraktowaliśmy w sposób dosyć ogólny, choć w miarę kompletny. Zauważyliśmy też zapewne, że w przyszłości coraz mniej uwagi będziemy zmuszeni poświęcać czasochłonnemu „drucikowaniu”, czyli łączeniu jakiś przewodów i domyślaniu się, który sygnał jest za co odpowiedzialny. Za to w coraz większym stopniu zaczyna absorbować nas sam proces tworzenia oprogramowania. Po przeczytaniu tego fragmentu książki nie powinno już stanowić dla nas problemu przesłanie poprzez interfejs RS 232C pojedynczego znaku, ciągu znaków czy nawet dosyć sporego pliku.
Ćwiczenia
1. Zmodyfikuj przedstawiony na wydruku 5.6 kod programu RS_05.cpp tak, aby korzystał jedynie z funkcji CreateFile() oraz ReadFile() przy przesyłaniu plików.
2. Zmodyfikuj przedstawiony na wydruku 5.7 kod programu RS_06.cpp tak, aby istniała możliwość przesyłania zaznaczonego (i ewentualnie skopiowanego do schowka) fragmentu pliku.
3. Zmodyfikuj przedstawiony na wydruku 5.8 kod programu RS_07.cpp tak, aby zapisywał na dysku cyklicznie odbierane dane. Postaraj się nie tracić informacji w momencie odłączenia przyrządu lub innego komputera.
Funkcje _lopen(), _lread(), _lwrite(), _lclose() niekiedy określa się mianem tzw. obsolete functions, czyli funkcji przestarzałych. Prostota ich użycia powoduje jednak, iż są często stosowane nie tylko przy operacjach dyskowych w aplikacjach obsługujących porty komunikacyjne. Między innymi z tego powodu zostały zachowane w Win32.
Pisząc w Delphi, możemy też skorzystać z wielce użytecznych procedur: BlockRead() oraz BlockWrite().
140 Część I ♦ Podstawy obsługi systemu WhizBang (Nagłówek strony)
H:\Książki\!Lukasz\RS 232. Praktyczne programowanie\4 po jezykowej\R_5_II.doc
119
Konstrukcja zdania uniemożliwia zrozumienie dopowiedzenia. O co tu chodzi? [RG] Urządzenie wymagało czy wymaga?