Etiket arşivi: TThread

Synapse ile Seri Port Arayüzü

Lazarus/Freepascal ile seri port kullanımına basit bir örnek olarak bu notları paylaşıyorum.
Aşağıdaki notları Lazarus ile yaptığım bir veri toplama ünitesi bağlantı programından özetledim.
Synapse kütüphanesi hakkında gerekli bilgilere şu bağlantıdan ulaşılabilir:
https://wiki.freepascal.org/Synapse

Synapse, kurulması gereken bir kütüphane değil. Kütüphane klasörünü bir yere olduğu gibi kopyalayıp klasörün içindeki laz_synapse.lpk kütüphane projesi dosyasını açmış ve derlemiş olmamız yeterli. (Package > Open Package File (.lpk) seçimi ile yapılıyor)

Bu kütüphaneyi kullanacak bir uygulama projesi başlattığında,
Project Inspector’de sol üstteki ADD butonuna ve sonra New Requirement’e tıkla.
(Project inspector görünür değilse Project > Project Inspector seçimi yap, bunun tuş kısayolu yok!)
Gelen listede laz_synapse seçilebilir olmalı, (çünkü derlemiştik) onu seç.
Project Inspector’de artık projenin Required Packages dalının altında laz_synapse da gözüküyor olmalı.
Artık, projenin seri port kullanması gereken ünitesinin uses kısmında synaser unit’ini ekleyebilirsin.
Bu ünite görünür bir form da içeriyorsa (ayarlar için içerse iyi olur) o formun private alanında programın ana mantığının bilmeye ihtiyaç duyacağı olayları tanımla:

 FonDeviceConnect : TNotifyEvent; 
 FonSpecificPacketArrived : TNotifyEvent; 

Olaylar, belli bir paketin gelmesi ya da belli bir işlemin tamamlanması gibi durumlara ait olabilirler. Kaç tane olacağı uygulamanın karmaşıklığına kalmış.

Ardından, gelen veriyi almak için bir dinleyici nesne yaz. Bu, doğal olarak bir TThread ardılı olacak:

   TCommListenThread = class(TThread)
    private
      fCom: TBlockSerial;
      fcmdTerminate: boolean;
      fPacketTimeout: integer;
      fErrorDesc: String;
      frxdata: AnsiString;
      rxstate: TRXState;
      rxstring: AnsiString;
      Frxsize: integer;

      FRXEvent : TNotifyEvent;
      FErrEvent: TNotifyEvent;

    public
      constructor Create(Acom: TBlockSerial); overload;
      procedure Start_Listening(poll_period: integer);
      procedure Stop_Listening;
      property onRX : TNotifyEvent read FRXEvent write FRXEvent;
      property onErr: TNotifyEvent read FErrEvent write FErrEvent;
      property ReceivedString: AnsiString read frxdata;
      property rxsize: integer read Frxsize;
    protected
       procedure Execute; override;
  end;


Overload ettiğimiz Create constructor’unda kullanıcıdan TBlockSerial tipli bir seri port erişim nesnesi vermesini istiyoruz. Bu, ünite içinde tanımlı bir değişken olmalı ve bu dinleme nesnesinden önce yaratılmış olmalı.
Thread’in execute’u içinde state’ler tanımlayarak veri gelirken anında bazı işler yapabiliriz ama seri portta buna çok gerek yok. O yüzden execute döngüsü şöyle olsa şimdilik yeterli:

procedure TCommListenThread.Execute;
begin
  while not(fcmdTerminate) do
  begin
    case rxstate of
    //////////////////////////////////////////////////////////////////////////
    RX_IDLE:
    begin

    end;
    //////////////////////////////////////////////////////////////////////////
    //////////////////////////////////////////////////////////////////////////
    RX_LISTEN:
    begin
      // gelen veriyi bekle:
      rxstring:= Fcom.RecvPacket(fPacketTimeout);
      if Fcom.LastError = ErrTimeout then continue;
      // hata döndüyse
      if Fcom.LastError <> sOK then
      begin
        rxstate:= RX_ERROR;
      end else
      begin
        // rxdata string'ini kullanıcıyla property olarak paylaştığımız
        // nesne alanına kopyala:
        Frxsize:= Length(rxstring);
        frxdata:= Copy(rxstring,0, Frxsize);
        if Assigned(onRX) then onRX(self);
      end;
    end;
    //////////////////////////////////////////////////////////////////////////
    //////////////////////////////////////////////////////////////////////////
    RX_ERROR:
    begin
      FErrorDesc:= Fcom.LastErrorDesc;
      Fcom.Flush;
      if Assigned(onErr) then onErr(self);
      rxstate:= RX_IDLE;
      self.Suspended:= true;
    end;
    //////////////////////////////////////////////////////////////////////////
    end;
  end;
 end;   

rxstring, içine com dosyasından veri gelmesini beklediğimiz temel değişken. Veri geldiğinde bunu frxdata ‘ya kopyalayıp dinleyiciyi serbest bırakıyoruz.
frxdata da ReceivedString property’si ile okunabilen bir değişken.
Böylece, onRX olayı ile tetiklenen bir fonksiyon ReceivedString’de gelen veriyi hazır bulacak.

Bir veri alındığında, dinleyici thread onRX olayını tetikleyecek. Bu olayı karşılayan bir procedure olmalı.
Bu procedure’ü yukarıda bahsettiğim, formun private alanında tanımlı bir fonksiyona bağla. Bu bağlama işini de
haberleşmeyi başlatmak (/ayarlamak) için kullanılan bir kullanıcı buton tıklamasının içine yaz. Burada nesneler de başlatılıyor olmalı:

  com:= TBlockSerial.Create;
  data_processor:= TCommListenThread.Create(com);
  data_processor.onRX:= @fcomm.Process_RXData;
  data_processor.onRX:= @commform.Process_RXData;
  data_processor.onErr:= @commform.Process_CommError;   

Process_RXData, seri porta bir veri geldiğinde, her ne gelirse gelsin çalışacak fonksiyondur.
Gelen veriyi data_processor thread’inin ReceivedString property’sinden okuyup rxdata dizisine (ünite değişkeni) yazarak işe başla:

  len:= Length(data_processor.ReceivedString);
  SetLength(rxdata, len);
  for i:=1 to len do
  begin
    x:= Byte(data_processor.ReceivedString[i]);
    rxdata[i-1]:= x;
  end;   

Buradan sonrası daha uygulamaya has kısım.
Paket tümleşikliğini (integrity) burada kontrol edip hata varsa işi sonlandırabilirsin.
Paket iyi gözüküyorsa beklediğin paketlerden hangisi olduğuna burada bakıp bir yönlendirme mantığı çalıştırabilirsin.
Başta yazmış olduğum
FonSpecificPacketArrived gibi olayları bu ayrımı yaptıktan sonra çağırabilirsin.
Bağlantı işleminin başında, bu olayların çağıracağı ana program procedure’lerini de bunlara bağlamış olmalısın (farklı ünitelerde çalışan farklı procedure’ler).

SendMessage() ile Uygulamalar Arası Veri Paylaşımı

Bazen, iki uygulama arasında veri alış verişi yapmam gerekiyor. Örneğin zamanında bir donanımla haberleşen bir program yazmışız ama sonra aynı donanımdan alınan veriyle başka bir şey yapan uygulamaya ihtiyacımız oluyor. Veya bir cihazdan gelen verileri kaydeden bir uygulamada radikal bir değişiklik isteniyor ve söz gelimi önceden binary dosyaya yaptığımız kaydı artık bir veritabanına yapmamız gerekiyor gibi.
Burada da belki örneğini vereceğim bir başka örnek durumda da, bazen bir programı denemek için normalde gerçek dünyadan gelecek olan verileri bilgisayarda simüle eden bir şeyler yazıyorum.

Uygulama hali hazırda dışarıdan aldığı verileri TCP ya da UDP üzerinden alıyorsa yerel sunucu/istemci üzerinden yine TCP/UDP haberleşmesi yapmak en basit yöntem. Ama daha genel bir uygulamalar arası veri iletişimi çözümü olarak Windows’un mesajlaşma mekanizmasını kullanmayı tercih ediyorum.

Windows’ta süreçlerin birbirleriyle asenkron olarak haberleşmesi için bir mesajlaşma yapısı var. Çeşitli olayları yanıtlayan nesneler yazarken bunları zaten kullanıyoruz. Ben özellikle TThread sınıfından türettiğim bazı veri işleme nesnelerinde mesajları veri giriş çıkışı için kullanıyorum. Bu mesajlaşma mekanizması aynı zamada uygulamalar arasında da veri iletişimi için kullanılabilir. Bunun için ben WM_COPYDATA mesajını gönderiyor/alıyorum:

WM_COPYDATA mesajının msg alanında TCopyDataStruct diye bir veri türü var. Mesaj içeriğinin aktarımını temelde bu record ayarlıyor.
Gönderici kısımda bu veri türünden bir değişkeni doldurup SENDMESSAGE( ) ile yayınlıyorum.

copyData: TCopyDataStruct;
// dataexchange ünitesindeki copyData değişkenini dolduruyorum:
dataexchange.copyData.dwData:= $0325;   // packet type identifier
dataexchange.copyData.cbData:= event_exchange.Size;
dataexchange.copyData.lpData:= event_exchange.Memory;

Windows.pas içinde tanımlı TCopyDataStruct diye bir tip var. Bu tipten bir değişken tanımlamam gerekiyor: copyData
Bunun dwData alanında mesajla iletilen veri tipini belirtiyorum.
cbData alanında da verilen pointer’ın işaretlediği verinin boyunu belirtiyorum.
lpData alanında da payload işaretçisini paylaşıyorum.
Bu arada, uygulama tarafındaki event_exchange nesnesi de bir TMemoryStream. Yollamak istediğim veriyi buraya yüklüyorum. Genel bir tanımlama olması için bir stream üzerinde çalışmayı uygun buldum. Yerel bir değişken tipi de tanımlayabilirdik.

SendMessage( ) ‘ı kullanmadan önce, hedeflediğimiz alıcı pencerenin o anda canlı olup olmadığına bakabiliriz. Bunun için bir başka windows api’si var:
FINDWINDOW( )
Bu fonksiyonun parametrelerinden söz etmem gerek: Harici bir uygulamanın penceresine erişmek için (buna Delphi’de form diyoruz) bize iki parametre gerekiyor. Biri hedef formun sınıf türü, diğeri de pencere (form) adı. FindWindow() api’si bu iki parametreyi istiyor ve bize dönüş olarak handle sayısını döndürüyor. Alıcı uygulama hangi platformda hazırlanmışsa, o platformun win api tanımını bilip bu iki değeri PChar olarak verebiliyor olmamız gerekir.

SendMessage( ) fonksiyonunun parametrelerinden biri FindWindow ile bulunan receiver_handle. sender_handle büyük ihtimalle gönderici ana formunun handle’ı olur. Daha sonra veri göndermek tek bir fonksiyon çağrısına kalır:

res := SENDMESSAGE(receiverHandle, WM_COPYDATA, Integer(Handle), Integer(@copyData));

Alıcı tarafta yapılması gereken, gönderici tarafta “alıcı form” olarak işaretlenen formun üstünde tanımlanmış bir mesaj yakalama procedure’ü tanımlamaktır:

 procedure WMCopyData(var Msg : TWMCopyData) ; message WM_COPYDATA;

WM_COPYDATA mesajını yakalaması için yazdığımız fonksiyona bir değişken parametre aktarmalıyız ki bu, Msg diye tanımladığım bir TWMCopyData tipinde bir kayıttır. Bu kayıt tipinin .copyDataStruct alanı göndericinin oluşturduğu copyData değişkeninin ta kendisidir!

  // gönderici handle numarası:
  sender:= Msg.From;
  // mesajın data içeriğinin dwData alanını mesaj tipini belirtmede kullanıyorum:
  packet_type:= Msg.CopyDataStruct.dwData;
  packet_size:= Msg.CopyDataStruct.cbData;
  if packet_type = MSG_EVENT_BUFFER then
  begin
   // paket işaretçisini cardinal tipli bir pointer'a eşliyorum:
    pc:= Msg.CopyDataStruct.lpData;  
    inc(pc);
    // ardından "asıl" payload geliyor (burada packet_size'a bakmak gerekebilir)
    pEvent:= PEventRec(pc);
    Process_Event_Data( pEvent , packet_size );
  end;

Yukarıdaki koddan anlayabileceğiniz gibi, gönderici uygulamanın bize gönderdiği payload’u pEvent diye bir işaretçiyle alıyor ve onu işleyen procedure’e veriyorum. En başta cardinal tipli bir sayaç verisi var, onu burada alıyorum. Payload birden çok Event datası içerebilir, o yüzden packet_size değerini de procedure’e veriyorum.

Bir arayüz simülasyonunda bu çalışmanın hayata geçmiş halini şuradan görebilirsiniz. Event receiver ve Event Simulator birbirinden bağımsız iki uygulama. Simulator ile geliştirmekte olduğum kullanıcı arayüzü ve kontrol birimini istediğim türde veriler ile besleyebiliyorum.