
# HG changeset patch # User Ran Katz rankatz@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@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@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@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)] + +@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@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-@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@63U0H-;JAuf-B%EcmzBO9s}3Fb#O!U@Oe)h@=?e~As>Z& z6!L}7`_9tR`f}V~SmGKn&Cv50r%};0Q*enyB9TZW5{X12kt`;}Z@9dF_LFy7gdUHI zR;}{*w7y~UJ-*Ek-#Bi)`?43Tb!~p$vO5{CR&WOPNv~zM4_@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