#!/usr/bin/env python
usage="""\
Compare tracking on two units and show basic differences.
Looks for:
 1- large SNR diffs
 2- missing signals/SVs
 3- cycle slips on one unit and not the other

Examples:
  ./compare_svs.py 10.1.150.xx admin:tr.imble@10.1.150.yy:81
"""

import requests
import xmltodict
import time
import math
from datetime import datetime

ignore_sigs=[] # e.g., ['E6','B3','LEX']
ignore_antennas = [] # e.g., ['1']
ignore_unhealthy=True
snr_diff = 6.  # minimum SNR diff in dBHz to say receivers are different

class SigInfo():
    def __init__(self,cno,health,elev,slip_cnt,has_slip):
        self.cno=cno
        self.health=health
        self.elev=elev
        self.slip_cnt=slip_cnt
        self.has_slip=has_slip
    def __str__(self):
        out = "cno=%.1f el=%.1f slip=%d"%(self.cno,self.elev,self.has_slip)
        if self.health=='unhealthy':
            out += "(U)"
        return out

def guess_password( ip ):
    """Add username/password to IP (if needed).
    e.g., "10.1.150.x" -> "admin:password@10.1.150.x"
    """
    if ip.find('@') > 0:
        return ip
    for passwd in ['password','tr.imble']:
        new_ip = 'admin:%s@%s'%(passwd,ip)
        r = requests.get( 'http://%s/xml/dynamic/svData.xml'%new_ip,
                          timeout=10 )
        if r.status_code == requests.codes.ok:
            return new_ip
    return ip


def get_track( ip, elev_mask, last_result ):
    """Convert receiver svData.xml to a dictionary:
         key = sys-prn-antenna-signal (e.g., GALILEO-1-0-E1CBOC)
         entry = SigInfo()
       Inputs:
         ip = receiver web address
         elev_mask = min elevation to return [deg]
         last_result = last output from this function
    """
    r = requests.get( 'http://%s/xml/dynamic/svData.xml'%ip,
                      timeout=10 )
    r.raise_for_status()
    result = {}
    for sv in xmltodict.parse(r.text)['svTrack']['sv']:
        antenna = sv['@antenna']
        if antenna in ignore_antennas:
            continue
        elev = float(sv['elev'])
        if elev < elev_mask:
            continue
        if type(sv['cno']) != list:
            sv_cno = [sv['cno']]
        else:
            sv_cno = sv['cno']
        if type(sv['adv']) != list:
            sv_adv = [sv['adv']]
        else:
            sv_adv = sv['adv']
        for cno,adv in zip(sv_cno,sv_adv):
            cno_value = float(cno['#text'])
            if cno_value <= 0.:
                continue
            sig_type = cno['@type']
            if sig_type in ignore_sigs:
                continue
            sig_txt = '%s-%s-%s-%s' % (sv['@sys'],sv['@PRN'],antenna,sig_type)
            health = 'healthy'
            if '@health' in sv:
                health = 'unhealthy'
                if ignore_unhealthy:
                    continue
            slip_cnt = int(adv['slips'])
            try:
                last_slip_cnt = last_result[sig_txt].slip_cnt
                has_slip = (slip_cnt != last_slip_cnt)
            except:
                has_slip = False
            result[sig_txt] = SigInfo( cno_value, health, elev, slip_cnt, has_slip )
    return result, r.text

def comp_track( d1, d2, elev_mask, min_snr ):
    """Print diagnostic and return True if differences between d1/d2 are found.
    Inputs:
      d1 = from get_track()
      d2 = from get_track()
      elev_mask = min elevation to analyze [deg]
      min_snr = min SNR to analyze [dBHz]
    """
    is_different = False
    all_sigs = set.union(set(d1.keys()),set(d2.keys()))
    for sig in all_sigs:
        if sig not in d1:
            if d2[sig].elev > elev_mask+.5 and d2[sig].cno>min_snr:
                print('missing rcvr1',sig,d2[sig])
                is_different = True
        elif sig not in d2:
            if d1[sig].elev > elev_mask+.5 and d1[sig].cno>min_snr:
                print('missing rcvr2',sig,d1[sig])
                is_different = True
        elif d1[sig].cno > min_snr or d2[sig].cno > min_snr:
            if abs(d1[sig].cno-d2[sig].cno) > snr_diff:
                print('different SNR rcvr1 %.1f rcvr2 %.1f'%(d1[sig].cno,d2[sig].cno),
                      sig)
                is_different = True
            if d1[sig].has_slip != d2[sig].has_slip:
                print('different slip rcvr1 %d rcvr2 %d'%(d1[sig].has_slip,d2[sig].has_slip),
                      sig)
                is_different = True
    return is_different

def main( rcvr1, rcvr2, elev_mask, min_snr, poll_secs, n_loop, log_raw_filename ):
    rcvr1 = guess_password( rcvr1 )
    rcvr2 = guess_password( rcvr2 )
    log_raw_file = None
    if log_raw_filename is not None:
        log_raw_file = open(log_raw_filename,'w')
        print("Opening '%s' for logging differences"%log_raw_filename)
    n_diff = 0
    n_comp = 0
    d1, d2 = None, None
    while n_loop > 0:
        d1,raw1 = get_track( rcvr1, elev_mask, d1 )
        d2,raw2 = get_track( rcvr2, elev_mask, d2 )
        is_different = comp_track( d1, d2, elev_mask, min_snr )
        if is_different:
            n_diff += 1
            if log_raw_file is not None:
                log_raw_file.write("rcvr1: "+raw1+"\n")
                log_raw_file.write("rcvr2: "+raw2+"\n")
                log_raw_file.flush()
        n_comp += 1
        t_now = datetime.now()
        print(f"{t_now:%Y-%m-%d %H:%M:%S} : # runs = {n_comp}, # diffs = {n_diff}")
        time.sleep(poll_secs)
        n_loop -= 1


if __name__ == "__main__":
    import argparse
    parser = argparse.ArgumentParser(formatter_class=argparse.RawTextHelpFormatter,
                                     description=usage)
    parser.add_argument('rcvr1',
                        help='receiver addr, e.g. 10.1.150.x')
    parser.add_argument('rcvr2',
                        help='receiver addr, e.g. 10.1.150.y')
    parser.add_argument('--log_raw',
                        help='filename - when there is a diff, log raw XML?')
    parser.add_argument('--elev',
                        help='elevation mask [deg] (default 10)',
                        default=10)
    parser.add_argument('--min_snr',
                        help='minimum SNR [dBHz] (default 35)',
                        default=35)
    parser.add_argument('--secs',
                        help='# of seconds between each query (default 10)',
                        type=int,
                        default=10)
    parser.add_argument('--num',
                        help='# of loops to run (default infinite)',
                        default=math.inf)
    args = parser.parse_args()

    main( args.rcvr1, args.rcvr2, args.elev, args.min_snr, args.secs,
          args.num, args.log_raw )
