[chirp_devel] [PATCH] Driver for TG-UV2+ (and probably TG-UV2)

Ran Katz
Tue Feb 8 14:57:21 PST 2022


Hi,
I added the image file to issue 8591

Ran

On Wed, Feb 9, 2022 at 12:55 AM Ran Katz <rankatz at gmail.com> wrote:

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


More information about the chirp_devel mailing list