A simple communications program

Let's assume you want a simple communications program to use for accessing a local computer bulletin board. 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, be able 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, and uses PowerBASIC's new DDT features to create the user interface:

'----------------------------------------------------------

'

' Serial Communications Example for PowerBASIC for Windows

' Copyright (C) 2004-2009 PowerBASIC, Inc.

'

' Be sure to set the $ComPort constant to the appropriate

' COM port before compiling this example!

'

'----------------------------------------------------------

#COMPILE EXE

#DIM ALL

#INCLUDE "WIN32API.INC"

$ComPort = "COM1"

$AppTitle = "PowerBASIC for Windows Comm Example"

%IDD_MAIN = 100

%IDC_LISTBOX1 = 101

%IDC_EDIT1 = 102

%IDC_SEND = 103

%IDC_QUIT = 106

%IDC_ECHO = 107

GLOBAL hComm AS LONG

GLOBAL Updating AS LONG

GLOBAL hThread AS DWORD

GLOBAL ThreadClose AS LONG

DECLARE FUNCTION StartComms AS LONG

DECLARE FUNCTION SendLine(ASCIIZ) AS LONG

DECLARE THREAD FUNCTION ReceiveData(BYVAL LONG) AS LONG

DECLARE FUNCTION EndComms AS LONG

DECLARE FUNCTION AddLine(BYVAL LONG, BYVAL LONG, ASCIIZ) AS LONG

CALLBACK FUNCTION Dialog_Callback() AS LONG

SELECT CASE CB.MSG

CASE %WM_INITDIALOG

' Set focus to the edit control

CONTROL SET FOCUS CB.HNDL, %IDC_EDIT1

' Set SELECTION range to highlight the initial entry

CONTROL SEND CB.HNDL, %IDC_EDIT1, %EM_SETSEL, 0, -1

' Return 0 to stop dialog box engine setting focus

FUNCTION = %FALSE

END SELECT

END FUNCTION

CALLBACK FUNCTION Send_Callback() AS LONG

DIM SendText AS ASCIIZ * 1024, ListCount AS LONG

DIM lResult AS LONG, hListBox AS DWORD

' Obtain the text to send from the edit control

CONTROL GET TEXT CB.HNDL, %IDC_EDIT1 TO SendText

' Set the update flag

Updating = %TRUE

' Send the line to the comm port

IF SendLine(SendText) THEN

SendText = "Transmission Error!"

ELSE

' Check the Echo mode state

CONTROL GET CHECK CB.HNDL, %IDC_ECHO TO lResult

IF ISTRUE lResult THEN SkipEcho

END IF

' Add the echo to the listbox

CALL AddLine(CB.HNDL, %IDC_LISTBOX1, "<== " + SendText)

SkipEcho:

' Set the SELECTION range for the edit control so the

' next keypress "clears" the existing text

CONTROL SEND CB.HNDL, %IDC_EDIT1, %EM_SETSEL, 0, -1

' restore the keyboard focus to the edit control

CONTROL SET FOCUS CB.HNDL, %IDC_EDIT1

' Release the update flag

Updating = %FALSE

FUNCTION = %TRUE

END FUNCTION

CALLBACK FUNCTION Quit_Callback() AS LONG

' Kill the dialog and let PBMAIN() continue

DIALOG END CB.HNDL, 0

FUNCTION = 1

END FUNCTION

FUNCTION AddLine(BYVAL hWnd AS DWORD, BYVAL nID AS LONG, SendText AS ASCIIZ) AS LONG

DIM ListCount AS LONG

' Find the current listbox count

LISTBOX GET COUNT hWnd, nID TO ListCount

' Update the listbox

LISTBOX ADD hWnd, nID, SendText

' Scroll the new item into view

LISTBOX SELECT hWnd, nID, ListCount + 1

END FUNCTION

FUNCTION PBMAIN

' Build our GUI interface.

DIM hDlg AS DWORD, Txt(1 TO 1) AS STRING, lResult AS LONG

' Initialize the port ready for the session

IF ISFALSE StartComms THEN

MSGBOX "Failure to start communications!",, $AppTitle

EXIT FUNCTION

END IF

Txt(1) = "Listbox holds the transmission I/O stream..."

' Create a modal dialog box

DIALOG NEW 0, $AppTitle,,, 330, 203, %WS_POPUP OR %WS_VISIBLE OR %WS_CLIPCHILDREN OR _

%WS_CAPTION OR %WS_SYSMENU OR %WS_MINIMIZEBOX, 0 TO hDlg

' Add our application controls

CONTROL ADD LABEL, hDlg, -1, "Transmission &log for " & $ComPort, 9, 5, 100, 10, 0

CONTROL ADD LISTBOX, hDlg, %IDC_LISTBOX1, Txt(), 9, 15, 313, 133, %WS_BORDER OR _

%LBS_WANTKEYBOARDINPUT OR %LBS_DISABLENOSCROLL OR %WS_VSCROLL OR %WS_GROUP OR _

%WS_TABSTOP OR %LBS_NOINTEGRALHEIGHT

CONTROL ADD LABEL, hDlg, -1, "Te&xt to send", 9, 151, 100, 10, 0

CONTROL ADD TEXTBOX, hDlg, %IDC_EDIT1, "ATZ", 9, 161, 257, 12, %ES_AUTOHSCROLL OR _

%ES_NOHIDESEL OR %WS_BORDER OR %WS_GROUP OR %WS_TABSTOP

CONTROL ADD BUTTON, hDlg, %IDC_SEND, "Send &Text", 273, 160, 50, 14, %WS_GROUP OR _

%WS_TABSTOP OR %BS_DEFPUSHBUTTON CALL Send_Callback

CONTROL ADD BUTTON, hDlg, %IDC_QUIT, "&Quit", 273, 182, 50, 14, %WS_GROUP OR %WS_TABSTOP _

CALL Quit_Callback

CONTROL ADD CHECKBOX, hDlg, %IDC_ECHO, "Disable Local "+ "&Echo", 252, 5, 70, 10, _

%WS_GROUP OR %WS_TABSTOP OR %BS_AUTOCHECKBOX OR %BS_LEFTTEXT

' Erase our array to free memory no longer required

REDIM Txt()

' Create a "listen" Thread to monitor input from the modem

THREAD CREATE ReceiveData(hDlg) TO hThread

' Start the dialog box & run until DIALOG END executed.

DIALOG SHOW MODAL hDlg, CALL Dialog_Callback TO lResult

' Close down our "listen" Thread

ThreadClose = %TRUE

DO

THREAD CLOSE hThread TO lResult

' Release time-slice for improved multitasking

SLEEP 0

LOOP UNTIL ISTRUE lResult

' Flush & close the comm port

CALL EndComms

FUNCTION = %TRUE

END FUNCTION

FUNCTION StartComms AS LONG

hComm = FREEFILE

COMM OPEN $COMPORT AS #hComm

IF ERRCLEAR THEN EXIT FUNCTION ' Port problem?

COMM SET #hComm, BAUD = 14400 ' 14400 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 = 4096 ' 4 Kb transmit buffer

COMM SET #hComm, RXBUFFER = 4096 ' 4 Kb receive buffer

FUNCTION = %TRUE

END FUNCTION

FUNCTION SendLine(SendText AS ASCIIZ) AS LONG

COMM PRINT #hComm, SendText

END FUNCTION

THREAD FUNCTION ReceiveData(BYVAL hWnd AS DWORD) AS LONG

DIM InboundData AS STRING

DIM Stuf AS STRING, ListCount AS LONG

DIM Qty AS LONG, x AS LONG, a AS STRING

WHILE ISFALSE ThreadClose

' Test the RX buffer

Qty = COMM(#hComm, RXQUE)

' Abort this iteration if sending

IF ISFALSE Qty OR Updating THEN

SLEEP 100

ITERATE LOOP

END IF

' Read incoming characters

COMM RECV #hComm, Qty, Stuf

InBoundData = InBoundData & Stuf

' strip out LF characters

REPLACE CHR$(10) WITH "" IN InBoundData

' process only complete lines of data terminated by CR

WHILE INSTR(InboundData, CHR$(13))

' Display the data

CALL AddLine(hWnd, %IDC_LISTBOX1, "==> " + EXTRACT$(InBoundData, CHR$(13)))

' reduce the buffer to remove the "displayed" line

InBoundData = STRDELETE$(InBoundData, 1, LEN(EXTRACT$(InBoundData, CHR$(13))) + 1)

WEND

WEND

FUNCTION = %TRUE

END FUNCTION

FUNCTION EndComms() AS LONG

DIM dummy AS STRING

' Flush the RX buffer & close the port

SLEEP 1000

IF COMM(#hComm, RXQUE) THEN

COMM RECV #hComm, COMM(#hComm, RXQUE), dummy

END IF

COMM CLOSE #hComm

END FUNCTION

This short 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:

You can dial the bulletin board manually. When you're 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 should 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 on screen? 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 double lines of characters, click on the Disable Local Echo button. This simply prevents the code from adding your characters to the transmission log window.

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 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, it is interpreted as a command to get the name of a disk file, read that disk file, and send it to the bulletin board.

Let's add a new button to our dialog window to provide access to this feature - we will label this button Send File. In addition, we must also add a Callback Function to handle the event from this button. Lets start by adding the following equate definition to the block near the beginning of the file:

%IDC_SENDFILE = 104

Now we will insert the new Callback Function to the code. We'll add this immediately after the Send Callback() function ends:

CALLBACK FUNCTION SendFile_Callback() AS LONG

STATIC SendFileName AS STRING

LOCAL hReadFile AS LONG, FileLen AS LONG, Chunk AS LONG

LOCAL i AS LONG, Buff1 AS STRING

Buff1 = INPUTBOX$("Name of disk file to transmit?", $AppTitle, SendFileName)

IF ISFALSE LEN(Buff1) OR ISFALSE LEN(DIR$(Buff1)) THEN EXIT FUNCTION

SendFileName = Buff1

CALL AddLine(CB.HNDL, %IDC_LISTBOX1, "Wait... Sending " & SendFileName)

DIALOG DOEVENTS

' send the file

hReadFile = FREEFILE

OPEN SendFileName FOR BINARY AS #hReadFile ' Binary mode

FileLen = LOF(hReadFile) ' File length

Chunk = MAX&(32, COMM(#hComm, TXBUFFER) \ 2) ' 1/2*Buf

FOR ix = 1 TO FileLen \ Chunk

GET$ #hReadFile, Chunk, Buff1 ' Read a chunk

COMM SEND #hComm, Buff1 ' and send it

SLEEP 0

NEXT i

IF FileLen MOD Chunk <> 0 THEN ' More to send?

GET$ #hReadFile, FileLen MOD Chunk, Buff1

COMM SEND #hComm, Buff1

END IF

CLOSE #hReadFile

CALL AddLine(CB.HNDL, %IDC_LISTBOX1, "Transmission complete!")

END FUNCTION

Finally, we insert the code that adds a new control button on the dialog box. Add the following line to the group of CONTROL ADD statements in the PBMAIN function.

CONTROL ADD BUTTON, hDlg, %IDC_SENDFILE, "&Send File", 9, 182, 50, 14, %WS_GROUP OR _

%WS_TABSTOP CALL SendFile_Callback

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'll probably want to add some kind of error checking to the program for those reasons.

To receive a disk file, we will add yet another button to the dialog window titled Receive File. However, things are not quite as simple as the code we added to send a file: you must be able to use the program at the same time as the data is stored, as it comes in from the serial port. We also need a way to stop receiving a disk file.

First, we will add another equate to the beginning of the file, exactly as before:

%IDC_RECEIVEFILE = 105

Add the following line at the end of the GLOBAL variable declarations, just below the equates:

GLOBAL hWriteFile AS LONG

Next, add the Callback Function code, immediately after the SendFile_Callback() function that we just added.

CALLBACK FUNCTION ReceiveFile_Callback() AS LONG

STATIC ReceiveFileName AS STRING

LOCAL Buff2 AS STRING

' First check if file is already open

IF hWriteFile THEN

' Close the file

CLOSE #hWriteFile

CALL AddLine(CB.HNDL, %IDC_LISTBOX1, "Finished writing file!")

' Update the button label

CONTROL SET TEXT CB.HNDL, %IDC_RECEIVEFILE, "&Receive File"

RESET hWriteFile

EXIT FUNCTION

END IF

' Create a new file

Buff2 = INPUTBOX$("Output file name?", $AppTitle, ReceiveFileName)

IF ISFALSE LEN(Buff2) THEN EXIT FUNCTION

ReceiveFileName = Buff2

hWriteFile = FREEFILE

OPEN ReceiveFileName FOR APPEND AS #hWriteFile

IF ERRCLEAR THEN

' Error opening the file

RESET hWriteFile

ELSE

' Update the dialog

CALL AddLine(CB.HNDL, %IDC_LISTBOX1, "Receiving data stream to " & ReceiveFileName)

CONTROL SET TEXT CB.HNDL, %IDC_RECEIVEFILE, "Stop &Receive"

END IF

END FUNCTION

Now add the CONTROL ADD statement into PBMAIN in the same manner as before.

CONTROL ADD BUTTON, hDlg, %IDC_RECEIVEFILE, "&Receive File", 62, 182, 50, 14, %WS_GROUP OR _

%WS_TABSTOP CALL ReceiveFile_Callback

Finally, to ensure that the disk file is closed correctly, if the program is closed before the file is closed, insert the following lines just before the END FUNCTION within PBMAIN.

IF hWriteFile THEN CLOSE #hWriteFile

When we click on the new Receive File button, we enter the file name that will be used to save the data. At this point, the output file is opened. 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 to the ReceiveData() function, immediately after the line:

InBoundData = InBoundData & Stuf

The added line reads:

' If Receive mode is on, write raw data to the file

IF hWriteFile THEN PRINT #hWriteFile, Stuf;

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 both deceptive and difficult bugs 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? Simple... we set the global variable that holds the file number when the file was open. If this number is non-zero (logical TRUE), we can simply assume we need to close the file before finally exiting the program.

After the line that reads:

CALL EndComms

We add the following line to the file:

IF hWriteFile THEN CLOSE #hWriteFile

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 hWriteFile 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 poor approach. It is always better to write code that is fail-safe in as many conditions as possible.

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 test for such problems as the List box control filling up to the limits of the operating system (i.e., 32767 entries in Windows 95/98/ME), and even add a few more buttons to send certain preformatted strings to modem, for example "ATZ" or "ATDT555-1234".

Compared to DOS applications, this communications application may seem overly complex. This is because we simply cannot afford to use 100% of the CPU just to monitor a serial port. If we did, your multitasking operating system would suddenly take a huge drop in performance. If you examine the code a little more deeply, you will see it takes advantage of a very handy feature of 32-bit Windows: multi-threading.

This communications program consists of two threads in total: (1) the main thread handles the user commands and sending data to the modem; (2) the second thread simply monitors the serial port for receive data. If we used only one single thread in this application, the code would need to share its time between both data reception and transmission, but by using two, we ensure that the CPU is not heavily loaded unnecessarily.

Using a second thread in this way effectively splits the application into two (almost) independent sections. The only time these threads need to be aware of each other is when one is writing to the list box control. To handle this, we used a GLOBAL variable to signal when data was being displayed; temporarily "locking" the other thread until the task was complete.

For further experimentation, you could split the main thread down even further and create a separate thread just for writing data to the serial port. You could even try replacing the TEXTBOX control with a COMBOBOX so users can scroll back through the most recent "send" strings, providing a simple "history" feature.

See Also

Communications Basics

Communication Buffers

Parity and general error checking

Start and Stop bits

Opening a communications port

Reading and writing data