Electronic – arduino – How to interpret complex commands from serial with arduino

arduinoserial

In a project involving a serial communication and an Arduino I would like to use the serial interface to run multiple routines on the board.

The idea is to send a unique string with tags and values in order to execute several istructions at the same time. Let's say that we want to set the heading of an aircraft to 180°, the altitude to 3 meters and 20 cm from the ground and to maintain a horizontal profile with roll and pitch angle of 0°. The string would be:

X,heading,180,roll,0,pitch,0,altitude,3.20,X

For simplicity's sake I'll suppose to send a less complex string such as:

X,tag1,tag2,tag3,val3,X

X,Roll,Con,Kp,1.12,X

In order to receive and elaborate the string I've tried using 3 arrays of chars and a few counters. Here is the code:

   byte byteRead;
   // Store decimal numbers, determine decimal point
   double num1, num2;
   double complNum,counter;
   int numOfDec;
   boolean mySwitch=false;
   // Use a boolean var to enter in the command receiving mode. 
   // If you are interpreting several commands type this could be a way
   boolean cmplx = false;
   // arrays to store tags
   char opt1[3];
   char opt2[3];
   char opt3[2];  
   // Counters to determine tags
   int optCount=0,letterCount=0;

   void setup() 
   {                
     Serial.begin(9600);
     num1=0;
     num2=0;
     complNum=0;
     counter=1;
     numOfDec=0;
   }

   void loop() 
   {
     /*  check if data has been sent from the computer: */
     while (Serial.available()) 
     {
       /* read the most recent byte */
       byteRead = Serial.read();

       if (byteRead == 'X')
       {
         if (!cmplx)
         {
           // begin of the string
           cmplx = true;
         }
         else
         {
           // end of the string - reset values
           cmplx=false;
           optCount=0;
           /* Create the double from num1 and num2 */
           complNum=num1+(num2/(counter));
           /* Reset the variables for the next round */

           // Debug Stuff ignore it
           Serial.println();
           Serial.print("     opt1: ");
           Serial.print(opt1);
           Serial.print("     opt2: ");
           Serial.print(opt2);
           Serial.print("     opt3: ");
           Serial.print(opt3);
           Serial.print("    NUMBER: ");
           Serial.print(complNum);
           Serial.print("     letterCount: ");
           Serial.print(letterCount);
           Serial.print("    optCount: ");
           Serial.print(optCount);

           // How to reset arrays?
           opt1[0] = (char)0;
           opt2[0] = (char)0;
           opt3[0] = (char)0;
           num1=0;
           num2=0;
           complNum=0;
           counter=1;
           mySwitch=false;
           numOfDec=0;
         }
       }
       if (byteRead==44)
       {
         // Comma
         optCount++;
         // Debug stuff
         Serial.println();
         Serial.print("Virgola numero: ");
         Serial.println(optCount);
         letterCount = 0;
       }
       // Listen for a capital letter or a normal one
       if ((byteRead>=65 && byteRead<=90) || (byteRead>=97 && byteRead<=122))
       {
         // Debug stuff        
         Serial.println();
         Serial.print("lettera (Ascii value): ");
         Serial.print(byteRead);
         Serial.print("   ");
         if (cmplx)
         {
          if (optCount==1 && letterCount<=3)
          {
           opt1[letterCount] = byteRead;
           // Debug stuff
           Serial.print("letterCount: ");
           Serial.print(letterCount);
           Serial.print("opt1: ");
           Serial.print(opt1);
          }
          else if (optCount==2 && letterCount<=3)
          {
           opt2[letterCount] = byteRead;
           // Debug stuff
           Serial.print("letterCount: ");
           Serial.print(letterCount);
           Serial.print("opt2: ");
           Serial.print(opt2);
          }
          else if (optCount==3 && letterCount<=2)
          {
           opt3[letterCount] = byteRead;
           // Debug stuff
           Serial.print("letterCount: ");
           Serial.print(letterCount);
           Serial.print("opt3: ");
           Serial.print(opt3);
          }
          letterCount++;
         }
       }
       //listen for numbers between 0-9
       if(byteRead>47 && byteRead<58)
       {
          //number found
          if (cmplx)
          {
            /* If mySwitch is true, then populate the num1 variable
            otherwise populate the num2 variable*/
            if(!mySwitch)
            {
              num1=(num1*10)+(byteRead-48);
            }
            else
            {
              num2=(num2*10)+(byteRead-48);       
              // Counters used to correctly store decimal numbers
              counter=counter*10;
              numOfDec++;
            }
          }
       }
      // Looks for decimal points
      if (byteRead==46)
      {
          mySwitch=true;
      }
   }
 }

Once the opt1, opt2 and opt3 arrays are correctly populated I can compare them with tags and then call the respective routine.

The problem

I'm pretty close, the code stores the decimal numbers correctly but it doesn't with the arrays. The output I get inserting this string

X,Rol,Con,Kd,1.12,X

is the following:

lettera: 88   
virgola numero: 1

lettera: 82   letterCount: 0   opt1: R
lettera: 111   letterCount: 1   opt1: Ro
lettera: 108   letterCount: 2   opt1: Rol
virgola numero: 2

lettera: 67   letterCount: 0   opt2: C
lettera: 111   letterCount: 1   opt2: Co
lettera: 110   letterCount: 2   opt2: Con
virgola numero: 3

lettera: 75   letterCount: 0   opt3: K
lettera: 100   letterCount: 1   opt3: KdX,Rol,Con,Kd,1.12,X  <- WTF?
virgola numero: 4

virgola numero: 5

opt1: RolConKdX,Rol,Con,Kd,1.12,X   <- WTF?
opt2: ConKdX,Rol,Con,Kd,1.12,X  <- WTF? 
opt3: KdX,Rol,Con,Kd,1.12,X  <- WTF?
NUMBER: 1.12 
lettera: 88 

How to reset arrays of chars quickly and why do I get arrays completely messed up?

Best Answer

I really think you ought to step back and re think the idea of doing this with strings, and consider designing a binary protocol. In addition to better organizing your code, you will end up with something that is less prone to error, results in shorter transmissions (and more compact code), and will better lend itself to future enhancements. Backward compatibility will also be hugely easier! For example, you could create a some simple structures like the below, which then could be easily placed or recovered from a send or receive buffer, through the use of structure pointers.

    typedef struct mcTag {
       unsigned char srcAddr;
       unsigned char dstAddr;
       unsigned long version
       unsigned char numCmds;
       unsigned char hdrLength;
       unsigned char cmdLength;
       unsigned short totalLen
       unsigned short crc } MyControlHdr;

    typedef struct ctrTag {
      unsigned char cmd;
      long param1;
      long param2;
      long param3;
      long param4;   } MyCommand;

    unsigned char buffer[255];  // or worst case message size
    MyControlHdr * pCtrlHdr = (MyControlHdr *)(&buffer[0]);

Again the above is just to illustrate an idea. By defining a header to start every message, you can now add things like source and destination addresses. That one little addition could allow your project to easily send and receive from multiple senders, or at least differentiate between them. The header would be placed first in the communication buffer, and filled using the pCtrlHdr pointer, as easily as this...

pCtrlHdr->srcAddr = 1;
pCtrlHdr->dstAddr = 1;    // maybe you'll make 2555 a broadcast address?
pCtrlHdr->version = 1;    // possible way to let a receiver know a code version
pCtrlHdr->numCmds = 2;    // how many commands will be in the message
pCtrlHdr->hdrLength = sizeof(MyControlHdr );  // tells receiver where commands start
pCtrlHdr->cmdLength = sizeof(MyCommand );     // tells receiver size of each command 
   // include total length of entire message
pCtrlHdr->totalLen= sizeof(MyControlHdr ) + (sizeof(MyCommand) * pCtrlHdr->numCmds);

Then you could place your commands in the buffer (2 in this case) like this

MyCommand * pMyCmd = (MyCommand *)(&buffer[sizeof(MyControlHdr)]);
// first command in this message
pMyCmd->cmd = 3;      // might indicate heading
pMyCmd->param1 =100;  // various data, perhaps X,Y, and Z
pMyCmd->param2 =5;
pMyCmd->param3 =0
pMyCmd->param4 =0;

pMyCmd++;           // moves pointer to next command position in message
pMyCmd->cmd = 6;    // might indicate a speed directive
pMyCmd->param1 =8;  // various data, unused items left 0
pMyCmd->param2 =0;
pMyCmd->param3 =0
pMyCmd->param4 =0;

Finally, you might want to add a CRC of the entire message, back in the header. This of course would require a separate CRC calculation function.

pCtrlHdr->crc = 0;   // dummy temp value
pCtrlHdr->crc = calcCrc(buffer, pCtrlHdr->totalLen); // crc function

Now again this is just a simple example of what you might do with a binary protocol, and here are some of the advantages.

  1. The receiver can instantly test for the integrity of the whole message, by checking the transmitted CRC against a local calculation. CRCs are important! Serial drivers are notorious for dropping characters, turning a "100" into a "00"!!!. And, if you ever decide to migrate to radio, you can easily check for a bad message caused by interference.
  2. If you ever add anything to the header or command structure, and commit to always adding new items at the END of each structure, you can now write receiver code that can easily compensate for your version changes and remain compatible. And since the size of the headers and commands are included in the message, the receiver code can even compensate for expanded header and command sizes. The inclusion of an actual version is also helpful to detect when things change to the point where a compatibility issue might exist.
  3. All sizes and buffer positions in the code are now done by the compiler, reducing the risk of error.
  4. The biggest advantage is that this kind of protocol planning will make your code much easier to write, debug, and maintain.

On that last point, in my previous life I wrote a ton of communication drivers over the years to recover data and perform control over many devices, over serial, radio, and network links, with several other coders. When there was a device with a binary protocol to be dealt with, it was always a pleasure to write the driver. But when it was an ASCII protocol, consisting of strings and decimal numbers, we would all cringe. The lengths we would have to go through to account for every possible error would consume much too much time, be difficult to fully test, and in the end would often fall short and require endless fixes. And just as bad, since it was ASCII (presumably to make it easier for a human to read) any manufacturer upgrade or change to the device would always cause unforeseen bugs, breaking not only our own driver code but the manufacturer's as well! :-) A binary protocol, with a little planning, will make your project much more friendly to maintain, and you will definitely thank yourself for going the extra mile when the project expands, or goes to a radio link! :-)

Related Topic