Odczyt z portu COM

Juhas

Witam, jakiś czas temu popełniłem klasę usprawniającą odczyt z portu COM. Mam teraz chwilę, więc postanowiłem podzielić się tym kodem z Wami.

Nie będę go szczegółowo omawiał, gdyż pisałem to dość dawno i sam już dokładnie wszystkiego nie pamiętam.

Mój kod wygląda nieco inaczej, gdyż korzysta z innych moich unitów, ale to, co potrzebne starałem zawrzeć się w sekcji PRIVATE. Jeśli będą jakieś pytania, proszę na maila - pewnie odpowiem ;)

Wymagany do działania klasy jest moduł CPort, a więc komponent TComPort.

Całość wygląda następująco:

unit uComReader;

interface
uses Windows, SysUtils, CPort, Classes, forms;

//=============Czytnik=============\\
//==Klasa służąca do odczytu z portu COM==\\
//========autor: Adam Jachocki=========\\

type
  TCzytnikRead = procedure(Sender: TObject; Str: string) of object;
  TCzytnik = class(TObject)

  private
    FReadLength: integer;
    FReadString: string;
    FBuffLength: integer;
    ComPort: TComPort;
    CDP: TComDataPacket;
    FCzytnikRead: TCzytnikRead;
    FLastCzytnikRead: TCzytnikRead;
    FPrefix, FSuffix: string;
    FPort: string;
    FLengthToRead: integer;
    procedure SetPort(APort: string);
    procedure SetPrefix(APrefix: string);
    procedure SetSuffix(ASuffix: string);
    procedure OnPacket(Sender: TObject; const Str: string);
    procedure OnRxBuf(Sender: TObject; const Buffer; Count: Integer);
    procedure SetCzytnikRead(ACzytnikRead: TCzytnikRead);

    procedure DefaultCzytnikRead(Sender: TObject; Str: string);
    procedure SetLengthToRead(ALengthToRead: integer);

    function IsHex(str: string): boolean;
    function GetNonHex(str: string): string;
  public
    procedure Prepare;
    procedure Unprepare;
    procedure Open;
    procedure Close;

    property BufferLength: integer read FBuffLength write FBuffLength default 8;
    property Port: string read FPort write SetPort;
    property Prefix: string read FPrefix write SetPrefix;
    property Suffix: string read FSuffix write SetSuffix;
    property LengthToRead: integer read FLengthToRead write SetLengthToRead;

    property OnCzytnikRead: TCzytnikRead read FCzytnikRead write SetCzytnikRead;

    constructor Create(AOwner: TComponent; Prefix: string = ''; Suffix: string = '');
    destructor Destroy; override;
end;


implementation

{ TCzytnik }

//===Sprawdza, czy string ma znaki sterujące, czy nie====\\
function TCzytnik.IsHex(str: string): boolean;
var
  i: integer;
begin
  result:=false;
  for i:=1 to length(str) do
    if (ord(str[i])<20) or (ord(str[i])>126) then
    begin
      result:=true;
      exit;
    end;
end;

//=======Zwraca z łańcucha wszystko, co nie jest hexem=======\\
function TCzytnik.GetNonHex(str: string): string;
var
  i: integer;
begin
  for i:=1 to length(str) do
    if (ord(str[i])>=20) and (ord(str[i])<=126) then
    begin
      result:=result+str[i];
    end;
end;


procedure TCzytnik.Close;
begin
  ComPort.Close;
end;

constructor TCzytnik.Create(AOwner: TComponent; Prefix: string = ''; Suffix: string = '');
begin
//kilka moich ustawień domyślnych
  FBuffLength:=8;
  FLengthToRead:=0;
  FPrefix:=Prefix;
  FSuffix:=Suffix;

//tworzenie ComPort i ComDataPacket i odpowiednie ustawienie
  ComPort:=TComPort.Create(AOwner);
  CDP:=TComDataPacket.Create(AOwner);
  CDP.ComPort:=ComPort;
  ComPort.Events:=[evRxChar];
  ComPort.FlowControl.ControlDTR:=dtrEnable;
  ComPort.OnRxBuf:=OnRXBuf;
  CDP.OnPacket:=OnPacket;

  CDP.StartString:=FPrefix;
  CDP.StopString:=FSuffix;

  self.OnCzytnikRead:=DefaultCzytnikRead;
end;

destructor TCzytnik.Destroy;
begin
  ComPort.Connected:=false;
  ComPort.Free;
  CDP.Free;
  inherited;
end;


//================Przyjście pakietu danych===============\\
procedure TCzytnik.OnPacket(Sender: TObject; const Str: string);
var
  vStr: string;
begin
  vStr:=str;
  if isHex(vStr) then vStr:=GetNonHex(vstr);
  FReadString:=FReadString+vStr;
  if FLengthToRead>0 then
  begin
     if length(FReadString) = FLengthToRead then
     begin
       OnCzytnikRead(Self, FReadString);
       Prepare;
     end;
  end else
  begin
    OnCzytnikRead(Self, FReadString);
    Prepare;
  end;
end;

procedure TCzytnik.OnRxBuf(Sender: TObject; const Buffer; Count: Integer);
begin
//  FReadLength:=FReadLength+Count;
end;

procedure TCzytnik.Open;
begin
  ComPort.Open;
end;

procedure TCzytnik.Prepare;
begin
  FReadString:='';
  FReadLength:=0;
end;

procedure TCzytnik.SetLengthToRead(ALengthToRead: integer);
begin
  FLengthToRead:=ALengthToRead;
  if FLengthToRead>0 then
  begin
    FPrefix:='';
    FSuffix:='';
    CDP.StartString:='';
    CDP.StopString:='';
  end;
end;

procedure TCzytnik.SetPort(APort: string);
begin
  ComPort.Port:=APort;
  FPort:=APort;
  ComPort.Connected:=true;
  Prepare;
end;

procedure TCzytnik.SetSuffix(ASuffix: string);
begin
  FSuffix:=ASuffix;
  CDP.StopString:=ASuffix;
  FLengthToRead:=0;
end;

procedure TCzytnik.SetPrefix(APrefix: string);
begin
  FPrefix:=APrefix;
  CDP.StartString:=FPrefix;
  FLengthToRead:=0;
end;


procedure TCzytnik.Unprepare;
begin
  if @FLastCzytnikRead<>nil then Self.OnCzytnikRead:=FLastCzytnikRead else
     Self.OnCzytnikRead:=DefaultCzytnikRead;
{  if @FLastCzytnikRead<>nil then Self.FCzytnikRead:=FLastCzytnikRead else
     Self.FCzytnikRead:=DefaultCzytnikRead;}
end;

procedure TCzytnik.DefaultCzytnikRead(Sender: TObject; Str: string);
begin
  application.MessageBox('Zły moment odczytu', PChar(Application.Title), mb_OK+mb_IconExclamation);
end;


//===========Ustawianie procedury odczytu======================\\
procedure TCzytnik.SetCzytnikRead(ACzytnikRead: TCzytnikRead);
begin
  if @ACzytnikRead<>@FCzytnikRead then
  begin
    FLastCzytnikRead:=FCzytnikRead;
    FCzytnikRead:=ACzytnikRead;
  end;
end;

end.

Teraz mała prezentacja, jak się tego używa:

W unicie z formą eklarujemy procedurę TCzytnikRead, np:

procedure CzytnikRead(Sender: TObject; Str: String);

Jest to zupełnie obojętne, czy będzie to w sekcji private, public, czy protected.

Następnie trzeba stworzyć obiekt:

var
  Czytnik: TCzytnik;
begin
  Czytnik:=TCzytnik.Create(AOwner);
  Czytnik.Prefix:='(';
  Czytnik.Suffix:='('; //prefix i suffix można przekazać bezpośrednio w konstruktorze

  Czytnik.Port:='COM1';
  Czytnik.Prepare;
  Czytnik.OnCzytnikRead:=CzytnikRead;
end;

Następnie należy oprogramować procedurę CzytnikRead, tutaj jest już wszystko, czyli np:

procedure TForm1.CzytnikRead(Sender: TObject; Str: String);
begin
  showMessage('Przyszły dane: '+str);
end;

Procedura CzytnikRead jest wywoływana za każdym razem, gdy odczytano dane.

Teraz dość ważna rzecz.
Prawdopodobnie stworzycie tylko jedną instancję tej klasy na cały projekt(bo po co więcej? :))
Ale możliwe, że w projekcie w różnych oknach będziecie używali czytnika. Np. w oknie1 będzie on odczytywał cenę towaru, w oknie 2, będzie czytał kod kreskowy z karty klienta.

Podczas otwierania okna, w którym będzie używany czytnik należy obiektowi przypisać odpowiednią procedurę:

Czytnik.OnCzytnikRead:=CzytnikRead;

Natomiast podczas zamykania formy(gdy mamy pewność, że czytnik już w danym oknie nie zostanie wykorzystany) trzeba wykonać operację:

Czytnik.Unprepare;

Spowoduje to przypisanie automatycznie poprzedniej procedury.

Teraz co nieco i właściwościach i procedurach:

    procedure Prepare;

Procedura przygotowuje obiekt do działania. Powinna być wywołana zaraz po utworzeniu obiektu i ustawieniu mu portu, prefiksu i suffiksu. Czyli w sumie wystarczy tylko raz w instancji. Jeśli coś będzie nie tak przy odczycie w innych oknach, to można spróbować wywołać ją przed każdym przypisaniem procedury.

    procedure Unprepare;

Wyjaśnione wyżej ;)

    procedure Open;
    procedure Close;

Procedury do otwarcia i zamknięcia portu.

    property BufferLength

Długość bufora. Nie pamiętam za bardzo do czego to używam.

    property Port

Port, z którego ma czytać

    property Prefix
    property Suffix

Prefix i suffix :) Spójrzcie na uwagi na dole.

    property LengthToRead

Ilość danych do odczytania. Ustawiamy albo tą wartość, albo prefix i suffix - spójrzcie na uwagi na dole. Jeśli np. mamy 10 znakowy kod do odczytania, to ustawiamy na 10. UWAGA! Ustawiając właściwość LengthToRead MUSIMY wiedzieć, ile znaków ma przyjść ostatecznie przy jednym odczycie. Jeśli mając 10 znaków do odczytu ustawimy wartość na 5, przypisana procedura OnCzytnikRead powinna wywołać się 2 razy.

    property OnCzytnikRead

No to chyba już wiadomo :)

Uwagi na dole ;)
OK, co to prefix i suffix.
Kod powstał po to, aby łatwo mi było obsługiwać czytniki kodów kreskowych i pasków magnetycznych. Te urządzenia można tak zaprogramować(ale z tego co wiem, to nie wszystkie), że przesyłana informacja będzie zawarta pomiędzy prefiksem i suffiksem.
Jakby to jaśniej....
Np. informacja, którą odczytujemy z portu to: 12345
Jeśli prefix to '(', a suffix ')', tak naprawdę na port przyjdzie: (12345)
Oczywiście z tym sobie radzi ComPort.
Prefix i suffix mogą być ciągami wieloznakowymi. Ważne, żeby obiektowi przypisać takie wartości, jakie naprawdę są w czytniku.

Czytnik może w ogóle nie mieć ustawionego prefiksu, ani suffiksu. Dlatego powstała właściwość LengthToRead, która określa ilość danych do odczytania.
Jeśli kod, który mamy odczytać to: 123456, widać, że jego długość wynosi 6. I tak trzeba ustawić. Jeśli ustawimy np. na 3, wtedy procedura OnCzytnikRead wykonać się powinna dwukrotnie i przyjdą następujące dane:
Pierwszy raz: 123
Drugi raz: 456

Tak więc wpisując wartość LengthToRead MUSIMY znać długość danych. Jeśli wpiszemy ją źle, klasa będzie źle działać.

Z tego, co pamiętam, to miałem pewne problemy z tą wartością. Nie pamiętam już dokładnie o co chodziło i czy się z tym uporałem, ale pamiętam, że zawsze lepiej było mieć ustawiony na czytniku prefiks i suffiks(przypominam, że w obiekcie wpisujemy ALBO lengthToRead, ALBO prefix i suffix. Nigdy jednocześnie).

No, jakieś pytania to na maila.
Mam nadzieję, że nie zawiłem za bardzo ;)

Czujcie się wolni do poprawiania kodu i dalszego rozwoju, jeśli poczujecie taką ochotę ;) Tylko proszę o uprzedzenie mnie o tym fakcie.

Autorzy:
Juhas

2 komentarzy

Generalnie chodziło mi o to, żeby bezpośrednio nie posługiwać się ComPort i, żeby za pomocą jednej instancji można było obsłużyć wszystkie odczyty w programie, i, żeby odczytany ciąg był cały w jednej procedurze.

Jeśli miałbyś kilka odczytów w programie(tzn. np. w kilku oknach miałbyć do odczytania kod i miałbyś wykonać operacje związane jedynie z danym oknem), to musiałbyś dla każdego okna stworzyć TComPort albo nieźle się napocić. Ta klasa to bardzo ułatwia. Zresztą nie będę się tłumaczył. Zrobiłem coś, co potrzebowałem i używam. Uznałem, że się komuś może przydać(bo kilka osób mnie prosiło wcześniej, żebym to wysłał). Jak Ci się nie podoba, to przecież nikt Ci nie broni programować w WinApi :)

Yyy.. czy to jakiś żart? Już sama klasa ComPort wydaje się udziwnieniem, bo po co obudowywać w komponent funkcje WINAPI proste jak budowa cepa. Ale obudowywać klasą taki komponent to zdecydowanie przesada.