
                          MODEM PROGRAMMING GUIDE
                          =======================

          The Atari ST MODEM programming guide October 12, 1991.

Copyright 1991 Steve Yelvington
        Internet: <steve@thelake.mn.org>
        GEnie:    S.YELVINGTO2

Introduction
------------
MoDem I/O on the   ST  isn't   difficult.   In   fact,  compared with other
personal  systems,  the  ST  is  unusually   easy  to  program  for  serial
communications.

The hard work is done  for  you.  TOS includes interrupt-driven serial port
routines. This means that when a character  arrives at the serial port, TOS
breaks away from whatever it's doing,  grabs  the character, stores it in a
buffer, and returns to the  task  that  was  interrupted. You don't need to
write code to do that.

The serial port buffers -- one for input, one for output -- are FIFO (First
In, First Out) queues. TOS  provides  functions  for reading from the input
buffer, writing to the output  buffer,  and  even  changing the size of the
buffers. All you have to do is read from  or write to the serial port as if
it were a console.

This document contains examples  in  C.  If  you're  programming in another
language, don't despair, because C is used here only to string together TOS
functions. The strategies don't change when  you shift to another system of
notation.

The MoDem I/O routines
----------------------
Both BIOS and GEMDOS provide functions for single-character serial I/O that
work exactly like their console equivalents.

 >From GEMDOS:

   Console      Serial         Description
   -------      ------         -----------
   Cconin                      Read a character with echo
   Cnecin       Cauxin         Read a character without echo
   Cconout      Cauxout        Write a character
   Cconis       Cauxis         Return TRUE if a character is available
   Cconos       Cauxos         Return TRUE if output is possible, i.e.,
                               the output buffer has room for more data

There is not a serial port equivalent for Cconws, but you should be able to
write one easily enough:

#include <osbind.h>
void Cauxws(s) /* write a nul-terminated string to the aux port */
        register char *s;
        {
        register char c;
        while (c=*s++)
                Cauxout(c);
        }

In C, GEMDOS functions usually are  implemented as preprocessor MACROS that
translate into gemdos() function  calls.  Other  languages may require that
you write GEMDOS  calls  explicitly.  If  you're  coding  in assembler, you
should be able to translate the following  by keeping in mind that you push
the arguments onto the stack from right  to  left and then call trap #1 for
GEMDOS.

Here are some common  C  preprocessor  definitions  for the above-mentioned
operations (plus a few more).

#define Cconin()        (long)  gemdos(0x1)
#define Cconout(a)      (void)  gemdos(0x2,(short)(a))
#define Cauxin()        (short) gemdos(0x3)
#define Cauxout(a)      (void)  gemdos(0x4,(short)(a))
#define Cprnout(a)      (void)  gemdos(0x5,(short)(a))
#define Cnecin()        (long)  gemdos(0x8)
#define Crawio(a)       (long)  gemdos(0x6,(short)(a))
#define Crawcin()       (long)  gemdos(0x7)
#define Cconws(a)       (void)  gemdos(0x9,(char*)(a))
#define Cconrs(a)       (void)  gemdos(0x0a,(char*)(a))
#define Cconis()        (short) gemdos(0x0b)
#define Cconos()        (short) gemdos(0x10)
#define Cprnos()        (short) gemdos(0x11)
#define Cauxis()        (short) gemdos(0x12)
#define Cauxos()        (short) gemdos(0x13)

What about BIOS?

For BIOS, the approach is  slightly  different.  BIOS provides one function
for each operation, with the  device  indicated  by  an argument. The first
argument to the function is the device number:

   Console      Serial         Description
   -------      ------         -----------
   Bconin(2)    Bconin(1)      Read a character
   Bconout(2,c) Bconout(1,c)   Write a character c
   Bconstat(2)  Bconstat(1)    Return TRUE if a character is available
   Bcostat(2)   Bcostat(1)     Return TRUE if output is possible

Note that the device numbers used by  the  BIOS are different from the file
handles used at the GEMDOS level. The BIOS device numbers may be defined in
this fashion:

#define BCON_PRT        0 /* printer */
#define BCON_AUX        1 /* aux (modem) port */
#define BCON_CON        2 /* console */
#define BCON_MIDI       3 /* MIDI port */
#define BCON_KBD        4 /* intelligent keyboard device output */
#define BCON_RAW        5 /* raw console output, no VT52 processing */

The BIOS macros are defined as follows:

#define Bconstat(dev)   bios(1,(short)(dev))
#define Bconin(dev)     bios(2,(short)(dev))
#define Bconout(dev,ch) bios(3,(short)(dev),(short)(ch))
#define Bcostat(dev)    bios(8,(short)(dev))

If you're using assembly language, the BIOS trap is #13.

There is no function to read a   string  from the serial port at either the
GEMDOS level or the BIOS level. For reasons that will become clear shortly,
you should write your own.

All of the GEMDOS and BIOS input functions return 32-bit LONG values.

In the case of the serial port, only the low 8 bits are of interest.

In the case of the console,  the  low  8  bits  will contain the ASCII code
corresponding to the key -- if  there  is  one.  If the keystroke is a non-
standard key, such as a function key, HELP or UNDO, the low eight bits will
be an ASCII NUL value. In  all  cases,  the  upper 24 bits contain a unique
keyscan code and information regarding the state of the shift keys.

A simple terminal program
-------------------------
A 'dumb' terminal program using these functions is simple to write. All you
need is a loop in which you will 'poll' the console and the serial port. It
might look like this:

#include <osbind.h>
#define BANNER "\033EIncredibly dumb full-duplex terminal version 0.1\r\n"

main()
    {
    int c;
    Cconws(BANNER);
    for(;;)
            {
            if (Cauxstat()) /* data waiting at the aux port */
                    {
                    c = (int) (Cauxin() & 0xFF); /* get it and clean it */
                    Cconout(c);
                    }
            if (Cconstat()) /* data waiting at the console */
                    {
                    c = (int) (Cnecin() & 0xFF); /* get it and clean it */
                    if (c) /* if it's not a NUL from a function key */
                            Cauxout(c);
                    }
            }
    }

You'll notice that the above example  runs forever. Well, not quite. GEMDOS
console I/O processes control-characters,  and  a  control-C  will kill the
program. Of course, that means you  can't  send  control-C out to the modem
port. That's one of many reasons it's called an 'incredibly dumb' terminal.
You'll probably want to rewrite the  program  using BIOS functions and test
for use of the UNDO or F10 keys.

Whatever happened to GEM?
-------------------------
None of this discussion has dealt with  GEM.  Why? Because so far as GEM is
concerned, the serial port  doesn't  exist.  The  AES Event Manager doesn't
poll the aux port to determine whether an event has occurred.

If you want to write  a  GEM  event-driven communications program that will
co-operate with desk accessories for multitasking,  you'll  need to use the
timer with a time value of 0. This  will allow a task switch to take place,
and when you regain control  you  can  check  the serial port's status with
GEMDOS or BIOS.  If  you're  worried  that  the  serial  input  buffer will
overflow while some  system-hog  task  is  running,  resize  the  buffer as
described below.

The real world intrudes
-----------------------
None of the above functions care  whether  the  MoDem is live or dead, they
only deal with  data.  If  your  caller  falls  asleep  at  the keyboard or
abruptly hangs up, the above functions won't worry about a thing. If you're
writing a communications program or a  BBS,  you'll have to deal with these
real-world conditions. MoDems and phone lines are not perfect.

There are many ways to handle such errors.  In C, you might want to use the
setjmp and longjmp  functions  to  'bail  out'  and  restart  your program.
Assuming you have set up a  jump  buffer  called  'panic' you might write a
low-level aux port input function that looks like this:

#include <time.h>
#include <osbind.h>
#define TIMEOUT         60      /* 60-second timeout */

int auxin()
        {
        clock_t time1,elapsed;
        if (!rs232cd())
                {
                hangup();
                longjmp(panic);
                }
        time1 = clock();  /* get tick time */

        /* loop for up to TIMEOUT seconds, waiting for aux input */
        for(;;)
                {
                if (Cauxis()) /* these could just as well be BIOS calls */
                        return Cauxin();

                /* no activity yet, so ... */
                /* get new tick time and convert to seconds */

                elapsed = (clock() - time1) / CLK_TCK;

                /* if too much time has elapsed, restart */

                if (elapsed > TIMEOUT)
                        {
                        hangup();
                        longjmp(panic);
                        }
                } /* continue the loop */
        }

The above code has used a  couple  of  functions that aren't in the library
and bear some explaining. Here  is  the  function  that checks the carrier-
detect status of the aux port by examining a hardware register:

#include <osbind.h>
int rs232cd()                   /* state of rs232 carrier detect line */
        {
        register long ssp;
        register int *mfp, status;
        mfp = ((int *) 0xFFFFFA00L);            /* base address of MFP */
        ssp = Super(0L);                        /* enter supervisor mode */
        status = *mfp;                          /* get MFP status */
        Super(ssp);                             /* return to user mode */
        return(!(status & 0x0002));             /* check for carrier */
        }

Here is a function that hangs up  the  phone line by dropping the DTR (Data
Terminal Ready) signal. Note that most  MoDems are shipped from the factory
with DTR sensitivity disabled. You should  be  able  to  turn it back on by
toggling a DIP switch or with a command from the keyboard.

#include <osbind.h>
void hangup()
        {
        Ongibit(0x10);  /* dtr off */
        sleep(1);       /* wait one second */
        Offgibit(0xEF); /* dtr on */
        }

Now what about getting a string  from  the  modem port? You'll need to call
your custom  auxin()  function,  as  above.  Here  is  an  auxgets function
modified from the dLibs getln. Note  that  it  uses a function 'auxput' for
output; you can rewrite or #define  it  to  mirror the BIOS or GEMDOS-level
routine you used for input:

#define KEY_CR          0x0D /* carriage return */
#define KEY_LF          0x0A /* linefeed */
#define KEY_BS          0x08 /* backspace */
#define KEY_DEL         0x7F /* delete */

char *auxgets(buffer, limit)
     char *buffer; /* where keystrokes will be stored */
     register int limit; /* size of the buffer */
     {
     register char *bp = buffer;
     register int c, i = 0;
     for(;;)
             {
             c = auxin() & 0xFF; /* mask off high bits, just in case */

              /* end of line */
             if((c == KEY_CR) || (c == KEY_LF))
                     {
                     *bp = '\0';
                     break;
                     }

             /* backspace or delete */
             else if(((c == KEY_BS) || (c == KEY_DEL)) && (bp != buffer))
                     {
                     --bp;
                     auxput('\b');
                     auxput(' ');
                     auxput('\b');
                     --i;
                     }
             else if((c >= ' ') && (i < limit))
                     {
                     auxput(*bp++ = c);
                     ++i;
                     }
             }
     return(buffer);
     }

Configuring the serial port
---------------------------
In order to communicate with  another  machine  over the serial port, speed
and flow-control settings may need  to  be altered. Atari's XBIOS (Extended
BIOS) provides a function that can  configure the serial port. The function
Rsconf sets the speed, flow-control and four bitmapped hardware registers.

Speed (bits per second, often incorrectly called baud) can be set to any of
16 values:

#define BPS_19200       0 /* standard */
#define BPS_9600        1 /* standard */
#define BPS_4800        2 /* standard */
#define BPS_3600        3
#define BPS_2400        4 /* standard */
#define BPS_2000        5
#define BPS_1800        6
#define BPS_1200        7 /* standard */
#define BPS_600         8
#define BPS_300         9 /* standard */
#define BPS_200         10
#define BPS_150         11
#define BPS_134         12
#define BPS_110         13
#define BPS_75          14
#define BPS_50          15

Only  the  values  marked   /*standard*/   are   commonly   used  in  MoDem
communications.

If you are using a slow- or medium-speed modem (under 9600bps) you probably
will need to match the ST's serial port speed to that of the remote system.

If you are using a high-speed MoDem,  you  may  be able to set the speed at
19,200bps and let the MoDem handle speed-matching chores. In this case, you
probably  will  need  to  use  RTS/CTS  (hardware)  flow-control  to  avoid
overloading the ST's input buffer and the modem's output buffer. RTS/CTS is
faulty in most versions  of  TOS,  and  an  /auto/  folder  program must be
installed to fix it.

Flow control can be configured to any of four settings:

#define FLOW_NONE       0 /* default */
#define FLOW_XONXOFF    1 /* control-s, control-q software */
#define FLOW_RTSCTS     2 /* hardware */
#define FLOW_BOTH       3 /* both hardware and software */

For binary file transfers, software flow control must not be used.

The hardware registers are not useful in ordinary MoDem-related programming
and altering their values  incorrectly  will  have unpleasant side effects.
Passing -1 to Rsconf will leave the corresponding setting unchanged.

Thus, to set bps rate to 2400 and flow control to none, the Rsconf function
call would look like this:

        Rsconf(BPS_1200,FLOW_NONE,-1,-1,-1,-1)

Parity and duplex
-----------------
Parity is not commonly used  in  communicating with personal computers, but
you may need to deal with it  when communicating with a mainframe. The idea
behind parity is to provide at least  a  small degree of assurance that the
transmitted character is the same as  the  received character. The way this
is done is by fiddling with  the  high  bit  of each byte, which ordinarily
would be zero for an ASCII character.

'Even parity' means that the high bit is either set or unset, as necessary,
to ensure that  the  result  is  an  even  number.  'Odd  parity' means the
opposite. You can test this with the  modulus operator in C, then strip the
high bit by ANDing the byte with  0x7F  or  set the high bit by ANDing with
0xFF.

TOS also can generate parity  values  automatically  if the proper hardware
register is set. However, setting the register requires that you also worry
about start/stop bits, word length and the serial hardware frequency, which
is a can of worms.

Duplex is a potentially  confusing  term  that  describes  whether you or a
remote system is responsible for putting a character on your screen.

Most bulletin board systems run 'full  duplex' which means that a keystroke
from a caller is sent to the BBS,  which then echoes that character back to
the caller. This relationship is  not  symmetrical. The caller doesn't echo
anything back to  the  BBS,  because  the  echoes  would  be interpreted as
commands.


'Half duplex' means that each party is  responsible for managing his or her
own display. GEnie normally runs half duplex. Neither caller nor BBS echoes
anything.

If you're writing a BBS or a  terminal  program  with a 'chat mode' you may
want to include a 'both local and remote echo' option. This would allow the
remote to run  full  duplex  while  you  manage  both  your  screen and the
caller's screen.

Resizing the serial buffers
---------------------------
When a character arrives  at  the  serial  port,  TOS  interrupts what it's
doing, grabs the character before it disappears, and records it in an input
buffer. When your program wishes to  write  to the serial port, TOS buffers
the output so that  your  program  can  proceed  with  other chores without
having to wait for a slow MoDem to transmit the data.

TOS provides those buffers, but TOS  also  provides a facility for changing
them. Why would you want to do so? Here's a scenario:

You're using Xmodem and you have a  slow floppy drive. Xmodem ACKS a block,
then proceeds to write it to disk while the sender sends the next block. So
far, so good. But if  you  switch  to  Ymodem's  1K  blocks and a very fast
MoDem, the TOS  default  buffer  might  overflow  while  you're writing the
previous block to that  slow  floppy  drive.  Solution:  Use a bigger input
buffer.

Another scenario: You're  running  a  BBS.  The  caller  is  reading a long
message and decides it's boring. The caller pushes the cancel key (control-
C, or S, or whatever you've  decided  makes  sense.) The BBS stops sending,
but that big output buffer is full of data -- and it just keeps coming, and
coming .... Solution: Use small I/O buffers for interacting with humans.

TOS provides an XBIOS function that  returns  a  pointer to an input buffer
record. It requires one argument: The BIOS number of a device (see above).

#define Iorec(dev) (void*) xbios(14, (short)(dev))

Assembly programmers: The xbios  trap  is  #14,  so  push the device number
followed by two 14's, then do the trap. The pointer will be found in d0.

The iorec structure is defined as follows:

struct iorec
        {
        char *buf;    /* pointer to an array: char buffer[bufsiz] */
        short bufsiz; /* size of the array         */
        short head;   /* index for writing         */
        short tail;   /* index for reading         */
        short low;    /* low water mark, for XON   */
        short high;   /* high water mark, for XOFF */
        }

For the AUX port, the  output  buffer  record immediately follows the input
buffer record.

Before you go poking around inside  these  records,  keep in mind that they
are internal to TOS, and they are shared  by all processes that run on your
system. You can change the  buffers  and  even  bypass  GEMDOS and BIOS for
reading and writing, if you so  desire,  but  you  must be prepared to live
with the consequences.  In  particular,  be  very  careful  to  restore the
original record when your program  terminates  (unless you're writing a TSR
with the express purpose of enlarging a buffer).

Immediately after the  serial  port's  input  and  output  records are some
internal TOS variables that may be useful. The following code has worked on
all versions of TOS  that  I've  encountered.  I  don't  know whether Atari
guarantees that the variables won't be changed on future versions of TOS.

With those cautions in mind, here  is  some code, originally lifted from an
early ST communications program written by Dale Schumacher. I've been using
the open_aux and close_aux functions for years, but I haven't had a need to
deal with the rsr, tsr, and flow control variables.

typedef struct
        {
        struct iorec in;        /*  0 Input buffer record  */
        struct iorec out;       /* 14 Output buffer record */
        char rsr_status;        /* 28 MFP(0x1C) Receiver status */
        char tsr_status;        /* 29 MFP(0x1D) Transmit status */
        char xoff_sent;         /* 30 TRUE if we sent XOFF      */
        char xoff_received;     /* 31 TRUE if we got XOFF       */
        char mode;              /* 32 Bit 0 XON, Bit 1 RTS mode */
        char filler;            /* 33 Unused                    */
        } IOREC;

#define IBUFSIZ 1200
#define OBUFSIZ 2

char    st_ibuf[IBUFSIZ];       /* our own input buffer         */
char    st_obuf[OBUFSIZ];       /* and our own output buffer    */

IOREC   *st_sysr;               /* ptr to system rs232 record   */
IOREC   st_savr;                /* to save system rs232 record  */

IOREC   st_myiorec =
        {
        /* first, an input record */
        st_ibuf, IBUFSIZ, 0, 0, (IBUFSIZ/4), (3*(IBUFSIZ/4)),

        /* then an output record */
        st_obuf, OBUFSIZ, 0, 0, 0, 1,

        /* and the rsr, tsr, flow control stuff */
        0, 0, 0, 0, 0, 0
        };

void openaux()          /* set up our own rs232 input and output buffers */
        {
        while(Bconstat(AUX))            /* flush existing buffer */
                Bconin(AUX);
        st_sysr = (IOREC *)Iorec(0);
        st_savr = *st_sysr;             /* Save system buffer   */
        *st_sysr = st_myiorec;          /* Set my io buffer     */
        }

void closeaux()         /* restore system i/o buffer */
        {
        *st_sysr = st_savr;
        }

Take a break
------------
Some remote systems, notably Unix sites,  need  a BREAK signal to wake them
up and help them synchronize with a  caller's bps rate. To generate a BREAK
on the ST, you briefly toggle  the  transmit  status register. Here is code
that does so by directly accessing  the  hardware  on  the ST. I think Dale
Schumacher is the author of this code.

#include <time.h>
#include <osbind.h>

void nap(n)                     /* do nothing for n-100ths of a second */
        register int n;
        {
        register clock_t t, dt;
        dt = (CLK_TCK * ((clock_t) n)) / ((clock_t) 100);
        t = clock() + dt;
        while(clock() < t)
                ;
        }

void snd_brk()          /* send a BREAK to the rs232 port */
        {
        register long ssp;
        register char *tsr;
        tsr = ((char *) 0xFFFFFA2DL);           /* address of tsr */
        ssp = (long) Super(0L);                 /* enter supervisor mode */
        *tsr = 0x09;                            /* turn break on */
        Super(ssp);                             /* return to user mode */
        nap(30);                                /* 0.3 second BREAK */
        ssp = (long) Super(0L);                 /* enter supervisor mode */
        *tsr = 0x01;                            /* turn break off */
        Super(ssp);                             /* return to user mode */
        }

The clock() function should be available  with  most C compilers. If you're
writing in a language that doesn't have  an equivalent, the trick is simply
to peek at memory location 4BA (hex),  which is the 200Hz system timer. You
must be in supervisor mode to do so. The value is a 32-bit long.

File transfers
--------------
When transferring a binary file, the sender  and the receiver have to agree
on a method for  signalling  the  end  of  the  transmission. Ideally, they
should have some method of determining whether the file was mangled by line
noise. And if it was, there should be a way to retransmit the bad data.

The methods for solving these  problems are called file-transfer protocols,
and their name should be legion, for they are many. Xmodem, Ymodem, Zmodem,
Quick B, WXmodem, Kermit, Punter, FAST, Telink,  UUCP 'G' ... the list goes
on.

Most of them follow the  same  basic  strategy.  A  file  is broken up into
multiple packets. Each packet starts with  a  header that includes a packet
number and a mathematical value,  called  a  checksum,  that can be used to
determine whether the data in the packet has been corrupted.

XMODEM is a very simple implementation of this strategy.

It uses 128-byte data packets. It uses  packet headers consisting of a byte
for the packet number and (in the  original version) a byte for a checksum.
The checksum is calculated by  adding  the  numeric  value of each byte and
throwing away the overflow. It's not foolproof, but it detects most errors.

Later  versions  of  Xmodem  use   a  16-bit  'cyclical  redundancy  check'
calculated by using a fairly complex algorithm. It's much harder to fool.

If the Xmodem receiver adds up  all  the  values and finds they don't match
the total that was encoded in  the  packet  header it sends a NAK (Negative
AcKnowledgement) signal to the sender, which then retransmits the corrupted
packet. If they match, the receiver sends an ACK.

Because the packages are small, and because  the sender always waits for an
ACK before proceeding, Xmodem is  not  particularly  fast. When XMODEM runs
over a network  that  does  its  own  error-checking  and packet operations
internally (such as CompuServe  or  GEnie's  data  systems), Xmodem can get
painfully slow. That lonely  ACK  signal  can  take  a  long time to wander
through the network and get back to you.

There are two ways of dealing with  Xmodem's  speed problems. One is to use
bigger packets, which is the approach taken by most alternatives, including
Ymodem. The other is to use 'windowing'  which is used by WXmodem, UUCP 'G'
and Zmodem.

A windowing protocol doesn't wait  for  an  ACK before sending. Instead, it
transmits the next block. If a NAK  arrives  it  can back up and resend the
bad block. The number of blocks that can be 'remembered' is the size of the
'window'. A sliding-window  implementation  is  one  in  which the sender's
window moves along as you  move  through  the  file  and  get ACKs from the
receiver.

And now, the code.

This document isn't going to  attempt  to  present  a C implementation of a
file-transfer protocol. But the question of 'how do I calculate a checksum'
comes up frequently, here is C  code  to  create the values used by Xmodem,
Ymodem and some related programs.

Some CRC functions index the CRC  values  out  of an array of precalculated
numbers. That approach probably is faster  than  the  one below, but the ST
has plenty of horsepower for this sort of thing.

Don't ask me to explain the math. I  can't. I didn't write this; I just use
it.

 /*
  * This function calculates the CRC used by the XMODEM/CRC
  * Protocol.
  * The first argument is a pointer to the message block.
  * The second argument is the number of bytes in the message block.
  * The function returns a short integer that contains the CRC value.
  */
 short crc16(ptr, count)
 register char *ptr;
 register short count;
    {
    register short crc = 0, i;
    while(--count >= 0)
       {

       crc = crc ^ (short)*ptr++ << 8;
       for(i=0; i<8; ++i)
          {
          if(crc & 0x8000)
             crc = crc << 1 ^ 0x1021;
          else
             crc = crc << 1;
          }
       }
    return(crc & 0xFFFF);
    }


If you want to port this to another language, keep in mind that :-

  a << b  evaluates to the bit pattern of a shifted left b bits

  * de-references a pointer; *ptr++ evaluates to the pointed-to value
    and then increments ptr by the size of the pointed-to value

  ^ is a bitwise exclusive OR operator; 11110000 (binary) ^ 00001111
    evaluates to 11111111

  & in the context above is a bitwise AND operator; crc & 0xFFFF
    basically strips anything above 16 bits (important if your
    environment supports 32-bit ints).


Acknowledgements
----------------
This document wouldn't have  been  possible  without  the assistance of the
following people. Some  provided  instructive  code  samples; others caught
errors in earlier versions of this  document or raised important questions.
Thanks to: Dale Schumacher,  John  Stanley,  Robert  Royar, Adam David, Rob
McMullen, Michael Fischer.

Corrections
-----------
Send corrections to steve@thelake.mn.org  (Internet), S.YELVINGTO2 (GEnie),
or snail mail to P.O. Box 38, Marine on St. Croix, MN 55047 USA.

Permissions
-----------
This file may be distributed  only  in  electronic  format, and only in its
original, unaltered state.  All  other  reproduction  is prohibited without
written permission of  the  author.  This  information  is  intended  to be
accurate, but the author makes  no  representation  that is free of errors.
This information is presented without warranty  of any sort, and the author
is not responsible  for  any  side  effects.  None  of  this information is
extracted from any proprietary documents  or trade secrets. PS: Shakespeare
was right about the lawyers.

Last modified Nov 23, '94 by Christer Gustavsson.
