Step 1: Description
For example, “Eyes Open” sends the SSC-32 command “#2P750” through the serial connection.
See, I think the former is much easier to remember than the latter. Similarly, we define the phrase “Eyes Closed”, as well as approximately sixty two other phrases. These description and value pairs are an motion object.
Animatron has another feature. It can play these moves in a user defined sequence and these sequences can be saved to be called again or from another part of the program. To do this, it has defined several actions to be performed on the objects. There are motion objects and others, too. There are currently sixteen action objects plus a handful of special cases.
Step 2: Scripts
These components form a group, of an action, an object and a value. Each line represents one line in a script. Multiple lines can be combined to form a player (a script executing in parallel with other scripts.
For example, we have Eyes Closed and Eyes Open. Introducing one other command “ScriptPause, nnn” (which pauses for nnn milliseconds), we can write a script to blink the eyes for three times, pausing three seconds between blinks.
Note the “30” at the end of the line. This value gives the servo time to respond and can be tuned per servo brand, by experimentation.
Another quick script flaps its wings:
PlayMove, Arms Up, 30
PlayMove, Arms Down, 30
Note that this is two scripts. One to define flapping the wings and a second to flap them three times.
First, there are currently 16 commands which the interpreter recognizes. A summary of these commands follows. (You can also get a sense of the history of adding features, by the position in the list of each command ) If you don’t want to get bored right now with the minutiae of the commands, skip forward to the program description to see how this all works.
|PlayMove||send commands to controller|
|PlayScript||execute script in parallel; script must be defined in same file|
|StartScript||define new script; main script MUST be last in file|
|EndScript||end of script routine definition|
|JumpTo||goto command; label MUST exist (is not checked for)|
|Label||definition of label used in "JumpTo" command|
|SyncPoint||definition of scripts which will synchronize with other script(s)|
|EndSync||definition of step in script at which to wait for synchronization|
|ScriptPause||"Pause" or "Delay" command; pauses execution for n milliseconds|
|Say||play external sound file; will cause servo defined in "scbase" (e.g. mouth servo) to synchronize to sound|
|RandomMove||Randomly perform one of the following n actions|
|RandomPause||Pause some random time between the two times specified on the command|
|CallScript||Call a script rather than running it in parallel in its own player|
|EndWait||network command to clear pauses in execution at a "NetWait" point; not used in scripts|
|NetWait||Define a point in script at which a network command can cause a pause in execution|
|OneOnly||define a command that will exit a script if it is already running in a separate player|
|ActionSeq||loop sequentially through command group in a script.|
Step 3: Descriptions of the Commands
The following sections provide short descriptions of the commands and how the program processes them. Note the following jargon is used to describe the commands:
- · Script Actionis the action of the command.
- · ScriptDescription is the object of the action, or in some cases a value used in the action.
- · ScriptOption is another value field, used as input to the action.
- · ScriptStack is a generated value – normally containing the address of the next instruction to be executed.
Step 4: Basic Move Commands
ScriptPause is a simple command, placed in this section because it is used to pause for long times (relatively speaking) between commands. Its ScriptOption value is the number of microseconds to pause, and is added to the current timer value and stared in the PlayerEndWait() cell. As each player is given its time slice, if the current time is less than the end wait time, nothing is done and execution passes to the next script/player/process.
PlayMove contains an index to the move table, whose corresponding string is sent to the serial port to control the animatronic’s servos. Its optional value is used to extend the time before the player executes again, to allow the servo to move and other processes time to execute as well. The next script step is set to the value of ScriptStack().
PlayScript creates another player. As described, PlayerStep() is set to the index value contained in ScriptDescription() and the PlayerEndWait() is initially set to 0. Note that there is no attempt to keep the array elements sequential. There may be unused player entries as scripts finish between active players. A PlayerStep() value of 0 indicates an unused or inactive player.
CallScript is similar to PlayScript, however the script is not executed in parallel or in a player... The commands therein are executed sequentially within the current script. This is accomplished by using the ScriptStack() value of the EndScript command to point back to the current script.
Step 5: Random (and Not So Random) Moves
RandomMove takes a parameter which represents a number of moves to select from. Then, the interpreter using a random number generator to select one of the next number of commands. Here again, the ScriptStack() value is used, as each of the following commands have the value here of the next command after the block of randomly selected commands.
RandomPause uses a random number generator to pause somewhere between the minimum and maximum pause time. This is a special case where the ScriptDescription() contains something other than an index to another piece of data, but a time specified in milliseconds.
ActionSeq is not so much a random command, but in reality quite the opposite. It is used to execute a block of commands in sequence each time the script is called. Hence, if there are four commands specified, the first time the script is called, the first command in the block is run; the fourth time the script is called, the fourth command is run; the fifth time the script is called, the first command is run. I use this to have the animatronic have a conversation, over a random period of time. Here again, the ScriptStack() value of the ActionSeq command is used to keep track of which command to run next.
Step 6: Talking
The Say command uses a Windows system call to play a .wav file in the background. My animatronics take the audio from my laptop and feed it into custom circuitry on board, which plays the sound in a speaker in the figures mouth and feeds back a digital value from the controllers ADC circuit, so that the software can move the mouth in synchronization with the sound.
Step 7: Other Commands
The following commands, at this point, hopefully are self-explanatory.
* StartScript * EndScript * JumpTo * Label
The following features won’t be described here, but are used for network control and synchronization between scripts. Initially, I thought they would be necessary but in practice do not use them much.*
Ok, here’s the time that I am going to fess up. I’m going to skip the description of the first part of the program. The short and sweet version is that it reads the translation file (containing the phrase and the exoteric command) and the show file (containing the script definitions and what we actually want it to do), and convert all that text into four integer arrays. Tokens, I call it. It is a pre-compiler or tokenizer, which parses the input and prepares it for a compiler or in our case, an interpreter. ‘Nuff said.
The 50,000 foot description of how the program works is presented in the picture below. We’ll dive into a little deeper after we review it.
Network control, talking control and other sensors is where external environmental inputs are processed. The main control loop is where the scripted actions occur. (And be careful when calling them scripted actions, as some of the commands cause random actions to occur.) After processing, there is a pointer to the array where the main script starts (each show must have a main script). Execution starts there. Whatever action is requested, the program jumps to the appropriate section of code and executes. (For the advanced, this is done with the Select…Case structure.) Each parallel instance of a script is called a Player (like thread, process, etc…)
One of the first steps is to reserve memory for the player pointer, and its expected end time. Then the code executes and returns control to the next player to be executed. Note that the first player may not be complete. First, each player is given time enough to execute just one line. Second, the time specified on each line may not have elapsed. So the next time this script is given control, it is checked to see if its time has expired. If not, there is more time for another script to execute. If it has, the next line in the script runs. If the script has finished (time <=0 or script pointer set to zero), the control program continues on.
If ((TimeDone(Player)) And (Execution(Player)=Running) Then
Step 9: Summary
The secret lies in coding threads old style, with time slicing. A multi-threaded compiler could be used, but cooperation between threads would have to be thought out. Also, I didn’t describe it much here, but if the code can get a socket connection over the Internet, it overrides the scripted action and takes its commands from the great Web.