Servo control

I posted this in the Storage section a few days ago because at the time I was trying to figure out if what I wanted to do would work with an SD Card. The answer was yes. So now I'm moving on to the actual programming aspect of the same thing. The idea is as follows:

I have two servos that will function as a pan and tilt system. I'm going to cue them with music. I have several cue points during the audio piece where I need the servos to do something. Taking a page from a similar broadcast system, I thought I could have a single text file with commands listed in it like so:

angle_pan   speed_pan   angle_tilt   speed_tilt   pause
       90           2           90            2       5
       45           4           90            0       2
      135           4           90            0       5

Which I would read in as:

  • move the pan servo to 90 degrees with speed = 2, move the tilt servo to 90 degrees with speed = 2, wait for 5 seconds.
  • after 5 seconds, move the pan servo to 45 degrees with speed = 4, leave the tilt servo at 90 degrees, wait for 2 seconds.
  • after 2 seconds, move the pan servo to 135 degrees with speed = 4, leave the tilt servo at 90 degrees, wait 5 seconds.
    Etc., etc. (speed will be somewhere between 2 (fast) and 10 (slow))

The controller's code will then open the SD card, and read the file in bit by bit as it needs to and move the servos as needed.

I choose this method primarily because then I don't have to do anything with the actual code on the controller, just the text file that's on the SD card. The controller simply reads it in as needed. When I need to change what the servos do, I simply update the SD card with a new file (for a different piece of music), and off I go.

My question is, is this the best way of doing this. Will I have to content with a lot of lag between the time it takes for the data to be read from the SD card each time? I don't plan on reading the whole thing in at the beginning of the program, it will be too big to fit in memory. So I'll constantly be reading from the card and pass instructions to the servos.

Does anyone have suggestions on how to improve this system, or even come up with a different design perhaps? I suppose I can always add a DataFlash chip on the controller board. The controller then reads the whole file from SD card into memory for faster access. I don't know if that would really make much of a difference though.

Ideas/suggestions anyone?

sub'ing to keep up with the thread. (cool idea)..

much like creating a web app.. and then just editing the .xml file to make changes..etc

anyways.. maybe to get rid of the overhead of accessing the SD card..reading, parsing date.....etc..

maybe read in a 'few' line/variables at a time.. maybe the first 10 to give you a bit of a 'buffer' for smoother/continuous movement..?

How many records will there be?

You can't alter how the SD read is done. It will read 512 byte chunks from the file, or less if there is not that much data. If all the values are byte sized, you could create a binary file, with 5 bytes per record (no cr/lf needed). That would allow for 102 records before another disk read is performed.

The proper thing to do,though is perform a timing test. See if the read will be fast enough.

xl97:
anyways.. maybe to get rid of the overhead of accessing the SD card..reading, parsing date.....etc..

maybe read in a 'few' line/variables at a time.. maybe the first 10 to give you a bit of a 'buffer' for smoother/continuous movement..?

Where would you be reading those 'few' lines/variables at a time from? Thin air? They're coming off of the SD card.

PaulS:
How many records will there be?

You can't alter how the SD read is done. It will read 512 byte chunks from the file, or less if there is not that much data. If all the values are byte sized, you could create a binary file, with 5 bytes per record (no cr/lf needed). That would allow for 102 records before another disk read is performed.

The proper thing to do,though is perform a timing test. See if the read will be fast enough.

How many records, not entirely sure yet. It's a 6 minute show, and the servos will be doing "stuff" the whole time. At the most, they'll be receiving a new instruction every second. For the moment, the instruction that needs to get parsed is what I've shown above, 5 parameters per record. Two of them will have a number between 0 and 180, two of them will have a number between 0 and 10, and the last one is 0 to 30 ... maybe larger, but no more than 3 digits.

Can I fit it all in 512 bytes? Just a random set of instructions, sending both servos into sweep mode and changing every 2-5 seconds, and I can get about 35 instructions, one per line (cr/lf at the end), in a TEXT file. I want to stay away from having to create a binary file. The idea is that anyone can enter their instructions in an Excel file and export the sheet as a CSV file.

I'm going to need more than 35 instructions, that's for sure. :slight_smile:

I want to stay away from having to create a binary file. The idea is that anyone can enter their instructions in an Excel file and export the sheet as a CSV file.

Like I said, you should run a test. Create a file that contains more than 512 bytes. Call micros() before and after each read(). Compute the difference, and write it to the serial port. The 1st and the 513th read should take significantly longer than the 2nd, 3rd, etc. reads. Whether that actual time is significant, or not, only you can decide.

One thing you will notice is that parsing and using the text data is going to take significantly longer than reading and using binary data would.

It might be worth the effort to develop a csv to bin converter to create the binary file. Only testing will tell.

Agreed, testing is definitely needed.

While futzing around last night, learning the SD library and what not, I came up with this little snippet:

#include <SD.h>

File dataFile;
int done = 0;
char c;
String data;

void setup() {
  Serial.begin(9600);
  Serial.println("Initializing SD card...");
  pinMode(10, OUTPUT);
   
  if (!SD.begin(4)) {
    Serial.println("initialization failed!");
    return;
  }
  Serial.println("initialization done.");
}

void loop() {
  dataFile = SD.open("data.txt", FILE_READ);
  
  if (!done) {
    if (dataFile) {
      while (dataFile.available()) {
        c = dataFile.read();
        switch(c) {
          case ',':
            Serial.print(data);
            Serial.print(" - ");
            data = "";
            break;
          case '\n':
            Serial.println(data);
            /*
              Process instructions here ...
              Break up into {angle_pan (INT), speed_pan (INT), angle_tilt (INT), speed_tilt (INT), pause (INT)}
            */
            Serial.println("new line detected!");
            data = "";
            break;
          default:
            data = data + String(c);
            break;
        }
      }
      done = 1;
      dataFile.close();
      Serial.println();
      Serial.println("Done reading file.");
    } else {
      Serial.println("error opening data.txt");
    }
  }
}

The data.txt file looks like this:

90,2,90,2,5
45,4,135,4,2
135,4,45,4,2
90,2,90,2,5

The sketch does what I'm expecting it to do, it prints out (I'm not at my workbench, so I'm writing from memory here)

90 - 2 - 90 - 5
new line detected!
45 - 4 - 135 - 4 - 2
new line detected!
etc., etc.

Now, I did that as an exercise, see if I could figure out the parsing and all of that. Great. However, what I need are INTs, not Strings. Because I need to pass that data on to the Servo library. And this is where I wish I had paid more attention, converting from one to the other.

If you notice, in my sketch I wrote myself a little comment of where I need to parse the full instruction line. I don't know how to actually do that. Ideally it's just an array with the various values in it which I will then pass on to the servo function to do its thing.

Some help will be much appreciated here.

Now, I did that as an exercise, see if I could figure out the parsing and all of that. Great. However, what I need are INTs, not Strings.

A couple corrections. What you need are ints, not strings. Ditch the String class. You will run out of memory that way.

A fixed array of chars that you store c in (and increment an index and add a NULL terminator to) will, overall, use less memory. Each time you encounter a comma, call atoi() to convert the value to an int, and reset index (and put a NULL in the 0th position of the array).

PaulS:
A couple corrections. What you need are ints, not strings. Ditch the String class. You will run out of memory that way.

A fixed array of chars that you store c in (and increment an index and add a NULL terminator to) will, overall, use less memory. Each time you encounter a comma, call atoi() to convert the value to an int, and reset index (and put a NULL in the 0th position of the array).

While I understand what you're explaining to me, the whole process of converting from one to the other is elusive to me. The fixed array, would that be for the 5 elements I will eventually end up with, or is that for each character read to be stored in prior to converting to INT?

The fixed array, would that be for the 5 elements I will eventually end up with, or is that for each character read to be stored in prior to converting to INT?

Could be either one. Depends on whether you want to read a whole record, then parse it, and convert each token to an int, or whether you want to read and parse in one step, as you are doing now.

PaulS:
Could be either one. Depends on whether you want to read a whole record, then parse it, and convert each token to an int, or whether you want to read and parse in one step, as you are doing now.

Probably the first method, read till I get a new line, then parse the whole thing. Unless you can think of a reason why I should be doing it one at a time. In the end, I'd like to just pass all the arguments on to the servo function as one piece (at least, I think I can.) So something like myServos(arg1, arg2, arg3, arg4). The pause argument is being used by the main loop itself, not the servo function.

Probably the first method, read till I get a new line, then parse the whole thing.

Something like this, then:

char buffer[40]; // Hold a whole record (may be larger than needed
byte index;

void loop()
{
   dataFile = SD.open("data.txt", FILE_READ);
   if (dataFile)
   {
      index = 0;
      buffer[index] = '\0';

      while (dataFile.available())
      {
         c = dataFile.read();
         if(c == '\n' || c == '\r')
         {
            parseBuffer();
            index = 0;
            buffer[index] = '\0';
         }
         else if(index < 39)
         {
            buffer[index++} = c;
            buffer[index] = '\0';
         }
      }
   }
}

void parseBuffer()
{
   if(strlen(buffer) > 0)
   {
       int vals[] = {0,0,0,0,0};
       byte index = 0;
       char *token = strtok(buffer, ",");
       while(token)
       {
          vals[index] = atoi(token);
          token = strtok(NULL, ",");
       }

       // Use the values here...
   }
}

Hrm, getting something odd here

#include <SD.h>

File dataFile;
int done = 0;
char c;
char buffer[4];
byte index;

void setup() {
  Serial.begin(9600);
  Serial.println("Initializing SD card...");
  pinMode(10, OUTPUT);
   
  if (!SD.begin(4)) {
    Serial.println("initialization failed!");
    return;
  }
  Serial.println("initialization done.");
}

void loop() {
  dataFile = SD.open("data.txt", FILE_READ);
  
  if (!done) {
    if (dataFile) {
      index = 0;
      buffer[index] = '\0';
      
      while (dataFile.available()) {
        c = dataFile.read();
        if (c == '\n' || c == '\r') {
          parseBuffer();
          index = 0;
          buffer[index] = '\0';
        } else if (index < 39) {
          buffer[index++] = c;
          buffer[index] = '\0';
        }
      }
      done = 1;
      dataFile.close();
      Serial.println();
      Serial.println("Done reading file.");
    } else {
      Serial.println("error opening data.txt");
    }
  }
}

void parseBuffer() {
  if (strlen(buffer) > 0) {
    int vals[] = {0, 0, 0, 0, 0};
    byte index = 0;
    char *token = strtok(buffer, ",");
    while (token) {
      vals[index] = atoi(token);
      token = strtok(NULL, ",");
    }
    // print vals
    for (int i = 0; i < 5; i++) {
      Serial.print(vals[i]);
      Serial.print(" - ");
    }
    Serial.println();
  }
}

Data file contains:

90,2,90,2,5
45,4,135,4,2
135,4,45,4,2
90,2,90,2,5

The ouputs the following to the serial monitor:

Initializing SD card...
initialization done.
5 - 0 - 0 - 0 - 0 -
5 - 0 - 0 - 0 - 0 -
5 - 0 - 0 - 0 - 0 -
5 - 0 - 0 - 0 - 0 -

Done reading file.

On a side note, why can't I use 'i < sizeof(vals)' ? When I try that, I get a different output:

Initializing SD card...
initialization done.
5 - 0 - 0 - 0 - 0 - 2268 - 0 - 1297 - -1534 - 397 -
5 - 0 - 0 - 0 - 0 - 2268 - 0 - 1297 - -1534 - 397 -
5 - 0 - 0 - 0 - 0 - 2268 - 0 - 1297 - -1534 - 397 -
5 - 0 - 0 - 0 - 0 - 2268 - 0 - 1297 - -1534 - 397 -

Done reading file.

This probably isn't right

token = strtok(NULL, ",");

edit, try this:

void parseBuffer() {
  if (strlen(buffer) > 0) {
    int vals[] = {0, 0, 0, 0, 0};
    byte index = 0;
    char *token = strtok(buffer, ",");
    while (token) {
      vals[index++] = atoi(token);
      token = strtok(buffer, ",");
    }
    // print vals
    for (int i = 0; i < 5; i++) {
      Serial.print(vals[i]);
      Serial.print(" - ");
    }
    Serial.println();
  }
}

@remiss
Yes, it is. Using NULL as the first argument tells strtok() to keep parsing the same string. Your modification will have strtok() start parsing the original string again, which will result in an endless loop.

On a side note, why can't I use 'i < sizeof(vals)' ? When I try that, I get a different output:

vals is an array of ints. There are 5 ints in the array, so that is a total of 10 bytes, so sizeof(vals) returns 10.

sizeof(vals)/sizeof(vals[0]) would return the number of elements in the array.

As to your overall problem, there could be an issue with the reading of the file, or there could be an issue with the parsing. Add:

Serial.print("String to parse: [");
Serial.print(buffer);
Serial.println("]");

to the top of parseBuffer(). This will isolate the error to either the reading or the parsing.

char c;
char buffer[4];
byte index;

buffer is WAY too small. I suggested a size of 40, not 4!

After fixing the buffer size ... (how'd I miss that) ... and adding the Serial.print/println lines, it looks like this:

void parseBuffer() {
  Serial.print("String to parse: [");
  Serial.print(buffer);
  Serial.println("]");
  if (strlen(buffer) > 0) {
    int vals[] = {0, 0, 0, 0, 0};
    byte index = 0;
    char *token = strtok(buffer, ",");
    while (token) {
      vals[index] = atoi(token);
      token = strtok(NULL, ",");
    }
    // print vals
    for (int i = 0; i < 5; i++) {
      Serial.print(vals[i]);
      Serial.print(" - ");
    }
    Serial.println();
  }
}

And the result is this:

Initializing SD card...
initialization done.
String to parse: [90,2,90,2,5]
5 - 0 - 0 - 0 - 0 - 
String to parse: []
String to parse: [45,4,135,2,5]
5 - 0 - 0 - 0 - 0 - 
String to parse: []
String to parse: [135,2,45,4,5]
5 - 0 - 0 - 0 - 0 - 
String to parse: []
String to parse: [90,2,90,2,5]
5 - 0 - 0 - 0 - 0 - 
String to parse: []

Done reading file.

Right, sorry about that.. But index++ will be needed won't it?

Ta-ahaaa, I forgot to increment index, you're right. With index++ inside of the while loop, it works. Now on to step umpteen ...

Normally, I'd have to go through and assign each variable, one by one:

data[] = {90, 2, 90, 2, 5};
angle_pan = data[0];
speed_pan = data[1];
angle_tilt = data[2];
speed_tilt = data[3];
pause = data[4];

Is there a way to something like this? (this is PHP code code by the way)

$data = Array(90, 2, 90, 2, 5);
list($angle_pan, $speed_pan, $angle_tilt, $speed_tilt, $pause) = $data;

That assigns all the values to the various variables in one line.

Is there a way to something like this?

No, not natively. You could create a function to do that, though.