kopia lustrzana https://github.com/sq5bpf/uvk5-reverse-engineering
Porównaj commity
7 Commity
8427c0618f
...
d30a1aa68a
Autor | SHA1 | Data |
---|---|---|
sq5bpf | d30a1aa68a | |
sq5bpf | 389cf2e335 | |
sq5bpf | 74a864f5dd | |
sq5bpf | 351cf7fd6e | |
sq5bpf | dfe60eb6d3 | |
sq5bpf | 7a781c7ba8 | |
sq5bpf | 11f26a2fe6 |
Plik binarny nie jest wyświetlany.
202
uvk5.py
202
uvk5.py
|
@ -8,9 +8,7 @@
|
|||
# https://github.com/sq5bpf/uvk5-reverse-engineering
|
||||
#
|
||||
# Warning: this driver is experimental, it may brick your radio,
|
||||
# eat your lunch and mess up your configuration. Before even attempting
|
||||
# to use it save a memory image from the radio using k5prog:
|
||||
# https://github.com/sq5bpf/k5prog
|
||||
# eat your lunch and mess up your configuration.
|
||||
#
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
|
@ -47,7 +45,8 @@ 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 v20230621 (c) Jacek Lipkowski SQ5BPF"
|
||||
# TODO: remove the driver version when it's in mainline chirp
|
||||
DRIVER_VERSION = "Quansheng UV-K5 driver v20230626 (c) Jacek Lipkowski SQ5BPF"
|
||||
|
||||
MEM_FORMAT = """
|
||||
#seekto 0x0000;
|
||||
|
@ -112,8 +111,8 @@ u8 call_channel;
|
|||
u8 squelch;
|
||||
u8 max_talk_time;
|
||||
u8 noaa_autoscan;
|
||||
u8 unknown1;
|
||||
u8 unknown2;
|
||||
u8 key_lock;
|
||||
u8 vox_switch;
|
||||
u8 vox_level;
|
||||
u8 mic_gain;
|
||||
u8 unknown3;
|
||||
|
@ -121,6 +120,7 @@ u8 channel_display_mode;
|
|||
u8 crossband;
|
||||
u8 battery_save;
|
||||
u8 dual_watch;
|
||||
u8 backlight_auto_mode;
|
||||
u8 tail_note_elimination;
|
||||
u8 vfo_open;
|
||||
|
||||
|
@ -193,7 +193,7 @@ u8 int_unknown1;
|
|||
u8 int_200tx;
|
||||
u8 int_500tx;
|
||||
u8 int_350en;
|
||||
u8 int_screen;
|
||||
u8 int_scren;
|
||||
|
||||
#seekto 0xf50;
|
||||
struct {
|
||||
|
@ -241,6 +241,9 @@ CHANNELDISP_LIST = ["Frequency", "Channel No", "Channel Name"]
|
|||
# battery save
|
||||
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_LIST = ["Off", "Band A", "Band B"]
|
||||
DUALWATCH_LIST = CROSSBAND_LIST
|
||||
|
@ -324,6 +327,23 @@ BANDS_NOLIMITS = {
|
|||
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",
|
||||
"F2(108M-136M)A", "F2(108M-136M)B",
|
||||
"F3(136M-174M)A", "F3(136M-174M)B",
|
||||
|
@ -403,8 +423,6 @@ def _receive_reply(serport):
|
|||
(util.hexprint(header), len(header)))
|
||||
raise errors.RadioError("Bad response header")
|
||||
|
||||
return False
|
||||
|
||||
cmd = serport.read(int(header[2]))
|
||||
if len(cmd) != int(header[2]):
|
||||
LOG.warning("Body short read: [%s] len=%i" %
|
||||
|
@ -425,7 +443,6 @@ def _receive_reply(serport):
|
|||
LOG.warning("Bad response footer: %s len=%i" %
|
||||
(util.hexprint(footer), len(footer)))
|
||||
raise errors.RadioError("Bad response footer")
|
||||
return False
|
||||
|
||||
if DEBUG_SHOW_OBFUSCATED_COMMANDS:
|
||||
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"
|
||||
|
||||
tries = 5
|
||||
while (True):
|
||||
while True:
|
||||
LOG.debug("Sending hello packet")
|
||||
_send_command(serport, hellopacket)
|
||||
o = _receive_reply(serport)
|
||||
|
@ -462,7 +479,6 @@ def _sayhello(serport):
|
|||
if tries == 0:
|
||||
LOG.warning("Failed to initialise radio")
|
||||
raise errors.RadioError("Failed to initialize radio")
|
||||
return False
|
||||
firmware = _getstring(o, 4, 16)
|
||||
LOG.info("Found firmware: %s" % firmware)
|
||||
return firmware
|
||||
|
@ -509,7 +525,6 @@ def _writemem(serport, data, offset):
|
|||
else:
|
||||
LOG.warning("Bad data from writemem")
|
||||
raise errors.RadioError("Bad response to writemem")
|
||||
return False
|
||||
|
||||
|
||||
def _resetradio(serport):
|
||||
|
@ -610,10 +625,10 @@ class UVK5Radio(chirp_common.CloneModeRadio):
|
|||
def get_prompts(x=None):
|
||||
rp = chirp_common.RadioPrompts()
|
||||
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'
|
||||
'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 '
|
||||
'original settings. \n\n'
|
||||
'some details are not yet implemented')
|
||||
|
@ -622,14 +637,14 @@ class UVK5Radio(chirp_common.CloneModeRadio):
|
|||
"2. Connect cable to mic/spkr connector.\n"
|
||||
"3. Make sure connector is firmly connected.\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")
|
||||
rp.pre_upload = _(
|
||||
"1. Turn radio on.\n"
|
||||
"2. Connect cable to mic/spkr connector.\n"
|
||||
"3. Make sure connector is firmly connected.\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")
|
||||
return rp
|
||||
|
||||
|
@ -643,8 +658,9 @@ class UVK5Radio(chirp_common.CloneModeRadio):
|
|||
rf.has_ctone = True
|
||||
rf.has_settings = True
|
||||
rf.has_comment = False
|
||||
rf.valid_name_length = 16
|
||||
rf.valid_name_length = 10
|
||||
rf.valid_power_levels = UVK5_POWER_LEVELS
|
||||
rf.valid_special_chans = list(SPECIALS.keys())
|
||||
|
||||
# hack so we can input any frequency,
|
||||
# the 0.1 and 0.01 steps don't work unfortunately
|
||||
|
@ -661,7 +677,7 @@ class UVK5Radio(chirp_common.CloneModeRadio):
|
|||
rf.valid_skips = [""]
|
||||
|
||||
# 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
|
||||
# Will leave it in a comment, might be useful someday
|
||||
|
@ -694,6 +710,28 @@ class UVK5Radio(chirp_common.CloneModeRadio):
|
|||
|
||||
def validate_memory(self, mem):
|
||||
msgs = super().validate_memory(mem)
|
||||
|
||||
# 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(self, txfreq)
|
||||
if band is False:
|
||||
msg = "Transmit frequency %.4fMHz is not supported by this radio" \
|
||||
% (txfreq/1000000.0)
|
||||
msgs.append(chirp_common.ValidationWarning(msg))
|
||||
|
||||
band = _find_band(self, mem.freq)
|
||||
if band is False:
|
||||
msg = "The frequency %.4fMHz is not supported by this radio" \
|
||||
% (mem.freq/1000000.0)
|
||||
msgs.append(chirp_common.ValidationWarning(msg))
|
||||
|
||||
return msgs
|
||||
|
||||
def _set_tone(self, mem, _mem):
|
||||
|
@ -770,22 +808,21 @@ class UVK5Radio(chirp_common.CloneModeRadio):
|
|||
# Extract a high-level memory object from the low-level memory map
|
||||
# This is called to populate a memory in the UI
|
||||
def get_memory(self, number2):
|
||||
number = number2-1 # in the radio memories start with 0
|
||||
|
||||
mem = chirp_common.Memory()
|
||||
|
||||
# cutting and pasting configs from different radios
|
||||
# might try to set channel 0
|
||||
if number2 == 0:
|
||||
LOG.warning("Attempt to get channel 0")
|
||||
return mem
|
||||
if isinstance(number2, str):
|
||||
number = SPECIALS[number2]
|
||||
mem.extd_number = number2
|
||||
else:
|
||||
number = number2 - 1
|
||||
|
||||
mem.number = number + 1
|
||||
|
||||
_mem = self._memobj.channel[number]
|
||||
|
||||
tmpcomment = ""
|
||||
|
||||
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):
|
||||
|
@ -963,10 +1000,15 @@ class UVK5Radio(chirp_common.CloneModeRadio):
|
|||
# TOT
|
||||
if element.get_name() == "tot":
|
||||
_mem.max_talk_time = int(element.value)
|
||||
|
||||
# NOAA autoscan
|
||||
if element.get_name() == "noaa_autoscan":
|
||||
_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
|
||||
if element.get_name() == "vox_level":
|
||||
_mem.vox_level = int(element.value)-1
|
||||
|
@ -991,6 +1033,11 @@ class UVK5Radio(chirp_common.CloneModeRadio):
|
|||
if element.get_name() == "dualwatch":
|
||||
_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
|
||||
if element.get_name() == "tail_note_elimination":
|
||||
_mem.tail_note_elimination = element.value and 1 or 0
|
||||
|
@ -1008,6 +1055,10 @@ class UVK5Radio(chirp_common.CloneModeRadio):
|
|||
_mem.scan_resume_mode = SCANRESUME_LIST.index(
|
||||
str(element.value))
|
||||
|
||||
# Keypad lock
|
||||
if element.get_name() == "key_lock":
|
||||
_mem.key_lock = element.value and 1 or 0
|
||||
|
||||
# Auto keypad lock
|
||||
if element.get_name() == "auto_keypad_lock":
|
||||
_mem.auto_keypad_lock = element.value and 1 or 0
|
||||
|
@ -1041,12 +1092,12 @@ class UVK5Radio(chirp_common.CloneModeRadio):
|
|||
# Logo string 1
|
||||
if element.get_name() == "logo1":
|
||||
b = str(element.value).rstrip("\x20\xff\x00")+"\x00"*12
|
||||
_mem.logo_line1 = b[0:12]+"\xff\xff\xff\xff"
|
||||
_mem.logo_line1 = b[0:12]+"\x00\xff\xff\xff"
|
||||
|
||||
# Logo string 2
|
||||
if element.get_name() == "logo2":
|
||||
b = str(element.value).rstrip("\x20\xff\x00")+"\x00"*12
|
||||
_mem.logo_line2 = b[0:12]+"\xff\xff\xff\xff"
|
||||
_mem.logo_line2 = b[0:12]+"\x00\xff\xff\xff"
|
||||
|
||||
# unlock settings
|
||||
|
||||
|
@ -1058,10 +1109,6 @@ class UVK5Radio(chirp_common.CloneModeRadio):
|
|||
if element.get_name() == "350tx":
|
||||
_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
|
||||
if element.get_name() == "200tx":
|
||||
_mem.int_200tx = element.value and 1 or 0
|
||||
|
@ -1074,6 +1121,10 @@ class UVK5Radio(chirp_common.CloneModeRadio):
|
|||
if element.get_name() == "350en":
|
||||
_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
|
||||
for i in range(1, 21):
|
||||
freqname = "FM_" + str(i)
|
||||
|
@ -1548,6 +1599,13 @@ class UVK5Radio(chirp_common.CloneModeRadio):
|
|||
bool(_mem.noaa_autoscan > 0)))
|
||||
basic.append(rs)
|
||||
|
||||
# VOX switch
|
||||
rs = RadioSetting(
|
||||
"vox_switch",
|
||||
"VOX enabled", RadioSettingValueBoolean(
|
||||
bool(_mem.vox_switch > 0)))
|
||||
basic.append(rs)
|
||||
|
||||
# VOX Level
|
||||
tmpvox = _mem.vox_level+1
|
||||
if tmpvox > 10:
|
||||
|
@ -1608,6 +1666,17 @@ class UVK5Radio(chirp_common.CloneModeRadio):
|
|||
DUALWATCH_LIST, DUALWATCH_LIST[tmpdual]))
|
||||
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
|
||||
rs = RadioSetting(
|
||||
"tail_note_elimination",
|
||||
|
@ -1640,6 +1709,13 @@ class UVK5Radio(chirp_common.CloneModeRadio):
|
|||
SCANRESUME_LIST[tmpscanres]))
|
||||
basic.append(rs)
|
||||
|
||||
# Keypad locked
|
||||
rs = RadioSetting(
|
||||
"key_lock",
|
||||
"Keypad lock",
|
||||
RadioSettingValueBoolean(bool(_mem.key_lock > 0)))
|
||||
basic.append(rs)
|
||||
|
||||
# Auto keypad lock
|
||||
rs = RadioSetting(
|
||||
"auto_keypad_lock",
|
||||
|
@ -1744,34 +1820,33 @@ class UVK5Radio(chirp_common.CloneModeRadio):
|
|||
unlock.append(rs)
|
||||
|
||||
# 350TX
|
||||
rs = RadioSetting("350tx", "350TX", RadioSettingValueBoolean(
|
||||
bool(_mem.int_350tx > 0)))
|
||||
unlock.append(rs)
|
||||
|
||||
# unknown1
|
||||
rs = RadioSetting("unknown11", "UNKNOWN1",
|
||||
rs = RadioSetting("350tx", "350TX - unlock 350-400MHz TX",
|
||||
RadioSettingValueBoolean(
|
||||
bool(_mem.int_unknown1 > 0)))
|
||||
bool(_mem.int_350tx > 0)))
|
||||
unlock.append(rs)
|
||||
|
||||
# 200TX
|
||||
rs = RadioSetting("200tx", "200TX", RadioSettingValueBoolean(
|
||||
bool(_mem.int_200tx > 0)))
|
||||
rs = RadioSetting("200tx", "200TX - unlock 174-350MHz TX",
|
||||
RadioSettingValueBoolean(
|
||||
bool(_mem.int_200tx > 0)))
|
||||
unlock.append(rs)
|
||||
|
||||
# 500TX
|
||||
rs = RadioSetting("500tx", "500TX", RadioSettingValueBoolean(
|
||||
bool(_mem.int_500tx > 0)))
|
||||
rs = RadioSetting("500tx", "500TX - unlock 500-600MHz TX",
|
||||
RadioSettingValueBoolean(
|
||||
bool(_mem.int_500tx > 0)))
|
||||
unlock.append(rs)
|
||||
|
||||
# 350EN
|
||||
rs = RadioSetting("350en", "350EN", RadioSettingValueBoolean(
|
||||
bool(_mem.int_350en > 0)))
|
||||
rs = RadioSetting("350en", "350EN - unlock 350-400MHz RX",
|
||||
RadioSettingValueBoolean(
|
||||
bool(_mem.int_350en > 0)))
|
||||
unlock.append(rs)
|
||||
|
||||
# SCREEN
|
||||
rs = RadioSetting("screen", "SCREEN", RadioSettingValueBoolean(
|
||||
bool(_mem.int_screen > 0)))
|
||||
rs = RadioSetting("scren", "SCREN - scrambler enable",
|
||||
RadioSettingValueBoolean(
|
||||
bool(_mem.int_scren > 0)))
|
||||
unlock.append(rs)
|
||||
|
||||
# readonly info
|
||||
|
@ -1787,6 +1862,7 @@ class UVK5Radio(chirp_common.CloneModeRadio):
|
|||
rs = RadioSetting("fw_ver", "Firmware Version", val)
|
||||
roinfo.append(rs)
|
||||
|
||||
# TODO: remove showing the driver version when it's in mainline chirp
|
||||
# Driver version
|
||||
val = RadioSettingValueString(0, 128, DRIVER_VERSION)
|
||||
val.set_mutable(False)
|
||||
|
@ -1831,12 +1907,12 @@ class UVK5Radio(chirp_common.CloneModeRadio):
|
|||
else:
|
||||
# this memory was't empty, save some bits that we don't know the
|
||||
# meaning of, or that we don't support yet
|
||||
prev_0a = ord(_mem.get_raw()[0x0a]) & SAVE_MASK_0A
|
||||
prev_0b = ord(_mem.get_raw()[0x0b]) & SAVE_MASK_0B
|
||||
prev_0c = ord(_mem.get_raw()[0x0c]) & SAVE_MASK_0C
|
||||
prev_0d = ord(_mem.get_raw()[0x0d]) & SAVE_MASK_0D
|
||||
prev_0e = ord(_mem.get_raw()[0x0e]) & SAVE_MASK_0E
|
||||
prev_0f = ord(_mem.get_raw()[0x0f]) & SAVE_MASK_0F
|
||||
prev_0a = _mem.get_raw(asbytes=True)[0x0a] & SAVE_MASK_0A
|
||||
prev_0b = _mem.get_raw(asbytes=True)[0x0b] & SAVE_MASK_0B
|
||||
prev_0c = _mem.get_raw(asbytes=True)[0x0c] & SAVE_MASK_0C
|
||||
prev_0d = _mem.get_raw(asbytes=True)[0x0d] & SAVE_MASK_0D
|
||||
prev_0e = _mem.get_raw(asbytes=True)[0x0e] & SAVE_MASK_0E
|
||||
prev_0f = _mem.get_raw(asbytes=True)[0x0f] & SAVE_MASK_0F
|
||||
_mem.set_raw("\x00" * 10 +
|
||||
chr(prev_0a) + chr(prev_0b) + chr(prev_0c) +
|
||||
chr(prev_0d) + chr(prev_0e) + chr(prev_0f))
|
||||
|
@ -1849,24 +1925,8 @@ class UVK5Radio(chirp_common.CloneModeRadio):
|
|||
_mem4.channel_attributes[number].is_free = 1
|
||||
_mem4.channel_attributes[number].band = 0x7
|
||||
|
||||
# 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(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:
|
||||
return mem
|
||||
|
||||
# mode
|
||||
if mem.mode == "NFM":
|
||||
|
@ -1902,7 +1962,7 @@ class UVK5Radio(chirp_common.CloneModeRadio):
|
|||
# channels >200 are the 14 VFO chanells and don't have names
|
||||
if number < 200:
|
||||
_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
|
||||
|
||||
# tone data
|
||||
|
|
Ładowanie…
Reference in New Issue