How to build specialized data scanners using the OnTriggerAvail event.

 

Sometimes you need to look for data packets that just cannot be described using the TApdDataPacket component.  In these cases, it is easier to build a custom data packet scanner on the OnTriggerAvail event.

There are a couple of scenarios where custom scanners should be used.  The first is where the ending string of the data packet can show up before the actual end of the data.  In this case, an escape character to indicate this is not really the end of the data packet usually prefixes the ending string.

Escaped characters in the ending string

This example will look for a data packet that starts with ‘>’ and ends with ‘<’.  However, both the starting string and the ending string can be prefixed with a backslash to indicate a literal ‘>’ or ‘<’ instead of the packet start and stop strings.

The easiest and most flexible way to code a custom data scanner is to use a state machine.  First declare the states.  For this example, the only states are “Not in a packet” (psNone) and “In a packet” (psGettingData).  This will be expanded upon in the next example.

For Delphi the code is:

type

  TPacket1State = (psNone,

                   psGettingData);

For C++ Builder this would be:

typedef enum {psNone, psGettingData} TPacket1State;

Next, declare some variables to keep track of what is going on.  FState is the current state of the scanner.  Buffer will hold the data packet.  BufferPos is the position to add a character to the buffer.  When an entire data packet is found, this will contain the length of the packet.  Finally, LastChar is used to keep track of the last character read.  This is used to determine if the current character is escaped or not.

In Delphi this would be:

    FState : TPacket1State;

    Buffer : array [0..4096] of Char;

    BufferPos : Integer;

    LastChar : Char;

And C++ Builder:

    TPacket1State FState;

    char Buffer[4096];

    int BufferPos;

    char LastChar;

Next, initialize the variables. 

Delphi:

procedure TForm1.FormCreate(Sender: TObject);

begin

  FState := psNone;

  BufferPos := 0;

  LastChar := #$00;

end;

C++ Builder:

void __fastcall TForm1::FormCreate(TObject *Sender)

{

  FState = psNone;

  BufferPos = 0;

  LastChar = 0x00;

}

Finally, implement the OnTriggerAvail event.  This event will loop through all characters that have been read.

If the starting string of the data packet has been found and it is time to collect the data packet, the data will be added to the buffer.  The state machine will then look at the current and the last character and toggle between collecting data and ignoring it.

procedure TForm1.ApdWinsockPort2TriggerAvail(CP: TObject; Count: Word);

var

  i : Integer;

  c : Char;

 

begin

  // Get all of the characters out of the buffer.

  for i := 1 to Count do begin

    c := ApdWinsockPort2.GetChar;

 

    // If data is being collected, then add it to the buffer.

    if FState <> psNone then begin

      Buffer[BufferPos] := c;

      Inc (BufferPos);

    end else

      BufferPos := 0;

 

    case FState of

      psNone :

        // Not inside of data packet.  Check for the starting character.

        if (c = '>') and (LastChar <> '\') then begin

          // Manually insert the first character

          BufferPos := 0;

          Buffer[BufferPos] := c;

          Inc (BufferPos);

          FState := psGettingData;

        end;

 

      psGettingData :

        // The starting condition has been met.  Add data to the buffer and

        // look for the ending condition.

        if (c = '<') and (LastChar <> '\') then begin

          // Add a 0 to the end so the buffer can be used like a PChar

          Buffer[BufferPos] := #$0;

          // Packet found !!!!!

          // Do your OnPacket processing here.

          FState := psNone;

        end;

    end;

    LastChar := c;

  end;

end;

And C++ Builder:

void __fastcall TForm1::ApdWinsockPort2TriggerAvail(TObject *CP,

      WORD Count)

{

  // Get all of the characters out of the buffer.

  for (int i = 1; i <= Count; i++) {

    char c = ApdWinsockPort2->GetChar();

 

    // If data is being collected, then add it to the buffer.

    if (FState != psNone)

      Buffer[BufferPos++] = c;

    else

      BufferPos = 0;

 

    switch (FState) {

      case psNone :

        // Not inside of data packet.  Check for the starting character.

        if (c == '>' && LastChar != '\\') {

          // Manually insert the first character

          BufferPos = 0;

          Buffer[BufferPos++] = c;

          FState = psGettingData;

        }

        break;

 

      case psGettingData :

        // The starting condition has been met.  Add data to the buffer and

        // look for the first character of the ending condition.

        if (c == '<' && LastChar != '\\') {

          // Add a 0 to the end so the buffer can be used like a PChar

          Buffer[BufferPos] = 0x00;

          // Packet found !!!!!

          // Do your OnPacket processing here.

          FState = psNone;

        }

    }

    LastChar = c;

  }

}

Unusual wildcards

The second scenario where custom data scanners are useful is the case in which unusual wildcard characters are used.  In this example a data packet that starts with a ‘!’ and ends with ‘$??$03’ will be implemented.  The ‘?’ matches any character that is not $10 (0x10).

As with the previous example, declare the types, variables and initialize them.  This is similar to the last example, except there is quite a few more states.

For Delphi this is something like:

type

  TPacket1State = (psNone,

                   psGettingData,

                   psEnd1,

                   psEnd2,

                   psEnd3,

                   psEnd4,

                   psEnd5);

    FState : TPacket1State;

    Buffer : array [0..4096] of Char;

    BufferPos : Integer;

procedure TForm1.FormCreate(Sender: TObject);

begin

  FState := psNone;

  BufferPos := 0;

end;

And for C++ Builder it is:

typedef enum { psNone, psGettingData, psEnd1, psEnd2, psEnd3, psEnd4,

               psEnd5 } TPacket1State;

        TPacket1State FState;

        char Buffer[4096];

        int BufferPos;

void __fastcall TForm1::FormCreate(TObject *Sender)

{

  FState = psNone;

  BufferPos = 0;

}

Implement the state machine on the OnTriggerAvail event.  The ‘!’ will start the packet.  The ending of the packet is a bit more complicated.  The psEnd1, psEnd2, psEnd3, psEnd4 and psEnd5 states will check for each of the characters needed to end the packet.  As long as each expected character is found in sequence, the state machine will progress.  If anything seems out of the ordinary, the state machine will go back to collecting data.

For Delphi:

procedure TForm1.ApdWinsockPort2TriggerAvail(CP: TObject; Count: Word);

{

  This example shows using the OnTriggerAvail event to match a data packet

  that starts with a '!' and ends with the string '$??$03'.  The ? can be

  any character except for a $10.

 

  Excluding characters in a wildcard is something that the TApdDataPacket

  component cannot do, so instead, a custom scanner using a state machine

  will be used instead.

}

var

  i : Integer;

  c : Char;

 

begin

  // Get all of the characters out of the buffer.

  for i := 1 to Count do begin

    c := ApdWinsockPort2.GetChar;

 

    // If data is being collected, then add it to the buffer.

    if FState <> psNone then begin

      Buffer[BufferPos] := c;

      Inc (BufferPos);

    end else

      BufferPos := 0;

 

    case FState of

      psNone :

        // Not inside of data packet.  Check for the starting character.

        if c = '!' then begin

          // Manually insert the first character

          BufferPos := 0;

          Buffer[BufferPos] := c;

          Inc (BufferPos);

          FState := psGettingData;

        end;

 

      psGettingData :

        // The starting condition has been met.  Add data to the buffer and

        // look for the first character of the ending condition.

        if c = '$' then

          FState := psEnd1;

 

      psEnd1 :

        // The first character of the ending condition has been met.  Now

        // check for characters that are not $10.  If it fails, go back to

        // collecting data.

        if c <> #$10 then

          FState := psEnd2

        else

          FState := psGettingData;

 

      psEnd2 :

        // Check for the second character that is not $10

        if c <> #$10 then

          FState := psEnd3

        else

          FState := psGettingData;

 

      psEnd3 :

        // check for the $ that follows the two wildcards

        if c = '$' then

          FState := psEnd4

        else

          FState := psGettingData;

 

      psEnd4 :

        // Check for the 0 following the $

        if c = '0' then

          FState := psEnd5

        else

          FState := psGettingData;

 

      psEnd5 :

        // Check for the final 3.  If it is found, do the processing for the

        // data packet.  The data will be in Buffer with a size of BufferPos.

        begin

          if c = '3' then begin

            // Add a 0 to the end so the buffer can be used like a PChar

            Buffer[BufferPos] := #$0;

            // Packet found !!!!!

            // Do your OnPacket processing here.

          end;

          FState := psNone;

        end;

    end;

  end;

end;

And C++ Builder:

void __fastcall TForm1::ApdWinsockPort2TriggerAvail(TObject *CP,

      WORD Count)

{

/*

  This example shows using the OnTriggerAvail event to match a data packet

  that starts with a '!' and ends with the string '$??$03'.  The ? can be

  any character except for a $10.

 

  Excluding characters in a wildcard is something that the TApdDataPacket

  component cannot do, so instead, a custom scanner using a state machine

  will be used instead.

*/

  // Get all of the characters out of the buffer.

  for (int i = 1; i <= Count; i++) {

    char c = ApdWinsockPort2->GetChar();

 

    // If data is being collected, then add it to the buffer.

    if (FState != psNone)

      Buffer[BufferPos++] = c;

    else

      BufferPos = 0;

 

    switch (FState) {

      case psNone :

        // Not inside of data packet.  Check for the starting character.

        if (c == '!') {

          // Manually insert the first character

          BufferPos = 0;

          Buffer[BufferPos++] = c;

          FState = psGettingData;

        }

        break;

 

      case psGettingData :

        // The starting condition has been met.  Add data to the buffer and

        // look for the first character of the ending condition.

        if (c == '$')

          FState = psEnd1;

        break;

 

      case psEnd1 :

        // The first character of the ending condition has been met.  Now

        // check for characters that are not $10.  If it fails, go back to

        // collecting data.

        if (c != 0x10)

          FState = psEnd2;

        else

          FState = psGettingData;

        break;

 

      case psEnd2 :

        // Check for the second character that is not $10

        if (c != 0x10)

          FState = psEnd3;

        else

          FState = psGettingData;

        break;

 

      case psEnd3 :

        // check for the $ that follows the two wildcards

        if (c == '$')

          FState = psEnd4;

        else

          FState = psGettingData;

        break;

 

      case psEnd4 :

        // Check for the 0 following the $

        if (c == '0')

          FState = psEnd5;

        else

          FState = psGettingData;

        break;

 

      case psEnd5 :

        // Check for the final 3.  If it is found, do the processing for the

        // data packet.  The data will be in Buffer with a size of BufferPos.

        if (c == '3') {

          // Add a 0 to the end so the buffer can be used like a PChar

          Buffer[BufferPos] = 0x00;

          // Packet found !!!!!

          // Do your OnPacket processing here.

        }

        FState = psNone;

        break;

    }

  }

}

What to do when the packet has been found

Once the data packet has been found, the data is placed in the Buffer variable, but needing further processing.

Chances are the processing that needs to be done will be to intensive to be implemented directly in the OnTriggerAvail method.  In these cases, posting a message to a custom message handler is the way to go.  Control will be returned back to the APRO dispatcher, and the event handler is free to do whatever it needs to.

In Delphi this is pretty easy to do.

First declare a constant for the custom message.

const

  Am_MyMessage  = WM_USER + $301;

Next, declare the method that will handle the message:

    procedure MyMessageHandler (var Msg : TMessage); message Am_MyMessage;

In the OnTriggerAvail event, post a message to this handler.  So the message handler can make use of the data, allocate a block of memory to contain the data.  Pass this new block of memory and the length to the message.  The message handler will free it up.  The buffer needs to copied to a block because the next time that the OnTriggerAvail event fires, the buffer will be reset.  In a lot of cases, the message handler will not be done processing the buffer when this happens.

var

  WorkBuffer : Pointer;

 

// When you have a good packet, do the following…

          GetMem (WorkBuffer, BufferPos);

          Move (Buffer, WorkBuffer^, BufferPos);

          PostMessage (Handle, Am_MyMessage, BufferPos,

                       Integer (WorkBuffer));

Finally, implement the message handler.  The message handler will extract the data from message, do whatever processing needs to be done and then will free up the memory that was allocated when the message was posted.  In this example, the message handler will add the data collected by the data scanner to a memo.

procedure TForm1.MyMessageHandler (var Msg : TMessage);

begin

  Memo1.Lines.Add ('Packet! [' + PChar (Msg.lParam) + '] Len: ' +

                   IntToStr (Msg.wParam));

  FreeMem (Pointer (Msg.lParam));

end;

In C++ Builder, this is all a bit trickier.  Declaring custom message types is harder in C++ Builder than in Delphi.

First, declare the constant for the custom message:

#define Am_MyMessage WM_USER + 0x300

Next, declare the method that will handle the message.

        void __fastcall MyMessageHandler(TMessage& Message);

In the public section of your form, add a message map to map the custom message to the handler.

public:           // User declarations

        __fastcall TForm1(TComponent* Owner);

 

  BEGIN_MESSAGE_MAP

    MESSAGE_HANDLER(Am_MyMessage, TMessage, MyMessageHandler)

  END_MESSAGE_MAP(TForm)

};

In the OnTriggerAvail event, post the custom message to the handle of the form.  To be able to access the buffer, memory is allocated and the buffer is copied into it.

          char *WorkBuffer = new char[BufferPos];

          memcpy (WorkBuffer, Buffer, BufferPos);

          PostMessage (Handle, Am_MyMessage, BufferPos, (int) WorkBuffer);

Finally, in the custom message handler, process the data and free up the memory allocated in the OnTriggerAvail event.  In this example, the data trigger is displayed on a memo.

void __fastcall TForm1::MyMessageHandler (TMessage& Message)

{

  Memo1->Lines->Add ("Packet! [" + AnsiString((char *) Message.LParam) + "] Len: " +

                     IntToStr (Message.WParam));

  delete[] (char *) Message.LParam;

}

Conclusion

This technique can be expanded on in many ways.  Data packets that require a string to appear somewhere in the middle of the packet can be implemented.  Data packets with multiple start and stop strings can be implemented.  In circumstances where the normal TApdDataPacket cannot be used to find the data, this is the next best thing.

This site is not affiliated, endorsed, or otherwise associated with the entity formerly known as TurboPower Software. The owners and maintainers of Aprozilla.com were merely meager employees of the aforementioned organization, providing this site out of the pure goodness of their collective hearts. SourceForge.net Logo

Last updated: July 22, 2003.