'''
/** Original work Copyright 2024 damiano IU3QGD
 * https://to-be-defined
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 *     Unless required by applicable law or agreed to in writing, software
 *     distributed under the License is distributed on an "AS IS" BASIS,
 *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *     See the License for the specific language governing permissions and
 *     limitations under the License.
 
 * Addition to the licence: NO company with more than 50employees can use this SW and you are NOT entitled to store this software 
 * on ANY storage that in some way attempts to monetize it (eg: github)
 * corporations and banks are too rich and too powerful, they MUST be split and ALL the money given back to the state
 
 

Other parts of the system should send messages here to be sent
There has to be a class that manage the queue of incoming commands
this is because the GUI cannot do IO (will be come sluggish) and the serial channel is NON sharable by default

'''


from __future__ import annotations

import ipaddress
import pathlib
import queue
import serial.tools.list_ports  # type: ignore
from threading import Thread
import time 
from tkinter import StringVar
from tkinter.filedialog import askopenfilename
from tkinter.ttk import Widget
import traceback
from typing import cast
import typing

from app_config import AppConfig, ConfigSavable
from glob_class import Ipv4_address
from glob_eeprom import Uvk5Firmware, UVK5_fw_block
import glob_eeprom
from glob_fun import getApplicationResourcesPath, getResourcesPhotoimage
from glob_gui import JtkWinToplevel, TV_Entry, LogPanel, JtkCombo, \
    GUI_hide_show_window, JtkPanelPackTop, JtkPanelGrid, \
    JtkLabel, JtkPanelTabbed, JtkCheckbox, ImageLabel, JtkButtonText, \
    JtkPanelPackLeft
from glob_ippqs import  IppChunk, \
    Qsk5_res_Counters, Qsk5_res_Spectrum, Qsk5_res_Loudest, Qsk5_res_PollDtmf, \
    Qsk5_res_RadioPrintf, Qsk5_res_RadioIdentity, Qsk5_res_RadioConfiguration, \
    Qsk5_res_Generic, Qsk5_res_Eeprom, Qsk5_req_read_eeprom_quansheng, \
    Qsk5_res_pollipp16, Qsk5_res_PollMessages, Qsk5_res_RadioStatus, \
    Qsk5_req_radio_poll, Qsk5_res_screen_bmap
import glob_ippqs
from ipp_parser import Ippparser
from qcounters_gui import Qcounters_gui
import qpystat
import tkinter.ttk as ttk
from uvk5_lib import RadioReceiveRes, \
    QuaFlash, Flash_V2, \
    UVk5_BLOADER_BEACONv2_hex, UVk5_BLOADER_BEACONv5_hex, Flash_V5, \
    BloaderBeaconParser, QswioSerial, QswioStreamSocket, CONN_SERIAL_ID, \
    CONN_TCPV4_ID, Qswio, QSTCP_PORT, Flash_128K_V2


CONN_CHOICES=[CONN_SERIAL_ID,CONN_TCPV4_ID]

FWTYPE_NULL='No FW Upload'
FWTYPE_PREPPER='PrepperRadio FW'
FWTYPE_QUANSHENG='Quansheng FW'
FWTYPE_USER='Other FW'

FW_TYPES = (FWTYPE_NULL,FWTYPE_PREPPER,FWTYPE_QUANSHENG,FWTYPE_USER)



# ====================================================================
class QconnectThread (Thread,ConfigSavable,GUI_hide_show_window):

    # ----------------------------------------------------------------
    def __init__(self, stat : qpystat.Qpystat):
        Thread.__init__(self,name="QConnect THREAD",daemon=True)
        
        self.stat = stat
        
        self._oqueue : queue.Queue = queue.Queue()
        self.uvk5fw = Uvk5Firmware(stat)
       
        self.stat.appconfig.addMyselfToSavables(self)

        self._toplevel = JtkWinToplevel("QConnect Window")
        
        a_panel=JtkPanelPackTop(self._toplevel, padding=2 )     
    
        a_panel.addItem(self._newTopTabbedPanel(a_panel))
        
        a_panel.addItem(self._newStatusPanel(a_panel))

        a_panel.addItem(self._newBottomTabbedPanel(a_panel),fill='both',expand=True)

        a_panel.pack(fill='both',expand=True)

        self.GUI_hide_window() 

        # serial can be initialized and used when needed
        self._qswio_serial = QswioSerial(self._qconn_log)
        
        # make sure that there is always a live socket
        self._qswio_socket = QswioStreamSocket(self._qconn_log)

        # the current connection is initialized to None
        self._qswio_current : Qswio = Qswio('None',self._qconn_log)
        
        self.cur_radio_fw_version=''
        self.cur_radio_cpu_id_hex=''
        
        self._println("QConnect init complete")

    # -------------------------------------------------------------------
    # prints something on this window log
    def _println(self, msg):
        self._qconn_log.println(msg)    
        
    # --------------------------------------------------------------------
    # show a window that has been hidden using the withdraw method
    def GUI_show_window(self):
        self._toplevel.deiconify()    

    # --------------------------------------------------------------------
    # hide completely a window
    def GUI_hide_window(self):
        self._toplevel.withdraw()    

    # -----------------------------------------------------------
    # Mostly a reminder that this method is available everywhere    
    def _runOnGuiIdle(self, func, *args ):
        self._toplevel.after_idle(func, *args)

    # -----------------------------------------------------------
    def _newTopTabbedPanel(self, parent : Widget ):

        a_panel = JtkPanelTabbed(parent)

        a_panel.addItem(self._newSelectConnectionPanel(a_panel),"Connection")

        a_panel.addItem(self._newRssiPanel(a_panel),"RSSI")
        
        return a_panel

    # -----------------------------------------------------------
    def _newBottomTabbedPanel(self, parent : Widget ):

        a_panel = JtkPanelTabbed(parent)

        self._qconn_log = LogPanel(a_panel,"Connection Log")

        a_panel.addItem(self._qconn_log,"Qconnect Log")

        self._qcounters = Qcounters_gui(self.stat, a_panel)  
        
        a_panel.addItem(self._qcounters.getWorkPanel(),"Q COunters")
    
        return a_panel
    
    
    # -------------------------------------------------------------------------------
    # make a new frame and put an input line in it
    def _newSelectConnectionPanel(self, parent):

        a_panel=JtkPanelGrid(parent) # ,padding='3')

        # I have a combo with the list of supposedly detected ports
        a_panel.addItem(JtkLabel(a_panel,"Select connection: "))
        
        self._select_conntype_combo = JtkCombo(a_panel, values=CONN_CHOICES)
        self._select_conntype_combo.setWriteCallback(self._on_connection_changed)
        
        a_panel.addItem(self._select_conntype_combo)
        
        # -------------------- the image should span various rows
        self._current_fw_version = '0.00.00'
        self._radio_image = RadioImage(a_panel,self._current_fw_version)
        
        a_panel.addItem(self._radio_image,rowspan=6)
        
        a_panel.nextRow() # -------------------------------
        
        a_panel.addItem(JtkLabel(a_panel,"Active connection: "))
        self._currentConntype = StringVar(a_panel,CONN_SERIAL_ID)
        
        entry=TV_Entry(a_panel,self._currentConntype)
        entry.setEnabled('disabled')
        a_panel.addItem(entry)

        a_panel.nextRow() # -------------------------------
        
        a_panel.addItem(JtkLabel(a_panel,"WiRadio IP v4 "))
        a_panel.addItem(self._new_ipv4_input_panel(a_panel))
        a_panel.nextRow() # -------------------------------

        # this variable will store whatever the user has typed
        self._serial_name_var = StringVar(a_panel)

        # I have a combo with the list of supposedly detected ports
        a_panel.addItem(JtkLabel(a_panel,"Select Serial: "))
        
        self._serial_name_combo = JtkCombo(a_panel)
        self._serial_name_combo.setWriteCallback(self._on_serial_combo_change)

        a_panel.addItem(self._serial_name_combo)

        a_panel.nextRow() # -------------------------------
        
        a_panel.addItem(JtkLabel(a_panel," Selected serial:"))
        a_panel.addItem(TV_Entry(a_panel, self._serial_name_var, width=20))
        
        a_panel.nextRow()
        
        a_panel.addItem(JtkLabel(a_panel,"Automagic Firmware"))
        self._want_fw_type = JtkCombo(a_panel,values=FW_TYPES)
        a_panel.addItem(self._want_fw_type)

        a_panel.nextRow()

        a_panel.addItem(JtkLabel(a_panel,"Pause connection"))
        self._pauseConnection = JtkCheckbox(a_panel, "Pause")
        a_panel.addItem(self._pauseConnection)

        return a_panel

    # ----------------------------------------------------------------------------------
    def _new_ipv4_input_panel(self, a_parent ) -> ttk.Widget:
        a_panel = JtkPanelPackLeft(a_parent)

        # this variable will store whatever the user has typed
        self._qsipv4_address = Ipv4_address(a_panel)
        
        a_panel.addItem(TV_Entry(a_panel, self._qsipv4_address._ipv4_address, width=12))
        a_panel.addItem(JtkButtonText(a_panel,'Apply',command=self._apply_ipv4_address_press))
        
        return a_panel

    # ----------------------------------------------------------------------------------
    def _apply_ipv4_address_press(self):
        self._qsipv4_address.on_apply_button_pressed()
    
    # ----------------------------------------------------------------------------------
    # MUST be called from swing thread to show current radio
    # it uses a firmare version 
    def _swing_update_radio_image(self):
        self._radio_image.update_image(self._current_fw_version)    

    # ----------------------------------------------------------------------------------
    # called when the user changes the combo
    # it is ALSO called when changing the combo selection programmatically !
    # it should change the text value in the input box AND disconnecte the current port
    # 
    def _on_connection_changed(self, _v1, _v2, _v3):
        # cannot do much, since I am on GUI thread
        self._println(self._select_conntype_combo.getStringVarValue())

    # -------------------------------------------------------------------------------
    # get an ipv4 address or none if not possible
    def get_wiradio_ipv4(self) -> ipaddress.IPv4Address:
        return self._qsipv4_address.get_value_ipv4()


    # -------------------------------------------------------------------------------
    # make a new frame and put an input line in it
    def _newStatusPanel(self, parent):

        a_panel=JtkPanelPackTop(parent, padding=3, borderwidth=1, relief='solid')
        
        self.rxFrequency = JtkLabel(a_panel, font=('calibri',14))
        a_panel.addItem(self.rxFrequency)
        
        self.radioStatus = JtkLabel(a_panel, font=('calibri',14))
        a_panel.addItem(self.radioStatus)
        
        return a_panel

    # ----------------------------------------------------------------------------------
    # NOTE that there is style definition in _adjustTtkStyle
    def _newRssiPanel(self, parent):

        rssi_panel = JtkPanelGrid( parent, borderwidth=1, relief='solid', padding=3) 

        rssi_panel.columnconfigure(2, weight = 1)   # since the other is zero, this will get all the extra space

        w_font = ('Consolas',10)
        w_font_bold = ('Consolas',10,'bold')

        #RSSI
        rssi_panel.addItem(JtkLabel(rssi_panel, "RSSI dBm", font=w_font))
        self._rssi_vLabel = JtkLabel(rssi_panel, font=w_font_bold)
        rssi_panel.addItem(self._rssi_vLabel,sticky='e')
        self._rssiBar = ttk.Progressbar(rssi_panel, orient='horizontal', mode='determinate', maximum=0x1FF/2)
        rssi_panel.addItem(self._rssiBar)

        rssi_panel.nextRow()

        #NOISE
        rssi_panel.addItem(JtkLabel(rssi_panel, "Ex-noise", font=w_font))
        self._noise_Label = JtkLabel(rssi_panel, font=w_font_bold)
        rssi_panel.addItem(self._noise_Label,sticky='e')
        self.noiseBar = ttk.Progressbar(rssi_panel, orient='horizontal', mode='determinate', maximum=0x7F)
        rssi_panel.addItem(self.noiseBar)

        rssi_panel.nextRow()

        #GLITCH
        rssi_panel.addItem(JtkLabel(rssi_panel, "Glitch indicator", font=w_font))
        self._glitch_Label = JtkLabel(rssi_panel, font=w_font_bold)
        rssi_panel.addItem(self._glitch_Label,sticky='e')
        self.glitchBar = ttk.Progressbar(rssi_panel, orient='horizontal', mode='determinate', maximum=0xFF)
        rssi_panel.addItem(self.glitchBar)

        rssi_panel.nextRow()
        
        #BATTERY
        rssi_panel.addItem(JtkLabel(rssi_panel, "Battery Voltage", font=w_font))
        self._battV_Label = JtkLabel(rssi_panel, font=w_font_bold)
        rssi_panel.addItem(self._battV_Label,sticky='e')
        self.battBar = ttk.Progressbar(rssi_panel, orient='horizontal', mode='determinate', maximum=1000)
        rssi_panel.addItem(self.battBar)

        rssi_panel.nextRow()

        #CPU Temperature
        rssi_panel.addItem(JtkLabel(rssi_panel, "CPU Temperature", font=w_font ))
        self._tcpu_Label = JtkLabel(rssi_panel, font=w_font_bold)
        rssi_panel.addItem(self._tcpu_Label,sticky='e')
        self.tcpuBar = ttk.Progressbar(rssi_panel, orient='horizontal', mode='determinate', maximum=4000)
        rssi_panel.addItem(self.tcpuBar)

        return rssi_panel 
    
    # ---------------------------------------------------------------------------------
    # this updates only the chunk with radio status
    def _updateRadioStatus(self, chunk : Qsk5_res_RadioStatus):
        
        rssi = chunk.rx_rssi / 2 - 160
        
        self._rssi_vLabel.config(text="{:7.2f}".format(rssi))
        self._rssiBar['value'] = rssi+160
        
        self._noise_Label.config(text="{:d}".format(chunk.rx_noise))
        self.noiseBar['value'] = chunk.rx_noise
                
        self._glitch_Label.config(text="{:d}".format(chunk.rx_glitch))
        self.glitchBar['value'] = chunk.rx_glitch

        self._battV_Label.config(text="{:.2f}".format(chunk.batteryV))
        self.battBar['value'] = chunk.batteryV_2dec

        self._tcpu_Label.config(text="{:.2f}".format(chunk.tc_cpu))
        self.tcpuBar['value'] = chunk.tc_cpu_2dec

        self.rxFrequency.config(text="RX Frequency HZ  {}".format(chunk.rxfreq))
        self.radioStatus.config(text="Radio Status bits  {:#04x}".format(chunk.radio_status))

    
    
    
    

    # -------------------------------------------------------------------
    # implement the config savable
    def appImportConfig(self, cfg : AppConfig ):
        adict = cfg.getDictValue("qconnect_cfg", {})
        
        try:
            self._println("geometry "+self._toplevel.geometry())

            self._serial_name_var.set(adict['serial_port'])    
            self._toplevel.setGeometry(adict['gui_cfg'])
            self._qsipv4_address.set_value_string(adict['qstcpv4_address'])
            self._select_conntype_combo.set(adict['connection_type'])
            self._want_fw_type.setStringVarValue(adict['automagic_upload_type'])

        except Exception as _exc :
            pass            

    # -------------------------------------------------------------------
    # implement the config savable
    def appSaveConfig(self, cfg : AppConfig ):
        # get the current dictionary bound to qconnect
        adict = cfg.getDictValue("qconnect_cfg", {})

        adict['gui_cfg'] = self._toplevel.getGeometry()
        adict['serial_port'] = self._serial_name_var.get()
        adict['qstcpv4_address'] = self._qsipv4_address.get_value_string()
        adict['connection_type'] = self._select_conntype_combo.getStringVarValue()        
        adict['automagic_upload_type'] = self._want_fw_type.getStringVarValue() 

        cfg.setDictValue("qconnect_cfg", adict )            
    
    # ------------------------------------------------------
    # use this method to queue a command to be executed
    # it has to be a child of a command
    def queuePut (self,line : glob_ippqs.Qsk5_command):
        self._oqueue.put(line)

    # --------------------------------------------------------
    # this is here since python cannot handle import in a kind of circular way
    # meaning, cannot allocate an instance and queue put as you please
    def SHIT_python_circular_import(self):
        cmd = Qsk5_req_read_eeprom_quansheng(0,glob_eeprom.GlobEeprom.ee_blocks_max)
        self.queuePut(cmd)



    # ----------------------------------------------------------------------------------
    # called when the user changes the combo
    # it is ALSO called when changing the combo selection programmatically !
    # it should change the text value in the input box AND disconnecte the current port
    def _on_serial_combo_change(self, _v1, _v2, _v3):
        
        sel_port = self._serial_name_combo.getStringVarValue()
        
        self._println("combo selected "+sel_port)
        
        self._serial_name_var.set(sel_port)

        # if there was anything ocnnected, disconnect
        self._qswio_serial.close_connection()

    # --------------------------------------------------------------
    # attempt to adjust the combo with thelist of serial ports detected
    # NOTE that an emptylist is a VALID setting, meaning, non ports detected
    def _swing_update_ports_combo(self, ports_list : typing.List[str]):
        
        sorted_list = sorted(ports_list)
        current_list = self._serial_name_combo['values']
        
        if sorted_list == current_list:
            return 
         
        self._serial_name_combo['values'] = sorted(ports_list)

        if not sorted_list:
            return 
        
        u_port_val = self._serial_name_var.get()
        
        # the isea is that if the user has NOT set a value AND there is NOTHING selected in the combo
        # this is because a user combo change WILL result in a change AND disconnect !
        if not u_port_val and self._serial_name_combo.current() < 0:
            self._serial_name_combo.current(0)
            
        #self._println('UFFA '+self._serial_name_combo.getStringVarValue())
            


    # --------------------------------------------------------------
    # of 3 strings: port name, human readable description and a hardware ID.
    # this is called when I wish to detect ports, once the ports are detected they will be push to the combo
    def _serial_ports_detect(self):
        port_list=serial.tools.list_ports.comports()

        ports_list = []

        for row  in port_list:
            a_port : typing.Tuple[str,str,str] = row
            self._println('port '+str(a_port[0])+' '+str(a_port[1]) )
            ports_list.append(a_port[0])

        self._toplevel.after_idle(self._swing_update_ports_combo, ports_list)


    
    # ------------------------------------------------------
    # attempt to open the currently selected serial port
    # this is NOT in swing thread, so, no real gui messing here
    # TRY to request a combo reconfigure    
    def _open_serial_port(self) -> bool:

        # request a port detect, in any case
        self._serial_ports_detect()

        # I use a combo with detected ports AND an input box, since, I REALLY wish to give the user an option to type what they wish
        portname=self._serial_name_var.get()
        
        if not portname:
            self._println("Qconnect: port name empty")
            return False
    
        self._println("Qconnect opening "+portname)

        return self._qswio_serial.open_connection(portname)
    
    # -------------------------------------------------------
    # this should assign the _qswio_current with the selected connection
    # and update the GUI
    def _align_current_connection(self):

        # this is the selected type
        s_type = self._select_conntype_combo.getStringVarValue()
    
        # by default I end up selecting a serial port
        if self._qswio_socket.isTypeEqual(s_type):
            self._qswio_current = self._qswio_socket
        else:
            self._qswio_current = self._qswio_serial
            
        self._currentConntype.set(self._qswio_current.conn_type)
             
    # ------------------------------------------------------
    def _open_current_connection(self) -> bool:

        if self._qswio_current.isTypeEqual(CONN_SERIAL_ID):
            return self._open_serial_port()
        
        if self._qswio_current.isTypeEqual(CONN_TCPV4_ID):
            c_address=self._qsipv4_address.get_value_string()
            return self._qswio_current.open_connection((c_address,QSTCP_PORT))
    
        self._println('Invalid conn_type='+str(self._qswio_current.conn_type))

        return False
    
    
    # ------------------------------------------------------
    # wrapped open, to avoid lots of garbage on open error
    # @return True if all goes well
    def _open_qswio_connection(self) -> bool:
        try:
            # just make sure that the current connection is closed
            self._qswio_current.close_connection()
            
            self._align_current_connection()
            
            return self._open_current_connection()
        except Exception as _err:
            self._println("Port open FAIL")
            return False

    # ------------------------------------------------------
    def _oqueue_flush(self):
        while not self._oqueue.empty():
            self._oqueue.get_nowait()
    
    # ------------------------------------------------------
    # Attempt to open the device and do some polling
    def _qconn_open_and_poll(self):
        
        try:
            
            if not self._open_qswio_connection():
                return
            
            self._println("  Connection is OPEN")
            self._oqueue_flush()

            while self._qconn_loop_poll_one():
                time.sleep(0.05)
        
        except Exception as _err:
            self._println("exc "+str(_err))
            self._println(traceback.format_exc())
    

    
    # --------------------------------------------------------------
    # Called by the Thread, do NOT call it, use start()
    # I wish this to stop on first error, so I can investigate
    def run(self):
        self._println ("START "+self.name)

        try_count=0
        
        while True:
            self._println ("----------> "+str(try_count))
            self._qconn_open_and_poll()
            self._println ("poll errors: wait 5s")
            time.sleep(4)
            try_count+=1

        self._println ("END "+self.name)


    # --------------------------------------------------------------
    # example: return a byte array of a string padded to the given len
    def _pad_String(self, astring : str , pad_to : int ):
        ascii_bytes = bytes(astring,'ascii')
        
        return ascii_bytes + b'0x00' * (40 - len(ascii_bytes))
        
    
    
    # ------------------------------------------------------
    # Attempt to poll the queue of commands to send to quansheng
    # NOTE that now the command is also parsing the content !
    # stay here pulling commands until there are no more
    def _qconn_poll_queue(self):
        
        while not self._oqueue.empty():
            
            # in theory there should be something, the wait time is just to be sure to never get stuck
            command : glob_ippqs.Qsk5_command = self._oqueue.get(True, 0.3)
        
            self._println("_qconn_poll_queue: have command "+str(command))
        
            command.send_request_new(self.stat, self._qswio_current)

    # ------------------------------------------------------
    '''
       uint16_t w_printf:1;    // bit0 poll for printf
       uint16_t w_ipp:1;       // bit1 poll for IPP packets
       uint16_t w_dtmfipp:1;   // bit2 poll for DTMF IPP packets
       uint16_t w_counters:1;  // bit3 poll for counters
       uint16_t w_dtmfrxtx:1;  // bit4 poll for DTMF
       uint16_t w_qsmsg:1;     // bit5 poll for Quansheng messages
       uint16_t w_scanloud:1;  // bit6 poll for scanloud result
       uint16_t padding:9;     // MSB on LE CPU
    '''
    def _qconn_poll_periodic(self):

        basic_req = 0b11110111
        
        # this is for fb refresh
        basic_req = basic_req | 0b100000000
            
        # create a poll request
        a_req = Qsk5_req_radio_poll(basic_req) 

        # and send it off
        a_req.send_request(self._qswio_current)
        
        r_res : RadioReceiveRes = RadioReceiveRes(self._qswio_current)
        
        if r_res.isIppRes():
            parser = Ippparser(r_res.pk_data)
            self.parseReceivedChunks(parser)
        elif r_res.isQuanshengRes():
            self._parseReceivedQuanshengRes(r_res)

            
    # ------------------------------------------------------
    def _parseReceivedQuanshengRes(self, r_res : RadioReceiveRes):

        self._println("QUANSHENG protocol")

        if r_res.error_message:
            self._println("  "+r_res.error_message)
            return
    
        bloader = BloaderBeaconParser(r_res.pk_data)
            
        self._println("  bootloader 0x%04x 0x%04x" % ( bloader.cmd_code, bloader.cmd_data_len) )

        if bloader.cmd_code == 0:
            return
        
        self._println("  fw_vers: "+bloader.fw_version )
        self._println("  chip_id: "+bloader.chip_id.hex() )

        self._current_fw_version = bloader.fw_version
        
        self._runOnGuiIdle(self._swing_update_radio_image)
        
        wwant_fw_type=self._want_fw_type.getStringVarValue()

        if wwant_fw_type is None or wwant_fw_type == FWTYPE_NULL:
            return 

        # attempt to load the correct filename for the wanted FW
        if not self._fw_load_filename(wwant_fw_type, bloader.fw_version):
            return

        # show the Qconnect window, otherwise the sheeple will get scared
        self.GUI_show_window()

        # now, attempt to upload this firmware
        self._fw_upload_firmware()


    # --------------------------------------------------------------------------------------
    # look in a specific order for the given filename
    # return the first suitable match
    def _fw_search_filename(self, fw_fname : str) -> pathlib.Path:
        
        p_dir : pathlib.Path=self.stat.glob_mlang.getMlangTopDir()
        fw_fullname = p_dir / fw_fname
        if fw_fullname.exists():
            return fw_fullname
        
        p_dir = getApplicationResourcesPath()
        fw_fullname = p_dir / fw_fname
        if fw_fullname.exists():
            return fw_fullname
        
        return cast(pathlib.Path,None)
        
        
    def _fw_make_fullname(self, fw_type : str, fw_version : str) -> str:
        if fw_type == FWTYPE_QUANSHENG:
            return 'QS-Kradio-'+fw_version+'.bin'
        elif fw_type == FWTYPE_PREPPER:
            return 'Prepper-Kradio-'+fw_version+'.bin'
        else:
            return 'OtherFW-Kradio-'+fw_version+'.bin'
        
    # --------------------------------------------------------------------------------------
    # The actual writing HAS to be done using the polling thread
    # First I need to pick up the file
    # return True if it can load the filename
    def _fw_load_filename(self, fw_type : str, fw_version : str ) -> bool:

        fw_short = self._fw_make_fullname(fw_type, fw_version)

        fw_fullname = self._fw_search_filename(fw_short)
        
        if not fw_fullname:
            self._println("CANNOT find: "+str(fw_short))
            return False

        self._println("Loading: "+str(fw_fullname))

        if self.uvk5fw.load_from_filename(fw_fullname):        
            self._println("  Firmware size "+str(self.uvk5fw.getFirmwareSize()))
            return True

        self._println("_fw_load_filename: CANNOT load "+str(fw_fullname))
        return False

    # ------------------------------------------------------
    def _is_128k_firmware(self, fw_ident : str, fw_len : int ):
 
        if fw_len > 60*1024:
            return True
        
        return fw_ident == '7.02.02' or fw_ident == "7.00.07" or fw_ident == "7.03.01" 
    
    # ------------------------------------------------------
    # If I have a firmware to load, attempt to    
    def _fw_upload_firmware(self):

        fw_length = self.uvk5fw.getFirmwareSize()
        
        # bootloader will check the length, no need for me to check it here
        self._println("UPLOAD FIRMWARE length "+str(fw_length))

        qua_flash = QuaFlash(self._qswio_current)
        
        if not qua_flash.qua_FWR_wait_bootloader(10):
            self._println("UPLOAD FIRMWARE cannot wait bootloader, ABORT")
            return
        
        bl_code = qua_flash.bloader.cmd_code
        fw_version = qua_flash.bloader.fw_version
        
        if bl_code == UVk5_BLOADER_BEACONv2_hex:
            if self._is_128k_firmware(fw_version, fw_length):
                qua_flash = Flash_128K_V2(qua_flash)
            else:
                qua_flash = Flash_V2(qua_flash)
        elif bl_code == UVk5_BLOADER_BEACONv5_hex:
            qua_flash = Flash_V5(qua_flash)
        else:            
            self._println("NO BOOTLOADER MODE "+str(qua_flash.bloader))
            return
            
        self._println("BOOTLOADER MODE "+qua_flash.qua_FWR_py_version())  

        if not qua_flash.qua_FWR_TX_version():
            self._println("TX firmware version FAIL")
            return

        self._println("TX FW version OK")
        
        time.sleep(0.05)  # also attempt for apple mac    
        
        # note that the fw_length is actually checked
        last_write_block_address = (fw_length & 0xFFFF00) + 0x100 
        
        for row in self.uvk5fw._blocklist:
            ablock : UVK5_fw_block = row
            
            if not qua_flash.qua_FWR_write_block(ablock.w_address ,last_write_block_address ,ablock.b_data): 
                self._println(" WRITE FAIL, ABORT")
                break;

        self._println("FW upload END")


    # ------------------------------------------------------
    # use this method to queue a command to be executed
    def _qconn_loop_poll_one(self) -> bool:
        
        if self._pauseConnection.isChecked():
            time.sleep(0.5)
            # dump possibly queued packets
            self._oqueue_flush()
            return True 
        
        try:
            w_ctype=self._select_conntype_combo.getStringVarValue()
            
            if not self._qswio_current.isTypeEqual(w_ctype):
                self._println('Connection type changed, loop end')
                return False
            
            # try to poll the queue of commands
            self._qconn_poll_queue()
            
            # then do a polling for standard values
            self._qconn_poll_periodic()
            
        except TimeoutError:
            self._println("COMM timeout")
            return False
            
        except Exception as _err:
            self._println(traceback.format_exc())
            return False
            
        return True

    # -------------------------------------------------------------
    # @return true if it has parsed at least one chunk
    def parseReceivedChunks(self, parser : Ippparser ) -> bool:
        
        if not parser:
            return False

        have_chunks = False

        while parser.hasNextChunk():
            chunk=parser.getNextIppChunk()
            
            if chunk:
                self._parse_received_chunk(chunk)
                have_chunks=True
        
        return have_chunks
        
    # -------------------------------------------------------------
    def _parse_received_chunk(self, chunk : IppChunk ):
        
        if chunk.chunk_id == glob_ippqs.IPPU_pollprintf_resid:
            self._showRadioPrintf(cast(Qsk5_res_RadioPrintf,chunk))
        elif chunk.chunk_id == glob_ippqs.IPPU_pollradiostat_resid:
            self._runOnGuiIdle(self._updateRadioStatus,chunk)
        elif chunk.chunk_id == glob_ippqs.IPPU_pollqsmsg_resid:
            self.stat.qsmsg_gui.parseIncomingChunk(cast(Qsk5_res_PollMessages,chunk))
        elif chunk.chunk_id == glob_ippqs.IPPU_ident_resid:
            self._showRadioIdentity(cast(Qsk5_res_RadioIdentity,chunk))
        elif chunk.chunk_id == glob_ippqs.IPPU_radiocfg_resid:
            self._showRadioConfig(cast(Qsk5_res_RadioConfiguration,chunk))
        elif chunk.chunk_id == glob_ippqs.IPPU_generic_resid:
            self._showGenericResponse(cast(Qsk5_res_Generic,chunk))
        elif chunk.chunk_id == glob_ippqs.IPPU_polldtmf16_resid:
            self._showPollDtmfRes(cast(Qsk5_res_PollDtmf,chunk))
        elif chunk.chunk_id == glob_ippqs.IPPU_scan_loudest_resid:
            self._showScanLoudestRes(cast(Qsk5_res_Loudest,chunk))
        elif chunk.chunk_id == glob_ippqs.IPPU_res_counters16:
            self._showResCounters(cast(Qsk5_res_Counters,chunk))
        elif chunk.chunk_id == glob_ippqs.IPPU_res_spectrum16:
            self._showResSpectrum(cast(Qsk5_res_Spectrum,chunk))
        elif chunk.chunk_id == glob_ippqs.IPPU_res_eeprom:
            self._parse_eepromResFromRadio(cast(Qsk5_res_Eeprom,chunk))
        elif chunk.chunk_id == glob_ippqs.IPPU_res_pollipp16:
            self._parse_pollipp16(cast(Qsk5_res_pollipp16,chunk))
        elif chunk.chunk_id == glob_ippqs.IPPU_res_screen_bmap:
            self.stat.wiremote_gui.parse_screen_bmap(Qsk5_res_screen_bmap(chunk))
        else:
            self._println("POLL unsupported chunkid="+str(chunk.chunk_id))


    # ------------------------------------------------------------
    def _parse_pollipp16(self,res_ipp16 : Qsk5_res_pollipp16):
        self.stat.racommand_gui.parseIncomingIpp16(res_ipp16)
    
    # ------------------------------------------------------------
    def  _parse_eepromResFromRadio (self, chunk : Qsk5_res_Eeprom ):
        
        if chunk.isReadResponse():
            self.stat.globeeprom.eepromUpdateFromRadio(cast(Qsk5_res_Eeprom,chunk))
        elif chunk.isWriteResponse():
            w_Address=chunk.ee_rw_address
            self.stat.eeprom_gui.setLoadingProgress(w_Address/16)
    
    # -------------------------------------------------------------
    # show in the main window the res counters result
    def _showResCounters(self, chunk : Qsk5_res_Counters):
        self._qcounters.parse_chunk(chunk)
            
    # -------------------------------------------------------------
    # the result should be given to the spectrum_gui window
    def _showResSpectrum(self, chunk : Qsk5_res_Spectrum):
        self._println("\nhave spectrum_gui res F= "+str(chunk.start_freq_1x))
        self.stat.spectrum_gui.showSpectrum_res(chunk)

    # -------------------------------------------------------------
    # show in the main window the identify result
    def _showScanLoudestRes(self, chunk : Qsk5_res_Loudest):
        self.stat.spectrum_gui._println("Scan Loudest="+str(chunk.found_F_1x))
        self.stat.qscalibrate_gui.receive_scan_loud_res(chunk)
        
    # -------------------------------------------------------------
    def _showPollDtmfRes(self, chunk : Qsk5_res_PollDtmf):
        self.stat.qsdtmf_gui.dtmf_println(chunk.dtmf_string)
        
    # -------------------------------------------------------------
    def _showRadioPrintf(self, chunk : Qsk5_res_RadioPrintf):
        self._qconn_log.write(chunk.print_this)
        
    # -------------------------------------------------------------
    # show in the main window the identify result
    def _showRadioIdentity(self, chunk : Qsk5_res_RadioIdentity):

        self.cur_radio_fw_version = chunk.sw_version
        self.cur_radio_cpu_id_hex = chunk.getCpuid_hex()
        self.cur_radio_hw_model = chunk.hw_model
        
        self._println("HW model="+chunk.hw_model)
        self._println("CPU id="+self.cur_radio_cpu_id_hex)
        self._println("SW version="+self.cur_radio_fw_version)
        self._println("HW model="+self.cur_radio_hw_model)
        
        self.stat.eeprom_gui.checkVersionCompatible(chunk.sw_version)
        self.stat.eeprom_gui.update_CPU_id(self.cur_radio_cpu_id_hex)
    
    # -------------------------------------------------------------
    def _showRadioConfig(self, chunk : Qsk5_res_RadioConfiguration):
        self._println("RADIO CONFIG RES:")
        self._println("  rxf_1x "+str(chunk.rxfreq_1x))

    # -------------------------------------------------------------
    def _showGenericResponse(self, chunk : Qsk5_res_Generic):
        a_s = "\nGENERIC RES: oreqid "+str(chunk.from_reqid)+" reason="+str(chunk.reason)+" code="+str(chunk.rescode)+" message="+str(chunk.message)
        self._println(a_s)
        self.stat.app_println(a_s)

    # --------------------------------------------------------------------------------------
    # not used anymore, for reference only
    def _fw_choose_filename(self, initdir : pathlib.Path ) -> pathlib.Path:
        filetypes = ( ('binary files', '*.bin'),   ('All files', '*.*')   )

        filename = askopenfilename(parent=self._toplevel, title='Open FIRMWARE file', initialdir=initdir, filetypes=filetypes)

        self._println("_clicWriteFirmware: file "+str(filename))
    
        return pathlib.Path(str(filename))

# ===============================================================================
# I need to avoid changing the image if not requested
class RadioImage(ImageLabel):
    
    # ----------------------------------------------------------------------------------
    def __init__(self, parent_panel, current_version ):
        
        self._current_fw_version = current_version
        ImageLabel.__init__(self, parent_panel, getResourcesPhotoimage(self._new_image_kradio_fname()) )

    # ----------------------------------------------------------------------------------
    def _new_image_kradio_fname(self) -> str:
        return 'img-Kradio-'+str(self._current_fw_version)+'.png'

    # ------------------------------------------------------------------------
    # must be called from swing thread to change image
    # image will be changed if the fw_version changes
    def update_image(self, fw_version : str ):
        
        if not fw_version:
            return
        
        if  self._current_fw_version == fw_version:
            return 
        
        # I can set it even if I have not found it
        self._current_fw_version = fw_version
        
        image = getResourcesPhotoimage(self._new_image_kradio_fname())
        
        if image:
            self.updateImage(image)    
        
        



