import os
import datetime
import random
import time
import threading
import socket
import signal
import sys
import json

# RXTools.py is a collection of helper functions used to 
# interface to Trimble receivers. It is maintained in CVS
# in "GPSTools/pythonTools/
#
# Under Linux/Bash to get this script to use the version 
# from CVS add the following to your .bashrc file:
#
# export PYTHONPATH=$HOME/GPSTools/pythonTools/
#
# This assumes you have checked out GPSTools to ~/GPSTools,
# for example:
#
# cd ~
# cvs co GPSTools
#
import RXTools
import leapseconds

GotPVTTime = False
skipFWBuild = False
RebootTime = [] # if using DCOL reboot, what time was reset sent?
PowerUpTime = []
RXUp = False
sendEphFlag = False

def signal_handler(signal, frame):
  global Running
  global TestActive
  print('You pressed Ctrl+C!')
  Running = False
  TestActive = True
  time.sleep(5)
  thread.join()
  EphThread.join()
  print("Exiting")
  sys.exit(0)

# Get the current GPS Time. Return the GPS week number / week seconds.
def getTime():
  # startTime is the start of GPS time
  startTime = datetime.datetime(1980,1,6)

  now = leapseconds.utc_to_gps(datetime.datetime.utcnow())
  delta_time = (now - startTime).total_seconds()
  weekNum = int(delta_time/(86400*7))
  Secs    = delta_time - weekNum*86400*7
  return weekNum,Secs

# Once a week we'll build and install new firmware. To determine
# whether we need to install new firmware we check the week number
# and compare to a file where we store the week number when we 
# last updated the firmware
def checkFirmwareInstall():
  if skipFWBuild:
    return
  FWWeekFile = 'FW_week.txt'
  if(os.path.isfile(FWWeekFile)): 
    with open(FWWeekFile,'r') as f:
      week = int(f.readline())
  else:
    week = 0

  [CurrentWeek, CurrentSecs] = getTime()

  print("%d %d" % (CurrentWeek, week))

  if(CurrentWeek != week):
    FWRoot = 'coreBuild'
    RXTools.BuildFW(FWRoot,target)
    try:
      RXTools.upgradeFW(RXIP,"admin","password",
                        FWRoot + '/' + target + '_output/'  + target + '.timg',
                        False)
    except:
      print("ToDo - Python 3 throws an exception")

    fid = open(FWWeekFile,'w')
    fid.write("%d" % CurrentWeek)
    fid.close()
    # Give the receiver time to boot
    time.sleep(45)

def GpsDt(week_secs_now, week_secs_old):
  """\
  Input: week_secs_now = [GPS week, GPS secs in week]
         week_secs_old = [GPS week, GPS secs in week]
  Returns week_secs_now - week_secs_old in seconds
  """
  dt = (week_secs_now[0] - week_secs_old[0])*60*60*24*7.
  dt += week_secs_now[1] - week_secs_old[1]
  return dt

# This just injects the ephemeris into the receiver
def sendEphWorker():
  global RefEphAlm 
  global sendEphFlag

  while(True):
    # The RX is up if we are injecting eph/alm push it now
    if((sendEphFlag == True) and (fastInject == True)):
      try:
        if(RefEphAlm):
          RXTools.sendDColStartupData( RXIP, SendEphPort, RefEphAlm)
          sendEphFlag = False
          print("Ref Eph/Alm Sent")
        else:
          print("No reference Eph/Alm")
      except:
        print("Eph/Alm Inject Failed")
    time.sleep(0.1)


# Worker is in a thread it will loop around for ever. It does the
# following
#
# 1. Loop around pinging the receiver with a 1 second delay for 
#    each loop. Once we can ping the receiver continue
# 2. Establish a TCP/IP connection to the receiver
# 3. Loop around until the main thread has changed the TestActive 
#    to False. The main thread sets TestActive to False before it 
#    power cycles the unit and re-enables after the unit is powered
#    up. This should allow us to cleanly connect/disconnect to the
#    TCP/IP port of the receiver.
# 4. In the main loop with TestActive read from the TCP/IP port,
#    find the NMEA::GGA, when we get this data check for valid data.
#    The first time we get data after TestActive goes True is the 
#    "ConnectTime" total time from when power is reapplied to when we
#    can connect to the receiver. We also see when we first get a 
#    PVT solution by testing the contents of the NMEA message
#
#    ToDo - when we get a serial to TCP/IP adaptor we should be able to
#    simplify this and effectively connect directly to the receiver's
#    serial port. This should come back faster than the network stack.
#    We may monitor both!
#
def worker():
  global GotPVTTime
  global PowerUpTime
  global RXUp
  global RefEphAlm 
  global sendEphFlag
  RXUp = False
  print(getTime(), "Worker Start")

  while(Running == True):
    connectStr = ""
    pvtStr = ""
    saveTime = False

    # If we want to inject eph/alm from a reference receiver to 
    # the DUT grab the eph/alm now from the reference receiver.
    # If this fails we'll use the data from last time... if
    # this fails for a long time we'll have data but it will have 
    # timeed out.
    if(fastInject == True):
      try:
        RefEphAlm = RXTools.getDColStartupData( refIP, refPort )
        print("Got Eph/Alm")
      except:
        print("Get Eph/Alm from Ref Failed")
        pass

    # Loop around until the TCP/IP stack is up
    while(RXUp == False):
      #if(RXTools.ping(RXIP) != True):
      pingRet = RXTools.ping(RXIP)
      if(pingRet != True):
        print(pingRet)
        RXUp = False;
        time.sleep(0.1)
      else:
        print(getTime(), "Ping Passed")
        RXUp = True
    
    # The receiver is up
    # Kicks off the send ephemeris task which will send 
    # the ephemeris to the receiver in a seperate task
    sendEphFlag = True

    GotConnectTime = False
    GotPVTTime = False

    try:
      # The TCP/IP stack should be up. Now connect to a port on the
      # receiver that is streaming 10Hz NMEA data. 
      tcp_client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
      #tcp_client.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
      tcp_client.settimeout(5.0)
      # Establish connection to TCP server
      tcp_client.connect((RXIP, 5018))

      # Loop around until the main thread sets TestActive to False. It 
      # does this just before it power cycles the receiver. It then sets
      # to True after power is reapplied (we loop around the main loop 
      # here and will ping the receiver until we get a response)
      try:
        # the makefile() call automatically splits on line boundaries, so
        # it is easier to parse NMEA
        for data in tcp_client.makefile('r'):
          if not TestActive or not Running:
            break

          # We have some data
          TimeNow = getTime()
          now = leapseconds.utc_to_gps(datetime.datetime.utcnow())

          # We have a "power up time"
          if(len(PowerUpTime) > 0):
            # Find the first connection time (boot + TCP/IP stack up)
            if(GotConnectTime == False):
              connectTime = TimeNow
              print("ConnectTime = ",TimeNow[0],TimeNow[1],GpsDt(TimeNow,PowerUpTime))
              print(GotPVTTime,PowerUpTime,RXUp,sendEphFlag,RXUp)
              if useDcolReset:
                print("  RebootTime = %.1f" % GpsDt(PowerUpTime,RebootTime))
              connectStr = ("%s %s %s" % (TimeNow[0],TimeNow[1],GpsDt(TimeNow,PowerUpTime)))
              if useDcolReset:
                connectStr += (" %s" % (GpsDt(PowerUpTime,RebootTime)))
              #fid = open("results/" + str(now.year) + '-' + str(now.month).zfill(2) + '-' + str(now.day).zfill(2) + '-Connect.txt' ,'a')
              #fid.write("%s %s %s" % (TimeNow[0],TimeNow[1],GpsDt(TimeNow,PowerUpTime)))
              #if useDcolReset:
              #  fid.write(" %s" % (GpsDt(PowerUpTime,RebootTime)))
              #fid.write("\n")
              #fid.close()
              GotConnectTime = True

            # Find the first PVT solution (power applied thru first PVT
            # solution
            if(GotPVTTime == False):
              tokens = data.split(',')
              if( (tokens[0][0:2] == '$G') and (tokens[0][3:] == 'GGA') ):
                if(tokens[7]):
                  pvtTime = TimeNow
                  print("PVTTime = ",TimeNow[0],TimeNow[1],GpsDt(TimeNow,PowerUpTime),tokens[1],tokens[6],tokens[7])
                  print(GotPVTTime,PowerUpTime,RXUp,sendEphFlag,RXUp)
                  GotPVTTime = True
                  pvtStr = ("%s %s %s %s %s" % (TimeNow[0],TimeNow[1],GpsDt(TimeNow,PowerUpTime),tokens[6],tokens[7]))
                  #fid = open("results/" + str(now.year) + '-' + str(now.month).zfill(2) + '-' + str(now.day).zfill(2) + '-PVTTime.txt' ,'a')
                  #fid.write("%s %s %s %s %s\n" % (TimeNow[0],TimeNow[1],GpsDt(TimeNow,PowerUpTime),tokens[6],tokens[7]))
                  #fid.close()

            if( (GotConnectTime == True) and (GotPVTTime == True) and (saveTime == False) ):
              saveTime = True
              fid = open("results/" + str(now.year) + '-' + str(now.month).zfill(2) + '-' + str(now.day).zfill(2) + '-Connect.txt' ,'a')
              fid.write(connectStr + '\n')
              fid.close()
              
              fid = open("results/" + str(now.year) + '-' + str(now.month).zfill(2) + '-' + str(now.day).zfill(2) + '-PVTTime.txt' ,'a')
              # The time to PVT and connect are almost the same, this probably means we were positioning
              # by the time we managed to connect to the receiver. Note this as we haven't correctly 
              # measured the TTFF
              if((pvtTime[1] - connectTime[1]) < 1.0): 
                fid.write(pvtStr + ' Timeout\n')
              else:
                fid.write(pvtStr + '\n')
              fid.close()

      except:
        print("Socket Error")

      # Close the socket
      tcp_client.close()
      print("Socket Closed")
    except:
      print("High level socket issue")

    if useDcolReset:
      # Wait here until the main thread has reapplied power
      while(TestActive == False and Running):
        time.sleep(0.1)

      # It may take a while for the DCOL reset to go through, so wait
      # until the receiver drops off the net.
      RXUp = True
      ping_secs = time.time()
      while RXUp and Running:
        if(RXTools.ping(RXIP) != True):
          RXUp = False;
        else:
          RXUp = True
          if time.time() - ping_secs > 1.0:
            print("Ping Good - waiting for DCOL reboot to finish...")
            ping_secs = time.time()
          time.sleep(0.1)
      PowerUpTime = getTime()
      if Running:
        TimeNow = getTime()
        print("Network Down = ",TimeNow[0],TimeNow[1],GpsDt(TimeNow,RebootTime))
    else:
      # Wait until power-strip has reapplied power
      RXUp = False
      time.sleep(0.1)

      # Wait here until the main thread has reapplied power
      while(TestActive == False and Running):
        time.sleep(0.1)


######################################################
#                      Code Start
######################################################

# Make sure there is no proxy setting. We've noticed
# issues if this is proxy.trimble.com:3128, something
# appears to get cached and it fails after many days
# Make sure to enable NMEA GGA 10Hz on TCP port 5018
os.environ["http_proxy"] = ""

if(len(sys.argv) != 2):
  print("Usage: python PowerTest.py config.json")
  print("Where: config.json defines the receiver etc\nMake sure to enable NMEA GGA 10Hz on port 5018\n")
  exit(1)
 
filename = sys.argv[1] 
if(not os.path.isfile(filename)):
  print("Can't open %s" % filename)
  exit(2)

#
# Read and parse the config file
#
jsonFile = open(filename)
jsonStr = jsonFile.read()
testData = json.loads(jsonStr)

RXIP      = testData['RXIP']

if('PowerIP' in testData):
  usePowerStrip = True
  PowerIP   = testData['PowerIP']
  PowerPort = int(testData['PowerPort'])
  if((PowerPort < 1) or (PowerPort > 5)):
    print("Power port should be 1-5")
    exit(3)
else:
  usePowerStrip = False

useDcolReset = False
if 'useDcolReset' in testData:
  useDcolReset = True

if( (usePowerStrip == False) and (useDcolReset == False) ):
  print("Reset method missing")
  exit(5)
elif( (usePowerStrip == True) and (useDcolReset == True) ):
  print("DCOL and Power reset cannot both be enabled")
  exit(6)

if 'skipFWBuild' in testData:
  skipFWBuild = True
else:
  if('target' in testData):
    target = testData['target']
  else:
    print("Missing firmware target")
    exit(4)

if('eraseEph' in testData):
  eraseEph = int(testData['eraseEph'])
else:
  eraseEph = False

# When set to true will inject ephemeris etc data from 
# another receiver into this receiver
if('fastInject' in testData):
  fastInject  = int(testData['fastInject'])
  refIP       = testData['RefIP']
  refPort     = int(testData['RefPort'])
  SendEphPort = int(testData['SendEphPort'])
else:
  fastInject= False

if('eraseAlm' in testData):
  eraseAlm = int(testData['eraseAlm'])
else:
  eraseAlm = False


randStart = int(testData['randStart'])
randEnd   = int(testData['randEnd'])

if('lastPos' in testData):
  changeLastPos = True
  lastPosLat,lastPosLon,lastPosHgt  = testData['lastPos'].split(',')
else:
  changeLastPos = False

if('erasePos' in testData):
  erasePos = int(testData['erasePos'])
else:
  erasePos = False

timeout  = int(testData['timeout'])

# Make sure the output directories exist
directory = 'results'
if(not os.path.exists(directory)):
  os.makedirs(directory)
directory = 'data'
if(not os.path.exists(directory)):
  os.makedirs(directory)

TestActive = True
Running = True
cycles = 0 

print(getTime(), "Start")

# Make sure the power is on
if useDcolReset:
  RXTools.sendDColAntennaOnCmd(RXIP,28001)
else:
  # Power Bar (Synaccess Networks) Command is:
  #
  # cmd.cgi?$A3 port state
  # port = 1-4
  # state = 0 (off) / 1 (on)
  #
  # The extra "%20"s in the string are the HTTP encoding of spaces
  # The following define off and on for the given power port
  off = '/cmd.cgi?$A3%20' + str(PowerPort) + '%200'
  on  = '/cmd.cgi?$A3%20' + str(PowerPort) + '%201'
  RXTools.SendHttpPostRetry( PowerIP, on, 'admin', 'admin')

# In case the unit was off give it enough time to power up
time.sleep(15)

try:
  # Make sure we have all the files downloaded
  RXTools.SyncLoggedFiles(RXIP,'admin','password','data')
except:
  print("Can't access FTP - file system not mounted?")

# Build and upgrade the firmware if we have old FW
checkFirmwareInstall()

thread = threading.Thread(target=worker)
thread.start()

EphThread = threading.Thread(target=sendEphWorker)
EphThread.start()


signal.signal(signal.SIGINT, signal_handler)

while(Running == True):
  Sleep = random.randint(randStart,randEnd)

  # The firt time through we don't have a PowerUpTime 
  # and don't get a PVTTime so skip the next test
  if(len(PowerUpTime) > 0):
    print("Random Uptime = ",Sleep,'\n')
    if(eraseAlm == False):
      # If we are *not* erasing the almanac simply sleep
      time.sleep(Sleep)
    else:
      # We are going to erase the almanacs after we get a 
      # position. Once we have a position there's no 
      # point tracking any longer as we are going to throw
      # away the almanacs. Loop around in 1 second chunks, if
      # we get position wait a short random period and then 
      # transition to power cycling. This should mean we'll 
      # perform more power fails per day and get better
      # statistics
      elapsedTime = 0
      while(elapsedTime < Sleep):
        # Sleep a second at a time
        time.sleep(1)
        elapsedTime += 1
        # If we get a position and we are more than 30 seconds
        # from when we would have terminated sleep for a random
        # 30 (to avoid message sync) and then drop out of the
        # sleep loop so we terminate this cycle early and get
        # more reboots per day
        if( (elapsedTime > 60) and (GotPVTTime == True) and (RXUp == True) ):
          if( (Sleep - elapsedTime) > 30):
            randWait = random.randint(0,30)
            time.sleep(randWait)
            elapsedTime += randWait
            print("Early sleep termination: " + str(elapsedTime))
            break
    print("Completed Sleep")
    print(GotPVTTime,PowerUpTime,RXUp,sendEphFlag)

    # Let's make sure we got a position
    if(GotPVTTime == False):
      # We still haven't positioned! Sleep to get to 
      # a total of 10 minutes. Sleep was between 120 and 240 seconds
      time.sleep(timeout - Sleep)
      if(GotPVTTime == False):
        # After 10 minutes total we still have not acquired!
        # The worker thread uses the PowerUpTime to determine the TTFF. If this
        # is not set then we'll skip the test. As we have failed to get
        # a fix set this to an empty list which prevents the worker task from
        # outputting anything to the stats file in case it manages to get a 
        # position between here and when we power down the receiver.
        PowerUpTime = []

        # Output some information to indicate that we failed to position
        TimeNow = getTime()
        now = leapseconds.utc_to_gps(datetime.datetime.utcnow())
        fid = open("results/" + str(now.year) + '-' + str(now.month).zfill(2) + '-' + str(now.day).zfill(2) + '-PVTTime.txt' ,'a')
        fid.write("%s %s %d NaN NaN Fail\n" % (TimeNow[0],TimeNow[1],timeout))
        fid.close()

  fileAccessComplete = False
  while(fileAccessComplete == False):
    try:
      # Turn off the logging before we download the data
      RXTools.DisableDefaultLogging(RXIP,'admin','password')
      print("DisableDefaultLogging",GotPVTTime,PowerUpTime,RXUp,sendEphFlag)
      print(getTime(), "Getting Files")

      try:
        RXTools.SyncLoggedFiles(RXIP,'admin','password','data')
      except:
        print("Can't access FTP - file system not mounted?")

      # Periodically erase all files on the receiver. As the
      # files are small if you don't do this you end up with 
      # 1,000s of files on the receiver which leads to getting
      # the file listing in SyncLoggedFiles() taking forever
      if(cycles >= 250):
        #RXTools.DeleteInternalFiles(RXIP,"admin","password")
        RXTools.getFileListAndDelete(RXIP,"admin","password","/Internal/","T04")
        RXTools.getFileListAndDelete(RXIP,"admin","password","/Internal/FFTs","png")
        RXTools.getFileListAndDelete(RXIP,"admin","password","/Internal/FFTs/tmp","png")
        cycles = 0
      else:
        cycles = cycles + 1

      # We've downloaded the data, turn the logging back on
      # this will probably result in a small file that will
      # be corrupt as we are about to pull the power
      RXTools.EnableDefaultLogging(RXIP,'admin','password')
      print("EnableDefaultLogging",GotPVTTime,PowerUpTime,RXUp,sendEphFlag)
      fileAccessComplete = True
    except:
      print(getTime(), "Get Files Failed")
      time.sleep(30)

  print(getTime(), "Powering Off")
  # Forces a tear down of the TCP/IP connection
  TestActive = False
  
  # Build and upgrade the firmware if we have old FW
  # the function will upgrade FW on every week rool
  checkFirmwareInstall()

  # Sleep for a second to give the script time to tear
  # down the TCP/IP connection
  time.sleep(1)

  # Now delete the ephemeris
  # Make sure we aren't tracking so we don't write fresh data
  try:
    if eraseEph or eraseAlm:
      # Try to turn off non-volatile storage, but don't throw an error
      # if the receiver has old firmware without the command.
      RXTools.sendDColDisableNV(RXIP,28001,need_ack=False)
    RXTools.sendDColAntennaOffCmd(RXIP,28001)
    user     = 'admin'
    password = 'password'
    
    if(eraseEph):
      RXTools.deleteNVEph(RXIP,user,password)
    
    if(eraseAlm):
      # Assume we are using modern F/W
      RXTools.deleteNVAlm(RXIP,user,password,False)

    if(changeLastPos):
      # Upload an alternate last position file
      RXTools.modifyNVPosition('position.bin',  # Input file
                               'position',      # output file
                               float(lastPosLat), # New Last Pos Lat/Long/Hgt
                               float(lastPosLon),
                               float(lastPosHgt))
      RXTools.DeleteFile(RXIP,user,password,"/bbffs/gnssData","position")
      url = 'http://' + user + ':' + password + '@' + RXIP+ '/prog/Upload?DataFile&file=/bbffs/gnssData/position'
      RXTools.UploadFile(RXIP,user,password,url,'position')
    elif(erasePos):
      RXTools.deleteNVLastPos(RXIP,user,password)

    # Unknown why - but you need to sleep after sending a Trimcomm command
    # otherwise the next TCP/IP connection isn't accepted.
    time.sleep(1)
    # We should not need this as the command is not sticky. However, 
    # in case the power bar fails to power cycle we don't want to falsely
    # report we don't acquire on the next loop due to the antenna being "off"
    RXTools.sendDColAntennaOnCmd(RXIP,28001)
    time.sleep(1)
  except:
    print("Error with Delete Eph / Antenna Switch")

  if useDcolReset:
    RXTools.sendDColRebootRcvr(RXIP,28001)

    # Brings back up the TCP/IP connection
    TestActive = True
    # This is the time that the reboot command was sent
    RebootTime = getTime()
    # PowerUpTime will get updated later, but provide a backup
    PowerUpTime = getTime()
  else:
    # Now power off the receiver
    resp = RXTools.SendHttpPostRetry( PowerIP, off, 'admin', 'admin')

    time.sleep(5)

    print(getTime(), "Powering On")
    resp = RXTools.SendHttpPostRetry( PowerIP, on, 'admin', 'admin')

    # Brings back up the TCP/IP connection
    TestActive = True
    # This is the time that the power came back on
    PowerUpTime = getTime()
