# Parrot post processor

# To do:
#  Integrated accel doesn't match baro data very well.  I have some ideas:
#   - Axial-G sensor calibration is off.  (Or geographical gravity differs.)
#   - Zero out the integration at the moment of launch
#  Deployment detection (parrot v2 pyro continuity opens; but what about
#   parrot v1 or along-for-the-ride mode? gotta detect baro spike)
#  ?adjust for filter's time-delay

import sys, math

PARROT_POST_VERSION = 1.3

PRE_LAUNCH,BOOST,COAST,DESCENT,DOWN = range(5) # a la Enum
NOM_VREF = 1.8 # Nominal reference voltage
G_THRESH_DEFAULT = 2.5 # which is 1.5 vertical Gs plus 1 for gravity
G_THRESH_COASTING = -0.1 # compare axial accel against this to detect burnout
DECAY=.97 # exponential decay factor: 0=no filtering, 1=infinite
SLOW_DECAY=.2 # less filtering when sampling rate drops (4Hz after 308 sec)
DESC_RATE_DECAY = .98
G_FILT_DECAY = .75 # filter factor for Accel plot; launch detect is unfiltered
DOWN_THESH = 200 # above this alt, don't run arrived-on-ground test
DESC_WINDOW = 50 # #-of-reading backward to look in descent delta math
DOWN_DESC_RATE_THRESH = .5 # ft/sec .. DOWN test true if descent rate < this
DOWN_THRESH_SEC = 2 # sec .. DOWN test must hold off this long after launch
VERBOSE_DEFAULT = 1
EARTH_G = 32.174 # ft/sec/sec = 1 G
STD_ATM_LAPSE_RATE = 0.00198*9./5. # deg F/foot .. for baro temp compensation

# Interpretations of bits of FER (Flight Event Register)
UPPER_BYTE = 256
#FER_BITMASK_LIFTOFF_DETECTED = 128 * UPPER_BYTE # Signed!! sheesh.
FER_BITMASK_ACCEL_LT_ZERO = 32 * UPPER_BYTE
FER_BITMASK_TIME_LT_TVAL = 16 * UPPER_BYTE
FER_BITMASK_TIME_GT_TVAL = 8 * UPPER_BYTE
FER_BITMASK_DELTA_PRESS_LT_PVAL1 = 4 * UPPER_BYTE
FER_BITMASK_DELTA_PRESS_GT_PVAL2 = 2 * UPPER_BYTE
FER_BITMASK_PRESS_INCREASING = 32
FER_BITMASK_LATCH_OUTPUT = 16
FER_BITMASK_PRESS_DECREASING = 8
FER_BITMASK_VEL_LT_400MPH = 4
FER_BITMASK_VEL_LT_0 = 2

FER_ACCEL_APOGEE = (FER_BITMASK_TIME_GT_TVAL \
                     + FER_BITMASK_VEL_LT_400MPH \
                     + FER_BITMASK_VEL_LT_0)

FER_BURNOUT = (FER_BITMASK_ACCEL_LT_ZERO)
               
FER_LOW_ALT_DEPLOY = (FER_BITMASK_TIME_GT_TVAL \
                       + FER_BITMASK_DELTA_PRESS_LT_PVAL1 \
                       + FER_BITMASK_PRESS_INCREASING \
                       + FER_BITMASK_VEL_LT_400MPH)

FER_BARO_APOGEE = (FER_BITMASK_TIME_GT_TVAL \
                    + FER_BITMASK_PRESS_INCREASING \
                    + FER_BITMASK_VEL_LT_400MPH \
                    + FER_BITMASK_LATCH_OUTPUT)

# ----- csv -------------------------------------------------------------------
#

def csv(arg,OFS):

  """If string contain field-separator chars, they must be quoted
     CSV is really a misnomer, since this should work
     for other field separators besides commas (e.g. tabs)
     (Isn't there a standard CSV module in Python now?!!)"""

  ret = str(arg)
  if OFS in ret: # I think you could also handle \n chars in fields here also
    if '"' in ret:
       # Ugh. Emit "" for each "
       ret = ''
       for ch in str(arg):
         if ch == '"':  ret += '""'
         else:          ret += ch
    return '"' + ret + '"'
  return ret


# ----- print1line ------------------------------------------------------------
#

# Indices of print-buffer fld[]
N_COLS = 20 # number of fields in fld[]
# note: the first 8 match what comes out of the Parrot
SEX_IX,AXIAL_G_IX,LATERAL_G_IX,VBAT_IX,VREF_IX,TEMP_IX,ATM_IX,IND_IX,\
 SEX_ADJ_IX,G_AX_ADJ_IX,G_LAT_ADJ_IX,ATM_ADJ_IX,BARO_ASL_ALT_IX,\
 BARO_AGL_ALT_IX,MAX_IX,FILT_ALT_IX,DOWN_IX,FILT_ACCEL_IX,INTACC_VEL_IX,\
 TCOMP_BARO_ALT = range(N_COLS)
PYRO_CONT_IX = VBAT_IX # Parrot v.2: fld[4] is pyro continuity voltage
FER_IX = VREF_IX       # Parrot v.2: fld[4] is Flight Event Register

def print1line(fld,outfile,behavior):

  """Given a list of fields, print them, separated by field-separator.
     In spreadsheet mode, dumps them all; otherwise, be more selective.
     If no outfile, skips writing it."""

  if outfile:
    OFS = behavior['OFS']
    outfile.write( OFS.join( csv(x,OFS) for x in [
     fld[SEX_ADJ_IX],fld[G_AX_ADJ_IX],fld[TEMP_IX],fld[ATM_ADJ_IX],
      fld[BARO_ASL_ALT_IX],fld[BARO_AGL_ALT_IX],fld[FILT_ALT_IX],fld[MAX_IX],
      fld[DOWN_IX],fld[FILT_ACCEL_IX],fld[FER_IX],fld[PYRO_CONT_IX],\
      fld[TCOMP_BARO_ALT]])\
       + "\n")


class Flight:
  def __init__(self):
    self.liftoff_time = 0.0  #sec
    self.liftoff_alt = 0     # calculate this when launch detected
    self.ambient_pad_temp = 0# calculate this when launch detected
    self.alt_hist = []       # All baro alt readings, unfiltered
    self.filt_hist = []      # All baro alt readings, filtered
    self.time_hist = []      # Corresponding times
    self.desc_hist = []      # rate-of-descent history
    self.desc_rate = 0
    self.accel_hist = []     # filtered axial acceleration, in Gs
    self.liftoff_time_ix = 0 # how many samples in *_hist[] before liftoff
    self.time_hist_tplus = []# Corresponding times (starts at t0); also for FER
    # time_hist_tplus[] should be equivalent to time_hist[liftoff_time_ix:]
    self.baro_alt_max = 0
    self.pre_launch = []     # list of fld[]s which need liftoff_alt fixup
    self.max_alt_time = 0    # when apogee happened (after takeoff)
    self.phase = PRE_LAUNCH
    self.ofname = None       # output file name
    self.filt_alt = 0.0      # filtered barometric altitude AGL
    self.filt_accel = 0.0
    self.avg_thrust_accel = 0
    self.burnout_time = 0
    self.n_samples = 0
    self.max_accel = 0
    self.temp_hist = []      # temperatures
    self.intacc_alt = 0      # integrated-accelerometer altitude
    self.accel_alt_hist = [] #  .. and a list of that
    self.tcomp_alt_hist = [] # temperature-compensated baro alt, AGL ft
    self.tcomp_liftoff_alt = 0  # calculate this when launch detected
    self.delta_temp_from_std_atm = 0
    self.intacc_vel = 0      # integrated-accelerometer velocity
    self.acc_vel_hist = []   #  .. a list of that (negative, to match descent)
    self.deploy_vel_acc = 0  # velocity at deployment, integrated accelerometer
    self.deploy_vel_baro = 0 # velocity at deployment, differentiated barometer
    self.landing_alt = 0     # baro altitude at landing
    self.FER_hist = []
    self.FER_accel_apo = 0
    self.FER_accel_apo_time = None
    self.FER_baro_apo = 0
    self.FER_baro_apo_time = None
    self.FER_low_alt_deploy = 0
    self.FER_low_alt_deploy_time = None
    self.FER_liftoff_time = None
    self.main_deploy_alt = 0
    self.FER_burnout_time = None


  # ----- fer_events ----------------------------------------------------------
  #

  def fer_events( self, FER, t):

    """Identify flight event times,
       based on bits in the Flight Events Register.
       Also latch velocity at deployment (accel and baro).
       Returns string of anything interesting to be logged."""

    ret = None

    if FER >=0:  return # liftoff not detected, so nothing else matters

    if self.FER_liftoff_time == None:
      self.FER_liftoff_time = t
      # Transition of self.phase to BOOST happens elsewhere
    if self.FER_burnout_time == None:
      if (FER & FER_BURNOUT) == FER_BURNOUT:
        self.FER_burnout_time = t
        # Transition of self.phase to COAST happens elsewhere
    if self.FER_accel_apo_time == None:
      if (FER & FER_ACCEL_APOGEE) == FER_ACCEL_APOGEE:
        self.FER_accel_apo_time = t # time of deployment initiation
        self.deploy_vel_acc = self.intacc_vel # velocity at deployment (accel)
        if self.phase != DESCENT:
          self.phase = DESCENT
          ret = "Phase -> Descent, time = %g sec" % t
    if self.FER_baro_apo_time == None:
      if (FER & FER_BARO_APOGEE) == FER_BARO_APOGEE:
        self.FER_baro_apo_time = t
        # Capture velocity at deployment (baro)
        # This isn't very accurate, since it looks back a whole second.
        self.deploy_vel_baro = -self.down
        if self.phase != DESCENT:
          self.phase = DESCENT
          ret = "Phase -> Descent, time = %g sec" % t
    if self.FER_low_alt_deploy_time == None:
      if (FER & FER_LOW_ALT_DEPLOY) == FER_LOW_ALT_DEPLOY:
        self.FER_low_alt_deploy_time = t
        # Latch alt, to calculate final-descent speed
        self.main_deploy_alt = self.filt_alt

    return ret


  # ----- temp_comp_baro ------------------------------------------------------
  #

  def temp_comp_baro( self, baro_asl):
    """Temperature compensation of baro readings.
       Requires pre-launch average temperature, temp-adjusted liftoff-altitude,
       and delta_temp_from_std_atm to be known before results are meaningful.
       Returns alt offset adjustment."""

    return (self.delta_temp_from_std_atm/STD_ATM_LAPSE_RATE) \
            * math.log(1.+STD_ATM_LAPSE_RATE*baro_asl/(59.0+460.0+STD_ATM_LAPSE_RATE*baro_asl))


  # ----- post_process_line ---------------------------------------------------
  #

  def post_process_line( self, linebuf, outfile, err, log, behavior):

    """Perform post-processing on Parrot raw flight data.
       String linebuf is input.  Func err(text) reports errors;  func
       log() gives info.  Tolerates outfile==None if no output desired."""

    # Input, Parrot v1 e.g.
    # 1712.010	77	2	3818	1800	781	8162	4
    try:
      fld = linebuf.split() # cut up @ whitespace (leading SPs, tab separators)
    except:
      err("Could not parse line: %s" % repr(linebuf) )
      return
    if len(fld) != 8: # 8 columns
      err("Could not parse line as 8 fields: %s (skipping)" % repr(linebuf) )
      return
    self.n_samples += 1
    sex = float(fld[SEX_IX])      # [ss]s.sss time since log start, NOT liftoff
    axial_g = float(fld[AXIAL_G_IX])/100   # hundredths of a G -> Gs
    lateral_g = float(fld[LATERAL_G_IX])/100 # ditto
    if behavior['parrot_version']>=2:
      vref = NOM_VREF                 # avoid divide-by-zero crash.. use nominal
      FER = int(fld[FER_IX])          # Flight Event Register
      vcont = int(fld[PYRO_CONT_IX])  # pyro continuity voltage
    else: # Parrot v.1
      vbat = float(fld[VBAT_IX])/1000 # Battery voltage .. mV -> Volts
      vref = float(fld[VREF_IX])/1000 # Reference voltage .. mV -> Volts
      FER=''
    temp = float(fld[TEMP_IX])/10     # raw is tenths of a degrees F
    atm = float(fld[ATM_IX])/10000    # 10000 times pressure in atm (absolute)
    ind = int(fld[IND_IX])            # flight index of dumped data

    # Adjusted/calculated columns
    atm_adj = atm*NOM_VREF/vref
    if atm_adj < 0: return # avoid math error
    max = ''
    self.down = ''

    sex_adj = sex - self.liftoff_time
    g_ax_adj = axial_g*NOM_VREF/vref # Axial G-sensor, adjusted
    g_lat_adj = lateral_g*NOM_VREF/vref # Lateral G-sensor, adjusted
    baro_asl_alt = (1-atm_adj**0.190263)*288.15/0.00198122
    baro_agl_alt = baro_asl_alt - self.liftoff_alt

    temp_adj_asl_ft = self.temp_comp_baro(baro_asl_alt) + baro_asl_alt

    self.temp_hist.append( temp) # append to temperature history list

    # Apply digital filters: unit-delay*DECAY + current-value*(1-DECAY)
    # DECAY of .97 works well for the flights I've analyzed, at the
    # initial sampling rate of 200Hz.  When the sample rate drops to
    # 4/sec after about five minutes into the flight, DECAY should decrease.
    # Curiously, it's not a regular 4 Hz, but three quick samples then a pause.
    try:
      delta_t = sex_adj - self.time_hist[-1] # time tween prior two samples
      if delta_t < .01:
        self.filt_alt = self.filt_alt*DECAY + (1-DECAY)*baro_agl_alt
      else:
        self.filt_alt = self.filt_alt*SLOW_DECAY + (1-SLOW_DECAY)*baro_agl_alt
    except IndexError: # first time: self.time_hist has no data yet
      delta_t = 0
    # Filter axial-G data.  Subtract 1.0 G first, to account for gravity
    self.filt_accel = self.filt_accel*G_FILT_DECAY \
                       + (1-G_FILT_DECAY)*(g_ax_adj-1)
    if self.phase < COAST  and  self.filt_accel > self.max_accel:
      self.max_accel = self.filt_accel
    # Integrated-accelerometer altitude
    if delta_t > 0:
      # Get average acceleration between this reading and prior, in ft/sec/sec
      avg_accel = EARTH_G * (self.filt_accel + self.accel_hist[-1]) / 2.0
    else:
      avg_accel = EARTH_G * self.filt_accel / 2.0 # assume prior G reading = 0
      delta_t = .05 # assume nominal
    vel_incr = avg_accel * delta_t # (accel) velocity increase, ft/sec
    self.intacc_vel += vel_incr # new velocity, avg over last reading frame
    self.intacc_alt += self.intacc_vel * delta_t
    self.accel_hist.append(self.filt_accel) # store filtered accel hist
    self.acc_vel_hist.append(-self.intacc_vel) # store integ-accel-velocity
    self.accel_alt_hist.append(self.intacc_alt) # store integrated-accel-alt

    if self.phase >= BOOST:
      self.alt_hist.append(baro_agl_alt) # Add to barometric alt history list
      self.filt_hist.append(self.filt_alt) # and to filtered alt history list
      self.time_hist.append(sex_adj)  # and to parallel time list
      self.tcomp_alt_hist.append(temp_adj_asl_ft - self.tcomp_liftoff_alt)
      if self.filt_alt > self.baro_alt_max:
        self.baro_alt_max = self.filt_alt # latch new max altitude
        self.max_alt_time = sex_adj
      max = self.baro_alt_max

      # Coast phase detection
      if self.phase < COAST:
        if self.filt_accel < G_THRESH_COASTING:
          self.phase = COAST
          log("Phase -> Coasting, time = %g sec" % sex_adj)
          self.burnout_time = sex_adj
          self.avg_thrust_accel = sum(self.accel_hist[self.liftoff_time_ix:])
          self.avg_thrust_accel /= float(len(self.accel_hist) \
                                          - self.liftoff_time_ix)
          # That's in Gs, not ft/sec/sec
      # Descent rate calculation, and arrived-at-ground test
      if len(self.filt_hist) >= DESC_WINDOW:
        this_desc_window = DESC_WINDOW
      else:
        this_desc_window = len(self.filt_hist)
      delta_t = sex_adj - self.time_hist[-this_desc_window]
      climb = self.filt_alt - self.filt_hist[-this_desc_window] # alt. incr.
      # Apply another filter, then look for unchanging descent rate
      new_desc_rate = self.desc_rate*DESC_RATE_DECAY \
                       - (1-DESC_RATE_DECAY)*climb
      self.down = self.desc_rate/delta_t # Ft/sec
      # Only check whether Down when altitude is low (but not boosting)
      if sex_adj > DOWN_THRESH_SEC  and  self.filt_alt < DOWN_THESH \
          and math.fabs(self.down) < DOWN_DESC_RATE_THRESH:
        if self.phase != DOWN:
          if behavior['verbose'] > 0:
            log("Phase -> Down, time = %g sec" % sex_adj)
          self.phase = DOWN
          try: # average the last five baro-alt readings to get landing-alt
            self.landing_alt = sum(self.filt_hist[-5:])/5.0
          except IndexError:
            self.landing_alt = self.filt_alt # shrug
	if behavior['proc_cutoff_downed']:
          return
        # otherwise, continue processing
      self.desc_rate = new_desc_rate
      self.desc_hist.append(self.down) # was: new_desc_rate
      self.time_hist_tplus.append(sex_adj)
    #end if self.phase >= BOOST:

    if behavior['parrot_version'] >= 2:
      self.FER_hist.append(FER)
      str = self.fer_events(FER,sex_adj)
      if str: log(str)

    fld.extend( ['']*(N_COLS-len(fld)))

    fld[SEX_ADJ_IX] = sex_adj
    fld[G_AX_ADJ_IX] = g_ax_adj
    fld[G_LAT_ADJ_IX] = g_lat_adj
    if behavior['parrot_version'] >= 2:
      fld[FER_IX] = FER
      fld[PYRO_CONT_IX] = vcont
    else:
      fld[VBAT_IX] = vbat
      fld[VREF_IX] = vref
    fld[TEMP_IX] = temp
    fld[ATM_ADJ_IX] = atm_adj
    fld[BARO_ASL_ALT_IX] = baro_asl_alt
    fld[BARO_AGL_ALT_IX] = baro_agl_alt
    fld[MAX_IX] = max
    fld[FILT_ALT_IX] = self.filt_alt
    fld[DOWN_IX] = self.down
    fld[FILT_ACCEL_IX] = self.filt_accel
    fld[INTACC_VEL_IX] = self.intacc_vel
    fld[TCOMP_BARO_ALT] = temp_adj_asl_ft - self.tcomp_liftoff_alt

    if self.phase >= BOOST:
      print1line(fld,outfile,behavior)
      return

    # Otherwise, we're in pre-launch phase
    self.pre_launch.append( fld)
  
    # Detect launch
    # Parrot v2 detects launch and reports it in Flight Event Register (FER)
    # It's the MSB of byte of 16-bit int, so the simple test is this: negative?
    if behavior['parrot_version'] >= 2 \
        and behavior['launch_detect_method'] == 'FER':
      if FER == 0:
        return # launch condition not satisfied
      if behavior['verbose']:
        log("Launch FER (%d) detected, time = %g sec" % (FER,sex_adj))
      n = len(self.pre_launch)
      self.liftoff_time_ix = len(self.time_hist)
      self.liftoff_time = sex_adj
    else: # Gotta auto-detect when no FER available, or when user says so
      # three consecutive axial G readings (unfiltered) over 3.5 => launch
      if len(self.pre_launch) <= 3:
        return # too early to test
      if self.pre_launch[-1][G_AX_ADJ_IX] < behavior['G_THRESH'] \
          or self.pre_launch[-2][G_AX_ADJ_IX] < behavior['G_THRESH'] \
          or self.pre_launch[-3][G_AX_ADJ_IX] < behavior['G_THRESH']:
        return # launch condition not satisfied
      if behavior['verbose']:  log("Launch detected, time = %g sec" % sex_adj)
      n = len(self.pre_launch) - 3 # exclude final 3 readings
      self.liftoff_time = self.pre_launch[-3][SEX_ADJ_IX]
      self.liftoff_time_ix = n
    # Launch detected .. Calculate average temp and barometric altitude ASL 
    for pre_fld in self.pre_launch[0:n]:
      self.liftoff_alt += pre_fld[BARO_ASL_ALT_IX]
      self.ambient_pad_temp += pre_fld[TEMP_IX]
    self.liftoff_alt /= n # average
    self.ambient_pad_temp /= n # average
    self.delta_temp_from_std_atm = self.ambient_pad_temp \
                                    - (59.0-STD_ATM_LAPSE_RATE*self.liftoff_alt)
    # Calculate average temp-compensated barometric altitude ASL 
    for fld in self.pre_launch[0:n]:
      fld[TCOMP_BARO_ALT] = self.temp_comp_baro(fld[BARO_ASL_ALT_IX]) \
                             + fld[BARO_ASL_ALT_IX]
      self.tcomp_liftoff_alt += fld[TCOMP_BARO_ALT]
    self.tcomp_liftoff_alt /= n
    if behavior['verbose'] > 1:
      log("Temp-compensated avg pre-launch = %g ft ASL, avg temp = %g F" \
           % (self.tcomp_liftoff_alt,self.ambient_pad_temp))

    self.phase = BOOST

    # Now we can finally flush out the data we've been squirreling away
    # after performing fixup based on liftoff-altitude and -time
    header(self.liftoff_time,self.liftoff_alt,outfile,behavior)
    for fld in self.pre_launch:
      fld[SEX_ADJ_IX] -= self.liftoff_time # i.e. sex_adj
      baro_agl_alt = fld[BARO_AGL_ALT_IX] - self.liftoff_alt
      fld[BARO_AGL_ALT_IX] = baro_agl_alt
      self.alt_hist.append(baro_agl_alt)
      # Await liftoff to start up digital filter on filt_alt; now still 0
      self.filt_hist.append(self.filt_alt) # add to filtered alt history list
      self.time_hist.append(fld[SEX_ADJ_IX])  #  ... and to parallel time list
      #self.accel_hist.append(self.filt_accel)
      fld[FILT_ALT_IX] = self.filt_alt
      fld[TCOMP_BARO_ALT] -= self.tcomp_liftoff_alt
      self.tcomp_alt_hist.append(fld[TCOMP_BARO_ALT])
      print1line(fld,outfile,behavior)
    del self.pre_launch

    return


# ----- header ----------------------------------------------------------------
#

def header(liftoff_time,liftoff_alt,outfile,behavior):

  """"Print header"""

  global N_COLS

  # Header-line listing each column's contents
  fld = ['']*N_COLS
  fld[SEX_IX] = "Raw"
  fld[SEX_ADJ_IX] = 'Tminus'
  fld[G_AX_ADJ_IX] = 'Axial'
  fld[G_LAT_ADJ_IX] = 'Lateral'
  if behavior['parrot_version'] >= 2:
    fld[PYRO_CONT_IX] = 'Cont'
    fld[VREF_IX] = 'FER'
  else:
    fld[VBAT_IX] = 'Bat'
    fld[VREF_IX] = 'Ref'
  fld[TEMP_IX] = 'Temp'
  fld[ATM_ADJ_IX] = 'Pressure'
  fld[BARO_ASL_ALT_IX] = 'Baro Alt'
  fld[BARO_AGL_ALT_IX] = 'Baro Alt'
  fld[MAX_IX] = 'Max Alt'
  fld[FILT_ALT_IX] = 'Filt Alt'
  fld[DOWN_IX] = 'Descent'
  fld[FILT_ACCEL_IX] = 'Accel'
  fld[FER_IX] = 'FER'
  fld[INTACC_VEL_IX] = 'Accel Vel'
  fld[TCOMP_BARO_ALT] = 'Temp-comp Alt'
  print1line(fld,outfile,behavior)

  # Header-line listing units of measure
  fld = ['']*N_COLS
  fld[SEX_IX] = 't'
  fld[AXIAL_G_IX] = 'Gax'
  fld[LATERAL_G_IX] = 'Glat'
  if behavior['parrot_version'] >= 2:
    fld[PYRO_CONT_IX] = 'mV'
    fld[FER_IX] = '' # isn't FER stored elsewhere too?
  else:
    fld[VBAT_IX] = 'V'
    fld[VREF_IX] = 'V'
  fld[TEMP_IX] = 'Tmp'
  fld[ATM_IX] = 'Baro'
  fld[IND_IX] = 'flt'
  fld[SEX_ADJ_IX] = 'sec'
  fld[G_AX_ADJ_IX] = 'Gs'
  fld[G_LAT_ADJ_IX] = 'Gs'
  fld[TEMP_IX] = 'deg F'
  fld[ATM_ADJ_IX] = 'Atm'
  fld[BARO_ASL_ALT_IX] = 'ft ASL'
  fld[BARO_AGL_ALT_IX] = 'ft AGL'
  fld[MAX_IX] = 'ft AGL'
  fld[FILT_ALT_IX] = 'ft AGL'
  fld[DOWN_IX] = 'ft/sec'
  fld[FILT_ACCEL_IX] = 'G'
  fld[FER_IX] = ''
  fld[INTACC_VEL_IX] = 'ft/sec'
  fld[TCOMP_BARO_ALT] = 'ft AGL'
  print1line(fld,outfile,behavior)
