Order the book from O'Reilly

Previous PageTable Of ContentsIndexNext Page

In this chapter:

 9.  Communications

In this chapter, we discuss the types of communication available on the Palm OS. Next we go into detail about two of these types and show you how to write code to use them.

Palm OS supports three kinds of communication: IrDA, serial, and TCP/IP:

IrDA

This is an industry-standard hardware and software protocol. We won't discuss the details of communicating using IrDA. We will, however, show you how to use the Exchange Manager to implement beaming (see the section entitled "Beaming" on page 235). Beaming is a data exchange method built on top of IrDA.

Serial

Serial communication occurs between the handheld and other devices using the cradle port. This is the most common form of communication on the Palm OS, and as an example we develop a special serial application that communicates (indirectly) with satellites.

TCP/IP

Currently, this communication standard is available only via a serial or modem connection. The future has no boundaries, however, so you might expect to see built-in Ethernet or devices using wireless TCP/IP appear some day. To show you how to use TCP/IP, we create a small application that sends email to a server.

Serial

Top Of Page

The Serial Manager is fairly straightforward. There are routines to do all of the following:

Serial I/O is synchronous, so there's no notification when data gets received. Instead, your code must poll to see whether data has arrived.

Tips for Using the Serial Manager

Here are a bunch of miscellaneous tips that will help you when it's time to add serial functionality to an application:

Open port error

If your code calls SerOpen and it returns the error serErrAlreadyOpen, your open has succeeded, but some other code already opened the port. Although it's possible to share the port, a sane person wouldn't normally want to do so. Sharing reads and writes with some other code is a recipe for mangled data. If you get this error, you should notify the user that the port is in use and gracefully call SerClose.

Open the serial port only for short periods of time

Don't leave the serial port open any longer than absolutely necessary. If your application reads data from the serial port every five minutes, don't leave it open for that entire time. Instead, close the port, and reopen it after five minutes. As a rule of thumb, leave the serial port open for no longer than 30 seconds if it is not in use.

Similar advice is often given to drivers about stopped cars. If you will move again within a few minutes, leave the car idling; otherwise, shut the car off and restart it when you are ready to go. Just as an idling car wastes gas, an idle serial port wastes batteries by providing power to the serial chip. Such behavior will really annoy your users, who don't want an application that sucks all the life out of their batteries. Don't have sloppy serial code.

Preventing automatic sleep

If you don't want the Palm OS device to sleep while you are communicating, call  EvtResetAutoOffTimer at least once a minute. This prevents the automatic sleep that happens when no user input occurs. If you have communication that shouldn't be interrupted, you certainly should do this, as you will lose the serial data when the device goes to sleep.

Adjusting the receiving buffer size

The default receive buffer is 512 bytes. Think of this receiving buffer as similar to a reservoir. The incoming data flows into the buffer, and reads from the buffer drain the data out the other side. Just as with a reservoir, if you get too much incoming data, the buffer overflows, and data spills out and is lost. The error you get is serLineErrorSWOverrun.

If you expect a lot of data, it's best to adjust your buffer to accommodate greater inflows. You can set the size using  SerSetReceiveBuffer. When you're done, make sure to release the buffer before you close the port; do so by calling SerSetReceiveBuffer with a size of 0. SerClose won't release the buffer, so if you don't do it yourself, you'll leak memory.

Knowing when there is data in the receive buffer

When reading data, it is best to do it in two steps. The first step is to call  SerReceiveWait, which blocks until the specified number of bytes are available in the buffer. To provide a timeout mechanism, SerReceiveWait takes as a parameter an interbyte tick timeout. This timeout is used for a watchdog timer that is reset on every received byte. If the timer expires, the function returns with serErrTimeOut. Once SerReceiveWait returns, the second step is to call SerReceive to actually read the data from the receive buffer.

The timeout measures the time between successive bytes, not the time for all bytes. For example, if you call SerReceiveWait waiting for 200 bytes with a 50-tick timeout, SerReceiveWait returns either when 200 bytes are available, or when 50 ticks have passed since the last received byte. In the slowest case, if bytes come in one every 49 ticks, SerReceiveWait won't time out.

SerReceiveWait is the preferred call, because it can put the processor into a low-power state while waiting for incoming data-another battery-saving technique that will make your users happy.

Handling user input during a serial event

Don't ignore user input while communicating. Wherever possible, you need to structure your application so that it deals with serial communication when the user isn't doing other stuff. Practically, this means you can do one of two things. Your application could communicate every so often by calling EvtGetEvent with a timeout value. Or, if your communication code is in a tight loop that doesn't call EvtGetEvent, you can call   SysEventAvail every so often (certainly no less than once a second). This allows you to see whether there's a user event to process. If there are user events, return to your event loop to process the event before attempting any more serial communication.

NOTE:

A user who can't cancel is an unhappy user.

Receiving error messages

If SerReceive, SerReceiveWait, or SerReceiveCheck return   serErrLineErr, you need to clear the error using SerClearErr. Alternatively, you should use SerReceiveFlush if you also need to flush the buffer, since it will call SerClearErr.

Palm OS version differences

SerSend and SerReceive have been enhanced in Palm OS 2.0. In this OS, they return the number of bytes sent or received. If you are running on Palm OS 1.0, you need to use SerSend10 and SerReceive10, which have different parameters. Make sure your code does the right thing for each OS version.

Serial to a state machine

Often, serial protocols are best written as though they are going to a state machine. You can do this by defining various states and the transitions that cause changes from one state to another. You use a global to contain information on the current state. While it might sound complicated, writing your serial code this way often makes it simpler and easier to maintain.

For example, if you use your Palm device to log into a Unix machine, you might send <CR><CR> and then enter the "Waiting for login:" state. In this state, you'd read until you got the characters "login:". You would then send your account name and enter the "Waiting for password:" state. In that state, you'd read until you got the characters "password:". Then you would send the password and enter yet another state.

Sample Serial Application

Our Sales application doesn't have serial code in it. Instead, we've written a small application that communicates with a Global Positioning System (GPS) device. A GPS device reads information sent by orbiting satellites; from that data it determines the location of the device. In addition to the location (latitude, longitude, and altitude), the device obtains the Universal Time Coordinate (UTC) time.

Features of the sample application

Our sample application communicates with this GPS device using the industry-standard National Marine Electronics Association (NMEA) 0183 serial protocol.

The application's startup screen is shown in Figure 9-1. As you can see, it is blank except for telling the user that it has no GPS information. The state changes as soon as the handheld has established communication with the GPS device and has acquired a satellite. Now it displays the time, latitude, and longitude, as shown in Figure 9-2. The application updates these values every five seconds to make sure the location and time are up-to-date. If the GPS device loses contact with the satellite (as might happen when the user enters a building), the sample application promptly warns the user (as shown in Figure 9-3).

-Figure 9- 1. The GPS application when it is has not recently heard from the GPS device

Figure 9- 2. The GPS application displaying the current time, latitude, and longitude

Figure 9- 3. The GPS application warning that the satellite has been lost

NOTE:

A GPS device hooked to a Palm OS handheld is a compact and useful combination. Imagine the versatile and convenient applications you could create that would use information augmented with exact location and precision time-stamping. For example, a nature specialist could create a custom trail guide that other people could use to retrace the guide's path.

The GPS device

We're using a Garmin 12 GPS device purchased for under $200 from the sporting goods section of a discount store. The serial connector on this device is custom, so we bought a Garmin-to-DB-9 serial cable. (A DB-9 connector is a nine-pin connector commonly used for serial connections.) Connected to the Palm device is a HotSync cable. Between the two devices is a null-modem converter. The Garmin is configured to send NMEA 0183 2.0 at 9600 baud. Figure 9-4 shows the setup.

Figure 9- 4. The GPS device and handheld setup

*

The NMEA protocol

In our application, we want to update the time, latitude, and longitude every 5 seconds. Updating more often seems unnecessary and would reduce the battery life of the Palm OS device. The GPS device sends 10 lines of information every second; of the 50 that are sent over a 5-second period, we simply parse out the information we need. The rest we ignore. As a result, we don't have to understand the entire NMEA 0183 protocol, but just the bit that pertains to our one line of interest.ü

If we have valid satellite data, the relevant part will look similar to this string:

$GPRMC,204700,A,3403.868,N,11709.432,W,001.9,336.9,170698,013.6,E*6E 

Let's look more closely at this example to see what each part means. Note that we care about only the first seven pieces of data. In Table 9-1 the important parts of the string are laid out with the definitions beside each item.

-Table 9- 1. NMEA String from GPS Device

If we aren't receiving valid satellite data, the string is of the form:

$GPRMC,UTC_TIME,V,...

And here's a typical example:

$GPRMC,204149,V,,,,,,,170698,,*3A 

Now that you have an idea of what we want to accomplish and the tools we are going to use, it is time to look at the code for the sample application.

The sample application serial code

We are going to open the serial port in our StartApplication routine:

UInt  gSerialRefNum;
char  gSerialBuffer[900];  // should be more than enough for one second of 
                           // data--10 lines @ 80 chars per line

static Boolean StartApplication(void)
{
   Err   err;
   
   err = SysLibFind("Serial Library", &gSerialRefNum);
   ErrNonFatalDisplayIf(err != 0, "Can't find serial library");
   
   err = SerOpen(gSerialRefNum, 0, 9600);
   if (err != 0) {
      if (err == serErrAlreadyOpen) {
         FrmAlert(SerialInUseAlert);
         SerClose(gSerialRefNum);
      } else
         FrmAlert(CantopenserialAlert);
      return true;
   }
   err = SerSetReceiveBuffer(gSerialRefNum, gSerialBuffer,
      sizeof(gSerialBuffer));
   return false;
}

We set our own receive buffer so that we can hold an entire second's worth of data. We don't want to risk losing any data, so we give ourselves ample room.

In StopApplication, we close the port (after resetting the buffer to the default):

static void StopApplication(void)
{
   // restore the default buffer before closing the serial port
   SerSetReceiveBuffer(gSerialRefNum, NULL, 0);
   SerClose(gSerialRefNum);
}

We need to create some globals to store information about timing:

// tickCount of last time we read data from GPS
ULong    gLastSuccessfulReception = 0;    

// tickCount of last time we displayed GPS data on the Palm device
ULong    gLastTimeDisplay = 0;

// tickCount of the next scheduled read
ULong    gNextReadTime = 0;

Boolean  gFormOpened = false;

// if we go this long without updating the time
// then update as soon as we get a valid time
// (without waiting for an even 5-second time)
#define  kMaxTicksWithoutTime (6 * sysTicksPerSecond)

// if we go this long without communicating with GPS,
// we've lost it and need to notify the user
#define  kTicksToLoseGPS (15 * sysTicksPerSecond)

We initialize gFormOpened in MainViewInit. We keep track of this because we don't want to start receiving nil events until the form has been opened and displayed:

static void MainViewInit(void)
{
   FormPtr        frm = FrmGetActiveForm();

   // Draw the form.
   FrmDrawForm(frm);
   gFormOpened = true;
}

In our event loop, instead of calling EvtGetEvent with no timeout, we call the function  TimeUntilNextRead to obtain a timeout when we need it. Here's our EventLoop:

static void EventLoop(void)
{
   EventType   event;
   Word        error;
   
   do
      {
      // Get the next available event.
      EvtGetEvent(&event, TimeUntilNextRead());
      if (! SysHandleEvent(&event))
         if (! MenuHandleEvent(0, &event, &error))
            if (! ApplicationHandleEvent(&event))
               FrmDispatchEvent(&event);
      }
   while (event.eType != appStopEvent);
}
NOTE:

 EvtGetEvent, like SerReceiveCheck, enters a low-power processor mode if possible.

Note that TimeUntilNextRead returns the number of ticks until the next scheduled read:

static long TimeUntilNextRead(void)
{
   if (!gFormOpened)
      return evtWaitForever;
   else {
      Long  timeRemaining;
         
      timeRemaining = gNextReadTime - TimGetTicks();
   
      if (timeRemaining < 0)
         timeRemaining = 0;
      return timeRemaining;
   }
}

The guts of the application are in the event handler, MainViewHandleEvent:

case nilEvent:
   handled = true;
   SerReceiveFlush(gSerialRefNum, 1);  // throw away anything in the, 
                                       // buffer-- we want fresh data
   // we loop until an event occurs, or until
   // we update the display
   do {  
      ULong numBytesPending;
         // is the lost satellite label currently displayed
      static Boolean showingLostSatellite = false;
      ULong now = TimGetTicks();
      char     theData[165];  // two lines (80 chars with <CR><LF>
                              //  + one for null byte
      
      // if we've gone too long without hearing from the GPS
      // tell the user
      if ((now - gLastSuccessfulReception) > kTicksToLoseGPS) {
         FormPtr  frm = FrmGetActiveForm();
         
         FrmCopyLabel(frm, GPSMainTimeLabel, "No GPS!");
         FrmCopyLabel(frm, GPSMainLatitudeLabel, "");
         FrmCopyLabel(frm, GPSMainLongtitudeLabel, "");
      }
      
      // We'll fill our read buffer, or 1/2 second between
      // bytes, whichever comes first.
      err = SerReceiveWait(gSerialRefNum, sizeof(theData) - 1, 30);
      if (err == serErrLineErr) {
         SerReceiveFlush(gSerialRefNum, 1);  // will clear the error
         continue;         // go back and try reading again
      } 
      if (err != serErrTimeOut)
         ErrFatalDisplayIf(err != 0, "SerReceiveWait");
      err = SerReceiveCheck(gSerialRefNum, &numBytesPending);
      if (err == serErrLineErr) {
         SerReceiveFlush(gSerialRefNum, 1);  // will clear the error
         continue;         // go back and try reading again
      }
      ErrFatalDisplayIf(err != 0, "SerReceiveCheckFail");
      if (numBytesPending > 0) {
         ULong    numBytes;
         char     *startOfMessage;
         
         // read however many bytes are waiting
         numBytes = SerReceive(gSerialRefNum, theData, 
            numBytesPending, 0, &err);
         if (err == serErrLineErr) {
            SerReceiveFlush(gSerialRefNum, 1);  // will clear the error
            continue;      // go back and try reading again
         }
         theData[numBytes] = '\0';  // null-terminate theData
         
         // look for our magic string
         if ((startOfMessage = StrStr(theData, "$GPRMC")) != NULL) {
            char  s[10];
            gLastSuccessfulReception = now;  // we successfully read
            if (GetField(startOfMessage, 1, s)) {
               // even multiple of five seconds OR it's been at 
               // least kMaxTicksWithoutTime seconds since a display
               // That way, if we lose 11:11:35, we won't have the
               // time go from 11:11:30 to 11:11:40. Instead, it'll go
               // 11:11:30, 11:11:36, 11:11:40
               if (s[5] == '0' || s[5] == '5' ||
                  (now - gLastTimeDisplay) > kMaxTicksWithoutTime) {
                  FormPtr  frm = FrmGetActiveForm();
                  
                  updatedTime = true;
                  // change from HHMMSS to HH:MM:SS
                  s[8] = '\0';
                  s[7] = s[5];
                  s[6] = s[4];
                  s[5] = ':';
                  s[4] = s[3];
                  s[3] = s[2];
                  s[2] = ':';
                  
                  // Most of the time, we'll be on a multiple of five. 
                  // Thus, we want to read in four more seconds.
                  // Otherwise, we want to read immediately
                  if (s[5] == '0' || s[5] == '5')
                     gNextReadTime = gLastSuccessfulReception + 
                     þþþ4*sysTicksPerSecond;
                  else
                     gNextReadTime = 0;
                  
                  // update the time display
                  FrmCopyLabel(frm, GPSMainTimeLabel, s);
                  gLastTimeDisplay = gLastSuccessfulReception;
                  
                  if (GetField(startOfMessage, 2, s)) {
                     // update "Lost satellite" label
                     if (s[0] == 'V' && !showingLostSatellite) {
                        showingLostSatellite = true;
                        FrmShowObject(frm, FrmGetObjectIndex(frm, 
                           GPSMainLostSatelliteLabel));
                     } else if (s[0] == 'A' && showingLostSatellite) {
                        showingLostSatellite = false;
                        FrmHideObject(frm, FrmGetObjectIndex(frm, 
                           GPSMainLostSatelliteLabel));
                     }
                     
                     // update Lat & Lon
                     if (s[0] != 'V')  {
                        // 4 is N or S for Lat direction, 3 is lat
                        if (GetField(startOfMessage, 4, s) && 
                           GetField(startOfMessage, 3, s + StrLen(s)))
                           FrmCopyLabel(frm, GPSMainLatitudeLabel, s);
                        
                        // 5 is E or W for Lat direction, 6 is lon
                        if (GetField(startOfMessage, 6, s) && 
                           GetField(startOfMessage, 5, s + StrLen(s)))
                           FrmCopyLabel(frm, GPSMainLongtitudeLabel, s);
                     }
                  }
               }
            }
         }
      }
   } while (!updatedTime && !EvtSysEventAvail(false));
   break;

Remember that the GPS device is spewing out data once a second. We are not going to update that often; we've settled on updating every five seconds as a happy medium. Normally when we receive an idle event, we have just been in the event loop, dozing for four seconds. Thus, our receive buffer could have old data in it or could have overflowed. In the previous code, we first flush our buffer (we don't want any stale information from the buffer) and then read in two lines of data.

Next, we look for our string by searching for $GPRMC. If we find it, we know we've communicated with the GPS device. gLastSuccessfulReception then gets updated.

Next, we parse out the time from the string. If it is a multiple of five or we've gone too long without updating the display, we do all of the following:

1. Set the next time to read.

2. Parse out the remaining information.

3. Update our display with the new information (the current position or an indication that the link to the satellite is lost).

4. Return to the event loop.

Otherwise, we continue in a loop until we do successfully read or until a user event occurs.

Last, we need a small utility routine, GetField, to parse out a comma-delimited field:

// returns n'th (0-based) comma-delimeted field within buffer
// true if field found, false otherwise
static Boolean GetField(const char *buffer, UInt n, char *result)
{
   int   i;
   
   // skip n commas
   for (i = 0; i < n; i++) {
      while (*buffer && *buffer != ',')
         buffer++;
      if (*buffer == '\0')
         return false;
      buffer++;
   }
   while (*buffer && *buffer != ',')
      *result++ = *buffer++;
   if (*buffer != ',')
      return false;
   *result = '\0';
   return true;
}

That is all there is to our application. But as you can see from the discussion prior to the code, the difficulty with serial is not in the actual calls but in configuring your application to do the right thing. Much of the complexity is in being responsive to the user while doing communications and in conserving battery life by idling with EvtGetEvent and SerReceiveWait.

TCP/IP

Top Of Page

In this section, we show you how to use TCP/IP on a Palm device. To accomplish this, we discuss the API for networking on a Palm device, we give you some programming tips for implementing TCP/IP, and last we create a small sample application that sends email to a Simple Mail Transfer Protocol (SMTP) server.

Network API

The Palm SDK (Version 2.0 or later) contains a net library that provides network services, like TCP/IP, to applications. With this library, an application on the Palm device can connect to any other machine on a network using standard TCP/IP protocols. The API for this library is a socket interface, modeled very closely on the Berkeley Sockets API.

NOTE:

Sockets are a communication mechanism. Information is sent into a socket on one machine and comes out of a socket on a remote machine (and vice versa). With a connection-oriented socket interface, you can establish the connection between the two machines prior to sending data. The connection stay opens whether or not data is sent. A good example of this is TCP. Sockets also allow a connectionless mode for sending datagrams. These can be sent to an address without any prior connection using a protocol like User Datagram Protocol (UDP).

NOTE:

For a brief introduction to Berkeley Sockets and socket programming, see http://www.ibrado.com/sock-faq/. For a more detailed discussion, see Unix Network Programming, by W. Richard Stevens (Prentice Hall; ISBN: 0-13-949876-1).

The similarity between the Berkeley Sockets API and the net library is so close that you can compile Berkeley Sockets code for the Palm OS with minor-and sometimes no-changes. As a result, porting networking code to the Palm OS is very simple.

The ported code works so nicely because the net library includes header files with macros that convert Berkeley Sockets calls to Palm OS calls. The main difference between the two is that calls to the net library accept three additional parameters. These are:

A networking reference number

All calls to the net library need to use an integer reference to the net library. The Berkeley Sockets macros pass the global AppNetRefnum as their first parameter.

An error code pointer

The Berkeley Sockets macros pass the address of the global variable errno.

A timeout

The net library routines return if they haven't finished before the timeout. The Berkeley Sockets macros pass the global AppNetTimeout. Note that the default timeout (two seconds) may be too short for communicating by modem to busy servers.

Tips for Using TCP/IP

Use Berkeley Sockets

Use the Berkeley Sockets interface in preference to the Palm OS API. This gives you two advantages. Your networking code is portable to other platforms, and programmers who are new to the Palm OS will find your code easier to read.

Use the Palm OS API if necessary

If you need to call networking code when your application globals aren't available, you must use the Palm OS API. You can't use the Berkeley Sockets API (which relies on the global variables errno, AppNetRefnum, and AppNetTimeout). Indeed, the only choice available to you is the Palm OS API, which allows, but doesn't require, globals.

Write and test the code on another platform

Consider writing and testing your networking code on another platform, perhaps Unix or Linux. Debugging tools for networking for the Palm OS are very primitive at the time of this book's writing (although POSE can make a dial-up connection on some machines, it's not able to do so reliably on all configurations). Much more sophisticated debugging tools are available in the Unix/Linux world.

Even if source-level debugging were available, the need to dial up a remote machine still makes this a choice of last resort. As debugging would require a dial-up connection, the test portion of the edit/compile/download/test cycle would be tediously long. On a Unix/Linux machine, you can probably test without a dial-up connection, as your machine might be on Ethernet. Single-machine testing is also possible via the loopback interface (an interface enabling a machine to make a network connection to itself).

Don't immediately close the connection

When you close the net library with NetLibClose, pass false as the immediate parameter; the net library then remains open until the user-specified timer expires. As a result of the clever design of NetLibClose, the user can switch to another application that makes networking calls without having to redial. If you pass true as the immediate parameter, the dial-up connection closed immediately, and the connection must be reestablished when the user switches to another application.

NOTE:

Imagine the situation of a user with three different network applications on the Palm device. The user might first check email with one application, next read a newsgroup, and last look at a few web sites. This is so common a situation that you should account for it in your code. If the emailer, newsreader, and web browser each closed the network connection when they closed, the user would be fairly annoyed at the unnecessary waits to reestablish a connection.

NOTE:

A better solution is to let the user determine when to close the network connection using the Preferences application. While it is true that the net library, when open, sucks up an enormous amount of memory and should be closed when not needed, it is also true that users often handle network tasks during discrete periods of time. Letting the network close after a user-specified time seems the best solution to both conditions. In your network application documentation, you can direct the user to the Preferences dialog, explain the situation concerning the network connection, and guide the choice of setting.

When to open a network connection

Consider carefully when to open the net library and establish a connection. Your StartApplication routine is probably not a very good choice, as the Palm device would dial-up for a connection as soon as the user tapped the application icon. A better way to handle this is to wait for some explicit user request to make the connection; such a request could be put in a menu.

Sample Network Application

Our Sales application does not use network services, so we've created a custom sample application to show you how to use the net library and communicate over a network. Our example sends email to an SMTP server. The user fills in:

When the user taps Send, we connect to the SMTP server and send the email message using the SMTP protocol.

NOTE:

The SMTP protocol is documented in RFC 821. Various sites contain the RFC documents; see http://www.yahoo.com/Computers_and_Internet/Standards/RFCs/ for a list.

The sample on Linux

Following our own advice, we first created this sample application on another platform and then ported it to the Palm OS. The application was originally written on a Linux machine and tested with a simple command-line interface. Here's the header file that describes the interface to sendmail:

typedef void   (*StatusCallbackFunc)(char *status);
typedef void   (*ErrorCallbackFunc)(char *problem, char *extraInfo);

int sendmail(char *smtpHost, char *from, char *to, char *subject, 
   char *data, StatusCallbackFunc statusFunc,
   ErrorCallbackFunc errorFunc);

The data parameter is the body of the mail message, with individual lines separated by newline characters ('\n'). The   StatusCallbackFunc and ErrorCallbackFunc are used to provide status (although sendmail doesn't currently provide status) and error information to the caller. These are abstracted from the sendmail routine itself to make porting the program easier. A Linux command-line program has very different error reporting than a Palm OS application.

The Linux main program

Here's the Linux main program that operates as a test harness for sendmail:

#include "sendmail.h"
#include <stdio.h>

void MyStatusFunc(char *status)
{
   printf("status: %s\n", status);
}

void MyErrorFunc(char *err, char *extra)
{
   if (extra)
      printf("error %s: %s\n", err, extra);
   else
      printf("error %s\n", err);
}

char    gMailMessage[5000];

int main(int argc, char **argv)
{
   if (argc != 5) {
      fprintf(stderr,
         "Usage: TestSendMail smtpServer fromAddress toAddres subject\n");
         exit(1);
   }
   fread(gMailMessage, sizeof(gMailMessage), 1, stdin);
   sendmail(argv[1], argv[2], argv[3],
      argv[4], gMailMessage,
      MyStatusFunc, MyErrorFunc);
   return 0;
}

Linux include files and global definitions

Here are the include files and global definitions from sendmail.c:

#include <sys/socket.h>
#include <netdb.h>
#include <netinet/in.h>
#include <arpa/inet.h>

// Application headers

#include "sendmail.h"


static const int kLinefeedChr = '\012';
static const int kCrChr = '\015';

static StatusCallbackFunc gStatusFunc;
static ErrorCallbackFunc  gErrorFunc;

Sending the mail

Here's the sendmail function, where we send data:

#define  kOK         '2'
#define  kWantMore   '3'

int sendmail(char *smtpHost, char *from, char *to, char *subject, 
   char *data, StatusCallbackFunc statusFunc,
   ErrorCallbackFunc errorFunc)
{
   int success = 0;
   int   fd        = -1;      // socket file descriptor
   
   gErrorFunc = errorFunc;
   gStatusFunc = statusFunc;

   // open connection to the server
   if ((fd = make_connection("smtp", SOCK_STREAM, smtpHost)) < 0 )
   {
      (*errorFunc)("Couldn't open connection", NULL);
      goto _Exit;
   }

   // send & receive the data
   if (!GotReply(fd, kOK))
      goto _Exit;
            
   if (!Send(fd, "HELO [", "127.0.0.1", "]"))
      goto _Exit;    
   if (!GotReply(fd, kOK))
      goto _Exit;
      
   if (!Send(fd, "MAIL from:<", from, ">"))
      goto _Exit;    
   if (!GotReply(fd, kOK))
      goto _Exit;

   if (!Send(fd, "RCPT to:<", to, ">"))
      goto _Exit;    
   if (!GotReply(fd, kOK))
      goto _Exit;

   if (!Send(fd, "DATA", NULL, NULL))
      goto _Exit;    
   if (!GotReply(fd, kWantMore))    
      goto _Exit;

   if (!Send(fd, "Subject: ", subject, NULL))
      goto _Exit;    

// need empty line between headers and data
   if (!Send(fd, NULL, NULL, NULL))
      goto _Exit;    
   
   if (!SendBody(fd,data))
      goto _Exit;    
   if (!Send(fd, ".", NULL, NULL))
      goto _Exit;    
      
   if (!GotReply(fd, kOK))    
      goto _Exit;

   if (!Send(fd, "QUIT", NULL, NULL))
      goto _Exit;    
      

   success = 1;
   // cleanup the mess...

_Exit:

   if ( fd >= 0 ) close( fd );

   return success;
}  

We make a connection to the SMTP server and alternate receiving status information and sending data. The entire conversation is in ASCII; every response from the SMTP server has a numeric code as the first three digits. We look at the first digit to determine whether a problem has occurred. A digit of "2" signifies that everything is fine. A digit of "3" signifies that more data is needed (the expected response when we send the DATA command; it's asking us to send the body of the email). Any other digit (for our purposes) represents an error.

The protocol specifies that each sent line ends with <CRLF> and that the body of the email ends with a period (.) on a line by itself. Any lines beginning with a period must have the period duplicated (for instance, ".xyz" is sent as "..xyz"); the SMTP server strips the extra period before processing the email.

Connecting to the server

The function  make_connection actually makes the connection to the SMTP server:

/* This is a generic function to make a connection to a given server/port.
   service is the port name/number,
   type is either SOCK_STREAM or SOCK_DGRAM, and
   netaddress is the host name to connect to.
   The function returns the socket, ready for action.*/
static int make_connection(char *service, int type, char *netaddress)
{
  /* First convert service from a string, to a number... */
  int port = -1;
  struct in_addr *addr;
  int sock, connected;
  struct sockaddr_in address;

  if (type == SOCK_STREAM) 
    port = atoport(service, "tcp");
  if (type == SOCK_DGRAM)
    port = atoport(service, "udp");
  if (port == -1) {
   (*gErrorFunc)("make_connection:  Invalid socket type.\n", NULL);
    return -1;
  }
  addr = atoaddr(netaddress);
  if (addr == NULL) {
    (*gErrorFunc)("make_connection:  Invalid network address.\n", NULL);
    return -1;
  }
 
  memset((char *) &address, 0, sizeof(address));
  address.sin_family = AF_INET;
  address.sin_port = (port);
  address.sin_addr.s_addr = addr->s_addr;

  sock = socket(AF_INET, type, 0);

  if (type == SOCK_STREAM) {
    connected = connect(sock, (struct sockaddr *) &address, 
      sizeof(address));
    if (connected < 0) {
     (*gErrorFunc)("connect", NULL);
      return -1;
    }
    return sock;
  }
  /* Otherwise, must be for udp, so bind to address. */
  if (bind(sock, (struct sockaddr *) &address, sizeof(address)) < 0) {
   (*gErrorFunc)("bind", NULL);
    return -1;
  }
  return sock;
}

This function uses the Berkeley Sockets API calls socket and connect (bind is used only for datagram sockets). Note that connect returns a file descriptor that is used in later read, write, and close calls.

Getting a port

To connect, we have to specify an address consisting of an IP address and a port number. We use atoport to convert a well-known service name to a port number:

/* Take a service name, and a service type, and return a port number.  The 
   number returned is byte ordered for the network. */
static int atoport(char *service, char *proto)
{
  int port;
  struct servent *serv;

  /* First try to read it from /etc/services */
  serv = getservbyname(service, proto);
  if (serv != NULL)
    port = serv->s_port;
  else { 
      return -1; /* Invalid port address */
  }
  return port;
}

atoport uses the Berkeley Sockets API function getservbyname. Then atoaddr converts a hostname (or string of the form "aaa.bbb.ccc.ddd") to an IP address:

/* Converts ascii text to in_addr struct.  NULL is returned if the address
   can not be found. */
static struct in_addr *atoaddr(char *address)
{
  struct hostent *host;
  static struct in_addr saddr;

  /* First try it as aaa.bbb.ccc.ddd. */
  saddr.s_addr = inet_addr(address);
  if (saddr.s_addr != -1) {
    return &saddr;
  }
  host = gethostbyname(address);
  if (host != NULL) {
    return (struct in_addr *) *host->h_addr_list;
  }
  return NULL;
}

Note also that atoaddr uses the Berkley Sockets gethostbyname call.

Reading data character by character

Once the connection has been made, we need to start sending and receiving data. We use a utility routine,  sock_gets, to read an entire <CRLF>-delimited line (note that it reads one character at a time):

/* This function reads from a socket, until it receives a linefeed
   character.  It fills the buffer "str" up to the maximum size "count".
   This function will return -1 if the socket is closed during the read
   operation.

   Note that if a single line exceeds the length of count, the extra data
   will be read and discarded!  You have been warned. */
static int sock_gets(int sockfd, char *str, size_t count)
{
  int bytes_read;
  int total_count = 0;
  char *current_position;
  char last_read = 0;
  const char kLinefeed = 10;
  const char kCR = 13;

  current_position = str;
  while (last_read != kLinefeed) {
    bytes_read = read(sockfd, &last_read, 1);
    if (bytes_read <= 0) {
      /* The other side may have closed unexpectedly */
      return -1; /* Is this effective on other platforms than linux? */
    }
    if ( (total_count < count) && (last_read != kLinefeed) && 
       (last_read != kCR) ) 
    {
      current_position[0] = last_read;
      current_position++;
      total_count++;
    }
  }
  if (count > 0)
    current_position[0] = 0;
  return total_count;
}

The sendmail protocol specifies that the server may send us multiple lines for any reply. The last line will start with a three-digit numeric code and a space (###þ ); any previous lines will have a - instead of the space (###-). We need to keep reading until we read the last line.  ReadReply does that:

#define IsDigit(c) ((c) >= '0' && (c) <= '9')

// reads lines until we get a non-continuation line
static int ReadReply(int fd, char *s, unsigned int sLen)
{
   int      numBytes;
   do {
      numBytes = sock_gets(fd, s, sLen);
   } while (numBytes >= 0 && !(strlen(s) >= 4 && s[3] == ' ' && 
      IsDigit(s[0]) && IsDigit(s[1]) && IsDigit(s[2])));
   if (numBytes < 0)
         return numBytes;
   else
      return 0;
}

We use ReadReply in  GotReply, which takes an expected status character and returns true if we receive that character and false otherwise:

#define kMaxReplySize 512

static int GotReply(int fd, char expectedLeadingChar)
{
   int      err;
   char  reply[kMaxReplySize];
   
   err = ReadReply(fd, reply, sizeof(reply));
   if (err != 0) {
      (*gErrorFunc)("Read error", NULL);
      return 0;
   }
   if (*reply != expectedLeadingChar) {
      (*gErrorFunc)("Protocol error", reply);
      return 0;
   }     
   return 1;
}

The SMTP protocol specifies that no reply will exceed 512 characters (including the trailing <CRLF> characters); that's why we can safely define kMaxReplySize as we did. If the digit we read doesn't match the expected character, we call the error function, passing the line itself. This works well because the server usually provides a reasonable English error message with the numeric code. As a result, the user gets more than "Protocol error" for error information. This is all there is to reading data.

Sending data character by character

Having taken care of reading data, now we need to deal with sending it.  Send sends one line of data (and tacks on a <CRLF> pair at the end):

// sends s1 followed by s2 followed by s3 followed by CRLF
static int Send(int fd, char *s1, char *s2, char *s3)
{
   
   if (s1 && nwrite(fd, s1, strlen(s1)) < 0)
      goto error;
   if (s2 && nwrite(fd, s2, strlen(s2)) < 0)
      goto error;
   if (s3 && nwrite(fd, s3, strlen(s3)) < 0)
      goto error;
   if (nwrite(fd, "\015\012", 2) < 0)
      goto error;
   return 1;
   
error:
   (*gErrorFunc)("Write error", NULL);
   return 0;
}

 SendBody sends the body of the email:

static int SendBody(int fd, char *body)
{
   char  *lineStart = body;
   int   result = 0;

   // send all the newline-terminated lines
   while (*body != '\0' && result == 0) {
      if (*body == '\n') {
         result = SendSingleBodyLine(fd, lineStart,
            body - lineStart);
         lineStart = body + 1;
      }
      body++;
   }

   // send the last partial line
   if (lineStart < body && result == 0) 
      result = SendSingleBodyLine(fd, lineStart,
         body - lineStart);
   return result;
}

It relies on  SendSingleBodyLine, which converts \n chars to <CRLF> and doubles "." characters that occur at the beginning of lines:

 // sends aLine which is length chars long
static int SendSingleBodyLine(int fd, char *aLine, int length)
{
   if (*aLine == '.') // double-up on '.' lines
      if (nwrite(fd, ".", 1) < 0)
         goto error;
   if (nwrite(fd, aLine, length) < 0)
      goto error;
   if (nwrite(fd, "\015\012", 2) < 0)
      goto error;
error:
   (*gErrorFunc)("Write error", NULL);
   return 0;

Both these sending routines use  nwrite, a utility routine that does our writing:

static unsigned int nwrite(int fd, char *ptr, unsigned int nbytes)
{
   unsigned int   nleft;
   int            chunk;
   int         nwritten;

   nleft = nbytes;
   while (nleft > 0) {
   
      if (nleft > 0x7000) chunk = 0x7000;
      else chunk = nleft;
      
      nwritten = write(fd, ptr, chunk);
      if (nwritten <= 0)
         return(nwritten);    /* error */

      nleft -= nwritten;
      ptr   += nwritten;
   }
   return(nbytes - nleft);
}

This routine loops through, calling write over and over until all the data is sent. For sockets, the write routine may not send all the data you request. A lesser amount may be all that will fit in a packet.

Testing the Linux application

Testing was simplified because the Linux machine is on a network with a full-time connection to the Internet. Therefore, we have no time delays in making a connection. (If it hadn't had a full-time connection, we could have run an SMTP server on the Linux machine and run standalone, with no connection to the Internet.)

We used the Linux source-level debugger, GDB, to step through the original code. We also fixed some errors in our original attempt.

Porting the Linux application to Palm OS

Now let's take a look at what it will take to port the Linux application to the Palm OS world. The sendmail.c requires only one small change in order to work under the Palm OS. Another couple of changes need to be made to the include files for the Palm OS, as they are slightly different. We use sys_socket.h instead of sys/socket.h. No other changes to the guts of the application, sendmail.c, are necessary:

#ifdef linux
#include <sys/socket.h>
#include <netdb.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#else
#include <sys_socket.h>
#endif

We need to handle the user interface of a Palm OS application. We won't use the command-line interface of the Linux application. In our main source file, PilotSend.c, we must include NetMgr.h, declare AppNetRefnum, and define errno:

#include <NetMgr.h>
extern Word AppNetRefnum;
Err   errno;               // needed for Berkely socket interfaces

We have fairly primitive error and status routines; all they do is put up an alert:

static void MyErrorFunc(char *error, char *additional)
{
   FrmCustomAlert(ErrorAlert, error, additional ? additional : "", NULL);
}

static void MyStatusFunc(char *status)
{
   FrmCustomAlert(StatusAlert, status, NULL, NULL);
}

We also need a new utility routine that returns the text in a field:

// returns (locked) text in a field object
static char *GetLockedPtr(Word objectID)
{
   FormPtr  frm = FrmGetActiveForm();
   FieldPtr fld = FrmGetObjectPtr(frm, FrmGetObjectIndex(frm, objectID));
   Handle   h = FldGetTextHandle(fld);
   
   if (h)
      return MemHandleLock(h);
   else
      return 0;
}

Here's the guts of the sending portion of our event-handling routine. Just as in the Linux version of the application, we still call sendmail to send the data:

if (event->data.ctlEnter.controlID == SendmailMainSendButton) {
   if (SysLibFind( "Net.lib", &AppNetRefnum) == 0) {
      Word  interfaceError;
      Err   error;
      char  *smtpServer = GetLockedPtr(SendmailMainSmtpHostField);
      char  *to = GetLockedPtr(SendmailMainToField);
      char  *from = GetLockedPtr(SendmailMainFromField);
      char  *subject = GetLockedPtr(SendmailMainSubjectField);
      char  *body = GetLockedPtr(SendmailMainBodyField);
      
      if (!smtpServer)
         MyErrorFunc("Missing smtpServer", NULL);
      else if (!to)
         MyErrorFunc("Missing to", NULL);
      else if (!from)
         MyErrorFunc("Missing from", NULL);
      else if (!body)
         MyErrorFunc("Missing body", NULL);
      else  {
         error = NetLibOpen(AppNetRefnum, &interfaceError);
         if (interfaceError != 0) {
            MyErrorFunc("NetLibOpen: interface error", NULL);
            NetLibClose(AppNetRefnum, true);
         } else if (error == 0 || error == netErrAlreadyOpen) {
            if (sendmail(smtpServer, from, to, 
               subject, body, MyStatusFunc, MyErrorFunc))
               MyStatusFunc("Completed successfully");
            NetLibClose(AppNetRefnum, false);   
         } else
            MyErrorFunc("netLibOpen error", NULL);
      }
         if (smtpServer)
            MemPtrUnlock(smtpServer);
         if (to)
            MemPtrUnlock(to);
         if (from)
            MemPtrUnlock(from);
         if (subject)
            MemPtrUnlock(subject);
         if (body)
            MemPtrUnlock(body);
  }
   else
      MyErrorFunc("Can't SysLibFind", NULL);
}
handled = true;
break;

The only additional networking code we need is a call to open the net library (NetLibOpen) and a call to close it (NetLibClose). Note that NetLibClose does not immediately close the network connection, but relies on the user's preferences for when to do so.

TCP/IP Summary

You can see from this example that writing code that uses network services on the Palm OS is fairly simple. A distinct advantage of Palm's implementation of the Berkeley Sockets API is that you can easily have code that ports to many platforms. This also made it possible to write the data-sending portion of the email program, the sendmail function, on another platform where testing was easier. Very little was required to get that email program up and running on the Palm platform after the Linux version was tested. We simply had to give the Palm application a user interface, including error information, and put a new shell around the data-sending portion of the code.


* Had we been willing to sacrifice a HotSync cable, we could have cut off the DB-9 end and soldered on a replacement Garmin end. However, we weren't willing to make the sacrifice (although Figure 9-4 would certainly have looked less cluttered).

ü The NMEA 0183 protocol is a document that is available only in hard copy form. It can be ordered from NMEA at (252) 638-2626.

Palm Programming: The Developer's Guide
Copyright © 1999, O'Rielly and Associates, Inc.
Published on the web by permission of O'Rielly and Associates, Inc. Contents modified for web display.

Previous PageTop Of PageTable Of ContentsIndexNext Page