# -*- coding: utf-8 -*-
#
# trimble_menu.py   - quick menu based routines to check if trimble is up and running
#                         and to "size" the responses for binary commands
#
# mit haystack obs
# rps 6/18/2015

#
"""
  commands:
    whoami                   -     reads hostname
    init_serial              -     init serial port
    read_serial              -     serial port read, if debug then print
    readOrTmo                -     serial port read, timeout set in open
    cmd_binary_listen        -     listens, file writes
    bin_forever              -     loops forever (or if test, then iteration count)
    close_serial             -     close serial port

  classes:
    theMenu
      menu
    kbhit
      set_normal_term
      getch
      getarrow
      kbhit
    ExceptionString

  other:
    kbtest                      - test of kbhit
    kbpause                     - see if anyone is listening
    parse_command_line          - accept flag that indicates there's an ini file
    readIniValues               - get values from ini file
    main_program                - daemonized entry point

"""
import sys
import os
import serial          # http://pyserial.sourceforge.net/shortintro.html
import string
import time
import scanf           # https://hkn.eecs.berkeley.edu/~dyoo/python/scanf/
import datetime
#import mahaliSiteIni  # brings in gevents for logger, fails python v2.7.9
import daemon          # in preparation for running as a daemon
import optparse        # command line parsing
import iniUtilities    # ini file parsing (reworks logger to avoid gevents)
import socket          # for id'ing host 

import os

# for kbhit   
import termios
import atexit
from select import select 

#
#-----------------------------------------------------------------------------------
#
#            GLOBALS
#
#-----------------------------------------------------------------------------------
#
# The first two variables affect the function "bin_forever" only
#
DEBUG = False                       # if set true, then loop exits after ITERATION
ITERATIONS = 1                      # if DEBUG then cycle ITERATIONS and out

MENU = True                         # if set false, read loop runs in background
VERBOSE = False                     # increases printing
OPEN_TIMEOUT = 2.0                  # initial value
READ_TIMEOUT = (5*OPEN_TIMEOUT)     # open_timeout isn't long enough
UNKNOWN_SITE = 'unkn'               # replace with ini file

DEFAULT_PORT = '/dev/ttyUSB1'
DEFAULT_BAUDRATE = 115200


g_serial_port = DEFAULT_PORT   # mahali port name, set on intialization
g_baud_rate = DEFAULT_BAUDRATE     # gps unit baudrate
#
#-------------------------------------------------------------------------
#
class ExceptionString(Exception):
  def __str__(self):
      return repr(self.args[0])  # caller must use str(ExceptionString(<excep164tion>))
# end class ExceptionString 

#
#-----------------------------------------------------------------------------------
#
def whoami(quiet_f=False):
  develop_f = False
  thisHost = socket.gethostname()
  if (thisHost=="kronos"):
    if (not quiet_f):
      print "This program is running on:",thisHost
    develop_f = True
  else:
    if (thisHost.find("mahali")>=0):   # mahali hostname begins with "mahali
      if (not quiet_f):
        print "This program is running on mahali"
    else:                              # unknown host, error.
       print "Unknown host:",thisHost
  return develop_f
# end whoami
#
#-----------------------------------------------------------------------------------
#
def init_serial(port=g_serial_port,baud=g_baud_rate):
  err_f = False
  ser = None
  print "using port:",port
  print "setting baud rate:",baud
  try:
    ser=serial.Serial(port     = port,
                      baudrate = baud,
                      parity   = serial.PARITY_NONE,
                      stopbits = serial.STOPBITS_ONE,
                      bytesize = serial.EIGHTBITS,
                      timeout  = 0.1) # was OPEN_TIMEOUT
  except Exception as eobj:
    print "Exception in Serial: ",eobj
    err_f = True
  else:
    ser.close()   # may have been opened previously
    ser.open()
  return err_f,ser
# end init_serial
#
#-----------------------------------------------------------------------------------
#
def readOrTmo(ser,tmo_val=READ_TIMEOUT):
  err_f = False
  line =""
  try:
    line = ser.read(1024)
  except Exception as eobj:
    esobj = ExceptionString(exobj)
    errStr = str(esobj)
    if (errStr.find('device disconnected') >=0):
       print errStr
       print "Is the receiver relay off?"
    else:
       print eobj
       err_f = True
  return err_f,line
# end readOrTmo
#
#-----------------------------------------------------------------------------------
#
def read_serial(ser,              # serial port
                count,            # count to read, if -1 then listen for seconds
                ascii_f=True,     # response is known to be ascii
                print_f=True,     # print response
                filter_f=False):   # if set, filters message 
  jj = 0          # line counter
  tmo_ctr = 0          # tmo counter
  tmo_limit = 10   # max contiguous timeouts before giving up
  retmsg = ""
  first_f = True     # reduces messages
  while jj < count:
     tmo_f = False                  # reset timeout detector
     err_f,imsg = readOrTmo(ser)
     if (len(imsg) > 0):
       tmo_ctr = 0        # reset timeout counter
       #
       # Need to properly size the ascii buffer for binary reads
       #
       if print_f:
         omsg = "read(%d) length %d: %s"%(jj+1,len(imsg),imsg)
         if (ascii_f):
           print omsg,              # ',' because already has linefeed
         else:
           ii = 0
           while (ii<len(imsg)):
             print hex(ord(imsg[ii])),
             ii = ii + 1
           # end while
       retmsg = retmsg + imsg     # append
     else:
       tmo_ctr = tmo_ctr + 1    # increment timeout counter
       if print_f:
         print "read timeout"
       if (tmo_ctr == tmo_limit):    # max timeouts
         break                  # give up - no more data
     jj = jj + 1
  # end while 
  return retmsg
# end read_serial
#-----------------------------------------------------------------------------------
#
def cmd_binary_listen(ser,              # serial port handle
                      filename,         # file to write to
                      dur_sec=60):      # how long to listen  
  err_f = False
  quit_f = False
  #
  verbose_f = False       # Note: All data will be dumped to console             
    
  fpw = None
  try:
    fpw = open(filename,"w")
  except Exception as eobj:
    print "cannot open",filename
    print eobj
    err_f = True      # error caught further down 
    return err_f
  
  # still here
  if (verbose_f):
    print "opened:",filename

  ii = 0
  
  baseTime = time.time()         # base time for duration calculation
  flushTime = baseTime           # flush base time for watching file grow
  flush_sec = 1

  jj = 0           # count bytes per line
  theData = ""     # init read
  first_f = True
  while (not err_f):                      # inner loop 
    #if (kbpause(0.1,False)):                # user quit
    #   print "user break"
    #   quit_f = True
    #   break
    nowTime = time.time()                 # test loop duration
    if ((nowTime - baseTime) > dur_sec):
      break
    err_f,theValue=readOrTmo(ser)

    if theValue=="":                      # end of data
      continue                            # keep trying until timeout
    if (verbose_f):
      if ((jj%16)==0):                    # N/4 blocks of 4 per line
        omsg = "\n%0.2d: "%(jj/8)         # hex format
        print omsg,                       # word count
      print "%0.2X"%(ord(theValue)),      # value plus space because of comma
      jj = jj + 1
      if (jj %4) == 0:                    # space every 4 chars
        print " ",
    # endif verbose
    theData = theData + theValue          # store the data

    nowTime = time.time()                 # test loop duration
    if ((nowTime - baseTime) > dur_sec):
      break                               # done with collection
    if ((nowTime - flushTime) > flush_sec):   # time to flush buffer
      flushTime = nowTime                    # reset clock
      idx = 0
      #endif first time
      fpw.write(theData[idx:])              # save to file, binary
      fpw.flush()                           # allows watching file grow
      theData =""                           # re-init buffer
    # endif time for file flush
  #end while inner loop
  if (verbose_f):
    print                             # formatting neatness
                                        
  fpw.write(theData)                  # save to file, binary  

  fpw.close()
  statinfo = os.stat(filename)
  if (statinfo.st_size == 0):
    print "Zero length file:",filename
    err_f = True
  return err_f,quit_f
# end cmd_binary_listen
#
#-----------------------------------------------------------------------------------
#
def bin_forever(ser,           # the open serial device
                port,          # serial port
                baud,          # serial baud rate
                test_f=True,   # when True, then not forever
                iterations=10): # test iterations
  global UNKNOWN_SITE
  #
  err_f = False
  dur_sec = 3600                           # 1 hour = 3600 seconds
  loop_max = iterations                    # max iterations for testing
  if (test_f):                             # if testing
    print "test mode, iterations=",loop_max
    dur_sec = 600                           # seconds file
    print "looping for:",dur_sec*loop_max," seconds"
  
  sitename = UNKNOWN_SITE  # replace this with value from ini file
  path = '/data/raw'
  if (ser ==None):
    err_f,ser=init_serial(port,baud)  # device and baudrate picked up in __main__
    if err_f:
       print "unable to open serial device"
       return err_f
  # endif error
  #
  #
   #
  ii = 0           # test iterator
  while (True):    # continue on errors

    utcnow_dt = datetime.datetime.utcnow()  
    year        = utcnow_dt.strftime('%Y')
    day_of_year = utcnow_dt.strftime('%j')
    hour        = utcnow_dt.strftime('%H')
    minute      = utcnow_dt.strftime('%M')
      
    utcsec = (utcnow_dt - datetime.datetime(1970, 1, 1)).total_seconds()
    filename = sitename+"@"+year+"_"+day_of_year+"_"+ str(int(utcsec)) +".obs"
    fullPath = path + "/" + filename
    print "filename =", fullPath

    err_f,quit_f = cmd_binary_listen(ser=ser,                        # serial port
                                     filename  = fullPath,  
                                     dur_sec   = dur_sec)           # duration
    if (quit_f):
      break

    if (test_f):
       ii = ii + 1
       print "iteration=",ii
       if (ii >= loop_max):
         print "test complete"
         break
 
  # end while

  print "done."
 
# end bin_forever
#
#-----------------------------------------------------------------------------------
#
def close_serial(ser):
  if (ser != None):
    ser.flush()
    ser.write('unlogall\r\n')        # leave in a friendly mode
    ser.close()
  else:
    pass                             # can't close what was never opened
# end close_serial
#
#-------------------------------------------------------------------------
#
def main_program(serial_port,baud_rate,verbose_f=False):

  global DEBUG
  global ITERATIONS   # ignored if not DEBUG
  if (DEBUG):
    print "trimble monitoring is in DEBUG mode, exits early"

  test_f = DEBUG  
  iterations = ITERATIONS
  #
  # serial port will be opened inside call
  # done this way to allow sharing the function with the menu
  #
  ser = None           
  try:
   bin_forever(ser=ser,
               port=serial_port,
               baud=baud_rate,
               test_f=test_f,
               iterations=iterations)  
  except KeyboardInterrupt:
    if (verbose_f):
      print "Control C detected, closing open handles"
      close_serial(ser)
# end main_program
#
#-----------------------------------------------------------------------------------
#
#
# --------------------------------------------------------------------------
# --------------- class for direct commanding via menu  --------------------
# --------------------------------------------------------------------------

class theMenu:  # i/o blocks -> can't use w/gevents or threads

  global DEBUG
  global ITERATIONS

  #
  #------------------------------------------------------
  #
  def __init__(self,serial_port,baud_rate):
   self.serial_port = serial_port
   self.baud_rate = baud_rate
   #
   # command delimiter, sometimes ',' is used by device
   # 
   self.CDL = ';'                      

  #
  #------------------------------------------------------
  #
  def getFromUser(self,prompt):
    retval = raw_input(prompt+" --> ")
    retval = retval.strip()
    return retval
  #
  #------------------------------------------------------
  #
  def menu(self): 
    
    err_f = False
    result = None
    ser = None
 
    while 1:
      print 
      print "welcome to the trimble menu"
      print "1: open serial port [ <port> [%s <baudrate>]]"%(self.CDL)
      print "2: TBD"
      print "3: 3%s<count> read <count> messages from trimble ASCII"%(self.CDL)
      print "4: TBD"
      print "5: TBD"
      print "6: TBD"
      print "7: Read in a loop, store to file, forever"
      print "8: 8%s<count> read <count> messages from serial BINARY"%(self.CDL)
      print "9: TBD"
      print "10: close serial"
      print "99: exit"
 
      line = raw_input("input --> ")  # raw_input adds quotes
    
      if (line=='\n'): # skip empty lines
        print "<no value entered>"
        continue
      
      params = line.split(self.CDL)  # 
      
      ii = 0
      while (ii < len(params)):
        params[ii] = params[ii].lstrip(" ")  # remove space from left
        params[ii] = params[ii].rstrip(" ")  # remove space from right
        ii = ii + 1
          
      try:  
        ival = int(params[0])      # decode menu first parameter
      except ValueError, emsg:
         print "unrecognized value"
         continue                  # try again

      if (ival == 1):                        # 1 - open
        err_f = False
        port = self.serial_port
        baudrate = self.baud_rate
        if (len(params) > 1):
          port = params[1]
          self.serial_port = port
        if (len(params) > 2):
          try:
            baudrate = int(params[2])
          except Exception as eobj:
            print "int conversion exception:",eobj
            print "using baudrate:", self.baud_rate
          else:
            self.baud_rate = baudrate
        err_f,ser = init_serial(self.serial_port,self.baud_rate)
        if (err_f):
           print "device not open"
        else:
           print "ok."
      
      elif (ival == 2):                      # 2 - write   
        pass

      elif (ival == 3):                      # 3 - read N ASCII
        err_f = False             
        if (ser == None):
          print "device closed, nothing to read"
        else:
          if (len(params) < 2):
             count = 1
          else:
             try:
               count = int(params[1])
             except Exception as eobj:
                print eobj
          print "count=",count
          read_serial(ser,count)    
          print "ok."

      elif (ival == 4):                      # 4 -TBD 
        err_f = False            
        ipass

      elif (ival == 5):                    # 5 - TBD    
        pass

      elif (ival == 6):                    # 6 - TBD    
        pass

      elif (ival == 7):                   # 7 - bin forever
        bin_forever(ser=ser,
                    port=self.serial_port,baud=self.baud_rate,
                    test_f=True,iterations=ITERATIONS)  

      elif (ival == 8):                  # 8 read N BINARY
        err_f = False             
        if (ser == None):
          print "serial port closed, nothing to read"
        else:
           if (len(params) < 2):
             count = 1
           else:
             try:
               count = int(params[1])
             except Exception as eobj:
                print eobj
                print "setting count to 1"
             # end exception
           # end else
           read_serial(ser=ser,count=count,ascii_f=False) 
           print "ok."
        # end else
      elif (ival == 9):                     # 9 - TBD
        pass           

      elif (ival == 10):                   # 10 - close serial port 
        close_serial(ser)            
        ser = None
        print "ok."
        
      elif ival == 99:
        print 99          
        print "bye"
        close_serial(ser)   # on close, also close serial port    
        ser = None
        break
      else:
        print "invalid entry"
    
      sys.stdout.write("enter any key to continue ")
      err_f = 0      # reset errors
      try: 
        char = sys.stdin.read(1)
      except IOError, emsg:
        print "IOError: ", emsg
      #end while
  #end menu
#end theMenu
# --------------------------------------------------------------------------
# ---------------------------- class KBHit  --------------------------------
# --------------------------------------------------------------------------
class KBHit:
    
    def __init__(self):
        '''Creates a KBHit object that you can call to do various keyboard things.
        '''

        if os.name == 'nt':
            pass
        
        else:
    
            # Save the terminal settings
            self.fd = sys.stdin.fileno()
            self.new_term = termios.tcgetattr(self.fd)
            self.old_term = termios.tcgetattr(self.fd)
    
            # New terminal setting unbuffered
            self.new_term[3] = (self.new_term[3] & ~termios.ICANON & ~termios.ECHO)
            termios.tcsetattr(self.fd, termios.TCSAFLUSH, self.new_term)
    
            # Support normal-terminal reset at exit
            atexit.register(self.set_normal_term)
    
    
    def set_normal_term(self):
        ''' Resets to normal terminal.  On Windows this is a no-op.
        '''
        
        if os.name == 'nt':
            pass
        
        else:
            termios.tcsetattr(self.fd, termios.TCSAFLUSH, self.old_term)


    def getch(self):
        ''' Returns a keyboard character after kbhit() has been called.
            Should not be called in the same program as getarrow().
        '''
        
        s = ''
        
        if os.name == 'nt':
            return msvcrt.getch().decode('utf-8')
        
        else:
            return sys.stdin.read(1)
                        

    def getarrow(self):
        ''' Returns an arrow-key code after kbhit() has been called. Codes are
        0 : up
        1 : right
        2 : down
        3 : left
        Should not be called in the same program as getch().
        '''
        
        if os.name == 'nt':
            msvcrt.getch() # skip 0xE0
            c = msvcrt.getch()
            vals = [72, 77, 80, 75]
            
        else:
            c = sys.stdin.read(3)[2]
            vals = [65, 67, 66, 68]
        
        return vals.index(ord(c.decode('utf-8')))
        

    def kbhit(self):
        ''' Returns True if keyboard character was hit, False otherwise.
        '''
        if os.name == 'nt':
            return msvcrt.kbhit()
        
        else:
            dr,dw,de = select([sys.stdin], [], [], 0)
            return dr != []
   # end kbhit
# end class KBHit
#
#---------------------------------------------------------
#
#          
#-------------------------------------------------------------------
#
def parse_command_line():
    #
    # trimble_menu [-i ] 
    #
    #
    parser = optparse.OptionParser()
    parser.add_option("-i", "--ini_flag",action="store_true", 
                      dest="ini_f", default=False,help="ini flag.")

    
    (options, args) = parser.parse_args()
    
    return (options, args)
# end parse_command_line
#          
#-------------------------------------------------------------------
#
def kbtest():

  kb = KBHit()

  print 'Hit any key, or ESC to exit'
  sys.stdout.flush()

  while True:

      if kb.kbhit():
          c = kb.getch()
          if ord(c) == 27: # ESC
              break
          print(c)
             
  kb.set_normal_term()
  
# end kbtest
#          
#-------------------------------------------------------------------
#
def kbpause(wait_sec,verbose_f=True):

  break_f = False
  kb = KBHit()

  if (verbose_f):
    print'Hit any key, or wait %d seconds'%(wait_sec)
  sys.stdout.flush()

  timeStart = time.time()           # start time
  while True:

      if kb.kbhit():
        break_f = True
        break
             
      timeNow = time.time()        # moving time
      if ((timeNow - timeStart) > wait_sec):
         if (verbose_f):
           print 'Continuing'
         break

  kb.set_normal_term()
  return break_f
# end kbpause
#          
#-------------------------------------------------------------------
#
#              MAIN
#          
#-------------------------------------------------------------------
#

if __name__ == '__main__':
  options = None
  args = None
  options, args = parse_command_line() # parse command line options
  ini_f = options.ini_f

  #
  # if ini flag not set, then use hard coded defaults
  # else grab  paths from  ini file, the path to the ini file is hard coded
  #
  err_f = False
  g_baud_rate    = DEFAULT_BAUDRATE
  g_serial_port  = DEFAULT_PORT   

  if (MENU):
    
    #kbtest()  # test kbhit() function

    menu = theMenu(g_serial_port,g_baud_rate)
    menu.menu()
  else:
    with daemon.DaemonContext():
      main_program(g_serial_port,g_baud_rate,verbose_f=VERBOSE)


