'''
/** 
 * Original work Quansheng_UV-K5_Firmware that you can find on github
 *
 * Modified 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

# to add serial driver on linux

apt-get install python3-serial
apt-get install python3-pycryptodome
 
on windows, NOTE the x on pycryptodomex so, it install the corerect library
LASO note that the serial library is pyserial and not serial !

pip install pyserial
pip install pycryptodomex
 
 
'''

from __future__ import annotations

from Cryptodome import Cipher  
import serial  # type: ignore 
import socket
import struct

import glob_eeprom
from glob_fun import get_utc_timestamp_milli
from glob_gui import LogPanel


# note that the following comment is magic
# see https://mypy.readthedocs.io/en/stable/running_mypy.html
CRC16_table = [0, 4129, 8258, 12387, 16516, 20645, 24774, 28903, 33032, 37161, 41290, 45419, 49548, 53677, 57806, 61935, 4657, 528, 12915, 8786, 21173, 17044, 29431, 25302,
            37689, 33560, 45947, 41818, 54205, 50076, 62463, 58334, 9314, 13379, 1056, 5121, 25830, 29895, 17572, 21637, 42346, 46411, 34088, 38153, 58862, 62927, 50604, 54669, 13907,
            9842, 5649, 1584, 30423, 26358, 22165, 18100, 46939, 42874, 38681, 34616, 63455, 59390, 55197, 51132, 18628, 22757, 26758, 30887, 2112, 6241, 10242, 14371, 51660, 55789,
            59790, 63919, 35144, 39273, 43274, 47403, 23285, 19156, 31415, 27286, 6769, 2640,14899, 10770, 56317, 52188, 64447, 60318, 39801, 35672, 47931, 43802, 27814, 31879,
            19684, 23749, 11298, 15363, 3168, 7233, 60846, 64911, 52716, 56781, 44330, 48395,36200, 40265, 32407, 28342, 24277, 20212, 15891, 11826, 7761, 3696, 65439, 61374,
            57309, 53244, 48923, 44858, 40793, 36728, 37256, 33193, 45514, 41451, 53516, 49453, 61774, 57711, 4224, 161, 12482, 8419, 20484, 16421, 28742, 24679, 33721, 37784, 41979,
            46042, 49981, 54044, 58239, 62302, 689, 4752, 8947, 13010, 16949, 21012, 25207, 29270, 46570, 42443, 38312, 34185, 62830, 58703, 54572, 50445, 13538, 9411, 5280, 1153, 29798,
            25671, 21540, 17413, 42971, 47098, 34713, 38840, 59231, 63358, 50973, 55100, 9939, 14066, 1681, 5808, 26199, 30326, 17941, 22068, 55628, 51565, 63758, 59695, 39368,
            35305, 47498, 43435, 22596, 18533, 30726, 26663, 6336, 2273, 14466, 10403, 52093, 56156, 60223, 64286, 35833, 39896, 43963, 48026, 19061, 23124, 27191, 31254, 2801,
            6864, 10931, 14994, 64814, 60687, 56684, 52557, 48554, 44427, 40424, 36297, 31782, 27655, 23652, 19525, 15522, 11395, 7392, 3265, 61215, 65342, 53085, 57212, 44955,
            49082, 36825, 40952, 28183, 32310, 20053, 24180, 11923, 16050, 3793, 7920]


CONN_SERIAL_ID="Serial"
CONN_TCPV4_ID="TCPv4"

QSTCP_PORT=1234


UVK5_FLASH_LEN_MAX=0xF000
UVK5_FLASH_BLOCK_LEN=0x100      
UVK5_FLASH_MAX_SIZE=0xF000    # because AFTER this there is the bootloader


UVk5_BLOADER_BEACONv2_hex=0x0518
UVk5_BLOADER_BEACONv5_hex=0x057A


    
UVK5_FOOTER_bytes = b'\xDC\xBA'


# needs to be here since python is still not capable to get the circular inport shit managed
PROTO_UVK5_hex = 0xCDAB
PROTO_IPP_hex = 0xBDAB






# -------------------------------------------------------------------------------
def qua_crc16_ccitt(data : bytes ) -> int:
    v_out = 0
    for v_index in range(0, len(data)):
        out = CRC16_table[((v_out >> 8) ^ data[v_index]) & 0xFF]
        v_out = out ^ (v_out << 8)
    return 0xFFFF & v_out

# -------------------------------------------------------------------------------
# stays out of it since it is used in different classes
def qua_payload_xor(payload : bytes ) -> bytearray:
    #xor_array = bytes.fromhex('166c14e62e910d402135d5401303e980')
    xor_array = [22, 108, 20, 230, 46, 145, 13, 64, 33, 53, 213, 64, 19, 3, 233, 128]
    xor_len   = len(xor_array)

    ba_out=bytearray(payload)
    _xor_idx = 0
    
    for v_index in range(0,len(ba_out)):

        ba_out[v_index] ^= xor_array[_xor_idx]
        
        _xor_idx += 1
        
        if _xor_idx >= xor_len:
            _xor_idx=0
        
    return ba_out


# ========================================================================================
# I need a class that wrap up an IO to - from a device that is either a radio or wiradio
# it should also wrap the idea that it uses a serial or TCP connection
# both, serial + tcp have the idea of a timeout, so, it should be feasible
# this is the abstract class, parent of QswioSerial and QswioTCP
class Qswio():

    CMD_READ_FW_MEM  = b'\x17\x05' #0x0517 -> 0x0518
    CMD_WRITE_FW_MEM = b'\x19\x05' #0x0519 -> 0x051a //Only in bootloader

    CMD_READ_CFG_MEM = b'\x1B\x05' #0x051B -> 0x051C
    CMD_WRITE_CFG_MEM= b'\x1D\x05' #0x051D -> 0x051E   // the reply comand is 1E
    
    CMD_REBOOT       = b'\xDD\x05' #0x05DD -> no reply

    # ----------------------------------------------------------------------
    # I really wish to have a way to log messages 
    def __init__(self, conn_type: str, log_panel : LogPanel ):
        self._log_panel = log_panel
        self.conn_type=conn_type
        
        self.sessTimestamp = b'\x46\x9C\x6F\x64'
        self.qs_version=''   
        self.debug=False;

    # ----------------------------------------------------------------------
    # return True if the type is the same
    def isTypeEqual(self, w_type : str ):
        return self.conn_type == w_type
        
    # ----------------------------------------------------------------------
    def _println(self, message ):
        self._log_panel.println(message)

    # ----------------------------------------------------------------------
    # this MUST be overwritten by the subclass
    # NOTE that the params will be given on subclass
    # @return true if connection successful
    def open_connection(self, parameters ) -> bool:
        self._println("ERROR: method open_connection not override")
        return False

    # ----------------------------------------------------------------------
    # this MUST be overwritten by the subclass
    def close_connection(self):
        pass

    # ----------------------------------------------------------------------
    # flush the input queue from data
    def flush_input(self):
        pass
    
    # ----------------------------------------------------------------------
    # this MUST be overwritten by the subclass
    def write_bytes(self, send_bytes : bytes ) -> int:
        return 0
    
    # ----------------------------------------------------------------------
    # this MUST be overwritten by the subclass
    def read_bytes(self, how_many : int ) -> bytearray:
        return bytearray()



    # -----------------------------------------------------------------------------------
    # send the given command code and the associated data as an obfuscated command
    
    # header      0xCDAB first byte is AB             NOT obfuscated  NOTE that a HEX number written in memory is REVERSED ! LSB first !
    # pk_dlen     lsb first, two bytes                NOT obfuscated  (from cmd_id to parameters, NOT include CRC)
    # cmd_id      lsb first, two bytes                OBFUSCATED
    # parameters  to arrive to data len               OBFUSCATED
    # crc         lsb first, two bytes                OBFUSCATED      (from cmd id to parameters)
    # footer      0cDCBA first byte DC                NOT OBFUSCATED
    
    def _qua_TX_obfusc_command(self, command_code : int, command_body : bytes ) -> int:
        
        cmd_bytes = struct.pack('<HH',command_code,len(command_body))+ command_body
        
        cmd_crc_bytes = struct.pack('<H',qua_crc16_ccitt(cmd_bytes))
        
        # obfuscation is over the command block AND crc
        obfusc_block = qua_payload_xor(cmd_bytes + cmd_crc_bytes)
        
        # NOTE that when written in LE mode 0xCDAB result as ABCD in memory, LITTle endian first
        header = struct.pack('<HH',0xCDAB, len(cmd_bytes))
        
        cmd_full =  header + obfusc_block + UVK5_FOOTER_bytes

        return self.write_bytes(cmd_full)


    # ---------------------------------------------------------------------------
    # Can use this to send a generic chunk8 to the radio
    # NOTE that the payload len MUST fit into a uint8
    def IPP_send_chunk(self, chunk_id : int, payload : bytes ):
        self.flush_input()
        
        # this makes a packet and sends it
        chunklen=len(payload)+2;

        cmd = struct.pack('<BB',chunk_id,chunklen)+payload
        cmd = struct.pack('<HH',PROTO_IPP_hex,len(cmd)+4)+cmd   # the length of the packet
        self.write_bytes(cmd)

    
    # ---------------------------------------------------------------------------
    # Can use this to send a generic chunk16 to the radio
    # NOTE that the payload len MUST fint into an uint16
    def IPP_send_chunk16(self, chunk_id : int, payload : bytes ):
        self.flush_input()
        
        # this makes a packet and sends it
        chunklen=len(payload)+4;
        
        cmd = struct.pack('<BBH',chunk_id,0,chunklen)+payload
        cmd = struct.pack('<HH',PROTO_IPP_hex,len(cmd)+4)+cmd   # the length of the packet
        self.write_bytes(cmd)

    # ----------------------------------------------------------------------------
    # quansheng command
    # read the given EEPROM memory starting from the given address and the given len
    # this does not need to be eight bytes aligned
    # requested len must be <= 128 bytes
    def qua_read_EEPROM(self,address,length):
        
        self._qua_TX_obfusc_command(0x051B, struct.pack('<HH',address,length) + self.sessTimestamp)
        
        reply = self._qua_RX_obfusc_msg(length+16)
        return reply[12:-4]


    # ----------------------------------------------------------------------------
    # receive an obfuscated message, when you know the length
    # this is mostly obsolete !
    def _qua_RX_obfusc_msg(self,length):
        msg_raw = self.read_bytes(length)

        if self.debug: 
            self._println('<raw<'+msg_raw.hex())
        
        msg_dec = msg_raw[:4] + qua_payload_xor(msg_raw[4:-2]) + msg_raw[-2:]
        
        if self.debug: 
            self._println('<clear<'+msg_dec.hex())
            
        return msg_dec


    
    # ----------------------------------------------------------------------------
    # quansheng command
    # Mandatory initial command for EEPROM operations
    # Can be called once, as far as I understand
    def qua_get_fw_version(self):
        
        if self.qs_version:
            return self.qs_version
        
        self._qua_TX_obfusc_command(0x0514, self.sessTimestamp)
        
        reply = self._qua_RX_obfusc_msg(24)
        self.qs_version = reply[8:].split(b'\0', 1)[0].decode()
        
        return self.qs_version 













# ================================================================================================================
# Library adapted from the original 
# NOTE that prepper radio ONLY support reading using the quansheng protocol (not writing)
# this is because chirp is TOTALLY out of sync with the memory layout
# and no, adjusting chirp is a mess, someone invented another language to describe memory
# another wheel reinvented as a pentagon, the alternative ? use python, or even better, c struct

# ========================================================================================
# this is the class dealing with the serial comm channel
class QswioSerial(Qswio):

    # ----------------------------------------------------------------------
    # I really wish to have a way to log messages 
    def __init__(self, log_panel : LogPanel ):
        Qswio.__init__(self, CONN_SERIAL_ID, log_panel)

        # apparently jcomport support the exclusive flag
        self.s_device = serial.Serial(exclusive=True)
        self.s_device.baudrate = 38400
        #self.serial.stopbits=2  # 12 March 2025, try this to solve apple mac issues

        # this has to be more than the timeout to reset the UART on quansheng
        self.s_device.timeout=3
        
        # this has to be more than the timeout to reset the UART on quansheng
        # in any case, there is no write flow control, so, this will never happen
        self.s_device.write_timeout=3
        

    # ----------------------------------------------------------------------
    # this MUST be overwritten by the subclass
    # NOTE that the params will be given on subclass
    def open_connection(self, portName='/dev/ttyUSB0' ) -> bool:
        self.s_device.port=portName
        self.s_device.open()
        
        adict = self.s_device.get_settings()
        self._println(str(adict))
        
        return self.s_device.is_open

    # ----------------------------------------------------------------------
    # this MUST be overwritten by the subclass
    def close_connection(self):
        try:
            self.s_device.close()
        except Exception as _err:
            pass

    # ----------------------------------------------------------------------
    def flush_input(self):
        self.s_device.flushInput()

    # ----------------------------------------------------------------------
    # this MUST be overwritten by the subclass
    def write_bytes(self, send_bytes : bytes ) -> int:
        return self.s_device.write(send_bytes)
    
    # ----------------------------------------------------------------------
    # this MUST be overwritten by the subclass
    def read_bytes(self, how_many : int ) -> bytearray:
        return self.s_device.read(how_many)
    




# =================================================================
# Kind of duplicate of Wisocket
# try to have only one
 
class QswioStreamSocket(Qswio):
    
    # ------------------------------------------------------------
    def __init__(self, log_panel : LogPanel ):
        Qswio.__init__(self, CONN_TCPV4_ID, log_panel)

        self.s_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        
    # ----------------------------------------------------------------------
    # this MUST be overwritten by the subclass
    # NOTE that the params will be given on subclass
    def open_connection(self, address ) -> bool:
        
        try:
            self.close_connection()
            
            # need to recreate, unfortunately
            self.s_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            
            # should be small enough not to be too slow to reset
            self.s_socket.settimeout(3)

            # this either complete or fails by timeout or other errors
            self.s_socket.connect(address)

            return True
        except Exception as exc:
            self._println("Connect exc "+str(exc))
            return False

    # ----------------------------------------------------------------------
    # this MUST be overwritten by the subclass
    def close_connection(self):
        try:
            self.s_socket.close()
        except Exception as _err:
            pass

    # ----------------------------------------------------------------------
    def flush_input(self):
        pass

    # ----------------------------------------------------------------------
    # this MUST be overwritten by the subclass
    def write_bytes(self, send_bytes : bytes ) -> int:
        return self.s_socket.send(send_bytes)
    
    # ----------------------------------------------------------------------
    # this MUST be overwritten by the subclass
    def read_bytes(self, how_many : int ) -> bytearray:
        return bytearray(self.s_socket.recv(how_many))
        


























        
        
    
# ===================================================================================
# I may receive eithern IPP packet OR a firmware ready to flash packet
# So, I wrap all of it in here and the result will be dealt with    
class RadioReceiveRes():
    
    # -------------------------------------------------------------------------------
    # you can receive the response from the radio by calling this
    # The response CAN contain either an IPP or QUANSHENG packet
    
    def __init__(self, qswio : Qswio ):
        self._qswio = qswio
        
        h_four_bytes : bytes = qswio.read_bytes(4)
        
        self.h_proto=0
        self.h_len=0
        self.data_and_crc=bytearray()   # for debug
        self.pk_data=bytearray()
        self.error_message=''
        
        if h_four_bytes and len(h_four_bytes) == 4:
            self.h_proto,self.h_len=struct.unpack('<HH', h_four_bytes)
        else:
            self.error_message='missing header data'

        if self.h_proto == PROTO_UVK5_hex:
            self._receiveQuansheng()
        elif self.h_proto == PROTO_IPP_hex:
            self._receiveIpp()

        # --------------------------------------------------
    # this is the equivalent of toString()
    def __str__(self):
        return "isIPP="+str(self.isIppRes())+" h_len="+str(self.h_len)
        
    # -------------------------------------------------------------------------------
    # @return true if no four bytes or not suported protocol
    def isEmpty(self) -> bool:    
        return self.h_len==0
    
    # -------------------------------------------------------------------------------
    # true if not null and proto is correct
    def isIppRes(self) -> bool:
        return self.h_len > 0 and self.h_proto == PROTO_IPP_hex
    
    # -------------------------------------------------------------------------------
    # true if not null and proto is correct
    def isQuanshengRes(self) -> bool:
        return self.h_len > 0 and self.h_proto == PROTO_UVK5_hex

    # ---------------------------------------------------------------------------------
    # receives the quansheng protocol, removing obfuscation
    # this version strip all the useless part, meaning headers, obfuscation, crc
    # @return the payload clear, meaning from the command onward
    
    # header      0xABCD first byte is AB             NOT obfuscated
    # pk_len      lsb first, two bytes                NOT obfuscated  (from cmd_id to parameters, NOT include CRC)
    # cmd_id      lsb first, two bytes                OBFUSCATED
    # parameters  to arrive to data len               OBFUSCATED
    # crc         lsb first, two bytes                OBFUSCATED      (from cmd id to parameters)
    # footer      0cDCBA first byte DC                NOT OBFUSCATED
    
    def _receiveQuansheng(self):
        
        # quansheng protocol defines data len as the data, missing the CRC and the FOOTER
        toread_len = self.h_len + 4
        
        # assume empty response
        self.pk_data=bytearray()

        data_bytes=self._qswio.read_bytes(toread_len)

        if len(data_bytes) != toread_len:
            self.error_message='QSK cannot read len '+str(toread_len)
            return

        # here I should check for the FOOTER
        h_footer = data_bytes[-2:]
        
        if h_footer != UVK5_FOOTER_bytes:
            self.error_message='QSK BAD footer'+h_footer.hex()
            return
        
        # I can now shorten the buffer of the footer
        obfusc_data_and_crc = data_bytes[:-2]
        
        # I have a crc to check IF the crc BEFORE removing obfuscation is NOT ffff
        have_crc : bool = obfusc_data_and_crc[-2:] != b'\xFF\xFF'

        # this will remove obfuscation, that covers the crc         
        self.data_and_crc=qua_payload_xor(obfusc_data_and_crc)
        
        # now, I can check the crc for the data, it is from start to the end MINUS TWO
        clean_data=self.data_and_crc[:-2]
        
        if have_crc:
            # checksum is the last two bytes, NOTE that the fun returns an array, regardless....
            h_crap = struct.unpack('<H', self.data_and_crc[-2:])
            want_crc = h_crap[0]
            
            calc_crc = qua_crc16_ccitt(clean_data)
            
            if want_crc != calc_crc:
                self.error_message='QSK BAD checksum %04x != %04x'% ( want_crc, calc_crc)

        # in any case, this is the clean data, even if CRC bad
        self.pk_data=clean_data
    
    # ---------------------------------------------------------------------------------
    def _receiveIpp(self):
        # in IPP the declared len INCLUDES the header
        w_len = self.h_len-4;

        if w_len <= 0:
            self.error_message='IPP bad len '+str(w_len)
            return
        
        self.pk_data=self._qswio.read_bytes(w_len)


        
        
        
# =============================================================================================
# this defines the virtual to manage the connection
# At first you instantiate this one, since it is not known what bootloader we are talking abount
# call the wait_bootloader to retrieve the bootloader type
# NOTE that block data must be aligned to 4 bytes, apparently 
# about radio versions, apparently
# QS con PY32F030 (flash con 64K flash)
#    1.02.01
#    1.01.01
#    1.01.07
# QS con il vecchio ARM
#    2.00.06
# QS con PY32F070 flash da 128K
#    7.00.07
# 
  
class QuaFlash():
    
    # some sort of number that is given back on fw write block OK
    _qua_sequence_no=0x00011000
    
    # ------------------------------------------------------------------------
    # NEED a place where to log messages    
    def __init__(self, qswio : Qswio ):
        self._qswio = qswio
        
        # make sure there is nothing in the pipe
        self._qswio.flush_input()

    # ------------------------------------------------------------------------
    def _println(self, msg : str ):
        self._qswio._println(msg)
        
    # -------------------------------------------------------------------------------------
    def _print_now_milli(self):
        now_m = get_utc_timestamp_milli()
        self._println('   now '+str(now_m))
        
    # -------------------------------------------------------------------------------------
    # attempt to wait for bootloader response
    # @return the actual bootloader response command code
    # 7a05 2000 01020404185357354e47ff076700bb 00 352e30302e303100280c000000000020 d16e
    # return True if the waiting is supposedly good
    def qua_FWR_wait_bootloader(self, wait_count : int ) -> bool:
        
        for _ in range(wait_count):
            
            r_res : RadioReceiveRes = RadioReceiveRes(self._qswio)

            if not r_res.isQuanshengRes():
                continue
            
            if r_res.error_message:
                self._println('qua_FWR_wait_bootloader '+r_res.error_message)
                self._println('   '+r_res.data_and_crc.hex())
                continue
            
            self.bloader : BloaderBeaconParser = BloaderBeaconParser(r_res.pk_data)
            
            self._println(str(self.bloader))

            if self.bloader.cmd_code != 0:
                return True

        return False
        

    
    # ---------------------------------------------------------------------
    # I wish to have an OK for the given W address
    # apparently, if the encrypt keys are wrong you get this 7c05080000000000000001ba
    # a good ack is something like this (for address 0) 
    # 7c05 0800 d793c322 00000000
    # it is a loop of 3 since there may be some pending tranmissions on first blocks
    # 0x057C is for V5
    # 0x051A is for V2
    # w_unpack_mode is < for LSB mode and > for MSB mode
    def _qua_FWR_wait_write_ok(self, w_cmd_code : int, w_address : int , w_unpack_mode : str ) -> bool: 
        
        for _ in range(5):
            r_res : RadioReceiveRes = RadioReceiveRes(self._qswio)

            if not r_res.isQuanshengRes():
                self._println('qua_FWR_wait_write_ok NO quansheng')
                self._println('   <'+r_res.pk_data.hex())
                self._print_now_milli()
                continue

            if r_res.error_message:
                self._println('qua_FWR_wait_write_ok '+r_res.error_message)
                self._println('   <'+r_res.data_and_crc.hex())
                self._println("continue wait_write_ok")
                #self._print_now_milli()
                continue

            abuffer = r_res.pk_data
            
            h_cmd_code,d_len=struct.unpack_from("<HH",abuffer)

            if w_cmd_code != h_cmd_code or d_len != 8:
                # this could happen since there is still the old packet in the queue
                self._println('wait_write_ok w_code 0x%04x h_code 0x%x dlen %d' % (w_cmd_code, h_cmd_code, d_len))
                self._println("   <"+abuffer.hex())
                self._print_now_milli()
                continue

            # the address is written in BIG endian mode....
            # AH FUCK python, if you have only ONE var it puts ALL of it in it, so, a tuple !
            h_qua_sequence_no,h_address,h_result=struct.unpack_from(w_unpack_mode+"IHBx", abuffer, 4)
            
            if h_qua_sequence_no != self._qua_sequence_no:
                self._println('wait_write_ok h_seq 0x{:x} w_seq 0x{:x} '.format(h_qua_sequence_no,self._qua_sequence_no))
                self._println('  raw '+abuffer.hex())
                return False
            
            if h_result != 0:
                self._println('wait_write_ok h_result %d' % (h_result))
                return False
            
            if  h_address == w_address:
                return True

            self._println('wait_write_ok h_address 0x%04x != w_address 0x%04x' % (h_address, w_address))
            self._println("   <"+abuffer.hex())
            self._print_now_milli()
            
            return False

        self._println('wait_write_ok LOOP exceeded')
        
        return False

    # ---------------------------------------------------------------------------------
    # if needed fill the firmware block with 0xFF
    # @return the ready to use block, possibly filled
    def _qua_fw_block_fill(self, a_bytes : bytes ) -> bytes:
        
        b_len=len(a_bytes)
        
        if b_len >= 0x100:
            return a_bytes
        
        filler = bytearray(0x100-b_len)

        # this is the optimal way to do it, apparently (as in memory footprint)
        # and it is useful NOT to write zeros always at the end of eeprom
        for index in range(len(filler)):
            filler[index] = 0xFF
        
        return a_bytes+filler

    # ------------------------------------------------------------------------
    # this should be override by subclasses
    # return the version of the bootloader code in python    
    def qua_FWR_py_version(self) -> str:
        return "Abstract"
    
        
    # ------------------------------------------------------------------------
    # this should be override by subclasses    
    def qua_FWR_TX_version(self) -> bool:
        return False

    
    # ------------------------------------------------------------------------
    # this should be override by subclasses    
    def qua_FWR_write_block(self, w_address : int, last_w_address : int, a_bytes : bytes ) -> bool:
        return False        
        
# =============================================================================================
# The old and simple bootloader code        
class Flash_V2(QuaFlash):
    
    # ------------------------------------------------------------------------
    def __init__(self, parent : QuaFlash ):        
        QuaFlash.__init__(self,parent._qswio)
        
    # -----------------------------------------------------------------------
    def qua_FWR_py_version(self) -> str:
        return "Flash_V2 mode"
        
    # -----------------------------------------------------------------------
    # inform bootloader fo the version that we will be flashing
    # return true if the spet is successful
    def qua_FWR_TX_version(self) -> bool:

        # proper way to pack a string with a defined length
        cmd_data=struct.pack('16s',"*.01.23".encode('ascii'))

        self._qswio._qua_TX_obfusc_command(0x0530, cmd_data)
        
        return self.qua_FWR_wait_bootloader(2) != 0

    # -----------------------------------------------------------------------
    # Write a flash block
    # return true if the request is reasonable
    # actually the flash is bigger, but there is a bootloader at 0xf000 that we don't want to overwrite 
    # So, the bootloader is actually at the end of flash, NOT at the beginning !
    # the flasher needs to know the last write address
    def qua_FWR_write_block(self, w_address : int, last_w_address : int, a_bytes : bytes ) -> bool:
        
        a_bytes = self._qua_fw_block_fill(a_bytes)
            
        block_len=len(a_bytes)

        if w_address+block_len >= UVK5_FLASH_MAX_SIZE:
            # this test is boundary side, could be wrong !
            self._println("qua_FWR_write_block BAD "+str(w_address)+" "+str(block_len) )
            return False
                    
        pk_a = struct.pack(">IHH",self._qua_sequence_no,w_address,last_w_address)
        pk_a = pk_a + struct.pack("<HH",block_len,0)

        self._println("fw_write_blk> "+pk_a.hex()+' '+a_bytes[:4].hex())

        pk_full = pk_a + a_bytes
        
        self._qswio._qua_TX_obfusc_command(0x0519, pk_full)
        
        return self._qua_FWR_wait_write_ok( 0x051A, w_address, '>')
        
        
        
        
        
# =============================================================================================
# it has a bunch of peculiarities   
class Flash_V5(QuaFlash):
    
    # ------------------------------------------------------------------------
    def __init__(self, parent : QuaFlash ):        
        QuaFlash.__init__(self,parent._qswio)

        self._use_crypto_index=0
        
        keys : Aes_UVk5_keys = Aes_UVk5_keys()
        key : Aes_IV_Key = keys[self._use_crypto_index]
        
        self.crypto = QuafwCrypt(key)

    # -----------------------------------------------------------------------
    def qua_FWR_py_version(self) -> str:
        return "Flash_V5 mode CRYPTO"
        
    # -----------------------------------------------------------------------
    # inform bootloader fo the version that we will be flashing
    # return true if the spet is successful
    def qua_FWR_TX_version(self) -> bool:

        # this is the proper way to create a 16 bytes filled with zero "string"
        # at location 20 (right after the version, add a byte telling what is the encryption to be used
        # then, pad averything to four bytes alignment 
        cmd_data=struct.pack('16sBxxx',"5.00.05".encode('ascii'),self._use_crypto_index)

        self._println("TX_firmware_version "+cmd_data.hex())

        self._qswio._qua_TX_obfusc_command(0x057D, cmd_data)
        
        return self.qua_FWR_wait_bootloader(2) != 0


    
    # -----------------------------------------------------------------------
    # Write a flash block
    # actually the flash is bigger, but there is a bootloader at 0xf000 that we don't want to overwrite 
    # So, the bootloader is actually at the end of flash, NOT at the beginning !
    # the flasher needs to know the last write address
    #  ./K5TOOL.exe -port com4 -wrflashraw qsk5_prepper_fw.bin
    # @return True if the write is successful
    def qua_FWR_write_block(self, w_address : int, last_w_address : int, a_bytes : bytes ) -> bool:
        
        a_bytes = self._qua_fw_block_fill(a_bytes)
            
        b_len=len(a_bytes)

        if w_address+b_len >= UVK5_FLASH_MAX_SIZE:
            self._println("qua_FWR_write_block BAD "+str(w_address)+" "+str(b_len) )
            return False
        
        # here I should crypto the data, quite some magic going on here 
        a_bytes = self.crypto.encrypt(a_bytes)
                    
        pk_a = struct.pack(">IHH",self._qua_sequence_no,w_address,last_w_address)
        pk_a = pk_a + struct.pack("<HH",b_len,0)

        self._println("fw_write_blk> "+pk_a.hex()+' '+a_bytes[:4].hex())

        pk_full = pk_a + a_bytes
        
        self._qswio._qua_TX_obfusc_command(0x057B, pk_full)
        
        return self._qua_FWR_wait_write_ok(0x057C, w_address, '>')



# =============================================================================================
# This should be for the new V3 with puya CPU

'''

This is the firmware data packet format from the new bootloader:

MSG_CALMS_FileUpdateData
Offset  Size  Field
0       2     u16MsgType (505)
2       2     u16MsgLen (272)
4       4     u32SeqNo (sequence number)
8       2     u16BlockIndex (current block index)
10      2     u16BlockSumNum (total blocks)
12      2     u16ByteCount (bytes in this block, typically 256)
14      1     u8Version (firmware version first character)
15      1     u16Rsv (reserved)
16      4     u32MagicCode
20      256   Firmware data (encrypted)

MSG_CALMS_UpdateConnReq
Offset  Size  Field
0       2     u16MsgType (528)
2       2     u16MsgLen (24)
4       4     u32MagicCode (random value)
8       16    versionString (firmware version)
24      1     u8SeedIndex (0-15, selects encryption key)
25      3     rsv (reserved, padding? weird that it's not included in MsgLen)

The new protocol:
     - CPS generates random seed (0-15, selects encryption key)
     - CPS sends MSG_CALMS_UpdateConnReq with seed index
     - CPS receives MSG_MSCAL_UpdateConnRsp from radio with random code
     - CPS derives AES key and IV from randCode
     - CPS encrypts entire firmware with AES
Both protocols then converge on the same path:
     - CPS divides firmware into 256-byte blocks
     - For each block:
     - CPS Creates MSG_CALMS_FileUpdateData packet
     - CPS Includes block index, total blocks, sequence number
     - CPS Sends packet (wrapped in frame)
     - CPS Waits for MSG_MSCAL_FileUpdateDataRsp (type 506)
     - CPS Verifies acknowledgment from radio

'''
   
# =============================================================================================
#         
class Flash_128K_V2(QuaFlash):
    
    # ------------------------------------------------------------------------
    def __init__(self, parent : QuaFlash ):        
        QuaFlash.__init__(self,parent._qswio)

    # -----------------------------------------------------------------------
    def qua_FWR_py_version(self) -> str:
        return "Flash 128K V2 mode"
        
    # -----------------------------------------------------------------------
    # inform bootloader fo the version that we will be flashing
    # return true if the spet is successful
    def qua_FWR_TX_version(self) -> bool:

        # proper way to pack a string with a defined length
        pk_a = struct.pack('16s',"*.01.23".encode('ascii'))  

        self._qswio._qua_TX_obfusc_command(0x530, pk_a)
        
        return self.qua_FWR_wait_bootloader(2) != 0

    # -----------------------------------------------------------------------
    # Write a flash block
    # return true if the request is reasonable
    # this is for puya 128K, the meaning of fields have changed 

    def qua_FWR_write_block(self, w_address : int, last_w_address : int, a_bytes : bytes ) -> bool:
        
        a_bytes = self._qua_fw_block_fill(a_bytes)
            
        block_len=len(a_bytes)

        w_block      = w_address >> 8
        last_w_block = last_w_address >> 8
                    
        pk_a = struct.pack("<IHH",self._qua_sequence_no,w_block,last_w_block)
        pk_a = pk_a + struct.pack("<HH",block_len,0)

        self._println("fw_write_blk> "+pk_a.hex()+' '+a_bytes[:4].hex())

        pk_full = pk_a + a_bytes
        
        self._qswio._qua_TX_obfusc_command(0x519, pk_full)
        
        return self._qua_FWR_wait_write_ok( 0x51A, w_block, '<')
        
        








# =======================================================================================================
# this decodes a bootloader beacon
# IF on exit the error_msg is not empty, then, there has been an error !
# this apparently is solid, even with different radios
class BloaderBeaconParser():
    
    # -----------------------------------------------------------------------------------------
    # receives the data that is in the packet, just the data, cleaned up
    def __init__(self, beacon_data : bytearray ):
        self.b_data = beacon_data
        self.cmd_code = 0
        self.cmd_data_len = 0
        self.error_msg=''
        self.chip_id=bytearray()
        self.fw_version=''

        # get the command and the command data len
        cmd_code,cmd_data_len=struct.unpack_from("<HH", beacon_data)

        if len(beacon_data) != cmd_data_len+4:
            self.error_msg="BAD len %04x != %04x" % ( len(beacon_data), cmd_data_len+4) 
            return

        # abort parsing if command codes are not recognized
        if cmd_code != UVk5_BLOADER_BEACONv2_hex and cmd_code != UVk5_BLOADER_BEACONv5_hex:
            self.error_msg="BAD cmd_code %04x " % (cmd_code) 
            return 

        self.cmd_code = cmd_code
        self.cmd_data_len = cmd_data_len

        # command data is a subset of the beacon data
        cmd_data = beacon_data[4:]
        
        if len(cmd_data) < 16:
            # there is no chip id 
            return
            
        # the chip ID is the first 16 bytes of the data
        self.chip_id = cmd_data[:16]    

        # if there is anything it is after this        
        fw_version_bytes = cmd_data[16:]
        
        if len(fw_version_bytes) < 16:
            # there is no bootloader version
            return
        
        fw_version = fw_version_bytes.decode('ascii', errors='ignore')
        
        # this actually works to remove trailing zeros from the string
        # because, you know, the codepoint 0 is an ASCII char .....
        # idiots, reinventing the wheel as a pentagon
        
        # this is really the firmware version
        self.fw_version = fw_version.split('\x00', 1)[0]        
        
    # ---------------------------------------------------------------------------
    def __str__(self) -> str:
        return "  b_res: 0x%04x %d" % ( self.cmd_code, self.cmd_data_len) 
    


# =======================================================================================================
# holds the info to crypt a block of data to be sent
# for AES under python windows: pip install pycryptodomex
# under linux, debian derivative: apt-get install python3-pycryptodome
# be aware that there are some crap peculiarities on windows and linux
# NOTE that Yyou MUST create a new object at each firmware transfer !     

class QuafwCrypt():
    
    # -----------------------------------------------------------------------------------------
    # when you allocate a crypto, you need to give it a key to use
    def __init__(self, a_key : Aes_IV_Key ):
        self._cypher = Cipher.AES.new(a_key.aes_key, Cipher.AES.MODE_CBC, a_key.init_vector)
    
    # -----------------------------------------------------------------------------------------
    # the block MUST be a multiple of 16 bytes
    def encrypt (self, a_bytes : bytes ) -> bytes:
        
        if len(a_bytes) & 0x0F:
            raise ValueError("bad block len "+str(len(a_bytes)))
        
        return self._cypher.encrypt(a_bytes)



# =======================================================================================================
# It is a class cince it is WAY easier to handle
class Aes_IV_Key():

    # -----------------------------------------------------------------------------------------
    # the values are in some "little endian mode' (even if they are bytes)
    # most likely, because they are optimized for the radio CPU
    def __init__(self, a_iv='', a_key='' ):
        try:
            self.init_vector = self.from_LE_to_BE(bytes.fromhex(a_iv))
            self.aes_key = self.from_LE_to_BE(bytes.fromhex(a_key))
            self.isValid=True
        except Exception as _err:
            self.init_vector = bytearray(16)
            self.aes_key = bytearray(16)
            self.isValid=False
            

    # -----------------------------------------------------------------------------------------
    # to be used in the generic crypto module they have to be converted to BE
    def from_LE_to_BE(self, a_bytes : bytes ) -> bytes:
        v0,v1,v2,v3=struct.unpack('<IIII', a_bytes)
        return struct.pack('>IIII',v0,v1,v2,v3)

# =======================================================================================================
# keys taken from K5TOOL
class Aes_UVk5_keys(list[Aes_IV_Key]):
    
    def __init__(self):
        
        self.append(Aes_IV_Key('14b7a2be0223e259b2066d8886977e36','e16e0d29e0c83418987f9433f5ff620e'))
        self.append(Aes_IV_Key('916c50fb9e480693b155b2e555cb780a','b0d93af7761a50cacb966eb8a805bcbb'))
        self.append(Aes_IV_Key('fb149d4c45e7d7a95aa64bc22765a8c0','1357cc24d138a57799dd0eec5b9a18f9'))
        self.append(Aes_IV_Key('32c860dfce65ca302ec534aa5f88884b','a8ef4f917688c5c1487f2a6a811e554d'))
        self.append(Aes_IV_Key('e8ab4e0e344cb5a0b1e7db7a05d468c3','97a387567b234b55c921cd82f65f0087'))
        self.append(Aes_IV_Key('c6116bd5bea38673746205ee534d589f','f0240e02ed5da965539d815306f2d34b'))
        self.append(Aes_IV_Key('4bfe35582436578014e970ed8dab9ea0','734dd8ac84c0c422cbca28fec8856473'))
        self.append(Aes_IV_Key('96858cdf59454a5fa84faa3663748b8d','7f67480570ba5254d7cee59ca2a483b5'))
        self.append(Aes_IV_Key('2f9a34bcec302bc6c5b6bc552c166dc9','67ec7c38cefa9edb38bb89b58986eb1a'))
        self.append(Aes_IV_Key('5f6f2ecd835d760b5c1e78f0be1a8787','08ea3a7dc8a6e961b7061f7e52ec8e6d'))
        self.append(Aes_IV_Key('3d680b15a6274c72bc3313a183f1372b','4e324c565e32be8a6aaf26d9ff37ee87'))
        self.append(Aes_IV_Key('a10be7032960c6d5dcbdc6b4ecfbe31a','4fd9d7243916543c23ad995937e4bb7c'))
        self.append(Aes_IV_Key('925eb778232e8b4b1f06a582498b2149','a1d6c61c8c5c2281dc7e371ca3c3f168'))
        self.append(Aes_IV_Key('8b67978471fbd371dcf44ed92f56562d','11e065ed3f9e8b96bae61f79a7d8a317'))
        self.append(Aes_IV_Key('5b7237ddb9c5290e15519018ceacba76','c4636958e1830f23c6d9ce15b2ddc35a'))
        self.append(Aes_IV_Key('8172ea14916b606855ff2aabe52e993c','bf9862d680f948195f5c90545e1d8578'))




        
        
# =======================================================================================================
# this decodes a quansheng read eeprom

# Reply.Header.ID   = 0x051C;
# Reply.Header.Size = pCmd->Size + 4;
# Reply.Data.Offset = pCmd->Offset;
# Reply.Data.Size   = pCmd->Size;

'''
typedef struct {
    Header_t Header;
    struct {
        uint16_t Offset;
        uint8_t  Size;
        uint8_t  Padding;
        uint8_t  Data[128];
    } Data;
} REPLY_051B_t;
'''

class QS_read_EE_Parser():
    
    # -----------------------------------------------------------------------------------------
    # receives the data that is in the packet, just the data, cleaned up
    def __init__(self, qsEE_data : bytearray ):
        self.b_data = qsEE_data
        self.cmd_code = 0
        self.cmd_data_len = 0
        self.error_msg=''
        self.ee_bytearray=bytearray()
        self.ee_start_a=0          # starring address

        # get the command and the command data len, this takes the first 4 bytes
        cmd_code,cmd_data_len=struct.unpack_from("<HH", qsEE_data)

        if len(qsEE_data) != cmd_data_len+4:
            self.error_msg="BAD len %04x != %04x" % ( len(qsEE_data), cmd_data_len+4) 
            return

        # abort parsing if command codes are not recognized
        if cmd_code != 0x051C:
            return 

        self.cmd_code = cmd_code
        self.cmd_data_len = cmd_data_len

        # command data is a subset of read eeprom packet
        cmd_data = qsEE_data[4:]
        
        if len(cmd_data) < 4:
            # there is nothing in the packet 
            return

        # this takes the first 4 bytes, as required
        self.ee_start_a,b_len,_pad=struct.unpack_from("<HBB", cmd_data)

        if b_len < 1:
            # there is nothing in the packet 
            return

        # the actual byte array is after
        self.ee_bytearray=cmd_data[4:]

        if b_len != len(self.ee_bytearray):
            self.error_msg="BAD B len %04x != %04x" % ( len(self.ee_bytearray), b_len) 
            return

    def get_EEBlock(self):
        return glob_eeprom.EEblock(self.ee_start_a,self.ee_bytearray)

    def isValid(self) -> bool:
        return len(self.ee_bytearray) > 0
        
        
    


