serial parsing examples

Upload: alberto-yescas

Post on 19-Oct-2015

15 views

Category:

Documents


0 download

TRANSCRIPT

Serial parsing examples

Serial Parsing Examples.

When you are trying to receive data from a serial device, the techniques you use depend on the format of the data coming back. There are a few broad categories that most responses fall into. The following is a discussion of the most common type.

Unique delimiter, unique response:

This is the most common category, and the easiest to parse. This form is characterized by:

1. The response always ends in a particular character that cant appear anywhere else in the body of the message. Example terminators are Carriage Return ($0D, sometimes printed as ), STX (ASCII code 3), or some chosen ASCII letter.

2. There is enough information in each response to be able to correctly interpret it out of context. For example: if you could query the volume for a device, a response like vol=30 could be easily interpreted. A response like 30 doesnt say anything about volume, so interpreting it relies on knowing what question was asked.

With this form of response, the interpretation of responses can be treated as a completely separate process from sending the commands. We dont need to track what was sent because the responses can stand alone.

To extract the data from the response, first we need to make sure we have a whole command. It is possible, but not very common, to receive partial transmissions from a serial device. If incomplete data has been received, it should not be processed. We need to gather all the incoming data in a buffer, then look in that buffer for whole commands.

For this example, I will use the protocol for a Denon AVR-3806. It falls cleanly into this category, and the protocol is readily available at usa.denon.com.

Step 1 Create a variable to use as a buffer for incoming serial data

DEFINE_DEVICE

dvReceiver = 5001:1:0 // DENON AVR-3806 ON FIRST SERIAL PORT

dvTP

=10001:1:0 // G4 TOUCHPANEL

DEFINE_VARIABLE

CHAR cMyBuffer[200]

(* should be bigger than the largest response you could ever get*)

DEFINE_START

CREATE_BUFFER dvReceiver, cMyBuffer

Now any data that comes back from the receiver is stored in the variable cMyBuffer.

Step 2 Find and extract each whole command that is present in the buffer

We will need another variable to temporarily hold the new command.

DEFINE_VARIABLE

CHAR cMyBuffer[200]

(* should be bigger than the largest response you could ever get*)

CHAR cTemp[100]

(* should be bigger than any one response can be *)

Next, we can use the DATA_EVENT to let us know when new information has arrived.

DATA_EVENT[dvReceiver]

{

STRING:

{

/// THIS CODE RUNS WHEN NEW DATA COMES IN

/// THE INCOMING DATA IS STILL IN cMyBuffer

/// DATA.TEXT ONLY CONTAINS THE LAST MESSAGE OR FRAGMENT

}

}

Next, we look for the presence of a delimiter inside our buffer. Since its the last character in, it indicates that we have a whole message. If there is a message, we strip it out and place it into our cTemp variable.

DATA_EVENT[dvReceiver]

{

STRING:

{

/// THIS CODE RUNS WHEN NEW DATA COMES IN

/// THE INCOMING DATA IS STILL IN cMyBuffer

/// DATA.TEXT ONLY CONTAINS THE LAST MESSAGE OR FRAGMENT

WHILE( FIND_STRING(cMyBuffer,$0d,1))

{

cTemp = REMOVE_STRING(cMyBuffer,$0d,1)

// NOW cTemp HAS A SINGLE, WHOLE REPONSE

parseDenonResponse(cTemp) // WELL DO THIS NEXT

}

}

}

For the sake of clarity and modularity, well create a function call, parseDenonResponse(), to interpret the response. We could write the code to interpret the response right inside the DATA_EVENT. Adding the function call should improve the ability to read and troubleshoot the code.

Lastly, while we are editing the DATA_EVENT, its a good time for us to add the initialization for the serial port.

DATA_EVENT[dvReceiver]

{

ONLINE:

{

SEND_COMMAND dvReceiver,SET BAUD 9600,N,8,1 485 DISABLE

SEND_COMMAND dvReceiver,HSOFF

}

STRING:

{

/// THIS CODE RUNS WHEN NEW DATA COMES IN

/// THE INCOMING DATA IS STILL IN cMyBuffer

/// DATA.TEXT ONLY CONTAINS THE LAST MESSAGE OR FRAGMENT

WHILE( FIND_STRING(cMyBuffer,$0d,1))

{

cTemp = REMOVE_STRING(cMyBuffer,$0d,1)

// NOW cTemp HAS A SINGLE, WHOLE REPONSE

parseDenonResponse(cTemp) // WELL DO THIS NEXT

}

}

}

The manual for the AVR-3806 indicated that the baud rate was fixed at 9600 bps, with 8 data bits and one stop bit. Parity is not used. This is RS-232 communication, so the serial port needs to know not to use RS-485. The serial ports are all capable of RS-232, RS-422, and RS-485. Handshaking is also not necessary, since this is a 3 wire connection.

Step 3 Determine what kind of message has been sent as a response

In the previous step, we set up the need for a function call to be defined that has the task of interpreting the response we have received. To make the previous code work, we need to define the parseDenonResponse() function call.

DEFINE_FUNCTION parseDenonResponse(CHAR response[])

{

}

This creates the call, and lets it accept the character array that we planned to pass it in the previouse step.

All the messages have a carriage return to separate each one. Since we have only one command, we can get rid of the carriage return (it is no longer needed). Since it is always going to be in the last character of the response variable, we can just shorten the effective length of response by one:

DEFINE_FUNCTION parseDenonResponse(CHAR response[])

{

SET_LENGTH_STRING( response, LENGTH_STRING(response) - 1 )

/// ditch the carriage return by shortening response by 1

}

The statement SET_LENGTH_STRING( response, LENGTH_STRING(response)- 1 )

literally means set the effective length of response to its current length minus 1.

Next, we need to know what kind of responses the controlled equipment can send us. This information is crucial to the process of interpreting the information that comes back. In the protocol manual for the Denon AVR-3806, the responses are spelled out on pages 11-18. We need to know all the different kinds of responses that can come back, but we can choose to implement only the responses we care about for our program.

For our example, lets choose:

Power

Mute

Source Input

Surround Mode

Master Volume

For these status messages, we need to create some variables so that the controller will remember the current condition of the receiver. For the sake of readability and transportability, Ill use a STRUCTURE to store all the different facets of the receiver in one large variable.

DEFINE_TYPE

STRUCTURE _DenonInfo

{

INTEGER power

CHAR inputName[10]

INTEGER rawVolume

FLOAT displayVolume

INTEGER mute

CHAR mode[20]

}

DEFINE_VARIABLE

_DenonInfo DenonInfo /// create a variable of type _DenonInfo

For power, page 11 tells us that there are two kinds of power responses:

PWON AND PWSTANDBY.

Armed with this knowledge, we can test to see if our string contains these messages. Since we are testing for lots of different reponses, we can use a SELECT..ACTIVE block to perform multiple tests on our message. The same result could be achieved with multiple IF..ELSE IF statements. SELECT..ACTIVE adds some structure to the process, making troubleshooting easier.

To test if the response was a power status response, we use FIND_STRING to find these messages.

DEFINE_FUNCTION parseDenonResponse(CHAR response[])

{

SELECT

{

ACTIVE( FIND_STRING(response,PWON,1) ):

{

DenonInfo.power = 1

}

ACTIVE( FIND_STRING(response,PWSTANDBY,1) ):

{

DenonInfo.power = 0

}

/// ADD MORE ACTIVES TO TEST FOR MORE RESPONSES

}

}

Now the variable DenonInfo.power reflects the current power status of the unit. Since DenonInfo.power is a global variable, we can now use that information anywhere in the program that we need it.

Mute is also a simple response. We can handle it similarly to the Power Status.

DEFINE_FUNCTION parseDenonResponse(CHAR response[])

{

SELECT

{

ACTIVE( FIND_STRING(response,PWON,1) ):

{

DenonInfo.power = 1

}

ACTIVE( FIND_STRING(response,PWSTANDBY,1) ):

{

DenonInfo.power = 0

}

ACTIVE( FIND_STRING(response,MUON,1) ):

{

DenonInfo.mute = 1

}

ACTIVE( FIND_STRING(response,MUOFF,1) ):

{

DenonInfo.mute = 0

}

/// ADD MORE ACTIVES TO TEST FOR MORE RESPONSES

}

}

For the Power Status and Mute information, there were only a couple of messages to handle. For the next property, Source Input, there are more than a few possibilities. This will require I different technique to interpret the incoming data.

Instead of checking for each possible surround mode separately, we can search for the header. The SI should occur in the first two characters of the response. The information that follows it is the input name that we are looking for. Our job is to detect the SI header, then strip the input name out of the message.

DEFINE_FUNCTION parseDenonResponse(CHAR response[])

{

SELECT

{

ACTIVE( FIND_STRING(response,PWON,1) ):

{

DenonInfo.power = 1

}

ACTIVE( FIND_STRING(response,PWSTANDBY,1) ):

{

DenonInfo.power = 0

}

ACTIVE( FIND_STRING(response,MUON,1) ):

{

DenonInfo.mute = 1

}

ACTIVE( FIND_STRING(response,MUOFF,1) ):

{

DenonInfo.mute = 0

}

ACTIVE( LEFT_STRING(response,2) = SI):

{

REMOVE_STRING(response,SI,1) // DITCH THE SI

DenonInfo.inputName = response

}

/// ADD MORE ACTIVES TO TEST FOR MORE RESPONSES

}

}

LEFT_STRING non-destructively returns the first two characters from response. Our code looks at the result to see if it is SI, indicating that this is indeed a Source Input message. If it is, then the SI is stripped out of the response by the REMOVE_STRING operation, leaving only the name of the input.

Surround mode is very similar to Source Input and the same technique can be used.

DEFINE_FUNCTION parseDenonResponse(CHAR response[])

{

SELECT

{

ACTIVE( FIND_STRING(response,PWON,1) ):

{

DenonInfo.power = 1

}

ACTIVE( FIND_STRING(response,PWSTANDBY,1) ):

{

DenonInfo.power = 0

}

ACTIVE( FIND_STRING(response,MUON,1) ):

{

DenonInfo.mute = 1

}

ACTIVE( FIND_STRING(response,MUOFF,1) ):

{

DenonInfo.mute = 0

}

ACTIVE( LEFT_STRING(response,2) = SI):

{

REMOVE_STRING(response,SI,1) // DITCH THE SI

DenonInfo.inputName = response

}

ACTIVE( LEFT_STRING(response,2) = MS):

{

REMOVE_STRING(response,MS,1) // DITCH THE MS

DenonInfo.mode = response

}

/// ADD MORE ACTIVES TO TEST FOR MORE RESPONSES

}

}

The Master Volume response for the Denon AVR-3806 presents us with an interesting problem. The response can have 2 digits or 3, and needs to be interpreted differently for either case. The third digit is always 5, and indicates a half decibel step. Our code needs to take this into account. In this case, Ill choose to add a third digit of 0 and save the information uniformly as 3 digits

DEFINE_FUNCTION parseDenonResponse(CHAR response[])

{

SELECT

{

ACTIVE( FIND_STRING(response,PWON,1) ):

{

DenonInfo.power = 1

}

ACTIVE( FIND_STRING(response,PWSTANDBY,1) ):

{

DenonInfo.power = 0

}

ACTIVE( FIND_STRING(response,MUON,1) ):

{

DenonInfo.mute = 1

}

ACTIVE( FIND_STRING(response,MUOFF,1) ):

{

DenonInfo.mute = 0

}

ACTIVE( LEFT_STRING(response,2) = SI):

{

REMOVE_STRING(response,SI,1) // DITCH THE SI

DenonInfo.inputName = response

}

ACTIVE( LEFT_STRING(response,2) = MS):

{

REMOVE_STRING(response,MS,1) // DITCH THE MS

DenonInfo.mode = response

}

ACTIVE( FIND_STRING(response,MV,1) ):

{

REMOVE_STRING(response,MV,1) // STRIP HEADER

IF(LENGTH_STRING(response)=2) // HALF dB step

{

response = response,0 (* tack a zero

to the end of response*)

}

denonInfo.rawVolume = ATOI(RESPONSE)

// rawVolume now as a number from 0-990

calculateDisplayVolume() // more on this later

}

/// ADD MORE ACTIVES TO TEST FOR MORE RESPONSES

}

}

By adding a trailing zero to the incoming response, weve made it possible to interpret the data in a uniform fashion. One drawback to this technique is that the volume we are storing is not consistent with the documentation in the manual.

Next we can use the returned value to calculate what the real decibel value of the volume is. In the previous code, we created the need for a new function call by saying: calculateDisplayVolume(). Lets define it now.DEFINE_FUNCTION calculateDislplayVolume()

{

IF( denonInfo.rawVolume = 990)

{

/// this is a special case. It means off

denonInfo.displayVolume = -99.0 /// the point zero forces

/// a float to be used

}

ELSE /// THE NORMAL CASE

{

denonInfo.displayVolume = (denonInfo.rawVolume/10.0)-80.0

// the real dB value is the raw value minus 80

}

/// if we were displaying volume, this would be a good time.

}

The information that we received from the switcher is now available for use in the rest of the program, such as feedback in DEFINE_PROGRAM.

Here is the final result:

DEFINE_DEVICE

dvReceiver = 5001:1:0 // DENON AVR-3806 ON FIRST SERIAL PORT

dvTP

=10001:1:0 // G4 TOUCHPANEL

DEFINE_TYPE

STRUCTURE _DenonInfo

{

INTEGER power

CHAR inputName[10]

INTEGER rawVolume

FLOAT displayVolume

INTEGER mute

CHAR mode[20]

}

DEFINE_VARIABLE

CHAR cMyBuffer[200]

(* should be bigger than the largest response you could ever get*)

DEFINE_FUNCTION parseDenonResponse(CHAR response[])

{

SELECT

{

ACTIVE( FIND_STRING(response,PWON,1) ):

{

DenonInfo.power = 1

}

ACTIVE( FIND_STRING(response,PWSTANDBY,1) ):

{

DenonInfo.power = 0

}

ACTIVE( FIND_STRING(response,MUON,1) ):

{

DenonInfo.mute = 1

}

ACTIVE( FIND_STRING(response,MUOFF,1) ):

{

DenonInfo.mute = 0

}

ACTIVE( LEFT_STRING(response,2) = SI):

{

REMOVE_STRING(response,SI,1) // DITCH THE SI

DenonInfo.inputName = response

}

ACTIVE( LEFT_STRING(response,2) = MS):

{

REMOVE_STRING(response,MS,1) // DITCH THE MS

DenonInfo.mode = response

}

ACTIVE( FIND_STRING(response,MV,1) ):

{

REMOVE_STRING(response,MV,1) // STRIP HEADER

IF(LENGTH_STRING(response)=2) // HALF dB step

{

response = response,0 (* tack a zero

to the end of response*)

}

denonInfo.rawVolume = ATOI(RESPONSE)

// rawVolume now as a number from 0-990

calculateDisplayVolume() // more on this later

}

/// ADD MORE ACTIVES TO TEST FOR MORE RESPONSES

}

}

DEFINE_FUNCTION calculateDislplayVolume()

{

IF( denonInfo.rawVolume = 990)

{

/// this is a special case. It means off

denonInfo.displayVolume = -99.0 /// the point zero forces

/// a float to be used

}

ELSE /// THE NORMAL CASE

{

denonInfo.displayVolume = (denonInfo.rawVolume/10.0)-80.0

// the real dB value is the raw value minus 80

}

/// if we were displaying volume, this would be a good time.

}

DEFINE_START

CREATE_BUFFER dvReceiver, cMyBuffer

(* collect incoming msgs in cMybuffer *)

DEFINE_EVENT

DATA_EVENT[dvReceiver]

{

ONLINE:

{

SEND_COMMAND dvReceiver,SET BAUD 9600,N,8,1 485 DISABLE

SEND_COMMAND dvReceiver,HSOFF

}

STRING:

{

/// THIS CODE RUNS WHEN NEW DATA COMES IN

/// THE INCOMING DATA IS STILL IN cMyBuffer

/// DATA.TEXT ONLY CONTAINS THE LAST MESSAGE OR FRAGMENT

WHILE( FIND_STRING(cMyBuffer,$0d,1))

{

cTemp = REMOVE_STRING(cMyBuffer,$0d,1)

// NOW cTemp HAS A SINGLE, WHOLE REPONSE

parseDenonResponse(cTemp) // send to parser

}

}

}