# Copyright 2011 Dan Smith # Copyright 2012 Alan Hill # Copyright 2015 Benito Vitale # # 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 3 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 radios management module""" import struct import logging import time import os import serial from chirp import util, chirp_common, bitwise, memmap, errors, directory from chirp.settings import RadioSetting, RadioSettingGroup, \ RadioSettingValueBoolean, RadioSettingValueList, \ RadioSettingValueInteger, RadioSettingValueString, \ RadioSettingValueFloat, RadioSettings from textwrap import dedent LOG = logging.getLogger(__name__) def wipe_memory(_mem, byte): """Cleanup a memory""" _mem.set_raw(byte * (_mem.size() / 8)) def do_upload(radio, start, end, blocksize): """Initiate an upload of @radio between @start and @end""" ptr = start for i in range(start, end, blocksize): print "Writing block at %x" % i chunk = radio.get_mmap()[ptr:ptr+blocksize] ptr += blocksize while 1: cmd = struct.pack(">cHb", "W", i, blocksize) radio.pipe.write(cmd + chunk) LOG.debug(util.hexprint(cmd + chunk)) ack = radio.pipe.read(1) if not ack == "\x06": print "Radio did not ack block %i" % i print "Retrying..." time.sleep(0.5) #radio._identify() continue # radio.pipe.write(ack) break if radio.status_fn: status = chirp_common.Status() status.cur = i status.max = end status.msg = "Cloning to radio" radio.status_fn(status) def do_download(radio, start, end, blocksize): """Initiate a download of @radio between @start and @end""" image = "" for i in range(start, end, blocksize): print "Reading block at %x" % i while 1: cmd = struct.pack(">cHb", "R", i, blocksize) #LOG.debug(util.hexprint(cmd)) print "SENT: %i" % len(cmd) print "SENT: %s" % util.hexprint(cmd) radio.pipe.write(cmd) length = len(cmd) + blocksize time.sleep(0.2) resp = radio.pipe.read(length) print "GOT: %s" % util.hexprint(resp) block_hdr = struct.pack(">cHb", "W", i, blocksize) if len(resp) >= 1 and resp[0] == "\x00": print "Got null" print "Retrying..." time.sleep(0.5) radio._identify() continue if len(resp) != (len(cmd) + blocksize): #LOG.debug(util.hexprint(resp)) print "Failed to read full block (%i!=%i)" % ( len(resp), len(cmd) + blocksize) print "Retrying..." time.sleep(1) radio._identify() continue if block_hdr != resp[0:4]: print "Got wrong block (%s!=%s)" % ( util.hexprint(block_hdr), util.hexprint(resp[0:4])) print "Retrying..." time.sleep(1) #radio._identify() continue #radio.pipe.write("\x06") #radio.pipe.read(1) image += resp[4:] break if radio.status_fn: status = chirp_common.Status() status.cur = i status.max = end status.msg = "Cloning from radio" radio.status_fn(status) return memmap.MemoryMap(image) # writing bad frequency ranges on the radio can brick it # the encode function here has not been tested # it has been included for documentation purpouse only as I never call it # you have been warned, use at your own risk def encode_freq(freq): """Convert frequency (4 decimal digits) to quansheng format (2 bytes)""" enc = 0 div = 1000 for i in range(0, 4): enc <<= 8 enc |= FREQ_ENCODE_TABLE[ (freq/div) % 10 ] div /= 10 return enc def decode_freq(data): """Convert from quansheng format (2 bytes) to frequency (4 decimal digits)""" freq = 0 shift = 12 for i in range(0, 4): freq *= 10 freq += FREQ_ENCODE_TABLE.index( (data>>shift) & 0xf ) shift -= 4 # print "data %04x freq %d shift %d" % (data, freq, shift) return freq @directory.register class TGUV2Radio(chirp_common.CloneModeRadio, chirp_common.ExperimentalRadio): """Quansheng TG-UV2""" VENDOR = "Quansheng" BAUD_RATE = 9600 # writing bad frequency ranges on the radio can brick it MODEL = "TG-UV2" _model = "555" _pgmmode = "\x02PnOGdAM" _querymodel = "M\x02" _answermodel = "P5555\xf4\x00\x00" _memsize = 0x2000 CHARSET = list("0123456789ABCDEFGHIJKLMNOPWRSTUVWXYZ |* +-\0") POWER_LEVELS = [chirp_common.PowerLevel("High", watts=5.00), chirp_common.PowerLevel("Med", watts=2.50), chirp_common.PowerLevel("Low", watts=1.00)] TUNING_STEPS = [ 5.0, 6.25, 10.0, 12.5, 15, 20, 25, 30, 50, 100, 101, 102, 103, 104, 105, 5.0 ] TUNING_STEPS_STR = map(str, TUNING_STEPS) valid_freq = [ (88000000, 108000000), (136000000, 174000000), (350000000, 390000000), (400000000, 470000000), (470000000, 520000000) ] display_options = ["Frequency", "Channel", "Name" ] _MEM_FORMAT = """ struct { bbcd rx_freq[4]; bbcd tx_freq_offset[4]; u8 rx_tone; u8 tx_tone; u8 tx_code_type:4, rx_code_type:4; u8 flags1:6, split_neg:1, split_pos:1; u8 flags2a:3, iswide:1 ,flags2b:2, scramble:1, reverse:1; u8 flags3; u8 unk:4, bandwith:4; u8 tx_power; } memory[200]; struct { bbcd rx_freq[4]; bbcd tx_freq_offset[4]; u8 rx_tone; u8 tx_tone; u8 tx_code_type:4, rx_code_type:4; u8 unknown_1; u8 unknown_2; u8 unknown_3; u8 bandwith; u8 tx_power; } band_settings[8]; struct { u8 unknown_1:3, scan:1, unknown_2:1, band:3; } memory_scan_band[200]; u8 bandid[8]; u8 reserved1[48]; u8 unk1; u8 squelch; u8 time_out_timer; u8 priority_channel; u8 keylock; u8 busy_lock; u8 vox; u8 unk2; u8 keybeep; u8 display; u8 unk2b:4, step:4; u8 unk3; u8 unk4; u8 mode; u8 end_tone; u8 vfo_model; struct { u8 current; u8 chan; u8 memno; } vfo[2]; u8 unk5; u8 reserved2[9]; u8 band_restrict; u8 txen350390; u8 reserved3[222]; struct { u8 name[6]; u8 data[10]; } names[200]; """ def __init__(self, pipe): print "in ctor" chirp_common.CloneModeRadio.__init__(self, pipe) if not isinstance(pipe, str): pipe.stopbits = serial.STOPBITS_TWO @classmethod def get_prompts(cls): rp = chirp_common.RadioPrompts() rp.experimental = \ ('Experimental. Proceed at your own risk!') rp.pre_download = _(dedent("""\ Are you sure???""")) rp.pre_upload = _(dedent("""\ Are you sure???""")) return rp def _set_pgm_mode(self): # Set programming mode for _i in range(0, 5): print "SENT:\n%s" % util.hexprint(self._pgmmode ) self.pipe.write(self._pgmmode ) resp = self.pipe.read(1) print "Got:\n%s" % util.hexprint(resp) if resp[0]!= "\x06": print "Retrying identification..." time.sleep(0.5) continue print "PGM mode accepted..." return # Ok raise Exception("Giving up") def _identify(self): """Do the Quansheng identification dance""" self._set_pgm_mode() self.pipe.write(self._querymodel ) resp = self.pipe.read(8) if resp != self._answermodel: raise Exception("I can't talk to this model \nGOT(%s)\nEXP(%s)" % (util.hexprint(resp),util.hexprint(self._answermodel))) print "Model authenticated" def _start_transfer(self): """Tell the radio to go into transfer mode""" return self.pipe.write("\x02\x06") time.sleep(0.05) ack = self.pipe.read(1) if ack != "\x06": raise Exception("Radio refused transfer mode") def _download(self): """Talk to an original quansheng and do a download""" try: self._identify() #self._start_transfer() return do_download(self, 0x0000, 0x2000, 0x0008) except errors.RadioError: raise #except Exception, e: # raise errors.RadioError("Failed to communicate with radio: %s" % e) def _upload(self): """Talk to an original quansheng and do an upload""" try: self._identify() #self._start_transfer() return do_upload(self, 0x0000, 0x2000, 0x0008) except errors.RadioError: raise except Exception, e: raise errors.RadioError("Failed to communicate with radio: %s" % e) def sync_in(self): self._mmap = self._download() self.process_mmap() def sync_out(self): self._upload() def process_mmap(self): self._memobj = bitwise.parse(self._MEM_FORMAT, self._mmap) #print self._memobj def get_features(self): rf = chirp_common.RadioFeatures() rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS", "Cross"] rf.valid_cross_modes = [ "Tone->Tone", "Tone->DTCS", "DTCS->Tone", "DTCS->", "->Tone", "->DTCS", "DTCS->DTCS", ] rf.valid_modes = ["FM", "NFM"] rf.valid_power_levels = self.POWER_LEVELS rf.valid_bands = self.valid_freq rf.valid_characters = "".join(self.CHARSET) rf.valid_name_length = 6 rf.valid_tuning_steps = list(self.TUNING_STEPS) rf.has_ctone = True rf.has_rx_dtcs = True rf.has_cross = True rf.has_bank = False rf.has_settings = True rf.memory_bounds = (0, 199) rf.can_odd_split = True return rf def get_settings(self): #freqranges = RadioSettingGroup("freqranges", "Freq ranges (read only)") basic = RadioSettingGroup("basic", "Basic Settings") group = RadioSettings(basic) rs = RadioSetting("squelch", "Squelch Level", RadioSettingValueInteger(0,9,int(self._memobj.squelch))) basic.append(rs) #print self._memobj.time_out_timer rs = RadioSetting("time_out_timer", "Time-Out Timer (mins) 0=Disable", RadioSettingValueInteger(0,9, int(self._memobj.time_out_timer))) basic.append(rs) rs = RadioSetting("priority_channel", "Priority Scan Channel", RadioSettingValueInteger(0,199,self._memobj.priority_channel)) basic.append(rs) rs = RadioSetting("busy_lock", "Busy LockOut", RadioSettingValueBoolean( not self._memobj.busy_lock)) basic.append(rs) rs = RadioSetting("keylock", "Key Lock", RadioSettingValueBoolean( not self._memobj.keylock)) basic.append(rs) #print self._memobj.vox rs = RadioSetting("vox", "VOX (level) 0=Disable", RadioSettingValueInteger(0,9,int(self._memobj.vox))) basic.append(rs) rs = RadioSetting("keybeep", "Key Beep", RadioSettingValueBoolean( not self._memobj.keybeep)) basic.append(rs) rs = RadioSetting("display", "Display Mode", RadioSettingValueList( self.display_options, self.display_options[int(self._memobj.display)])) basic.append(rs) rs = RadioSetting("step", "Tuning Step", RadioSettingValueList( self.TUNING_STEPS_STR, self.TUNING_STEPS_STR[int(self._memobj.step)])) basic.append(rs) #u8 mode; print self._memobj.end_tone rs = RadioSetting("end_tone", "Ending Tone", RadioSettingValueBoolean( not self._memobj.end_tone)) basic.append(rs) rs = RadioSetting("band_restrict", "Band Restrictions", RadioSettingValueBoolean( int(self._memobj.band_restrict) == 1)) basic.append(rs) rs = RadioSetting("txen350390", "TX Enabled on 350-390 MHz", RadioSettingValueBoolean( int(self._memobj.txen350390) == 1)) basic.append(rs) return group def set_settings(self, settings): return basic = settings[0] print basic['squelch'] print basic['squelch'].get_value() self._memobj.squelch = int(basic['squelch'].get_value()) self._memobj.time_out_timer = int(basic['time_out_timer'].get_value()) self._memobj.priority_channel = int(basic['priority_channel'].get_value()) self._memobj.vox = int(basic['vox'].get_value()) self._memobj.display = self.display_options.index(int(basic['priority_channel'].get_value())) self._memobj.step = self.TUNING_STEPS_STR.index(int(basic['vox'].get_value())) self._memobj.busy_lock = not bool(basic['busy_lock'].get_value()) self._memobj.keylock = not bool(basic['keylock'].get_value()) self._memobj.keybeep = not bool(basic['keybeep'].get_value()) self._memobj.end_tone = not bool(basic['end_tone'].get_value()) self._memobj.band_restrict = int(basic['band_restrict'].get_value()) self._memobj.txen350390 = int(basic['txen350390'].get_value()) def get_raw_memory(self, number): return repr(self._memobj.memory[number - 1]) def _get_tone(self, _mem, mem): #print _mem.tx_tone, _mem.tx_code_type #print _mem.rx_tone, _mem.rx_code_type tpol = "N" txmode = "" if int(_mem.tx_code_type) == 0: txmode = "" elif int(_mem.tx_code_type) == 1: txmode = "Tone" mem.rtone = chirp_common.TONES[int(_mem.tx_tone)] elif int(_mem.tx_code_type) == 2: txmode = "DTCS" tpol = "N" mem.dtcs = chirp_common.DTCS_CODES[int(_mem.tx_tone)] elif int(_mem.tx_code_type) == 3: txmode = "DTCS" tpol = "R" mem.dtcs = chirp_common.DTCS_CODES[int(_mem.tx_tone)] rpol = "N" rxmode = "" if int(_mem.rx_code_type) == 0: rxmode = "" elif int(_mem.rx_code_type) == 1: rxmode = "Tone" mem.ctone = chirp_common.TONES[int(_mem.rx_tone)] elif int(_mem.rx_code_type) == 2: rxmode = "DTCS" rpol = "N" mem.rx_dtcs = chirp_common.DTCS_CODES[int(_mem.rx_tone)] elif int(_mem.rx_code_type) == 3: rxmode = "DTCS" rpol = "R" mem.rx_dtcs = chirp_common.DTCS_CODES[int(_mem.rx_tone)] mem.tmode = "" if txmode == "Tone" and not rxmode: mem.tmode = "Tone" elif txmode == rxmode and txmode == "Tone" and mem.rtone == mem.ctone: mem.tmode = "TSQL" elif txmode == rxmode and txmode == "DTCS" and mem.dtcs == mem.rx_dtcs: mem.tmode = "DTCS" elif rxmode or txmode: mem.tmode = "Cross" mem.cross_mode = "%s->%s" % (txmode, rxmode) mem.dtcs_polarity = "%s%s" % (tpol, rpol) if os.getenv("CHIRP_DEBUG"): print "Got TX %s (%i %i) RX %s (%i %i)" % (txmode, mem.ctone, mem.dtcs, rxmode, mem.rtone, mem.rx_dtcs,) def get_memory(self, number): _mem = self._memobj.memory[number] _nam = self._memobj.names[number] _scanband = self._memobj.memory_scan_band[number] #print _mem #print _nam #print _scanband mem = chirp_common.Memory() mem.number = number if _mem.get_raw() == ("\xff" * 16): mem.empty = True return mem mem.freq = int(_mem.rx_freq) * 10 mem.duplex = "" mem.offset = 0 if _mem.split_pos: mem.duplex = "+" mem.offset = int(_mem.tx_freq_offset) * 10 if _mem.split_neg: mem.duplex = "-" mem.offset = int(_mem.tx_freq_offset) * 10 if not _scanband.scan: mem.skip = "S" #print mem.skip #print int(_scanband.band) if not _mem.iswide: mem.mode = "NFM" else: mem.mode = "FM" self._get_tone(_mem, mem) mem.power = self.POWER_LEVELS[int(_mem.tx_power)] mem.tuning_step = self.TUNING_STEPS[int(_mem.bandwith)] #print mem.power #print mem.tuning_step for i in _nam.name: if i == 0xFF: break #print int(i) mem.name += self.CHARSET[int(i)] mem.extra = RadioSettingGroup("Extra", "extra") rev = RadioSetting("REV", "reverse", RadioSettingValueBoolean(bool(not _mem.reverse))) rev.set_doc("Reverse") mem.extra.append(rev) scr = RadioSetting("SCR", "scramble", RadioSettingValueBoolean(bool(not _mem.scramble))) scr.set_doc("Scramble") mem.extra.append(scr) return mem def _set_tone(self, mem, _mem): if mem.tmode == "Cross": tx_mode, rx_mode = mem.cross_mode.split("->") elif mem.tmode == "Tone": tx_mode = mem.tmode rx_mode = None elif mem.tmode == "TSQL": tx_mode = "Tone" rx_mode = "Tone" else: tx_mode = rx_mode = mem.tmode if tx_mode == "Tone": _mem.tx_code_type = 1 _mem.tx_tone = chirp_common.TONES.index(mem.rtone) elif tx_mode == "DTCS": _mem.tx_code_type = 2 if mem.dtcs_polarity[0] == "R": _mem.tx_code_type = 3 _mem.tx_tone = chirp_common.DTCS_CODES.index(mem.dtcs) else: _mem.tx_code_type = 0 if rx_mode == "Tone": _mem.rx_code_type = 1 _mem.rx_tone = chirp_common.TONES.index(mem.ctone) elif rx_mode == "DTCS": _mem.rx_code_type = 2 if mem.dtcs_polarity[1] == "R": _mem.rx_code_type = 3 _mem.rx_tone = chirp_common.DTCS_CODES.index(mem.rx_dtcs) else: _mem.rx_code_type = 0 if os.getenv("CHIRP_DEBUG"): print "Set TX %s (%i %i) RX %s (%i %i)" % (txmode, mem.ctone, mem.dtcs, rxmode, mem.rtone, mem.rx_dtcs,) def set_memory(self, mem): _mem = self._memobj.memory[mem.number] _nam = self._memobj.names[mem.number] _scanband = self._memobj.memory_scan_band[mem.number] if mem.empty: wipe_memory(_mem, "\xFF") wipe_memory(_nam, "\xFF") wipe_memory(_scanband, "\xFF") return _mem.rx_freq = int(mem.freq / 10) _mem.tx_freq_offset = 0 _mem.split_pos = 0 _mem.split_neg = 0 if mem.duplex == "+": _mem.split_pos = 1 _mem.tx_freq_offset = int(mem.offset / 10) if mem.duplex == "-": _mem.split_neg = 1 _mem.tx_freq_offset = int(mem.offset / 10) _scanband.scan = 1 if mem.skip == "S": _scanband.scan = 0 for i in range(len(self.valid_freq)): band = self.valid_freq[i] if band[0]<= mem.freq and mem.freq < band[1]: _scanband.band = i print "BAND: %i" % int(_scanband.band) _mem.iswide = 1 if mem.mode == "NFM": _mem.iswide = 0 self._set_tone(mem, _mem) _mem.tx_power = self.POWER_LEVELS.index(mem.power) _mem.bandwith = self.TUNING_STEPS.index(mem.tuning_step) newName = [0xFF] * 6 for i in range(0, len(mem.name)): try: newName[i] = self.CHARSET.index(mem.name[i]) except IndexError: raise Exception("Character `%s' not supported") #print newName _nam.name = newName for setting in mem.extra: setattr(_mem, setting.get_shortname(), not setting.value) @classmethod def match_model(cls, filedata, filename): if len(filedata) == 8192: return True return False