<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 &lt;<a href="mailto:rankatz@gmail.com">rankatz@gmail.com</a>&gt; 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 &lt;<a href="mailto:rankatz@gmai.com" target="_blank">rankatz@gmai.com</a>&gt;<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 &#39;C&#39; utility) was developed a decade ago for the TG-UV2,<br>
and I could not find any differences.<br>
<br>
---------------<br>
user: Ran Katz &lt;<a href="mailto:rankatz@gmai.com" target="_blank">rankatz@gmai.com</a>&gt;<br>
branch &#39;default&#39;<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 &lt;<a href="mailto:dsmith@danplanet.com" target="_blank">dsmith@danplanet.com</a>&gt;<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 &lt;<a href="http://www.gnu.org/licenses/" rel="noreferrer" target="_blank">http://www.gnu.org/licenses/</a>&gt;.<br>
+<br>
+# This driver was derived from the:<br>
+# Quansheng TG-UV2 Utility by Mike Nix &lt;<a href="mailto:mnix@wanm.com.au" target="_blank">mnix@wanm.com.au</a>&gt;<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 = &quot;&quot;&quot;<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>
+&quot;&quot;&quot;<br>
+<br>
+def do_ident(radio):<br>
+    radio.pipe.timeout = 3<br>
+    radio.pipe.write(&quot;\x02PnOGdAM&quot;)<br>
+    for x in xrange(10):<br>
+        ack = radio.pipe.read(1)<br>
+        if ack == &#39;\x06&#39;:<br>
+            break<br>
+    else:<br>
+        raise errors.RadioError(&quot;Radio did not ack programming mode&quot;)<br>
+    radio.pipe.write(&quot;\x40\x02&quot;)<br>
+    ident = radio.pipe.read(8)<br>
+    LOG.debug(util.hexprint(ident))<br>
+    if not ident.startswith(&#39;P5555&#39;):<br>
+        raise errors.RadioError(&quot;Unsupported model&quot;)<br>
+    radio.pipe.write(&quot;\x06&quot;)<br>
+    ack = radio.pipe.read(1)<br>
+    if ack != &quot;\x06&quot;:<br>
+        raise errors.RadioError(&quot;Radio did not ack ident&quot;)<br>
+<br>
+<br>
+def do_status(radio, direction, addr):<br>
+    status = chirp_common.Status()<br>
+    status.msg = &quot;Cloning %s radio&quot; % 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 = &quot;TG-UV2+ Radio Program Data v1.0\x00&quot;<br>
+    data += (&quot;\x00&quot; * 16)<br>
+<br>
+    firstack = None<br>
+    for i in range(0, 0x2000, 8):<br>
+        frame = struct.pack(&quot;&gt;cHB&quot;, &quot;R&quot;, i, 8)<br>
+        radio.pipe.write(frame)<br>
+        result = radio.pipe.read(12)<br>
+        if not (result[0]==&quot;W&quot; and frame[1:4]==result[1:4]):<br>
+            LOG.debug(util.hexprint(result))<br>
+            raise errors.RadioError(&quot;Invalid response for address 0x%04x&quot; % i)<br>
+        radio.pipe.write(&quot;\x06&quot;)<br>
+        ack = radio.pipe.read(1)<br>
+        if not firstack:<br>
+            firstack = ack<br>
+        else:<br>
+            if not ack == firstack:<br>
+                LOG.debug(&quot;first ack: %s ack received: %s&quot;,<br>
+                          util.hexprint(firstack), util.hexprint(ack))<br>
+                raise errors.RadioError(&quot;Unexpected response&quot;)<br>
+        data += result[4:]<br>
+        do_status(radio, &quot;from&quot;, 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(&quot;&gt;cHB&quot;, &quot;W&quot;, i, 8)<br>
+        frame += data[i:i + 8]<br>
+        radio.pipe.write(frame)<br>
+        ack = radio.pipe.read(1)<br>
+        if ack != &quot;\x06&quot;:<br>
+            LOG.debug(&quot;Radio NAK&#39;d block at address 0x%04x&quot; % i)<br>
+            raise errors.RadioError(<br>
+                    &quot;Radio NAK&#39;d block at address 0x%04x&quot; % i)<br>
+        LOG.debug(&quot;Radio ACK&#39;d block at address 0x%04x&quot; % i)<br>
+        do_status(radio, &quot;to&quot;, i)<br>
+<br>
+DUPLEX = [&quot;&quot;, &quot;+&quot;, &quot;-&quot;]<br>
+TGUV2P_STEPS = [5, 6.25, 10, 12.5, 15, 20, 25, 30, 50, 100,]<br>
+CHARSET = &quot;0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_|* +-&quot;<br>
+POWER_LEVELS = [chirp_common.PowerLevel(&quot;High&quot;, watts=10),<br>
+                               chirp_common.PowerLevel(&quot;Med&quot;, watts=5),<br>
+                chirp_common.PowerLevel(&quot;Low&quot;, watts=1)]<br>
+POWER_LEVELS_STR = [&quot;High&quot;, &quot;Med&quot;, &quot;Low&quot;]<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>
+    &quot;&quot;&quot;Quansheng TG-UV2+&quot;&quot;&quot;<br>
+    VENDOR = &quot;Quansheng&quot;<br>
+    MODEL = &quot;TG-UV2+&quot;<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>
+            (&#39;Experimental version for TG-UV2/2+ radios &#39;<br>
+             &#39;Proceed at your own risk!&#39;)<br>
+        rp.pre_download = _(dedent(&quot;&quot;&quot;\<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.&quot;&quot;&quot;))<br>
+        rp.pre_upload = _(dedent(&quot;&quot;&quot;\<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.&quot;&quot;&quot;))<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 = [&quot;&quot;, &quot;Tone&quot;, &quot;TSQL&quot;, &quot;DTCS&quot;, &quot;Cross&quot;]<br>
+        rf.valid_cross_modes = [&quot;Tone-&gt;Tone&quot;, &quot;Tone-&gt;DTCS&quot;, &quot;DTCS-&gt;Tone&quot;,<br>
+                                &quot;-&gt;Tone&quot;, &quot;-&gt;DTCS&quot;, &quot;DTCS-&gt;&quot;, &quot;DTCS-&gt;DTCS&quot;]<br>
+        rf.valid_duplexes = DUPLEX<br>
+        rf.can_odd_split = False<br>
+        rf.valid_skips = [&quot;&quot;, &quot;S&quot;]<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 = [&quot;FM&quot;, &quot;NFM&quot;]<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(&quot;Failed to communicate with radio: %s&quot; % 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(&quot;Failed to communicate with radio: %s&quot; % 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, &quot;%s%s&quot; % (which, field))<br>
+<br>
+        value = _get(&#39;tone&#39;)<br>
+        tmode = _get(&#39;tmode&#39;)<br>
+<br>
+        if (value &lt;= 104) and (tmode &lt;= 3):<br>
+            if tmode == 0:<br>
+                mode = val = pol = None<br>
+            elif tmode == 1:<br>
+                mode = &#39;Tone&#39;<br>
+                val = chirp_common.TONES[value]<br>
+                pol = None<br>
+            else:<br>
+                mode = &#39;DTCS&#39;<br>
+                val = chirp_common.DTCS_CODES[value]<br>
+                pol = &quot;N&quot; if (tmode == 2) else &quot;R&quot;<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, &quot;%s%s&quot; % (which, field), value)<br>
+<br>
+        if (mode == &quot;Tone&quot;):<br>
+            _set(&quot;tone&quot;, chirp_common.TONES.index(val))<br>
+            _set(&quot;tmode&quot;, 0x01)<br>
+        elif mode == &quot;DTCS&quot;:<br>
+            _set(&quot;tone&quot;, chirp_common.DTCS_CODES.index(val))<br>
+            if pol == &quot;N&quot;:<br>
+                _set(&quot;tmode&quot;, 0x02)<br>
+            else:<br>
+                _set(&quot;tmode&quot;, 0x03)<br>
+        else:<br>
+            _set(&quot;tone&quot;, 0)<br>
+            _set(&quot;tmode&quot;, 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] == &quot;\xFF&quot;) or (_bf.band == &quot;\x0F&quot;):<br>
+            mem.empty = True<br>
+            return mem<br>
+<br>
+        mem.freq = int(_mem.freq) * 10<br>
+<br>
+        if _mem.offset.get_raw()[0] == &quot;\xFF&quot; :<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, &quot;tx&quot;),<br>
+            self._decode_tone(_mem, &quot;rx&quot;))<br>
+<br>
+        if &#39;step&#39; in _mem and _mem.step &gt; 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 &quot;NFM&quot; or &quot;FM&quot;<br>
+        mem.skip = &quot;&quot; if bool(_bf.scanadd) else &quot;S&quot;<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(&quot;Extra&quot;, &quot;extra&quot;)<br>
+<br>
+        rs = RadioSetting(&quot;not_scramble&quot;, &quot;(not)SCRAMBLE&quot;,<br>
+                          RadioSettingValueBoolean(_mem.not_scramble))<br>
+        mem.extra.append(rs)<br>
+<br>
+        rs = RadioSetting(&quot;not_revfreq&quot;, &quot;(not)Reverse Duplex&quot;,<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(&quot;\xFF&quot;)<br>
+<br>
+<br>
+        if mem.empty:<br>
+            _mem.set_raw(&quot;\xFF&quot; * 16)<br>
+            return<br>
+<br>
+        #if _mem.get_raw() == (&quot;\xFF&quot; * 16):<br>
+        _mem.set_raw(&quot;\x00&quot; * 12 + &quot;\xFF&quot; * 2 + &quot;\x00&quot;*2)<br>
+<br>
+        _bf.scanadd = int(mem.skip != &quot;S&quot;)<br>
+        _bf.band = 0x0F<br>
+        for idx, ele in enumerate(VALID_BANDS):<br>
+            if mem.freq &gt;= ele[0] and mem.freq &lt;= 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, &#39;tx&#39;, *tx)<br>
+        self._encode_tone(_mem, &#39;rx&#39;, *rx)<br>
+<br>
+        _mem.duplex = DUPLEX.index(mem.duplex)<br>
+        _mem.isnarrow = mem.mode == &quot;NFM&quot;<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(&quot;cfg_grp&quot;, &quot;Configuration&quot;)<br>
+        vfoa_grp = RadioSettingGroup(&quot;vfoa_grp&quot;, &quot;VFO A Settings&quot;)<br>
+        vfob_grp = RadioSettingGroup(&quot;vfob_grp&quot;, &quot;VFO B Settings&quot;)<br>
+<br>
+<br>
+        group = RadioSettings(cfg_grp, vfoa_grp, vfob_grp)<br>
+        #<br>
+        # Configuration Settings<br>
+        #<br>
+        options = [&quot;Off&quot;] + [&quot;%s min&quot; % x for x in range(1, 10)]<br>
+        rs = RadioSetting(&quot;timeout&quot;, &quot;Time Out Timer&quot;,<br>
+                          RadioSettingValueList(<br>
+                              options, options[_settings.time_out_timer]))<br>
+        cfg_grp.append(rs)<br>
+<br>
+        options = [&quot;Frequency&quot;, &quot;Channel&quot;, &quot;Name&quot;]<br>
+        rs = RadioSetting(&quot;isplay&quot;, &quot;Channel Display Moe&quot;,<br>
+                          RadioSettingValueList(<br>
+                              options, options[_settings.display]))<br>
+        cfg_grp.append(rs)<br>
+<br>
+        rs = RadioSetting(&quot;squelch&quot;, &quot;Squelch Level&quot;,<br>
+                          RadioSettingValueInteger(0, 9, _settings.squelch))<br>
+        cfg_grp.append(rs)<br>
+<br>
+        if _settings.vox == 0:<br>
+            rs = RadioSetting(&quot;vox&quot;, &quot;VOX&quot;,<br>
+                              RadioSettingValueString(0,10,&quot;Off&quot;))<br>
+            cfg_grp.append(rs)<br>
+        else:<br>
+            rs = RadioSetting(&quot;vox&quot;, &quot;VOX Level&quot;,<br>
+                              RadioSettingValueInteger(1, 9, _settings.vox))<br>
+            cfg_grp.append(rs)<br>
+<br>
+        rs = RadioSetting(&quot;beep_tone_disabled&quot;, &quot;Beep Prompt&quot;,<br>
+                          RadioSettingValueBoolean(<br>
+                               not _settings.beep_tone_disabled))<br>
+        cfg_grp.append(rs)<br>
+<br>
+        options = [&quot;Dual Watch&quot;, &quot;CrossBand&quot;, &quot;Normal&quot;]<br>
+        if _settings.rxmode &gt;=2:<br>
+            _rxmode = 2<br>
+        else:<br>
+            _rxmode = _settings.rxmode<br>
+        rs = RadioSetting(&quot;RX mode&quot;, &quot;Dual Watch/CrossBand Monitor&quot;,<br>
+                          RadioSettingValueList(<br>
+                            options, options[_rxmode]))<br>
+        cfg_grp.append(rs)<br>
+<br>
+        rs = RadioSetting(&quot;bcl&quot;, &quot;Busy Channel Lock&quot;,<br>
+                          RadioSettingValueBoolean(<br>
+                             not _settings.busy_lockout))<br>
+        cfg_grp.append(rs)<br>
+<br>
+        rs = RadioSetting(&quot;keylock&quot;, &quot;Keypad Lock&quot;,<br>
+                          RadioSettingValueBoolean(<br>
+                              not _settings.keyunlocked))<br>
+        cfg_grp.append(rs)<br>
+<br>
+        if _settings.priority_channel &gt;= 200:<br>
+            rs = RadioSetting(&quot;pri_ch&quot;, &quot;Priority Channel&quot;,<br>
+                              RadioSettingValueString(0,10,&quot;Not Set&quot;))<br>
+            cfg_grp.append(rs)<br>
+        else:<br>
+            rs = RadioSetting(&quot;pri_ch&quot;, &quot;Priority Channel&quot;,<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 = [&quot;vfoa&quot;, &quot;vfob&quot;]<br>
+        vfo_upper = [&quot;VFOA&quot;, &quot;VFOB&quot;]<br>
+<br>
+        for idx,vfo_group in enumerate(vfo_groups):<br>
+<br>
+            options = [&quot;Channel&quot;, &quot;Frequency&quot;]<br>
+            tempvar = 0 if (vfo_mem[idx].current &lt; 200) else 1<br>
+            rs = RadioSetting(vfo_lower[idx]+&quot;_mode&quot;, vfo_upper[idx]+&quot; Mode&quot;,<br>
+                              RadioSettingValueList(<br>
+                                  options, options[tempvar]))<br>
+            vfo_group.append(rs)<br>
+<br>
+            if tempvar == 0:<br>
+                rs = RadioSetting(vfo_lower[idx]+&quot;_ch&quot;, vfo_upper[idx]+&quot; Channel&quot;,<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]+&quot;_freq&quot;, vfo_upper[idx]+&quot; Frequency&quot;,<br>
+                                  RadioSettingValueFloat(0.0, 520.0, freq / 1000000.0, precision=6))<br>
+                vfo_group.append(rs)<br>
+<br>
+                if offset &gt; 70e6:<br>
+                    offset = 0<br>
+                rs = RadioSetting(vfo_lower[idx]+&quot;_offset&quot;, vfo_upper[idx]+&quot; Offset&quot;,<br>
+                                  RadioSettingValueFloat(0.0, 69.995, offset / 100000.0, resolution= 0.005))<br>
+                vfo_group.append(rs)<br>
+<br>
+                rs = RadioSetting(vfo_lower[idx]+&quot;_duplex&quot;, vfo_upper[idx]+&quot; Shift&quot;,<br>
+                                  RadioSettingValueList(<br>
+                                      DUPLEX, DUPLEX[_bandsettings[band_num].duplex]))<br>
+                vfo_group.append(rs)<br>
+<br>
+                rs = RadioSetting(vfo_lower[idx]+&quot;_step&quot;, vfo_upper[idx]+&quot; Step&quot;,<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]+&quot;_pwr&quot;, vfo_upper[idx]+&quot; Power&quot;,<br>
+                                  RadioSettingValueList(<br>
+                                      POWER_LEVELS_STR, POWER_LEVELS_STR[_bandsettings[band_num].power]))<br>
+                vfo_group.append(rs)<br>
+<br>
+                options = [&quot;None&quot;, &quot;Tone&quot;, &quot;DTCS-N&quot;, &quot;DTCS-I&quot;]<br>
+                rs = RadioSetting(vfo_lower[idx]+&quot;_ttmode&quot;, vfo_upper[idx]+&quot; TX tone mode&quot;,<br>
+                                  RadioSettingValueList( options, options[txtmode]))<br>
+                vfo_group.append(rs)<br>
+                if txtmode == 1:<br>
+                    rs =  RadioSetting(vfo_lower[idx]+&quot;_ttone&quot;, vfo_upper[idx]+&quot; TX tone&quot;,<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 &gt;= 2:<br>
+                    txtone = _bandsettings[band_num].txtone<br>
+                    rs =  RadioSetting(vfo_lower[idx]+&quot;_tdtcs&quot;, vfo_upper[idx]+&quot; TX DTCS&quot;,<br>
+                                       RadioSettingValueInteger(<br>
+                                           0, 1000, chirp_common.DTCS_CODES[txtone]))<br>
+                    vfo_group.append(rs)<br>
+<br>
+                options = [&quot;None&quot;, &quot;Tone&quot;, &quot;DTCS-N&quot;, &quot;DTCS-I&quot; ]<br>
+                rs = RadioSetting(vfo_lower[idx]+&quot;_rtmode&quot;, vfo_upper[idx]+&quot; RX tone mode&quot;,<br>
+                                  RadioSettingValueList( options, options[rxtmode]))<br>
+                vfo_group.append(rs)<br>
+<br>
+                if rxtmode == 1:<br>
+                    rs =  RadioSetting(vfo_lower[idx]+&quot;_rtone&quot;, vfo_upper[idx]+&quot; RX tone&quot;,<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 &gt;= 2:<br>
+                    rxtone = _bandsettings[band_num].rxtone<br>
+                    rs =  RadioSetting(vfo_lower[idx]+&quot;_rdtcs&quot;, vfo_upper[idx]+&quot; TX rTCS&quot;,<br>
+                                       RadioSettingValueInteger(<br>
+                                           0, 1000, chirp_common.DTCS_CODES[rxtone]))<br>
+                    vfo_group.append(rs)<br>
+<br>
+<br>
+                options = [&quot;FM&quot;, &quot;NFM&quot;]<br>
+                rs =  RadioSetting(vfo_lower[idx]+&quot;_fm&quot;, vfo_upper[idx]+&quot; FM BW &quot;,<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(&quot;TG-UV2+ Radio Program Data&quot;) 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&gt;8$ef!#gW+S(W!JaB&lt;M7!7Qr?Zw0j6qgiRs$GOr2dM0{{Aob8&lt;)$9=<br>
z0~il}q((oBNz)IoE|BWZ4=*MraF{0{n`hp6^ZuCK!|v&lt;C!$Ze&lt;kT_sX-b?KFd#yg}<br>
zBy#K&gt;OL&amp;&lt;_X@dB`PO`o~Uf1&lt;;O(i?Y&amp;!VSR!8gI7KL!0J^bcYG5cYep--CG`^n1{M<br>
z4*kiAhI}Y`ihw7&lt;p+5!vCiI(PKc08)D3&lt;(FrzCfDZzy^O&gt;RG5eP&lt;NsJ0qS4GI)nKP<br>
z&lt;};YjU_OKS4CXVK&amp;&amp;2up)$n=EK9BPz`zg5J9wCOYqKxycRf71jkQb(Y+gPECm|r51<br>
zNF)-8WYN&amp;yPv4mc`4L&lt;Vvi9u8{jn=s-@b;O&lt;UihgxlH$$q5jVSt_RnD!Jl3E-EejF<br>
zKmPcl5T#o18t(gwF;P|hetGM1&amp;Y9-Oa{RtYP0&lt;x_6+8kS1&amp;@Ji;5xVgP6;&gt;#SHM;9<br>
z2zV4c2CjkY;D+c5&lt;WnG@63U0H-;JAuf-B%EcmzBO9s}3Fb#O!U@Oe)h@=?e~As&gt;Z&amp;<br>
z6!L}7`_9tR`f}V~SmGKn&amp;Cv50r%};0Q*enyB9TZW5{X12kt`;}Z@9dF_LFy7gdUHI<br>
zR;}{*w7y~UJ-*Ek-#Bi)`?43Tb!~p$vO5{CR&amp;WOPNv~zM4_@p#PNl&gt;Ld^fwztCoA#<br>
z=sthj&gt;^P%_)%NP#eqv{fr`1Y(aQrGgD&amp;_3pm%rG{+Jj(yRJAVL#ztvoj+1XX&amp;PlO6<br>
Tnb)nkjZQnLl_&amp;e`Zu;pj#yjz3<br>
<br>
<br>
</blockquote></div>