Initial support for Yaesu FT-90R. Probably a bit buggy. #1087

diff -r 86910885e998 -r 98bdf0cae780 chirp/ft90.py
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/chirp/ft90.py Sat Aug 24 19:31:50 2013 -0500
@@ -0,0 +1,317 @@
+# Copyright 2011 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 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 <http://www.gnu.org/licenses/>.
+
+from chirp import chirp_common, memmap, directory, errors, util, yaesu_clone
+from chirp import bitwise
+import time, os, traceback
+
+CHIRP_DEBUG=True
+CMD_ACK = chr(0x06)
+
+@directory.register
+class FT90Radio(yaesu_clone.YaesuCloneModeRadio):
+    VENDOR = "Yaesu"
+    MODEL = "FT-90"
+    ID = "ft90"
+    
+    STEPS = [5, 10, 12.5, 15, 20, 25, 50]
+    MODES = ["AM", "FM", "Auto"]
+    TMODES = ["", "Tone", "TSQL", "Bell", "DTCS"]
+    TONES = [ 67.0, 69.3, 71.9, 74.4, 77.0, 79.7, 82.5,
+          85.4, 88.5, 91.5, 94.8, 97.4, 100.0, 103.5,
+          107.2, 110.9, 114.8, 118.8, 123.0, 127.3,
+          131.8, 136.5, 141.3, 146.2, 151.4, 156.7,
+          159.8, 162.2, 167.9, 173.8, 179.9, 183.5,
+          186.2, 189.9, 192.8, 196.6, 199.5, 203.5, 
+          206.5, 210.7, 218.1, 225.7, 229.1, 233.6,
+          241.8, 250.3, 254.1,
+          ]          
+    
+    POWER_LEVELS = ["Hi","Mid1","Mid2","Low"]
+    #DUPLEX = ["", "-", "+", "split", "Auto"]
+    DUPLEX = ["", "-", "+", "split"]
+     
+    _memsize = 4063
+    # block 03 (200 Bytes long) repeats 18 times; channel memories
+    _block_lengths = [ 2, 232, 24, 200, 205]
+    
+    mem_format = """
+    #seekto 0x102;
+    struct {
+      u8 mode:2,
+         isUhf1:1,
+         unknown1:2,
+         step:3;
+      u8 artsmode:2,
+         unknown2:1,
+         isUhf2:1
+         power:2,
+         shift:2;
+      u8 skip:1,
+         showname:1,
+         unknown3:1,
+         isUhfHi:1,
+         unknown4:1,
+         tmode:3;
+      u32 rxfreq;
+      u32 txfreqoffset;
+      u8 UseDefaultName:1,
+         ars:1,
+         tone:6;
+      u8 packetmode:1,
+         unknown5:1,
+         dcstone:6;
+      char name[7];
+    } memory[180];
+    """
+
+    @classmethod
+    def match_model(cls, filedata, filename):
+        return len(filedata) == cls._memsize
+
+    def get_features(self):
+        rf = chirp_common.RadioFeatures()
+        rf.has_ctone = False
+        rf.has_bank = False
+        rf.has_dtcs_polarity = False
+        rf.valid_modes = list(set(self.MODES))
+        rf.valid_tmodes = list(self.TMODES)
+        rf.valid_duplexes = list(self.DUPLEX)
+        rf.valid_tuning_steps = list(self.STEPS)
+        rf.valid_power_levels = self.POWER_LEVELS
+        rf.valid_name_length = 7
+        rf.valid_characters = chirp_common.CHARSET_ASCII
+        rf.valid_skips = ["", "S"]
+        rf.memory_bounds = (1, 180)
+        rf.valid_bands = [(100000000, 230000000), 
+            (300000000, 530000000), (810000000, 999975000)]
+
+        return rf
+
+    def _read(self, blocksize, blocknum):
+        data = self.pipe.read(blocksize+2)
+        
+        # chew echo'd ack
+        self.pipe.write(CMD_ACK)
+        time.sleep(0.02)
+        self.pipe.read(1) # chew echoed ACK from 1-wire serial
+        
+        if len(data) == blocksize+2 and data[0] == chr(blocknum):
+            checksum = yaesu_clone.YaesuChecksum(1, blocksize)
+            if checksum.get_existing(data) != checksum.get_calculated(data):
+                raise Exception("Checksum Failed [%02X<>%02X] block %02X, data len: %i" %
+                                    (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        
+    
+    def _clone_in(self):
+        # Be very patient with the radio
+        self.pipe.setTimeout(4)
+        start = time.time()
+    
+        data = ""
+        blocknum = 0
+        status = chirp_common.Status()
+        status.msg = "Cloning from radio.\nPut radio into clone mode then\npress SET to send"
+        self.status_fn(status)
+        status.max = len(self._block_lengths) + 18
+        for blocksize in self._block_lengths:
+            if blocksize == 200:
+                # repeated read of 200 block same size (memory area)
+                repeat = 18
+            else:
+                repeat = 1
+            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 
+        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
+                repeat = 18
+            else:
+                repeat = 1
+            for _i in range(0, repeat):
+                time.sleep(0.1)
+                checksum = yaesu_clone.YaesuChecksum(pos, pos+blocksize-1)
+                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:
+                    print "Block %i - will send from %i to %i byte " % \
+                        (blocknum, pos, pos + blocksize)
+                    print util.hexprint(blocknumbyte)
+                    print util.hexprint(payloadbytes)
+                    print util.hexprint(checksumbyte)
+                # send wrapped bytes
+                self.pipe.write(blocknumbyte)
+                self.pipe.write(payloadbytes)
+                self.pipe.write(checksumbyte)
+                tmp = self.pipe.read(blocksize+2)  #chew echo
+                if os.getenv("CHIRP_DEBUG") or CHIRP_DEBUG:                
+                    print "bytes echoed: "
+                    print util.hexprint(tmp)
+                # radio is slow to write/ack:
+                time.sleep(0.9) 
+                buf = self.pipe.read(1)
+                if os.getenv("CHIRP_DEBUG") or CHIRP_DEBUG:                
+                    print "ack recd:"
+                    print util.hexprint(buf)
+                if buf != CMD_ACK:
+                    raise Exception("Radio did not ack block %i" % blocknum)
+                pos += blocksize
+                blocknum += 1
+                status.cur = blocknum
+                self.status_fn(status)
+    
+        print "Clone completed in %i seconds" % (time.time() - start)
+    
+    def sync_in(self):
+        try:
+            self._mmap = self._clone_in()
+        except errors.RadioError:
+            raise
+        except Exception, e:
+            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()
+        except errors.RadioError:
+            raise
+        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      
+        mem.offset = _mem.txfreqoffset * 10
+        if not _mem.tmode < len(self.TMODES):
+            _mem.tmode = 0
+        mem.tmode = self.TMODES[_mem.tmode]
+        mem.rtone = self.TONES[_mem.tone]
+        mem.mode = self.MODES[_mem.mode]
+
+        '''
+        # ars mode note yet working...
+        # ARS mode:
+        if _mem.ars and _mem.shift == 0:
+            mem.duplex = self.DUPLEX[4] 
+        else:
+            mem.duplex = self.DUPLEX[_mem.shift]
+        '''
+        mem.duplex = self.DUPLEX[_mem.shift]
+        mem.power = self.POWER_LEVELS[_mem.power]
+        # radio has a known bug with 5khz step and squelch
+        if _mem.step == 0:
+            _mem.step = 2
+        mem.tuning_step = self.STEPS[_mem.step]
+        mem.skip = _mem.skip and "S" or ""
+        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]
+        _mem.skip = mem.skip == "S"
+        # radio has a known bug with 5khz step and dead squelch
+        if mem.tuning_step == self.STEPS[0]:
+            _mem.step = 2
+        else:
+            _mem.step = self.STEPS.index(mem.tuning_step)
+        _mem.rxfreq = mem.freq / 10
+        # vfo will unlock if not in right band?
+        if mem.freq > 300000000: 
+            # uhf
+            _mem.isUhf1 = 1
+            _mem.isUhf2 = 1
+            if mem.freq > 810000000:
+                # uhf hiband
+                _mem.isUhfHi = 1
+            else:
+                _mem.isUhfHi = 0
+        else:
+            # vhf
+            _mem.isUhf1 = 0
+            _mem.isUhf2 = 0
+            _mem.isUhfHi = 0
+        _mem.txfreqoffset = mem.offset / 10
+        _mem.tone = self.TONES.index(mem.rtone)
+        _mem.tmode = self.TMODES.index(mem.tmode)
+        _mem.mode = self.MODES.index(mem.mode)
+        '''
+        # ars not yet working
+        # ARS mode:
+        if mem.duplex == 4:
+            _mem.shift = 0
+            _mem.ars = 1
+        else:
+            _mem.shift = self.DUPLEX.index(mem.duplex)
+        '''
+        _mem.shift = self.DUPLEX.index(mem.duplex)    
+        #_mem.dtcs = chirp_common.DTCS_CODES.index(mem.dtcs)
+        if self.get_features().has_tuning_step:
+            _mem.step = self.STEPS.index(mem.tuning_step)
+        _mem.shift = self.DUPLEX.index(mem.duplex)
+        if mem.power:
+            _mem.power = self.POWER_LEVELS.index(mem.power)
+        else:
+            _mem.power = 3  # default to low power
+        _mem.name = mem.name.ljust(7)
+