From 82633f7decc9c1a41cac4816d81cb9d0b88f20b6 Mon Sep 17 00:00:00 2001 From: ei2081 <175010419+ei2081@users.noreply.github.com> Date: Wed, 14 Aug 2024 23:39:27 +0100 Subject: [PATCH 1/4] Create uvk5_99.py and foldable multi-line comments ftw! --- chirp/drivers/uvk5_99.py | 2090 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 2090 insertions(+) create mode 100644 chirp/drivers/uvk5_99.py diff --git a/chirp/drivers/uvk5_99.py b/chirp/drivers/uvk5_99.py new file mode 100644 index 00000000..c9869f39 --- /dev/null +++ b/chirp/drivers/uvk5_99.py @@ -0,0 +1,2090 @@ +""" Quansheng UV-K5(99) undriver (c) 2024 dara/ei2081 +# +# based on Quansheng UV-K5 driver (c) 2023 Jacek Lipkowski +# +# based on template.py Copyright 2012 Dan Smith +# +# +# This is a preliminary version of a driver for the UV-K5 +# It is based on my reverse engineering effort described here: +# 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. +# +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# """ + + +import struct +import logging + +from chirp import chirp_common, directory, bitwise, memmap, errors, util +from chirp.settings import RadioSetting, RadioSettingGroup, \ + RadioSettingValueBoolean, RadioSettingValueList, \ + RadioSettingValueInteger, RadioSettingValueString, \ + RadioSettings + +LOG = logging.getLogger(__name__) + +# Show the obfuscated version of commands. Not needed normally, but +# might be useful for someone who is debugging a similar radio +DEBUG_SHOW_OBFUSCATED_COMMANDS = False + +# Show the memory being written/received. Not needed normally, because +# this is the same information as in the packet hexdumps, but +# might be useful for someone debugging some obscure memory issue +DEBUG_SHOW_MEMORY_ACTIONS = False + +MEM_FORMAT = """ +#seekto 0x0000; +struct { + ul32 freq; + ul32 offset; + u8 rxcode; + u8 txcode; + + u8 txcodeflag:4, + rxcodeflag:4; + + //u8 flags1; + u8 flags1_unknown7:1, + flags1_unknown6:1, + flags1_unknown5:1, + enable_am:1, + flags1_unknown3:1, + is_in_scanlist:1, + shift:2; + + //u8 flags2; + u8 flags2_unknown7:1, + flags2_unknown6:1, + flags2_unknown5:1, + bclo:1, + txpower:2, + bandwidth:1, + freq_reverse:1; + + //u8 dtmf_flags; + u8 dtmf_flags_unknown7:1, + dtmf_flags_unknown6:1, + dtmf_flags_unknown5:1, + dtmf_flags_unknown4:1, + dtmf_flags_unknown3:1, + dtmf_pttid:2, + dtmf_decode:1; + + + u8 step; + u8 scrambler; +} channel[214]; + +#seekto 0xd60; +struct { +u8 is_scanlist1:1, +is_scanlist2:1, +compander:2, +is_free:1, +band:3; +} channel_attributes[200]; + +#seekto 0xe40; +ul16 fmfreq[20]; + +#seekto 0xe70; +u8 call_channel; +u8 squelch; +u8 max_talk_time; +u8 noaa_autoscan; +u8 key_lock; +u8 vox_switch; +u8 vox_level; +u8 mic_gain; +u8 unknown3; +u8 channel_display_mode; +u8 crossband; +u8 battery_save; +u8 dual_watch; +u8 backlight_auto_mode; +u8 tail_note_elimination; +u8 vfo_open; + +#seekto 0xe90; +u8 beep_control; +u8 key1_shortpress_action; +u8 key1_longpress_action; +u8 key2_shortpress_action; +u8 key2_longpress_action; +u8 scan_resume_mode; +u8 auto_keypad_lock; +u8 power_on_dispmode; +u8 password[4]; + +#seekto 0xea0; +u8 keypad_tone; +u8 language; + +#seekto 0xea8; +u8 alarm_mode; +u8 reminding_of_end_talk; +u8 repeater_tail_elimination; + +#seekto 0xeb0; +char logo_line1[16]; +char logo_line2[16]; + +#seekto 0xed0; +struct { +u8 side_tone; +char separate_code; +char group_call_code; +u8 decode_response; +u8 auto_reset_time; +u8 preload_time; +u8 first_code_persist_time; +u8 hash_persist_time; +u8 code_persist_time; +u8 code_interval_time; +u8 permit_remote_kill; +} dtmf_settings; + +#seekto 0xee0; +struct { +char dtmf_local_code[3]; +char unused1[5]; +char kill_code[5]; +char unused2[3]; +char revive_code[5]; +char unused3[3]; +char dtmf_up_code[16]; +char dtmf_down_code[16]; +} dtmf_settings_numbers; + +#seekto 0xf18; +u8 scanlist_default; +u8 scanlist1_priority_scan; +u8 scanlist1_priority_ch1; +u8 scanlist1_priority_ch2; +u8 scanlist2_priority_scan; +u8 scanlist2_priority_ch1; +u8 scanlist2_priority_ch2; +u8 scanlist_unknown_0xff; + + +#seekto 0xf40; +struct { +u8 flock; +u8 tx350; +u8 killed; +u8 tx200; +u8 tx500; +u8 en350; +u8 enscramble; +} lock; + +#seekto 0xf50; +struct { +char name[16]; +} channelname[200]; + +#seekto 0x1c00; +struct { +char name[8]; +char number[3]; +char unused_00[5]; +} dtmfcontact[16]; + +#seekto 0x1ed0; +struct { +struct { + u8 start; + u8 mid; + u8 end; +} low; +struct { + u8 start; + u8 mid; + u8 end; +} medium; +struct { + u8 start; + u8 mid; + u8 end; +} high; +u8 unused_00[7]; +} perbandpowersettings[7]; + +#seekto 0x1f40; +ul16 battery_level[6]; +""" + +# bits that we will save from the channel structure (mostly unknown) +SAVE_MASK_0A = 0b11001100 +SAVE_MASK_0B = 0b11101100 +SAVE_MASK_0C = 0b11100000 +SAVE_MASK_0D = 0b11111000 +SAVE_MASK_0E = 0b11110001 +SAVE_MASK_0F = 0b11110000 + +# flags1 +FLAGS1_OFFSET_NONE = 0b00 +FLAGS1_OFFSET_MINUS = 0b10 +FLAGS1_OFFSET_PLUS = 0b01 + +POWER_HIGH = 0b10 +POWER_MEDIUM = 0b01 +POWER_LOW = 0b00 + +# power +UVK5_POWER_LEVELS = [chirp_common.PowerLevel("Low", watts=1.50), + chirp_common.PowerLevel("Med", watts=3.00), + chirp_common.PowerLevel("High", watts=5.00), + ] + +# scrambler +SCRAMBLER_LIST = ["off", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10"] + +# channel display mode +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 + +# ctcss/dcs codes +TMODES = ["", "Tone", "DTCS", "DTCS"] +TONE_NONE = 0 +TONE_CTCSS = 1 +TONE_DCS = 2 +TONE_RDCS = 3 + + +CTCSS_TONES = [ + 67.0, 69.3, 71.9, 74.4, 77.0, 79.7, 82.5, 85.4, + 88.5, 91.5, 94.8, 97.4, 100.0, 103.5, 107.2, 110.9, + 114.8, 118.8, 123.0, 127.3, 131.8, 136.5, 141.3, 146.2, + 151.4, 156.7, 159.8, 162.2, 165.5, 167.9, 171.3, 173.8, + 177.3, 179.9, 183.5, 186.2, 189.9, 192.8, 196.6, 199.5, + 203.5, 206.5, 210.7, 218.1, 225.7, 229.1, 233.6, 241.8, + 250.3, 254.1 +] + +# lifted from ft4.py +DTCS_CODES = [ + 23, 25, 26, 31, 32, 36, 43, 47, 51, 53, 54, + 65, 71, 72, 73, 74, 114, 115, 116, 122, 125, 131, + 132, 134, 143, 145, 152, 155, 156, 162, 165, 172, 174, + 205, 212, 223, 225, 226, 243, 244, 245, 246, 251, 252, + 255, 261, 263, 265, 266, 271, 274, 306, 311, 315, 325, + 331, 332, 343, 346, 351, 356, 364, 365, 371, 411, 412, + 413, 423, 431, 432, 445, 446, 452, 454, 455, 462, 464, + 465, 466, 503, 506, 516, 523, 526, 532, 546, 565, 606, + 612, 624, 627, 631, 632, 654, 662, 664, 703, 712, 723, + 731, 732, 734, 743, 754 +] + +FLOCK_LIST = ["Off", "FCC", "CE", "GB", "430", "438"] + +SCANRESUME_LIST = ["TO: Resume after 5 seconds", + "CO: Resume after signal disappears", + "SE: Stop scanning after receiving a signal"] + +WELCOME_LIST = ["Full Screen", "Welcome Info", "Voltage"] +KEYPADTONE_LIST = ["Off", "Chinese", "English"] +LANGUAGE_LIST = ["Chinese", "English"] +ALARMMODE_LIST = ["SITE", "TONE"] +REMENDOFTALK_LIST = ["Off", "ROGER", "MDC"] +RTE_LIST = ["Off", "100ms", "200ms", "300ms", "400ms", + "500ms", "600ms", "700ms", "800ms", "900ms"] + +MEM_SIZE = 0x2000 # size of all memory +PROG_SIZE = 0x1d00 # size of the memory that we will write +MEM_BLOCK = 0x80 # largest block of memory that we can reliably write + +# fm radio supported frequencies +FMMIN = 76.0 +FMMAX = 108.0 + +# bands supported by the UV-K5 +BANDS = { + 0: [50.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, 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] + } + +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", + "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"] + +SCANLIST_LIST = ["None", "1", "2", "1+2"] + +DTMF_CHARS = "0123456789ABCD*# " +DTMF_CHARS_ID = "0123456789ABCDabcd" +DTMF_CHARS_KILL = "0123456789ABCDabcd" +DTMF_CHARS_UPDOWN = "0123456789ABCDabcd#* " +DTMF_CODE_CHARS = "ABCD*# " +DTMF_DECODE_RESPONSE_LIST = ["None", "Ring", "Reply", "Both"] + +KEYACTIONS_LIST = ["None", "Flashlight on/off", "Power select", + "Monitor", "Scan on/off", "VOX on/off", + "Alarm on/off", "FM radio on/off", "Transmit 1750 Hz"] + + +def xorarr(data: bytes): + """the communication is obfuscated using this fine mechanism""" + tbl = [22, 108, 20, 230, 46, 145, 13, 64, 33, 53, 213, 64, 19, 3, 233, 128] + ret = b"" + idx = 0 + for byte in data: + ret += bytes([byte ^ tbl[idx]]) + idx = (idx+1) % len(tbl) + return ret + + +def calculate_crc16_xmodem(data: bytes): + """ + if this crc was used for communication to AND from the radio, then it + would be a measure to increase reliability. + but it's only used towards the radio, so it's for further obfuscation + """ + poly = 0x1021 + crc = 0x0 + for byte in data: + crc = crc ^ (byte << 8) + for _ in range(8): + crc = crc << 1 + if crc & 0x10000: + crc = (crc ^ poly) & 0xFFFF + return crc & 0xFFFF + + +def _send_command(serport, data: bytes): + """Send a command to UV-K5 radio""" + LOG.debug("Sending command (unobfuscated) len=0x%4.4x:\n%s", + len(data), util.hexprint(data)) + + crc = calculate_crc16_xmodem(data) + 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: + result = serport.write(command) + except Exception as e: + raise errors.RadioError("Error writing data to radio") from e + return result + + +def _receive_reply(serport): + header = serport.read(4) + if not header: + raise errors.RadioError("No response from radio") + elif len(header) != 4: + LOG.warning("Header short read: [%s] len=%i", + util.hexprint(header), len(header)) + raise errors.RadioError("Header short read") + elif header[0] != 0xAB or header[1] != 0xCD or header[3] != 0x00: + LOG.warning("Bad response header: %s len=%i", + util.hexprint(header), len(header)) + raise errors.RadioError("Bad response header") + + cmd = serport.read(int(header[2])) + if len(cmd) != int(header[2]): + LOG.warning("Body short read: [%s] len=%i", + util.hexprint(cmd), len(cmd)) + raise errors.RadioError("Command body short read") + + footer = serport.read(4) + + if len(footer) != 4: + LOG.warning("Footer short read: [%s] len=%i", + util.hexprint(footer), len(footer)) + raise errors.RadioError("Footer short read") + + if footer[2] != 0xDC or footer[3] != 0xBA: + LOG.debug("Reply before bad response footer (obfuscated)" + "len=0x%4.4x:\n%s", len(cmd), util.hexprint(cmd)) + LOG.warning("Bad response footer: %s len=%i", + util.hexprint(footer), len(footer)) + raise errors.RadioError("Bad response footer") + + if DEBUG_SHOW_OBFUSCATED_COMMANDS: + LOG.debug("Received reply (obfuscated) len=0x%4.4x:\n%s", + len(cmd), util.hexprint(cmd)) + + cmd2 = xorarr(cmd) + + LOG.debug("Received reply (unobfuscated) len=0x%4.4x:\n%s", + len(cmd2), util.hexprint(cmd2)) + + return cmd2 + + +def _getstring(data: bytes, begin, maxlen): + tmplen = min(maxlen+1, len(data)) + ss = [data[i] for i in range(begin, tmplen)] + key = 0 + for key, val in enumerate(ss): + if val < ord(' ') or val > ord('~'): + return ''.join(chr(x) for x in ss[0:key]) + return '' + + +def _sayhello(serport): + hellopacket = b"\x14\x05\x04\x00\x6a\x39\x57\x64" + + tries = 5 + while True: + LOG.debug("Sending hello packet") + _send_command(serport, hellopacket) + rep = _receive_reply(serport) + if rep: + break + tries -= 1 + if tries == 0: + LOG.warning("Failed to initialise radio") + raise errors.RadioError("Failed to initialize radio") + if rep.startswith(b'\x18\x05'): + raise errors.RadioError("Radio is in programming mode, " + "restart radio into normal mode") + firmware = _getstring(rep, 4, 24) + + LOG.info("Found firmware: %s", firmware) + return firmware + + +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" + \ + struct.pack("> 8) & 0xff): + return True + + LOG.warning("Bad data from writemem") + raise errors.RadioError("Bad response to writemem") + + +def _resetradio(serport): + resetpacket = b"\xdd\x05\x00\x00" + _send_command(serport, resetpacket) + + +def do_download(radio): + """download eeprom from radio""" + serport = radio.pipe + serport.timeout = 0.5 + status = chirp_common.Status() + status.cur = 0 + status.max = MEM_SIZE + status.msg = "Downloading from radio" + radio.status_fn(status) + + eeprom = b"" + f = _sayhello(serport) + if not f: + raise errors.RadioError('Unable to determine firmware version') + + if not radio.k5_approve_firmware(f): + raise errors.RadioError( + 'Firmware version is not supported by this driver') + + radio.metadata = {'uvk5_firmware': f} + + addr = 0 + while addr < MEM_SIZE: + data = _readmem(serport, addr, MEM_BLOCK) + status.cur = addr + radio.status_fn(status) + + if data and len(data) == MEM_BLOCK: + eeprom += data + addr += MEM_BLOCK + else: + raise errors.RadioError("Memory download incomplete") + + return memmap.MemoryMapBytes(eeprom) + + +def do_upload(radio): + """upload configuration to radio eeprom""" + serport = radio.pipe + serport.timeout = 0.5 + status = chirp_common.Status() + status.cur = 0 + status.msg = "Uploading to radio" + + if radio._upload_calibration: + status.max = MEM_SIZE - radio._cal_start + start_addr = radio._cal_start + stop_addr = MEM_SIZE + else: + status.max = PROG_SIZE + start_addr = 0 + stop_addr = PROG_SIZE + + radio.status_fn(status) + + f = _sayhello(serport) + if not f: + raise errors.RadioError('Unable to determine firmware version') + + if not radio.k5_approve_firmware(f): + raise errors.RadioError( + 'Firmware version is not supported by this driver') + LOG.info('Uploading image from firmware %r to radio with %r', + radio.metadata.get('uvk5_firmware', 'unknown'), f) + addr = start_addr + while addr < stop_addr: + dat = radio.get_mmap()[addr:addr+MEM_BLOCK] + _writemem(serport, dat, addr) + status.cur = addr - start_addr + radio.status_fn(status) + if dat: + addr += MEM_BLOCK + else: + raise errors.RadioError("Memory upload incomplete") + status.msg = "Uploaded OK" + + _resetradio(serport) + + return True + + +def _find_band(nolimits, hz): + mhz = hz/1000000.0 + if nolimits: + B = BANDS_NOLIMITS + else: + B = BANDS + + # currently the hacked firmware sets band=1 below 50 MHz + if nolimits and mhz < 50.0: + return 1 + + for a in B: + if mhz >= B[a][0] and mhz <= B[a][1]: + return a + return False + + +class UVK5RadioBase(chirp_common.CloneModeRadio): + """Quansheng UV-K5""" + VENDOR = "Quansheng" + MODEL = "UV-K5" + BAUD_RATE = 38400 + _cal_start = 0 + _expanded_limits = False + _upload_calibration = False + _pttid_list = ["off", "BOT", "EOT", "BOTH"] + _steps = [1.0, 2.5, 5.0, 6.25, 10.0, 12.5, 25.0, 8.33] + + @classmethod + def k5_approve_firmware(cls, firmware): + # All subclasses must implement this + raise NotImplementedError() + + def get_prompts(x=None): + rp = chirp_common.RadioPrompts() + rp.experimental = _( + '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 ' + 'and keep it. This can be later used to recover the ' + 'original settings. \n\n' + 'some details are not yet implemented') + rp.pre_download = _( + "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 download image from device.\n\n" + "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 on the radio " + "with the cable already attached") + return rp + + # Return information about this radio's features, including + # how many memories it has, what bands it supports, etc + def get_features(self): + rf = chirp_common.RadioFeatures() + rf.has_bank = False + rf.valid_dtcs_codes = DTCS_CODES + rf.has_rx_dtcs = True + rf.has_ctone = True + rf.has_settings = True + rf.has_comment = False + rf.valid_name_length = 10 + rf.valid_power_levels = UVK5_POWER_LEVELS + rf.valid_special_chans = list(SPECIALS.keys()) + rf.valid_duplexes = ["", "-", "+", "off"] + + # hack so we can input any frequency, + # the 0.1 and 0.01 steps don't work unfortunately + rf.valid_tuning_steps = list(self._steps) + + rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS", "Cross"] + rf.valid_cross_modes = ["Tone->Tone", "Tone->DTCS", "DTCS->Tone", + "->Tone", "->DTCS", "DTCS->", "DTCS->DTCS"] + + rf.valid_characters = chirp_common.CHARSET_ASCII + rf.valid_modes = ["FM", "NFM", "AM", "NAM"] + rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS", "Cross"] + + rf.valid_skips = [""] + + # This radio supports memories 1-200, 201-214 are the VFO memories + rf.memory_bounds = (1, 200) + + rf.valid_bands = [] + for a in BANDS_NOLIMITS: + rf.valid_bands.append( + (int(BANDS_NOLIMITS[a][0]*1000000), + int(BANDS_NOLIMITS[a][1]*1000000))) + return rf + + # Do a download of the radio from the serial port + def sync_in(self): + self._mmap = do_download(self) + self.process_mmap() + + # Do an upload of the radio to the serial port + def sync_out(self): + do_upload(self) + + def _check_firmware_at_load(self): + firmware = self.metadata.get('uvk5_firmware') + if not firmware: + LOG.warning(_('This image is missing firmware information. ' + 'It may have been generated with an old or ' + 'modified version of CHIRP. It is advised that ' + 'you download a fresh image from your radio and ' + 'use that going forward for the best safety and ' + 'compatibility.')) + elif not self.k5_approve_firmware(self.metadata['uvk5_firmware']): + raise errors.RadioError( + 'Image firmware is %r but is not supported by ' + 'this driver' % firmware) + + # Convert the raw byte array into a memory object structure + def process_mmap(self): + self._check_firmware_at_load() + self._memobj = bitwise.parse(MEM_FORMAT, self._mmap) + + # Return a raw representation of the memory object, which + # is very helpful for development + def get_raw_memory(self, number): + return repr(self._memobj.channel[number-1]) + + def _find_band(self, hz): + return _find_band(self._expanded_limits, hz) + + def validate_memory(self, mem): + msgs = super().validate_memory(mem) + + if mem.duplex == 'off': + return msgs + + # 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 = self._find_band(txfreq) + if band is False: + msg = "Transmit frequency %.4f MHz is not supported by this radio"\ + % (txfreq/1000000.0) + msgs.append(chirp_common.ValidationError(msg)) + + band = self._find_band(mem.freq) + if band is False: + msg = "The frequency %.4f MHz is not supported by this radio" \ + % (mem.freq/1000000.0) + msgs.append(chirp_common.ValidationError(msg)) + + return msgs + + def _set_tone(self, mem, _mem): + ((txmode, txtone, txpol), + (rxmode, rxtone, rxpol)) = chirp_common.split_tone_encode(mem) + + if txmode == "Tone": + txtoval = CTCSS_TONES.index(txtone) + txmoval = 0b01 + elif txmode == "DTCS": + txmoval = txpol == "R" and 0b11 or 0b10 + txtoval = DTCS_CODES.index(txtone) + else: + txmoval = 0 + txtoval = 0 + + if rxmode == "Tone": + rxtoval = CTCSS_TONES.index(rxtone) + rxmoval = 0b01 + elif rxmode == "DTCS": + rxmoval = rxpol == "R" and 0b11 or 0b10 + rxtoval = DTCS_CODES.index(rxtone) + else: + rxmoval = 0 + rxtoval = 0 + + _mem.rxcodeflag = rxmoval + _mem.txcodeflag = txmoval + _mem.rxcode = rxtoval + _mem.txcode = txtoval + + def _get_tone(self, mem, _mem): + rxtype = _mem.rxcodeflag + txtype = _mem.txcodeflag + rx_tmode = TMODES[rxtype] + tx_tmode = TMODES[txtype] + + rx_tone = tx_tone = None + + if tx_tmode == "Tone": + if _mem.txcode < len(CTCSS_TONES): + tx_tone = CTCSS_TONES[_mem.txcode] + else: + tx_tone = 0 + tx_tmode = "" + elif tx_tmode == "DTCS": + if _mem.txcode < len(DTCS_CODES): + tx_tone = DTCS_CODES[_mem.txcode] + else: + tx_tone = 0 + tx_tmode = "" + + if rx_tmode == "Tone": + if _mem.rxcode < len(CTCSS_TONES): + rx_tone = CTCSS_TONES[_mem.rxcode] + else: + rx_tone = 0 + rx_tmode = "" + elif rx_tmode == "DTCS": + if _mem.rxcode < len(DTCS_CODES): + rx_tone = DTCS_CODES[_mem.rxcode] + else: + rx_tone = 0 + rx_tmode = "" + + tx_pol = txtype == 0x03 and "R" or "N" + rx_pol = rxtype == 0x03 and "R" or "N" + + chirp_common.split_tone_decode(mem, (tx_tmode, tx_tone, tx_pol), + (rx_tmode, rx_tone, rx_pol)) + + def _get_mem_extra(self, mem, _mem): + tmpscn = SCANLIST_LIST[0] + + # We'll also look at the channel attributes if a memory has them + if mem.number <= 200: + _mem3 = self._memobj.channel_attributes[mem.number - 1] + # free memory bit + if _mem3.is_free > 0: + mem.empty = True + # scanlists + if _mem3.is_scanlist1 > 0 and _mem3.is_scanlist2 > 0: + tmpscn = SCANLIST_LIST[3] # "1+2" + elif _mem3.is_scanlist1 > 0: + tmpscn = SCANLIST_LIST[1] # "1" + elif _mem3.is_scanlist2 > 0: + tmpscn = SCANLIST_LIST[2] # "2" + + mem.extra = RadioSettingGroup("Extra", "extra") + + # BCLO + is_bclo = not mem.empty and bool(_mem.bclo > 0) + rs = RadioSetting("bclo", "BCLO", RadioSettingValueBoolean(is_bclo)) + mem.extra.append(rs) + + # Frequency reverse - reverse tx/rx frequency + is_frev = not mem.empty and bool(_mem.freq_reverse > 0) + rs = RadioSetting("frev", "FreqRev", RadioSettingValueBoolean(is_frev)) + mem.extra.append(rs) + + # PTTID + try: + pttid = self._pttid_list[_mem.dtmf_pttid] + except IndexError: + pttid = 0 + rs = RadioSetting("pttid", "PTTID", RadioSettingValueList( + self._pttid_list, pttid)) + mem.extra.append(rs) + + # DTMF DECODE + is_dtmf = not mem.empty and bool(_mem.dtmf_decode > 0) + rs = RadioSetting("dtmfdecode", _("DTMF decode"), + RadioSettingValueBoolean(is_dtmf)) + mem.extra.append(rs) + + # Scrambler + if _mem.scrambler & 0x0f < len(SCRAMBLER_LIST): + enc = _mem.scrambler & 0x0f + else: + enc = 0 + + rs = RadioSetting("scrambler", _("Scrambler"), RadioSettingValueList( + SCRAMBLER_LIST, SCRAMBLER_LIST[enc])) + mem.extra.append(rs) + + rs = RadioSetting("scanlists", _("Scanlists"), RadioSettingValueList( + SCANLIST_LIST, tmpscn)) + mem.extra.append(rs) + + def _get_mem_mode(self, _mem): + if _mem.enable_am > 0: + if _mem.bandwidth > 0: + return "NAM" + else: + return "AM" + else: + if _mem.bandwidth > 0: + return "NFM" + else: + return "FM" + + def _get_specials(self): + return dict(SPECIALS) + + # 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): + + mem = chirp_common.Memory() + + if isinstance(number2, str): + number = self._get_specials()[number2] + mem.extd_number = number2 + else: + number = number2 - 1 + + mem.number = number + 1 + + _mem = self._memobj.channel[number] + + # We'll consider any blank (i.e. 0 MHz frequency) to be empty + if (_mem.freq == 0xffffffff) or (_mem.freq == 0): + mem.empty = True + + self._get_mem_extra(mem, _mem) + + if mem.empty: + return mem + + if number > 199: + mem.immutable = ["name", "scanlists"] + else: + _mem2 = self._memobj.channelname[number] + for char in _mem2.name: + if str(char) == "\xFF" or str(char) == "\x00": + break + mem.name += str(char) + mem.name = mem.name.rstrip() + + # Convert your low-level frequency to Hertz + mem.freq = int(_mem.freq)*10 + mem.offset = int(_mem.offset)*10 + + if (mem.offset == 0): + mem.duplex = '' + else: + if _mem.shift == FLAGS1_OFFSET_MINUS: + if _mem.freq == _mem.offset: + # fake tx disable by setting tx to 0 MHz + mem.duplex = 'off' + mem.offset = 0 + else: + mem.duplex = '-' + elif _mem.shift == FLAGS1_OFFSET_PLUS: + mem.duplex = '+' + else: + mem.duplex = '' + + # tone data + self._get_tone(mem, _mem) + + mem.mode = self._get_mem_mode(_mem) + + # tuning step + try: + mem.tuning_step = self._steps[_mem.step] + except IndexError: + mem.tuning_step = 2.5 + + # power + if _mem.txpower == POWER_HIGH: + mem.power = UVK5_POWER_LEVELS[2] + elif _mem.txpower == POWER_MEDIUM: + mem.power = UVK5_POWER_LEVELS[1] + else: + mem.power = UVK5_POWER_LEVELS[0] + + # We'll consider any blank (i.e. 0 MHz frequency) to be empty + if (_mem.freq == 0xffffffff) or (_mem.freq == 0): + mem.empty = True + else: + mem.empty = False + + return mem + + def set_settings(self, settings): + _mem = self._memobj + for element in settings: + if not isinstance(element, RadioSetting): + self.set_settings(element) + continue + + # basic settings + + # call channel + if element.get_name() == "call_channel": + _mem.call_channel = int(element.value)-1 + + # squelch + if element.get_name() == "squelch": + _mem.squelch = int(element.value) + # 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 + + # mic gain + if element.get_name() == "mic_gain": + _mem.mic_gain = int(element.value) + + # Channel display mode + if element.get_name() == "channel_display_mode": + _mem.channel_display_mode = CHANNELDISP_LIST.index( + str(element.value)) + + # Crossband receiving/transmitting + if element.get_name() == "crossband": + _mem.crossband = CROSSBAND_LIST.index(str(element.value)) + + # Battery Save + if element.get_name() == "battery_save": + _mem.battery_save = BATSAVE_LIST.index(str(element.value)) + # Dual Watch + 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 + + # VFO Open + if element.get_name() == "vfo_open": + _mem.vfo_open = element.value and 1 or 0 + + # Beep control + if element.get_name() == "beep_control": + _mem.beep_control = element.value and 1 or 0 + + # Scan resume mode + if element.get_name() == "scan_resume_mode": + _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 + + # Power on display mode + if element.get_name() == "welcome_mode": + _mem.power_on_dispmode = WELCOME_LIST.index(str(element.value)) + + # Keypad Tone + if element.get_name() == "keypad_tone": + _mem.keypad_tone = KEYPADTONE_LIST.index(str(element.value)) + + # Language + if element.get_name() == "language": + _mem.language = LANGUAGE_LIST.index(str(element.value)) + + # Alarm mode + if element.get_name() == "alarm_mode": + _mem.alarm_mode = ALARMMODE_LIST.index(str(element.value)) + + # Reminding of end of talk + if element.get_name() == "reminding_of_end_talk": + _mem.reminding_of_end_talk = REMENDOFTALK_LIST.index( + str(element.value)) + + # Repeater tail tone elimination + if element.get_name() == "repeater_tail_elimination": + _mem.repeater_tail_elimination = RTE_LIST.index( + str(element.value)) + + # Logo string 1 + if element.get_name() == "logo1": + b = str(element.value).rstrip("\x20\xff\x00")+"\x00"*12 + _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]+"\x00\xff\xff\xff" + + # unlock settings + + # FLOCK + if element.get_name() == "flock": + _mem.lock.flock = FLOCK_LIST.index(str(element.value)) + + # 350TX + if element.get_name() == "tx350": + _mem.lock.tx350 = element.value and 1 or 0 + + # 200TX + if element.get_name() == "tx200": + _mem.lock.tx200 = element.value and 1 or 0 + + # 500TX + if element.get_name() == "tx500": + _mem.lock.tx500 = element.value and 1 or 0 + + # 350EN + if element.get_name() == "en350": + _mem.lock.en350 = element.value and 1 or 0 + + # SCREN + if element.get_name() == "enscramble": + _mem.lock.enscramble = element.value and 1 or 0 + + # KILLED + if element.get_name() == "killed": + _mem.lock.killed = element.value and 1 or 0 + + # fm radio + for i in range(1, 21): + freqname = "FM_" + str(i) + if element.get_name() == freqname: + val = str(element.value).strip() + try: + val2 = int(float(val)*10) + except Exception: + val2 = 0xffff + + if val2 < FMMIN*10 or val2 > FMMAX*10: + val2 = 0xffff +# raise errors.InvalidValueError( +# "FM radio frequency should be a value " +# "in the range %.1f - %.1f" % (FMMIN , FMMAX)) + _mem.fmfreq[i-1] = val2 + + # dtmf settings + if element.get_name() == "dtmf_side_tone": + _mem.dtmf_settings.side_tone = \ + element.value and 1 or 0 + + if element.get_name() == "dtmf_separate_code": + _mem.dtmf_settings.separate_code = str(element.value) + + if element.get_name() == "dtmf_group_call_code": + _mem.dtmf_settings.group_call_code = element.value + + if element.get_name() == "dtmf_decode_response": + _mem.dtmf_settings.decode_response = \ + DTMF_DECODE_RESPONSE_LIST.index(str(element.value)) + + if element.get_name() == "dtmf_auto_reset_time": + _mem.dtmf_settings.auto_reset_time = \ + int(int(element.value)/10) + + if element.get_name() == "dtmf_preload_time": + _mem.dtmf_settings.preload_time = \ + int(int(element.value)/10) + + if element.get_name() == "dtmf_first_code_persist_time": + _mem.dtmf_settings.first_code_persist_time = \ + int(int(element.value)/10) + + if element.get_name() == "dtmf_hash_persist_time": + _mem.dtmf_settings.hash_persist_time = \ + int(int(element.value)/10) + + if element.get_name() == "dtmf_code_persist_time": + _mem.dtmf_settings.code_persist_time = \ + int(int(element.value)/10) + + if element.get_name() == "dtmf_code_interval_time": + _mem.dtmf_settings.code_interval_time = \ + int(int(element.value)/10) + + if element.get_name() == "dtmf_permit_remote_kill": + _mem.dtmf_settings.permit_remote_kill = \ + element.value and 1 or 0 + + if element.get_name() == "dtmf_dtmf_local_code": + k = str(element.value).rstrip("\x20\xff\x00") + "\x00"*3 + _mem.dtmf_settings_numbers.dtmf_local_code = k[0:3] + + if element.get_name() == "dtmf_dtmf_up_code": + k = str(element.value).strip("\x20\xff\x00") + "\x00"*16 + _mem.dtmf_settings_numbers.dtmf_up_code = k[0:16] + + if element.get_name() == "dtmf_dtmf_down_code": + k = str(element.value).rstrip("\x20\xff\x00") + "\x00"*16 + _mem.dtmf_settings_numbers.dtmf_down_code = k[0:16] + + if element.get_name() == "dtmf_kill_code": + k = str(element.value).strip("\x20\xff\x00") + "\x00"*5 + _mem.dtmf_settings_numbers.kill_code = k[0:5] + + if element.get_name() == "dtmf_revive_code": + k = str(element.value).strip("\x20\xff\x00") + "\x00"*5 + _mem.dtmf_settings_numbers.revive_code = k[0:5] + + # dtmf contacts + for i in range(1, 17): + varname = "DTMF_" + str(i) + if element.get_name() == varname: + k = str(element.value).rstrip("\x20\xff\x00") + "\x00"*8 + _mem.dtmfcontact[i-1].name = k[0:8] + + varnumname = "DTMFNUM_" + str(i) + if element.get_name() == varnumname: + k = str(element.value).rstrip("\x20\xff\x00") + "\xff"*3 + _mem.dtmfcontact[i-1].number = k[0:3] + + # scanlist stuff + if element.get_name() == "scanlist_default": + val = (int(element.value) == 2) and 1 or 0 + _mem.scanlist_default = val + + if element.get_name() == "scanlist1_priority_scan": + _mem.scanlist1_priority_scan = \ + element.value and 1 or 0 + + if element.get_name() == "scanlist2_priority_scan": + _mem.scanlist2_priority_scan = \ + element.value and 1 or 0 + + if element.get_name() == "scanlist1_priority_ch1" or \ + element.get_name() == "scanlist1_priority_ch2" or \ + element.get_name() == "scanlist2_priority_ch1" or \ + element.get_name() == "scanlist2_priority_ch2": + + val = int(element.value) + + if val > 200 or val < 1: + val = 0xff + else: + val -= 1 + + if element.get_name() == "scanlist1_priority_ch1": + _mem.scanlist1_priority_ch1 = val + if element.get_name() == "scanlist1_priority_ch2": + _mem.scanlist1_priority_ch2 = val + if element.get_name() == "scanlist2_priority_ch1": + _mem.scanlist2_priority_ch1 = val + if element.get_name() == "scanlist2_priority_ch2": + _mem.scanlist2_priority_ch2 = val + + if element.get_name() == "key1_shortpress_action": + _mem.key1_shortpress_action = KEYACTIONS_LIST.index( + str(element.value)) + + if element.get_name() == "key1_longpress_action": + _mem.key1_longpress_action = KEYACTIONS_LIST.index( + str(element.value)) + + if element.get_name() == "key2_shortpress_action": + _mem.key2_shortpress_action = KEYACTIONS_LIST.index( + str(element.value)) + + if element.get_name() == "key2_longpress_action": + _mem.key2_longpress_action = KEYACTIONS_LIST.index( + str(element.value)) + + if element.get_name() == "nolimits": + LOG.warning("User expanded band limits") + self._expanded_limits = bool(element.value) + + def get_settings(self): + _mem = self._memobj + basic = RadioSettingGroup("basic", "Basic Settings") + keya = RadioSettingGroup("keya", "Programmable keys") + dtmf = RadioSettingGroup("dtmf", "DTMF Settings") + dtmfc = RadioSettingGroup("dtmfc", "DTMF Contacts") + scanl = RadioSettingGroup("scn", "Scan Lists") + unlock = RadioSettingGroup("unlock", "Unlock Settings") + fmradio = RadioSettingGroup("fmradio", _("FM Radio")) + + roinfo = RadioSettingGroup("roinfo", _("Driver information")) + + top = RadioSettings( + basic, keya, dtmf, dtmfc, scanl, unlock, fmradio, roinfo) + + # Programmable keys + tmpval = int(_mem.key1_shortpress_action) + if tmpval >= len(KEYACTIONS_LIST): + tmpval = 0 + rs = RadioSetting("key1_shortpress_action", "Side key 1 short press", + RadioSettingValueList( + KEYACTIONS_LIST, KEYACTIONS_LIST[tmpval])) + keya.append(rs) + + tmpval = int(_mem.key1_longpress_action) + if tmpval >= len(KEYACTIONS_LIST): + tmpval = 0 + rs = RadioSetting("key1_longpress_action", "Side key 1 long press", + RadioSettingValueList( + KEYACTIONS_LIST, KEYACTIONS_LIST[tmpval])) + keya.append(rs) + + tmpval = int(_mem.key2_shortpress_action) + if tmpval >= len(KEYACTIONS_LIST): + tmpval = 0 + rs = RadioSetting("key2_shortpress_action", "Side key 2 short press", + RadioSettingValueList( + KEYACTIONS_LIST, KEYACTIONS_LIST[tmpval])) + keya.append(rs) + + tmpval = int(_mem.key2_longpress_action) + if tmpval >= len(KEYACTIONS_LIST): + tmpval = 0 + rs = RadioSetting("key2_longpress_action", "Side key 2 long press", + RadioSettingValueList( + KEYACTIONS_LIST, KEYACTIONS_LIST[tmpval])) + keya.append(rs) + + # DTMF settings + tmppr = bool(_mem.dtmf_settings.side_tone > 0) + rs = RadioSetting( + "dtmf_side_tone", + "DTMF Sidetone", + RadioSettingValueBoolean(tmppr)) + dtmf.append(rs) + + tmpval = str(_mem.dtmf_settings.separate_code) + if tmpval not in DTMF_CODE_CHARS: + tmpval = '*' + val = RadioSettingValueString(1, 1, tmpval) + val.set_charset(DTMF_CODE_CHARS) + rs = RadioSetting("dtmf_separate_code", "Separate Code", val) + dtmf.append(rs) + + tmpval = str(_mem.dtmf_settings.group_call_code) + if tmpval not in DTMF_CODE_CHARS: + tmpval = '#' + val = RadioSettingValueString(1, 1, tmpval) + val.set_charset(DTMF_CODE_CHARS) + rs = RadioSetting("dtmf_group_call_code", "Group Call Code", val) + dtmf.append(rs) + + tmpval = _mem.dtmf_settings.decode_response + if tmpval >= len(DTMF_DECODE_RESPONSE_LIST): + tmpval = 0 + rs = RadioSetting("dtmf_decode_response", "Decode Response", + RadioSettingValueList( + DTMF_DECODE_RESPONSE_LIST, + DTMF_DECODE_RESPONSE_LIST[tmpval])) + dtmf.append(rs) + + tmpval = _mem.dtmf_settings.auto_reset_time + if tmpval > 60 or tmpval < 5: + tmpval = 5 + rs = RadioSetting("dtmf_auto_reset_time", + "Auto reset time (s)", + RadioSettingValueInteger(5, 60, tmpval)) + dtmf.append(rs) + + tmpval = int(_mem.dtmf_settings.preload_time) + if tmpval > 100 or tmpval < 3: + tmpval = 30 + tmpval *= 10 + rs = RadioSetting("dtmf_preload_time", + "Pre-load time (ms)", + RadioSettingValueInteger(30, 1000, tmpval, 10)) + dtmf.append(rs) + + tmpval = int(_mem.dtmf_settings.first_code_persist_time) + if tmpval > 100 or tmpval < 3: + tmpval = 30 + tmpval *= 10 + rs = RadioSetting("dtmf_first_code_persist_time", + "First code persist time (ms)", + RadioSettingValueInteger(30, 1000, tmpval, 10)) + dtmf.append(rs) + + tmpval = int(_mem.dtmf_settings.hash_persist_time) + if tmpval > 100 or tmpval < 3: + tmpval = 30 + tmpval *= 10 + rs = RadioSetting("dtmf_hash_persist_time", + "#/* persist time (ms)", + RadioSettingValueInteger(30, 1000, tmpval, 10)) + dtmf.append(rs) + + tmpval = int(_mem.dtmf_settings.code_persist_time) + if tmpval > 100 or tmpval < 3: + tmpval = 30 + tmpval *= 10 + rs = RadioSetting("dtmf_code_persist_time", + "Code persist time (ms)", + RadioSettingValueInteger(30, 1000, tmpval, 10)) + dtmf.append(rs) + + tmpval = int(_mem.dtmf_settings.code_interval_time) + if tmpval > 100 or tmpval < 3: + tmpval = 30 + tmpval *= 10 + rs = RadioSetting("dtmf_code_interval_time", + "Code interval time (ms)", + RadioSettingValueInteger(30, 1000, tmpval, 10)) + dtmf.append(rs) + + tmpval = bool(_mem.dtmf_settings.permit_remote_kill > 0) + rs = RadioSetting( + "dtmf_permit_remote_kill", + "Permit remote kill", + RadioSettingValueBoolean(tmpval)) + dtmf.append(rs) + + tmpval = str(_mem.dtmf_settings_numbers.dtmf_local_code).upper().strip( + "\x00\xff\x20") + for i in tmpval: + if i in DTMF_CHARS_ID: + continue + else: + tmpval = "103" + break + val = RadioSettingValueString(3, 3, tmpval) + val.set_charset(DTMF_CHARS_ID) + rs = RadioSetting("dtmf_dtmf_local_code", + "Local code (3 chars 0-9 ABCD)", val) + dtmf.append(rs) + + tmpval = str(_mem.dtmf_settings_numbers.dtmf_up_code).upper().strip( + "\x00\xff\x20") + for i in tmpval: + if i in DTMF_CHARS_UPDOWN or i == "": + continue + else: + tmpval = "123" + break + val = RadioSettingValueString(1, 16, tmpval) + val.set_charset(DTMF_CHARS_UPDOWN) + rs = RadioSetting("dtmf_dtmf_up_code", + "Up code (1-16 chars 0-9 ABCD*#)", val) + dtmf.append(rs) + + tmpval = str(_mem.dtmf_settings_numbers.dtmf_down_code).upper().strip( + "\x00\xff\x20") + for i in tmpval: + if i in DTMF_CHARS_UPDOWN: + continue + else: + tmpval = "456" + break + val = RadioSettingValueString(1, 16, tmpval) + val.set_charset(DTMF_CHARS_UPDOWN) + rs = RadioSetting("dtmf_dtmf_down_code", + "Down code (1-16 chars 0-9 ABCD*#)", val) + dtmf.append(rs) + + tmpval = str(_mem.dtmf_settings_numbers.kill_code).upper().strip( + "\x00\xff\x20") + for i in tmpval: + if i in DTMF_CHARS_KILL: + continue + else: + tmpval = "77777" + break + if not len(tmpval) == 5: + tmpval = "77777" + val = RadioSettingValueString(5, 5, tmpval) + val.set_charset(DTMF_CHARS_KILL) + rs = RadioSetting("dtmf_kill_code", + "Kill code (5 chars 0-9 ABCD)", val) + dtmf.append(rs) + + tmpval = str(_mem.dtmf_settings_numbers.revive_code).upper().strip( + "\x00\xff\x20") + for i in tmpval: + if i in DTMF_CHARS_KILL: + continue + else: + tmpval = "88888" + break + if not len(tmpval) == 5: + tmpval = "88888" + val = RadioSettingValueString(5, 5, tmpval) + val.set_charset(DTMF_CHARS_KILL) + rs = RadioSetting("dtmf_revive_code", + "Revive code (5 chars 0-9 ABCD)", val) + dtmf.append(rs) + + for i in range(1, 17): + varname = "DTMF_"+str(i) + varnumname = "DTMFNUM_"+str(i) + vardescr = "DTMF Contact "+str(i)+" name" + varinumdescr = "DTMF Contact "+str(i)+" number" + + cntn = str(_mem.dtmfcontact[i-1].name).strip("\x20\x00\xff") + cntnum = str(_mem.dtmfcontact[i-1].number).strip("\x20\x00\xff") + + val = RadioSettingValueString(0, 8, cntn) + rs = RadioSetting(varname, vardescr, val) + dtmfc.append(rs) + + val = RadioSettingValueString(0, 3, cntnum) + val.set_charset(DTMF_CHARS) + rs = RadioSetting(varnumname, varinumdescr, val) + dtmfc.append(rs) + rs.set_doc("DTMF Contacts are 3 codes (valid: 0-9 * # ABCD), " + "or an empty string") + + # scanlists + if _mem.scanlist_default == 1: + tmpsc = 2 + else: + tmpsc = 1 + rs = RadioSetting("scanlist_default", + "Default scanlist", + RadioSettingValueInteger(1, 2, tmpsc)) + scanl.append(rs) + + tmppr = bool((_mem.scanlist1_priority_scan & 1) > 0) + rs = RadioSetting( + "scanlist1_priority_scan", + "Scanlist 1 priority channel scan", + RadioSettingValueBoolean(tmppr)) + scanl.append(rs) + + tmpch = _mem.scanlist1_priority_ch1 + 1 + if tmpch > 200: + tmpch = 0 + rs = RadioSetting("scanlist1_priority_ch1", + "Scanlist 1 priority channel 1 (0 - off)", + RadioSettingValueInteger(0, 200, tmpch)) + scanl.append(rs) + + tmpch = _mem.scanlist1_priority_ch2 + 1 + if tmpch > 200: + tmpch = 0 + rs = RadioSetting("scanlist1_priority_ch2", + "Scanlist 1 priority channel 2 (0 - off)", + RadioSettingValueInteger(0, 200, tmpch)) + scanl.append(rs) + + tmppr = bool((_mem.scanlist2_priority_scan & 1) > 0) + rs = RadioSetting( + "scanlist2_priority_scan", + "Scanlist 2 priority channel scan", + RadioSettingValueBoolean(tmppr)) + scanl.append(rs) + + tmpch = _mem.scanlist2_priority_ch1 + 1 + if tmpch > 200: + tmpch = 0 + rs = RadioSetting("scanlist2_priority_ch1", + "Scanlist 2 priority channel 1 (0 - off)", + RadioSettingValueInteger(0, 200, tmpch)) + scanl.append(rs) + + tmpch = _mem.scanlist2_priority_ch2 + 1 + if tmpch > 200: + tmpch = 0 + rs = RadioSetting("scanlist2_priority_ch2", + "Scanlist 2 priority channel 2 (0 - off)", + RadioSettingValueInteger(0, 200, tmpch)) + scanl.append(rs) + + # basic settings + + # call channel + tmpc = _mem.call_channel+1 + if tmpc > 200: + tmpc = 1 + rs = RadioSetting("call_channel", "One key call channel", + RadioSettingValueInteger(1, 200, tmpc)) + basic.append(rs) + + # squelch + tmpsq = _mem.squelch + if tmpsq > 9: + tmpsq = 1 + rs = RadioSetting("squelch", "Squelch", + RadioSettingValueInteger(0, 9, tmpsq)) + basic.append(rs) + + # TOT + tmptot = _mem.max_talk_time + if tmptot > 10: + tmptot = 10 + rs = RadioSetting( + "tot", + "Max talk time [min]", + RadioSettingValueInteger(0, 10, tmptot)) + basic.append(rs) + + # NOAA autoscan + rs = RadioSetting( + "noaa_autoscan", + "NOAA Autoscan", RadioSettingValueBoolean( + 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: + tmpvox = 10 + rs = RadioSetting("vox_level", "VOX Level", + RadioSettingValueInteger(1, 10, tmpvox)) + basic.append(rs) + + # Mic gain + tmpmicgain = _mem.mic_gain + if tmpmicgain > 4: + tmpmicgain = 4 + rs = RadioSetting("mic_gain", "Mic Gain", + RadioSettingValueInteger(0, 4, tmpmicgain)) + basic.append(rs) + + # Channel display mode + tmpchdispmode = _mem.channel_display_mode + if tmpchdispmode >= len(CHANNELDISP_LIST): + tmpchdispmode = 0 + rs = RadioSetting( + "channel_display_mode", + "Channel display mode", + RadioSettingValueList( + CHANNELDISP_LIST, + CHANNELDISP_LIST[tmpchdispmode])) + basic.append(rs) + + # Crossband receiving/transmitting + tmpcross = _mem.crossband + if tmpcross >= len(CROSSBAND_LIST): + tmpcross = 0 + rs = RadioSetting( + "crossband", + "Cross-band receiving/transmitting", + RadioSettingValueList( + CROSSBAND_LIST, + CROSSBAND_LIST[tmpcross])) + 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[tmpbatsave])) + basic.append(rs) + + # Dual watch + tmpdual = _mem.dual_watch + if tmpdual >= len(DUALWATCH_LIST): + tmpdual = 0 + rs = RadioSetting("dualwatch", "Dual Watch", RadioSettingValueList( + 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", + "Tail tone elimination", + RadioSettingValueBoolean( + bool(_mem.tail_note_elimination > 0))) + basic.append(rs) + + # VFO open + rs = RadioSetting("vfo_open", "VFO open", + RadioSettingValueBoolean(bool(_mem.vfo_open > 0))) + basic.append(rs) + + # Beep control + rs = RadioSetting( + "beep_control", + "Beep control", + RadioSettingValueBoolean(bool(_mem.beep_control > 0))) + basic.append(rs) + + # Scan resume mode + tmpscanres = _mem.scan_resume_mode + if tmpscanres >= len(SCANRESUME_LIST): + tmpscanres = 0 + rs = RadioSetting( + "scan_resume_mode", + "Scan resume mode", + RadioSettingValueList( + SCANRESUME_LIST, + 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", + "Auto keypad lock", + RadioSettingValueBoolean(bool(_mem.auto_keypad_lock > 0))) + basic.append(rs) + + # Power on display mode + tmpdispmode = _mem.power_on_dispmode + if tmpdispmode >= len(WELCOME_LIST): + tmpdispmode = 0 + rs = RadioSetting( + "welcome_mode", + "Power on display mode", + RadioSettingValueList( + WELCOME_LIST, + WELCOME_LIST[tmpdispmode])) + basic.append(rs) + + # Keypad Tone + tmpkeypadtone = _mem.keypad_tone + if tmpkeypadtone >= len(KEYPADTONE_LIST): + tmpkeypadtone = 0 + rs = RadioSetting("keypad_tone", "Keypad tone", RadioSettingValueList( + KEYPADTONE_LIST, KEYPADTONE_LIST[tmpkeypadtone])) + basic.append(rs) + + # Language + tmplanguage = _mem.language + if tmplanguage >= len(LANGUAGE_LIST): + tmplanguage = 0 + rs = RadioSetting("language", "Language", RadioSettingValueList( + LANGUAGE_LIST, LANGUAGE_LIST[tmplanguage])) + basic.append(rs) + + # Alarm mode + tmpalarmmode = _mem.alarm_mode + if tmpalarmmode >= len(ALARMMODE_LIST): + tmpalarmmode = 0 + rs = RadioSetting("alarm_mode", "Alarm mode", RadioSettingValueList( + ALARMMODE_LIST, ALARMMODE_LIST[tmpalarmmode])) + basic.append(rs) + + # Reminding of end of talk + tmpalarmmode = _mem.reminding_of_end_talk + if tmpalarmmode >= len(REMENDOFTALK_LIST): + tmpalarmmode = 0 + rs = RadioSetting( + "reminding_of_end_talk", + "Reminding of end of talk", + RadioSettingValueList( + REMENDOFTALK_LIST, + REMENDOFTALK_LIST[tmpalarmmode])) + basic.append(rs) + + # Repeater tail tone elimination + tmprte = _mem.repeater_tail_elimination + if tmprte >= len(RTE_LIST): + tmprte = 0 + rs = RadioSetting( + "repeater_tail_elimination", + "Repeater tail tone elimination", + RadioSettingValueList(RTE_LIST, RTE_LIST[tmprte])) + basic.append(rs) + + # Logo string 1 + logo1 = str(_mem.logo_line1).strip("\x20\x00\xff") + "\x00" + logo1 = _getstring(logo1.encode('ascii', errors='ignore'), 0, 12) + rs = RadioSetting("logo1", _("Logo string 1 (12 characters)"), + RadioSettingValueString(0, 12, logo1)) + basic.append(rs) + + # Logo string 2 + logo2 = str(_mem.logo_line2).strip("\x20\x00\xff") + "\x00" + logo2 = _getstring(logo2.encode('ascii', errors='ignore'), 0, 12) + rs = RadioSetting("logo2", _("Logo string 2 (12 characters)"), + RadioSettingValueString(0, 12, logo2)) + basic.append(rs) + + # FM radio + for i in range(1, 21): + freqname = "FM_"+str(i) + fmfreq = _mem.fmfreq[i-1]/10.0 + if fmfreq < FMMIN or fmfreq > FMMAX: + rs = RadioSetting(freqname, freqname, + RadioSettingValueString(0, 5, "")) + else: + rs = RadioSetting(freqname, freqname, + RadioSettingValueString(0, 5, str(fmfreq))) + + fmradio.append(rs) + + # unlock settings + + # F-LOCK + tmpflock = _mem.lock.flock + if tmpflock >= len(FLOCK_LIST): + tmpflock = 0 + rs = RadioSetting( + "flock", "F-LOCK", + RadioSettingValueList(FLOCK_LIST, FLOCK_LIST[tmpflock])) + unlock.append(rs) + + # 350TX + rs = RadioSetting("tx350", "350TX - unlock 350-400 MHz TX", + RadioSettingValueBoolean( + bool(_mem.lock.tx350 > 0))) + unlock.append(rs) + + # Killed + rs = RadioSetting("Killed", "KILLED Device was disabled (via DTMF)", + RadioSettingValueBoolean( + bool(_mem.lock.killed > 0))) + unlock.append(rs) + + # 200TX + rs = RadioSetting("tx200", "200TX - unlock 174-350 MHz TX", + RadioSettingValueBoolean( + bool(_mem.lock.tx200 > 0))) + unlock.append(rs) + + # 500TX + rs = RadioSetting("tx500", "500TX - unlock 500-600 MHz TX", + RadioSettingValueBoolean( + bool(_mem.lock.tx500 > 0))) + unlock.append(rs) + + # 350EN + rs = RadioSetting("en350", "350EN - unlock 350-400 MHz RX", + RadioSettingValueBoolean( + bool(_mem.lock.en350 > 0))) + unlock.append(rs) + + # SCREEN + rs = RadioSetting("scrambler", "SCREN - scrambler enable", + RadioSettingValueBoolean( + bool(_mem.lock.enscramble > 0))) + unlock.append(rs) + + # readonly info + # Firmware + firmware = self.metadata.get('uvk5_firmware', 'UNKNOWN') + + val = RadioSettingValueString(0, 128, firmware) + val.set_mutable(False) + rs = RadioSetting("fw_ver", "Firmware Version", val) + roinfo.append(rs) + + # No limits version for hacked firmware + val = RadioSettingValueBoolean(self._expanded_limits) + rs = RadioSetting("nolimits", "Limits disabled for modified firmware", + val) + rs.set_warning(_( + 'This should only be enabled if you are using modified firmware ' + 'that supports wider frequency coverage. Enabling this will cause ' + 'CHIRP not to enforce OEM restrictions and may lead to undefined ' + 'or unregulated behavior. Use at your own risk!'), + safe_value=False) + roinfo.append(rs) + + return top + + def _set_mem_mode(self, _mem, mode): + if mode == "NFM": + _mem.bandwidth = 1 + _mem.enable_am = 0 + elif mode == "FM": + _mem.bandwidth = 0 + _mem.enable_am = 0 + elif mode == "NAM": + _mem.bandwidth = 1 + _mem.enable_am = 1 + elif mode == "AM": + _mem.bandwidth = 0 + _mem.enable_am = 1 + + # Store details about a high-level memory to the memory map + # This is called when a user edits a memory in the UI + def set_memory(self, mem): + number = mem.number-1 + + # Get a low-level memory object mapped to the image + _mem = self._memobj.channel[number] + _mem4 = self._memobj + # empty memory + if mem.empty: + _mem.set_raw(b"\xFF" * 16) + if number < 200: + _mem2 = self._memobj.channelname[number] + _mem2.set_raw(b"\xFF" * 16) + _mem4.channel_attributes[number].is_scanlist1 = 0 + _mem4.channel_attributes[number].is_scanlist2 = 0 + # Compander in other models, not supported here + _mem4.channel_attributes[number].compander = 0 + _mem4.channel_attributes[number].is_free = 1 + _mem4.channel_attributes[number].band = 0x7 + return mem + + # clean the channel memory, restore some bits if it was used before + if _mem.get_raw(asbytes=False)[0] == "\xff": + # this was an empty memory + _mem.set_raw(b"\x00" * 16) + else: + # this memory wasn't empty, save some bits that we don't know the + # meaning of, or that we don't support yet + prev_0a = _mem.get_raw()[0x0a] & SAVE_MASK_0A + prev_0b = _mem.get_raw()[0x0b] & SAVE_MASK_0B + prev_0c = _mem.get_raw()[0x0c] & SAVE_MASK_0C + prev_0d = _mem.get_raw()[0x0d] & SAVE_MASK_0D + prev_0e = _mem.get_raw()[0x0e] & SAVE_MASK_0E + prev_0f = _mem.get_raw()[0x0f] & SAVE_MASK_0F + _mem.set_raw(b"\x00" * 10 + + bytes([prev_0a, prev_0b, prev_0c, + prev_0d, prev_0e, prev_0f])) + + if number < 200: + _mem4.channel_attributes[number].is_scanlist1 = 0 + _mem4.channel_attributes[number].is_scanlist2 = 0 + _mem4.channel_attributes[number].compander = 0 + _mem4.channel_attributes[number].is_free = 1 + _mem4.channel_attributes[number].band = 0x7 + + # find band + band = _find_band(self, mem.freq) + + self._set_mem_mode(_mem, mem.mode) + + # frequency/offset + _mem.freq = mem.freq/10 + _mem.offset = mem.offset/10 + + if mem.duplex == "": + _mem.offset = 0 + _mem.shift = 0 + elif mem.duplex == '-': + _mem.shift = FLAGS1_OFFSET_MINUS + elif mem.duplex == '+': + _mem.shift = FLAGS1_OFFSET_PLUS + elif mem.duplex == 'off': + # we fake tx disable by setting the tx freq to 0 MHz + _mem.shift = FLAGS1_OFFSET_MINUS + _mem.offset = _mem.freq + + # set band + if number < 200: + _mem4.channel_attributes[number].is_free = 0 + _mem4.channel_attributes[number].band = band + + # channels >200 are the 14 VFO chanells and don't have names + if number < 200: + _mem2 = self._memobj.channelname[number] + tag = mem.name.ljust(10) + "\x00"*6 + _mem2.name = tag # Store the alpha tag + + # tone data + self._set_tone(mem, _mem) + + # step + _mem.step = self._steps.index(mem.tuning_step) + + # tx power + if str(mem.power) == str(UVK5_POWER_LEVELS[2]): + _mem.txpower = POWER_HIGH + elif str(mem.power) == str(UVK5_POWER_LEVELS[1]): + _mem.txpower = POWER_MEDIUM + else: + _mem.txpower = POWER_LOW + + for setting in mem.extra: + sname = setting.get_name() + svalue = setting.value.get_value() + + if sname == "bclo": + _mem.bclo = svalue and 1 or 0 + + if sname == "pttid": + _mem.dtmf_pttid = self._pttid_list.index(svalue) + + if sname == "frev": + _mem.freq_reverse = svalue and 1 or 0 + + if sname == "dtmfdecode": + _mem.dtmf_decode = svalue and 1 or 0 + + if sname == "scrambler": + _mem.scrambler = ( + _mem.scrambler & 0xf0) | SCRAMBLER_LIST.index(svalue) + + if number < 200 and sname == "scanlists": + if svalue == "1": + _mem4.channel_attributes[number].is_scanlist1 = 1 + _mem4.channel_attributes[number].is_scanlist2 = 0 + elif svalue == "2": + _mem4.channel_attributes[number].is_scanlist1 = 0 + _mem4.channel_attributes[number].is_scanlist2 = 1 + elif svalue == "1+2": + _mem4.channel_attributes[number].is_scanlist1 = 1 + _mem4.channel_attributes[number].is_scanlist2 = 1 + else: + _mem4.channel_attributes[number].is_scanlist1 = 0 + _mem4.channel_attributes[number].is_scanlist2 = 0 + + return mem + + +@directory.register +class UVK5Radio(UVK5RadioBase): + @classmethod + def k5_approve_firmware(cls, firmware): + approved_prefixes = ('k5_2.01.', 'app_2.01.', '2.01.', + '1o11', '4.00.', 'k5_4.00.') + return any(firmware.startswith(x) for x in approved_prefixes) + + @classmethod + def detect_from_serial(cls, pipe): + firmware = _sayhello(pipe) + for rclass in cls.detected_models(): + if rclass.k5_approve_firmware(firmware): + return rclass + raise errors.RadioError('Firmware %r not supported' % firmware) + + +@directory.register +class RA79Radio(UVK5Radio): + """Retevis RA79""" + VENDOR = "Retevis" + MODEL = "RA79" From 0409de8a8178e385a641e46cee05764f6e5c67b7 Mon Sep 17 00:00:00 2001 From: ei2081 <175010419+ei2081@users.noreply.github.com> Date: Thu, 15 Aug 2024 02:30:38 +0100 Subject: [PATCH 2/4] uk-k5(99) variant Updated comment to foldable. Referred DTCS and CTCSS tone codes to the chirp_common as they are the same. Made a bunch of other changes but reverted. Added a registered radio variant with the firmward code of my handset only. Tested it with connect radio, download memories (ca. 10 of), replace memories (ca. 125 of - repeaters, marine, pmr, etc), wrote to radio. Radio still works so far. --- chirp/drivers/uvk5.py | 89 ++++++++++++++++++++----------------------- 1 file changed, 42 insertions(+), 47 deletions(-) diff --git a/chirp/drivers/uvk5.py b/chirp/drivers/uvk5.py index 2065c126..b2e2a20a 100644 --- a/chirp/drivers/uvk5.py +++ b/chirp/drivers/uvk5.py @@ -1,29 +1,30 @@ -# Quansheng UV-K5 driver (c) 2023 Jacek Lipkowski -# -# based on template.py Copyright 2012 Dan Smith -# -# -# This is a preliminary version of a driver for the UV-K5 -# It is based on my reverse engineering effort described here: -# 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. -# -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - +""" + # Quansheng UV-K5 driver (c) 2023 Jacek Lipkowski + # + # based on template.py Copyright 2012 Dan Smith + # + # + # This is a preliminary version of a driver for the UV-K5 + # It is based on my reverse engineering effort described here: + # 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. + # + # + # This program is free software: you can redistribute it and/or modify + # it under the terms of the GNU General Public License as published by + # the Free Software Foundation, either version 2 of the License, or + # (at your option) any later version. + # + # This program is distributed in the hope that it will be useful, + # but WITHOUT ANY WARRANTY; without even the implied warranty of + # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + # GNU General Public License for more details. + # + # You should have received a copy of the GNU General Public License + # along with this program. If not, see . + """ import struct import logging @@ -272,29 +273,11 @@ TONE_RDCS = 3 -CTCSS_TONES = [ - 67.0, 69.3, 71.9, 74.4, 77.0, 79.7, 82.5, 85.4, - 88.5, 91.5, 94.8, 97.4, 100.0, 103.5, 107.2, 110.9, - 114.8, 118.8, 123.0, 127.3, 131.8, 136.5, 141.3, 146.2, - 151.4, 156.7, 159.8, 162.2, 165.5, 167.9, 171.3, 173.8, - 177.3, 179.9, 183.5, 186.2, 189.9, 192.8, 196.6, 199.5, - 203.5, 206.5, 210.7, 218.1, 225.7, 229.1, 233.6, 241.8, - 250.3, 254.1 -] +CTCSS_TONES = chirp_common.TONES # yes they were an exact match. # lifted from ft4.py -DTCS_CODES = [ - 23, 25, 26, 31, 32, 36, 43, 47, 51, 53, 54, - 65, 71, 72, 73, 74, 114, 115, 116, 122, 125, 131, - 132, 134, 143, 145, 152, 155, 156, 162, 165, 172, 174, - 205, 212, 223, 225, 226, 243, 244, 245, 246, 251, 252, - 255, 261, 263, 265, 266, 271, 274, 306, 311, 315, 325, - 331, 332, 343, 346, 351, 356, 364, 365, 371, 411, 412, - 413, 423, 431, 432, 445, 446, 452, 454, 455, 462, 464, - 465, 466, 503, 506, 516, 523, 526, 532, 546, 565, 606, - 612, 624, 627, 631, 632, 654, 662, 664, 703, 712, 723, - 731, 732, 734, 743, 754 -] +DTCS_CODES = chirp_common.DTCS_CODES # yes, they were an exact match also + FLOCK_LIST = ["Off", "FCC", "CE", "GB", "430", "438"] @@ -2083,3 +2066,15 @@ class RA79Radio(UVK5Radio): """Retevis RA79""" VENDOR = "Retevis" MODEL = "RA79" + +# EI2081 +# This is from a radio i purchased from AliExperess store "Eagle Store" +# which was quoted as both "Quansheng UV-K6" (in title) and "Quansheng UV-K5(99)" in the imagery. +# I'm not 100% sure it's legit Quansheng! +@directory.register +class UVK5_99_Radio(UVK5Radio): + MODEL = "UV-K5(99)" + @classmethod + def k5_approve_firmware(cls, firmware): + approved_prefixes = ('OSFW-bd90ca3') + return any(firmware.startswith(x) for x in approved_prefixes) From 99f5708c4ae187238a79cd5fee8aa01a0c583032 Mon Sep 17 00:00:00 2001 From: ei2081 <175010419+ei2081@users.noreply.github.com> Date: Thu, 15 Aug 2024 02:34:32 +0100 Subject: [PATCH 3/4] Delete uvk5_99.py didn't mean for that one to commit --- chirp/drivers/uvk5_99.py | 2090 -------------------------------------- 1 file changed, 2090 deletions(-) delete mode 100644 chirp/drivers/uvk5_99.py diff --git a/chirp/drivers/uvk5_99.py b/chirp/drivers/uvk5_99.py deleted file mode 100644 index c9869f39..00000000 --- a/chirp/drivers/uvk5_99.py +++ /dev/null @@ -1,2090 +0,0 @@ -""" Quansheng UV-K5(99) undriver (c) 2024 dara/ei2081 -# -# based on Quansheng UV-K5 driver (c) 2023 Jacek Lipkowski -# -# based on template.py Copyright 2012 Dan Smith -# -# -# This is a preliminary version of a driver for the UV-K5 -# It is based on my reverse engineering effort described here: -# 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. -# -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# -# """ - - -import struct -import logging - -from chirp import chirp_common, directory, bitwise, memmap, errors, util -from chirp.settings import RadioSetting, RadioSettingGroup, \ - RadioSettingValueBoolean, RadioSettingValueList, \ - RadioSettingValueInteger, RadioSettingValueString, \ - RadioSettings - -LOG = logging.getLogger(__name__) - -# Show the obfuscated version of commands. Not needed normally, but -# might be useful for someone who is debugging a similar radio -DEBUG_SHOW_OBFUSCATED_COMMANDS = False - -# Show the memory being written/received. Not needed normally, because -# this is the same information as in the packet hexdumps, but -# might be useful for someone debugging some obscure memory issue -DEBUG_SHOW_MEMORY_ACTIONS = False - -MEM_FORMAT = """ -#seekto 0x0000; -struct { - ul32 freq; - ul32 offset; - u8 rxcode; - u8 txcode; - - u8 txcodeflag:4, - rxcodeflag:4; - - //u8 flags1; - u8 flags1_unknown7:1, - flags1_unknown6:1, - flags1_unknown5:1, - enable_am:1, - flags1_unknown3:1, - is_in_scanlist:1, - shift:2; - - //u8 flags2; - u8 flags2_unknown7:1, - flags2_unknown6:1, - flags2_unknown5:1, - bclo:1, - txpower:2, - bandwidth:1, - freq_reverse:1; - - //u8 dtmf_flags; - u8 dtmf_flags_unknown7:1, - dtmf_flags_unknown6:1, - dtmf_flags_unknown5:1, - dtmf_flags_unknown4:1, - dtmf_flags_unknown3:1, - dtmf_pttid:2, - dtmf_decode:1; - - - u8 step; - u8 scrambler; -} channel[214]; - -#seekto 0xd60; -struct { -u8 is_scanlist1:1, -is_scanlist2:1, -compander:2, -is_free:1, -band:3; -} channel_attributes[200]; - -#seekto 0xe40; -ul16 fmfreq[20]; - -#seekto 0xe70; -u8 call_channel; -u8 squelch; -u8 max_talk_time; -u8 noaa_autoscan; -u8 key_lock; -u8 vox_switch; -u8 vox_level; -u8 mic_gain; -u8 unknown3; -u8 channel_display_mode; -u8 crossband; -u8 battery_save; -u8 dual_watch; -u8 backlight_auto_mode; -u8 tail_note_elimination; -u8 vfo_open; - -#seekto 0xe90; -u8 beep_control; -u8 key1_shortpress_action; -u8 key1_longpress_action; -u8 key2_shortpress_action; -u8 key2_longpress_action; -u8 scan_resume_mode; -u8 auto_keypad_lock; -u8 power_on_dispmode; -u8 password[4]; - -#seekto 0xea0; -u8 keypad_tone; -u8 language; - -#seekto 0xea8; -u8 alarm_mode; -u8 reminding_of_end_talk; -u8 repeater_tail_elimination; - -#seekto 0xeb0; -char logo_line1[16]; -char logo_line2[16]; - -#seekto 0xed0; -struct { -u8 side_tone; -char separate_code; -char group_call_code; -u8 decode_response; -u8 auto_reset_time; -u8 preload_time; -u8 first_code_persist_time; -u8 hash_persist_time; -u8 code_persist_time; -u8 code_interval_time; -u8 permit_remote_kill; -} dtmf_settings; - -#seekto 0xee0; -struct { -char dtmf_local_code[3]; -char unused1[5]; -char kill_code[5]; -char unused2[3]; -char revive_code[5]; -char unused3[3]; -char dtmf_up_code[16]; -char dtmf_down_code[16]; -} dtmf_settings_numbers; - -#seekto 0xf18; -u8 scanlist_default; -u8 scanlist1_priority_scan; -u8 scanlist1_priority_ch1; -u8 scanlist1_priority_ch2; -u8 scanlist2_priority_scan; -u8 scanlist2_priority_ch1; -u8 scanlist2_priority_ch2; -u8 scanlist_unknown_0xff; - - -#seekto 0xf40; -struct { -u8 flock; -u8 tx350; -u8 killed; -u8 tx200; -u8 tx500; -u8 en350; -u8 enscramble; -} lock; - -#seekto 0xf50; -struct { -char name[16]; -} channelname[200]; - -#seekto 0x1c00; -struct { -char name[8]; -char number[3]; -char unused_00[5]; -} dtmfcontact[16]; - -#seekto 0x1ed0; -struct { -struct { - u8 start; - u8 mid; - u8 end; -} low; -struct { - u8 start; - u8 mid; - u8 end; -} medium; -struct { - u8 start; - u8 mid; - u8 end; -} high; -u8 unused_00[7]; -} perbandpowersettings[7]; - -#seekto 0x1f40; -ul16 battery_level[6]; -""" - -# bits that we will save from the channel structure (mostly unknown) -SAVE_MASK_0A = 0b11001100 -SAVE_MASK_0B = 0b11101100 -SAVE_MASK_0C = 0b11100000 -SAVE_MASK_0D = 0b11111000 -SAVE_MASK_0E = 0b11110001 -SAVE_MASK_0F = 0b11110000 - -# flags1 -FLAGS1_OFFSET_NONE = 0b00 -FLAGS1_OFFSET_MINUS = 0b10 -FLAGS1_OFFSET_PLUS = 0b01 - -POWER_HIGH = 0b10 -POWER_MEDIUM = 0b01 -POWER_LOW = 0b00 - -# power -UVK5_POWER_LEVELS = [chirp_common.PowerLevel("Low", watts=1.50), - chirp_common.PowerLevel("Med", watts=3.00), - chirp_common.PowerLevel("High", watts=5.00), - ] - -# scrambler -SCRAMBLER_LIST = ["off", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10"] - -# channel display mode -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 - -# ctcss/dcs codes -TMODES = ["", "Tone", "DTCS", "DTCS"] -TONE_NONE = 0 -TONE_CTCSS = 1 -TONE_DCS = 2 -TONE_RDCS = 3 - - -CTCSS_TONES = [ - 67.0, 69.3, 71.9, 74.4, 77.0, 79.7, 82.5, 85.4, - 88.5, 91.5, 94.8, 97.4, 100.0, 103.5, 107.2, 110.9, - 114.8, 118.8, 123.0, 127.3, 131.8, 136.5, 141.3, 146.2, - 151.4, 156.7, 159.8, 162.2, 165.5, 167.9, 171.3, 173.8, - 177.3, 179.9, 183.5, 186.2, 189.9, 192.8, 196.6, 199.5, - 203.5, 206.5, 210.7, 218.1, 225.7, 229.1, 233.6, 241.8, - 250.3, 254.1 -] - -# lifted from ft4.py -DTCS_CODES = [ - 23, 25, 26, 31, 32, 36, 43, 47, 51, 53, 54, - 65, 71, 72, 73, 74, 114, 115, 116, 122, 125, 131, - 132, 134, 143, 145, 152, 155, 156, 162, 165, 172, 174, - 205, 212, 223, 225, 226, 243, 244, 245, 246, 251, 252, - 255, 261, 263, 265, 266, 271, 274, 306, 311, 315, 325, - 331, 332, 343, 346, 351, 356, 364, 365, 371, 411, 412, - 413, 423, 431, 432, 445, 446, 452, 454, 455, 462, 464, - 465, 466, 503, 506, 516, 523, 526, 532, 546, 565, 606, - 612, 624, 627, 631, 632, 654, 662, 664, 703, 712, 723, - 731, 732, 734, 743, 754 -] - -FLOCK_LIST = ["Off", "FCC", "CE", "GB", "430", "438"] - -SCANRESUME_LIST = ["TO: Resume after 5 seconds", - "CO: Resume after signal disappears", - "SE: Stop scanning after receiving a signal"] - -WELCOME_LIST = ["Full Screen", "Welcome Info", "Voltage"] -KEYPADTONE_LIST = ["Off", "Chinese", "English"] -LANGUAGE_LIST = ["Chinese", "English"] -ALARMMODE_LIST = ["SITE", "TONE"] -REMENDOFTALK_LIST = ["Off", "ROGER", "MDC"] -RTE_LIST = ["Off", "100ms", "200ms", "300ms", "400ms", - "500ms", "600ms", "700ms", "800ms", "900ms"] - -MEM_SIZE = 0x2000 # size of all memory -PROG_SIZE = 0x1d00 # size of the memory that we will write -MEM_BLOCK = 0x80 # largest block of memory that we can reliably write - -# fm radio supported frequencies -FMMIN = 76.0 -FMMAX = 108.0 - -# bands supported by the UV-K5 -BANDS = { - 0: [50.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, 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] - } - -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", - "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"] - -SCANLIST_LIST = ["None", "1", "2", "1+2"] - -DTMF_CHARS = "0123456789ABCD*# " -DTMF_CHARS_ID = "0123456789ABCDabcd" -DTMF_CHARS_KILL = "0123456789ABCDabcd" -DTMF_CHARS_UPDOWN = "0123456789ABCDabcd#* " -DTMF_CODE_CHARS = "ABCD*# " -DTMF_DECODE_RESPONSE_LIST = ["None", "Ring", "Reply", "Both"] - -KEYACTIONS_LIST = ["None", "Flashlight on/off", "Power select", - "Monitor", "Scan on/off", "VOX on/off", - "Alarm on/off", "FM radio on/off", "Transmit 1750 Hz"] - - -def xorarr(data: bytes): - """the communication is obfuscated using this fine mechanism""" - tbl = [22, 108, 20, 230, 46, 145, 13, 64, 33, 53, 213, 64, 19, 3, 233, 128] - ret = b"" - idx = 0 - for byte in data: - ret += bytes([byte ^ tbl[idx]]) - idx = (idx+1) % len(tbl) - return ret - - -def calculate_crc16_xmodem(data: bytes): - """ - if this crc was used for communication to AND from the radio, then it - would be a measure to increase reliability. - but it's only used towards the radio, so it's for further obfuscation - """ - poly = 0x1021 - crc = 0x0 - for byte in data: - crc = crc ^ (byte << 8) - for _ in range(8): - crc = crc << 1 - if crc & 0x10000: - crc = (crc ^ poly) & 0xFFFF - return crc & 0xFFFF - - -def _send_command(serport, data: bytes): - """Send a command to UV-K5 radio""" - LOG.debug("Sending command (unobfuscated) len=0x%4.4x:\n%s", - len(data), util.hexprint(data)) - - crc = calculate_crc16_xmodem(data) - 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: - result = serport.write(command) - except Exception as e: - raise errors.RadioError("Error writing data to radio") from e - return result - - -def _receive_reply(serport): - header = serport.read(4) - if not header: - raise errors.RadioError("No response from radio") - elif len(header) != 4: - LOG.warning("Header short read: [%s] len=%i", - util.hexprint(header), len(header)) - raise errors.RadioError("Header short read") - elif header[0] != 0xAB or header[1] != 0xCD or header[3] != 0x00: - LOG.warning("Bad response header: %s len=%i", - util.hexprint(header), len(header)) - raise errors.RadioError("Bad response header") - - cmd = serport.read(int(header[2])) - if len(cmd) != int(header[2]): - LOG.warning("Body short read: [%s] len=%i", - util.hexprint(cmd), len(cmd)) - raise errors.RadioError("Command body short read") - - footer = serport.read(4) - - if len(footer) != 4: - LOG.warning("Footer short read: [%s] len=%i", - util.hexprint(footer), len(footer)) - raise errors.RadioError("Footer short read") - - if footer[2] != 0xDC or footer[3] != 0xBA: - LOG.debug("Reply before bad response footer (obfuscated)" - "len=0x%4.4x:\n%s", len(cmd), util.hexprint(cmd)) - LOG.warning("Bad response footer: %s len=%i", - util.hexprint(footer), len(footer)) - raise errors.RadioError("Bad response footer") - - if DEBUG_SHOW_OBFUSCATED_COMMANDS: - LOG.debug("Received reply (obfuscated) len=0x%4.4x:\n%s", - len(cmd), util.hexprint(cmd)) - - cmd2 = xorarr(cmd) - - LOG.debug("Received reply (unobfuscated) len=0x%4.4x:\n%s", - len(cmd2), util.hexprint(cmd2)) - - return cmd2 - - -def _getstring(data: bytes, begin, maxlen): - tmplen = min(maxlen+1, len(data)) - ss = [data[i] for i in range(begin, tmplen)] - key = 0 - for key, val in enumerate(ss): - if val < ord(' ') or val > ord('~'): - return ''.join(chr(x) for x in ss[0:key]) - return '' - - -def _sayhello(serport): - hellopacket = b"\x14\x05\x04\x00\x6a\x39\x57\x64" - - tries = 5 - while True: - LOG.debug("Sending hello packet") - _send_command(serport, hellopacket) - rep = _receive_reply(serport) - if rep: - break - tries -= 1 - if tries == 0: - LOG.warning("Failed to initialise radio") - raise errors.RadioError("Failed to initialize radio") - if rep.startswith(b'\x18\x05'): - raise errors.RadioError("Radio is in programming mode, " - "restart radio into normal mode") - firmware = _getstring(rep, 4, 24) - - LOG.info("Found firmware: %s", firmware) - return firmware - - -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" + \ - struct.pack("> 8) & 0xff): - return True - - LOG.warning("Bad data from writemem") - raise errors.RadioError("Bad response to writemem") - - -def _resetradio(serport): - resetpacket = b"\xdd\x05\x00\x00" - _send_command(serport, resetpacket) - - -def do_download(radio): - """download eeprom from radio""" - serport = radio.pipe - serport.timeout = 0.5 - status = chirp_common.Status() - status.cur = 0 - status.max = MEM_SIZE - status.msg = "Downloading from radio" - radio.status_fn(status) - - eeprom = b"" - f = _sayhello(serport) - if not f: - raise errors.RadioError('Unable to determine firmware version') - - if not radio.k5_approve_firmware(f): - raise errors.RadioError( - 'Firmware version is not supported by this driver') - - radio.metadata = {'uvk5_firmware': f} - - addr = 0 - while addr < MEM_SIZE: - data = _readmem(serport, addr, MEM_BLOCK) - status.cur = addr - radio.status_fn(status) - - if data and len(data) == MEM_BLOCK: - eeprom += data - addr += MEM_BLOCK - else: - raise errors.RadioError("Memory download incomplete") - - return memmap.MemoryMapBytes(eeprom) - - -def do_upload(radio): - """upload configuration to radio eeprom""" - serport = radio.pipe - serport.timeout = 0.5 - status = chirp_common.Status() - status.cur = 0 - status.msg = "Uploading to radio" - - if radio._upload_calibration: - status.max = MEM_SIZE - radio._cal_start - start_addr = radio._cal_start - stop_addr = MEM_SIZE - else: - status.max = PROG_SIZE - start_addr = 0 - stop_addr = PROG_SIZE - - radio.status_fn(status) - - f = _sayhello(serport) - if not f: - raise errors.RadioError('Unable to determine firmware version') - - if not radio.k5_approve_firmware(f): - raise errors.RadioError( - 'Firmware version is not supported by this driver') - LOG.info('Uploading image from firmware %r to radio with %r', - radio.metadata.get('uvk5_firmware', 'unknown'), f) - addr = start_addr - while addr < stop_addr: - dat = radio.get_mmap()[addr:addr+MEM_BLOCK] - _writemem(serport, dat, addr) - status.cur = addr - start_addr - radio.status_fn(status) - if dat: - addr += MEM_BLOCK - else: - raise errors.RadioError("Memory upload incomplete") - status.msg = "Uploaded OK" - - _resetradio(serport) - - return True - - -def _find_band(nolimits, hz): - mhz = hz/1000000.0 - if nolimits: - B = BANDS_NOLIMITS - else: - B = BANDS - - # currently the hacked firmware sets band=1 below 50 MHz - if nolimits and mhz < 50.0: - return 1 - - for a in B: - if mhz >= B[a][0] and mhz <= B[a][1]: - return a - return False - - -class UVK5RadioBase(chirp_common.CloneModeRadio): - """Quansheng UV-K5""" - VENDOR = "Quansheng" - MODEL = "UV-K5" - BAUD_RATE = 38400 - _cal_start = 0 - _expanded_limits = False - _upload_calibration = False - _pttid_list = ["off", "BOT", "EOT", "BOTH"] - _steps = [1.0, 2.5, 5.0, 6.25, 10.0, 12.5, 25.0, 8.33] - - @classmethod - def k5_approve_firmware(cls, firmware): - # All subclasses must implement this - raise NotImplementedError() - - def get_prompts(x=None): - rp = chirp_common.RadioPrompts() - rp.experimental = _( - '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 ' - 'and keep it. This can be later used to recover the ' - 'original settings. \n\n' - 'some details are not yet implemented') - rp.pre_download = _( - "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 download image from device.\n\n" - "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 on the radio " - "with the cable already attached") - return rp - - # Return information about this radio's features, including - # how many memories it has, what bands it supports, etc - def get_features(self): - rf = chirp_common.RadioFeatures() - rf.has_bank = False - rf.valid_dtcs_codes = DTCS_CODES - rf.has_rx_dtcs = True - rf.has_ctone = True - rf.has_settings = True - rf.has_comment = False - rf.valid_name_length = 10 - rf.valid_power_levels = UVK5_POWER_LEVELS - rf.valid_special_chans = list(SPECIALS.keys()) - rf.valid_duplexes = ["", "-", "+", "off"] - - # hack so we can input any frequency, - # the 0.1 and 0.01 steps don't work unfortunately - rf.valid_tuning_steps = list(self._steps) - - rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS", "Cross"] - rf.valid_cross_modes = ["Tone->Tone", "Tone->DTCS", "DTCS->Tone", - "->Tone", "->DTCS", "DTCS->", "DTCS->DTCS"] - - rf.valid_characters = chirp_common.CHARSET_ASCII - rf.valid_modes = ["FM", "NFM", "AM", "NAM"] - rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS", "Cross"] - - rf.valid_skips = [""] - - # This radio supports memories 1-200, 201-214 are the VFO memories - rf.memory_bounds = (1, 200) - - rf.valid_bands = [] - for a in BANDS_NOLIMITS: - rf.valid_bands.append( - (int(BANDS_NOLIMITS[a][0]*1000000), - int(BANDS_NOLIMITS[a][1]*1000000))) - return rf - - # Do a download of the radio from the serial port - def sync_in(self): - self._mmap = do_download(self) - self.process_mmap() - - # Do an upload of the radio to the serial port - def sync_out(self): - do_upload(self) - - def _check_firmware_at_load(self): - firmware = self.metadata.get('uvk5_firmware') - if not firmware: - LOG.warning(_('This image is missing firmware information. ' - 'It may have been generated with an old or ' - 'modified version of CHIRP. It is advised that ' - 'you download a fresh image from your radio and ' - 'use that going forward for the best safety and ' - 'compatibility.')) - elif not self.k5_approve_firmware(self.metadata['uvk5_firmware']): - raise errors.RadioError( - 'Image firmware is %r but is not supported by ' - 'this driver' % firmware) - - # Convert the raw byte array into a memory object structure - def process_mmap(self): - self._check_firmware_at_load() - self._memobj = bitwise.parse(MEM_FORMAT, self._mmap) - - # Return a raw representation of the memory object, which - # is very helpful for development - def get_raw_memory(self, number): - return repr(self._memobj.channel[number-1]) - - def _find_band(self, hz): - return _find_band(self._expanded_limits, hz) - - def validate_memory(self, mem): - msgs = super().validate_memory(mem) - - if mem.duplex == 'off': - return msgs - - # 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 = self._find_band(txfreq) - if band is False: - msg = "Transmit frequency %.4f MHz is not supported by this radio"\ - % (txfreq/1000000.0) - msgs.append(chirp_common.ValidationError(msg)) - - band = self._find_band(mem.freq) - if band is False: - msg = "The frequency %.4f MHz is not supported by this radio" \ - % (mem.freq/1000000.0) - msgs.append(chirp_common.ValidationError(msg)) - - return msgs - - def _set_tone(self, mem, _mem): - ((txmode, txtone, txpol), - (rxmode, rxtone, rxpol)) = chirp_common.split_tone_encode(mem) - - if txmode == "Tone": - txtoval = CTCSS_TONES.index(txtone) - txmoval = 0b01 - elif txmode == "DTCS": - txmoval = txpol == "R" and 0b11 or 0b10 - txtoval = DTCS_CODES.index(txtone) - else: - txmoval = 0 - txtoval = 0 - - if rxmode == "Tone": - rxtoval = CTCSS_TONES.index(rxtone) - rxmoval = 0b01 - elif rxmode == "DTCS": - rxmoval = rxpol == "R" and 0b11 or 0b10 - rxtoval = DTCS_CODES.index(rxtone) - else: - rxmoval = 0 - rxtoval = 0 - - _mem.rxcodeflag = rxmoval - _mem.txcodeflag = txmoval - _mem.rxcode = rxtoval - _mem.txcode = txtoval - - def _get_tone(self, mem, _mem): - rxtype = _mem.rxcodeflag - txtype = _mem.txcodeflag - rx_tmode = TMODES[rxtype] - tx_tmode = TMODES[txtype] - - rx_tone = tx_tone = None - - if tx_tmode == "Tone": - if _mem.txcode < len(CTCSS_TONES): - tx_tone = CTCSS_TONES[_mem.txcode] - else: - tx_tone = 0 - tx_tmode = "" - elif tx_tmode == "DTCS": - if _mem.txcode < len(DTCS_CODES): - tx_tone = DTCS_CODES[_mem.txcode] - else: - tx_tone = 0 - tx_tmode = "" - - if rx_tmode == "Tone": - if _mem.rxcode < len(CTCSS_TONES): - rx_tone = CTCSS_TONES[_mem.rxcode] - else: - rx_tone = 0 - rx_tmode = "" - elif rx_tmode == "DTCS": - if _mem.rxcode < len(DTCS_CODES): - rx_tone = DTCS_CODES[_mem.rxcode] - else: - rx_tone = 0 - rx_tmode = "" - - tx_pol = txtype == 0x03 and "R" or "N" - rx_pol = rxtype == 0x03 and "R" or "N" - - chirp_common.split_tone_decode(mem, (tx_tmode, tx_tone, tx_pol), - (rx_tmode, rx_tone, rx_pol)) - - def _get_mem_extra(self, mem, _mem): - tmpscn = SCANLIST_LIST[0] - - # We'll also look at the channel attributes if a memory has them - if mem.number <= 200: - _mem3 = self._memobj.channel_attributes[mem.number - 1] - # free memory bit - if _mem3.is_free > 0: - mem.empty = True - # scanlists - if _mem3.is_scanlist1 > 0 and _mem3.is_scanlist2 > 0: - tmpscn = SCANLIST_LIST[3] # "1+2" - elif _mem3.is_scanlist1 > 0: - tmpscn = SCANLIST_LIST[1] # "1" - elif _mem3.is_scanlist2 > 0: - tmpscn = SCANLIST_LIST[2] # "2" - - mem.extra = RadioSettingGroup("Extra", "extra") - - # BCLO - is_bclo = not mem.empty and bool(_mem.bclo > 0) - rs = RadioSetting("bclo", "BCLO", RadioSettingValueBoolean(is_bclo)) - mem.extra.append(rs) - - # Frequency reverse - reverse tx/rx frequency - is_frev = not mem.empty and bool(_mem.freq_reverse > 0) - rs = RadioSetting("frev", "FreqRev", RadioSettingValueBoolean(is_frev)) - mem.extra.append(rs) - - # PTTID - try: - pttid = self._pttid_list[_mem.dtmf_pttid] - except IndexError: - pttid = 0 - rs = RadioSetting("pttid", "PTTID", RadioSettingValueList( - self._pttid_list, pttid)) - mem.extra.append(rs) - - # DTMF DECODE - is_dtmf = not mem.empty and bool(_mem.dtmf_decode > 0) - rs = RadioSetting("dtmfdecode", _("DTMF decode"), - RadioSettingValueBoolean(is_dtmf)) - mem.extra.append(rs) - - # Scrambler - if _mem.scrambler & 0x0f < len(SCRAMBLER_LIST): - enc = _mem.scrambler & 0x0f - else: - enc = 0 - - rs = RadioSetting("scrambler", _("Scrambler"), RadioSettingValueList( - SCRAMBLER_LIST, SCRAMBLER_LIST[enc])) - mem.extra.append(rs) - - rs = RadioSetting("scanlists", _("Scanlists"), RadioSettingValueList( - SCANLIST_LIST, tmpscn)) - mem.extra.append(rs) - - def _get_mem_mode(self, _mem): - if _mem.enable_am > 0: - if _mem.bandwidth > 0: - return "NAM" - else: - return "AM" - else: - if _mem.bandwidth > 0: - return "NFM" - else: - return "FM" - - def _get_specials(self): - return dict(SPECIALS) - - # 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): - - mem = chirp_common.Memory() - - if isinstance(number2, str): - number = self._get_specials()[number2] - mem.extd_number = number2 - else: - number = number2 - 1 - - mem.number = number + 1 - - _mem = self._memobj.channel[number] - - # We'll consider any blank (i.e. 0 MHz frequency) to be empty - if (_mem.freq == 0xffffffff) or (_mem.freq == 0): - mem.empty = True - - self._get_mem_extra(mem, _mem) - - if mem.empty: - return mem - - if number > 199: - mem.immutable = ["name", "scanlists"] - else: - _mem2 = self._memobj.channelname[number] - for char in _mem2.name: - if str(char) == "\xFF" or str(char) == "\x00": - break - mem.name += str(char) - mem.name = mem.name.rstrip() - - # Convert your low-level frequency to Hertz - mem.freq = int(_mem.freq)*10 - mem.offset = int(_mem.offset)*10 - - if (mem.offset == 0): - mem.duplex = '' - else: - if _mem.shift == FLAGS1_OFFSET_MINUS: - if _mem.freq == _mem.offset: - # fake tx disable by setting tx to 0 MHz - mem.duplex = 'off' - mem.offset = 0 - else: - mem.duplex = '-' - elif _mem.shift == FLAGS1_OFFSET_PLUS: - mem.duplex = '+' - else: - mem.duplex = '' - - # tone data - self._get_tone(mem, _mem) - - mem.mode = self._get_mem_mode(_mem) - - # tuning step - try: - mem.tuning_step = self._steps[_mem.step] - except IndexError: - mem.tuning_step = 2.5 - - # power - if _mem.txpower == POWER_HIGH: - mem.power = UVK5_POWER_LEVELS[2] - elif _mem.txpower == POWER_MEDIUM: - mem.power = UVK5_POWER_LEVELS[1] - else: - mem.power = UVK5_POWER_LEVELS[0] - - # We'll consider any blank (i.e. 0 MHz frequency) to be empty - if (_mem.freq == 0xffffffff) or (_mem.freq == 0): - mem.empty = True - else: - mem.empty = False - - return mem - - def set_settings(self, settings): - _mem = self._memobj - for element in settings: - if not isinstance(element, RadioSetting): - self.set_settings(element) - continue - - # basic settings - - # call channel - if element.get_name() == "call_channel": - _mem.call_channel = int(element.value)-1 - - # squelch - if element.get_name() == "squelch": - _mem.squelch = int(element.value) - # 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 - - # mic gain - if element.get_name() == "mic_gain": - _mem.mic_gain = int(element.value) - - # Channel display mode - if element.get_name() == "channel_display_mode": - _mem.channel_display_mode = CHANNELDISP_LIST.index( - str(element.value)) - - # Crossband receiving/transmitting - if element.get_name() == "crossband": - _mem.crossband = CROSSBAND_LIST.index(str(element.value)) - - # Battery Save - if element.get_name() == "battery_save": - _mem.battery_save = BATSAVE_LIST.index(str(element.value)) - # Dual Watch - 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 - - # VFO Open - if element.get_name() == "vfo_open": - _mem.vfo_open = element.value and 1 or 0 - - # Beep control - if element.get_name() == "beep_control": - _mem.beep_control = element.value and 1 or 0 - - # Scan resume mode - if element.get_name() == "scan_resume_mode": - _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 - - # Power on display mode - if element.get_name() == "welcome_mode": - _mem.power_on_dispmode = WELCOME_LIST.index(str(element.value)) - - # Keypad Tone - if element.get_name() == "keypad_tone": - _mem.keypad_tone = KEYPADTONE_LIST.index(str(element.value)) - - # Language - if element.get_name() == "language": - _mem.language = LANGUAGE_LIST.index(str(element.value)) - - # Alarm mode - if element.get_name() == "alarm_mode": - _mem.alarm_mode = ALARMMODE_LIST.index(str(element.value)) - - # Reminding of end of talk - if element.get_name() == "reminding_of_end_talk": - _mem.reminding_of_end_talk = REMENDOFTALK_LIST.index( - str(element.value)) - - # Repeater tail tone elimination - if element.get_name() == "repeater_tail_elimination": - _mem.repeater_tail_elimination = RTE_LIST.index( - str(element.value)) - - # Logo string 1 - if element.get_name() == "logo1": - b = str(element.value).rstrip("\x20\xff\x00")+"\x00"*12 - _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]+"\x00\xff\xff\xff" - - # unlock settings - - # FLOCK - if element.get_name() == "flock": - _mem.lock.flock = FLOCK_LIST.index(str(element.value)) - - # 350TX - if element.get_name() == "tx350": - _mem.lock.tx350 = element.value and 1 or 0 - - # 200TX - if element.get_name() == "tx200": - _mem.lock.tx200 = element.value and 1 or 0 - - # 500TX - if element.get_name() == "tx500": - _mem.lock.tx500 = element.value and 1 or 0 - - # 350EN - if element.get_name() == "en350": - _mem.lock.en350 = element.value and 1 or 0 - - # SCREN - if element.get_name() == "enscramble": - _mem.lock.enscramble = element.value and 1 or 0 - - # KILLED - if element.get_name() == "killed": - _mem.lock.killed = element.value and 1 or 0 - - # fm radio - for i in range(1, 21): - freqname = "FM_" + str(i) - if element.get_name() == freqname: - val = str(element.value).strip() - try: - val2 = int(float(val)*10) - except Exception: - val2 = 0xffff - - if val2 < FMMIN*10 or val2 > FMMAX*10: - val2 = 0xffff -# raise errors.InvalidValueError( -# "FM radio frequency should be a value " -# "in the range %.1f - %.1f" % (FMMIN , FMMAX)) - _mem.fmfreq[i-1] = val2 - - # dtmf settings - if element.get_name() == "dtmf_side_tone": - _mem.dtmf_settings.side_tone = \ - element.value and 1 or 0 - - if element.get_name() == "dtmf_separate_code": - _mem.dtmf_settings.separate_code = str(element.value) - - if element.get_name() == "dtmf_group_call_code": - _mem.dtmf_settings.group_call_code = element.value - - if element.get_name() == "dtmf_decode_response": - _mem.dtmf_settings.decode_response = \ - DTMF_DECODE_RESPONSE_LIST.index(str(element.value)) - - if element.get_name() == "dtmf_auto_reset_time": - _mem.dtmf_settings.auto_reset_time = \ - int(int(element.value)/10) - - if element.get_name() == "dtmf_preload_time": - _mem.dtmf_settings.preload_time = \ - int(int(element.value)/10) - - if element.get_name() == "dtmf_first_code_persist_time": - _mem.dtmf_settings.first_code_persist_time = \ - int(int(element.value)/10) - - if element.get_name() == "dtmf_hash_persist_time": - _mem.dtmf_settings.hash_persist_time = \ - int(int(element.value)/10) - - if element.get_name() == "dtmf_code_persist_time": - _mem.dtmf_settings.code_persist_time = \ - int(int(element.value)/10) - - if element.get_name() == "dtmf_code_interval_time": - _mem.dtmf_settings.code_interval_time = \ - int(int(element.value)/10) - - if element.get_name() == "dtmf_permit_remote_kill": - _mem.dtmf_settings.permit_remote_kill = \ - element.value and 1 or 0 - - if element.get_name() == "dtmf_dtmf_local_code": - k = str(element.value).rstrip("\x20\xff\x00") + "\x00"*3 - _mem.dtmf_settings_numbers.dtmf_local_code = k[0:3] - - if element.get_name() == "dtmf_dtmf_up_code": - k = str(element.value).strip("\x20\xff\x00") + "\x00"*16 - _mem.dtmf_settings_numbers.dtmf_up_code = k[0:16] - - if element.get_name() == "dtmf_dtmf_down_code": - k = str(element.value).rstrip("\x20\xff\x00") + "\x00"*16 - _mem.dtmf_settings_numbers.dtmf_down_code = k[0:16] - - if element.get_name() == "dtmf_kill_code": - k = str(element.value).strip("\x20\xff\x00") + "\x00"*5 - _mem.dtmf_settings_numbers.kill_code = k[0:5] - - if element.get_name() == "dtmf_revive_code": - k = str(element.value).strip("\x20\xff\x00") + "\x00"*5 - _mem.dtmf_settings_numbers.revive_code = k[0:5] - - # dtmf contacts - for i in range(1, 17): - varname = "DTMF_" + str(i) - if element.get_name() == varname: - k = str(element.value).rstrip("\x20\xff\x00") + "\x00"*8 - _mem.dtmfcontact[i-1].name = k[0:8] - - varnumname = "DTMFNUM_" + str(i) - if element.get_name() == varnumname: - k = str(element.value).rstrip("\x20\xff\x00") + "\xff"*3 - _mem.dtmfcontact[i-1].number = k[0:3] - - # scanlist stuff - if element.get_name() == "scanlist_default": - val = (int(element.value) == 2) and 1 or 0 - _mem.scanlist_default = val - - if element.get_name() == "scanlist1_priority_scan": - _mem.scanlist1_priority_scan = \ - element.value and 1 or 0 - - if element.get_name() == "scanlist2_priority_scan": - _mem.scanlist2_priority_scan = \ - element.value and 1 or 0 - - if element.get_name() == "scanlist1_priority_ch1" or \ - element.get_name() == "scanlist1_priority_ch2" or \ - element.get_name() == "scanlist2_priority_ch1" or \ - element.get_name() == "scanlist2_priority_ch2": - - val = int(element.value) - - if val > 200 or val < 1: - val = 0xff - else: - val -= 1 - - if element.get_name() == "scanlist1_priority_ch1": - _mem.scanlist1_priority_ch1 = val - if element.get_name() == "scanlist1_priority_ch2": - _mem.scanlist1_priority_ch2 = val - if element.get_name() == "scanlist2_priority_ch1": - _mem.scanlist2_priority_ch1 = val - if element.get_name() == "scanlist2_priority_ch2": - _mem.scanlist2_priority_ch2 = val - - if element.get_name() == "key1_shortpress_action": - _mem.key1_shortpress_action = KEYACTIONS_LIST.index( - str(element.value)) - - if element.get_name() == "key1_longpress_action": - _mem.key1_longpress_action = KEYACTIONS_LIST.index( - str(element.value)) - - if element.get_name() == "key2_shortpress_action": - _mem.key2_shortpress_action = KEYACTIONS_LIST.index( - str(element.value)) - - if element.get_name() == "key2_longpress_action": - _mem.key2_longpress_action = KEYACTIONS_LIST.index( - str(element.value)) - - if element.get_name() == "nolimits": - LOG.warning("User expanded band limits") - self._expanded_limits = bool(element.value) - - def get_settings(self): - _mem = self._memobj - basic = RadioSettingGroup("basic", "Basic Settings") - keya = RadioSettingGroup("keya", "Programmable keys") - dtmf = RadioSettingGroup("dtmf", "DTMF Settings") - dtmfc = RadioSettingGroup("dtmfc", "DTMF Contacts") - scanl = RadioSettingGroup("scn", "Scan Lists") - unlock = RadioSettingGroup("unlock", "Unlock Settings") - fmradio = RadioSettingGroup("fmradio", _("FM Radio")) - - roinfo = RadioSettingGroup("roinfo", _("Driver information")) - - top = RadioSettings( - basic, keya, dtmf, dtmfc, scanl, unlock, fmradio, roinfo) - - # Programmable keys - tmpval = int(_mem.key1_shortpress_action) - if tmpval >= len(KEYACTIONS_LIST): - tmpval = 0 - rs = RadioSetting("key1_shortpress_action", "Side key 1 short press", - RadioSettingValueList( - KEYACTIONS_LIST, KEYACTIONS_LIST[tmpval])) - keya.append(rs) - - tmpval = int(_mem.key1_longpress_action) - if tmpval >= len(KEYACTIONS_LIST): - tmpval = 0 - rs = RadioSetting("key1_longpress_action", "Side key 1 long press", - RadioSettingValueList( - KEYACTIONS_LIST, KEYACTIONS_LIST[tmpval])) - keya.append(rs) - - tmpval = int(_mem.key2_shortpress_action) - if tmpval >= len(KEYACTIONS_LIST): - tmpval = 0 - rs = RadioSetting("key2_shortpress_action", "Side key 2 short press", - RadioSettingValueList( - KEYACTIONS_LIST, KEYACTIONS_LIST[tmpval])) - keya.append(rs) - - tmpval = int(_mem.key2_longpress_action) - if tmpval >= len(KEYACTIONS_LIST): - tmpval = 0 - rs = RadioSetting("key2_longpress_action", "Side key 2 long press", - RadioSettingValueList( - KEYACTIONS_LIST, KEYACTIONS_LIST[tmpval])) - keya.append(rs) - - # DTMF settings - tmppr = bool(_mem.dtmf_settings.side_tone > 0) - rs = RadioSetting( - "dtmf_side_tone", - "DTMF Sidetone", - RadioSettingValueBoolean(tmppr)) - dtmf.append(rs) - - tmpval = str(_mem.dtmf_settings.separate_code) - if tmpval not in DTMF_CODE_CHARS: - tmpval = '*' - val = RadioSettingValueString(1, 1, tmpval) - val.set_charset(DTMF_CODE_CHARS) - rs = RadioSetting("dtmf_separate_code", "Separate Code", val) - dtmf.append(rs) - - tmpval = str(_mem.dtmf_settings.group_call_code) - if tmpval not in DTMF_CODE_CHARS: - tmpval = '#' - val = RadioSettingValueString(1, 1, tmpval) - val.set_charset(DTMF_CODE_CHARS) - rs = RadioSetting("dtmf_group_call_code", "Group Call Code", val) - dtmf.append(rs) - - tmpval = _mem.dtmf_settings.decode_response - if tmpval >= len(DTMF_DECODE_RESPONSE_LIST): - tmpval = 0 - rs = RadioSetting("dtmf_decode_response", "Decode Response", - RadioSettingValueList( - DTMF_DECODE_RESPONSE_LIST, - DTMF_DECODE_RESPONSE_LIST[tmpval])) - dtmf.append(rs) - - tmpval = _mem.dtmf_settings.auto_reset_time - if tmpval > 60 or tmpval < 5: - tmpval = 5 - rs = RadioSetting("dtmf_auto_reset_time", - "Auto reset time (s)", - RadioSettingValueInteger(5, 60, tmpval)) - dtmf.append(rs) - - tmpval = int(_mem.dtmf_settings.preload_time) - if tmpval > 100 or tmpval < 3: - tmpval = 30 - tmpval *= 10 - rs = RadioSetting("dtmf_preload_time", - "Pre-load time (ms)", - RadioSettingValueInteger(30, 1000, tmpval, 10)) - dtmf.append(rs) - - tmpval = int(_mem.dtmf_settings.first_code_persist_time) - if tmpval > 100 or tmpval < 3: - tmpval = 30 - tmpval *= 10 - rs = RadioSetting("dtmf_first_code_persist_time", - "First code persist time (ms)", - RadioSettingValueInteger(30, 1000, tmpval, 10)) - dtmf.append(rs) - - tmpval = int(_mem.dtmf_settings.hash_persist_time) - if tmpval > 100 or tmpval < 3: - tmpval = 30 - tmpval *= 10 - rs = RadioSetting("dtmf_hash_persist_time", - "#/* persist time (ms)", - RadioSettingValueInteger(30, 1000, tmpval, 10)) - dtmf.append(rs) - - tmpval = int(_mem.dtmf_settings.code_persist_time) - if tmpval > 100 or tmpval < 3: - tmpval = 30 - tmpval *= 10 - rs = RadioSetting("dtmf_code_persist_time", - "Code persist time (ms)", - RadioSettingValueInteger(30, 1000, tmpval, 10)) - dtmf.append(rs) - - tmpval = int(_mem.dtmf_settings.code_interval_time) - if tmpval > 100 or tmpval < 3: - tmpval = 30 - tmpval *= 10 - rs = RadioSetting("dtmf_code_interval_time", - "Code interval time (ms)", - RadioSettingValueInteger(30, 1000, tmpval, 10)) - dtmf.append(rs) - - tmpval = bool(_mem.dtmf_settings.permit_remote_kill > 0) - rs = RadioSetting( - "dtmf_permit_remote_kill", - "Permit remote kill", - RadioSettingValueBoolean(tmpval)) - dtmf.append(rs) - - tmpval = str(_mem.dtmf_settings_numbers.dtmf_local_code).upper().strip( - "\x00\xff\x20") - for i in tmpval: - if i in DTMF_CHARS_ID: - continue - else: - tmpval = "103" - break - val = RadioSettingValueString(3, 3, tmpval) - val.set_charset(DTMF_CHARS_ID) - rs = RadioSetting("dtmf_dtmf_local_code", - "Local code (3 chars 0-9 ABCD)", val) - dtmf.append(rs) - - tmpval = str(_mem.dtmf_settings_numbers.dtmf_up_code).upper().strip( - "\x00\xff\x20") - for i in tmpval: - if i in DTMF_CHARS_UPDOWN or i == "": - continue - else: - tmpval = "123" - break - val = RadioSettingValueString(1, 16, tmpval) - val.set_charset(DTMF_CHARS_UPDOWN) - rs = RadioSetting("dtmf_dtmf_up_code", - "Up code (1-16 chars 0-9 ABCD*#)", val) - dtmf.append(rs) - - tmpval = str(_mem.dtmf_settings_numbers.dtmf_down_code).upper().strip( - "\x00\xff\x20") - for i in tmpval: - if i in DTMF_CHARS_UPDOWN: - continue - else: - tmpval = "456" - break - val = RadioSettingValueString(1, 16, tmpval) - val.set_charset(DTMF_CHARS_UPDOWN) - rs = RadioSetting("dtmf_dtmf_down_code", - "Down code (1-16 chars 0-9 ABCD*#)", val) - dtmf.append(rs) - - tmpval = str(_mem.dtmf_settings_numbers.kill_code).upper().strip( - "\x00\xff\x20") - for i in tmpval: - if i in DTMF_CHARS_KILL: - continue - else: - tmpval = "77777" - break - if not len(tmpval) == 5: - tmpval = "77777" - val = RadioSettingValueString(5, 5, tmpval) - val.set_charset(DTMF_CHARS_KILL) - rs = RadioSetting("dtmf_kill_code", - "Kill code (5 chars 0-9 ABCD)", val) - dtmf.append(rs) - - tmpval = str(_mem.dtmf_settings_numbers.revive_code).upper().strip( - "\x00\xff\x20") - for i in tmpval: - if i in DTMF_CHARS_KILL: - continue - else: - tmpval = "88888" - break - if not len(tmpval) == 5: - tmpval = "88888" - val = RadioSettingValueString(5, 5, tmpval) - val.set_charset(DTMF_CHARS_KILL) - rs = RadioSetting("dtmf_revive_code", - "Revive code (5 chars 0-9 ABCD)", val) - dtmf.append(rs) - - for i in range(1, 17): - varname = "DTMF_"+str(i) - varnumname = "DTMFNUM_"+str(i) - vardescr = "DTMF Contact "+str(i)+" name" - varinumdescr = "DTMF Contact "+str(i)+" number" - - cntn = str(_mem.dtmfcontact[i-1].name).strip("\x20\x00\xff") - cntnum = str(_mem.dtmfcontact[i-1].number).strip("\x20\x00\xff") - - val = RadioSettingValueString(0, 8, cntn) - rs = RadioSetting(varname, vardescr, val) - dtmfc.append(rs) - - val = RadioSettingValueString(0, 3, cntnum) - val.set_charset(DTMF_CHARS) - rs = RadioSetting(varnumname, varinumdescr, val) - dtmfc.append(rs) - rs.set_doc("DTMF Contacts are 3 codes (valid: 0-9 * # ABCD), " - "or an empty string") - - # scanlists - if _mem.scanlist_default == 1: - tmpsc = 2 - else: - tmpsc = 1 - rs = RadioSetting("scanlist_default", - "Default scanlist", - RadioSettingValueInteger(1, 2, tmpsc)) - scanl.append(rs) - - tmppr = bool((_mem.scanlist1_priority_scan & 1) > 0) - rs = RadioSetting( - "scanlist1_priority_scan", - "Scanlist 1 priority channel scan", - RadioSettingValueBoolean(tmppr)) - scanl.append(rs) - - tmpch = _mem.scanlist1_priority_ch1 + 1 - if tmpch > 200: - tmpch = 0 - rs = RadioSetting("scanlist1_priority_ch1", - "Scanlist 1 priority channel 1 (0 - off)", - RadioSettingValueInteger(0, 200, tmpch)) - scanl.append(rs) - - tmpch = _mem.scanlist1_priority_ch2 + 1 - if tmpch > 200: - tmpch = 0 - rs = RadioSetting("scanlist1_priority_ch2", - "Scanlist 1 priority channel 2 (0 - off)", - RadioSettingValueInteger(0, 200, tmpch)) - scanl.append(rs) - - tmppr = bool((_mem.scanlist2_priority_scan & 1) > 0) - rs = RadioSetting( - "scanlist2_priority_scan", - "Scanlist 2 priority channel scan", - RadioSettingValueBoolean(tmppr)) - scanl.append(rs) - - tmpch = _mem.scanlist2_priority_ch1 + 1 - if tmpch > 200: - tmpch = 0 - rs = RadioSetting("scanlist2_priority_ch1", - "Scanlist 2 priority channel 1 (0 - off)", - RadioSettingValueInteger(0, 200, tmpch)) - scanl.append(rs) - - tmpch = _mem.scanlist2_priority_ch2 + 1 - if tmpch > 200: - tmpch = 0 - rs = RadioSetting("scanlist2_priority_ch2", - "Scanlist 2 priority channel 2 (0 - off)", - RadioSettingValueInteger(0, 200, tmpch)) - scanl.append(rs) - - # basic settings - - # call channel - tmpc = _mem.call_channel+1 - if tmpc > 200: - tmpc = 1 - rs = RadioSetting("call_channel", "One key call channel", - RadioSettingValueInteger(1, 200, tmpc)) - basic.append(rs) - - # squelch - tmpsq = _mem.squelch - if tmpsq > 9: - tmpsq = 1 - rs = RadioSetting("squelch", "Squelch", - RadioSettingValueInteger(0, 9, tmpsq)) - basic.append(rs) - - # TOT - tmptot = _mem.max_talk_time - if tmptot > 10: - tmptot = 10 - rs = RadioSetting( - "tot", - "Max talk time [min]", - RadioSettingValueInteger(0, 10, tmptot)) - basic.append(rs) - - # NOAA autoscan - rs = RadioSetting( - "noaa_autoscan", - "NOAA Autoscan", RadioSettingValueBoolean( - 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: - tmpvox = 10 - rs = RadioSetting("vox_level", "VOX Level", - RadioSettingValueInteger(1, 10, tmpvox)) - basic.append(rs) - - # Mic gain - tmpmicgain = _mem.mic_gain - if tmpmicgain > 4: - tmpmicgain = 4 - rs = RadioSetting("mic_gain", "Mic Gain", - RadioSettingValueInteger(0, 4, tmpmicgain)) - basic.append(rs) - - # Channel display mode - tmpchdispmode = _mem.channel_display_mode - if tmpchdispmode >= len(CHANNELDISP_LIST): - tmpchdispmode = 0 - rs = RadioSetting( - "channel_display_mode", - "Channel display mode", - RadioSettingValueList( - CHANNELDISP_LIST, - CHANNELDISP_LIST[tmpchdispmode])) - basic.append(rs) - - # Crossband receiving/transmitting - tmpcross = _mem.crossband - if tmpcross >= len(CROSSBAND_LIST): - tmpcross = 0 - rs = RadioSetting( - "crossband", - "Cross-band receiving/transmitting", - RadioSettingValueList( - CROSSBAND_LIST, - CROSSBAND_LIST[tmpcross])) - 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[tmpbatsave])) - basic.append(rs) - - # Dual watch - tmpdual = _mem.dual_watch - if tmpdual >= len(DUALWATCH_LIST): - tmpdual = 0 - rs = RadioSetting("dualwatch", "Dual Watch", RadioSettingValueList( - 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", - "Tail tone elimination", - RadioSettingValueBoolean( - bool(_mem.tail_note_elimination > 0))) - basic.append(rs) - - # VFO open - rs = RadioSetting("vfo_open", "VFO open", - RadioSettingValueBoolean(bool(_mem.vfo_open > 0))) - basic.append(rs) - - # Beep control - rs = RadioSetting( - "beep_control", - "Beep control", - RadioSettingValueBoolean(bool(_mem.beep_control > 0))) - basic.append(rs) - - # Scan resume mode - tmpscanres = _mem.scan_resume_mode - if tmpscanres >= len(SCANRESUME_LIST): - tmpscanres = 0 - rs = RadioSetting( - "scan_resume_mode", - "Scan resume mode", - RadioSettingValueList( - SCANRESUME_LIST, - 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", - "Auto keypad lock", - RadioSettingValueBoolean(bool(_mem.auto_keypad_lock > 0))) - basic.append(rs) - - # Power on display mode - tmpdispmode = _mem.power_on_dispmode - if tmpdispmode >= len(WELCOME_LIST): - tmpdispmode = 0 - rs = RadioSetting( - "welcome_mode", - "Power on display mode", - RadioSettingValueList( - WELCOME_LIST, - WELCOME_LIST[tmpdispmode])) - basic.append(rs) - - # Keypad Tone - tmpkeypadtone = _mem.keypad_tone - if tmpkeypadtone >= len(KEYPADTONE_LIST): - tmpkeypadtone = 0 - rs = RadioSetting("keypad_tone", "Keypad tone", RadioSettingValueList( - KEYPADTONE_LIST, KEYPADTONE_LIST[tmpkeypadtone])) - basic.append(rs) - - # Language - tmplanguage = _mem.language - if tmplanguage >= len(LANGUAGE_LIST): - tmplanguage = 0 - rs = RadioSetting("language", "Language", RadioSettingValueList( - LANGUAGE_LIST, LANGUAGE_LIST[tmplanguage])) - basic.append(rs) - - # Alarm mode - tmpalarmmode = _mem.alarm_mode - if tmpalarmmode >= len(ALARMMODE_LIST): - tmpalarmmode = 0 - rs = RadioSetting("alarm_mode", "Alarm mode", RadioSettingValueList( - ALARMMODE_LIST, ALARMMODE_LIST[tmpalarmmode])) - basic.append(rs) - - # Reminding of end of talk - tmpalarmmode = _mem.reminding_of_end_talk - if tmpalarmmode >= len(REMENDOFTALK_LIST): - tmpalarmmode = 0 - rs = RadioSetting( - "reminding_of_end_talk", - "Reminding of end of talk", - RadioSettingValueList( - REMENDOFTALK_LIST, - REMENDOFTALK_LIST[tmpalarmmode])) - basic.append(rs) - - # Repeater tail tone elimination - tmprte = _mem.repeater_tail_elimination - if tmprte >= len(RTE_LIST): - tmprte = 0 - rs = RadioSetting( - "repeater_tail_elimination", - "Repeater tail tone elimination", - RadioSettingValueList(RTE_LIST, RTE_LIST[tmprte])) - basic.append(rs) - - # Logo string 1 - logo1 = str(_mem.logo_line1).strip("\x20\x00\xff") + "\x00" - logo1 = _getstring(logo1.encode('ascii', errors='ignore'), 0, 12) - rs = RadioSetting("logo1", _("Logo string 1 (12 characters)"), - RadioSettingValueString(0, 12, logo1)) - basic.append(rs) - - # Logo string 2 - logo2 = str(_mem.logo_line2).strip("\x20\x00\xff") + "\x00" - logo2 = _getstring(logo2.encode('ascii', errors='ignore'), 0, 12) - rs = RadioSetting("logo2", _("Logo string 2 (12 characters)"), - RadioSettingValueString(0, 12, logo2)) - basic.append(rs) - - # FM radio - for i in range(1, 21): - freqname = "FM_"+str(i) - fmfreq = _mem.fmfreq[i-1]/10.0 - if fmfreq < FMMIN or fmfreq > FMMAX: - rs = RadioSetting(freqname, freqname, - RadioSettingValueString(0, 5, "")) - else: - rs = RadioSetting(freqname, freqname, - RadioSettingValueString(0, 5, str(fmfreq))) - - fmradio.append(rs) - - # unlock settings - - # F-LOCK - tmpflock = _mem.lock.flock - if tmpflock >= len(FLOCK_LIST): - tmpflock = 0 - rs = RadioSetting( - "flock", "F-LOCK", - RadioSettingValueList(FLOCK_LIST, FLOCK_LIST[tmpflock])) - unlock.append(rs) - - # 350TX - rs = RadioSetting("tx350", "350TX - unlock 350-400 MHz TX", - RadioSettingValueBoolean( - bool(_mem.lock.tx350 > 0))) - unlock.append(rs) - - # Killed - rs = RadioSetting("Killed", "KILLED Device was disabled (via DTMF)", - RadioSettingValueBoolean( - bool(_mem.lock.killed > 0))) - unlock.append(rs) - - # 200TX - rs = RadioSetting("tx200", "200TX - unlock 174-350 MHz TX", - RadioSettingValueBoolean( - bool(_mem.lock.tx200 > 0))) - unlock.append(rs) - - # 500TX - rs = RadioSetting("tx500", "500TX - unlock 500-600 MHz TX", - RadioSettingValueBoolean( - bool(_mem.lock.tx500 > 0))) - unlock.append(rs) - - # 350EN - rs = RadioSetting("en350", "350EN - unlock 350-400 MHz RX", - RadioSettingValueBoolean( - bool(_mem.lock.en350 > 0))) - unlock.append(rs) - - # SCREEN - rs = RadioSetting("scrambler", "SCREN - scrambler enable", - RadioSettingValueBoolean( - bool(_mem.lock.enscramble > 0))) - unlock.append(rs) - - # readonly info - # Firmware - firmware = self.metadata.get('uvk5_firmware', 'UNKNOWN') - - val = RadioSettingValueString(0, 128, firmware) - val.set_mutable(False) - rs = RadioSetting("fw_ver", "Firmware Version", val) - roinfo.append(rs) - - # No limits version for hacked firmware - val = RadioSettingValueBoolean(self._expanded_limits) - rs = RadioSetting("nolimits", "Limits disabled for modified firmware", - val) - rs.set_warning(_( - 'This should only be enabled if you are using modified firmware ' - 'that supports wider frequency coverage. Enabling this will cause ' - 'CHIRP not to enforce OEM restrictions and may lead to undefined ' - 'or unregulated behavior. Use at your own risk!'), - safe_value=False) - roinfo.append(rs) - - return top - - def _set_mem_mode(self, _mem, mode): - if mode == "NFM": - _mem.bandwidth = 1 - _mem.enable_am = 0 - elif mode == "FM": - _mem.bandwidth = 0 - _mem.enable_am = 0 - elif mode == "NAM": - _mem.bandwidth = 1 - _mem.enable_am = 1 - elif mode == "AM": - _mem.bandwidth = 0 - _mem.enable_am = 1 - - # Store details about a high-level memory to the memory map - # This is called when a user edits a memory in the UI - def set_memory(self, mem): - number = mem.number-1 - - # Get a low-level memory object mapped to the image - _mem = self._memobj.channel[number] - _mem4 = self._memobj - # empty memory - if mem.empty: - _mem.set_raw(b"\xFF" * 16) - if number < 200: - _mem2 = self._memobj.channelname[number] - _mem2.set_raw(b"\xFF" * 16) - _mem4.channel_attributes[number].is_scanlist1 = 0 - _mem4.channel_attributes[number].is_scanlist2 = 0 - # Compander in other models, not supported here - _mem4.channel_attributes[number].compander = 0 - _mem4.channel_attributes[number].is_free = 1 - _mem4.channel_attributes[number].band = 0x7 - return mem - - # clean the channel memory, restore some bits if it was used before - if _mem.get_raw(asbytes=False)[0] == "\xff": - # this was an empty memory - _mem.set_raw(b"\x00" * 16) - else: - # this memory wasn't empty, save some bits that we don't know the - # meaning of, or that we don't support yet - prev_0a = _mem.get_raw()[0x0a] & SAVE_MASK_0A - prev_0b = _mem.get_raw()[0x0b] & SAVE_MASK_0B - prev_0c = _mem.get_raw()[0x0c] & SAVE_MASK_0C - prev_0d = _mem.get_raw()[0x0d] & SAVE_MASK_0D - prev_0e = _mem.get_raw()[0x0e] & SAVE_MASK_0E - prev_0f = _mem.get_raw()[0x0f] & SAVE_MASK_0F - _mem.set_raw(b"\x00" * 10 + - bytes([prev_0a, prev_0b, prev_0c, - prev_0d, prev_0e, prev_0f])) - - if number < 200: - _mem4.channel_attributes[number].is_scanlist1 = 0 - _mem4.channel_attributes[number].is_scanlist2 = 0 - _mem4.channel_attributes[number].compander = 0 - _mem4.channel_attributes[number].is_free = 1 - _mem4.channel_attributes[number].band = 0x7 - - # find band - band = _find_band(self, mem.freq) - - self._set_mem_mode(_mem, mem.mode) - - # frequency/offset - _mem.freq = mem.freq/10 - _mem.offset = mem.offset/10 - - if mem.duplex == "": - _mem.offset = 0 - _mem.shift = 0 - elif mem.duplex == '-': - _mem.shift = FLAGS1_OFFSET_MINUS - elif mem.duplex == '+': - _mem.shift = FLAGS1_OFFSET_PLUS - elif mem.duplex == 'off': - # we fake tx disable by setting the tx freq to 0 MHz - _mem.shift = FLAGS1_OFFSET_MINUS - _mem.offset = _mem.freq - - # set band - if number < 200: - _mem4.channel_attributes[number].is_free = 0 - _mem4.channel_attributes[number].band = band - - # channels >200 are the 14 VFO chanells and don't have names - if number < 200: - _mem2 = self._memobj.channelname[number] - tag = mem.name.ljust(10) + "\x00"*6 - _mem2.name = tag # Store the alpha tag - - # tone data - self._set_tone(mem, _mem) - - # step - _mem.step = self._steps.index(mem.tuning_step) - - # tx power - if str(mem.power) == str(UVK5_POWER_LEVELS[2]): - _mem.txpower = POWER_HIGH - elif str(mem.power) == str(UVK5_POWER_LEVELS[1]): - _mem.txpower = POWER_MEDIUM - else: - _mem.txpower = POWER_LOW - - for setting in mem.extra: - sname = setting.get_name() - svalue = setting.value.get_value() - - if sname == "bclo": - _mem.bclo = svalue and 1 or 0 - - if sname == "pttid": - _mem.dtmf_pttid = self._pttid_list.index(svalue) - - if sname == "frev": - _mem.freq_reverse = svalue and 1 or 0 - - if sname == "dtmfdecode": - _mem.dtmf_decode = svalue and 1 or 0 - - if sname == "scrambler": - _mem.scrambler = ( - _mem.scrambler & 0xf0) | SCRAMBLER_LIST.index(svalue) - - if number < 200 and sname == "scanlists": - if svalue == "1": - _mem4.channel_attributes[number].is_scanlist1 = 1 - _mem4.channel_attributes[number].is_scanlist2 = 0 - elif svalue == "2": - _mem4.channel_attributes[number].is_scanlist1 = 0 - _mem4.channel_attributes[number].is_scanlist2 = 1 - elif svalue == "1+2": - _mem4.channel_attributes[number].is_scanlist1 = 1 - _mem4.channel_attributes[number].is_scanlist2 = 1 - else: - _mem4.channel_attributes[number].is_scanlist1 = 0 - _mem4.channel_attributes[number].is_scanlist2 = 0 - - return mem - - -@directory.register -class UVK5Radio(UVK5RadioBase): - @classmethod - def k5_approve_firmware(cls, firmware): - approved_prefixes = ('k5_2.01.', 'app_2.01.', '2.01.', - '1o11', '4.00.', 'k5_4.00.') - return any(firmware.startswith(x) for x in approved_prefixes) - - @classmethod - def detect_from_serial(cls, pipe): - firmware = _sayhello(pipe) - for rclass in cls.detected_models(): - if rclass.k5_approve_firmware(firmware): - return rclass - raise errors.RadioError('Firmware %r not supported' % firmware) - - -@directory.register -class RA79Radio(UVK5Radio): - """Retevis RA79""" - VENDOR = "Retevis" - MODEL = "RA79" From e4c9626fcbfd70f99dbddbd792515248bfe24da0 Mon Sep 17 00:00:00 2001 From: ei2081 <175010419+ei2081@users.noreply.github.com> Date: Thu, 15 Aug 2024 02:45:10 +0100 Subject: [PATCH 4/4] Update uvk5.py style checks were not happy, lets see how this goes --- chirp/drivers/uvk5.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/chirp/drivers/uvk5.py b/chirp/drivers/uvk5.py index b2e2a20a..880e47b8 100644 --- a/chirp/drivers/uvk5.py +++ b/chirp/drivers/uvk5.py @@ -273,10 +273,8 @@ TONE_RDCS = 3 -CTCSS_TONES = chirp_common.TONES # yes they were an exact match. - -# lifted from ft4.py -DTCS_CODES = chirp_common.DTCS_CODES # yes, they were an exact match also +CTCSS_TONES = chirp_common.TONES # tone list was exact match to common. +DTCS_CODES = chirp_common.DTCS_CODES # tone list was exact match to common. FLOCK_LIST = ["Off", "FCC", "CE", "GB", "430", "438"] @@ -2067,13 +2065,17 @@ class RA79Radio(UVK5Radio): VENDOR = "Retevis" MODEL = "RA79" -# EI2081 -# This is from a radio i purchased from AliExperess store "Eagle Store" -# which was quoted as both "Quansheng UV-K6" (in title) and "Quansheng UV-K5(99)" in the imagery. + +# This is from a radio i purchased from AliExperess store "Eagle Store" +# which was quoted as both "Quansheng UV-K6" (in title) +# and "Quansheng UV-K5(99)" in the imagery. # I'm not 100% sure it's legit Quansheng! + + @directory.register class UVK5_99_Radio(UVK5Radio): MODEL = "UV-K5(99)" + @classmethod def k5_approve_firmware(cls, firmware): approved_prefixes = ('OSFW-bd90ca3')