#!/usr/bin/env python

# Featherweight Altimeters' Parrot I/O program -- Jim Yehle -- Nov 11, 2008

#todo
# Print
# overlay: multiple keys: add flight name, cycle colors; max time scaling
# if no plot title, use file basename, not full name
# command line opt -c: specify config file dir/name .. debug
# Mode which doesn't write post-processed file
#  can be activated using plot-only button or cl-option
# Plot checkboxes: tight? mark events?
# Help: vertical line events
# Help: flight label, parrot2: plug in, push button first
# Help: command line stuff
# Help: double-click .par
# Help: grid, overlay, integ accel, temp-comp
# Real-time DAQ reporting: interpret, and fill static boxes; don't scroll
#  This will make transition to binary transfer easier
#  Then Linux may get fixed also
# registry lookup: \HKEY_LOCAL_MACHINE\System\CurrentControlSet\Enum\USB
#  may not be helpful.. try plug/unplug USB, diff com-ports lists
# Add another no-com-port state: not configured, and user wants it that way.
#  Command line option to omit comm
# Changing COM ports doesn't work (could I delete g_serport?)
# After exit, dump goes to console briefly, creates error log (parrot.exe)
# Linux version crashes in gtk
# Program parrot eeprom (always times out on serial-port write)
# Sometimes, a realtime dump line gets captured at the start of captured file
# validate reprogramming string
# Read/write profile-programs to file (i.e. custom profiles)
# ?If it's not an actual raw-flight input, list-index-out-of-range err
# Unicode incompatible with matplotlib menubar print, .png output

VERSION = 1.4

import os, threading, getopt
import pickle # to read/write config file
import time # sleep
from collections import deque
import matplotlib
matplotlib.use('WXAgg')
#from matplotlib.backends.backend_wx import Toolbar,FigureCanvasWx
from matplotlib.backends.backend_wx import FigureManager
from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg as FigureCanvas
from matplotlib.backends.backend_wx import NavigationToolbar2Wx
from matplotlib.figure import Figure
from matplotlib.axes import Subplot
import wx
from wx.html import HtmlEasyPrinting
import wx.lib.dialogs
import serial # Chris Liechti's pySerial package

from parrot_post import *

DOWNLOAD_CHAR = 'd' # Commands Parrot to start dumping flight data
STANDBY_CHAR = 'w'
ERASE_CHAR = 'E'
PROGRAM_CHAR = 'P'
parrot_prog = [ ('Default as shipped' ,            "vhrJvZ@L@rNT"),
                ('2nd Stage at Burnout' ,          "vhrJ^n@L@rNT"),
                ('2nd Stage with Vertical Check' , "vhrJlb@L@rNT"),
                ('Timer, Output 3, 6 sec' ,   "vhrJvnBJ@rNT"),
                ('Timer, Output 3, 8 sec' ,   "vhrJvnBd@rNT"),
                ('Timer, Output 3, 10 sec' ,  "vhrJvnB|@rNT"), # Template wrong .. this sets all 3 continuous outputs
                ('Timer, Output 3, 10 sec, continuous 3' ,  "vhrJv~B|@rNT"),
                ('Timer, Output 3, 12 sec' ,  "vhrJvnDV@rNT"),
                ('Timer, Output 3, 14 sec' ,  "vhrJvnDn@rNT"),
                ('Output 3 Disabled' ,             "vhrJfF@L@rNT")
              ]
# GUI widget IDs
EXIT_ID,HELP_DONE_ID,OPEN_RAWFILE_ID,MENU_GRAPH_ID,MENU_EXIT_ID,MENU_CONF1_ID,\
 MENU_CONF2_ID,MENU_CONF3_ID,CONF_DONE_ID,CONF_CANCEL_ID,CAPT_DONE_ID,\
 CAPT_ERASE_ID,CAPT_CALC_ID,CAPT_CAPT_ID,CAPT_STOP_ID,MENU_CAPT_ID,\
 MENU_CAPT0_ID,MENU_ABOUT_ID,MENU_HELP_ID,MENU_PROG_ID,CONF_RAW_DIR_ID,\
 TEXT3_PROC_ID = range(101,123)

def help_text():
  return """Capturing

Retrieving flight data from a Parrot altimeter starts by capturing
the readings into a raw-flight file.  When you select Capture from
the menu, a dialog box will appear.  Real-time data being emitted
from the Parrot will appear in the main text box of this dialog box.
You will need to specify two things before downloading: the flight
index, and a name for the raw-flight file.  After that, clicking
the Capture button will start transferring flight data from the
Parrot into the file.  It takes a while.  When the data end, the
program closes the file, and reverts the Parrot to Wait mode,
resuming the once-per-second data echo.  The Capture processor
cuts off output when the data end, for example, when you turn off
the Parrot after a flight.  If you have more flights stored, you
can download them now by changing the flight index and file names
and clicking Capture again.

(I have to warn you about one thing here.  As the Capture process
is going on, everything gets echoed to the screen.  If your
computer isn't particularly fast, some of the downloaded data
can get dropped, and never make it into the file.  If that happens,
try setting Verbosity to 0 in the Configuration.  It can be 
disconcerting when you can't see data from a (very long) download.
You can see the blue LED on the Parrot go out, so perhaps that
can restore your confidence.)

The Capture screen is also where you can erase a flight.  I prefer
to make certain that the capture worked, by plotting the results,
before wiping out the flight.

The next step is processing the captured data.  You can exit the
Capture dialog by clicking Exit, but if you click Process, the
program will immediately start processing the last downloaded
raw flight file, saving you the step of opening the raw file
from the main screen.

The main screen is where the post-processor reads a raw flight file,
calculates and plots the flight.  You can also output a text file of
the processed data, which can be either a simple tabular data dump
of the calculated values, or a file which can be pasted into the
Parrot Analysis Template using spreadsheet software for further
processing.  The software detects the end of flight ("touchdown")
and cuts off further data, unless you configure it to continue.


Configuration

After you have installed the FTDI device driver, figure out which
virtual com port it has assigned.  On Windows, you can run System
(from Control Panel), then click the Hardware tab, click Device
Manager, and expand Ports (COM & LPT).  Then open the Configure
Comm dialog and set the right port.  You'll need to restart the 
program before the change takes effect.  The exception to this
rule is when you run the program for the first time, and the
Configure Comm dialog pops up first thing--you choose a com port,
and it starts up.

This program works with version 1 Parrots, but ver. 2 is the default
model.  In ver-1 mode, launch must be detected by the software, and
you can set the G threshold used.  Remember, a threshold of 3.5G means
2.5G vertical acceleration plus 1 G of gravitational force detected by
the transducer.  For version 2 Parrots, the Flight-Event Register (FER)
can be inspected for launch detection.  This tends to be a "safe"
launch detection algorithm, and as such, doesn't declare launch until
it's totally obvious.  This may skew the pre-launch altitude zeroing,
since a few readings after launch are included in the average.  You
will find a more accurate assessment of launch by using the G-threshold
method.

Select the file extensions you prefer for raw and processed files
using the associated text fields for them.  For example, use ".csv"
if you choose Comma as the output-field separator.


Processing and Plotting

A single plot is drawn with zero-time set to the detected launch time.
The initial behavior of the plot window is to fit all data.  You can
use the plot toolbar to zoom, pan, return to original scaling, or save
the plot to a graphical file.  (Don't try the .png extension, since
there's a bug in the library which causes it to fail.)

Under the Processing menu, you also have the option to download the
Parrot's profile, a.k.a. flight-index zero.  The resulting file can
be loaded into the Excel template to interpret and create new profiles.
"""

class HelpDialog(wx.Dialog): # -----------------------------------------------

  def __init__(self,parent,id,title):

    global behavior # InOut

    wx.Dialog.__init__(self, None, -1, title)
    panel = wx.Panel(self,-1)
    txt = wx.TextCtrl(panel, size=(500,500),
                      style=wx.TE_MULTILINE|wx.TE_READONLY)

    txt.SetValue(help_text())
    vsizer = wx.BoxSizer(wx.VERTICAL)
    vsizer.Add( panel)
    done_button = wx.Button( self, HELP_DONE_ID, "Ok")
    wx.EVT_BUTTON( self, HELP_DONE_ID, self.OnExit)
    vsizer.Add(done_button, flag=wx.RIGHT)

    self.SetSizerAndFit(vsizer)

  def OnExit( self, e):
    e.Skip()
    self.Bind(wx.EVT_CLOSE, None)    
    self.Close(True)


def os_comport_name(port): # --------------------------------------------------

  """Operating-system specific name for a serial port,
     given a zero-based port number.
     e.g. os_comport_name(0) gives "COM1" for windows"""

  if os.name == 'nt':
    return 'COM%d' % (port+1)
  elif os.name == 'posix':
    return 'ttyS%01d' % port

  raise "Sorry, no implementation for your platform (%s) available."\
        % os.name
  return None
 

def os_comport_num(name): # ---------------------------------------------------

  """Operating-system specific number for a named serial port.
     e.g. os_comport_num("COM1") gives 0 for windows"""

  if os.name == 'nt':
    return int(name[3:]) - 1 # e.g. "COM3"
  elif os.name == 'posix':
    return int(name[4:]) # e.g. 'ttyUSB0' which I soft-linked to /dev/ttyS4

  raise "Sorry, no implementation for your platform (%s) available."\
        % os.name
  return None
 

def extant_com_ports(verbose): # ----------------------------------------------

  """Build a list of com ports which exist"""

  ports = []
  baudrate = 115200
  for port in range(0,255):
    try:
      serport = serial.Serial(port, baudrate, rtscts=0, xonxoff=0)
      del serport
      if verbose>1: log("com port %s exists .. os.name = %s" % (port,os.name))
      portname = os_comport_name(port)
      if portname != None:
        ports.append(portname)
        if verbose>1: log(" appended com port %s" % portname)
    except serial.serialutil.SerialException:
      if verbose>2: log("Could not open port %s" % port)
  if verbose>1: log("List of extant com ports: %s" % repr(ports))
  return ports


class Printer(HtmlEasyPrinting): # --------------------------------------------
  def __init__(self):
    HtmlEasyPrinting.__init__(self)

  def Print(self, html, hdr=None):
    if hdr:  self.SetHeader(hdr)
    self.PrintText(html,doc_name)

  def Preview(self, html, hdr=None):
    if hdr:  self.SetHeader(hdr)
    HtmlEasyPrinting.PreviewText(self, html)


class CommConfDialog(wx.Dialog): # --------------------------------------------

  def __init__(self,parent,id,title):

    #####################################################################
    #                                                                   #
    # .- Comm Configuration ----.                                       #
    # |                         |                                       #
    # |  Parrot Version  ______ | lbl1, self.txt1        |gsizer|       #
    # |  COM-port        ______ | comlbl,self.comport_chx|      |       #
    # |                         | gap_spacer                    |vsizer #
    # |   Cancel        Ok      | cancel__btn,done_btn|btnsizer |       #
    # `-------------------------'                                       #
    #                                                                   #
    #####################################################################

    global behavior # InOut
    global cfg_fname, baudrate # In

    wx.Dialog.__init__(self, None, -1, title)
    txt_ht = 17
    gap_spacer = wx.Size(4,2) # for gaps and borders

    lbl1 = wx.StaticText(self, label="Parrot Version")
    self.parvers = ['1', '2', '2.1']
    self.parver_chx = wx.Choice( self, choices=self.parvers)
    try:
      parver = str(behavior['parrot_version'])
      i = self.parvers.index(parver) # is current behavior[] value valid?
      self.parver_chx.SetSelection(i)
    except ValueError:
      self.parver_chx.SetSelection(1) #default version 2

    self.comports = extant_com_ports(behavior['verbose'])
    if os_comport_name(behavior['com_port']) not in self.comports:
      # if can't open configured-com-port, gotta add it to list ourselves
      if behavior['com_port'] != -1: # -1 means none
        self.comports.append(os_comport_name(behavior['com_port']))
        self.comports.sort()
    comlbl = wx.StaticText(self, label="COM port:")
    self.comport_chx = wx.Choice( self, choices=self.comports)
    zportnum = int(behavior['com_port'])
    try:
      i = self.comports.index(os_comport_name(zportnum))
      self.comport_chx.SetSelection(i)
    except ValueError: pass # current behavior-specified com port doesn't exist

    gsizer = wx.FlexGridSizer(cols=4,hgap=5,vgap=10)
    gsizer.AddMany( [gap_spacer, (0,0),  (0,0),            (0,0),
                     gap_spacer, lbl1,   self.parver_chx,  gap_spacer,
                     gap_spacer, comlbl, self.comport_chx, gap_spacer,
                     gap_spacer, (0,0),  (0,0),            (0,0),] )

    cancel_btn = wx.Button( self, CONF_CANCEL_ID, "Cancel")
    wx.EVT_BUTTON( self, CONF_CANCEL_ID, self.OnExit)
    self.Bind( wx.EVT_CLOSE, self.OnExit)

    done_btn = wx.Button( self, CONF_DONE_ID, "Ok")
    wx.EVT_BUTTON( self, CONF_DONE_ID, self.OnOk)

    btnsizer = wx.BoxSizer(wx.HORIZONTAL) # for buttons
    btnsizer.Add(gap_spacer)
    btnsizer.Add(cancel_btn, flag=wx.LEFT)
    btnsizer.Add(gap_spacer)
    btnsizer.Add(done_btn, flag=wx.RIGHT)
    btnsizer.Add(gap_spacer)

    vsizer = wx.BoxSizer(wx.VERTICAL)
    vsizer.Add(gap_spacer)
    vsizer.Add(gsizer)
    vsizer.Add(gap_spacer)
    vsizer.Add(btnsizer)
    vsizer.Add(gap_spacer)

    self.SetSizerAndFit( vsizer)

  def OnOk( self, e):
    global behavior, thr, g_serport # InOut
    try:
      parver_str = self.parvers[self.parver_chx.GetSelection()]
      behavior['parrot_version'] = float( parver_str)
      comport_name = self.comports[int(self.comport_chx.GetSelection())]
      new_com_port = os_comport_num(comport_name)
    except ValueError:
      display("Bad numeric value")
    if new_com_port != behavior['com_port']:
      log("Changing com-port from %d to %d" \
          % (behavior['com_port'],new_com_port) )
      try:
        serport = serial.Serial(new_com_port, baudrate, rtscts=0, xonxoff=0)
      except:
        wx.MessageBox( "Could not open port " + os_comport_name(new_com_port),
                       caption="Comm Error", style=wx.OK|wx.ICON_ERROR)
        serport = None
      if g_serport == None:  startup_needed = thr
      else:                  startup_needed = None
      if serport != None:
        g_serport = serport
        if startup_needed != None:
          startup_needed.start()
          log("Starting reader-thread")
      behavior['com_port'] = new_com_port
    cfg_file = open( cfg_fname, 'w')
    pickle.dump(behavior,cfg_file)
    cfg_file.close()
    self.OnExit(e,wx.ID_OK)

  def OnExit( self, e, ret=wx.ID_CANCEL):
    self.EndModal(ret)
    #e.Skip()
    #self.Bind(wx.EVT_CLOSE, None)    
    #self.Close(True)


class ProcConfDialog(wx.Dialog): # --------------------------------------------

  def __init__(self,parent,id,title):


    ########################################################################
    #                                                                      #
    # .- Processing Configuration ----.                                    #
    # |                               | gap_spacer                |        #
    # |  _  Plot Descent Rate         | self.plot_desc_ck         |cksizer #
    # |  _  Plot Acceleration         | self.plot_accel_ck        | (panel)#
    # |  _  Plot Temperature          | self.plot_temp_ck         |        #
    # |  _  Plot Temp-corrected Alt.  | self.plot_tcomp_alt_ck    |        #
    # |  _  Plot Accel-derived Alt.   | self.plot_integ_accel_ck  |        #
    # |  _  Add Grids to Plots        | self.plot_grids_ck        |        #
    # |  _  Overlay Plots             | self.overlay_plots_ck     |        #
    # |  _  End processing at landfall| self.proc_cutoff_downed_ck|        #
    # |                               | gap_spacer                |        #
    # | .- Launch Detect Method ---.  |                                    #
    # | | O  G-Threshold           |  | ldm_rb | rbsizer                   #
    # | | O  Flight Event          |  |                                    #
    # | `--------------------------'  |                                    #
    # |                               |                                    #
    # |  Launch G threshold ________  | lbl2, self.txt2|                   #
    # |  Verbosity          ________  | lbl3, self.txt3|gsizer             #
    # |                               |                                    #
    # |   Cancel        Ok            | cancel_btn,done_btn|btnsizer       #
    # `-------------------------------'                                    #
    # Note: all sizers are wrapped by one overall vertical sizer, vsizer   #
    ########################################################################

    global behavior # InOut
    global cfg_fname # In

    wx.Dialog.__init__(self, None, -1, title)
    txt_ht = 17
    gap_spacer = wx.Size(4,2) # for gaps and borders

    panel = wx.Panel(self,-1)
    self.plot_desc_ck = wx.CheckBox(panel,-1,"Plot Descent Rate")
    self.plot_desc_ck.SetValue( behavior['plot_desc'] )
    self.plot_accel_ck = wx.CheckBox(panel,-1,"Plot Acceleration")
    self.plot_accel_ck.SetValue( behavior['plot_accel'] )
    self.plot_temp_ck = wx.CheckBox(panel,-1,"Plot Temperature")
    self.plot_temp_ck.SetValue( behavior['plot_temperature'] )
    self.proc_cutoff_downed_ck = wx.CheckBox(panel,-1,
                                             "End processing at landfall")
    self.proc_cutoff_downed_ck.SetValue( behavior['proc_cutoff_downed'] )
    self.plot_integ_accel_ck = wx.CheckBox(panel,-1,
                                           "Plot Accel-derived Altitude")
    self.plot_integ_accel_ck.SetValue( behavior['plot_integ_accel'] )
    self.plot_tcomp_alt_ck = wx.CheckBox(panel,-1, "Plot Temp-corrected Alt.")
    self.plot_tcomp_alt_ck.SetValue( behavior['plot_tempcomp_alt'] )
    self.plot_grids_ck = wx.CheckBox(panel,-1,"Add Grids to Plots")
    self.plot_grids_ck.SetValue( behavior['plot_grids'] )
    self.overlay_plots_ck = wx.CheckBox(panel,-1,"Overlay Plots")
    self.overlay_plots_ck.SetValue( behavior['overlay_plots'] )
    # Don't set behavior['xxx'] in handler, in case Cancel
    #self.Bind(wx.EVT_CHECKBOX, self.OnCk3, self.plot_desc_ck)
    cksizer = wx.FlexGridSizer(cols=3,hgap=5,vgap=10)
    cksizer.AddMany( [gap_spacer, (0,0),                      (0,0),
                      gap_spacer, self.plot_desc_ck,          gap_spacer,
                      gap_spacer, self.plot_accel_ck,         gap_spacer,
                      gap_spacer, self.plot_temp_ck,          gap_spacer,
                      gap_spacer, self.plot_tcomp_alt_ck,     gap_spacer,
                      gap_spacer, self.plot_integ_accel_ck,   gap_spacer,
                      gap_spacer, self.plot_grids_ck,         gap_spacer,
                      gap_spacer, self.overlay_plots_ck,      gap_spacer,
                      gap_spacer, self.proc_cutoff_downed_ck, gap_spacer,
                      gap_spacer, (0,0),                      (0,0)      ] )
    panel.SetSizerAndFit( cksizer)

    rbsizer = wx.FlexGridSizer(cols=3,hgap=10,vgap=10) # for radio box
    ldm_choices = [ 'G-Threshold', 'Flight Event']
    self.ldm_rb = wx.RadioBox(self,choices=ldm_choices, style=wx.RA_VERTICAL,
                              label='Launch Detect Method')
    if behavior['launch_detect_method'] == 'FER':
      self.ldm_rb.SetSelection(1)
    rbsizer.AddMany( [gap_spacer, self.ldm_rb, (0,0)] )

    lbl2 = wx.StaticText(self, label="Launch G threshold")
    self.txt2 = wx.TextCtrl(self, size=(50,txt_ht))
    self.txt2.SetValue(str(behavior['G_THRESH']))
    lbl3 = wx.StaticText(self, label="Verbosity")
    self.txt3 = wx.TextCtrl(self, size=(50,txt_ht))
    self.txt3.SetValue(str(behavior['verbose']))

    gsizer = wx.FlexGridSizer(cols=4,hgap=5,vgap=8)
    gsizer.AddMany( [gap_spacer, (0,0),  (0,0),     (0,0),
                     gap_spacer, lbl2,   self.txt2, gap_spacer,
                     gap_spacer, lbl3,   self.txt3, gap_spacer] )

    cancel_btn = wx.Button( self, CONF_CANCEL_ID, "Cancel")
    wx.EVT_BUTTON( self, CONF_CANCEL_ID, self.OnExit)
    self.Bind( wx.EVT_CLOSE, self.OnExit)

    done_btn = wx.Button( self, CONF_DONE_ID, "Ok")
    wx.EVT_BUTTON( self, CONF_DONE_ID, self.OnOk)

    btnsizer = wx.BoxSizer(wx.HORIZONTAL) # for buttons
    btnsizer.Add(gap_spacer)
    btnsizer.Add(cancel_btn, flag=wx.LEFT)
    btnsizer.Add(gap_spacer)
    btnsizer.Add(done_btn, flag=wx.RIGHT)
    btnsizer.Add(gap_spacer)

    vsizer = wx.BoxSizer(wx.VERTICAL)
    vsizer.Add(gap_spacer)
    vsizer.Add(panel)
    vsizer.Add(gap_spacer)
    vsizer.Add(rbsizer)
    vsizer.Add(gsizer)
    vsizer.Add(gap_spacer)
    vsizer.Add(gap_spacer)
    vsizer.Add(gap_spacer)
    vsizer.Add(btnsizer)
    vsizer.Add(gap_spacer)

    self.SetSizerAndFit(vsizer)

  def OnOk( self, e):
    global behavior # InOut
    global g_verbose # Out
    try:
      behavior['G_THRESH'] = float( self.txt2.GetValue() )
      behavior['verbose'] = int( self.txt3.GetValue() )
    except ValueError:
      display("Bad numeric value")
    behavior['plot_desc'] = self.plot_desc_ck.GetValue()
    behavior['plot_accel'] = self.plot_accel_ck.GetValue()
    behavior['plot_temperature'] = self.plot_temp_ck.GetValue()
    behavior['plot_integ_accel'] = self.plot_integ_accel_ck.GetValue()
    behavior['plot_tempcomp_alt'] = self.plot_tcomp_alt_ck.GetValue()
    behavior['plot_grids'] = self.plot_grids_ck.GetValue()
    behavior['overlay_plots'] = self.overlay_plots_ck.GetValue()
    behavior['proc_cutoff_downed'] = self.proc_cutoff_downed_ck.GetValue()
    behavior['plot_integ_accel'] = self.plot_integ_accel_ck.GetValue()
    if self.ldm_rb.GetSelection() == 0: # 0 is G_THRESH
      behavior['launch_detect_method'] = 'G_THRESH'
    else:
      behavior['launch_detect_method'] = 'FER'
    cfg_file = open( cfg_fname, 'w')
    pickle.dump(behavior,cfg_file)
    cfg_file.close()
    g_verbose = behavior['verbose']
    self.OnExit(e)

  def OnExit( self, e):
    e.Skip()
    self.Bind(wx.EVT_CLOSE, None)    
    self.Close(True)


class FileConfDialog(wx.Dialog): # --------------------------------------------

  def __init__(self,parent,id,title):

    ##########################################################################
    #                                                                        #
    # .- Files Configuration -------------.                                  #
    # |                                   |                                  #
    # |       Raw file directory          | lbl1                     |       #
    # |   ____________________________    |                          |       #
    # |  |____________________________|   | self.txt6                |       #
    # |                                   |                          |       #
    # |            Browse                 | raw_dir_btn              |       #
    # |                                   |                          |       #
    # |   .- Output Field Separator -.    |                          |       #
    # |   | O  Comma                 |    | ofs_rb | rb_sizer        |       #
    # |   | O  Tab                   |    |                          |       #
    # |   `--------------------------'    |                          |vsizer #
    # |                                   |                          |       #
    # |  Raw flight extension   ________  | lbl4, self.txt4 |gsizer  |       #
    # |  Output file extension  ________  | lbl5, self.txt5 |        |       #
    # |                                   |                          |       #
    # |   Cancel             Ok           | cancel_,done_btn|btnsizer|       #
    # `-----------------------------------'                                  #
    ##########################################################################

    global behavior # InOut
    global cfg_fname # In

    wx.Dialog.__init__(self, None, -1, title)
    txt_ht = 17
    hborder = 4
    gap_spacer = wx.Size(hborder,2) # for gaps and borders

    lbl1 = wx.StaticText(self, label="Raw file directory")
    self.txt6 = wx.TextCtrl(self,size=(180,txt_ht))
    self.txt6.SetValue(behavior['raw_file_dir'])
    raw_dir_btn = wx.Button( self, CONF_RAW_DIR_ID, "Browse")
    wx.EVT_BUTTON( self, CONF_RAW_DIR_ID, self.OnRawDir)
    rsizer = wx.GridBagSizer(hgap=5,vgap=5) # cols=3
    rsizer.Add( lbl1, pos=(0,2), border=wx.TOP)
    # txt6 needs to span 3 columns
    rsizer.Add( gap_spacer, pos=(1,0) )
    rsizer.Add( self.txt6, pos=(1,1), span=(1,3), border=wx.LEFT)
    rsizer.Add( raw_dir_btn, pos=(2,2) )

    lbl4 = wx.StaticText(self, label="Raw flight extension")
    self.txt4 = wx.TextCtrl(self, size=(50,txt_ht))
    self.txt4.SetValue(behavior['raw_file_ext'])
    lbl5 = wx.StaticText(self, label="Output file extension")
    self.txt5 = wx.TextCtrl(self, size=(50,txt_ht))
    self.txt5.SetValue(behavior['post_file_ext'])

    gsizer = wx.FlexGridSizer(cols=4,hgap=5,vgap=10)
    gsizer.AddMany( [gap_spacer, (0,0), (0,0),     (0,0),
                     gap_spacer, lbl4,  self.txt4, gap_spacer,
                     gap_spacer, lbl5,  self.txt5, gap_spacer] )

    rbsizer = wx.FlexGridSizer(cols=3,hgap=10,vgap=10) # for radio box
    ofs_choices = ['Comma','Tab']
    self.ofs_rb = wx.RadioBox(self,choices=ofs_choices, style=wx.RA_VERTICAL,
                              label='Output Field Separator')
    if behavior['OFS'] == '\t':
      self.ofs_rb.SetSelection(1) # default initial choice is 0 (comma)
    rbsizer.AddMany( [gap_spacer, self.ofs_rb, (0,0),
                      gap_spacer, (0,0),       (0,0) ] )

    cancel_btn = wx.Button( self, CONF_CANCEL_ID, "Cancel")
    wx.EVT_BUTTON( self, CONF_CANCEL_ID, self.OnExit)
    self.Bind( wx.EVT_CLOSE, self.OnExit)

    done_btn = wx.Button( self, CONF_DONE_ID, "Ok")
    wx.EVT_BUTTON( self, CONF_DONE_ID, self.OnOk)

    btsizer = wx.BoxSizer(wx.HORIZONTAL) # for buttons
    btsizer.Add(gap_spacer)
    btsizer.Add(cancel_btn, flag=wx.LEFT)
    btsizer.Add(gap_spacer)
    btsizer.Add(done_btn, flag=wx.RIGHT)
    btsizer.Add(gap_spacer)

    vsizer = wx.BoxSizer(wx.VERTICAL)
    vsizer.Add(gap_spacer)
    vsizer.Add(rsizer)
    vsizer.Add(gap_spacer)
    vsizer.Add(gsizer)
    vsizer.Add(gap_spacer)
    vsizer.Add(rbsizer)
    vsizer.Add(gap_spacer)
    vsizer.Add(btsizer)
    vsizer.Add(gap_spacer)

    self.SetSizer(vsizer)
    self.Fit()

  def OnOk( self, e):
    global behavior # InOut
    behavior['raw_file_ext'] = self.txt4.GetValue()
    behavior['post_file_ext'] = self.txt5.GetValue()
    behavior['raw_file_dir'] = self.txt6.GetValue()
    if self.ofs_rb.GetSelection() == 0: # 0 is comma
      behavior['OFS'] = ','
    else:
      behavior['OFS'] = '\t'
    cfg_file = open( cfg_fname, 'w')
    pickle.dump(behavior,cfg_file)
    cfg_file.close()
    self.OnExit(e)

  def OnExit( self, e):
    e.Skip()
    self.Bind(wx.EVT_CLOSE, None)    
    self.Close(True)

  def OnRawDir(self,event):
    dlg = wx.DirDialog(None,message="Choose a directory",
                       defaultPath=behavior['raw_file_dir'],
                       style=wx.DD_NEW_DIR_BUTTON)
    if dlg.ShowModal() == wx.ID_OK:
      self.txt6.SetValue(dlg.GetPath())
    dlg.Destroy()


class CaptureDialog( wx.Dialog): # -------------------------------------------

  def __init__(self,parent,id,title):

    # .-Capture----------------------------------------.
    # |                                                |
    # | Flight-index ___  Raw-flight-filename ________ |self.flight_idx_chx,
    # |                                                | self.rawfname
    # | .--------------------------------------------. |
    # | |                                            | |
    # | |  Log Area                                  | |self.logtext|logsizer
    # | |                                            | |
    # | |                                            | |
    # | |                                            | |
    # | |                                            | |
    # | `--------------------------------------------' |
    # |                                                |
    # |   Progress  ||||||||||||||||||||||||||||||     |self.pg|panel|pgsizer
    # |                                                |
    # | Capture  Stop-Capture  Process  Erase     Exit |(5 buttons)|btsizer
    # `------------------------------------------------'

    global behavior # In
    global global_gui, g_downloading, g_rawfname # Out
    global g_download_progress_total # for progress-bar

    wx.Dialog.__init__(self, None, -1, title)
    self.Bind( wx.EVT_CLOSE, self.OnOk)

    g_downloading = False
    g_rawfname = None
    self.verbose = behavior['verbose']
    self.raw_file_dir = behavior['raw_file_dir']

    txt_ht = 17
    gap_spacer = wx.Size(4,2) # for gaps and borders
    med_hgap_spacer = wx.Size(8,2)
    big_hgap_spacer = wx.Size(120-35,2)

    # Top row of controls .. Flight-index, Raw-flight filename
    topsizer = wx.BoxSizer(wx.HORIZONTAL)
    topsizer.Add( gap_spacer, 0, wx.ALL, 2)
    panel = wx.Panel(self, -1)
    flight_indices = [ '1', '2', '3', '4', '5' ]
    wx.StaticText(panel, -1, "Flight index", (10,3) )
    self.flight_idx_chx = wx.Choice( panel, -1, (72,0), choices=flight_indices)
    self.Bind( wx.EVT_CHOICE, self.OnEditFields, self.flight_idx_chx)
    topsizer.Add( panel, 0, wx.ALL, 2)
    topsizer.Add( gap_spacer, 0, wx.ALL, 2)
    panel = wx.Panel(self, -1)
    wx.StaticText(panel, label="Raw flight filename", pos=(10,3) )
    self.rawfname = wx.TextCtrl(panel, size=(120,txt_ht), pos=(110,0) )
    self.Bind( wx.EVT_TEXT, self.OnEditFields, self.rawfname)
    topsizer.Add( panel, 0, wx.ALL, 2)
    topsizer.Add( gap_spacer, 0, wx.ALL, 2)

    # Log-window
    self.logtext = wx.TextCtrl(self, style=wx.TE_MULTILINE, size=(480,220))
    self.logtext.SetEditable(False)
    point_size = 8
    # Use a monospaced (fixed-char-width) font to maintain columnar alignment
    font = wx.Font( point_size, wx.TELETYPE, wx.NORMAL, wx.NORMAL)
    self.logtext.SetFont(font)

    logsizer = wx.BoxSizer(wx.HORIZONTAL)
    logsizer.Add( gap_spacer, 0, wx.ALL, 2)
    logsizer.Add( self.logtext, 8, wx.LEFT|wx.TOP|wx.GROW)
    logsizer.Add( gap_spacer, 0, wx.ALL, 1)

    # Progress-bar
    pgsizer = wx.BoxSizer(wx.HORIZONTAL)
    pgsizer.Add( gap_spacer, 0, wx.ALL, 2)
    panel = wx.Panel(self, -1)
    wx.StaticText(panel, label="Progress", pos=(10,5) )
    g_download_progress_total = 200 * 308 + 4 * 25 # total number of samples
    self.pg = wx.Gauge( panel, -1, g_download_progress_total, size=(400,20),
                        pos=(60,2) )
    #self.Bind(wx.EVT_IDLE,self.OnIdle)
    pgsizer.Add( panel, 0, wx.ALL, 2)
    pgsizer.Add( gap_spacer, 0, wx.ALL, 2)

    # Five buttons across bottom, in a horizontal box sizer
    btsizer = wx.BoxSizer(wx.HORIZONTAL)
    btsizer.Add( med_hgap_spacer)
    self.btcapt = wx.Button(self,CAPT_CAPT_ID,"Capture")
    btsizer.Add( self.btcapt, flag=wx.LEFT)
    self.btcapt.Disable() # until some text typed
    wx.EVT_BUTTON( self, CAPT_CAPT_ID, self.OnCapt)
    btsizer.Add( gap_spacer)
    self.btstop = wx.Button( self, CAPT_STOP_ID, "Stop Capture")
    btsizer.Add( self.btstop, flag=wx.LEFT)
    wx.EVT_BUTTON( self, CAPT_STOP_ID, self.OnStopCapt)
    self.btstop.Disable()
    btsizer.Add( gap_spacer)
    self.btproc = wx.Button(self,CAPT_CALC_ID,"Process")
    self.btproc.Disable()
    btsizer.Add( self.btproc, flag=wx.LEFT)
    wx.EVT_BUTTON( self, CAPT_CALC_ID, self.OnCalc)
    btsizer.Add( gap_spacer)
    btsizer.Add( wx.Button(self,CAPT_ERASE_ID,"Erase"), flag=wx.LEFT)
    wx.EVT_BUTTON( self, CAPT_ERASE_ID, self.OnErase)
    btsizer.Add( gap_spacer)
    # It would look better if the exit button were flush right
    btsizer.Add( big_hgap_spacer, 1)
    btsizer.Add( wx.Button(self,CAPT_DONE_ID,"Exit"), flag=wx.LEFT)
    wx.EVT_BUTTON( self, CAPT_DONE_ID, self.OnOk)
    btsizer.Add( gap_spacer)

    vsizer = wx.BoxSizer(wx.VERTICAL)
    vsizer.Add(gap_spacer)
    vsizer.Add(topsizer)
    vsizer.Add(gap_spacer)
    vsizer.Add(logsizer)
    vsizer.Add(gap_spacer)
    vsizer.Add(gap_spacer)
    vsizer.Add(pgsizer)
    vsizer.Add(gap_spacer)
    vsizer.Add(gap_spacer)
    vsizer.Add(btsizer)
    vsizer.Add(gap_spacer)
    vsizer.Add(gap_spacer)

    self.SetSizerAndFit(vsizer)

    self.gui_save = global_gui
    global_gui = self # when this dialog up, log to its log window

  # Yes, this looks like a kludge.  But putting EVT_IDLE handlers or
  # loop/sleep Update() handlers just doesn't work.  Python's thread
  # management leaves a bit to be desired.
  def update_progress(self):
    """Update progress-bar"""
    global g_download_progress, g_download_progress_total
    if g_downloading:
      if g_download_progress<g_download_progress_total:
        self.pg.SetValue(g_download_progress)
    else: # Done
      self.pg.SetValue(0)  
      self.btstop.Disable()
      self.btcapt.Enable()
      self.btproc.Enable()

  #def OnIdle( self, e):
  #  self.update_progress()

  def log(self,text):
    self.logtext.AppendText(text + '\n')
  def log_nolf(self,text):
    self.logtext.AppendText(text)

  def OnErase( self, e):
    """Erase one flight from altimeter memory"""
    self.flight_index = self.flight_idx_chx.GetSelection() + 1
    if self.flight_index < 1 or self.flight_index > 5:
      self.log("Bad flight index")
    if not prompt_yesno( "Really erase flight-index %d?" % self.flight_index):
      return
    if g_serport == None:
      wx.MessageBox( "Not communicating with Parrot.", caption="Comm Error",
                     style=wx.OK|wx.ICON_ERROR)
      return
    g_serport.write(str(self.flight_index)) # Tell parrot which flight we want
    g_serport.write(ERASE_CHAR)

  def OnCapt( self, e):
    global g_downloading, g_dumpfile # Out (set, but not clear)
    global g_serport # Write
    global g_rawfname # Out
    flindex = self.flight_idx_chx.GetSelection() + 1
    self.log("Flight index is %d" % flindex)
    if flindex < 1 or flindex > 5:
      wx.MessageBox( "Bad flight index", caption="Parameter Error",
                     style=wx.OK|wx.ICON_ERROR)
      return
    if g_serport == None:
      wx.MessageBox( "Not communicating with Parrot.", caption="Comm Error",
                     style=wx.OK|wx.ICON_ERROR)
      return
    g_serport.write(str(flindex)) # Tell parrot which flight we want
    # Open the output-dump file
    if self.rawfname.GetValue() == '':
      wx.MessageBox("Must supply raw capture filename.",
                    caption="Parameter Error",style=wx.OK|wx.ICON_EXCLAMATION)
      return
    fname = self.raw_file_dir + '/' + self.rawfname.GetValue()
    try: # Add raw-flight extension (typically ".par") if no extension supplied
      fname.index('.') # if no .ext, throw exception
    except ValueError:
      fname += behavior['raw_file_ext']
    while 1:
      # See if file exists; prompt for overwrite/rename
      try:
        tmpfile = open(fname,"r");  tmpfile.close()
        extant = True
      except IOError:
        extant = False
        break
      if prompt_yesno( "File %s exists.  Overwrite?" % fname) != True:
        return
      break # Could prompt for a different file name here, and continue
    try:
      dumpfile = open(fname,'w')
    except IOError:
      self.log("Can't open dump-file '%s'" % fname)
      return
    # Tell the serport-reader thread to start a download, via global variables
    g_serport.write(STANDBY_CHAR)
    g_dumpfile = dumpfile
    g_download_progress = 0 # for progress-bar
    g_downloading = True
    g_serport.write(DOWNLOAD_CHAR)
    g_rawfname = fname
    self.btcapt.Disable()
    self.btstop.Enable()

  def OnStopCapt( self, e):
    global g_serport # Write
    global g_rawfname, g_downloading # Out
    if g_serport != None: g_serport.write(STANDBY_CHAR)
    g_rawfname = None
    g_downloading = False
    self.pg.SetValue(0)
    self.btstop.Disable()
    self.btcapt.Enable()

  def OnEditFields( self, e):
    flindex = self.flight_idx_chx.GetSelection() + 1
    if len(self.rawfname.GetValue())!=0 and flindex>=1 and flindex<=5:
      self.btcapt.Enable()
    else:
      self.btcapt.Disable()

  def OnCalc( self, e): # exit and process captured flight
    global g_rawfname # Out
    global g_downloading # In
    # I am attempting to Disable() this until it's okay to click,
    #  so this checking should be unnecessary
    if g_downloading or g_rawfname==None: # Early exit?
      if g_downloading: self.log("Currently downloading.")
      if g_rawfname==None: self.log("No raw-filename.")
      wx.MessageBox( "Raw data not captured.  Can't process.",
                     caption="Data Error", style=wx.OK|wx.ICON_ERROR)
      g_rawfname = None # prevent processing upon dialog exit
      return
    # Otherwise, parent dialog will process g_rawfname when this dialog done
    self.on_exit(e)

  def OnOk( self, e):
    global g_rawfname # Out
    g_rawfname = None # prevent processing upon dialog exit
    self.on_exit(e)

  def on_exit( self, e):
    global global_gui # Out
    global_gui = self.gui_save # restore logging to main GUI window
    e.Skip()
    self.Bind(wx.EVT_CLOSE, None)
    self.Close(True)

    
class CaptProfileDialog( wx.Dialog): # ----------------------------------------

  def __init__(self,parent,id,title):

    # .-Capture Profile -------------------------.
    # |                                          |
    # |   filename ________                      |self.rawfname|topsizer|
    # | .--------------------------------------. |                      |
    # | |                                      | |                      |
    # | |  Log Area                            | |self.logtext|logsizer |
    # | |                                      | |                      |
    # | |                                      | |                      |
    # | |                                      | |                      |vsizer
    # | |                                      | |                      |
    # | `--------------------------------------' |                      |
    # |                                          |                      |
    # |   Progress  ||||||||||||||||||||||||     |self.pg|panel|pgsizer |
    # |                                          |                      |
    # | Capture  Stop-Capture               Exit |(3 buttons)|btsizer   |
    # |                                          |
    # `------------------------------------------'

    global behavior # InOut
    global global_gui, g_downloading, g_rawfname # Out
    global g_download_progress_total # for progress-bar

    wx.Dialog.__init__(self, None, -1, title)
    self.Bind( wx.EVT_CLOSE, self.OnOk)

    g_downloading = False
    g_rawfname = None
    self.pgd = None # progress dialog
    self.verbose = behavior['verbose']
    self.raw_file_dir = behavior['raw_file_dir']

    txt_ht = 17
    log_wind_width = 480
    gap_spacer = wx.Size(4,2) # for gaps and borders
    med_hgap_spacer = wx.Size(8,2)
    big_hgap_spacer = wx.Size(log_wind_width-230,2)

    # Top row of controls .. Raw-flight filename
    topsizer = wx.BoxSizer(wx.HORIZONTAL)
    topsizer.Add( gap_spacer, 0, wx.ALL, 2)
    panel = wx.Panel(self, -1)
    topsizer.Add( panel, 0, wx.ALL, 2)
    topsizer.Add( gap_spacer, 0, wx.ALL, 2)
    panel = wx.Panel(self, -1) # Why did I make so many panels?
    wx.StaticText(panel, label="Filename", pos=(2,2) )
    self.rawfname = wx.TextCtrl(panel, size=(200,txt_ht), pos=(70,0) )
    self.Bind( wx.EVT_TEXT, self.OnTextEnter, self.rawfname)
    topsizer.Add( panel, 0, wx.ALL, 2)
    topsizer.Add( gap_spacer, 0, wx.ALL, 2)

    # Log-window
    self.logtext = wx.TextCtrl(self, style=wx.TE_MULTILINE,
                               size=(log_wind_width,220))
    self.logtext.SetEditable(False)
    point_size = 8
    # Use a monospaced (fixed-char-width) font to maintain columnar alignment
    font = wx.Font( point_size, wx.TELETYPE, wx.NORMAL, wx.NORMAL)
    self.logtext.SetFont(font)

    logsizer = wx.BoxSizer(wx.HORIZONTAL)
    logsizer.Add( gap_spacer, 0, wx.ALL, 2)
    logsizer.Add( self.logtext, 8, wx.LEFT|wx.TOP|wx.GROW)
    logsizer.Add( gap_spacer, 0, wx.ALL, 1)

    # Progress-bar
    pgsizer = wx.BoxSizer(wx.HORIZONTAL)
    pgsizer.Add( gap_spacer, 0, wx.ALL, 2)
    panel = wx.Panel(self, -1)
    wx.StaticText(panel, label="Progress", pos=(50,5) )
    g_download_progress_total = 200 * 2 # total number of samples
    self.pg = wx.Gauge( panel, -1, g_download_progress_total, size=(250,20),
                        pos=(120,2) )
    pgsizer.Add( panel, 0, wx.ALL, 2)
    pgsizer.Add( gap_spacer, 0, wx.ALL, 2)

    # Three buttons across bottom, in a horizontal box sizer
    btsizer = wx.BoxSizer(wx.HORIZONTAL)
    btsizer.Add( med_hgap_spacer)
    self.btcapt = wx.Button(self,CAPT_CAPT_ID,"Capture")
    self.btcapt.Disable() # until some text typed
    btsizer.Add( self.btcapt, flag=wx.LEFT)
    wx.EVT_BUTTON( self, CAPT_CAPT_ID, self.OnCapt)
    btsizer.Add( gap_spacer)
    self.btstop = wx.Button( self, CAPT_STOP_ID, "Stop Capture")
    btsizer.Add( self.btstop, flag=wx.LEFT)
    self.btstop.Disable()
    wx.EVT_BUTTON( self, CAPT_STOP_ID, self.OnStopCapt)
    btsizer.Add( gap_spacer)
    # It would look better if the exit button were flush right
    btsizer.Add( big_hgap_spacer, 1)
    btsizer.Add( wx.Button(self,CAPT_DONE_ID,"Exit"), flag=wx.LEFT)
    wx.EVT_BUTTON( self, CAPT_DONE_ID, self.OnOk)
    btsizer.Add( gap_spacer)

    vsizer = wx.BoxSizer(wx.VERTICAL)
    vsizer.Add(gap_spacer)
    vsizer.Add(topsizer)
    vsizer.Add(gap_spacer)
    vsizer.Add(logsizer)
    vsizer.Add(gap_spacer)
    vsizer.Add(gap_spacer)
    vsizer.Add(pgsizer)
    vsizer.Add(gap_spacer)
    vsizer.Add(gap_spacer)
    vsizer.Add(btsizer)
    vsizer.Add(gap_spacer)
    vsizer.Add(gap_spacer)

    self.SetSizerAndFit(vsizer)

    self.gui_save = global_gui
    global_gui = self # when this dialog up, log to its log window

  def OnTextEnter( self, e):
    if len(self.rawfname.GetValue()):
      self.btcapt.Enable()
    else:
      self.btcapt.Disable()

  def update_progress(self):
    global g_download_progress, g_download_progress_total # for progress-bar
    if g_downloading:
      if g_download_progress<g_download_progress_total:
        self.pg.SetValue(g_download_progress)
    else: # Done
      self.pg.SetValue(0)
      self.btstop.Disable()
      self.btcapt.Enable()

  def log(self,text):
    self.logtext.AppendText(text + '\n')
  def log_nolf(self,text):
    self.logtext.AppendText(text)

  def OnCapt( self, e):
    global g_downloading, g_dumpfile # Out (set, but not clear)
    global g_serport # Write
    global g_rawfname # Out
    global g_download_progress # for progress-bar
    if g_serport == None:
      wx.MessageBox( "Not communicating with Parrot.", caption="Comm Error",
                     style=wx.OK|wx.ICON_ERROR)
      return
    # Open the output-dump file
    if self.rawfname.GetValue() == '':
      wx.MessageBox("Must supply raw capture filename.",
                    caption="Parameter Error",style=wx.OK|wx.ICON_EXCLAMATION)
      return
    fname = self.raw_file_dir + '/' + self.rawfname.GetValue()
    while 1:
      # See if file exists; prompt for overwrite/rename
      try:
        dumpfile = open(fname,"r"); dumpfile.close()
        extant = True
      except IOError:
        extant = False
        break
      if prompt_yesno( "File %s exists.  Overwrite?" % fname) != True:
        return
      break # Could prompt for a different file name here, and continue
    try:
      dumpfile = open(fname,'w')
    except IOError:
      self.log("Can't open dump-file '%s'" % fname)
      return
    # Tell the serport-reader thread to start a download, via global variables
    g_serport.write(STANDBY_CHAR)
    g_serport.write('0') # Tell parrot we want "flight zero"
    g_dumpfile = dumpfile
    g_download_progress = 0 # for progress-bar
    g_downloading = True
    g_serport.write(DOWNLOAD_CHAR)
    g_rawfname = fname
    self.btcapt.Disable()
    self.btstop.Enable()
    #if not behavior['verbose']: # updates are extremely slow
    #  # This might work, if using a global updater from downloader thread
    #  self.pgd = wx.ProgressDialog("Flight Download","Download Progress",
    #                               g_download_progress_total,
    #                               style=wx.PD_AUTO_HIDE|wx.PD_ELAPSED_TIME\
    #                                      |wx.PD_ESTIMATED_TIME\
    #                                      |wx.PD_CAN_ABORT)#|wx.PD_APP_MODAL)
    #  contin = True
    #  while g_downloading and contin \
    #         and g_download_progress<g_download_progress_total:
    #    time.sleep(.2)
    #    contin = self.pgd.Update(g_download_progress)
    #  # This doesn't seem to detect Cancel (or parent exit)
    #  self.pgd.Destroy()
    #  self.pgd = None
    #also doesn't work (hangs)
    #if not behavior['verbose']:
    #  while g_downloading and g_download_progress<g_download_progress_total:
    #    time.sleep(.2)
    #    self.self.update_progress()
    #self.pg.SetValue(0)

  def OnStopCapt( self, e):
    global g_serport # Write
    global g_rawfname, g_downloading # Out
    if g_serport != None: g_serport.write(STANDBY_CHAR)
    g_rawfname = None
    g_downloading = False
    self.pg.SetValue(0)
    self.btstop.Disable()
    self.btcapt.Enable()

  def OnOk( self, e):
    global g_rawfname # Out
    g_rawfname = None # prevent processing upon dialog exit
    self.on_exit(e)

  def on_exit( self, e):
    global global_gui # Out
    global_gui = self.gui_save # restore logging to main GUI window
    if self.pgd:  self.pgd.Destroy()
    e.Skip()
    self.Bind(wx.EVT_CLOSE, None)
    self.Close(True)

    
class TextEntryDialog3( wx.Dialog): # ----------------------------------------

  def __init__(self,parent,id,title,prompt,initial):

    # .- Output Filename -----------------------.
    # |                                         |
    # | Enter the processed file to create      |
    # | .------------------------------------.  |
    # | |  Text Entry                        |  |
    # | `------------------------------------'  |
    # |     Skip output-file creation           |
    # |                                         |
    # |  Cancel                         Ok      |
    # `-----------------------------------------'

    wx.Dialog.__init__(self, None, -1, title, size=(275,165))
    self.Bind( wx.EVT_CLOSE, self.OnCancel)

    # Let's just try it once without sizers
    wx.StaticText(self, label=prompt, pos=(12,10) )
    self.text = wx.TextCtrl(self, size=(250,20), pos=(10,30) )
    wx.Button(self,TEXT3_PROC_ID,"Skip output-file creation", pos=(70,60))
    wx.EVT_BUTTON( self, TEXT3_PROC_ID, self.OnProc)
    wx.Button(self,wx.ID_CANCEL,"Cancel",pos=(10,110))
    wx.Button(self,wx.ID_OK,"Ok",pos=(185,110))

    self.text.SetValue(initial)
    self.text.SetEditable(True)

  def OnProc(self,e):
    self.EndModal(TEXT3_PROC_ID)

  def OnCancel(self,e):
    self.EndModal(wx.ID_CANCEL)

  def GetValue(self):
    return self.text.GetValue()


class PlotFigure(wx.Frame): # -------------------------------------------------
  def __init__(self,initial_err,initial_raw_fname):
    wx.Frame.__init__(self, None, -1, "Parrot processor")

    ###################################################################
    # File              Settings            Capture           Help    #
    #  Open Raw Flight   Configure Comm      Download Flight   About  #
    #  Save Flight       Configure Proc      Download Profile  Help   #
    #  Exit              Configure Files                              #
    #                    (Program Parrot)                             #
    #                                                                 #
    #   Plot Label: ______________________   Open-raw-flight   Exit   #
    #                                                                 #
    #  .----------------------------------------------------------.   #
    #  |                                                           |  #
    #  |                                                           |  #
    #  |                                                           |  #
    #  |                         Plot                              |  #
    #  |                                                           |  #
    #  |                                                           |  #
    #  |                                                           |  #
    #  |                                                           |  #
    #  |                                                           |  #
    #  |                                                           |  #
    #  |                                                           |  #
    #  |                                                           |  #
    #  |                                                           |  #
    #  |                                                           |  #
    #  |-----------------------------------------------------------'  #
    #  | Plot-toolbar                        |                        #
    #  `-------------------------------------'                        #
    # Log: .---------------------------. Flight:.-------------------. #
    #      |                           |        |                   | #
    #      |                           |        |                   | #
    #      |                           |        |                   | #
    #      |                           |        |                   | #
    #      `---------------------------'        `-------------------' #
    ##################################################################


    global global_gui # Out .. to provide logging for all
    global behavior, initial_log # In

    # Add a panel so it looks correct on all platforms
    #self.panel = wx.Panel(self, wx.ID_ANY)

    txt_ht = 15
    flightlbl = wx.StaticText(self, label="Plot Label", pos=(2,2),
                              size=(50,txt_ht))
    self.flighttxt = wx.TextCtrl(self, size=(150,txt_ht))
    start_btn = wx.Button( self, OPEN_RAWFILE_ID, "Open Raw Flight")
    #graph_btn = wx.Button( self, GRAPH_ID, "Graph")
    exit_btn = wx.Button( self, EXIT_ID, "Exit")
    wx.EVT_BUTTON( self, OPEN_RAWFILE_ID, self.OnRawFileOpen)
    #wx.EVT_BUTTON( self, GRAPH_ID, self.OnGraph) # plot w/o making output file
    wx.EVT_BUTTON( self, EXIT_ID, self.OnExit)

    gap_spacer = wx.Size(4,2) # for gaps and borders
    topsizer = wx.BoxSizer(wx.HORIZONTAL)
    topsizer.Add(gap_spacer)
    topsizer.Add(flightlbl, flag=wx.LEFT)
    topsizer.Add(gap_spacer)
    topsizer.Add(self.flighttxt, 8, wx.GROW)
    topsizer.Add(gap_spacer)
    topsizer.Add(start_btn)
    topsizer.Add(gap_spacer)
    #topsizer.Add(graph_btn)
    #topsizer.Add(gap_spacer)
    topsizer.Add(exit_btn, 0, wx.RIGHT)
    topsizer.Add(gap_spacer)

    self.fig = Figure((9.5,6), 75) # 9.5x6 inches size, 75 dpi
    self.canvas = FigureCanvas(self, -1, self.fig)
    #old toolbar:self.toolbar = Toolbar(self.canvas)
    self.toolbar = NavigationToolbar2Wx(self.canvas)
    self.toolbar.Realize()
    if os.name.startswith('darwin'):#is that right?
      # Mac platform (OSX 10.3, MacPython) does not seem to cope with
      # having a toolbar in a sizer. This work-around gets the buttons
      # back, but at the expense of having the toolbar at the top
      self.SetToolBar(self.toolbar)
    elif os.name == 'nt':
      # On Windows, default frame size behaviour is incorrect, so set
      # toolbar width to figure width.
      tw, th = self.toolbar.GetSizeTuple()
      fw, fh = self.canvas.GetSizeTuple()
      # By adding toolbar in sizer, we are able to put it at the bottom
      # of the frame - so appearance is closer to GTK version.
      # As noted above, doesn't work for Mac.
      self.toolbar.SetSize(wx.Size(fw, th))
    else: # you don't need this under Linux
      pass
    self.toolbar.update()
    # Create a figure manager to manage things (what does this do?)
    self.figmgr = FigureManager(self.canvas, 1, self)
    plotsizer = wx.BoxSizer( wx.VERTICAL)
    plotsizer.Add( self.canvas, 4, wx.GROW)
    plotsizer.Add( self.toolbar, 0, wx.GROW)
  
    # Log-window is across the bottom
    log_label = wx.StaticText(self, label="Log", style=wx.ALIGN_RIGHT)
    self.logtext = wx.TextCtrl(self, style=wx.TE_MULTILINE, size=(140,50))
    self.logtext.SetEditable(False)

    stats_label = wx.StaticText(self, label="Flight", style=wx.ALIGN_RIGHT)
    self.stats_txt = wx.TextCtrl(self, style=wx.TE_MULTILINE, size=(100,50))
    self.stats_txt.SetEditable(False)

    #font = wx.Font( point_size, wx.TELETYPE, wx.NORMAL, wx.NORMAL)
    #self.batch_rec.SetFont(font)

    logsizer = wx.BoxSizer(wx.HORIZONTAL)
    logsizer.Add( gap_spacer, 0, wx.ALL, 2)
    logsizer.Add( log_label, 0, wx.LEFT|wx.TOP)
    logsizer.Add( self.logtext, 17, wx.LEFT|wx.TOP|wx.GROW)
    logsizer.Add( gap_spacer, 0, wx.ALL, 1)
    logsizer.Add( stats_label, 0, wx.LEFT|wx.TOP)
    logsizer.Add( self.stats_txt, 12, wx.LEFT|wx.TOP|wx.GROW)
    logsizer.Add( gap_spacer, 0, wx.ALL, 1)

    # Arrange plot-canvas and loggers using a vertical sizer
    sizer = wx.BoxSizer(wx.VERTICAL)
    # This way of adding to sizer allows resizing .. 5:1 ratio graphs:logs
    sizer.Add( gap_spacer, 0, wx.ALL, 2)
    sizer.Add( topsizer, 0, wx.GROW)
    sizer.Add( gap_spacer, 0, wx.ALL, 2)
    sizer.Add( plotsizer, 5, wx.LEFT|wx.TOP|wx.GROW)
    sizer.Add( gap_spacer, 0, wx.ALL, 2)
    sizer.Add( logsizer, 1, wx.LEFT|wx.BOTTOM|wx.GROW)
    sizer.Add( gap_spacer, 0, wx.ALL, 2)
    self.SetSizer(sizer)

    menubar = wx.MenuBar()

    menu = wx.Menu() # vertical drop-down, to go under "File" on menu-bar
    menubar.Append( menu, "File")
    menu.Append( OPEN_RAWFILE_ID, "&Open File",
                 " Post-process a Flight") # StatusBar tooltip
    # Bind callback to menu
    wx.EVT_MENU( self, OPEN_RAWFILE_ID, self.OnRawFileOpen)
    menu.Append( MENU_EXIT_ID, "E&xit", "Exit Application")
    wx.EVT_MENU( self, MENU_EXIT_ID, self.OnExit)

    menu = wx.Menu() # drop-down under "Settings" on menu-bar
    menubar.Append( menu, "Settings")
    # Don't really need a drop-down submenu, since there's only one item
    menu.Append( MENU_CONF1_ID, "Configure &Communication", " Configure Comm")
    wx.EVT_MENU( self, MENU_CONF1_ID, self.OnCommConf)
    menu.Append( MENU_CONF2_ID, "Configure &Processing", " Configure Proc")
    wx.EVT_MENU( self, MENU_CONF2_ID, self.OnProcConf)
    menu.Append( MENU_CONF3_ID, "Configure &Files", " Configure Files")
    wx.EVT_MENU( self, MENU_CONF3_ID, self.OnFileConf)
    #menu.Append( MENU_PROG_ID, "&Program Parrot", " Program Parrot")
    #wx.EVT_MENU( self, MENU_PROG_ID, self.OnProg)

    menu = wx.Menu() # drop-down under "Capture" on menu-bar
    menubar.Append( menu, "Capture")
    # Don't really need a drop-down submenu, since there's only one item
    menu.Append( MENU_CAPT_ID, "Download &Flight", " Download Flight")
    wx.EVT_MENU( self, MENU_CAPT_ID, self.OnCapt)
    menu.Append( MENU_CAPT0_ID, "Download &Profile", " Download Profile")
    wx.EVT_MENU( self, MENU_CAPT0_ID, self.OnCapt0)

    menu = wx.Menu() # vertical drop-down, to go under "Help" on menu-bar
    menubar.Append( menu, "Help")
    menu.Append( MENU_ABOUT_ID, "&About")
    menu.Append( MENU_HELP_ID, "&Help")
    wx.EVT_MENU( self, MENU_ABOUT_ID, self.OnAbout)
    wx.EVT_MENU( self, MENU_HELP_ID, self.OnHelp)

    self.SetMenuBar( menubar) # Hook menu-bar into the main Frame

    self.Fit() # self is a Frame

    if initial_log!=None and initial_log!='':
      self.log( initial_log)
    initial_log = None # signals that it has been processed
    if initial_err:
      wx.MessageBox( initial_err, caption="Startup Error",
                     style=wx.OK|wx.ICON_ERROR)
      self.log( initial_err)
    if behavior['com_port'] == -1:
      self.OnCommConf(None)
    global_gui = self
    if initial_raw_fname: # from command-line argument
      self.proc_rawfile(initial_raw_fname)
    self.max_alt = 0

  def OnPaint(self, event):
    self.canvas.draw()

  def OnCommConf( self, e):
    """Configure Comm"""
    tmp_dlg = CommConfDialog( self, -1, "Comm Configuration")
    tmp_dlg.ShowModal()
    tmp_dlg.Destroy()

  def OnProcConf( self, e):
    """Configure Processing"""
    tmp_dlg = ProcConfDialog( self, -1, "Processing Configuration")
    tmp_dlg.ShowModal()
    tmp_dlg.Destroy()

  def OnFileConf( self, e):
    """Configure Files"""
    tmp_dlg = FileConfDialog( self, -1, "File Configuration")
    tmp_dlg.ShowModal()
    tmp_dlg.Destroy()

  def OnProg( self, e):
    """Program the Parrot"""
    global g_serport, parrot_prog
    if g_serport == None:
      wx.MessageBox( "Not communicating with Parrot.", caption="Comm Error",
                     style=wx.OK|wx.ICON_ERROR)
      return
    choices = [] # There must be a better
    progs = []   #  (more Python-idiomatic) way to do this!
    for progname,value in parrot_prog:
      choices.append(progname)
      progs.append(value)
    chx_dlg = wx.SingleChoiceDialog(None,"Choose Flight Profile","Profiles",
                                    choices)
    if chx_dlg.ShowModal() == wx.ID_OK:
      idx = chx_dlg.GetSelection()
      #self.log("SingleChoiceDialog() key=%s value=%s"%(idx,progs[idx]))
      text_dlg = wx.TextEntryDialog(None,"Click Ok to Program",
                                    "Parrot Program Entry", progs[idx],
                                    style=wx.OK|wx.CANCEL)
      if text_dlg.ShowModal() == wx.ID_OK:
        prog = text_dlg.GetValue()
        if behavior['verbose']>0: self.log("Programming " + prog)
        g_serport.write(PROGRAM_CHAR)
        #Too fast(?!) g_serport.write(text_dlg.GetValue())
        for c in prog:     # This just doesn't work.
          time.sleep(.1)
          try:
            g_serport.write(c)
          except serial.serialutil.SerialTimeoutException:
            time.sleep(.1)
            g_serport.write(c)
      text_dlg.Destroy()
    chx_dlg.Destroy()

  def OnCapt( self, e):
    """Capture flight(s)"""
    global global_gui, g_rawfname
    tmp_dlg = CaptureDialog( self, -1, "Capture")
    tmp_dlg.ShowModal()
    # Was Capture-dialog exited using "Process" button?
    if g_rawfname != None:
      self.log("Capture dialog exited, raw-filename = '%s'" % g_rawfname)
      self.proc_rawfile(g_rawfname)
    tmp_dlg.Destroy()

  def OnCapt0( self, e):
    """Capture flight-index 0"""
    global global_gui, g_rawfname
    tmp_dlg = CaptProfileDialog( self, -1, "Capture Profile")
    tmp_dlg.ShowModal()
    tmp_dlg.Destroy()

  def OnAbout( self, e):
    tmp_dlg = wx.MessageDialog( self, "parrot-all version " + str(VERSION),
                                caption="About", style=wx.OK)
    tmp_dlg.ShowModal()
    tmp_dlg.Destroy()

  def OnHelp( self, e):
    tmp_dlg = wx.lib.dialogs.ScrolledMessageDialog( self, help_text(), "Help",
                                                    size=(400,600) )
    #tmp_dlg = HelpDialog( self, -1, "Help")
    tmp_dlg.ShowModal()
    tmp_dlg.Destroy()

  def mark_events(self,ax,flight):
    """Put vertical lines on a plot at various flight events"""
    # Vertical magenta lines marking inferred burnout, max-alt time
    ax.axvline(flight.burnout_time,linewidth=1,color='magenta',\
               label='Flight events')
    ax.axvline(flight.max_alt_time,linewidth=1,color='magenta')
    # Vertical orange lines marking FER events
    if flight.FER_liftoff_time != None:
      ax.axvline(flight.FER_liftoff_time,linewidth=1,color='orange',\
                 label='FER events')
    if flight.FER_burnout_time != None:
      ax.axvline(flight.FER_burnout_time,linewidth=1,color='orange')
    if flight.FER_baro_apo_time != None:
      ax.axvline(flight.FER_baro_apo_time,linewidth=1,color='orange')
    if flight.FER_accel_apo_time != None:
      ax.axvline(flight.FER_accel_apo_time,linewidth=1,color='orange')
    if flight.FER_low_alt_deploy_time != None:
      ax.axvline(flight.FER_low_alt_deploy_time,linewidth=1,color='orange')

  def plot(self,flight,behavior,infilename):
    """Plot a flight using matplotlib tools"""
    if not behavior['overlay_plots']:
      self.fig.clear()
      self.max_alt = 0
    ylabel = 'Feet AGL'
    title = self.flighttxt.GetValue()
    if not title:
      title = infilename # Text-entry box empty .. use filename

    n_subplots = 1 # always plot altitude
    if behavior['plot_desc']:  n_subplots += 1
    if behavior['plot_accel']: n_subplots += 1
    if behavior['plot_temperature']: n_subplots += 1
    subplot_ids_list = [[111],[211,212],[311,312,313],[411,412,413,414]]
    subplot_ids = subplot_ids_list[n_subplots-1]

    #axes = self.fig.add_subplot(111) # Use this if using a toolbar
    #axes = Subplot(self.fig, 111) # Or this if there is no toolbar
    axes = []
    for id in subplot_ids:
      these_axes = self.fig.add_subplot(id)
      axes.append( these_axes)
    these_axes.set_xlabel('time (sec) after liftoff')
    # The following color abbreviations are supported (among others)
    #  b=blue, g=green, r=red, c=cyan, m=magenta, y=yellow, k=black, w=white
    ai=0 # axes index
    if self.max_alt == 0: # virgin (overlay)
      lb1 = 'Raw'
      lb2 = 'Filtered'
      lb3 = 'Integrated Accel'
      lb4 = 'Temp comp'
    else: lb1 = lb2 = lb3 = None
    axes[ai].plot( flight.time_hist, flight.alt_hist,'green', label=lb1)
    axes[ai].plot( flight.time_hist, flight.filt_hist,'red', label=lb2)
    if behavior['plot_integ_accel']:
      axes[ai].plot( flight.time_hist, flight.accel_alt_hist,'blue', label=lb3)
      self.max_alt = max( self.max_alt, max(flight.accel_alt_hist) )
    else:
      self.max_alt = max( self.max_alt, flight.baro_alt_max)
    if behavior['plot_tempcomp_alt']:
      axes[ai].plot( flight.time_hist, flight.tcomp_alt_hist, 'black',label=lb4)
      self.max_alt = max( self.max_alt, max(flight.tcomp_alt_hist) )
    if behavior['plot_grids']:  axes[ai].grid(True)
    #axes[ai].set_xlim(-1,flight.time_hist[-1]) #doesn't work
    self.mark_events( axes[ai], flight)
    axes[ai].axis( [-1,flight.time_hist[-1],
                    -50,self.max_alt*1.02]) # x,y scaling
    #axes[ai].autoscale_view(tight=False, scalex=False, scaley=True)
    axes[ai].set_title(title)
    axes[ai].set_ylabel(ylabel)
    axes[ai].legend(loc='best') # best is probably 'upper right'
    ai += 1
    if behavior['plot_desc']:  
      axes[ai].plot(flight.time_hist_tplus,flight.desc_hist,'red',label='Baro')
      axes[ai].plot(flight.time_hist,flight.acc_vel_hist,'black',label='Accel')
      if behavior['plot_grids']:  axes[ai].grid(True)
      self.mark_events( axes[ai], flight)
      axes[ai].axis( [-1,flight.time_hist[-1], -100,100]) # x,y scaling
      axes[ai].autoscale_view(tight=True, scalex=False, scaley=True)
      axes[ai].set_ylabel('Descent, ft/s')
      axes[ai].legend(loc='best') # best is probably 'lower right'
      ai += 1
    if behavior['plot_accel']:
      axes[ai].plot(flight.time_hist,flight.accel_hist,'black')
      if behavior['plot_grids']:  axes[ai].grid(True)
      self.mark_events( axes[ai], flight)
      axes[ai].axis( [-1,flight.time_hist[-1], -20,50]) # x,y scaling
      axes[ai].autoscale_view(tight=True, scalex=False, scaley=True)
      axes[ai].set_ylabel('Accel, Gs')
      ai += 1
    if behavior['plot_temperature']:
      axes[ai].plot(flight.time_hist,flight.temp_hist,'black')
      if behavior['plot_grids']:  axes[ai].grid(True)
      self.mark_events( axes[ai], flight)
      axes[ai].axis( [-1,flight.time_hist[-1], 0,150]) # x,y scaling
      axes[ai].autoscale_view(tight=True, scalex=False, scaley=True)
      if n_subplots<3: axes[ai].set_ylabel('Temperature, deg F')
      else:            axes[ai].set_ylabel('Temp, deg F')
      ai += 1

    # Plotting FER:
    # axes[ai].plot(flight.time_hist_tplus,flight.FER_hist,'black')

    # Adjust borders
    # subplots_adjust(left=None, bottom=None, right=None,
    #                 wspace=None, hspace=None)
    # defaults: left = 0.125, right = 0.9, bottom = 0.1, top = 0.9,
    #           wspace = 0.2 width for blank space between subplots
    #           hspace = 0.2
    self.fig.subplots_adjust(left=.09,right=.98,top=.94,bottom=0.08)

    self.toolbar.update()
    self.canvas.draw()

  def OnRawFileOpen(self,e):
    self.log("Open Raw File clicked")
    raw_ext = behavior['raw_file_ext']
    wildcards = 'Raw flight (*%s)|*%s|All files (*.*)|*.*' % (raw_ext,raw_ext)
    tmp_dlg = wx.FileDialog( self, message="Raw Flight File Selection",
                             defaultFile='', style=wx.OPEN|wx.CHANGE_DIR,
                             wildcard=wildcards)
    if tmp_dlg.ShowModal() != wx.ID_OK: # cancel
      tmp_dlg.Destroy()
      return
    rawfname = tmp_dlg.GetPath()
    tmp_dlg.Destroy()
    if behavior['verbose'] > 0:
      self.log("Raw file '%s' chosen" % rawfname)
    self.proc_rawfile(rawfname)

  def proc_rawfile(self,rawfname):
    self.cookedfname = None # If ==None, Proc and graph w/o file output
    while 1: # get output file name
      ofname = rawfname
      # Output filename is input filename, with extension changed (if possible)
      if ofname.endswith(behavior['raw_file_ext']):
        ofname = ofname[0:-len(behavior['raw_file_ext'])]
      ofname += behavior['post_file_ext']
      ofdlg = TextEntryDialog3( self, -1, "Output filename",
                                "Enter the processed file to create", ofname)
      ret = ofdlg.ShowModal()
      #self.log("Dialog returned %s (OK==%s)" % (repr(ret),repr(wx.ID_OK)))
      if ret == wx.ID_OK:
        self.cookedfname = ofdlg.GetValue()
        self.log("Dialog user-entered value = " + repr(self.cookedfname))
      ofdlg.Destroy()
      if ret == wx.ID_CANCEL:
        return
      if ret == TEXT3_PROC_ID:
        break # Process and graph, omitting file output
      try:
        outfile = open(self.cookedfname,"r"); outfile.close()
        #self.log("open/close cycle worked .. thus, extant")
      except: # file doesn't exist, most likely
        #self.log("open/close cycle failed .. thus, fnexist")
        break
      ans = prompt_yesno( "File %s exists.  Overwrite?" % self.cookedfname)
      if ans==True:
        break
    if behavior['verbose'] > 1:
      self.log("calc(in=%s,out=%s)"%(rawfname,self.cookedfname))
    self.calc( rawfname, behavior, self.cookedfname, 
               self.flighttxt.GetValue() )

  def log(self,text):
    self.logtext.AppendText(text+"\n")
  def log_nolf(self,text):
    self.logtext.AppendText(text)
  def report(self,text):
    self.stats_txt.AppendText(text+"\n")
  def clear_report(self):
    self.stats_txt.Clear()

  def OnExit(self,e):
    # This traps the Close-App (upper-right X on title bar), and Exit button
    #self.log("Exit clicked\n")
    e.Skip()
    #un-register the OnClose event to prevent infinite recursion
    self.Bind(wx.EVT_CLOSE, None)
    # mop up .. close log & batch-record
    self.Close(True)

  def calc(self,ifname,behavior,ofname,flight_name):
    try:    infile = open( ifname, 'r')
    except: err("Can't open file for input: " + ifname);  return
    if ofname:
      try:    outfile = open(ofname, 'w')
      except: err("Can't open file for output: " + ofname);  return
    else:
      outfile = None
    flight = Flight()
    global PRE_LAUNCH,BOOST,COAST,DESCENT,DOWN # Phases of flight

    for line in infile:
      textline = line.strip()
      flight.post_process_line( textline, outfile, err, log, behavior)
      if flight.phase==DOWN and behavior['proc_cutoff_downed']==True:
	# Cut off processing, since it's long and boring
        break

    if flight.phase == PRE_LAUNCH:
      err("End of data; launch not detected")
    else:
      gnd_alt = flight.filt_hist[-1] # same as landing_alt?
      if flight.phase < DOWN:
        err("Flight data ended above ground level (%g ft)" % gnd_alt)
      avg_desc_rate = (flight.baro_alt_max-flight.landing_alt) \
                       / (flight.time_hist[-1] - flight.max_alt_time)
      self.clear_report()
      if flight_name:
        self.report("Flight: " + flight_name)
      self.report("Average pre-launch altitude = %g ft ASL" \
                  % flight.liftoff_alt)
      self.report("Maximum positive acceleration = %g G" % flight.max_accel)
      if flight.FER_burnout_time != None:
        self.report("FER Burnout: T+ %g sec" % flight.FER_burnout_time)
      self.report("Average accel during thrust %g G" \
                   % flight.avg_thrust_accel)
      self.report("Apogee = %g ft AGL at T+ %g sec" \
                  % (flight.baro_alt_max,flight.max_alt_time) )
      if flight.FER_accel_apo_time != None:
        self.report("FER Accel Apogee: %g sec, %g ft/sec" \
                     % (flight.FER_accel_apo_time,flight.deploy_vel_acc) )
      if flight.FER_baro_apo_time != None:
        self.report("FER Baro Apogee: %g sec, %g ft/sec" \
                     % (flight.FER_baro_apo_time,flight.deploy_vel_baro) )
      self.report("Average descent rate = %g ft/sec" % avg_desc_rate)
      if flight.FER_low_alt_deploy_time != None:
        final_desc = (flight.main_deploy_alt-flight.landing_alt) \
                      /(flight.time_hist[-1]-flight.FER_low_alt_deploy_time)
        self.report("FER Main: %g sec, final desc. %g ft/sec"\
                     % (flight.FER_low_alt_deploy_time,final_desc) )
      self.report("Landing altitude %g ft AGL" % flight.landing_alt)
        
      # Do some graphing
      if behavior['verbose'] > 0:  self.log("Starting matplotlib")
      self.plot(flight,behavior,ifname)

    del flight
    log( "Done with flight")

# ----- err, log, log_nolf ----------------------------------------------------
#

def con_err( message):
  """Show error message on console screen"""
  sys.stderr.write( message)

def err( message):
  """Pop up text in a dialog."""
  global global_gui
  if global_gui:
    wx.MessageDialog( None, message, "Error", wx.OK|wx.ICON_ERROR)
  else:
    con_err( message + "\n")

def log( message):
  """Log a line of text to console, or GUI window if active"""
  global global_gui, initial_log
  if initial_log == None: # it's been processed
    if global_gui:
      global_gui.log( message)
    else:
      con_err( message + "\n")
  else:
    initial_log += message + "\n"

def log_nolf( message):
  """Log text to console, or GUI window if active, without adding a linefeed"""
  global global_gui
  if global_gui:
    global_gui.log_nolf( message)
  else:
    con_err( message)


# ----- prompt_yesno ----------------------------------------------------------

def prompt_yesno( prompt):

  """Ask yes or no, in a pop-up dialog; return True of False"""

  ret = wx.MessageBox( prompt, "Yes-No Dialog", wx.YES_NO|wx.ICON_QUESTION)
  if ret==wx.YES: return True
  return False


# ----- process_line ----------------------------------------------------------
#

def process_line( linebuf, dumpfile):

  """If in download mode, parse and copy parrot data into file.
      Return end-of-data flag"""

  global g_downloading, g_verbose
  # e.g.
  # 1712.010    77      2       3818    1800    781     8162    4
  try:
    fld = linebuf.split() # cut up at whitespace (leading SPs, tab separators)
  except:
    err("\nCould not parse line: %s" % repr(linebuf) )
    return 0
  if len(fld) != 8:
    err("\nCould not parse line as 8 fields: %s" % repr(linebuf) )
    return 0
  # Parrot v.2 flight-event field is 5th?
  try:
    sex,axial_g,lateral_g,vbat,vref,temp,atm,ind = (float(x) for x in fld)
  except ValueError:
    err("\nCould not parse 8 fields as floats: %s" % repr(linebuf) )
    return 0
  if ind==0 and sex>2.0: # Useful Profile (flight-index 0) info ends at "2 sec"
    return 1
  # The G-sensors can read -1 normally; -1 means end of data for other fields
  if vbat==-1 or vref==-1 or temp==-1 or atm==-1:
    if g_verbose:  log("\nEnd of data detected")
    return 1
  # Write the line to the output file for this flight index
  if g_downloading:  # We're in capture mode
    dumpfile.write( linebuf + '\n')
  return 0


# ----- g_reader_thread -------------------------------------------------------

#  Global reader thread.  I can't get it working when it's a member function
#  of the capture dialog.

def g_reader_thread():
  """Serial-port reader thread: read, echo, optionally capturing to file"""
  global g_verbose, g_dumpfile, g_serport, g_downloading, g_flight_index
  global g_download_progress # Out: for progress-bar
  linebuf = ''
  if g_verbose:
    if g_log!=None and g_verbose>2: g_log("g_reader_thread() started.")
  if g_serport != None:           # Parrot v2 won't spit out real-time data
    g_serport.write(STANDBY_CHAR) #  until you send it a 'w' character.

  while 1:
    try:
      ch = g_serport.read() # one char
    except AttributeError:
      # 'NoneType' object has no attribute 'read'
      # This will happen if serial-port isn't set (correctly)
      ch = None #  .. wait,loop until it is.
      time.sleep(.1) # sometimes this fails
    #except ? When you unplug the USB, it throws
    #File "C:\Python25\Lib\site-packages\serial\serialwin32.py", line 208, in read
    #  n = win32file.GetOverlappedResult(self.hComPort, self._overlappedRead, 1)
    #error: (995, 'GetOverlappedResult', 'The I/O operation has been aborted because
    #of either a thread exit or an application request.')

    # Append each char to buffer
    # On newline, process the line in buffer & empty it
    if ch == '\n':
      if g_downloading:
        # (If downloading, char-by-char echo is too slow, and drops chars,
        #  so echoing happens only after a full line is received)
        if g_verbose>0 and g_log!=None: g_log(linebuf+"\n")
        g_download_progress += 1 # bump line-count for progress-bar
        try:    global_gui.update_progress()
        except: pass
        if process_line( linebuf, g_dumpfile): # return!=0 .. end of data
          g_dumpfile.close()
          #g_filename = ''
          g_downloading = False
          # Put Parrot into wait mode (stop dumping data)
          g_serport.write('w')
          g_flight_index = -1
          if g_log!=None: g_log("Download complete\n")
          try:    global_gui.update_progress() # back to zero
          except: pass
      else:
        if g_verbose>=3 and g_log!=None: g_log(repr(ch))
        if g_verbose>0 and g_log!=None: g_log(ch)
      linebuf = ''
    # Char is not a newline (LF)
    elif ch == '\r': # don't store CRs; only echo CRs (as \r) if very-verbose
      if g_verbose>=3 and g_log!=None: # this will show LF as \n etc.
        g_log(repr('\r'))
    else:
      # Append the char to our line-buffer
      linebuf += ch
      # Echo the received char (if verbose)
      # (If downloading, char-by-char echo is too slow; see \n case above)
      if g_downloading or g_verbose==0 or g_log==None: continue
      if g_verbose>=3: # this will show LF as \n etc.
        g_log(repr(ch))
      else:
        g_log(ch) # \r and \n both show up in GUI log as \n


# ----- main ------------------------------------------------------------------

class Usage(Exception):
  def __init__(self, msg):
    self.msg = msg

def main(argv=None):

  """Process command line options and argument,
     create config dir & file if missing,
     start serial-port reader thread, launch GUI."""

  usage_str="Usage:\n"\
            " parrot-all [options] [raw-flight]\n"\
            "  where options are:\n"\
            "   -h --help:   print this message\n"\
            "   -c --config: specify a config-file\n"\
            "  raw-flight is: a file (.par by default) to open and process"

  global behavior, global_gui, g_log, cfg_fname
  global g_verbose, g_dumpfile, g_serport, g_downloading, g_flight_index
  global thr, baudrate, initial_log, g_download_progress
  cfg_fname = None
  initial_raw_fname = None
  g_dumpfile = None
  global_gui = None
  g_serport = None
  g_downloading = False
  g_download_progress = 0 # for progress-bar
  g_flight_index = -1
  g_log = None
  initial_log = '' # Can't log to console, py2exe gui version
  initial_err = '' #  so store up any missives until gui starts
  g_log = log_nolf

  if argv is None:
    argv = sys.argv
  try:
    try:
      opts, args = getopt.getopt(argv[1:], "hc:", ["help","config="])
    except getopt.error, msg:
      raise Usage(msg)
    for o, a in opts: # Command-line options
      if o in ("-h", "--help"):
        print usage_str
        return 3
      elif o in ("-c","--config"):
        cfg_fname = a
    for arg in args: # command-line argument: initial raw flightname to process
      initial_raw_fname = arg
  except Usage, err:
    print >>sys.stderr, err.msg
    print >>sys.stderr, "for help use --help"
    return 2

  if cfg_fname is None:
    if os.name == 'nt':
      cfg_dirname = 'c:/Program Files/parrot'
    elif os.name == 'posix':
      cfg_dirname = '.' # How do I get $0? ARGV? $HOME? ENV?
    cfg_fname = cfg_dirname + '/parrot.cfg'
    if not os.path.exists( cfg_dirname):
      os.mkdir( cfg_dirname)
  try:
    cfg_file = open( cfg_fname, 'r')
    behavior = pickle.load(cfg_file)
    cfg_file.close()
  except IOError: # presumably, no config file
    behavior = { 'parrot_version' : 2,
                 'com_port' : -1, # (-1=None) 0=COM1, 1=COM2, ...
                 'spreadsheet_output_mode' : False,
                 'include_spreadsheet_header' : True,
                 'plot_desc': False,
                 'plot_accel': False,
                 'plot_temperature': False,
                 'verbose': VERBOSE_DEFAULT,
                 'raw_file_ext': '.par',
                 'raw_file_dir': 'c:/rocketry/flights',
                 'post_file_ext': '.csv',
                 'OFS': ",", #"\t",           # output-field separator
                 'G_THRESH': G_THRESH_DEFAULT,
                 'launch_detect_method': 'G_THRESH' # FER (Parrot 2), G_THRESH
               }
  # Extend old config file
  if 'proc_cutoff_downed' not in behavior.keys():
    behavior['proc_cutoff_downed'] = True
  if 'plot_temperature' not in behavior.keys():
    behavior['plot_temperature'] = False
  if 'plot_integ_accel' not in behavior.keys():
    behavior['plot_integ_accel'] = False
  if 'plot_grids' not in behavior.keys():
    behavior['plot_grids'] = True
  if 'overlay_plots' not in behavior.keys():
    behavior['overlay_plots'] = False
  if 'plot_tempcomp_alt' not in behavior.keys():
    behavior['plot_tempcomp_alt'] = False
  g_verbose = int(behavior['verbose'])
  app = wx.PySimpleApp(0)
  # Start up reader-thread before OnCapt, to get real-time sensor
  # readings, which appear even when not dumping flight
  try:
    thr = threading.Thread(target=g_reader_thread)
    thr.setDaemon(1)
  except:
    initial_err += "Could not start com-port reader thread"
    thr = None # I should probably kill the whole app!
  baudrate = 115200
  port = int(behavior['com_port']) # e.g. port 0 is COM1 or /dev/tty0
  conf_needed = False
  if port == -1:
    conf_needed = True
    # Maybe put up an informational dialog:
    #  Plug in USB to Parrot
    #  If it's v2, turn it on and push the button
    #  Maybe something about figuring out which serial port they're on
  else:
    initial_log += "Opening port %d (%s)\n" % (port,os_comport_name(port))
    try:
      g_serport = serial.Serial(port, baudrate, rtscts=0, xonxoff=0)
      if thr != None:
        thr.start()
    except:  # Bad serial port configuration .. let user fix it
      conf_needed = True

  if conf_needed:
    dlg = CommConfDialog( app, -1, "Comm Configuration")
    r = dlg.ShowModal() # returns wx.ID_CANCEL or wx.ID_OK
    dlg.Destroy()
    if r==wx.ID_OK and g_serport==None:
      initial_err += "Could not open com port"

  frame = PlotFigure(initial_err, initial_raw_fname)
  frame.Show()
  app.MainLoop()
  return 0

if __name__ == '__main__':
  sys.exit(main())
