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/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
|
||||||
# import struct
|
import struct
|
||||||
import logging
|
import logging
|
||||||
# import serial
|
|
||||||
|
|
||||||
from chirp import chirp_common, directory, bitwise, memmap, errors, util
|
from chirp import chirp_common, directory, bitwise, memmap, errors, util
|
||||||
from chirp.settings import RadioSetting, RadioSettingGroup, \
|
from chirp.settings import RadioSetting, RadioSettingGroup, \
|
||||||
RadioSettingValueBoolean, RadioSettingValueList, \
|
RadioSettingValueBoolean, RadioSettingValueList, \
|
||||||
RadioSettingValueInteger, RadioSettingValueString, \
|
RadioSettingValueInteger, RadioSettingValueString, \
|
||||||
RadioSettings
|
RadioSettings
|
||||||
# from chirp.settings import RadioSettingValueFloat, RadioSettingValueMap
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -49,7 +47,7 @@ DEBUG_SHOW_OBFUSCATED_COMMANDS = False
|
||||||
# might be useful for someone debugging some obscure memory issue
|
# might be useful for someone debugging some obscure memory issue
|
||||||
DEBUG_SHOW_MEMORY_ACTIONS = False
|
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
|
PRINT_CONSOLE = False
|
||||||
|
|
||||||
MEM_FORMAT = """
|
MEM_FORMAT = """
|
||||||
|
@ -240,8 +238,27 @@ BANDS = {
|
||||||
5: [400.0, 469.9999],
|
5: [400.0, 469.9999],
|
||||||
6: [470.0, 600.0]
|
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
|
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
|
# the communication is obfuscated using this fine mechanism
|
||||||
def xorarr(data: bytes):
|
def xorarr(data: bytes):
|
||||||
|
@ -275,9 +292,11 @@ def _send_command(serport, data: bytes):
|
||||||
(len(data), util.hexprint(data)))
|
(len(data), util.hexprint(data)))
|
||||||
|
|
||||||
crc = calculate_crc16_xmodem(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:
|
if DEBUG_SHOW_OBFUSCATED_COMMANDS:
|
||||||
LOG.debug("Sending command (obfuscated):\n%s" % util.hexprint(command))
|
LOG.debug("Sending command (obfuscated):\n%s" % util.hexprint(command))
|
||||||
try:
|
try:
|
||||||
|
@ -335,16 +354,12 @@ def _receive_reply(serport):
|
||||||
|
|
||||||
|
|
||||||
def _getstring(data: bytes, begin, maxlen):
|
def _getstring(data: bytes, begin, maxlen):
|
||||||
s = ""
|
tmplen = min(maxlen+1, len(data))
|
||||||
c = 0
|
s = [data[i] for i in range(begin, tmplen)]
|
||||||
for i in data:
|
for key, val in enumerate(s):
|
||||||
c += 1
|
if val < ord(' ') or val > ord('~'):
|
||||||
if c < begin:
|
|
||||||
continue
|
|
||||||
if i < ord(' ') or i > ord('~'):
|
|
||||||
break
|
break
|
||||||
s += chr(i)
|
return ''.join(chr(x) for x in s[0:key])
|
||||||
return s
|
|
||||||
|
|
||||||
|
|
||||||
def _sayhello(serport):
|
def _sayhello(serport):
|
||||||
|
@ -362,7 +377,7 @@ def _sayhello(serport):
|
||||||
LOG.warning("Failed to initialise radio")
|
LOG.warning("Failed to initialise radio")
|
||||||
raise errors.RadioError("Failed to initialize radio")
|
raise errors.RadioError("Failed to initialize radio")
|
||||||
return False
|
return False
|
||||||
firmware = _getstring(o, 5, 16)
|
firmware = _getstring(o, 4, 16)
|
||||||
LOG.info("Found firmware: %s" % firmware)
|
LOG.info("Found firmware: %s" % firmware)
|
||||||
return 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))
|
LOG.debug("Sending readmem offset=0x%4.4x len=0x%4.4x" % (offset, length))
|
||||||
|
|
||||||
readmem = b"\x1b\x05\x08\x00" + \
|
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"
|
b"\x6a\x39\x57\x64"
|
||||||
_send_command(serport, readmem)
|
_send_command(serport, readmem)
|
||||||
o = _receive_reply(serport)
|
o = _receive_reply(serport)
|
||||||
|
@ -390,8 +405,8 @@ def _writemem(serport, data, offset):
|
||||||
(offset, len(data), util.hexprint(data)))
|
(offset, len(data), util.hexprint(data)))
|
||||||
|
|
||||||
dlen = len(data)
|
dlen = len(data)
|
||||||
writemem = b"\x1d\x05"+bytes([dlen+8])+b"\x00" + \
|
writemem = b"\x1d\x05" + \
|
||||||
bytes([offset & 0xff, (offset >> 8) & 0xff, dlen, 1]) + \
|
struct.pack("<BBHBB", dlen+8, 0, offset, dlen, 1) + \
|
||||||
b"\x6a\x39\x57\x64"+data
|
b"\x6a\x39\x57\x64"+data
|
||||||
|
|
||||||
_send_command(serport, writemem)
|
_send_command(serport, writemem)
|
||||||
|
@ -479,16 +494,20 @@ def do_upload(radio):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def _find_band(hz):
|
def _find_band(self, hz):
|
||||||
mhz = hz/1000000.0
|
mhz = hz/1000000.0
|
||||||
for a in BANDS:
|
if self.FIRMWARE_NOLIMITS:
|
||||||
if mhz >= BANDS[a][0] and mhz <= BANDS[a][1]:
|
B = BANDS_NOLIMITS
|
||||||
|
else:
|
||||||
|
B = BANDS
|
||||||
|
for a in B:
|
||||||
|
if mhz >= B[a][0] and mhz <= B[a][1]:
|
||||||
return a
|
return a
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
@directory.register
|
@directory.register
|
||||||
class TemplateRadio(chirp_common.CloneModeRadio):
|
class UVK5Radio(chirp_common.CloneModeRadio):
|
||||||
"""Quansheng UV-K5"""
|
"""Quansheng UV-K5"""
|
||||||
VENDOR = "Quansheng"
|
VENDOR = "Quansheng"
|
||||||
MODEL = "UV-K5"
|
MODEL = "UV-K5"
|
||||||
|
@ -496,6 +515,7 @@ class TemplateRadio(chirp_common.CloneModeRadio):
|
||||||
|
|
||||||
NEEDS_COMPAT_SERIAL = False
|
NEEDS_COMPAT_SERIAL = False
|
||||||
FIRMWARE_VERSION = ""
|
FIRMWARE_VERSION = ""
|
||||||
|
FIRMWARE_NOLIMITS = False
|
||||||
|
|
||||||
def get_prompts(x=None):
|
def get_prompts(x=None):
|
||||||
rp = chirp_common.RadioPrompts()
|
rp = chirp_common.RadioPrompts()
|
||||||
|
@ -545,7 +565,7 @@ class TemplateRadio(chirp_common.CloneModeRadio):
|
||||||
"->Tone", "->DTCS", "DTCS->", "DTCS->DTCS"]
|
"->Tone", "->DTCS", "DTCS->", "DTCS->DTCS"]
|
||||||
|
|
||||||
rf.valid_characters = chirp_common.CHARSET_ASCII
|
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_tmodes = ["", "Tone", "TSQL", "DTCS", "Cross"]
|
||||||
|
|
||||||
rf.valid_skips = [""]
|
rf.valid_skips = [""]
|
||||||
|
@ -675,8 +695,18 @@ class TemplateRadio(chirp_common.CloneModeRadio):
|
||||||
|
|
||||||
mem.number = number2
|
mem.number = number2
|
||||||
|
|
||||||
|
is_empty = False
|
||||||
# We'll consider any blank (i.e. 0MHz frequency) to be empty
|
# We'll consider any blank (i.e. 0MHz frequency) to be empty
|
||||||
if (_mem.freq == 0xffffffff) or (_mem.freq == 0):
|
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
|
mem.empty = True
|
||||||
# set some sane defaults:
|
# set some sane defaults:
|
||||||
mem.power = UVK5_POWER_LEVELS[2]
|
mem.power = UVK5_POWER_LEVELS[2]
|
||||||
|
@ -704,7 +734,7 @@ class TemplateRadio(chirp_common.CloneModeRadio):
|
||||||
return mem
|
return mem
|
||||||
|
|
||||||
if number > 199:
|
if number > 199:
|
||||||
mem.name = "VFO_"+str(number-199)
|
mem.name = VFO_CHANNEL_NAMES[number-200]
|
||||||
mem.immutable = ["name"]
|
mem.immutable = ["name"]
|
||||||
else:
|
else:
|
||||||
_mem2 = self._memobj.channelname[number]
|
_mem2 = self._memobj.channelname[number]
|
||||||
|
@ -733,9 +763,10 @@ class TemplateRadio(chirp_common.CloneModeRadio):
|
||||||
|
|
||||||
# mode
|
# mode
|
||||||
if (_mem.flags1 & FLAGS1_ISAM) > 0:
|
if (_mem.flags1 & FLAGS1_ISAM) > 0:
|
||||||
# Actually not sure if internally there aren't "Narrow AM"
|
if (_mem.flags2 & FLAGS2_BANDWIDTH) > 0:
|
||||||
# and "Wide AM" modes. To be investigated.
|
mem.mode = "NAM"
|
||||||
mem.mode = "AM"
|
else:
|
||||||
|
mem.mode = "AM"
|
||||||
else:
|
else:
|
||||||
if (_mem.flags2 & FLAGS2_BANDWIDTH) > 0:
|
if (_mem.flags2 & FLAGS2_BANDWIDTH) > 0:
|
||||||
mem.mode = "NFM"
|
mem.mode = "NFM"
|
||||||
|
@ -1019,12 +1050,15 @@ class TemplateRadio(chirp_common.CloneModeRadio):
|
||||||
basic.append(rs)
|
basic.append(rs)
|
||||||
|
|
||||||
# Battery save
|
# Battery save
|
||||||
|
tmpbatsave = _mem.battery_save
|
||||||
|
if tmpbatsave >= len(BATSAVE_LIST):
|
||||||
|
tmpbatsave = BATSAVE_LIST.index("1:4")
|
||||||
rs = RadioSetting(
|
rs = RadioSetting(
|
||||||
"battery_save",
|
"battery_save",
|
||||||
"Battery Save",
|
"Battery Save",
|
||||||
RadioSettingValueList(
|
RadioSettingValueList(
|
||||||
BATSAVE_LIST,
|
BATSAVE_LIST,
|
||||||
BATSAVE_LIST[_mem.battery_save]))
|
BATSAVE_LIST[tmpbatsave]))
|
||||||
basic.append(rs)
|
basic.append(rs)
|
||||||
|
|
||||||
# Dual watch
|
# Dual watch
|
||||||
|
@ -1207,6 +1241,13 @@ class TemplateRadio(chirp_common.CloneModeRadio):
|
||||||
rs = RadioSetting("driver_ver", "Driver version", val)
|
rs = RadioSetting("driver_ver", "Driver version", val)
|
||||||
roinfo.append(rs)
|
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
|
return top
|
||||||
|
|
||||||
# Store details about a high-level memory to the memory map
|
# Store details about a high-level memory to the memory map
|
||||||
|
@ -1246,23 +1287,38 @@ class TemplateRadio(chirp_common.CloneModeRadio):
|
||||||
if number < 200:
|
if number < 200:
|
||||||
_mem4.channel_attributes[number] = 0x0f
|
_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
|
# 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:
|
if band is False:
|
||||||
# raise errors.RadioError(
|
|
||||||
# "Frequency is outside the supported bands")
|
|
||||||
return mem
|
return mem
|
||||||
|
|
||||||
# mode
|
# mode
|
||||||
if mem.mode == "AM":
|
if mem.mode == "NFM":
|
||||||
_mem.flags1 = _mem.flags1 | FLAGS1_ISAM
|
_mem.flags2 = _mem.flags2 | FLAGS2_BANDWIDTH
|
||||||
_mem.flags2 = _mem.flags2 & ~FLAGS2_BANDWIDTH
|
|
||||||
else:
|
|
||||||
_mem.flags1 = _mem.flags1 & ~FLAGS1_ISAM
|
_mem.flags1 = _mem.flags1 & ~FLAGS1_ISAM
|
||||||
if mem.mode == "NFM":
|
elif mem.mode == "FM":
|
||||||
_mem.flags2 = _mem.flags2 | FLAGS2_BANDWIDTH
|
_mem.flags2 = _mem.flags2 & ~FLAGS2_BANDWIDTH
|
||||||
else:
|
_mem.flags1 = _mem.flags1 & ~FLAGS1_ISAM
|
||||||
_mem.flags2 = _mem.flags2 & ~FLAGS2_BANDWIDTH
|
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
|
# frequency/offset
|
||||||
_mem.freq = mem.freq/10
|
_mem.freq = mem.freq/10
|
||||||
|
@ -1337,3 +1393,19 @@ class TemplateRadio(chirp_common.CloneModeRadio):
|
||||||
_mem.scrambler & 0xf0) | SCRAMBLER_LIST.index(svalue)
|
_mem.scrambler & 0xf0) | SCRAMBLER_LIST.index(svalue)
|
||||||
|
|
||||||
return mem
|
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