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.
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 |