#!/usr/bin/env python
###############################################################################
# Copyright 2020 - 2022 Trimble Inc.
# $Id: nopilive.py,v 1.6 2024/02/11 20:58:50 wlentz Exp $
# $Source: /home/wlentz/cvs_stinger/GPSTools/NoPiLive/nopilive.py,v $
###############################################################################

""" NoPiLive
    NoPiLive is a GNSS single/double difference measurement generation and
    visualisation tool. It decodes the GNSS measurements from RT27 streams so
    that the receiver tracking performance can be monitored in real-time.
    Handy for finding rare bugs that are normally only spotted after the fact
    using data post-processing.
"""

import matplotlib.style as mplstyle
mplstyle.use('fast')
from curve_fit_collector import CurveFitCollectorThread
from rt27_collector import RT27MeasCollectorThread
from rt27_meas_ddiff import RT27MeasDDiffThread
from nopilive_config import VERSION_STR, COPYRIGHT_STR, NoPiLiveConfig, nopilive_cli
from nopilive_gui import create_and_run_gui
import threading
import time, os

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

class _InterThreadGlueLogic():
    """ Class holding the glue logic functions for inter-thread communication.
        The threads are not all of the same class type. Using glue logic adds
        an extra calling layer but reduces the ammount of information each
        thread object needs to know about other object classes.
    """
    def __init__(self, config, base_rt27_thrd, rovr_rt27_thrd, gen_diffs_thrd):
        # The UI runs in the main thread, not available here
        self.config = config
        self.base_rt27_thrd = base_rt27_thrd
        self.rovr_rt27_thrd = rovr_rt27_thrd
        self.gen_diffs_thrd = gen_diffs_thrd

    def rx_info(self, is_base):
        """ Return the receiver information in a dictionary
            Set is_base True to return information for the base receiver or to
            False to return information for the rover receiver.
        """
        if is_base:
            cfg = self.config.base_cfg
            rt27 = self.base_rt27_thrd
        else:
            cfg = self.config.rovr_cfg
            rt27 = self.rovr_rt27_thrd
        rx_info = {}
        rx_info['Name'] = cfg.name
        rx_info['RT27Src'] = cfg.rt27_source()
        rx_info['AntName'] = cfg.ant_name
        rx_info['AntXYZ'] = cfg.ant_xyz
        rx_info['ZBLine'] = self.config.zero_baseline()
        rx_info['Epochs'] = rt27.num_epochs()
        rx_info['Time'] = rt27.get_time(rx_info['Epochs'] - 1)
        return rx_info

    def get_diffs(self):
        """ Return the current single/double difference data array.
            The format matches the NoPi output format.
        """
        if self.gen_diffs_thrd is None:
            return None
        return self.gen_diffs_thrd.get_nopi_diffs()

    def clear_data(self):
        """ Clear all of the RT27 data. This also implies clearing the
            generated single/double difference data, carrier ambiguity data
            etc.. RT27 collection is not halted, it just starts again.
        """
        if self.base_rt27_thrd is not None:
            self.base_rt27_thrd.del_all_epochs()
        if self.rovr_rt27_thrd is not None:
            self.rovr_rt27_thrd.del_all_epochs()
        if self.gen_diffs_thrd is not None:
            self.gen_diffs_thrd.del_all_data()

    def select_combo(self, sat_type, sub_type, is_ref):
        """ Select a new signal combination (combo) for single/double
            difference generation.
            The signal is defined by sat_type and sub_type using the Stinger
            enumeration. See, for example, mutils/RcvrConst.py.
            Need to specify both the reference signal combination, is_ref is
            True, and test signal combination, is_ref is False, to fully
            configure the combo. The reference and test modes are typically the
            same values.
            Note that only the reference signal combo is currently used. The
            code will be updated later to support different reference and test
            combos.
        """
        if self.gen_diffs_thrd is not None:
            self.gen_diffs_thrd.set_combo(sat_type, sub_type, is_ref)


class WatchdogThread(threading.Thread):
    """Watch all threads and kill the entire program if any of them fails.
    Is there a better way to do this?
    """
    def __init__(self, all_threads):
        threading.Thread.__init__(self)
        self.all_threads = all_threads
    def run(self):
        while True:
            time.sleep(0.2)
            for thread in self.all_threads:
                if not thread.is_alive():
                    print("%s thread died. Exiting"%thread.name)
                    os._exit(1)

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

def _print_start_up():
    """ Display the NoPiLive start-up screen information.
    """
    print("NoPiLive - version "+VERSION_STR)
    print(COPYRIGHT_STR)
    print("Internal use only, not licensed for distribution outside of Trimble Engineering.")
    print()
    print("Handy Hints")
    print("1) Select Control -> Pause updates before using the plot toolbar.")
    print("2) Try Ctrl + C twice or Ctrl + Break to halt threads if the GUI is not active.")
    print()

def main():
    """ Run NoPiLive """

    _print_start_up()

    # Default configuration for NoPiLive and user options from command line
    # or .ini file
    config = NoPiLiveConfig()
    good_cfg = nopilive_cli(config)
    if not good_cfg:
        return

    # Create the RT27 collection threads and the measurement differencing thread
    base_rt27_cltr_thread = RT27MeasCollectorThread('Base',
                                                    config.base_cfg.ip_addr,
                                                    config.base_cfg.port_num,
                                                    config.base_cfg.ant_num,
                                                    config.base_cfg.rt27_file_name,
                                                    verbose=config.verbose)
    rovr_rt27_cltr_thread = RT27MeasCollectorThread('Rover',
                                                    config.rovr_cfg.ip_addr,
                                                    config.rovr_cfg.port_num,
                                                    config.rovr_cfg.ant_num,
                                                    config.rovr_cfg.rt27_file_name,
                                                    verbose=config.verbose)
    curve_fit_thread = CurveFitCollectorThread(config.cfit_cfg.ip_addr,
                                               config.cfit_cfg.user,
                                               config.cfit_cfg.password,
                                               config.cfit_cfg.req_rate)
    gen_diffs_thread = RT27MeasDDiffThread(base_rt27_cltr_thread,
                                           rovr_rt27_cltr_thread,
                                           curve_fit_thread,
                                           config.base_cfg.ant_xyz,
                                           config.rovr_cfg.ant_xyz,
                                           config.base_cfg.cmn_clk,
                                           verbose=config.verbose)

    # Create and initialise the inter-thread glue logic
    glue_logic = _InterThreadGlueLogic(config,
                                       base_rt27_cltr_thread,
                                       rovr_rt27_cltr_thread,
                                       gen_diffs_thread)
    config.func_rx_info = glue_logic.rx_info
    config.func_get_diffs = glue_logic.get_diffs
    config.func_clear_rt27_data = glue_logic.clear_data
    config.func_select_combo = glue_logic.select_combo

    # Start all of the threads. Make them "daemon" so if main thread exits they stop.
    all_threads = [base_rt27_cltr_thread,
                   rovr_rt27_cltr_thread,
                   gen_diffs_thread ]
    watchdog_thread = WatchdogThread(all_threads)
    all_threads.append( watchdog_thread)
    for thread in all_threads:
        # daemon=True --> all threads automatically clean up when main() exits
        thread.daemon=True
        thread.start()

    # Create and run the NoPiLive UI
    create_and_run_gui(config)


if __name__ == '__main__':
    main()
