Extending Visual Basic's Comm Control

The communication control that comes with Visual Basic 4.0 works fine when streaming text, but doesn't support binary transfers. Michael extends this control by adding support for Xmodem protocol.


December 01, 1995
URL:http://www.drdobbs.com/windows/extending-visual-basics-comm-control/184409679

Figure 1


Copyright © 1995, Dr. Dobb's Journal

Figure 1


Copyright © 1995, Dr. Dobb's Journal

DEC95: Extending Visual Basic's Comm Control

Extending Visual Basic's Comm Control

Adding Xmodem support

Michael Floyd

Michael is executive editor for DDJ and author of Developing Visual Basic 4 Communications Applications (Coriolis Group, 1995). He can be contacted at the DDJ offices or [email protected].


Support for OLE custom controls (OCXs) is a new feature in Visual Basic 4.0 (VB4). One OCX, the Visual Basic communications (Comm) control, hides the low-level details of serial communications (fetching characters from the UART and the like) while providing a high-level interface based on the event-driven model. You simply write code in response to events. When a character comes through the serial port, you can grab it using an Input method. Thus, the Comm control works fine when streaming text from the serial port into a terminal window. However, the Visual Basic Comm control does not directly support protocols such as Xmodem for transferring binary files--a necessity for any real communications program.

There are several options for supporting binary transfers. You can, for instance, buy an enhanced, add-on Comm control from Crescent Software (the original developer of the bare-bones Visual Basic Comm control) that supports most popular file-transfer protocols--Xmodem, Ymodem, Zmodem, and the like. Alternatively, you can write your own Comm control, or extend the minimal control included with VB4. In this article, I'll describe how to extend the Comm control by adding support for the Xmodem protocol (sometimes called "Modem7"). Although this implementation only supports checksums for error detection, you can easily add a cyclic redundancy check (CRC).

Comm-Control Crash Course

The Comm control supports both event-driven and polling methods to send and receive data through the serial port. It generates a single event, OnComm, which can trap events such as receiving data through the serial port and errors such as a full transmit buffer. You can control how often the CommEvReceive event is generated by setting the RThreshold property. For example, setting RThreshold to 1 causes an CommEvReceive event to be generated each time a character is received in the input buffer. The value of an event or error is stored as an integer in the CommEvent property, so you can use CommEvent to determine the most recent event or error. For polling, you can disable the generation of the CommEvReceive event by setting the RThreshold property to 0.

You can begin "talking" to the serial port in as few as ten lines of code. Characters are sent using the Output function and received using the Input function; see Example 1. You start by setting the CommPort property to establish which serial port your modem is connected to. If you are on COM1:, set ComPort=1. You then establish the port settings with the Settings property. A typical setting might be Settings="9600,n,8,1". Before opening the communications port, you also need to tell the Comm control how many characters to fetch from the input buffer. The InputLen property sets or returns the number of characters to be read when using the Input procedure. Setting InputLen to 0 tells the communications control that the entire receive buffer should be read when using the Input procedure.

TinyComm

To demonstrate how to develop a serial-communications program, I've written a terminal program called "TinyComm." One of the reasons I chose this name is that the program's Xmodem code is based on that presented by Al Stevens in his "C Programming" column (DDJ, February and March 1989). TinyComm is a minimal implementation of a terminal program, but despite its simplicity, TinyComm is more robust than the sample VBTERM application supplied with Visual Basic. TinyComm allows you to select a remote system from a phone-book entry and dial that system. Once connected, you can log in and interact with the remote system. Here, I will focus on just a few of TinyComm's subroutines; the complete project is available electronically (see "Availability," on page 3).

TinyComm consists of three forms: a main window (TinyCom.frm), a phone-book-dialer window (Phonebk.frm), and an About box (AboutBox.frm). The main window consists of the terminal window and six pull-down menus. The terminal window presents some interesting challenges. A text-box control provides much of the functionality this window will require. However, some modifications must be made to support communications. For instance, characters typed into the text box must be sent to the serial port and echoed back on the screen. Additionally, when data comes through the serial port, you need to display it to the screen. You would also like to be able to resize the window, and have the data adjust itself properly.

CommCtrl.OnComm() (I've named this instance of the control CommCtrl) uses a Case statement to direct the program to the proper handler. When data is received through the serial port, the CommEvReceive case is triggered. CommEvReceive first grabs data from the communications control's receive buffer and assigns it to the TerminalTxt string variable. This gives us a chance to filter the data before displaying it on the screen. Next, the cursor position in the terminal window is determined by calculating the length of text in the Window and assigning that value to TerminalWindow.SelStart. Normally, the SelStart property determines the starting point of text that has been highlighted and selected in the text box. If no text has been selected, as in this case, SelStart indicates the position to begin inserting text.

To handle backspace characters, you test for the ASCII-character value 8H. If a backspace is detected, you subtract 1 from the current cursor position and assign that as the new SelStart position. On the screen, this moves the cursor back one space and erases the character that was in this position. Finally, the filtered text is displayed on the screen using SelText. Of course, there are plenty of other things you could filter and handle here, including special characters used in terminal emulation.

The other important form involves the phone book, an Access database (.MDB) file. This version of TinyComm provides no direct interface to the phone book, so you must add entries using the Data Manager. The PhoneBook form consists of a data grid and five button controls, three of which are currently disabled. The fourth button control closes the PhoneBook form, and the final button, Dial, dials the phone number of the system currently selected in the data grid.

The DialBtn_Click() subroutine retrieves the dialup information from the database and passes Comm control settings and the phone number to CallNum(). DialBtn_Click() first disables the Dial and Quit buttons and enables the Cancel button (which aborts the dialing process). Next, the database is opened and the record pointer is moved to the first record in the database. I use FindFirst to locate the NameStr in the database, so a SQL query is constructed and stored in the Query variable. The phone number associated with NameStr is placed in the Number variable and passed to CallNum for dialing. When control returns from CallNum, the buttons are reset to their prior state.

Xmodem Refresher

The Xmodem protocol is well documented and has been covered extensively in DDJ (most recently in "Intelligent XYModem," by Tim Kientzle, December 1994). Still, a quick refresher is in order. According to the Xmodem protocol, data are broken into 128-byte chunks and packaged into blocks (or packets) for transmission. Each data packet is prefixed with a special Start of Header (SOH) character, a block number, and the one's complement of the block number. This is followed by 128 bytes of data. Finally, a checksum character is appended to the data packet. Figure 1 shows how the message block is packaged.

In general terms, the transfer begins with the receiver sending out a series of Negative Acknowledgment (NAK) characters at ten-second intervals. When the sending program sees a NAK, it sends the first block of data. The receiving program examines the data block and checks it for problems. If there are none, the receiver sends an Acknowledgment (ACK) character indicating the block is fine. If, on the other hand, there is a problem with the transmission, a NAK is sent. The sender responds to a NAK by resending the bad block, and to an ACK, by sending the next block. The process continues until an End of Transmission (EOT) character is received or the file transfer is aborted.

When the first packet is received, the receiver examines the first byte for the SOH character (01H). If the header character is found, the receiver assumes that the message block is valid and begins the file transfer in earnest. The receiving program next takes the block number and calculates the one's complement to the block number. This value is compared to the one's complement sent by the sending program. If the two values match, everything is fine and the receiver extracts the 128 bytes of data. The one's complement is computed in VB by performing a bitwise Not(). If the two values do not match, there is an error in the transmission and the packet must be resent. The receiving program notifies the sender by sending a NAK (&H15).

The next step in the process is for the receiver to calculate a checksum of the 128 bytes and compare this value to the checksum that was sent by the remote system. Assuming the two checksum values match and the one's complement values match, the receiver sends an ACK character (&H6). The checksum is calculated by summing each of the data bytes. Figure 2 shows the algorithm for downloading a file using Xmodem.

Implementing Xmodem

Listing One shows the download_xmodem() routine, which is based on C code written by Al Stevens. Thus, if you're a C programmer, you should be able to follow the Visual Basic code without difficulty. However, while variables and subroutines may follow the general structure of Al's code, there are some significant differences.

The download_xmodem() routine takes a file handle as an argument. Thus, the calling routine must create a valid file handle and open the file prior to calling download_xmodem(). The calling routine is also responsible for closing the file after the file transfer is complete. The code in Figure 3 can be used to call the download_xmodem() subroutine.

My version of download_xmodem() polls the serial port for input rather than using event-driven methods. This is accomplished by first setting the InputLen property to 1, which tells the control to receive characters through the comm port, one at a time. Next, the Comm control's RThreshold property is set to 0, thus disabling the generation of the OnComm event. In TinyComm, disabling CommEvReceive has the side effect of disabling output to the terminal window. Unfortunately, this also disables all other events and error messages processed by this event. I've temporarily disabled the CommEvReceive event to simplify the discussion and to focus on Xmodem rather than event processing. Note, however, that the global TerminalMode variable has been set to False at the beginning of Listing One. Code in OnComm's receive event handler checks TerminalMode and disables output to the window when the variable is set to False.

One difference between the C and Visual Basic versions of download_xmodem() shows up in the ReadComm() subroutine. To be useful to download_xmodem(), each string character retrieved from the Comm control's Input method must be converted to an integer value representing its ASCII equivalent. The ReadComm() subroutine shown in Listing One grabs a string character from the serial port, converts it to an integer, and returns the result. Visual Basic's Asc() function performs the conversion. If a null string is encountered, ReadComm() returns 0.

Another difference involves the Delay() function (which Al calls the sleep() function). Delay() is used to pause the system for a predetermined period of time. For example, to initiate the file transfer, download_xmodem() sends a NAK and checks the input buffer to see if an ACK response character has been sent. If not, the subroutine waits approximately six seconds (I've shortened the delay time), then sends out another NAK. Visual Basic provides a Timer control that can be used for just this purpose. The Timer control uses the PC's system clock to generate an event after a set period of time (specified in milliseconds). However, the accuracy of the Timer control is limited by the system clock, which generates a clock tick every 1/18th of a second. When the Timer event is generated, I increment a global variable called SecondsElapsed. The Delay() subroutine loops until the desired number of seconds have elapsed.

Delay() also periodically issues a DoEvents(), which hands control over to Windows to process other events within the system. Without DoEvents(), the system appears to be hung. I've found Delay() useful in many situations, and have even included it as part of a scripting language for TinyComm that I call "TinyScript.''

Conclusion

Clearly, I've only touched on TinyComm's highlights. You can add many features, including more event and error handlers. Most terminal programs support file capture, as well as ASCII file send and receive capability. I have also shown only the basics of Xmodem support. There is, of course, a complimentary upload_xmodem() subroutine. In addition, it is rather easy to add CRC support to the Xmodem subroutines, and you will undoubtedly want to take full advantage of the visual controls supplied by Visual Basic.

Example 1: Opening the serial port and initializing the modem.

CommCtrl.CommPort = 1
CommCtrl.Settings = "9600,n,8,1"
CommCtrl.InputLen = 0
CommCtrl.PortOpen = True
CommCtrl.Output = "ATZ" + Chr(13) + chr(10)

Do
    DummyVar = DoEvents()
Loop Until CommCtrl.InBufferCount >= 2
InString$ = CommCtrl.Input
CommCtrl.PortOpen = False
Figure 1: Xmodem data packet.

Figure 2: Xmodem's download algorithm.

Send NAKs every 10 seconds until a packet is received
If packet received then check for SOH
If SOH then
    get block number
    calculate One's complement to block number
    compare complement to the complement sent in the packet
    If local complement <> remote complement then
        Send NAK and repeat process
    Else
        get 128 bytes of data
        calculate checksum
        compare local checksum to packet checksum
        If local checksum <> packet checksum then
            Send NAK and repeat process
        Else
            write data to file
            send ACK
        End If
    End If
Read next SOH
If SOH = EOT then transmission successful
If SOH = CAN then transmission aborted
If SOH = &H01 then get next packet (repeat)
Figure 3: Calling download_xmodem(). FileHandle = FreeFile 'Get the next free file handle FileName = "SomeFile" Open FileName For Output As FileHandle download_xmodem (FileHandle) Close

Listing One

' xmodem.bas  -- Michael Floyd -- Dr. Dobb's Journal, December 1995.)
Global Const RETRIES = 12
Global Const CRCTRIES = 2
Global Const PADCHAR = &H1A
Global Const SOH = &H1
Global Const EOT = &H4
Global Const ACK = &H6
Global Const NAK = &H15
Global Const CAN = &H18
Global Const CRC = "C"
Global tries, SecondsElapsed As Integer           
Global InBuffer As String
Sub Delay(Seconds)   
    SecondsElapsed = 0
    If Seconds < 1 Then
        Terminal.Timer1.Interval = 1000 * Seconds
    Else
        Terminal.Timer1.Interval = 1000
    End If
    Terminal.Timer1.Enabled = True       'Enable timer
    Do While SecondsElapsed <= Seconds
        If I Mod 10 = 0 Then DoEvents
        Terminal.Label1.Caption = SecondsElapsed
        I = I + 1
    Loop
    Terminal.Timer1.Enabled = False
End Sub
Sub download_xmodem(FileNum)
Dim buffer, Checksum, Block, RemoteChecksum, RemoteComplement, _____LINEEND____
    RemoteBlockNumber, SOHChar As Integer
Dim ByteArray$(1 To 128)
    TerminalMode = False                'Disable output to terminal
    Block = 0
    SOHChar = 0
    fst = True
    Terminal.CommCtrl.InBufferCount = 0  'Flush the Input buffer
    Terminal.CommCtrl.InputLen = 1       'Receive one char at a time
    Terminal.CommCtrl.RThreshold = 0     'Disable generation  of OnComm Event
    tries = 0
    TIMEOUT = 6
    test_wordlen
    ' send NAKs until the sender starts sending
    Do While (SOHChar <> SOH) And (tries < RETRIES)
        tries = tries + 1
        Terminal.CommCtrl.Output = Chr$(NAK)
        Delay 1
        SOHChar = ReadComm()
        If SOHChar <> SOH Then
            Delay 6
        End If
    Loop
    Do While tries < RETRIES
        ' -- Receive the data and build the file --
        Terminal.Label1.Caption = "Block  " + Str(Block + 1)
        If Not (fst) Then
            TIMEOUT = 10
            SOHChar = ReadComm()
            If TimedOut() Then
                MsgBox "Timed Out"
            End If
            If SOHChar = CAN Then
                MsgBox "CAN Received"
                Exit Do
            End If
            If SOHChar = EOT Then
                Terminal.CommCtrl.Output = Chr$(ACK)
                MsgBox "EOT Received"
                Exit Do
            End If
            If SOHChar <> SOH Then
                If SOHChar = EOT Then
                    Terminal.CommCtrl.Output = Chr$(ACK)
                    MsgBox "EOT Received"
                    Exit Do
                End If
                Do While (SOHChar <> SOH)
                    If tries >= RETRIES Then
                        MsgBox "SOH errors!"
                        Exit Do
                    End If
                    tries = tries + 1
                    Terminal.CommCtrl.InBufferCount = 0  'Flush Input buffer
                    Terminal.CommCtrl.Output = Chr$(NAK)
                    Delay 1
                    SOHChar = ReadComm()
                Loop
            End If
        End If
        fst = False
        TIMEOUT = 1                         ' Switch to one sec. timeouts
        
        RemoteBlockNumber = ReadComm()      ' Read block number
        RemoteComplement = ReadComm()       ' Read 1's complement
        Checksum = 0
        DLInfo.Label1.Caption = "Block: " + Str(RemoteBlockNumber) + _____LINEEND____
        " SOHChar: " + Str(SOHChar)
        ' ---- data block -----
        For I = 1 To 128
            buffer = ReadComm()
            Buf$ = Buf$ + Chr$(buffer)
            Checksum = Checksum + buffer
        Next
        Checksum = Checksum And 255
        ' ---- checksum  from sender ----
        RemoteChecksum = ReadComm()
        ' --- Handle resent blocks ---
        If RemoteBlockNumber = Block Then
            FilePos = Seek(FileNum)
            Seek FileNum, FilePos - 128
        ' --- handle out of synch block numbers ---
        ElseIf RemoteBlockNumber <> (Block + 1) Then
            receive_error "No next sequential block", CAN
            Exit Do
        End If
        Block = RemoteBlockNumber
        ' --- test the block # 1's complement ---
        BlocksComplement = (Not RemoteBlockNumber And &HFF)
        If (RemoteComplement And &HFF) <> BlocksComplement Then
            receive_error "One's complement does not match", NAK
        End If
        ' --- test chksum or crc vs one sent ---
        If Checksum <> RemoteChecksum Then
            receive_error "non-matching Checksums", NAK
        End If
        ' --- write the block to disk ---
        For I = 1 to Len(Buf$)
            Print #FileNum, Mid(Buf$, I, 1)
        Next I
        Terminal.CommCtrl.Output = Chr$(ACK)
        Delay 0.5
    Loop
    If SOHChar = EOT Then
        MsgBox "Transfer Complete"
    Else
        MsgBox "Transfer Aborted"
    End If
    TIMEOUT = 10
    Terminal.CommCtrl.InBufferCount = 0  'Flush the buffer
    Terminal.CommCtrl.InputLen = 0       'Receive all chars in buffer
    Terminal.CommCtrl.RThreshold = 1     'Enable generation of OnComm Event
    TerminalMode = True                  'Enable output to terminal
End Sub
 Function ReadComm() As Integer
 Dim Tmp As String
 ' ReadComm reads a character from the Comm control's input buffer
 ' and returns the ASCII value of that character. If a null string is
 ' encountered, ReadComm returns 0.
 
    If Terminal.CommCtrl.InBufferCount > 0 Then
        Tmp = Terminal.CommCtrl.Input
        If Tmp <> "" Then
            ReadComm = Asc(Tmp)
        Else
            ReadComm = 0
        End If
    Else
        ReadComm = 0
    End If
End Function
Static Sub receive_error(ErrorMsg, Rtn)
    tries = tries + 1
    If TIMEOUT = 1 Then
        MsgBox "error  " + ErrorMsg
    End If
End Sub
Sub test_wordlen()
    Settings = Terminal.CommCtrl.Settings
    If InStr(Settings, ",8,") = 0 Then
        MsgBox "Must be 8 Data Bits"
        tries = RETRIES
    End If
End Sub
Function TimedOut() As Integer
    Ticker = 1
    If Ticker = 0 Then
        TimedOut = True
    Else
        TimedOut = False
    End If
End Function


Copyright © 1995, Dr. Dobb's Journal

Terms of Service | Privacy Statement | Copyright © 2024 UBM Tech, All rights reserved.