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