###############################################################################
# Copyright 2020 - 2022 Trimble Inc.
# $Id: nopilive_gui.py,v 1.5 2024/02/05 04:55:15 wlentz Exp $
# $Source: /home/wlentz/cvs_stinger/GPSTools/NoPiLive/nopilive_gui.py,v $
###############################################################################

""" NoPiLive
    Graphical User Interface for NoPiLive. Extends the NoPi/small_DI.py
    interface to include NoPiLive specific functionality.
"""

from functools import partial
from six.moves import tkinter as Tk
from matplotlib import animation
from mutils import RcvrConst as st_types
from small_DI import State, make_ui, draw_base_cno, draw_base_elev, \
                     draw_dd_code, draw_dd_carr, draw_sd_cno, draw_dd_dopp
from nopilive_config import VERSION_STR, COPYRIGHT_STR

###############################################################################
# Local constants

# Plot types
_PLOT_DD_CODE = 0
_PLOT_DD_CARR = 1
_PLOT_DD_DOPP = 2
_PLOT_SD_CNO = 3
_PLOT_BASE_CN0 = 4
_PLOT_BASE_ELV = 5
_PLOT_MAX = 6

# Maximum SV Id
_MAX_SVID = 210

# Define all of the signal combinations (combos) with name strings and the
# sat_type and sub_type values of each.
_COMBOS = [("GPS", st_types.SAT_TYPE_GPS,
            (("L1C/A", st_types.SUBTYPE_L1CA),
             ("L1C", st_types.SUBTYPE_L1C),
             ("L1E", st_types.SUBTYPE_L1E),
             ("L2C", st_types.SUBTYPE_L2C),
             ("L2E", st_types.SUBTYPE_L2E),
             ("L5", st_types.SUBTYPE_L5))
           ),
           ("SBAS", st_types.SAT_TYPE_SBAS,
            (("L1C/A", st_types.SUBTYPE_L1CA),
             ("L5", st_types.SUBTYPE_L5))
           ),
           ("GLONASS", st_types.SAT_TYPE_GLONASS,
            (("G1C", st_types.SUBTYPE_G1C),
             ("G1P", st_types.SUBTYPE_G1P),
             ("G2C", st_types.SUBTYPE_G2C),
             ("G2P", st_types.SUBTYPE_G2P),
             ("G3", st_types.SUBTYPE_G3))
           ),
           ("Galileo", st_types.SAT_TYPE_GALILEO,
            (("E1", st_types.SUBTYPE_E1),
             ("E5A", st_types.SUBTYPE_E5A),
             ("E5B", st_types.SUBTYPE_E5B),
             ("E5AltBOC", st_types.SUBTYPE_E5AltBOC),
             ("E6", st_types.SUBTYPE_E6))
           ),
           ("QZSS", st_types.SAT_TYPE_QZSS,
            (("L1C/A", st_types.SUBTYPE_L1CA),
             ("L1C", st_types.SUBTYPE_L1C),
             ("L2C", st_types.SUBTYPE_L2C),
             ("L5", st_types.SUBTYPE_L5))
           ),
           ("Beidou", st_types.SAT_TYPE_BEIDOU,
            (("B1", st_types.SUBTYPE_B1),
             ("B1C", st_types.SUBTYPE_B1C),
             ("B2", st_types.SUBTYPE_B2),
             ("B2A", st_types.SUBTYPE_B2A),
             ("B2B", st_types.SUBTYPE_B2B),
             ("B2AceBOC", st_types.SUBTYPE_B2AceBOC),
             ("B3", st_types.SUBTYPE_B3))
           ),
           ("IRNSS", st_types.SAT_TYPE_IRNSS,
            (("L5C/A", st_types.SUBTYPE_L5CA),
             ("S1C/A", st_types.SUBTYPE_S1CA))
           )
          ]

###############################################################################

def _extra_gui_init(state):
    """ NoPiLive GUI specific variable initialisation. This is called from
        the small_DI configuration state initialiser.
    """
    state.func_animate = None
    state.func_get_data = None
    state.func_rx_info = None
    state.func_get_diffs = None
    state.func_clear_rt27_data = None
    state.func_select_combo = None

    state.rx_info_visible = False
    state.signal_selector_visible = False
    state.about_box_visible = False
    state.hack_box_visible = False

    state.update_rate = 1.0

    combo = _COMBOS[0]
    sigs = combo[2]
    state.ref_sig = _RefTstSignal(state, combo[1], sigs[0][1], True)
    state.tst_sig = _RefTstSignal(state, combo[1], sigs[0][1], False)

    state.sv_enbs = [True] * _MAX_SVID

    state.max_sigs = 0
    for sys in _COMBOS:
        sigs = sys[2]
        nsig = len(sigs)
        if nsig > state.max_sigs:
            state.max_sigs = nsig

###############################################################################

class _RefTstSignal():
    """ Class containing the signal combo (sat_type and sub_type) for reference
        and test signals to be differenced.
    """
    def __init__(self, state, sat_type, sub_type, is_ref):
        self.is_ref = is_ref
        if is_ref:
            self.name = "ref"
        else:
            self.name = "tst"
        self.combo_name = "Unkn:Unkn"
        self.set_sat_sub_type(state, sat_type, sub_type)

    def _construct_combo_name(self):
        """ Construct a string of the current signal combo name. i.e. GPS:L1C/A. """
        sys_name = "Unkn"
        sig_name = "Unkn"
        for sys in _COMBOS:
            if sys[1] != self.sat_type:
                continue
            sys_name = sys[0]
            sigs = sys[2]
            for sig in sigs:
                if sig[1] != self.sub_type:
                    continue
                sig_name = sig[0]
        self.combo_name = sys_name+":"+sig_name

    def get_sat_sub_type(self):
        """ Returns a tuple of the current signal combo.
            (sat_type, sub_type)
        """
        return (self.sat_type, self.sub_type)

    def set_sat_sub_type(self, state, sat_type, sub_type):
        """ Set the signal combo to the given sat_type & sub_type.
            Need to provide the GUI state to access callbacks to other modules
            that also need to know the new signal combo.
        """
        self.sat_type = sat_type
        self.sub_type = sub_type
        self._construct_combo_name()
        _update_selected_combo(state, self.sat_type, self.sub_type, self.is_ref)

    def get_combo_name(self):
        """ Return a string of the current signal combo name. i.e. GPS:L1C/A. """
        return self.combo_name

def _full_combo_name(ref, tst):
    """ Returns a string of the current full signal combo name.
        Only the reference signal combo is used if the reference and test
        signal combos are the same. i.e. GPS:L1C/A.
        Otherwise both are reported i.e. GPS:L1C/A<->QZSS:L1C.
    """
    ref_combo = ref.get_combo_name()
    tst_combo = tst.get_combo_name()
    if ref_combo == tst_combo:
        return ref_combo
    return ref_combo+'<->'+tst_combo

def _update_selected_combo(state, sat_type, sub_type, is_ref):
    """ Call when the one of the signal combos is updated.
        Callbacks to other modules that also need to know the new signal combo.
    """
    if state.func_select_combo is not None:
        state.func_select_combo(sat_type, sub_type, is_ref)

###############################################################################
# Pop-up menu utilities

class PopUpBox():
    """ Class to create a pop-up box menu attached to the main GUI. """
    def __init__(self, root, title=None):
        self.cancelled = False
        self.popup = Tk.Toplevel(root)
        if title is not None:
            self.popup.wm_title(title)
        # This magic replaces the default X button operation to cancel the
        # window. Calls cancel_popup() instead.
        self.popup.protocol("WM_DELETE_WINDOW", self.cancel_popup)
        self.ok_btn = None

    def create_ok_button(self, grid_not_pack, **kwargs):
        """ Create an OK button. Specify the button placement using grid() or pack().
            Pass configuration parameters to grid() or pack() using the kwargs.
        """
        self.ok_btn = Tk.Button(self.popup, text="OK", command=self.close_popup)
        if grid_not_pack:
            self.ok_btn.grid(kwargs)
        else:
            self.ok_btn.pack(kwargs)

    def run_popup(self):
        """ Call to run the pop-up box menu.
            Check the value of self.cancelled when this completes to determine
            if the action was OK'ed or cancelled.
        """
        self.popup.mainloop()

    def close_popup(self):
        """ Close the pop-up box. """
        self.popup.quit()
        self.popup.destroy()

    def cancel_popup(self):
        """ Cancel the pop-up box. """
        print("Cancel")
        self.close_popup()
        self.cancelled = True

###############################################################################
# Receiver information pop-up box

def _get_rx_info(state, is_base):
    """ Returns the certain information on the base or rover receiver.
        Callback to other modules.
    """
    rx_info = state.func_rx_info(is_base)
    return rx_info

def _populate_info(popup, label, base, rovr, row):
    """ Helper to populate the receiver information pop-up box.
        The data displayed in a table-like format.
    """
    Tk.Label(popup.popup, text=label).grid(row=row, column=0)
    Tk.Label(popup.popup, text=base).grid(row=row, column=1)
    Tk.Label(popup.popup, text=rovr).grid(row=row, column=2)

def _format_week_tow(week_tow):
    """ Helper to format the week # and time-of-week string """
    return str(week_tow[0])+' / '+str(week_tow[1])

def _show_rx_info(state):
    """ Creates and runs the receiver information pop-up box. """
    # Only show one receiver information pop-up at a time
    if state.rx_info_visible:
        return
    state.rx_info_visible = True

    rxinfo = PopUpBox(state.root, title="RX Information")
    base = _get_rx_info(state, True)
    rovr = _get_rx_info(state, False)
    base_time = _format_week_tow(base['Time'])
    rovr_time = _format_week_tow(rovr['Time'])
    if base['ZBLine']:
        baseline = 'Zero'
    else:
        baseline = 'Non-Zero'
    _populate_info(rxinfo, '', base['Name'], rovr['Name'], 0)
    _populate_info(rxinfo, 'RT27 Src', base['RT27Src'], rovr['RT27Src'], 1)
    _populate_info(rxinfo, '# Epochs', base['Epochs'], rovr['Epochs'], 3)
    _populate_info(rxinfo, 'Time (Wk/ToW)', base_time, rovr_time, 4)
    _populate_info(rxinfo, 'Antenna', base['AntName'], rovr['AntName'], 6)
    _populate_info(rxinfo, 'Pos. X', base['AntXYZ'][0], rovr['AntXYZ'][0], 7)
    _populate_info(rxinfo, 'Pos. Y', base['AntXYZ'][1], rovr['AntXYZ'][1], 8)
    _populate_info(rxinfo, 'Pos. Z', base['AntXYZ'][2], rovr['AntXYZ'][2], 9)
    _populate_info(rxinfo, 'Baseline', baseline, '', 10)
    rxinfo.create_ok_button(True, row=11, column=1)
    rxinfo.run_popup()
    # Clear the receiver information pop-up visible flag
    state.rx_info_visible = False

###############################################################################
# Signal combo selector pop-up box

def _get_type_for_rb_var(max_sig, rb_var):
    """ Returns the selected signal combo tuple (sat_type, sub_type) for the
        given signal selector radio button number.
    """
    row = int(rb_var / max_sig)
    col = rb_var % max_sig
    if row < len(_COMBOS):
        combo = _COMBOS[row]
        signals = combo[2]
        if col >= len(signals):
            col = 0
    else:
        row = 0
        col = 0
    combo = _COMBOS[row]
    signals = combo[2]
    sat_type = combo[1]
    sub_type = signals[col][1]
    return (sat_type, sub_type)

def _get_rb_var_for_type(max_sig, sat_type, sub_type):
    """ Returns the signal combo selector radio button number for the given
        sat_type and sub_type signal combo.
    """
    for row, combo in enumerate(_COMBOS):
        if combo[1] == sat_type:
            signals = combo[2]
            for col, sigs in enumerate(signals):
                if sigs[1] == sub_type:
                    rb_var = row * max_sig + col
                    return rb_var
    # Failed to find the radio button so default to the first one
    return 0

def _populate_signal_selector(state, sigsel, rb_var):
    """ Populate the signal combo selector pop-up box. """
    for row, combo in enumerate(_COMBOS):
        txt = combo[0]
        Tk.Label(sigsel.popup, text=txt).grid(row=row+1, column=0, sticky='E')

    for row, combo in enumerate(_COMBOS):
        signals = combo[2]
        for col, signal in enumerate(signals):
            txt = signal[0]
            val = row * state.max_sigs + col
            rbtn = Tk.Radiobutton(sigsel.popup, text=txt, variable=rb_var, value=val)
            rbtn.grid(row=row+1, column=col+1, padx=5, sticky='W')

def _signal_selector(state, ref_tst, tst_ref):
    """ Creates and runs the signal combo selector pop-up box. """
    # Only show one signal combo selector pop-up at a time
    if state.signal_selector_visible:
        return
    state.signal_selector_visible = True

    init_sel = _get_rb_var_for_type(state.max_sigs, ref_tst.sat_type, ref_tst.sub_type)
    rb_var = Tk.IntVar(state.root, init_sel)

    sigsel = PopUpBox(state.root, title="Select "+ref_tst.name+" signal type")
    _populate_signal_selector(state, sigsel, rb_var)

    match_var = Tk.BooleanVar(state.root, True)
    #For now, only support a single signal type for reference and test signals
    #Expand later to support different ref. and test signals, just like NoPi
    #Enable the checkbox below and the menu option once rt27_meas_ddiff updated
    #txt = "Set "+tst_ref.name+" signal type to the same type?"
    #match_btn = Tk.Checkbutton(sigsel.popup, text=txt, variable=match_var)
    txt = "Ref and tst signal types will be set to match"
    match_btn = Tk.Label(sigsel.popup, text=txt)
    match_btn.grid(row=len(_COMBOS)+2, column=0, columnspan=state.max_sigs+1)

    sigsel.create_ok_button(True, row=len(_COMBOS)+3, column=0, columnspan=state.max_sigs+1)
    sigsel.run_popup()

    # Check if the pop-up was OK'ed or cancelled
    if not sigsel.cancelled:
        sat_type, sub_type = _get_type_for_rb_var(state.max_sigs, rb_var.get())
        ref_tst.set_sat_sub_type(state, sat_type, sub_type)
        if match_var.get():
            tst_ref.set_sat_sub_type(state, sat_type, sub_type)

    # Clear the signal combo selector pop-up visible flag
    state.signal_selector_visible = False

###############################################################################
# About NoPiLive pop-up box

def _show_about_box(state):
    """ Creates and runs the About NoPiLive pop-up box. """
    # Only show one about NoPiLive pop-up at a time
    if state.about_box_visible:
        return
    state.about_box_visible = True

    msg = """\
    About NoPi Live...

    Version: {}
    {}""".format(VERSION_STR, COPYRIGHT_STR)

    about = PopUpBox(state.root, title="About NoPi Live")
    Tk.Label(about.popup, text=msg).grid(row=0, column=0)
    img_trmb = Tk.PhotoImage(file="Images/TrimbleLogo250w.png")
    trmb_canvas = Tk.Canvas(about.popup, width=250, height=130)
    trmb_canvas.create_image((125, 65), image=img_trmb)
    trmb_canvas.grid(row=1, column=0)
    about.create_ok_button(True, row=2, column=0)
    about.run_popup()
    # Clear the about NoPiLive pop-up visible flag
    state.about_box_visible = False

def _show_hackathon_box(state):
    """ Creates and runs the Hackathon pop-up box. """
    # Only show one Hackathon pop-up at a time
    if state.hack_box_visible:
        return
    state.hack_box_visible = True

    msg = """\
    Created as part of Trimble Hackathon 2020, 2021 & 2022
    http://hack2020.trimble.com/
    http://hack2021.trimble.com/
    http://hack2022.trimble.com/
    """
    hack = PopUpBox(state.root, title="Hackathon 2020, 2021 & 2022")
    Tk.Label(hack.popup, text=msg).grid(row=0, column=0)
    img_hack2020 = Tk.PhotoImage(file="Images/Hackathon.png")
    img_hack2021 = Tk.PhotoImage(file="Images/Hack2021.png")
    hack_canvas = Tk.Canvas(hack.popup, width=250, height=220)
    hack_canvas.create_image((125, 60), image=img_hack2020)
    hack_canvas.create_image((125, 170), image=img_hack2021)
    hack_canvas.grid(row=1, column=0)
    hack.create_ok_button(True, row=2, column=0)
    hack.run_popup()
    # Clear the hackathon pop-up visible flag
    state.hack_box_visible = False

###############################################################################
# Other menu selection functions

def _plot_selector(state, select):
    """ Call to select the given plot type, using the _PLOT_... constants. """
    for idx, var in enumerate(state.plot_type_selector):
        var.set(idx == select)

def _pause_updates(state, pause):
    """ Call to (un)pause the updating of the main plot window.
        Important to call this before trying to use the MatplotLib toolbar
        functions.
    """
    if pause:
        state.ctrl_paused.set(True)
        state.ctrl_unpaused.set(False)
    else:
        state.ctrl_paused.set(False)
        state.ctrl_unpaused.set(True)

def _enable_all_svs(state, enable):
    """ Call to (dis/en)able the plotting of all SVs. """
    for idx, _ in enumerate(state.sv_enbs):
        state.sv_enbs[idx] = enable

def _clear_rt27_data(state):
    """ Call to clear all of the RT27 data.
        Callback to other modules that need to clear data.
    """
    if state.func_clear_rt27_data is not None:
        state.func_clear_rt27_data()

###############################################################################
# Menubar creation functions

def _add_menu_commands(menu, cfg):
    """ Helper function to add a new menu command. """
    for lbl, cmd, und in cfg:
        menu.add_command(label=lbl, command=cmd, underline=und)

def _add_menu_checkbutton(menu, cfg):
    """ Helper function to add a new menu checkbutton command. """
    for lbl, var, cmd, und in cfg:
        menu.add_checkbutton(label=lbl, variable=var, command=cmd, underline=und)

def _make_menubar(state, menubar, cfg=None):
    """ Create the NoPiLive menu bar. This is called from the small_DI UI
        creation in place of the default small_DI menubar.
    """
    nopi_cmds = [("RX Info.", partial(_show_rx_info, state), 3),
                 ("Exit", state.root.destroy, 1)]
    nopi_menu = Tk.Menu(menubar, tearoff=0)
    _add_menu_commands(nopi_menu, nopi_cmds)
    menubar.add_cascade(label="NoPi", menu=nopi_menu, underline=0)

    plot_cmds = [("Reference signal",
                  partial(_signal_selector, state, state.ref_sig, state.tst_sig), 0)
                 #("Test signal",
                 # partial(_signal_selector, state, state.tst_sig, state.ref_sig), 0)
                ]
    state.plot_type_selector = []
    def_plot = 0
    if cfg is not None:
        def_plot = cfg.plot_type
    for idx in range(_PLOT_MAX):
        var = Tk.BooleanVar(state.root, idx == def_plot)
        state.plot_type_selector.append(var)
    mode = 'D'
    if cfg.menu_sdiff:
        mode = 'S'
    plot_chbn = [(mode+".D. Pseudorange", state.plot_type_selector[_PLOT_DD_CODE],
                  partial(_plot_selector, state, _PLOT_DD_CODE), 5),
                 (mode+".D. Carrier", state.plot_type_selector[_PLOT_DD_CARR],
                  partial(_plot_selector, state, _PLOT_DD_CARR), 5),
                 (mode+".D. Doppler", state.plot_type_selector[_PLOT_DD_DOPP],
                  partial(_plot_selector, state, _PLOT_DD_DOPP), 5),
                 ("S.D. CNo", state.plot_type_selector[_PLOT_SD_CNO],
                  partial(_plot_selector, state, _PLOT_SD_CNO), 6),
                 ("Base CNo", state.plot_type_selector[_PLOT_BASE_CN0],
                  partial(_plot_selector, state, _PLOT_BASE_CN0), 0),
                 ("Base Elev", state.plot_type_selector[_PLOT_BASE_ELV],
                  partial(_plot_selector, state, _PLOT_BASE_ELV), 6)]
    plot_menu = Tk.Menu(menubar, tearoff=0)
    _add_menu_commands(plot_menu, plot_cmds)
    plot_menu.add_separator()
    _add_menu_checkbutton(plot_menu, plot_chbn)
    menubar.add_cascade(label="Plot Type", menu=plot_menu, underline=0)

    state.ctrl_paused = Tk.BooleanVar(state.root, False)
    state.ctrl_unpaused = Tk.BooleanVar(state.root, True)

    control_menu = Tk.Menu(menubar, tearoff=0)
    control_chbn = [("Pause updates", state.ctrl_paused,
                     partial(_pause_updates, state, True), 0),
                    ("Unpause updates", state.ctrl_unpaused,
                     partial(_pause_updates, state, False), 0)]
    control_cmds = [("Enable all SVs", partial(_enable_all_svs, state, True), 0),
                    ("Disable all SVs", partial(_enable_all_svs, state, False), 0),
                    ("Clear all RT27 data", partial(_clear_rt27_data, state), 7)]
    _add_menu_checkbutton(control_menu, control_chbn)
    _add_menu_commands(control_menu, control_cmds)
    menubar.add_cascade(label="Controls", menu=control_menu, underline=0)

    about_cmds = [("About", partial(_show_about_box, state), 0),
                  ("Hackathon", partial(_show_hackathon_box, state), 0)]
    about_menu = Tk.Menu(menubar, tearoff=0)
    _add_menu_commands(about_menu, about_cmds)
    menubar.add_cascade(label="About", menu=about_menu, underline=0)

    # May need to detect the OS type and select the correct icon type
    # https://stackoverflow.com/questions/11176638/tkinter-tclerror-error-reading-bitmap-file
    # But it also sounds like it is best avoided in the first place
    #state.root.iconbitmap("Images/NoPi.ico")

###############################################################################
# SV enable / disable check-boxes

class EnableCheckbox():
    """ Class for the SV enable/disable check-boxed. """
    def __init__(self, master, sv_enbs, svid, color):
        self.var = Tk.BooleanVar(value=sv_enbs[svid])
        self.svid = svid
        self.color = color
        text = "%2d"%svid
        self.widget = Tk.Checkbutton(master,
                                     text=text,
                                     selectcolor=self.color,
                                     variable=self.var,
                                     command=partial(self.toggle, sv_enbs))
        self.widget.grid(sticky='W')
    def toggle(self, sv_enbs):
        """ Call when the check-box is clicked to toggle the plotting state. """
        sv_enbs[self.svid] = self.var.get()
    def destroy(self):
        """ Destroy the check-box to remove it from the GUI. """
        self.widget.destroy()

def get_line_color(cnum, cycle10=False):
    """ Get plot color #
        Set cycle10 true to cycle through the deault 10 colors used by Matlplotlib
        Otherwise cycle through 60 colors based on Matlplotlib
    """
    # Ten colour codes used by Matplotlib, these cycle
    colors = ["#1f77b4", "#ff7f0e", "#2ca02c", "#d62728", "#9467bd",
              "#8c564b", "#e377c2", "#7f7f7f", "#bcbd22", "#17becf"]
    idx = cnum % 10
    if cycle10:
        return colors[idx]

    # Scramble RBG as a simple way to generate more colours
    rval = colors[idx][1:3]
    gval = colors[idx][3:5]
    bval = colors[idx][5:7]
    cnum = cnum % 60
    if cnum >= 50:
        ret_col = bval+gval+rval
    elif cnum >= 40:
        ret_col = bval+rval+gval
    elif cnum >= 30:
        ret_col = gval+bval+rval
    elif cnum >= 20:
        ret_col = gval+rval+bval
    elif cnum >= 10:
        ret_col = rval+bval+gval
    else:
        ret_col = rval+gval+bval
    return "#"+ret_col

###############################################################################
# Other callbacks from small_DI

def _get_data(state, _):
    """ Return the single/double difference to plot.
        Callback to other modules that produce the data.
        _ unused input parameter, file_idx, for NoPi Live
    """
    return state.func_get_diffs()

def _sv_checkboxes(state, clear_list, svid, line_hndl, slip_hndl):
    """ Add a new SV checkbox to the current plot.
    """
    # Clear the list when the plot is re-drawn.
    if clear_list:
        for cbox in state.check_list:
            cbox.destroy()
        state.check_list = []
    # Need to:
    # - order the checkboxes by SV Id and
    # - maintain the plot colours across plot updates.
    svid = int(svid)
    svenb = state.sv_enbs[svid]
    color = get_line_color(svid)
    if line_hndl is not None:
        line_hndl.set_color(color)
        line_hndl.set_visible(svenb)
    if slip_hndl is not None:
        slip_hndl.set_color(color)
        slip_hndl.set_visible(svenb)
    if svid not in [cbox.svid for cbox in state.check_list]:
        cbox = EnableCheckbox(state.check_frame, state.sv_enbs, svid, color)
        state.check_list.append(cbox)

def _init_animate(state):
    """ Initialise the plot animation.
        interval is the plot update rate (in ms).
    """
    # update_rate in (s), interval in (ms)
    interval = 1000 * state.update_rate
    state.func_animate = animation.FuncAnimation(state.fig, _animate_plots,
                                                 fargs=(state,), interval=interval,
                                                 cache_frame_data = False)

def _animate_plots(_, state):
    """ Call to update the plot on each animation update.
        _ unused input parameter, frame_idx, for NoPi Live
    """
    if state.ctrl_paused.get():
        print('Plotting updates paused')
        return

    combo_name = _full_combo_name(state.ref_sig, state.tst_sig)
    state.filetypes[0] = combo_name
    if state.plot_type_selector[_PLOT_DD_CODE].get():
        draw_dd_code(state, 0)
    elif state.plot_type_selector[_PLOT_DD_CARR].get():
        draw_dd_carr(state, 0)
    elif state.plot_type_selector[_PLOT_DD_DOPP].get():
        draw_dd_dopp(state, 0)
    elif state.plot_type_selector[_PLOT_SD_CNO].get():
        draw_sd_cno(state, 0, scaling=1.0)
    elif state.plot_type_selector[_PLOT_BASE_CN0].get():
        draw_base_cno(state, 0)
    elif state.plot_type_selector[_PLOT_BASE_ELV].get():
        draw_base_elev(state, 0)

###############################################################################

def create_and_run_gui(config):
    """ Create and run the NoPiLive GUI """
    gui_state = State(True, func_extra_init=_extra_gui_init)
    gui_state.animating = True

    # Over-write the default small_DI configuration functions
    gui_state.func_make_menubar = _make_menubar
    gui_state.func_get_data = _get_data
    gui_state.func_checkboxes = _sv_checkboxes
    gui_state.func_init_animate = _init_animate

    # NoPi Live specific configuration functions
    gui_state.func_rx_info = config.func_rx_info
    gui_state.func_get_diffs = config.func_get_diffs
    gui_state.func_clear_rt27_data = config.func_clear_rt27_data
    gui_state.func_select_combo = config.func_select_combo
    _update_selected_combo(gui_state, gui_state.ref_sig.sat_type,
                           gui_state.ref_sig.sub_type, True)
    _update_selected_combo(gui_state, gui_state.tst_sig.sat_type,
                           gui_state.tst_sig.sub_type, False)

    # Over-write the default small_DI configuration settings
    gui_state.console = False
    gui_state.filetypes.append('Unkn')

    update_rate = config.plot_cfg.get_update_rate()
    gui_state.update_rate = update_rate

    figsize = config.plot_cfg.get_plot_size()
    make_ui(gui_state, ui_title="NoPi Live", figsize=figsize, cfg=config.startup)

    #print('UI shut-down cleanly')


###############################################################################

def main():
    """Create and run just the NoPiLive GUI
       There will be no data plotting without the full configuration
    """
    class Config():
        """Fake configuration class"""
        def __init__(self):
            self.func_get_diffs = None
            self.func_clear_rt27_data = None
            self.func_select_combo = None
    config = Config()
    create_and_run_gui(config)

if __name__ == '__main__':
    main()
