Jun 98 Tips
Volume Number: 14 (1998)
Issue Number: 6
Column Tag: Tips & Tidbits
June 1998 Tips & Tidbits
by Steve Sisak
Over the past few months we've had a few tips submitted on how to open a serial port or detect if it is in use. Unfortunately, we haven't received one that was sufficiently correct or complete to publish as a winner. Since this seems like an interesting topic (and one that lots of people get wrong), I'm going to try a new format: a terse mini-article with enough information to get you started and pointers to more information.
If you like this format, have a good idea for a topic but don't know the answer, or have other ideas how to make this space more useful, please send mail to tips@mactech.com. I'll be glad to pay the standard reward and give you credit for a really good question (assuming I can find the answer and it's generally useful).
Serial Port TidBits
If you've ever tried to write an application which uses the serial port on a Macintosh, you've probably discovered that (1) it didn't work on the first try, (2) the information on what to do was scattered all over the place and (3) it still didn't work in all cases.
In case you haven't, the information for how to correctly open and use the serial drivers is scattered across Inside Macintosh, the Communication Toolbox documentation, the ARA SDK and various tech notes. There are also several misleading and obsolete descriptions in Inside Macintosh Vols. I-VI.
The most authoritative sources are Inside Macintosh: Devices and Tech note 1119, by Quinn (The Eskimo!) which pulls most of the relevant information together in one place.
Listing the Serial Ports
In the beginning, every Macintosh had exactly 2 serial ports named "Modem Port" and "Printer Port" and the names of their drivers were hard coded -- these days PowerBooks often have only one port and/or a built-in modem, NuBus and PCI cards make it possible for the user to add ports, and software creates "virtual" ports to make it possible for multiple programs to share the same physical port.
To determine how many ports a machine has and what their human-readable name are, you need to use the Communications Resource Manager (CRM), which is part of the Communications Toolbox (one of those managers that Apple has declared obsolete, but hasn't gotten around to replacing yet).
For each port, the CRM maintains a CRMSerialRecord containing the following information:
typedef struct CRMSerialRecord {
short version;
StringHandle inputDriverName;
StringHandle outputDriverName;
StringHandle name;
CRMIconHandle deviceIcon;
long ratedSpeed;
long maxSpeed;
long reserved;
} CRMSerialRecord, *CRMSerialPtr;
To iterate over the available ports, you use the function CRMSearch(). The following code fragment finds a port by name -- you can easily adapt it to build a menu, etc.:
CRMSerialPtr FindPortInfo(ConstStr255Param name)
{
CRMRec crmRec;
CRMRecPtr crm = &crmRec;
// Get the search started
crmRec.crmDeviceType = crmSerialDevice; crmRec.crmDeviceID = 0;
while ((crm = CRMSearch(crm)) != nil)
{
CRMSerialPtr portInfo =
(CRMSerialPtr) crm->crmAttributes;
if (EqualString(*portInfo->name, name, false, true))
{
return portInfo;
}
}
return nil;
}
Opening, Initializing and Closing a Serial port
There is a specific sequence of calls you must use to open, configure and close a serial port. It is listed in Inside Macintosh: Devices on page 7-11. If you do not make the calls in this order, strange things will happen.
The sequence is:
- Open the output driver, then the input driver; always open both.
- (optional) allocate a buffer larger than the default 64-byte buffer and call SerSetBuf.
- Set the handshaking mode.
- Set the baud rate and data format.
- Read and/or write the desired data.
- Call KillIO on both drivers to terminate any pending IO.
- Restore the default input buffers.
- Close the input driver, then the output driver.
Determining If a Serial Driver is Open
Determining if a serial driver is open in use is a little bit tricky and a lot of software gets it wrong. The problem is twofold: first, OpenDriver() doesn't bother to check if a driver is already open -- it just returns noErr and the reference number of the already-open driver. If you use it (or worse, close it when you're done) Bad Things(tm) will happen.
To get around this you must walk the device list in low memory to see if a driver is already open before trying to open it again.
The following routine finds the index of a driver in the unit table (or -1 if it doesn't exist):
short FindDriverIndex(ConstStr255Param name)
{
StringPtr driverName;
short index;
AuxDCEHandle entry;
AuxDCEHandle* table = (AuxDCEHandle*) LMGetUTableBase();
short count = LMGetUnitNtryCount();
for (index = 0; index < count; index++)
{
if ((entry = table[index]) != nil)
{
if ((**entry).dCtlFlags & dRAMBasedMask)
{
driverName = (**((DRVRHeaderHandle)((**entry).dCtlDriver))).drvrName;
}
else
{
driverName = (*((DRVRHeaderPtr)((**entry).dCtlDriver))).drvrName;
}
if (EqualString(driverName, name, false, true))
{
return index;
}
}
}
return -1;
}
To check if a port is open, we can write:
Boolean IsDriverOpen(ConstStr255Param name)
{
short index = FindDriverIndex(name);
if (index >= 0)
{
AuxDCEHandle dce =
((AuxDCEHandle*) LMGetUTableBase())[index];
if ((**dce).dCtlFlags & dOpenedMask)
{
return true;
}
}
return false;
}
NOTE: LMGetUTableBase() is missing from some versions of the Universal Headers you may have to implement it yourself (or use newer headers).
Now for the second half of the problem -- the Serial Port Arbitrator, included with the Appletalk Remote Access server and other software allows a port to be opened "passively" meaning that a server may have a the port open to look for an incoming call, but will relinquish it if another application wants to use it.
In this case OpenDriver will return portInUse (-97) if the driver is open or noErr if it is not. (in either case, it will return a valid refNum). However, software which walks the device table will incorrectly think that the driver is open and report an error.
The correct procedure here is to use Gestalt to determine if the Serial Port Arbitrator is present and, if it is, then just call OpenDriver(), otherwise, walk the Unit Table:
Boolean HaveSerialPortArbitration(void)
{
long result;
OSErr err;
err = Gestalt(gestaltArbitorAttr, &result);
return (err == noErr) && (result & (1 << gestaltSerialArbitrationExists));
}
OSErr OpenSerialDriver(ConstStr255Param name, short* refNum)
{
if (!HaveSerialPortArbitration())
{
short index = FindDriverIndex(name);
if (index >= 0) // Driver is already open
{
*refNum = ~index;
return portInUse;
}
}
return OpenDriver(name, refNum);
}
Reading from a Serial Port
You can read from a serial port just like a file by using PBRead or FSRead, however you can't seek and if you try to read more bytes are actually available, you will hang the machine in SyncWait() until the number of bytes you requested is actually available.
To avoid this, you can make a status call to the input driver with csCode = 2 to find out how many bytes are available in the drivers buffer and then only request that many bytes.
Correction to May Tip -- Wrapper for ParamText
The tip "Wrapper for ParamText" incorrectly states that one cannot pass nil arguments to ParamText(). In fact, ParamText() has always accepted nil arguments, and leaves the corresponding parameters unchanged.
I use this feature frequently in order to conserve stack space. I can set all 4 parameters using only one Str255, as long as I do it one at a time.
Actually, this example only sets 3 params:
void
ReportError( short doingIx, const char *what )
{
Str255 str;
if( spareMem )
DisposeHandle( spareMem );
spareMem = NULL;
CtoPstrcpy( str, (const unsigned char *)what, sizeof str );
ParamText( str, NULL, NULL, NULL );
GetErrString( doingIx, str );
ParamText( NULL, str, NULL, NULL );
/* Eww. But I don't have to ParamText menu item text every command,
which might slow things down if we're being scripted.
*/
if( doingIx == doingMenuCmd ) {
str[0] = 0;
if( tg.doingItem )
GetID( tg.doingItem, str );
ParamText( NULL, NULL, NULL, str );
}
else if( doingIx == doingMenuAct ) {
str[0] = 0;
if( tg.doingItem && tg.doingMenu )
GetMenuItemText( GetMenu(tg.doingMenu), tg.doingItem, str );
ParamText( NULL, NULL, NULL, str );
}
VerifyAlert( 144 );
InitCursor();
StopAlert( 144, DefaultFilter );
}
Tony Nelson
tonyn@tiac.net