Home
Archives
About us...
Advertising
Contacts
Site Map
 

ruby in steel

 

VISUAL BASIC COMMUNICATIONS #3

In the final part of this series we look at communication event monitoring in Visual Basic 6
by Dermot Hogan

Requirements:
Visual Basic 6 (service pack 5)

 

Download The Source Code:
vb3src.zip

 

See also Part One and Part Two

One of the problems you face if you abandon using Microsoft’s own MsComm communications control is that you also lose an easy way to signal events (such as the fact that DSR may have changed) to your program.

Note: For an explanation of the basics of communications, such as DSR, refer to part one of this series

In essence it is quite easy to detect these situations – you can just ‘poll’ the communications device through the API, looking for a change in the signal. There are two basic ways of doing this. Either the program can sit in a tight loop looking at the state of the communication port every time it passes round or you can use a timer. The first way is rather crude and, if you have other things to do, it is a non-starter; the second is better, but still not ideal. Even with a fast PC firing the timer every tenth of a second, say, you’ll still have a ‘latency’ of up to 100 milliseconds before you notice anything. While this isn’t a huge amount of time in human terms, it’s of geological proportions to a 3 GHz microprocessor.

But in fact, 100 milliseconds isn’t too bad for most serial communications programs – you can get away with that timing latitude, usually with relatively few problems. However, it might seem to you (as it does to me) to be a bit inelegant. What you really want is to fire a Visual Basic event whenever DSR changes. That turns out to be quite difficult with Visual Basic 6. The problem lies with ‘threads’. To get a truly ‘asynchronous’ event fired – that is an event which isn’t dependent in any way on the main program - you need to create a separate program ‘thread’ or execution unit.

In Windows XP (as in Linux), a program isn’t scheduled to run by the operating system. Instead, a smaller execution unit – a thread – is run instead. Each program must have at least one thread, but often programs have several threads running or idle at the same time. It turns out that a program (strictly, a process) is really just a handy container for one or more threads. A good definition of a program is an address space that allows one or more threads to run; a thread, on the other hand, is ‘a unit of execution’ as seen by the operating system’s scheduler, which knows very little about the processes themselves.

The problem: COM, Threads and Visual Basic

Part of the trouble with threads and Visual Basic 6 lies in the Component Object Model (COM). A COM component is designed to ‘plug-in’ to a program with the minimal amount of effort of the programmer’s part. The problem comes in determining which thread the component is to run in. Normally, the component just runs on the main thread of the program (this is how Visual Basic 6 mostly handles components). Most components assume that this is the case and the COM plumbing takes care of the rest. However, this simplistic component approach leads to problems, especially in high performance servers. Having one thread in an application server is a good way of introducing a serious bottleneck: it’s a bit like reducing a six lane freeway down to one lane.

COM introduces the idea of ‘threading models’ to simplify matters. The simple, basic model is ‘single threading’. All code runs on one thread – and there is only one thread. This is how a typical “hello world” style application runs. The next step up is called ‘apartment’ threading. The idea is that all components live in the same thread ‘apartment’ – that is they all use the same thread. However, there can be several ‘apartments’. The term ‘apartment’ comes from the fact that components and code that live in one apartment shouldn’t know about the code and objects in another: the apartment ‘walls’ shield them from one another. The third level is called ‘free threading’ – a better term would be ‘free-for-all’ threading. There are no rules and it’s up to you to get your code to work. I recommend a good course in device driver design to understand how asynchronous processes work before using free threading in Visual Basic 6 – it’s not easy!

Now the problem with threads comes from things on one thread interfering with things on another – synchronisation in other words. Because all the code lives in the same process (program), there’s nothing to stop one thread doing horrible things to another thread. So apartment threading is a set of rules designed to ensure that multiple threads can run in the same process but not interfere. Most people think of COM as specifying a set of interface rules. Not so: it also specifies synchronisation mechanisms. A COM object has to obey both sets of rules or disaster will ensue.

Visual Basic 6 implements single threading and apartment threading. However, Visual Basic imposes a further restriction in the use of apartment threading. No communication between apartments is allowed via global data. Now this is a pretty severe restriction. In making things easy for Visual Basic, things have been made very difficult for those who wish to use threads. This restriction essentially means that apartments inside a Visual Basic program work almost as separate programs.

The Visual Basic rules may state that two apartments can’t communicate with each other using global data – they each have separate copies of the global data – but that doesn’t mean that you can’t be a little sneaky and get round the ‘apartment rule’ other ways. But really, it’s better to forget about threads in Visual Basic 6 and move to Visual Basic .NET for this aspect of communications programming.

However, we can still do reasonable event detection in Visual Basic 6 using the a timer. So this month, I’ll concentrate on the communications APIs needed to determine if a significant communication event has occurred and I’ll use a basic timer mechanism to synchronise these with the rest of the program.

Detecting DSR and CTS changes

Normally – in a language that allows threading and ‘overlapped’ (i.e. asynchronous) input/output operations – you might use the API functions SetCommMask and WaitCommEvent to determine if a communications line has done something interesting. However, as I’ve described above, we’re a bit restricted in this respect in Visual Basic 6, so we’ll have to do something a little different. Here, I’ll use a timer and the GetCommModemStatus to poke around in the serial ports.


Now you can check on the state of the DTS and CTS lines by pressing the Set and Clear buttons on the form

Load the project, comms.vbp. In the initialization code, I’ll set the initial port states using GetCommModemStatus and start off a timer. Here, I’ve chosen 100 milliseconds as the timer interval:

' initialise the communication states

' and kick off a timer r = GetCommModemStatus(hCom(0), comState(0)) r = GetCommModemStatus(hCom(1), comState(1)) Timer1.Interval = 100

The timer code that runs when the timer fires looks like this :

Private Sub Timer1_Timer()
Dim cs(1) As Long, r As Long r = GetCommModemStatus(hCom(0), cs(0)) r = GetCommModemStatus(hCom(1), cs(1)) If (cs(0) And MS_CTS_ON) Xor (comState(0) And MS_CTS_ON) Then EventLog.Text = EventLog.Text & "CTS changed on port 0" & vbCrLf End If If (cs(0) And MS_DSR_ON) Xor (comState(0) And MS_DSR_ON) Then EventLog.Text = EventLog.Text & "DSR changed on port 0" & vbCrLf End If If (cs(1) And MS_CTS_ON) Xor (comState(1) And MS_CTS_ON) Then EventLog.Text = EventLog.Text & "CTS changed on port 1" & vbCrLf End If If (cs(1) And MS_DSR_ON) Xor (comState(1) And MS_DSR_ON) Then EventLog.Text = EventLog.Text & "DSR changed on port 1" & vbCrLf End If comState(0) = cs(0) comState(1) = cs(1) End Sub

The main work of the timer is to get the modem status bits via GetCommModemStatus to determine if anything has changed since last time we looked. It does this using the exclusive or Xor operator. Now, I have to say, I’ve never seen the Xor operator ever used in any Visual Basic code I’ve come across (or indeed written till now). It’s a useful trick to keep up your sleeve, though. Simply put, the Xor operator detects if a bit or logical expression has changed – not if they are equal, but rather if the two logical expressions or bits are UNEQUAL. This is handy in bit twiddling operations and communications processing where you want to detect if a state or variable has changed – usually in a quick and easy manner.

You can express the outcome of an exclusive or operation like this:

  • if a is true and b is true then (a XOR b) is false
  • if a is false and b is false then (a XOR b) is false
  • if a is false and b is true then (a XOR b) is true
  • if a is true and b is false then (a XOR b) is true

Incidentally, you can set a variable to zero by xor-ing it with itself:

(a XOR a) = 0

This may seem a little perverse, but in fact it’s the standard way for a microprocessor (Intel’s anyway) to set a memory location to zero – apparently it’s the fastest way to do it.

The result of the timer is to examine the modem states every 100ms and to display a message if either CTS or DSR has changed state.

API Error Messages

One of the problems with calling APIs from Visual Basic 6 is finding out what went wrong (a not infrequent state of affairs, it has to be said). Like all functions, most APIs return an error code of some sort. However, it is often not in the return value of the API itself, but kept somewhere else. For example, the CreateFile API returns a handle to the device if it succeeds, and a sort of general error if it can’t. To determine the last error specifically – such as ‘file not found’, say, you have to call the GetLastError API. However, Visual Basic 6 gets in the way and if you try to do this, you’ll probably find that GetLastError returns zero!

The reason is that Visual Basic 6 can also call APIs behind the scenes in between your Visual Basic statements – and these usually succeed. To get round this, Visual Basic 6 provides a slot in the standard error object, Err, to store the last error returned from an external DLL (which is how Visual Basic 6 views APIs – they are just routines in an external library). The API error code is then returned in lastDllError and you can look at it just like any other variable.


API error handling isn’t straightforward – you have to use the Err object to determine the API error. You can try out the example API error code by pressing the Initialize button twice

The next problem is finding out what the code means. You do this with the FormatMessage API. Like many APIs, this has a pile of parameters and flags, most of which are not useful when the function is called in a simple manner from Visual Basic. The function is used like this (for example):

 SetCommMask(hCom(1), EV_CTS Or EV_DSR Or EV_ERR)

If Err.LastDllError <> 0 Then lastError = Err.LastDllError msgbuf = Space(256) i = FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM, 0, lastError, 0, _ msgbuf, 256, 0) MsgBox "Error in API: " & Left(msgbuf,i) End If

Here, msgbuf is a string and I set it to have 256 spaces (a good round binary number). The string is passed to the FormatMessage API in the 5th parameter with the length set to 256 in the 6th parameter. The error code is passed in the 3rd parameter. The API is told to search for a system error message corresponding to that number by the first parameter. The API returns the number of characters in the resulting error message and the spaces in the message are finally trimmed out using that information combined with Left.

 

Next month: I'll be looking at using communications APIs in VB .NET

 

July 2005

 


Home | Archives | Contacts

Copyright © 2006 Dark Neon Ltd. :: not to be reproduced without permission