c++serial-portdevice

Read Bytes off a Serial Device (and make sense of them??)


I'm pulling my hair out trying to figure out how to read bytes off a serial device, check a checksum, and then convert them into something that I can actually read.

I have a device which "should" be sending me various messages, each started with the byte $83 and ended with the byte $84. The second to last byte is supposedly a checksum, generated by XORign all the other values together and comparing.

The actual values coming back should be alphanumeric, but I can't make heads or tail of the data. I'm newish to C++ - I'm sure that's not helping.

I've read several guides on serial programming, but I'm lost.

Can anyone help me, link me, or show me how to read bytes off a serial device, watch for $83 and $84, and then make sense of the data in between?

Here is the format of each message:

$FF byte Destination Address
$10 byte Message Length 16 Bytes
$37 byte Message Type
$00 byte Message subtype
BankAngle int -179 to +180
PitchAngle int -90 to +90
YawAngle int -179 to +180
Slip sint -50 to +50
GForce fps 0 to 6G
MISC byte Mode bits
Heading word 0 to 359 
N/A not used
Voltage byte input voltage

This is all coming off an MGL SP-4 AHRS, and for ease of use I am targeting a Linux system, specifically Ubuntu. I am using the GCC compiler end the Eclipse CDT for development.

Where I'm lost I can read the data into a buffer, but then I'm not versed enough in C++ to make sense of it after that, since it's not ASCII. I'm interested in learning what I need to know, but I don't know what I need to know.

I have a Perl / Java background.


Solution

  • Accomplishing this is going to be wholly dependent on the Operating System and platform that you target. Since the device you mention is mounted internally to an aircraft in the general use-case, I will assume you are not targeting a Windows platform, but more likely a Linux or embedded system. There are a number of resources available for performing serial I/O on such platforms (for example: the Serial Programming HOW-TO) that you should look at. Additionally, as suggested in the device's Installation Manual (available here about halfway down the page), you should "Consult the SP-4 OEM manual for message formats and message type selection." I suspect you will obtain the most relevant and useful information from that document. You may want to check if the manufacturer provides an API for your platform, as that would negate the need for you to implement the actual communication routine.

    As far as making sense of the data, once you can read bytes from your serial interface, you can leverage structs and unions to make accessing your data more programmer-friendly. For the rough message outline you provided, something like this might be appropriate:

    struct _message
    {
        uint8_t  DestinationAddress;
        uint8_t  MessageLength;
        uint8_t  MessageType;
        uint8_t  MessageSubtype;
        int32_t  BankAngle; //assuming an int is 32 bits
        int32_t  PitchAngle;
        int32_t  YawAngle;
        sint_t   Slip; //not sure what a 'sint' is
        fps_t    GForce; //likewise 'fps'
        uint8_t  MISC;
        uint16_t Heading; //assuming a word is 16 bits
        uint8_t  Unused[UNUSED_BYTES]; //however many there are
        uintt_t  Voltage;
    }
    
    struct myMessage
    {
        union
        {
            char raw[MAX_MESSAGE_SIZE]; //sizeof(largest possible message)
            struct _message message;
        }
    }
    

    This way, if you were to declare struct myMessage serialData;, you can read your message into serialData.raw, and then conveniently access its members (e.g. serialData.message.DestinationAddress).

    Edit: In response to your edit, I'll provide an example of how to make sense of your data. This example supposes there is only one message type you have to worry about, but it can be easily extended to other types.

    struct myMessage serialData;
    memcpy(serialData.raw, serialDataBuffer, MAX_MESSAGE_SIZE); //copy data from your buffer
    if(serialData.message.MessageType == SOME_MESSAGE_TYPE)
    {
        //you have usable data here.
        printf("I am a SOME_MESSAGE!\n");
    }
    

    Now, supposing that these integral types are really only useful for data transmission, you need to translate these bits into "usable data". Say one of these fields is actually an encoded floating-point number. One common scheme is to select a bit-weight (sometimes also called resolution). I don't know if this is directly applicable to your device, or if it is what the real values are, but let's say for the sake of discussion, that the YawAngle field had a resolution of 0.00014 degrees/bit. To translate the value in your message (serialData.message.YawAngle) from its uint32_t value to a double, for example, you might do this:

    double YawAngleValue = 0.00014 * serialData.message.YawAngle;
    

    ...and that's about it. The OEM manual should tell you how the data is encoded, and you should be able to work out how to decode it from there.

    Now, let's say you've got two message types to handle. The one I've already shown you, and a theoretical CRITICAL_BITS message. To add that type using the scheme I've laid out, you would first define the CRITICAL_BITS structure (perhaps as follows):

    struct _critical_bits
    {
        uint8_t  DestinationAddress;
        uint8_t  MessageLength;
        uint8_t  MessageType;
        uint8_t  MessageSubtype;
        uint32_t SomeCriticalData;
    }
    

    ...and then add it to the struct myMessage definition like so:

    struct myMessage
    {
        union
        {
            char raw[MAX_MESSAGE_SIZE]; //sizeof(largest possible message)
            struct _message message;
            struct _critical_bits critical_message;
        }
    }
    

    ...then you can access the SomeCriticalData just like the other fields.

    if(serialData.message.MessageType == CRITICAL_MESSAGE_TYPE)
    {
        uint32_t critical_bits = serialData.critical_message.SomeCriticalData;
    }
    

    You can find a little more information on how this works by reading about structs. Bear in mind, that instances of the struct myMessage type will only ever contain one set of meaningful data at a time. Put more simply, if serialData contains CRITICAL_MESSAGE_TYPE data, then the data in serialData.critical_message is valid, but serialData.message is not --even though the language does not prevent you from accessing that data if you request it.

    Edit: One more example; to calculate the checksum of a message, using the algorithm you've specified, you would probably want something like this (assuming you already know the message is completely within the buffer):

    uint8_t calculate_checksum(struct myMessage *data)
    {
        uint8_t number_bytes = data->message.MessageLength;
        uint8_t checksum = 0;
        int i;
        for(i=0; i<number_bytes; ++i)
        {
            //this performs a XOR with checksum and the byte
            //in the message at offset i
            checksum ^= data->raw[i];
        }
        return checksum;
    }
    

    You might need to adjust that function for bytes that aren't included, check to make sure that data != NULL, etc. but it should get you started.