<div dir="ltr">Hi,<div>I added the image file to issue 8591</div><div><br></div><div>Ran</div></div><br><div class="gmail_quote"><div dir="ltr" class="gmail_attr">On Wed, Feb 9, 2022 at 12:55 AM Ran Katz <<a href="mailto:rankatz@gmail.com">rankatz@gmail.com</a>> wrote:<br></div><blockquote class="gmail_quote" style="margin:0px 0px 0px 0.8ex;border-left-width:1px;border-left-style:solid;border-left-color:rgb(204,204,204);padding-left:1ex"># HG changeset patch<br>
# User Ran Katz <<a href="mailto:rankatz@gmai.com" target="_blank">rankatz@gmai.com</a>><br>
# Date 1644360111 -7200<br>
# Wed Feb 09 00:41:51 2022 +0200<br>
# Node ID 7cfb9fdcbb21c14217859e5ac61e42f99c157b05<br>
# Parent 164528caafdcef4cc871bdced922ca7985c71ec1<br>
Driver for TG-UV2+ (and probably TG-UV2)<br>
See Issues #8591 and #177<br>
Tested on TG-UV2+ , however teh code base (a 'C' utility) was developed a decade ago for the TG-UV2,<br>
and I could not find any differences.<br>
<br>
---------------<br>
user: Ran Katz <<a href="mailto:rankatz@gmai.com" target="_blank">rankatz@gmai.com</a>><br>
branch 'default'<br>
added chirp/drivers/tg_uv2p.py<br>
added tests/images/Quansheng_TG-UV2+.img<br>
<br>
diff --git a/chirp/drivers/tg_uv2p.py b/chirp/drivers/tg_uv2p.py<br>
new file mode 100644<br>
--- /dev/null<br>
+++ b/chirp/drivers/tg_uv2p.py<br>
@@ -0,0 +1,603 @@<br>
+# Copyright 2013 Dan Smith <<a href="mailto:dsmith@danplanet.com" target="_blank">dsmith@danplanet.com</a>><br>
+#<br>
+# This program is free software: you can redistribute it and/or modify<br>
+# it under the terms of the GNU General Public License as published by<br>
+# the Free Software Foundation, either version 2 of the License, or<br>
+# (at your option) any later version.<br>
+#<br>
+# This program is distributed in the hope that it will be useful,<br>
+# but WITHOUT ANY WARRANTY; without even the implied warranty of<br>
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the<br>
+# GNU General Public License for more details.<br>
+#<br>
+# You should have received a copy of the GNU General Public License<br>
+# along with this program. If not, see <<a href="http://www.gnu.org/licenses/" rel="noreferrer" target="_blank">http://www.gnu.org/licenses/</a>>.<br>
+<br>
+# This driver was derived from the:<br>
+# Quansheng TG-UV2 Utility by Mike Nix <<a href="mailto:mnix@wanm.com.au" target="_blank">mnix@wanm.com.au</a>><br>
+# (So thanks Mike!)<br>
+<br>
+import struct<br>
+import logging<br>
+from chirp import chirp_common, directory, bitwise, memmap, errors, util<br>
+from chirp.settings import RadioSetting, RadioSettingGroup, \<br>
+ RadioSettingValueBoolean, RadioSettingValueList, \<br>
+ RadioSettingValueInteger, RadioSettingValueString, \<br>
+ RadioSettingValueFloat, RadioSettings<br>
+from textwrap import dedent<br>
+<br>
+LOG = logging.getLogger(__name__)<br>
+<br>
+mem_format = """<br>
+struct memory {<br>
+ bbcd freq[4];<br>
+ bbcd offset[4];<br>
+ u8 rxtone;<br>
+ u8 txtone;<br>
+ u8 unknown1:2,<br>
+ txtmode:2,<br>
+ unknown2:2,<br>
+ rxtmode:2;<br>
+ u8 duplex;<br>
+ u8 unknown3:3,<br>
+ isnarrow:1,<br>
+ unknown4:2,<br>
+ not_scramble:1,<br>
+ not_revfreq:1;<br>
+ u8 flag3;<br>
+ u8 step;<br>
+ u8 power;<br>
+};<br>
+<br>
+struct bandflag {<br>
+ u8 scanadd:1,<br>
+ unknown1:3,<br>
+ band:4;<br>
+};<br>
+<br>
+struct tguv2_config {<br>
+ u8 unknown1;<br>
+ u8 squelch;<br>
+ u8 time_out_timer;<br>
+ u8 priority_channel;<br>
+<br>
+ u8 unknown2:7,<br>
+ keyunlocked:1;<br>
+ u8 busy_lockout;<br>
+ u8 vox;<br>
+ u8 unknown3;<br>
+<br>
+ u8 beep_tone_disabled;<br>
+ u8 display;<br>
+ u8 step;<br>
+ u8 unknown4;<br>
+<br>
+ u8 unknown5;<br>
+ u8 rxmode;<br>
+ u8 unknown6:7,<br>
+ no_end_tone:1;<br>
+ u8 vfo_model;<br>
+};<br>
+<br>
+struct vfo {<br>
+ u8 current; <br>
+ u8 chan; <br>
+ u8 memno;<br>
+};<br>
+<br>
+struct name {<br>
+ u8 name[6];<br>
+ u8 unknown1[10];<br>
+};<br>
+<br>
+#seekto 0x0000;<br>
+char ident[32];<br>
+u8 blank[16];<br>
+<br>
+struct memory channels[200];<br>
+struct memory bands[5];<br>
+<br>
+#seekto 0x0D30;<br>
+struct bandflag bandflags[200];<br>
+<br>
+#seekto 0x0E30;<br>
+struct tguv2_config settings;<br>
+struct vfo vfos[2];<br>
+u8 unk5; <br>
+u8 reserved2[9];<br>
+u8 band_restrict; <br>
+u8 txen350390; <br>
+<br>
+#seekto 0x0F30;<br>
+struct name names[200];<br>
+<br>
+"""<br>
+<br>
+def do_ident(radio):<br>
+ radio.pipe.timeout = 3<br>
+ radio.pipe.write("\x02PnOGdAM")<br>
+ for x in xrange(10):<br>
+ ack = radio.pipe.read(1)<br>
+ if ack == '\x06':<br>
+ break<br>
+ else:<br>
+ raise errors.RadioError("Radio did not ack programming mode")<br>
+ radio.pipe.write("\x40\x02")<br>
+ ident = radio.pipe.read(8)<br>
+ LOG.debug(util.hexprint(ident))<br>
+ if not ident.startswith('P5555'):<br>
+ raise errors.RadioError("Unsupported model")<br>
+ radio.pipe.write("\x06")<br>
+ ack = radio.pipe.read(1)<br>
+ if ack != "\x06":<br>
+ raise errors.RadioError("Radio did not ack ident")<br>
+<br>
+<br>
+def do_status(radio, direction, addr):<br>
+ status = chirp_common.Status()<br>
+ status.msg = "Cloning %s radio" % direction<br>
+ status.cur = addr<br>
+ status.max = 0x2000<br>
+ radio.status_fn(status)<br>
+<br>
+<br>
+def do_download(radio):<br>
+ do_ident(radio)<br>
+ data = "TG-UV2+ Radio Program Data v1.0\x00"<br>
+ data += ("\x00" * 16)<br>
+<br>
+ firstack = None<br>
+ for i in range(0, 0x2000, 8):<br>
+ frame = struct.pack(">cHB", "R", i, 8)<br>
+ radio.pipe.write(frame)<br>
+ result = radio.pipe.read(12)<br>
+ if not (result[0]=="W" and frame[1:4]==result[1:4]):<br>
+ LOG.debug(util.hexprint(result))<br>
+ raise errors.RadioError("Invalid response for address 0x%04x" % i)<br>
+ radio.pipe.write("\x06")<br>
+ ack = radio.pipe.read(1)<br>
+ if not firstack:<br>
+ firstack = ack<br>
+ else:<br>
+ if not ack == firstack:<br>
+ LOG.debug("first ack: %s ack received: %s",<br>
+ util.hexprint(firstack), util.hexprint(ack))<br>
+ raise errors.RadioError("Unexpected response")<br>
+ data += result[4:]<br>
+ do_status(radio, "from", i)<br>
+<br>
+ return memmap.MemoryMap(data)<br>
+<br>
+<br>
+def do_upload(radio):<br>
+ do_ident(radio)<br>
+ data = radio._mmap[0x0030:]<br>
+<br>
+ for i in range(0, 0x2000, 8):<br>
+ frame = struct.pack(">cHB", "W", i, 8)<br>
+ frame += data[i:i + 8]<br>
+ radio.pipe.write(frame)<br>
+ ack = radio.pipe.read(1)<br>
+ if ack != "\x06":<br>
+ LOG.debug("Radio NAK'd block at address 0x%04x" % i)<br>
+ raise errors.RadioError(<br>
+ "Radio NAK'd block at address 0x%04x" % i)<br>
+ LOG.debug("Radio ACK'd block at address 0x%04x" % i)<br>
+ do_status(radio, "to", i)<br>
+<br>
+DUPLEX = ["", "+", "-"]<br>
+TGUV2P_STEPS = [5, 6.25, 10, 12.5, 15, 20, 25, 30, 50, 100,]<br>
+CHARSET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_|* +-"<br>
+POWER_LEVELS = [chirp_common.PowerLevel("High", watts=10),<br>
+ chirp_common.PowerLevel("Med", watts=5),<br>
+ chirp_common.PowerLevel("Low", watts=1)]<br>
+POWER_LEVELS_STR = ["High", "Med", "Low"]<br>
+VALID_BANDS = [(88000000, 108000000),<br>
+ (136000000, 174000000),<br>
+ (350000000, 390000000),<br>
+ (400000000, 470000000),<br>
+ (470000000, 520000000)]<br>
+<br>
+@directory.register<br>
+class QuanshengTGUV2P(chirp_common.CloneModeRadio,<br>
+ chirp_common.ExperimentalRadio):<br>
+ """Quansheng TG-UV2+"""<br>
+ VENDOR = "Quansheng"<br>
+ MODEL = "TG-UV2+"<br>
+ BAUD_RATE = 9600<br>
+<br>
+ _memsize = 0x2000<br>
+<br>
+ @classmethod<br>
+ def get_prompts(cls):<br>
+ rp = chirp_common.RadioPrompts()<br>
+ rp.experimental = \<br>
+ ('Experimental version for TG-UV2/2+ radios '<br>
+ 'Proceed at your own risk!')<br>
+ rp.pre_download = _(dedent("""\<br>
+ 1. Turn radio off.<br>
+ 2. Connect cable to mic/spkr connector.<br>
+ 3. Make sure connector is firmly connected.<br>
+ 4. Turn radio on.<br>
+ 5. Ensure that the radio is tuned to channel with no activity.<br>
+ 6. Click OK to download image from device."""))<br>
+ rp.pre_upload = _(dedent("""\<br>
+ 1. Turn radio off.<br>
+ 2. Connect cable to mic/spkr connector.<br>
+ 3. Make sure connector is firmly connected.<br>
+ 4. Turn radio on.<br>
+ 5. Ensure that the radio is tuned to channel with no activity.<br>
+ 6. Click OK to upload image to device."""))<br>
+ return rp<br>
+<br>
+ def get_features(self):<br>
+ rf = chirp_common.RadioFeatures()<br>
+ rf.has_settings = True<br>
+ rf.has_cross = True<br>
+ rf.has_rx_dtcs = True<br>
+ rf.has_dtcs_polarity = True<br>
+ rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS", "Cross"]<br>
+ rf.valid_cross_modes = ["Tone->Tone", "Tone->DTCS", "DTCS->Tone",<br>
+ "->Tone", "->DTCS", "DTCS->", "DTCS->DTCS"]<br>
+ rf.valid_duplexes = DUPLEX<br>
+ rf.can_odd_split = False<br>
+ rf.valid_skips = ["", "S"]<br>
+ rf.valid_characters = CHARSET<br>
+ rf.valid_name_length = 6<br>
+ rf.valid_tuning_steps = TGUV2P_STEPS<br>
+ rf.valid_bands = VALID_BANDS<br>
+<br>
+ rf.valid_modes = ["FM", "NFM"]<br>
+ rf.valid_power_levels = POWER_LEVELS<br>
+ rf.has_ctone = True<br>
+ rf.has_bank = False<br>
+ rf.has_tuning_step = True<br>
+ rf.memory_bounds = (1, 200)<br>
+ return rf<br>
+<br>
+ def sync_in(self):<br>
+ try:<br>
+ self._mmap = do_download(self)<br>
+ except errors.RadioError:<br>
+ raise<br>
+ except Exception, e:<br>
+ raise errors.RadioError("Failed to communicate with radio: %s" % e)<br>
+ self.process_mmap()<br>
+<br>
+ def sync_out(self):<br>
+ try:<br>
+ do_upload(self)<br>
+ except errors.RadioError:<br>
+ raise<br>
+ except Exception, e:<br>
+ raise errors.RadioError("Failed to communicate with radio: %s" % e)<br>
+<br>
+ def process_mmap(self):<br>
+ self._memobj = bitwise.parse(mem_format, self._mmap)<br>
+<br>
+ def get_raw_memory(self, number):<br>
+ return repr(self._memobj.channels[number - 1])<br>
+<br>
+ def _decode_tone(self, _mem, which):<br>
+ def _get(field):<br>
+ return getattr(_mem, "%s%s" % (which, field))<br>
+<br>
+ value = _get('tone')<br>
+ tmode = _get('tmode')<br>
+<br>
+ if (value <= 104) and (tmode <= 3):<br>
+ if tmode == 0:<br>
+ mode = val = pol = None<br>
+ elif tmode == 1:<br>
+ mode = 'Tone'<br>
+ val = chirp_common.TONES[value]<br>
+ pol = None<br>
+ else:<br>
+ mode = 'DTCS'<br>
+ val = chirp_common.DTCS_CODES[value]<br>
+ pol = "N" if (tmode == 2) else "R"<br>
+ else:<br>
+ mode = val = pol = None<br>
+<br>
+ return mode, val, pol<br>
+<br>
+ def _encode_tone(self, _mem, which, mode, val, pol):<br>
+ def _set(field, value):<br>
+ setattr(_mem, "%s%s" % (which, field), value)<br>
+<br>
+ if (mode == "Tone"):<br>
+ _set("tone", chirp_common.TONES.index(val))<br>
+ _set("tmode", 0x01)<br>
+ elif mode == "DTCS":<br>
+ _set("tone", chirp_common.DTCS_CODES.index(val))<br>
+ if pol == "N":<br>
+ _set("tmode", 0x02)<br>
+ else:<br>
+ _set("tmode", 0x03)<br>
+ else:<br>
+ _set("tone", 0)<br>
+ _set("tmode", 0)<br>
+<br>
+ def _get_memobjs(self, number):<br>
+ if isinstance(number, str):<br>
+ return (getattr(self._memobj, number.lower()), None)<br>
+<br>
+ else:<br>
+ return (self._memobj.channels[number - 1],<br>
+ self._memobj.bandflags[number -1],<br>
+ self._memobj.names[number - 1].name)<br>
+<br>
+ def get_memory(self, number):<br>
+ _mem, _bf, _nam = self._get_memobjs(number)<br>
+ mem = chirp_common.Memory()<br>
+ if isinstance(number, str):<br>
+ mem.extd_number = number<br>
+ else:<br>
+ mem.number = number<br>
+<br>
+ if (_mem.freq.get_raw()[0] == "\xFF") or (_bf.band == "\x0F"):<br>
+ mem.empty = True<br>
+ return mem<br>
+<br>
+ mem.freq = int(_mem.freq) * 10<br>
+<br>
+ if _mem.offset.get_raw()[0] == "\xFF" :<br>
+ mem.offset = 0<br>
+ else:<br>
+ mem.offset = int(_mem.offset) * 10<br>
+<br>
+<br>
+ chirp_common.split_tone_decode(<br>
+ mem,<br>
+ self._decode_tone(_mem, "tx"),<br>
+ self._decode_tone(_mem, "rx"))<br>
+<br>
+ if 'step' in _mem and _mem.step > len(TGUV2P_STEPS):<br>
+ _mem.step = 0x00<br>
+ mem.tuning_step = TGUV2P_STEPS[_mem.step]<br>
+ mem.duplex = DUPLEX[_mem.duplex]<br>
+ mem.mode = _mem.isnarrow and "NFM" or "FM"<br>
+ mem.skip = "" if bool(_bf.scanadd) else "S"<br>
+ mem.power = POWER_LEVELS[_mem.power]<br>
+<br>
+ if _nam:<br>
+ for char in _nam:<br>
+ try:<br>
+ <a href="http://mem.name" rel="noreferrer" target="_blank">mem.name</a> += CHARSET[char]<br>
+ except IndexError:<br>
+ break<br>
+ <a href="http://mem.name" rel="noreferrer" target="_blank">mem.name</a> = mem.name.rstrip()<br>
+<br>
+ mem.extra = RadioSettingGroup("Extra", "extra")<br>
+<br>
+ rs = RadioSetting("not_scramble", "(not)SCRAMBLE",<br>
+ RadioSettingValueBoolean(_mem.not_scramble))<br>
+ mem.extra.append(rs)<br>
+<br>
+ rs = RadioSetting("not_revfreq", "(not)Reverse Duplex",<br>
+ RadioSettingValueBoolean(_mem.not_revfreq))<br>
+ mem.extra.append(rs)<br>
+<br>
+ return mem<br>
+<br>
+ def set_memory(self, mem):<br>
+ _mem, _bf, _nam = self._get_memobjs(mem.number)<br>
+<br>
+ _bf.set_raw("\xFF")<br>
+<br>
+<br>
+ if mem.empty:<br>
+ _mem.set_raw("\xFF" * 16)<br>
+ return<br>
+<br>
+ #if _mem.get_raw() == ("\xFF" * 16):<br>
+ _mem.set_raw("\x00" * 12 + "\xFF" * 2 + "\x00"*2)<br>
+<br>
+ _bf.scanadd = int(mem.skip != "S")<br>
+ _bf.band = 0x0F<br>
+ for idx, ele in enumerate(VALID_BANDS):<br>
+ if mem.freq >= ele[0] and mem.freq <= ele[1]:<br>
+ _bf.band = idx<br>
+<br>
+ _mem.freq = mem.freq / 10<br>
+ _mem.offset = mem.offset / 10<br>
+<br>
+ tx, rx = chirp_common.split_tone_encode(mem)<br>
+ self._encode_tone(_mem, 'tx', *tx)<br>
+ self._encode_tone(_mem, 'rx', *rx)<br>
+<br>
+ _mem.duplex = DUPLEX.index(mem.duplex)<br>
+ _mem.isnarrow = mem.mode == "NFM"<br>
+ _mem.step = TGUV2P_STEPS.index(mem.tuning_step)<br>
+<br>
+ if mem.power == None :<br>
+ _mem.power = 0<br>
+ else:<br>
+ _mem.power = POWER_LEVELS.index(mem.power)<br>
+<br>
+ if _nam:<br>
+ for i in range(0, 6):<br>
+ try:<br>
+ _nam[i] = CHARSET.index(<a href="http://mem.name" rel="noreferrer" target="_blank">mem.name</a>[i])<br>
+ except IndexError:<br>
+ _nam[i] = 0xFF<br>
+<br>
+ for setting in mem.extra:<br>
+ setattr(_mem, setting.get_name(), setting.value)<br>
+<br>
+ def get_settings(self):<br>
+ _settings = self._memobj.settings<br>
+ _vfoa = self._memobj.vfos[0]<br>
+ _vfob = self._memobj.vfos[1]<br>
+ _bandsettings = self._memobj.bands<br>
+<br>
+<br>
+ cfg_grp = RadioSettingGroup("cfg_grp", "Configuration")<br>
+ vfoa_grp = RadioSettingGroup("vfoa_grp", "VFO A Settings")<br>
+ vfob_grp = RadioSettingGroup("vfob_grp", "VFO B Settings")<br>
+<br>
+<br>
+ group = RadioSettings(cfg_grp, vfoa_grp, vfob_grp)<br>
+ #<br>
+ # Configuration Settings<br>
+ #<br>
+ options = ["Off"] + ["%s min" % x for x in range(1, 10)]<br>
+ rs = RadioSetting("timeout", "Time Out Timer",<br>
+ RadioSettingValueList(<br>
+ options, options[_settings.time_out_timer]))<br>
+ cfg_grp.append(rs)<br>
+<br>
+ options = ["Frequency", "Channel", "Name"]<br>
+ rs = RadioSetting("isplay", "Channel Display Moe",<br>
+ RadioSettingValueList(<br>
+ options, options[_settings.display]))<br>
+ cfg_grp.append(rs)<br>
+<br>
+ rs = RadioSetting("squelch", "Squelch Level",<br>
+ RadioSettingValueInteger(0, 9, _settings.squelch))<br>
+ cfg_grp.append(rs)<br>
+<br>
+ if _settings.vox == 0:<br>
+ rs = RadioSetting("vox", "VOX",<br>
+ RadioSettingValueString(0,10,"Off"))<br>
+ cfg_grp.append(rs)<br>
+ else:<br>
+ rs = RadioSetting("vox", "VOX Level",<br>
+ RadioSettingValueInteger(1, 9, _settings.vox))<br>
+ cfg_grp.append(rs)<br>
+<br>
+ rs = RadioSetting("beep_tone_disabled", "Beep Prompt",<br>
+ RadioSettingValueBoolean(<br>
+ not _settings.beep_tone_disabled))<br>
+ cfg_grp.append(rs)<br>
+<br>
+ options = ["Dual Watch", "CrossBand", "Normal"]<br>
+ if _settings.rxmode >=2:<br>
+ _rxmode = 2<br>
+ else:<br>
+ _rxmode = _settings.rxmode<br>
+ rs = RadioSetting("RX mode", "Dual Watch/CrossBand Monitor",<br>
+ RadioSettingValueList(<br>
+ options, options[_rxmode]))<br>
+ cfg_grp.append(rs)<br>
+<br>
+ rs = RadioSetting("bcl", "Busy Channel Lock",<br>
+ RadioSettingValueBoolean(<br>
+ not _settings.busy_lockout))<br>
+ cfg_grp.append(rs)<br>
+<br>
+ rs = RadioSetting("keylock", "Keypad Lock",<br>
+ RadioSettingValueBoolean(<br>
+ not _settings.keyunlocked))<br>
+ cfg_grp.append(rs)<br>
+<br>
+ if _settings.priority_channel >= 200:<br>
+ rs = RadioSetting("pri_ch", "Priority Channel",<br>
+ RadioSettingValueString(0,10,"Not Set"))<br>
+ cfg_grp.append(rs)<br>
+ else:<br>
+ rs = RadioSetting("pri_ch", "Priority Channel",<br>
+ RadioSettingValueInteger(0, 199, _settings.priority_channel))<br>
+ cfg_grp.append(rs)<br>
+<br>
+ #<br>
+ # VFO Settings<br>
+ #<br>
+<br>
+ vfo_groups = [vfoa_grp, vfob_grp]<br>
+ vfo_mem = [_vfoa, _vfob]<br>
+ vfo_lower = ["vfoa", "vfob"]<br>
+ vfo_upper = ["VFOA", "VFOB"]<br>
+<br>
+ for idx,vfo_group in enumerate(vfo_groups):<br>
+<br>
+ options = ["Channel", "Frequency"]<br>
+ tempvar = 0 if (vfo_mem[idx].current < 200) else 1<br>
+ rs = RadioSetting(vfo_lower[idx]+"_mode", vfo_upper[idx]+" Mode",<br>
+ RadioSettingValueList(<br>
+ options, options[tempvar]))<br>
+ vfo_group.append(rs)<br>
+<br>
+ if tempvar == 0:<br>
+ rs = RadioSetting(vfo_lower[idx]+"_ch", vfo_upper[idx]+" Channel",<br>
+ RadioSettingValueInteger(0, 199, vfo_mem[idx].current))<br>
+ vfo_group.append(rs)<br>
+ else:<br>
+ band_num = vfo_mem[idx].current - 200<br>
+ freq = int(_bandsettings[band_num].freq) * 10<br>
+ offset = int(_bandsettings[band_num].offset) * 10<br>
+ txtmode = _bandsettings[band_num].txtmode<br>
+ rxtmode = _bandsettings[band_num].rxtmode<br>
+<br>
+ rs = RadioSetting(vfo_lower[idx]+"_freq", vfo_upper[idx]+" Frequency",<br>
+ RadioSettingValueFloat(0.0, 520.0, freq / 1000000.0, precision=6))<br>
+ vfo_group.append(rs)<br>
+<br>
+ if offset > 70e6:<br>
+ offset = 0<br>
+ rs = RadioSetting(vfo_lower[idx]+"_offset", vfo_upper[idx]+" Offset",<br>
+ RadioSettingValueFloat(0.0, 69.995, offset / 100000.0, resolution= 0.005))<br>
+ vfo_group.append(rs)<br>
+<br>
+ rs = RadioSetting(vfo_lower[idx]+"_duplex", vfo_upper[idx]+" Shift",<br>
+ RadioSettingValueList(<br>
+ DUPLEX, DUPLEX[_bandsettings[band_num].duplex]))<br>
+ vfo_group.append(rs)<br>
+<br>
+ rs = RadioSetting(vfo_lower[idx]+"_step", vfo_upper[idx]+" Step",<br>
+ RadioSettingValueFloat(<br>
+ 0.0, 1000.0, TGUV2P_STEPS[_bandsettings[band_num].step], resolution=0.25))<br>
+ vfo_group.append(rs)<br>
+<br>
+ rs = RadioSetting(vfo_lower[idx]+"_pwr", vfo_upper[idx]+" Power",<br>
+ RadioSettingValueList(<br>
+ POWER_LEVELS_STR, POWER_LEVELS_STR[_bandsettings[band_num].power]))<br>
+ vfo_group.append(rs)<br>
+<br>
+ options = ["None", "Tone", "DTCS-N", "DTCS-I"]<br>
+ rs = RadioSetting(vfo_lower[idx]+"_ttmode", vfo_upper[idx]+" TX tone mode",<br>
+ RadioSettingValueList( options, options[txtmode]))<br>
+ vfo_group.append(rs)<br>
+ if txtmode == 1:<br>
+ rs = RadioSetting(vfo_lower[idx]+"_ttone", vfo_upper[idx]+" TX tone",<br>
+ RadioSettingValueFloat(<br>
+ 0.0, 1000.0, chirp_common.TONES[_bandsettings[band_num].txtone], resolution=0.1))<br>
+ vfo_group.append(rs)<br>
+ elif txtmode >= 2:<br>
+ txtone = _bandsettings[band_num].txtone<br>
+ rs = RadioSetting(vfo_lower[idx]+"_tdtcs", vfo_upper[idx]+" TX DTCS",<br>
+ RadioSettingValueInteger(<br>
+ 0, 1000, chirp_common.DTCS_CODES[txtone]))<br>
+ vfo_group.append(rs)<br>
+<br>
+ options = ["None", "Tone", "DTCS-N", "DTCS-I" ]<br>
+ rs = RadioSetting(vfo_lower[idx]+"_rtmode", vfo_upper[idx]+" RX tone mode",<br>
+ RadioSettingValueList( options, options[rxtmode]))<br>
+ vfo_group.append(rs)<br>
+<br>
+ if rxtmode == 1:<br>
+ rs = RadioSetting(vfo_lower[idx]+"_rtone", vfo_upper[idx]+" RX tone",<br>
+ RadioSettingValueFloat(<br>
+ 0.0, 1000.0, chirp_common.TONES[_bandsettings[band_num].rxtone], resolution=0.1))<br>
+ vfo_group.append(rs)<br>
+ elif rxtmode >= 2:<br>
+ rxtone = _bandsettings[band_num].rxtone<br>
+ rs = RadioSetting(vfo_lower[idx]+"_rdtcs", vfo_upper[idx]+" TX rTCS",<br>
+ RadioSettingValueInteger(<br>
+ 0, 1000, chirp_common.DTCS_CODES[rxtone]))<br>
+ vfo_group.append(rs)<br>
+<br>
+<br>
+ options = ["FM", "NFM"]<br>
+ rs = RadioSetting(vfo_lower[idx]+"_fm", vfo_upper[idx]+" FM BW ",<br>
+ RadioSettingValueList(<br>
+ options, options[_bandsettings[band_num].isnarrow]))<br>
+ vfo_group.append(rs)<br>
+<br>
+ return group<br>
+<br>
+<br>
+ @classmethod<br>
+ def match_model(cls, filedata, filename):<br>
+ return (filedata.startswith("TG-UV2+ Radio Program Data") and<br>
+ len(filedata) == (cls._memsize + 0x30))<br>
diff --git a/tests/images/Quansheng_TG-UV2+.img b/tests/images/Quansheng_TG-UV2+.img<br>
new file mode 100644<br>
index 0000000000000000000000000000000000000000..71ed9aaf9a7e7f7e2b8f6846469f0a3b4d12fc22<br>
GIT binary patch<br>
literal 8417<br>
zc%1E*Pj3=I7>8$ef!#gW+S(W!JaB<M7!7Qr?Zw0j6qgiRs$GOr2dM0{{Aob8<)$9=<br>
z0~il}q((oBNz)IoE|BWZ4=*MraF{0{n`hp6^ZuCK!|v<C!$Ze<kT_sX-b?KFd#yg}<br>
zBy#K>OL&<_X@dB`PO`o~Uf1<;O(i?Y&!VSR!8gI7KL!0J^bcYG5cYep--CG`^n1{M<br>
z4*kiAhI}Y`ihw7<p+5!vCiI(PKc08)D3<(FrzCfDZzy^O>RG5eP<NsJ0qS4GI)nKP<br>
z<};YjU_OKS4CXVK&&2up)$n=EK9BPz`zg5J9wCOYqKxycRf71jkQb(Y+gPECm|r51<br>
zNF)-8WYN&yPv4mc`4L<Vvi9u8{jn=s-@b;O<UihgxlH$$q5jVSt_RnD!Jl3E-EejF<br>
zKmPcl5T#o18t(gwF;P|hetGM1&Y9-Oa{RtYP0<x_6+8kS1&@Ji;5xVgP6;>#SHM;9<br>
z2zV4c2CjkY;D+c5<WnG@63U0H-;JAuf-B%EcmzBO9s}3Fb#O!U@Oe)h@=?e~As>Z&<br>
z6!L}7`_9tR`f}V~SmGKn&Cv50r%};0Q*enyB9TZW5{X12kt`;}Z@9dF_LFy7gdUHI<br>
zR;}{*w7y~UJ-*Ek-#Bi)`?43Tb!~p$vO5{CR&WOPNv~zM4_@p#PNl>Ld^fwztCoA#<br>
z=sthj>^P%_)%NP#eqv{fr`1Y(aQrGgD&_3pm%rG{+Jj(yRJAVL#ztvoj+1XX&PlO6<br>
Tnb)nkjZQnLl_&e`Zu;pj#yjz3<br>
<br>
<br>
</blockquote></div>