Animatron started as an Excel spreadsheet and then grew into a FreeBasic program. The program ran on my laptop and communicated to my robotic penguin to control it's behavior. The next generation is what I am going to describe in this Instructable.
It is an ~800 line sketch, which runs on an Arduino Mega 2560. It implements all the functions of its predecessor, with the exception of being able to controlled via an Internet socket connection. (This is not out of reach, with a wireless shield, but just not implemented this round. The Arduino Mega also has an MP3 player from Adafruit, through which the penguin "speaks". The Adafruit shield stores its MP3 files on and SD card, which is aso used to store the program the controller executes. For you see, the controller is actually a run time module for a tokenized language I had developed for this purpose. I call it "a-code", for "animatronic code"
Note: Don't forget to vote!
Teachers! Did you use this instructable in your classroom?
Add a Teacher Note to share how you incorporated it into your lesson.
Step 1: Overall View of the Software
Animatron8.3 is organized into six modules. Initialization and definition of functions, data and tasks take up more than half the code. It's my philosophy that data drives code; not the other way around. I use "#defines" so that the code is self documenting. It is much easier to understand what RandomMove is for, rather than remembering what a 10 means.
The modules are as follows:
- Initialization of global objects and definitions
- Defining commonly used functions. I will use a function with variable numbers of arguments, to simplify a common step - like providing progress or debugging output. I will also use a function to separate complex code from where it is used, so as to make the flow easier to understand.
- Setup is where my serial communications are defined, where the SD card is initialized, where the servo controller is initialized, etc...
- The main program starts with definition and initialization of local variables. These are variables used in execution, that were not required to be defined for Initialization and Setup. Such as the "Players" array. This array is used to save the current script step and the time before the next step should be executed. Other such arrays are used to synchronize different parts of the program or to keep track of which script is running (in case you specify that only one instance is allowed).
- The module labelled start execution is where the sketch loops through all active commands. However first, it performs two tasks. There is a dedicated switch connected to the Arduino, through which the program may be paused. It checks that here. Secondly, the penguin can react to external stimulus. In the Instructable step which describes this module, you can see it check for audio (it is saying something) and control the mouth servo accordingly. It is also where in the future, motion sensors and tracking, possibly voice recognition and conversation, and other reactions will be checked.
- The remainder of the code actually performs the script. It is a switch...case structure and the code for each feature is implemented in the case block.
Step 2: Initialization
First, we include the necessary libraries for the sketch. Besides the standard SPI and SD libraries, which are needed to talk to the shield, I included the MP3 Player library. Felt like adding my public library, but ran out of space on the Arduino.
Talking about space... Also, pgmspace.h is included to allow storage of the large Move Command strings in program space, freeing up heap memory for execution.
MemoryFree is included for debugging.
The next block of #defines are to set aliases for the command tokens. When compiled, each command is represented by a different numerical token. By using the defines, the program is easier to read. Another one of my philosophies.
Then, all the Arduino pins in use by the program are defined (I noted while writing this, that soundpin has snuck to the top, out of place! Naughty soundpin!)
The static move command are defined next, with an include. I'll discuss in more detail next step.
Then, the core arrays... the creme de la creme... the heart and sould of the sketch, are defined next. These four arrays, ScriptAction, ScriptDescription, ScriptOption and ScriptStack, are used to contain the tokenized program. They contain the action, the object, the timing and the script counter for each step. There is another define here, equating EndMain to ScriptSize. The value is used in two separate places with different intent - so this define lets the name be appropriate with the function the value is being used for.
Note: to view the code samples, right-click on the picture and open them in another tab. You can zoom them to a larger size to see the actual code! Try it!
Also, I've attached a text file copy of the program, with annotations identifying the sections in which the code is referenced. Use it as a companion document.
Step 3: Using Program Space for Static String Values
Arduino includes a method where it is possible to store variables in program space. Heap space for variables is limited, so this allows an efficient use of that space.
The ProgMem library is what is used and it extends variable typing to include specify storage area. In this sketch's case, I first define each move command in a character array stored in program memory. I then define an array of pointers, pointing to each variable length character previously defined. While extracting values from variables stored this way is a little non-intuitive, I only had to do it in one location. And once you have the syntax down, it's as easy as indexing a normal array!
Step 4: Functions, Functions
I used three functions. These two are used to display progress and format the output.
The Arduino's basic printing functions are somewhat limited. AFAIK, you can only print one value at a time; there are no formatting function, like tabs. I could be wrong, but the first function here takes a parameter and prints a number of spaces, to indent the remaining output.
The second function is a little more complex. It will accept two through five parameters and print them on one line, indented by the tab function. The first two parameters are always required and are the number of the script being executed (its player number), and the action the script is performing, It will then print up to three optional parameters, if they are present.
Step 5: And 'nother Function
This function reads a programs token values from a file on the SD card and loads them into the program arrays (ScriptAction, et.al.). It is also defined so that it accepts the parameters by reference, so that it can return all values read. It is used to read the A-Code!
It reads from the file a character at a time. If the character is numeric, it appends it to a strings until it finds either a comma, the end of the line or the end of file. In either of these cases, it converts the string to an integer and stores it in a local array.
When the end of line is reached, the array items are transferred to the function parameters and the function returns a boolean indicating whether or not the end of file was reached.
The coding in this manner has some intended side effects. First, it does not have to return any values. SO it can be used to read one or two tokens. I use this to read two parameters at the beginning of the file: MainScript or where the program starts and Scriptsize, how many lines are there in the program.
Secondly, since it only looks for numeric characters (it is intended to read positive integers only), the file can be self documenting. For example, one line could look like:
Action=3, Move=11, Option=50ms, Stack=7
Writing about code is so dry... What I could use is a bottle of dry wine right now.
Step 6: Setting Up and Getting Ready to GO!
It seems like forever, but we aren't ready, Freddy, for action yet!
The Arduino Setup module for this sketch is fairly long. It sets up several (eight) components of the system and software. Including:
- Serial communications - Both communications to the console and the servo controller are setup.
- Servo controller - Sets up communications and sends a test transaction.
- Random numbers - In typical Arduino fashion, the random number generator is seeded with the value returned on an unconnected digital port.
- Sound shield - the shield is inialized and tested for connectivity by the Mega.
- SD card - The SD card is initialized.
- A-Code - The function described in the last step is used to read in the program token values.
- Digital control pins - the pins used for pausing the sketch.
- Displays Free Memory - Displays Free Memory
Step 7: Now We Are in the Main LOOP
But we're not ready to run yet. So far, we've defined terms and global values, written a couple of functions, setup the hardware and software, read in the program and yet...
We need to define the local variables. These are variables which will only be used in the main loop. They are used for:
Variables used in sound/mouth processing, such as the limits of the ADC input for various mouth movements, and the string for the servo command...
The Player object are the arrays which contain the time and actions from the script of a given "Player". A Player executes a command simultaneously with other commands. In the penguin, his head could be looking left while his right flippers waves. Hence, two actions, two players. (Pssst, it's not really simultaneously; it's time slicing)
Move command variables are the index to the command (Everything must be defined!) and a buffer into which the program memory instance is copied.
Sync. scripts are a special function of the language which Animatron8.3 executes. Two or more different scripts in as many players can be programmed to wait for each other, allowing synchronized movements
Random play - Another index used when randomly selecting an action. Useful in the penguin when you write several head or torso scripts, and want to randomly select one to be executed. When I show the penguin, I comment that it has a mind of its own. I never know what he's going to do. There are days when I don't know what I am doing!
Command index - YOU WILL DEFINE EVERYTHING!
Pause/random pause - Intermediate variables used to calculate the actual time to pause when instructed to randomly pause.
Loop control - the main loop uses this variable. It simply loops "while(NotDone);".
Pause control variables - These are variables that are set with a digitalRead of the pins connected to the control switch, and used to determine whether or not the sketch should pause.
"Define first step" defines important variables. It defines the first player and sets it's value to be the script start or MainScript (discussed in the read A-Code function). One 'step' closer to running!
Step 8: Executing A-Code in Animatron8.3
Now we are ready to start executing. The main LOOP performs three major functions:
- Checks for sketch control and pauses if an external switch is set
- Reacts to external input. Right now, it is a hybrid of external and internal inout. The only scenario coded is if the script calls for an audio file to play, Animatron8.3 will read the audio envelope of the sound and move the mouth in unison, However, this is where additional features will be implemented, such as:
check for location of visitor
check for proximity of visitor
check for vsitor comment (hearing)
The third function is to execute the A-Code. Each pre-defined action, acts upon the Script Object and the Player Object in order to accomplish the desired result. (Ok, for you purists, I know these aren't 'objects', but if this were programmed in C++, they would be. So please cut me some slack).
I'll provide a couple of pseudo-code examples. Note that each action starts with a debug statement; I'm skipping that
Ok, Random Pause...
- set min=ScriptDescription
- Set max=ScriptOption
- Set timepause=rnd()*(Max-min)+min
- Set PlayerEndWait*=currenttime(ms)+timepause
- Set the next step for the player to ScriptStack
* PlayerEndWait is an array used in the definition of Players. A player does not get another time slice until the current time is greater than the PlayerEndWait.
Another popular command, PlayMove
- Get the move command from memory indexed by ScriptDescription
- Send it via serial communications to the servo controller.
- Set the next step for the player to ScriptStack
Step 9: Animatron8.3: an Animatronic Control Language Run-Time!
The program is currently working in test mode. All functions have been tested on the bench,with a variety of scripts Integrating with the penguin is in progress.
.I was considering attaching a copy of the A-Code, but it would look like a matrix of random integers. SO I've attached a sample script, before compiling. (See ScriptExample.txt) And I've also attached the current program. (Its in two files Animatron8.3.txt and MoveCommands.txt)
I hope you have found my descriptions instructional and at times amusing.
Thanks for tuning in!
Note: Don't forget to vote!