'''
/** 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

There is only ONE poll, this is a GUI and if there is something to send it should be set to the poll

So, what should this do ? send commands to a radio, remote, in a secure way

Why is difficult ? need a nonce mechanism that is secure

Why is extra difficult ? slave should "talk" as little as possible

What is needed
- a destination and sender radio ID (the one that is in the selective calls), 4 bytes
- The generic IPP header, 2 bytes packet len + 1 byte packet type(20) + the rest + CRC (2 bytes)
- The radio creates/has a nonce, something that is at least three bytes and will accept packets with that nonce only
  the nonce MUST change at every command received !
  the nonce should be possibly stored for later retrieval
  Master can request a new nonce be created and sent to master

- to send a request master must have:
  to radio id
  from radio id
  to cpu id
  to AES key (the shared key)
  current nonce (does not apply for the status req packet)
- possible packets
  get status: (nonce req, .....)
  set channel params
  set radio params
  
How does it work ?
  master send a "get status" to receive the nonce
     request is crypted and the first byte is random to make it a "randomic" packet
  
  slave receives the get status
    check the checksum
    check the destination is self
    decrypt using the cpu_id + shared key
    creates a new nonce to send
    nonce is sent by means of an encrypted packet, possibly including other info
     
  master receives the nonce
    check the checksum
    check the destination is self
    decrypt using the cpu_id + shared key
    creates a request that has the received nonce inside
    
  slave receives the command
    check the checksum
    check the destination is self
    decrypt using the cpu_id + shared key
    check that the provided nonce match, if not, do nothing (possibly increment spoofing counter)



'''

from __future__ import annotations

from Cryptodome.Cipher import AES
import secrets
import struct
from tkinter import messagebox, StringVar, IntVar
import typing

from app_config import AppConfig, ConfigSavable
from app_csv import Csv_Writer, CsvTable
from glob_fun import getResourcesPhotoimage, string_to_int
from glob_gui import JtkWinToplevel, LogPanel, JtkPanelGrid, TV_Entry, \
    GUI_hide_show_window, \
    JtkPanelPackLeft, JtkTableColdef, JtkTable, JtkPanelPackTop, \
    table_row_selected_background_color, JtkPanelTabbed, JtkCheckbox, \
    JtkComboIndexed, TV_EntryFrequency_MHz_1x, JtkLabel, JtkButtonImage 
import glob_ippqs
from glob_mlang import MlangLabel
from ipp_parser import Qsk5_req_send_dtmf
import qpystat
from qseeprom_gui import Qseeprom_gui
import tkinter.ttk as ttk
from uvk5_lib import qua_crc16_ccitt, Aes_IV_Key


RACMD_IPP_REQ_STATUS=0x0101
RACMD_IPP_RES_STATUS=0x0102

RACMD_IPP_REQ_SET_CHANNEL=0x0103
RACMD_IPP_RES_SET_CHANNEL=0x0104

RACMD_IPP_REQ_SET_RADIO=0x0105
RACMD_IPP_RES_SET_RADIO=0x0106









rapwcol_idx="row_idx"
rapwcol_raname="rapwcol_raname"
rapwcol_radioid="rapwcol_radioid"
rapwcol_cpuid="rapwcol_cpuid"
rapwcol_aeskey="rapwcol_aeskey"
rapwcol_nonce="rapwcol_nonce"
rapwcol_note="rapwcol_note"

    
# -------------------------------------------------------
# Generic way to define a map from a column name to some properties        
racocols_map = { 
    rapwcol_idx      : JtkTableColdef(rapwcol_idx,'IDx', True, 40), 
    rapwcol_raname   : JtkTableColdef(rapwcol_raname,"Name", False, 110),
    rapwcol_radioid  : JtkTableColdef(rapwcol_radioid,"Radio ID", False, 110),
    rapwcol_cpuid    : JtkTableColdef(rapwcol_cpuid,"CPU ID (AES IV)", False, 210),      # the AES init vector
    rapwcol_aeskey   : JtkTableColdef(rapwcol_aeskey,"AES key", False, 210),
    rapwcol_nonce    : JtkTableColdef(rapwcol_nonce,"Nonce", True, 110),
    rapwcol_note     : JtkTableColdef(rapwcol_note,"Note", False, 210),
    }


# ===================================================================
# It is safer to deal with objects instead of a generic map
class Rapw_row:

    csv_heading=['row_idx' ,'rapwcol_raname', 'rapwcol_radioid', 'rapwcol_cpuid', 'rapwcol_aeskey', 'rapwcol_nonce', 'rapwcol_note' ]
    
    # ----------------------------------------------------------------
    # I just need to create the iid
    # the row is always shown, with zero values
    def __init__(self, index : int ):
        self.row_idx=index
        self._clear()

    # ----------------------------------------------------------------
    # return true if the row is usable to send a command
    def isUsable (self) -> bool:
        if not self.row_radioid:
            return False
        
        if not self.row_aeskey:
            return False 
        
        if not self.row_cpuid:
            return False
        
        return True

    # ----------------------------------------------------------------
    def _clear(self):
        self.row_raname = ''
        self.row_radioid=''
        self.row_cpuid=''         # this is the IV for the AES
        self.row_aeskey=''
        self.row_nonce=0
        self.row_note = '';

    # ----------------------------------------------------------------
    def nonce_increment(self):
        self.row_nonce = self.row_nonce+1

    # ----------------------------------------------------------------
    def saveConfig(self) -> typing.Dict[str,str]:

        pw_dict=dict()
        
        pw_dict['rapwcol_raname']=self.row_raname
        pw_dict['rapwcol_radioid']=self.row_radioid
        pw_dict['rapwcol_cpuid']=self.row_cpuid
        pw_dict['rapwcol_aeskey']=self.row_aeskey
        pw_dict['rapwcol_nonce']=str(self.row_nonce)
        pw_dict['rapwcol_note']=self.row_note
        
        return pw_dict
            
    # -----------------------------------------------------------------
    def setFromConfig(self, a_table : dict):
        self.row_raname=a_table['rapwcol_raname']
        self.row_radioid=a_table['rapwcol_radioid']
        self.row_cpuid=a_table['rapwcol_cpuid']
        self.row_aeskey=a_table['rapwcol_aeskey']
        self.row_nonce=string_to_int(a_table['rapwcol_nonce'],1)
        self.row_note=a_table['rapwcol_note']
            
    # -----------------------------------------------------------------
    # must return the values as a list of strings in the same order as the header
    def csv_get_values(self) -> typing.List[str]:
        
        risul : typing.List[str] = []
        
        risul.append(str(self.row_idx))
        risul.append(str(self.row_raname))
        risul.append(str(self.row_radioid))
        risul.append(str(self.row_cpuid))
        risul.append(str(self.row_aeskey))
        risul.append(str(self.row_nonce))
        risul.append(str(self.row_note))
        
        return risul
            
    # -----------------------------------------------------------------
    # return the list of values to show into the table, the GUI table
    # NOTE that the order has to be consistent with the GUI definition
    def getValuesForTreeview(self):
        risul = (
            self.row_idx,
            self.row_raname,
            self.row_radioid,
            self.row_cpuid,
            self.row_aeskey,
            str(self.row_nonce),
            self.row_note
            )
            
        return risul         
    
    # ------------------------------------------------------------
    def encrypt_decrypt_aes_block(self, i_block : bytes, w_encrypt : bool ) ->bytes:
        
        aes_iv = bytes.fromhex(self.row_cpuid)
        if len(aes_iv) != 16:
            raise Exception("AES IV lenght is not 16 bytes "+str(len(self.row_cpuid)))

        aes_key = bytes.fromhex(self.row_aeskey)
        if len(aes_key) != 16:
            raise Exception("AES KEY lenght is not 16 bytes "+str(len(self.row_aeskey)))

        if len(i_block) != 16:
            raise Exception("AES data lenght is not 16 bytes "+str(len(i_block)))

        aes_cypher = AES.new(aes_key, AES.MODE_CBC, aes_iv)
        
        if w_encrypt:
            return aes_cypher.encrypt(i_block)
        else:
            return aes_cypher.decrypt(i_block)
    
    # --------------------------------------------------
    # this is the equivalent of toString()
    def __str__(self):
        return 'ID '+self.row_radioid+' : '+self.row_raname+' - '+self.row_note

    

    
# ========================================================================
# Let me try to have some decent OO wrapping in this shitty language
class Rapw_list(list[Rapw_row]):

    # -----------------------------------------------------------------
    # I need to preallocate list and never deallocate them
    def __init__(self):
        
        for r_idx in range(0,10):
            self.append(Rapw_row(r_idx))

    # -----------------------------------------------------------------
    # return a tuple/list of values to display in combo box
    def get_combo_values(self) -> list[str]:

        risul = []

        for a_row in self:
            pw_row : Rapw_row = a_row
            row_raname = pw_row.row_raname
            if row_raname:
                risul.append(str(pw_row))
            else:
                risul.append('')

        return risul
    
    # -----------------------------------------------------------------
    # return or throw error
    def get_row (self, r_index : int ) -> Rapw_row:
        return self[r_index]


    # -----------------------------------------------------------------
    #def saveConfig(self) -> typing.Dict[str,dict[str,str]]:
    # trying to define a return type is a MESS
    def saveConfig(self) -> dict:

        rapw_tbl=dict()
                
        for a_row in self:
            pw_row : Rapw_row = a_row
            rapw_tbl[pw_row.row_idx] = pw_row.saveConfig()                    

        return rapw_tbl

    # -----------------------------------------------------------------
    def importConfig(self, a_table : dict):
        
        for index,row in enumerate(self):
            row.setFromConfig(a_table[str(index)])

    # ------------------------------------------------------------
    # given the list of iid_po clear them up
    def clear_rowlist(self, list_idx : typing.List[int]):
        
        for row_idx in list_idx:
            a_row : Rapw_row = self.get_row(row_idx)
            
            a_row._clear()


        
# =====================================================================
# the main GUI

class Racommand_gui (ConfigSavable,GUI_hide_show_window,CsvTable):

    # ----------------------------------------------------------------
    def __init__(self, stat : qpystat.Qpystat):
        self._stat = stat
        
        self._stat.appconfig.addMyselfToSavables(self)
        self._stat.appcsv.add_to_tablesList(self)

        self._rapw_list : Rapw_list = Rapw_list()

        # now, let me make the gui 
        self._toplevel = JtkWinToplevel("Radio Commands")
        
        a_frame = JtkPanelPackTop(self._toplevel, padding=2 )
        a_frame.addItem(self._newTabPanel(a_frame),fill='both',expand=True)

        a_frame.pack(fill='both',expand=True)
        
        self.GUI_hide_window()

        self.GUI_refresh()
        
        self._println("Radio Command init complete")

    # -----------------------------------------------------------------------
    # The gui has different panels depending on what You need to do
    # Most basic activity is to define radio passwords
    # then there is sending the request for nonce
    def _newTabPanel(self, parent_panel) -> ttk.Widget:

        a_panel=JtkPanelTabbed(parent_panel)
        
        a_panel.addItem(self._newTablePanel(a_panel),"My Radios")
        
        self._req_status_gui = Rac_status_gui(self,a_panel,'Request Status')
        a_panel.addItem(self._req_status_gui.getPanel(),'Request Status')
        
        self._channel_config_gui = Rac_channel_config_gui(self,a_panel,'Set Channel Configuration')
        a_panel.addItem(self._channel_config_gui.getPanel(),'Set Channel Configuration')

        self._radio_config_gui = Rac_radio_config_gui(self,a_panel,'Set Radio Configuration')
        a_panel.addItem(self._radio_config_gui.getPanel(),'Set Radio Configuration')
        
        a_panel.addItem(self._newLogPanel(a_panel),'Log')

        a_panel.bind('<<NotebookTabChanged>>',self._onTabChanged)

        self._tabPanel=a_panel

        return a_panel

    # ----------------------------------------------------------------------
    # given parameter is just the coordinates
    # there is no easy way to know what is the selected tab
    # so, I just refresh all of them...
    def _onTabChanged(self,_par1):
        self._println('on Tab changed click')
    

    # -----------------------------------------------------------------------
    def _newLogPanel(self, parent_panel) -> ttk.Widget:
        self._racom_log : LogPanel = LogPanel(parent_panel,"Commands Log")
        return self._racom_log
        
    # -----------------------------------------------------------------------
    # Should provide a button to import current radio config into the table
    def _newTablePanel(self, parent_panel) -> ttk.Widget:
        
        a_panel=JtkPanelPackTop(parent_panel)     
        
        a_panel.addItem(JtkLabel(a_panel,'My radio directory',style='Header1.TLabel', anchor='center'))
        
        self._table_view = JtkTable(a_panel,racocols_map )
        self._table_view.getComponent().pack(fill='both', expand=True)
        
        tk_tbl : ttk.Treeview = self._table_view.getTable()
        
        tk_tbl.tag_configure('Updated',  background=table_row_selected_background_color)
        tk_tbl.bind('<<TreeviewSelect>>', self._jtable_row_selected)
        tk_tbl.bind('<Delete>', self._jtable_delete)
        
        self._edit_gui=Rapw_edit_gui(self,a_panel)
        a_panel.addItem(self._edit_gui.getComponent())

        return a_panel

    # -------------------------------------------------------------------
    # standard delete 
    def _jtable_delete(self, sel_coord):

        list_idx : list[int] = self._get_selected_idx()

        if not list_idx:
            return
        
        # now requesta a series of rows clear
        self._rapw_list.clear_rowlist(list_idx)

        self.GUI_refresh()

    # -------------------------------------------------------------------
    # ok, use the _po version, I may need it
    def _get_selected_idx(self) -> list[int]:

        a_sel = self._table_view.getSelected_iid()
        
        if not a_sel:
            self._println('_jtable_ctrl_x: NO rows selected')
            return [] 

        # this is an experiment, basically, shugar, useless code.... 
        # the pythonic way to get diabetes
        seleted_idx : list[int] = [int(iid_po) for iid_po in a_sel]
        
        return seleted_idx


    # -------------------------------------------------------------------
    # get the currently selected IV and Key
    # NOTE that the IV is the cpuid of the selected radioid
    # NOTE that it is possible to have the currently connected radio (using serial port) as destination !
    # NOTE that if no info are provided the returned object will have a status of not valid
    def _jtable_get_IV_Key(self) -> Aes_IV_Key:
        a_sel = self._table_view.getSelected_iid()
        
        if not a_sel:
            return Aes_IV_Key() 

        row_idx : int = int(a_sel[0])

        sel_item : Rapw_row = self._rapw_list[row_idx]

        return Aes_IV_Key(sel_item.row_cpuid,sel_item.row_aeskey)



    # -------------------------------------------------------------------
    def _jtable_row_selected(self, _sel_coord):
        
        a_sel = self._table_view.getSelected_iid()
        
        if not a_sel:
            return 
        
        #self._println(str(a_sel))
        
        sel_item = self._rapw_list[int(a_sel[0])]

        self._edit_gui.showThisRow(sel_item)


    # ----------------------------------------------------------
    # override CSV
    def csv_tableName(self) -> str:
        return 'radio_command_passwords'
    
    # ----------------------------------------------------------
    # override CSV
    def csv_write_header(self, a_writer : Csv_Writer):
        a_writer.writerow(Rapw_row.csv_heading)

    # ---------------------------------------------------------
    # in a different thread than swing, write the table to this csv writer
    def csv_write_table(self, a_writer : Csv_Writer):
        
        for row in self._rapw_list:
            v_list = row.csv_get_values()
            a_writer.writerow(v_list)


    # -------------------------------------------------------------------
    # prints something on the application log
    def _println(self, msg):
        self._racom_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()    
    

    # -------------------------------------------------------------------
    # implement the config savable
    def appImportConfig(self, cfg : AppConfig ):
        adict = cfg.getDictValue("racommand_cfg", {})
        
        try:
            self._toplevel.setGeometry(adict['gui_cfg'])
            
            self._rapw_list.importConfig(adict['rapw_table'])
            
            self.GUI_refresh()

        except Exception as _exc :
            self._println("racommand.appImportConfig "+str(_exc))            

    # -------------------------------------------------------------------
    # implement the config savable
    def appSaveConfig(self, cfg : AppConfig ):
        #adict = dict(str,typing.Any)
        adict = dict()

        adict['gui_cfg'] = self._toplevel.getGeometry()
        adict['rapw_table'] = self._rapw_list.saveConfig()

        cfg.setDictValue("racommand_cfg", adict )        
        



    # -------------------------------------------------------------------
    # At any time it should be possible to filter the entries and show them
    # Note that this may be called at the beginning , when there is nothing in the list...
    def GUI_refresh(self ):    
        
        for row in self._rapw_list:
            e_row : Rapw_row = row
            if not self._table_view.row_exist(e_row.row_idx):
                self._table_view.row_append(e_row.row_idx,e_row.getValuesForTreeview())
            else:
                self._table_view.set_row(e_row.row_idx,e_row.getValuesForTreeview(),'')
                
        # request subgui refresh
        self._req_status_gui.GUI_refresh()
        self._channel_config_gui.GUI_refresh()
        self._radio_config_gui.GUI_refresh()
        
                

    # ------------------------------------------------------------------------------
    # this should update the swing table content for the given row
    def _jtable_refresh_using_row(self, a_row : Rapw_row ):

        if not a_row:
            return
            
        self._table_view.set_row(a_row.row_idx,a_row.getValuesForTreeview(),'')   


    # ------------------------------------------------------------------------
    def _showInfo(self, a_message : str ):
        messagebox.showinfo("Radio Commands", a_message, parent=self._toplevel, type='ok')    
        
    # ------------------------------------------------------------------------------
    # I need to get the id, quite often
    def _get_EEPROM_my_id(self) -> str:
        return self._stat.eeprom_gui._DTMF_myID.csv_getValue()
        
    # ------------------------------------------------------------------------
    # the poller will call myself to parse the incoming packet
    # I only know that this is an IPP packet and I have the packet len at the beginning
    def parseIncomingIpp16(self, res_ipp16 : glob_ippqs.Qsk5_res_pollipp16 ):
        self._println(str(res_ipp16))
        
        res_racommand : IPPU_res_racommand = IPPU_res_racommand(res_ipp16.ipp_packet,self._racom_log)
        
        self._println(str(res_racommand))
        
        if not res_racommand.isValid:
            self._println('parseIncomingIpp16: parse incoming ipp16 is invalid')
            return
        
        if res_racommand.custom_cid == RACMD_IPP_RES_STATUS:
            self._req_status_gui.parse_ipp_res(res_racommand)
        elif res_racommand.custom_cid == RACMD_IPP_RES_SET_RADIO:
            self._radio_config_gui.parse_ipp_res(res_racommand)
        elif res_racommand.custom_cid == RACMD_IPP_RES_SET_CHANNEL:
            self._channel_config_gui.parse_ipp_res(res_racommand)
        else:
            self._println('parseIncomingIpp16: unsupported custom_cid=0x%x' % (res_racommand.custom_cid))

        
# ==============================================================================
# it is kind of easier to have a separate class            
class Rapw_edit_gui():

    # ------------------------------------------------------------
    # I make this a frame for ease of use, it should a bit hidden...    
    def __init__(self, parent_gui : Racommand_gui , parent_panel : ttk.Frame ):
        
        self._parent_gui = parent_gui
        self._stat = parent_gui._stat

        # need an empty row, by default
        self.sel_item : Rapw_row = Rapw_row(0)

        self._work_panel = self._newWorkPanel(parent_panel)

    # ------------------------------------------------------------
    def _println(self, a_msg : str ):
        self._parent_gui._println(a_msg)

    # ------------------------------------------------------------
    def _showInfo(self, a_message : str ):
        self._parent_gui._showInfo(a_message)

    # ------------------------------------------------------------
    def getComponent(self) -> ttk.Frame:
        return self._work_panel

    # ------------------------------------------------------------------------
    # there is a circular import issue, so, I need to split the addition
    # idiotic snake
    def _newMlangLabel(self, parent_panel : ttk.Frame, en_msg : str ) -> MlangLabel:
        alabel=MlangLabel(parent_panel,en_msg,self._parent_gui._stat.glob_mlang)
        return alabel


    def _newSaveImportPanel(self,parent_panel : ttk.Frame ) -> ttk.Frame:        

        a_panel = JtkPanelPackTop(parent_panel)

        a_panel.addItem(JtkButtonImage(a_panel,getResourcesPhotoimage('racmd-read-radio.png'), command=self._clickReadEEPROM))
        #a_panel.addItem(ttk.Button(a_panel,text="Read EEPROM", command=self._clickReadEEPROM))

        a_panel.addItem(JtkButtonImage(a_panel,getResourcesPhotoimage('racmd-import-data.png'), command=self._clickImportRadio))
        #a_panel.addItem(ttk.Button(a_panel,text="Import cabled Radio", command=self._clickImportRadio))

        a_panel.addItem(JtkButtonImage(a_panel,getResourcesPhotoimage('racmd-save-data.png'), command=self._clicSaveEditRapw))
        #self._a_i1=getResourcesPhotoimage('save-64x64.png')
        #a_panel.addItem(ttk.Button(a_panel, image=self._a_i1, command=self._clicSaveEditRapw))
        
        a_panel.addItem(JtkButtonImage(a_panel,getResourcesPhotoimage('racmd-write-radio.png'), command=self._click_write_on_radio))
        #a_panel.addItem(ttk.Button(a_panel,text="Write cabled Radio", command=self._click_write_on_radio))

        # multilingua
        #a_panel.addItem(self._newMlangLabel(a_panel,"Nothing is saved on radio"))
        
        return a_panel

    # -------------------------------------------------------------------------------
    def _clickReadEEPROM(self):
        self._println("_clickReadEEPROM")
        
        eeprom : Qseeprom_gui = self._stat.eeprom_gui
        
        eeprom._click_readEeprom()

        self._showInfo("Reading EEPROM... (+/- 10 sec)")



    # -------------------------------------------------------------------------------
    # should write on eeprom the current config
    def _click_write_on_radio(self):
        self._println("_click_write_on_radio")

        eeprom : Qseeprom_gui = self._stat.eeprom_gui

        if not eeprom.is_EEPROM_loaded:
            self._showInfo('Please first read EEPROM')
            return

        eeprom._radio_name.csv_setValue(self._edit_radio_name.get())
        eeprom._DTMF_myID.csv_setValue(self._edit_radio_id.get())
        eeprom._aes_key.csv_setValue(self._edit_aes_key.get())
        eeprom._aes_nonce.csv_setValue(str(self._edit_nonce.get()))

        # this will simulate a write button
        eeprom._writeButton.invoke()


    # -------------------------------------------------------------------------------
    # should import from eeprom, if there is an eeprom
    # if AES key is not available , create a new one
    def _clickImportRadio(self):
        self._println("_clickImportRadio")
        
        eeprom : Qseeprom_gui = self._stat.eeprom_gui
        
        if not eeprom.is_EEPROM_loaded:
            self._showInfo('Please first read EEPROM')
            return

        # this is not taken from EEPROM, it is acquired on read 
        self._edit_cpu_id.set(self._stat.qconnect.cur_radio_cpu_id_hex)

        self._edit_radio_name.set(eeprom._radio_name.csv_getValue())
        self._edit_radio_id.set(self._parent_gui._get_EEPROM_my_id())
        self._edit_aes_key.set(eeprom._aes_key.csv_getValue())
        self._edit_nonce.set(string_to_int(eeprom._aes_nonce.csv_getValue(),0))

        self._AES_key_adjust()
        
        #self._clicSaveEditRapw()

    # -------------------------------------------------------------------------------
    # You can run this anytime to adjust the AES key
    # meaning, make a new one if the key is invalid
    def _AES_key_adjust(self):
        try:
            h_aes_hex = self._edit_aes_key.get()
            h_aes= bytes.fromhex(h_aes_hex)
            if len(h_aes) != 16:
                self._edit_aes_key.set(secrets.token_hex(16))
            elif h_aes[0]==0xFF:
                self._edit_aes_key.set(secrets.token_hex(16))
            else:
                self._edit_aes_key.set(h_aes.hex())
        except Exception as _exc:
            self._edit_aes_key.set(secrets.token_hex(16))

    # ------------------------------------------------------------
    def _newWorkPanel(self, parent_panel : ttk.Frame ) -> ttk.Frame:
        apanel = JtkPanelPackLeft(parent_panel)
        
        apanel.addItem(self._newGridPanel(apanel))
        apanel.addItem(self._newSaveImportPanel(apanel))
        
        return apanel

    # -------------------------------------------------------------------------------
    def _newGridPanel(self, parent_panel) -> JtkPanelGrid:
        gridpanel = JtkPanelGrid(parent_panel) # , borderwidth=1, relief='solid', padding=3)

        self._edit_radio_name = StringVar(gridpanel)
        self._edit_radio_id = StringVar(gridpanel)
        self._edit_cpu_id   = StringVar(gridpanel)
        self._edit_aes_key  = StringVar(gridpanel)
        self._edit_nonce    = IntVar(gridpanel)
        self._edit_note     = StringVar(gridpanel)

        # ------------------------------------------------------------
        gridpanel.nextRow()
        gridpanel.addItem(JtkLabel(gridpanel,"Radio Name"))
        gridpanel.addItem(TV_Entry(gridpanel, self._edit_radio_name, width=32))

        gridpanel.nextRow()

        gridpanel.addItem(JtkLabel(gridpanel,"Radio ID"))
        gridpanel.addItem(TV_Entry(gridpanel, self._edit_radio_id))

        gridpanel.nextRow()

        gridpanel.addItem(JtkLabel(gridpanel,"CPU ID"))
        gridpanel.addItem(TV_Entry(gridpanel, self._edit_cpu_id, width=32))

        gridpanel.nextRow()

        gridpanel.addItem(JtkLabel(gridpanel,"AES key"))
        gridpanel.addItem(TV_Entry(gridpanel, self._edit_aes_key, width=32))

        gridpanel.nextRow()

        gridpanel.addItem(JtkLabel(gridpanel,"Nonce"))
        gridpanel.addItem(TV_Entry(gridpanel, self._edit_nonce, width=32))

        gridpanel.nextRow()

        gridpanel.addItem(JtkLabel(gridpanel,"Note"))
        gridpanel.addItem(TV_Entry(gridpanel, self._edit_note))

        return gridpanel

    # -------------------------------------------------------------------------------
    # The user click SAVE in the edit panel
    # this should map the values back to the selected row
    def _clicSaveEditRapw(self):

        if not self.sel_item:
            self._showInfo("_clicSaveEditRapw: Please select a row")

        self.sel_item.row_raname = self._edit_radio_name.get()
        self.sel_item.row_radioid = self._edit_radio_id.get()
        self.sel_item.row_cpuid = self._edit_cpu_id.get()
        self.sel_item.row_aeskey = self._edit_aes_key.get()
        self.sel_item.row_nonce = self._edit_nonce.get()
        self.sel_item.row_note = self._edit_note.get()

        self._parent_gui.GUI_refresh()

    # -------------------------------------------------------------------------------
    # Parent request to show this row
    def showThisRow(self, sel_item : Rapw_row ):

        if not sel_item:
            self._showInfo("showThisRow: Please select a row")
            return;
        
        self.sel_item=sel_item  # will be used in the save
        
        self._edit_radio_name.set(sel_item.row_raname)
        self._edit_radio_id.set(sel_item.row_radioid)
        self._edit_cpu_id.set(sel_item.row_cpuid)
        self._edit_aes_key.set(sel_item.row_aeskey)
        self._edit_nonce.set(sel_item.row_nonce)
        self._edit_note.set(sel_item.row_note)

        self._AES_key_adjust()        


# =================================================================================
# this is given an IPP packet (including the initial len and will unpack it
class IPPU_res_racommand():
    '''
    uint16_t ipp_packet_len
    struct IPP_racmd_header
       uint8_t  chunk_id;       // chunk id RMSG_IPP_GENERIC_ID8
       uint8_t  chunk_len;      // the whole chunk len
       uint16_t custom_cid;     // DTMF_IPP_REQ_STATUS, this is the REQ !
       uint32_t from_radio_id;  // the one in the eeprom
       uint32_t to_radio_id;    // the one to reply to
    '''
    # --------------------------------------------------------------------------
    def __init__(self, ipp_packet : bytes, log_panel : LogPanel ):
        
        self._ipp_packet=ipp_packet
        self._log_panel=log_panel
        
        self.isValid=False
        
        if len(ipp_packet) < 4:
            self._log_panel.println('IPPU_res_racommand: len(ipp_packet) < 4 '+str(len(ipp_packet)))
            return
        
        # unpack_from does not care if the buffer is longer
        pk_len,chunk_id,chunk_len=struct.unpack_from('<HBB', ipp_packet)
        
        self.pk_len=pk_len
        self.chunk_id=chunk_id
        self.chunk_len=chunk_len
        
        if self.chunk_len < 30:
            self._log_panel.println('IPPU_res_racommand: self.chunk_len < 32: '+str(self.chunk_len))
            return
        
        custom_cid,from_radio_id,to_radio_id=struct.unpack_from('<HII',ipp_packet,4)
        self.custom_cid=custom_cid
        self.from_radio_id=from_radio_id
        self.to_radio_id=to_radio_id
        
        self._chunk_buffer=self._ipp_packet[2:]
        
        self.isValid=self.is_valid_crc()
        
    # --------------------------------------------------
    # the crc is calculated 
    def is_valid_crc(self)-> bool:
        
        # want crc is at the end of the chunk buffer
        a_tuple=struct.unpack_from('<H',self._chunk_buffer,len(self._chunk_buffer)-2)
        
        w_crc = a_tuple[0]
        h_crc = qua_crc16_ccitt(self._chunk_buffer[:-2])

        self._log_panel.println( 'w_crc=%x h_crc=%x ' %(w_crc,h_crc) )
        
        return w_crc==h_crc
    
    # --------------------------------------------------
    def __str__(self) -> str:
        a_str = "IPPU_res_racommand: %d 0x%x %d 0x%x f=%d t=%d" % (self.pk_len,self.chunk_id,self.chunk_len,self.custom_cid,self.from_radio_id,self.to_radio_id)
        return a_str


# --------------------------------------------------------------------------
# All three panels share some common code
class Rac_gui_common():
    
    # ----------------------------------------------------------------------
    # All panels are grid with a heading name
    def __init__(self, parent_gui : Racommand_gui, parent_frame : ttk.Widget, panel_name : str ):
        self._parent_gui=parent_gui
        self._stat = parent_gui._stat
        
        self._work_panel : JtkPanelGrid = self._new_common_panel(parent_frame, panel_name)
        
    # ----------------------------------------------------------------------
    def _new_common_panel(self, parent_panel : ttk.Widget, panel_name : str ) -> JtkPanelGrid:    
        a_panel=JtkPanelGrid(parent_panel)     
        
        a_panel.addItem(JtkLabel(a_panel,panel_name,style='Header1.TLabel', anchor='center'), columnspan=2)
        a_panel.nextRow()
        
        #----------------------------------------------
        self._want_row_index=IntVar(a_panel,0)
        a_panel.addItem(JtkLabel(a_panel,'Select Radio ',style='Header1.TLabel'))
        self._want_row_combo = JtkComboIndexed(a_panel,self._want_row_index, width=30)
        self._want_row_combo.set_on_selected_fun(self._on_combo_change)
        a_panel.addItem(self._want_row_combo)
        a_panel.nextRow()   

        #----------------------------------------------
        a_panel.addItem(MlangLabel(a_panel,"Selected radio",self._stat.glob_mlang, style='Bold.TLabel' ))
        self._selected_radio_label=JtkLabel(a_panel) # show the selected radio here
        a_panel.addItem(self._selected_radio_label)
        a_panel.nextRow()
        
        return a_panel
        
    # ----------------------------------------------------------------------
    # Update the gui on combo change
    def _on_combo_change(self):
        pw_row : Rapw_row = self._get_rapw_row()        
        self._selected_radio_label.config(text=str(pw_row))

        
    # ----------------------------------------------------------------------
    # It should refresh the combo content
    def GUI_refresh(self ):
        self._want_row_combo['values'] = self._parent_gui._rapw_list.get_combo_values()
        
    # ----------------------------------------------------------------------
    # Another fun to go to superclass
    def _println(self, msg : str ):
        self._parent_gui._println(msg)
        
    # ----------------------------------------------------------------------
    # Another fun to go to superclass
    def _showInfo(self, msg : str ):
        self._parent_gui._showInfo(msg)

    # ----------------------------------------------------------------------
    # Get the panel to put on gui 
    def getPanel(self) -> ttk.Widget:
        return self._work_panel
    
    # ----------------------------------------------------------------------
    # Another fun to go to superclass
    def _get_rapw_row(self) -> Rapw_row:
        
        w_row_idx= self._want_row_index.get()
        
        self._println("want row index="+str(w_row_idx))
        
        return self._parent_gui._rapw_list[w_row_idx]
        
        
# =========================================================================
# The gui for the status part
class Rac_status_gui(Rac_gui_common):
    
    # ----------------------------------------------------------------------
    # 
    def __init__(self, parent_gui : Racommand_gui, parent_frame : ttk.Widget, panel_name : str  ):
        Rac_gui_common.__init__(self, parent_gui, parent_frame, panel_name)
        
        self._fill_work_panel()
                
    # ----------------------------------------------------------------------
    # It should be possible to send requests to different radio
    def _fill_work_panel(self):
        
        a_panel=self._work_panel
        
        a_panel.addItem(ttk.Button(a_panel,text="Request Status", command=self._clic_req_status ), columnspan=2)
        a_panel.nextRow()
        
        a_panel.addItem(JtkLabel(a_panel,'Battery Voltage'))
        self._battV_show=JtkLabel(a_panel)
        a_panel.addItem(self._battV_show)
        a_panel.nextRow()

        a_panel.addItem(JtkLabel(a_panel,'CPU Temeprature'))
        self._cpu_Tc_show=JtkLabel(a_panel)
        a_panel.addItem(self._cpu_Tc_show)
        a_panel.nextRow()
        
        a_panel.addItem(JtkLabel(a_panel,'Response Message'))
        self._res_message_show=JtkLabel(a_panel)
        a_panel.addItem(self._res_message_show)
        a_panel.nextRow()
    
        a_panel.addItem(JtkLabel(a_panel,'Speaker '))
        self._res_speaker_dis_show=JtkLabel(a_panel)
        a_panel.addItem(self._res_speaker_dis_show)
        a_panel.nextRow()

        a_panel.addItem(JtkLabel(a_panel,'PTT '))
        self._res_ptt_dis_show=JtkLabel(a_panel)
        a_panel.addItem(self._res_ptt_dis_show)
        a_panel.nextRow()

        a_panel.addItem(JtkLabel(a_panel,'LED Blink '))
        self._res_blink_show=JtkLabel(a_panel)
        a_panel.addItem(self._res_blink_show)

    # -------------------------------------------------------------------------------
    # here I should make a packet....
    def _clic_req_status(self, _extra=None):
        self._println("SEND Status req")
        
        try:
            self._battV_show.clear()
            self._cpu_Tc_show.clear()
            self._res_message_show.setText('sending request')
            self._res_ptt_dis_show.clear()
            self._res_speaker_dis_show.clear()
            self._res_blink_show.clear()
            self._send_req_status()
        except Exception as _exc :
            self._showInfo("_clic_req_status: "+str(_exc))
            
        
    # -------------------------------------------------------------------------------
    def _send_req_status(self):

        my_id=self._parent_gui._get_EEPROM_my_id()

        if not my_id:
            self._showInfo('please read connected radio eeprom')
            return

        pw_row : Rapw_row = self._get_rapw_row()

        if not pw_row.isUsable():
            self._showInfo('please select a valid row')
            return

        to_id=pw_row.row_radioid

        self._println(" ------------------------ request Status")
        
        a_nonce=pw_row.row_nonce
        self._println("  nonce=%d" % (a_nonce) )
        
        aes_in=struct.pack('<Hxxxxxxxxxxxxxx',a_nonce)

        self._println("  AES_in "+aes_in.hex() )
        aes_out=pw_row.encrypt_decrypt_aes_block(aes_in, True)
        self._println("  AES_out "+aes_out.hex() )
        
        # leave out the chunk_id and chunk_len, start from custom_cid
        pack_d1 = struct.pack('<HII',RACMD_IPP_REQ_STATUS ,int(my_id) ,int(to_id))+aes_out

        # add the header, the first byte is the chunk_id
        # the length adds tho bytes of the chunk_id and len PLUS the CRC at the end !
        pack_d2 = struct.pack('<BB',glob_ippqs.RMSG_IPP_GENERIC_ID8,len(pack_d1)+4)+pack_d1

        # now, let me add the CRC AT THE END of packet, NOTE that the chunk len is already correct
        crc_val = qua_crc16_ccitt(pack_d2)
        pack_d3 = pack_d2+struct.pack('<H',crc_val)
        
        # the initial len is the whole chunk plus the two bytes of len
        # also, the AC is the prefix for IPP when sent over the radio
        pack_risul=struct.pack('<H',len(pack_d3)+2)+pack_d3
    
        dtmf_digits_count=len(pack_risul)*2
    
        cmd = Qsk5_req_send_dtmf(10 ,10 ,dtmf_digits_count ,pack_risul, True)
        self._stat.qconnect.queuePut(cmd)
        
        self._println("Status req sent")
    
    # ------------------------------------------------------------------------
    def _get_status(self, h_flags : int, h_mask : int, on_true : str, on_false : str  ):
        if h_flags & h_mask:
            return on_true;
        else:
            return on_false;
    
    # ------------------------------------------------------------------------
    # The main action is to decrypt the AES and store away the nonce, for later use
    # NOTE that the received nonce is the one to be used NEXT TIME ! 
    
    '''
   uint16_t aes_nonce;         // MUST be first. the nonce sent or received ,0 is a NULL nonce !
   uint16_t batteryV_2dec;     // current battery level or zero if new nonce
   uint16_t cpu_Tc_2dec;       // CPU temperature or zero if new nonce
   enum RCMD_status_code res_code;   //
   uint8_t  pad[9];
    '''
    
    def parse_ipp_res(self, res_racommand : IPPU_res_racommand):
        
        self._println('parse_ipp_res status: '+str(res_racommand))

        pw_row : Rapw_row = self._get_rapw_row()

        if not pw_row.isUsable():
            self._showInfo('please select a valid row')
            return

        i_block = res_racommand._chunk_buffer[12:12+16]

        self._println('parse_ipp_res: i_block '+i_block.hex())

        o_block : bytes = pw_row.encrypt_decrypt_aes_block(i_block, False)
         
        self._println('parse_ipp_res: o_block '+o_block.hex())
        
        rx_nonce, batteryV_2dec, cpu_Tc_2dec, status_code, status_flags=struct.unpack_from('<HHHBB', o_block)
        
        self._println("rx_nonce: %u" % (rx_nonce))
        self._println("have_nonce: %u" % (pw_row.row_nonce))
        self._println("status code: %u" % (status_code))

        self._println("batteryV_2dec: %f" % (float(batteryV_2dec)/100.0))
        self._battV_show.setText("V %f" % (float(batteryV_2dec)/100.0))
        
        self._println("cpu_Tc_2dec: %f" % (float(cpu_Tc_2dec)/100.0))
        self._cpu_Tc_show.configure(text=" C %f" % (float(cpu_Tc_2dec)/100.0))

        self._println("status flags: 0x%x" % (status_flags))

        self._res_speaker_dis_show.setText(self._get_status(status_flags, 0b00000001, 'Disabled', 'Enabled'))
        self._res_ptt_dis_show.setText(self._get_status(status_flags, 0b00000010, 'Disabled', 'Enabled'))
        self._res_blink_show.setText(self._get_status(status_flags, 0b00000100, 'Enabled', 'Disabled'))

        # the logic is that if the incoming nonce is correct I just increment        
        if rx_nonce == pw_row.row_nonce: 
            self._res_message_show.setText("Nonce valid")
        elif batteryV_2dec == 0:
            pw_row.row_nonce=rx_nonce
            self._res_message_show.setText("Nonce sync")
        else:
            self._println("INVALID packet received: nonce do not match AND battery is NOT zero");
            self._res_message_show.setText("Invalid Packet")
        
        self._parent_gui.GUI_refresh()
        
        
        
# =========================================================================
# The gui for the status part
class Rac_radio_config_gui(Rac_gui_common):
    
    # ----------------------------------------------------------------------
    # 
    def __init__(self, parent_gui : Racommand_gui, parent_frame : ttk.Widget, panel_name : str  ):
        Rac_gui_common.__init__(self, parent_gui, parent_frame, panel_name)
        
        self._fill_work_panel()
                
    # ----------------------------------------------------------------------
    # It should be possible to send requests to different radio
    def _fill_work_panel(self):

        a_panel = self._work_panel
        #----------------------------------------------
        self._disable_speaker=JtkCheckbox(a_panel,"Disable Speaker")
        a_panel.addItem(self._disable_speaker)
        a_panel.nextRow()

        #----------------------------------------------
        self._disable_ptt=JtkCheckbox(a_panel,"Disable PTT")
        a_panel.addItem(self._disable_ptt)
        a_panel.nextRow()
        
        #----------------------------------------------
        self._enable_blink=JtkCheckbox(a_panel,"Enable LED Blink")
        a_panel.addItem(self._enable_blink)
        a_panel.nextRow()

        #----------------------------------------------
        a_panel.addItem(ttk.Button(a_panel,text="Set Configuration", command=self._click_set_radio_configuration ), columnspan=2)

    #----------------------------------------------
    def _click_set_radio_configuration(self):
        
        self._println("------- SET Radio Configuration")

        try:
            self._send_set_radio_parameters()
        except Exception as _exc :
            self._showInfo("_click_set_radio_configuration: "+str(_exc))
            
        
    # -------------------------------------------------------------------------------
    def _send_set_radio_parameters(self):

        '''
           uint16_t aes_nonce;         // PC->Radio: radio checks valid nonce Radio -> PC pc checks valid nonce
           uint8_t  radio_flags_bits;  // See RACMD_RADIO_FLAGS_.....
           uint8_t  pad[13];
        '''

        my_id=self._parent_gui._get_EEPROM_my_id()

        if not my_id:
            self._showInfo('please read connected radio eeprom')
            return

        pw_row : Rapw_row = self._get_rapw_row()

        if not pw_row.isUsable():
            self._showInfo('please select a valid row')
            return

        to_id=pw_row.row_radioid
        
        self._println(" ------------------------ Set Radio")
        
        flags = 0

        if self._disable_speaker.isChecked():
            flags |= 0b00000001
            
        if self._disable_ptt.isChecked():
            flags |= 0b00000010
            
        if self._enable_blink.isChecked():
            flags |= 0b00000100

        self._println("  flags=0x%x" % (flags) )
        
        a_nonce=pw_row.row_nonce
        self._println("  nonce=%d" % (a_nonce) )
        
        aes_in=struct.pack('<HBxxxxxxxxxxxxx',a_nonce,flags)

        self._println("  AES_in "+aes_in.hex() )
        aes_out=pw_row.encrypt_decrypt_aes_block(aes_in, True)
        self._println("  AES_out "+aes_out.hex() )
        
        # leave out the chunk_id and chunk_len, start from custom_cid
        pack_d1 = struct.pack('<HII',RACMD_IPP_REQ_SET_RADIO ,int(my_id) ,int(to_id))+aes_out

        # add the header, the first byte is the chunk_id
        # the length adds tho bytes of the chunk_id and len PLUS the CRC at the end !
        pack_d2 = struct.pack('<BB',glob_ippqs.RMSG_IPP_GENERIC_ID8,len(pack_d1)+4)+pack_d1

        # now, let me add the CRC AT THE END of packet, NOTE that the chunk len is already correct
        crc_val = qua_crc16_ccitt(pack_d2)
        pack_d3 = pack_d2+struct.pack('<H',crc_val)
        
        # the initial len is the whole chunk plus the two bytes of len
        # also, the AC is the prefix for IPP when sent over the radio
        pack_risul=struct.pack('<H',len(pack_d3)+2)+pack_d3
    
        dtmf_digits_count=len(pack_risul)*2
    
        cmd = Qsk5_req_send_dtmf(10 ,10 ,dtmf_digits_count ,pack_risul, True)
        self._stat.qconnect.queuePut(cmd)
        
        self._println("SET Radio Parameters sent")
            
    # ------------------------------------------------------------------------
    def parse_ipp_res(self, res_racommand : IPPU_res_racommand):
        self._println('parse_ipp_res radio: '+str(res_racommand))

        pw_row : Rapw_row = self._get_rapw_row()

        if not pw_row.isUsable():
            self._showInfo('please select a valid row')
            return

        i_block = res_racommand._chunk_buffer[12:12+16]

        o_block : bytes = pw_row.encrypt_decrypt_aes_block(i_block, False)
        
        self._println('o_block '+o_block.hex())

        have_nonce, flags=struct.unpack_from('<HB', o_block)
        
        self._println("have_nonce: %u" % (have_nonce))
        self._println("flags: %x" % (flags))

        # the logic is that if the incoming nonce is correct I just increment        
        if have_nonce == pw_row.row_nonce: 
            pw_row.nonce_increment()
        else:
            self._println("INVALID packet received: nonce do not match");
        

# =========================================================================
# The gui for the channel config
class Rac_channel_config_gui(Rac_gui_common):
    
    # ----------------------------------------------------------------------
    # 
    def __init__(self, parent_gui : Racommand_gui, parent_frame : ttk.Widget, panel_name : str  ):
        Rac_gui_common.__init__(self, parent_gui, parent_frame, panel_name)
        
        self._fill_work_panel()
                
    # ----------------------------------------------------------------------
    # It should be possible to send requests to different radio
    def _fill_work_panel(self):

        a_panel = self._work_panel

        self._chan_index_po = IntVar(a_panel)
        a_panel.addItem(JtkLabel(a_panel,'Channel number'))
        a_panel.addItem(TV_Entry(a_panel,self._chan_index_po))
        a_panel.nextRow()
        
        self._chan_F_1x = TV_EntryFrequency_MHz_1x(a_panel)
        a_panel.addItem(JtkLabel(a_panel,'RX Frequency'))
        a_panel.addItem(self._chan_F_1x)
        a_panel.nextRow()
        
        a_panel.addItem(ttk.Button(a_panel,text="Set Configuration", command=self._click_set_configuration ), columnspan=2)
        a_panel.nextRow()

        a_panel.addItem(JtkLabel(a_panel,'Channel number'))
        self._channel_number_label=ttk.Label(a_panel)
        a_panel.addItem(self._channel_number_label)
        a_panel.nextRow()

        a_panel.addItem(JtkLabel(a_panel,'RX Frequency'))
        self.rx_F_label=ttk.Label(a_panel)
        a_panel.addItem(self.rx_F_label)
        a_panel.nextRow()



    #----------------------------------------------
    # Wrap all into try catch
    def _click_set_configuration(self):
        self._println("------- SET Channel Configuration")

        try:
            self._send_set_channel_parameters()
        except Exception as _exc :
            self._showInfo("_send_set_channel_parameters: "+str(_exc))
        
    # -------------------------------------------------------------------------------
    def _send_set_channel_parameters(self):
        '''
           uint16_t aes_nonce;         // PC->Radio: radio checks valid nonce
           uint32_t rx_freq_1x;        // if requested set this RX frequency
           uint8_t  chan_flags_bits;   // See RACMD_CHAN_FLAGS_.....
           uint8_t  channel_idx_po;    // so, 0 means NULL
           uint8_t  pad[8];
        '''

        my_id=self._parent_gui._get_EEPROM_my_id()

        if not my_id:
            self._showInfo('please read connected radio eeprom')
            return

        pw_row : Rapw_row = self._get_rapw_row()

        if not pw_row.isUsable():
            self._showInfo('please select a valid row')
            return

        to_id=pw_row.row_radioid
        
        self._println(" ------------------------ Set Channel")
        
        a_nonce=pw_row.row_nonce
        self._println("  nonce=%d" % (a_nonce) )
        
        a_F_1x = self._chan_F_1x.get_value()
        self._println("  a_F_1x=%d" % (a_F_1x) )
        
        a_chan_po = self._chan_index_po.get()
        self._println("  a_chan_po=%d" % (a_chan_po) )

        aes_in=struct.pack('<HIBBxxxxxxxx',a_nonce,a_F_1x,0,a_chan_po)

        self._println("  AES_in "+aes_in.hex() )
        aes_out=pw_row.encrypt_decrypt_aes_block(aes_in, True)
        self._println("  AES_out "+aes_out.hex() )
        
        # leave out the chunk_id and chunk_len, start from custom_cid
        pack_d1 = struct.pack('<HII',RACMD_IPP_REQ_SET_CHANNEL ,int(my_id) ,int(to_id))+aes_out

        # add the header, the first byte is the chunk_id
        # the length adds tho bytes of the chunk_id and len PLUS the CRC at the end !
        pack_d2 = struct.pack('<BB',glob_ippqs.RMSG_IPP_GENERIC_ID8,len(pack_d1)+4)+pack_d1

        # now, let me add the CRC AT THE END of packet, NOTE that the chunk len is already correct
        crc_val = qua_crc16_ccitt(pack_d2)
        pack_d3 = pack_d2+struct.pack('<H',crc_val)
        
        # the initial len is the whole chunk plus the two bytes of len
        # also, the AC is the prefix for IPP when sent over the radio
        pack_risul=struct.pack('<H',len(pack_d3)+2)+pack_d3
    
        dtmf_digits_count=len(pack_risul)*2
    
        cmd = Qsk5_req_send_dtmf(10 ,10 ,dtmf_digits_count ,pack_risul, True)
        self._stat.qconnect.queuePut(cmd)
        
        self._println("SET Radio Parameters sent")

    # ----------------------------------------------------------------------
    # What is sent back is the current F and chan
    # I use the selected radio since there may be radio with the same ID
    def parse_ipp_res(self, res_racommand : IPPU_res_racommand):
        self._println('parse_ipp_res channel: '+str(res_racommand))

        pw_row : Rapw_row = self._get_rapw_row()

        if not pw_row.isUsable():
            self._showInfo('please select a valid row')
            return

        i_block = res_racommand._chunk_buffer[12:12+16]

        o_block : bytes = pw_row.encrypt_decrypt_aes_block(i_block, False)
        
        self._println('o_block '+o_block.hex())
        
        rx_nonce, rx_F_1x, _flags, chan_po=struct.unpack_from('<HIBB', o_block)
        
        self._println("rx_nonce: %u" % (rx_nonce))
        self._println("have_nonce: %u" % (pw_row.row_nonce))
        
        self._println("chan_po %u" % (chan_po))
        self._channel_number_label.configure(text="%d" % (chan_po))

        self._println("RX_F_1x %u" % (rx_F_1x))
        self.rx_F_label.configure(text="V %u" % (rx_F_1x))
        

        # the logic is that if the incoming nonce is correct I just increment        
        if rx_nonce == pw_row.row_nonce: 
            pw_row.nonce_increment()
        else:
            self._println("INVALID packet received: nonce do not match");
        
        
        
        
        