#!/usr/bin/perl -w

# stuff we use, this wheel's been round for awhile

# -------------------------------------------------------

use strict;  # let the interpreter catch as many errors of mine as possible
use feature qw(switch say);
use Tk;
#use Tk::LabEntry; # for fancy labled edit boxes

use Tk::MsgBox;
use Tk::DialogBox;
use Tk::LabEntry;

use DBI;
use Device::SerialPort;
my ($port, $portname);
my @linevals;  # array of geiger/A-D valuse after split
my $debug = 0;  # disable/0 enable/1 debug output

#this is a GUI that is meant to help acquire data from various instruments around the fusor
#physics project and shove it into a MySQL database for later data mining and analysis.

#**********************************************************************************************
#The idea here is to gather some user input about what we want to get from a run, and pass this
#info to our own little realtime run loop that also shovels some events to make the GUI stay
#working.

#Expect this to grow and become more complex as time passes.  We are beginning with the outputs
#of the MulitGeiger hardware, passed to us on a serial connection, but plan to add things like
#the DSO scope, the soundcard, and the mass spectrometer as time goes along +++ whatever else,
# some of which might need started up or initialized or settings-captured on startup.


#@@@ GUI-TODO add display options (eg make a realtime plot --popup , and schema select for each thing
# allow schema  to be edited or added here?
#@@@ we need to catch window close with "X" and clean up the database,
#just as if user had pressed stop (though stop may ask for post run comments, this may not),
#instead, or if there's anything "dirty" needing saved -- same as any editor.


# style notes:  I am a C(++) programmer, which should be obvious looking at this.
# I DO know how to reduce this to quite a few less lines, but am going for
# clarity here, and easy-to-copy examples of how to do some things.
# so expect (and if you're adding code please put in) copious good comments.


# declare stuff upfront -- fast and clean -- globals

my      ($ssbutton,$getADck,$getGeigersck,              # widgets mostly
       $lstat,$rstat,$prerun,$postrun,
       $getSCck,$getSpecspecck,$getSpecspec,$getSpecADck,

       $mw,$bframe,$rframe,$lframe,$tframe,            # frames for layout
       $mgframe,$scframe,$specframe,   # frames inside data aq flags frame (currently lframe)

       $sstext,$MWrunning, $timetext,                  # global vars
       $lstatext,$rstatext,$preruntext,                #
       $postruntext,$getAD, $getGeigers,               #
       $getSpecAD,$runtimeseconds,$runtimeminutes,     #
       $runtimehours,
       $PICtimeBase, $PICstart, $PICstop,          # PIC time, to sync data recorded separately

       $timerid                );                      # IDs

my $pid;
my $runID;   # set when Start button pressed
my $savePrc = 0;
my $spcb = 0;

#  SoundCard globals
#
my $getSC;
my $getSCtype;
my $SCstart = 0;
my $SCcommand = "";

# Scope globals
#
my $scopeState = 0;  # state variable for scope config fsm
my ($scopeframe, $getScopeck, $getScope);  # vars for scope settings aq
my ($screenframe, $getScreenck, $getScreen);  # vars for screen image aq

#  A/D conversion factors - @@@ FIX THESE so factors are retrieved from ADSchema[SetupID]
#
my $KvFactor = 0.009293;  # derivation: PSvoltage(Kv) / AD[1]value , e.g. 50.0 / 5380.0 = .009293
my $mAfactor = 0.007571;  # derivation: PScurrent(mA) / AD[2]value , e.g. 2.9 / 383.0 = 0.007571
my $mbarFactor = 1;       # derivation: TBD
my $noiseFactor = 1;      # derivation: TBD

# time stuff for localtime conversions
# there is a perl module that does some of this, but this is leaner and quicker, I think,
# not to metion we now have the relevant documentation typed in as a comment below.
my @curtime; # for time data from localtime() on ticks
# the above will contain at offset:
# 0    1     2    3     4     5    6     7      8
#sec, min, hour, mday, mon, year, wday, yday, isdst

#lookups into English names for the time numbers --  @@@ is there a const trick we can use here?
my @days = ('Sun','Mon','Tue','Wed','Thu','Fri','Sat'); #lookup from zero based #'s
my @months = ('January','February','March','April','May',
       'June','July','August','September','October','November','December');

$PICtimeBase = 1;
$PICstart = 1;
$PICstop = 1;

###
#  Connect to DB
#
my $db = DBI->connect('dbi:mysql:fusordata','fusoraq','dotdot') or die
   "Couldn't connect DBI err is: $DBI::errstr, syserr is: $!";

# ------------------------------
#  askBox() - a generic question message box, with yes/no answer
#     IN: $askTitle - set to yes/no title
#         $askText  - set to yes/no question/text
#    OUT: returns 1 on YES
#                 0 on NO
#
my $askTitle = ""; # set by caller
my $askText = "";  #set by caller
my $askw;
sub opQuery() {
  my $rc;
  $askw = $mw->MsgBox(
        -title=>$askTitle,
        -message=>$askText,
        -icon=>'question',
        -type=>'yesno',
        -default=>'yes' );
  my $askA = $askw->Show;
  print "askA = [$askA]\n";
  if ( $askA eq 'yes' ){
    $rc = 1;
  } else {
    $rc = 0;
  }
  return $rc;
}

# addSignal() - add a new signal description to the scopeSchema
#
my $sigAdd = $db->prepare("insert into ScopeSchema (signal) values (?)");
sub addSignal(){
 my $signal;
 my $ans;
 my $asDia;

 $asDia = $mw->DialogBox(
             -title=>"Add Scope Signal Description",
             -buttons=>['Done','Cancel']);
 $asDia->add('LabEntry',-textvariable=>\$signal, -width=>60,
           -label=>'Signal',-labelPack=>[-side=>'left'])->pack;
 $ans = $asDia->Show();
 if ( $ans eq "Done" ){
   print "add [$signal] to db\n";
   $sigAdd->execute($signal);
 } else {  # cancel
   return;
 }
}


# -------------------------------------------------------
#    chooseSignal() - show available scope signals from scopeSchema
#       IN: operator selection
#      out: ssList, ssDesc parallel arrays of rowNo/signal values from ScopeSchema
#           scopeState set to 2 - indicates a selection made
#
my $getSScmsh = $db->prepare("select rowNo, signal from ScopeSchema");
my @ssRow;
my $ssel;

sub chooseSignal(){
  my $sindx = 0;
  my $lb;

  $ssel = "none";  # array returned by selection
  if ( $scopeState == 10) {  # create list of signals for selection
    $lb = $mw->Scrolled("Listbox", -scrollbars => "e", -selectmode => 'single')->pack();
          # single == radiobutton
    $getSScmsh->execute or die "scopeSchema select failed";
    if($debug){
      printf("ScopeSchema rows: %d fields: %d\n",
              $getSScmsh->rows, $getSScmsh->{'NUM_OF_FIELDS'});
      printf("Indx Signal  Description\n");
    }
    while (@ssRow = $getSScmsh->fetchrow()){

      #  Add rows to the listbox
      $lb->insert('end',$ssRow[0]."-".$ssRow[1]);
      $sindx++;
    }
    $getSScmsh->finish();  # close DB query
    $lb->insert('end',"ADD NEW SIGNAL");

    # establish callback for selection event
    $lb->bind('<ButtonRelease-1>', sub{
           $ssel = $lb->get($lb->curselection);
           $lb->destroy;
           if($debug){print "sela is [$ssel]\n";}
           $scopeState = 13; # indicate a signal has been selected
         } );
    $scopeState = 12;
  } else {
    return;
  }
}

# ----------------------------------------------
#  scopeConfig() - display scope channel/signal configuration, for (re)config
#
#
my $sigSet = 0;  # 0 if no signals set
my @sigRow = [0,0,0,0];
my @scopeSig = ["","","",""];
my $curChannel = 0;  # chan/state: [1...4] - sig selection pending on channel, 0 - done

sub scopeConfig(){
 # show scope channels with blank/prior signals with buttons (to select different signal)
 my $ans;
 my $scDia;
 my @tok;

 $scDia = $mw->DialogBox(
             -title=>"Configure Scope Channel/Signal",
             -buttons=>['Chan1', 'Chan2', 'Chan3', 'Chan4','Done','Reset']);
 $scDia->add('LabEntry',-textvariable=>\$scopeSig[0], -width=>100,
           -label=>'Channel 1',-labelPack=>[-side=>'left'])->pack;
 $scDia->add('LabEntry',-textvariable=>\$scopeSig[1], -width=>100,
           -label=>'Channel 2',-labelPack=>[-side=>'left'])->pack;
 $scDia->add('LabEntry',-textvariable=>\$scopeSig[2], -width=>100,
           -label=>'Channel 3',-labelPack=>[-side=>'left'])->pack;
 $scDia->add('LabEntry',-textvariable=>\$scopeSig[3], -width=>100,
           -label=>'Channel 4',-labelPack=>[-side=>'left'])->pack;
 $ans = $scDia->Show();
 given($ans){
   when("Chan1"){
     $curChannel = 1;
     chooseSignal();
   }
   when("Chan2"){
     $curChannel = 2;
     chooseSignal();
   }
   when("Chan3"){
     $curChannel = 3;
     chooseSignal();
   }
   when("Chan4"){
     $curChannel = 4;
     chooseSignal();
   }
   when("Done"){
     $curChannel = 0;
     $scopeState = 14;
   }
   when("Reset"){  # Reset current values
     $curChannel = 0;
     print "Reset\n";
         $sigSet = 0;
     @sigRow = [0,0,0,0];
     @scopeSig = ["","","",""];
   }
   default{ # effectively a cancel
   $curChannel = 0;
   }
 }
}

# chkScopeState() - fsm to manage scope channel/signal config
#                    Called initially from startStop(), then from rt_loop() until done.
#
#       scopeState - fsm state variable:
#                0 - ask op to config scope chan/sig for this run
#               10 - signal selection in progress
#               12 - waiting for signal select
#               13 - signal selected, process, back to state 10
#               14 - config done
#               15 - run in progress
#               16 - waiting for next start
#
sub chkScopeState(){
  my $sindex = -1;
  my $sDesc = "none";
  given($scopeState){
    when(0){ # ask operator about scope config
          if ( $sigSet > 0 ) { # if previous run spec'd channel/signals
            $scopeState = 10;
                scopeConfig();  # call scopeConfig (to allow reconfig/reset)
          } else { # ask op whether to config channel/signals
        $sstext = sprintf("Scope/Signal config [getGeigers:%d getAd:%d]", $getGeigers, $getAD);
        $askTitle = "Scope Channel/Signal Configuration";
        $askText = "Describe signals on active scope channels";
        if (opQuery() > 0){ # ask op to set/modify scope channel/signal config
           $scopeState = 10;
           scopeConfig();  # set channel/sig
        } else {
          $scopeState = 14;
        }
      }
        }
    when(10){ # config for this run in progress
      scopeConfig();  # try for another
    }
    when(12){ # waiting for a signal selection
      return;
    }
    when(14){
      return;
    }
    when(13){ # selection made
       $scopeState = 10; # go back for another
       print "ssel [$ssel]\n";

       if ($ssel eq "ADD NEW SIGNAL") {
         addSignal();
       } else {
         # set channel selection
         my @tok = split(/-/,$ssel);
         printf ("tok[0]: {%02d} tok[1]: {$tok[1]}\n", $tok[0]);
         # save selection in @scopeSig
         if ( $curChannel > 0 ) {
            $sigRow[$curChannel-1] = $tok[0];
            $scopeSig[$curChannel-1] = $tok[1];
                        $sigSet = 1;
         }
       }
    }
    when(15){ # run in progress
       return;
    }
    when(16){  # run complete, ready to start another
      return;
    }
    default{print "debug: scopeState ($scopeState)\n";}
  }
 }

# ---------------------------------------------------------------------------------------------
#  selectRunRype() - fsm to manage runType selection (from RunSchema DB table)
#                    Called initially from startStop(), then from rt_loop() until done.
#
#           $sel - fsm State variable:
#              0 - run "Start" hit
#              1 - initial/tentative selection made, put up "GO" button
#              2 - waiting for "GO" button hit to save selection
#              3 - final selection made
#              4 - ** Stopped after a run
#              5 - waiting for op Answer "use prev runType?"
#              10 - listbox up, waiting for initial selection
#
my $sel = 0;  #  state variable
#
my $runType = 0;  # currently selected runtype (0 == none selected)
my $lastRunType = 0;  # previous tuntype selection
my $rindx = 0;
my @rsRow;  # a retrieved row
my @rtList; # array of numeric runtypes gleaned from retrieved rows (as $rsRow[0])
my @rtDesc; # array of runtype descriptions from rows
my @csel;   # cursor selection
my $runDesc; # description of current runtype
my $lb = 0;  # listbox widget ptr
my $lbAns = -1;  # lbAsk() result
my $gb = 0;  # go buttn widget ptr
my $askb1;
my $askb2;  # ask button ptr
#
my $goHit = 0;  # set to 1 when GO button clicked, reset to 0 by the oneshot in rt_loop()
#
sub selectRunType(){  #
 given ($sel) {
   when(0){  # state 0 == no previous runType
     rtListBox();
     $sel = 10;
   }
   when(4){
         if ( $runDesc eq "" ) {
       $sel = 0;  # don't ask, call rtListBox on next entry
         } else {
          lbAsk();
         }
       } # state 4 (not 1st run) == ask if op wants to use previous runtype
 }
}

# lbAsk() - ask operator whether to choose another runType
#
sub lbAsk(){
   $askTitle = "Previous Run Type was:";
   $askText = "[$runDesc]\nChoose another Run Type?";
   if (opQuery() > 0){ # op answered "yes"
       rtListBox();  # ask op to select a new runType & runDesc
       $sel = 10;     # go to state 10 (waiting for op to choose new runType)
   } else { # op answered "no"
       $runType = $lastRunType;
       $sel = 5;      # go to state 5 GO button w/o list
   }
}

#  rtListBox() - Put up a listbox of runTypes from Run Schema
#
my $getrScmsh = $db->prepare("select RunType, Description from RunSchema");
sub rtListBox(){
  $sstext = sprintf("RunType config [getGeigers:%d getAd:%d]", $getGeigers, $getAD);
  $lb = $mw->Scrolled("Listbox",-scrollbars => "e", -selectmode => 'single')->pack();
                                                       # single == radiobutton
  $getrScmsh->execute;
  if($debug){
    printf("RunSchema rows: %d fields: %d\n", $getrScmsh->rows, $getrScmsh->{'NUM_OF_FIELDS'});
    printf("Indx RunType  Description\n");
  }
  while (@rsRow = $getrScmsh->fetchrow()){
    if($debug){printf("%02d - %02d %s\n",$rindx,$rsRow[0],$rsRow[1]);}
    $rtList[$rindx] = $rsRow[0];  #  save runType value from DB (not necessarily same as rowNo)
    $rtDesc[$rindx] = $rsRow[1];  #  save runtype description - used in lbAsk()
    if($debug){printf("   rtList[%d]=%s\n",$rindx,$rtList[$rindx]);}
    #  Add rows to the listbox
    $lb->insert('end',$rsRow[0]." - ".$rsRow[1]);
    $rindx++;
  }
  $rindx = 0;
  $getrScmsh->finish();  # close DB query
  if($debug){
    print "\@rtList:@rtList";
  }
  $lb->bind('<Button-1>',
      sub{$lb->configure(-selectforeground => "red");
        print "at bind Button-1 hit - sel:$sel\n";
        if ($sel == 10) { # only change state if waiting for initial selection
            $sel = 1;  # go to state 1 = indicate a selection has been made
        }  
      } );  # highlight the left-button selection
  $csel[0] = -1;
  # selections are handled by rtSelChk() when called from on_tick()
}

#  rtSelChk() - fsm to ensure that a runType selection is made before starting the run.
#             On initial selection, put up a "Go" button.  Selection is finalized when
#             Go button is clicked.
#             Called from on_tick()
#
sub set2go(){  # for use by when(1) and when(5) below (only)
 $sel = 2;  # go to state 2 = waiting for GO button hit
 $sstext = sprintf("Clink GO BUTTON to commence Data Acquisition \n(click here to CANCEL)");
}
sub rtselChk(){
 if($debug+1){print "rtselChk: sel = $sel lbAns = $lbAns\n";}
 given($sel){
   when(1){  # initial listbox selection has been made, put up a "GO" button
       $gb = $mw->Button(-text => "Go", -command => sub{
          @csel = $lb->curselection();
          my $xx = $csel[0];
          $runType = $rtList[$xx];
          $runDesc = $rtDesc[$xx];
          if($debug){printf("xx is [%s] runType set to [%s]\n",$xx,$runType);}
          $lb->destroy;
                  $lb = 0;
          $gb->destroy;
                  $gb = 0;
          $sel = 3;  # go to state 3 = indicate GO button hit
          $goHit = 1;
       })->pack(-side => "bottom");
       set2go();
   }  # end when(1)
   when(5){  # handles Go button when using runType from previous run
       $gb = $mw->Button(-text => "Go", -command => sub{
          $gb->destroy;
                  $gb = 0;
          $sel = 3;  # go to state 3 = indicate GO button hit
          $goHit = 1;
       })->pack(-side => "bottom");
       set2go();
   }  # end when(5)
  }  # end of given($sel)
}

# -------------------------------------------------------------
#  Enhanced (10/21/2010) Run Detail stuff
#
#  variables kept specifically for RunDetail stats 
#

my $cntWaveforms = 0; # count
my $cntScreens = 0;   # total screens (auto + manual capture)
my $cntGeigers = 0;  # count this run
my $cntAD = 0;  # AD count this run
my $geigerBackground = 0;  # @@@  avg background, TBD


my $putrDetsh = $db->prepare("insert into RunDetail  ( 
         modTime,createTime,
         runID,RunType,DetailType,Details,
         serialPort,soundCapture,soundParms,
         cfgWaveform,sigCh1,sigCh2,sigCh3,sigCh4,cntWaveforms,
         cfgScreensnap,cntScreens,
         cfgGeiger,cntGeigers,geigerBackground,
         AD,ADcount,KvFactor,mAfactor,mbarFactor,noiseFactor
       ) values (NULL,NULL,
                  ?,?,?,?,
                  ?,?,?,
                  ?,?,?,?,?,?,
                  ?,?,
                  ?,?,?,
                  ?,?,?,?,?,?
        )"
    );
    
# --------------------------------------
#  saveRunDetail( detailType, detailValue )
#   where: detailType = { "prerun" | "postrun" }
#          detailValue = { $prerun->Contents | $postrun->COntents }
#
sub saveRunDetail{
  my ($dt, $dv) = @_;    # 
  print "dt:[$dt] dv[$dv]\n";       
  $putrDetsh->execute($runID,$runType,$dt,$dv,
     $portname,$getSC,$getSCtype,
     $getScope,$sigRow[0],$sigRow[1],$sigRow[2],$sigRow[3],$cntWaveforms,
     $getScreen,$cntScreens,
     $getGeigers,$cntGeigers,$geigerBackground,
     $getAD,$cntAD,$KvFactor,$mAfactor,$mbarFactor,$noiseFactor);
}


sub resetRunStats(){
  $cntWaveforms = 
  $cntScreens = 
  $cntGeigers = 
  $cntAD = 0;
}

#
# mysql prepare stmts for geiger & a/d data inserts
#
# statment handle prepares() hoisted out of loop for speed at the mySQL/DBI end of things
# and will later save time reinstantiating them when below is a sub in a repeat loop
# @@@ this is where we set mapping between device output and our DB columns
#
my $adsh = $db->prepare("INSERT INTO SlowAD (modTime,createTime,runID,PICtime,ch0,ch1,ch2,ch3,lashup)
                              values (NULL,NULL,?,?,?,?,?,?,?)");
my $csh = $db->prepare("INSERT INTO Counters (modtime,createtime,runID,PICTime,duration,Geigers,p1,p2,p3,p4,LID)
                              values (NULL,NULL,?,?,?,?,?,?,?,?,?)");
my ($beginat, $stopat);
my $scsh = $db->prepare(
  "insert into sound (modTime,createTime,starttime,endtime,runID,PICstart,PICstop,soundID,audioType,audio)
               values(NULL,NULL,?,?,?,?,?,?,?,LOAD_FILE(?))");
my $audType = "flac";  # default audio recording type

# @@@ The following 2 prepares insert the scope settings and scope.bmp files created by
#     the interim scope tools and the scopeSnap script - to be replaced with a perl module
#     interface to the gds-bin-read function when available.  09/16-DMK
#
my $scopecsh = $db->prepare(
  "insert into scopeSettings (modTime,createTime,runID,PICtime,settings)
               values(NULL,NULL,?,?,LOAD_FILE(?))");

my $screencsh = $db->prepare(
  "insert into screenShot (modTime,createTime,runID,PICtime,imType,screen)
               values(NULL,NULL,?,?,?,LOAD_FILE(?))");

# @@@ ---------------------
#
#  This code should be replaced with (GUI) code that determines the A/D SetupID, and retrieves the factors
#  from fusordata.ADschema[SetupID]
#
# @@@ Assume "lashup value" to be the "setupID" (i.e. the key into ADSchema)?
my $SetupID = 1;   # @@@ fixme -  This is the MultiGeiger LID/lashup value


# @@@ ---------------------

#***********************************************************************************************

# finally, getting to the code....


# I like to put my subs first, just to pee off folks who do it otherwise
# besides, in a gui, this is where all the action is, the main is just setup,
# then pushing events while you let it run.  When *you* run, you gotta push them -- or the
# UI will lock up.

#/////////////////////////////
#
# Periodic Scope settings & screenImage acquisition
#
my $ssTid;
my $ssInterval = 1000;  # interval (milliseconds) between scope readings
                       # @@@ grab this from a scope schema or GUI TBD?
sub scopeGet(){
 if ( $sel != 3 ){  # only collect data during a run
   print "scopeGet ignored - no run in progress (sel = 3)\n";
 }
 printf("scopeGet: runID = $runID\n");  # testing
 # pull scope settings (once)
 system "scopeSnap";
 if ( $? == 0 ) { # save /tmp/scope.settings in DB
   $scopecsh->execute($runID,$PICtimeBase,"/tmp/scope.settings");
   $cntWaveforms++;
 } else {
   die "scopeSnap RC not zero $!\n";
 }
 if ($MWrunning) {
   $ssTid = $mw->after($ssInterval => \&scopeGet);  # reschedule
 }
}
sub scopeGetStart(){
 $ssTid = $mw->after($ssInterval => \&scopeGet);
}
sub scopeGetStop(){
 if($ssTid) { $ssTid->cancel};  # cancel pending event
 $ssTid = 0;
}

my $siTid = 0;
my $siInterval = 5000;  # interval (milliseconds) between scope readings
                       # @@@ grab this from a scope schema TBD?
sub screenGet(){
 if ($sel != 3 ){ # only collect screen shots when run in progress
   print "screenGet ignored - no run in progress (sel = 3)\n";
   return;  # no, ignore
 }
 printf("screenGet: runID = $runID\n");  # testing
 # pull scope screen image
 system "scopeSnap", "-i";
 # save in DB
 if ( $? == 0 ) { # save /tmp/scope.bmp in DB
   $screencsh->execute($runID,$PICtimeBase,"BMP","/tmp/scope.bmp");
   $cntScreens++;
 } else {
   die "scopeSnap RC not zero $!\n";
 }
}

sub screenIntvlExp(){
 screenGet();
 if ($MWrunning ) {
   $siTid = $mw->after($siInterval => \&screenIntvlExp);  # reschedule
 }
}

sub screenGetStart(){
 $siTid = $mw->after($siInterval => \&screenIntvlExp);
}

sub screenGetStop(){
 if ($siTid) {$siTid->cancel};  # cancel pending event
 $siTid = 0;
}


#///////////////////////
#
#   setDT() - returns localtime() as a mysql datetime-format string
#
sub setDT() {
  my $dstring;
  @curtime = localtime();
  $dstring = sprintf("%02d-%02d-%02d %02d:%02d:%02d",
      $curtime[5]+1900,$curtime[4]+1,$curtime[3],
      $curtime[2],$curtime[1],$curtime[0]);
  if($debug){printf("setDT: dstring = [%s]\n", $dstring);}
  return($dstring);
 }

#@@@ add start and stop init/cleanup to this too.

sub savePostRun(){
   if ($savePrc) {
      saveRunDetail("postrun",$postrun->Contents);
          $savePrc = 0;
          if ( $spcb ) {
            $spcb->destroy;
            $spcb = 0;
          }
       }
}

sub startstop # I hope the names are obvious enough
{
 my $fname = "/tmp/test.flac"; # this needs to be better
 if ($MWrunning)
 {  # do whatever we do to stop, and clean up after a run

   scopeGetStop();
   screenGetStop();

   $scopeState = 16;  # run finished, ready for next start

   if ($getSC > 0) {  # post-run cleanup
     kill ( 'INT', $pid);  # kill the SoX instance
     $stopat = setDT();
     $PICstop = $PICtimeBase;
         if($debug){printf("after kill(), stopat:%s\n",$stopat);}
     close (AUD);  # end the capture/audio file
     #
     #  Insert audio file in the DB (as a LONGBLOB, potential max is 4GB)
     #  Note: audio files can be large, the default mysql max longblob 
     #  size is 16MB, but can be made larger by editing /etc/mysql/my.cnf
     #  setting for  max_allowed_packet=75M (for example)
     #
     $scsh->execute($beginat,$stopat,$runID,$PICstart,$PICstop,$runID,$audType,$fname) or die "insert sound failed:$DBI::errstr";
         if(1+$debug){printf("added test.flac\n\n");}
   }

   #if ( $getGeigers > 0 ) {  # post-run cleanup
   #}

   #if ($getAD > 0 ) {  # post-run cleanup
   #}

   $postrun->Insert("Data acquisition $rstatext \n"); # save runtime info in postrun comment
   $savePrc = 1;  # post-run comments will be saved on Save/Start button or termination.

   # @@@ Solicit user input to the postrun area before saving (on "Save" button click)
   #     and/or provide a means to edit the postrun (and prerun?) comments later (another pgm).
   $spcb = $mw->Button(-text => "Save POST-RUN\nComments Now", -command => sub{
          savePostRun();
          $sstext = "Start"; # change the button text
      })->pack(-side => "bottom");

   $MWrunning = 0;             # presumeably someone is watching this variable
   $lastRunType = $runType;  # save runType
   $runType = 0;           # run won't start until  runType is selected
   $sstext = "Data acquisition stopped"; #change the button text
   $sel = 4;  # go to selectRuntime() state 4 = stopped after a run
       if ($gb != 0){  # clear go button, if still up
         $gb->destroy;
         $gb = 0;
       }
       if ($lb != 0){  # clear go button, if still up
     $lb->destroy;
         $lb = 0;
       }
   #print "end STOP:  sel:$sel runType:$runType lastRunType:$lastRunType running:$running";
 } else
 {
   # do whatever to begin aux program in BG, save pid for possible kill.  We won't know
   #    if it terminates on its own (and leaves a zombie process).  Regardless, the Stop
   #    (kill/close) will get rid of the zombie process if it exists.
   # e.g.:  $pid = open(FOO, "| xtest 2> xtest.log") or die "couldn't fork xtest $!\n";

   savePostRun();  # just in case user didn't do it
   
   resetRunStats();

   #  Establish the (new) runID
   $runID = time;  # runID is number of second from the epoch (Unix time

   $scopeState = 0;  # start up scope config
   chkScopeState();

   # note periodic scope setting/screen captures commence when official run starts
   if ( $getScope > 0 ) {
     scopeGetStart(); # initiate periodic scope settings capture
   }

   if ( $getScreen > 0 ) {
     screenGetStart();  # initiate periodic scope screen shots
   }

   #  Set up Sound Capture via SoX -> audio file -
   if ( $getSC > 0 ) {
     my $recType = "-c 1 -r 8000";
     if ( $getSCtype == 2 ) { $recType = "-c 2 -r 8000";}
     if ( $getSCtype == 3 ) { $recType = "-c 1 -r 44100";}
     if ( $getSCtype == 4 ) { $recType = "-c 2 -r 44100";}

     # use "-q" (quiet) to suppress the "In:..." output to stderr
     $SCcommand = sprintf("| rec -q $recType %s", $fname);
     $audType = "flac";  # default audioType
         if($debug){printf("SCcmd:[%s] - audType:[%s]\n",$SCcommand,$audType);}
         $SCstart = 1;
     $MWrunning = 1;
   }


     if ( $SCstart > 0 ) {
       $beginat = setDT();
       $runtimeseconds = $runtimeminutes = $runtimehours = 0;
           $pid = open(AUD, $SCcommand) or die "couldn't fork SoX $!\n";
               $SCstart = 0;
         }
   if ($getGeigers > 0 || $getAD > 0 ) { #  Set up to acquire Geigers & A/D from a Serial Port
   #
   # @@@config mechanism needs to be better!
   #
   my $ttyPort = "ttyUSB0";   # @@@ edit if Multi-Geiger on a different tty port
   #my $ttyPort = "ttyS0";   # @@@ edit if Multi-Geiger on a different tty port

   my $cfgfile = sprintf("%s_test.cfg", $ttyPort);
   $portname = sprintf("/dev/%s", $ttyPort);

       if ( -e $cfgfile ) {   # cfgfile exists
     $port = Device::SerialPort->start ($cfgfile) or die "Can't start $cfgfile\n";
       } else {   # cfgfile does not exist (e.g. 1st time for a tty device)
     $port = new Device::SerialPort ($portname, 1) || die "Can't open $portname: $!\n";
       }

   # some of this not needed if config file exists, this is for suspenders (and first times)
     $port->databits(8);
     $port->baudrate(57600);
     $port->parity("none");
     $port->stopbits(1);
     $port->handshake("none");
     $port->stty_icanon(1);    # this is what gets us on line boundaries
     $port->are_match("\n");                       # possible end strings
     $port->lookclear;                             # empty the buffer
     $port->write_settings || undef $port; # die, sort of, if not working
     $port->save($cfgfile) || warn "Can't save $cfgfile: $!\n"; # save for next time
     #### now we can do things with the port
     $runtimeseconds = $runtimeminutes = $runtimehours = 0;
     $MWrunning = 1;
   }

   rt_loop(); # call the realtime data aq stuff and let it return when $MWrunning is set to zero
 }
} #end modechange

#///////////////////////

sub on_tick # update status labels every second or so
{
@curtime = localtime(); # get the time right now

 if ($sel == 1 || $sel == 5 ) {
   rtselChk(); #  sets $sel to 3 when ready to start the actual run
 }

#printf ("time now:%02d:%02d:%02d  %s, %s %02d, %04d\n", # other uses for this line of code, @@@ steal me
#$curtime[2],$curtime[1],$curtime[0],$days[$curtime[6]],$months[$curtime[4]],$curtime[3],$curtime[5]+1900);

# put formatted time in upper left status window label
$lstatext = sprintf ("%02d:%02d:%02d  %s, %s %02d, %04d",
$curtime[2],$curtime[1],$curtime[0],$days[$curtime[6]],$months[$curtime[4]],$curtime[3],$curtime[5]+1900);

#todo, count run time here too when MWrunning.
 #if ($MWrunning)
 if ( $sel == 3 )
 {
  if (++$runtimeseconds >= 60) # keep track of MWrunning time
  {
   $runtimeseconds = 0;
   if (++$runtimeminutes >= 60)
   {
    $runtimeminutes = 0;
    $runtimehours++;
   }
 }

 $rstatext = sprintf("runID:%d  Runtime: %04d:%02d:%02d",
                     $runID,$runtimehours,$runtimeminutes,$runtimeseconds);

 } else {
   $rstatext = "Not running now.";
 }

}  # end on_tick()

#///////////////////////

#///////////////////////

#///////////////////////*************************************///////////////////////////////////
#This is our data processing loop during a run.  It has to push events itself to keep the GUI alive.

#warning:  dox say use this, but even with hardcoded DONT_WAIT = 0x01, doesn't fly right
# $t=DoOneEvent(); # push as many events as are backed up, but didn't work as planned

sub rt_loop
{
 my $w=0;

 while ($MWrunning)
 {
 $mw->update(); # this does work, pretty fast and doesn't seem to wait, about 5khz with a print on slow machine
 #if ($w % 1000) {print (" $w");}
 #$w++;

 # now here we will test which of the acquisition subroutines to call and call them in a loop while doing
 # the update() above in between to keep all the events moving.

 #-----------------------------------
 #   Final config/setup for a run
 #

 # 1 - Scope Channel/Signal details
 #        scopeState of 15 means a run is in progress
 if ( $scopeState != 15 ) {
   chkScopeState(); #  give chkScopeState() fsm control
 }

 # 2 - When scopeConfig done, select runType, then hit GO
 #         scopeState of 14 indicates scopeConfig complete
 #         sel of 0 or 4 indicates runType not yet set for this run
 if ($scopeState == 14 && ($sel == 0 || $sel == 4)) {
   selectRunType();  # give selectRunTYpe() fsm control
 }

 # 3 - save run details, pre-run comments, start the run
 #             goHit is set to 1 in selectRunType()'s GO button callback,
 #             and reset to 0 here
 if ( $goHit != 0 ) {
  $goHit = 0;  # reset

  # save the setup and pre-run comments
  saveRunDetail("prerun",$prerun->Contents);

  # If Sound Capture configured (SCstart > 0), start recording now
  $debug && print "SCstart:[$SCstart]\n";
  if ( $SCstart > 0 ) {
    $beginat = setDT();
    $pid = open(AUD, $SCcommand) or die "couldn't fork SoX $!\n";
    $debug && print "SC started\n";
    $SCstart = 0;
  } else {
    $debug && print "SC not started\n";
  }

  #  Official Starting point
  $runtimeseconds = $runtimeminutes = $runtimehours = 0;
  #$sstext = sprintf("STOP RUN [getGeigers:%d getAd:%d", $getGeigers, $getAD);
  $sstext = sprintf("STOP RUN");
 }

 # periodic scope settings are handled asynchronously in scopeGet() and screenGet()

 # this is the one-shot screen snapshot (button clicked during a run)
 if ( $getScreen > 0 ) {
  if ($siTid == 0 ) {
     $getScreen = 0;  # reset button, if no interval running
     screenGet();  # get scope screen,  once
  }

 }

 # The geigers & a/d info is handled here
 if ($getAD > 0 || $getGeigers > 0 ){
   my $gotit = "";                             # start clean
       $gotit = $port->lookfor;                # poll until data ready
       die "Aborted without match\n" unless (defined $gotit); # @@@ fixme -- I think you can set a timeout

   my $match = $port->matchclear;      #       so we dont keep reading the same line over and over

   if ( $gotit ne "" ) {
       $gotit =~ tr/\r//d;  # remove '\r' (of the '\r\n'), if present from data PIC dlog "lines"
       @linevals = split (/,/,$gotit);
       my $tct = @linevals;  # array in scalar context is number of elements

       my $ADct = 6;  # tokens in a Multi-geiger A/D line
       my $Gct = 8;   # tokens in a Mutli-geiger Geigers line
       if($debug){
           printf ("gotit: split parsed %d tokens\n", $tct);
           for ( my $n = 0; $n < $tct; $n++) {
               printf("linevals[%d]:%s:\n", $n,$linevals[$n]);
           }
       }

       # Validate Multi-geiger data as A/D or Geiger, set PICtimeBase,
       #   and initialize PICstart for sound capture use
       if ($tct == $ADct || $tct == $Gct) {
         $PICtimeBase = hex($linevals[1]);  # linevals[1] is PICtime as hex (here), (post-shift - [0]) conv to decimal below
#          printf("  PICtimeBase set to %d\n", $PICtimeBase);
         if ($PICstart < 0 ){
           $PICstart = $PICtimeBase;  # init PICstart value when we get a valid PICtime
         }
       }
       my $lv0 = "";
       if ($getAD > 0 && $tct == $ADct && $linevals[0] =~ /ADres/) {
           shift (@linevals); #remove (Multigeiger) data type identifier at front
           @linevals = map {hex();} @linevals; # change remaining array from hex to decimal

           push @linevals, $SetupID;  # put SetupID as last element (same as "lashup" id)
           unshift @linevals, $runID;  # put runID as 1st element in linevals

           # Now,linevals:runID,PICtime,Kv,  mA,  mbar,noise,LID
           #                [0]   [1]   [2]  [3]  [4]  [5]
            if ($sel == 3) {  # run in progress, display A/D data & save in DB
               printf("AD:\t%5.2f kv %4.2f ma %d %d\n",
                  $linevals[2]*$KvFactor,
                  $linevals[3]*$mAfactor,
                  $linevals[4]*$mbarFactor,
                  $linevals[5]*$noiseFactor
                  );

               $adsh->execute (@linevals)
                  or die "AD insert failed:$DBI::errstr";  # do or die
               $cntAD++;
           }  # ditch data if run has not officially started

       } # end if ADRes
       else {
           if ($getGeigers > 0 && $tct == $Gct && $linevals[0] =~ /intExp/) {
               shift (@linevals); #remove data type identifier at front, we know this
               $linevals[0] = hex($linevals[0]);   # make pictime into decimal number
               push (@linevals, $SetupID); # put SetupID as last element of linevals (same as "lashup")
                       unshift (@linevals, $runID);
               if ($sel == 3 ) {  # display geiger data & save in DB when run in progress
               printf("\tGeiger:\t%d %d %d %d\n",
                      $linevals[2],$linevals[3],$linevals[4],$linevals[5]);

                   $csh->execute (@linevals) or die "Geiger insert failed:$DBI::errstr";
                   $cntGeigers++;
               }  # ditch if run not officially started
           } 
       } # end else for ADRes
       #print ("gotit: $gotit\n"); # testing
       if ( $lv0 ne "" && $lv0 ne "intExp" && $lv0 !~ "ADres"  ) {
           printf ("PIC input fmt unknown/gotit = [%s]\n", $gotit);
       } 
    } #end if gotit
  }  # end if getAD...
 }  # end while(MWrunning)
}  # end sub rt_loop

#///////////////////////

# !!! start of main program

#init things
$MWrunning = 0;
$sstext = "Start";
# no longer needed, placeholder till the func worked $lstatext = "Time goes here";
$rstatext = "Other stuff here -- run progress time, say";


# create main window
$mw = MainWindow->new;
$mw->title("Fusor Data Acquisition");

# to grab the real bottom, then have things side by side there, this has to go first.
$bframe = $mw->Frame()->pack(-side => 'bottom', -expand => 1, -fill => 'x');
$tframe = $mw->Frame()->pack(-side => 'top', -fill=>'x');
# these contain the main user input stuff
$rframe = $mw->Frame(-label=>'----- Run Information text: -----',-relief=>'groove',-borderwidth=>5,)->pack
(-side => 'right');
$lframe = $mw->Frame(-label=>'Data to acquire',-relief=>'groove',-borderwidth=>5)->pack
(-expand=>1,-fill=> 'both',-side => 'left', -anchor=>'nw');

# widgets for the frames to contain

# top status frame
$lstat = $tframe->Label(-textvariable => \$lstatext,-relief=>'groove',-width =>35,-anchor=>'w')
       ->pack(-side => 'left');
$rstat = $tframe->Label(-textvariable => \$rstatext,-relief=>'groove',-width =>50,-anchor=>'w')
       ->pack(-side => 'right', -fill=>'x',-expand=>1);

# right hand comment entry(s) frame and widgets in it

#the Text type means we have to do some more managing - it doesn't have a -textvariable
#
# @@@ the setup & post-run comments need to be collected and saved in the runDetail table
# @@@ the setup comments should be saved when the start button is hit, the post-run comments
#     should be saved after the STOP button is hit (put up a "Save PostRun Comments" button,
#     or on GUI termination.
#
$rframe->Label(-text=>'Pre-run/Setup comments:')->pack(-side=>'top');
$prerun = $rframe->Text(-background=>'white',-width=>50,-wrap=>'word',
height=>15)->pack(-side=>'top');
$rframe->Label(-text=>'Post-run comments:')->pack(-side=>'top');
$postrun = $rframe->Text(-background=>'white',-width=>50,-wrap=>'word',
height=>15)->pack(-side=>'top');


#another way for the above, but text stuck on one line...
#$prerun = $rframe->LabEntry(-textvariable=>\$preruntext,-label=>'Run setup information:',-background=>'white',-width=>40,
#height=>10,-labelPack=>[-side=>'top'])->pack(-side=>'top');
#$postrun = $rframe->LabEntry(-textvariable=>\$postruntext,-label=>'Post-run comments:',-background=>'white',-width=>40,
#-labelPack=>[-side=>'top'])->pack(-side=>'top');

# frame and widgets for MultiGeiger hardware
$mgframe = $lframe->Frame(-label =>' --- MultiGeiger --- ',-relief=>'groove',-borderwidth=>2, -labelPack=>[-side=>'top',
-anchor=>'w'])->pack(-side => 'top', -fill=>'x');

$getGeigers = 1;  # so not uninitialized
$getGeigersck = $mgframe-> Checkbutton(-text=>'Get Geigers?',-variable =>\$getGeigers)->pack(-side=>'top', -anchor=>'w');

$getAD = 1;
$getADck = $mgframe-> Checkbutton(-text=>'Get A/D values?',-variable =>\$getAD)->pack(-side=>'top', -anchor=>'w');

#frame and widgets for soundcard

$scframe = $lframe->Frame(-label =>' --- Soundcard --- ',-relief=>'groove',-borderwidth=>2,
       -labelPack=>[-side=>'top',-anchor=>'w'])->pack(-side => 'top', -fill=>'x');
$getSC = 0;  # default is no record
$getSCck = $scframe-> Checkbutton(-text=>'Record during run?',-variable =>\$getSC)->pack(-side=>'top', -anchor=>'w');
# radio buttons for number of channels & sample rate
            $getSCtype = 1; # set default chan/rate to 1chan/8k
            $scframe-> Radiobutton(-text=>"1chan/8K",-variable=>\$getSCtype,-value=>1)->pack;
            $scframe-> Radiobutton(-text=>"2chan/8K",-variable=>\$getSCtype,-value=>2)->pack;
            $scframe-> Radiobutton(-text=>"1chan/44.1K",-variable=>\$getSCtype,-value=>3)->pack;
            $scframe-> Radiobutton(-text=>"2chan/44.1K",-variable=>\$getSCtype,-value=>4)->pack;

#frame and widgets for DSO

$getScope = 0;  # default is no scope settings
$scopeframe = $lframe->Frame(-label =>' --- Scope --- ',-relief=>'groove',-borderwidth=>2,
       -labelPack=>[-side=>'top',-anchor=>'w'])->pack(-side => 'top', -fill=>'x');
$getScopeck = $scopeframe-> Checkbutton(-text=>'Get Scope waveforms?',-variable =>\$getScope)->pack(-side=>'top', -anchor=>'w');

# @@@ TBD whether to grab screen image manually or periodically - define interval here as well?
#
$getScreen = 0;  # default is no screen image
$screenframe = $lframe->Frame(-label =>' --- Scope --- ',-relief=>'groove',-borderwidth=>2,
       -labelPack=>[-side=>'top',-anchor=>'w'])->pack(-side => 'top', -fill=>'x');
$getScreenck = $screenframe-> Checkbutton(-text=>'Get Scope Screen?',-variable =>\$getScreen)->pack(-side=>'top', -anchor=>'w');

#frame and widgets for mass spec

$specframe = $lframe->Frame(-label =>' --- Mass Spectrometer --- ',-relief=>'groove',-borderwidth=>2,
       -labelPack=>[-side=>'top',-anchor=>'w'])->pack(-side => 'top', -fill=>'x');
$getSpecspec = 0; # so not uninitialized
$getSpecspecck = $specframe-> Checkbutton(-text=>'Mass spectrum?',-variable =>\$getSpecspec)->pack(-side=>'top', -anchor=>'w');
$getSpecAD = 0; # so not uninitialized
$getSpecADck = $specframe-> Checkbutton(-text=>'A/D readings?',-variable =>\$getSpecAD)->pack(-side=>'top', -anchor=>'w');

# get the database here, and report in some nice way if we can't get a connection.  Init the i/o for
# the other stuff in the start/stop routine above on start.

#*************************************************************************************
# final setup and run Mainloop until it passes control to realtime stuff (which should pass it back on quit)

#run start-stop button
$ssbutton = $bframe->Button(-textvariable => \$sstext, -cursor => 'exchange',
                       -command => \&startstop)->pack(-expand => 1, -fill=>'x', -side => 'left');

# begin status update events
$timerid = $mw->repeat(1000, \&on_tick); # every second, works like a dream

print ("program name:$0\n");

MainLoop;
# Mainloop will draw the windows and process events until "start", which will pass off
#  Tk event processing to another routine (rt_loop) for realtime data aq

# savePostRun(); # save on termination won't work here because Tk objects are gone...

#print "Manloop exit\n";

