#! /usr/bin/perl -w

##########################################################
#
# Capture and display data from GW-Instek scopes
#
# (C) 2013 Doug Coulter, GPL v2 license -- share and enjoy,
# but if you improve it, be sure and share it back, to
# clab@swva.net, or www.coultersmithing.com/forums
#
#
# See use section below for any modules you'll need to install first
##########################################################
# It's probably obvious I'm coding this like I was a C programmer
# (because I am), and squishing fancy expressions into single liners
# would just make the inevitable changes this is going to need harder
# to figure out later -- for a C programmer (me).
#
# Anything I thought was dodgy is marked with an @@@ for searching
#
# If you're a perl purist or stylist, sorry 'bout that.  
# TMTOWTDI and, TANSTAAFL for good measure.
# lots of steal-me code here for beginning perl people
##########################################################


# Bring in a pile of library modules, this code is already written and tested by 
# someone else (mostly).  Most of these aren't standard, but have to be gotten from
# CPAN and installed via the standard way for your platform
# note, ubuntu software center and synaptic can usually find and install 
# these, usually with lib prepended to the name, and some mention of perl
# in the comments.  Should be obvious/simple if you try that.

# If you're missing any - you'll get an appropriate error message when
# you run this from a terminal to help you find the right ones.

use strict; # make perl tell me about typos and how dumb I am - built in
use Device::SerialPort; # the correct one for linux - cheats I tried with file funcs no good
## the above uses posix and a trick select to detect available bytes - and even that
# doesn't work with arb length reads, it says in its dox.  So you wind up doing some weird
# stuff, as I do below, to read things

use Fcntl; # to get some constants for sysopen
use Gtk2 -init; # sets up GUI basics
use Glib qw(TRUE FALSE); # they have their own version of truth
use Gtk2::GladeXML; # lets GUI come from a glade file
use Graphics::GnuplotIF qw(GnuplotIF); # gnuplot for plots
use File::Spec::Functions qw/:ALL/; # changes things like ./ to a real path
use File::Basename; # to get path to program for config files and logs
use Time::HiRes qw ( sleep ); # makes sleep take float inputs for short times
use Graphics::GnuplotIF qw(GnuplotIF); # gnuplot interface for plots
# obviously, you have to have gnuplot too somewhere

#############################################################
#################  Globals   ################################
#############################################################

# @@@  You will have to change this path to match your scope!!!
my $ttypath = '/dev/serial/by-id/usb-GOOD_WILL_INSTRUMENT_CO.__LTD._GDS-1022_EI181103-if00';
# if your scope isn't a gds 1022, you might have to change a lot of things,
# see your programming manual from GW-Instek

my $path; # absolute path to program dir, set by getmypath()
my $scope; # scope serial port object

my $datheader; # header from scope data
my $junk;  # for useless header stuff
my $samprate; # for sample rate of scope from header


my $raw;  # raw scope data

my @xaxis; # for plotting scope
my @yaxis;

my @peaks; # for plotting spectra
my @energy; # may calibrate someday
my $hits; # total hits counted for spectrum

my $plotdivisor = 1; # we'll only plot every nth time we get a scope grab
my $pollhit = 0; # the hit counter we'll plot modulo the above

#################### GUI stuff
my $gui; # ref to the whole mess
my $running = FALSE; # at least at first

my $gscope; # plot object for scope plots
my $gspect; # plot object for spectrum plots

### refs to the individual GUI stuff
my $pscope; # pointer to scope plot check
my $pspect; # pointer to spectrum plot check
my $threshb; # pointer to threshold widget
my $minwidthb; # pointer to min width widget
my $maxwidthb; # pointer to max width widget
my $gostopb; # pointer to go button
my $saveb;   # pointer to save file button

### values of controls checked at start time - only, not a bug, a feature
# if I let you change things during a run, it's garbage results, and
# I create the plot objects requested *once* at startup - and only then,
# as a time saver (your time).
my $scopeplot;
my $specplot;
my $threshold;
my $minwidth;
my $maxwidth;

my $debug = 1; # set true to make this noisy on STDOUT, and slow 

#############################################################
################ Subroutines   ##############################
#############################################################

###########################################################
# get pathname to program directory for use in config and log files
# we need this insane dance as the program path in $0 might be ./name
# which is kind of hard to use right.  We want an absolute path where
# the script lives, and this gets it (with two modules needed!)
sub getmypath()
{
 $path = rel2abs($0); # fix up any relative path we got
# print ("\nmunged path is: $path\n"); # debug
 $path = dirname($path); # now get just the dir without progname on it
#print ("\nfinally, path to program is: $path\n"); # debug 
}

###########################################################
# suck the data out of scope, channel 1, and convert to 
# signed perl numbers.  What were 9 bit ints packed big-endian
# become an array of (perl) doubles here, packed native.
sub getscope
{
 my $timeout;
 my $i = 0; # index/count of bytes read/reading
 my $hex = "";
 my $buffer = "";
 my $buffsize = 0;
 my $STALL_DEFAULT=10; # how many timeouts to wait for new input

 $scope->read_char_time(0);     # don't wait for each character
 $scope->read_const_time(200); #  milliseconds per unfulfilled "read" call
 $raw = ""; # clear out old stuff

 writescope(":STOP\n"); # we don't want half-weird buffers
 sleep .1; # give it time, it's a non-blocking write...
# here goes with the scary stuff - if USB gets outa sync, have to power cycle scope!
 writescope(":acq1:mem?\n"); # ask for entire chnl 1 memory (4k shorts + header)

 $timeout=$STALL_DEFAULT;
 while ($timeout>0) # get data header, should be simple? 
 {
  $! = 0;
  ($i,$datheader)=$scope->read(14); # header size
  if ($i > 0)  { last if ($i >= 14)}
  else {$timeout--;}
 }
 if ($timeout==0) {die "Waited $STALL_DEFAULT seconds and never got header\n";} 
# print "timeout:$timeout count:$i :header\n" if $debug; # test on header read

#now try to get data block...and then unpack it all correctly...
 $i=0; # restart count for main data block (8000 bytes)
 $timeout=$STALL_DEFAULT;

 while ($timeout>0) 
 {
  ($i,$hex)=$scope->read(255); # will read _up to_ 255 chars
  if ($i > 0) 
  {
   $buffsize+=$i;
   $raw.=$hex;
   last if $buffsize >= 8000;
  }
   else {$timeout--;}
 }
# print "timeout:$timeout buffsize:$buffsize\n" if $debug;
 if ($timeout==0) {die "Waited for scope and never saw what I wanted\n";}

 writescope (":RUN\n"); # restart scope
# writescope ("*TRG\n"); # force trigger, we're free-running here
}

###########################################################
sub openscope # I  tried not to use Device::serialport, 
# it's too character-based and we have a lotta binary here.
# but all other attempts failed...sigh
{
     $scope = new Device::SerialPort ($ttypath, 1) || die "Can't open $ttypath: $!\n";
     $scope->databits(8);
     $scope->baudrate(115200);
     $scope->parity("none");
     $scope->stopbits(1);
     $scope->handshake("none");
     $scope->datatype('raw');
     $scope->stty_icanon(0);
     $scope->are_match("\n");                       # possible end strings
     $scope->lookclear;                             # empty the buffer
     $scope->write_settings or undef $scope; # die, sort of, if not working
     die "no scope at $ttypath: $!" unless $scope;  # die, really 
     sleep 1;  # give this time
}

###########################################################
sub writescope
{
 my $written;
 my $cmd = shift;

 $written = $scope->write($cmd); # do the deed
 warn "write failed\n"         unless ($written);
 warn "write incomplete\n"     if ( $written != length($cmd));
}

###########################################################
sub creategui()
{# create a local scope so I can slurp internal "DATA" file whole
 my $guidata; # packed the xml internally, goes here
 local $/; # which makes this undefined just for this sub
 $guidata = <main::DATA>; # which in turn makes this slurp the entire file
 $gui = Gtk2::GladeXML->new_from_buffer($guidata); # create gui from in-program data, cool.
 $gui->signal_autoconnect_from_package('main'); # hooks subs to signals if things are named right
# not so program-specific you couldn't use it again
# in fact, this is about the 4th reuse I've had of this one
}

###########################################################
sub get_widget_pointers # this is always very program-specific
{ # so we can get or set their state when we want
 $pscope = $gui->get_widget('plotscope'); # pointer to scope plot check
 $pspect = $gui->get_widget('plotspec'); # pointer to spectrum plot check
 $threshb = $gui->get_widget('thresh'); # pointer to threshold widget
 $minwidthb = $gui->get_widget('minwidth'); # pointer to min width widget
 $maxwidthb = $gui->get_widget('maxwidth'); # pointer to max width widget
 $gostopb = $gui->get_widget('gostop'); # pointer to go button
 $saveb = $gui->get_widget('save');   # pointer to safe file button
}

###########################################################
sub get_widget_values # also very program-specific
{ # find out what user has asked for, maybe fix it if stupid.
 $scopeplot = $pscope->get_active() ? 1 : 0;
 $specplot = $pspect->get_active() ? 1 : 0;
 $threshold = $threshb->get_value_as_int();
 $minwidth = $minwidthb->get_value_as_int(); 
 $maxwidth = $maxwidthb->get_value_as_int();
 if ($minwidth >= $maxwidth)
 {
  $maxwidth = $minwidth + 2; # @@@ kludge...
  $maxwidthb->set_value($maxwidth); # force max > min
 }
 print "scope:$scopeplot spec:$specplot thresh:$threshold min:$minwidth max:$maxwidth\n" if $debug;
}

###########################################################
#@@@ - remember, this is stupid, so when copying for a new program read below
# to hook this up, in glade use gtk_main_quit as the handler in the mainwin/GtkWidget/destory-event
# also needed Gtkobject destroy event and check "after"?  Did both, now it works.
sub gtk_main_quit  { Gtk2->main_quit; } # fall out of main event loop
# if you don't have this, you can't kill the damn thing with the X button!
###########################################################
sub on_gostop_clicked # this starts or stops us - and either sets up or cleans up
{
 print "gostop clicked\n" if $debug;
 if ($running) # stop then, this is a toggle type thing
 {
  $running = FALSE; # so poll will quit doing things
  print "Stopping\n" if $debug;
  $gostopb->set_label('Go');



 } else
 { # start the world
  print "Starting\n" if $debug;
  $gostopb->set_label('Stop!'); # label to match new function for it
  get_widget_values(); # find out what we're supposed to be doing

  undef ($gscope); # kill any old plots
  undef ($gspect); # this timing leaves them up as long as possible

# if requested by user, generate gnuplot instances for the requests
# these will be filled in and plots generated in poll()
  if ($scopeplot) 
   {
    $gscope =Graphics::GnuplotIF->new(title => "Scope Data", raise => FALSE,
       style =>"lines", xlabel => 'Samples', ylabel =>'Amplitude');
    $gscope->gnuplot_cmd ("set grid");
    $gscope->gnuplot_set_plot_titles("RawScope");
   }

  if ($specplot) 
   {
    $gspect =Graphics::GnuplotIF->new(title => "Gamma Spectrum", raise => FALSE,
       style =>"lines", xlabel => 'Energy', ylabel =>'Counts');
    $gspect->gnuplot_cmd ("set grid");
    $gspect->gnuplot_set_plot_titles("Raw Spectrum");
   }

  resetdata(); # clear out any data from a previous run
  $running = TRUE;
  Glib::Timeout->add(1000, \&poll); # start the timed calls to the poll subroutine
 }
}
###########################################################
sub on_save_clicked
{ # someday, this will save a spectrum as a file we could plot later
 print "save clicked\n" if $debug;
}
###########################################################
sub resetdata
{
 $raw = "";  # raw scope data
 @xaxis = (); # for plotting scope
 @yaxis = ();
 @peaks = (); # for plotting spectra
 @energy = (); # may calibrate someday
 $hits = (); # total hits counted for spectrum
 $pollhit = 0; # the hit counter we'll plot modulo the above
}
###########################################################
###########################################################
sub poll # this is where the action is while running
{
 my $sample;
 my $i;
 my $case;
 my $can;
 my $canwidth;

 # cases
 # 0 - look for below threshold, if find, go to case 1
 # 1 look for candidate start above threshold (if find, init $can with it)
 # 2 if still above thresh, add to $can; if exceeds max width, go to 0 (and clean up can and width)
 # if goes below thresh, increment $peaks[$can] if width is above min,
 # then go to case 1
 # if in case 2 at end of data - ignore candidate

 print "poll hit, running=$running\n" if $debug;
 return FALSE unless $running; # stops GTK scheduling us
# do capture and plot stuff here
 getscope();  # grab scope data into @raw array
# take raw scope data and make it plottable in whatever form

 @yaxis = unpack("n1" x 4000,$raw);  # arcane, but works
 $i=0;
 foreach (@yaxis) {$xaxis[$i] = $i++};
 
 $can = 0;
 $canwidth = 0;
 $i = 0;
 if ($yaxis[0] > $threshold) {$case = 0;} else {$case = 1;}
 if ($specplot) # then do the math for that, otherwise skip it
 {
  foreach $sample (@yaxis)  
  {
   if ($case == 0)
   {
    if ($sample < $threshold) {$case = 1;} # go to next case and look for a start
   } elsif ($case == 1) # end of case 0
   {
    if ($sample >= $threshold)
    {
     $can = $sample; # start accumulating energy in $can
     $canwidth = 1; # because we just added one
     $case = 2; # continue adding to $can till falls below threshold
    }
   } elsif ($case == 2) # end of case 1
   {
    if ($sample >= $threshold)
    {
     $can += $sample; # continue accumulating energy
     $canwidth++; # keep track of width for further tests
    } else  # candidate ended.  If within width limits, update @peaks
    {
     if ($canwidth >= $minwidth && $canwidth <= $maxwidth) {$peaks[$can]++} # increment histogram bin
     $can = 0; # reset this when used!
     $case = 1; # no matter if it's passed width test, go to case 1
    } # fell below threshhold
   } # end of case 2
  } # foreach sample
# now, we should zero the undefs in @peaks, and set up the @energy array as well
  for($i=0;$i<=$#peaks;$i++)
  {
   $energy[$i] = $i; # arbitrary scale for now
   if (!defined $peaks[$i]) {$peaks[$i] = 0;} # zero any we didn't hit at all
  }

 } # if $specplot

 if ($pollhit >= $plotdivisor) # plot data
 {
  $pollhit = 0; # controls how often we plot any data - reset count when we do
  if ($scopeplot) {$gscope->gnuplot_plot_xy(\@xaxis, \@yaxis);} # rewrite plot
  
  if ($specplot) {$gspect->gnuplot_plot_xy( \@energy, \@peaks);} # rewrite plot
 }





 $pollhit++; # count the hits here so we know when to re-plot
 return TRUE; # keep the timer hits coming
}
###########################################################

#############################################################
################## MAIN #####################################
#############################################################

getmypath(); # path to program location, for other files etc
print "path is:$path\n" if $debug;
openscope(); # get the scope "file" open for reading/writing
creategui(); # create and draw the main GUI
get_widget_pointers(); # so we can talk to them

print "after create GUI\n" if $debug;

$pscope->set_active(TRUE) if $debug;


#getscope(); # try to read scope memory
# Enter the event loop
Gtk2->main; # make events work
# we die when the above exits
# do any cleanup or saving of presets here

$scope->close or die "failed to close";
#print "Stopping, close:$ret\n" if $debug;

# fall out of program...we're done


# after the __END__ is all the xml that defines the GUI widgets.
# this saves loading it from a file we might lose or something, and is faster
# anyway. See creategui() above.
__END__
<?xml version="1.0" encoding="UTF-8"?>
<glade-interface>
  <!-- interface-requires gtk+ 2.16 -->
  <!-- interface-naming-policy project-wide -->
  <widget class="GtkWindow" id="window1">
    <property name="visible">True</property>
    <property name="can_focus">False</property>
    <property name="title" translatable="yes">MCA</property>
    <signal name="destroy_event" handler="gtk_main_quit" swapped="no"/>
    <signal name="destroy" handler="gtk_main_quit" after="yes" swapped="no"/>
    <child>
      <widget class="GtkVBox" id="vbox1">
        <property name="visible">True</property>
        <property name="can_focus">False</property>
        <child>
          <widget class="GtkLabel" id="label1">
            <property name="visible">True</property>
            <property name="can_focus">False</property>
            <property name="label" translatable="yes">   Scope-based MCA for PC Linux   </property>
          </widget>
          <packing>
            <property name="expand">True</property>
            <property name="fill">True</property>
            <property name="position">0</property>
          </packing>
        </child>
        <child>
          <widget class="GtkHSeparator" id="hseparator1">
            <property name="visible">True</property>
            <property name="can_focus">False</property>
          </widget>
          <packing>
            <property name="expand">False</property>
            <property name="fill">True</property>
            <property name="position">1</property>
          </packing>
        </child>
        <child>
          <widget class="GtkCheckButton" id="plotscope">
            <property name="label" translatable="yes">Plot raw scope data?</property>
            <property name="visible">True</property>
            <property name="can_focus">True</property>
            <property name="receives_default">False</property>
            <property name="use_action_appearance">False</property>
            <property name="draw_indicator">True</property>
          </widget>
          <packing>
            <property name="expand">True</property>
            <property name="fill">True</property>
            <property name="position">2</property>
          </packing>
        </child>
        <child>
          <widget class="GtkCheckButton" id="plotspec">
            <property name="label" translatable="yes">Plot MCA spectrum?</property>
            <property name="visible">True</property>
            <property name="can_focus">True</property>
            <property name="receives_default">False</property>
            <property name="use_action_appearance">False</property>
            <property name="draw_indicator">True</property>
          </widget>
          <packing>
            <property name="expand">True</property>
            <property name="fill">True</property>
            <property name="position">3</property>
          </packing>
        </child>
        <child>
          <widget class="GtkHSeparator" id="hseparator2">
            <property name="visible">True</property>
            <property name="can_focus">False</property>
          </widget>
          <packing>
            <property name="expand">False</property>
            <property name="fill">True</property>
            <property name="position">4</property>
          </packing>
        </child>
        <child>
          <widget class="GtkLabel" id="label2">
            <property name="visible">True</property>
            <property name="can_focus">False</property>
            <property name="label" translatable="yes">Spectrum analysis settings</property>
          </widget>
          <packing>
            <property name="expand">True</property>
            <property name="fill">True</property>
            <property name="position">5</property>
          </packing>
        </child>
        <child>
          <widget class="GtkHBox" id="hbox1">
            <property name="visible">True</property>
            <property name="can_focus">False</property>
            <child>
              <widget class="GtkLabel" id="label3">
                <property name="visible">True</property>
                <property name="can_focus">False</property>
                <property name="label" translatable="yes">Threshold: </property>
              </widget>
              <packing>
                <property name="expand">False</property>
                <property name="fill">True</property>
                <property name="position">0</property>
              </packing>
            </child>
            <child>
              <widget class="GtkSpinButton" id="thresh">
                <property name="visible">True</property>
                <property name="can_focus">True</property>
                <property name="invisible_char">•</property>
                <property name="primary_icon_activatable">False</property>
                <property name="secondary_icon_activatable">False</property>
                <property name="primary_icon_sensitive">True</property>
                <property name="secondary_icon_sensitive">True</property>
                <property name="adjustment">10 1 50 1 10 0</property>
                <property name="climb_rate">0.01</property>
                <property name="numeric">True</property>
              </widget>
              <packing>
                <property name="expand">True</property>
                <property name="fill">True</property>
                <property name="position">1</property>
              </packing>
            </child>
          </widget>
          <packing>
            <property name="expand">True</property>
            <property name="fill">True</property>
            <property name="position">6</property>
          </packing>
        </child>
        <child>
          <widget class="GtkHBox" id="hbox2">
            <property name="visible">True</property>
            <property name="can_focus">False</property>
            <child>
              <widget class="GtkLabel" id="label4">
                <property name="visible">True</property>
                <property name="can_focus">False</property>
                <property name="label" translatable="yes">Min Width: </property>
              </widget>
              <packing>
                <property name="expand">False</property>
                <property name="fill">True</property>
                <property name="position">0</property>
              </packing>
            </child>
            <child>
              <widget class="GtkSpinButton" id="minwidth">
                <property name="visible">True</property>
                <property name="can_focus">True</property>
                <property name="invisible_char">•</property>
                <property name="primary_icon_activatable">False</property>
                <property name="secondary_icon_activatable">False</property>
                <property name="primary_icon_sensitive">True</property>
                <property name="secondary_icon_sensitive">True</property>
                <property name="adjustment">10 1 200 1 10 0</property>
                <property name="climb_rate">0.01</property>
                <property name="numeric">True</property>
              </widget>
              <packing>
                <property name="expand">True</property>
                <property name="fill">True</property>
                <property name="position">1</property>
              </packing>
            </child>
          </widget>
          <packing>
            <property name="expand">True</property>
            <property name="fill">True</property>
            <property name="position">7</property>
          </packing>
        </child>
        <child>
          <widget class="GtkHBox" id="hbox3">
            <property name="visible">True</property>
            <property name="can_focus">False</property>
            <child>
              <widget class="GtkLabel" id="label5">
                <property name="visible">True</property>
                <property name="can_focus">False</property>
                <property name="label" translatable="yes">Max width: </property>
              </widget>
              <packing>
                <property name="expand">False</property>
                <property name="fill">True</property>
                <property name="position">0</property>
              </packing>
            </child>
            <child>
              <widget class="GtkSpinButton" id="maxwidth">
                <property name="visible">True</property>
                <property name="can_focus">True</property>
                <property name="invisible_char">•</property>
                <property name="primary_icon_activatable">False</property>
                <property name="secondary_icon_activatable">False</property>
                <property name="primary_icon_sensitive">True</property>
                <property name="secondary_icon_sensitive">True</property>
                <property name="adjustment">10 10 600 1 10 0</property>
                <property name="climb_rate">0.01</property>
                <property name="numeric">True</property>
              </widget>
              <packing>
                <property name="expand">True</property>
                <property name="fill">True</property>
                <property name="position">1</property>
              </packing>
            </child>
          </widget>
          <packing>
            <property name="expand">True</property>
            <property name="fill">True</property>
            <property name="position">8</property>
          </packing>
        </child>
        <child>
          <widget class="GtkHSeparator" id="hseparator3">
            <property name="visible">True</property>
            <property name="can_focus">False</property>
          </widget>
          <packing>
            <property name="expand">False</property>
            <property name="fill">True</property>
            <property name="position">9</property>
          </packing>
        </child>
        <child>
          <widget class="GtkHSeparator" id="hseparator6">
            <property name="visible">True</property>
            <property name="can_focus">False</property>
          </widget>
          <packing>
            <property name="expand">False</property>
            <property name="fill">True</property>
            <property name="position">10</property>
          </packing>
        </child>
        <child>
          <widget class="GtkHSeparator" id="hseparator5">
            <property name="visible">True</property>
            <property name="can_focus">False</property>
          </widget>
          <packing>
            <property name="expand">False</property>
            <property name="fill">True</property>
            <property name="position">11</property>
          </packing>
        </child>
        <child>
          <widget class="GtkHSeparator" id="hseparator4">
            <property name="visible">True</property>
            <property name="can_focus">False</property>
          </widget>
          <packing>
            <property name="expand">False</property>
            <property name="fill">True</property>
            <property name="position">12</property>
          </packing>
        </child>
        <child>
          <widget class="GtkLabel" id="label6">
            <property name="visible">True</property>
            <property name="can_focus">False</property>
            <property name="label" translatable="yes">Actions</property>
          </widget>
          <packing>
            <property name="expand">True</property>
            <property name="fill">True</property>
            <property name="position">13</property>
          </packing>
        </child>
        <child>
          <widget class="GtkButton" id="save">
            <property name="label" translatable="yes">Save Spectrum</property>
            <property name="visible">True</property>
            <property name="can_focus">True</property>
            <property name="receives_default">True</property>
            <property name="border_width">3</property>
            <property name="use_action_appearance">False</property>
            <signal name="clicked" handler="on_save_clicked" after="yes" swapped="no"/>
          </widget>
          <packing>
            <property name="expand">True</property>
            <property name="fill">True</property>
            <property name="position">14</property>
          </packing>
        </child>
        <child>
          <widget class="GtkButton" id="gostop">
            <property name="label" translatable="yes">Go</property>
            <property name="visible">True</property>
            <property name="can_focus">True</property>
            <property name="has_default">True</property>
            <property name="receives_default">True</property>
            <property name="border_width">3</property>
            <property name="use_action_appearance">False</property>
            <signal name="clicked" handler="on_gostop_clicked" after="yes" swapped="no"/>
          </widget>
          <packing>
            <property name="expand">True</property>
            <property name="fill">True</property>
            <property name="position">15</property>
          </packing>
        </child>
      </widget>
    </child>
  </widget>
</glade-interface>
