#!/usr/bin/python3
# 
# Copyright 2017 ghostop14
# 
# This is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3, or (at your option)
# any later version.
# 
# This software is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
# 
# You should have received a copy of the GNU General Public License
# along with this software; see the file COPYING.  If not, write to
# the Free Software Foundation, Inc., 51 Franklin Street,
# Boston, MA 02110-1301, USA.
# 

from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QApplication, QLabel, QComboBox, QLineEdit, QPushButton, QAbstractItemView
from PyQt5.QtWidgets  import QFileDialog, QSpinBox, QDesktopWidget, QMessageBox, QTableWidget, QHeaderView,QTableWidgetItem,  QMenu, QAction
from sparrowtablewidgets import DateTableWidgetItem, FloatTableWidgetItem, IntTableWidgetItem
from PyQt5.QtCore import Qt,QTimer
from PyQt5 import QtCore

from socket import *
import datetime
from threading import Thread, Lock
from time import sleep
import requests
import json
import re
# import urllib
from urllib.request import urlretrieve

import os

from sparrowmap import MapEngine
from sparrowwifiagent import FileSystemFile
from sparrowbluetooth import SparrowBluetooth, BluetoothDevice
from telemetry import BluetoothTelemetry
from sparrowmap import MapMarker
from wirelessengine import WirelessEngine

# ------------------  Global File Dialogs ------------------------------
def openFileDialog(fileSpec="CSV Files (*.csv);;All Files (*)"):    
    options = QFileDialog.Options()
    options |= QFileDialog.DontUseNativeDialog
    fileName, _ = QFileDialog.getOpenFileName(None,"QFileDialog.getOpenFileName()", "",fileSpec, options=options)
    if fileName:
        return fileName
    else:
        return None


def saveFileDialog(fileSpec="CSV Files (*.csv);;All Files (*)"):    
    options = QFileDialog.Options()
    options |= QFileDialog.DontUseNativeDialog
    fileName, _ = QFileDialog.getSaveFileName(None,"QFileDialog.getSaveFileName()","",fileSpec, options=options)
    if fileName:
        return fileName
    else:
        return None


# ------------------  Global functions for agent HTTP requests ------------------------------
def makeGetRequest(url):
    try:
        # Not using a timeout can cause the request to hang indefinitely
        response = requests.get(url, timeout=2)
    except:
        return -1, ""
        
    if response.status_code != 200:
        return response.status_code, ""
        
    htmlResponse=response.text
    return response.status_code, htmlResponse

def getRemoteBluetoothRunningServices(agentIP, agentPort):
    url = "http://" + agentIP + ":" + str(agentPort) + "/bluetooth/running"
    statusCode, responsestr = makeGetRequest(url)
    
    if statusCode == 200:
        try:
            responsedict = json.loads(responsestr)
            errcode = responsedict['errcode']
            errmsg = responsedict['errmsg']
            hasBluetooth = responsedict['hasbluetooth']
            hasUbertooth = responsedict['hasubertooth']
            spectrumScanRunning = responsedict['spectrumscanrunning']
            discoveryScanRunning = responsedict['discoveryscanrunning']
            
            return errcode, errmsg, hasBluetooth, hasUbertooth, spectrumScanRunning, discoveryScanRunning
        except:
            return -1, 'Error parsing response', False, False, False, False
    else:
            return -2, 'Bad response from agent [' + str(statusCode) + ']', False, False, False, False
        
def startRemoteBluetoothDiscoveryScan(agentIP, agentPort, ubertooth):
    if ubertooth:
        # Promiscuous
        url = "http://" + agentIP + ":" + str(agentPort) + "/bluetooth/discoverystartp"
    else:
        # Advertisements only
        url = "http://" + agentIP + ":" + str(agentPort) + "/bluetooth/discoverystarta"
        
    statusCode, responsestr = makeGetRequest(url)
    
    if statusCode == 200:
        try:
            responsedict = json.loads(responsestr)
            errcode = responsedict['errcode']
            errmsg = responsedict['errmsg']
            return errcode, errmsg
        except:
            return -1, 'Error parsing response'
    else:
            return -2, 'Bad response from agent [' + str(statusCode) + ']'
        
def stopRemoteBluetoothDiscoveryScan(agentIP, agentPort):
    url = "http://" + agentIP + ":" + str(agentPort) + "/bluetooth/discoverystop"
    statusCode, responsestr = makeGetRequest(url)
    
    if statusCode == 200:
        try:
            responsedict = json.loads(responsestr)
            errcode = responsedict['errcode']
            errmsg = responsedict['errmsg']
            return errcode, errmsg
        except:
            return -1, 'Error parsing response'
    else:
            return -2, 'Bad response from agent [' + str(statusCode) + ']'

def getRemoteBluetoothDiscoveryStatus(agentIP, agentPort):
    url = "http://" + agentIP + ":" + str(agentPort) + "/bluetooth/discoverystatus"
    statusCode, responsestr = makeGetRequest(url)
    
    if statusCode == 200:
        try:
            responsedict = json.loads(responsestr)
            errcode = responsedict['errcode']
            errmsg = responsedict['errmsg']
            tmpDeviceData = responsedict['devices']
            devices = {}
            for curDevice in tmpDeviceData:
                newdevice = BluetoothDevice()
                try:
                    newdevice.fromJsondict(curDevice)
                    devices[newdevice.macAddress] = newdevice
                except:
                    pass
            return errcode, errmsg, devices
        except:
            return -1, 'Error parsing response', None
    else:
            return -2, 'Bad response from agent [' + str(statusCode) + ']', None
        
def getRemoteRecordingsFiles(agentIP, agentPort):
    url = "http://" + agentIP + ":" + str(agentPort) + "/system/getrecordings"
    statusCode, responsestr = makeGetRequest(url)
    
    if statusCode == 200:
        try:
            responsedict = json.loads(responsestr)
            filelist = []
            try:
                for curFileDict in responsedict['files']:
                    curFile = FileSystemFile()
                    curFile.fromJsondict(curFileDict)
                    filelist.append(curFile)
                return 0, "", filelist
            except:
                return 2, "Error parsing response: " + responsestr, None
        except:
            return 1, "Error parsing response: " + responsestr, None
    else:
        return statusCode, 'Received error code: ' + str(statusCode), None
        
def delRemoteRecordingFiles(remoteIP, remotePort, filelist):
    url = "http://" + remoteIP + ":" + str(remotePort) + "/system/deleterecordings"
    
    filedict={}
    filedict['files'] = filelist
        
    jsonstr = json.dumps(filedict)
    statusCode, responsestr = makePostRequest(url, jsonstr)

    errcode = -1
    errmsg = ""
    
    if statusCode == 200 or statusCode == 400:
        try:
            responsedict = json.loads(responsestr)
            try:
                errcode = responsedict['errcode']
                errmsg = responsedict['errmsg']
            except:
                # response json didn't have the expected field
                if len(responsestr) == 0:
                    errmsg = "Error parsing agent response.  Is it still running?"
                else:
                    errmsg = "Error parsing agent response:" + responsestr                
        except:
            # Parsing json threw exception
            if len(responsestr) == 0:
                errmsg = "Error parsing agent response.  Is it still running?"
            else:
                errmsg = "Error parsing agent response:" + responsestr
    else:
        # This should never happen
        if len(responsestr) == 0:
            errmsg = "Error updating remote agent.  Is it still running?"
        else:
            errmsg = "Error updating remote agent:" + responsestr
            
    return errcode, errmsg

def startRecord(agentIP, agentPort, interface):
    url = "http://" + agentIP + ":" + str(agentPort) + "/system/startrecord/" + interface
    statusCode, responsestr = makeGetRequest(url)
    
    if statusCode == 200:
        try:
            responsedict = json.loads(responsestr)
            try:
                errcode = responsedict['errcode']
                errmsg = responsedict['errmsg']
                return errcode, errmsg
            except:
                return 2, "Error parsing response: " + responsestr
        except:
            return 1, "Error parsing response: " + responsestr
    else:
        return statusCode, 'Received error code: ' + str(statusCode)
        
def stopRecord(agentIP, agentPort):
    url = "http://" + agentIP + ":" + str(agentPort) + "/system/stoprecord"
    statusCode, responsestr = makeGetRequest(url)
    
    if statusCode == 200:
        try:
            responsedict = json.loads(responsestr)
            try:
                errcode = responsedict['errcode']
                errmsg = responsedict['errmsg']
                return errcode, errmsg
            except:
                return 2, "Error parsing response: " + responsestr
        except:
            return 1, "Error parsing response: " + responsestr
    else:
        return statusCode, 'Received error code: ' + str(statusCode)
        
def makePostRequest(url, jsonstr):
        # use something like jsonstr = json.dumps(somestring) to get the right format
        try:
            response = requests.post(url, data=jsonstr, timeout=2)
        except:
            return -1, ""
        
        htmlResponse=response.text
        return response.status_code, htmlResponse
        
def updateRemoteConfig(remoteIP, remotePort, startupCfg, runningCfg, sendRestart=False):
    url = "http://" + remoteIP + ":" + str(remotePort) + "/system/config"
    
    cfgdict = {}
    cfgdict['startup'] = startupCfg.toJsondict()
    cfgdict['running'] = runningCfg.toJsondict()
    
    if sendRestart:
        cfgdict['rebootagent'] = True
        
    jsonstr = json.dumps(cfgdict)
    statusCode, responsestr = makePostRequest(url, jsonstr)

    errmsg = ""
    
    if statusCode == 200:
        return 0, ""
    elif statusCode == 400:
        # 400 is a JSON response
        try:
            responsedict = json.loads(responsestr)
            try:
                errmsg = responsedict['errmsg']
            except:
                # response json didn't have the expected field
                if len(responsestr) == 0:
                    errmsg = "Error parsing agent response.  Is it still running?"
                else:
                    errmsg = "Error parsing agent response:" + responsestr                
        except:
            # Parsing json threw exception
            if len(responsestr) == 0:
                errmsg = "Error parsing agent response.  Is it still running?"
            else:
                errmsg = "Error parsing agent response:" + responsestr
    else:
        # This should never happen
        if len(responsestr) == 0:
            errmsg = "Error updating remote agent.  Is it still running?"
        else:
            errmsg = "Error updating remote agent:" + responsestr
            
        return -1, errmsg

#  -----------  DB Settings ----------------------------
# Note: This is not used in the main GUI
class DBSettings(object):
    SQLITE = 1
    POSTGRES = 2
    
    def __init__(self):
        super().__init__()
        self.dbMode = DBSettings.SQLITE
        self.db = ""  # This will be a file for SQLite, or a database name for Postgres
        self.tablename = "wirelessnetworks"
        
        # These are only needed for Postgres
        self.hostip = ""
        self.username = ""
        self.password = ""
        
class DBSettingsDialog(QDialog):
    def __init__(self, parent = None):
        super(DBSettingsDialog, self).__init__(parent)

        self.dbMode = DBSettings.SQLITE  # 1 = SQLite, 2 = Postgres
        # layout = QVBoxLayout(self)

        # DB Type droplist
        self.lblDBType = QLabel("DB Type", self)
        self.lblDBType.setGeometry(30, 26, 100, 30)
        
        self.combo = QComboBox(self)
        self.combo.move(110, 30)
        self.combo.addItem("SQLite")
        self.combo.addItem("Postgres")
        self.combo.currentIndexChanged.connect(self.onDBChanged)

        # SQLLite:
        self.lblDB = QLabel("DB/File: ", self)
        self.lblDB.move(30, 84)
        self.dbinput = QLineEdit(self)
        self.dbinput.setGeometry(110, 80, 250, 20)
        self.btnOpen = QPushButton("&Open", self)
        self.btnOpen.move(380, 80)
        self.btnOpen.clicked.connect(self.onFileClicked)

        spacing = 35
        # Table name
        self.lblDBHost = QLabel("Table Name: ", self)
        self.lblDBHost.move(30, 88+spacing)
        self.dbtable = QLineEdit(self)
        self.dbtable.setText("wirelessnetworks")
        self.dbtable.setGeometry(110, 84+spacing, 200, 20)

        # Postgres:
        self.lblDBHost = QLabel("Host IP: ", self)
        self.lblDBHost.move(30, 87+spacing*2)
        self.dbhost = QLineEdit(self)
        self.dbhost.setText("127.0.0.1")
        self.dbhost.setGeometry(110, 84+spacing*2, 200, 20)
        
        self.lblDBUser = QLabel("Username: ", self)
        self.lblDBUser.move(30, 90+spacing*3)
        self.dbuser = QLineEdit(self)
        self.dbuser.setGeometry(110, 88+spacing*3, 200, 20)
        
        self.lblDBPass = QLabel("Password: ", self)
        self.lblDBPass.move(30, 86+spacing*4)
        self.dbpass = QLineEdit(self)
        self.dbpass.setEchoMode(QLineEdit.Password)
        self.dbpass.setGeometry(110, 84+spacing*4, 200, 20)

        # Start in SQLite Mode:
        self.setPostgresVisible(False)
        
        # OK and Cancel buttons
        buttons = QDialogButtonBox(
            QDialogButtonBox.Ok | QDialogButtonBox.Cancel,
            Qt.Horizontal, self)
        buttons.accepted.connect(self.accept)
        buttons.rejected.connect(self.reject)
        buttons.move(170, 280)
        #layout.addWidget(buttons)
        self.setGeometry(self.geometry().x(), self.geometry().y(), 500,320)
        self.setWindowTitle("Database Settings")


    def setPostgresVisible(self, vis):
        self.lblDBHost.setVisible(vis)
        self.dbhost.setVisible(vis)
        self.lblDBUser.setVisible(vis)
        self.dbuser.setVisible(vis)
        self.lblDBPass.setVisible(vis)
        self.dbpass.setVisible(vis)

    def onFileClicked(self):
        fileName = self.saveFileDialog()

        if not fileName:
            return
        else:
            self.dbinput.setText(fileName)

    def onDBChanged(self, index):
        self.dbMode = index
        
        if index == 0:
            self.setPostgresVisible(False)
        else:
            self.setPostgresVisible(True)
        
    def saveFileDialog(self):    
        options = QFileDialog.Options()
        options |= QFileDialog.DontUseNativeDialog
        fileName, _ = QFileDialog.getSaveFileName(self,"QFileDialog.getSaveFileName()","","SQLite3 Files (*.sqlite3);;All Files (*)", options=options)
        if fileName:
            return fileName
        else:
            return None

    def getDBSettings(self):
        dbSettings = DBSettings()
        dbSettings.dbMode = self.dbMode
        dbSettings.db = self.dbinput.text()
        dbSettings.hostip = self.dbhost.text()
        dbSettings.user = self.dbuser.text()
        dbSettings.password = self.dbpass.text()
        dbSettings.tableName = self.dbtable.text()
        
        return dbSettings
        
    # static method to create the dialog and return (date, time, accepted)
    @staticmethod
    def getSettings(parent = None):
        dialog = DBSettingsDialog(parent)
        result = dialog.exec_()
        # date = dialog.dateTime()
        dbSettings = dialog.getDBSettings()
        return (dbSettings, result == QDialog.Accepted)

class MapSettings(object):
    def __init__(self):
        super().__init__()
        self.maptype = MapEngine.MAP_TYPE_DEFAULT
        self.plotstrongest = True
        self.outputfile = ""
        self.title = ""
        self.maxLabelLength = 15
        
class MapSettingsDialog(QDialog):
    def __init__(self, parent = None, skipControls = False):
        super(MapSettingsDialog, self).__init__(parent)

        self.center()
        
        if skipControls:
            return
            
        # Map Type droplist
        self.lblMapType = QLabel("Map Type", self)
        self.lblMapType.setGeometry(30, 26, 100, 30)
        
        self.combo = QComboBox(self)
        self.combo.setGeometry(115, 30, 140, 30)
        self.combo.addItem("Standard Street")
        self.combo.addItem("Hybrid Satellite")
        self.combo.addItem("Satellite Only")
        self.combo.addItem("Terrain")

        # Plot strongest or last
        self.lblMapType = QLabel("Coord Set", self)
        self.lblMapType.setGeometry(30, 84, 100, 30)
        
        self.comboplot = QComboBox(self)
        self.comboplot.move(115, 84)
        self.comboplot.addItem("Strongest Signal")
        self.comboplot.addItem("Last Signal")

        # File:
        self.lblFile = QLabel("Output File: ", self)
        self.lblFile.move(30, 124)
        self.fileinput = QLineEdit(self)
        self.fileinput.setGeometry(115, 120, 250, 20)
        self.btnOpen = QPushButton("&Save", self)
        self.btnOpen.move(380, 120)
        self.btnOpen.clicked.connect(self.onFileClicked)

        spacing = 35
        # Table name
        self.lblTitle = QLabel("Map Title: ", self)
        self.lblTitle.move(30, 129+spacing)
        self.title = QLineEdit(self)
        self.title.setText("Access Point Map")
        self.title.setGeometry(115, 124+spacing, 200, 20)

        self.lblMaxLen = QLabel("Max Label Length: ", self)
        self.lblMaxLen.move(30, 133+spacing*2)
        self.spinMaxLen = QSpinBox(self)
        self.spinMaxLen.setRange(1, 100)
        
        self.spinMaxLen.setValue(15)
        self.spinMaxLen.setGeometry(150, 125+spacing*2, 50, 28)

        # OK and Cancel buttons
        buttons = QDialogButtonBox(
            QDialogButtonBox.Ok | QDialogButtonBox.Cancel,
            Qt.Horizontal, self)
        buttons.accepted.connect(self.accept)
        buttons.rejected.connect(self.reject)
        buttons.move(170, 280)
        #layout.addWidget(buttons)
        self.setGeometry(self.geometry().x(), self.geometry().y(), 500,320)
        self.setWindowTitle("Map Settings")

    def center(self):
        # Get our geometry
        qr = self.frameGeometry()
        # Find the desktop center point
        cp = QDesktopWidget().availableGeometry().center()
        # Move our center point to the desktop center point
        qr.moveCenter(cp)
        # Move the top-left point of the application window to the top-left point of the qr rectangle, 
        # basically centering the window
        self.move(qr.topLeft())
        
    def onFileClicked(self):
        fileName = self.saveFileDialog()

        if not fileName:
            return
        else:
            self.fileinput.setText(fileName)

    def saveFileDialog(self):    
        options = QFileDialog.Options()
        options |= QFileDialog.DontUseNativeDialog
        fileName, _ = QFileDialog.getSaveFileName(self,"QFileDialog.getSaveFileName()","","HTML Files (*.html);;All Files (*)", options=options)
        if fileName:
            return fileName
        else:
            return None

    def done(self, result):
        if result == QDialog.Accepted:
            if len(self.fileinput.text()) == 0:
                QMessageBox.question(self, 'Error',"Please provide an output file.", QMessageBox.Ok)

                return
            
        super().done(result)
        
    def getMapSettings(self):
        mapSettings = MapSettings()
        
        strType = self.combo.currentText()
        
        if (strType == 'Hybrid Satellite'):
            mapSettings.mapType = MapEngine.MAP_TYPE_HYBRID
        elif (strType == 'Satellite Only'):
            mapSettings.mapType = MapEngine.MAP_TYPE_SATELLITE_ONLY
        elif (strType == 'Terrain'):
            mapSettings.mapType = MapEngine.MAP_TYPE_TERRAIN
        else:
            mapSettings.mapType = MapEngine.MAP_TYPE_DEFAULT
            
        if (self.comboplot.currentText() == 'Strongest Signal'):
            mapSettings.plotstrongest = True
        else:
            mapSettings.plotstrongest = False

        mapSettings.title = self.title.text()
        mapSettings.outputfile = self.fileinput.text()
        mapSettings.maxLabelLength = self.spinMaxLen.value()
        
        return mapSettings
        
    # static method to create the dialog and return (date, time, accepted)
    @staticmethod
    def getSettings(parent = None):
        dialog = MapSettingsDialog(parent)
        result = dialog.exec_()
        # date = dialog.dateTime()
        mapSettings = dialog.getMapSettings()
        return (mapSettings, result == QDialog.Accepted)

class TelemetryMapSettings(MapSettings):
    def __init__(self):
        super().__init__()
        self.inputfile = ""
        self.plotNthPoint = 1
        
class TelemetryMapSettingsDialog(MapSettingsDialog):
    def __init__(self, parent = None):
        super(TelemetryMapSettingsDialog, self).__init__(parent, True)

        # Map Type droplist
        self.lblMapType = QLabel("Map Type", self)
        self.lblMapType.setGeometry(30, 26, 100, 30)
        
        self.combo = QComboBox(self)
        self.combo.setGeometry(115, 30, 140, 30)
        self.combo.addItem("Standard Street")
        self.combo.addItem("Hybrid Satellite")
        self.combo.addItem("Satellite Only")
        self.combo.addItem("Terrain")

        # Input File:
        self.lblInputFile = QLabel("Input File: ", self)
        self.lblInputFile.move(30, 84)
        self.inputfileinput = QLineEdit(self)
        self.inputfileinput.setGeometry(115, 84, 250, 20)
        self.btnInputOpen = QPushButton("&Open", self)
        self.btnInputOpen.move(380, 84)
        self.btnInputOpen.clicked.connect(self.onInputFileClicked)

        # Output File:
        self.lblFile = QLabel("Output File: ", self)
        self.lblFile.move(30, 124)
        self.fileinput = QLineEdit(self)
        self.fileinput.setGeometry(115, 120, 250, 20)
        self.btnOpen = QPushButton("&Save", self)
        self.btnOpen.move(380, 120)
        self.btnOpen.clicked.connect(self.onFileClicked)

        spacing = 35
        # Map Title
        self.lblTitle = QLabel("Map Title: ", self)
        self.lblTitle.move(30, 129+spacing)
        self.title = QLineEdit(self)
        self.title.setText("SSID Map")
        self.title.setGeometry(115, 124+spacing, 200, 20)

        # Max label length
        self.lblMaxLen = QLabel("Max Label Length: ", self)
        self.lblMaxLen.move(30, 133+spacing*2)
        self.spinMaxLen = QSpinBox(self)
        self.spinMaxLen.setRange(1, 100)
        self.spinMaxLen.setValue(15)
        self.spinMaxLen.setGeometry(145, 126+spacing*2, 50, 28)
        
        # Nth Point
        self.lblplot = QLabel("Plot every ", self)
        self.lblplot.move(30, 133+spacing*3)
        self.spinplot = QSpinBox(self)
        self.spinplot.setRange(1, 1000)
        self.lblplot2 = QLabel("points", self)
        self.lblplot2.move(170, 133+spacing*3)
        
        self.spinplot.setValue(1)
        self.spinplot.setGeometry(115, 125+spacing*3, 50, 28)

        # OK and Cancel buttons
        buttons = QDialogButtonBox(
            QDialogButtonBox.Ok | QDialogButtonBox.Cancel,
            Qt.Horizontal, self)
        buttons.accepted.connect(self.accept)
        buttons.rejected.connect(self.reject)
        buttons.move(170, 280)
        
        self.setGeometry(self.geometry().x(), self.geometry().y(), 500,320)
        self.setWindowTitle("SSID Map Settings")

    def onInputFileClicked(self):
        fileName = self.openFileDialog()

        if not fileName:
            return
        else:
            self.inputfileinput.setText(fileName)
        
    def done(self, result):
        if result == QDialog.Accepted:
            if len(self.inputfileinput.text()) == 0:
                QMessageBox.question(self, 'Error',"Please provide an input file.", QMessageBox.Ok)
                return
                
            if len(self.fileinput.text()) == 0:
                QMessageBox.question(self, 'Error',"Please provide an output file.", QMessageBox.Ok)
                return
            
        super().done(result)
        
    def openFileDialog(self):    
        options = QFileDialog.Options()
        options |= QFileDialog.DontUseNativeDialog
        fileName, _ = QFileDialog.getOpenFileName(self,"QFileDialog.getOpenFileName()", "","CSV Files (*.csv);;All Files (*)", options=options)
        if fileName:
            return fileName
        else:
            return None
 
    def getMapSettings(self):
        mapSettings = TelemetryMapSettings()
        
        strType = self.combo.currentText()
        
        if (strType == 'Hybrid Satellite'):
            mapSettings.mapType = MapEngine.MAP_TYPE_HYBRID
        elif (strType == 'Satellite Only'):
            mapSettings.mapType = MapEngine.MAP_TYPE_SATELLITE_ONLY
        elif (strType == 'Terrain'):
            mapSettings.mapType = MapEngine.MAP_TYPE_TERRAIN
        else:
            mapSettings.mapType = MapEngine.MAP_TYPE_DEFAULT
            
        mapSettings.title = self.title.text()
        mapSettings.outputfile = self.fileinput.text()
        mapSettings.maxLabelLength = self.spinMaxLen.value()
        
        mapSettings.inputfile = self.inputfileinput.text()
        
        mapSettings.plotNthPoint = self.spinplot.value()
        
        return mapSettings
        
    # static method to create the dialog and return (date, time, accepted)
    @staticmethod
    def getSettings(parent = None):
        dialog = TelemetryMapSettingsDialog(parent)
        result = dialog.exec_()
        # date = dialog.dateTime()
        mapSettings = dialog.getMapSettings()
        return (mapSettings, result == QDialog.Accepted)

# ------------------  UDP Listen thread  ------------------------------
class AgentListenerThread(Thread):
    def __init__(self, parentWin, port):
        super(AgentListenerThread, self).__init__()
        self.signalStop = False
        self.threadRunning = False
        
        self.port = port
        self.parentWin = parentWin
        
        self.sock = socket(AF_INET, SOCK_DGRAM)
        self.server_address = ('0.0.0.0', self.port)
        
        # This can throw an exception if it can't bind
        self.sock.bind(self.server_address)
        
    def sendAnnounce(self):
        try:
            self.broadcastSocket.sendto(bytes('sparrowwifiagent', "utf-8"),self.broadcastAddr)
        except:
            pass
        
    def run(self):
        
        if not self.sock:
            self.threadRunning = False
            return
            
        self.threadRunning = True
        
        self.sock.settimeout(6) # receive timeout
        
        while (not self.signalStop):
            try:
                data, address = self.sock.recvfrom(1024)
                self.parentWin.agentAnnounce.emit(address[0], self.port)
            except timeout:
                pass
                    
        self.threadRunning = False
        
        if (self.sock):
            self.sock.close()
        
# ------------------  Agent Listener  ------------------------------
class AgentListenerDialog(QDialog):
    agentAnnounce = QtCore.pyqtSignal(str, int)

    def __init__(self, mainWin = None,  parent = None):
        super(AgentListenerDialog, self).__init__(parent)

        self.parentWin = mainWin
        
        self.broadcastSocket = socket(AF_INET, SOCK_DGRAM)
        self.broadcastSocket.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
        self.broadcastSocket.setsockopt(SOL_SOCKET, SO_BROADCAST, 1)        
        
        self.agentAnnounce.connect(self.onAgentAnnounce)

        # Map Type droplist
        self.lblAgentPort = QLabel("Agent Port", self)
        self.lblAgentPort.setGeometry(10, 10, 100, 30)

        self.spinPort = QSpinBox(self)
        self.spinPort.setRange(1, 65535)
        self.spinPort.setValue(8020)
        self.spinPort.setGeometry(100, 10, 100, 28)
        self.spinPort.valueChanged.connect(self.spinChanged)
        
        # self.broadcastAddr=('255.255.255.255', int(self.spinPort.value()))
        self.agentListenerThread = AgentListenerThread(self,  int(self.spinPort.value()))
        self.agentListenerThread.start()
        
        self.agentTable = QTableWidget(self)
        self.agentTable.setColumnCount(2)
        self.agentTable.setShowGrid(True)
        self.agentTable.setHorizontalHeaderLabels(['IP Address', 'Port'])
        self.agentTable.setGeometry(10, 30, 100, 30)
        self.agentTable.resizeColumnsToContents()
        self.agentTable.setRowCount(0)
        self.agentTable.horizontalHeader().setSectionResizeMode(0, QHeaderView.Stretch)
        self.agentTable.horizontalHeader().sectionClicked.connect(self.onTableHeadingClicked)
        
        # OK and Cancel buttons
        self.buttons = QDialogButtonBox(
            QDialogButtonBox.Ok | QDialogButtonBox.Cancel,
            Qt.Horizontal, self)
        self.buttons.accepted.connect(self.accept)
        self.buttons.rejected.connect(self.reject)
        self.buttons.move(170, 280)

        self.setBlackoutColors()
        
        self.setGeometry(self.geometry().x(), self.geometry().y(), 500,320)
        self.setWindowTitle("Remote Agent Detection")
        self.center()

    def setBlackoutColors(self):
        self.agentTable.setStyleSheet("background-color: black;gridline-color: white;color: white")
        headerStyle = "QHeaderView::section{background-color: white;border: 1px solid black;color: black}"
        self.agentTable.horizontalHeader().setStyleSheet(headerStyle)
        self.agentTable.verticalHeader().setStyleSheet(headerStyle)
        
    def center(self):
        # Get our geometry
        qr = self.frameGeometry()
        # Find the desktop center point
        cp = QDesktopWidget().availableGeometry().center()
        # Move our center point to the desktop center point
        qr.moveCenter(cp)
        # Move the top-left point of the application window to the top-left point of the qr rectangle, 
        # basically centering the window
        self.move(qr.topLeft())
        
    def spinChanged(self):
        self.agentListenerThread.signalStop = True
        
        while (self.agentListenerThread.threadRunning):
            sleep(1)
            
        self.agentListenerThread = None
        
        self.agentListenerThread = AgentListenerThread(self,  int(self.spinPort.value()))
        self.agentListenerThread.start()

    def done(self, result):
        super().done(result)
        
        if self.parentWin:
            self.parentWin.agentListenerClosed.emit()
        
    def closeEvent(self, event):
        self.agentListenerThread.signalStop = True
        
        while (self.agentListenerThread.threadRunning):
            sleep(1)

        if self.parentWin:
            self.parentWin.agentListenerClosed.emit()
            
        event.accept()
            
    def resizeEvent(self, event):
        # self.resized.emit()
        # self.statusBar().showMessage('Window resized.')
        # return super(mainWin, self).resizeEvent(event)
        size = self.geometry()
        self.agentTable.setGeometry(10, 50, size.width()-20, size.height()-100)
        self.buttons.move(size.width()/2-80, size.height() - 40)

    def onTableHeadingClicked(self, logical_index):
        header = self.agentTable.horizontalHeader()
        order = Qt.DescendingOrder
        # order = Qt.DescendingOrder
        if not header.isSortIndicatorShown():
            header.setSortIndicatorShown( True )
        elif header.sortIndicatorSection()==logical_index:
            # apparently, the sort order on the header is already switched
            # when the section was clicked, so there is no need to reverse it
            order = header.sortIndicatorOrder()
        header.setSortIndicator( logical_index, order )
        self.agentTable.sortItems(logical_index, order )
        
    def agentInTable(self, ipAddr, port):
        rowPosition = self.agentTable.rowCount()
        if rowPosition <= 0:
            return False
            
        for curRow in range(0, rowPosition):
            if (self.agentTable.item(curRow, 0).text() == ipAddr) and (self.agentTable.item(curRow, 1).text() == str(port)):
                return True
                
        return False
        
    def onAgentAnnounce(self, ipAddr, port):
        if not self.agentInTable(ipAddr, port):
            rowPosition = self.agentTable.rowCount()
            rowPosition -= 1
            addedFirstRow = False
            if rowPosition < 0:
                addedFirstRow = True
                rowPosition = 0
                
            self.agentTable.insertRow(rowPosition)
            
            # Just make sure we don't get an extra blank row
            if (addedFirstRow):
                self.agentTable.setRowCount(1)

            self.agentTable.setItem(rowPosition, 0, QTableWidgetItem(ipAddr))
            self.agentTable.setItem(rowPosition, 1, IntTableWidgetItem(str(port)))
            self.agentTable.resizeColumnsToContents()
            self.agentTable.horizontalHeader().setSectionResizeMode(0, QHeaderView.Stretch)
        
    def getAgentInfo(self):
        curRow = self.agentTable.currentRow()
        if curRow < 0:
            return '', 0
            
        return self.agentTable.item(curRow, 0).text(), int(self.agentTable.item(curRow, 1).text())
        
    # static method to create the dialog and return (date, time, accepted)
    @staticmethod
    def getAgent(parent = None):
        dialog = AgentListenerDialog(parent)
        result = dialog.exec_()
        # date = dialog.dateTime()
        agentIP, port = dialog.getAgentInfo()
        return (agentIP, port, result == QDialog.Accepted)


# ------------------  GPS Coordinate  ------------------------------
class GPSCoordDialog(QDialog):
    visibility = QtCore.pyqtSignal(bool)
    
    def __init__(self, mainWin, parent = None):
        super(GPSCoordDialog, self).__init__(parent)

        self.visibility.connect(self.onVisibilityChanged)
        
        self.mainWin = mainWin
        

        # Set up GPS check timer
        self.gpsTimer = QTimer()
        self.gpsTimer.timeout.connect(self.onGPSTimer)
        self.gpsTimer.setSingleShot(True)
        self.gpsTimerTimeout = 2000
        self.gpsTimer.start(self.gpsTimerTimeout)
        
        self.lastGPS = None
        self.firstUpdate = True
        
        self.lblMsg = QLabel("Newest coordinates are at the top", self)
        self.lblMsg.move(10, 20)

        self.historyTable = QTableWidget(self)
        self.historyTable.setColumnCount(6)
        self.historyTable.setShowGrid(True)
        self.historyTable.setHorizontalHeaderLabels(['Timestamp','Valid','Latitude', 'Longitude', 'Altitude', 'Speed'])
        self.historyTable.setGeometry(10, 30, 100, 30)
        self.historyTable.resizeColumnsToContents()
        self.historyTable.setRowCount(0)
        self.historyTable.horizontalHeader().setSectionResizeMode(0, QHeaderView.Stretch)
       #  self.historyTable.horizontalHeader().sectionClicked.connect(self.onTableHeadingClicked)
       
        self.ntRightClickMenu = QMenu(self)
        newAct = QAction('Copy', self)        
        newAct.setStatusTip('Copy data to clipboard')
        newAct.triggered.connect(self.onCopy)
        self.ntRightClickMenu.addAction(newAct)
 
        # Attach it to the table
        self.historyTable.setContextMenuPolicy(Qt.CustomContextMenu)
        self.historyTable.customContextMenuRequested.connect(self.showNTContextMenu)
        
        self.setBlackoutColors()
        
        self.setGeometry(self.geometry().x(), self.geometry().y(), 500,320)
        self.setWindowTitle("GPS Coordinate Viewer")
        self.center()

        # initial update:
        if self.mainWin:
            curGPS = self.mainWin.getCurrentGPS()
            self.updateTable(curGPS)

    def setBlackoutColors(self):
        self.historyTable.setStyleSheet("QTableView {background-color: black;gridline-color: white;color: white} QTableCornerButton::section{background-color: white;}")
        headerStyle = "QHeaderView::section{background-color: white;border: 1px solid black;color: black;} QHeaderView::down-arrow,QHeaderView::up-arrow {background: none;}"
        self.historyTable.horizontalHeader().setStyleSheet(headerStyle)
        self.historyTable.verticalHeader().setStyleSheet(headerStyle)
        
    def center(self):
        # Get our geometry
        qr = self.frameGeometry()
        # Find the desktop center point
        cp = QDesktopWidget().availableGeometry().center()
        # Move our center point to the desktop center point
        qr.moveCenter(cp)
        # Move the top-left point of the application window to the top-left point of the qr rectangle, 
        # basically centering the window
        self.move(qr.topLeft())
        
    def closeEvent(self, event):
        self.gpsTimer.stop()
        event.accept()
            
    def resizeEvent(self, event):
        # self.resized.emit()
        # self.statusBar().showMessage('Window resized.')
        # return super(mainWin, self).resizeEvent(event)
        size = self.geometry()
        self.historyTable.setGeometry(10, 50, size.width()-20, size.height()-60)

    def showNTContextMenu(self, pos):
        curRow = self.historyTable.currentRow()
        
        if curRow == -1:
            return
            
        self.ntRightClickMenu.exec_(self.historyTable.mapToGlobal(pos))
 
    def onCopy(self):
        curRow = self.historyTable.currentRow()
        curCol = self.historyTable.currentColumn()
        
        if curRow == -1 or curCol == -1:
            return
        
        curText = self.historyTable.item(curRow, curCol).text()
            
        clipboard = QApplication.clipboard()
        clipboard.setText(curText)
        
    def updateTable(self, curGPS):        
        if curGPS == self.lastGPS:
            # Don't update if nothing's changed and we're not on our first iteration
            return
        
        self.lastGPS = curGPS  # Set for the next pass
        
        rowCount = self.historyTable.rowCount()
        rowCount -= 1
        addedFirstRow = False
        if rowCount < 0:
            addedFirstRow = True
            rowCount = 0
            
        # Insert new at the top
        self.historyTable.insertRow(0)

        # Just make sure we don't get an extra blank row
        if (addedFirstRow):
            self.historyTable.setRowCount(1)

        rowPosition = 0 # Always at the first row
        self.historyTable.setItem(rowPosition, 0, DateTableWidgetItem(datetime.datetime.now().strftime("%m/%d/%Y %H:%M:%S")))
        if (curGPS.isValid):
            self.historyTable.setItem(rowPosition,1, QTableWidgetItem('Yes'))
        else:
            self.historyTable.setItem(rowPosition,1, QTableWidgetItem('No'))
            
        self.historyTable.setItem(rowPosition, 2, FloatTableWidgetItem(str(curGPS.latitude)))
        self.historyTable.setItem(rowPosition, 3, FloatTableWidgetItem(str(curGPS.longitude)))
        self.historyTable.setItem(rowPosition, 4, FloatTableWidgetItem(str(curGPS.altitude)))
        self.historyTable.setItem(rowPosition, 5, FloatTableWidgetItem(str(curGPS.speed)))

        # limit to 20 entries
        if (self.historyTable.rowCount() > 20):
            self.historyTable.setRowCount(20)
        
    def onGPSTimer(self):
        if not self.mainWin:
            # We'll just take one shot coming in here for debug purposes.  Technically we don't need to come in here
            # if there's no main win
            return

        curGPS = self.mainWin.getCurrentGPS()
        
        self.updateTable(curGPS)
            
        self.gpsTimer.start(self.gpsTimerTimeout)
        
    def hideEvent(self, event):
        self.visibility.emit(False)
        
    def showEvent(self, event):
        self.visibility.emit(True)
        
    def onVisibilityChanged(self, visible):
        if not visible:
            self.gpsTimer.stop()
        else:
            if not self.gpsTimer.isActive():
                self.gpsTimer.start(self.gpsTimerTimeout)
            
# ------------------  GPS Coordinate  ------------------------------
class BluetoothDialog(QDialog):
    visibility = QtCore.pyqtSignal(bool)
    
    def __init__(self, mainWin, bluetooth,  useRemoteAgent=False, remoteAgentIP="",  remoteAgentPort=8020, parent = None):
        super().__init__()
        self.mainWin = mainWin
        self.visibility.connect(self.onVisibilityChanged)

        self.usingRemoteAgent = useRemoteAgent
        self.remoteAgentIP = remoteAgentIP
        self.remoteAgentPort = remoteAgentPort

        self.updateWindowTitle()

        self.telemetryWindows = {}
        
        self.updateLock = Lock()
        self.telemetryWindows = {}
        self.bluetooth = bluetooth
        self.hasBlueHydra = True
        self.scanPromiscuous = True
        
        # Set up timer
        self.btTimer = QTimer()
        self.btTimer.timeout.connect(self.onBtTimer)
        self.btTimer.setSingleShot(True)
        self.btTimerTimeout = 500
        
        self.firstUpdate = True

        self.lblInterface = QLabel("Scan Type:", self)
        self.lblInterface.setGeometry(5, 10, 70, 30)
        
        self.comboScanType = QComboBox(self)
        self.comboScanType.move(90, 15)

        self.fillScanTypes()

        # Scan Button
        self.btnScan = QPushButton("&Scan", self)
        self.btnScan.setCheckable(True)
        self.btnScan.setShortcut('Ctrl+S')
        self.btnScan.setStyleSheet("background-color: rgba(0,128,192,255); border: none;")
        self.btnScan.setGeometry(298, 12, 120, 27)
        self.btnScan.clicked[bool].connect(self.onScanClicked)

        # Map Button
        self.btnMap = QPushButton("&Map", self)
        self.btnMap.setStyleSheet("background-color: rgba(0,128,192,255);")
        self.btnMap.clicked.connect(self.onMap)
        
        # Export Button
        self.btnExport = QPushButton("&Export", self)
        self.btnExport.setStyleSheet("background-color: rgba(0,128,192,255);")
        self.btnExport.clicked.connect(self.onExportClicked)
        
        # Data table
        self.bluetoothTable = QTableWidget(self)
        self.bluetoothTable.setColumnCount(11)
        self.bluetoothTable.setShowGrid(True)
        self.bluetoothTable.setHorizontalHeaderLabels(['uuid', 'Address', 'Name', 'Company', 'Manufacturer','Type', 'RSSI','TX Power','Est Range (m)','Last Seen','GPS'])
        self.bluetoothTable.setGeometry(10, 30, 100, 30)
        self.bluetoothTable.resizeColumnsToContents()
        self.bluetoothTable.setRowCount(0)
        self.bluetoothTable.horizontalHeader().setSectionResizeMode(2, QHeaderView.Stretch)
       #  self.historyTable.horizontalHeader().sectionClicked.connect(self.onTableHeadingClicked)
        self.bluetoothTable.horizontalHeader().sectionClicked.connect(self.onTableHeadingClicked)
        self.bluetoothTable.setSelectionMode( QAbstractItemView.SingleSelection )
       
        self.ntRightClickMenu = QMenu(self)
        newAct = QAction('Copy', self)        
        newAct.setStatusTip('Copy data to clipboard')
        newAct.triggered.connect(self.onCopy)
        self.ntRightClickMenu.addAction(newAct)

        self.ntRightClickMenu.addSeparator()
        newAct = QAction('Telemetry', self)        
        newAct.setStatusTip('View network telemetry data')
        newAct.triggered.connect(self.onShowTelemetry)
        self.ntRightClickMenu.addAction(newAct)

        self.btTableSortOrder = Qt.DescendingOrder
        self.btTableSortIndex = -1
 
        # Attach it to the table
        self.bluetoothTable.setContextMenuPolicy(Qt.CustomContextMenu)
        self.bluetoothTable.customContextMenuRequested.connect(self.showNTContextMenu)
        
        self.setBlackoutColors()
        
        # self.setGeometry(self.geometry().x(), self.geometry().y(), 700,500)
        desktopSize = QApplication.desktop().screenGeometry()
        self.mainWidth = desktopSize.width() * 2 / 3
        self.mainHeight = desktopSize.height() / 2
        self.resize(self.mainWidth, self.mainHeight)

        self.center()
        
        if not self.usingRemoteAgent:
            if self.mainWin.hasUbertooth and (not os.path.isfile('/opt/bluetooth/blue_hydra/bin/blue_hydra')):
                QMessageBox.question(self, 'Error',"Blue Hydra not found at /opt/bluetooth/blue_hydra/bin/blue_hydra.  Promiscuous scans will fail.", QMessageBox.Ok)

    def fillScanTypes(self):
        self.comboScanType.clear()
        
        if not self.usingRemoteAgent:
            # Local
            if self.mainWin.hasUbertooth:
                self.comboScanType.addItem('Promiscuous Discovery')
                
            self.comboScanType.addItem('LE Advertisement Discovery')
        else:
            if self.mainWin.hasRemoteUbertooth:
                self.comboScanType.addItem('Promiscuous Discovery')
                
            self.comboScanType.addItem('LE Advertisement Discovery')
            
    def onShowTelemetry(self):
        self.updateLock.acquire()
        
        curRow = self.bluetoothTable.currentRow()
        
        if curRow == -1:
            self.updateLock.release()
            return
        
        curNet = self.bluetoothTable.item(curRow, 0).data(Qt.UserRole)
        
        if curNet == None:
            self.updateLock.release()
            return
       
        if curNet.getKey() not in self.telemetryWindows.keys():
            telemetryWindow = BluetoothTelemetry()
            telemetryWindow.show()
            self.telemetryWindows[curNet.getKey()] = telemetryWindow
        else:
            telemetryWindow = self.telemetryWindows[curNet.getKey()]
        
        # Can also key off of self.telemetryWindow.isVisible()
        telemetryWindow.show()
        telemetryWindow.activateWindow()
        
        # Can do telemetry window updates after release
        self.updateLock.release()
        
        # User could have selected a different network.
        telemetryWindow.updateNetworkData(curNet)            
        
    def setLocal(self):
        self.usingRemoteAgent = False
        
        self.updateWindowTitle()
        self.btnScan.setStyleSheet("background-color: rgba(2,128,192,255); border: none;")
        self.btnScan.setText('&Scan')
        self.comboScanType.setEnabled(True)
        self.fillScanTypes()
        
    def setRemoteAgent(self, agentIP, agentPort):
        self.usingRemoteAgent = True
        self.remoteAgentIP = agentIP
        self.remoteAgentPort = agentPort

        # Check if we're running local.  If so stop it
        if self.bluetooth.discoveryRunning():
            self.bluetooth.stopDiscovery()

        self.fillScanTypes()
        
        self.updateWindowTitle()
        self.checkScanAlreadyRunning()

    def scanRunning(self):
        return 'Stop' in self.btnScan.text()
        
    def checkScanAlreadyRunning(self):
        errcode, errmsg, hasBluetooth, hasUbertooth, spectrumScanRunning, discoveryScanRunning =  getRemoteBluetoothRunningServices(self.remoteAgentIP, self.remoteAgentPort)      
        
        if errcode == 0:
            if discoveryScanRunning:
                self.btnScan.setStyleSheet("background-color: rgba(255,0,0,255); border: none;")
                self.btnScan.setText('&Stop scanning')
                self.comboScanType.setEnabled(False)
            else:
                self.btnScan.setStyleSheet("background-color: rgba(2,128,192,255); border: none;")
                self.btnScan.setText('&Scan')
                self.comboScanType.setEnabled(True)
        else:
                QMessageBox.question(self, 'Error',"Error getting remote agent discovery status: " + errmsg, QMessageBox.Ok)

                self.btnScan.setStyleSheet("background-color: rgba(2,128,192,255); border: none;")
                self.btnScan.setText('&Scan')
                self.comboScanType.setEnabled(True)
                
    def updateWindowTitle(self):
        title = 'Bluetooth'
        
        if self.usingRemoteAgent:
            title += " - " + self.remoteAgentIP + ":" + str(self.remoteAgentPort)
            
        self.setWindowTitle(title)
        
    def onScanClicked(self, pressed):
        if self.btnScan.isChecked():
            # Scanning is on.  Turn red to indicate click would stop
            if self.comboScanType.currentText() == 'Promiscuous Discovery':
                ubertooth = True
                
                if not self.usingRemoteAgent:
                    if not self.mainWin.hasUbertooth:
                        self.btnScan.setChecked(False)
                        return
                else:
                    if not self.mainWin.hasRemoteUbertooth:
                        self.btnScan.setChecked(False)
                        return
            else:
                ubertooth = False
                
            self.btnScan.setStyleSheet("background-color: rgba(255,0,0,255); border: none;")
            self.btnScan.setText('&Stop scanning')
            self.comboScanType.setEnabled(False)
            
            if not self.mainWin.remoteAgentUp:
                self.scanPromiscuous = ubertooth
                self.bluetooth.startDiscovery(ubertooth)
            else:
                self.setCursor(Qt.WaitCursor)
                errcode, errmsg = startRemoteBluetoothDiscoveryScan(self.remoteAgentIP, self.remoteAgentPort, ubertooth)
                self.setCursor(Qt.ArrowCursor)

                if errcode != 0:
                    QMessageBox.question(self, 'Error',"Could not start remote scan: " + errmsg, QMessageBox.Ok)
                    self.btnScan.setChecked(False)
                    self.btnScan.setStyleSheet("background-color: rgba(2,128,192,255); border: none;")
                    self.btnScan.setText('&Scan')
                    self.comboScanType.setEnabled(True)
                    return
                    
            self.btTimer.start(self.btTimerTimeout)
        else:
            self.btTimer.stop()
            
            self.btnScan.setStyleSheet("background-color: rgba(2,128,192,255); border: none;")
            self.btnScan.setText('&Scan')
            self.comboScanType.setEnabled(True)
            self.setCursor(Qt.WaitCursor)
            if not self.mainWin.remoteAgentUp:
                self.bluetooth.stopDiscovery()
            else:
                errcode, errmsg = stopRemoteBluetoothDiscoveryScan(self.remoteAgentIP, self.remoteAgentPort)
            self.setCursor(Qt.ArrowCursor)

    def setBlackoutColors(self):
        self.bluetoothTable.setStyleSheet("background-color: black;gridline-color: white;color: white")
        headerStyle = "QHeaderView::section{background-color: white;border: 1px solid black;color: black}"
        self.bluetoothTable.horizontalHeader().setStyleSheet(headerStyle)
        self.bluetoothTable.verticalHeader().setStyleSheet(headerStyle)
        
    def center(self):
        # Get our geometry
        qr = self.frameGeometry()
        # Find the desktop center point
        cp = QDesktopWidget().availableGeometry().center()
        # Move our center point to the desktop center point
        qr.moveCenter(cp)
        # Move the top-left point of the application window to the top-left point of the qr rectangle, 
        # basically centering the window
        self.move(qr.topLeft())
        
    def closeEvent(self, event):
        self.btTimer.stop()
        
        if not self.usingRemoteAgent:
            self.bluetooth.stopDiscovery()

        for curKey in self.telemetryWindows.keys():
            curWindow = self.telemetryWindows[curKey]
            try:
                curWindow.close()
                self.telemetryWindows[curKey] = None
            except:
                pass
        
        if self.mainWin:
            self.mainWin.bluetoothDiscoveryClosed.emit()
            
        event.accept()
            
    def resizeEvent(self, event):
        # self.resized.emit()
        # self.statusBar().showMessage('Window resized.')
        # return super(mainWin, self).resizeEvent(event)
        size = self.geometry()

        if size.width() < 500:
            self.setGeometry(size.x(), size.y(), 800, size.height())
        
        size = self.geometry()
        
        self.bluetoothTable.setGeometry(10, 50, size.width()-20, size.height()-60)
        
        self.btnExport.setGeometry(size.width()-130, 10, 120, 25)
        self.btnMap.setGeometry(size.width()-280, 10, 120, 25)

    def showNTContextMenu(self, pos):
        curRow = self.bluetoothTable.currentRow()
        
        if curRow == -1:
            return
            
        self.ntRightClickMenu.exec_(self.bluetoothTable.mapToGlobal(pos))
 
    def onCopy(self):
        curRow = self.bluetoothTable.currentRow()
        curCol = self.bluetoothTable.currentColumn()
        
        if curRow == -1 or curCol == -1:
            return
        
        curText = self.bluetoothTable.item(curRow, curCol).text()
            
        clipboard = QApplication.clipboard()
        clipboard.setText(curText)
        
        
    def tableEntryChanged(self, device1, device2):
        return True
        
        if (device1.lastSeen != device2.lastSeen) or (device1.rssi != device2.rssi):
            if (not self.scanPromiscuous) or (device1.name != device2.name):
                # if we're doing an advertisement scan we won't get the name
                return True
            else:
                return False
        else:
            return False
            
    def updateTable(self, deviceList):        
        self.updateLock.acquire()
        
        rowCount = self.bluetoothTable.rowCount()
        rowCount -= 1
        if rowCount < 0:
            rowCount = 0

        # Update existing
        numRows = self.bluetoothTable.rowCount()
        
        if numRows > 0:
            # Loop through each network in the network table, and compare it against the new networks.
            # If we find one, then we already know the network.  Just update it.
            
            # Range goes to last # - 1
            for curRow in range(0, numRows):
                try:
                    curData = self.bluetoothTable.item(curRow, 0).data(Qt.UserRole)
                except:
                    curData = None
                    
                if (curData):
                    # We already have the network.  just update it
                    for curKey in deviceList.keys():
                        curDevice = deviceList[curKey]
                        if curData.getKey() == curDevice.getKey():
                            curDevice.foundInList = True
                            
                            if not self.tableEntryChanged(curData, curDevice):
                                # Nothing has changed, so don't update anything
                                continue
                            
                            curDevice.firstSeen = curData.firstSeen # This is one field to carry forward
                            
                            if self.scanPromiscuous:
                                # Need the other attributes:
                                curDevice.name = curData.name
                                curDevice.manufacturer = curData.manufacturer
                                curDevice.uuid = curData.uuid
                                curDevice.bluetoothDescription = curData.bluetoothDescription

                            if curDevice.txPowerValid and curDevice.iBeaconRange == -1:
                                curDevice.calcRange()
                                
                            # curData is already in the table
                            if curData.strongestRssi > curDevice.rssi or (curData.strongestRssi > (curDevice.rssi*0.9) and curData.gps.isValid and (not curDevice.strongestgps.isValid)):
                                curDevice.strongestRssi = curData.rssi
                                curDevice.strongestgps.latitude = curData.gps.latitude
                                curDevice.strongestgps.longitude = curData.gps.longitude
                                curDevice.strongestgps.altitude = curData.gps.altitude
                                curDevice.strongestgps.speed = curData.gps.speed
                                curDevice.strongestgps.isValid = curData.gps.isValid
                            
                            self.bluetoothTable.item(curRow,2).setText(curDevice.name)
                            self.bluetoothTable.item(curRow, 6).setText(str(curDevice.rssi))
                            
                            if curDevice.txPowerValid:
                                self.bluetoothTable.item(curRow, 7).setText(str(curDevice.txPower))
                            else:
                                self.bluetoothTable.item(curRow, 7).setText('Unknown')
                            
                            if curDevice.iBeaconRange != -1 and curDevice.txPowerValid:
                                self.bluetoothTable.item(curRow, 8).setText(str(curDevice.iBeaconRange))
                            else:
                                self.bluetoothTable.item(curRow, 8).setText('Unknown')
                                
                            self.bluetoothTable.item(curRow, 9).setText(curDevice.lastSeen.strftime("%m/%d/%Y %H:%M:%S"))
                            if (curDevice.gps.isValid):
                                self.bluetoothTable.item(curRow,10).setText('Yes')
                            else:
                                self.bluetoothTable.item(curRow,10).setText('No')
                                
                            self.bluetoothTable.item(curRow, 0).setData(Qt.UserRole, curDevice)

                            # Check if we have a telemetry window
                            if curDevice.getKey() in self.telemetryWindows.keys():
                                telemetryWindow = self.telemetryWindows[curDevice.getKey()]
                                telemetryWindow.updateNetworkData(curDevice)            
                            break

        addedNetworks = 0
        
        for curKey in deviceList.keys():
            curDevice = deviceList[curKey]
            if not curDevice.foundInList:
                addedNetworks += 1
                # Insert new at the top
                self.bluetoothTable.insertRow(0)

                rowPosition = 0 # Always at the first row
                # 'uuid', 'Address', 'name', 'company', 'manufacturer','type', 'RSSI','iBeacon Range','Last Seen','GPS'
                newDevice = QTableWidgetItem(curDevice.uuid)
                newDevice.setData(Qt.UserRole, curDevice)
                self.bluetoothTable.setItem(rowPosition, 0, newDevice)
                
                self.bluetoothTable.setItem(rowPosition,1, QTableWidgetItem(curDevice.macAddress))
                self.bluetoothTable.setItem(rowPosition,2, QTableWidgetItem(curDevice.name))
                self.bluetoothTable.setItem(rowPosition,3, QTableWidgetItem(curDevice.company))
                self.bluetoothTable.setItem(rowPosition,4, QTableWidgetItem(curDevice.manufacturer))

                if curDevice.btType == BluetoothDevice.BT_LE:
                    self.bluetoothTable.setItem(rowPosition,5, QTableWidgetItem('BTLE'))
                else:
                    self.bluetoothTable.setItem(rowPosition,5, QTableWidgetItem('Classic'))

                self.bluetoothTable.setItem(rowPosition, 6, IntTableWidgetItem(str(curDevice.rssi)))
                
                if curDevice.txPowerValid:
                    self.bluetoothTable.setItem(rowPosition, 7, IntTableWidgetItem(str(curDevice.txPower)))
                else:
                    self.bluetoothTable.setItem(rowPosition, 7, IntTableWidgetItem('Unknown'))
                
                if curDevice.iBeaconRange != -1 and curDevice.txPowerValid:
                    self.bluetoothTable.setItem(rowPosition, 8, FloatTableWidgetItem(str(curDevice.iBeaconRange)))
                else:
                    self.bluetoothTable.setItem(rowPosition, 8, FloatTableWidgetItem('Unknown'))
                    
                self.bluetoothTable.setItem(rowPosition, 9, DateTableWidgetItem(curDevice.lastSeen.strftime("%m/%d/%Y %H:%M:%S")))
                if (curDevice.gps.isValid):
                    self.bluetoothTable.setItem(rowPosition,10, QTableWidgetItem('Yes'))
                else:
                    self.bluetoothTable.setItem(rowPosition,10, QTableWidgetItem('No'))
                
        if addedNetworks > 0:
            if self.btTableSortIndex >=0:
                self.bluetoothTable.sortItems(self.btTableSortIndex, self.btTableSortOrder )

        self.updateLock.release()
        
    def onMap(self):
        rowPosition = self.bluetoothTable.rowCount()

        if rowPosition <= 0:
            QMessageBox.question(self, 'Error',"There's no devices in the table.  Please run a scan first.", QMessageBox.Ok)
            return
            
        mapSettings, ok = MapSettingsDialog.getSettings()

        if not ok:
            return
            
        if len(mapSettings.outputfile) == 0:
            QMessageBox.question(self, 'Error',"Please provide an output file.", QMessageBox.Ok)
            return
            
        markerDict = {}
        markers = []
        
        # Range goes to last # - 1
        for curRow in range(0, rowPosition):
            try:
                curData = self.bluetoothTable.item(curRow, 0).data(Qt.UserRole)
            except:
                curData = None
            
            if (curData):
                newMarker = MapMarker()
                
                if len(curData.name) > 0:
                    newMarker.label = curData.name
                else:
                    newMarker.label = curData.macAddress
                    
                newMarker.label = newMarker.label[:mapSettings.maxLabelLength]
                
                if mapSettings.plotstrongest:
                    if curData.strongestgps.isValid:
                        newMarker.gpsValid = True
                        newMarker.latitude = curData.strongestgps.latitude
                        newMarker.longitude = curData.strongestgps.longitude
                    else:
                        newMarker.gpsValid = False
                        newMarker.latitude = 0.0
                        newMarker.longitude = 0.0
                        
                    newMarker.barCount = WirelessEngine.getSignalQualityFromDB0To5(curData.strongestRssi)
                else:
                    if curData.gps.isValid:
                        newMarker.gpsValid = True
                        newMarker.latitude = curData.gps.latitude
                        newMarker.longitude = curData.gps.longitude
                    else:
                        newMarker.gpsValid = False
                        newMarker.latitude = 0.0
                        newMarker.longitude = 0.0
                        
                    newMarker.barCount = WirelessEngine.getSignalQualityFromDB0To5(curData.rssi)
                
                markerKey = newMarker.getKey()
                if markerKey in markerDict:
                    curMarker = markerDict[markerKey]
                    curMarker.addLabel(newMarker.label)
                    if curMarker.barCount > newMarker.barCount:
                        curMarker.barCount = newMarker.barCount
                else:
                    # Move label to list
                    newMarker.addLabel(newMarker.label)
                    newMarker.label = ''
                    markerDict[markerKey] = newMarker

        # Now send consolidated list
        for curKey in markerDict.keys():
            markers.append(markerDict[curKey])
        
        if len(markers) > 0:
            retVal = MapEngine.createMap(mapSettings.outputfile,mapSettings.title,markers, connectMarkers=False, openWhenDone=True, mapType=mapSettings.mapType)
            
            if not retVal:
                QMessageBox.question(self, 'Error',"Unable to generate map to " + mapSettings.outputfile, QMessageBox.Ok)
        
    def onExportClicked(self):
        fileName = saveFileDialog()

        if not fileName:
            return
            
        try:
            outputFile = open(fileName, 'w')
        except:
            QMessageBox.question(self, 'Error',"Unable to write to " + fileName, QMessageBox.Ok)
            return
            
        outputFile.write('uuid,Address,Name,Company,Manufacturer,Type,RSSI,TX Power,Strongest RSSI,Est Range (m),Last Seen,GPS Valid,Latitude,Longitude,Altitude,Speed,Strongest GPS Valid,Strongest Latitude,Strongest Longitude,Strongest Altitude,Strongest Speed\n')

        self.updateLock.acquire()

        numItems = self.bluetoothTable.rowCount()
        
        if numItems == 0:
            outputFile.close()
            self.updateLock.release()
            return
           
        for i in range(0, numItems):
            curData = self.bluetoothTable.item(i, 0).data(Qt.UserRole)

            btType = ""
            if curData.btType == BluetoothDevice.BT_LE:
                btType = "BTLE"
            else:
                btType = "Classic"
                
            if curData.txPowerValid:
                txPower = str(curData.txPower)
            else:
                txPower = 'Unknown'
                
            outputFile.write(curData.uuid  + ',' + curData.macAddress + ',"' + curData.name + '","' + curData.company + '","' + curData.manufacturer)
            outputFile.write('","' + btType + '",' + str(curData.rssi) + ',' + str(curData.strongestRssi) + ',' + txPower + ',' + str(curData.iBeaconRange) + ',' +
                                    curData.lastSeen.strftime("%m/%d/%Y %H:%M:%S") + ',' + 
                                    str(curData.gps.isValid) + ',' + str(curData.gps.latitude) + ',' + str(curData.gps.longitude) + ',' + str(curData.gps.altitude) + ',' + str(curData.gps.speed) + ',' + 
                                    str(curData.strongestgps.isValid) + ',' + str(curData.strongestgps.latitude) + ',' + str(curData.strongestgps.longitude) + ',' + str(curData.strongestgps.altitude) + ',' + str(curData.strongestgps.speed) + '\n')
            
        outputFile.close()
        
        self.updateLock.release()
                        
    def onBtTimer(self):
        if not self.mainWin:
            # We'll just take one shot coming in here for debug purposes.  Technically we don't need to come in here
            # if there's no main win
            return

        curGPS = self.mainWin.getCurrentGPS()
        
        if self.usingRemoteAgent:
            errcode, errmsg, devices = getRemoteBluetoothDiscoveryStatus(self.remoteAgentIP, self.remoteAgentPort)
        else:
            errcode = 0
            self.bluetooth.updateDeviceList()
            devices= self.bluetooth.devices
        
        if (errcode == 0) and (devices is not None) and (len(devices) > 0):
            now = datetime.datetime.now()
            if not self.usingRemoteAgent:
                self.bluetooth.deviceLock.acquire()
                
            for curKey in devices.keys():
                curDevice = devices[curKey]
                curDevice.manufacturer = self.mainWin.ouiLookup(curDevice.macAddress)
                if curDevice.manufacturer is None:
                    curDevice.manufacturer = ''
                
                if not self.usingRemoteAgent:
                    # Remote agent takes care of this before sending it.
                    elapsedTime =  now - curDevice.lastSeen
                    
                    # This is a little bit of a hack for the BlueHydra side since it can take a while to see devices or have
                    # them show up in the db.  For LE discovery scans this will always be pretty quick.
                    if elapsedTime.total_seconds() < 120:
                        curDevice.gps.copy(curGPS)
                        if curDevice.rssi >= curDevice.strongestRssi:
                            curDevice.strongestRssi = curDevice.rssi
                            curDevice.strongestgps.copy(curGPS)
                
            self.updateTable(devices)
            
            if not self.usingRemoteAgent:
                self.bluetooth.deviceLock.release()
            
        self.btTimer.start(self.btTimerTimeout)
        
    def hideEvent(self, event):
        self.visibility.emit(False)
        
    def showEvent(self, event):
        self.visibility.emit(True)
        
    def onVisibilityChanged(self, visible):
        if not visible:
            self.btTimer.stop()
        else:
            if self.btnScan.isChecked():
                self.btTimer.start(self.btTimerTimeout)

    def onTableHeadingClicked(self, logical_index):
        header = self.bluetoothTable.horizontalHeader()
        order = Qt.DescendingOrder
        # order = Qt.DescendingOrder
        if not header.isSortIndicatorShown():
            header.setSortIndicatorShown( True )
        elif header.sortIndicatorSection()==logical_index:
            # apparently, the sort order on the header is already switched
            # when the section was clicked, so there is no need to reverse it
            order = header.sortIndicatorOrder()
        header.setSortIndicator( logical_index, order )

        self.btTableSortOrder = order
        self.btTableSortIndex = logical_index
        self.bluetoothTable.sortItems(logical_index, order )
        
            
# ------------------  Agent Configuration  ------------------------------
class AgentConfigDialog(QDialog):
    def __init__(self, startupCfg, runningCfg, interfaces, agentIP='127.0.0.1', agentPort=8020,parent = None):
        super(AgentConfigDialog,  self).__init__(parent)

        self.agentIP = agentIP
        self.agentPort = agentPort
        self.interfaces = interfaces
        
        agentString = agentIP + ":" + str(agentPort)
        
        self.startupCfg = startupCfg
        self.runningCfg = runningCfg
        
        self.lblMsg = QLabel("Startup", self)
        self.lblMsg.move(120, 20)
        self.lblMsg = QLabel("Running", self)
        self.lblMsg.move(250, 20)


        # Cancel Startup Controls
        self.lblMsg = QLabel("Cancel Startup:", self)
        self.lblMsg.move(10, 50)

        self.comboCancelStartupCfgFile = QComboBox(self)
        self.comboCancelStartupCfgFile.move(118, 45)
        self.comboCancelStartupCfgFile.addItem("Yes")
        self.comboCancelStartupCfgFile.addItem("No")
        
        if startupCfg.cancelStart:
            self.comboCancelStartupCfgFile.setCurrentIndex(0)
        else:
            self.comboCancelStartupCfgFile.setCurrentIndex(1)
        
        # Port controls
        self.lblPort = QLabel("Port: ", self)
        self.lblPort.move(10, 90)
        self.spinPortStartup = QSpinBox(self)
        self.spinPortStartup.move(118, 85)
        self.spinPortStartup.setRange(1, 65535)
        self.spinPortStartup.setValue(startupCfg.port)
        
        self.spinPortRunning = QSpinBox(self)
        self.spinPortRunning.move(250, 85)
        self.spinPortRunning.setRange(1, 65535)
        self.spinPortRunning.setValue(runningCfg.port)
        self.spinPortRunning.setEnabled(False)
        
        # Announce controls
        self.lblMsg = QLabel("Announce Agent:", self)
        self.lblMsg.move(10, 130)

        self.comboSendAnnouncementsStartup = QComboBox(self)
        self.comboSendAnnouncementsStartup.move(118, 125)
        self.comboSendAnnouncementsStartup.addItem("Yes")
        self.comboSendAnnouncementsStartup.addItem("No")
        
        if startupCfg.announce:
            self.comboSendAnnouncementsStartup.setCurrentIndex(0)
        else:
            self.comboSendAnnouncementsStartup.setCurrentIndex(1)
        
        self.comboSendAnnouncementsRunning = QComboBox(self)
        self.comboSendAnnouncementsRunning.move(250, 125)
        self.comboSendAnnouncementsRunning.addItem("Yes")
        self.comboSendAnnouncementsRunning.addItem("No")
        
        if runningCfg.announce:
            self.comboSendAnnouncementsRunning.setCurrentIndex(0)
        else:
            self.comboSendAnnouncementsRunning.setCurrentIndex(1)
        
        # RPi LEDs
        self.lblMsg = QLabel("Use RPi LEDs:", self)
        self.lblMsg.move(10, 170)

        self.comboRPiLEDsStartup = QComboBox(self)
        self.comboRPiLEDsStartup.move(118, 165)
        self.comboRPiLEDsStartup.addItem("Yes")
        self.comboRPiLEDsStartup.addItem("No")
        
        if startupCfg.useRPiLEDs:
            self.comboRPiLEDsStartup.setCurrentIndex(0)
        else:
            self.comboRPiLEDsStartup.setCurrentIndex(1)
        
        self.comboRPiLEDsRunning = QComboBox(self)
        self.comboRPiLEDsRunning.move(250, 165)
        self.comboRPiLEDsRunning.addItem("Yes")
        self.comboRPiLEDsRunning.addItem("No")
        
        if runningCfg.useRPiLEDs:
            self.comboRPiLEDsRunning.setCurrentIndex(0)
        else:
            self.comboRPiLEDsRunning.setCurrentIndex(1)
        
        # Record on Startup
        self.lblMsg = QLabel("Record Local:", self)
        self.lblMsg.move(10, 210)

        # self.comboRecordStartup = QComboBox(self)
        # self.comboRecordStartup.move(118, 205)
        # self.comboRecordStartup.addItem("Yes")
        # self.comboRecordStartup.addItem("No")
        
        self.btnRecordStartStop = QPushButton("Start", self)
        self.btnRecordStartStop.move(250, 205)
        self.btnRecordStartStop.clicked.connect(self.onStartStopRecord)
        
        # Record Interface
        self.lblMsg = QLabel("Record Interface:", self)
        self.lblMsg.move(10, 250)

        self.recordInterfaceStartup = QLineEdit(self)
        self.recordInterfaceStartup.setGeometry(118, 245, 100, 25)
        self.recordInterfaceStartup.setText(startupCfg.recordInterface)
        
        self.recordInterfaceRunning = QLineEdit(self)
        self.recordInterfaceRunning.setGeometry(250, 245, 100, 25)
        self.recordInterfaceRunning.setText(runningCfg.recordInterface)
        
        self.btnShowInterfaces = QPushButton("Interfaces", self)
        self.btnShowInterfaces.move(360, 245)
        self.btnShowInterfaces.clicked.connect(self.onShowInterfaces)
        
        if runningCfg.recordRunning:
            self.recordInterfaceRunning.setEnabled(False)
            self.btnRecordStartStop.setText('Stop')
        
        # Mavlink GPS
        self.lblMsg = QLabel("Mavlink GPS:", self)
        self.lblMsg.move(10, 290)

        self.mavlinkGPSStartup = QLineEdit(self)
        self.mavlinkGPSStartup.setGeometry(118, 285, 100, 25)
        self.mavlinkGPSStartup.setText(startupCfg.mavlinkGPS)
        
        # IP Allow List
        self.lblMsg = QLabel("IP Allow List:", self)
        self.lblMsg.move(10, 330)

        self.ipAllowStartup = QLineEdit(self)
        self.ipAllowStartup.setGeometry(118, 325, 100, 25)
        self.ipAllowStartup.setText(startupCfg.ipAllowedList)
        
        self.ipAllowRunning = QLineEdit(self)
        self.ipAllowRunning.setGeometry(250, 325, 100, 25)
        self.ipAllowRunning.setText(runningCfg.ipAllowedList)
        
        # OK and Cancel buttons
        buttons = QDialogButtonBox(
            QDialogButtonBox.Ok | QDialogButtonBox.Cancel,
            Qt.Horizontal, self)
        buttons.accepted.connect(self.accept)
        buttons.rejected.connect(self.reject)
        buttons.move(145, 380)

        self.btnReboot = QPushButton("Save and Restart", self)
        self.btnReboot.setGeometry(145, 430, 170, 30)
        self.btnReboot.clicked.connect(self.onRestart)

        # Window geometry
        self.setGeometry(self.geometry().x(), self.geometry().y(), 450,480)
        self.setWindowTitle("Agent Configuration:" + agentString)
        self.center()

    def comboTrueFalse(self, combo):
        if combo.currentIndex() == 0:
            return True
        else:
            return False
    
    def validateAllowedIPs(self, allowedIPstr):
        if len(allowedIPstr) > 0:
            ippattern = re.compile('([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})')
            if ',' in allowedIPstr:
                tmpList = allowedIPstr.split(',')
                for curItem in tmpList:
                    ipStr = curItem.replace(' ', '')
                    try:
                        ipValue = ippattern.search(ipStr).group(1)
                    except:
                        QMessageBox.question(self, 'Error','ERROR: Unknown IP pattern: ' + ipStr, QMessageBox.Ok)
                        return False
            else:
                ipStr = allowedIPstr.replace(' ', '')
                try:
                    ipValue = ippattern.search(ipStr).group(1)
                except:
                    QMessageBox.question(self, 'Error','ERROR: Unknown IP pattern: ' + ipStr, QMessageBox.Ok)
                    return False
                    
        return True
    
    def validateMavlink(self, mavlinkstr):
        if mavlinkstr == '3dr' or mavlinkstr == 'sitl':
            return True
            
        # for the moment we'll assume the user knows how to create a custom mavlink connection string.  I know.....
        return True
        
    def validateAndSend(self, sendRestart=False):
        settingsChanged = False
        
        if self.btnRecordStartStop.text() == 'Start':
            # Just make sure we clear the field or it may start recording on us.
            self.recordInterfaceRunning.setText('')

        tmpBool = self.comboTrueFalse(self.comboCancelStartupCfgFile)
        
        if self.startupCfg.cancelStart != tmpBool:
            self.startupCfg.cancelStart = self.comboTrueFalse(self.comboCancelStartupCfgFile)
            settingsChanged = True
        
        tmpBool = self.comboTrueFalse(self.comboSendAnnouncementsStartup)
        
        if self.startupCfg.announce != tmpBool:
            self.startupCfg.announce = self.comboTrueFalse(self.comboSendAnnouncementsStartup)
            settingsChanged = True
            
        tmpBool = self.comboTrueFalse(self.comboSendAnnouncementsRunning)
        if self.runningCfg.announce != tmpBool:
            self.runningCfg.announce = self.comboTrueFalse(self.comboSendAnnouncementsRunning)
            settingsChanged = True
        
        if self.startupCfg.port != int(self.spinPortStartup.value()):
            self.startupCfg.port = int(self.spinPortStartup.value())
            settingsChanged = True
            
        self.runningCfg.port = self.agentPort # Can't change this
        
        tmpBool = self.comboTrueFalse(self.comboRPiLEDsStartup)
        if self.startupCfg.useRPiLEDs != tmpBool:
            self.startupCfg.useRPiLEDs = self.comboTrueFalse(self.comboRPiLEDsStartup)
            settingsChanged = True
            
        tmpBool = self.comboTrueFalse(self.comboRPiLEDsRunning)
        if self.runningCfg.useRPiLEDs != tmpBool:
            self.runningCfg.useRPiLEDs = self.comboTrueFalse(self.comboRPiLEDsRunning)
            settingsChanged = True

        if self.startupCfg.recordInterface != self.recordInterfaceStartup.text():
            settingsChanged = True
            
            if recordOnStartup:
                self.startupCfg.recordInterface = self.recordInterfaceStartup.text()
            else:
                self.startupCfg.recordInterface = ""
            
        if self.runningCfg.recordInterface != self.recordInterfaceRunning.text():
            self.runningCfg.recordInterface = self.recordInterfaceRunning.text().replace(' ', '')
            settingsChanged = True
        
        mavlinkstr = self.mavlinkGPSStartup.text().replace(' ', '')
        
        if not self.validateMavlink(mavlinkstr):
            return False
        
        if self.startupCfg.mavlinkGPS != mavlinkstr:
            self.startupCfg = mavlinkstr
            settingsChanged = True
            
        iptext = self.ipAllowStartup.text().replace(' ', '')
        if not self.validateAllowedIPs(iptext):
            return False
            
        if self.startupCfg.ipAllowedList != iptext:
            self.startupCfg.ipAllowedList = iptext
            settingsChanged = True
        
        iptext = self.ipAllowRunning.text().replace(' ', '')
        if not self.validateAllowedIPs(iptext):
            return False
            
        if self.runningCfg.ipAllowedList != iptext:
            self.runningCfg.ipAllowedList = iptext
            settingsChanged = True
        
        # Transmit updates here and notify the user if anything went wrong
        if settingsChanged or sendRestart:
            retVal, errmsg = updateRemoteConfig(self.agentIP, self.agentPort, self.startupCfg, self.runningCfg, sendRestart)

            if retVal != 0:
                QMessageBox.question(self, 'Error',errmsg, QMessageBox.Ok)
                return False
                
        return True
        
    def onRestart(self):
        retVal = self.validateAndSend(True)
        if not retVal:
            return
        
        # Behave like OK but send restart flag
        super().done(QDialog.Accepted)
        
    def done(self, result):
        if result == QDialog.Accepted:
            retVal = validateAndSend(False)
            if not retVal:
                return
            
        super().done(result)
    
    def onShowInterfaces(self):
        validlist = ""
        for curInt in self.interfaces:
            if len(validlist) > 0:
                validlist += ', ' + curInt
            else:
                validlist = curInt

        if len(validlist) > 0:
            QMessageBox.question(self, 'Error',"Interfaces reported by the remote agent are:\n\n" + validlist, QMessageBox.Ok)
        else:
            QMessageBox.question(self, 'Error',"No wireless interfaces found.", QMessageBox.Ok)
        
    def onStartStopRecord(self):
        if self.btnRecordStartStop.text() == 'Stop':
            # Transition to start
            retVal, errmsg = stopRecord(self.agentIP, self.agentPort)
            
            if retVal != 0:
                QMessageBox.question(self, 'Error',errmsg, QMessageBox.Ok)
                return
                
            self.btnRecordStartStop.setText('Start')
            self.recordInterfaceRunning.setEnabled(True)
            self.recordInterfaceRunning.setText('')
        else:
            if len(self.recordInterfaceRunning.text()) == 0:
                QMessageBox.question(self, 'Error',"Please provide a valid wireless interface name.", QMessageBox.Ok)
                return

            interface = self.recordInterfaceRunning.text().replace(' ', '')
            
            if interface not in self.interfaces:
                validlist = ""
                for curInt in self.interfaces:
                    if len(validlist) > 0:
                        validlist += ', ' + curInt
                    else:
                        validlist = curInt

                if len(validlist) > 0:
                    QMessageBox.question(self, 'Error',"The requested interface does not appear to be valid.  Interfaces seen on remote agent are:\n\n" + validlist, QMessageBox.Ok)
                else:
                    QMessageBox.question(self, 'Error',"No wireless interfaces found.", QMessageBox.Ok)
                    
                return
                
            # transition to stop
            retVal, errmsg = startRecord(self.agentIP, self.agentPort, interface)
            
            if retVal != 0:
                QMessageBox.question(self, 'Error',errmsg, QMessageBox.Ok)
                return
                
            self.btnRecordStartStop.setText('Stop')
            self.recordInterfaceRunning.setEnabled(False)
        
    def center(self):
        # Get our geometry
        qr = self.frameGeometry()
        # Find the desktop center point
        cp = QDesktopWidget().availableGeometry().center()
        # Move our center point to the desktop center point
        qr.moveCenter(cp)
        # Move the top-left point of the application window to the top-left point of the qr rectangle, 
        # basically centering the window
        self.move(qr.topLeft())
        
    def closeEvent(self, event):
        event.accept()
            
    def resizeEvent(self, event):
        # self.resized.emit()
        # self.statusBar().showMessage('Window resized.')
        # return super(mainWin, self).resizeEvent(event)
        size = self.geometry()


# ------------------  GPS Coordinate  ------------------------------
class RemoteFilesDialog(QDialog):
    visibility = QtCore.pyqtSignal(bool)
    
    def __init__(self, mainWin, agentIP, agentPort, parent = None):
        super(RemoteFilesDialog, self).__init__(parent)

        self.visibility.connect(self.onVisibilityChanged)
        
        self.mainWin = mainWin
        
        self.remoteAgentIP = agentIP
        self.remoteAgentPort = agentPort

        self.lblMsg = QLabel("Remote Files", self)
        self.lblMsg.move(10, 20)

        self.btnRefresh = QPushButton("&Refresh", self)
        self.btnRefresh.setShortcut('Ctrl+R')
        self.btnRefresh.clicked.connect(self.onRefreshFiles)
        # self.btnRefresh.setStyleSheet("background-color: rgba(0,128,192,255); border: none;")
        # self.btnRefresh.move(90, 30)

        self.btnCopy = QPushButton("&Copy", self)
        self.btnCopy.clicked.connect(self.onCopyFiles)
        
        self.btnDelete = QPushButton("&Delete", self)
        self.btnDelete.clicked.connect(self.onDeleteFiles)
        
        self.fileTable = QTableWidget(self)
        self.fileTable.setColumnCount(3)
        self.fileTable.setShowGrid(True)
        self.fileTable.setHorizontalHeaderLabels(['Filename','Size','Last Modified'])
        #self.fileTable.setGeometry(10, 30, 100, 30)
        self.fileTable.resizeColumnsToContents()
        self.fileTable.setRowCount(0)
        self.fileTable.horizontalHeader().setSectionResizeMode(0, QHeaderView.Stretch)
        self.fileTable.horizontalHeader().sectionClicked.connect(self.onTableHeadingClicked)
       
        self.fileTableSortOrder = Qt.DescendingOrder
        self.fileTableSortIndex = -1
        
        self.ntRightClickMenu = QMenu(self)
        newAct = QAction('Copy', self)        
        newAct.setStatusTip('Copy data to clipboard')
        newAct.triggered.connect(self.onCopy)
        self.ntRightClickMenu.addAction(newAct)
 
        # Attach it to the table
        self.fileTable.setContextMenuPolicy(Qt.CustomContextMenu)
        self.fileTable.customContextMenuRequested.connect(self.showNTContextMenu)
        
        self.setBlackoutColors()
        
        self.setGeometry(self.geometry().x(), self.geometry().y(), 650,400)
        self.setWindowTitle("Remote Files: " + self.remoteAgentIP + ':' + str(self.remoteAgentPort))
        self.center()
        
        self.onRefreshFiles()

    def resizeEvent(self, event):
        # self.resized.emit()
        # self.statusBar().showMessage('Window resized.')
        # return super(mainWin, self).resizeEvent(event)
        size = self.geometry()
        self.fileTable.setGeometry(10, 50, size.width()-120, size.height()-60)
        
        self.btnRefresh.setGeometry(size.width()-170, 10, 60, 30)

        self.btnCopy.setGeometry(size.width()-90, 80, 80, 30)
        self.btnDelete.setGeometry(size.width()-90, 130, 80, 30)

    def setBlackoutColors(self):
        self.fileTable.setStyleSheet("background-color: black;gridline-color: white;color: white")
        headerStyle = "QHeaderView::section{background-color: white;border: 1px solid black;color: black}"
        self.fileTable.horizontalHeader().setStyleSheet(headerStyle)
        self.fileTable.verticalHeader().setStyleSheet(headerStyle)
        
    def center(self):
        # Get our geometry
        qr = self.frameGeometry()
        # Find the desktop center point
        cp = QDesktopWidget().availableGeometry().center()
        # Move our center point to the desktop center point
        qr.moveCenter(cp)
        # Move the top-left point of the application window to the top-left point of the qr rectangle, 
        # basically centering the window
        self.move(qr.topLeft())
        
    def closeEvent(self, event):
        event.accept()
            
    def onTableHeadingClicked(self, logical_index):
        header = self.fileTable.horizontalHeader()
        order = Qt.DescendingOrder
        # order = Qt.DescendingOrder
        if not header.isSortIndicatorShown():
            header.setSortIndicatorShown( True )
        elif header.sortIndicatorSection()==logical_index:
            # apparently, the sort order on the header is already switched
            # when the section was clicked, so there is no need to reverse it
            order = header.sortIndicatorOrder()
        header.setSortIndicator( logical_index, order )

        self.fileTableSortOrder = order
        self.fileTableSortIndex = logical_index
        self.fileTable.sortItems(logical_index, order )
        
    def showNTContextMenu(self, pos):
        curRow = self.fileTable.currentRow()
        
        if curRow == -1:
            return
            
        self.ntRightClickMenu.exec_(self.fileTable.mapToGlobal(pos))
 
    def onCopy(self):
        curRow = self.fileTable.currentRow()
        curCol = self.fileTable.currentColumn()
        
        if curRow == -1 or curCol == -1:
            return
        
        curText = self.fileTable.item(curRow, curCol).text()
            
        clipboard = QApplication.clipboard()
        clipboard.setText(curText)

    def onRefreshFiles(self):
        retVal, errmsg, filelist = getRemoteRecordingsFiles(self.remoteAgentIP, self.remoteAgentPort)
        
        if retVal != 0:
            QMessageBox.question(self, 'Error',"Could not list remote files: " + errmsg, QMessageBox.Ok)
            return
            
        self.populateTable(filelist)
            
    
    def getSelectedFilenames(self):
        retVal = []
        
        selectedItems = self.fileTable.selectedIndexes()
        
        for curIndex in selectedItems:
            curRow = curIndex.row()
            curFilename = self.fileTable.item(curRow, 0).text()
            
            retVal.append(curFilename)
            
        return retVal
        
    def getRemoteFile(self, agentIP, agentPort, filename):
        url = "http://" + agentIP + ":" + str(agentPort) + "/system/getrecording/" + filename

        dirname, runfilename = os.path.split(os.path.abspath(__file__))
        recordingsDir = dirname + '/recordings'
        fullPath = recordingsDir + '/' + filename
        
        if os.path.isfile(fullPath):
            reply = QMessageBox.question(self, 'Question',"Local file by that name already exists.  Overwrite?", QMessageBox.Yes | QMessageBox.No, QMessageBox.No)

            if reply == QMessageBox.No:
                return
        
        try:
            # urllib.urlretrieve(url, fullPath)
            urlretrieve(url, fullPath)
            return 0, ""
        except:
            return 1, "Error downloading and saving file."
            
    def onCopyFiles(self):
        filenames = self.getSelectedFilenames()
        
        if len(filenames) == 0:
            return
        
        for curFile in filenames:
            retVal, errmsg = self.getRemoteFile(self.remoteAgentIP, self.remoteAgentPort, curFile)
            
            if retVal != 0:
                QMessageBox.question(self, 'Error',errmsg, QMessageBox.Ok)

        self.onRefreshFiles()
        
    def onDeleteFiles(self):
        filenames = self.getSelectedFilenames()
        
        if len(filenames) == 0:
            return

        retVal, errmsg = delRemoteRecordingFiles(self.remoteAgentIP, self.remoteAgentPort, filenames)
        
        if retVal != 0:
            QMessageBox.question(self, 'Error',errmsg, QMessageBox.Ok)
            
        self.onRefreshFiles()
        
    def populateTable(self, filelist):
        self.fileTable.setRowCount(0)
        
        for curFile in filelist:
            rowCount = self.fileTable.rowCount()
            rowCount -= 1
            addedFirstRow = False
            if rowCount < 0:
                addedFirstRow = True
                rowCount = 0
                
            # Insert new at the top
            self.fileTable.insertRow(0)

            # Just make sure we don't get an extra blank row
            if (addedFirstRow):
                self.fileTable.setRowCount(1)

            rowPosition = 0 # Always at the first row
            self.fileTable.setItem(rowPosition,0, QTableWidgetItem(curFile.filename))
            self.fileTable.setItem(rowPosition,1, IntTableWidgetItem(str(curFile.size)))
            self.fileTable.setItem(rowPosition, 2, DateTableWidgetItem(curFile.timestamp.strftime("%m/%d/%Y %H:%M:%S")))
            
        if self.fileTableSortIndex >=0:
            self.fileTable.sortItems(self.fileTableSortIndex, self.fileTableSortOrder )
            
    def hideEvent(self, event):
        self.visibility.emit(False)
        
    def showEvent(self, event):
        self.visibility.emit(True)
        
    def onVisibilityChanged(self, visible):
        if not visible:
            pass
        else:
            pass
            
# -------  Main Routine For Debugging-------------------------

if __name__ == '__main__':
    app = QApplication([])
    #dbSettings, ok = DBSettingsDialog.getSettings()
    #mapSettings, ok = MapSettingsDialog.getSettings()
    # mapSettings, ok = TelemetryMapSettingsDialog.getSettings()
    # agentIP, port, accepted = AgentListenerDialog.getAgent()
    # testWin = GPSCoordDialog(mainWin=None)
    
    #from sparrowwifiagent import AgentConfigSettings
    #startupCfg = AgentConfigSettings()
    #runningCfg = AgentConfigSettings()
    #testWin = AgentConfigDialog(startupCfg, runningCfg, ['test'])
    
    testWin = RemoteFilesDialog(None,'127.0.0.1', 8020)
    testWin.exec()
    
    app.exec_()