fix eeprom offsets, convert VFO channels to be special channels, change unlock settings names, and general cleanups

main
sq5bpf 2023-06-26 12:57:34 +02:00
rodzic 8427c0618f
commit 11f26a2fe6
1 zmienionych plików z 100 dodań i 46 usunięć

146
uvk5.py
Wyświetl plik

@ -8,9 +8,7 @@
# https://github.com/sq5bpf/uvk5-reverse-engineering # https://github.com/sq5bpf/uvk5-reverse-engineering
# #
# Warning: this driver is experimental, it may brick your radio, # Warning: this driver is experimental, it may brick your radio,
# eat your lunch and mess up your configuration. Before even attempting # eat your lunch and mess up your configuration.
# to use it save a memory image from the radio using k5prog:
# https://github.com/sq5bpf/k5prog
# #
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
@ -47,6 +45,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
# TODO: remove the driver version when it's in mainline chirp
DRIVER_VERSION = "Quansheng UV-K5 driver v20230621 (c) Jacek Lipkowski SQ5BPF" DRIVER_VERSION = "Quansheng UV-K5 driver v20230621 (c) Jacek Lipkowski SQ5BPF"
MEM_FORMAT = """ MEM_FORMAT = """
@ -112,8 +111,8 @@ u8 call_channel;
u8 squelch; u8 squelch;
u8 max_talk_time; u8 max_talk_time;
u8 noaa_autoscan; u8 noaa_autoscan;
u8 unknown1; u8 key_lock;
u8 unknown2; u8 vox_switch;
u8 vox_level; u8 vox_level;
u8 mic_gain; u8 mic_gain;
u8 unknown3; u8 unknown3;
@ -121,6 +120,7 @@ u8 channel_display_mode;
u8 crossband; u8 crossband;
u8 battery_save; u8 battery_save;
u8 dual_watch; u8 dual_watch;
u8 backlight_auto_mode;
u8 tail_note_elimination; u8 tail_note_elimination;
u8 vfo_open; u8 vfo_open;
@ -193,7 +193,7 @@ u8 int_unknown1;
u8 int_200tx; u8 int_200tx;
u8 int_500tx; u8 int_500tx;
u8 int_350en; u8 int_350en;
u8 int_screen; u8 int_scren;
#seekto 0xf50; #seekto 0xf50;
struct { struct {
@ -241,6 +241,9 @@ CHANNELDISP_LIST = ["Frequency", "Channel No", "Channel Name"]
# battery save # battery save
BATSAVE_LIST = ["OFF", "1:1", "1:2", "1:3", "1:4"] BATSAVE_LIST = ["OFF", "1:1", "1:2", "1:3", "1:4"]
# Backlight auto mode
BACKLIGHT_LIST = ["Off", "1s", "2s", "3s", "4s", "5s"]
# Crossband receiving/transmitting # Crossband receiving/transmitting
CROSSBAND_LIST = ["Off", "Band A", "Band B"] CROSSBAND_LIST = ["Off", "Band A", "Band B"]
DUALWATCH_LIST = CROSSBAND_LIST DUALWATCH_LIST = CROSSBAND_LIST
@ -324,6 +327,23 @@ BANDS_NOLIMITS = {
6: [470.0, 1300.0] 6: [470.0, 1300.0]
} }
SPECIALS = {
"F1(50M-76M)A": 200,
"F1(50M-76M)B": 201,
"F2(108M-136M)A": 202,
"F2(108M-136M)B": 203,
"F3(136M-174M)A": 204,
"F3(136M-174M)B": 205,
"F4(174M-350M)A": 206,
"F4(174M-350M)B": 207,
"F5(350M-400M)A": 208,
"F5(350M-400M)B": 209,
"F6(400M-470M)A": 210,
"F6(400M-470M)B": 211,
"F7(470M-600M)A": 212,
"F7(470M-600M)B": 213
}
VFO_CHANNEL_NAMES = ["F1(50M-76M)A", "F1(50M-76M)B", VFO_CHANNEL_NAMES = ["F1(50M-76M)A", "F1(50M-76M)B",
"F2(108M-136M)A", "F2(108M-136M)B", "F2(108M-136M)A", "F2(108M-136M)B",
"F3(136M-174M)A", "F3(136M-174M)B", "F3(136M-174M)A", "F3(136M-174M)B",
@ -403,8 +423,6 @@ def _receive_reply(serport):
(util.hexprint(header), len(header))) (util.hexprint(header), len(header)))
raise errors.RadioError("Bad response header") raise errors.RadioError("Bad response header")
return False
cmd = serport.read(int(header[2])) cmd = serport.read(int(header[2]))
if len(cmd) != int(header[2]): if len(cmd) != int(header[2]):
LOG.warning("Body short read: [%s] len=%i" % LOG.warning("Body short read: [%s] len=%i" %
@ -425,7 +443,6 @@ def _receive_reply(serport):
LOG.warning("Bad response footer: %s len=%i" % LOG.warning("Bad response footer: %s len=%i" %
(util.hexprint(footer), len(footer))) (util.hexprint(footer), len(footer)))
raise errors.RadioError("Bad response footer") raise errors.RadioError("Bad response footer")
return False
if DEBUG_SHOW_OBFUSCATED_COMMANDS: if DEBUG_SHOW_OBFUSCATED_COMMANDS:
LOG.debug("Received reply (obfuscated) len=0x%4.4x:\n%s" % LOG.debug("Received reply (obfuscated) len=0x%4.4x:\n%s" %
@ -452,7 +469,7 @@ def _sayhello(serport):
hellopacket = b"\x14\x05\x04\x00\x6a\x39\x57\x64" hellopacket = b"\x14\x05\x04\x00\x6a\x39\x57\x64"
tries = 5 tries = 5
while (True): while True:
LOG.debug("Sending hello packet") LOG.debug("Sending hello packet")
_send_command(serport, hellopacket) _send_command(serport, hellopacket)
o = _receive_reply(serport) o = _receive_reply(serport)
@ -462,7 +479,6 @@ def _sayhello(serport):
if tries == 0: if tries == 0:
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
firmware = _getstring(o, 4, 16) firmware = _getstring(o, 4, 16)
LOG.info("Found firmware: %s" % firmware) LOG.info("Found firmware: %s" % firmware)
return firmware return firmware
@ -509,7 +525,6 @@ def _writemem(serport, data, offset):
else: else:
LOG.warning("Bad data from writemem") LOG.warning("Bad data from writemem")
raise errors.RadioError("Bad response to writemem") raise errors.RadioError("Bad response to writemem")
return False
def _resetradio(serport): def _resetradio(serport):
@ -610,10 +625,10 @@ class UVK5Radio(chirp_common.CloneModeRadio):
def get_prompts(x=None): def get_prompts(x=None):
rp = chirp_common.RadioPrompts() rp = chirp_common.RadioPrompts()
rp.experimental = \ rp.experimental = \
('This is an experimental driver for the Quanscheng UV-K5. ' ('This is an experimental driver for the Quansheng UV-K5. '
'It may harm your radio, or worse. Use at your own risk.\n\n' 'It may harm your radio, or worse. Use at your own risk.\n\n'
'Before attempting to do any changes please download' 'Before attempting to do any changes please download'
'the memory image from the radio with chirp or k5prog ' 'the memory image from the radio with chirp '
'and keep it. This can be later used to recover the ' 'and keep it. This can be later used to recover the '
'original settings. \n\n' 'original settings. \n\n'
'some details are not yet implemented') 'some details are not yet implemented')
@ -622,14 +637,14 @@ class UVK5Radio(chirp_common.CloneModeRadio):
"2. Connect cable to mic/spkr connector.\n" "2. Connect cable to mic/spkr connector.\n"
"3. Make sure connector is firmly connected.\n" "3. Make sure connector is firmly connected.\n"
"4. Click OK to download image from device.\n\n" "4. Click OK to download image from device.\n\n"
"It will may not work if you turn o the radio " "It will may not work if you turn on the radio "
"with the cable already attached\n") "with the cable already attached\n")
rp.pre_upload = _( rp.pre_upload = _(
"1. Turn radio on.\n" "1. Turn radio on.\n"
"2. Connect cable to mic/spkr connector.\n" "2. Connect cable to mic/spkr connector.\n"
"3. Make sure connector is firmly connected.\n" "3. Make sure connector is firmly connected.\n"
"4. Click OK to upload the image to device.\n\n" "4. Click OK to upload the image to device.\n\n"
"It will may not work if you turn o the radio " "It will may not work if you turn on the radio "
"with the cable already attached") "with the cable already attached")
return rp return rp
@ -643,8 +658,9 @@ class UVK5Radio(chirp_common.CloneModeRadio):
rf.has_ctone = True rf.has_ctone = True
rf.has_settings = True rf.has_settings = True
rf.has_comment = False rf.has_comment = False
rf.valid_name_length = 16 rf.valid_name_length = 10
rf.valid_power_levels = UVK5_POWER_LEVELS rf.valid_power_levels = UVK5_POWER_LEVELS
rf.valid_special_chans = list(SPECIALS.keys())
# hack so we can input any frequency, # hack so we can input any frequency,
# the 0.1 and 0.01 steps don't work unfortunately # the 0.1 and 0.01 steps don't work unfortunately
@ -661,7 +677,7 @@ class UVK5Radio(chirp_common.CloneModeRadio):
rf.valid_skips = [""] rf.valid_skips = [""]
# This radio supports memories 1-200, 201-214 are the VFO memories # This radio supports memories 1-200, 201-214 are the VFO memories
rf.memory_bounds = (1, 214) rf.memory_bounds = (1, 200)
# This is what the BK4819 chip supports # This is what the BK4819 chip supports
# Will leave it in a comment, might be useful someday # Will leave it in a comment, might be useful someday
@ -770,22 +786,21 @@ class UVK5Radio(chirp_common.CloneModeRadio):
# Extract a high-level memory object from the low-level memory map # Extract a high-level memory object from the low-level memory map
# This is called to populate a memory in the UI # This is called to populate a memory in the UI
def get_memory(self, number2): def get_memory(self, number2):
number = number2-1 # in the radio memories start with 0
mem = chirp_common.Memory() mem = chirp_common.Memory()
# cutting and pasting configs from different radios if isinstance(number2, str):
# might try to set channel 0 number = SPECIALS[number2]
if number2 == 0: mem.extd_number = number2
LOG.warning("Attempt to get channel 0") else:
return mem number = number2 - 1
mem.number = number + 1
_mem = self._memobj.channel[number] _mem = self._memobj.channel[number]
tmpcomment = "" tmpcomment = ""
mem.number = number2
is_empty = False 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):
@ -963,10 +978,15 @@ class UVK5Radio(chirp_common.CloneModeRadio):
# TOT # TOT
if element.get_name() == "tot": if element.get_name() == "tot":
_mem.max_talk_time = int(element.value) _mem.max_talk_time = int(element.value)
# NOAA autoscan # NOAA autoscan
if element.get_name() == "noaa_autoscan": if element.get_name() == "noaa_autoscan":
_mem.noaa_autoscan = element.value and 1 or 0 _mem.noaa_autoscan = element.value and 1 or 0
# VOX switch
if element.get_name() == "vox_switch":
_mem.vox_switch = element.value and 1 or 0
# vox level # vox level
if element.get_name() == "vox_level": if element.get_name() == "vox_level":
_mem.vox_level = int(element.value)-1 _mem.vox_level = int(element.value)-1
@ -991,6 +1011,11 @@ class UVK5Radio(chirp_common.CloneModeRadio):
if element.get_name() == "dualwatch": if element.get_name() == "dualwatch":
_mem.dual_watch = DUALWATCH_LIST.index(str(element.value)) _mem.dual_watch = DUALWATCH_LIST.index(str(element.value))
# Backlight auto mode
if element.get_name() == "backlight_auto_mode":
_mem.backlight_auto_mode = \
BACKLIGHT_LIST.index(str(element.value))
# Tail tone elimination # Tail tone elimination
if element.get_name() == "tail_note_elimination": if element.get_name() == "tail_note_elimination":
_mem.tail_note_elimination = element.value and 1 or 0 _mem.tail_note_elimination = element.value and 1 or 0
@ -1008,6 +1033,10 @@ class UVK5Radio(chirp_common.CloneModeRadio):
_mem.scan_resume_mode = SCANRESUME_LIST.index( _mem.scan_resume_mode = SCANRESUME_LIST.index(
str(element.value)) str(element.value))
# Keypad lock
if element.get_name() == "key_lock":
_mem.key_lock = element.value and 1 or 0
# Auto keypad lock # Auto keypad lock
if element.get_name() == "auto_keypad_lock": if element.get_name() == "auto_keypad_lock":
_mem.auto_keypad_lock = element.value and 1 or 0 _mem.auto_keypad_lock = element.value and 1 or 0
@ -1058,10 +1087,6 @@ class UVK5Radio(chirp_common.CloneModeRadio):
if element.get_name() == "350tx": if element.get_name() == "350tx":
_mem.int_350tx = element.value and 1 or 0 _mem.int_350tx = element.value and 1 or 0
# UNKNOWN1
if element.get_name() == "unknown1":
_mem.int_unknown1 = element.value and 1 or 0
# 200TX # 200TX
if element.get_name() == "200tx": if element.get_name() == "200tx":
_mem.int_200tx = element.value and 1 or 0 _mem.int_200tx = element.value and 1 or 0
@ -1074,6 +1099,10 @@ class UVK5Radio(chirp_common.CloneModeRadio):
if element.get_name() == "350en": if element.get_name() == "350en":
_mem.int_350en = element.value and 1 or 0 _mem.int_350en = element.value and 1 or 0
# SCREN
if element.get_name() == "scren":
_mem.int_scren = element.value and 1 or 0
# fm radio # fm radio
for i in range(1, 21): for i in range(1, 21):
freqname = "FM_" + str(i) freqname = "FM_" + str(i)
@ -1548,6 +1577,13 @@ class UVK5Radio(chirp_common.CloneModeRadio):
bool(_mem.noaa_autoscan > 0))) bool(_mem.noaa_autoscan > 0)))
basic.append(rs) basic.append(rs)
# VOX switch
rs = RadioSetting(
"vox_switch",
"VOX enabled", RadioSettingValueBoolean(
bool(_mem.vox_switch > 0)))
basic.append(rs)
# VOX Level # VOX Level
tmpvox = _mem.vox_level+1 tmpvox = _mem.vox_level+1
if tmpvox > 10: if tmpvox > 10:
@ -1608,6 +1644,17 @@ class UVK5Radio(chirp_common.CloneModeRadio):
DUALWATCH_LIST, DUALWATCH_LIST[tmpdual])) DUALWATCH_LIST, DUALWATCH_LIST[tmpdual]))
basic.append(rs) basic.append(rs)
# Backlight auto mode
tmpback = _mem.backlight_auto_mode
if tmpback >= len(BACKLIGHT_LIST):
tmpback = 0
rs = RadioSetting("backlight_auto_mode",
"Backlight auto mode",
RadioSettingValueList(
BACKLIGHT_LIST,
BACKLIGHT_LIST[tmpback]))
basic.append(rs)
# Tail tone elimination # Tail tone elimination
rs = RadioSetting( rs = RadioSetting(
"tail_note_elimination", "tail_note_elimination",
@ -1640,6 +1687,13 @@ class UVK5Radio(chirp_common.CloneModeRadio):
SCANRESUME_LIST[tmpscanres])) SCANRESUME_LIST[tmpscanres]))
basic.append(rs) basic.append(rs)
# Keypad locked
rs = RadioSetting(
"key_lock",
"Keypad lock",
RadioSettingValueBoolean(bool(_mem.key_lock > 0)))
basic.append(rs)
# Auto keypad lock # Auto keypad lock
rs = RadioSetting( rs = RadioSetting(
"auto_keypad_lock", "auto_keypad_lock",
@ -1744,34 +1798,33 @@ class UVK5Radio(chirp_common.CloneModeRadio):
unlock.append(rs) unlock.append(rs)
# 350TX # 350TX
rs = RadioSetting("350tx", "350TX", RadioSettingValueBoolean( rs = RadioSetting("350tx", "350TX - unlock 350-400MHz TX",
bool(_mem.int_350tx > 0)))
unlock.append(rs)
# unknown1
rs = RadioSetting("unknown11", "UNKNOWN1",
RadioSettingValueBoolean( RadioSettingValueBoolean(
bool(_mem.int_unknown1 > 0))) bool(_mem.int_350tx > 0)))
unlock.append(rs) unlock.append(rs)
# 200TX # 200TX
rs = RadioSetting("200tx", "200TX", RadioSettingValueBoolean( rs = RadioSetting("200tx", "200TX - unlock 174-350MHz TX",
bool(_mem.int_200tx > 0))) RadioSettingValueBoolean(
bool(_mem.int_200tx > 0)))
unlock.append(rs) unlock.append(rs)
# 500TX # 500TX
rs = RadioSetting("500tx", "500TX", RadioSettingValueBoolean( rs = RadioSetting("500tx", "500TX - unlock 500-600MHz TX",
bool(_mem.int_500tx > 0))) RadioSettingValueBoolean(
bool(_mem.int_500tx > 0)))
unlock.append(rs) unlock.append(rs)
# 350EN # 350EN
rs = RadioSetting("350en", "350EN", RadioSettingValueBoolean( rs = RadioSetting("350en", "350EN - unlock 350-400MHz RX",
bool(_mem.int_350en > 0))) RadioSettingValueBoolean(
bool(_mem.int_350en > 0)))
unlock.append(rs) unlock.append(rs)
# SCREEN # SCREEN
rs = RadioSetting("screen", "SCREEN", RadioSettingValueBoolean( rs = RadioSetting("scren", "SCREN - scrambler enable",
bool(_mem.int_screen > 0))) RadioSettingValueBoolean(
bool(_mem.int_scren > 0)))
unlock.append(rs) unlock.append(rs)
# readonly info # readonly info
@ -1787,6 +1840,7 @@ class UVK5Radio(chirp_common.CloneModeRadio):
rs = RadioSetting("fw_ver", "Firmware Version", val) rs = RadioSetting("fw_ver", "Firmware Version", val)
roinfo.append(rs) roinfo.append(rs)
# TODO: remove showing the driver version when it's in mainline chirp
# Driver version # Driver version
val = RadioSettingValueString(0, 128, DRIVER_VERSION) val = RadioSettingValueString(0, 128, DRIVER_VERSION)
val.set_mutable(False) val.set_mutable(False)
@ -1902,7 +1956,7 @@ class UVK5Radio(chirp_common.CloneModeRadio):
# channels >200 are the 14 VFO chanells and don't have names # channels >200 are the 14 VFO chanells and don't have names
if number < 200: if number < 200:
_mem2 = self._memobj.channelname[number] _mem2 = self._memobj.channelname[number]
tag = mem.name.ljust(16)[:16] tag = mem.name.ljust(10) + "\x00"*6
_mem2.name = tag # Store the alpha tag _mem2.name = tag # Store the alpha tag
# tone data # tone data