Here I will present a complete solution in Perl, from requirements to implementation - a sophisticated serial port monitor with ability to monitor full-duplex communications with time stamping.
Believe it or not, I found no such program in the Internet (I'll be delighted if someone has...). There are several COM monitors around the web, but almost none use time stamping, and almost none monitor more than one port simultaneously. Therefore, there was a need to implement such a program. The language of choice (as is always the case for me :-) - Perl.
What makes the task difficult is the time-stamping requirement. For it to happen, characters should be read from the port one-at-a-time, which imposes severe timing restrictions and we'll soon see.
Even at the measly 2400 baud, in a continuous data stream a new character arrives each 4 ms. Even using Time::HiRes I couldn't achieve a reliable sleep time of less than 30 ms, and that's in idial conditions (lets remember that Windows is not even close to being a RTOS).
So, a background thread is necessary. The Tk GUI does its job (that is, sleeps most of the time), sending messages to a background monitoring thread when needed. The background thread is live all the time and not only on "wakeup" intervals, so it will be possible to achieve the harsh timing constraints.
Fortunately, there is a solution. What we should do is create the background thread *before* the Tk GUI goes live. This way, its creation won't have to copy all the Tk data in, and Tk won't die when the thread exits - they are as independent as possible, the only thing connecting between them is a message queue. When the application exits, it kills this monitoring thread.
# 'manager' loggin thread: communication queues initialization # and thread creation # my ($mgr_id, $mgr_req_q, $mgr_ack_q); $mgr_req_q = new Thread::Queue; $mgr_ack_q = new Thread::Queue; $mgr_id = threads->create("SerialLogger::manager"); # Main window # my $w_top = MainWindow->new(-title => "COM Monitor"); ... ... # tk stuff MainLoop; # Clean up and exit (after MainLoop returned) $mgr_req_q->enqueue("die"); $mgr_id->join(); exit(0);
Note the communication queues. I couldn't think of a better way than two, one for each direction.
I first tried to non-blocking alternative, in an attempt to do all the monitoring in that single background thread. The idea: loop over all the open ports and ask them (non blockingly) if they have data for us, if they do, log it.
This approach worked - but had a serious flaw. The logging thread would take up 100% CPU and seriously clogged the PC - naturally, after all it ran an endless loop. The solution: wait/sleep/whatever between iterations. However, as I already said in the case of Tk's "after", we can't reliably wait for short periods (at least 30ms), and that caused missed characters and stuck communication.
So, the only solution left is blocking read(). When SerialPort::read(1) is called, it blocks until the monitored port receives a character, and then returns that character. This block is what I call "sleepy" - it keeps the CPU free (most likely that it's implemented with an interrupt deep down in the Windows API). Hurray ! We can monitor ports and keep the CPU free.
Not so quick... It would be fine if we only had one port to monitor. But when it's two or more, this approach won't work. While we block on one port, the other may receive data and we'll lose it after its buffer overflows (which happens quickly, especially in continuous data streams). Looks like a more complicated solution is due...
As we saw, blocking read() is required to make the program's performance reasonable (installing a CPU clogging utility is very rude...). But a logging thread can't block on more than one port at the same time. SO... what we need is one (worker) thread per port, and a single (manager) thread to manage these workers.
The manager thread will keep a queue (actually two queues, for bidirectional communications) for each worker. It will send the worker "init", "run" and "stop" commands, and the worker will act according to those. To demonstrate, here is the complete "worker" thread function that is called (with threads->create) by the manager for each port it's told to monitor:
As you can see, each worker has a state - it's either idle or running (monitoring). When idle, it just waits for 10 ms using 'select' (this will probably result in a 30 ms wait). We don't care waiting here, because it starts running only after the manager is being asked so by a GUI events. The non-blocking dequeue_nb() call checks if it has new messages from the manager on each iteration. When a command to open a port is received, the worker dutifully opens a Win32::SerialPort. When a command to run is received, the worker is in "running" state, where it no longer sleeps on each iteration, but reads data from the port with a blocking read, and logs it to the dump file when received.# worker thread function # sub serial_logger { my ($num, $fh) = @_; my $state = "idle"; my $mycom; print "worker $num born\n" if $THR_DEBUG; while (1) { if (my $msg = $worker_req_q[$num]->dequeue_nb()) { print "worker $num got '$msg'\n" if $THR_DEBUG; if ($msg eq "die") { $worker_ack_q[$num]->enqueue("OK"); return; } if ($state eq "idle") { my $ret; if ($msg =~ /^open\s(.*)$/) { ($ret, $mycom) = @{open_com($1)}; $worker_ack_q[$num]->enqueue($ret); } elsif ($msg eq "run") { $state = "running"; } } elsif ($state eq "running") { if ($msg eq "stop") { clean(); $state = "idle"; } } } if ($state eq "running" and $mycom->{is_open} == 1) { eval { my ($rb, $byte) = $mycom->{port}->read(1); if ($rb > 0) { my $s = gettimeofday(); my $prt = join(",", $mycom->{name}, ord($byte), "$ +s"); print $fh "$prt\n"; } }; if ($@) { $state = "idle"; } } else { select(undef, undef, undef, 0.01); } } }
To see the manager's side of the communication, here's the manager's response to the "open" and "run" messages from the GUI:
When an "open" command arrives, the manager creates a worker thread to handle the port. When the "run" command is received, the manager sends a "run" command to all the workers. What about CPU clogging ? There's none. The manager thread sleeps on each iteration - it doesn't mind, it only responds to GUI requests. The workers sleep when not running, and block on read() when running, so no "naked loops" and the program hardly bothers the CPU.elsif ($msg =~ /^open\s(.*)$/) { my $thrid = threads->create("serial_logger", $n_workers, $mgr_fh); push(@worker_id, $thrid); $worker_req_q[$n_workers]->enqueue($msg); my $ret = $worker_ack_q[$n_workers]->dequeue; $mgr_ack_q->enqueue($ret); ++$n_workers; } elsif ($msg eq "run") { $mgr_state = "running"; broadcast_to_workers("run"); }
Some problems still remain:
All in all, however, this task proves that Perl is good for just about anything. This is a serious industrial-level application, very useful for debugging serial protocols.
I'll be happy to hear insights on the problems I ran into. Tk/threads/SerialPort is a rare combination, and as you saw many interesting issues arise.
The full code can be downloaded from here
|
---|
Replies are listed 'Best First'. | |
---|---|
Freeware port monitor
by Anonymous Monk on Dec 22, 2004 at 17:56 UTC | |
by spurperl (Priest) on Dec 23, 2004 at 08:36 UTC | |
by perlwanna (Initiate) on Nov 15, 2007 at 22:14 UTC | |
Re: Marrying Tk, threads and SerialPort - a COM port monitor
by Bruce32903 (Scribe) on Mar 05, 2008 at 23:05 UTC | |
by spurperl (Priest) on Mar 08, 2008 at 09:44 UTC |