Control Freaks Alert
Volume Number: 16 (2000)
Issue Number: 3
Column Tag: Data Acquisition on the Macintosh
Control Freaks Alert!
by Tom Djajadiningrat
A clean interface to Beehive Technologies' ADB I/O using object-oriented Lingo
Introduction
The ADB I/O by Beehive Technologies is a device which you can connect to a Macintosh to communicate with external electronics. Simply put, the ADB I/O provides digital inputs (to read switches), digital outputs (to switch things on or off) and analog inputs with 8-bit resolution (to read all kinds of sensors). For the Macintosh platform, it has opened up areas of application to mainstream users that were previously the sole territory of electronic hobbyists or users with high end data acquisition hardware. A few of those areas are home automation, robotics, kiosks, scientific experiments and product simulation.
Many applications can communicate with the ADB I/O. One particularly powerful combination is the ADB I/O and Macromedia Director. Even if you intend to control your ADB I/O through AppleEvents or C/C++, you may find it worthwhile to test your set-up with Director. Director is easy to program, especially when it comes to the graphical user interface.
There is of course a downside to Director's ease of use: the temptation of sloppy programming. Director does not require you to declare local variables or provide function prototypes. Add Director's cryptic error messages and your program can turn into a tangled mess which is difficult to debug. While this is of course undesirable for any program, with a program that communicates with external devices, for example motors, the consequences of messy programming may be more severe.
This article aims to help you in battling messy Director code. By using Director's object-oriented features you will get to build a friendly and clean interface to the ADB I/O. In addition, in this article consistent naming for variables is used, which may be of help to you in writing your own scripts.
Required Hardware and Software
Required hardware:
- A Macintosh with an ADB port or a Macintosh with a USB port and a Griffin Technology iMate USB to ADB adapter.
- A Beehive Technologies' ADB I/O.
Required software:
- ADB I/O XCMDs 1.0.1. This file comes with the ADB I/O. It is also downloadable from the Beehive Technologies website.
- Macromedia Director 5 or 6 (Director 7 is not compatible with the ADB I/O XCMDs 1.01 file. A Director 7 compatible Xtra for the ADB I/O is currently in beta and downloadable from the Beehive Technologies website). Though this article explains things for Director 6, the differences with Director 5 are minimal and the sample files which accompany this article are provided in both Director 5 and 6 versions.
What You Need to Know
You need to be familiar with Macromedia Director and its scripting language Lingo. You need not have worked with object-oriented Lingo. What you need to know about object-oriented Lingo will be explained. You should be familiar with the capabilities of the ADB I/O, though a summary of what you need to know is provided.
Contents
We will start with an introduction to object-oriented Lingo. We will then cover the ADB I/O's hardware and software details relevant to this article. If you are already comfortable with these subjects, you may wish to skip them. Once we have established this ground, we start with building a Director Lingo object which forms a friendly and clean interface to the ADB I/O. At the end of this article you will find a section on troubleshooting and a section with suggestions for enhancements.
Object-Oriented Lingo
Instead of starting with a theoretical discussion of the advantages of object-oriented Lingo, we will dive straight in and put together a simple object-oriented Director movie. I think you will find that the virtues of object-oriented Lingo are much easier to understand once you have seen a concrete example. Our movie consists of three scripts: a parent script, a movie script and a frame script. First we will write the scripts, then we will test and discuss the resulting movie. Don't worry too much if you don't completely 'get it' when we write the scripts. All will be explained.
The parent script
A Director object is described in a parent script. Create a new movie first, then create a parent script by choosing Script from the Window menu (Figure 1). The script window opens. Name the script by typing testParentScript in the field at the top of the scripting window (Figure 2). To make sure that the script is a parent script, click on the info button (Figure 3) and choose parent from the type pop-up menu (Figure 4). Click OK.
Figure 1.Open the script window
Figure 2.Name the script
Figure 3.Press the info button
Figure 4.Make the script a parent script.
Now on to the parent script itself. It consists of three parts: a birthing handler, message handlers and properties. We will discuss each part in turn.
1. The birthing handler
This is where a child object is created. It typically has the form:
on new me
return me
end
The birthing handler returns a pointer to a newly created child object. We will add one line of code to our new handler to put a message in the message box telling us that the birthing handler was indeed called.
on new me
- print some feedback in the message window
put "the birthing handler of testParentScript was called"
- return a pointer to the new child object
return me
end
2. Message handler
In a message handler you can specify a behaviour of an object. A message handler has the form:
on messageHandler me
end
We will create an message handler called hello which beeps and puts a message in the message box.
on hello me
- beep once
beep
- print some feedback in the message window
put "Hello World!"
end
3. Properties
A property is a variable that belongs to an object. It is declared at the top of an object's parent script, preceded by the Lingo keyword property, like this:
property pProperty
Within the parent script you can set or get a property by just its name:
set pProperty = 1
set lVariable = pProperty
When we test the Director movie, you will see how to get at a property from outside of the parent script. Notice how, to maintain an overview, we start a property with the letter p and a local variable with the letter l. For a full description of our naming convention for variables and parameters, please refer to Table 1.
Prefix |
Suffix |
Meaning |
Example |
i |
|
input parameter |
iChannel |
l |
|
local variable |
lResult |
o |
|
child object |
oADB1 |
p |
|
property variable |
pUnit |
|
List |
list variable |
pChannel |
Table 1.Variable and parameter naming conventions.
For now, we will create a single property called pProperty and two member functions. One to set the value of pProperty and one to get the value of pProperty.
property pProperty
on setProperty me, iValue
set pProperty = iValue
end
on getProperty me
return pProperty
end
The movie script
To create the movie script, bring up the script window if necessary and click on the + button to create a new script. Name the script movieScript by typing into the field at the top of the script window. Click on the info button. A dialog box appears. Make sure that movie is selected in the type popup-menu. Click OK.
In our movie script we will write two handlers. The first one, startMovie, is called when the movie starts, the second one, stopMovie, is called when the movie stops.
on startMovie
end
on stopMovie
end
In this example we will add some code to the startMovie handler only. First we print some feedback to the message window to show that the startMovie handler was called. We then create a global called oTestObject1. Finally, we assign to this global a child object of the testParentScript that we wrote previously by calling the birthing handler.
on startMovie
- Print some feedback in the message window
put "startMovie was called."
- create a global oTestObject1
global oTestObject1
- make oTestObject1 a child object of the parentScript testParentScript
- by calling the birthing handler.
set oTestObject1 = new (script "testParentScript")
end
The frame script
We will now create a simple frame script to make our Director movie loop in frame 1. In the score window, double click on the score channel of frame 1 and add one line of code to the exitFrame handler.
on exitFrame
go to the frame
end
Testing our movie
It is time to test our movie. If you get stuck at any point and think it is because you did not quite get the explanation above, you may wish to look at testOOMovie.dir. Run the movie and open the message box. Type
put oTestObject1
The message box should display
- <offspring "testParentScript" 2 876a72a>
indicating that oTestObject1 is a child object based on our testParentScript. Don't worry if the alpha-numeric string that you get differs from the one shown above. As this is a memory address it is in fact highly unlikely that it would be identical.
Now try the hello message handler. The syntax for sending a message to a child object is <messageHandler> <childObject>.
hello oTestObject1
The Mac should beep and the message box should display
- "Hello World!"
Now send the getProperty message to our child object. Since getProperty is a function (a handler which returns a value), you need to enclose its parameters in round brackets.
put getProperty (oTestObject1)
The message box shows that pProperty has the value void. No worries, pProperty has not been set yet. Now type:
setProperty oTestObject1, 1
Now try again:
put getProperty (oTestObject1)
And you will see that pProperty has changed its value from void to 1.
setProperty and getProperty are message handlers which we wrote ourselves. Another way to set and get properties of a child object outside of its parent script is:
set the <property> of <childObject> = <value>
set lVariable = the <property> of <childObject>
Notice how we need to add the Lingo 'the' keyword, now that we try to access a property outside of its parent script.
Let's try:
set the pProperty of oTestObject1 = 2
put the pProperty of oTestObject1
The message box shows that pProperty of oTestObject1 now has the value 2.
Play around with accessing properties through the message handlers setProperty and getProperty as well as with accessing the properties directly through the 'the' keyword. You will find that you can mix both methods.
What's the point of object-oriented Lingo?
Before we answer that question, try creating a second child object:
set oTestObject2 = new (script "testParentScript")
The message box responds with
- "new handler of testParentScript was called"
Try entering
put oTestObject2
The message box responds with
- <offspring "testParentScript" 2 876a6ee>
Notice how the address of oTestObject2 differs from the address of oTestObject1. The values of the properties are also individual to each child object. Try
setProperty oTestObject1, 7
setProperty oTestObject2, 666
Now type:
put getProperty (oTestObject1)
The message box shows that the property pProperty is now 7.
And try:
put getProperty (oTestObject2)
This time the message box responds that the property pProperty equals 666. That is pretty kewl: both child objects have a property called pProperty but each can hold a different value.
This example illustrates the three characteristics of object-oriented Lingo that we use in this article. The first is the ability to create multiple child objects based on the same parent script, which are all capable of listening to certain message through the message handlers defined in the parent script. The second characteristic is the ability to hide code - in our case a simple put statement - within the birthing handler of a parent script. This code is automatically executed when a child object is created. The third characteristic is the ability to assign individual property values to child objects. While these characteristics may seem trivial, we can use them to great effect in improving the cleanliness of our code. By hiding set-up details in the birthing handler, these details are prevented from littering your main program. Through the use of properties, variables can be encapsulated within an object. This greatly reduces the need for global variables. Global variables are a major source of bugs in Director because each time you forget to declare a variable as a global, Director will assume you are creating a new, local variable.
You have now seen enough of object-oriented Lingo to be able to understand the ADB I/O parent script that we will build later on. However, there is a lot more to object-oriented Lingo than you have seen so far. To learn more, you can have a look at Director's on-line help under parent script or have a look at Small (1999).
If you are familiar with some other object-oriented language such as C++ or Java, you will probably have worked out the relationship between Lingo and general object-oriented terminology by now. A parent script is a combination of a source and a header file in which you define an object, a birthing handler is a constructor, a message handler is the equivalent of a member function, a property is a data member and a child object is an instance.
The ADB I/O Hardware
Units
You can have a maximum of four ADB I/O units daisy chained to your Mac. Each ADB I/O needs to be set to its own unique ID number.
Ports
The ADB I/O has two ports, called port A and port B. Each port has four channels. The channels of port A and B differ in their capabilities.
Port A
Each of the four channels of port A can be configured as either a digital input or a digital output. Analog input is not possible on port A. Each channel can have either a relay or an opto-isolator installed. The ADB I/O comes with relays pre-installed on all channels of port A, making them suitable for digital output. If you wish to use any of these channels for digital input, you will need to replace a channel's relay by an opto-isolator. Through the use of relays or opto-isolators, external electronics are electrically isolated from the ADB I/O.
Port B
port B allows not only digital input and digital output, but also analog input and an analog reference voltage. However, you cannot freely mix all four channel types. Only the following configurations are allowed.
analog in, analog in, analog in, analog in
analog in, analog in, analog in, reference
analog in, analog in, digital in/out, digital in/out
digital in/out, digital in/out, digital in/out, digital in/out
Each channel of port B is equipped with a jumper. A jumper can be set to pull up, pull down or open. For digital input, the jumper needs to be set to either pull-up or pull-down. Note that to switch a channel from digital to analog, you not only need to change the configuration of port B through software, but you also need to move the jumpers.
The ADB I/O Software
To use the ADB I/O with Director 5 or 6, XCMDs are used. These come in a file called ADBIO XCMDs 1.01. You need to make sure that this file resides in the same folder as your Director movie. We can have a look at the functionality offered by the ADBIO XCMDs 1.01 file by creating a simple Director movie (If you get stuck during the creation of this movie you can take a peek at testXCMDs.dir). Start a new movie, create a movie script and add startMovie and stopMovie handlers like this:
on startMovie
openXLib "ADB I/O XCMDs 1.0.1"
end
on stopMovie
closeXLib "ADB I/O XCMDs 1.0.1"
end
Add a frame script in the score channel of frame 1 to loop the movie. Run the movie and open the message box. Now type
showXLib
A slew of Xtras, XCMDs and XFCNs show up. We are only interested in the ones that concern the ADBIO:
- "ADB I/O XCMDs 1.0.1"
- XCMD: Flash Id:0
- XCMD: Picture Id:4
- XCMD: Palette Id:5
- XCMD: ADBReInit Id:6701
- XCMD: CopyRes Id:1077
- XCMD: ConfigureADBIO Id:1200
- XCMD: SetADBIO Id:1203
- XFCN: GetADBIO Id:1201
For now let's look at ConfigureADBIO, SetADBIO and GetADBIO. Here only a short description of these calls is given. For a full description please refer to the ADB I/O manual.
ConfigureADBIO
- ConfigureADBIO has the syntax:
ConfigureADBIO <Unit> ,<port>,¬
<Channel1Type>, <Channel2Type>,¬
<Channel3Type>, <Channel4Type>
- What does it do? - With ConfigureADBIO you specify what function the four channels of a particular port of a particular ADB I/O unit should fulfill.
- Parameters - The first parameter, Unit, is used to address a particular ADB I/O unit and can range from 1 to 4. The second parameter, port, is used to address either port A or port B of an ADB I/O. The remaining parameters specify what channel type you wish to assign to each channel of the port. Keep in mind that the capabilities of the ports differ. If you call ConfigureADBIO with unallowed channel types it will return an error message.
- Return values - ConfigureADBIO returns either 0 (success) or an error string (failure).
SetADBIO
- SetADBIO has the syntax:
SetADBIO <Unit>, <Port>, <Channel>¬
<Number> [,<'high'|'low'>] [,<Time>]
- What does it do? - You can think of this call as three separate calls. All three versions apply to digital outputs only. The first version sets a channel of a port of a unit to high or low:
SetADBIO <Unit>, <Port>, <Channel>, <'high'|'low'>
The second sets a channel of a port of a unit to high or low for a certain duration, after which it returns to the opposite state:
SetADBIO <Unit>, <Port>, <Channel>, <'high"|'low'>, <Time>
The third sets a port of a unit in such a way that the values of its channels form the binary equivalent of a decimal number:
SetADBIO <Unit>, <Port>, <Number>
- Parameters - You already know the meaning of the Unit and Port parameters. Channel specifies the number of a channel and should therefore be in the range 1-4. 'high' means relay actived on port A and +5V on port B, 'low' means relay non-active on port A and 0V on port B. Time specifies a duration from 0-25.5 s in 0.1s increments through a integer ranging from 0 to 255. Number ranges from 0 (all channels 'low') to 15 (all channels 'high').
- Return values - SetADBIO returns either 0 (success) or an error string (failure).
GetADBIO
GetADBIO(<Unit>, <port>[,'Number'])
- What does it do? - You can think of this call as two separate calls. The first version:
GetADBIO(<Unit>, <Port>)
returns a channel record with the channel type and channel value for each channel of a port:
<Channel1Type>,<Channel1Value>
<Channel2Type>,<Channel2Value>
<Channel3Type>,<Channel3Value>
<Channel4Type>,<Channel4Value>
The second version:
GetADBIO(<Unit>, <port>, <"Number">)
returns the decimal equivalent of the binary pattern on the channels of a port.
Return values - GetADBIO returns four lines of channel type and channel value combinations (success) or a decimal number in the range 0-15 (success) or an error string (failure).
Trying it out
Make sure you have at least one ADB I/O unit hooked up to your Mac, run the Director movie you just created and open the message box (Note that according to the ADB I/O manual, your Macintosh should be turned off before you attach or disconnect an ADB I/O). Play about with the three main calls ConfigureADIO, SetADBIO and GetADBIO. For each of these calls, make sure you type the Director keyword put in front of it and that you enclose the call's parameters in brackets so that you get to see the return value in the message box. For example, type:
put configureADBIO (1, "A",¬
"digital out", "digital out",¬
"digital out", "digital out")
No error should occur and the XCMD should answer with 0.
Now be evil and force some error messages, for example:
put ConfigureADBIO 1, "A",¬
"analog in", "analog in",¬
"analog in", "analog in"
The return value is an error message:
- "Error : Wrong combination of ChannelType parameters"
Clearly, the XCMD is not happy about analog in on port A because only digital in and out are allowed. Let's try something else:
put SetADBIO 1, "A", 5, "high"
Again the return value is an error message:
- "Error : Invalid Channel parameter"
This time the XCMD is not happy because there are only four channels on each port. Play about a bit more. Don't worry about the channel values in the channel record returned by GetADBIO. For the moment no external electronics need to be attached to the ADB I/O, so never mind the values of the channels and ports. Just try to get a general feel for the required parameters and for the return values of the three handlers. Notice how the error messages returned by the three handlers are actually pretty specific. We will make use of this when we build our Director object to control the ADB I/O.
A Director Parent Script for the ADB I/O
Now that we have covered the basics of both object-oriented Lingo and the ADB I/O, we can write a parent script to control the ADB I/O. The parent script is called ADB I/O and forms part of testADBI/OMovie.dir included with the accompanying code. First we discuss the handlers concerning initialization and configuration, then those for input and then those for output. Finally we try out the whole parent script.
Initialization and configuration
Listing 1 shows the parent script's properties, a birthing handler and a configurePort handler.
Listing 1
properties
property pDEBUGMessage, pUnit
property pChannelTypesAList, pChannelTypesBList
property pChannelValuesAList, pChannelValuesBList
new
This handler creates a child object and hides set-up details.
Example: set oADB1 = new (script "ADB I/O", 1, TRUE)
Possible return values: An object address (success) or 0 (failure).
on new me, iUnit, iDEBUGMessage
- Requested error reporting.
set pDEBUGMessage = iDEBUGMessage
- We are working with an object per ADB I/O unit.
set pUnit = iUnit
- These two lists hold the channel types
- for port A and port B.
set pChannelTypesAList = []
set pChannelTypesBList = []
- These two lists hold the channel values
- for port A and port B.
set pChannelValuesAList = []
set pChannelValuesBList = []
- The default initialization of the hardware is:
- "digital out" for all channels of port A and
- "digital in" for all channels of port B.
- Call ConfigureADBIO for both port A and port B
- with this default initialization.
- According to the ADB I/O manual this is good practice.
- The settings can always be changed later.
- Initialize port A
- Unit pUnit, port A, all channels digital out
set lResult = configurePort(me, "A",¬"digital out", "digital out",¬
"digital out", "digital out")
if (lResult <> 0) then
if pDEBUGMessage = TRUE then put lResult
return 0
end if
- Initialize port B
- Unit pUnit, port B, all channels digital in
set lResult = configurePort(me, "B",¬ "digital in", "digital in",¬
"digital in", "digital in")
if (lResult <> 0) then
if pDEBUGMessage = TRUE then put lResult
return 0
end if
return me
end
configurePort
This handler configures a port.
Example:
configurePort (oADB1, "A", "digital out", "digital out",¬
"digital out", "digital out")
Possible return values:
Empty string "" (success) or an error string (failure).
on configurePort me, iPort, iChannel1Type, iChannel2Type,¬
iChannel3Type, iChannel4Type
- ConfigureADBIO returns either an empty string
- "" (success) or an error string (failure).
put configureADBIO (pUnit, iPort,¬
iChannel1Type, iChannel2Type,¬
iChannel3Type, iChannel4Type)¬
into lResult
if lResult = 0 then
case iPort of
"A": set pChannelTypesAList =¬
[ iChannel1Type, iChannel2Type,¬
iChannel3Type, iChannel4Type]¬
"B": set pChannelTypesBList =¬
[ iChannel1Type, iChannel2Type,¬
iChannel3Type, iChannel4Type]
end case
else
- An error occurred
if (pDEBUGMessage=true) then put¬
"ConfigureADBIO for port" && iPort && "of unit"¬
&& pUnit && "failed."
end if
return lResult
end
Starting with the birthing handler, we come across the parent script's properties straight away. pUnit holds an integer with the ID number of an ADB I/O device which we pass into the birthing handler through the parameter iUnit.
pDEBUGMessage is a boolean property which we can turn on to get a bit more debugging info in the message window. We can turn it on through the birthing handler's parameter iDEBUGMessage. The chosen approach to debug messages is to report nothing while there are no errors. Only if there is an error will a debug message be printed to the message box. This is because writing to the message box considerably slows down a Director movie. If there are no errors we want to avoid any slow downs as they may cause timing problems.
Next is a set of four lists. pChannelTypesAList is a list which holds the channel types for port A. Similarly, pChannelTypesBList holds the channel types for port B. pChannelValuesAList is a list with the channel values of port A. Finally, pChannelValuesBList is a list with the channel values of port B. All lists are initialized as empty lists.
We use the birthing handler to hide some of the port set-up details of the ADB I/O. The default hardware configuration is all channels of port A configured as digital outputs and all channels of port B configured as digital inputs. We match the default hardware configuration by calling the message handler configurePort for port A and port B. If you look at configurePort you see that it calls ConfigureADBIO and that if it succeeds it sets either the property pChannelTypesAList or pChannelTypesBList to the requested channel types. If you need other settings for either port A or B you can call always call configurePort with the desired channel types after the child object has been created.
Note the flow of error messages: if ConfigureADBIO returns an error message, then configurePort returns an error message and the birthing handler returns 0. Also note the use of the property pDEBUGMessage. If it is true we print some error messages to the message window. ConfigureADBIO is pretty important. During debugging we need to know if it fails and why.
Reading from inputs
We will now look at the calls for getting information from channels and ports. There are four calls: getChannelDigital, getChannelAnalog, getPort and getPortDigital. Let's start with getChannelDigital (Listing 2).
Listing 2
getChannelDigital
This handler returns the value of a digital input channel.
Example: getChannelDigital (oADB1, "A", 1)
Possible return values: 0 (low), 1 (high) or an error string (failure).
on getChannelDigital me, iPort, iChannel
- Make sure iChannel is within range 1-4.
if iChannel<1 OR iChannel>4 then
set lResult = "Error : Invalid Channel parameter"
return lResult
end if
- Get the channel types and values for this port.
put GetADBIO(pUnit, iPort) into lResult
if word 1 of lResult = "Error" then
- GetADBIO returned an error.
return lResult
else
- GetADBIO returned no error.
- Extract the relevant line for the desired channel.
put line iChannel of lResult into lThisChannelString
- Check whether whether our channel is a digital input
- that is high, low, or not a digital input at all.
case lThisChannelString of
"digital in,high":
- This is a digital input and it is high.
return 1
"digital in,low":
- This is a digital input and it is low.
return 0
otherwise:
- This channel does not allow digital input.
set lResult = "Error: Channel" && iChannel &&¬
"of port" && iPort && "of unit" &&¬
pUnit && "is not a digital input."
return lResult
end case
end if
end
If you just want to look at a single digital input channel, getChannelDigital is the way to do it. First it makes sure iChannel is within the range 1-4, then it calls GetADBIO to retrieve an up-to-date channel record for the port you specify in the iPort parameter. Based on the iChannel parameter it extracts the relevant line from the channel record. It then determines whether the channel is high, low or not a digital channel at all. If the channel is high, getChannelDigital returns 1, if it is low it returns 0. If the channel is not a digital input channel, getChannelDigital returns an error message.
The next call we examine is getChannelAnalog (Listing 3).
Listing 3
getChannelAnalog
This handler returns the value of an analog input channel. Since analog input is only possible on port B, we do not need to pass the port as a parameter.
Example: getChannelAnalog (oADB1, 1)
Possible return values: 0-255 (success) or an error string (failure).
on getChannelAnalog me, iChannel
- Make sure iChannel is within range 1-4.
if iChannel<1 OR iChannel>4 then
set lResult = "Error : Invalid Channel parameter"
return lResult
end if
put GetADBIO(pUnit, "B") into lResult
if word 1 of lResult = "Error" then
- GetADBIO returned an error.
return lResult
else
- GetADBIO returned no error.
- Extract the relevant line for the desired channel.
put line iChannel of lResult into lThisChannelString
- Check whether analog in is allowed on this channel.
if lThisChannelString contains "analog in" then
- This channel does allow analog input.
- Extract its value.
put char 11 to length(lThisChannelString)¬
of lThisChannelString into lValueString
return value(lValueString)
else
- This channel does not allow analog input.
set lResult = "Error : Channel" && iChannel &&¬
"of port B of unit" && pUnit &&¬
"is not a analog input."
return lResult
end if
end if
end
getChannelAnalog is the analog equivalent of getChannelDigital: it is the way to go if you need to monitor a single analog input channel. First it makes sure the parameter iChannel is within the range 1-4, then it calls GetADBIO to retrieve an up-to-date channel record for the port you specify in the iPort parameter. Based on the iChannel parameter it extracts the relevant line from the channel record. It then determines whether the channel allows analog input. If the channel is an analog input getChannelAnalog extracts the channel value. If the channel is not a analog input channel getChannelAnalog returns an error message.
But what if you need to monitor multiple channels? Calling getChannelDigital or getChannelAnalog repeatedly is not very efficient, as each time you call them, you in fact call GetADBIO which returns a channel record with the values of all the channels of a port. Reading from or writing to a port too often can make other input devices on the ADB bus act sluggishly (Beehive Technologies recommends not to read more than 12 times per second and not to write more than 6 times per second). A more efficient way to look at multiple channels is by means of getPort (Listing 4).
Listing 4
getPort
This handler gets both the channel types and the channel values of the
inputs of a particular port. The channel types are stored in
pChannelTypesAList or pChannelTypesBList, depending on the port. The
channel values are stored in pChannelValuesAList or pChannelValuesBList,
depending on the port.
Example: getPort (oADB1, "A")
Possible return values: 0 (success) or an error string (failure).
on getPort me, iPort
set lResult = GetADBIO (pUnit, iPort)
if word 1 of lResult = "Error" then
- GetADBIO returned an error.
return lResult
else
- GetADBIO returned no error.
- Work out the channel value and the channel type.
repeat with lChannel = 1 to 4
set lThisChannelString = line lChannel of lResult
case (lThisChannelString) of
"digital in,low":
set lChannelType = "digital in"
set lChannelValue = 0
"digital in,high":
set lChannelType = "digital in"
set lChannelValue = 1
"digital out,low":
set lChannelType = "digital out"
set lChannelValue = 0
"digital out,high":
set lChannelType = "digital out"
set lChannelValue = 1
"analog ref,255":
set lChannelType = "analog ref"
set lChannelValue = 255
otherwise:
set lChannelType = "analog in"
put char 11 to length(lThisChannelString) of¬
lThisChannelString into lChannelValueString
set lChannelValue = value(lChannelValueString)
end case
- Store the channel type and channel value in
- their respective arrays.
if (iPort = "A") then
setAt pChannelTypesAList, lChannel, lChannelType
setAt pChannelValuesAList, lChannel, lChannelValue
else
setAt pChannelTypesBList, lChannel, lChannelType
setAt pChannelValuesBList, lChannel, lChannelValue
end if
end repeat
return 0
end if
end
In getPort we finally make use of the properties pChannelValuesAList and pChannelValuesBList. getPort calls GetADBIO for the desired port. If GetADBIO fails, getPort returns an error message. If getPort executes successfully and returns a channel record, getPort looks at each line of the channel record, puts the channel type in either pChannelTypesAList or pChannelTypesBList, and the channel value in either pChannelValuesAList or pChannelValuesBList. In the process the values of the input channels are converted to integers: 0 or 1 for a digital input and 0-255 for an analog input. After calling getPort you can easily retrieve an input channel's value by using the standard Lingo list access call getAt. You will see how to use the getPort-getAt combo in more detail when we put the parent script into action.
We don't discuss GetPortDigital in detail here. If you look at the code, you will see that it is simply a wrapper for GetADBIO with use of the number parameter. It reads all four channels of a port, converts the binary pattern on the port to a decimal number and returns this number.
Writing to outputs
There are three calls for setting output channels and ports: setChannelDigital, setChannelDigitalDuration and setPortDigital. Let's start with setChannelDigital (Listing 5).
Listing 5
setChannelDigital
This handler sets a digital output channel high or low.
Example: setChannelDigital (oADB1, "B", 1, 1)
Possible return values:
0 (success) or an error string (failure)
on setChannelDigital me, iPort, iChannel, iChannelValue
- Check whether the requested channel value is allowed.
if iChannelValue<>0 AND iChannelValue<>1 then
- The requested value is not allowed.
set lResult = "Error: channel value must be 0 or 1"
return lResult
end if
- Try setting the channel to the requested value.
if iChannelValue=0 then
set lResult = SetADBIO(pUnit, iPort, iChannel, "low")
else
set lResult = SetADBIO(pUnit, iPort, iChannel, "high")
end if
return lResult
end
setChannelDigital is a convenient way of setting a single digital output channel. First it checks whether the requested channel value is 0 or 1. If the channel value is not 0 or 1, setChannelDigital returns an error. Otherwise it uses a call to SetADBIO to try to fulfill the request.
We are not going to cover setChannelDigitalDuration or setPortDigital in detail. If you look at the accompanying code, you will see that setChannelDigitalDuration is similar to setChannelDigital. The main difference is that the former uses the Time parameter in SetADBIO to set a digital output channel for a certain duration. This feature is available on port A only. With SetPortDigital you can set a whole port in one go, providing all the channels are digital outputs.
Taking It All for a Spin
Ok, let's try to put our parent script to the test. If you get stuck in this section, you may wish to peek ahead at the trouble shooting section towards the end of this article.
External electronics
To test the parent script you need to hook up some external electronics to the ADB I/O (Note that according to the ADB I/O manual, your Macintosh should be turned off before you attach or disconnect electrical devices from the ADB I/O). Connect an LED to each channel of port A. Hook up potentiometers to channel 1 and channel 2 of port B. Connect an LED to channel 3 of port B. Finally, connect a push-to-make switch to channel 4 of port B, which connects the channel to ground when you press the switch. If you feel unsure about how to connect these components, have a look at the application notes on the Beehive Technologies website.
Initialization and configuration
Have a look at the startMovie handler in the movie script of testADBIOMovie.dir. There are only three lines of code:
global oADB1
openXLib "ADB I/O XCMDs 1.0.1"
set oADB1 = new(script "ADB I/O", 1, TRUE)
The three lines should look familiar to you from the testOOMovie.dir and the testXCMDsMovie.dir. The first line creates a global variable called oADB1. The second opens the library with the XCMDs. The third line assigns to the global variable oADB1 a child object based on the ADB I/O parent script by calling the birthing handler. As iUnit parameter we pass 1 (the unit ID number) and as iDEBUGMessage we pass true.
Before we start playing around with the handlers remember that we took the pessimistic approach to debugging messages: we only get to see bad news. No message is good news. Now open the message box, delete any text that litters it, and play the movie. If the message box remains empty that is good news: the birthing handler and the embedded calls to configurePort for port A and port B executed successfully. If you do get an error message you should be able to figure out what is wrong. After all, we carefully passed back the original error message from ConfigureADBIO.
We can still get some kind of confirmation that the birthing handler executed successfully. Let's look at the channel types of port A by typing:
put the pChannelTypesAList of oADB1
The message box responds with:
- [ "digital out", "digital out", "digital out",¬
"digital out"]
That is good. It is default hardware configuration for port A which we requested in the birthing handler. Now look at the channel types of port B. Type:
put the pChannelTypesBList of oADB1
- [ "digital in", "digital in", "digital in",¬
"digital in"]
Again, that is what we expected. It is the default hardware configuration for port B which we requested in the birthing handler.
From the electronic components you can infer how we want the ports to be configured. All channels on port A need to be digital outputs. On port B, channels 1 and 2 need to be analog inputs, channel 3 needs to be a digital output and channel 4 a digital input. Since the desired configuration of port A matches the default configuration we do not need to reconfigure port A. The desired configuration of port B differs from the default configuration so we call:
put configurePort (oADB1, "B", "analog in", "analog in",¬
"digital out", "digital in")
The message box should respond with 0 indicating that no errors occurred.
Reading from inputs
We will cover the remaining handlers in the order they occur in the parent script. Let's try reading channel 4 of port B, a digital input.
put getChannelDigital (oADB1, "B", 4)
Providing the jumper on this channel is set to pull-up and you use a push-to-make switch connected to ground, getChannelDigital returns 1 with the switch open and 0 with the switch closed.
Next we try reading an analog input, channel 1 of port B.
put getChannelAnalog (oADB1, 1)
getChannelAnalog should return a value. Now turn the potentiometer connected to this channel and call getChannelAnalog again. Notice how the value of the channel changes. Now try getChannelAnalog with channel 2 of port B. It should behave in the same way.
The last call for reading inputs is GetPort. Let's call it for port B:
put GetPort (oADB1, "B")
Remember that GetPort stores the channel values of a port in either pChannelValuesAList or pChannelValuesBList. Let's have a look at the property pChannelValuesBList.
put the pChannelValuesBList of oADB1
The message box shows a list with four values which correspond with the four channels of port B. We can simply get at each entry in the list through the use of getAt.
put getAt (the pChannelValuesBList of oADB1, 1)
gives us the value of the analog input channel 1 of port B. Likewise,
put getAt (the pChannelValuesBList of oADB1, 2)
gives us the value of the analog input channel 2 of port B. And
put getAt (the pChannelValuesBList of oADB1, 4)
gives us the value of the digital input channel 4 of port B.
List access calls to pChannelValuesAList or pChannelValuesBList must be preceded by a call to getPort. If you do not call getPort first, the lists contain old values which may differ from the current values. Think of getPort and getAt as a combo.
The last of the message handlers for reading from the ADB I/O is getPortDigital. With the components we have got hooked up, we can't really try it out in a meaningful way which would be to convert the binary pattern on a port with four digital input channels to a decimal number. What we can do is show its use with the four digital output channels of port A. First set the port with the XCMD SetADBIO, then read it again with getPortDigital.
SetADBIO 1, "A", 15
put getPortDigital (oADB1, "A")
If you play about with these commands, you will find that you can read back from the port the value you just wrote to it. Note that if you use getPortDigital with a port that has one or more channels set to analog in, you will not get an error message. An analog input channel will have the value 0. While that is not useful in itself, it allows you to work out the values of any remaining digital channels.
Writing to outputs
Luckily, writing to channels and ports is a bit easier than reading from them. Let's write to channel 3 of port B, a digital output. Typing
put setChannelDigital (oADB1, "B", 3, 0)
turns off the LED.
put setChannelDigital (oADB1, "B", 3, 1)
turns on the LED. Try using setChannelDigital on port A.
Next, try setChannelDigitalDuration. Remember, this call only works on port A. Let's try it on channel 1 of port A.
put setChannelDigitalDuration (oADB1, 1, 1, 10)
This turns on the LED for 10 x 0.1 = 1 second after which the LED turns off.
Finally, there is setPort for setting a whole port in one go. Enter
put setPort (oADB1, "A", 15)
This turns on all LEDs on port A, since the binary equivalent of the decimal integer 15 is 1111.
Play around a bit more with all these handlers. Try forcing some errors. For example, try writing to an input channel or reading from an output channel. Error messages will inform you if you ask for the impossible.
Trouble Shooting
Strange bugs but no error messages? Or error messages which completely puzzle you? This section lists some possible causes.
Software
- Have you enclosed the parameters in brackets? All our message handlers are in fact functions, that is, they return values. Lingo expects functions to have their parameters enclosed in brackets.
- Do the values you pass into the handler match the handler's function prototype? If they do not, confusing error messages may result.
- Are all child objects of the ADB I/O parent script declared as globals? If you expect a child object to stay around in memory, while it is actually a local child object which goes out of scope at the end of the handler in which it was created, you can get very confusing errors. If in doubt try using put to print the value of the child object to the message box. If you get void you are half way to finding the bug.
Hardware
- Is your port A channel correctly equipped with a relay or an opto-isolator? If you need digital output on a port A channel you need to have a relay installed, if you need digital input you need an opto-isolator installed. Obviously, if you have a relay installed on a channel and try to configure it as a digital input, or have an opto-isolator installed and try to configure it as a digital output, that channel will not work correctly. But as the processor in the ADB I/O cannot distinguish between relays and opto-isolators ConfigureADBIO will not return an error string.
- Is your port B channel jumper correctly set to pull-up, pull-down or open circuit? If you need digital input or digital output on a channel of port B, you need to set the jumper to pull-up or pull-down. If you need analog input you need to disable the jumper (open circuit). If you try analog input with pull-up or pull-down you will get erroneous values, but you will not receive any error message.
Enhancements
To keep the number of files to a minimum I made the testADBI/OMovie.dir self-contained. You may find it convenient to put the parent script in an external cast, so that you can easily include it with different movies.
Another point which you may have noticed is that there is no analog output available on the ADB I/O. However, Application Note 6 on the Beehive Technologies website documents how to use both port A and B to drive four digital potentiometers to create four analog outputs. If you have a need for analog outputs you may wish to extend the ADB I/O parent script with a setChannelAnalog handler which considers each digital potentiometer to be a channel. The prototype would look like:
on setChannelAnalog me, iChannel, iChannelValue
Conclusions
In this article we used object-oriented Lingo to create a friendly and clean interface between the ADB I/O and your Director movie. Object-oriented Lingo allowed us to hide set-up details in the parent script's birthing handler and get rid of global variables through the use of property variables. If you had never done anything like this before, you don't know half how spoiled you are. Director and the ADB I/O form a truly Mac-worthy couple: this is data acquisition 'for the rest of us'. Have fun!
References
Tom spent a large part of his life trying to work out which letter groupings minimize confusion when spelling out his name on the phone. Now that he has found that Djaj-adi-nin-grat leads to significantly better results than Dja-ja-di-nin-grat, he can fully concentrate on his research on interaction design at Delft University. Since he does not own any cats, you won't find any pictures of them on http://www.io.tudelft.nl/research/vormtheorie/djajadiningrat.html.