Notifications
Clear all

REVISIT: Using SD Cards with Arduino - Record Servo Motor Movements

58 Posts
6 Users
7 Reactions
5,811 Views
Will
 Will
(@will)
Member
Joined: 4 years ago
Posts: 2594
 

@inq @hinobot

I understand what you're saying and if the devices were extremely fast, then I'd agree. But we're talking cheap servos which are slow. If we imagine your example of multiple heads, consider the difficulty of simulating simultaneity of turning them while adjusting lips, jaw openings, eye rolls and so on.

Even with reading and interpreting one line at a time it'll still be slow, but at least the cumulative effects of all movements will be correct every "tick" microseconds. "tick" will, of course, depend on the number of servos, their speed and the relative distance each has to move.

A superior (although much more difficult) approach would be to manage each pot to collect and store (for each servo) an angular displacement and an elapsed time for that movement. The playback would then read that combination (ID, change in angle, time for change) and then update the position of the servo by moving change/time x loop time. That would be beneficial in keeping all of the movement relatively in synch and cut down on data storage as well. But it'd be hell to implement 🙂

Anything seems possible when you don't know what you're talking about.


   
ReplyQuote
Will
 Will
(@will)
Member
Joined: 4 years ago
Posts: 2594
 
Posted by: @inq
  1. I'd just wrap it up in a simple C++ class for one servo.  From then on you simply add servos to your heart's delight and no changes to file format or to the main sketch's logic.

But as I mentioned... there are as many opinions here as there are people.  It is totally what makes the most sense to @hinobot and his daughters.  Some people think procedurally.  Fewer people (I have to admit) thing Object Oriented.

I agree completely about handling it (either way) with a class.

But, as I understand it, hinobot and his daughters aren't even C programmers (yet :)) and I think this project will be more difficult than they imagined in the first place. I don't want to scare them off !

Anything seems possible when you don't know what you're talking about.


   
ReplyQuote
robotBuilder
(@robotbuilder)
Member
Joined: 6 years ago
Posts: 2264
 

@hinobot 

I don't have any attachment to the code posted.  The original code posted by DroneBot Workshop 2019 works perfect for my needs, one to record, the other to play back.  Its just fine, I just need to add the ability to capture 4 pots and 4 servos, and play them back at the same rate recorded.

I have ordered the sd card module so when it arrives I will give it a go and maybe in the meantime figure out how to modify Bill's code to use 4 servos but it may take time and you may have is all working before then 🙂

 

 


   
HinoBot reacted
ReplyQuote
HinoBot
(@hinobot)
Member
Joined: 3 years ago
Posts: 6
Topic starter  

@robotbuilder Thank you, I got all the servos working with modifying the original code, the rub is when we try to record each channel on the SD card, and then as @bill mentions, there is no time code during the recording, so it unceremoniously goes through the entire movements very fast (on one channel). 


   
ReplyQuote
robotBuilder
(@robotbuilder)
Member
Joined: 6 years ago
Posts: 2264
 

@hinobot 

You can use millis() for timing.

For play you can read the card and update the next positions in fixed intervals of time.

For record you can read the pots at the same fixed time intervals and save the positions on the card.

 

unsigned long startMillis;  //some global variables available anywhere in the program
unsigned long currentMillis;
const unsigned long period = 1000;  //the value is a number of milliseconds

void setup()
{
  Serial.begin(115200);  //start Serial in case we need to print debugging info
  // other setup stuff here
  startMillis = millis();  //initial start time
}

void loop()
{
  currentMillis = millis();  //get the current "time" (actually the number of milliseconds since the program started)
  if (currentMillis - startMillis >= period)  //test whether the period has elapsed
  {
    // ======================     DO YOU THING HERE  ================================
    //                        READ and UPDATE POSITIONS
    // ============================================================================
    startMillis = currentMillis;  //IMPORTANT to save the start time.
  }
}

 

 


   
ReplyQuote
Will
 Will
(@will)
Member
Joined: 4 years ago
Posts: 2594
 

@robotbuilder @hinobot

It would probably be better to use a button to signify that you're finished adjusting all the pots (especially when you add more). That would allow you plenty of time to carefully adjust each pot (and therefore servo) to just the right place. When the button is pushed, those positions can be collected, converted to a string and then written out to the SD card very quickly.

Anything seems possible when you don't know what you're talking about.


   
ReplyQuote
robotBuilder
(@robotbuilder)
Member
Joined: 6 years ago
Posts: 2264
 

@hinobot

@will
It would probably be better to use a button to signify that you're finished adjusting all the pots (especially when you add more).

Ways to synchronize movements (a kinetic melody) I think is a major consideration when recording them. Also the ability to edit the movements. Usually with animatronics you have synchronized speech or other lights and sounds.

I probably wouldn't record movements rather generate them from code as my interest is robotics. This could also have sensors to react to anyone that comes into the room to make those eyes and head follow people. Much spookier if the machine reacts to stimuli 🙂

Here is an example from James Button for smoother movements.

It would help to own a 3d printer and be able to design your own parts.

 


   
Inq reacted
ReplyQuote
Inq
 Inq
(@inq)
Member
Joined: 3 years ago
Posts: 1904
 
Posted by: @will

I understand what you're saying and if the devices were extremely fast, then I'd agree. But we're talking cheap servos which are slow.

I'm not sure I understand your point.  In my opinion, them being slow REQUIRES you to have a timing value for each for when you read the analog pin connected to each potentiometer and writing it to a file and then writing to the servo to actually move it.  If you use one timestamp for potentially dozens of pots/servos, by the time you write the last one it might be significant fractions of a second.  When played back they will not be in sync visually.

@hinobot - I haven't looked at Bill's or your code, but I took Will's concern to actually try it out.  The following two sketches do a recording / playback.  

RECORDING - RalfRec.ino

  • You can have as many servos/pots as you want.  I only had two servos and only one potentiometer laying around to test with.  I used the same pot for both servos in the code below.  You'll obviously want to change the second A0 pin.  I marked the code with "SERVOS" to indicate the places where you need to change the code for 4 or 50 servos.
  • It's not very friendly in that the recorder starts recording as soon as it boots up.
  • There is a provision for a stop button to stop the recording.  
  • It doesn't have any provisions for making multiple recordings.
  • Its currently set up to record at about 60 Hz.
  • I've never messed with an SD card.  I used an ESP8266 MPU and it has a 3MB file system available.  I just used that.  It can store about 55 minutes of recording as it is set up now.  You'll have to convert the file loading over to the SD.  It shouldn't be that different as all these file systems are based on the C/C++ versions of file handling. 
#include <Servo.h>
#include <LittleFS.h>

// To change the number of servos, change at the 
// places marked with "SERVOS".  These must be consistent 
// with those written in Ralf.ino

struct Position
{
    u32 timestamp;
    u8 servo;
    u8 angle;
};

// GLOBAL VARIABLES
// File object to access data in recorded file.
File _file;
// Start Time for playing the recording.
u32 _start = 0;
// How fast you want to sample
const u32 INTERVAL = 17;      // 17 milliseconds is ~59 Hz.
// Last interval occurred.
u32 _last;
// Stop recording pin
const u8 pinStop = D2;

// Your servos... doesn't matter if it's one or a hundred.  Totally dependent
// on what your MPU can handle hardware wise.
const u8 CNT = 2;           // SERVOS
Servo servos[CNT]; 
u8 pots[CNT];

// This is number of positions that can be stored.  On an ESP8266 having a
// file system size of ~3MB, and using our ~59 Hz and having 2 servos, we can
// store about 55 minutes of recording.
u32 maxPos;
u32 wrotePos = 0;

void setup() 
{
    // So we can debug stuff.
    Serial.begin(115200);
    Serial.flush();
    Serial.printf("\nRalph Recorder is recording!\n");

    // SERVOS
    // Attach as many pins to your servos as you have.
    // Make sure the number you attach is consistent with CNT.
    servos[0].attach(D3);
    servos[1].attach(D4);
    
    // SERVOS
    // Set the corresponding analog pins for each servo.
    // Make sure the number you configure is consistent with CNT.
    // ESP8266 only has one analog pin, have to use the same one.
    pots[0] = A0;
    pots[1] = A0;  
    
    // Open the file system.
    if (!LittleFS.begin()) 
    {
        Serial.printf("Unable to begin(), aborting\n");
        return;
    }
    // Determine how many samples we can store in our file.
    FSInfo info;
    LittleFS.info(info);
    maxPos = info.totalBytes / sizeof(Position);
    Serial.printf("Can store %u samples\n", maxPos);

    // Open up file to recorded.  Overwrites old file.
    _file = LittleFS.open("/EyeMovement.rec", "w");
    if (!_file) 
    {
        Serial.printf("Unable to open file for writing, aborting\n");
        return;
    }

    // Setup switch to stop the recording.
    pinMode(pinStop, INPUT_PULLUP);

    // We're just going to start recording on boot-up.  You'll want to trigger
    // the start variable with some button press???
    _start = millis();
    
    // The last variable is the time that we took a snapshot of all the servos.
    _last = _start;    
}

void loop() 
{    
    u32 now = millis();
    if ((wrotePos < maxPos) && (now - _last > INTERVAL))
    {
        // We only get here on the period we've set.
        // First check to see if the stop button is pressed.
        if (!digitalRead(pinStop))
        {
            // Stop the recording.
            Serial.println("Recording stopped.");
            Serial.printf("Stored %u data points\nApproximately %.1f seconds\n",
                wrotePos, (float)wrotePos / CNT * INTERVAL / 1000);
            maxPos = 0;     // This keeps it from recording any more.
            _file.close();  // Close the file... ready for Playing.
            return;
        }
        
        // Read every pot, write the postion and move the servo.
        for(u8 i=0; i<CNT; i++)
        {
            // Read each pot.
            int analog = analogRead(pots[i]);

            // ESP8266 analog pin A0, ranges 0 to 1024.
            // We want to convert this to 0 to 180 degrees.
            analog = map(analog, 0, 1024, 0, 180);
            
            // Create our position object and write it to the file
            Position pos = { millis(), i, analog };
            _file.write((u8*) &pos, sizeof(pos));
            // Decrement how many we can store.
            wrotePos++;
            
            // Write to the servo
            servos[i].write(analog);
        }
        // update our last interval variable.
        _last = now;
    }
}

PLAYBACK - Ralf.ino

  • Again... You can have as many servos/pots as you want.  I marked the code with "SERVOS" to indicate the places where you need to change the code for 4 or 50 servos.  Obviously, it has to be set up with the same number as the RalfRec version.
  • It's not very friendly in that the playback starts as soon as it boots up.
  • There is a provision for it to loop the playback.  I know it works as it has been driving me crazy replaying the same routine over and over as I've written this.
#include <Servo.h>
#include <LittleFS.h>

// To change the number of servos, change at the 
// places marked with "SERVOS".  These must be consistent 
// with those written in RalfRec.ino

struct Position
{
    u32 timestamp;
    u8 servo;
    u8 angle;
};

// GLOBAL VARIABLES
// Do we want to repeat the file.
bool _repeat = true;
// File object to access data in recorded file.
File _file;
// Current position waiting for time to move.
Position _current;
// Start Time for playing the recording.
u32 _start = 0;

// Your servos... doesn't matter if it's one or a hundred.  Totally dependent
// on what your MPU can handle hardware wise.
const u8 CNT = 2;       // SERVOS
Servo servos[CNT]; 

void setup() 
{
    // So we can debug stuff.
    Serial.begin(115200);
    Serial.flush();
    Serial.printf("\nRalph is here!\n");

    // SERVOS
    // Attach as many pins to your servos as you have.
    // Make sure the number you attach is consistent with CNT.
    servos[0].attach(D3);
    servos[1].attach(D4);
    
    // Open the file system.
    if (!LittleFS.begin()) 
    {
        Serial.printf("Unable to begin(), aborting\n");
        return;
    }

    // Open up pre-recorded _file for the positions.
    _file = LittleFS.open("/EyeMovement.rec", "r");
    if (!_file) 
    {
        Serial.printf("Unable to open file for reading, aborting\n");
        return;
    }
    
    u32 pos = _file.size() / sizeof(Position);
    Serial.printf("File has %u data points\nApproximately %.1f seconds\n",
        pos, (float)pos / CNT * 17 / 1000);

    // Read the first position in the file.
    readNext();
    
    // We're just going to start playing on boot-up.  You'll want to trigger
    // the start variable with some button press???
    _start = millis();
}

void loop() 
{    
    if (!_file)        
        return;     // We have no data to process.
    
    u32 timeSinceStart = millis() - _start;
    if (timeSinceStart >= _current.timestamp)
    {
        servos[_current.servo].write(_current.angle);
        readNext();
    }
}

void readNext()
{
    // Read next position data into the "_current" position.
    // Check to see if the returned amount of data agrees with
    // the required amount.  If it does not agree, its because 
    // the end of the _file has been reached.
    if (_file.read((u8*)&_current, sizeof(_current)) != sizeof(_current))
    {
        // We've finished reading the file.
        if (_repeat)
        {
            // Rewind the file.
            _file.seek(0, SeekSet);
            // Use recursion to read the first entry.
            readNext();            
            // Set the start time to the current time.
            _start = millis();
        }
        else
            // Close the file.
            _file.close();
    }
}

As you'll see, I've commented them pretty well.  But... IF you want to try them and IF you have any troubles, let me know.

I would include a video, but I don't have enough bandwidth here, but I think you can see the blurred motion of the servos. 😆 

Ralf

3 lines of code = InqPortal = Complete IoT, App, Web Server w/ GUI Admin Client, WiFi Manager, Drag & Drop File Manager, OTA, Performance Metrics, Web Socket Comms, Easy App API, All running on ESP8266...
Even usable on ESP-01S - Quickest Start Guide


   
ReplyQuote
frogandtoad
(@frogandtoad)
Member
Joined: 6 years ago
Posts: 1458
 

@zander

Posted by: @zander

@will Ok, I checked the original sketch from Bill. If they want to enlarge that to 3 or 4, isn't that a  good place to use a class? (can't believe I just said that)

Me either, LOL!

Looking forward to an example to suit!

Cheers


   
ReplyQuote
Ron
 Ron
(@zander)
Father of a miniature Wookie
Joined: 4 years ago
Posts: 8015
 

@frogandtoad Not likely you will ever see anything written by me, I am barely able to read the code. Maybe modify something very similar but that's about it.

First computer 1959. Retired from my own computer company 2004.
Hardware - Expert in 1401, 360, fairly knowledge in PC plus numerous MPU's & MCU's
Major Languages - Machine language, 360 Macro Assembler, Intel Assembler, PL/I and PL1, Pascal, Basic, C plus numerous job control and scripting languages.
My personal scorecard is now 1 PC hardware fix (circa 1982), 1 open source fix (at age 82), and 2 zero day bugs in a major OS.


   
ReplyQuote
Will
 Will
(@will)
Member
Joined: 4 years ago
Posts: 2594
 
Posted by: @inq
Posted by: @will

I understand what you're saying and if the devices were extremely fast, then I'd agree. But we're talking cheap servos which are slow.

I'm not sure I understand your point.  In my opinion, them being slow REQUIRES you to have a timing value for each for when you read the analog pin connected to each potentiometer and writing it to a file and then writing to the servo to actually move it.  If you use one timestamp for potentially dozens of pots/servos, by the time you write the last one it might be significant fractions of a second.  When played back they will not be in sync visually.

That was my point. Somebody mentioned Claymation earlier in the thread. There, the creator adjusts all of the parts of the animation that need to move and then takes a snapshot and then moves on to the next frame.

Here, you can set all the servos to their next position and then take a snapshot of them. But during playback you have to wait for all of them to be moved. So the timing has to be based on the playback, and the servo settings have to be set according to the distance they'll have to travel during that time.

Anything seems possible when you don't know what you're talking about.


   
ReplyQuote
Inq
 Inq
(@inq)
Member
Joined: 3 years ago
Posts: 1904
 
Posted by: @will

That was my point. Somebody mentioned Claymation earlier in the thread. There, the creator adjusts all of the parts of the animation that need to move and then takes a snapshot and then moves on to the next frame.

Here, you can set all the servos to their next position and then take a snapshot of them. But during playback you have to wait for all of them to be moved. So the timing has to be based on the playback, and the servo settings have to be set according to the distance they'll have to travel during that time.

Now, I see our differences... I completely agree the sketches I provided will serve poorly in that case.  I was the one that made that allusion to Claymation type choreography, but @hinobot came back with this:

Posted by: @hinobot

We are building a pair of eyes that go in a painting for our haunted house.  The eyes remain closed for some time, open, move around for a bit, stare off for a moment, and then close.  The idea is that the pattern repeat over and over again.  Think of a tape recorder that captures information in real time and then plays it back again.

... that he wants to record in real-time (not step by step like I had guessed).  I'm now guessing he and his daughters work together to drive the eyes to their desired choreography.  His scenario actually makes the coding problem quite simple.  Both of the sketches (Recorder/Playback Sketches).  Keeping them in two Sketches also make it easy to study and digest what is going on in the coding.  A wise choice on his part.  The programs are less than 70 lines if you take out all of the comments and they should be pretty easy to digest if he studies the comments and my logic summary - Logic Summary

Although I only tested with two servos, four should not be a problem and they could make a whole animatronic body with dozens of servos with the same code.  With some good practice with coding, they could come up with ways of combining files... say one controlling eyes blinking occasionally and another controlling head movements into one script.  

I thought it a fun project in my insomnia last night.  I'll probably use it in something someday.  Maybe they or someone else will improve on it.  😎

VBR,

Inq 

3 lines of code = InqPortal = Complete IoT, App, Web Server w/ GUI Admin Client, WiFi Manager, Drag & Drop File Manager, OTA, Performance Metrics, Web Socket Comms, Easy App API, All running on ESP8266...
Even usable on ESP-01S - Quickest Start Guide


   
ReplyQuote
Will
 Will
(@will)
Member
Joined: 4 years ago
Posts: 2594
 
Posted by: @inq

 but @hinobot came back with this:

Posted by: @hinobot

We are building a pair of eyes that go in a painting for our haunted house.  The eyes remain closed for some time, open, move around for a bit, stare off for a moment, and then close.  The idea is that the pattern repeat over and over again.  Think of a tape recorder that captures information in real time and then plays it back again.

In that specific case, I think @zander had the best idea. They should adjust the pots to get the desired servo readings with a desired elapsed time, make a note of the time and the ones that changed and so on. However, instead of recording the movements on an SD card, they should just write a sketch that exhibits pauses and executes servo.write commands according to the notes made. That would be totally editable to adjust timing and movement, but it would take up too much space for very long sequences.

Personally, I'm skeptical that three people can cope with 4 pots being manipulated "in real time", I feel certain that somebody will over- or under- adjust their pot and mess up that "take". But then, I'm a life-long cynic so take this with a handful of salt 🙂

Anything seems possible when you don't know what you're talking about.


   
ReplyQuote
HinoBot
(@hinobot)
Member
Joined: 3 years ago
Posts: 6
Topic starter  

@inq Thank you so much for the code and layout, we just purchased the ACEIRMC ESP8266 module from Amazon, we will upload your code and see if it works for our needs... thanks!


   
ReplyQuote
Inq
 Inq
(@inq)
Member
Joined: 3 years ago
Posts: 1904
 
Posted by: @hinobot

@inq Thank you so much for the code and layout, we just purchased the ACEIRMC ESP8266 module from Amazon, we will upload your code and see if it works for our needs... thanks!

I sure hope it works out for you.  If it doesn't, you know where tech support is. 😉 

Years ago I bought a bunch of the shields for the ESP8266 WeMos that I like to use.  I put them in a drawer and forgot about them.  I just sorted through them and one of them is a SD card shield.  I'm using your code sample from your OP as a guide.  I'll revise the code and re-post it.

VBR,

Inq

3 lines of code = InqPortal = Complete IoT, App, Web Server w/ GUI Admin Client, WiFi Manager, Drag & Drop File Manager, OTA, Performance Metrics, Web Socket Comms, Easy App API, All running on ESP8266...
Even usable on ESP-01S - Quickest Start Guide


   
ReplyQuote
Page 3 / 4