# Folder structure:
# GPSTools/
#   ├─ pythonTools
#   │   └─ DSD
#   │       └─ simple_hackrf_control.py
#   └─ AntiJamTest
#       ├─ server_main
#       ├─ server_control_spirent
#       ├─ server_control_jammer
#       └─ server_control_hackrf
#           ├─ server.py
#           └─ web
#               └─ app
#                   └─ app_hackRF.py


import multiprocessing
import multiprocessing.connection as mp_con
import os, sys
from datetime import datetime
import time
import json
import signal

sys.path.append("..\\..\\pythonTools\\DSD")     # For server running on windows
import simple_hackrf_control


base_dir = './'
sys.path.append(base_dir)

verbose = False

def getHackrfStatus(access_lock):
  '''
    Input:
      access_lock = multiprocessing.Manager.Lock() to allow single access to COM port or HackRF, otherwise there will be permission issue
    Return:
      dictionary of serial status
  '''
  fn = 'getHackrfStatus()'
  d = {}

  if verbose: print(fn,"Acquiring access_lock.")
  acq_res = access_lock.acquire(False)
  if acq_res:
    if verbose: print(fn,"Acquired access_lock.")
    d['status'] = 'Not in use'
    access_lock.release()
    if verbose: print(fn,"Released access_lock.")
  else:
    if verbose: print(fn,"Cannot acquire access_lock.")
    try:
      with open("hackrf_status.json") as f: 
        d = json.load( f )
    except:
      d['status'] = 'In use'

  return d


def wrap_turn_on_hackrf(access_lock, interference_type, desired_pwr_dBm, center_freq_MHz, sec):
  '''
  Turn on HackRF by calling:
  simple_hackrf_control.set('GAUSSIAN_RFI_1MHz',  -20,            1598.0,         5)
                            RFI type,             desired power,  center freq.,   how long
                                                  [dBm]           [MHz]           [sec]
  This function needs to be defined at the top-level to avoid Attribute Error: can't pickle local object
  Functions are only picklable if they are defined at the top-level of a module.
  Nested functions defind within turn_on_hackrf() won't be importable by the child.
  '''
  fn = 'wrap_turn_on_hackrf'
  try_cnt = 2
  while try_cnt > 0:
    try:
      simple_hackrf_control.set(interference_type, desired_pwr_dBm, center_freq_MHz, sec)
      access_lock.release()
      if verbose: 
        print("")
        print(fn,"Released access_lock.")
      
      f=open('server.log','w')
      t = datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f')
      msg = "try simple_hackrf_control.set() succeeded at "+t+"\n"
      f.write(msg)
      f.close()

      if verbose: print(fn,msg)      
      return True

    except:
      f=open('server.log','w')
      t = datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f')
      msg = "try simple_hackrf_control.set() failed at "+t+"\n"
      f.write(msg)
      f.close()

      if verbose: print(fn,msg)

    try_cnt -= 1
    time.sleep(0.1)
    
  access_lock.release()
  if verbose: 
    print("")
    print(fn,"Released access_lock.")

  return False


def turn_on_hackrf(access_lock, ip_addr, interference_type, center_freq_MHz, desired_pwr_dBm, sec):
  '''
  Input:
    access_lock = multiprocessing.Manager.Lock() allows single access to HackRF.
    ip_addr = string, ip address of request
    interference_type = string, RFI type, eg. 'GAUSSIAN_RFI_1MHz',
    center_freq_MHz = float, unit in MHz, eg. 1598.0
    desired_pwr_dBm = float, unit in dBm, eg. -20.0
    sec = int, how many seconds does it last, eg. 5
  '''

  fn = 'turn_on_hackrf'
  if verbose: print(fn+":: Acquiring access_lock... ")
  acq_res = access_lock.acquire(False)

  if acq_res:
    if verbose: print(fn+":: Acquired access_lock. Turnning HackRF On.")

    proc = multiprocessing.Process( target=wrap_turn_on_hackrf, args=(access_lock, interference_type, desired_pwr_dBm, center_freq_MHz, sec,) )
    proc.start()

    end_time = datetime.fromtimestamp(time.time()+sec).strftime('%Y-%m-%d %H:%M:%S')
    response = {'status': 'In use', 'end_time':end_time, 'pid':proc.pid}
    with open("hackrf_status.json","w") as f:
      json.dump( response, f )
    return response
  else:
    if verbose: print(fn,"Cannot acquire access_lock.")
    try:
      with open("hackrf_status.json") as f: 
        response = json.load( f )
    except:
      response = {'status': 'In use'}
    return response
  

def turn_off_hackrf(access_lock):
  '''
  Input:
    access_lock = multiprocessing.Manager.Lock() allows single access to HackRF.
  Return: True/False
  '''

  fn = 'turn_off_hackrf()'

  num_try = 1
  acq_res = None
  response = {}

  while num_try > 0:
    if verbose: print(fn,"Acquiring access_lock...", num_try)
    acq_res = access_lock.acquire(False) 
    if acq_res:
      break
    time.sleep(0.1)
    num_try -= 1
  
  if acq_res:
    access_lock.release()
    if verbose: print(fn,"Released access_lock.")
    # Do nothing
    return False
  else:
    if verbose: print(fn,"Cannot acquire access_lock.")
    try:
      with open("hackrf_status.json") as f: # hackrf_status.json
        d = json.load( f )
      pid = int(d['pid'])
      if verbose: print(fn,"pid", pid)
      os.kill(pid, signal.SIGTERM) #FIXME This seems to be not enough. Send another cmd will stop hackrf.
      if verbose: print(fn,"Kill process ", pid)
      if verbose: print(fn,"Then overwrite on time to 0.1 sec to turn it off.")
      simple_hackrf_control.set('PULSED_RFI_1msec', -40, 1598.0, 0.1)
    except:
      if verbose: print(fn, "Load hackrf_status failed")

    if verbose: print(fn,"Release lock anyway.")
    access_lock.release()
    return True


def handle_req(conn, lck_hackrf):
  """Input: conn = socket for communication with app.py
            lck_hackrf = multiprocessing.Manager.Lock() allows single access to HackRF.
     Handle requests from app.py and send a response back over the 'conn' socket.
  """
  msg = conn.recv()
  if verbose:
    print("Got msg:", msg)
  
  if msg[0] == 'getHackrfStatus':
    if verbose: print(msg)
    rt = getHackrfStatus(lck_hackrf)
    conn.send( rt )
  elif msg[0] == 'turnOnHackRF':
    if verbose: print(msg)
    interference_type, center_freq_MHz, desired_pwr_dBm, sec, ip_addr = str(msg[1]), float(msg[2]), float(msg[3]), int(msg[4]), str(msg[5])
    if verbose: print("interference_type, center_freq_MHz, desired_pwr_dBm, sec, ip_addr:", interference_type, center_freq_MHz, desired_pwr_dBm, sec, ip_addr )
    rt = turn_on_hackrf(lck_hackrf, ip_addr, interference_type, center_freq_MHz, desired_pwr_dBm, sec)
    conn.send( rt )
  elif msg[0] == 'turnOffHackRF':
    if verbose: print(msg)
    rt = turn_off_hackrf( lck_hackrf )
    conn.send( str(rt) )
  else:
    print("Unknown msg:",msg)
  conn.close()
  log_str = datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f') + ": " + " ".join(map(str,msg))
  if verbose: print(log_str)

def run_server(lck_hackrf):
  """
  Input: 
    lck_hackrf = multiprocessing.Manager.Lock() to allow single access to HackRF
  Start listening on port ? for requests from app.py.  Each request is
  handled in a separate thread so multiple people can access the server.
  Notes:
    Fork is the default on Linux (it isn't available on Windows), while Windows and MacOS use spawn by default.
    Spawn starts a Python child process from scratch without the parent process's memory, file descriptors, threads, etc. 
    Technically, spawn forks a duplicate of the current process.
    On Windows, Process() will have a copy of parent process's memory.
  """
  control_port = 10003

  addr = ('localhost', control_port)
  print('Listen on {}'.format(addr))
  listener = mp_con.Listener(addr)
  try:
    while True:
      conn = listener.accept()
      proc = multiprocessing.Process(target=handle_req, args=(conn, lck_hackrf))
      proc.start()
      # conn.close()
  except KeyboardInterrupt:
    print("Receive Ctrl + C")
    pass

if __name__ == "__main__":
  os.environ['no_proxy'] = '*'
  m = multiprocessing.Manager()
  lck_hackrf = m.Lock()
  run_server(lck_hackrf)

