diff --git a/uvk5.py b/uvk5.py index 702fe57..a9922a9 100644 --- a/uvk5.py +++ b/uvk5.py @@ -27,16 +27,14 @@ # along with this program. If not, see . -# import struct +import struct import logging -# import serial from chirp import chirp_common, directory, bitwise, memmap, errors, util from chirp.settings import RadioSetting, RadioSettingGroup, \ RadioSettingValueBoolean, RadioSettingValueList, \ RadioSettingValueInteger, RadioSettingValueString, \ RadioSettings -# from chirp.settings import RadioSettingValueFloat, RadioSettingValueMap LOG = logging.getLogger(__name__) @@ -49,7 +47,7 @@ DEBUG_SHOW_OBFUSCATED_COMMANDS = False # might be useful for someone debugging some obscure memory issue DEBUG_SHOW_MEMORY_ACTIONS = False -DRIVER_VERSION = "Quansheng UV-K5 driver v20230529 (c) Jacek Lipkowski SQ5BPF" +DRIVER_VERSION = "Quansheng UV-K5 driver v20230608 (c) Jacek Lipkowski SQ5BPF" PRINT_CONSOLE = False MEM_FORMAT = """ @@ -240,8 +238,27 @@ BANDS = { 5: [400.0, 469.9999], 6: [470.0, 600.0] } + +# for radios with modified firmware: +BANDS_NOLIMITS = { + 0: [18.0, 76.0], + 1: [108.0, 135.9999], + 2: [136.0, 199.9990], + 3: [200.0, 299.9999], + 4: [350.0, 399.9999], + 5: [400.0, 469.9999], + 6: [470.0, 1300.0] + } BANDMASK = 0b1111 +VFO_CHANNEL_NAMES = ["F1(50M-76M)A", "F1(50M-76M)B", + "F2(108M-136M)A", "F2(108M-136M)B", + "F3(136M-174M)A", "F3(136M-174M)B", + "F4(174M-350M)A", "F4(174M-350M)B", + "F5(350M-400M)A", "F5(350M-400M)B", + "F6(400M-470M)A", "F6(400M-470M)B", + "F7(470M-600M)A", "F7(470M-600M)B"] + # the communication is obfuscated using this fine mechanism def xorarr(data: bytes): @@ -275,9 +292,11 @@ def _send_command(serport, data: bytes): (len(data), util.hexprint(data))) crc = calculate_crc16_xmodem(data) - data2 = data+bytes([crc & 0xff, (crc >> 8) & 0xff]) + data2 = data + struct.pack("HBB", 0xabcd, len(data), 0) + \ + xorarr(data2) + \ + struct.pack(">H", 0xdcba) if DEBUG_SHOW_OBFUSCATED_COMMANDS: LOG.debug("Sending command (obfuscated):\n%s" % util.hexprint(command)) try: @@ -335,16 +354,12 @@ def _receive_reply(serport): def _getstring(data: bytes, begin, maxlen): - s = "" - c = 0 - for i in data: - c += 1 - if c < begin: - continue - if i < ord(' ') or i > ord('~'): + tmplen = min(maxlen+1, len(data)) + s = [data[i] for i in range(begin, tmplen)] + for key, val in enumerate(s): + if val < ord(' ') or val > ord('~'): break - s += chr(i) - return s + return ''.join(chr(x) for x in s[0:key]) def _sayhello(serport): @@ -362,7 +377,7 @@ def _sayhello(serport): LOG.warning("Failed to initialise radio") raise errors.RadioError("Failed to initialize radio") return False - firmware = _getstring(o, 5, 16) + firmware = _getstring(o, 4, 16) LOG.info("Found firmware: %s" % firmware) return firmware @@ -371,7 +386,7 @@ def _readmem(serport, offset, length): LOG.debug("Sending readmem offset=0x%4.4x len=0x%4.4x" % (offset, length)) readmem = b"\x1b\x05\x08\x00" + \ - bytes([offset & 0xff, (offset >> 8) & 0xff, length, 0]) + \ + struct.pack("> 8) & 0xff, dlen, 1]) + \ + writemem = b"\x1d\x05" + \ + struct.pack("= BANDS[a][0] and mhz <= BANDS[a][1]: + if self.FIRMWARE_NOLIMITS: + B = BANDS_NOLIMITS + else: + B = BANDS + for a in B: + if mhz >= B[a][0] and mhz <= B[a][1]: return a return False @directory.register -class TemplateRadio(chirp_common.CloneModeRadio): +class UVK5Radio(chirp_common.CloneModeRadio): """Quansheng UV-K5""" VENDOR = "Quansheng" MODEL = "UV-K5" @@ -496,6 +515,7 @@ class TemplateRadio(chirp_common.CloneModeRadio): NEEDS_COMPAT_SERIAL = False FIRMWARE_VERSION = "" + FIRMWARE_NOLIMITS = False def get_prompts(x=None): rp = chirp_common.RadioPrompts() @@ -545,7 +565,7 @@ class TemplateRadio(chirp_common.CloneModeRadio): "->Tone", "->DTCS", "DTCS->", "DTCS->DTCS"] rf.valid_characters = chirp_common.CHARSET_ASCII - rf.valid_modes = ["FM", "NFM", "AM"] + rf.valid_modes = ["FM", "NFM", "AM", "NAM"] rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS", "Cross"] rf.valid_skips = [""] @@ -675,8 +695,18 @@ class TemplateRadio(chirp_common.CloneModeRadio): mem.number = number2 + is_empty = False # We'll consider any blank (i.e. 0MHz frequency) to be empty if (_mem.freq == 0xffffffff) or (_mem.freq == 0): + is_empty = True + + # We'll also look at the channel attributes if a memory has them + if number < 200: + _mem3 = self._memobj.channel_attributes[number] + if _mem3 & 0x08 > 0: + is_empty = True + + if is_empty: mem.empty = True # set some sane defaults: mem.power = UVK5_POWER_LEVELS[2] @@ -704,7 +734,7 @@ class TemplateRadio(chirp_common.CloneModeRadio): return mem if number > 199: - mem.name = "VFO_"+str(number-199) + mem.name = VFO_CHANNEL_NAMES[number-200] mem.immutable = ["name"] else: _mem2 = self._memobj.channelname[number] @@ -733,9 +763,10 @@ class TemplateRadio(chirp_common.CloneModeRadio): # mode if (_mem.flags1 & FLAGS1_ISAM) > 0: - # Actually not sure if internally there aren't "Narrow AM" - # and "Wide AM" modes. To be investigated. - mem.mode = "AM" + if (_mem.flags2 & FLAGS2_BANDWIDTH) > 0: + mem.mode = "NAM" + else: + mem.mode = "AM" else: if (_mem.flags2 & FLAGS2_BANDWIDTH) > 0: mem.mode = "NFM" @@ -1019,12 +1050,15 @@ class TemplateRadio(chirp_common.CloneModeRadio): basic.append(rs) # Battery save + tmpbatsave = _mem.battery_save + if tmpbatsave >= len(BATSAVE_LIST): + tmpbatsave = BATSAVE_LIST.index("1:4") rs = RadioSetting( "battery_save", "Battery Save", RadioSettingValueList( BATSAVE_LIST, - BATSAVE_LIST[_mem.battery_save])) + BATSAVE_LIST[tmpbatsave])) basic.append(rs) # Dual watch @@ -1207,6 +1241,13 @@ class TemplateRadio(chirp_common.CloneModeRadio): rs = RadioSetting("driver_ver", "Driver version", val) roinfo.append(rs) + # No limits version for hacked firmware + val = RadioSettingValueBoolean(self.FIRMWARE_NOLIMITS) + val.set_mutable(False) + rs = RadioSetting("nolimits", "Limits disabled for modified firmware", + val) + roinfo.append(rs) + return top # Store details about a high-level memory to the memory map @@ -1246,23 +1287,38 @@ class TemplateRadio(chirp_common.CloneModeRadio): if number < 200: _mem4.channel_attributes[number] = 0x0f + # find tx frequency + if mem.duplex == '-': + txfreq = mem.freq - mem.offset + elif mem.duplex == '+': + txfreq = mem.freq + mem.offset + else: + txfreq = mem.freq + # find band - band = _find_band(mem.freq) + band = _find_band(self, txfreq) + if band is False: + raise errors.RadioError( + "Transmit frequency %.4fMHz is not supported by this radio" + % txfreq/1000000.0) + + band = _find_band(self, mem.freq) if band is False: - # raise errors.RadioError( - # "Frequency is outside the supported bands") return mem # mode - if mem.mode == "AM": - _mem.flags1 = _mem.flags1 | FLAGS1_ISAM - _mem.flags2 = _mem.flags2 & ~FLAGS2_BANDWIDTH - else: + if mem.mode == "NFM": + _mem.flags2 = _mem.flags2 | FLAGS2_BANDWIDTH _mem.flags1 = _mem.flags1 & ~FLAGS1_ISAM - if mem.mode == "NFM": - _mem.flags2 = _mem.flags2 | FLAGS2_BANDWIDTH - else: - _mem.flags2 = _mem.flags2 & ~FLAGS2_BANDWIDTH + elif mem.mode == "FM": + _mem.flags2 = _mem.flags2 & ~FLAGS2_BANDWIDTH + _mem.flags1 = _mem.flags1 & ~FLAGS1_ISAM + elif mem.mode == "NAM": + _mem.flags2 = _mem.flags2 | FLAGS2_BANDWIDTH + _mem.flags1 = _mem.flags1 | FLAGS1_ISAM + elif mem.mode == "AM": + _mem.flags2 = _mem.flags2 & ~FLAGS2_BANDWIDTH + _mem.flags1 = _mem.flags1 | FLAGS1_ISAM # frequency/offset _mem.freq = mem.freq/10 @@ -1337,3 +1393,19 @@ class TemplateRadio(chirp_common.CloneModeRadio): _mem.scrambler & 0xf0) | SCRAMBLER_LIST.index(svalue) return mem + + +@directory.register +class UVK5Radio_nolimit(UVK5Radio): + VENDOR = "Quansheng" + MODEL = "UV-K5 (modified firmware)" + VARIANT = "nolimits" + FIRMWARE_NOLIMITS = True + + def get_features(self): + rf = UVK5Radio.get_features(self) + # This is what the BK4819 chip supports + rf.valid_bands = [(18000000, 620000000), + (840000000, 1300000000) + ] + return rf