BASIC programming for Multi-Tasking Control
written by djsfantasi
"Hi, I’m a Penguin. My name is Peter. Do you want to go down to the ice floe for some fun?"
Then the animatronic penguin breaks into a little dance, flapping his wings and blinking his eyes in time to the music. (A video link is provided at the end of the article.) This short impromptu show is courtesy of what I call A-code, an interpretive language that is executed by a FreeBASIC program. Animatron can program the animatronic for a specific show or a non-deterministic show, based on the A-code program provided. The interpreter’s programming is done old-style, but using OOP concepts. It executes up to 64 different actions in parallel, but with time-sharing rather than threading. It uses arrays and indices rather than UDTs and pointers.
Peter Penguin started as a small, general purpose animated figure. The torso would lean left, right, front and back and dance. The head would rotate. The arms would flap. The mouth would move in synch with voice or music. Of course, the question arose as to how the Penguin was going to be controlled? Was it going to use separate servo controllers for each motor, controlled in some analog fashion? Was it going to be autonomous or require a PC for control? This was new ground for me and I had to do a lot of reading and research. The idea of a serial servo controller was appealing, as it simplified the interface to a computing resource. But the commands for the controller were a bit esoteric. I wanted the eyes to look left and had to code "#0P2000 #1P2000 T1500".
Data Structures
So the first programming I did was in Excel, with a lookup table that matched a friendly description to specific controller codes. It then used this value to create a DOS command, which was copied into a .bat file and run to animate the penguin. This same concept was used in my FreeBASIC interpreter to execute moves. It also is the first data structure that I created which is the "Move Description Table" [Note the left columns in my tables are line numbers for reference within this article and are NOT in the actual source code or data file]
1 | ReDim MoveDescription(0) As String ' Move lookup table: description of move, e.g. "Move Eyes Left" |
2 | ReDim MoveCommand(0) As String ' actual servo move command, e.g. "#0P2000 #1P2000 T100" |
3 | Dim MoveIndex As Integer =0 ' array index of Move arrays |
The data for this table is read in from a CSV (comma separated value) file, which defines specific moves and controller commands, a sample of which is provided below:
1 | Squint eyes | #2P1100 |
2 | Cross-eyed | #0P2000 #1P1100 T100 |
3 | Open Eyes wide | #2P750 |
4 | Open Eyes Normal | #2P1000 |
5 | Open eyes slowly | #2P1000 T3000 |
6 | Squint eyes slowly | #2P1500 T2000 |
7 | Blink eyes | #2P1200 |
8 | Close eyes | #2P2000 |
9 | Close eyes slowly | #2P2000 T3000 |
The defined moves (or commands) can be combined together into scripts. Scripts can refer to other scripts. Think of a "script" as a subroutine. Scripts are coded in a single file, called a "show". This show is another major data structure, but since the scripts must be identified uniquely somehow, there is another supporting data structure used to map the script friendly name with a pointer into the show code data.
1 | ReDim LabelDescription(0) As String ' Label lookup table; name of label or script |
2 | ReDim LabelScriptIndex(0) As Integer ' index to step after label in script arrays |
3 | Dim LabelIndex As Integer =0 ' array index for Label arrays |
There are two major data structures left to define. Before we delve into the show’s data structure, let’s view the "Player" data structure. The term "player" comes from the controller I used, a LynxMotion SSC-32. One might consider a player as a process that is executed in a time-shared fashion by the interpreter. It is also similar to a thread - but controlled by the interpreter rather than the operating system. There is one entry per process in the player data structure arrays. One entry is a pointer to the step in the show code that is being executed and a time value which determines when (in time) the next step can be performed. This later detail was necessary because real-time for the servos was orders of magnitude larger than the program execution time, This allowed me plenty of time for multi-tasking.
1 | ReDim PlayerStep(0) As Integer ' Player properties: index to step in script to execute next |
2 | ReDim As Double PlayerEndWait(0)' array of time before next command is executed |
3 | PlayerEndWait(0)=0 |
4 | Dim Player As Integer =0 |
The last major data structure is the actual show code. It is what the A-code represents. A-code is a triplet, of an action, object and a value in its abstract form. Specifically for example, it could represent an action of making a move, what move to make and how long to allow the move to run.The program takes the A-code and tokenizes it, converting labels, names, and actions into tokens, values, indices, etc...
1 | ReDim ScriptAction(0) As Integer ' Script properties/command token: action to take |
2 | ReDim ScriptDescription(0) As Integer ' Index to servo move table or show step |
3 | ReDim ScriptOption(0) As Integer ' value for action, i.e. pause length after command is executed |
4 | ReDim ScriptStack(0) As Integer ' stack for saved script addresses (could be used for other purposes) |
5 | Dim ScriptStep As Integer =0, MainScript As Integer=0 |
And here is an example of raw a-code input into the program:
Startscript, Main, 0 Playmove, Look left slowly, 1500 Playmove, Look right slowly, 1500 Playmove, Look ahead, 20 Playmove, Open eyes wide, 20 Scriptpause, pause, 2000 Playmove, Open Eyes Normal, 20 Say, merry_xmas.wav, 0 Playmove, Torso LU, 333 Playmove, Torso RU, 333 Scriptpause, pause, 1000 Playmove, Stand Up, 500 Playmove, Squint eyes, 20 Playmove, Open eyes wide, 20 Playmove, Close eyes, 20 Endscript, Main, 0
Program Structure
You may note that I have spent more time describing the structure of the variables and data within the program than I am going to use to describe the program. This is part of my belief that good data design drives good program design.
The program listing is available at http://pastebin.com/n1avtP2n and downloadable here.
First, there are currently 17 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 token value of each command :) )
Token | Command | Description |
0 | PlayMove | send commands to controller |
1 | PlayScript | execute script in parallel; script must be defined in same file |
2 | StartScript | define new script; main script MUST be last in file |
3 | EndScript | end of script routine definition |
4 | JumpTo | "goto" command; label MUST exist (is not checked for) |
5 | Label | definition of label used in "JumpTo" command |
6 | SyncPoint | definition of scripts which will synchronize with other script(s) |
7 | EndSync | definition of step in script at which to wait for synchronization |
8 | ScriptPause | "Pause" or "Delay" command; pauses execution for n milliseconds |
9 | Say | play external sound file; will cause servo defined in "scbase" (e.g. mouth servo) to synchronize to sound |
10 | RandomMove | Randomly perform on of the following n actions |
11 | RandomPause | Pause some random time between the two times specified on the command |
12 | CallScript | Call a script rather than running it in parallel in its own player |
13 | EndWait | network command to clear pauses in execution at a "NetWait" point; not used in scripts |
14 | NetWait | Define a point in script at which a network command can cause a pause in execution |
15 | OneOnly | define a command that will exit a script if it is already running in a separate player |
16 | ActionSeq | loop sequentially through command group in a script. |
Let’s look at the short A-code show file presented earlier and see how it is processed. I am going to use the line
Playmove, Open Eyes Wide, 20
as an example. The tokenized version of this line would be:
0,3,20,6
But how do we get the tokenized version? This is the fifth line of code, so the value of ScriptStep would be 5. When initially processing this line, the program would identify "Playmove" as token 0, and put this into value into ScriptAction(5). In the example, "Open Eyes Wide" is the third entry in the Move Table, so the program would put a 3 into the ScriptDescription(5). The value of 20 in this example means to pause for at least 20 milliseconds after issuing the command, so it would put a 20 into ScriptOption(5). Finally, the next step in this simple command is the next step in the interpreted code, so the program would put a 6 into ScriptStack(5).
Now when executing this show, note there is only one script defined, so there would only be one player defined. The program would check for external inputs and process them first. Then, each player would be given a chance to execute; assume that the current PlayerStep(0) is equal to 5. The token at ScriptAction(5) is a zero, which is a PlayMove command. The SelectCase structure would then execute the following BASIC code.
Case PlayMove MoveIndex=ScriptDescription(ScriptStep) Put #1,,MoveCommand(MoveIndex) PlayerEndWait(Player)=Timer+ScriptOption(ScriptStep)/1000 PlayerStep(Player)=ScriptStack(ScriptStep)
This would pull the command from the move table, send it to the serial port ("Put #1,,..."), set the next time this player can execute to the current time plus the milliseconds specified in ScriptOption(5), and finally set the next step this player to execute to be ScriptStack(5), which has a value of 6.
For each of the 17 commands, a similar process is followed. Each of the cases is identified and the proper code executed by the Select...Case construct. In several cases, the processing of multiple commands is done identically, so they share the same basic code. For example, the StartScript command and the Label command both define a point or address in the code, so they share the same Case block. The only difference is that the StartScript command saves the last index for a pointer to the main script, as the last script in a show file is by definition the "main" script.
A thread is started to listen for network traffic. This was implemented for iPhone or internet control of the animatronic; its details are not covered in this article. The basic block structure of the processing portion of the program is below. It is in actuality very simple.
- An outer while loop runs as long as the program is not stopped or the end of the main script has not been reached.
- Outside inputs to the animatronic are checked, to see if any action is necessary. Currently, we check for network activity which processes a small subset of commands
- For each active player or process, a time slot is given to execute one command, if the process is allowed to run (see the description of ScriptPause below for an explanation of why it might not be allowed to run)
- Outside inputs, such as if there is any feedback from the audio amplifier, indicating that the animatronic is trying to say something (as a result of the "Say" command). In this case, a mouth servo is controlled depending on the audio (the animatronic audio amplifier and envelope follower circuit and ADC controls are not described in this article)
- A Select...Case statement is executed for the specified command.
- The next player is selected
- End of the while loop
Multi-Tasking
Now if there are multiple players or processes running, the code would execute like this. First, as it loops through all players, looking for an active player. A player is inactive if the current timer is less than the PlayerEndTime() defined in the array. It is also inactive if the PlayerStep() value is zero. Conversely, if the PlayerStep() value is non-zero, it points to a step in the show file which it will exectue next. But only if it its time has come to execute - meaning the timer is greater than the PlayerEndWait().
While it is looping through all the players, it gives some time to process the audio being played through the sound system. Again - servo commands take microseconds to send and milliseconds to actually move, giving us plenty of processing time to do other things. Such as detect that the speaker is playing something loudly and the animatronic's mouth should open wide. In the future, I would like to check for proximity of people and have the code execute a script to face them directly. This would also go into the "processing external inputs" block.
There is also some code in this loop that checks for keyboard input. This code (and code in each Case statement that displays debug information) has not been described, as it is used for testing and debugging A-code scripts. Briefly, it allows the execution of a script to be stopped or paused, will write out the current tokenized array data, and will allow single stepping through the A-code. In the non-deterministic scripts (all random moves), this code is used to stop the penguin from running (I.e. press the Esc key to stop execution and leave the program.)
' For each Player For Player=0 To MaxPlayers ' "Player = 0" is the main script ' Pause/Single Step script on request ControlKey=InKey If ControlKey = PauseKey Then {debug code omitted} EndIf ' Speech processing ... If (SoundEndWait-Timer <0) Then {sound processing omitted} EndIf ' Execute active player If ((PlayerEndWait(Player)-Timer)< 0) And (PlayerStep(Player)<>0) Then {debug code omitted} PlayerEndWait(Player)=0 ScriptStep=PlayerStep(Player) Select Case ScriptAction(ScriptStep) {omitted}
Descriptions of the Commands
The following sections provide short descriptions of the commands and how the program processes them.
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.
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.
Talking
Say 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.
Naming Commands
The following commands, at this point, hopefully are self-explanatory.
* StartScript * EndScript * JumpTo * Label
Advanced Features
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.
* SyncPoint * EndSync * EndWait * OneOnly
Finally
A video of Peter (my animatronic) in action, using this software and the A-code example linked to can be found at http://www.youtube.com/watch?v=3X322SL2UdA.
A complete listing of the A-code example is available at http://pastebin.com/TGTevNXp
The program listing is available at http://pastebin.com/n1avtP2n and downloadable here.
Hope you enjoy my little exercise and can learn from the techniques I have used and presented. The entire language of A-code is written in approximately 800 lines of code. If you have any questions, I can be reached at the FreeBASIC forum as the user, djsfantasi.