Inter-Computer Coordination
Volume Number: 13 (1997)
Issue Number: 11
Column Tag: Programming Techniques
High-Speed Inter-Computer Coordination
by Dr. Scott B. Steinman
Combining C++, FrontierScript, LabVIEW and multiple Macs to solve a real world problem
Introduction
A few years ago, I was asked to put together an infant vision clinic that would offer not only state of the art tests for assessing the function and health of an infant's visual system, but which would also be fast, easily operated and flexible enough to readily allow future expansion. It didn't take much thinking to come to the conclusion that only the Macintosh could do it. But the programming was far from straightforward at the time, since some of the system software that I needed had only just become available and was not well documented.
This article will discuss the specific programming issues that had to be solved in order to achieve the design goals of the clinic, as well as many of the workarounds that had to be made. These issues also apply to other medical, scientific and engineering tasks. We'll see that sometimes a seemingly more roundabout design actually produces a simpler, faster and more flexible solution.
Design Decisions
The infant vision tests typically involved displaying a rapidly animated visual stimulus that moved targets on a screen or changed the target's contrast dynamically. At the same time, physiological data such as eye movement recordings or evoked potential recordings ("brainwaves") have to be obtained in response to the stimulus. Both tasks need to be executed rapidly -- the animation may be run at refresh rates up to 60 Hz, while the data acquisition may occur at up to 1000 Hz.
While speed and flexibility are always two important design goals, the system had additional restrictions that made these goals even more critical. First, the time required to conduct the full test sequence needed to be extremely short since our subjects were very young infants whose attention span is limited in duration. The examiner has to collect data extremely rapidly on any given test, then quickly proceed to the next test. Second, the same examiner who has to control the data acquisition also has the responsibility of directing the infant's attention to a visual stimulus. This not only reinforced the need for speed of testing, but also ease of operation. Finally, the software system needed to be modular and easily extended when new tests of infant vision were introduced.
I chose National Instruments' LabVIEW(tm) as the language in which to write the data recording and analysis code. LabVIEW is a true "visual" (that is, iconic) dataflow programming paradigm, much like Prograph CPX(tm). Visual programming allows rapid application development (RAD) in a fraction of the time required to write an equivalent C/C++ program (for example, see Steinman & Carver, 1995). LabVIEW also includes a huge library of data acquisition and data analysis code modules (called Virtual Instruments, or VIs for short) that may be used to build a recording system. In addition, LabVIEW's integrated user interface design tools provide a uniform intuitive "front panel" of controls that can be easily operated even by assistants.
Unfortunately, while LabVIEW is well suited for laboratory data acquisition and analysis, it is not capable of performing acquisition and analysis while simultaneously generating rapid animated visual stimulus displays. One solution that was considered was to call Macintosh Toolbox and QuickDraw routines via external C language external code modules, but these graphics routines could not be called in parallel with the data acquisition VIs. Even if these routines were called in separate programs, running all of these tasks on a single machine could still be risky since processor interrupts of the data acquisition hardware and those of the visual stimulus animation code could potentially interfere with each other -- that is, data samples could be "skipped" during graphics operations or graphics could "shear" when data samples are acquired.
We therefore chose to implement the system on two Macintosh Quadra computers -- one solely for recording and the other solely for stimulus display. This would allow each computer to perform its single specialized task at maximum speed without interfering with each other.
Both the recording program on one computer and the stimulus program on the other computer had to "know" what the other was doing at any given moment. Their operations needed to be tightly synchronized if the two-computer system were to act as a single unit. The overall operation of the laboratory software would be controlled by the examiner on the recording computer, and the second computer would act as a "slave" to the recording computer. This computer would act only in response to commands sent to it by the "master" recording computer.
This article will discuss the specific programming issues that had to be solved in order to achieve real-time inter-computer coordination while meeting the design goals of the clinic. I'll discuss many of the workarounds that needed to be made. These issues also apply to other medical, scientific and engineering tasks. We'll see that sometimes a seemingly more roundabout design actually produces a simpler, faster and more flexible solution.
Clearly, two forms of communication were required. Each would be executed independently. The first was to maintain tight control over the timing of the sequence of events during data acquisition. Digital I/O lines (NB-DIO-96, National Instruments) joined by a ribbon cable were used to transmit these signals.
The second form of communication was to transmit commands from the recording computer to the stimulus computer to "tell" it what stimulus to display and when. This would, of course, involve Apple events. These Apple events would be sent across an EtherTalk network cable that interconnected these two computers alone. In initial testing of the software system, it was found that LocalTalk was too slow. When Apple event commands were sent to the stimulus computer, the recording computer often had to wait for a reply that indicated that the command was received correctly. An Apple event could "time out" while waiting for this reply if the transmission time was too long, locking up the data acquisition when the recording computer kept waiting for the reply. The much quicker EtherTalk was the solution to this problem, as replies could be received fairly instantaneously.
Finally, two more decisions related to programming language had to be made. The first was which programming language to use for the stimulus display software. LabVIEW would not do. While LabVIEW has some capabilities for sending Apple events (with provisos to be mentioned below), it has extremely limited abilities to respond to Apple events. C++, on the other hand, is well suited for writing Apple event handlers, but also allows for rapid interrupt code for accurately-timed stimulus display animation (see Steinman and Nawrot, or Steinman for such graphics techniques).
Although Apple events would be involved in the inter-program communication between the data acquisition and stimulus display computers, they could not be used in a uniform manner throughout the software due to limitations in LabVIEW's inter-program communication code. One possible solution to this problem would be to use AppleScript, but for several reasons to be discussed below, inter-program communication was implemented with the UserLand Frontier(tm) scripting language (now available free at www.scripting.com/frontier).
It could be argued that the ethernet connection could be replaced by a simple high baud rate serial line that carried messages from one computer to the other. However, there is one disadvantage to doing so. In the current system, both the LabVIEW recording program and the C++ stimulus display program are not aware of whether they reside on two computers or on a single computer. One side effect of using Frontier as an intermediary for passing commands is that Frontier hides these details from the LabVIEW and C++ programs. This means that if the present stimulus generation and recording system could be implemented on a single computer in the future, all that would need to be done is to edit one subset of Frontier scripts -- neither the recording software nor stimulus display software would need rewriting. If a serial line had been used instead, all of the inter-program communication code would need to be completely rewritten.
Software System Components
For a better understanding of the workings of the software, the general infant testing sequence is as follows: The main program establishes the connection between the recording and stimulus computers across the EtherTalk network. The digital I/O lines are then initialized. At this point, the examiner selects a test to be run.
A command is then transmitted to the stimulus computer to launch a particular stimulus display program, whose stimulus parameters are transmitted to the display program by a second command. The examiner then clicks a button on the recording computer screen to start recording. A digital signal is sent to the stimulus computer to enable the stimulus animation, and the stimulus computer returns a digital signal to trigger data acquisition, time-locked to the stimulus display. After data collection is completed, commands are sent to the stimulus computer to stop the stimulus presentation, then quit the stimulus display program.
The software system has been designed to be both flexible and modular. It is composed of three major components: The first, implemented in LabVIEW(tm) on the recording computer, initiates an Apple event link between the two computers, initializes the data acquisition and digital I/O boards, allows the examiner to select which to run and specification of stimulus and recording parameters, as well as starting, pausing or halting data acquisition. A uniform user interface across all tests simplifies operation of the tests.
Visual stimuli are presented via the second component, a collection of very small programs written in Metrowerks CodeWarrior C++ and Mathemaesthetics Resorcerer(tm). These programs take advantage of a reusable code library of drawing and animation routines that allows both palette animation (see Baro) and frame animation (see Steinman and Nawrot, or Steinman). These programs are small because they perform only three chores: (a) receive Apple event commands, (b) display animated graphics, and (c) read and write to the digital I/O lines for synchronization with the recording software.
The third component coordinates the stimulus generation and data acquisition via Apple Open Scripting Architecture-compatible scripts in the Frontier(tm) scripting language.
Inter-Program Communication Problems and Solutions
With a combination of different programming tools, several problems that are specific to real-time simultaneous visual stimulation and data acquisition have been solved. Specifically, we will present an easy way to synchronize two Macintosh computers to work as a single laboratory device, via software commands and hardware signals passed between.
The first problem which we had to confront and solve dealt with shortcomings in LabVIEW's capacities for inter-program communication. While Apple event support is included in LabVIEW, it is mostly specialized as VIs that are used to execute other VIs, such as AESend Run VI, AESend Open, Run, Close VI, AESend Close VI, AESend Abort VI and AESend VI Active?, or responses to these commands. For programs composed of code other than its own VIs, LabVIEW is more capable of responding to commands than sending them. A few other Apple event-related VIs exist, which are geared towards starting or quitting other programs, but these are not sufficient for laboratory program control.
Fortunately, a LabVIEW VI that is often overlooked by programmers just happens to be the one VI that is critical for controlling the operation of the stimulus graphics programs by the recording computer. This VI is called AESend Do Script. As its name implies, it is specialized towards sending scripts. When such scripts are received by a target program, their text must be decoded into a series of instructions to be carried out by that program.
The next decision was to choose a specific scripting language. AppleScript presented several obstacles. The first is that while AppleScript is relatively slow, even on PowerPC-based Macintoshes. The second problem is that AppleScript was not primarily designed for sending commands quickly over a network to a second computer. To send an AppleScript or Apple event across a network, the PPC Browser must be invoked. The PPC Browser is intended to allow users to choose which computer and application should be sent a command, but it also has one shortcoming for our purposes. It provides a level of security via the User Identity dialog box. While this is useful for preventing unwanted connections across the Internet, it is a severe impediment to our design goals. Every time we need to send an AppleScript or individual Apple event to each stimulus display program, we will be faced with the PPC Browser and User Identity dialog boxes! This is not only disruptive to the examiner using the software, but will also slow down our data collection.
A third problem is that if only AppleScripts are transmitted to the stimulus computer to initiate a stimulus display, these stimulus programs must be capable of receiving and parsing the AppleScripts. This presents a very difficult task for several reasons: (1) AppleScript programming is not entirely intuitive. (2) AppleScript programming requires programming knowledge about 'aete' resources that identify the Apple events "understood" by the receiving program. (3) In the present software system, this would require adding AppleScript support to each of a dozen small stimulus display programs. While adding AppleScript is a fruitful option for single large commercial programs, the time and effort required to add AppleScript support to several small specialized laboratory programs is simply not cost effective. We have selected an alternative that is simpler, yet overcomes many of the limitations of AppleScript.
That alternative is UserLand Frontier(tm). Frontier can transmit sequences of Apple events across a network at least ten times more rapidly than AppleScript scripts, and does not require the creation of 'aete' resources.
In our system, Frontier is installed on both the recording computer and the stimulus computer. When a command must be sent from the LabVIEW program on the recording computer to a C++ visual display program on the stimulus computer, it is first sent as a script from LabVIEW to a "master" copy of Frontier on the recording computer. This Frontier application is "told" to run a script stored within its Object Database that essentially transmits the command contained in the LabVIEW script across the Ethernet network to a "slave" copy of Frontier on the second computer. This second copy of Frontier translates the command into Apple event format and relays the command to the stimulus program, where the command is carried out.
Why use such a circuitous route? Why not just send scripts directly from the LabVIEW recording program to the stimulus display program on the second computer? Two reasons have already been mentioned: (1) Frontier speeds up the transmission of commands across the network, and (2) to avoid the need to add AppleScript-parsing code to each stimulus program. Let us add a to more important reasons to use Frontier as an intermediary in passing along commands: (3) Frontier simplifies our programming task by translating the original command from LabVIEW's Do Script VI, which is in textual script format, into a form that can be handled with simple code in the stimulus programs -- Apple event handlers. (4) Because Frontier itself sends the commands across the ethernet network, the intrusive PPC Browser and User Identity dialog boxes are avoided during the time-critical portions of data collection.
When the LabVIEW recording program must find a program on the stimulus computer to which to "connect" and send commands, it connects to the "master" copy of Frontier on its own machine. Similarly, the "slave" copy of Frontier on the stimulus computer connects to stimulus programs that reside on its own computer.. These two communication paths do not invoke the PPC Browser or User Identity dialog boxes, since each connection between programs is made within a single machine. The connection between the two machines is made by Frontier before the LabVIEW program is executed. Before launching the LabVIEW recording program, a small Frontier script is run on the recording Macintosh that results in the "master" copy of Frontier on that Macintosh establishing a connection across the network to the "slave" copy of Frontier on the stimulus Macintosh and telling that computer to do something trivial such as sounding a beep. This is the only time at which the PPC Browser and User Identity dialog boxes appear. During all subsequent experimental testing, these dialog boxes never reappear even when the experimenter switches between test types and stimulus types, because the connection across the network has already been established from one copy of Frontier to the other.
Let's examine the Frontier code that sets up the inter-computer communication across the network. Listing 1 is a Frontier script entitled testConnect that first establishes the network connection between the two Macintoshes prior to the launching of the LabVIEW recording program. Each copy of Frontier contains an Object Database of frequently-used scripts and data that may be thought of as the commands that Frontier itself can execute, send or receive, as well as parameters for those commands. The testConnect script is stored in the Object Database of Frontier on the recording computer. The testConnect script simply sends a command to the stimulus computer to sound a beep. Because this is the first script to be transmitted across the EtherTalk network, it forces the PPC Browser and User Identity dialog boxes to be displayed at this time. This is beneficial because data acquisition hasn't initiated yet, so these dialogs cannot slow us the test sequence.
Listing 1: testConnect script
testConnect
Establishes interconnection with copy of Frontier on second computer. This Frontier script on the "master" recording computer transmits a trivial script to sound a beep to Frontier on the "slave" stimulus computer to force the display of the PPC Browser and User Identity dialog boxes prior to test parameter selection and data acquisition. This script is simpler than that of Listing 3 because no parameters are transmitted with the "stimulus.beep" command.
© 1997 by Scott B. Steinman. All rights reserved.
on testConnect()
Create local variable named "netScriptStr" to construct
text of new script
local (netScriptStr)
Script will instruct stimulus computer to run script
named "speaker.beep" in Object Database of copy of
Frontier on stimulus computer
netScriptStr = "speaker.beep()"
Construct script command:
1. Create new script at location "scratchpad.netScript"
in Object Database of Frontier on recording computer.
This script will be to be sent to stimulus computer
2. Tell Frontier that next few operations will work on
the newly-created script by setting target of Frontier's
operations to that location
3. Clear contents at location "scratchpad.netScript"
4. Set contents of location "scratchpad.netScript" to
text contained in netStringStr
5. Reset Frontier operation target to the testConnect
script
new (scriptType,@scratchpad.netScript)
target.set (@scratchpad.netScript)
op.wipe() Clear current contents of netScript
op.setLineText (netScriptStr)
target.clear ()
Send script command stored at "scratchpad.netScript"
across network to "slave" stimulus computer
NetFrontier.runScript (@scratchpad.netScript,true)
Clean up
delete (@scratchpad.netScript)
The testConnect script starts by creating a variable named netScriptStr to contain the text of the command to be sent to the "slave" copy of Frontier. This string is simply the name of a Frontier script ("speaker.beep") within the stimulus computer's Frontier Object Database that we want executed. NetFrontier has been designed to transmit scripts stored at a specific locations in the Object Database rather than transmitting raw text strings, so we need to transfer this command string into a new script, then transmit that script. In order to do this, we create a temporary entry named "netScript" in the recording computer's Object Database's "scatchpad" area to contain this text (its Frontier data type is, appropriately enough, scriptType). We then must copy the text of the script ("speaker.beep") from the netScriptStr string into the newly created script. We are now ready to transmit the contents of this newly-created script text across the network with NetFrontier's runScript command.
How is this testConnect script called to perform these actions if the LabVIEW recording program isn't running yet? Along with its own internal scripts in the Object Database, Frontier can create stand-alone, double-clickable scripts. A stand-alone script called ConnectMacs is executed by the experimenter before launching the LabVIEW recording program. It calls the testConnect script.
Experienced Frontier programmers might notice that we have added a second parameter to the runScript NetFrontier script to allow the option of waiting or not waiting for a reply during transmission of the script to the stimulus computer. This is because by default the Apple event Frontier command that sends information from Frontier to other applications waits for a reply from the receiving application. When we send commands to show visual stimuli just as we are about to begin data acquisition, we cannot afford the luxury of waiting for an Apple event reply, as this would delay the onset of the data acquisition. It is therefore imperative to avoid a reply by using Frontier's finderEvent command instead of Apple event to send the information; finderEvent by default does not wait for a response. This command was originally intended for sending scripts from Frontier to the Macintosh System 7's scriptable Finder, but it also suites our purposes well. The modified script is shown in Listing 2.
Listing 2: runScript script
runScript
Modified runScript Frontier script. This Frontier script, part of the NetFrontier suite, sends a script across a network to control a second Macintosh computer. It has been modified to allow the option of waiting for an Apple event reply or ignoring the reply.
© 1997 by UserLand & Scott B. Steinman. All rights reserved.
on runScript (adr,waitReply)
on callback (netAddress)
local (data,val)
pack (adr^,@data)
if waitReply equals true
Wait for reply from Apple event
(by default, all Apple event transmissions produce
reply in Frontier)
if not AppleEvent (netAddress,'netf','inst',1,
"scratchpad.netScript",2,data)
return (false)
val = AppleEvent (netAddress,'netf','runs',1,
"scratchpad.netScript")
AppleEvent(netAddress,'netf','dele',1,
"scratchpad.netScript")
else
finderEvent command sends Apple event but
ignores reply
if not finderEvent (netAddress,'netf','inst',1,
"scratchpad.netScript",2,data)
return (false)
val = finderEvent(netAddress,'netf','runs',1,
"scratchpad.netScript")
finderEvent(netAddress,'netf','dele',1,
"scratchpad.netScript")
return (val)
NetFrontier.buddyLoop (@callback)
Once the inter-computer connection is made, we launch the LabVIEW recording program and set stimulus and recording parameters prior to data collection. Setting stimulus parameters requires sending commands and data across the EtherTalk network, as dictated by scripts sent from LabVIEW to Frontier within the recording computer. But LabVIEW's script transmission VIs expect to be sending commands by connecting to another program across a network, and we don't want that to occur. We want LabVIEW to communicate only with Frontier on the same computer, and let Frontier handle the communication across the network. We therefore have to force LabVIEW to establish a "connection" directly to Frontier within the recording computer. Figure 1 displays LabVIEW's graphical code for locating this particular copy of the Frontier application, then sending a small test script to Frontier to confirm that the connect was made properly.
Figure 1. LabVIEW ConnectMacs VI.
The Find Frontier VI in Figure 2 takes advantage of a LabVIEW PPC Toolkit VI named Get Target ID, which receives the name of the application to find and returns the its target ID, a LabVIEW structure (or "cluster" in LabVIEW terminology) that stores the location of a program on a network. We restrict the search to the copy of Frontier on the same machine hosting the LabVIEW recording program. The target ID returned by this search is used in all subsequent command transmissions to Frontier.
Figure 2. LabVIEW Find Frontier VI.
Now let's discuss the specific programming steps required to send commands and data from the LabVIEW recording program to their real target--the C++ stimulus display programs. In the example code to follow, the runStimulus command will be explained. This code is used once recording and stimulus parameters have been chosen by the examiner and we are ready to initiate data acquisition in response to a visual stimulus. The LabVIEW program initializes its data acquisition VIs, then sends a runStimulus command containing stimulus settings in the form of an Apple event to Frontier. Frontier then repackages the text contained within this Apple event into a Frontier script and transmits it. Upon receipt of the restructured script, the stimulus Macintosh's copy of Frontier executes the script, extracts the parameters of the script, and sends both a command and the parameters in an Apple event to the stimulus display program. An Apple event handler in that program retrieves the stimulus settings from the Apple event parameters, and displays the stimulus after sending a sync signal via the DIO lines to let the recording Macintosh know that it is time to start acquiring data. The net result is that the stimulus program only needs to receive an Apple event and its parameters in an easy-to-extract format, rather than a textual AppleScript that requires complex code to decipher.
The first step in this process is handled by the LabVIEW recording program. Once stimulus parameters have been selected and the experimenter is ready to record data, the Send RunStim Dosc AE VI is called (Figure 3) to tell Frontier to execute a script that receives stimulus parameters and then ships them to the stimulus computer. The script to be executed on the recording computer is named Stimuli.PVEP.netRunStim, and it receives as its argument the text contained in the script transmitted by LabVIEW. The script text contains the name of the Frontier script to execute -- Stimuli.PVEP.netRunStim (a stimulus display program running a Pattern VEP visual stimulus) -- and an argument to that script containing the list of the values to which the stimulus parameters are to be set ("50 380 8").
Figure 3. LabVIEW RunStim Dosc AE VI.
The netRunStim Frontier script is shown in Listing 3. The purpose of this script is to relay the command specified within the LabVIEW script to the stimulus computer, that is, a command to prepare a stimulus display, along with the desired stimulus parameters received as a single string argument to the netRunStim script named "str". This is done by packaging the command and stimulus parameters into a form that may be transmitted across the network by NetFrontier.
Listing 3: netRunStim script
netRunStim
Frontier script within the recording computer's Object Database for interpreting the LabVIEW runStimulus script and forwarding it as a Frontier script to the stimulus computer. The command extracted from the LabVIEW script and the arguments for that command are transmitted separately by Frontier to the stimulus computer, using the NetFrontier.broadcast and NetFrontier.runScript commands, respectively. Returns true if successful, false otherwise
© 1997 by Scott B. Steinman. All rights reserved.
on netRunScript(str)
Create local variable named "netScriptStr" to
construct text of new script
local (netScriptStr)
netScriptStr = "Stimuli.PVEP.runStim()"
Construct script command:
1. Create new script at location "scratchpad.netScript"
in Object Database of Frontier on recording computer.
This script will be to be sent to stimulus computer
2. Tell Frontier that next few commands will operate on
the newly-created script by setting target of Frontier's
operations to that location
3. Clear contents at location "scratchpad.netScript"
4. Set contents of location "scratchpad.netScript" to
text contained in netStringStr
5. Reset Frontier operation target to the testConnect
script
new (scriptType,@scratchpad.netScript)
target.set (@scratchpad.netScript)
op.wipe() Clear current contents of netScript
op.setLineText (netScriptStr)
target.clear ()
Construct script argument:
1. Delete current contents at Object Database location
"scratchpad.scriptArgument", if any
2. Create new string at location
"scratchpad.scriptArgument" to
hold parameters transmitted with command to run
"Stimuli.PVEP.runStim" script.
if defined (scratchpad.scriptArgument)
delete (@scratchpad.scriptArgument)
new (stringType,@scratchpad.scriptArgument)
scratchpad.scriptArgument=str
Send argument to stimulus computer via
NetFrontier.broadcast
if not NetFrontier.broadcast (@scratchpad.netArgument)
speaker.beep ()
delete (@scratchpad.netScript)
delete (@scratchpad.netArgument)
return (false)
Send script command to stimulus computer via
NetFrontier.runScript
if not NetFrontier.runScript (@scratchpad.netScript,false)
speaker.beep ()
delete (@scratchpad.netScript)
delete (@scratchpad.netArgument)
return (false)
Clean up
delete (@scratchpad.netScript)
delete (@scratchpad.netArgument)
return (true)
As in the testConnect script of Listing 1, The netRunStim script creates a local string variable to hold the text of the command that will be sent via NetFrontier to Frontier on the stimulus Macintosh. Remember that the purpose of NetFrontier is to direct a copy of Frontier on another computer to execute one of its own scripts. This is what netRunStim will do -- instruct Frontier on the stimulus Macintosh to execute a script named runStim contained in its own Object Database. A temporary script (containing the Frontier script to be executed on the stimulus computer) is constructed exactly as was done in Listing 1, but in this case the name of the script we are asking to execute on the stimulus computer is "Stimuli.PVEP.runStim". In other words, the runStim script resides in the stimulus computer's Frontier Object Database in its Stimuli.PVEP script table.
The next steps that the netRunStim script take are to send data to the stimulus computer -- data that specifies how the stimulus display will appear. This data forms the arguments to the runStim script that will be called on the stimulus computer. As we did above for the script itself, storage for the argument of the script is constructed in the "scratchpad" region of the Frontier Object Database, this time a string variable stored at the "scratchpad.netArgument" location. If a pre-existing argument string is still lingering at that location from a previous execution of this script, it is cleared. Finally, the content of the string "str" -- the stimulus parameter list "50 380 8" -- is copied directly into the string stored at "scratchpad.netArgument."
Now we are ready to transmit the script and the script argument to the stimulus computer. This is accomplished in two steps: (1) The NetFrontier broadcast command is first used to send the script argument across the network. If this transmission fails, the user is warned with a beep and all allocated storage is cleared. (2) The copy of Frontier on the stimulus computer is told to execute the script (named Stimuli.PVEP.runStim), using the script arguments just shipped, via the NetFrontier runScript command.
The copy of Frontier running on the stimulus computer must now receive this command along with its argument, decode it, and execute its own runStim script. This script is shown in Listing 4.
Listing 4: runStim script
runStim
Frontier script within the stimulus computer's Object Database for receiving script sent by the recording computer in Listing 3.The runStim script extracts the script command sent by NetFrontier.runScript and the stimulus parameters sent by NetFrontier.broadcast, then constructs an Apple event containing the stimulus parameters to the stimulus display program. Returns true if successful, false otherwise
© 1997 by Scott B. Steinman. All rights reserved.
Extract stimulus parameter strings from incoming Frontier script from recording computer. Place stimulus parameter values into local integer variables
local (sPer=long(
string.nthWord( scratchpad.scriptArgument,1 )))
local (diam=short(
string.nthWord( scratchpad.scriptArgument,2 )))
local (tPer=long(
string.nthWord( scratchpad.scriptArgument,3 )))
Construct Apple event to transmit parameters to stimulus
program. This Apple event will instruct the stimulus
display program to modify the grating stimulus parameters
and set up the graphical stimulus display. However, the
stimulus will not be displayed by the stimulus program a
until 'program ready' sync signal is received across the
digital I/O lines from the recording computer.
The arguments to the Frontier "Apple event" command are:
1. 'STIM' -- the suite or group of Apple events to which
the stimulus-running Apple event belongs
2. 'RnSt' -- the identifier for the stimulus-running Apple
event that tells the stimulus display program to prepare a
stimulus display
3. 'scyc' -- the identifier for the parameter that
specifies the number of grating spatial cycles to display
4. sPer -- the value of the spatial cycles parameter,
stored in the Frontier long integer variable sPer created
above.
5. 'diam' -- the identifier for the parameter that
specifies the grating diameter
6. diam -- the value of the diameter parameter, stored in
the Frontier short integer variable diam created above.
7. 'tcyc' -- the identifier for the parameter that
specifies the number of grating temporal cycles (contrast
reversals) to display
8. tPer -- the value of the temporal cycles parameter,
stored in the Frontier long integer variable tPer created
above.
if not AppleEvent (Stimuli.PVEP.id,'STIM','RnSt','scyc',sPer,'diam',diam,
'tcyc',tPer)
If not successful, clean up and warn user with beep
delete (@scratchpad.scriptArgument)
speaker.beep ()
return (false)
Clean up
delete (@scratchpad.scriptArgument)
return (true)
The program first extracts each individual stimulus parameter contained in the script argument from a string in the scratchpad. Local variables are created in the form of named variables of specific types. The first such variable, named sPer, is a long integer to hold the first script argument, the desired grating spatial period. The grating diameter is the second of these arguments, and the grating reversal period is the third. With the stimulus parameters retrieved and separated, they can be packaged into individual Apple event parameters to be transmitted to the counterphase sinewave grating display program. Frontier's Apple event command does this for us. The Apple event command is passed the application signature of the grating stimulus program, stored in the Object Database location Stimuli.PVEP.id. The signature for the C++ sinewave grating display program is 'SinG'. Following this is the sinewave grating display program's Apple event Suite; that is, an identifier for a group of Apple events specific to that program. We use the Apple event Suite identifier 'STIM'. The following identifier is that of the sinewave grating display program's runStimulus Apple event. This identifier allows the display program to recognize the command to display a sinewave grating stimulus. All that remains is to pass the individual Apple event parameters that define the desired appearance of the sinewave. These form the three remaining pairs of arguments to the Apple event command. Each pair consists of an Apple event parameter identifier for the stimulus parameter to be used when the stimulus program decodes the Apple event, and the value of the parameter. For example, the grating spatial period is given the Apple event parameter identifier 'scyc' and the value of the Frontier sPer variable follows it.
When the Frontier Apple event command is executed, Frontier transmits a raw Apple event that will be received by the stimulus display program. This forms the last step in the chain of message-passing from LabVIEW, to Frontier on the recording computer, to the second copy of Frontier on the stimulus computer, to the stimulus display program. It is now the job of that program to decipher the incoming Apple event to determine what the stimulus display program has been asked to do by LabVIEW. In other words, the display program need not "understand" the scripting code output by LabVIEW, nor the scripts transmitted by Frontier. Such code is difficult to decipher by C++ programs. Rather, it need only understand very basic, low-level Apple events, which require a minimum of programming to decode. Our Frontier programming has allowed us to simplify our programming task for each stimulus display program to the point that each program needs only be a small skeleton program written in C++ consisting solely of Apple event-handling and stimulus generation graphics code.
The Apple event handler called when the 'RnSt' Apple event is received is shown in Listing 5. Before this code is examined, the code to set up Apple event handling must be explained. Prior to entering the event loop of the stimulus display program, the means by which the program receives high-level events like Apple events, the Apple event handler must be installed with a call to the InitAEStuff function of Listing 5.
Listing 5: AppleEvents.cp
AppleEvents.cp
// C++ code in the stimulus display program that receives and
// handles the Apple event transmitted by the runStim
// Frontier script of Listing 4. This Apple event handler
// parses the stimulus parameters from the Apple event, then
// calls the DoAETrial function to display the stimulus
// (a sinewave grating in this case).
// Each stimulus parameter has its own Apple event keyword
// identifier to allow easy extraction from the Apple event.
// © 1997 by Scott B. Steinman. All rights reserved.
#include AppleEvents.h
#include EPPC.h
#include GestaltEqu.h
#include PPCToolbox.h
#include Processes.h
static ProcessSerialNumber gPSN;
void InitAEStuff( void )
{
static AEinstalls HandlersToInstall[] = {
// Required Apple events (not shown),
// plus our custom Apple event.
{ 'STIM', 'RnSt',
(AEEventHandlerUPP) AERunStimulusHandler }
};
OSErr aevtErr = noErr;
long aLong = 0;
Boolean gHasAppleEvents = false;
// Get process serial number of this program.
aevtErr = GetCurrentProcess( &gPSN );
// Check machine for ability to handle Apple events.
// If not present (ie, not System 7.0 or above), exit.
gHasAppleEvents =
(Gestalt( gestaltAppleEventsAttr, &aLong ) == noErr);
// Installs our Apple event Handler. Whenever an Apple
// event is received and we call AEProcessEvent, the
// Apple event manager will check our list of handlers
// and dispatch to our custom Apple event handler, if it
// exists.
if ( gHasAppleEvents ) {
// Required Apple events would also be installed here...
aevtErr = AEInstallEventHandler( 'STIM', 'RnSt',
(AEEventHandlerUPP) AERunStimulusHandler, 0, false );
if ( aevtErr )
ExitToShell(); // Just abort program if error occurs
AESetInteractionAllowed( gInteractNow );
} else
ExitToShell();
}
// Apple event handler called when Apple event with
// identifier of 'RnSt' in suite 'STIM' is received.
pascal OSErr
AERunStimulusHandler( AppleEvent *messagein,
AppleEvent *reply, long /* refIn */ )
{
DescType returnedType;
Size actualSize;
OSErr error;
// Bring stimulus display window to front
error = SetFrontProcess( &gPSN );
error = AEInteractWithUser( kAEDefaultTimeout, 0,
(AEIdleUPP) idleProc );
if (error != errAENoUserInteraction) {
// Extract stimulus parameters from incoming 'RnSt'
// Apple event
// Stimulus spatial period
error = AEGetParamPtr( messagein, (AEKeyword) 'scyc',
typeLongInteger, &returnedType, (Ptr) &spatialPeriod,
sizeof( spatialPeriod ), &actualSize );
if (error) return( -1111 ); // Our own unique error code
// Stimulus grating diameter
error = AEGetParamPtr( messagein, (AEKeyword) 'diam',
typeShortInteger, &returnedType, (Ptr) &diameter,
sizeof( diameter ), &actualSize );
if (error) return( -2222 );
// Stimulus temporal period
error = AEGetParamPtr( messagein, (AEKeyword) 'tcyc',
typeLongInteger, &returnedType, (Ptr) &cycleVBlanks,
sizeof( cycleVBlanks ), &actualSize );
if (error) return( -3333 );
DoAETrial();
}
else
return( -1 );
return( noErr );
}
void DoAETrial( void )
{
// PlayGrating waits for digital signal before displaying
// sinewave grating visual stimulus.
PlayGrating();
}
InitAEStuff first determines the process serial number of this stimulus display program. The process serial number is a unique identifier for each program or task currently executing on the computer. The GetCurrentProcess Toolbox routine retrieves this serial number for us, and stores it in the global variable gPSN. After confirming with the Gestalt Manager that this machine does in fact support Apple events, we install the Apple event handlers. Here we show only the example handler and omit for the sake of brevity the four required Apple events for opening a program or program file, printing and quitting a program. Finally, we call AESetInteractionAllowed to set the program interaction mode, that is, how the user is permitted to interact with this Apple event "slave" program -- is the program run solely in the background, can it receive mouse clicks from the program user, and must the calling program be on the same computer to allow such interaction? We permit user interaction from calling programs on any computer, as most programs will.
Now we can examine the Apple event handler code itself. The first act of the example AERunStimulusHandler handler is to bring the stimulus graphics display window to the forefront by calling SetFrontProcess with the process serial number of the stimulus program, then we initiate program interaction if it was permitted. At this point, we can retrieve the stimulus parameters contained in the Apple event that will determine the appearance and behavior of the stimulus display. Each parameter is extracted from the Apple event with AEGetParamPtr, which is passed as two of its parameters an identifier or Apple event keyword that determines which parameter is retrieved, and the type of variable the parameter is. In the case of this sample code, the stimulus is a spatial sinewave grating. The first stimulus parameter stored in the Apple event to be retrieved is the spatial period of the sinewave. This parameter's keyword is 'scyc' and its type is a long integer. AEGetParamPtr stores the retrived parameter in the stimulus program's long integer variable spatialPeriod. If any error occurs in this process, the process is aborted. A similar sequence of steps is used to retrieve the grating diameter and counterphase reversal temporal period. If each parameter is extracted successfully, the DoAETrial routine is entered, which is responsible for displaying the grating stimulus. DoAETrial sends a sync signal via the digital I/O line (noted by the Boolean variable stimEnabled) to the recording computer to signal that the stimulus is ready and recording may begin.
One aspect of this inter-program control that has not yet been discussed is the launching and quitting of the stimulus display program prior to and following data acquisition. Frontier simplifies these tasks as well. NetFrontier can instruct the copy of Frontier on the stimulus computer to execute two other Frontier commands: The first is Frontier's launch command, which sends an Apple event via the Finder to launch a program. The form of this command that we use is launch.usingID(Stimuli.PVEP.id). This command finds the application whose signature is stored in a variable called "id" in the Object Database table PVEP within the Stimulus script suite. The second command, to be used after data collection, is Frontier's quit command. This command takes the form core.quit(Stimuli.PVEP.id, "no"), where the second argument is a string stating whether or not a data file is to be saved when the requested program is quit.
The example code demonstrates that Frontier can be used to handle all aspects of the inter-program communication that is at the core of the laboratory electrophysiology software. Frontier launches the stimulus generation programs, signals each to display stimuli with specific stimulus parameters, then forces them to quit when we no longer need them. The sole purpose of the digital I/O lines is to ensure the tight time-locking of stimulus display and data recording.
Conclusions
In the construction of software systems, complex decisions regarding individual aspects of the system design must be made that have serious implications for the design of the remainder of the system. In the present case, the overwhelming need for program execution speed dictated the choice of a dual-computer system and the method of communication between the computers. Such a system could not have been constructed easily without combining the strengths of LabVIEW, Frontier, and C++. The use of LabVIEW shortened the program development cycle considerably. In addition, inter-computer communication of the speed and complexity used here would not have been achieved easily using C++ without the addition of Frontier.
Despite the complexity of this system, the software is easy to operate by the clinician due to the intuitive user interface imposed by LabVIEW's instrument panel paradigm and the operation of all tests from a single program on the recording computer. More importantly, the system design does not sacrifice modularity and expandability. Adding a new test requires adding only a small skeleton C++ stimulus display program, a few LabVIEW VIs to select stimulus parameters, send scripts and record data, and a few Frontier scripts. These code modules are very similar in each of the recording, stimulus and intercommunication programs, in great deal due to a high degree of code reuse in LabVIEW, Frontier and C++ libraries for handling Apple events and generating animated graphics. Much of the task of creating a new set of tests simply involves duplicating, then modifying, existing code.
In the future, it may be possible to remove some of the complexity of the system once it is possible to perform all of the stimulus display and recording tasks on a single dual-monitor dual-processor computer. The inter-program communication could then be restricted to one machine, eliminating the need for NetFrontier -- scripts could be sent from LabVIEW to Frontier and directly translated into Apple events to be received by the stimulus program. Frontier can use the fast Component Manager of Power Macintoshes for Apple event transmission within a single machine, which increases the speed of inter-program communication even more. However, even with the present single-processor technology, we have demonstrated that the creation of powerful, flexible, real-time software is facilitated when it makes use of innovations found on the Macintosh computer. The same programming principles outlined in this paper may be applied to a wide range of applications.
Bibliography and References
- Apple Computer Company, Inside Macintosh: Inter-program Communication, Addison-Wesley.
- Baro, John A. and Hughes, Howard C. "The Display And Animation Of Full-Color Images In Real Time On The Macintosh Computer". Behavioral Research Methods, Instruments and Computers, 23 (1991), pp. 537-545.
- Johnson, Gary W. "LabVIEW Graphical Programming: Practical Applications In Instrumentation And Control". (1994), New York: MacGraw-Hill.
- Steinman, Scott B. "Simple Real-Time Color Frame Animation". MacTech, 9:9 (September 1993), pp. 21-35.
- Steinman, Scott B. "Extendable Real-Time Simultaneous Data Acquisition And Stimulus Generation On The Macintosh Computer". Behavioral Research Methods, Instruments and Computers, In Press-.
- Steinman, Scott B. and Carver, Kevin. "Visual Programming with Prograph CPX(tm)". (1995), Greenwich, Connecticut: Manning Publications / Prentice-Hall.
- Steinman, Scott B. and Nawrot, Mark. "Real-Time Color Frame Animation For Visual Psychophysics On The Macintosh Computer". Behavioral Research Methods, Instruments and Computers, 24 (1992), pp. 439-452.
Dr. Scott Steinman is a vision scientist and Chair of the Department of Biomedical Sciences at the Southern College of Optometry. He develops software for clinical vision testing, research and education. Scott programs in C++, LabVIEW, Frontier, Prograph and Java. He has published numerous articles on computer programming, two of them previously in MacTech. You may have noticed his book, "Visual Programming with Prograph CPX", on sale in the DevDepot (do I have to hint more than this?). You can reach him at steinman@sco.edu.