kopia lustrzana https://github.com/sq5bpf/uvk5-reverse-engineering
add variant for radios with modified firmware
rodzic
55fe65c3f5
commit
b769749f62
152
uvk5.py
152
uvk5.py
|
@ -27,16 +27,14 @@
|
|||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
# 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("<H", crc)
|
||||
|
||||
command = b"\xAB\xCD"+bytes([len(data)])+b"\x00"+xorarr(data2)+b"\xDC\xBA"
|
||||
command = 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("<HBB", offset, length, 0) + \
|
||||
b"\x6a\x39\x57\x64"
|
||||
_send_command(serport, readmem)
|
||||
o = _receive_reply(serport)
|
||||
|
@ -390,8 +405,8 @@ def _writemem(serport, data, offset):
|
|||
(offset, len(data), util.hexprint(data)))
|
||||
|
||||
dlen = len(data)
|
||||
writemem = b"\x1d\x05"+bytes([dlen+8])+b"\x00" + \
|
||||
bytes([offset & 0xff, (offset >> 8) & 0xff, dlen, 1]) + \
|
||||
writemem = b"\x1d\x05" + \
|
||||
struct.pack("<BBHBB", dlen+8, 0, offset, dlen, 1) + \
|
||||
b"\x6a\x39\x57\x64"+data
|
||||
|
||||
_send_command(serport, writemem)
|
||||
|
@ -479,16 +494,20 @@ def do_upload(radio):
|
|||
return True
|
||||
|
||||
|
||||
def _find_band(hz):
|
||||
def _find_band(self, hz):
|
||||
mhz = hz/1000000.0
|
||||
for a in BANDS:
|
||||
if mhz >= 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
|
||||
|
|
Ładowanie…
Reference in New Issue