#!/usr/bin/env python
'''
is there a bug in Foc Motor Direction? can it be auto ???

bug in touch:
* combobox
  sometimes it doesn't open but just blinks
  sometimes the dropdown stays
  
changing Baudrate ????  doesn't seem to work, no effect od setBAudrate ????
  
https://wiki.python.org/moin/PyQt/QML%20callback%20function  
'''

QMLVersion = 6 #004

ApplicationStr = "STorM32Gui"
AppName = "STorM32 App"
AppVersion = "v2.xx"
AppDate = "01. Jun. 2018"

TopLineAppNameStr = AppName + "  " + AppVersion  #"STorM32 App  v2.xx"
TopLineAppVersionStr = AppDate + "  www.olliw.eu"  #"18. Sep. 2017  www.olliw.eu"
IniFileStr = "./" + ApplicationStr + ".ini"   #"./STorM32Gui.ini"

EspWifiUrl = '192.168.4.1'
EspWifiPort = 80
EspWifiName = 'STorM32 ESP'


forceCreateMyNavBarQml = False
#forceCreateMyNavBarQml = True


import sys
import os
import re
import time

from PyQt5.QtCore import pyqtSignal, pyqtSlot
from PyQt5.QtCore import QUrl, QObject, QSettings, QMetaObject, QVariant, Q_ARG, Q_RETURN_ARG, Qt, QTimer, QIODevice, QTranslator
from PyQt5.QtGui import QGuiApplication, QIcon
from PyQt5.QtQuick import QQuickView, QQuickItem

from PyQt5.QtSerialPort import QSerialPortInfo, QSerialPort
from MySerial import cSerialStream
from PyQt5.QtNetwork import QNetworkConfigurationManager

#this is the JSON of parameters
# it is different from paramItems, in that 'page' needs to be translated to 'nav'
# otherwise, we MUST ensure that those properties which are in paramItems are also in paramJson !!!!
# TODO: add such checks in createQmlObjectFiles()!!
# currently, e.g., not all entries have an adr property!
from PySTorM32App_paramjson import paramJson, paramLayoutVersion

#for converting 'page' to 'nav'
# in paramJson the tab of the parameter is identified by 'page'
# in navParamItems, used by the GUI, however by 'nav'
# this I may change once the old GUI is totally obsolete 
page2nav = {
    'dashboard': 'Dashboard',
    'pid': 'PID',
    'pan': 'Pan',
    'rcinputs': 'Rc Inputs',
    'functions': 'Functions',
    'scripts': 'Scripts',
    'setup': 'Setup',
    'gimbalconfig': 'Gimbal Configuration',
    'interfaces': 'Interfaces',
    'expert': 'Expert',
} 

navBarItems = [
    { 'nav': 'Dashboard', 'icon': '../icons/ic_home_black_48dp.png'},
    { 'nav': 'PID', 'icon': ''},
    { 'nav': 'Pan', 'icon': ''},
    { 'nav': 'Rc Inputs', 'icon': ''},
    { 'nav': 'Functions', 'icon': ''},
    { 'nav': 'Scripts', 'icon': ''},
    { 'nav': 'Setup', 'icon': ''},
    { 'nav': 'Gimbal Configuration', 'icon': ''},
    { 'nav': 'Calibrations', 'icon': ''},
    { 'nav': 'Interfaces', 'icon': ''},
    { 'nav': 'Expert', 'icon': ''},
    { 'nav': 'Tools', 'icon': ''},
    { 'nav': 'Firmware Update', 'icon': ''},
]



from STorM32Gui_createQmlObjectFiles import createNavBarQmlObjectFile

def createQmlObjectFiles():
    # create the MyNavBar.qml
    # this avoids having to create it dynamically
    createNavBarQmlObjectFile(navBarItems,paramJson,paramLayoutVersion,page2nav,forceCreateMyNavBarQml)

    
#//capability flags
cBOARD_CAPABILITY_FOC               = 0x0100

#//status flags
cSTATUS_IMU_PRESENT                 = 0x8000
#cSTATUS_IMU_HIGHADR                 = 0x4000 #DEPRECATED in NT version!!
cSTATUS_IMU_OK                      = 0x0020
cSTATUS_IMU2_PRESENT                = 0x1000
cSTATUS_IMU2_HIGHADR                = 0x0800
cSTATUS_IMU2_NTBUS                  = 0x0400
cSTATUS_IMU2_OK                     = 0x0040

cSTATUS_BAT_VOLTAGEISLOW            = 0x0010
cSTATUS_BAT_ISCONNECTED             = 0x0008
cSTATUS_LEVEL_FAILED                = 0x0004

cSTATUS_NTBUS_INUSE                 = 0x0200

cSTATUS_STORM32LINK_PRESENT         = 0x0100
cSTATUS_STORM32LINK_OK              = 0x0002
cSTATUS_STORM32LINK_INUSE           = 0x0001

cSTATUS_EMERGENCY                   = 0x0080

##cSTATUS_MAG_PRESENT              =  0x2000 #DEPRECATED

#//status2 flags
cSTATUS2_ENCODERS_PRESENT           = 0x8000
cSTATUS2_ENCODERYAW_OK              = 0x4000
cSTATUS2_ENCODERROLL_OK             = 0x2000
cSTATUS2_ENCODERPITCH_OK            = 0x1000

cSTATUS2_IRCAMERA                   = 0x0800
cSTATUS2_RECENTER_YAW               = 0x0400
cSTATUS2_RECENTER_ROLL              = 0x0200
cSTATUS2_RECENTER_PITCH             = 0x0100

cSTATUS2_MOTORYAW_ACTIVE            = 0x0020
cSTATUS2_MOTORROLL_ACTIVE           = 0x0010
cSTATUS2_MOTORPITCH_ACTIVE          = 0x0008

cSTATUS2_ISBUSY                     = 0x0001

#//status3 flags
cSTATUS3_GPSTARGET_PRESENT          = 0x0001
cSTATUS3_GPSTARGET_OK               = 0x0002
cSTATUS3_GPSHOME_PRESENT            = 0x0004
cSTATUS3_GPSHOME_OK                 = 0x0008
cSTATUS3_ISTRACKING                 = 0x0010

cSTATUS3_WIFIISCONNECTED            = 0x0100

#//state constants
cSTATE_STARTUP_MOTORS               = 0
cSTATE_STARTUP_SETTLE               = 1
cSTATE_STARTUP_CALIBRATE            = 2
cSTATE_STARTUP_LEVEL                = 3
cSTATE_STARTUP_MOTORDIRDETECT       = 4
cSTATE_STARTUP_RELEVEL              = 5
cSTATE_NORMAL                       = 6
cSTATE_STARTUP_FASTLEVEL            = 7
cSTATE_STANDBY                      = 99 #ATTENTION: in the @StateText array it is at position 8, so we need to map here


cSCRIPTSIZE             = 128
cCMD_s_PARAMETER_ZAHL   = 7 #number of values transmitted with a 's' get data command, V1: 5, V2: 7
cCMD_d_PARAMETER_ZAHL   = 32 #number of values transmitted with a 'd' get data command, V1: 32, V2: 32
cCMD_g_PARAMETER_ZAHL   = 155 #number of values transmitted with a 'g' get data command #is identical for all option lists

cRCCMDINSTX                         = b'\xFA'
cRCCMDINSTXWORESPONSE               = b'\xF9'
cRCCMDOUTSTX                        = b'\xFB'

cRC_CMD_GETVERSION                  = b'\x01'
cRC_CMD_GETVERSION_RESPONSE_LEN     = 11
cRC_CMD_GETVERSIONSTR               = b'\x02'
cRC_CMD_GETVERSIONSTR_RESPONSE_LEN  = 3 + 3*16 + 2
cRC_CMD_GETPARAMETER                = b'\x03'
cRC_CMD_GETPARAMETER_RESPONSE_LEN   = 9
cRC_CMD_GETDATA                     = b'\x05'
cRC_CMD_GETDATA_RESPONSE_LEN        = 3 + 2+2*cCMD_d_PARAMETER_ZAHL + 2
cRC_CMD_GETDATAFIELDS               = b'\x06' #response length depends on cmd!!
cRC_CMD_ACK_LEN                     = 6


cConnectionMaxAllowedLosts = 4
   
   
hashStatus = {}
hashInfo = {}
hashDataDisplay = {}

StateText = ['strtMOTOR','SETTLE','CALIBRATE','LEVEL','AUTODIR','RELEVEL','NORMAL','FASTLEVEL','STANDBY','unknown','unknown']
   
        
class cMain(QQuickView):

    def __init__(self,_settings):
        super(QQuickView,self).__init__()
        self.setTitle('    ') #TopLineAppNameStr+'  '+VersionStr) #'   ')
        #this sets the icon in the titlebar
#        self.setIcon(QIcon('resources/icons/owdefault.ico'))
        #self.setIcon(QIcon('resources/icons/hurricane.png'))
#        self.setIcon(QIcon('resources/icons/output.ico'))
        self.setIcon(QIcon('resources/icons/logo-storm32.ico')) 

        self.isFoc = True
        self.isConnected = False
        self.executeCmdIsRunning = False
        self.dataDisplayIsRunning = False
        self.configureGimbalToolIsRunning = False
        self.accCalibrationIsRunning = False
        
        self.lastBlinkerTickTime = 0.0
        self.lastStatusUpdateTickTime = 0.0
        self.communicationBlinker = -1 #blinker off
        self.connectionLostCounter = 0

        self.uartBaudrate = 115200 #0: 9600, 1: 19200, 2: 38400, 3: 57600, 4: 115200
        self.readPortIsRunning = False #a QThread based solution would be MUCH better
        self.extendedTimoutFirst = 0.0

        self.networkManager = QNetworkConfigurationManager() 
        
        self.initQmlInterface()
        self.portPopulateList()
        self.signalChangedAllToDefault() #send signals to adapt UI to default state

        self.settings = _settings
        self.readSettings() #this sets Port, uartBaudrate
        
        self.serial = cSerialStream(self.uartBaudrate)

        time.perf_counter()
        
        self.TextOut('\n\nPlease do first a read to get controller settings!\n')
        self.TextOutEnabled = False
        
        #do this at last, to ensure that timer doesn't start ticking before everything is set up
        self.communicationTimer = QTimer()
        self.communicationTimer.timeout.connect(self.CommunicationTimerTick)
        self.communicationTimer.start(100)
        
    def closeEvent(self):
        self.writeSettings()

    def readSettings(self):
        ##settings = QSettings(IniFileStr, QSettings.IniFormat)
        #if int(settings.value('SYSTEM/LoadTraffic',0)):
        #    self.bLoadTraffic.setCheckState(QtCore.Qt.Checked)
        #if( int(settings.value('SYSTEM/GraphShowPoints',0)) ):
        #    self.bGraphShowPoints.setCheckState(QtCore.Qt.Checked)
        p = self.settings.value('PORT/Port')
        if p: self.portSetPort(p)
        p = self.settings.value('PORT/Baudrate')
        self.uartBaudrate  = 115200
        if p:
            if (p == '9600') or (p == '19200') or (p == '38400') or (p == '57600') or (p == '115200'):
                self.uartBaudrate = int(p)

    def writeSettings(self):
        ##settings = QSettings(IniFileStr, QSettings.IniFormat)
        #if self.bLoadTraffic.checkState()==QtCore.Qt.Checked:
        #    settings.setValue('SYSTEM/LoadTraffic',1)
        #else:
        #    settings.setValue('SYSTEM/LoadTraffic',0)
        #if self.bGraphShowPoints.checkState()==QtCore.Qt.Checked:
        #    settings.setValue('SYSTEM/GraphShowPoints',1)
        #else:
        #    settings.setValue('SYSTEM/GraphShowPoints',0)
        #settings.setValue('SYSTEM/Style',appPalette)
        self.settings.setValue('PORT/Port', self.portGetPort())
        self.settings.setValue('PORT/Baudrate', self.uartBaudrate)
        ##self.settings.sync()
        
        
    '''
    ####################################################    
    # interface to QML
    ####################################################    
    '''
    #methods to communciate between Qml and Py
    # 1) send signals to Qml
    # 2) call functions of Qml
    # 3) receive signals from Qml
    # 
    # 4) provide functions which Qml can call, this is "outsourced" to cpyQml class
    
    def initQmlInterface(self):
        createQmlObjectFiles()
        # load the Qml
        QMLFile = ApplicationStr + '_q'+'{:0>3}'.format(QMLVersion)+'.qml'
        #this is for freezing with pyinstaller        
        if getattr(sys, 'frozen', False):
            bundle_dir = sys._MEIPASS # we are running in a bundle
        else:
            bundle_dir = os.path.dirname(os.path.abspath(__file__)) # we are running in a normal Python environment
        self.setSource(QUrl.fromLocalFile(os.path.join(bundle_dir,QMLFile)))

        #QUESTION:  rootObject? rootContext?
        self.qmlRoot = self.rootObject()
        
        #QML signals received from qml
        self.closing.connect(self.closeEvent) #this signal is emitted by QQuickWindow, doesn't have a closeEvent() like QWidget
        
        self.qmlRoot.btConnectQmlClicked.connect(self.btConnectClicked)
        self.qmlRoot.btReadQmlClicked.connect(self.btReadClicked)
        self.qmlRoot.btWriteQmlClicked.connect(self.btWriteClicked)
        
        self.qmlRoot.btAutoTrimQmlClicked.connect(self.btAutoTrimClicked)
        self.qmlRoot.btEnableAllMotorsQmlClicked.connect(self.btEnableAllMotorsClicked)
        self.qmlRoot.btDisableAllMotorsQmlClicked.connect(self.btDisableAllMotorsClicked)
        self.qmlRoot.btGimbalConfigureToolQmlClicked.connect(self.btGimbalConfigureToolClicked)
        
        self.qmlRoot.btLevelGimbalQmlClicked.connect(self.btLevelGimbalClicked)
        self.qmlRoot.btRestartControllerQmlClicked.connect(self.btRestartControllerClicked)
        self.qmlRoot.btGetCurrentEncoderPositionQmlClicked.connect(self.btGetCurrentEncoderPositionClicked)
        self.qmlRoot.btEraseEepromQmlClicked.connect(self.btEraseEepromClicked)
        self.qmlRoot.btEsp8266ConfigureToolQmlClicked.connect(self.btEsp8266ConfigureToolClicked)
        self.qmlRoot.btChangeBoardNameToolQmlClicked.connect(self.btChangeBoardNameToolClicked)
        self.qmlRoot.btChangeUartBaudrateToolQmlClicked.connect(self.btChangeUartBaudrateToolClicked)
        self.qmlRoot.btChangeEncoderSupportToolQmlClicked.connect(self.btChangeEncoderSupportToolClicked)
        
        self.qmlRoot.btShareSettingsScreenShotQmlClicked.connect(self.btShareSettingsScreenShotClicked)

        #QML items
        self.qmlCbPort = self.qmlRoot.findChild(QObject, 'cbPort')
        self.qmlCbPortPopup = self.qmlRoot.findChild(QObject, 'cbPortPopup')
        self.qmlCbPortPopup.aboutToShow.connect(self.portPopupAboutToShow)
        
        QMetaObject.invokeMethod(
            self.qmlRoot, 'setAppNameVersion', Qt.DirectConnection,
            Q_ARG(QVariant,QVariant(TopLineAppNameStr)), Q_ARG(QVariant,QVariant(TopLineAppVersionStr)),
        )
   

    ####################################################    
    # Port ComboBox
    
    # signal received from Qml
    def portPopupAboutToShow(self):
        print( 'aboutToShow handler called' )
        self.portPopulateList()
    
    # function to Qml 
    def portPopulateListWith(self,list):
        self.invokeQmlMethod('cbPortPopulateListWith',list)

    # function to Qml 
    def portSetPort(self,port):
        self.invokeQmlMethod('cbPortSetPort',port)

    # function to Qml 
    def portGetPort(self):
        return re.sub(r'\s', '', self.qmlCbPort.property('displayText'))
    
    def portKey(self,_s):
        return int(_s[3:7])

    def portPopulateList(self):
        availableSerialPortList = []
        # use QSerialPortInfo to discover COM ports
        availableSerialPortInfoList = QSerialPortInfo().availablePorts()
        for portInfo in availableSerialPortInfoList:
            s = portInfo.portName()
            if len(s) == 5: s += '     '
            else:           s += '       '
            p = portInfo.description()
            if re.search(r'Virtual COM Port',p):  d = 'Virtual COM Port'
            #elif re.search(r'USB to UART Bridge',p): d = 'Virtual COM Port'
            elif re.search(r'Silicon Labs CP',p): d = 'Silabs Virtual COM Port'
            elif re.search(r'USB Serial Port',p): d = 'USB Serial Port'
            elif re.search(r'Bluetooth',p):       d = 'Bluetooth'
            #elif re.search(r'Standard',p):        d = 'Standard Port'
            else: d = 'Standard'
            availableSerialPortList.append(s+'('+d+')')
        # ensure that we have at least one entry
        if len(availableSerialPortList) <= 0: availableSerialPortList = ['COM1    Standard']
        availableSerialPortList.sort(key=self.portKey)
        # use QNetworkManager to discover wifi access points
        confs = self.networkManager.allConfigurations()
        for conf in confs:
            if (conf.name() == EspWifiName) and (int(conf.state()) == 0x0e): #QNetworkConfiguration.Active, flag 0x08 is set
                availableSerialPortList.append('ESP           ('+EspWifiName+')')
        # done, send signal to Qml        
        self.portPopulateListWith(availableSerialPortList)

        
    ####################################################    
    # functions to communicate with Qml
    
    def invokeQmlMethod(self,func,param):
        QMetaObject.invokeMethod(self.qmlRoot, func, Qt.DirectConnection, Q_ARG(QVariant,QVariant(param)))
        
    def logText(self,txt):
        #self.invokeQmlMethod('logArea',txt)
        QMetaObject.invokeMethod(
            self.qmlRoot,
            'logText',
            Qt.DirectConnection,
            Q_ARG(QVariant,QVariant(txt))
        )
        
    def setParamValues(self,newParamValues):
        QMetaObject.invokeMethod(
            self.qmlRoot, 
            'setParamValues', 
            Qt.DirectConnection, 
            Q_ARG(QVariant,QVariant(newParamValues))
        )

    def getParamValues(self):
        jsres = QMetaObject.invokeMethod(
            self.qmlRoot, 
            'getParamValues', 
            Qt.DirectConnection,
            Q_RETURN_ARG(QVariant)
            #Q_ARG(QVariant,QVariant(-1))
        )
        res = jsres.toVariant()
        return res
        
        
    ####################################################    
    # signals for sending to Qml
    # we also use them to handle some status values here
    
    def signalFocChanged(self, foc):
        self.isFoc = foc
        self.qmlRoot.signalFocChanged.emit(self.isFoc)
        
    def signalInfoChanged(self, infoList): #{}
        self.qmlRoot.signalInfoChanged.emit(infoList)

    def signalStatusChanged(self, statusText):
        self.qmlRoot.signalStatusChanged.emit(statusText)

    # -1: blinker off, 0: blinker light, 1: blinker bright        
    def signalBlinkerChanged(self, blinker):
        self.communicationBlinker = blinker
        self.qmlRoot.signalBlinkerChanged.emit(blinker)

    def signalConnectedChanged(self, connected):
        self.isConnected = connected
        self.qmlRoot.signalConnectedChanged.emit(self.isConnected)
        
    def signalVersionChanged(self, versionList): #{}
        self.qmlRoot.signalVersionChanged.emit(versionList)
        
    def signalParametersChanged(self, paramArray):
        self.qmlRoot.signalParametersChanged.emit(paramArray)
        
    def signalParamValueByNameChanged(self, paramName, newParamValue):
        self.qmlRoot.signalParamValueByNameChanged.emit(paramName, newParamValue)
        
    def signalParamStrByNameChanged(self, paramName, newParamStr):
        self.qmlRoot.signalParamStrByNameChanged.emit(paramName, newParamStr)
        
    def signalChangedAllToDefault(self):
        self.signalFocChanged(True)
        self.signalConnectedChanged(False)
        
        
    ####################################################    
    # signals received from Qml

    def btConnectClicked(self):
        print( 'Connect handler called' )
        if self.isConnected:
            self.DoDisconnectFromBoard()
        else:    
            self.DoConnectToBoard()
       
    def btReadClicked(self):
        print( 'Read handler called' )
        if self.executeCmdIsRunning: return #prevents double clicks
        self.executeCmdIsRunning = True
        if self.isConnected:
            self.DoRead()
        else:
            self.DoConnectToBoard() #ConnectToBoard() does a read
        #SynchroniseConfigTabs();
        self.executeCmdIsRunning = False
       
    def btWriteClicked(self, withStore):
        print( 'Write handler called', withStore )
        if not self.isConnected: return
        if self.executeCmdIsRunning: return #prevents double clicks
        self.executeCmdIsRunning = True
        self.DoWrite(withStore)
        self.executeCmdIsRunning = False
       
    def btAutoTrimClicked(self):
        print( 'AutoTrim handler called' )
        if not self.isConnected: return
        if self.executeCmdIsRunning: return
        self.executeCmdIsRunning = True
        self.DoCalibrateRcTrim()
        self.executeCmdIsRunning = False

    def btEnableAllMotorsClicked(self):
        print( 'EnableAllMotors handler called' )
        if not self.isConnected: return
        if self.executeCmdIsRunning: return
        self.executeCmdIsRunning = True
        self.DoSetUsageOfAllMotors(0) # 0 = 'normal'
        self.executeCmdIsRunning = False

    def btDisableAllMotorsClicked(self):
        print( 'DisableAllMotors handler called' )
        if not self.isConnected: return
        if self.executeCmdIsRunning: return
        self.executeCmdIsRunning = True
        self.DoSetUsageOfAllMotors(3) # 3 = 'disabled'
        self.executeCmdIsRunning = False

    def btLevelGimbalClicked(self):
        print( 'LevelGimbal handler called' )
        if not self.isConnected: return
        if self.executeCmdIsRunning: return
        self.executeCmdIsRunning = True
        self.DoLevelGimbal()
        self.executeCmdIsRunning = False

    def btRestartControllerClicked(self):
        print( 'RestartController handler called' )
        if not self.isConnected: return
        if self.executeCmdIsRunning: return
        self.executeCmdIsRunning = True
        self.DoRestartController()
        self.executeCmdIsRunning = False

    def btGetCurrentEncoderPositionClicked(self):
        print( 'GetCurrentEncoderPositions handler called' )
        if not self.isConnected: return
        if self.executeCmdIsRunning: return
        self.executeCmdIsRunning = True
        self.DoGetCurrentEncoderPositions()
        self.executeCmdIsRunning = False

    def btEraseEepromClicked(self):
        print( 'EraseEeprom handler called' )
        if not self.isConnected: return
        if self.executeCmdIsRunning: return
        self.executeCmdIsRunning = True
        self.DoEraseEeprom()
        self.executeCmdIsRunning = False

    def btGimbalConfigureToolClicked(self):
        print( 'GimbalConfigureTool handler called' )

    def btEsp8266ConfigureToolClicked(self):
        print( 'Esp8266ConfigureTool handler called' )

    def btChangeBoardNameToolClicked(self,boardName):
        print( 'BoardNameTool handler called', boardName )
        if not self.isConnected: return
        if self.executeCmdIsRunning: return
        self.executeCmdIsRunning = True
        self.DoChangeBoardName(boardName)
        self.executeCmdIsRunning = False

    def btChangeUartBaudrateToolClicked(self,baudrate):
        print( 'UartBaudrateTool handler called', baudrate )
        if not self.isConnected: return
        if self.executeCmdIsRunning: return
        self.executeCmdIsRunning = True
        self.DoChangeUartBaudrate(baudrate)
        self.executeCmdIsRunning = False

    def btChangeEncoderSupportToolClicked(self):
        print( 'ChangeEncoderSupportTool handler called' )
        if not self.isConnected: return
        if self.executeCmdIsRunning: return
        self.executeCmdIsRunning = True
        self.DoChangeEncoderSupport()
        self.executeCmdIsRunning = False
        
    def btShareSettingsScreenShotClicked(self,objectName,fileUrl): 
        print( 'ShareSettingsScreenShot handler called' )
        if fileUrl[0:5] != 'file:': return
        obj = self.qmlRoot.findChild(QQuickItem, objectName)
        img = obj.grabToImage()
        self.DelaySec(1) #the grab is asynchronous, and one would have to cathc the signal QQuickItemGrabResult::ready()
        img.saveToFile(fileUrl[8:])


    '''
    ####################################################    
    # 
    ####################################################    
    '''
    
    def DelaySec(self,delayTime):
        tStart = self.getTime()
        while True:
            QGuiApplication.processEvents() #a thread based solution would be MUCH better
            t = self.getTime()
            if t-tStart > delayTime: return

            
    def TextOut(self,_text):
        self.logText(_text)

    def TextOutW(self,_text):
        self.TextOut(_text)
        self.DelaySec(0.05)
        
        
    def EnableTextOut_(self):
        self.TextOutEnabled = True
        
    def DisableTextOut_(self):
        self.TextOutEnabled = False

    def TextOut_(self,_text):
        #print(_text)
        if self.TextOutEnabled: self.TextOut(_text)

    def TextOutW_(self,_text):
        #print(_text)
        if self.TextOutEnabled: self.TextOutW(_text)

        
    def GREEN(self,_text):
        return '<font color=green>' + _text + '</font>'

    def RED(self,_text):
        return '<font color=red><b>' + _text + '</b></font>'
        
        
    '''
    ####################################################    
    # basic communication routines
    ####################################################    
    '''
    
    #uses MAVLINK's x25 checksum
    #https://github.com/mavlink/pymavlink/blob/master/mavutil.py
    def DoCrc(self,barray):
        crc = 0xFFFF
        for b in barray:
            tmp = int(b) #tmp = int.from_bytes(b, byteorder='little')
            tmp = tmp ^ (crc & 0xFF )
            tmp = (tmp ^ (tmp<<4)) & 0xFF
            crc = (crc>>8) ^ (tmp<<8) ^ (tmp<<3) ^ (tmp>>4)
            crc = crc & 0xFFFF
        return crc

    def AddCrc(self,barray):
        crc = self.DoCrc(barray)
        return crc.to_bytes(2, byteorder='little')

    def Int16FromBytes(self,b):
        return int.from_bytes(b, byteorder='little')
        
    def Int16FromBytesAt(self,b,i):
        return int.from_bytes(b[2*i:2*i+2], byteorder='little')
        
    def Int16ToBytes(self,i):
        return i.to_bytes(2, byteorder='little')

    def Int8ToBytes(self,i):
        return i.to_bytes(1, byteorder='little')
        
        
    def getTime(self):
        return time.perf_counter()
        

    def serialBytesAvailable(self):
        return self.serial.bytesAvailable()
        
    def serialReadOneByte(self):
        return self.serial.readOneByte()
        
    def OpenPort(self):
        port = self.portGetPort()
        if port == '': return False
        if port == 'ESP':
            self.serial.open(port,True,EspWifiUrl,EspWifiPort)
        else:
            #return self.serial.open(port)
            self.serial.open(port)
        return self.serial.isValid()
        
    def ClosePort(self):
        self.serial.close()

    def FlushPort(self):
        self.serial.flush()

    def WritePort(self,cmdByte):
        self.serial.writeBytes(cmdByte)


    def SetExtendedTimoutFirst(self,tmo):
        self.extendedTimoutFirst = tmo
    
    def GetTimeoutsForReading(self):
        timeoutFirst = 0.5 + self.extendedTimoutFirst
        self.extendedTimoutFirst = 0.0
        timeout = 0.1
        return (timeoutFirst, timeout)  #tmo=0.1, tmofirst=0.01 works for 'v'
        '''
        timeoutFirst = 0.02 * self.executeCmdTimeOutFirst + self.extendedTimoutFirst
        extendedTimoutFirst = 0.0
        timeout = 20.0 * self.executeCmdTimeOut #timeout in s
        if timeout < 0.2: timeout = 0.2
        #if( $Baudrate <= 38400 ){ $timeout += 20*$ExecuteCmdTimeOut; } #add time for slow connetions
        if self.PortIsBlueTooth():
            timeoutFirst += 0.2
            timeout += 0.2 + 0.02 * self.executeCmdBTAddedTimeOut
        return (timeout, timeoutFirst)
        '''
    
    # main serial read command
    #  len > 0 indicates that the command returns more than the end char, and that a CRC is recieved
    #  len is exclusive the CRC and the 'o' endbyte 
    # global parameters
    #  executeCmdTimeOutFirst
    #  executeCmdTimeOut
    #  executeCmdBTAddedTimeOut
    # flags:
    #  extendedTimoutFirst: allows to momentarily increase the timeout for first char, this is needed for e.g. StoreToEERPOM
    #COMMENT: for len=0 where must be no 't''e''c''o' in the data stream!
    #ensure that result is always at least one byte long! otherwise test [-1:] may fail
    def ReadPortSimpleCmd(self,_len=0):
        if _len < 0: _len = 0
        if _len > 0: _len += 2 #take CRC into account
        cmd = b''
        result = b''
        (tmoFirst,tmo) = self.GetTimeoutsForReading()
        tStart = self.getTime()
        self.readPortIsRunning = True #required to prevent timer to tick #a QThread based solution would be MUCH better
        while True:
            QGuiApplication.processEvents() #a QThread based solution would be MUCH better
            curTmo = tmo
            if len(result) == 0: curTmo = tmoFirst
            t = self.getTime()
            if t-tStart > curTmo: self.readPortIsRunning = False; return b'T' #timeout!
            if self.serialBytesAvailable():
                b = self.serialReadOneByte()    
                tStart = t #restart tmo
                result += b
                if _len > 0:
                    # we wait for a response of defined length
                    # the '>' accounts for the endbyte
                    if( len(result) > _len ): cmd = b; #break; #get the last char of the result string
                else:
                    cmd = b; #break; #get the last char of the result string
                if cmd == b't': self.readPortIsRunning = False; return b't'
                if cmd == b'e': self.readPortIsRunning = False; return b'e'
                if cmd == b'c': self.readPortIsRunning = False; return b'c'
                if cmd == b'o': break
        if _len > 0:
            #check CRC (uses MAVLINK's x25 checksum)
            #crc = result[-3:-1]
            #print(crc)
            crc2 = self.DoCrc(result[:-1])
            #print(crc2)
            if crc2 != 0: self.readPortIsRunning = False; return b'c'
        #print(result)
        self.readPortIsRunning = False #a QThread based solution would be MUCH better
        return result

    # for simple serial commands        
    def SendCmd(self,cmdByte):
        self.WritePort(cmdByte)
        return self.ReadPortSimpleCmd(self.CmdLength(cmdByte))   #len)
        
    # for simple serial commands, which have data        
    def SendCmdwData(self,cmdByte,dataByte):
        self.WritePort(cmdByte + dataByte + self.AddCrc(dataByte))
        return self.ReadPortSimpleCmd(self.CmdLength(cmdByte))

    # for simple serial commands 's', 'd', called in communiction loop
    def SendCmdinCommunicationLoop(self,cmdByte):
        self.WritePort(cmdByte)
        return self.ReadPortSimpleCmd(self.CmdLength(cmdByte))
        
    # get the length of the response to a simple serial command
    def CmdLength(self,cmdByte):
        if cmdByte == b's': return cCMD_s_PARAMETER_ZAHL*2 #7 * 2 
        if cmdByte == b'd': return cCMD_d_PARAMETER_ZAHL*2 #32 * 2
        if cmdByte == b'g': return cCMD_g_PARAMETER_ZAHL*2 + cSCRIPTSIZE #155 * 2 + 128
        if cmdByte == b'v': return 54
        return 0
        
    def GetControllerVersion(self):
        version = ''; name = ''; board = ''; layout = 0; capabilities = 0;
        res = self.SendCmd(b'v')
        if res[-1:] == b'o':
            version = res[0:16].replace(b'\x00',b'').decode("utf-8").strip()
            name = res[16:32].replace(b'\x00',b'').decode("utf-8").strip()
            board = res[32:48].replace(b'\x00',b'').decode("utf-8").strip()
            ver = self.Int16FromBytes(res[48:50])
            layout = self.Int16FromBytes(res[50:52])
            capabilities = self.Int16FromBytes(res[52:54])
        return (version,name,board,layout,capabilities)

    #the status info is digested by 3 Qml objects
    # - the status line
    # - the info field
    # - the DataDisplay
    # we handle each of them individually
    # we could do the math in Qml using javascript, which could have the advantage of code reuse with WebApp
    # but for the moment we do it here
    
    def GetStateText(self,state):
        index = -1
        if state <= cSTATE_STARTUP_FASTLEVEL: index = state
        elif state == cSTATE_STANDBY:         index = 8
        else:                                 index = -1
        return StateText[index]
    
    
    # we generate a hash with the id as key
    # this extracts info from the 's' command data stream, and stores it in the StatusInfoHash hash for later use
    def ExtractStatus(self,data):
        state = self.Int16FromBytesAt(data,0) #state
        status = self.Int16FromBytesAt(data,1) #status
        status2 = self.Int16FromBytesAt(data,2) #status2
        status3 = self.Int16FromBytesAt(data,3) #status3
        errors = self.Int16FromBytesAt(data,5)
        voltage = self.Int16FromBytesAt(data,6)
        # IMU
        sA = ''; sB = ''
        if status & cSTATUS_IMU_PRESENT:
            sA = QGuiApplication.translate('pyStatus','is PRESENT')
            sA += ' @ NTBUS' #status is always $STATUS_NTBUS_INUSE!
            if status & cSTATUS_IMU_OK: sB = 'OK'
            else:                       sB = 'ERR'
        else:
            sA = QGuiApplication.translate('pyStatus','is not available')
            sB = QGuiApplication.translate('pyStatus','none')
        hashInfo['imu1A'] = sA
        hashInfo['imu1B'] = sB
        hashStatus['imu1'] = sB
        # IMU2
        if status & cSTATUS_IMU2_PRESENT:
            sA = QGuiApplication.translate('pyStatus','is PRESENT')
            if status & cSTATUS_IMU2_NTBUS:
                sA += ' @ NTBUS'
            else:
                if status & cSTATUS_IMU2_HIGHADR:
                    sA += ' @ HIGH ADR = on-board IMU'
                else:
                    sA += ' @ LOW ADR = external IMU'
            if status & cSTATUS_IMU_OK: sB = 'OK'
            else:                       sB = 'ERR'
        else:
            sA = QGuiApplication.translate('pyStatus','is not available')
            sB = QGuiApplication.translate('pyStatus','none')
        hashInfo['imu2A'] = sA
        hashInfo['imu2B'] = sB
        hashStatus['imu2'] = sB
        # ENCODERS
        if status2 & cSTATUS2_ENCODERS_PRESENT: 
            sA = QGuiApplication.translate('pyStatus','are PRESENT')
            if status2 & cSTATUS2_ENCODERPITCH_OK: sB = 'OK'
            else: sB = 'ERR'
            if status2 & cSTATUS2_ENCODERROLL_OK: sB += ' OK'
            else: sB += ' ERR'
            if status2 & cSTATUS2_ENCODERYAW_OK: sB += ' OK'
            else: sB += ' ERR'
        else:
            sA = QGuiApplication.translate('pyStatus','are not available')
            sB = '- - -'
        hashInfo['encodersA'] = sA
        hashInfo['encodersB'] = sB
        # STORM-LINK
        if status & cSTATUS_STORM32LINK_PRESENT:
            if status & cSTATUS_STORM32LINK_INUSE:
                sA = QGuiApplication.translate('pyStatus','is INUSE')
                if status & cSTATUS_STORM32LINK_OK: sB = 'OK'
                else: sB = 'ERR'
            else:
                sA = QGuiApplication.translate('pyStatus','is PRESENT')
                if status & cSTATUS_STORM32LINK_OK: sB = 'OK'
                else: sB = 'PSNT'
        else:
            sA = QGuiApplication.translate('pyStatus','is not available')
            sB = QGuiApplication.translate('pyStatus','none')
        hashInfo['link'] = sA
        # BAT
        if status & cSTATUS_BAT_ISCONNECTED:
            sA = QGuiApplication.translate('pyStatus','is CONNECTED')
        else:
            sA = QGuiApplication.translate('pyStatus','is not connected')
        hashInfo['bat'] = sA
        # MOTORS
        sA = QGuiApplication.translate('pyStatus','are')
        if status2 & cSTATUS2_MOTORPITCH_ACTIVE: sA += ' ' + QGuiApplication.translate('pyStatus','ACTIVE')
        else: sA += ' ' + QGuiApplication.translate('pyStatus','OFF')
        if status2 & cSTATUS2_MOTORROLL_ACTIVE: sA += ' ' + QGuiApplication.translate('pyStatus','ACTIVE')
        else: sA += ' ' + QGuiApplication.translate('pyStatus','OFF')
        if status2 & cSTATUS2_MOTORYAW_ACTIVE: sA += ' ' + QGuiApplication.translate('pyStatus','ACTIVE')
        else: sA += ' ' + QGuiApplication.translate('pyStatus','OFF')
        hashInfo['motors'] = sA
        # STATE
        sA = self.GetStateText(state)
        hashInfo['state'] = QGuiApplication.translate('pyStatus','is') + ' ' + sA #.toupper()
        hashStatus['state'] = sA
        # VOLTAGE
        sV = '{:.2f}'.format(voltage/1000.0)+' V'
        if status & cSTATUS_BAT_VOLTAGEISLOW:
            sA = QGuiApplication.translate('pyStatus','LOW')
        else:
            sA = 'OK'
        hashInfo['voltage'] = QGuiApplication.translate('pyStatus','is') + ' ' + sA + ': ' + sV
        hashStatus['voltage'] = sA + ' ' + sV
        # ERRORS
        sA = str(errors)
        hashInfo['errors'] = sA
        hashStatus['errors'] = sA
        # WIFI
        if status3 & cSTATUS3_WIFIISCONNECTED:
            sA = 'connected'
        else:
            sA = 'not connected'
        hashInfo['wifi'] = sA
        

    def GetSatusText(self):
        s = ''
        s += 'IMU: ' + hashStatus['imu1']
        s += '    '
        s += 'IMU2: ' + hashStatus['imu2']
        s += '    '
        s += QGuiApplication.translate('pyStatus','VOLTAGE') + ': ' + hashStatus['voltage']
        s += '    '
        s += QGuiApplication.translate('pyStatus','STATE') + ': ' + hashStatus['state']
        s += '                   '
        #s += $StatusInfoHash{ERRORcondition};
        return s

        
    '''
    ####################################################    
    # communication handling
    ####################################################    
    '''
        
    # main communication scheduler     
    def CommunicationTimerTick(self):
        if self.readPortIsRunning: return
        if self.executeCmdIsRunning: return
        self.currentTickTime = time.perf_counter()
        if not self.isConnected: return
        '''
        $AutoWritePIDTimerCounter--;
        if( $AutoWritePIDTimerCounter<0 ){ $AutoWritePIDTimerCounter = SetIntTime(250); } #5 } #X00ms
        '''
        if (not self.isConnected) and (not self.configureGimbalToolIsRunning) and (not self.accCalibrationIsRunning):
            #BUG: do we really want to do this every tick!!!!!
            #self.signalInfoChanged('')
            #self.signalBlinkerChanged(-1)
            #self.signalStatusChanged('no connection')
            return
        
        # blinker
        if self.currentTickTime - self.lastBlinkerTickTime > 0.25:
            self.lastBlinkerTickTime = self.currentTickTime
            if self.communicationBlinker > 0:
                self.signalBlinkerChanged(0)
            else:
                self.signalBlinkerChanged(1)
            
        # read required data
        res = b''
        if self.dataDisplayIsRunning:
            #the timer loop is running at the speed of the DataDisplay update rate
            self.FlushPort()
            res = self.SendCmdinCommunicationLoop(b'd')
        elif self.accCalibrationIsRunning:
            #$w_Main->m_StatusField->Text( 'acc calibration' );
            #DashboardStatusTextClearTo( 'acc calibration running' );
            self.signalStatusChanged('acc calibration running')
            return
        elif self.configureGimbalToolIsRunning:
            #$w_Main->m_StatusField->Text( 'configure gimbal tool' );
            #DashboardStatusTextClearTo( 'configure gimbal tool running' );
            self.signalStatusChanged('configure gimbal tool running')
            return
        else:
            if self.currentTickTime - self.lastStatusUpdateTickTime < 0.25:
                return
            self.lastStatusUpdateTickTime = self.currentTickTime    
            self.FlushPort() #this is to get back in track in case of issue
            res = self.SendCmdinCommunicationLoop(b's')
            if res[-1:] == b'o':
                self.ExtractStatus(res)
                self.signalInfoChanged(hashInfo)
                self.signalStatusChanged(self.GetSatusText())
            
        # check if connection still ok
        if res[-1:] != b'o':
            self.connectionLostCounter += 1
            if self.connectionLostCounter > cConnectionMaxAllowedLosts:
                self.TextOut( "\n\n"+self.RED(self.tr('CONNECTION is LOST!')) )
                self.DoDisconnectFromBoard() #self.DisconnectFromBoard()
            return
        self.connectionLostCounter = 0
        
        '''
        # do the required things,
        if( $DataDisplay_IsRunning ){
            DataDisplayDoTimer( $s );
            if( $LastDataDisplayState==0 ){
                $w_Main->m_StatusField->Text( 'data display' );
                DashboardStatusTextClearTo( 'data display running' );
            }
        }
        $LastDataDisplayState = $DataDisplay_IsRunning;

        EXIT:
        AutoWritePID_CheckIfItShouldBeDone();
        '''
        
    def ConnectionIsValid(self):
        if not self.isConnected: return False
        if self.connectionLostCounter > 0: return False
        return True

        
    #parameter: 1 = clear text field
    def DisconnectFromBoard(self,clearOptions=False):
        #DataDisplayHalt();
        #DataDisplayClearStatusFields();
        #if( $Acc16PCalibration_IsRunning ){ Acc16PCalibrationHalt(); } #in order to not call it when acc calib was not yet created
        self.ClosePort()
        self.signalConnectedChanged(False)

    def DoDisconnectFromBoard(self,clearOptions=False):
        self.DisconnectFromBoard(clearOptions)
        self.TextOut('\n\n\n'+self.tr('Please do first a read to get controller settings!')+'\n')

        
    def ConnectToBoard(self,skipExecuteRead=False):
        if self.isConnected: return True
        self.TextOutW_( "\n\n"+self.tr('Connecting') + '... ' + self.tr('please wait') )
        # open port
        if not self.OpenPort():
            self.TextOut_( "\n"+self.RED(self.tr('Connecting') + '... ' + self.tr('ABORTED!')) )
            self.TextOut_( "\n"+self.tr('Opening port') + ' ' + self.portGetPort() + ' ' + self.tr('FAILED!') )
            self.DisconnectFromBoard()
            return False
        # check connection
        self.TextOutW_( "\n"+'v... ' )
        (version,name,board,layout,capabilities) = self.GetControllerVersion()
        if version == '':
            self.TextOut_( "\n"+self.RED(self.tr('Read') + '... ' + self.tr('ABORTED!')) )
            self.DisconnectFromBoard()
            return False
        self.TextOut_( version )
        # check layout version
        layoutIsSupported = False
        if layout == paramLayoutVersion: 
            layoutIsSupported = True
        if not layoutIsSupported:
            self.TextOut_( "\n"+self.RED(self.tr('Read') + '... ' + self.tr('ABORTED!')) )
            self.TextOut_( "\n"+self.tr('The connected controller board or its firmware version is not supported!') )
            self.TextOut_( "\n"+self.tr('Retry with GUI version') + ' ' + version )
            self.DisconnectFromBoard()
            return False
        # everything is OK
        #$f_Tab{calibrateacc}->caac_StoreInEEprom->Enable();
        self.TextOutW_( "\n"+self.GREEN(self.tr('CONNECTED')) )
        self.connectionLostCounter = 0
        self.signalConnectedChanged(True)
        # handle the capabilities
        if capabilities & cBOARD_CAPABILITY_FOC: 
            self.signalFocChanged(True)
        else:
            self.signalFocChanged(False)
        #update dashboard
        self.signalVersionChanged( { 'Firmware Version' : version, 
                                     'Board' : board,
                                     'Name' : name } )
        if not skipExecuteRead:
            self.DisableTextOut_()
            if not self.ExecuteRead(): 
                self.DisconnectFromBoard() # this should not happen, but who knows
                return False

        return True

        
    def ConnectToBoardwoRead(self):
        self.ConnectToBoard(True)

        
    def DoConnectToBoard(self,skipExecuteRead=False):
        self.EnableTextOut_()
        self.ConnectToBoard(skipExecuteRead)
        self.DisableTextOut_()

        
    '''
    ####################################################    
    # higher level communication routines
    ####################################################    
    '''

    def ParamListFromParamBytes(self,b):
        params = []
        for i in range(cCMD_g_PARAMETER_ZAHL):
            params.append( self.Int16FromBytesAt(b,i) )
        for i in range(cSCRIPTSIZE):
           params.append( int(b[cCMD_g_PARAMETER_ZAHL*2+i]) )
        return params
            
    def ParamBytesFromParamList(self,l):
        b = b''
        for i in range(cCMD_g_PARAMETER_ZAHL):
            b += self.Int16ToBytes(int(l[i]))
        for i in range(cSCRIPTSIZE):
            b += self.Int8ToBytes(int(l[cCMD_g_PARAMETER_ZAHL+i]))
        return b
        
            
    def ExecuteRead(self):
        # command 'g'
        self.TextOut_( "\n" + 'g... ' )
        res = self.SendCmd(b'g')
        if res[-1:] != b'o': return False
        self.TextOut_( self.tr('ok') )
        params = self.ParamListFromParamBytes(res)
        self.signalParametersChanged(params)
        return True
        
    def DoRead(self):
        s = self.tr('Read')
        self.TextOutW( "\n\n" + s + '... ' + self.tr('please wait') )
        self.EnableTextOut_()
        ok = self.ExecuteRead()
        self.DisableTextOut_()
        if not ok:
            self.TextOut( "\n" + self.RED(s + '... ' + self.tr('FAILED!')) )
            return
        self.TextOut( "\n" + self.GREEN(s + '... ' + self.tr('DONE!')) )

        
    def ExecuteWrite(self, withStore):
        # command 'p'
        self.TextOut_( "\n" + 'g... ' )
        res = self.SendCmd(b'g')
        if res[-1:] != b'o': return False
        self.TextOut_( self.tr('ok') )
        params = self.ParamListFromParamBytes(res)
        #get gui paramValues, and write them into the read paramValues
        guiparams = self.getParamValues() #this is WITH scripts!!!
        if len(guiparams) != len(params):
            print('SHIT!') #this MUST never happen!!
            self.TextOut_( self.RED('SHIT') )
            return False
        #copy those entries which are NOT in the GUI from the read list to the gui list
        # overwrite those entries for which there are values in the GUI
        for item in paramJson:
            if 'adr' in item:
                adr = item['adr']
                if adr >= 0:
                    params[adr] = guiparams[adr]
        for i in range(cSCRIPTSIZE):
            cp = cCMD_g_PARAMETER_ZAHL + i
            params[cp] = guiparams[cp]
        #now do the write        
        data = self.ParamBytesFromParamList(params)
        self.TextOut_( "\n" + 'p... ' )
        res = self.SendCmdwData(b'p',data)
        if res[-1:] != b'o': return False  #this is a SERIOUS fault, data corruption, we should disconnect??
        self.TextOut_( self.tr('ok') )
        #update qml        
        # this may look like an overkill, since it set the fields again, but why not, it ensures data consistency
        self.signalParametersChanged(params)
        #store if asked
        if withStore:
            return self.ExecuteStoreToEeprom()
        return True

    def DoWrite(self, withStore):
        s = self.tr('Write')
        if withStore: s += ' ' + self.tr('with Store')
        self.TextOutW( "\n\n" + s + '... ' + self.tr('please wait') )
        self.EnableTextOut_()
        ok = self.ExecuteWrite(withStore)
        self.DisableTextOut_()
        if not ok:
            self.TextOut( "\n" + self.RED(s + '... ' + self.tr('FAILED!')) )
            return
        self.TextOut( "\n" + self.GREEN(s + '... ' + self.tr('DONE!')) )
        
        
    def ExecuteSimpleCmd(self,cmdByte,dataByte=b''):
        self.TextOut_( "\n" + cmdByte.decode('utf-8') + '... ' )
        if dataByte == b'':
            res = self.SendCmd(cmdByte)
        else:
            res = self.SendCmdwData(cmdByte,dataByte)
        if res[-1:] != b'o': return False
        self.TextOut_( self.tr('ok') )
        return True
        
        
    def ExecuteStoreToEeprom(self):
        self.SetExtendedTimoutFirst(1.0) #StoreToEeprom can take a while! so extend timeout
        return self.ExecuteSimpleCmd(b'xs')

    def ExecuteRetrieveFromEeprom(self):
        if not self.ExecuteSimpleCmd(b'xr'): return False
        #wait before read
        self.DelaySec(1.5)
        return self.ExecuteRead()

    def ExecuteDefault(self):
        if not self.ExecuteSimpleCmd(b'xd'): return False
        #wait before read
        self.DelaySec(1.5)
        return self.ExecuteRead()

        
    def ExecuteLevelGimbal(self):
        return self.ExecuteSimpleCmd(b'xl')
        
    def DoLevelGimbal(self):
        s = self.tr('Level Gimbal')
        self.TextOutW( "\n\n" + s + '... ' + self.tr('please wait') )
        self.EnableTextOut_()
        ok = self.ExecuteLevelGimbal()
        self.DisableTextOut_()
        if not ok:
            self.TextOut( "\n" + s + '... ' + self.tr('FAILED!') )
            return
        self.TextOut( "\n" + s + '... ' + self.tr('DONE!') )
        

    def ExecuteGetCurrentEncoderPositions(self):
        if not self.ExecuteSimpleCmd(b'xp'): return False
        return self.ExecuteRead()

    def DoGetCurrentEncoderPositions(self):
        s = self.tr('Get Current Encoder Positions')
        self.TextOutW( "\n\n" + s + '... ' + self.tr('please wait') )
        self.EnableTextOut_()
        ok = self.ExecuteGetCurrentEncoderPositions()
        self.DisableTextOut_()
        if not ok:
            self.TextOut( "\n" + s + '... ' + self.tr('FAILED!') )
            return
        self.TextOut( "\n" + s + '... ' + self.tr('DONE!') )
        
        
    def ExecuteCalibrateRcTrim(self):
        if not self.ExecuteSimpleCmd(b'CR'): return False
        #wait before read
        self.DelaySec(1.5)
        return self.ExecuteRead()
        
    def DoCalibrateRcTrim(self):
        s = self.tr('Calibrate Rc Trim')
        self.TextOutW( "\n\n" + s + '... ' + self.tr('please wait') )
        self.EnableTextOut_()
        ok = self.ExecuteCalibrateRcTrim()
        self.DisableTextOut_()
        if not ok:
            self.TextOut( "\n" + s + '... ' + self.tr('FAILED!') )
            return
        self.TextOut( "\n" + s + '... ' + self.tr('DONE!') )
        

    def ExecuteRestartController(self):
        #handle case that Data Display is running
        '''
        my $DataDisplay_WasRunning=  $DataDisplay_IsRunning;
        if( $DataDisplay_IsRunning>0 ){
            DataDisplayHalt();
            _delay_ms(1000);
        }
        '''
        #now execute the "normal" execution route
        if not self.ExecuteSimpleCmd(b'xx'): return False
        #we will also issue a restart, so wait
        self.DelaySec(1.5)
        if not self.ExecuteRead(): return False
        #handle case that Data Display is running
        '''
        if( $DataDisplay_WasRunning>0 ){
            _delay_ms(1000);
            DataDisplayRun();
        }
        '''
        return True
      
    def DoRestartController(self):
        s = self.tr('Restart Controller')
        self.TextOutW( "\n\n" + s + '... ' + self.tr('please wait') )
        self.EnableTextOut_()
        ok = self.ExecuteRestartController()
        self.DisableTextOut_()
        if not ok:
            self.TextOut( "\n" + s + '... ' + self.tr('FAILED!') )
            return
        self.TextOut( "\n" + s + '... ' + self.tr('DONE!') )

        
    def ExecuteSetUsageOfAllMotors(self,usage):
        # ensure that usage is in range
        if usage < 0: return False
        if usage > 3: return False
        self.signalParamValueByNameChanged( 'Pitch Motor Usage', usage )   #THIS IS A BUG, right ?????
        self.signalParamValueByNameChanged( 'Roll Motor Usage', usage )
        self.signalParamValueByNameChanged( 'Yaw Motor Usage', usage )
        return self.ExecuteWrite(False)
        
    def DoSetUsageOfAllMotors(self,usage):
        if usage==3: s = self.tr('Disable all motors')
        elif usage==2: s = self.tr('Set all motors to startup pos')
        elif usage==1: s = self.tr('Set all motors to level')
        else: 
            s = self.tr('Enable all motors')
            usage = 0 #this ensures that usage is in range [0..3]
        self.TextOutW('\n\n' + s + '... ' + self.tr('please wait'))
        self.EnableTextOut_()
        ok = self.ExecuteSetUsageOfAllMotors(usage)
        self.DisableTextOut_()
        if not ok:
            self.TextOut( "\n" + s + '... ' + self.tr('FAILED!') )
            return
        self.TextOut( "\n" + s + '... ' + self.tr('DONE!') )
        
        
    def ExecuteChangeBoardName(self,boardName):
        boardName = boardName[:16]
        while len(boardName) < 16: boardName += ' '
        data = boardName.encode('utf-8')
        self.SetExtendedTimoutFirst(1.0)  #storing to Eeprom can take a while! so extend timeout
        return self.ExecuteSimpleCmd(b'xn',data)
        return True
        
    def DoChangeBoardName(self,boardName):
        #  DataDisplayHalt();
        s = self.tr('Change Board Name')
        self.TextOutW('\n\n' + s + '... ' + self.tr('please wait'))
        self.EnableTextOut_()
        ok = self.ExecuteChangeBoardName(boardName)
        self.DisableTextOut_()
        if not ok:
            self.TextOut( "\n" + s + '... ' + self.tr('FAILED!') )
            return
        #inform the qml    
        self.signalParamStrByNameChanged('Name', boardName)
        self.TextOut( "\n" + s + '... ' + self.tr('DONE!') )
        
        
    def ExecuteChangeUartBaudrate(self,baudrate):
        #0: 9600, 1: 19200, 2: 38400, 3: 57600, 4: 115200
        if baudrate < 0: return False
        if baudrate > 4: return False
        data = self.Int16ToBytes(baudrate)
        self.SetExtendedTimoutFirst(1.0)  #storing to Eeprom can take a while! so extend timeout
        return self.ExecuteSimpleCmd(b'xu',data)
        
    def DoChangeUartBaudrate(self,baudrate):
        #  DataDisplayHalt();
        s = self.tr('Change Uart Baudrate')
        self.TextOutW('\n\n' + s + '... ' + self.tr('please wait'))
        self.EnableTextOut_()
        ok = self.ExecuteChangeUartBaudrate(baudrate)
        self.DisableTextOut_()
        if not ok:
            self.TextOut( "\n" + s + '... ' + self.tr('FAILED!') )
            return
        self.TextOut( "\n" + s + '... ' + self.tr('DONE!') )
        if baudrate == 0: self.uartBaudrate = 9600
        elif baudrate == 1: self.uartBaudrate = 19200
        elif baudrate == 2: self.uartBaudrate = 38400
        elif baudrate == 3: self.uartBaudrate = 57600
        else: self.uartBaudrate = 115200
        #XXprint(self.uartBaudrate)

        
    def ExecuteChangeEncoderSupport(self,focEnable):
        if focEnable:  data = self.Int16ToBytes(1)
        else:          data = self.Int16ToBytes(0)
        self.SetExtendedTimoutFirst(1.0)  #storing to Eeprom can take a while! so extend timeout
        return self.ExecuteSimpleCmd(b'xf',data)
        return True
        
    def DoChangeEncoderSupport(self):
        #  DataDisplayHalt();
        s = self.tr('Change Encoder Support')
        self.TextOutW('\n\n' + s + '... ' + self.tr('please wait'))
        self.EnableTextOut_()
        ok = self.ExecuteChangeEncoderSupport(not self.isFoc)
        self.DisableTextOut_()
        if not ok:
            self.TextOut( "\n" + s + '... ' + self.tr('FAILED!') ) # should never happen
            return
        #restart controller    
        self.TextOut( "\n" + self.tr('restart controller') + '...')
        ok = self.ExecuteSimpleCmd(b'xx')
        if not ok:
            self.TextOut( "\n" + self.RED(s + '... ' + self.tr('ABORTED!'))+'\n'+self.tr('Restarting controller failed.') ) # should never happen
            return
        #disconnect and wait and reconnect
        self.TextOut( "\n" + self.tr('disconnect') + '...')
        self.DisconnectFromBoard()
        self.TextOut( "\n" + self.tr('waiting a moment before reconnecting') + '...')
        self.DelaySec(1.5)
        self.TextOut( "\n" + self.GREEN(s + '... ' + self.tr('DONE!')) )
        
        # the connect also informs the qml
        self.ConnectToBoard()

        
    def ExecuteEraseEeprom(self):
        return self.ExecuteSimpleCmd(b'xc')
        #a restart will also be issued, so no need to do a read here
        
    def DoEraseEeprom(self):
        s = self.tr('Erase Eeprom')
        self.TextOutW('\n\n' + s + '... ' + self.tr('please wait'))
        self.EnableTextOut_()
        ok = self.ExecuteEraseEeprom()
        self.DisableTextOut_()
        if not ok:
            self.TextOut( "\n" + s + '... ' + self.tr('FAILED!') )
            return
        self.TextOut( "\n" + s + '... ' + self.tr('DONE!') )
        #restart controller    
        self.TextOut( "\n" + self.tr('restarting controller') + '...')
        ok = self.ExecuteSimpleCmd(b'xx')
        if not ok:
            self.TextOut( "\n" + self.RED(s + '... ' + self.tr('ABORTED!'))+'\n'+self.tr('Restarting controller failed.') ) # should never happen
            return
        #disconnect and wait and reconnect
        self.TextOut( "\n" + self.tr('disconnect') + '...')
        self.DisconnectFromBoard()
        self.TextOut( "\n" + self.tr('waiting a moment before reconnecting') + '...')
        self.DelaySec(1.5)
        self.TextOut( "\n" + self.GREEN(s + '... ' + self.tr('DONE!')) )
        # the connect also informs the qml
        self.ConnectToBoard()

        
        
        
        
        
        
        
        
        
    def ReadPortRcCmd(self,_len=-1):
        print('ReadPortRceCmd()', _len) #XX
        result = b''
        (tmoFirst,tmo) = self.GetTimeoutsForReading()
        tStart = self.getTime()
        self.readPortIsRunning = True #required to prevent timer to tick #a QThread based solution would be MUCH better
        while True:
            QGuiApplication.processEvents() #a QThread based solution would be MUCH better
            curTmo = tmo
            if len(result) == 0: curTmo = tmoFirst
            t = self.getTime()
            if t-tStart > curTmo: self.readPortIsRunning = False; return b'T' #timeout!
            if self.serialBytesAvailable():
                b = self.serialReadOneByte()    
                tStart = t #restart tmo
                result += b
                if( len(result) >= _len ): break
        #check CRC (uses MAVLINK's x25 checksum)
        #crc = result[-3:-1]
        #print(crc)
        crc2 = self.DoCrc(result)
        #print(crc2)
        if crc2 != 0: self.readPortIsRunning = False; return b'c'
        #print(result)
        self.readPortIsRunning = False #a QThread based solution would be MUCH better
        return result

    def SendRcCmd(self,cmdByte,dataByte,_len=-1):
        msgByte = cRCCMDINSTX + self.Int8ToBytes(len(dataByte)) + cmdByte
        msgByte += dataByte
        msgByte += b'\x33\x34' #crc check is not activated, hence dummy crc
        print(cmdByte)           
        self.WritePort(cmdByte)
        if _len < 0: _len = RcCmdLength(cmd) #we need to determine response length
        return ReadPortRcCmd(_len)
        
    # get the length of the response to a RC serial command
    def RcCmdLength(self,cmdByte):
        if cmdByte == cRC_CMD_GETVERSION: return cRC_CMD_GETVERSION_RESPONSE_LEN
        if cmdByte == cRC_CMD_GETVERSIONSTR: return cRC_CMD_GETVERSIONSTR_RESPONSE_LEN
        if cmdByte == cRC_CMD_GETPARAMETER: return cRC_CMD_GETPARAMETER_RESPONSE_LEN
        if cmdByte == cRC_CMD_GETDATA: return cRC_CMD_GETDATA_RESPONSE_LEN
        if cmdByte == cRC_CMD_GETDATAFIELDS: return -1 #response length depends on cmd!!
        return cRC_CMD_ACK_LEN
        
        
        
'''
####################################################    
# interface for QML
####################################################    
'''
   
class cPyQml(QObject):

    def __init__(self,_app,_main):
        super().__init__()
        self.app = _app
        self.main = _main

    @pyqtSlot(str, result=QVariant)
    def loadFile(self, url):
        if url[0:5] != 'file:':
            return { 'ok': False, 'content': '' }
        file = url[8:]
        try: 
            F = open( file, 'r')
        except IOError: 
            return { 'ok': False, 'content': '' }
        else:
            s = F.read()
            F.close()
        return { 'ok': True, 'content': s }
        
    @pyqtSlot(str, str, result=QVariant)
    def saveFile(self, url, content):
        if url[0:5] != 'file:':
            return { 'ok': False }
        file = url[8:]
        try: 
            F = open( file, 'w')
        except IOError: 
            return { 'ok': False }
        else:
            F.write(content)
            F.close()
        return { 'ok': True }
        
    @pyqtSlot(result=QVariant)
    def getPrimaryScreenWidthHeight(self):
        g = self.app.primaryScreen().geometry()
        return { 'width': g.width(), 'height': g.height() }

    @pyqtSlot(result=int)
    def getUartBaudrate(self):
        return self.main.uartBaudrate
        


if __name__ == '__main__':

    #sys_argv = sys.argv
    #sys_argv += ['--style', 'material']
    #sys_argv += ['--style', 'universal']
    #app = QGuiApplication(sys_argv)
    app = QGuiApplication([])

    #print( app.primaryScreen().geometry() ) #size() )
    
    settings = QSettings(IniFileStr, QSettings.IniFormat)
    
    language = 'en'
    p = settings.value('SYSTEM/Language')
    if p: language = p
    if( language != 'en' ):
        translator = QTranslator()
        if translator.load('resources/i18n/'+ApplicationStr+'_'+language):
            print('language '+language+' installed')
            app.installTranslator(translator)
    
    
    main = cMain(settings)
    context = main.rootContext()
    pyQml = cPyQml(app, main)
    context.setContextProperty("pyQml", pyQml)    
    main.show()
    sysexit = app.exec_()
    
    settings.setValue('SYSTEM/Language', language)
    settings.sync()
    
    sys.exit(sysexit)