# HG changeset patch # User Jens Jensen # Date 1378161540 18000 # Mon Sep 02 17:39:00 2013 -0500 # Node ID 426f6997e7ac130a44a759247a75af829ca9d937 # Parent 208026ed42fcccd1e92bd92d6bb6775db81e04d6 adding initial support for settings and special channels (pms, vfo, home) for FT-90. #1087 diff -r 208026ed42fc -r 426f6997e7ac chirp/ft90.py --- a/chirp/ft90.py Mon Aug 26 13:28:57 2013 -0500 +++ b/chirp/ft90.py Mon Sep 02 17:39:00 2013 -0500 @@ -15,9 +15,16 @@ # along with this program. If not, see . from chirp import chirp_common, bitwise, memmap, directory, errors, util, yaesu_clone -import time, os, traceback, string +from chirp.settings import RadioSetting, RadioSettingGroup, \ + RadioSettingValueInteger, RadioSettingValueList, \ + RadioSettingValueBoolean, RadioSettingValueString +import time, os, traceback, string, re -CHIRP_DEBUG=False +if os.getenv("CHIRP_DEBUG"): + CHIRP_DEBUG = True +else: + CHIRP_DEBUG = False + CMD_ACK = chr(0x06) FT90_STEPS = [5.0, 10.0, 12.5, 15.0, 20.0, 25.0, 50.0] @@ -28,71 +35,84 @@ FT90_TONES.remove(tone) FT90_POWER_LEVELS = ["Hi", "Mid1", "Mid2", "Low"] FT90_DUPLEX = ["", "-", "+", "split"] -FT90_CWID_CHARS = list(string.digits) + list(string.uppercase) +FT90_CWID_CHARS = list(string.digits) + list(string.uppercase) + list(" ") +FT90_DTMF_CHARS = list("0123456789ABCD*#") +FT90_SPECIAL = [ "vfo_vhf", "home_vhf", "vfo_uhf", "home_uhf", \ + "pms_1L", "pms_1U", "pms_2L", "pms_2U"] @directory.register class FT90Radio(yaesu_clone.YaesuCloneModeRadio): VENDOR = "Yaesu" MODEL = "FT-90" ID = "\x8E\xF6" - + _memsize = 4063 # block 03 (200 Bytes long) repeats 18 times; channel memories _block_lengths = [ 2, 232, 24, 200, 205] mem_format = """ - #seekto 0x22; - struct { - u8 dtmf_active; - u8 dtmf1_len; - u8 dtmf2_len; - u8 dtmf3_len; - u8 dtmf4_len; - u8 dtmf5_len; - u8 dtmf6_len; - u8 dtmf7_len; - u8 dtmf8_len; - bbcd dtmf1[8]; - bbcd dtmf2[8]; - bbcd dtmf3[8]; - bbcd dtmf4[8]; - bbcd dtmf5[8]; - bbcd dtmf6[8]; - bbcd dtmf7[8]; - bbcd dtmf8[8]; - char cwid[7]; - u8 unk1; - u8 unk2:2, - beep:1, - unk3:1, - rfsqlvl:4; - u8 cwid_en:1, - unk4:3, - txnarrow:1, - dtmfspeed:1, - pttlock:2; - u8 dtmftxdelay:3, - fancontrol:2, - unk5:3; - u8 dimmer:3, - unk6:1, - lcdcontrast:4; - u8 tot; - u8 unk8:1, - ars:1, - lock:1, - txpwrsave:1, - apo:4; - u8 key_rt; - u8 key_lt; - u8 key_p1; - u8 key_p2; - u8 key_acc; - char demomsg1[32]; - char demomsg2[32]; - - } settings; - + #seekto 0x22; + struct { + u8 dtmf_active; + u8 dtmf1_len; + u8 dtmf2_len; + u8 dtmf3_len; + u8 dtmf4_len; + u8 dtmf5_len; + u8 dtmf6_len; + u8 dtmf7_len; + u8 dtmf8_len; + u8 dtmf1[8]; + u8 dtmf2[8]; + u8 dtmf3[8]; + u8 dtmf4[8]; + u8 dtmf5[8]; + u8 dtmf6[8]; + u8 dtmf7[8]; + u8 dtmf8[8]; + char cwid[7]; + u8 unk1; + u8 scan1:2, + beep:1, + unk3:3, + rfsqlvl:2; + u8 unk4:2, + scan2:1, + cwid_en:1, + txnarrow:1, + dtmfspeed:1, + pttlock:2; + u8 dtmftxdelay:3, + fancontrol:2, + unk5:3; + u8 dimmer:3, + unk6:1, + lcdcontrast:4; + u8 dcsmode:2, + unk16:2, + tot:4; + u8 unk14; + u8 unk8:1, + ars:1, + lock:1, + txpwrsave:1, + apo:4; + u8 unk15; + u8 unk9:4, + key_lt:4; + u8 unk10:4, + key_rt:4; + u8 unk11:4, + key_p1:4; + u8 unk12:4, + key_p2:4; + u8 unk13:4, + key_acc:4; + char demomsg1[32]; + char demomsg2[32]; + + } settings; + struct mem_struct { u8 mode:2, isUhf1:1, @@ -119,29 +139,30 @@ dcstone:6; char name[7]; }; - - #seekto 0x86; - struct mem_struct vfo_v; - struct mem_struct call_v; - struct mem_struct vfo_u; - struct mem_struct call_u; - - #seekto 0x102; + + #seekto 0x86; + struct mem_struct vfo_vhf; + struct mem_struct home_vhf; + struct mem_struct vfo_uhf; + struct mem_struct home_uhf; + + #seekto 0x102; struct mem_struct memory[180]; - - #seekto 0xf12; - struct mem_struct pms_1L; - struct mem_struct pms_1U; - struct mem_struct pms_2L; - struct mem_struct pms_2U; + + #seekto 0xf12; + struct mem_struct pms_1L; + struct mem_struct pms_1U; + struct mem_struct pms_2L; + struct mem_struct pms_2U; """ - + @classmethod def match_model(cls, filedata, filename): return len(filedata) == cls._memsize - + def get_features(self): rf = chirp_common.RadioFeatures() + rf.has_settings = True rf.has_ctone = False rf.has_bank = False rf.has_dtcs_polarity = False @@ -154,12 +175,13 @@ rf.valid_name_length = 7 rf.valid_characters = chirp_common.CHARSET_ASCII rf.valid_skips = ["", "S"] + rf.valid_special_chans = FT90_SPECIAL rf.memory_bounds = (1, 180) - rf.valid_bands = [(100000000, 230000000), + rf.valid_bands = [(100000000, 230000000), (300000000, 530000000), (810000000, 999975000)] - + return rf - + def _read(self, blocksize, blocknum): data = self.pipe.read(blocksize+2) @@ -175,18 +197,18 @@ (checksum.get_existing(data), checksum.get_calculated(data), blocknum, len(data) )) data = data[1:blocksize+1] # Chew blocknum and checksum - + else: raise Exception("Unable to read blocknum %02X expected blocksize %i got %i." % (blocknum, blocksize+2, len(data))) - - return data + + return data def _clone_in(self): # Be very patient with the radio self.pipe.setTimeout(4) start = time.time() - + data = "" blocknum = 0 status = chirp_common.Status() @@ -199,35 +221,35 @@ repeat = 18 else: repeat = 1 - for _i in range(0, repeat): + for _i in range(0, repeat): data += self._read(blocksize, blocknum) - + blocknum += 1 status.cur = blocknum self.status_fn(status) - + status.msg = "Clone completed." self.status_fn(status) - + print "Clone completed in %i seconds, blocks read: %i" % (time.time() - start, blocknum) - + return memmap.MemoryMap(data) def _clone_out(self): delay = 0.2 start = time.time() - + blocknum = 0 pos = 0 status = chirp_common.Status() status.msg = "Cloning to radio.\nPut radio into clone mode and press DISP/SS\n to start receive within 3 secs..." self.status_fn(status) - # radio likes to have port open + # radio likes to have port open self.pipe.open() time.sleep(3) status.max = len(self._block_lengths) + 18 - + for blocksize in self._block_lengths: if blocksize == 200: # repeat channel blocks @@ -240,7 +262,7 @@ blocknumbyte = chr(blocknum) payloadbytes = self.get_mmap()[pos:pos+blocksize] checksumbyte = chr(checksum.get_calculated(self.get_mmap())) - if os.getenv("CHIRP_DEBUG") or CHIRP_DEBUG: + if CHIRP_DEBUG: print "Block %i - will send from %i to %i byte " % \ (blocknum, pos, pos + blocksize) print util.hexprint(blocknumbyte) @@ -251,13 +273,13 @@ self.pipe.write(payloadbytes) self.pipe.write(checksumbyte) tmp = self.pipe.read(blocksize+2) #chew echo - if os.getenv("CHIRP_DEBUG") or CHIRP_DEBUG: + if CHIRP_DEBUG: print "bytes echoed: " print util.hexprint(tmp) # radio is slow to write/ack: - time.sleep(0.9) + time.sleep(0.9) buf = self.pipe.read(1) - if os.getenv("CHIRP_DEBUG") or CHIRP_DEBUG: + if CHIRP_DEBUG: print "ack recd:" print util.hexprint(buf) if buf != CMD_ACK: @@ -266,7 +288,7 @@ blocknum += 1 status.cur = blocknum self.status_fn(status) - + print "Clone completed in %i seconds" % (time.time() - start) def sync_in(self): @@ -278,7 +300,7 @@ trace = traceback.format_exc() raise errors.RadioError("Failed to communicate with radio: %s" % trace) self.process_mmap() - + def sync_out(self): try: self._clone_out() @@ -287,16 +309,22 @@ except Exception, e: trace = traceback.format_exc() raise errors.RadioError("Failed to communicate with radio: %s" % trace) - + def process_mmap(self): self._memobj = bitwise.parse(self.mem_format, self._mmap) - + def get_memory(self, number): - _mem = self._memobj.memory[number-1] - mem = chirp_common.Memory() - mem.number = number - mem.freq = _mem.rxfreq * 10 + if isinstance(number, str): + # special channel + _mem = getattr(self._memobj, number) + mem.number = - len(FT90_SPECIAL) + FT90_SPECIAL.index(number) + mem.extd_number = number + else: + # regular memory + _mem = self._memobj.memory[number-1] + mem.number = number + mem.freq = _mem.rxfreq * 10 mem.offset = _mem.txfreqoffset * 10 if not _mem.tmode < len(FT90_TMODES): _mem.tmode = 0 @@ -307,19 +335,25 @@ mem.duplex = FT90_DUPLEX[_mem.shift] mem.power = FT90_POWER_LEVELS[_mem.power] # radio has a known bug with 5khz step and squelch - if _mem.step == 0: + if _mem.step == 0 or _mem.step > len(FT90_STEPS)-1: _mem.step = 2 mem.tuning_step = FT90_STEPS[_mem.step] mem.skip = _mem.skip and "S" or "" - mem.name = _mem.name + if not all(char in chirp_common.CHARSET_ASCII for char in str(_mem.name)): + # dont display blank/junk name + mem.name = "" + else: + mem.name = _mem.name return mem - + def get_raw_memory(self, number): return repr(self._memobj.memory[number-1]) def set_memory(self, mem): - - _mem = self._memobj.memory[mem.number - 1] + if mem.number < 0: # special channels + _mem = getattr(self._memobj, mem.extd_number) + else: + _mem = self._memobj.memory[mem.number - 1] _mem.skip = mem.skip == "S" # radio has a known bug with 5khz step and dead squelch if not mem.tuning_step or mem.tuning_step == FT90_STEPS[0]: @@ -328,7 +362,7 @@ _mem.step = FT90_STEPS.index(mem.tuning_step) _mem.rxfreq = mem.freq / 10 # vfo will unlock if not in right band? - if mem.freq > 300000000: + if mem.freq > 300000000: # uhf _mem.isUhf1 = 1 _mem.isUhf2 = 1 @@ -346,7 +380,7 @@ _mem.tone = FT90_TONES.index(mem.rtone) _mem.tmode = FT90_TMODES.index(mem.tmode) _mem.mode = FT90_MODES.index(mem.mode) - _mem.shift = FT90_DUPLEX.index(mem.duplex) + _mem.shift = FT90_DUPLEX.index(mem.duplex) _mem.dcstone = chirp_common.DTCS_CODES.index(mem.dtcs) _mem.step = FT90_STEPS.index(mem.tuning_step) _mem.shift = FT90_DUPLEX.index(mem.duplex) @@ -354,6 +388,201 @@ _mem.power = FT90_POWER_LEVELS.index(mem.power) else: _mem.power = 3 # default to low power - _mem.name = mem.name.ljust(7) + if (len(mem.name) == 0): + _mem.name = bytearray.fromhex("80ffffffffffff") + _mem.showname = 0 + else: + _mem.name = str(mem.name).ljust(7) + _mem.showname = 1 + _mem.UseDefaultName = 0 + + def _decode_cwid(self, cwidarr): + cwid = "" + if CHIRP_DEBUG: + print "@ +_decode_cwid:" + for byte in cwidarr.get_value(): + char = int(byte) + if CHIRP_DEBUG: + print char + # bitwise wraps in quotes! get rid of those + if char < len(FT90_CWID_CHARS): + cwid += FT90_CWID_CHARS[char] + return cwid + + def _encode_cwid(self, cwidarr): + cwid = "" + if CHIRP_DEBUG: + print "@ _encode_cwid:" + for char in cwidarr.get_value(): + cwid += chr(FT90_CWID_CHARS.index(char)) + if CHIRP_DEBUG: + print cwid + return cwid + def _bbcd2dtmf(self, bcdarr, strlen = 16): + # doing bbcd, but with support for ABCD*# + if CHIRP_DEBUG: + print bcdarr.get_value() + string = ''.join("%02X" % b for b in bcdarr) + if CHIRP_DEBUG: + print "@_bbcd2dtmf, received: %s" % string + string = string.replace('E','*').replace('F','#') + if strlen <= 16: + string = string[:strlen] + return string + def _dtmf2bbcd(self, dtmf): + dtmfstr = dtmf.get_value() + dtmfstr = dtmfstr.replace('*', 'E').replace('#', 'F') + dtmfstr = str.ljust(dtmfstr.strip(), 16, "0" ) + bcdarr = list(bytearray.fromhex(dtmfstr)) + if CHIRP_DEBUG: + print "@_dtmf2bbcd, sending: %s" % bcdarr + return bcdarr + + def get_settings(self): + _settings = self._memobj.settings + basic = RadioSettingGroup("basic", "Basic") + autodial = RadioSettingGroup("autodial", "AutoDial") + keymaps = RadioSettingGroup("keymaps", "KeyMaps") + top = RadioSettingGroup("top", "All Settings", basic, keymaps, autodial) + + rs = RadioSetting("beep", "Beep", + RadioSettingValueBoolean(_settings.beep)) + basic.append(rs) + rs = RadioSetting("lock", "Lock", + RadioSettingValueBoolean(_settings.lock)) + basic.append(rs) + rs = RadioSetting("ars", "Auto Repeater Shift", + RadioSettingValueBoolean(_settings.ars)) + basic.append(rs) + rs = RadioSetting("txpwrsave", "TX Power Save", + RadioSettingValueBoolean(_settings.txpwrsave)) + basic.append(rs) + rs = RadioSetting("txnarrow", "TX Narrow", + RadioSettingValueBoolean(_settings.txnarrow)) + basic.append(rs) + options = ["Off", "S-3", "S-5", "S-Full"] + rs = RadioSetting("rfsqlvl", "RF Squelch Level", + RadioSettingValueList(options, + options[_settings.rfsqlvl])) + basic.append(rs) + options = ["Off", "Band A", "Band B", "Both"] + rs = RadioSetting("pttlock", "PTT Lock", + RadioSettingValueList(options, + options[_settings.pttlock])) + basic.append(rs) + + rs = RadioSetting("cwid_en", "CWID Enable", + RadioSettingValueBoolean(_settings.cwid_en)) + basic.append(rs) + + cwid = RadioSettingValueString(0, 7, self._decode_cwid(_settings.cwid)) + cwid.set_charset(FT90_CWID_CHARS) + rs = RadioSetting("cwid", "CWID", cwid) + basic.append(rs) + + options = ["OFF"] + map(str, range(1, 12+1)) + rs = RadioSetting("apo", "APO time (hrs)", + RadioSettingValueList(options, + options[_settings.apo])) + basic.append(rs) + + options = ["Off"] + map(str, range(1, 60+1)) + rs = RadioSetting("tot", "Time Out Timer (mins)", + RadioSettingValueList(options,options[_settings.tot])) + basic.append(rs) + + options = ["off", "Auto/TX", "Auto", "TX"] + rs = RadioSetting("fancontrol", "Fan Control", + RadioSettingValueList(options,options[_settings.fancontrol])) + basic.append(rs) + + keyopts = ["Scan Up", "Scan Down", "Repeater", "Reverse", "Tone Burst", + "Tx Power", "Home Ch", "VFO/MR", "Tone", "Priority"] + rs = RadioSetting("key_lt", "Left Key", + RadioSettingValueList(keyopts,keyopts[_settings.key_lt])) + keymaps.append(rs) + rs = RadioSetting("key_rt", "Right Key", + RadioSettingValueList(keyopts,keyopts[_settings.key_rt])) + keymaps.append(rs) + rs = RadioSetting("key_p1", "P1 Key", + RadioSettingValueList(keyopts,keyopts[_settings.key_p1])) + keymaps.append(rs) + rs = RadioSetting("key_p2", "P2 Key", + RadioSettingValueList(keyopts,keyopts[_settings.key_p2])) + keymaps.append(rs) + rs = RadioSetting("key_acc", "ACC Key", + RadioSettingValueList(keyopts,keyopts[_settings.key_acc])) + keymaps.append(rs) + + options = map(str, range(0,12+1)) + rs = RadioSetting("lcdcontrast", "LCD Contrast", + RadioSettingValueList(options,options[_settings.lcdcontrast])) + basic.append(rs) + + options = ["off", "d4", "d3", "d2", "d1"] + rs = RadioSetting("dimmer", "Dimmer", + RadioSettingValueList(options,options[_settings.dimmer])) + basic.append(rs) + + options = ["TRX Normal", "RX Reverse", "TX Reverse", "TRX Reverse"] + rs = RadioSetting("dcsmode", "DCS Mode", + RadioSettingValueList(options,options[_settings.dcsmode])) + basic.append(rs) + + options = ["50 ms", "100 ms"] + rs = RadioSetting("dtmfspeed", "DTMF Speed", + RadioSettingValueList(options,options[_settings.dtmfspeed])) + autodial.append(rs) + + options = ["50 ms", "250 ms", "450 ms", "750 ms", "1 sec"] + rs = RadioSetting("dtmftxdelay", "DTMF TX Delay", + RadioSettingValueList(options,options[_settings.dtmftxdelay])) + autodial.append(rs) + + options = map(str,range(1,8+1)) + rs = RadioSetting("dtmf_active", "DTMF Active", + RadioSettingValueList(options,options[_settings.dtmf_active])) + autodial.append(rs) + + # setup 8 dtmf autodial entries + for i in map(str, range(1,9)): + objname = "dtmf" + i + dtmfsetting = getattr(_settings, objname) + dtmflen = getattr(_settings, objname + "_len") + dtmfstr = self._bbcd2dtmf(dtmfsetting, dtmflen) + dtmf = RadioSettingValueString(0, 16, dtmfstr) + dtmf.set_charset(FT90_DTMF_CHARS + list(" ")) + rs = RadioSetting(objname, objname.upper(), dtmf) + autodial.append(rs) + + return top + + def set_settings(self, uisettings): + _settings = self._memobj.settings + for element in uisettings: + if not isinstance(element, RadioSetting): + self.set_settings(element) + continue + if not element.changed(): + continue + try: + setting = element.get_name() + oldval = getattr(_settings, setting) + newval = element.value + if setting == "cwid": + newval = self._encode_cwid(newval) + if re.match('dtmf\d', setting): + # set dtmf length field and then get bcd dtmf + dtmfstrlen = len(str(newval).strip()) + setattr(_settings, setting + "_len", dtmfstrlen) + newval = self._dtmf2bbcd(newval) + if CHIRP_DEBUG: + print "Setting %s(%s) <= %s" % (setting, + oldval, newval) + setattr(_settings, setting, newval) + except Exception, e: + print element.get_name() + raise +