#!/usr/bin/env python3

import sys, os, datetime, re, logging
from collections import namedtuple
from pytz import timezone
import xml.etree.ElementTree as ET
from PyQt5.QtCore import Qt, QTimer
from PyQt5.QtGui import QPalette
from PyQt5.QtWidgets import (QApplication, qApp, QLabel, QPushButton, QCheckBox, QWidget, QMainWindow,
                                 QVBoxLayout, QHBoxLayout, QGridLayout, QScrollArea, QAction, QFileDialog)
from astropy.coordinates import EarthLocation
from astropy.time import Time
from astropy import units as u

from j5control import JIVE5AB
from Medusa import Medusa, MedusaConnectionError

## TODO
#


# Need to ensure these are matched to defaults in uwlCalSetup
calConfig = dict( calOn=False, frequency=0.2, dutyCycle=50, phase=0, startEdge='Rising',
                      startBat='0x122b5790caa8a3', calEpoch='2020-12-09-04:23:55.141475',
                      tsysAvgTime=5, tsysFreqRes=1.0)
calSetup = os.getenv('LBAUWB_CAL','/home/pksobs/lba/medusa/lbauwb.cal')

logging.Formatter.converter = lambda *args: datetime.datetime.now(tz=timezone('UTC')).timetuple()

Scan = namedtuple('Scan', 'name source start stop mode')
Zoom = namedtuple('Zoom', 'freq bandwidth polarisation sideband')

jive5abPort = None
medusaHost = None
antennaId = None
disableMedusa = None
disableRecorder = None
noCal = False
bits = 2

fileType = 'vdif'
dataport = 10000
tstat_update = 10 # Seconds

preStart = 0.3 # Start scans 0.5 sec early
preStart /= 24*24*60.0

log = logging.getLogger('main')

def getElem(parent, elem):
    child = parent.find(elem)
    if child is None:
        sys.exit("Error: Could not find <{}>".format(elem))  # TODO Handle more gracefully - we need to close Medusa and j5 connection
    return child.text

def base_exception_handler(exctype, value, tb):
    log.critical("Unhandled Exception ({})!!!".format(exctype))
    log.critical("  {}".format(value))
    log.critical("Foo", exc_info=(exctype, value, tb))
    #print('Type:', exctype)
    #print('Value:', value)
    #print('Traceback:', tb)

class vlbiControl(QMainWindow):
    def __init__(self, parent = None):
        super(vlbiControl, self).__init__(parent)

        self.Recording = False
        self.Streaming = False
        self.Running = False
        self.multipleDatastream = False
        self.medusaEnabled = not disableMedusa
        self.jive5abEnabled = not disableRecorder
        self.controlCal = False
        self.errorSet = dict(medusaRecording=True, medusaConnected=True, flexbufRecording=True)

        self.j5 = JIVE5AB()
        self.medusa = Medusa()

        #sys.excepthook = base_exception_handler

        # Connect to flexbuff recorder
        try:
            self.j5.open(jive5abHost, jive5abPort)
        except ConnectionRefusedError as e:
            log.error("Error connecting to jive5ab server: {}".format(e))
            sys.exit(1)

        self.bits = bits

        whitePalette = QPalette()
        whitePalette.setColor(QPalette.Window,Qt.white)

        w = QWidget()

        #layout = QHBoxLayout()
        bar = self.menuBar()
        bar.setNativeMenuBar(False)

        file = bar.addMenu("File")

        load = QAction("Load Experiment...",self)
        file.addAction(load)
        load.triggered.connect(self.LoadExperiment)

        quit = QAction("Quit",self)
        file.addAction(quit)
        quit.triggered.connect(qApp.quit)

        vb = QVBoxLayout()

        # Recorder and Medusa Status
        statusGrid = QGridLayout()

        # Current experiment name
        iw = 0
        experLab = QLabel()
        experLab.setText("Experiment:")
        statusGrid.addWidget(experLab,iw,0)
        self.experActual = QLabel()
        self.experActual.setText("None")
        self.experActual.setAutoFillBackground(True)
        self.experActual.setPalette(whitePalette)
        statusGrid.addWidget(self.experActual,iw,1)
        iw += 1

        recStatusLab = QLabel()
        recStatusLab.setText("Status:")
        statusGrid.addWidget(recStatusLab,iw,0)
        self.recStatus = QLabel()
        self.recStatus.setAutoFillBackground(True)
        self.recStatus.setPalette(whitePalette)
        statusGrid.addWidget(self.recStatus,iw,1)
        iw += 1

        self.scanLab = QLabel()
        self.scanLab.setText("Scan:")
        statusGrid.addWidget(self.scanLab,iw,0)
        self.scanStatus = QLabel()
        self.scanStatus.setAutoFillBackground(True)
        self.scanStatus.setPalette(whitePalette)
        statusGrid.addWidget(self.scanStatus,iw,1)
        iw += 1

        scanTimeLab = QLabel()
        scanTimeLab.setText("Scan Time:")
        statusGrid.addWidget(scanTimeLab,iw,0)
        self.scanTime = QLabel()
        self.scanTime.setAutoFillBackground(True)
        self.scanTime.setPalette(whitePalette)
        statusGrid.addWidget(self.scanTime,iw,1)
        iw += 1

        sourceLab = QLabel()
        sourceLab.setText("Source:")
        statusGrid.addWidget(sourceLab,iw,0)
        self.source = QLabel()
        self.source.setAutoFillBackground(True)
        self.source.setPalette(whitePalette)
        statusGrid.addWidget(self.source,iw,1)
        iw += 1

        nextSourceLab = QLabel()
        nextSourceLab.setText("Next Source:")
        statusGrid.addWidget(nextSourceLab,iw,0)
        self.nextSource = QLabel()
        self.nextSource.setAutoFillBackground(True)
        self.nextSource.setPalette(whitePalette)
        statusGrid.addWidget(self.nextSource,iw,1)
        iw += 1

        # Second Column
        iw = 0

        # Current UTC, LST Time 
        self.utcLabel = QLabel()
        self.utcLabel.setFixedSize(130, 20)  # ??
        #self.utcLabel.setStyleSheet("border: 1px solid black;")
        statusGrid.addWidget(self.utcLabel,iw,2)
        self.lstLabel = QLabel()
        self.lstLabel.setFixedSize(100, 20)
        statusGrid.addWidget(self.lstLabel,iw,3)
        #self.lstLabel.setStyleSheet("border: 1px solid black;")
        iw += 1

        recTimeLab = QLabel()
        recTimeLab.setText("Time till end:")
        statusGrid.addWidget(recTimeLab,iw,2)
        self.recTimeStatus = QLabel()
        self.recTimeStatus.setAutoFillBackground(True)
        self.recTimeStatus.setPalette(whitePalette)
        statusGrid.addWidget(self.recTimeStatus,iw,3)
        iw += 1

        medStatusLab = QLabel()
        medStatusLab.setText("Medusa:")
        statusGrid.addWidget(medStatusLab,iw,2)
        self.medStatus = QLabel()
        self.medStatus.setAutoFillBackground(True)
        self.medStatus.setPalette(whitePalette)
        statusGrid.addWidget(self.medStatus,iw,3)
        iw += 1

        modeLab = QLabel()
        modeLab.setText("Mode:")
        statusGrid.addWidget(modeLab,iw,2)
        self.modeStatus = QLabel()
        self.modeStatus.setAutoFillBackground(True)
        self.modeStatus.setPalette(whitePalette)
        statusGrid.addWidget(self.modeStatus,iw,3)
        iw += 1

        diskStatusLab = QLabel()
        diskStatusLab.setText("Disk Capacity:")
        statusGrid.addWidget(diskStatusLab,iw,2)
        self.diskStatus = QLabel()
        self.diskStatus.setAutoFillBackground(True)
        self.diskStatus.setPalette(whitePalette)
        statusGrid.addWidget(self.diskStatus,iw,3)
        iw += 1

        recRateLab = QLabel()
        recRateLab.setText("Rate:")
        statusGrid.addWidget(recRateLab,iw,2)
        self.recRate = QLabel()
        self.recRate.setAutoFillBackground(True)
        self.recRate.setPalette(whitePalette)
        statusGrid.addWidget(self.recRate,iw,3)
        iw += 1

        recLossLab = QLabel()
        recLossLab.setText("Packet Loss:")
        statusGrid.addWidget(recLossLab,iw,2)
        self.recLoss = QLabel()
        self.recLoss.setAutoFillBackground(True)
        self.recLoss.setPalette(whitePalette)
        statusGrid.addWidget(self.recLoss,iw,3)
        iw += 1



        vb.addLayout(statusGrid)

        # Start, Stop and enable recorder/Medusa
        self.startButton = QPushButton('Start')
        self.startButton.clicked.connect(self.startExper)

        self.stopButton = QPushButton('Stop')
        self.stopButton.clicked.connect(self.stopExper)
        self.stopButton.setEnabled(False)

        # TODO respond to clicking check boxes when it happens not when Start clicked
        self.calCB = QCheckBox("Control Cal")
        if (noCal): 
            self.calCB.setEnabled(False)
            self.calCB.setChecked(False)
        else:
            self.calCB.setChecked(True)

        self.recorderCB = QCheckBox("Enable Recorder")
        self.recorderCB.setChecked(not disableRecorder)
        self.recorderCB.setEnabled(False)

        self.medusaCB = QCheckBox("Enable Medusa")
        self.medusaCB.setChecked(not disableMedusa)
        #self.medusaCB.setEnabled(False)

        hb2 = QHBoxLayout()
        hb2.addWidget(self.startButton)
        hb2.addWidget(self.stopButton)
        hb2.addWidget(self.calCB)
        hb2.addWidget(self.recorderCB)
        hb2.addWidget(self.medusaCB)
        vb.addLayout(hb2)

        self.logGUI = QLabel()
        self.logGUI.setGeometry(100, 100, 200, 80)
        self.logGUI.setAlignment(Qt.AlignLeft | Qt.AlignTop)
        self.logGUI.setWordWrap(True)
        #self.logGUI.setText("vlbicontrol.py started")

        self.scroll = QScrollArea()  # Scroll Area which contains the widgets, set as the centralWidget
        self.scroll.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
        self.scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.scroll.setWidgetResizable(True)
        self.scroll.setWidget(self.logGUI)
        vb.addWidget(self.scroll)
        self.addLog("vlbicontrol.py started")

        w.setLayout(vb)
        self.setCentralWidget(w)
        self.setWindowTitle("VLBI Control")

        # Update LST and UTC in realtime
        self.observing_location = EarthLocation(lat=-32.99840*u.deg, lon=148.26352*u.deg)

        # Timer to show current time
        timer = QTimer(self)
        timer.timeout.connect(self.showtime)
        timer.start(1000)
        self.showtime()

        # Timer to control scans
        self.scanTimer = QTimer(self)
        self.scanTimer.timeout.connect(self.checkScan)

        # Timer for monitoring Medusa and other slowly changing values
        statusUpdate = QTimer(self)
        statusUpdate.timeout.connect(self.statusUpdate)
        statusUpdate.start(10000)

        self.clearExper()
        self.statusUpdate()


    def __del__(self):
        print("***** Deleting pyqt5 app****")
        if self.Recording:
            self.j5.recordingOff()
            if self.medusaEnabled: self.medusa.stopStreaming()
        self.j5.close()
        del self.j5
        del self.medusa

    def clearExper(self):
        self.ExperXML = None
        self.experName = None
        self.Scans = None
        self.Modes = None
        self.mode = None
        self.experActual.setText("None")
        self.Recording = False
        self.Running = False
        self.expername = None
        self.lastRecorderCheck = None
        self.errorSet = dict.fromkeys(self.errorSet, True)

    def showtime(self):
        utc = Time(datetime.datetime.utcnow(), scale='utc', location=self.observing_location)
        lst = utc.sidereal_time('mean')

        utc_hms = utc.datetime.strftime("%j/%H:%M:%S")
        lst_hms = lst.to_string(unit=u.hour,sep=':', precision=0)

        self.utcLabel.setText("UTC:  {}".format(utc_hms))
        self.lstLabel.setText("LST:  {}".format(lst_hms))

    def addLog(self, text, level=logging.INFO):
        hms = Time.now().datetime.strftime("%H:%M:%S:  ")
        thisText = hms+text
        if (False and level==logging.ERROR):
            thisText = '<font color="red">'+thisText+'</font>'
        self.logGUI.setText(self.logGUI.text()+thisText+"\n")
        vbar = self.scroll.verticalScrollBar()
        vbar.setValue(vbar.maximum());
        log.log(level, text)

    def LoadExperiment(self):
        # TODO add filter?
        options = QFileDialog.Options()
        options |= QFileDialog.DontUseNativeDialog
        experiment_file, _filter = QFileDialog.getOpenFileName(self, 'Load Experiment', options=options, filter="*.xml")
        if experiment_file == "": return
        self.ExperXML = experiment_file
        self.experName, self.Scans, self.Modes = self.loadXML()
        if (self.experName is not None):
            self.addLog("Loading {}".format(experiment_file))
            now = Time.now()
            if self.Scans[-1].stop < now:
                self.addLog("{} already finished".format(self.experName), level=logging.WARN)
                self.clearExper()
            else:
                self.experActual.setText(self.experName)

    def stopExper(self):
        self.stopButton.setEnabled(False)
        if self.Recording:
            self.Recording = False
            self.j5.recordingOff()
        self.Running = False
        self.scanTimer.stop()
        if (self.multipleDatastream):
            self.j5.resetDatastream()
            self.multipleDatastream = False
        #self.j5.close()
        self.mode = None
        if self.medusaEnabled: 
            self.medusa.stopStreaming()
            self.Streaming = False
        self.recTimeStatus.setText("")
        self.scanLab.setText("Scan:")
        self.scanStatus.setText("")
        self.modeStatus.setText("")
        self.source.setText("")
        self.nextSource.setText("")
        self.recRate.setText("")
        self.recLoss.setText("")
        self.scanTime.setText("")
        self.medusaCB.setEnabled(True)
        self.recorderCB.setEnabled(False)
        self.calCB.setEnabled(True)
        self.startButton.setEnabled(True)
        self.errorSet = dict.fromkeys(self.errorSet, True)

    def startExper(self):
        if self.ExperXML is None:
            self.addLog("Error: No XML loaded", level=logging.ERROR)
            return

        # Has experiment already finished
        now = Time.now()
        if self.Scans[-1].stop < now:
            self.addLog("{} already finished".format(self.experName), level=logging.WARN)
            self.clearExper()
            return

        self.startButton.setEnabled(False)
        self.stopButton.setEnabled(True)

        self.medusaCB.setEnabled(False)
        self.recorderCB.setEnabled(False)
        self.calCB.setEnabled(False)

        self.addLog("Starting {}".format(self.experName))
        if not self.recorderCB.isChecked(): self.addLog("Not controlling Recorder", level=logging.WARN)
        if not self.medusaCB.isChecked():
            self.medusaEnabled = False
            self.addLog("Not controlling Medusa", level=logging.WARN)
        else:
            self.medusaEnabled = True

        # Setup Cal
        if not noCal: # TODO Remove from global
            if  self.calCB.isChecked():
                self.calEnabled = True
                fullCalFile = self.readCal(calSetup)
                if fullCalFile is None:
                    self.stopExper()
                    return
                calConfig['calOn'] = True
                self.addLog('Enabling Cal')
                ret = os.system('uwlCalSetup --no-med -f {} calon'.format(fullCalFile))
            else:
                self.calEnabled = False
                calConfig['calOn'] = False
                ret = os.system('uwlCalSetup --no-med calOff')
            if ret!=0:
                self.addLog("uwlCalSetup failed to run ({})".format(os.WEXITSTATUS(ret)),level=logging.ERROR)
                self.stopExper()
                return

        # Skip scans already completed
        skippedSome = False
        while len(self.Scans)>0 and (self.Scans[0].stop<now):
            skippedSome = True
            del self.Scans[0]
        if skippedSome:
            self.addLog("Schedule started - skipping old scans")

        status = self.checkMode()
        if not status: 
            self.stopExper()
            return

        if self.Scans[0].start<(now-preStart):
            log.info("Starting scan {}".format(self.Scans[0].name))
            self.startScan()
        else:
            self.setNextScan()

        self.Running = True
        self.scanTimer.start(500)

    def startScan(self):
        status = self.checkMode()
        if not status: return 

        self.scanLab.setText('Current Scan:')
        self.scanStatus.setText(self.Scans[0].name)
        self.source.setText(self.Scans[0].source)
        if len(self.Scans)>1:
            self.nextSource.setText(self.Scans[1].source)
        else:
            self.nextSource.setText("-")
        self.setScanTimes()
        
        log.info("Start recording {}".format(self.Scans[0].name))
        self.j5.recordingOn("{}_{}_{}.{}".format(self.experName,antennaId,self.Scans[0].name,fileType))
        self.j5.tstat_query() # Resets counters
        self.lastRecorderCheck = Time.now()
        self.Recording = True

    def setRecorderMode(self):
        thisMode = self.recordingMode()
        log.info("Using mode {}".format(thisMode))
        self.j5.mode(thisMode)
        if len(self.Modes[self.mode])>1 and not self.multipleDatastream:
            self.addLog("Enabling multiple datastream")
            self.j5.splitDatastream()
            self.multipleDatastream = True
        elif len(self.Modes[self.mode])==1 and self.multipleDatastream:
            self.addLog("Disabling multiple datastream")
            self.j5.resetDatastream()
            self.multipleDatastream = False
            
    def configRecorder(self):
        self.j5.net_port(dataport)
        self.j5.net_protocol('udpsnor', '768M', '128M')
        self.j5.mtu(9000)
        self.setRecorderMode()        

    def startMedusa(self):
        def findSubband(freq, bandwidth, subbands):
            freq0 = freq-bandwidth/2
            freq1 = freq+bandwidth/2

            for subband, band in subbands.items():
                f = band[0]
                bw = band[1]
                f0 = f-bw/2.0
                f1 = f+bw/2.0

                # Does zoom sit within this subband
                if (freq0>=f0 and freq1<=f1):
                    if band[2] is None:
                        band[2] = freq
                        band[3] = bandwidth
                    elif band[4] is None:  # TODO Should add a check for second 128 MHz here
                        band[4] = freq
                        band[5] = bandwidth
                    else:
                        log.error("Error: Maximum 2 zooms per subband possible")
                        return(None)
                    return(subband)
                if (freq0>f0 and freq0<f1) or (freq1>f0 and freq1<f1):  # Straddles subband edge, but not fully enclosed
                    self.addLog("Warning  Zoom {}:{} straddles Subband {} edge".format(freq,bandwidth,subband),level=logging.WARNING)
                    return(None)

        if self.Streaming:
            self.medusa.stopStreaming()
            self.Streaming = False

        try:
            self.medusa.requestStatus()
        except MedusaConnectionError as e:
            self.addLog("Could not connect to Medusa ({})".format(e), level=logging.ERROR)
            return False

        state = self.medusa.getStatus()
        if not (state == 'Idle' or  state == 'Configured') :
            self.addLog("Warning: Medusa system currently \"{}\". Will not try and configure system".format(state), level=logging.ERROR)
            return False

        # Start building subband setup

        instConf = self.medusa.statusXML.find("instrument_configuration")
        #nstream = int(instConf.attrib['nstream'])

        subbands = {}

        ns = 0
        for stream in instConf.findall('stream'):
            freq = float(getElem(stream, 'centre_frequency'))
            bw = abs(float(getElem(stream, 'bandwidth')))
            subbands[stream.attrib['id']] = [freq,bw,None,None,None,None]

            ns += 1

        thisSetup = self.Modes[self.mode]
        for zoom in thisSetup:
            matchSubband = findSubband(zoom.freq, zoom.bandwidth, subbands)
            if matchSubband is None:
                self.addLog("Error: Could not match zoom {}:{}".format(zoom.freq, zoom.bandwidth), level=logging.ERROR)
                return False

        # Create Medusa Config and Load it
        configXML = self.medusa.createConfig(subbands, self.experName, calConfig, self.bits, dataport) ## TODO Set port and bits
        self.medusa.sendConfig(configXML)
        #ET.dump(configXML)

        # Now start Medusa streaming.
        #time.sleep(1) # Wait a jiffie
        self.medusa.requestStatus()
        state = self.medusa.getStatus()
        if not (state == 'Configured') :
            self.addLog("Error: Medusa system currently \"{}\". Needs to be configured to start".format(state), level=logging.ERROR)
            return False
        self.medusa.startStreaming()
        self.Streaming = True

        return True

    def setScanTimes(self):
        if self.Scans is not None and len(self.Scans)>0:
            startHMS =  self.Scans[0].start.datetime.strftime("%H:%M:%S")
            stopHMS  =  self.Scans[0].stop.datetime.strftime("%H:%M:%S")
            self.scanTime.setText("{}-{}".format(startHMS,stopHMS))
        else:
            self.scanTime.setText("")

    def setNextScan(self):
        nScan = len(self.Scans)
        if nScan>0:
            self.scanLab.setText('Next Scan:')
            self.scanStatus.setText(self.Scans[0].name)
            self.source.setText(self.Scans[0].source)
            if nScan>1:
                self.nextSource.setText(self.Scans[1].source)
            else:
                self.nextSource.setText("-")
        else:
            self.scanLab.setText('Scan:')
            self.scanStatus.setText("")
            self.source.setText("")
            self.nextSource.setText("")
        self.setScanTimes()
            
    def checkMode(self):
        if self.mode != self.Scans[0].mode:
            self.mode = self.Scans[0].mode
            self.addLog("Changing mode to {}".format(self.mode))
            self.modeStatus.setText(self.mode)
            self.configRecorder()
            if self.medusaEnabled:
                status = self.startMedusa()
                if not status:
                    self.stopExper()
                    return False
        return True

    def checkScan(self):
        # Run periodically to start/stop recorder between scans
        now = Time.now()

        # Set time till end of experiment

        timeTillEnd = (self.Scans[-1].stop-now).sec
        if timeTillEnd > 60*60:
            self.recTimeStatus.setText("{:.1f} hours".format(timeTillEnd/(60.0*60.0)))
        elif timeTillEnd > 180:
            self.recTimeStatus.setText("{:.1f} min".format(timeTillEnd/60.0))
        else:
            self.recTimeStatus.setText("{:.0f} sec".format(timeTillEnd))

        if self.Recording:
            if now>self.Scans[0].stop:
                self.j5.recordingOff()
                log.info("Scan finished")
                self.Recording = False
                self.recRate.setText("")
                self.recLoss.setText("")

                del self.Scans[0]
                if  len(self.Scans)==0:
                    # Experiment over
                    self.addLog("{} finished".format(self.experName))
                    self.stopExper()
                    self.clearExper()
                    self.scanLab.setText('Scan:')
                    self.scanStatus.setText("")
                    return
                else:
                    # TODO Probably should check if next scan should be immediately starting
                    self.setNextScan()
                    #self.scanLab.setText('Next Scan:')
                    #self.scanStatus.setText(self.Scans[0].name)
                    #self.source.setText(self.Scans[0].source)
                    #if len(self.Scans)>1:
                    #    self.nextSource.setText(self.Scans[1].source)
                    #    self.setScanTimes()
                    #else:
                    #    self.nextSource.setText("-")

                    self.checkMode()
                        
            elif (now-self.lastRecorderCheck).sec > tstat_update:
                tstat = self.j5.tstat_query()
                if tstat is not None and not "Retry - we're initialized now" in tstat[1]:
                    # !tstat? 0 : 0.0 : no_transfer ;
                    # !tstat?  0 : 10.02s : vbsrecord : UdpsNorReadStream 153.197Mbps : F 0.0% ;
                    if len(tstat)>3:
                        rawRate = tstat[3].split()[1]

                        m = re.match("(\d+(?:\.\d+)?)((?:.)?bps)", rawRate)
                        if m is not None:
                            units = m.group(2)
                            r = float(m.group(1))
                            self.recRate.setText("{:.1f} {}".format(r,units))
                        else:
                            self.addLog("Error: Could not match recorder data rate {}".format(tstat[3]), level=logging.ERROR)
                    elif 'no_transfer' in tstat[2]:
                        if self.errorSet['flexbufRecording']:
                            self.addLog("Error: Recorder status 'no_transfer' during scan", level=logging.ERROR)
                            self.errorSet['flexbufRecording'] = False
                    else:
                        self.errorSet['flexbufRecording'] = True

                evlbi_status = self.j5.evlbi_query()
                # !evlbi? 0 : total : 33320269 : loss : 57689 ( 0.17%) : out-of-order : 0 ( 0.00%) : extent : 0seqnr/pkt ;
                if evlbi_status is not None:
                    if len(evlbi_status)>4:
                        m = re.match("\d+\s*\(\s*(\S+\%)\s*\)", evlbi_status[4])
                        if m is not None:
                            self.recLoss.setText(m.group(1))
                        else:
                            log.error("Could not match evlbi packet loss {}".format(evlbi_status[4]))

                rec_status = self.j5.status_query()
                # '!status?  0', '0x00000059 ;'
                if rec_status is not None:
                    status_int = int(rec_status[1].split()[0],16)
                    if status_int&0x40 == 0: # 
                        if self.errorSet['flexbufRecording']:
                            self.addLog("Error: Recorder not running during scan. status={}".format(rec_status[1]), level=logging.ERROR)
                            self.errorSet['flexbufRecording'] = False
                    else:
                        self.errorSet['flexbufRecording'] = True
                else:
                    self.addLog("Error: 'status?' returned 'None'", level=logging.ERROR)
                self.lastRecorderCheck = now

        else:
            if self.Scans[0].start<=now:
                self.startScan()
                self.setScanTimes()

    def loadXML(self):
        def parseTime(thisTime):
            return Time(thisTime.replace('/',':'), format='yday', scale='utc')

        tree = ET.parse(self.ExperXML)  # Error handling needed
        root = tree.getroot()

        exper = getElem(root, 'exper')
        scans = []

        for s in root.findall('./sched/scan'):
            start = parseTime(getElem(s, 'start'))
            stop = parseTime(getElem(s, 'stop'))
            name = getElem(s, 'name')
            source = getElem(s, 'source')
            mode = getElem(s, 'mode')
            scans.append(Scan(name, source, start, stop, mode))
        scans.sort(key=lambda x: x.start)

        modes = {}
        for s in root.findall('./mode/setup'):
            modeName = getElem(s, 'name')
            zooms = []
            for z in s.findall('zoom'):
                freq = float(getElem(z, 'frequency'))
                bandwidth = float(getElem(z, 'bandwidth'))
                polarisation = getElem(z, 'polarisation')
                sideband = getElem(z, 'sideband')
                zooms.append(Zoom(freq,bandwidth,polarisation,sideband))
            modes[modeName] = zooms.copy()

        return exper, scans, modes

    def recordingMode(self):
        bandwidth = self.Modes[self.mode][0].bandwidth         # TODO check consistency with other zooms

        nchan = 2  # Assume for now dual pol as that is all Parkes supports
        rate = int(round(float(bandwidth)*2*nchan*self.bits))
        framesize = 8000
        #if bandwidth == 128: framesize = 8192  # Seems to come down  to data rate

        return "VDIFC_{}-{}-{}-{}".format(framesize, rate, nchan, self.bits)

    def statusUpdate(self):
        if (self.medusaEnabled):
            gotStatus = False
            try:
                self.medusa.requestStatus()
                gotStatus = True
                self.errorSet['medusaConnected'] = True
            except MedusaConnectionError as e:
                if self.errorSet['medusaConnected']:
                    self.addLog("Could not connect to Medusa ({})".format(e), level=logging.ERROR)
                    self.errorSet['medusaConnected'] = False  # Don't repeat error
            if gotStatus:
                currentStatus = self.medusa.getStatus()
                self.medStatus.setText(currentStatus)
                if self.Running:
                    if currentStatus != 'Recording':
                        if self.errorSet['medusaRecording']: 
                            self.addLog("Warning: Medusa state ({}) not Recording while experiment running, ".format(currentStatus), level=logging.WARN)
                            self.errorSet['medusaRecording'] = False  # Don't repeat error
                    else:
                        self.errorSet['medusaRecording'] = True
            else:
                self.medStatus.setText("<em>Disconnected</em>")
        else:
            self.medStatus.setText("-")

        reGB = re.compile('(\d+(?:\.\d+)?)GB')

        rePercent = re.compile('(\d+(?:\.\d+)?)\%')
        rtime = self.j5.rtime()
        if rtime is not None:
            m = reGB.match(rtime[2])
            if m:
                diskAvail = float(m.group(1))
                if diskAvail > 1000:
                    diskAvail = "{:.1f} TB".format(diskAvail/1000.0)
                else:
                    diskAvail = "{:.1f} GB".format(diskAvail)
            else:
                self.addLog("Error: rtime could not match \"{}\"".format(rtime[2]), level=logging.ERROR)
                diskAvail = "-"
            m = rePercent.match(rtime[3])
            if m:
                diskPercent = "{:.1f}%".format(float(m.group(1)))
            else:
                self.addLog("Error: rtime could not match \"{}\"".format(rtime[3]),level=logging.ERROR)
                diskPercent = "-"
            self.diskStatus.setText("{} ({})".format(diskAvail,diskPercent))

    def readCal(self, calFile):
        commentRE = re.compile(r'^([^#]*)#(.*)$')

        if not os.path.isfile(calFile):
            self.addLog("Error: '{}' does not exist".format(calSetup), level=logging.ERROR)
            return None

        with open(calFile, 'r') as f:
            for line in f:
                m = commentRE.match(line)
                if m:
                    line = m.group(1)
                    # Skip blank lines
                    if line.isspace() or line=="":
                        continue
                    ll = line.split('=')
                if len(ll)!=2:
                    self.addLog("Error: Failed to parse '{}' in {}".format(line, calFile), level=logging.ERROR)
                    return None
                key = ll[0].strip()
                val = ll[1].strip()
                if not key in calConfig:
                    self.addLog("Error: {} not in know calibration settings".format(key), level=logging.ERROR)
                    return None
                log.info("Setting cal '{}' to '{}'".format(key,val))
                calConfig[key] = val  # This is a string now

        return os.path.abspath(calFile)

## TODO  Add signal handler

if __name__ == '__main__':
    import argparse

    # Root logger
    root_logger = logging.getLogger()
    root_logger.setLevel(logging.DEBUG)

    # Console output
    console = logging.StreamHandler()
    formatter = logging.Formatter('%(asctime)s - %(levelname)-5s - %(message)s', datefmt="%Y-%m-%d %H:%M:%S")
    console.setFormatter(formatter)
    console.setLevel(logging.INFO)
    root_logger.addHandler(console)

    # Logfile output
    file_logger = logging.FileHandler('vlbicontrol.log')
    formatter = logging.Formatter('%(asctime)s: %(name)-9s - %(levelname)-5s - %(message)s', datefmt="%Y-%m-%d %H:%M:%S")
    file_logger.setFormatter(formatter)
    file_logger.setLevel(logging.DEBUG)
    root_logger.addHandler(file_logger)
    
    jive5abHost = os.environ.get('LBA_JIVE5ABHOST', 'pkstore.atnf.csiro.au')
    jive5abPort = os.environ.get('LBA_JIVE5ABPORT', 2620)
    medusaHost = os.environ.get('LBA_MEDUSAHOST', 'medusa-srv0.atnf.csiro.au')
    antennaId = os.environ.get('LBA_ANTID', 'Pa')

    parser = argparse.ArgumentParser()
    parser.add_argument('-j5host', '--j5host', help="Jive5ab Host")
    parser.add_argument('-medusahost', '--medusahost', help="Medusa Host")
    parser.add_argument('-disablemedusa', '--disablemedusa', help="Don't connect to Medusa", action="store_true")
    parser.add_argument('-disablerecorder', '--disablerecorder', help="Don't connect to Recorder", action="store_true")
    parser.add_argument('-nocal', '--nocal', help="Don't touch cal at all", action="store_true")
    parser.add_argument('-bits', '--bits', help="Number of bits/sample", default=2, type=int)
    args, unparsed_args = parser.parse_known_args()

    disableMedusa = args.disablemedusa
    disableRecorder = args.disablerecorder
    noCal = args.nocal
    bits = args.bits
    if args.j5host is not None: jive5abHost = args.j5host
    if args.medusahost is not None: medusaHost = args.medusahost
    medusaHost = args.medusahost

    app = QApplication(sys.argv[:1] + unparsed_args)
    ex = vlbiControl()
    ex.show()

    rt = app.exec_()
    #del ex
    sys.exit(rt)
