#!/usr/bin/env python

# Featherweight Altimeters' Parrot I/O program -- Jim Yehle -- Oct 25, 2008

VERSION = '1.4'

# To do:
#  'P' echoes final char twice
#  Add a mode to automatically grab all flights
#  Deal with differences in Parrot v.2 output format
#  Document Linux /dev/ttyUSB0
#  Find MacOS serial extension (uspp?)
#  Detect return to terra firma, and cut off dump, like in post-process

# Parts adapted from terminal program from pySerial

import sys, os, threading, getopt
import serial # Chris Liechti's pySerial package

EXITCHARACTER = 'q'   # Type Q to quit
DOWNLOAD_CHAR = 'd'   # Commands Parrot to start dumping flight data
ERASE_CHAR = 'E'
PROGRAM_CHAR = 'P'
PROFILE_NCHARS = 12
HELP_CHAR = 'h'
HELP2_CHAR = '?'
CRLF = "\r\n"
PARROT_DEF_BASENAME = "parrot" # Default dumpfile: "parrot<flight>.txt"
EXT = '.txt'
MAX_FLIGHT_INDEX = 5
MIN_FLIGHT_INDEX = 1
PROFILE_FLIGHT_INDEX = 0

# Choose a platform-dependant way to read single characters from the console
if os.name == 'nt':
  import msvcrt
  def getkey():
    while 1:
      z = msvcrt.getche()
      if z == '\0' or z == '\xe0': #functions keys are two bytes
        z = z * 256 + msvcrt.getche()
      elif z == '\r':
        z = '\n'
      return z

elif os.name == 'posix':
  import termios, sys, os
  fd = sys.stdin.fileno()
  old = termios.tcgetattr(fd)
  new = termios.tcgetattr(fd)
  new[3] = new[3] & ~termios.ICANON & ~termios.ECHO
  new[6][termios.VMIN] = 1
  new[6][termios.VTIME] = 0
  termios.tcsetattr(fd, termios.TCSANOW, new)
  serport = ''    # We'll save the characters typed and add them to the pool.
  def getkey():
    c = os.read(fd, 1)
    #~ c = sys.stdin.read(1)
    return c
  def cleanup_console():
    termios.tcsetattr(fd, termios.TCSAFLUSH, old)
  sys.exitfunc = cleanup_console #terminal modes have to be restored on exit

else:
  raise "Sorry, no implementation for your platform (%s) available."\
        % sys.platform


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

def log( message):

  """Show message on screen.  Console version."""

  print(message)

def err( message):

  """Show error message on screen"""

  sys.stderr.write( message + "\n")


# ----- prompt ----------------------------------------------------------------
#

def prompt( say_what):

  """Ask a question, get an answer.  Console version."""
  global verbose
  verbose_save = verbose
  verbose = 0 # Shut up when I'm talking to you!
  ans = raw_input("\n" + say_what)
  verbose = verbose_save
  return ans


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

def process_line( linebuf, dumpfile):

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

  global downloading
  # 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
  # Detect end-of-data
  if downloading==2: # this means flight-index 0 "profile"
    if sex>=2.0:
      if verbose:  log("\nEnd of data detected")
      return 1
  else:
    # 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 verbose:  log("\nEnd of data detected")
      return 1
  # Write the line to the output file for this flight index
  dumpfile.write( linebuf + '\n')
  return 0


# ----- reader_thread ---------------------------------------------------------
#

def reader_thread():

  """Serial-port reader thread: read, echo, optionally capturing to file"""

  global serport, dumpfile, filename
  global flight_index, verbose, downloading
  linebuf = ''
  while 1:
    ch = serport.read() # one char
    # Append each char to buffer
    # On newline, process the line in buffer & empty it
    if ch == '\n':
      if downloading:
        if process_line( linebuf, dumpfile): # nonzero return .. end of data
          dumpfile.close()
          filename = ''
          downloading = 0
          serport.write('w')  # Put Parrot into wait mode (stop dumping data)
          flight_index = -1
      linebuf = ''
    elif ch != '\r': # don't store CRs
      linebuf += ch
    # Now echo the received char to screen
    if verbose >= 3: # this will show LF as \n etc.
      sys.stdout.write(repr(ch))
    elif verbose:
      sys.stdout.write(ch)
      sys.stdout.flush()


# ----- writer ----------------------------------------------------------------
#

def writer():

  """Send keystrokes to serial port until exit-key pressed.
     Activate flight-capturing when number-keys pressed."""

  global flight_index, filename, dumpfile, downloading, fbase, verbose
  while 1:
    c = getkey()
    if verbose>1:  log("Got key %s\n" % repr(c) )
    if c == EXITCHARACTER:
      if downloading:
        downloading = 0
        serport.write('w')
        dumpfile.close()
      break                       #exit app
    elif c == DOWNLOAD_CHAR:
      if flight_index == -1:
        err("Flight index not specified yet")
        continue
      while 1:
        filename = fbase + str(flight_index) + EXT
        # See if file exists; prompt for overwrite/rename
        extant = False
        try:
          dumpfile = open(filename,"r"); dumpfile.close()
          extant = True
        except:
          break
        ans = prompt( "File %s exists.  Overwrite? [n] " % filename)
        if ans in ("y","Y","YES","yes"):
          break
        fbase = prompt( "Enter file (base) name: ")
      try:
        dumpfile = open(filename,"w")
        if flight_index == 0:  downloading = 2 # special downloading mode
        else:                  downloading = 1
      except:
        err("Can't open file: " + filename)
        filename = ''
        continue
    elif c>=str(MIN_FLIGHT_INDEX) and c<=str(MAX_FLIGHT_INDEX):
      if verbose: log("\nSetting flight index to %s" % c)
      flight_index = int(c)
    elif c==str(PROFILE_FLIGHT_INDEX):
      if verbose: log("\nSetting flight index to %s" % c)
      flight_index = int(c)
    elif c == 'w': # Manual abort of download
      if downloading:
        dumpfile.close()
        filename = ''
        downloading = 0
        flight_index = -1
    elif c == ERASE_CHAR:
      ans = prompt("Really erase current flight? [n] ")
      if not ans in ("y","Y","YES","yes"):
        continue
    elif c in (HELP_CHAR,HELP2_CHAR):
      keystrokes()
    elif c == PROGRAM_CHAR:
      ans = prompt("Really program Parrot flight profile? [n] ")
      if not ans in ("y","Y","YES","yes"):
        continue
      log("Type the %d-character profile string:" % PROFILE_NCHARS)
      verbose_save = verbose
      verbose = 0 # Shut up reader-echo, to avoid double echoing each char
      serport.write(c)
      for i in range(PROFILE_NCHARS):
        c = getkey()
        if verbose_save>1:  log("Got key %s\n" % repr(c) )
        serport.write(c)
      verbose = verbose_save # too quick; 12 char gets echoed twice
    else:
      continue # unrecognized keystroke

    # Send typed character to Parrot via USB (virtual serial port)
    serport.write(c)


# ----- keystrokes, usage -----------------------------------------------------
#

def keystrokes():

  err("""\nKeystrokes:
    h or ? Show this list
    0      Select profile program, a.k.a. flight-index zero, to capture 
    1-5    Select flight number to capture
    d      Begin capture
    w      Truncate capture
    E      Erase current flight
    P      Program Parrot's behavior profile
    q      Quit
""")

def usage():

  """Print a short help message"""

  err("""Parrot-term   version %s

An interface program for Featherweight Altimeters' Parrot model

    USAGE: python %s [options]
    options:
    -f  --file=NAME: file basename for flight downloads (default '%s')
    -p, --port=PORT: port, a number, default = 0 or a device name
    -h, --help:      print this message
    -v, --verbose    increase verbosity (default %d)
    -q, --quiet      set verbosity to zero
""" % (VERSION, sys.argv[0], PARROT_DEF_BASENAME, verbose))

  keystrokes()


# ----- __main__ --------------------------------------------------------------
#

if __name__ == '__main__':
  # set defaults
  port = -1 # e.g. port 0 is COM1 or /dev/tty0
  baudrate = 115200
  rtscts = 0
  xonxoff = 0
  fbase = PARROT_DEF_BASENAME
  flight_index = -1    # -1 = not set yet. 1-5 when in file-dump mode.
  filename = ''
  verbose = 1
  downloading = 0

  log("Parrot-term v. %s" % VERSION)

  # Process command line
  try:
    opts, args = getopt.getopt( sys.argv[1:], "hf:p:vq",
                                ["help","file=","port=","verbose","quiet"] )
  except getopt.GetoptError:
    usage()     # Print help information
    sys.exit(2) #  .. and exit(error)
  
  for o, a in opts:
    if o in ("-h", "--help"):
      usage()
      sys.exit(0)
    elif o in ("-p", "--port"):     #specified port
      try:
        if a.startswith("COM") or a.startswith('com'): # Windows
          port = int(a[3:]) - 1
        elif a.startswith("/dev"): # Linux device name
          port = a
        else: # Unix/Linux (Mac OS?) .. just an integer?
          port = int(a)
      except:
        err('Unrecognized serial port "%s"' % a)
        sys.exit(3)
    elif o in ("-v", "--verbose"):
      verbose += 1
    elif o in ("-q", "--quiet"):
      verbose = 0
    elif o in ("-f", "--file"):
      fbase = a

  if port == -1:
    usage()
    sys.exit(3)
  try:
    serport = serial.Serial(port, baudrate, rtscts=rtscts, xonxoff=xonxoff)
  except:
    err("Could not open port %s" % port)
    sys.exit(1)
  log("Type q to quit")
  r = threading.Thread(target=reader_thread)
  r.setDaemon(1)
  r.start()
  writer()
