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.
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
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
Parity and general error checking