A simple communications program

Let us assume you want a simple communications program to use for accessing a local computer bulletin board (BBS).  You know the parameters for the board: it is 14400 baud, 8 data bits, one stop bit, and no parity.

You want to display data on your screen, to type data, and have it sent to the bulletin board.  You intend to use a modem connected to COM1.  The following short program serves as a starting point:

#COMPILE EXE

#DIM ALL

%TRUE  = -1

%FALSE = 0

 

DECLARE SUB Display(BYVAL sData AS STRING)

 

FUNCTION PBMAIN

  LOCAL hComm   AS LONG

  LOCAL Echo    AS LONG

  LOCAL Qty     AS LONG

  LOCAL Stuf    AS STRING

  LOCAL MyInput AS STRING

 

  hComm = FREEFILE

  COMM OPEN "COM1" AS #hComm

  IF ERRCLEAR THEN EXIT FUNCTION 'Exit if port cannot be opened

 

  COMM SET #hComm, BAUD     = 14400    ' 14K4 baud

  COMM SET #hComm, BYTE     = 8        ' 8 bits

  COMM SET #hComm, PARITY   = %FALSE   ' No parity

  COMM SET #hComm, STOP     = 0        ' 1 stop bit

  COMM SET #hComm, TXBUFFER = 1024     ' 1 Kb transmit buffer

  COMM SET #hComm, RXBUFFER = 1024     ' 1 Kb receive buffer

 

  Echo = -1                         ' Set echo ON

  WHILE %TRUE                       ' loop forever

    WHILE NOT INSTAT                ' unless key pressed

      Qty = COMM(#hComm, RXQUE)

      IF ISTRUE Qty THEN

        COMM RECV #hComm, Qty, Stuf ' read incoming characters

        Display Stuf                ' display the raw data

      END IF                        ' transmitter

    WEND

 

    WHILE INSTAT                    ' Any keypresses?

      MyInput = INKEY$              ' get them

      IF MyInput = $ESC THEN Terminate

      COMM SEND #hComm, MyInput     ' send typed characters

      IF Echo THEN Display MyInput  ' display them

    WEND

  WEND                ' check for more incoming characters

 

Terminate:

  COMM CLOSE #hComm   ' Close the comm port and exit

 

END FUNCTION

 

SUB Display(BYVAL sData AS STRING)  ' handles embedded CR/LF bytes

  LOCAL sDataPtr AS BYTE PTR

  LOCAL y AS LONG

 

  REPLACE $LF WITH "" IN sData      ' reduce CR/LF to CR

 

  sDataPtr = STRPTR(sData)

 

  FOR y = 0 TO LEN(sData) - 1

    IF @sDataPtr[y] = 13& THEN

      PRINT                         ' force new line on CR

      ITERATE FOR

    END IF

    PRINT CHR$(@sDataPtr[y]);       ' display current char

  NEXT y

END SUB

The above program allows you to connect with the bulletin board, but it will not dial the number of the bulletin board through the program itself.  You can do that easily, though, in one of two ways, as follows:

You can dial the bulletin board manually.  When you are done dialing, connect the telephone line to the modem (or press a button on your modem, switching the line from the telephone back to the modem).  The program should now be ready to receive whatever the bulletin board sends.

You can send the appropriate signals directly to the modem itself.  Most modems recognize a common command set originated by the Hayes Company.  To initialize the modem and dial, you would enter the following commands:

ATZ

ATDT18005551212

Note: some modems require capital letters for AT commands.  Lowercase letters will not work.

After you have entered the ATZ command, the modem responds.  You will see the message "OK" on your screen.  After you have entered ATDT and the telephone number, the modem's lights flicker for a moment.  If your modem is capable of making a sound, you will hear the sounds of the number being dialed and the telephone ringing at the other end.

If the number is busy, you may hear a busy signal through your modem speaker, or you may not hear anything more.  If the connection is made, you may see some garbage characters on your screen.

At this point, many users become concerned and think that something must be wrong.  Why are there illegible characters onscreen?  Relax: this happens often.  The computer you called does not yet know what baud rate and communications parameters you are using.  In most cases, you should press ENTER a few times; the computer at the other end will use that character to determine what your parameters are and will adjust itself accordingly.  Soon afterward, you should see a welcoming message.  You may now type whatever you like.

If you see doubled characters, change the line that sets the value of echo.  Instead of setting echo to -1, set it to 0.  If you wish to send a stream of AT commands to a modem in quick succession, you may be required to add a small delay between each AT command in order to give the modem time to decode each command and respond appropriately.  A delay of 100 to 200 milliseconds (mSec) is usually sufficient.

Using disk files  

The sample program above does not let you save material to a disk file, or send data from a disk file to the bulletin board.  Nevertheless, those two options are very useful.  How do you do it?

Let us suppose you wanted to send a disk file to the bulletin board.  To do that, the routine that sends your keystrokes to the bulletin board must be altered.  The usual way to do this is to assign a special keystroke a different meaning.  Instead of being sent, the keystroke is interpreted as a command to get the name of a disk file, read that disk file, and then send it to the bulletin board.

Let us make CTRL+T mean "transmit a disk file".  CTRL+T is ASCII code 20; alter the previous program to check for that character.  To do that, add one line immediately after the line reading:

MyInput = INKEY$

The added line reads:

IF MyInput = CHR$(20) THEN GOSUB ReadDiskFile

Now add the following DIM statements immediately after the original set of DIM statements (just under the top of the PBMAIN function header):

' ReadDiskFile variables

LOCAL FileNam AS STRING

LOCAL hFile   AS LONG

LOCAL FileLen AS LONG

LOCAL Chunk   AS LONG

LOCAL i       AS LONG

LOCAL Stuf2   AS STRING

Now add the following code immediately after the EXIT FUNCTION and before the END FUNCTION statement at the bottom of the file:

ReadDiskFile:

  ' a subroutine to read a disk file and transmit it

 

  LINE INPUT "Name of the disk file: "; FileNam

 

  hFile = FREEFILE

  OPEN FileNam FOR BINARY AS #hFile    'open as binary file

  FileLen = LOF(hFile)                 'get the file length

  Chunk   = COMM(#hComm, TXBUFFER) \ 2 'use 1/2 TX buffer per

                                       'chunk

 

  FOR ix = 1 TO FileLen \ Chunk

    GET$ #hFile, Chunk, Stuf2   'read the file

    COMM SEND #hComm, Stuf2     'and send it

  NEXT

 

  IF FileLen MOD Chunk <> 0 THEN 'is there more to send?

    GET$ #hFile, FileLen MOD Chunk, Stuf2

    COMM SEND #hComm, Stuf2

  END IF

 

  CLOSE #hFile

 

  MyInput = "" 'don't send the original CTRL+T

  RETURN

The routine works, but there's no error-checking in it.  If the disk file does not exist, nothing is sent, but a zero-length file is created.  If you enter an illegal file name, the program will set the ERR system variable to indicate that [a potentially fatal] error has occurred.  You will probably want to add some kind of error checking to the program for those reasons.  See Errors and Error Trapping for detailed information on error trapping.

To receive a disk file, we will use CTRL+R (ASCII 18); you can choose another key if you like.  However, things are not quite that simple: there should also be a way to stop receiving a disk file.  We will use a second CTRL+R to stop receiving to a disk file as well.  To accomplish this, you will need yet another subroutine.

After the line:

IF MyInput = CHR$(20) THEN GOSUB ReadDiskFile

Add the following line:

IF MyInput = CHR$(18) THEN GOSUB WriteDiskFile

Now add the following variables immediately after the existing LOCAL statements:

' WriteDiskFile variables

LOCAL Already AS LONG

LOCAL hFile2  AS LONG

Here is the new subroutine.  Place this after the ReadDiskFile routine we added:

WriteDiskFile:

  ' subroutine to write received data to a disk file

 

  MyInput = ""

  IF Already THEN  ' already writing to disk file so stop

    RESET Already

    CLOSE hFile2                       ' close the file

    PRINT "Finished writing file!"

    RETURN

  END IF

 

  hFile2 = FREEFILE

  Already = 1   ' set flag when file is open

  LINE INPUT "Output file name: "; FileNam

  OPEN FileNam FOR APPEND AS #hFile2

  RETURN

The output file is now open.  The received data will be appended to the end of any existing file of that name.  However, we have not provided any way to actually save any of that information.  To do that, add one more small line of code immediately after the line:

COMM RECV #hComm, Qty, S     tuf ' read incoming characters

The added line reads:

IF Already THEN PRINT #hFile2, Stuf; ' save to disk file

Finishing touches  

If we examine this example file, we find that we have overlooked one problem: if the program is terminated while the output file is in use, the file is not closed.

While this is not a fatal condition, it is a poor approach to program design: we should always close the files we have opened.  Remembering to perform this chore will stand you in good steed when it comes to using the Windows API functions.  In many cases, failing to close a registry key or delete a GDI object can cause bugs, both deceptive and difficult to locate, or memory leaks that reduce system memory even after your program has ended.  The golden rule should always be before you leave, clean up after yourself.

So, faced with this problem, how do we know if the output file is open before we end the program?  PowerBASIC provides an effective mechanism to test the state of a file number: the FILEATTR function.

After the line that reads:

COMM CLOSE #hComm ' Close the comm port and exit

We add the following line to the file:

IF ISTRUE hFile2 AND ISTRUE FILEATTR(hFile2, 0) THEN CLOSE #hFile2

This line initially tests for a non-zero value in hFile2.  Because PowerBASIC uses Short-circuit evaluation within IF/THEN statements, the FILEATTR function is only called if the file number variable is found to be non-zero (logical TRUE).  FILEATTR with a 0 in the second parameter position returns a non-zero result if the file is still open.

In this instance, we control three possible scenarios with only one line of code:

1.        The output file feature was not used (hFile2 = 0)

2.        The output file remained open when the program was about to end (hFile2 <> 0)

3.        The output file had been used, but had been closed before program termination (hFile2 <> 0)

It is true that we could have just closed the file associated with hFile2 regardless of the state of the file or the value of the file number.  However, in most programming circles, that is considered to be a bad approach.  It is always better to write code that is fail-safe in as many conditions as possible.

For final improvements, we have added another small feature that allows the user to change the Echo State while the program is running.  We have written an addition to the program so that pressing CTRL+E changes Echo from 1 to 0 or from 0 to 1.

The final program can be found in the PB\SAMPLES\COMM folder of your PowerBASIC installation.  It is not very large, but it handles a surprising number of ordinary communications tasks.  It lacks some error checking, as has been noted.  If you choose to modify this program, you might want to put some error checking in.  You might also want to allow the operator to enter no file name, and return with no damage done.

In addition, the structure of the program puts first priority to keyboard input.  If the operator takes too long to enter a file name, and the other computer is sending material at the same time, the input buffer will overflow.  Maybe you can have the program check for that possibility and store the incoming buffered data into a temporary string, or perhaps expand the input buffer to more than 1024 bytes.  At 1200 baud, with no parity, 8 data bits, and 1 stop bit, it takes about eight seconds for the buffer to fill.  With a buffer of 8192 bytes, it takes over a minute for the buffer to fill; that should be plenty of time for the operator to enter a file name.  With modern modem speeds of 56 Kbps, you are likely to find that a much larger buffer is required.

If you see doubled characters onscreen whenever you type, the other computer is echoing (sending back a copy of) what you type.  To stop this echoing, press CTRL+E.  By experimenting and adding features here and there, you can get a very effective program that does exactly what you want.

 

See Also

Serial Communications

Communications Basics

Communication Buffers

Parity and general error checking

Start and Stop bits

Opening a communications port

Reading and writing data