Bruce32903 has asked for the wisdom of the Perl Monks concerning the following question:

I am trying to write a Perl program to collect data from a serial port, parse it and post the fragments to various text boxes in a Perl/Tk window. I did quite a bit of searching and thought I had a good starting point with the PerlMonks node 416132. But, there were two problems. First, the code link for node 416132 is broken. Second, the code in the text of 416132 seems to be a little advanced for this senior-newbee / junior-intermediate programmer. Therefore, I started off on my own.

I have many small test files, a big and bloated file with no clear direction to it and since I don't yet have a clear direction I don't have a single clean file to post for comments. Thus, I have pulled and rewritten fragments to show my approach.

Pulling from my big file of test code I have the following to represent my approach with Tk:
use Tk; my $mw = MainWindow -> new; $mw -> title("Test Code With Serial Port and GUI"); my $btn_01=$mw->Button(-text =>"Done", -command=>sub{exit}) ->pack; my $btn_02=$mw->Button(-text=>"Serial Port", -command=>sub{$thr=threads->new(\&serial_port)} +) ->pack; MainLoop;
In the SAME FILE the following represents a streamlined version of my thread code (note $Rval is shared):
use threads; use threads::shared; my $thr; # thread my $Rval:shared;# return value inside thread my $RTval; # return value intended to be "out in Tk code" # the following 3 lines of code would be invoked by a button press $thr=threads->new(\&sub_name); $TRval = $thr->join; print "$TRval"; # $TRval is in MainWindow Tk code sub sub_name { $Rval = `a_command_line_function`; # final code will have serial po +rt activity }
I was quite suprised at how long it took to find sample serial port code that looked as easy as I thought it should be. Finally I found some and the snipped below shows my approach (note use of shared $Rval):
use Device::SerialPort; my $port=Device::SerialPort->new("/dev/ttyS0"); $port->baudrate(9600); $port->parity("none"); $port->handshake("none"); $port->databits(8); $port->stopbits(1); $port->read_char_time(0); $port->read_const_time(1); my $readChars=0; # init my $readBytes=""; # init while(1) # yes, I know this thread is blocking { ($readChars, $readBytes)=$port->read(1); if ($readChars ne 0) { print("$readBytes"); # $Rval = $readBytes when attempting to sha +re from thread to main window $readBytes=""; # re-init } }
Once I got all of these pieces working alone (I did) I thought that I could put them together and have a working model. Expanding the model for my "real world" data stream and my "real world" display requirements and I would be in good shape.

I'm guessing that I am probably facing two (or more) problems that I did not expect. First, I seem to need some sort of a timer or change detector to cause the Tk main loop to do a callback to code that will post the passed data to the window (about once a second). Second, the always-running serial port code is blocking the serial port thread (as expected) but it also seems to be (I'm not sure) blocking the MainLoop (not expected). In addition, the data might not be getting passed (shared) and there could be more problems that I don't yet know about.

I have browsed through several books and dozens of internet postings. I have seen (at a top level) "Anatomy of the MainLoop", shared thread data, forks, callbacks, bindings, events, pipes, sockets, semaphores, blocking, nonblocking, before and after. My head is starting to spin. There is probably a good chance that one or more of these topics may solve my problem(s). But, I have spent two days on this already and I need to try go get code working this weekend. There is no way for me to dive into a study of all of these topics and still hit my goals. There probably are clean and "easy looking" ways to make a serial port thread "play nice" with a Tk main window. Any suggestions you have would be greatly appreciated.

Thank you,
Bruce

P.S. At this point I would like to learn how to finish my code. But, if it is available somewhere I would love to also have a copy of the code from PerlMonks node 416132 "Marrying Tk, Threads and SerialPort, a COM Port Monitor".

Replies are listed 'Best First'.
Re: Posting thread data in a Perl/Tk window
by zentara (Cardinal) on Mar 08, 2008 at 19:22 UTC
    # the following 3 lines of code would be invoked by a button press $thr=threads->new(\&sub_name);

    I see a glaring mistake here. In Tk threaded apps, you MUST create the threads BEFORE any Tk code is invoked, or you will get thread-safety problems. Since you are invoking the thread from a Button, you already are risking thread safety. Just see threads: spawn early to avoid the crush. or search here for "Tk threads" and see how all the examples create the threads right before any Tk is called. So what you will need to do, is start your SerialPort stuff in the thread first, possible sitting in a while loop, waiting for a signal to start. THEN have a Tk Button that sets a shared variable to signal the while loop to start execution. I have an example at Tk-with-worker-threads.

    Also, remember, that you can share the serial port filehandle through a thread by it's fileno.

    #!/usr/bin/perl use warnings; use strict; use threads; use threads::shared; use Tk; my %shash; #share(%shash); #will work only for first level keys my %hash; share ($shash{'go'}); share ($shash{'fileno'}); share ($shash{'pid'}); share ($shash{'die'}); $shash{'go'} = 0; $shash{'fileno'} = -1; $shash{'pid'} = -1; $shash{'die'} = 0; $hash{'thread'} = threads->new(\&work); my $mw = MainWindow->new(-background => 'gray50'); my $text = $mw->Scrolled('Text')->pack(); my $repeater; my $buf; my $startb = $mw->Button( -text => 'Start', -command=>sub{ $shash{'go'} = 1; $mw->after(1000); my $fileno = $shash{'fileno'}; print "fileno_m $fileno\n"; open (my $fh, "<&=$fileno") or warn "$!\n"; while(<$fh>){ $text->insert('end',$_); $text->see('end'); $mw->update; } # $repeater = $mw->repeat(10, # sub { # my $bytes = sysread( $fh, $buf, 8192); # $text->insert('end',$buf); # $text->see('end'); # if( $shash{'go'} == 0 ){ $repeater->cancel } # } # ); } )->pack(); my $stopb = $mw->Button( -text => 'Stop', -command=>sub{ exit; }, )->pack(); MainLoop; ################################################################## sub work{ $|++; while(1){ if($shash{'die'} == 1){ goto END }; if ( $shash{'go'} == 1 ){ my $pid = open(FH, "top -b |" ) or warn "$!\n"; my $fileno = fileno(FH); print "fileno_t->$fileno\n"; $shash{'fileno'} = $fileno; $shash{'go'} = 0; #turn off self before returning }else { sleep 1 } } END: } ###################################
    Also, you can setup your serial port and read it from a Tk timer, but threads will give you more Start/Stop control. Here is an old newsgroup post.

    I'm not really a human, but I play one on earth. Cogito ergo sum a bum
Re: Posting thread data in a Perl/Tk window
by ww (Archbishop) on Mar 08, 2008 at 16:26 UTC
      I see three blocks of sample code that don't seem to be 100% complete. At the bottom of the page is "The full code can be downloaded from here". Clicking on the "here" link redirects the browser to a "Sorry, the page you requested was not found" page.
      I see three blocks of sample code that don't seem to be 100% complete. At the bottom of the page is "The full code can be downloaded from here". Clicking on the "here" link redirects the browser to a "Sorry, the page you requested was not found" page.
Re: Posting thread data in a Perl/Tk window
by pc88mxer (Vicar) on Mar 08, 2008 at 16:54 UTC
    What about just using a file event callback? Something like:
    $widget->fileevent($fh, 'readable' => sub { ...call-back code... });
    In the call-back code you can directly interact with other widgets - e.g. change the text of a widget, etc.

    Also, since you are accessing the serial port as /dev/ttyS0, you should just be able to open and read/write to it directly just like any other file object:

    open(S, '+<', '/dev/ttyS0'); read(S, $buf, 1024, length($buf)); print S "etc.\n";
    The Device::SerialPort module is still useful for configuring the port, but it's not needed to read/write to it. One additional configuration you might want to use it to put the port into 'raw' mode instead of line-buffered mode. To be honest, I'm not really sure how it's done with Device::SerialPort -- I've always used the old-school system("stty -F /dev/ttyS0 raw"). :-)

    Finally, I haven't tried it, but judging from this example code, file event call-backs should work even in a Win32 environment.

    Update: Have a look at this page for an example of the fileevent call-back (under 'Executing Nonblocking System Commands') The code reads from a pipe, but the principle is the same with a serial port.