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-2005 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 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 CBMSG

    CASE %WM_INITDIALOG

      ' Set focus to the edit control

      CONTROL SET FOCUS CBHNDL, %IDC_EDIT1

 

      ' Set SELECTION range to highlight the initial entry

      CONTROL SEND CBHNDL, %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 CBHNDL, %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 CBHNDL, %IDC_ECHO TO lResult

    IF ISTRUE lResult THEN SkipEcho

  END IF

 

  ' Add the echo to the listbox

  CALL AddLine(CBHNDL, %IDC_LISTBOX1, "<== " + SendText)

 

SkipEcho:

  ' Set the SELECTION range for the edit control so the

  ' next keypress "clears" the existing text

  CONTROL SEND CBHNDL, %IDC_EDIT1, %EM_SETSEL, 0, -1

 

  ' restore the keyboard focus to the edit control

  CONTROL SET FOCUS CBHNDL, %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 CBHNDL, 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

  CONTROL SEND hWnd, nID, %LB_GETCOUNT, 0, 0 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

 

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(CBHNDL, %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(CBHNDL, %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(CBHNDL, %IDC_LISTBOX1, "Finished writing file!")

 

    ' Update the button label

    CONTROL SET TEXT CBHNDL, %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(CBHNDL, %IDC_LISTBOX1, "Receiving data stream to " & ReceiveFileName)

    CONTROL SET TEXT CBHNDL, %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