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