#!/usr/bin/env python
'''
Created on 21 Sep 2020

@author: pgm

radcelf-tune-pps 
reads meanfreq output from ppsmon
uses error from m1, m10, m100 for proportional control

KPROP=0.05  : 0.5 was unstable. Must be some error in my calcs, but this does converge well

A. Kirincich 10/2022: edited to
    (1) hard code the default fclk to 25.165824MHZ
    (2) hard code staring point as 25.165824MHZ  or FTW1=15798EE2308C
    (3) insert a trap in the start of process_last to skip all tuning for the step if 
    the err = mfdata['m1'] - args.fclk is greater than args.fclk/4 ... this might happen if the 
    meanfreq.txt data misses a scan gets an ml  ~ 2*fclk  ** does this work to stop? need to log the tuning data.
    (4) log the meanfreq message on non-volatile SD card /mnt/local/meanfreq.txt

Chang Huan Meng 2025.02.26:  added 300s and 1000s averaging

'''

import sys
import acq400_hapi
from acq400_hapi import AD9854
import argparse
import time
from builtins import int
from datetime import datetime

def web_message(message):
    print("web_message {} {}".format(type(message), message))
    with open('/dev/shm/radcelf-tune-pps.txt', 'w') as fd:
        for line in message.split('\n'):
            print("write {}".format(line))
            fd.write("{}\n".format(line))
        
def init(args): 

    uut = args.uut  
    print("configured clkdDB routing to source CLK from ddsC")
    uut.clkdB.CSPD = '02'
    uut.clkdB.UPDATE = '01'
    
# AK check for recent last value
    currtime = datetime.utcnow()
    tunereport = []
    try:
        with open('/mnt/local/lasttunes.txt', 'r') as tfp:
           oldtuning = tfp.readlines()
        for iline in reversed(oldtuning):
            lastdate = datetime.fromisoformat(iline.split()[0])
            timepassed = currtime - lastdate
            if (timepassed.days * 86400 + timepassed.seconds) < 3600:
                ftw1dec = oldtuning.split()[1]
                # check for wild tuning and exclude
                pratio = AD9854.ftw2ratio('15798EE2308C')
                if abs(ftw1dec-pratio) / pratio < 0.1:
                    ftw1hex = oldtuning.split()[2]
                else:
                    continue
            else:
                ftw1hex = '15798EE2308C'
    except:
        ftw1hex = '15798EE2308C'
        
    print("Initialise DDS C to a ratio for nominal {} MHz".format(args.fclk))
    uut.s2.ddsC_upd_clk_fpga = '1'
    crx =   18 if uut.s2.MTYPE == '70' else 12 
    uut.ddsC.CR   = '004C0041'
    uut.ddsC.FTW1 = ftw1hex   # hard code this to match the input needed for UH_WHOI systems

    time.sleep(30)   #what is the right number here...it looks like you need >10 to make sure the clock settles after the initialization?
   
# AK use last tune when restarting
def savelast(ftw1dec, ftw1hex):
    try:
        with open('/mnt/local/lasttunes.txt', 'r') as tfpw:
            oldlines = tfpw.readlines()
        if len(oldlines) > 100:
            tunereport = oldlines[-100:]
        else:
            tunereport = oldlines
    except:
        tunereport = []
    currtime = datetime.utcnow()
    tunereport.append(currtime.isoformat() + ' {} {}\n'.format(ftw1dec,ftw1hex))
    with open('/mnt/local/lasttunes.txt', 'w') as tfpw:
        for report in tunereport:
            tfpw.write(report)

def control(args, prop_err, KPL):
    uut = args.uut
    ftw1 = args.uut.ddsC.FTW1
    xx = AD9854.ftw2ratio(ftw1)
    yy = xx - prop_err*KPL
    ftw1_yy = AD9854.ratio2ftw(yy)
    
    message = "fclk {} {}\nXX:{} ratio:{} - err:{} * KP:{} => yy:{} YY:{}".\
            format(args.fclk, "FINE" if args.fine else "INIT", ftw1, xx, prop_err, KPL, yy, ftw1_yy) 
    
    print(message)
    web_message(message)
      
    uut.s2.ddsC_upd_clk_fpga = '1'
    uut.ddsC.FTW1 = ftw1_yy
    uut.s2.ddsC_upd_clk_fpga = '0'
    
    savelast(yy, ftw1_yy)
    
def process_last(args, line):   
# compute updated output based on error band return #secs to sleep. 
# NB: MUST not be too quick for the counter, which is itself quite slow..
    #print(line)
    mfdata = {}
    errs = []
    for pair in line.split():
        #print(pair)
        (key, value) = pair.split("=")
        mfdata[key] = float(value)
        
    err = mfdata['m1'] - args.fclk
    errs.append(('m1', err))
    
# AK trap skip all tuning
    if abs(err) > args.fclk/10:
        print("M1 error {}".format(err))
        print("ERROR! M1 error is high, something is wrong with the scan count, no change was made this iteration")
# do nothing, just wait, for the next line, the scan of /dev/shm/meanfreq.txt to get a different number
#        control(args, err/args.fclk, KP)
# wait a bit longer for the error to get off the meanfreq list...
        return 10
   
    if abs(err) > 1:
        print("M1 error {}".format(err))
        control(args, err/args.fclk, KP)
        return 2

    err = mfdata['m10'] - args.fclk
    errs.append(('m10', err))
   
    if abs(err) > 0.2:
        print("M10 error {}".format(err))
        control(args, err/args.fclk, KP*0.6)
        return 10

    err = mfdata['m30'] - args.fclk
    errs.append(('m30', err))
    
    if  abs(err) > 0.031:
        print("M30 error {}".format(err))
        control(args, err/args.fclk, KP*0.4)
        return 30        

    err = mfdata['m100'] - args.fclk
    errs.append(('m100', err))
    
    if  abs(err) > 0.011:
        print("M100 error {}".format(err))
        control(args, err/args.fclk, KP*0.25)
        return 100

# CM average to 1000 to match revised ppsmon

    err = mfdata['m300'] - args.fclk                                                                     
    errs.append(('m300', err))                                                                           
                                                                                                         
    if  abs(err) > 0.005:                                                                                
        print("M300 error {}".format(err))                                                               
        control(args, err/args.fclk, KP*0.15)                                                            
        return 300                                                                                       
                                                                                                         
    err = mfdata['m1000'] - args.fclk                                                                    
    errs.append(('m1000', err))                                                                          
                                                                                                         
    if  abs(err) > 0.002:                                                                                
        print("M1000 error {}".format(err))                                                              
        control(args, err/args.fclk, KP*0.1)                                                            
        return 1000        
    
    if args.fine:
        now = datetime.now()
        print("{}: errs:".format(now.strftime("%y%m%d:%H:%M.%S")), end="")
        for label, err in errs:
            print("{:4} {:5.2f}, ".format(label, err), end="")
        print("")
        return 1000
    
    print("TARGET Achieved, quitting...")
    exit(0)
               
        
# called from radcelf_tune, goal is to load the last data on the meanfreq.txt
# e.g.
#   s=02541 c=88326129089  m1=25165824 m10=25165824.0 m30=25165823.97 m100=25165823.97
# and sends the last line of meanfreq to process_last, 
# returns the output of process_last to radcelf_tune as the time.sleep tuner, 
# note that only the args, the last line of lines, and the global KP move forward
def process(args):
    with open("/dev/shm/meanfreq.txt", "r") as mf:
        lines = mf.readlines()
        
    return process_last(args, lines[-1])
    
# called from run_main with args passed, output of process(args) is a sleep time in seconds, 
# when the sleep time return is =0, radcelf_tune stops running
def radcelf_tune(args):
    while True:
        waiting_time = process(args)
        print("waiting_time=",waiting_time)
        time.sleep(waiting_time)
    
# main is the excuting script that actually runs everthing above this point
def run_main():
    global KP
# uses the loaded parser subroutines to pull in the data called from the excuted script
    parser = argparse.ArgumentParser("radcelf-tune-pps [fclk]")
    parser.add_argument('--best', default=True, help="select BEST clock for PPS sync")
    parser.add_argument('--fine', default=False, help="fine tune, runs forever")
    parser.add_argument('--Kp', default=0.05, type=float, help="Kp, proportional control constant" )
    parser.add_argument('fclk', nargs='*', default=[25165824], type=int, help="required clock frequency")
    args = parser.parse_args()  # parser is set above, a list of arguments, parse_args acts on parser to define the args. to be used here
    fclk = args.fclk[0]
    # extract from args to place in to local or global variables
    KP = args.Kp  
    message = "radcelf-tune-pps"
    # best is default true, so this will run unless you say  --best=0
    if args.best:  
        # estimates the best clock rate based on the input (or default) and the subroutine...
        fbest =acq400_hapi.RAD3DDS.best_clock_pps_sync(fclk) 
        # why is this actually running? as fbest should equal args.fclk at this point... 
        if fbest != args.fclk:  
        # != means not equal, so if they are not the same, it resets this to fbest?
            m2 = "Selected BEST clock {} => {}".format(fclk, fbest)
            print(m2)
            message = "{}\n{}".format(message, m2)
        # this resaves fclk as fbest, thus
        fclk = fbest  
    else:
        fclk = fclk
    args.fclk = fclk
    args.uut = acq400_hapi.RAD3DDS("localhost")
    
    web_message(message)

# if fine~=1, do a hard start first    
# but most of these knobs were set in the radcelf_clock.sh script, 
# in case they are different, start again
    if not args.fine:
        init(args)
        print("Starting Tuning")
        
#radcelf_tune  is the action/worker that takes the args and does the tuning
    radcelf_tune(args)
        
# execution starts here

if __name__ == '__main__':
    run_main()
    

